Skip to content

Commit 1c6e3a3

Browse files
committed
XSkull improvements + Titles minor changes
1 parent 18bed72 commit 1c6e3a3

File tree

10 files changed

+110
-27
lines changed

10 files changed

+110
-27
lines changed

core/src/main/java/com/cryptomorin/xseries/XItemStack.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,12 @@ private void customModelData() {
927927
}
928928
}
929929
}
930+
931+
// Setting an empty component will save it and change the internal meta which affects isSimilar()
932+
if (!customModelData.getColors().isEmpty() || !customModelData.getStrings().isEmpty() ||
933+
!customModelData.getFlags().isEmpty() || !customModelData.getFloats().isEmpty()) {
934+
meta.setCustomModelDataComponent(customModelData);
935+
}
930936
} else if (SUPPORTS_CUSTOM_MODEL_DATA) {
931937
String modelData = config.getString("custom-model-data");
932938
if (modelData != null && !modelData.isEmpty()) {

core/src/main/java/com/cryptomorin/xseries/messages/Titles.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import com.cryptomorin.xseries.reflection.XReflection;
2525
import com.cryptomorin.xseries.reflection.minecraft.MinecraftClassHandle;
26-
import com.cryptomorin.xseries.reflection.minecraft.MinecraftConnection;
2726
import com.cryptomorin.xseries.reflection.minecraft.MinecraftPackage;
2827
import net.md_5.bungee.api.chat.BaseComponent;
2928
import org.bukkit.configuration.ConfigurationSection;
@@ -54,7 +53,7 @@
5453
* PacketPlayOutTitle: https://minecraft.wiki/w/Protocol#Title
5554
*
5655
* @author Crypto Morin
57-
* @version 4.0.0
56+
* @version 4.0.1
5857
* @see XReflection
5958
* @see ActionBar
6059
*/
@@ -240,11 +239,11 @@ public static void sendTitle(@NotNull Player player,
240239
if (subtitle != null) {
241240
packets.add(ClientboundSetSubtitleTextPacket.invoke(MessageComponents.bungeeToVanilla(subtitle.asComponent())));
242241
}
243-
} catch (Throwable e) {
244-
throw new RuntimeException(e);
242+
} catch (Throwable ex) {
243+
throw new IllegalStateException("Failed to create packets with title: " + title + " and subtitle: " + subtitle, ex);
245244
}
246245

247-
MinecraftConnection.sendPacket(player, packets.toArray(new Object[0]));
246+
sendPacket(player, packets.toArray(new Object[0]));
248247
return;
249248
}
250249

@@ -254,19 +253,23 @@ public static void sendTitle(@NotNull Player player,
254253
}
255254

256255
try {
257-
Object timesPacket = PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_TIMES, CHAT_COMPONENT_TEXT.invoke(title), fadeIn, stay, fadeOut);
258-
sendPacket(player, timesPacket);
256+
// There are also constructors with only the fade/stay/fadeout or just the text components,
257+
// but we will use the full constructor for all of them just to be sure.
258+
List<Object> packets = new ArrayList<>(3);
259+
Object titleComponent = CHAT_COMPONENT_TEXT.invoke(title);
260+
261+
packets.add(PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_TIMES, titleComponent, fadeIn, stay, fadeOut));
259262

260263
if (title != null) {
261-
Object titlePacket = PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_TITLE, CHAT_COMPONENT_TEXT.invoke(title), fadeIn, stay, fadeOut);
262-
sendPacket(player, titlePacket);
264+
packets.add(PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_TITLE, titleComponent, fadeIn, stay, fadeOut));
263265
}
264266
if (subtitle != null) {
265-
Object subtitlePacket = PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_SUBTITLE, CHAT_COMPONENT_TEXT.invoke(subtitle), fadeIn, stay, fadeOut);
266-
sendPacket(player, subtitlePacket);
267+
packets.add(PACKET_PLAY_OUT_TITLE.invoke(TITLE_ACTION_SUBTITLE, CHAT_COMPONENT_TEXT.invoke(subtitle), fadeIn, stay, fadeOut));
267268
}
268-
} catch (Throwable throwable) {
269-
throwable.printStackTrace();
269+
270+
sendPacket(player, packets.toArray(new Object[0]));
271+
} catch (Throwable ex) {
272+
throw new IllegalStateException("Failed to send packets for title: " + title + " and subtitle: " + subtitle, ex);
270273
}
271274
}
272275

core/src/main/java/com/cryptomorin/xseries/profiles/builder/ProfileInstruction.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.cryptomorin.xseries.profiles.objects.DelegateProfileable;
3232
import com.cryptomorin.xseries.profiles.objects.ProfileContainer;
3333
import com.cryptomorin.xseries.profiles.objects.Profileable;
34+
import com.google.common.base.Function;
3435
import com.mojang.authlib.GameProfile;
3536
import org.bukkit.block.BlockState;
3637
import org.bukkit.inventory.ItemStack;
@@ -40,17 +41,16 @@
4041
import org.jetbrains.annotations.NotNull;
4142
import org.jetbrains.annotations.Nullable;
4243

43-
import java.util.ArrayList;
44-
import java.util.Arrays;
45-
import java.util.List;
46-
import java.util.Objects;
44+
import java.util.*;
4745
import java.util.concurrent.CompletableFuture;
4846
import java.util.function.Consumer;
4947

5048
/**
5149
* Represents an instruction that sets a property of a {@link GameProfile}.
5250
* It uses a {@link #profileContainer} to define how to set the property and
5351
* a {@link #profileable} to define what to set in the property.
52+
* <p>
53+
* This class must be created from one of {@link XSkull} methods.
5454
*
5555
* @param <T> The type of the result produced by the {@link #profileContainer} function.
5656
*/
@@ -87,9 +87,9 @@ public T removeProfile() {
8787
return profileContainer.getObject();
8888
}
8989

90-
@ApiStatus.Experimental
9190
@NotNull
9291
@Contract(value = "_ -> this", mutates = "this")
92+
@ApiStatus.Experimental
9393
public ProfileInstruction<T> profileRequestConfiguration(ProfileRequestConfiguration config) {
9494
this.profileRequestConfiguration = config;
9595
return this;
@@ -111,13 +111,15 @@ public ProfileInstruction<T> lenient() {
111111
*/
112112
@Override
113113
@Nullable
114+
@ApiStatus.Internal
114115
public GameProfile getProfile() {
115116
// Just here to handle the JavaDocs.
116117
return profileContainer.getProfile();
117118
}
118119

119120
@Override
120121
@Contract(pure = true)
122+
@ApiStatus.Internal
121123
public Profileable getDelegateProfile() {
122124
return profileContainer;
123125
}
@@ -151,6 +153,8 @@ public ProfileInstruction<T> fallback(@NotNull Profileable... fallbacks) {
151153
/**
152154
* Called when any of the {@link #fallback(Profileable...)} profiles are used,
153155
* this is also called if no fallback profile is provided, but the main one {@link #profile(Profileable)} fails.
156+
* <p>
157+
* Make sure that {@link #lenient()} is not, otherwise this will never be used.
154158
*
155159
* @see #onFallback(Runnable)
156160
*/
@@ -187,6 +191,7 @@ public ProfileInstruction<T> onFallback(@NotNull Runnable onFallback) {
187191
* @throws ProfileChangeException If any type of {@link ProfileException} occurs, they will be accumulated
188192
* in form of suppressed exceptions ({@link Exception#getSuppressed()}) in this single exception
189193
* starting from the main profile, followed by the fallback profiles.
194+
* @see #applyAsync()
190195
*/
191196
@NotNull
192197
public T apply() {
@@ -270,6 +275,9 @@ public T apply() {
270275
* }</pre>
271276
*
272277
* @return A {@link CompletableFuture} that will complete asynchronously.
278+
* @see #apply()
279+
* @see Profileable#prepare()
280+
* @see Profileable#prepare(Collection, ProfileRequestConfiguration, Function)
273281
*/
274282
@NotNull
275283
public CompletableFuture<T> applyAsync() {

core/src/main/java/com/cryptomorin/xseries/profiles/builder/XSkull.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@
7373
* I don't know if this cache system works across other servers or is just specific to one server.
7474
*
7575
* @author Crypto Morin, Erick Alexander
76-
* @version 11.2.1
77-
* @see XMaterial
76+
* @version 12.0.0
77+
* @see Profileable
7878
* @see XReflection
7979
*/
8080
public final class XSkull {
@@ -140,7 +140,7 @@ public static ProfileInstruction<Skull> of(@NotNull BlockState state) {
140140

141141

142142
/**
143-
* We'll just return an x shaped hardcoded skull.<br>
143+
* We'll just return a prohibition sign hardcoded skull.<br>
144144
* <a href="https://minecraft-heads.com/custom-heads/miscellaneous/58141-cross">minecraft-heads.com</a>
145145
*/
146146
private static final GameProfile DEFAULT_PROFILE = PlayerProfiles.signXSeries(ProfileInputType.BASE64.getProfile(
@@ -150,15 +150,17 @@ public static ProfileInstruction<Skull> of(@NotNull BlockState state) {
150150
));
151151

152152
/**
153-
* Retrieves the default {@link GameProfile} used by XSkull.
153+
* The default {@link GameProfile} used by {@link ProfileInstruction} as a last resort
154+
* when none of the fallback values could be retrieved. This profile represents
155+
* <a href="http://textures.minecraft.net/texture/c10591e6909e6a281b371836e462d67a2c78fa0952e910f32b41a26c48c1757c">
156+
* a red prohibition sign on a silver head.</a>
154157
* This method creates a clone of the default profile to prevent modifications to the original.
155158
*
156-
* @return A clone of the default {@link GameProfile}.
159+
* @return A clone of the default {@link Profileable}.
157160
*/
158161
@NotNull
159162
@Contract(value = "-> new", pure = true)
160163
protected static Profileable getDefaultProfile() {
161-
// We copy this just in case something changes the GameProfile properties.
162164
return Profileable.of(PlayerProfiles.clone(DEFAULT_PROFILE), false);
163165
}
164166
}

core/src/main/java/com/cryptomorin/xseries/profiles/mojang/PlayerProfileFetcherThread.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
@ApiStatus.Internal
3535
public final class PlayerProfileFetcherThread implements ThreadFactory {
3636
/**
37-
* An executor service with a fixed thread pool of size 2, used for asynchronous operations.
37+
* An executor service with a fixed thread pool of size 10, used for asynchronous operations.
3838
*/
39-
public static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, new PlayerProfileFetcherThread());
39+
public static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(10, new PlayerProfileFetcherThread());
4040

4141
private static final AtomicInteger COUNT = new AtomicInteger();
4242

core/src/main/java/com/cryptomorin/xseries/profiles/objects/Profileable.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,16 @@
5858
* Represents any object that has a {@link GameProfile} or one can be created with it.
5959
* These objects are cached.
6060
* <p>
61+
* Usually this class is intended to be used with {@link com.cryptomorin.xseries.profiles.builder.XSkull XSkull}
62+
* by defining the profile to set for {@link ProfileInstruction#profile(Profileable)}, however it can be used by itself for more advanced
63+
* systems if you know what you're doing.
64+
* <p>
6165
* A {@link GameProfile} is an object that represents information about a Minecraft player's
6266
* account in general (not specific to this or any other server)
6367
* The most important information contained within this profile however, is the
6468
* skin texture URL which the client needs to properly see the texture on items/blocks.
69+
* The server itself doesn't process the texture from the URL, it gives it to the client for
70+
* it to download and process the texture instead.
6571
*
6672
* @see com.cryptomorin.xseries.profiles.objects.cache.CacheableProfileable
6773
* @see TimedCacheableProfileable
@@ -93,6 +99,7 @@ public interface Profileable {
9399
* and doesn't need to a request any data using {@link com.cryptomorin.xseries.profiles.mojang.MinecraftClient MinecraftClient}.
94100
*
95101
* @see #prepare(Collection)
102+
* @since 12.0.0
96103
*/
97104
@Contract(pure = true)
98105
boolean isReady();
@@ -169,10 +176,30 @@ default String getProfileValue() {
169176
return PlayerProfiles.getOriginalValue(getProfile());
170177
}
171178

179+
/**
180+
* A single version of {@link #prepare(Collection, ProfileRequestConfiguration, Function)}.
181+
* This simply performs profile lookup asynchrously and returns after it's done, or it
182+
* will instantly return if {@link #isReady()} is true.
183+
*
184+
* @return A future containing the same object.
185+
* @since 12.0.0
186+
*/
187+
@NotNull
188+
@Contract("-> new")
189+
@ApiStatus.Experimental
190+
default CompletableFuture<Profileable> prepare() {
191+
if (isReady()) return CompletableFuture.completedFuture(this);
192+
193+
return XReflection.stacktrace(CompletableFuture
194+
.supplyAsync(this::getProfile, PlayerProfileFetcherThread.EXECUTOR)
195+
.thenApply(x -> this));
196+
}
197+
172198
/**
173199
* @see #prepare(Collection, ProfileRequestConfiguration, Function)
174200
*/
175201
@NotNull
202+
@Contract("_ -> new")
176203
@ApiStatus.Experimental
177204
static <C extends Collection<Profileable>> CompletableFuture<C> prepare(@NotNull C profileables) {
178205
return prepare(profileables, null, null);
@@ -188,8 +215,10 @@ static <C extends Collection<Profileable>> CompletableFuture<C> prepare(@NotNull
188215
* otherwise that specific profile will be ignored.
189216
* @param <C> the type of the collection used for the profiles.
190217
* @return the same collection unmodified. (The profiles are cached inside the {@link Profileable} itself, so you can use them directly now)
218+
* @see #prepare()
191219
*/
192220
@NotNull
221+
@Contract("_, _, _ -> new")
193222
@ApiStatus.Experimental
194223
static <C extends Collection<Profileable>> CompletableFuture<C> prepare(
195224
@NotNull C profileables, @Nullable ProfileRequestConfiguration config,

core/src/test/com/cryptomorin/xseries/test/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static Path getTestPath() {
4747
}
4848

4949
/**
50-
* This sends unnecessary requests to Mojang and also delays out work too,
50+
* This sends unnecessary requests to Mojang and also delays our work too,
5151
* so let's not test when it's not needed.
5252
*/
5353
public static final boolean TEST_MOJANG_API = false;

core/src/test/com/cryptomorin/xseries/test/XSeriesTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ private static void testSkulls() {
548548
.lenient().apply();
549549

550550
// Currently broken. Seems like Mojang disabled this API? Read MojangAPI.usernamesToUUIDs for more info.
551+
// [5/31/2025] It's working again!
551552
if (Constants.TEST_MOJANG_API_BULK) {
552553
log("Testing bulk username to UUID conversion");
553554
Map<UUID, String> mapped = MojangAPI.usernamesToUUIDs(Arrays.asList("yourmom1212",

core/src/test/com/cryptomorin/xseries/test/XSkullRequestQueueTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
import com.cryptomorin.xseries.profiles.lock.KeyedLock;
2626
import com.cryptomorin.xseries.profiles.lock.KeyedLockMap;
2727
import com.cryptomorin.xseries.profiles.lock.MojangRequestQueue;
28+
import com.cryptomorin.xseries.profiles.objects.Profileable;
2829
import com.cryptomorin.xseries.reflection.XReflection;
2930
import com.cryptomorin.xseries.test.util.XLogger;
3031
import org.junit.jupiter.api.Assertions;
3132

3233
import java.lang.invoke.MethodHandle;
3334
import java.util.*;
35+
import java.util.concurrent.CompletableFuture;
3436
import java.util.concurrent.atomic.AtomicInteger;
3537

3638
final class XSkullRequestQueueTest {
@@ -80,6 +82,24 @@ public static void test() {
8082
int times = cache.getValue().get();
8183
Assertions.assertEquals(1, times, () -> "Cached more than once: " + cache.getKey() + " -> " + times);
8284
}
85+
86+
realTest();
87+
}
88+
89+
private static void realTest() {
90+
// This can vary specially for the second request since
91+
// they have to wait for the first one to finish.
92+
// The best way to test this would be to somehow limit your
93+
// internet speed significantly.
94+
// Whatever the duration is, it should progressively get smaller and smaller.
95+
// 800ms -> 630ms -> 600ms -> 5ms -> 0ms
96+
List<CompletableFuture<Void>> profiles = new ArrayList<>(5);
97+
98+
for (int i = 1; i <= 5; i++) {
99+
profiles.add(XLogger.logTimingsAsync("Notch Async Lookup " + i, () -> Profileable.username("Hex_26").getProfile()));
100+
}
101+
102+
CompletableFuture.allOf(profiles.toArray(new CompletableFuture[0])).join();
83103
}
84104

85105
private static String blackhole(int i) {

core/src/test/com/cryptomorin/xseries/test/util/XLogger.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,23 @@
2222

2323
package com.cryptomorin.xseries.test.util;
2424

25+
import java.util.concurrent.CompletableFuture;
26+
2527
public final class XLogger {
2628
public static void log(String message) {
2729
// We should probably use an actual logger?
2830
System.out.println("[XSeries] " + message);
2931
}
32+
33+
public static CompletableFuture<Void> logTimingsAsync(String prefix, Runnable runnable) {
34+
return CompletableFuture.runAsync(() -> logTimings(prefix, runnable));
35+
}
36+
37+
public static void logTimings(String prefix, Runnable runnable) {
38+
long before = System.currentTimeMillis();
39+
runnable.run();
40+
long after = System.currentTimeMillis();
41+
long diff = after - before;
42+
log('[' + prefix + "] Took " + diff + "ms");
43+
}
3044
}

0 commit comments

Comments
 (0)