local dbg if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end people = {} people.hud_show_by_player = {} people.attach_by_player = {} 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. When complete, a second boolean is also returned which says -- whether the action completed succesfully, or failed due to an error. 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") dofile(people_modpath .. "/actions/dig.lua") dofile(people_modpath .. "/actions/place.lua") dofile(people_modpath .. "/actions/gather.lua") dofile(people_modpath .. "/actions/stash_and_retrieve.lua") dofile(people_modpath .. "/actions/buy_and_sell.lua") dofile(people_modpath .. "/actions/obtain.lua") dofile(people_modpath .. "/actions/craft.lua") dofile(people_modpath .. "/actions/attackplayer.lua") dofile(people_modpath .. "/actions/pickup.lua") dofile(people_modpath .. "/actions/rightclicknode.lua") dofile(people_modpath .. "/actions/harvest.lua") dofile(people_modpath .. "/actions/tunnel.lua") dofile(people_modpath .. "/actions/building.lua") dofile(people_modpath .. "/actions/claimplot.lua") people.presets = {} for p, priv in pairs({FollowOwner=false, SimpleCommands=false, RouteWalker=false, FarmHand=true, CowTest=true, Miner=true, TreeFarmer=true}) 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, priv=priv}) end 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.auth_table[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 function copyaction(orig) if type(orig) ~= "table" then return orig end local copy = {} for k, v in next, orig, nil do copy[k] = copyaction(v) end return copy end 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 = {} if self.inventory then 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 end return st end function people.create(pos, name, owner) local obj = minetest.add_entity(pos, "people:person") if not obj then return "Failed to add_entity" end local ent = obj:get_luaentity() ent.name = name ent.owner = owner ent.inventory = minetest.create_detached_inventory("people_"..name, nil) ent.inventory:set_size("main", 8*4); people.people_add(ent) return nil end minetest.register_entity("people:person" ,{ hp_max = 20, physical = true, -- TODO For now at least, don't collide with objects - specifically I don't -- want them colliding with other 'people' or players, for the same reason -- players don't collide with each other. collide_with_objects = false, 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, can_use_doors = true, owner = "", code = "@"..people.presets[1].name, -- Current action. A table, as described in README.md -- Is nil when it's time for the person to select a new action. action = nil, -- Time (game time) to wait until before doing anything else wait = 0, stepheight = 1.1, on_activate = function(self, staticdata) local setactive = false self.memory = {} self.env = {} self.props = {} -- The following controls gathering, which is the digging of nodes and -- picking up of items, which happens in parallel with any other action. -- self.gather = { nodes = {}, -- list of nodes to dig up topnodes = {}, -- list of topnodes to dig items = {}, -- list of items to pick up plant = nil, -- name of seed to plant, or nil animal = nil, -- name of animal to gather from animal_wield = nil, -- when animal set, the item to wield } self.name = "" local restored if staticdata and staticdata ~= "" then restored = minetest.deserialize(staticdata) if restored then if restored.action then self.action = restored.action 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 if restored.gather then self.gather = restored.gather -- backward compatibility... if not self.gather.topnodes then self.gather.topnodes = {} end else self.gather = { nodes = {}, topnodes = {}, items = {}, } end self.inventory = minetest.create_detached_inventory("people_"..self.name, nil) self.inventory:set_size("main", 8*4); if 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 if not people.is_valid_name(self.name) then self.object:remove() dbg.v1("Removing badly named person") return end 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 dbg.v3("Done on_activate for "..self.name) 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, gather = self.gather, 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) local playername = puncher:get_player_name() if killed then if people.people[self.name] then people.people[self.name].dead = true if playername then dbg.v1(self.name.." killed by "..playername) else dbg.v1(self.name.." killed by something mysterious") end else dbg.v1("Unknown person killed") end return end 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 self.object:setvelocity({x=0,y=-10,z=0}) return end dbg.v3(self.name.." finished wait at "..minetest.get_gametime()) self.wait = 0 end local pos = self.object:getpos() -- If we don't have a current action, we let the lua code choose one... if not self.action then self.gather.lastpos = nil local event = {type="act"} local err = people.exec_event(self, event) if err then dbg.v1("Lua error "..err) end if not self.action then self.action = {"wait", time=120} dbg.v2(self.name.." failed to choose action - setting wait, at "..minetest.pos_to_string(pos)) else dbg.v1(self.name.." set new action: "..minetest.serialize(self.action)) end end -- Handle gathering -- TODO: Only happens during go and follow currently. Can't happen -- during "dig" or any subaction that "dig" might set - which poses -- a problem if "dig" were to use "go"!! Better way of handling this -- required. if self.action and (self.action[1] == "go" or self.action[1] == "follow" or self.action[1] == "wait") and (#self.gather.items + #self.gather.nodes + #self.gather.topnodes > 0 or self.gather.plant or self.gather.animal) then local dist = 99 if self.gather.lastpos then dist = vector.distance(pos, self.gather.lastpos) end if dist > 3 then local done = false if #self.gather.nodes > 0 then dbg.v2(self.name.." looking for "..minetest.serialize(self.gather.nodes).." near "..minetest.pos_to_string(pos)) local foundpos = minetest.find_node_near( vector.add(vector.round(pos), {x=0,y=1,z=0}), 3, self.gather.nodes) if foundpos then local n = minetest.get_node(foundpos) dbg.v2(self.name.." found "..n.name.." at "..minetest.pos_to_string(foundpos)) self.action = {"dig", pos=foundpos, prevaction=self.action} done = true end end if not done and #self.gather.topnodes > 0 then dbg.v2(self.name.." looking for topnodes "..minetest.serialize(self.gather.topnodes).." near "..minetest.pos_to_string(pos)) local minp = vector.add(pos, {x=-3,y=-3,z=-3}) local maxp = vector.add(pos, {x=3, y=3, z=3}) local pp = minetest.find_nodes_in_area(minp, maxp, self.gather.topnodes) for _, ppp in pairs(pp) do local n = minetest.get_node(ppp) local pabove = vector.add(ppp, {x=0, y=1, z=0}) if minetest.get_node(pabove).name == "air" then local pbelow = vector.add(ppp, {x=0, y=-1, z=0}) if minetest.get_node(pbelow).name == n.name then dbg.v2(self.name.." found "..n.name.." at "..minetest.pos_to_string(ppp)) self.action = {"dig", pos=ppp, prevaction=self.action} done = true end end end end if not done and #self.gather.items > 0 then dbg.v2(self.name.." looking for items "..minetest.serialize(self.gather.items).." near "..minetest.pos_to_string(pos)) local objects = minetest.get_objects_inside_radius( vector.add(vector.round(pos), {x=0,y=1,z=0}), 6) for _, obj in ipairs(objects) do if obj:get_entity_name() == "__builtin:item" then local ent = obj:get_luaentity() if ent.itemstring then for _, ti in ipairs(self.gather.items) do if string.match("^" .. ti .. " .*", ent.itemstring) then self.action = {"pickup", pos=obj:getpos(), item=ti, prevaction=self.action} done = true break end end end end if done then break end end end if not done and self.gather.plant then if self.gather.plant and type(self.gather.plant) == "string" and self.inventory:contains_item("main", ItemStack(self.gather.plant)) then if self.gather.plant == "default:sapling" then soilitems = {"default:dirt", "default:dirt_with_grass"} elseif self.gather.plant == "farming_plus:cocoa_sapling" then soilitems = {"default:desert_sand", "default:sand"} else soilitems = {"farming:soil_wet"} end soils = minetest.find_nodes_in_area( {x=pos.x-3, y=pos.y-2, z=pos.z-3}, {x=pos.x+3, y=pos.y+2, z=pos.z+3}, soilitems) for _, foundpos in pairs(soils) do plantpos = {x=foundpos.x,y=foundpos.y+1,z=foundpos.z} local n = minetest.get_node(plantpos) if n.name == "air" then dbg.v1(self.name.." found empty soil below "..minetest.pos_to_string(plantpos)) self.action = {"place", pos=plantpos, item=self.gather.plant, prevaction=self.action} done = true break end end end end if not done and animals and (self.gather.animal and self.gather.animal_wield) then if self.inventory:contains_item("main", ItemStack(self.gather.animal_wield)) then self.wielded = self.gather.animal_wield local spinfo = animals.species[self.gather.animal] if not spinfo then dbg.v1(self.name.." trying to harvest unknown species "..self.gather.animal) self.gather.animal = nil self.gather.animal_wield = nil else local hitit = nil local objects = minetest.get_objects_inside_radius(pos, 5) for k, obj in ipairs(objects) do if not obj:is_player() then local ent = obj:get_luaentity() if ent then dbg.v3(self.name.." examining "..(ent.name or "")) if ent.species == self.gather.animal then if spinfo.is_harvestable and spinfo.is_harvestable(ent) then hitit = ent break end end end end end if hitit then dbg.v1(self.name.." decided to harvest "..hitit.name) self.action = {"harvest", name=hitit.name, prevaction=self.action} done = true end end end end if not done then -- We didn't find anything to do here, so we can move on self.gather.lastpos = pos end end 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 = pos, -- 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 or replaced action action = self.action, -- In - current gather settings, Out - modified or replaced gather settings gather = self.gather, -- 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 - nil, Out - if set, use that animation - otherwise attempt to -- select automatically anim = nil, -- In - 0, Out - damage to be applied. damage = 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, } -- Run the appropriate handler for the current action... local action_done = false local action_success = false local handler = people.actions[state.action[1]] if handler then action_done, action_success = handler(state) -- Because state.action can be replaced... if state.action ~= self.action then dbg.v3(state.ent.name.." action handler replaced action with "..minetest.serialize(state.action)) self.action = state.action end if state.gather ~= self.gather then self.gather = state.gather end else dbg.v1(state.ent.name.." has unknown action: "..minetest.serialize(self.action)) self.action = nil end if action_done then local lastaction = state.action if state.action.prevaction then dbg.v3(state.ent.name.." completed action - setting prevaction") self.action = state.action.prevaction else if action_success == true then dbg.v3(state.ent.name.." completed action") if state.action.successaction then dbg.v3(state.ent.name.." completed action - setting successaction") self.action = state.action.successaction else self.action = nil end else dbg.v3(state.ent.name.." failed action: "..minetest.serialize(action_success)) self.action = nil local event = {type="actfail", failedaction=lastaction, failcode=action_success} local err = people.exec_event(self, event) if err then dbg.v1("Lua error "..err) end end end end if state.wait ~= 0 then self.wait = minetest.get_gametime() + state.wait dbg.v3(self.name.." will 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 = state.anim if not wantanim then wantanim = "stand" if state.speed > 0 then wantanim = "walk" end 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 { log = function(msg) dbg.v1(" >> "..msg) end, dump = dump, 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) dbg.v3(ent.name.." exec event "..minetest.serialize(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.name = ent.name env.gather = ent.gather env.mem = ent.memory env.carrying = function(item) local inv = ent.inventory local count = 0 for i, stack in ipairs(inv:get_list("main")) do local nn = stack:get_name() if nn == item then count = count + stack:get_count() end end return count end env.has_money = function() local inv = ent.inventory return currency.count_money(inv, "main") end env.vector = vector 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 if ent.action ~= env.action then ent.action = copyaction(env.action) end ent.gather = env.gather end local tick_wake = 0 local rate_wake = 1 local tick_hud = 0 local rate_hud = 1.5 minetest.register_globalstep(function(dtime) tick_wake = tick_wake + dtime tick_hud = tick_hud + dtime if tick_wake > rate_wake then tick_wake = 0 people.people_wake() end if tick_hud > rate_hud then tick_hud = 0 dbg.v3("HUD TICK") for _, player in pairs(minetest.get_connected_players()) do local playername = player:get_player_name() local pshow = people.hud_show_by_player[playername] if pshow then if not people.people[pshow.name] then -- Probably the person died or was deleted... people.hud_show_by_player[playername] = nil player:hud_remove(pshow.id) else local pos, col local ent = people.people[pshow.name].entity if ent then pos = ent.object:getpos() col = 0xFF00FF else pos = vector.new(people.people[pshow.name].pos) col = 0xFFFF00 end pos.y = pos.y + 1.5 if not pshow.hudid then pshow.hudid = player:hud_add({ hud_elem_type = "waypoint", name = pshow.name, text = "m", number = col, world_pos = pos }) pshow.pos = pos pshow.col = col else dbg.v3("HUD DIST ".. vector.distance(pos, pshow.pos)) if pshow.pos and vector.distance(pos, pshow.pos) > 1 then pshow.pos = pos player:hud_change(pshow.hudid, "world_pos", pos) end if pshow.col and col ~= pshow.col then pshow.col = col player:hud_change(pshow.hudid, "number", col) end end end end end end end) -- Get a fake player object that resembles a real one, for the specified -- entity. Can be used, in some circumstances, for making API calls that -- would normally require a real player. -- -- The player name is actually returned as the owner of the person. This -- should allow it to, for example, dig with the same permissions as the -- owner in protected areas, open doors, etc. people.get_fake_player = function(ent) local defer = function(x) return (function() return x end) end return { get_inventory_formspec = defer(""), get_look_dir = defer({x=1,y=0,z=0}), get_look_pitch = defer(0), get_look_yaw = defer(0), get_player_control = defer({jump=false, right=false, left=false, LMB=false, RMB=false, sneak=false, aux1=false, down=false, up=false}), get_player_control_bits = defer(0), get_player_name = defer(ent.owner), is_player = defer(true), set_inventory_formspec = defer(), getpos = defer(ent.object:getpos()), get_hp = defer(ent.object:get_hp()), get_inventory = defer(ent.inventory), get_wielded_item = function() if ent.wielded and ent.inventory:contains_item("main", ent.wielded) then return ItemStack(ent.wielded) end return ItemStack("") end, get_wield_index = defer(), get_wield_list = defer("main"), moveto = defer(), punch = defer(), remove = defer(), right_click = defer(), setpos = defer(), set_hp = defer(), set_properties = defer(), set_wielded_item = defer(false), set_animation = defer(), set_attach = defer(), set_detach = defer(), set_bone_position = defer(), } end