first commit

This commit is contained in:
2021-12-18 20:10:30 +01:00
parent 4d04f63b99
commit 215bed9faf
108 changed files with 3630 additions and 0 deletions

15
lib/knife/base.lua Normal file
View File

@@ -0,0 +1,15 @@
return {
extend = function (self, subtype)
subtype = subtype or {}
local meta = { __index = subtype }
return setmetatable(subtype, {
__index = self,
__call = function (self, ...)
local instance = setmetatable({}, meta)
return instance, instance:constructor(...)
end
})
end,
constructor = function () end,
}

78
lib/knife/behavior.lua Normal file
View File

@@ -0,0 +1,78 @@
-- behavior.lua -- a state manager
-- internal/external api
local function getCurrentFrame (behavior)
return behavior.states[behavior.state][behavior.index]
end
local function advanceFrame (behavior)
local nextState = behavior.frame.after
local nextIndex = behavior.index + 1
local maxIndex = #behavior.states[behavior.state]
if nextState then
behavior.state = nextState
nextIndex = 1
elseif nextIndex > maxIndex then
nextIndex = 1
end
behavior.index = nextIndex
behavior.frame = behavior:getCurrentFrame()
end
local function performAction (behavior)
local act = behavior.frame.action
if act then
act(behavior, behavior.subject)
end
end
-- external api
local function update (behavior, dt)
behavior.elapsed = behavior.elapsed + dt
while behavior.elapsed >= behavior.frame.duration do
behavior.elapsed = behavior.elapsed - behavior.frame.duration
behavior:advanceFrame()
behavior:performAction()
end
return behavior
end
local function setState (behavior, state, index)
behavior.state = state
behavior.index = index or 1
behavior.elapsed = 0
behavior.frame = behavior:getCurrentFrame()
behavior:performAction()
return behavior
end
-- behavior factory
return function (states, subject)
local behavior = {
states = states,
subject = subject,
elapsed = 0,
state = 'default',
index = 1,
-- internal api
getCurrentFrame = getCurrentFrame,
advanceFrame = advanceFrame,
performAction = performAction,
-- external api
update = update,
setState = setState
}
behavior.frame = behavior:getCurrentFrame()
behavior:performAction()
return behavior
end

27
lib/knife/bind.lua Normal file
View File

@@ -0,0 +1,27 @@
local loadstring = _G.loadstring or _G.load
local tconcat = table.concat
local helperCache = {}
local function buildHelper (argCount)
if helperCache[argCount] then
return helperCache[argCount]
end
local argList1 = { 'f' }
local argList2 = {}
for index = 1, argCount do
argList1[index + 1] = 'a' .. index
argList2[index] = 'a' .. index
end
argList2[argCount + 1] = '...'
local source = 'return function(' .. tconcat(argList1, ', ') ..
') return function(...) return f(' .. tconcat(argList2, ', ') ..
') end end'
local helper = loadstring(source)()
helperCache[argCount] = helper
return helper
end
return function (func, ...)
return buildHelper(select('#', ...))(func, ...)
end

31
lib/knife/chain.lua Normal file
View File

@@ -0,0 +1,31 @@
local function Invoker (links, index)
return function (...)
local link = links[index]
if not link then
return
end
local continue = Invoker(links, index + 1)
local returned = link(continue, ...)
if returned then
returned(function (_, ...) continue(...) end)
end
end
end
return function (...)
local links = { ... }
local function chain (...)
if not (...) then
return Invoker(links, 1)(select(2, ...))
end
local offset = #links
for index = 1, select('#', ...) do
links[offset + index] = select(index, ...)
end
return chain
end
return chain
end

54
lib/knife/convoke.lua Normal file
View File

@@ -0,0 +1,54 @@
return function (routine)
local routines = { routine }
local routineIndex = 1
local isFinished = false
local function execute ()
local continueCount = 0
local run
local function continue ()
continueCount = continueCount + 1
return function (...)
continueCount = continueCount - 1
if continueCount == 0 then
return run(...)
end
end
end
local function wait (...)
return coroutine.yield(...)
end
local r = coroutine.create(function ()
isFinished = false
while routineIndex <= #routines do
routines[routineIndex](continue, wait)
continueCount = 0
routineIndex = routineIndex + 1
end
isFinished = true
end)
run = function (...)
return coroutine.resume(r, ...)
end
run()
end
local function appendOrExecute (routine)
if routine then
routines[#routines + 1] = routine
if isFinished then
execute()
end
return appendOrExecute
else
execute()
end
end
return appendOrExecute
end

111
lib/knife/event.lua Normal file
View File

@@ -0,0 +1,111 @@
-- require('mobdebug').start()
-- Event module
local Event = {}
-- Event handler registry
Event.handlers = {}
-- Remove an event handler from the registry
local function remove (self)
if not self.isRegistered then
return self
end
if self.prevHandler then
self.prevHandler.nextHandler = self.nextHandler
end
if self.nextHandler then
self.nextHandler.prevHandler = self.prevHandler
end
if Event.handlers[self.name] == self then
Event.handlers[self.name] = self.nextHandler
end
self.prevHandler = nil
self.nextHandler = nil
self.isRegistered = false
return self
end
-- Insert an event handler into the registry
local function register (self)
if self.isRegistered then
return self
end
self.nextHandler = Event.handlers[self.name]
if self.nextHandler then
self.nextHandler.prevHandler = self
end
Event.handlers[self.name] = self
self.isRegistered = true
return self
end
-- Create an event handler
local function Handler (name, callback)
return {
name = name,
callback = callback,
isRegistered = false,
remove = remove,
register = register
}
end
-- Create and register a new event handler
function Event.on (name, callback)
return register(Handler(name, callback))
end
-- Dispatch an event
function Event.dispatch (name, ...)
local handler = Event.handlers[name]
while handler do
if handler.callback(...) == false then
return handler
end
handler = handler.nextHandler
end
end
local function isCallable (value)
return type(value) == 'function' or
getmetatable(value) and getmetatable(value).__call
end
-- Inject a dispatcher into a table.
local function hookDispatcher (t, key)
local original = t[key]
if isCallable(original) then
t[key] = function (...)
original(...)
return Event.dispatch(key, ...)
end
else
t[key] = function (...)
return Event.dispatch(key, ...)
end
end
end
-- Inject dispatchers into a table. Examples:
-- Event.hook(love.handlers)
-- Event.hook(love, { 'load', 'update', 'draw' })
function Event.hook (t, keys)
if keys then
for _, key in ipairs(keys) do
hookDispatcher(t, key)
end
else
for key in pairs(t) do
hookDispatcher(t, key)
end
end
end
return Event

2
lib/knife/gun.lua Normal file
View File

@@ -0,0 +1,2 @@
-- GUN (Give Up Now)
os.exit()

77
lib/knife/memoize.lua Normal file
View File

@@ -0,0 +1,77 @@
local loadstring = _G.loadstring or _G.load
local weakKeys = { __mode = 'k' }
local cache = setmetatable({}, weakKeys)
local resultsKey = {}
local nilKey = {}
local function getMetaCall (callable)
local meta = getmetatable(callable)
return meta and meta.__call
end
local tupleConstructorCache = {}
local function buildTupleConstructor (n)
if tupleConstructorCache[n] then
return tupleConstructorCache[n]
end
local t = {}
for i = 1, n do
t[i] = "a" .. i
end
local args = table.concat(t, ',')
local ctor = loadstring('return function(' .. args ..
') return function() return ' .. args .. ' end end')()
tupleConstructorCache[n] = ctor
return ctor
end
local function tuple (...)
return buildTupleConstructor(select('#', ...))(...)
end
return function (callable)
local metaCall = getMetaCall(callable)
if type(callable) ~= 'function' and not metaCall then
error 'Attempted to memoize a non-callable value.'
end
cache[callable] = setmetatable({}, weakKeys)
local function run (...)
local node = cache[callable]
local argc = select('#', ...)
for i = 1, argc do
local key = select(i, ...)
if key == nil then
key = nilKey
end
if not node[key] then
node[key] = setmetatable({}, weakKeys)
end
node = node[key]
end
if not node[resultsKey] then
node[resultsKey] = tuple(callable(...))
end
return node[resultsKey]()
end
if metaCall then
return function (...)
local call = getMetaCall(callable)
if call ~= metaCall then
cache[callable] = setmetatable({}, weakKeys)
metaCall = call
end
return run(...)
end, cache, resultsKey, nilKey
end
return run, cache, resultsKey, nilKey
end

78
lib/knife/serialize.lua Normal file
View File

@@ -0,0 +1,78 @@
local tinsert, tconcat, infinity = table.insert, table.concat, math.huge
return function (value)
local intro, outro, ready, known = {}, {}, {}, {}
local knownCount = 0
local writer = {}
-- get writer delegate for this value's type
local function getWriter (value)
return writer[type(value)]
end
-- check if a value has a representation yet
local function isReady (value)
return type(value) ~= 'table' or ready[value]
end
-- serialize tables
function writer.table (value)
if known[value] then
return known[value]
end
knownCount = knownCount + 1
local variable = ('v%i'):format(knownCount)
known[value] = variable
local parts = {}
for k, v in pairs(value) do
local writeKey, writeValue = getWriter(k), getWriter(v)
if writeKey and writeValue then
local key, value = writeKey(k), writeValue(v)
if isReady(k) and isReady(v) then
tinsert(parts, ('[%s]=%s'):format(key, value))
else
tinsert(outro, ('%s[%s]=%s'):format(variable, key, value))
end
end
end
local fields = tconcat(parts, ',')
tinsert(intro, ('local %s={%s}'):format(variable, fields))
ready[value] = true
return variable
end
-- preserve sign bit on NaN, since Lua prints "nan" or "-nan"
local function writeNan (n)
return tostring(n) == tostring(0/0) and '0/0' or '-(0/0)'
end
-- serialize numbers
function writer.number (value)
return value == infinity and '1/0'
or value == -infinity and '-1/0'
or value ~= value and writeNan(value)
or ('%.17G'):format(value)
end
-- serialize strings
function writer.string (value)
return ('%q'):format(value)
end
-- serialize booleans
writer.boolean = tostring
-- concatenate array, joined by and terminated with line break
local function lines (t)
return #t == 0 and '' or tconcat(t, '\n') .. '\n'
end
-- generate serialized result
local write = getWriter(value)
local result = write and write(value) or 'nil'
return lines(intro) .. lines(outro) .. 'return ' .. result
end

69
lib/knife/system.lua Normal file
View File

@@ -0,0 +1,69 @@
local loadstring = _G.loadstring or _G.load
local tconcat = table.concat
local type = type
local function hasSigil (sigil, value)
return type(value) == 'string' and sigil:byte() == value:byte()
end
return function (aspects, process)
local args = {}
local cond = {}
local results = {}
local localIndex = 0
local choicePattern = '([^|]+)'
local function suppress (aspect, condition)
cond[#cond + 1] = 'if nil'
for option in aspect:gmatch(choicePattern) do
cond[#cond + 1] = condition:format(option)
end
cond[#cond + 1] = 'then return end'
end
local function supply (aspect, isOptional, isReturned)
localIndex = localIndex + 1
cond[#cond + 1] = ('local l%d = nil'):format(localIndex)
for option in aspect:gmatch(choicePattern) do
cond[#cond + 1] = ('or _entity[%q]'):format(option)
end
if not isOptional then
cond[#cond + 1] = ('if not l%d then return end'):format(localIndex)
end
if isReturned then
results[#results + 1] = ('_entity[%q]'):format(aspect)
end
args[#args + 1] = ('l%d'):format(localIndex)
end
for index = 1, #aspects do
local aspect = aspects[index]
if hasSigil('_', aspect) then
args[#args + 1] = aspect
elseif hasSigil('!', aspect) or hasSigil('~', aspect) then
suppress(aspect:sub(2), 'or _entity[%q]')
elseif hasSigil('-', aspect) then
suppress(aspect:sub(2), 'or not _entity[%q]')
elseif hasSigil('?', aspect) then
supply(aspect:sub(2), true)
elseif hasSigil('=', aspect) then
supply(aspect:sub(2), false, true)
else
supply(aspect, false)
end
end
local source = ([[
local _aspects, _process = ...
return function (_entity, ...)
%s
%s _process(%s ...)
return true
end]]):format(
tconcat(cond, ' '),
results[1] and (tconcat(results, ',') .. ' = ') or '',
args[1] and (tconcat(args, ', ') .. ', ') or '')
return loadstring(source)(aspects, process)
end

128
lib/knife/test.lua Normal file
View File

@@ -0,0 +1,128 @@
local test, testAssert, testError
-- Create a node representing a test section
local function createNode (parent, description, process)
return setmetatable({
parent = parent,
description = description,
process = process,
nodes = {},
activeNodeIndex = 1,
currentNodeIndex = 0,
assert = testAssert,
error = testError,
}, { __call = test })
end
-- Run a node
local function runNode (node)
node.currentNodeIndex = 0
return node:process()
end
-- Get the root node for a given node
local function getRootNode (node)
local parent = node.parent
return parent and getRootNode(parent) or node
end
-- Update the active child node of the given node
local function updateActiveNode (node, description, process)
local activeNodeIndex = node.activeNodeIndex
local nodes = node.nodes
local activeNode = nodes[activeNodeIndex]
if not activeNode then
activeNode = createNode(node, description, process)
nodes[activeNodeIndex] = activeNode
else
activeNode.process = process
end
getRootNode(node).lastActiveLeaf = activeNode
return activeNode
end
-- Run the active child node of the given node
local function runActiveNode (node, description, process)
local activeNode = updateActiveNode(node, description, process)
return runNode(activeNode)
end
-- Get ancestors of a node, including the node
local function getAncestors (node)
local ancestors = { node }
for ancestor in function () return node.parent end do
ancestors[#ancestors + 1] = ancestor
node = ancestor
end
return ancestors
end
-- Print a message describing one execution path in the test scenario
local function printScenario (node)
local ancestors = getAncestors(node)
for i = #ancestors, 1, -1 do
io.stderr:write(ancestors[i].description or '')
io.stderr:write('\n')
end
end
-- Print a message and stop the test scenario when an assertion fails
local function failAssert (node, description, message)
io.stderr:write(message or '')
io.stderr:write('\n\n')
printScenario(node)
io.stderr:write(description or '')
io.stderr:write('\n\n')
error(message or '', 2)
end
-- Create a branch node for a test scenario
test = function (node, description, process)
node.currentNodeIndex = node.currentNodeIndex + 1
if node.currentNodeIndex == node.activeNodeIndex then
return runActiveNode(node, description, process)
end
end
-- Test an assertion
testAssert = function (self, value, description)
if not value then
return failAssert(self, description, 'Test failed: assertion failed')
end
return value
end
-- Expect function f to fail
testError = function (self, f, description)
if pcall(f) then
return failAssert(self, description, 'Test failed: expected error')
end
end
-- Create the root node for a test scenario
local function T (description, process)
local root = createNode(nil, description, process)
runNode(root)
while root.activeNodeIndex <= #root.nodes do
local lastActiveBranch = root.lastActiveLeaf.parent
lastActiveBranch.activeNodeIndex = lastActiveBranch.activeNodeIndex + 1
runNode(root)
end
return root
end
-- Run any other files passed from CLI.
if arg and arg[0] and arg[0]:gmatch('test.lua') then
_G.T = T
for i = 1, #arg do
dofile(arg[i])
end
_G.T = nil
end
return T

246
lib/knife/timer.lua Normal file
View File

@@ -0,0 +1,246 @@
-- require('mobdebug').start()
local Timer = {}
-- group management
local function detach (group, item)
local index = item.index
group[index] = group[#group]
group[index].index = index
group[#group] = nil
item.groupField = nil
end
local function attach (group, item)
if item.groupField then
detach (item.groupField, item)
end
local index = #group + 1
item.index = index
group[index] = item
item.groupField = group
item.lastGroup = group
end
-- instance update methods
local function updateContinuous (self, dt)
local cutoff = self.cutoff
local elapsed = self.elapsed + dt
if self:callback(dt) == false or elapsed >= cutoff then
if self.finishField then
self:finishField(elapsed - cutoff)
end
self:remove()
end
self.elapsed = elapsed
return
end
local function updateIntermittent (self, dt)
local duration = self.delay or self.interval
local elapsed = self.elapsed + dt
while elapsed >= duration do
elapsed = elapsed - duration
if self.limitField then
self.limitField = self.limitField - 1
end
if self:callback(elapsed) == false
or self.delay or self.limitField == 0 then
if self.finishField then
self:finishField(elapsed)
end
self:remove()
return
end
end
self.elapsed = elapsed
end
local function updateTween (self, dt)
local elapsed = self.elapsed + dt
local plan = self.plan
local duration = self.duration
self.elapsed = elapsed
if elapsed >= duration then
for index = 1, #plan do
local task = plan[index]
task.target[task.key] = task.final
end
if self.finishField then
self:finishField(elapsed - duration)
end
self:remove()
return
end
local ease = self.easeField
for index = 1, #plan do
local task = plan[index]
local target, key = task.target, task.key
local initial, change = task.initial, task.change
target[key] = ease(elapsed, initial, change, duration)
end
end
-- shared instance methods
local defaultGroup = {}
local function group (self, group)
if not group then
group = defaultGroup
end
attach(group, self)
return self
end
local function remove (self)
if self.groupField then
detach(self.groupField, self)
end
return self
end
local function register (self)
attach(self.lastGroup, self)
return self
end
local function limit (self, limitField)
self.limitField = limitField
return self
end
local function finish (self, finishField)
self.finishField = finishField
return self
end
local function ease (self, easeField)
self.easeField = easeField
return self
end
-- tweening helper functions
local function planTween (definition)
local plan = {}
for target, values in pairs(definition) do
for key, final in pairs(values) do
local initial = target[key]
plan[#plan + 1] = {
target = target,
key = key,
initial = initial,
final = final,
change = final - initial,
}
end
end
return plan
end
local function easeLinear (elapsed, initial, change, duration)
return change * elapsed / duration + initial
end
-- instance initializer
local function initialize (timer)
timer.elapsed = 0
timer.group = group
timer.remove = remove
timer.register = register
attach(defaultGroup, timer)
return timer
end
-- static api
function Timer.after (delay, callback)
return initialize {
delay = delay,
callback = callback,
update = updateIntermittent,
}
end
function Timer.every (interval, callback)
return initialize {
interval = interval,
callback = callback,
update = updateIntermittent,
limit = limit,
finish = finish,
}
end
function Timer.prior (cutoff, callback)
return initialize {
cutoff = cutoff,
callback = callback,
update = updateContinuous,
finish = finish,
}
end
function Timer.tween (duration, definition)
return initialize {
duration = duration,
plan = planTween(definition),
update = updateTween,
easeField = easeLinear,
ease = ease,
finish = finish,
}
end
function Timer.update (dt, group)
if not group then
group = defaultGroup
end
for index = #group, 1, -1 do
group[index]:update(dt)
end
end
function Timer.clear (group)
if not group then
group = defaultGroup
end
for i = 1, #group do
group[i] = nil
end
end
Timer.defaultGroup = defaultGroup
return Timer