├── drone-sort.lua ├── geo2holo.lua ├── gol-tiny.lua ├── gol.lua ├── holo-count.lua ├── holo-flow.lua ├── holo-text.lua ├── lisp.lua ├── midi.lua ├── miner.lua ├── models ├── button.3dm ├── cc.3dm ├── cover.3dm ├── diamond_block.3dm ├── example.3dm ├── netherportal.3dm ├── pillar_base.3dm ├── pillar_mid.3dm ├── pillar_top.3dm ├── pot.3dm ├── slab.3dm ├── stairs.3dm ├── torch.3dm ├── trapdoor.3dm ├── tree.3dm ├── tree2.3dm ├── villager.3dm └── well.3dm ├── noise.lua ├── print3d-cc.lua ├── print3d-view.lua ├── print3d.lua ├── programs.cfg └── raytracer.lua /drone-sort.lua: -------------------------------------------------------------------------------- 1 | -- Simple drone program demonstrating the use of waypoints (added in 1.5.9) on 2 | -- the example of a simple sorting system, pulling items from multiple input 3 | -- inventories and placing the items into multiple output inventories, with 4 | -- optional filtering, based on the waypoint labels. 5 | 6 | -- The only config option: how far a waypoint may be away from the starting 7 | -- position for it to be used. 8 | local range = 32 9 | 10 | local function proxyFor(name, required) 11 | local address = component and component.list(name)() 12 | if not address and required then 13 | error("missing component '" .. name .. "'") 14 | end 15 | return address and component.proxy(address) or nil 16 | end 17 | 18 | local drone = proxyFor("drone", true) 19 | local nav = proxyFor("navigation", true) 20 | local invctrl = proxyFor("inventory_controller") 21 | 22 | -- Colors used to indicate different states of operation. 23 | local colorCharing = 0xFFCC33 24 | local colorSearching = 0x66CC66 25 | local colorDelivering = 0x6699FF 26 | 27 | -- Keep track of our own position, relative to our starting position. 28 | local px, py, pz = 0, 0, 0 29 | 30 | local function moveTo(x, y, z) 31 | if type(x) == "table" then 32 | x, y, z = x[1], x[2], x[3] 33 | end 34 | local rx, ry, rz = x - px, y - py, z - pz 35 | drone.move(rx, ry, rz) 36 | while drone.getOffset() > 0.5 or drone.getVelocity() > 0.5 do 37 | computer.pullSignal(0.5) 38 | end 39 | px, py, pz = x, y, z 40 | end 41 | 42 | local function recharge() 43 | drone.setLightColor(colorCharing) 44 | moveTo(0, 0, 0) 45 | if computer.energy() < computer.maxEnergy() * 0.1 then 46 | while computer.energy() < computer.maxEnergy() * 0.9 do 47 | computer.pullSignal(1) 48 | end 49 | end 50 | drone.setLightColor(colorSearching) 51 | end 52 | 53 | local function cargoSize() 54 | local result = 0 55 | for slot = 1, drone.inventorySize() do 56 | result = result + drone.count(slot) 57 | end 58 | return result 59 | end 60 | 61 | local function pullItems() 62 | -- Only wait up to 5 seconds, avoids dribbling inputs from stalling us. 63 | local start = computer.uptime() 64 | repeat until not drone.suck(0) or computer.uptime() - start > 5 65 | end 66 | 67 | local function matchCargo(slot, filter) 68 | if not invctrl or not filter or filter == "" then 69 | return true 70 | end 71 | local stack = invctrl.getStackInInternalSlot(slot) 72 | return stack and stack.name:match(filter) 73 | end 74 | 75 | local function haveCargoFor(filter) 76 | for slot = 1, drone.inventorySize() do 77 | if matchCargo(slot, filter) then 78 | return true 79 | end 80 | end 81 | end 82 | 83 | local function dropItems(filter) 84 | for slot = 1, drone.inventorySize() do 85 | if matchCargo(slot, filter) then 86 | drone.select(slot) 87 | drone.drop(0) 88 | end 89 | end 90 | end 91 | 92 | -- List of known waypoints and their positions relative to the position of the 93 | -- drone when it was started. Waypoints are detected when the program first 94 | -- starts, and when it returns to its home position. 95 | local waypoints 96 | 97 | local function updateWaypoints() 98 | waypoints = nav.findWaypoints(range) 99 | end 100 | 101 | local function filterWaypoints(filter) 102 | local result = {} 103 | for _, w in ipairs(waypoints) do 104 | if filter(w) then 105 | table.insert(result, w) 106 | end 107 | end 108 | return result 109 | end 110 | 111 | -- Main program loop; keep returning to recharge, then check all inputs 112 | -- sequentially, distributing what can be picked up from them to all 113 | -- outputs. 114 | while true do 115 | recharge() 116 | updateWaypoints() 117 | -- Get waypoints marking input inventories; defined as those with a high 118 | -- redstone signal going into them (because there'll usually be more input 119 | -- than output inventories). 120 | local inputs = filterWaypoints(function(w) return w.redstone > 0 end) 121 | local outputs = filterWaypoints(function(w) return w.redstone < 1 end) 122 | for _, input in ipairs(inputs) do 123 | moveTo(input.position) 124 | pullItems() 125 | drone.setLightColor(colorDelivering) 126 | for _, output in ipairs(outputs) do 127 | if cargoSize() == 0 then break end 128 | if haveCargoFor(output.label) then 129 | moveTo(output.position) 130 | dropItems(output.label) 131 | end 132 | end 133 | drone.setLightColor(colorSearching) 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /geo2holo.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | if not component.isAvailable("geolyzer") then 3 | io.stderr:write("This program requires a Geolyzer to run.\n") 4 | return 5 | end 6 | if not component.isAvailable("hologram") then 7 | io.stderr:write("This program requires a Hologram Projector to run.\n") 8 | return 9 | end 10 | 11 | local sx, sz = 48, 48 12 | local ox, oz = -24, -24 13 | local starty, stopy = -5 14 | 15 | local function validateY(value, min, max, default) 16 | value = tonumber(value) or default 17 | if value < min or value > max then 18 | io.stderr:write("invalid y coordinate, must be in [" .. min .. ", " .. max .. "]\n") 19 | os.exit(1) 20 | end 21 | return value 22 | end 23 | 24 | do 25 | local args = {...} 26 | starty = validateY(args[1], -32, 31, starty) 27 | stopy = validateY(args[2], starty, starty + 32, math.min(starty + 32, 31)) 28 | end 29 | 30 | component.hologram.clear() 31 | for x=ox,sx+ox do 32 | for z=oz,sz+oz do 33 | local hx, hz = 1 + x - ox, 1 + z - oz 34 | local column = component.geolyzer.scan(x, z, false) 35 | for y=1,1+stopy-starty do 36 | local color = 0 37 | if column then 38 | local hardness = column[y + starty + 32] 39 | if hardness == 0 or not hardness then 40 | color = 0 41 | elseif hardness < 3 then 42 | color = 2 43 | elseif hardness < 100 then 44 | color = 1 45 | else 46 | color = 3 47 | end 48 | end 49 | if component.hologram.maxDepth() > 1 then 50 | component.hologram.set(hx, y, hz, color) 51 | else 52 | component.hologram.set(hx, y, hz, math.min(color, 1)) 53 | end 54 | end 55 | os.sleep(0) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /gol-tiny.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | local event = require("event") 3 | local keyboard = require("keyboard") 4 | local term = require("term") 5 | local unicode = require("unicode") 6 | 7 | print("Left click: mark a cell as 'alive'.") 8 | print("Right click: mark a cell as 'dead'.") 9 | print("Space: toggle pause of the simulation.") 10 | print("Q: exit the program.") 11 | print("Press any key to begin.") 12 | os.sleep(0.1) 13 | event.pull("key") 14 | 15 | local off = " " 16 | local off2on = unicode.char(0x2593) 17 | local on = unicode.char(0x2588) 18 | local on2off = unicode.char(0x2592) 19 | 20 | local gpu = component.gpu 21 | local ow, oh = gpu.getResolution() 22 | gpu.setResolution(27, 15) 23 | local w, h = gpu.getResolution() 24 | gpu.setBackground(0x000000) 25 | gpu.setForeground(0xFFFFFF) 26 | gpu.fill(1, 1, w, h, off) 27 | 28 | local function isAlive(x, y) 29 | x = (x + w - 1) % w + 1 30 | y = (y + h - 1) % h + 1 31 | local char = gpu.get(x, y) 32 | return char == on or char == on2off 33 | end 34 | local function setAlive(x, y, alive) 35 | if isAlive(x, y) == alive then 36 | return 37 | end 38 | if alive then 39 | gpu.set(x, y, off2on) 40 | else 41 | gpu.set(x, y, on2off) 42 | end 43 | end 44 | local function flush() 45 | for y = 1, h do 46 | local row = "" 47 | for x = 1, w do 48 | row = row .. gpu.get(x, y) 49 | end 50 | local final = row:gsub(on2off, off):gsub(off2on, on) 51 | if final ~= row then 52 | gpu.set(1, y, final) 53 | end 54 | end 55 | end 56 | 57 | local function count(x, y) 58 | local n = 0 59 | for rx = -1, 1 do 60 | local nx = x + rx 61 | for ry = -1, 1 do 62 | local ny = y + ry 63 | if (rx ~= 0 or ry ~= 0) and isAlive(nx, ny) then 64 | n = n + 1 65 | end 66 | end 67 | end 68 | return n 69 | end 70 | local function map(x, y) 71 | local n = count(x, y) 72 | return n == 2 and isAlive(x, y) or n == 3 73 | end 74 | local function step() 75 | for y = 1, h do 76 | for x = 1, w do 77 | setAlive(x, y, map(x, y)) 78 | end 79 | end 80 | flush() 81 | end 82 | 83 | local running = false 84 | while true do 85 | local e = table.pack(event.pull(0.25)) 86 | if e[1] == "key_down" then 87 | local _, _, _, code = table.unpack(e) 88 | if code == keyboard.keys.space then 89 | running = not running 90 | elseif code == keyboard.keys.q then 91 | gpu.setResolution(ow, oh) 92 | term.clear() 93 | return 94 | end 95 | elseif e[1] == "touch" then 96 | local _, _, x, y, button = table.unpack(e) 97 | gpu.set(x, y, button == 0 and on or off) 98 | end 99 | if running then 100 | step() 101 | end 102 | end -------------------------------------------------------------------------------- /gol.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | local event = require("event") 3 | local keyboard = require("keyboard") 4 | local term = require("term") 5 | local unicode = require("unicode") 6 | 7 | print("Left click: mark a cell as 'alive'.") 8 | print("Right click: mark a cell as 'dead'.") 9 | print("Space: toggle pause of the simulation.") 10 | print("Q: exit the program.") 11 | print("Press any key to begin.") 12 | os.sleep(0.1) 13 | event.pull("key") 14 | 15 | local off = " " 16 | local on = unicode.char(0x2588) 17 | 18 | local gpu = component.gpu 19 | local ow, oh = gpu.getResolution() 20 | gpu.setResolution(27, 15) 21 | local w, h = gpu.getResolution() 22 | gpu.setBackground(0x000000) 23 | gpu.setForeground(0xFFFFFF) 24 | gpu.fill(1, 1, w, h, off) 25 | 26 | local buffers = {{}, {}} 27 | local dirty = {} 28 | for y = 1, h do 29 | buffers[1][y] = {} 30 | buffers[2][y] = {} 31 | dirty[y] = false 32 | for x = 1, w do 33 | buffers[1][y][x] = off 34 | buffers[2][y][x] = off 35 | end 36 | end 37 | 38 | local function isAlive(x, y) 39 | x = (x + w - 1) % w + 1 40 | y = (y + h - 1) % h + 1 41 | return buffers[1][y][x] == on 42 | end 43 | local function setAlive(x, y, alive) 44 | buffers[2][y][x] = alive and on or off 45 | if isAlive(x, y) ~= alive then 46 | dirty[y] = true 47 | end 48 | end 49 | local function flush() 50 | buffers[1], buffers[2] = buffers[2], buffers[1] 51 | for y = 1, h do 52 | if dirty[y] then 53 | gpu.set(1, y, table.concat(buffers[1][y])) 54 | end 55 | dirty[y] = false 56 | end 57 | end 58 | 59 | local function count(x, y) 60 | local n = 0 61 | for rx = -1, 1 do 62 | local nx = x + rx 63 | for ry = -1, 1 do 64 | local ny = y + ry 65 | if (rx ~= 0 or ry ~= 0) and isAlive(nx, ny) then 66 | n = n + 1 67 | end 68 | end 69 | end 70 | return n 71 | end 72 | local function map(x, y) 73 | local n = count(x, y) 74 | return n == 2 and isAlive(x, y) or n == 3 75 | end 76 | local function step() 77 | for y = 1, h do 78 | for x = 1, w do 79 | setAlive(x, y, map(x, y)) 80 | end 81 | end 82 | flush() 83 | end 84 | 85 | local running = false 86 | while true do 87 | local e = table.pack(event.pull(0.25)) 88 | if e[1] == "key_down" then 89 | local _, _, _, code = table.unpack(e) 90 | if code == keyboard.keys.space then 91 | running = not running 92 | elseif code == keyboard.keys.q then 93 | gpu.setResolution(ow, oh) 94 | term.clear() 95 | return 96 | end 97 | elseif e[1] == "touch" then 98 | local _, _, x, y, button = table.unpack(e) 99 | local char = button == 0 and on or off 100 | buffers[1][y][x] = char 101 | gpu.set(x, y, char) 102 | end 103 | if running then 104 | step() 105 | end 106 | end -------------------------------------------------------------------------------- /holo-count.lua: -------------------------------------------------------------------------------- 1 | -- Generates a random heightmap and displays scrolling text above it. 2 | 3 | local component = require("component") 4 | local keyboard = require("keyboard") 5 | local hologram = component.hologram 6 | 7 | hologram.clear() 8 | 9 | -- in this example all of these must be five wide 10 | local glyphs = { 11 | [0]=[[ 12 | XXX 13 | X X 14 | X X X 15 | X X 16 | XXX 17 | ]], 18 | [1]=[[ 19 | XX 20 | X X 21 | X 22 | X 23 | X 24 | ]], 25 | [2]=[[ 26 | XXXX 27 | X 28 | X 29 | X 30 | XXXXX 31 | ]], 32 | [3]=[[ 33 | XXXX 34 | X 35 | XXX 36 | X 37 | XXXX 38 | ]], 39 | [4]=[[ 40 | X X 41 | X X 42 | XXXXX 43 | X 44 | X 45 | ]], 46 | [5]=[[ 47 | XXXXX 48 | X 49 | XXXX 50 | X 51 | XXXX 52 | ]], 53 | [6]=[[ 54 | XXX 55 | X 56 | XXXX 57 | X X 58 | XXX 59 | ]], 60 | [7]=[[ 61 | XXXXX 62 | X 63 | XXX 64 | X 65 | X 66 | ]], 67 | [8]=[[ 68 | XXX 69 | X X 70 | XXX 71 | X X 72 | XXX 73 | ]], 74 | [9]=[[ 75 | XXX 76 | X X 77 | XXXX 78 | X 79 | XXX 80 | ]] 81 | } 82 | 83 | -- Prepopulate data table; this represents the whole hologram data as a single 84 | -- linear array, nested as y,z,x. In other words, if the hologram were 3x3x3: 85 | -- {(1,1,1),(1,2,1),(1,3,1),(1,1,2),(1,2,2),(1,3,2),(1,1,3),(1,2,3),(1,3,3),(2,1,1),...} 86 | -- Each entry must be a single char; they are merged into a single byte array 87 | -- (or string) before sending the data to the hologram via table.concat. We don't 88 | -- keep the data as a string, because we'd have to create a new string when 89 | -- modifying it, anyway (because you can't do myString[3]="\0"). 90 | local data = {} 91 | for i = 1, 32*48*48 do data[i] = "\0" end 92 | 93 | local w,h=5,5 94 | local x0 = math.floor((48-w)/2) 95 | local z0 = math.floor(48/2) 96 | local y0 = math.floor((32-h)/2) 97 | 98 | print("Press Ctrl+W to stop.") 99 | for i = 1, math.huge do 100 | local num = glyphs[i % 9 + 1] 101 | for y = 1, h do 102 | for x = 1, w do 103 | -- flip chars vertically, because hologram y axis goes up 104 | local charIdx = 1 + (x-1)+(h-y)*(w+1) 105 | local dataIdx = 1 + (y0+y-1) + z0*32 + (x0+x-1)*32*48 106 | data[dataIdx] = num:sub(charIdx, charIdx) == " " and "\0" or "\1" 107 | end 108 | end 109 | hologram.setRaw(table.concat(data)) 110 | os.sleep(0) -- for event handling for keyboard keydown checks. 111 | if keyboard.isKeyDown(keyboard.keys.w) and keyboard.isControlDown() then 112 | hologram.clear() 113 | os.exit() 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /holo-flow.lua: -------------------------------------------------------------------------------- 1 | -- Generates a heightmap and 'moves' across it over time, creating 2 | -- the effect of a flowing terrain. 3 | 4 | local component = require("component") 5 | local noise = require("noise") 6 | local holo = component.hologram 7 | local keyboard = require("keyboard") 8 | print("Press Ctrl+W to stop") 9 | 10 | holo.clear() 11 | local i = 0 12 | while true do 13 | os.sleep(0.1) 14 | i = i + 0.05 15 | for x = 1, 16 * 3 do 16 | for z = 1, 16 * 3 do 17 | local yMax = 15 + noise.fbm(x/(16*3) + i, 1, z/(16*3) + i) * 28 18 | holo.fill(x, z, yMax, 1) 19 | holo.fill(x, z, yMax + 1, 32, 0) 20 | if keyboard.isKeyDown(keyboard.keys.w) and keyboard.isControlDown() then 21 | os.exit() 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /holo-text.lua: -------------------------------------------------------------------------------- 1 | -- Generates a random heightmap and displays scrolling text above it. 2 | 3 | local component = require("component") 4 | local noise = require("noise") 5 | local keyboard = require("keyboard") 6 | local shell = require("shell") 7 | local hologram = component.hologram 8 | 9 | hologram.clear() 10 | 11 | local seed = math.random(0xFFFFFFFF) 12 | for x = 1, 16 * 3 do 13 | for z = 1, 16 * 3 do 14 | hologram.fill(x, z, 15 + noise.fbm(x/(16*3) + seed, 1, z/(16*3) + seed) * 28,1) 15 | end 16 | end 17 | 18 | local glyphs = { 19 | ["a"]=[[ 20 | XXXXX 21 | X X 22 | XXXXX 23 | X X 24 | X X 25 | ]], 26 | ["b"]=[[ 27 | XXXX 28 | X X 29 | XXXX 30 | X X 31 | XXXX 32 | ]], 33 | ["c"]=[[ 34 | XXXXX 35 | X 36 | X 37 | X 38 | XXXXX 39 | ]], 40 | ["d"]=[[ 41 | XXXX 42 | X X 43 | X X 44 | X X 45 | XXXX 46 | ]], 47 | ["e"]=[[ 48 | XXXXX 49 | X 50 | XXXX 51 | X 52 | XXXXX 53 | ]], 54 | ["f"]=[[ 55 | XXXXX 56 | X 57 | XXXX 58 | X 59 | X 60 | ]], 61 | ["g"]=[[ 62 | XXXXX 63 | X 64 | X XXX 65 | X X 66 | XXXXX 67 | ]], 68 | ["h"]=[[ 69 | X X 70 | X X 71 | XXXXX 72 | X X 73 | X X 74 | ]], 75 | ["i"]=[[ 76 | XXX 77 | X 78 | X 79 | X 80 | XXX 81 | ]], 82 | ["j"]=[[ 83 | X 84 | X 85 | X 86 | X X 87 | XXXXX 88 | ]], 89 | ["k"]=[[ 90 | X X 91 | X X 92 | XXX 93 | X X 94 | X X 95 | ]], 96 | ["l"]=[[ 97 | X 98 | X 99 | X 100 | X 101 | XXXXX 102 | ]], 103 | ["m"]=[[ 104 | X X 105 | XX XX 106 | X X X 107 | X X 108 | X X 109 | ]], 110 | ["n"]=[[ 111 | X X 112 | XX X 113 | X X X 114 | X XX 115 | X X 116 | ]], 117 | ["o"]=[[ 118 | XXXXX 119 | X X 120 | X X 121 | X X 122 | XXXXX 123 | ]], 124 | ["p"]=[[ 125 | XXXXX 126 | X X 127 | XXXXX 128 | X 129 | X 130 | ]], 131 | ["q"]=[[ 132 | XXXXX 133 | X X 134 | X X 135 | X X 136 | XXX X 137 | ]], 138 | ["r"]=[[ 139 | XXXXX 140 | X X 141 | XXXX 142 | X X 143 | X X 144 | ]], 145 | ["s"]=[[ 146 | XXXXX 147 | X 148 | XXXXX 149 | X 150 | XXXXX 151 | ]], 152 | ["t"]=[[ 153 | XXXXX 154 | X 155 | X 156 | X 157 | X 158 | ]], 159 | ["u"]=[[ 160 | X X 161 | X X 162 | X X 163 | X X 164 | XXXXX 165 | ]], 166 | ["v"]=[[ 167 | X X 168 | X X 169 | X X 170 | X X 171 | X 172 | ]], 173 | ["w"]=[[ 174 | X X 175 | X X 176 | X X X 177 | X X X 178 | X X 179 | ]], 180 | ["x"]=[[ 181 | X X 182 | X X 183 | X 184 | X X 185 | X X 186 | ]], 187 | ["y"]=[[ 188 | X X 189 | X X 190 | XXX 191 | X 192 | X 193 | ]], 194 | ["z"]=[[ 195 | XXXXX 196 | X 197 | XXX 198 | X 199 | XXXXX 200 | ]], 201 | ["0"]=[[ 202 | XXX 203 | X X 204 | X X X 205 | X X 206 | XXX 207 | ]], 208 | ["1"]=[[ 209 | XX 210 | X X 211 | X 212 | X 213 | X 214 | ]], 215 | ["2"]=[[ 216 | XXXX 217 | X 218 | X 219 | X 220 | XXXXX 221 | ]], 222 | ["3"]=[[ 223 | XXXX 224 | X 225 | XXX 226 | X 227 | XXXX 228 | ]], 229 | ["4"]=[[ 230 | X X 231 | X X 232 | XXXXX 233 | X 234 | X 235 | ]], 236 | ["5"]=[[ 237 | XXXXX 238 | X 239 | XXXX 240 | X 241 | XXXX 242 | ]], 243 | ["6"]=[[ 244 | XXX 245 | X 246 | XXXX 247 | X X 248 | XXX 249 | ]], 250 | ["7"]=[[ 251 | XXXXX 252 | X 253 | XXX 254 | X 255 | X 256 | ]], 257 | ["8"]=[[ 258 | XXX 259 | X X 260 | XXX 261 | X X 262 | XXX 263 | ]], 264 | ["9"]=[[ 265 | XXX 266 | X X 267 | XXXX 268 | X 269 | XXX 270 | ]], 271 | [" "]=[[ 272 | 273 | 274 | 275 | 276 | 277 | ]], 278 | ["."]=[[ 279 | 280 | 281 | 282 | 283 | X 284 | ]], 285 | [","]=[[ 286 | 287 | 288 | 289 | X 290 | X 291 | ]], 292 | [";"]=[[ 293 | 294 | X 295 | 296 | X 297 | X 298 | ]], 299 | ["-"]=[[ 300 | 301 | 302 | XXX 303 | 304 | 305 | ]], 306 | ["+"]=[[ 307 | 308 | X 309 | XXX 310 | X 311 | 312 | ]], 313 | ["*"]=[[ 314 | 315 | X X 316 | X 317 | X X 318 | 319 | ]], 320 | ["/"]=[[ 321 | X 322 | X 323 | X 324 | X 325 | X 326 | ]], 327 | ["\\"]=[[ 328 | X 329 | X 330 | X 331 | X 332 | X 333 | ]], 334 | ["_"]=[[ 335 | 336 | 337 | 338 | 339 | XXXXX 340 | ]], 341 | ["!"]=[[ 342 | X 343 | X 344 | X 345 | 346 | X 347 | ]], 348 | ["?"]=[[ 349 | XXX 350 | X 351 | XX 352 | 353 | X 354 | ]], 355 | ["("]=[[ 356 | X 357 | X 358 | X 359 | X 360 | X 361 | ]], 362 | [")"]=[[ 363 | X 364 | X 365 | X 366 | X 367 | X 368 | ]], 369 | } 370 | 371 | local args = shell.parse(...) 372 | local text = "Open Computers" 373 | if args[1] then 374 | text = tostring(args[1]) 375 | else 376 | print("No text specified, using default value 'Open Computers'.") 377 | end 378 | local text = text .. " " 379 | 380 | -- Generate one big string that represents the concatenated glyphs for the provided text. 381 | local value = "" 382 | for row = 1, 5 do 383 | for col = 1, #text do 384 | local char = string.sub(text:lower(), col, col) 385 | local glyph = glyphs[char] 386 | if glyph then 387 | local s = 0 388 | for _ = 2, row do 389 | s = string.find(glyph, "\n", s + 1, true) 390 | if not s then 391 | break 392 | end 393 | end 394 | if s then 395 | local line = string.sub(glyph, s + 1, (string.find(glyph, "\n", s + 1, true) or 0) - 1) 396 | value = value .. line .. " " 397 | end 398 | end 399 | end 400 | value = value .. "\n" 401 | end 402 | 403 | local bm = {} 404 | for token in value:gmatch("([^\r\n]*)") do 405 | if token ~= "" then 406 | table.insert(bm, token) 407 | end 408 | end 409 | local h,w = #bm,#bm[1] 410 | local sx, sy = math.max(0,(16*3-w)/2), 2*16-h-1 411 | local z = 16*3/2 412 | 413 | print("Press Ctrl+W to stop.") 414 | for i = 1, math.huge do 415 | os.sleep(0.1) 416 | local function col(n) 417 | return (n - 1 + i) % w + 1 418 | end 419 | for i=1, math.min(16*3,w) do 420 | local x = sx + i 421 | local i = col(i) 422 | for j=1, h do 423 | local y = sy + j-1 424 | if bm[1+h-j]:sub(i, i) ~= " " then 425 | hologram.set(x, y, z, 1) 426 | else 427 | hologram.set(x, y, z, 0) 428 | end 429 | if keyboard.isKeyDown(keyboard.keys.w) and keyboard.isControlDown() then 430 | hologram.clear() 431 | os.exit() 432 | end 433 | end 434 | end 435 | end 436 | -------------------------------------------------------------------------------- /lisp.lua: -------------------------------------------------------------------------------- 1 | --[[ This software is licensed under the M.I.T. license. 2 | Author: David Bergman 3 | Source: https://code.google.com/p/lualisp/ 4 | 5 | This is a Scheme/Lisp interpreter, written in Lua. 6 | Adjusted for Lua 5.2 and amalgamated for OpenComputers. 7 | Run it without parameters to get into interpreter mode. 8 | Alternatively pass it a file name of a lisp script. 9 | ]] 10 | 11 | 12 | local environment = {} 13 | 14 | -- Lookup a symbol, going from the most local to the most global scope. 15 | function environment:lookup(symbol) 16 | for i = self.scopeCount, 1, -1 do 17 | local tab = self.scopes[i] 18 | local val = tab[symbol] 19 | if val then 20 | return val 21 | end 22 | end 23 | return nil 24 | end 25 | 26 | -- Add a new key or change an existing one in the most local scope. 27 | function environment:add(key, value) 28 | self.scopes[self.scopeCount][key] = value 29 | return self.scopes[self.scopeCount][key] 30 | end 31 | 32 | -- Create a string representation of the environment. 33 | function environment:tostring() 34 | local str = {} 35 | table.insert(str, "Environment[scopeCount=" .. self.scopeCount .. "\n") 36 | for _, scope in ipairs(self.scopes) do 37 | table.insert(str, "Scope[") 38 | for k, v in pairs(scope) do 39 | table.insert(str, tostring(k)) 40 | table.insert(str, "=") 41 | table.insert(str, tostring(v)) 42 | table.insert(str, " ") 43 | end 44 | table.insert(str, "]\n") 45 | end 46 | table.insert(str, "]") 47 | return table.concat(str) 48 | end 49 | 50 | function environment:addBindings(formalList, actualList) 51 | return self:addLocalScope(environment.bind({}, formalList, actualList)) 52 | end 53 | 54 | function environment.bind(scope, formalList, actualList) 55 | if formalList.type == "CONS" then 56 | scope[formalList.car.lexeme] = actualList.car 57 | return environment.bind(scope, formalList.cdr, actualList.cdr) 58 | else 59 | return scope 60 | end 61 | end 62 | 63 | -- Create local scope and return new extended environment. 64 | function environment:addLocalScope(localScope) 65 | -- Add a new empty local scope. 66 | local newScopes = {} 67 | for _, scope in ipairs(self.scopes) do 68 | table.insert(newScopes, scope) 69 | end 70 | table.insert(newScopes, localScope) 71 | return setmetatable({ 72 | scopeCount = self.scopeCount + 1, 73 | scopes = newScopes, 74 | add = environment.add, 75 | addBindings = environment.addBindins, 76 | addLocalScope = environment.addLocalScope 77 | }, environment.mt) 78 | end 79 | 80 | environment.mt = { 81 | __index = environment.lookup, 82 | __newindex = environment.add, 83 | __tostring = environment.tostring 84 | } 85 | 86 | function environment.new(scope) 87 | -- The scopes are stored from most global to most local. 88 | return setmetatable({ 89 | scopeCount = 1, 90 | scopes = {scope}, 91 | add = environment.add, 92 | addBindings = environment.addBindings, 93 | addLocalScope = environment.addLocalScope, 94 | lookup = environment.lookup 95 | }, environment.mt) 96 | end 97 | 98 | -- Deals with (unevaluated or not) S-expressions, which are simply atoms or CONS cells. 99 | -- The atoms are either: 100 | -- 1. Literals (t or nil) 101 | -- 2. Numericals 102 | -- 3. Operators [',`] 103 | -- 4. Symbols 104 | -- 5. Function references 105 | local Sexpr = {} 106 | 107 | Sexpr.constants = {["t"]=true, ["nil"]=true} 108 | Sexpr.mt = {} 109 | function Sexpr.mt.__tostring(expr) 110 | if expr.type == "CONS" then 111 | return "(" .. tostring(expr.car) .. " . " .. tostring(expr.cdr) .. ")" 112 | else 113 | return "atom[type=" .. expr.type .. ", lex=\"" .. expr.lexeme .. "\"]" 114 | end 115 | end 116 | 117 | -- Atoms 118 | 119 | function Sexpr.newBool(cond) 120 | if cond then 121 | return Sexpr.newAtom("t") 122 | else 123 | return Sexpr.newAtom("nil") 124 | end 125 | end 126 | 127 | function Sexpr.newString(content) 128 | return setmetatable({type="STR", lexeme=content}, Sexpr.mt) 129 | end 130 | 131 | function Sexpr.newOperator(op) 132 | local type 133 | if op == "(" then 134 | type = "LEFTPAREN" 135 | elseif op == ")" then 136 | type = "RIGHTPAREN" 137 | else 138 | type = "OP" 139 | end 140 | return setmetatable({type=type, lexeme=op}, Sexpr.mt) 141 | end 142 | 143 | function Sexpr.newAtom(atom) 144 | -- Make sure to use the string from here on 145 | atom = tostring(atom) 146 | local expr 147 | if Sexpr.constants[atom] then 148 | expr = {type="LITERAL", lexeme=atom} 149 | elseif string.find(atom, "^-?%d*.?%d+$") then 150 | expr = {type="NUM", lexeme=atom} 151 | else 152 | expr = {type="SYM", lexeme=atom} 153 | end 154 | return setmetatable(expr, Sexpr.mt) 155 | end 156 | 157 | -- Create a new function reference, where the special parameter can be nil 158 | -- (for a normal function) or 'lazy' for functions handling their own internal 159 | -- evaluation, or 'macro' for functions mereley replacing their body, for 160 | -- further evaluation. 161 | function Sexpr.newFun(name, fun, special) 162 | return {type="FUN", lexeme=name, fun=fun, special=special} 163 | end 164 | 165 | function Sexpr.cons(a, b) 166 | return setmetatable({type="CONS", car=a, cdr=b} , Sexpr.mt) 167 | end 168 | 169 | function Sexpr.prettyPrint(sexpr, inList) 170 | local pretty 171 | sexpr = sexpr or Sexpr.newAtom("nil") 172 | if sexpr.type == "CONS" then 173 | local str = {} 174 | -- If we are inside a list, we skip the initial '('. 175 | if inList then 176 | table.insert(str, " ") 177 | else 178 | table.insert(str, "(") 179 | end 180 | table.insert(str, Sexpr.prettyPrint(sexpr.car)) 181 | 182 | -- Pretty print the CDR part in list mode. 183 | table.insert(str, Sexpr.prettyPrint(sexpr.cdr, true)) 184 | 185 | -- Close with a ')' if we were not in a list mode already. 186 | if not inList then 187 | table.insert(str, ")") 188 | end 189 | pretty = table.concat(str) 190 | else 191 | local str = {} 192 | if inList and 193 | (sexpr.type ~= "LITERAL" or sexpr.lexeme ~= "nil") then 194 | table.insert(str, " . ") 195 | end 196 | if sexpr.type == "FUN" then 197 | if sexpr.special == "macro" then 198 | table.insert(str, "#macro'") 199 | else 200 | table.insert(str, "#'") 201 | end 202 | end 203 | -- We just add the lexeme, unless we are a nil in the end of a list... 204 | if not inList or sexpr.type ~= "LITERAL" or sexpr.lexeme ~= "nil" then 205 | if sexpr.type == "STR" then 206 | table.insert(str, "\"") 207 | end 208 | table.insert(str, sexpr.lexeme) 209 | if sexpr.type == "STR" then 210 | table.insert(str, "\"") 211 | end 212 | end 213 | pretty = table.concat(str) 214 | end 215 | return pretty 216 | end 217 | 218 | local parser = { 219 | operators = { 220 | ["("] = true, [")"] = true, 221 | [","] = true, ["'"] = true, 222 | ["`"] = true, ["."] = true 223 | } 224 | } 225 | 226 | -- Parse the code snippet, yielding a list of (unevaluated) S-expr. 227 | function parser.parseSexpr(expr) 228 | local tokenList = parser.parseTokens(expr) 229 | local next = 1 230 | local sexpr 231 | local sexprList = {} 232 | repeat 233 | next, sexpr = parser.createSexpr(tokenList, next) 234 | if sexpr then 235 | table.insert(sexprList, sexpr) 236 | end 237 | until not sexpr 238 | return sexprList 239 | end 240 | 241 | function parser.createSexpr(tokens, start) 242 | -- If the first token is a '(', we should expect a "list". 243 | local firstToken = tokens[start] 244 | if not firstToken then 245 | return start, nil 246 | end 247 | if firstToken.type == "LEFTPAREN" then 248 | return parser.createCons(tokens, start + 1) 249 | elseif firstToken.type == "OP" then 250 | local next, cdr = parser.createSexpr(tokens, start + 1) 251 | return next, Sexpr.cons(firstToken, cdr) 252 | else 253 | return start + 1, firstToken 254 | end 255 | end 256 | 257 | function parser.createCons(tokens, start) 258 | -- If the first token is a '.', we just return the second token, as is, 259 | -- while skipping a subsequent ')', else if it is a ')' we return NIL, 260 | -- else we get the first Sexpr and CONS it with the rest. 261 | local firstTok = tokens[start] 262 | if not firstTok then 263 | error("Token index " .. start .. " is out of range when creating CONS S-Expr", 2) 264 | end 265 | if firstTok.type == "OP" and firstTok.lexeme == "." then 266 | -- We skip the last ')'. 267 | local next, cdr = parser.createSexpr(tokens, start + 1) 268 | if not tokens[next] or tokens[next].type ~= "RIGHTPAREN" then 269 | error("The CDR part ending with " .. tokens[next - 1].lexeme .. " was not followed by a ')'") 270 | end 271 | return next + 1, cdr 272 | elseif firstTok.type == "RIGHTPAREN" then 273 | return start + 1, Sexpr.newAtom("nil") 274 | else 275 | local next, car = parser.createSexpr(tokens, start) 276 | local rest, cdr = parser.createCons(tokens, next) 277 | return rest, Sexpr.cons(car, cdr) 278 | end 279 | end 280 | 281 | -- Parse a sub expression, returning both an expression and 282 | -- the index following this sub expression. 283 | function parser.parseTokens(expr) 284 | local tokens = {} 285 | -- We do it character by character, using queues to 286 | -- handle strings as well as regular lexemes 287 | local currentToken = {} 288 | local inString = false 289 | local isEscaping = false 290 | for i = 1, string.len(expr) do 291 | local c = string.sub(expr, i, i) 292 | -- We have seven (7) main cases: 293 | if isEscaping then 294 | -- 1. Escaping this character, whether in a string or not. 295 | table.insert(currentToken, c) 296 | isEscaping = false 297 | elseif c == "\\" then 298 | -- 2. An escape character 299 | isEscaping = true 300 | elseif c == "\"" then 301 | -- 3. A quotation mark 302 | if not inString then 303 | -- a. starting a new string 304 | -- If we already had a token, let us finish that up first 305 | if #currentToken > 0 then 306 | table.insert(tokens, Sexpr.newAtom(table.concat(currentToken))) 307 | end 308 | currentToken = {} 309 | inString = true 310 | else 311 | -- b. ending a string 312 | table.insert(tokens, Sexpr.newString(table.concat(currentToken))) 313 | currentToken = {} 314 | inString = false 315 | end 316 | elseif inString then 317 | -- 4. inside a string, so just add the character 318 | table.insert(currentToken, c) 319 | elseif parser.operators[c] then 320 | -- 5. special operator (and not inside string) 321 | -- We add any saved token 322 | if #currentToken > 0 then 323 | table.insert(tokens, Sexpr.newAtom(table.concat(currentToken))) 324 | currentToken = {} 325 | end 326 | table.insert(tokens, Sexpr.newOperator(c)) 327 | elseif string.find(c, "%s") then 328 | -- 6. A blank character, which should add the current token, if any. 329 | if #currentToken > 0 then 330 | table.insert(tokens, Sexpr.newAtom(table.concat(currentToken))) 331 | currentToken = {} 332 | end 333 | else 334 | -- 7. A non-blank character being part of the a symbol 335 | table.insert(currentToken, c) 336 | end 337 | end 338 | -- Add any trailing token... 339 | if #currentToken > 0 then 340 | local atom 341 | if inString then 342 | atom = Sexpr.newString(table.concat(currentToken)) 343 | else 344 | atom = Sexpr.newAtom(table.concat(currentToken)) 345 | end 346 | table.insert(tokens, atom) 347 | end 348 | return tokens 349 | end 350 | 351 | local lisp = {} 352 | 353 | function lisp.evalExpr(env, expr) 354 | return lisp.evalSexprList(env, parser.parseSexpr(expr)) 355 | end 356 | 357 | function lisp.evalQuote(env, sexpr) 358 | local value 359 | if not sexpr.type then 360 | error("Invalid S-expr: ", 2) 361 | end 362 | if sexpr.type == "CONS" then 363 | local car = sexpr.car 364 | if car.type == "OP" and car.lexeme == "," then 365 | value = lisp.evalSexpr(env, sexpr.cdr) 366 | else 367 | local evalCar = lisp.evalQuote(env, car) 368 | local cdr = lisp.evalQuote(env, sexpr.cdr) 369 | value = Sexpr.cons(evalCar, cdr) 370 | end 371 | else 372 | value = sexpr 373 | end 374 | return value 375 | end 376 | 377 | function lisp.evalSexprList(env, sexprList, index) 378 | if not index then 379 | index = 1 380 | end 381 | local count = #sexprList 382 | if index > count then 383 | return nil 384 | else 385 | local firstValue = lisp.evalSexpr(env, sexprList[index]) 386 | if index == count then 387 | return firstValue 388 | else 389 | return lisp.evalSexprList(env, sexprList, index + 1) 390 | end 391 | end 392 | end 393 | 394 | function lisp.evalSexpr(env, sexpr) 395 | local value 396 | if not sexpr.type then 397 | error("Invalid S-expr: " .. sexpr, 2) 398 | end 399 | if sexpr.type == "CONS" then 400 | -- 1. Cons cell 401 | local car = sexpr.car 402 | if car.type == "OP" and car.lexeme == "'" then 403 | value = sexpr.cdr 404 | elseif car.type == "OP" and car.lexeme == "`" then 405 | value = lisp.evalQuote(env, sexpr.cdr) 406 | else 407 | local fun = lisp.evalSexpr(env, car) 408 | if not fun or fun.type ~= "FUN" then 409 | error("The S-expr did not evaluate to a function: " .. tostring(car)) 410 | end 411 | -- The function can be eithe "lazy", in that it deals with 412 | -- evaluation of its arguments itself, a "macro", which requires 413 | -- a second evaluation after the macro expansion, or 414 | -- a regular eager one 415 | local args 416 | if fun.special == "lazy" or fun.special == "macro" then 417 | args = sexpr.cdr 418 | else 419 | args = lisp.evalList(env, sexpr.cdr) 420 | end 421 | value = fun.fun(env, args) 422 | end 423 | elseif sexpr.type == "SYM" then 424 | -- a. symbol 425 | value = env[sexpr.lexeme] 426 | if not value then 427 | error("The symbol '" .. sexpr.lexeme .. "' is not defined") 428 | end 429 | else 430 | -- b. constant 431 | value = sexpr 432 | end 433 | return value 434 | end 435 | 436 | -- Evaluate each item in a list 437 | function lisp.evalList(env, list) 438 | if list.type == "CONS" then 439 | return Sexpr.cons(lisp.evalSexpr(env, list.car), 440 | lisp.evalList(env, list.cdr)) 441 | else 442 | return list 443 | end 444 | end 445 | 446 | -- Apply an environment and get the substituted S-exp 447 | function lisp.applyEnv(env, expr) 448 | if expr.type == "CONS" then 449 | return Sexpr.cons(lisp.applyEnv(env, expr.car), 450 | lisp.applyEnv(env, expr.cdr)) 451 | elseif expr.type == "SYM" then 452 | return env[expr.lexeme] or expr 453 | else 454 | return expr 455 | end 456 | end 457 | 458 | -- Some primitives 459 | 460 | function lisp.prim_car(env, args) 461 | return args.car.car 462 | end 463 | 464 | function lisp.prim_cdr(env, args) 465 | return args.car.cdr 466 | end 467 | 468 | function lisp.prim_cons(env, args) 469 | return Sexpr.cons(args.car, args.cdr.car) 470 | end 471 | 472 | function lisp.prim_plus(env, args, acc) 473 | if not args or not args.car then 474 | return Sexpr.newAtom(acc) 475 | else 476 | return lisp.prim_plus(env, args.cdr, (acc or 0) + tonumber(lisp.evalSexpr(env, args.car).lexeme)) 477 | end 478 | end 479 | 480 | function lisp.prim_mult(env, args, acc) 481 | if not args or not args.car then 482 | return Sexpr.newAtom(acc) 483 | else 484 | return lisp.prim_mult(env, args.cdr, (acc or 1) * tonumber(lisp.evalSexpr(env, args.car).lexeme)) 485 | end 486 | end 487 | 488 | function lisp.prim_lambda(env, args) 489 | local formalParams = args.car 490 | local body = args.cdr.car 491 | return Sexpr.newFun("(lambda " .. 492 | Sexpr.prettyPrint(formalParams) .. 493 | " " .. Sexpr.prettyPrint(body) .. ")", 494 | function(env2, actualParams) 495 | local localEnv = env:addBindings(formalParams, actualParams) 496 | return lisp.evalSexpr(localEnv, body) 497 | end) 498 | end 499 | 500 | function lisp.prim_if(env, args) 501 | local cond = lisp.evalSexpr(env, args.car) 502 | if cond.type == "LITERAL" and cond.lexeme == "nil" then 503 | return lisp.evalSexpr(env, args.cdr.cdr.car) 504 | else 505 | return lisp.evalSexpr(env, args.cdr.car) 506 | end 507 | end 508 | 509 | function lisp.prim_eq(env, args) 510 | local arg1 = args.car 511 | local arg2 = args.cdr.car 512 | return Sexpr.newBool(arg1.type == arg2.type and arg1.type ~= "CONS" and arg1.lexeme == arg2.lexeme) 513 | end 514 | 515 | function lisp.prim_lt(env, args) 516 | return Sexpr.newBool(tonumber(lisp.evalSexpr(args.car).lexeme) < tonumber(lisp.evalSexpr(args.cdr.car).lexeme)) 517 | end 518 | 519 | function lisp.prim_consp(env, args) 520 | return Sexpr.newBool(args.car.type == "CONS") 521 | end 522 | 523 | function lisp.prim_neg(env, args) 524 | if not args or not args.car then 525 | return Sexpr.newBool(false) 526 | else 527 | return Sexpr.cons(Sexpr.newAtom(-tonumber(lisp.evalSexpr(env, args.car).lexeme)), lisp.prim_neg(env, args.cdr)) 528 | end 529 | end 530 | 531 | function lisp.prim_setq(env, args) 532 | local value = lisp.evalSexpr(env, args.cdr.car) 533 | env[args.car.lexeme] = value 534 | return value 535 | end 536 | 537 | function lisp.prim_eval(env, sexpr) 538 | local car = sexpr.car 539 | if car.type == "STR" then 540 | return lisp.evalExpr(env, car.lexeme) 541 | else 542 | return lisp.evalSexpr(env, car) 543 | end 544 | end 545 | 546 | function lisp.prim_load(env, sexpr) 547 | lisp.runFile(env, sexpr.car.lexeme) 548 | return Sexpr.newBool(true) 549 | end 550 | 551 | function lisp.prim_echo(env, sexpr) 552 | print(Sexpr.prettyPrint(sexpr.car)) 553 | return Sexpr.newBool(true) 554 | end 555 | 556 | function lisp.prim_defmacro(env, sexpr) 557 | local name = sexpr.car 558 | local params = sexpr.cdr.car 559 | local body = sexpr.cdr.cdr.car 560 | local fun = Sexpr.newFun("(defmacro " .. name.lexeme .. 561 | " " .. Sexpr.prettyPrint(params) .. 562 | " " .. Sexpr.prettyPrint(body) .. 563 | ")", function (env, e) 564 | return lisp.evalSexpr(env, lisp.applyEnv(environment.new(environment.bind({}, params, e)), body)) 565 | end, "macro") 566 | env[name.lexeme] = fun 567 | return fun 568 | end 569 | 570 | function lisp.getPrimitiveScope() 571 | return { 572 | ["car"] = Sexpr.newFun("car", lisp.prim_car), 573 | ["cdr"] = Sexpr.newFun("cdr", lisp.prim_cdr), 574 | ["cons"] = Sexpr.newFun("cons", lisp.prim_cons), 575 | ["lambda"] = Sexpr.newFun("lambda", lisp.prim_lambda, "lazy"), 576 | ["setq"] = Sexpr.newFun("setq", lisp.prim_setq, "lazy"), 577 | ["<"] = Sexpr.newFun("<", lisp.prim_lt), 578 | ["+"] = Sexpr.newFun("+", lisp.prim_plus), 579 | ["*"] = Sexpr.newFun("*", lisp.prim_mult), 580 | ["neg"] = Sexpr.newFun("neg", lisp.prim_neg), 581 | ["eq"] = Sexpr.newFun("eq", lisp.prim_eq), 582 | ["consp"] = Sexpr.newFun("consp", lisp.prim_consp), 583 | ["eval"] = Sexpr.newFun("eval", lisp.prim_eval), 584 | ["load"] = Sexpr.newFun("load", lisp.prim_load), 585 | ["echo"] = Sexpr.newFun("echo", lisp.prim_echo), 586 | ["defmacro"] = Sexpr.newFun("defmacro", lisp.prim_defmacro, "lazy"), 587 | ["if"] = Sexpr.newFun("if", lisp.prim_if, "lazy") 588 | } 589 | end 590 | 591 | function lisp.getGlobalEnv() 592 | local env = environment.new(lisp.getPrimitiveScope()) 593 | lisp.evalExpr(env, [[ 594 | (defmacro defun (name params body) 595 | (setq name (lambda params body))) 596 | 597 | (defmacro or (a b) 598 | (if a a b)) 599 | 600 | (defmacro and (a b) 601 | (if a b nil)) 602 | 603 | (defun <= (x y) 604 | (or (< x y) (eq x y))) 605 | 606 | (defun > (x y) 607 | (< y x)) 608 | 609 | (defun >= (x y) 610 | (<= y x)) 611 | 612 | (defun - (x y) 613 | (+ (cons x (neg y))))) 614 | 615 | (defun nullp (x) 616 | (eq x nil)) 617 | ]]) 618 | return env 619 | end 620 | 621 | function lisp.runFile(env, filename) 622 | local f, reason = io.open(filename, "r") 623 | if not f then 624 | error(reason) 625 | end 626 | local head = f:read("*line") 627 | if not head then return end 628 | if head:sub(1, 2) ~= "#!" then 629 | f:seek("set", 0) 630 | end 631 | local code = f:read("*all") 632 | f:close() 633 | return lisp.evalExpr(env, code) 634 | end 635 | 636 | function lisp.readEval() 637 | local component = require("component") 638 | local term = require("term") 639 | local history = {} 640 | local env = lisp.getGlobalEnv() 641 | while term.isAvailable() do 642 | local foreground = component.gpu.setForeground(0x00FF00) 643 | term.write("lisp> ") 644 | component.gpu.setForeground(foreground) 645 | local code = term.read(history) 646 | if code == nil then 647 | return 648 | end 649 | while #history > 10 do 650 | table.remove(history, 1) 651 | end 652 | if code then 653 | local result = table.pack(pcall(lisp.evalExpr, env, code)) 654 | if not result[1] or result.n > 1 then 655 | for i = 2, result.n do 656 | if result[i] and type(result[i]) ~= "string" then 657 | result[i] = Sexpr.prettyPrint(result[i]) 658 | end 659 | end 660 | print(table.unpack(result, 2, result.n)) 661 | end 662 | end 663 | end 664 | end 665 | 666 | local shell = require("shell") 667 | local args = shell.parse(...) 668 | 669 | if #args > 0 then 670 | lisp.runFile(lisp.getGlobalEnv(), shell.resolve(args[1])) 671 | else 672 | lisp.readEval() 673 | end -------------------------------------------------------------------------------- /midi.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | local computer = require("computer") 3 | local shell = require("shell") 4 | local keyboard = require("keyboard") 5 | local note = require("note") 6 | local bit32 = require("bit32") 7 | 8 | local args, options = shell.parse(...) 9 | if #args < 1 then 10 | print("Usage: midi [-i] [track1 [track2 [...]]]") 11 | print("Where the tracks are the numbers of the tracks to actually play.") 12 | print(" -i: only parse and show track info, don't play.") 13 | return 14 | end 15 | 16 | -- Implements the essentials for MIDI file parsing, references: 17 | -- http://www.recordingblogs.com/sa/tabid/88/Default.aspx?topic=Musical+Instrument+Digital+Interface+(MIDI) 18 | -- http://www.sonicspot.com/guide/midifiles.html 19 | 20 | local enabledTracks = {n=0} 21 | for i = 2, #args do 22 | enabledTracks[tonumber(args[i])] = true 23 | enabledTracks.n = enabledTracks.n + 1 24 | end 25 | 26 | local instruments = {} 27 | for address in component.list("note_block") do 28 | table.insert(instruments, function(note) 29 | -- 60 = C in MIDI, 6 = C in Minecraft 30 | component.invoke(address, "trigger", (note + 6 - 60) % 24 + 1) 31 | end) 32 | end 33 | if #instruments == 0 then 34 | local function beepableFrequency(midiCode) 35 | local freq = note.freq(midiCode) 36 | if freq <= 0 then error("Nonpositive frequency") end 37 | -- shift it by octaves so we at least get the right pitch 38 | while freq < 20 do freq = freq * 2 end 39 | while freq > 2000 do freq = freq / 2 end 40 | return freq 41 | end 42 | if component.isAvailable("beep") then 43 | print("No note blocks found, falling back to beep card.") 44 | local notes = {} 45 | instruments[1] = function(note, duration) 46 | notes[beepableFrequency(note)] = duration or 0.05 47 | end 48 | instruments.flush = function() 49 | component.beep.beep(notes) 50 | for k, v in pairs(notes) do 51 | notes[k] = nil 52 | end 53 | end 54 | else 55 | print("No note blocks or beep card found, falling back to built-in speaker.") 56 | instruments[1] = function(note, duration) 57 | pcall(computer.beep, beepableFrequency(note), duration or 0.05) 58 | return true -- only one event per tick 59 | end 60 | end 61 | else 62 | print("Using " .. #instruments .. " note blocks.") 63 | end 64 | 65 | local filename = shell.resolve(args[1]) 66 | local f, reason = io.open(filename, "rb") 67 | if not f then 68 | print(reason) 69 | return 70 | end 71 | 72 | local function parseVarInt(s, bits) -- parses multiple bytes as an integer 73 | if not s then 74 | error("error parsing file") 75 | end 76 | bits = bits or 8 77 | local mask = bit32.rshift(0xFF, 8 - bits) 78 | local num = 0 79 | for i = 1, s:len() do 80 | num = num + bit32.lshift(bit32.band(s:byte(i), mask), (s:len() - i) * bits) 81 | end 82 | return num 83 | end 84 | 85 | local function readChunkInfo() -- reads chunk header info 86 | local id = f:read(4) 87 | if not id then 88 | return 89 | end 90 | return id, parseVarInt(f:read(4)) 91 | end 92 | 93 | -- Read the file header and with if file information. 94 | local id, size = readChunkInfo() 95 | if id ~= "MThd" or size ~= 6 then 96 | print("error parsing header (" .. id .. "/" .. size .. ")") 97 | return 98 | end 99 | 100 | local format = parseVarInt(f:read(2)) 101 | local tracks = parseVarInt(f:read(2)) 102 | local delta = parseVarInt(f:read(2)) 103 | 104 | if format < 0 or format > 2 then 105 | print("unknown format") 106 | return 107 | end 108 | 109 | local formatName = ({"single", "synchronous", "asynchronous"})[format + 1] 110 | print(string.format("Found %d %s tracks.", tracks, formatName)) 111 | 112 | if format == 2 then 113 | print("Sorry, asynchronous tracks are not supported.") 114 | return 115 | end 116 | 117 | -- Figure out our time system and prepare accordingly. 118 | local time = {division = bit32.band(0x8000, delta) == 0 and "tpb" or "fps"} 119 | if time.division == "tpb" then 120 | time.tpb = bit32.band(0x7FFF, delta) 121 | time.mspb = 500000 122 | function time.tick() 123 | return time.mspb / time.tpb 124 | end 125 | print(string.format("Time division is in %d ticks per beat.", time.tpb)) 126 | else 127 | time.fps = bit32.band(0x7F00, delta) 128 | time.tpf = bit32.band(0x00FF, delta) 129 | function time.tick() 130 | return 1000000 / (time.fps * time.tpf) 131 | end 132 | print(string.format("Time division is in %d frames per second with %d ticks per frame.", time.fps, time.tpf)) 133 | end 134 | function time.calcDelay(later, earlier) 135 | return (later - earlier) * time.tick() / 1000000 136 | end 137 | 138 | -- Parse all track chunks. 139 | local totalOffset = 0 140 | local totalLength = 0 141 | local tracks = {} 142 | while true do 143 | local id, size = readChunkInfo() 144 | if not id then 145 | break 146 | end 147 | if id == "MTrk" then 148 | local track = {} 149 | local cursor = 0 150 | local start, offset = f:seek(), 0 151 | local inSysEx = false 152 | local running = 0 153 | 154 | local function read(n) 155 | n = n or 1 156 | if n > 0 then 157 | offset = offset + n 158 | return f:read(n) 159 | end 160 | end 161 | local function readVariableLength() 162 | local total = "" 163 | for i = 1, math.huge do 164 | local part = read() 165 | total = total .. part 166 | if bit32.band(0x80, part:byte(1)) == 0 then 167 | return parseVarInt(total, 7) 168 | end 169 | end 170 | end 171 | local function parseVoiceMessage(event) 172 | local channel = bit32.band(0xF, event) 173 | local note = parseVarInt(read()) 174 | local velocity = parseVarInt(read()) 175 | return channel, note, velocity 176 | end 177 | local currentNoteEvents = {} 178 | local function noteOn(cursor, channel, note, velocity) 179 | track[cursor] = {channel, note, velocity} 180 | if not currentNoteEvents[channel] then 181 | currentNoteEvents[channel] = {} 182 | end 183 | currentNoteEvents[channel][note] = {event=track[cursor], tick=cursor} 184 | end 185 | local function noteOff(cursor, channel, note, velocity) 186 | if not (currentNoteEvents[channel] and currentNoteEvents[channel][note]) then return end 187 | table.insert(currentNoteEvents[channel][note].event 188 | , time.calcDelay(cursor, currentNoteEvents[channel][note].tick)) 189 | currentNoteEvents[channel][note] = nil 190 | end 191 | 192 | while offset < size do 193 | cursor = cursor + readVariableLength() 194 | totalLength = math.max(totalLength, cursor) 195 | local test = parseVarInt(read()) 196 | if inSysEx and test ~= 0xF7 then 197 | error("corrupt file: could not find continuation of divided sysex event") 198 | end 199 | local event 200 | if bit32.band(test, 0x80) == 0 then 201 | if running == 0 then 202 | error("corrupt file: invalid running status") 203 | end 204 | f.bufferRead = string.char(test) .. f.bufferRead 205 | offset = offset - 1 206 | event = running 207 | else 208 | event = test 209 | if test < 0xF0 then 210 | running = test 211 | end 212 | end 213 | local status = bit32.band(0xF0, event) 214 | if status == 0x80 then -- Note off. 215 | local channel, note, velocity = parseVoiceMessage(event) 216 | noteOff(cursor, channel, note, velocity) 217 | elseif status == 0x90 then -- Note on. 218 | local channel, note, velocity = parseVoiceMessage(event) 219 | if velocity == 0 then 220 | noteOff(cursor, channel, note, velocity) 221 | else 222 | noteOn(cursor, channel, note, velocity) 223 | end 224 | elseif status == 0xA0 then -- Aftertouch / key pressure 225 | parseVoiceMessage(event) -- not handled 226 | elseif status == 0xB0 then -- Controller 227 | parseVoiceMessage(event) -- not handled 228 | elseif status == 0xC0 then -- Program change 229 | parseVarInt(read()) -- not handled 230 | elseif status == 0xD0 then -- Channel pressure 231 | parseVarInt(read()) -- not handled 232 | elseif status == 0xE0 then -- Pitch / modulation wheel 233 | parseVarInt(read(2), 7) -- not handled 234 | elseif event == 0xF0 then -- System exclusive event 235 | local length = readVariableLength() 236 | if length > 0 then 237 | read(length - 1) 238 | inSysEx = read(1):byte(1) ~= 0xF7 239 | end 240 | elseif event == 0xF1 then -- MIDI time code quarter frame 241 | parseVarInt(read()) -- not handled 242 | elseif event == 0xF2 then -- Song position pointer 243 | parseVarInt(read(2), 7) -- not handled 244 | elseif event == 0xF3 then -- Song select 245 | parseVarInt(read(2), 7) -- not handled 246 | elseif event == 0xF7 then -- Divided system exclusive event 247 | local length = readVariableLength() 248 | if length > 0 then 249 | read(length - 1) 250 | inSysEx = read(1):byte(1) ~= 0xF7 251 | else 252 | inSysEx = false 253 | end 254 | elseif event >= 0xF8 and event <= 0xFE then -- System real-time event 255 | -- not handled 256 | elseif event == 0xFF then 257 | -- Meta message. 258 | local metaType = parseVarInt(read()) 259 | local length = parseVarInt(read()) 260 | local data = read(length) 261 | 262 | if metaType == 0x00 then -- Sequence number 263 | track.sequence = parseVarInt(data) 264 | elseif metaType == 0x01 then -- Text event 265 | elseif metaType == 0x02 then -- Copyright notice 266 | elseif metaType == 0x03 then -- Sequence / track name 267 | track.name = data 268 | elseif metaType == 0x04 then -- Instrument name 269 | track.instrument = data 270 | elseif metaType == 0x05 then -- Lyric text 271 | elseif metaType == 0x06 then -- Marker text 272 | elseif metaType == 0x07 then -- Cue point 273 | elseif metaType == 0x20 then -- Channel prefix assignment 274 | elseif metaType == 0x2F then -- End of track 275 | track.eot = cursor 276 | elseif metaType == 0x51 then -- Tempo setting 277 | track[cursor] = parseVarInt(data) 278 | elseif metaType == 0x54 then -- SMPTE offset 279 | elseif metaType == 0x58 then -- Time signature 280 | elseif metaType == 0x59 then -- Key signature 281 | elseif metaType == 0x7F then -- Sequencer specific event 282 | end 283 | else 284 | f:seek("cur", -9) 285 | local area = f:read(16) 286 | local dump = "" 287 | for i = 1, area:len() do 288 | dump = dump .. string.format(" %02X", area:byte(i)) 289 | if i % 4 == 0 then 290 | dump = dump .. "\n" 291 | end 292 | end 293 | error(string.format("midi file contains unhandled event types:\n0x%X at offset %d/%d\ndump of the surrounding area:\n%s", event, offset, size, dump)) 294 | end 295 | end 296 | -- turn off any remaining notes 297 | for iChannel, iNotes in pairs(currentNoteEvents) do 298 | for iNote, iEntry in pairs(currentNoteEvents[iChannel]) do 299 | noteOff(cursor, iChannel, iNote) 300 | end 301 | end 302 | local delta = size - offset 303 | if delta ~= 0 then 304 | f:seek("cur", delta) 305 | end 306 | totalOffset = totalOffset + size 307 | table.insert(tracks, track) 308 | else 309 | print(string.format("Encountered unknown chunk type %s, skipping.", id)) 310 | f:seek("cur", size) 311 | end 312 | end 313 | 314 | f:close() 315 | 316 | if options.i then 317 | print(string.format("Found %d tracks, total length is %d ticks.", #tracks, totalLength)) 318 | for i, track in ipairs(tracks) do 319 | if track.name then 320 | print(string.format("#%d: %s", i, track.name)) 321 | end 322 | end 323 | return 324 | end 325 | 326 | local removed = 0 327 | if enabledTracks.n > 0 then 328 | for i = #tracks, 1, -1 do 329 | if not enabledTracks[i] then 330 | table.remove(tracks, i) 331 | removed = removed + 1 332 | end 333 | end 334 | end 335 | print("Playing " .. #tracks .. " tracks:") 336 | for _, track in ipairs(tracks) do 337 | if track.name then 338 | print(string.format("%s", track.name)) 339 | end 340 | end 341 | 342 | local channels = {n=0} 343 | local lastTick, lastTime = 0, computer.uptime() 344 | print("Press Ctrl+C to exit.") 345 | for tick = 1, totalLength do 346 | local hasEvent = false 347 | for _, track in ipairs(tracks) do 348 | if track[tick] then 349 | hasEvent = true 350 | break 351 | end 352 | end 353 | if hasEvent then 354 | local delay = time.calcDelay(tick, lastTick) 355 | -- delay % 0.05 == 0 doesn't seem to work 356 | if math.floor(delay * 100 + 0.5) % 5 == 0 then 357 | os.sleep(delay) 358 | else 359 | -- Busy idle otherwise, because a sleep will take up to 50ms. 360 | local begin = os.clock() 361 | while os.clock() - begin < delay do end 362 | end 363 | lastTick = tick 364 | lastTime = computer.uptime() 365 | for _, track in ipairs(tracks) do 366 | local event = track[tick] 367 | if event then 368 | if type(event) == "number" then 369 | time.mspb = event 370 | elseif type(event) == "table" then 371 | local channel, note, velocity, duration = table.unpack(event) 372 | local instrument 373 | if not channels[channel] then 374 | channels.n = channels.n + 1 375 | channels[channel] = instruments[1 + (channels.n % #instruments)] 376 | end 377 | if channels[channel](note, duration) then 378 | break 379 | end 380 | end 381 | end 382 | end 383 | if instruments.flush then instruments.flush() end 384 | end 385 | if keyboard.isKeyDown(keyboard.keys.c) and keyboard.isControlDown() then os.exit() end 386 | end -------------------------------------------------------------------------------- /miner.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Branch mining program for OpenComputers robots. 3 | 4 | This program is designed to dig out branches, in a fashion that allows 5 | players to easily navigate the dug out tunnels. The primary concern was 6 | not the performance of the mining, only a good detection rate, and nice 7 | tunnels. Suggested upgrades for this are the geolyzer and inventory 8 | controller upgrade, and depending on your world gen (ravines?) a hover 9 | upgrade might be necessary. The rest is up to you (chunkloading, more 10 | inventory, battery upgrades). 11 | 12 | By Sangar, 2015 13 | 14 | This program is licensed under the MIT license. 15 | http://opensource.org/licenses/mit-license.php 16 | ]] 17 | 18 | local component = require("component") 19 | local computer = require("computer") 20 | local robot = require("robot") 21 | local shell = require("shell") 22 | local sides = require("sides") 23 | local event = require("event") 24 | local args, options = shell.parse(...) 25 | 26 | --[[ Config ]]----------------------------------------------------------------- 27 | 28 | -- Every how many blocks to dig a side shaft. The default makes for a 29 | -- two wide wall between tunnels, making sure we don't miss anything. 30 | local shaftInterval = 3 31 | 32 | -- Max recursion level for mining ore veins. We abort early because we 33 | -- assume we'll encounter the same vein again from an adjacent tunnel. 34 | local maxVeinRecursion = 8 35 | 36 | -- Every how many blocks to place a torch when placing torches. 37 | local torchInverval = 11 38 | 39 | --[[ Constants ]]-------------------------------------------------------------- 40 | 41 | -- Quick look-up table for inverting directions. 42 | local oppositeSides = { 43 | [sides.north] = sides.south, 44 | [sides.south] = sides.north, 45 | [sides.east] = sides.west, 46 | [sides.west] = sides.east, 47 | [sides.up] = sides.down, 48 | [sides.down] = sides.up 49 | } 50 | 51 | -- For pushTurn() readability. 52 | local left = false 53 | local right = not left 54 | 55 | --[[ State ]]------------------------------------------------------------------ 56 | 57 | -- Slots we don't want to drop. Filled in during initialization, based 58 | -- on items already in the inventory. Useful for stuff like /dev/null. 59 | local keepSlot = {} 60 | 61 | -- Slots that we keep torches in, updated when stocking up on torches. 62 | local torchSlots = {} 63 | 64 | --[[ "Passive" logic ]]-------------------------------------------------------- 65 | 66 | -- Keep track of moves we're away from our origin, and average energy used per 67 | -- move. This is used to compute the threshold at which we have to return to 68 | -- maintenance to recharge. 69 | local preMoveEnergy, averageMoveCost, distanceToOrigin = 0, 15, 0 70 | 71 | -- The actual callback called in postMove(). 72 | local onMove 73 | 74 | -- Called whenever we're about to move, used to compute move cost. 75 | local function preMove() 76 | preMoveEnergy = computer.energy() 77 | end 78 | 79 | -- Called whenever we're done moving, used for automatic torch placement an digging. 80 | local function postMove() 81 | local moveCost = preMoveEnergy - computer.energy() 82 | if moveCost > 0 then 83 | averageMoveCost = (averageMoveCost + moveCost) / 2 84 | end 85 | if onMove then 86 | onMove() 87 | end 88 | end 89 | 90 | --[[ Utility ]]---------------------------------------------------------------- 91 | 92 | local function prompt(message) 93 | io.write(message .. " [Y/n] ") 94 | local result = io.read() 95 | return result and (result == "" or result:lower() == "y") 96 | end 97 | 98 | -- Check if a block with the specified info should be mined. 99 | local function shouldMine(info) 100 | return info and info.name and (info.name:match(".*ore.*") or info.name:match(".*Ore.*")) 101 | end 102 | 103 | -- Number of stacks of torches to keep; default is 1 per inventory upgrade. 104 | local function torchStacks() 105 | return math.max(1, math.ceil(robot.inventorySize() / 16)) 106 | end 107 | 108 | -- Look for the first empty slot in our inventory. 109 | local function findEmptySlot() 110 | for slot = 1, robot.inventorySize() do 111 | if robot.count(slot) == 0 then 112 | return slot 113 | end 114 | end 115 | end 116 | 117 | -- Find the first torch slot that still contains torches. 118 | local function findTorchSlot() 119 | for _, slot in ipairs(torchSlots) do 120 | if robot.count(slot) > 0 then 121 | return slot 122 | end 123 | end 124 | end 125 | 126 | -- Since robot.select() is an indirect call, we can speed things up a bit. 127 | local selectedSlot 128 | local function cachedSelect(slot) 129 | if slot ~= selectedSlot then 130 | robot.select(slot) 131 | selectedSlot = slot 132 | end 133 | end 134 | 135 | -- Place a single torch above the robot, if there are any torches left. 136 | local function placeTorch() 137 | local slot = findTorchSlot() 138 | local result = false 139 | if slot then 140 | cachedSelect(slot) 141 | result = robot.placeUp() 142 | cachedSelect(1) 143 | end 144 | return result 145 | end 146 | 147 | -- Dig out a block on the specified side, without tool if possible. 148 | local function dig(side, callback) 149 | repeat 150 | -- Check for maintenance first, to make sure we make the return trip when 151 | -- our batteries are running low. 152 | local emptySlot = findEmptySlot() 153 | if callback then 154 | callback(not emptySlot) -- Parameter: is inventory full. 155 | emptySlot = findEmptySlot() 156 | end 157 | cachedSelect(1) 158 | 159 | local something, what = component.robot.detect(side) 160 | if not something or what == "replaceable" or what == "liquid" then 161 | return true -- We can just move into whatever is there. 162 | end 163 | 164 | local brokeSomething 165 | 166 | local info = component.isAvailable("geolyzer") and 167 | component.geolyzer.analyze(side) 168 | if info and info.name == "OpenComputers:robot" then 169 | brokeSomething = true -- Wait for other robot to go away. 170 | os.sleep(0.5) 171 | elseif component.isAvailable("inventory_controller") and emptySlot then 172 | cachedSelect(emptySlot) 173 | component.inventory_controller.equip() -- Save some tool durability. 174 | cachedSelect(1) 175 | brokeSomething = component.robot.swing(side) 176 | cachedSelect(emptySlot) 177 | component.inventory_controller.equip() 178 | cachedSelect(1) 179 | end 180 | if not brokeSomething then 181 | brokeSomething = component.robot.swing(side) 182 | end 183 | until not brokeSomething 184 | end 185 | 186 | -- Force a move towards in the specified direction. 187 | local function forceMove(side, delta) 188 | preMove() 189 | local result = component.robot.move(side) 190 | if result then 191 | distanceToOrigin = distanceToOrigin + delta 192 | postMove() 193 | else 194 | -- Obstructed, try to clear the way. 195 | if side == sides.back then 196 | -- Moving backwards, turn around. 197 | component.robot.turn(left) 198 | component.robot.turn(left) 199 | repeat 200 | dig(sides.forward) 201 | preMove() 202 | until robot.forward() 203 | distanceToOrigin = distanceToOrigin + delta 204 | component.robot.turn(left) 205 | component.robot.turn(left) 206 | postMove() -- Slightly falsifies move cost, but must ensure we're rotated 207 | -- correctly in case postMove() triggers going to maintenance. 208 | else 209 | repeat 210 | dig(side) 211 | preMove() 212 | until component.robot.move(side) 213 | distanceToOrigin = distanceToOrigin + delta 214 | postMove() 215 | end 216 | end 217 | return true 218 | end 219 | 220 | --[[ Navigation ]]------------------------------------------------------------- 221 | 222 | -- Keeps track of our moves to allow "undoing" them for returning to the 223 | -- docking station. Format is a list of moves, represented as tables 224 | -- containing the type of move and distance to move, e.g. 225 | -- {move=sides.back, count=10}, 226 | -- {turn=true, count=2} 227 | -- means we first moved back 10 blocks, then turned around. 228 | local moves = {} 229 | 230 | -- Undo a *single* move, i.e. reduce the count of the latest move type. 231 | local function undoMove(move) 232 | if move.move then 233 | local side = oppositeSides[move.move] 234 | forceMove(side, -1) 235 | else 236 | local direction = not move.turn 237 | component.robot.turn(direction) 238 | end 239 | move.count = move.count - 1 240 | end 241 | 242 | -- Make a turn in the specified direction. 243 | local function pushTurn(direction) 244 | component.robot.turn(direction) 245 | if moves[#moves] and moves[#moves].turn == direction then 246 | moves[#moves].count = moves[#moves].count + 1 247 | else 248 | moves[#moves + 1] = {turn=direction, count=1} 249 | end 250 | return true -- Allows for `return pushMove() and pushTurn() and pushMove()`. 251 | end 252 | 253 | -- Try to make a move towards the specified side. 254 | local function pushMove(side, force) 255 | preMove() 256 | local result, reason = (force and forceMove or component.robot.move)(side, 1) 257 | if result then 258 | if moves[#moves] and moves[#moves].move == side then 259 | moves[#moves].count = moves[#moves].count + 1 260 | else 261 | moves[#moves + 1] = {move=side, count=1} 262 | end 263 | if not force then 264 | distanceToOrigin = distanceToOrigin + 1 265 | end 266 | postMove() 267 | end 268 | return result, reason 269 | end 270 | 271 | -- Undo the most recent move *type*. I.e. will undo all moves of the most 272 | -- recent type (say we moved forwards twice, this will go back twice). 273 | local function popMove() 274 | -- Deep copy the move for returning it. 275 | local move = moves[#moves] and {move=moves[#moves].move, 276 | turn=moves[#moves].turn, 277 | count=moves[#moves].count} 278 | while moves[#moves] and moves[#moves].count > 0 do 279 | undoMove(moves[#moves]) 280 | end 281 | moves[#moves] = nil 282 | return move 283 | end 284 | 285 | -- Get the current top and count values, to be used as a position snapshot 286 | -- that can be restored later on by calling setTop(). 287 | local function getTop() 288 | if moves[#moves] then 289 | return #moves, moves[#moves].count 290 | else 291 | return 0, 0 292 | end 293 | end 294 | 295 | -- Undo some moves based on a stored top and count received from getTop(). 296 | local function setTop(top, count, unsafe) 297 | assert(top >= 0) 298 | assert(top <= #moves) 299 | assert(count >= 0) 300 | assert(top < #moves or count <= moves[#moves].count) 301 | while #moves > top do 302 | if unsafe then 303 | if moves[#moves].move then 304 | distanceToOrigin = distanceToOrigin - moves[#moves].count 305 | end 306 | moves[#moves] = nil 307 | else 308 | popMove() 309 | end 310 | end 311 | local move = moves[#moves] 312 | if move then 313 | while move.count > count do 314 | if unsafe then 315 | move.count = move.count - 1 316 | distanceToOrigin = distanceToOrigin - 1 317 | else 318 | undoMove(move) 319 | end 320 | end 321 | if move.count < 1 then 322 | moves[#moves] = nil 323 | end 324 | end 325 | end 326 | 327 | -- Undo *all* moves made since program start, return the list of moves. 328 | local function popMoves() 329 | local result = {} 330 | local move = popMove() 331 | while move do 332 | table.insert(result, 1, move) 333 | move = popMove() 334 | end 335 | return result 336 | end 337 | 338 | -- Repeat the specified set of moves. 339 | local function pushMoves(moves) 340 | for _, move in ipairs(moves) do 341 | if move.move then 342 | for _ = 1, move.count do 343 | pushMove(move.move, true) 344 | end 345 | else 346 | for _ = 1, move.count do 347 | pushTurn(move.turn) 348 | end 349 | end 350 | end 351 | end 352 | 353 | --[[ Maintenance ]]------------------------------------------------------------ 354 | 355 | -- Energy required to return to docking bay. 356 | local function costToReturn() 357 | -- Overestimate a bit, to account for obstacles such as gravel or mobs. 358 | return 5000 + averageMoveCost * distanceToOrigin * 1.25 359 | end 360 | 361 | -- Checks whether we need maintenance. 362 | local function needsMaintenance() 363 | return not robot.durability() or -- Tool broken? 364 | computer.energy() < costToReturn() or -- Out of juice? 365 | not findTorchSlot() -- No more torches? 366 | end 367 | 368 | -- Drops all inventory contents that are not marked for keeping. 369 | local function dropMinedBlocks() 370 | if component.isAvailable("inventory_controller") then 371 | if not component.inventory_controller.getInventorySize(sides.down) then 372 | io.write("There doesn't seem to be an inventory below me! Waiting to avoid spilling stuffs into the world.\n") 373 | end 374 | repeat os.sleep(5) until component.inventory_controller.getInventorySize(sides.down) 375 | end 376 | io.write("Dropping what I found.\n") 377 | for slot = 1, robot.inventorySize() do 378 | while not keepSlot[slot] and robot.count(slot) > 0 do 379 | cachedSelect(slot) 380 | robot.dropDown() 381 | end 382 | end 383 | cachedSelect(1) 384 | end 385 | 386 | -- Ensures we have a tool with durability. 387 | local function checkTool() 388 | if not robot.durability() then 389 | io.write("Tool is broken, getting a new one.\n") 390 | if component.isAvailable("inventory_controller") then 391 | cachedSelect(findEmptySlot()) -- Select an empty slot for working. 392 | repeat 393 | component.inventory_controller.equip() -- Drop whatever's in the tool slot. 394 | while robot.count() > 0 do 395 | robot.dropDown() 396 | end 397 | robot.suckUp(1) -- Pull something from above and equip it. 398 | component.inventory_controller.equip() 399 | until robot.durability() 400 | cachedSelect(1) 401 | else 402 | -- Can't re-equip autonomously, wait for player to give us a tool. 403 | io.write("HALP! I need a new tool.\n") 404 | repeat 405 | event.pull(10, "inventory_changed") 406 | until robot.durability() 407 | end 408 | end 409 | end 410 | 411 | -- Ensures we have some torches. 412 | local function checkTorches() 413 | -- First, clean up our list and look for empty slots. 414 | io.write("Getting my fill of torches.\n") 415 | local oldTorchSlots = torchSlots 416 | torchSlots = {} 417 | for _, slot in ipairs(oldTorchSlots) do 418 | keepSlot[slot] = nil 419 | if robot.count(slot) > 0 then 420 | torchSlots[#torchSlots + 1] = slot 421 | end 422 | end 423 | while #torchSlots < torchStacks() do 424 | local slot = findEmptySlot() 425 | if not slot then 426 | break -- This should never happen... 427 | end 428 | torchSlots[#torchSlots + 1] = slot 429 | end 430 | -- Then fill the slots with torches. 431 | robot.turnLeft() 432 | for _, slot in ipairs(torchSlots) do 433 | keepSlot[slot] = true 434 | if robot.space(slot) > 0 then 435 | cachedSelect(slot) 436 | repeat 437 | local before = robot.space() 438 | robot.suck(robot.space()) 439 | if robot.space() == before then 440 | os.sleep(5) -- Don't busy idle. 441 | end 442 | until robot.space() < 1 443 | cachedSelect(1) 444 | end 445 | end 446 | robot.turnRight() 447 | end 448 | 449 | -- Recharge our batteries. 450 | local function recharge() 451 | io.write("Waiting until my batteries are full.\n") 452 | while computer.maxEnergy() - computer.energy() > 100 do 453 | os.sleep(1) 454 | end 455 | end 456 | 457 | -- Go back to the docking bay for general maintenance if necessary. 458 | local function gotoMaintenance(force) 459 | if not force and not needsMaintenance() then 460 | return -- No need yet. 461 | end 462 | 463 | -- Save some values for later, temporarily remove onMove callback. 464 | local returnCost = costToReturn() 465 | local moveCallback = onMove 466 | onMove = nil 467 | 468 | local top, count = getTop() 469 | 470 | io.write("Going back for maintenance!\n") 471 | local moves = popMoves() 472 | 473 | assert(distanceToOrigin == 0) 474 | 475 | dropMinedBlocks() 476 | checkTool() 477 | checkTorches() 478 | recharge() -- Last so we can charge some during the other operations. 479 | 480 | if moves and #moves > 0 then 481 | if returnCost * 2 > computer.maxEnergy() and 482 | not options.f and 483 | not prompt("Going back will cost me half my energy. There's a good chance I will not return. Do you want to send me to my doom anyway?") 484 | then 485 | os.exit() 486 | end 487 | io.write("Returning to where I left off.\n") 488 | pushMoves(moves) 489 | end 490 | 491 | local newTop, newCount = getTop() 492 | assert(top == newTop) 493 | assert(count == newCount) 494 | 495 | onMove = moveCallback 496 | end 497 | 498 | --[[ Mining ]]----------------------------------------------------------------- 499 | 500 | -- Move towards the specified direction, digging out blocks as necessary. 501 | -- This is a "soft" version of forceMove in that it will try to clear its path, 502 | -- but fail if it can't. 503 | local function move(side) 504 | local result, reason, retry 505 | repeat 506 | retry = false 507 | if side ~= sides.back then 508 | retry = dig(side, gotoMaintenance) 509 | else 510 | gotoMaintenance() 511 | end 512 | result, reason = pushMove(side) 513 | until result or not retry 514 | return result, reason 515 | end 516 | 517 | -- Turn to face the specified, relative orientation. 518 | local function turnTowards(side) 519 | if side == sides.left then 520 | pushTurn(left) 521 | elseif side == sides.right then 522 | pushTurn(right) 523 | elseif side == sides.back then 524 | pushTurn(left) 525 | pushTurn(left) 526 | end 527 | end 528 | 529 | --[[ On move callbacks ]]------------------------------------------------------ 530 | 531 | -- Start automatically placing torches in the configured interval. 532 | local function beginPlacingTorches() 533 | local counter = 2 534 | onMove = function() 535 | if counter < 1 then 536 | if placeTorch() then 537 | counter = torchInverval 538 | end 539 | else 540 | counter = counter - 1 541 | end 542 | end 543 | end 544 | 545 | -- Start digging out the block below us after each move. 546 | local function beginDigginTrench() 547 | onMove = function() 548 | dig(sides.down, gotoMaintenance) 549 | end 550 | end 551 | 552 | -- Stop automatically placing torches. 553 | local function clearMoveCallback() 554 | onMove = nil 555 | end 556 | 557 | --[[ Moving ]]----------------------------------------------------------------- 558 | 559 | -- Dig out any interesting ores adjacent to the current position, recursively. 560 | -- POST: back to the starting position and facing. 561 | local function digVein(maxDepth) 562 | if maxDepth < 1 then return end 563 | for _, side in ipairs(sides) do 564 | local sideIdx = sides[side] 565 | -- skip unknown side 566 | if oppositeSides[side] and shouldMine(component.geolyzer.analyze(sideIdx)) then 567 | local top, count = getTop() 568 | turnTowards(sideIdx) 569 | if sideIdx == sides.up or sideIdx == sides.down then 570 | move(sideIdx) 571 | else 572 | move(sides.forward) 573 | end 574 | digVein(maxDepth - 1) 575 | setTop(top, count) 576 | end 577 | end 578 | end 579 | 580 | -- Dig out any interesting ores adjacent to the current position, recursively. 581 | -- Also checks blocks adjacent to above block in exhaustive mode. 582 | -- POST: back at the starting position and facing. 583 | local function digVeins(exhaustive) 584 | if component.isAvailable("geolyzer") then 585 | digVein(maxVeinRecursion) 586 | if exhaustive and move(sides.up) then 587 | digVein(maxVeinRecursion) 588 | popMove() 589 | end 590 | end 591 | end 592 | 593 | -- Dig a 1x2 tunnel of the specified length. Checks for ores. 594 | -- Also checks upper row for ores in exhaustive mode. 595 | -- PRE: bottom front of tunnel to dig. 596 | -- POST: at the end of the tunnel. 597 | local function dig1x2(length, exhaustive) 598 | while length > 0 and move(sides.forward) do 599 | dig(sides.up, gotoMaintenance) 600 | digVeins(exhaustive) 601 | length = length - 1 602 | end 603 | return length < 1 604 | end 605 | 606 | -- Dig a 1x3 tunnel of the specified length. 607 | -- PRE: center front of tunnel to dig. 608 | -- POST: at the end of the tunnel. 609 | local function dig1x3(length) 610 | while length > 0 and move(sides.forward) do 611 | dig(sides.up, gotoMaintenance) 612 | dig(sides.down, gotoMaintenance) 613 | length = length - 1 614 | end 615 | return length < 1 616 | end 617 | 618 | -- Dig out a main shaft. 619 | -- PRE: bottom front of main shaft. 620 | -- POST: bottom front of main shaft. 621 | local function digMainShaft(length) 622 | io.write("Digging main shaft.\n") 623 | 624 | if not move(sides.up) then 625 | return false 626 | end 627 | 628 | local top, count = getTop() 629 | 630 | if not (dig1x3(length) and 631 | pushTurn(left) and 632 | dig1x3(1) and 633 | pushTurn(left) and 634 | dig1x3(length - 1) and 635 | (placeTorch() or true) and -- Just keep going... 636 | pushTurn(left) and 637 | dig1x3(1)) 638 | then 639 | return false 640 | end 641 | 642 | -- Create snapshot for shortcut below. 643 | local midTop, midCount = getTop() 644 | 645 | if not (dig1x3(1) and 646 | pushTurn(left) and 647 | dig1x3(length - 1)) 648 | then 649 | return false 650 | end 651 | placeTorch() 652 | 653 | -- Shortcut: manually move back to start, do an unsafe setTop. 654 | -- Otherwise we'd have to retrace all three rows. 655 | setTop(midTop, midCount) 656 | if pushTurn(left) and move(sides.back) then 657 | setTop(top, count, true) 658 | return true 659 | end 660 | 661 | return false 662 | end 663 | 664 | -- Dig all shafts in one cardinal direction (the one we're facing). 665 | -- PRE: bottom front of a main shaft. 666 | -- POST: bottom front of a main shaft. 667 | local function digShafts(length) 668 | local top, count = getTop() -- Remember start of main shaft. 669 | local ok = digMainShaft(length) 670 | setTop(top, count) 671 | if not ok then 672 | io.write("Failed digging main shaft, skipping.\n") 673 | return 674 | end 675 | 676 | io.write("Beginning work on side shafts.\n") 677 | for i = shaftInterval, length, shaftInterval do 678 | io.write("Working on shafts #" .. (i / shaftInterval) .. ".\n") 679 | 680 | if not dig1x2(shaftInterval) then -- Move to height of shaft. 681 | break 682 | end 683 | local sideTop, sideCount = getTop() -- Remember position. 684 | 685 | pushTurn(left) -- Dig left shaft. 686 | dig1x2(i + 2, true) 687 | beginPlacingTorches() 688 | setTop(sideTop, sideCount) 689 | clearMoveCallback() 690 | 691 | pushTurn(right) -- Dig right shaft. 692 | dig1x2(i + 2, true) 693 | beginPlacingTorches() 694 | setTop(sideTop, sideCount) 695 | clearMoveCallback() 696 | end 697 | 698 | -- Go back to start of main shaft. Dig out the center of the main shaft 699 | -- while we're at it, so we break through the ceiling between levels. 700 | beginDigginTrench() 701 | setTop(top, count) 702 | clearMoveCallback() 703 | end 704 | 705 | -- Moves to the next main shaft, clockwise. 706 | -- PRE: bottom front of nth main shaft. 707 | -- POST: bottom front of (n+1)th main shaft. 708 | local function gotoNextMainShaft() 709 | return pushTurn(right) and 710 | dig1x2(2) and 711 | pushTurn(right) and 712 | dig1x2(2) and 713 | pushTurn(left) 714 | end 715 | 716 | --[[ Main ]]------------------------------------------------------------------- 717 | 718 | local function main(radius, levels, full) 719 | -- We dig tunnels every three blocks, to have a spacing of 720 | -- two blocks between them (so we don't really have to follow 721 | -- veins but can just break the blocks in the wall that are 722 | -- interesting to us). So adjust the length accordingly. 723 | radius = radius - radius % shaftInterval 724 | 725 | -- Flag slots that contain something as do-not-drop and check 726 | -- that we have some free inventory space at all. 727 | local freeSlots = robot.inventorySize() 728 | for slot = 1, robot.inventorySize() do 729 | if robot.count(slot) > 0 then 730 | keepSlot[slot] = true 731 | freeSlots = freeSlots - 1 732 | end 733 | end 734 | if freeSlots < 2 + torchStacks() then -- Place for mined blocks + torches. 735 | io.write("Sorry, but I need more empty inventory space to work.\n") 736 | os.exit() 737 | end 738 | gotoMaintenance(true) 739 | 740 | if not move(sides.forward) then 741 | io.write("Exit from docking bay obstructed, aborting.\n") 742 | os.exit() 743 | end 744 | 745 | for level = 1, levels do 746 | if level > 1 then 747 | for _ = 1, 4 do 748 | if not move(sides.down) then 749 | io.write("Access to level " .. level .. " obstructed, aborting.\n") 750 | popMoves() 751 | gotoMaintenance(true) 752 | os.exit() 753 | end 754 | end 755 | end 756 | 757 | local top, count = getTop() 758 | 759 | for shaft = 1, full and 4 or 1 do 760 | if shaft > 1 and not gotoNextMainShaft() then 761 | break 762 | end 763 | digShafts(radius) 764 | end 765 | if full then 766 | gotoNextMainShaft() -- Finish the circle. 767 | end 768 | 769 | setTop(top, count) 770 | end 771 | 772 | io.write("All done! Going home to clean up.\n") 773 | popMoves() 774 | gotoMaintenance(true) 775 | end 776 | 777 | if options.h or options.help then 778 | io.write("Usage: miner [-hsf] [radius [levels [full]]]\n") 779 | io.write(" -h: this help listing.\n") 780 | io.write(" -s: start without prompting.\n") 781 | io.write(" -f: force mining to continue even if max\n") 782 | io.write(" fuel may be insufficient to return.\n") 783 | io.write(" radius: the radius in blocks of the area to\n") 784 | io.write(" mine. Adjusted to be a multiple of\n") 785 | io.write(" three. Default: 9.\n") 786 | io.write(" levels: the number of vertical levels to mine.\n") 787 | io.write(" Default: 1.\n") 788 | io.write(" full: whether to mine a full level (all four\n") 789 | io.write(" cardinal directions). Default: false.\n") 790 | os.exit() 791 | end 792 | 793 | local radius = tonumber(args[1]) or 9 794 | local levels = tonumber(args[2]) or 1 795 | local full = args[3] and args[3] == "true" or args[3] == "yes" 796 | 797 | io.write("Will mine " .. levels .. " levels in a radius of " .. radius .. ".\n") 798 | if full then 799 | io.write("Will mine all four cardinal directions.\n") 800 | end 801 | if not component.isAvailable("geolyzer") then 802 | io.write("Installing a geolyzer upgrade is strongly recommended.\n") 803 | end 804 | if not component.isAvailable("inventory_controller") then 805 | io.write("Installing an inventory controller upgrade is strongly recommended.\n") 806 | end 807 | 808 | io.write("I'll drop mined out stuff below me.\n") 809 | io.write("I'll be looking for torches on my left.\n") 810 | if component.isAvailable("inventory_controller") then 811 | io.write("I'll try to get new tools from above me.\n") 812 | else 813 | io.write("You'll need to manually provide me with new tools if they break.\n") 814 | end 815 | 816 | io.write("Run with -h or --help for parameter info.\n") 817 | 818 | if options.s or prompt("Shall we begin?") then 819 | main(radius, levels, full) 820 | end 821 | -------------------------------------------------------------------------------- /models/button.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Big Button", 3 | emitRedstone = true, -- if this is false it behaves like a door 4 | buttonMode = true, -- if this is false it behaves like a lever 5 | shapes={ 6 | {1,1,14,15,15,16,texture="lapis_block"}, 7 | {4,10,13,6,12,14,texture="sandstone_top"}, 8 | {10,10,13,12,12,14,texture="sandstone_top"}, 9 | {2,4,13,4,6,14,texture="sandstone_top"}, 10 | {12,4,13,14,6,14,texture="sandstone_top"}, 11 | {4,2,13,12,4,14,texture="sandstone_top"}, 12 | 13 | {1,1,14,15,15,16,texture="lapis_block",state=true}, 14 | {4,10,13,6,12,14,texture="sandstone_top",state=true}, 15 | {10,10,13,12,12,14,texture="sandstone_top",state=true}, 16 | {2,2,13,4,4,14,texture="sandstone_top",state=true}, 17 | {12,2,13,14,4,14,texture="sandstone_top",state=true}, 18 | {4,4,13,12,6,14,texture="sandstone_top",state=true} 19 | } 20 | } -------------------------------------------------------------------------------- /models/cc.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label="Companion Cube", 3 | shapes={ 4 | -- Center 5 | {2,2,2,14,14,14, texture="wool_colored_gray"}, 6 | 7 | -- Corners 8 | { 0, 0, 0, 5, 5, 5, texture="wool_colored_silver"}, 9 | { 0, 0,11, 5, 5,16, texture="wool_colored_silver"}, 10 | { 0,11, 0, 5,16, 5, texture="wool_colored_silver"}, 11 | { 0,11,11, 5,16,16, texture="wool_colored_silver"}, 12 | {11, 0, 0,16, 5, 5, texture="wool_colored_silver"}, 13 | {11, 0,11,16, 5,16, texture="wool_colored_silver"}, 14 | {11,11, 0,16,16, 5, texture="wool_colored_silver"}, 15 | {11,11,11,16,16,16, texture="wool_colored_silver"}, 16 | 17 | -- Edges 18 | { 1, 6, 1, 3,10, 3, texture="wool_colored_silver"}, 19 | { 1, 6,13, 3,10,15, texture="wool_colored_silver"}, 20 | {13, 6, 1,15,10, 3, texture="wool_colored_silver"}, 21 | {13, 6,13,15,10,15, texture="wool_colored_silver"}, 22 | { 6, 1, 1,10, 3, 3, texture="wool_colored_silver"}, 23 | { 6, 1,13,10, 3,15, texture="wool_colored_silver"}, 24 | { 6,13, 1,10,15, 3, texture="wool_colored_silver"}, 25 | { 6,13,13,10,15,15, texture="wool_colored_silver"}, 26 | { 1, 1, 6, 3, 3,10, texture="wool_colored_silver"}, 27 | {13, 1, 6,15, 3,10, texture="wool_colored_silver"}, 28 | { 1,13, 6, 3,15,10, texture="wool_colored_silver"}, 29 | {13,13, 6,15,15,10, texture="wool_colored_silver"}, 30 | 31 | -- Centers 32 | { 1, 6, 6,15,10,10, texture="wool_colored_magenta"}, 33 | { 6, 6, 1,10,10,15, texture="wool_colored_magenta"}, 34 | { 6, 1, 6,10,15,10, texture="wool_colored_magenta"} 35 | } 36 | } -------------------------------------------------------------------------------- /models/cover.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Cover", 3 | tooltip = "§7A simple cover.", 4 | shapes={{0,0,0,16,4,16,texture="planks_oak"}} 5 | } -------------------------------------------------------------------------------- /models/diamond_block.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Block of Diamond", 3 | shapes = {{0,0,0,16,16,16, texture="diamond_block"}} 4 | } -------------------------------------------------------------------------------- /models/example.3dm: -------------------------------------------------------------------------------- 1 | { 2 | -- This is the label of the model, i.e. the name of the item when the print 3 | -- is in item form (i.e. the name you'll see in the item's tooltip). 4 | -- If this is not set, the default name will used ("3D Print"). 5 | label = "Example Model", 6 | 7 | -- This is the tooltip of the model, i.e. the text displayed in the item's 8 | -- tooltip. If this is not set, the item won't have a detailed tooltip. 9 | tooltip = "This is an example model, demonstrating all the features.", 10 | 11 | lightLevel = 0, --[[ The amount of light the printed block should emit. 12 | By default, values in the interval [0,8] are 13 | possible. To create blocks with a higher light 14 | level, craft the printed block with some glowstone 15 | dust, allowing them to reach up to the maximum 16 | light level, 15. This is only available in 17 | OpenComputers version 1.5.7 and later. ]] 18 | 19 | emitRedstone = false, --[[ If this is false it behaves like a door, i.e. 20 | it switches to the model's active state when 21 | a redstone signal goes into the block - if there 22 | is an active state. 23 | If this is true it behaves like a lever or button, 24 | i.e. it emits a redstone signal when in its active 25 | state (set via a player right-clicking the block). 26 | It will not react to redstone if it is an emitter. 27 | The default is false. 28 | Starting with version 1.5.7 of OpenComputers, it is 29 | also possible to provide a number in the interval 30 | [0,15] here, where 0 is equivalent to `false`, and 31 | 15 is equivalent to `true`. Arbitrary output 32 | signal strength, i.e. values other than 0 or 15, 33 | will require more material to print. ]] 34 | 35 | buttonMode = false, --[[ If this is false, the model behaves like a door or 36 | lever, i.e. it will remain in its active state 37 | until there is no redstone signal or it is 38 | activated again by a player. 39 | If this is true, the model will automatically 40 | return to its inactive if it was activated by a 41 | redstone signal or a player, i.e. it will behave 42 | like a button. 43 | The default is false. ]] 44 | 45 | collidable = {true, true}, --[[ Determines whether the states of the print are 46 | collidable, i.e. whether players and other 47 | entities collide with them. If false, players and 48 | entities will be able to walk right through the 49 | respective state of the print. This way you can 50 | have a block that becomes passable when in its 51 | alternate state, or make a secret passage. 52 | The default is true for both states. ]] 53 | 54 | -- This is the list of shapes defining how the model looks. There has to be 55 | -- at least one shape for the model's inactive state for the model to be 56 | -- valid. Shapes cannot be "flat", i.e. they have to have a volume. In other 57 | -- words, the minimum and maximum coordinate on a single axis must not be 58 | -- identical. 59 | -- Each shape is declared as 6 numbers, those being the minimum and maximum 60 | -- coordinates of the corners of the box represented by the shape (shapes are 61 | -- axis aligned bounding boxes), as such: minX, minY, minZ, maxX, maxY, maxZ. 62 | -- When looking onto a model, right is positive X, up is positive Y and back 63 | -- is positive Z. 64 | -- Additionally, a texture is required per shape. To get a name use the 65 | -- Texture Picker tool on blocks in the game. As a special case you may set 66 | -- the texture name to an empty string, in which case the shape will be fully 67 | -- transparent. You can use this in combination with `collidable` (see above) 68 | -- to make a print completely "vanish", as has been requested for special 69 | -- door constructions, for example. 70 | -- 71 | -- Models can have two states, the inactive state, which is the default, and 72 | -- an optional, active state, which is shown when the block is either 73 | -- activated (right-clicked) by a player, or when `emitRedstone = false` and 74 | -- a redstone signal goes into the block. To assign a shape to the active 75 | -- state, set `state = true` in a shape (default is false). 76 | shapes = { 77 | -- A shape going from <0, 0, 0> (left bottom front) to <8, 8, 8> (middle), 78 | -- using the texture of a Lapis Laszuli Block. 79 | { 0, 0, 0, 8, 8, 8, texture = "lapis_block" }, 80 | 81 | -- A shape going from <8, 8, 8> (middle) to <16, 16, 16> (right top back), 82 | -- using the oak leaves texture and a green tint and assigned to the 83 | -- model's active state. 84 | { 8, 8, 8, 16, 16, 16, texture = "leaves_oak", state = true, tint = 0x48B518 } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /models/netherportal.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {2,0,7,14,3,10,texture="obsidian"}, 3 | {2,3,7,5,13,10,texture="obsidian"}, 4 | {11,3,7,14,13,10,texture="obsidian"}, 5 | {2,13,7,14,16,10,texture="obsidian"}, 6 | {5,3,8,11,13,9,texture="portal"} 7 | }} -------------------------------------------------------------------------------- /models/pillar_base.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {0,0,0,16,8,16,texture="quartz_block_side"}, 3 | {4,8,4,12,16,12,texture="quartz_block_side"}, 4 | {3,8,1,5,16,15,texture="quartz_block_side"}, 5 | {7,8,1,9,16,15,texture="quartz_block_side"}, 6 | {11,8,1,13,16,15,texture="quartz_block_side"}, 7 | {1,8,3,15,16,5,texture="quartz_block_side"}, 8 | {1,8,7,15,16,9,texture="quartz_block_side"}, 9 | {1,8,11,15,16,13,texture="quartz_block_side"} 10 | }} -------------------------------------------------------------------------------- /models/pillar_mid.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {4,0,4,12,16,12,texture="quartz_block_side"}, 3 | {3,0,1,5,16,15,texture="quartz_block_side"}, 4 | {7,0,1,9,16,15,texture="quartz_block_side"}, 5 | {11,0,1,13,16,15,texture="quartz_block_side"}, 6 | {1,0,3,15,16,5,texture="quartz_block_side"}, 7 | {1,0,7,15,16,9,texture="quartz_block_side"}, 8 | {1,0,11,15,16,13,texture="quartz_block_side"} 9 | }} -------------------------------------------------------------------------------- /models/pillar_top.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {0,8,0,16,16,16,texture="quartz_block_side"}, 3 | {4,0,4,12,8,12,texture="quartz_block_side"}, 4 | {3,0,1,5,8,15,texture="quartz_block_side"}, 5 | {7,0,1,9,8,15,texture="quartz_block_side"}, 6 | {11,0,1,13,8,15,texture="quartz_block_side"}, 7 | {1,0,3,15,8,5,texture="quartz_block_side"}, 8 | {1,0,7,15,8,9,texture="quartz_block_side"}, 9 | {1,0,11,15,8,13,texture="quartz_block_side"} 10 | }} -------------------------------------------------------------------------------- /models/pot.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Pot", 3 | tooltip = "§7A slightly larger but useless flower pot.", 4 | shapes={ 5 | {0,0,0,16,2,16,texture="brick"}, 6 | {0,2,0,2,12,16,texture="brick"}, 7 | {14,2,0,16,12,16,texture="brick"}, 8 | {2,2,0,14,12,2,texture="brick"}, 9 | {2,2,14,14,12,16,texture="brick"}, 10 | {2,2,2,14,10,14,texture="dirt"} 11 | } 12 | } -------------------------------------------------------------------------------- /models/slab.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Oak Wood Slab", 3 | shapes={{0,0,0,16,8,16,texture="planks_oak"}} 4 | } -------------------------------------------------------------------------------- /models/stairs.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Stone Brick Stairs", 3 | shapes = { 4 | {0, 0, 0, 16, 8, 16, texture = "stonebrick"}, 5 | {0, 8, 8, 16, 16, 16, texture = "stonebrick"} 6 | } 7 | } -------------------------------------------------------------------------------- /models/torch.3dm: -------------------------------------------------------------------------------- 1 | { 2 | label = "Redstone Torch", 3 | emitRedstone = 5, 4 | lightLevel = 8, 5 | shapes = {{7,0,7,9,9,9, texture="redstone_torch_on"}} 6 | } -------------------------------------------------------------------------------- /models/trapdoor.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {0,13,0,16,16,16,texture="trapdoor"}, 3 | {0,0,13,16,16,16,texture="trapdoor",state=true} 4 | }} -------------------------------------------------------------------------------- /models/tree.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {7,0,7,9,6,9,texture="log_oak"}, 3 | {3,6,3,13,10,13,texture="wool_colored_green"}, 4 | {5,10,5,11,12,11,texture="wool_colored_green"}, 5 | {5,12,7,11,14,9,texture="wool_colored_green"}, 6 | {7,12,5,9,14,11,texture="wool_colored_green"} 7 | }} -------------------------------------------------------------------------------- /models/tree2.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {7,0,7,9,8,9,texture="log_oak"}, 3 | {3,6,3,13,10,13,texture="leaves_oak",tint=0x48B518}, 4 | {5,10,5,11,12,11,texture="leaves_oak",tint=0x48B518}, 5 | {5,12,7,11,14,9,texture="leaves_oak",tint=0x48B518}, 6 | {7,12,5,9,14,11,texture="leaves_oak",tint=0x48B518} 7 | }} -------------------------------------------------------------------------------- /models/villager.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {6,0,7,10,5,9,texture="wool_colored_silver"}, 3 | {6,5,7,10,9,9,texture="wool_colored_green"}, 4 | {5,7,7,11,12,9,texture="wool_colored_green"}, 5 | {5,7,6,11,9,7,texture="wool_colored_green"}, 6 | {7,12,7,9,15,9,texture="wool_colored_pink"} 7 | }} -------------------------------------------------------------------------------- /models/well.3dm: -------------------------------------------------------------------------------- 1 | {shapes={ 2 | {0,0,0,16,2,16,texture="sand"}, 3 | {0,2,0,6,4,6,texture="sand"}, 4 | {10,2,0,16,4,6,texture="sand"}, 5 | {0,2,10,6,4,16,texture="sand"}, 6 | {10,2,10,16,4,16,texture="sand"}, 7 | {3,4,3,6,10,6,texture="sand"}, 8 | {10,4,3,13,10,6,texture="sand"}, 9 | {3,4,10,6,10,13,texture="sand"}, 10 | {10,4,10,13,10,13,texture="sand"}, 11 | {3,10,3,13,12,13,texture="sand"}, 12 | {6,12,6,10,14,10,texture="sand"}, 13 | {3,2,3,13,4,13,texture="water_still"} 14 | }} -------------------------------------------------------------------------------- /noise.lua: -------------------------------------------------------------------------------- 1 | -- Perlin noise and more in pure Lua. 2 | -- Found this on stackoverflow but can't find the URL anymore. 3 | local noise = {} 4 | 5 | local p = {} 6 | local permutation = {151,160,137,91,90,15, 7 | 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, 8 | 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, 9 | 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, 10 | 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, 11 | 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, 12 | 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, 13 | 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, 14 | 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, 15 | 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, 16 | 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, 17 | 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, 18 | 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 19 | } 20 | 21 | for i = 0, 255 do 22 | p[i] = permutation[i + 1] 23 | p[256 + i] = permutation[i + 1] 24 | end 25 | 26 | local function fade(t) 27 | return t * t * t * (t * (t * 6 - 15) + 10) 28 | end 29 | 30 | local function lerp(t, a, b) 31 | return a + t * (b - a) 32 | end 33 | 34 | local function grad(hash, x, y, z) 35 | local h, u, v = hash % 16 36 | if h < 8 then u = x else u = y end 37 | if h < 4 then v = y elseif h == 12 or h == 14 then v = x else v = z end 38 | local r 39 | if h % 2 == 0 then r = u else r = -u end 40 | if h % 4 == 0 then r = r + v else r = r - v end 41 | return r 42 | end 43 | 44 | function noise.perlin(x, y, z) 45 | y = y or 0 46 | z = z or 0 47 | local X = math.floor(x % 255) 48 | local Y = math.floor(y % 255) 49 | local Z = math.floor(z % 255) 50 | x = x - math.floor(x) 51 | y = y - math.floor(y) 52 | z = z - math.floor(z) 53 | local u = fade(x) 54 | local v = fade(y) 55 | local w = fade(z) 56 | 57 | A = p[X ] + Y 58 | AA = p[A ] + Z 59 | AB = p[A + 1] + Z 60 | B = p[X + 1] + Y 61 | BA = p[B ] + Z 62 | BB = p[B + 1] + Z 63 | 64 | return lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), 65 | grad(p[BA ], x - 1, y , z )), 66 | lerp(u, grad(p[AB ], x , y - 1, z ), 67 | grad(p[BB ], x - 1, y - 1, z ))), 68 | lerp(v, lerp(u, grad(p[AA + 1], x , y , z - 1), 69 | grad(p[BA + 1], x - 1, y , z - 1)), 70 | lerp(u, grad(p[AB + 1], x , y - 1, z - 1), 71 | grad(p[BB + 1], x - 1, y - 1, z - 1)))) 72 | end 73 | 74 | function noise.fbm(x, y, z, octaves, lacunarity, gain) 75 | octaves = octaves or 8 76 | lacunarity = lacunarity or 2 77 | gain = gain or 0.5 78 | local amplitude = 1.0 79 | local frequency = 1.0 80 | local sum = 0.0 81 | for i = 0, octaves do 82 | sum = sum + amplitude * noise.perlin(x * frequency, y * frequency, z * frequency) 83 | amplitude = amplitude * gain 84 | frequency = frequency * lacunarity 85 | end 86 | return sum 87 | end 88 | 89 | return noise -------------------------------------------------------------------------------- /print3d-cc.lua: -------------------------------------------------------------------------------- 1 | local printer = peripheral.find("printer3d") 2 | 3 | local args = {...} 4 | if #args < 1 then 5 | io.write("Usage: print3d FILE [count]\n") 6 | return 0 7 | end 8 | local count = 1 9 | if #args > 1 then 10 | count = assert(tonumber(args[2]), tostring(args[2]) .. " is not a valid count") 11 | end 12 | 13 | local file = fs.open(args[1], "r") 14 | if not file then 15 | io.write("Failed opening file\n") 16 | return 1 17 | end 18 | 19 | local rawdata = file.readAll() 20 | file.close() 21 | local data, reason = loadstring("return " .. rawdata) 22 | if not data then 23 | io.write("Failed loading model: " .. reason .. "\n") 24 | return 2 25 | end 26 | data = data() 27 | 28 | io.write("Configuring...\n") 29 | 30 | printer.reset() 31 | if data.label then 32 | printer.setLabel(data.label) 33 | end 34 | if data.tooltip then 35 | printer.setTooltip(data.tooltip) 36 | end 37 | if data.lightLevel and printer.setLightLevel then -- as of OC 1.5.7 38 | printer.setLightLevel(data.lightLevel) 39 | end 40 | if data.emitRedstone then 41 | printer.setRedstoneEmitter(data.emitRedstone) 42 | end 43 | if data.buttonMode then 44 | printer.setButtonMode(data.buttonMode) 45 | end 46 | if data.collidable and printer.setCollidable then 47 | printer.setCollidable(not not data.collidable[1], not not data.collidable[2]) 48 | end 49 | for i, shape in ipairs(data.shapes or {}) do 50 | local result, reason = printer.addShape(shape[1], shape[2], shape[3], shape[4], shape[5], shape[6], shape.texture, shape.state, shape.tint) 51 | if not result then 52 | io.write("Failed adding shape: " .. tostring(reason) .. "\n") 53 | end 54 | end 55 | 56 | io.write("Label: '" .. (printer.getLabel() or "not set") .. "'\n") 57 | io.write("Tooltip: '" .. (printer.getTooltip() or "not set") .. "'\n") 58 | if printer.getLightLevel then -- as of OC 1.5.7 59 | io.write("Light level: " .. printer.getLightLevel() .. "\n") 60 | end 61 | io.write("Redstone level: " .. select(2, printer.isRedstoneEmitter()) .. "\n") 62 | io.write("Button mode: " .. tostring(printer.isButtonMode()) .. "\n") 63 | if printer.isCollidable then -- as of OC 1.5.something 64 | io.write("Collidable: " .. tostring(select(1, printer.isCollidable())) .. "/" .. tostring(select(2, printer.isCollidable())) .. "\n") 65 | end 66 | io.write("Shapes: " .. printer.getShapeCount() .. " inactive, " .. select(2, printer.getShapeCount()) .. " active\n") 67 | 68 | local result, reason = printer.commit(count) 69 | if result then 70 | io.write("Job successfully committed!\n") 71 | else 72 | io.write("Failed committing job: " .. tostring(reason) .. "\n") 73 | end 74 | -------------------------------------------------------------------------------- /print3d-view.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | local event = require("event") 3 | local keyboard = require("keyboard") 4 | local shell = require("shell") 5 | local term = require("term") 6 | local unicode = require("unicode") 7 | local raytracer = require("raytracer") 8 | 9 | local args = shell.parse(...) 10 | if #args < 1 then 11 | io.write("Usage: print3d-view FILE [fov]\n") 12 | os.exit(0) 13 | end 14 | 15 | -- model loading 16 | 17 | local file, reason = io.open(args[1], "r") 18 | if not file then 19 | io.stderr:write("Failed opening file: " .. reason .. "\n") 20 | os.exit(1) 21 | end 22 | 23 | local rawdata = file:read("*all") 24 | file:close() 25 | local data, reason = load("return " .. rawdata) 26 | if not data then 27 | io.stderr:write("Failed loading model: " .. reason .. "\n") 28 | os.exit(2) 29 | end 30 | data = data() 31 | 32 | -- set up raytracer 33 | 34 | local rt = raytracer.new() 35 | rt.camera.position={-22+8,20+8,-22+8} 36 | rt.camera.target={8,8,8} 37 | rt.camera.fov=tonumber(args[2]) or 90 38 | 39 | local state 40 | local function setState(value) 41 | if state ~= value then 42 | state = value 43 | rt.model = {} 44 | for _, shape in ipairs(data.shapes or {}) do 45 | if not not shape.state == state then 46 | table.insert(rt.model, shape) 47 | end 48 | end 49 | if state and #rt.model < 1 then -- no shapes for active state 50 | setState(false) 51 | end 52 | end 53 | end 54 | setState(false) 55 | 56 | -- set up gpu 57 | 58 | local gpu = component.gpu 59 | local cfg, cbg 60 | local function setForeground(color) 61 | if cfg ~= color then 62 | gpu.setForeground(color) 63 | cfg = color 64 | end 65 | end 66 | local function setBackground(color) 67 | if cbg ~= color then 68 | gpu.setBackground(color) 69 | cbg = color 70 | end 71 | end 72 | 73 | -- helper functions 74 | 75 | local function vrotate(v, origin, angle) 76 | local x, y = v[1]-origin[1], v[3]-origin[3] 77 | local s = math.sin(angle) 78 | local c = math.cos(angle) 79 | 80 | local rotx = x * c + y * s 81 | local roty = -x * s + y * c 82 | return {rotx+origin[1], v[2], roty+origin[3]} 83 | end 84 | 85 | local function ambient(normal) 86 | if math.abs(normal[1]) > 0.5 then 87 | return 0.6 88 | elseif math.abs(normal[3]) > 0.5 then 89 | return 0.8 90 | elseif normal[2] > 0 then 91 | return 1.0 92 | else 93 | return 0.4 94 | end 95 | end 96 | 97 | local function hash(str) 98 | local result = 7 99 | for i=1,#str do 100 | result = (result*31 + string.byte(str, i))%0xFFFFFFFF 101 | end 102 | return result 103 | end 104 | 105 | local function multiply(color, brightness) 106 | local r,b,g=(color/2^16)%256,(color/2^8)%256,color%256 107 | r = r*brightness 108 | g = g*brightness 109 | b = b*brightness 110 | return r*2^16+g*2^8+b 111 | end 112 | 113 | local palette = {0x0000FF, 0x00FF00, 0x00FFFF, 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF} 114 | 115 | -- render model 116 | while true do 117 | setForeground(0x000000) 118 | setBackground(0x000000) 119 | local rx, ry = gpu.getResolution() 120 | gpu.fill(1, 1, rx, ry, unicode.char(0x2580)) 121 | 122 | rt:render(rx, ry*2, function(x, y, shape, normal) 123 | local sx, sy = x, math.ceil(y / 2) 124 | local ch, fg, bg = gpu.get(sx, sy) 125 | local brightness = ambient(normal) 126 | local color = multiply(data.palette and data.palette[shape.texture] or palette[hash(shape.texture or "") % #palette + 1], brightness) 127 | if color == 0x000000 then return end 128 | if y % 2 == 1 then 129 | setBackground(bg) 130 | setForeground(color) 131 | else 132 | setBackground(color) 133 | setForeground(fg) 134 | end 135 | gpu.set(sx, sy, ch) 136 | end) 137 | 138 | gpu.setForeground(0xFFFFFF) 139 | gpu.setBackground(0x000000) 140 | 141 | gpu.set(1, ry, "[q] Quit [left/right] Rotate [space] Toggle state") 142 | os.sleep(0.1) -- consume events that arrived in the meantime 143 | while true do 144 | local _,_,_,code=event.pull("key_down") 145 | if code == keyboard.keys.q then 146 | term.clear() 147 | os.exit(0) 148 | elseif code == keyboard.keys.space then 149 | setState(not state) 150 | break 151 | elseif code == keyboard.keys.left then 152 | local step = 10 153 | if keyboard.isShiftDown() then step = 90 end 154 | rt.camera.position = vrotate(rt.camera.position, rt.camera.target, -step/180*math.pi) 155 | break 156 | elseif code == keyboard.keys.right then 157 | local step = 10 158 | if keyboard.isShiftDown() then step = 90 end 159 | rt.camera.position = vrotate(rt.camera.position, rt.camera.target, step/180*math.pi) 160 | break 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /print3d.lua: -------------------------------------------------------------------------------- 1 | local component = require("component") 2 | local shell = require("shell") 3 | 4 | local addresses = {} 5 | for address in component.list("printer3d") do 6 | table.insert(addresses, address) 7 | print(#addresses .. ": " .. address) 8 | end 9 | if #addresses > 1 then 10 | io.write("Choose printer: ") 11 | local index 12 | repeat 13 | index = tonumber(io.read("*n")) 14 | if not (index and addresses[index]) then 15 | io.write("\nInvalid index!\nChoose printer: ") 16 | end 17 | until index and addresses[index] 18 | component.setPrimary("printer3d", addresses[index]) 19 | end 20 | 21 | local printer = component.printer3d 22 | 23 | local args = shell.parse(...) 24 | if #args < 1 then 25 | io.write("Usage: print3d FILE [count]\n") 26 | os.exit(0) 27 | end 28 | local count = 1 29 | if #args > 1 then 30 | count = assert(tonumber(args[2]), tostring(args[2]) .. " is not a valid count") 31 | end 32 | 33 | local file, reason = io.open(args[1], "r") 34 | if not file then 35 | io.stderr:write("Failed opening file: " .. reason .. "\n") 36 | os.exit(1) 37 | end 38 | 39 | local rawdata = file:read("*all") 40 | file:close() 41 | local data, reason = load("return " .. rawdata) 42 | if not data then 43 | io.stderr:write("Failed loading model: " .. reason .. "\n") 44 | os.exit(2) 45 | end 46 | data = data() 47 | 48 | io.write("Configuring...\n") 49 | 50 | printer.reset() 51 | if data.label then 52 | printer.setLabel(data.label) 53 | end 54 | if data.tooltip then 55 | printer.setTooltip(data.tooltip) 56 | end 57 | if data.lightLevel and printer.setLightLevel then -- as of OC 1.5.7 58 | printer.setLightLevel(data.lightLevel) 59 | end 60 | if data.emitRedstone then 61 | printer.setRedstoneEmitter(data.emitRedstone) 62 | end 63 | if data.buttonMode then 64 | printer.setButtonMode(data.buttonMode) 65 | end 66 | if data.collidable and printer.setCollidable then 67 | printer.setCollidable(not not data.collidable[1], not not data.collidable[2]) 68 | end 69 | for i, shape in ipairs(data.shapes or {}) do 70 | local result, reason = printer.addShape(shape[1], shape[2], shape[3], shape[4], shape[5], shape[6], shape.texture, shape.state, shape.tint) 71 | if not result then 72 | io.write("Failed adding shape: " .. tostring(reason) .. "\n") 73 | end 74 | end 75 | 76 | io.write("Label: '" .. (printer.getLabel() or "not set") .. "'\n") 77 | io.write("Tooltip: '" .. (printer.getTooltip() or "not set") .. "'\n") 78 | if printer.getLightLevel then -- as of OC 1.5.7 79 | io.write("Light level: " .. printer.getLightLevel() .. "\n") 80 | end 81 | io.write("Redstone level: " .. select(2, printer.isRedstoneEmitter()) .. "\n") 82 | io.write("Button mode: " .. tostring(printer.isButtonMode()) .. "\n") 83 | if printer.isCollidable then -- as of OC 1.5.something 84 | io.write("Collidable: " .. tostring(select(1, printer.isCollidable())) .. "/" .. tostring(select(2, printer.isCollidable())) .. "\n") 85 | end 86 | io.write("Shapes: " .. printer.getShapeCount() .. " inactive, " .. select(2, printer.getShapeCount()) .. " active\n") 87 | 88 | local result, reason = printer.commit(count) 89 | if result then 90 | io.write("Job successfully committed!\n") 91 | else 92 | io.stderr:write("Failed committing job: " .. tostring(reason) .. "\n") 93 | end 94 | -------------------------------------------------------------------------------- /programs.cfg: -------------------------------------------------------------------------------- 1 | { 2 | ["drone-sort"] = { 3 | files = { 4 | ["master/drone-sort.lua"] = "/share/bios" 5 | }, 6 | name = "Sorting Drone BIOS", 7 | description = "This BIOS can be installed on drones to use them in a sorting system employing waypoints.", 8 | authors = "Sangar", 9 | repo = "tree/master/drone-sort.lua" 10 | }, 11 | ["geo2holo"] = { 12 | files = { 13 | ["master/geo2holo.lua"] = "/bin" 14 | }, 15 | name = "Terrain Visualizer", 16 | description = "Will plot the terrain surrounding the Geolyzer to the Hologram.", 17 | authors = "Sangar", 18 | note = "Requires a Geolyzer and a Hologram Projector.", 19 | repo = "tree/master/geo2holo.lua" 20 | }, 21 | ["gol"] = { 22 | files = { 23 | ["master/gol.lua"] = "/bin", 24 | ["master/gol-tiny.lua"] = "/bin" 25 | }, 26 | name = "Conway's Game of Life", 27 | description = "Simple implementations of the well-known cellular automaton Game of Life.", 28 | authors = "Sangar", 29 | note = "The 'tiny' version requires much less RAM, but also runs a lot slower.", 30 | repo = "tree/master/gol.lua" 31 | }, 32 | ["holo-demos"] = { 33 | files = { 34 | ["master/holo-count.lua"] = "/bin", 35 | ["master/holo-flow.lua"] = "/bin", 36 | ["master/holo-text.lua"] = "/bin" 37 | }, 38 | dependencies = { 39 | ["libnoise"] = "/" 40 | }, 41 | name = "Hologram Demos", 42 | description = "Useful example programs for the Hologram block.", 43 | authors = "Sangar", 44 | note = "Requires a Hologram Projector", 45 | repo = "tree/master" 46 | }, 47 | ["midi"] = { 48 | files = { 49 | ["master/midi.lua"] = "/bin" 50 | }, 51 | name = "Crappy MIDI Player", 52 | description = "Can be used to play real MIDI files using either Note Blocks or the built-in speakers.", 53 | authors = "Sangar", 54 | note = "The quality very much depends on the MIDI file/type of song.", 55 | repo = "tree/master/midi.lua" 56 | }, 57 | ["libnoise"] = { 58 | files = { 59 | ["master/noise.lua"] = "/lib" 60 | }, 61 | name = "Perlin Noise Library", 62 | description = "This package provides a library for generating Perlin noise, and some utility methods.", 63 | authors = "Sangar", 64 | note = "This is a slightly adjusted port from some code originally found on StackOverflow.", 65 | repo = "tree/master/noise.lua" 66 | }, 67 | ["librt"] = { 68 | files = { 69 | ["master/raytracer.lua"] = "/lib" 70 | }, 71 | name = "Simple Raytracing Library", 72 | description = "This package provides a library for performing very basic raytracing.", 73 | authors = "Sangar", 74 | note = "Only supports axis aligned bounding boxes.", 75 | repo = "tree/master/raytracer.lua" 76 | }, 77 | ["lisp"] = { 78 | files = { 79 | ["master/lisp.lua"] = "/bin" 80 | }, 81 | name = "LuaLisp", 82 | description = "This program allows running a simple Lisp REPL and Lisp programs.", 83 | authors = "Sangar", 84 | note = "Port of https://code.google.com/p/lualisp/ to work in OC, with some minor tweaks.", 85 | repo = "tree/master/lisp.lua" 86 | }, 87 | ["print3d"] = { 88 | files = { 89 | ["master/print3d.lua"] = "/bin" 90 | }, 91 | name = "Print3D", 92 | description = "This program allows sending custom 3D model files to a 3D printer.", 93 | authors = "Sangar", 94 | note = "Also install print3d-examples to have some example models.", 95 | repo = "tree/master/print3d.lua" 96 | }, 97 | ["print3d-examples"] = { 98 | files = { 99 | [":master/models/"] = "/share/models" 100 | }, 101 | dependencies = { 102 | ["print3d"] = "/" 103 | }, 104 | name = "Print3D Examples", 105 | description = "Contains a couple of example models for the print3d program.", 106 | authors = "Sangar", 107 | note = "See the `/usr/share/models` directory for the example models.", 108 | repo = "tree/master/models" 109 | }, 110 | ["print3d-view"] = { 111 | files = { 112 | ["master/print3d-view.lua"] = "/bin" 113 | }, 114 | dependencies = { 115 | ["librt"] = "/" 116 | }, 117 | name = "3D Print Viewer", 118 | description = "This program allows previewing 3D model files used for print3d.", 119 | authors = "Sangar", 120 | repo = "tree/master/print3d-view.lua" 121 | }, 122 | ["derp"] = { 123 | files = { 124 | ["master/idontexist.lua"] = "/bin" 125 | }, 126 | name = "Missing File Test", 127 | description = "Test for OPPM's behavior if files are missing.", 128 | hidden = true, 129 | authors = "Sangar", 130 | repo = "tree/master" 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /raytracer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Very basic raytracer, passing results (i.e. hit "pixels") to a callback. 3 | 4 | Usage: 5 | local rt = require("raytracer").new() 6 | table.insert(rt.model, {0,0,0,16,16,16}) 7 | --rt.camera.position = {-20,20,0} 8 | --rt.camera.target = {8,8,8} 9 | --rt.camera.fov = 100 10 | rt:render(width, height, function(hitX, hitY, box, normal) 11 | -- do stuff with the hit information, e.g. set pixel at hitX/hitY to boxes color 12 | end) 13 | 14 | Shapes must at least have their min/max coordinates given as the first six 15 | integer indexed entries of the table, as {minX,minY,minZ,maxX,maxY,maxZ}. 16 | The returned normal is a sequence with the x/y/z components of the normal. 17 | 18 | The camera can be configured as shown in the example above, i.e. it has a 19 | position, target and field of view (which is in degrees). 20 | 21 | MIT Licensed, Copyright Sangar 2015 22 | ]] 23 | local M = {} 24 | 25 | -- vector math stuffs 26 | 27 | local function vadd(v1, v2) 28 | return {v1[1]+v2[1], v1[2]+v2[2], v1[3]+v2[3]} 29 | end 30 | local function vsub(v1, v2) 31 | return {v1[1]-v2[1], v1[2]-v2[2], v1[3]-v2[3]} 32 | end 33 | local function vmul(v1, v2) 34 | return {v1[1]*v2[1], v1[2]*v2[2], v1[3]*v2[3]} 35 | end 36 | local function vcross(v1, v2) 37 | return {v1[2]*v2[3]-v1[3]*v2[2], v1[3]*v2[1]-v1[1]*v2[3], v1[1]*v2[2]-v1[2]*v2[1]} 38 | end 39 | local function vmuls(v, s) 40 | return vmul(v, {s, s, s}) 41 | end 42 | local function vdot(v1, v2) 43 | return v1[1]*v2[1] + v1[2]*v2[2] + v1[3]*v2[3] 44 | end 45 | local function vnorm(v) 46 | return vdot(v, v) 47 | end 48 | local function vlen(v) 49 | return math.sqrt(vnorm(v)) 50 | end 51 | local function vnormalize(v) 52 | return vmuls(v, 1/vlen(v)) 53 | end 54 | 55 | -- collision stuffs 56 | 57 | -- http://tog.acm.org/resources/GraphicsGems/gems/RayBox.c 58 | -- adjusted version also returning the surface normal 59 | local function collideRayBox(box, origin, dir) 60 | local inside = true 61 | local quadrant = {0,0,0} 62 | local minB = {box[1],box[2],box[3]} 63 | local maxB = {box[4],box[5],box[6]} 64 | local maxT = {0,0,0} 65 | local candidatePlane = {0,0,0} 66 | local sign = 0 67 | 68 | -- Find candidate planes; this loop can be avoided if 69 | -- rays cast all from the eye(assume perpsective view) 70 | for i=1,3 do 71 | if origin[i] < minB[i] then 72 | quadrant[i] = true 73 | candidatePlane[i] = minB[i] 74 | inside = false 75 | sign = -1 76 | elseif origin[i] > maxB[i] then 77 | quadrant[i] = true 78 | candidatePlane[i] = maxB[i] 79 | inside = false 80 | sign = 1 81 | else 82 | quadrant[i] = false 83 | end 84 | end 85 | 86 | -- Ray origin inside bounding box 87 | if inside then 88 | return nil 89 | end 90 | 91 | -- Calculate T distances to candidate planes 92 | for i=1,3 do 93 | if quadrant[i] and dir[i] ~= 0 then 94 | maxT[i] = (candidatePlane[i] - origin[i]) / dir[i] 95 | else 96 | maxT[i] = -1 97 | end 98 | end 99 | 100 | -- Get largest of the maxT's for final choice of intersection 101 | local whichPlane = 1 102 | for i=2,3 do 103 | if maxT[whichPlane] < maxT[i] then 104 | whichPlane = i 105 | end 106 | end 107 | 108 | -- Check final candidate actually inside box 109 | if maxT[whichPlane] < 0 then return nil end 110 | local coord,normal = {0,0,0},{0,0,0} 111 | for i=1,3 do 112 | if whichPlane ~= i then 113 | coord[i] = origin[i] + maxT[whichPlane] * dir[i] 114 | if coord[i] < minB[i] or coord[i] > maxB[i] then 115 | return nil 116 | end 117 | else 118 | coord[i] = candidatePlane[i] 119 | normal[i] = sign 120 | end 121 | end 122 | 123 | return coord, normal -- ray hits box 124 | end 125 | 126 | local function trace(model, origin, dir) 127 | local bestBox, bestNormal, bestDist = nil, nil, math.huge 128 | for _, box in ipairs(model) do 129 | local hit, normal = collideRayBox(box, origin, dir) 130 | if hit then 131 | local dist = vlen(vsub(hit, origin)) 132 | if dist < bestDist then 133 | bestBox = box 134 | bestNormal = normal 135 | bestDist = dist 136 | end 137 | end 138 | end 139 | return bestBox, bestNormal 140 | end 141 | 142 | -- public api 143 | 144 | function M.new() 145 | return setmetatable({model={},camera={position={-1,1,-1},target={0,0,0},fov=90}}, {__index=M}) 146 | end 147 | 148 | function M:render(w, h, f) 149 | if #self.model < 1 then return end 150 | -- overall model bounds, for quick empty space skipping 151 | local bounds = {self.model[1][1],self.model[1][2],self.model[1][3],self.model[1][4],self.model[1][5],self.model[1][6]} 152 | for _, shape in ipairs(self.model) do 153 | bounds[1] = math.min(bounds[1], shape[1]) 154 | bounds[2] = math.min(bounds[2], shape[2]) 155 | bounds[3] = math.min(bounds[3], shape[3]) 156 | bounds[4] = math.max(bounds[4], shape[4]) 157 | bounds[5] = math.max(bounds[5], shape[5]) 158 | bounds[6] = math.max(bounds[6], shape[6]) 159 | end 160 | bounds = {bounds} 161 | -- setup framework for ray generation 162 | local origin = self.camera.position 163 | local forward = vnormalize(vsub(self.camera.target, origin)) 164 | local plane = vadd(origin, forward) 165 | local side = vcross(forward, {0,1,0}) 166 | local up = vcross(forward, side) 167 | local lside = math.tan(self.camera.fov/2/180*math.pi) 168 | -- generate ray for each pixel, left-to-right, top-to-bottom 169 | local blanks = 0 170 | for sy = 1, h do 171 | local ry = (sy/h - 0.5)*lside 172 | local py = vadd(plane, vmuls(up, ry)) 173 | for sx = 1, w do 174 | local rx = (sx/w - 0.5)*lside 175 | local px = vadd(py, vmuls(side, rx)) 176 | local dir = vnormalize(vsub(px, origin)) 177 | if trace(bounds, origin, dir) then 178 | local box, normal = trace(self.model, origin, dir) 179 | if box then 180 | blanks = 0 181 | if f(sx, sy, box, normal) == false then 182 | return 183 | end 184 | else 185 | blanks = blanks + 1 186 | end 187 | else 188 | blanks = blanks + 1 189 | end 190 | if blanks > 50 then 191 | blanks = 0 192 | os.sleep(0) -- avoid too long without yielding 193 | end 194 | end 195 | end 196 | end 197 | 198 | return M 199 | --------------------------------------------------------------------------------