Files
ccphone/global-libraries/shrekbox.lua
2025-12-23 12:46:47 -08:00

1054 lines
32 KiB
Lua

local shrekbox = {}
---@class Layer
---@field _render fun(rblit:blitmap)
---@field pixel fun(lx:integer,ly:integer,color:ccTweaked.colors.color)
---@field text fun(lx:integer,ly:integer,text:string,fg:ccTweaked.colors.color?,bg:ccTweaked.colors.color?)
---@field set_pos fun(sx:integer,sy:integer)
---@field get_pos fun():integer,integer
---@field stl_coords fun(sx:number,sy:number):number,number
---@field lts_coords fun(sx:number,sy:number):number,number
---@field buffer ccTweaked.colors.color[][]
---@field size fun():number,number
---@field clear fun()
---@field scale_x number
---@field scale_y number
---@field z number
---@field label string?
---@field hidden boolean?
---@alias blittable {[1]:string,[2]:string,[3]:string}
---@alias blitmap table<integer,table<integer,blittable>>
---@alias blitbuffer {[1]:string[],[2]:blitchar[],[3]:blitchar[]}[]
---@generic A
---@generic B
---@generic V
---@param f fun(a:A,b:B,v:V):boolean?
local function iter_2d(t, f)
for a, at in pairs(t) do
for b, v in pairs(at) do
if f(a, b, v) then return end
end
end
end
local function assert_int(n)
if n ~= math.floor(n) then
error(("Number %f is not an integer!"):format(n), 2)
end
end
---@generic A
---@generic B
---@generic V
---@param t table<A,table<B,V>>
---@param a A
---@param b B
---@param v V
local function set_index_2d(t, a, b, v)
t[a] = t[a] or {}
t[a][b] = v
end
---@generic A
---@generic B
---@generic V
---@param t table<A,table<B,V>>
---@param a A
---@param b B
---@return V?
local function get_index_2d(t, a, b)
if not t[a] then return end
return t[a][b]
end
---@alias oblit_part {v:blittable,y:integer,x:integer}
---@alias oblit oblit_part[]
---@alias background_lookup ccTweaked.colors.color[][]
---@param win ccTweaked.Window
---@param oblit oblit
---@param bg blitchar
local function normalize_blit_strings(a, b, c, width, bg)
a = a or ""
b = b or ""
c = c or ""
local lenA = #a
local lenB = #b
local lenC = #c
local target = width
local function adjust(str, len, padChar)
if len > target then
return str:sub(1, target)
elseif len < target then
return str .. string.rep(padChar, target - len)
end
return str
end
-- Default to space when background char is unavailable.
local bgChar = bg or " "
a = adjust(a, lenA, " ")
b = adjust(b, lenB, bgChar)
c = adjust(c, lenC, bgChar)
return a, b, c
end
local function render_blit(win, oblit, bg)
local w, h = win.getSize()
for i = 1, h do
local v = oblit[i]
win.setCursorPos(1, i)
if v then
local text = table.concat(v[1])
local fg = table.concat(v[2]):gsub(shrekbox.transparent_char, bg)
local bgStr = table.concat(v[3]):gsub(shrekbox.transparent_char, bg)
text, fg, bgStr = normalize_blit_strings(text, fg, bgStr, w, bg)
win.blit(text, fg, bgStr)
v[1] = {}
else
local blank = string.rep(" ", w)
local bgRow = string.rep(bg, w)
win.blit(blank, bgRow, bgRow)
end
end
end
local blit_lut = {}
for i = 0, 15 do
local n = 2 ^ i
local s = colors.toBlit(n)
blit_lut[n] = s
blit_lut[s] = n
end
shrekbox._blit_lut = blit_lut
-- Transparency
shrekbox.transparent = 0
shrekbox.transparent_char = " "
blit_lut[shrekbox.transparent] = shrekbox.transparent_char
blit_lut[shrekbox.transparent_char] = shrekbox.transparent
shrekbox.contrast = -1
shrekbox.contrast_char = "_"
blit_lut[shrekbox.contrast] = shrekbox.contrast_char
blit_lut[shrekbox.contrast_char] = shrekbox.contrast
---@alias blitchar "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"a"|"b"|"c"|"d"|"e"|"f"|" "|"_"
local closest_color_to_lookup = {}
---@type table<blitchar,number[]>
local palette_colors = {}
---@type table<blitchar,blitchar>
local contrast_lookup = {}
---@param a number[]
---@param b number[]
---@return number
local function color_dist(a, b)
return (a[1] - b[1]) ^ 2 + (a[2] - b[2]) ^ 2 + (a[3] - b[3]) ^ 2
end
--- Array of colors from darkest (1) to brightest (16)
---@type blitchar[]
local color_bright_order = {}
--- Lookup from blitcolor to brightness index into color_bright_order
---@type table<blitchar,integer>
local bright_pos_lookup = {}
--- Array of colors from desaturated (1) to saturated (16)
---@type blitchar[]
local color_sat_order = {}
--- Lookup from blitcolor to saturation index into color_sat_order
---@type table<blitchar,integer>
local sat_pos_lookup = {}
---Shift a given blitchar color brighter/darker by a number of levels
---@param ch blitchar
---@param dir integer
function shrekbox.shift_brightness(ch, dir)
local index = bright_pos_lookup[ch]
index = math.max(1, math.min(index + dir, 16))
return color_bright_order[index]
end
---Shift a given blitchar color saturated/desaturated by a number of levels
---@param ch blitchar
---@param dir integer
function shrekbox.shift_saturation(ch, dir)
local index = sat_pos_lookup[ch]
index = math.max(1, math.min(index + dir, 16))
return color_sat_order[index]
end
---@return integer
---@return ...
function shrekbox.round(n, ...)
---@diagnostic disable-next-line: missing-return-value
if not n then return end
return math.floor(n + 0.5), shrekbox.round(...)
end
---@param c number[]
---@return number
local function brightness(c)
return c[1] ^ 2 + c[2] ^ 2 + c[3] ^ 2
end
local function rgb_2_hsl(c)
local r, g, b = c[1], c[2], c[3]
local cmax = math.max(r, g, b)
local cmin = math.min(r, g, b)
local delta = cmax - cmin
local hue, sat = 0, 0
local lum = (cmax + cmin) / 2
sat = lum > 0.5 and (delta / (2 - cmax - cmin)) or delta / (cmax + cmin)
if delta == 0 then
hue = 0
sat = 0
elseif delta == r then
hue = (g - b) / delta + (g < b and 6 or 0)
elseif delta == g then
hue = (b - r) / delta + 2
elseif delta == b then
hue = (r - g) / delta + 4
end
hue = hue / 6
return { hue, sat, lum }
end
local expect = require("cc.expect").expect
---@param color blitchar
---@param a blitchar
---@param b blitchar
---@return blitchar
local function closer_color(color, a, b)
expect(1, color, "string")
expect(2, a, "string")
expect(3, b, "string")
a, b = a < b and a or b, b >= a and b or a
if color == a then return a end
if color == b then return b end
if a == b then return a end
if color == shrekbox.transparent_char then
return color
elseif a == shrekbox.transparent_char then
return b
elseif b == shrekbox.transparent_char then
return a
end
local lookup_index = a .. b
local lookup = get_index_2d(closest_color_to_lookup, lookup_index, color)
if lookup then return lookup end
local rgb = palette_colors[color]
if not rgb then error(("Invalid color %s"):format(color), 2) end
if not palette_colors[a] then error(("Invalid color a %s"):format(a), 2) end
if not palette_colors[b] then error(("Invalid color b %s"):format(b), 2) end
local a_dist = color_dist(rgb, palette_colors[a])
local b_dist = color_dist(rgb, palette_colors[b])
local closest = a_dist < b_dist and a or b
set_index_2d(closest_color_to_lookup, lookup_index, color, closest)
return closest
end
local function get_palette_colors()
local brighness_calc = {}
local saturation_calc = {}
for i = 0, 15 do
local color = 2 ^ i
local ch = blit_lut[color]
local rgb = { term.getPaletteColor(color) }
palette_colors[ch] = rgb
local bright = brightness(rgb)
brighness_calc[#brighness_calc + 1] = { bright, ch }
local hsl = rgb_2_hsl(rgb)
saturation_calc[#saturation_calc + 1] = { hsl[2], ch }
end
table.sort(brighness_calc, function(a, b)
return a[1] > b[1]
end)
table.sort(saturation_calc, function(a, b)
return a[1] > b[1]
end)
for i = 1, 16 do
color_bright_order[i] = brighness_calc[i][2]
bright_pos_lookup[brighness_calc[i][2]] = i
color_sat_order[i] = saturation_calc[i][2]
sat_pos_lookup[saturation_calc[i][2]] = i
end
local darkest, brightest = color_bright_order[1], color_bright_order[16]
closest_color_to_lookup = {}
for i = 0, 15 do
local color = 2 ^ i
local ch = blit_lut[color]
contrast_lookup[ch] = closer_color(ch, darkest, brightest) == darkest and brightest or darkest
end
end
get_palette_colors()
---@type table<string,string[]>
local textel_blit_lut = {}
local textel_majority_color_lut = {}
---@param textel_layout string[]
---@return blitchar
---@return blitchar
local function get_majority_color(textel_layout)
local color_frequency_map = {}
local max_count = 0
local max_color = "0"
local max_2_count = 0
local max_2_color = "0"
for i = 1, 6 do
local ch = textel_layout[i]
color_frequency_map[ch] = (color_frequency_map[ch] or 0) + 1
if color_frequency_map[ch] > max_count then
if max_color ~= ch then
max_2_count = max_count
max_2_color = max_color
end
max_color = ch
max_count = color_frequency_map[ch]
elseif color_frequency_map[ch] > max_2_count then
max_2_count = color_frequency_map[ch]
max_2_color = ch
end
end
return max_color, max_2_color
end
local function clone_blit(t)
return { table.unpack(t, 1, 3) }
end
textel_blit_lut[shrekbox.transparent_char:rep(6)] = {}
---@param textel_layout string[]
---@return string[]
---@return blitchar common
local function get_textel_blit(textel_layout, box)
local textel_layout_s = table.concat(textel_layout, "")
if textel_blit_lut[textel_layout_s] then
return clone_blit(textel_blit_lut[textel_layout_s]), textel_majority_color_lut[textel_layout_s]
end
box.profiler.start_region("textel_blit")
local ch = textel_layout[6]
local textel = 0
local max_color, max_2_color = get_majority_color(textel_layout)
local majority_color = max_color
ch = closer_color(ch, max_color, max_2_color)
if ch == max_color then
-- swap colors
max_color, max_2_color = max_2_color, max_color
end
local interp_colors = ch
for i = 5, 1, -1 do
ch = textel_layout[i]
ch = closer_color(ch, max_color, max_2_color)
interp_colors = interp_colors .. ch
textel = bit32.lshift(textel, 1) + (ch == max_color and 1 or 0)
end
ch = string.char(textel + 128)
local blit = {
ch,
max_color,
max_2_color
}
textel_blit_lut[textel_layout_s] = clone_blit(blit)
textel_majority_color_lut[textel_layout_s] = majority_color
box.profiler.end_region("textel_blit")
return blit, majority_color
end
local transparency_lookup = {
[shrekbox.transparent_char] = true,
[shrekbox.contrast_char] = true
}
---@param fg blitchar
---@param bg blitchar
---@return boolean
local function is_blit_transparent(fg, bg)
return transparency_lookup[fg] or transparency_lookup[bg]
end
---@param box ShrekBox
---@param rblit blitbuffer
---@param x integer
---@param y integer
local function apply_transparent(ch, fg, bg, color, box, rblit, x, y)
box.profiler.start_region("trans")
bg = bg == shrekbox.transparent_char and color or bg
fg = (fg == shrekbox.transparent_char and color) or
(fg == shrekbox.contrast_char and contrast_lookup[bg]) or fg
rblit[y][1][x] = ch
rblit[y][2][x] = fg
rblit[y][3][x] = bg
box.profiler.end_region("trans")
end
---@param box ShrekBox
---@param layer Layer
local function insert_default_layer_funcs(box, layer)
--- Get the size of this layer (in layer units)
---@return number
---@return number
function layer.size()
local sw, sh = box._get_window().getSize()
return sw * layer.scale_x, sh * layer.scale_y
end
--- Screen To Layer Coordinates
---@param sx number
---@param sy number
---@return number
---@return number
function layer.stl_coords(sx, sy)
local lpx, lpy = layer.get_pos()
return (sx - lpx + 1) * layer.scale_x, (sy - lpy + 1) * layer.scale_y
end
--- Layer to screen coordinates
---@param lx number
---@param ly number
---@return number
---@return number
function layer.lts_coords(lx, ly)
local lpx, lpy = layer.get_pos()
return lx / layer.scale_x - lpx + 1, ly / layer.scale_y - lpy + 1
end
end
local buffer_meta = {
__index = function(t, k)
t[k] = {}
return t[k]
end
}
---@return table<integer,table<integer,ccTweaked.colors.color>>
local function generate_buffer()
return setmetatable({}, buffer_meta)
end
local rblit_buffer_meta = {
__index = function(t, k)
t[k] = generate_buffer()
return t[k]
end
}
local function generate_rblit_buffer()
return setmetatable({}, rblit_buffer_meta)
end
local function emtpy_textel()
return { ' ', ' ', ' ', ' ', ' ', ' ' }
end
---@param buffer table<integer,table<integer,ccTweaked.colors.color>>
local function pixel_layer_render(buffer, textel_buffer)
for ly, line in pairs(buffer) do
for lx, color in pairs(line) do
local iy = math.ceil(ly / 3)
local dy = (ly - 1) % 3
local ix = math.ceil(lx / 2)
local dx = (lx - 1) % 2
local i = (dy * 2 + dx) + 1
local t = textel_buffer[iy][ix] or emtpy_textel()
t[i] = blit_lut[color]
textel_buffer[iy][ix] = t
-- if t[1] == shrekbox.transparent_char and t[1] == t[2] and t[2] == t[3] and t[4] == t[5] and t[5] == t[6] then
-- textel_buffer[iy][ix] = nil
-- end
end
end
end
---@param box ShrekBox
---@param label string?
---@return Layer
local function new_pixel_layer(box, label)
local px, py = 1, 1
local layer
local textel_buffer = generate_buffer()
layer = {
label = label,
scale_x = 2,
scale_y = 3,
z = 0,
clear = function()
textel_buffer = generate_buffer()
end,
buffer = generate_buffer(),
pixel = function(lx, ly, color)
box.profiler.start_region("pixel")
assert_int(lx)
assert_int(ly)
layer.buffer[ly][lx] = color
box.profiler.end_region("pixel")
end,
_render = function(rblit)
local tid = layer.label or ("pr_" .. layer.z)
box.profiler.start_region(tid)
pixel_layer_render(layer.buffer, textel_buffer)
box.profiler.start_region("gen_blit")
iter_2d(textel_buffer, function(y, x, v)
x = x + px - 1
y = y + py - 1
if not box.pos_on_screen(x, y) then return end
local ch, fg, bg = rblit[y][1][x], rblit[y][2][x], rblit[y][3][x]
if ch and is_blit_transparent(fg, bg) then
local c = get_majority_color(v)
apply_transparent(ch, fg, bg, c, box, rblit, x, y)
return
elseif ch then
return
end
local textel_blit, majority = get_textel_blit(v, box)
rblit[y][1][x] = textel_blit[1]
rblit[y][2][x] = textel_blit[2]
rblit[y][3][x] = textel_blit[3]
end)
box.profiler.end_region("gen_blit")
layer.buffer = generate_buffer()
box.profiler.end_region(tid)
end,
text = function(lx, ly, text, fg, bg)
assert_int(lx)
assert_int(ly)
error("NYI", 2)
end,
set_pos = function(sx, sy)
assert_int(sx)
assert_int(sy)
px, py = sx, sy
end,
get_pos = function()
return px, py
end
}
insert_default_layer_funcs(box, layer)
return layer
end
local function empty_bixtel()
return { ' ', ' ', ' ' }
end
---@param buffer table<integer,table<integer,ccTweaked.colors.color>>
local function bixel_layer_render(buffer, bixtel_buffer)
for ly, line in pairs(buffer) do
for lx, color in pairs(line) do
local iy = math.ceil(ly / 3)
local dy = (ly - 1) % 3
local i = dy + 1
local t = bixtel_buffer[iy][lx] or empty_bixtel()
t[i] = blit_lut[color]
bixtel_buffer[iy][lx] = t
-- if t[1] == shrekbox.transparent_char and t[1] == t[2] and t[2] == t[3] then
-- set_index_2d(bixtel_buffer, iy, lx, nil)
-- end
end
end
end
---@param box ShrekBox
---@param label string?
---@return Layer
local function new_bixel_layer(box, label)
local px, py = 1, 1
local layer
local bixtel_buffer = generate_buffer()
layer = {
label = label,
scale_x = 1,
scale_y = 1.5,
z = 0,
clear = function()
bixtel_buffer = generate_buffer()
end,
buffer = generate_buffer(),
pixel = function(lx, ly, color)
box.profiler.start_region("bixel")
assert_int(lx)
assert_int(ly)
layer.buffer[ly][lx] = color
box.profiler.end_region("bixel")
end,
_render = function(rblit)
local tid = layer.label or ("br_" .. layer.z)
box.profiler.start_region(tid)
bixel_layer_render(layer.buffer, bixtel_buffer)
iter_2d(bixtel_buffer, function(y, x, v)
x = x + px - 1
y = y + py - 1
local upper_y = (y * 2) - 1
local lower_y = y * 2
if not box.pos_on_screen(x, y) then return end
local uch, ufg, ubg = rblit[upper_y][1][x], rblit[upper_y][2][x], rblit[upper_y][3][x]
local lch, lfg, lbg = rblit[lower_y][1][x], rblit[lower_y][2][x], rblit[lower_y][3][x]
local col_1 = v[1]
local col_2 = v[2]
local col_3 = v[3]
if uch and is_blit_transparent(ufg, ubg) then
apply_transparent(uch, ufg, ubg, col_1, box, rblit, x, upper_y)
elseif not uch then
rblit[upper_y][1][x] = "\143"
rblit[upper_y][2][x] = col_1
rblit[upper_y][3][x] = col_2
end
if lch and is_blit_transparent(lfg, lbg) then
apply_transparent(lch, lfg, lbg, col_3, box, rblit, x, lower_y)
elseif not lch then
rblit[lower_y][1][x] = "\131"
rblit[lower_y][2][x] = col_2
rblit[lower_y][3][x] = col_3
end
end)
layer.buffer = generate_buffer()
box.profiler.end_region(tid)
end,
text = function(lx, ly, text, fg, bg)
assert_int(lx)
assert_int(ly)
error("NYI", 2)
end,
set_pos = function(sx, sy)
assert_int(sx)
assert_int(sy)
px, py = sx, sy
end,
get_pos = function()
return px, py
end
}
insert_default_layer_funcs(box, layer)
return layer
end
---@param box ShrekBox
---@return Layer
local function new_text_layer(box, label)
local text_buffer = {}
local px, py = 1, 1
local layer
layer = {
label = label,
scale_x = 1,
scale_y = 1,
z = 0,
clear = function()
text_buffer = {}
layer.buffer = generate_buffer()
end,
buffer = generate_buffer(),
text = function(lx, ly, text, fg, bg)
assert_int(lx)
assert_int(ly)
local fgc = fg and blit_lut[fg] or shrekbox.contrast_char
local bgc = bg and blit_lut[bg] or shrekbox.transparent_char
for i = 1, #text do
local ch = text:sub(i, i)
set_index_2d(text_buffer, ly, lx + i - 1, {
ch,
fgc,
bgc
})
end
end,
pixel = function(lx, ly, color)
assert_int(lx)
assert_int(ly)
if color == shrekbox.transparent then
set_index_2d(text_buffer, ly, lx, nil)
return
end
set_index_2d(text_buffer, ly, lx, {
" ",
blit_lut[color],
blit_lut[color]
})
end,
_render = function(rblit)
local tid = layer.label or ("tr_" .. layer.z)
box.profiler.start_region(tid)
iter_2d(text_buffer, function(y, x, v)
x = x + px - 1
y = y + py - 1
if not box.pos_on_screen(x, y) then return end
local ofg, obg = v[2], v[3]
local ch, efg, ebg = rblit[y][1][x], rblit[y][2][x], rblit[y][3][x]
if ch and is_blit_transparent(efg, ebg) then
apply_transparent(ch, efg, ebg, obg, box, rblit, x, y)
return
elseif ch then
return
end
rblit[y][1][x] = v[1]
rblit[y][2][x] = ofg
rblit[y][3][x] = obg
end)
box.profiler.end_region(tid)
end,
set_pos = function(sx, sy)
assert_int(sx)
assert_int(sy)
px, py = sx, sy
end,
get_pos = function()
return px, py
end
}
insert_default_layer_funcs(box, layer)
return layer
end
---@diagnostic disable-next-line: undefined-global
local is_craftos_pc = not not periphemu
local epoch_unit = is_craftos_pc and "nano" or "utc"
local time_ms_divider = is_craftos_pc and 1000000 or 1
local time_divider = is_craftos_pc and 1000000000 or 1000
---@param root_name string
---@return Profiler
local function new_profiler(root_name)
---@class Profile
---@field total number
---@field frame number
---@field name string
---@field t0 number?
---@field children Profile[]
---@field child_map table<string,Profile>
---@field parent Profile?
---@field depth integer
---@field count number
---@field total_count number
---@field average_time_ms number
---@type Profile
local active_profile = nil
---@class Profiler
local profiler = {}
---@type table<string,boolean>
local hidden_regions = {}
---@type table<string,boolean>
local collapsed_regions = {}
---@type table<string,boolean>
local yield_regions = {}
local total_frames = 1
local active = false
---@param name string
local function add_profile(name)
---@type Profile
local region_timer = {
total = 0,
frame = 0,
name = name,
parent = active_profile,
children = {},
child_map = {},
depth = 0,
count = 0,
total_count = 0,
average_time_ms = 1
}
if active_profile then
region_timer.depth = active_profile.depth + 1
active_profile.children[#active_profile.children + 1] = region_timer
active_profile.child_map[name] = region_timer
end
return region_timer
end
local root_profile = add_profile(root_name)
---Start a new region, creating it if it hasn't been made before.
---Any code within this region will be timed.
---Multiple calls in the same frame (and parent region) are timed together.
---@param name string
function profiler.start_region(name)
if not active then return end
local child_region
if not active_profile and name == root_profile.name then
child_region = root_profile
else
child_region = active_profile.child_map[name]
end
if not child_region then
child_region = add_profile(name)
end
active_profile = child_region
---@diagnostic disable-next-line: param-type-mismatch
active_profile.t0 = os.epoch(epoch_unit)
end
---Mark the end of a started region, will error if there is some other active region
---@param name string
function profiler.end_region(name, _allow_empty)
if not active then return end
if not active_profile and _allow_empty then
return
end
assert(active_profile.name == name, ("Attempt to end region not active! %s"):format(name))
local delta = os.epoch(epoch_unit) - active_profile.t0
active_profile.frame = active_profile.frame + delta
active_profile.count = active_profile.count + 1
active_profile = active_profile.parent
end
---Start a new yield region, creating it if it hasn't been made before.
---Any code within this region will be timed and subtracted from the frametime.
---Multiple calls in the same frame (and parent region) are timed together.
---@param name string
function profiler.start_yield(name)
yield_regions[name] = true
profiler.start_region(name)
end
---Mark the end of a started yield region, will error if there is some other active region
---@param name string
function profiler.end_yield(name)
profiler.end_region(name)
end
function profiler.start_frame()
active = true
profiler.start_region(root_profile.name)
end
---@param profile Profile
local function profiler_end_frame(profile)
profile.total = profile.total + profile.frame
profile.total_count = profile.total_count + profile.count
if yield_regions[profile.name] then
local parent = assert(profile.parent)
repeat
parent.total = parent.total - profile.frame
parent = parent.parent
until not parent
end
profile.count = 0
profile.frame = 0
for i, v in ipairs(profile.children) do
profiler_end_frame(v)
end
end
function profiler.end_frame()
total_frames = total_frames + 1
profiler.end_region(root_profile.name, true)
profiler_end_frame(root_profile)
end
---@param layer Layer
---@param profile Profile
local function render_profile(layer, profile, y)
if hidden_regions[profile.name] then
return y
end
local s = ("|"):rep(profile.depth)
if collapsed_regions[profile.name] then
s = s .. "+"
else
s = s .. "\\"
end
local average_time_ms = profile.total / total_frames / time_ms_divider
profile.average_time_ms = average_time_ms
local average_count = profile.total_count / total_frames
local parent_time_ms = profile.parent and profile.parent.average_time_ms or average_time_ms
local percent = average_time_ms / parent_time_ms * 100
if percent ~= percent then percent = 100 end
percent = shrekbox.round(percent)
if yield_regions[profile.name] then
s = s .. "[YLD] "
percent = 0
else
s = s .. ("[%3d] "):format(percent)
end
s = s .. ("%s) avg:%.2fms, avg#:%.1f, #:%d"):format(profile.name,
average_time_ms, average_count, profile.count)
layer.text(1, y, s, colors.white, colors.black)
y = y + 1
if not collapsed_regions[profile.name] then
for i, v in ipairs(profile.children) do
y = render_profile(layer, v, y)
end
end
return y
end
---@param layer Layer
---@param y integer
function profiler.render(layer, y)
render_profile(layer, root_profile, y)
end
---Hide a profile and it's children by name
---@param name string
---@param hide boolean? true default
function profiler.hide(name, hide)
if hide == nil then hide = true end
hidden_regions[name] = hide
end
---Collapse (hide) a profile's children by name
---@param name string
---@param collapse boolean? true default
function profiler.collapse(name, collapse)
if collapse == nil then collapse = true end
collapsed_regions[name] = collapse
end
---@return Profile
function profiler._get_root()
return root_profile
end
return profiler
end
---@param win ccTweaked.Window
function shrekbox.new(win)
---@class ShrekBox
local box = {
overlay = false
}
local overlay_layer = new_text_layer(box, "overlay")
overlay_layer.z = math.huge
local background_layer = new_text_layer(box, "bg_fill")
overlay_layer.z = -math.huge
local layers = {}
local profiler = new_profiler("total")
-- By default collapse the internal shrekbox timings
-- You can uncollapse this by doing box.profiler.collapse("shrekbox", false)
profiler.collapse("shrekbox")
box.profiler = profiler
function box.sort_layers()
table.sort(layers, function(a, b)
return a.z > b.z -- sort this the opposite way, render front to back
end)
end
function box._get_window()
return win
end
---@param z number
---@param label string?
function box.add_pixel_layer(z, label)
local layer = new_pixel_layer(box, label)
layers[#layers + 1] = layer
layer.z = z
box.sort_layers()
return layer
end
---@param z number
---@param label string?
function box.add_bixel_layer(z, label)
local layer = new_bixel_layer(box, label)
layers[#layers + 1] = layer
layer.z = z
box.sort_layers()
return layer
end
---@param z number
---@param label string?
function box.add_text_layer(z, label)
local layer = new_text_layer(box, label)
layers[#layers + 1] = layer
layer.z = z
box.sort_layers()
return layer
end
---Fill the background layer with a color
---@param color ccTweaked.colors.color
function box.fill(color)
local ww, wh = win.getSize()
local s = (" "):rep(ww)
for y = 1, wh do
background_layer.text(1, y, s, color, color)
end
end
local total_time = 1
local last_frametime = 0
local total_frames = 0
local function render_debug()
local real_fps = total_frames / (total_time / time_divider)
local root = profiler._get_root()
local theoretical_fps = root.total_count / (root.total / time_divider)
if theoretical_fps ~= theoretical_fps then theoretical_fps = 0 end
overlay_layer.text(1, 1,
("%3dfps (t:%3dfps)"):format(real_fps, theoretical_fps),
colors.white, colors.black)
profiler.render(overlay_layer, 2)
end
---@type blitbuffer
local rblit = generate_rblit_buffer()
local t0 = os.epoch(epoch_unit)
local function _render()
profiler.end_region("user", true)
win.setVisible(false)
win.setCursorPos(1, 1)
---@diagnostic disable-next-line: param-type-mismatch
local t1 = os.epoch(epoch_unit)
last_frametime = (t1 - t0)
total_time = total_time + last_frametime
total_frames = total_frames + 1
---@diagnostic disable-next-line: param-type-mismatch
t0 = os.epoch(epoch_unit)
overlay_layer.clear()
if box.overlay then
render_debug()
overlay_layer._render(rblit)
end
profiler.end_frame()
profiler.start_frame()
profiler.start_region("shrekbox")
for i, v in ipairs(layers) do -- rendering from front to back
if not v.hidden then
v._render(rblit)
end
end
background_layer._render(rblit)
profiler.start_region("blit!")
render_blit(win, rblit, "a")
profiler.end_region("blit!")
profiler.end_region("shrekbox")
win.setVisible(true)
profiler.start_region("user")
end
function box.render()
local ok, err = pcall(_render)
if not ok then
term.clear()
term.setCursorPos(1, 1)
print("An error occured while rendering!")
error(err, 0)
end
end
---Check if a position is on the visible window
---@param sx number
---@param sy number
---@return boolean
function box.pos_on_screen(sx, sy)
local sw, sh = win.getSize()
if sx < 1 or sy < 1 then return false end
if sx > sw or sy > sh then return false end
return true
end
box.fill(colors.black)
return box
end
function shrekbox.load_file(fn)
local f = assert(fs.open(fn, "r"))
local s = f.readAll() --[[@as string]]
f.close()
return s
end
function shrekbox.save_file(fn, s)
local f = assert(fs.open(fn, "w"))
f.write(s)
f.close()
end
return shrekbox