lib/transactions.ts
5.7 KB · sha256:4335a92b5a93b11f5caebd1ed1490a5c92697c533fcba2a839481f1455beccb5
export type TransactionArgs = Record<string, unknown>;
export type TransactionCheck =
| { ok: true }
| { ok: false; reason: string; need?: number | string; have?: number | string };
export interface TransactionHandler<A extends TransactionArgs = TransactionArgs> {
check(player: Player, args: A): TransactionCheck | Promise<TransactionCheck>;
consume(player: Player, args: A): void | Promise<void>;
refund(player: Player, args: A): void | Promise<void>;
describe(args: A): string;
}
const registry = new Map<string, TransactionHandler>();
const missingPlugins = new Map<string, string>();
export function registerTransaction<A extends TransactionArgs = TransactionArgs>(
id: string,
handler: TransactionHandler<A>,
): void {
const key = id.toLowerCase();
if (registry.has(key)) {
console.warn(`ward.transactions: overwriting existing handler "${key}"`);
}
registry.set(key, handler as TransactionHandler);
}
export function unregisterTransaction(id: string): void {
registry.delete(id.toLowerCase());
}
export function getHandler(id: string): TransactionHandler | undefined {
return registry.get(id.toLowerCase());
}
export function listHandlers(): string[] {
return [...registry.keys()].sort();
}
export function markMissingPlugin(id: string, pluginName: string): void {
missingPlugins.set(id.toLowerCase(), pluginName);
}
export function missingPluginFor(id: string): string | undefined {
return missingPlugins.get(id.toLowerCase());
}
function numericArg(args: TransactionArgs, key: string): number | null {
const raw = args[key];
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
if (typeof raw === "string") {
const n = Number(raw);
if (Number.isFinite(n)) return n;
}
return null;
}
interface VaultEconomyRef {
getBalance(player: OfflinePlayer): number;
has(player: OfflinePlayer, amount: number): boolean;
withdrawPlayer(player: OfflinePlayer, amount: number): { transactionSuccess(): boolean; errorMessage?: string };
depositPlayer(player: OfflinePlayer, amount: number): { transactionSuccess(): boolean; errorMessage?: string };
}
function vaultEconomy(): VaultEconomyRef | null {
if (typeof vault === "undefined") return null;
try {
const eco = vault.Vault.getEconomy();
return (eco as unknown as VaultEconomyRef) ?? null;
} catch (e) {
console.warn(
"ward.transactions: vault probe failed: " +
((e as Error)?.message ?? e),
);
return null;
}
}
export function registerDefaultTransactions(): void {
const vaultPlugin = rune.bukkit.getPluginManager().getPlugin("Vault");
const eco = vaultEconomy();
if (eco) {
console.info(
"ward.transactions: vault:money ready (Vault2 detected, Economy provider registered)",
);
registerTransaction<{ amount: number }>("vault:money", {
check(player, args) {
const need = numericArg(args, "amount");
if (need == null || need < 0) return { ok: false, reason: "invalid amount" };
const have = eco.getBalance(player);
if (have < need) return { ok: false, reason: "insufficient funds", need, have };
return { ok: true };
},
consume(player, args) {
const need = numericArg(args, "amount") ?? 0;
const res = eco.withdrawPlayer(player, need);
if (!res.transactionSuccess()) {
throw new Error(res.errorMessage || "vault withdraw failed");
}
},
refund(player, args) {
const need = numericArg(args, "amount") ?? 0;
eco.depositPlayer(player, need);
},
describe(args) {
const n = numericArg(args, "amount");
return n == null ? "vault:money" : `$${n}`;
},
});
} else {
markMissingPlugin("vault:money", "Vault");
if (!vaultPlugin) {
console.warn(
"ward.transactions: vault:money disabled -- Vault2 plugin not installed. " +
"Download from https://github.com/shalom25/Vault2.0/releases " +
"and restart the server.",
);
} else {
console.warn(
"ward.transactions: vault:money disabled -- Vault2 is installed but no " +
"Economy provider is registered with the ServicesManager.",
);
}
}
registerTransaction<{ amount: number }>("vanilla:exp_levels", {
check(player, args) {
const need = numericArg(args, "amount");
if (need == null || need < 0) return { ok: false, reason: "invalid amount" };
const have = player.getLevel();
if (have < need) return { ok: false, reason: "insufficient levels", need, have };
return { ok: true };
},
consume(player, args) {
const need = numericArg(args, "amount") ?? 0;
player.setLevel(Math.max(0, player.getLevel() - need));
},
refund(player, args) {
const need = numericArg(args, "amount") ?? 0;
player.setLevel(player.getLevel() + need);
},
describe(args) {
const n = numericArg(args, "amount");
return n == null ? "xp levels" : `${n} xp level${n === 1 ? "" : "s"}`;
},
});
registerTransaction<{ amount: number }>("vanilla:exp_points", {
check(player, args) {
const need = numericArg(args, "amount");
if (need == null || need < 0) return { ok: false, reason: "invalid amount" };
const have = player.getTotalExperience();
if (have < need) return { ok: false, reason: "insufficient xp", need, have };
return { ok: true };
},
consume(player, args) {
const need = numericArg(args, "amount") ?? 0;
player.giveExp(-need);
},
refund(player, args) {
const need = numericArg(args, "amount") ?? 0;
player.giveExp(need);
},
describe(args) {
const n = numericArg(args, "amount");
return n == null ? "xp points" : `${n} xp`;
},
});
}