{"id":"4c45AdjCXW","url":"https://pastebin.ca/4c45AdjCXW","raw_url":"https://raw.anybin.ca/4c45AdjCXW","visibility":"public","access":"public","created_at":1789894152849,"expires_at":1790498952849,"fetch_limit":null,"fetches_used":0,"reads_remaining":null,"size_bytes":12392,"syntax_hint":null,"title":null,"filename":null,"change_note":null,"cipher":null,"cipher_meta":null,"parent_id":null,"root_id":"4c45AdjCXW","version":1,"owner_id":null,"recipient_id":null,"body":"-- ============================================================================\n-- Online Player Viewer\n-- Requires: monitor + playerDetector peripherals\n-- Dependencies: CC:Tweaked + Advanced Peripherals\n-- ============================================================================\n\n-- ============================================================================\n-- Configuration\n-- ============================================================================\nlocal REFRESH_INTERVAL = 1                 -- Refresh interval in seconds\nlocal TEXT_SCALE = 0.5                     -- Text scale (0.5 = smallest / most room)\n\n-- ============================================================================\n-- Color Theme\n-- ============================================================================\nlocal CLR = {\n    bg = colors.black,\n    title = colors.yellow,\n    header = colors.cyan,\n    text = colors.white,\n    white = colors.white,\n    dim = colors.lightGray,\n    online = colors.lime,\n    offline = colors.red,\n    warn = colors.orange,\n    highlight = colors.green,\n    accent = colors.pink,\n    border = colors.gray,\n}\n\n-- Dimension short names\nlocal DIM_NAMES = {\n    [\"minecraft:overworld\"] = \"Overworld\",\n    [\"minecraft:the_nether\"] = \"Nether\",\n    [\"minecraft:the_end\"] = \"End\",\n}\n\n-- ============================================================================\n-- Utility Functions\n-- ============================================================================\n\n--- Format dimension name\nlocal function formatDimension(dim)\n    return DIM_NAMES[dim] or dim or \"???\"\nend\n\n--- Format coordinates\nlocal function formatPos(x, y, z)\n    if not x then return \"---, ---, ---\" end\n    return string.format(\"%d, %d, %d\", math.floor(x), math.floor(y), math.floor(z))\nend\n\n--- Format health\nlocal function formatHealth(health, maxHealth)\n    if not health then return \"?/?\" end\n    return string.format(\"%.0f/%.0f\", health, maxHealth)\nend\n\n--- Draw centered text\nlocal function drawCentered(mon, y, text, color)\n    local w = mon.getSize()\n    mon.setTextColor(color or CLR.text)\n    local x = math.floor((w - #text) / 2) + 1\n    if x < 1 then x = 1 end\n    mon.setCursorPos(x, y)\n    mon.write(text)\nend\n\n--- Draw horizontal separator\nlocal function drawSeparator(mon, y, char, color)\n    local w = mon.getSize()\n    char = char or \"-\"\n    mon.setTextColor(color or CLR.border)\n    mon.setCursorPos(1, y)\n    mon.write(string.rep(char, w))\nend\n\n--- Truncate text to fit width\nlocal function truncate(text, maxLen)\n    if #text <= maxLen then return text end\n    return text:sub(1, maxLen - 1) .. \".\"\nend\n\n-- ============================================================================\n-- Peripheral Setup\n-- ============================================================================\n\nlocal function findPeripherals()\n    local monitor = nil\n    local detector = nil\n\n    local names = peripheral.getNames()\n    for _, name in ipairs(names) do\n        local ptype = peripheral.getType(name)\n        if ptype == \"monitor\" and not monitor then\n            monitor = peripheral.wrap(name)\n        elseif ptype == \"player_detector\" and not detector then\n            detector = peripheral.wrap(name)\n        end\n    end\n\n    return monitor, detector\nend\n\n-- ============================================================================\n-- Data Fetching\n-- ============================================================================\n\nlocal function getPlayerData(detector)\n    local players = {}\n    local ok, onlineList = pcall(detector.getOnlinePlayers, detector)\n\n    if not ok then\n        return nil, \"Failed to get player list: \" .. tostring(onlineList)\n    end\n\n    if not onlineList or #onlineList == 0 then\n        return {}, nil\n    end\n\n    for _, name in ipairs(onlineList) do\n        local info = detector.getPlayer(name)\n        if info then\n            table.insert(players, {\n                name = name,\n                x = info.x,\n                y = info.y,\n                z = info.z,\n                dimension = info.dimension,\n                health = info.health,\n                maxHealth = info.maxHealth,\n            })\n        else\n            -- Still show player even if detail fetch fails\n            table.insert(players, {\n                name = name,\n                x = nil, y = nil, z = nil,\n                dimension = nil,\n                health = nil, maxHealth = nil,\n            })\n        end\n    end\n\n    -- Sort alphabetically\n    table.sort(players, function(a, b) return a.name:lower() < b.name:lower() end)\n    return players, nil\nend\n\n-- ============================================================================\n-- Rendering\n-- ============================================================================\n\n-- Fixed row layout (the static frame is drawn once; only dynamic rows update)\nlocal ROW_STATS   = 4   -- player count / refresh rate / timestamp\nlocal ROW_HEADER  = 6   -- column headers\nlocal ROW_FIRST   = 8   -- first player row\n\n--- Blank a vertical region with the background color. Used instead of\n--- clearing the whole monitor so a refresh never flashes the entire screen.\nlocal function clearRows(mon, y1, y2)\n    local w = mon.getSize()\n    for y = y1, y2 do\n        mon.setBackgroundColor(CLR.bg)\n        mon.setCursorPos(1, y)\n        mon.write(string.rep(\" \", w))\n    end\nend\n\n--- Draw the static frame (title bar, separators, column header) once.\n--- It stays on screen; only the dynamic rows are rewritten afterwards.\nlocal function drawStaticFrame(mon)\n    local w = mon.getSize()\n\n    mon.setBackgroundColor(CLR.bg)\n    mon.clear()\n\n    -- Title bar\n    mon.setTextColor(CLR.title)\n    drawSeparator(mon, 1, \"=\", CLR.title)\n    drawCentered(mon, 2, \"** Online Player Viewer **\", CLR.title)\n    drawSeparator(mon, 3, \"=\", CLR.title)\n\n    -- Column header\n    local nameW = math.min(18, math.floor(w * 0.22))\n    local dimW  = math.min(10, math.floor(w * 0.12))\n    local posW  = math.min(24, math.floor(w * 0.35))\n    local hpW   = math.min(10, math.floor(w * 0.12))\n\n    mon.setTextColor(CLR.header)\n    mon.setCursorPos(2, ROW_HEADER)\n    local headerFmt = \"%-\" .. nameW .. \"s  %-\" .. dimW .. \"s  %-\" .. posW .. \"s  %\" .. hpW .. \"s\"\n    mon.write(string.format(headerFmt, \"Name\", \"Dimension\", \"Position (X, Y, Z)\", \"Health\"))\n    drawSeparator(mon, ROW_HEADER + 1, \"-\", CLR.border)\nend\n\n--- Redraw only the stats row (no full clear, so no flicker)\nlocal function renderStats(mon, playerCount, timestamp)\n    local w = mon.getSize()\n\n    mon.setBackgroundColor(CLR.bg)\n    mon.setCursorPos(1, ROW_STATS)\n    mon.write(string.rep(\" \", w))\n\n    mon.setTextColor(CLR.text)\n    mon.setCursorPos(2, ROW_STATS)\n    mon.write(\"Online: \")\n    mon.setTextColor(CLR.online)\n    mon.write(tostring(playerCount))\n\n    mon.setTextColor(CLR.dim)\n    mon.setCursorPos(math.floor(w / 2) + 1, ROW_STATS)\n    mon.write(\"Refresh: \" .. REFRESH_INTERVAL .. \"s\")\n\n    -- Timestamp on the right\n    local timeStr = \"Updated: \" .. timestamp\n    mon.setCursorPos(w - #timeStr, ROW_STATS)\n    mon.write(timeStr)\nend\n\nlocal function renderPlayerTable(mon, players)\n    local w, h = mon.getSize()\n    local lastRow = h - 2\n\n    if #players == 0 then\n        clearRows(mon, ROW_FIRST, lastRow)\n        drawCentered(mon, ROW_FIRST, \"(No players online)\", CLR.dim)\n        return\n    end\n\n    -- Column widths\n    local nameW = math.min(18, math.floor(w * 0.22))\n    local dimW  = math.min(10, math.floor(w * 0.12))\n    local posW  = math.min(24, math.floor(w * 0.35))\n    local hpW   = math.min(10, math.floor(w * 0.12))\n\n    local row = ROW_FIRST\n    for i, p in ipairs(players) do\n        if row > lastRow then\n            mon.setTextColor(CLR.dim)\n            mon.setCursorPos(2, lastRow)\n            mon.write(\"... +\" .. (#players - i + 1) .. \" more players\")\n            break\n        end\n\n        -- Alternating row colors\n        if i % 2 == 0 then\n            mon.setTextColor(CLR.text)\n        else\n            mon.setTextColor(CLR.white)\n        end\n\n        mon.setCursorPos(2, row)\n\n        -- Name\n        local name = truncate(p.name, nameW)\n        mon.write(string.format(\"%-\" .. nameW .. \"s\", name))\n        mon.setCursorPos(2 + nameW + 2, row)\n\n        -- Dimension\n        local dim = truncate(formatDimension(p.dimension), dimW)\n        if p.dimension == \"minecraft:the_nether\" then\n            mon.setTextColor(CLR.warn)\n        elseif p.dimension == \"minecraft:the_end\" then\n            mon.setTextColor(CLR.accent)\n        end\n        mon.write(string.format(\"%-\" .. dimW .. \"s\", dim))\n\n        -- Position\n        if i % 2 == 0 then\n            mon.setTextColor(CLR.text)\n        else\n            mon.setTextColor(CLR.white)\n        end\n        mon.setCursorPos(2 + nameW + 2 + dimW + 2, row)\n        local pos = formatPos(p.x, p.y, p.z)\n        mon.write(string.format(\"%-\" .. posW .. \"s\", pos))\n\n        -- Health\n        mon.setCursorPos(2 + nameW + 2 + dimW + 2 + posW + 2, row)\n        local hp = formatHealth(p.health, p.maxHealth)\n        if p.health and p.maxHealth then\n            local ratio = p.health / p.maxHealth\n            if ratio < 0.25 then\n                mon.setTextColor(CLR.offline)\n            elseif ratio < 0.5 then\n                mon.setTextColor(CLR.warn)\n            else\n                mon.setTextColor(CLR.highlight)\n            end\n        end\n        mon.write(string.format(\"%\" .. hpW .. \"s\", hp))\n\n        row = row + 1\n    end\n\n    -- Blank the rows left over when the new list is shorter\n    clearRows(mon, row, lastRow)\nend\n\nlocal function renderFooter(mon, errorMsg)\n    local w, h = mon.getSize()\n\n    mon.setBackgroundColor(CLR.bg)\n    mon.setCursorPos(1, h - 1)\n    mon.write(string.rep(\" \", w))\n    mon.setCursorPos(1, h)\n    mon.write(string.rep(\" \", w))\n\n    if errorMsg then\n        drawSeparator(mon, h - 1, \"-\", CLR.warn)\n        mon.setTextColor(CLR.warn)\n        mon.setCursorPos(2, h)\n        mon.write(\"! \" .. truncate(errorMsg, w - 4))\n    else\n        drawSeparator(mon, h - 1, \"-\", CLR.border)\n        mon.setTextColor(CLR.dim)\n        mon.setCursorPos(2, h)\n        mon.write(\"CC:Tweaked + Advanced Peripherals\")\n    end\nend\n\n-- ============================================================================\n-- Main Loop\n-- ============================================================================\n\nlocal function main()\n    local monitor, detector = findPeripherals()\n\n    if not monitor then\n        print(\"Error: Monitor peripheral not found!\")\n        print(\"Available: \" .. table.concat(peripheral.getNames(), \", \"))\n        return\n    end\n\n    if not detector then\n        print(\"Error: Player detector peripheral not found!\")\n        print(\"Available: \" .. table.concat(peripheral.getNames(), \", \"))\n        -- Show error on monitor too\n        monitor.setTextScale(TEXT_SCALE)\n        monitor.setBackgroundColor(CLR.bg)\n        monitor.clear()\n        monitor.setTextColor(CLR.warn)\n        drawCentered(monitor, math.floor(monitor.getSize() / 2), \"! Player detector not found !\", CLR.warn)\n        return\n    end\n\n    -- Init monitor\n    monitor.setTextScale(TEXT_SCALE)\n\n    print(\"Online Player Viewer started!\")\n    print(\"Monitor size: \" .. monitor.getSize())\n    print(\"Press Ctrl+T to stop\")\n\n    -- Draw the static frame once; only dynamic rows are redrawn each tick\n    drawStaticFrame(monitor)\n\n    -- Main loop\n    while true do\n        local timestamp = os.date(\"%H:%M:%S\")\n        local players, err = getPlayerData(detector)\n\n        renderStats(monitor, players and #players or 0, timestamp)\n        if players then\n            renderPlayerTable(monitor, players)\n        else\n            local _, h = monitor.getSize()\n            clearRows(monitor, ROW_FIRST, h - 2)\n        end\n        renderFooter(monitor, err)\n\n        sleep(REFRESH_INTERVAL)\n    end\nend\n\n-- ============================================================================\n-- Entry Point\n-- ============================================================================\n\nlocal ok, err = pcall(main)\nif not ok then\n    local monitor = peripheral.find(\"monitor\")\n    if monitor then\n        monitor.setTextScale(TEXT_SCALE)\n        monitor.setBackgroundColor(colors.black)\n        monitor.clear()\n        monitor.setTextColor(colors.red)\n        monitor.setCursorPos(1, 1)\n        monitor.write(\"Crash:\\n\" .. tostring(err))\n    end\n    print(\"Error: \" .. tostring(err))\nend\n"}