Skip to content

fix(playerui): build heads from the live player profile - #108

Open
bobhenl wants to merge 2 commits into
CyR1en:developmentfrom
bobhenl:fix/playerui-live-profile-heads
Open

bobhenl wants to merge 2 commits into
CyR1en:developmentfrom
bobhenl:fix/playerui-live-profile-heads

Conversation

@bobhenl

@bobhenl bobhenl commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor
  • PlayerUI skulls of players without skin textures no longer get a profile the server has to look up. A textureless skull profile makes the server run PlayerProfile.update() on every build (and GuiItem re-applies the meta on every render). On offline-mode servers those Mojang lookups for v3 UUIDs never succeed, so they just pile up.
  • That plain-head branch is the effective change: untextured non-v4 players get a plain head, setOwningPlayer stays only for v4 UUIDs. Textured players get their live PlayerProfile, which on real Paper is behaviour-neutral (setOwningPlayer(Player) already uses the live profile).
  • The player name is stored in the head's PDC, so plain heads stay selectable and filter formats still apply to them.
  • The cache keeps the head and the skin/cape it was built from in one entry. A head is rebuilt when the textures change (skin arriving late from a skin plugin, or a skin change mid-session).
  • Offline players and failed builds leave no cache entry.
  • Tests in HeadCacheTest and PlayerUIScreenTest, including v3 untextured players. The first commit has run on our network (3 Paper/Leaf 26.3 servers, offline mode behind Velocity) since Oct 29 without issues.

Skull profiles without textures trigger a Mojang lookup on every build, which never succeeds for offline-mode UUIDs. Use the player's own profile instead and keep setOwningPlayer only for v4 UUIDs.
@CyR1en
CyR1en self-requested a review November 11, 2026 15:06

@CyR1en CyR1en left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I checked the diagnosis against the Paper 26.1.2–26.3 sources and it holds: CraftMetaSkull.applyToItem calls update() whenever the owner profile has no skin/cape, that goes straight to an uncached fetchProfile(uuid, true), and Mojang answers 204 for v3 UUIDs. It is actually worse than described, because GuiItem.applyUUID() re-applies the meta on every GUI render.

Requesting changes for one regression:

Plain heads can't be selected. PlayerUIScreen identifies the clicked player with skullMeta.getOwningPlayer() (PlayerUIScreen.java:132-134). The new plain head has no owner, so clicking an untextured non-v4 player does nothing. applyFirstFilterFormat (lines 261-263) has the same dependency, so filter formats are skipped for those heads too. Both reproduce in MockBukkit with a v3 player and pass on development. CI misses it because the default mock players get random v4 UUIDs.

Suggested fix: write the player name (or UUID) to the head's PDC in createHead and read that in both places, with tests using a v3 untextured player.

Smaller points:

  • hasTextures checks for a non-empty property, but the server triggers on "no skin and no cape URL". !profile.getTextures().isEmpty() mirrors it exactly.
  • cache and untextured are updated in two steps (invalidate, clear), which can drop a marker under Folia. A single map with a (item, textured) value would remove the invariant, or use synchronized (cache).
  • untextured.add happens before the build completes, so an exception leaves a marker with no entry.
  • On real Paper, setOwningPlayer(Player) already uses the live profile, so the textured branch is behaviour-neutral. Worth saying in the description that the effective change is the plain-head branch.
  • Nits: createHead returns null from an Optional method; the map can no longer hold Optional.empty(); the Javadoc says "version 3" but the check is "not version 4".

Optional follow-up: compare the cached textures value with the live one so mid-session skin changes are picked up as well.

Store the owner name in the head's PDC, mirror the server's skin/cape check, keep head and textures key in one entry and rebuild on skin change.
@bobhenl

bobhenl commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Pushed 7a9d8e5:

  • Plain heads are selectable again: createHead writes the player name to the head's PDC (commandprompter:head_owner), and both the click handler (now selectHead) and applyFirstFilterFormat read it through HeadCache.getOwnerName, falling back to getOwningPlayer. New tests with a v3 untextured player cover the click and the filter format; with the PDC write removed they fail.
  • hasTextures is gone; the check is now !profile.getTextures().isEmpty(), same as the server.
  • untextured is removed. The cache is a single map of (item, textures), and the compare-and-rebuild in getHeadFor runs under synchronized (cache). Nothing is stored if the build throws (test added).
  • Follow-up included: the cached textures key (skin + cape URL) is compared with the live one, so a mid-session skin change rebuilds the head.
  • Nits: createHead returns a nullable record instead of a null Optional, the map no longer holds Optional at all (the empty-entry tests are replaced), and the Javadoc now says non-version-4.

I also updated the description: on real Paper the textured branch is behaviour-neutral, the effective change is the plain-head branch.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants