local url = "http://justus.l--n.de:3000/JUFS/cc_housebuild/raw/branch/main/src/main.lua" if not _G["turtle"] then error("This program only runs on turtles!") end local turtle = _G["turtle"] print("House building system") print("JUFS Technologies (c) 2026 (Justus Wolff)") print("fuel: "..tostring(turtle.getFuelLevel()).."/"..tostring(turtle.getFuelLimit())) os.sleep(0.1) function term.setcol(fc, bc) term.setTextColor(fc) term.setBackgroundColor(bc) end local function reset() term.setcol(colors.white, colors.black) term.clear() term.setCursorPos(1,1) end local function incline() local _,y = term.getCursorPos() term.setCursorPos(1,y+1) end local function selopt(options, title) local selected = 1 while true do reset() term.setcol(colors.black, colors.white) term.write(title) for i,v in pairs(options) do incline() if i == selected then term.setcol(colors.black, colors.blue) else term.setcol(colors.white, colors.black) end term.write(v) end local event,key = os.pullEvent("key") if keys.getName(key) == "w" and selected ~= 1 then selected = selected - 1 end if keys.getName(key) == "s" and selected ~= #options then selected = selected + 1 end if keys.getName(key) == "enter" then return selected end end end local function integritycheck() if not fs.exists("designs") then fs.makeDir("designs") end end local function posasstring(...) local positions = {...} local out = "" for _,v in pairs(positions) do out = out .. tostring(v) .. ":" end return out end local function load(name) local file = fs.open("designs/"..name, "r") local content = file.readAll() file.close() content = textutils.unserialiseJSON(content) return content["buf"],content["dimensions"] end local function bufistype(buf, x,y,z, targettype) if buf[posasstring(x,y,z)] == targettype then return true else return targettype == 0 and buf[posasstring(x,y,z)] == nil end end local function fill_getneighbors(x,y,z, sx,sz, seltype,buf) local out = {} if x ~= sx and bufistype(buf, x+1,y,z, seltype) then table.insert(out, {x+1,z}) end if x ~= 1 and bufistype(buf, x-1,y,z, seltype) then table.insert(out, {x-1,z}) end if z ~= sz and bufistype(buf, x,y,z+1, seltype) then table.insert(out, {x,z+1}) end if z ~= 1 and bufistype(buf, x,y,z-1, seltype) then table.insert(out, {x,z-1}) end return out end local function fill(ntype, x,y,z, buf, sx,sz) local inittype = buf[posasstring(x,y,z)] if inittype == ntype then return end -- no work needs to be done local neighbors = fill_getneighbors(x,y,z, sx,sz, inittype,buf) buf[posasstring(x,y,z)] = ntype local processed = 0 local totalneighbors = #neighbors term.setcol(colors.white, colors.black) while #neighbors > 0 do term.setCursorPos(1,1) term.write(tostring(processed).."/"..tostring(totalneighbors)) local cn = table.remove(neighbors, 1) local cx,cz = cn[1],cn[2] local newneigh = fill_getneighbors(cx,y,cz, sx,sz, inittype,buf) for _,v in pairs(newneigh) do table.insert(neighbors, v) buf[posasstring(v[1],y,v[2])] = ntype totalneighbors = totalneighbors + 1 end buf[posasstring(cx,y,cz)] = ntype processed = processed + 1 end end local function newdesign() local buf = {} local dimensions = { x = 8, z = 8, y = 1 } local currentfloor = 1 local camx,camy = 0,0 local currentblock = 2 local blockindex = 1 local blocks = { 2, -- no ceiling 3, -- doorway 4, -- glass } local function renderblock(index) local block = blocks[index] if block == 2 then -- no ceiling term.blit("C", colors.toBlit(colors.black), colors.toBlit(colors.red)) end if block == 3 then -- doorway term.blit("D", colors.toBlit(colors.black), colors.toBlit(colors.brown)) end if block == 4 then -- glass term.blit("W", colors.toBlit(colors.black), colors.toBlit(colors.white)) end end local shift = false while true do -- render buf reset() for x=1+camx,dimensions["x"]+camx,1 do for z=1+camy,dimensions["z"]+camy,1 do local currentbuf = buf[posasstring(x-camx, currentfloor, z-camy)] term.setCursorPos(x, z) if currentbuf == 1 then -- wall term.blit(" ", colors.toBlit(colors.white), colors.toBlit(colors.white)) elseif currentbuf and currentbuf > 1 then -- blocks renderblock(currentbuf-1) --term.blit("C", colors.toBlit(colors.black), colors.toBlit(colors.red)) elseif currentbuf == 0 or currentbuf == nil then -- nothing if buf[posasstring(x-camx, currentfloor-1, z-camy)] == 2 then term.blit("\127", colors.toBlit(colors.red), colors.toBlit(colors.black)) else term.blit("\127", colors.toBlit(colors.gray), colors.toBlit(colors.black)) end end end end term.setcol(colors.yellow, colors.black) local _,y = term.getSize() term.setCursorPos(1,y-1) term.write("Press Ctrl for menu. ") term.setCursorPos(1,y) term.write("x: "..tostring(camx)) term.write(" z: "..tostring(camy)) term.write(" floor: "..tostring(currentfloor)) term.write(" Block: ") renderblock(blockindex) -- user input local event = table.pack(os.pullEvent()) if event[1] == "mouse_click" or event[1] == "mouse_drag" then event[3] = event[3]-camx event[4] = event[4]-camy if event[3] <= dimensions["x"] and event[4] <= dimensions["z"] then if event[2] == 1 then -- left button, set if shift then fill(1, event[3],currentfloor,event[4], buf,dimensions["x"],dimensions["z"]) end buf[posasstring(event[3], currentfloor, event[4])] = 1 end if event[2] == 3 then -- middle button, set special block if shift then fill(currentblock, event[3],currentfloor,event[4], buf,dimensions["x"],dimensions["z"]) end buf[posasstring(event[3], currentfloor, event[4])] = currentblock end if event[2] == 2 then -- right button, erase if shift then fill(0, event[3],currentfloor,event[4], buf,dimensions["x"],dimensions["z"]) end buf[posasstring(event[3], currentfloor, event[4])] = 0 end end end if event[1] == "mouse_scroll" then if event[2] == 1 then -- down if blockindex == #blocks then blockindex = 1 else blockindex = blockindex + 1 end end if event[2] == -1 then -- up if blockindex == 1 then blockindex = #blocks else blockindex = blockindex - 1 end end currentblock = blocks[blockindex] end if event[1] == "key" then if keys.getName(event[2]) == "leftShift" then shift = true end if keys.getName(event[2]) == "leftCtrl" then -- menu local action = selopt({ "Save", "Load", "Exit", "Change X size", "Change floor amount", "Change Z size" }, "menu") if action == 3 then return end -- exit if action == 1 then -- save reset() for _,v in pairs(fs.list("designs")) do term.write(v) incline() end write("Enter name: ") local name = read() local file = fs.open("designs/"..name, "w") file.write(textutils.serialiseJSON({ ["dimensions"] = dimensions, ["buf"] = buf })) file.close() end if action == 2 then -- load reset() for _,v in pairs(fs.list("designs")) do term.write(v) incline() end write("Enter name: ") local name = read() if not fs.exists("designs/"..name) or name == "" then printError("Design not found!") else buf,dimensions = load(name) camx,camy,currentfloor = 0,0,1 end end if action == 4 then -- change X size reset() write("Enter new X size: ") local xsize = tonumber(read()) dimensions["x"] = xsize end if action == 5 then -- change floor amount reset() write("Enter new floor amount: ") local ysize = tonumber(read()) dimensions["y"] = ysize end if action == 6 then -- change Z size reset() write("Enter new Z size: ") local zsize = tonumber(read()) dimensions["z"] = zsize end end if keys.getName(event[2]) == "q" and currentfloor < dimensions["y"] then -- go up currentfloor = currentfloor + 1 end if keys.getName(event[2]) == "e" and currentfloor > 1 then -- go down currentfloor = currentfloor - 1 end if keys.getName(event[2]) == "w" and camy < dimensions["z"] then -- pan up camy = camy + 1 end if keys.getName(event[2]) == "s" and camy > -dimensions["z"] then -- pan down camy = camy - 1 end if keys.getName(event[2]) == "a" and camx < dimensions["x"] then -- pan left camx = camx + 1 end if keys.getName(event[2]) == "d" and camx > -dimensions["z"] then -- pan right camx = camx - 1 end end if event[1] == "key_up" then if keys.getName(event[2]) == "leftShift" then shift = false end end end end local function move(direction, continousattempt) if direction == "left" then turtle.turnLeft() return end if direction == "right" then turtle.turnRight() return end while true do if turtle.getFuelLevel() == 0 then reset() print("Out of fuel! Please insert fuel into current slot.") while true do local suc = turtle.refuel(64) if suc then print("Refuelled. Press enter to continue.") read("") break end os.sleep(1) end end local suc,reason = turtle[direction]() if not suc and not continousattempt then printError(reason) print("Resolve the error and press enter to continue.") read("") elseif suc then break end end end local function place(direction) local func = turtle.place local cfunc = turtle.compare if direction == "down" then func = turtle.placeDown cfunc = turtle.compareDown end if direction == "up" then func = turtle.placeUp cfunc = turtle.compareUp end while true do local suc = func() if not suc then -- next slot if cfunc() and turtle.getItemDetail(turtle.getSelectedSlot()) ~= nil then return end local current = turtle.getSelectedSlot() if current == 16 then turtle.select(1) else turtle.select(current+1) end else break end os.sleep(0) -- yield end end local function placebuf(buf, x, y, z) if buf[posasstring(x,y,z)] == 1 then place("down") end end local function directiondist(direction, wanted) if direction == 0 then return 1 end if direction == 1 and wanted == -1 then return 2 end if direction == -1 and wanted == 1 then return 2 end if direction ~= wanted then return 1 end -- failsafes if direction == wanted then return 0 end end local function needstobeplaced(placecode, clayer) if clayer == 0 or clayer == 2 then return placecode == 1 end if clayer == 1 then return placecode == 1 or placecode == 4 end if clayer == 3 then return placecode == 1 or placecode == 4 or placecode == 3 end end local function getnearestunplaced(buf, pbuf, cx,cy,cz,clayer, sx,sz, direction) local distance = math.huge local selected = nil for x=1,sx,1 do for y=1,sz,1 do local extracost = 0 if x ~= cx and direction ~= 0 then extracost = extracost + 1 end if y > cz and direction ~= 1 then extracost = extracost + directiondist(direction, 1) end if y < cz and direction ~= -1 then extracost = extracost + directiondist(direction, -1) end local needplace = buf[posasstring(x,cy,y)] if pbuf[posasstring(x,cy,y)] then needplace = 0 end -- already placed needplace = needstobeplaced(needplace, clayer) if needplace then -- needs to be placed, calculate distance local cd = math.abs(y-cz)+math.abs(x-cx) -- raw distance (amount of blocks between) cd = cd + extracost if cd < distance then distance = cd selected = {x,y} end end end end return selected end local function center(direction, wanted) if direction == 1 and wanted ~= 1 then move("left") return true end if direction == -1 and wanted ~= -1 then move("right") return true end if direction == 0 then return true end return false end local function moveto(x,y,cx,cz,direction) while x ~= cx do if cx < x then -- x center(direction) direction = 0 cx = cx + 1 move("forward") end if cx > x then center(direction) direction = 0 cx = cx - 1 move("back") end end while y ~= cz do if y > cz then -- z if center(direction, 1) then move("right") end cz = cz + 1 move("forward") direction = 1 end if y < cz then if center(direction, -1) then move("left") end cz = cz - 1 move("forward") direction = -1 end end return cx,cz,direction end local function tcontains(x, y) for _,v in pairs(x) do if v == y then return true end end return false end local function render(buf, pbuf, cx,cy,cz, tx,tz, sx,sz, shouldsetlist,invertslist) shouldsetlist = shouldsetlist or { 1, } local tsx,tsy = term.getSize() local camx,camy = cx-(tsx/2),cz-(tsy/2) reset() for x=1,sx,1 do for z=1,sz,1 do local currentbuf = pbuf[posasstring(x, cy, z)] term.setCursorPos(x-camx, z-camy) if currentbuf then -- wall term.blit(" ", colors.toBlit(colors.white), colors.toBlit(colors.white)) elseif (currentbuf == false or currentbuf == nil) and (invertslist and not tcontains(shouldsetlist, buf[posasstring(x, cy, z)]) or tcontains(shouldsetlist, buf[posasstring(x, cy, z)])) then -- nothing term.blit("\127", colors.toBlit(colors.gray), colors.toBlit(colors.black)) end end end term.setCursorPos(cx-camx,cz-camy) term.blit(" ", colors.toBlit(colors.red), colors.toBlit(colors.red)) term.setCursorPos(tx-camx,tz-camy) term.blit(" ", colors.toBlit(colors.lime), colors.toBlit(colors.lime)) end local function printdes(buf, dimensions) move("up") move("forward") local cx = 1 local cz = 1 local direction = 0 for cy=1,dimensions["y"],1 do local setlists = { {1,4}, {1}, {1,3,4}, } for clayer=1,3,1 do -- build walls local pbuf = {} while true do local target = getnearestunplaced(buf, pbuf, cx,cy,cz,clayer, dimensions["x"],dimensions["z"], direction) if not target then break end render(buf, pbuf, cx,cy,cz, target[1],target[2], dimensions["x"],dimensions["z"], setlists[clayer]) --read("") cx,cz,direction = moveto(target[1],target[2],cx,cz,direction) place("down") pbuf[posasstring(cx,cy,cz)] = true end move("up") end -- build ceiling/floor local pbuf = {} local cbuf = {} for _cz=1,dimensions["z"],1 do for _cx=1,dimensions["x"],1 do if buf[posasstring(_cx,cy,_cz)] ~= 2 then cbuf[posasstring(_cx,0,_cz)] = 1 else cbuf[posasstring(_cx,0,_cz)] = 0 end end end while true do local target = getnearestunplaced(cbuf, pbuf, cx,0,cz,0, dimensions["x"],dimensions["z"], direction) if not target then break end render(cbuf, pbuf, cx,0,cz, target[1],target[2], dimensions["x"],dimensions["z"], {1}) cx,cz,direction = moveto(target[1],target[2],cx,cz,direction) place("down") pbuf[posasstring(cx,0,cz)] = true end -- return to standard pos but +1 to y cx,cz,direction = moveto(1,1,cx,cz,direction) move("up") end cx,cz,direction = moveto(0,1, cx,cz, direction) for _=1,dimensions["y"]*(4)+1,1 do -- go back to starting position move("down") end center(direction) end -- vertical stack printing local function VP_movetoy(y, target) while y > target do move("down", true) y = y - 1 end while y < target do move("up", true) y = y + 1 end return target end local function VP_moveto(x,z, cx,cz,direction) local y = 0 --[[ y lanes for directions: 1: +x 2: -x 3: +z 4: -z ]]-- while x ~= cx do if cx < x then -- x center(direction) direction = 0 y = VP_movetoy(y, 1) cx = cx + 1 move("forward", true) end if cx > x then center(direction) direction = 0 y = VP_movetoy(y, 2) cx = cx - 1 move("back", true) end end while z ~= cz do if z > cz then -- z if center(direction, 1) then move("right") end y = VP_movetoy(y, 3) cz = cz + 1 move("forward", true) direction = 1 end if z < cz then if center(direction, -1) then move("left") end y = VP_movetoy(y, 4) cz = cz - 1 move("forward", true) direction = -1 end end VP_movetoy(y, 0) return cx,cz,direction end local function VP_optimizestack(target) -- go from top to bottom and erase false entries until we encounter a true one. local index = #target while #target > 0 do if target[index] then break else table.remove(target, index) index = index - 1 end end return target end local function VP_createstack(buf, dimensions) local fout = { ["height"] = dimensions["y"]*(4)+1, ["dimensions"] = dimensions, } local out = {} for x=1,dimensions["x"],1 do for z=1,dimensions["z"],1 do local tempbuf = {} for y=1,dimensions["y"],1 do -- for each y dimension local placecode = buf[posasstring(x,y,z)] for layer=1,3,1 do -- layers table.insert(tempbuf, needstobeplaced(placecode, layer)) end -- ceiling table.insert(tempbuf, buf[posasstring(x,y,z)] ~= 2) end VP_optimizestack(tempbuf) if #tempbuf > 0 then out[posasstring(x,z)] = tempbuf end end end fout["stacks"] = out return fout end local function VP_lengthofdict(target) local out = 0 for _,v in pairs(target) do out = out + 1 end return out end local function VP_splitstack(_stack, x) local stack = _stack["stacks"] local dimensions = _stack["dimensions"] local sx = dimensions["x"] local sz = dimensions["z"] local height = _stack["height"] local stacks = {} local unevenstack = {} local stackmod = math.fmod(VP_lengthofdict(stack), x) local stackdiv = math.floor(VP_lengthofdict(stack)/x) if stackmod ~= 0 then for tempstackindex=0,stackmod,1 do unevenstack[posasstring(math.fmod(tempstackindex, sx),math.floor(tempstackindex/sx))] = stack[posasstring(math.fmod(tempstackindex, sx),math.floor(tempstackindex/sx))] end end if stackdiv ~= 0 then local eind = 0 local x2 = x if stackmod ~= 0 then x2 = x - 1 end for _=0,x2,1 do local buf = {} for stackind=eind,stackdiv+eind,1 do buf[posasstring(math.fmod(stackind, sx),math.floor(stackind/sx))] = stack[posasstring(math.fmod(stackind, sx),math.floor(stackind/sx))] end table.insert(stacks, buf) eind = eind + stackdiv end end local out = { height=height, stacks={} } if #unevenstack > 0 then table.insert(out["stacks"],unevenstack) end for _,v in pairs(stacks) do table.insert(out["stacks"],v) end return out end --[[local function VP_inspectsplitted(stacks, dimensions) -- I will probably never finish this. local currentselected = 1 while true do reset() -- render local _,y = term.getSize() term.setCursorPos(1,y) term.setcol(colors.yellow, colors.black) term.write("Currentlayer: ") term.write(tostring(currentselected)) local event = table.pack(os.pullEvent()) if event[1] == "key" then end end end]] local function VP_printstack(buf, dimensions, cx,cy,cz,direction) reset() move("up") for _=1,buf["height"]-cy,1 do move("up") end local ox,oz = cx,cz cx,cz,direction = VP_moveto(cx+1,cz, cx,cz,direction) for x=1,dimensions["x"],1 do for z=1,dimensions["z"],1 do local tempbuf = buf["stacks"][posasstring(x,z)] if tempbuf then cx,cz,direction = VP_moveto(x,z, cx,cz,direction) for _=1,buf["height"],1 do move("down") end for _,v in pairs(tempbuf) do if v then place("down") end move("up") end move("up") end end end VP_moveto(ox,oz, cx,cz,direction) for _=1,buf["height"],1 do move("down") end return cx,cz,direction end local function VP_calccost(stacks, height,sx,sz) -- calculate needed blocks local cost = 0 for cx=1,sx,1 do for cz=1,sz,1 do local tempbuf = stacks[posasstring(cx,cz)] if tempbuf then for ind=1,height,1 do if tempbuf[ind] then cost = cost + 1 end end end end end return cost end local function VP_selectpos(design,dimensions,msg, cx,cz) cx,cz = cx or 0, cz or 0 local camx,camz = cx,cz while true do render(design,{}, camx,1,camz, camx,camz, dimensions["x"],dimensions["z"], {1,4}) term.setCursorPos(1,1) term.write(msg) local _,y = term.getSize() term.setCursorPos(1,y) term.write("x: ") term.write(tostring(camx)) term.write(" z: ") term.write(tostring(camz)) local _,key = os.pullEvent("key") key = keys.getName(key) if key == "w" then camz = camz - 1 end if key == "s" then camz = camz + 1 end if key == "a" then camx = camx - 1 end if key == "d" then camx = camx + 1 end if key == "enter" then break end end return camx,camz end local function expwarn() reset() term.write("This mode is experimental! Be cautious!") incline() term.write("Enter to continue.") read("") end local function randomstr(length) local possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" local out = "" for _=1,length,1 do local ind = math.random(#possible) out = out .. string.sub(possible, ind, ind) end return out end while true do integritycheck() local action = selopt({ "New Design", "Print Design", "VPrint Design", "Join VPrint", "Update", }, "Select Action") if action == 1 then newdesign() end if action == 2 then reset() for _,v in pairs(fs.list("designs")) do term.write(v) incline() end write("Enter name: ") local name = read() if not fs.exists("designs/"..name) or name == "" then printError("Design not found!") else local buf,dimensions = load(name) write("Calculating fuel cost...") local cost = dimensions["x"]*dimensions["z"]*(dimensions["y"]*4-1) print(tostring(turtle.getFuelLevel()).."/"..tostring(cost)) if turtle.getFuelLevel() < cost then write("WARNING: Not enough fuel! Continue anyway? y/n: ") while true do local ans = read("") if ans == "y" then printdes(buf, dimensions) break elseif ans == "n" then break end end else printdes(buf, dimensions) end end end if action == 3 then expwarn() peripheral.find("modem", rednet.open) reset() for _,v in pairs(fs.list("designs")) do term.write(v) incline() end write("Enter name: ") local name = read() if not fs.exists("designs/"..name) or name == "" then printError("Design not found!") else local buf,dimensions = load(name) local stacks = VP_createstack(buf, dimensions) local height = stacks["height"] print("Stacks created.") local code = randomstr(5) print("Pair code: '"..code.."'") write("Enter amount of partaking turtles: ") local expam = tonumber(read()) write("Press enter to send pair request.") read("") rednet.broadcast({ code=code, }, "HB_vprint_pair") print("Sent. awaiting pair accepts.") local accepted = {} while #accepted < expam do local id,msg = rednet.receive("HB_vprint_pairespond") if msg["code"] == code then table.insert(accepted, id) reset() term.write(tostring(#accepted).."/"..tostring(expam)) end end reset() print("Paired. Splitting stacks and sending out...") stacks = VP_splitstack(stacks, #accepted+1) -- plus 1 because we also build as the master turtle. if #stacks["stacks"] ~= #accepted+1 then printError("Internal error with VP_splitstack. It returned "..#stacks["stacks"].." stacks.") return end for i,v in pairs(accepted) do reset() print("ID "..tostring(v).." needs "..VP_calccost(stacks["stacks"][i], stacks["height"],dimensions["x"],dimensions["z"]).." blocks.") print("Please ensure that turtle has that many blocks and press enter to continue.") read("") rednet.send(v, { stack=stacks["stacks"][i], dimensions=dimensions, height=height }, "HB_vprint_pairacknowledge") end print("This turtle needs "..tostring(VP_calccost(stacks["stacks"][expam+1], stacks["height"],dimensions["x"],dimensions["z"])).." blocks.") print("Please ensure that turtle has that many blocks and press enter to continue.") read("") local x,y = 0,0 for _,v in pairs(accepted) do x,y = VP_selectpos(buf,dimensions,"Select position of turtle "..tostring(v), x,y) rednet.send(v, { cx=x, cz=y, }, "HB_vprint_pairpossend") end reset() write("Ready to begin, press enter to continue") read("") print("Sending begin signal.") for _,v in pairs(accepted) do rednet.send(v, {}, "HB_vprint_begin") end local ownstack = { stacks=stacks["stacks"][expam+1], dimensions=dimensions, height=height } VP_printstack(ownstack,dimensions, 0,0,1,0) end end if action == 4 then expwarn() peripheral.find("modem", rednet.open) reset() term.write("Enter the code: ") local code = read() local masterid = 0 local buf = {} local dimensions = {} local height = 0 local cx,cz = 0,0 while true do -- await pairing request and respond local id,request = rednet.receive("HB_vprint_pair") if request["code"] == code then masterid = id print("Detected master: "..tostring(masterid)) print("Responding...") rednet.send(masterid, { code=code, }, "HB_vprint_pairespond") print("Waiting for response... ID: "..tostring(os.getComputerID())) local id,msg = -1,{} repeat id,msg = rednet.receive("HB_vprint_pairacknowledge") until id == masterid buf = msg["stack"] dimensions = msg["dimensions"] height = msg["height"] break end end print("Received stacks and dimensions. ID: "..tostring(os.getComputerID())) local id,msg = -1,"" -- wait until we get our current position repeat id,msg = rednet.receive("HB_vprint_pairpossend") until id == masterid cx,cz = msg["cx"],msg["cz"] repeat -- wait until we can begin id = rednet.receive("HB_vprint_begin") until id == masterid local ownstack = { stacks=buf, dimensions=dimensions, height=height } VP_printstack(ownstack,dimensions, cx,0,cz,0) end if action == 5 then shell.run("rm", "main.lua") shell.run("wget", url) return end end