CCCPaste

serverstats

-- server_stats.lua  (CC:Tweaked / ATM10)
-- Estimates TPS/MSPT by comparing in-game time to real time.
-- Works on ANY monitor size; auto-scales text and keeps layout tight.

-- ===== Helpers =====
local function pickTextScale(w, h)
  -- Try to fit 5 lines comfortably: title + tps + mspt + age + footer
  -- Larger monitors get smaller scale (more pixels, more columns)
  if w >= 60 and h >= 20 then return 0.5 end
  if w >= 28 and h >= 10 then return 0.75 end
  return 1 -- tiny monitor
end

local function findMonitor()
  local mon = peripheral.find("monitor")
  if not mon then error("No monitor attached. Place any monitor touching the computer.", 0) end
  return mon
end

local function setColors(mon)
  -- If not advanced, colors.* still exist but only white/black will show.
  if term.isColor() then
    mon.setTextColor(colors.white)
    mon.setBackgroundColor(colors.black)
  else
    mon.setTextColor(colors.white)
    mon.setBackgroundColor(colors.black)
  end
end

local function center(mon, y, s)
  local w, _ = mon.getSize()
  local x = math.max(1, math.floor((w - #s)/2) + 1)
  mon.setCursorPos(x, y)
  mon.write(s)
end

local function right(mon, y, s)
  local w, _ = mon.getSize()
  local x = math.max(1, w - #s + 1)
  mon.setCursorPos(x, y)
  mon.write(s)
end

local function avg(a)
  local s=0
  for i=1,#a do s = s + a[i] end
  return (#a > 0) and (s/#a) or 0
end

-- Estimate from clocks
-- os.epoch("ingame") advances ~50 ms per game tick at 20 TPS when healthy.
local function estimate(sample_ms)
  local r0 = os.epoch("utc")
  local g0 = os.epoch("ingame")
  sleep(sample_ms/1000)
  local r1 = os.epoch("utc")
  local g1 = os.epoch("ingame")
  local dR = math.max(1, r1 - r0)
  local dG = math.max(1, g1 - g0)
  -- If server is healthy: dG ≈ dR; TPS ≈ 20*(dG/dR), MSPT ≈ 50*(dR/dG)
  local tps  = 20 * (dG / dR)
  if tps < 0 then tps = 0 end
  if tps > 20 then tps = 20 end
  local mspt = 50 * (dR / dG)
  return tps, mspt
end

local function worldAgeText()
  local ms = os.epoch("ingame")
  local ticks = math.floor(ms / 50)
  local days  = math.floor(ticks / 24000)
  local hrs   = math.floor((ticks % 24000) / 1000) -- ~1000 ticks ≈ 1 in-game hour
  return ("%dd %dh"):format(days, hrs)
end

-- ===== Setup =====
local mon = findMonitor()
setColors(mon)
mon.clear()

-- Choose a text scale that actually fits the content
local w, h = mon.getSize()
local scale = pickTextScale(w, h)
mon.setTextScale(scale)
w, h = mon.getSize() -- refresh after scale change

-- Precompute line positions (keep it tight for small displays)
-- Layout (5 rows total):
-- 1: TITLE
-- 2: TPS line
-- 3: MSPT line
-- 4: WORLD AGE line
-- h: FOOTER
local rowTitle   = 1
local rowTPS     = math.min(2, h-3)
local rowMSPT    = math.min(3, h-2)
local rowAge     = math.min(4, h-1)
local rowFooter  = h

local samples = {}     -- rolling TPS average
local keepSamples = 6  -- ~6 seconds smoothing with 1s sample

-- ===== Main loop =====
while true do
  -- Take a 1s sample
  local tps, mspt = estimate(1000)
  table.insert(samples, tps)
  if #samples > keepSamples then table.remove(samples, 1) end
  local tpsAvg = avg(samples)

  -- Draw
  setColors(mon)
  mon.clear()

  center(mon, rowTitle, "SERVER STATS")
  -- TPS line
  local tpsStr  = ("TPS: %.2f"):format(tpsAvg)
  local tpsHint = (tpsAvg >= 19.5 and "OK") or (tpsAvg >= 15 and "WARN") or "LAG"
  local tpsLine = tpsStr .. "  [" .. tpsHint .. "]"
  center(mon, rowTPS, tpsLine)

  -- MSPT line
  local msptLine = ("MSPT: %.2f ms"):format(mspt)
  center(mon, rowMSPT, msptLine)

  -- World Age
  local ageLine = "World age: " .. worldAgeText()
  center(mon, rowAge, ageLine)

  -- Footer heartbeat + monitor size (debug on right so you can tell it's updating)
  mon.setCursorPos(1, rowFooter)
  mon.write("Ctrl+T to stop")
  right(mon, rowFooter, ("%dx%d x%.2f"):format(w, h, scale))
end