Skip to content

Commit 85997c0

Browse files
devnatanCopilot
andauthored
Dynamic inventory title update Kyori's Adventure component support (#817)
Signed-off-by: Natan Vieira <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 8bbdd72 commit 85997c0

File tree

17 files changed

+180
-25
lines changed

17 files changed

+180
-25
lines changed

inventory-framework-api/src/main/java/me/devnatan/inventoryframework/ViewContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public interface ViewContainer {
8181

8282
void close(@NotNull Viewer viewer);
8383

84-
void changeTitle(@Nullable String title, @NotNull Viewer target);
84+
void changeTitle(@Nullable Object title, @NotNull Viewer target);
8585

8686
boolean isEntityContainer();
8787

inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFConfinedContext.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,33 @@ public interface IFConfinedContext extends IFContext {
5656
* library developers to add support to your version.
5757
*
5858
* @param title The new container title.
59+
* @see <a href="https://github.com/DevNatan/inventory-framework/wiki/navigating-between-views">Navigating between Views on Wiki</a>
5960
*/
6061
void updateTitleForPlayer(@NotNull String title);
6162

63+
/**
64+
* Updates the container title only for the player in the current scope of execution using a component or non-String object.
65+
*
66+
* <p>This should not be used before the container is opened. If you need to set the __initial
67+
* title__, use {@link IFOpenContext#modifyConfig()} on open handler instead.
68+
*
69+
* <p>This method is version dependent, so it may be that your server version is not yet
70+
* supported. If you try to use this method and it fails (can fail silently), report it to the
71+
* library developers to add support for your version.
72+
* <p>
73+
*
74+
* <a href="https://github.com/KyoriPowered/adventure">Kyori's Adventure Text Component</a> is supported if your platform is PaperSpigot
75+
* in a non-legacy version. Non-{@link String} titles will be converted to plain text.
76+
* <p>
77+
* <b><i> This API is experimental and is not subject to the general compatibility guarantees;
78+
* such API may be changed or may be removed completely in any further release. </i></b>
79+
*
80+
* @param titleComponent The new container title as a component or object.
81+
* @see <a href="https://github.com/DevNatan/inventory-framework/wiki/dynamic-title-update">Dynamic Title Update on Wiki</a>
82+
*/
83+
@ApiStatus.Experimental
84+
void updateTitleForPlayer(@NotNull Object titleComponent);
85+
6286
/**
6387
* Resets the container title only for the player current scope of execution to the initially
6488
* defined title. Must be used after {@link #updateTitleForPlayer(String)} to take effect.

inventory-framework-api/src/main/java/me/devnatan/inventoryframework/context/IFContext.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,27 @@ void performClickInComponent(
212212
*/
213213
void updateTitleForEveryone(@NotNull String title);
214214

215+
/**
216+
* Experimental: Updates the container title for everyone that's viewing it, supporting additional title types.
217+
*
218+
* <p>This should not be used before the container is opened. If you need to set the __initial
219+
* title__, use {@link IFOpenContext#modifyConfig()} on open handler instead.
220+
*
221+
* <p>This method is version dependant, so it may be that your server version is not yet
222+
* supported. If you try to use this method and it fails (can fail silently), report it to the
223+
* library developers to add support for your version.
224+
* <p>
225+
* <a href="https://github.com/KyoriPowered/adventure">Kyori's Adventure Text Component</a> is supported if your platform is PaperSpigot
226+
* in a non-legacy version. Non-{@link String} titles will be converted to plain text.
227+
* <p>
228+
* <b><i> This API is experimental and is not subject to the general compatibility guarantees.
229+
* Such API may be changed or may be removed completely in any further release. </i></b>
230+
*
231+
* @param title The new container title (may be a String or a supported component type).
232+
*/
233+
@ApiStatus.Experimental
234+
void updateTitleForEveryone(@NotNull Object title);
235+
215236
/**
216237
* Updates the container title to all viewers in this context, to the initially defined title.
217238
* Must be used after {@link #updateTitleForEveryone(String)} to take effect.

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/BukkitViewContainer.java

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

33
import java.util.ArrayList;
44
import java.util.Objects;
5-
import me.devnatan.inventoryframework.runtime.thirdparty.InventoryUpdate;
5+
import me.devnatan.inventoryframework.internal.InventoryFactory;
66
import org.bukkit.entity.HumanEntity;
77
import org.bukkit.entity.Player;
88
import org.bukkit.inventory.Inventory;
@@ -147,12 +147,12 @@ public int getLastSlot() {
147147
}
148148

149149
@Override
150-
public void changeTitle(@Nullable String title, @NotNull Viewer target) {
150+
public void changeTitle(@Nullable Object title, @NotNull Viewer target) {
151151
changeTitle(title, ((BukkitViewer) target).getPlayer());
152152
}
153153

154-
public void changeTitle(@Nullable String title, @NotNull Player target) {
155-
InventoryUpdate.updateInventory(target, title);
154+
public void changeTitle(@Nullable Object title, @NotNull Player target) {
155+
InventoryFactory.current().setInventoryTitle(target, title);
156156
}
157157

158158
@Override

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/SlotClickContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ public final void updateTitleForPlayer(@NotNull String title) {
162162
getParent().updateTitleForPlayer(title);
163163
}
164164

165+
@Override
166+
public final void updateTitleForPlayer(@NotNull Object title) {
167+
getParent().updateTitleForPlayer(title);
168+
}
169+
165170
@Override
166171
public final void resetTitleForPlayer() {
167172
getParent().resetTitleForPlayer();

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/context/SlotRenderContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ public final void updateTitleForPlayer(@NotNull String title) {
119119
getParent().updateTitleForPlayer(title);
120120
}
121121

122+
@Override
123+
public final void updateTitleForPlayer(@NotNull Object title) {
124+
getParent().updateTitleForPlayer(title);
125+
}
126+
122127
@Override
123128
public final void resetTitleForPlayer() {
124129
getParent().resetTitleForPlayer();

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/internal/BukkitInventoryFactory.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import static me.devnatan.inventoryframework.runtime.util.InventoryUtils.toInventoryType;
55

66
import me.devnatan.inventoryframework.ViewType;
7+
import me.devnatan.inventoryframework.runtime.thirdparty.InventoryUpdate;
78
import org.bukkit.Bukkit;
9+
import org.bukkit.entity.Player;
810
import org.bukkit.inventory.Inventory;
911
import org.bukkit.inventory.InventoryHolder;
1012

@@ -30,4 +32,13 @@ public Inventory createInventory(InventoryHolder holder, ViewType type, int size
3032
}
3133
return inventory;
3234
}
35+
36+
@Override
37+
public void setInventoryTitle(Player player, Object newTitle) {
38+
if (!(newTitle instanceof String))
39+
throw new UnsupportedOperationException(
40+
"Import inventory-framework-platform-paper to use Component as inventory title.");
41+
42+
InventoryUpdate.updateInventory(player, newTitle);
43+
}
3344
}

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/internal/InventoryFactory.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
import me.devnatan.inventoryframework.IFDebug;
44
import me.devnatan.inventoryframework.ViewType;
5+
import org.bukkit.entity.Player;
56
import org.bukkit.inventory.Inventory;
67
import org.bukkit.inventory.InventoryHolder;
8+
import org.jetbrains.annotations.ApiStatus;
79

8-
abstract class InventoryFactory {
10+
/**
11+
* <b><i> This is an internal inventory-framework API that should not be used from outside of
12+
* this library. No compatibility guarantees are provided. </i></b>
13+
*/
14+
@ApiStatus.Internal
15+
public abstract class InventoryFactory {
916

1017
protected static InventoryFactory instance;
1118

@@ -35,4 +42,6 @@ private static InventoryFactory forCurrentPlatform() {
3542
}
3643

3744
public abstract Inventory createInventory(InventoryHolder holder, ViewType type, int size, Object title);
45+
46+
public abstract void setInventoryTitle(Player player, Object newTitle);
3847
}

inventory-framework-platform-bukkit/src/main/java/me/devnatan/inventoryframework/runtime/thirdparty/InventoryUpdate.java

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ public final class InventoryUpdate {
4848
public static final Class<?> CONTAINER;
4949
private static final Class<?> CONTAINERS;
5050
private static final Class<?> I_CHAT_MUTABLE_COMPONENT;
51+
private static final Class<?> MINECRAFT_MENU_TYPE;
5152

5253
// Methods
5354
public static final MethodHandle getBukkitView;
5455
private static final MethodHandle literal;
56+
private static final MethodHandle asVanilla;
5557

5658
// Constructors
5759
private static final MethodHandle chatMessage;
@@ -66,7 +68,7 @@ public final class InventoryUpdate {
6668

6769
private static final Set<String> UNOPENABLES = Sets.newHashSet("CRAFTING", "CREATIVE", "PLAYER");
6870
private static final boolean SUPPORTS_19 = McVersion.supports(19);
69-
private static final Object[] DUMMY_COLOR_MODIFIERS = new Object[0];
71+
public static final Object[] DUMMY_COLOR_MODIFIERS = new Object[0];
7072

7173
static {
7274
// Initialize classes.
@@ -78,6 +80,7 @@ public final class InventoryUpdate {
7880
CONTAINER = ReflectionUtils.getNMSClass("world.inventory", "Container");
7981
I_CHAT_MUTABLE_COMPONENT =
8082
SUPPORTS_19 ? ReflectionUtils.getNMSClass("network.chat", "IChatMutableComponent") : null;
83+
MINECRAFT_MENU_TYPE = getClassOrNull("net.minecraft.world.inventory.MenuType");
8184

8285
// Initialize methods.
8386
getBukkitView = getMethod(CONTAINER, "getBukkitView", MethodType.methodType(InventoryView.class));
@@ -86,6 +89,17 @@ public final class InventoryUpdate {
8689
I_CHAT_BASE_COMPONENT, "b", MethodType.methodType(I_CHAT_MUTABLE_COMPONENT, String.class), true)
8790
: null;
8891

92+
final Class<?> paperAdventure = getClassOrNull("io.papermc.paper.adventure.PaperAdventure");
93+
asVanilla = paperAdventure == null
94+
? null
95+
: getMethod(
96+
paperAdventure,
97+
"asVanilla",
98+
MethodType.methodType(
99+
getClassOrNull("net.minecraft.network.chat.Component"),
100+
getClassOrNull("net.kyori.adventure.text.Component")),
101+
true);
102+
89103
// Initialize constructors.
90104
chatMessage = SUPPORTS_19 ? null : getConstructor(CHAT_MESSAGE, String.class, Object[].class);
91105
packetPlayOutOpenWindow = (useContainers())
@@ -100,37 +114,42 @@ public final class InventoryUpdate {
100114
windowId = getField(CONTAINER, int.class, "windowId", "j", "containerId");
101115
}
102116

117+
private static Class<?> getClassOrNull(String className) {
118+
try {
119+
return Class.forName(className);
120+
} catch (final ClassNotFoundException ignored) {
121+
}
122+
return null;
123+
}
124+
103125
/**
104126
* Update the player inventory, so you can change the title.
105127
*
106128
* @param player whose inventory will be updated.
107129
* @param newTitle the new title for the inventory.
108130
*/
109131
@SuppressWarnings("UnstableApiUsage")
110-
public static void updateInventory(Player player, String newTitle) {
132+
public static void updateInventory(Player player, Object newTitle) {
111133
Preconditions.checkArgument(player != null, "Cannot update inventory to null player.");
112134

113135
if (newTitle == null) newTitle = "";
114136

115137
try {
116-
if (newTitle.length() > 32) {
117-
newTitle = newTitle.substring(0, 32);
138+
if (newTitle instanceof String && ((String) newTitle).length() > 32) {
139+
newTitle = ((String) newTitle).substring(0, 32);
118140
}
119141

120-
if (McVersion.supports(20)) {
142+
if (newTitle instanceof String && McVersion.supports(20)) {
121143
InventoryView open = player.getOpenInventory();
122144
if (UNOPENABLES.contains(open.getType().name())) return;
123-
open.setTitle(newTitle);
145+
open.setTitle((String) newTitle);
124146
return;
125147
}
126148

127149
// Get EntityPlayer from CraftPlayer.
128150
Object craftPlayer = CRAFT_PLAYER.cast(player);
129151
Object entityPlayer = GET_HANDLE.invoke(craftPlayer);
130152

131-
// Create new title.
132-
Object title = createTitleComponent(newTitle);
133-
134153
// Get activeContainer from EntityPlayer.
135154
Object activeContainer = getActiveContainer(entityPlayer);
136155

@@ -162,12 +181,27 @@ public static void updateInventory(Player player, String newTitle) {
162181
return;
163182
}
164183

165-
Object object = getContainerOrName(container, type);
184+
Object packet;
185+
if (!(newTitle instanceof String)) {
186+
final Object minecraftComponent = asVanilla.invoke(newTitle);
166187

167-
// Create packet.
168-
Object packet = useContainers()
169-
? packetPlayOutOpenWindow.invoke(windowId, object, title)
170-
: packetPlayOutOpenWindow.invoke(windowId, object, title, size);
188+
// Bukkit uses uppercase "X", Minecraft uses lowercase "x"
189+
// Bukkit: GENERIC_9X3 / Minecraft: GENERIC_9x3
190+
final String minecraftEnumName = container.name().replace("X", "x");
191+
192+
final Object menuType =
193+
MINECRAFT_MENU_TYPE.getField(minecraftEnumName).get(null);
194+
195+
packet = packetPlayOutOpenWindow.invoke(windowId, menuType, minecraftComponent);
196+
} else {
197+
// Create new title.
198+
Object title = createTitleComponent(newTitle);
199+
Object object = getContainerOrName(container, type);
200+
201+
packet = useContainers()
202+
? packetPlayOutOpenWindow.invoke(windowId, object, title)
203+
: packetPlayOutOpenWindow.invoke(windowId, object, title, size);
204+
}
171205

172206
// Send packet sync.
173207
ReflectionUtils.sendPacketSync(player, packet);
@@ -392,8 +426,10 @@ public Object getObject() {
392426
String name = (version == 14 && this == CARTOGRAPHY_TABLE) ? "CARTOGRAPHY" : name();
393427
// Since 1.17, containers go from "a" to "x".
394428
if (version > 16 && version <= 20) name = String.valueOf(alphabet[ordinal()]);
429+
395430
Field field = CONTAINERS.getField(name);
396431
return field.get(null);
432+
397433
} catch (ReflectiveOperationException exception) {
398434
exception.printStackTrace();
399435
}

inventory-framework-platform-minestom/src/main/kotlin/me/devnatan/inventoryframework/MinestomViewContainer.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,15 @@ class MinestomViewContainer(
105105
}
106106

107107
override fun changeTitle(
108-
title: String?,
108+
title: Any?,
109109
target: Viewer,
110110
) {
111-
changeTitle(
112-
title?.let { Component.text(it) } ?: Component.empty(),
113-
(target as MinestomViewer).player,
114-
)
111+
val newTitle =
112+
title as? Component
113+
?: title?.let { raw -> Component.text(raw as String) }
114+
?: Component.empty()
115+
116+
changeTitle(newTitle, (target as MinestomViewer).player)
115117
}
116118

117119
fun changeTitle(

0 commit comments

Comments
 (0)