Skip to content

Commit b6ddaa6

Browse files
authored
Add optional PacketEvents-backed client-only item lore event (#5)
1 parent b6ba3fe commit b6ddaa6

8 files changed

Lines changed: 330 additions & 3 deletions

File tree

build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ subprojects {
1313
maven("https://repo.negative.games/repository/maven-releases/")
1414
maven("https://repo.negative.games/repository/maven-snapshots/")
1515

16+
// CodeMC
17+
maven("https://repo.codemc.io/repository/maven-releases/")
18+
maven("https://repo.codemc.io/repository/maven-snapshots/")
19+
1620
// PaperMC
1721
maven("https://repo.papermc.io/repository/maven-public/")
1822

@@ -47,4 +51,4 @@ subprojects {
4751
}
4852
}
4953
}
50-
}
54+
}

gradlew

100644100755
File mode changed.

paper/build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66

77
var id = "plugin-engine-paper"
88
var domain = "games.negative.engine"
9-
var apiVersion = "1.1.0"
9+
var apiVersion = "1.2.0"
1010

1111
repositories {
1212
mavenCentral()
@@ -29,6 +29,9 @@ dependencies {
2929
// PlaceholderAPI
3030
compileOnly("me.clip:placeholderapi:2.11.7")
3131

32+
// PacketEvents
33+
compileOnly("com.github.retrooper:packetevents-spigot:2.11.2")
34+
3235
// Spring & Jakarta
3336
compileOnly("org.springframework:spring-context:6.2.13")
3437
compileOnly("jakarta.annotation:jakarta.annotation-api:3.0.0")
@@ -89,4 +92,4 @@ publishing {
8992
}
9093
}
9194
}
92-
}
95+
}

paper/src/main/java/games/negative/engine/paper/PaperPlugin.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import games.negative.engine.Plugin;
44
import games.negative.engine.message.util.MiniMessageUtil;
5+
import games.negative.engine.paper.event.ClientItemLorePacketBridge;
56
import games.negative.engine.paper.scheduler.Scheduler;
67
import games.negative.engine.state.Reloadable;
78
import games.negative.moss.paper.MossPaper;
@@ -20,6 +21,8 @@
2021
@Slf4j
2122
public abstract class PaperPlugin extends MossPaper implements Plugin {
2223

24+
private ClientItemLorePacketBridge clientItemLorePacketBridge;
25+
2326
@Override
2427
public void loadInitialComponents(AnnotationConfigApplicationContext context) {
2528
super.loadInitialComponents(context);
@@ -37,6 +40,19 @@ public void onEnable() {
3740
listener -> getServer().getPluginManager().registerEvents(listener, this),
3841
(listener, e) -> log.error("Failed to register listener: {}", listener.getClass().getSimpleName(), e)
3942
);
43+
44+
this.clientItemLorePacketBridge = new ClientItemLorePacketBridge(this);
45+
this.clientItemLorePacketBridge.enable();
46+
}
47+
48+
@Override
49+
public void onDisable() {
50+
if (this.clientItemLorePacketBridge != null) {
51+
this.clientItemLorePacketBridge.disable();
52+
this.clientItemLorePacketBridge = null;
53+
}
54+
55+
super.onDisable();
4056
}
4157

4258
@Override
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package games.negative.engine.paper.event;
2+
3+
public interface ClientItemLoreBridge {
4+
5+
void enable();
6+
7+
void disable();
8+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package games.negative.engine.paper.event;
2+
3+
import net.kyori.adventure.text.Component;
4+
import org.bukkit.entity.Player;
5+
import org.bukkit.event.HandlerList;
6+
import org.bukkit.event.player.PlayerEvent;
7+
import org.bukkit.inventory.ItemStack;
8+
import org.bukkit.inventory.meta.ItemMeta;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
13+
/**
14+
* Fired when a client-bound item packet is about to be sent to a player.
15+
* Changes made through this event only affect the client view of the item lore.
16+
*/
17+
public final class ClientItemLoreEvent extends PlayerEvent {
18+
19+
private static final HandlerList HANDLERS = new HandlerList();
20+
21+
private final int windowId;
22+
private final int slot;
23+
private final ItemStack originalItem;
24+
private List<Component> lore;
25+
private boolean modified;
26+
27+
public ClientItemLoreEvent(Player player, int windowId, int slot, ItemStack item) {
28+
super(player);
29+
this.windowId = windowId;
30+
this.slot = slot;
31+
this.originalItem = item;
32+
33+
ItemMeta meta = item.getItemMeta();
34+
List<Component> currentLore = meta == null ? List.of() : meta.lore();
35+
this.lore = currentLore == null ? new ArrayList<>() : new ArrayList<>(currentLore);
36+
}
37+
38+
public int getWindowId() {
39+
return windowId;
40+
}
41+
42+
public int getSlot() {
43+
return slot;
44+
}
45+
46+
public ItemStack getOriginalItem() {
47+
return originalItem.clone();
48+
}
49+
50+
/**
51+
* Returns an immutable snapshot of the current client-side lore.
52+
* Use {@link #setLore(List)}, {@link #addLoreLine(Component)}, or {@link #clearLore()} to change it.
53+
*/
54+
public List<Component> getLore() {
55+
return List.copyOf(lore);
56+
}
57+
58+
public void setLore(List<Component> lore) {
59+
this.lore = lore == null ? new ArrayList<>() : new ArrayList<>(lore);
60+
this.modified = true;
61+
}
62+
63+
public void addLoreLine(Component line) {
64+
this.lore.add(line);
65+
this.modified = true;
66+
}
67+
68+
public void clearLore() {
69+
this.lore.clear();
70+
this.modified = true;
71+
}
72+
73+
boolean isModified() {
74+
return modified;
75+
}
76+
77+
@Override
78+
public HandlerList getHandlers() {
79+
return HANDLERS;
80+
}
81+
82+
public static HandlerList getHandlerList() {
83+
return HANDLERS;
84+
}
85+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package games.negative.engine.paper.event;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.bukkit.plugin.Plugin;
5+
import org.bukkit.plugin.PluginManager;
6+
7+
/**
8+
* Optional loader for PacketEvents-backed client item lore handling.
9+
*/
10+
@Slf4j
11+
public final class ClientItemLorePacketBridge {
12+
13+
private static final String[] PACKET_EVENTS_PLUGIN_NAMES = {"packetevents", "PacketEvents"};
14+
15+
private final Plugin plugin;
16+
private ClientItemLoreBridge delegate;
17+
18+
public ClientItemLorePacketBridge(Plugin plugin) {
19+
this.plugin = plugin;
20+
}
21+
22+
public void enable() {
23+
if (delegate != null) return;
24+
if (!isPacketEventsPresent()) {
25+
log.info("PacketEvents not present; client item lore bridge will remain disabled");
26+
return;
27+
}
28+
29+
try {
30+
ClientItemLoreBridge bridge = new PacketEventsClientItemLorePacketBridge(plugin);
31+
bridge.enable();
32+
this.delegate = bridge;
33+
} catch (NoClassDefFoundError exception) {
34+
log.warn("Failed to initialize optional PacketEvents client item lore bridge; runtime classes are unavailable", exception);
35+
} catch (LinkageError exception) {
36+
log.warn("Failed to initialize PacketEvents client item lore bridge due to a linkage problem", exception);
37+
}
38+
}
39+
40+
public void disable() {
41+
if (delegate == null) return;
42+
43+
delegate.disable();
44+
delegate = null;
45+
}
46+
47+
private boolean isPacketEventsPresent() {
48+
return getPacketEventsPlugin() != null;
49+
}
50+
51+
private Plugin getPacketEventsPlugin() {
52+
PluginManager pluginManager = plugin.getServer().getPluginManager();
53+
for (String pluginName : PACKET_EVENTS_PLUGIN_NAMES) {
54+
Plugin packetEventsPlugin = pluginManager.getPlugin(pluginName);
55+
if (packetEventsPlugin != null) {
56+
return packetEventsPlugin;
57+
}
58+
}
59+
return null;
60+
}
61+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package games.negative.engine.paper.event;
2+
3+
import com.github.retrooper.packetevents.PacketEvents;
4+
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
5+
import com.github.retrooper.packetevents.event.PacketListenerCommon;
6+
import com.github.retrooper.packetevents.event.PacketSendEvent;
7+
import com.github.retrooper.packetevents.protocol.item.ItemStack;
8+
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
9+
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot;
10+
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowItems;
11+
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
12+
import lombok.extern.slf4j.Slf4j;
13+
import net.kyori.adventure.text.Component;
14+
import org.bukkit.entity.Player;
15+
import org.bukkit.inventory.meta.ItemMeta;
16+
import org.bukkit.plugin.Plugin;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Optional;
21+
22+
/**
23+
* PacketEvents-specific bridge that fires {@link ClientItemLoreEvent} for outgoing item packets.
24+
*/
25+
@Slf4j
26+
public final class PacketEventsClientItemLorePacketBridge extends PacketListenerAbstract implements ClientItemLoreBridge {
27+
28+
private static final int CARRIED_ITEM_SLOT = -1;
29+
30+
private final Plugin plugin;
31+
private PacketListenerCommon registeredListener;
32+
33+
public PacketEventsClientItemLorePacketBridge(Plugin plugin) {
34+
super();
35+
this.plugin = plugin;
36+
}
37+
38+
@Override
39+
public void enable() {
40+
if (registeredListener != null) return;
41+
if (PacketEvents.getAPI() == null) {
42+
log.warn("PacketEvents API is unavailable; client item lore bridge was not registered");
43+
return;
44+
}
45+
46+
this.registeredListener = PacketEvents.getAPI().getEventManager().registerListener(this);
47+
log.info("Enabled PacketEvents client item lore bridge");
48+
}
49+
50+
@Override
51+
public void disable() {
52+
if (registeredListener == null) return;
53+
if (PacketEvents.getAPI() == null) {
54+
this.registeredListener = null;
55+
return;
56+
}
57+
58+
PacketEvents.getAPI().getEventManager().unregisterListener(registeredListener);
59+
this.registeredListener = null;
60+
}
61+
62+
@Override
63+
public void onPacketSend(PacketSendEvent event) {
64+
if (!(event.getPlayer() instanceof Player player)) return;
65+
66+
if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
67+
handleSetSlot(event, player);
68+
return;
69+
}
70+
71+
if (event.getPacketType() == PacketType.Play.Server.WINDOW_ITEMS) {
72+
handleWindowItems(event, player);
73+
}
74+
}
75+
76+
private void handleSetSlot(PacketSendEvent event, Player player) {
77+
WrapperPlayServerSetSlot wrapper = new WrapperPlayServerSetSlot(event);
78+
ItemStack updatedItem = applyLore(
79+
player,
80+
wrapper.getWindowId(),
81+
wrapper.getSlot(),
82+
wrapper.getItem()
83+
);
84+
85+
if (updatedItem != null) {
86+
wrapper.setItem(updatedItem);
87+
}
88+
}
89+
90+
private void handleWindowItems(PacketSendEvent event, Player player) {
91+
WrapperPlayServerWindowItems wrapper = new WrapperPlayServerWindowItems(event);
92+
93+
List<ItemStack> items = wrapper.getItems();
94+
List<ItemStack> modifiedItems = new ArrayList<>(items.size());
95+
for (int slot = 0; slot < items.size(); slot++) {
96+
ItemStack originalItem = items.get(slot);
97+
ItemStack updatedItem = applyLore(
98+
player,
99+
wrapper.getWindowId(),
100+
slot,
101+
originalItem
102+
);
103+
modifiedItems.add(updatedItem == null ? originalItem : updatedItem);
104+
}
105+
wrapper.setItems(modifiedItems);
106+
107+
Optional<ItemStack> carriedItem = wrapper.getCarriedItem();
108+
if (carriedItem.isPresent()) {
109+
ItemStack updatedCarriedItem = applyLore(
110+
player,
111+
wrapper.getWindowId(),
112+
CARRIED_ITEM_SLOT,
113+
carriedItem.get()
114+
);
115+
if (updatedCarriedItem != null) {
116+
wrapper.setCarriedItem(updatedCarriedItem);
117+
}
118+
}
119+
}
120+
121+
private ItemStack applyLore(
122+
Player player,
123+
int windowId,
124+
int slot,
125+
ItemStack packetItem
126+
) {
127+
if (packetItem == null) return null;
128+
129+
org.bukkit.inventory.ItemStack bukkitItem = SpigotConversionUtil.toBukkitItemStack(packetItem);
130+
if (bukkitItem == null || bukkitItem.getType().isAir()) return null;
131+
132+
ClientItemLoreEvent event = new ClientItemLoreEvent(player, windowId, slot, bukkitItem);
133+
plugin.getServer().getPluginManager().callEvent(event);
134+
135+
if (!event.isModified()) {
136+
return null;
137+
}
138+
139+
org.bukkit.inventory.ItemStack clientItem = bukkitItem.clone();
140+
ItemMeta meta = clientItem.getItemMeta();
141+
if (meta == null) {
142+
log.warn("Unable to apply client-side lore to item without item meta: {}", clientItem.getType());
143+
return null;
144+
}
145+
146+
meta.lore(event.getLore());
147+
clientItem.setItemMeta(meta);
148+
return SpigotConversionUtil.fromBukkitItemStack(clientItem);
149+
}
150+
}

0 commit comments

Comments
 (0)