Skip to content

Commit 6f2a6e2

Browse files
authored
Use Sparrow virtual items with vanilla client pickup (#2)
Merge the completed InteractionVisualizer rendering and pickup animation changes.
2 parents 392a52d + 5843f39 commit 6f2a6e2

31 files changed

Lines changed: 1145 additions & 167 deletions

File tree

.github/workflows/build.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
name: Java 25 build
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 20
18+
19+
steps:
20+
- name: Check out repository
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Java 25
24+
uses: actions/setup-java@v4
25+
with:
26+
distribution: temurin
27+
java-version: "25"
28+
29+
- name: Set up Gradle
30+
uses: gradle/actions/setup-gradle@v4
31+
32+
- name: Run checks and build production jar
33+
run: ./gradlew clean check shadowJar --no-daemon --no-build-cache --rerun-tasks
34+
35+
- name: Upload production jar
36+
if: success()
37+
uses: actions/upload-artifact@v4
38+
with:
39+
name: InteractionVisualizer-${{ github.sha }}
40+
path: build/libs/InteractionVisualizer-*.jar
41+
if-no-files-found: error
42+
retention-days: 14

README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,23 @@ outside the supported range.
1717

1818
## Rewrite highlights
1919

20-
- Native Paper `ItemDisplay` and `TextDisplay` entities replace Armor Stands and
21-
raw metadata packets.
22-
- One Paper API implementation replaces the old per-version NMS modules.
23-
- Per-viewer visibility uses Paper's `showEntity` / `hideEntity` API.
20+
- Native Paper `ItemDisplay` and `TextDisplay` entities replace Armor Stands.
21+
- Dropped-item visuals use shaded
22+
[Sparrow Heart](https://github.com/Xiao-MoMi/sparrow-heart) client-side vanilla
23+
`ITEM` entities. Empty Paper display anchors retain tracker and chunk lifecycle
24+
without enabling server item collision physics.
25+
- One Paper API implementation replaces the old in-project per-version NMS modules.
26+
InteractionVisualizer uses Heart 0.72 only for the client-side item packet path
27+
on the supported 26.1/26.2 runtimes.
28+
- Per-viewer anchor visibility uses Paper's `showEntity` / `hideEntity` API;
29+
tracker enter/leave events create and destroy the matching virtual `ITEM`.
30+
- Static items bob and spin entirely client-side with no animation task; only
31+
active gravity motion needs per-viewer correction because Heart marks its
32+
fake items as no-gravity.
33+
- Block-to-player pickup uses Minecraft's native take-item packet through one
34+
isolated reflection bridge. The client supplies the vanilla sound and
35+
three-tick live-target absorption; arbitrary location-to-location throws keep
36+
the existing custom motion path. No Sparrow fork or ProtocolLib is required.
2437
- Display updates are revision-coalesced; there is no 5 ms packet scan loop.
2538
- Player/chunk proximity queries use one allocation-light snapshot per world and
2639
server tick.
@@ -30,9 +43,8 @@ outside the supported range.
3043
- Gradle replaces the former Maven multi-module build and verifies the same
3144
sources against both supported Paper API lines.
3245

33-
The other Sparrow modules were deliberately not added: metadata, reflection,
34-
NBT, heart, and Redis messaging either duplicate Paper 26 APIs, reintroduce
35-
version/NMS coupling, or do not serve this single-server rendering path.
46+
Other Sparrow modules were deliberately not added: metadata, reflection, NBT,
47+
and Redis messaging do not serve this single-server rendering path.
3648

3749
## Building
3850

abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/Item.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
import org.bukkit.inventory.ItemStack;
2121
import org.bukkit.util.Vector;
2222

23-
/** Logical state for a real, non-persistent Paper item entity. */
23+
/**
24+
* Logical state for a client-side vanilla item visual.
25+
* Velocity and gravity are animation inputs; no server item physics is enabled.
26+
*/
2427
public class Item extends VisualizerEntity {
2528

2629
private ItemStack item;
@@ -117,6 +120,7 @@ public Vector getVelocity() {
117120

118121
public void setVelocity(Vector velocity) {
119122
Vector normalized = velocity == null ? new Vector() : velocity.clone();
123+
normalized.checkFinite();
120124
if (!this.velocity.equals(normalized)) {
121125
this.velocity = normalized;
122126
markDirty();

build.gradle.kts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ val pluginVersion = version.toString()
1313
val paper26_1Version = "26.1.2.build.74-stable"
1414
val paper26_2Version = "26.2.build.56-alpha"
1515
val craftEngineVersion = "26.7.2"
16+
val sparrowHeartVersion = "0.72"
1617

1718
val paper26_2CompileClasspath = configurations.create("paper26_2CompileClasspath") {
1819
isCanBeConsumed = false
@@ -35,6 +36,7 @@ dependencies {
3536
compileOnly(files("common/lib/LightAPI-fork-3.5.2.jar"))
3637

3738
implementation("net.momirealms:sparrow-yaml:1.0.7")
39+
implementation("net.momirealms:sparrow-heart:$sparrowHeartVersion")
3840

3941
testImplementation(platform("org.junit:junit-bom:5.13.4"))
4042
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -113,7 +115,7 @@ val compilePaper26_2 = tasks.register<JavaCompile>("compilePaper26_2") {
113115
}
114116

115117
val verifyPaperOnlyArchitecture = tasks.register("verifyPaperOnlyArchitecture") {
116-
description = "Rejects reintroduction of NMS, Armor Stands, or legacy compatibility layers."
118+
description = "Confines NMS reflection to the client pickup bridge and rejects legacy layers."
117119
group = LifecycleBasePlugin.VERIFICATION_GROUP
118120
val sources = sourceSets.main.get().allJava
119121
inputs.files(sources)
@@ -136,9 +138,16 @@ val verifyPaperOnlyArchitecture = tasks.register("verifyPaperOnlyArchitecture")
136138
"getPluginLoader()",
137139
".spigot()",
138140
)
141+
val pickupBridge = file(
142+
"common/src/main/java/com/loohp/interactionvisualizer/integration/packet/ClientPickupAnimationBridge.java",
143+
).canonicalFile
144+
val bridgeTokens = setOf("net.minecraft", "org.bukkit.craftbukkit")
139145
val violations = sources.files.flatMap { source ->
140146
val text = source.readText()
141-
forbidden.filter(text::contains).map { token -> "${source.relativeTo(rootDir)}: $token" }
147+
forbidden.filter(text::contains).mapNotNull { token ->
148+
val allowed = source.canonicalFile == pickupBridge && token in bridgeTokens
149+
if (allowed) null else "${source.relativeTo(rootDir)}: $token"
150+
}
142151
}
143152
check(violations.isEmpty()) {
144153
"Paper-only architecture violations:\n${violations.joinToString("\n")}"
@@ -194,6 +203,7 @@ tasks.named<ShadowJar>("shadowJar") {
194203
mergeServiceFiles()
195204

196205
relocate("net.momirealms.sparrow.yaml", "com.loohp.interactionvisualizer.libs.sparrow.yaml")
206+
relocate("net.momirealms.sparrow.heart", "com.loohp.interactionvisualizer.libs.sparrow.heart")
197207

198208
isPreserveFileTimestamps = false
199209
isReproducibleFileOrder = true
@@ -208,6 +218,26 @@ tasks.named<ShadowJar>("shadowJar") {
208218
"CraftEngine must remain compileOnly, but provider classes were bundled:\n" +
209219
bundledCraftEngine.joinToString("\n")
210220
}
221+
check(jar.getEntry("com/loohp/interactionvisualizer/libs/sparrow/heart/SparrowHeart.class") != null) {
222+
"The shaded Sparrow Heart runtime is missing"
223+
}
224+
listOf("r26_1", "r26_2").forEach { adapter ->
225+
check(jar.getEntry(
226+
"com/loohp/interactionvisualizer/libs/sparrow/heart/impl/$adapter/Heart.class",
227+
) != null) {
228+
"The shaded Sparrow Heart $adapter adapter is missing"
229+
}
230+
}
231+
val unrelocatedHeart = jar.entries().asSequence()
232+
.map { it.name }
233+
.filter { it.startsWith("net/momirealms/sparrow/heart/") }
234+
.toList()
235+
check(unrelocatedHeart.isEmpty()) {
236+
"Unrelocated Sparrow Heart classes were bundled:\n" + unrelocatedHeart.joinToString("\n")
237+
}
238+
check(jar.getEntry("META-INF/licenses/sparrow-heart.txt") != null) {
239+
"The Sparrow Heart MIT license notice is missing"
240+
}
211241
}
212242
}
213243
}

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
2828
import com.loohp.interactionvisualizer.entityholders.Item;
2929
import com.loohp.interactionvisualizer.managers.DisplayManager;
30-
import com.loohp.interactionvisualizer.managers.SoundManager;
3130
import com.loohp.interactionvisualizer.objectholders.EntryKey;
3231
import com.loohp.interactionvisualizer.utils.InventoryUtils;
3332
import com.loohp.interactionvisualizer.utils.LocationUtils;
@@ -285,19 +284,13 @@ public void onAnvil(InventoryClickEvent event) {
285284
}, 6);
286285

287286
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
288-
Vector lift = new Vector(0.0, 0.15, 0.0);
289-
Vector pickup = player.getEyeLocation().add(0.0, -0.5, 0.0).add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector().subtract(loc.clone().add(0.5, 1.2, 0.5).toVector()).multiply(0.15).add(lift);
290287
item.setItemStack(itemstack);
291-
item.setVelocity(pickup);
292-
item.setGravity(true);
293-
item.setPickupDelay(32767);
294288
DisplayManager.updateItem(item);
289+
DisplayManager.collectItem(item, player);
295290

296291
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
297-
SoundManager.playItemPickup(item.getLocation(), InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY));
298292
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot0);
299293
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
300-
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
301294
}, 8);
302295
}, 10);
303296
}, 1);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,12 @@ public Map<String, DisplayEntity> spawnDisplayEntitys(Block block) {
283283
Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
284284

285285
Location loc0 = block.getLocation().clone().add(direction).add(0.5, 0.25, 0.5);
286+
loc0.setDirection(facing.getDirection());
286287
DisplayEntity line0 = new DisplayEntity(loc0.clone());
287288
setStand(line0);
288289

289290
Location loc1 = block.getLocation().clone().add(direction).add(0.5, 0, 0.5);
291+
loc1.setDirection(facing.getDirection());
290292
DisplayEntity line1 = new DisplayEntity(loc1.clone());
291293
setStand(line1);
292294

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,12 @@ public Map<String, DisplayEntity> spawnDisplayEntitys(Block block) {
283283
Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
284284

285285
Location loc0 = block.getLocation().clone().add(direction).add(0.5, 0.25, 0.5);
286+
loc0.setDirection(facing.getDirection());
286287
DisplayEntity line0 = new DisplayEntity(loc0.clone());
287288
setStand(line0);
288289

289290
Location loc1 = block.getLocation().clone().add(direction).add(0.5, 0, 0.5);
291+
loc1.setDirection(facing.getDirection());
290292
DisplayEntity line1 = new DisplayEntity(loc1.clone());
291293
setStand(line1);
292294

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import com.loohp.interactionvisualizer.entityholders.Item;
3131
import com.loohp.interactionvisualizer.managers.DisplayManager;
3232
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
33-
import com.loohp.interactionvisualizer.managers.SoundManager;
3433
import com.loohp.interactionvisualizer.managers.TileEntityManager;
3534
import com.loohp.interactionvisualizer.objectholders.EntryKey;
3635
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
@@ -360,7 +359,6 @@ public void onBlastFurnace(InventoryClickEvent event) {
360359

361360
int slot = event.getRawSlot();
362361
ItemStack itemstack = event.getCurrentItem().clone();
363-
Location loc = block.getLocation();
364362
Player player = (Player) event.getWhoClicked();
365363

366364
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
@@ -378,17 +376,8 @@ public void onBlastFurnace(InventoryClickEvent event) {
378376
item.setItemStack(itemstack);
379377
item.setLocked(true);
380378

381-
Vector lift = new Vector(0.0, 0.15, 0.0);
382-
Vector pickup = player.getEyeLocation().add(0.0, -0.5, 0.0).add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector().subtract(loc.clone().add(0.5, 1.2, 0.5).toVector()).multiply(0.15).add(lift);
383-
item.setVelocity(pickup);
384-
item.setGravity(true);
385-
item.setPickupDelay(32767);
386379
DisplayManager.updateItem(item);
387-
388-
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
389-
SoundManager.playItemPickup(item.getLocation(), InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY));
390-
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
391-
}, 8);
380+
DisplayManager.collectItem(item, player);
392381
}, 1);
393382
}
394383

@@ -511,6 +500,7 @@ public Map<String, DisplayEntity> spawnDisplayEntitys(Block block) {
511500
Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
512501

513502
Location loc = block.getLocation().clone().add(direction).add(0.5, 0.2, 0.5);
503+
loc.setDirection(facing.getDirection());
514504
DisplayEntity slot1 = new DisplayEntity(loc.clone());
515505
setStand(slot1);
516506

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
2828
import com.loohp.interactionvisualizer.entityholders.Item;
2929
import com.loohp.interactionvisualizer.managers.DisplayManager;
30-
import com.loohp.interactionvisualizer.managers.SoundManager;
3130
import com.loohp.interactionvisualizer.objectholders.EntryKey;
3231
import com.loohp.interactionvisualizer.objectholders.LightType;
3332
import com.loohp.interactionvisualizer.utils.InventoryUtils;
@@ -381,16 +380,11 @@ public void onCraft(InventoryClickEvent event) {
381380
}, 6);
382381

383382
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
384-
Vector lift = new Vector(0.0, 0.15, 0.0);
385-
Vector pickup = player.getEyeLocation().add(0.0, -0.5, 0.0).add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector().subtract(loc.clone().add(0.5, 1.2, 0.5).toVector()).multiply(0.15).add(lift);
386383
item.setItemStack(itemstack);
387-
item.setVelocity(pickup);
388-
item.setGravity(true);
389-
item.setPickupDelay(32767);
390384
DisplayManager.updateItem(item);
385+
DisplayManager.collectItem(item, player);
391386

392387
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
393-
SoundManager.playItemPickup(item.getLocation(), InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY));
394388
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
395389
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot2);
396390
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot3);
@@ -400,7 +394,6 @@ public void onCraft(InventoryClickEvent event) {
400394
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot7);
401395
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot8);
402396
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot9);
403-
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
404397
}, 8);
405398
}, 10);
406399
}, 1);

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import com.loohp.interactionvisualizer.entityholders.Item;
3131
import com.loohp.interactionvisualizer.managers.DisplayManager;
3232
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
33-
import com.loohp.interactionvisualizer.managers.SoundManager;
3433
import com.loohp.interactionvisualizer.managers.TileEntityManager;
3534
import com.loohp.interactionvisualizer.objectholders.EntryKey;
3635
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
@@ -361,7 +360,6 @@ public void onFurnace(InventoryClickEvent event) {
361360

362361
int slot = event.getRawSlot();
363362
ItemStack itemstack = event.getCurrentItem().clone();
364-
Location loc = block.getLocation();
365363
Player player = (Player) event.getWhoClicked();
366364

367365
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
@@ -380,17 +378,8 @@ public void onFurnace(InventoryClickEvent event) {
380378
item.setItemStack(itemstack);
381379
item.setLocked(true);
382380

383-
Vector lift = new Vector(0.0, 0.15, 0.0);
384-
Vector pickup = player.getEyeLocation().add(0.0, -0.5, 0.0).add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector().subtract(loc.clone().add(0.5, 1.2, 0.5).toVector()).multiply(0.15).add(lift);
385-
item.setVelocity(pickup);
386-
item.setGravity(true);
387-
item.setPickupDelay(32767);
388381
DisplayManager.updateItem(item);
389-
390-
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
391-
SoundManager.playItemPickup(item.getLocation(), InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY));
392-
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
393-
}, 8);
382+
DisplayManager.collectItem(item, player);
394383
}, 1);
395384
}
396385

@@ -512,6 +501,7 @@ public Map<String, DisplayEntity> spawnDisplayEntitys(Block block) {
512501
Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
513502

514503
Location loc = block.getLocation().clone().add(direction).add(0.5, 0.2, 0.5);
504+
loc.setDirection(facing.getDirection());
515505
DisplayEntity slot1 = new DisplayEntity(loc.clone());
516506
setStand(slot1);
517507

0 commit comments

Comments
 (0)