people/init.lua

503 lines
11 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")
dofile(people_modpath .. "/form.lua")
-- 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 = {}
dofile(people_modpath .. "/actions/step.lua")
dofile(people_modpath .. "/actions/face.lua")
dofile(people_modpath .. "/actions/wait.lua")
dofile(people_modpath .. "/actions/follow.lua")
dofile(people_modpath .. "/actions/go.lua")
dofile(people_modpath .. "/actions/drop.lua")
people.presets = {}
for _, p in ipairs({"FollowOwner", "SimpleCommands", "RouteWalker"}) do
local file = io.open(minetest.get_modpath("people").."/presets/"..p..".lua", "r")
local code
if file then
code = file:read("*all")
else
code = "MISSING"
end
table.insert(people.presets, {code=code, name=p})
end
dbg.v2("Presets: "..dump(people.presets))
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 escape_pattern
do
local matches =
{
["^"] = "%^";
["$"] = "%$";
["("] = "%(";
[")"] = "%)";
["%"] = "%%";
["."] = "%.";
["["] = "%[";
["]"] = "%]";
["*"] = "%*";
["+"] = "%+";
["-"] = "%-";
["?"] = "%?";
["\0"] = "%z";
}
people.escape_pattern = function(s)
return (s:gsub(".", matches))
end
end
function people.get_inv(self, name)
local st = {}
local list = self.inventory:get_list(name)
if list then
for i=1,#list,1 do
table.insert(st,list[i]:to_string())
end
end
return st
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 = "",
code = "@"..people.presets[1].name,
memory = {},
-- Current action. A table, as described in README.md
action = {"wait", time=5},
-- Time (game time) to wait until before doing anything else
wait = 0,
-- Lua environment
env = {},
stepheight = 0.6,
-- Customised properties
props = {},
on_activate = function(self, staticdata)
local setactive = false
dbg.v3("on_activate, staticdata = "..dump(staticdata))
local restored
if staticdata and staticdata ~= "" then
restored = minetest.deserialize(staticdata)
if restored then
if restored.action then
self.action = restored.action
else
self.action = {"wait", time=5}
end
if restored.memory then
self.memory = restored.memory
else
self.memory = {}
end
if restored.code then
self.code = restored.code
else
self.code = "@"..people.presets[1].name
end
if restored.name then
self.name = restored.name
end
if restored.owner then
self.owner = restored.owner
end
if restored.wait then
self.wait = restored.wait
end
if restored.props then
self.props = restored.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.inventory = minetest.create_detached_inventory("people_"..self.name, nil)
self.inventory:set_size("main", 8*4);
if restored and restored.inv_main then
local inv_main = restored.inv_main
for i=1,#inv_main,1 do
self.inventory:set_stack("main", i, inv_main[i])
end
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,
code = self.code,
action = self.action,
memory = self.memory,
wait = self.wait,
props = self.props,
inv_main = people.get_inv(self, "main")
}
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()
-- Use a temporary code editing variable until changes are submitted
self.fcode = self.code
people.show_form(self, playername)
end,
on_tell = function(self, sender, message)
local event = {type="tell", sender=sender:get_player_name(),
senderpos=sender:getpos(), 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.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.action
if not state.action then
self.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.action))
end
state.action = self.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.action = state.action
else
dbg.v1(state.ent.name.." has unknown action: "..dump(self.action))
state.action = nil
end
if action_done then
if state.action.prevaction then
self.action = state.action.prevaction
else
self.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 = vector.from_speed_yaw(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.memory,
log = function(msg)
dbg.v1(" >> "..msg)
end,
tostring = tostring,
tonumber = tonumber,
type = type,
vector = vector,
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.code
if string.sub(code, 1, 1) == "@" then
local f = string.sub(code, 2)
for _, p in pairs(people.presets) do
if p.name == f then
code = p.code
break
end
end
end
local env = ent.env
env.action = ent.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.action) or env.action[1] ~= ent.action[1]) then
-- Cancel a wait if the action type was changed
ent.wait = 0
end
ent.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)