-
Notifications
You must be signed in to change notification settings - Fork 0
Add optional PacketEvents-backed client-only item lore event #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ericlmao
merged 18 commits into
snapshot
from
copilot/create-client-side-item-lore-event
Apr 4, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
72d311a
Initial plan
Copilot fcc6560
feat: add client-side item lore event bridge
Copilot 3b609dc
fix: harden PacketEvents lore bridge proxy
Copilot 697fc2c
chore: revert gradlew permission change
Copilot 034a588
docs: clarify lore event mutation flow
Copilot d60dd0d
chore: restore gradlew permissions
Copilot aafc067
Add PacketEvents-backed client-only item lore event
Copilot 044e233
refactor: use direct PacketEvents API
Copilot 63cde04
style: polish PacketEvents bridge cleanup
Copilot 64e73da
docs: clarify direct PacketEvents bridge
Copilot 416bbc0
Update paper/src/main/java/games/negative/engine/paper/event/ClientIt…
ericlmao f3377e3
fix: lazily load PacketEvents lore bridge
Copilot 6db09c6
refactor: make PacketEvents bridge optional again
Copilot b587bdb
refactor: remove PacketEvents reflection loader
Copilot b2d4fc8
chore: clarify PacketEvents bridge linkage logs
Copilot 2b8fd80
chore: refine PacketEvents loader warning
Copilot 972a9c4
chore: clarify optional PacketEvents warning
Copilot b8c10be
chore: bump paper module version to 1.2.0
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
paper/src/main/java/games/negative/engine/paper/event/ClientItemLoreBridge.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package games.negative.engine.paper.event; | ||
|
|
||
| public interface ClientItemLoreBridge { | ||
|
|
||
| void enable(); | ||
|
|
||
| void disable(); | ||
| } |
85 changes: 85 additions & 0 deletions
85
paper/src/main/java/games/negative/engine/paper/event/ClientItemLoreEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| package games.negative.engine.paper.event; | ||
|
|
||
| import net.kyori.adventure.text.Component; | ||
| import org.bukkit.entity.Player; | ||
| import org.bukkit.event.HandlerList; | ||
| import org.bukkit.event.player.PlayerEvent; | ||
| import org.bukkit.inventory.ItemStack; | ||
| import org.bukkit.inventory.meta.ItemMeta; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Fired when a client-bound item packet is about to be sent to a player. | ||
| * Changes made through this event only affect the client view of the item lore. | ||
| */ | ||
| public final class ClientItemLoreEvent extends PlayerEvent { | ||
|
|
||
| private static final HandlerList HANDLERS = new HandlerList(); | ||
|
|
||
| private final int windowId; | ||
| private final int slot; | ||
| private final ItemStack originalItem; | ||
| private List<Component> lore; | ||
| private boolean modified; | ||
|
|
||
| public ClientItemLoreEvent(Player player, int windowId, int slot, ItemStack item) { | ||
| super(player); | ||
| this.windowId = windowId; | ||
| this.slot = slot; | ||
| this.originalItem = item; | ||
|
|
||
| ItemMeta meta = item.getItemMeta(); | ||
| List<Component> currentLore = meta == null ? List.of() : meta.lore(); | ||
| this.lore = currentLore == null ? new ArrayList<>() : new ArrayList<>(currentLore); | ||
| } | ||
|
|
||
| public int getWindowId() { | ||
| return windowId; | ||
| } | ||
|
|
||
| public int getSlot() { | ||
| return slot; | ||
| } | ||
|
|
||
| public ItemStack getOriginalItem() { | ||
| return originalItem.clone(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns an immutable snapshot of the current client-side lore. | ||
| * Use {@link #setLore(List)}, {@link #addLoreLine(Component)}, or {@link #clearLore()} to change it. | ||
| */ | ||
| public List<Component> getLore() { | ||
| return List.copyOf(lore); | ||
| } | ||
|
|
||
| public void setLore(List<Component> lore) { | ||
| this.lore = lore == null ? new ArrayList<>() : new ArrayList<>(lore); | ||
| this.modified = true; | ||
| } | ||
|
|
||
| public void addLoreLine(Component line) { | ||
| this.lore.add(line); | ||
| this.modified = true; | ||
| } | ||
|
|
||
| public void clearLore() { | ||
| this.lore.clear(); | ||
| this.modified = true; | ||
| } | ||
|
|
||
| boolean isModified() { | ||
| return modified; | ||
| } | ||
|
|
||
| @Override | ||
| public HandlerList getHandlers() { | ||
| return HANDLERS; | ||
| } | ||
|
|
||
| public static HandlerList getHandlerList() { | ||
| return HANDLERS; | ||
| } | ||
| } |
61 changes: 61 additions & 0 deletions
61
paper/src/main/java/games/negative/engine/paper/event/ClientItemLorePacketBridge.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package games.negative.engine.paper.event; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.bukkit.plugin.Plugin; | ||
| import org.bukkit.plugin.PluginManager; | ||
|
|
||
| /** | ||
| * Optional loader for PacketEvents-backed client item lore handling. | ||
| */ | ||
| @Slf4j | ||
| public final class ClientItemLorePacketBridge { | ||
|
|
||
| private static final String[] PACKET_EVENTS_PLUGIN_NAMES = {"packetevents", "PacketEvents"}; | ||
|
|
||
| private final Plugin plugin; | ||
| private ClientItemLoreBridge delegate; | ||
|
|
||
| public ClientItemLorePacketBridge(Plugin plugin) { | ||
| this.plugin = plugin; | ||
| } | ||
|
|
||
| public void enable() { | ||
| if (delegate != null) return; | ||
| if (!isPacketEventsPresent()) { | ||
| log.info("PacketEvents not present; client item lore bridge will remain disabled"); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| ClientItemLoreBridge bridge = new PacketEventsClientItemLorePacketBridge(plugin); | ||
| bridge.enable(); | ||
| this.delegate = bridge; | ||
| } catch (NoClassDefFoundError exception) { | ||
| log.warn("Failed to initialize optional PacketEvents client item lore bridge; runtime classes are unavailable", exception); | ||
| } catch (LinkageError exception) { | ||
| log.warn("Failed to initialize PacketEvents client item lore bridge due to a linkage problem", exception); | ||
| } | ||
| } | ||
|
|
||
| public void disable() { | ||
| if (delegate == null) return; | ||
|
|
||
| delegate.disable(); | ||
| delegate = null; | ||
| } | ||
|
|
||
| private boolean isPacketEventsPresent() { | ||
| return getPacketEventsPlugin() != null; | ||
| } | ||
|
|
||
| private Plugin getPacketEventsPlugin() { | ||
| PluginManager pluginManager = plugin.getServer().getPluginManager(); | ||
| for (String pluginName : PACKET_EVENTS_PLUGIN_NAMES) { | ||
| Plugin packetEventsPlugin = pluginManager.getPlugin(pluginName); | ||
| if (packetEventsPlugin != null) { | ||
| return packetEventsPlugin; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| } |
150 changes: 150 additions & 0 deletions
150
...c/main/java/games/negative/engine/paper/event/PacketEventsClientItemLorePacketBridge.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package games.negative.engine.paper.event; | ||
|
|
||
| import com.github.retrooper.packetevents.PacketEvents; | ||
| import com.github.retrooper.packetevents.event.PacketListenerAbstract; | ||
| import com.github.retrooper.packetevents.event.PacketListenerCommon; | ||
| import com.github.retrooper.packetevents.event.PacketSendEvent; | ||
| import com.github.retrooper.packetevents.protocol.item.ItemStack; | ||
| import com.github.retrooper.packetevents.protocol.packettype.PacketType; | ||
| import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot; | ||
| import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowItems; | ||
| import io.github.retrooper.packetevents.util.SpigotConversionUtil; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.kyori.adventure.text.Component; | ||
| import org.bukkit.entity.Player; | ||
| import org.bukkit.inventory.meta.ItemMeta; | ||
| import org.bukkit.plugin.Plugin; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * PacketEvents-specific bridge that fires {@link ClientItemLoreEvent} for outgoing item packets. | ||
| */ | ||
| @Slf4j | ||
| public final class PacketEventsClientItemLorePacketBridge extends PacketListenerAbstract implements ClientItemLoreBridge { | ||
|
|
||
| private static final int CARRIED_ITEM_SLOT = -1; | ||
|
|
||
| private final Plugin plugin; | ||
| private PacketListenerCommon registeredListener; | ||
|
|
||
| public PacketEventsClientItemLorePacketBridge(Plugin plugin) { | ||
| super(); | ||
| this.plugin = plugin; | ||
| } | ||
|
|
||
| @Override | ||
| public void enable() { | ||
| if (registeredListener != null) return; | ||
| if (PacketEvents.getAPI() == null) { | ||
| log.warn("PacketEvents API is unavailable; client item lore bridge was not registered"); | ||
| return; | ||
| } | ||
|
|
||
| this.registeredListener = PacketEvents.getAPI().getEventManager().registerListener(this); | ||
| log.info("Enabled PacketEvents client item lore bridge"); | ||
| } | ||
|
|
||
| @Override | ||
| public void disable() { | ||
| if (registeredListener == null) return; | ||
| if (PacketEvents.getAPI() == null) { | ||
| this.registeredListener = null; | ||
| return; | ||
| } | ||
|
|
||
| PacketEvents.getAPI().getEventManager().unregisterListener(registeredListener); | ||
| this.registeredListener = null; | ||
| } | ||
|
|
||
| @Override | ||
| public void onPacketSend(PacketSendEvent event) { | ||
| if (!(event.getPlayer() instanceof Player player)) return; | ||
|
|
||
| if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) { | ||
| handleSetSlot(event, player); | ||
| return; | ||
| } | ||
|
|
||
| if (event.getPacketType() == PacketType.Play.Server.WINDOW_ITEMS) { | ||
| handleWindowItems(event, player); | ||
| } | ||
| } | ||
|
|
||
| private void handleSetSlot(PacketSendEvent event, Player player) { | ||
| WrapperPlayServerSetSlot wrapper = new WrapperPlayServerSetSlot(event); | ||
| ItemStack updatedItem = applyLore( | ||
| player, | ||
| wrapper.getWindowId(), | ||
| wrapper.getSlot(), | ||
| wrapper.getItem() | ||
| ); | ||
|
|
||
| if (updatedItem != null) { | ||
| wrapper.setItem(updatedItem); | ||
| } | ||
| } | ||
|
|
||
| private void handleWindowItems(PacketSendEvent event, Player player) { | ||
| WrapperPlayServerWindowItems wrapper = new WrapperPlayServerWindowItems(event); | ||
|
|
||
| List<ItemStack> items = wrapper.getItems(); | ||
| List<ItemStack> modifiedItems = new ArrayList<>(items.size()); | ||
| for (int slot = 0; slot < items.size(); slot++) { | ||
| ItemStack originalItem = items.get(slot); | ||
| ItemStack updatedItem = applyLore( | ||
| player, | ||
| wrapper.getWindowId(), | ||
| slot, | ||
| originalItem | ||
| ); | ||
| modifiedItems.add(updatedItem == null ? originalItem : updatedItem); | ||
| } | ||
| wrapper.setItems(modifiedItems); | ||
|
|
||
| Optional<ItemStack> carriedItem = wrapper.getCarriedItem(); | ||
| if (carriedItem.isPresent()) { | ||
| ItemStack updatedCarriedItem = applyLore( | ||
| player, | ||
| wrapper.getWindowId(), | ||
| CARRIED_ITEM_SLOT, | ||
| carriedItem.get() | ||
| ); | ||
| if (updatedCarriedItem != null) { | ||
| wrapper.setCarriedItem(updatedCarriedItem); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private ItemStack applyLore( | ||
| Player player, | ||
| int windowId, | ||
| int slot, | ||
| ItemStack packetItem | ||
| ) { | ||
| if (packetItem == null) return null; | ||
|
|
||
| org.bukkit.inventory.ItemStack bukkitItem = SpigotConversionUtil.toBukkitItemStack(packetItem); | ||
| if (bukkitItem == null || bukkitItem.getType().isAir()) return null; | ||
|
|
||
| ClientItemLoreEvent event = new ClientItemLoreEvent(player, windowId, slot, bukkitItem); | ||
| plugin.getServer().getPluginManager().callEvent(event); | ||
|
|
||
| if (!event.isModified()) { | ||
| return null; | ||
| } | ||
|
|
||
| org.bukkit.inventory.ItemStack clientItem = bukkitItem.clone(); | ||
| ItemMeta meta = clientItem.getItemMeta(); | ||
| if (meta == null) { | ||
| log.warn("Unable to apply client-side lore to item without item meta: {}", clientItem.getType()); | ||
| return null; | ||
| } | ||
|
|
||
| meta.lore(event.getLore()); | ||
| clientItem.setItemMeta(meta); | ||
| return SpigotConversionUtil.fromBukkitItemStack(clientItem); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.