714 lines
15 KiB
Lua
714 lines
15 KiB
Lua
|
|
-- TODO: Incomplete.
|
|
|
|
-- Blackboard
|
|
local blackboardPrototype = {}
|
|
blackboardPrototype.__index = blackboardPrototype
|
|
|
|
local nodeParametersTypes = {
|
|
{'json', 'Json'},
|
|
{'entity', 'Entity'},
|
|
{'position', 'Position'},
|
|
{'vec2', 'Vec2'},
|
|
{'number', 'Number'},
|
|
{'bool', 'Bool'},
|
|
{'list', 'List'},
|
|
{'table', 'Table'},
|
|
{'string', 'String'},
|
|
}
|
|
|
|
local mappedParameterTypes = {}
|
|
|
|
for i, data in ipairs(nodeParametersTypes) do
|
|
mappedParameterTypes[i] = i
|
|
mappedParameterTypes[data[1]] = i
|
|
end
|
|
|
|
function blackboardPrototype:ctor()
|
|
self.board = {}
|
|
self.input = {}
|
|
self._parameters = {}
|
|
self.vectorNumberInput = {}
|
|
self.ephemeral = {}
|
|
|
|
for i, data in ipairs(nodeParametersTypes) do
|
|
self.board[data[1]] = {}
|
|
self.input[data[1]] = {}
|
|
self.ephemeral[i] = {}
|
|
|
|
self.board[i] = self.board[data[1]]
|
|
self.input[i] = self.input[data[1]]
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
local function blackboardSet(self, t, key, value)
|
|
self.board[t][key] = value
|
|
|
|
local input = self.input[t][key]
|
|
|
|
if input then
|
|
for _, pair in ipairs(input) do
|
|
local index = pair[1]
|
|
local tab = pair[2]
|
|
tab[index] = value
|
|
end
|
|
end
|
|
|
|
-- dumb special case for setting number outputs to vec2 inputs
|
|
if t == 5 then
|
|
local mappings = self.vectorNumberInput[key]
|
|
|
|
if mappings then
|
|
for _, pair in pairs(mappings) do
|
|
local index = pair[1]
|
|
local tab = pair[2]
|
|
tab[index] = value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function blackboardPrototype:set(t, key, value)
|
|
local lookup = mappedParameterTypes[t]
|
|
|
|
if not lookup then
|
|
error('unknown blackboard value type ' .. tostring(t))
|
|
end
|
|
|
|
blackboardSet(self, lookup, key, value)
|
|
end
|
|
|
|
function blackboardPrototype:setRaw(t, key, value)
|
|
blackboardSet(self, t, key, value)
|
|
end
|
|
|
|
function blackboardPrototype:get(t, key)
|
|
return self.board[t][key]
|
|
end
|
|
|
|
for i, data in ipairs(nodeParametersTypes) do
|
|
blackboardPrototype['get' .. data[2]] = function(self, key)
|
|
return self.board[i][key]
|
|
end
|
|
|
|
blackboardPrototype['set' .. data[2]] = function(self, key, value)
|
|
blackboardSet(self, i, key, value)
|
|
end
|
|
end
|
|
|
|
function blackboardPrototype:parameters(parameters, nodeID)
|
|
local tab = self._parameters[nodeID]
|
|
if tab then return tab end
|
|
tab = {}
|
|
|
|
for parameterName, parameter in pairs(parameters) do
|
|
local t = assert(mappedParameterTypes[parameter.type])
|
|
local pKey = parameter.key
|
|
local pValue = parameter.value
|
|
|
|
if pKey then
|
|
local typeInput = self.input[t][pKey]
|
|
|
|
if not typeInput then
|
|
typeInput = {}
|
|
self.input[t][pKey] = typeInput
|
|
end
|
|
|
|
table.insert(typeInput, {parameterName, tab})
|
|
tab[parameterName] = self.board[t][pKey]
|
|
elseif pValue ~= nil then
|
|
if t == 4 then -- vec2
|
|
-- dumb special case for allowing a vec2 of blackboard number keys
|
|
if type(pValue) ~= 'table' then
|
|
error(string.format('parameter %s of type %s for node %s has has non-table value: %s', parameterName, parameter.type, nodeID, type(pValue)))
|
|
end
|
|
|
|
local vector = {}
|
|
|
|
for i, vValue in ipairs(pValue) do
|
|
if type(vValue) == 'string' then
|
|
-- vector part with symbolic reference
|
|
local typeInput = self.vectorNumberInput[vValue]
|
|
|
|
if not typeInput then
|
|
typeInput = {}
|
|
self.vectorNumberInput[vValue] = typeInput
|
|
end
|
|
|
|
table.insert(typeInput, {i, vector})
|
|
vector[i] = self.board[5][vValue] -- number
|
|
else
|
|
vector[i] = vValue
|
|
end
|
|
end
|
|
|
|
tab[parameterName] = vector
|
|
else
|
|
tab[parameterName] = pValue
|
|
end
|
|
end
|
|
end
|
|
|
|
self._parameters[nodeID] = tab
|
|
return tab
|
|
end
|
|
|
|
function blackboardPrototype:setOutput(node, output)
|
|
for key, out in pairs(node.outputs) do
|
|
if out.key then
|
|
local t = out.type
|
|
blackboardSet(self, t, out.key, output[key])
|
|
|
|
if out.ephemeral then
|
|
self.ephemeral[t][out.key] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function blackboardPrototype:takeEphemerals()
|
|
local value = self.ephemeral
|
|
self.ephemeral = {}
|
|
|
|
for i, _ in ipairs(nodeParametersTypes) do
|
|
self.ephemeral[i] = {}
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
function blackboardPrototype:clearEphemerals(ephemerals)
|
|
for i, keys in ipairs(ephemerals) do
|
|
for key in pairs(keys) do
|
|
blackboardSet(self, i, key, nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Blackboard()
|
|
return setmetatable({}, blackboardPrototype):ctor()
|
|
end
|
|
|
|
-- //// Blackboard
|
|
|
|
local SUCCESS = true
|
|
local FAILURE = false
|
|
local RUNNING = nil
|
|
local nextNodeID = 0
|
|
|
|
local function runAndReset(self, ...)
|
|
local status = self:run(...)
|
|
|
|
if status ~= RUNNING then
|
|
self:reset()
|
|
end
|
|
|
|
return status
|
|
end
|
|
|
|
local function reconstructTree(stack)
|
|
local top = #stack
|
|
if top == 0 then return '' end
|
|
|
|
local result = {'\nbehavior tree traceback:'}
|
|
|
|
for i = top, 1, -1 do
|
|
table.insert(result, string.format('%s%d. - %q', string.rep(' ', top - i + 1), top - i + 1, stack[i]))
|
|
end
|
|
|
|
return table.concat(result, '\n')
|
|
end
|
|
|
|
-- ActionNode
|
|
|
|
local actionNode = {}
|
|
actionNode.__index = actionNode
|
|
|
|
function actionNode:ctor(name, parameters, outputs)
|
|
self.name = name
|
|
self.parameters = parameters
|
|
self.outputs = outputs
|
|
self.calls = 0
|
|
self.nodeID = nextNodeID
|
|
nextNodeID = nextNodeID + 1
|
|
|
|
return self
|
|
end
|
|
|
|
function actionNode:bake()
|
|
self.callable = _ENV[self.name]
|
|
|
|
if type(self.callable) ~= 'function' then
|
|
error('expected global ' .. self.name .. ' to be a function, but got ' .. type(self.callable))
|
|
end
|
|
end
|
|
|
|
do
|
|
local create = coroutine.create
|
|
local resume = coroutine.resume
|
|
local status = coroutine.status
|
|
|
|
function actionNode:run(delta, blackboard, stack)
|
|
--table.insert(stack, self.name)
|
|
self.calls = self.calls + 1
|
|
local status, nodeStatus, nodeExtra
|
|
|
|
if not self.coroutine then
|
|
self.coroutine = create(self.callable)
|
|
local parameters = blackboard:parameters(self.parameters, self)
|
|
status, nodeStatus, nodeExtra = resume(self.coroutine, parameters, blackboard, self.nodeID, delta)
|
|
else
|
|
status, nodeStatus, nodeExtra = resume(self.coroutine, delta)
|
|
end
|
|
|
|
if not status then
|
|
sb.logError(debug.traceback(self.coroutine, string.format('Behavior ActionNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
|
|
--table.remove(stack)
|
|
return FAILURE
|
|
end
|
|
|
|
if nodeExtra ~= nil then
|
|
blackboard:setOutput(self, nodeExtra)
|
|
end
|
|
|
|
--table.remove(stack)
|
|
|
|
if nodeStatus == nil then
|
|
return RUNNING
|
|
elseif nodeStatus == false then
|
|
return FAILURE
|
|
else
|
|
return SUCCESS
|
|
end
|
|
end
|
|
end
|
|
|
|
function actionNode:reset()
|
|
self.coroutine = nil
|
|
end
|
|
|
|
function actionNode:__tostring()
|
|
return string.format('ActionNode[%q / %d / %s]', self.name, self.calls, tostring(self.coroutine or 'none'))
|
|
end
|
|
|
|
function ActionNode(...)
|
|
return setmetatable({}, actionNode):ctor(...)
|
|
end
|
|
|
|
-- //// ActionNode
|
|
|
|
-- DecoratorNode
|
|
|
|
local decoratorNode = {}
|
|
decoratorNode.__index = decoratorNode
|
|
|
|
function decoratorNode:ctor(name, parameters, child)
|
|
self.name = name
|
|
self.parameters = parameters
|
|
self.child = child
|
|
self.calls = 0
|
|
self.nodeID = nextNodeID
|
|
nextNodeID = nextNodeID + 1
|
|
|
|
return self
|
|
end
|
|
|
|
function decoratorNode:bake()
|
|
self.callable = _ENV[self.name]
|
|
|
|
if type(self.callable) ~= 'function' then
|
|
error('expected global ' .. self.name .. ' to be a function, but got ' .. type(self.callable))
|
|
end
|
|
|
|
self.child:bake()
|
|
end
|
|
|
|
do
|
|
local create = coroutine.create
|
|
local resume = coroutine.resume
|
|
local coroutine_status = coroutine.status
|
|
|
|
function decoratorNode:run(delta, blackboard, stack)
|
|
--table.insert(stack, self.name)
|
|
self.calls = self.calls + 1
|
|
|
|
if not self.coroutine then
|
|
local parameters = blackboard:parameters(self.parameters, self)
|
|
local coroutine = create(self.callable)
|
|
local status, nodeStatus = resume(coroutine, parameters, blackboard, self.nodeID, delta)
|
|
|
|
if not status then
|
|
sb.logError(debug.traceback(coroutine, string.format('Behavior DecoratorNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
|
|
--table.remove(stack)
|
|
return FAILURE
|
|
end
|
|
|
|
if nodeStatus == true then
|
|
return SUCCESS
|
|
elseif nodeStatus == false then
|
|
return FAILURE
|
|
else
|
|
self.coroutine = coroutine
|
|
end
|
|
end
|
|
|
|
while true do
|
|
local childStatus = runAndReset(self.child, delta, blackboard, stack)
|
|
|
|
if childStatus == RUNNING then
|
|
table.remove(stack)
|
|
return RUNNING
|
|
end
|
|
|
|
local status, nodeStatus = resume(self.coroutine, childStatus)
|
|
|
|
if not status then
|
|
sb.logError(debug.traceback(coroutine, string.format('Behavior DecoratorNode %q failed: %s%s', self.name, nodeStatus, reconstructTree(stack))))
|
|
--table.remove(stack)
|
|
return FAILURE
|
|
end
|
|
|
|
if nodeStatus == true then
|
|
return SUCCESS
|
|
elseif nodeStatus == false then
|
|
return FAILURE
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function decoratorNode:reset()
|
|
self.coroutine = nil
|
|
self.child:reset()
|
|
end
|
|
|
|
function actionNode:__tostring()
|
|
return string.format('DecoratorNode[%q / %d / %s; %s]', self.name, self.calls, tostring(self.coroutine or 'none'), tostring(self.child))
|
|
end
|
|
|
|
function DecoratorNode(...)
|
|
return setmetatable({}, decoratorNode):ctor(...)
|
|
end
|
|
|
|
-- //// DecoratorNode
|
|
|
|
-- Composite nodes
|
|
|
|
local seqNode = {}
|
|
seqNode.__index = seqNode
|
|
|
|
function seqNode:ctor(children, isSelector)
|
|
self.children = children
|
|
self.isSelector = isSelector
|
|
self.calls = 0
|
|
self.index = 1
|
|
self.size = #children
|
|
|
|
return self
|
|
end
|
|
|
|
function seqNode:run(delta, blackboard, stack)
|
|
self.calls = self.calls + 1
|
|
local size = self.size
|
|
local isSelector = self.isSelector
|
|
|
|
--[[if isSelector then
|
|
table.insert(stack, 'SelectorNode')
|
|
else
|
|
table.insert(stack, 'SequenceNode')
|
|
end]]
|
|
|
|
while self.index <= size do
|
|
local child = self.children[self.index]
|
|
local status = runAndReset(child, delta, blackboard, stack)
|
|
|
|
if status == RUNNING then
|
|
--table.remove(stack)
|
|
return RUNNING
|
|
elseif isSelector and status == SUCCESS then
|
|
--table.remove(stack)
|
|
return SUCCESS
|
|
elseif not isSelector and status == FAILURE then
|
|
--table.remove(stack)
|
|
return FAILURE
|
|
end
|
|
|
|
self.index = self.index + 1
|
|
end
|
|
|
|
if isSelector then return FAILURE end
|
|
return SUCCESS
|
|
end
|
|
|
|
function seqNode:reset()
|
|
self.index = 1
|
|
|
|
for _, child in ipairs(self.children) do
|
|
child:reset()
|
|
end
|
|
end
|
|
|
|
function seqNode:bake()
|
|
for _, child in ipairs(self.children) do
|
|
child:bake()
|
|
end
|
|
end
|
|
|
|
function seqNode:__tostring()
|
|
if self.isSelector then
|
|
return string.format('SelectorNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index]))
|
|
else
|
|
return string.format('SequenceNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index]))
|
|
end
|
|
end
|
|
|
|
function SequenceNode(p, c)
|
|
return setmetatable({}, seqNode):ctor(c, false)
|
|
end
|
|
|
|
function SelectorNode(p, c)
|
|
return setmetatable({}, seqNode):ctor(c, true)
|
|
end
|
|
|
|
local parallelNode = {}
|
|
parallelNode.__index = parallelNode
|
|
|
|
function parallelNode:ctor(parameters, children)
|
|
self.children = children
|
|
|
|
if type(parameters.success) == 'number' and parameters.success >= 0 then
|
|
self.successLimit = parameters.success
|
|
else
|
|
self.successLimit = #children
|
|
end
|
|
|
|
if type(parameters.fail) == 'number' and parameters.fail >= 0 then
|
|
self.failLimit = parameters.fail
|
|
else
|
|
self.failLimit = #children
|
|
end
|
|
|
|
self.lastFailed = -1
|
|
self.lastSucceed = -1
|
|
self.calls = 0
|
|
|
|
return self
|
|
end
|
|
|
|
function parallelNode:run(delta, blackboard, stack)
|
|
self.calls = self.calls + 1
|
|
local failed = 0
|
|
local succeeded = 0
|
|
local failLimit = self.failLimit
|
|
local successLimit = self.successLimit
|
|
|
|
--table.insert(stack, 'ParallelNode')
|
|
|
|
for _, node in ipairs(self.children) do
|
|
local status = runAndReset(node, delta, blackboard, stack)
|
|
|
|
if status == SUCCESS then
|
|
succeeded = succeeded + 1
|
|
elseif status == FAILURE then
|
|
failed = failed + 1
|
|
end
|
|
|
|
if failed >= failLimit then
|
|
self.lastFailed = failed
|
|
self.lastSucceed = succeeded
|
|
--table.remove(stack)
|
|
return FAILURE
|
|
elseif succeeded >= successLimit then
|
|
self.lastFailed = failed
|
|
self.lastSucceed = succeeded
|
|
--table.remove(stack)
|
|
return SUCCESS
|
|
end
|
|
end
|
|
|
|
self.lastFailed = failed
|
|
self.lastSucceed = succeeded
|
|
--table.remove(stack)
|
|
return RUNNING
|
|
end
|
|
|
|
function parallelNode:bake()
|
|
for _, child in ipairs(self.children) do
|
|
child:bake()
|
|
end
|
|
end
|
|
|
|
function parallelNode:reset()
|
|
self.lastFailed = -1
|
|
self.lastSucceed = -1
|
|
|
|
for _, child in ipairs(self.children) do
|
|
child:reset()
|
|
end
|
|
end
|
|
|
|
function parallelNode:__tostring()
|
|
return string.format('ParallelNode[%d / %d, limits=%d / %d, last=%d / %d]', self.calls, #self.children, self.successLimit, self.failLimit, self.lastSucceed, self.lastFailed)
|
|
end
|
|
|
|
function ParallelNode(p, c)
|
|
return setmetatable({}, parallelNode):ctor(p, c)
|
|
end
|
|
|
|
local dynNode = {}
|
|
dynNode.__index = dynNode
|
|
|
|
function dynNode:ctor(children)
|
|
self.children = children
|
|
self.calls = 0
|
|
self.index = 1
|
|
self.size = #children
|
|
|
|
return self
|
|
end
|
|
|
|
function dynNode:run(delta, blackboard, stack)
|
|
self.calls = self.calls + 1
|
|
--table.insert(stack, 'DynamicNode')
|
|
|
|
local i = 1
|
|
local children = self.children
|
|
|
|
while i <= self.index do
|
|
local child = children[i]
|
|
local status = runAndReset(child, delta, blackboard, stack)
|
|
|
|
if status == FAILURE and i == self.index then
|
|
self.index = self.index + 1
|
|
end
|
|
|
|
if i < self.index and (status == SUCCESS or status == RUNNING) then
|
|
child:reset()
|
|
self.index = i
|
|
end
|
|
|
|
if status == SUCCESS or self.index > self.size then
|
|
--table.remove(stack)
|
|
return status
|
|
end
|
|
|
|
i = i + 1
|
|
end
|
|
|
|
--table.remove(stack)
|
|
return RUNNING
|
|
end
|
|
|
|
function dynNode:bake()
|
|
for _, child in ipairs(self.children) do
|
|
child:bake()
|
|
end
|
|
end
|
|
|
|
function dynNode:reset()
|
|
self.index = 1
|
|
|
|
for _, child in ipairs(self.children) do
|
|
child:reset()
|
|
end
|
|
end
|
|
|
|
function dynNode:__tostring()
|
|
return string.format('DynamicNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index]))
|
|
end
|
|
|
|
function DynamicNode(p, c)
|
|
return setmetatable({}, dynNode):ctor(c)
|
|
end
|
|
|
|
local randNode = {}
|
|
randNode.__index = randNode
|
|
|
|
function randNode:ctor(children)
|
|
self.children = children
|
|
self.index = -1
|
|
self.calls = 0
|
|
self.size = #children
|
|
|
|
return self
|
|
end
|
|
|
|
function randNode:run(delta, blackboard, stack)
|
|
self.calls = self.calls + 1
|
|
|
|
if self.index == -1 and self.size ~= 0 then
|
|
self.index = math.random(1, self.size)
|
|
end
|
|
|
|
if self.index == -1 then
|
|
return FAILURE
|
|
else
|
|
--table.insert(stack, 'RandomNode')
|
|
local value = runAndReset(self.children[self.index], delta, blackboard, stack)
|
|
--table.remove(stack)
|
|
return value
|
|
end
|
|
end
|
|
|
|
function randNode:bake()
|
|
for _, child in ipairs(self.children) do
|
|
child:bake()
|
|
end
|
|
end
|
|
|
|
function randNode:reset()
|
|
self.index = -1
|
|
|
|
for _, child in ipairs(self.children) do
|
|
child:reset()
|
|
end
|
|
end
|
|
|
|
function randNode:__tostring()
|
|
return string.format('RandomNode[%d / %d, index=%d, current=%s]', self.calls, self.size, self.index, tostring(self.children[self.index]))
|
|
end
|
|
|
|
function RandomNode(p, c)
|
|
return setmetatable({}, randNode):ctor(c)
|
|
end
|
|
|
|
-- //// Composite nodes
|
|
|
|
local statePrototype = {}
|
|
statePrototype.__index = statePrototype
|
|
|
|
function statePrototype:ctor(blackboard, root, name)
|
|
self.root = root
|
|
self._blackboard = blackboard
|
|
self.name = name or 'unnamed'
|
|
|
|
return self
|
|
end
|
|
|
|
function statePrototype:run(delta)
|
|
local stack = {}
|
|
local ephemerals = self._blackboard:takeEphemerals()
|
|
local status = runAndReset(self.root, delta, self._blackboard, stack)
|
|
self._blackboard:clearEphemerals(ephemerals)
|
|
|
|
return status
|
|
end
|
|
|
|
function statePrototype:clear()
|
|
self.root:reset()
|
|
end
|
|
|
|
function statePrototype:bake()
|
|
self.root:bake()
|
|
end
|
|
|
|
function statePrototype:blackboard()
|
|
return self._blackboard
|
|
end
|
|
|
|
function BehaviorState(...)
|
|
return setmetatable({}, statePrototype):ctor(...)
|
|
end
|