diff --git a/keymap/CHANGELOG.md b/keymap/CHANGELOG.md new file mode 100644 index 0000000..13fa7e4 --- /dev/null +++ b/keymap/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to Keymap are documented in this file. + +## [1.3.1] - 2026-07-21 + +### Fixed + +- Prevented startup timeouts while parsing larger Niri, Hyprland, and MangoWC configurations. +- Prevented intermittent callback timeouts when rapidly switching keyboard modifier layers. + +### Changed + +- Accelerated stable fingerprints with Luau's native `bit32` operations while retaining a plain-Lua fallback. +- Avoided reading Niri's root configuration twice during a refresh. +- Made loading snapshots lightweight instead of serializing the previous bind tree again. +- Cached panel translations, settings, keyboard indexes, colors, and dynamic key callbacks between renders. + +### Tests + +- Added 93-bind scale regressions for Niri, Hyprland, and MangoWC. diff --git a/keymap/mangowc_service.luau b/keymap/mangowc_service.luau index 867619b..eaa7134 100644 --- a/keymap/mangowc_service.luau +++ b/keymap/mangowc_service.luau @@ -442,35 +442,30 @@ end -- FNV-1a over the exact source line. The editor recalculates this before a -- write so a bind is never replaced after the file changed behind its back. --- Multiplication is split into 16-bit words to retain exact 32-bit arithmetic --- in runtimes where Lua numbers are doubles. -local function xorLowByte(value, byte) - local low = value % 256 +-- Splitting the prime into 2^24 + 403 retains exact 32-bit arithmetic in +-- runtimes where Lua numbers are doubles. +local function xorByte(left, right) local result = 0 local place = 1 for _ = 1, 8 do - if low % 2 ~= byte % 2 then + if left % 2 ~= right % 2 then result = result + place end - low = math.floor(low / 2) - byte = math.floor(byte / 2) + left = math.floor(left / 2) + right = math.floor(right / 2) place = place * 2 end - return value - (value % 256) + result + return result end +local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte + fingerprint = function(rawSnippet) local hash = 2166136261 for index = 1, #rawSnippet do - hash = xorLowByte(hash, rawSnippet:byte(index)) - local low = hash % 65536 - local high = math.floor(hash / 65536) - local lowProduct = low * 403 - local resultLow = lowProduct % 65536 - local resultHigh = ( - math.floor(lowProduct / 65536) + low * 256 + high * 403 - ) % 65536 - hash = resultHigh * 65536 + resultLow + local low = hash % 256 + hash = hash - low + xorByteFast(low, rawSnippet:byte(index)) + hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296 end return string.format("%08x", hash) end @@ -918,15 +913,11 @@ function refresh() refreshing = true local source = configPath() - local previous = noctalia.state.get(SNAPSHOT_KEY) - local sameCompositor = type(previous) == "table" and previous.compositor == "MangoWC" - local previousCategories = sameCompositor and previous.categories or {} - local previousTotal = sameCompositor and previous.total or 0 - local previousUpdatedAt = sameCompositor and previous.updated_at or "" - local previousHidden = sameCompositor and previous.hidden or {} + -- The panel keeps its last ready snapshot while this status is loading. + -- Avoid serializing the complete bind tree a second time on every refresh. noctalia.state.set( SNAPSHOT_KEY, - snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden) + snapshot("loading", source, "", {}, 0, {}, "", {}) ) local parsed = parseConfig(source) diff --git a/keymap/niri_service.luau b/keymap/niri_service.luau index 1c860d9..f59ff07 100644 --- a/keymap/niri_service.luau +++ b/keymap/niri_service.luau @@ -452,14 +452,17 @@ local function xorByte(left, right) return result end --- FNV-1a 32-bit without bit libraries. Splitting the prime (0x01000193) into --- 2^24 + 403 keeps every intermediate below the exact integer limit of a --- Lua/Luau number. +local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte + +-- Splitting the FNV prime (0x01000193) into 2^24 + 403 keeps every +-- intermediate below the exact integer limit of a Lua/Luau number. Luau's +-- native bit32 fast path avoids eight interpreted operations for every byte; +-- the arithmetic fallback keeps the parser usable in plain-Lua tests. local function stableFingerprint(value) local hash = 2166136261 for index = 1, #value do local low = hash % 256 - hash = hash - low + xorByte(low, value:byte(index)) + hash = hash - low + xorByteFast(low, value:byte(index)) hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296 end return string.format("%08x", hash) @@ -688,7 +691,7 @@ local function parseBind(combo, attributes, action, category, records, activeByC activeByCombo[signature] = bind end -local function parseConfig(root) +local function parseConfig(root, rootSource) local records = {} local hidden = {} local activeByCombo = {} @@ -706,7 +709,7 @@ local function parseConfig(root) end local parseFile - parseFile = function(path, optional) + parseFile = function(path, optional, providedSource) if visiting[path] then warn("niri_include_cycle:" .. path) return @@ -715,7 +718,7 @@ local function parseConfig(root) warn("niri_file_limit_reached") return end - local source = noctalia.readFile(path) + local source = providedSource or noctalia.readFile(path) if type(source) ~= "string" then warn((optional and "niri_optional_include_missing:" or "niri_include_unreadable:") .. path) if not optional then @@ -858,7 +861,7 @@ local function parseConfig(root) visiting[path] = nil end - parseFile(root, false) + parseFile(root, false, rootSource) local categories, total = buildCategories(records) return categories, total, hidden, warnings, fatalError end @@ -897,18 +900,16 @@ function refresh() end refreshing = true local source = sourcePath() - local previous = noctalia.state.get(SNAPSHOT_KEY) - local keepPrevious = type(previous) == "table" and previous.compositor == "Niri" - local previousCategories = keepPrevious and previous.categories or {} - local previousTotal = keepPrevious and previous.total or 0 - local previousUpdatedAt = keepPrevious and previous.updated_at or "" - local previousHidden = keepPrevious and previous.hidden or {} + -- The panel keeps its last ready snapshot while this status is loading. + -- Do not copy the full bind tree through the shared-state serializer merely + -- to replace it again at the end of this synchronous refresh. noctalia.state.set( SNAPSHOT_KEY, - snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden) + snapshot("loading", source, "", {}, 0, {}, "", {}) ) - if type(noctalia.readFile(source)) ~= "string" then + local rootSource = noctalia.readFile(source) + if type(rootSource) ~= "string" then noctalia.state.set( SNAPSHOT_KEY, snapshot("error", source, "niri_config_unreadable", {}, 0, {}, os.date("%H:%M:%S")) @@ -917,7 +918,7 @@ function refresh() return end - local categories, total, hidden, warnings, fatalError = parseConfig(source) + local categories, total, hidden, warnings, fatalError = parseConfig(source, rootSource) if fatalError ~= nil then noctalia.state.set( SNAPSHOT_KEY, diff --git a/keymap/panel.luau b/keymap/panel.luau index 837264e..677c254 100644 --- a/keymap/panel.luau +++ b/keymap/panel.luau @@ -112,12 +112,36 @@ do end end +local NIL_CACHE_VALUE = {} +local configCache = {} +local translationCache = {} + local function tr(key, args) - return noctalia.tr(key, args) + if args ~= nil then + return noctalia.tr(key, args) + end + local cached = translationCache[key] + if cached ~= nil then + return cached + end + local value = noctalia.tr(key) + translationCache[key] = value + return value end local function cfg(key) - return noctalia.getConfig(key) + local cached = configCache[key] + if cached ~= nil then + return cached ~= NIL_CACHE_VALUE and cached or nil + end + local value = noctalia.getConfig(key) + configCache[key] = value ~= nil and value or NIL_CACHE_VALUE + return value +end + +local function clearHostValueCaches() + configCache = {} + translationCache = {} end local function asString(value, fallback) @@ -297,7 +321,13 @@ local function expandedKeys(value) return { canonical } end +local keyboardIndexSnapshot = nil +local keyboardIndexCache = nil + local function keyboardIndex() + if keyboardIndexSnapshot == snapshot and keyboardIndexCache ~= nil then + return keyboardIndexCache + end local exact = {} local any = {} for _, category in ipairs(asArray(snapshot.categories)) do @@ -318,14 +348,20 @@ local function keyboardIndex() end end end - return { exact = exact, any = any } + keyboardIndexSnapshot = snapshot + keyboardIndexCache = { exact = exact, any = any } + return keyboardIndexCache end +local keyCallbackCache = {} local function keyCallback(id, code) local name = "onKeyboardKey_" .. id - return registerDynamicCallback(name, function() - selectKeyboardKey(code) - end) + local callback = keyCallbackCache[name] + if callback == nil then + callback = function() selectKeyboardKey(code) end + keyCallbackCache[name] = callback + end + return registerDynamicCallback(name, callback) end local function contains(haystack, needle) @@ -848,21 +884,32 @@ local function keyboardUnitsWidth(units) return math.max(1, math.floor(value * KEYBOARD_UNIT + (value - 1) * KEYBOARD_GAP + 0.5)) end +local colorWithOpacityCache = {} local function colorWithOpacity(value, fallback, opacity) + local cacheKey = asString(value) .. "\0" .. asString(fallback) .. "\0" .. tostring(opacity) + local cached = colorWithOpacityCache[cacheKey] + if cached ~= nil then return cached end local color = asString(value, fallback) + local result if string.sub(color, 1, 1) == "#" then local alpha = string.format("%02X", math.floor(math.max(0, math.min(1, opacity)) * 255 + 0.5)) if #color == 7 then - return color .. alpha + result = color .. alpha elseif #color == 9 then - return string.sub(color, 1, 7) .. alpha + result = string.sub(color, 1, 7) .. alpha + else + result = color end - return color + else + result = color .. "/" .. string.format("%.2f", opacity) end - return color .. "/" .. string.format("%.2f", opacity) + colorWithOpacityCache[cacheKey] = result + return result end -local function keyboardKeyNode(spec, index) +local EMPTY_ENTRIES = {} + +local function keyboardKeyNode(spec, index, signature) if spec.spacer ~= nil then -- `ui.spacer` is flexible: inside a fixed-width row it absorbs remaining -- space and moves every key that follows it. Keyboard gaps must be rigid, @@ -892,9 +939,8 @@ local function keyboardKeyNode(spec, index) end local code = canonicalKey(spec.code) - local signature = activeModifierSignature() - local exactEntries = asArray(index.exact[signature .. "|" .. code]) - local anyEntries = asArray(index.any[code]) + local exactEntries = index.exact[signature .. "|" .. code] or EMPTY_ENTRIES + local anyEntries = index.any[code] or EMPTY_ENTRIES local physicalModifier = MODIFIER_ALIASES[normalized(spec.code)] local modifierActive = physicalModifier ~= nil and activeModifiers[physicalModifier] == true local selected = creatorOpen and creatorHasKey(code) or selectedKeyboardKey == code @@ -1571,7 +1617,7 @@ local function keyboardBody() for rowIndex, row in ipairs(layout.rows) do local keys = {} for _, spec in ipairs(row) do - keys[#keys + 1] = keyboardKeyNode(spec, index) + keys[#keys + 1] = keyboardKeyNode(spec, index, signature) end keyboardRows[#keyboardRows + 1] = ui.row({ key = "keyboard-row-" .. layout.id .. "-" .. tostring(rowIndex), @@ -2627,7 +2673,7 @@ end render = function() renderCallbackNames = {} local status = asString(snapshot.status, "idle") - local categories = filteredCategories() + local categories = viewMode == "list" and filteredCategories() or EMPTY_ENTRIES local requestedColumns = math.max(1, math.min(4, math.floor(tonumber(cfg("columns")) or 3))) local body if status == "error" then @@ -2728,6 +2774,7 @@ noctalia.state.watch(UPDATE_RESULT_KEY, function(result) end) function onOpen(_context) + clearHostValueCaches() local configuredLayout = asString(cfg("keyboard_layout"), "100") keyboardLayoutId = KEYBOARD_LAYOUTS[configuredLayout] ~= nil and configuredLayout or "100" local current = noctalia.state.get(SNAPSHOT_KEY) @@ -2755,6 +2802,7 @@ function onOpen(_context) end function onConfigChanged() + clearHostValueCaches() render() end diff --git a/keymap/plugin.toml b/keymap/plugin.toml index aad8572..b2fe007 100644 --- a/keymap/plugin.toml +++ b/keymap/plugin.toml @@ -1,6 +1,6 @@ id = "blackbartblues/keymap" name = "Keymap" -version = "1.3.0" +version = "1.3.1" plugin_api = 5 author = "blackbartblues" license = "MIT" diff --git a/keymap/service.luau b/keymap/service.luau index 3ba0c7f..72299fc 100644 --- a/keymap/service.luau +++ b/keymap/service.luau @@ -314,11 +314,13 @@ local function xorByte(left, right) return result end +local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte + local function fingerprint(value) local hash = 2166136261 for index = 1, #value do local low = hash % 256 - hash = hash - low + xorByte(low, value:byte(index)) + hash = hash - low + xorByteFast(low, value:byte(index)) hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296 end return string.format("%08x", hash) @@ -1003,14 +1005,11 @@ function refresh() refreshing = true local source = expandedConfigPath() - local previous = noctalia.state.get(SNAPSHOT_KEY) - local previousCategories = type(previous) == "table" and previous.categories or {} - local previousTotal = type(previous) == "table" and previous.total or 0 - local previousUpdatedAt = type(previous) == "table" and previous.updated_at or "" - local previousHidden = type(previous) == "table" and previous.hidden or {} + -- The panel keeps its last ready snapshot while this status is loading. + -- Avoid serializing the complete bind tree a second time on every refresh. noctalia.state.set( SNAPSHOT_KEY, - snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden) + snapshot("loading", source, "", {}, 0, {}, "", {}) ) if not noctalia.commandExists("hyprctl") then diff --git a/keymap/tests/hypr_scale_test.lua b/keymap/tests/hypr_scale_test.lua new file mode 100644 index 0000000..a4a1bc9 --- /dev/null +++ b/keymap/tests/hypr_scale_test.lua @@ -0,0 +1,87 @@ +local sourceLines = {} +local liveBinds = {} +for index = 1, 93 do + if index % 20 == 1 then + sourceLines[#sourceLines + 1] = "-- " .. tostring(math.floor(index / 20) + 1) .. ". Group " + .. tostring(math.floor(index / 20) + 1) + end + sourceLines[#sourceLines + 1] = string.format( + 'hl.bind("SUPER+Key%d", action, { description = "Action %d" })', index, index + ) + liveBinds[#liveBinds + 1] = { + modmask = 64, key = "Key" .. tostring(index), description = "Action " .. tostring(index), + has_description = true, dispatcher = "__lua", arg = tostring(index), submap = "", + } +end +local source = table.concat(sourceLines, "\n") + +local values, watchers = { + ["keymap.snapshot"] = { + status = "ready", compositor = "Hyprland", total = 1, + categories = { { name = "Previous", binds = { { id = "previous" } } } }, + }, +}, {} +local reads, xorCalls, loadingCategoryCount = 0, 0, -1 +local originalBit32 = bit32 +bit32 = { + bxor = function(left, right) + xorCalls = xorCalls + 1 + local result, place = 0, 1 + for _ = 1, 8 do + if left % 2 ~= right % 2 then result = result + place end + left = math.floor(left / 2) + right = math.floor(right / 2) + place = place * 2 + end + return result + end, +} + +noctalia = { + state = { + get = function(key) return values[key] end, + set = function(key, value) + if key == "keymap.snapshot" and value.status == "loading" then + loadingCategoryCount = #(value.categories or {}) + end + values[key] = value + if watchers[key] ~= nil then watchers[key](value) end + end, + watch = function(key, callback) watchers[key] = callback end, + }, + getConfig = function(key) + return ({ + compositor = "hyprland", hyprland_config = "/fixture/hyprland.lua", + merge_sequential = false, show_undescribed = true, + })[key] + end, + getenv = function(key) return key == "HYPRLAND_INSTANCE_SIGNATURE" and "test" or "" end, + expandPath = function(path) return path end, + fileExists = function(path) return path == "/fixture/hyprland.lua" end, + listDir = function() return nil end, + readFile = function(path) + if path ~= "/fixture/hyprland.lua" then return nil end + reads = reads + 1 + return source + end, + commandExists = function(command) return command == "hyprctl" end, + json = { decode = function() return liveBinds end }, + runAsync = function(command, callback) + callback({ exitCode = 0, timedOut = false, stdout = "fixture" }) + return true + end, + tr = function(key) return key == "category.other" and "Other" or key end, +} + +assert(loadfile("service.luau"))() +bit32 = originalBit32 + +local snapshot = values["keymap.snapshot"] +assert(snapshot.status == "ready", "large Hyprland fixture did not parse") +assert(snapshot.total == 93, "large Hyprland fixture lost binds") +assert(#snapshot.categories == 5, "large Hyprland fixture lost category markers") +assert(reads == 1, "Hyprland root config should only be read once per refresh") +assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") +assert(xorCalls > 0, "Hyprland parser did not use the native-xor fingerprint path") + +print("hypr scale tests: ok") diff --git a/keymap/tests/mangowc_scale_test.lua b/keymap/tests/mangowc_scale_test.lua new file mode 100644 index 0000000..7478586 --- /dev/null +++ b/keymap/tests/mangowc_scale_test.lua @@ -0,0 +1,75 @@ +local sourceLines = {} +for index = 1, 93 do + if index % 20 == 1 then + sourceLines[#sourceLines + 1] = "# Group " .. tostring(math.floor(index / 20) + 1) + end + sourceLines[#sourceLines + 1] = string.format( + 'bind=SUPER,Key%d,spawn_shell,true #"Action %d"', index, index + ) +end +local source = table.concat(sourceLines, "\n") + +local values, watchers = { + ["keymap.snapshot"] = { + status = "ready", compositor = "MangoWC", total = 1, + categories = { { name = "Previous", binds = { { id = "previous" } } } }, + }, +}, {} +local reads, xorCalls, loadingCategoryCount = 0, 0, -1 +local originalBit32 = bit32 +bit32 = { + bxor = function(left, right) + xorCalls = xorCalls + 1 + local result, place = 0, 1 + for _ = 1, 8 do + if left % 2 ~= right % 2 then result = result + place end + left = math.floor(left / 2) + right = math.floor(right / 2) + place = place * 2 + end + return result + end, +} + +noctalia = { + state = { + get = function(key) return values[key] end, + set = function(key, value) + if key == "keymap.snapshot" and value.status == "loading" then + loadingCategoryCount = #(value.categories or {}) + end + values[key] = value + if watchers[key] ~= nil then watchers[key](value) end + end, + watch = function(key, callback) watchers[key] = callback end, + }, + getConfig = function(key) + return ({ compositor = "mangowc", mangowc_config = "/fixture/config.conf", merge_sequential = false })[key] + end, + getenv = function(key) return key == "MANGO_INSTANCE_SIGNATURE" and "test" or "" end, + expandPath = function(path) return path end, + fileExists = function(path) return path == "/fixture/config.conf" end, + listDir = function() return nil end, + readFile = function(path) + if path ~= "/fixture/config.conf" then return nil end + reads = reads + 1 + return source + end, + tr = function(key, args) + if args ~= nil and args.action ~= nil then return args.action end + return key == "category.other" and "Other" or key + end, +} + +assert(loadfile("mangowc_service.luau"))() +bit32 = originalBit32 + +local snapshot = values["keymap.snapshot"] +assert(snapshot.status == "ready", "large MangoWC fixture did not parse") +assert(snapshot.total == 93, "large MangoWC fixture lost binds") +assert(#snapshot.categories == 5, "large MangoWC fixture lost category markers") +assert(reads == 1, "MangoWC root config should only be read once per refresh") +assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") +assert(xorCalls > 0, "MangoWC parser did not use the native-xor fingerprint path") + +print("MangoWC scale tests: ok") diff --git a/keymap/tests/niri_scale_test.lua b/keymap/tests/niri_scale_test.lua new file mode 100644 index 0000000..69a9299 --- /dev/null +++ b/keymap/tests/niri_scale_test.lua @@ -0,0 +1,84 @@ +local bindLines = { "binds {" } +for index = 1, 93 do + if index % 20 == 1 then + bindLines[#bindLines + 1] = ' // #"Group ' .. tostring(math.floor(index / 20) + 1) .. '"' + end + bindLines[#bindLines + 1] = string.format( + ' Mod+Key%d repeat=false cooldown-ms=150 { focus-workspace %d; }', index, index + ) +end +bindLines[#bindLines + 1] = "}" +local source = table.concat(bindLines, "\n") + +local values, watchers = { + ["keymap.snapshot"] = { + status = "ready", compositor = "Niri", total = 1, + categories = { { name = "Previous", binds = { { id = "previous" } } } }, + }, +}, {} +local reads = 0 +local xorCalls = 0 +local loadingCategoryCount = -1 +local originalBit32 = bit32 +bit32 = { + bxor = function(left, right) + xorCalls = xorCalls + 1 + local result, place = 0, 1 + for _ = 1, 8 do + if left % 2 ~= right % 2 then result = result + place end + left = math.floor(left / 2) + right = math.floor(right / 2) + place = place * 2 + end + return result + end, +} + +noctalia = { + state = { + get = function(key) return values[key] end, + set = function(key, value) + if key == "keymap.snapshot" and value.status == "loading" then + loadingCategoryCount = #(value.categories or {}) + end + values[key] = value + if watchers[key] ~= nil then watchers[key](value) end + end, + watch = function(key, callback) watchers[key] = callback end, + }, + getConfig = function(key) + return ({ compositor = "niri", niri_config = "/fixture/config.kdl", merge_sequential = false })[key] + end, + getenv = function(key) return key == "NIRI_SOCKET" and "test" or "" end, + fileExists = function(path) return path == "/fixture/config.kdl" end, + listDir = function() return nil end, + readFile = function(path) + if path ~= "/fixture/config.kdl" then return nil end + reads = reads + 1 + return source + end, + tr = function(key, args) + if args ~= nil and args.action ~= nil then return args.action end + return key == "category.other" and "Other" or key + end, +} + +assert(loadfile("niri_service.luau"))() +bit32 = originalBit32 + +local snapshot = values["keymap.snapshot"] +assert(snapshot.status == "ready", "large Niri fixture did not parse") +assert(snapshot.total == 93, "large Niri fixture lost binds") +assert(#snapshot.categories == 5, "large Niri fixture lost category markers") +assert(reads == 1, "Niri root config should only be read once per refresh") +assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") +assert(xorCalls > 0, "Niri parser did not use the native-xor fingerprint path") + +for _, category in ipairs(snapshot.categories) do + for _, bind in ipairs(category.binds) do + assert(bind.id:match("^niri:[0-9a-f]+$"), "invalid bind fingerprint") + assert(bind.fingerprint:match("^[0-9a-f]+$"), "invalid source fingerprint") + end +end + +print("niri scale tests: ok") diff --git a/keymap/writer_service.luau b/keymap/writer_service.luau index 675f121..35c25ea 100644 --- a/keymap/writer_service.luau +++ b/keymap/writer_service.luau @@ -756,21 +756,24 @@ processValidatedRequest = function(request) verifyAndReload(request, transaction) end -local function contentFingerprint(value) - local function xorByte(left, right) - local result, place = 0, 1 - for _ = 1, 8 do - if left % 2 ~= right % 2 then result = result + place end - left = math.floor(left / 2) - right = math.floor(right / 2) - place = place * 2 - end - return result +local function xorByte(left, right) + local result, place = 0, 1 + for _ = 1, 8 do + if left % 2 ~= right % 2 then result = result + place end + left = math.floor(left / 2) + right = math.floor(right / 2) + place = place * 2 end + return result +end + +local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte + +local function contentFingerprint(value) local hash = 2166136261 for index = 1, #value do local low = hash % 256 - hash = hash - low + xorByte(low, value:byte(index)) + hash = hash - low + xorByteFast(low, value:byte(index)) hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296 end return string.format("%08x", hash)