people/init.lua

779 lines
21 KiB
Lua

local dbg
if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end
people = {}
local people_modpath = minetest.get_modpath("people")
dofile(people_modpath .. "/commands.lua")
dofile(people_modpath .. "/tracking.lua")
people.is_valid_name = function(name)
if not name then return false end
-- Characters must be valid
if not name:match("^[A-Za-z0-9%_%-]+$") then return false end
-- Can't be the name of a player
if minetest.get_player_last_online(name) then return false end
return true
end
local animations = {
stand = { x= 0, y= 79, },
lay = { x=162, y=166, },
walk = { x=168, y=187, },
mine = { x=189, y=198, },
walk_mine = { x=200, y=219, },
sit = { x= 81, y=160, },
}
local face_dir = function(pos1, pos2)
local dx = pos1.x - pos2.x
local dz = pos2.z - pos1.z
return math.atan2(dx, dz)
end
local walk_velocity = function(speed, yaw)
if speed == 0 then
return {x=0, y=0, z=0}
end
yaw = yaw + math.pi * 0.5
local x = math.cos(yaw) * speed
local z = math.sin(yaw) * speed
return {x=x, y=0, z=z}
end
--- Find the next node along a footpath
-- All positions are exact (rounded) node positions.
-- @param curpos The position of a current footpath node
-- @param lastpos The position of the previous footpath node
-- @param samedironly True to only look in the current direction of travel
-- @return The position of the next footpath node, or nil
local footpath_findnext = function(curpos, lastpos, samedironly)
dbg.v3("footpath_findnext, cur="..minetest.pos_to_string(curpos)..", last="..minetest.pos_to_string(lastpos)..", same="..dump(samedironly))
local xz
if samedironly then
xz = {}
else
xz = {{x=1,z=0}, {x=0,z=1}, {x=-1,z=0}, {x=0,z=-1}}
end
-- Favour the direction we're already going
-- TODO - that means we could check it twice (but then again, only
-- if we reach an invalid bit of footpath!
table.insert(xz, 1, {x=curpos.x-lastpos.x, z=curpos.z-lastpos.z})
for y = 1, -1, -1 do
for _, cxz in ipairs(xz) do
local x = cxz.x
local z = cxz.z
local npos = vector.add(curpos, vector.new(x, y, z))
if not vector.equals(npos, lastpos) then
local n = minetest.get_node(npos)
if n.name == 'default:cobble' or
n.name == 'stairs:stair_cobble' or
n.name == 'default:wood' or
n.name == 'stairs:stair_wood' then
local n = minetest.get_node({x=npos.x,y=npos.y+1,z=npos.z})
if n.name == "default:snow" then
-- Snow is walkable, but we will either walk over it
-- or dig it out of the way, so that's fine...
dbg.v3("footpath_findnext found (snow-covered) "..minetest.pos_to_string(npos))
return npos
end
-- Otherwise, so long as there's nothing walkable above the
-- node it should be a valid footpath node...
if not minetest.registered_nodes[n.name].walkable then
dbg.v3("footpath_findnext found "..minetest.pos_to_string(npos))
return npos
end
end
end
end
end
return nil
end
local escape_pattern
do
local matches =
{
["^"] = "%^";
["$"] = "%$";
["("] = "%(";
[")"] = "%)";
["%"] = "%%";
["."] = "%.";
["["] = "%[";
["]"] = "%]";
["*"] = "%*";
["+"] = "%+";
["-"] = "%-";
["?"] = "%?";
["\0"] = "%z";
}
escape_pattern = function(s)
return (s:gsub(".", matches))
end
end
-- Contains all action handlers. Each action handler takes a 'state' table as
-- a parameter, and returns true if the current action is complete, false
-- otherwise.
people.actions = {}
people.actions.step = function(state)
local event = {type="step", dtime=state.dtime}
local err = people.exec_event(self, event)
if err then dbg.v1(state.ent.name ..":Lua error "..err) end
state.yaw = self.env.yaw
state.speed = self.env.speed
return false
end
people.actions.face = function(state)
if state.action.pos and state.action.pos.x and state.action.pos.y and state.action.pos.z then
state.yaw = face_dir(statepos, state.action.pos)
end
return true
end
people.actions.wait = function(state)
if state.action.time and tonumber(state.action.time) then
state.wait = tonumber(state.action.time)
end
return true
end
people.actions.follow = function(state)
if not state.action.name or type(state.action.name) ~= "string" then
dbg.v1(state.ent.name.." has invalid follow target")
return true
end
local player = minetest.get_player_by_name(state.action.name)
if not player then
dbg.v1(state.ent.name.." can't find "..state.action.name.." to follow them")
return true
end
local targetpos = player:getpos()
local dist = vector.length(vector.subtract(targetpos, state.pos))
if dist < 5 then
-- We're near enough, so just face the target and wait a bit
state.yaw = face_dir(state.pos, targetpos)
state.wait = 4
return false
end
local dir = math.random(math.pi*2)
targetpos = vector.add(targetpos, walk_velocity(2, dir))
targetpos = vector.round(targetpos)
-- TODO find surface Y
dbg.v3(state.ent.name.." following "..state.action.name.." via "..minetest.pos_to_string(targetpos))
state.action = {"go", pos=targetpos, prevaction=state.action}
return false
end
people.actions.go = function(state)
if (not state.action.pos) and (not state.action.footpathdest) and state.action.name and areas and type(state.action.name) == "string" then
-- Look for a path node with the right name
local pathdestarea = areas:findNearestArea(state.pos, "^path_"..escape_pattern(state.action.name).."$")
if pathdestarea then
dbg.v2(state.ent.name.." selected path destination "..pathdestarea.name)
local pathstartarea = areas:findNearestArea(state.pos, "^path_.+")
if pathstartarea then
dbg.v2(state.ent.name.." selected path start "..pathstartarea.name.." at "..minetest.pos_to_string(pathstartarea.pos1))
state.action.pos = vector.new(pathstartarea.pos1)
state.action.pos.y = state.action.pos.y + 0.5
state.action.footpathdest = pathdestarea
return false
end
end
-- No path node, so look for any area with the actual name and
-- just go there. (TODO: could then find the nearest path junction
-- to it and head for that)
local nearest = areas:findNearestArea(state.pos, "$"..escape_pattern(state.action.name).."^")
if not nearest then
dbg.v2(state.ent.name.." couldn't find location "..state.action.name)
return true
end
dbg.v2(state.ent.name.." set destination for centre of area "..nearest.name)
state.action.pos = vector.interpolate(nearest.pos1, nearest.pos2, 0.5)
state.action.pos.y = state.action.pos.y - 0.5
return false
end
if not state.action.pos and state.action.footpathdest then
-- path handling - only comes here when we've arrived at a new path square
-- (note, the range passed to findNearestArea determines our lookahead distance
local curpatharea, distance = areas:findNearestArea(state.pos, "^path_.+", nil, 10)
dbg.v3(state.ent.name.." at new footpath node at "..minetest.pos_to_string(state.pos))
if curpatharea and curpatharea.pos1.x == state.pos.x and curpatharea.pos1.z == state.pos.z then
if curpatharea.name == state.action.footpathdest.name then
dbg.v2(state.ent.name.." arrived at footpath destination")
return true
end
-- We're at a junction
dbg.v2(state.ent.name.." is at footpath junction "..curpatharea.name.." - looking for route to "..state.action.name)
state.action.lastpathpos = vector.new(state.pos.x, state.pos.y - 0.5, state.pos.z)
local nextpathdest = areas:findNearestArea(state.pos, "^to_.*"..escape_pattern(state.action.name)..";.*", 3)
if not nextpathdest then
nextpathdest = areas:findNearestArea(state.pos, "^to_%*$", 3)
end
if not nextpathdest then
dbg.v2(state.ent.name.." can't find next footpath direction")
return true
end
dbg.v2(state.ent.name.." leaving junction via "..nextpathdest.name.." "..minetest.pos_to_string(nextpathdest.pos1))
state.action.pos = vector.add(nextpathdest.pos1, {x=0, y=-0.5, z=0})
return false
end
if not state.action.lastpathpos then
dbg.v1(state.ent.name.." should have lastpathpos")
return true
end
local thispathpos = vector.new(state.pos.x, state.pos.y - 0.5, state.pos.z)
local nextpathpos = footpath_findnext(thispathpos, state.action.lastpathpos, false)
if not nextpathpos then
dbg.v1(state.ent.name.." can't find next footpath node")
return true
end
if not distance then distance = 10 end
while distance > 2 do
-- Look ahead a bit while going in a straight line, to increase speed
local aheadpathpos = footpath_findnext(nextpathpos, thispathpos, true)
if aheadpathpos then
thispathpos = nextpathpos
nextpathpos = aheadpathpos
distance = distance - 1
else
break
end
end
state.action.lastpathpos = thispathpos
state.action.pos = vector.add(nextpathpos, {x=0, y=0.5, z=0})
dbg.v3(state.ent.name.." set next footpath dest "..minetest.pos_to_string(state.action.pos))
return false
end
if not state.action.pos or not state.action.pos.x or not state.action.pos.y or not state.action.pos.z then
dbg.v1(state.ent.name.." has invalid go action")
return true
end
-- Get our current destination, which might be either our
-- actual final one, or an intermediate point
local curdest
if state.action.intermediate then
curdest = state.action.intermediate[1]
else
curdest = state.action.pos
end
if not (type(curdest) == "table" and curdest.x and curdest.y and curdest.z) then
dbg.v1(state.ent.name.." has invalid destination")
curdest = pos
end
local distance = vector.distance({x=state.pos.x, y=0, z=state.pos.z}, {x=curdest.x, y=0, z=curdest.z})
if distance < 0.2 then
if state.action.intermediate then
dbg.v3(state.ent.name.." arrived intermediate ")
if #state.action.intermediate == 1 then
state.action.intermediate = nil
else
table.remove(state.action.intermediate, 1)
end
end
dbg.v2(state.ent.name.." arrived near "..minetest.pos_to_string(curdest))
state.setpos = {x=curdest.x, y=state.pos.y, z=curdest.z}
if state.action.footpathdest or not state.action.intermediate then
-- If we arrived at the pos, but we still have a
-- pathdest or intermediate dest, we carry on to do that.
state.action.pos = nil
return false
end
-- We've finished!
dbg.v1(state.ent.name.." completed go action at "..minetest.pos_to_string(curdest))
return true
else
local colres = state.ent.object:get_last_collision_result()
if colres.collides_xz then
dbg.v2(state.ent.name.." collided at "..minetest.pos_to_string(state.pos))
-- We've hit something...
if distance < 32 then
local path = minetest.find_path(state.pos, curdest,
distance + 2, 1, 3, "A*_noprefetch")
if not path then
dbg.v2(state.ent.name.." cannot find path from "..
minetest.pos_to_string(state.pos).." to "..
minetest.pos_to_string(curdest))
if state.action.intermediate then
state.action.intermediate = nil
end
state.wait = 2
return false
else
if not state.action.intermediate then
state.action.intermediate = {}
end
-- for i = #path, 2, -1 do -- OLD implementation - opposite order to new implementation!
for i = 1, #path-1 do
table.insert(state.action.intermediate, 1, {x=path[i].x, y=path[i].y + 0.5, z=path[i].z})
end
dbg.v3(state.ent.name.." found path: "..dump(path))
end
else
-- pick a couple of nearby points and walk to them, then try again
local randomdir = math.random(math.pi * 2)
vec = walk_velocity(24, randomdir)
local rpos = {x=math.floor(state.pos.x + vec.x + 0.5), y=state.pos.y, z=math.floor(state.pos.z + vec.z + 0.5)}
randomdir = math.random(math.pi * 2)
vec = walk_velocity(24, randomdir)
local rpos2 = {x=math.floor(rpos.x + vec.x + 0.5), y=state.pos.y, z=math.floor(rpos.z + vec.z + 0.5)}
if not state.action.intermediate then
state.action.intermediate = {}
end
table.insert(state.action.intermediate, 1, rpos2)
table.insert(state.action.intermediate, 1, rpos)
dbg.v2(state.ent.name.." hit something, far from destination - talking a random detour")
end
else
if distance < 1.5 then
state.speed = distance * 0.75
else
state.speed = 2
end
state.yaw = face_dir(state.pos, curdest)
end
end
return false
end
local lastformbyplayer = {}
minetest.register_on_player_receive_fields(function(sender, formname, fields)
if formname ~= "people:form" then return end
if fields.quit then return end
if not fields.program then return end
local playername = sender:get_player_name()
local ent = lastformbyplayer[playername]
if not ent then return end
if playername ~= ent.owner then return end
ent.metadata.code = fields.code
local event = {type="program"}
local err = people.exec_event(ent, event)
if err then dbg.v1("Lua error "..err) end
end)
minetest.register_entity("people:person" ,{
hp_max = 1,
physical = true,
armor_groups = {immortal=1},
collisionbox = {-0.35,0,-0.35, 0.35,1.75,0.35},
visual = "mesh",
visual_size = {x=1, y=1},
mesh = "people_character.x",
textures = {"people_character.png"},
makes_footstep_sound = true,
name = "APerson",
owner = "",
metadata = {
code = "",
memory = {},
-- Current action. A table, as described in README.md
action = nil,
},
-- Time (game time) to wait until before doing anything else
wait = 0,
-- Lua environment
env = {},
stepheight = 1.1,
-- Customised properties
props = {},
on_activate = function(self, staticdata)
dbg.v3("on_activate, staticdata = "..dump(staticdata))
if staticdata and staticdata ~= "" then
local data = minetest.deserialize(staticdata)
if data then
if data.metadata then
self.metadata = data.metadata
end
if data.name then
self.name = data.name
end
if data.owner then
self.owner = data.owner
end
if data.wait then
self.wait = data.wait
end
if data.props then
self.props = data.props
end
end
if not people.is_valid_name(self.name) then
self.object:remove()
dbg.v1("Removing badly named person")
return
end
-- Entity becomes active
people.people_set_active(self)
end
self.env = people.create_environment(self)
self:update_props()
self.anim = nil
local event = {type="activate", dtime_s=dtime_s}
local err = people.exec_event(self, event)
if err then dbg.v1("Lua error "..err) end
end,
update_props = function(self)
self.object:set_properties(self.props)
end,
on_deactivate = function(self)
people.people_set_inactive(self)
end,
get_staticdata = function(self)
local data = {
name = self.name,
owner = self.owner,
metadata = self.metadata,
wait = self.wait,
props = self.props,
}
return minetest.serialize(data)
end,
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, killed)
if killed then
if people.people[self.name] then
people.people[self.name].dead = true
else
dbg.v1("Unknown person killed")
end
return
end
local playername = puncher:get_player_name()
if playername then
local event = {type="punched", puncher=playername}
local err = people.exec_event(self, event)
if err then dbg.v1("Lua error "..err) end
end
end,
on_rightclick = function(self, clicker)
local playername = clicker:get_player_name()
errmsg = "..."
if playername == self.owner then
formspec = "size[10,8]"..
"textarea[0.2,0.6;10.2,5;code;;"..minetest.formspec_escape(self.metadata.code).."]"..
"button[3.75,6;2.5,1;program;Program]"..
"label[0.1,5;"..minetest.formspec_escape(errmsg).."]"
else
local message = "Hello non-owner"
formspec = "size[8,4]"
.."label[0,0;"..message.."]"
end
minetest.show_formspec(playername, "people:form", formspec)
lastformbyplayer[playername] = self
end,
on_tell = function(self, sender, senderpos, message)
local event = {type="tell", sender=sender, senderpos=senderpos, message=message}
local err = people.exec_event(self, event)
if err then dbg.v1("Lua error "..err) end
end,
on_step = function(self, dtime)
if self.wait ~= 0 then
if minetest.get_gametime() < self.wait then return end
dbg.v3(self.name.." finished wait at "..minetest.get_gametime())
self.wait = 0
end
-- Prepare state table for action handler
local state = {
-- In - current yaw, Out - used as new yaw
yaw = self.object:getyaw(),
-- In - current position
pos = self.object:getpos(),
-- In - 0, Out - new speed, in yaw direction
speed = 0,
-- Out - if set, a new position to set
setpos = nil,
-- In - current action, Out - modified action
action = self.metadata.action,
-- In - 0, Out - non-zero sets a wait of that many seconds - doing this
-- will cause a wait and then the current action will
-- continue - different to setting a "wait" action!
wait = 0,
-- In - the entity, in case anything needs to be called on it (avoid
-- that if possible, but for example, getting the last collision
-- result is currently done this way)
ent = self,
-- In - dtime
dtime = dtime,
}
local action_done = false
if not state.action or not state.action[1] then
-- No current action, so let the lua code choose one...
local event = {type="act", dtime=state.dtime}
local err = people.exec_event(self, event)
if err then dbg.v1("Lua error "..err) end
state.action = self.metadata.action
if not state.action then
self.metadata.action = {"wait", time=120}
dbg.v2(state.ent.name.." failed to choose action - setting wait")
else
dbg.v1(state.ent.name.." set new action: "..dump(self.metadata.action))
end
state.action = self.metadata.action
end
-- Run the appropriate handler for the current action...
local handler = people.actions[state.action[1]]
if handler then
action_done = handler(state)
-- Because state.action can be replaced...
self.metadata.action = state.action
else
dbg.v1(state.ent.name.." has unknown action: "..dump(self.metadata.action))
state.action = nil
end
if action_done then
if state.action.prevaction then
self.metadata.action = state.action.prevaction
else
self.metadata.action = nil
end
end
if state.wait ~= 0 then
self.wait = minetest.get_gametime() + state.wait
dbg.v3("Set "..self.name.." to wait until "..self.wait)
if state.wait > 30 then
self.object:set_autonomous(0)
else
self.object:set_autonomous(1)
end
else
self.object:set_autonomous(1)
end
-- Set appropriate animation
local wantanim = "stand"
if state.speed > 0 then
wantanim = "walk"
end
if wantanim ~= self.anim then
self.object:set_animation(animations[wantanim], 15, 0)
self.anim = wantanim
end
if state.setpos then
self.object:setpos(state.setpos)
end
local setvel = walk_velocity(state.speed, state.yaw)
setvel.y = -10
self.object:setvelocity(setvel)
self.object:setyaw(state.yaw)
end,
})
people.create_environment = function(ent)
return {
mem = ent.metadata.memory,
log = function(msg)
dbg.v1(" >> "..msg)
end,
tostring = tostring,
tonumber = tonumber,
type = type,
string = {
byte = string.byte,
char = string.char,
find = string.find,
format = string.format,
gmatch = string.gmatch,
gsub = string.gsub,
len = string.len,
lower = string.lower,
upper = string.upper,
match = string.match,
rep = string.rep,
reverse = string.reverse,
sub = string.sub,
},
math = {
abs = math.abs,
acos = math.acos,
asin = math.asin,
atan = math.atan,
atan2 = math.atan2,
ceil = math.ceil,
cos = math.cos,
cosh = math.cosh,
deg = math.deg,
exp = math.exp,
floor = math.floor,
fmod = math.fmod,
frexp = math.frexp,
huge = math.huge,
ldexp = math.ldexp,
log = math.log,
log10 = math.log10,
max = math.max,
min = math.min,
modf = math.modf,
pi = math.pi,
pow = math.pow,
rad = math.rad,
random = math.random,
sin = math.sin,
sinh = math.sinh,
sqrt = math.sqrt,
tan = math.tan,
tanh = math.tanh,
},
table = {
insert = table.insert,
maxn = table.maxn,
remove = table.remove,
sort = table.sort
}
}
end
local create_sandbox = function (code, env)
-- Create Sandbox
if code:byte(1) == 27 then
return _, "You Hacker You! Don't use binary code!"
end
f, msg = loadstring(code)
if not f then return nil, msg end
setfenv(f, env)
return f
end
local code_prohibited = function(code)
-- Clean code
local prohibited = {"while", "for", "repeat", "until", "function", "goto"}
for _, p in ipairs(prohibited) do
if string.find(code, p) then
return "Prohibited command: "..p
end
end
end
people.exec_event = function(ent, event)
local code = ent.metadata.code
local env = ent.env
env.action = ent.metadata.action
env.event = event
local pos = ent.object:getpos()
local vel = ent.object:getvelocity()
local yaw = ent.object:getyaw()
env.pos = {x=pos.x,y=pos.y,z=pos.z}
env.vel = {x=vel.x,y=vel.y,z=vel.z}
env.yaw = yaw
env.speed = 0
local prohibited = code_prohibited(code)
if prohibited then return prohibited end
-- create the sandbox and execute code
local chunk, msg = create_sandbox(code, env)
if not chunk then return msg end
local success, msg = pcall(chunk)
if not success then return msg end
if env.action and ((not ent.metadata.action) or env.action[1] ~= ent.metadata.action[1]) then
-- Cancel a wait if the action type was changed
ent.wait = 0
end
ent.metadata.action = env.action
end
local tick_wake = 0
local rate_wake = 1
minetest.register_globalstep(function(dtime)
tick_wake = tick_wake + dtime
if tick_wake > rate_wake then
tick_wake = 0
people.people_wake()
end
end)