local ok, failure = pcall(function() local preset = (function() local GROUP, MODE, INTENSITY, GLOW, RADIUS, RED, GREEN, BLUE, FADE_MS, FLAGS, RECORD_SIZE, PREFIX, PAGE_SIZE, create, presetCount, pageKeys, label, select, setMode, setIntensity, setFadeMs, setFullbright, save, selectedKey, currentMode, currentIntensity, currentGlow, currentRadius, currentFadeMs, isEnabled, isFullbright, currentColor GROUP = 0 -- u8 MODE = 1 -- u8, zero-based mode ID INTENSITY = 2 -- u8, whole percent GLOW = 3 -- u8, whole percent RADIUS = 4 -- u8, whole meters RED = 5 -- u8, sRGB GREEN = 6 -- u8, sRGB BLUE = 7 -- u8, sRGB FADE_MS = 8 -- u16, milliseconds FLAGS = 10 -- enabled, fullbright, castLight, sync, ownerOnly RECORD_SIZE = 11 PREFIX = "p:" -- one linkset data key per preset, e.g. p:Evening PAGE_SIZE = 10 -- dialog labels per page, leaving two navigation buttons local PATTERN = "^p:" local MODES = {"steady", "fade", "pulse", "cycle"} local ENABLED = bit32.lshift(1, 0) local FULLBRIGHT = bit32.lshift(1, 1) local key = "" local record --- Read once per selection and kept in variables for code that runs often. local mode = "steady" local intensity = 0 local glow = 0 local radius = 0 local fadeMs = 0 local flags = 0 local color = vector.create(0, 0, 0) --- Save a new preset with default settings. Returns false if the name is taken or the write fails. function create(name: string): boolean assert(#name >= 1 and #name <= 24, "names need 1 to 24 bytes") local presetKey = PREFIX .. name if ll.LinksetDataRead(presetKey) ~= "" then return false end local data = buffer.create(RECORD_SIZE) buffer.writeu8(data, GROUP, 1) buffer.writeu8(data, INTENSITY, 75) buffer.writeu8(data, GLOW, 5) buffer.writeu8(data, RADIUS, 10) buffer.writeu8(data, RED, 255) buffer.writeu8(data, GREEN, 200) buffer.writeu8(data, BLUE, 128) buffer.writeu16(data, FADE_MS, 250) buffer.writeu8(data, FLAGS, ENABLED) return ll.LinksetDataWrite( presetKey, llbase64.encode(data) ) == LINKSETDATA_OK end --- Counts saved presets without loading their keys. function presetCount(): number return ll.LinksetDataCountFound(PATTERN) end --- Keys for one dialog page, in alphabetical order. SLua's `ll` counts `start` from 1. function pageKeys(page: number) return ll.LinksetDataFindKeys(PATTERN, page * PAGE_SIZE + 1, PAGE_SIZE) end --- The button label for a key: its name without the prefix. function label(presetKey: string): string return string.sub(presetKey, #PREFIX + 1) end --- Load one preset and read its values. Returns false if the key holds no preset. function select(presetKey: string): boolean local saved = ll.LinksetDataRead(presetKey) if saved == "" then return false end key = presetKey record = llbase64.decode(saved, true) mode = MODES[buffer.readu8(record, MODE) + 1] intensity = buffer.readu8(record, INTENSITY) / 100 glow = buffer.readu8(record, GLOW) / 100 radius = buffer.readu8(record, RADIUS) fadeMs = buffer.readu16(record, FADE_MS) flags = buffer.readu8(record, FLAGS) color = ll.sRGB2Linear(vector.create( buffer.readu8(record, RED) / 255, buffer.readu8(record, GREEN) / 255, buffer.readu8(record, BLUE) / 255 )) return true end function setMode(modeId: number) assert( modeId >= 0 and modeId < #MODES and modeId == math.floor(modeId), "invalid mode" ) buffer.writeu8(record, MODE, modeId) mode = MODES[modeId + 1] end function setIntensity(value: number) assert(value >= 0 and value <= 1, "intensity must be between 0 and 1") local percent = math.floor(value * 100 + 0.5) buffer.writeu8(record, INTENSITY, percent) intensity = percent / 100 end function setFadeMs(value: number) assert( value >= 0 and value <= 65535 and value == math.floor(value), "invalid fade duration" ) buffer.writeu16(record, FADE_MS, value) fadeMs = value end function setFullbright(enabled: boolean) flags = enabled and bit32.bor(flags, FULLBRIGHT) or bit32.band( flags, bit32.bnot(FULLBRIGHT) ) buffer.writeu8(record, FLAGS, flags) end --- Write the selected preset back to its own key. Keep the edit if this returns false. function save(): boolean local status = ll.LinksetDataWrite( key, llbase64.encode(record) ) return status == LINKSETDATA_OK or status == LINKSETDATA_NOUPDATE end function selectedKey(): string return key end function currentMode(): string return mode end function currentIntensity(): number return intensity end function currentGlow(): number return glow end function currentRadius(): number return radius end function currentFadeMs(): number return fadeMs end function isEnabled(): boolean return bit32.band(flags, ENABLED) ~= 0 end function isFullbright(): boolean return bit32.band(flags, FULLBRIGHT) ~= 0 end --- Linear color for `PRIM_POINT_LIGHT`, converted once per selection. function currentColor() return color end return { GROUP = GROUP, MODE = MODE, INTENSITY = INTENSITY, GLOW = GLOW, RADIUS = RADIUS, RED = RED, GREEN = GREEN, BLUE = BLUE, FADE_MS = FADE_MS, FLAGS = FLAGS, RECORD_SIZE = RECORD_SIZE, PREFIX = PREFIX, PAGE_SIZE = PAGE_SIZE, create = create, presetCount = presetCount, pageKeys = pageKeys, label = label, select = select, setMode = setMode, setIntensity = setIntensity, setFadeMs = setFadeMs, setFullbright = setFullbright, save = save, selectedKey = selectedKey, currentMode = currentMode, currentIntensity = currentIntensity, currentGlow = currentGlow, currentRadius = currentRadius, currentFadeMs = currentFadeMs, isEnabled = isEnabled, isFullbright = isFullbright, currentColor = currentColor } end)() -- Run on a disposable object with no other p: keys. This test creates and removes its own. local function assertNear(actual: number, expected: number) assert(math.abs(actual - expected) < 0.000001, `expected {expected}, got {actual}`) end local function expectFailure(callback: () -> ()) local ok = pcall(callback) assert(not ok, "invalid input was accepted") end assert(preset.presetCount() == 0, "remove existing p: keys first") assert(not preset.select("p:Missing")) for _, name in { "Movie", "Evening", "Aurora" } do assert(preset.create(name)) end assert(not preset.create("Evening")) assert(preset.presetCount() == 3) local page = preset.pageKeys(0) assert(#page == 3 and page[1] == "p:Aurora" and page[2] == "p:Evening" and page[3] == "p:Movie") assert(#preset.pageKeys(1) == 0) assert(preset.label(page[2]) == "Evening") assert(preset.select("p:Evening") and preset.selectedKey() == "p:Evening") assert(preset.currentMode() == "steady" and preset.currentIntensity() == 0.75) assert(preset.currentGlow() == 0.05 and preset.currentRadius() == 10 and preset.currentFadeMs() == 250) assert(preset.isEnabled() and not preset.isFullbright()) assertNear(preset.currentColor().x, 1) assertNear(preset.currentColor().y, 0.5775804) -- 0.29 * 100 is just below 29, so the setter rounds before its integer write. preset.setIntensity(0.29) assert(preset.currentIntensity() == 0.29) preset.setMode(2) preset.setFadeMs(65535) preset.setFullbright(true) assert(preset.save()) -- Unchanged text reports LINKSETDATA_NOUPDATE, which still counts as saved. assert(preset.save()) assert(preset.select("p:Movie")) assert(preset.currentIntensity() == 0.75 and not preset.isFullbright()) assert(preset.select("p:Evening")) assert(preset.currentIntensity() == 0.29 and preset.currentMode() == "pulse") assert(preset.currentFadeMs() == 65535 and preset.isFullbright() and preset.isEnabled()) preset.setFullbright(false) assert(not preset.isFullbright() and preset.isEnabled()) -- A refused write keeps the edit in memory for a retry. local previous = ll.LinksetDataRead("p:Evening") assert(ll.LinksetDataDelete("p:Evening") == LINKSETDATA_OK) assert(ll.LinksetDataWriteProtected("p:Evening", previous, "example-test") == LINKSETDATA_OK) assert(not preset.save()) assert(not preset.isFullbright()) assert(ll.LinksetDataDeleteProtected("p:Evening", "example-test") == LINKSETDATA_OK) assert(preset.save()) assert(preset.select("p:Evening") and not preset.isFullbright()) expectFailure(function() preset.create("") end) expectFailure(function() preset.create("ABCDEFGHIJKLMNOPQRSTUVWXY") end) expectFailure(function() preset.setIntensity(-0.01) end) expectFailure(function() preset.setIntensity(1.01) end) expectFailure(function() preset.setIntensity(0 / 0) end) expectFailure(function() preset.setMode(4) end) expectFailure(function() preset.setMode(0.5) end) expectFailure(function() preset.setFadeMs(70000) end) expectFailure(function() preset.setFadeMs(0.5) end) for _, presetKey in preset.pageKeys(0) do assert(ll.LinksetDataDelete(presetKey) == LINKSETDATA_OK) end assert(preset.presetCount() == 0) print("Result: PASS; presets=3") end) if not ok then local message = string.gsub(tostring(failure), "[%c;=]", " ") message = string.sub(message, 1, 800) if not string.find(message, "%S") then message = "runtime error" end print("Result: FAIL; error=" .. message) end