-- UTF-8 Display Library for ComputerCraft -- 文件名: utf8display.lua local utf8display = {} -- 配置参数 utf8display.config = { fontUrl = "https://git.liulikeji.cn/xingluo/ComputerCraft-Utf8/raw/branch/main/fonts/fusion-pixel-8px-proportional-zh_hans.lua", fontPath = nil, cacheFont = true, autoScroll = true } -- 内部状态 local state = { font = nil, fontHeight = 8, loadedFonts = {} } -- 字体管理模块 local fontManager = {} function fontManager.loadRemoteFont(url) if state.loadedFonts[url] then return state.loadedFonts[url] end local response = http.get(url) if not response then return nil, "无法连接到字体服务器: " .. url end if response.getResponseCode() ~= 200 then return nil, "字体服务器返回错误: " .. response.getResponseCode() end local content = response.readAll() response.close() local sandbox = {} local chunk, err = load(content, "=remoteFont", "t", sandbox) if not chunk then return nil, "加载字体失败: " .. err end local success, result = pcall(chunk) if not success then return nil, "执行字体脚本失败: " .. result end local fontData = sandbox.font or sandbox[1] or result if utf8display.config.cacheFont then state.loadedFonts[url] = fontData end return fontData end function fontManager.loadLocalFont(path) if state.loadedFonts[path] then return state.loadedFonts[path] end if not fs.exists(path) then return nil, "字体文件不存在: " .. path end local file = fs.open(path, "r") local content = file.readAll() file.close() local sandbox = {} local chunk, err = load(content, "=localFont", "t", sandbox) if not chunk then return nil, "加载本地字体失败: " .. err end local success, result = pcall(chunk) if not success then return nil, "执行本地字体脚本失败: " .. result end local fontData = sandbox.font or sandbox[1] or result if utf8display.config.cacheFont then state.loadedFonts[path] = fontData end return fontData end function fontManager.getFont() if not state.font then local success, err = utf8display.loadFont() if not success then error("字体加载失败: " .. err) end end return state.font end function fontManager.getFontHeight() local font = fontManager.getFont() if font and font[32] then return #font[32] end return state.fontHeight end -- 渲染引擎模块 local renderer = {} function renderer.displayChar(charMap, x, y, textColor, backgroundColor) if not charMap or #charMap == 0 then return end -- 保存当前终端颜色和光标位置 local originalTextColor = term.getTextColor() local originalBackgroundColor = term.getBackgroundColor() local originalCursorX, originalCursorY = term.getCursorPos() -- 如果没有提供自定义颜色,则使用当前终端颜色 local useTextColor = textColor or originalTextColor local useBackgroundColor = backgroundColor or originalBackgroundColor -- 渲染字符(仅在可视区域内) local width, height = term.getSize() for row = 1, #charMap do local drawY = y + row - 1 if drawY > height then break end -- 超出底部,不再绘制 term.setCursorPos(x, drawY) local line = charMap[row] for col = 1, #line do local byte = string.byte(line, col) if byte < 128 then term.setTextColor(useBackgroundColor) term.setBackgroundColor(useTextColor) term.write(string.char(byte + 128)) else term.setTextColor(useTextColor) term.setBackgroundColor(useBackgroundColor) term.write(string.char(byte)) end end end -- 恢复原始终端颜色和光标位置 term.setTextColor(originalTextColor) term.setBackgroundColor(originalBackgroundColor) term.setCursorPos(originalCursorX, originalCursorY) end function renderer.utf8Decode(str) local i = 1 return function() if i > #str then return end local b1 = string.byte(str, i) i = i + 1 if b1 < 0x80 then return b1 elseif b1 >= 0xC0 and b1 < 0xE0 then local b2 = string.byte(str, i) or 0 i = i + 1 return (b1 - 0xC0) * 64 + (b2 - 0x80) elseif b1 >= 0xE0 and b1 < 0xF0 then local b2 = string.byte(str, i) or 0 i = i + 1 local b3 = string.byte(str, i) or 0 i = i + 1 return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80) else return 32 end end end -- 公共API函数 function utf8display.setConfig(key, value) if utf8display.config[key] ~= nil then utf8display.config[key] = value return true end return false, "配置项不存在" end function utf8display.getConfig(key) if utf8display.config[key] ~= nil then return utf8display.config[key] end return nil, "配置项不存在" end function utf8display.loadFont() if utf8display.config.fontPath then state.font, err = fontManager.loadLocalFont(utf8display.config.fontPath) if not state.font then return false, err end else state.font, err = fontManager.loadRemoteFont(utf8display.config.fontUrl) if not state.font then return false, err end end state.fontHeight = fontManager.getFontHeight() return true end -- 主动初始化字体函数 function utf8display.initFont() return utf8display.loadFont() end local function clampCursorPos(x, y, width, height) return math.min(math.max(x, 1), width), math.min(math.max(y, 1), height) end function utf8display.write(raw_str) str = tostring(raw_str) -- 将换行符替换为空格 str = string.gsub(str, "\n", " ") local font = fontManager.getFont() local fontHeight = fontManager.getFontHeight() local textColor = term.getTextColor() local backgroundColor = term.getBackgroundColor() local startX, startY = term.getCursorPos() local width, height = term.getSize() local cursorX, cursorY = startX, startY local charCount = 0 local contentFitsHorizontally = true local contentFitsVertically = true for code in renderer.utf8Decode(str) do if code == 10 then -- 换行符 (理论上不会再有,因为我们已经替换了) cursorX = 1 cursorY = cursorY + fontHeight else local charMap = font[code] or font[32] local charWidth = #charMap[1] -- 不自动换行:即使超出横向宽度也继续写(但不渲染超出部分) if cursorX + charWidth - 1 > width then contentFitsHorizontally = false end -- 判断是否还能在竖直方向绘制(至少第一行能画) if cursorY <= height then renderer.displayChar(charMap, cursorX, cursorY, textColor, backgroundColor) charCount = charCount + 1 else contentFitsVertically = false -- 不 break,继续推进光标逻辑(用于正确设置最终光标位置) end cursorX = cursorX + charWidth end end -- 确定最终光标位置 local finalX, finalY if not contentFitsHorizontally or not contentFitsVertically then -- 若任一方向溢出,光标回到起始位置 finalX, finalY = startX, startY else -- 否则放在正常结束位置 finalX, finalY = cursorX, cursorY end -- 安全设置光标(避免超出屏幕导致卡住) finalX, finalY = clampCursorPos(finalX, finalY, width, height) term.setCursorPos(finalX, finalY) return true, { textColor = textColor, backgroundColor = backgroundColor, startX = startX, startY = startY, endX = finalX, endY = finalY, charCount = charCount, fontHeight = fontHeight, overflowX = not contentFitsHorizontally, overflowY = not contentFitsVertically } end function utf8display.print(raw_str) str = tostring(raw_str) local font = fontManager.getFont() local fontHeight = fontManager.getFontHeight() local textColor = term.getTextColor() local backgroundColor = term.getBackgroundColor() local startX, startY = term.getCursorPos() local width, height = term.getSize() -- 预计算所需总高度(用于滚动) local tempCursorX, tempCursorY = startX, startY for code in renderer.utf8Decode(str) do if code == 10 then tempCursorX = 1 tempCursorY = tempCursorY + fontHeight else local charMap = font[code] or font[32] local charWidth = #charMap[1] if tempCursorX + charWidth - 1 > width then tempCursorX = 1 tempCursorY = tempCursorY + fontHeight end tempCursorX = tempCursorX + charWidth end end local totalContentHeight = tempCursorY + fontHeight - startY local availableHeight = height - startY + 1 local scrollHeight = math.max(0, totalContentHeight - availableHeight) if scrollHeight > 0 and utf8display.config.autoScroll then term.scroll(scrollHeight) startY = startY - scrollHeight end -- 实际渲染 local cursorX, cursorY = startX, startY local charCount = 0 for code in renderer.utf8Decode(str) do if code == 10 then cursorX = 1 cursorY = cursorY + fontHeight else local charMap = font[code] or font[32] local charWidth = #charMap[1] if cursorX + charWidth - 1 > width then cursorX = 1 cursorY = cursorY + fontHeight end if cursorY <= height then renderer.displayChar(charMap, cursorX, cursorY, textColor, backgroundColor) charCount = charCount + 1 end cursorX = cursorX + charWidth end end -- print 总是把光标放到下一行开头(符合常规行为) local finalY = cursorY + fontHeight local finalX = 1 finalX, finalY = clampCursorPos(finalX, finalY, width, height) term.setCursorPos(finalX, finalY) return true, { textColor = textColor, backgroundColor = backgroundColor, startX = startX, startY = startY, endX = finalX, endY = finalY, charCount = charCount, fontHeight = fontHeight } end function utf8display.blit(raw_str, textColorStr, backgroundColorStr) text = tostring(raw_str) -- 将换行符替换为空格 text = string.gsub(text, "\n", " ") local font = fontManager.getFont() local fontHeight = fontManager.getFontHeight() local startX, startY = term.getCursorPos() local width, height = term.getSize() local cursorX, cursorY = startX, startY local charCount = 0 local contentFitsHorizontally = true local contentFitsVertically = true -- 解析颜色字符串 local textColors = {} local backgroundColors = {} for i = 1, #textColorStr do textColors[i] = colors.fromBlit(string.sub(textColorStr, i, i)) end for i = 1, #backgroundColorStr do backgroundColors[i] = colors.fromBlit(string.sub(backgroundColorStr, i, i)) end local index = 1 for code in renderer.utf8Decode(text) do if code == 10 then -- 换行符 (理论上不会再有,因为我们已经替换了) cursorX = 1 cursorY = cursorY + fontHeight else local charMap = font[code] or font[32] local charWidth = #charMap[1] if cursorX + charWidth - 1 > width then contentFitsHorizontally = false end if cursorY <= height then local useTextColor = textColors[index] or term.getTextColor() local useBackgroundColor = backgroundColors[index] or term.getBackgroundColor() renderer.displayChar(charMap, cursorX, cursorY, useTextColor, useBackgroundColor) charCount = charCount + 1 else contentFitsVertically = false end cursorX = cursorX + charWidth index = index + 1 end end local finalX, finalY if not contentFitsHorizontally or not contentFitsVertically then finalX, finalY = startX, startY else finalX, finalY = cursorX, cursorY end finalX, finalY = clampCursorPos(finalX, finalY, width, height) term.setCursorPos(finalX, finalY) return true, { startX = startX, startY = startY, endX = finalX, endY = finalY, charCount = charCount, fontHeight = fontHeight, overflowX = not contentFitsHorizontally, overflowY = not contentFitsVertically } end -- 工具函数 function utf8display.getFontInfo() local font = fontManager.getFont() local fontHeight = fontManager.getFontHeight() return { height = fontHeight, loaded = font ~= nil, source = utf8display.config.fontPath or utf8display.config.fontUrl, cached = state.loadedFonts[utf8display.config.fontPath or utf8display.config.fontUrl] ~= nil } end function utf8display.isInitialized() return state.font ~= nil end -- 自动初始化(第一次使用时) setmetatable(utf8display, { __index = function(self, key) if key == "write" or key == "print" or key == "blit" then if not utf8display.isInitialized() then local success, err = utf8display.loadFont() if not success then error("自动初始化失败: " .. err) end end return rawget(self, key) end return rawget(self, key) end }) return utf8display