Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- geo_radar.lua
- -- Advanced Pocket Computer + Advanced Peripherals "geo_scanner"
- -- 26x20 terminal: 17x17 radar + skinny right-side panel (no right border).
- --
- -- Scheduling:
- -- Single tick timer; run scan/chunk based on elapsed time (no chained timer IDs).
- -- scan() every 0.5s
- -- chunkAnalyze every 3.0s
- --
- -- Hotkeys:
- -- Q quit (top row)
- -- F find (add a tracked block id)
- --
- -- Symbols:
- -- ^ above, v below, x within ±1 Y
- local term, colors, os = term, colors, os
- -- ----------------------------
- -- Config
- -- ----------------------------
- local SCAN_RADIUS = 12
- local RADAR_W, RADAR_H = 17, 17
- local SCREEN_W, SCREEN_H = term.getSize()
- local RADAR_X0, RADAR_Y0 = 1, 2
- local SUMMARY_X0, SUMMARY_Y0 = 19, 2
- local PANEL_W = SCREEN_W - SUMMARY_X0 + 1
- local EMPTY_BG, EMPTY_FG, EMPTY_CH = colors.black, colors.gray, "."
- local SCAN_PERIOD_S = 0.5
- local CHUNK_PERIOD_S = 3.0
- local TICK_S = 0.10
- -- ----------------------------
- -- Peripheral
- -- ----------------------------
- local geo = peripheral.find("geo_scanner")
- if not geo then
- error("No geo_scanner found. Ensure the geo scanner is installed on this pocket computer.")
- end
- -- ----------------------------
- -- Find targets (user-configurable tracked blocks)
- -- id -> { abbr="....", fg=colors..., bg=colors... }
- -- ----------------------------
- local FIND_TARGETS = {}
- local function normalizeId(id)
- id = tostring(id or ""):gsub("%s+", "")
- if #id == 0 then return nil end
- return id
- end
- local function defaultAbbrFromId(id)
- local suffix = id:gsub("^.+:", "")
- local s = suffix:upper():gsub("[^A-Z0-9]", "")
- if #s == 0 then return "FIND" end
- return s:sub(1, 4)
- end
- local function colorFromName(name)
- name = tostring(name or ""):lower():gsub("%s+", "")
- if #name == 0 then return nil end
- return colors[name]
- end
- -- ----------------------------
- -- Ore classification + styling
- -- ----------------------------
- local function oreScore(relY)
- local ay = math.abs(relY)
- local band = (ay <= 1) and 0 or 1
- return band * 1000 + ay
- end
- local function hasOreTag(tags)
- if type(tags) ~= "table" then return false end
- for _, t in ipairs(tags) do
- if t == "forge:ores" or t == "c:ores" or t:find(":ores") then
- return true
- end
- end
- return false
- end
- local function isOreBlock(blockName, tags)
- if type(blockName) ~= "string" then return false end
- if hasOreTag(tags) then return true end
- if blockName:find("_ore") then return true end
- if blockName:find("ancient_debris") then return true end
- return false
- end
- local function oreKeyFromName(blockName)
- local n = blockName:gsub("^.+:", "")
- if n == "ancient_debris" then return "ancient_debris" end
- n = n:gsub("^deepslate_", "")
- n = n:gsub("^nether_", "")
- n = n:gsub("_ore$", "")
- return n
- end
- -- 4-char abbreviations for skinny panel.
- local ORE_ABBR = {
- diamond = "DIAM",
- emerald = "EMER",
- gold = "GOLD",
- iron = "IRON",
- copper = "COPP",
- redstone = "RDST",
- lapis = "LAPI",
- coal = "COAL",
- quartz = "QRTZ",
- ancient_debris = "DEBR",
- debris = "DEBR",
- }
- local function abbrForOreKey(key)
- local a = ORE_ABBR[key]
- if a then return a end
- local s = tostring(key):upper():gsub("[^A-Z0-9]", "")
- if #s == 0 then s = "ORE" end
- return s:sub(1, 4)
- end
- local ORE_STYLE = {
- diamond = { bg = colors.gray, fg = colors.lightBlue },
- emerald = { bg = colors.gray, fg = colors.lime },
- gold = { bg = colors.brown, fg = colors.yellow },
- iron = { bg = colors.gray, fg = colors.white },
- copper = { bg = colors.brown, fg = colors.orange },
- redstone = { bg = colors.gray, fg = colors.red },
- lapis = { bg = colors.gray, fg = colors.blue },
- coal = { bg = colors.black, fg = colors.lightGray },
- quartz = { bg = colors.gray, fg = colors.white },
- ancient_debris = { bg = colors.brown, fg = colors.orange },
- debris = { bg = colors.brown, fg = colors.orange },
- }
- local DEFAULT_ORE_STYLE = { bg = colors.gray, fg = colors.white }
- local function styleForOreKey(key)
- return ORE_STYLE[key] or DEFAULT_ORE_STYLE
- end
- local function symbolForRelY(relY)
- if relY > 1 then return "^" end
- if relY < -1 then return "v" end
- return "x"
- end
- -- ----------------------------
- -- Terminal helpers
- -- ----------------------------
- local function clearScreen()
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1, 1)
- end
- local function writeAt(x, y, text, fg, bg)
- if x < 1 or y < 1 or x > SCREEN_W or y > SCREEN_H then return end
- if bg then term.setBackgroundColor(bg) end
- if fg then term.setTextColor(fg) end
- term.setCursorPos(x, y)
- term.write(text)
- end
- local function fillRow(x, y, w, fg, bg)
- writeAt(x, y, string.rep(" ", w), fg, bg)
- end
- local function drawBox(x, y, w, h, title)
- writeAt(x, y, "+" .. string.rep("-", w - 2) .. "+", colors.lightGray, colors.black)
- for yy = y + 1, y + h - 2 do
- writeAt(x, yy, "|", colors.lightGray, colors.black)
- writeAt(x + w - 1, yy, "|", colors.lightGray, colors.black)
- end
- writeAt(x, y + h - 1, "+" .. string.rep("-", w - 2) .. "+", colors.lightGray, colors.black)
- if title and #title > 0 then
- local t = (" " .. title .. " "):sub(1, w - 2)
- writeAt(x + 1, y, t, colors.white, colors.black)
- end
- end
- -- Styled hotkey: highlighted letter + normal tail (no brackets).
- local function hotkeyToken(x, y, keyChar, tail)
- local keyFg, keyBg = colors.black, colors.yellow
- local restFg, restBg = colors.lightGray, colors.black
- writeAt(x, y, keyChar, keyFg, keyBg)
- writeAt(x + 1, y, tail, restFg, restBg)
- return 1 + #tail
- end
- -- Bottom-line prompt helper (read() consumes timer events; see addFindTarget()).
- local function promptLine(label)
- fillRow(1, SCREEN_H, SCREEN_W, colors.white, colors.black)
- writeAt(1, SCREEN_H, label, colors.lightGray, colors.black)
- term.setCursorPos(#label + 1, SCREEN_H)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.black)
- return read()
- end
- -- ----------------------------
- -- Formatting: 7 chars "AAAA999"
- -- ----------------------------
- local function oreLine7(abbr4, count)
- local c = math.floor(tonumber(count) or 0)
- if c > 999 then c = 999 end
- return (abbr4:sub(1, 4) .. string.format("%03d", c)):sub(1, 7)
- end
- -- ----------------------------
- -- Radar model
- -- ----------------------------
- local function newGrid()
- local g = {}
- for gx = 1, RADAR_W do
- g[gx] = {}
- for gz = 1, RADAR_H do
- g[gx][gz] = nil
- end
- end
- return g
- end
- local function relToGrid(relX, relZ)
- local cx = math.floor(RADAR_W / 2) + 1
- local cz = math.floor(RADAR_H / 2) + 1
- local gx = cx + relX
- local gz = cz + relZ
- if gx < 1 or gx > RADAR_W or gz < 1 or gz > RADAR_H then return nil end
- return gx, gz
- end
- -- grid cell stores:
- -- { relY=?, key=?, kind="find"|"ore" }
- local function buildRadar(blocks)
- local grid = newGrid()
- local oreCounts = {}
- local findCounts = {}
- for _, b in ipairs(blocks or {}) do
- local name = b.name
- -- Priority 1: explicit find targets
- local findDef = FIND_TARGETS[name]
- local isFind = (findDef ~= nil)
- -- Priority 2: ore classification
- local isOre = (not isFind) and isOreBlock(name, b.tags)
- if isFind or isOre then
- local gx, gz = relToGrid(b.x, b.z)
- if gx and gz then
- local kind, key
- if isFind then
- kind = "find"
- key = name
- findCounts[key] = (findCounts[key] or 0) + 1
- else
- kind = "ore"
- key = oreKeyFromName(name)
- oreCounts[key] = (oreCounts[key] or 0) + 1
- end
- local cand = { relY = b.y, key = key, kind = kind }
- local existing = grid[gx][gz]
- if not existing or oreScore(cand.relY) < oreScore(existing.relY) then
- grid[gx][gz] = cand
- end
- end
- end
- end
- return grid, oreCounts, findCounts
- end
- local function topNCounts(counts, n)
- local arr = {}
- for k, v in pairs(counts or {}) do
- arr[#arr + 1] = { k = k, v = v }
- end
- table.sort(arr, function(a, b)
- if a.v == b.v then return a.k < b.k end
- return a.v > b.v
- end)
- local out = {}
- for i = 1, math.min(n, #arr) do out[#out + 1] = arr[i] end
- return out
- end
- -- ----------------------------
- -- UI
- -- ----------------------------
- local function renderTopRow(statusLine)
- fillRow(1, 1, SCREEN_W, colors.white, colors.black)
- writeAt(1, 1, "Geo Ore Radar", colors.white, colors.black)
- if statusLine and #statusLine > 0 then
- local max = SCREEN_W - 22
- if max > 0 then
- local s = ("!" .. statusLine)
- if #s > max then s = s:sub(1, max) end
- writeAt(14, 1, s, colors.orange, colors.black)
- end
- end
- -- Right side tokens: F ind Q uit
- local qTail = "uit"
- local qStart = SCREEN_W - #qTail
- hotkeyToken(qStart, 1, "Q", qTail)
- local fTail = "ind"
- local fStart = qStart - (#fTail + 2) -- 1 for key+tail, 1 space
- if fStart >= 14 then
- hotkeyToken(fStart, 1, "F", fTail)
- end
- end
- local function renderRadar(grid)
- drawBox(RADAR_X0, RADAR_Y0, RADAR_W + 2, RADAR_H + 2, "Radar 17x17")
- for gz = 1, RADAR_H do
- for gx = 1, RADAR_W do
- local cell = grid[gx][gz]
- local x = RADAR_X0 + gx
- local y = RADAR_Y0 + gz
- if cell then
- local st
- if cell.kind == "find" then
- local def = FIND_TARGETS[cell.key]
- st = def and { fg = def.fg, bg = def.bg } or { fg = colors.white, bg = colors.purple }
- else
- st = styleForOreKey(cell.key)
- end
- writeAt(x, y, symbolForRelY(cell.relY), st.fg, st.bg)
- else
- writeAt(x, y, EMPTY_CH, EMPTY_FG, EMPTY_BG)
- end
- end
- end
- local cx = RADAR_X0 + (math.floor(RADAR_W / 2) + 1)
- local cy = RADAR_Y0 + (math.floor(RADAR_H / 2) + 1)
- writeAt(cx, cy, "@", colors.white, colors.black)
- end
- local function renderRightPanel(findCounts, oreCounts, chunkList)
- -- Clear panel
- for yy = SUMMARY_Y0, SCREEN_H do
- fillRow(SUMMARY_X0, yy, PANEL_W, colors.white, colors.black)
- end
- local x, y = SUMMARY_X0, SUMMARY_Y0
- -- FIND section (priority)
- if next(FIND_TARGETS) ~= nil then
- writeAt(x, y, "FIND", colors.lightGray, colors.black)
- y = y + 1
- local topFind = topNCounts(findCounts or {}, 12)
- for _, it in ipairs(topFind) do
- if y > SCREEN_H then break end
- local def = FIND_TARGETS[it.k]
- local abbr = (def and def.abbr) or defaultAbbrFromId(it.k)
- local fg = (def and def.fg) or colors.white
- local bg = (def and def.bg) or colors.purple
- local txt = oreLine7(abbr, it.v)
- writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), fg, bg)
- if PANEL_W > #txt then
- writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
- end
- y = y + 1
- end
- end
- -- NEAR ores
- if y <= SCREEN_H then
- writeAt(x, y, "NEAR", colors.lightGray, colors.black)
- y = y + 1
- end
- local topNear = topNCounts(oreCounts or {}, 12)
- for _, it in ipairs(topNear) do
- if y > SCREEN_H then break end
- local st = styleForOreKey(it.k)
- local txt = oreLine7(abbrForOreKey(it.k), it.v)
- writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), st.fg, st.bg)
- if PANEL_W > #txt then
- writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
- end
- y = y + 1
- end
- -- CHNK (chunkAnalyze)
- if chunkList and y + 2 <= SCREEN_H then
- writeAt(x, y, "CHNK", colors.lightGray, colors.black)
- y = y + 1
- for i = 1, #chunkList do
- if y > SCREEN_H then break end
- local it = chunkList[i]
- local key = oreKeyFromName(it.name)
- local st = styleForOreKey(key)
- local txt = oreLine7(abbrForOreKey(key), it.count)
- writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), st.fg, st.bg)
- if PANEL_W > #txt then
- writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
- end
- y = y + 1
- end
- end
- end
- -- ----------------------------
- -- Operations
- -- ----------------------------
- local function doScan()
- local ok, blocksOrErr = pcall(function() return geo.scan(SCAN_RADIUS) end)
- if not ok then
- return nil, nil, nil, "scan failed"
- end
- local grid, oreCounts, findCounts = buildRadar(blocksOrErr)
- return grid, oreCounts, findCounts, nil
- end
- local function doChunkAnalyze()
- local ok, dataOrErr = pcall(function() return geo.chunkAnalyze() end)
- if not ok then
- return nil, nil, "chunkAnalyze failed"
- end
- if type(dataOrErr) ~= "table" then
- return nil, nil, "chunkAnalyze bad result"
- end
- local arr = {}
- local counts = {}
- for name, count in pairs(dataOrErr) do
- local n = tostring(name)
- local c = tonumber(count) or 0
- arr[#arr + 1] = { name = n, count = c }
- local k = oreKeyFromName(n)
- counts[k] = (counts[k] or 0) + c
- end
- table.sort(arr, function(a, b)
- if a.count == b.count then return a.name < b.name end
- return a.count > b.count
- end)
- return arr, counts, nil
- end
- -- ----------------------------
- -- Scheduler
- -- ----------------------------
- local function nowMs()
- return os.epoch("utc")
- end
- local tickTimerId = nil
- local function armTickTimer()
- tickTimerId = os.startTimer(TICK_S)
- end
- -- ----------------------------
- -- Main-loop state
- -- ----------------------------
- local scanPeriodMs = math.floor(SCAN_PERIOD_S * 1000 + 0.5)
- local chunkPeriodMs = math.floor(CHUNK_PERIOD_S * 1000 + 0.5)
- local nextScanAt = nil
- local nextChunkAt = nil
- -- ----------------------------
- -- Find UI (FIXED: restart timer after read())
- -- ----------------------------
- local function addFindTarget()
- -- read() consumes timer events; invalidate timer id so we don't wait on a dead id.
- tickTimerId = nil
- local id = normalizeId(promptLine("Find id: "))
- if not id then
- -- Re-arm scheduler even if user entered nothing
- local t = nowMs()
- nextScanAt = t
- nextChunkAt = t
- armTickTimer()
- return
- end
- local abbr = promptLine("Abbr (4) [blank=auto]: ")
- abbr = tostring(abbr or ""):gsub("%s+", "")
- if #abbr == 0 then
- abbr = defaultAbbrFromId(id)
- else
- abbr = abbr:upper():sub(1, 4)
- if #abbr < 4 then abbr = abbr .. string.rep(" ", 4 - #abbr) end
- end
- local fgName = promptLine("FG color [blank=white]: ")
- local bgName = promptLine("BG color [blank=purple]: ")
- local fg = colorFromName(fgName) or colors.white
- local bg = colorFromName(bgName) or colors.purple
- FIND_TARGETS[id] = { abbr = abbr, fg = fg, bg = bg }
- -- Restart scheduler and realign deadlines to "now"
- local t = nowMs()
- nextScanAt = t
- nextChunkAt = t
- armTickTimer()
- end
- -- ----------------------------
- -- Main
- -- ----------------------------
- if SCREEN_W < 26 or SCREEN_H < 20 then
- error(("Terminal too small (%dx%d). Need at least 26x20."):format(SCREEN_W, SCREEN_H))
- end
- local grid, oreCounts, findCounts, scanErr = doScan()
- local chunkList, chunkCounts, chunkErr = doChunkAnalyze()
- local statusLine = scanErr or chunkErr
- do
- local t0 = nowMs()
- nextScanAt = t0
- nextChunkAt = t0
- end
- armTickTimer()
- while true do
- clearScreen()
- renderTopRow(statusLine)
- if grid then renderRadar(grid) end
- renderRightPanel(findCounts or {}, oreCounts or {}, chunkList)
- -- Ensure a tick is always armed (important after prompts)
- if not tickTimerId then
- armTickTimer()
- end
- local e, p1 = os.pullEvent()
- if e == "timer" and p1 == tickTimerId then
- local t = nowMs()
- if t >= nextScanAt then
- grid, oreCounts, findCounts, scanErr = doScan()
- statusLine = scanErr or chunkErr
- repeat nextScanAt = nextScanAt + scanPeriodMs until nextScanAt > t
- end
- if t >= nextChunkAt then
- chunkList, chunkCounts, chunkErr = doChunkAnalyze()
- statusLine = scanErr or chunkErr
- repeat nextChunkAt = nextChunkAt + chunkPeriodMs until nextChunkAt > t
- end
- armTickTimer()
- elseif e == "char" then
- local ch = tostring(p1):lower()
- if ch == "q" then
- clearScreen()
- term.setCursorPos(1, 1)
- return
- elseif ch == "f" then
- addFindTarget()
- -- Force an immediate scan after adding a target so it shows quickly.
- grid, oreCounts, findCounts, scanErr = doScan()
- statusLine = scanErr or chunkErr
- local t = nowMs()
- nextScanAt = t + scanPeriodMs
- -- tick timer is already re-armed by addFindTarget(); still safe to ensure:
- if not tickTimerId then armTickTimer() end
- end
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment