-- 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