-- 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