Skip to content

Commit

Permalink
Docs + XSkull & XItemStack Fixes
Browse files Browse the repository at this point in the history
* Fixed a few issues with `XSkull`
* Added more documentations to `XReflection` with a few renames.
* Changed the default material for `XItemStack` (Fixes #275)
  • Loading branch information
CryptoMorin committed Jun 23, 2024
1 parent 01fb76f commit 219f510
Show file tree
Hide file tree
Showing 42 changed files with 683 additions and 533 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ repositories {
mavenCentral()
}
dependencies {
implementation("com.github.cryptomorin:XSeries:version") { isTransitive = false }
implementation("com.github.cryptomorin:XSeries:version")
}
```

Expand Down
24 changes: 16 additions & 8 deletions src/main/java/com/cryptomorin/xseries/XItemStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
* <a href="https://hub.spigotmc.org/javadocs/spigot/org/bukkit/inventory/ItemStack.html">ItemStack</a>
*
* @author Crypto Morin
* @version 7.5.0
* @version 7.5.1
* @see XMaterial
* @see XPotion
* @see XSkull
Expand All @@ -86,11 +86,12 @@
*/
public final class XItemStack {
public static final ItemFlag[] ITEM_FLAGS = ItemFlag.values();
public static final boolean SUPPORTS_CUSTOM_MODEL_DATA;

/**
* Because item metas cannot be applied to AIR, apparently.
* Because {@link ItemMeta} cannot be applied to {@link Material#AIR}.
*/
private static final XMaterial DEFAULT_MATERIAL = XMaterial.NETHER_PORTAL;
private static final XMaterial DEFAULT_MATERIAL = XMaterial.BARRIER;
private static final boolean SUPPORTS_POTION_COLOR;

static {
Expand All @@ -104,11 +105,18 @@ public final class XItemStack {
SUPPORTS_POTION_COLOR = supportsPotionColor;
}

private XItemStack() {
static {
boolean supportsCustomModelData = false;
try {
ItemMeta.class.getMethod("hasCustomModelData");
supportsCustomModelData = true;
} catch (Throwable ignored) {
}

SUPPORTS_CUSTOM_MODEL_DATA = supportsCustomModelData;
}

public static boolean isDefaultItem(ItemStack item) {
return DEFAULT_MATERIAL.isSimilar(item);
private XItemStack() {
}

private static BlockState safeBlockState(BlockStateMeta meta) {
Expand Down Expand Up @@ -452,7 +460,7 @@ public static Map<String, Object> serialize(@Nonnull ItemStack item) {
*/
@Nonnull
public static ItemStack deserialize(@Nonnull ConfigurationSection config) {
return edit(new ItemStack(DEFAULT_MATERIAL.parseMaterial()), config, Function.identity(), null);
return edit(DEFAULT_MATERIAL.parseItem(), config, Function.identity(), null);
}

/**
Expand Down Expand Up @@ -485,7 +493,7 @@ public static ItemStack deserialize(@Nonnull ConfigurationSection config,
public static ItemStack deserialize(@Nonnull ConfigurationSection config,
@Nonnull Function<String, String> translator,
@Nullable Consumer<Exception> restart) {
return edit(new ItemStack(DEFAULT_MATERIAL.parseMaterial()), config, translator, restart);
return edit(DEFAULT_MATERIAL.parseItem(), config, translator, restart);
}


Expand Down
59 changes: 27 additions & 32 deletions src/main/java/com/cryptomorin/xseries/profiles/PlayerProfiles.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.cryptomorin.xseries.profiles;

import com.google.common.collect.Iterables;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.ApiStatus;

import javax.annotation.Nonnull;
Expand All @@ -28,7 +27,7 @@ public final class PlayerProfiles {
private static final Property XSERIES_GAMEPROFILE_SIGNATURE = new Property(DEFAULT_PROFILE_NAME, "true");
public static final String TEXTURES_PROPERTY = "textures";

public static final GameProfile NIL = signXSeries(new GameProfile(PlayerUUIDs.IDENTITY_UUID, DEFAULT_PROFILE_NAME));
public static final GameProfile NIL = createGameProfile(PlayerUUIDs.IDENTITY_UUID, DEFAULT_PROFILE_NAME);


/**
Expand All @@ -51,6 +50,7 @@ public final class PlayerProfiles {
public static final String TEXTURES_BASE_URL = "http://textures.minecraft.net/texture/";

public static Optional<Property> getTextureProperty(GameProfile profile) {
// This is the property with Base64 encoded value.
return Optional.ofNullable(Iterables.getFirst(profile.getProperties().get(TEXTURES_PROPERTY), null));
}

Expand All @@ -69,17 +69,14 @@ public static String getSkinValue(@Nonnull GameProfile profile) {

/**
* Retrieves the value of a {@link Property}, handling differences between versions.
*
* @param property The {@link Property} from which to retrieve the value.
* @return The value of the {@link Property}.
* @since 4.0.1
*/
public static String getPropertyValue(Property property) {
if (ProfilesCore.NULLABILITY_RECORD_UPDATE) return property.value();
try {
return (String) ProfilesCore.PROPERTY_GET_VALUE.invoke(property);
return (String) ProfilesCore.Property_getValue.invoke(property);
} catch (Throwable throwable) {
throw new RuntimeException("Unable to get a texture value: " + property, throwable);
throw new RuntimeException("Unable to get a property value: " + property, throwable);
}
}

Expand All @@ -93,7 +90,6 @@ public static boolean hasTextures(GameProfile profile) {
return getTextureProperty(profile).isPresent();
}


/**
* Constructs a {@link GameProfile} using the provided texture hash and base64 string.
*
Expand All @@ -112,35 +108,25 @@ public static GameProfile profileFromHashAndBase64(String hash, String base64) {
}

/**
* Sanitizes the provided {@link GameProfile} by removing unnecessary timestamp data
* and caches the profile.
* Uses the online/offline UUID depending on {@link Bukkit#getOnlineMode()}.
* @return may return the same or a new profile.
*
* @param profile The {@link GameProfile} to be sanitized.
* @return The sanitized {@link GameProfile}.
* @param profile must have complete name and UUID
*/
@SuppressWarnings("deprecation")
public static GameProfile sanitizeProfile(GameProfile profile) {
JsonObject jsonObject = Optional.ofNullable(getSkinValue(profile)).map(PlayerProfiles::decodeBase64)
.map((decoded) -> new JsonParser().parse(decoded).getAsJsonObject())
.orElse(null);

if (jsonObject == null || !jsonObject.has("timestamp")) return profile;
JsonObject texture = new JsonObject();
texture.add(TEXTURES_PROPERTY, jsonObject.get(TEXTURES_PROPERTY));

This comment has been minimized.

Copy link
@Condordito

Condordito Jun 23, 2024

Contributor

The timestamp does affect the generated Base64, as it will be different after restarts. Removing it ensures that it stays constant without causing stacking issues

This comment has been minimized.

Copy link
@CryptoMorin

CryptoMorin Jun 23, 2024

Author Owner

Where are these stacking issues happening? YggdrasilMinecraftSessionService doesn't remove the timestamps either

// We could remove the unnecessary timestamp data, but let's keep it there, the texture is Base64 encoded anyway.
// It doesn't affect it in terms of performance.
// The timestamp property is the last time the values have been updated, this
// is instant in most cases, but are sometimes a few minutes? (or hours?) behind
// because of Mojang server's cache.

// The stored cache UUID must be according to online/offline servers.
UUID id;
if (PlayerUUIDs.isOnlineMode()) {
id = profile.getId();
} else {
id = PlayerUUIDs.getOfflineUUID(profile.getName());
PlayerUUIDs.ONLINE_TO_OFFLINE.put(profile.getId(), id);
}
if (PlayerUUIDs.isOnlineMode()) return profile;
UUID offlineId = PlayerUUIDs.getOfflineUUID(profile.getName());
PlayerUUIDs.ONLINE_TO_OFFLINE.put(profile.getId(), offlineId);

GameProfile clone = new GameProfile(id, profile.getName());
GameProfile clone = createGameProfile(offlineId, profile.getName());
clone.getProperties().putAll(profile.getProperties());
addTexturesProperty(clone, encodeBase64(texture.toString()));
signXSeries(clone);
return clone;
}

Expand Down Expand Up @@ -182,6 +168,15 @@ public static String decodeBase64(String base64) {
}
}

public static GameProfile createGameProfile(UUID uuid, String username) {
return signXSeries(new GameProfile(uuid, username));
}

/**
* All {@link GameProfile} created/modified by this library should have a special signature for debugging
* purposes, specially since we're directly messing with the server's internal cache
* it should be there in case something goes wrong.
*/
public static GameProfile signXSeries(GameProfile profile) {
// Just as an indicator that this is not a vanilla-created profile.
PropertyMap properties = profile.getProperties();
Expand All @@ -190,6 +185,6 @@ public static GameProfile signXSeries(GameProfile profile) {
}

public static GameProfile createNamelessGameProfile(UUID id) {
return signXSeries(new GameProfile(id, DEFAULT_PROFILE_NAME));
return createGameProfile(id, DEFAULT_PROFILE_NAME);
}
}
25 changes: 16 additions & 9 deletions src/main/java/com/cryptomorin/xseries/profiles/ProfilesCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.cryptomorin.xseries.reflection.XReflection;
import com.cryptomorin.xseries.reflection.jvm.FieldMemberHandle;
import com.cryptomorin.xseries.reflection.jvm.MethodMemberHandle;
import com.cryptomorin.xseries.reflection.jvm.ReflectiveNamespace;
import com.cryptomorin.xseries.reflection.ReflectiveNamespace;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftClassHandle;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftMapping;
import com.google.common.cache.LoadingCache;
Expand All @@ -16,7 +16,6 @@

import java.lang.invoke.MethodHandle;
import java.net.Proxy;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;

Expand All @@ -37,7 +36,7 @@ public final class ProfilesCore {
FILL_PROFILE_PROPERTIES, GET_PROFILE_BY_NAME, GET_PROFILE_BY_UUID, CACHE_PROFILE,
CRAFT_META_SKULL_PROFILE_GETTER, CRAFT_META_SKULL_PROFILE_SETTER,
CRAFT_SKULL_PROFILE_SETTER, CRAFT_SKULL_PROFILE_GETTER,
PROPERTY_GET_VALUE, UserCacheEntry_getProfile;
Property_getValue, UserCache_getNextOperation, UserCacheEntry_getProfile, UserCacheEntry_setLastAccess;

/**
* In v1.20.2, Mojang switched to {@code record} class types for their {@link Property} class.
Expand All @@ -48,7 +47,7 @@ public final class ProfilesCore {
Object userCache, minecraftSessionService, insecureProfiles = null;
Proxy proxy;
MethodHandle fillProfileProperties = null, getProfileByName, getProfileByUUID, cacheProfile;
MethodHandle profileSetterMeta, profileGetterMeta, getPropertyValue = null;
MethodHandle profileSetterMeta, profileGetterMeta;

ReflectiveNamespace ns = XReflection.namespaced()
.imports(GameProfile.class, MinecraftSessionService.class, LoadingCache.class);
Expand Down Expand Up @@ -107,6 +106,10 @@ public final class ProfilesCore {
).reflect();
}

// noinspection MethodMayBeStatic
UserCache_getNextOperation = GameProfileCache.method("private long getNextOperation();")
.map(MinecraftMapping.OBFUSCATED, v(21, "e").v(16, "d").orElse("d")).reflectOrNull();

MethodMemberHandle profileByName = GameProfileCache.method().named(/* v1.17.1 */ "getProfile", "a");
MethodMemberHandle profileByUUID = GameProfileCache.method().named(/* v1.17.1 */ "getProfile", "a");
getProfileByName = XReflection.anyOf(
Expand Down Expand Up @@ -137,9 +140,8 @@ public final class ProfilesCore {

FieldMemberHandle craftProfile = CraftSkull.field("private GameProfile profile;");

if (!NULLABILITY_RECORD_UPDATE) {
getPropertyValue = ns.of(Property.class).method("public String getValue();").unreflect();
}
Property_getValue = NULLABILITY_RECORD_UPDATE ? null :
ns.of(Property.class).method("public String getValue();").unreflect();

PROXY = proxy;
USER_CACHE = userCache;
Expand All @@ -149,7 +151,6 @@ public final class ProfilesCore {
GET_PROFILE_BY_NAME = getProfileByName;
GET_PROFILE_BY_UUID = getProfileByUUID;
CACHE_PROFILE = cacheProfile;
PROPERTY_GET_VALUE = getPropertyValue;
CRAFT_META_SKULL_PROFILE_SETTER = profileSetterMeta;
CRAFT_META_SKULL_PROFILE_GETTER = profileGetterMeta;
CRAFT_SKULL_PROFILE_SETTER = craftProfile.setter().unreflect();
Expand All @@ -162,6 +163,8 @@ public final class ProfilesCore {
UserCacheEntry_getProfile = UserCacheEntry.method("public GameProfile getProfile();")
.map(MinecraftMapping.OBFUSCATED, "a").makeAccessible()
.unreflect();
UserCacheEntry_setLastAccess = UserCacheEntry.method("public void setLastAccess(long i);")
.map(MinecraftMapping.OBFUSCATED, "a").reflectOrNull();

try {
// private final Map<String, UserCache.UserCacheEntry> profilesByName = Maps.newConcurrentMap();
Expand All @@ -172,12 +175,16 @@ public final class ProfilesCore {
UserCache_profilesByUUID = (Map<UUID, Object>) GameProfileCache.field("private final Map<UUID, UserCache.UserCacheEntry> profilesByUUID;")
.getter().map(MinecraftMapping.OBFUSCATED, v(17, "f").v(16, 2, "d").v(9, "e").orElse("d"))
.reflect().invoke(userCache);

// private final Deque<GameProfile> f = new LinkedBlockingDeque(); Removed in v1.16
// MethodHandle deque = GameProfileCache.field("private final Deque<GameProfile> f;")
// .getter().reflectOrNull();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

public static void debug(String mainMessage, Object... variables) {
LOGGER.debug(mainMessage, variables);
LOGGER.info(mainMessage, variables);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.cryptomorin.xseries.profiles.builder;

import com.cryptomorin.xseries.profiles.ProfilesCore;
import com.cryptomorin.xseries.profiles.exceptions.*;
import com.cryptomorin.xseries.profiles.exceptions.InvalidProfileException;
import com.cryptomorin.xseries.profiles.exceptions.MojangAPIException;
import com.cryptomorin.xseries.profiles.exceptions.ProfileChangeException;
import com.cryptomorin.xseries.profiles.exceptions.ProfileException;
import com.cryptomorin.xseries.profiles.mojang.PlayerProfileFetcherThread;
import com.cryptomorin.xseries.profiles.mojang.ProfileRequestConfiguration;
import com.cryptomorin.xseries.profiles.objects.ProfileContainer;
Expand Down Expand Up @@ -64,7 +67,7 @@ public ProfileInstruction<T> profileRequestConfiguration(ProfileRequestConfigura
}

/**
* Fails silently if any string based issues occur from a configuration standpoint.
* Fails silently if any of the {@link ProfileException} errors occur.
* Mainly affects {@link Profileable#detect(String)}
*/
public ProfileInstruction<T> lenient() {
Expand Down Expand Up @@ -137,17 +140,15 @@ public ProfileInstruction<T> onFallback(Runnable onFallback) {
* requires internet connection, will delay things a lot.
*
* @return The result after setting the generated profile.
* @throws APIRetryException due to being ratelimited or network issues that can be fixed if the request is sent later again.
* @throws MojangAPIException if any unknown non-recoverable network issues occur.
* @throws UnknownPlayerException if a specific player-identifying {@link Profileable} is not found.
* @throws InvalidProfileException if a given {@link Profileable} has incorrect value (more general than {@link UnknownPlayerException})
* @throws ProfileChangeException all the exceptions above are added as a suppressed exception to this exception.
* @throws ProfileChangeException If any type of {@link ProfileException} occurs, they will be accumulated
* in form of suppressed exceptions ({@link Exception#getSuppressed()}) in this single exception
* starting from the main profile, followed by the fallback profiles.
*/
public T apply() {
Objects.requireNonNull(profileable, "No profile was set");
ProfileChangeException exception = null;

List<Profileable> tries = new ArrayList<>(1 + fallbacks.size());
List<Profileable> tries = new ArrayList<>(2 + fallbacks.size());
tries.add(profileable);
tries.addAll(fallbacks);
if (lenient) tries.add(XSkull.getDefaultProfile());
Expand All @@ -160,7 +161,7 @@ public T apply() {
profileContainer.setProfile(profile);
success = true;
break;
} catch (MojangAPIException | InvalidProfileException | APIRetryException ex) {
} catch (ProfileException ex) {
if (exception == null) {
exception = new ProfileChangeException("Could not set the profile for " + profileContainer);
}
Expand All @@ -170,7 +171,7 @@ public T apply() {
}

if (exception != null) {
if (success || lenient) ProfilesCore.debug("apply()", exception);
if (success || lenient) ProfilesCore.debug("apply() silenced exception {}", exception);
else throw exception;
}

Expand All @@ -188,7 +189,7 @@ public T apply() {
* This method is designed for non-blocking execution, allowing tasks to be performed
* in the background without blocking the server's main thread.
* This method will always execute async, even if the results are cached.
* <p>
* <br>
* <h2>Reference Issues</h2>
* Note that while these methods apply to the item/block instances, passing these instances
* to certain methods, for example {@link org.bukkit.inventory.Inventory#setItem(int, ItemStack)}
Expand All @@ -201,6 +202,12 @@ public T apply() {
* .thenAcceptAsync(item -> inventory.setItem(slot, item));
* }</pre>
*
* To make this cleaner, you could change the first line of the item's lore to something like "Loading..."
* and set it to the inventory right away so the player knows that the data is not fully loaded.
* Once this method is done, you could change the lore back and set the item back to the inventory.
* (The lore is preferred because it has less text limit compared to the title, it also gives the player
* all the textual information they need rather than the visual information if you're in a hurry)
* <br><br><br>
* <h2>Usage example:</h2>
* <pre>{@code
* XSkull.createItem().profile(player).applyAsync()
Expand Down
Loading

0 comments on commit 219f510

Please sign in to comment.