MudkipTheEpic

Pocket Geo Scanner (AI Slop)

Jan 14th, 2026 (edited)
303
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.73 KB | None | 0 0
  1. -- geo_radar.lua
  2. -- Advanced Pocket Computer + Advanced Peripherals "geo_scanner"
  3. -- 26x20 terminal: 17x17 radar + skinny right-side panel (no right border).
  4. --
  5. -- Scheduling:
  6. --   Single tick timer; run scan/chunk based on elapsed time (no chained timer IDs).
  7. --   scan()        every 0.5s
  8. --   chunkAnalyze  every 3.0s
  9. --
  10. -- Hotkeys:
  11. --   Q quit (top row)
  12. --   F find (add a tracked block id)
  13. --
  14. -- Symbols:
  15. --   ^ above, v below, x within ±1 Y
  16.  
  17. local term, colors, os = term, colors, os
  18.  
  19. -- ----------------------------
  20. -- Config
  21. -- ----------------------------
  22. local SCAN_RADIUS = 12
  23.  
  24. local RADAR_W, RADAR_H = 17, 17
  25. local SCREEN_W, SCREEN_H = term.getSize()
  26.  
  27. local RADAR_X0, RADAR_Y0 = 1, 2
  28. local SUMMARY_X0, SUMMARY_Y0 = 19, 2
  29. local PANEL_W = SCREEN_W - SUMMARY_X0 + 1
  30.  
  31. local EMPTY_BG, EMPTY_FG, EMPTY_CH = colors.black, colors.gray, "."
  32.  
  33. local SCAN_PERIOD_S = 0.5
  34. local CHUNK_PERIOD_S = 3.0
  35. local TICK_S = 0.10
  36.  
  37. -- ----------------------------
  38. -- Peripheral
  39. -- ----------------------------
  40. local geo = peripheral.find("geo_scanner")
  41. if not geo then
  42.   error("No geo_scanner found. Ensure the geo scanner is installed on this pocket computer.")
  43. end
  44.  
  45. -- ----------------------------
  46. -- Find targets (user-configurable tracked blocks)
  47. -- id -> { abbr="....", fg=colors..., bg=colors... }
  48. -- ----------------------------
  49. local FIND_TARGETS = {}
  50.  
  51. local function normalizeId(id)
  52.   id = tostring(id or ""):gsub("%s+", "")
  53.   if #id == 0 then return nil end
  54.   return id
  55. end
  56.  
  57. local function defaultAbbrFromId(id)
  58.   local suffix = id:gsub("^.+:", "")
  59.   local s = suffix:upper():gsub("[^A-Z0-9]", "")
  60.   if #s == 0 then return "FIND" end
  61.   return s:sub(1, 4)
  62. end
  63.  
  64. local function colorFromName(name)
  65.   name = tostring(name or ""):lower():gsub("%s+", "")
  66.   if #name == 0 then return nil end
  67.   return colors[name]
  68. end
  69.  
  70. -- ----------------------------
  71. -- Ore classification + styling
  72. -- ----------------------------
  73. local function oreScore(relY)
  74.   local ay = math.abs(relY)
  75.   local band = (ay <= 1) and 0 or 1
  76.   return band * 1000 + ay
  77. end
  78.  
  79. local function hasOreTag(tags)
  80.   if type(tags) ~= "table" then return false end
  81.   for _, t in ipairs(tags) do
  82.     if t == "forge:ores" or t == "c:ores" or t:find(":ores") then
  83.       return true
  84.     end
  85.   end
  86.   return false
  87. end
  88.  
  89. local function isOreBlock(blockName, tags)
  90.   if type(blockName) ~= "string" then return false end
  91.   if hasOreTag(tags) then return true end
  92.   if blockName:find("_ore") then return true end
  93.   if blockName:find("ancient_debris") then return true end
  94.   return false
  95. end
  96.  
  97. local function oreKeyFromName(blockName)
  98.   local n = blockName:gsub("^.+:", "")
  99.   if n == "ancient_debris" then return "ancient_debris" end
  100.   n = n:gsub("^deepslate_", "")
  101.   n = n:gsub("^nether_", "")
  102.   n = n:gsub("_ore$", "")
  103.   return n
  104. end
  105.  
  106. -- 4-char abbreviations for skinny panel.
  107. local ORE_ABBR = {
  108.   diamond        = "DIAM",
  109.   emerald        = "EMER",
  110.   gold           = "GOLD",
  111.   iron           = "IRON",
  112.   copper         = "COPP",
  113.   redstone       = "RDST",
  114.   lapis          = "LAPI",
  115.   coal           = "COAL",
  116.   quartz         = "QRTZ",
  117.   ancient_debris = "DEBR",
  118.   debris         = "DEBR",
  119. }
  120.  
  121. local function abbrForOreKey(key)
  122.   local a = ORE_ABBR[key]
  123.   if a then return a end
  124.   local s = tostring(key):upper():gsub("[^A-Z0-9]", "")
  125.   if #s == 0 then s = "ORE" end
  126.   return s:sub(1, 4)
  127. end
  128.  
  129. local ORE_STYLE = {
  130.   diamond        = { bg = colors.gray,      fg = colors.lightBlue },
  131.   emerald        = { bg = colors.gray,      fg = colors.lime },
  132.   gold           = { bg = colors.brown,     fg = colors.yellow },
  133.   iron           = { bg = colors.gray,      fg = colors.white },
  134.   copper         = { bg = colors.brown,     fg = colors.orange },
  135.   redstone       = { bg = colors.gray,      fg = colors.red },
  136.   lapis          = { bg = colors.gray,      fg = colors.blue },
  137.   coal           = { bg = colors.black,     fg = colors.lightGray },
  138.   quartz         = { bg = colors.gray,      fg = colors.white },
  139.   ancient_debris = { bg = colors.brown,     fg = colors.orange },
  140.   debris         = { bg = colors.brown,     fg = colors.orange },
  141. }
  142. local DEFAULT_ORE_STYLE = { bg = colors.gray, fg = colors.white }
  143.  
  144. local function styleForOreKey(key)
  145.   return ORE_STYLE[key] or DEFAULT_ORE_STYLE
  146. end
  147.  
  148. local function symbolForRelY(relY)
  149.   if relY > 1 then return "^" end
  150.   if relY < -1 then return "v" end
  151.   return "x"
  152. end
  153.  
  154. -- ----------------------------
  155. -- Terminal helpers
  156. -- ----------------------------
  157. local function clearScreen()
  158.   term.setBackgroundColor(colors.black)
  159.   term.setTextColor(colors.white)
  160.   term.clear()
  161.   term.setCursorPos(1, 1)
  162. end
  163.  
  164. local function writeAt(x, y, text, fg, bg)
  165.   if x < 1 or y < 1 or x > SCREEN_W or y > SCREEN_H then return end
  166.   if bg then term.setBackgroundColor(bg) end
  167.   if fg then term.setTextColor(fg) end
  168.   term.setCursorPos(x, y)
  169.   term.write(text)
  170. end
  171.  
  172. local function fillRow(x, y, w, fg, bg)
  173.   writeAt(x, y, string.rep(" ", w), fg, bg)
  174. end
  175.  
  176. local function drawBox(x, y, w, h, title)
  177.   writeAt(x, y, "+" .. string.rep("-", w - 2) .. "+", colors.lightGray, colors.black)
  178.   for yy = y + 1, y + h - 2 do
  179.     writeAt(x, yy, "|", colors.lightGray, colors.black)
  180.     writeAt(x + w - 1, yy, "|", colors.lightGray, colors.black)
  181.   end
  182.   writeAt(x, y + h - 1, "+" .. string.rep("-", w - 2) .. "+", colors.lightGray, colors.black)
  183.   if title and #title > 0 then
  184.     local t = (" " .. title .. " "):sub(1, w - 2)
  185.     writeAt(x + 1, y, t, colors.white, colors.black)
  186.   end
  187. end
  188.  
  189. -- Styled hotkey: highlighted letter + normal tail (no brackets).
  190. local function hotkeyToken(x, y, keyChar, tail)
  191.   local keyFg, keyBg = colors.black, colors.yellow
  192.   local restFg, restBg = colors.lightGray, colors.black
  193.   writeAt(x, y, keyChar, keyFg, keyBg)
  194.   writeAt(x + 1, y, tail, restFg, restBg)
  195.   return 1 + #tail
  196. end
  197.  
  198. -- Bottom-line prompt helper (read() consumes timer events; see addFindTarget()).
  199. local function promptLine(label)
  200.   fillRow(1, SCREEN_H, SCREEN_W, colors.white, colors.black)
  201.   writeAt(1, SCREEN_H, label, colors.lightGray, colors.black)
  202.   term.setCursorPos(#label + 1, SCREEN_H)
  203.   term.setTextColor(colors.white)
  204.   term.setBackgroundColor(colors.black)
  205.   return read()
  206. end
  207.  
  208. -- ----------------------------
  209. -- Formatting: 7 chars "AAAA999"
  210. -- ----------------------------
  211. local function oreLine7(abbr4, count)
  212.   local c = math.floor(tonumber(count) or 0)
  213.   if c > 999 then c = 999 end
  214.   return (abbr4:sub(1, 4) .. string.format("%03d", c)):sub(1, 7)
  215. end
  216.  
  217. -- ----------------------------
  218. -- Radar model
  219. -- ----------------------------
  220. local function newGrid()
  221.   local g = {}
  222.   for gx = 1, RADAR_W do
  223.     g[gx] = {}
  224.     for gz = 1, RADAR_H do
  225.       g[gx][gz] = nil
  226.     end
  227.   end
  228.   return g
  229. end
  230.  
  231. local function relToGrid(relX, relZ)
  232.   local cx = math.floor(RADAR_W / 2) + 1
  233.   local cz = math.floor(RADAR_H / 2) + 1
  234.   local gx = cx + relX
  235.   local gz = cz + relZ
  236.   if gx < 1 or gx > RADAR_W or gz < 1 or gz > RADAR_H then return nil end
  237.   return gx, gz
  238. end
  239.  
  240. -- grid cell stores:
  241. --   { relY=?, key=?, kind="find"|"ore" }
  242. local function buildRadar(blocks)
  243.   local grid = newGrid()
  244.   local oreCounts = {}
  245.   local findCounts = {}
  246.  
  247.   for _, b in ipairs(blocks or {}) do
  248.     local name = b.name
  249.  
  250.     -- Priority 1: explicit find targets
  251.     local findDef = FIND_TARGETS[name]
  252.     local isFind = (findDef ~= nil)
  253.  
  254.     -- Priority 2: ore classification
  255.     local isOre = (not isFind) and isOreBlock(name, b.tags)
  256.  
  257.     if isFind or isOre then
  258.       local gx, gz = relToGrid(b.x, b.z)
  259.       if gx and gz then
  260.         local kind, key
  261.  
  262.         if isFind then
  263.           kind = "find"
  264.           key = name
  265.           findCounts[key] = (findCounts[key] or 0) + 1
  266.         else
  267.           kind = "ore"
  268.           key = oreKeyFromName(name)
  269.           oreCounts[key] = (oreCounts[key] or 0) + 1
  270.         end
  271.  
  272.         local cand = { relY = b.y, key = key, kind = kind }
  273.         local existing = grid[gx][gz]
  274.         if not existing or oreScore(cand.relY) < oreScore(existing.relY) then
  275.           grid[gx][gz] = cand
  276.         end
  277.       end
  278.     end
  279.   end
  280.  
  281.   return grid, oreCounts, findCounts
  282. end
  283.  
  284. local function topNCounts(counts, n)
  285.   local arr = {}
  286.   for k, v in pairs(counts or {}) do
  287.     arr[#arr + 1] = { k = k, v = v }
  288.   end
  289.   table.sort(arr, function(a, b)
  290.     if a.v == b.v then return a.k < b.k end
  291.     return a.v > b.v
  292.   end)
  293.   local out = {}
  294.   for i = 1, math.min(n, #arr) do out[#out + 1] = arr[i] end
  295.   return out
  296. end
  297.  
  298. -- ----------------------------
  299. -- UI
  300. -- ----------------------------
  301. local function renderTopRow(statusLine)
  302.   fillRow(1, 1, SCREEN_W, colors.white, colors.black)
  303.   writeAt(1, 1, "Geo Ore Radar", colors.white, colors.black)
  304.  
  305.   if statusLine and #statusLine > 0 then
  306.     local max = SCREEN_W - 22
  307.     if max > 0 then
  308.       local s = ("!" .. statusLine)
  309.       if #s > max then s = s:sub(1, max) end
  310.       writeAt(14, 1, s, colors.orange, colors.black)
  311.     end
  312.   end
  313.  
  314.   -- Right side tokens: F ind  Q uit
  315.   local qTail = "uit"
  316.   local qStart = SCREEN_W - #qTail
  317.   hotkeyToken(qStart, 1, "Q", qTail)
  318.  
  319.   local fTail = "ind"
  320.   local fStart = qStart - (#fTail + 2) -- 1 for key+tail, 1 space
  321.   if fStart >= 14 then
  322.     hotkeyToken(fStart, 1, "F", fTail)
  323.   end
  324. end
  325.  
  326. local function renderRadar(grid)
  327.   drawBox(RADAR_X0, RADAR_Y0, RADAR_W + 2, RADAR_H + 2, "Radar 17x17")
  328.   for gz = 1, RADAR_H do
  329.     for gx = 1, RADAR_W do
  330.       local cell = grid[gx][gz]
  331.       local x = RADAR_X0 + gx
  332.       local y = RADAR_Y0 + gz
  333.  
  334.       if cell then
  335.         local st
  336.         if cell.kind == "find" then
  337.           local def = FIND_TARGETS[cell.key]
  338.           st = def and { fg = def.fg, bg = def.bg } or { fg = colors.white, bg = colors.purple }
  339.         else
  340.           st = styleForOreKey(cell.key)
  341.         end
  342.         writeAt(x, y, symbolForRelY(cell.relY), st.fg, st.bg)
  343.       else
  344.         writeAt(x, y, EMPTY_CH, EMPTY_FG, EMPTY_BG)
  345.       end
  346.     end
  347.   end
  348.  
  349.   local cx = RADAR_X0 + (math.floor(RADAR_W / 2) + 1)
  350.   local cy = RADAR_Y0 + (math.floor(RADAR_H / 2) + 1)
  351.   writeAt(cx, cy, "@", colors.white, colors.black)
  352. end
  353.  
  354. local function renderRightPanel(findCounts, oreCounts, chunkList)
  355.   -- Clear panel
  356.   for yy = SUMMARY_Y0, SCREEN_H do
  357.     fillRow(SUMMARY_X0, yy, PANEL_W, colors.white, colors.black)
  358.   end
  359.  
  360.   local x, y = SUMMARY_X0, SUMMARY_Y0
  361.  
  362.   -- FIND section (priority)
  363.   if next(FIND_TARGETS) ~= nil then
  364.     writeAt(x, y, "FIND", colors.lightGray, colors.black)
  365.     y = y + 1
  366.  
  367.     local topFind = topNCounts(findCounts or {}, 12)
  368.     for _, it in ipairs(topFind) do
  369.       if y > SCREEN_H then break end
  370.       local def = FIND_TARGETS[it.k]
  371.       local abbr = (def and def.abbr) or defaultAbbrFromId(it.k)
  372.       local fg = (def and def.fg) or colors.white
  373.       local bg = (def and def.bg) or colors.purple
  374.       local txt = oreLine7(abbr, it.v)
  375.       writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), fg, bg)
  376.       if PANEL_W > #txt then
  377.         writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
  378.       end
  379.       y = y + 1
  380.     end
  381.   end
  382.  
  383.   -- NEAR ores
  384.   if y <= SCREEN_H then
  385.     writeAt(x, y, "NEAR", colors.lightGray, colors.black)
  386.     y = y + 1
  387.   end
  388.  
  389.   local topNear = topNCounts(oreCounts or {}, 12)
  390.   for _, it in ipairs(topNear) do
  391.     if y > SCREEN_H then break end
  392.     local st = styleForOreKey(it.k)
  393.     local txt = oreLine7(abbrForOreKey(it.k), it.v)
  394.     writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), st.fg, st.bg)
  395.     if PANEL_W > #txt then
  396.       writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
  397.     end
  398.     y = y + 1
  399.   end
  400.  
  401.   -- CHNK (chunkAnalyze)
  402.   if chunkList and y + 2 <= SCREEN_H then
  403.     writeAt(x, y, "CHNK", colors.lightGray, colors.black)
  404.     y = y + 1
  405.  
  406.     for i = 1, #chunkList do
  407.       if y > SCREEN_H then break end
  408.       local it = chunkList[i]
  409.       local key = oreKeyFromName(it.name)
  410.       local st = styleForOreKey(key)
  411.       local txt = oreLine7(abbrForOreKey(key), it.count)
  412.       writeAt(x, y, txt:sub(1, math.min(#txt, PANEL_W)), st.fg, st.bg)
  413.       if PANEL_W > #txt then
  414.         writeAt(x + #txt, y, string.rep(" ", PANEL_W - #txt), colors.white, colors.black)
  415.       end
  416.       y = y + 1
  417.     end
  418.   end
  419. end
  420.  
  421. -- ----------------------------
  422. -- Operations
  423. -- ----------------------------
  424. local function doScan()
  425.   local ok, blocksOrErr = pcall(function() return geo.scan(SCAN_RADIUS) end)
  426.   if not ok then
  427.     return nil, nil, nil, "scan failed"
  428.   end
  429.   local grid, oreCounts, findCounts = buildRadar(blocksOrErr)
  430.   return grid, oreCounts, findCounts, nil
  431. end
  432.  
  433. local function doChunkAnalyze()
  434.   local ok, dataOrErr = pcall(function() return geo.chunkAnalyze() end)
  435.   if not ok then
  436.     return nil, nil, "chunkAnalyze failed"
  437.   end
  438.   if type(dataOrErr) ~= "table" then
  439.     return nil, nil, "chunkAnalyze bad result"
  440.   end
  441.  
  442.   local arr = {}
  443.   local counts = {}
  444.   for name, count in pairs(dataOrErr) do
  445.     local n = tostring(name)
  446.     local c = tonumber(count) or 0
  447.     arr[#arr + 1] = { name = n, count = c }
  448.     local k = oreKeyFromName(n)
  449.     counts[k] = (counts[k] or 0) + c
  450.   end
  451.   table.sort(arr, function(a, b)
  452.     if a.count == b.count then return a.name < b.name end
  453.     return a.count > b.count
  454.   end)
  455.  
  456.   return arr, counts, nil
  457. end
  458.  
  459. -- ----------------------------
  460. -- Scheduler
  461. -- ----------------------------
  462. local function nowMs()
  463.   return os.epoch("utc")
  464. end
  465.  
  466. local tickTimerId = nil
  467. local function armTickTimer()
  468.   tickTimerId = os.startTimer(TICK_S)
  469. end
  470.  
  471. -- ----------------------------
  472. -- Main-loop state
  473. -- ----------------------------
  474. local scanPeriodMs = math.floor(SCAN_PERIOD_S * 1000 + 0.5)
  475. local chunkPeriodMs = math.floor(CHUNK_PERIOD_S * 1000 + 0.5)
  476.  
  477. local nextScanAt = nil
  478. local nextChunkAt = nil
  479.  
  480. -- ----------------------------
  481. -- Find UI (FIXED: restart timer after read())
  482. -- ----------------------------
  483. local function addFindTarget()
  484.   -- read() consumes timer events; invalidate timer id so we don't wait on a dead id.
  485.   tickTimerId = nil
  486.  
  487.   local id = normalizeId(promptLine("Find id: "))
  488.   if not id then
  489.     -- Re-arm scheduler even if user entered nothing
  490.     local t = nowMs()
  491.     nextScanAt = t
  492.     nextChunkAt = t
  493.     armTickTimer()
  494.     return
  495.   end
  496.  
  497.   local abbr = promptLine("Abbr (4) [blank=auto]: ")
  498.   abbr = tostring(abbr or ""):gsub("%s+", "")
  499.   if #abbr == 0 then
  500.     abbr = defaultAbbrFromId(id)
  501.   else
  502.     abbr = abbr:upper():sub(1, 4)
  503.     if #abbr < 4 then abbr = abbr .. string.rep(" ", 4 - #abbr) end
  504.   end
  505.  
  506.   local fgName = promptLine("FG color [blank=white]: ")
  507.   local bgName = promptLine("BG color [blank=purple]: ")
  508.  
  509.   local fg = colorFromName(fgName) or colors.white
  510.   local bg = colorFromName(bgName) or colors.purple
  511.  
  512.   FIND_TARGETS[id] = { abbr = abbr, fg = fg, bg = bg }
  513.  
  514.   -- Restart scheduler and realign deadlines to "now"
  515.   local t = nowMs()
  516.   nextScanAt = t
  517.   nextChunkAt = t
  518.   armTickTimer()
  519. end
  520.  
  521. -- ----------------------------
  522. -- Main
  523. -- ----------------------------
  524. if SCREEN_W < 26 or SCREEN_H < 20 then
  525.   error(("Terminal too small (%dx%d). Need at least 26x20."):format(SCREEN_W, SCREEN_H))
  526. end
  527.  
  528. local grid, oreCounts, findCounts, scanErr = doScan()
  529. local chunkList, chunkCounts, chunkErr = doChunkAnalyze()
  530. local statusLine = scanErr or chunkErr
  531.  
  532. do
  533.   local t0 = nowMs()
  534.   nextScanAt = t0
  535.   nextChunkAt = t0
  536. end
  537.  
  538. armTickTimer()
  539.  
  540. while true do
  541.   clearScreen()
  542.   renderTopRow(statusLine)
  543.   if grid then renderRadar(grid) end
  544.   renderRightPanel(findCounts or {}, oreCounts or {}, chunkList)
  545.  
  546.   -- Ensure a tick is always armed (important after prompts)
  547.   if not tickTimerId then
  548.     armTickTimer()
  549.   end
  550.  
  551.   local e, p1 = os.pullEvent()
  552.  
  553.   if e == "timer" and p1 == tickTimerId then
  554.     local t = nowMs()
  555.  
  556.     if t >= nextScanAt then
  557.       grid, oreCounts, findCounts, scanErr = doScan()
  558.       statusLine = scanErr or chunkErr
  559.       repeat nextScanAt = nextScanAt + scanPeriodMs until nextScanAt > t
  560.     end
  561.  
  562.     if t >= nextChunkAt then
  563.       chunkList, chunkCounts, chunkErr = doChunkAnalyze()
  564.       statusLine = scanErr or chunkErr
  565.       repeat nextChunkAt = nextChunkAt + chunkPeriodMs until nextChunkAt > t
  566.     end
  567.  
  568.     armTickTimer()
  569.  
  570.   elseif e == "char" then
  571.     local ch = tostring(p1):lower()
  572.     if ch == "q" then
  573.       clearScreen()
  574.       term.setCursorPos(1, 1)
  575.       return
  576.     elseif ch == "f" then
  577.       addFindTarget()
  578.       -- Force an immediate scan after adding a target so it shows quickly.
  579.       grid, oreCounts, findCounts, scanErr = doScan()
  580.       statusLine = scanErr or chunkErr
  581.       local t = nowMs()
  582.       nextScanAt = t + scanPeriodMs
  583.       -- tick timer is already re-armed by addFindTarget(); still safe to ensure:
  584.       if not tickTimerId then armTickTimer() end
  585.     end
  586.   end
  587. end
  588.  
Advertisement
Add Comment
Please, Sign In to add comment