Found in the cross-plugin Bedrock/Floodgate audit. Highest-leverage remaining fix — it's the binding resolver for every @Arg(...) OfflinePlayer command across Treasury and Business.
Bug: commander/resolvers/OfflinePlayerResolver.resolve tries Bukkit.getOfflinePlayerIfCached(token) then falls back to Bukkit.getOfflinePlayer(token) (deprecated). On an online-mode server (DC) that String overload does a blocking Mojang lookup, which FAILS for Bedrock players (their .-prefixed names aren't in Mojang's DB) — returning a fabricated/offline UUID, never the real Floodgate UUID. A cached Bedrock player (joined before, in usercache under .name) resolves fine; a not-yet-cached one resolves to a ghost.
Consumer impact:
/eco, /fine, /transactions audit, /tax trigger — mostly safe: each guards with hasPlayedBefore() right after, so a synthetic is rejected (just a worse error path + a needless Mojang call on the async thread).RequestCommands (HIGH): offer/hire and the transfer begin/confirm/cancel/complete/reject family take @Arg("user") OfflinePlayer with NO guard — hiring/transferring to a never-seen player by name writes the fabricated UUID into firm_staff/proprietor records, so the real (Bedrock) player is never actually employed/granted. Ghost rows.Recommended fix (one change, fixes all consumers): drop the Mojang fallback. Either (a) return the cache-only result and Optional.empty() when uncached — the framework then rejects with "unknown target" and no command acts on a ghost (cleanest; minor message regression vs the current synthetic-so-the-handler-can-message design), or (b) build the synthetic from a locally-computed offline UUID (UUID.nameUUIDFromBytes("OfflinePlayer:"+name)) instead of the Mojang call, preserving the "handler decides" contract but removing the network hit — then add hasPlayedBefore() guards to Business RequestCommands.offer/beginTransfer to stop ghost rows. Realty uses its own cache-only resolver and is unaffected.
Note: framework change → needs publishToMavenLocal + treasury/business rebuilds to take effect.
Released and rolled out to consumers.
1.0.2 published (main tip def658c). Version bumped from the already-released v1.0.1 so no collision.io.paradaux:hibernia-framework → 1.0.2 in the command plugins that carry @Arg OfflinePlayer commands and rebuilt clean against it:
a674fb2)0b6cf8b)4e17430)Not bumped: treasury-ingest (1.0.1) — no command surface, doesn't use the resolver; and business-api (compileOnly 0.1.2) — the API jar doesn't bundle the resolver and the SPI types it references are unchanged. Both fine to leave; can align later if desired.
Verified locally against 1.0.2 from mavenLocal (treasury + business unit suites green; treasury-api-plugin compiles). Their CI/release builds will resolve 1.0.2 from repo.paradaux.io.
Fixed on hibernia-framework
develop(889594c). Resolver is now cache-only — uncached names return empty so the framework rejects them; no Mojang call, no fabricated UUID. Verified treasury + business rebuild green against the new framework (published to mavenLocal).Deploy chain: this is a framework change, so for release/CI builds the new hibernia jar must be published to repo.paradaux.io (
./gradlew :treasury-api…— i.e. hiberniapublish), and treasury + business rebuilt to bundle it. Until then their shaded jars carry the old resolver. (Local mavenLocal is updated for dev builds.)Behavioural note: a never-seen name now fails with the framework's generic "invalid target" instead of a plugin-specific message — acceptable, and the data-integrity win (no ghost employment/proprietor rows from
/firm hire/transfer) is the point. Cached players, including Bedrock under their.-prefixed name, resolve as before.