placeholders/papi.ts
7.1 KB · sha256:416eb0e3efcd4d9774d684f2df8a61f6933fa5b54496803862bb530bd05663e7
import { env } from "../config/env";
import {
findPlayerByUuid,
upsertPlayer,
hasPermission,
} from "../models/Player";
import { findGroup } from "../models/Group";
import { getLastDisplay } from "../lib/chatbridge";
let expansionRef: any = null;
export function registerPapiExpansion(): boolean {
if (!env.PAPI_EXPANSION) {
console.info("ward.papi: disabled via env.PAPI_EXPANSION=false");
return false;
}
if (typeof papi === "undefined" || !papi?.PlaceholderAPI) {
console.info("ward.papi: PAPI not present; skipping expansion register");
return false;
}
try {
const plugin =
papi.PlaceholderAPI.getInstance?.() ??
rune.bukkit.getPluginManager().getPlugin("PlaceholderAPI");
const mgr = plugin?.getLocalExpansionManager?.();
const existing = mgr?.getExpansion?.("ward");
if (existing) mgr.unregisterExpansion(existing);
} catch (e) {
console.debug(
"ward.papi: prior cleanup skipped:",
(e as Error)?.message ?? e,
);
}
try {
expansionRef = rune.implement(
"me.clip.placeholderapi.expansion.PlaceholderExpansion",
{
getIdentifier: () => "ward",
getAuthor: () => "ward",
getVersion: () => "1.0",
persist: () => true,
canRegister: () => true,
onRequest: (offlinePlayer: any, rawParams: any): string => {
const params = String(rawParams ?? "");
if (!offlinePlayer) return "";
const uuid = String(offlinePlayer.getUniqueId());
const display = getLastDisplay(uuid);
if (params === "prefix") return display.prefix;
if (params === "suffix") return display.suffix;
try {
return resolveParam(uuid, offlinePlayer, params);
} catch (e) {
console.warn(
`ward.papi: onRequest('${params}') failed:`,
(e as Error)?.message ?? e,
);
return "";
}
},
},
);
if (!expansionRef) {
console.warn("ward.papi: rune.implement returned null");
return false;
}
const ok = papi.PlaceholderAPI.registerExpansion(expansionRef);
if (ok) {
console.info("ward.papi: expansion registered (id=ward)");
} else {
console.warn(
"ward.papi: registerExpansion returned false (already registered?)",
);
}
return ok;
} catch (e) {
console.error("ward.papi: register failed:", (e as Error)?.stack ?? e);
return false;
}
}
export function unregisterPapiExpansion(): void {
if (!expansionRef || typeof papi === "undefined") return;
try {
papi.PlaceholderAPI.unregisterExpansion(expansionRef);
} catch {
}
expansionRef = null;
}
function resolveParam(
uuid: string,
offlinePlayer: any,
params: string,
): string {
const cached = cachedPlayer(uuid);
if (!cached) {
const name = offlinePlayer?.getName?.() ?? uuid;
void upsertPlayer(uuid, name).catch(() => {
});
return "";
}
if (params === "group" || params === "primary_group")
return cached.primaryGroup;
if (params === "groups") {
return cached.nodes
.filter((n) => n.type === "inheritance" && n.value)
.map((n) => n.key.slice("group.".length))
.join(",");
}
if (params === "weight") {
if (!cached.primaryGroup) return "0";
const fast = fastGroup(cached.primaryGroup);
return fast ? String(fast.weight) : "0";
}
if (params.startsWith("in_group_")) {
const target = params.slice("in_group_".length).toLowerCase();
const found = cached.nodes.some(
(n) => n.type === "inheritance" && n.value && n.key === `group.${target}`,
);
return found ? "true" : "false";
}
if (params.startsWith("has_perm_")) {
const perm = params.slice("has_perm_".length);
// hasPermission is async (resolves the full perm map). For PAPI's
// sync path we approximate via the cached node list: a direct grant
// wins, a direct deny wins, otherwise check wildcards in the
// player's own nodes. Group-inherited perms aren't visible without
// an async lookup -- fire-and-forget the warm and return "false"
// for unknowns so chat plugins render something sane.
const direct = cached.nodes.find(
(n) => n.type === "permission" && n.key === perm,
);
if (direct) return direct.value ? "true" : "false";
void hasPermission(uuid, perm).catch(() => {});
return "false";
}
if (params.startsWith("meta_")) {
const key = params.slice("meta_".length);
const meta = cached.nodes.find(
(n) => n.type === "meta" && n.key.startsWith(`meta.${key}.`),
);
if (!meta) return "";
return meta.key.slice(`meta.${key}.`.length);
}
return "";
}
// ---------------------------------------------------------------------------
// Sync cache shims. mongoose holds an identity-map of recently-fetched
// docs; we exploit that to avoid awaiting in PAPI's sync path.
// ---------------------------------------------------------------------------
const playerCache = new Map<string, ReturnType<typeof shapePlayer>>();
const groupCache = new Map<string, { weight: number }>();
function shapePlayer(doc: Awaited<ReturnType<typeof findPlayerByUuid>>) {
if (!doc) return null;
return {
primaryGroup: doc.primaryGroup,
nodes: doc.nodes.map((n) => ({
type: n.type,
key: n.key,
value: n.value,
expiry: n.expiry,
})),
};
}
/** Returns the cached snapshot (or null) AND warms it lazily. */
function cachedPlayer(uuid: string) {
const hit = playerCache.get(uuid);
if (hit !== undefined) return hit;
// Async warm; subsequent PAPI calls within the same session hit cache.
void findPlayerByUuid(uuid)
.then((doc) => playerCache.set(uuid, shapePlayer(doc)))
.catch(() => {});
return null;
}
function fastGroup(name: string) {
const hit = groupCache.get(name.toLowerCase());
if (hit !== undefined) return hit;
void findGroup(name)
.then((g) =>
groupCache.set(
name.toLowerCase(),
g ? { weight: g.weight } : { weight: 0 },
),
)
.catch(() => {});
return null;
}
/** Invalidate the cached player snapshot after a mutation. */
export function invalidatePlayer(uuid: string): void {
playerCache.delete(uuid);
}
/**
* Eagerly reload the cached snapshot from Mongo so the very next sync
* PAPI read (chat formatting, scoreboard tick, hologram refresh) hits a
* warm cache instead of returning "" and kicking a background warm.
*
* Call this whenever you'd otherwise call invalidatePlayer AND you know
* a placeholder read is likely to happen before another tick rolls by
* (e.g. straight after applyToPlayer on join).
*/
export async function refreshPlayerSnapshot(uuid: string): Promise<void> {
try {
const doc = await findPlayerByUuid(uuid);
playerCache.set(uuid, shapePlayer(doc));
} catch (e) {
// Leave whatever's there; the lazy warm in cachedPlayer retries.
console.warn(
`ward.papi: refreshPlayerSnapshot(${uuid}) failed:`,
(e as Error)?.message ?? e,
);
}
}
/** Invalidate a cached group snapshot (used by /ward group weight, etc). */
export function invalidateGroup(name: string): void {
groupCache.delete(name.toLowerCase());
}