Myros27

DefragBot V3

May 18th, 2025 (edited)
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 39.05 KB | None | 0 0
  1. --[[
  2.     DefragBot v1.8.1 - Non-Stackable Disk Handling Update
  3.     - performDefragCycle updated to correctly collect non-stackable disks from ME Drives.
  4.     - Each disk is sucked into an individual turtle slot.
  5.     - Dropping to chest and subsequent IO processing handles these individual disks.
  6. ]]
  7.  
  8. --#region Configuration
  9. local CHAT_BOX_PERIPHERAL_NAME = "chatBox"
  10. local COMMAND_PREFIX = "@defrag"
  11. local CHAT_BOT_NAME = "DefragBot"
  12. local CHAT_BOT_BRACKETS = "[]"
  13. local CHAT_BOT_BRACKET_COLOR = "&3" -- Cyan
  14.  
  15. local DEBUG_LOGGING_ENABLED = true
  16. local LOG_FILE_NAME = "defrag.log"
  17. --#endregion
  18.  
  19. --#region Global State
  20. -- local lastAnnounceTime = 0 -- Removed for simpler announce
  21. local chatBox = nil
  22. local chatBoxFunctional = true -- Assume functional until a send error specifically
  23. --#endregion
  24.  
  25. --#region Logger Setup
  26. local function writeToLogFile(message) if not DEBUG_LOGGING_ENABLED then return end; local f,e=fs.open(LOG_FILE_NAME,"a"); if f then f.write(string.format("[%s] %s\n",os.date("%Y-%m-%d %H:%M:%S"),message));f.close() else print("LOG ERR: "..tostring(e))end end
  27. local function botLog(msg)local fm="["..CHAT_BOT_NAME.."-Debug] "..msg; print(fm); writeToLogFile(fm)end
  28. local function moveLog(msg)local fm="[MoveLib-Debug] "..msg; print(fm); writeToLogFile(fm)end
  29. --#endregion
  30.  
  31. --#region Movement Library Code (Integrated - Assumed same as v1.7)
  32. local AUTOMATA_PERIPHERAL_NAME="endAutomata";local POSITION_FILE="turtle_pos.json";local REFUEL_SLOT=16;local FUEL_ITEM_NAME_PART="coal";local automata=nil;local current_pos={x=nil,y=nil,z=nil};local current_dir=nil;local DIR_VECTORS={[0]={x=0,y=0,z=1},[1]={x=1,y=0,z=0},[2]={x=0,y=0,z=-1},[3]={x=-1,y=0,z=0}};local DIR_NAMES={[0]="North (+Z)",[1]="East (+X)",[2]="South (-Z)",[3]="West (-X)"};local function savePosition()if current_pos.x==nil or current_dir==nil then moveLog("No save, state unknown.");return end;local d={x=current_pos.x,y=current_pos.y,z=current_pos.z,dir=current_dir};local f,e=fs.open(POSITION_FILE,"w");if f then f.write(textutils.serialiseJSON(d));f.close();moveLog("Pos saved: X:"..d.x.." Y:"..d.y.." Z:"..d.z.." Dir:"..(DIR_NAMES[d.dir]or"Unk"))else moveLog("Err save pos: "..(e or"unk"))end end;local function loadPosition()if fs.exists(POSITION_FILE)then local f,e=fs.open(POSITION_FILE,"r");if f then local sD=f.readAll();f.close();local s,d=pcall(textutils.unserialiseJSON,sD);if s and d and d.x~=nil and d.y~=nil and d.z~=nil and d.dir~=nil then current_pos.x=d.x;current_pos.y=d.y;current_pos.z=d.z;current_dir=d.dir;moveLog("Pos loaded: X:"..current_pos.x.." Y:"..current_pos.y.." Z:"..current_pos.z.." Dir:"..(DIR_NAMES[current_dir]or"Unk"));return true else moveLog("Fail parse pos file: "..tostring(d))end else moveLog("Err open pos file: "..(e or"unk"))end else moveLog("Pos file not found.")end;return false end;local function dirNameToNumber(n)n=string.lower(n or"");if n=="n"or n=="north"then return 0 elseif n=="e"or n=="east"then return 1 elseif n=="s"or n=="south"then return 2 elseif n=="w"or n=="west"then return 3 end;moveLog("Warn: Invalid dir '"..(n or"nil").."'. Default N.");return 0 end;local function turnLeft()if current_dir==nil then moveLog("No turn: dir unk.");return false end;if turtle.turnLeft()then current_dir=(current_dir-1+4)%4;moveLog("Left. Dir:"..(DIR_NAMES[current_dir]or"Unk"));savePosition();return true else moveLog("Fail L turn.");return false end end;local function turnRight()if current_dir==nil then moveLog("No turn: dir unk.");return false end;if turtle.turnRight()then current_dir=(current_dir+1)%4;moveLog("Right. Dir:"..(DIR_NAMES[current_dir]or"Unk"));savePosition();return true else moveLog("Fail R turn.");return false end end;local function turnAround()if current_dir==nil then moveLog("No turn around: dir unk.");return false end;moveLog("Turn around...");if turnRight()and turnRight()then moveLog("Around. Dir:"..(DIR_NAMES[current_dir]or"Unk"));return true else moveLog("Fail turn around.");return false end end;local function forward()if current_pos.x==nil then moveLog("No move: pos unk.");return false end;if turtle.getFuelLevel()<1 and turtle.getFuelLimit()~=0 then moveLog("Fwd: <1 fuel.");return false end;if turtle.forward()then local v=DIR_VECTORS[current_dir];current_pos.x=current_pos.x+v.x;current_pos.y=current_pos.y+v.y;current_pos.z=current_pos.z+v.z;moveLog("Fwd. Pos: X:"..current_pos.x.." Y:"..current_pos.y.." Z:"..current_pos.z);savePosition();return true else moveLog("Fail fwd (block).");return false end end;local function back()if current_pos.x==nil then moveLog("No move: pos unk.");return false end;if turtle.getFuelLevel()<1 and turtle.getFuelLimit()~=0 then moveLog("Back: <1 fuel.");return false end;if turtle.back()then local v=DIR_VECTORS[current_dir];current_pos.x=current_pos.x-v.x;current_pos.y=current_pos.y-v.y;current_pos.z=current_pos.z-v.z;moveLog("Back. Pos: X:"..current_pos.x.." Y:"..current_pos.y.." Z:"..current_pos.z);savePosition();return true else moveLog("Fail back (block).");return false end end;local function up()if current_pos.y==nil then moveLog("No up: Y pos unk.");return false end;if turtle.getFuelLevel()<1 and turtle.getFuelLimit()~=0 then moveLog("Up: <1 fuel.");return false end;if turtle.up()then current_pos.y=current_pos.y+1;moveLog("Up. Pos: X:"..current_pos.x.." Y:"..current_pos.y.." Z:"..current_pos.z);savePosition();return true else moveLog("Fail up (block/no fuel).");return false end end;local function down()if current_pos.y==nil then moveLog("No down: Y pos unk.");return false end;if turtle.down()then current_pos.y=current_pos.y-1;moveLog("Down. Pos: X:"..current_pos.x.." Y:"..current_pos.y.." Z:"..current_pos.z);savePosition();return true else moveLog("Fail down (block).");return false end end;local function home()if not automata then automata=peripheral.find(AUTOMATA_PERIPHERAL_NAME);if not automata then moveLog("Err: EndAuto ('"..AUTOMATA_PERIPHERAL_NAME.."') miss.");return false end end;moveLog("Warp home...");local s,r=pcall(function()return automata.warpToPoint("home")end);if s and r then moveLog("Warp home OK.");current_pos.x=0;current_pos.y=0;current_pos.z=0;current_dir=0;moveLog("Pos reset home (000N).");savePosition();return true else moveLog("Fail warp home: "..tostring(r or"pcall err"));return false end end;local function refuel()moveLog("Refuel...");turtle.select(REFUEL_SLOT);local iBFS=turtle.getItemCount(REFUEL_SLOT);local iD=turtle.getItemDetail(REFUEL_SLOT);local iN="";if iD and iD.name then iN=string.lower(iD.name)else moveLog("Fuel slot "..REFUEL_SLOT.." empty/no name.")end;if iBFS>0 and string.find(iN,FUEL_ITEM_NAME_PART)then moveLog("Refuel from slot "..REFUEL_SLOT.." ("..(iD.name or"Unk Fuel").." x"..iBFS..")");if turtle.refuel(0)then local iAFS=turtle.getItemCount(REFUEL_SLOT);local cI=iBFS-iAFS;if cI>0 then moveLog("Refueled OK. Consumed "..cI.." "..(iD.name or"fuel")..".")else moveLog("Refueled, 0 consumed. Fuel: "..turtle.getFuelLevel())end else moveLog("turtle.refuel(0) fail. Fuel: "..turtle.getFuelLevel())end elseif iBFS>0 then moveLog("Item in fuel slot not fuel: "..(iD and iD.name or"Unk"))else moveLog("Fuel slot "..REFUEL_SLOT.." init empty.")end;local iTS=64-turtle.getItemCount(REFUEL_SLOT);local sTC=0;if iTS>0 then moveLog("Suck "..iTS.." to replenish fuel slot...");for _=1,iTS do if turtle.getItemCount(REFUEL_SLOT)>=64 then break end;local cSD=turtle.getItemDetail(REFUEL_SLOT);local cSN="";if cSD and cSD.name then cSN=string.lower(cSD.name)end;if turtle.getItemCount(REFUEL_SLOT)>0 and not string.find(cSN,FUEL_ITEM_NAME_PART)then moveLog("Slot "..REFUEL_SLOT.." has non-fuel ("..(cSD.name or"Unk").."). Stop suck.");break end;if turtle.suck()then sTC=sTC+1 else moveLog("Fail suck/chest empty after "..sTC.." items.");break end;sleep(0.1)end;if sTC>0 then moveLog("Sucked "..sTC.." items to replenish.")end else moveLog("Fuel slot full/non-fuel, no suck.")end;moveLog("Refuel finish. Fuel: "..turtle.getFuelLevel());return true end;local function setPos(x,y,z,dir)if type(x)~="number"or type(y)~="number"or type(z)~="number"then moveLog("Err: setPos xyz num.");return false end;current_pos.x=x;current_pos.y=y;current_pos.z=z;if type(dir)=="number"then current_dir=dir%4 else current_dir=dirNameToNumber(dir)end;moveLog("Pos set: X:"..x.." Y:"..y.." Z:"..z.." Dir:"..(DIR_NAMES[current_dir]or"Unk"));savePosition();return true end;local function turnToDir(td)if current_dir==nil then moveLog("No turnToDir: dir unk.");return false end;if current_dir==td then return true end;moveLog("Turn to dir: "..(DIR_NAMES[td]or"Unk"));local df=(td-current_dir+4)%4;if df==1 then return turnRight()elseif df==2 then return turnAround()elseif df==3 then return turnLeft()end;return false end;local function moveTo(tx,ty,tz,tdir)if current_pos.x==nil then moveLog("No moveTo: pos unk.");return false end;if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then if ty>current_pos.y then moveLog("moveTo: Low fuel ("..turtle.getFuelLevel()..") need up.")end end;moveLog(string.format("MoveTo: Tgt X%s Y%s Z%s Dir%s",tostring(tx),tostring(ty),tostring(tz),tostring(tdir)));local tdn;if type(tdir)=="number"then tdn=tdir%4 else tdn=dirNameToNumber(tdir)end;local att=0;local max_att=100;local max_stuck_ax=3;while(current_pos.x~=tx or current_pos.y~=ty or current_pos.z~=tz)and att<max_att do att=att+1;local moved=false;local cur_ax_stuck=0;if current_pos.y<ty then if up()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block UP("..cur_ax_stuck..")");if(current_pos.x~=tx or current_pos.z~=tz)and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir(math.random(0,3))and forward()then moved=true end else moveLog("moveTo: Stuck Y(up). Abort Y.");ty=current_pos.y end end elseif current_pos.y>ty then if down()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block DOWN("..cur_ax_stuck..")");if(current_pos.x~=tx or current_pos.z~=tz)and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir(math.random(0,3))and forward()then moved=true end else moveLog("moveTo: Stuck Y(down). Abort Y.");ty=current_pos.y end end end;if moved then goto continue_main_loop end;cur_ax_stuck=0;if current_pos.x<tx then if turnToDir(1)and forward()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block EAST("..cur_ax_stuck..")");if current_pos.z~=tz and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir((math.random(0,1)*2))and forward()then moved=true end else moveLog("moveTo: Stuck X(east). Abort X.");tx=current_pos.x end end elseif current_pos.x>tx then if turnToDir(3)and forward()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block WEST("..cur_ax_stuck..")");if current_pos.z~=tz and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir((math.random(0,1)*2))and forward()then moved=true end else moveLog("moveTo: Stuck X(west). Abort X.");tx=current_pos.x end end end;if moved then goto continue_main_loop end;cur_ax_stuck=0;if current_pos.z<tz then if turnToDir(0)and forward()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block NORTH("..cur_ax_stuck..")");if current_pos.x~=tx and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir((math.random(0,1)*2)+1)and forward()then moved=true end else moveLog("moveTo: Stuck Z(north). Abort Z.");tz=current_pos.z end end elseif current_pos.z>tz then if turnToDir(2)and forward()then moved=true else cur_ax_stuck=cur_ax_stuck+1;moveLog("moveTo: Block SOUTH("..cur_ax_stuck..")");if current_pos.x~=tx and cur_ax_stuck<max_stuck_ax then elseif cur_ax_stuck<max_stuck_ax then if turnToDir((math.random(0,1)*2)+1)and forward()then moved=true end else moveLog("moveTo: Stuck Z(south). Abort Z.");tz=current_pos.z end end end;if moved then goto continue_main_loop end;if not moved then moveLog("moveTo: No move. Att: "..att);if cur_ax_stuck>=max_stuck_ax then break end end;::continue_main_loop::;sleep(0.05)end;if att>=max_att then moveLog("moveTo: Max att.")end;if not turnToDir(tdn)then moveLog("moveTo: Fail final turn.")end;local s=current_pos.x==tx and current_pos.y==ty and current_pos.z==tz and current_dir==tdn;if s then moveLog("moveTo: OK.")else moveLog(string.format("moveTo: Finish. Pos X%s Y%s Z%s Dir%s.",tostring(current_pos.x),tostring(current_pos.y),tostring(current_pos.z),(DIR_NAMES[current_dir]or"Unk")))end;return s end;local function initMoveLibrary()moveLog("Init MoveLib...");automata=peripheral.find(AUTOMATA_PERIPHERAL_NAME);if not automata then moveLog("CRIT WARN: EndAuto ('"..AUTOMATA_PERIPHERAL_NAME.."') miss. Home warp fail.")end;if not loadPosition()then moveLog("Pos unk/load fail. Warp home & set default.");if home()then moveLog("Init home OK.")else moveLog("CRIT: Fail init home. Pos unk.");current_pos.x=nil;current_pos.y=nil;current_pos.z=nil;current_dir=nil end end;moveLog("MoveLib Init. Pos: X:"..tostring(current_pos.x).." Y:"..tostring(current_pos.y).." Z:"..tostring(current_pos.z).." Dir:"..(current_dir and DIR_NAMES[current_dir]or"Unk"))end;local function getPosition()return current_pos end;local function getDirection()return current_dir end;local function getDirectionName()return current_dir and DIR_NAMES[current_dir]or"Unknown"end;local ml={l=turnLeft,turnLeft=turnLeft,r=turnRight,turnRight=turnRight,f=forward,forward=forward,b=back,back=back,a=turnAround,turnAround=turnAround,u=up,up=up,d=down,down=down,h=home,home=home,refuel=refuel,setPos=setPos,m=moveTo,moveTo=moveTo,getPosition=getPosition,getDirection=getDirection,getDirectionName=getDirectionName,init=initMoveLibrary};initMoveLibrary();
  33. --#endregion End of Integrated Movement Library
  34.  
  35. --#region DefragBot Specific Data & Logic
  36. local LOCATIONS={home={x=0,y=0,z=0,dir='N',name="Refuel Station"},io_port={x=1,y=0,z=0,dir='N',name="IO Port (Disk Input)"},hopper={x=1,y=1,z=0,dir='N',name="Hopper"},chest_temp_storage={x=1,y=2,z=0,dir='N',name="Chest (Temp Disk Storage)"}}
  37. local ME_DRIVE_BAY_RANGES={{y=0,x_start=-9,x_end=-2,dir='N',name="Lower Bay Row 1"},{y=2,x_start=-9,x_end=-2,dir='N',name="Lower Bay Row 2"},{y=4,x_start=-9,x_end=-2,dir='N',name="Lower Bay Row 3"},{y=9,x_start=-9,x_end=-2,dir='N',name="Upper Bay Row 1"},{y=11,x_start=-9,x_end=-2,dir='N',name="Upper Bay Row 2"},{y=13,x_start=-9,x_end=-2,dir='N',name="Upper Bay Row 3"},}
  38. local function getAllMeDriveBayCoordinates()local cs={};for _,br in ipairs(ME_DRIVE_BAY_RANGES)do for x=br.x_start,br.x_end do table.insert(cs,{x=x,y=br.y,z=0,dir=br.dir,name=br.name.." (X:"..x..")"})end end;return cs end
  39. local ALL_ME_DRIVE_LOCATIONS=getAllMeDriveBayCoordinates()
  40. local isDefragRunning=false;local fuelWarningSent=false
  41. --#endregion
  42.  
  43. --#region DefragBot Peripherals and Helpers
  44. local function sendFormattedChat(messageComponents) -- Simplified, always global
  45.     local plainText = ""
  46.     if type(messageComponents) == "table" then
  47.         for _,c_comp in ipairs(messageComponents) do
  48.             plainText = plainText .. (type(c_comp) == "table" and c_comp.text or tostring(c_comp) or "")
  49.         end
  50.     elseif messageComponents ~= nil then plainText = tostring(messageComponents)
  51.     else plainText = "nil_message_components_to_sendFormattedChat" end
  52.    
  53.     if not chatBox then -- Check if chatBox was found at startup
  54.         botLog("[NoChat] " .. plainText);
  55.         return false
  56.     end
  57.    
  58.     local jsonMessage_success, jsonMessage = pcall(textutils.serialiseJSON, messageComponents)
  59.     if not jsonMessage_success then
  60.         botLog("!!! textutils.serialiseJSON FAILED: " .. tostring(jsonMessage) .. " -- Original: " .. textutils.serialize(messageComponents,{compact=true,max_depth=2}))
  61.         print("ERROR: Could not serialize chat message!"); return false
  62.     end
  63.  
  64.     -- Use an anonymous function for the pcall to ensure 'self' is handled correctly by the chatBox method
  65.     local peripheral_call_success, peripheral_error = pcall(function()
  66.         -- Call using the documented signature for Advanced Peripherals' sendFormattedMessage
  67.         return chatBox.sendFormattedMessage(jsonMessage, CHAT_BOT_NAME, CHAT_BOT_BRACKETS, CHAT_BOT_BRACKET_COLOR)
  68.     end)
  69.    
  70.     if not peripheral_call_success then
  71.         botLog("!!! chatBox.sendFormattedMessage FAILED: " .. tostring(peripheral_error) .. " -- JSON was: " .. jsonMessage)
  72.         print("ERROR: Failed to send formatted message via chatBox: " .. tostring(peripheral_error))
  73.         -- Consider if chatBoxFunctional should be set to false here if this fails repeatedly
  74.         return false
  75.     end
  76.     return true
  77. end
  78.  
  79. local function announce(messageComponents)
  80.     -- Simpler announce: just calls sendFormattedChat and adds a fixed small delay.
  81.     local success = sendFormattedChat(messageComponents)
  82.     if not success then
  83.         botLog("Announce: sendFormattedChat failed.")
  84.     end
  85.     sleep(0.3) -- A slightly longer default delay than 0.2 to be safe with AP chatbox.
  86.     return success
  87. end
  88.  
  89. local COLORS={GOLD="gold",AQUA="aqua",GRAY="gray",RED="red",GREEN="green",YELLOW="yellow",WHITE="white",DARK_RED="dark_red",DARK_GRAY="dark_gray"}
  90. --#endregion
  91.  
  92. --#region DefragBot Command Handlers
  93. local commandHandlers = {}
  94.  
  95. commandHandlers.help = function(u,_)
  96.     announce({{text="--- DefragBot Cmds ("..COMMAND_PREFIX..") ---",c=COLORS.GOLD,b=true}})
  97.     announce({{text=COMMAND_PREFIX.." help",c=COLORS.AQUA},{text=" - Help.",c=COLORS.GRAY}})
  98.     announce({{text=COMMAND_PREFIX.." cmd <ml.func()>",c=COLORS.AQUA},{text=" - Move func.",c=COLORS.GRAY}})
  99.     announce({{text=COMMAND_PREFIX.." mult <seq>",c=COLORS.AQUA},{text=" - Seq l,r,f,b,u,d,a.",c=COLORS.GRAY}})
  100.     announce({{text=COMMAND_PREFIX.." pos",c=COLORS.AQUA},{text=" - Pos/dir.",c=COLORS.GRAY}})
  101.     announce({{text=COMMAND_PREFIX.." fuel",c=COLORS.AQUA},{text=" - Fuel.",c=COLORS.GRAY}})
  102.     announce({{text=COMMAND_PREFIX.." unstuck",c=COLORS.AQUA},{text=" - Warp home, reset pos.",c=COLORS.GRAY}})
  103.     announce({{text=COMMAND_PREFIX.." startdefrag",c=COLORS.AQUA},{text=" - Start ME Drive defrag.",c=COLORS.GRAY}})
  104.     announce({{text=COMMAND_PREFIX.." stopdefrag",c=COLORS.AQUA},{text=" - Stop defrag.",c=COLORS.GRAY}})
  105. end
  106. commandHandlers.cmd = function(u,a)
  107.     if #a==0 then announce({{text="Usage: "..COMMAND_PREFIX.." cmd <ml.func()>",c=COLORS.YELLOW}});return end
  108.     local cs=table.concat(a," ");botLog("Usr "..u.." exec: "..cs)
  109.     local fn,e=load("return "..cs,"ucmd","t",{ml=ml})
  110.     if not fn then announce({{text="Err parsing: ",c=COLORS.RED},{text=tostring(e),c=COLORS.YELLOW}});botLog("Parse err: "..tostring(e));return end
  111.     local s,r=pcall(fn)
  112.     if s then announce({{text="Cmd exec. Res: ",c=COLORS.GREEN},{text=tostring(r),c=COLORS.WHITE}});botLog("Cmd res: "..tostring(r))
  113.     else announce({{text="Err exec: ",c=COLORS.RED},{text=tostring(r),c=COLORS.YELLOW}});botLog("Exec err: "..tostring(r))end
  114. end
  115. commandHandlers.mult = function(u,a)
  116.     if #a~=1 or type(a[1])~="string"then announce({{text="Usage: "..COMMAND_PREFIX.." mult <seq>",c=COLORS.YELLOW}});announce({{text="Seq: lrfbuda. Ex: lffr",c=COLORS.GRAY}});return end
  117.     local sq=string.lower(a[1]);botLog("Usr "..u.." mult: "..sq);announce({{text="Exec seq: ",c=COLORS.AQUA},{text=sq,c=COLORS.WHITE}});local sc=true
  118.     for i=1,#sq do
  119.         local mc=string.sub(sq,i,i);local mf=nil
  120.         if mc=="l"then mf=ml.l elseif mc=="r"then mf=ml.r elseif mc=="f"then mf=ml.f elseif mc=="b"then mf=ml.b
  121.         elseif mc=="u"then mf=ml.u elseif mc=="d"then mf=ml.d elseif mc=="a"then mf=ml.a end
  122.         if mf then
  123.             announce({{text="Exec: ",c=COLORS.GRAY},{text=mc,c=COLORS.YELLOW}})
  124.             local ms=mf()
  125.             if not ms then
  126.                 announce({{text="Move '",c=COLORS.RED},{text=mc,c=COLORS.YELLOW},{text="' fail. Stop.",c=COLORS.RED}})
  127.                 if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 and not fuelWarningSent then
  128.                     announce({{text="CRIT: Fuel low ("..turtle.getFuelLevel()..")! Unstuck?",c=COLORS.DARK_RED,b=true}});fuelWarningSent=true
  129.                 end
  130.                 sc=false;break
  131.             end
  132.         else announce({{text="Unk char '",c=COLORS.RED},{text=mc,c=COLORS.YELLOW},{text="' in seq. Stop.",c=COLORS.RED}});sc=false;break end
  133.     end
  134.     if sc then announce({{text="Seq finish.",c=COLORS.GREEN}})end
  135. end
  136. commandHandlers.pos = function(u,_)
  137.     local pD=ml.getPosition();local dNS=ml.getDirectionName()
  138.     if pD and pD.x~=nil then announce({{text="Pos: ",c=COLORS.AQUA},{text="X:"..tostring(pD.x).." Y:"..tostring(pD.y).." Z:"..tostring(pD.z),c=COLORS.WHITE},{text=" Facing: "..dNS,c=COLORS.WHITE}})
  139.     else announce({{text="Pos unknown.",c=COLORS.YELLOW}})end
  140. end
  141. commandHandlers.fuel = function(u,_)
  142.     local l,lm=turtle.getFuelLevel(),turtle.getFuelLimit()
  143.     announce({{text="Fuel: ",c=COLORS.AQUA},{text=tostring(l)..(lm>0 and(" / "..tostring(lm))or""),c=COLORS.WHITE}})
  144. end
  145. commandHandlers.unstuck = function(u,_)
  146.     announce({{text="Unstuck: warp home...",c=COLORS.YELLOW}});botLog(u.." unstuck.")
  147.     isDefragRunning=false
  148.     if ml.h()then announce({{text="Warp home & reset pos OK.",c=COLORS.GREEN}})
  149.     else announce({{text="Fail warp home. Manual help?",c=COLORS.RED}})end
  150. end
  151. --#endregion
  152.  
  153. --#region DefragBot Core Logic
  154. local function handleMoveFailure(actionDescription, criticalMoveFailed)
  155.     botLog("!!! INSIDE handleMoveFailure for: " .. actionDescription)
  156.     announce({{text="CRIT FAIL: Failed to " .. actionDescription .. ". ABORT DEFRAG.", c=COLORS.DARK_RED, b=true}})
  157.     if criticalMoveFailed and turtle.getFuelLevel() < 10 and turtle.getFuelLimit() ~= 0 and not fuelWarningSent then
  158.         announce({{text="HELP: Out of fuel ("..turtle.getFuelLevel()..") during critical move! Refuel & '@defrag unstuck'.", c=COLORS.RED, b=true}})
  159.         fuelWarningSent = true
  160.     end
  161.     isDefragRunning = false
  162.     botLog("!!! handleMoveFailure set isDefragRunning to false.")
  163. end
  164.  
  165. local function performDefragCycle()
  166.     botLog(">>> performDefragCycle STARTING. isDefragRunning should be true.")
  167.     isDefragRunning = true; fuelWarningSent = false
  168.    
  169.     local announce_success_initial, announce_error_initial = pcall(announce, {{text="Starting ME Drive Defragmentation Cycle...",c=COLORS.GOLD,b=true}})
  170.     if not announce_success_initial then botLog("!!! Initial announce in performDefragCycle FAILED: " .. tostring(announce_error_initial)) end
  171.    
  172.     botLog(">>> Current pos: X:"..tostring(current_pos.x).." Y:"..tostring(current_pos.y).." Z:"..tostring(current_pos.z).." Dir:"..(current_dir and DIR_NAMES[current_dir]or"Unk"))
  173.     botLog(">>> Target home: X:"..LOCATIONS.home.x.." Y:"..LOCATIONS.home.y.." Z:"..LOCATIONS.home.z.." Dir:"..LOCATIONS.home.dir)
  174.    
  175.     announce({{text="Stage 1: Initial setup...",c=COLORS.AQUA}})
  176.     if not ml.moveTo(LOCATIONS.home.x,LOCATIONS.home.y,LOCATIONS.home.z,LOCATIONS.home.dir)then botLog(">>> moveTo home FAILED.");handleMoveFailure("move to home",true);botLog(">>> pDC RETURN from home fail.");return end
  177.     botLog(">>> moveTo home OK.");ml.refuel()
  178.    
  179.     local initInvItems={};for i=1,15 do if turtle.getItemCount(i)>0 then local d=turtle.getItemDetail(i);table.insert(initInvItems,{slot=i,count=turtle.getItemCount(i),name=(d and d.name or "Unk")})end end
  180.     if #initInvItems>0 then
  181.         announce({{text="Pre-existing items found. Placing...",c=COLORS.YELLOW}});local allPlaced=true
  182.         for itemIdx=#initInvItems,1,-1 do local itemD=initInvItems[itemIdx];announce({{text="Placing "..itemD.count.."x "..itemD.name,c=COLORS.GRAY}});local curPlaced=false
  183.             for _,driveL in ipairs(ALL_ME_DRIVE_LOCATIONS)do local skipDrive=false
  184.                 if not isDefragRunning then announce({{text="Defrag stopped.",c=COLORS.YELLOW}});return end
  185.                 if turtle.getItemCount(itemD.slot)==0 then curPlaced=true;break end
  186.                 if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to ME Drive low fuel",true);return end
  187.                 if not ml.moveTo(driveL.x,driveL.y,driveL.z,driveL.dir)then announce({{text="Fail move to ME Drive "..driveL.name..". Skip.",c=COLORS.RED}});skipDrive=true end
  188.                 if not skipDrive then turtle.select(itemD.slot);local dropAtt=turtle.getItemCount(itemD.slot)
  189.                     for _=1,dropAtt do if not turtle.drop()then moveLog("Fail drop itemD to "..driveL.name);break end;moveLog("Placed 1x "..itemD.name);if turtle.getItemCount(itemD.slot)==0 then curPlaced=true;break end;sleep(0.2)end
  190.                     if curPlaced then break end
  191.                 end
  192.             end
  193.             if not curPlaced then announce({{text="Could not place all "..itemD.name,c=COLORS.RED}});allPlaced=false end
  194.         end
  195.         if not allPlaced then announce({{text="WARN: Some pre-existing items not placed.",c=COLORS.YELLOW}})else announce({{text="All pre-existing items placed.",c=COLORS.GREEN}})end
  196.     end
  197.  
  198.     for _,locKey in ipairs({"io_port","hopper","chest_temp_storage"})do local skipClear=false
  199.         if not isDefragRunning then announce({{text="Defrag stopped.",c=COLORS.YELLOW}});return end;local loc=LOCATIONS[locKey];announce({{text="Clearing "..loc.name.."...",c=COLORS.GRAY}})
  200.         if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to "..loc.name.." low fuel",true);return end
  201.         if not ml.moveTo(loc.x,loc.y,loc.z,loc.dir)then announce({{text="Fail move to "..loc.name..". Skip.",c=COLORS.RED}});skipClear=true end
  202.         if not skipClear then local suckedC=0;for _=1,16*64 do if turtle.suck()then suckedC=suckedC+1 else break end end
  203.             if suckedC>0 then announce({{text="Sucked "..suckedC.." from "..loc.name,c=COLORS.AQUA}})
  204.                 if locKey~="chest_temp_storage"then local tempL=LOCATIONS.chest_temp_storage
  205.                     if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to temp chest low fuel",true);return end
  206.                     if not ml.moveTo(tempL.x,tempL.y,tempL.z,tempL.dir)then announce({{text="Fail move to temp chest. Items remain.",c=COLORS.RED}})else
  207.                         for slot=1,15 do if turtle.getItemCount(slot)>0 then turtle.select(slot);turtle.drop()end end;announce({{text="Moved items from "..loc.name.." to temp chest.",c=COLORS.GRAY}})
  208.                     end
  209.                 end
  210.             end
  211.         end
  212.     end
  213.     announce({{text="Stage 2: Processing ME Drive Bays...",c=COLORS.AQUA}})
  214.     for i,driveL in ipairs(ALL_ME_DRIVE_LOCATIONS)do local skipDriveLoop=false
  215.         if not isDefragRunning then announce({{text="Defrag stopped.",c=COLORS.YELLOW}});return end
  216.         announce({{text="Processing ME Drive "..i.."/"..#ALL_ME_DRIVE_LOCATIONS.." ("..driveL.name..")",c=COLORS.GOLD}})
  217.         if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to ME Drive "..driveL.name.." (low fuel)",true);return end
  218.         if not ml.moveTo(driveL.x,driveL.y,driveL.z,driveL.dir)then announce({{text="Fail move to ME Drive. Skip.",c=COLORS.RED}});skipDriveLoop=true end
  219.        
  220.         -- VVVVVV MODIFIED SECTION FOR NON-STACKABLE DISK COLLECTION VVVVVV
  221.         if not skipDriveLoop then
  222.             local disksToProc={} -- Stores {slot=turtle_slot_num, name="disk_name", count=1 for non-stackable}
  223.             announce({{text="Retrieving disks from ME Drive "..driveL.name.."...",c=COLORS.GRAY}})
  224.  
  225.             -- New sucking logic for non-stackable disks:
  226.             -- Suck up to 15 disks (one for each non-fuel slot) or until ME Drive is empty / turtle full.
  227.             local successfullySuckedCount = 0
  228.             for suck_idx = 1, 15 do -- turtle.suck() pulls ONE item into AN available slot.
  229.                 if not turtle.suck() then
  230.                     botLog("turtle.suck() from ME Drive returned false. Attempt: " .. suck_idx .. ". ME Drive might be empty or turtle inventory full.")
  231.                     break -- Stop if ME drive empty or turtle inventory full
  232.                 else
  233.                     successfullySuckedCount = successfullySuckedCount + 1
  234.                     botLog("turtle.suck() from ME Drive successful. Total items sucked in this batch so far: " .. successfullySuckedCount)
  235.                     sleep(0.1) -- Small delay between sucks, can be adjusted or removed
  236.                 end
  237.             end
  238.             botLog("Finished sucking attempt from ME Drive "..driveL.name..". Succeeded " .. successfullySuckedCount .. " times.")
  239.  
  240.             -- Inventory turtle to build disksToProc table
  241.             for slot = 1, 15 do -- Check all usable turtle slots (1-15, 16 is fuel)
  242.                 if turtle.getItemCount(slot) > 0 then
  243.                     local itemDetail = turtle.getItemDetail(slot)
  244.                     local itemName = (itemDetail and itemDetail.name or "Unknown Item")
  245.                     local itemCount = turtle.getItemCount(slot) -- Should be 1 for non-stackable disks
  246.                    
  247.                     -- Optional: If ME drives might contain non-disk items you don't want to process,
  248.                     -- you could add a filter here, e.g.:
  249.                     -- if itemDetail and itemDetail.name and string.find(string.lower(itemDetail.name), "disk") then
  250.                     --     table.insert(disksToProc, {slot = slot, name = itemName, count = itemCount})
  251.                     --     botLog("Inventoried DISK: Slot " .. slot .. " has " .. itemCount .. "x " .. itemName)
  252.                     -- else
  253.                     --     botLog("Inventoried OTHER ITEM (not processed): Slot " .. slot .. " has " .. itemCount .. "x " .. itemName)
  254.                     -- end
  255.                     table.insert(disksToProc, {slot = slot, name = itemName, count = itemCount})
  256.                     botLog("Inventoried from ME Drive: Slot " .. slot .. " has " .. itemCount .. "x " .. itemName)
  257.                 end
  258.             end
  259.            
  260.             if #disksToProc == 0 then
  261.                 announce({{text="No disks found/retrieved in this ME Drive.",c=COLORS.YELLOW}})
  262.                 skipDriveLoop = true -- No need to proceed to chest/IO port for this drive
  263.             end
  264.             -- ^^^^^^ END OF MODIFIED SECTION FOR NON-STACKABLE DISK COLLECTION ^^^^^^
  265.  
  266.             if not skipDriveLoop then
  267.                 local totalDisks=0; for _,d in ipairs(disksToProc) do totalDisks=totalDisks+d.count end
  268.                 announce({{text="Collected "..totalDisks.." disk(s) total from ME Drive.",c=COLORS.GREEN}})
  269.                
  270.                 local chestL=LOCATIONS.chest_temp_storage
  271.                 if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to temp chest low fuel",true);return end
  272.                 if not ml.moveTo(chestL.x,chestL.y,chestL.z,chestL.dir)then handleMoveFailure("move to temp chest",false);return end
  273.                
  274.                 announce({{text="Dropping "..totalDisks.." disk(s) to chest...",c=COLORS.GRAY}})
  275.                 for _,diskD in ipairs(disksToProc) do
  276.                     turtle.select(diskD.slot)
  277.                     if not turtle.drop() then -- turtle.drop() drops the entire stack from selected slot (which is 1 disk)
  278.                         announce({{text="Fail drop disk '",c=COLORS.RED},{text=diskD.name,c=COLORS.YELLOW},{text="' from slot "..diskD.slot,c=COLORS.RED}})
  279.                         botLog("Failed to drop disk " .. diskD.name .. " from slot " .. diskD.slot .. " into chest.")
  280.                     else
  281.                         botLog("Dropped disk " .. diskD.name .. " from slot " .. diskD.slot .. " into chest.")
  282.                     end
  283.                 end
  284.                
  285.                 local ioL=LOCATIONS.io_port
  286.                 if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move to IO port low fuel",true);return end
  287.                 if not ml.moveTo(ioL.x,ioL.y,ioL.z,ioL.dir)then handleMoveFailure("move to IO port",false);return end
  288.                
  289.                 announce({{text="Waiting for "..totalDisks.." processed disk(s)...",c=COLORS.GRAY}});local procRet=0;local noDiskTries=0;local MAX_NO_DISK_TRIES=20
  290.                 while procRet<totalDisks and noDiskTries<MAX_NO_DISK_TRIES do
  291.                     if not isDefragRunning then announce({{text="Defrag stopped.",c=COLORS.YELLOW}});return end;local retrievedThis=false
  292.                     for slot=1,15 do if turtle.getItemCount(slot)==0 then turtle.select(slot)
  293.                         if turtle.suck()then procRet=procRet+1;announce({{text="Retrieved proc disk "..procRet.."/"..totalDisks,c=COLORS.AQUA}});noDiskTries=0;retrievedThis=true;break end
  294.                     end end
  295.                     if procRet>=totalDisks then break end
  296.                     if not retrievedThis then noDiskTries=noDiskTries+1;announce({{text="Waiting... (No disk try "..noDiskTries.."/"..MAX_NO_DISK_TRIES..")",c=COLORS.DARK_GRAY}})end;sleep(10)
  297.                 end
  298.  
  299.                 if procRet<totalDisks then announce({{text="Timeout: Retrieved "..procRet.."/"..totalDisks..". Proceeding.",c=COLORS.RED}})end
  300.                 if procRet==0 and totalDisks>0 then announce({{text="No disks from IO Port. Skip return.",c=COLORS.RED}});skipDriveLoop=true end
  301.                
  302.                 if not skipDriveLoop then
  303.                     if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move home refuel low fuel",true);return end
  304.                     if not ml.moveTo(LOCATIONS.home.x,LOCATIONS.home.y,LOCATIONS.home.z,LOCATIONS.home.dir)then announce({{text="Fail move home for refuel.",c=COLORS.RED}})end
  305.                     ml.refuel()
  306.                     if turtle.getFuelLevel()<10 and turtle.getFuelLimit()~=0 then handleMoveFailure("move back ME Drive low fuel",true);return end
  307.                     if not ml.moveTo(driveL.x,driveL.y,driveL.z,driveL.dir)then announce({{text="Fail return to ME Drive. Disks remain.",c=COLORS.RED}});skipDriveLoop=true end
  308.                    
  309.                     if not skipDriveLoop then announce({{text="Returning "..procRet.." processed disk(s)...",c=COLORS.GRAY}})
  310.                         for slot=1,15 do if turtle.getItemCount(slot)>0 then turtle.select(slot);local countIS=turtle.getItemCount(slot);local droppedC=0
  311.                             for _=1,countIS do if turtle.drop()then droppedC=droppedC+1 else break end end
  312.                             if droppedC>0 then announce({{text="Returned "..droppedC.." disk(s) from slot "..slot,c=COLORS.GREEN}})end
  313.                             if droppedC<countIS then announce({{text="Failed return all from slot "..slot,c=COLORS.RED}})end
  314.                         end end;announce({{text="Finished ME Drive "..driveL.name,c=COLORS.GOLD}})
  315.                     end
  316.                 end
  317.             end
  318.         end;sleep(1)
  319.     end
  320.     announce({{text="Defragmentation Cycle Complete!",c=COLORS.GOLD,b=true}});isDefragRunning=false
  321. end
  322.  
  323. commandHandlers.startdefrag=function(u,_)
  324.     botLog(">>> startdefrag command called. Current isDefragRunning: " .. tostring(isDefragRunning))
  325.     if isDefragRunning then announce({{text="Defrag already running.",c=COLORS.YELLOW}}); botLog(">>> startdefrag: Already running, exiting."); return end;
  326.     botLog(u.." started defrag.")
  327.     performDefragCycle() -- Run directly in the main thread
  328.     botLog(">>> startdefrag: performDefragCycle completed or errored.")
  329. end
  330. commandHandlers.stopdefrag=function(u,_)
  331.     if isDefragRunning then
  332.         announce({{text="Attempting to signal stop to defrag process...",c=COLORS.YELLOW}})
  333.         isDefragRunning=false; -- Set the flag
  334.         botLog(u.." requested stop. isDefragRunning set to false.")
  335.         -- Note: Since performDefragCycle is now in main thread, this flag only stops it at its next check.
  336.         -- A true immediate stop would require more complex inter-thread communication or event handling within loops.
  337.     else
  338.         announce({{text="Defrag not running.",c=COLORS.GRAY}})
  339.     end
  340. end
  341. --#endregion
  342.  
  343. --#region DefragBot Main Loop
  344. local function run()
  345.     if DEBUG_LOGGING_ENABLED then
  346.         local file, err = fs.open(LOG_FILE_NAME, "w")
  347.         if file then file.write(string.format("[%s] %s Log Initialized.\n", os.date("%Y-%m-%d %H:%M:%S"), CHAT_BOT_NAME)); file.write("============================================================\n"); file.close()
  348.         else print("LOGGING ERROR: Could not initialize "..LOG_FILE_NAME..": "..tostring(err)) end
  349.     end
  350.     term.clear();term.setCursorPos(1,1);
  351.    
  352.     chatBox = peripheral.find(CHAT_BOX_PERIPHERAL_NAME)
  353.     botLog("DefragBot initializing...")
  354.     botLog("Initial ChatBox peripheral check: " .. tostring(chatBox))
  355.     -- Determine chatBoxFunctional based on direct method check
  356.     if chatBox and chatBox.sendFormattedMessage and chatBox.sendFormattedMessageToPlayer then
  357.         chatBoxFunctional = true
  358.         botLog("ChatBox determined to be FUNCTIONAL (key methods exist via direct check).")
  359.     else
  360.         if chatBox then
  361.             botLog("ChatBox found, but MISSING ESSENTIAL sendFormattedMessage(ToPlayer) methods. Marking as non-functional.")
  362.         else
  363.             botLog("ChatBox peripheral IS NIL at startup. Marking as non-functional.")
  364.         end
  365.         chatBoxFunctional = false
  366.     end
  367.    
  368.     botLog(CHAT_BOT_NAME.." online. MoveLib init complete.")
  369.     print(CHAT_BOT_NAME.." online. '"..COMMAND_PREFIX.." help' or '@all'.")
  370.    
  371.     local initial_announce_success = announce({{text=CHAT_BOT_NAME.." online, defrag duties (v1.8.1)!",c=COLORS.GREEN}})
  372.     if not initial_announce_success then
  373.         botLog("Initial online announcement FAILED. Chat will be console/log only if ChatBox is non-functional.")
  374.     end
  375.    
  376.     while true do
  377.         -- If defrag is running in main thread, this loop won't process events until it finishes.
  378.         -- This is the trade-off for easier debugging of performDefragCycle.
  379.         local eventData={os.pullEvent()}
  380.         local eventType=eventData[1]
  381.  
  382.         if not isDefragRunning then -- Only process chat if defrag isn't blocking
  383.             if eventType=="chat"then local eU,eM=eventData[2],eventData[3]
  384.                 if eM then if string.lower(eM)=="@all"then botLog("@all from "..eU);announce({{text="Use '",c=COLORS.GREEN},{text=COMMAND_PREFIX.." help",c=COLORS.AQUA},{text="' for my commands.",c=COLORS.GREEN}})
  385.                     elseif string.sub(eM,1,#COMMAND_PREFIX)==COMMAND_PREFIX then botLog("Chat cmd from "..eU..": "..eM);local p={};for pt in string.gmatch(eM,"[^%s]+")do table.insert(p,pt)end;local cn="";if p[2]then cn=string.lower(p[2])end;local ca={};for i=3,#p do table.insert(ca,p[i])end
  386.                         if commandHandlers[cn]then commandHandlers[cn](eU,ca)elseif cn~=""then announce({{text="Unk Cmd: '",c=COLORS.RED},{text=cn,c=COLORS.YELLOW},{text="'.",c=COLORS.RED}})end
  387.                     end
  388.                 end
  389.             elseif eventType=="terminate"then
  390.                 botLog("Terminate. Shutdown.");isDefragRunning=false;
  391.                 if chatBoxFunctional then announce({{text=CHAT_BOT_NAME.." shut down.",c=COLORS.YELLOW}}) end;
  392.                 return
  393.             end
  394.         elseif eventType == "terminate" then -- Still handle terminate if defrag is running
  395.             botLog("Terminate received during defrag. Shutting down.");isDefragRunning=false;
  396.             if chatBoxFunctional then announce({{text=CHAT_BOT_NAME.." shut down (during defrag).",c=COLORS.YELLOW}}) end;
  397.             return
  398.         else
  399.             -- If defrag is running, we might just ignore other events or log them
  400.             botLog("Event received during defrag (ignored by main loop): " .. eventType)
  401.         end
  402.     end
  403. end
  404. run()
  405. --#endregion
Add Comment
Please, Sign In to add comment