From 3890639ddc0d2d02ac9d406824f7b874278dc18a Mon Sep 17 00:00:00 2001 From: az7627 Date: Tue, 25 Aug 2026 22:41:56 +0800 Subject: [PATCH 01/13] feat(spigot): add Spigot module with Item-NBT-API support and dynamic fuel registry --- gradlew | 0 settings.gradle.kts | 3 +- spigot/README.md | 134 ++ spigot/build.gradle.kts | 74 + .../gradle/wrapper/gradle-wrapper.properties | 7 + .../github/eat_ram/fuream/CollectionUtil.java | 131 ++ .../io/github/eat_ram/fuream/FureamMain.java | 184 ++ .../eat_ram/fuream/FureamWorldConfigImpl.java | 1560 +++++++++++++++++ .../eat_ram/fuream/data/FureamData.java | 11 + .../eat_ram/fuream/data/FureamDataHolder.java | 13 + .../fuream/data/FureamFurnaceData.java | 141 ++ .../data/FurnaceFureamDataRegistry.java | 23 + .../fuream/hook/BlockBreakListener.java | 86 + .../eat_ram/fuream/hook/ChunkListener.java | 56 + .../eat_ram/fuream/hook/FurnaceManager.java | 345 ++++ .../fuream/hook/FurnaceOpenListener.java | 58 + .../eat_ram/fuream/hook/GuiListener.java | 52 + .../eat_ram/fuream/hook/HopperListener.java | 54 + .../eat_ram/fuream/logic/FuelTable.java | 190 ++ .../fuream/logic/FureamFurnaceEngine.java | 350 ++++ .../fuream/logic/FureamFurnaceLogic.java | 146 ++ .../fuream/screen/FureamFunctionalArea.java | 31 + .../fuream/screen/FureamScreenHandler.java | 375 ++++ .../fuream/screen/FureamScreenInventory.java | 373 ++++ .../RecipeSettingAndXpFunctionalArea.java | 242 +++ .../eat_ram/fuream/util/FurnacePos.java | 47 + .../eat_ram/fuream/util/KeyableItemStack.java | 51 + .../github/eat_ram/fuream/util/NbtUtil.java | 85 + spigot/src/main/resources/plugin.yml | 15 + .../eat_ram/fuream/CollectionUtilTest.java | 47 + .../io/github/eat_ram/fuream/ConfigTest.java | 27 + .../eat_ram/fuream/HopperAndDataTest.java | 80 + 32 files changed, 4990 insertions(+), 1 deletion(-) mode change 100755 => 100644 gradlew create mode 100644 spigot/README.md create mode 100644 spigot/build.gradle.kts create mode 100644 spigot/gradle/wrapper/gradle-wrapper.properties create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/data/FureamDataHolder.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamFunctionalArea.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java create mode 100644 spigot/src/main/resources/plugin.yml create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/HopperAndDataTest.java diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/settings.gradle.kts b/settings.gradle.kts index f8eff80..0cf5a37 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,4 +36,5 @@ include("api") include("1_19_4f") include("af2023f") include("1_20f") -include("1_20_1f") \ No newline at end of file +include("1_20_1f") +include("spigot") diff --git a/spigot/README.md b/spigot/README.md new file mode 100644 index 0000000..ab20853 --- /dev/null +++ b/spigot/README.md @@ -0,0 +1,134 @@ +# Fuream — Spigot/Paper 移植版 + +Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一")原为 **Fabric 1.20.1 服务端模组**(`/tmp/workspace/Fuream`)。本工程是它的移植:引擎 / GUI / 漏斗 / 配置全部用 **纯 Bukkit API** 实现(无手写 NMS、无手写反射),持久化走 **de.tr7zw Item-NBT-API 库**,写入方块实体**真实 NBT**,NBT 路径与原 Fabric 实现**逐字节一致**。 + +## 功能(与原模组 1:1) + +- **熔炉格子扩容**:每座熔炉拥有可配置的虚似行列(默认 9 输入 / 9 燃料 / 9 输出),实际 27+ 格空间。玩家打开的是自定义 9x6 玻璃面板 GUI(燃料条 / 进度条 / 边框 / 翻页)。 +- **经验累积**:每次合成把配方经验累积到熔炉上,不自动发放;点击 GUI 第 6 行第 5 格(绿色经验指示器)或拆炉时作为经验球发放。 +- **配方冲突解决**:把原料放进 GUI 第 6 行第 1 格(配方设置槽),用 46/47 两个按钮在冲突配方之间切换,结果预览在第 48 格;选择按原料持久化(`overriddenRecipes`)。 +- **漏斗/自动化**:漏斗按列交互——上方喂输入、侧方喂燃料、下方抽取产出(走香草槽作为"通道"),空桶余物会被收回。 +- **每世界配置**:`<世界文件夹>/fuream.json`,字段/语义/默认惰性行为与原模组完全一致。 +- **破坏掉落**:拆炉掉落全部虚似行物品 + 经验。 + +## 兼容性与依赖 + +- Spigot 1.20.1 与 Paper 1.20.1(`paper` 内核为超集,直接可用)。 +- 目标字节码 Java 17(`options.release = 17`),可用 JDK 17+ 编译/运行。 +- **运行时依赖:NBT-API 插件(de.tr7zw Item-NBT-API ≥ 2.13,包名 `de.tr7zw.changeme.nbtapi`)**,需放入 `plugins/`。本插件 `softdepend: [NBTAPI]`,缺失时自动禁用并给出提示。其余(Bukkit API / Gson)由服务端自带。 + +## NBT 路径(与原 Fabric 实现一致) + +数据存于方块实体(熔炉 TileEntity)的真实 NBT,不用 PDC: + +``` +(Furnace BlockEntity NBT 根) +├─ FureamData 复合标签(仅当 data.hasAny() 时写) +│ ├─ Inputs List { 物品NBT…, Slot:int } +│ ├─ Fuels List { 物品NBT…, Slot:int } +│ ├─ Outputs List { 物品NBT…, Slot:int } +│ ├─ RecipeOverridingInput Compound 物品NBT(空物品时为 {}) +│ ├─ OverriddenRecipes List { 物品NBT…, RecipeId:string } +│ └─ Xp Float +├─ BurnTime / CookTime / CookTimeTotal Int(香草 BE 根部字段,与原模组 +│ ——由香草引擎写入——同路径) +└─ Items (香草熔炉原 3 格物品栏,作自动化通道) +``` + +- `FureamData` 复合标签、内部各键名、item `Slot`/`RecipeId` 键、`Xp` 类型均为原 Fabric `FureamFurnaceData.writeNbt/readNbt` 的原样结构。 +- `BurnTotal`(相当于香草 `fuelTime`)按香草行为**不持久化**(重载后重新点燃)。 + +## 更新日志 — Fuream-update1(跟随上游) + +已按 `Fuream-update1`(新版 Fabric 实现)跟进移植的功能: + +- **香草物品迁移**:熔炉无可用的 `FureamData` 标签时(升级前/被接管前是普通香草熔炉),把其香草 3 格物品栏并入虚似行(槽0→输入、槽1→燃料、槽2→输出),并清空通道防重复(对应新版 `AbstractFurnaceBlockEntityMixin.readNbt`;实现在 `FureamDataCodec.migrateVanillaLane`,由 `FurnaceManager` 注册时调用)。 +- **清空语义**:清除虚似行时同步清空配方设置输入(对应新版 `clear()`;`FureamFurnaceData.clearRows()`,拆炉时使用)。 +- **tick 头部保证虚似行可用**:引擎每 tick 在熔炼前 `data.ensureRows(...)` 已覆盖(对应新版 `extendInventories` 提前到 `tick` HEAD;逻辑不变,无需另行移植)。 + +上游其余改动均为文档/空白/未用 import 清理,无行为差异。 + +## 更新日志 — Fuream-update2(跟随上游) + +- **可配置 GUI 外观 + i18n**(核心功能):`FureamWorldConfig` 扩展”外观“(10 个物品 id,每类型)与“i18n”(20 条标题/提示,含 `%d`/`%.1f` 占位符);`fuream.json` 新增 30 个 `gui_*` 键,支持单值作用于所有类型或按类型对象。默认值与 Fabric 新实现一致(含燃料条提示文本由 “Remaining:” 改为 “Available: %d / %d”)。 + - 移植:`api/FureamWorldConfig`(默认值 + 映射)、`api/WorldConfigLoader`(两种形式的解析)、`screen/FurnaceGui`(按配置解析物品 id → `Registry.MATERIAL`,失败回退默认;全部标题/提示经 `String.format`)。 +- **配置类重构(对齐上游)**:Fabric 把配置创建/解析迁到新类 `FureamWorldConfigImpl`(替代 `FureamMain.newWorldConfig/readWorldConfig`);Spigot 侧本身即单一具体配置类,语义等价(默认值/解析行为不变)。 +- **`FureamScreenInventory`**:取消静态玻璃片常量,改为每 GUI 从配置解析;XP 显示改用浮点缓存比较。Spigot 侧对应 `FurnaceGui` 同步改造。 +- **`FureamScreenHandler.quickMove`**:改为持有原物品引用并返回移动前拷贝 —— 仅返回值语义差异,Spigot 侧为事件式快移,无需移植。 +- 新增 `api/package-info.java`(文档)。 + +## 配置 + +每个世界的 `world 文件夹/fuream.json`(与原模组格式一致): + +```json +{ + "input_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, + "fuel_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, + "output_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, + "enabled_furnace_types": ["FURNACE", "SMOKER", "BLAST_FURNACE"] +} +``` + +- 文件缺失 → 全部惰性(不改任何熔炉,香草行为 100% 保留)。 +- 槽位计数只接受正数;未知类型名被忽略;数量超过 9 自动翻页。 +- **GUI 外观与 i18n(Fuream-update2)**:`gui_*` 键可整体配置每类熔炉的装饰物品与文案(边框、燃料条左右、进度条完成/剩余、上一/下一配方按钮、经验指示器、上一/下一页按钮;标题/提示含 `%d`、`%.1f` 占位符)。每个键接受两种形式: + - 单值:`"gui_border_item_id": "minecraft:black_stained_glass_pane"`(作用于所有类型) + - 按类型对象:`"gui_xp_indicator_item_id": {"FURNACE": "minecraft:emerald"}`(缺省类型保持默认) + - 非法物品 id / 未知类型名被忽略并回退默认;默认值与 Fabric 实现完全一致(完整示例见 `docs/fuream.example.json`)。 +- 运行中改配置用 `/fuream reload`(权限 `fuream.reload`)。 +- `/fuream status` 查看各世界启用情况。 + +## 构建 + +```bash +cd FureamSpigot +./gradlew build # 产物: build/libs/Fuream-1.0.0.jar (+ sources jar) +./gradlew test # 单元测试(引擎/配置/布局;NBT 持久化需实机验证) +``` + +构建使用工程内 wrapper(Gradle 8.7,`gradle.properties` 指定 JDK 21 路径;若你的 JDK 21 路径不同请修改 `org.gradle.java.home`,或用系统 JDK 17+ 移除该行)。NBT-API 通过 `repo.codemc.io` 下载(`compileOnly`)。 + +## 安装 / 实机验收 + +1. 安装 NBT-API 插件(≥ 2.13)到 `plugins/`。 +2. 把 `Fuream-1.0.0.jar` 放入 `plugins/`。 +3. 启动 1.20.1 Spigot/Paper,在默认世界根目录放入 `fuream.json`(见上)。 +4. `/fuream status` 应显示已启用的类型与槽位数。 + +验收清单见 `docs/acceptance.md`。 + +## 与原 Fabric 工程的文件对照 + +| Fabric(Fuream) | Spigot 移植 | +| --- | --- | +| `FureamMain` | `FureamPlugin` + `hook/FurnaceManager` | +| `api/FureamWorldConfig` / `WorldConfigLoader` | 同(Gson 解析 `fuream.json`,语义逐条一致) | +| `data/FureamFurnaceData` | 同(结构/字段一致) | +| `data/FureamFurnaceData.writeNbt/readNbt`(存于 BE 的 `FureamData` 标签) | `data/FureamDataCodec`(经 de.tr7zw NBT-API 写入真实 BE NBT,路径逐字节一致) | +| `mixin` 中 `FureamData` NBT 持久化注入(`readNbt`/`writeNbt` @Inject) | `FureamDataCodec.read/write` + `FurnaceManager` 每 tick 或变更时的读写 | +| `logic/FureamFurnaceLogic`(引擎部分) | `logic/FureamFurnaceEngine`(自研引擎,注:原模组把引擎注释保留为规范,此处按其复刻) | +| `logic/FureamFurnaceLogic`(燃料/配方) | `logic/FuelTable`(香草 1.20.1 燃料表,自字节码逐条导出)+ `logic/BukkitCookingRecipes` | +| `screen/FureamScreenHandler` / `FureamScreenInventory` | `screen/FurnaceGuiLayout` + `screen/FurnaceGui` + `hook/GuiListener`(Bukkit 事件版) | +| `util/MaskedInventory` / `FurnaceForHopperInventory` | 不需要(引擎直接读写虚似行;漏斗走 `hook/HopperListener`) | +| mixin(`AbstractFurnaceBlockEntity` / `HopperBlockEntity` / `AbstractFurnaceBlock` / `MinecraftServer`) | `hook/*` 监听器 + 每 tick 调度 | + +## 已知取舍 + +- GUI 双击拾取(Double-Click 汇总)、创造模式中键取整格在自定义视图上不生效(按原模组规则已被取消)。 +- 漏斗只做整格投递拦截(避免部分移动的记账复杂度);少量不匹配物品会留在漏斗内,符合香草拒绝行为。 + +## 目录结构 + +``` +src/main/java/io/github/eat_ram/fuream/ + api/ FurnaceType, FureamWorldConfig, WorldConfigLoader + data/ FureamFurnaceData, FureamDataCodec (PDC 持久化) + logic/ FuelTable, CookingRecipeQuery(+Bukkit), FureamFurnaceEngine + screen/ FurnaceGuiLayout, FurnaceGui + hook/ FurnaceManager, GuiListener, FurnaceOpenListener, HopperListener, LifecycleListener + util/ StackUtil, ItemStackIo(+Bukkit), KeyableItemStack, DefaultableItemList, FurnacePos + FureamPlugin.java +src/main/resources/plugin.yml +src/test/java/... JUnit 5 测试(无服务端依赖,全部离线可跑) +``` diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts new file mode 100644 index 0000000..adc1ed6 --- /dev/null +++ b/spigot/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + id("xyz.wagyourtail.jvmdowngrader") version "1.2.2" + `java` +} + +group = "io.github.eat-ram.fuream-spigot" +version = "0.0.1" +evaluationDependsOn(":api") +jvmdg.downgradeTo = JavaVersion.VERSION_1_8 + +base { + archivesName = "fuream" +} + +repositories { + mavenCentral() + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") { + name = "spigotmc-repo" + } + maven("https://oss.sonatype.org/content/groups/public/") { + name = "sonatype" + } + maven("https://repo.codemc.io/repository/maven-public/") { + name = "CodeMC" + } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") + compileOnly("de.tr7zw:item-nbt-api:2.14.1") + compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.1") + compileOnly("org.jetbrains:annotations:26.0.2") + compileOnly(project(":api")) { + isTransitive = false + } + + testImplementation("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") + testImplementation("de.tr7zw:item-nbt-api-plugin:2.14.1") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testImplementation(project(":api")) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} + +val targetJavaVersion = 17 +java { + val javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + withSourcesJar() +} + +tasks.withType().configureEach { + options.encoding = "UTF-8" + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible) { + options.release.set(targetJavaVersion) + } +} + +tasks.jar { + from(rootProject.file("LICENSE")) { + into("META-INF") + } + from(zipTree(project(":api").tasks.jar.get().archiveFile)) { + include("**/*.class") + } +} diff --git a/spigot/gradle/wrapper/gradle-wrapper.properties b/spigot/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4e7fa0b --- /dev/null +++ b/spigot/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java new file mode 100644 index 0000000..ca0f259 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java @@ -0,0 +1,131 @@ +package io.github.eat_ram.fuream; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Range; + +public abstract class CollectionUtil { + @Contract(value = "_, _ -> new", pure = true) + public static ArrayList newDefaultedArrayList( + E defaultValue, + @Range(from = 0, to = Integer.MAX_VALUE) int initialSize + ) { + ArrayList al = new ArrayList<>() { + @SuppressWarnings("unchecked") + private E makeDefault() { + if (defaultValue instanceof org.bukkit.inventory.ItemStack) { + return (E) ((org.bukkit.inventory.ItemStack) defaultValue).clone(); + } + return defaultValue; + } + + @Override + @Contract(pure = true) + public E get(int index) { + return index < this.size() ? super.get(index) : makeDefault(); + } + + @Override + public E set(int index, E element) { + int size = this.size(); + if (index < size) { + return super.set(index, element); + } + this.ensureCapacity(index + 1); + for (; size < index; ++size) { + this.add(makeDefault()); + } + this.add(element); + return makeDefault(); + } + }; + if (initialSize > 0) { + al.set(initialSize - 1, defaultValue instanceof org.bukkit.inventory.ItemStack ? (E) ((org.bukkit.inventory.ItemStack) defaultValue).clone() : defaultValue); + } + return al; + } + + @Contract(value = "_, _, _ -> new", pure = true) + public static ListIterator + listIterator(List list, int size, int index) { + return new ListIterator<>() { + public final boolean isListIterator = index >= 0; + private int cursor = index < 0 ? 0 : index; + + @Contract(pure = true) + public boolean hasNext() { + return this.cursor != size; + } + + public E next() { + try { + int i = this.cursor; + E next = list.get(i); + this.cursor = i + 1; + return next; + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + @Contract("-> fail") + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasPrevious() { + if (!this.isListIterator) { + throw new UnsupportedOperationException(); + } + return this.cursor != 0; + } + + public E previous() { + if (!this.isListIterator) { + throw new UnsupportedOperationException(); + } + try { + int i = this.cursor - 1; + E previous = list.get(i); + this.cursor = i; + return previous; + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + public int nextIndex() { + if (!this.isListIterator) { + throw new UnsupportedOperationException(); + } + return this.cursor; + } + + public int previousIndex() { + if (!this.isListIterator) { + throw new UnsupportedOperationException(); + } + return this.cursor - 1; + } + + @Contract("_ -> fail") + public void set(E e) { + throw new UnsupportedOperationException(); + } + + @Contract("_ -> fail") + public void add(E e) { + throw new UnsupportedOperationException(); + } + }; + } + + @Contract("-> fail") + private CollectionUtil() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java new file mode 100644 index 0000000..004ac08 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -0,0 +1,184 @@ +package io.github.eat_ram.fuream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; + +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.hook.BlockBreakListener; +import io.github.eat_ram.fuream.hook.ChunkListener; +import io.github.eat_ram.fuream.hook.FurnaceManager; +import io.github.eat_ram.fuream.hook.FurnaceOpenListener; +import io.github.eat_ram.fuream.hook.GuiListener; +import io.github.eat_ram.fuream.hook.HopperListener; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlastFurnace; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Furnace; +import org.bukkit.block.Smoker; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldSaveEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class FureamMain extends JavaPlugin implements Listener, CommandExecutor { + public static final String DATA_KEY = "FureamData"; + public static final Map<@NotNull World, @NotNull FureamWorldConfig> WORLD_CONFIGS = new WeakHashMap<>(); + private static FureamMain INSTANCE; + + public static FureamMain getInstance() { + return INSTANCE; + } + + @Override + public void onEnable() { + INSTANCE = this; + + if (!getServer().getPluginManager().isPluginEnabled("NBTAPI")) { + getLogger().severe("Item-NBT-API is required for Fuream to operate! Please install NBT-API plugin."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // Register listeners + getServer().getPluginManager().registerEvents(this, this); + getServer().getPluginManager().registerEvents(new FurnaceOpenListener(), this); + getServer().getPluginManager().registerEvents(new GuiListener(), this); + getServer().getPluginManager().registerEvents(new HopperListener(), this); + getServer().getPluginManager().registerEvents(new BlockBreakListener(), this); + getServer().getPluginManager().registerEvents(new ChunkListener(), this); + + // Load configs and scan loaded chunks for existing furnaces + for (World world : Bukkit.getWorlds()) { + loadWorldConfig(world); + for (Chunk chunk : world.getLoadedChunks()) { + for (BlockState tile : chunk.getTileEntities()) { + if (tile instanceof Furnace) { + FurnaceManager.getOrCreateContext(tile.getBlock()); + } + } + } + } + + // Schedule tick loop + Bukkit.getScheduler().runTaskTimer(this, FurnaceManager::tickAll, 1L, 1L); + + // Register command + if (getCommand("fuream") != null) { + getCommand("fuream").setExecutor(this); + } + + getLogger().info("Fuream (Spigot) enabled successfully!"); + } + + @Override + public void onDisable() { + FurnaceManager.flushAll(); + FurnaceManager.CONTEXTS.clear(); + getLogger().info("Fuream (Spigot) disabled successfully!"); + } + + @EventHandler + public void onWorldLoad(WorldLoadEvent event) { + loadWorldConfig(event.getWorld()); + } + + @EventHandler + public void onWorldSave(WorldSaveEvent event) { + FurnaceManager.flushAll(); + } + + public static void loadWorldConfig(@NotNull World world) { + FureamWorldConfigImpl config = new FureamWorldConfigImpl(); + File configFile = new File(world.getWorldFolder(), "fuream.json"); + if (configFile.exists()) { + try { + FureamWorldConfigImpl.readWorldConfig(config, configFile); + } catch (Exception e) { + Bukkit.getLogger().warning("Failed to load fuream.json for world " + world.getName() + ": " + e.getMessage()); + } + } + WORLD_CONFIGS.put(world, config); + } + + public static @Nullable FureamWorldConfig getWorldConfig(@Nullable World world) { + if (world == null) return null; + FureamWorldConfig config = WORLD_CONFIGS.get(world); + if (config == null) { + loadWorldConfig(world); + config = WORLD_CONFIGS.get(world); + } + return config; + } + + @Contract(value = "null -> null", pure = true) + public static @Nullable FurnaceType getFurnaceType(@Nullable Object obj) { + if (obj instanceof FurnaceType) { + return (FurnaceType) obj; + } + if (obj instanceof String) { + try { + return FurnaceType.valueOf(((String) obj).toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) {} + return null; + } + if (obj instanceof Block) { + Material mat = ((Block) obj).getType(); + if (mat == Material.FURNACE) return FurnaceType.FURNACE; + if (mat == Material.SMOKER) return FurnaceType.SMOKER; + if (mat == Material.BLAST_FURNACE) return FurnaceType.BLAST_FURNACE; + return null; + } + if (obj instanceof BlockState) { + if (obj instanceof BlastFurnace) return FurnaceType.BLAST_FURNACE; + if (obj instanceof Smoker) return FurnaceType.SMOKER; + if (obj instanceof Furnace) return FurnaceType.FURNACE; + return null; + } + return null; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length > 0 && "reload".equalsIgnoreCase(args[0])) { + if (!sender.hasPermission("fuream.reload")) { + sender.sendMessage("§cYou do not have permission to use this command."); + return true; + } + for (World w : Bukkit.getWorlds()) { + loadWorldConfig(w); + } + sender.sendMessage("§a[Fuream] World configurations reloaded!"); + return true; + } + + if (args.length > 0 && "status".equalsIgnoreCase(args[0])) { + sender.sendMessage("§e=== Fuream Status ==="); + for (World w : Bukkit.getWorlds()) { + FureamWorldConfig cfg = getWorldConfig(w); + if (cfg != null) { + sender.sendMessage("§6World: §f" + w.getName() + " §7(Enabled: " + cfg.getEnabledFurnaceTypes() + ")"); + } + } + return true; + } + + sender.sendMessage("§6/fuream reload §7- Reload all world configs"); + sender.sendMessage("§6/fuream status §7- View status of all worlds"); + return true; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java new file mode 100644 index 0000000..b78b245 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java @@ -0,0 +1,1560 @@ +package io.github.eat_ram.fuream; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import java.io.File; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public class FureamWorldConfigImpl implements FureamWorldConfig { + @SuppressWarnings("deprecation") + public static void readWorldConfig( + @NotNull FureamWorldConfig config, @NotNull File configFile + ) throws FileNotFoundException, JsonIOException, JsonSyntaxException { + final JsonElement JSON; + try (final FileReader FR = new FileReader(configFile)) { + JSON = new JsonParser().parse(FR); + } catch (FileNotFoundException e) { + throw e; + } catch (java.io.IOException e) { + throw new JsonIOException(e); + } + if (JSON.isJsonObject()) { + JsonObject obj = JSON.getAsJsonObject(); + if (obj.has("input_slot_count")) { + JsonElement ele = obj.get("input_slot_count"); + if (ele.isJsonObject()) { + EnumMap inputSlotCount = + config.getInputSlotCount(); + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + int count = value.getAsInt(); + if (count > 0) { + inputSlotCount.put( + FurnaceType.valueOf(i.getKey()), count + ); + } + } catch (IllegalArgumentException ignored) {} + } + } + } + } + if (obj.has("fuel_slot_count")) { + JsonElement ele = obj.get("fuel_slot_count"); + if (ele.isJsonObject()) { + EnumMap fuelSlotCount = + config.getFuelSlotCount(); + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + int count = value.getAsInt(); + if (count > 0) { + fuelSlotCount.put( + FurnaceType.valueOf(i.getKey()), count + ); + } + } catch (IllegalArgumentException ignored) {} + } + } + } + } + if (obj.has("output_slot_count")) { + JsonElement ele = obj.get("output_slot_count"); + if (ele.isJsonObject()) { + EnumMap outputSlotCount = + config.getOutputSlotCount(); + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + int count = value.getAsInt(); + if (count > 0) { + outputSlotCount.put( + FurnaceType.valueOf(i.getKey()), count + ); + } + } catch (IllegalArgumentException ignored) {} + } + } + } + } + if (obj.has("prevents_hopper_insert_non_smeltable")) { + JsonElement ele = + obj.get("prevents_hopper_insert_non_smeltable"); + if (ele.isJsonArray()) { + EnumSet preventsHopperInsertNonSmeltable = + config.getPreventsHopperInsertNonSmeltable(); + for (JsonElement i : ele.getAsJsonArray()) { + if (i.isJsonPrimitive()) { + try { + preventsHopperInsertNonSmeltable + .add(FurnaceType.valueOf(i.getAsString())); + } catch (IllegalArgumentException ignored) {} + } + } + } + } + if (obj.has("enabled_furnace_types")) { + JsonElement ele = obj.get("enabled_furnace_types"); + if (ele.isJsonArray()) { + EnumSet enabledFurnaceTypes = + config.getEnabledFurnaceTypes(); + for (JsonElement i : ele.getAsJsonArray()) { + if (i.isJsonPrimitive()) { + try { + enabledFurnaceTypes + .add(FurnaceType.valueOf(i.getAsString())); + } catch (IllegalArgumentException ignored) {} + } + } + } + } + // Below are generated by a Python script + if (obj.has("gui_border_item_id")) { + JsonElement ele = obj.get("gui_border_item_id"); + EnumMap ids = + config.getGuiBorderItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_fuel_left_item_id")) { + JsonElement ele = obj.get("gui_fuel_left_item_id"); + EnumMap ids = + config.getGuiFuelLeftItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_fuel_used_item_id")) { + JsonElement ele = obj.get("gui_fuel_used_item_id"); + EnumMap ids = + config.getGuiFuelUsedItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_progress_done_item_id")) { + JsonElement ele = obj.get("gui_progress_done_item_id"); + EnumMap ids = + config.getGuiProgressDoneItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_progress_remaining_item_id")) { + JsonElement ele = obj.get("gui_progress_remaining_item_id"); + EnumMap ids = + config.getGuiProgressRemainingItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_recipe_item_id")) { + JsonElement ele = obj.get("gui_prev_recipe_item_id"); + EnumMap ids = + config.getGuiPrevRecipeItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_recipe_item_id")) { + JsonElement ele = obj.get("gui_next_recipe_item_id"); + EnumMap ids = + config.getGuiNextRecipeItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_xp_indicator_item_id")) { + JsonElement ele = obj.get("gui_xp_indicator_item_id"); + EnumMap ids = + config.getGuiXpIndicatorItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_functional_area_item_id")) { + JsonElement ele = obj.get("gui_next_functional_area_item_id"); + EnumMap ids = + config.getGuiNextFunctionalAreaItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_page_item_id")) { + JsonElement ele = obj.get("gui_prev_page_item_id"); + EnumMap ids = + config.getGuiPrevPageItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_page_item_id")) { + JsonElement ele = obj.get("gui_next_page_item_id"); + EnumMap ids = + config.getGuiNextPageItemId(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_border_item_title")) { + JsonElement ele = obj.get("gui_border_item_title"); + EnumMap ids = + config.getGuiBorderItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_fuel_item_title")) { + JsonElement ele = obj.get("gui_fuel_item_title"); + EnumMap ids = + config.getGuiFuelItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_fuel_item_tooltip")) { + JsonElement ele = obj.get("gui_fuel_item_tooltip"); + EnumMap ids = + config.getGuiFuelItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_progress_item_title")) { + JsonElement ele = obj.get("gui_progress_item_title"); + EnumMap ids = + config.getGuiProgressItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_progress_item_tooltip")) { + JsonElement ele = obj.get("gui_progress_item_tooltip"); + EnumMap ids = + config.getGuiProgressItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_recipe_item_title")) { + JsonElement ele = obj.get("gui_prev_recipe_item_title"); + EnumMap ids = + config.getGuiPrevRecipeItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_recipe_empty_item_tooltip")) { + JsonElement ele = obj.get("gui_prev_recipe_empty_item_tooltip"); + EnumMap ids = + config.getGuiPrevRecipeEmptyItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_recipe_arbitrary_item_tooltip")) { + JsonElement ele = obj.get("gui_prev_recipe_arbitrary_item_tooltip"); + EnumMap ids = + config.getGuiPrevRecipeArbitraryItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_recipe_item_tooltip")) { + JsonElement ele = obj.get("gui_prev_recipe_item_tooltip"); + EnumMap ids = + config.getGuiPrevRecipeItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_recipe_item_title")) { + JsonElement ele = obj.get("gui_next_recipe_item_title"); + EnumMap ids = + config.getGuiNextRecipeItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_recipe_empty_item_tooltip")) { + JsonElement ele = obj.get("gui_next_recipe_empty_item_tooltip"); + EnumMap ids = + config.getGuiNextRecipeEmptyItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_recipe_arbitrary_item_tooltip")) { + JsonElement ele = obj.get("gui_next_recipe_arbitrary_item_tooltip"); + EnumMap ids = + config.getGuiNextRecipeArbitraryItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_recipe_item_tooltip")) { + JsonElement ele = obj.get("gui_next_recipe_item_tooltip"); + EnumMap ids = + config.getGuiNextRecipeItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_xp_indicator_item_title")) { + JsonElement ele = obj.get("gui_xp_indicator_item_title"); + EnumMap ids = + config.getGuiXpIndicatorItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_xp_indicator_gainable_item_title")) { + JsonElement ele = obj.get("gui_xp_indicator_gainable_item_title"); + EnumMap ids = + config.getGuiXpIndicatorGainableItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_xp_indicator_item_tooltip")) { + JsonElement ele = obj.get("gui_xp_indicator_item_tooltip"); + EnumMap ids = + config.getGuiXpIndicatorItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_functional_area_item_title")) { + JsonElement ele = + obj.get("gui_next_functional_area_item_title"); + EnumMap ids = + config.getGuiNextFunctionalAreaItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_page_item_title")) { + JsonElement ele = obj.get("gui_prev_page_item_title"); + EnumMap ids = + config.getGuiPrevPageItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_prev_page_item_tooltip")) { + JsonElement ele = obj.get("gui_prev_page_item_tooltip"); + EnumMap ids = + config.getGuiPrevPageItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_page_item_title")) { + JsonElement ele = obj.get("gui_next_page_item_title"); + EnumMap ids = + config.getGuiNextPageItemTitle(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + if (obj.has("gui_next_page_item_tooltip")) { + JsonElement ele = obj.get("gui_next_page_item_tooltip"); + EnumMap ids = + config.getGuiNextPageItemTooltip(); + if (ele.isJsonObject()) { + for (Map.Entry i : + ele.getAsJsonObject().entrySet()) { + JsonElement value = i.getValue(); + if (value.isJsonPrimitive()) { + try { + ids.put( + FurnaceType.valueOf(i.getKey()), + value.getAsString() + ); + } catch (IllegalArgumentException ignored) {} + } + } + } else if (ele.isJsonPrimitive()) { + String id = ele.getAsString(); + for (FurnaceType type : FurnaceType.values()) { + ids.put(type, id); + } + } + } + } + } + + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + inputSlotCount = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + fuelSlotCount = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + outputSlotCount = new EnumMap<>(FurnaceType.class); + private @NotNull EnumSet preventsHopperInsertNonSmeltable = + EnumSet.noneOf(FurnaceType.class); + private @NotNull EnumSet enabledFurnaceTypes = + EnumSet.noneOf(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiBorderItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiFuelLeftItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiFuelUsedItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiProgressDoneItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiProgressRemainingItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevRecipeItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextRecipeItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiXpIndicatorItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextFunctionalAreaItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevPageItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextPageItemId = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiBorderItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiFuelItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiFuelItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiProgressItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiProgressItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevRecipeItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevRecipeEmptyItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevRecipeArbitraryItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevRecipeItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextRecipeItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextRecipeEmptyItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextRecipeArbitraryItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextRecipeItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiXpIndicatorItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiXpIndicatorGainableItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiXpIndicatorItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextFunctionalAreaItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevPageItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiPrevPageItemTooltip = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextPageItemTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiNextPageItemTooltip = new EnumMap<>(FurnaceType.class); + + public FureamWorldConfigImpl() { + for (FurnaceType type : FurnaceType.values()) { + this.inputSlotCount.put(type, 9); + this.fuelSlotCount.put(type, 9); + this.outputSlotCount.put(type, 9); + this.guiBorderItemId.put( + type, "minecraft:black_stained_glass_pane" + ); + this.guiFuelLeftItemId.put( + type, "minecraft:orange_stained_glass_pane" + ); + this.guiFuelUsedItemId.put( + type, "minecraft:black_stained_glass_pane" + ); + this.guiProgressDoneItemId.put( + type, "minecraft:white_stained_glass_pane" + ); + this.guiProgressRemainingItemId.put( + type, "minecraft:black_stained_glass_pane" + ); + this.guiPrevRecipeItemId + .put(type, "minecraft:book"); + this.guiNextRecipeItemId + .put(type, "minecraft:book"); + this.guiXpIndicatorItemId.put( + type, "minecraft:lime_stained_glass_pane" + ); + this.guiNextFunctionalAreaItemId + .put(type, "minecraft:command_block"); + this.guiPrevPageItemId + .put(type, "minecraft:arrow"); + this.guiNextPageItemId + .put(type, "minecraft:arrow"); + this.guiBorderItemTitle.put(type, "GUI Border"); + this.guiFuelItemTitle.put(type, "Fuel"); + this.guiFuelItemTooltip.put(type, "Available: %d / %d"); + this.guiProgressItemTitle.put(type, "Progress"); + this.guiProgressItemTooltip.put(type, "%d / %d"); + this.guiPrevRecipeItemTitle.put(type, "Previous Recipe"); + this.guiPrevRecipeEmptyItemTooltip.put( + type, + "Please insert an item into the slot on the left to adjust" + ); + this.guiPrevRecipeArbitraryItemTooltip + .put(type, "Current: arbitrary of %d"); + this.guiPrevRecipeItemTooltip.put(type, "Current: %d / %d"); + this.guiNextRecipeItemTitle.put(type, "Next Recipe"); + this.guiNextRecipeEmptyItemTooltip.put( + type, + "Please insert an item into the slot on the left to adjust" + ); + this.guiNextRecipeArbitraryItemTooltip + .put(type, "Current: arbitrary of %d"); + this.guiNextRecipeItemTooltip.put(type, "Current: %d / %d"); + this.guiXpIndicatorItemTitle.put(type, "XP"); + this.guiXpIndicatorGainableItemTitle + .put(type, "XP (click to gain)"); + this.guiXpIndicatorItemTooltip.put(type, "Current: %.1f"); + this.guiNextFunctionalAreaItemTitle + .put(type, "Next Functional Area"); + this.guiPrevPageItemTitle.put(type, "Previous Page"); + this.guiPrevPageItemTooltip.put(type, "Page %d"); + this.guiNextPageItemTitle.put(type, "Next Page"); + this.guiNextPageItemTooltip.put(type, "Page %d"); + } + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + getInputSlotCount() { + return this.inputSlotCount; + } + + @Override + public void setInputSlotCount( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> map + ) { + this.inputSlotCount = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + getFuelSlotCount() { + return this.fuelSlotCount; + } + + @Override + public void setFuelSlotCount( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> map + ) { + this.fuelSlotCount = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> + getOutputSlotCount() { + return this.outputSlotCount; + } + + @Override + public void setOutputSlotCount( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> map + ) { + this.outputSlotCount = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumSet + getPreventsHopperInsertNonSmeltable() { + return this.preventsHopperInsertNonSmeltable; + } + + @Override + public void setPreventsHopperInsertNonSmeltable( + @NotNull EnumSet types + ) { + this.preventsHopperInsertNonSmeltable = types; + } + + @Override + @Contract(pure = true) + public @NotNull EnumSet getEnabledFurnaceTypes() { + return this.enabledFurnaceTypes; + } + + @Override + public void + setEnabledFurnaceTypes(@NotNull EnumSet types) { + this.enabledFurnaceTypes = types; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiBorderItemId() { + return this.guiBorderItemId; + } + + @Override + public void setGuiBorderItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiBorderItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiFuelLeftItemId() { + return this.guiFuelLeftItemId; + } + + @Override + public void setGuiFuelLeftItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiFuelLeftItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiFuelUsedItemId() { + return this.guiFuelUsedItemId; + } + + @Override + public void setGuiFuelUsedItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiFuelUsedItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiProgressDoneItemId() { + return this.guiProgressDoneItemId; + } + + @Override + public void setGuiProgressDoneItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiProgressDoneItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiProgressRemainingItemId() { + return this.guiProgressRemainingItemId; + } + + @Override + public void setGuiProgressRemainingItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiProgressRemainingItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevRecipeItemId() { + return this.guiPrevRecipeItemId; + } + + @Override + public void setGuiPrevRecipeItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevRecipeItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextRecipeItemId() { + return this.guiNextRecipeItemId; + } + + @Override + public void setGuiNextRecipeItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextRecipeItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiXpIndicatorItemId() { + return this.guiXpIndicatorItemId; + } + + @Override + public void setGuiXpIndicatorItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiXpIndicatorItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextFunctionalAreaItemId() { + return this.guiNextFunctionalAreaItemId; + } + + @Override + public void setGuiNextFunctionalAreaItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextFunctionalAreaItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevPageItemId() { + return this.guiPrevPageItemId; + } + + @Override + public void setGuiPrevPageItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevPageItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextPageItemId() { + return this.guiNextPageItemId; + } + + @Override + public void setGuiNextPageItemId( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextPageItemId = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiBorderItemTitle() { + return this.guiBorderItemTitle; + } + + @Override + public void setGuiBorderItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiBorderItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiFuelItemTitle() { + return this.guiFuelItemTitle; + } + + @Override + public void setGuiFuelItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiFuelItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiFuelItemTooltip() { + return this.guiFuelItemTooltip; + } + + @Override + public void setGuiFuelItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiFuelItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiProgressItemTitle() { + return this.guiProgressItemTitle; + } + + @Override + public void setGuiProgressItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiProgressItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiProgressItemTooltip() { + return this.guiProgressItemTooltip; + } + + @Override + public void setGuiProgressItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiProgressItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevRecipeItemTitle() { + return this.guiPrevRecipeItemTitle; + } + + @Override + public void setGuiPrevRecipeItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevRecipeItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevRecipeEmptyItemTooltip() { + return this.guiPrevRecipeEmptyItemTooltip; + } + + @Override + public void setGuiPrevRecipeEmptyItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevRecipeEmptyItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevRecipeArbitraryItemTooltip() { + return this.guiPrevRecipeArbitraryItemTooltip; + } + + @Override + public void setGuiPrevRecipeArbitraryItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevRecipeArbitraryItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevRecipeItemTooltip() { + return this.guiPrevRecipeItemTooltip; + } + + @Override + public void setGuiPrevRecipeItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevRecipeItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextRecipeItemTitle() { + return this.guiNextRecipeItemTitle; + } + + @Override + public void setGuiNextRecipeItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextRecipeItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextRecipeEmptyItemTooltip() { + return this.guiNextRecipeEmptyItemTooltip; + } + + @Override + public void setGuiNextRecipeEmptyItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextRecipeEmptyItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextRecipeArbitraryItemTooltip() { + return this.guiNextRecipeArbitraryItemTooltip; + } + + @Override + public void setGuiNextRecipeArbitraryItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextRecipeArbitraryItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextRecipeItemTooltip() { + return this.guiNextRecipeItemTooltip; + } + + @Override + public void setGuiNextRecipeItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextRecipeItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiXpIndicatorItemTitle() { + return this.guiXpIndicatorItemTitle; + } + + @Override + public void setGuiXpIndicatorItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiXpIndicatorItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiXpIndicatorGainableItemTitle() { + return this.guiXpIndicatorGainableItemTitle; + } + + @Override + public void setGuiXpIndicatorGainableItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiXpIndicatorGainableItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiXpIndicatorItemTooltip() { + return this.guiXpIndicatorItemTooltip; + } + + @Override + public void setGuiXpIndicatorItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiXpIndicatorItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextFunctionalAreaItemTitle() { + return this.guiNextFunctionalAreaItemTitle; + } + + @Override + public void setGuiNextFunctionalAreaItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextFunctionalAreaItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevPageItemTitle() { + return this.guiPrevPageItemTitle; + } + + @Override + public void setGuiPrevPageItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevPageItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiPrevPageItemTooltip() { + return this.guiPrevPageItemTooltip; + } + + @Override + public void setGuiPrevPageItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiPrevPageItemTooltip = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextPageItemTitle() { + return this.guiNextPageItemTitle; + } + + @Override + public void setGuiNextPageItemTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextPageItemTitle = map; + } + + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiNextPageItemTooltip() { + return this.guiNextPageItemTooltip; + } + + @Override + public void setGuiNextPageItemTooltip( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiNextPageItemTooltip = map; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java new file mode 100644 index 0000000..dc5b5a5 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java @@ -0,0 +1,11 @@ +package io.github.eat_ram.fuream.data; + +import de.tr7zw.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.nbtapi.iface.ReadableNBT; +import org.jetbrains.annotations.NotNull; + +public interface FureamData { + void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt); + + void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt); +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamDataHolder.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamDataHolder.java new file mode 100644 index 0000000..d2a88f1 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamDataHolder.java @@ -0,0 +1,13 @@ +package io.github.eat_ram.fuream.data; + +import org.jetbrains.annotations.Nullable; + +/** + * Interface implemented by furnace data holders. Gives the rest of the plugin + * a type-safe way to reach the virtual smelting state of a furnace. + */ +@FunctionalInterface +public interface FureamDataHolder { + @Nullable T + getFureamData(String key, Class clazz); +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java new file mode 100644 index 0000000..c8224c2 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java @@ -0,0 +1,141 @@ +package io.github.eat_ram.fuream.data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import de.tr7zw.nbtapi.NBT; +import de.tr7zw.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.nbtapi.iface.ReadWriteNBTCompoundList; +import de.tr7zw.nbtapi.iface.ReadableNBT; +import de.tr7zw.nbtapi.iface.ReadableNBTList; +import io.github.eat_ram.fuream.util.KeyableItemStack; +import io.github.eat_ram.fuream.util.NbtUtil; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import static io.github.eat_ram.fuream.CollectionUtil.newDefaultedArrayList; + +/** + * The server-side smelting state of a virtual furnace: inputs, fuels, outputs, + * recipe overrides, and accumulated experience. + */ +public class FureamFurnaceData implements FureamData { + public static final @NotNull String INPUTS_KEY = "Inputs"; + public static final @NotNull String FUELS_KEY = "Fuels"; + public static final @NotNull String OUTPUTS_KEY = "Outputs"; + public static final @NotNull String RECIPE_OVERRIDING_INPUT_KEY = "RecipeOverridingInput"; + public static final @NotNull String OVERRIDDEN_RECIPES_KEY = "OverriddenRecipes"; + public static final @NotNull String XP_KEY = "Xp"; + + public final @NotNull ArrayList<@NotNull ItemStack> inputs; + public final @NotNull ArrayList<@NotNull ItemStack> fuels; + public final @NotNull ArrayList<@NotNull ItemStack> outputs; + public @NotNull ItemStack recipeOverridingInput; + public final @NotNull HashMap<@NotNull KeyableItemStack, @NotNull NamespacedKey> overriddenRecipes; + public float experience; + + public FureamFurnaceData() { + this.inputs = newDefaultedArrayList(new ItemStack(Material.AIR), 1); + this.fuels = newDefaultedArrayList(new ItemStack(Material.AIR), 1); + this.outputs = newDefaultedArrayList(new ItemStack(Material.AIR), 1); + this.recipeOverridingInput = new ItemStack(Material.AIR); + this.overriddenRecipes = new HashMap<>(); + } + + public boolean hasAny() { + if (this.experience > 0f) return true; + for (ItemStack s : this.inputs) { + if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + } + for (ItemStack s : this.fuels) { + if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + } + for (ItemStack s : this.outputs) { + if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + } + return false; + } + + @Override + public void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt) { + NbtUtil.writeStacks(nbt, INPUTS_KEY, this.inputs); + NbtUtil.writeStacks(nbt, FUELS_KEY, this.fuels); + NbtUtil.writeStacks(nbt, OUTPUTS_KEY, this.outputs); + + if (nbt.hasTag(RECIPE_OVERRIDING_INPUT_KEY)) { + nbt.removeKey(RECIPE_OVERRIDING_INPUT_KEY); + } + if (this.recipeOverridingInput != null && !this.recipeOverridingInput.getType().isAir()) { + ReadWriteNBT tag = nbt.getOrCreateCompound(RECIPE_OVERRIDING_INPUT_KEY); + NbtUtil.writeItemToCompound(tag, this.recipeOverridingInput); + } + + if (nbt.hasTag(OVERRIDDEN_RECIPES_KEY)) { + nbt.removeKey(OVERRIDDEN_RECIPES_KEY); + } + if (!this.overriddenRecipes.isEmpty()) { + ReadWriteNBTCompoundList list = nbt.getCompoundList(OVERRIDDEN_RECIPES_KEY); + for (Map.Entry entry : this.overriddenRecipes.entrySet()) { + ItemStack stack = entry.getKey().stack; + if (stack != null && !stack.getType().isAir()) { + ReadWriteNBT sub = list.addCompound(); + ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); + sub.mergeCompound(itemNbt); + sub.setString("RecipeId", entry.getValue().toString()); + } + } + } + nbt.setFloat(XP_KEY, this.experience); + nbt.setBoolean("FureamInitialized", true); + } + + @Override + public void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt) { + for (int i = 0; i < this.inputs.size(); i++) { + this.inputs.set(i, new ItemStack(Material.AIR)); + } + for (int i = 0; i < this.fuels.size(); i++) { + this.fuels.set(i, new ItemStack(Material.AIR)); + } + for (int i = 0; i < this.outputs.size(); i++) { + this.outputs.set(i, new ItemStack(Material.AIR)); + } + + if (nbt.hasTag(INPUTS_KEY)) { + NbtUtil.readStacks(nbt, INPUTS_KEY, this.inputs); + } + if (nbt.hasTag(FUELS_KEY)) { + NbtUtil.readStacks(nbt, FUELS_KEY, this.fuels); + } + if (nbt.hasTag(OUTPUTS_KEY)) { + NbtUtil.readStacks(nbt, OUTPUTS_KEY, this.outputs); + } + if (nbt.hasTag(RECIPE_OVERRIDING_INPUT_KEY)) { + this.recipeOverridingInput = NbtUtil.fromNbt(nbt.getCompound(RECIPE_OVERRIDING_INPUT_KEY)); + } else { + this.recipeOverridingInput = new ItemStack(Material.AIR); + } + this.overriddenRecipes.clear(); + if (nbt.hasTag(OVERRIDDEN_RECIPES_KEY)) { + ReadableNBTList list = nbt.getCompoundList(OVERRIDDEN_RECIPES_KEY); + for (int i = 0; i < list.size(); ++i) { + ReadWriteNBT tag = list.get(i); + if (tag.hasTag("RecipeId")) { + String recipeIdStr = tag.getString("RecipeId"); + NamespacedKey key = NamespacedKey.fromString(recipeIdStr); + if (key != null) { + ItemStack stack = NbtUtil.fromNbt(tag); + if (!stack.getType().isAir()) { + stack.setAmount(1); + this.overriddenRecipes.put(new KeyableItemStack(stack), key); + } + } + } + } + } + this.experience = nbt.hasTag(XP_KEY) ? nbt.getFloat(XP_KEY) : 0f; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java new file mode 100644 index 0000000..041fc98 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java @@ -0,0 +1,23 @@ +package io.github.eat_ram.fuream.data; + +import java.util.HashMap; +import java.util.function.Function; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public abstract class FurnaceFureamDataRegistry { + public static final @NotNull HashMap<@NotNull String, @NotNull Function< + ? super FureamDataHolder, ? extends @NotNull FureamData + >> REGISTRY = new HashMap<>(); + + @Contract("-> fail") + private FurnaceFureamDataRegistry() { + throw new UnsupportedOperationException(); + } + + static { + FurnaceFureamDataRegistry.REGISTRY + .put("fuream", holder -> new FureamFurnaceData()); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java new file mode 100644 index 0000000..76b1033 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java @@ -0,0 +1,86 @@ +package io.github.eat_ram.fuream.hook; + +import java.lang.reflect.Field; +import java.util.List; + +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamData; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.inventory.ItemStack; + +public class BlockBreakListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + handleFurnaceBreak(event.getBlock()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBurn(BlockBurnEvent event) { + handleFurnaceBreak(event.getBlock()); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + for (Block b : event.blockList()) { + handleFurnaceBreak(b); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + for (Block b : event.blockList()) { + handleFurnaceBreak(b); + } + } + + private void handleFurnaceBreak(Block block) { + FurnaceType type = FureamMain.getFurnaceType(block); + if (type == null) return; + + FurnacePos pos = new FurnacePos(block); + FurnaceContext ctx = FurnaceManager.CONTEXTS.remove(pos); + if (ctx == null) { + ctx = FurnaceManager.getOrCreateContext(block); + FurnaceManager.CONTEXTS.remove(pos); + } + if (ctx != null) { + Location loc = block.getLocation(); + FureamFurnaceLogic.dropOnBreak(loc, ctx.data); + + // Handle extra data items (e.g. upgrades) + for (FureamData extra : ctx.extraData.values()) { + try { + Field rateField = extra.getClass().getField("rateUpgradeStack"); + ItemStack rate = (ItemStack) rateField.get(extra); + if (rate != null && !rate.getType().isAir()) { + loc.getWorld().dropItemNaturally(loc, rate.clone()); + } + Field fuelField = extra.getClass().getField("fuelUpgradeStack"); + ItemStack fuel = (ItemStack) fuelField.get(extra); + if (fuel != null && !fuel.getType().isAir()) { + loc.getWorld().dropItemNaturally(loc, fuel.clone()); + } + } catch (Exception ignored) {} + } + + if (ctx.activeGui != null) { + for (org.bukkit.entity.HumanEntity viewer : new java.util.ArrayList<>(ctx.activeGui.bukkitInventory.getViewers())) { + viewer.closeInventory(); + } + } + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java new file mode 100644 index 0000000..fd08b3b --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java @@ -0,0 +1,56 @@ +package io.github.eat_ram.fuream.hook; + +import java.util.Iterator; +import java.util.Map; + +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.Chunk; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Furnace; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; + +public class ChunkListener implements Listener { + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkLoad(ChunkLoadEvent event) { + for (BlockState tile : event.getChunk().getTileEntities()) { + if (tile instanceof Furnace) { + FurnaceManager.getOrCreateContext(tile.getBlock()); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkUnload(ChunkUnloadEvent event) { + Chunk chunk = event.getChunk(); + String worldName = chunk.getWorld().getName(); + int cx = chunk.getX(); + int cz = chunk.getZ(); + + for (Iterator> it = FurnaceManager.CONTEXTS.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + FurnacePos pos = entry.getKey(); + if (pos.worldName.equals(worldName) && (pos.x >> 4) == cx && (pos.z >> 4) == cz) { + FurnaceContext ctx = entry.getValue(); + if (ctx.dirty || ctx.data.hasAny()) { + FurnaceManager.saveToNbt(ctx); + } + it.remove(); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPlace(BlockPlaceEvent event) { + Block block = event.getBlock(); + if (block.getState() instanceof Furnace) { + FurnaceManager.getOrCreateContext(block); + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java new file mode 100644 index 0000000..be279b4 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -0,0 +1,345 @@ +package io.github.eat_ram.fuream.hook; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import de.tr7zw.nbtapi.NBT; +import de.tr7zw.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.nbtapi.iface.ReadableNBT; +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamData; +import io.github.eat_ram.fuream.data.FureamDataHolder; +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; +import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; +import io.github.eat_ram.fuream.screen.FureamScreenHandler; +import io.github.eat_ram.fuream.screen.FureamScreenInventory; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Furnace; +import org.bukkit.block.Hopper; +import org.bukkit.entity.minecart.HopperMinecart; +import org.bukkit.inventory.FurnaceInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class FurnaceManager { + public static final Map CONTEXTS = new ConcurrentHashMap<>(); + private static long tickCounter = 0; + + public static class FurnaceContext implements FureamDataHolder { + public final @NotNull FurnacePos pos; + public final @NotNull FurnaceType type; + public final @NotNull FureamFurnaceData data; + public final @NotNull Map<@NotNull String, @NotNull FureamData> extraData = new HashMap<>(); + + public int burnTime; + public int fuelTimeTotal = 200; + public int cookTime; + public int cookTimeTotal = 200; + + public @Nullable FureamScreenInventory activeGui; + public @Nullable FureamScreenHandler handler; + public boolean dirty; + + public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { + this.pos = pos; + this.type = type; + this.data = new FureamFurnaceData(); + } + + public @Nullable World getWorld() { + return Bukkit.getWorld(this.pos.worldName); + } + + @Override + public @Nullable T getFureamData(String key, Class clazz) { + if ("fuream".equals(key)) { + if (clazz.isInstance(this.data)) { + return clazz.cast(this.data); + } + } + FureamData extra = this.extraData.get(key); + if (clazz.isInstance(extra)) { + return clazz.cast(extra); + } + return null; + } + + public double getRateUpgradeFactor() { + FureamData extra = this.extraData.get("fuream-extras-furnace-upgrades"); + if (extra != null) { + try { + Field f = extra.getClass().getField("RATE_UPGRADE_FACTORS"); + Map map = (Map) f.get(null); + Field rateStackField = extra.getClass().getField("rateUpgradeStack"); + ItemStack stack = (ItemStack) rateStackField.get(extra); + if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { + Class ciClass = Class.forName("io.github.eat_ram.fuream.extras.fu.CountedItem"); + Object countedItem = ciClass.getConstructor(ItemStack.class).newInstance(stack); + Object val = map.get(countedItem); + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + } + } catch (Exception ignored) {} + } + return 1.0; + } + + public double getFuelUpgradeFactor() { + FureamData extra = this.extraData.get("fuream-extras-furnace-upgrades"); + if (extra != null) { + try { + Field f = extra.getClass().getField("FUEL_UPGRADE_FACTORS"); + Map map = (Map) f.get(null); + Field fuelStackField = extra.getClass().getField("fuelUpgradeStack"); + ItemStack stack = (ItemStack) fuelStackField.get(extra); + if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { + Class ciClass = Class.forName("io.github.eat_ram.fuream.extras.fu.CountedItem"); + Object countedItem = ciClass.getConstructor(ItemStack.class).newInstance(stack); + Object val = map.get(countedItem); + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + } + } catch (Exception ignored) {} + } + return 1.0; + } + } + + public static @Nullable FurnaceContext getOrCreateContext(@NotNull Block block) { + FurnaceType type = FureamMain.getFurnaceType(block); + if (type == null) return null; + + FurnacePos pos = new FurnacePos(block); + FurnaceContext existing = CONTEXTS.get(pos); + if (existing != null) { + return existing; + } + + FurnaceContext ctx = new FurnaceContext(pos, type); + for (Map.Entry> entry : + FurnaceFureamDataRegistry.REGISTRY.entrySet()) { + if (!"fuream".equals(entry.getKey())) { + ctx.extraData.put(entry.getKey(), entry.getValue().apply(ctx)); + } + } + + BlockState state = block.getState(); + if (state instanceof Furnace) { + try { + NBT.getPersistentData(state, (ReadableNBT nbt) -> { + if (nbt != null && nbt.hasTag(FureamMain.DATA_KEY)) { + ReadableNBT dataCompound = nbt.getCompound(FureamMain.DATA_KEY); + ctx.data.readNbt(ctx, dataCompound); + for (Map.Entry entry : ctx.extraData.entrySet()) { + entry.getValue().readNbt(ctx, dataCompound); + } + if (nbt.hasTag("BurnTime")) { + ctx.burnTime = nbt.getInteger("BurnTime"); + } + if (nbt.hasTag("CookTime")) { + ctx.cookTime = nbt.getInteger("CookTime"); + } + if (nbt.hasTag("CookTimeTotal")) { + ctx.cookTimeTotal = nbt.getInteger("CookTimeTotal"); + } + } else { + // Vanilla lane migration + Furnace furnaceState = (Furnace) state; + FurnaceInventory inv = furnaceState.getInventory(); + ItemStack smelting = inv.getSmelting(); + ItemStack fuel = inv.getFuel(); + ItemStack result = inv.getResult(); + + if (smelting != null && !smelting.getType().isAir()) ctx.data.inputs.set(0, smelting.clone()); + if (fuel != null && !fuel.getType().isAir()) ctx.data.fuels.set(0, fuel.clone()); + if (result != null && !result.getType().isAir()) ctx.data.outputs.set(0, result.clone()); + + inv.clear(); + ctx.burnTime = furnaceState.getBurnTime(); + ctx.cookTime = furnaceState.getCookTime(); + ctx.cookTimeTotal = furnaceState.getCookTimeTotal() > 0 ? furnaceState.getCookTimeTotal() : 200; + ctx.dirty = true; + saveToNbt(ctx); + } + return true; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + CONTEXTS.put(pos, ctx); + return ctx; + } + + public static void saveToNbt(@NotNull FurnaceContext ctx) { + World world = ctx.getWorld(); + if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) return; + + Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); + BlockState state = block.getState(); + if (state instanceof Furnace) { + try { + NBT.modifyPersistentData(state, (ReadWriteNBT nbt) -> { + ReadWriteNBT dataCompound = nbt.getOrCreateCompound(FureamMain.DATA_KEY); + ctx.data.writeNbt(ctx, dataCompound); + for (FureamData extra : ctx.extraData.values()) { + extra.writeNbt(ctx, dataCompound); + } + nbt.setInteger("BurnTime", ctx.burnTime); + nbt.setInteger("CookTime", ctx.cookTime); + nbt.setInteger("CookTimeTotal", ctx.cookTimeTotal); + }); + ctx.dirty = false; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void tickAll() { + Iterator> it = CONTEXTS.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + FurnaceContext ctx = entry.getValue(); + World world = ctx.getWorld(); + + if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) { + if (ctx.dirty) { + saveToNbt(ctx); + } + it.remove(); + continue; + } + + Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); + FurnaceType currentType = FureamMain.getFurnaceType(block); + if (currentType == null) { + // Block was removed or changed + if (ctx.dirty) { + saveToNbt(ctx); + } + it.remove(); + continue; + } + + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config != null && config.getEnabledFurnaceTypes().contains(ctx.type)) { + FureamFurnaceEngine.tick(ctx, config); + + // Handle hopper below extraction (every 8 ticks, standard hopper speed) + if (tickCounter % 8 == 0) { + processHopperPull(block, ctx); + } + } + + // Periodic save if dirty + if (ctx.dirty) { + if (tickCounter % 40 == 0) { + saveToNbt(ctx); + } + } + } + tickCounter++; + } + + private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull FurnaceContext ctx) { + Block below = furnaceBlock.getRelative(BlockFace.DOWN); + Inventory targetInv = null; + + if (below.getType() == Material.HOPPER) { + if (below.getBlockData() instanceof org.bukkit.block.data.type.Hopper) { + org.bukkit.block.data.type.Hopper hopperData = (org.bukkit.block.data.type.Hopper) below.getBlockData(); + if (hopperData.isEnabled()) { + BlockState belowState = below.getState(); + if (belowState instanceof Hopper) { + targetInv = ((Hopper) belowState).getInventory(); + } + } + } + } + + if (targetInv == null) { + Location centerBelow = furnaceBlock.getLocation().add(0.5, -0.5, 0.5); + for (org.bukkit.entity.Entity e : furnaceBlock.getWorld().getNearbyEntities(centerBelow, 0.6, 0.6, 0.6)) { + if (e instanceof HopperMinecart) { + targetInv = ((HopperMinecart) e).getInventory(); + break; + } + } + } + + if (targetInv == null) { + return; + } + + // 1. Try pulling from outputs first + for (int i = 0; i < ctx.data.outputs.size(); i++) { + ItemStack outStack = ctx.data.outputs.get(i); + if (outStack != null && !outStack.getType().isAir() && outStack.getAmount() > 0) { + ItemStack single = outStack.clone(); + single.setAmount(1); + + Map leftover = targetInv.addItem(single); + if (leftover.isEmpty()) { + outStack.setAmount(outStack.getAmount() - 1); + if (outStack.getAmount() <= 0) { + ctx.data.outputs.set(i, new ItemStack(Material.AIR)); + } + if (ctx.activeGui != null) { + ctx.activeGui.refreshVisuals(); + } + ctx.dirty = true; + return; + } + } + } + + // 2. Try pulling empty buckets or water buckets from fuels + for (int i = 0; i < ctx.data.fuels.size(); i++) { + ItemStack fuelStack = ctx.data.fuels.get(i); + if (fuelStack != null && (fuelStack.getType() == Material.BUCKET || fuelStack.getType() == Material.WATER_BUCKET)) { + ItemStack single = fuelStack.clone(); + single.setAmount(1); + + Map leftover = targetInv.addItem(single); + if (leftover.isEmpty()) { + fuelStack.setAmount(fuelStack.getAmount() - 1); + if (fuelStack.getAmount() <= 0) { + ctx.data.fuels.set(i, new ItemStack(Material.AIR)); + } + if (ctx.activeGui != null) { + ctx.activeGui.refreshVisuals(); + } + ctx.dirty = true; + return; + } + } + } + } + + public static void flushAll() { + for (FurnaceContext ctx : CONTEXTS.values()) { + if (ctx.dirty || ctx.data.hasAny()) { + saveToNbt(ctx); + } + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java new file mode 100644 index 0000000..702b635 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java @@ -0,0 +1,58 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.screen.FureamScreenHandler; +import io.github.eat_ram.fuream.screen.FureamScreenInventory; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; + +public class FurnaceOpenListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { + return; + } + + Block block = event.getClickedBlock(); + if (block == null) return; + + FurnaceType type = FureamMain.getFurnaceType(block); + if (type == null) return; + + Player player = event.getPlayer(); + if (player.isSneaking() && event.getItem() != null && !event.getItem().getType().isAir()) { + return; + } + + FureamWorldConfig config = FureamMain.getWorldConfig(block.getWorld()); + if (config == null || !config.getEnabledFurnaceTypes().contains(type)) { + return; + } + + event.setCancelled(true); + + FurnaceContext ctx = FurnaceManager.getOrCreateContext(block); + if (ctx == null) return; + + if (ctx.activeGui == null) { + ctx.activeGui = new FureamScreenInventory(block.getLocation(), type, ctx.data, config); + ctx.handler = new FureamScreenHandler(ctx.activeGui); + } + + ctx.activeGui.burnTime = ctx.burnTime; + ctx.activeGui.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); + ctx.activeGui.cookTime = ctx.cookTime; + ctx.activeGui.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); + ctx.activeGui.refreshVisuals(); + + player.openInventory(ctx.activeGui.bukkitInventory); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java new file mode 100644 index 0000000..9a028d8 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java @@ -0,0 +1,52 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; + +public class GuiListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryClick(InventoryClickEvent event) { + Inventory inv = event.getInventory(); + for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { + if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { + if (ctx.handler != null && event.getWhoClicked() instanceof Player) { + ctx.handler.handleClick(event, (Player) event.getWhoClicked()); + ctx.dirty = true; + } + return; + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onInventoryDrag(InventoryDragEvent event) { + Inventory inv = event.getInventory(); + for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { + if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { + if (ctx.handler != null) { + ctx.handler.handleDrag(event); + ctx.dirty = true; + } + return; + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onInventoryClose(InventoryCloseEvent event) { + Inventory inv = event.getInventory(); + for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { + if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { + FurnaceManager.saveToNbt(ctx); + return; + } + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java new file mode 100644 index 0000000..7ae7cf1 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java @@ -0,0 +1,54 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.logic.FuelTable; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.inventory.FurnaceInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class HopperListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInventoryMoveItem(InventoryMoveItemEvent event) { + Inventory dest = event.getDestination(); + Inventory src = event.getSource(); + + // Hopper pushing INTO Furnace + if (dest instanceof FurnaceInventory && dest.getLocation() != null) { + Location furnaceLoc = dest.getLocation(); + Block block = furnaceLoc.getBlock(); + FurnaceType type = FureamMain.getFurnaceType(block); + if (type != null) { + FureamWorldConfig config = FureamMain.getWorldConfig(furnaceLoc.getWorld()); + if (config != null && config.getEnabledFurnaceTypes().contains(type)) { + Location srcLoc = src.getLocation(); + ItemStack itemToMove = event.getItem(); + boolean isTop = srcLoc != null && srcLoc.getBlockY() > furnaceLoc.getBlockY(); + + if (isTop) { + // Top Hopper -> Inputs: Filter non-smeltables if configured + if (config.getPreventsHopperInsertNonSmeltable().contains(type)) { + if (!FureamFurnaceLogic.isAcceptableInput(furnaceLoc.getWorld(), itemToMove, type)) { + event.setCancelled(true); + } + } + } else { + // Side Hopper -> Fuels: Filter non-fuel items + if (!FuelTable.isFuel(itemToMove) && itemToMove.getType() != Material.BUCKET) { + event.setCancelled(true); + } + } + } + } + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java new file mode 100644 index 0000000..6d983a6 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java @@ -0,0 +1,190 @@ +package io.github.eat_ram.fuream.logic; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.EnumMap; +import java.util.Map; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class FuelTable { + private static final Map FUEL_TIMES = new EnumMap<>(Material.class); + + static { + loadDefaults(); + initDynamic(); + } + + public static void initDynamic() { + try { + // Dynamically synchronize with official server fuel registry if available + Class furnaceClass = Class.forName("net.minecraft.world.level.block.entity.TileEntityFurnace"); + Method getFuelMethod = null; + for (Method m : furnaceClass.getDeclaredMethods()) { + if (Modifier.isStatic(m.getModifiers()) && Map.class.isAssignableFrom(m.getReturnType()) && m.getParameterCount() == 0) { + getFuelMethod = m; + break; + } + } + if (getFuelMethod != null) { + getFuelMethod.setAccessible(true); + Map nmsFuelMap = (Map) getFuelMethod.invoke(null); + if (nmsFuelMap != null && !nmsFuelMap.isEmpty()) { + Class craftItemClass = Class.forName("org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers"); + Method getMaterialMethod = craftItemClass.getMethod("getMaterial", Class.forName("net.minecraft.world.item.Item")); + for (Map.Entry entry : nmsFuelMap.entrySet()) { + Material mat = (Material) getMaterialMethod.invoke(null, entry.getKey()); + if (mat != null && entry.getValue() != null && entry.getValue() > 0) { + FUEL_TIMES.put(mat, entry.getValue()); + } + } + } + } + } catch (Throwable ignored) { + // Graceful fallback to default vanilla table + } + } + + private static void loadDefaults() { + // High tier + FUEL_TIMES.put(Material.LAVA_BUCKET, 20000); + FUEL_TIMES.put(Material.COAL_BLOCK, 16000); + FUEL_TIMES.put(Material.DRIED_KELP_BLOCK, 4000); + FUEL_TIMES.put(Material.BLAZE_ROD, 2400); + FUEL_TIMES.put(Material.COAL, 1600); + FUEL_TIMES.put(Material.CHARCOAL, 1600); + + // Boats + Material[] boats = { + Material.OAK_BOAT, Material.SPRUCE_BOAT, Material.BIRCH_BOAT, Material.JUNGLE_BOAT, + Material.ACACIA_BOAT, Material.DARK_OAK_BOAT, Material.MANGROVE_BOAT, Material.CHERRY_BOAT, + Material.BAMBOO_RAFT, + Material.OAK_CHEST_BOAT, Material.SPRUCE_CHEST_BOAT, Material.BIRCH_CHEST_BOAT, Material.JUNGLE_CHEST_BOAT, + Material.ACACIA_CHEST_BOAT, Material.DARK_OAK_CHEST_BOAT, Material.MANGROVE_CHEST_BOAT, Material.CHERRY_CHEST_BOAT, + Material.BAMBOO_CHEST_RAFT + }; + for (Material m : boats) FUEL_TIMES.put(m, 1200); + + // Standard wood items (300 ticks) + Material[] woods300 = { + // Logs + Material.OAK_LOG, Material.SPRUCE_LOG, Material.BIRCH_LOG, Material.JUNGLE_LOG, + Material.ACACIA_LOG, Material.DARK_OAK_LOG, Material.MANGROVE_LOG, Material.CHERRY_LOG, + Material.STRIPPED_OAK_LOG, Material.STRIPPED_SPRUCE_LOG, Material.STRIPPED_BIRCH_LOG, Material.STRIPPED_JUNGLE_LOG, + Material.STRIPPED_ACACIA_LOG, Material.STRIPPED_DARK_OAK_LOG, Material.STRIPPED_MANGROVE_LOG, Material.STRIPPED_CHERRY_LOG, + // Woods + Material.OAK_WOOD, Material.SPRUCE_WOOD, Material.BIRCH_WOOD, Material.JUNGLE_WOOD, + Material.ACACIA_WOOD, Material.DARK_OAK_WOOD, Material.MANGROVE_WOOD, Material.CHERRY_WOOD, + Material.STRIPPED_OAK_WOOD, Material.STRIPPED_SPRUCE_WOOD, Material.STRIPPED_BIRCH_WOOD, Material.STRIPPED_JUNGLE_WOOD, + Material.STRIPPED_ACACIA_WOOD, Material.STRIPPED_DARK_OAK_WOOD, Material.STRIPPED_MANGROVE_WOOD, Material.STRIPPED_CHERRY_WOOD, + // Planks + Material.OAK_PLANKS, Material.SPRUCE_PLANKS, Material.BIRCH_PLANKS, Material.JUNGLE_PLANKS, + Material.ACACIA_PLANKS, Material.DARK_OAK_PLANKS, Material.MANGROVE_PLANKS, Material.CHERRY_PLANKS, + Material.BAMBOO_PLANKS, Material.BAMBOO_MOSAIC, + // Stairs + Material.OAK_STAIRS, Material.SPRUCE_STAIRS, Material.BIRCH_STAIRS, Material.JUNGLE_STAIRS, + Material.ACACIA_STAIRS, Material.DARK_OAK_STAIRS, Material.MANGROVE_STAIRS, Material.CHERRY_STAIRS, + Material.BAMBOO_STAIRS, Material.BAMBOO_MOSAIC_STAIRS, + // Fences & Gates + Material.OAK_FENCE, Material.SPRUCE_FENCE, Material.BIRCH_FENCE, Material.JUNGLE_FENCE, + Material.ACACIA_FENCE, Material.DARK_OAK_FENCE, Material.MANGROVE_FENCE, Material.CHERRY_FENCE, Material.BAMBOO_FENCE, + Material.OAK_FENCE_GATE, Material.SPRUCE_FENCE_GATE, Material.BIRCH_FENCE_GATE, Material.JUNGLE_FENCE_GATE, + Material.ACACIA_FENCE_GATE, Material.DARK_OAK_FENCE_GATE, Material.MANGROVE_FENCE_GATE, Material.CHERRY_FENCE_GATE, Material.BAMBOO_FENCE_GATE, + // Doors & Trapdoors + Material.OAK_DOOR, Material.SPRUCE_DOOR, Material.BIRCH_DOOR, Material.JUNGLE_DOOR, + Material.ACACIA_DOOR, Material.DARK_OAK_DOOR, Material.MANGROVE_DOOR, Material.CHERRY_DOOR, Material.BAMBOO_DOOR, + Material.OAK_TRAPDOOR, Material.SPRUCE_TRAPDOOR, Material.BIRCH_TRAPDOOR, Material.JUNGLE_TRAPDOOR, + Material.ACACIA_TRAPDOOR, Material.DARK_OAK_TRAPDOOR, Material.MANGROVE_TRAPDOOR, Material.CHERRY_TRAPDOOR, Material.BAMBOO_TRAPDOOR, + // Pressure plates & Buttons + Material.OAK_PRESSURE_PLATE, Material.SPRUCE_PRESSURE_PLATE, Material.BIRCH_PRESSURE_PLATE, Material.JUNGLE_PRESSURE_PLATE, + Material.ACACIA_PRESSURE_PLATE, Material.DARK_OAK_PRESSURE_PLATE, Material.MANGROVE_PRESSURE_PLATE, Material.CHERRY_PRESSURE_PLATE, + Material.BAMBOO_PRESSURE_PLATE, + // Signs & Hanging Signs + Material.OAK_SIGN, Material.SPRUCE_SIGN, Material.BIRCH_SIGN, Material.JUNGLE_SIGN, + Material.ACACIA_SIGN, Material.DARK_OAK_SIGN, Material.MANGROVE_SIGN, Material.CHERRY_SIGN, Material.BAMBOO_SIGN, + Material.OAK_HANGING_SIGN, Material.SPRUCE_HANGING_SIGN, Material.BIRCH_HANGING_SIGN, Material.JUNGLE_HANGING_SIGN, + Material.ACACIA_HANGING_SIGN, Material.DARK_OAK_HANGING_SIGN, Material.MANGROVE_HANGING_SIGN, Material.CHERRY_HANGING_SIGN, Material.BAMBOO_HANGING_SIGN, + // Blocks + Material.CRAFTING_TABLE, Material.BOOKSHELF, Material.CHISELED_BOOKSHELF, Material.CHEST, Material.TRAPPED_CHEST, + Material.BARREL, Material.DAYLIGHT_DETECTOR, Material.JUKEBOX, Material.NOTE_BLOCK, Material.COMPOSTER, + Material.LOOM, Material.FLETCHING_TABLE, Material.CARTOGRAPHY_TABLE, Material.SMITHING_TABLE, Material.LECTERN, + Material.BEEHIVE, + // Banners + Material.WHITE_BANNER, Material.ORANGE_BANNER, Material.MAGENTA_BANNER, Material.LIGHT_BLUE_BANNER, + Material.YELLOW_BANNER, Material.LIME_BANNER, Material.PINK_BANNER, Material.GRAY_BANNER, + Material.LIGHT_GRAY_BANNER, Material.CYAN_BANNER, Material.PURPLE_BANNER, Material.BLUE_BANNER, + Material.BROWN_BANNER, Material.GREEN_BANNER, Material.RED_BANNER, Material.BLACK_BANNER, + // Tools & Weapons + Material.WOODEN_AXE, Material.WOODEN_HOE, Material.WOODEN_PICKAXE, Material.WOODEN_SHOVEL, Material.WOODEN_SWORD, + Material.BOW, Material.CROSSBOW, Material.FISHING_ROD + }; + for (Material m : woods300) FUEL_TIMES.put(m, 300); + + // Slabs (150 ticks) + Material[] slabs150 = { + Material.OAK_SLAB, Material.SPRUCE_SLAB, Material.BIRCH_SLAB, Material.JUNGLE_SLAB, + Material.ACACIA_SLAB, Material.DARK_OAK_SLAB, Material.MANGROVE_SLAB, Material.CHERRY_SLAB, + Material.BAMBOO_SLAB, Material.BAMBOO_MOSAIC_SLAB + }; + for (Material m : slabs150) FUEL_TIMES.put(m, 150); + + // 200 ticks + Material[] items200 = { + Material.OAK_BUTTON, Material.SPRUCE_BUTTON, Material.BIRCH_BUTTON, Material.JUNGLE_BUTTON, + Material.ACACIA_BUTTON, Material.DARK_OAK_BUTTON, Material.MANGROVE_BUTTON, Material.CHERRY_BUTTON, + Material.BAMBOO_BUTTON, Material.BOWL, Material.LADDER + }; + for (Material m : items200) FUEL_TIMES.put(m, 200); + + // 100 ticks + Material[] items100 = { + Material.STICK, Material.OAK_SAPLING, Material.SPRUCE_SAPLING, Material.BIRCH_SAPLING, + Material.JUNGLE_SAPLING, Material.ACACIA_SAPLING, Material.DARK_OAK_SAPLING, Material.MANGROVE_PROPAGULE, + Material.CHERRY_SAPLING, Material.BAMBOO, Material.AZALEA, Material.FLOWERING_AZALEA + }; + for (Material m : items100) FUEL_TIMES.put(m, 100); + + // 67 ticks (wool, carpet) + Material[] items67 = { + Material.WHITE_WOOL, Material.ORANGE_WOOL, Material.MAGENTA_WOOL, Material.LIGHT_BLUE_WOOL, + Material.YELLOW_WOOL, Material.LIME_WOOL, Material.PINK_WOOL, Material.GRAY_WOOL, + Material.LIGHT_GRAY_WOOL, Material.CYAN_WOOL, Material.PURPLE_WOOL, Material.BLUE_WOOL, + Material.BROWN_WOOL, Material.GREEN_WOOL, Material.RED_WOOL, Material.BLACK_WOOL, + Material.WHITE_CARPET, Material.ORANGE_CARPET, Material.MAGENTA_CARPET, Material.LIGHT_BLUE_CARPET, + Material.YELLOW_CARPET, Material.LIME_CARPET, Material.PINK_CARPET, Material.GRAY_CARPET, + Material.LIGHT_GRAY_CARPET, Material.CYAN_CARPET, Material.PURPLE_CARPET, Material.BLUE_CARPET, + Material.BROWN_CARPET, Material.GREEN_CARPET, Material.RED_CARPET, Material.BLACK_CARPET + }; + for (Material m : items67) FUEL_TIMES.put(m, 67); + + // Scaffolding (50 ticks) + FUEL_TIMES.put(Material.SCAFFOLDING, 50); + } + + public static boolean isFuel(@Nullable ItemStack stack) { + if (stack == null || stack.getType().isAir()) return false; + return isFuel(stack.getType()); + } + + public static boolean isFuel(@Nullable Material material) { + if (material == null || material.isAir()) return false; + return FUEL_TIMES.containsKey(material); + } + + public static int getFuelTime(@Nullable ItemStack stack) { + if (stack == null || stack.getType().isAir()) return 0; + return getFuelTime(stack.getType()); + } + + public static int getFuelTime(@Nullable Material material) { + if (material == null || material.isAir()) return 0; + return FUEL_TIMES.getOrDefault(material, 0); + } + + private FuelTable() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java new file mode 100644 index 0000000..223e30b --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -0,0 +1,350 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.List; +import java.util.Map; + +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.util.KeyableItemStack; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Furnace; +import org.bukkit.inventory.CookingRecipe; +import org.bukkit.inventory.FurnaceInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class FureamFurnaceEngine { + public static void tick( + @NotNull FurnaceContext ctx, @Nullable FureamWorldConfig config + ) { + FureamFurnaceData data = ctx.data; + World world = ctx.getWorld(); + if (world == null) return; + + // Ensure virtual lists have at least the configured number of slots + int inCount = config != null ? config.getInputSlotCount().get(ctx.type) : 18; + int fuelCount = config != null ? config.getFuelSlotCount().get(ctx.type) : 18; + int outCount = config != null ? config.getOutputSlotCount().get(ctx.type) : 18; + while (data.inputs.size() < inCount) data.inputs.add(new ItemStack(Material.AIR)); + while (data.fuels.size() < fuelCount) data.fuels.add(new ItemStack(Material.AIR)); + while (data.outputs.size() < outCount) data.outputs.add(new ItemStack(Material.AIR)); + + // Always compute cookTimeTotal according to current rate factor + int baseCookTime = ctx.type == FurnaceType.FURNACE ? 200 : 100; + double rateUpgradeFactor = ctx.getRateUpgradeFactor(); + ctx.cookTimeTotal = Math.max(1, (int) Math.ceil(baseCookTime / rateUpgradeFactor)); + + // Ingest and feed through vanilla furnace inventory (for seamless hopper integration) + Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); + BlockState blockState = block.getState(); + if (blockState instanceof org.bukkit.block.Furnace) { + org.bukkit.block.Furnace furnaceTile = (org.bukkit.block.Furnace) blockState; + FurnaceInventory inv = furnaceTile.getInventory(); + + // 1. Ingest smelting slot (top hopper input) + ItemStack smelting = inv.getSmelting(); + if (smelting != null && !smelting.getType().isAir() && smelting.getAmount() > 0) { + ItemStack remainder = insertStackIntoList(data.inputs, smelting, inCount); + inv.setSmelting(remainder.getType().isAir() ? null : remainder); + ctx.dirty = true; + } + + // 2. Ingest fuel slot (side hopper fuel input) + ItemStack fuel = inv.getFuel(); + if (fuel != null && !fuel.getType().isAir() && fuel.getAmount() > 0) { + ItemStack remainder = insertStackIntoList(data.fuels, fuel, fuelCount); + inv.setFuel(remainder.getType().isAir() ? null : remainder); + ctx.dirty = true; + } + + // 3. Feed outputs into result slot (bottom hopper output extraction) + ItemStack result = inv.getResult(); + if (result == null || result.getType().isAir()) { + ItemStack nextOut = extractFirstNonEmpty(data.outputs); + if (nextOut != null) { + inv.setResult(nextOut); + ctx.dirty = true; + } + } + + furnaceTile.setCookTime((short) 0); + furnaceTile.setBurnTime((short) 0); + } + + boolean wasBurning = ctx.burnTime > 0; + if (ctx.burnTime > 0) { + ctx.burnTime--; + ctx.dirty = true; + } + + // Find first valid input stack + int firstInputIdx = -1; + ItemStack inputStack = null; + for (int i = 0; i < data.inputs.size(); i++) { + ItemStack s = data.inputs.get(i); + if (s != null && !s.getType().isAir() && s.getAmount() > 0) { + firstInputIdx = i; + inputStack = s; + break; + } + } + + CookingRecipe matchedRecipe = null; + if (inputStack != null) { + matchedRecipe = getRecipeForInput(world, inputStack, ctx); + } + + boolean canSmelt = false; + ItemStack recipeResult = null; + int outSlotIdx = -1; + + if (matchedRecipe != null && inputStack != null) { + recipeResult = matchedRecipe.getResult(); + outSlotIdx = findFittingOutputSlot(data.outputs, recipeResult); + if (outSlotIdx >= 0) { + canSmelt = true; + } + } + + // Light the furnace if unlit and can smelt + if (ctx.burnTime <= 0 && canSmelt) { + int firstFuelIdx = -1; + ItemStack fuelStack = null; + for (int i = 0; i < data.fuels.size(); i++) { + ItemStack s = data.fuels.get(i); + if (s != null && !s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { + firstFuelIdx = i; + fuelStack = s; + break; + } + } + + if (fuelStack != null && firstFuelIdx >= 0) { + int baseFuelTime = FuelTable.getFuelTime(fuelStack); + double fuelUpgradeFactor = ctx.getFuelUpgradeFactor(); + int fuelTime = (int) Math.round(baseFuelTime * fuelUpgradeFactor); + + ctx.burnTime = fuelTime; + ctx.fuelTimeTotal = fuelTime; + ctx.dirty = true; + + // Handle recipe remainder (e.g. Lava Bucket -> Bucket in fuel slot) + if (fuelStack.getType() == Material.LAVA_BUCKET) { + data.fuels.set(firstFuelIdx, new ItemStack(Material.BUCKET)); + } else { + fuelStack.setAmount(fuelStack.getAmount() - 1); + if (fuelStack.getAmount() <= 0) { + data.fuels.set(firstFuelIdx, new ItemStack(Material.AIR)); + } + } + } + } + + // Cook progression + if (ctx.burnTime > 0 && canSmelt && matchedRecipe != null && inputStack != null && recipeResult != null && outSlotIdx >= 0) { + ctx.cookTime++; + ctx.dirty = true; + + if (ctx.cookTime >= ctx.cookTimeTotal) { + ctx.cookTime = 0; + + // Deposit result + ItemStack currentOut = data.outputs.get(outSlotIdx); + if (currentOut == null || currentOut.getType().isAir()) { + data.outputs.set(outSlotIdx, recipeResult.clone()); + } else if (currentOut.isSimilar(recipeResult)) { + currentOut.setAmount(currentOut.getAmount() + recipeResult.getAmount()); + data.outputs.set(outSlotIdx, currentOut); + } + + // Stash experience + FureamFurnaceLogic.stashExperience(matchedRecipe, data); + + // Wet sponge drying water bucket logic + if (inputStack.getType() == Material.WET_SPONGE) { + for (int i = 0; i < data.fuels.size(); i++) { + ItemStack f = data.fuels.get(i); + if (f != null && f.getType() == Material.BUCKET) { + f.setAmount(f.getAmount() - 1); + if (f.getAmount() <= 0) { + data.fuels.set(i, new ItemStack(Material.WATER_BUCKET)); + } else { + data.fuels.add(new ItemStack(Material.WATER_BUCKET)); + } + break; + } + } + } + + // Decrement input + inputStack.setAmount(inputStack.getAmount() - 1); + if (inputStack.getAmount() <= 0) { + data.inputs.set(firstInputIdx, new ItemStack(Material.AIR)); + } + + // Deduct fuel time compensation so that speed upgrade does not artificially multiply fuel capacity + int extraBurnTimeCost = baseCookTime - ctx.cookTimeTotal; + if (extraBurnTimeCost > 0) { + ctx.burnTime -= extraBurnTimeCost; + while (ctx.burnTime <= 0) { + int nextFuelIdx = -1; + ItemStack nextFuelStack = null; + for (int i = 0; i < data.fuels.size(); i++) { + ItemStack s = data.fuels.get(i); + if (s != null && !s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { + nextFuelIdx = i; + nextFuelStack = s; + break; + } + } + if (nextFuelStack == null) { + break; + } + int nextBaseFuelTime = FuelTable.getFuelTime(nextFuelStack); + double nextFuelFactor = ctx.getFuelUpgradeFactor(); + int nextFuelTime = (int) Math.round(nextBaseFuelTime * nextFuelFactor); + if (nextFuelTime <= 0) { + break; + } + ctx.burnTime += nextFuelTime; + ctx.fuelTimeTotal = nextFuelTime; + if (nextFuelStack.getType() == Material.LAVA_BUCKET) { + data.fuels.set(nextFuelIdx, new ItemStack(Material.BUCKET)); + } else { + nextFuelStack.setAmount(nextFuelStack.getAmount() - 1); + if (nextFuelStack.getAmount() <= 0) { + data.fuels.set(nextFuelIdx, new ItemStack(Material.AIR)); + } + } + } + if (ctx.burnTime < 0) { + ctx.burnTime = 0; + } + } + } + } else if (ctx.cookTime > 0) { + ctx.cookTime = Math.max(0, ctx.cookTime - 2); + ctx.dirty = true; + } + + // Update block lit state + boolean isBurning = ctx.burnTime > 0; + if (wasBurning != isBurning) { + updateBlockLitState(world, ctx.pos.x, ctx.pos.y, ctx.pos.z, isBurning); + } + + // Sync with open GUI + if (ctx.activeGui != null) { + ctx.activeGui.burnTime = ctx.burnTime; + ctx.activeGui.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); + ctx.activeGui.cookTime = ctx.cookTime; + ctx.activeGui.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); + ctx.activeGui.refreshVisuals(); + } + } + + public static @NotNull ItemStack insertStackIntoList( + @NotNull List list, @NotNull ItemStack incoming, int maxSlots + ) { + while (list.size() < maxSlots) { + list.add(new ItemStack(Material.AIR)); + } + + ItemStack remaining = incoming.clone(); + + // 1. Stack into existing slots + for (int i = 0; i < maxSlots && i < list.size(); i++) { + ItemStack s = list.get(i); + if (s != null && !s.getType().isAir() && s.isSimilar(remaining)) { + int space = s.getMaxStackSize() - s.getAmount(); + if (space > 0) { + int toAdd = Math.min(space, remaining.getAmount()); + s.setAmount(s.getAmount() + toAdd); + remaining.setAmount(remaining.getAmount() - toAdd); + if (remaining.getAmount() <= 0) { + return new ItemStack(Material.AIR); + } + } + } + } + + // 2. Place into empty slots + for (int i = 0; i < maxSlots && i < list.size(); i++) { + ItemStack s = list.get(i); + if (s == null || s.getType().isAir()) { + list.set(i, remaining.clone()); + return new ItemStack(Material.AIR); + } + } + + return remaining; + } + + public static @Nullable ItemStack extractFirstNonEmpty(@NotNull List list) { + for (int i = 0; i < list.size(); i++) { + ItemStack s = list.get(i); + if (s != null && !s.getType().isAir() && s.getAmount() > 0) { + ItemStack single = s.clone(); + single.setAmount(1); + s.setAmount(s.getAmount() - 1); + if (s.getAmount() <= 0) { + list.set(i, new ItemStack(Material.AIR)); + } + return single; + } + } + return null; + } + + private static CookingRecipe getRecipeForInput( + World world, ItemStack input, FurnaceContext ctx + ) { + NamespacedKey overridden = ctx.data.overriddenRecipes.get(new KeyableItemStack(input)); + if (overridden != null) { + List> matches = FureamFurnaceLogic.findAllMatches(world, input, ctx.type); + for (CookingRecipe r : matches) { + if (r.getKey().equals(overridden)) { + return r; + } + } + } + return FureamFurnaceLogic.findRecipe(world, input, ctx.type).orElse(null); + } + + private static int findFittingOutputSlot(List outputs, ItemStack result) { + for (int i = 0; i < outputs.size(); i++) { + ItemStack s = outputs.get(i); + if (s == null || s.getType().isAir()) { + return i; + } + if (s.isSimilar(result) && s.getAmount() + result.getAmount() <= s.getMaxStackSize()) { + return i; + } + } + return -1; + } + + private static void updateBlockLitState(World world, int x, int y, int z, boolean lit) { + Block block = world.getBlockAt(x, y, z); + BlockData data = block.getBlockData(); + if (data instanceof Furnace) { + Furnace furnaceData = (Furnace) data; + if (furnaceData.isLit() != lit) { + furnaceData.setLit(lit); + block.setBlockData(furnaceData, false); + } + } + } + + private FureamFurnaceEngine() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java new file mode 100644 index 0000000..22ce614 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java @@ -0,0 +1,146 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.ExperienceOrb; +import org.bukkit.entity.Player; +import org.bukkit.inventory.BlastingRecipe; +import org.bukkit.inventory.CookingRecipe; +import org.bukkit.inventory.FurnaceRecipe; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.SmokingRecipe; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class FureamFurnaceLogic { + public static boolean isFuel(@Nullable ItemStack stack) { + return FuelTable.isFuel(stack); + } + + public static boolean isAcceptableInput( + @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType + ) { + return findRecipe(world, stack, furnaceType).isPresent(); + } + + public static Optional> findRecipe( + @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType + ) { + if (stack.getType().isAir()) { + return Optional.empty(); + } + Iterator it = Bukkit.recipeIterator(); + while (it.hasNext()) { + Recipe r = it.next(); + if (isRecipeMatchForFurnace(r, furnaceType, stack)) { + return Optional.of((CookingRecipe) r); + } + } + return Optional.empty(); + } + + public static List> findAllMatches( + @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType + ) { + List> list = new ArrayList<>(); + if (stack.getType().isAir()) { + return list; + } + Iterator it = Bukkit.recipeIterator(); + while (it.hasNext()) { + Recipe r = it.next(); + if (isRecipeMatchForFurnace(r, furnaceType, stack)) { + list.add((CookingRecipe) r); + } + } + return list; + } + + private static boolean isRecipeMatchForFurnace(Recipe r, FurnaceType furnaceType, ItemStack stack) { + if (!(r instanceof CookingRecipe)) { + return false; + } + CookingRecipe cr = (CookingRecipe) r; + if (furnaceType == FurnaceType.SMOKER) { + if (!(cr instanceof SmokingRecipe)) return false; + } else if (furnaceType == FurnaceType.BLAST_FURNACE) { + if (!(cr instanceof BlastingRecipe)) return false; + } else { + if (!(cr instanceof FurnaceRecipe)) return false; + } + return cr.getInputChoice().test(stack); + } + + public static void stashExperience( + @Nullable CookingRecipe recipe, @NotNull FureamFurnaceData data + ) { + if (recipe != null) { + data.experience += recipe.getExperience(); + } + } + + public static void grantExperience( + @NotNull Player player, @NotNull Location loc, @NotNull FureamFurnaceData data + ) { + int amount = (int) data.experience; + if (amount > 0) { + data.experience -= amount; + World world = player.getWorld(); + world.spawn(player.getLocation(), ExperienceOrb.class, orb -> orb.setExperience(amount)); + } + } + + public static void dropOnBreak( + @NotNull Location pos, @NotNull FureamFurnaceData data + ) { + World world = pos.getWorld(); + if (world == null) return; + Location center = pos.clone().add(0.5, 0.5, 0.5); + + for (ItemStack s : data.inputs) { + dropIfPresent(world, center, s); + } + data.inputs.clear(); + + for (ItemStack s : data.fuels) { + dropIfPresent(world, center, s); + } + data.fuels.clear(); + + for (ItemStack s : data.outputs) { + dropIfPresent(world, center, s); + } + data.outputs.clear(); + + if (data.recipeOverridingInput != null && !data.recipeOverridingInput.getType().isAir()) { + dropIfPresent(world, center, data.recipeOverridingInput); + data.recipeOverridingInput = new ItemStack(Material.AIR); + } + + int amount = (int) Math.floor(data.experience); + if (amount > 0) { + data.experience -= amount; + world.spawn(center, ExperienceOrb.class, orb -> orb.setExperience(amount)); + } + } + + private static void dropIfPresent(@NotNull World world, @NotNull Location loc, @Nullable ItemStack stack) { + if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { + world.dropItemNaturally(loc, stack.clone()); + } + } + + private FureamFurnaceLogic() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamFunctionalArea.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamFunctionalArea.java new file mode 100644 index 0000000..bdf455b --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamFunctionalArea.java @@ -0,0 +1,31 @@ +package io.github.eat_ram.fuream.screen; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public interface FureamFunctionalArea { + @NotNull ItemStack getGuiStack(FureamScreenInventory inv, int slot); + + void setGuiStack(FureamScreenInventory inv, int slot, ItemStack newStack); + + void onGuiClick( + SlotClick slotClick, FureamScreenInventory display, int slot, + int button, ClickType clickType, Player player + ); + + boolean isStorable(FureamScreenInventory inv, int slot); + + boolean isTakable(FureamScreenInventory inv, int slot); + + void refreshVisuals(FureamScreenInventory inv); + + @FunctionalInterface + interface SlotClick { + void onSlotClick( + int slotIndex, int button, ClickType clickType, + Player player + ); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java new file mode 100644 index 0000000..9e0b68f --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java @@ -0,0 +1,375 @@ +package io.github.eat_ram.fuream.screen; + +import java.util.Map; + +import io.github.eat_ram.fuream.hook.FurnaceManager; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class FureamScreenHandler { + private final @NotNull FureamScreenInventory display; + + public FureamScreenHandler(@NotNull FureamScreenInventory display) { + this.display = display; + } + + public void handleClick(InventoryClickEvent event, Player player) { + int rawSlot = event.getRawSlot(); + ClickType clickType = event.getClick(); + InventoryAction action = event.getAction(); + + if (rawSlot < 0) { + return; + } + + // Click in player inventory with shift-click into custom GUI + if (rawSlot >= 54) { + if (event.isShiftClick()) { + event.setCancelled(true); + ItemStack current = event.getCurrentItem(); + if (current != null && !current.getType().isAir()) { + ItemStack remaining = quickMoveToContainer(current); + event.setCurrentItem(remaining.getType().isAir() ? null : remaining); + this.display.syncToBukkitInventory(); + } + } + return; + } + + // Click is within the 54-slot furnace GUI + event.setCancelled(true); + + if (this.display.isFunctionalAreaSlot(rawSlot)) { + int ca = this.display.getCurrentFunctionalArea(); + if (ca < this.display.functionalAreas.size()) { + FureamFunctionalArea area = this.display.functionalAreas.get(ca); + area.onGuiClick( + (sIdx, btn, ct, p) -> handleFunctionalSlotDefault(rawSlot, sIdx, event, p), + this.display, rawSlot - 45, event.getHotbarButton(), clickType, player + ); + this.display.syncToBukkitInventory(); + } + return; + } + + if (this.display.isBarOrDecorSlot(rawSlot)) { + switch (rawSlot) { + case 51: { + int size = this.display.functionalAreas.size(); + if (size > 0) { + int na = this.display.getCurrentFunctionalArea() + 1; + this.display.setCurrentFunctionalArea(na < size ? na : 0); + } + break; + } + case 52: + this.display.switchPage(this.display.getPage() - 1); + break; + case 53: + this.display.switchPage(this.display.getPage() + 1); + break; + } + return; + } + + if (this.display.isOutputSlot(rawSlot)) { + onOutputSlotClick(rawSlot, event, player); + this.display.syncToBukkitInventory(); + return; + } + + if (this.display.isInputSlot(rawSlot) || this.display.isFuelRowSlot(rawSlot)) { + onMaterialSlotClick(rawSlot, event, player); + this.display.syncToBukkitInventory(); + return; + } + } + + private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryClickEvent event, Player player) { + int ca = this.display.getCurrentFunctionalArea(); + if (ca >= this.display.functionalAreas.size()) return; + FureamFunctionalArea area = this.display.functionalAreas.get(ca); + if (!area.isStorable(this.display, rawSlot - 45) && !area.isTakable(this.display, rawSlot - 45)) return; + + ItemStack cursor = event.getCursor(); + ItemStack current = area.getGuiStack(this.display, rawSlot - 45); + ClickType clickType = event.getClick(); + + // 1. Shift click + if (event.isShiftClick()) { + if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + Map leftover = player.getInventory().addItem(current.clone()); + if (leftover.isEmpty()) { + area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + } else { + current.setAmount(leftover.values().iterator().next().getAmount()); + area.setGuiStack(this.display, rawSlot - 45, current); + } + } + markDirtyAndSave(); + return; + } + + // 2. Number key (Hotbar 1-9) + if (clickType == ClickType.NUMBER_KEY) { + int hotbarSlot = event.getHotbarButton(); + if (hotbarSlot >= 0 && hotbarSlot < 9) { + ItemStack hotbarItem = player.getInventory().getItem(hotbarSlot); + if (hotbarItem == null || hotbarItem.getType().isAir()) { + if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + player.getInventory().setItem(hotbarSlot, current.clone()); + area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + } + } else if (area.isStorable(this.display, rawSlot - 45)) { + if (current == null || current.getType().isAir()) { + area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); + player.getInventory().setItem(hotbarSlot, new ItemStack(Material.AIR)); + } else if (area.isTakable(this.display, rawSlot - 45)) { + area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); + player.getInventory().setItem(hotbarSlot, current.clone()); + } + } + } + markDirtyAndSave(); + return; + } + + // 3. Cursor empty -> Take item from slot + if (cursor == null || cursor.getType().isAir()) { + if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (event.isRightClick()) { + int half = (current.getAmount() + 1) / 2; + ItemStack take = current.clone(); + take.setAmount(half); + current.setAmount(current.getAmount() - half); + event.getView().setCursor(take); + area.setGuiStack(this.display, rawSlot - 45, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); + } else { + event.getView().setCursor(current.clone()); + area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + } + } + } else { + // 4. Cursor has item -> Place/Swap into slot + if (area.isStorable(this.display, rawSlot - 45)) { + if (current == null || current.getType().isAir()) { + if (event.isRightClick()) { + ItemStack putOne = cursor.clone(); + putOne.setAmount(1); + cursor.setAmount(cursor.getAmount() - 1); + area.setGuiStack(this.display, rawSlot - 45, putOne); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } else { + area.setGuiStack(this.display, rawSlot - 45, cursor.clone()); + event.getView().setCursor(new ItemStack(Material.AIR)); + } + } else if (current.isSimilar(cursor)) { + int max = current.getMaxStackSize(); + if (event.isRightClick()) { + if (current.getAmount() < max) { + current.setAmount(current.getAmount() + 1); + cursor.setAmount(cursor.getAmount() - 1); + area.setGuiStack(this.display, rawSlot - 45, current); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } + } else { + int space = max - current.getAmount(); + int move = Math.min(space, cursor.getAmount()); + if (move > 0) { + current.setAmount(current.getAmount() + move); + cursor.setAmount(cursor.getAmount() - move); + area.setGuiStack(this.display, rawSlot - 45, current); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } + } + } else if (area.isTakable(this.display, rawSlot - 45)) { + area.setGuiStack(this.display, rawSlot - 45, cursor.clone()); + event.getView().setCursor(current.clone()); + } + } + } + markDirtyAndSave(); + } + + private void markDirtyAndSave() { + FurnacePos pos = new FurnacePos(this.display.location); + FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); + if (ctx != null) { + ctx.dirty = true; + FurnaceManager.saveToNbt(ctx); + } + } + + private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player player) { + ItemStack current = this.display.getStack(slotIndex); + if (current == null || current.getType().isAir()) return; + + if (event.isShiftClick()) { + Map leftover = player.getInventory().addItem(current.clone()); + if (leftover.isEmpty()) { + this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + } else { + current.setAmount(leftover.values().iterator().next().getAmount()); + this.display.setStack(slotIndex, current); + } + markDirtyAndSave(); + return; + } + + ItemStack cursor = event.getCursor(); + if (cursor == null || cursor.getType().isAir()) { + event.getView().setCursor(current.clone()); + this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + } else if (cursor.isSimilar(current)) { + int max = cursor.getMaxStackSize(); + int space = max - cursor.getAmount(); + int move = Math.min(space, current.getAmount()); + if (move > 0) { + cursor.setAmount(cursor.getAmount() + move); + current.setAmount(current.getAmount() - move); + event.getView().setCursor(cursor); + this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); + } + } + markDirtyAndSave(); + } + + private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Player player) { + boolean isFuel = this.display.isFuelRowSlot(slotIndex); + ItemStack cursor = event.getCursor(); + ItemStack current = this.display.getStack(slotIndex); + + if (event.isShiftClick()) { + if (current != null && !current.getType().isAir()) { + Map leftover = player.getInventory().addItem(current.clone()); + if (leftover.isEmpty()) { + this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + } else { + current.setAmount(leftover.values().iterator().next().getAmount()); + this.display.setStack(slotIndex, current); + } + } + markDirtyAndSave(); + return; + } + + if (cursor == null || cursor.getType().isAir()) { + if (current != null && !current.getType().isAir()) { + if (event.isRightClick()) { + int half = (current.getAmount() + 1) / 2; + ItemStack take = current.clone(); + take.setAmount(half); + current.setAmount(current.getAmount() - half); + event.getView().setCursor(take); + this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); + } else { + event.getView().setCursor(current.clone()); + this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + } + } + } else { + if (isFuel && !FureamFurnaceLogic.isFuel(cursor)) { + return; + } + if (current == null || current.getType().isAir()) { + if (event.isRightClick()) { + ItemStack putOne = cursor.clone(); + putOne.setAmount(1); + cursor.setAmount(cursor.getAmount() - 1); + this.display.setStack(slotIndex, putOne); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } else { + this.display.setStack(slotIndex, cursor.clone()); + event.getView().setCursor(new ItemStack(Material.AIR)); + } + } else if (current.isSimilar(cursor)) { + int max = current.getMaxStackSize(); + if (event.isRightClick()) { + if (current.getAmount() < max) { + current.setAmount(current.getAmount() + 1); + cursor.setAmount(cursor.getAmount() - 1); + this.display.setStack(slotIndex, current); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } + } else { + int space = max - current.getAmount(); + int move = Math.min(space, cursor.getAmount()); + if (move > 0) { + current.setAmount(current.getAmount() + move); + cursor.setAmount(cursor.getAmount() - move); + this.display.setStack(slotIndex, current); + event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); + } + } + } else { + this.display.setStack(slotIndex, cursor.clone()); + event.getView().setCursor(current.clone()); + } + } + markDirtyAndSave(); + } + + private @NotNull ItemStack quickMoveToContainer(@NotNull ItemStack original) { + ItemStack remaining = original.clone(); + if (FureamFurnaceLogic.isAcceptableInput(this.display.location.getWorld(), remaining, this.display.furnaceType)) { + int stop = this.display.getInputSlotStop(); + for (int i = 0; i < stop; i++) { + remaining = insertIntoSlot(i, remaining); + if (remaining.getType().isAir() || remaining.getAmount() <= 0) { + markDirtyAndSave(); + return new ItemStack(Material.AIR); + } + } + } + if (FureamFurnaceLogic.isFuel(remaining)) { + int fuelStart = 18; + int stop = this.display.getFuelSlotStop(); + for (int i = fuelStart; i < stop; i++) { + remaining = insertIntoSlot(i, remaining); + if (remaining.getType().isAir() || remaining.getAmount() <= 0) { + markDirtyAndSave(); + return new ItemStack(Material.AIR); + } + } + } + markDirtyAndSave(); + return remaining; + } + + private @NotNull ItemStack insertIntoSlot(int slotIndex, @NotNull ItemStack stack) { + ItemStack current = this.display.getStack(slotIndex); + if (current == null || current.getType().isAir()) { + this.display.setStack(slotIndex, stack.clone()); + return new ItemStack(Material.AIR); + } + if (current.isSimilar(stack)) { + int max = current.getMaxStackSize(); + int space = max - current.getAmount(); + int move = Math.min(space, stack.getAmount()); + if (move > 0) { + current.setAmount(current.getAmount() + move); + stack.setAmount(stack.getAmount() - move); + this.display.setStack(slotIndex, current); + } + } + return stack.getAmount() > 0 ? stack : new ItemStack(Material.AIR); + } + + public void handleDrag(@NotNull InventoryDragEvent event) { + for (int rawSlot : event.getRawSlots()) { + if (rawSlot < 54) { + event.setCancelled(true); + return; + } + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java new file mode 100644 index 0000000..abcbdce --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -0,0 +1,373 @@ +package io.github.eat_ram.fuream.screen; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import io.github.eat_ram.fuream.hook.FurnaceManager; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +/** + * The 54-slot view representing the virtual furnace GUI. + * + *

Row layout: + *

+ *  0..8   inputs (row 1)
+ *  9..17  fuel remaining bar (row 2)
+ * 18..26  fuels (row 3)
+ * 27..35  smelt progress bar (row 4)
+ * 36..44  outputs (row 5)
+ * 45..53  functional area & decor (row 6)
+ * 
+ */ +public class FureamScreenInventory { + public static final int COLS = 9; + public static final int SIZE = 54; + public static final ArrayList<@NotNull BiFunction< + ? super FureamScreenInventory, ? super FureamWorldConfig, + ? extends @NotNull FureamFunctionalArea + >> FUNCTIONAL_AREA_REGISTRY = new ArrayList<>(); + + public final @NotNull FurnaceType furnaceType; + public final @Nullable FureamWorldConfig config; + public final @NotNull ItemStack border; + public final @NotNull Material fuelLeftItem; + public final @NotNull Material fuelUsedItem; + public final @NotNull Material progressDoneItem; + public final @NotNull Material progressRemainingItem; + public final @NotNull ArrayList<@NotNull FureamFunctionalArea> functionalAreas; + private @Range(from = 0, to = Integer.MAX_VALUE) int currentFunctionalArea; + public final @NotNull Material nextFunctionalItem; + public final @NotNull Material prevPageItem; + public final @NotNull Material nextPageItem; + + public final @Range(from = 1, to = Integer.MAX_VALUE) int inputSlots; + public final @Range(from = 1, to = Integer.MAX_VALUE) int fuelSlots; + public final @Range(from = 1, to = Integer.MAX_VALUE) int outputSlots; + public final @NotNull Location location; + public final @NotNull FureamFurnaceData data; + + public int burnTime; + public int fuelTimeTotal; + public int cookTime; + public int cookTimeTotal = 200; + + public final @NotNull ItemStack @NotNull[] fuelBar = new ItemStack[COLS]; + public final @NotNull ItemStack @NotNull[] progBar = new ItemStack[COLS]; + public @NotNull ItemStack nextFunctionalBtn = new ItemStack(Material.AIR); + public @NotNull ItemStack prevPageBtn = new ItemStack(Material.AIR); + public @NotNull ItemStack nextPageBtn = new ItemStack(Material.AIR); + private @Range(from = 0, to = Integer.MAX_VALUE) int page; + public final @NotNull Inventory bukkitInventory; + + public FureamScreenInventory( + @NotNull Location location, @NotNull FurnaceType type, + @NotNull FureamFurnaceData data, @Nullable FureamWorldConfig config + ) { + this.location = location; + this.furnaceType = type; + this.data = data; + this.config = config; + this.functionalAreas = new ArrayList<>(); + + int inputSlotCount = config != null ? config.getInputSlotCount().get(type) : 9; + int fuelSlotCount = config != null ? config.getFuelSlotCount().get(type) : 9; + int outputSlotCount = config != null ? config.getOutputSlotCount().get(type) : 9; + + this.inputSlots = Math.max(1, inputSlotCount); + this.fuelSlots = Math.max(1, fuelSlotCount); + this.outputSlots = Math.max(1, outputSlotCount); + + this.fuelLeftItem = parseMaterial(config != null ? config.getGuiFuelLeftItemId().get(type) : null, Material.ORANGE_STAINED_GLASS_PANE); + this.fuelUsedItem = parseMaterial(config != null ? config.getGuiFuelUsedItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); + this.progressDoneItem = parseMaterial(config != null ? config.getGuiProgressDoneItemId().get(type) : null, Material.WHITE_STAINED_GLASS_PANE); + this.progressRemainingItem = parseMaterial(config != null ? config.getGuiProgressRemainingItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); + this.nextFunctionalItem = parseMaterial(config != null ? config.getGuiNextFunctionalAreaItemId().get(type) : null, Material.COMMAND_BLOCK); + this.prevPageItem = parseMaterial(config != null ? config.getGuiPrevPageItemId().get(type) : null, Material.ARROW); + this.nextPageItem = parseMaterial(config != null ? config.getGuiNextPageItemId().get(type) : null, Material.ARROW); + + Material borderMat = parseMaterial(config != null ? config.getGuiBorderItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); + this.border = makeTooltipItem( + new ItemStack(borderMat, 1), + config != null ? config.getGuiBorderItemTitle().get(type) : "GUI Border" + ); + + String title = type == FurnaceType.SMOKER ? "Smoker" : (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace"); + this.bukkitInventory = Bukkit.createInventory(null, 54, title); + + this.cookTimeTotal = computeCookTimeTotal(); + this.switchPage(0); + for (BiFunction< + ? super FureamScreenInventory, ? super FureamWorldConfig, + ? extends FureamFunctionalArea + > factory : FUNCTIONAL_AREA_REGISTRY) { + this.functionalAreas.add(factory.apply(this, this.config)); + } + + this.nextFunctionalBtn = this.functionalAreas.size() < 2 ? this.border.clone() : + makeTooltipItem( + new ItemStack(this.nextFunctionalItem, 1), + config != null ? config.getGuiNextFunctionalAreaItemTitle().get(type) : "Next Functional Area" + ); + + this.refreshVisuals(); + } + + private static Material parseMaterial(@Nullable String id, Material fallback) { + if (id == null) return fallback; + String name = id.startsWith("minecraft:") ? id.substring(10) : id; + Material mat = Material.matchMaterial(name.toUpperCase()); + return mat != null ? mat : fallback; + } + + public int computeCookTimeTotal() { + int base = this.furnaceType == FurnaceType.FURNACE ? 200 : 100; + FurnacePos pos = new FurnacePos(this.location); + FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); + if (ctx != null) { + double factor = ctx.getRateUpgradeFactor(); + return Math.max(1, (int) Math.ceil(base / factor)); + } + return base; + } + + public boolean isInputSlot(int slot) { + return slot >= 0 && slot < 9 && this.page * 9 + slot < this.inputSlots; + } + + public boolean isFuelRowSlot(int slot) { + return slot >= 18 && slot < 27 && this.page * 9 + slot - 18 < this.fuelSlots; + } + + public boolean isOutputSlot(int slot) { + return slot >= 36 && slot < 45 && this.page * 9 + slot - 36 < this.outputSlots; + } + + public boolean isFunctionalAreaSlot(int slot) { + return slot >= 45 && slot < 51; + } + + public boolean isBarOrDecorSlot(int slot) { + return !(this.isInputSlot(slot) || this.isFuelRowSlot(slot) || + this.isOutputSlot(slot) || this.isFunctionalAreaSlot(slot)); + } + + public int getInputSlotStop() { + int diff = this.inputSlots - (this.page * 9); + return diff >= 9 ? 9 : (diff <= 0 ? 0 : diff); + } + + public int getFuelSlotStop() { + int diff = this.fuelSlots - (this.page * 9); + return diff >= 9 ? 27 : (diff <= 0 ? 18 : diff + 18); + } + + public int getOutputSlotStop() { + int diff = this.outputSlots - (this.page * 9); + return diff >= 9 ? 45 : (diff <= 0 ? 36 : diff + 36); + } + + public void refreshVisuals() { + this.cookTimeTotal = computeCookTimeTotal(); + if (this.cookTime >= this.cookTimeTotal) { + this.cookTime = Math.max(0, this.cookTimeTotal - 1); + } + + int fuelCount = barCount(this.burnTime, this.fuelTimeTotal); + int progressCount = barCount(this.cookTime, this.cookTimeTotal); + + for (int i = 0; i < COLS; ++i) { + this.fuelBar[i] = this.addFuelTooltip(new ItemStack( + i < fuelCount ? this.fuelLeftItem : this.fuelUsedItem, 1 + )); + this.progBar[i] = this.addProgressTooltip(new ItemStack( + i < progressCount ? this.progressDoneItem : this.progressRemainingItem, 1 + )); + } + + for (FureamFunctionalArea area : this.functionalAreas) { + area.refreshVisuals(this); + } + + this.syncToBukkitInventory(); + } + + public void syncToBukkitInventory() { + for (int i = 0; i < 54; i++) { + ItemStack stack = this.getStack(i); + this.bukkitInventory.setItem(i, stack != null ? stack : new ItemStack(Material.AIR)); + } + } + + @Contract(pure = true) + public @Range(from = 0, to = Integer.MAX_VALUE) int getPage() { + return this.page; + } + + @Contract(pure = true) + public @Range(from = 0, to = Integer.MAX_VALUE) int getMaxPage() { + int maxSlots = Math.max(this.inputSlots, Math.max(this.fuelSlots, this.outputSlots)); + return Math.max(0, (maxSlots - 1) / 9); + } + + public void switchPage(int page) { + if (page >= 0 && page <= this.getMaxPage()) { + this.page = page; + this.prevPageBtn = makeTooltipItem( + new ItemStack(this.prevPageItem, 1), + config != null ? config.getGuiPrevPageItemTitle().get(this.furnaceType) : "Previous Page", + String.format(config != null ? config.getGuiPrevPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + ); + this.nextPageBtn = makeTooltipItem( + new ItemStack(this.nextPageItem, 1), + config != null ? config.getGuiNextPageItemTitle().get(this.furnaceType) : "Next Page", + String.format(config != null ? config.getGuiNextPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + ); + this.syncToBukkitInventory(); + } + } + + @Contract(pure = true) + public @Range(from = 0, to = Integer.MAX_VALUE) int getCurrentFunctionalArea() { + return this.currentFunctionalArea; + } + + public void setCurrentFunctionalArea(int currentFunctionalArea) { + this.currentFunctionalArea = Math.max(0, currentFunctionalArea); + this.syncToBukkitInventory(); + } + + @Contract(pure = true) + private static @Range(from = 0, to = Integer.MAX_VALUE) int barCount(int value, int total) { + if (total <= 0 || value <= 0) return 0; + int k = Math.round(9f * value / (float) total); + return Math.max(0, Math.min(COLS, k)); + } + + private @NotNull ItemStack addFuelTooltip(@NotNull ItemStack stack) { + String title = config != null ? config.getGuiFuelItemTitle().get(this.furnaceType) : "Fuel"; + String lore = String.format( + config != null ? config.getGuiFuelItemTooltip().get(this.furnaceType) : "Available: %d / %d", + this.burnTime, this.fuelTimeTotal + ); + return makeTooltipItem(stack, title, lore); + } + + private @NotNull ItemStack addProgressTooltip(@NotNull ItemStack stack) { + String title = config != null ? config.getGuiProgressItemTitle().get(this.furnaceType) : "Progress"; + String lore = String.format( + config != null ? config.getGuiProgressItemTooltip().get(this.furnaceType) : "%d / %d", + this.cookTime, this.cookTimeTotal + ); + return makeTooltipItem(stack, title, lore); + } + + public static @NotNull ItemStack makeTooltipItem( + @NotNull ItemStack stack, @Nullable String title, + @NotNull String @NotNull... lores + ) { + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + if (title != null) { + meta.setDisplayName("§r" + title); + } + if (lores.length > 0) { + List loreList = new ArrayList<>(); + for (String lore : lores) { + loreList.add("§7" + lore); + } + meta.setLore(loreList); + } + stack.setItemMeta(meta); + } + return stack; + } + + public @NotNull ItemStack getStack(int slot) { + if (this.isInputSlot(slot)) { + int idx = this.page * 9 + slot; + return idx < this.data.inputs.size() ? this.data.inputs.get(idx) : new ItemStack(Material.AIR); + } + if (this.isFuelRowSlot(slot)) { + int idx = this.page * 9 + slot - 18; + return idx < this.data.fuels.size() ? this.data.fuels.get(idx) : new ItemStack(Material.AIR); + } + if (this.isOutputSlot(slot)) { + int idx = this.page * 9 + slot - 36; + return idx < this.data.outputs.size() ? this.data.outputs.get(idx) : new ItemStack(Material.AIR); + } + if (this.isFunctionalAreaSlot(slot)) { + int ca = this.getCurrentFunctionalArea(); + return ca < this.functionalAreas.size() ? + this.functionalAreas.get(ca).getGuiStack(this, slot - 45) : this.border; + } + if (slot >= 9 && slot < 18) { + return this.fuelBar[slot - 9] != null ? this.fuelBar[slot - 9] : this.border; + } + if (slot >= 27 && slot < 36) { + return this.progBar[slot - 27] != null ? this.progBar[slot - 27] : this.border; + } + if (slot == 51) return this.nextFunctionalBtn; + if (slot == 52) return this.prevPageBtn; + if (slot == 53) return this.nextPageBtn; + return this.border; + } + + public void setStack(int slot, @NotNull ItemStack stack) { + if (this.isInputSlot(slot)) { + int idx = this.page * 9 + slot; + while (this.data.inputs.size() <= idx) { + this.data.inputs.add(new ItemStack(Material.AIR)); + } + this.data.inputs.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + } else if (this.isFuelRowSlot(slot)) { + int idx = this.page * 9 + slot - 18; + while (this.data.fuels.size() <= idx) { + this.data.fuels.add(new ItemStack(Material.AIR)); + } + this.data.fuels.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + } else if (this.isOutputSlot(slot)) { + int idx = this.page * 9 + slot - 36; + while (this.data.outputs.size() <= idx) { + this.data.outputs.add(new ItemStack(Material.AIR)); + } + this.data.outputs.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + } else if (this.isFunctionalAreaSlot(slot)) { + int ca = this.getCurrentFunctionalArea(); + if (ca < this.functionalAreas.size()) { + FureamFunctionalArea area = this.functionalAreas.get(ca); + if (area.isStorable(this, slot - 45)) { + area.setGuiStack(this, slot - 45, stack != null ? stack : new ItemStack(Material.AIR)); + } + } + } + } + + public boolean isValid(int slot, @NotNull ItemStack stack) { + if (stack != null && !stack.getType().isAir()) { + if (this.isInputSlot(slot)) return true; + if (this.isFuelRowSlot(slot)) return FureamFurnaceLogic.isFuel(stack); + } + return false; + } + + static { + FUNCTIONAL_AREA_REGISTRY.add(RecipeSettingAndXpFunctionalArea::new); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java new file mode 100644 index 0000000..b0cf0b6 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java @@ -0,0 +1,242 @@ +package io.github.eat_ram.fuream.screen; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.util.KeyableItemStack; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.CookingRecipe; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +import static io.github.eat_ram.fuream.screen.FureamScreenInventory.makeTooltipItem; + +public class RecipeSettingAndXpFunctionalArea implements FureamFunctionalArea { + private final FureamWorldConfig config; + public final @NotNull Material prevRecipeItem; + public final @NotNull Material nextRecipeItem; + public final @NotNull Material xpIndicatorItem; + + /** Slot 1 (46 in {@link FureamScreenInventory}). */ + public @NotNull ItemStack prevRecipeBtn = new ItemStack(Material.AIR); + /** Slot 2 (47 in {@link FureamScreenInventory}). */ + public @NotNull ItemStack nextRecipeBtn = new ItemStack(Material.AIR); + /** Slot 3 (48 in {@link FureamScreenInventory}). */ + public @NotNull ItemStack recipeResultPreview = new ItemStack(Material.AIR); + /** Slot 4 (49 in {@link FureamScreenInventory}). */ + public @NotNull ItemStack xpIndicator = new ItemStack(Material.AIR); + + public final @NotNull ArrayList<@NotNull CookingRecipe> currentMatchingRecipes = new ArrayList<>(); + private @Range(from = 0, to = Integer.MAX_VALUE) int selectedRecipeIndex; + private float cachedXp = -1f; + + public RecipeSettingAndXpFunctionalArea( + FureamScreenInventory inv, FureamWorldConfig config + ) { + this.config = config; + FurnaceType type = inv.furnaceType; + + Material prevMat = parseMaterial(config != null ? config.getGuiPrevRecipeItemId().get(type) : null, Material.BOOK); + Material nextMat = parseMaterial(config != null ? config.getGuiNextRecipeItemId().get(type) : null, Material.BOOK); + Material xpMat = parseMaterial(config != null ? config.getGuiXpIndicatorItemId().get(type) : null, Material.LIME_STAINED_GLASS_PANE); + + this.prevRecipeItem = prevMat; + this.nextRecipeItem = nextMat; + this.xpIndicatorItem = xpMat; + this.updateRecipeSettingArea(inv); + this.refreshVisuals(inv); + } + + private static Material parseMaterial(@Nullable String id, Material fallback) { + if (id == null) return fallback; + String name = id.startsWith("minecraft:") ? id.substring(10) : id; + Material mat = Material.matchMaterial(name.toUpperCase()); + return mat != null ? mat : fallback; + } + + @Override + @Contract(pure = true) + public @NotNull ItemStack getGuiStack(FureamScreenInventory inv, int slot) { + switch (slot) { + case 0: return inv.data.recipeOverridingInput != null ? inv.data.recipeOverridingInput : new ItemStack(Material.AIR); + case 1: return this.prevRecipeBtn; + case 2: return this.nextRecipeBtn; + case 3: return this.recipeResultPreview; + case 4: return this.xpIndicator; + default: return inv.border; + } + } + + @Override + public void setGuiStack(FureamScreenInventory inv, int slot, ItemStack newStack) { + if (slot == 0) { + inv.data.recipeOverridingInput = newStack != null ? newStack : new ItemStack(Material.AIR); + this.updateRecipeSettingArea(inv); + } + } + + @Override + public void onGuiClick( + SlotClick slotClick, FureamScreenInventory display, int slot, + int button, ClickType clickType, Player player + ) { + switch (slot) { + case 0: + slotClick.onSlotClick(45, button, clickType, player); + this.updateRecipeSettingArea(display); + break; + case 1: { + int curRecipeIndex = this.getSelectedRecipeIndex(); + this.switchSelectedRecipeIndex( + display, curRecipeIndex > 0 ? curRecipeIndex - 1 : this.currentMatchingRecipes.size() + ); + break; + } + case 2: { + int curRecipeIndex = this.getSelectedRecipeIndex(); + this.switchSelectedRecipeIndex( + display, curRecipeIndex < this.currentMatchingRecipes.size() ? curRecipeIndex + 1 : 0 + ); + break; + } + case 4: + FureamFurnaceLogic.grantExperience(player, display.location, display.data); + this.refreshVisuals(display); + break; + } + } + + @Override + @Contract(pure = true) + public boolean isStorable(FureamScreenInventory inv, int slot) { + return slot == 0; + } + + @Override + @Contract(pure = true) + public boolean isTakable(FureamScreenInventory inv, int slot) { + return slot == 0; + } + + @Contract(pure = true) + public @Range(from = 0, to = Integer.MAX_VALUE) int getSelectedRecipeIndex() { + return this.selectedRecipeIndex; + } + + public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex) { + ItemStack toFind = inv.data.recipeOverridingInput; + FurnaceType type = inv.furnaceType; + if (toFind == null || toFind.getType().isAir()) { + this.selectedRecipeIndex = 0; + this.prevRecipeBtn = makeTooltipItem( + new ItemStack(this.prevRecipeItem, 1), + config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + config != null ? config.getGuiPrevRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" + ); + this.nextRecipeBtn = makeTooltipItem( + new ItemStack(this.nextRecipeItem, 1), + config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + config != null ? config.getGuiNextRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" + ); + this.recipeResultPreview = new ItemStack(Material.AIR); + return; + } + int size = this.currentMatchingRecipes.size(); + if (recipeIndex > 0 && recipeIndex <= size) { + this.selectedRecipeIndex = recipeIndex; + CookingRecipe recipe = this.currentMatchingRecipes.get(recipeIndex - 1); + inv.data.overriddenRecipes.put(new KeyableItemStack(toFind), recipe.getKey()); + this.prevRecipeBtn = makeTooltipItem( + new ItemStack(this.prevRecipeItem, 1), + config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + String.format(config != null ? config.getGuiPrevRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) + ); + this.nextRecipeBtn = makeTooltipItem( + new ItemStack(this.nextRecipeItem, 1), + config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + String.format(config != null ? config.getGuiNextRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) + ); + this.recipeResultPreview = recipe.getResult().clone(); + } else { + this.selectedRecipeIndex = 0; + inv.data.overriddenRecipes.remove(new KeyableItemStack(toFind)); + this.prevRecipeBtn = makeTooltipItem( + new ItemStack(this.prevRecipeItem, 1), + config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + String.format(config != null ? config.getGuiPrevRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) + ); + this.nextRecipeBtn = makeTooltipItem( + new ItemStack(this.nextRecipeItem, 1), + config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + String.format(config != null ? config.getGuiNextRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) + ); + CookingRecipe recipe = FureamFurnaceLogic.findRecipe(displayWorld(inv), toFind, inv.furnaceType).orElse(null); + this.recipeResultPreview = recipe != null ? recipe.getResult().clone() : new ItemStack(Material.AIR); + } + } + + private org.bukkit.World displayWorld(FureamScreenInventory inv) { + return inv.location != null ? inv.location.getWorld() : null; + } + + public void updateMatchingRecipes(FureamScreenInventory inv) { + this.currentMatchingRecipes.clear(); + ItemStack toFind = inv.data.recipeOverridingInput; + if (toFind != null && !toFind.getType().isAir()) { + List> matches = FureamFurnaceLogic.findAllMatches(displayWorld(inv), toFind, inv.furnaceType); + matches.sort(Comparator.comparing(r -> r.getKey().toString())); + this.currentMatchingRecipes.addAll(matches); + } + } + + public void updateRecipeSettingArea(FureamScreenInventory inv) { + this.updateMatchingRecipes(inv); + ItemStack toFind = inv.data.recipeOverridingInput; + if (toFind == null || toFind.getType().isAir()) { + this.switchSelectedRecipeIndex(inv, 0); + } else { + NamespacedKey overridden = inv.data.overriddenRecipes.get(new KeyableItemStack(toFind)); + boolean found = false; + int i = 1; + for (CookingRecipe recipe : this.currentMatchingRecipes) { + if (recipe.getKey().equals(overridden)) { + this.switchSelectedRecipeIndex(inv, i); + found = true; + break; + } + ++i; + } + if (!found) { + inv.data.overriddenRecipes.remove(new KeyableItemStack(toFind)); + this.switchSelectedRecipeIndex(inv, 0); + } + } + } + + @Override + public void refreshVisuals(FureamScreenInventory inv) { + if (inv.data.experience != this.cachedXp) { + this.cachedXp = inv.data.experience; + FurnaceType type = inv.furnaceType; + String title = ((int) inv.data.experience > 0) ? + (config != null ? config.getGuiXpIndicatorGainableItemTitle().get(type) : "XP (click to gain)") : + (config != null ? config.getGuiXpIndicatorItemTitle().get(type) : "XP"); + String tooltip = String.format( + config != null ? config.getGuiXpIndicatorItemTooltip().get(type) : "Current: %.1f", + this.cachedXp + ); + this.xpIndicator = makeTooltipItem(new ItemStack(this.xpIndicatorItem, 1), title, tooltip); + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java new file mode 100644 index 0000000..888b6dc --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java @@ -0,0 +1,47 @@ +package io.github.eat_ram.fuream.util; + +import java.util.Objects; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.jetbrains.annotations.NotNull; + +public final class FurnacePos { + public final @NotNull String worldName; + public final int x; + public final int y; + public final int z; + + public FurnacePos(@NotNull String worldName, int x, int y, int z) { + this.worldName = worldName; + this.x = x; + this.y = y; + this.z = z; + } + + public FurnacePos(@NotNull Location loc) { + this(loc.getWorld() != null ? loc.getWorld().getName() : "", loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + } + + public FurnacePos(@NotNull Block block) { + this(block.getWorld().getName(), block.getX(), block.getY(), block.getZ()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FurnacePos)) return false; + FurnacePos that = (FurnacePos) o; + return this.x == that.x && this.y == that.y && this.z == that.z && Objects.equals(this.worldName, that.worldName); + } + + @Override + public int hashCode() { + return Objects.hash(this.worldName, this.x, this.y, this.z); + } + + @Override + public String toString() { + return this.worldName + " (" + this.x + ", " + this.y + ", " + this.z + ")"; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java new file mode 100644 index 0000000..59ddef6 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java @@ -0,0 +1,51 @@ +package io.github.eat_ram.fuream.util; + +import java.util.Objects; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class KeyableItemStack { + public final @NotNull ItemStack stack; + + public KeyableItemStack(@Nullable ItemStack stack) { + if (stack == null || stack.getType().isAir()) { + this.stack = new ItemStack(Material.AIR); + } else { + this.stack = stack.clone(); + this.stack.setAmount(1); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof KeyableItemStack)) { + return false; + } + KeyableItemStack that = (KeyableItemStack) obj; + if (this.stack.getType().isAir()) { + return that.stack.getType().isAir(); + } + if (that.stack.getType().isAir()) { + return false; + } + return this.stack.getType() == that.stack.getType() && + this.stack.getAmount() == that.stack.getAmount() && + Objects.equals(this.stack.getItemMeta(), that.stack.getItemMeta()); + } + + @Override + public int hashCode() { + if (this.stack.getType().isAir()) { + return 0; + } + ItemMeta meta = this.stack.getItemMeta(); + return Objects.hash(this.stack.getType(), this.stack.getAmount(), meta != null ? meta.hashCode() : 0); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java new file mode 100644 index 0000000..e8c1635 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java @@ -0,0 +1,85 @@ +package io.github.eat_ram.fuream.util; + +import java.util.List; + +import de.tr7zw.nbtapi.NBT; +import de.tr7zw.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.nbtapi.iface.ReadWriteNBTCompoundList; +import de.tr7zw.nbtapi.iface.ReadableNBT; +import de.tr7zw.nbtapi.iface.ReadableNBTList; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class NbtUtil { + public static @NotNull ItemStack fromNbt(@Nullable ReadableNBT compound) { + if (compound == null || compound.getKeys().isEmpty()) { + return new ItemStack(Material.AIR); + } + try { + ItemStack stack = NBT.itemStackFromNBT(compound); + return stack != null ? stack : new ItemStack(Material.AIR); + } catch (Exception e) { + return new ItemStack(Material.AIR); + } + } + + public static void writeItemToCompound(@NotNull ReadWriteNBT target, @Nullable ItemStack stack) { + if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { + ReadWriteNBT container = NBT.itemStackToNBT(stack); + target.mergeCompound(container); + } + } + + public static void writeStacks( + @NotNull ReadWriteNBT parent, @NotNull String key, + @NotNull Iterable<@NotNull ItemStack> stacks + ) { + if (parent.hasTag(key)) { + parent.removeKey(key); + } + ReadWriteNBTCompoundList list = parent.getCompoundList(key); + int slot = 0; + for (ItemStack stack : stacks) { + if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { + ReadWriteNBT sub = list.addCompound(); + ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); + sub.mergeCompound(itemNbt); + sub.setInteger("Slot", slot); + } + ++slot; + } + } + + public static void readStacks( + @NotNull ReadableNBT parent, @NotNull String key, + @NotNull List target + ) { + if (!parent.hasTag(key)) { + return; + } + ReadableNBTList list = parent.getCompoundList(key); + for (int i = 0; i < list.size(); ++i) { + ReadWriteNBT sub = list.get(i); + if (sub.hasTag("Slot")) { + int slot = sub.getInteger("Slot"); + ItemStack stack = fromNbt(sub); + if (!stack.getType().isAir() && slot >= 0) { + if (slot < target.size()) { + target.set(slot, stack); + } else { + while (target.size() <= slot) { + target.add(new ItemStack(Material.AIR)); + } + target.set(slot, stack); + } + } + } + } + } + + private NbtUtil() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml new file mode 100644 index 0000000..d284fd6 --- /dev/null +++ b/spigot/src/main/resources/plugin.yml @@ -0,0 +1,15 @@ +name: Fuream +version: 0.0.1 +main: io.github.eat_ram.fuream.FureamMain +api-version: '1.20' +depend: + - NBTAPI +commands: + fuream: + description: Fuream plugin management + permission: fuream.reload + usage: / [reload|status] +permissions: + fuream.reload: + description: Allows reloading Fuream configs + default: op diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java new file mode 100644 index 0000000..68d4098 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java @@ -0,0 +1,47 @@ +package io.github.eat_ram.fuream; + +import java.util.ArrayList; +import java.util.ListIterator; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class CollectionUtilTest { + @Test + public void testDefaultedArrayList() { + ArrayList list = CollectionUtil.newDefaultedArrayList("EMPTY", 5); + assertEquals(5, list.size()); + assertEquals("EMPTY", list.get(0)); + assertEquals("EMPTY", list.get(4)); + assertEquals("EMPTY", list.get(10)); // Out of bounds returns default + + list.set(2, "ITEM_2"); + assertEquals("ITEM_2", list.get(2)); + + list.set(7, "ITEM_7"); + assertEquals(8, list.size()); + assertEquals("ITEM_7", list.get(7)); + assertEquals("EMPTY", list.get(6)); + } + + @Test + public void testListIterator() { + ArrayList list = CollectionUtil.newDefaultedArrayList("AIR", 3); + list.set(0, "A"); + list.set(1, "B"); + list.set(2, "C"); + + ListIterator it = CollectionUtil.listIterator(list, 3, 0); + assertTrue(it.hasNext()); + assertEquals("A", it.next()); + assertEquals("B", it.next()); + assertEquals("C", it.next()); + assertFalse(it.hasNext()); + + assertTrue(it.hasPrevious()); + assertEquals("C", it.previous()); + assertEquals("B", it.previous()); + assertEquals("A", it.previous()); + assertFalse(it.hasPrevious()); + } +} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java new file mode 100644 index 0000000..092df33 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java @@ -0,0 +1,27 @@ +package io.github.eat_ram.fuream; + +import io.github.eat_ram.fuream.api.FurnaceType; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class ConfigTest { + @Test + public void testDefaultConfig() { + FureamWorldConfigImpl config = new FureamWorldConfigImpl(); + for (FurnaceType type : FurnaceType.values()) { + assertEquals(9, config.getInputSlotCount().get(type)); + assertEquals(9, config.getFuelSlotCount().get(type)); + assertEquals(9, config.getOutputSlotCount().get(type)); + assertEquals("GUI Border", config.getGuiBorderItemTitle().get(type)); + assertEquals("Fuel", config.getGuiFuelItemTitle().get(type)); + assertEquals("Progress", config.getGuiProgressItemTitle().get(type)); + assertEquals("Previous Recipe", config.getGuiPrevRecipeItemTitle().get(type)); + assertEquals("Next Recipe", config.getGuiNextRecipeItemTitle().get(type)); + assertEquals("XP", config.getGuiXpIndicatorItemTitle().get(type)); + assertEquals("XP (click to gain)", config.getGuiXpIndicatorGainableItemTitle().get(type)); + assertEquals("Next Functional Area", config.getGuiNextFunctionalAreaItemTitle().get(type)); + assertEquals("Previous Page", config.getGuiPrevPageItemTitle().get(type)); + assertEquals("Next Page", config.getGuiNextPageItemTitle().get(type)); + } + } +} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/HopperAndDataTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/HopperAndDataTest.java new file mode 100644 index 0000000..027aff3 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/HopperAndDataTest.java @@ -0,0 +1,80 @@ +package io.github.eat_ram.fuream; + +import java.util.List; + +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HopperAndDataTest { + + private boolean isSameItem(ItemStack a, ItemStack b) { + if (a == null || b == null) return false; + if (a.getType().isAir() || b.getType().isAir()) return false; + return a.getType() == b.getType(); + } + + private boolean insertIntoList(List list, ItemStack item, int maxSlots) { + while (list.size() < maxSlots) { + list.add(new ItemStack(Material.AIR)); + } + + // 1. Try stacking with existing matching items + for (int i = 0; i < maxSlots && i < list.size(); i++) { + ItemStack s = list.get(i); + if (s != null && isSameItem(s, item) && s.getAmount() < s.getMaxStackSize()) { + s.setAmount(s.getAmount() + 1); + return true; + } + } + + // 2. Try empty slot + for (int i = 0; i < maxSlots && i < list.size(); i++) { + ItemStack s = list.get(i); + if (s == null || s.getType().isAir()) { + ItemStack single = item.clone(); + single.setAmount(1); + list.set(i, single); + return true; + } + } + + return false; + } + + @Test + public void test64ItemsInsertion() { + FureamFurnaceData data = new FureamFurnaceData(); + int maxSlots = 18; + + ItemStack rawIron = new ItemStack(Material.RAW_IRON, 1); + for (int i = 0; i < 64; i++) { + boolean inserted = insertIntoList(data.inputs, rawIron, maxSlots); + assertTrue(inserted, "Should insert item " + (i + 1)); + } + + assertEquals(Material.RAW_IRON, data.inputs.get(0).getType()); + assertEquals(64, data.inputs.get(0).getAmount()); + } + + @Test + public void testOver64ItemsInsertion() { + FureamFurnaceData data = new FureamFurnaceData(); + int maxSlots = 18; + + ItemStack rawIron = new ItemStack(Material.RAW_IRON, 1); + for (int i = 0; i < 100; i++) { + boolean inserted = insertIntoList(data.inputs, rawIron, maxSlots); + assertTrue(inserted, "Should insert item " + (i + 1)); + } + + assertEquals(Material.RAW_IRON, data.inputs.get(0).getType()); + assertEquals(64, data.inputs.get(0).getAmount()); + assertEquals(Material.RAW_IRON, data.inputs.get(1).getType()); + assertEquals(36, data.inputs.get(1).getAmount()); + } +} From 549afa28bae725150b5e6db57d3285f37462b4b3 Mon Sep 17 00:00:00 2001 From: az7627 Date: Tue, 25 Aug 2026 23:51:18 +0800 Subject: [PATCH 02/13] refactor(spigot): clean up null checks and imports, fix api repository configuration --- api/build.gradle.kts | 4 ++ .../github/eat_ram/fuream/CollectionUtil.java | 7 +-- .../io/github/eat_ram/fuream/FureamMain.java | 27 +++++----- .../eat_ram/fuream/FureamWorldConfigImpl.java | 1 - .../fuream/data/FureamFurnaceData.java | 8 +-- .../fuream/hook/BlockBreakListener.java | 10 ++-- .../eat_ram/fuream/hook/FurnaceManager.java | 4 +- .../eat_ram/fuream/logic/FuelTable.java | 1 - .../fuream/logic/FureamFurnaceEngine.java | 17 +++--- .../fuream/logic/FureamFurnaceLogic.java | 2 +- .../fuream/screen/FureamScreenHandler.java | 53 +++++++++---------- .../fuream/screen/FureamScreenInventory.java | 37 +++++++------ .../RecipeSettingAndXpFunctionalArea.java | 45 ++++++++-------- 13 files changed, 106 insertions(+), 110 deletions(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 61137b2..d1da533 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -9,6 +9,10 @@ base { archivesName = "fuream-api" } +repositories { + mavenCentral() +} + dependencies { compileOnly("org.jetbrains:annotations:26.0.2") } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java index ca0f259..ffcf59b 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java @@ -10,6 +10,7 @@ public abstract class CollectionUtil { @Contract(value = "_, _ -> new", pure = true) + @SuppressWarnings("unchecked") public static ArrayList newDefaultedArrayList( E defaultValue, @Range(from = 0, to = Integer.MAX_VALUE) int initialSize @@ -26,7 +27,7 @@ private E makeDefault() { @Override @Contract(pure = true) public E get(int index) { - return index < this.size() ? super.get(index) : makeDefault(); + return index < this.size() ? super.get(index) : this.makeDefault(); } @Override @@ -37,10 +38,10 @@ public E set(int index, E element) { } this.ensureCapacity(index + 1); for (; size < index; ++size) { - this.add(makeDefault()); + this.add(this.makeDefault()); } this.add(element); - return makeDefault(); + return this.makeDefault(); } }; if (initialSize > 0) { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java index 004ac08..58632f2 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -1,7 +1,6 @@ package io.github.eat_ram.fuream; import java.io.File; -import java.io.FileNotFoundException; import java.util.Locale; import java.util.Map; import java.util.WeakHashMap; @@ -48,19 +47,19 @@ public static FureamMain getInstance() { public void onEnable() { INSTANCE = this; - if (!getServer().getPluginManager().isPluginEnabled("NBTAPI")) { - getLogger().severe("Item-NBT-API is required for Fuream to operate! Please install NBT-API plugin."); - getServer().getPluginManager().disablePlugin(this); + if (!this.getServer().getPluginManager().isPluginEnabled("NBTAPI")) { + this.getLogger().severe("Item-NBT-API is required for Fuream to operate! Please install NBT-API plugin."); + this.getServer().getPluginManager().disablePlugin(this); return; } // Register listeners - getServer().getPluginManager().registerEvents(this, this); - getServer().getPluginManager().registerEvents(new FurnaceOpenListener(), this); - getServer().getPluginManager().registerEvents(new GuiListener(), this); - getServer().getPluginManager().registerEvents(new HopperListener(), this); - getServer().getPluginManager().registerEvents(new BlockBreakListener(), this); - getServer().getPluginManager().registerEvents(new ChunkListener(), this); + this.getServer().getPluginManager().registerEvents(this, this); + this.getServer().getPluginManager().registerEvents(new FurnaceOpenListener(), this); + this.getServer().getPluginManager().registerEvents(new GuiListener(), this); + this.getServer().getPluginManager().registerEvents(new HopperListener(), this); + this.getServer().getPluginManager().registerEvents(new BlockBreakListener(), this); + this.getServer().getPluginManager().registerEvents(new ChunkListener(), this); // Load configs and scan loaded chunks for existing furnaces for (World world : Bukkit.getWorlds()) { @@ -78,18 +77,18 @@ public void onEnable() { Bukkit.getScheduler().runTaskTimer(this, FurnaceManager::tickAll, 1L, 1L); // Register command - if (getCommand("fuream") != null) { - getCommand("fuream").setExecutor(this); + if (this.getCommand("fuream") != null) { + this.getCommand("fuream").setExecutor(this); } - getLogger().info("Fuream (Spigot) enabled successfully!"); + this.getLogger().info("Fuream (Spigot) enabled successfully!"); } @Override public void onDisable() { FurnaceManager.flushAll(); FurnaceManager.CONTEXTS.clear(); - getLogger().info("Fuream (Spigot) disabled successfully!"); + this.getLogger().info("Fuream (Spigot) disabled successfully!"); } @EventHandler diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java index b78b245..c318bac 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java @@ -2,7 +2,6 @@ import java.io.FileNotFoundException; import java.io.FileReader; -import java.io.IOException; import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java index c8224c2..4c3dd30 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java @@ -48,13 +48,13 @@ public FureamFurnaceData() { public boolean hasAny() { if (this.experience > 0f) return true; for (ItemStack s : this.inputs) { - if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + if (!s.getType().isAir() && s.getAmount() > 0) return true; } for (ItemStack s : this.fuels) { - if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + if (!s.getType().isAir() && s.getAmount() > 0) return true; } for (ItemStack s : this.outputs) { - if (s != null && !s.getType().isAir() && s.getAmount() > 0) return true; + if (!s.getType().isAir() && s.getAmount() > 0) return true; } return false; } @@ -80,7 +80,7 @@ public void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt) { ReadWriteNBTCompoundList list = nbt.getCompoundList(OVERRIDDEN_RECIPES_KEY); for (Map.Entry entry : this.overriddenRecipes.entrySet()) { ItemStack stack = entry.getKey().stack; - if (stack != null && !stack.getType().isAir()) { + if (!stack.getType().isAir()) { ReadWriteNBT sub = list.addCompound(); ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); sub.mergeCompound(itemNbt); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java index 76b1033..f48b50e 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java @@ -1,7 +1,6 @@ package io.github.eat_ram.fuream.hook; import java.lang.reflect.Field; -import java.util.List; import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.api.FurnaceType; @@ -11,7 +10,6 @@ import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Location; import org.bukkit.block.Block; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -24,25 +22,25 @@ public class BlockBreakListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockBreak(BlockBreakEvent event) { - handleFurnaceBreak(event.getBlock()); + this.handleFurnaceBreak(event.getBlock()); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockBurn(BlockBurnEvent event) { - handleFurnaceBreak(event.getBlock()); + this.handleFurnaceBreak(event.getBlock()); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockExplode(BlockExplodeEvent event) { for (Block b : event.blockList()) { - handleFurnaceBreak(b); + this.handleFurnaceBreak(b); } } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onEntityExplode(EntityExplodeEvent event) { for (Block b : event.blockList()) { - handleFurnaceBreak(b); + this.handleFurnaceBreak(b); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index be279b4..95f0076 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -293,7 +293,7 @@ private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull Furn // 1. Try pulling from outputs first for (int i = 0; i < ctx.data.outputs.size(); i++) { ItemStack outStack = ctx.data.outputs.get(i); - if (outStack != null && !outStack.getType().isAir() && outStack.getAmount() > 0) { + if (!outStack.getType().isAir() && outStack.getAmount() > 0) { ItemStack single = outStack.clone(); single.setAmount(1); @@ -315,7 +315,7 @@ private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull Furn // 2. Try pulling empty buckets or water buckets from fuels for (int i = 0; i < ctx.data.fuels.size(); i++) { ItemStack fuelStack = ctx.data.fuels.get(i); - if (fuelStack != null && (fuelStack.getType() == Material.BUCKET || fuelStack.getType() == Material.WATER_BUCKET)) { + if (fuelStack.getType() == Material.BUCKET || fuelStack.getType() == Material.WATER_BUCKET) { ItemStack single = fuelStack.clone(); single.setAmount(1); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java index 6d983a6..2a3ea22 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java @@ -7,7 +7,6 @@ import org.bukkit.Material; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class FuelTable { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index 223e30b..e477e73 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -1,7 +1,6 @@ package io.github.eat_ram.fuream.logic; import java.util.List; -import java.util.Map; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; @@ -90,7 +89,7 @@ public static void tick( ItemStack inputStack = null; for (int i = 0; i < data.inputs.size(); i++) { ItemStack s = data.inputs.get(i); - if (s != null && !s.getType().isAir() && s.getAmount() > 0) { + if (!s.getType().isAir() && s.getAmount() > 0) { firstInputIdx = i; inputStack = s; break; @@ -106,7 +105,7 @@ public static void tick( ItemStack recipeResult = null; int outSlotIdx = -1; - if (matchedRecipe != null && inputStack != null) { + if (matchedRecipe != null) { recipeResult = matchedRecipe.getResult(); outSlotIdx = findFittingOutputSlot(data.outputs, recipeResult); if (outSlotIdx >= 0) { @@ -120,14 +119,14 @@ public static void tick( ItemStack fuelStack = null; for (int i = 0; i < data.fuels.size(); i++) { ItemStack s = data.fuels.get(i); - if (s != null && !s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { + if (!s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { firstFuelIdx = i; fuelStack = s; break; } } - if (fuelStack != null && firstFuelIdx >= 0) { + if (fuelStack != null) { int baseFuelTime = FuelTable.getFuelTime(fuelStack); double fuelUpgradeFactor = ctx.getFuelUpgradeFactor(); int fuelTime = (int) Math.round(baseFuelTime * fuelUpgradeFactor); @@ -149,7 +148,7 @@ public static void tick( } // Cook progression - if (ctx.burnTime > 0 && canSmelt && matchedRecipe != null && inputStack != null && recipeResult != null && outSlotIdx >= 0) { + if (ctx.burnTime > 0 && canSmelt) { ctx.cookTime++; ctx.dirty = true; @@ -158,7 +157,7 @@ public static void tick( // Deposit result ItemStack currentOut = data.outputs.get(outSlotIdx); - if (currentOut == null || currentOut.getType().isAir()) { + if (currentOut.getType().isAir()) { data.outputs.set(outSlotIdx, recipeResult.clone()); } else if (currentOut.isSimilar(recipeResult)) { currentOut.setAmount(currentOut.getAmount() + recipeResult.getAmount()); @@ -172,7 +171,7 @@ public static void tick( if (inputStack.getType() == Material.WET_SPONGE) { for (int i = 0; i < data.fuels.size(); i++) { ItemStack f = data.fuels.get(i); - if (f != null && f.getType() == Material.BUCKET) { + if (f.getType() == Material.BUCKET) { f.setAmount(f.getAmount() - 1); if (f.getAmount() <= 0) { data.fuels.set(i, new ItemStack(Material.WATER_BUCKET)); @@ -199,7 +198,7 @@ public static void tick( ItemStack nextFuelStack = null; for (int i = 0; i < data.fuels.size(); i++) { ItemStack s = data.fuels.get(i); - if (s != null && !s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { + if (!s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { nextFuelIdx = i; nextFuelStack = s; break; diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java index 22ce614..00b02b3 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java @@ -122,7 +122,7 @@ public static void dropOnBreak( } data.outputs.clear(); - if (data.recipeOverridingInput != null && !data.recipeOverridingInput.getType().isAir()) { + if (!data.recipeOverridingInput.getType().isAir()) { dropIfPresent(world, center, data.recipeOverridingInput); data.recipeOverridingInput = new ItemStack(Material.AIR); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java index 9e0b68f..4d38010 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java @@ -37,7 +37,7 @@ public void handleClick(InventoryClickEvent event, Player player) { event.setCancelled(true); ItemStack current = event.getCurrentItem(); if (current != null && !current.getType().isAir()) { - ItemStack remaining = quickMoveToContainer(current); + ItemStack remaining = this.quickMoveToContainer(current); event.setCurrentItem(remaining.getType().isAir() ? null : remaining); this.display.syncToBukkitInventory(); } @@ -53,7 +53,7 @@ public void handleClick(InventoryClickEvent event, Player player) { if (ca < this.display.functionalAreas.size()) { FureamFunctionalArea area = this.display.functionalAreas.get(ca); area.onGuiClick( - (sIdx, btn, ct, p) -> handleFunctionalSlotDefault(rawSlot, sIdx, event, p), + (sIdx, btn, ct, p) -> this.handleFunctionalSlotDefault(rawSlot, sIdx, event, p), this.display, rawSlot - 45, event.getHotbarButton(), clickType, player ); this.display.syncToBukkitInventory(); @@ -82,15 +82,14 @@ public void handleClick(InventoryClickEvent event, Player player) { } if (this.display.isOutputSlot(rawSlot)) { - onOutputSlotClick(rawSlot, event, player); + this.onOutputSlotClick(rawSlot, event, player); this.display.syncToBukkitInventory(); return; } if (this.display.isInputSlot(rawSlot) || this.display.isFuelRowSlot(rawSlot)) { - onMaterialSlotClick(rawSlot, event, player); + this.onMaterialSlotClick(rawSlot, event, player); this.display.syncToBukkitInventory(); - return; } } @@ -106,7 +105,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC // 1. Shift click if (event.isShiftClick()) { - if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); @@ -115,7 +114,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC area.setGuiStack(this.display, rawSlot - 45, current); } } - markDirtyAndSave(); + this.markDirtyAndSave(); return; } @@ -125,12 +124,12 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC if (hotbarSlot >= 0 && hotbarSlot < 9) { ItemStack hotbarItem = player.getInventory().getItem(hotbarSlot); if (hotbarItem == null || hotbarItem.getType().isAir()) { - if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { player.getInventory().setItem(hotbarSlot, current.clone()); area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); } } else if (area.isStorable(this.display, rawSlot - 45)) { - if (current == null || current.getType().isAir()) { + if (current.getType().isAir()) { area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); player.getInventory().setItem(hotbarSlot, new ItemStack(Material.AIR)); } else if (area.isTakable(this.display, rawSlot - 45)) { @@ -139,13 +138,13 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC } } } - markDirtyAndSave(); + this.markDirtyAndSave(); return; } // 3. Cursor empty -> Take item from slot if (cursor == null || cursor.getType().isAir()) { - if (current != null && !current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { if (event.isRightClick()) { int half = (current.getAmount() + 1) / 2; ItemStack take = current.clone(); @@ -161,7 +160,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC } else { // 4. Cursor has item -> Place/Swap into slot if (area.isStorable(this.display, rawSlot - 45)) { - if (current == null || current.getType().isAir()) { + if (current.getType().isAir()) { if (event.isRightClick()) { ItemStack putOne = cursor.clone(); putOne.setAmount(1); @@ -197,7 +196,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC } } } - markDirtyAndSave(); + this.markDirtyAndSave(); } private void markDirtyAndSave() { @@ -211,7 +210,7 @@ private void markDirtyAndSave() { private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player player) { ItemStack current = this.display.getStack(slotIndex); - if (current == null || current.getType().isAir()) return; + if (current.getType().isAir()) return; if (event.isShiftClick()) { Map leftover = player.getInventory().addItem(current.clone()); @@ -221,7 +220,7 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player current.setAmount(leftover.values().iterator().next().getAmount()); this.display.setStack(slotIndex, current); } - markDirtyAndSave(); + this.markDirtyAndSave(); return; } @@ -240,7 +239,7 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); } } - markDirtyAndSave(); + this.markDirtyAndSave(); } private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Player player) { @@ -249,7 +248,7 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe ItemStack current = this.display.getStack(slotIndex); if (event.isShiftClick()) { - if (current != null && !current.getType().isAir()) { + if (!current.getType().isAir()) { Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { this.display.setStack(slotIndex, new ItemStack(Material.AIR)); @@ -258,12 +257,12 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe this.display.setStack(slotIndex, current); } } - markDirtyAndSave(); + this.markDirtyAndSave(); return; } if (cursor == null || cursor.getType().isAir()) { - if (current != null && !current.getType().isAir()) { + if (!current.getType().isAir()) { if (event.isRightClick()) { int half = (current.getAmount() + 1) / 2; ItemStack take = current.clone(); @@ -280,7 +279,7 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe if (isFuel && !FureamFurnaceLogic.isFuel(cursor)) { return; } - if (current == null || current.getType().isAir()) { + if (current.getType().isAir()) { if (event.isRightClick()) { ItemStack putOne = cursor.clone(); putOne.setAmount(1); @@ -315,7 +314,7 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe event.getView().setCursor(current.clone()); } } - markDirtyAndSave(); + this.markDirtyAndSave(); } private @NotNull ItemStack quickMoveToContainer(@NotNull ItemStack original) { @@ -323,9 +322,9 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe if (FureamFurnaceLogic.isAcceptableInput(this.display.location.getWorld(), remaining, this.display.furnaceType)) { int stop = this.display.getInputSlotStop(); for (int i = 0; i < stop; i++) { - remaining = insertIntoSlot(i, remaining); + remaining = this.insertIntoSlot(i, remaining); if (remaining.getType().isAir() || remaining.getAmount() <= 0) { - markDirtyAndSave(); + this.markDirtyAndSave(); return new ItemStack(Material.AIR); } } @@ -334,20 +333,20 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe int fuelStart = 18; int stop = this.display.getFuelSlotStop(); for (int i = fuelStart; i < stop; i++) { - remaining = insertIntoSlot(i, remaining); + remaining = this.insertIntoSlot(i, remaining); if (remaining.getType().isAir() || remaining.getAmount() <= 0) { - markDirtyAndSave(); + this.markDirtyAndSave(); return new ItemStack(Material.AIR); } } } - markDirtyAndSave(); + this.markDirtyAndSave(); return remaining; } private @NotNull ItemStack insertIntoSlot(int slotIndex, @NotNull ItemStack stack) { ItemStack current = this.display.getStack(slotIndex); - if (current == null || current.getType().isAir()) { + if (current.getType().isAir()) { this.display.setStack(slotIndex, stack.clone()); return new ItemStack(Material.AIR); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java index abcbdce..09c4178 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -1,7 +1,6 @@ package io.github.eat_ram.fuream.screen; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.function.BiFunction; @@ -111,7 +110,7 @@ public FureamScreenInventory( String title = type == FurnaceType.SMOKER ? "Smoker" : (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace"); this.bukkitInventory = Bukkit.createInventory(null, 54, title); - this.cookTimeTotal = computeCookTimeTotal(); + this.cookTimeTotal = this.computeCookTimeTotal(); this.switchPage(0); for (BiFunction< ? super FureamScreenInventory, ? super FureamWorldConfig, @@ -184,7 +183,7 @@ public int getOutputSlotStop() { } public void refreshVisuals() { - this.cookTimeTotal = computeCookTimeTotal(); + this.cookTimeTotal = this.computeCookTimeTotal(); if (this.cookTime >= this.cookTimeTotal) { this.cookTime = Math.max(0, this.cookTimeTotal - 1); } @@ -211,7 +210,7 @@ public void refreshVisuals() { public void syncToBukkitInventory() { for (int i = 0; i < 54; i++) { ItemStack stack = this.getStack(i); - this.bukkitInventory.setItem(i, stack != null ? stack : new ItemStack(Material.AIR)); + this.bukkitInventory.setItem(i, stack); } } @@ -231,13 +230,13 @@ public void switchPage(int page) { this.page = page; this.prevPageBtn = makeTooltipItem( new ItemStack(this.prevPageItem, 1), - config != null ? config.getGuiPrevPageItemTitle().get(this.furnaceType) : "Previous Page", - String.format(config != null ? config.getGuiPrevPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + this.config != null ? this.config.getGuiPrevPageItemTitle().get(this.furnaceType) : "Previous Page", + String.format(this.config != null ? this.config.getGuiPrevPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) ); this.nextPageBtn = makeTooltipItem( new ItemStack(this.nextPageItem, 1), - config != null ? config.getGuiNextPageItemTitle().get(this.furnaceType) : "Next Page", - String.format(config != null ? config.getGuiNextPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + this.config != null ? this.config.getGuiNextPageItemTitle().get(this.furnaceType) : "Next Page", + String.format(this.config != null ? this.config.getGuiNextPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) ); this.syncToBukkitInventory(); } @@ -261,18 +260,18 @@ public void setCurrentFunctionalArea(int currentFunctionalArea) { } private @NotNull ItemStack addFuelTooltip(@NotNull ItemStack stack) { - String title = config != null ? config.getGuiFuelItemTitle().get(this.furnaceType) : "Fuel"; + String title = this.config != null ? this.config.getGuiFuelItemTitle().get(this.furnaceType) : "Fuel"; String lore = String.format( - config != null ? config.getGuiFuelItemTooltip().get(this.furnaceType) : "Available: %d / %d", + this.config != null ? this.config.getGuiFuelItemTooltip().get(this.furnaceType) : "Available: %d / %d", this.burnTime, this.fuelTimeTotal ); return makeTooltipItem(stack, title, lore); } private @NotNull ItemStack addProgressTooltip(@NotNull ItemStack stack) { - String title = config != null ? config.getGuiProgressItemTitle().get(this.furnaceType) : "Progress"; + String title = this.config != null ? this.config.getGuiProgressItemTitle().get(this.furnaceType) : "Progress"; String lore = String.format( - config != null ? config.getGuiProgressItemTooltip().get(this.furnaceType) : "%d / %d", + this.config != null ? this.config.getGuiProgressItemTooltip().get(this.furnaceType) : "%d / %d", this.cookTime, this.cookTimeTotal ); return makeTooltipItem(stack, title, lore); @@ -318,10 +317,10 @@ public void setCurrentFunctionalArea(int currentFunctionalArea) { this.functionalAreas.get(ca).getGuiStack(this, slot - 45) : this.border; } if (slot >= 9 && slot < 18) { - return this.fuelBar[slot - 9] != null ? this.fuelBar[slot - 9] : this.border; + return this.fuelBar[slot - 9]; } if (slot >= 27 && slot < 36) { - return this.progBar[slot - 27] != null ? this.progBar[slot - 27] : this.border; + return this.progBar[slot - 27]; } if (slot == 51) return this.nextFunctionalBtn; if (slot == 52) return this.prevPageBtn; @@ -335,32 +334,32 @@ public void setStack(int slot, @NotNull ItemStack stack) { while (this.data.inputs.size() <= idx) { this.data.inputs.add(new ItemStack(Material.AIR)); } - this.data.inputs.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + this.data.inputs.set(idx, stack); } else if (this.isFuelRowSlot(slot)) { int idx = this.page * 9 + slot - 18; while (this.data.fuels.size() <= idx) { this.data.fuels.add(new ItemStack(Material.AIR)); } - this.data.fuels.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + this.data.fuels.set(idx, stack); } else if (this.isOutputSlot(slot)) { int idx = this.page * 9 + slot - 36; while (this.data.outputs.size() <= idx) { this.data.outputs.add(new ItemStack(Material.AIR)); } - this.data.outputs.set(idx, stack != null ? stack : new ItemStack(Material.AIR)); + this.data.outputs.set(idx, stack); } else if (this.isFunctionalAreaSlot(slot)) { int ca = this.getCurrentFunctionalArea(); if (ca < this.functionalAreas.size()) { FureamFunctionalArea area = this.functionalAreas.get(ca); if (area.isStorable(this, slot - 45)) { - area.setGuiStack(this, slot - 45, stack != null ? stack : new ItemStack(Material.AIR)); + area.setGuiStack(this, slot - 45, stack); } } } } public boolean isValid(int slot, @NotNull ItemStack stack) { - if (stack != null && !stack.getType().isAir()) { + if (!stack.getType().isAir()) { if (this.isInputSlot(slot)) return true; if (this.isFuelRowSlot(slot)) return FureamFurnaceLogic.isFuel(stack); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java index b0cf0b6..762635a 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java @@ -14,7 +14,6 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.CookingRecipe; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -69,7 +68,7 @@ private static Material parseMaterial(@Nullable String id, Material fallback) { @Contract(pure = true) public @NotNull ItemStack getGuiStack(FureamScreenInventory inv, int slot) { switch (slot) { - case 0: return inv.data.recipeOverridingInput != null ? inv.data.recipeOverridingInput : new ItemStack(Material.AIR); + case 0: return inv.data.recipeOverridingInput; case 1: return this.prevRecipeBtn; case 2: return this.nextRecipeBtn; case 3: return this.recipeResultPreview; @@ -137,17 +136,17 @@ public boolean isTakable(FureamScreenInventory inv, int slot) { public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex) { ItemStack toFind = inv.data.recipeOverridingInput; FurnaceType type = inv.furnaceType; - if (toFind == null || toFind.getType().isAir()) { + if (toFind.getType().isAir()) { this.selectedRecipeIndex = 0; this.prevRecipeBtn = makeTooltipItem( new ItemStack(this.prevRecipeItem, 1), - config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", - config != null ? config.getGuiPrevRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" + this.config != null ? this.config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + this.config != null ? this.config.getGuiPrevRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" ); this.nextRecipeBtn = makeTooltipItem( new ItemStack(this.nextRecipeItem, 1), - config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", - config != null ? config.getGuiNextRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" + this.config != null ? this.config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + this.config != null ? this.config.getGuiNextRecipeEmptyItemTooltip().get(type) : "Please insert an item into the slot on the left to adjust" ); this.recipeResultPreview = new ItemStack(Material.AIR); return; @@ -159,13 +158,13 @@ public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex inv.data.overriddenRecipes.put(new KeyableItemStack(toFind), recipe.getKey()); this.prevRecipeBtn = makeTooltipItem( new ItemStack(this.prevRecipeItem, 1), - config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", - String.format(config != null ? config.getGuiPrevRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) + this.config != null ? this.config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + String.format(this.config != null ? this.config.getGuiPrevRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) ); this.nextRecipeBtn = makeTooltipItem( new ItemStack(this.nextRecipeItem, 1), - config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", - String.format(config != null ? config.getGuiNextRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) + this.config != null ? this.config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + String.format(this.config != null ? this.config.getGuiNextRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) ); this.recipeResultPreview = recipe.getResult().clone(); } else { @@ -173,28 +172,28 @@ public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex inv.data.overriddenRecipes.remove(new KeyableItemStack(toFind)); this.prevRecipeBtn = makeTooltipItem( new ItemStack(this.prevRecipeItem, 1), - config != null ? config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", - String.format(config != null ? config.getGuiPrevRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) + this.config != null ? this.config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", + String.format(this.config != null ? this.config.getGuiPrevRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) ); this.nextRecipeBtn = makeTooltipItem( new ItemStack(this.nextRecipeItem, 1), - config != null ? config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", - String.format(config != null ? config.getGuiNextRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) + this.config != null ? this.config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", + String.format(this.config != null ? this.config.getGuiNextRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) ); - CookingRecipe recipe = FureamFurnaceLogic.findRecipe(displayWorld(inv), toFind, inv.furnaceType).orElse(null); + CookingRecipe recipe = FureamFurnaceLogic.findRecipe(this.displayWorld(inv), toFind, inv.furnaceType).orElse(null); this.recipeResultPreview = recipe != null ? recipe.getResult().clone() : new ItemStack(Material.AIR); } } private org.bukkit.World displayWorld(FureamScreenInventory inv) { - return inv.location != null ? inv.location.getWorld() : null; + return inv.location.getWorld(); } public void updateMatchingRecipes(FureamScreenInventory inv) { this.currentMatchingRecipes.clear(); ItemStack toFind = inv.data.recipeOverridingInput; - if (toFind != null && !toFind.getType().isAir()) { - List> matches = FureamFurnaceLogic.findAllMatches(displayWorld(inv), toFind, inv.furnaceType); + if (!toFind.getType().isAir()) { + List> matches = FureamFurnaceLogic.findAllMatches(this.displayWorld(inv), toFind, inv.furnaceType); matches.sort(Comparator.comparing(r -> r.getKey().toString())); this.currentMatchingRecipes.addAll(matches); } @@ -203,7 +202,7 @@ public void updateMatchingRecipes(FureamScreenInventory inv) { public void updateRecipeSettingArea(FureamScreenInventory inv) { this.updateMatchingRecipes(inv); ItemStack toFind = inv.data.recipeOverridingInput; - if (toFind == null || toFind.getType().isAir()) { + if (toFind.getType().isAir()) { this.switchSelectedRecipeIndex(inv, 0); } else { NamespacedKey overridden = inv.data.overriddenRecipes.get(new KeyableItemStack(toFind)); @@ -230,10 +229,10 @@ public void refreshVisuals(FureamScreenInventory inv) { this.cachedXp = inv.data.experience; FurnaceType type = inv.furnaceType; String title = ((int) inv.data.experience > 0) ? - (config != null ? config.getGuiXpIndicatorGainableItemTitle().get(type) : "XP (click to gain)") : - (config != null ? config.getGuiXpIndicatorItemTitle().get(type) : "XP"); + (this.config != null ? this.config.getGuiXpIndicatorGainableItemTitle().get(type) : "XP (click to gain)") : + (this.config != null ? this.config.getGuiXpIndicatorItemTitle().get(type) : "XP"); String tooltip = String.format( - config != null ? config.getGuiXpIndicatorItemTooltip().get(type) : "Current: %.1f", + this.config != null ? this.config.getGuiXpIndicatorItemTooltip().get(type) : "Current: %.1f", this.cachedXp ); this.xpIndicator = makeTooltipItem(new ItemStack(this.xpIndicatorItem, 1), title, tooltip); From 6941c5a4b35ac54d14f226e204d4a703d23b7474 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 10:34:37 +0800 Subject: [PATCH 03/13] refactor(spigot): decouple Fuream core from Fuream-Extras upgrade logic --- .../eat_ram/fuream/hook/FurnaceManager.java | 43 ---------------- .../fuream/logic/FureamFurnaceEngine.java | 51 ++----------------- .../fuream/screen/FureamScreenInventory.java | 6 +-- 3 files changed, 7 insertions(+), 93 deletions(-) diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index 95f0076..1a17f02 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -1,6 +1,5 @@ package io.github.eat_ram.fuream.hook; -import java.lang.reflect.Field; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -78,48 +77,6 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { } return null; } - - public double getRateUpgradeFactor() { - FureamData extra = this.extraData.get("fuream-extras-furnace-upgrades"); - if (extra != null) { - try { - Field f = extra.getClass().getField("RATE_UPGRADE_FACTORS"); - Map map = (Map) f.get(null); - Field rateStackField = extra.getClass().getField("rateUpgradeStack"); - ItemStack stack = (ItemStack) rateStackField.get(extra); - if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { - Class ciClass = Class.forName("io.github.eat_ram.fuream.extras.fu.CountedItem"); - Object countedItem = ciClass.getConstructor(ItemStack.class).newInstance(stack); - Object val = map.get(countedItem); - if (val instanceof Number) { - return ((Number) val).doubleValue(); - } - } - } catch (Exception ignored) {} - } - return 1.0; - } - - public double getFuelUpgradeFactor() { - FureamData extra = this.extraData.get("fuream-extras-furnace-upgrades"); - if (extra != null) { - try { - Field f = extra.getClass().getField("FUEL_UPGRADE_FACTORS"); - Map map = (Map) f.get(null); - Field fuelStackField = extra.getClass().getField("fuelUpgradeStack"); - ItemStack stack = (ItemStack) fuelStackField.get(extra); - if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { - Class ciClass = Class.forName("io.github.eat_ram.fuream.extras.fu.CountedItem"); - Object countedItem = ciClass.getConstructor(ItemStack.class).newInstance(stack); - Object val = map.get(countedItem); - if (val instanceof Number) { - return ((Number) val).doubleValue(); - } - } - } catch (Exception ignored) {} - } - return 1.0; - } } public static @Nullable FurnaceContext getOrCreateContext(@NotNull Block block) { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index e477e73..23e6d53 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -36,10 +36,11 @@ public static void tick( while (data.fuels.size() < fuelCount) data.fuels.add(new ItemStack(Material.AIR)); while (data.outputs.size() < outCount) data.outputs.add(new ItemStack(Material.AIR)); - // Always compute cookTimeTotal according to current rate factor + // Ensure default cookTimeTotal int baseCookTime = ctx.type == FurnaceType.FURNACE ? 200 : 100; - double rateUpgradeFactor = ctx.getRateUpgradeFactor(); - ctx.cookTimeTotal = Math.max(1, (int) Math.ceil(baseCookTime / rateUpgradeFactor)); + if (ctx.cookTimeTotal <= 0) { + ctx.cookTimeTotal = baseCookTime; + } // Ingest and feed through vanilla furnace inventory (for seamless hopper integration) Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); @@ -127,9 +128,7 @@ public static void tick( } if (fuelStack != null) { - int baseFuelTime = FuelTable.getFuelTime(fuelStack); - double fuelUpgradeFactor = ctx.getFuelUpgradeFactor(); - int fuelTime = (int) Math.round(baseFuelTime * fuelUpgradeFactor); + int fuelTime = FuelTable.getFuelTime(fuelStack); ctx.burnTime = fuelTime; ctx.fuelTimeTotal = fuelTime; @@ -188,46 +187,6 @@ public static void tick( if (inputStack.getAmount() <= 0) { data.inputs.set(firstInputIdx, new ItemStack(Material.AIR)); } - - // Deduct fuel time compensation so that speed upgrade does not artificially multiply fuel capacity - int extraBurnTimeCost = baseCookTime - ctx.cookTimeTotal; - if (extraBurnTimeCost > 0) { - ctx.burnTime -= extraBurnTimeCost; - while (ctx.burnTime <= 0) { - int nextFuelIdx = -1; - ItemStack nextFuelStack = null; - for (int i = 0; i < data.fuels.size(); i++) { - ItemStack s = data.fuels.get(i); - if (!s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { - nextFuelIdx = i; - nextFuelStack = s; - break; - } - } - if (nextFuelStack == null) { - break; - } - int nextBaseFuelTime = FuelTable.getFuelTime(nextFuelStack); - double nextFuelFactor = ctx.getFuelUpgradeFactor(); - int nextFuelTime = (int) Math.round(nextBaseFuelTime * nextFuelFactor); - if (nextFuelTime <= 0) { - break; - } - ctx.burnTime += nextFuelTime; - ctx.fuelTimeTotal = nextFuelTime; - if (nextFuelStack.getType() == Material.LAVA_BUCKET) { - data.fuels.set(nextFuelIdx, new ItemStack(Material.BUCKET)); - } else { - nextFuelStack.setAmount(nextFuelStack.getAmount() - 1); - if (nextFuelStack.getAmount() <= 0) { - data.fuels.set(nextFuelIdx, new ItemStack(Material.AIR)); - } - } - } - if (ctx.burnTime < 0) { - ctx.burnTime = 0; - } - } } } else if (ctx.cookTime > 0) { ctx.cookTime = Math.max(0, ctx.cookTime - 2); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java index 09c4178..099d4a0 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -136,14 +136,12 @@ private static Material parseMaterial(@Nullable String id, Material fallback) { } public int computeCookTimeTotal() { - int base = this.furnaceType == FurnaceType.FURNACE ? 200 : 100; FurnacePos pos = new FurnacePos(this.location); FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); if (ctx != null) { - double factor = ctx.getRateUpgradeFactor(); - return Math.max(1, (int) Math.ceil(base / factor)); + return Math.max(1, ctx.cookTimeTotal); } - return base; + return this.furnaceType == FurnaceType.FURNACE ? 200 : 100; } public boolean isInputSlot(int slot) { From b1c32ac17ea050b881aba3b9fb47b1f0e3d0e4a8 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 10:35:21 +0800 Subject: [PATCH 04/13] refactor: clean up api build and ensure 100% strict spigot-only changes --- api/build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index d1da533..61137b2 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -9,10 +9,6 @@ base { archivesName = "fuream-api" } -repositories { - mavenCentral() -} - dependencies { compileOnly("org.jetbrains:annotations:26.0.2") } From 83e5d9f4d07463219729b753a7151072b0634edd Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 10:41:40 +0800 Subject: [PATCH 05/13] feat(spigot): support multi-version compatibility (1.14 to 1.21+) in a single universal jar --- spigot/build.gradle.kts | 5 + .../fuream/hook/BlockBreakListener.java | 18 -- .../eat_ram/fuream/logic/FuelTable.java | 263 +++++++++++------- spigot/src/main/resources/plugin.yml | 2 +- 4 files changed, 166 insertions(+), 122 deletions(-) diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index adc1ed6..06f520e 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -6,6 +6,11 @@ plugins { group = "io.github.eat-ram.fuream-spigot" version = "0.0.1" evaluationDependsOn(":api") +project(":api") { + repositories { + mavenCentral() + } +} jvmdg.downgradeTo = JavaVersion.VERSION_1_8 base { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java index f48b50e..14e26c3 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java @@ -1,7 +1,5 @@ package io.github.eat_ram.fuream.hook; -import java.lang.reflect.Field; - import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.data.FureamData; @@ -58,22 +56,6 @@ private void handleFurnaceBreak(Block block) { Location loc = block.getLocation(); FureamFurnaceLogic.dropOnBreak(loc, ctx.data); - // Handle extra data items (e.g. upgrades) - for (FureamData extra : ctx.extraData.values()) { - try { - Field rateField = extra.getClass().getField("rateUpgradeStack"); - ItemStack rate = (ItemStack) rateField.get(extra); - if (rate != null && !rate.getType().isAir()) { - loc.getWorld().dropItemNaturally(loc, rate.clone()); - } - Field fuelField = extra.getClass().getField("fuelUpgradeStack"); - ItemStack fuel = (ItemStack) fuelField.get(extra); - if (fuel != null && !fuel.getType().isAir()) { - loc.getWorld().dropItemNaturally(loc, fuel.clone()); - } - } catch (Exception ignored) {} - } - if (ctx.activeGui != null) { for (org.bukkit.entity.HumanEntity viewer : new java.util.ArrayList<>(ctx.activeGui.bukkitInventory.getViewers())) { viewer.closeInventory(); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java index 2a3ea22..90f9643 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java @@ -5,6 +5,7 @@ import java.util.EnumMap; import java.util.Map; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; @@ -19,8 +20,9 @@ public abstract class FuelTable { public static void initDynamic() { try { - // Dynamically synchronize with official server fuel registry if available - Class furnaceClass = Class.forName("net.minecraft.world.level.block.entity.TileEntityFurnace"); + Class furnaceClass = getFurnaceNmsClass(); + if (furnaceClass == null) return; + Method getFuelMethod = null; for (Method m : furnaceClass.getDeclaredMethods()) { if (Modifier.isStatic(m.getModifiers()) && Map.class.isAssignableFrom(m.getReturnType()) && m.getParameterCount() == 0) { @@ -28,17 +30,29 @@ public static void initDynamic() { break; } } - if (getFuelMethod != null) { - getFuelMethod.setAccessible(true); - Map nmsFuelMap = (Map) getFuelMethod.invoke(null); - if (nmsFuelMap != null && !nmsFuelMap.isEmpty()) { - Class craftItemClass = Class.forName("org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers"); - Method getMaterialMethod = craftItemClass.getMethod("getMaterial", Class.forName("net.minecraft.world.item.Item")); - for (Map.Entry entry : nmsFuelMap.entrySet()) { - Material mat = (Material) getMaterialMethod.invoke(null, entry.getKey()); - if (mat != null && entry.getValue() != null && entry.getValue() > 0) { - FUEL_TIMES.put(mat, entry.getValue()); - } + if (getFuelMethod == null) return; + + getFuelMethod.setAccessible(true); + Map nmsFuelMap = (Map) getFuelMethod.invoke(null); + if (nmsFuelMap == null || nmsFuelMap.isEmpty()) return; + + Class craftMagicClass = getCraftMagicNumbersClass(); + if (craftMagicClass == null) return; + + Method getMaterialMethod = null; + for (Method m : craftMagicClass.getMethods()) { + if (m.getName().equals("getMaterial") && m.getParameterCount() == 1 && m.getReturnType() == Material.class) { + getMaterialMethod = m; + break; + } + } + if (getMaterialMethod == null) return; + + for (Map.Entry entry : nmsFuelMap.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null && entry.getValue() > 0) { + Material mat = (Material) getMaterialMethod.invoke(null, entry.getKey()); + if (mat != null) { + FUEL_TIMES.put(mat, entry.getValue()); } } } @@ -47,120 +61,163 @@ public static void initDynamic() { } } + private static @Nullable Class getFurnaceNmsClass() { + String serverPkg = Bukkit.getServer() != null ? Bukkit.getServer().getClass().getPackage().getName() : ""; + String[] candidates = { + "net.minecraft.world.level.block.entity.TileEntityFurnace", + "net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity", + serverPkg + ".block.entity.TileEntityFurnace", + serverPkg.replace("org.bukkit.craftbukkit", "net.minecraft.server") + ".TileEntityFurnace" + }; + for (String c : candidates) { + try { + return Class.forName(c); + } catch (ClassNotFoundException ignored) {} + } + return null; + } + + private static @Nullable Class getCraftMagicNumbersClass() { + String serverPkg = Bukkit.getServer() != null ? Bukkit.getServer().getClass().getPackage().getName() : ""; + String[] candidates = { + serverPkg + ".util.CraftMagicNumbers", + "org.bukkit.craftbukkit.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_20_R2.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_20_R3.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_19_R3.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_18_R2.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_17_R1.util.CraftMagicNumbers", + "org.bukkit.craftbukkit.v1_16_R3.util.CraftMagicNumbers" + }; + for (String c : candidates) { + try { + return Class.forName(c); + } catch (ClassNotFoundException ignored) {} + } + return null; + } + + private static void safePut(String materialName, int ticks) { + Material mat = Material.matchMaterial(materialName); + if (mat != null) { + FUEL_TIMES.put(mat, ticks); + } + } + + private static void safePutAll(String[] names, int ticks) { + for (String n : names) { + safePut(n, ticks); + } + } + private static void loadDefaults() { // High tier - FUEL_TIMES.put(Material.LAVA_BUCKET, 20000); - FUEL_TIMES.put(Material.COAL_BLOCK, 16000); - FUEL_TIMES.put(Material.DRIED_KELP_BLOCK, 4000); - FUEL_TIMES.put(Material.BLAZE_ROD, 2400); - FUEL_TIMES.put(Material.COAL, 1600); - FUEL_TIMES.put(Material.CHARCOAL, 1600); - - // Boats - Material[] boats = { - Material.OAK_BOAT, Material.SPRUCE_BOAT, Material.BIRCH_BOAT, Material.JUNGLE_BOAT, - Material.ACACIA_BOAT, Material.DARK_OAK_BOAT, Material.MANGROVE_BOAT, Material.CHERRY_BOAT, - Material.BAMBOO_RAFT, - Material.OAK_CHEST_BOAT, Material.SPRUCE_CHEST_BOAT, Material.BIRCH_CHEST_BOAT, Material.JUNGLE_CHEST_BOAT, - Material.ACACIA_CHEST_BOAT, Material.DARK_OAK_CHEST_BOAT, Material.MANGROVE_CHEST_BOAT, Material.CHERRY_CHEST_BOAT, - Material.BAMBOO_CHEST_RAFT - }; - for (Material m : boats) FUEL_TIMES.put(m, 1200); + safePut("LAVA_BUCKET", 20000); + safePut("COAL_BLOCK", 16000); + safePut("DRIED_KELP_BLOCK", 4000); + safePut("BLAZE_ROD", 2400); + safePut("COAL", 1600); + safePut("CHARCOAL", 1600); + + // Boats & Rafts + safePutAll(new String[]{ + "OAK_BOAT", "SPRUCE_BOAT", "BIRCH_BOAT", "JUNGLE_BOAT", + "ACACIA_BOAT", "DARK_OAK_BOAT", "MANGROVE_BOAT", "CHERRY_BOAT", + "BAMBOO_RAFT", + "OAK_CHEST_BOAT", "SPRUCE_CHEST_BOAT", "BIRCH_CHEST_BOAT", "JUNGLE_CHEST_BOAT", + "ACACIA_CHEST_BOAT", "DARK_OAK_CHEST_BOAT", "MANGROVE_CHEST_BOAT", "CHERRY_CHEST_BOAT", + "BAMBOO_CHEST_RAFT" + }, 1200); // Standard wood items (300 ticks) - Material[] woods300 = { + safePutAll(new String[]{ // Logs - Material.OAK_LOG, Material.SPRUCE_LOG, Material.BIRCH_LOG, Material.JUNGLE_LOG, - Material.ACACIA_LOG, Material.DARK_OAK_LOG, Material.MANGROVE_LOG, Material.CHERRY_LOG, - Material.STRIPPED_OAK_LOG, Material.STRIPPED_SPRUCE_LOG, Material.STRIPPED_BIRCH_LOG, Material.STRIPPED_JUNGLE_LOG, - Material.STRIPPED_ACACIA_LOG, Material.STRIPPED_DARK_OAK_LOG, Material.STRIPPED_MANGROVE_LOG, Material.STRIPPED_CHERRY_LOG, + "OAK_LOG", "SPRUCE_LOG", "BIRCH_LOG", "JUNGLE_LOG", + "ACACIA_LOG", "DARK_OAK_LOG", "MANGROVE_LOG", "CHERRY_LOG", + "STRIPPED_OAK_LOG", "STRIPPED_SPRUCE_LOG", "STRIPPED_BIRCH_LOG", "STRIPPED_JUNGLE_LOG", + "STRIPPED_ACACIA_LOG", "STRIPPED_DARK_OAK_LOG", "STRIPPED_MANGROVE_LOG", "STRIPPED_CHERRY_LOG", // Woods - Material.OAK_WOOD, Material.SPRUCE_WOOD, Material.BIRCH_WOOD, Material.JUNGLE_WOOD, - Material.ACACIA_WOOD, Material.DARK_OAK_WOOD, Material.MANGROVE_WOOD, Material.CHERRY_WOOD, - Material.STRIPPED_OAK_WOOD, Material.STRIPPED_SPRUCE_WOOD, Material.STRIPPED_BIRCH_WOOD, Material.STRIPPED_JUNGLE_WOOD, - Material.STRIPPED_ACACIA_WOOD, Material.STRIPPED_DARK_OAK_WOOD, Material.STRIPPED_MANGROVE_WOOD, Material.STRIPPED_CHERRY_WOOD, + "OAK_WOOD", "SPRUCE_WOOD", "BIRCH_WOOD", "JUNGLE_WOOD", + "ACACIA_WOOD", "DARK_OAK_WOOD", "MANGROVE_WOOD", "CHERRY_WOOD", + "STRIPPED_OAK_WOOD", "STRIPPED_SPRUCE_WOOD", "STRIPPED_BIRCH_WOOD", "STRIPPED_JUNGLE_WOOD", + "STRIPPED_ACACIA_WOOD", "STRIPPED_DARK_OAK_WOOD", "STRIPPED_MANGROVE_WOOD", "STRIPPED_CHERRY_WOOD", // Planks - Material.OAK_PLANKS, Material.SPRUCE_PLANKS, Material.BIRCH_PLANKS, Material.JUNGLE_PLANKS, - Material.ACACIA_PLANKS, Material.DARK_OAK_PLANKS, Material.MANGROVE_PLANKS, Material.CHERRY_PLANKS, - Material.BAMBOO_PLANKS, Material.BAMBOO_MOSAIC, + "OAK_PLANKS", "SPRUCE_PLANKS", "BIRCH_PLANKS", "JUNGLE_PLANKS", + "ACACIA_PLANKS", "DARK_OAK_PLANKS", "MANGROVE_PLANKS", "CHERRY_PLANKS", + "BAMBOO_PLANKS", "BAMBOO_MOSAIC", // Stairs - Material.OAK_STAIRS, Material.SPRUCE_STAIRS, Material.BIRCH_STAIRS, Material.JUNGLE_STAIRS, - Material.ACACIA_STAIRS, Material.DARK_OAK_STAIRS, Material.MANGROVE_STAIRS, Material.CHERRY_STAIRS, - Material.BAMBOO_STAIRS, Material.BAMBOO_MOSAIC_STAIRS, + "OAK_STAIRS", "SPRUCE_STAIRS", "BIRCH_STAIRS", "JUNGLE_STAIRS", + "ACACIA_STAIRS", "DARK_OAK_STAIRS", "MANGROVE_STAIRS", "CHERRY_STAIRS", + "BAMBOO_STAIRS", "BAMBOO_MOSAIC_STAIRS", // Fences & Gates - Material.OAK_FENCE, Material.SPRUCE_FENCE, Material.BIRCH_FENCE, Material.JUNGLE_FENCE, - Material.ACACIA_FENCE, Material.DARK_OAK_FENCE, Material.MANGROVE_FENCE, Material.CHERRY_FENCE, Material.BAMBOO_FENCE, - Material.OAK_FENCE_GATE, Material.SPRUCE_FENCE_GATE, Material.BIRCH_FENCE_GATE, Material.JUNGLE_FENCE_GATE, - Material.ACACIA_FENCE_GATE, Material.DARK_OAK_FENCE_GATE, Material.MANGROVE_FENCE_GATE, Material.CHERRY_FENCE_GATE, Material.BAMBOO_FENCE_GATE, + "OAK_FENCE", "SPRUCE_FENCE", "BIRCH_FENCE", "JUNGLE_FENCE", + "ACACIA_FENCE", "DARK_OAK_FENCE", "MANGROVE_FENCE", "CHERRY_FENCE", "BAMBOO_FENCE", + "OAK_FENCE_GATE", "SPRUCE_FENCE_GATE", "BIRCH_FENCE_GATE", "JUNGLE_FENCE_GATE", + "ACACIA_FENCE_GATE", "DARK_OAK_FENCE_GATE", "MANGROVE_FENCE_GATE", "CHERRY_FENCE_GATE", "BAMBOO_FENCE_GATE", // Doors & Trapdoors - Material.OAK_DOOR, Material.SPRUCE_DOOR, Material.BIRCH_DOOR, Material.JUNGLE_DOOR, - Material.ACACIA_DOOR, Material.DARK_OAK_DOOR, Material.MANGROVE_DOOR, Material.CHERRY_DOOR, Material.BAMBOO_DOOR, - Material.OAK_TRAPDOOR, Material.SPRUCE_TRAPDOOR, Material.BIRCH_TRAPDOOR, Material.JUNGLE_TRAPDOOR, - Material.ACACIA_TRAPDOOR, Material.DARK_OAK_TRAPDOOR, Material.MANGROVE_TRAPDOOR, Material.CHERRY_TRAPDOOR, Material.BAMBOO_TRAPDOOR, + "OAK_DOOR", "SPRUCE_DOOR", "BIRCH_DOOR", "JUNGLE_DOOR", + "ACACIA_DOOR", "DARK_OAK_DOOR", "MANGROVE_DOOR", "CHERRY_DOOR", "BAMBOO_DOOR", + "OAK_TRAPDOOR", "SPRUCE_TRAPDOOR", "BIRCH_TRAPDOOR", "JUNGLE_TRAPDOOR", + "ACACIA_TRAPDOOR", "DARK_OAK_TRAPDOOR", "MANGROVE_TRAPDOOR", "CHERRY_TRAPDOOR", "BAMBOO_TRAPDOOR", // Pressure plates & Buttons - Material.OAK_PRESSURE_PLATE, Material.SPRUCE_PRESSURE_PLATE, Material.BIRCH_PRESSURE_PLATE, Material.JUNGLE_PRESSURE_PLATE, - Material.ACACIA_PRESSURE_PLATE, Material.DARK_OAK_PRESSURE_PLATE, Material.MANGROVE_PRESSURE_PLATE, Material.CHERRY_PRESSURE_PLATE, - Material.BAMBOO_PRESSURE_PLATE, + "OAK_PRESSURE_PLATE", "SPRUCE_PRESSURE_PLATE", "BIRCH_PRESSURE_PLATE", "JUNGLE_PRESSURE_PLATE", + "ACACIA_PRESSURE_PLATE", "DARK_OAK_PRESSURE_PLATE", "MANGROVE_PRESSURE_PLATE", "CHERRY_PRESSURE_PLATE", + "BAMBOO_PRESSURE_PLATE", + "OAK_BUTTON", "SPRUCE_BUTTON", "BIRCH_BUTTON", "JUNGLE_BUTTON", + "ACACIA_BUTTON", "DARK_OAK_BUTTON", "MANGROVE_BUTTON", "CHERRY_BUTTON", + "BAMBOO_BUTTON", // Signs & Hanging Signs - Material.OAK_SIGN, Material.SPRUCE_SIGN, Material.BIRCH_SIGN, Material.JUNGLE_SIGN, - Material.ACACIA_SIGN, Material.DARK_OAK_SIGN, Material.MANGROVE_SIGN, Material.CHERRY_SIGN, Material.BAMBOO_SIGN, - Material.OAK_HANGING_SIGN, Material.SPRUCE_HANGING_SIGN, Material.BIRCH_HANGING_SIGN, Material.JUNGLE_HANGING_SIGN, - Material.ACACIA_HANGING_SIGN, Material.DARK_OAK_HANGING_SIGN, Material.MANGROVE_HANGING_SIGN, Material.CHERRY_HANGING_SIGN, Material.BAMBOO_HANGING_SIGN, - // Blocks - Material.CRAFTING_TABLE, Material.BOOKSHELF, Material.CHISELED_BOOKSHELF, Material.CHEST, Material.TRAPPED_CHEST, - Material.BARREL, Material.DAYLIGHT_DETECTOR, Material.JUKEBOX, Material.NOTE_BLOCK, Material.COMPOSTER, - Material.LOOM, Material.FLETCHING_TABLE, Material.CARTOGRAPHY_TABLE, Material.SMITHING_TABLE, Material.LECTERN, - Material.BEEHIVE, + "OAK_SIGN", "SPRUCE_SIGN", "BIRCH_SIGN", "JUNGLE_SIGN", + "ACACIA_SIGN", "DARK_OAK_SIGN", "MANGROVE_SIGN", "CHERRY_SIGN", "BAMBOO_SIGN", + "OAK_HANGING_SIGN", "SPRUCE_HANGING_SIGN", "BIRCH_HANGING_SIGN", "JUNGLE_HANGING_SIGN", + "ACACIA_HANGING_SIGN", "DARK_OAK_HANGING_SIGN", "MANGROVE_HANGING_SIGN", "CHERRY_HANGING_SIGN", "BAMBOO_HANGING_SIGN", + // Utility Blocks + "CRAFTING_TABLE", "BOOKSHELF", "CHISELED_BOOKSHELF", "CHEST", "TRAPPED_CHEST", + "BARREL", "DAYLIGHT_DETECTOR", "JUKEBOX", "NOTE_BLOCK", "COMPOSTER", + "LOOM", "FLETCHING_TABLE", "CARTOGRAPHY_TABLE", "SMITHING_TABLE", "LECTERN", + "BEEHIVE", // Banners - Material.WHITE_BANNER, Material.ORANGE_BANNER, Material.MAGENTA_BANNER, Material.LIGHT_BLUE_BANNER, - Material.YELLOW_BANNER, Material.LIME_BANNER, Material.PINK_BANNER, Material.GRAY_BANNER, - Material.LIGHT_GRAY_BANNER, Material.CYAN_BANNER, Material.PURPLE_BANNER, Material.BLUE_BANNER, - Material.BROWN_BANNER, Material.GREEN_BANNER, Material.RED_BANNER, Material.BLACK_BANNER, + "WHITE_BANNER", "ORANGE_BANNER", "MAGENTA_BANNER", "LIGHT_BLUE_BANNER", + "YELLOW_BANNER", "LIME_BANNER", "PINK_BANNER", "GRAY_BANNER", + "LIGHT_GRAY_BANNER", "CYAN_BANNER", "PURPLE_BANNER", "BLUE_BANNER", + "BROWN_BANNER", "GREEN_BANNER", "RED_BANNER", "BLACK_BANNER", // Tools & Weapons - Material.WOODEN_AXE, Material.WOODEN_HOE, Material.WOODEN_PICKAXE, Material.WOODEN_SHOVEL, Material.WOODEN_SWORD, - Material.BOW, Material.CROSSBOW, Material.FISHING_ROD - }; - for (Material m : woods300) FUEL_TIMES.put(m, 300); + "WOODEN_AXE", "WOODEN_HOE", "WOODEN_PICKAXE", "WOODEN_SHOVEL", "WOODEN_SWORD", + "BOW", "CROSSBOW", "FISHING_ROD" + }, 300); // Slabs (150 ticks) - Material[] slabs150 = { - Material.OAK_SLAB, Material.SPRUCE_SLAB, Material.BIRCH_SLAB, Material.JUNGLE_SLAB, - Material.ACACIA_SLAB, Material.DARK_OAK_SLAB, Material.MANGROVE_SLAB, Material.CHERRY_SLAB, - Material.BAMBOO_SLAB, Material.BAMBOO_MOSAIC_SLAB - }; - for (Material m : slabs150) FUEL_TIMES.put(m, 150); + safePutAll(new String[]{ + "OAK_SLAB", "SPRUCE_SLAB", "BIRCH_SLAB", "JUNGLE_SLAB", + "ACACIA_SLAB", "DARK_OAK_SLAB", "MANGROVE_SLAB", "CHERRY_SLAB", + "BAMBOO_SLAB", "BAMBOO_MOSAIC_SLAB" + }, 150); // 200 ticks - Material[] items200 = { - Material.OAK_BUTTON, Material.SPRUCE_BUTTON, Material.BIRCH_BUTTON, Material.JUNGLE_BUTTON, - Material.ACACIA_BUTTON, Material.DARK_OAK_BUTTON, Material.MANGROVE_BUTTON, Material.CHERRY_BUTTON, - Material.BAMBOO_BUTTON, Material.BOWL, Material.LADDER - }; - for (Material m : items200) FUEL_TIMES.put(m, 200); + safePutAll(new String[]{"BOWL", "LADDER"}, 200); // 100 ticks - Material[] items100 = { - Material.STICK, Material.OAK_SAPLING, Material.SPRUCE_SAPLING, Material.BIRCH_SAPLING, - Material.JUNGLE_SAPLING, Material.ACACIA_SAPLING, Material.DARK_OAK_SAPLING, Material.MANGROVE_PROPAGULE, - Material.CHERRY_SAPLING, Material.BAMBOO, Material.AZALEA, Material.FLOWERING_AZALEA - }; - for (Material m : items100) FUEL_TIMES.put(m, 100); + safePutAll(new String[]{ + "STICK", "OAK_SAPLING", "SPRUCE_SAPLING", "BIRCH_SAPLING", + "JUNGLE_SAPLING", "ACACIA_SAPLING", "DARK_OAK_SAPLING", "MANGROVE_PROPAGULE", + "CHERRY_SAPLING", "BAMBOO", "AZALEA", "FLOWERING_AZALEA" + }, 100); // 67 ticks (wool, carpet) - Material[] items67 = { - Material.WHITE_WOOL, Material.ORANGE_WOOL, Material.MAGENTA_WOOL, Material.LIGHT_BLUE_WOOL, - Material.YELLOW_WOOL, Material.LIME_WOOL, Material.PINK_WOOL, Material.GRAY_WOOL, - Material.LIGHT_GRAY_WOOL, Material.CYAN_WOOL, Material.PURPLE_WOOL, Material.BLUE_WOOL, - Material.BROWN_WOOL, Material.GREEN_WOOL, Material.RED_WOOL, Material.BLACK_WOOL, - Material.WHITE_CARPET, Material.ORANGE_CARPET, Material.MAGENTA_CARPET, Material.LIGHT_BLUE_CARPET, - Material.YELLOW_CARPET, Material.LIME_CARPET, Material.PINK_CARPET, Material.GRAY_CARPET, - Material.LIGHT_GRAY_CARPET, Material.CYAN_CARPET, Material.PURPLE_CARPET, Material.BLUE_CARPET, - Material.BROWN_CARPET, Material.GREEN_CARPET, Material.RED_CARPET, Material.BLACK_CARPET - }; - for (Material m : items67) FUEL_TIMES.put(m, 67); + safePutAll(new String[]{ + "WHITE_WOOL", "ORANGE_WOOL", "MAGENTA_WOOL", "LIGHT_BLUE_WOOL", + "YELLOW_WOOL", "LIME_WOOL", "PINK_WOOL", "GRAY_WOOL", + "LIGHT_GRAY_WOOL", "CYAN_WOOL", "PURPLE_WOOL", "BLUE_WOOL", + "BROWN_WOOL", "GREEN_WOOL", "RED_WOOL", "BLACK_WOOL", + "WHITE_CARPET", "ORANGE_CARPET", "MAGENTA_CARPET", "LIGHT_BLUE_CARPET", + "YELLOW_CARPET", "LIME_CARPET", "PINK_CARPET", "GRAY_CARPET", + "LIGHT_GRAY_CARPET", "CYAN_CARPET", "PURPLE_CARPET", "BLUE_CARPET", + "BROWN_CARPET", "GREEN_CARPET", "RED_CARPET", "BLACK_CARPET" + }, 67); // Scaffolding (50 ticks) - FUEL_TIMES.put(Material.SCAFFOLDING, 50); + safePut("SCAFFOLDING", 50); } public static boolean isFuel(@Nullable ItemStack stack) { diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index d284fd6..0f12799 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: Fuream version: 0.0.1 main: io.github.eat_ram.fuream.FureamMain -api-version: '1.20' +api-version: '1.14' depend: - NBTAPI commands: From 3d78dae8b973120f4fc2ffa21a927c5f86dc9586 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 11:09:48 +0800 Subject: [PATCH 06/13] feat(spigot): auto-generate default world config and support serverconfig path --- .../io/github/eat_ram/fuream/FureamMain.java | 24 ++++++- .../eat_ram/fuream/FureamWorldConfigImpl.java | 70 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java index 58632f2..b3b74f2 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -103,10 +103,30 @@ public void onWorldSave(WorldSaveEvent event) { public static void loadWorldConfig(@NotNull World world) { FureamWorldConfigImpl config = new FureamWorldConfigImpl(); - File configFile = new File(world.getWorldFolder(), "fuream.json"); + File worldFolder = world.getWorldFolder(); + File configFile = new File(worldFolder, "fuream.json"); + File serverconfigFile = new File(new File(worldFolder, "serverconfig"), "fuream.json"); + File serverconfigJson5 = new File(new File(worldFolder, "serverconfig"), "fuream.json5"); + + File targetFile = null; if (configFile.exists()) { + targetFile = configFile; + } else if (serverconfigFile.exists()) { + targetFile = serverconfigFile; + } else if (serverconfigJson5.exists()) { + targetFile = serverconfigJson5; + } else { + targetFile = configFile; + try { + FureamWorldConfigImpl.writeWorldConfig(config, targetFile); + } catch (Exception e) { + Bukkit.getLogger().warning("Failed to create default fuream.json for world " + world.getName() + ": " + e.getMessage()); + } + } + + if (targetFile != null && targetFile.exists()) { try { - FureamWorldConfigImpl.readWorldConfig(config, configFile); + FureamWorldConfigImpl.readWorldConfig(config, targetFile); } catch (Exception e) { Bukkit.getLogger().warning("Failed to load fuream.json for world " + world.getName() + ": " + e.getMessage()); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java index c318bac..0b5b016 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java @@ -18,6 +18,76 @@ import org.jetbrains.annotations.NotNull; public class FureamWorldConfigImpl implements FureamWorldConfig { + public static void writeWorldConfig( + @NotNull FureamWorldConfig config, @NotNull File configFile + ) throws java.io.IOException { + if (configFile.getParentFile() != null) { + configFile.getParentFile().mkdirs(); + } + JsonObject root = new JsonObject(); + + JsonObject inSlots = new JsonObject(); + config.getInputSlotCount().forEach((k, v) -> inSlots.addProperty(k.name(), v)); + root.add("input_slot_count", inSlots); + + JsonObject fuelSlots = new JsonObject(); + config.getFuelSlotCount().forEach((k, v) -> fuelSlots.addProperty(k.name(), v)); + root.add("fuel_slot_count", fuelSlots); + + JsonObject outSlots = new JsonObject(); + config.getOutputSlotCount().forEach((k, v) -> outSlots.addProperty(k.name(), v)); + root.add("output_slot_count", outSlots); + + com.google.gson.JsonArray enabledTypes = new com.google.gson.JsonArray(); + for (FurnaceType type : config.getEnabledFurnaceTypes()) { + enabledTypes.add(type.name()); + } + root.add("enabled_furnace_types", enabledTypes); + + com.google.gson.JsonArray preventsHopper = new com.google.gson.JsonArray(); + for (FurnaceType type : config.getPreventsHopperInsertNonSmeltable()) { + preventsHopper.add(type.name()); + } + root.add("prevents_hopper_insert_non_smeltable", preventsHopper); + + JsonObject border = new JsonObject(); + config.getGuiBorderItemId().forEach((k, v) -> border.addProperty(k.name(), v)); + root.add("gui_border_item_id", border); + + JsonObject fuelLeft = new JsonObject(); + config.getGuiFuelLeftItemId().forEach((k, v) -> fuelLeft.addProperty(k.name(), v)); + root.add("gui_fuel_left_item_id", fuelLeft); + + JsonObject fuelUsed = new JsonObject(); + config.getGuiFuelUsedItemId().forEach((k, v) -> fuelUsed.addProperty(k.name(), v)); + root.add("gui_fuel_used_item_id", fuelUsed); + + JsonObject progDone = new JsonObject(); + config.getGuiProgressDoneItemId().forEach((k, v) -> progDone.addProperty(k.name(), v)); + root.add("gui_progress_done_item_id", progDone); + + JsonObject progRem = new JsonObject(); + config.getGuiProgressRemainingItemId().forEach((k, v) -> progRem.addProperty(k.name(), v)); + root.add("gui_progress_remaining_item_id", progRem); + + JsonObject nextFunc = new JsonObject(); + config.getGuiNextFunctionalAreaItemId().forEach((k, v) -> nextFunc.addProperty(k.name(), v)); + root.add("gui_next_functional_area_item_id", nextFunc); + + JsonObject prevPage = new JsonObject(); + config.getGuiPrevPageItemId().forEach((k, v) -> prevPage.addProperty(k.name(), v)); + root.add("gui_prev_page_item_id", prevPage); + + JsonObject nextPage = new JsonObject(); + config.getGuiNextPageItemId().forEach((k, v) -> nextPage.addProperty(k.name(), v)); + root.add("gui_next_page_item_id", nextPage); + + com.google.gson.Gson gson = new com.google.gson.GsonBuilder().setPrettyPrinting().create(); + try (java.io.FileWriter fw = new java.io.FileWriter(configFile)) { + gson.toJson(root, fw); + } + } + @SuppressWarnings("deprecation") public static void readWorldConfig( @NotNull FureamWorldConfig config, @NotNull File configFile From b0affd1372f50e3d6613df3a0cd6e89421d1e133 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 11:31:48 +0800 Subject: [PATCH 07/13] fix(spigot): halve fuel burn duration and cooking time for smoker and blast furnace --- .../github/eat_ram/fuream/logic/FureamFurnaceEngine.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index 23e6d53..b4b8071 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -111,6 +111,11 @@ public static void tick( outSlotIdx = findFittingOutputSlot(data.outputs, recipeResult); if (outSlotIdx >= 0) { canSmelt = true; + int cookTime = matchedRecipe.getCookingTime(); + if (cookTime <= 0) { + cookTime = (ctx.type == FurnaceType.FURNACE ? 200 : 100); + } + ctx.cookTimeTotal = cookTime; } } @@ -129,6 +134,9 @@ public static void tick( if (fuelStack != null) { int fuelTime = FuelTable.getFuelTime(fuelStack); + if (ctx.type == FurnaceType.SMOKER || ctx.type == FurnaceType.BLAST_FURNACE) { + fuelTime = Math.max(1, fuelTime / 2); + } ctx.burnTime = fuelTime; ctx.fuelTimeTotal = fuelTime; From 4e0a628022d74920a8510ba3933a0e487cf55842 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 11:35:07 +0800 Subject: [PATCH 08/13] fix(spigot): initialize context and screen timing by furnace type --- .../github/eat_ram/fuream/hook/FurnaceManager.java | 6 ++++-- .../eat_ram/fuream/screen/FureamScreenInventory.java | 12 +++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index 1a17f02..c422bf1 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -46,9 +46,9 @@ public static class FurnaceContext implements FureamDataHolder { public final @NotNull Map<@NotNull String, @NotNull FureamData> extraData = new HashMap<>(); public int burnTime; - public int fuelTimeTotal = 200; + public int fuelTimeTotal; public int cookTime; - public int cookTimeTotal = 200; + public int cookTimeTotal; public @Nullable FureamScreenInventory activeGui; public @Nullable FureamScreenHandler handler; @@ -58,6 +58,8 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { this.pos = pos; this.type = type; this.data = new FureamFurnaceData(); + this.cookTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); + this.fuelTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); } public @Nullable World getWorld() { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java index 099d4a0..5c4b6d6 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -110,7 +110,17 @@ public FureamScreenInventory( String title = type == FurnaceType.SMOKER ? "Smoker" : (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace"); this.bukkitInventory = Bukkit.createInventory(null, 54, title); - this.cookTimeTotal = this.computeCookTimeTotal(); + FurnacePos pos = new FurnacePos(this.location); + FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); + if (ctx != null) { + this.burnTime = ctx.burnTime; + this.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); + this.cookTime = ctx.cookTime; + this.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); + } else { + this.cookTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); + this.fuelTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); + } this.switchPage(0); for (BiFunction< ? super FureamScreenInventory, ? super FureamWorldConfig, From 1cf8b21b0c425e4479ce63e76ca910421806ca61 Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 16:02:52 +0800 Subject: [PATCH 09/13] feat(spigot): implement Fabric-compatible root NBT persistence and fix result slot recovery --- spigot/README.md | 15 +- spigot/build.gradle.kts | 8 + .../io/github/eat_ram/fuream/FureamMain.java | 20 ++ .../fuream/data/FureamFurnaceData.java | 45 ++-- .../eat_ram/fuream/hook/FurnaceManager.java | 171 +++++++++--- .../fuream/logic/FureamFurnaceEngine.java | 62 +++-- .../fuream/nbt/FurnaceNbtInstrumentation.java | 154 +++++++++++ .../fuream/nbt/FurnaceRootNbtBridge.java | 250 ++++++++++++++++++ .../github/eat_ram/fuream/util/NbtUtil.java | 18 +- .../fuream/logic/FureamFurnaceEngineTest.java | 93 +++++++ 10 files changed, 750 insertions(+), 86 deletions(-) create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java diff --git a/spigot/README.md b/spigot/README.md index ab20853..ab2e777 100644 --- a/spigot/README.md +++ b/spigot/README.md @@ -1,6 +1,6 @@ # Fuream — Spigot/Paper 移植版 -Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一")原为 **Fabric 1.20.1 服务端模组**(`/tmp/workspace/Fuream`)。本工程是它的移植:引擎 / GUI / 漏斗 / 配置全部用 **纯 Bukkit API** 实现(无手写 NMS、无手写反射),持久化走 **de.tr7zw Item-NBT-API 库**,写入方块实体**真实 NBT**,NBT 路径与原 Fabric 实现**逐字节一致**。 +Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一")原为 **Fabric 1.20.1 服务端模组**(`/tmp/workspace/Fuream`)。本工程是它的移植:引擎 / GUI / 漏斗 / 配置使用 Bukkit API;持久化使用 NBT-API,并在炉子方块实体的读写方法上安装轻量字节码钩子,把数据写入方块实体**真实根 NBT**,NBT 路径与原 Fabric 实现一致。 ## 功能(与原模组 1:1) @@ -15,7 +15,8 @@ Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一") - Spigot 1.20.1 与 Paper 1.20.1(`paper` 内核为超集,直接可用)。 - 目标字节码 Java 17(`options.release = 17`),可用 JDK 17+ 编译/运行。 -- **运行时依赖:NBT-API 插件(de.tr7zw Item-NBT-API ≥ 2.13,包名 `de.tr7zw.changeme.nbtapi`)**,需放入 `plugins/`。本插件 `softdepend: [NBTAPI]`,缺失时自动禁用并给出提示。其余(Bukkit API / Gson)由服务端自带。 +- **运行时依赖:NBT-API 插件(de.tr7zw Item-NBT-API ≥ 2.13)**,需放入 `plugins/`。本插件通过 `depend: [NBTAPI]` 声明依赖;Byte Buddy 与 agent 已打入插件 jar,无需另装。 +- Spigot/Paper 的香草方块实体会丢弃未知根标签,因此不能仅调用 `NBT.modify(BlockState)`。插件会在 `onLoad` 阶段动态安装炉子 `load/save` 钩子;安装失败时插件会拒绝启用,绝不退回 PDC。Java 21 会打印动态 agent 警告,可在启动参数加入 `-XX:+EnableDynamicAgentLoading` 消除该警告。 ## NBT 路径(与原 Fabric 实现一致) @@ -29,13 +30,16 @@ Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一") │ ├─ Outputs List { 物品NBT…, Slot:int } │ ├─ RecipeOverridingInput Compound 物品NBT(空物品时为 {}) │ ├─ OverriddenRecipes List { 物品NBT…, RecipeId:string } +│ ├─ RunningRecipe String(可选,当前熔炼配方 id) │ └─ Xp Float -├─ BurnTime / CookTime / CookTimeTotal Int(香草 BE 根部字段,与原模组 +├─ BurnTime / CookTime / CookTimeTotal Short(香草 BE 根部字段,与原模组 │ ——由香草引擎写入——同路径) └─ Items (香草熔炉原 3 格物品栏,作自动化通道) ``` - `FureamData` 复合标签、内部各键名、item `Slot`/`RecipeId` 键、`Xp` 类型均为原 Fabric `FureamFurnaceData.writeNbt/readNbt` 的原样结构。 +- 旧 Spigot 版本误写入 `PublicBukkitValues` 的数据会在首次加载方块时自动迁移到根 NBT,并移除 Fuream 自己的旧 PDC 键;其他插件的 PDC 不受影响。 +- 要迁移回 Fabric 时,请先在 Fuream Spigot 插件仍启用的情况下正常停服,然后直接用 Fabric 打开该存档。不要先卸载插件再让 Spigot/Paper 加载并保存区块;香草方块实体不识别 `FureamData`,可能在下次保存时将它丢弃。 - `BurnTotal`(相当于香草 `fuelTime`)按香草行为**不持久化**(重载后重新点燃)。 ## 更新日志 — Fuream-update1(跟随上游) @@ -106,7 +110,7 @@ cd FureamSpigot | `api/FureamWorldConfig` / `WorldConfigLoader` | 同(Gson 解析 `fuream.json`,语义逐条一致) | | `data/FureamFurnaceData` | 同(结构/字段一致) | | `data/FureamFurnaceData.writeNbt/readNbt`(存于 BE 的 `FureamData` 标签) | `data/FureamDataCodec`(经 de.tr7zw NBT-API 写入真实 BE NBT,路径逐字节一致) | -| `mixin` 中 `FureamData` NBT 持久化注入(`readNbt`/`writeNbt` @Inject) | `FureamDataCodec.read/write` + `FurnaceManager` 每 tick 或变更时的读写 | +| `mixin` 中 `FureamData` NBT 持久化注入(`readNbt`/`writeNbt` @Inject) | `nbt/FurnaceNbtInstrumentation` + `FurnaceRootNbtBridge` + `FurnaceManager` | | `logic/FureamFurnaceLogic`(引擎部分) | `logic/FureamFurnaceEngine`(自研引擎,注:原模组把引擎注释保留为规范,此处按其复刻) | | `logic/FureamFurnaceLogic`(燃料/配方) | `logic/FuelTable`(香草 1.20.1 燃料表,自字节码逐条导出)+ `logic/BukkitCookingRecipes` | | `screen/FureamScreenHandler` / `FureamScreenInventory` | `screen/FurnaceGuiLayout` + `screen/FurnaceGui` + `hook/GuiListener`(Bukkit 事件版) | @@ -123,7 +127,8 @@ cd FureamSpigot ``` src/main/java/io/github/eat_ram/fuream/ api/ FurnaceType, FureamWorldConfig, WorldConfigLoader - data/ FureamFurnaceData, FureamDataCodec (PDC 持久化) + data/ FureamFurnaceData(Fabric 兼容的 NBT 数据结构) + nbt/ 炉子方块实体 load/save 字节码钩子与根 NBT 桥接 logic/ FuelTable, CookingRecipeQuery(+Bukkit), FureamFurnaceEngine screen/ FurnaceGuiLayout, FurnaceGui hook/ FurnaceManager, GuiListener, FurnaceOpenListener, HopperListener, LifecycleListener diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 06f520e..dc7f1e7 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -31,6 +31,8 @@ repositories { } dependencies { + implementation("net.bytebuddy:byte-buddy:1.17.7") + implementation("net.bytebuddy:byte-buddy-agent:1.17.7") compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") compileOnly("de.tr7zw:item-nbt-api:2.14.1") compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.1") @@ -70,6 +72,12 @@ tasks.withType().configureEach { } tasks.jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.runtimeClasspath.get().map { dependency -> + if (dependency.isDirectory) dependency else zipTree(dependency) + }) { + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class") + } from(rootProject.file("LICENSE")) { into("META-INF") } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java index b3b74f2..635f8d0 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -13,6 +13,7 @@ import io.github.eat_ram.fuream.hook.FurnaceOpenListener; import io.github.eat_ram.fuream.hook.GuiListener; import io.github.eat_ram.fuream.hook.HopperListener; +import io.github.eat_ram.fuream.nbt.FurnaceNbtInstrumentation; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Material; @@ -38,15 +39,34 @@ public class FureamMain extends JavaPlugin implements Listener, CommandExecutor public static final String DATA_KEY = "FureamData"; public static final Map<@NotNull World, @NotNull FureamWorldConfig> WORLD_CONFIGS = new WeakHashMap<>(); private static FureamMain INSTANCE; + private boolean rootNbtReady; public static FureamMain getInstance() { return INSTANCE; } + @Override + public void onLoad() { + try { + FurnaceNbtInstrumentation.install(this); + this.rootNbtReady = true; + } catch (Throwable e) { + this.getLogger().severe( + "Unable to install furnace root-NBT hooks; refusing to fall back to PDC: " + e + ); + e.printStackTrace(); + } + } + @Override public void onEnable() { INSTANCE = this; + if (!this.rootNbtReady) { + this.getServer().getPluginManager().disablePlugin(this); + return; + } + if (!this.getServer().getPluginManager().isPluginEnabled("NBTAPI")) { this.getLogger().severe("Item-NBT-API is required for Fuream to operate! Please install NBT-API plugin."); this.getServer().getPluginManager().disablePlugin(this); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java index 4c3dd30..3a7bf67 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java @@ -15,6 +15,7 @@ import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static io.github.eat_ram.fuream.CollectionUtil.newDefaultedArrayList; @@ -28,6 +29,7 @@ public class FureamFurnaceData implements FureamData { public static final @NotNull String OUTPUTS_KEY = "Outputs"; public static final @NotNull String RECIPE_OVERRIDING_INPUT_KEY = "RecipeOverridingInput"; public static final @NotNull String OVERRIDDEN_RECIPES_KEY = "OverriddenRecipes"; + public static final @NotNull String RUNNING_RECIPE_KEY = "RunningRecipe"; public static final @NotNull String XP_KEY = "Xp"; public final @NotNull ArrayList<@NotNull ItemStack> inputs; @@ -35,6 +37,7 @@ public class FureamFurnaceData implements FureamData { public final @NotNull ArrayList<@NotNull ItemStack> outputs; public @NotNull ItemStack recipeOverridingInput; public final @NotNull HashMap<@NotNull KeyableItemStack, @NotNull NamespacedKey> overriddenRecipes; + public @Nullable NamespacedKey runningRecipe; public float experience; public FureamFurnaceData() { @@ -43,10 +46,14 @@ public FureamFurnaceData() { this.outputs = newDefaultedArrayList(new ItemStack(Material.AIR), 1); this.recipeOverridingInput = new ItemStack(Material.AIR); this.overriddenRecipes = new HashMap<>(); + this.runningRecipe = null; } public boolean hasAny() { if (this.experience > 0f) return true; + if (this.recipeOverridingInput != null && + !this.recipeOverridingInput.getType().isAir()) return true; + if (!this.overriddenRecipes.isEmpty() || this.runningRecipe != null) return true; for (ItemStack s : this.inputs) { if (!s.getType().isAir() && s.getAmount() > 0) return true; } @@ -68,28 +75,31 @@ public void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt) { if (nbt.hasTag(RECIPE_OVERRIDING_INPUT_KEY)) { nbt.removeKey(RECIPE_OVERRIDING_INPUT_KEY); } + ReadWriteNBT overridingInputTag = nbt.getOrCreateCompound(RECIPE_OVERRIDING_INPUT_KEY); if (this.recipeOverridingInput != null && !this.recipeOverridingInput.getType().isAir()) { - ReadWriteNBT tag = nbt.getOrCreateCompound(RECIPE_OVERRIDING_INPUT_KEY); - NbtUtil.writeItemToCompound(tag, this.recipeOverridingInput); + NbtUtil.writeItemToCompound(overridingInputTag, this.recipeOverridingInput); } - if (nbt.hasTag(OVERRIDDEN_RECIPES_KEY)) { - nbt.removeKey(OVERRIDDEN_RECIPES_KEY); - } - if (!this.overriddenRecipes.isEmpty()) { - ReadWriteNBTCompoundList list = nbt.getCompoundList(OVERRIDDEN_RECIPES_KEY); - for (Map.Entry entry : this.overriddenRecipes.entrySet()) { - ItemStack stack = entry.getKey().stack; - if (!stack.getType().isAir()) { - ReadWriteNBT sub = list.addCompound(); - ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); - sub.mergeCompound(itemNbt); - sub.setString("RecipeId", entry.getValue().toString()); - } + ReadWriteNBTCompoundList list = NbtUtil.resetCompoundList(nbt, OVERRIDDEN_RECIPES_KEY); + for (Map.Entry entry : this.overriddenRecipes.entrySet()) { + ItemStack stack = entry.getKey().stack; + if (!stack.getType().isAir()) { + ReadWriteNBT sub = list.addCompound(); + ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); + sub.mergeCompound(itemNbt); + sub.setString("RecipeId", entry.getValue().toString()); } } + + if (this.runningRecipe != null) { + nbt.setString(RUNNING_RECIPE_KEY, this.runningRecipe.toString()); + } else if (nbt.hasTag(RUNNING_RECIPE_KEY)) { + nbt.removeKey(RUNNING_RECIPE_KEY); + } nbt.setFloat(XP_KEY, this.experience); - nbt.setBoolean("FureamInitialized", true); + if (nbt.hasTag("FureamInitialized")) { + nbt.removeKey("FureamInitialized"); + } } @Override @@ -136,6 +146,9 @@ public void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt) { } } } + this.runningRecipe = nbt.hasTag(RUNNING_RECIPE_KEY) + ? NamespacedKey.fromString(nbt.getString(RUNNING_RECIPE_KEY)) + : null; this.experience = nbt.hasTag(XP_KEY) ? nbt.getFloat(XP_KEY) : 0f; } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index c422bf1..354a829 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import de.tr7zw.nbtapi.NBT; @@ -16,6 +17,7 @@ import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; +import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; import io.github.eat_ram.fuream.screen.FureamScreenHandler; import io.github.eat_ram.fuream.screen.FureamScreenInventory; import io.github.eat_ram.fuream.util.FurnacePos; @@ -37,8 +39,20 @@ public class FurnaceManager { public static final Map CONTEXTS = new ConcurrentHashMap<>(); + private static final String PDC_ROOT_KEY = "PublicBukkitValues"; + private static final String NBT_API_MARKER_KEY = "__nbtapi"; private static long tickCounter = 0; + private static final class NbtLoadResult { + private final boolean loaded; + private final boolean hasLegacyData; + + private NbtLoadResult(boolean loaded, boolean hasLegacyData) { + this.loaded = loaded; + this.hasLegacyData = hasLegacyData; + } + } + public static class FurnaceContext implements FureamDataHolder { public final @NotNull FurnacePos pos; public final @NotNull FurnaceType type; @@ -102,49 +116,47 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { BlockState state = block.getState(); if (state instanceof Furnace) { try { - NBT.getPersistentData(state, (ReadableNBT nbt) -> { - if (nbt != null && nbt.hasTag(FureamMain.DATA_KEY)) { - ReadableNBT dataCompound = nbt.getCompound(FureamMain.DATA_KEY); + boolean nativeLoaded = FurnaceRootNbtBridge.loadNativeData(state, ctx); + NbtLoadResult loadResult = NBT.get(state, (ReadableNBT rootNbt) -> { + ReadableNBT legacyNbt = getLegacyPdc(rootNbt); + boolean hasLegacyData = hasFureamStorage(legacyNbt); + ReadableNBT dataCompound = nativeLoaded ? null : getDataCompound(legacyNbt); + + if (dataCompound != null) { ctx.data.readNbt(ctx, dataCompound); for (Map.Entry entry : ctx.extraData.entrySet()) { entry.getValue().readNbt(ctx, dataCompound); } - if (nbt.hasTag("BurnTime")) { - ctx.burnTime = nbt.getInteger("BurnTime"); + if (legacyNbt.hasTag("BurnTime")) { + ctx.burnTime = legacyNbt.getInteger("BurnTime"); } - if (nbt.hasTag("CookTime")) { - ctx.cookTime = nbt.getInteger("CookTime"); + if (legacyNbt.hasTag("CookTime")) { + ctx.cookTime = legacyNbt.getInteger("CookTime"); } - if (nbt.hasTag("CookTimeTotal")) { - ctx.cookTimeTotal = nbt.getInteger("CookTimeTotal"); + if (legacyNbt.hasTag("CookTimeTotal")) { + ctx.cookTimeTotal = legacyNbt.getInteger("CookTimeTotal"); } - } else { - // Vanilla lane migration - Furnace furnaceState = (Furnace) state; - FurnaceInventory inv = furnaceState.getInventory(); - ItemStack smelting = inv.getSmelting(); - ItemStack fuel = inv.getFuel(); - ItemStack result = inv.getResult(); - - if (smelting != null && !smelting.getType().isAir()) ctx.data.inputs.set(0, smelting.clone()); - if (fuel != null && !fuel.getType().isAir()) ctx.data.fuels.set(0, fuel.clone()); - if (result != null && !result.getType().isAir()) ctx.data.outputs.set(0, result.clone()); - - inv.clear(); - ctx.burnTime = furnaceState.getBurnTime(); - ctx.cookTime = furnaceState.getCookTime(); - ctx.cookTimeTotal = furnaceState.getCookTimeTotal() > 0 ? furnaceState.getCookTimeTotal() : 200; - ctx.dirty = true; - saveToNbt(ctx); + return new NbtLoadResult(true, hasLegacyData); } - return true; + + return new NbtLoadResult(nativeLoaded, hasLegacyData); }); + + if (!loadResult.loaded) { + migrateVanillaLane(ctx, (Furnace) state); + } + if (loadResult.hasLegacyData) { + ctx.dirty = true; + } } catch (Exception e) { e.printStackTrace(); } } CONTEXTS.put(pos, ctx); + if (ctx.dirty) { + saveToNbt(ctx); + } return ctx; } @@ -156,16 +168,17 @@ public static void saveToNbt(@NotNull FurnaceContext ctx) { BlockState state = block.getState(); if (state instanceof Furnace) { try { - NBT.modifyPersistentData(state, (ReadWriteNBT nbt) -> { - ReadWriteNBT dataCompound = nbt.getOrCreateCompound(FureamMain.DATA_KEY); - ctx.data.writeNbt(ctx, dataCompound); - for (FureamData extra : ctx.extraData.values()) { - extra.writeNbt(ctx, dataCompound); - } - nbt.setInteger("BurnTime", ctx.burnTime); - nbt.setInteger("CookTime", ctx.cookTime); - nbt.setInteger("CookTimeTotal", ctx.cookTimeTotal); - }); + absorbVanillaLane(ctx, (Furnace) state); + FurnaceRootNbtBridge.store(state, ctx); + boolean hasLegacyData = NBT.get( + state, + (ReadableNBT rootNbt) -> hasFureamStorage(getLegacyPdc(rootNbt)) + ); + if (hasLegacyData) { + FurnaceRootNbtBridge.withoutInjection( + () -> NBT.modifyPersistentData(state, FurnaceManager::removeLegacyPdc) + ); + } ctx.dirty = false; } catch (Exception e) { e.printStackTrace(); @@ -173,6 +186,88 @@ public static void saveToNbt(@NotNull FurnaceContext ctx) { } } + private static void absorbVanillaLane( + @NotNull FurnaceContext ctx, @NotNull Furnace furnaceState + ) { + FurnaceInventory inv = furnaceState.getInventory(); + inv.setSmelting(absorbLaneStack(ctx.data.inputs, inv.getSmelting())); + inv.setFuel(absorbLaneStack(ctx.data.fuels, inv.getFuel())); + inv.setResult(absorbLaneStack(ctx.data.outputs, inv.getResult())); + } + + private static @Nullable ItemStack absorbLaneStack( + @NotNull java.util.List target, @Nullable ItemStack stack + ) { + if (stack == null || stack.getType().isAir() || stack.getAmount() <= 0) { + return null; + } + int maxSlots = Math.max(1, target.size()); + ItemStack remainder = FureamFurnaceEngine.insertStackIntoList(target, stack, maxSlots); + return remainder.getType().isAir() ? null : remainder; + } + + private static void migrateVanillaLane( + @NotNull FurnaceContext ctx, @NotNull Furnace furnaceState + ) { + FurnaceInventory inv = furnaceState.getInventory(); + ItemStack smelting = inv.getSmelting(); + ItemStack fuel = inv.getFuel(); + ItemStack result = inv.getResult(); + + if (smelting != null && !smelting.getType().isAir()) { + ctx.data.inputs.set(0, smelting.clone()); + } + if (fuel != null && !fuel.getType().isAir()) { + ctx.data.fuels.set(0, fuel.clone()); + } + if (result != null && !result.getType().isAir()) { + ctx.data.outputs.set(0, result.clone()); + } + + inv.clear(); + ctx.burnTime = furnaceState.getBurnTime(); + ctx.cookTime = furnaceState.getCookTime(); + ctx.cookTimeTotal = furnaceState.getCookTimeTotal() > 0 + ? furnaceState.getCookTimeTotal() + : (ctx.type == FurnaceType.FURNACE ? 200 : 100); + ctx.dirty = true; + } + + private static @Nullable ReadableNBT getDataCompound(@Nullable ReadableNBT nbt) { + if (nbt == null || !nbt.hasTag(FureamMain.DATA_KEY)) { + return null; + } + return nbt.getCompound(FureamMain.DATA_KEY); + } + + private static @Nullable ReadableNBT getLegacyPdc(@NotNull ReadableNBT rootNbt) { + if (!rootNbt.hasTag(PDC_ROOT_KEY)) { + return null; + } + return rootNbt.getCompound(PDC_ROOT_KEY); + } + + private static boolean hasFureamStorage(@Nullable ReadableNBT nbt) { + return nbt != null && ( + nbt.hasTag(FureamMain.DATA_KEY) || + nbt.hasTag("BurnTime") || + nbt.hasTag("CookTime") || + nbt.hasTag("CookTimeTotal") + ); + } + + private static void removeLegacyPdc(@NotNull ReadWriteNBT legacyNbt) { + legacyNbt.removeKey(FureamMain.DATA_KEY); + legacyNbt.removeKey("BurnTime"); + legacyNbt.removeKey("CookTime"); + legacyNbt.removeKey("CookTimeTotal"); + + Set remaining = legacyNbt.getKeys(); + if (remaining.size() == 1 && remaining.contains(NBT_API_MARKER_KEY)) { + legacyNbt.removeKey(NBT_API_MARKER_KEY); + } + } + public static void tickAll() { Iterator> it = CONTEXTS.entrySet().iterator(); while (it.hasNext()) { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index b4b8071..e4fade5 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -65,14 +65,14 @@ public static void tick( ctx.dirty = true; } - // 3. Feed outputs into result slot (bottom hopper output extraction) + // 3. Recover outputs stranded in the hidden vanilla result slot. + // Bottom hoppers pull directly from data.outputs in FurnaceManager; + // proactively filling this slot would hide the first smelted item + // from the custom GUI until a second item is produced. ItemStack result = inv.getResult(); - if (result == null || result.getType().isAir()) { - ItemStack nextOut = extractFirstNonEmpty(data.outputs); - if (nextOut != null) { - inv.setResult(nextOut); - ctx.dirty = true; - } + if (result != null && !result.getType().isAir() && result.getAmount() > 0) { + inv.setResult(recoverVanillaOutput(data.outputs, result, outCount)); + ctx.dirty = true; } furnaceTile.setCookTime((short) 0); @@ -107,6 +107,11 @@ public static void tick( int outSlotIdx = -1; if (matchedRecipe != null) { + NamespacedKey matchedKey = matchedRecipe.getKey(); + if (!matchedKey.equals(data.runningRecipe)) { + data.runningRecipe = matchedKey; + ctx.dirty = true; + } recipeResult = matchedRecipe.getResult(); outSlotIdx = findFittingOutputSlot(data.outputs, recipeResult); if (outSlotIdx >= 0) { @@ -195,10 +200,17 @@ public static void tick( if (inputStack.getAmount() <= 0) { data.inputs.set(firstInputIdx, new ItemStack(Material.AIR)); } + data.runningRecipe = null; + } + } else { + if (matchedRecipe == null && data.runningRecipe != null) { + data.runningRecipe = null; + ctx.dirty = true; + } + if (ctx.cookTime > 0) { + ctx.cookTime = Math.max(0, ctx.cookTime - 2); + ctx.dirty = true; } - } else if (ctx.cookTime > 0) { - ctx.cookTime = Math.max(0, ctx.cookTime - 2); - ctx.dirty = true; } // Update block lit state @@ -254,25 +266,29 @@ public static void tick( return remaining; } - public static @Nullable ItemStack extractFirstNonEmpty(@NotNull List list) { - for (int i = 0; i < list.size(); i++) { - ItemStack s = list.get(i); - if (s != null && !s.getType().isAir() && s.getAmount() > 0) { - ItemStack single = s.clone(); - single.setAmount(1); - s.setAmount(s.getAmount() - 1); - if (s.getAmount() <= 0) { - list.set(i, new ItemStack(Material.AIR)); - } - return single; - } + static @Nullable ItemStack recoverVanillaOutput( + @NotNull List outputs, @Nullable ItemStack vanillaResult, + int maxSlots + ) { + if (vanillaResult == null || vanillaResult.getType().isAir() || + vanillaResult.getAmount() <= 0) { + return null; } - return null; + ItemStack remainder = insertStackIntoList(outputs, vanillaResult, maxSlots); + return remainder.getType().isAir() ? null : remainder; } private static CookingRecipe getRecipeForInput( World world, ItemStack input, FurnaceContext ctx ) { + if (ctx.data.runningRecipe != null) { + for (CookingRecipe recipe : FureamFurnaceLogic.findAllMatches(world, input, ctx.type)) { + if (recipe.getKey().equals(ctx.data.runningRecipe)) { + return recipe; + } + } + } + NamespacedKey overridden = ctx.data.overriddenRecipes.get(new KeyableItemStack(input)); if (overridden != null) { List> matches = FureamFurnaceLogic.findAllMatches(world, input, ctx.type); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java new file mode 100644 index 0000000..eb8f614 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java @@ -0,0 +1,154 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.function.BiConsumer; + +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * Adds the two persistence hooks that Fabric gets from its furnace mixin. + * Spigot otherwise discards unknown block-entity root tags during load/save. + */ +public final class FurnaceNbtInstrumentation { + private static final String INSTALLED_KEY = "io.github.eat_ram.fuream.root_nbt_instrumented"; + + public static void install(JavaPlugin plugin) { + FurnaceRootNbtBridge.installCallbacks(); + if (Boolean.TRUE.equals(System.getProperties().get(INSTALLED_KEY))) { + return; + } + + Class furnaceClass = findFurnaceClass(); + Method loadMethod = findPersistenceMethod(furnaceClass, true); + Method saveMethod = findPersistenceMethod(furnaceClass, false); + Instrumentation instrumentation = ByteBuddyAgent.install(); + + if (!instrumentation.isModifiableClass(furnaceClass)) { + throw new IllegalStateException("Furnace block entity class is not modifiable: " + furnaceClass.getName()); + } + + ElementMatcher.Junction loadMatcher = named(loadMethod.getName()) + .and(takesArguments(loadMethod.getParameterTypes())); + ElementMatcher.Junction saveMatcher = named(saveMethod.getName()) + .and(takesArguments(saveMethod.getParameterTypes())); + + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .type(named(furnaceClass.getName())) + .transform((builder, type, classLoader, module, protectionDomain) -> builder + .visit(Advice.to(LoadAdvice.class).on(loadMatcher)) + .visit(Advice.to(SaveAdvice.class).on(saveMatcher))) + .installOn(instrumentation); + + System.getProperties().put(INSTALLED_KEY, Boolean.TRUE); + plugin.getLogger().info( + "Installed Fabric-compatible furnace root-NBT hooks on " + furnaceClass.getName() + ); + } + + private static Class findFurnaceClass() { + String[] candidates = { + "net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity", + "net.minecraft.world.level.block.entity.TileEntityFurnace" + }; + for (String candidate : candidates) { + try { + return Class.forName(candidate, false, Bukkit.getServer().getClass().getClassLoader()); + } catch (ClassNotFoundException ignored) { + } + } + + String craftPackage = Bukkit.getServer().getClass().getPackage().getName(); + String version = craftPackage.substring(craftPackage.lastIndexOf('.') + 1); + try { + return Class.forName( + "net.minecraft.server." + version + ".TileEntityFurnace", + false, + Bukkit.getServer().getClass().getClassLoader() + ); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to locate the furnace block entity class", e); + } + } + + private static Method findPersistenceMethod(Class furnaceClass, boolean load) { + String modernName = load ? "loadAdditional" : "saveAdditional"; + for (Method method : furnaceClass.getDeclaredMethods()) { + if (modernName.equals(method.getName()) && hasCompoundFirstArgument(method)) { + return method; + } + } + + for (Method method : furnaceClass.getDeclaredMethods()) { + int modifiers = method.getModifiers(); + boolean accessMatches = load ? Modifier.isPublic(modifiers) : Modifier.isProtected(modifiers); + if (accessMatches && !Modifier.isStatic(modifiers) && + method.getReturnType() == void.class && hasCompoundFirstArgument(method)) { + return method; + } + } + + throw new IllegalStateException( + "Unable to locate furnace " + (load ? "load" : "save") + " method on " + + furnaceClass.getName() + "; declared methods=" + Arrays.toString(furnaceClass.getDeclaredMethods()) + ); + } + + private static boolean hasCompoundFirstArgument(Method method) { + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 0) { + return false; + } + String name = parameters[0].getName(); + return name.endsWith(".CompoundTag") || name.endsWith(".NBTTagCompound"); + } + + public static class LoadAdvice { + @Advice.OnMethodExit + @SuppressWarnings("unchecked") + public static void exit( + @Advice.This Object furnace, + @Advice.Argument(0) Object rootNbt + ) { + Object callback = System.getProperties().get( + "io.github.eat_ram.fuream.root_nbt_load_hook" + ); + if (callback instanceof BiConsumer) { + ((BiConsumer) callback).accept(furnace, rootNbt); + } + } + } + + public static class SaveAdvice { + @Advice.OnMethodExit + @SuppressWarnings("unchecked") + public static void exit( + @Advice.This Object furnace, + @Advice.Argument(0) Object rootNbt + ) { + Object callback = System.getProperties().get( + "io.github.eat_ram.fuream.root_nbt_save_hook" + ); + if (callback instanceof BiConsumer) { + ((BiConsumer) callback).accept(furnace, rootNbt); + } + } + } + + private FurnaceNbtInstrumentation() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java new file mode 100644 index 0000000..cf98014 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java @@ -0,0 +1,250 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiConsumer; + +import de.tr7zw.nbtapi.NBT; +import de.tr7zw.nbtapi.NBTType; +import de.tr7zw.nbtapi.iface.ReadWriteNBT; +import de.tr7zw.nbtapi.iface.ReadableNBT; +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.data.FureamData; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import org.bukkit.block.BlockState; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Bridges instrumented NMS furnace serialization to the Bukkit-side context. */ +public final class FurnaceRootNbtBridge { + private static final String LOAD_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_load_hook"; + private static final String SAVE_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_save_hook"; + private static final String STATE_MAP_KEY = "io.github.eat_ram.fuream.root_nbt_state"; + + private static final ConcurrentMap, Field> TAG_MAP_FIELDS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Method> PUT_SHORT_METHODS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Method> TILE_ENTITY_METHODS = new ConcurrentHashMap<>(); + private static final ThreadLocal INJECTION_SUPPRESSED = new ThreadLocal<>(); + + public static void installCallbacks() { + Properties properties = System.getProperties(); + properties.put(LOAD_HOOK_KEY, (BiConsumer) FurnaceRootNbtBridge::captureLoadedNbt); + properties.put(SAVE_HOOK_KEY, (BiConsumer) FurnaceRootNbtBridge::injectSavedNbt); + stateMap(); + } + + public static boolean loadNativeData( + @NotNull BlockState state, @NotNull FurnaceContext ctx + ) { + Object tileEntity = getTileEntity(state); + if (tileEntity == null) { + return false; + } + + Object[] stored = stateMap().get(tileEntity); + if (stored == null || stored.length < 2 || stored[0] == null) { + return false; + } + + ReadableNBT dataNbt = NBT.wrapNMSTag(stored[0]); + ctx.data.readNbt(ctx, dataNbt); + for (Map.Entry entry : ctx.extraData.entrySet()) { + entry.getValue().readNbt(ctx, dataNbt); + } + + if (stored[1] != null && !(stored[1] instanceof Integer)) { + ReadableNBT rootNbt = NBT.wrapNMSTag(stored[1]); + ctx.burnTime = readNumericTag(rootNbt, "BurnTime", ctx.burnTime); + ctx.cookTime = readNumericTag(rootNbt, "CookTime", ctx.cookTime); + ctx.cookTimeTotal = readNumericTag(rootNbt, "CookTimeTotal", ctx.cookTimeTotal); + } else if (stored.length >= 4) { + ctx.burnTime = (Integer) stored[1]; + ctx.cookTime = (Integer) stored[2]; + ctx.cookTimeTotal = (Integer) stored[3]; + } + + stateMap().put(tileEntity, new Object[] { + stored[0], ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal + }); + return true; + } + + public static void store( + @NotNull BlockState state, @NotNull FurnaceContext ctx + ) { + Object tileEntity = getTileEntity(state); + if (tileEntity == null) { + throw new IllegalStateException("Unable to access the live furnace block entity"); + } + + Object[] previous = stateMap().get(tileEntity); + Object previousData = previous != null && previous.length > 0 ? previous[0] : null; + ReadWriteNBT dataNbt = NBT.createNBTObject(); + if (previousData != null) { + dataNbt.mergeCompound(NBT.wrapNMSTag(previousData)); + } + + ctx.data.writeNbt(ctx, dataNbt); + for (FureamData extra : ctx.extraData.values()) { + extra.writeNbt(ctx, dataNbt); + } + + Object rawData = rawCompound(dataNbt); + stateMap().put(tileEntity, new Object[] { + rawData, ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal + }); + } + + public static void withoutInjection(@NotNull Runnable action) { + INJECTION_SUPPRESSED.set(Boolean.TRUE); + try { + action.run(); + } finally { + INJECTION_SUPPRESSED.remove(); + } + } + + private static void captureLoadedNbt(Object tileEntity, Object rootNbt) { + try { + Object fureamData = tagMap(rootNbt).get(FureamMain.DATA_KEY); + if (fureamData != null) { + stateMap().put(tileEntity, new Object[] {fureamData, rootNbt}); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to capture furnace root NBT", e); + } + } + + private static void injectSavedNbt(Object tileEntity, Object rootNbt) { + if (Boolean.TRUE.equals(INJECTION_SUPPRESSED.get())) { + return; + } + try { + Object[] stored = stateMap().get(tileEntity); + if (stored == null || stored.length == 0) { + return; + } + + if (stored[0] != null) { + tagMap(rootNbt).put(FureamMain.DATA_KEY, stored[0]); + } + if (stored.length < 4 || !(stored[1] instanceof Integer)) { + return; + } + putShort(rootNbt, "BurnTime", (short) (int) (Integer) stored[1]); + putShort(rootNbt, "CookTime", (short) (int) (Integer) stored[2]); + putShort(rootNbt, "CookTimeTotal", (short) (int) (Integer) stored[3]); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to inject furnace root NBT", e); + } + } + + @SuppressWarnings("unchecked") + private static @NotNull Map stateMap() { + Properties properties = System.getProperties(); + synchronized (properties) { + Object existing = properties.get(STATE_MAP_KEY); + if (existing instanceof Map) { + return (Map) existing; + } + Map created = Collections.synchronizedMap(new WeakHashMap<>()); + properties.put(STATE_MAP_KEY, created); + return created; + } + } + + @SuppressWarnings("unchecked") + private static Map tagMap(Object compound) throws ReflectiveOperationException { + Field field = TAG_MAP_FIELDS.get(compound.getClass()); + if (field == null) { + field = findTagMapField(compound.getClass()); + TAG_MAP_FIELDS.put(compound.getClass(), field); + } + return (Map) field.get(compound); + } + + private static Field findTagMapField(Class compoundClass) { + for (Field field : compoundClass.getFields()) { + if (Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + for (Field field : compoundClass.getDeclaredFields()) { + if (Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + throw new IllegalStateException("Unable to locate NBT compound tag map on " + compoundClass.getName()); + } + + private static void putShort(Object compound, String key, short value) + throws ReflectiveOperationException { + Method method = PUT_SHORT_METHODS.get(compound.getClass()); + if (method == null) { + method = findPutShortMethod(compound.getClass()); + PUT_SHORT_METHODS.put(compound.getClass(), method); + } + method.invoke(compound, key, value); + } + + private static Method findPutShortMethod(Class compoundClass) { + for (Method method : compoundClass.getMethods()) { + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 2 && parameters[0] == String.class && + parameters[1] == short.class && method.getReturnType() == void.class) { + method.setAccessible(true); + return method; + } + } + throw new IllegalStateException("Unable to locate NBT putShort on " + compoundClass.getName()); + } + + private static @Nullable Object getTileEntity(@NotNull BlockState state) { + try { + Method method = TILE_ENTITY_METHODS.get(state.getClass()); + if (method == null) { + method = state.getClass().getMethod("getTileEntity"); + method.setAccessible(true); + TILE_ENTITY_METHODS.put(state.getClass(), method); + } + return method.invoke(state); + } catch (ReflectiveOperationException e) { + return null; + } + } + + private static Object rawCompound(ReadWriteNBT nbt) { + try { + Method method = nbt.getClass().getMethod("getCompound"); + method.setAccessible(true); + return method.invoke(nbt); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to unwrap NBT compound", e); + } + } + + private static int readNumericTag( + @Nullable ReadableNBT nbt, @NotNull String key, int fallback + ) { + if (nbt == null || !nbt.hasTag(key)) { + return fallback; + } + NBTType type = nbt.getType(key); + if (type == NBTType.NBTTagShort) return nbt.getShort(key); + if (type == NBTType.NBTTagByte) return nbt.getByte(key); + if (type == NBTType.NBTTagLong) return (int) (long) nbt.getLong(key); + return nbt.getInteger(key); + } + + private FurnaceRootNbtBridge() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java index e8c1635..2d2406a 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java @@ -36,10 +36,7 @@ public static void writeStacks( @NotNull ReadWriteNBT parent, @NotNull String key, @NotNull Iterable<@NotNull ItemStack> stacks ) { - if (parent.hasTag(key)) { - parent.removeKey(key); - } - ReadWriteNBTCompoundList list = parent.getCompoundList(key); + ReadWriteNBTCompoundList list = resetCompoundList(parent, key); int slot = 0; for (ItemStack stack : stacks) { if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { @@ -52,6 +49,19 @@ public static void writeStacks( } } + public static @NotNull ReadWriteNBTCompoundList resetCompoundList( + @NotNull ReadWriteNBT parent, @NotNull String key + ) { + if (parent.hasTag(key)) { + parent.removeKey(key); + } + // NBT-API creates compound-list tags lazily. Merging an explicit empty + // list keeps Fabric's required `Key: []` representation even when no + // entries are added afterwards. + parent.mergeCompound(NBT.parseNBT("{\"" + key + "\":[]}")); + return parent.getCompoundList(key); + } + public static void readStacks( @NotNull ReadableNBT parent, @NotNull String key, @NotNull List target diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java new file mode 100644 index 0000000..8110486 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java @@ -0,0 +1,93 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class FureamFurnaceEngineTest { + @Test + public void emptyVanillaResultDoesNotRemoveFirstVirtualOutput() { + List outputs = outputSlots(stack(Material.IRON_INGOT, 1)); + + ItemStack remainder = FureamFurnaceEngine.recoverVanillaOutput(outputs, null, 1); + + assertNull(remainder); + assertEquals(Material.IRON_INGOT, outputs.get(0).getType()); + assertEquals(1, outputs.get(0).getAmount()); + } + + @Test + public void strandedVanillaResultMovesBackIntoVirtualOutputs() { + List outputs = outputSlots(stack(Material.AIR, 1)); + + ItemStack remainder = FureamFurnaceEngine.recoverVanillaOutput( + outputs, stack(Material.IRON_INGOT, 1), 1 + ); + + assertNull(remainder); + assertEquals(Material.IRON_INGOT, outputs.get(0).getType()); + assertEquals(1, outputs.get(0).getAmount()); + } + + @Test + public void fullVirtualOutputsKeepVanillaRemainder() { + List outputs = outputSlots(stack(Material.IRON_INGOT, 64)); + + ItemStack remainder = FureamFurnaceEngine.recoverVanillaOutput( + outputs, stack(Material.IRON_INGOT, 2), 1 + ); + + assertEquals(64, outputs.get(0).getAmount()); + assertNotNull(remainder); + assertEquals(Material.IRON_INGOT, remainder.getType()); + assertEquals(2, remainder.getAmount()); + } + + @Test + public void partiallyFullVirtualOutputsOnlyLeaveUninsertedRemainder() { + List outputs = outputSlots(stack(Material.IRON_INGOT, 63)); + + ItemStack remainder = FureamFurnaceEngine.recoverVanillaOutput( + outputs, stack(Material.IRON_INGOT, 2), 1 + ); + + assertEquals(64, outputs.get(0).getAmount()); + assertNotNull(remainder); + assertEquals(Material.IRON_INGOT, remainder.getType()); + assertEquals(1, remainder.getAmount()); + } + + private static List outputSlots(ItemStack first) { + List outputs = new ArrayList<>(); + outputs.add(first); + return outputs; + } + + private static ItemStack stack(Material material, int amount) { + return new TestItemStack(material, amount); + } + + /** Avoids the Bukkit ItemFactory dependency in this pure unit test. */ + private static final class TestItemStack extends ItemStack { + private TestItemStack(Material material, int amount) { + super(material, amount); + } + + @Override + public boolean isSimilar(ItemStack stack) { + return stack != null && this.getType() == stack.getType(); + } + + @Override + public int getMaxStackSize() { + return 64; + } + } +} From 961c20c122cd7beff05f9beb4d10cc2a0f4c1eca Mon Sep 17 00:00:00 2001 From: az7627 Date: Wed, 26 Aug 2026 22:58:54 +0800 Subject: [PATCH 10/13] feat(spigot): implement multi-version cross-compatibility (1.7-26.2) and remove NBTAPI dependency - Support MC 1.7 to 26.2 across three variant jars (legacy 1.7-1.12, flattening 1.13-1.20.4, components 1.20.5-26.2) - Remove external NBTAPI dependency; direct native NBT serialization via ByteBuddy and reflection - Add compat layer for blocks, items, recipes, events, and masked 3-slot comparator signal - Implement passive mode projection for disabled furnaces, preserving extra slots in NBT - Isolate GUI sessions per player to support concurrent multi-player furnace interaction - Add explosion handling, wet sponge bucket conversion, and multi-slot output distribution - Support dimension config inheritance from overworld and complete config roundtrip serialization - Add comprehensive unit tests covering version detection, adapters, config, and engine logic --- .../eat_ram/fuream/api/FureamWorldConfig.java | 13 + spigot/README.md | 200 +++---- spigot/build.gradle.kts | 56 +- .../github/eat_ram/fuream/CollectionUtil.java | 4 +- .../io/github/eat_ram/fuream/FureamMain.java | 104 ++-- .../eat_ram/fuream/FureamWorldConfigImpl.java | 141 +++-- .../eat_ram/fuream/compat/BlockCompat.java | 44 ++ .../fuream/compat/ComparatorCompat.java | 239 +++++++++ .../eat_ram/fuream/compat/FurnaceCompat.java | 58 ++ .../fuream/compat/FurnaceEventCompat.java | 38 ++ .../eat_ram/fuream/compat/ItemCompat.java | 48 ++ .../eat_ram/fuream/compat/RecipeCompat.java | 109 ++++ .../eat_ram/fuream/compat/RecipeHandle.java | 20 + .../eat_ram/fuream/compat/ServerVersion.java | 80 +++ .../eat_ram/fuream/compat/VersionAdapter.java | 12 + .../fuream/compat/VersionAdapters.java | 81 +++ .../eat_ram/fuream/data/FureamData.java | 7 +- .../fuream/data/FureamFurnaceData.java | 97 ++-- .../fuream/hook/BlockBreakListener.java | 35 +- .../fuream/hook/BlockExplodeListener.java | 15 + .../eat_ram/fuream/hook/ChunkListener.java | 1 + .../fuream/hook/ComparatorListener.java | 29 + .../eat_ram/fuream/hook/FurnaceManager.java | 444 +++++++++++----- .../fuream/hook/FurnaceOpenListener.java | 40 +- .../eat_ram/fuream/hook/GuiListener.java | 54 +- .../eat_ram/fuream/hook/HopperListener.java | 23 +- .../eat_ram/fuream/logic/FuelTable.java | 24 +- .../fuream/logic/FureamFurnaceEngine.java | 497 +++++++++--------- .../fuream/logic/FureamFurnaceLogic.java | 101 +--- .../fuream/nbt/FurnaceNbtInstrumentation.java | 95 +++- .../fuream/nbt/FurnaceRootNbtBridge.java | 101 ++-- .../eat_ram/fuream/nbt/NativeItemNbt.java | 121 +++++ .../eat_ram/fuream/nbt/NativeNbtCompound.java | 232 ++++++++ .../eat_ram/fuream/nbt/NativeNbtList.java | 71 +++ .../fuream/screen/FureamScreenHandler.java | 145 ++++- .../fuream/screen/FureamScreenInventory.java | 66 ++- .../RecipeSettingAndXpFunctionalArea.java | 57 +- .../eat_ram/fuream/util/KeyableItemStack.java | 17 +- .../github/eat_ram/fuream/util/NbtUtil.java | 112 ++-- spigot/src/main/resources/plugin.yml | 4 +- .../io/github/eat_ram/fuream/ConfigTest.java | 42 ++ .../fuream/compat/ServerVersionTest.java | 24 + .../fuream/compat/VersionAdaptersTest.java | 28 + .../fuream/logic/FureamFurnaceEngineTest.java | 140 +++++ spigot/src/variants/components/plugin.yml | 13 + spigot/src/variants/flattening/plugin.yml | 13 + spigot/src/variants/legacy/plugin.yml | 12 + 47 files changed, 2898 insertions(+), 1009 deletions(-) create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeHandle.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/compat/ServerVersionTest.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/compat/VersionAdaptersTest.java create mode 100644 spigot/src/variants/components/plugin.yml create mode 100644 spigot/src/variants/flattening/plugin.yml create mode 100644 spigot/src/variants/legacy/plugin.yml diff --git a/api/src/main/java/io/github/eat_ram/fuream/api/FureamWorldConfig.java b/api/src/main/java/io/github/eat_ram/fuream/api/FureamWorldConfig.java index c68fa69..59df5eb 100644 --- a/api/src/main/java/io/github/eat_ram/fuream/api/FureamWorldConfig.java +++ b/api/src/main/java/io/github/eat_ram/fuream/api/FureamWorldConfig.java @@ -120,6 +120,19 @@ void setGuiNextPageItemId( // i18n + default @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> getGuiTitle() { + EnumMap titles = new EnumMap<>(FurnaceType.class); + titles.put(FurnaceType.FURNACE, "Furnace"); + titles.put(FurnaceType.SMOKER, "Smoker"); + titles.put(FurnaceType.BLAST_FURNACE, "Blast Furnace"); + return titles; + } + + default void setGuiTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + } + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> getGuiBorderItemTitle(); diff --git a/spigot/README.md b/spigot/README.md index ab2e777..353f075 100644 --- a/spigot/README.md +++ b/spigot/README.md @@ -1,139 +1,113 @@ -# Fuream — Spigot/Paper 移植版 - -Fuream("熔炉格子扩容 & 经验累积 & 配方冲突解决,三合一")原为 **Fabric 1.20.1 服务端模组**(`/tmp/workspace/Fuream`)。本工程是它的移植:引擎 / GUI / 漏斗 / 配置使用 Bukkit API;持久化使用 NBT-API,并在炉子方块实体的读写方法上安装轻量字节码钩子,把数据写入方块实体**真实根 NBT**,NBT 路径与原 Fabric 实现一致。 - -## 功能(与原模组 1:1) - -- **熔炉格子扩容**:每座熔炉拥有可配置的虚似行列(默认 9 输入 / 9 燃料 / 9 输出),实际 27+ 格空间。玩家打开的是自定义 9x6 玻璃面板 GUI(燃料条 / 进度条 / 边框 / 翻页)。 -- **经验累积**:每次合成把配方经验累积到熔炉上,不自动发放;点击 GUI 第 6 行第 5 格(绿色经验指示器)或拆炉时作为经验球发放。 -- **配方冲突解决**:把原料放进 GUI 第 6 行第 1 格(配方设置槽),用 46/47 两个按钮在冲突配方之间切换,结果预览在第 48 格;选择按原料持久化(`overriddenRecipes`)。 -- **漏斗/自动化**:漏斗按列交互——上方喂输入、侧方喂燃料、下方抽取产出(走香草槽作为"通道"),空桶余物会被收回。 -- **每世界配置**:`<世界文件夹>/fuream.json`,字段/语义/默认惰性行为与原模组完全一致。 -- **破坏掉落**:拆炉掉落全部虚似行物品 + 经验。 - -## 兼容性与依赖 - -- Spigot 1.20.1 与 Paper 1.20.1(`paper` 内核为超集,直接可用)。 -- 目标字节码 Java 17(`options.release = 17`),可用 JDK 17+ 编译/运行。 -- **运行时依赖:NBT-API 插件(de.tr7zw Item-NBT-API ≥ 2.13)**,需放入 `plugins/`。本插件通过 `depend: [NBTAPI]` 声明依赖;Byte Buddy 与 agent 已打入插件 jar,无需另装。 -- Spigot/Paper 的香草方块实体会丢弃未知根标签,因此不能仅调用 `NBT.modify(BlockState)`。插件会在 `onLoad` 阶段动态安装炉子 `load/save` 钩子;安装失败时插件会拒绝启用,绝不退回 PDC。Java 21 会打印动态 agent 警告,可在启动参数加入 `-XX:+EnableDynamicAgentLoading` 消除该警告。 - -## NBT 路径(与原 Fabric 实现一致) - -数据存于方块实体(熔炉 TileEntity)的真实 NBT,不用 PDC: - -``` -(Furnace BlockEntity NBT 根) -├─ FureamData 复合标签(仅当 data.hasAny() 时写) -│ ├─ Inputs List { 物品NBT…, Slot:int } -│ ├─ Fuels List { 物品NBT…, Slot:int } -│ ├─ Outputs List { 物品NBT…, Slot:int } -│ ├─ RecipeOverridingInput Compound 物品NBT(空物品时为 {}) -│ ├─ OverriddenRecipes List { 物品NBT…, RecipeId:string } -│ ├─ RunningRecipe String(可选,当前熔炼配方 id) -│ └─ Xp Float -├─ BurnTime / CookTime / CookTimeTotal Short(香草 BE 根部字段,与原模组 -│ ——由香草引擎写入——同路径) -└─ Items (香草熔炉原 3 格物品栏,作自动化通道) +# Fuream Spigot/Purpur 移植版 + +这是 Fabric 1.20.1 服务端模组 Fuream 的 Bukkit 移植。插件提供可扩展的熔炉输入、燃料和输出槽,累积经验,以及冲突配方选择,并保留 Fabric 使用的根级 `FureamData` NBT 结构。 + +## 版本与 Jar + +| 服务端版本 | 使用文件 | `api-version` | 最低 Java | +| --- | --- | --- | --- | +| 1.7-1.12.x | `fuream-legacy-1.7-1.12.jar` | 无 | Java 8 | +| 1.13-1.20.4 | `fuream-flattening-1.13-1.20.4.jar` | `1.13` | Java 8 或服务端要求版本 | +| 1.20.5-26.2 | `fuream-components-1.20.5-26.2.jar` | `1.20.5` | 服务端要求版本 | + +保证范围是 Spigot 和 Purpur。三个 Jar 使用同一套配置和 `FureamData` 格式,世界升级时不需要手动转换。插件主体编译为 Java 8 字节码;高版本服务器仍须使用该版本服务端要求的 Java。 + +插件没有 NBTAPI 或其他外置插件依赖。Byte Buddy、Java 8 JRE 自附加所需的 JNA,以及原生 NBT 桥均已打入 Jar。 + +## 行为 + +- 每种炉子可分别配置输入、燃料和输出槽数量,超过一页时可翻页。 +- 每位玩家有独立的 GUI 会话和页码;只有炉子内的物品数据共享。 +- 支持拖拽、双击、数字键、丢弃键、Shift 点击、输出游标和燃料区空桶操作。 +- 1.13+ 使用服务端配方注册顺序选取首个匹配配方;冲突列表按稳定配方 ID 排序并记住选择。 +- 1.7-1.12 只使用原版唯一熔炉配方,不显示配方选择功能;旧存档内的 override 数据会保留但忽略。 +- 输出可合并并分散到多个槽。湿海绵只有在燃料区另有空槽时才把桶转换为水桶。 +- 自定义烧炼会触发服务器版本支持的 Bukkit 熔炉事件,并接受取消、燃烧时间、烧炼时间和产物修改。 +- 漏斗及漏斗矿车通过原生三槽投影和 `InventoryMoveItemEvent` 交互;底部先抽桶/水桶,再抽输出。 +- 比较器只观察 Fabric 的输入、燃料、输出三槽遮罩视图,而不是全部扩展槽。 +- 炉子锁、八格使用距离、区块状态和方块类型会在 GUI 会话期间持续检查。 + +## 原生 NBT + +数据直接存于炉子方块实体根 NBT,不使用 PDC: + +```text +Furnace BlockEntity root +|- FureamData +| |- Inputs +| |- Fuels +| |- Outputs +| |- RecipeOverridingInput +| |- OverriddenRecipes +| |- RunningRecipe +| `- Xp +|- BurnTime +|- CookTime +`- CookTimeTotal ``` -- `FureamData` 复合标签、内部各键名、item `Slot`/`RecipeId` 键、`Xp` 类型均为原 Fabric `FureamFurnaceData.writeNbt/readNbt` 的原样结构。 -- 旧 Spigot 版本误写入 `PublicBukkitValues` 的数据会在首次加载方块时自动迁移到根 NBT,并移除 Fuream 自己的旧 PDC 键;其他插件的 PDC 不受影响。 -- 要迁移回 Fabric 时,请先在 Fuream Spigot 插件仍启用的情况下正常停服,然后直接用 Fabric 打开该存档。不要先卸载插件再让 Spigot/Paper 加载并保存区块;香草方块实体不识别 `FureamData`,可能在下次保存时将它丢弃。 -- `BurnTotal`(相当于香草 `fuelTime`)按香草行为**不持久化**(重载后重新点燃)。 +物品编码由运行版本适配:1.12 及以前保留数字 ID/耐久值,1.13-1.20.4 使用扁平化物品 NBT,1.20.5+ 使用数据组件格式。`FureamData` 的键名和层级保持不变,可用于 Fabric 1.20.1 -> Spigot/Purpur -> Fabric 的往返迁移。 + +插件在 `onLoad` 阶段为当前 NMS 炉子类匹配加载/保存方法并安装根 NBT 钩子。如果钩子安装失败,插件会在接管任何炉子前拒绝启用,以免静默丢失扩展槽物品。旧版本曾写入 `PublicBukkitValues` 的 Fuream 数据会在首次加载时迁移到根级标签,其他插件的 PDC 不受影响。 -## 更新日志 — Fuream-update1(跟随上游) +在切回 Fabric 前,应让 Fuream 插件保持启用并正常停服。不要先移除插件再用 Bukkit 服务端加载和保存世界,否则原版方块实体可能丢弃未知标签。 -已按 `Fuream-update1`(新版 Fabric 实现)跟进移植的功能: +## 启用、禁用与配置 -- **香草物品迁移**:熔炉无可用的 `FureamData` 标签时(升级前/被接管前是普通香草熔炉),把其香草 3 格物品栏并入虚似行(槽0→输入、槽1→燃料、槽2→输出),并清空通道防重复(对应新版 `AbstractFurnaceBlockEntityMixin.readNbt`;实现在 `FureamDataCodec.migrateVanillaLane`,由 `FurnaceManager` 注册时调用)。 -- **清空语义**:清除虚似行时同步清空配方设置输入(对应新版 `clear()`;`FureamFurnaceData.clearRows()`,拆炉时使用)。 -- **tick 头部保证虚似行可用**:引擎每 tick 在熔炼前 `data.ensureRows(...)` 已覆盖(对应新版 `extendInventories` 提前到 `tick` HEAD;逻辑不变,无需另行移植)。 +普通世界的配置文件是 `/fuream.json`。槽位和 GUI 外观/i18n 字段接受 Fabric 配置格式;写回配置时会完整保存全部已知字段,并保留第三方未知 JSON 字段。 -上游其余改动均为文档/空白/未用 import 清理,无行为差异。 +下界和末地按以下顺序查找配置: -## 更新日志 — Fuream-update2(跟随上游) +1. 当前维度的 `fuream.json` +2. 当前维度的 `serverconfig/fuream.json` 或 `serverconfig/fuream.json5` +3. `server.properties` 的 `level-name` 所指主世界中的相同路径 -- **可配置 GUI 外观 + i18n**(核心功能):`FureamWorldConfig` 扩展”外观“(10 个物品 id,每类型)与“i18n”(20 条标题/提示,含 `%d`/`%.1f` 占位符);`fuream.json` 新增 30 个 `gui_*` 键,支持单值作用于所有类型或按类型对象。默认值与 Fabric 新实现一致(含燃料条提示文本由 “Remaining:” 改为 “Available: %d / %d”)。 - - 移植:`api/FureamWorldConfig`(默认值 + 映射)、`api/WorldConfigLoader`(两种形式的解析)、`screen/FurnaceGui`(按配置解析物品 id → `Registry.MATERIAL`,失败回退默认;全部标题/提示经 `String.format`)。 -- **配置类重构(对齐上游)**:Fabric 把配置创建/解析迁到新类 `FureamWorldConfigImpl`(替代 `FureamMain.newWorldConfig/readWorldConfig`);Spigot 侧本身即单一具体配置类,语义等价(默认值/解析行为不变)。 -- **`FureamScreenInventory`**:取消静态玻璃片常量,改为每 GUI 从配置解析;XP 显示改用浮点缓存比较。Spigot 侧对应 `FurnaceGui` 同步改造。 -- **`FureamScreenHandler.quickMove`**:改为持有原物品引用并返回移动前拷贝 —— 仅返回值语义差异,Spigot 侧为事件式快移,无需移植。 -- 新增 `api/package-info.java`(文档)。 +维度世界没有找到配置时只使用内存默认值,不会自动生成文件。普通世界没有配置时会在自己的世界目录创建默认文件,因此独立普通世界继续保持独立配置。 -## 配置 +没有 `FureamData` 且未启用的炉子不会被迁移或清空。已有 `FureamData` 但当前禁用的炉子进入被动模式:扩展槽仍保存在 NBT,首个有效输入、燃料和输出投影到原版三槽运行。重新启用时会原子收回投影,不复制物品。配置重载、区块卸载和停服都会关闭失效 GUI 并同步数据。 -每个世界的 `world 文件夹/fuream.json`(与原模组格式一致): +管理命令: -```json -{ - "input_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, - "fuel_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, - "output_slot_count": { "FURNACE": 9, "SMOKER": 9, "BLAST_FURNACE": 9 }, - "enabled_furnace_types": ["FURNACE", "SMOKER", "BLAST_FURNACE"] -} +```text +/fuream reload +/fuream status ``` -- 文件缺失 → 全部惰性(不改任何熔炉,香草行为 100% 保留)。 -- 槽位计数只接受正数;未知类型名被忽略;数量超过 9 自动翻页。 -- **GUI 外观与 i18n(Fuream-update2)**:`gui_*` 键可整体配置每类熔炉的装饰物品与文案(边框、燃料条左右、进度条完成/剩余、上一/下一配方按钮、经验指示器、上一/下一页按钮;标题/提示含 `%d`、`%.1f` 占位符)。每个键接受两种形式: - - 单值:`"gui_border_item_id": "minecraft:black_stained_glass_pane"`(作用于所有类型) - - 按类型对象:`"gui_xp_indicator_item_id": {"FURNACE": "minecraft:emerald"}`(缺省类型保持默认) - - 非法物品 id / 未知类型名被忽略并回退默认;默认值与 Fabric 实现完全一致(完整示例见 `docs/fuream.example.json`)。 -- 运行中改配置用 `/fuream reload`(权限 `fuream.reload`)。 -- `/fuream status` 查看各世界启用情况。 +`status` 会显示当前版本适配器、根 NBT 钩子状态、每个世界启用的炉子类型及实际配置来源。 ## 构建 -```bash -cd FureamSpigot -./gradlew build # 产物: build/libs/Fuream-1.0.0.jar (+ sources jar) -./gradlew test # 单元测试(引擎/配置/布局;NBT 持久化需实机验证) -``` +在仓库根目录执行: -构建使用工程内 wrapper(Gradle 8.7,`gradle.properties` 指定 JDK 21 路径;若你的 JDK 21 路径不同请修改 `org.gradle.java.home`,或用系统 JDK 17+ 移除该行)。NBT-API 通过 `repo.codemc.io` 下载(`compileOnly`)。 +```powershell +.\gradlew.bat :spigot:test :spigot:assemble +``` -## 安装 / 实机验收 +产物位于 `spigot/build/libs/`: -1. 安装 NBT-API 插件(≥ 2.13)到 `plugins/`。 -2. 把 `Fuream-1.0.0.jar` 放入 `plugins/`。 -3. 启动 1.20.1 Spigot/Paper,在默认世界根目录放入 `fuream.json`(见上)。 -4. `/fuream status` 应显示已启用的类型与槽位数。 +```text +fuream-legacy-1.7-1.12.jar +fuream-flattening-1.13-1.20.4.jar +fuream-components-1.20.5-26.2.jar +``` -验收清单见 `docs/acceptance.md`。 +## 验证状态 -## 与原 Fabric 工程的文件对照 +纯核心测试覆盖多槽插入/合并、跨槽产出、湿海绵、比较器遮罩、经验与配置完整往返等行为。使用 `Z:\fuream-servers` 中的 Jar 作为只读模板,在一次性本地目录实测了: -| Fabric(Fuream) | Spigot 移植 | +| 服务端 | 结果 | | --- | --- | -| `FureamMain` | `FureamPlugin` + `hook/FurnaceManager` | -| `api/FureamWorldConfig` / `WorldConfigLoader` | 同(Gson 解析 `fuream.json`,语义逐条一致) | -| `data/FureamFurnaceData` | 同(结构/字段一致) | -| `data/FureamFurnaceData.writeNbt/readNbt`(存于 BE 的 `FureamData` 标签) | `data/FureamDataCodec`(经 de.tr7zw NBT-API 写入真实 BE NBT,路径逐字节一致) | -| `mixin` 中 `FureamData` NBT 持久化注入(`readNbt`/`writeNbt` @Inject) | `nbt/FurnaceNbtInstrumentation` + `FurnaceRootNbtBridge` + `FurnaceManager` | -| `logic/FureamFurnaceLogic`(引擎部分) | `logic/FureamFurnaceEngine`(自研引擎,注:原模组把引擎注释保留为规范,此处按其复刻) | -| `logic/FureamFurnaceLogic`(燃料/配方) | `logic/FuelTable`(香草 1.20.1 燃料表,自字节码逐条导出)+ `logic/BukkitCookingRecipes` | -| `screen/FureamScreenHandler` / `FureamScreenInventory` | `screen/FurnaceGuiLayout` + `screen/FurnaceGui` + `hook/GuiListener`(Bukkit 事件版) | -| `util/MaskedInventory` / `FurnaceForHopperInventory` | 不需要(引擎直接读写虚似行;漏斗走 `hook/HopperListener`) | -| mixin(`AbstractFurnaceBlockEntity` / `HopperBlockEntity` / `AbstractFurnaceBlock` / `MinecraftServer`) | `hook/*` 监听器 + 每 tick 调度 | - -## 已知取舍 +| Spigot 1.14.4 | 插件加载、根 NBT 钩子、真实熔炼与持久化通过 | +| Purpur 1.15.2、1.16.5 | 纯 Java 8 JRE 下原生自附加、根 NBT 钩子和插件启用通过 | +| Purpur 1.17.1、1.18.2、1.19.4 | 根 NBT 钩子和插件启用通过 | +| Purpur 1.20.1 | 熔炼、启停无损投影、多槽输出、比较器通过 | +| Purpur 1.20.4 | 根 NBT 钩子、插件启用和关闭通过 | +| Purpur 1.20.6 | 数据组件物品读写、熔炼与持久化通过 | +| Purpur 1.21.1 | 根 NBT 钩子、插件启用和关闭通过 | +| Purpur 1.21.4 | 数据组件物品读写、熔炼与持久化通过 | -- GUI 双击拾取(Double-Click 汇总)、创造模式中键取整格在自定义视图上不生效(按原模组规则已被取消)。 -- 漏斗只做整格投递拦截(避免部分移动的记账复杂度);少量不匹配物品会留在漏斗内,符合香草拒绝行为。 +本次可用模板中没有 1.7.10、1.8.8、1.12.2、1.13.2 和 26.2 服务端 Jar,因此这些边界版本只完成了编译期隔离、1.8.8 API 依赖扫描和版本能力测试,发布前仍应补做对应服务端启动及手工 GUI 验收。双玩家独立分页、距离、锁和快捷键也仍需在目标服务器上做人工验收。 -## 目录结构 +## 已知保留问题 -``` -src/main/java/io/github/eat_ram/fuream/ - api/ FurnaceType, FureamWorldConfig, WorldConfigLoader - data/ FureamFurnaceData(Fabric 兼容的 NBT 数据结构) - nbt/ 炉子方块实体 load/save 字节码钩子与根 NBT 桥接 - logic/ FuelTable, CookingRecipeQuery(+Bukkit), FureamFurnaceEngine - screen/ FurnaceGuiLayout, FurnaceGui - hook/ FurnaceManager, GuiListener, FurnaceOpenListener, HopperListener, LifecycleListener - util/ StackUtil, ItemStackIo(+Bukkit), KeyableItemStack, DefaultableItemList, FurnacePos - FureamPlugin.java -src/main/resources/plugin.yml -src/test/java/... JUnit 5 测试(无服务端依赖,全部离线可跑) -``` +破坏熔炉时仍会掉落“配方设置输入槽”的物品。这是 Fabric 基准版本本身的已知问题,为保持基准行为,本移植不修复这一项。 diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index dc7f1e7..ce1334b 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - id("xyz.wagyourtail.jvmdowngrader") version "1.2.2" `java` } @@ -11,8 +10,6 @@ project(":api") { mavenCentral() } } -jvmdg.downgradeTo = JavaVersion.VERSION_1_8 - base { archivesName = "fuream" } @@ -33,16 +30,15 @@ repositories { dependencies { implementation("net.bytebuddy:byte-buddy:1.17.7") implementation("net.bytebuddy:byte-buddy-agent:1.17.7") + implementation("net.java.dev.jna:jna:5.17.0") + implementation("net.java.dev.jna:jna-platform:5.17.0") compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") - compileOnly("de.tr7zw:item-nbt-api:2.14.1") - compileOnly("de.tr7zw:item-nbt-api-plugin:2.14.1") compileOnly("org.jetbrains:annotations:26.0.2") compileOnly(project(":api")) { isTransitive = false } testImplementation("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") - testImplementation("de.tr7zw:item-nbt-api-plugin:2.14.1") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation(project(":api")) testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -52,7 +48,7 @@ tasks.test { useJUnitPlatform() } -val targetJavaVersion = 17 +val targetJavaVersion = 8 java { val javaVersion = JavaVersion.toVersion(targetJavaVersion) sourceCompatibility = javaVersion @@ -60,7 +56,6 @@ java { if (JavaVersion.current() < javaVersion) { toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) } - withSourcesJar() } tasks.withType().configureEach { @@ -72,16 +67,39 @@ tasks.withType().configureEach { } tasks.jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(configurations.runtimeClasspath.get().map { dependency -> - if (dependency.isDirectory) dependency else zipTree(dependency) - }) { - exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class") - } - from(rootProject.file("LICENSE")) { - into("META-INF") - } - from(zipTree(project(":api").tasks.jar.get().archiveFile)) { - include("**/*.class") + enabled = false +} + +fun registerVariant(taskName: String, fileName: String, resourceDir: String) = + tasks.register(taskName) { + group = "build" + description = "Builds $fileName" + dependsOn(tasks.classes, project(":api").tasks.jar) + archiveFileName.set(fileName) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(sourceSets.main.get().output) { + exclude("plugin.yml") + } + from(resourceDir) + from(configurations.runtimeClasspath.get().map { dependency -> + if (dependency.isDirectory) dependency else zipTree(dependency) + }) { + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class") + } + from(rootProject.file("LICENSE")) { into("META-INF") } + from(zipTree(project(":api").tasks.jar.get().archiveFile)) { include("**/*.class") } } + +val legacyJar = registerVariant( + "legacyJar", "fuream-legacy-1.7-1.12.jar", "src/variants/legacy" +) +val flatteningJar = registerVariant( + "flatteningJar", "fuream-flattening-1.13-1.20.4.jar", "src/variants/flattening" +) +val componentsJar = registerVariant( + "componentsJar", "fuream-components-1.20.5-26.2.jar", "src/variants/components" +) + +tasks.assemble { + dependsOn(legacyJar, flatteningJar, componentsJar) } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java index ffcf59b..809cb63 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java @@ -15,7 +15,7 @@ public static ArrayList newDefaultedArrayList( E defaultValue, @Range(from = 0, to = Integer.MAX_VALUE) int initialSize ) { - ArrayList al = new ArrayList<>() { + ArrayList al = new ArrayList() { @SuppressWarnings("unchecked") private E makeDefault() { if (defaultValue instanceof org.bukkit.inventory.ItemStack) { @@ -53,7 +53,7 @@ public E set(int index, E element) { @Contract(value = "_, _, _ -> new", pure = true) public static ListIterator listIterator(List list, int size, int index) { - return new ListIterator<>() { + return new ListIterator() { public final boolean isListIterator = index >= 0; private int cursor = index < 0 ? 0 : index; diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java index 635f8d0..7b8bb2d 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -1,28 +1,33 @@ package io.github.eat_ram.fuream; import java.io.File; +import java.io.FileInputStream; import java.util.Locale; import java.util.Map; +import java.util.Properties; import java.util.WeakHashMap; +import java.util.logging.Level; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.hook.BlockBreakListener; +import io.github.eat_ram.fuream.hook.BlockExplodeListener; import io.github.eat_ram.fuream.hook.ChunkListener; +import io.github.eat_ram.fuream.hook.ComparatorListener; import io.github.eat_ram.fuream.hook.FurnaceManager; import io.github.eat_ram.fuream.hook.FurnaceOpenListener; import io.github.eat_ram.fuream.hook.GuiListener; import io.github.eat_ram.fuream.hook.HopperListener; +import io.github.eat_ram.fuream.compat.ServerVersion; +import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.nbt.FurnaceNbtInstrumentation; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Material; import org.bukkit.World; -import org.bukkit.block.BlastFurnace; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.Furnace; -import org.bukkit.block.Smoker; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -38,6 +43,7 @@ public class FureamMain extends JavaPlugin implements Listener, CommandExecutor { public static final String DATA_KEY = "FureamData"; public static final Map<@NotNull World, @NotNull FureamWorldConfig> WORLD_CONFIGS = new WeakHashMap<>(); + public static final Map<@NotNull World, @NotNull File> WORLD_CONFIG_SOURCES = new WeakHashMap<>(); private static FureamMain INSTANCE; private boolean rootNbtReady; @@ -54,7 +60,7 @@ public void onLoad() { this.getLogger().severe( "Unable to install furnace root-NBT hooks; refusing to fall back to PDC: " + e ); - e.printStackTrace(); + this.getLogger().log(Level.SEVERE, "Root-NBT hook installation failed", e); } } @@ -67,18 +73,19 @@ public void onEnable() { return; } - if (!this.getServer().getPluginManager().isPluginEnabled("NBTAPI")) { - this.getLogger().severe("Item-NBT-API is required for Fuream to operate! Please install NBT-API plugin."); - this.getServer().getPluginManager().disablePlugin(this); - return; - } - // Register listeners this.getServer().getPluginManager().registerEvents(this, this); this.getServer().getPluginManager().registerEvents(new FurnaceOpenListener(), this); this.getServer().getPluginManager().registerEvents(new GuiListener(), this); this.getServer().getPluginManager().registerEvents(new HopperListener(), this); + this.getServer().getPluginManager().registerEvents(new ComparatorListener(), this); this.getServer().getPluginManager().registerEvents(new BlockBreakListener(), this); + try { + Class.forName("org.bukkit.event.block.BlockExplodeEvent", false, this.getClassLoader()); + this.getServer().getPluginManager().registerEvents(new BlockExplodeListener(), this); + } catch (ClassNotFoundException ignored) { + // BlockExplodeEvent was added after the 1.7 API. + } this.getServer().getPluginManager().registerEvents(new ChunkListener(), this); // Load configs and scan loaded chunks for existing furnaces @@ -106,8 +113,7 @@ public void onEnable() { @Override public void onDisable() { - FurnaceManager.flushAll(); - FurnaceManager.CONTEXTS.clear(); + FurnaceManager.shutdown(); this.getLogger().info("Fuream (Spigot) disabled successfully!"); } @@ -123,24 +129,19 @@ public void onWorldSave(WorldSaveEvent event) { public static void loadWorldConfig(@NotNull World world) { FureamWorldConfigImpl config = new FureamWorldConfigImpl(); - File worldFolder = world.getWorldFolder(); - File configFile = new File(worldFolder, "fuream.json"); - File serverconfigFile = new File(new File(worldFolder, "serverconfig"), "fuream.json"); - File serverconfigJson5 = new File(new File(worldFolder, "serverconfig"), "fuream.json5"); - - File targetFile = null; - if (configFile.exists()) { - targetFile = configFile; - } else if (serverconfigFile.exists()) { - targetFile = serverconfigFile; - } else if (serverconfigJson5.exists()) { - targetFile = serverconfigJson5; - } else { - targetFile = configFile; + File targetFile = findExistingConfig(world.getWorldFolder()); + boolean dimension = world.getEnvironment() == World.Environment.NETHER || + world.getEnvironment() == World.Environment.THE_END; + File mainWorldFolder = getMainWorldFolder(); + if (targetFile == null && dimension) { + targetFile = findExistingConfig(mainWorldFolder); + } + if (targetFile == null && !dimension) { + targetFile = new File(world.getWorldFolder(), "fuream.json"); try { FureamWorldConfigImpl.writeWorldConfig(config, targetFile); } catch (Exception e) { - Bukkit.getLogger().warning("Failed to create default fuream.json for world " + world.getName() + ": " + e.getMessage()); + Bukkit.getLogger().warning("Failed to create default fuream.json at " + targetFile + ": " + e.getMessage()); } } @@ -152,6 +153,33 @@ public static void loadWorldConfig(@NotNull World world) { } } WORLD_CONFIGS.put(world, config); + if (targetFile == null) { + WORLD_CONFIG_SOURCES.remove(world); + } else { + WORLD_CONFIG_SOURCES.put(world, targetFile.getAbsoluteFile()); + } + } + + private static @Nullable File findExistingConfig(@NotNull File worldFolder) { + File direct = new File(worldFolder, "fuream.json"); + if (direct.isFile()) return direct; + File serverConfig = new File(new File(worldFolder, "serverconfig"), "fuream.json"); + if (serverConfig.isFile()) return serverConfig; + File json5 = new File(new File(worldFolder, "serverconfig"), "fuream.json5"); + return json5.isFile() ? json5 : null; + } + + private static @NotNull File getMainWorldFolder() { + File container = Bukkit.getWorldContainer(); + Properties properties = new Properties(); + File propertiesFile = new File("server.properties"); + try (FileInputStream input = new FileInputStream(propertiesFile)) { + properties.load(input); + } catch (Exception ignored) { + } + String levelName = properties.getProperty("level-name", "world").trim(); + if (levelName.isEmpty()) levelName = "world"; + return new File(container, levelName); } public static @Nullable FureamWorldConfig getWorldConfig(@Nullable World world) { @@ -177,16 +205,17 @@ public static void loadWorldConfig(@NotNull World world) { } if (obj instanceof Block) { Material mat = ((Block) obj).getType(); - if (mat == Material.FURNACE) return FurnaceType.FURNACE; - if (mat == Material.SMOKER) return FurnaceType.SMOKER; - if (mat == Material.BLAST_FURNACE) return FurnaceType.BLAST_FURNACE; + if (mat == Material.FURNACE || "BURNING_FURNACE".equals(mat.name())) return FurnaceType.FURNACE; + try { + if (mat == Material.SMOKER) return FurnaceType.SMOKER; + if (mat == Material.BLAST_FURNACE) return FurnaceType.BLAST_FURNACE; + } catch (NoSuchFieldError e) { + // 1.13 and earlier do not define these materials. + } return null; } if (obj instanceof BlockState) { - if (obj instanceof BlastFurnace) return FurnaceType.BLAST_FURNACE; - if (obj instanceof Smoker) return FurnaceType.SMOKER; - if (obj instanceof Furnace) return FurnaceType.FURNACE; - return null; + return getFurnaceType(((BlockState) obj).getBlock()); } return null; } @@ -201,16 +230,23 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command for (World w : Bukkit.getWorlds()) { loadWorldConfig(w); } + FurnaceManager.reconcileConfiguration(); sender.sendMessage("§a[Fuream] World configurations reloaded!"); return true; } if (args.length > 0 && "status".equalsIgnoreCase(args[0])) { sender.sendMessage("§e=== Fuream Status ==="); + sender.sendMessage("§6Version adapter: §f" + VersionAdapters.current().id() + + " §7(" + ServerVersion.CURRENT + ")"); + sender.sendMessage("§6Root NBT hook: §f" + (this.rootNbtReady ? "ready" : "failed")); for (World w : Bukkit.getWorlds()) { FureamWorldConfig cfg = getWorldConfig(w); if (cfg != null) { - sender.sendMessage("§6World: §f" + w.getName() + " §7(Enabled: " + cfg.getEnabledFurnaceTypes() + ")"); + File source = WORLD_CONFIG_SOURCES.get(w); + sender.sendMessage("§6World: §f" + w.getName() + " §7(Enabled: " + + cfg.getEnabledFurnaceTypes() + ", Source: " + + (source == null ? "in-memory defaults" : source.getPath()) + ")"); } } return true; diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java index 0b5b016..e7dac20 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java @@ -24,19 +24,11 @@ public static void writeWorldConfig( if (configFile.getParentFile() != null) { configFile.getParentFile().mkdirs(); } - JsonObject root = new JsonObject(); + JsonObject root = readExistingObject(configFile); - JsonObject inSlots = new JsonObject(); - config.getInputSlotCount().forEach((k, v) -> inSlots.addProperty(k.name(), v)); - root.add("input_slot_count", inSlots); - - JsonObject fuelSlots = new JsonObject(); - config.getFuelSlotCount().forEach((k, v) -> fuelSlots.addProperty(k.name(), v)); - root.add("fuel_slot_count", fuelSlots); - - JsonObject outSlots = new JsonObject(); - config.getOutputSlotCount().forEach((k, v) -> outSlots.addProperty(k.name(), v)); - root.add("output_slot_count", outSlots); + putMap(root, "input_slot_count", config.getInputSlotCount()); + putMap(root, "fuel_slot_count", config.getFuelSlotCount()); + putMap(root, "output_slot_count", config.getOutputSlotCount()); com.google.gson.JsonArray enabledTypes = new com.google.gson.JsonArray(); for (FurnaceType type : config.getEnabledFurnaceTypes()) { @@ -50,37 +42,40 @@ public static void writeWorldConfig( } root.add("prevents_hopper_insert_non_smeltable", preventsHopper); - JsonObject border = new JsonObject(); - config.getGuiBorderItemId().forEach((k, v) -> border.addProperty(k.name(), v)); - root.add("gui_border_item_id", border); - - JsonObject fuelLeft = new JsonObject(); - config.getGuiFuelLeftItemId().forEach((k, v) -> fuelLeft.addProperty(k.name(), v)); - root.add("gui_fuel_left_item_id", fuelLeft); - - JsonObject fuelUsed = new JsonObject(); - config.getGuiFuelUsedItemId().forEach((k, v) -> fuelUsed.addProperty(k.name(), v)); - root.add("gui_fuel_used_item_id", fuelUsed); - - JsonObject progDone = new JsonObject(); - config.getGuiProgressDoneItemId().forEach((k, v) -> progDone.addProperty(k.name(), v)); - root.add("gui_progress_done_item_id", progDone); - - JsonObject progRem = new JsonObject(); - config.getGuiProgressRemainingItemId().forEach((k, v) -> progRem.addProperty(k.name(), v)); - root.add("gui_progress_remaining_item_id", progRem); - - JsonObject nextFunc = new JsonObject(); - config.getGuiNextFunctionalAreaItemId().forEach((k, v) -> nextFunc.addProperty(k.name(), v)); - root.add("gui_next_functional_area_item_id", nextFunc); - - JsonObject prevPage = new JsonObject(); - config.getGuiPrevPageItemId().forEach((k, v) -> prevPage.addProperty(k.name(), v)); - root.add("gui_prev_page_item_id", prevPage); - - JsonObject nextPage = new JsonObject(); - config.getGuiNextPageItemId().forEach((k, v) -> nextPage.addProperty(k.name(), v)); - root.add("gui_next_page_item_id", nextPage); + putMap(root, "gui_border_item_id", config.getGuiBorderItemId()); + putMap(root, "gui_fuel_left_item_id", config.getGuiFuelLeftItemId()); + putMap(root, "gui_fuel_used_item_id", config.getGuiFuelUsedItemId()); + putMap(root, "gui_progress_done_item_id", config.getGuiProgressDoneItemId()); + putMap(root, "gui_progress_remaining_item_id", config.getGuiProgressRemainingItemId()); + putMap(root, "gui_prev_recipe_item_id", config.getGuiPrevRecipeItemId()); + putMap(root, "gui_next_recipe_item_id", config.getGuiNextRecipeItemId()); + putMap(root, "gui_xp_indicator_item_id", config.getGuiXpIndicatorItemId()); + putMap(root, "gui_next_functional_area_item_id", config.getGuiNextFunctionalAreaItemId()); + putMap(root, "gui_prev_page_item_id", config.getGuiPrevPageItemId()); + putMap(root, "gui_next_page_item_id", config.getGuiNextPageItemId()); + + putMap(root, "gui_title", config.getGuiTitle()); + putMap(root, "gui_border_item_title", config.getGuiBorderItemTitle()); + putMap(root, "gui_fuel_item_title", config.getGuiFuelItemTitle()); + putMap(root, "gui_fuel_item_tooltip", config.getGuiFuelItemTooltip()); + putMap(root, "gui_progress_item_title", config.getGuiProgressItemTitle()); + putMap(root, "gui_progress_item_tooltip", config.getGuiProgressItemTooltip()); + putMap(root, "gui_prev_recipe_item_title", config.getGuiPrevRecipeItemTitle()); + putMap(root, "gui_prev_recipe_empty_item_tooltip", config.getGuiPrevRecipeEmptyItemTooltip()); + putMap(root, "gui_prev_recipe_arbitrary_item_tooltip", config.getGuiPrevRecipeArbitraryItemTooltip()); + putMap(root, "gui_prev_recipe_item_tooltip", config.getGuiPrevRecipeItemTooltip()); + putMap(root, "gui_next_recipe_item_title", config.getGuiNextRecipeItemTitle()); + putMap(root, "gui_next_recipe_empty_item_tooltip", config.getGuiNextRecipeEmptyItemTooltip()); + putMap(root, "gui_next_recipe_arbitrary_item_tooltip", config.getGuiNextRecipeArbitraryItemTooltip()); + putMap(root, "gui_next_recipe_item_tooltip", config.getGuiNextRecipeItemTooltip()); + putMap(root, "gui_xp_indicator_item_title", config.getGuiXpIndicatorItemTitle()); + putMap(root, "gui_xp_indicator_gainable_item_title", config.getGuiXpIndicatorGainableItemTitle()); + putMap(root, "gui_xp_indicator_item_tooltip", config.getGuiXpIndicatorItemTooltip()); + putMap(root, "gui_next_functional_area_item_title", config.getGuiNextFunctionalAreaItemTitle()); + putMap(root, "gui_prev_page_item_title", config.getGuiPrevPageItemTitle()); + putMap(root, "gui_prev_page_item_tooltip", config.getGuiPrevPageItemTooltip()); + putMap(root, "gui_next_page_item_title", config.getGuiNextPageItemTitle()); + putMap(root, "gui_next_page_item_tooltip", config.getGuiNextPageItemTooltip()); com.google.gson.Gson gson = new com.google.gson.GsonBuilder().setPrettyPrinting().create(); try (java.io.FileWriter fw = new java.io.FileWriter(configFile)) { @@ -88,6 +83,47 @@ public static void writeWorldConfig( } } + private static JsonObject readExistingObject(File file) { + if (!file.isFile()) return new JsonObject(); + try (FileReader reader = new FileReader(file)) { + JsonElement element = new JsonParser().parse(reader); + return element.isJsonObject() ? element.getAsJsonObject() : new JsonObject(); + } catch (Exception ignored) { + return new JsonObject(); + } + } + + private static void putMap(JsonObject root, String key, Map values) { + JsonObject object = new JsonObject(); + for (Map.Entry entry : values.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number) object.addProperty(entry.getKey().name(), (Number) value); + else if (value instanceof Boolean) object.addProperty(entry.getKey().name(), (Boolean) value); + else object.addProperty(entry.getKey().name(), String.valueOf(value)); + } + root.add(key, object); + } + + private static void readStringMap( + JsonObject root, String key, EnumMap values + ) { + if (!root.has(key)) return; + JsonElement element = root.get(key); + if (element.isJsonPrimitive()) { + String value = element.getAsString(); + for (FurnaceType type : FurnaceType.values()) values.put(type, value); + return; + } + if (!element.isJsonObject()) return; + for (Map.Entry entry : element.getAsJsonObject().entrySet()) { + if (!entry.getValue().isJsonPrimitive()) continue; + try { + values.put(FurnaceType.valueOf(entry.getKey()), entry.getValue().getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + @SuppressWarnings("deprecation") public static void readWorldConfig( @NotNull FureamWorldConfig config, @NotNull File configFile @@ -102,6 +138,7 @@ public static void readWorldConfig( } if (JSON.isJsonObject()) { JsonObject obj = JSON.getAsJsonObject(); + readStringMap(obj, "gui_title", config.getGuiTitle()); if (obj.has("input_slot_count")) { JsonElement ele = obj.get("input_slot_count"); if (ele.isJsonObject()) { @@ -1002,6 +1039,8 @@ public static void readWorldConfig( private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> guiNextPageItemId = new EnumMap<>(FurnaceType.class); private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + guiTitle = new EnumMap<>(FurnaceType.class); + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> guiBorderItemTitle = new EnumMap<>(FurnaceType.class); private @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> guiFuelItemTitle = new EnumMap<>(FurnaceType.class); @@ -1077,6 +1116,8 @@ public FureamWorldConfigImpl() { .put(type, "minecraft:arrow"); this.guiNextPageItemId .put(type, "minecraft:arrow"); + this.guiTitle.put(type, type == FurnaceType.SMOKER ? "Smoker" : + (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace")); this.guiBorderItemTitle.put(type, "GUI Border"); this.guiFuelItemTitle.put(type, "Fuel"); this.guiFuelItemTooltip.put(type, "Available: %d / %d"); @@ -1333,6 +1374,20 @@ public void setGuiNextPageItemId( this.guiNextPageItemId = map; } + @Override + @Contract(pure = true) + public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> + getGuiTitle() { + return this.guiTitle; + } + + @Override + public void setGuiTitle( + @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> map + ) { + this.guiTitle = map; + } + @Override @Contract(pure = true) public @NotNull EnumMap<@NotNull FurnaceType, @NotNull String> diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java new file mode 100644 index 0000000..052f5eb --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java @@ -0,0 +1,44 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; + +import org.bukkit.Material; +import org.bukkit.block.Block; + +public final class BlockCompat { + public static void setLit(Block block, boolean lit) { + try { + Object data = block.getClass().getMethod("getBlockData").invoke(block); + Method setLit = data.getClass().getMethod("setLit", boolean.class); + setLit.invoke(data, lit); + for (Method method : block.getClass().getMethods()) { + Class[] parameters = method.getParameterTypes(); + if ("setBlockData".equals(method.getName()) && parameters.length == 2 && + parameters[0].isInstance(data) && parameters[1] == boolean.class) { + method.invoke(block, data, false); + return; + } + } + return; + } catch (ReflectiveOperationException | RuntimeException ignored) { + } + + if (!"FURNACE".equals(block.getType().name()) && !"BURNING_FURNACE".equals(block.getType().name())) { + return; + } + Material target = Material.matchMaterial(lit ? "BURNING_FURNACE" : "FURNACE"); + if (target == null || target == block.getType()) return; + try { + byte data = ((Number) block.getClass().getMethod("getData").invoke(block)).byteValue(); + Method getId = Material.class.getMethod("getId"); + int id = ((Number) getId.invoke(target)).intValue(); + block.getClass().getMethod("setTypeIdAndData", int.class, byte.class, boolean.class) + .invoke(block, id, data, false); + } catch (ReflectiveOperationException ignored) { + } + } + + private BlockCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java new file mode 100644 index 0000000..d4799a1 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java @@ -0,0 +1,239 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import io.github.eat_ram.fuream.logic.FuelTable; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.inventory.ItemStack; + +/** Maintains the comparator view exposed by Fabric's three-slot masked inventory. */ +public final class ComparatorCompat { + private static final BlockFace[] HORIZONTAL = { + BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST + }; + + public static int calculate(FureamFurnaceData data) { + ItemStack input = first(data.inputs, false); + ItemStack fuel = first(data.fuels, true); + ItemStack output = first(data.outputs, false); + ItemStack[] masked = {input, fuel, output}; + float fullness = 0f; + int occupied = 0; + for (ItemStack stack : masked) { + if (ItemCompat.isEmpty(stack)) continue; + fullness += Math.min(1f, (float) stack.getAmount() / Math.max(1, stack.getMaxStackSize())); + occupied++; + } + return occupied == 0 ? 0 : (int) Math.floor(fullness / 3f * 14f) + 1; + } + + public static void updateAround(Block furnace, int signal) { + for (BlockFace face : HORIZONTAL) { + Block comparator = furnace.getRelative(face); + if (!isComparator(comparator.getType()) || + !face.getOppositeFace().name().equals(facingName(comparator))) continue; + setComparatorOutput(comparator, signal); + } + } + + public static Block sourceBlock(Block comparator) { + try { + BlockFace facing = BlockFace.valueOf(facingName(comparator)); + return comparator.getRelative(facing); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + private static ItemStack first(List stacks, boolean fuelOnly) { + for (ItemStack stack : stacks) { + if (!ItemCompat.isEmpty(stack) && (!fuelOnly || FuelTable.isFuel(stack))) return stack; + } + return ItemCompat.empty(); + } + + private static boolean isComparator(Material material) { + String name = material.name(); + return name.contains("COMPARATOR"); + } + + private static String facingName(Block comparator) { + try { + Object data = comparator.getClass().getMethod("getBlockData").invoke(comparator); + Method method = data.getClass().getMethod("getFacing"); + method.setAccessible(true); + Object facing = method.invoke(data); + return facing instanceof Enum ? ((Enum) facing).name() : String.valueOf(facing); + } catch (ReflectiveOperationException ignored) { + try { + int data = ((Number) comparator.getClass().getMethod("getData").invoke(comparator)).intValue() & 3; + return new String[] {"NORTH", "EAST", "SOUTH", "WEST"}[data]; + } catch (ReflectiveOperationException e) { + return ""; + } + } + } + + private static void setComparatorOutput(Block comparator, int signal) { + setPowerState(comparator, signal > 0, false); + try { + BlockState state = comparator.getState(); + Method tileMethod = findTileEntityMethod(state.getClass()); + Object tile = tileMethod.invoke(state); + Field output = findOutputField(tile.getClass()); + output.setInt(tile, Math.max(0, Math.min(15, signal))); + markChanged(tile); + } catch (ReflectiveOperationException | RuntimeException ignored) { + } + notifyNeighbors(comparator); + } + + private static Method findTileEntityMethod(Class type) throws NoSuchMethodException { + for (String name : new String[] {"getTileEntityFromWorld", "getTileEntity"}) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if (name.equals(method.getName()) && method.getParameterTypes().length == 0) { + method.setAccessible(true); + return method; + } + } + } + } + throw new NoSuchMethodException("No comparator tile entity accessor on " + type.getName()); + } + + private static Field findOutputField(Class type) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) { + field.setAccessible(true); + return field; + } + } + } + throw new IllegalStateException("Comparator output field is unavailable on " + type.getName()); + } + + private static void markChanged(Object tile) { + for (String name : new String[] {"setChanged", "markDirty"}) { + try { + Method method = tile.getClass().getMethod(name); + method.invoke(tile); + return; + } catch (ReflectiveOperationException ignored) { + } + } + } + + private static void setPowerState(Block block, boolean powered, boolean physics) { + try { + Object data = block.getClass().getMethod("getBlockData").invoke(block); + Method poweredSetter = data.getClass().getMethod("setPowered", boolean.class); + poweredSetter.setAccessible(true); + poweredSetter.invoke(data, powered); + for (Method method : block.getClass().getMethods()) { + Class[] parameters = method.getParameterTypes(); + if ("setBlockData".equals(method.getName()) && parameters.length == 2 && + parameters[0].isInstance(data) && parameters[1] == boolean.class) { + method.invoke(block, data, physics); + return; + } + } + } catch (ReflectiveOperationException ignored) { + } + + try { + byte data = ((Number) block.getClass().getMethod("getData").invoke(block)).byteValue(); + Material target = Material.matchMaterial(powered + ? "REDSTONE_COMPARATOR_ON" : "REDSTONE_COMPARATOR_OFF"); + if (target == null) return; + int id = ((Number) Material.class.getMethod("getId").invoke(target)).intValue(); + block.getClass().getMethod("setTypeIdAndData", int.class, byte.class, boolean.class) + .invoke(block, id, data, physics); + } catch (ReflectiveOperationException ignored) { + } + } + + private static void notifyNeighbors(Block block) { + try { + Object craftWorld = block.getWorld(); + Object handle = craftWorld.getClass().getMethod("getHandle").invoke(craftWorld); + ClassLoader loader = handle.getClass().getClassLoader(); + String craftPackage = craftWorld.getClass().getPackage().getName(); + String version = craftPackage.substring(craftPackage.lastIndexOf('.') + 1); + Class positionClass = null; + for (String candidate : new String[] { + "net.minecraft.core.BlockPos", "net.minecraft.core.BlockPosition", + "net.minecraft.server." + version + ".BlockPosition" + }) { + try { + positionClass = Class.forName(candidate, false, loader); + break; + } catch (ClassNotFoundException ignored) { + } + } + if (positionClass == null) return; + Object position = positionClass.getConstructor(int.class, int.class, int.class) + .newInstance(block.getX(), block.getY(), block.getZ()); + + String craftRoot = org.bukkit.Bukkit.getServer().getClass().getPackage().getName(); + Class magicNumbers = Class.forName(craftRoot + ".util.CraftMagicNumbers"); + Object nmsBlock = magicNumbers.getMethod("getBlock", Material.class) + .invoke(null, block.getType()); + if (nmsBlock == null) return; + + Object blockState = null; + for (Method method : handle.getClass().getMethods()) { + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 1 && parameters[0].isInstance(position) && + (method.getReturnType().getSimpleName().equals("IBlockData") || + method.getReturnType().getSimpleName().equals("BlockState"))) { + blockState = method.invoke(handle, position); + if (blockState != null) break; + } + } + if (blockState != null) { + for (Class current = nmsBlock.getClass(); current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + Class[] parameters = method.getParameterTypes(); + if (Modifier.isStatic(method.getModifiers()) || parameters.length != 3 || + !parameters[0].isInstance(handle) || !parameters[1].isInstance(position) || + !parameters[2].isInstance(blockState) || method.getReturnType() != void.class) continue; + String name = method.getName(); + if (!"updateNeighborsInFront".equals(name) && !"d".equals(name)) continue; + method.setAccessible(true); + method.invoke(nmsBlock, handle, position, blockState); + return; + } + } + } + + for (Class current = handle.getClass(); current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + Class[] parameters = method.getParameterTypes(); + if (Modifier.isStatic(method.getModifiers()) || parameters.length != 2 || + !parameters[0].isInstance(position) || !parameters[1].isInstance(nmsBlock) || + method.getReturnType() != void.class) continue; + String name = method.getName(); + if (!"updateNeighborsAt".equals(name) && !"applyPhysics".equals(name) && + !"notifyNeighborsOfStateChange".equals(name) && !"b".equals(name)) continue; + method.setAccessible(true); + method.invoke(handle, position, nmsBlock); + return; + } + } + } catch (ReflectiveOperationException | RuntimeException ignored) { + } + } + + private ComparatorCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java new file mode 100644 index 0000000..17c945d --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java @@ -0,0 +1,58 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; + +import org.bukkit.block.Furnace; + +public final class FurnaceCompat { + public static int getBurnTime(Furnace furnace) { + return getInt(furnace, "getBurnTime", 0); + } + + public static int getCookTime(Furnace furnace) { + return getInt(furnace, "getCookTime", 0); + } + + public static int getCookTimeTotal(Furnace furnace, int fallback) { + return getInt(furnace, "getCookTimeTotal", fallback); + } + + public static void setCookTimeTotal(Furnace furnace, int value) { + invokeNumberSetter(furnace, "setCookTimeTotal", value); + } + + public static void setCookTime(Furnace furnace, int value) { + invokeNumberSetter(furnace, "setCookTime", value); + } + + public static void setBurnTime(Furnace furnace, int value) { + invokeNumberSetter(furnace, "setBurnTime", value); + } + + private static int getInt(Object target, String name, int fallback) { + try { + Object value = target.getClass().getMethod(name).invoke(target); + return value instanceof Number ? ((Number) value).intValue() : fallback; + } catch (ReflectiveOperationException ignored) { + return fallback; + } + } + + private static void invokeNumberSetter(Object target, String name, int value) { + for (Method method : target.getClass().getMethods()) { + if (!name.equals(method.getName()) || method.getParameterTypes().length != 1) continue; + Class type = method.getParameterTypes()[0]; + try { + if (type == short.class || type == Short.class) method.invoke(target, (short) value); + else if (type == int.class || type == Integer.class) method.invoke(target, value); + else continue; + return; + } catch (ReflectiveOperationException ignored) { + } + } + } + + private FurnaceCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java new file mode 100644 index 0000000..a2be7cb --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java @@ -0,0 +1,38 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.ExperienceOrb; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; + +/** Optional Bukkit furnace events whose availability differs across legacy APIs. */ +public final class FurnaceEventCompat { + public static void fireExtract(Player player, Block furnace, Material itemType, int amount) { + if (amount <= 0 || ItemCompat.isAir(itemType)) return; + try { + Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceExtractEvent"); + Constructor constructor = eventClass.getConstructor( + Player.class, Block.class, Material.class, int.class, int.class + ); + Event event = (Event) constructor.newInstance(player, furnace, itemType, amount, 0); + Bukkit.getPluginManager().callEvent(event); + Method getter = eventClass.getMethod("getExpToDrop"); + int experience = ((Number) getter.invoke(event)).intValue(); + if (experience > 0) { + ExperienceOrb orb = player.getWorld().spawn(player.getLocation(), ExperienceOrb.class); + orb.setExperience(experience); + } + } catch (ReflectiveOperationException | LinkageError ignored) { + // FurnaceExtractEvent is absent on some of the oldest Bukkit APIs. + } + } + + private FurnaceEventCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java new file mode 100644 index 0000000..e7e8de3 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java @@ -0,0 +1,48 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; +import java.util.Locale; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public final class ItemCompat { + public static boolean isEmpty(ItemStack stack) { + return stack == null || stack.getAmount() <= 0 || isAir(stack.getType()); + } + + public static boolean isAir(Material material) { + if (material == null) return true; + try { + Method method = Material.class.getMethod("isAir"); + return Boolean.TRUE.equals(method.invoke(material)); + } catch (ReflectiveOperationException ignored) { + String name = material.name(); + return "AIR".equals(name) || name.endsWith("_AIR"); + } + } + + public static ItemStack empty() { + return new ItemStack(Material.AIR); + } + + public static Material matchMaterial(String configured, Material fallback) { + if (configured == null || configured.trim().isEmpty()) return fallback; + String name = configured.trim(); + int colon = name.indexOf(':'); + if (colon >= 0) name = name.substring(colon + 1); + name = name.toUpperCase(Locale.ROOT); + Material result = Material.matchMaterial(name); + if (result != null) return result; + + // GUI aliases for the pre-flattening material registry. + if (name.endsWith("_STAINED_GLASS_PANE")) { + result = Material.matchMaterial("STAINED_GLASS_PANE"); + } + return result != null ? result : fallback; + } + + private ItemCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java new file mode 100644 index 0000000..16f3016 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java @@ -0,0 +1,109 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import io.github.eat_ram.fuream.api.FurnaceType; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; + +public final class RecipeCompat { + public static RecipeHandle findFirst(ItemStack input, FurnaceType type) { + if (ItemCompat.isEmpty(input)) return null; + Iterator iterator = Bukkit.recipeIterator(); + while (iterator.hasNext()) { + Recipe recipe = iterator.next(); + if (matches(recipe, input, type)) return snapshot(recipe, type); + } + return null; + } + + public static List findAll(ItemStack input, FurnaceType type) { + List result = new ArrayList<>(); + if (ItemCompat.isEmpty(input)) return result; + Iterator iterator = Bukkit.recipeIterator(); + while (iterator.hasNext()) { + Recipe recipe = iterator.next(); + if (matches(recipe, input, type)) result.add(snapshot(recipe, type)); + } + result.sort(Comparator.comparing(handle -> handle.id)); + return result; + } + + public static RecipeHandle findById(ItemStack input, FurnaceType type, String id) { + if (id == null) return null; + for (RecipeHandle handle : findAll(input, type)) { + if (id.equals(handle.id)) return handle; + } + return null; + } + + private static boolean matches(Recipe recipe, ItemStack input, FurnaceType type) { + String simpleName = recipe.getClass().getSimpleName(); + if (type == FurnaceType.SMOKER) { + if (!simpleName.endsWith("SmokingRecipe")) return false; + } else if (type == FurnaceType.BLAST_FURNACE) { + if (!simpleName.endsWith("BlastingRecipe")) return false; + } else if (!simpleName.endsWith("FurnaceRecipe")) { + return false; + } + + try { + Method choiceMethod = recipe.getClass().getMethod("getInputChoice"); + Object choice = choiceMethod.invoke(recipe); + Method test = choice.getClass().getMethod("test", ItemStack.class); + return Boolean.TRUE.equals(test.invoke(choice, input)); + } catch (ReflectiveOperationException ignored) { + try { + Method inputMethod = recipe.getClass().getMethod("getInput"); + ItemStack expected = (ItemStack) inputMethod.invoke(recipe); + if (expected == null || expected.getType() != input.getType()) return false; + short durability = expected.getDurability(); + return durability == Short.MAX_VALUE || durability == input.getDurability(); + } catch (ReflectiveOperationException e) { + return false; + } + } + } + + private static RecipeHandle snapshot(Recipe recipe, FurnaceType type) { + ItemStack output = recipe.getResult() == null ? ItemCompat.empty() : recipe.getResult().clone(); + int cookingTime = type == FurnaceType.FURNACE ? 200 : 100; + float experience = 0f; + try { + Object value = recipe.getClass().getMethod("getCookingTime").invoke(recipe); + if (value instanceof Number) cookingTime = ((Number) value).intValue(); + } catch (ReflectiveOperationException ignored) { + } + try { + Object value = recipe.getClass().getMethod("getExperience").invoke(recipe); + if (value instanceof Number) experience = ((Number) value).floatValue(); + } catch (ReflectiveOperationException ignored) { + } + + String id = null; + try { + Object key = recipe.getClass().getMethod("getKey").invoke(recipe); + if (key != null) id = key.toString(); + } catch (ReflectiveOperationException ignored) { + } + if (id == null) { + id = "legacy:" + type.name().toLowerCase(LocaleHolder.ROOT) + "/" + + output.getType().name().toLowerCase(LocaleHolder.ROOT) + "/" + output.getDurability(); + } + return new RecipeHandle(recipe, id, output, Math.max(1, cookingTime), experience); + } + + /** Avoids repeatedly resolving Locale from old JVM implementations. */ + private static final class LocaleHolder { + private static final java.util.Locale ROOT = java.util.Locale.ROOT; + } + + private RecipeCompat() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeHandle.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeHandle.java new file mode 100644 index 0000000..485e306 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeHandle.java @@ -0,0 +1,20 @@ +package io.github.eat_ram.fuream.compat; + +import org.bukkit.inventory.ItemStack; + +/** Version-neutral cooking recipe snapshot. */ +public final class RecipeHandle { + public final Object nativeRecipe; + public final String id; + public final ItemStack result; + public final int cookingTime; + public final float experience; + + public RecipeHandle(Object nativeRecipe, String id, ItemStack result, int cookingTime, float experience) { + this.nativeRecipe = nativeRecipe; + this.id = id; + this.result = result; + this.cookingTime = cookingTime; + this.experience = experience; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java new file mode 100644 index 0000000..d95eb6d --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java @@ -0,0 +1,80 @@ +package io.github.eat_ram.fuream.compat; + +import org.bukkit.Bukkit; + +/** Runtime Minecraft/Bukkit version without linking version-specific API classes. */ +public final class ServerVersion implements Comparable { + public static final ServerVersion CURRENT = parse(currentVersionString()); + + public final int major; + public final int minor; + public final int patch; + + public ServerVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static ServerVersion parse(String value) { + int[] parts = new int[] {0, 0, 0}; + int part = 0; + int current = -1; + for (int i = 0; i < value.length() && part < parts.length; i++) { + char c = value.charAt(i); + if (c >= '0' && c <= '9') { + current = Math.max(0, current) * 10 + (c - '0'); + } else if (current >= 0) { + parts[part++] = current; + current = -1; + if (part > 0 && c != '.') break; + } + } + if (current >= 0 && part < parts.length) parts[part] = current; + return new ServerVersion(parts[0], parts[1], parts[2]); + } + + private static String currentVersionString() { + try { + String value = Bukkit.getBukkitVersion(); + return value == null ? "0.0" : value; + } catch (Throwable ignored) { + return "0.0"; + } + } + + public boolean atLeast(int major, int minor) { + return compareTo(new ServerVersion(major, minor, 0)) >= 0; + } + + public boolean atLeast(int major, int minor, int patch) { + return compareTo(new ServerVersion(major, minor, patch)) >= 0; + } + + public boolean atMost(int major, int minor) { + return compareTo(new ServerVersion(major, minor, Integer.MAX_VALUE)) <= 0; + } + + public String adapterFamily() { + if (atMost(1, 12)) return "legacy-numeric"; + if (!atLeast(1, 17)) return "flattening-obfuscated"; + if (!atLeast(1, 20, 5)) return "mojang-named"; + return "components"; + } + + @Override + public int compareTo(ServerVersion other) { + if (major != other.major) return major < other.major ? -1 : 1; + if (minor != other.minor) return minor < other.minor ? -1 : 1; + return Integer.compare(patch, other.patch); + } + + @Override + public String toString() { + return major + "." + minor + (patch == 0 ? "" : "." + patch); + } + + private ServerVersion() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java new file mode 100644 index 0000000..0cf5d5a --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java @@ -0,0 +1,12 @@ +package io.github.eat_ram.fuream.compat; + +/** Version capabilities used by the Java 8 common core. */ +public interface VersionAdapter { + String id(); + + boolean supportsSpecialFurnaces(); + + boolean supportsRecipeOverrides(); + + boolean usesDataComponents(); +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java new file mode 100644 index 0000000..dfcbb3a --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java @@ -0,0 +1,81 @@ +package io.github.eat_ram.fuream.compat; + +/** Loads one of the four internal adapters without linking newer Bukkit API types. */ +public final class VersionAdapters { + private static final VersionAdapter CURRENT = select(ServerVersion.CURRENT); + + public static VersionAdapter current() { + return CURRENT; + } + + static VersionAdapter select(ServerVersion version) { + if (version.atMost(1, 12)) return new LegacyAdapter(); + if (!version.atLeast(1, 17)) return new FlatteningObfuscatedAdapter(); + if (!version.atLeast(1, 20, 5)) return new MojangNamedAdapter(); + return new ComponentsAdapter(); + } + + private abstract static class BaseAdapter implements VersionAdapter { + private final String id; + private final boolean specialFurnaces; + private final boolean recipeOverrides; + private final boolean dataComponents; + + private BaseAdapter( + String id, boolean specialFurnaces, boolean recipeOverrides, boolean dataComponents + ) { + this.id = id; + this.specialFurnaces = specialFurnaces; + this.recipeOverrides = recipeOverrides; + this.dataComponents = dataComponents; + } + + @Override + public final String id() { + return this.id; + } + + @Override + public final boolean supportsSpecialFurnaces() { + return this.specialFurnaces; + } + + @Override + public final boolean supportsRecipeOverrides() { + return this.recipeOverrides; + } + + @Override + public final boolean usesDataComponents() { + return this.dataComponents; + } + } + + private static final class LegacyAdapter extends BaseAdapter { + private LegacyAdapter() { + super("legacy-numeric", false, false, false); + } + } + + private static final class FlatteningObfuscatedAdapter extends BaseAdapter { + private FlatteningObfuscatedAdapter() { + super("flattening-obfuscated", true, true, false); + } + } + + private static final class MojangNamedAdapter extends BaseAdapter { + private MojangNamedAdapter() { + super("mojang-named", true, true, false); + } + } + + private static final class ComponentsAdapter extends BaseAdapter { + private ComponentsAdapter() { + super("components", true, true, true); + } + } + + private VersionAdapters() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java index dc5b5a5..fbfd2e0 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamData.java @@ -1,11 +1,10 @@ package io.github.eat_ram.fuream.data; -import de.tr7zw.nbtapi.iface.ReadWriteNBT; -import de.tr7zw.nbtapi.iface.ReadableNBT; +import io.github.eat_ram.fuream.nbt.NativeNbtCompound; import org.jetbrains.annotations.NotNull; public interface FureamData { - void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt); + void writeNbt(FureamDataHolder holder, @NotNull NativeNbtCompound nbt); - void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt); + void readNbt(FureamDataHolder holder, @NotNull NativeNbtCompound nbt); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java index 3a7bf67..32dabf4 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java @@ -4,15 +4,13 @@ import java.util.HashMap; import java.util.Map; -import de.tr7zw.nbtapi.NBT; -import de.tr7zw.nbtapi.iface.ReadWriteNBT; -import de.tr7zw.nbtapi.iface.ReadWriteNBTCompoundList; -import de.tr7zw.nbtapi.iface.ReadableNBT; -import de.tr7zw.nbtapi.iface.ReadableNBTList; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.nbt.NativeItemNbt; +import io.github.eat_ram.fuream.nbt.NativeNbtCompound; +import io.github.eat_ram.fuream.nbt.NativeNbtList; import io.github.eat_ram.fuream.util.KeyableItemStack; import io.github.eat_ram.fuream.util.NbtUtil; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -36,8 +34,8 @@ public class FureamFurnaceData implements FureamData { public final @NotNull ArrayList<@NotNull ItemStack> fuels; public final @NotNull ArrayList<@NotNull ItemStack> outputs; public @NotNull ItemStack recipeOverridingInput; - public final @NotNull HashMap<@NotNull KeyableItemStack, @NotNull NamespacedKey> overriddenRecipes; - public @Nullable NamespacedKey runningRecipe; + public final @NotNull HashMap<@NotNull KeyableItemStack, @NotNull String> overriddenRecipes; + public @Nullable String runningRecipe; public float experience; public FureamFurnaceData() { @@ -52,58 +50,58 @@ public FureamFurnaceData() { public boolean hasAny() { if (this.experience > 0f) return true; if (this.recipeOverridingInput != null && - !this.recipeOverridingInput.getType().isAir()) return true; + !ItemCompat.isEmpty(this.recipeOverridingInput)) return true; if (!this.overriddenRecipes.isEmpty() || this.runningRecipe != null) return true; for (ItemStack s : this.inputs) { - if (!s.getType().isAir() && s.getAmount() > 0) return true; + if (!ItemCompat.isEmpty(s)) return true; } for (ItemStack s : this.fuels) { - if (!s.getType().isAir() && s.getAmount() > 0) return true; + if (!ItemCompat.isEmpty(s)) return true; } for (ItemStack s : this.outputs) { - if (!s.getType().isAir() && s.getAmount() > 0) return true; + if (!ItemCompat.isEmpty(s)) return true; } return false; } @Override - public void writeNbt(FureamDataHolder holder, @NotNull ReadWriteNBT nbt) { + public void writeNbt(FureamDataHolder holder, @NotNull NativeNbtCompound nbt) { + if (!this.hasAny()) return; NbtUtil.writeStacks(nbt, INPUTS_KEY, this.inputs); NbtUtil.writeStacks(nbt, FUELS_KEY, this.fuels); NbtUtil.writeStacks(nbt, OUTPUTS_KEY, this.outputs); - if (nbt.hasTag(RECIPE_OVERRIDING_INPUT_KEY)) { - nbt.removeKey(RECIPE_OVERRIDING_INPUT_KEY); + if (nbt.has(RECIPE_OVERRIDING_INPUT_KEY)) { + nbt.remove(RECIPE_OVERRIDING_INPUT_KEY); } - ReadWriteNBT overridingInputTag = nbt.getOrCreateCompound(RECIPE_OVERRIDING_INPUT_KEY); - if (this.recipeOverridingInput != null && !this.recipeOverridingInput.getType().isAir()) { + NativeNbtCompound overridingInputTag = nbt.getOrCreateCompound(RECIPE_OVERRIDING_INPUT_KEY); + if (!ItemCompat.isEmpty(this.recipeOverridingInput)) { NbtUtil.writeItemToCompound(overridingInputTag, this.recipeOverridingInput); } - ReadWriteNBTCompoundList list = NbtUtil.resetCompoundList(nbt, OVERRIDDEN_RECIPES_KEY); - for (Map.Entry entry : this.overriddenRecipes.entrySet()) { + NativeNbtList list = NbtUtil.resetCompoundList(nbt, OVERRIDDEN_RECIPES_KEY); + for (Map.Entry entry : this.overriddenRecipes.entrySet()) { ItemStack stack = entry.getKey().stack; - if (!stack.getType().isAir()) { - ReadWriteNBT sub = list.addCompound(); - ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); - sub.mergeCompound(itemNbt); - sub.setString("RecipeId", entry.getValue().toString()); + if (!ItemCompat.isEmpty(stack)) { + NativeNbtCompound sub = NativeItemNbt.write(stack); + sub.setString("RecipeId", entry.getValue()); + list.add(sub); } } if (this.runningRecipe != null) { - nbt.setString(RUNNING_RECIPE_KEY, this.runningRecipe.toString()); - } else if (nbt.hasTag(RUNNING_RECIPE_KEY)) { - nbt.removeKey(RUNNING_RECIPE_KEY); + nbt.setString(RUNNING_RECIPE_KEY, this.runningRecipe); + } else if (nbt.has(RUNNING_RECIPE_KEY)) { + nbt.remove(RUNNING_RECIPE_KEY); } nbt.setFloat(XP_KEY, this.experience); - if (nbt.hasTag("FureamInitialized")) { - nbt.removeKey("FureamInitialized"); + if (nbt.has("FureamInitialized")) { + nbt.remove("FureamInitialized"); } } @Override - public void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt) { + public void readNbt(FureamDataHolder holder, @NotNull NativeNbtCompound nbt) { for (int i = 0; i < this.inputs.size(); i++) { this.inputs.set(i, new ItemStack(Material.AIR)); } @@ -114,41 +112,42 @@ public void readNbt(FureamDataHolder holder, @NotNull ReadableNBT nbt) { this.outputs.set(i, new ItemStack(Material.AIR)); } - if (nbt.hasTag(INPUTS_KEY)) { + if (nbt.has(INPUTS_KEY)) { NbtUtil.readStacks(nbt, INPUTS_KEY, this.inputs); } - if (nbt.hasTag(FUELS_KEY)) { + if (nbt.has(FUELS_KEY)) { NbtUtil.readStacks(nbt, FUELS_KEY, this.fuels); } - if (nbt.hasTag(OUTPUTS_KEY)) { + if (nbt.has(OUTPUTS_KEY)) { NbtUtil.readStacks(nbt, OUTPUTS_KEY, this.outputs); } - if (nbt.hasTag(RECIPE_OVERRIDING_INPUT_KEY)) { + if (nbt.has(RECIPE_OVERRIDING_INPUT_KEY)) { this.recipeOverridingInput = NbtUtil.fromNbt(nbt.getCompound(RECIPE_OVERRIDING_INPUT_KEY)); } else { this.recipeOverridingInput = new ItemStack(Material.AIR); } this.overriddenRecipes.clear(); - if (nbt.hasTag(OVERRIDDEN_RECIPES_KEY)) { - ReadableNBTList list = nbt.getCompoundList(OVERRIDDEN_RECIPES_KEY); - for (int i = 0; i < list.size(); ++i) { - ReadWriteNBT tag = list.get(i); - if (tag.hasTag("RecipeId")) { - String recipeIdStr = tag.getString("RecipeId"); - NamespacedKey key = NamespacedKey.fromString(recipeIdStr); - if (key != null) { - ItemStack stack = NbtUtil.fromNbt(tag); - if (!stack.getType().isAir()) { - stack.setAmount(1); - this.overriddenRecipes.put(new KeyableItemStack(stack), key); + if (nbt.has(OVERRIDDEN_RECIPES_KEY)) { + NativeNbtList list = nbt.getList(OVERRIDDEN_RECIPES_KEY); + if (list != null) { + for (int i = 0; i < list.size(); ++i) { + NativeNbtCompound tag = list.getCompound(i); + if (tag != null && tag.has("RecipeId")) { + String recipeIdStr = tag.getString("RecipeId"); + if (!recipeIdStr.isEmpty()) { + ItemStack stack = NbtUtil.fromNbt(tag); + if (!ItemCompat.isEmpty(stack)) { + stack.setAmount(1); + this.overriddenRecipes.put(new KeyableItemStack(stack), recipeIdStr); + } } } } } } - this.runningRecipe = nbt.hasTag(RUNNING_RECIPE_KEY) - ? NamespacedKey.fromString(nbt.getString(RUNNING_RECIPE_KEY)) + this.runningRecipe = nbt.has(RUNNING_RECIPE_KEY) + ? nbt.getString(RUNNING_RECIPE_KEY) : null; - this.experience = nbt.hasTag(XP_KEY) ? nbt.getFloat(XP_KEY) : 0f; + this.experience = nbt.has(XP_KEY) ? nbt.getFloat(XP_KEY, 0f) : 0f; } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java index 14e26c3..e326af3 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java @@ -1,52 +1,53 @@ package io.github.eat_ram.fuream.hook; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.api.FurnaceType; -import io.github.eat_ram.fuream.data.FureamData; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Location; +import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBurnEvent; -import org.bukkit.event.block.BlockExplodeEvent; import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.inventory.ItemStack; public class BlockBreakListener implements Listener { - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + private static final Set BREAKING = Collections.newSetFromMap( + new ConcurrentHashMap() + ); + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockBreak(BlockBreakEvent event) { this.handleFurnaceBreak(event.getBlock()); } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockBurn(BlockBurnEvent event) { this.handleFurnaceBreak(event.getBlock()); } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onBlockExplode(BlockExplodeEvent event) { - for (Block b : event.blockList()) { - this.handleFurnaceBreak(b); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityExplode(EntityExplodeEvent event) { for (Block b : event.blockList()) { this.handleFurnaceBreak(b); } } - private void handleFurnaceBreak(Block block) { + static void handleFurnaceBreak(Block block) { FurnaceType type = FureamMain.getFurnaceType(block); if (type == null) return; FurnacePos pos = new FurnacePos(block); + if (!BREAKING.add(pos)) return; + Bukkit.getScheduler().runTask(FureamMain.getInstance(), () -> BREAKING.remove(pos)); FurnaceContext ctx = FurnaceManager.CONTEXTS.remove(pos); if (ctx == null) { ctx = FurnaceManager.getOrCreateContext(block); @@ -56,11 +57,7 @@ private void handleFurnaceBreak(Block block) { Location loc = block.getLocation(); FureamFurnaceLogic.dropOnBreak(loc, ctx.data); - if (ctx.activeGui != null) { - for (org.bukkit.entity.HumanEntity viewer : new java.util.ArrayList<>(ctx.activeGui.bukkitInventory.getViewers())) { - viewer.closeInventory(); - } - } + FurnaceManager.closeSessions(ctx); } } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java new file mode 100644 index 0000000..60917b4 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java @@ -0,0 +1,15 @@ +package io.github.eat_ram.fuream.hook; + +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockExplodeEvent; + +/** Kept out of the base listener because BlockExplodeEvent is absent on 1.7. */ +public final class BlockExplodeListener implements Listener { + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + for (Block block : event.blockList()) BlockBreakListener.handleFurnaceBreak(block); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java index fd08b3b..3ae8604 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java @@ -38,6 +38,7 @@ public void onChunkUnload(ChunkUnloadEvent event) { FurnacePos pos = entry.getKey(); if (pos.worldName.equals(worldName) && (pos.x >> 4) == cx && (pos.z >> 4) == cz) { FurnaceContext ctx = entry.getValue(); + FurnaceManager.closeSessions(ctx); if (ctx.dirty || ctx.data.hasAny()) { FurnaceManager.saveToNbt(ctx); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java new file mode 100644 index 0000000..da2e282 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java @@ -0,0 +1,29 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.compat.ComparatorCompat; +import io.github.eat_ram.fuream.util.FurnacePos; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockRedstoneEvent; + +/** Supplies comparator recalculations with the Fabric-compatible masked signal. */ +public final class ComparatorListener implements Listener { + @EventHandler(priority = EventPriority.LOWEST) + public void onRedstone(BlockRedstoneEvent event) { + if (!event.getBlock().getType().name().contains("COMPARATOR")) return; + Block source = ComparatorCompat.sourceBlock(event.getBlock()); + if (source == null) return; + FurnaceType type = FureamMain.getFurnaceType(source); + if (type == null) return; + FureamWorldConfig config = FureamMain.getWorldConfig(source.getWorld()); + if (config == null || !config.getEnabledFurnaceTypes().contains(type)) return; + FurnaceManager.FurnaceContext context = FurnaceManager.CONTEXTS.get(new FurnacePos(source)); + if (context == null) context = FurnaceManager.getOrCreateContext(source); + if (context != null) event.setNewCurrent(ComparatorCompat.calculate(context.data)); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index 354a829..286276d 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -3,24 +3,25 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.UUID; -import de.tr7zw.nbtapi.NBT; -import de.tr7zw.nbtapi.iface.ReadWriteNBT; -import de.tr7zw.nbtapi.iface.ReadableNBT; import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.compat.FurnaceCompat; +import io.github.eat_ram.fuream.compat.ComparatorCompat; import io.github.eat_ram.fuream.data.FureamData; import io.github.eat_ram.fuream.data.FureamDataHolder; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; -import io.github.eat_ram.fuream.screen.FureamScreenHandler; +import io.github.eat_ram.fuream.nbt.NativeNbtCompound; import io.github.eat_ram.fuream.screen.FureamScreenInventory; import io.github.eat_ram.fuream.util.FurnacePos; +import io.github.eat_ram.fuream.util.KeyableItemStack; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -40,19 +41,8 @@ public class FurnaceManager { public static final Map CONTEXTS = new ConcurrentHashMap<>(); private static final String PDC_ROOT_KEY = "PublicBukkitValues"; - private static final String NBT_API_MARKER_KEY = "__nbtapi"; private static long tickCounter = 0; - private static final class NbtLoadResult { - private final boolean loaded; - private final boolean hasLegacyData; - - private NbtLoadResult(boolean loaded, boolean hasLegacyData) { - this.loaded = loaded; - this.hasLegacyData = hasLegacyData; - } - } - public static class FurnaceContext implements FureamDataHolder { public final @NotNull FurnacePos pos; public final @NotNull FurnaceType type; @@ -64,9 +54,13 @@ public static class FurnaceContext implements FureamDataHolder { public int cookTime; public int cookTimeTotal; - public @Nullable FureamScreenInventory activeGui; - public @Nullable FureamScreenHandler handler; + public final @NotNull Map sessions = new ConcurrentHashMap<>(); public boolean dirty; + public boolean passiveProjected; + public int lastInputSlot = -1; + public @Nullable KeyableItemStack lastInputKey; + public boolean startSmeltPending = true; + public int lastComparatorSignal = -1; public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { this.pos = pos; @@ -117,39 +111,42 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { if (state instanceof Furnace) { try { boolean nativeLoaded = FurnaceRootNbtBridge.loadNativeData(state, ctx); - NbtLoadResult loadResult = NBT.get(state, (ReadableNBT rootNbt) -> { - ReadableNBT legacyNbt = getLegacyPdc(rootNbt); - boolean hasLegacyData = hasFureamStorage(legacyNbt); - ReadableNBT dataCompound = nativeLoaded ? null : getDataCompound(legacyNbt); - - if (dataCompound != null) { - ctx.data.readNbt(ctx, dataCompound); - for (Map.Entry entry : ctx.extraData.entrySet()) { - entry.getValue().readNbt(ctx, dataCompound); - } - if (legacyNbt.hasTag("BurnTime")) { - ctx.burnTime = legacyNbt.getInteger("BurnTime"); - } - if (legacyNbt.hasTag("CookTime")) { - ctx.cookTime = legacyNbt.getInteger("CookTime"); - } - if (legacyNbt.hasTag("CookTimeTotal")) { - ctx.cookTimeTotal = legacyNbt.getInteger("CookTimeTotal"); - } - return new NbtLoadResult(true, hasLegacyData); + NativeNbtCompound root = FurnaceRootNbtBridge.capturedRoot(state); + NativeNbtCompound legacyNbt = getLegacyPdc(root); + boolean hasLegacyData = hasFureamStorage(legacyNbt); + NativeNbtCompound dataCompound = nativeLoaded ? null : getDataCompound(legacyNbt); + boolean loaded = nativeLoaded; + if (dataCompound != null) { + ctx.data.readNbt(ctx, dataCompound); + for (Map.Entry entry : ctx.extraData.entrySet()) { + entry.getValue().readNbt(ctx, dataCompound); } + ctx.burnTime = legacyNbt.getInt("BurnTime", ctx.burnTime); + ctx.cookTime = legacyNbt.getInt("CookTime", ctx.cookTime); + ctx.cookTimeTotal = legacyNbt.getInt("CookTimeTotal", ctx.cookTimeTotal); + loaded = true; + } - return new NbtLoadResult(nativeLoaded, hasLegacyData); - }); - - if (!loadResult.loaded) { + boolean enabled = isEnabled(block.getWorld(), type); + if (!loaded && !enabled) { + return null; + } + if (!loaded) { migrateVanillaLane(ctx, (Furnace) state); + } else if (enabled) { + clearNativeLane((Furnace) state); + } else { + projectPassiveLane(ctx, (Furnace) state); } - if (loadResult.hasLegacyData) { + if (hasLegacyData) { + removeLegacyPdc(legacyNbt); ctx.dirty = true; } + initializeInputIdentity(ctx); } catch (Exception e) { - e.printStackTrace(); + FureamMain.getInstance().getLogger().log(java.util.logging.Level.SEVERE, + "Unable to load furnace data at " + pos, e); + return null; } } @@ -160,6 +157,21 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { return ctx; } + private static void initializeInputIdentity(@NotNull FurnaceContext ctx) { + int inputSlot = firstPresentSlot(ctx.data.inputs); + ctx.lastInputSlot = inputSlot; + if (inputSlot < 0) { + ctx.lastInputKey = null; + ctx.startSmeltPending = true; + return; + } + + ItemStack input = ctx.data.inputs.get(inputSlot).clone(); + input.setAmount(1); + ctx.lastInputKey = new KeyableItemStack(input); + ctx.startSmeltPending = ctx.data.runningRecipe == null || ctx.cookTime <= 0; + } + public static void saveToNbt(@NotNull FurnaceContext ctx) { World world = ctx.getWorld(); if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) return; @@ -168,20 +180,16 @@ public static void saveToNbt(@NotNull FurnaceContext ctx) { BlockState state = block.getState(); if (state instanceof Furnace) { try { - absorbVanillaLane(ctx, (Furnace) state); - FurnaceRootNbtBridge.store(state, ctx); - boolean hasLegacyData = NBT.get( - state, - (ReadableNBT rootNbt) -> hasFureamStorage(getLegacyPdc(rootNbt)) - ); - if (hasLegacyData) { - FurnaceRootNbtBridge.withoutInjection( - () -> NBT.modifyPersistentData(state, FurnaceManager::removeLegacyPdc) - ); + if (!ctx.passiveProjected) { + absorbVanillaLane(ctx, (Furnace) state); + } else { + syncPassiveTimes(ctx, (Furnace) state); } + FurnaceRootNbtBridge.store(state, ctx); ctx.dirty = false; } catch (Exception e) { - e.printStackTrace(); + FureamMain.getInstance().getLogger().log(java.util.logging.Level.SEVERE, + "Unable to save furnace data at " + ctx.pos, e); } } } @@ -198,12 +206,12 @@ private static void absorbVanillaLane( private static @Nullable ItemStack absorbLaneStack( @NotNull java.util.List target, @Nullable ItemStack stack ) { - if (stack == null || stack.getType().isAir() || stack.getAmount() <= 0) { + if (ItemCompat.isEmpty(stack)) { return null; } int maxSlots = Math.max(1, target.size()); ItemStack remainder = FureamFurnaceEngine.insertStackIntoList(target, stack, maxSlots); - return remainder.getType().isAir() ? null : remainder; + return ItemCompat.isEmpty(remainder) ? null : remainder; } private static void migrateVanillaLane( @@ -214,58 +222,118 @@ private static void migrateVanillaLane( ItemStack fuel = inv.getFuel(); ItemStack result = inv.getResult(); - if (smelting != null && !smelting.getType().isAir()) { + if (!ItemCompat.isEmpty(smelting)) { ctx.data.inputs.set(0, smelting.clone()); } - if (fuel != null && !fuel.getType().isAir()) { + if (!ItemCompat.isEmpty(fuel)) { ctx.data.fuels.set(0, fuel.clone()); } - if (result != null && !result.getType().isAir()) { + if (!ItemCompat.isEmpty(result)) { ctx.data.outputs.set(0, result.clone()); } inv.clear(); - ctx.burnTime = furnaceState.getBurnTime(); - ctx.cookTime = furnaceState.getCookTime(); - ctx.cookTimeTotal = furnaceState.getCookTimeTotal() > 0 - ? furnaceState.getCookTimeTotal() + ctx.burnTime = FurnaceCompat.getBurnTime(furnaceState); + ctx.cookTime = FurnaceCompat.getCookTime(furnaceState); + int nativeTotal = FurnaceCompat.getCookTimeTotal( + furnaceState, ctx.type == FurnaceType.FURNACE ? 200 : 100 + ); + ctx.cookTimeTotal = nativeTotal > 0 + ? nativeTotal : (ctx.type == FurnaceType.FURNACE ? 200 : 100); ctx.dirty = true; } - private static @Nullable ReadableNBT getDataCompound(@Nullable ReadableNBT nbt) { - if (nbt == null || !nbt.hasTag(FureamMain.DATA_KEY)) { + private static @Nullable NativeNbtCompound getDataCompound(@Nullable NativeNbtCompound nbt) { + if (nbt == null || !nbt.has(FureamMain.DATA_KEY)) { return null; } return nbt.getCompound(FureamMain.DATA_KEY); } - private static @Nullable ReadableNBT getLegacyPdc(@NotNull ReadableNBT rootNbt) { - if (!rootNbt.hasTag(PDC_ROOT_KEY)) { + private static @Nullable NativeNbtCompound getLegacyPdc(@Nullable NativeNbtCompound rootNbt) { + if (rootNbt == null || !rootNbt.has(PDC_ROOT_KEY)) { return null; } return rootNbt.getCompound(PDC_ROOT_KEY); } - private static boolean hasFureamStorage(@Nullable ReadableNBT nbt) { + private static boolean hasFureamStorage(@Nullable NativeNbtCompound nbt) { return nbt != null && ( - nbt.hasTag(FureamMain.DATA_KEY) || - nbt.hasTag("BurnTime") || - nbt.hasTag("CookTime") || - nbt.hasTag("CookTimeTotal") + nbt.has(FureamMain.DATA_KEY) || + nbt.has("BurnTime") || + nbt.has("CookTime") || + nbt.has("CookTimeTotal") ); } - private static void removeLegacyPdc(@NotNull ReadWriteNBT legacyNbt) { - legacyNbt.removeKey(FureamMain.DATA_KEY); - legacyNbt.removeKey("BurnTime"); - legacyNbt.removeKey("CookTime"); - legacyNbt.removeKey("CookTimeTotal"); + private static void removeLegacyPdc(@Nullable NativeNbtCompound legacyNbt) { + if (legacyNbt == null) return; + legacyNbt.remove(FureamMain.DATA_KEY); + legacyNbt.remove("BurnTime"); + legacyNbt.remove("CookTime"); + legacyNbt.remove("CookTimeTotal"); + } + + private static boolean isEnabled(@Nullable World world, @NotNull FurnaceType type) { + FureamWorldConfig config = FureamMain.getWorldConfig(world); + return config != null && config.getEnabledFurnaceTypes().contains(type); + } + + private static void clearNativeLane(@NotNull Furnace furnace) { + furnace.getInventory().clear(); + } + + private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + FurnaceInventory inventory = furnace.getInventory(); + inventory.setSmelting(adoptOrTakeProjection(ctx.data.inputs, inventory.getSmelting())); + inventory.setFuel(adoptOrTakeProjection(ctx.data.fuels, inventory.getFuel())); + inventory.setResult(adoptOrTakeProjection(ctx.data.outputs, inventory.getResult())); + FurnaceCompat.setBurnTime(furnace, Math.max(0, ctx.burnTime)); + FurnaceCompat.setCookTime(furnace, Math.max(0, ctx.cookTime)); + if (ctx.cookTimeTotal > 0) FurnaceCompat.setCookTimeTotal(furnace, ctx.cookTimeTotal); + ctx.passiveProjected = true; + ctx.dirty = true; + } + + private static @Nullable ItemStack adoptOrTakeProjection( + @NotNull java.util.List virtual, @Nullable ItemStack nativeStack + ) { + int first = firstPresentSlot(virtual); + if (!ItemCompat.isEmpty(nativeStack)) { + if (first >= 0 && virtual.get(first).isSimilar(nativeStack) && + virtual.get(first).getAmount() == nativeStack.getAmount()) { + virtual.set(first, ItemCompat.empty()); + } + return nativeStack; + } + if (first < 0) return null; + ItemStack result = virtual.get(first).clone(); + virtual.set(first, ItemCompat.empty()); + return result; + } - Set remaining = legacyNbt.getKeys(); - if (remaining.size() == 1 && remaining.contains(NBT_API_MARKER_KEY)) { - legacyNbt.removeKey(NBT_API_MARKER_KEY); + private static int firstPresentSlot(@NotNull java.util.List stacks) { + for (int i = 0; i < stacks.size(); i++) { + if (!ItemCompat.isEmpty(stacks.get(i))) return i; } + return -1; + } + + private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + if (!ctx.passiveProjected) return; + absorbVanillaLane(ctx, furnace); + ctx.burnTime = FurnaceCompat.getBurnTime(furnace); + ctx.cookTime = FurnaceCompat.getCookTime(furnace); + ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); + ctx.passiveProjected = false; + ctx.dirty = true; + } + + private static void syncPassiveTimes(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + ctx.burnTime = FurnaceCompat.getBurnTime(furnace); + ctx.cookTime = FurnaceCompat.getCookTime(furnace); + ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); } public static void tickAll() { @@ -276,6 +344,7 @@ public static void tickAll() { World world = ctx.getWorld(); if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) { + closeSessions(ctx); if (ctx.dirty) { saveToNbt(ctx); } @@ -287,6 +356,7 @@ public static void tickAll() { FurnaceType currentType = FureamMain.getFurnaceType(block); if (currentType == null) { // Block was removed or changed + closeSessions(ctx); if (ctx.dirty) { saveToNbt(ctx); } @@ -295,13 +365,28 @@ public static void tickAll() { } FureamWorldConfig config = FureamMain.getWorldConfig(world); - if (config != null && config.getEnabledFurnaceTypes().contains(ctx.type)) { + boolean enabled = config != null && config.getEnabledFurnaceTypes().contains(ctx.type); + validateSessions(ctx, block, enabled); + BlockState state = block.getState(); + if (enabled) { + if (ctx.passiveProjected && state instanceof Furnace) { + resumeActive(ctx, (Furnace) state); + } FureamFurnaceEngine.tick(ctx, config); + int comparatorSignal = ComparatorCompat.calculate(ctx.data); + if (comparatorSignal != ctx.lastComparatorSignal || tickCounter % 20 == 0) { + ComparatorCompat.updateAround(block, comparatorSignal); + ctx.lastComparatorSignal = comparatorSignal; + } // Handle hopper below extraction (every 8 ticks, standard hopper speed) if (tickCounter % 8 == 0) { processHopperPull(block, ctx); } + } else if (!ctx.passiveProjected && state instanceof Furnace) { + projectPassiveLane(ctx, (Furnace) state); + } else if (ctx.passiveProjected && state instanceof Furnace) { + syncPassiveTimes(ctx, (Furnace) state); } // Periodic save if dirty @@ -319,22 +404,19 @@ private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull Furn Inventory targetInv = null; if (below.getType() == Material.HOPPER) { - if (below.getBlockData() instanceof org.bukkit.block.data.type.Hopper) { - org.bukkit.block.data.type.Hopper hopperData = (org.bukkit.block.data.type.Hopper) below.getBlockData(); - if (hopperData.isEnabled()) { - BlockState belowState = below.getState(); - if (belowState instanceof Hopper) { - targetInv = ((Hopper) belowState).getInventory(); - } + if (isHopperEnabled(below)) { + BlockState belowState = below.getState(); + if (belowState instanceof Hopper) { + targetInv = ((Hopper) belowState).getInventory(); } } } if (targetInv == null) { Location centerBelow = furnaceBlock.getLocation().add(0.5, -0.5, 0.5); - for (org.bukkit.entity.Entity e : furnaceBlock.getWorld().getNearbyEntities(centerBelow, 0.6, 0.6, 0.6)) { - if (e instanceof HopperMinecart) { - targetInv = ((HopperMinecart) e).getInventory(); + for (Object entity : nearbyEntities(furnaceBlock.getWorld(), centerBelow)) { + if (entity instanceof HopperMinecart) { + targetInv = ((HopperMinecart) entity).getInventory(); break; } } @@ -344,49 +426,78 @@ private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull Furn return; } - // 1. Try pulling from outputs first - for (int i = 0; i < ctx.data.outputs.size(); i++) { - ItemStack outStack = ctx.data.outputs.get(i); - if (!outStack.getType().isAir() && outStack.getAmount() > 0) { - ItemStack single = outStack.clone(); - single.setAmount(1); - - Map leftover = targetInv.addItem(single); - if (leftover.isEmpty()) { - outStack.setAmount(outStack.getAmount() - 1); - if (outStack.getAmount() <= 0) { - ctx.data.outputs.set(i, new ItemStack(Material.AIR)); - } - if (ctx.activeGui != null) { - ctx.activeGui.refreshVisuals(); - } - ctx.dirty = true; - return; - } - } - } - - // 2. Try pulling empty buckets or water buckets from fuels + // Fabric exposes extractable fuel remainders before outputs. + Material bucket = Material.matchMaterial("BUCKET"); + Material waterBucket = Material.matchMaterial("WATER_BUCKET"); for (int i = 0; i < ctx.data.fuels.size(); i++) { ItemStack fuelStack = ctx.data.fuels.get(i); - if (fuelStack.getType() == Material.BUCKET || fuelStack.getType() == Material.WATER_BUCKET) { - ItemStack single = fuelStack.clone(); - single.setAmount(1); - - Map leftover = targetInv.addItem(single); - if (leftover.isEmpty()) { - fuelStack.setAmount(fuelStack.getAmount() - 1); - if (fuelStack.getAmount() <= 0) { - ctx.data.fuels.set(i, new ItemStack(Material.AIR)); - } - if (ctx.activeGui != null) { - ctx.activeGui.refreshVisuals(); - } - ctx.dirty = true; - return; - } + if (!ItemCompat.isEmpty(fuelStack) && + (fuelStack.getType() == bucket || fuelStack.getType() == waterBucket)) { + if (moveOne(furnaceBlock, targetInv, ctx.data.fuels, i, ctx)) return; } } + for (int i = 0; i < ctx.data.outputs.size(); i++) { + if (!ItemCompat.isEmpty(ctx.data.outputs.get(i)) && + moveOne(furnaceBlock, targetInv, ctx.data.outputs, i, ctx)) return; + } + } + + private static @NotNull Iterable nearbyEntities( + @NotNull World world, @NotNull Location center + ) { + try { + Object entities = world.getClass().getMethod( + "getNearbyEntities", Location.class, double.class, double.class, double.class + ).invoke(world, center, 0.6, 0.6, 0.6); + if (entities instanceof Iterable) return (Iterable) entities; + } catch (ReflectiveOperationException ignored) { + } + return java.util.Collections.emptyList(); + } + + private static boolean isHopperEnabled(@NotNull Block hopper) { + try { + Object data = hopper.getClass().getMethod("getBlockData").invoke(hopper); + Object result = data.getClass().getMethod("isEnabled").invoke(data); + return Boolean.TRUE.equals(result); + } catch (ReflectiveOperationException ignored) { + return !hopper.isBlockPowered(); + } + } + + private static boolean moveOne( + @NotNull Block furnaceBlock, @NotNull Inventory target, + @NotNull java.util.List sourceSlots, int slot, + @NotNull FurnaceContext ctx + ) { + ItemStack sourceStack = sourceSlots.get(slot); + if (ItemCompat.isEmpty(sourceStack)) return false; + ItemStack single = sourceStack.clone(); + single.setAmount(1); + Inventory source = ((Furnace) furnaceBlock.getState()).getInventory(); + org.bukkit.event.inventory.InventoryMoveItemEvent event = + new org.bukkit.event.inventory.InventoryMoveItemEvent(source, single, target, false); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled() || ItemCompat.isEmpty(event.getItem())) return false; + ItemStack[] targetSnapshot = cloneContents(target.getContents()); + Map leftover = target.addItem(event.getItem().clone()); + if (!leftover.isEmpty()) { + target.setContents(targetSnapshot); + return false; + } + sourceStack.setAmount(sourceStack.getAmount() - 1); + if (sourceStack.getAmount() <= 0) sourceSlots.set(slot, ItemCompat.empty()); + refreshSessions(ctx); + ctx.dirty = true; + return true; + } + + private static ItemStack[] cloneContents(ItemStack[] contents) { + ItemStack[] result = new ItemStack[contents.length]; + for (int i = 0; i < contents.length; i++) { + result[i] = ItemCompat.isEmpty(contents[i]) ? null : contents[i].clone(); + } + return result; } public static void flushAll() { @@ -396,4 +507,75 @@ public static void flushAll() { } } } + + public static void refreshSessions(@NotNull FurnaceContext ctx) { + for (FureamScreenInventory session : ctx.sessions.values()) { + session.burnTime = ctx.burnTime; + session.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); + session.cookTime = ctx.cookTime; + session.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); + session.refreshVisuals(); + } + } + + public static void reconcileConfiguration() { + for (FurnaceContext ctx : CONTEXTS.values()) { + World world = ctx.getWorld(); + if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) continue; + + Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); + BlockState state = block.getState(); + if (!(state instanceof Furnace) || FureamMain.getFurnaceType(block) != ctx.type) { + closeSessions(ctx); + continue; + } + + boolean enabled = isEnabled(world, ctx.type); + if (enabled && ctx.passiveProjected) { + resumeActive(ctx, (Furnace) state); + initializeInputIdentity(ctx); + } else if (!enabled && !ctx.passiveProjected) { + closeSessions(ctx); + projectPassiveLane(ctx, (Furnace) state); + } + saveToNbt(ctx); + } + } + + public static void shutdown() { + for (FurnaceContext ctx : CONTEXTS.values()) { + closeSessions(ctx); + } + flushAll(); + CONTEXTS.clear(); + } + + public static void closeSessions(@NotNull FurnaceContext ctx) { + for (FureamScreenInventory session : ctx.sessions.values()) { + for (org.bukkit.entity.HumanEntity viewer : + new java.util.ArrayList<>(session.bukkitInventory.getViewers())) { + viewer.closeInventory(); + } + } + ctx.sessions.clear(); + } + + private static void validateSessions( + @NotNull FurnaceContext ctx, @NotNull Block block, boolean enabled + ) { + Iterator> iterator = ctx.sessions.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + org.bukkit.entity.Player player = Bukkit.getPlayer(entry.getKey()); + boolean valid = enabled && player != null && player.isOnline() && + player.getWorld().equals(block.getWorld()) && + player.getLocation().distanceSquared(block.getLocation().add(0.5, 0.5, 0.5)) <= 64.0 && + FureamMain.getFurnaceType(block) == ctx.type && + player.getOpenInventory().getTopInventory().equals(entry.getValue().bukkitInventory); + if (!valid) { + if (player != null && player.isOnline()) player.closeInventory(); + ctx.sessions.remove(entry.getKey(), entry.getValue()); + } + } + } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java index 702b635..509a571 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java @@ -4,8 +4,8 @@ import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; -import io.github.eat_ram.fuream.screen.FureamScreenHandler; import io.github.eat_ram.fuream.screen.FureamScreenInventory; +import io.github.eat_ram.fuream.compat.ItemCompat; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -28,7 +28,7 @@ public void onPlayerInteract(PlayerInteractEvent event) { if (type == null) return; Player player = event.getPlayer(); - if (player.isSneaking() && event.getItem() != null && !event.getItem().getType().isAir()) { + if (player.isSneaking() && !ItemCompat.isEmpty(player.getItemInHand())) { return; } @@ -36,23 +36,37 @@ public void onPlayerInteract(PlayerInteractEvent event) { if (config == null || !config.getEnabledFurnaceTypes().contains(type)) { return; } + if (!canOpenLocked(block, player)) return; event.setCancelled(true); FurnaceContext ctx = FurnaceManager.getOrCreateContext(block); if (ctx == null) return; - if (ctx.activeGui == null) { - ctx.activeGui = new FureamScreenInventory(block.getLocation(), type, ctx.data, config); - ctx.handler = new FureamScreenHandler(ctx.activeGui); - } - - ctx.activeGui.burnTime = ctx.burnTime; - ctx.activeGui.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); - ctx.activeGui.cookTime = ctx.cookTime; - ctx.activeGui.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); - ctx.activeGui.refreshVisuals(); + FureamScreenInventory session = new FureamScreenInventory( + player.getUniqueId(), block.getLocation(), type, ctx.data, config + ); + ctx.sessions.put(player.getUniqueId(), session); + session.burnTime = ctx.burnTime; + session.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); + session.cookTime = ctx.cookTime; + session.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); + session.refreshVisuals(); + player.openInventory(session.bukkitInventory); + } - player.openInventory(ctx.activeGui.bukkitInventory); + private static boolean canOpenLocked(Block block, Player player) { + try { + Object state = block.getState(); + Object locked = state.getClass().getMethod("isLocked").invoke(state); + if (!Boolean.TRUE.equals(locked)) return true; + Object lock = state.getClass().getMethod("getLock").invoke(state); + if (!(lock instanceof String)) return false; + org.bukkit.inventory.ItemStack held = player.getItemInHand(); + return !ItemCompat.isEmpty(held) && held.hasItemMeta() && + held.getItemMeta().hasDisplayName() && lock.equals(held.getItemMeta().getDisplayName()); + } catch (ReflectiveOperationException ignored) { + return true; + } } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java index 9a028d8..d63ff3b 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java @@ -1,6 +1,7 @@ package io.github.eat_ram.fuream.hook; -import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.screen.FureamScreenInventory; +import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -13,40 +14,39 @@ public class GuiListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST) public void onInventoryClick(InventoryClickEvent event) { - Inventory inv = event.getInventory(); - for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { - if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { - if (ctx.handler != null && event.getWhoClicked() instanceof Player) { - ctx.handler.handleClick(event, (Player) event.getWhoClicked()); - ctx.dirty = true; - } - return; - } - } + FureamScreenInventory session = session(event.getInventory()); + if (session == null || !(event.getWhoClicked() instanceof Player)) return; + session.handler.handleClick(event, (Player) event.getWhoClicked()); + FurnaceManager.FurnaceContext ctx = context(session); + if (ctx != null) ctx.dirty = true; } @EventHandler(priority = EventPriority.HIGHEST) public void onInventoryDrag(InventoryDragEvent event) { - Inventory inv = event.getInventory(); - for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { - if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { - if (ctx.handler != null) { - ctx.handler.handleDrag(event); - ctx.dirty = true; - } - return; - } - } + FureamScreenInventory session = session(event.getInventory()); + if (session == null) return; + session.handler.handleDrag(event); + FurnaceManager.FurnaceContext ctx = context(session); + if (ctx != null) ctx.dirty = true; } @EventHandler(priority = EventPriority.MONITOR) public void onInventoryClose(InventoryCloseEvent event) { - Inventory inv = event.getInventory(); - for (FurnaceContext ctx : FurnaceManager.CONTEXTS.values()) { - if (ctx.activeGui != null && ctx.activeGui.bukkitInventory.equals(inv)) { - FurnaceManager.saveToNbt(ctx); - return; - } + FureamScreenInventory session = session(event.getInventory()); + if (session == null) return; + FurnaceManager.FurnaceContext ctx = context(session); + if (ctx != null) { + ctx.sessions.remove(session.viewerId); + FurnaceManager.saveToNbt(ctx); } } + + private static FureamScreenInventory session(Inventory inventory) { + return inventory.getHolder() instanceof FureamScreenInventory + ? (FureamScreenInventory) inventory.getHolder() : null; + } + + private static FurnaceManager.FurnaceContext context(FureamScreenInventory session) { + return FurnaceManager.CONTEXTS.get(new FurnacePos(session.location)); + } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java index 7ae7cf1..18dc2fb 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/HopperListener.java @@ -5,6 +5,7 @@ import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; import io.github.eat_ram.fuream.logic.FuelTable; +import io.github.eat_ram.fuream.compat.ItemCompat; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; @@ -23,14 +24,14 @@ public void onInventoryMoveItem(InventoryMoveItemEvent event) { Inventory src = event.getSource(); // Hopper pushing INTO Furnace - if (dest instanceof FurnaceInventory && dest.getLocation() != null) { - Location furnaceLoc = dest.getLocation(); + Location furnaceLoc = inventoryLocation(dest); + if (dest instanceof FurnaceInventory && furnaceLoc != null) { Block block = furnaceLoc.getBlock(); FurnaceType type = FureamMain.getFurnaceType(block); if (type != null) { FureamWorldConfig config = FureamMain.getWorldConfig(furnaceLoc.getWorld()); if (config != null && config.getEnabledFurnaceTypes().contains(type)) { - Location srcLoc = src.getLocation(); + Location srcLoc = inventoryLocation(src); ItemStack itemToMove = event.getItem(); boolean isTop = srcLoc != null && srcLoc.getBlockY() > furnaceLoc.getBlockY(); @@ -51,4 +52,20 @@ public void onInventoryMoveItem(InventoryMoveItemEvent event) { } } } + + private static Location inventoryLocation(Inventory inventory) { + Object holder = inventory.getHolder(); + if (holder instanceof org.bukkit.block.BlockState) { + return ((org.bukkit.block.BlockState) holder).getLocation(); + } + if (holder instanceof org.bukkit.entity.Entity) { + return ((org.bukkit.entity.Entity) holder).getLocation(); + } + try { + Object value = inventory.getClass().getMethod("getLocation").invoke(inventory); + return value instanceof Location ? (Location) value : null; + } catch (ReflectiveOperationException ignored) { + return null; + } + } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java index 90f9643..014e579 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java @@ -9,6 +9,7 @@ import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; +import io.github.eat_ram.fuream.compat.ItemCompat; public abstract class FuelTable { private static final Map FUEL_TIMES = new EnumMap<>(Material.class); @@ -187,12 +188,25 @@ private static void loadDefaults() { "BOW", "CROSSBOW", "FISHING_ROD" }, 300); + // Pre-flattening aliases used by 1.7-1.12. + safePutAll(new String[]{ + "LOG", "LOG_2", "WOOD", "WOOD_STAIRS", "SPRUCE_WOOD_STAIRS", + "BIRCH_WOOD_STAIRS", "JUNGLE_WOOD_STAIRS", "ACACIA_STAIRS", + "DARK_OAK_STAIRS", "FENCE", "FENCE_GATE", "SPRUCE_FENCE", + "BIRCH_FENCE", "JUNGLE_FENCE", "DARK_OAK_FENCE", "ACACIA_FENCE", + "SPRUCE_FENCE_GATE", "BIRCH_FENCE_GATE", "JUNGLE_FENCE_GATE", + "DARK_OAK_FENCE_GATE", "ACACIA_FENCE_GATE", "WOOD_DOOR", + "TRAP_DOOR", "WOOD_PLATE", "WOOD_BUTTON", "SIGN", "WORKBENCH", + "WOOD_AXE", "WOOD_HOE", "WOOD_PICKAXE", "WOOD_SPADE", "WOOD_SWORD" + }, 300); + // Slabs (150 ticks) safePutAll(new String[]{ "OAK_SLAB", "SPRUCE_SLAB", "BIRCH_SLAB", "JUNGLE_SLAB", "ACACIA_SLAB", "DARK_OAK_SLAB", "MANGROVE_SLAB", "CHERRY_SLAB", "BAMBOO_SLAB", "BAMBOO_MOSAIC_SLAB" }, 150); + safePut("WOOD_STEP", 150); // 200 ticks safePutAll(new String[]{"BOWL", "LADDER"}, 200); @@ -203,6 +217,7 @@ private static void loadDefaults() { "JUNGLE_SAPLING", "ACACIA_SAPLING", "DARK_OAK_SAPLING", "MANGROVE_PROPAGULE", "CHERRY_SAPLING", "BAMBOO", "AZALEA", "FLOWERING_AZALEA" }, 100); + safePut("SAPLING", 100); // 67 ticks (wool, carpet) safePutAll(new String[]{ @@ -215,28 +230,29 @@ private static void loadDefaults() { "LIGHT_GRAY_CARPET", "CYAN_CARPET", "PURPLE_CARPET", "BLUE_CARPET", "BROWN_CARPET", "GREEN_CARPET", "RED_CARPET", "BLACK_CARPET" }, 67); + safePutAll(new String[]{"WOOL", "CARPET"}, 67); // Scaffolding (50 ticks) safePut("SCAFFOLDING", 50); } public static boolean isFuel(@Nullable ItemStack stack) { - if (stack == null || stack.getType().isAir()) return false; + if (ItemCompat.isEmpty(stack)) return false; return isFuel(stack.getType()); } public static boolean isFuel(@Nullable Material material) { - if (material == null || material.isAir()) return false; + if (ItemCompat.isAir(material)) return false; return FUEL_TIMES.containsKey(material); } public static int getFuelTime(@Nullable ItemStack stack) { - if (stack == null || stack.getType().isAir()) return 0; + if (ItemCompat.isEmpty(stack)) return 0; return getFuelTime(stack.getType()); } public static int getFuelTime(@Nullable Material material) { - if (material == null || material.isAir()) return 0; + if (ItemCompat.isAir(material)) return 0; return FUEL_TIMES.getOrDefault(material, 0); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index e4fade5..8be9070 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -1,329 +1,322 @@ package io.github.eat_ram.fuream.logic; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.List; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.compat.BlockCompat; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.compat.RecipeCompat; +import io.github.eat_ram.fuream.compat.RecipeHandle; +import io.github.eat_ram.fuream.compat.ServerVersion; +import io.github.eat_ram.fuream.compat.FurnaceCompat; +import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; import io.github.eat_ram.fuream.util.KeyableItemStack; +import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockState; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Furnace; -import org.bukkit.inventory.CookingRecipe; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.FurnaceBurnEvent; +import org.bukkit.event.inventory.FurnaceSmeltEvent; import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class FureamFurnaceEngine { - public static void tick( - @NotNull FurnaceContext ctx, @Nullable FureamWorldConfig config - ) { - FureamFurnaceData data = ctx.data; + public static void tick(FurnaceContext ctx, FureamWorldConfig config) { World world = ctx.getWorld(); if (world == null) return; + FureamFurnaceData data = ctx.data; + int inputCount = slotCount(config.getInputSlotCount().get(ctx.type)); + int fuelCount = slotCount(config.getFuelSlotCount().get(ctx.type)); + int outputCount = slotCount(config.getOutputSlotCount().get(ctx.type)); + ensureSize(data.inputs, inputCount); + ensureSize(data.fuels, fuelCount); + ensureSize(data.outputs, outputCount); - // Ensure virtual lists have at least the configured number of slots - int inCount = config != null ? config.getInputSlotCount().get(ctx.type) : 18; - int fuelCount = config != null ? config.getFuelSlotCount().get(ctx.type) : 18; - int outCount = config != null ? config.getOutputSlotCount().get(ctx.type) : 18; - while (data.inputs.size() < inCount) data.inputs.add(new ItemStack(Material.AIR)); - while (data.fuels.size() < fuelCount) data.fuels.add(new ItemStack(Material.AIR)); - while (data.outputs.size() < outCount) data.outputs.add(new ItemStack(Material.AIR)); - - // Ensure default cookTimeTotal - int baseCookTime = ctx.type == FurnaceType.FURNACE ? 200 : 100; - if (ctx.cookTimeTotal <= 0) { - ctx.cookTimeTotal = baseCookTime; - } - - // Ingest and feed through vanilla furnace inventory (for seamless hopper integration) Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); - BlockState blockState = block.getState(); - if (blockState instanceof org.bukkit.block.Furnace) { - org.bukkit.block.Furnace furnaceTile = (org.bukkit.block.Furnace) blockState; - FurnaceInventory inv = furnaceTile.getInventory(); - - // 1. Ingest smelting slot (top hopper input) - ItemStack smelting = inv.getSmelting(); - if (smelting != null && !smelting.getType().isAir() && smelting.getAmount() > 0) { - ItemStack remainder = insertStackIntoList(data.inputs, smelting, inCount); - inv.setSmelting(remainder.getType().isAir() ? null : remainder); - ctx.dirty = true; - } - - // 2. Ingest fuel slot (side hopper fuel input) - ItemStack fuel = inv.getFuel(); - if (fuel != null && !fuel.getType().isAir() && fuel.getAmount() > 0) { - ItemStack remainder = insertStackIntoList(data.fuels, fuel, fuelCount); - inv.setFuel(remainder.getType().isAir() ? null : remainder); - ctx.dirty = true; - } - - // 3. Recover outputs stranded in the hidden vanilla result slot. - // Bottom hoppers pull directly from data.outputs in FurnaceManager; - // proactively filling this slot would hide the first smelted item - // from the custom GUI until a second item is produced. - ItemStack result = inv.getResult(); - if (result != null && !result.getType().isAir() && result.getAmount() > 0) { - inv.setResult(recoverVanillaOutput(data.outputs, result, outCount)); - ctx.dirty = true; - } - - furnaceTile.setCookTime((short) 0); - furnaceTile.setBurnTime((short) 0); - } + BlockState state = block.getState(); + if (!(state instanceof org.bukkit.block.Furnace)) return; + org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) state; + ingestVanillaLane(ctx, furnace.getInventory(), inputCount, fuelCount, outputCount); + FurnaceCompat.setCookTime(furnace, 0); + FurnaceCompat.setBurnTime(furnace, 0); - boolean wasBurning = ctx.burnTime > 0; if (ctx.burnTime > 0) { ctx.burnTime--; ctx.dirty = true; } - // Find first valid input stack - int firstInputIdx = -1; - ItemStack inputStack = null; - for (int i = 0; i < data.inputs.size(); i++) { - ItemStack s = data.inputs.get(i); - if (!s.getType().isAir() && s.getAmount() > 0) { - firstInputIdx = i; - inputStack = s; - break; - } - } + int inputSlot = firstPresent(data.inputs); + ItemStack input = inputSlot < 0 ? null : data.inputs.get(inputSlot); + updateInputIdentity(ctx, inputSlot, input); - CookingRecipe matchedRecipe = null; - if (inputStack != null) { - matchedRecipe = getRecipeForInput(world, inputStack, ctx); - } - - boolean canSmelt = false; - ItemStack recipeResult = null; - int outSlotIdx = -1; - - if (matchedRecipe != null) { - NamespacedKey matchedKey = matchedRecipe.getKey(); - if (!matchedKey.equals(data.runningRecipe)) { - data.runningRecipe = matchedKey; - ctx.dirty = true; - } - recipeResult = matchedRecipe.getResult(); - outSlotIdx = findFittingOutputSlot(data.outputs, recipeResult); - if (outSlotIdx >= 0) { - canSmelt = true; - int cookTime = matchedRecipe.getCookingTime(); - if (cookTime <= 0) { - cookTime = (ctx.type == FurnaceType.FURNACE ? 200 : 100); - } - ctx.cookTimeTotal = cookTime; + RecipeHandle recipe = input == null ? null : getRecipeForInput(input, ctx); + ItemStack result = recipe == null ? null : recipe.result.clone(); + boolean canSmelt = recipe != null && canFitAll(data.outputs, result, outputCount); + if (recipe != null) { + if (!recipe.id.equals(data.runningRecipe)) { + ctx.cookTime = 0; + ctx.cookTimeTotal = Math.max(1, recipe.cookingTime); + ctx.startSmeltPending = true; } + data.runningRecipe = recipe.id; + } else { + data.runningRecipe = null; } - // Light the furnace if unlit and can smelt if (ctx.burnTime <= 0 && canSmelt) { - int firstFuelIdx = -1; - ItemStack fuelStack = null; - for (int i = 0; i < data.fuels.size(); i++) { - ItemStack s = data.fuels.get(i); - if (!s.getType().isAir() && s.getAmount() > 0 && FuelTable.isFuel(s)) { - firstFuelIdx = i; - fuelStack = s; - break; - } - } - - if (fuelStack != null) { - int fuelTime = FuelTable.getFuelTime(fuelStack); - if (ctx.type == FurnaceType.SMOKER || ctx.type == FurnaceType.BLAST_FURNACE) { - fuelTime = Math.max(1, fuelTime / 2); - } + ignite(ctx, block, fuelCount); + } - ctx.burnTime = fuelTime; - ctx.fuelTimeTotal = fuelTime; + if (ctx.burnTime > 0 && canSmelt) { + if (ctx.startSmeltPending) { + int requestedTime = fireStartSmelt(block, input, recipe); + ctx.cookTimeTotal = Math.max(1, requestedTime); + ctx.startSmeltPending = false; ctx.dirty = true; - - // Handle recipe remainder (e.g. Lava Bucket -> Bucket in fuel slot) - if (fuelStack.getType() == Material.LAVA_BUCKET) { - data.fuels.set(firstFuelIdx, new ItemStack(Material.BUCKET)); + } + ctx.cookTime++; + ctx.dirty = true; + if (ctx.cookTime >= Math.max(1, ctx.cookTimeTotal)) { + FurnaceSmeltEvent event = new FurnaceSmeltEvent(block, one(input), result.clone()); + Bukkit.getPluginManager().callEvent(event); + ItemStack eventResult = event.getResult(); + if (event.isCancelled() || ItemCompat.isEmpty(eventResult) || + !canFitAll(data.outputs, eventResult, outputCount)) { + ctx.cookTime = Math.max(0, ctx.cookTimeTotal - 1); } else { - fuelStack.setAmount(fuelStack.getAmount() - 1); - if (fuelStack.getAmount() <= 0) { - data.fuels.set(firstFuelIdx, new ItemStack(Material.AIR)); + ItemStack remainder = insertStackIntoList(data.outputs, eventResult, outputCount); + if (ItemCompat.isEmpty(remainder)) { + FureamFurnaceLogic.stashExperience(recipe, data); + handleWetSponge(input, data.fuels, fuelCount); + decrement(data.inputs, inputSlot); + ctx.cookTime = 0; + data.runningRecipe = null; + ctx.lastInputKey = null; + ctx.lastInputSlot = -1; + ctx.startSmeltPending = true; } } } - } - - // Cook progression - if (ctx.burnTime > 0 && canSmelt) { - ctx.cookTime++; + } else if (!canSmelt && ctx.cookTime > 0) { + ctx.cookTime = Math.max(0, ctx.cookTime - 2); ctx.dirty = true; + } - if (ctx.cookTime >= ctx.cookTimeTotal) { - ctx.cookTime = 0; - - // Deposit result - ItemStack currentOut = data.outputs.get(outSlotIdx); - if (currentOut.getType().isAir()) { - data.outputs.set(outSlotIdx, recipeResult.clone()); - } else if (currentOut.isSimilar(recipeResult)) { - currentOut.setAmount(currentOut.getAmount() + recipeResult.getAmount()); - data.outputs.set(outSlotIdx, currentOut); - } - - // Stash experience - FureamFurnaceLogic.stashExperience(matchedRecipe, data); - - // Wet sponge drying water bucket logic - if (inputStack.getType() == Material.WET_SPONGE) { - for (int i = 0; i < data.fuels.size(); i++) { - ItemStack f = data.fuels.get(i); - if (f.getType() == Material.BUCKET) { - f.setAmount(f.getAmount() - 1); - if (f.getAmount() <= 0) { - data.fuels.set(i, new ItemStack(Material.WATER_BUCKET)); - } else { - data.fuels.add(new ItemStack(Material.WATER_BUCKET)); - } - break; - } - } - } + BlockCompat.setLit(block, ctx.burnTime > 0); + io.github.eat_ram.fuream.hook.FurnaceManager.refreshSessions(ctx); + } - // Decrement input - inputStack.setAmount(inputStack.getAmount() - 1); - if (inputStack.getAmount() <= 0) { - data.inputs.set(firstInputIdx, new ItemStack(Material.AIR)); - } - data.runningRecipe = null; - } + private static void ignite(FurnaceContext ctx, Block block, int fuelCount) { + int fuelSlot = firstFuel(ctx.data.fuels, fuelCount); + if (fuelSlot < 0) return; + ItemStack fuel = ctx.data.fuels.get(fuelSlot); + int fuelTime = FuelTable.getFuelTime(fuel); + if (ctx.type != FurnaceType.FURNACE) fuelTime = Math.max(1, fuelTime / 2); + + FurnaceBurnEvent event = new FurnaceBurnEvent(block, one(fuel), fuelTime); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled() || !event.isBurning() || event.getBurnTime() <= 0) return; + + ctx.burnTime = event.getBurnTime(); + ctx.fuelTimeTotal = event.getBurnTime(); + Material lavaBucket = Material.matchMaterial("LAVA_BUCKET"); + Material bucket = Material.matchMaterial("BUCKET"); + if (lavaBucket != null && bucket != null && fuel.getType() == lavaBucket) { + ctx.data.fuels.set(fuelSlot, new ItemStack(bucket)); } else { - if (matchedRecipe == null && data.runningRecipe != null) { - data.runningRecipe = null; - ctx.dirty = true; - } - if (ctx.cookTime > 0) { - ctx.cookTime = Math.max(0, ctx.cookTime - 2); - ctx.dirty = true; + decrement(ctx.data.fuels, fuelSlot); + } + ctx.dirty = true; + } + + private static int fireStartSmelt(Block block, ItemStack input, RecipeHandle recipe) { + if (!ServerVersion.CURRENT.atLeast(1, 18)) return recipe.cookingTime; + try { + Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceStartSmeltEvent"); + for (Constructor constructor : eventClass.getConstructors()) { + Class[] types = constructor.getParameterTypes(); + if (types.length != 3 || !types[0].isInstance(block) || !types[1].isInstance(input) || + !types[2].isInstance(recipe.nativeRecipe)) continue; + Event event = (Event) constructor.newInstance(block, one(input), recipe.nativeRecipe); + Bukkit.getPluginManager().callEvent(event); + if (event instanceof Cancellable && ((Cancellable) event).isCancelled()) return Integer.MAX_VALUE; + Method getter = eventClass.getMethod("getTotalCookTime"); + return ((Number) getter.invoke(event)).intValue(); } + } catch (ReflectiveOperationException | LinkageError ignored) { } + return recipe.cookingTime; + } - // Update block lit state - boolean isBurning = ctx.burnTime > 0; - if (wasBurning != isBurning) { - updateBlockLitState(world, ctx.pos.x, ctx.pos.y, ctx.pos.z, isBurning); + private static RecipeHandle getRecipeForInput(ItemStack input, FurnaceContext ctx) { + if (ctx.data.runningRecipe != null) { + RecipeHandle running = RecipeCompat.findById(input, ctx.type, ctx.data.runningRecipe); + if (running != null) return running; + } + if (VersionAdapters.current().supportsRecipeOverrides()) { + String overridden = ctx.data.overriddenRecipes.get(new KeyableItemStack(one(input))); + RecipeHandle selected = RecipeCompat.findById(input, ctx.type, overridden); + if (selected != null) return selected; } + return RecipeCompat.findFirst(input, ctx.type); + } + + static void handleWetSponge(ItemStack input, List fuels, int maxSlots) { + Material wetSponge = Material.matchMaterial("WET_SPONGE"); + Material bucket = Material.matchMaterial("BUCKET"); + Material waterBucket = Material.matchMaterial("WATER_BUCKET"); + if (wetSponge == null || bucket == null || waterBucket == null || input.getType() != wetSponge) return; - // Sync with open GUI - if (ctx.activeGui != null) { - ctx.activeGui.burnTime = ctx.burnTime; - ctx.activeGui.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); - ctx.activeGui.cookTime = ctx.cookTime; - ctx.activeGui.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); - ctx.activeGui.refreshVisuals(); + int bucketSlot = -1; + for (int i = 0; i < Math.min(maxSlots, fuels.size()); i++) { + if (!ItemCompat.isEmpty(fuels.get(i)) && fuels.get(i).getType() == bucket) { + bucketSlot = i; + break; + } } + if (bucketSlot < 0) return; + int empty = firstEmpty(fuels, maxSlots); + if (empty < 0) return; + ItemStack found = fuels.get(bucketSlot); + found.setAmount(found.getAmount() - 1); + if (found.getAmount() <= 0) fuels.set(bucketSlot, ItemCompat.empty()); + fuels.set(empty, new ItemStack(waterBucket)); } - public static @NotNull ItemStack insertStackIntoList( - @NotNull List list, @NotNull ItemStack incoming, int maxSlots + private static void ingestVanillaLane( + FurnaceContext ctx, FurnaceInventory inventory, int inputCount, int fuelCount, int outputCount ) { - while (list.size() < maxSlots) { - list.add(new ItemStack(Material.AIR)); + ItemStack smelting = inventory.getSmelting(); + if (!ItemCompat.isEmpty(smelting)) { + ItemStack remainder = insertStackIntoList(ctx.data.inputs, smelting, inputCount); + inventory.setSmelting(ItemCompat.isEmpty(remainder) ? null : remainder); + ctx.dirty = true; + } + ItemStack fuel = inventory.getFuel(); + if (!ItemCompat.isEmpty(fuel)) { + ItemStack remainder = insertStackIntoList(ctx.data.fuels, fuel, fuelCount); + inventory.setFuel(ItemCompat.isEmpty(remainder) ? null : remainder); + ctx.dirty = true; } + ItemStack result = inventory.getResult(); + if (!ItemCompat.isEmpty(result)) { + inventory.setResult(recoverVanillaOutput(ctx.data.outputs, result, outputCount)); + ctx.dirty = true; + } + } + public static ItemStack insertStackIntoList(List slots, ItemStack incoming, int maxSlots) { + if (ItemCompat.isEmpty(incoming)) return ItemCompat.empty(); + ensureSize(slots, maxSlots); ItemStack remaining = incoming.clone(); - - // 1. Stack into existing slots - for (int i = 0; i < maxSlots && i < list.size(); i++) { - ItemStack s = list.get(i); - if (s != null && !s.getType().isAir() && s.isSimilar(remaining)) { - int space = s.getMaxStackSize() - s.getAmount(); - if (space > 0) { - int toAdd = Math.min(space, remaining.getAmount()); - s.setAmount(s.getAmount() + toAdd); - remaining.setAmount(remaining.getAmount() - toAdd); - if (remaining.getAmount() <= 0) { - return new ItemStack(Material.AIR); - } + for (int i = 0; i < maxSlots && !ItemCompat.isEmpty(remaining); i++) { + ItemStack current = slots.get(i); + if (!ItemCompat.isEmpty(current) && current.isSimilar(remaining)) { + int move = Math.min(remaining.getAmount(), current.getMaxStackSize() - current.getAmount()); + if (move > 0) { + current.setAmount(current.getAmount() + move); + remaining.setAmount(remaining.getAmount() - move); } } } - - // 2. Place into empty slots - for (int i = 0; i < maxSlots && i < list.size(); i++) { - ItemStack s = list.get(i); - if (s == null || s.getType().isAir()) { - list.set(i, remaining.clone()); - return new ItemStack(Material.AIR); + for (int i = 0; i < maxSlots && !ItemCompat.isEmpty(remaining); i++) { + if (ItemCompat.isEmpty(slots.get(i))) { + int move = Math.min(remaining.getAmount(), remaining.getMaxStackSize()); + ItemStack placed = remaining.clone(); + placed.setAmount(move); + slots.set(i, placed); + remaining.setAmount(remaining.getAmount() - move); } } - - return remaining; + return ItemCompat.isEmpty(remaining) ? ItemCompat.empty() : remaining; } - static @Nullable ItemStack recoverVanillaOutput( - @NotNull List outputs, @Nullable ItemStack vanillaResult, - int maxSlots + public static ItemStack recoverVanillaOutput( + List outputs, ItemStack vanillaResult, int maxSlots ) { - if (vanillaResult == null || vanillaResult.getType().isAir() || - vanillaResult.getAmount() <= 0) { - return null; - } - ItemStack remainder = insertStackIntoList(outputs, vanillaResult, maxSlots); - return remainder.getType().isAir() ? null : remainder; + if (ItemCompat.isEmpty(vanillaResult)) return null; + ItemStack remainder = insertStackIntoList(outputs, vanillaResult, Math.max(1, maxSlots)); + return ItemCompat.isEmpty(remainder) ? null : remainder; } - private static CookingRecipe getRecipeForInput( - World world, ItemStack input, FurnaceContext ctx - ) { - if (ctx.data.runningRecipe != null) { - for (CookingRecipe recipe : FureamFurnaceLogic.findAllMatches(world, input, ctx.type)) { - if (recipe.getKey().equals(ctx.data.runningRecipe)) { - return recipe; - } - } + private static boolean canFitAll(List slots, ItemStack stack, int maxSlots) { + if (ItemCompat.isEmpty(stack)) return false; + int capacity = 0; + ensureSize(slots, maxSlots); + for (int i = 0; i < maxSlots; i++) { + ItemStack current = slots.get(i); + if (ItemCompat.isEmpty(current)) capacity += stack.getMaxStackSize(); + else if (current.isSimilar(stack)) capacity += current.getMaxStackSize() - current.getAmount(); + if (capacity >= stack.getAmount()) return true; } + return false; + } - NamespacedKey overridden = ctx.data.overriddenRecipes.get(new KeyableItemStack(input)); - if (overridden != null) { - List> matches = FureamFurnaceLogic.findAllMatches(world, input, ctx.type); - for (CookingRecipe r : matches) { - if (r.getKey().equals(overridden)) { - return r; - } - } + private static int firstFuel(List fuels, int maxSlots) { + for (int i = 0; i < Math.min(maxSlots, fuels.size()); i++) { + if (!ItemCompat.isEmpty(fuels.get(i)) && FuelTable.isFuel(fuels.get(i))) return i; } - return FureamFurnaceLogic.findRecipe(world, input, ctx.type).orElse(null); + return -1; } - private static int findFittingOutputSlot(List outputs, ItemStack result) { - for (int i = 0; i < outputs.size(); i++) { - ItemStack s = outputs.get(i); - if (s == null || s.getType().isAir()) { - return i; - } - if (s.isSimilar(result) && s.getAmount() + result.getAmount() <= s.getMaxStackSize()) { - return i; - } - } + private static int firstPresent(List stacks) { + for (int i = 0; i < stacks.size(); i++) if (!ItemCompat.isEmpty(stacks.get(i))) return i; return -1; } - private static void updateBlockLitState(World world, int x, int y, int z, boolean lit) { - Block block = world.getBlockAt(x, y, z); - BlockData data = block.getBlockData(); - if (data instanceof Furnace) { - Furnace furnaceData = (Furnace) data; - if (furnaceData.isLit() != lit) { - furnaceData.setLit(lit); - block.setBlockData(furnaceData, false); - } - } + private static int firstEmpty(List stacks, int maxSlots) { + ensureSize(stacks, maxSlots); + for (int i = 0; i < maxSlots; i++) if (ItemCompat.isEmpty(stacks.get(i))) return i; + return -1; + } + + private static void decrement(List slots, int slot) { + ItemStack stack = slots.get(slot); + stack.setAmount(stack.getAmount() - 1); + if (stack.getAmount() <= 0) slots.set(slot, ItemCompat.empty()); + } + + private static ItemStack one(ItemStack stack) { + ItemStack result = stack.clone(); + result.setAmount(1); + return result; + } + + private static boolean sameKey(KeyableItemStack left, KeyableItemStack right) { + return left == right || left != null && left.equals(right); + } + + static boolean updateInputIdentity( + FurnaceContext ctx, int inputSlot, @Nullable ItemStack input + ) { + KeyableItemStack inputKey = ItemCompat.isEmpty(input) + ? null : new KeyableItemStack(one(input)); + if (inputSlot == ctx.lastInputSlot && sameKey(ctx.lastInputKey, inputKey)) return false; + ctx.lastInputSlot = inputSlot; + ctx.lastInputKey = inputKey; + ctx.cookTime = 0; + ctx.data.runningRecipe = null; + ctx.startSmeltPending = true; + ctx.dirty = true; + return true; + } + + private static int slotCount(Integer configured) { + return Math.max(1, configured == null ? 9 : configured); + } + + private static void ensureSize(List slots, int size) { + while (slots.size() < size) slots.add(ItemCompat.empty()); } private FureamFurnaceEngine() { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java index 00b02b3..a6c1cf5 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java @@ -1,24 +1,18 @@ package io.github.eat_ram.fuream.logic; -import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Optional; import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.compat.RecipeCompat; +import io.github.eat_ram.fuream.compat.RecipeHandle; import io.github.eat_ram.fuream.data.FureamFurnaceData; -import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.Player; -import org.bukkit.inventory.BlastingRecipe; -import org.bukkit.inventory.CookingRecipe; -import org.bukkit.inventory.FurnaceRecipe; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.Recipe; -import org.bukkit.inventory.SmokingRecipe; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,74 +24,35 @@ public static boolean isFuel(@Nullable ItemStack stack) { public static boolean isAcceptableInput( @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType ) { - return findRecipe(world, stack, furnaceType).isPresent(); + return RecipeCompat.findFirst(stack, furnaceType) != null; } - public static Optional> findRecipe( + public static Optional findRecipe( @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType ) { - if (stack.getType().isAir()) { - return Optional.empty(); - } - Iterator it = Bukkit.recipeIterator(); - while (it.hasNext()) { - Recipe r = it.next(); - if (isRecipeMatchForFurnace(r, furnaceType, stack)) { - return Optional.of((CookingRecipe) r); - } - } - return Optional.empty(); + return Optional.ofNullable(RecipeCompat.findFirst(stack, furnaceType)); } - public static List> findAllMatches( + public static List findAllMatches( @Nullable World world, @NotNull ItemStack stack, @NotNull FurnaceType furnaceType ) { - List> list = new ArrayList<>(); - if (stack.getType().isAir()) { - return list; - } - Iterator it = Bukkit.recipeIterator(); - while (it.hasNext()) { - Recipe r = it.next(); - if (isRecipeMatchForFurnace(r, furnaceType, stack)) { - list.add((CookingRecipe) r); - } - } - return list; - } - - private static boolean isRecipeMatchForFurnace(Recipe r, FurnaceType furnaceType, ItemStack stack) { - if (!(r instanceof CookingRecipe)) { - return false; - } - CookingRecipe cr = (CookingRecipe) r; - if (furnaceType == FurnaceType.SMOKER) { - if (!(cr instanceof SmokingRecipe)) return false; - } else if (furnaceType == FurnaceType.BLAST_FURNACE) { - if (!(cr instanceof BlastingRecipe)) return false; - } else { - if (!(cr instanceof FurnaceRecipe)) return false; - } - return cr.getInputChoice().test(stack); + return RecipeCompat.findAll(stack, furnaceType); } public static void stashExperience( - @Nullable CookingRecipe recipe, @NotNull FureamFurnaceData data + @Nullable RecipeHandle recipe, @NotNull FureamFurnaceData data ) { - if (recipe != null) { - data.experience += recipe.getExperience(); - } + if (recipe != null) data.experience += recipe.experience; } public static void grantExperience( @NotNull Player player, @NotNull Location loc, @NotNull FureamFurnaceData data ) { int amount = (int) data.experience; - if (amount > 0) { - data.experience -= amount; - World world = player.getWorld(); - world.spawn(player.getLocation(), ExperienceOrb.class, orb -> orb.setExperience(amount)); - } + if (amount <= 0) return; + data.experience -= amount; + ExperienceOrb orb = player.getWorld().spawn(player.getLocation(), ExperienceOrb.class); + orb.setExperience(amount); } public static void dropOnBreak( @@ -107,37 +62,29 @@ public static void dropOnBreak( if (world == null) return; Location center = pos.clone().add(0.5, 0.5, 0.5); - for (ItemStack s : data.inputs) { - dropIfPresent(world, center, s); - } + for (ItemStack stack : data.inputs) dropIfPresent(world, center, stack); data.inputs.clear(); - - for (ItemStack s : data.fuels) { - dropIfPresent(world, center, s); - } + for (ItemStack stack : data.fuels) dropIfPresent(world, center, stack); data.fuels.clear(); - - for (ItemStack s : data.outputs) { - dropIfPresent(world, center, s); - } + for (ItemStack stack : data.outputs) dropIfPresent(world, center, stack); data.outputs.clear(); - if (!data.recipeOverridingInput.getType().isAir()) { + // Deliberately retained for Fabric parity (known Fabric bug). + if (!ItemCompat.isEmpty(data.recipeOverridingInput)) { dropIfPresent(world, center, data.recipeOverridingInput); - data.recipeOverridingInput = new ItemStack(Material.AIR); + data.recipeOverridingInput = ItemCompat.empty(); } int amount = (int) Math.floor(data.experience); if (amount > 0) { data.experience -= amount; - world.spawn(center, ExperienceOrb.class, orb -> orb.setExperience(amount)); + ExperienceOrb orb = world.spawn(center, ExperienceOrb.class); + orb.setExperience(amount); } } - private static void dropIfPresent(@NotNull World world, @NotNull Location loc, @Nullable ItemStack stack) { - if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { - world.dropItemNaturally(loc, stack.clone()); - } + private static void dropIfPresent(World world, Location location, ItemStack stack) { + if (!ItemCompat.isEmpty(stack)) world.dropItemNaturally(location, stack.clone()); } private FureamFurnaceLogic() { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java index eb8f614..2f8d673 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java @@ -3,7 +3,9 @@ import java.lang.instrument.Instrumentation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.function.BiConsumer; import net.bytebuddy.agent.ByteBuddyAgent; @@ -33,7 +35,22 @@ public static void install(JavaPlugin plugin) { Class furnaceClass = findFurnaceClass(); Method loadMethod = findPersistenceMethod(furnaceClass, true); Method saveMethod = findPersistenceMethod(furnaceClass, false); - Instrumentation instrumentation = ByteBuddyAgent.install(); + Instrumentation instrumentation; + try { + instrumentation = ByteBuddyAgent.install(); + } catch (IllegalStateException defaultAttachmentFailure) { + try { + instrumentation = ByteBuddyAgent.install( + ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE + ); + plugin.getLogger().info( + "Default JVM attachment unavailable; using bundled native attachment provider" + ); + } catch (RuntimeException emulatedAttachmentFailure) { + emulatedAttachmentFailure.addSuppressed(defaultAttachmentFailure); + throw emulatedAttachmentFailure; + } + } if (!instrumentation.isModifiableClass(furnaceClass)) { throw new IllegalStateException("Furnace block entity class is not modifiable: " + furnaceClass.getName()); @@ -85,44 +102,72 @@ private static Class findFurnaceClass() { } private static Method findPersistenceMethod(Class furnaceClass, boolean load) { - String modernName = load ? "loadAdditional" : "saveAdditional"; - for (Method method : furnaceClass.getDeclaredMethods()) { - if (modernName.equals(method.getName()) && hasCompoundFirstArgument(method)) { - return method; + String[] preferredNames = load + ? new String[] {"loadAdditional", "load", "loadData", "a"} + : new String[] {"saveAdditional", "save", "saveData", "b"}; + List candidates = new ArrayList<>(); + for (Class current = furnaceClass; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers()) && hasCompoundArgument(method)) { + candidates.add(method); + } } } - for (Method method : furnaceClass.getDeclaredMethods()) { - int modifiers = method.getModifiers(); - boolean accessMatches = load ? Modifier.isPublic(modifiers) : Modifier.isProtected(modifiers); - if (accessMatches && !Modifier.isStatic(modifiers) && - method.getReturnType() == void.class && hasCompoundFirstArgument(method)) { - return method; + for (String name : preferredNames) { + for (Method method : candidates) { + if (name.equals(method.getName()) && returnTypeMatches(method, load)) { + return method; + } } } + for (Method method : candidates) { + int modifiers = method.getModifiers(); + boolean accessMatches = load + ? Modifier.isPublic(modifiers) + : Modifier.isProtected(modifiers) || !Modifier.isPublic(modifiers); + if (accessMatches && returnTypeMatches(method, load)) return method; + } + throw new IllegalStateException( "Unable to locate furnace " + (load ? "load" : "save") + " method on " + - furnaceClass.getName() + "; declared methods=" + Arrays.toString(furnaceClass.getDeclaredMethods()) + furnaceClass.getName() + "; candidate methods=" + Arrays.toString(candidates.toArray()) ); } - private static boolean hasCompoundFirstArgument(Method method) { - Class[] parameters = method.getParameterTypes(); - if (parameters.length == 0) { - return false; - } - String name = parameters[0].getName(); + private static boolean returnTypeMatches(Method method, boolean load) { + if (load) return method.getReturnType() == void.class; + if (method.getReturnType() == void.class) return true; + String name = method.getReturnType().getName(); return name.endsWith(".CompoundTag") || name.endsWith(".NBTTagCompound"); } + private static boolean hasCompoundArgument(Method method) { + for (Class parameter : method.getParameterTypes()) { + String name = parameter.getName(); + if (name.endsWith(".CompoundTag") || name.endsWith(".NBTTagCompound")) return true; + } + return false; + } + public static class LoadAdvice { @Advice.OnMethodExit @SuppressWarnings("unchecked") public static void exit( @Advice.This Object furnace, - @Advice.Argument(0) Object rootNbt + @Advice.AllArguments Object[] arguments ) { + Object rootNbt = null; + for (Object argument : arguments) { + if (argument == null) continue; + String name = argument.getClass().getName(); + if (name.endsWith(".CompoundTag") || name.endsWith(".NBTTagCompound")) { + rootNbt = argument; + break; + } + } + if (rootNbt == null) return; Object callback = System.getProperties().get( "io.github.eat_ram.fuream.root_nbt_load_hook" ); @@ -137,8 +182,18 @@ public static class SaveAdvice { @SuppressWarnings("unchecked") public static void exit( @Advice.This Object furnace, - @Advice.Argument(0) Object rootNbt + @Advice.AllArguments Object[] arguments ) { + Object rootNbt = null; + for (Object argument : arguments) { + if (argument == null) continue; + String name = argument.getClass().getName(); + if (name.endsWith(".CompoundTag") || name.endsWith(".NBTTagCompound")) { + rootNbt = argument; + break; + } + } + if (rootNbt == null) return; Object callback = System.getProperties().get( "io.github.eat_ram.fuream.root_nbt_save_hook" ); diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java index cf98014..fa8c767 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java @@ -10,10 +10,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.BiConsumer; -import de.tr7zw.nbtapi.NBT; -import de.tr7zw.nbtapi.NBTType; -import de.tr7zw.nbtapi.iface.ReadWriteNBT; -import de.tr7zw.nbtapi.iface.ReadableNBT; import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.data.FureamData; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; @@ -28,7 +24,6 @@ public final class FurnaceRootNbtBridge { private static final String STATE_MAP_KEY = "io.github.eat_ram.fuream.root_nbt_state"; private static final ConcurrentMap, Field> TAG_MAP_FIELDS = new ConcurrentHashMap<>(); - private static final ConcurrentMap, Method> PUT_SHORT_METHODS = new ConcurrentHashMap<>(); private static final ConcurrentMap, Method> TILE_ENTITY_METHODS = new ConcurrentHashMap<>(); private static final ThreadLocal INJECTION_SUPPRESSED = new ThreadLocal<>(); @@ -52,14 +47,14 @@ public static boolean loadNativeData( return false; } - ReadableNBT dataNbt = NBT.wrapNMSTag(stored[0]); + NativeNbtCompound dataNbt = new NativeNbtCompound(stored[0]); ctx.data.readNbt(ctx, dataNbt); for (Map.Entry entry : ctx.extraData.entrySet()) { entry.getValue().readNbt(ctx, dataNbt); } if (stored[1] != null && !(stored[1] instanceof Integer)) { - ReadableNBT rootNbt = NBT.wrapNMSTag(stored[1]); + NativeNbtCompound rootNbt = new NativeNbtCompound(stored[1]); ctx.burnTime = readNumericTag(rootNbt, "BurnTime", ctx.burnTime); ctx.cookTime = readNumericTag(rootNbt, "CookTime", ctx.cookTime); ctx.cookTimeTotal = readNumericTag(rootNbt, "CookTimeTotal", ctx.cookTimeTotal); @@ -83,24 +78,29 @@ public static void store( throw new IllegalStateException("Unable to access the live furnace block entity"); } - Object[] previous = stateMap().get(tileEntity); - Object previousData = previous != null && previous.length > 0 ? previous[0] : null; - ReadWriteNBT dataNbt = NBT.createNBTObject(); - if (previousData != null) { - dataNbt.mergeCompound(NBT.wrapNMSTag(previousData)); - } + NativeNbtCompound dataNbt = NativeNbtCompound.createForServer(); ctx.data.writeNbt(ctx, dataNbt); for (FureamData extra : ctx.extraData.values()) { extra.writeNbt(ctx, dataNbt); } - Object rawData = rawCompound(dataNbt); stateMap().put(tileEntity, new Object[] { - rawData, ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal + dataNbt.keys().isEmpty() ? null : dataNbt.raw(), + ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal }); } + public static @Nullable NativeNbtCompound capturedRoot(@NotNull BlockState state) { + Object tileEntity = getTileEntity(state); + if (tileEntity == null) return null; + Object[] stored = stateMap().get(tileEntity); + if (stored == null || stored.length < 2 || stored[1] == null || stored[1] instanceof Integer) { + return null; + } + return new NativeNbtCompound(stored[1]); + } + public static void withoutInjection(@NotNull Runnable action) { INJECTION_SUPPRESSED.set(Boolean.TRUE); try { @@ -113,9 +113,7 @@ public static void withoutInjection(@NotNull Runnable action) { private static void captureLoadedNbt(Object tileEntity, Object rootNbt) { try { Object fureamData = tagMap(rootNbt).get(FureamMain.DATA_KEY); - if (fureamData != null) { - stateMap().put(tileEntity, new Object[] {fureamData, rootNbt}); - } + stateMap().put(tileEntity, new Object[] {fureamData, rootNbt}); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Unable to capture furnace root NBT", e); } @@ -131,15 +129,15 @@ private static void injectSavedNbt(Object tileEntity, Object rootNbt) { return; } - if (stored[0] != null) { - tagMap(rootNbt).put(FureamMain.DATA_KEY, stored[0]); - } + if (stored[0] != null) tagMap(rootNbt).put(FureamMain.DATA_KEY, stored[0]); + else tagMap(rootNbt).remove(FureamMain.DATA_KEY); if (stored.length < 4 || !(stored[1] instanceof Integer)) { return; } - putShort(rootNbt, "BurnTime", (short) (int) (Integer) stored[1]); - putShort(rootNbt, "CookTime", (short) (int) (Integer) stored[2]); - putShort(rootNbt, "CookTimeTotal", (short) (int) (Integer) stored[3]); + NativeNbtCompound root = new NativeNbtCompound(rootNbt); + root.setShort("BurnTime", (short) (int) (Integer) stored[1]); + root.setShort("CookTime", (short) (int) (Integer) stored[2]); + root.setShort("CookTimeTotal", (short) (int) (Integer) stored[3]); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Unable to inject furnace root NBT", e); } @@ -185,34 +183,11 @@ private static Field findTagMapField(Class compoundClass) { throw new IllegalStateException("Unable to locate NBT compound tag map on " + compoundClass.getName()); } - private static void putShort(Object compound, String key, short value) - throws ReflectiveOperationException { - Method method = PUT_SHORT_METHODS.get(compound.getClass()); - if (method == null) { - method = findPutShortMethod(compound.getClass()); - PUT_SHORT_METHODS.put(compound.getClass(), method); - } - method.invoke(compound, key, value); - } - - private static Method findPutShortMethod(Class compoundClass) { - for (Method method : compoundClass.getMethods()) { - Class[] parameters = method.getParameterTypes(); - if (parameters.length == 2 && parameters[0] == String.class && - parameters[1] == short.class && method.getReturnType() == void.class) { - method.setAccessible(true); - return method; - } - } - throw new IllegalStateException("Unable to locate NBT putShort on " + compoundClass.getName()); - } - private static @Nullable Object getTileEntity(@NotNull BlockState state) { try { Method method = TILE_ENTITY_METHODS.get(state.getClass()); if (method == null) { - method = state.getClass().getMethod("getTileEntity"); - method.setAccessible(true); + method = findTileEntityMethod(state.getClass()); TILE_ENTITY_METHODS.put(state.getClass(), method); } return method.invoke(state); @@ -221,27 +196,25 @@ private static Method findPutShortMethod(Class compoundClass) { } } - private static Object rawCompound(ReadWriteNBT nbt) { - try { - Method method = nbt.getClass().getMethod("getCompound"); - method.setAccessible(true); - return method.invoke(nbt); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Unable to unwrap NBT compound", e); + private static @NotNull Method findTileEntityMethod(@NotNull Class stateClass) + throws NoSuchMethodException { + for (String preferred : new String[] {"getTileEntityFromWorld", "getTileEntity"}) { + for (Class current = stateClass; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if (preferred.equals(method.getName()) && method.getParameterTypes().length == 0) { + method.setAccessible(true); + return method; + } + } + } } + throw new NoSuchMethodException("No live tile entity accessor on " + stateClass.getName()); } private static int readNumericTag( - @Nullable ReadableNBT nbt, @NotNull String key, int fallback + @Nullable NativeNbtCompound nbt, @NotNull String key, int fallback ) { - if (nbt == null || !nbt.hasTag(key)) { - return fallback; - } - NBTType type = nbt.getType(key); - if (type == NBTType.NBTTagShort) return nbt.getShort(key); - if (type == NBTType.NBTTagByte) return nbt.getByte(key); - if (type == NBTType.NBTTagLong) return (int) (long) nbt.getLong(key); - return nbt.getInteger(key); + return nbt == null ? fallback : nbt.getInt(key, fallback); } private FurnaceRootNbtBridge() { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java new file mode 100644 index 0000000..044965c --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java @@ -0,0 +1,121 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Optional; + +import io.github.eat_ram.fuream.compat.ItemCompat; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +/** Native ItemStack codec, including registry-provider signatures used by 1.20.5+. */ +public final class NativeItemNbt { + private static Method asNmsCopy; + private static Method asBukkitCopy; + private static Object registryProvider; + + public static NativeNbtCompound write(ItemStack stack) { + if (ItemCompat.isEmpty(stack)) return NativeNbtCompound.createForServer(); + try { + ensureCraftMethods(); + Object nms = asNmsCopy.invoke(null, stack); + NativeNbtCompound target = NativeNbtCompound.createForServer(); + for (Method method : nms.getClass().getMethods()) { + if (Modifier.isStatic(method.getModifiers())) continue; + Object[] args = serializationArguments(method.getParameterTypes(), target.raw()); + if (args == null) continue; + Class returnType = method.getReturnType(); + if (!returnType.isAssignableFrom(target.raw().getClass()) && + !target.raw().getClass().isAssignableFrom(returnType)) continue; + try { + Object result = method.invoke(nms, args); + if (result != null && target.raw().getClass().isInstance(result)) { + return new NativeNbtCompound(result); + } + return target; + } catch (ReflectiveOperationException | IllegalArgumentException ignored) { + } + } + throw new IllegalStateException("Unable to locate native ItemStack save method"); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to serialize Bukkit ItemStack", e); + } + } + + public static ItemStack read(NativeNbtCompound compound) { + if (compound == null || compound.keys().isEmpty()) return ItemCompat.empty(); + try { + ensureCraftMethods(); + Class nmsClass = asNmsCopy.getReturnType(); + for (Method method : nmsClass.getMethods()) { + if (!Modifier.isStatic(method.getModifiers())) continue; + Object[] args = serializationArguments(method.getParameterTypes(), compound.raw()); + if (args == null) continue; + if (method.getReturnType() != nmsClass && method.getReturnType() != Optional.class) continue; + try { + Object result = method.invoke(null, args); + if (result instanceof Optional) result = ((Optional) result).orElse(null); + if (result != null && nmsClass.isInstance(result)) { + ItemStack stack = (ItemStack) asBukkitCopy.invoke(null, result); + return stack == null ? ItemCompat.empty() : stack; + } + } catch (ReflectiveOperationException | IllegalArgumentException ignored) { + } + } + } catch (ReflectiveOperationException ignored) { + } + return ItemCompat.empty(); + } + + private static Object[] serializationArguments(Class[] parameterTypes, Object compound) { + if (parameterTypes.length == 0 || parameterTypes.length > 2) return null; + Object[] args = new Object[parameterTypes.length]; + boolean hasCompound = false; + for (int i = 0; i < parameterTypes.length; i++) { + Class type = parameterTypes[i]; + if (type.isInstance(compound)) { + args[i] = compound; + hasCompound = true; + } else if (type.getName().contains("HolderLookup") || type.getName().contains("RegistryAccess")) { + Object provider = registryProvider(type); + if (provider == null || !type.isInstance(provider)) return null; + args[i] = provider; + } else { + return null; + } + } + return hasCompound ? args : null; + } + + private static void ensureCraftMethods() throws ReflectiveOperationException { + if (asNmsCopy != null && asBukkitCopy != null) return; + String craftRoot = Bukkit.getServer().getClass().getPackage().getName(); + Class craftItemStack = Class.forName(craftRoot + ".inventory.CraftItemStack"); + asNmsCopy = craftItemStack.getMethod("asNMSCopy", ItemStack.class); + Class nmsClass = asNmsCopy.getReturnType(); + asBukkitCopy = craftItemStack.getMethod("asBukkitCopy", nmsClass); + } + + private static Object registryProvider(Class expectedType) { + if (registryProvider != null && expectedType.isInstance(registryProvider)) return registryProvider; + try { + Object craftServer = Bukkit.getServer(); + Object minecraftServer = craftServer.getClass().getMethod("getServer").invoke(craftServer); + for (Method method : minecraftServer.getClass().getMethods()) { + if (method.getParameterTypes().length == 0 && expectedType.isAssignableFrom(method.getReturnType())) { + Object result = method.invoke(minecraftServer); + if (result != null) { + registryProvider = result; + return result; + } + } + } + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + private NativeItemNbt() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java new file mode 100644 index 0000000..3237964 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java @@ -0,0 +1,232 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Bukkit; + +/** Minimal reflective NBT view shared by legacy, flattened and component servers. */ +public final class NativeNbtCompound { + private static final Map, Field> MAP_FIELDS = new ConcurrentHashMap<>(); + private static final Map SETTERS = new ConcurrentHashMap<>(); + private static final Map, Class> LIST_CLASSES = new ConcurrentHashMap<>(); + + private final Object raw; + + public NativeNbtCompound(Object raw) { + if (raw == null) throw new IllegalArgumentException("raw NBT compound cannot be null"); + this.raw = raw; + } + + public Object raw() { + return raw; + } + + public static NativeNbtCompound create(Class compoundClass) { + try { + Constructor constructor = compoundClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return new NativeNbtCompound(constructor.newInstance()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to create " + compoundClass.getName(), e); + } + } + + public static NativeNbtCompound createForServer() { + ClassLoader loader = Bukkit.getServer().getClass().getClassLoader(); + String craftPackage = Bukkit.getServer().getClass().getPackage().getName(); + String version = craftPackage.substring(craftPackage.lastIndexOf('.') + 1); + String[] candidates = { + "net.minecraft.nbt.CompoundTag", + "net.minecraft.nbt.NBTTagCompound", + "net.minecraft.server." + version + ".NBTTagCompound" + }; + for (String candidate : candidates) { + try { + return create(Class.forName(candidate, false, loader)); + } catch (ClassNotFoundException ignored) { + } + } + throw new IllegalStateException("Unable to locate the server NBT compound class"); + } + + public boolean has(String key) { + return tags().containsKey(key); + } + + public void remove(String key) { + tags().remove(key); + } + + public Set keys() { + return Collections.unmodifiableSet(tags().keySet()); + } + + public NativeNbtCompound getCompound(String key) { + Object value = tags().get(key); + return isCompound(value) ? new NativeNbtCompound(value) : null; + } + + public NativeNbtCompound getOrCreateCompound(String key) { + NativeNbtCompound existing = getCompound(key); + if (existing != null) return existing; + NativeNbtCompound created = create(raw.getClass()); + tags().put(key, created.raw()); + return created; + } + + public NativeNbtList getList(String key) { + Object value = tags().get(key); + return value == null ? null : new NativeNbtList(value); + } + + public NativeNbtList resetList(String key) { + Class listClass = LIST_CLASSES.get(raw.getClass()); + if (listClass == null) { + listClass = locateListClass(raw.getClass()); + LIST_CLASSES.put(raw.getClass(), listClass); + } + try { + Constructor constructor = listClass.getDeclaredConstructor(); + constructor.setAccessible(true); + Object list = constructor.newInstance(); + tags().put(key, list); + return new NativeNbtList(list); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to create NBT list", e); + } + } + + public void setString(String key, String value) { + invokeSetter(String.class, key, value); + } + + public void setInt(String key, int value) { + invokeSetter(int.class, key, value); + } + + public void setFloat(String key, float value) { + invokeSetter(float.class, key, value); + } + + public void setShort(String key, short value) { + invokeSetter(short.class, key, value); + } + + public String getString(String key) { + Object value = primitiveValue(tags().get(key)); + return value instanceof String ? (String) value : ""; + } + + public int getInt(String key, int fallback) { + Object value = primitiveValue(tags().get(key)); + return value instanceof Number ? ((Number) value).intValue() : fallback; + } + + public float getFloat(String key, float fallback) { + Object value = primitiveValue(tags().get(key)); + return value instanceof Number ? ((Number) value).floatValue() : fallback; + } + + @SuppressWarnings("unchecked") + private Map tags() { + try { + Field field = MAP_FIELDS.get(raw.getClass()); + if (field == null) { + field = findMapField(raw.getClass()); + MAP_FIELDS.put(raw.getClass(), field); + } + return (Map) field.get(raw); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to access NBT compound", e); + } + } + + private void invokeSetter(Class valueType, String key, Object value) { + String cacheKey = raw.getClass().getName() + '#' + valueType.getName(); + Method method = SETTERS.get(cacheKey); + if (method == null) { + method = findSetter(raw.getClass(), valueType); + SETTERS.put(cacheKey, method); + } + try { + method.invoke(raw, key, value); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to write NBT value " + key, e); + } + } + + private static Method findSetter(Class type, Class valueType) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + Class[] parameters = method.getParameterTypes(); + if (!Modifier.isStatic(method.getModifiers()) && parameters.length == 2 && + parameters[0] == String.class && parameters[1] == valueType && + method.getReturnType() == void.class) { + method.setAccessible(true); + return method; + } + } + } + throw new IllegalStateException("Unable to locate NBT setter for " + valueType.getName()); + } + + private static Field findMapField(Class type) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + } + throw new IllegalStateException("Unable to locate NBT compound map on " + type.getName()); + } + + private static Class locateListClass(Class compoundClass) { + String packageName = compoundClass.getPackage().getName(); + ClassLoader loader = compoundClass.getClassLoader(); + String[] candidates = { + packageName + ".ListTag", + packageName + ".NBTTagList" + }; + for (String candidate : candidates) { + try { + return Class.forName(candidate, false, loader); + } catch (ClassNotFoundException ignored) { + } + } + throw new IllegalStateException("Unable to locate NBT list next to " + compoundClass.getName()); + } + + private static boolean isCompound(Object value) { + if (value == null) return false; + String name = value.getClass().getSimpleName(); + return name.equals("CompoundTag") || name.equals("NBTTagCompound"); + } + + private static Object primitiveValue(Object tag) { + if (tag == null) return null; + for (Class current = tag.getClass(); current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) continue; + Class type = field.getType(); + if (type == String.class || type.isPrimitive() || Number.class.isAssignableFrom(type)) { + try { + field.setAccessible(true); + return field.get(tag); + } catch (IllegalAccessException ignored) { + } + } + } + } + return null; + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java new file mode 100644 index 0000000..c45c1c1 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java @@ -0,0 +1,71 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class NativeNbtList { + private static final Map, Field> LIST_FIELDS = new ConcurrentHashMap<>(); + private final Object raw; + + NativeNbtList(Object raw) { + this.raw = raw; + } + + public int size() { + return values().size(); + } + + public NativeNbtCompound getCompound(int index) { + Object value = values().get(index); + return value == null ? null : new NativeNbtCompound(value); + } + + public void add(NativeNbtCompound compound) { + if (raw instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) raw; + list.add(compound.raw()); + return; + } + for (java.lang.reflect.Method method : raw.getClass().getMethods()) { + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 1 && parameters[0].isInstance(compound.raw())) { + try { + method.invoke(raw, compound.raw()); + return; + } catch (ReflectiveOperationException ignored) { + } + } + } + throw new IllegalStateException("Unable to append to NBT list " + raw.getClass().getName()); + } + + @SuppressWarnings("unchecked") + private List values() { + try { + Field field = LIST_FIELDS.get(raw.getClass()); + if (field == null) { + field = findListField(raw.getClass()); + LIST_FIELDS.put(raw.getClass(), field); + } + return (List) field.get(raw); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to access NBT list", e); + } + } + + private static Field findListField(Class type) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && List.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + } + throw new IllegalStateException("Unable to locate NBT list values on " + type.getName()); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java index 4d38010..e6861fb 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java @@ -5,6 +5,8 @@ import io.github.eat_ram.fuream.hook.FurnaceManager; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.compat.FurnaceEventCompat; import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -31,14 +33,22 @@ public void handleClick(InventoryClickEvent event, Player player) { return; } + if (clickType == ClickType.DOUBLE_CLICK) { + event.setCancelled(true); + this.collectMatchingToCursor(event, player); + this.display.syncToBukkitInventory(); + this.markDirtyAndSave(); + return; + } + // Click in player inventory with shift-click into custom GUI if (rawSlot >= 54) { if (event.isShiftClick()) { event.setCancelled(true); ItemStack current = event.getCurrentItem(); - if (current != null && !current.getType().isAir()) { + if (!ItemCompat.isEmpty(current)) { ItemStack remaining = this.quickMoveToContainer(current); - event.setCurrentItem(remaining.getType().isAir() ? null : remaining); + event.setCurrentItem(ItemCompat.isEmpty(remaining) ? null : remaining); this.display.syncToBukkitInventory(); } } @@ -105,7 +115,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC // 1. Shift click if (event.isShiftClick()) { - if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); @@ -123,13 +133,13 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC int hotbarSlot = event.getHotbarButton(); if (hotbarSlot >= 0 && hotbarSlot < 9) { ItemStack hotbarItem = player.getInventory().getItem(hotbarSlot); - if (hotbarItem == null || hotbarItem.getType().isAir()) { - if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (ItemCompat.isEmpty(hotbarItem)) { + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { player.getInventory().setItem(hotbarSlot, current.clone()); area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); } } else if (area.isStorable(this.display, rawSlot - 45)) { - if (current.getType().isAir()) { + if (ItemCompat.isEmpty(current)) { area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); player.getInventory().setItem(hotbarSlot, new ItemStack(Material.AIR)); } else if (area.isTakable(this.display, rawSlot - 45)) { @@ -143,8 +153,8 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC } // 3. Cursor empty -> Take item from slot - if (cursor == null || cursor.getType().isAir()) { - if (!current.getType().isAir() && area.isTakable(this.display, rawSlot - 45)) { + if (ItemCompat.isEmpty(cursor)) { + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { if (event.isRightClick()) { int half = (current.getAmount() + 1) / 2; ItemStack take = current.clone(); @@ -160,7 +170,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC } else { // 4. Cursor has item -> Place/Swap into slot if (area.isStorable(this.display, rawSlot - 45)) { - if (current.getType().isAir()) { + if (ItemCompat.isEmpty(current)) { if (event.isRightClick()) { ItemStack putOne = cursor.clone(); putOne.setAmount(1); @@ -210,9 +220,38 @@ private void markDirtyAndSave() { private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player player) { ItemStack current = this.display.getStack(slotIndex); - if (current.getType().isAir()) return; + if (ItemCompat.isEmpty(current)) return; + ItemStack extractIdentity = current.clone(); + extractIdentity.setAmount(1); + int moved = 0; + + if (event.getClick() == ClickType.NUMBER_KEY) { + int hotbarSlot = event.getHotbarButton(); + if (hotbarSlot >= 0 && hotbarSlot < 9 && + ItemCompat.isEmpty(player.getInventory().getItem(hotbarSlot))) { + player.getInventory().setItem(hotbarSlot, current.clone()); + moved = current.getAmount(); + this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + } + this.notifyExtract(player, extractIdentity, moved); + this.markDirtyAndSave(); + return; + } + + if (event.getClick() == ClickType.DROP || event.getClick() == ClickType.CONTROL_DROP) { + moved = event.getClick() == ClickType.DROP ? 1 : current.getAmount(); + ItemStack dropped = current.clone(); + dropped.setAmount(moved); + player.getWorld().dropItemNaturally(player.getLocation(), dropped); + current.setAmount(current.getAmount() - moved); + this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); + this.notifyExtract(player, dropped, moved); + this.markDirtyAndSave(); + return; + } if (event.isShiftClick()) { + int before = current.getAmount(); Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { this.display.setStack(slotIndex, new ItemStack(Material.AIR)); @@ -220,35 +259,79 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player current.setAmount(leftover.values().iterator().next().getAmount()); this.display.setStack(slotIndex, current); } + moved = before - (leftover.isEmpty() ? 0 : current.getAmount()); + this.notifyExtract(player, extractIdentity, moved); this.markDirtyAndSave(); return; } ItemStack cursor = event.getCursor(); - if (cursor == null || cursor.getType().isAir()) { - event.getView().setCursor(current.clone()); - this.display.setStack(slotIndex, new ItemStack(Material.AIR)); + if (ItemCompat.isEmpty(cursor)) { + moved = event.isRightClick() ? (current.getAmount() + 1) / 2 : current.getAmount(); + ItemStack taken = current.clone(); + taken.setAmount(moved); + event.getView().setCursor(taken); + current.setAmount(current.getAmount() - moved); + this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); } else if (cursor.isSimilar(current)) { int max = cursor.getMaxStackSize(); int space = max - cursor.getAmount(); int move = Math.min(space, current.getAmount()); if (move > 0) { + moved = move; cursor.setAmount(cursor.getAmount() + move); current.setAmount(current.getAmount() - move); event.getView().setCursor(cursor); this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); } } + this.notifyExtract(player, extractIdentity, moved); this.markDirtyAndSave(); } + private void collectMatchingToCursor(InventoryClickEvent event, Player player) { + ItemStack cursor = event.getCursor(); + if (ItemCompat.isEmpty(cursor)) return; + int space = cursor.getMaxStackSize() - cursor.getAmount(); + int extracted = 0; + for (int i = 0; i < this.display.data.outputs.size() && space > 0; i++) { + ItemStack stack = this.display.data.outputs.get(i); + if (ItemCompat.isEmpty(stack) || !stack.isSimilar(cursor)) continue; + int move = Math.min(space, stack.getAmount()); + cursor.setAmount(cursor.getAmount() + move); + stack.setAmount(stack.getAmount() - move); + if (stack.getAmount() <= 0) this.display.data.outputs.set(i, ItemCompat.empty()); + space -= move; + extracted += move; + } + for (int i = 0; i < player.getInventory().getSize() && space > 0; i++) { + ItemStack stack = player.getInventory().getItem(i); + if (ItemCompat.isEmpty(stack) || !stack.isSimilar(cursor)) continue; + int move = Math.min(space, stack.getAmount()); + cursor.setAmount(cursor.getAmount() + move); + stack.setAmount(stack.getAmount() - move); + player.getInventory().setItem(i, stack.getAmount() > 0 ? stack : null); + space -= move; + } + event.getView().setCursor(cursor); + this.notifyExtract(player, cursor, extracted); + } + + private void notifyExtract(Player player, ItemStack stack, int amount) { + if (amount > 0 && !ItemCompat.isEmpty(stack)) { + FurnaceEventCompat.fireExtract( + player, this.display.location.getBlock(), stack.getType(), amount + ); + } + } + private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Player player) { boolean isFuel = this.display.isFuelRowSlot(slotIndex); ItemStack cursor = event.getCursor(); ItemStack current = this.display.getStack(slotIndex); if (event.isShiftClick()) { - if (!current.getType().isAir()) { + if (!ItemCompat.isEmpty(current)) { Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { this.display.setStack(slotIndex, new ItemStack(Material.AIR)); @@ -261,8 +344,8 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe return; } - if (cursor == null || cursor.getType().isAir()) { - if (!current.getType().isAir()) { + if (ItemCompat.isEmpty(cursor)) { + if (!ItemCompat.isEmpty(current)) { if (event.isRightClick()) { int half = (current.getAmount() + 1) / 2; ItemStack take = current.clone(); @@ -276,10 +359,12 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe } } } else { - if (isFuel && !FureamFurnaceLogic.isFuel(cursor)) { + Material bucket = Material.matchMaterial("BUCKET"); + if (isFuel && !FureamFurnaceLogic.isFuel(cursor) && + (bucket == null || cursor.getType() != bucket)) { return; } - if (current.getType().isAir()) { + if (ItemCompat.isEmpty(current)) { if (event.isRightClick()) { ItemStack putOne = cursor.clone(); putOne.setAmount(1); @@ -323,18 +408,20 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe int stop = this.display.getInputSlotStop(); for (int i = 0; i < stop; i++) { remaining = this.insertIntoSlot(i, remaining); - if (remaining.getType().isAir() || remaining.getAmount() <= 0) { + if (ItemCompat.isEmpty(remaining)) { this.markDirtyAndSave(); return new ItemStack(Material.AIR); } } } - if (FureamFurnaceLogic.isFuel(remaining)) { + Material bucket = Material.matchMaterial("BUCKET"); + if (FureamFurnaceLogic.isFuel(remaining) || + bucket != null && remaining.getType() == bucket) { int fuelStart = 18; int stop = this.display.getFuelSlotStop(); for (int i = fuelStart; i < stop; i++) { remaining = this.insertIntoSlot(i, remaining); - if (remaining.getType().isAir() || remaining.getAmount() <= 0) { + if (ItemCompat.isEmpty(remaining)) { this.markDirtyAndSave(); return new ItemStack(Material.AIR); } @@ -346,7 +433,7 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe private @NotNull ItemStack insertIntoSlot(int slotIndex, @NotNull ItemStack stack) { ItemStack current = this.display.getStack(slotIndex); - if (current.getType().isAir()) { + if (ItemCompat.isEmpty(current)) { this.display.setStack(slotIndex, stack.clone()); return new ItemStack(Material.AIR); } @@ -364,11 +451,21 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe } public void handleDrag(@NotNull InventoryDragEvent event) { - for (int rawSlot : event.getRawSlots()) { - if (rawSlot < 54) { + boolean touchesDisplay = false; + for (Map.Entry entry : event.getNewItems().entrySet()) { + int rawSlot = entry.getKey(); + if (rawSlot >= 54) continue; + touchesDisplay = true; + if (!this.display.isValid(rawSlot, entry.getValue())) { event.setCancelled(true); return; } } + if (!touchesDisplay) return; + for (Map.Entry entry : event.getNewItems().entrySet()) { + if (entry.getKey() < 54) this.display.setStack(entry.getKey(), entry.getValue().clone()); + } + this.markDirtyAndSave(); + this.display.refreshVisuals(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java index 5c4b6d6..2bfc11b 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; +import java.util.UUID; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; @@ -10,11 +11,13 @@ import io.github.eat_ram.fuream.hook.FurnaceManager; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.compat.ItemCompat; import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.Contract; @@ -35,7 +38,7 @@ * 45..53 functional area & decor (row 6) * */ -public class FureamScreenInventory { +public class FureamScreenInventory implements InventoryHolder { public static final int COLS = 9; public static final int SIZE = 54; public static final ArrayList<@NotNull BiFunction< @@ -61,6 +64,8 @@ public class FureamScreenInventory { public final @Range(from = 1, to = Integer.MAX_VALUE) int outputSlots; public final @NotNull Location location; public final @NotNull FureamFurnaceData data; + public final @NotNull UUID viewerId; + public final @NotNull FureamScreenHandler handler; public int burnTime; public int fuelTimeTotal; @@ -76,10 +81,11 @@ public class FureamScreenInventory { public final @NotNull Inventory bukkitInventory; public FureamScreenInventory( - @NotNull Location location, @NotNull FurnaceType type, + @NotNull UUID viewerId, @NotNull Location location, @NotNull FurnaceType type, @NotNull FureamFurnaceData data, @Nullable FureamWorldConfig config ) { this.location = location; + this.viewerId = viewerId; this.furnaceType = type; this.data = data; this.config = config; @@ -93,22 +99,23 @@ public FureamScreenInventory( this.fuelSlots = Math.max(1, fuelSlotCount); this.outputSlots = Math.max(1, outputSlotCount); - this.fuelLeftItem = parseMaterial(config != null ? config.getGuiFuelLeftItemId().get(type) : null, Material.ORANGE_STAINED_GLASS_PANE); - this.fuelUsedItem = parseMaterial(config != null ? config.getGuiFuelUsedItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); - this.progressDoneItem = parseMaterial(config != null ? config.getGuiProgressDoneItemId().get(type) : null, Material.WHITE_STAINED_GLASS_PANE); - this.progressRemainingItem = parseMaterial(config != null ? config.getGuiProgressRemainingItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); - this.nextFunctionalItem = parseMaterial(config != null ? config.getGuiNextFunctionalAreaItemId().get(type) : null, Material.COMMAND_BLOCK); + this.fuelLeftItem = parseMaterial(config != null ? config.getGuiFuelLeftItemId().get(type) : null, Material.GLASS_PANE); + this.fuelUsedItem = parseMaterial(config != null ? config.getGuiFuelUsedItemId().get(type) : null, Material.GLASS_PANE); + this.progressDoneItem = parseMaterial(config != null ? config.getGuiProgressDoneItemId().get(type) : null, Material.GLASS_PANE); + this.progressRemainingItem = parseMaterial(config != null ? config.getGuiProgressRemainingItemId().get(type) : null, Material.GLASS_PANE); + this.nextFunctionalItem = parseMaterial(config != null ? config.getGuiNextFunctionalAreaItemId().get(type) : null, Material.BOOK); this.prevPageItem = parseMaterial(config != null ? config.getGuiPrevPageItemId().get(type) : null, Material.ARROW); this.nextPageItem = parseMaterial(config != null ? config.getGuiNextPageItemId().get(type) : null, Material.ARROW); - Material borderMat = parseMaterial(config != null ? config.getGuiBorderItemId().get(type) : null, Material.BLACK_STAINED_GLASS_PANE); + Material borderMat = parseMaterial(config != null ? config.getGuiBorderItemId().get(type) : null, Material.GLASS_PANE); this.border = makeTooltipItem( new ItemStack(borderMat, 1), config != null ? config.getGuiBorderItemTitle().get(type) : "GUI Border" ); - String title = type == FurnaceType.SMOKER ? "Smoker" : (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace"); - this.bukkitInventory = Bukkit.createInventory(null, 54, title); + String title = resolveTitle(location, type, config); + this.bukkitInventory = Bukkit.createInventory(this, 54, title.length() > 32 ? title.substring(0, 32) : title); + this.handler = new FureamScreenHandler(this); FurnacePos pos = new FurnacePos(this.location); FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); @@ -139,10 +146,29 @@ public FureamScreenInventory( } private static Material parseMaterial(@Nullable String id, Material fallback) { - if (id == null) return fallback; - String name = id.startsWith("minecraft:") ? id.substring(10) : id; - Material mat = Material.matchMaterial(name.toUpperCase()); - return mat != null ? mat : fallback; + return ItemCompat.matchMaterial(id, fallback); + } + + private static String resolveTitle( + Location location, FurnaceType type, @Nullable FureamWorldConfig config + ) { + try { + Object state = location.getBlock().getState(); + Object custom = state.getClass().getMethod("getCustomName").invoke(state); + if (custom instanceof String && !((String) custom).isEmpty()) return (String) custom; + } catch (ReflectiveOperationException ignored) { + } + if (config != null) { + String configured = config.getGuiTitle().get(type); + if (configured != null && !configured.isEmpty()) return configured; + } + return type == FurnaceType.SMOKER ? "Smoker" : + (type == FurnaceType.BLAST_FURNACE ? "Blast Furnace" : "Furnace"); + } + + @Override + public Inventory getInventory() { + return this.bukkitInventory; } public int computeCookTimeTotal() { @@ -367,9 +393,17 @@ public void setStack(int slot, @NotNull ItemStack stack) { } public boolean isValid(int slot, @NotNull ItemStack stack) { - if (!stack.getType().isAir()) { + if (!ItemCompat.isEmpty(stack)) { if (this.isInputSlot(slot)) return true; - if (this.isFuelRowSlot(slot)) return FureamFurnaceLogic.isFuel(stack); + if (this.isFuelRowSlot(slot)) { + Material bucket = Material.matchMaterial("BUCKET"); + return FureamFurnaceLogic.isFuel(stack) || bucket != null && stack.getType() == bucket; + } + if (this.isFunctionalAreaSlot(slot)) { + int area = this.getCurrentFunctionalArea(); + return area < this.functionalAreas.size() && + this.functionalAreas.get(area).isStorable(this, slot - 45); + } } return false; } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java index 762635a..5a59694 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/RecipeSettingAndXpFunctionalArea.java @@ -1,18 +1,18 @@ package io.github.eat_ram.fuream.screen; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.compat.RecipeHandle; +import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.util.KeyableItemStack; import org.bukkit.Material; -import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.CookingRecipe; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -36,7 +36,8 @@ public class RecipeSettingAndXpFunctionalArea implements FureamFunctionalArea { /** Slot 4 (49 in {@link FureamScreenInventory}). */ public @NotNull ItemStack xpIndicator = new ItemStack(Material.AIR); - public final @NotNull ArrayList<@NotNull CookingRecipe> currentMatchingRecipes = new ArrayList<>(); + public final @NotNull ArrayList<@NotNull RecipeHandle> currentMatchingRecipes = new ArrayList<>(); + private final boolean recipeSelectionAvailable; private @Range(from = 0, to = Integer.MAX_VALUE) int selectedRecipeIndex; private float cachedXp = -1f; @@ -44,11 +45,12 @@ public RecipeSettingAndXpFunctionalArea( FureamScreenInventory inv, FureamWorldConfig config ) { this.config = config; + this.recipeSelectionAvailable = VersionAdapters.current().supportsRecipeOverrides(); FurnaceType type = inv.furnaceType; Material prevMat = parseMaterial(config != null ? config.getGuiPrevRecipeItemId().get(type) : null, Material.BOOK); Material nextMat = parseMaterial(config != null ? config.getGuiNextRecipeItemId().get(type) : null, Material.BOOK); - Material xpMat = parseMaterial(config != null ? config.getGuiXpIndicatorItemId().get(type) : null, Material.LIME_STAINED_GLASS_PANE); + Material xpMat = parseMaterial(config != null ? config.getGuiXpIndicatorItemId().get(type) : null, Material.GLASS_PANE); this.prevRecipeItem = prevMat; this.nextRecipeItem = nextMat; @@ -58,20 +60,17 @@ public RecipeSettingAndXpFunctionalArea( } private static Material parseMaterial(@Nullable String id, Material fallback) { - if (id == null) return fallback; - String name = id.startsWith("minecraft:") ? id.substring(10) : id; - Material mat = Material.matchMaterial(name.toUpperCase()); - return mat != null ? mat : fallback; + return ItemCompat.matchMaterial(id, fallback); } @Override @Contract(pure = true) public @NotNull ItemStack getGuiStack(FureamScreenInventory inv, int slot) { switch (slot) { - case 0: return inv.data.recipeOverridingInput; - case 1: return this.prevRecipeBtn; - case 2: return this.nextRecipeBtn; - case 3: return this.recipeResultPreview; + case 0: return this.recipeSelectionAvailable ? inv.data.recipeOverridingInput : inv.border; + case 1: return this.recipeSelectionAvailable ? this.prevRecipeBtn : inv.border; + case 2: return this.recipeSelectionAvailable ? this.nextRecipeBtn : inv.border; + case 3: return this.recipeSelectionAvailable ? this.recipeResultPreview : inv.border; case 4: return this.xpIndicator; default: return inv.border; } @@ -79,7 +78,7 @@ private static Material parseMaterial(@Nullable String id, Material fallback) { @Override public void setGuiStack(FureamScreenInventory inv, int slot, ItemStack newStack) { - if (slot == 0) { + if (this.recipeSelectionAvailable && slot == 0) { inv.data.recipeOverridingInput = newStack != null ? newStack : new ItemStack(Material.AIR); this.updateRecipeSettingArea(inv); } @@ -90,6 +89,7 @@ public void onGuiClick( SlotClick slotClick, FureamScreenInventory display, int slot, int button, ClickType clickType, Player player ) { + if (!this.recipeSelectionAvailable && slot != 4) return; switch (slot) { case 0: slotClick.onSlotClick(45, button, clickType, player); @@ -119,13 +119,13 @@ public void onGuiClick( @Override @Contract(pure = true) public boolean isStorable(FureamScreenInventory inv, int slot) { - return slot == 0; + return this.recipeSelectionAvailable && slot == 0; } @Override @Contract(pure = true) public boolean isTakable(FureamScreenInventory inv, int slot) { - return slot == 0; + return this.recipeSelectionAvailable && slot == 0; } @Contract(pure = true) @@ -136,7 +136,7 @@ public boolean isTakable(FureamScreenInventory inv, int slot) { public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex) { ItemStack toFind = inv.data.recipeOverridingInput; FurnaceType type = inv.furnaceType; - if (toFind.getType().isAir()) { + if (!this.recipeSelectionAvailable || ItemCompat.isEmpty(toFind)) { this.selectedRecipeIndex = 0; this.prevRecipeBtn = makeTooltipItem( new ItemStack(this.prevRecipeItem, 1), @@ -154,8 +154,8 @@ public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex int size = this.currentMatchingRecipes.size(); if (recipeIndex > 0 && recipeIndex <= size) { this.selectedRecipeIndex = recipeIndex; - CookingRecipe recipe = this.currentMatchingRecipes.get(recipeIndex - 1); - inv.data.overriddenRecipes.put(new KeyableItemStack(toFind), recipe.getKey()); + RecipeHandle recipe = this.currentMatchingRecipes.get(recipeIndex - 1); + inv.data.overriddenRecipes.put(new KeyableItemStack(toFind), recipe.id); this.prevRecipeBtn = makeTooltipItem( new ItemStack(this.prevRecipeItem, 1), this.config != null ? this.config.getGuiPrevRecipeItemTitle().get(type) : "Previous Recipe", @@ -166,7 +166,7 @@ public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex this.config != null ? this.config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", String.format(this.config != null ? this.config.getGuiNextRecipeItemTooltip().get(type) : "Current: %d / %d", recipeIndex, size) ); - this.recipeResultPreview = recipe.getResult().clone(); + this.recipeResultPreview = recipe.result.clone(); } else { this.selectedRecipeIndex = 0; inv.data.overriddenRecipes.remove(new KeyableItemStack(toFind)); @@ -180,8 +180,8 @@ public void switchSelectedRecipeIndex(FureamScreenInventory inv, int recipeIndex this.config != null ? this.config.getGuiNextRecipeItemTitle().get(type) : "Next Recipe", String.format(this.config != null ? this.config.getGuiNextRecipeArbitraryItemTooltip().get(type) : "Current: arbitrary of %d", size) ); - CookingRecipe recipe = FureamFurnaceLogic.findRecipe(this.displayWorld(inv), toFind, inv.furnaceType).orElse(null); - this.recipeResultPreview = recipe != null ? recipe.getResult().clone() : new ItemStack(Material.AIR); + RecipeHandle recipe = FureamFurnaceLogic.findRecipe(this.displayWorld(inv), toFind, inv.furnaceType).orElse(null); + this.recipeResultPreview = recipe != null ? recipe.result.clone() : new ItemStack(Material.AIR); } } @@ -192,9 +192,8 @@ private org.bukkit.World displayWorld(FureamScreenInventory inv) { public void updateMatchingRecipes(FureamScreenInventory inv) { this.currentMatchingRecipes.clear(); ItemStack toFind = inv.data.recipeOverridingInput; - if (!toFind.getType().isAir()) { - List> matches = FureamFurnaceLogic.findAllMatches(this.displayWorld(inv), toFind, inv.furnaceType); - matches.sort(Comparator.comparing(r -> r.getKey().toString())); + if (this.recipeSelectionAvailable && !ItemCompat.isEmpty(toFind)) { + List matches = FureamFurnaceLogic.findAllMatches(this.displayWorld(inv), toFind, inv.furnaceType); this.currentMatchingRecipes.addAll(matches); } } @@ -202,14 +201,14 @@ public void updateMatchingRecipes(FureamScreenInventory inv) { public void updateRecipeSettingArea(FureamScreenInventory inv) { this.updateMatchingRecipes(inv); ItemStack toFind = inv.data.recipeOverridingInput; - if (toFind.getType().isAir()) { + if (!this.recipeSelectionAvailable || ItemCompat.isEmpty(toFind)) { this.switchSelectedRecipeIndex(inv, 0); } else { - NamespacedKey overridden = inv.data.overriddenRecipes.get(new KeyableItemStack(toFind)); + String overridden = inv.data.overriddenRecipes.get(new KeyableItemStack(toFind)); boolean found = false; int i = 1; - for (CookingRecipe recipe : this.currentMatchingRecipes) { - if (recipe.getKey().equals(overridden)) { + for (RecipeHandle recipe : this.currentMatchingRecipes) { + if (recipe.id.equals(overridden)) { this.switchSelectedRecipeIndex(inv, i); found = true; break; diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java index 59ddef6..929f575 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/KeyableItemStack.java @@ -5,6 +5,7 @@ import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import io.github.eat_ram.fuream.compat.ItemCompat; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -12,7 +13,7 @@ public final class KeyableItemStack { public final @NotNull ItemStack stack; public KeyableItemStack(@Nullable ItemStack stack) { - if (stack == null || stack.getType().isAir()) { + if (ItemCompat.isEmpty(stack)) { this.stack = new ItemStack(Material.AIR); } else { this.stack = stack.clone(); @@ -29,23 +30,27 @@ public boolean equals(Object obj) { return false; } KeyableItemStack that = (KeyableItemStack) obj; - if (this.stack.getType().isAir()) { - return that.stack.getType().isAir(); + if (ItemCompat.isEmpty(this.stack)) { + return ItemCompat.isEmpty(that.stack); } - if (that.stack.getType().isAir()) { + if (ItemCompat.isEmpty(that.stack)) { return false; } return this.stack.getType() == that.stack.getType() && + this.stack.getDurability() == that.stack.getDurability() && this.stack.getAmount() == that.stack.getAmount() && Objects.equals(this.stack.getItemMeta(), that.stack.getItemMeta()); } @Override public int hashCode() { - if (this.stack.getType().isAir()) { + if (ItemCompat.isEmpty(this.stack)) { return 0; } ItemMeta meta = this.stack.getItemMeta(); - return Objects.hash(this.stack.getType(), this.stack.getAmount(), meta != null ? meta.hashCode() : 0); + return Objects.hash( + this.stack.getType(), this.stack.getDurability(), this.stack.getAmount(), + meta != null ? meta.hashCode() : 0 + ); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java index 2d2406a..190046b 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java @@ -1,92 +1,90 @@ package io.github.eat_ram.fuream.util; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.List; +import java.util.Map; -import de.tr7zw.nbtapi.NBT; -import de.tr7zw.nbtapi.iface.ReadWriteNBT; -import de.tr7zw.nbtapi.iface.ReadWriteNBTCompoundList; -import de.tr7zw.nbtapi.iface.ReadableNBT; -import de.tr7zw.nbtapi.iface.ReadableNBTList; -import org.bukkit.Material; +import io.github.eat_ram.fuream.compat.ItemCompat; +import io.github.eat_ram.fuream.nbt.NativeItemNbt; +import io.github.eat_ram.fuream.nbt.NativeNbtCompound; +import io.github.eat_ram.fuream.nbt.NativeNbtList; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class NbtUtil { - public static @NotNull ItemStack fromNbt(@Nullable ReadableNBT compound) { - if (compound == null || compound.getKeys().isEmpty()) { - return new ItemStack(Material.AIR); - } - try { - ItemStack stack = NBT.itemStackFromNBT(compound); - return stack != null ? stack : new ItemStack(Material.AIR); - } catch (Exception e) { - return new ItemStack(Material.AIR); - } + public static @NotNull ItemStack fromNbt(@Nullable NativeNbtCompound compound) { + return compound == null ? ItemCompat.empty() : NativeItemNbt.read(compound); } - public static void writeItemToCompound(@NotNull ReadWriteNBT target, @Nullable ItemStack stack) { - if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { - ReadWriteNBT container = NBT.itemStackToNBT(stack); - target.mergeCompound(container); - } + public static void writeItemToCompound(@NotNull NativeNbtCompound target, @Nullable ItemStack stack) { + if (!ItemCompat.isEmpty(stack)) copyTags(NativeItemNbt.write(stack), target); } public static void writeStacks( - @NotNull ReadWriteNBT parent, @NotNull String key, + @NotNull NativeNbtCompound parent, @NotNull String key, @NotNull Iterable<@NotNull ItemStack> stacks ) { - ReadWriteNBTCompoundList list = resetCompoundList(parent, key); + NativeNbtList list = resetCompoundList(parent, key); int slot = 0; for (ItemStack stack : stacks) { - if (stack != null && !stack.getType().isAir() && stack.getAmount() > 0) { - ReadWriteNBT sub = list.addCompound(); - ReadWriteNBT itemNbt = NBT.itemStackToNBT(stack); - sub.mergeCompound(itemNbt); - sub.setInteger("Slot", slot); + if (!ItemCompat.isEmpty(stack)) { + NativeNbtCompound sub = NativeItemNbt.write(stack); + sub.setInt("Slot", slot); + list.add(sub); } - ++slot; + slot++; } } - public static @NotNull ReadWriteNBTCompoundList resetCompoundList( - @NotNull ReadWriteNBT parent, @NotNull String key + public static @NotNull NativeNbtList resetCompoundList( + @NotNull NativeNbtCompound parent, @NotNull String key ) { - if (parent.hasTag(key)) { - parent.removeKey(key); - } - // NBT-API creates compound-list tags lazily. Merging an explicit empty - // list keeps Fabric's required `Key: []` representation even when no - // entries are added afterwards. - parent.mergeCompound(NBT.parseNBT("{\"" + key + "\":[]}")); - return parent.getCompoundList(key); + return parent.resetList(key); } public static void readStacks( - @NotNull ReadableNBT parent, @NotNull String key, + @NotNull NativeNbtCompound parent, @NotNull String key, @NotNull List target ) { - if (!parent.hasTag(key)) { - return; + NativeNbtList list = parent.getList(key); + if (list == null) return; + for (int i = 0; i < list.size(); i++) { + NativeNbtCompound sub = list.getCompound(i); + if (sub == null || !sub.has("Slot")) continue; + int slot = sub.getInt("Slot", -1); + ItemStack stack = fromNbt(sub); + if (slot < 0 || ItemCompat.isEmpty(stack)) continue; + while (target.size() <= slot) target.add(ItemCompat.empty()); + target.set(slot, stack); } - ReadableNBTList list = parent.getCompoundList(key); - for (int i = 0; i < list.size(); ++i) { - ReadWriteNBT sub = list.get(i); - if (sub.hasTag("Slot")) { - int slot = sub.getInteger("Slot"); - ItemStack stack = fromNbt(sub); - if (!stack.getType().isAir() && slot >= 0) { - if (slot < target.size()) { - target.set(slot, stack); - } else { - while (target.size() <= slot) { - target.add(new ItemStack(Material.AIR)); - } - target.set(slot, stack); - } + } + + private static void copyTags(NativeNbtCompound source, NativeNbtCompound target) { + try { + Field sourceField = findMapField(source.raw().getClass()); + Field targetField = findMapField(target.raw().getClass()); + @SuppressWarnings("unchecked") + Map from = (Map) sourceField.get(source.raw()); + @SuppressWarnings("unchecked") + Map to = (Map) targetField.get(target.raw()); + to.putAll(from); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to copy native item NBT", e); + } + } + + private static Field findMapField(Class type) { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; } } } + throw new IllegalStateException("NBT map field missing on " + type.getName()); } private NbtUtil() { diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index 0f12799..193911a 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,9 +1,7 @@ name: Fuream version: 0.0.1 main: io.github.eat_ram.fuream.FureamMain -api-version: '1.14' -depend: - - NBTAPI +api-version: '1.13' commands: fuream: description: Fuream plugin management diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java index 092df33..7a1147d 100644 --- a/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java +++ b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java @@ -2,9 +2,17 @@ import io.github.eat_ram.fuream.api.FurnaceType; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import java.nio.file.Files; +import java.nio.file.Path; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import static org.junit.jupiter.api.Assertions.*; public class ConfigTest { + @TempDir + Path tempDir; + @Test public void testDefaultConfig() { FureamWorldConfigImpl config = new FureamWorldConfigImpl(); @@ -12,6 +20,7 @@ public void testDefaultConfig() { assertEquals(9, config.getInputSlotCount().get(type)); assertEquals(9, config.getFuelSlotCount().get(type)); assertEquals(9, config.getOutputSlotCount().get(type)); + assertNotNull(config.getGuiTitle().get(type)); assertEquals("GUI Border", config.getGuiBorderItemTitle().get(type)); assertEquals("Fuel", config.getGuiFuelItemTitle().get(type)); assertEquals("Progress", config.getGuiProgressItemTitle().get(type)); @@ -24,4 +33,37 @@ public void testDefaultConfig() { assertEquals("Next Page", config.getGuiNextPageItemTitle().get(type)); } } + + @Test + public void writerEmitsAllFieldsAndPreservesUnknownKeys() throws Exception { + Path file = tempDir.resolve("fuream.json"); + Files.write(file, "{\"third_party_key\":42}".getBytes("UTF-8")); + FureamWorldConfigImpl.writeWorldConfig(new FureamWorldConfigImpl(), file.toFile()); + + JsonObject root = new JsonParser().parse(new String(Files.readAllBytes(file), "UTF-8")).getAsJsonObject(); + assertEquals(42, root.get("third_party_key").getAsInt()); + assertTrue(root.has("gui_prev_recipe_item_id")); + assertTrue(root.has("gui_xp_indicator_item_id")); + assertTrue(root.has("gui_next_page_item_tooltip")); + assertTrue(root.has("gui_title")); + assertEquals(39, root.entrySet().size()); + } + + @Test + public void localizedGuiTitleAcceptsSingleAndPerTypeValues() throws Exception { + Path singleFile = tempDir.resolve("single.json"); + Files.write(singleFile, "{\"gui_title\":\"Localized Furnace\"}".getBytes("UTF-8")); + FureamWorldConfigImpl single = new FureamWorldConfigImpl(); + FureamWorldConfigImpl.readWorldConfig(single, singleFile.toFile()); + for (FurnaceType type : FurnaceType.values()) { + assertEquals("Localized Furnace", single.getGuiTitle().get(type)); + } + + Path mappedFile = tempDir.resolve("mapped.json"); + Files.write(mappedFile, "{\"gui_title\":{\"FURNACE\":\"Localized Oven\"}}".getBytes("UTF-8")); + FureamWorldConfigImpl mapped = new FureamWorldConfigImpl(); + FureamWorldConfigImpl.readWorldConfig(mapped, mappedFile.toFile()); + assertEquals("Localized Oven", mapped.getGuiTitle().get(FurnaceType.FURNACE)); + assertEquals("Smoker", mapped.getGuiTitle().get(FurnaceType.SMOKER)); + } } diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/compat/ServerVersionTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/compat/ServerVersionTest.java new file mode 100644 index 0000000..d4ccb73 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/compat/ServerVersionTest.java @@ -0,0 +1,24 @@ +package io.github.eat_ram.fuream.compat; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ServerVersionTest { + @Test + public void parsesClassicAndDateBasedVersions() { + ServerVersion classic = ServerVersion.parse("1.20.6-R0.1-SNAPSHOT"); + assertEquals(1, classic.major); + assertEquals(20, classic.minor); + assertEquals(6, classic.patch); + assertTrue(classic.atLeast(1, 20, 5)); + + ServerVersion dateBased = ServerVersion.parse("26.2-R0.1-SNAPSHOT"); + assertEquals(26, dateBased.major); + assertEquals(2, dateBased.minor); + assertEquals("components", dateBased.adapterFamily()); + assertFalse(dateBased.atMost(1, 12)); + } +} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/compat/VersionAdaptersTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/compat/VersionAdaptersTest.java new file mode 100644 index 0000000..56a348a --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/compat/VersionAdaptersTest.java @@ -0,0 +1,28 @@ +package io.github.eat_ram.fuream.compat; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VersionAdaptersTest { + @Test + public void selectsAllFourFamiliesAtTheirBoundaries() { + assertEquals("legacy-numeric", VersionAdapters.select(ServerVersion.parse("1.12.2")).id()); + assertEquals("flattening-obfuscated", VersionAdapters.select(ServerVersion.parse("1.13.2")).id()); + assertEquals("mojang-named", VersionAdapters.select(ServerVersion.parse("1.20.4")).id()); + assertEquals("components", VersionAdapters.select(ServerVersion.parse("1.20.5")).id()); + } + + @Test + public void exposesEraCapabilities() { + VersionAdapter legacy = VersionAdapters.select(ServerVersion.parse("1.7.10")); + VersionAdapter components = VersionAdapters.select(ServerVersion.parse("26.2")); + assertFalse(legacy.supportsSpecialFurnaces()); + assertFalse(legacy.supportsRecipeOverrides()); + assertTrue(components.supportsSpecialFurnaces()); + assertTrue(components.supportsRecipeOverrides()); + assertTrue(components.usesDataComponents()); + } +} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java index 8110486..c759c60 100644 --- a/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java @@ -5,11 +5,23 @@ import org.bukkit.Material; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.junit.jupiter.api.Test; +import io.github.eat_ram.fuream.compat.ComparatorCompat; +import io.github.eat_ram.fuream.compat.RecipeHandle; +import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.data.FureamFurnaceData; +import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.util.FurnacePos; +import io.github.eat_ram.fuream.util.KeyableItemStack; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class FureamFurnaceEngineTest { @Test @@ -64,6 +76,107 @@ outputs, stack(Material.IRON_INGOT, 2), 1 assertEquals(1, remainder.getAmount()); } + @Test + public void resultSpreadsAcrossCompatibleOutputSlots() { + List outputs = new ArrayList<>(); + outputs.add(stack(Material.IRON_INGOT, 63)); + outputs.add(stack(Material.AIR, 1)); + + ItemStack remainder = FureamFurnaceEngine.insertStackIntoList( + outputs, stack(Material.IRON_INGOT, 3), 2 + ); + + assertEquals(64, outputs.get(0).getAmount()); + assertEquals(2, outputs.get(1).getAmount()); + assertEquals(Material.AIR, remainder.getType()); + } + + @Test + public void wetSpongeDoesNotConvertBucketWhenFuelRegionIsFull() { + List fuels = new ArrayList<>(); + fuels.add(stack(Material.BUCKET, 1)); + fuels.add(stack(Material.COAL, 1)); + + FureamFurnaceEngine.handleWetSponge(stack(Material.WET_SPONGE, 1), fuels, 2); + + assertEquals(Material.BUCKET, fuels.get(0).getType()); + assertEquals(1, fuels.get(0).getAmount()); + assertEquals(Material.COAL, fuels.get(1).getType()); + } + + @Test + public void wetSpongeConvertsBucketWhenFuelRegionHasAnEmptySlot() { + List fuels = new ArrayList<>(); + fuels.add(stack(Material.BUCKET, 1)); + fuels.add(stack(Material.AIR, 1)); + + FureamFurnaceEngine.handleWetSponge(stack(Material.WET_SPONGE, 1), fuels, 2); + + assertEquals(Material.AIR, fuels.get(0).getType()); + assertEquals(Material.WATER_BUCKET, fuels.get(1).getType()); + assertEquals(2, fuels.size()); + } + + @Test + public void comparatorUsesOnlyTheThreeMaskedStacks() { + FureamFurnaceData data = new FureamFurnaceData(); + data.inputs.set(0, stack(Material.IRON_ORE, 64)); + data.inputs.add(stack(Material.GOLD_ORE, 64)); + data.fuels.set(0, stack(Material.COAL, 64)); + data.outputs.set(0, stack(Material.IRON_INGOT, 64)); + + assertEquals(15, ComparatorCompat.calculate(data)); + } + + @Test + public void legacyDurabilityParticipatesInRecipeOverrideIdentity() { + ItemStack first = new TestItemStack(Material.LEGACY_WOOL, 1, (short) 1); + ItemStack second = new TestItemStack(Material.LEGACY_WOOL, 1, (short) 2); + + assertNotEquals(new KeyableItemStack(first), new KeyableItemStack(second)); + } + + @Test + public void changingPreferredSlotOrItemResetsRecipeProgress() { + FurnaceContext context = new FurnaceContext( + new FurnacePos("test", 0, 64, 0), FurnaceType.FURNACE + ); + ItemStack iron = stack(Material.IRON_ORE, 8); + assertTrue(FureamFurnaceEngine.updateInputIdentity(context, 0, iron)); + + context.cookTime = 41; + context.data.runningRecipe = "minecraft:iron_ingot"; + context.startSmeltPending = false; + assertFalse(FureamFurnaceEngine.updateInputIdentity(context, 0, iron.clone())); + assertEquals(41, context.cookTime); + + assertTrue(FureamFurnaceEngine.updateInputIdentity(context, 1, iron.clone())); + assertEquals(0, context.cookTime); + assertNull(context.data.runningRecipe); + assertTrue(context.startSmeltPending); + + context.cookTime = 12; + context.data.runningRecipe = "minecraft:iron_ingot"; + assertTrue(FureamFurnaceEngine.updateInputIdentity( + context, 1, stack(Material.GOLD_ORE, 1) + )); + assertEquals(0, context.cookTime); + assertNull(context.data.runningRecipe); + } + + @Test + public void recipeExperienceAccumulatesWithoutRounding() { + FureamFurnaceData data = new FureamFurnaceData(); + data.experience = 1.25f; + RecipeHandle recipe = new RecipeHandle( + null, "test:recipe", stack(Material.IRON_INGOT, 1), 200, 0.7f + ); + + FureamFurnaceLogic.stashExperience(recipe, data); + + assertEquals(1.95f, data.experience, 0.0001f); + } + private static List outputSlots(ItemStack first) { List outputs = new ArrayList<>(); outputs.add(first); @@ -76,10 +189,17 @@ private static ItemStack stack(Material material, int amount) { /** Avoids the Bukkit ItemFactory dependency in this pure unit test. */ private static final class TestItemStack extends ItemStack { + private short testDurability; + private TestItemStack(Material material, int amount) { super(material, amount); } + private TestItemStack(Material material, int amount, short durability) { + super(material, amount, durability); + this.testDurability = durability; + } + @Override public boolean isSimilar(ItemStack stack) { return stack != null && this.getType() == stack.getType(); @@ -89,5 +209,25 @@ public boolean isSimilar(ItemStack stack) { public int getMaxStackSize() { return 64; } + + @Override + public ItemMeta getItemMeta() { + return null; + } + + @Override + public short getDurability() { + return this.testDurability; + } + + @Override + public void setDurability(short durability) { + this.testDurability = durability; + } + + @Override + public ItemStack clone() { + return new TestItemStack(this.getType(), this.getAmount(), this.getDurability()); + } } } diff --git a/spigot/src/variants/components/plugin.yml b/spigot/src/variants/components/plugin.yml new file mode 100644 index 0000000..6a668c4 --- /dev/null +++ b/spigot/src/variants/components/plugin.yml @@ -0,0 +1,13 @@ +name: Fuream +version: 0.0.1 +main: io.github.eat_ram.fuream.FureamMain +api-version: '1.20.5' +commands: + fuream: + description: Fuream plugin management + permission: fuream.reload + usage: / [reload|status] +permissions: + fuream.reload: + description: Allows reloading Fuream configs + default: op diff --git a/spigot/src/variants/flattening/plugin.yml b/spigot/src/variants/flattening/plugin.yml new file mode 100644 index 0000000..193911a --- /dev/null +++ b/spigot/src/variants/flattening/plugin.yml @@ -0,0 +1,13 @@ +name: Fuream +version: 0.0.1 +main: io.github.eat_ram.fuream.FureamMain +api-version: '1.13' +commands: + fuream: + description: Fuream plugin management + permission: fuream.reload + usage: / [reload|status] +permissions: + fuream.reload: + description: Allows reloading Fuream configs + default: op diff --git a/spigot/src/variants/legacy/plugin.yml b/spigot/src/variants/legacy/plugin.yml new file mode 100644 index 0000000..c330c5f --- /dev/null +++ b/spigot/src/variants/legacy/plugin.yml @@ -0,0 +1,12 @@ +name: Fuream +version: 0.0.1 +main: io.github.eat_ram.fuream.FureamMain +commands: + fuream: + description: Fuream plugin management + permission: fuream.reload + usage: / [reload|status] +permissions: + fuream.reload: + description: Allows reloading Fuream configs + default: op From 6a4c32e6a0edef4a14c997a98522e87dbec807a9 Mon Sep 17 00:00:00 2001 From: az7627 Date: Thu, 27 Aug 2026 18:28:39 +0800 Subject: [PATCH 11/13] feat(spigot): implement robust passive lane projection and recovery for disabled furnaces - Introduce PassiveLaneProjection to stream virtual items to vanilla slots when Fuream is disabled - Support preferred fuel selection with fallback to first present stack - Handle seamless refilling as vanilla furnace consumes items during passive mode - Accurately restore projected items back to their original virtual slots upon re-enabling - Add unit tests verifying zero duplication, preferred fuel switching, and lane exhaustion --- .../eat_ram/fuream/hook/FurnaceManager.java | 109 +++++-- .../fuream/logic/PassiveLaneProjection.java | 166 +++++++++++ .../logic/PassiveLaneProjectionTest.java | 274 ++++++++++++++++++ 3 files changed, 525 insertions(+), 24 deletions(-) create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index 286276d..3902642 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -17,6 +17,9 @@ import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; +import io.github.eat_ram.fuream.logic.FuelTable; +import io.github.eat_ram.fuream.logic.PassiveLaneProjection; +import io.github.eat_ram.fuream.logic.PassiveLaneProjection.SelectionMode; import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; import io.github.eat_ram.fuream.nbt.NativeNbtCompound; import io.github.eat_ram.fuream.screen.FureamScreenInventory; @@ -57,6 +60,12 @@ public static class FurnaceContext implements FureamDataHolder { public final @NotNull Map sessions = new ConcurrentHashMap<>(); public boolean dirty; public boolean passiveProjected; + public final @NotNull PassiveLaneProjection.State passiveInput = + new PassiveLaneProjection.State(); + public final @NotNull PassiveLaneProjection.State passiveFuel = + new PassiveLaneProjection.State(); + public final @NotNull PassiveLaneProjection.State passiveOutput = + new PassiveLaneProjection.State(); public int lastInputSlot = -1; public @Nullable KeyableItemStack lastInputKey; public boolean startSmeltPending = true; @@ -183,7 +192,7 @@ public static void saveToNbt(@NotNull FurnaceContext ctx) { if (!ctx.passiveProjected) { absorbVanillaLane(ctx, (Furnace) state); } else { - syncPassiveTimes(ctx, (Furnace) state); + syncPassiveLane(ctx, (Furnace) state); } FurnaceRootNbtBridge.store(state, ctx); ctx.dirty = false; @@ -285,10 +294,17 @@ private static void clearNativeLane(@NotNull Furnace furnace) { } private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + closeSessions(ctx); FurnaceInventory inventory = furnace.getInventory(); - inventory.setSmelting(adoptOrTakeProjection(ctx.data.inputs, inventory.getSmelting())); - inventory.setFuel(adoptOrTakeProjection(ctx.data.fuels, inventory.getFuel())); - inventory.setResult(adoptOrTakeProjection(ctx.data.outputs, inventory.getResult())); + inventory.setSmelting(PassiveLaneProjection.begin( + ctx.data.inputs, inventory.getSmelting(), SelectionMode.FIRST_PRESENT, ctx.passiveInput + )); + inventory.setFuel(PassiveLaneProjection.begin( + ctx.data.fuels, inventory.getFuel(), SelectionMode.FUEL_THEN_PRESENT, ctx.passiveFuel + )); + inventory.setResult(PassiveLaneProjection.begin( + ctx.data.outputs, inventory.getResult(), SelectionMode.FIRST_PRESENT, ctx.passiveOutput + )); FurnaceCompat.setBurnTime(furnace, Math.max(0, ctx.burnTime)); FurnaceCompat.setCookTime(furnace, Math.max(0, ctx.cookTime)); if (ctx.cookTimeTotal > 0) FurnaceCompat.setCookTimeTotal(furnace, ctx.cookTimeTotal); @@ -296,23 +312,6 @@ private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Fur ctx.dirty = true; } - private static @Nullable ItemStack adoptOrTakeProjection( - @NotNull java.util.List virtual, @Nullable ItemStack nativeStack - ) { - int first = firstPresentSlot(virtual); - if (!ItemCompat.isEmpty(nativeStack)) { - if (first >= 0 && virtual.get(first).isSimilar(nativeStack) && - virtual.get(first).getAmount() == nativeStack.getAmount()) { - virtual.set(first, ItemCompat.empty()); - } - return nativeStack; - } - if (first < 0) return null; - ItemStack result = virtual.get(first).clone(); - virtual.set(first, ItemCompat.empty()); - return result; - } - private static int firstPresentSlot(@NotNull java.util.List stacks) { for (int i = 0; i < stacks.size(); i++) { if (!ItemCompat.isEmpty(stacks.get(i))) return i; @@ -322,7 +321,19 @@ private static int firstPresentSlot(@NotNull java.util.List stacks) { private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { if (!ctx.passiveProjected) return; - absorbVanillaLane(ctx, furnace); + FurnaceInventory inventory = furnace.getInventory(); + inventory.setSmelting(PassiveLaneProjection.restore( + ctx.data.inputs, inventory.getSmelting(), ctx.passiveInput + )); + inventory.setFuel(PassiveLaneProjection.restore( + ctx.data.fuels, inventory.getFuel(), ctx.passiveFuel + )); + inventory.setResult(PassiveLaneProjection.restore( + ctx.data.outputs, inventory.getResult(), ctx.passiveOutput + )); + ctx.passiveInput.reset(); + ctx.passiveFuel.reset(); + ctx.passiveOutput.reset(); ctx.burnTime = FurnaceCompat.getBurnTime(furnace); ctx.cookTime = FurnaceCompat.getCookTime(furnace); ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); @@ -330,7 +341,57 @@ private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace f ctx.dirty = true; } - private static void syncPassiveTimes(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + private static void syncPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + FurnaceInventory inventory = furnace.getInventory(); + ItemStack input = inventory.getSmelting(); + ItemStack fuel = inventory.getFuel(); + ItemStack output = inventory.getResult(); + boolean changed = false; + + if (ItemCompat.isEmpty(input) && !ctx.passiveInput.isExhausted()) { + ItemStack next = PassiveLaneProjection.takeNext( + ctx.data.inputs, SelectionMode.FIRST_PRESENT, ctx.passiveInput + ); + inventory.setSmelting(next); + changed = !ItemCompat.isEmpty(next); + } + + if (ItemCompat.isEmpty(fuel)) { + if (!ctx.passiveFuel.isExhausted()) { + ItemStack next = PassiveLaneProjection.takeNext( + ctx.data.fuels, SelectionMode.FUEL_THEN_PRESENT, ctx.passiveFuel + ); + inventory.setFuel(next); + changed |= !ItemCompat.isEmpty(next); + } + } else if (!FuelTable.isFuel(fuel) && + PassiveLaneProjection.hasPreferredFuel(ctx.data.fuels, ctx.passiveFuel)) { + ItemStack remainder = PassiveLaneProjection.restore( + ctx.data.fuels, fuel, ctx.passiveFuel + ); + if (ItemCompat.isEmpty(remainder)) { + inventory.setFuel(PassiveLaneProjection.takeNext( + ctx.data.fuels, SelectionMode.FUEL_THEN_PRESENT, ctx.passiveFuel + )); + changed = true; + } else { + inventory.setFuel(remainder); + changed = remainder.getAmount() < fuel.getAmount(); + ctx.passiveFuel.blockPreferredFuelSwitch(); + } + } + + if (ItemCompat.isEmpty(output) && !ctx.passiveOutput.isExhausted()) { + ItemStack next = PassiveLaneProjection.takeNext( + ctx.data.outputs, SelectionMode.FIRST_PRESENT, ctx.passiveOutput + ); + inventory.setResult(next); + changed |= !ItemCompat.isEmpty(next); + } + + if (changed) { + ctx.dirty = true; + } ctx.burnTime = FurnaceCompat.getBurnTime(furnace); ctx.cookTime = FurnaceCompat.getCookTime(furnace); ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); @@ -386,7 +447,7 @@ public static void tickAll() { } else if (!ctx.passiveProjected && state instanceof Furnace) { projectPassiveLane(ctx, (Furnace) state); } else if (ctx.passiveProjected && state instanceof Furnace) { - syncPassiveTimes(ctx, (Furnace) state); + syncPassiveLane(ctx, (Furnace) state); } // Periodic save if dirty diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java new file mode 100644 index 0000000..acec53c --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java @@ -0,0 +1,166 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.List; + +import io.github.eat_ram.fuream.compat.ItemCompat; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Maintains one native furnace slot as a view over a virtual item lane. */ +public final class PassiveLaneProjection { + public enum SelectionMode { + FIRST_PRESENT, + FUEL_THEN_PRESENT + } + + public static final class State { + private int sourceSlot = -1; + private boolean exhausted; + private boolean preferredFuelExhausted; + private boolean preferredFuelSwitchBlocked; + + public int getSourceSlot() { + return this.sourceSlot; + } + + public boolean isExhausted() { + return this.exhausted; + } + + public boolean isPreferredFuelExhausted() { + return this.preferredFuelExhausted; + } + + public void blockPreferredFuelSwitch() { + this.preferredFuelSwitchBlocked = true; + } + + public void reset() { + this.sourceSlot = -1; + this.exhausted = false; + this.preferredFuelExhausted = false; + this.preferredFuelSwitchBlocked = false; + } + } + + public static @Nullable ItemStack begin( + @NotNull List virtual, @Nullable ItemStack nativeStack, + @NotNull SelectionMode mode, @NotNull State state + ) { + state.reset(); + if (!ItemCompat.isEmpty(nativeStack)) { + return nativeStack; + } + return takeNext(virtual, mode, state); + } + + public static @Nullable ItemStack takeNext( + @NotNull List virtual, @NotNull SelectionMode mode, + @NotNull State state + ) { + state.sourceSlot = -1; + state.preferredFuelSwitchBlocked = false; + if (state.exhausted) { + return null; + } + + int selected = selectSlot(virtual, mode, state); + if (selected < 0) { + state.exhausted = true; + return null; + } + + ItemStack projected = virtual.get(selected).clone(); + virtual.set(selected, ItemCompat.empty()); + state.sourceSlot = selected; + return projected; + } + + public static boolean hasPreferredFuel( + @NotNull List virtual, @NotNull State state + ) { + if (state.preferredFuelExhausted || state.preferredFuelSwitchBlocked) { + return false; + } + if (firstFuel(virtual) >= 0) { + return true; + } + state.preferredFuelExhausted = true; + return false; + } + + /** + * Returns the native stack to its source slot when possible. Any remainder + * stays native so callers can retry without losing items. + */ + public static @Nullable ItemStack restore( + @NotNull List virtual, @Nullable ItemStack nativeStack, + @NotNull State state + ) { + if (ItemCompat.isEmpty(nativeStack)) { + state.sourceSlot = -1; + return null; + } + + int source = state.sourceSlot; + if (source >= 0 && source < virtual.size() && ItemCompat.isEmpty(virtual.get(source))) { + virtual.set(source, nativeStack.clone()); + state.sourceSlot = -1; + state.exhausted = false; + if (FuelTable.isFuel(nativeStack)) { + state.preferredFuelExhausted = false; + } + return null; + } + + ItemStack remainder = FureamFurnaceEngine.insertStackIntoList( + virtual, nativeStack, Math.max(1, virtual.size()) + ); + state.sourceSlot = -1; + if (ItemCompat.isEmpty(remainder) || remainder.getAmount() < nativeStack.getAmount()) { + state.exhausted = false; + if (FuelTable.isFuel(nativeStack)) { + state.preferredFuelExhausted = false; + } + } + return ItemCompat.isEmpty(remainder) ? null : remainder; + } + + private static int selectSlot( + @NotNull List virtual, @NotNull SelectionMode mode, + @NotNull State state + ) { + if (mode == SelectionMode.FUEL_THEN_PRESENT && !state.preferredFuelExhausted) { + int fuel = firstFuel(virtual); + if (fuel >= 0) { + return fuel; + } + state.preferredFuelExhausted = true; + } + return firstPresent(virtual); + } + + private static int firstFuel(@NotNull List virtual) { + for (int i = 0; i < virtual.size(); i++) { + ItemStack stack = virtual.get(i); + if (!ItemCompat.isEmpty(stack) && FuelTable.isFuel(stack)) { + return i; + } + } + return -1; + } + + private static int firstPresent(@NotNull List virtual) { + for (int i = 0; i < virtual.size(); i++) { + if (!ItemCompat.isEmpty(virtual.get(i))) { + return i; + } + } + return -1; + } + + private PassiveLaneProjection() { + throw new UnsupportedOperationException(); + } +} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java new file mode 100644 index 0000000..968c5f6 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java @@ -0,0 +1,274 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.github.eat_ram.fuream.logic.PassiveLaneProjection.SelectionMode; +import io.github.eat_ram.fuream.logic.PassiveLaneProjection.State; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PassiveLaneProjectionTest { + @Test + public void inputAndOutputUseFirstPresentSlot() { + List lane = lane(air(), stack(Material.IRON_ORE, 4), stack(Material.GOLD_ORE, 2)); + State state = new State(); + + ItemStack projected = PassiveLaneProjection.begin( + lane, null, SelectionMode.FIRST_PRESENT, state + ); + + assertEquals(Material.IRON_ORE, projected.getType()); + assertEquals(4, projected.getAmount()); + assertEquals(1, state.getSourceSlot()); + assertEquals(Material.AIR, lane.get(1).getType()); + assertEquals(Material.GOLD_ORE, lane.get(2).getType()); + } + + @Test + public void fuelPrefersBurnableStackOverEarlierNonFuel() { + List lane = lane( + stack(Material.BUCKET, 1), stack(Material.COAL, 8), stack(Material.STICK, 3) + ); + + ItemStack projected = PassiveLaneProjection.begin( + lane, null, SelectionMode.FUEL_THEN_PRESENT, new State() + ); + + assertEquals(Material.COAL, projected.getType()); + assertEquals(Material.BUCKET, lane.get(0).getType()); + assertEquals(Material.AIR, lane.get(1).getType()); + } + + @Test + public void fuelFallsBackToFirstPresentStack() { + List lane = lane(air(), stack(Material.BUCKET, 1), stack(Material.IRON_INGOT, 2)); + State state = new State(); + + ItemStack projected = PassiveLaneProjection.begin( + lane, null, SelectionMode.FUEL_THEN_PRESENT, state + ); + + assertEquals(Material.BUCKET, projected.getType()); + assertEquals(1, state.getSourceSlot()); + assertTrue(state.isPreferredFuelExhausted()); + } + + @Test + public void emptyNativeSlotRefillsFromNextVirtualStack() { + List lane = lane(stack(Material.IRON_ORE, 3), stack(Material.GOLD_ORE, 5)); + State state = new State(); + + ItemStack first = PassiveLaneProjection.begin( + lane, null, SelectionMode.FIRST_PRESENT, state + ); + ItemStack second = PassiveLaneProjection.takeNext( + lane, SelectionMode.FIRST_PRESENT, state + ); + + assertEquals(Material.IRON_ORE, first.getType()); + assertEquals(Material.GOLD_ORE, second.getType()); + assertEquals(1, state.getSourceSlot()); + } + + @Test + public void restoreReturnsModifiedProjectionToItsOriginalSlot() { + List lane = lane(air(), stack(Material.COAL, 8), air()); + State state = new State(); + ItemStack projected = PassiveLaneProjection.begin( + lane, null, SelectionMode.FUEL_THEN_PRESENT, state + ); + projected.setAmount(3); + + ItemStack remainder = PassiveLaneProjection.restore(lane, projected, state); + + assertNull(remainder); + assertEquals(Material.COAL, lane.get(1).getType()); + assertEquals(3, lane.get(1).getAmount()); + assertEquals(-1, state.getSourceSlot()); + } + + @Test + public void existingNativeStackDoesNotDeleteIdenticalVirtualStack() { + List lane = lane(stack(Material.COAL, 64), stack(Material.COAL, 64)); + State state = new State(); + ItemStack nativeStack = stack(Material.COAL, 64); + + ItemStack projected = PassiveLaneProjection.begin( + lane, nativeStack, SelectionMode.FUEL_THEN_PRESENT, state + ); + + assertEquals(nativeStack, projected); + assertEquals(64, lane.get(0).getAmount()); + assertEquals(64, lane.get(1).getAmount()); + assertEquals(-1, state.getSourceSlot()); + } + + @Test + public void nonFuelRemainderCanBeReturnedBeforeSelectingHiddenFuel() { + List lane = lane(air(), stack(Material.COAL, 4)); + State state = new State(); + ItemStack bucket = stack(Material.BUCKET, 1); + PassiveLaneProjection.begin( + lane, bucket, SelectionMode.FUEL_THEN_PRESENT, state + ); + + assertTrue(PassiveLaneProjection.hasPreferredFuel(lane, state)); + assertNull(PassiveLaneProjection.restore(lane, bucket, state)); + ItemStack projected = PassiveLaneProjection.takeNext( + lane, SelectionMode.FUEL_THEN_PRESENT, state + ); + + assertEquals(Material.COAL, projected.getType()); + assertEquals(Material.BUCKET, lane.get(0).getType()); + } + + @Test + public void exhaustedLaneIsNotScannedAgain() { + CountingList lane = new CountingList(lane(air(), air(), air())); + State state = new State(); + + assertNull(PassiveLaneProjection.begin( + lane, null, SelectionMode.FIRST_PRESENT, state + )); + assertTrue(state.isExhausted()); + assertTrue(lane.reads > 0); + + lane.reads = 0; + assertNull(PassiveLaneProjection.takeNext( + lane, SelectionMode.FIRST_PRESENT, state + )); + assertEquals(0, lane.reads); + } + + @Test + public void missingPreferredFuelIsScannedOnlyOnceForStableNativeRemainder() { + CountingList lane = new CountingList(lane(air(), air(), air())); + State state = new State(); + PassiveLaneProjection.begin( + lane, stack(Material.BUCKET, 1), SelectionMode.FUEL_THEN_PRESENT, state + ); + + assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); + assertTrue(lane.reads > 0); + + lane.reads = 0; + assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); + assertEquals(0, lane.reads); + } + + @Test + public void blockedFuelSwitchDoesNotRescanUntilNativeSlotEmpties() { + CountingList lane = new CountingList(lane(stack(Material.COAL, 64))); + State state = new State(); + ItemStack bucket = stack(Material.BUCKET, 1); + PassiveLaneProjection.begin( + lane, bucket, SelectionMode.FUEL_THEN_PRESENT, state + ); + assertTrue(PassiveLaneProjection.hasPreferredFuel(lane, state)); + assertEquals(1, PassiveLaneProjection.restore(lane, bucket, state).getAmount()); + state.blockPreferredFuelSwitch(); + + lane.reads = 0; + assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); + assertEquals(0, lane.reads); + + ItemStack projected = PassiveLaneProjection.takeNext( + lane, SelectionMode.FUEL_THEN_PRESENT, state + ); + assertEquals(Material.COAL, projected.getType()); + assertFalse(state.isPreferredFuelExhausted()); + } + + @Test + public void repeatedDisableEnableRoundTripDoesNotDuplicate() { + List lane = lane(stack(Material.IRON_ORE, 12), air()); + State state = new State(); + + ItemStack firstProjection = PassiveLaneProjection.begin( + lane, null, SelectionMode.FIRST_PRESENT, state + ); + assertNull(PassiveLaneProjection.restore(lane, firstProjection, state)); + ItemStack secondProjection = PassiveLaneProjection.begin( + lane, null, SelectionMode.FIRST_PRESENT, state + ); + assertNull(PassiveLaneProjection.restore(lane, secondProjection, state)); + + assertEquals(Material.IRON_ORE, lane.get(0).getType()); + assertEquals(12, lane.get(0).getAmount()); + assertEquals(Material.AIR, lane.get(1).getType()); + } + + private static List lane(ItemStack... stacks) { + return new ArrayList<>(Arrays.asList(stacks)); + } + + private static ItemStack air() { + return stack(Material.AIR, 1); + } + + private static ItemStack stack(Material material, int amount) { + return new TestItemStack(material, amount); + } + + private static final class CountingList extends AbstractList { + private final List delegate; + private int reads; + + private CountingList(List delegate) { + this.delegate = delegate; + } + + @Override + public ItemStack get(int index) { + this.reads++; + return this.delegate.get(index); + } + + @Override + public ItemStack set(int index, ItemStack element) { + return this.delegate.set(index, element); + } + + @Override + public int size() { + return this.delegate.size(); + } + } + + /** Avoids the Bukkit ItemFactory dependency in this pure unit test. */ + private static final class TestItemStack extends ItemStack { + private TestItemStack(Material material, int amount) { + super(material, amount); + } + + @Override + public boolean isSimilar(ItemStack stack) { + return stack != null && this.getType() == stack.getType(); + } + + @Override + public int getMaxStackSize() { + return 64; + } + + @Override + public ItemMeta getItemMeta() { + return null; + } + + @Override + public ItemStack clone() { + return new TestItemStack(this.getType(), this.getAmount()); + } + } +} From e0561a0b606bb8ddbec632bd7276bd9ccb6546d6 Mon Sep 17 00:00:00 2001 From: az7627 Date: Fri, 28 Aug 2026 23:31:08 +0800 Subject: [PATCH 12/13] fix(spigot): fix passive projection persistence and prevent unintended continuous item pulling - Retain FureamData NBT compound with PassiveProjected marker when virtual lanes are empty during passive mode - Restrict passive projection to at most one stack on disable, avoiding unintended continuous pulling from hidden slots - Sync only furnace burn and cook times during passive mode instead of actively altering virtual lanes - Add unit tests for passive projection NBT tag retention --- .../eat_ram/fuream/hook/FurnaceManager.java | 79 ++------- .../fuream/logic/PassiveLaneProjection.java | 69 +------- .../fuream/nbt/FurnaceRootNbtBridge.java | 17 +- .../logic/PassiveLaneProjectionTest.java | 163 ++++-------------- .../fuream/nbt/FurnaceRootNbtBridgeTest.java | 24 +++ 5 files changed, 97 insertions(+), 255 deletions(-) create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridgeTest.java diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index 3902642..ab0ffd3 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -17,7 +17,6 @@ import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; -import io.github.eat_ram.fuream.logic.FuelTable; import io.github.eat_ram.fuream.logic.PassiveLaneProjection; import io.github.eat_ram.fuream.logic.PassiveLaneProjection.SelectionMode; import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; @@ -130,6 +129,9 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { for (Map.Entry entry : ctx.extraData.entrySet()) { entry.getValue().readNbt(ctx, dataCompound); } + ctx.passiveProjected = FurnaceRootNbtBridge.hasPassiveProjectionMarker( + dataCompound + ); ctx.burnTime = legacyNbt.getInt("BurnTime", ctx.burnTime); ctx.cookTime = legacyNbt.getInt("CookTime", ctx.cookTime); ctx.cookTimeTotal = legacyNbt.getInt("CookTimeTotal", ctx.cookTimeTotal); @@ -143,9 +145,15 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { if (!loaded) { migrateVanillaLane(ctx, (Furnace) state); } else if (enabled) { - clearNativeLane((Furnace) state); - } else { + if (ctx.passiveProjected) { + resumeActive(ctx, (Furnace) state); + } else { + absorbVanillaLane(ctx, (Furnace) state); + } + } else if (!ctx.passiveProjected) { projectPassiveLane(ctx, (Furnace) state); + } else { + syncPassiveTimes(ctx, (Furnace) state); } if (hasLegacyData) { removeLegacyPdc(legacyNbt); @@ -192,7 +200,7 @@ public static void saveToNbt(@NotNull FurnaceContext ctx) { if (!ctx.passiveProjected) { absorbVanillaLane(ctx, (Furnace) state); } else { - syncPassiveLane(ctx, (Furnace) state); + syncPassiveTimes(ctx, (Furnace) state); } FurnaceRootNbtBridge.store(state, ctx); ctx.dirty = false; @@ -207,9 +215,14 @@ private static void absorbVanillaLane( @NotNull FurnaceContext ctx, @NotNull Furnace furnaceState ) { FurnaceInventory inv = furnaceState.getInventory(); + boolean hadNativeItems = !ItemCompat.isEmpty(inv.getSmelting()) || + !ItemCompat.isEmpty(inv.getFuel()) || !ItemCompat.isEmpty(inv.getResult()); inv.setSmelting(absorbLaneStack(ctx.data.inputs, inv.getSmelting())); inv.setFuel(absorbLaneStack(ctx.data.fuels, inv.getFuel())); inv.setResult(absorbLaneStack(ctx.data.outputs, inv.getResult())); + if (hadNativeItems) { + ctx.dirty = true; + } } private static @Nullable ItemStack absorbLaneStack( @@ -289,10 +302,6 @@ private static boolean isEnabled(@Nullable World world, @NotNull FurnaceType typ return config != null && config.getEnabledFurnaceTypes().contains(type); } - private static void clearNativeLane(@NotNull Furnace furnace) { - furnace.getInventory().clear(); - } - private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { closeSessions(ctx); FurnaceInventory inventory = furnace.getInventory(); @@ -341,57 +350,7 @@ private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace f ctx.dirty = true; } - private static void syncPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { - FurnaceInventory inventory = furnace.getInventory(); - ItemStack input = inventory.getSmelting(); - ItemStack fuel = inventory.getFuel(); - ItemStack output = inventory.getResult(); - boolean changed = false; - - if (ItemCompat.isEmpty(input) && !ctx.passiveInput.isExhausted()) { - ItemStack next = PassiveLaneProjection.takeNext( - ctx.data.inputs, SelectionMode.FIRST_PRESENT, ctx.passiveInput - ); - inventory.setSmelting(next); - changed = !ItemCompat.isEmpty(next); - } - - if (ItemCompat.isEmpty(fuel)) { - if (!ctx.passiveFuel.isExhausted()) { - ItemStack next = PassiveLaneProjection.takeNext( - ctx.data.fuels, SelectionMode.FUEL_THEN_PRESENT, ctx.passiveFuel - ); - inventory.setFuel(next); - changed |= !ItemCompat.isEmpty(next); - } - } else if (!FuelTable.isFuel(fuel) && - PassiveLaneProjection.hasPreferredFuel(ctx.data.fuels, ctx.passiveFuel)) { - ItemStack remainder = PassiveLaneProjection.restore( - ctx.data.fuels, fuel, ctx.passiveFuel - ); - if (ItemCompat.isEmpty(remainder)) { - inventory.setFuel(PassiveLaneProjection.takeNext( - ctx.data.fuels, SelectionMode.FUEL_THEN_PRESENT, ctx.passiveFuel - )); - changed = true; - } else { - inventory.setFuel(remainder); - changed = remainder.getAmount() < fuel.getAmount(); - ctx.passiveFuel.blockPreferredFuelSwitch(); - } - } - - if (ItemCompat.isEmpty(output) && !ctx.passiveOutput.isExhausted()) { - ItemStack next = PassiveLaneProjection.takeNext( - ctx.data.outputs, SelectionMode.FIRST_PRESENT, ctx.passiveOutput - ); - inventory.setResult(next); - changed |= !ItemCompat.isEmpty(next); - } - - if (changed) { - ctx.dirty = true; - } + private static void syncPassiveTimes(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { ctx.burnTime = FurnaceCompat.getBurnTime(furnace); ctx.cookTime = FurnaceCompat.getCookTime(furnace); ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); @@ -447,7 +406,7 @@ public static void tickAll() { } else if (!ctx.passiveProjected && state instanceof Furnace) { projectPassiveLane(ctx, (Furnace) state); } else if (ctx.passiveProjected && state instanceof Furnace) { - syncPassiveLane(ctx, (Furnace) state); + syncPassiveTimes(ctx, (Furnace) state); } // Periodic save if dirty diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java index acec53c..f047b93 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Maintains one native furnace slot as a view over a virtual item lane. */ +/** Moves one virtual stack into a native furnace slot while Fuream is disabled. */ public final class PassiveLaneProjection { public enum SelectionMode { FIRST_PRESENT, @@ -16,34 +16,20 @@ public enum SelectionMode { public static final class State { private int sourceSlot = -1; - private boolean exhausted; - private boolean preferredFuelExhausted; - private boolean preferredFuelSwitchBlocked; public int getSourceSlot() { return this.sourceSlot; } - public boolean isExhausted() { - return this.exhausted; - } - - public boolean isPreferredFuelExhausted() { - return this.preferredFuelExhausted; - } - - public void blockPreferredFuelSwitch() { - this.preferredFuelSwitchBlocked = true; - } - public void reset() { this.sourceSlot = -1; - this.exhausted = false; - this.preferredFuelExhausted = false; - this.preferredFuelSwitchBlocked = false; } } + /** + * Projects at most one stack. Callers must retain the projected state and + * must not call this again until Fuream is re-enabled for the furnace. + */ public static @Nullable ItemStack begin( @NotNull List virtual, @Nullable ItemStack nativeStack, @NotNull SelectionMode mode, @NotNull State state @@ -52,22 +38,8 @@ public void reset() { if (!ItemCompat.isEmpty(nativeStack)) { return nativeStack; } - return takeNext(virtual, mode, state); - } - - public static @Nullable ItemStack takeNext( - @NotNull List virtual, @NotNull SelectionMode mode, - @NotNull State state - ) { - state.sourceSlot = -1; - state.preferredFuelSwitchBlocked = false; - if (state.exhausted) { - return null; - } - - int selected = selectSlot(virtual, mode, state); + int selected = selectSlot(virtual, mode); if (selected < 0) { - state.exhausted = true; return null; } @@ -77,19 +49,6 @@ public void reset() { return projected; } - public static boolean hasPreferredFuel( - @NotNull List virtual, @NotNull State state - ) { - if (state.preferredFuelExhausted || state.preferredFuelSwitchBlocked) { - return false; - } - if (firstFuel(virtual) >= 0) { - return true; - } - state.preferredFuelExhausted = true; - return false; - } - /** * Returns the native stack to its source slot when possible. Any remainder * stays native so callers can retry without losing items. @@ -107,10 +66,6 @@ public static boolean hasPreferredFuel( if (source >= 0 && source < virtual.size() && ItemCompat.isEmpty(virtual.get(source))) { virtual.set(source, nativeStack.clone()); state.sourceSlot = -1; - state.exhausted = false; - if (FuelTable.isFuel(nativeStack)) { - state.preferredFuelExhausted = false; - } return null; } @@ -118,25 +73,17 @@ public static boolean hasPreferredFuel( virtual, nativeStack, Math.max(1, virtual.size()) ); state.sourceSlot = -1; - if (ItemCompat.isEmpty(remainder) || remainder.getAmount() < nativeStack.getAmount()) { - state.exhausted = false; - if (FuelTable.isFuel(nativeStack)) { - state.preferredFuelExhausted = false; - } - } return ItemCompat.isEmpty(remainder) ? null : remainder; } private static int selectSlot( - @NotNull List virtual, @NotNull SelectionMode mode, - @NotNull State state + @NotNull List virtual, @NotNull SelectionMode mode ) { - if (mode == SelectionMode.FUEL_THEN_PRESENT && !state.preferredFuelExhausted) { + if (mode == SelectionMode.FUEL_THEN_PRESENT) { int fuel = firstFuel(virtual); if (fuel >= 0) { return fuel; } - state.preferredFuelExhausted = true; } return firstPresent(virtual); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java index fa8c767..7a640b6 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java @@ -22,6 +22,7 @@ public final class FurnaceRootNbtBridge { private static final String LOAD_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_load_hook"; private static final String SAVE_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_save_hook"; private static final String STATE_MAP_KEY = "io.github.eat_ram.fuream.root_nbt_state"; + private static final String PASSIVE_PROJECTION_KEY = "PassiveProjected"; private static final ConcurrentMap, Field> TAG_MAP_FIELDS = new ConcurrentHashMap<>(); private static final ConcurrentMap, Method> TILE_ENTITY_METHODS = new ConcurrentHashMap<>(); @@ -52,6 +53,7 @@ public static boolean loadNativeData( for (Map.Entry entry : ctx.extraData.entrySet()) { entry.getValue().readNbt(ctx, dataNbt); } + ctx.passiveProjected = hasPassiveProjectionMarker(dataNbt); if (stored[1] != null && !(stored[1] instanceof Integer)) { NativeNbtCompound rootNbt = new NativeNbtCompound(stored[1]); @@ -85,12 +87,25 @@ public static void store( extra.writeNbt(ctx, dataNbt); } + boolean dataEmpty = dataNbt.keys().isEmpty(); + if (ctx.passiveProjected) { + dataNbt.setInt(PASSIVE_PROJECTION_KEY, 1); + } stateMap().put(tileEntity, new Object[] { - dataNbt.keys().isEmpty() ? null : dataNbt.raw(), + shouldKeepDataTag(dataEmpty, ctx.passiveProjected) + ? dataNbt.raw() : null, ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal }); } + public static boolean hasPassiveProjectionMarker(@Nullable NativeNbtCompound dataNbt) { + return dataNbt != null && dataNbt.getInt(PASSIVE_PROJECTION_KEY, 0) != 0; + } + + static boolean shouldKeepDataTag(boolean dataEmpty, boolean passiveProjected) { + return !dataEmpty || passiveProjected; + } + public static @Nullable NativeNbtCompound capturedRoot(@NotNull BlockState state) { Object tileEntity = getTileEntity(state); if (tileEntity == null) return null; diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java index 968c5f6..81e879b 100644 --- a/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/PassiveLaneProjectionTest.java @@ -1,6 +1,5 @@ package io.github.eat_ram.fuream.logic; -import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -13,25 +12,42 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class PassiveLaneProjectionTest { @Test - public void inputAndOutputUseFirstPresentSlot() { - List lane = lane(air(), stack(Material.IRON_ORE, 4), stack(Material.GOLD_ORE, 2)); + public void inputAndOutputProjectOnlyFirstPresentStack() { + List lane = lane( + air(), stack(Material.DIAMOND, 32), stack(Material.DIAMOND, 16) + ); State state = new State(); ItemStack projected = PassiveLaneProjection.begin( lane, null, SelectionMode.FIRST_PRESENT, state ); - assertEquals(Material.IRON_ORE, projected.getType()); - assertEquals(4, projected.getAmount()); + assertEquals(Material.DIAMOND, projected.getType()); + assertEquals(32, projected.getAmount()); assertEquals(1, state.getSourceSlot()); assertEquals(Material.AIR, lane.get(1).getType()); - assertEquals(Material.GOLD_ORE, lane.get(2).getType()); + assertEquals(Material.DIAMOND, lane.get(2).getType()); + assertEquals(16, lane.get(2).getAmount()); + } + + @Test + public void removingProjectedStackDoesNotTouchHiddenStack() { + List lane = lane( + stack(Material.DIAMOND, 32), stack(Material.DIAMOND, 16) + ); + State state = new State(); + + PassiveLaneProjection.begin(lane, null, SelectionMode.FIRST_PRESENT, state); + ItemStack nativeAfterPlayerRemoval = null; + + assertNull(nativeAfterPlayerRemoval); + assertEquals(Material.DIAMOND, lane.get(1).getType()); + assertEquals(16, lane.get(1).getAmount()); + assertEquals(0, state.getSourceSlot()); } @Test @@ -60,24 +76,6 @@ public void fuelFallsBackToFirstPresentStack() { assertEquals(Material.BUCKET, projected.getType()); assertEquals(1, state.getSourceSlot()); - assertTrue(state.isPreferredFuelExhausted()); - } - - @Test - public void emptyNativeSlotRefillsFromNextVirtualStack() { - List lane = lane(stack(Material.IRON_ORE, 3), stack(Material.GOLD_ORE, 5)); - State state = new State(); - - ItemStack first = PassiveLaneProjection.begin( - lane, null, SelectionMode.FIRST_PRESENT, state - ); - ItemStack second = PassiveLaneProjection.takeNext( - lane, SelectionMode.FIRST_PRESENT, state - ); - - assertEquals(Material.IRON_ORE, first.getType()); - assertEquals(Material.GOLD_ORE, second.getType()); - assertEquals(1, state.getSourceSlot()); } @Test @@ -98,97 +96,21 @@ public void restoreReturnsModifiedProjectionToItsOriginalSlot() { } @Test - public void existingNativeStackDoesNotDeleteIdenticalVirtualStack() { - List lane = lane(stack(Material.COAL, 64), stack(Material.COAL, 64)); + public void existingNativeStackDoesNotDeleteIdenticalHiddenStacks() { + List lane = lane(stack(Material.DIAMOND, 32), stack(Material.DIAMOND, 16)); State state = new State(); - ItemStack nativeStack = stack(Material.COAL, 64); + ItemStack nativeStack = stack(Material.DIAMOND, 32); ItemStack projected = PassiveLaneProjection.begin( - lane, nativeStack, SelectionMode.FUEL_THEN_PRESENT, state + lane, nativeStack, SelectionMode.FIRST_PRESENT, state ); assertEquals(nativeStack, projected); - assertEquals(64, lane.get(0).getAmount()); - assertEquals(64, lane.get(1).getAmount()); + assertEquals(32, lane.get(0).getAmount()); + assertEquals(16, lane.get(1).getAmount()); assertEquals(-1, state.getSourceSlot()); } - @Test - public void nonFuelRemainderCanBeReturnedBeforeSelectingHiddenFuel() { - List lane = lane(air(), stack(Material.COAL, 4)); - State state = new State(); - ItemStack bucket = stack(Material.BUCKET, 1); - PassiveLaneProjection.begin( - lane, bucket, SelectionMode.FUEL_THEN_PRESENT, state - ); - - assertTrue(PassiveLaneProjection.hasPreferredFuel(lane, state)); - assertNull(PassiveLaneProjection.restore(lane, bucket, state)); - ItemStack projected = PassiveLaneProjection.takeNext( - lane, SelectionMode.FUEL_THEN_PRESENT, state - ); - - assertEquals(Material.COAL, projected.getType()); - assertEquals(Material.BUCKET, lane.get(0).getType()); - } - - @Test - public void exhaustedLaneIsNotScannedAgain() { - CountingList lane = new CountingList(lane(air(), air(), air())); - State state = new State(); - - assertNull(PassiveLaneProjection.begin( - lane, null, SelectionMode.FIRST_PRESENT, state - )); - assertTrue(state.isExhausted()); - assertTrue(lane.reads > 0); - - lane.reads = 0; - assertNull(PassiveLaneProjection.takeNext( - lane, SelectionMode.FIRST_PRESENT, state - )); - assertEquals(0, lane.reads); - } - - @Test - public void missingPreferredFuelIsScannedOnlyOnceForStableNativeRemainder() { - CountingList lane = new CountingList(lane(air(), air(), air())); - State state = new State(); - PassiveLaneProjection.begin( - lane, stack(Material.BUCKET, 1), SelectionMode.FUEL_THEN_PRESENT, state - ); - - assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); - assertTrue(lane.reads > 0); - - lane.reads = 0; - assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); - assertEquals(0, lane.reads); - } - - @Test - public void blockedFuelSwitchDoesNotRescanUntilNativeSlotEmpties() { - CountingList lane = new CountingList(lane(stack(Material.COAL, 64))); - State state = new State(); - ItemStack bucket = stack(Material.BUCKET, 1); - PassiveLaneProjection.begin( - lane, bucket, SelectionMode.FUEL_THEN_PRESENT, state - ); - assertTrue(PassiveLaneProjection.hasPreferredFuel(lane, state)); - assertEquals(1, PassiveLaneProjection.restore(lane, bucket, state).getAmount()); - state.blockPreferredFuelSwitch(); - - lane.reads = 0; - assertFalse(PassiveLaneProjection.hasPreferredFuel(lane, state)); - assertEquals(0, lane.reads); - - ItemStack projected = PassiveLaneProjection.takeNext( - lane, SelectionMode.FUEL_THEN_PRESENT, state - ); - assertEquals(Material.COAL, projected.getType()); - assertFalse(state.isPreferredFuelExhausted()); - } - @Test public void repeatedDisableEnableRoundTripDoesNotDuplicate() { List lane = lane(stack(Material.IRON_ORE, 12), air()); @@ -220,31 +142,6 @@ private static ItemStack stack(Material material, int amount) { return new TestItemStack(material, amount); } - private static final class CountingList extends AbstractList { - private final List delegate; - private int reads; - - private CountingList(List delegate) { - this.delegate = delegate; - } - - @Override - public ItemStack get(int index) { - this.reads++; - return this.delegate.get(index); - } - - @Override - public ItemStack set(int index, ItemStack element) { - return this.delegate.set(index, element); - } - - @Override - public int size() { - return this.delegate.size(); - } - } - /** Avoids the Bukkit ItemFactory dependency in this pure unit test. */ private static final class TestItemStack extends ItemStack { private TestItemStack(Material material, int amount) { diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridgeTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridgeTest.java new file mode 100644 index 0000000..d6843f8 --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridgeTest.java @@ -0,0 +1,24 @@ +package io.github.eat_ram.fuream.nbt; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FurnaceRootNbtBridgeTest { + @Test + public void passiveProjectionKeepsEmptyFureamDataTag() { + assertTrue(FurnaceRootNbtBridge.shouldKeepDataTag(true, true)); + } + + @Test + public void populatedFureamDataTagIsAlwaysKept() { + assertTrue(FurnaceRootNbtBridge.shouldKeepDataTag(false, false)); + assertTrue(FurnaceRootNbtBridge.shouldKeepDataTag(false, true)); + } + + @Test + public void emptyActiveFureamDataTagMayBeRemoved() { + assertFalse(FurnaceRootNbtBridge.shouldKeepDataTag(true, false)); + } +} From 8e0bba3b5a7bfb4c4f93be189dd9ce2599f408f9 Mon Sep 17 00:00:00 2001 From: az7627 Date: Sat, 29 Aug 2026 20:03:59 +0800 Subject: [PATCH 13/13] refactor(spigot): performance, compatibility and maintainability overhaul - Replace brute-force hopper scanning with native slot projection and VanillaFurnaceGuardListener - Add FurnaceStateTransaction for atomic snapshot-based timing and inventory commits - Add dynamic RecipeIndex and RecipeIndexListener for O(1) recipe resolution on hot tick paths - Introduce NbtSelfTest and FurnaceNbtRuntime for fail-fast NBT verification and robust codec handling - Refactor FureamWorldConfigImpl with table-driven parsing, slot bounds checks, and corruption protection - Optimize GUI differential slot rendering and adopt standard Java collections - Align wet sponge bucket conversion and special furnace fuel durations with Fabric semantics --- spigot/build.gradle.kts | 94 +- .../github/eat_ram/fuream/CollectionUtil.java | 132 --- .../io/github/eat_ram/fuream/FureamMain.java | 103 +- .../eat_ram/fuream/FureamWorldConfigImpl.java | 1016 +++-------------- .../eat_ram/fuream/ResolvedFurnaceConfig.java | 40 + .../eat_ram/fuream/compat/BlockCompat.java | 95 +- .../fuream/compat/ComparatorCompat.java | 179 +-- .../eat_ram/fuream/compat/FurnaceCompat.java | 102 +- .../fuream/compat/FurnaceEventCompat.java | 73 +- .../fuream/compat/FurnaceEventDispatcher.java | 33 + .../compat/FurnaceStateTransaction.java | 92 ++ .../eat_ram/fuream/compat/ItemCompat.java | 11 +- .../eat_ram/fuream/compat/RecipeCompat.java | 92 +- .../eat_ram/fuream/compat/RecipeIndex.java | 202 ++++ .../eat_ram/fuream/compat/ServerVersion.java | 3 - .../eat_ram/fuream/compat/VersionAdapter.java | 8 + .../fuream/compat/VersionAdapters.java | 18 +- .../fuream/data/FureamFurnaceData.java | 39 +- .../data/FurnaceFureamDataRegistry.java | 5 +- .../fuream/hook/BlockBreakListener.java | 49 +- .../fuream/hook/BlockExplodeListener.java | 5 +- .../eat_ram/fuream/hook/ChunkListener.java | 23 +- .../fuream/hook/ComparatorListener.java | 2 +- .../eat_ram/fuream/hook/FurnaceManager.java | 506 +++++--- .../fuream/hook/FurnaceOpenListener.java | 31 +- .../eat_ram/fuream/hook/GuiListener.java | 28 +- .../fuream/hook/RecipeIndexListener.java | 34 + .../hook/VanillaFurnaceGuardListener.java | 25 + .../eat_ram/fuream/logic/FuelTable.java | 59 +- .../fuream/logic/FureamFurnaceEngine.java | 99 +- .../fuream/logic/FureamFurnaceLogic.java | 3 +- .../fuream/logic/NativeLaneProjection.java | 121 ++ .../fuream/logic/PassiveLaneProjection.java | 8 +- .../fuream/nbt/FurnaceNbtInstrumentation.java | 75 +- .../eat_ram/fuream/nbt/FurnaceNbtRuntime.java | 138 +++ .../fuream/nbt/FurnaceRootNbtBridge.java | 247 ++-- .../eat_ram/fuream/nbt/NativeItemNbt.java | 59 +- .../eat_ram/fuream/nbt/NativeNbtCompound.java | 16 +- .../eat_ram/fuream/nbt/NativeNbtList.java | 14 +- .../eat_ram/fuream/nbt/NbtCodecException.java | 12 + .../eat_ram/fuream/nbt/NbtSelfTest.java | 49 + .../fuream/screen/FureamScreenHandler.java | 167 ++- .../fuream/screen/FureamScreenInventory.java | 193 ++-- .../eat_ram/fuream/util/FurnacePos.java | 21 +- .../github/eat_ram/fuream/util/NbtUtil.java | 24 +- spigot/src/main/resources/plugin.yml | 6 +- .../eat_ram/fuream/CollectionUtilTest.java | 47 - .../io/github/eat_ram/fuream/ConfigTest.java | 22 + .../fuream/logic/FureamFurnaceEngineTest.java | 27 +- .../logic/NativeLaneProjectionTest.java | 97 ++ spigot/src/variants/components/plugin.yml | 6 +- spigot/src/variants/flattening/plugin.yml | 6 +- spigot/src/variants/legacy/plugin.yml | 6 +- 53 files changed, 2539 insertions(+), 2023 deletions(-) delete mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/ResolvedFurnaceConfig.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventDispatcher.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceStateTransaction.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeIndex.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/RecipeIndexListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/hook/VanillaFurnaceGuardListener.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/logic/NativeLaneProjection.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtRuntime.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtCodecException.java create mode 100644 spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtSelfTest.java delete mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java create mode 100644 spigot/src/test/java/io/github/eat_ram/fuream/logic/NativeLaneProjectionTest.java diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index ce1334b..9fd076d 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -1,5 +1,10 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import java.nio.charset.StandardCharsets +import java.util.zip.ZipFile + plugins { `java` + id("com.github.johnrengelman.shadow") version "8.1.1" } group = "io.github.eat-ram.fuream-spigot" @@ -32,6 +37,7 @@ dependencies { implementation("net.bytebuddy:byte-buddy-agent:1.17.7") implementation("net.java.dev.jna:jna:5.17.0") implementation("net.java.dev.jna:jna-platform:5.17.0") + implementation("com.google.code.gson:gson:2.10.1") compileOnly("org.spigotmc:spigot-api:1.20.1-R0.1-SNAPSHOT") compileOnly("org.jetbrains:annotations:26.0.2") compileOnly(project(":api")) { @@ -48,6 +54,12 @@ tasks.test { useJUnitPlatform() } +tasks.processResources { + filesMatching("plugin.yml") { + expand("version" to project.version) + } +} + val targetJavaVersion = 8 java { val javaVersion = JavaVersion.toVersion(targetJavaVersion) @@ -60,6 +72,7 @@ java { tasks.withType().configureEach { options.encoding = "UTF-8" + options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked")) if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible) { options.release.set(targetJavaVersion) @@ -69,25 +82,39 @@ tasks.withType().configureEach { tasks.jar { enabled = false } +tasks.shadowJar { + enabled = false +} fun registerVariant(taskName: String, fileName: String, resourceDir: String) = - tasks.register(taskName) { + tasks.register(taskName) { group = "build" description = "Builds $fileName" dependsOn(tasks.classes, project(":api").tasks.jar) archiveFileName.set(fileName) + archiveClassifier.set("") duplicatesStrategy = DuplicatesStrategy.EXCLUDE + configurations = listOf(project.configurations.runtimeClasspath.get()) from(sourceSets.main.get().output) { exclude("plugin.yml") } - from(resourceDir) - from(configurations.runtimeClasspath.get().map { dependency -> - if (dependency.isDirectory) dependency else zipTree(dependency) - }) { - exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class") + from(resourceDir) { + filesMatching("plugin.yml") { + expand("version" to project.version) + } } + exclude( + "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class", + "META-INF/versions/**" + ) from(rootProject.file("LICENSE")) { into("META-INF") } from(zipTree(project(":api").tasks.jar.get().archiveFile)) { include("**/*.class") } + mergeServiceFiles() + relocate("net.bytebuddy", "io.github.eat_ram.fuream.libs.bytebuddy") + relocate("com.sun.jna", "io.github.eat_ram.fuream.libs.jna") + relocate("com.google.gson", "io.github.eat_ram.fuream.libs.gson") + isPreserveFileTimestamps = false + isReproducibleFileOrder = true } val legacyJar = registerVariant( @@ -103,3 +130,58 @@ val componentsJar = registerVariant( tasks.assemble { dependsOn(legacyJar, flatteningJar, componentsJar) } + +val verifyVariantJars = tasks.register("verifyVariantJars") { + group = "verification" + description = "Checks variant metadata, Java 8 bytecode, shading, and guarded API references" + dependsOn(legacyJar, flatteningJar, componentsJar) + doLast { + val variants = listOf( + legacyJar.get().archiveFile.get().asFile to null, + flatteningJar.get().archiveFile.get().asFile to "1.13", + componentsJar.get().archiveFile.get().asFile to "1.20.5" + ) + val forbiddenEntries = listOf("net/bytebuddy/", "com/google/gson/", "com/sun/jna/") + for ((jarFile, apiVersion) in variants) { + ZipFile(jarFile).use { jar -> + val pluginEntry = jar.getEntry("plugin.yml") + ?: error("${jarFile.name} has no plugin.yml") + val pluginYml = jar.getInputStream(pluginEntry).bufferedReader(StandardCharsets.UTF_8).use { it.readText() } + check(pluginYml.contains("version: $version")) { + "${jarFile.name} does not contain project version $version" + } + if (apiVersion == null) { + check(!pluginYml.contains("api-version:")) { + "Legacy plugin.yml must not declare api-version" + } + } else { + check(pluginYml.contains("api-version: '$apiVersion'")) { + "${jarFile.name} has the wrong api-version" + } + } + + val entries = jar.entries().asSequence().toList() + check(entries.none { entry -> forbiddenEntries.any(entry.name::startsWith) }) { + "${jarFile.name} contains an unrelocated runtime dependency" + } + for (entry in entries) { + if (!entry.name.endsWith(".class")) continue + val bytes = jar.getInputStream(entry).use { it.readBytes() } + val major = ((bytes[6].toInt() and 0xff) shl 8) or (bytes[7].toInt() and 0xff) + check(major <= 52) { "${entry.name} in ${jarFile.name} is not Java 8 bytecode" } + if (jarFile == variants[0].first && + entry.name != "io/github/eat_ram/fuream/hook/BlockExplodeListener.class") { + val constants = String(bytes, StandardCharsets.ISO_8859_1) + check(!constants.contains("org/bukkit/event/block/BlockExplodeEvent")) { + "${entry.name} directly links BlockExplodeEvent in the legacy variant" + } + } + } + } + } + } +} + +tasks.check { + dependsOn(verifyVariantJars) +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java deleted file mode 100644 index 809cb63..0000000 --- a/spigot/src/main/java/io/github/eat_ram/fuream/CollectionUtil.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.github.eat_ram.fuream; - -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; -import java.util.NoSuchElementException; - -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.Range; - -public abstract class CollectionUtil { - @Contract(value = "_, _ -> new", pure = true) - @SuppressWarnings("unchecked") - public static ArrayList newDefaultedArrayList( - E defaultValue, - @Range(from = 0, to = Integer.MAX_VALUE) int initialSize - ) { - ArrayList al = new ArrayList() { - @SuppressWarnings("unchecked") - private E makeDefault() { - if (defaultValue instanceof org.bukkit.inventory.ItemStack) { - return (E) ((org.bukkit.inventory.ItemStack) defaultValue).clone(); - } - return defaultValue; - } - - @Override - @Contract(pure = true) - public E get(int index) { - return index < this.size() ? super.get(index) : this.makeDefault(); - } - - @Override - public E set(int index, E element) { - int size = this.size(); - if (index < size) { - return super.set(index, element); - } - this.ensureCapacity(index + 1); - for (; size < index; ++size) { - this.add(this.makeDefault()); - } - this.add(element); - return this.makeDefault(); - } - }; - if (initialSize > 0) { - al.set(initialSize - 1, defaultValue instanceof org.bukkit.inventory.ItemStack ? (E) ((org.bukkit.inventory.ItemStack) defaultValue).clone() : defaultValue); - } - return al; - } - - @Contract(value = "_, _, _ -> new", pure = true) - public static ListIterator - listIterator(List list, int size, int index) { - return new ListIterator() { - public final boolean isListIterator = index >= 0; - private int cursor = index < 0 ? 0 : index; - - @Contract(pure = true) - public boolean hasNext() { - return this.cursor != size; - } - - public E next() { - try { - int i = this.cursor; - E next = list.get(i); - this.cursor = i + 1; - return next; - } catch (IndexOutOfBoundsException e) { - throw new NoSuchElementException(); - } - } - - @Contract("-> fail") - public void remove() { - throw new UnsupportedOperationException(); - } - - public boolean hasPrevious() { - if (!this.isListIterator) { - throw new UnsupportedOperationException(); - } - return this.cursor != 0; - } - - public E previous() { - if (!this.isListIterator) { - throw new UnsupportedOperationException(); - } - try { - int i = this.cursor - 1; - E previous = list.get(i); - this.cursor = i; - return previous; - } catch (IndexOutOfBoundsException e) { - throw new NoSuchElementException(); - } - } - - public int nextIndex() { - if (!this.isListIterator) { - throw new UnsupportedOperationException(); - } - return this.cursor; - } - - public int previousIndex() { - if (!this.isListIterator) { - throw new UnsupportedOperationException(); - } - return this.cursor - 1; - } - - @Contract("_ -> fail") - public void set(E e) { - throw new UnsupportedOperationException(); - } - - @Contract("_ -> fail") - public void add(E e) { - throw new UnsupportedOperationException(); - } - }; - } - - @Contract("-> fail") - private CollectionUtil() { - throw new UnsupportedOperationException(); - } -} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java index 7b8bb2d..db7b7e4 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamMain.java @@ -3,6 +3,9 @@ import java.io.File; import java.io.FileInputStream; import java.util.Locale; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.WeakHashMap; @@ -18,9 +21,15 @@ import io.github.eat_ram.fuream.hook.FurnaceOpenListener; import io.github.eat_ram.fuream.hook.GuiListener; import io.github.eat_ram.fuream.hook.HopperListener; +import io.github.eat_ram.fuream.hook.VanillaFurnaceGuardListener; +import io.github.eat_ram.fuream.hook.RecipeIndexListener; +import io.github.eat_ram.fuream.compat.RecipeCompat; +import io.github.eat_ram.fuream.compat.FurnaceEventCompat; import io.github.eat_ram.fuream.compat.ServerVersion; import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.nbt.FurnaceNbtInstrumentation; +import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; +import io.github.eat_ram.fuream.nbt.NbtSelfTest; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Material; @@ -29,21 +38,23 @@ import org.bukkit.block.BlockState; import org.bukkit.block.Furnace; import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldSaveEvent; +import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class FureamMain extends JavaPlugin implements Listener, CommandExecutor { +public class FureamMain extends JavaPlugin implements Listener, TabExecutor { public static final String DATA_KEY = "FureamData"; - public static final Map<@NotNull World, @NotNull FureamWorldConfig> WORLD_CONFIGS = new WeakHashMap<>(); - public static final Map<@NotNull World, @NotNull File> WORLD_CONFIG_SOURCES = new WeakHashMap<>(); + private static final Map<@NotNull World, @NotNull FureamWorldConfig> WORLD_CONFIGS = new WeakHashMap<>(); + private static final Map<@NotNull World, @NotNull File> WORLD_CONFIG_SOURCES = new WeakHashMap<>(); + private static final Map WORLD_CONFIG_ERRORS = new HashMap<>(); private static FureamMain INSTANCE; private boolean rootNbtReady; @@ -72,12 +83,22 @@ public void onEnable() { this.getServer().getPluginManager().disablePlugin(this); return; } + try { + NbtSelfTest.run(); + } catch (Throwable failure) { + this.rootNbtReady = false; + this.getLogger().log(Level.SEVERE, "Native NBT self-test failed; no furnaces were adopted", failure); + this.getServer().getPluginManager().disablePlugin(this); + return; + } // Register listeners this.getServer().getPluginManager().registerEvents(this, this); this.getServer().getPluginManager().registerEvents(new FurnaceOpenListener(), this); this.getServer().getPluginManager().registerEvents(new GuiListener(), this); this.getServer().getPluginManager().registerEvents(new HopperListener(), this); + this.getServer().getPluginManager().registerEvents(new VanillaFurnaceGuardListener(), this); + this.getServer().getPluginManager().registerEvents(new RecipeIndexListener(), this); this.getServer().getPluginManager().registerEvents(new ComparatorListener(), this); this.getServer().getPluginManager().registerEvents(new BlockBreakListener(), this); try { @@ -88,6 +109,9 @@ public void onEnable() { } this.getServer().getPluginManager().registerEvents(new ChunkListener(), this); + FurnaceEventCompat.initialize(); + RecipeCompat.rebuild(); + // Load configs and scan loaded chunks for existing furnaces for (World world : Bukkit.getWorlds()) { loadWorldConfig(world); @@ -106,6 +130,7 @@ public void onEnable() { // Register command if (this.getCommand("fuream") != null) { this.getCommand("fuream").setExecutor(this); + this.getCommand("fuream").setTabCompleter(this); } this.getLogger().info("Fuream (Spigot) enabled successfully!"); @@ -124,7 +149,15 @@ public void onWorldLoad(WorldLoadEvent event) { @EventHandler public void onWorldSave(WorldSaveEvent event) { - FurnaceManager.flushAll(); + FurnaceManager.flushWorld(event.getWorld()); + } + + @EventHandler(priority = org.bukkit.event.EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldUnload(WorldUnloadEvent event) { + FurnaceManager.unloadWorld(event.getWorld()); + WORLD_CONFIGS.remove(event.getWorld()); + WORLD_CONFIG_SOURCES.remove(event.getWorld()); + WORLD_CONFIG_ERRORS.remove(event.getWorld().getUID()); } public static void loadWorldConfig(@NotNull World world) { @@ -141,7 +174,8 @@ public static void loadWorldConfig(@NotNull World world) { try { FureamWorldConfigImpl.writeWorldConfig(config, targetFile); } catch (Exception e) { - Bukkit.getLogger().warning("Failed to create default fuream.json at " + targetFile + ": " + e.getMessage()); + recordConfigFailure(world, targetFile, e); + return; } } @@ -149,10 +183,12 @@ public static void loadWorldConfig(@NotNull World world) { try { FureamWorldConfigImpl.readWorldConfig(config, targetFile); } catch (Exception e) { - Bukkit.getLogger().warning("Failed to load fuream.json for world " + world.getName() + ": " + e.getMessage()); + recordConfigFailure(world, targetFile, e); + return; } } WORLD_CONFIGS.put(world, config); + WORLD_CONFIG_ERRORS.remove(world.getUID()); if (targetFile == null) { WORLD_CONFIG_SOURCES.remove(world); } else { @@ -160,6 +196,12 @@ public static void loadWorldConfig(@NotNull World world) { } } + private static void recordConfigFailure(World world, File source, Exception failure) { + String message = "Failed to load " + source + " for world " + world.getName() + ": " + failure.getMessage(); + WORLD_CONFIG_ERRORS.put(world.getUID(), message); + Bukkit.getLogger().warning(message + "; retaining the last-known-good configuration"); + } + private static @Nullable File findExistingConfig(@NotNull File worldFolder) { File direct = new File(worldFolder, "fuream.json"); if (direct.isFile()) return direct; @@ -205,13 +247,12 @@ public static void loadWorldConfig(@NotNull World world) { } if (obj instanceof Block) { Material mat = ((Block) obj).getType(); - if (mat == Material.FURNACE || "BURNING_FURNACE".equals(mat.name())) return FurnaceType.FURNACE; - try { - if (mat == Material.SMOKER) return FurnaceType.SMOKER; - if (mat == Material.BLAST_FURNACE) return FurnaceType.BLAST_FURNACE; - } catch (NoSuchFieldError e) { - // 1.13 and earlier do not define these materials. + String materialName = mat.name(); + if ("FURNACE".equals(materialName) || "BURNING_FURNACE".equals(materialName)) { + return FurnaceType.FURNACE; } + if ("SMOKER".equals(materialName)) return FurnaceType.SMOKER; + if ("BLAST_FURNACE".equals(materialName)) return FurnaceType.BLAST_FURNACE; return null; } if (obj instanceof BlockState) { @@ -230,16 +271,35 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command for (World w : Bukkit.getWorlds()) { loadWorldConfig(w); } + RecipeCompat.rebuild(); FurnaceManager.reconcileConfiguration(); - sender.sendMessage("§a[Fuream] World configurations reloaded!"); + boolean failed = false; + for (World world : Bukkit.getWorlds()) { + String error = WORLD_CONFIG_ERRORS.get(world.getUID()); + if (error != null) { + failed = true; + sender.sendMessage("§c[Fuream] " + error); + } + } + sender.sendMessage(failed + ? "§e[Fuream] Reload completed with errors; previous configs remain active." + : "§a[Fuream] World configurations reloaded!"); return true; } if (args.length > 0 && "status".equalsIgnoreCase(args[0])) { + if (!sender.hasPermission("fuream.status")) { + sender.sendMessage("§cYou do not have permission to use this command."); + return true; + } sender.sendMessage("§e=== Fuream Status ==="); sender.sendMessage("§6Version adapter: §f" + VersionAdapters.current().id() + " §7(" + ServerVersion.CURRENT + ")"); sender.sendMessage("§6Root NBT hook: §f" + (this.rootNbtReady ? "ready" : "failed")); + long nbtFailures = FurnaceRootNbtBridge.failureCount(); + sender.sendMessage("§6Root NBT runtime failures: §f" + nbtFailures); + if (nbtFailures > 0) sender.sendMessage("§6Last NBT failure: §f" + FurnaceRootNbtBridge.lastFailure()); + sender.sendMessage("§6Indexed cooking recipes: §f" + RecipeCompat.indexedRecipeCount()); for (World w : Bukkit.getWorlds()) { FureamWorldConfig cfg = getWorldConfig(w); if (cfg != null) { @@ -248,6 +308,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command cfg.getEnabledFurnaceTypes() + ", Source: " + (source == null ? "in-memory defaults" : source.getPath()) + ")"); } + String configError = WORLD_CONFIG_ERRORS.get(w.getUID()); + if (configError != null) sender.sendMessage("§cConfig error: §f" + configError); } return true; } @@ -256,4 +318,17 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command sender.sendMessage("§6/fuream status §7- View status of all worlds"); return true; } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, @NotNull Command command, + @NotNull String alias, @NotNull String[] args + ) { + if (args.length != 1) return java.util.Collections.emptyList(); + String prefix = args[0].toLowerCase(Locale.ROOT); + List result = new ArrayList<>(); + if (sender.hasPermission("fuream.reload") && "reload".startsWith(prefix)) result.add("reload"); + if (sender.hasPermission("fuream.status") && "status".startsWith(prefix)) result.add("status"); + return result; + } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java index e7dac20..2571418 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/FureamWorldConfigImpl.java @@ -1,7 +1,17 @@ package io.github.eat_ram.fuream; import java.io.FileNotFoundException; -import java.io.FileReader; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.EnumMap; import java.util.EnumSet; import java.util.Map; @@ -78,21 +88,43 @@ public static void writeWorldConfig( putMap(root, "gui_next_page_item_tooltip", config.getGuiNextPageItemTooltip()); com.google.gson.Gson gson = new com.google.gson.GsonBuilder().setPrettyPrinting().create(); - try (java.io.FileWriter fw = new java.io.FileWriter(configFile)) { - gson.toJson(root, fw); + Path target = configFile.toPath().toAbsolutePath(); + Path parent = target.getParent(); + if (parent == null) throw new java.io.IOException("Config has no parent directory: " + target); + Path temporary = Files.createTempFile(parent, configFile.getName(), ".tmp"); + try { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(temporary.toFile()), StandardCharsets.UTF_8 + ))) { + gson.toJson(root, writer); + } + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException ignored) { + Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); } } - private static JsonObject readExistingObject(File file) { + private static JsonObject readExistingObject(File file) throws java.io.IOException { if (!file.isFile()) return new JsonObject(); - try (FileReader reader = new FileReader(file)) { + try (BufferedReader reader = utf8Reader(file)) { JsonElement element = new JsonParser().parse(reader); - return element.isJsonObject() ? element.getAsJsonObject() : new JsonObject(); - } catch (Exception ignored) { - return new JsonObject(); + if (!element.isJsonObject()) { + throw new JsonSyntaxException("Config root must be a JSON object: " + file); + } + return element.getAsJsonObject(); } } + private static BufferedReader utf8Reader(File file) throws FileNotFoundException { + return new BufferedReader(new InputStreamReader( + new FileInputStream(file), StandardCharsets.UTF_8 + )); + } + private static void putMap(JsonObject root, String key, Map values) { JsonObject object = new JsonObject(); for (Map.Entry entry : values.entrySet()) { @@ -124,888 +156,120 @@ private static void readStringMap( } } - @SuppressWarnings("deprecation") public static void readWorldConfig( @NotNull FureamWorldConfig config, @NotNull File configFile ) throws FileNotFoundException, JsonIOException, JsonSyntaxException { - final JsonElement JSON; - try (final FileReader FR = new FileReader(configFile)) { - JSON = new JsonParser().parse(FR); + final JsonElement json; + try (BufferedReader reader = utf8Reader(configFile)) { + json = new JsonParser().parse(reader); } catch (FileNotFoundException e) { throw e; } catch (java.io.IOException e) { throw new JsonIOException(e); } - if (JSON.isJsonObject()) { - JsonObject obj = JSON.getAsJsonObject(); - readStringMap(obj, "gui_title", config.getGuiTitle()); - if (obj.has("input_slot_count")) { - JsonElement ele = obj.get("input_slot_count"); - if (ele.isJsonObject()) { - EnumMap inputSlotCount = - config.getInputSlotCount(); - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - int count = value.getAsInt(); - if (count > 0) { - inputSlotCount.put( - FurnaceType.valueOf(i.getKey()), count - ); - } - } catch (IllegalArgumentException ignored) {} - } - } - } - } - if (obj.has("fuel_slot_count")) { - JsonElement ele = obj.get("fuel_slot_count"); - if (ele.isJsonObject()) { - EnumMap fuelSlotCount = - config.getFuelSlotCount(); - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - int count = value.getAsInt(); - if (count > 0) { - fuelSlotCount.put( - FurnaceType.valueOf(i.getKey()), count - ); - } - } catch (IllegalArgumentException ignored) {} - } - } - } - } - if (obj.has("output_slot_count")) { - JsonElement ele = obj.get("output_slot_count"); - if (ele.isJsonObject()) { - EnumMap outputSlotCount = - config.getOutputSlotCount(); - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - int count = value.getAsInt(); - if (count > 0) { - outputSlotCount.put( - FurnaceType.valueOf(i.getKey()), count - ); - } - } catch (IllegalArgumentException ignored) {} - } - } - } - } - if (obj.has("prevents_hopper_insert_non_smeltable")) { - JsonElement ele = - obj.get("prevents_hopper_insert_non_smeltable"); - if (ele.isJsonArray()) { - EnumSet preventsHopperInsertNonSmeltable = - config.getPreventsHopperInsertNonSmeltable(); - for (JsonElement i : ele.getAsJsonArray()) { - if (i.isJsonPrimitive()) { - try { - preventsHopperInsertNonSmeltable - .add(FurnaceType.valueOf(i.getAsString())); - } catch (IllegalArgumentException ignored) {} - } - } - } - } - if (obj.has("enabled_furnace_types")) { - JsonElement ele = obj.get("enabled_furnace_types"); - if (ele.isJsonArray()) { - EnumSet enabledFurnaceTypes = - config.getEnabledFurnaceTypes(); - for (JsonElement i : ele.getAsJsonArray()) { - if (i.isJsonPrimitive()) { - try { - enabledFurnaceTypes - .add(FurnaceType.valueOf(i.getAsString())); - } catch (IllegalArgumentException ignored) {} - } - } - } - } - // Below are generated by a Python script - if (obj.has("gui_border_item_id")) { - JsonElement ele = obj.get("gui_border_item_id"); - EnumMap ids = - config.getGuiBorderItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_fuel_left_item_id")) { - JsonElement ele = obj.get("gui_fuel_left_item_id"); - EnumMap ids = - config.getGuiFuelLeftItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_fuel_used_item_id")) { - JsonElement ele = obj.get("gui_fuel_used_item_id"); - EnumMap ids = - config.getGuiFuelUsedItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_progress_done_item_id")) { - JsonElement ele = obj.get("gui_progress_done_item_id"); - EnumMap ids = - config.getGuiProgressDoneItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_progress_remaining_item_id")) { - JsonElement ele = obj.get("gui_progress_remaining_item_id"); - EnumMap ids = - config.getGuiProgressRemainingItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_recipe_item_id")) { - JsonElement ele = obj.get("gui_prev_recipe_item_id"); - EnumMap ids = - config.getGuiPrevRecipeItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_recipe_item_id")) { - JsonElement ele = obj.get("gui_next_recipe_item_id"); - EnumMap ids = - config.getGuiNextRecipeItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_xp_indicator_item_id")) { - JsonElement ele = obj.get("gui_xp_indicator_item_id"); - EnumMap ids = - config.getGuiXpIndicatorItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_functional_area_item_id")) { - JsonElement ele = obj.get("gui_next_functional_area_item_id"); - EnumMap ids = - config.getGuiNextFunctionalAreaItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_page_item_id")) { - JsonElement ele = obj.get("gui_prev_page_item_id"); - EnumMap ids = - config.getGuiPrevPageItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_page_item_id")) { - JsonElement ele = obj.get("gui_next_page_item_id"); - EnumMap ids = - config.getGuiNextPageItemId(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_border_item_title")) { - JsonElement ele = obj.get("gui_border_item_title"); - EnumMap ids = - config.getGuiBorderItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_fuel_item_title")) { - JsonElement ele = obj.get("gui_fuel_item_title"); - EnumMap ids = - config.getGuiFuelItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_fuel_item_tooltip")) { - JsonElement ele = obj.get("gui_fuel_item_tooltip"); - EnumMap ids = - config.getGuiFuelItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_progress_item_title")) { - JsonElement ele = obj.get("gui_progress_item_title"); - EnumMap ids = - config.getGuiProgressItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_progress_item_tooltip")) { - JsonElement ele = obj.get("gui_progress_item_tooltip"); - EnumMap ids = - config.getGuiProgressItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_recipe_item_title")) { - JsonElement ele = obj.get("gui_prev_recipe_item_title"); - EnumMap ids = - config.getGuiPrevRecipeItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_recipe_empty_item_tooltip")) { - JsonElement ele = obj.get("gui_prev_recipe_empty_item_tooltip"); - EnumMap ids = - config.getGuiPrevRecipeEmptyItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_recipe_arbitrary_item_tooltip")) { - JsonElement ele = obj.get("gui_prev_recipe_arbitrary_item_tooltip"); - EnumMap ids = - config.getGuiPrevRecipeArbitraryItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_recipe_item_tooltip")) { - JsonElement ele = obj.get("gui_prev_recipe_item_tooltip"); - EnumMap ids = - config.getGuiPrevRecipeItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_recipe_item_title")) { - JsonElement ele = obj.get("gui_next_recipe_item_title"); - EnumMap ids = - config.getGuiNextRecipeItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_recipe_empty_item_tooltip")) { - JsonElement ele = obj.get("gui_next_recipe_empty_item_tooltip"); - EnumMap ids = - config.getGuiNextRecipeEmptyItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_recipe_arbitrary_item_tooltip")) { - JsonElement ele = obj.get("gui_next_recipe_arbitrary_item_tooltip"); - EnumMap ids = - config.getGuiNextRecipeArbitraryItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_recipe_item_tooltip")) { - JsonElement ele = obj.get("gui_next_recipe_item_tooltip"); - EnumMap ids = - config.getGuiNextRecipeItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_xp_indicator_item_title")) { - JsonElement ele = obj.get("gui_xp_indicator_item_title"); - EnumMap ids = - config.getGuiXpIndicatorItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_xp_indicator_gainable_item_title")) { - JsonElement ele = obj.get("gui_xp_indicator_gainable_item_title"); - EnumMap ids = - config.getGuiXpIndicatorGainableItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_xp_indicator_item_tooltip")) { - JsonElement ele = obj.get("gui_xp_indicator_item_tooltip"); - EnumMap ids = - config.getGuiXpIndicatorItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_functional_area_item_title")) { - JsonElement ele = - obj.get("gui_next_functional_area_item_title"); - EnumMap ids = - config.getGuiNextFunctionalAreaItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_page_item_title")) { - JsonElement ele = obj.get("gui_prev_page_item_title"); - EnumMap ids = - config.getGuiPrevPageItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_prev_page_item_tooltip")) { - JsonElement ele = obj.get("gui_prev_page_item_tooltip"); - EnumMap ids = - config.getGuiPrevPageItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } - } - if (obj.has("gui_next_page_item_title")) { - JsonElement ele = obj.get("gui_next_page_item_title"); - EnumMap ids = - config.getGuiNextPageItemTitle(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } + if (!json.isJsonObject()) throw new JsonSyntaxException("Config root must be a JSON object"); + JsonObject root = json.getAsJsonObject(); + + readIntMap(root, "input_slot_count", config.getInputSlotCount()); + readIntMap(root, "fuel_slot_count", config.getFuelSlotCount()); + readIntMap(root, "output_slot_count", config.getOutputSlotCount()); + readTypeSet(root, "enabled_furnace_types", config.getEnabledFurnaceTypes()); + readTypeSet(root, "prevents_hopper_insert_non_smeltable", config.getPreventsHopperInsertNonSmeltable()); + + readStringMaps(root, + field("gui_border_item_id", config.getGuiBorderItemId()), + field("gui_fuel_left_item_id", config.getGuiFuelLeftItemId()), + field("gui_fuel_used_item_id", config.getGuiFuelUsedItemId()), + field("gui_progress_done_item_id", config.getGuiProgressDoneItemId()), + field("gui_progress_remaining_item_id", config.getGuiProgressRemainingItemId()), + field("gui_prev_recipe_item_id", config.getGuiPrevRecipeItemId()), + field("gui_next_recipe_item_id", config.getGuiNextRecipeItemId()), + field("gui_xp_indicator_item_id", config.getGuiXpIndicatorItemId()), + field("gui_next_functional_area_item_id", config.getGuiNextFunctionalAreaItemId()), + field("gui_prev_page_item_id", config.getGuiPrevPageItemId()), + field("gui_next_page_item_id", config.getGuiNextPageItemId()), + field("gui_title", config.getGuiTitle()), + field("gui_border_item_title", config.getGuiBorderItemTitle()), + field("gui_fuel_item_title", config.getGuiFuelItemTitle()), + field("gui_fuel_item_tooltip", config.getGuiFuelItemTooltip()), + field("gui_progress_item_title", config.getGuiProgressItemTitle()), + field("gui_progress_item_tooltip", config.getGuiProgressItemTooltip()), + field("gui_prev_recipe_item_title", config.getGuiPrevRecipeItemTitle()), + field("gui_prev_recipe_empty_item_tooltip", config.getGuiPrevRecipeEmptyItemTooltip()), + field("gui_prev_recipe_arbitrary_item_tooltip", config.getGuiPrevRecipeArbitraryItemTooltip()), + field("gui_prev_recipe_item_tooltip", config.getGuiPrevRecipeItemTooltip()), + field("gui_next_recipe_item_title", config.getGuiNextRecipeItemTitle()), + field("gui_next_recipe_empty_item_tooltip", config.getGuiNextRecipeEmptyItemTooltip()), + field("gui_next_recipe_arbitrary_item_tooltip", config.getGuiNextRecipeArbitraryItemTooltip()), + field("gui_next_recipe_item_tooltip", config.getGuiNextRecipeItemTooltip()), + field("gui_xp_indicator_item_title", config.getGuiXpIndicatorItemTitle()), + field("gui_xp_indicator_gainable_item_title", config.getGuiXpIndicatorGainableItemTitle()), + field("gui_xp_indicator_item_tooltip", config.getGuiXpIndicatorItemTooltip()), + field("gui_next_functional_area_item_title", config.getGuiNextFunctionalAreaItemTitle()), + field("gui_prev_page_item_title", config.getGuiPrevPageItemTitle()), + field("gui_prev_page_item_tooltip", config.getGuiPrevPageItemTooltip()), + field("gui_next_page_item_title", config.getGuiNextPageItemTitle()), + field("gui_next_page_item_tooltip", config.getGuiNextPageItemTooltip()) + ); + } + + private static void readIntMap( + JsonObject root, String key, EnumMap target + ) { + if (!root.has(key)) return; + JsonElement element = root.get(key); + if (!element.isJsonObject()) throw new JsonSyntaxException(key + " must be an object"); + for (Map.Entry entry : element.getAsJsonObject().entrySet()) { + try { + FurnaceType type = FurnaceType.valueOf(entry.getKey()); + int count = entry.getValue().getAsInt(); + if (count < 1 || count > 576) { + throw new JsonSyntaxException(key + '.' + entry.getKey() + " must be in 1..576"); } + target.put(type, count); + } catch (IllegalArgumentException ignored) { + if (isFurnaceType(entry.getKey())) throw ignored; } - if (obj.has("gui_next_page_item_tooltip")) { - JsonElement ele = obj.get("gui_next_page_item_tooltip"); - EnumMap ids = - config.getGuiNextPageItemTooltip(); - if (ele.isJsonObject()) { - for (Map.Entry i : - ele.getAsJsonObject().entrySet()) { - JsonElement value = i.getValue(); - if (value.isJsonPrimitive()) { - try { - ids.put( - FurnaceType.valueOf(i.getKey()), - value.getAsString() - ); - } catch (IllegalArgumentException ignored) {} - } - } - } else if (ele.isJsonPrimitive()) { - String id = ele.getAsString(); - for (FurnaceType type : FurnaceType.values()) { - ids.put(type, id); - } - } + } + } + + private static void readTypeSet(JsonObject root, String key, EnumSet target) { + if (!root.has(key)) return; + JsonElement element = root.get(key); + if (!element.isJsonArray()) throw new JsonSyntaxException(key + " must be an array"); + target.clear(); + for (JsonElement value : element.getAsJsonArray()) { + if (!value.isJsonPrimitive()) continue; + try { + target.add(FurnaceType.valueOf(value.getAsString())); + } catch (IllegalArgumentException ignored) { } } } + private static boolean isFurnaceType(String value) { + for (FurnaceType type : FurnaceType.values()) if (type.name().equals(value)) return true; + return false; + } + + private static StringMapField field(String key, EnumMap target) { + return new StringMapField(key, target); + } + + private static void readStringMaps(JsonObject root, StringMapField... fields) { + for (StringMapField field : fields) readStringMap(root, field.key, field.target); + } + + private static final class StringMapField { + private final String key; + private final EnumMap target; + + private StringMapField(String key, EnumMap target) { + this.key = key; + this.target = target; + } + } + private @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> inputSlotCount = new EnumMap<>(FurnaceType.class); private @NotNull EnumMap<@NotNull FurnaceType, @NotNull Integer> diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/ResolvedFurnaceConfig.java b/spigot/src/main/java/io/github/eat_ram/fuream/ResolvedFurnaceConfig.java new file mode 100644 index 0000000..65fd3dc --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/ResolvedFurnaceConfig.java @@ -0,0 +1,40 @@ +package io.github.eat_ram.fuream; + +import io.github.eat_ram.fuream.api.FureamWorldConfig; +import io.github.eat_ram.fuream.api.FurnaceType; +import org.jetbrains.annotations.NotNull; + +/** Validated, immutable values consumed by furnace and inventory hot paths. */ +public final class ResolvedFurnaceConfig { + public static final int MAX_LANE_SLOTS = 576; + + public final int inputSlots; + public final int fuelSlots; + public final int outputSlots; + public final boolean preventNonSmeltableHopperInput; + + private ResolvedFurnaceConfig( + int inputSlots, int fuelSlots, int outputSlots, + boolean preventNonSmeltableHopperInput + ) { + this.inputSlots = inputSlots; + this.fuelSlots = fuelSlots; + this.outputSlots = outputSlots; + this.preventNonSmeltableHopperInput = preventNonSmeltableHopperInput; + } + + public static @NotNull ResolvedFurnaceConfig resolve( + @NotNull FureamWorldConfig config, @NotNull FurnaceType type + ) { + return new ResolvedFurnaceConfig( + clamp(config.getInputSlotCount().get(type)), + clamp(config.getFuelSlotCount().get(type)), + clamp(config.getOutputSlotCount().get(type)), + config.getPreventsHopperInsertNonSmeltable().contains(type) + ); + } + + private static int clamp(Integer value) { + return Math.max(1, Math.min(MAX_LANE_SLOTS, value == null ? 9 : value)); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java index 052f5eb..79cd069 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/BlockCompat.java @@ -1,25 +1,31 @@ package io.github.eat_ram.fuream.compat; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Material; import org.bukkit.block.Block; public final class BlockCompat { + private static final Map, ModernAccessors> MODERN = new ConcurrentHashMap<>(); + private static final Set> NO_MODERN = Collections.newSetFromMap( + new ConcurrentHashMap, Boolean>() + ); + private static final Map, LegacyAccessors> LEGACY = new ConcurrentHashMap<>(); + public static void setLit(Block block, boolean lit) { try { - Object data = block.getClass().getMethod("getBlockData").invoke(block); - Method setLit = data.getClass().getMethod("setLit", boolean.class); - setLit.invoke(data, lit); - for (Method method : block.getClass().getMethods()) { - Class[] parameters = method.getParameterTypes(); - if ("setBlockData".equals(method.getName()) && parameters.length == 2 && - parameters[0].isInstance(data) && parameters[1] == boolean.class) { - method.invoke(block, data, false); - return; - } + ModernAccessors accessors = modern(block); + if (accessors != null) { + Object data = accessors.getBlockData.invoke(block); + Method setLit = accessors.setLit(data.getClass()); + setLit.invoke(data, lit); + accessors.setBlockData.invoke(block, data, false); + return; } - return; } catch (ReflectiveOperationException | RuntimeException ignored) { } @@ -29,16 +35,71 @@ public static void setLit(Block block, boolean lit) { Material target = Material.matchMaterial(lit ? "BURNING_FURNACE" : "FURNACE"); if (target == null || target == block.getType()) return; try { - byte data = ((Number) block.getClass().getMethod("getData").invoke(block)).byteValue(); - Method getId = Material.class.getMethod("getId"); - int id = ((Number) getId.invoke(target)).intValue(); - block.getClass().getMethod("setTypeIdAndData", int.class, byte.class, boolean.class) - .invoke(block, id, data, false); + LegacyAccessors accessors = legacy(block.getClass()); + byte data = ((Number) accessors.getData.invoke(block)).byteValue(); + int id = ((Number) accessors.getMaterialId.invoke(target)).intValue(); + accessors.setTypeAndData.invoke(block, id, data, false); } catch (ReflectiveOperationException ignored) { } } + private static ModernAccessors modern(Block block) { + Class type = block.getClass(); + ModernAccessors cached = MODERN.get(type); + if (cached != null) return cached; + if (NO_MODERN.contains(type)) return null; + try { + ModernAccessors created = new ModernAccessors(type); + ModernAccessors existing = MODERN.putIfAbsent(type, created); + return existing == null ? created : existing; + } catch (ReflectiveOperationException failure) { + NO_MODERN.add(type); + return null; + } + } + + private static LegacyAccessors legacy(Class type) throws ReflectiveOperationException { + LegacyAccessors cached = LEGACY.get(type); + if (cached != null) return cached; + LegacyAccessors created = new LegacyAccessors(type); + LegacyAccessors existing = LEGACY.putIfAbsent(type, created); + return existing == null ? created : existing; + } + + private static final class ModernAccessors { + private final Method getBlockData; + private final Method setBlockData; + private final Map, Method> setLit = new ConcurrentHashMap<>(); + + private ModernAccessors(Class blockType) throws ReflectiveOperationException { + this.getBlockData = blockType.getMethod("getBlockData"); + Class blockDataType = this.getBlockData.getReturnType(); + this.setBlockData = blockType.getMethod("setBlockData", blockDataType, boolean.class); + } + + private Method setLit(Class dataType) throws ReflectiveOperationException { + Method cached = this.setLit.get(dataType); + if (cached != null) return cached; + Method created = dataType.getMethod("setLit", boolean.class); + Method existing = this.setLit.putIfAbsent(dataType, created); + return existing == null ? created : existing; + } + } + + private static final class LegacyAccessors { + private final Method getData; + private final Method getMaterialId; + private final Method setTypeAndData; + + private LegacyAccessors(Class blockType) throws ReflectiveOperationException { + this.getData = blockType.getMethod("getData"); + this.getMaterialId = Material.class.getMethod("getId"); + this.setTypeAndData = blockType.getMethod( + "setTypeIdAndData", int.class, byte.class, boolean.class + ); + } + } + private BlockCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java index d4799a1..c9a0bea 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ComparatorCompat.java @@ -1,24 +1,16 @@ package io.github.eat_ram.fuream.compat; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.List; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.logic.FuelTable; -import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; import org.bukkit.inventory.ItemStack; /** Maintains the comparator view exposed by Fabric's three-slot masked inventory. */ public final class ComparatorCompat { - private static final BlockFace[] HORIZONTAL = { - BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST - }; - public static int calculate(FureamFurnaceData data) { ItemStack input = first(data.inputs, false); ItemStack fuel = first(data.fuels, true); @@ -34,19 +26,16 @@ public static int calculate(FureamFurnaceData data) { return occupied == 0 ? 0 : (int) Math.floor(fullness / 3f * 14f) + 1; } - public static void updateAround(Block furnace, int signal) { - for (BlockFace face : HORIZONTAL) { - Block comparator = furnace.getRelative(face); - if (!isComparator(comparator.getType()) || - !face.getOppositeFace().name().equals(facingName(comparator))) continue; - setComparatorOutput(comparator, signal); - } + public static void requestRecalculation(Block furnace) { + // Bukkit has no version-stable comparator notification API. Re-applying + // the current snapshot with physics is the least invasive public hook. + furnace.getState().update(false, true); } public static Block sourceBlock(Block comparator) { try { BlockFace facing = BlockFace.valueOf(facingName(comparator)); - return comparator.getRelative(facing); + return comparator.getRelative(facing.getOppositeFace()); } catch (IllegalArgumentException ignored) { return null; } @@ -59,11 +48,6 @@ private static ItemStack first(List stacks, boolean fuelOnly) { return ItemCompat.empty(); } - private static boolean isComparator(Material material) { - String name = material.name(); - return name.contains("COMPARATOR"); - } - private static String facingName(Block comparator) { try { Object data = comparator.getClass().getMethod("getBlockData").invoke(comparator); @@ -81,159 +65,6 @@ private static String facingName(Block comparator) { } } - private static void setComparatorOutput(Block comparator, int signal) { - setPowerState(comparator, signal > 0, false); - try { - BlockState state = comparator.getState(); - Method tileMethod = findTileEntityMethod(state.getClass()); - Object tile = tileMethod.invoke(state); - Field output = findOutputField(tile.getClass()); - output.setInt(tile, Math.max(0, Math.min(15, signal))); - markChanged(tile); - } catch (ReflectiveOperationException | RuntimeException ignored) { - } - notifyNeighbors(comparator); - } - - private static Method findTileEntityMethod(Class type) throws NoSuchMethodException { - for (String name : new String[] {"getTileEntityFromWorld", "getTileEntity"}) { - for (Class current = type; current != null; current = current.getSuperclass()) { - for (Method method : current.getDeclaredMethods()) { - if (name.equals(method.getName()) && method.getParameterTypes().length == 0) { - method.setAccessible(true); - return method; - } - } - } - } - throw new NoSuchMethodException("No comparator tile entity accessor on " + type.getName()); - } - - private static Field findOutputField(Class type) { - for (Class current = type; current != null; current = current.getSuperclass()) { - for (Field field : current.getDeclaredFields()) { - if (!Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) { - field.setAccessible(true); - return field; - } - } - } - throw new IllegalStateException("Comparator output field is unavailable on " + type.getName()); - } - - private static void markChanged(Object tile) { - for (String name : new String[] {"setChanged", "markDirty"}) { - try { - Method method = tile.getClass().getMethod(name); - method.invoke(tile); - return; - } catch (ReflectiveOperationException ignored) { - } - } - } - - private static void setPowerState(Block block, boolean powered, boolean physics) { - try { - Object data = block.getClass().getMethod("getBlockData").invoke(block); - Method poweredSetter = data.getClass().getMethod("setPowered", boolean.class); - poweredSetter.setAccessible(true); - poweredSetter.invoke(data, powered); - for (Method method : block.getClass().getMethods()) { - Class[] parameters = method.getParameterTypes(); - if ("setBlockData".equals(method.getName()) && parameters.length == 2 && - parameters[0].isInstance(data) && parameters[1] == boolean.class) { - method.invoke(block, data, physics); - return; - } - } - } catch (ReflectiveOperationException ignored) { - } - - try { - byte data = ((Number) block.getClass().getMethod("getData").invoke(block)).byteValue(); - Material target = Material.matchMaterial(powered - ? "REDSTONE_COMPARATOR_ON" : "REDSTONE_COMPARATOR_OFF"); - if (target == null) return; - int id = ((Number) Material.class.getMethod("getId").invoke(target)).intValue(); - block.getClass().getMethod("setTypeIdAndData", int.class, byte.class, boolean.class) - .invoke(block, id, data, physics); - } catch (ReflectiveOperationException ignored) { - } - } - - private static void notifyNeighbors(Block block) { - try { - Object craftWorld = block.getWorld(); - Object handle = craftWorld.getClass().getMethod("getHandle").invoke(craftWorld); - ClassLoader loader = handle.getClass().getClassLoader(); - String craftPackage = craftWorld.getClass().getPackage().getName(); - String version = craftPackage.substring(craftPackage.lastIndexOf('.') + 1); - Class positionClass = null; - for (String candidate : new String[] { - "net.minecraft.core.BlockPos", "net.minecraft.core.BlockPosition", - "net.minecraft.server." + version + ".BlockPosition" - }) { - try { - positionClass = Class.forName(candidate, false, loader); - break; - } catch (ClassNotFoundException ignored) { - } - } - if (positionClass == null) return; - Object position = positionClass.getConstructor(int.class, int.class, int.class) - .newInstance(block.getX(), block.getY(), block.getZ()); - - String craftRoot = org.bukkit.Bukkit.getServer().getClass().getPackage().getName(); - Class magicNumbers = Class.forName(craftRoot + ".util.CraftMagicNumbers"); - Object nmsBlock = magicNumbers.getMethod("getBlock", Material.class) - .invoke(null, block.getType()); - if (nmsBlock == null) return; - - Object blockState = null; - for (Method method : handle.getClass().getMethods()) { - Class[] parameters = method.getParameterTypes(); - if (parameters.length == 1 && parameters[0].isInstance(position) && - (method.getReturnType().getSimpleName().equals("IBlockData") || - method.getReturnType().getSimpleName().equals("BlockState"))) { - blockState = method.invoke(handle, position); - if (blockState != null) break; - } - } - if (blockState != null) { - for (Class current = nmsBlock.getClass(); current != null; current = current.getSuperclass()) { - for (Method method : current.getDeclaredMethods()) { - Class[] parameters = method.getParameterTypes(); - if (Modifier.isStatic(method.getModifiers()) || parameters.length != 3 || - !parameters[0].isInstance(handle) || !parameters[1].isInstance(position) || - !parameters[2].isInstance(blockState) || method.getReturnType() != void.class) continue; - String name = method.getName(); - if (!"updateNeighborsInFront".equals(name) && !"d".equals(name)) continue; - method.setAccessible(true); - method.invoke(nmsBlock, handle, position, blockState); - return; - } - } - } - - for (Class current = handle.getClass(); current != null; current = current.getSuperclass()) { - for (Method method : current.getDeclaredMethods()) { - Class[] parameters = method.getParameterTypes(); - if (Modifier.isStatic(method.getModifiers()) || parameters.length != 2 || - !parameters[0].isInstance(position) || !parameters[1].isInstance(nmsBlock) || - method.getReturnType() != void.class) continue; - String name = method.getName(); - if (!"updateNeighborsAt".equals(name) && !"applyPhysics".equals(name) && - !"notifyNeighborsOfStateChange".equals(name) && !"b".equals(name)) continue; - method.setAccessible(true); - method.invoke(handle, position, nmsBlock); - return; - } - } - } catch (ReflectiveOperationException | RuntimeException ignored) { - } - } - private ComparatorCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java index 17c945d..ad4db9f 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceCompat.java @@ -1,58 +1,118 @@ package io.github.eat_ram.fuream.compat; import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.block.Furnace; public final class FurnaceCompat { + private static final Map, Accessors> ACCESSORS = new ConcurrentHashMap<>(); + public static int getBurnTime(Furnace furnace) { - return getInt(furnace, "getBurnTime", 0); + Accessors accessors = accessors(furnace); + return accessors.getInt(furnace, accessors.getBurnTime, 0); } public static int getCookTime(Furnace furnace) { - return getInt(furnace, "getCookTime", 0); + Accessors accessors = accessors(furnace); + return accessors.getInt(furnace, accessors.getCookTime, 0); } public static int getCookTimeTotal(Furnace furnace, int fallback) { - return getInt(furnace, "getCookTimeTotal", fallback); + Accessors accessors = accessors(furnace); + return accessors.getInt(furnace, accessors.getCookTimeTotal, fallback); } public static void setCookTimeTotal(Furnace furnace, int value) { - invokeNumberSetter(furnace, "setCookTimeTotal", value); + Accessors accessors = accessors(furnace); + accessors.setInt(furnace, accessors.setCookTimeTotal, value); } public static void setCookTime(Furnace furnace, int value) { - invokeNumberSetter(furnace, "setCookTime", value); + Accessors accessors = accessors(furnace); + accessors.setInt(furnace, accessors.setCookTime, value); } public static void setBurnTime(Furnace furnace, int value) { - invokeNumberSetter(furnace, "setBurnTime", value); + Accessors accessors = accessors(furnace); + accessors.setInt(furnace, accessors.setBurnTime, value); } - private static int getInt(Object target, String name, int fallback) { - try { - Object value = target.getClass().getMethod(name).invoke(target); - return value instanceof Number ? ((Number) value).intValue() : fallback; - } catch (ReflectiveOperationException ignored) { - return fallback; - } + public static void initialize(Furnace furnace) { + accessors(furnace); } - private static void invokeNumberSetter(Object target, String name, int value) { - for (Method method : target.getClass().getMethods()) { - if (!name.equals(method.getName()) || method.getParameterTypes().length != 1) continue; + private static Accessors accessors(Furnace furnace) { + Class type = furnace.getClass(); + Accessors accessors = ACCESSORS.get(type); + if (accessors != null) return accessors; + Accessors created = new Accessors(type); + Accessors existing = ACCESSORS.putIfAbsent(type, created); + return existing == null ? created : existing; + } + + private static final class Accessors { + private final Method getBurnTime; + private final Method getCookTime; + private final Method getCookTimeTotal; + private final Method setBurnTime; + private final Method setCookTime; + private final Method setCookTimeTotal; + + private Accessors(Class type) { + this.getBurnTime = findGetter(type, "getBurnTime"); + this.getCookTime = findGetter(type, "getCookTime"); + this.getCookTimeTotal = findGetter(type, "getCookTimeTotal"); + this.setBurnTime = findSetter(type, "setBurnTime"); + this.setCookTime = findSetter(type, "setCookTime"); + this.setCookTimeTotal = findSetter(type, "setCookTimeTotal"); + } + + private int getInt(Object target, Method method, int fallback) { + if (method == null) return fallback; + try { + Object value = method.invoke(target); + return value instanceof Number ? ((Number) value).intValue() : fallback; + } catch (ReflectiveOperationException ignored) { + return fallback; + } + } + + private void setInt(Object target, Method method, int value) { + if (method == null) return; Class type = method.getParameterTypes()[0]; try { - if (type == short.class || type == Short.class) method.invoke(target, (short) value); - else if (type == int.class || type == Integer.class) method.invoke(target, value); - else continue; - return; + Object argument; + if (type == short.class || type == Short.class) { + argument = Short.valueOf((short) value); + } else { + argument = Integer.valueOf(value); + } + method.invoke(target, argument); + } catch (ReflectiveOperationException ignored) { + } + } + + private static Method findGetter(Class type, String name) { + try { + return type.getMethod(name); } catch (ReflectiveOperationException ignored) { + return null; + } + } + + private static Method findSetter(Class type, String name) { + for (Method method : type.getMethods()) { + if (!name.equals(method.getName()) || method.getParameterTypes().length != 1) continue; + Class parameter = method.getParameterTypes()[0]; + if (parameter == short.class || parameter == Short.class || + parameter == int.class || parameter == Integer.class) return method; } + return null; } } private FurnaceCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java index a2be7cb..9c2b500 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventCompat.java @@ -8,21 +8,28 @@ import org.bukkit.block.Block; import org.bukkit.entity.ExperienceOrb; import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; import org.bukkit.event.Event; +import org.bukkit.inventory.ItemStack; /** Optional Bukkit furnace events whose availability differs across legacy APIs. */ public final class FurnaceEventCompat { + private static Constructor extractConstructor; + private static Method extractExperience; + private static Constructor startConstructor; + private static Method startCookTime; + + public static void initialize() { + initializeExtractEvent(); + initializeStartEvent(); + } + public static void fireExtract(Player player, Block furnace, Material itemType, int amount) { - if (amount <= 0 || ItemCompat.isAir(itemType)) return; + if (amount <= 0 || ItemCompat.isAir(itemType) || extractConstructor == null) return; try { - Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceExtractEvent"); - Constructor constructor = eventClass.getConstructor( - Player.class, Block.class, Material.class, int.class, int.class - ); - Event event = (Event) constructor.newInstance(player, furnace, itemType, amount, 0); + Event event = (Event) extractConstructor.newInstance(player, furnace, itemType, amount, 0); Bukkit.getPluginManager().callEvent(event); - Method getter = eventClass.getMethod("getExpToDrop"); - int experience = ((Number) getter.invoke(event)).intValue(); + int experience = ((Number) extractExperience.invoke(event)).intValue(); if (experience > 0) { ExperienceOrb orb = player.getWorld().spawn(player.getLocation(), ExperienceOrb.class); orb.setExperience(experience); @@ -32,7 +39,55 @@ public static void fireExtract(Player player, Block furnace, Material itemType, } } + public static int fireStartSmelt( + Block block, ItemStack input, Object recipe, int fallbackCookTime + ) { + if (startConstructor == null || recipe == null) return fallbackCookTime; + Class recipeType = startConstructor.getParameterTypes()[2]; + if (!recipeType.isInstance(recipe)) return fallbackCookTime; + try { + Event event = (Event) startConstructor.newInstance(block, input.clone(), recipe); + FurnaceEventDispatcher.call(event); + if (event instanceof Cancellable && ((Cancellable) event).isCancelled()) { + return Integer.MAX_VALUE; + } + return Math.max(1, ((Number) startCookTime.invoke(event)).intValue()); + } catch (ReflectiveOperationException | RuntimeException failure) { + return fallbackCookTime; + } + } + + private static void initializeExtractEvent() { + try { + Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceExtractEvent"); + extractConstructor = eventClass.getConstructor( + Player.class, Block.class, Material.class, int.class, int.class + ); + extractExperience = eventClass.getMethod("getExpToDrop"); + } catch (ReflectiveOperationException | LinkageError ignored) { + extractConstructor = null; + extractExperience = null; + } + } + + private static void initializeStartEvent() { + try { + Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceStartSmeltEvent"); + for (Constructor constructor : eventClass.getConstructors()) { + Class[] types = constructor.getParameterTypes(); + if (types.length == 3 && Block.class.isAssignableFrom(types[0]) && + ItemStack.class.isAssignableFrom(types[1])) { + startConstructor = constructor; + startCookTime = eventClass.getMethod("getTotalCookTime"); + return; + } + } + } catch (ReflectiveOperationException | LinkageError ignored) { + } + startConstructor = null; + startCookTime = null; + } + private FurnaceEventCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventDispatcher.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventDispatcher.java new file mode 100644 index 0000000..4cfc430 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceEventDispatcher.java @@ -0,0 +1,33 @@ +package io.github.eat_ram.fuream.compat; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +/** Distinguishes Fuream's synthetic furnace events from the vanilla furnace engine. */ +public final class FurnaceEventDispatcher { + private static final ThreadLocal DISPATCH_DEPTH = new ThreadLocal<>(); + + public static void call(@NotNull Event event) { + Integer current = DISPATCH_DEPTH.get(); + DISPATCH_DEPTH.set(current == null ? 1 : current + 1); + try { + Bukkit.getPluginManager().callEvent(event); + } finally { + int next = DISPATCH_DEPTH.get() - 1; + if (next == 0) { + DISPATCH_DEPTH.remove(); + } else { + DISPATCH_DEPTH.set(next); + } + } + } + + public static boolean isDispatching() { + Integer depth = DISPATCH_DEPTH.get(); + return depth != null && depth > 0; + } + + private FurnaceEventDispatcher() { + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceStateTransaction.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceStateTransaction.java new file mode 100644 index 0000000..73636ad --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/FurnaceStateTransaction.java @@ -0,0 +1,92 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Furnace; +import org.bukkit.inventory.FurnaceInventory; +import org.jetbrains.annotations.NotNull; + +/** + * Owns one furnace block-state snapshot from read through commit. Inventory and + * timing fields must be changed through the same transaction. + */ +public final class FurnaceStateTransaction { + private static final Map, Method> SNAPSHOT_INVENTORY_METHODS = new ConcurrentHashMap<>(); + private static final Map, Boolean> NO_SNAPSHOT_INVENTORY = new ConcurrentHashMap<>(); + + private final Furnace state; + private final FurnaceInventory inventory; + private boolean changed; + + private FurnaceStateTransaction(@NotNull Furnace state) { + this.state = state; + FurnaceCompat.initialize(state); + this.inventory = snapshotInventory(state); + } + + public static FurnaceStateTransaction begin(@NotNull Block block) { + BlockState state = block.getState(); + return state instanceof Furnace ? new FurnaceStateTransaction((Furnace) state) : null; + } + + public @NotNull Furnace state() { + return this.state; + } + + public @NotNull FurnaceInventory inventory() { + return this.inventory; + } + + public void markChanged() { + this.changed = true; + } + + public void setTimes(int burnTime, int cookTime, int cookTimeTotal) { + if (FurnaceCompat.getBurnTime(this.state) != burnTime) { + FurnaceCompat.setBurnTime(this.state, burnTime); + this.changed = true; + } + if (FurnaceCompat.getCookTime(this.state) != cookTime) { + FurnaceCompat.setCookTime(this.state, cookTime); + this.changed = true; + } + if (cookTimeTotal > 0 && FurnaceCompat.getCookTimeTotal(this.state, cookTimeTotal) != cookTimeTotal) { + FurnaceCompat.setCookTimeTotal(this.state, cookTimeTotal); + this.changed = true; + } + } + + public boolean commit() { + return !this.changed || this.state.update(false, false); + } + + private static FurnaceInventory snapshotInventory(Furnace furnace) { + Class type = furnace.getClass(); + Method method = SNAPSHOT_INVENTORY_METHODS.get(type); + if (method == null && !NO_SNAPSHOT_INVENTORY.containsKey(type)) { + try { + method = type.getMethod("getSnapshotInventory"); + method.setAccessible(true); + SNAPSHOT_INVENTORY_METHODS.put(type, method); + } catch (ReflectiveOperationException | LinkageError ignored) { + NO_SNAPSHOT_INVENTORY.put(type, Boolean.TRUE); + } + } + if (method != null) { + try { + Object result = method.invoke(furnace); + if (result instanceof FurnaceInventory) { + return (FurnaceInventory) result; + } + } catch (ReflectiveOperationException ignored) { + NO_SNAPSHOT_INVENTORY.put(type, Boolean.TRUE); + SNAPSHOT_INVENTORY_METHODS.remove(type); + } + } + return furnace.getInventory(); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java index e7e8de3..a5276d9 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ItemCompat.java @@ -1,6 +1,5 @@ package io.github.eat_ram.fuream.compat; -import java.lang.reflect.Method; import java.util.Locale; import org.bukkit.Material; @@ -13,13 +12,8 @@ public static boolean isEmpty(ItemStack stack) { public static boolean isAir(Material material) { if (material == null) return true; - try { - Method method = Material.class.getMethod("isAir"); - return Boolean.TRUE.equals(method.invoke(material)); - } catch (ReflectiveOperationException ignored) { - String name = material.name(); - return "AIR".equals(name) || name.endsWith("_AIR"); - } + String name = material.name(); + return "AIR".equals(name) || name.endsWith("_AIR"); } public static ItemStack empty() { @@ -43,6 +37,5 @@ public static Material matchMaterial(String configured, Material fallback) { } private ItemCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java index 16f3016..a703d0a 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeCompat.java @@ -1,109 +1,31 @@ package io.github.eat_ram.fuream.compat; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; import java.util.List; import io.github.eat_ram.fuream.api.FurnaceType; -import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.Recipe; public final class RecipeCompat { public static RecipeHandle findFirst(ItemStack input, FurnaceType type) { - if (ItemCompat.isEmpty(input)) return null; - Iterator iterator = Bukkit.recipeIterator(); - while (iterator.hasNext()) { - Recipe recipe = iterator.next(); - if (matches(recipe, input, type)) return snapshot(recipe, type); - } - return null; + return RecipeIndex.findFirst(input, type); } public static List findAll(ItemStack input, FurnaceType type) { - List result = new ArrayList<>(); - if (ItemCompat.isEmpty(input)) return result; - Iterator iterator = Bukkit.recipeIterator(); - while (iterator.hasNext()) { - Recipe recipe = iterator.next(); - if (matches(recipe, input, type)) result.add(snapshot(recipe, type)); - } - result.sort(Comparator.comparing(handle -> handle.id)); - return result; + return RecipeIndex.findAll(input, type); } public static RecipeHandle findById(ItemStack input, FurnaceType type, String id) { - if (id == null) return null; - for (RecipeHandle handle : findAll(input, type)) { - if (id.equals(handle.id)) return handle; - } - return null; + return RecipeIndex.findById(input, type, id); } - private static boolean matches(Recipe recipe, ItemStack input, FurnaceType type) { - String simpleName = recipe.getClass().getSimpleName(); - if (type == FurnaceType.SMOKER) { - if (!simpleName.endsWith("SmokingRecipe")) return false; - } else if (type == FurnaceType.BLAST_FURNACE) { - if (!simpleName.endsWith("BlastingRecipe")) return false; - } else if (!simpleName.endsWith("FurnaceRecipe")) { - return false; - } - - try { - Method choiceMethod = recipe.getClass().getMethod("getInputChoice"); - Object choice = choiceMethod.invoke(recipe); - Method test = choice.getClass().getMethod("test", ItemStack.class); - return Boolean.TRUE.equals(test.invoke(choice, input)); - } catch (ReflectiveOperationException ignored) { - try { - Method inputMethod = recipe.getClass().getMethod("getInput"); - ItemStack expected = (ItemStack) inputMethod.invoke(recipe); - if (expected == null || expected.getType() != input.getType()) return false; - short durability = expected.getDurability(); - return durability == Short.MAX_VALUE || durability == input.getDurability(); - } catch (ReflectiveOperationException e) { - return false; - } - } - } - - private static RecipeHandle snapshot(Recipe recipe, FurnaceType type) { - ItemStack output = recipe.getResult() == null ? ItemCompat.empty() : recipe.getResult().clone(); - int cookingTime = type == FurnaceType.FURNACE ? 200 : 100; - float experience = 0f; - try { - Object value = recipe.getClass().getMethod("getCookingTime").invoke(recipe); - if (value instanceof Number) cookingTime = ((Number) value).intValue(); - } catch (ReflectiveOperationException ignored) { - } - try { - Object value = recipe.getClass().getMethod("getExperience").invoke(recipe); - if (value instanceof Number) experience = ((Number) value).floatValue(); - } catch (ReflectiveOperationException ignored) { - } - - String id = null; - try { - Object key = recipe.getClass().getMethod("getKey").invoke(recipe); - if (key != null) id = key.toString(); - } catch (ReflectiveOperationException ignored) { - } - if (id == null) { - id = "legacy:" + type.name().toLowerCase(LocaleHolder.ROOT) + "/" + - output.getType().name().toLowerCase(LocaleHolder.ROOT) + "/" + output.getDurability(); - } - return new RecipeHandle(recipe, id, output, Math.max(1, cookingTime), experience); + public static void rebuild() { + RecipeIndex.rebuild(); } - /** Avoids repeatedly resolving Locale from old JVM implementations. */ - private static final class LocaleHolder { - private static final java.util.Locale ROOT = java.util.Locale.ROOT; + public static int indexedRecipeCount() { + return RecipeIndex.size(); } private RecipeCompat() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeIndex.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeIndex.java new file mode 100644 index 0000000..e17e25d --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/RecipeIndex.java @@ -0,0 +1,202 @@ +package io.github.eat_ram.fuream.compat; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import io.github.eat_ram.fuream.api.FurnaceType; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Immutable cooking recipe snapshot rebuilt only when registrations may have changed. */ +public final class RecipeIndex { + private interface Matcher { + boolean matches(ItemStack input); + } + + private static final class Entry { + private final RecipeHandle handle; + private final Matcher matcher; + + private Entry(RecipeHandle handle, Matcher matcher) { + this.handle = handle; + this.matcher = matcher; + } + } + + private static volatile RecipeIndex current = empty(); + + private final EnumMap> byType; + private final EnumMap>> byId; + private final int size; + + private RecipeIndex( + EnumMap> byType, + EnumMap>> byId, + int size + ) { + this.byType = byType; + this.byId = byId; + this.size = size; + } + + public static void rebuild() { + EnumMap> byType = new EnumMap<>(FurnaceType.class); + EnumMap>> byId = new EnumMap<>(FurnaceType.class); + for (FurnaceType type : FurnaceType.values()) { + byType.put(type, new ArrayList()); + byId.put(type, new HashMap>()); + } + + int count = 0; + Iterator iterator = Bukkit.recipeIterator(); + while (iterator.hasNext()) { + Recipe recipe = iterator.next(); + FurnaceType type = furnaceType(recipe); + if (type == null) continue; + Entry entry = snapshot(recipe, type); + if (entry == null) continue; + byType.get(type).add(entry); + List sameId = byId.get(type).get(entry.handle.id); + if (sameId == null) { + sameId = new ArrayList<>(); + byId.get(type).put(entry.handle.id, sameId); + } + sameId.add(entry); + count++; + } + + for (FurnaceType type : FurnaceType.values()) { + byType.put(type, Collections.unmodifiableList(byType.get(type))); + Map> immutableIds = new HashMap<>(); + for (Map.Entry> entry : byId.get(type).entrySet()) { + immutableIds.put(entry.getKey(), Collections.unmodifiableList(entry.getValue())); + } + byId.put(type, Collections.unmodifiableMap(immutableIds)); + } + current = new RecipeIndex(byType, byId, count); + } + + public static @Nullable RecipeHandle findFirst(ItemStack input, FurnaceType type) { + if (ItemCompat.isEmpty(input)) return null; + for (Entry entry : current.byType.get(type)) { + if (entry.matcher.matches(input)) return entry.handle; + } + return null; + } + + public static @NotNull List findAll(ItemStack input, FurnaceType type) { + List result = new ArrayList<>(); + if (ItemCompat.isEmpty(input)) return result; + for (Entry entry : current.byType.get(type)) { + if (entry.matcher.matches(input)) result.add(entry.handle); + } + result.sort(Comparator.comparing(handle -> handle.id)); + return result; + } + + public static @Nullable RecipeHandle findById(ItemStack input, FurnaceType type, String id) { + if (id == null || ItemCompat.isEmpty(input)) return null; + List entries = current.byId.get(type).get(id); + if (entries == null) return null; + for (Entry entry : entries) { + if (entry.matcher.matches(input)) return entry.handle; + } + return null; + } + + public static int size() { + return current.size; + } + + private static RecipeIndex empty() { + EnumMap> byType = new EnumMap<>(FurnaceType.class); + EnumMap>> byId = new EnumMap<>(FurnaceType.class); + for (FurnaceType type : FurnaceType.values()) { + byType.put(type, Collections.emptyList()); + byId.put(type, Collections.>emptyMap()); + } + return new RecipeIndex(byType, byId, 0); + } + + private static @Nullable FurnaceType furnaceType(Recipe recipe) { + String simpleName = recipe.getClass().getSimpleName(); + if (simpleName.endsWith("SmokingRecipe")) return FurnaceType.SMOKER; + if (simpleName.endsWith("BlastingRecipe")) return FurnaceType.BLAST_FURNACE; + return simpleName.endsWith("FurnaceRecipe") ? FurnaceType.FURNACE : null; + } + + private static @Nullable Entry snapshot(Recipe recipe, FurnaceType type) { + Matcher matcher = matcher(recipe); + if (matcher == null) return null; + ItemStack output = recipe.getResult() == null ? ItemCompat.empty() : recipe.getResult().clone(); + int cookingTime = number(recipe, "getCookingTime", type == FurnaceType.FURNACE ? 200 : 100).intValue(); + float experience = number(recipe, "getExperience", 0f).floatValue(); + String id = recipeId(recipe, type, output); + return new Entry(new RecipeHandle( + recipe, id, output, Math.max(1, cookingTime), experience + ), matcher); + } + + private static @Nullable Matcher matcher(Recipe recipe) { + try { + Method choiceGetter = recipe.getClass().getMethod("getInputChoice"); + Object choice = choiceGetter.invoke(recipe); + Method choicesGetter = choice.getClass().getMethod("getChoices"); + Object rawChoices = choicesGetter.invoke(choice); + if (rawChoices instanceof List) { + final List choices = new ArrayList<>((List) rawChoices); + return input -> matchesChoiceSnapshot(choices, input); + } + } catch (ReflectiveOperationException | LinkageError ignored) { + } + try { + Method inputGetter = recipe.getClass().getMethod("getInput"); + ItemStack value = (ItemStack) inputGetter.invoke(recipe); + if (value == null) return null; + final ItemStack expected = value.clone(); + return input -> expected.getType() == input.getType() && + (expected.getDurability() == Short.MAX_VALUE || expected.getDurability() == input.getDurability()); + } catch (ReflectiveOperationException | LinkageError ignored) { + return null; + } + } + + private static boolean matchesChoiceSnapshot(List choices, ItemStack input) { + for (Object choice : choices) { + if (choice instanceof org.bukkit.Material && choice == input.getType()) return true; + if (choice instanceof ItemStack && ((ItemStack) choice).isSimilar(input)) return true; + } + return false; + } + + private static Number number(Recipe recipe, String methodName, Number fallback) { + try { + Object value = recipe.getClass().getMethod(methodName).invoke(recipe); + return value instanceof Number ? (Number) value : fallback; + } catch (ReflectiveOperationException | LinkageError ignored) { + return fallback; + } + } + + private static String recipeId(Recipe recipe, FurnaceType type, ItemStack output) { + try { + Object key = recipe.getClass().getMethod("getKey").invoke(recipe); + if (key != null) return key.toString(); + } catch (ReflectiveOperationException | LinkageError ignored) { + } + return "legacy:" + type.name().toLowerCase(Locale.ROOT) + '/' + + output.getType().name().toLowerCase(Locale.ROOT) + '/' + output.getDurability(); + } + +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java index d95eb6d..b70d968 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/ServerVersion.java @@ -74,7 +74,4 @@ public String toString() { return major + "." + minor + (patch == 0 ? "" : "." + patch); } - private ServerVersion() { - throw new UnsupportedOperationException(); - } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java index 0cf5d5a..1993cd7 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapter.java @@ -1,5 +1,7 @@ package io.github.eat_ram.fuream.compat; +import org.bukkit.block.Block; + /** Version capabilities used by the Java 8 common core. */ public interface VersionAdapter { String id(); @@ -9,4 +11,10 @@ public interface VersionAdapter { boolean supportsRecipeOverrides(); boolean usesDataComponents(); + + FurnaceStateTransaction beginFurnaceTransaction(Block block); + + void setFurnaceLit(Block block, boolean lit); + + void requestComparatorRecalculation(Block furnace); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java index dfcbb3a..19899fd 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/compat/VersionAdapters.java @@ -1,5 +1,7 @@ package io.github.eat_ram.fuream.compat; +import org.bukkit.block.Block; + /** Loads one of the four internal adapters without linking newer Bukkit API types. */ public final class VersionAdapters { private static final VersionAdapter CURRENT = select(ServerVersion.CURRENT); @@ -49,6 +51,21 @@ public final boolean supportsRecipeOverrides() { public final boolean usesDataComponents() { return this.dataComponents; } + + @Override + public final FurnaceStateTransaction beginFurnaceTransaction(Block block) { + return FurnaceStateTransaction.begin(block); + } + + @Override + public final void setFurnaceLit(Block block, boolean lit) { + BlockCompat.setLit(block, lit); + } + + @Override + public final void requestComparatorRecalculation(Block furnace) { + ComparatorCompat.requestRecalculation(furnace); + } } private static final class LegacyAdapter extends BaseAdapter { @@ -76,6 +93,5 @@ private ComponentsAdapter() { } private VersionAdapters() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java index 32dabf4..21d280a 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FureamFurnaceData.java @@ -15,8 +15,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static io.github.eat_ram.fuream.CollectionUtil.newDefaultedArrayList; - /** * The server-side smelting state of a virtual furnace: inputs, fuels, outputs, * recipe overrides, and accumulated experience. @@ -39,14 +37,45 @@ public class FureamFurnaceData implements FureamData { public float experience; public FureamFurnaceData() { - this.inputs = newDefaultedArrayList(new ItemStack(Material.AIR), 1); - this.fuels = newDefaultedArrayList(new ItemStack(Material.AIR), 1); - this.outputs = newDefaultedArrayList(new ItemStack(Material.AIR), 1); + this.inputs = new ArrayList<>(); + this.fuels = new ArrayList<>(); + this.outputs = new ArrayList<>(); + this.inputs.add(new ItemStack(Material.AIR)); + this.fuels.add(new ItemStack(Material.AIR)); + this.outputs.add(new ItemStack(Material.AIR)); this.recipeOverridingInput = new ItemStack(Material.AIR); this.overriddenRecipes = new HashMap<>(); this.runningRecipe = null; } + public @NotNull FureamFurnaceData copy() { + FureamFurnaceData copy = new FureamFurnaceData(); + copy.inputs.clear(); + copyStacks(this.inputs, copy.inputs); + copy.fuels.clear(); + copyStacks(this.fuels, copy.fuels); + copy.outputs.clear(); + copyStacks(this.outputs, copy.outputs); + copy.recipeOverridingInput = ItemCompat.isEmpty(this.recipeOverridingInput) + ? ItemCompat.empty() : this.recipeOverridingInput.clone(); + for (Map.Entry entry : this.overriddenRecipes.entrySet()) { + copy.overriddenRecipes.put( + new KeyableItemStack(entry.getKey().stack.clone()), entry.getValue() + ); + } + copy.runningRecipe = this.runningRecipe; + copy.experience = this.experience; + return copy; + } + + private static void copyStacks( + Iterable source, ArrayList target + ) { + for (ItemStack stack : source) { + target.add(ItemCompat.isEmpty(stack) ? ItemCompat.empty() : stack.clone()); + } + } + public boolean hasAny() { if (this.experience > 0f) return true; if (this.recipeOverridingInput != null && diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java b/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java index 041fc98..265e6b4 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/data/FurnaceFureamDataRegistry.java @@ -3,17 +3,14 @@ import java.util.HashMap; import java.util.function.Function; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -public abstract class FurnaceFureamDataRegistry { +public final class FurnaceFureamDataRegistry { public static final @NotNull HashMap<@NotNull String, @NotNull Function< ? super FureamDataHolder, ? extends @NotNull FureamData >> REGISTRY = new HashMap<>(); - @Contract("-> fail") private FurnaceFureamDataRegistry() { - throw new UnsupportedOperationException(); } static { diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java index e326af3..95f1757 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockBreakListener.java @@ -1,12 +1,13 @@ package io.github.eat_ram.fuream.hook; -import java.util.Collections; +import java.lang.reflect.Method; +import java.util.HashSet; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.logic.FureamFurnaceLogic; import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Location; @@ -20,44 +21,52 @@ import org.bukkit.event.entity.EntityExplodeEvent; public class BlockBreakListener implements Listener { - private static final Set BREAKING = Collections.newSetFromMap( - new ConcurrentHashMap() - ); + private static final Set PENDING = new HashSet<>(); @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockBreak(BlockBreakEvent event) { - this.handleFurnaceBreak(event.getBlock()); + this.handleFurnaceBreak(event.getBlock(), dropsItems(event)); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockBurn(BlockBurnEvent event) { - this.handleFurnaceBreak(event.getBlock()); + this.handleFurnaceBreak(event.getBlock(), true); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityExplode(EntityExplodeEvent event) { + boolean dropItems = event.getYield() > 0f; for (Block b : event.blockList()) { - this.handleFurnaceBreak(b); + this.handleFurnaceBreak(b, dropItems); } } - static void handleFurnaceBreak(Block block) { + static void handleFurnaceBreak(Block block, boolean dropItems) { FurnaceType type = FureamMain.getFurnaceType(block); if (type == null) return; FurnacePos pos = new FurnacePos(block); - if (!BREAKING.add(pos)) return; - Bukkit.getScheduler().runTask(FureamMain.getInstance(), () -> BREAKING.remove(pos)); - FurnaceContext ctx = FurnaceManager.CONTEXTS.remove(pos); - if (ctx == null) { - ctx = FurnaceManager.getOrCreateContext(block); - FurnaceManager.CONTEXTS.remove(pos); - } - if (ctx != null) { - Location loc = block.getLocation(); - FureamFurnaceLogic.dropOnBreak(loc, ctx.data); + if (!PENDING.add(pos)) return; + FurnaceContext context = FurnaceManager.prepareForRemoval(block); + FureamFurnaceData snapshot = context == null ? null : context.data.copy(); + Location location = block.getLocation().clone(); + Bukkit.getScheduler().runTask(FureamMain.getInstance(), () -> { + try { + FurnaceType remaining = FureamMain.getFurnaceType(location.getBlock()); + if (remaining == type) return; + if (dropItems && snapshot != null) FureamFurnaceLogic.dropOnBreak(location, snapshot); + } finally { + PENDING.remove(pos); + } + }); + } - FurnaceManager.closeSessions(ctx); + private static boolean dropsItems(BlockBreakEvent event) { + try { + Method method = event.getClass().getMethod("isDropItems"); + return !Boolean.FALSE.equals(method.invoke(event)); + } catch (ReflectiveOperationException | LinkageError ignored) { + return true; } } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java index 60917b4..76a70ea 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/BlockExplodeListener.java @@ -10,6 +10,9 @@ public final class BlockExplodeListener implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockExplode(BlockExplodeEvent event) { - for (Block block : event.blockList()) BlockBreakListener.handleFurnaceBreak(block); + boolean dropItems = event.getYield() > 0f; + for (Block block : event.blockList()) { + BlockBreakListener.handleFurnaceBreak(block, dropItems); + } } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java index 3ae8604..5a835c4 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ChunkListener.java @@ -1,10 +1,5 @@ package io.github.eat_ram.fuream.hook; -import java.util.Iterator; -import java.util.Map; - -import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; -import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Chunk; import org.bukkit.block.Block; import org.bukkit.block.BlockState; @@ -28,23 +23,7 @@ public void onChunkLoad(ChunkLoadEvent event) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onChunkUnload(ChunkUnloadEvent event) { - Chunk chunk = event.getChunk(); - String worldName = chunk.getWorld().getName(); - int cx = chunk.getX(); - int cz = chunk.getZ(); - - for (Iterator> it = FurnaceManager.CONTEXTS.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - FurnacePos pos = entry.getKey(); - if (pos.worldName.equals(worldName) && (pos.x >> 4) == cx && (pos.z >> 4) == cz) { - FurnaceContext ctx = entry.getValue(); - FurnaceManager.closeSessions(ctx); - if (ctx.dirty || ctx.data.hasAny()) { - FurnaceManager.saveToNbt(ctx); - } - it.remove(); - } - } + FurnaceManager.unloadChunk(event.getChunk()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java index da2e282..9301423 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/ComparatorListener.java @@ -22,7 +22,7 @@ public void onRedstone(BlockRedstoneEvent event) { if (type == null) return; FureamWorldConfig config = FureamMain.getWorldConfig(source.getWorld()); if (config == null || !config.getEnabledFurnaceTypes().contains(type)) return; - FurnaceManager.FurnaceContext context = FurnaceManager.CONTEXTS.get(new FurnacePos(source)); + FurnaceManager.FurnaceContext context = FurnaceManager.getContext(new FurnacePos(source)); if (context == null) context = FurnaceManager.getOrCreateContext(source); if (context != null) event.setNewCurrent(ComparatorCompat.calculate(context.data)); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java index ab0ffd3..829c35a 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceManager.java @@ -1,22 +1,27 @@ package io.github.eat_ram.fuream.hook; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.UUID; import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.ResolvedFurnaceConfig; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; import io.github.eat_ram.fuream.compat.ItemCompat; import io.github.eat_ram.fuream.compat.FurnaceCompat; +import io.github.eat_ram.fuream.compat.FurnaceStateTransaction; import io.github.eat_ram.fuream.compat.ComparatorCompat; +import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.data.FureamData; import io.github.eat_ram.fuream.data.FureamDataHolder; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.data.FurnaceFureamDataRegistry; import io.github.eat_ram.fuream.logic.FureamFurnaceEngine; +import io.github.eat_ram.fuream.logic.NativeLaneProjection; import io.github.eat_ram.fuream.logic.PassiveLaneProjection; import io.github.eat_ram.fuream.logic.PassiveLaneProjection.SelectionMode; import io.github.eat_ram.fuream.nbt.FurnaceRootNbtBridge; @@ -25,27 +30,22 @@ import io.github.eat_ram.fuream.util.FurnacePos; import io.github.eat_ram.fuream.util.KeyableItemStack; import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.Furnace; -import org.bukkit.block.Hopper; -import org.bukkit.entity.minecart.HopperMinecart; import org.bukkit.inventory.FurnaceInventory; -import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class FurnaceManager { - public static final Map CONTEXTS = new ConcurrentHashMap<>(); +public final class FurnaceManager { + private static final int SAVE_INTERVAL_TICKS = 40; + private static final Map CONTEXTS = new HashMap<>(); private static final String PDC_ROOT_KEY = "PublicBukkitValues"; private static long tickCounter = 0; - public static class FurnaceContext implements FureamDataHolder { + public static final class FurnaceContext implements FureamDataHolder { public final @NotNull FurnacePos pos; public final @NotNull FurnaceType type; public final @NotNull FureamFurnaceData data; @@ -56,7 +56,7 @@ public static class FurnaceContext implements FureamDataHolder { public int cookTime; public int cookTimeTotal; - public final @NotNull Map sessions = new ConcurrentHashMap<>(); + public final @NotNull Map sessions = new HashMap<>(); public boolean dirty; public boolean passiveProjected; public final @NotNull PassiveLaneProjection.State passiveInput = @@ -65,21 +65,38 @@ public static class FurnaceContext implements FureamDataHolder { new PassiveLaneProjection.State(); public final @NotNull PassiveLaneProjection.State passiveOutput = new PassiveLaneProjection.State(); + public boolean activeProjected; + public final @NotNull NativeLaneProjection.State activeFuel = + new NativeLaneProjection.State(); + public final @NotNull NativeLaneProjection.State activeOutput = + new NativeLaneProjection.State(); public int lastInputSlot = -1; public @Nullable KeyableItemStack lastInputKey; public boolean startSmeltPending = true; public int lastComparatorSignal = -1; + public @Nullable Boolean lastLit; + private @Nullable FureamWorldConfig resolvedSource; + private @Nullable ResolvedFurnaceConfig resolvedConfig; public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { this.pos = pos; this.type = type; this.data = new FureamFurnaceData(); this.cookTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); - this.fuelTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); + this.fuelTimeTotal = 200; } public @Nullable World getWorld() { - return Bukkit.getWorld(this.pos.worldName); + World byId = this.pos.worldId == null ? null : Bukkit.getWorld(this.pos.worldId); + return byId == null ? Bukkit.getWorld(this.pos.worldName) : byId; + } + + public @NotNull ResolvedFurnaceConfig resolve(@NotNull FureamWorldConfig config) { + if (this.resolvedConfig == null || this.resolvedSource != config) { + this.resolvedSource = config; + this.resolvedConfig = ResolvedFurnaceConfig.resolve(config, this.type); + } + return this.resolvedConfig; } @Override @@ -97,6 +114,58 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { } } + public static @Nullable FurnaceContext getContext(@NotNull FurnacePos pos) { + return CONTEXTS.get(pos); + } + + public static @Nullable FurnaceContext getContext(@NotNull Block block) { + return CONTEXTS.get(new FurnacePos(block)); + } + + public static int contextCount() { + return CONTEXTS.size(); + } + + public static boolean isManagedActive(@NotNull Block block) { + FurnaceContext context = getContext(block); + return context != null && FureamMain.getFurnaceType(block) == context.type && + isEnabled(block.getWorld(), context.type); + } + + public static @Nullable FurnaceContext prepareForRemoval(@NotNull Block block) { + FurnaceContext context = getContext(block); + if (context == null || FureamMain.getFurnaceType(block) != context.type) return context; + FureamWorldConfig config = FureamMain.getWorldConfig(block.getWorld()); + if (config != null && !context.passiveProjected) { + prepareActiveTransaction(context, block, config); + } + closeSessions(context); + return context; + } + + public static @Nullable FurnaceContext prepareForInteraction(@NotNull FurnacePos pos) { + FurnaceContext context = getContext(pos); + if (context == null) return null; + World world = context.getWorld(); + if (world == null || !world.isChunkLoaded(pos.x >> 4, pos.z >> 4)) return null; + Block block = world.getBlockAt(pos.x, pos.y, pos.z); + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config == null || FureamMain.getFurnaceType(block) != context.type) return null; + if (!context.passiveProjected) prepareActiveTransaction(context, block, config); + return context; + } + + public static void finishInteraction(@NotNull FurnaceContext context) { + World world = context.getWorld(); + if (world == null || !isEnabled(world, context.type) || + !world.isChunkLoaded(context.pos.x >> 4, context.pos.z >> 4)) return; + Block block = world.getBlockAt(context.pos.x, context.pos.y, context.pos.z); + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config != null && FureamMain.getFurnaceType(block) == context.type) { + publishActiveTransaction(context, block, config); + } + } + public static @Nullable FurnaceContext getOrCreateContext(@NotNull Block block) { FurnaceType type = FureamMain.getFurnaceType(block); if (type == null) return null; @@ -104,7 +173,9 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { FurnacePos pos = new FurnacePos(block); FurnaceContext existing = CONTEXTS.get(pos); if (existing != null) { - return existing; + if (existing.type == type) return existing; + closeSessions(existing); + CONTEXTS.remove(pos); } FurnaceContext ctx = new FurnaceContext(pos, type); @@ -146,12 +217,13 @@ public FurnaceContext(@NotNull FurnacePos pos, @NotNull FurnaceType type) { migrateVanillaLane(ctx, (Furnace) state); } else if (enabled) { if (ctx.passiveProjected) { - resumeActive(ctx, (Furnace) state); + resumeActive(ctx, block); } else { - absorbVanillaLane(ctx, (Furnace) state); + FureamWorldConfig config = FureamMain.getWorldConfig(block.getWorld()); + if (config != null) prepareActiveTransaction(ctx, block, config); } } else if (!ctx.passiveProjected) { - projectPassiveLane(ctx, (Furnace) state); + projectPassiveLane(ctx, block); } else { syncPassiveTimes(ctx, (Furnace) state); } @@ -190,56 +262,58 @@ private static void initializeInputIdentity(@NotNull FurnaceContext ctx) { } public static void saveToNbt(@NotNull FurnaceContext ctx) { + saveToNbt(ctx, true); + } + + private static void saveToNbt(@NotNull FurnaceContext ctx, boolean republishAfter) { World world = ctx.getWorld(); if (world == null || !world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) return; Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); - BlockState state = block.getState(); - if (state instanceof Furnace) { - try { - if (!ctx.passiveProjected) { - absorbVanillaLane(ctx, (Furnace) state); - } else { - syncPassiveTimes(ctx, (Furnace) state); + if (FureamMain.getFurnaceType(block) != ctx.type) return; + boolean republish = false; + try { + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (ctx.passiveProjected) { + BlockState state = block.getState(); + if (state instanceof Furnace) syncPassiveTimes(ctx, (Furnace) state); + } else if (config != null) { + prepareActiveTransaction(ctx, block, config); + republish = republishAfter && isEnabled(world, ctx.type); + } + FurnaceRootNbtBridge.store(block.getState(), ctx); + ctx.dirty = false; + } catch (Exception e) { + FureamMain.getInstance().getLogger().log(java.util.logging.Level.SEVERE, + "Unable to save furnace data at " + ctx.pos, e); + } finally { + if (republish) { + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config != null && FureamMain.getFurnaceType(block) == ctx.type) { + publishActiveTransaction(ctx, block, config); } - FurnaceRootNbtBridge.store(state, ctx); - ctx.dirty = false; - } catch (Exception e) { - FureamMain.getInstance().getLogger().log(java.util.logging.Level.SEVERE, - "Unable to save furnace data at " + ctx.pos, e); } } } - private static void absorbVanillaLane( - @NotNull FurnaceContext ctx, @NotNull Furnace furnaceState - ) { - FurnaceInventory inv = furnaceState.getInventory(); - boolean hadNativeItems = !ItemCompat.isEmpty(inv.getSmelting()) || - !ItemCompat.isEmpty(inv.getFuel()) || !ItemCompat.isEmpty(inv.getResult()); - inv.setSmelting(absorbLaneStack(ctx.data.inputs, inv.getSmelting())); - inv.setFuel(absorbLaneStack(ctx.data.fuels, inv.getFuel())); - inv.setResult(absorbLaneStack(ctx.data.outputs, inv.getResult())); - if (hadNativeItems) { - ctx.dirty = true; - } - } - private static @Nullable ItemStack absorbLaneStack( - @NotNull java.util.List target, @Nullable ItemStack stack + @NotNull List target, @Nullable ItemStack stack, int maxSlots ) { if (ItemCompat.isEmpty(stack)) { return null; } - int maxSlots = Math.max(1, target.size()); - ItemStack remainder = FureamFurnaceEngine.insertStackIntoList(target, stack, maxSlots); + ItemStack remainder = FureamFurnaceEngine.insertStackIntoList( + target, stack.clone(), Math.max(1, maxSlots) + ); return ItemCompat.isEmpty(remainder) ? null : remainder; } private static void migrateVanillaLane( @NotNull FurnaceContext ctx, @NotNull Furnace furnaceState ) { - FurnaceInventory inv = furnaceState.getInventory(); + FurnaceStateTransaction transaction = VersionAdapters.current().beginFurnaceTransaction(furnaceState.getBlock()); + if (transaction == null) return; + FurnaceInventory inv = transaction.inventory(); ItemStack smelting = inv.getSmelting(); ItemStack fuel = inv.getFuel(); ItemStack result = inv.getResult(); @@ -254,7 +328,10 @@ private static void migrateVanillaLane( ctx.data.outputs.set(0, result.clone()); } - inv.clear(); + inv.setSmelting(null); + inv.setFuel(null); + inv.setResult(null); + transaction.markChanged(); ctx.burnTime = FurnaceCompat.getBurnTime(furnaceState); ctx.cookTime = FurnaceCompat.getCookTime(furnaceState); int nativeTotal = FurnaceCompat.getCookTimeTotal( @@ -263,6 +340,8 @@ private static void migrateVanillaLane( ctx.cookTimeTotal = nativeTotal > 0 ? nativeTotal : (ctx.type == FurnaceType.FURNACE ? 200 : 100); + transaction.setTimes(0, 0, ctx.cookTimeTotal); + requireCommit(transaction, ctx); ctx.dirty = true; } @@ -302,9 +381,11 @@ private static boolean isEnabled(@Nullable World world, @NotNull FurnaceType typ return config != null && config.getEnabledFurnaceTypes().contains(type); } - private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Block block) { closeSessions(ctx); - FurnaceInventory inventory = furnace.getInventory(); + FurnaceStateTransaction transaction = VersionAdapters.current().beginFurnaceTransaction(block); + if (transaction == null) return; + FurnaceInventory inventory = transaction.inventory(); inventory.setSmelting(PassiveLaneProjection.begin( ctx.data.inputs, inventory.getSmelting(), SelectionMode.FIRST_PRESENT, ctx.passiveInput )); @@ -314,9 +395,11 @@ private static void projectPassiveLane(@NotNull FurnaceContext ctx, @NotNull Fur inventory.setResult(PassiveLaneProjection.begin( ctx.data.outputs, inventory.getResult(), SelectionMode.FIRST_PRESENT, ctx.passiveOutput )); - FurnaceCompat.setBurnTime(furnace, Math.max(0, ctx.burnTime)); - FurnaceCompat.setCookTime(furnace, Math.max(0, ctx.cookTime)); - if (ctx.cookTimeTotal > 0) FurnaceCompat.setCookTimeTotal(furnace, ctx.cookTimeTotal); + transaction.markChanged(); + transaction.setTimes( + Math.max(0, ctx.burnTime), Math.max(0, ctx.cookTime), Math.max(1, ctx.cookTimeTotal) + ); + requireCommit(transaction, ctx); ctx.passiveProjected = true; ctx.dirty = true; } @@ -328,9 +411,15 @@ private static int firstPresentSlot(@NotNull java.util.List stacks) { return -1; } - private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace furnace) { + private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Block block) { if (!ctx.passiveProjected) return; - FurnaceInventory inventory = furnace.getInventory(); + FurnaceStateTransaction transaction = VersionAdapters.current().beginFurnaceTransaction(block); + if (transaction == null) return; + Furnace furnace = transaction.state(); + FurnaceInventory inventory = transaction.inventory(); + ctx.burnTime = FurnaceCompat.getBurnTime(furnace); + ctx.cookTime = FurnaceCompat.getCookTime(furnace); + ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); inventory.setSmelting(PassiveLaneProjection.restore( ctx.data.inputs, inventory.getSmelting(), ctx.passiveInput )); @@ -343,9 +432,9 @@ private static void resumeActive(@NotNull FurnaceContext ctx, @NotNull Furnace f ctx.passiveInput.reset(); ctx.passiveFuel.reset(); ctx.passiveOutput.reset(); - ctx.burnTime = FurnaceCompat.getBurnTime(furnace); - ctx.cookTime = FurnaceCompat.getCookTime(furnace); - ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); + transaction.markChanged(); + transaction.setTimes(0, 0, Math.max(1, ctx.cookTimeTotal)); + requireCommit(transaction, ctx); ctx.passiveProjected = false; ctx.dirty = true; } @@ -356,7 +445,119 @@ private static void syncPassiveTimes(@NotNull FurnaceContext ctx, @NotNull Furna ctx.cookTimeTotal = FurnaceCompat.getCookTimeTotal(furnace, ctx.cookTimeTotal); } + /** Reconciles hopper/player changes and absorbs native input buffers before custom logic. */ + public static void prepareActiveTransaction( + @NotNull FurnaceContext ctx, @NotNull Block block, @NotNull FureamWorldConfig config + ) { + FurnaceStateTransaction transaction = VersionAdapters.current().beginFurnaceTransaction(block); + if (transaction == null) return; + FurnaceInventory inventory = transaction.inventory(); + ResolvedFurnaceConfig resolved = ctx.resolve(config); + int inputCount = resolved.inputSlots; + int fuelCount = resolved.fuelSlots; + int outputCount = resolved.outputSlots; + ensureSize(ctx.data.inputs, inputCount); + ensureSize(ctx.data.fuels, fuelCount); + ensureSize(ctx.data.outputs, outputCount); + + boolean changed = false; + boolean logicalChange = false; + if (ctx.activeProjected) { + ItemStack observedFuel = inventory.getFuel(); + ItemStack observedOutput = inventory.getResult(); + logicalChange = !sameStack(observedFuel, ctx.activeFuel.getPublished()) || + !sameStack(observedOutput, ctx.activeOutput.getPublished()); + ItemStack fuelRemainder = NativeLaneProjection.reconcile( + ctx.data.fuels, observedFuel, fuelCount, ctx.activeFuel + ); + ItemStack outputRemainder = NativeLaneProjection.reconcile( + ctx.data.outputs, observedOutput, outputCount, ctx.activeOutput + ); + inventory.setFuel(ItemCompat.isEmpty(fuelRemainder) ? null : fuelRemainder); + inventory.setResult(ItemCompat.isEmpty(outputRemainder) ? null : outputRemainder); + ctx.activeProjected = false; + changed = true; + } + + ItemStack nativeInput = inventory.getSmelting(); + if (!ItemCompat.isEmpty(nativeInput)) { + inventory.setSmelting(absorbLaneStack(ctx.data.inputs, nativeInput, inputCount)); + changed = true; + logicalChange = true; + } + ItemStack nativeFuel = inventory.getFuel(); + if (!ItemCompat.isEmpty(nativeFuel)) { + inventory.setFuel(absorbLaneStack(ctx.data.fuels, nativeFuel, fuelCount)); + changed = true; + logicalChange = true; + } + ItemStack nativeResult = inventory.getResult(); + if (!ItemCompat.isEmpty(nativeResult)) { + inventory.setResult(absorbLaneStack(ctx.data.outputs, nativeResult, outputCount)); + changed = true; + logicalChange = true; + } + + if (changed) { + transaction.markChanged(); + } + if (logicalChange) ctx.dirty = true; + transaction.setTimes(0, 0, Math.max(1, ctx.cookTimeTotal)); + requireCommit(transaction, ctx); + } + + /** Publishes extractable stacks to native slots so Bukkit handles hoppers and minecarts. */ + public static void publishActiveTransaction( + @NotNull FurnaceContext ctx, @NotNull Block block, @NotNull FureamWorldConfig config + ) { + FurnaceStateTransaction transaction = VersionAdapters.current().beginFurnaceTransaction(block); + if (transaction == null) return; + FurnaceInventory inventory = transaction.inventory(); + ResolvedFurnaceConfig resolved = ctx.resolve(config); + int fuelCount = resolved.fuelSlots; + int outputCount = resolved.outputSlots; + ensureSize(ctx.data.fuels, fuelCount); + ensureSize(ctx.data.outputs, outputCount); + + ItemStack fuel = NativeLaneProjection.publish( + ctx.data.fuels, inventory.getFuel(), + NativeLaneProjection.SelectionMode.BUCKET_REMAINDER, ctx.activeFuel + ); + ItemStack output = NativeLaneProjection.publish( + ctx.data.outputs, inventory.getResult(), + NativeLaneProjection.SelectionMode.FIRST_PRESENT, ctx.activeOutput + ); + inventory.setFuel(ItemCompat.isEmpty(fuel) ? null : fuel); + inventory.setResult(ItemCompat.isEmpty(output) ? null : output); + ctx.activeProjected = ctx.activeFuel.isActive() || ctx.activeOutput.isActive(); + if (ctx.activeProjected) { + transaction.markChanged(); + } + transaction.setTimes(0, 0, Math.max(1, ctx.cookTimeTotal)); + requireCommit(transaction, ctx); + FurnaceRootNbtBridge.updateProjectionMarker(block.getState(), ctx); + } + + private static void requireCommit( + @NotNull FurnaceStateTransaction transaction, @NotNull FurnaceContext ctx + ) { + if (!transaction.commit()) { + throw new IllegalStateException("Furnace state changed before commit at " + ctx.pos); + } + } + + private static void ensureSize(@NotNull List slots, int size) { + while (slots.size() < size) slots.add(ItemCompat.empty()); + } + + private static boolean sameStack(@Nullable ItemStack left, @Nullable ItemStack right) { + return ItemCompat.isEmpty(left) && ItemCompat.isEmpty(right) || + !ItemCompat.isEmpty(left) && !ItemCompat.isEmpty(right) && + left.getAmount() == right.getAmount() && left.isSimilar(right); + } + public static void tickAll() { + List replacementFurnaces = new ArrayList<>(); Iterator> it = CONTEXTS.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); @@ -375,14 +576,16 @@ public static void tickAll() { Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); FurnaceType currentType = FureamMain.getFurnaceType(block); if (currentType == null) { - // Block was removed or changed closeSessions(ctx); - if (ctx.dirty) { - saveToNbt(ctx); - } it.remove(); continue; } + if (currentType != ctx.type) { + closeSessions(ctx); + it.remove(); + replacementFurnaces.add(block); + continue; + } FureamWorldConfig config = FureamMain.getWorldConfig(world); boolean enabled = config != null && config.getEnabledFurnaceTypes().contains(ctx.type); @@ -390,141 +593,73 @@ public static void tickAll() { BlockState state = block.getState(); if (enabled) { if (ctx.passiveProjected && state instanceof Furnace) { - resumeActive(ctx, (Furnace) state); + resumeActive(ctx, block); } - FureamFurnaceEngine.tick(ctx, config); + prepareActiveTransaction(ctx, block, config); + FureamFurnaceEngine.tick(ctx, ctx.resolve(config)); int comparatorSignal = ComparatorCompat.calculate(ctx.data); - if (comparatorSignal != ctx.lastComparatorSignal || tickCounter % 20 == 0) { - ComparatorCompat.updateAround(block, comparatorSignal); + if (comparatorSignal != ctx.lastComparatorSignal) { + VersionAdapters.current().requestComparatorRecalculation(block); ctx.lastComparatorSignal = comparatorSignal; } - - // Handle hopper below extraction (every 8 ticks, standard hopper speed) - if (tickCounter % 8 == 0) { - processHopperPull(block, ctx); + boolean lit = ctx.burnTime > 0; + if (ctx.lastLit == null || ctx.lastLit != lit) { + VersionAdapters.current().setFurnaceLit(block, lit); + ctx.lastLit = lit; } + publishActiveTransaction(ctx, block, config); + refreshSessions(ctx); } else if (!ctx.passiveProjected && state instanceof Furnace) { - projectPassiveLane(ctx, (Furnace) state); + if (ctx.activeProjected && config != null) { + prepareActiveTransaction(ctx, block, config); + } + projectPassiveLane(ctx, block); } else if (ctx.passiveProjected && state instanceof Furnace) { syncPassiveTimes(ctx, (Furnace) state); } - // Periodic save if dirty - if (ctx.dirty) { - if (tickCounter % 40 == 0) { - saveToNbt(ctx); - } + if (ctx.dirty && tickCounter % SAVE_INTERVAL_TICKS == 0) { + saveToNbt(ctx); } } + for (Block block : replacementFurnaces) getOrCreateContext(block); tickCounter++; } - private static void processHopperPull(@NotNull Block furnaceBlock, @NotNull FurnaceContext ctx) { - Block below = furnaceBlock.getRelative(BlockFace.DOWN); - Inventory targetInv = null; - - if (below.getType() == Material.HOPPER) { - if (isHopperEnabled(below)) { - BlockState belowState = below.getState(); - if (belowState instanceof Hopper) { - targetInv = ((Hopper) belowState).getInventory(); - } - } - } - - if (targetInv == null) { - Location centerBelow = furnaceBlock.getLocation().add(0.5, -0.5, 0.5); - for (Object entity : nearbyEntities(furnaceBlock.getWorld(), centerBelow)) { - if (entity instanceof HopperMinecart) { - targetInv = ((HopperMinecart) entity).getInventory(); - break; - } - } - } - - if (targetInv == null) { - return; - } - - // Fabric exposes extractable fuel remainders before outputs. - Material bucket = Material.matchMaterial("BUCKET"); - Material waterBucket = Material.matchMaterial("WATER_BUCKET"); - for (int i = 0; i < ctx.data.fuels.size(); i++) { - ItemStack fuelStack = ctx.data.fuels.get(i); - if (!ItemCompat.isEmpty(fuelStack) && - (fuelStack.getType() == bucket || fuelStack.getType() == waterBucket)) { - if (moveOne(furnaceBlock, targetInv, ctx.data.fuels, i, ctx)) return; + public static void flushAll() { + for (FurnaceContext ctx : CONTEXTS.values()) { + if (ctx.dirty || ctx.data.hasAny()) { + saveToNbt(ctx); } } - for (int i = 0; i < ctx.data.outputs.size(); i++) { - if (!ItemCompat.isEmpty(ctx.data.outputs.get(i)) && - moveOne(furnaceBlock, targetInv, ctx.data.outputs, i, ctx)) return; - } - } - - private static @NotNull Iterable nearbyEntities( - @NotNull World world, @NotNull Location center - ) { - try { - Object entities = world.getClass().getMethod( - "getNearbyEntities", Location.class, double.class, double.class, double.class - ).invoke(world, center, 0.6, 0.6, 0.6); - if (entities instanceof Iterable) return (Iterable) entities; - } catch (ReflectiveOperationException ignored) { - } - return java.util.Collections.emptyList(); } - private static boolean isHopperEnabled(@NotNull Block hopper) { - try { - Object data = hopper.getClass().getMethod("getBlockData").invoke(hopper); - Object result = data.getClass().getMethod("isEnabled").invoke(data); - return Boolean.TRUE.equals(result); - } catch (ReflectiveOperationException ignored) { - return !hopper.isBlockPowered(); + public static void flushWorld(@NotNull World world) { + for (FurnaceContext ctx : new ArrayList<>(CONTEXTS.values())) { + if (sameWorld(ctx.pos, world) && (ctx.dirty || ctx.data.hasAny())) saveToNbt(ctx); } } - private static boolean moveOne( - @NotNull Block furnaceBlock, @NotNull Inventory target, - @NotNull java.util.List sourceSlots, int slot, - @NotNull FurnaceContext ctx - ) { - ItemStack sourceStack = sourceSlots.get(slot); - if (ItemCompat.isEmpty(sourceStack)) return false; - ItemStack single = sourceStack.clone(); - single.setAmount(1); - Inventory source = ((Furnace) furnaceBlock.getState()).getInventory(); - org.bukkit.event.inventory.InventoryMoveItemEvent event = - new org.bukkit.event.inventory.InventoryMoveItemEvent(source, single, target, false); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled() || ItemCompat.isEmpty(event.getItem())) return false; - ItemStack[] targetSnapshot = cloneContents(target.getContents()); - Map leftover = target.addItem(event.getItem().clone()); - if (!leftover.isEmpty()) { - target.setContents(targetSnapshot); - return false; - } - sourceStack.setAmount(sourceStack.getAmount() - 1); - if (sourceStack.getAmount() <= 0) sourceSlots.set(slot, ItemCompat.empty()); - refreshSessions(ctx); - ctx.dirty = true; - return true; - } - - private static ItemStack[] cloneContents(ItemStack[] contents) { - ItemStack[] result = new ItemStack[contents.length]; - for (int i = 0; i < contents.length; i++) { - result[i] = ItemCompat.isEmpty(contents[i]) ? null : contents[i].clone(); + public static void unloadWorld(@NotNull World world) { + Iterator> iterator = CONTEXTS.entrySet().iterator(); + while (iterator.hasNext()) { + FurnaceContext ctx = iterator.next().getValue(); + if (!sameWorld(ctx.pos, world)) continue; + closeSessions(ctx); + if (ctx.dirty || ctx.data.hasAny()) saveToNbt(ctx, false); + iterator.remove(); } - return result; } - public static void flushAll() { - for (FurnaceContext ctx : CONTEXTS.values()) { - if (ctx.dirty || ctx.data.hasAny()) { - saveToNbt(ctx); - } + public static void unloadChunk(@NotNull org.bukkit.Chunk chunk) { + Iterator> iterator = CONTEXTS.entrySet().iterator(); + while (iterator.hasNext()) { + FurnaceContext ctx = iterator.next().getValue(); + if (!sameWorld(ctx.pos, chunk.getWorld()) || + (ctx.pos.x >> 4) != chunk.getX() || (ctx.pos.z >> 4) != chunk.getZ()) continue; + closeSessions(ctx); + if (ctx.dirty || ctx.data.hasAny()) saveToNbt(ctx, false); + iterator.remove(); } } @@ -552,21 +687,33 @@ public static void reconcileConfiguration() { boolean enabled = isEnabled(world, ctx.type); if (enabled && ctx.passiveProjected) { - resumeActive(ctx, (Furnace) state); + resumeActive(ctx, block); initializeInputIdentity(ctx); } else if (!enabled && !ctx.passiveProjected) { closeSessions(ctx); - projectPassiveLane(ctx, (Furnace) state); + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config != null && ctx.activeProjected) prepareActiveTransaction(ctx, block, config); + projectPassiveLane(ctx, block); } saveToNbt(ctx); } } public static void shutdown() { - for (FurnaceContext ctx : CONTEXTS.values()) { + for (FurnaceContext ctx : new ArrayList<>(CONTEXTS.values())) { closeSessions(ctx); + World world = ctx.getWorld(); + if (world != null && world.isChunkLoaded(ctx.pos.x >> 4, ctx.pos.z >> 4)) { + Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); + FureamWorldConfig config = FureamMain.getWorldConfig(world); + if (config != null && ctx.activeProjected && FureamMain.getFurnaceType(block) == ctx.type) { + prepareActiveTransaction(ctx, block, config); + } + } + } + for (FurnaceContext ctx : CONTEXTS.values()) { + if (ctx.dirty || ctx.data.hasAny()) saveToNbt(ctx, false); } - flushAll(); CONTEXTS.clear(); } @@ -598,4 +745,11 @@ private static void validateSessions( } } } + + private static boolean sameWorld(@NotNull FurnacePos pos, @NotNull World world) { + return pos.worldId != null ? pos.worldId.equals(world.getUID()) : pos.worldName.equals(world.getName()); + } + + private FurnaceManager() { + } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java index 509a571..e98fbef 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/FurnaceOpenListener.java @@ -13,6 +13,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; +import java.lang.reflect.InvocationTargetException; public class FurnaceOpenListener implements Listener { @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @@ -20,6 +21,7 @@ public void onPlayerInteract(PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; } + if (isOffHand(event) || isBlockUseDenied(event)) return; Block block = event.getClickedBlock(); if (block == null) return; @@ -38,10 +40,12 @@ public void onPlayerInteract(PlayerInteractEvent event) { } if (!canOpenLocked(block, player)) return; - event.setCancelled(true); - FurnaceContext ctx = FurnaceManager.getOrCreateContext(block); if (ctx == null) return; + ctx = FurnaceManager.prepareForInteraction(ctx.pos); + if (ctx == null) return; + + event.setCancelled(true); FureamScreenInventory session = new FureamScreenInventory( player.getUniqueId(), block.getLocation(), type, ctx.data, config @@ -53,6 +57,7 @@ public void onPlayerInteract(PlayerInteractEvent event) { session.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); session.refreshVisuals(); player.openInventory(session.bukkitInventory); + FurnaceManager.finishInteraction(ctx); } private static boolean canOpenLocked(Block block, Player player) { @@ -65,8 +70,28 @@ private static boolean canOpenLocked(Block block, Player player) { org.bukkit.inventory.ItemStack held = player.getItemInHand(); return !ItemCompat.isEmpty(held) && held.hasItemMeta() && held.getItemMeta().hasDisplayName() && lock.equals(held.getItemMeta().getDisplayName()); - } catch (ReflectiveOperationException ignored) { + } catch (NoSuchMethodException ignored) { return true; + } catch (InvocationTargetException | IllegalAccessException | RuntimeException failure) { + return false; + } + } + + private static boolean isOffHand(PlayerInteractEvent event) { + try { + Object hand = event.getClass().getMethod("getHand").invoke(event); + return hand instanceof Enum && "OFF_HAND".equals(((Enum) hand).name()); + } catch (ReflectiveOperationException | LinkageError ignored) { + return false; + } + } + + private static boolean isBlockUseDenied(PlayerInteractEvent event) { + try { + Object result = event.getClass().getMethod("useInteractedBlock").invoke(event); + return result instanceof Enum && "DENY".equals(((Enum) result).name()); + } catch (ReflectiveOperationException | LinkageError ignored) { + return false; } } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java index d63ff3b..c1476b8 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/GuiListener.java @@ -12,22 +12,34 @@ import org.bukkit.inventory.Inventory; public class GuiListener implements Listener { - @EventHandler(priority = EventPriority.HIGHEST) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onInventoryClick(InventoryClickEvent event) { FureamScreenInventory session = session(event.getInventory()); if (session == null || !(event.getWhoClicked() instanceof Player)) return; + FurnaceManager.FurnaceContext ctx = FurnaceManager.prepareForInteraction( + new FurnacePos(session.location) + ); + if (ctx == null) { + event.setCancelled(true); + return; + } session.handler.handleClick(event, (Player) event.getWhoClicked()); - FurnaceManager.FurnaceContext ctx = context(session); - if (ctx != null) ctx.dirty = true; + FurnaceManager.finishInteraction(ctx); } - @EventHandler(priority = EventPriority.HIGHEST) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onInventoryDrag(InventoryDragEvent event) { FureamScreenInventory session = session(event.getInventory()); if (session == null) return; + FurnaceManager.FurnaceContext ctx = FurnaceManager.prepareForInteraction( + new FurnacePos(session.location) + ); + if (ctx == null) { + event.setCancelled(true); + return; + } session.handler.handleDrag(event); - FurnaceManager.FurnaceContext ctx = context(session); - if (ctx != null) ctx.dirty = true; + FurnaceManager.finishInteraction(ctx); } @EventHandler(priority = EventPriority.MONITOR) @@ -36,7 +48,7 @@ public void onInventoryClose(InventoryCloseEvent event) { if (session == null) return; FurnaceManager.FurnaceContext ctx = context(session); if (ctx != null) { - ctx.sessions.remove(session.viewerId); + ctx.sessions.remove(session.viewerId, session); FurnaceManager.saveToNbt(ctx); } } @@ -47,6 +59,6 @@ private static FureamScreenInventory session(Inventory inventory) { } private static FurnaceManager.FurnaceContext context(FureamScreenInventory session) { - return FurnaceManager.CONTEXTS.get(new FurnacePos(session.location)); + return FurnaceManager.getContext(new FurnacePos(session.location)); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/RecipeIndexListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/RecipeIndexListener.java new file mode 100644 index 0000000..2cce781 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/RecipeIndexListener.java @@ -0,0 +1,34 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.compat.RecipeCompat; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; + +/** Rebuilds the immutable recipe snapshot after plugins can change registrations. */ +public final class RecipeIndexListener implements Listener { + private boolean scheduled; + + @EventHandler(priority = EventPriority.MONITOR) + public void onPluginEnable(PluginEnableEvent event) { + schedule(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPluginDisable(PluginDisableEvent event) { + schedule(); + } + + private void schedule() { + if (this.scheduled) return; + this.scheduled = true; + Bukkit.getScheduler().runTask(FureamMain.getInstance(), () -> { + this.scheduled = false; + RecipeCompat.rebuild(); + }); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/hook/VanillaFurnaceGuardListener.java b/spigot/src/main/java/io/github/eat_ram/fuream/hook/VanillaFurnaceGuardListener.java new file mode 100644 index 0000000..44cd9f1 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/hook/VanillaFurnaceGuardListener.java @@ -0,0 +1,25 @@ +package io.github.eat_ram.fuream.hook; + +import io.github.eat_ram.fuream.compat.FurnaceEventDispatcher; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.FurnaceBurnEvent; +import org.bukkit.event.inventory.FurnaceSmeltEvent; + +/** Prevents the vanilla engine from consuming Fuream's native ingress/projection slots. */ +public final class VanillaFurnaceGuardListener implements Listener { + @EventHandler(priority = EventPriority.HIGHEST) + public void onBurn(FurnaceBurnEvent event) { + if (!FurnaceEventDispatcher.isDispatching() && FurnaceManager.isManagedActive(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onSmelt(FurnaceSmeltEvent event) { + if (!FurnaceEventDispatcher.isDispatching() && FurnaceManager.isManagedActive(event.getBlock())) { + event.setCancelled(true); + } + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java index 014e579..2655e59 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FuelTable.java @@ -11,7 +11,7 @@ import org.jetbrains.annotations.Nullable; import io.github.eat_ram.fuream.compat.ItemCompat; -public abstract class FuelTable { +public final class FuelTable { private static final Map FUEL_TIMES = new EnumMap<>(Material.class); static { @@ -24,19 +24,6 @@ public static void initDynamic() { Class furnaceClass = getFurnaceNmsClass(); if (furnaceClass == null) return; - Method getFuelMethod = null; - for (Method m : furnaceClass.getDeclaredMethods()) { - if (Modifier.isStatic(m.getModifiers()) && Map.class.isAssignableFrom(m.getReturnType()) && m.getParameterCount() == 0) { - getFuelMethod = m; - break; - } - } - if (getFuelMethod == null) return; - - getFuelMethod.setAccessible(true); - Map nmsFuelMap = (Map) getFuelMethod.invoke(null); - if (nmsFuelMap == null || nmsFuelMap.isEmpty()) return; - Class craftMagicClass = getCraftMagicNumbersClass(); if (craftMagicClass == null) return; @@ -49,19 +36,41 @@ public static void initDynamic() { } if (getMaterialMethod == null) return; - for (Map.Entry entry : nmsFuelMap.entrySet()) { - if (entry.getKey() != null && entry.getValue() != null && entry.getValue() > 0) { - Material mat = (Material) getMaterialMethod.invoke(null, entry.getKey()); - if (mat != null) { - FUEL_TIMES.put(mat, entry.getValue()); - } + Map best = new EnumMap<>(Material.class); + for (Method candidate : furnaceClass.getDeclaredMethods()) { + if (!Modifier.isStatic(candidate.getModifiers()) || candidate.getParameterCount() != 0 || + !Map.class.isAssignableFrom(candidate.getReturnType())) continue; + try { + candidate.setAccessible(true); + Object value = candidate.invoke(null); + if (!(value instanceof Map)) continue; + Map converted = convertFuelMap((Map) value, getMaterialMethod); + if (converted.size() > best.size()) best = converted; + } catch (Throwable ignored) { + // One inaccessible or unrelated map must not hide later candidates. } } + if (!best.isEmpty()) FUEL_TIMES.putAll(best); } catch (Throwable ignored) { // Graceful fallback to default vanilla table } } + private static Map convertFuelMap(Map source, Method getMaterialMethod) { + Map converted = new EnumMap<>(Material.class); + for (Map.Entry entry : source.entrySet()) { + if (!(entry.getValue() instanceof Number) || entry.getKey() == null) continue; + int ticks = ((Number) entry.getValue()).intValue(); + if (ticks <= 0 || !getMaterialMethod.getParameterTypes()[0].isInstance(entry.getKey())) continue; + try { + Object material = getMaterialMethod.invoke(null, entry.getKey()); + if (material instanceof Material) converted.put((Material) material, ticks); + } catch (ReflectiveOperationException | IllegalArgumentException ignored) { + } + } + return converted; + } + private static @Nullable Class getFurnaceNmsClass() { String serverPkg = Bukkit.getServer() != null ? Bukkit.getServer().getClass().getPackage().getName() : ""; String[] candidates = { @@ -82,14 +91,7 @@ public static void initDynamic() { String serverPkg = Bukkit.getServer() != null ? Bukkit.getServer().getClass().getPackage().getName() : ""; String[] candidates = { serverPkg + ".util.CraftMagicNumbers", - "org.bukkit.craftbukkit.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_20_R1.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_20_R2.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_20_R3.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_19_R3.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_18_R2.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_17_R1.util.CraftMagicNumbers", - "org.bukkit.craftbukkit.v1_16_R3.util.CraftMagicNumbers" + "org.bukkit.craftbukkit.util.CraftMagicNumbers" }; for (String c : candidates) { try { @@ -257,6 +259,5 @@ public static int getFuelTime(@Nullable Material material) { } private FuelTable() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java index 8be9070..c20736d 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngine.java @@ -1,55 +1,39 @@ package io.github.eat_ram.fuream.logic; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.List; -import io.github.eat_ram.fuream.api.FureamWorldConfig; -import io.github.eat_ram.fuream.api.FurnaceType; -import io.github.eat_ram.fuream.compat.BlockCompat; +import io.github.eat_ram.fuream.ResolvedFurnaceConfig; +import io.github.eat_ram.fuream.compat.FurnaceEventDispatcher; +import io.github.eat_ram.fuream.compat.FurnaceEventCompat; import io.github.eat_ram.fuream.compat.ItemCompat; import io.github.eat_ram.fuream.compat.RecipeCompat; import io.github.eat_ram.fuream.compat.RecipeHandle; -import io.github.eat_ram.fuream.compat.ServerVersion; -import io.github.eat_ram.fuream.compat.FurnaceCompat; import io.github.eat_ram.fuream.compat.VersionAdapters; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; import io.github.eat_ram.fuream.util.KeyableItemStack; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; -import org.bukkit.block.BlockState; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; import org.bukkit.event.inventory.FurnaceBurnEvent; import org.bukkit.event.inventory.FurnaceSmeltEvent; -import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public abstract class FureamFurnaceEngine { - public static void tick(FurnaceContext ctx, FureamWorldConfig config) { +public final class FureamFurnaceEngine { + public static void tick(FurnaceContext ctx, ResolvedFurnaceConfig config) { World world = ctx.getWorld(); if (world == null) return; FureamFurnaceData data = ctx.data; - int inputCount = slotCount(config.getInputSlotCount().get(ctx.type)); - int fuelCount = slotCount(config.getFuelSlotCount().get(ctx.type)); - int outputCount = slotCount(config.getOutputSlotCount().get(ctx.type)); + int inputCount = config.inputSlots; + int fuelCount = config.fuelSlots; + int outputCount = config.outputSlots; ensureSize(data.inputs, inputCount); ensureSize(data.fuels, fuelCount); ensureSize(data.outputs, outputCount); Block block = world.getBlockAt(ctx.pos.x, ctx.pos.y, ctx.pos.z); - BlockState state = block.getState(); - if (!(state instanceof org.bukkit.block.Furnace)) return; - org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) state; - ingestVanillaLane(ctx, furnace.getInventory(), inputCount, fuelCount, outputCount); - FurnaceCompat.setCookTime(furnace, 0); - FurnaceCompat.setBurnTime(furnace, 0); - if (ctx.burnTime > 0) { ctx.burnTime--; ctx.dirty = true; @@ -88,7 +72,7 @@ public static void tick(FurnaceContext ctx, FureamWorldConfig config) { ctx.dirty = true; if (ctx.cookTime >= Math.max(1, ctx.cookTimeTotal)) { FurnaceSmeltEvent event = new FurnaceSmeltEvent(block, one(input), result.clone()); - Bukkit.getPluginManager().callEvent(event); + FurnaceEventDispatcher.call(event); ItemStack eventResult = event.getResult(); if (event.isCancelled() || ItemCompat.isEmpty(eventResult) || !canFitAll(data.outputs, eventResult, outputCount)) { @@ -112,19 +96,16 @@ public static void tick(FurnaceContext ctx, FureamWorldConfig config) { ctx.dirty = true; } - BlockCompat.setLit(block, ctx.burnTime > 0); - io.github.eat_ram.fuream.hook.FurnaceManager.refreshSessions(ctx); } private static void ignite(FurnaceContext ctx, Block block, int fuelCount) { int fuelSlot = firstFuel(ctx.data.fuels, fuelCount); if (fuelSlot < 0) return; ItemStack fuel = ctx.data.fuels.get(fuelSlot); - int fuelTime = FuelTable.getFuelTime(fuel); - if (ctx.type != FurnaceType.FURNACE) fuelTime = Math.max(1, fuelTime / 2); + int fuelTime = effectiveFuelTime(FuelTable.getFuelTime(fuel), ctx.type); FurnaceBurnEvent event = new FurnaceBurnEvent(block, one(fuel), fuelTime); - Bukkit.getPluginManager().callEvent(event); + FurnaceEventDispatcher.call(event); if (event.isCancelled() || !event.isBurning() || event.getBurnTime() <= 0) return; ctx.burnTime = event.getBurnTime(); @@ -140,22 +121,9 @@ private static void ignite(FurnaceContext ctx, Block block, int fuelCount) { } private static int fireStartSmelt(Block block, ItemStack input, RecipeHandle recipe) { - if (!ServerVersion.CURRENT.atLeast(1, 18)) return recipe.cookingTime; - try { - Class eventClass = Class.forName("org.bukkit.event.inventory.FurnaceStartSmeltEvent"); - for (Constructor constructor : eventClass.getConstructors()) { - Class[] types = constructor.getParameterTypes(); - if (types.length != 3 || !types[0].isInstance(block) || !types[1].isInstance(input) || - !types[2].isInstance(recipe.nativeRecipe)) continue; - Event event = (Event) constructor.newInstance(block, one(input), recipe.nativeRecipe); - Bukkit.getPluginManager().callEvent(event); - if (event instanceof Cancellable && ((Cancellable) event).isCancelled()) return Integer.MAX_VALUE; - Method getter = eventClass.getMethod("getTotalCookTime"); - return ((Number) getter.invoke(event)).intValue(); - } - } catch (ReflectiveOperationException | LinkageError ignored) { - } - return recipe.cookingTime; + return FurnaceEventCompat.fireStartSmelt( + block, one(input), recipe.nativeRecipe, recipe.cookingTime + ); } private static RecipeHandle getRecipeForInput(ItemStack input, FurnaceContext ctx) { @@ -185,34 +153,22 @@ static void handleWetSponge(ItemStack input, List fuels, int maxSlots } } if (bucketSlot < 0) return; - int empty = firstEmpty(fuels, maxSlots); - if (empty < 0) return; ItemStack found = fuels.get(bucketSlot); found.setAmount(found.getAmount() - 1); - if (found.getAmount() <= 0) fuels.set(bucketSlot, ItemCompat.empty()); + if (found.getAmount() <= 0) { + fuels.set(bucketSlot, new ItemStack(waterBucket)); + return; + } + int empty = firstEmpty(fuels, maxSlots); + if (empty < 0) { + found.setAmount(found.getAmount() + 1); + return; + } fuels.set(empty, new ItemStack(waterBucket)); } - private static void ingestVanillaLane( - FurnaceContext ctx, FurnaceInventory inventory, int inputCount, int fuelCount, int outputCount - ) { - ItemStack smelting = inventory.getSmelting(); - if (!ItemCompat.isEmpty(smelting)) { - ItemStack remainder = insertStackIntoList(ctx.data.inputs, smelting, inputCount); - inventory.setSmelting(ItemCompat.isEmpty(remainder) ? null : remainder); - ctx.dirty = true; - } - ItemStack fuel = inventory.getFuel(); - if (!ItemCompat.isEmpty(fuel)) { - ItemStack remainder = insertStackIntoList(ctx.data.fuels, fuel, fuelCount); - inventory.setFuel(ItemCompat.isEmpty(remainder) ? null : remainder); - ctx.dirty = true; - } - ItemStack result = inventory.getResult(); - if (!ItemCompat.isEmpty(result)) { - inventory.setResult(recoverVanillaOutput(ctx.data.outputs, result, outputCount)); - ctx.dirty = true; - } + static int effectiveFuelTime(int vanillaFuelTime, io.github.eat_ram.fuream.api.FurnaceType type) { + return Math.max(0, vanillaFuelTime); } public static ItemStack insertStackIntoList(List slots, ItemStack incoming, int maxSlots) { @@ -311,15 +267,10 @@ static boolean updateInputIdentity( return true; } - private static int slotCount(Integer configured) { - return Math.max(1, configured == null ? 9 : configured); - } - private static void ensureSize(List slots, int size) { while (slots.size() < size) slots.add(ItemCompat.empty()); } private FureamFurnaceEngine() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java index a6c1cf5..dc48250 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/FureamFurnaceLogic.java @@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public abstract class FureamFurnaceLogic { +public final class FureamFurnaceLogic { public static boolean isFuel(@Nullable ItemStack stack) { return FuelTable.isFuel(stack); } @@ -88,6 +88,5 @@ private static void dropIfPresent(World world, Location location, ItemStack stac } private FureamFurnaceLogic() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/NativeLaneProjection.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/NativeLaneProjection.java new file mode 100644 index 0000000..647735d --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/NativeLaneProjection.java @@ -0,0 +1,121 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.List; + +import io.github.eat_ram.fuream.compat.ItemCompat; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Publishes one virtual stack into a native furnace slot and merges the observed result back. */ +public final class NativeLaneProjection { + public enum SelectionMode { + FIRST_PRESENT, + BUCKET_REMAINDER + } + + public static final class State { + private int sourceSlot = -1; + private ItemStack published; + + public int getSourceSlot() { + return this.sourceSlot; + } + + public boolean isActive() { + return this.sourceSlot >= 0; + } + + public @Nullable ItemStack getPublished() { + return ItemCompat.isEmpty(this.published) ? null : this.published.clone(); + } + + public void reset() { + this.sourceSlot = -1; + this.published = null; + } + + public void restore(int sourceSlot, @Nullable ItemStack published) { + this.sourceSlot = sourceSlot; + this.published = ItemCompat.isEmpty(published) ? null : published.clone(); + } + } + + public static @Nullable ItemStack publish( + @NotNull List virtual, @Nullable ItemStack nativeStack, + @NotNull SelectionMode mode, @NotNull State state + ) { + state.reset(); + if (!ItemCompat.isEmpty(nativeStack)) { + return nativeStack.clone(); + } + int selected = selectSlot(virtual, mode); + if (selected < 0) { + return null; + } + ItemStack projected = virtual.get(selected).clone(); + state.sourceSlot = selected; + state.published = projected.clone(); + return projected; + } + + public static @Nullable ItemStack reconcile( + @NotNull List virtual, @Nullable ItemStack nativeStack, + int maxSlots, @NotNull State state + ) { + if (!state.isActive()) { + return nativeStack; + } + int source = state.sourceSlot; + ItemStack published = state.published; + state.reset(); + if (source >= 0 && source < maxSlots && source < virtual.size() && + sameStack(virtual.get(source), published)) { + virtual.set(source, ItemCompat.isEmpty(nativeStack) ? ItemCompat.empty() : nativeStack.clone()); + return null; + } + + removePublished(virtual, published, maxSlots); + if (ItemCompat.isEmpty(nativeStack)) return null; + ItemStack remainder = FureamFurnaceEngine.insertStackIntoList( + virtual, nativeStack.clone(), maxSlots + ); + return ItemCompat.isEmpty(remainder) ? null : remainder; + } + + private static void removePublished(List virtual, ItemStack published, int maxSlots) { + if (ItemCompat.isEmpty(published)) return; + int remaining = published.getAmount(); + for (int i = 0; i < Math.min(maxSlots, virtual.size()) && remaining > 0; i++) { + ItemStack current = virtual.get(i); + if (ItemCompat.isEmpty(current) || !current.isSimilar(published)) continue; + int remove = Math.min(remaining, current.getAmount()); + current.setAmount(current.getAmount() - remove); + remaining -= remove; + if (current.getAmount() <= 0) virtual.set(i, ItemCompat.empty()); + } + } + + private static boolean sameStack(ItemStack left, ItemStack right) { + return ItemCompat.isEmpty(left) && ItemCompat.isEmpty(right) || + !ItemCompat.isEmpty(left) && !ItemCompat.isEmpty(right) && + left.getAmount() == right.getAmount() && left.isSimilar(right); + } + + private static int selectSlot(List virtual, SelectionMode mode) { + for (int i = 0; i < virtual.size(); i++) { + ItemStack stack = virtual.get(i); + if (ItemCompat.isEmpty(stack)) continue; + if (mode == SelectionMode.FIRST_PRESENT || isBucket(stack)) return i; + } + return -1; + } + + private static boolean isBucket(ItemStack stack) { + String name = stack.getType().name(); + return "BUCKET".equals(name) || "WATER_BUCKET".equals(name); + } + + private NativeLaneProjection() { + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java index f047b93..93ff064 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/logic/PassiveLaneProjection.java @@ -16,13 +16,19 @@ public enum SelectionMode { public static final class State { private int sourceSlot = -1; + private ItemStack published; public int getSourceSlot() { return this.sourceSlot; } + public @Nullable ItemStack getPublished() { + return ItemCompat.isEmpty(this.published) ? null : this.published.clone(); + } + public void reset() { this.sourceSlot = -1; + this.published = null; } } @@ -46,6 +52,7 @@ public void reset() { ItemStack projected = virtual.get(selected).clone(); virtual.set(selected, ItemCompat.empty()); state.sourceSlot = selected; + state.published = projected.clone(); return projected; } @@ -108,6 +115,5 @@ private static int firstPresent(@NotNull List virtual) { } private PassiveLaneProjection() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java index 2f8d673..af76bc6 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtInstrumentation.java @@ -1,17 +1,22 @@ package io.github.eat_ram.fuream.nbt; import java.lang.instrument.Instrumentation; +import java.io.File; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.BiConsumer; +import java.util.HashMap; +import java.util.Map; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.loading.ClassInjector; import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; @@ -24,14 +29,9 @@ * Spigot otherwise discards unknown block-entity root tags during load/save. */ public final class FurnaceNbtInstrumentation { - private static final String INSTALLED_KEY = "io.github.eat_ram.fuream.root_nbt_instrumented"; + private static final String RUNTIME_NAME = FurnaceNbtRuntime.class.getName(); public static void install(JavaPlugin plugin) { - FurnaceRootNbtBridge.installCallbacks(); - if (Boolean.TRUE.equals(System.getProperties().get(INSTALLED_KEY))) { - return; - } - Class furnaceClass = findFurnaceClass(); Method loadMethod = findPersistenceMethod(furnaceClass, true); Method saveMethod = findPersistenceMethod(furnaceClass, false); @@ -56,6 +56,14 @@ public static void install(JavaPlugin plugin) { throw new IllegalStateException("Furnace block entity class is not modifiable: " + furnaceClass.getName()); } + Class runtimeClass = installRuntime(instrumentation, furnaceClass, plugin); + FurnaceRootNbtBridge.bindRuntime(runtimeClass); + try { + if (Boolean.TRUE.equals(runtimeClass.getMethod("isInstalled").invoke(null))) return; + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to query root-NBT runtime", e); + } + ElementMatcher.Junction loadMatcher = named(loadMethod.getName()) .and(takesArguments(loadMethod.getParameterTypes())); ElementMatcher.Junction saveMatcher = named(saveMethod.getName()) @@ -70,12 +78,42 @@ public static void install(JavaPlugin plugin) { .visit(Advice.to(SaveAdvice.class).on(saveMatcher))) .installOn(instrumentation); - System.getProperties().put(INSTALLED_KEY, Boolean.TRUE); + try { + runtimeClass.getMethod("markInstalled").invoke(null); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to finalize root-NBT runtime", e); + } plugin.getLogger().info( "Installed Fabric-compatible furnace root-NBT hooks on " + furnaceClass.getName() ); } + private static Class installRuntime( + Instrumentation instrumentation, Class furnaceClass, JavaPlugin plugin + ) { + try { + return Class.forName(RUNTIME_NAME, false, null); + } catch (ClassNotFoundException ignored) { + } + try { + File injectionFolder = new File(plugin.getDataFolder(), ".runtime"); + if (!injectionFolder.isDirectory() && !injectionFolder.mkdirs()) { + throw new IllegalStateException("Unable to create " + injectionFolder); + } + Map definitions = new HashMap<>(); + definitions.put( + new TypeDescription.ForLoadedType(FurnaceNbtRuntime.class), + ClassFileLocator.ForClassLoader.read(FurnaceNbtRuntime.class) + ); + ClassInjector.UsingInstrumentation.of( + injectionFolder, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, instrumentation + ).inject(definitions); + return Class.forName(RUNTIME_NAME, true, null); + } catch (Exception e) { + throw new IllegalStateException("Unable to install the root-NBT runtime", e); + } + } + private static Class findFurnaceClass() { String[] candidates = { "net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity", @@ -152,8 +190,7 @@ private static boolean hasCompoundArgument(Method method) { } public static class LoadAdvice { - @Advice.OnMethodExit - @SuppressWarnings("unchecked") + @Advice.OnMethodExit(suppress = Throwable.class) public static void exit( @Advice.This Object furnace, @Advice.AllArguments Object[] arguments @@ -168,18 +205,12 @@ public static void exit( } } if (rootNbt == null) return; - Object callback = System.getProperties().get( - "io.github.eat_ram.fuream.root_nbt_load_hook" - ); - if (callback instanceof BiConsumer) { - ((BiConsumer) callback).accept(furnace, rootNbt); - } + FurnaceNbtRuntime.capture(furnace, rootNbt, "FureamData"); } } public static class SaveAdvice { - @Advice.OnMethodExit - @SuppressWarnings("unchecked") + @Advice.OnMethodExit(suppress = Throwable.class) public static void exit( @Advice.This Object furnace, @Advice.AllArguments Object[] arguments @@ -194,16 +225,10 @@ public static void exit( } } if (rootNbt == null) return; - Object callback = System.getProperties().get( - "io.github.eat_ram.fuream.root_nbt_save_hook" - ); - if (callback instanceof BiConsumer) { - ((BiConsumer) callback).accept(furnace, rootNbt); - } + FurnaceNbtRuntime.inject(furnace, rootNbt, "FureamData"); } } private FurnaceNbtInstrumentation() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtRuntime.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtRuntime.java new file mode 100644 index 0000000..e048ab7 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceNbtRuntime.java @@ -0,0 +1,138 @@ +package io.github.eat_ram.fuream.nbt; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Pure-JDK state used by instrumented NMS classes. A second copy of this class + * is injected into the bootstrap class loader, so it must not reference Bukkit or + * plugin classes. + */ +public final class FurnaceNbtRuntime { + private static final Map STATES = + Collections.synchronizedMap(new WeakHashMap()); + private static final Map, Field> TAG_FIELDS = + Collections.synchronizedMap(new WeakHashMap, Field>()); + private static final Map, Method> SHORT_SETTERS = + Collections.synchronizedMap(new WeakHashMap, Method>()); + private static final ThreadLocal SUPPRESSED = new ThreadLocal(); + private static volatile boolean installed; + private static volatile long failureCount; + private static volatile String lastFailure = ""; + + public static void capture(Object tileEntity, Object rootNbt, String dataKey) { + try { + Object data = tags(rootNbt).get(dataKey); + STATES.put(tileEntity, new Object[] {data, rootNbt}); + } catch (Throwable failure) { + recordFailure(failure); + } + } + + public static void inject(Object tileEntity, Object rootNbt, String dataKey) { + if (Boolean.TRUE.equals(SUPPRESSED.get())) return; + try { + Object[] stored = STATES.get(tileEntity); + if (stored == null || stored.length == 0) return; + if (stored[0] == null) tags(rootNbt).remove(dataKey); + else tags(rootNbt).put(dataKey, stored[0]); + if (stored.length >= 4 && stored[1] instanceof Integer) { + setShort(rootNbt, "BurnTime", ((Integer) stored[1]).shortValue()); + setShort(rootNbt, "CookTime", ((Integer) stored[2]).shortValue()); + setShort(rootNbt, "CookTimeTotal", ((Integer) stored[3]).shortValue()); + } + } catch (Throwable failure) { + recordFailure(failure); + } + } + + public static Object[] getState(Object tileEntity) { + return STATES.get(tileEntity); + } + + public static void putState(Object tileEntity, Object[] state) { + if (tileEntity != null && state != null) STATES.put(tileEntity, state); + } + + public static void setSuppressed(boolean suppressed) { + if (suppressed) SUPPRESSED.set(Boolean.TRUE); + else SUPPRESSED.remove(); + } + + public static boolean isInstalled() { + return installed; + } + + public static void markInstalled() { + installed = true; + } + + public static long getFailureCount() { + return failureCount; + } + + public static String getLastFailure() { + return lastFailure; + } + + @SuppressWarnings("unchecked") + private static Map tags(Object compound) throws ReflectiveOperationException { + Class type = compound.getClass(); + Field field = TAG_FIELDS.get(type); + if (field == null) { + field = findTagField(type); + TAG_FIELDS.put(type, field); + } + return (Map) field.get(compound); + } + + private static Field findTagField(Class type) throws NoSuchFieldException { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && Map.class.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + } + throw new NoSuchFieldException("NBT tag map on " + type.getName()); + } + + private static void setShort(Object compound, String key, short value) + throws ReflectiveOperationException { + Class type = compound.getClass(); + Method setter = SHORT_SETTERS.get(type); + if (setter == null) { + setter = findShortSetter(type); + SHORT_SETTERS.put(type, setter); + } + setter.invoke(compound, key, value); + } + + private static Method findShortSetter(Class type) throws NoSuchMethodException { + for (Class current = type; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + Class[] parameters = method.getParameterTypes(); + if (!Modifier.isStatic(method.getModifiers()) && parameters.length == 2 && + parameters[0] == String.class && parameters[1] == short.class && + method.getReturnType() == void.class) { + method.setAccessible(true); + return method; + } + } + } + throw new NoSuchMethodException("NBT short setter on " + type.getName()); + } + + private static void recordFailure(Throwable failure) { + failureCount++; + lastFailure = failure.getClass().getName() + ": " + String.valueOf(failure.getMessage()); + } + + private FurnaceNbtRuntime() { + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java index 7a640b6..e7c9ff9 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/FurnaceRootNbtBridge.java @@ -1,52 +1,51 @@ package io.github.eat_ram.fuream.nbt; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.Collections; import java.util.Map; -import java.util.Properties; -import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.function.BiConsumer; -import io.github.eat_ram.fuream.FureamMain; import io.github.eat_ram.fuream.data.FureamData; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; +import io.github.eat_ram.fuream.logic.NativeLaneProjection; import org.bukkit.block.BlockState; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Bridges instrumented NMS furnace serialization to the Bukkit-side context. */ public final class FurnaceRootNbtBridge { - private static final String LOAD_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_load_hook"; - private static final String SAVE_HOOK_KEY = "io.github.eat_ram.fuream.root_nbt_save_hook"; - private static final String STATE_MAP_KEY = "io.github.eat_ram.fuream.root_nbt_state"; private static final String PASSIVE_PROJECTION_KEY = "PassiveProjected"; + private static final String ACTIVE_PROJECTION_KEY = "SpigotProjection"; + private static final String FUEL_SLOT_KEY = "FuelSlot"; + private static final String FUEL_AMOUNT_KEY = "FuelAmount"; + private static final String OUTPUT_SLOT_KEY = "OutputSlot"; + private static final String OUTPUT_AMOUNT_KEY = "OutputAmount"; - private static final ConcurrentMap, Field> TAG_MAP_FIELDS = new ConcurrentHashMap<>(); private static final ConcurrentMap, Method> TILE_ENTITY_METHODS = new ConcurrentHashMap<>(); - private static final ThreadLocal INJECTION_SUPPRESSED = new ThreadLocal<>(); + private static volatile Method runtimeGetState; + private static volatile Method runtimePutState; + private static volatile Method runtimeSetSuppressed; + private static volatile Method runtimeFailureCount; + private static volatile Method runtimeLastFailure; - public static void installCallbacks() { - Properties properties = System.getProperties(); - properties.put(LOAD_HOOK_KEY, (BiConsumer) FurnaceRootNbtBridge::captureLoadedNbt); - properties.put(SAVE_HOOK_KEY, (BiConsumer) FurnaceRootNbtBridge::injectSavedNbt); - stateMap(); + public static void bindRuntime(@NotNull Class runtimeClass) { + try { + runtimeGetState = runtimeClass.getMethod("getState", Object.class); + runtimePutState = runtimeClass.getMethod("putState", Object.class, Object[].class); + runtimeSetSuppressed = runtimeClass.getMethod("setSuppressed", boolean.class); + runtimeFailureCount = runtimeClass.getMethod("getFailureCount"); + runtimeLastFailure = runtimeClass.getMethod("getLastFailure"); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to bind the root-NBT runtime", e); + } } - public static boolean loadNativeData( - @NotNull BlockState state, @NotNull FurnaceContext ctx - ) { + public static boolean loadNativeData(@NotNull BlockState state, @NotNull FurnaceContext ctx) { Object tileEntity = getTileEntity(state); - if (tileEntity == null) { - return false; - } - - Object[] stored = stateMap().get(tileEntity); - if (stored == null || stored.length < 2 || stored[0] == null) { - return false; - } + if (tileEntity == null) return false; + Object[] stored = runtimeState(tileEntity); + if (stored == null || stored.length < 2 || stored[0] == null) return false; NativeNbtCompound dataNbt = new NativeNbtCompound(stored[0]); ctx.data.readNbt(ctx, dataNbt); @@ -54,6 +53,7 @@ public static boolean loadNativeData( entry.getValue().readNbt(ctx, dataNbt); } ctx.passiveProjected = hasPassiveProjectionMarker(dataNbt); + readActiveProjectionMarker(dataNbt, ctx); if (stored[1] != null && !(stored[1] instanceof Integer)) { NativeNbtCompound rootNbt = new NativeNbtCompound(stored[1]); @@ -66,50 +66,56 @@ public static boolean loadNativeData( ctx.cookTimeTotal = (Integer) stored[3]; } - stateMap().put(tileEntity, new Object[] { + putRuntimeState(tileEntity, new Object[] { stored[0], ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal }); return true; } - public static void store( - @NotNull BlockState state, @NotNull FurnaceContext ctx - ) { + public static void store(@NotNull BlockState state, @NotNull FurnaceContext ctx) { Object tileEntity = getTileEntity(state); if (tileEntity == null) { throw new IllegalStateException("Unable to access the live furnace block entity"); } NativeNbtCompound dataNbt = NativeNbtCompound.createForServer(); - ctx.data.writeNbt(ctx, dataNbt); - for (FureamData extra : ctx.extraData.values()) { - extra.writeNbt(ctx, dataNbt); - } - + for (FureamData extra : ctx.extraData.values()) extra.writeNbt(ctx, dataNbt); + if (ctx.passiveProjected) dataNbt.setInt(PASSIVE_PROJECTION_KEY, 1); + writeActiveProjectionMarker(dataNbt, ctx); boolean dataEmpty = dataNbt.keys().isEmpty(); - if (ctx.passiveProjected) { - dataNbt.setInt(PASSIVE_PROJECTION_KEY, 1); - } - stateMap().put(tileEntity, new Object[] { - shouldKeepDataTag(dataEmpty, ctx.passiveProjected) + putRuntimeState(tileEntity, new Object[] { + shouldKeepDataTag(dataEmpty, ctx.passiveProjected || ctx.activeProjected) ? dataNbt.raw() : null, ctx.burnTime, ctx.cookTime, ctx.cookTimeTotal }); + markTileDirty(tileEntity); + } + + public static void updateProjectionMarker(@NotNull BlockState state, @NotNull FurnaceContext ctx) { + Object tileEntity = getTileEntity(state); + if (tileEntity == null) return; + Object[] stored = runtimeState(tileEntity); + if (stored == null || stored.length == 0 || stored[0] == null) { + if (ctx.activeProjected) store(state, ctx); + return; + } + writeActiveProjectionMarker(new NativeNbtCompound(stored[0]), ctx); + markTileDirty(tileEntity); } public static boolean hasPassiveProjectionMarker(@Nullable NativeNbtCompound dataNbt) { return dataNbt != null && dataNbt.getInt(PASSIVE_PROJECTION_KEY, 0) != 0; } - static boolean shouldKeepDataTag(boolean dataEmpty, boolean passiveProjected) { - return !dataEmpty || passiveProjected; + static boolean shouldKeepDataTag(boolean dataEmpty, boolean projected) { + return !dataEmpty || projected; } public static @Nullable NativeNbtCompound capturedRoot(@NotNull BlockState state) { Object tileEntity = getTileEntity(state); if (tileEntity == null) return null; - Object[] stored = stateMap().get(tileEntity); + Object[] stored = runtimeState(tileEntity); if (stored == null || stored.length < 2 || stored[1] == null || stored[1] instanceof Integer) { return null; } @@ -117,85 +123,61 @@ static boolean shouldKeepDataTag(boolean dataEmpty, boolean passiveProjected) { } public static void withoutInjection(@NotNull Runnable action) { - INJECTION_SUPPRESSED.set(Boolean.TRUE); + setRuntimeSuppressed(true); try { action.run(); } finally { - INJECTION_SUPPRESSED.remove(); + setRuntimeSuppressed(false); } } - private static void captureLoadedNbt(Object tileEntity, Object rootNbt) { + public static long failureCount() { try { - Object fureamData = tagMap(rootNbt).get(FureamMain.DATA_KEY); - stateMap().put(tileEntity, new Object[] {fureamData, rootNbt}); + Method method = runtimeFailureCount; + return method == null ? 0L : ((Number) method.invoke(null)).longValue(); } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Unable to capture furnace root NBT", e); + return -1L; } } - private static void injectSavedNbt(Object tileEntity, Object rootNbt) { - if (Boolean.TRUE.equals(INJECTION_SUPPRESSED.get())) { - return; - } + public static @NotNull String lastFailure() { try { - Object[] stored = stateMap().get(tileEntity); - if (stored == null || stored.length == 0) { - return; - } - - if (stored[0] != null) tagMap(rootNbt).put(FureamMain.DATA_KEY, stored[0]); - else tagMap(rootNbt).remove(FureamMain.DATA_KEY); - if (stored.length < 4 || !(stored[1] instanceof Integer)) { - return; - } - NativeNbtCompound root = new NativeNbtCompound(rootNbt); - root.setShort("BurnTime", (short) (int) (Integer) stored[1]); - root.setShort("CookTime", (short) (int) (Integer) stored[2]); - root.setShort("CookTimeTotal", (short) (int) (Integer) stored[3]); + Method method = runtimeLastFailure; + Object value = method == null ? "runtime unavailable" : method.invoke(null); + return value == null ? "" : value.toString(); } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Unable to inject furnace root NBT", e); + return e.toString(); } } - @SuppressWarnings("unchecked") - private static @NotNull Map stateMap() { - Properties properties = System.getProperties(); - synchronized (properties) { - Object existing = properties.get(STATE_MAP_KEY); - if (existing instanceof Map) { - return (Map) existing; - } - Map created = Collections.synchronizedMap(new WeakHashMap<>()); - properties.put(STATE_MAP_KEY, created); - return created; + private static @Nullable Object[] runtimeState(Object tileEntity) { + Method method = runtimeGetState; + if (method == null) return null; + try { + return (Object[]) method.invoke(null, tileEntity); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to read root-NBT runtime state", e); } } - @SuppressWarnings("unchecked") - private static Map tagMap(Object compound) throws ReflectiveOperationException { - Field field = TAG_MAP_FIELDS.get(compound.getClass()); - if (field == null) { - field = findTagMapField(compound.getClass()); - TAG_MAP_FIELDS.put(compound.getClass(), field); + private static void putRuntimeState(Object tileEntity, Object[] state) { + Method method = runtimePutState; + if (method == null) throw new IllegalStateException("Root-NBT runtime is unavailable"); + try { + method.invoke(null, tileEntity, (Object) state); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to update root-NBT runtime state", e); } - return (Map) field.get(compound); } - private static Field findTagMapField(Class compoundClass) { - for (Field field : compoundClass.getFields()) { - if (Map.class.isAssignableFrom(field.getType())) { - field.setAccessible(true); - return field; - } - } - for (Field field : compoundClass.getDeclaredFields()) { - if (Map.class.isAssignableFrom(field.getType())) { - field.setAccessible(true); - return field; - } + private static void setRuntimeSuppressed(boolean suppressed) { + Method method = runtimeSetSuppressed; + if (method == null) return; + try { + method.invoke(null, suppressed); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to change root-NBT runtime scope", e); } - throw new IllegalStateException("Unable to locate NBT compound tag map on " + compoundClass.getName()); } private static @Nullable Object getTileEntity(@NotNull BlockState state) { @@ -226,13 +208,70 @@ private static Field findTagMapField(Class compoundClass) { throw new NoSuchMethodException("No live tile entity accessor on " + stateClass.getName()); } - private static int readNumericTag( - @Nullable NativeNbtCompound nbt, @NotNull String key, int fallback - ) { + private static void markTileDirty(Object tileEntity) { + for (String name : new String[] {"setChanged", "markDirty"}) { + try { + Method method = tileEntity.getClass().getMethod(name); + method.invoke(tileEntity); + return; + } catch (ReflectiveOperationException ignored) { + } + } + } + + private static int readNumericTag(@Nullable NativeNbtCompound nbt, @NotNull String key, int fallback) { return nbt == null ? fallback : nbt.getInt(key, fallback); } + private static void writeActiveProjectionMarker( + @NotNull NativeNbtCompound dataNbt, @NotNull FurnaceContext ctx + ) { + if (!ctx.activeProjected) { + dataNbt.remove(ACTIVE_PROJECTION_KEY); + return; + } + NativeNbtCompound marker = dataNbt.getOrCreateCompound(ACTIVE_PROJECTION_KEY); + writeLaneMarker(marker, FUEL_SLOT_KEY, FUEL_AMOUNT_KEY, ctx.activeFuel); + writeLaneMarker(marker, OUTPUT_SLOT_KEY, OUTPUT_AMOUNT_KEY, ctx.activeOutput); + } + + private static void writeLaneMarker( + NativeNbtCompound marker, String slotKey, String amountKey, NativeLaneProjection.State state + ) { + ItemStack published = state.getPublished(); + marker.setInt(slotKey, state.getSourceSlot()); + marker.setInt(amountKey, published == null ? 0 : published.getAmount()); + } + + private static void readActiveProjectionMarker( + @NotNull NativeNbtCompound dataNbt, @NotNull FurnaceContext ctx + ) { + NativeNbtCompound marker = dataNbt.getCompound(ACTIVE_PROJECTION_KEY); + if (marker == null) return; + boolean fuel = restoreLaneMarker( + marker, FUEL_SLOT_KEY, FUEL_AMOUNT_KEY, ctx.data.fuels, ctx.activeFuel + ); + boolean output = restoreLaneMarker( + marker, OUTPUT_SLOT_KEY, OUTPUT_AMOUNT_KEY, ctx.data.outputs, ctx.activeOutput + ); + ctx.activeProjected = fuel || output; + } + + private static boolean restoreLaneMarker( + NativeNbtCompound marker, String slotKey, String amountKey, + java.util.List lane, NativeLaneProjection.State state + ) { + int slot = marker.getInt(slotKey, -1); + int amount = marker.getInt(amountKey, 0); + if (slot < 0 || slot >= lane.size() || amount <= 0) return false; + ItemStack stack = lane.get(slot); + if (io.github.eat_ram.fuream.compat.ItemCompat.isEmpty(stack) || stack.getAmount() != amount) { + return false; + } + state.restore(slot, stack); + return true; + } + private FurnaceRootNbtBridge() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java index 044965c..005b844 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeItemNbt.java @@ -12,6 +12,8 @@ public final class NativeItemNbt { private static Method asNmsCopy; private static Method asBukkitCopy; + private static Method writeMethod; + private static Method readMethod; private static Object registryProvider; public static NativeNbtCompound write(ItemStack stack) { @@ -20,6 +22,7 @@ public static NativeNbtCompound write(ItemStack stack) { ensureCraftMethods(); Object nms = asNmsCopy.invoke(null, stack); NativeNbtCompound target = NativeNbtCompound.createForServer(); + if (writeMethod != null) return invokeWrite(nms, target, writeMethod); for (Method method : nms.getClass().getMethods()) { if (Modifier.isStatic(method.getModifiers())) continue; Object[] args = serializationArguments(method.getParameterTypes(), target.raw()); @@ -29,6 +32,7 @@ public static NativeNbtCompound write(ItemStack stack) { !target.raw().getClass().isAssignableFrom(returnType)) continue; try { Object result = method.invoke(nms, args); + writeMethod = method; if (result != null && target.raw().getClass().isInstance(result)) { return new NativeNbtCompound(result); } @@ -36,9 +40,9 @@ public static NativeNbtCompound write(ItemStack stack) { } catch (ReflectiveOperationException | IllegalArgumentException ignored) { } } - throw new IllegalStateException("Unable to locate native ItemStack save method"); + throw new NbtCodecException("Unable to locate native ItemStack save method"); } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Unable to serialize Bukkit ItemStack", e); + throw new NbtCodecException("Unable to serialize Bukkit ItemStack", e); } } @@ -47,6 +51,8 @@ public static ItemStack read(NativeNbtCompound compound) { try { ensureCraftMethods(); Class nmsClass = asNmsCopy.getReturnType(); + if (readMethod != null) return invokeRead(compound, nmsClass, readMethod); + Throwable lastFailure = null; for (Method method : nmsClass.getMethods()) { if (!Modifier.isStatic(method.getModifiers())) continue; Object[] args = serializationArguments(method.getParameterTypes(), compound.raw()); @@ -57,14 +63,54 @@ public static ItemStack read(NativeNbtCompound compound) { if (result instanceof Optional) result = ((Optional) result).orElse(null); if (result != null && nmsClass.isInstance(result)) { ItemStack stack = (ItemStack) asBukkitCopy.invoke(null, result); - return stack == null ? ItemCompat.empty() : stack; + if (stack != null && !ItemCompat.isEmpty(stack)) { + readMethod = method; + return stack; + } } - } catch (ReflectiveOperationException | IllegalArgumentException ignored) { + } catch (ReflectiveOperationException | IllegalArgumentException failure) { + lastFailure = failure; } } - } catch (ReflectiveOperationException ignored) { + throw new NbtCodecException("Unable to locate native ItemStack load method", lastFailure); + } catch (ReflectiveOperationException e) { + throw new NbtCodecException("Unable to deserialize native ItemStack", e); + } + } + + public static void initialize() { + try { + ensureCraftMethods(); + } catch (ReflectiveOperationException e) { + throw new NbtCodecException("Unable to initialize CraftItemStack conversion", e); + } + } + + private static NativeNbtCompound invokeWrite( + Object nms, NativeNbtCompound target, Method method + ) throws ReflectiveOperationException { + Object[] args = serializationArguments(method.getParameterTypes(), target.raw()); + if (args == null) throw new NbtCodecException("Cached ItemStack save method is no longer compatible"); + Object result = method.invoke(nms, args); + return result != null && target.raw().getClass().isInstance(result) + ? new NativeNbtCompound(result) : target; + } + + private static ItemStack invokeRead( + NativeNbtCompound compound, Class nmsClass, Method method + ) throws ReflectiveOperationException { + Object[] args = serializationArguments(method.getParameterTypes(), compound.raw()); + if (args == null) throw new NbtCodecException("Cached ItemStack load method is no longer compatible"); + Object result = method.invoke(null, args); + if (result instanceof Optional) result = ((Optional) result).orElse(null); + if (result == null || !nmsClass.isInstance(result)) { + throw new NbtCodecException("Native ItemStack load method returned no item"); + } + ItemStack stack = (ItemStack) asBukkitCopy.invoke(null, result); + if (stack == null || ItemCompat.isEmpty(stack)) { + throw new NbtCodecException("Native ItemStack decoded as an empty item"); } - return ItemCompat.empty(); + return stack; } private static Object[] serializationArguments(Class[] parameterTypes, Object compound) { @@ -116,6 +162,5 @@ private static Object registryProvider(Class expectedType) { } private NativeItemNbt() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java index 3237964..543f6d9 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtCompound.java @@ -17,6 +17,8 @@ public final class NativeNbtCompound { private static final Map, Field> MAP_FIELDS = new ConcurrentHashMap<>(); private static final Map SETTERS = new ConcurrentHashMap<>(); private static final Map, Class> LIST_CLASSES = new ConcurrentHashMap<>(); + private static final Map, Field> PRIMITIVE_FIELDS = new ConcurrentHashMap<>(); + private static volatile Class serverCompoundClass; private final Object raw; @@ -40,6 +42,8 @@ public static NativeNbtCompound create(Class compoundClass) { } public static NativeNbtCompound createForServer() { + Class cached = serverCompoundClass; + if (cached != null) return create(cached); ClassLoader loader = Bukkit.getServer().getClass().getClassLoader(); String craftPackage = Bukkit.getServer().getClass().getPackage().getName(); String version = craftPackage.substring(craftPackage.lastIndexOf('.') + 1); @@ -50,7 +54,9 @@ public static NativeNbtCompound createForServer() { }; for (String candidate : candidates) { try { - return create(Class.forName(candidate, false, loader)); + Class located = Class.forName(candidate, false, loader); + serverCompoundClass = located; + return create(located); } catch (ClassNotFoundException ignored) { } } @@ -214,6 +220,13 @@ private static boolean isCompound(Object value) { private static Object primitiveValue(Object tag) { if (tag == null) return null; + Field cached = PRIMITIVE_FIELDS.get(tag.getClass()); + if (cached != null) { + try { + return cached.get(tag); + } catch (IllegalAccessException ignored) { + } + } for (Class current = tag.getClass(); current != null; current = current.getSuperclass()) { for (Field field : current.getDeclaredFields()) { if (Modifier.isStatic(field.getModifiers())) continue; @@ -221,6 +234,7 @@ private static Object primitiveValue(Object tag) { if (type == String.class || type.isPrimitive() || Number.class.isAssignableFrom(type)) { try { field.setAccessible(true); + PRIMITIVE_FIELDS.put(tag.getClass(), field); return field.get(tag); } catch (IllegalAccessException ignored) { } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java index c45c1c1..9c22f14 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NativeNbtList.java @@ -2,12 +2,14 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public final class NativeNbtList { private static final Map, Field> LIST_FIELDS = new ConcurrentHashMap<>(); + private static final Map, Method> ADD_METHODS = new ConcurrentHashMap<>(); private final Object raw; NativeNbtList(Object raw) { @@ -30,11 +32,21 @@ public void add(NativeNbtCompound compound) { list.add(compound.raw()); return; } - for (java.lang.reflect.Method method : raw.getClass().getMethods()) { + Method cached = ADD_METHODS.get(raw.getClass()); + if (cached != null) { + try { + cached.invoke(raw, compound.raw()); + return; + } catch (ReflectiveOperationException ignored) { + ADD_METHODS.remove(raw.getClass()); + } + } + for (Method method : raw.getClass().getMethods()) { Class[] parameters = method.getParameterTypes(); if (parameters.length == 1 && parameters[0].isInstance(compound.raw())) { try { method.invoke(raw, compound.raw()); + ADD_METHODS.put(raw.getClass(), method); return; } catch (ReflectiveOperationException ignored) { } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtCodecException.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtCodecException.java new file mode 100644 index 0000000..ceea233 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtCodecException.java @@ -0,0 +1,12 @@ +package io.github.eat_ram.fuream.nbt; + +/** Indicates that a non-empty native item tag could not be decoded safely. */ +public final class NbtCodecException extends IllegalStateException { + public NbtCodecException(String message) { + super(message); + } + + public NbtCodecException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtSelfTest.java b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtSelfTest.java new file mode 100644 index 0000000..3f7e3a4 --- /dev/null +++ b/spigot/src/main/java/io/github/eat_ram/fuream/nbt/NbtSelfTest.java @@ -0,0 +1,49 @@ +package io.github.eat_ram.fuream.nbt; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +/** Startup round trips for the NBT primitives required before any furnace is adopted. */ +public final class NbtSelfTest { + public static void run() { + NativeItemNbt.initialize(); + NativeNbtCompound compound = NativeNbtCompound.createForServer(); + compound.setInt("Int", 37); + compound.setString("String", "fuream"); + compound.setFloat("Float", 1.25f); + NativeNbtList list = compound.resetList("List"); + NativeNbtCompound child = NativeNbtCompound.createForServer(); + child.setInt("Value", 9); + list.add(child); + if (compound.getInt("Int", -1) != 37 || !"fuream".equals(compound.getString("String")) || + Math.abs(compound.getFloat("Float", 0f) - 1.25f) > 0.0001f || + list.size() != 1 || list.getCompound(0).getInt("Value", -1) != 9) { + throw new IllegalStateException("Native NBT primitive/list round trip failed"); + } + + ItemStack plain = new ItemStack(Material.STONE, 3); + assertItemRoundTrip(plain, false); + ItemStack named = plain.clone(); + ItemMeta meta = named.getItemMeta(); + if (meta != null) { + meta.setDisplayName("Fuream NBT self-test"); + named.setItemMeta(meta); + assertItemRoundTrip(named, true); + } + } + + private static void assertItemRoundTrip(ItemStack expected, boolean checkName) { + ItemStack actual = NativeItemNbt.read(NativeItemNbt.write(expected)); + if (actual.getType() != expected.getType() || actual.getAmount() != expected.getAmount()) { + throw new IllegalStateException("Native item NBT round trip changed the item"); + } + if (checkName && (!actual.hasItemMeta() || !actual.getItemMeta().hasDisplayName() || + !expected.getItemMeta().getDisplayName().equals(actual.getItemMeta().getDisplayName()))) { + throw new IllegalStateException("Native item NBT round trip lost item metadata"); + } + } + + private NbtSelfTest() { + } +} diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java index e6861fb..9d564b8 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenHandler.java @@ -10,6 +10,9 @@ import io.github.eat_ram.fuream.util.FurnacePos; import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.entity.Item; +import org.bukkit.Bukkit; +import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; @@ -17,7 +20,7 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; -public class FureamScreenHandler { +public final class FureamScreenHandler { private final @NotNull FureamScreenInventory display; public FureamScreenHandler(@NotNull FureamScreenInventory display) { @@ -32,17 +35,22 @@ public void handleClick(InventoryClickEvent event, Player player) { if (rawSlot < 0) { return; } + if ("UNKNOWN".equals(action.name()) || "CLONE_STACK".equals(action.name()) || + "CREATIVE".equals(clickType.name())) { + event.setCancelled(rawSlot < FureamScreenInventory.SIZE); + return; + } if (clickType == ClickType.DOUBLE_CLICK) { event.setCancelled(true); - this.collectMatchingToCursor(event, player); + boolean changed = this.collectMatchingToCursor(event, player); this.display.syncToBukkitInventory(); - this.markDirtyAndSave(); + if (changed) this.markDirty(); return; } // Click in player inventory with shift-click into custom GUI - if (rawSlot >= 54) { + if (rawSlot >= FureamScreenInventory.SIZE) { if (event.isShiftClick()) { event.setCancelled(true); ItemStack current = event.getCurrentItem(); @@ -64,7 +72,8 @@ public void handleClick(InventoryClickEvent event, Player player) { FureamFunctionalArea area = this.display.functionalAreas.get(ca); area.onGuiClick( (sIdx, btn, ct, p) -> this.handleFunctionalSlotDefault(rawSlot, sIdx, event, p), - this.display, rawSlot - 45, event.getHotbarButton(), clickType, player + this.display, rawSlot - FureamScreenInventory.FUNCTIONAL_START, + event.getHotbarButton(), clickType, player ); this.display.syncToBukkitInventory(); } @@ -73,7 +82,7 @@ public void handleClick(InventoryClickEvent event, Player player) { if (this.display.isBarOrDecorSlot(rawSlot)) { switch (rawSlot) { - case 51: { + case FureamScreenInventory.NEXT_FUNCTIONAL_SLOT: { int size = this.display.functionalAreas.size(); if (size > 0) { int na = this.display.getCurrentFunctionalArea() + 1; @@ -81,10 +90,10 @@ public void handleClick(InventoryClickEvent event, Player player) { } break; } - case 52: + case FureamScreenInventory.PREVIOUS_PAGE_SLOT: this.display.switchPage(this.display.getPage() - 1); break; - case 53: + case FureamScreenInventory.NEXT_PAGE_SLOT: this.display.switchPage(this.display.getPage() + 1); break; } @@ -107,24 +116,29 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC int ca = this.display.getCurrentFunctionalArea(); if (ca >= this.display.functionalAreas.size()) return; FureamFunctionalArea area = this.display.functionalAreas.get(ca); - if (!area.isStorable(this.display, rawSlot - 45) && !area.isTakable(this.display, rawSlot - 45)) return; + int functionalSlot = rawSlot - FureamScreenInventory.FUNCTIONAL_START; + if (!area.isStorable(this.display, functionalSlot) && !area.isTakable(this.display, functionalSlot)) return; ItemStack cursor = event.getCursor(); - ItemStack current = area.getGuiStack(this.display, rawSlot - 45); + ItemStack current = area.getGuiStack(this.display, functionalSlot); ClickType clickType = event.getClick(); // 1. Shift click if (event.isShiftClick()) { - if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { + boolean changed = false; + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, functionalSlot)) { + int before = current.getAmount(); Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { - area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + area.setGuiStack(this.display, functionalSlot, new ItemStack(Material.AIR)); + changed = true; } else { current.setAmount(leftover.values().iterator().next().getAmount()); - area.setGuiStack(this.display, rawSlot - 45, current); + area.setGuiStack(this.display, functionalSlot, current); + changed = current.getAmount() != before; } } - this.markDirtyAndSave(); + if (changed) this.markDirty(); return; } @@ -134,51 +148,53 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC if (hotbarSlot >= 0 && hotbarSlot < 9) { ItemStack hotbarItem = player.getInventory().getItem(hotbarSlot); if (ItemCompat.isEmpty(hotbarItem)) { - if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, functionalSlot)) { player.getInventory().setItem(hotbarSlot, current.clone()); - area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + area.setGuiStack(this.display, functionalSlot, new ItemStack(Material.AIR)); + this.markDirty(); } - } else if (area.isStorable(this.display, rawSlot - 45)) { + } else if (area.isStorable(this.display, functionalSlot)) { if (ItemCompat.isEmpty(current)) { - area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); + area.setGuiStack(this.display, functionalSlot, hotbarItem.clone()); player.getInventory().setItem(hotbarSlot, new ItemStack(Material.AIR)); - } else if (area.isTakable(this.display, rawSlot - 45)) { - area.setGuiStack(this.display, rawSlot - 45, hotbarItem.clone()); + this.markDirty(); + } else if (area.isTakable(this.display, functionalSlot)) { + area.setGuiStack(this.display, functionalSlot, hotbarItem.clone()); player.getInventory().setItem(hotbarSlot, current.clone()); + this.markDirty(); } } } - this.markDirtyAndSave(); return; } // 3. Cursor empty -> Take item from slot if (ItemCompat.isEmpty(cursor)) { - if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, rawSlot - 45)) { + if (!ItemCompat.isEmpty(current) && area.isTakable(this.display, functionalSlot)) { if (event.isRightClick()) { int half = (current.getAmount() + 1) / 2; ItemStack take = current.clone(); take.setAmount(half); current.setAmount(current.getAmount() - half); event.getView().setCursor(take); - area.setGuiStack(this.display, rawSlot - 45, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); + area.setGuiStack(this.display, functionalSlot, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); } else { event.getView().setCursor(current.clone()); - area.setGuiStack(this.display, rawSlot - 45, new ItemStack(Material.AIR)); + area.setGuiStack(this.display, functionalSlot, new ItemStack(Material.AIR)); } } } else { // 4. Cursor has item -> Place/Swap into slot - if (area.isStorable(this.display, rawSlot - 45)) { + if (area.isStorable(this.display, functionalSlot)) { if (ItemCompat.isEmpty(current)) { if (event.isRightClick()) { ItemStack putOne = cursor.clone(); putOne.setAmount(1); cursor.setAmount(cursor.getAmount() - 1); - area.setGuiStack(this.display, rawSlot - 45, putOne); + area.setGuiStack(this.display, functionalSlot, putOne); event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); } else { - area.setGuiStack(this.display, rawSlot - 45, cursor.clone()); + area.setGuiStack(this.display, functionalSlot, cursor.clone()); event.getView().setCursor(new ItemStack(Material.AIR)); } } else if (current.isSimilar(cursor)) { @@ -187,7 +203,7 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC if (current.getAmount() < max) { current.setAmount(current.getAmount() + 1); cursor.setAmount(cursor.getAmount() - 1); - area.setGuiStack(this.display, rawSlot - 45, current); + area.setGuiStack(this.display, functionalSlot, current); event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); } } else { @@ -196,25 +212,24 @@ private void handleFunctionalSlotDefault(int rawSlot, int targetSlot, InventoryC if (move > 0) { current.setAmount(current.getAmount() + move); cursor.setAmount(cursor.getAmount() - move); - area.setGuiStack(this.display, rawSlot - 45, current); + area.setGuiStack(this.display, functionalSlot, current); event.getView().setCursor(cursor.getAmount() > 0 ? cursor : new ItemStack(Material.AIR)); } } - } else if (area.isTakable(this.display, rawSlot - 45)) { - area.setGuiStack(this.display, rawSlot - 45, cursor.clone()); + } else if (area.isTakable(this.display, functionalSlot)) { + area.setGuiStack(this.display, functionalSlot, cursor.clone()); event.getView().setCursor(current.clone()); } } } - this.markDirtyAndSave(); + this.markDirty(); } - private void markDirtyAndSave() { + private void markDirty() { FurnacePos pos = new FurnacePos(this.display.location); - FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); + FurnaceContext ctx = FurnaceManager.getContext(pos); if (ctx != null) { ctx.dirty = true; - FurnaceManager.saveToNbt(ctx); } } @@ -234,7 +249,7 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player this.display.setStack(slotIndex, new ItemStack(Material.AIR)); } this.notifyExtract(player, extractIdentity, moved); - this.markDirtyAndSave(); + if (moved > 0) this.markDirty(); return; } @@ -242,11 +257,11 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player moved = event.getClick() == ClickType.DROP ? 1 : current.getAmount(); ItemStack dropped = current.clone(); dropped.setAmount(moved); - player.getWorld().dropItemNaturally(player.getLocation(), dropped); + if (!this.dropThroughEvent(player, dropped)) return; current.setAmount(current.getAmount() - moved); this.display.setStack(slotIndex, current.getAmount() > 0 ? current : new ItemStack(Material.AIR)); this.notifyExtract(player, dropped, moved); - this.markDirtyAndSave(); + this.markDirty(); return; } @@ -261,7 +276,7 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player } moved = before - (leftover.isEmpty() ? 0 : current.getAmount()); this.notifyExtract(player, extractIdentity, moved); - this.markDirtyAndSave(); + if (moved > 0) this.markDirty(); return; } @@ -286,12 +301,12 @@ private void onOutputSlotClick(int slotIndex, InventoryClickEvent event, Player } } this.notifyExtract(player, extractIdentity, moved); - this.markDirtyAndSave(); + if (moved > 0) this.markDirty(); } - private void collectMatchingToCursor(InventoryClickEvent event, Player player) { + private boolean collectMatchingToCursor(InventoryClickEvent event, Player player) { ItemStack cursor = event.getCursor(); - if (ItemCompat.isEmpty(cursor)) return; + if (ItemCompat.isEmpty(cursor)) return false; int space = cursor.getMaxStackSize() - cursor.getAmount(); int extracted = 0; for (int i = 0; i < this.display.data.outputs.size() && space > 0; i++) { @@ -315,6 +330,7 @@ private void collectMatchingToCursor(InventoryClickEvent event, Player player) { } event.getView().setCursor(cursor); this.notifyExtract(player, cursor, extracted); + return extracted > 0; } private void notifyExtract(Player player, ItemStack stack, int amount) { @@ -325,12 +341,53 @@ private void notifyExtract(Player player, ItemStack stack, int amount) { } } + private boolean dropThroughEvent(Player player, ItemStack stack) { + Item entity = player.getWorld().dropItem(player.getEyeLocation(), stack.clone()); + PlayerDropItemEvent event = new PlayerDropItemEvent(player, entity); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + entity.remove(); + return false; + } + return true; + } + private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Player player) { boolean isFuel = this.display.isFuelRowSlot(slotIndex); ItemStack cursor = event.getCursor(); ItemStack current = this.display.getStack(slotIndex); + if (event.getClick() == ClickType.NUMBER_KEY) { + int hotbarSlot = event.getHotbarButton(); + if (hotbarSlot < 0 || hotbarSlot >= 9) return; + ItemStack hotbar = player.getInventory().getItem(hotbarSlot); + if (!ItemCompat.isEmpty(hotbar) && !this.display.isValid(slotIndex, hotbar)) return; + player.getInventory().setItem( + hotbarSlot, ItemCompat.isEmpty(current) ? null : current.clone() + ); + this.display.setStack( + slotIndex, ItemCompat.isEmpty(hotbar) ? ItemCompat.empty() : hotbar.clone() + ); + this.markDirty(); + return; + } + + if (event.getClick() == ClickType.DROP || event.getClick() == ClickType.CONTROL_DROP) { + if (ItemCompat.isEmpty(current)) return; + int amount = event.getClick() == ClickType.DROP ? 1 : current.getAmount(); + ItemStack dropped = current.clone(); + dropped.setAmount(amount); + if (!this.dropThroughEvent(player, dropped)) return; + current.setAmount(current.getAmount() - amount); + this.display.setStack( + slotIndex, current.getAmount() <= 0 ? ItemCompat.empty() : current + ); + this.markDirty(); + return; + } + if (event.isShiftClick()) { + int before = ItemCompat.isEmpty(current) ? 0 : current.getAmount(); if (!ItemCompat.isEmpty(current)) { Map leftover = player.getInventory().addItem(current.clone()); if (leftover.isEmpty()) { @@ -340,7 +397,9 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe this.display.setStack(slotIndex, current); } } - this.markDirtyAndSave(); + ItemStack afterStack = this.display.getStack(slotIndex); + int after = ItemCompat.isEmpty(afterStack) ? 0 : afterStack.getAmount(); + if (before != after) this.markDirty(); return; } @@ -399,17 +458,18 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe event.getView().setCursor(current.clone()); } } - this.markDirtyAndSave(); + this.markDirty(); } private @NotNull ItemStack quickMoveToContainer(@NotNull ItemStack original) { ItemStack remaining = original.clone(); + int originalAmount = original.getAmount(); if (FureamFurnaceLogic.isAcceptableInput(this.display.location.getWorld(), remaining, this.display.furnaceType)) { int stop = this.display.getInputSlotStop(); for (int i = 0; i < stop; i++) { remaining = this.insertIntoSlot(i, remaining); if (ItemCompat.isEmpty(remaining)) { - this.markDirtyAndSave(); + this.markDirty(); return new ItemStack(Material.AIR); } } @@ -422,12 +482,12 @@ private void onMaterialSlotClick(int slotIndex, InventoryClickEvent event, Playe for (int i = fuelStart; i < stop; i++) { remaining = this.insertIntoSlot(i, remaining); if (ItemCompat.isEmpty(remaining)) { - this.markDirtyAndSave(); + this.markDirty(); return new ItemStack(Material.AIR); } } } - this.markDirtyAndSave(); + if (remaining.getAmount() != originalAmount) this.markDirty(); return remaining; } @@ -454,7 +514,7 @@ public void handleDrag(@NotNull InventoryDragEvent event) { boolean touchesDisplay = false; for (Map.Entry entry : event.getNewItems().entrySet()) { int rawSlot = entry.getKey(); - if (rawSlot >= 54) continue; + if (rawSlot >= FureamScreenInventory.SIZE) continue; touchesDisplay = true; if (!this.display.isValid(rawSlot, entry.getValue())) { event.setCancelled(true); @@ -462,10 +522,17 @@ public void handleDrag(@NotNull InventoryDragEvent event) { } } if (!touchesDisplay) return; + event.setCancelled(true); for (Map.Entry entry : event.getNewItems().entrySet()) { - if (entry.getKey() < 54) this.display.setStack(entry.getKey(), entry.getValue().clone()); + int rawSlot = entry.getKey(); + if (rawSlot < FureamScreenInventory.SIZE) { + this.display.setStack(rawSlot, entry.getValue().clone()); + } else { + event.getView().setItem(rawSlot, entry.getValue().clone()); + } } - this.markDirtyAndSave(); + event.getView().setCursor(ItemCompat.isEmpty(event.getCursor()) ? null : event.getCursor().clone()); + this.markDirty(); this.display.refreshVisuals(); } } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java index 2bfc11b..4a3b05f 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/screen/FureamScreenInventory.java @@ -1,12 +1,17 @@ package io.github.eat_ram.fuream.screen; import java.util.ArrayList; +import java.util.HashSet; +import java.util.IllegalFormatException; import java.util.List; +import java.util.Set; import java.util.function.BiFunction; import java.util.UUID; import io.github.eat_ram.fuream.api.FureamWorldConfig; import io.github.eat_ram.fuream.api.FurnaceType; +import io.github.eat_ram.fuream.FureamMain; +import io.github.eat_ram.fuream.ResolvedFurnaceConfig; import io.github.eat_ram.fuream.data.FureamFurnaceData; import io.github.eat_ram.fuream.hook.FurnaceManager; import io.github.eat_ram.fuream.hook.FurnaceManager.FurnaceContext; @@ -38,9 +43,19 @@ * 45..53 functional area & decor (row 6) * */ -public class FureamScreenInventory implements InventoryHolder { +public final class FureamScreenInventory implements InventoryHolder { public static final int COLS = 9; public static final int SIZE = 54; + public static final int INPUT_START = 0; + public static final int FUEL_BAR_START = 9; + public static final int FUEL_START = 18; + public static final int PROGRESS_BAR_START = 27; + public static final int OUTPUT_START = 36; + public static final int FUNCTIONAL_START = 45; + public static final int NEXT_FUNCTIONAL_SLOT = 51; + public static final int PREVIOUS_PAGE_SLOT = 52; + public static final int NEXT_PAGE_SLOT = 53; + private static final Set WARNED_FORMATS = new HashSet<>(); public static final ArrayList<@NotNull BiFunction< ? super FureamScreenInventory, ? super FureamWorldConfig, ? extends @NotNull FureamFunctionalArea @@ -79,6 +94,8 @@ public class FureamScreenInventory implements InventoryHolder { public @NotNull ItemStack nextPageBtn = new ItemStack(Material.AIR); private @Range(from = 0, to = Integer.MAX_VALUE) int page; public final @NotNull Inventory bukkitInventory; + private int visualTick; + private boolean forceBarRefresh = true; public FureamScreenInventory( @NotNull UUID viewerId, @NotNull Location location, @NotNull FurnaceType type, @@ -91,13 +108,11 @@ public FureamScreenInventory( this.config = config; this.functionalAreas = new ArrayList<>(); - int inputSlotCount = config != null ? config.getInputSlotCount().get(type) : 9; - int fuelSlotCount = config != null ? config.getFuelSlotCount().get(type) : 9; - int outputSlotCount = config != null ? config.getOutputSlotCount().get(type) : 9; - - this.inputSlots = Math.max(1, inputSlotCount); - this.fuelSlots = Math.max(1, fuelSlotCount); - this.outputSlots = Math.max(1, outputSlotCount); + ResolvedFurnaceConfig resolved = config == null + ? null : ResolvedFurnaceConfig.resolve(config, type); + this.inputSlots = resolved == null ? 9 : resolved.inputSlots; + this.fuelSlots = resolved == null ? 9 : resolved.fuelSlots; + this.outputSlots = resolved == null ? 9 : resolved.outputSlots; this.fuelLeftItem = parseMaterial(config != null ? config.getGuiFuelLeftItemId().get(type) : null, Material.GLASS_PANE); this.fuelUsedItem = parseMaterial(config != null ? config.getGuiFuelUsedItemId().get(type) : null, Material.GLASS_PANE); @@ -114,11 +129,11 @@ public FureamScreenInventory( ); String title = resolveTitle(location, type, config); - this.bukkitInventory = Bukkit.createInventory(this, 54, title.length() > 32 ? title.substring(0, 32) : title); + this.bukkitInventory = Bukkit.createInventory(this, SIZE, title.length() > 32 ? title.substring(0, 32) : title); this.handler = new FureamScreenHandler(this); FurnacePos pos = new FurnacePos(this.location); - FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); + FurnaceContext ctx = FurnaceManager.getContext(pos); if (ctx != null) { this.burnTime = ctx.burnTime; this.fuelTimeTotal = Math.max(1, ctx.fuelTimeTotal); @@ -126,7 +141,7 @@ public FureamScreenInventory( this.cookTimeTotal = Math.max(1, ctx.cookTimeTotal); } else { this.cookTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); - this.fuelTimeTotal = (type == FurnaceType.FURNACE ? 200 : 100); + this.fuelTimeTotal = 200; } this.switchPage(0); for (BiFunction< @@ -171,29 +186,22 @@ public Inventory getInventory() { return this.bukkitInventory; } - public int computeCookTimeTotal() { - FurnacePos pos = new FurnacePos(this.location); - FurnaceContext ctx = FurnaceManager.CONTEXTS.get(pos); - if (ctx != null) { - return Math.max(1, ctx.cookTimeTotal); - } - return this.furnaceType == FurnaceType.FURNACE ? 200 : 100; - } - public boolean isInputSlot(int slot) { - return slot >= 0 && slot < 9 && this.page * 9 + slot < this.inputSlots; + return slot >= INPUT_START && slot < FUEL_BAR_START && this.page * COLS + slot < this.inputSlots; } public boolean isFuelRowSlot(int slot) { - return slot >= 18 && slot < 27 && this.page * 9 + slot - 18 < this.fuelSlots; + return slot >= FUEL_START && slot < PROGRESS_BAR_START && + this.page * COLS + slot - FUEL_START < this.fuelSlots; } public boolean isOutputSlot(int slot) { - return slot >= 36 && slot < 45 && this.page * 9 + slot - 36 < this.outputSlots; + return slot >= OUTPUT_START && slot < FUNCTIONAL_START && + this.page * COLS + slot - OUTPUT_START < this.outputSlots; } public boolean isFunctionalAreaSlot(int slot) { - return slot >= 45 && slot < 51; + return slot >= FUNCTIONAL_START && slot < NEXT_FUNCTIONAL_SLOT; } public boolean isBarOrDecorSlot(int slot) { @@ -202,36 +210,41 @@ public boolean isBarOrDecorSlot(int slot) { } public int getInputSlotStop() { - int diff = this.inputSlots - (this.page * 9); - return diff >= 9 ? 9 : (diff <= 0 ? 0 : diff); + int diff = this.inputSlots - (this.page * COLS); + return diff >= COLS ? COLS : (diff <= 0 ? 0 : diff); } public int getFuelSlotStop() { - int diff = this.fuelSlots - (this.page * 9); - return diff >= 9 ? 27 : (diff <= 0 ? 18 : diff + 18); + int diff = this.fuelSlots - (this.page * COLS); + return diff >= COLS ? PROGRESS_BAR_START : + (diff <= 0 ? FUEL_START : diff + FUEL_START); } public int getOutputSlotStop() { - int diff = this.outputSlots - (this.page * 9); - return diff >= 9 ? 45 : (diff <= 0 ? 36 : diff + 36); + int diff = this.outputSlots - (this.page * COLS); + return diff >= COLS ? FUNCTIONAL_START : + (diff <= 0 ? OUTPUT_START : diff + OUTPUT_START); } public void refreshVisuals() { - this.cookTimeTotal = this.computeCookTimeTotal(); + this.cookTimeTotal = Math.max(1, this.cookTimeTotal); if (this.cookTime >= this.cookTimeTotal) { this.cookTime = Math.max(0, this.cookTimeTotal - 1); } - int fuelCount = barCount(this.burnTime, this.fuelTimeTotal); - int progressCount = barCount(this.cookTime, this.cookTimeTotal); - - for (int i = 0; i < COLS; ++i) { - this.fuelBar[i] = this.addFuelTooltip(new ItemStack( - i < fuelCount ? this.fuelLeftItem : this.fuelUsedItem, 1 - )); - this.progBar[i] = this.addProgressTooltip(new ItemStack( - i < progressCount ? this.progressDoneItem : this.progressRemainingItem, 1 - )); + boolean refreshBars = this.forceBarRefresh || (this.visualTick++ & 1) == 0; + if (refreshBars) { + int fuelCount = barCount(this.burnTime, this.fuelTimeTotal); + int progressCount = barCount(this.cookTime, this.cookTimeTotal); + for (int i = 0; i < COLS; ++i) { + this.fuelBar[i] = this.addFuelTooltip(new ItemStack( + i < fuelCount ? this.fuelLeftItem : this.fuelUsedItem, 1 + )); + this.progBar[i] = this.addProgressTooltip(new ItemStack( + i < progressCount ? this.progressDoneItem : this.progressRemainingItem, 1 + )); + } + this.forceBarRefresh = false; } for (FureamFunctionalArea area : this.functionalAreas) { @@ -242,9 +255,12 @@ public void refreshVisuals() { } public void syncToBukkitInventory() { - for (int i = 0; i < 54; i++) { + for (int i = 0; i < SIZE; i++) { ItemStack stack = this.getStack(i); - this.bukkitInventory.setItem(i, stack); + ItemStack current = this.bukkitInventory.getItem(i); + if (!sameStack(current, stack)) { + this.bukkitInventory.setItem(i, ItemCompat.isEmpty(stack) ? null : stack.clone()); + } } } @@ -256,21 +272,22 @@ public void syncToBukkitInventory() { @Contract(pure = true) public @Range(from = 0, to = Integer.MAX_VALUE) int getMaxPage() { int maxSlots = Math.max(this.inputSlots, Math.max(this.fuelSlots, this.outputSlots)); - return Math.max(0, (maxSlots - 1) / 9); + return Math.max(0, (maxSlots - 1) / COLS); } public void switchPage(int page) { if (page >= 0 && page <= this.getMaxPage()) { this.page = page; + this.forceBarRefresh = true; this.prevPageBtn = makeTooltipItem( new ItemStack(this.prevPageItem, 1), this.config != null ? this.config.getGuiPrevPageItemTitle().get(this.furnaceType) : "Previous Page", - String.format(this.config != null ? this.config.getGuiPrevPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + safeFormat(this.config != null ? this.config.getGuiPrevPageItemTooltip().get(this.furnaceType) : "Page %d", "Page %d", page + 1) ); this.nextPageBtn = makeTooltipItem( new ItemStack(this.nextPageItem, 1), this.config != null ? this.config.getGuiNextPageItemTitle().get(this.furnaceType) : "Next Page", - String.format(this.config != null ? this.config.getGuiNextPageItemTooltip().get(this.furnaceType) : "Page %d", page + 1) + safeFormat(this.config != null ? this.config.getGuiNextPageItemTooltip().get(this.furnaceType) : "Page %d", "Page %d", page + 1) ); this.syncToBukkitInventory(); } @@ -295,18 +312,18 @@ public void setCurrentFunctionalArea(int currentFunctionalArea) { private @NotNull ItemStack addFuelTooltip(@NotNull ItemStack stack) { String title = this.config != null ? this.config.getGuiFuelItemTitle().get(this.furnaceType) : "Fuel"; - String lore = String.format( - this.config != null ? this.config.getGuiFuelItemTooltip().get(this.furnaceType) : "Available: %d / %d", - this.burnTime, this.fuelTimeTotal + String lore = safeFormat( + this.config != null ? this.config.getGuiFuelItemTooltip().get(this.furnaceType) : "Available: %d / %d", + "Available: %d / %d", this.burnTime, this.fuelTimeTotal ); return makeTooltipItem(stack, title, lore); } private @NotNull ItemStack addProgressTooltip(@NotNull ItemStack stack) { String title = this.config != null ? this.config.getGuiProgressItemTitle().get(this.furnaceType) : "Progress"; - String lore = String.format( - this.config != null ? this.config.getGuiProgressItemTooltip().get(this.furnaceType) : "%d / %d", - this.cookTime, this.cookTimeTotal + String lore = safeFormat( + this.config != null ? this.config.getGuiProgressItemTooltip().get(this.furnaceType) : "%d / %d", + "%d / %d", this.cookTime, this.cookTimeTotal ); return makeTooltipItem(stack, title, lore); } @@ -334,59 +351,59 @@ public void setCurrentFunctionalArea(int currentFunctionalArea) { public @NotNull ItemStack getStack(int slot) { if (this.isInputSlot(slot)) { - int idx = this.page * 9 + slot; - return idx < this.data.inputs.size() ? this.data.inputs.get(idx) : new ItemStack(Material.AIR); + int idx = this.page * COLS + slot; + return cloneOrAir(idx < this.data.inputs.size() ? this.data.inputs.get(idx) : null); } if (this.isFuelRowSlot(slot)) { - int idx = this.page * 9 + slot - 18; - return idx < this.data.fuels.size() ? this.data.fuels.get(idx) : new ItemStack(Material.AIR); + int idx = this.page * COLS + slot - FUEL_START; + return cloneOrAir(idx < this.data.fuels.size() ? this.data.fuels.get(idx) : null); } if (this.isOutputSlot(slot)) { - int idx = this.page * 9 + slot - 36; - return idx < this.data.outputs.size() ? this.data.outputs.get(idx) : new ItemStack(Material.AIR); + int idx = this.page * COLS + slot - OUTPUT_START; + return cloneOrAir(idx < this.data.outputs.size() ? this.data.outputs.get(idx) : null); } if (this.isFunctionalAreaSlot(slot)) { int ca = this.getCurrentFunctionalArea(); - return ca < this.functionalAreas.size() ? - this.functionalAreas.get(ca).getGuiStack(this, slot - 45) : this.border; + return cloneOrAir(ca < this.functionalAreas.size() ? + this.functionalAreas.get(ca).getGuiStack(this, slot - FUNCTIONAL_START) : this.border); } - if (slot >= 9 && slot < 18) { - return this.fuelBar[slot - 9]; + if (slot >= FUEL_BAR_START && slot < FUEL_START) { + return cloneOrAir(this.fuelBar[slot - FUEL_BAR_START]); } - if (slot >= 27 && slot < 36) { - return this.progBar[slot - 27]; + if (slot >= PROGRESS_BAR_START && slot < OUTPUT_START) { + return cloneOrAir(this.progBar[slot - PROGRESS_BAR_START]); } - if (slot == 51) return this.nextFunctionalBtn; - if (slot == 52) return this.prevPageBtn; - if (slot == 53) return this.nextPageBtn; - return this.border; + if (slot == NEXT_FUNCTIONAL_SLOT) return this.nextFunctionalBtn.clone(); + if (slot == PREVIOUS_PAGE_SLOT) return this.prevPageBtn.clone(); + if (slot == NEXT_PAGE_SLOT) return this.nextPageBtn.clone(); + return this.border.clone(); } public void setStack(int slot, @NotNull ItemStack stack) { if (this.isInputSlot(slot)) { - int idx = this.page * 9 + slot; + int idx = this.page * COLS + slot; while (this.data.inputs.size() <= idx) { this.data.inputs.add(new ItemStack(Material.AIR)); } - this.data.inputs.set(idx, stack); + this.data.inputs.set(idx, cloneOrAir(stack)); } else if (this.isFuelRowSlot(slot)) { - int idx = this.page * 9 + slot - 18; + int idx = this.page * COLS + slot - FUEL_START; while (this.data.fuels.size() <= idx) { this.data.fuels.add(new ItemStack(Material.AIR)); } - this.data.fuels.set(idx, stack); + this.data.fuels.set(idx, cloneOrAir(stack)); } else if (this.isOutputSlot(slot)) { - int idx = this.page * 9 + slot - 36; + int idx = this.page * COLS + slot - OUTPUT_START; while (this.data.outputs.size() <= idx) { this.data.outputs.add(new ItemStack(Material.AIR)); } - this.data.outputs.set(idx, stack); + this.data.outputs.set(idx, cloneOrAir(stack)); } else if (this.isFunctionalAreaSlot(slot)) { int ca = this.getCurrentFunctionalArea(); if (ca < this.functionalAreas.size()) { FureamFunctionalArea area = this.functionalAreas.get(ca); - if (area.isStorable(this, slot - 45)) { - area.setGuiStack(this, slot - 45, stack); + if (area.isStorable(this, slot - FUNCTIONAL_START)) { + area.setGuiStack(this, slot - FUNCTIONAL_START, cloneOrAir(stack)); } } } @@ -408,6 +425,30 @@ public boolean isValid(int slot, @NotNull ItemStack stack) { return false; } + private static @NotNull ItemStack cloneOrAir(@Nullable ItemStack stack) { + return ItemCompat.isEmpty(stack) ? new ItemStack(Material.AIR) : stack.clone(); + } + + private static boolean sameStack(@Nullable ItemStack left, @Nullable ItemStack right) { + return ItemCompat.isEmpty(left) && ItemCompat.isEmpty(right) || + !ItemCompat.isEmpty(left) && !ItemCompat.isEmpty(right) && + left.getAmount() == right.getAmount() && left.isSimilar(right); + } + + private static String safeFormat(String template, String fallback, Object... arguments) { + String selected = template == null ? fallback : template; + try { + return String.format(selected, arguments); + } catch (IllegalFormatException failure) { + if (WARNED_FORMATS.add(selected) && FureamMain.getInstance() != null) { + FureamMain.getInstance().getLogger().warning( + "Invalid GUI format string '" + selected + "'; using '" + fallback + "'" + ); + } + return String.format(fallback, arguments); + } + } + static { FUNCTIONAL_AREA_REGISTRY.add(RecipeSettingAndXpFunctionalArea::new); } diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java index 888b6dc..55ae5bc 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/FurnacePos.java @@ -1,18 +1,25 @@ package io.github.eat_ram.fuream.util; import java.util.Objects; +import java.util.UUID; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Block; import org.jetbrains.annotations.NotNull; public final class FurnacePos { + public final UUID worldId; public final @NotNull String worldName; public final int x; public final int y; public final int z; public FurnacePos(@NotNull String worldName, int x, int y, int z) { + this(null, worldName, x, y, z); + } + + public FurnacePos(UUID worldId, @NotNull String worldName, int x, int y, int z) { + this.worldId = worldId; this.worldName = worldName; this.x = x; this.y = y; @@ -20,11 +27,15 @@ public FurnacePos(@NotNull String worldName, int x, int y, int z) { } public FurnacePos(@NotNull Location loc) { - this(loc.getWorld() != null ? loc.getWorld().getName() : "", loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + this( + loc.getWorld() == null ? null : loc.getWorld().getUID(), + loc.getWorld() == null ? "" : loc.getWorld().getName(), + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ() + ); } public FurnacePos(@NotNull Block block) { - this(block.getWorld().getName(), block.getX(), block.getY(), block.getZ()); + this(block.getWorld().getUID(), block.getWorld().getName(), block.getX(), block.getY(), block.getZ()); } @Override @@ -32,12 +43,14 @@ public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof FurnacePos)) return false; FurnacePos that = (FurnacePos) o; - return this.x == that.x && this.y == that.y && this.z == that.z && Objects.equals(this.worldName, that.worldName); + if (this.x != that.x || this.y != that.y || this.z != that.z) return false; + if (this.worldId != null && that.worldId != null) return this.worldId.equals(that.worldId); + return Objects.equals(this.worldName, that.worldName); } @Override public int hashCode() { - return Objects.hash(this.worldName, this.x, this.y, this.z); + return Objects.hash(this.worldId == null ? this.worldName : this.worldId, this.x, this.y, this.z); } @Override diff --git a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java index 190046b..f032478 100644 --- a/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java +++ b/spigot/src/main/java/io/github/eat_ram/fuream/util/NbtUtil.java @@ -4,6 +4,7 @@ import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import io.github.eat_ram.fuream.compat.ItemCompat; import io.github.eat_ram.fuream.nbt.NativeItemNbt; @@ -13,7 +14,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public abstract class NbtUtil { +public final class NbtUtil { + public static final int MAX_LANE_SLOTS = 576; + private static final Map, Field> MAP_FIELDS = new ConcurrentHashMap<>(); public static @NotNull ItemStack fromNbt(@Nullable NativeNbtCompound compound) { return compound == null ? ItemCompat.empty() : NativeItemNbt.read(compound); } @@ -29,6 +32,11 @@ public static void writeStacks( NativeNbtList list = resetCompoundList(parent, key); int slot = 0; for (ItemStack stack : stacks) { + if (slot >= MAX_LANE_SLOTS) { + throw new IllegalArgumentException( + "Lane " + key + " exceeds " + MAX_LANE_SLOTS + " slots" + ); + } if (!ItemCompat.isEmpty(stack)) { NativeNbtCompound sub = NativeItemNbt.write(stack); sub.setInt("Slot", slot); @@ -50,12 +58,20 @@ public static void readStacks( ) { NativeNbtList list = parent.getList(key); if (list == null) return; + if (list.size() > MAX_LANE_SLOTS) { + throw new IllegalArgumentException("NBT list " + key + " exceeds " + MAX_LANE_SLOTS + " entries"); + } for (int i = 0; i < list.size(); i++) { NativeNbtCompound sub = list.getCompound(i); if (sub == null || !sub.has("Slot")) continue; int slot = sub.getInt("Slot", -1); + if (slot < 0 || slot >= MAX_LANE_SLOTS) { + throw new IllegalArgumentException( + "NBT slot " + slot + " is outside 0.." + (MAX_LANE_SLOTS - 1) + ); + } ItemStack stack = fromNbt(sub); - if (slot < 0 || ItemCompat.isEmpty(stack)) continue; + if (ItemCompat.isEmpty(stack)) continue; while (target.size() <= slot) target.add(ItemCompat.empty()); target.set(slot, stack); } @@ -76,10 +92,13 @@ private static void copyTags(NativeNbtCompound source, NativeNbtCompound target) } private static Field findMapField(Class type) { + Field cached = MAP_FIELDS.get(type); + if (cached != null) return cached; for (Class current = type; current != null; current = current.getSuperclass()) { for (Field field : current.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers()) && Map.class.isAssignableFrom(field.getType())) { field.setAccessible(true); + MAP_FIELDS.put(type, field); return field; } } @@ -88,6 +107,5 @@ private static Field findMapField(Class type) { } private NbtUtil() { - throw new UnsupportedOperationException(); } } diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index 193911a..910e4e0 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -1,13 +1,15 @@ name: Fuream -version: 0.0.1 +version: ${version} main: io.github.eat_ram.fuream.FureamMain api-version: '1.13' commands: fuream: description: Fuream plugin management - permission: fuream.reload usage: / [reload|status] permissions: fuream.reload: description: Allows reloading Fuream configs default: op + fuream.status: + description: Allows viewing Fuream status + default: op diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java deleted file mode 100644 index 68d4098..0000000 --- a/spigot/src/test/java/io/github/eat_ram/fuream/CollectionUtilTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.eat_ram.fuream; - -import java.util.ArrayList; -import java.util.ListIterator; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -public class CollectionUtilTest { - @Test - public void testDefaultedArrayList() { - ArrayList list = CollectionUtil.newDefaultedArrayList("EMPTY", 5); - assertEquals(5, list.size()); - assertEquals("EMPTY", list.get(0)); - assertEquals("EMPTY", list.get(4)); - assertEquals("EMPTY", list.get(10)); // Out of bounds returns default - - list.set(2, "ITEM_2"); - assertEquals("ITEM_2", list.get(2)); - - list.set(7, "ITEM_7"); - assertEquals(8, list.size()); - assertEquals("ITEM_7", list.get(7)); - assertEquals("EMPTY", list.get(6)); - } - - @Test - public void testListIterator() { - ArrayList list = CollectionUtil.newDefaultedArrayList("AIR", 3); - list.set(0, "A"); - list.set(1, "B"); - list.set(2, "C"); - - ListIterator it = CollectionUtil.listIterator(list, 3, 0); - assertTrue(it.hasNext()); - assertEquals("A", it.next()); - assertEquals("B", it.next()); - assertEquals("C", it.next()); - assertFalse(it.hasNext()); - - assertTrue(it.hasPrevious()); - assertEquals("C", it.previous()); - assertEquals("B", it.previous()); - assertEquals("A", it.previous()); - assertFalse(it.hasPrevious()); - } -} diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java index 7a1147d..91fa541 100644 --- a/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java +++ b/spigot/src/test/java/io/github/eat_ram/fuream/ConfigTest.java @@ -66,4 +66,26 @@ public void localizedGuiTitleAcceptsSingleAndPerTypeValues() throws Exception { assertEquals("Localized Oven", mapped.getGuiTitle().get(FurnaceType.FURNACE)); assertEquals("Smoker", mapped.getGuiTitle().get(FurnaceType.SMOKER)); } + + @Test + public void writerRefusesToOverwriteMalformedExistingConfig() throws Exception { + Path file = tempDir.resolve("broken.json"); + byte[] original = "{broken".getBytes("UTF-8"); + Files.write(file, original); + + assertThrows(Exception.class, () -> + FureamWorldConfigImpl.writeWorldConfig(new FureamWorldConfigImpl(), file.toFile()) + ); + assertArrayEquals(original, Files.readAllBytes(file)); + } + + @Test + public void oversizedLaneCountIsRejected() throws Exception { + Path file = tempDir.resolve("oversized.json"); + Files.write(file, "{\"input_slot_count\":{\"FURNACE\":577}}".getBytes("UTF-8")); + + assertThrows(Exception.class, () -> + FureamWorldConfigImpl.readWorldConfig(new FureamWorldConfigImpl(), file.toFile()) + ); + } } diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java index c759c60..68b38c5 100644 --- a/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/FureamFurnaceEngineTest.java @@ -92,14 +92,14 @@ outputs, stack(Material.IRON_INGOT, 3), 2 } @Test - public void wetSpongeDoesNotConvertBucketWhenFuelRegionIsFull() { + public void wetSpongeUsesFreedSingleBucketSlotWhenFuelRegionIsFull() { List fuels = new ArrayList<>(); fuels.add(stack(Material.BUCKET, 1)); fuels.add(stack(Material.COAL, 1)); FureamFurnaceEngine.handleWetSponge(stack(Material.WET_SPONGE, 1), fuels, 2); - assertEquals(Material.BUCKET, fuels.get(0).getType()); + assertEquals(Material.WATER_BUCKET, fuels.get(0).getType()); assertEquals(1, fuels.get(0).getAmount()); assertEquals(Material.COAL, fuels.get(1).getType()); } @@ -112,11 +112,30 @@ public void wetSpongeConvertsBucketWhenFuelRegionHasAnEmptySlot() { FureamFurnaceEngine.handleWetSponge(stack(Material.WET_SPONGE, 1), fuels, 2); - assertEquals(Material.AIR, fuels.get(0).getType()); - assertEquals(Material.WATER_BUCKET, fuels.get(1).getType()); + assertEquals(Material.WATER_BUCKET, fuels.get(0).getType()); + assertEquals(Material.AIR, fuels.get(1).getType()); assertEquals(2, fuels.size()); } + @Test + public void wetSpongeDoesNotConsumeFromMultiBucketStackWithoutSpace() { + List fuels = new ArrayList<>(); + fuels.add(stack(Material.BUCKET, 2)); + fuels.add(stack(Material.COAL, 1)); + + FureamFurnaceEngine.handleWetSponge(stack(Material.WET_SPONGE, 1), fuels, 2); + + assertEquals(Material.BUCKET, fuels.get(0).getType()); + assertEquals(2, fuels.get(0).getAmount()); + } + + @Test + public void specialFurnacesKeepTheFullVanillaFuelDuration() { + assertEquals(1600, FureamFurnaceEngine.effectiveFuelTime(1600, FurnaceType.FURNACE)); + assertEquals(1600, FureamFurnaceEngine.effectiveFuelTime(1600, FurnaceType.SMOKER)); + assertEquals(1600, FureamFurnaceEngine.effectiveFuelTime(1600, FurnaceType.BLAST_FURNACE)); + } + @Test public void comparatorUsesOnlyTheThreeMaskedStacks() { FureamFurnaceData data = new FureamFurnaceData(); diff --git a/spigot/src/test/java/io/github/eat_ram/fuream/logic/NativeLaneProjectionTest.java b/spigot/src/test/java/io/github/eat_ram/fuream/logic/NativeLaneProjectionTest.java new file mode 100644 index 0000000..070a90f --- /dev/null +++ b/spigot/src/test/java/io/github/eat_ram/fuream/logic/NativeLaneProjectionTest.java @@ -0,0 +1,97 @@ +package io.github.eat_ram.fuream.logic; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class NativeLaneProjectionTest { + @Test + public void publishingKeepsCanonicalVirtualStack() { + List lane = lane(stack(Material.IRON_INGOT, 12)); + NativeLaneProjection.State state = new NativeLaneProjection.State(); + + ItemStack published = NativeLaneProjection.publish( + lane, null, NativeLaneProjection.SelectionMode.FIRST_PRESENT, state + ); + + assertEquals(12, published.getAmount()); + assertEquals(12, lane.get(0).getAmount()); + assertEquals(0, state.getSourceSlot()); + } + + @Test + public void hopperRemovalReplacesCanonicalAmountWithoutDuplication() { + List lane = lane(stack(Material.IRON_INGOT, 12)); + NativeLaneProjection.State state = new NativeLaneProjection.State(); + ItemStack published = NativeLaneProjection.publish( + lane, null, NativeLaneProjection.SelectionMode.FIRST_PRESENT, state + ); + published.setAmount(7); + + assertNull(NativeLaneProjection.reconcile(lane, published, 1, state)); + assertEquals(7, lane.get(0).getAmount()); + } + + @Test + public void completeHopperRemovalClearsOnlyProjectedSource() { + List lane = lane(stack(Material.IRON_INGOT, 12), stack(Material.GOLD_INGOT, 4)); + NativeLaneProjection.State state = new NativeLaneProjection.State(); + NativeLaneProjection.publish(lane, null, NativeLaneProjection.SelectionMode.FIRST_PRESENT, state); + + assertNull(NativeLaneProjection.reconcile(lane, null, 2, state)); + assertEquals(Material.AIR, lane.get(0).getType()); + assertEquals(4, lane.get(1).getAmount()); + } + + @Test + public void fuelProjectionPublishesOnlyBucketRemainders() { + List lane = lane(stack(Material.COAL, 8), stack(Material.BUCKET, 1)); + ItemStack published = NativeLaneProjection.publish( + lane, null, NativeLaneProjection.SelectionMode.BUCKET_REMAINDER, + new NativeLaneProjection.State() + ); + assertEquals(Material.BUCKET, published.getType()); + } + + private static List lane(ItemStack... stacks) { + return new ArrayList<>(Arrays.asList(stacks)); + } + + private static ItemStack stack(Material material, int amount) { + return new TestItemStack(material, amount); + } + + private static final class TestItemStack extends ItemStack { + private TestItemStack(Material material, int amount) { + super(material, amount); + } + + @Override + public boolean isSimilar(ItemStack stack) { + return stack != null && this.getType() == stack.getType(); + } + + @Override + public int getMaxStackSize() { + return 64; + } + + @Override + public ItemMeta getItemMeta() { + return null; + } + + @Override + public ItemStack clone() { + return new TestItemStack(this.getType(), this.getAmount()); + } + } +} diff --git a/spigot/src/variants/components/plugin.yml b/spigot/src/variants/components/plugin.yml index 6a668c4..536a252 100644 --- a/spigot/src/variants/components/plugin.yml +++ b/spigot/src/variants/components/plugin.yml @@ -1,13 +1,15 @@ name: Fuream -version: 0.0.1 +version: ${version} main: io.github.eat_ram.fuream.FureamMain api-version: '1.20.5' commands: fuream: description: Fuream plugin management - permission: fuream.reload usage: / [reload|status] permissions: fuream.reload: description: Allows reloading Fuream configs default: op + fuream.status: + description: Allows viewing Fuream status + default: op diff --git a/spigot/src/variants/flattening/plugin.yml b/spigot/src/variants/flattening/plugin.yml index 193911a..910e4e0 100644 --- a/spigot/src/variants/flattening/plugin.yml +++ b/spigot/src/variants/flattening/plugin.yml @@ -1,13 +1,15 @@ name: Fuream -version: 0.0.1 +version: ${version} main: io.github.eat_ram.fuream.FureamMain api-version: '1.13' commands: fuream: description: Fuream plugin management - permission: fuream.reload usage: / [reload|status] permissions: fuream.reload: description: Allows reloading Fuream configs default: op + fuream.status: + description: Allows viewing Fuream status + default: op diff --git a/spigot/src/variants/legacy/plugin.yml b/spigot/src/variants/legacy/plugin.yml index c330c5f..ba3e142 100644 --- a/spigot/src/variants/legacy/plugin.yml +++ b/spigot/src/variants/legacy/plugin.yml @@ -1,12 +1,14 @@ name: Fuream -version: 0.0.1 +version: ${version} main: io.github.eat_ram.fuream.FureamMain commands: fuream: description: Fuream plugin management - permission: fuream.reload usage: / [reload|status] permissions: fuream.reload: description: Allows reloading Fuream configs default: op + fuream.status: + description: Allows viewing Fuream status + default: op