Skip to content

Commit a8f1a53

Browse files
committed
Add optional CraftEngine content recognition
1 parent 040e533 commit a8f1a53

21 files changed

Lines changed: 641 additions & 120 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ against Paper 26.2.
5252

5353
## Optional integrations
5454

55+
- [CraftEngine](https://github.com/Xiao-MoMi/craft-engine) 26.7.2: optional
56+
custom-item ID recognition. CraftEngine items can be selected by the third
57+
field of an item-label blacklist rule, and their display pose can be
58+
overridden in `material.yml` under `CustomItems`. CraftEngine is not bundled
59+
and the plugin behaves exactly as before when it is absent.
5560
- [LightAPI](https://www.spigotmc.org/resources/lightapi-fork.48247/)
5661
- [OpenInv](https://dev.bukkit.org/projects/openinv)
5762
- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/)

build.gradle.kts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
22
import org.gradle.jvm.tasks.Jar
3+
import java.util.zip.ZipFile
34

45
plugins {
56
java
@@ -11,6 +12,7 @@ version = "2026.1.2.0"
1112
val pluginVersion = version.toString()
1213
val paper26_1Version = "26.1.2.build.74-stable"
1314
val paper26_2Version = "26.2.build.56-alpha"
15+
val craftEngineVersion = "26.7.2"
1416

1517
val paper26_2CompileClasspath = configurations.create("paper26_2CompileClasspath") {
1618
isCanBeConsumed = false
@@ -28,16 +30,21 @@ repositories {
2830
dependencies {
2931
compileOnly("io.papermc.paper:paper-api:$paper26_1Version")
3032
compileOnly("me.clip:placeholderapi:2.11.7")
33+
compileOnly("net.momirealms:craft-engine-core:$craftEngineVersion")
34+
compileOnly("net.momirealms:craft-engine-bukkit:$craftEngineVersion")
3135
compileOnly(files("common/lib/LightAPI-fork-3.5.2.jar"))
3236

3337
implementation("net.momirealms:sparrow-yaml:1.0.7")
3438

3539
testImplementation(platform("org.junit:junit-bom:5.13.4"))
3640
testImplementation("org.junit.jupiter:junit-jupiter")
41+
testImplementation("io.papermc.paper:paper-api:$paper26_1Version")
3742
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
3843

3944
paper26_2CompileClasspath("io.papermc.paper:paper-api:$paper26_2Version")
4045
paper26_2CompileClasspath("me.clip:placeholderapi:2.11.7")
46+
paper26_2CompileClasspath("net.momirealms:craft-engine-core:$craftEngineVersion")
47+
paper26_2CompileClasspath("net.momirealms:craft-engine-bukkit:$craftEngineVersion")
4148
paper26_2CompileClasspath(files("common/lib/LightAPI-fork-3.5.2.jar"))
4249
}
4350

@@ -139,9 +146,47 @@ val verifyPaperOnlyArchitecture = tasks.register("verifyPaperOnlyArchitecture")
139146
}
140147
}
141148

149+
val verifyCustomContentIsolation = tasks.register("verifyCustomContentIsolation") {
150+
description = "Keeps optional provider APIs behind their reflection-loaded bridge implementations."
151+
group = LifecycleBasePlugin.VERIFICATION_GROUP
152+
val sources = sourceSets.main.get().allJava
153+
inputs.files(sources)
154+
doLast {
155+
val allowedCraftEngineSource = file(
156+
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineCustomContentBridge.java",
157+
).canonicalFile
158+
val managerSource = file(
159+
"common/src/main/java/com/loohp/interactionvisualizer/integration/CustomContentManager.java",
160+
).canonicalFile
161+
val stableApiClass = "net.momirealms.craftengine.bukkit.api.CraftEngineItems"
162+
val craftEngineToken = Regex("net\\.momirealms\\.craftengine(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)+")
163+
val violations = sources.files.flatMap { source ->
164+
craftEngineToken.findAll(source.readText()).mapNotNull { match ->
165+
val allowed = when (source.canonicalFile) {
166+
allowedCraftEngineSource, managerSource -> match.value == stableApiClass
167+
else -> false
168+
}
169+
if (allowed) null else "${source.relativeTo(rootDir)}: ${match.value}"
170+
}.toList()
171+
}.toMutableList()
172+
173+
val allowedArtifacts = setOf("craft-engine-core", "craft-engine-bukkit")
174+
configurations.flatMap { it.dependencies }
175+
.filter { it.group == "net.momirealms" && it.name.startsWith("craft-engine-") }
176+
.filter { it.name !in allowedArtifacts }
177+
.forEach { dependency ->
178+
violations.add("Unexpected CraftEngine artifact: ${dependency.group}:${dependency.name}")
179+
}
180+
check(violations.isEmpty()) {
181+
"CraftEngine API escaped its optional bridge implementation:\n${violations.joinToString("\n")}"
182+
}
183+
}
184+
}
185+
142186
tasks.check {
143187
dependsOn(compilePaper26_2)
144188
dependsOn(verifyPaperOnlyArchitecture)
189+
dependsOn(verifyCustomContentIsolation)
145190
}
146191

147192
tasks.named<ShadowJar>("shadowJar") {
@@ -152,6 +197,19 @@ tasks.named<ShadowJar>("shadowJar") {
152197

153198
isPreserveFileTimestamps = false
154199
isReproducibleFileOrder = true
200+
201+
doLast {
202+
ZipFile(archiveFile.get().asFile).use { jar ->
203+
val bundledCraftEngine = jar.entries().asSequence()
204+
.map { it.name }
205+
.filter { it.startsWith("net/momirealms/craftengine/") }
206+
.toList()
207+
check(bundledCraftEngine.isEmpty()) {
208+
"CraftEngine must remain compileOnly, but provider classes were bundled:\n" +
209+
bundledCraftEngine.joinToString("\n")
210+
}
211+
}
212+
}
155213
}
156214

157215
tasks.jar {

common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
2424
import com.loohp.interactionvisualizer.config.Config;
2525
import com.loohp.interactionvisualizer.database.Database;
26+
import com.loohp.interactionvisualizer.integration.CustomContentManager;
2627
import com.loohp.interactionvisualizer.managers.AsyncExecutorManager;
2728
import com.loohp.interactionvisualizer.managers.LangManager;
2829
import com.loohp.interactionvisualizer.managers.LightManager;
@@ -169,6 +170,10 @@ public void onEnable() {
169170
}
170171
loadConfig();
171172

173+
if (CustomContentManager.initialize(this).contains("craftengine")) {
174+
hookMessage("CraftEngine");
175+
}
176+
172177
if (getConfiguration().getBoolean("Options.DownloadLanguageFiles")) {
173178
asyncExecutorManager.runTaskAsynchronously(LangManager::generate);
174179
}
@@ -227,6 +232,7 @@ public void onEnable() {
227232

228233
@Override
229234
public void onDisable() {
235+
CustomContentManager.shutdown();
230236
if (preferenceManager != null) {
231237
preferenceManager.close();
232238
}

common/src/main/java/com/loohp/interactionvisualizer/blocks/AnvilDisplay.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,12 @@ public void process(Player player) {
154154
item = null;
155155
}
156156
if (item != null) {
157-
boolean changed = true;
158-
if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.BLOCK) && !standMode(stand).equals(MaterialMode.BLOCK)) {
159-
toggleStandMode(stand, "Block");
160-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.TOOL) && !standMode(stand).equals(MaterialMode.TOOL)) {
161-
toggleStandMode(stand, "Tool");
162-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.ITEM) && !standMode(stand).equals(MaterialMode.ITEM)) {
163-
toggleStandMode(stand, "Item");
164-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.STANDING) && !standMode(stand).equals(MaterialMode.STANDING)) {
165-
toggleStandMode(stand, "Standing");
166-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.LOWBLOCK) && !standMode(stand).equals(MaterialMode.LOWBLOCK)) {
167-
toggleStandMode(stand, "LowBlock");
168-
} else {
169-
changed = false;
157+
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
158+
boolean changed = materialMode != standMode(stand);
159+
if (changed) {
160+
toggleStandMode(stand, materialMode.toString());
170161
}
171-
if (!item.getType().equals(stand.getItemInMainHand().getType())) {
162+
if (!item.equals(stand.getItemInMainHand())) {
172163
changed = true;
173164
stand.setItemInMainHand(item);
174165
}

common/src/main/java/com/loohp/interactionvisualizer/blocks/CrafterDisplay.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -184,21 +184,12 @@ public void handleUpdate(Block block, Map<String, Object> map) {
184184
item = null;
185185
}
186186
if (item != null) {
187-
boolean changed = true;
188-
if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialUtils.MaterialMode.BLOCK) && !standMode(stand).equals(MaterialUtils.MaterialMode.BLOCK)) {
189-
toggleStandMode(stand, "Block");
190-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialUtils.MaterialMode.TOOL) && !standMode(stand).equals(MaterialUtils.MaterialMode.TOOL)) {
191-
toggleStandMode(stand, "Tool");
192-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialUtils.MaterialMode.ITEM) && !standMode(stand).equals(MaterialUtils.MaterialMode.ITEM)) {
193-
toggleStandMode(stand, "Item");
194-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialUtils.MaterialMode.STANDING) && !standMode(stand).equals(MaterialUtils.MaterialMode.STANDING)) {
195-
toggleStandMode(stand, "Standing");
196-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialUtils.MaterialMode.LOWBLOCK) && !standMode(stand).equals(MaterialUtils.MaterialMode.LOWBLOCK)) {
197-
toggleStandMode(stand, "LowBlock");
198-
} else {
199-
changed = false;
187+
MaterialUtils.MaterialMode materialMode = MaterialUtils.getMaterialType(item);
188+
boolean changed = materialMode != standMode(stand);
189+
if (changed) {
190+
toggleStandMode(stand, materialMode.toString());
200191
}
201-
if (!item.getType().equals(stand.getItemInMainHand().getType())) {
192+
if (!item.equals(stand.getItemInMainHand())) {
202193
changed = true;
203194
stand.setItemInMainHand(item);
204195
}

common/src/main/java/com/loohp/interactionvisualizer/blocks/CraftingTableDisplay.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -222,21 +222,12 @@ public void process(Player player) {
222222
item = null;
223223
}
224224
if (item != null) {
225-
boolean changed = true;
226-
if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.BLOCK) && !standMode(stand).equals(MaterialMode.BLOCK)) {
227-
toggleStandMode(stand, "Block");
228-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.TOOL) && !standMode(stand).equals(MaterialMode.TOOL)) {
229-
toggleStandMode(stand, "Tool");
230-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.ITEM) && !standMode(stand).equals(MaterialMode.ITEM)) {
231-
toggleStandMode(stand, "Item");
232-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.STANDING) && !standMode(stand).equals(MaterialMode.STANDING)) {
233-
toggleStandMode(stand, "Standing");
234-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.LOWBLOCK) && !standMode(stand).equals(MaterialMode.LOWBLOCK)) {
235-
toggleStandMode(stand, "LowBlock");
236-
} else {
237-
changed = false;
225+
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
226+
boolean changed = materialMode != standMode(stand);
227+
if (changed) {
228+
toggleStandMode(stand, materialMode.toString());
238229
}
239-
if (!item.getType().equals(stand.getItemInMainHand().getType())) {
230+
if (!item.equals(stand.getItemInMainHand())) {
240231
changed = true;
241232
stand.setItemInMainHand(item);
242233
}

common/src/main/java/com/loohp/interactionvisualizer/blocks/GrindstoneDisplay.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,12 @@ public void process(Player player) {
150150
item = null;
151151
}
152152
if (item != null) {
153-
boolean changed = true;
154-
if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.BLOCK) && !standMode(stand).equals(MaterialMode.BLOCK)) {
155-
toggleStandMode(stand, "Block");
156-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.TOOL) && !standMode(stand).equals(MaterialMode.TOOL)) {
157-
toggleStandMode(stand, "Tool");
158-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.ITEM) && !standMode(stand).equals(MaterialMode.ITEM)) {
159-
toggleStandMode(stand, "Item");
160-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.STANDING) && !standMode(stand).equals(MaterialMode.STANDING)) {
161-
toggleStandMode(stand, "Standing");
162-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.LOWBLOCK) && !standMode(stand).equals(MaterialMode.LOWBLOCK)) {
163-
toggleStandMode(stand, "LowBlock");
164-
} else {
165-
changed = false;
153+
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
154+
boolean changed = materialMode != standMode(stand);
155+
if (changed) {
156+
toggleStandMode(stand, materialMode.toString());
166157
}
167-
if (!item.getType().equals(stand.getItemInMainHand().getType())) {
158+
if (!item.equals(stand.getItemInMainHand())) {
168159
changed = true;
169160
stand.setItemInMainHand(item);
170161
}

common/src/main/java/com/loohp/interactionvisualizer/blocks/SmithingTableDisplay.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,12 @@ public void process(Player player) {
160160
item = null;
161161
}
162162
if (item != null) {
163-
boolean changed = true;
164-
if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.BLOCK) && !standMode(stand).equals(MaterialMode.BLOCK)) {
165-
toggleStandMode(stand, "Block");
166-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.TOOL) && !standMode(stand).equals(MaterialMode.TOOL)) {
167-
toggleStandMode(stand, "Tool");
168-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.ITEM) && !standMode(stand).equals(MaterialMode.ITEM)) {
169-
toggleStandMode(stand, "Item");
170-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.STANDING) && !standMode(stand).equals(MaterialMode.STANDING)) {
171-
toggleStandMode(stand, "Standing");
172-
} else if (MaterialUtils.getMaterialType(item.getType()).equals(MaterialMode.LOWBLOCK) && !standMode(stand).equals(MaterialMode.LOWBLOCK)) {
173-
toggleStandMode(stand, "LowBlock");
174-
} else {
175-
changed = false;
163+
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
164+
boolean changed = materialMode != standMode(stand);
165+
if (changed) {
166+
toggleStandMode(stand, materialMode.toString());
176167
}
177-
if (!item.getType().equals(stand.getItemInMainHand().getType())) {
168+
if (!item.equals(stand.getItemInMainHand())) {
178169
changed = true;
179170
stand.setItemInMainHand(item);
180171
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* This file is part of InteractionVisualizer.
3+
*
4+
* Copyright (C) 2026. Contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*/
11+
12+
package com.loohp.interactionvisualizer.entities;
13+
14+
import org.bukkit.Material;
15+
import org.bukkit.NamespacedKey;
16+
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.function.Consumer;
20+
import java.util.regex.Pattern;
21+
import java.util.regex.PatternSyntaxException;
22+
23+
/** Immutable, fail-closed rules for dropped-item labels. */
24+
final class DroppedItemBlacklist {
25+
26+
private static final DroppedItemBlacklist EMPTY = new DroppedItemBlacklist(List.of());
27+
28+
private final List<Rule> rules;
29+
private final boolean requiresCustomItemId;
30+
31+
private DroppedItemBlacklist(List<Rule> rules) {
32+
this.rules = List.copyOf(rules);
33+
this.requiresCustomItemId = rules.stream().anyMatch(rule -> rule.customItemId() != null);
34+
}
35+
36+
static DroppedItemBlacklist compile(List<?> entries, Consumer<String> warning) {
37+
if (entries == null || entries.isEmpty()) {
38+
return EMPTY;
39+
}
40+
41+
List<Rule> rules = new ArrayList<>();
42+
for (Object value : entries) {
43+
if (!(value instanceof List<?> entry) || entry.isEmpty()) {
44+
warning.accept("Ignoring malformed dropped-item blacklist entry: " + value);
45+
continue;
46+
}
47+
48+
Pattern namePattern;
49+
try {
50+
namePattern = Pattern.compile(String.valueOf(entry.getFirst()));
51+
} catch (PatternSyntaxException exception) {
52+
warning.accept("Ignoring invalid dropped-item blacklist regex: " + entry.getFirst());
53+
continue;
54+
}
55+
56+
Material material = null;
57+
if (entry.size() > 1 && !"*".equals(String.valueOf(entry.get(1)))) {
58+
String materialName = String.valueOf(entry.get(1));
59+
material = Material.matchMaterial(materialName);
60+
if (material == null) {
61+
warning.accept("Ignoring dropped-item blacklist entry with invalid material: " + materialName);
62+
continue;
63+
}
64+
}
65+
66+
NamespacedKey customItemId = null;
67+
if (entry.size() > 2) {
68+
String id = String.valueOf(entry.get(2));
69+
customItemId = id.indexOf(':') > 0 ? NamespacedKey.fromString(id) : null;
70+
if (customItemId == null) {
71+
warning.accept("Ignoring dropped-item blacklist entry with invalid custom item ID: " + id);
72+
continue;
73+
}
74+
}
75+
76+
rules.add(new Rule(namePattern, material, customItemId));
77+
}
78+
return rules.isEmpty() ? EMPTY : new DroppedItemBlacklist(rules);
79+
}
80+
81+
boolean requiresCustomItemId() {
82+
return requiresCustomItemId;
83+
}
84+
85+
boolean matches(String name, Material material, NamespacedKey customItemId) {
86+
for (Rule rule : rules) {
87+
if (rule.matches(name, material, customItemId)) {
88+
return true;
89+
}
90+
}
91+
return false;
92+
}
93+
94+
private record Rule(Pattern namePattern, Material material, NamespacedKey customItemId) {
95+
96+
private boolean matches(String name, Material candidateMaterial, NamespacedKey candidateCustomItemId) {
97+
return namePattern.matcher(name).matches()
98+
&& (material == null || material == candidateMaterial)
99+
&& (customItemId == null || customItemId.equals(candidateCustomItemId));
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)