Initial commit

master
Ciaran Gultnieks 2014-04-03 08:32:23 +01:00
commit 86aabfa9b2
8 changed files with 2723 additions and 0 deletions

212
README.md Normal file
View File

@ -0,0 +1,212 @@
#People Mod
NOTE: Requires my fork of minetest to work properly. In particular:
* https://github.com/CiaranG/minetest/commit/740f057253495633db4cc53c905d4ea4ff1167c8
* https://github.com/CiaranG/minetest/commit/7b7e8bd4b924b74271eb26b30fa681eb8bd780c5
* https://github.com/CiaranG/minetest/commit/8eb489ece109e56c4d0fa01a0b956f7fa9f0ab56
* https://github.com/CiaranG/minetest/commit/8772041b43b78c86bc32e5682391286bca498848
* https://github.com/CiaranG/minetest/commit/f5b73e0baabbfe68bc6a9a52c5221930e4306ed2
This mod provides 'people' (non player characters) that are programmable
in-game. Each NPC's Lua code runs inside a secure sandbox for that NPC only.
Author: Ciaran Gultnieks
License: LGPL
Includes lua sandbox setup code from mesecons by Jeija (also LGPL), and inspiration
from the npcf mod, and (obviously!) the mesecons lua controller design.
The character model is a modified version of the default player model from
the default minetest game.
##Requirements
Optionally, you might want these mods:
* areas (my fork of it) - https://github.com/CiaranG/areas - for footpath
navigation support
* moddebug - https://gitlab.com/CiaranG/moddebug - for debug logging
##Usage
To create a lua_npc, issue this command:
/people create Bob
This places a person called "Bob" at your current location.
Then, right-click the npc at any time to program it. For now, until I think
of a better name, we'll refer to the code you enter here as "the person's
lua code"!
##The Person's Lua Code
This code, as entered in the right-click dialog for a person you own, is
really an event handler. It runs in a sandbox, and when called will always
have a variable called 'event'. This is a table, and the "type" field
determines the type of event that caused the call.
Here is a very simple example:
if event.type == "program" then
action = {"follow", name="Ciaran"}
end
When you enter this and hit the Program button, the person will start following
the player called Ciaran and keep doing that until they log off. Because the
code does nothing in response to an "act" event, at that point they will go into
a permanent wait (sleep) state.
##Events
The following event types are received by the person's lua code:
###act
This happens when the person has no current action (see the Actions section).
The handler should select a new action, by setting the 'action' variable. If
it fails to do so, the person will do an automatic wait.
###tell
This happens when a player (who must be the owner, current) sends a message
to the person. It can be used to respond to commands.
###step
This happens only when the "step" action is running. It's called every server
step, with event.dtime containing the time since the last step. The handler
should update the speed and yaw variables, or set a new action.
###program
This happens when the lua code has been edited.
###activate
This happens when the entity is activated (either it's brand new, or is in
a block that was unloaded but is now loaded). It is called with event.dtime_s
containing the time since it was last active.
###punched
This happens when a player punches the entity. The name of the offending player
is sent as event.puncher.
##Actions
Actions are the basic building blocks of object behaviour. A person always
has a current action which they are completing, the execution of which is
handled by the mod. The job of the lua programming within the person is to
sit at a higher level of 'conciousness' and select the appropriate actions.
The person's lua code receives an "act" event when it should select an
action - usually when the current action completes. It can also set/replace
the current action in response to any other event. Setting the action is
a simple case of setting the 'action' variable within the lua code.
An action is defined by a table, where the first element [1] is the ID of
the action, and additional named fields contain parameters. For a simple
example, {"go", pos={5,0,5}} is an action to move the person to the given
position.
###Action types
The following actions are currently defined:
####go, with pos={x,y,z}
Walks to the given position.
####go, with name="string"
Walks to a named position - names are looked up using the areas module, if
present This converts to go, pos={x,y,z}, with name retained for reference.
####follow, with name="playername"
Follows the given player indefinitely (or at least until they log off).
####face, with pos={x,y,z}
Standing still, face towards the given position.
####wait, with time
Do nothing for that many seconds. This may allow the entity to be deactivated
and unloaded, in which case it will be reactivated at the appropriate time.
Short waits may not result in deactivation.
####step
Allow's the person's lua code to handle on_step itself. It will receive
"step" events (with dtime) and should reset the action when it no longer
needs to do this. It can update 'speed' and 'yaw' and these will be used.
###Nesting
Any action can have a "prevaction" field, which should be a valid action
itself. It will be set when the action has completed. This can be used to nest
actions.
The actions themselves may make use of this functionality. For example, the
follow action is implemented by repeatedly selecting positions near the target
player, and then replacing it self with a "go" action with the original follow
nested as a "prevaction" within it. Thus, when the "go" completes, the follow
resumes once more.
##Chat Commands
People can be managed and controlled via chat commands.
The following commands are available:
###/people help
Lists all the available commands.
###/people create <name>
Creates a new person, at your current location.
###/people delete <name>
Deletes a person.
###/people where <name>
Tells you the current location of a person.
###/people summon <name>
Moves the person to your current location.
###/people setowner <name> <playername>
Changes the owner of the person.
###/people tell <name> <message>
The person receives a "tell" event with the given message.
###/people status
Lists all people in the word, and their current status (active/inactive).
###/people skin <name> <skin_name|list>
Sets the skin of the person to the named one. (Or use 'list' to get a
list of available skins, which come from the skins mod).
##Autonomy and Activation
People are 'autonomous' - i.e. they will continue to act even when no player
is nearby. Indeed, they will continue to act even when no player at all is
logged in to the server.
However, they are designed to be able to not remain active unless necessary.
This is mostly done by use of the "wait" action.
If you set a person to wait for 5 minutes, then assuming no player or other
active entity is nearby, the block the person is in (and surrounding blocks)
will be unloaded, and the person will cease to exist as far as the minetest
engine is concerned. However, after 5 minutes, this mod will reactivate it
by reloading the block.
###Force Loading
This mod uses the 'force loading' API (which is silly and inadequate) to
reactive sleeping entities when it needs to. It does not require the
persistence over server restarts of force-loaded blocks, and while it ought
to clean up any force loading it does almost immediately, that cannot happen
if there is a server crash or other unclean shutdown at the wrong moment.
You could mitigate this by always removing the force loaded blocks file from
the world directory on startup.
Ultimately it won't use this 'feature' at all.

225
commands.lua Normal file
View File

@ -0,0 +1,225 @@
local dbg
if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end
--- Get a person name from the next argument
-- @return The person name, the actual person, and the rest of the
-- arguments. If the person name is nil, the arguments are invalid.
-- If person is nil, the name is valid, but it's not a person that
-- actually exists.
local get_person = function(args)
if not args then return nil, nil, nil end
person_name, args = string.match(args, "^([^ ]+)(.*)")
if person_name and not people.is_valid_name(person_name) then
return nil, nil, nil
end
local person
if person_name then person = people.people[person_name] end
if args then args = string.sub(args, 2) end
return person_name, person, args
end
local subcmd = {}
subcmd.help = function(playername, args)
msg = "Subcommands:"
for c, _ in pairs(subcmd) do
msg = msg.." "..c
end
return msg, true
end
subcmd.create = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not person_name then
return "Name needs to be specified", false
end
if person then
return "Person "..person_name.." already exists", false
end
local player = minetest.get_player_by_name(playername)
if not player then
return "people create can only be used by a player", false
end
local pos = player:getpos()
local obj = minetest.add_entity(pos, "people:person")
if not obj then
return "Failed to add_entity", false
end
local ent = obj:get_luaentity()
ent.name = person_name
ent.owner = playername
people.people_add(ent)
return "Created "..person_name, true
end
subcmd.delete = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not person then
return "Specify a valid person"
end
local ent = people.people[person_name].entity
if not ent then
return "Can't delete when inactive", false
end
ent.object:remove()
people.people[person_name] = nil
return "Deleted "..person_name, true
end
subcmd.summon = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not person then
return "Specify a valid person"
end
local player = minetest.get_player_by_name(playername)
if not player then
return "people summon can only be used by a player", false
end
local pos = player:getpos()
local ent = people.people[person_name].entity
if not ent then
return "Can't summon because not active", false
end
ent.object:setpos(pos)
return "Summoned "..person_name, true
end
subcmd.where = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not person then
return "Specify a valid person"
end
local ent = people.people[person_name].entity
if not ent then
return person_name.." is inactive", false
end
return person_name.." is at "..minetest.pos_to_string(ent.object:getpos()), true
end
subcmd.setowner = function(playername, person, person_name, args)
local person, person_name
person_name, person, args = get_person(args)
if not person then
return "Specify a valid person"
end
local ent = people.people[person_name].entity
if not ent then
return person_name.." is inactive", false
end
if not minetest.get_player_by_name(args) then
return "No such new owner"
end
ent.owner = args
return "Set owner of "..person_name.. " to "..args, true
end
subcmd.list = function(playername, args)
local count = 0
for k, v in pairs(people.people) do
local msg = k.." : "
if v.entity then
msg = msg.."active at "..minetest.pos_to_string(v.entity.object:getpos())
else
msg = msg.."inactive for another "..v.waketime - minetest.get_gametime().."s at "..minetest.pos_to_string(v.pos)
end
minetest.chat_send_player(playername, msg)
count = count + 1
end
return count.." people", true
end
subcmd.tell = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not person then
return "Specify a valid person"
end
local ent = people.people[person_name].entity
if not ent then
return person.name.." is inactive, so can't hear you", false
end
if playername ~= ent.owner then
return person.name.." doesn't listen to you", false
end
local sender = minetest.get_player_by_name(playername)
if not sender then return end
senderpos = sender:getpos()
ent.on_tell(sender, senderpos, args)
end
subcmd.skin = function(playername, args)
local person, person_name
person_name, person, args = get_person(args)
if not args then
return "More arguments needed", false
end
if not minetest.get_modpath("skins") then
return "Skins mod is not installed", false
end
if person_name == "list" then
local texlist = ""
for _, skin in ipairs(skins.list) do
if string.match(skin, "^character_") then
if texlist ~= "" then texlist = texlist.." " end
texlist = texlist..string.sub(skin, 11)
end
end
return "Available skins: "..texlist, true
end
if not person then
return "Specify a valid person, or 'list'", false
end
if not args then
return "Specify skin", false
end
local req = "character_"..args
for _, skin in ipairs(skins.list) do
if skin == req then
person.entity.props.textures = {req..".png"}
person.entity:update_props()
return "Skin updated", true
end
end
return "No such skin as '"..args.."'", false
end
minetest.register_chatcommand("people", {
params = "<cmd> [name] [args]",
description = "Commands for working with the people module. 'people help' for help.",
func = function(name, param)
if not minetest.check_player_privs(name, {server=true}) then
return "People commands are currently all restricted to admins", false
end
local cmd, args
cmd, args = string.match(param, "^([^ ]+)(.*)")
if not cmd then return subcmd.help(nil, nil, nil, nil) end
if subcmd[cmd] then
if args then args = string.sub(args, 2) end
return subcmd[cmd](name, args)
end
return "No such people command '"..cmd.."' - see 'people help'", false
end
})

1
depends.txt Normal file
View File

@ -0,0 +1 @@
areas?

778
init.lua Normal file
View File

@ -0,0 +1,778 @@
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)

Binary file not shown.

1392
models/people_character.x Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

115
tracking.lua Normal file
View File

@ -0,0 +1,115 @@
local dbg
if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end
-- This table tracks all the people in the world. It's keyed on the person
-- name, and each entry is a table keyed as follows:
-- entity - the active luaentity reference, or nil if not active
-- pos - when inactive, the entity's position
-- waketime - time in seconds until this entity should be re-awoken,
-- or nil (only ever set when inactive)
-- waking - used internally to track wakeup status
-- dead - used internally to track when dead
-- Changes to this should be via the functions below.
people.people = {}
local file = io.open(minetest.get_worldpath().."/people_people", "r")
if file then
people.people = minetest.deserialize(file:read("*all"))
file:close()
end
local people_save = function()
-- We save without the entity field, because all will be inactive
-- when reloaded
local speople = {}
for k, v in pairs(people.people) do
speople[k] = {
pos = v.pos,
waketime = v.waketime,
}
end
local file = io.open(minetest.get_worldpath().."/people_people", "w")
if file then
file:write(minetest.serialize(speople))
file:close()
end
end
people.people_add = function(entity)
people.people[entity.name] = {entity=entity}
dbg.v2(entity.name.." added")
end
people.people_set_active = function(entity)
local pr = people.people[entity.name]
if not pr then
-- Should never happen
dbg.v1(entity.name.." reactivated without existing record")
pr = {}
people.people[entity.name] = pr
end
pr.entity = entity
if pr.waketime then
local loadpos = pr.pos
if not loadpos then
dbg.v1("Activated entity has waketime but no pos")
else
dbg.v2("Free forceload for "..minetest.pos_to_string(loadpos))
minetest.forceload_free_block(loadpos)
end
pr.pos = nil
pr.waketime = nil
pr.waking = nil
end
dbg.v2(entity.name.." activated")
end
people.people_set_inactive = function(entity)
if not people.people[entity.name] then
dbg.v1("people_set_inactive for "..entity.name or nil.." with no people record")
return
end
if people.people[entity.name].dead then
people.people[entity.name] = nil
dbg.v2(entity.name.." deactivated forever")
else
people.people[entity.name].entity = nil
people.people[entity.name].pos = entity.object:getpos()
if entity.wait == 0 then
people.people[entity.name].waketime = minetest.get_gametime() + 1
else
people.people[entity.name].waketime = entity.wait
end
dbg.v2(entity.name.." deactivated")
end
people_save()
end
--- Called at regular intervals
-- Handles waking entities that are unloaded and shouldn't be any more.
-- Once a second is probably about right, although more frequently wouldn't
-- hurt.
people.people_wake = function()
for k, v in pairs(people.people) do
if v.waketime and v.waketime <= minetest.get_gametime() and not v.waking then
-- Need to flag that we're doing it, because there will be a
-- delay before the block is actually loaded.
v.waking = true
-- Wake the entity by (yuk) setting a forceload. The activation
-- of the entity will cause the forceload to be freed.
dbg.v2("Set forceload for "..minetest.pos_to_string(v.pos))
minetest.forceload_block(v.pos)
-- Only one at a time, even though this will introduce some
-- inaccuracy as to when they actually wake
break
end
end
end