first commit
This commit is contained in:
100
lib/class.lua
Normal file
100
lib/class.lua
Normal file
@@ -0,0 +1,100 @@
|
||||
-- require('mobdebug').start()
|
||||
|
||||
--[[
|
||||
Copyright (c) 2010-2013 Matthias Richter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
Except as contained in this notice, the name(s) of the above copyright holders
|
||||
shall not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
]]--
|
||||
|
||||
local function include_helper(to, from, seen)
|
||||
if from == nil then
|
||||
return to
|
||||
elseif type(from) ~= 'table' then
|
||||
return from
|
||||
elseif seen[from] then
|
||||
return seen[from]
|
||||
end
|
||||
|
||||
seen[from] = to
|
||||
for k,v in pairs(from) do
|
||||
k = include_helper({}, k, seen) -- keys might also be tables
|
||||
if to[k] == nil then
|
||||
to[k] = include_helper({}, v, seen)
|
||||
end
|
||||
end
|
||||
return to
|
||||
end
|
||||
|
||||
-- deeply copies `other' into `class'. keys in `other' that are already
|
||||
-- defined in `class' are omitted
|
||||
local function include(class, other)
|
||||
return include_helper(class, other, {})
|
||||
end
|
||||
|
||||
-- returns a deep copy of `other'
|
||||
local function clone(other)
|
||||
return setmetatable(include({}, other), getmetatable(other))
|
||||
end
|
||||
|
||||
local function new(class)
|
||||
-- mixins
|
||||
class = class or {} -- class can be nil
|
||||
local inc = class.__includes or {}
|
||||
if getmetatable(inc) then inc = {inc} end
|
||||
|
||||
for _, other in ipairs(inc) do
|
||||
if type(other) == "string" then
|
||||
other = _G[other]
|
||||
end
|
||||
include(class, other)
|
||||
end
|
||||
|
||||
-- class implementation
|
||||
class.__index = class
|
||||
class.init = class.init or class[1] or function() end
|
||||
class.include = class.include or include
|
||||
class.clone = class.clone or clone
|
||||
|
||||
-- constructor call
|
||||
return setmetatable(class, {__call = function(c, ...)
|
||||
local o = setmetatable({}, c)
|
||||
o:init(...)
|
||||
return o
|
||||
end})
|
||||
end
|
||||
|
||||
-- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons).
|
||||
if class_commons ~= false and not common then
|
||||
common = {}
|
||||
function common.class(name, prototype, parent)
|
||||
return new{__includes = {prototype, parent}}
|
||||
end
|
||||
function common.instance(class, ...)
|
||||
return class(...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- the module
|
||||
return setmetatable({new = new, include = include, clone = clone},
|
||||
{__call = function(_,...) return new(...) end})
|
||||
15
lib/knife/base.lua
Normal file
15
lib/knife/base.lua
Normal 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
78
lib/knife/behavior.lua
Normal 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
27
lib/knife/bind.lua
Normal 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
31
lib/knife/chain.lua
Normal 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
54
lib/knife/convoke.lua
Normal 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
111
lib/knife/event.lua
Normal 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
2
lib/knife/gun.lua
Normal file
@@ -0,0 +1,2 @@
|
||||
-- GUN (Give Up Now)
|
||||
os.exit()
|
||||
77
lib/knife/memoize.lua
Normal file
77
lib/knife/memoize.lua
Normal 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
78
lib/knife/serialize.lua
Normal 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
69
lib/knife/system.lua
Normal 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
128
lib/knife/test.lua
Normal 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
246
lib/knife/timer.lua
Normal 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
|
||||
281
lib/push.lua
Normal file
281
lib/push.lua
Normal file
@@ -0,0 +1,281 @@
|
||||
-- push.lua v0.4
|
||||
|
||||
-- Copyright (c) 2020 Ulysse Ramage
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
local love11 = love.getVersion() == 11
|
||||
local getDPI = love11 and love.window.getDPIScale or love.window.getPixelScale
|
||||
local windowUpdateMode = love11 and love.window.updateMode or function(width, height, settings)
|
||||
local _, _, flags = love.window.getMode()
|
||||
for k, v in pairs(settings) do flags[k] = v end
|
||||
love.window.setMode(width, height, flags)
|
||||
end
|
||||
|
||||
local push = {
|
||||
|
||||
defaults = {
|
||||
fullscreen = false,
|
||||
resizable = false,
|
||||
pixelperfect = false,
|
||||
highdpi = true,
|
||||
canvas = true,
|
||||
stencil = true
|
||||
}
|
||||
|
||||
}
|
||||
setmetatable(push, push)
|
||||
|
||||
function push:applySettings(settings)
|
||||
for k, v in pairs(settings) do
|
||||
self["_" .. k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function push:resetSettings() return self:applySettings(self.defaults) end
|
||||
|
||||
function push:setupScreen(WWIDTH, WHEIGHT, RWIDTH, RHEIGHT, settings)
|
||||
|
||||
settings = settings or {}
|
||||
|
||||
self._WWIDTH, self._WHEIGHT = WWIDTH, WHEIGHT
|
||||
self._RWIDTH, self._RHEIGHT = RWIDTH, RHEIGHT
|
||||
|
||||
self:applySettings(self.defaults) --set defaults first
|
||||
self:applySettings(settings) --then fill with custom settings
|
||||
|
||||
windowUpdateMode(self._RWIDTH, self._RHEIGHT, {
|
||||
fullscreen = self._fullscreen,
|
||||
resizable = self._resizable,
|
||||
highdpi = self._highdpi
|
||||
})
|
||||
|
||||
self:initValues()
|
||||
|
||||
if self._canvas then
|
||||
self:setupCanvas({ "default" }) --setup canvas
|
||||
end
|
||||
|
||||
self._borderColor = {0, 0, 0}
|
||||
|
||||
self._drawFunctions = {
|
||||
["start"] = self.start,
|
||||
["end"] = self.finish
|
||||
}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function push:setupCanvas(canvases)
|
||||
table.insert(canvases, { name = "_render", private = true }) --final render
|
||||
|
||||
self._canvas = true
|
||||
self.canvases = {}
|
||||
|
||||
for i = 1, #canvases do
|
||||
push:addCanvas(canvases[i])
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
function push:addCanvas(params)
|
||||
table.insert(self.canvases, {
|
||||
name = params.name,
|
||||
private = params.private,
|
||||
shader = params.shader,
|
||||
canvas = love.graphics.newCanvas(self._WWIDTH, self._WHEIGHT),
|
||||
stencil = params.stencil or self._stencil
|
||||
})
|
||||
end
|
||||
|
||||
function push:setCanvas(name)
|
||||
if not self._canvas then return true end
|
||||
local canvasTable = self:getCanvasTable(name)
|
||||
return love.graphics.setCanvas({ canvasTable.canvas, stencil = canvasTable.stencil })
|
||||
end
|
||||
function push:getCanvasTable(name)
|
||||
for i = 1, #self.canvases do
|
||||
if self.canvases[i].name == name then
|
||||
return self.canvases[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
function push:setShader(name, shader)
|
||||
if not shader then
|
||||
self:getCanvasTable("_render").shader = name
|
||||
else
|
||||
self:getCanvasTable(name).shader = shader
|
||||
end
|
||||
end
|
||||
|
||||
function push:initValues()
|
||||
self._PSCALE = (not love11 and self._highdpi) and getDPI() or 1
|
||||
|
||||
self._SCALE = {
|
||||
x = self._RWIDTH/self._WWIDTH * self._PSCALE,
|
||||
y = self._RHEIGHT/self._WHEIGHT * self._PSCALE
|
||||
}
|
||||
|
||||
if self._stretched then --if stretched, no need to apply offset
|
||||
self._OFFSET = {x = 0, y = 0}
|
||||
else
|
||||
local scale = math.min(self._SCALE.x, self._SCALE.y)
|
||||
if self._pixelperfect then scale = math.floor(scale) end
|
||||
|
||||
self._OFFSET = {x = (self._SCALE.x - scale) * (self._WWIDTH/2), y = (self._SCALE.y - scale) * (self._WHEIGHT/2)}
|
||||
self._SCALE.x, self._SCALE.y = scale, scale --apply same scale to X and Y
|
||||
end
|
||||
|
||||
self._GWIDTH = self._RWIDTH * self._PSCALE - self._OFFSET.x * 2
|
||||
self._GHEIGHT = self._RHEIGHT * self._PSCALE - self._OFFSET.y * 2
|
||||
end
|
||||
|
||||
function push:apply(operation, shader)
|
||||
self._drawFunctions[operation](self, shader)
|
||||
end
|
||||
|
||||
function push:start()
|
||||
if self._canvas then
|
||||
love.graphics.push()
|
||||
love.graphics.setCanvas({ self.canvases[1].canvas, stencil = self.canvases[1].stencil })
|
||||
|
||||
else
|
||||
love.graphics.translate(self._OFFSET.x, self._OFFSET.y)
|
||||
love.graphics.setScissor(self._OFFSET.x, self._OFFSET.y, self._WWIDTH*self._SCALE.x, self._WHEIGHT*self._SCALE.y)
|
||||
love.graphics.push()
|
||||
love.graphics.scale(self._SCALE.x, self._SCALE.y)
|
||||
end
|
||||
end
|
||||
|
||||
function push:applyShaders(canvas, shaders)
|
||||
local _shader = love.graphics.getShader()
|
||||
if #shaders <= 1 then
|
||||
love.graphics.setShader(shaders[1])
|
||||
love.graphics.draw(canvas)
|
||||
else
|
||||
local _canvas = love.graphics.getCanvas()
|
||||
|
||||
local _tmp = self:getCanvasTable("_tmp")
|
||||
if not _tmp then --create temp canvas only if needed
|
||||
self:addCanvas({ name = "_tmp", private = true, shader = nil })
|
||||
_tmp = self:getCanvasTable("_tmp")
|
||||
end
|
||||
|
||||
love.graphics.push()
|
||||
love.graphics.origin()
|
||||
local outputCanvas
|
||||
for i = 1, #shaders do
|
||||
local inputCanvas = i % 2 == 1 and canvas or _tmp.canvas
|
||||
outputCanvas = i % 2 == 0 and canvas or _tmp.canvas
|
||||
love.graphics.setCanvas(outputCanvas)
|
||||
love.graphics.clear()
|
||||
love.graphics.setShader(shaders[i])
|
||||
love.graphics.draw(inputCanvas)
|
||||
love.graphics.setCanvas(inputCanvas)
|
||||
end
|
||||
love.graphics.pop()
|
||||
|
||||
love.graphics.setCanvas(_canvas)
|
||||
love.graphics.draw(outputCanvas)
|
||||
end
|
||||
love.graphics.setShader(_shader)
|
||||
end
|
||||
|
||||
function push:finish(shader)
|
||||
love.graphics.setBackgroundColor(unpack(self._borderColor))
|
||||
if self._canvas then
|
||||
local _render = self:getCanvasTable("_render")
|
||||
|
||||
love.graphics.pop()
|
||||
|
||||
local white = love11 and 1 or 255
|
||||
love.graphics.setColor(white, white, white)
|
||||
|
||||
--draw canvas
|
||||
love.graphics.setCanvas(_render.canvas)
|
||||
for i = 1, #self.canvases do --do not draw _render yet
|
||||
local _table = self.canvases[i]
|
||||
if not _table.private then
|
||||
local _canvas = _table.canvas
|
||||
local _shader = _table.shader
|
||||
self:applyShaders(_canvas, type(_shader) == "table" and _shader or { _shader })
|
||||
end
|
||||
end
|
||||
love.graphics.setCanvas()
|
||||
|
||||
--draw render
|
||||
love.graphics.translate(self._OFFSET.x, self._OFFSET.y)
|
||||
local shader = shader or _render.shader
|
||||
love.graphics.push()
|
||||
love.graphics.scale(self._SCALE.x, self._SCALE.y)
|
||||
self:applyShaders(_render.canvas, type(shader) == "table" and shader or { shader })
|
||||
love.graphics.pop()
|
||||
|
||||
--clear canvas
|
||||
for i = 1, #self.canvases do
|
||||
love.graphics.setCanvas(self.canvases[i].canvas)
|
||||
love.graphics.clear()
|
||||
end
|
||||
|
||||
love.graphics.setCanvas()
|
||||
love.graphics.setShader()
|
||||
else
|
||||
love.graphics.pop()
|
||||
love.graphics.setScissor()
|
||||
end
|
||||
end
|
||||
|
||||
function push:setBorderColor(color, g, b)
|
||||
self._borderColor = g and {color, g, b} or color
|
||||
end
|
||||
|
||||
function push:toGame(x, y)
|
||||
x, y = x - self._OFFSET.x, y - self._OFFSET.y
|
||||
local normalX, normalY = x / self._GWIDTH, y / self._GHEIGHT
|
||||
|
||||
x = (x >= 0 and x <= self._WWIDTH * self._SCALE.x) and normalX * self._WWIDTH or nil
|
||||
y = (y >= 0 and y <= self._WHEIGHT * self._SCALE.y) and normalY * self._WHEIGHT or nil
|
||||
|
||||
return x, y
|
||||
end
|
||||
|
||||
function push:toReal(x, y)
|
||||
local realX = self._OFFSET.x + (self._GWIDTH * x)/self._WWIDTH
|
||||
local realY = self._OFFSET.y + (self._GHEIGHT * y)/self._WHEIGHT
|
||||
return realX, realY
|
||||
end
|
||||
|
||||
function push:switchFullscreen(winw, winh)
|
||||
self._fullscreen = not self._fullscreen
|
||||
local windowWidth, windowHeight = love.window.getDesktopDimensions()
|
||||
|
||||
if self._fullscreen then --save windowed dimensions for later
|
||||
self._WINWIDTH, self._WINHEIGHT = self._RWIDTH, self._RHEIGHT
|
||||
elseif not self._WINWIDTH or not self._WINHEIGHT then
|
||||
self._WINWIDTH, self._WINHEIGHT = windowWidth * .5, windowHeight * .5
|
||||
end
|
||||
|
||||
self._RWIDTH = self._fullscreen and windowWidth or winw or self._WINWIDTH
|
||||
self._RHEIGHT = self._fullscreen and windowHeight or winh or self._WINHEIGHT
|
||||
|
||||
self:initValues()
|
||||
|
||||
love.window.setFullscreen(self._fullscreen, "desktop")
|
||||
if not self._fullscreen and (winw or winh) then
|
||||
windowUpdateMode(self._RWIDTH, self._RHEIGHT) --set window dimensions
|
||||
end
|
||||
end
|
||||
|
||||
function push:resize(w, h)
|
||||
if self._highdpi then w, h = w / self._PSCALE, h / self._PSCALE end
|
||||
self._RWIDTH = w
|
||||
self._RHEIGHT = h
|
||||
self:initValues()
|
||||
end
|
||||
|
||||
function push:getWidth() return self._WWIDTH end
|
||||
function push:getHeight() return self._WHEIGHT end
|
||||
function push:getDimensions() return self._WWIDTH, self._WHEIGHT end
|
||||
|
||||
return push
|
||||
Reference in New Issue
Block a user