From 8454541b14b02d83d9c414544989203db7caac37 Mon Sep 17 00:00:00 2001 From: Mateusz Zaremba <0.zaremate@gmail.com> Date: Sat, 19 Sep 2026 11:31:07 +0200 Subject: [PATCH] feat: made clockin and notes scripts --- clockin.js | 1051 ++++++++++++++++++++++++++++++++++++++++++++++++++++ notes.js | 807 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1858 insertions(+) create mode 100644 clockin.js create mode 100644 notes.js diff --git a/clockin.js b/clockin.js new file mode 100644 index 0000000..7949ad7 --- /dev/null +++ b/clockin.js @@ -0,0 +1,1051 @@ +/* + * Copyright (c) 2026 @ZareMate + * Original author: @ZareMate + * + * Licensed under the MIT License. + * + * If you modify this script, you may additionally identify yourself as: + * + * Modified by: @YourUsername + * + * The original author attribution and copyright notice must remain. + */ + +// Dependencies: LuckPerms +(function () { + var CLOCKIN_FILE = "kubejs/data/clockin.json"; + var PERMISSION_LOGIN = "clockin.login"; + var PERMISSION_STATUS = "clockin.command.status"; + var PERMISSION_IN = "clockin.command.in"; + var PERMISSION_OUT = "clockin.command.out"; + var PERMISSION_LEADERBOARD = "clockin.command.leaderboard"; + var PERMISSION_INFO = "clockin.command.info"; + + var LuckPermsApiProvider = Java.loadClass( + "net.luckperms.api.LuckPermsProvider", + ); + + var luckPermsApi = null; + + function getLuckPerms() { + if (luckPermsApi) { + return luckPermsApi; + } + + try { + luckPermsApi = LuckPermsApiProvider.get(); + console.log("[ClockIn] LuckPerms API loaded."); + return luckPermsApi; + } catch (error) { + console.error( + "[ClockIn] Failed to load LuckPerms API: " + String(error), + ); + return null; + } + } + + function hasPermission(player, permission) { + var api = getLuckPerms(); + + if (!api || !player) { + return false; + } + + try { + var user = api.getUserManager().getUser(player.uuid); + + if (!user) { + return false; + } + + return user + .getCachedData() + .getPermissionData() + .checkPermission(permission) + .asBoolean(); + } catch (error) { + console.error( + "[ClockIn] Permission check failed for " + + String(player.username) + + ": " + + String(error), + ); + return false; + } + } + + function sourceHasPermission(source, permission) { + if (!source) { + return false; + } + + if (source.hasPermission(3)) { + return true; + } + + if (!source.isPlayer()) { + return false; + } + + return hasPermission(source.player, permission); + } + + function sourceHasAnyCommandPermission(source) { + return ( + sourceHasPermission(source, PERMISSION_STATUS) || + sourceHasPermission(source, PERMISSION_IN) || + sourceHasPermission(source, PERMISSION_OUT) || + sourceHasPermission(source, PERMISSION_LEADERBOARD) || + sourceHasPermission(source, PERMISSION_INFO) + ); + } + + var data = { + version: 1, + players: {}, + }; + + function createEmptyData() { + return { + version: 1, + players: {}, + }; + } + + function createPlayerData(uuid, name) { + return { + name: String(name), + totalSeconds: 0, + clockedIn: false, + clockInAt: 0, + autoClockedOut: false, + }; + } + + function getMapValue(object, key) { + if (!object) { + return null; + } + + try { + if (typeof object.get === "function") { + var value = object.get(key); + + if (value !== null && value !== undefined) { + return value; + } + } + } catch (error) {} + + try { + if (object[key] !== undefined && object[key] !== null) { + return object[key]; + } + } catch (error) {} + + return null; + } + + function toNumber(value, fallback) { + if (value === null || value === undefined) { + return fallback; + } + + try { + var number = Number(value); + + if (isFinite(number)) { + return number; + } + } catch (error) {} + + return fallback; + } + + function toBoolean(value, fallback) { + if (value === true || value === false) { + return value; + } + + var text = String(value).toLowerCase(); + + if (text === "true") { + return true; + } + + if (text === "false") { + return false; + } + + return fallback; + } + + function normalisePlayerData(input) { + var playerData = { + name: "Unknown", + totalSeconds: 0, + clockedIn: false, + clockInAt: 0, + autoClockedOut: false, + }; + + if (!input) { + return playerData; + } + + var name = getMapValue(input, "name"); + var totalSeconds = getMapValue(input, "totalSeconds"); + var clockedIn = getMapValue(input, "clockedIn"); + var clockInAt = getMapValue(input, "clockInAt"); + var autoClockedOut = getMapValue(input, "autoClockedOut"); + + if (name !== null && name !== undefined && String(name).length > 0) { + playerData.name = String(name); + } + + playerData.totalSeconds = Math.max( + 0, + Math.floor(toNumber(totalSeconds, 0)), + ); + + playerData.clockInAt = Math.max(0, Math.floor(toNumber(clockInAt, 0))); + + playerData.clockedIn = toBoolean(clockedIn, false); + playerData.autoClockedOut = toBoolean(autoClockedOut, false); + + return playerData; + } + + function loadData() { + try { + var loaded = JsonIO.read(CLOCKIN_FILE); + + console.log(loaded); + + data = createEmptyData(); + + if (!loaded) { + console.log("[ClockIn] No existing data file found."); + saveData(); + return; + } + + var version = getMapValue(loaded, "version"); + + if (version !== null && version !== undefined) { + data.version = Math.floor(toNumber(version, 1)); + } + + var loadedPlayers = getMapValue(loaded, "players"); + + if (!loadedPlayers) { + saveData(); + return; + } + + try { + if (typeof loadedPlayers.entrySet === "function") { + var entries = loadedPlayers.entrySet().iterator(); + + while (entries.hasNext()) { + var entry = entries.next(); + var uuid = String(entry.getKey()); + var playerData = normalisePlayerData(entry.getValue()); + + data.players[uuid] = playerData; + } + } else { + for (var uuid in loadedPlayers) { + var input = loadedPlayers[uuid]; + + if (!input) { + continue; + } + + data.players[String(uuid)] = normalisePlayerData(input); + } + } + } catch (error) { + console.error( + "[ClockIn] Failed to load player records: " + String(error), + ); + } + + console.log( + "[ClockIn] Loaded " + countPlayers() + " player record(s).", + ); + + saveData(); + } catch (error) { + console.error( + "[ClockIn] Failed to load clock-in data: " + String(error), + ); + + data = createEmptyData(); + saveData(); + } + } + + function saveData() { + try { + var output = { + version: 1, + players: {}, + }; + + for (var uuid in data.players) { + var playerData = data.players[uuid]; + + if (!playerData) { + continue; + } + + var totalSeconds = toNumber(playerData.totalSeconds, 0); + + if (!isFinite(totalSeconds) || totalSeconds < 0) { + totalSeconds = 0; + } + + var clockInAt = toNumber(playerData.clockInAt, 0); + + if (!isFinite(clockInAt) || clockInAt < 0) { + clockInAt = 0; + } + + output.players[String(uuid)] = { + name: String(playerData.name || "Unknown"), + totalSeconds: Math.floor(totalSeconds), + clockedIn: playerData.clockedIn === true, + clockInAt: Math.floor(clockInAt), + autoClockedOut: playerData.autoClockedOut === true, + }; + } + + JsonIO.write(CLOCKIN_FILE, output); + + console.log( + "[ClockIn] Saved " + countPlayers() + " player record(s).", + ); + } catch (error) { + console.error("[ClockIn] FAILED TO SAVE DATA: " + String(error)); + } + } + + function countPlayers() { + var count = 0; + + for (var uuid in data.players) { + count++; + } + + return count; + } + + function getPlayerUuid(player) { + return String(player.uuid); + } + + function getOrCreatePlayerData(player) { + var uuid = getPlayerUuid(player); + var playerData = data.players[uuid]; + + if (!playerData) { + playerData = createPlayerData(uuid, player.username); + + data.players[uuid] = playerData; + saveData(); + } else { + playerData.name = String(player.username); + + playerData.totalSeconds = Math.max( + 0, + Math.floor(toNumber(playerData.totalSeconds, 0)), + ); + + playerData.clockInAt = Math.max( + 0, + Math.floor(toNumber(playerData.clockInAt, 0)), + ); + } + + return playerData; + } + + function findPlayerDataByUuid(uuid) { + return data.players[String(uuid)] || null; + } + + function findPlayerDataByName(name) { + var target = String(name).toLowerCase(); + + for (var uuid in data.players) { + var playerData = data.players[uuid]; + + if (!playerData) { + continue; + } + + if (String(playerData.name).toLowerCase() === target) { + return playerData; + } + } + + return null; + } + + function getCurrentSessionSeconds(playerData) { + if ( + !playerData || + playerData.clockedIn !== true || + !playerData.clockInAt + ) { + return 0; + } + + var clockInAt = toNumber(playerData.clockInAt, 0); + + if (!isFinite(clockInAt) || clockInAt <= 0) { + return 0; + } + + var elapsed = Math.floor((Date.now() - clockInAt) / 1000); + + if (!isFinite(elapsed) || elapsed < 0) { + return 0; + } + + return elapsed; + } + + function getTotalSeconds(playerData) { + if (!playerData) { + return 0; + } + + var totalSeconds = toNumber(playerData.totalSeconds, 0); + + if (!isFinite(totalSeconds) || totalSeconds < 0) { + totalSeconds = 0; + } + + var currentSession = getCurrentSessionSeconds(playerData); + + return Math.max(0, Math.floor(totalSeconds) + currentSession); + } + + function formatDuration(totalSeconds) { + var seconds = toNumber(totalSeconds, 0); + + if (!isFinite(seconds) || seconds < 0) { + seconds = 0; + } + + seconds = Math.floor(seconds); + + var days = Math.floor(seconds / 86400); + seconds %= 86400; + + var hours = Math.floor(seconds / 3600); + seconds %= 3600; + + var minutes = Math.floor(seconds / 60); + seconds %= 60; + + if (days > 0) { + return ( + days + + "d " + + String(hours).padStart(2, "0") + + "h " + + String(minutes).padStart(2, "0") + + "m " + + String(seconds).padStart(2, "0") + + "s" + ); + } + + if (hours > 0) { + return ( + hours + + "h " + + String(minutes).padStart(2, "0") + + "m " + + String(seconds).padStart(2, "0") + + "s" + ); + } + + if (minutes > 0) { + return minutes + "m " + String(seconds).padStart(2, "0") + "s"; + } + + return seconds + "s"; + } + + function sendSeparator(player) { + player.tell(Text.of("────────────────────────────────────").darkGray()); + } + + function sendLoginPrompt(player) { + player.tell(""); + + player.tell(Text.of("ADMINISTRATION TIME TRACKING").gold().bold()); + + player.tell( + Text.of("You can track your administration help time.").gray(), + ); + + player.tell( + Text.of("[ CLOCK IN ]") + .green() + .bold() + .clickRunCommand("/clockin in"), + ); + + player.tell(Text.of("Use /clockin to view your tracked time.").gray()); + + player.tell(""); + } + + function clockIn(player) { + var playerData = getOrCreatePlayerData(player); + + if (playerData.clockedIn) { + player.tell(Text.of("You are already clocked in.").yellow()); + + return false; + } + + playerData.clockedIn = true; + playerData.clockInAt = Date.now(); + playerData.autoClockedOut = false; + + saveData(); + + player.tell(Text.of("You are now clocked in.").green()); + + player.tell( + Text.of("Administration time tracking has started.").gray(), + ); + + return true; + } + + function clockOut(player, automatic) { + var playerData = getOrCreatePlayerData(player); + + if (!playerData.clockedIn) { + player.tell(Text.of("You are not currently clocked in.").yellow()); + + return false; + } + + var sessionSeconds = getCurrentSessionSeconds(playerData); + + var totalSeconds = toNumber(playerData.totalSeconds, 0); + + if (!isFinite(totalSeconds) || totalSeconds < 0) { + totalSeconds = 0; + } + + playerData.totalSeconds = + Math.floor(totalSeconds) + Math.floor(sessionSeconds); + + playerData.clockedIn = false; + playerData.clockInAt = 0; + playerData.autoClockedOut = automatic === true; + + saveData(); + + if (automatic) { + player.tell( + Text.of( + "You were clocked out because you left the server.", + ).yellow(), + ); + + player.tell( + Text.of("Session time: ") + .gray() + .append(Text.of(formatDuration(sessionSeconds)).aqua()), + ); + + return true; + } + + player.tell(Text.of("You are now clocked out.").green()); + + player.tell( + Text.of("Session time: ") + .gray() + .append(Text.of(formatDuration(sessionSeconds)).aqua()), + ); + + player.tell( + Text.of("Total administration time: ") + .gray() + .append( + Text.of(formatDuration(playerData.totalSeconds)).aqua(), + ), + ); + + return true; + } + + function showStatus(player) { + var playerData = getOrCreatePlayerData(player); + var total = getTotalSeconds(playerData); + + sendSeparator(player); + + player.tell(Text.of("ADMIN CLOCK-IN").gold().bold()); + + if (playerData.clockedIn) { + player.tell( + Text.of("Status: ") + .gray() + .append(Text.of("CLOCKED IN").green().bold()), + ); + + player.tell( + Text.of("Current session: ") + .gray() + .append( + Text.of( + formatDuration( + getCurrentSessionSeconds(playerData), + ), + ).aqua(), + ), + ); + + player.tell( + Text.of("Total time: ") + .gray() + .append(Text.of(formatDuration(total)).aqua()), + ); + + player.tell(""); + + player.tell( + Text.of("[ CLOCK OUT ]") + .red() + .bold() + .clickRunCommand("/clockin out"), + ); + } else { + player.tell( + Text.of("Status: ") + .gray() + .append(Text.of("CLOCKED OUT").red().bold()), + ); + + player.tell( + Text.of("Total time: ") + .gray() + .append(Text.of(formatDuration(total)).aqua()), + ); + + player.tell(""); + + player.tell( + Text.of("[ CLOCK IN ]") + .green() + .bold() + .clickRunCommand("/clockin in"), + ); + } + + sendSeparator(player); + } + + function getLeaderboard() { + var entries = []; + + for (var uuid in data.players) { + var playerData = data.players[uuid]; + + if (!playerData) { + continue; + } + + entries.push({ + name: String(playerData.name || "Unknown"), + time: getTotalSeconds(playerData), + }); + } + + entries.sort(function (a, b) { + if (b.time !== a.time) { + return b.time - a.time; + } + + return a.name.localeCompare(b.name); + }); + + return entries; + } + + function showLeaderboard(source) { + var entries = getLeaderboard(); + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + source.sendSuccess( + Text.of("ADMIN CLOCK-IN LEADERBOARD").gold().bold(), + false, + ); + + if (entries.length === 0) { + source.sendSuccess( + Text.of("No administration time has been recorded yet.").gray(), + false, + ); + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + return; + } + + var limit = Math.min(entries.length, 10); + + for (var i = 0; i < limit; i++) { + var entry = entries[i]; + + source.sendSuccess( + Text.of("#" + (i + 1) + " ") + .gold() + .append(Text.of(entry.name).white()) + .append(Text.of(" — ").darkGray()) + .append(Text.of(formatDuration(entry.time)).aqua()), + false, + ); + } + + source.sendSuccess( + Text.of("Showing top " + limit + " players.").gray(), + false, + ); + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + } + + function showPlayerInfo(source, targetName) { + var playerData = findPlayerDataByName(targetName); + + if (!playerData) { + source.sendFailure( + Text.of("No clock-in data found for " + targetName + ".").red(), + ); + + return false; + } + + var total = getTotalSeconds(playerData); + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + source.sendSuccess(Text.of("ADMIN CLOCK-IN INFO").gold().bold(), false); + + source.sendSuccess( + Text.of("Player: ").gray().append(Text.of(playerData.name).white()), + false, + ); + + source.sendSuccess( + Text.of("Status: ") + .gray() + .append( + Text.of( + playerData.clockedIn ? "CLOCKED IN" : "CLOCKED OUT", + ).color(playerData.clockedIn ? "green" : "red"), + ), + false, + ); + + if (playerData.clockedIn) { + source.sendSuccess( + Text.of("Current session: ") + .gray() + .append( + Text.of( + formatDuration( + getCurrentSessionSeconds(playerData), + ), + ).aqua(), + ), + false, + ); + } + + source.sendSuccess( + Text.of("Total time: ") + .gray() + .append(Text.of(formatDuration(total)).aqua()), + false, + ); + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + return true; + } + + ServerEvents.commandRegistry(function (event) { + var Commands = event.commands; + var Arguments = event.arguments; + + var clockInCommand = Commands.literal("in") + .requires(function (source) { + return ( + source.isPlayer() && + sourceHasPermission(source, PERMISSION_IN) + ); + }) + .executes(function (ctx) { + try { + return clockIn(ctx.source.player) ? 1 : 0; + } catch (error) { + console.error( + "[ClockIn] /clockin in error: " + String(error), + ); + + ctx.source.sendFailure( + Text.of("Clock-in error: " + String(error)).red(), + ); + + return 0; + } + }); + + var clockOutCommand = Commands.literal("out") + .requires(function (source) { + return ( + source.isPlayer() && + sourceHasPermission(source, PERMISSION_OUT) + ); + }) + .executes(function (ctx) { + try { + return clockOut(ctx.source.player, false) ? 1 : 0; + } catch (error) { + console.error( + "[ClockIn] /clockin out error: " + String(error), + ); + + ctx.source.sendFailure( + Text.of("Clock-out error: " + String(error)).red(), + ); + + return 0; + } + }); + + var leaderboardCommand = Commands.literal("leaderboard") + .requires(function (source) { + return sourceHasPermission(source, PERMISSION_LEADERBOARD); + }) + .executes(function (ctx) { + try { + showLeaderboard(ctx.source); + return 1; + } catch (error) { + console.error( + "[ClockIn] /clockin leaderboard error: " + + String(error), + ); + + ctx.source.sendFailure( + Text.of("Leaderboard error: " + String(error)).red(), + ); + + return 0; + } + }); + + var infoCommand = Commands.literal("info") + .requires(function (source) { + return sourceHasPermission(source, PERMISSION_INFO); + }) + .then( + Commands.argument( + "player", + Arguments.STRING.create(event), + ).executes(function (ctx) { + try { + var targetName = Arguments.STRING.getResult( + ctx, + "player", + ); + + return showPlayerInfo(ctx.source, targetName) ? 1 : 0; + } catch (error) { + console.error( + "[ClockIn] /clockin info error: " + String(error), + ); + + ctx.source.sendFailure( + Text.of("Info error: " + String(error)).red(), + ); + + return 0; + } + }), + ); + + var clockInRoot = Commands.literal("clockin") + .requires(function (source) { + return sourceHasAnyCommandPermission(source); + }) + .executes(function (ctx) { + try { + if (!ctx.source.isPlayer()) { + ctx.source.sendFailure( + Text.of("Only players can use /clockin.").red(), + ); + + return 0; + } + + if (!sourceHasPermission(ctx.source, PERMISSION_STATUS)) { + ctx.source.sendFailure( + Text.of( + "You do not have permission to use /clockin.", + ).red(), + ); + + return 0; + } + + showStatus(ctx.source.player); + return 1; + } catch (error) { + console.error("[ClockIn] /clockin error: " + String(error)); + + ctx.source.sendFailure( + Text.of( + "Clock-in status error: " + String(error), + ).red(), + ); + + return 0; + } + }); + + clockInRoot.then(clockInCommand); + clockInRoot.then(clockOutCommand); + clockInRoot.then(leaderboardCommand); + clockInRoot.then(infoCommand); + + event.register(clockInRoot); + }); + + PlayerEvents.loggedIn(function (event) { + try { + var player = event.player; + var uuid = String(player.uuid); + var playerData = findPlayerDataByUuid(uuid); + + if (playerData && playerData.clockedIn) { + var sessionSeconds = getCurrentSessionSeconds(playerData); + + var totalSeconds = toNumber(playerData.totalSeconds, 0); + + if (!isFinite(totalSeconds) || totalSeconds < 0) { + totalSeconds = 0; + } + + playerData.totalSeconds = + Math.floor(totalSeconds) + Math.floor(sessionSeconds); + + playerData.clockedIn = false; + playerData.clockInAt = 0; + playerData.autoClockedOut = true; + playerData.name = String(player.username); + + saveData(); + } + + if (playerData && playerData.autoClockedOut) { + player.tell( + Text.of( + "You were clocked out because you left the server.", + ).yellow(), + ); + + player.tell( + Text.of( + "Your previous administration session has been closed.", + ).gray(), + ); + + playerData.autoClockedOut = false; + saveData(); + } + + if (hasPermission(player, PERMISSION_LOGIN)) { + sendLoginPrompt(player); + } + } catch (error) { + console.error("[ClockIn] Login handler error: " + String(error)); + } + }); + + PlayerEvents.loggedOut(function (event) { + try { + var player = event.player; + var uuid = String(player.uuid); + var playerData = findPlayerDataByUuid(uuid); + + if (!playerData || !playerData.clockedIn) { + return; + } + + var sessionSeconds = getCurrentSessionSeconds(playerData); + + var totalSeconds = toNumber(playerData.totalSeconds, 0); + + if (!isFinite(totalSeconds) || totalSeconds < 0) { + totalSeconds = 0; + } + + playerData.totalSeconds = + Math.floor(totalSeconds) + Math.floor(sessionSeconds); + + playerData.clockedIn = false; + playerData.clockInAt = 0; + playerData.autoClockedOut = true; + playerData.name = String(player.username); + + saveData(); + + console.log( + "[ClockIn] Automatically clocked out " + + String(player.username) + + " after " + + formatDuration(sessionSeconds) + + ".", + ); + } catch (error) { + console.error("[ClockIn] Logout handler error: " + String(error)); + } + }); + + loadData(); + + console.log("[ClockIn] Loaded successfully."); +})(); diff --git a/notes.js b/notes.js new file mode 100644 index 0000000..cf72f7d --- /dev/null +++ b/notes.js @@ -0,0 +1,807 @@ +/* + * Copyright (c) 2026 @ZareMate + * Original author: @ZareMate + * + * Licensed under the MIT License. + * + * If you modify this script, you may additionally identify yourself as: + * + * Modified by: @YourUsername + * + * The original author attribution and copyright notice must remain. + */ + +// Dependencies: LuckPerms +(function () { + var NOTES_FILE = "kubejs/data/notes.json"; + + var PERMISSION_READ = "note.read"; + var PERMISSION_ADD = "note.add"; + var PERMISSION_REMOVE = "note.rm"; + var PERMISSION_CLEAR = "note.clear"; + + var LuckPermsApiProvider = Java.loadClass( + "net.luckperms.api.LuckPermsProvider", + ); + + var luckPermsApi = null; + + var data = { + version: 1, + players: {}, + }; + + function getLuckPerms() { + if (luckPermsApi) { + return luckPermsApi; + } + + try { + luckPermsApi = LuckPermsApiProvider.get(); + return luckPermsApi; + } catch (error) { + console.error( + "[Notes] Failed to load LuckPerms API: " + String(error), + ); + + return null; + } + } + + function hasPermission(player, permission) { + var api = getLuckPerms(); + + if (!api || !player) { + return false; + } + + try { + var user = api.getUserManager().getUser(player.uuid); + + if (!user) { + return false; + } + + return user + .getCachedData() + .getPermissionData() + .checkPermission(permission) + .asBoolean(); + } catch (error) { + console.error("[Notes] Permission check failed: " + String(error)); + + return false; + } + } + + function sourceHasPermission(source, permission) { + if (!source) { + return false; + } + + if (source.hasPermission(3)) { + return true; + } + + if (!source.isPlayer()) { + return false; + } + + return hasPermission(source.player, permission); + } + + function hasAnyPermission(source) { + return ( + sourceHasPermission(source, PERMISSION_READ) || + sourceHasPermission(source, PERMISSION_ADD) || + sourceHasPermission(source, PERMISSION_REMOVE) || + sourceHasPermission(source, PERMISSION_CLEAR) + ); + } + + function getMapValue(object, key) { + if (!object) { + return null; + } + + try { + if (typeof object.get === "function") { + var value = object.get(key); + + if (value !== null && value !== undefined) { + return value; + } + } + } catch (error) {} + + try { + if (object[key] !== undefined && object[key] !== null) { + return object[key]; + } + } catch (error) {} + + return null; + } + + function getNumber(value, fallback) { + if (value === null || value === undefined) { + return fallback; + } + + try { + var number = Number(value); + + if (isFinite(number)) { + return number; + } + } catch (error) {} + + return fallback; + } + + function createPlayerData(name) { + return { + name: String(name), + notes: {}, + }; + } + + function createNote(author, text) { + return { + author: String(author), + text: String(text), + createdAt: Date.now(), + }; + } + + function normaliseNote(input) { + var note = { + author: "Unknown", + text: "", + createdAt: 0, + }; + + if (!input) { + return note; + } + + var author = getMapValue(input, "author"); + + var text = getMapValue(input, "text"); + + var createdAt = getMapValue(input, "createdAt"); + + if (author !== null && author !== undefined) { + note.author = String(author); + } + + if (text !== null && text !== undefined) { + note.text = String(text); + } + + note.createdAt = Math.floor(getNumber(createdAt, 0)); + + return note; + } + + function normalisePlayer(input) { + var player = { + name: "Unknown", + notes: {}, + }; + + if (!input) { + return player; + } + + var name = getMapValue(input, "name"); + + if (name !== null && name !== undefined) { + player.name = String(name); + } + + var loadedNotes = getMapValue(input, "notes"); + + if (!loadedNotes) { + return player; + } + + try { + if (typeof loadedNotes.entrySet === "function") { + var entries = loadedNotes.entrySet().iterator(); + + while (entries.hasNext()) { + var entry = entries.next(); + + var adminUuid = String(entry.getKey()); + + player.notes[adminUuid] = normaliseNote(entry.getValue()); + } + } else { + for (var adminUuid in loadedNotes) { + var note = loadedNotes[adminUuid]; + + if (!note) { + continue; + } + + player.notes[String(adminUuid)] = normaliseNote(note); + } + } + } catch (error) { + console.error("[Notes] Failed to load notes: " + String(error)); + } + + return player; + } + + function loadData() { + try { + var loaded = JsonIO.read(NOTES_FILE); + + data = { + version: 1, + players: {}, + }; + + if (!loaded) { + saveData(); + return; + } + + var loadedPlayers = getMapValue(loaded, "players"); + + if (!loadedPlayers) { + saveData(); + return; + } + + if (typeof loadedPlayers.entrySet === "function") { + var entries = loadedPlayers.entrySet().iterator(); + + while (entries.hasNext()) { + var entry = entries.next(); + + var uuid = String(entry.getKey()); + + data.players[uuid] = normalisePlayer(entry.getValue()); + } + } else { + for (var uuid in loadedPlayers) { + var player = loadedPlayers[uuid]; + + if (!player) { + continue; + } + + data.players[String(uuid)] = normalisePlayer(player); + } + } + + console.log( + "[Notes] Loaded " + countPlayers() + " player record(s).", + ); + + saveData(); + } catch (error) { + console.error("[Notes] Failed to load data: " + String(error)); + + data = { + version: 1, + players: {}, + }; + + saveData(); + } + } + + function saveData() { + try { + var output = { + version: 1, + players: {}, + }; + + for (var uuid in data.players) { + var player = data.players[uuid]; + + if (!player) { + continue; + } + + if (!player.notes || typeof player.notes !== "object") { + player.notes = {}; + } + + var playerOutput = { + name: String(player.name || "Unknown"), + notes: {}, + }; + + for (var adminUuid in player.notes) { + var note = player.notes[adminUuid]; + + if (!note) { + continue; + } + + playerOutput.notes[String(adminUuid)] = { + author: String(note.author || "Unknown"), + text: String(note.text || ""), + createdAt: Math.floor(getNumber(note.createdAt, 0)), + }; + } + + output.players[String(uuid)] = playerOutput; + } + + JsonIO.write(NOTES_FILE, output); + + console.log( + "[Notes] Saved " + countPlayers() + " player record(s).", + ); + } catch (error) { + console.error("[Notes] Failed to save data: " + String(error)); + } + } + + function countPlayers() { + var count = 0; + + for (var uuid in data.players) { + count++; + } + + return count; + } + + function getOnlinePlayer(name) { + try { + var players = Utils.server.getPlayerList().getPlayers(); + + var target = String(name).toLowerCase(); + + for (var i = 0; i < players.size(); i++) { + var player = players.get(i); + + if (String(player.username).toLowerCase() === target) { + return player; + } + } + } catch (error) { + console.error( + "[Notes] Failed to find online player: " + String(error), + ); + } + + return null; + } + + function findPlayerUuidByName(name) { + var online = getOnlinePlayer(name); + + if (online) { + return String(online.uuid); + } + + var target = String(name).toLowerCase(); + + for (var uuid in data.players) { + var player = data.players[uuid]; + + if (!player) { + continue; + } + + if (String(player.name).toLowerCase() === target) { + return String(uuid); + } + } + + return null; + } + + function getOrCreatePlayer(uuid, name) { + var key = String(uuid); + + var player = data.players[key]; + + if (!player) { + player = createPlayerData(name); + + data.players[key] = player; + } else { + player.name = String(name); + + if (!player.notes || typeof player.notes !== "object") { + player.notes = {}; + } + } + + return player; + } + + function formatDate(timestamp) { + var value = getNumber(timestamp, 0); + + if (value <= 0) { + return "Unknown"; + } + + try { + var date = new Date(value); + + function pad(number) { + return String(number).padStart(2, "0"); + } + + return ( + date.getFullYear() + + "-" + + pad(date.getMonth() + 1) + + "-" + + pad(date.getDate()) + + " " + + pad(date.getHours()) + + ":" + + pad(date.getMinutes()) + ); + } catch (error) { + return "Unknown"; + } + } + + function addNote(player, targetName, text) { + var noteText = String(text || "").trim(); + + if (noteText.length === 0) { + player.tell(Text.of("Note cannot be empty.").red()); + + return false; + } + + var targetUuid = findPlayerUuidByName(targetName); + + if (!targetUuid) { + player.tell(Text.of("Player not found.").red()); + + return false; + } + + var online = getOnlinePlayer(targetName); + + var targetNameStored; + + if (online) { + targetNameStored = String(online.username); + } else { + targetNameStored = String(data.players[targetUuid].name); + } + + var targetData = getOrCreatePlayer(targetUuid, targetNameStored); + + if (!targetData.notes || typeof targetData.notes !== "object") { + targetData.notes = {}; + } + + var adminUuid = String(player.uuid); + + targetData.notes[adminUuid] = createNote(player.username, noteText); + + saveData(); + + player.tell( + Text.of("Note saved for ") + .gray() + .append(Text.of(targetData.name).white()), + ); + + return true; + } + + function removeNote(player, targetName) { + var targetUuid = findPlayerUuidByName(targetName); + + if (!targetUuid) { + player.tell(Text.of("Player not found.").red()); + + return false; + } + + var targetData = data.players[targetUuid]; + + if (!targetData.notes || typeof targetData.notes !== "object") { + targetData.notes = {}; + } + + var adminUuid = String(player.uuid); + + if (!targetData.notes[adminUuid]) { + player.tell( + Text.of( + "You do not have a note for " + targetData.name + ".", + ).yellow(), + ); + + return false; + } + + delete targetData.notes[adminUuid]; + + saveData(); + + player.tell( + Text.of("Your note for ") + .gray() + .append(Text.of(targetData.name).white()) + .append(Text.of(" was removed.").green()), + ); + + return true; + } + + function clearNotes(source, targetName) { + var targetUuid = findPlayerUuidByName(targetName); + + if (!targetUuid) { + source.sendFailure(Text.of("Player not found.").red()); + + return false; + } + + var targetData = data.players[targetUuid]; + + if (!targetData.notes || typeof targetData.notes !== "object") { + targetData.notes = {}; + } + + var removed = 0; + + for (var adminUuid in targetData.notes) { + delete targetData.notes[adminUuid]; + + removed++; + } + + saveData(); + + source.sendSuccess( + Text.of("Cleared " + removed + " note(s) for ") + .gray() + .append(Text.of(targetData.name).white()), + false, + ); + + return true; + } + + function showNotes(source, targetName) { + var targetUuid = findPlayerUuidByName(targetName); + + if (!targetUuid) { + source.sendFailure(Text.of("Player not found.").red()); + + return false; + } + + var targetData = data.players[targetUuid]; + + if (!targetData.notes || typeof targetData.notes !== "object") { + targetData.notes = {}; + } + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + source.sendSuccess(Text.of("PLAYER NOTES").gold().bold(), false); + + source.sendSuccess( + Text.of("Player: ").gray().append(Text.of(targetData.name).white()), + false, + ); + + var count = 0; + + for (var adminUuid in targetData.notes) { + var note = targetData.notes[adminUuid]; + + if (!note) { + continue; + } + + count++; + + source.sendSuccess(Text.of("").white(), false); + + source.sendSuccess( + Text.of("[" + String(note.author) + "]") + .aqua() + .bold(), + false, + ); + + source.sendSuccess(Text.of(String(note.text)).white(), false); + + source.sendSuccess( + Text.of(formatDate(note.createdAt)).darkGray(), + false, + ); + } + + if (count === 0) { + source.sendSuccess( + Text.of("No notes have been added for this player.").gray(), + false, + ); + } + + source.sendSuccess( + Text.of("────────────────────────────────────").darkGray(), + false, + ); + + return true; + } + + ServerEvents.commandRegistry(function (event) { + var Commands = event.commands; + + var Arguments = event.arguments; + + var addCommand = Commands.literal("add") + .requires(function (source) { + return ( + source.isPlayer() && + sourceHasPermission(source, PERMISSION_ADD) + ); + }) + .then( + Commands.argument( + "player", + Arguments.STRING.create(event), + ).then( + Commands.argument( + "note", + Arguments.GREEDY_STRING.create(event), + ).executes(function (ctx) { + try { + var player = ctx.source.player; + + var targetName = Arguments.STRING.getResult( + ctx, + "player", + ); + + var noteText = Arguments.GREEDY_STRING.getResult( + ctx, + "note", + ); + + return addNote(player, targetName, noteText) + ? 1 + : 0; + } catch (error) { + console.error( + "[Notes] Add error: " + String(error), + ); + + ctx.source.sendFailure( + Text.of( + "Failed to add note: " + String(error), + ).red(), + ); + + return 0; + } + }), + ), + ); + + var removeCommand = Commands.literal("rm") + .requires(function (source) { + return ( + source.isPlayer() && + sourceHasPermission(source, PERMISSION_REMOVE) + ); + }) + .then( + Commands.argument( + "player", + Arguments.STRING.create(event), + ).executes(function (ctx) { + try { + return removeNote( + ctx.source.player, + Arguments.STRING.getResult(ctx, "player"), + ) + ? 1 + : 0; + } catch (error) { + console.error("[Notes] Remove error: " + String(error)); + + ctx.source.sendFailure( + Text.of( + "Failed to remove note: " + String(error), + ).red(), + ); + + return 0; + } + }), + ); + + var clearCommand = Commands.literal("clear") + .requires(function (source) { + return sourceHasPermission(source, PERMISSION_CLEAR); + }) + .then( + Commands.argument( + "player", + Arguments.STRING.create(event), + ).executes(function (ctx) { + try { + return clearNotes( + ctx.source, + Arguments.STRING.getResult(ctx, "player"), + ) + ? 1 + : 0; + } catch (error) { + console.error("[Notes] Clear error: " + String(error)); + + ctx.source.sendFailure( + Text.of( + "Failed to clear notes: " + String(error), + ).red(), + ); + + return 0; + } + }), + ); + + var noteRoot = Commands.literal("note") + .requires(function (source) { + return hasAnyPermission(source); + }) + .then(addCommand) + .then(removeCommand) + .then(clearCommand) + .then( + Commands.argument("player", Arguments.STRING.create(event)) + .requires(function (source) { + return sourceHasPermission(source, PERMISSION_READ); + }) + .executes(function (ctx) { + try { + return showNotes( + ctx.source, + Arguments.STRING.getResult(ctx, "player"), + ) + ? 1 + : 0; + } catch (error) { + console.error( + "[Notes] Read error: " + String(error), + ); + + ctx.source.sendFailure( + Text.of( + "Failed to read notes: " + String(error), + ).red(), + ); + + return 0; + } + }), + ); + + event.register(noteRoot); + }); + + loadData(); + + console.log("[Notes] Loaded successfully."); +})();