KStarbound/src/main/resources/scripts/behavior.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