ItemMessage: use item metadata to create popup messages

Discussion in 'Resources' started by desht, Aug 15, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    desht

    iZanax Not really sure what you mean by "disable regular displayname popup" - if you mean stop the client showing the name of items when they're selected, then that's impossible without a client mod, sorry.

    Sending a message without being queued up - that could be added, yes. The problem with that is that it's very easily abused - if everyone simply sends their message as urgent, then you'd end up with the last message sent being displayed first, and it just gets a bit silly.
     
  2. Offline

    Netizen

    Hey desht ! Thanks for this, it's great!

    BTW I believe I've reproduced the issue Assist had with the names not returning back to normal.
    For me, if I open my creative inventory while the message is being displayed, the message gets stuck on the item in my hand. Not a big deal for me since my players wont be in creative mode.
     
  3. Offline

    desht

    Thanks, I'll take a look into that.

    (Does re-logging clear the stuck message? Or even moving the item around in your inventory? Since the message is purely client-side, it should be easy enough to get rid of...)

    Update: confirmed, it does indeed behave weirdly in creative mode, leaving the message on the item, and it won't even clear if you re-log. I'll be looking into that...
     
    MTN likes this.
  4. Offline

    desht

    Should just mention that the current version (see link in first post) simply sends the message via player.sendMessage() if the player's in creative mode, and doesn't try to do anything clever.
     
  5. Offline

    Comphenix

    I can probably explain what's happening.

    In creative mode, the server will actually receive the modified item from the client (through Packet107SetCreativeSlot), and replace it in the server-side inventory. This is because the creative inventory is entirely client-side, along with the creation of new item stacks. Quite the hack, in my view.

    But it is possible to get around this. In ItemRenamer, I intercept the creative slot packet before it's processed by the server, and undo the modification by saving the original item stack in a custom NBT tag.

    Notice that I use getNetworkMarker() and read the item stack manually from the original packet buffer. This is because Bukkit will remove custom NBT tags when the creative slot packet is received. But you don't have to use custom NBT tags to store your data - it's also possible to use the new attribute system for that purpose.
     
    mkremins and desht like this.
  6. Offline

    MTN

    By the way, item messages seem to overwrite enchantments (temporally). If I hold an enchanted item, it looses the glow effect while the messages are received.

    In addition to that I noticed that it is sometimes not working for Items I had in my hand before (and receiving a message with them), if I scroll (did it with an empty space) it does work, scrolling back on the other items, does not. But the even weirder thing is that the item begins to move down and back up very fast, just as it would if you scroll really fast. It does that in an endless loop until I open the inventory and move the bugging items.

    Edit: I edited some small things, but as you can see in my pull request, nothing exotic.
     
  7. Offline

    desht

    Thanks for the PR - couple of minor tweaks needed (see my comments on github) and I'll pull it :)

    As for the enchanted item glow - yeah, the overwritten item meta would stop that glow. Should be purely visual though - as far as the server is concerned, it's still an enchanted item. I should be able to add a glow to the faked metadata for items which already have an enchantment, though.

    As for the scrolling issue, that does sound rather weird. I'll play around with that and see if I can reproduce it.

    I think I have some work to do here :)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 3, 2016
  8. Offline

    MTN

    Did the tweaks by the way ;)

    I think it's easier to reproduce if you kick the speed up a notch (I tried sending every 5 ticks)
     
  9. Offline

    Ad237

    desht Is it possible to disable the queue feature? If not could you add a option to disable it? I plan on using this for real time notifications and I would prefer if they weren't delayed due to the queue system. Thanks.
     
  10. Offline

    Flybelette

    Hi ! you made an Amazing work :) but i found some bugs :p We can't break blocks when we got the PopUp, then when we Right-Click with the PopUp the DisplayName of the Item appeer :) That's all, Bye ! :D
     
  11. Offline

    desht

    Right now, there's no way around the queue feature. I could implement a disable option, but consider: what happens when every other plugin decides to jump the queue too? Chaos would ensue as a bunch of messages all try to get displayed at the same time. So I'm disinclined to change the current behaviour...
     
  12. Offline

    MTN

    It should just cancel all the previous tasks, so that there is just one timer running at once.
     
  13. Offline

    MrOAriO

    Can you add a function to send this without a queue ? so that you send one and it will shown if a new one will send ?
     
  14. Offline

    MTN

    I noticed that it is possible (via /reload on my dev server) that


    Code:java
    1.  
    2. private PriorityQueue<MessageRecord> getMessageQueue(Player player) {
    3. ...
    4. }
    5.  


    returns null, causing NullPointerExceptions
     
  15. Offline

    vasil7112

    Hello there,
    Do you have something like that, that doesn't require ProtocolLib?
    Thanks for your efforts!
    Kind regards,
    vasil7112

    Hello there again.

    My results without using ProtocolLib:
    Code:java
    1. package me.vasil7112.SleepyFeeling.Util;
    2.  
    3. import java.lang.ref.WeakReference;
    4. import java.lang.reflect.Method;
    5. import java.util.List;
    6. import java.util.PriorityQueue;
    7.  
    8. import net.minecraft.server.v1_7_R1.PacketPlayOutSetSlot;
    9.  
    10. import org.apache.commons.lang.Validate;
    11. import org.bukkit.Bukkit;
    12. import org.bukkit.GameMode;
    13. import org.bukkit.Material;
    14. import org.bukkit.craftbukkit.v1_7_R1.entity.CraftPlayer;
    15. import org.bukkit.craftbukkit.v1_7_R1.inventory.CraftItemStack;
    16. import org.bukkit.entity.Player;
    17. import org.bukkit.event.EventHandler;
    18. import org.bukkit.event.EventPriority;
    19. import org.bukkit.event.HandlerList;
    20. import org.bukkit.event.Listener;
    21. import org.bukkit.event.player.PlayerItemHeldEvent;
    22. import org.bukkit.event.server.PluginDisableEvent;
    23. import org.bukkit.inventory.ItemStack;
    24. import org.bukkit.inventory.meta.ItemMeta;
    25. import org.bukkit.metadata.FixedMetadataValue;
    26. import org.bukkit.metadata.MetadataValue;
    27. import org.bukkit.plugin.Plugin;
    28. import org.bukkit.scheduler.BukkitRunnable;
    29. /**
    30. * Minecraft displays the item tooltip to the player briefly when it's changed. Let's (ab)use that to
    31. * send a "popup" message to the player via one of these tooltips. This is nice for temporary messages
    32. * where you don't want to clutter up the chat window.
    33. */
    34. public class ItemMessage {
    35. private int interval = 20; // ticks
    36. private static final int DEFAULT_DURATION = 2; // seconds
    37. private static final int DEFAULT_PRIORITY = 0;
    38. private static final String DEF_FORMAT_1 = "%s";
    39. private static final String DEF_FORMAT_2 = " %s ";
    40. private static final String METADATA_Q_KEY = "item-message:msg-queue";
    41. private static final String METADATA_ID_KEY = "item-message:id";
    42. private final Plugin plugin;
    43.  
    44. private String[] formats = new String[] { DEF_FORMAT_1, DEF_FORMAT_2 };
    45. private Material emptyHandReplacement = Material.SNOW;
    46.  
    47. public ItemMessage(Plugin plugin) {
    48. this.plugin = plugin;
    49. }
    50.  
    51. /**
    52.   * set the interval the player will receive packets with the formatted message
    53.   * Default is 20 for every second
    54.   * @param interval in ticks
    55.   * @throws IllegalArgumentException if interval is below 1
    56.   */
    57. public void setInterval(int interval){
    58. Validate.isTrue(interval > 0, "Interval can't be below 1!");
    59. this.interval = interval;
    60. }
    61.  
    62. /**
    63.   * Set which item the player should held if he receives a message
    64.   * without having something in his hand. Default is a snow layer
    65.   * @param material
    66.   * @throws IllegalArgumentException if material is null
    67.   */
    68. public void setEmptyHandReplacement(Material material){
    69. Validate.notNull(material,"There must be a replacement for an empty hand!");
    70. this.emptyHandReplacement = material;
    71. }
    72.  
    73. /**
    74.   * Send a popup message to the player, with a default duration of 2 seconds and default
    75.   * priority level of 0.
    76.   *
    77.   * @param message the message to send
    78.   * @throws IllegalStateException if the player is unavailable (e.g. went offline)
    79.   */
    80. public void sendMessage(Player player, String message) {
    81. sendMessage(player, message, DEFAULT_DURATION, DEFAULT_PRIORITY);
    82. }
    83.  
    84. /**
    85.   * Send a popup message to the player, for the given duration and default priority level
    86.   * of 0.
    87.   *
    88.   * @param message the message to send
    89.   * @param duration the duration, in seconds, for which the message will be displayed
    90.   * @throws IllegalStateException if the player is unavailable (e.g. went offline)
    91.   */
    92. public void sendMessage(Player player, String message, int duration) {
    93. sendMessage(player, message, duration, DEFAULT_PRIORITY);
    94. }
    95.  
    96. /**
    97.   * Send a popup message to the player, for the given duration and priority level.
    98.   *
    99.   * @param message the message to send
    100.   * @param duration the duration, in seconds, for which the message will be displayed
    101.   * @param priority priority of this message
    102.   * @throws IllegalStateException if the player is unavailable (e.g. went offline)
    103.   */
    104. public void sendMessage(Player player, String message, int duration, int priority) {
    105. if (player.getGameMode() == GameMode.CREATIVE) {
    106. // TODO: this doesn't work properly in creative mode. Need to investigate further
    107. // if it can be made to work, but for now, just send an old-fashioned chat message.
    108. player.sendMessage(message);
    109. } else {
    110. PriorityQueue<MessageRecord> msgQueue = getMessageQueue(player);
    111. msgQueue.add(new MessageRecord(message, duration, priority, getNextId(player)));
    112. if (msgQueue.size() == 1) {
    113. // there was nothing in the queue previously - kick off a NamerTask
    114. // (if there was already something in the queue, a new NamerTask will be kicked off
    115. // when the current task completes - see notifyDone())
    116. new NamerTask(player, msgQueue.peek()).runTaskTimer(plugin, 1L, interval);
    117. }
    118. }
    119. }
    120.  
    121. /**
    122.   * Set the alternating format strings for message display. The strings must be different
    123.   * and must each contain one (and only one) occurrence of '%s'.
    124.   *
    125.   * @param formats the format strings
    126.   * @throws IllegalArgumentException if the strings are the same, or do not contain a %s
    127.   */
    128. public void setFormats(String... formats){
    129. Validate.isTrue(formats.length > 1, "Two formats are minimum!");
    130. for(String format : formats){
    131. Validate.isTrue(format.contains("%s"), "format string \"" + format + "\" must contain a %s");
    132. }
    133. this.formats = formats;
    134. }
    135.  
    136. private long getNextId(Player player) {
    137. long id;
    138. if (player.hasMetadata(METADATA_ID_KEY)) {
    139. List<MetadataValue> l = player.getMetadata(METADATA_ID_KEY);
    140. id = l.size() >= 1 ? l.get(0).asLong() : 1L;
    141. } else {
    142. id = 1L;
    143. }
    144. player.setMetadata(METADATA_ID_KEY, new FixedMetadataValue(plugin, id + 1));
    145. return id;
    146. }
    147.  
    148. @SuppressWarnings("unchecked")
    149. private PriorityQueue<MessageRecord> getMessageQueue(Player player) {
    150. if (!player.hasMetadata(METADATA_Q_KEY)) {
    151. player.setMetadata(METADATA_Q_KEY, new FixedMetadataValue(plugin, new PriorityQueue<MessageRecord>()));
    152. }
    153. for (MetadataValue v : player.getMetadata(METADATA_Q_KEY)) {
    154. if (v.value() instanceof PriorityQueue<?>) {
    155. return (PriorityQueue<MessageRecord>) v.value();
    156. }
    157. }
    158. return null;
    159. }
    160.  
    161. private void notifyDone(Player player) {
    162. PriorityQueue<MessageRecord> msgQueue = getMessageQueue(player);
    163. msgQueue.poll();
    164. if (!msgQueue.isEmpty()) {
    165. MessageRecord rec = importOtherMessageRecord(msgQueue.peek());
    166. new NamerTask(player, rec).runTaskTimer(plugin, 1L, interval);
    167. }
    168. }
    169.  
    170. /**
    171.   * Import a foreign MessageRecord object, if possible. Why is this necessary? There may be multiple
    172.   * plugins putting message records into a player's metadata, and objects from different plugins are
    173.   * likely to be (should be!) in different packages, and will not be castable to one another. So we
    174.   * use reflection to convert the foreign MessageRecord's data into our local object.
    175.   *
    176.   * @param other the foreign message record
    177.   * @return a MessageRecord with the imported data, or null if there was a problem
    178.   */
    179. private MessageRecord importOtherMessageRecord(Object other) {
    180. if (other instanceof MessageRecord) {
    181. return (MessageRecord) other;
    182. } else if (other.getClass().getName().endsWith(".ItemMessage$MessageRecord")) {
    183. // looks like the same class as us - we make no assumptions about what package it's in, though
    184. try {
    185. Method m1 = other.getClass().getMethod("getId");
    186. Method m2 = other.getClass().getMethod("getPriority");
    187. Method m3 = other.getClass().getMethod("getMessage");
    188. Method m4 = other.getClass().getMethod("getDuration");
    189. long otherId = (Long) m1.invoke(other);
    190. int otherPriority = (Integer) m2.invoke(other);
    191. String otherMessage = (String) m3.invoke(other);
    192. int otherDuration = (Integer) m4.invoke(other);
    193. return new MessageRecord(otherMessage, otherDuration, otherPriority, otherId);
    194. } catch (Exception e) {
    195. e.printStackTrace();
    196. return null;
    197. }
    198. } else {
    199. return null;
    200. }
    201. }
    202.  
    203. private class NamerTask extends BukkitRunnable implements Listener {
    204. private final WeakReference<Player> playerRef;
    205. private final String message;
    206. private int slot;
    207. private int iterations;
    208.  
    209. public NamerTask(Player player, MessageRecord rec) {
    210. this.playerRef = new WeakReference<Player>(player);
    211. this.iterations = Math.max(1, (rec.getDuration() * 20) / interval);
    212. this.slot = player.getInventory().getHeldItemSlot();
    213. this.message = rec.getMessage();
    214. Bukkit.getPluginManager().registerEvents(this, plugin);
    215. }
    216.  
    217. @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    218. public void onItemHeldChange(PlayerItemHeldEvent event) {
    219. Player player = event.getPlayer();
    220. if (player.equals(playerRef.get())) {
    221. sendItemSlotChange(player, event.getPreviousSlot(), player.getInventory().getItem(event.getPreviousSlot()));
    222. slot = event.getNewSlot();
    223. refresh(event.getPlayer());
    224. }
    225. }
    226.  
    227. @EventHandler
    228. public void onPluginDisable(PluginDisableEvent event) {
    229. Player player = playerRef.get();
    230. if (event.getPlugin() == plugin && player != null) {
    231. getMessageQueue(player).clear();
    232. finish(playerRef.get());
    233. }
    234. }
    235.  
    236. @Override
    237. public void run() {
    238. Player player = playerRef.get();
    239. if (player != null) {
    240. if (iterations-- <= 0) {
    241. // finished - restore the previous item data and tidy up
    242. finish(player);
    243. } else {
    244. // refresh the item data
    245. refresh(player);
    246. }
    247. } else {
    248. // player probably disconnected - whatever, we're done here
    249. cleanup();
    250. }
    251. }
    252.  
    253. private void refresh(Player player) {
    254. sendItemSlotChange(player, slot, makeStack(player));
    255. }
    256.  
    257. private void finish(Player player) {
    258. sendItemSlotChange(player, slot, player.getInventory().getItem(slot));
    259. notifyDone(player);
    260. cleanup();
    261. }
    262.  
    263. private void cleanup() {
    264. cancel();
    265. HandlerList.unregisterAll(this);
    266. }
    267.  
    268. private ItemStack makeStack(Player player) {
    269. ItemStack stack0 = player.getInventory().getItem(slot);
    270. ItemStack stack;
    271. if (stack0 == null || stack0.getType() == Material.AIR) {
    272. // an empty slot can't display any custom item name, so we need to fake an item
    273. // a snow layer is a good choice, since it's visually quite unobtrusive
    274. stack = new ItemStack(emptyHandReplacement, 1);
    275. } else {
    276. stack = new ItemStack(stack0.getType(), stack0.getAmount(), stack0.getDurability());
    277. }
    278. ItemMeta meta = Bukkit.getItemFactory().getItemMeta(stack.getType());
    279. // fool the client into thinking the item name has changed, so it actually (re)displays it
    280. meta.setDisplayName(String.format(formats[iterations % formats.length], message));
    281. stack.setItemMeta(meta);
    282. return stack;
    283. }
    284.  
    285. private void sendItemSlotChange(Player player, int slot, ItemStack stack) {
    286. CraftPlayer cPlayer = (CraftPlayer) player;
    287. PacketPlayOutSetSlot packet = new PacketPlayOutSetSlot(0,slot + 36 , CraftItemStack.asNMSCopy(stack));
    288. cPlayer.getHandle().playerConnection.sendPacket(packet);
    289. }
    290. }
    291.  
    292. public class MessageRecord implements Comparable<Object> {
    293. private final String message;
    294. private final int duration;
    295. private final int priority;
    296. private final long id;
    297.  
    298. public MessageRecord(String message, int duration, int priority, long id) {
    299. this.message = message;
    300. this.duration = duration;
    301. this.priority = priority;
    302. this.id = id;
    303. }
    304.  
    305. public String getMessage() {
    306. return message;
    307. }
    308.  
    309. public int getDuration() {
    310. return duration;
    311. }
    312.  
    313. public int getPriority() {
    314. return priority;
    315. }
    316.  
    317. public long getId() {
    318. return id;
    319. }
    320.  
    321. @Override
    322. public int compareTo(Object other) {
    323. MessageRecord rec = importOtherMessageRecord(other);
    324. if (rec != null) {
    325. if (this.priority == rec.getPriority()) {
    326. return (Long.valueOf(this.id)).compareTo(rec.getId());
    327. } else {
    328. return (Integer.valueOf(this.priority)).compareTo(rec.getPriority());
    329. }
    330. } else {
    331. return 0;
    332. }
    333. }
    334. }
    335. }
    336.  

    Feel free to use the above code!:)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 3, 2016
  16. Offline

    desht

    I see you've managed to cook up a version using direct NMS calls - that's cool, but personally I'll stick with using ProtocolLib - I like the version independence :)

    Just realised I'd never looked at this potential problem, sorry. I'll try to remember to review that...

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 3, 2016
  17. Offline

    vasil7112

    Sure thing! Just trying to help:)

    Goodnight and continue the nice work!:)
     
  18. Offline

    Waffletastic

    Been playing around with this a bit. Can't seem to think of a way that I could have an itemmessage stay consistently displayed while a player is shooting snowballs? Every time a snowball fires the text updates and looks weird. I run a minigame server involving a lot of snowballs firing so this is why I care. I know I could increase the update interval but even at 1 tick it's noticable.
    Thanks
     
  19. Offline

    Templar3lf

    desht, sorry if this is almost reviving an old thread, although from the looks of things the last message was only a few months ago.
    I was wondering how you edited the map object to display a menu, and if it where possible to do the same to show a modified map, possibly one with regions colour coded into it. I can't seem to find anything else about how to do so with a few google searches, although I could be searching entirely the wrong thing.
    I am rather new at using Bukkit, and just as new to Java. If it's a super complicated thing don't feel too obligated to explain the workings of it. :p
     
  20. Offline

    ChipDev

    Me no think that.
    Pseudo:
    on slot change :
    get slot and cache to a int "Number".
    cancel event
    set slot to "Number"
    show message " " (Space)
    I think that is possible
     
  21. Offline

    Onlineids

  22. Offline

    MineStein

    This is awesome! I hate the clutter sending them messages in the chat! Thanks desht
     
Thread Status:
Not open for further replies.

Share This Page