diff --git a/lddterm-cc.lua b/lddterm-cc.lua new file mode 100644 index 0000000..d0bd034 --- /dev/null +++ b/lddterm-cc.lua @@ -0,0 +1,739 @@ +-- LDDTerm for ComputerCraft +-- Basically a replacement for the Window API but different + +local lddterm = {} +local scr_x, scr_y + +lddterm.alwaysRender = true -- renders after any and all screen-changing functions. +lddterm.useColors = true -- normal computers do not allow color, but this variable doesn't do anything yet +lddterm.baseTerm = term.current() -- will draw to this terminal +lddterm.transformation = nil -- will modify the current buffer as an NFT image before rendering +lddterm.drawFunction = nil -- will draw using this function instead of basic NFT drawing +lddterm.adjustX = 0 -- moves entire screen X +lddterm.adjustY = 0 -- moves entire screen Y +lddterm.windows = {} + +-- converts hex colors to colors api, and back +local to_colors, to_blit = { + [' '] = 0, + ['0'] = 1, + ['1'] = 2, + ['2'] = 4, + ['3'] = 8, + ['4'] = 16, + ['5'] = 32, + ['6'] = 64, + ['7'] = 128, + ['8'] = 256, + ['9'] = 512, + ['a'] = 1024, + ['b'] = 2048, + ['c'] = 4096, + ['d'] = 8192, + ['e'] = 16384, + ['f'] = 32768, +}, {} +for k,v in pairs(to_colors) do + to_blit[v] = k +end + +-- separates string into table based on divider +local explode = function(div, str, replstr, includeDiv) + if (div == '') then + return false + end + local pos, arr = 0, {} + for st, sp in function() return string.find(str, div, pos, false) end do + table.insert(arr, string.sub(replstr or str, pos, st - 1 + (includeDiv and #div or 0))) + pos = sp + 1 + end + table.insert(arr, string.sub(replstr or str, pos)) + return arr +end + +-- determines the size of the terminal before rendering always +local determineScreenSize = function() + scr_x, scr_y = term.getSize() + lddterm.screenWidth = scr_x + lddterm.screenHeight = scr_y +end + +determineScreenSize() + +-- takes two or more windows and checks if the first of them overlap the other(s) +lddterm.checkWindowOverlap = function(window, ...) + if #lddterm.windows < 2 then + return false + end + local list, win = {...} + for i = 1, #list do + win = list[i] + if win ~= window then + + if ( + window.x < win.x + win.width and + win.x < window.x + window.width and + window.y < win.y + win.height and + win.y < window.y + window.height + ) then + return true + end + + end + end + return false +end + +local fixCursorPos = function() + if lddterm.windows[1] then + lddterm.baseTerm.setCursorPos( + -1 + lddterm.windows[1].cursor[1] + lddterm.windows[1].x, + lddterm.windows[1].cursor[2] + lddterm.windows[1].y - 1 + ) + end +end + +-- renders the screen with optional transformation function +lddterm.render = function(transformation, drawFunction) + -- determine new screen size and change lddterm screen to fit + old_scr_x, old_scr_y = scr_x, scr_y + determineScreenSize() + if old_scr_x ~= scr_x or old_scr_y ~= scr_y then + lddterm.baseTerm.clear() + end + local image = lddterm.screenshot() + if type(transformation) == "function" then + image = transformation(image) + end + if drawFunction then + drawFunction(image, lddterm.baseTerm) + else + for y = 1, #image[1] do + lddterm.baseTerm.setCursorPos(1 + lddterm.adjustX, y + lddterm.adjustY) + lddterm.baseTerm.blit(image[1][y], image[2][y], image[3][y]) + end + end + fixCursorPos() +end + +-- paintutils is a set of art functions for loading and drawing images and drawing lines and boxes. +local makePaintutilsAPI = function(term) + local paintutils = {} + + local function drawPixelInternal( xPos, yPos ) + term.setCursorPos( xPos, yPos ) + term.write(" ") + end + + local tColourLookup = {} + for n=1,16 do + tColourLookup[ string.byte( "0123456789abcdef",n,n ) ] = 2^(n-1) + end + + local function parseLine( tImageArg, sLine ) + local tLine = {} + for x=1,sLine:len() do + tLine[x] = tColourLookup[ string.byte(sLine,x,x) ] or 0 + end + table.insert( tImageArg, tLine ) + end + + function paintutils.parseImage( sRawData ) + if type( sRawData ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( sRawData ) .. ")" ) + end + local tImage = {} + for sLine in ( sRawData .. "\n" ):gmatch( "(.-)\n" ) do -- read each line like original file handling did + parseLine( tImage, sLine ) + end + return tImage + end + + function paintutils.loadImage( sPath ) + if type( sPath ) ~= "string" then + error( "bad argument #1 (expected string, got " .. type( sPath ) .. ")", 2 ) + end + + local file = io.open( sPath, "r" ) + if file then + local sContent = file:read("*a") + file:close() + return paintutils.parseImage( sContent ) -- delegate image parse to parseImage + end + return nil + end + + function paintutils.drawPixel( xPos, yPos, nColour ) + if type( xPos ) ~= "number" then error( "bad argument #1 (expected number, got " .. type( xPos ) .. ")", 2 ) end + if type( yPos ) ~= "number" then error( "bad argument #2 (expected number, got " .. type( yPos ) .. ")", 2 ) end + if nColour ~= nil and type( nColour ) ~= "number" then error( "bad argument #3 (expected number, got " .. type( nColour ) .. ")", 2 ) end + if nColour then + term.setBackgroundColor( nColour ) + end + drawPixelInternal( xPos, yPos ) + end + + function paintutils.drawLine( startX, startY, endX, endY, nColour ) + if type( startX ) ~= "number" then error( "bad argument #1 (expected number, got " .. type( startX ) .. ")", 2 ) end + if type( startY ) ~= "number" then error( "bad argument #2 (expected number, got " .. type( startY ) .. ")", 2 ) end + if type( endX ) ~= "number" then error( "bad argument #3 (expected number, got " .. type( endX ) .. ")", 2 ) end + if type( endY ) ~= "number" then error( "bad argument #4 (expected number, got " .. type( endY ) .. ")", 2 ) end + if nColour ~= nil and type( nColour ) ~= "number" then error( "bad argument #5 (expected number, got " .. type( nColour ) .. ")", 2 ) end + + local alwaysRender = lddterm.alwaysRender + lddterm.alwaysRender = false + + startX = math.floor(startX) + startY = math.floor(startY) + endX = math.floor(endX) + endY = math.floor(endY) + + if nColour then + term.setBackgroundColor( nColour ) + end + if startX == endX and startY == endY then + drawPixelInternal( startX, startY ) + if alwaysRender then lddterm.render(lddterm.transformation, lddterm.drawFunction) end + lddterm.alwaysRender = alwaysRender + return + end + + local minX = math.min( startX, endX ) + local maxX, minY, maxY + if minX == startX then + minY = startY + maxX = endX + maxY = endY + else + minY = endY + maxX = startX + maxY = startY + end + + local xDiff = maxX - minX + local yDiff = maxY - minY + + if xDiff > math.abs(yDiff) then + local y = minY + local dy = yDiff / xDiff + for x=minX,maxX do + drawPixelInternal( x, math.floor( y + 0.5 ) ) + y = y + dy + end + else + local x = minX + local dx = xDiff / yDiff + if maxY >= minY then + for y=minY,maxY do + drawPixelInternal( math.floor( x + 0.5 ), y ) + x = x + dx + end + else + for y=minY,maxY,-1 do + drawPixelInternal( math.floor( x + 0.5 ), y ) + x = x - dx + end + end + end + if alwaysRender then lddterm.render(lddterm.transformation, lddterm.drawFunction) end + lddterm.alwaysRender = alwaysRender + end + + function paintutils.drawBox( startX, startY, endX, endY, nColour ) + if type( startX ) ~= "number" then error( "bad argument #1 (expected number, got " .. type( startX ) .. ")", 2 ) end + if type( startY ) ~= "number" then error( "bad argument #2 (expected number, got " .. type( startY ) .. ")", 2 ) end + if type( endX ) ~= "number" then error( "bad argument #3 (expected number, got " .. type( endX ) .. ")", 2 ) end + if type( endY ) ~= "number" then error( "bad argument #4 (expected number, got " .. type( endY ) .. ")", 2 ) end + if nColour ~= nil and type( nColour ) ~= "number" then error( "bad argument #5 (expected number, got " .. type( nColour ) .. ")", 2 ) end + + local alwaysRender = lddterm.alwaysRender + lddterm.alwaysRender = false + + startX = math.floor(startX) + startY = math.floor(startY) + endX = math.floor(endX) + endY = math.floor(endY) + + if nColour then + term.setBackgroundColor( nColour ) + end + if startX == endX and startY == endY then + drawPixelInternal( startX, startY ) + if alwaysRender then lddterm.render(lddterm.transformation, lddterm.drawFunction) end + lddterm.alwaysRender = alwaysRender + return + end + + local minX = math.min( startX, endX ) + local maxX, minY, maxY + if minX == startX then + minY = startY + maxX = endX + maxY = endY + else + minY = endY + maxX = startX + maxY = startY + end + + for x=minX,maxX do + drawPixelInternal( x, minY ) + drawPixelInternal( x, maxY ) + end + + if (maxY - minY) >= 2 then + for y=(minY+1),(maxY-1) do + drawPixelInternal( minX, y ) + drawPixelInternal( maxX, y ) + end + end + if alwaysRender then lddterm.render(lddterm.transformation, lddterm.drawFunction) end + lddterm.alwaysRender = alwaysRender + end + + function paintutils.drawFilledBox( startX, startY, endX, endY, nColour ) + if type( startX ) ~= "number" then error( "bad argument #1 (expected number, got " .. type( startX ) .. ")", 2 ) end + if type( startY ) ~= "number" then error( "bad argument #2 (expected number, got " .. type( startY ) .. ")", 2 ) end + if type( endX ) ~= "number" then error( "bad argument #3 (expected number, got " .. type( endX ) .. ")", 2 ) end + if type( endY ) ~= "number" then error( "bad argument #4 (expected number, got " .. type( endY ) .. ")", 2 ) end + if nColour ~= nil and type( nColour ) ~= "number" then error( "bad argument #5 (expected number, got " .. type( nColour ) .. ")", 2 ) end + + local alwaysRender = lddterm.alwaysRender + lddterm.alwaysRender = false + + startX = math.floor(startX) + startY = math.floor(startY) + endX = math.floor(endX) + endY = math.floor(endY) + + if nColour then + term.setBackgroundColor( nColour ) + end + if startX == endX and startY == endY then + drawPixelInternal( startX, startY ) + return + end + + local minX = math.min( startX, endX ) + local maxX, minY, maxY + if minX == startX then + minY = startY + maxX = endX + maxY = endY + else + minY = endY + maxX = startX + maxY = startY + end + + for x=minX,maxX do + for y=minY,maxY do + drawPixelInternal( x, y ) + end + end + if alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + lddterm.alwaysRender = alwaysRender + end + + function paintutils.drawImage( tImage, xPos, yPos ) + if type( tImage ) ~= "table" then error( "bad argument #1 (expected table, got " .. type( tImage ) .. ")", 2 ) end + if type( xPos ) ~= "number" then error( "bad argument #2 (expected number, got " .. type( xPos ) .. ")", 2 ) end + if type( yPos ) ~= "number" then error( "bad argument #3 (expected number, got " .. type( yPos ) .. ")", 2 ) end + + local alwaysRender = lddterm.alwaysRender + lddterm.alwaysRender = false + + for y=1,#tImage do + local tLine = tImage[y] + for x=1,#tLine do + if tLine[x] > 0 then + term.setBackgroundColor( tLine[x] ) + drawPixelInternal( x + xPos - 1, y + yPos - 1 ) + end + end + end + if alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + lddterm.alwaysRender = alwaysRender + end + + return paintutils + end + +lddterm.newWindow = function(width, height, x, y, meta) + meta = meta or {} + local window = { + width = math.floor(width), + height = math.floor(height), + cursor = meta.cursor or {1, 1}, + colors = meta.colors or {"0", "f"}, + clearChar = meta.clearChar or " ", + visible = meta.visible or true, + x = math.floor(x) or 1, + y = math.floor(y) or 1, + buffer = {{},{},{}}, + } + for y = 1, height do + window.buffer[1][y] = {} + window.buffer[2][y] = {} + window.buffer[3][y] = {} + for x = 1, width do + window.buffer[1][y][x] = window.clearChar + window.buffer[2][y][x] = window.colors[1] + window.buffer[3][y][x] = window.colors[2] + end + end + + window.handle = {} + window.handle.setCursorPos = function(x, y) + window.cursor = {x, y} + fixCursorPos() + end + window.handle.getCursorPos = function() + return window.cursor[1], window.cursor[2] + end + window.handle.setCursorBlink = function(blink) + return lddterm.baseTerm.setCursorBlink(blink) + end + window.handle.getCursorBlink = function() + return lddterm.baseTerm.getCursorBlink(blink) + end + window.handle.scroll = function(amount) + if amount > 0 then + for i = 1, amount do + for c = 1, 3 do + table.remove(window.buffer[c], 1) + window.buffer[c][window.height] = {} + for xx = 1, width do + window.buffer[c][window.height][xx] = ( + c == 1 and window.clearChar or + c == 2 and window.colors[1] or + c == 3 and window.colors[2] + ) + end + end + end + elseif amount < 0 then + for i = 1, -amount do + for c = 1, 3 do + window.buffer[c][window.height] = nil + table.insert(window.buffer[c], 1, {}) + for xx = 1, width do + window.buffer[c][1][xx] = ( + c == 1 and window.clearChar or + c == 2 and window.colors[1] or + c == 3 and window.colors[2] + ) + end + end + end + end + if lddterm.alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.scrollX = function(amount) + if amount > 0 then + for i = 1, amount do + for c = 1, 3 do + for y = 1, window.height do + table.remove(window.buffer[c][y], 1) + window.buffer[c][y][window.width] = ( + c == 1 and window.clearChar or + c == 2 and window.colors[1] or + c == 3 and window.colors[2] + ) + end + end + end + elseif amount < 0 then + for i = 1, -amount do + for c = 1, 3 do + for y = 1, window.height do + window.buffer[c][y][window.width] = nil + table.insert(window.buffer[c][y], 1, ( + c == 1 and window.clearChar or + c == 2 and window.colors[1] or + c == 3 and window.colors[2] + )) + end + end + end + end + if lddterm.alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.write = function(text, x, y, ignoreAlwaysRender) + assert(text ~= nil, "expected string 'text'") + text = tostring(text) + local cx = math.floor(tonumber(x) or window.cursor[1]) + local cy = math.floor(tonumber(y) or window.cursor[2]) + text = text:sub(math.max(0, -cx - 1)) + for i = 1, #text do + if cx >= 1 and cx <= window.width and cy >= 1 and cy <= window.height then + window.buffer[1][cy][cx] = text:sub(i,i) + window.buffer[2][cy][cx] = window.colors[1] + window.buffer[3][cy][cx] = window.colors[2] + end + cx = math.min(cx + 1, window.width + 1) + end + window.cursor = {cx, cy} + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.writeWrap = function(text, x, y, ignoreAlwaysRender) + local words = explode(" ", text, nil, true) + local cx, cy = x or window.cursor[1], y or window.cursor[2] + for i = 1, #words do + if cx + #words[i] > window.width + 1 then + cx = 1 + if cy >= window.height then + window.handle.scroll(1) + cy = window.height + else + cy = cy + 1 + end + end + window.handle.write(words[i], cx, cy, true) + cx = cx + #words[i] + end + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.blit = function(char, textCol, backCol, x, y) + if type(char) == "number" then + char = tostring(char) + end + if type(textCol) == "number" then + textCol = tostring(textCol) + end + if type(backCol) == "number" then + backCol = tostring(backCol) + end + assert(char ~= nil, "expected string 'char'") + local cx = math.floor(tonumber(x) or window.cursor[1]) + local cy = math.floor(tonumber(y) or window.cursor[2]) + char = char:sub(math.max(0, -cx - 1)) + for i = 1, #char do + if cx >= 1 and cx <= window.width and cy >= 1 and cy <= window.height then + window.buffer[1][cy][cx] = char:sub(i,i) + window.buffer[2][cy][cx] = textCol:sub(i,i) + window.buffer[3][cy][cx] = backCol:sub(i,i) + end + if cx >= window.width or cy < 1 then + cx = 1 + if cy >= window.height then + window.handle.scroll(1) + else + cy = cy + 1 + end + else + cx = cx + 1 + end + end + window.cursor = {cx, cy} + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.print = function(text, x, y) + text = text and tostring(text) + window.handle.write(text, x, y, true) + window.cursor[1] = 1 + if window.cursor[2] >= window.height then + window.handle.scroll(1) + else + window.cursor[2] = window.cursor[2] + 1 + if lddterm.alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + end + window.handle.clear = function(char, ignoreAlwaysRender) + local cx = 1 + for y = 1, window.height do + for x = 1, window.width do + if char then + cx = (x % #char) + 1 + end + window.buffer[1][y][x] = char and char:sub(cx, cx) or window.clearChar + window.buffer[2][y][x] = window.colors[1] + window.buffer[3][y][x] = window.colors[2] + end + end + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.clearLine = function(cy, char, ignoreAlwaysRender) + cy = math.floor(cy or window.cursor[2]) + local cx = 1 + for x = 1, window.width do + if char then + cx = (x % #char) + 1 + end + window.buffer[1][cy or window.cursor[2]][x] = char and char:sub(cx, cx) or window.clearChar + window.buffer[2][cy or window.cursor[2]][x] = window.colors[1] + window.buffer[3][cy or window.cursor[2]][x] = window.colors[2] + end + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.clearColumn = function(cx, char, ignoreAlwaysRender) + cx = math.floor(cx) + char = char and char:sub(1,1) + for y = 1, window.height do + window.buffer[1][y][cx or window.cursor[1]] = char and char or window.clearChar + window.buffer[2][y][cx or window.cursor[1]] = window.colors[1] + window.buffer[3][y][cx or window.cursor[1]] = window.colors[2] + end + if lddterm.alwaysRender and not ignoreAlwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.getSize = function() + return window.width, window.height + end + window.handle.isColor = function() + return lddterm.useColors + end + window.handle.isColour = window.handle.isColor + window.handle.setTextColor = function(color) + if to_blit[color] then + window.colors[1] = to_blit[color] + end + end + window.handle.setTextColour = window.handle.setTextColor + window.handle.setBackgroundColor = function(color) + if to_blit[color] then + window.colors[2] = to_blit[color] + end + end + window.handle.setBackgroundColour = window.handle.setBackgroundColor + window.handle.getTextColor = function() + return to_colors[window.colors[1]] or colors.white + end + window.handle.getTextColour = window.handle.getTextColor + window.handle.getBackgroundColor = function() + return to_colors[window.colors[2]] or colors.black + end + window.handle.getBackgroundColour = window.handle.getBackgroundColor + window.handle.reposition = function(x, y) + window.x = math.floor(x or window.x) + window.y = math.floor(y or window.y) + if lddterm.alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + end + window.handle.setPaletteColor = function(...) + return lddterm.baseTerm.setPaletteColor(...) + end + window.handle.setPaletteColour = window.handle.setPaletteColor + window.handle.getPaletteColor = function(...) + return lddterm.baseTerm.getPaletteColor(...) + end + window.handle.getPaletteColour = window.handle.getPaletteColor + window.handle.getPosition = function() + return window.x, window.y + end + window.handle.restoreCursor = function() + lddterm.baseTerm.setCursorPos( + -1 + window.cursor[1] + window.x, + window.cursor[2] + window.y - 1 + ) + end + window.handle.setVisible = function(visible) + window.visible = visible or false + end + + window.handle.redraw = lddterm.render + + window.ccapi = { + colors = colors, + paintutils = makePaintutilsAPI(window.handle) + } + + window.layer = #lddterm.windows + 1 + lddterm.windows[window.layer] = window + + return window, window.layer +end + +lddterm.setLayer = function(window, _layer) + local layer = math.max(1, math.min(#lddterm.windows, _layer)) + + local win = window + table.remove(lddterm.windows, win.layer) + table.insert(lddterm.windows, layer, win) + + if lddterm.alwaysRender then + lddterm.render(lddterm.transformation, lddterm.drawFunction) + end + return true +end + +-- if the screen changes size, the effect is broken +local old_scr_x, old_scr_y + +-- gets screenshot of whole lddterm desktop, OR a single window +lddterm.screenshot = function(window) + local output = {{},{},{}} + local line + if window then + for y = 1, #window.buffer do + line = {"","",""} + for x = 1, #window.buffer do + line = { + line[1] .. window.buffer[1][y][x], + line[2] .. window.buffer[2][y][x], + line[3] .. window.buffer[3][y][x] + } + end + output[1][y] = line[1] + output[2][y] = line[2] + output[3][y] = line[3] + end + else + for y = 1, scr_y do + line = {"","",""} + for x = 1, scr_x do + + c = " " + lt, lb = t, b + t, b = "0", "f" + for l = 1, #lddterm.windows do + if lddterm.windows[l].visible then + sx = 1 + x - lddterm.windows[l].x + sy = 1 + y - lddterm.windows[l].y + if lddterm.windows[l].buffer[1][sy] then + if lddterm.windows[l].buffer[1][sy][sx] then + c = lddterm.windows[l].buffer[1][sy][sx] or c + t = lddterm.windows[l].buffer[2][sy][sx] or t + b = lddterm.windows[l].buffer[3][sy][sx] or b + break + end + end + end + end + line = { + line[1] .. c, + line[2] .. t, + line[3] .. b + } + end + output[1][y] = line[1] + output[2][y] = line[2] + output[3][y] = line[3] + end + end + return output +end + +return lddterm