diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/advanced_npc.iml b/.idea/advanced_npc.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/advanced_npc.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index cfb415b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/preferred-vcs.xml b/.idea/preferred-vcs.xml deleted file mode 100644 index 848cfc4..0000000 --- a/.idea/preferred-vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - ApexVCS - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/actions/actions.lua b/actions/actions.lua index 4bd735d..c97c773 100644 --- a/actions/actions.lua +++ b/actions/actions.lua @@ -1,115 +1,232 @@ --- Actions code for Advanced NPC by Zorman2000 +-- Commands API code for Advanced NPC by Zorman2000 --------------------------------------------------------------------------------------- --- Action functionality +-- Commands API functionality --------------------------------------------------------------------------------------- --- The NPCs will be able to perform six fundamental actions that will allow --- for them to perform any other kind of interaction in the world. These --- fundamental actions are: place a node, dig a node, put items on an inventory, --- take items from an inventory, find a node closeby (radius 3) and --- walk a step on specific direction. These actions will be set on an action queue. --- The queue will have the specific steps, in order, for the NPC to be able to do --- something (example, go to a specific place and put a chest there). The --- fundamental actions are added to the action queue to make a complete task for the NPC. +-- +-- Description: +------------------------------------------------------------------------------------------ +-- The Commands API is a Bash Script-like execution environment for Minetest entities. +-- There are the following fundamental constructs: +-- - commands: execute a specific action (e.g. dig a node, search for nodes, etc.) +-- - variables: stores information temporarily (e.g. results of node search) +-- - control statements: if/else and loops +-- +-- All of these is done using the Lua programming language, and a few custom +-- expressions where Lua can't help. +-- +-- The fundamental concept is to use the three constructs together in the form of +-- a script to allow NPCs (and any Minetest entity) to perform complex actions, +-- like walking from one place to the other, operating furnaces, etc. In this sense, +-- this "commands" API can be considered a domain-specific language (DSL), that is +-- defined using Lua language structures. +-- +-- Basic definitions: +------------------------------------------------------------------------------------------ +-- A `variable` is any value that can be accessed using a specific key, or name. +-- In the context of the commands API, there is an `execution` context where +-- variables can be stored into, read and deleted from. The execution context +-- is nothing but a map of key-value pairs, with the key being the variable name. +-- Some rules regarding variables: +-- - A variable can be read-write or read-only. Read-only variables cannot be +-- updated, but can be deleted. +-- - A variable cannot be overwritten by another variable of the same name. +-- - The scope of variables is global *within* a script. The execution context +-- is cleared after a script finishes executing. For more info about scripts, +-- see below. +-- - The Lua entity variables, referring to any `self.*` value isn't available +-- as a variable. This is to keep the NPC integrity from a security perspective. +-- However, as some values are very useful and often needed (such as +-- `self.object:getpos()`), some variables are exposed. These are referred to +-- as *internal* variables. They are read-only. +-- +-- A 'command' is a Lua function, with only two parameters: `self`, the Lua entity, +-- and `args`, a Lua table containing all arguments required for the command. The +-- control statements (if/else, loop) and variable set/read are defined as commands +-- as well. The arguments are not strictly controlled, with the following exceptions: +-- - A `variable expression string` is special string that allows a variable +-- to be passed as an argument to a command. The reason why a function can't +-- be used for this is because the execution context, where variables are +-- stored, lives in the entity itself (the `self` object), to which there's no +-- access when a script is defined as a Lua array. +-- The special string has a specific format: ":" where the +-- accepted values for `` are: +-- - `var`, referring to a variable from the execution context, and, +-- - `ivar`, referring to an internal variable (an exposed self.* variable) +-- - A `function expression table` is a Lua table that contains a executable +-- Lua function and the arguments to be executed. The function is executed +-- at the proper moment when passed as an argument to a command, instead of +-- executing immediately while defining a script. +-- The function expression table has the following format: +-- { +-- func: , +-- args: +-- } +-- - A `boolean expression table` is a Lua table that is reconstructed into a +-- Lua boolean expression. The reason for this to exist is similar to the +-- above explanation, and is that, at the moment a script is defined as a +-- Lua array, any function or variable passed as a boolean expression will +-- evaluate, making the value effectively a constant. That would render +-- loops and if/else statements useless. +-- The boolean expression table has the following format: +-- { +-- left_side: , +-- operator: , +-- right_side: , +-- } +-- `operator` and `right_side` are optional: a single function in `left_side` is +-- enough as long as it evaluates to a `boolean` value. The `operator` argument +-- accepts the following values: +-- - `equals` +-- - `not_equals` +-- - `greater_than` +-- - `greater_than_equals` +-- - `less_than` +-- - `less_than_equals` +-- `right_side` is required if `operator` is defined. +-- +-- A `script` is an ordered sequence of commands to be executed, and is defined +-- using a Lua array, where each element is a Lua function corresponding to a +-- command. Scripts are intended to be implemented by users of the API, and as +-- such it is possible to register a script for other mods to use. For example, +-- a script can be used by a mod that creates a music player node so that NPCs +-- can also be able to use it. +-- Scripts can also be executed at certain times during a Minetest day thanks +-- to the schedules functionality. +-- +-- Execution: +------------------------------------------------------------------------------------------ +-- The execution of commands is performed on a timer basis, per NPC, with a default +-- interval value of one second. This interval can be changed by a command itself, +-- however it is the recommended interval is one second to avoid lag caused by many NPCs +-- executing commands. +-- Commands has to be enqueued in order to execute. Enqueuing commands directly isn't +-- recommended, and instead it should be done through a script. Nonetheless, the API +-- for enqueuing commands and scripts is the following: +-- - npc.enqueue_command(command_name, args) +-- - npc.enqueue_script(script_name, args) +-- +-- The control statement commands (if/else, loops) and variable set/read commands +-- will execute the next command in queue immediately after finishing instead of +-- waiting for the timer interval. +-- +-- There is an `execution context` which contains all the variables that are defined +-- using the variable set commands. Also, it contains values specific to the loops, +-- like the number of times it has executed. The execution context lives in the NPC +-- `self` object, and therefore, has to be carefully used, or otherwise it can create +-- huge memory usage. In order to avoid this, variables can be deleted from the execution +-- context using a specific command (`npc.commands.del_var(key)`). Also, as basic +-- memory management routine, the `execution context` is cleared after the end of +-- executing a script. +-- To keep global variables, use the npc.command.get/set_flag() API which is not +-- deleted after execution. +------------------------------------------------------------------------------------------ -npc.actions = {} +npc.commands = {} +--local registered_commands = {} -npc.actions.default_interval = 1 +npc.commands.default_interval = 1 -npc.actions.dir_data = { - -- North - [0] = { - yaw = 0, - vel = {x=0, y=0, z=1} - }, - -- East - [1] = { - yaw = (3 * math.pi) / 2, - vel = {x=1, y=0, z=0} - }, - -- South - [2] = { - yaw = math.pi, - vel = {x=0, y=0, z=-1} - }, - -- West - [3] = { - yaw = math.pi / 2, - vel = {x=-1, y=0, z=0} - }, - -- North east - [4] = { - yaw = (7 * math.pi) / 4, - vel = {x=1, y=0, z=1} - }, - -- North west - [5] = { - yaw = math.pi / 4, - vel = {x=-1, y=0, z=1} - }, - -- South east - [6] = { - yaw = (5 * math.pi) / 4, - vel = {x=1, y=0, z=-1} - }, - -- South west - [7] = { - yaw = (3 * math.pi) / 4, - vel = {x=-1, y=0, z=-1} - } +npc.commands.dir_data = { + -- North + [0] = { + yaw = 0, + vel = {x=0, y=0, z=1} + }, + -- East + [1] = { + yaw = (3 * math.pi) / 2, + vel = {x=1, y=0, z=0} + }, + -- South + [2] = { + yaw = math.pi, + vel = {x=0, y=0, z=-1} + }, + -- West + [3] = { + yaw = math.pi / 2, + vel = {x=-1, y=0, z=0} + }, + -- North east + [4] = { + yaw = (7 * math.pi) / 4, + vel = {x=1, y=0, z=1} + }, + -- North west + [5] = { + yaw = math.pi / 4, + vel = {x=-1, y=0, z=1} + }, + -- South east + [6] = { + yaw = (5 * math.pi) / 4, + vel = {x=1, y=0, z=-1} + }, + -- South west + [7] = { + yaw = (3 * math.pi) / 4, + vel = {x=-1, y=0, z=-1} + } } --- Describes actions with doors or openable nodes -npc.actions.const = { - doors = { - action = { - OPEN = 1, - CLOSE = 2 - }, - state = { - OPEN = 1, - CLOSED = 2 - } - }, - beds = { - LAY = 1, - GET_UP = 2 - }, - sittable = { - SIT = 1, - GET_UP = 2 - } +-- Describes commands with doors or openable nodes +npc.commands.const = { + doors = { + command = { + OPEN = 1, + CLOSE = 2 + }, + state = { + OPEN = 1, + CLOSED = 2 + } + }, + beds = { + LAY = 1, + GET_UP = 2 + }, + sittable = { + SIT = 1, + GET_UP = 2 + } } -npc.actions.cmd = { - SET_INTERVAL = 0, - FREEZE = 1, - ROTATE = 2, - WALK_STEP = 3, - STAND = 4, - SIT = 5, - LAY = 6, - PUT_ITEM = 7, - TAKE_ITEM = 8, - CHECK_ITEM = 9, - USE_OPENABLE = 10, - USE_FURNACE = 11, - USE_BED = 12, - USE_SITTABLE = 13, - WALK_TO_POS = 14, - DIG = 15, - PLACE = 16 +npc.commands.internal_values = { + POS = "self_pos", + -- Note: The following is by mobs_redo. + STANDING_IN = "node_standing_in" } ---npc.actions.one_nps_speed = 0.98 ---npc.actions.one_half_nps_speed = 1.40 ---npc.actions.two_nps_speed = 1.90' -npc.actions.one_nps_speed = 1 -npc.actions.one_half_nps_speed = 1.5 -npc.actions.two_nps_speed = 2 +npc.commands.cmd = { + SET_INTERVAL = 0, + FREEZE = 1, + ROTATE = 2, + WALK_STEP = 3, + STAND = 4, + SIT = 5, + LAY = 6, + PUT_ITEM = 7, + TAKE_ITEM = 8, + CHECK_ITEM = 9, + USE_OPENABLE = 10, + USE_FURNACE = 11, + USE_BED = 12, + USE_SITTABLE = 13, + WALK_TO_POS = 14, + DIG = 15, + PLACE = 16 +} -npc.actions.take_from_inventory = "take_from_inventory" -npc.actions.take_from_inventory_forced = "take_from_inventory_forced" -npc.actions.force_place = "force_place" +--npc.commands.one_nps_speed = 0.98 +--npc.commands.one_half_nps_speed = 1.40 +--npc.commands.two_nps_speed = 1.90' +npc.commands.one_nps_speed = 1 +npc.commands.one_half_nps_speed = 1.5 +npc.commands.two_nps_speed = 2 + +npc.commands.take_from_inventory = "take_from_inventory" +npc.commands.take_from_inventory_forced = "take_from_inventory_forced" +npc.commands.force_place = "force_place" -------------- -- Executor -- @@ -120,168 +237,416 @@ npc.actions.force_place = "force_place" -- Using constants to refer to each method of this API and a function that -- understands those constants and executes the proper function is the way to avoid -- this frequent crashes. -function npc.actions.execute(self, command, args) - if command == npc.actions.cmd.SET_INTERVAL then - -- - return npc.actions.set_interval(self, args) - elseif command == npc.actions.cmd.FREEZE then - -- - return npc.actions.freeze(self, args) - elseif command == npc.actions.cmd.ROTATE then - -- - return npc.actions.rotate(self, args) - elseif command == npc.actions.cmd.WALK_STEP then - -- - return npc.actions.walk_step(self, args) - elseif command == npc.actions.cmd.STAND then - -- - return npc.actions.stand(self, args) - elseif command == npc.actions.cmd.SIT then - -- - return npc.actions.sit(self, args) - elseif command == npc.actions.cmd.LAY then - -- - return npc.actions.lay(self, args) - elseif command == npc.actions.cmd.PUT_ITEM then - -- - return npc.actions.put_item_on_external_inventory(self, args) - elseif command == npc.actions.cmd.TAKE_ITEM then - -- - return npc.actions.take_item_from_external_inventory(self, args) - elseif command == npc.actions.cmd.CHECK_ITEM then - -- - return npc.actions.check_external_inventory_contains_item(self, args) - elseif command == npc.actions.cmd.USE_OPENABLE then - -- - return npc.actions.use_openable(self, args) - elseif command == npc.actions.cmd.USE_FURNACE then - -- - return npc.actions.use_furnace(self, args) - elseif command == npc.actions.cmd.USE_BED then - -- - return npc.actions.use_bed(self, args) - elseif command == npc.actions.cmd.USE_SITTABLE then - -- Call use sittable task - return npc.actions.use_sittable(self, args) - elseif command == npc.actions.cmd.WALK_TO_POS then - -- Call walk to position task - --minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args)) - return npc.actions.walk_to_pos(self, args) - elseif command == npc.actions.cmd.DIG then - -- Call dig node action - return npc.actions.dig(self, args) - elseif command == npc.actions.cmd.PLACE then - -- Call place node action - return npc.actions.place(self, args) - end +function npc.commands.execute(self, command, args) + if command == npc.commands.cmd.SET_INTERVAL then + -- + return npc.commands.set_interval(self, args) + elseif command == npc.commands.cmd.FREEZE then + -- + return npc.commands.freeze(self, args) + elseif command == npc.commands.cmd.ROTATE then + -- + return npc.commands.rotate(self, args) + elseif command == npc.commands.cmd.WALK_STEP then + -- + return npc.commands.walk_step(self, args) + elseif command == npc.commands.cmd.STAND then + -- + return npc.commands.stand(self, args) + elseif command == npc.commands.cmd.SIT then + -- + return npc.commands.sit(self, args) + elseif command == npc.commands.cmd.LAY then + -- + return npc.commands.lay(self, args) + elseif command == npc.commands.cmd.PUT_ITEM then + -- + return npc.commands.put_item_on_external_inventory(self, args) + elseif command == npc.commands.cmd.TAKE_ITEM then + -- + return npc.commands.take_item_from_external_inventory(self, args) + elseif command == npc.commands.cmd.CHECK_ITEM then + -- + return npc.commands.check_external_inventory_contains_item(self, args) + elseif command == npc.commands.cmd.USE_OPENABLE then + -- + return npc.commands.use_openable(self, args) + elseif command == npc.commands.cmd.USE_FURNACE then + -- + return npc.commands.use_furnace(self, args) + elseif command == npc.commands.cmd.USE_BED then + -- + return npc.commands.use_bed(self, args) + elseif command == npc.commands.cmd.USE_SITTABLE then + -- Call use sittable task + return npc.commands.use_sittable(self, args) + elseif command == npc.commands.cmd.WALK_TO_POS then + -- Call walk to position task + --minetest.log("Self: "..dump(self)..", Command: "..dump(command)..", args: "..dump(args)) + return npc.commands.walk_to_pos(self, args) + elseif command == npc.commands.cmd.DIG then + -- Call dig node command + return npc.commands.dig(self, args) + elseif command == npc.commands.cmd.PLACE then + -- Call place node command + return npc.commands.place(self, args) + end end --- TODO: Thanks to executor function, all the functions for Actions and Tasks +-- TODO: Thanks to executor function, all the functions for Commands and Tasks -- should be made into private API + --------------------------------------------------------------------------------------- --- Actions +-- Commands --------------------------------------------------------------------------------------- --- The following action alters the timer interval for executing actions, therefore --- making waits and pauses possible, or increase timing when some actions want to +-- Expression Helper functions +-- These functions provides validation and evaluation logic for the three custom +-- arguments supported: +-- - variable expression string +-- - function expression table +-- - boolean expression table +npc.commands.expr = {} + +npc.commands.expr.boolean_operators = { + EQUALS = "equals", + NOT_EQUALS = "not_equals", + GREATER_THAN = "greater_than", + GREATER_THAN_EQUALS = "greater_than_equals", + LESS_THAN = "less_than", + LESS_THAN_EQUALS = "less_than_equals" +} + +local function get_variable_arg(arg) + if type(arg) == "string" then + local var_exp = string.split(arg, ":") + if var_exp[1] == "var" or var_exp[1] == "ivar" then + return arg, true + end + end + return arg, false +end + +local function evaluate_variable_arg(self, var_arg) + local var_exp = string.split(var_arg, ":") + if var_exp[1] == "var" then + return npc.commands.get_var(self, {key=var_exp[2]}) + elseif var_exp[1] == "ivar" then + return npc.commands.get_internal_var(self, {key=var_exp[2]}) + end +end + +local function get_function_arg(arg) + if type(arg) == "table" then + -- Check if this is a function expression table. If it + -- is and is well defined, return it. + if arg.func and arg.args then + return arg, true + end + end + return arg, false +end + +local function evaluate_function_arg(func_arg) + return func_arg.func(unpack(func_arg.args)) +end + +local function get_boolean_arg(arg) + if type(arg) == "table" then + if (arg.left_side and not(arg.operator and arg.right_side)) + or (arg.left_side and arg.operator and arg.right_side) then + return arg, true + end + return arg, false + end +end + +local function evaluate_boolean_arg(bool_arg) + local left_side = npc.commands.expr.evaluate_argument(bool_arg.left_side) + local right_side, operator + if bool_arg.right_side then + right_side = npc.commands.expr.evaluate_argument(bool_arg.right_side) + operator = bool_arg.operator + end + if right_side then + if operator == npc.commands.expr.boolean_operators.EQUALS then + return left_side == right_side + elseif operator == npc.commands.expr.boolean_operators.NOT_EQUALS then + return left_side ~= right_side + elseif operator == npc.commands.expr.boolean_operators.GREATER_THAN then + return left_side > right_side + elseif operator == npc.commands.expr.boolean_operators.GREATER_THAN_EQUALS then + return left_side >= right_side + elseif operator == npc.commands.expr.boolean_operators.LESS_THAN then + return left_side < right_side + elseif operator == npc.commands.expr.boolean_operators.LESS_THAN_EQUALS then + return left_side <= right_side + end + else + return left_side + end +end + +-- This function identifies the type of argument we are dealing with. +function npc.commands.expr.get_argument_type(arg) + local _, check = get_function_arg(arg) + if check then + return "function_expression" + else + _, check = get_variable_arg(arg) + if check then + return "variable_expression" + else + _, check = get_boolean_arg(arg) + if check then + return "boolean_expression" + end + end + end + return type(arg) +end + +function npc.commands.expr.evaluate_argument(arg) + local argument_type = npc.commands.expr.get_argument_type(arg) + if argument_type == "function_expression" then + return evaluate_function_arg(arg) + elseif argument_type == "variable_expression" then + return evaluate_variable_arg(arg) + elseif argument_type == "boolean_expression" then + return evaluate_boolean_arg(arg) + else + return arg + end +end + +-------------------------- +-- Declarative commands -- +-------------------------- +-- These commands declare, assign and fetch variable values + +-- This command sets the value of a variable in the execution context. +-- If the variable doesn't exists, then it creates the variable and +-- sets its value. +-- Arguments: +-- - key: variable name +-- - value: variable value +-- Returns: Nothing +function npc.commands.set_var(self, args) + if args.key then + local result = npc.execution_context.get(self, args.key) + if result then + npc.execution_context.set(self, args.key, args.value) + else + npc.execution_context.put(self, args.key, args.value, false) + end + end +end + +-- This command returns the value of a variable in the execution context. +-- If the variable doesn't exists, returns nil. +-- Arguments: +-- - key: variable name +-- Returns: variable value if found, nil otherwise +function npc.commands.get_var(self, args) + if args.key then + return npc.execution_context.get(self, args.key) + end +end + +-- This command returns the value of an internal NPC variable. +-- These variables are the self.* variables, limited for security +-- purposes. The list of retrievable values is defined in +-- npc.commands.internal_values.* +-- Arguments: +-- - key: internal value as specified in npc.commands.internal_values.* +-- Returns: internal value +function npc.commands.get_internal_var(self, args) + local key = args.key + if key then + if key == npc.commands.internal_values.POS then + return self.object:getpos() + elseif key == npc.commands.internal_values.STANDING_IN then + return self.standing_in + end + end +end + +-- This command deletes a variable from the execution context. +-- If the deletion is successful, it returns the value of the deleted +-- variable. If not, it returns nil +-- Arguments: +-- - key: key-name of the variable to be deleted +function npc.commands.del_var(self, args) + local key = args.key + if key then + return npc.execution_context.remove(self, key) + end +end + +----------------------- +-- Control commands -- +----------------------- +-- The following command alters the timer interval for executing commands, therefore +-- making waits and pauses possible, or increase timing when some commands want to -- be performed faster, like walking. -function npc.actions.set_interval(self, args) - local self_actions = args.self_actions - local new_interval = args.interval - local freeze_mobs_api = args.freeze +function npc.commands.set_interval(self, args) + local self_actions = args.self_actions + local new_interval = args.interval + local freeze_mobs_api = args.freeze - self.actions.action_interval = new_interval - return not freeze_mobs_api + self.commands.action_interval = new_interval + return not freeze_mobs_api end --- The following action is for allowing the rest of mobs redo API to be executed --- after this action ends. This is useful for times when no action is needed +-- The following command is for allowing the rest of mobs redo API to be executed +-- after this command ends. This is useful for times when no command is needed -- and the NPC is allowed to roam freely. -function npc.actions.freeze(self, args) - local freeze_mobs_api = args.freeze - local disable_rightclick = args.disable_rightclick - if disable_rightclick ~= nil then - npc.log("INFO", "Enabling interactions for NPC "..self.npc_name..": "..dump(not(disable_rightclick))) - self.enable_rightclick_interaction = not(disable_rightclick) - end --- minetest.log("Received: "..dump(freeze_mobs_api)) --- minetest.log("Returning: "..dump(not(freeze_mobs_api))) - return not(freeze_mobs_api) +function npc.commands.freeze(self, args) + local freeze_mobs_api = args.freeze + local disable_rightclick = args.disable_rightclick + if disable_rightclick ~= nil then + npc.log("INFO", "Enabling interactions for NPC "..self.npc_name..": "..dump(not(disable_rightclick))) + self.enable_rightclick_interaction = not(disable_rightclick) + end + + return not(freeze_mobs_api) end --- This action digs the node at the given position +-- This command allows the conditional execution of two array of commands +-- depending on the evaluation of a certain condition. This is the typical +-- if-else statement of a programming language. If-else can be nested. +-- Arguments: +-- - `condition`: accepts two values: +-- - `boolean`: Lua boolean expression, any expression that evaluates to `true` or `false`. +-- - `table`: A boolean expression table. +-- - `true_commands`: an array of commands to be executed when the condition +-- evaluates to `true` +-- - `false_commands`: an array of commands to be executed when the condition +-- evaluates to `false` +function npc.commands.if_else(self, args) + +end + +-- This command works as the types of loops, depending on the arguments +-- given. It can work as a while, a for and a for-each loop. Loops can +-- be nested. +-- While-loop arguments: +-- - `name`: string, a key-name for this loop. Default is nil. If given, +-- it gives access to the number of times the loop has executed. +-- - `condition`: boolean, the loop will be executed as long as this condition +-- evaluates to `true` +-- - `commands`: array, array of commands to be executed during the loop +-- +-- For-loop arguments: +-- - `name`: string, a key-name for this loop. Default is `nil`. If given, it +-- gives access to the number of times this loop has executed. +-- - `initial_value`: integer, the starting value of the for-loop. If left +-- blank, default value is `1`. +-- - `condition`: boolean, the loop will be executed as long as this condition +-- evaluates to `true`. +-- - `modifier`: function, the loop will execute this modifier at the end of +-- every iteration. If left blank, default is: initial_value + 1 +-- - `commands`: array, array of commands to be executed during the loop +-- +-- Both of these loops store how many times they have been executed. To +-- access it, it is required to give pass the argument `name`. Then the +-- value will be stored on the execution context and the value retrievable +-- with `npc.commands.get_context(key)`, where `key` is the `name` argument. +-- +-- For-each-loop arguments: +-- - `name`: string, a key-name for this loop. Default is `nil`. If given, it +-- gives access to the number of times this loop has executed and the current +-- value of the array/table being evaluated. +-- - `iterable`: array or table of key-value pairs, this is an iterable array +-- or table for which the loop will execute commands at every element in the +-- iterable array/table. +-- - `commands`: array, array of commands to be executed during the loop +-- To get the current element being iterated in a for-each loop, you need to define +-- the `name` argument. Then, the value will be stored in the execution context and +-- will be retrievable with `npc.commands.get_context(key)`. It will return a table +-- like this: {loop_count = x, current_value = y} +function npc.commands.loop(self, args) + +end + + + +-------------------------- +-- Interaction commands -- +-------------------------- +-- This command digs the node at the given position -- If 'add_to_inventory' is true, it will put the digged node in the NPC -- inventory. -- Returns true if dig is successful, otherwise false -function npc.actions.dig(self, args) - local pos = args.pos - local add_to_inventory = args.add_to_inventory - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node = minetest.get_node_or_nil(pos) - if node then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) +function npc.commands.dig(self, args) + local pos = args.pos + local add_to_inventory = args.add_to_inventory + local bypass_protection = args.bypass_protection + local play_sound = args.play_sound or true + local node = minetest.get_node_or_nil(pos) + if node then + -- Set mine animation + self.object:set_animation({ + x = npc.ANIMATION_MINE_START, + y = npc.ANIMATION_MINE_END}, + self.animation.speed_normal, 0) - -- Play dig sound - if play_sound == true then - minetest.sound_play( - minetest.registered_nodes[node.name].sounds.dug, - { - max_hear_distance = 10, - object = self.object - } - ) - end + -- Play dig sound + if play_sound == true then + minetest.sound_play( + minetest.registered_nodes[node.name].sounds.dug, + { + max_hear_distance = 10, + object = self.object + } + ) + end - -- Check if protection not enforced - if not bypass_protection then - -- Try to dig node - if minetest.dig_node(pos) then - -- Add to inventory the node drops - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - --return true - return - end - else - -- Add to inventory - if add_to_inventory then - -- Get node drop - local drop = minetest.registered_nodes[node.name].drop - local drop_itemname = node.name - if drop and drop.items then - local random_item = drop.items[math.random(1, #drop.items)] - if random_item then - drop_itemname = random_item.items[1] - end - end - -- Add to NPC inventory - npc.add_item_to_inventory(self, drop_itemname, 1) - end - -- Dig node - minetest.set_node(pos, {name="air"}) - end - end - --return false + -- Check if protection not enforced + if not bypass_protection then + -- Try to dig node + if minetest.dig_node(pos) then + -- Add to inventory the node drops + if add_to_inventory then + -- Get node drop + local drop = minetest.registered_nodes[node.name].drop + local drop_itemname = node.name + if drop and drop.items then + local random_item = drop.items[math.random(1, #drop.items)] + if random_item then + drop_itemname = random_item.items[1] + end + end + -- Add to NPC inventory + npc.add_item_to_inventory(self, drop_itemname, 1) + end + --return true + return + end + else + -- Add to inventory + if add_to_inventory then + -- Get node drop + local drop = minetest.registered_nodes[node.name].drop + local drop_itemname = node.name + if drop and drop.items then + local random_item = drop.items[math.random(1, #drop.items)] + if random_item then + drop_itemname = random_item.items[1] + end + end + -- Add to NPC inventory + npc.add_item_to_inventory(self, drop_itemname, 1) + end + -- Dig node + minetest.set_node(pos, {name="air"}) + end + end + --return false end --- This action places a given node at the given position +-- This command places a given node at the given position -- There are three ways to source the node: -- 1. take_from_inventory: takes node from inventory. If not in inventory, -- node isn't placed. @@ -289,458 +654,489 @@ end -- inventory, node will be placed anyways. -- 3. force_place: places node regardless of inventory - will not touch -- the NPCs inventory -function npc.actions.place(self, args) - local pos = args.pos - local node = args.node - local source = args.source - local bypass_protection = args.bypass_protection - local play_sound = args.play_sound or true - local node_at_pos = minetest.get_node_or_nil(pos) - -- Check if position is empty or has a node that can be built to - if node_at_pos and - (node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then - -- Check protection - if (not bypass_protection and not minetest.is_protected(pos, self.npc_name)) - or bypass_protection == true then - -- Take from inventory if necessary - local place_item = false - if source == npc.actions.take_from_inventory then - if npc.take_item_from_inventory(self, node, 1) then - place_item = true - end - elseif source == npc.actions.take_from_inventory_forced then - npc.take_item_from_inventory(self, node, 1) - place_item = true - elseif source == npc.actions.force_place then - place_item = true - end - -- Place node - if place_item == true then - -- Set mine animation - self.object:set_animation({ - x = npc.ANIMATION_MINE_START, - y = npc.ANIMATION_MINE_END}, - self.animation.speed_normal, 0) - -- Place node - minetest.set_node(pos, {name=node}) - -- Play place sound - if play_sound == true then - minetest.sound_play( - minetest.registered_nodes[node].sounds.place, - { - max_hear_distance = 10, - object = self.object - } - ) - end - end - end - end +function npc.commands.place(self, args) + local pos = args.pos + local node = args.node + local source = args.source + local bypass_protection = args.bypass_protection + local play_sound = args.play_sound or true + local node_at_pos = minetest.get_node_or_nil(pos) + -- Check if position is empty or has a node that can be built to + if node_at_pos and + (node_at_pos.name == "air" or minetest.registered_nodes[node_at_pos.name].buildable_to == true) then + -- Check protection + if (not bypass_protection and not minetest.is_protected(pos, self.npc_name)) + or bypass_protection == true then + -- Take from inventory if necessary + local place_item = false + if source == npc.commands.take_from_inventory then + if npc.take_item_from_inventory(self, node, 1) then + place_item = true + end + elseif source == npc.commands.take_from_inventory_forced then + npc.take_item_from_inventory(self, node, 1) + place_item = true + elseif source == npc.commands.force_place then + place_item = true + end + -- Place node + if place_item == true then + -- Set mine animation + self.object:set_animation({ + x = npc.ANIMATION_MINE_START, + y = npc.ANIMATION_MINE_END}, + self.animation.speed_normal, 0) + -- Place node + minetest.set_node(pos, {name=node}) + -- Play place sound + if play_sound == true then + minetest.sound_play( + minetest.registered_nodes[node].sounds.place, + { + max_hear_distance = 10, + object = self.object + } + ) + end + end + end + end end +-- This function allows to query for nodes and entities within a radius. +-- Parameters: +-- - query_type: string, specifies whether to query nodes or entities. +-- Default value is "node". Accepted values are: +-- - "node" +-- - "entity" +-- - position: table or string, specifies the starting position +-- for query. This should be the center of a square box. If +-- given a String, the string should be the place type. +-- Two types of tables are accepted: +-- - A simple position table, {x=1, y=1, z=1} +-- - An improved position table in this format: +-- { +-- place_category = "", +-- place_type = "", +-- index = 1, (specific index in the places map) +-- use_access_node = false|true, +-- } +-- - radius: integer, specifies the radius of the square box to search +-- around the starting position. +-- - result_type: string, specifies how to return results. Accepted +-- values are: +-- - "first": Get the first result found (default if left blank), +-- - "nearest": Get the result nearest to the NPC, +-- - "all": Return array of all results +function npc.commands.query(self, args) + +end + + + -- This function allows to move into directions that are walkable. It -- avoids fences and allows to move on plants. -- This will make for nice wanderings, making the NPC move smartly instead -- of just getting stuck at places local function random_dir_helper(start_pos, speed, dir_start, dir_end) - -- Limit the number of tries - otherwise it could become an infinite loop - for i = 1, 8 do - local dir = math.random(dir_start, dir_end) - local vel = vector.multiply(npc.actions.dir_data[dir].vel, speed) - local pos = vector.add(start_pos, vel) - local node = minetest.get_node(pos) - if node then - if node.name == "air" - -- Any walkable node except fences - or (minetest.registered_nodes[node.name].walkable == true - and minetest.registered_nodes[node.name].groups.fence ~= 1) - -- Farming plants - or minetest.registered_nodes[node.name].groups.plant == 1 then - return dir - end - end - end - -- Return -1 signaling that no good direction could be found - return -1 + -- Limit the number of tries - otherwise it could become an infinite loop + for i = 1, 8 do + local dir = math.random(dir_start, dir_end) + local vel = vector.multiply(npc.commands.dir_data[dir].vel, speed) + local pos = vector.add(start_pos, vel) + local node = minetest.get_node(pos) + if node then + if node.name == "air" + -- Any walkable node except fences + or (minetest.registered_nodes[node.name].walkable == true + and minetest.registered_nodes[node.name].groups.fence ~= 1) + -- Farming plants + or minetest.registered_nodes[node.name].groups.plant == 1 then + return dir + end + end + end + -- Return -1 signaling that no good direction could be found + return -1 end --- This action is to rotate to mob to a specifc direction. Currently, the code +-- This command is to rotate to mob to a specifc direction. Currently, the code -- contains also for diagonals, but remaining in the orthogonal domain is preferrable. -function npc.actions.rotate(self, args) - local dir = args.dir - local yaw = args.yaw or 0 - local start_pos = args.start_pos - local end_pos = args.end_pos - -- Calculate dir if positions are given - if start_pos and end_pos and not dir then - dir = npc.actions.get_direction(start_pos, end_pos) - end - -- Only yaw was given - if yaw and not dir and not start_pos and not end_pos then - self.object:setyaw(yaw) - return - end +function npc.commands.rotate(self, args) + local dir = args.dir + local yaw = args.yaw or 0 + local start_pos = args.start_pos + local end_pos = args.end_pos + -- Calculate dir if positions are given + if start_pos and end_pos and not dir then + dir = npc.commands.get_direction(start_pos, end_pos) + end + -- Only yaw was given + if yaw and not dir and not start_pos and not end_pos then + self.object:setyaw(yaw) + return + end - self.rotate = 0 - if dir == npc.direction.north then - yaw = 0 - elseif dir == npc.direction.north_east then - yaw = (7 * math.pi) / 4 - elseif dir == npc.direction.east then - yaw = (3 * math.pi) / 2 - elseif dir == npc.direction.south_east then - yaw = (5 * math.pi) / 4 - elseif dir == npc.direction.south then - yaw = math.pi - elseif dir == npc.direction.south_west then - yaw = (3 * math.pi) / 4 - elseif dir == npc.direction.west then - yaw = math.pi / 2 - elseif dir == npc.direction.north_west then - yaw = math.pi / 4 - end - self.object:setyaw(yaw) + self.rotate = 0 + if dir == npc.direction.north then + yaw = 0 + elseif dir == npc.direction.north_east then + yaw = (7 * math.pi) / 4 + elseif dir == npc.direction.east then + yaw = (3 * math.pi) / 2 + elseif dir == npc.direction.south_east then + yaw = (5 * math.pi) / 4 + elseif dir == npc.direction.south then + yaw = math.pi + elseif dir == npc.direction.south_west then + yaw = (3 * math.pi) / 4 + elseif dir == npc.direction.west then + yaw = math.pi / 2 + elseif dir == npc.direction.north_west then + yaw = math.pi / 4 + end + self.object:setyaw(yaw) end --- This function will make the NPC walk one step on a --- specifc direction. One step means one node. It returns +-- This function will make the NPC walk one step on a +-- specifc direction. One step means one node. It returns -- true if it can move on that direction, and false if there is an obstacle -function npc.actions.walk_step(self, args) - local dir = args.dir - local step_into_air_only = args.step_into_air_only - local speed = args.speed - local target_pos = args.target_pos - local start_pos = args.start_pos - local vel = {} +function npc.commands.walk_step(self, args) + local dir = args.dir + local step_into_air_only = args.step_into_air_only + local speed = args.speed + local target_pos = args.target_pos + local start_pos = args.start_pos + local vel = {} - -- Set default node per seconds - if speed == nil then - speed = npc.actions.one_nps_speed - end + -- Set default node per seconds + if speed == nil then + speed = npc.commands.one_nps_speed + end - -- Check if dir should be random - if dir == "random_all" or dir == "random" then - dir = random_dir_helper(start_pos, speed, 0, 7) - end - if dir == "random_orthogonal" then - dir = random_dir_helper(start_pos, speed, 0, 3) - end + -- Check if dir should be random + if dir == "random_all" or dir == "random" then + dir = random_dir_helper(start_pos, speed, 0, 7) + end + if dir == "random_orthogonal" then + dir = random_dir_helper(start_pos, speed, 0, 3) + end - if dir == npc.direction.north then - vel = {x=0, y=0, z=speed} - elseif dir == npc.direction.north_east then - vel = {x=speed, y=0, z=speed} - elseif dir == npc.direction.east then - vel = {x=speed, y=0, z=0} - elseif dir == npc.direction.south_east then - vel = {x=speed, y=0, z=-speed} - elseif dir == npc.direction.south then - vel = {x=0, y=0, z=-speed} - elseif dir == npc.direction.south_west then - vel = {x=-speed, y=0, z=-speed} - elseif dir == npc.direction.west then - vel = {x=-speed, y=0, z=0} - elseif dir == npc.direction.north_west then - vel = {x=-speed, y=0, z=speed } - else - -- No direction provided or NPC is trapped, don't move NPC - vel = {x=0, y=0, z=0} - end + if dir == npc.direction.north then + vel = {x=0, y=0, z=speed} + elseif dir == npc.direction.north_east then + vel = {x=speed, y=0, z=speed} + elseif dir == npc.direction.east then + vel = {x=speed, y=0, z=0} + elseif dir == npc.direction.south_east then + vel = {x=speed, y=0, z=-speed} + elseif dir == npc.direction.south then + vel = {x=0, y=0, z=-speed} + elseif dir == npc.direction.south_west then + vel = {x=-speed, y=0, z=-speed} + elseif dir == npc.direction.west then + vel = {x=-speed, y=0, z=0} + elseif dir == npc.direction.north_west then + vel = {x=-speed, y=0, z=speed } + else + -- No direction provided or NPC is trapped, don't move NPC + vel = {x=0, y=0, z=0} + end - -- If there is a target position to reach, set it and set walking to true - if target_pos ~= nil then - self.actions.walking.target_pos = target_pos - -- Set is_walking = true - self.actions.walking.is_walking = true - end + -- If there is a target position to reach, set it and set walking to true + if target_pos ~= nil then + self.commands.walking.target_pos = target_pos + -- Set is_walking = true + self.commands.walking.is_walking = true + end - -- Rotate NPC - npc.actions.rotate(self, {dir=dir}) - -- Set velocity so that NPC walks - self.object:setvelocity(vel) - -- Set walk animation - self.object:set_animation({ - x = npc.ANIMATION_WALK_START, - y = npc.ANIMATION_WALK_END}, - self.animation.speed_normal, 0) + -- Rotate NPC + npc.commands.rotate(self, {dir=dir}) + -- Set velocity so that NPC walks + self.object:setvelocity(vel) + -- Set walk animation + self.object:set_animation({ + x = npc.ANIMATION_WALK_START, + y = npc.ANIMATION_WALK_END}, + self.animation.speed_normal, 0) end --- This action makes the NPC stand and remain like that -function npc.actions.stand(self, args) - local pos = args.pos - local dir = args.dir - -- Set is_walking = false - self.actions.walking.is_walking = false - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil then - npc.actions.rotate(self, {dir=dir}) - end - -- Set stand animation - self.object:set_animation({ - x = npc.ANIMATION_STAND_START, - y = npc.ANIMATION_STAND_END}, - self.animation.speed_normal, 0) +-- This command makes the NPC stand and remain like that +function npc.commands.stand(self, args) + local pos = args.pos + local dir = args.dir + -- Set is_walking = false + self.commands.walking.is_walking = false + -- Stop NPC + self.object:setvelocity({x=0, y=0, z=0}) + -- If position given, set to that position + if pos ~= nil then + self.object:moveto(pos) + end + -- If dir given, set to that dir + if dir ~= nil then + npc.commands.rotate(self, {dir=dir}) + end + -- Set stand animation + self.object:set_animation({ + x = npc.ANIMATION_STAND_START, + y = npc.ANIMATION_STAND_END}, + self.animation.speed_normal, 0) end --- This action makes the NPC sit on the node where it is -function npc.actions.sit(self, args) - local pos = args.pos - local dir = args.dir - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position given, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- If dir given, set to that dir - if dir ~= nil then - npc.actions.rotate(self, {dir=dir}) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_SIT_START, - y = npc.ANIMATION_SIT_END}, - self.animation.speed_normal, 0) +-- This command makes the NPC sit on the node where it is +function npc.commands.sit(self, args) + local pos = args.pos + local dir = args.dir + -- Stop NPC + self.object:setvelocity({x=0, y=0, z=0}) + -- If position given, set to that position + if pos ~= nil then + self.object:moveto(pos) + end + -- If dir given, set to that dir + if dir ~= nil then + npc.commands.rotate(self, {dir=dir}) + end + -- Set sit animation + self.object:set_animation({ + x = npc.ANIMATION_SIT_START, + y = npc.ANIMATION_SIT_END}, + self.animation.speed_normal, 0) end --- This action makes the NPC lay on the node where it is -function npc.actions.lay(self, args) - local pos = args.pos - -- Stop NPC - self.object:setvelocity({x=0, y=0, z=0}) - -- If position give, set to that position - if pos ~= nil then - self.object:moveto(pos) - end - -- Set sit animation - self.object:set_animation({ - x = npc.ANIMATION_LAY_START, - y = npc.ANIMATION_LAY_END}, - self.animation.speed_normal, 0) +-- This command makes the NPC lay on the node where it is +function npc.commands.lay(self, args) + local pos = args.pos + -- Stop NPC + self.object:setvelocity({x=0, y=0, z=0}) + -- If position give, set to that position + if pos ~= nil then + self.object:moveto(pos) + end + -- Set sit animation + self.object:set_animation({ + x = npc.ANIMATION_LAY_START, + y = npc.ANIMATION_LAY_END}, + self.animation.speed_normal, 0) end -- Inventory functions for players and for nodes -- This function is a convenience function to make it easy to put --- and get items from another inventory (be it a player inv or +-- and get items from another inventory (be it a player inv or -- a node inv) -function npc.actions.put_item_on_external_inventory(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local is_furnace = args.is_furnace - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end +function npc.commands.put_item_on_external_inventory(self, args) + local player = args.player + local pos = args.pos + local inv_list = args.inv_list + local item_name = args.item_name + local count = args.count + local is_furnace = args.is_furnace + local inv + if player ~= nil then + inv = minetest.get_inventory({type="player", name=player}) + else + inv = minetest.get_inventory({type="node", pos=pos}) + end - -- Create ItemStack to put on external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough room to add the item on external invenotry - if inv:room_for_item(inv_list, item) then - -- Take item from NPC's inventory - if npc.take_item_from_inventory_itemstring(self, item) then - -- NPC doesn't have item and/or specified quantity - return false - end - -- Add items to external inventory - inv:add_item(inv_list, item) + -- Create ItemStack to put on external inventory + local item = ItemStack(item_name.." "..count) + -- Check if there is enough room to add the item on external invenotry + if inv:room_for_item(inv_list, item) then + -- Take item from NPC's inventory + if npc.take_item_from_inventory_itemstring(self, item) then + -- NPC doesn't have item and/or specified quantity + return false + end + -- Add items to external inventory + inv:add_item(inv_list, item) - -- If this is a furnace, start furnace timer - if is_furnace == true then - minetest.get_node_timer(pos):start(1.0) - end + -- If this is a furnace, start furnace timer + if is_furnace == true then + minetest.get_node_timer(pos):start(1.0) + end - return true - end - -- Not able to put on external inventory - return false + return true + end + -- Not able to put on external inventory + return false end -function npc.actions.take_item_from_external_inventory(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end - -- Create ItemStack to take from external inventory - local item = ItemStack(item_name.." "..count) - -- Check if there is enough of the item to take - if inv:contains_item(inv_list, item) then - -- Add item to NPC's inventory - npc.add_item_to_inventory_itemstring(self, item) - -- Add items to external inventory - inv:remove_item(inv_list, item) - return true - end - -- Not able to put on external inventory - return false +function npc.commands.take_item_from_external_inventory(self, args) + local player = args.player + local pos = args.pos + local inv_list = args.inv_list + local item_name = args.item_name + local count = args.count + local inv + if player ~= nil then + inv = minetest.get_inventory({type="player", name=player}) + else + inv = minetest.get_inventory({type="node", pos=pos}) + end + -- Create ItemStack to take from external inventory + local item = ItemStack(item_name.." "..count) + -- Check if there is enough of the item to take + if inv:contains_item(inv_list, item) then + -- Add item to NPC's inventory + npc.add_item_to_inventory_itemstring(self, item) + -- Add items to external inventory + inv:remove_item(inv_list, item) + return true + end + -- Not able to put on external inventory + return false end -function npc.actions.check_external_inventory_contains_item(self, args) - local player = args.player - local pos = args.pos - local inv_list = args.inv_list - local item_name = args.item_name - local count = args.count - local inv - if player ~= nil then - inv = minetest.get_inventory({type="player", name=player}) - else - inv = minetest.get_inventory({type="node", pos=pos}) - end +function npc.commands.check_external_inventory_contains_item(self, args) + local player = args.player + local pos = args.pos + local inv_list = args.inv_list + local item_name = args.item_name + local count = args.count + local inv + if player ~= nil then + inv = minetest.get_inventory({type="player", name=player}) + else + inv = minetest.get_inventory({type="node", pos=pos}) + end - -- Create ItemStack for checking the external inventory - local item = ItemStack(item_name.." "..count) - -- Check if inventory contains item - return inv:contains_item(inv_list, item) + -- Create ItemStack for checking the external inventory + local item = ItemStack(item_name.." "..count) + -- Check if inventory contains item + return inv:contains_item(inv_list, item) end -- TODO: Refactor this function so that it uses a table to check -- for doors instead of having separate logic for each door type -function npc.actions.get_openable_node_state(node, pos, npc_dir) - --minetest.log("Node name: "..dump(node.name)) - local state = npc.actions.const.doors.state.CLOSED - -- Check for MTG doors and gates - local mtg_door_closed = false - if minetest.get_item_group(node.name, "door") > 0 then - local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2)) - local back_node = minetest.get_node(back_pos) - if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then - mtg_door_closed = true - end - end - -- Check for cottages gates - local open_i1, open_i2 = string.find(node.name, "_close") - -- Check for cottages half door - local half_door_is_closed = false - if node.name == "cottages:half_door" then - half_door_is_closed = (node.param2 + 2) % 4 == npc_dir - end - if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then - state = npc.actions.const.doors.state.OPEN - end - --minetest.log("Door state: "..dump(state)) - return state +function npc.commands.get_openable_node_state(node, pos, npc_dir) + --minetest.log("Node name: "..dump(node.name)) + local state = npc.commands.const.doors.state.CLOSED + -- Check for MTG doors and gates + local mtg_door_closed = false + if minetest.get_item_group(node.name, "door") > 0 then + local back_pos = vector.add(pos, minetest.facedir_to_dir(node.param2)) + local back_node = minetest.get_node(back_pos) + if back_node.name == "air" or minetest.registered_nodes[back_node.name].walkable == false then + mtg_door_closed = true + end + end + -- Check for cottages gates + local open_i1, open_i2 = string.find(node.name, "_close") + -- Check for cottages half door + local half_door_is_closed = false + if node.name == "cottages:half_door" then + half_door_is_closed = (node.param2 + 2) % 4 == npc_dir + end + if mtg_door_closed == false and open_i1 == nil and half_door_is_closed == false then + state = npc.commands.const.doors.state.OPEN + end + --minetest.log("Door state: "..dump(state)) + return state end -- This function is used to open or close openable nodes. -- Currently supported openable nodes are: any doors using the --- default doors API, and the cottages mod gates and doors. -function npc.actions.use_openable(self, args) - local pos = args.pos - local action = args.action - local dir = args.dir - local node = minetest.get_node(pos) - local state = npc.actions.get_openable_node_state(node, pos, dir) +-- default doors API, and the cottages mod gates and doors. +function npc.commands.use_openable(self, args) + local pos = args.pos + local command = args.command + local dir = args.dir + local node = minetest.get_node(pos) + local state = npc.commands.get_openable_node_state(node, pos, dir) - local clicker = self.object - if action ~= state then - minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil) - end + local clicker = self.object + if command ~= state then + minetest.registered_nodes[node.name].on_rightclick(pos, node, clicker, nil, nil) + end end --------------------------------------------------------------------------------------- -- Tasks functionality --------------------------------------------------------------------------------------- --- Tasks are operations that require many actions to perform. Basic tasks, like +-- Tasks are operations that require many commands to perform. Basic tasks, like -- walking from one place to another, operating a furnace, storing or taking -- items from a chest, are provided here. local function get_pos_argument(self, pos, use_access_node) - --minetest.log("Type of pos: "..dump(type(pos))) - -- Check which type of position argument we received - if type(pos) == "table" then - --minetest.log("Received table pos: "..dump(pos)) - -- Check if table is position - if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then - -- Position received, return position - return pos - elseif pos.place_type ~= nil then - -- Received table in the following format: - -- { - -- place_category = "", - -- place_type = "", - -- index = 1, - -- use_access_node = false|true, - -- try_alternative_if_used = true|false - -- } - local index = pos.index or 1 - local use_access_node = pos.use_access_node or false - local try_alternative_if_used = pos.try_alternative_if_used or false - local places = npc.places.get_by_type(self, pos.place_type) - --minetest.log("Place type: "..dump(pos.place_type)) - --minetest.log("Places: "..dump(places)) - -- Check index is valid on the places map - if #places >= index then - local place = places[index] - -- Check if place is used, and if it is, find alternative if required - if try_alternative_if_used == true then - place = npc.places.find_unused_place(self, pos.place_category, pos.place_type, place) + --minetest.log("Type of pos: "..dump(type(pos))) + -- Check which type of position argument we received + if type(pos) == "table" then + --minetest.log("Received table pos: "..dump(pos)) + -- Check if table is position + if pos.x ~= nil and pos.y ~= nil and pos.z ~= nil then + -- Position received, return position + return pos + elseif pos.place_type ~= nil then + -- Received table in the following format: + -- { + -- place_category = "", + -- place_type = "", + -- index = 1, + -- use_access_node = false|true, + -- try_alternative_if_used = true|false + -- } + local index = pos.index or 1 + local use_access_node = pos.use_access_node or false + local try_alternative_if_used = pos.try_alternative_if_used or false + local places = npc.places.get_by_type(self, pos.place_type) + --minetest.log("Place type: "..dump(pos.place_type)) + --minetest.log("Places: "..dump(places)) + -- Check index is valid on the places map + if #places >= index then + local place = places[index] + -- Check if place is used, and if it is, find alternative if required + if try_alternative_if_used == true then + place = npc.places.find_unused_place(self, pos.place_category, pos.place_type, place) - --minetest.log("Mark as used? "..dump(pos.mark_target_as_used)) - if pos.mark_target_as_used == true then - --minetest.log("Marking as used: "..minetest.pos_to_string(place.pos)) - npc.places.mark_place_used(place.pos, npc.places.USE_STATE.USED) - end + --minetest.log("Mark as used? "..dump(pos.mark_target_as_used)) + if pos.mark_target_as_used == true then + --minetest.log("Marking as used: "..minetest.pos_to_string(place.pos)) + npc.places.mark_place_used(place.pos, npc.places.USE_STATE.USED) + end - npc.places.add_shared_accessible_place( - self, {owner="", node_pos=place.pos}, npc.places.PLACE_TYPE.CALCULATED.TARGET, true, {}) - end - -- Check if access node is desired - if use_access_node == true then - -- Return actual node pos - return place.access_node, place.pos - else - -- Return node pos that allows access to node - return place.pos - end - end - end - elseif type(pos) == "string" then - -- Received name of place, so we are going to look for the actual pos - local places_pos = npc.places.get_by_type(self, pos, false) - -- Return nil if no position found - if places_pos == nil or #places_pos == 0 then - return nil - end - -- Check if received more than one position - if #places_pos > 1 then - -- Check all places, return owned if existent, else return the first one - for i = 1, #places_pos do - if places_pos[i].status == "owned" then - if use_access_node == true then - return places_pos[i].access_node, places_pos[i].pos - else - return places_pos[i].pos - end - end - end - end - -- Return the first position only if it couldn't find an owned - -- place, or if it there is only one - if use_access_node == true then - return places_pos[1].access_node, places_pos[1].pos - else - return places_pos[1].pos - end - end + npc.places.add_shared_accessible_place( + self, {owner="", node_pos=place.pos}, npc.places.PLACE_TYPE.CALCULATED.TARGET, true, {}) + end + -- Check if access node is desired + if use_access_node == true then + -- Return actual node pos + return place.access_node, place.pos + else + -- Return node pos that allows access to node + return place.pos + end + end + end + elseif type(pos) == "string" then + -- Received name of place, so we are going to look for the actual pos + local places_pos = npc.places.get_by_type(self, pos, false) + -- Return nil if no position found + if places_pos == nil or #places_pos == 0 then + return nil + end + -- Check if received more than one position + if #places_pos > 1 then + -- Check all places, return owned if existent, else return the first one + for i = 1, #places_pos do + if places_pos[i].status == "owned" then + if use_access_node == true then + return places_pos[i].access_node, places_pos[i].pos + else + return places_pos[i].pos + end + end + end + end + -- Return the first position only if it couldn't find an owned + -- place, or if it there is only one + if use_access_node == true then + return places_pos[1].access_node, places_pos[1].pos + else + return places_pos[1].pos + end + end end -- This function allows a NPC to use a furnace using only items from @@ -748,313 +1144,313 @@ end -- with the fuel items the NPC will take whatever was cooked and whatever -- remained to cook. The function received the position of the furnace -- to use, and the item to cook in furnace. Item is an itemstring -function npc.actions.use_furnace(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) - return - end +function npc.commands.use_furnace(self, args) + local pos = get_pos_argument(self, args.pos) + if pos == nil then + npc.log("WARNING", "Got nil position in 'use_furnace' using args.pos: "..dump(args.pos)) + return + end - local enable_usage_marking = args.enable_usage_marking or true - local item = args.item - local freeze = args.freeze - -- Define which items are usable as fuels. The NPC - -- will mainly use this as fuels to avoid getting useful - -- items (such as coal lumps) for burning - local fuels = {"default:leaves", - "default:pine_needles", - "default:tree", - "default:acacia_tree", - "default:aspen_tree", - "default:jungletree", - "default:pine_tree", - "default:coalblock", - "farming:straw"} + local enable_usage_marking = args.enable_usage_marking or true + local item = args.item + local freeze = args.freeze + -- Define which items are usable as fuels. The NPC + -- will mainly use this as fuels to avoid getting useful + -- items (such as coal lumps) for burning + local fuels = {"default:leaves", + "default:pine_needles", + "default:tree", + "default:acacia_tree", + "default:aspen_tree", + "default:jungletree", + "default:pine_tree", + "default:coalblock", + "farming:straw"} - -- Check if NPC has item to cook - local src_item = npc.inventory_contains(self, npc.get_item_name(item)) - if src_item == nil then - -- Unable to cook item that is not in inventory - return false - end + -- Check if NPC has item to cook + local src_item = npc.inventory_contains(self, npc.get_item_name(item)) + if src_item == nil then + -- Unable to cook item that is not in inventory + return false + end - -- Check if NPC has a fuel item - for i = 1,9 do - local fuel_item = npc.inventory_contains(self, fuels[i]) + -- Check if NPC has a fuel item + for i = 1,9 do + local fuel_item = npc.inventory_contains(self, fuels[i]) - if fuel_item ~= nil then - -- Get fuel item's burn time - local fuel_time = - minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time - local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string) - npc.log("DEBUG", "Fuel time: "..dump(fuel_time)) + if fuel_item ~= nil then + -- Get fuel item's burn time + local fuel_time = + minetest.get_craft_result({method="fuel", width=1, items={ItemStack(fuel_item.item_string)}}).time + local total_fuel_time = fuel_time * npc.get_item_count(fuel_item.item_string) + npc.log("DEBUG", "Fuel time: "..dump(fuel_time)) - -- Get item to cook's cooking time - local cook_result = - minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}}) - local total_cook_time = cook_result.time * npc.get_item_count(item) - npc.log("DEBUG", "Cook: "..dump(cook_result)) + -- Get item to cook's cooking time + local cook_result = + minetest.get_craft_result({method="cooking", width=1, items={ItemStack(src_item.item_string)}}) + local total_cook_time = cook_result.time * npc.get_item_count(item) + npc.log("DEBUG", "Cook: "..dump(cook_result)) - npc.log("DEBUG", "Total cook time: "..total_cook_time - ..", total fuel burn time: "..dump(total_fuel_time)) + npc.log("DEBUG", "Total cook time: "..total_cook_time + ..", total fuel burn time: "..dump(total_fuel_time)) - -- Check if there is enough fuel to cook all items - if total_cook_time > total_fuel_time then - -- Don't have enough fuel to cook item. Return the difference - -- so it may help on trying to acquire the fuel later. - -- NOTE: Yes, returning here means that NPC could probably have other - -- items usable as fuels and ignore them. This should be ok for now, - -- considering that fuel items are ordered in a way where cheaper, less - -- useless items come first, saving possible valuable items. - return cook_result.time - fuel_time - end + -- Check if there is enough fuel to cook all items + if total_cook_time > total_fuel_time then + -- Don't have enough fuel to cook item. Return the difference + -- so it may help on trying to acquire the fuel later. + -- NOTE: Yes, returning here means that NPC could probably have other + -- items usable as fuels and ignore them. This should be ok for now, + -- considering that fuel items are ordered in a way where cheaper, less + -- useless items come first, saving possible valuable items. + return cook_result.time - fuel_time + end - -- Set furnace as used if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) - end + -- Set furnace as used if flag is enabled + if enable_usage_marking then + -- Set place as used + npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) + end - -- Calculate how much fuel is needed - local fuel_amount = total_cook_time / fuel_time - if fuel_amount < 1 then - fuel_amount = 1 - end + -- Calculate how much fuel is needed + local fuel_amount = total_cook_time / fuel_time + if fuel_amount < 1 then + fuel_amount = 1 + end - npc.log("DEBUG", "Amount of fuel needed: "..fuel_amount) + npc.log("DEBUG", "Amount of fuel needed: "..fuel_amount) - -- Put this item on the fuel inventory list of the furnace - local args = { - player = nil, - pos = pos, - inv_list = "fuel", - item_name = npc.get_item_name(fuel_item.item_string), - count = fuel_amount - } - npc.add_action(self, npc.actions.cmd.PUT_ITEM, args) - -- Put the item that we want to cook on the furnace - args = { - player = nil, - pos = pos, - inv_list = "src", - item_name = npc.get_item_name(src_item.item_string), - count = npc.get_item_count(item), - is_furnace = true - } - npc.add_action(self, npc.actions.cmd.PUT_ITEM, args) + -- Put this item on the fuel inventory list of the furnace + local args = { + player = nil, + pos = pos, + inv_list = "fuel", + item_name = npc.get_item_name(fuel_item.item_string), + count = fuel_amount + } + npc.add_action(self, npc.commands.cmd.PUT_ITEM, args) + -- Put the item that we want to cook on the furnace + args = { + player = nil, + pos = pos, + inv_list = "src", + item_name = npc.get_item_name(src_item.item_string), + count = npc.get_item_count(item), + is_furnace = true + } + npc.add_action(self, npc.commands.cmd.PUT_ITEM, args) - -- Now, set NPC to wait until furnace is done. - npc.log("DEBUG", "Setting wait action for "..dump(total_cook_time)) - npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=total_cook_time, freeze=freeze}) + -- Now, set NPC to wait until furnace is done. + npc.log("DEBUG", "Setting wait command for "..dump(total_cook_time)) + npc.add_action(self, npc.commands.cmd.SET_INTERVAL, {interval=total_cook_time, freeze=freeze}) - -- Reset timer - npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=1, freeze=true}) + -- Reset timer + npc.add_action(self, npc.commands.cmd.SET_INTERVAL, {interval=1, freeze=true}) - -- If freeze is false, then we will have to find the way back to the furnace - -- once cooking is done. - if freeze == false then - npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos)) - npc.add_task(self, npc.actions.cmd.WALK_TO_POS, {end_pos=pos, walkable={}}) - end + -- If freeze is false, then we will have to find the way back to the furnace + -- once cooking is done. + if freeze == false then + npc.log("DEBUG", "Adding walk to position to wandering: "..dump(pos)) + npc.add_task(self, npc.commands.cmd.WALK_TO_POS, {end_pos=pos, walkable={}}) + end - -- Take cooked items back - args = { + -- Take cooked items back + args = { - player = nil, - pos = pos, - inv_list = "dst", - item_name = cook_result.item:get_name(), - count = npc.get_item_count(item), - is_furnace = false - } - npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos)) - npc.add_action(self, npc.actions.cmd.TAKE_ITEM, args) + player = nil, + pos = pos, + inv_list = "dst", + item_name = cook_result.item:get_name(), + count = npc.get_item_count(item), + is_furnace = false + } + npc.log("DEBUG", "Taking item back: "..minetest.pos_to_string(pos)) + npc.add_action(self, npc.commands.cmd.TAKE_ITEM, args) - npc.log("DEBUG", "Inventory: "..dump(self.inventory)) + npc.log("DEBUG", "Inventory: "..dump(self.inventory)) - -- Set furnace as unused if flag is enabled - if enable_usage_marking then - -- Set place as used - npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) - end + -- Set furnace as unused if flag is enabled + if enable_usage_marking then + -- Set place as used + npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) + end - return true - end - end - -- Couldn't use the furnace due to lack of items - return false + return true + end + end + -- Couldn't use the furnace due to lack of items + return false end -- This function makes the NPC lay or stand up from a bed. The --- pos is the location of the bed, action can be lay or get up -function npc.actions.use_bed(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) - return - end - local action = args.action - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) - --minetest.log(dump(node)) - local dir = minetest.facedir_to_dir(node.param2) +-- pos is the location of the bed, command can be lay or get up +function npc.commands.use_bed(self, args) + local pos = get_pos_argument(self, args.pos) + if pos == nil then + npc.log("WARNING", "Got nil position in 'use_bed' using args.pos: "..dump(args.pos)) + return + end + local command = args.command + local enable_usage_marking = args.enable_usage_marking or true + local node = minetest.get_node(pos) + --minetest.log(dump(node)) + local dir = minetest.facedir_to_dir(node.param2) - if action == npc.actions.const.beds.LAY then - -- Get position - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir) - -- Sit down on bed, rotate to correct direction - npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4}) - -- Lay down - npc.add_action(self, npc.actions.cmd.LAY, {}) - if enable_usage_marking then - -- Set place as used - npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) - end - self.actions.move_state.is_laying = true - else - -- Calculate position to get up - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local bed_pos_y = npc.actions.nodes.beds[node.name].get_lay_pos(pos, dir).y - local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z} - -- Sit up - npc.add_action(self, npc.actions.cmd.SIT, {pos=bed_pos}) - -- Initialize direction: Default is front of bottom of bed - local dir = (node.param2 + 2) % 4 - -- Find empty node around node - -- Take into account that mats are close to the floor, so y adjustmen is zero - local y_adjustment = -1 - if npc.actions.nodes.beds[node.name].type == "mat" then - y_adjustment = 0 - end + if command == npc.commands.const.beds.LAY then + -- Get position + -- Error here due to ignore. Need to come up with better solution + if node.name == "ignore" then + return + end + local bed_pos = npc.commands.nodes.beds[node.name].get_lay_pos(pos, dir) + -- Sit down on bed, rotate to correct direction + npc.add_action(self, npc.commands.cmd.SIT, {pos=bed_pos, dir=(node.param2 + 2) % 4}) + -- Lay down + npc.add_action(self, npc.commands.cmd.LAY, {}) + if enable_usage_marking then + -- Set place as used + npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) + end + self.commands.move_state.is_laying = true + else + -- Calculate position to get up + -- Error here due to ignore. Need to come up with better solution + if node.name == "ignore" then + return + end + local bed_pos_y = npc.commands.nodes.beds[node.name].get_lay_pos(pos, dir).y + local bed_pos = {x = pos.x, y = bed_pos_y, z = pos.z} + -- Sit up + npc.add_action(self, npc.commands.cmd.SIT, {pos=bed_pos}) + -- Initialize direction: Default is front of bottom of bed + local dir = (node.param2 + 2) % 4 + -- Find empty node around node + -- Take into account that mats are close to the floor, so y adjustmen is zero + local y_adjustment = -1 + if npc.commands.nodes.beds[node.name].type == "mat" then + y_adjustment = 0 + end - local pos_out_of_bed = pos - local empty_nodes = npc.places.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment) - if empty_nodes ~= nil and #empty_nodes > 0 then - -- Get direction to the empty node - dir = npc.actions.get_direction(bed_pos, empty_nodes[1].pos) + local pos_out_of_bed = pos + local empty_nodes = npc.places.find_node_orthogonally(bed_pos, {"air", "cottages:bench"}, y_adjustment) + if empty_nodes ~= nil and #empty_nodes > 0 then + -- Get direction to the empty node + dir = npc.commands.get_direction(bed_pos, empty_nodes[1].pos) - -- Calculate position to get out of bed - pos_out_of_bed = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - -- Account for benches if they are present to avoid standing over them - if empty_nodes[1].name == "cottages:bench" then - pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - if empty_nodes[1].param2 == 0 then - pos_out_of_bed.z = pos_out_of_bed.z - 0.3 - elseif empty_nodes[1].param2 == 1 then - pos_out_of_bed.x = pos_out_of_bed.x - 0.3 - elseif empty_nodes[1].param2 == 2 then - pos_out_of_bed.z = pos_out_of_bed.z + 0.3 - elseif empty_nodes[1].param2 == 3 then - pos_out_of_bed.x = pos_out_of_bed.x + 0.3 - end - end + -- Calculate position to get out of bed + pos_out_of_bed = + {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} + -- Account for benches if they are present to avoid standing over them + if empty_nodes[1].name == "cottages:bench" then + pos_out_of_bed = {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} + if empty_nodes[1].param2 == 0 then + pos_out_of_bed.z = pos_out_of_bed.z - 0.3 + elseif empty_nodes[1].param2 == 1 then + pos_out_of_bed.x = pos_out_of_bed.x - 0.3 + elseif empty_nodes[1].param2 == 2 then + pos_out_of_bed.z = pos_out_of_bed.z + 0.3 + elseif empty_nodes[1].param2 == 3 then + pos_out_of_bed.x = pos_out_of_bed.x + 0.3 + end + end - end - -- Stand out of bed - npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_bed, dir=dir}) - if enable_usage_marking then - -- Set place as unused - npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) - end - self.actions.move_state.is_laying = false - end + end + -- Stand out of bed + npc.add_action(self, npc.commands.cmd.STAND, {pos=pos_out_of_bed, dir=dir}) + if enable_usage_marking then + -- Set place as unused + npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) + end + self.commands.move_state.is_laying = false + end end -- This function makes the NPC lay or stand up from a bed. The --- pos is the location of the bed, action can be lay or get up -function npc.actions.use_sittable(self, args) - local pos = get_pos_argument(self, args.pos) - if pos == nil then - npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) - return - end - local action = args.action - local enable_usage_marking = args.enable_usage_marking or true - local node = minetest.get_node(pos) +-- pos is the location of the bed, command can be lay or get up +function npc.commands.use_sittable(self, args) + local pos = get_pos_argument(self, args.pos) + if pos == nil then + npc.log("WARNING", "Got nil position in 'use_sittable' using args.pos: "..dump(args.pos)) + return + end + local command = args.command + local enable_usage_marking = args.enable_usage_marking or true + local node = minetest.get_node(pos) - if action == npc.actions.const.sittable.SIT then - -- Calculate position depending on bench - -- Error here due to ignore. Need to come up with better solution - if node.name == "ignore" then - return - end - local sit_pos = npc.actions.nodes.sittable[node.name].get_sit_pos(pos, node.param2) - -- Sit down on bench/chair/stairs - npc.add_action(self, npc.actions.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4}) - if enable_usage_marking then - -- Set place as used - npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) - end - self.actions.move_state.is_sitting = true - else - if self.actions.move_state.is_sitting == false then - npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.") - return - end - -- Find empty areas around chair - local dir = node.param2 + 2 % 4 - -- Default it to the current position in case it can't find empty - -- position around sittable node. Weird - local pos_out_of_sittable = pos - local empty_nodes = npc.places.find_node_orthogonally(pos, {"air"}, 0) - if empty_nodes ~= nil and #empty_nodes > 0 then - --minetest.log("Empty nodes: "..dump(empty_nodes)) - --minetest.log("Npc.actions.get_direction: "..dump(npc.actions.get_direction)) - --minetest.log("Pos: "..dump(pos)) - -- Get direction to the empty node - dir = npc.actions.get_direction(pos, empty_nodes[1].pos) - -- Calculate position to get out of sittable node - pos_out_of_sittable = - {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} - end - -- Stand - npc.add_action(self, npc.actions.cmd.STAND, {pos=pos_out_of_sittable, dir=dir}) - minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used") - if enable_usage_marking then - -- Set place as unused - npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) - end - self.actions.move_state.is_sitting = false - end + if command == npc.commands.const.sittable.SIT then + -- Calculate position depending on bench + -- Error here due to ignore. Need to come up with better solution + if node.name == "ignore" then + return + end + local sit_pos = npc.commands.nodes.sittable[node.name].get_sit_pos(pos, node.param2) + -- Sit down on bench/chair/stairs + npc.add_action(self, npc.commands.cmd.SIT, {pos=sit_pos, dir=(node.param2 + 2) % 4}) + if enable_usage_marking then + -- Set place as used + npc.places.mark_place_used(pos, npc.places.USE_STATE.USED) + end + self.commands.move_state.is_sitting = true + else + if self.commands.move_state.is_sitting == false then + npc.log("DEBUG_ACTION", "NPC "..self.npc_name.." attempted to get up from sit when it is not sitting.") + return + end + -- Find empty areas around chair + local dir = node.param2 + 2 % 4 + -- Default it to the current position in case it can't find empty + -- position around sittable node. Weird + local pos_out_of_sittable = pos + local empty_nodes = npc.places.find_node_orthogonally(pos, {"air"}, 0) + if empty_nodes ~= nil and #empty_nodes > 0 then + --minetest.log("Empty nodes: "..dump(empty_nodes)) + --minetest.log("Npc.commands.get_direction: "..dump(npc.commands.get_direction)) + --minetest.log("Pos: "..dump(pos)) + -- Get direction to the empty node + dir = npc.commands.get_direction(pos, empty_nodes[1].pos) + -- Calculate position to get out of sittable node + pos_out_of_sittable = + {x=empty_nodes[1].pos.x, y=empty_nodes[1].pos.y + 1, z=empty_nodes[1].pos.z} + end + -- Stand + npc.add_action(self, npc.commands.cmd.STAND, {pos=pos_out_of_sittable, dir=dir}) + minetest.log("Setting sittable at "..minetest.pos_to_string(pos).." as not used") + if enable_usage_marking then + -- Set place as unused + npc.places.mark_place_used(pos, npc.places.USE_STATE.NOT_USED) + end + self.commands.move_state.is_sitting = false + end end -- This function returns the direction enum -- for the moving from v1 to v2 -function npc.actions.get_direction(v1, v2) - local vector_dir = vector.direction(v1, v2) - local dir = vector.round(vector_dir) +function npc.commands.get_direction(v1, v2) + local vector_dir = vector.direction(v1, v2) + local dir = vector.round(vector_dir) - if dir.x ~= 0 and dir.z ~= 0 then - if dir.x > 0 and dir.z > 0 then - return npc.direction.north_east - elseif dir.x > 0 and dir.z < 0 then - return npc.direction.south_east - elseif dir.x < 0 and dir.z > 0 then - return npc.direction.north_west - elseif dir.x < 0 and dir.z < 0 then - return npc.direction.south_west - end - elseif dir.x ~= 0 and dir.z == 0 then - if dir.x > 0 then - return npc.direction.east - else - return npc.direction.west - end - elseif dir.z ~= 0 and dir.x == 0 then - if dir.z > 0 then - return npc.direction.north - else - return npc.direction.south - end - end + if dir.x ~= 0 and dir.z ~= 0 then + if dir.x > 0 and dir.z > 0 then + return npc.direction.north_east + elseif dir.x > 0 and dir.z < 0 then + return npc.direction.south_east + elseif dir.x < 0 and dir.z > 0 then + return npc.direction.north_west + elseif dir.x < 0 and dir.z < 0 then + return npc.direction.south_west + end + elseif dir.x ~= 0 and dir.z == 0 then + if dir.x > 0 then + return npc.direction.east + else + return npc.direction.west + end + elseif dir.z ~= 0 and dir.x == 0 then + if dir.z > 0 then + return npc.direction.north + else + return npc.direction.south + end + end end @@ -1063,133 +1459,133 @@ end -- is included, which is a table of node names, these nodes are -- going to be considered walkable for the algorithm to find a -- path. -function npc.actions.walk_to_pos(self, args) - -- Get arguments for this task - local use_access_node = args.use_access_node or true - local end_pos, node_pos = get_pos_argument(self, args.end_pos, use_access_node) - if end_pos == nil then - npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) - return - end - local enforce_move = args.enforce_move or true - local walkable_nodes = args.walkable +function npc.commands.walk_to_pos(self, args) + -- Get arguments for this task + local use_access_node = args.use_access_node or true + local end_pos, node_pos = get_pos_argument(self, args.end_pos, use_access_node) + if end_pos == nil then + npc.log("WARNING", "Got nil position in 'walk_to_pos' using args.pos: "..dump(args.end_pos)) + return + end + local enforce_move = args.enforce_move or true + local walkable_nodes = args.walkable - -- Round start_pos to make sure it can find start and end - local start_pos = vector.round(self.object:getpos()) - npc.log("DEBUG", "walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos)) - npc.log("DEBUG", "walk_to_pos: End pos: "..minetest.pos_to_string(end_pos)) + -- Round start_pos to make sure it can find start and end + local start_pos = vector.round(self.object:getpos()) + npc.log("DEBUG", "walk_to_pos: Start pos: "..minetest.pos_to_string(start_pos)) + npc.log("DEBUG", "walk_to_pos: End pos: "..minetest.pos_to_string(end_pos)) - -- Check if start_pos and end_pos are the same - if vector.equals(start_pos, end_pos) == true then - -- Check if it was using access node, if it was, add action to - -- rotate NPC into that direction - if use_access_node == true then - local dir = npc.actions.get_direction(end_pos, node_pos) - npc.add_action(self, npc.actions.cmd.STAND, {dir = dir}) - end - npc.log("WARNING", "walk_to_pos Found start_pos == end_pos") - return - end + -- Check if start_pos and end_pos are the same + if vector.equals(start_pos, end_pos) == true then + -- Check if it was using access node, if it was, add command to + -- rotate NPC into that direction + if use_access_node == true then + local dir = npc.commands.get_direction(end_pos, node_pos) + npc.add_action(self, npc.commands.cmd.STAND, {dir = dir}) + end + npc.log("WARNING", "walk_to_pos Found start_pos == end_pos") + return + end - -- Set walkable nodes to empty if the parameter hasn't been used - if walkable_nodes == nil then - walkable_nodes = {} - end + -- Set walkable nodes to empty if the parameter hasn't been used + if walkable_nodes == nil then + walkable_nodes = {} + end - -- Find path - local path = npc.pathfinder.find_path(start_pos, end_pos, self, true) + -- Find path + local path = npc.pathfinder.find_path(start_pos, end_pos, self, true) - if path ~= nil and #path > 1 then - npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) - -- Store path - self.actions.walking.path = path + if path ~= nil and #path > 1 then + npc.log("INFO", "walk_to_pos Found path to node: "..minetest.pos_to_string(end_pos)) + -- Store path + self.commands.walking.path = path - -- Local variables - local door_opened = false - local speed = npc.actions.two_nps_speed + -- Local variables + local door_opened = false + local speed = npc.commands.two_nps_speed - -- Set the action timer interval to half second. This is to account for - -- the increased speed when walking. - npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=0.5, freeze=true}) + -- Set the command timer interval to half second. This is to account for + -- the increased speed when walking. + npc.add_action(self, npc.commands.cmd.SET_INTERVAL, {interval=0.5, freeze=true}) - -- Set the initial last and target positions - self.actions.walking.target_pos = path[1].pos + -- Set the initial last and target positions + self.commands.walking.target_pos = path[1].pos - -- Add steps to path - for i = 1, #path do - -- Do not add an extra step if reached the goal node - if (i+1) == #path then - -- Add direction to last node - local dir = npc.actions.get_direction(path[i].pos, end_pos) - -- Add the last step - npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) - -- Add stand animation at end - if use_access_node == true then - dir = npc.actions.get_direction(end_pos, node_pos) + -- Add steps to path + for i = 1, #path do + -- Do not add an extra step if reached the goal node + if (i+1) == #path then + -- Add direction to last node + local dir = npc.commands.get_direction(path[i].pos, end_pos) + -- Add the last step + npc.add_action(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) + -- Add stand animation at end + if use_access_node == true then + dir = npc.commands.get_direction(end_pos, node_pos) end minetest.log("Dir: "..dump(dir)) - -- Change dir if using access_node - npc.add_action(self, npc.actions.cmd.STAND, {dir = dir}) - break - end - -- Get direction to move from path[i] to path[i+1] - local dir = npc.actions.get_direction(path[i].pos, path[i+1].pos) - -- Check if next node is a door, if it is, open it, then walk - if path[i+1].type == npc.pathfinder.node_types.openable then - -- Check if door is already open - local node = minetest.get_node(path[i+1].pos) - if npc.actions.get_openable_node_state(node, path[i+1].pos, dir) == npc.actions.const.doors.state.CLOSED then - --minetest.log("Opening action to open door") - -- Stop to open door, this avoids misplaced movements later on - npc.add_action(self, npc.actions.cmd.STAND, {dir=dir}) - -- Open door - npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, action=npc.actions.const.doors.action.OPEN}) + -- Change dir if using access_node + npc.add_action(self, npc.commands.cmd.STAND, {dir = dir}) + break + end + -- Get direction to move from path[i] to path[i+1] + local dir = npc.commands.get_direction(path[i].pos, path[i+1].pos) + -- Check if next node is a door, if it is, open it, then walk + if path[i+1].type == npc.pathfinder.node_types.openable then + -- Check if door is already open + local node = minetest.get_node(path[i+1].pos) + if npc.commands.get_openable_node_state(node, path[i+1].pos, dir) == npc.commands.const.doors.state.CLOSED then + --minetest.log("Opening command to open door") + -- Stop to open door, this avoids misplaced movements later on + npc.add_action(self, npc.commands.cmd.STAND, {dir=dir}) + -- Open door + npc.add_action(self, npc.commands.cmd.USE_OPENABLE, {pos=path[i+1].pos, dir=dir, command=npc.commands.const.doors.command.OPEN}) - door_opened = true - end + door_opened = true + end - end + end - -- Add walk action to action queue - npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) + -- Add walk command to command queue + npc.add_action(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+1].pos}) - if door_opened then - -- Stop to close door, this avoids misplaced movements later on - -- local x_adj, z_adj = 0, 0 - -- if dir == 0 then - -- z_adj = 0.1 - -- elseif dir == 1 then - -- x_adj = 0.1 - -- elseif dir == 2 then - -- z_adj = -0.1 - -- elseif dir == 3 then - -- x_adj = -0.1 - -- end - -- local pos_on_close = {x=path[i+1].pos.x + x_adj, y=path[i+1].pos.y + 1, z=path[i+1].pos.z + z_adj} - -- Add extra walk step to ensure that one is standing at other side of openable node - -- npc.add_action(self, npc.actions.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos}) - -- Stop to close the door - npc.add_action(self, npc.actions.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close}) - -- Close door - npc.add_action(self, npc.actions.cmd.USE_OPENABLE, {pos=path[i+1].pos, action=npc.actions.const.doors.action.CLOSE}) + if door_opened then + -- Stop to close door, this avoids misplaced movements later on + -- local x_adj, z_adj = 0, 0 + -- if dir == 0 then + -- z_adj = 0.1 + -- elseif dir == 1 then + -- x_adj = 0.1 + -- elseif dir == 2 then + -- z_adj = -0.1 + -- elseif dir == 3 then + -- x_adj = -0.1 + -- end + -- local pos_on_close = {x=path[i+1].pos.x + x_adj, y=path[i+1].pos.y + 1, z=path[i+1].pos.z + z_adj} + -- Add extra walk step to ensure that one is standing at other side of openable node + -- npc.add_action(self, npc.commands.cmd.WALK_STEP, {dir = dir, speed = speed, target_pos = path[i+2].pos}) + -- Stop to close the door + npc.add_action(self, npc.commands.cmd.STAND, {dir=(dir + 2) % 4 })--, pos=pos_on_close}) + -- Close door + npc.add_action(self, npc.commands.cmd.USE_OPENABLE, {pos=path[i+1].pos, command=npc.commands.const.doors.command.CLOSE}) - door_opened = false - end + door_opened = false + end - end + end - -- Return the action interval to default interval of 1 second - -- By default, always freeze. - npc.add_action(self, npc.actions.cmd.SET_INTERVAL, {interval=1, freeze=true}) + -- Return the command interval to default interval of 1 second + -- By default, always freeze. + npc.add_action(self, npc.commands.cmd.SET_INTERVAL, {interval=1, freeze=true}) - else - -- Unable to find path - npc.log("WARNING", "walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos)) - -- Check if movement is enforced - if enforce_move then - -- Move to end pos - self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) - end - end + else + -- Unable to find path + npc.log("WARNING", "walk_to_pos Unable to find path. Teleporting to: "..minetest.pos_to_string(end_pos)) + -- Check if movement is enforced + if enforce_move then + -- Move to end pos + self.object:moveto({x=end_pos.x, y=end_pos.y+1, z=end_pos.z}) + end + end end \ No newline at end of file diff --git a/doc/new_api.md b/doc/new_api.md new file mode 100644 index 0000000..a42fbe7 --- /dev/null +++ b/doc/new_api.md @@ -0,0 +1,90 @@ +Advanced NPC 1.0 proposal +------------------------- + +While Advanced NPC provides functionality and a level of intelligence that no other mob mod can, it is still limited in some features and to its ultimate purpose of creating functional towns and/or simulated communities. The following are the areas that has been identified as lacking: + + - Idle/wandering + - When NPCs aren't executing actions, their movement is very dumb. They wander aimlessly, constantly and usually bump into obstacles and keep walking nevertheless. They get stuck at places they shouldn't. + - Relationships + - Relationships are very hardcoded, and there's no flexibility on them. + - Unable to add more functionality + - All actions are hardcoded. While the essentials are in, making a NPC operate another node that is not a furnace/chest/door is almost impossible. If a mod adds a node and wants NPC to be able to operate it, it is certainly very hard. + - More randomness in schedules + - While schedules are all about making NPCs do actions at certain times, it is not flexible enough to make it look more realistic. One morning a NPC can get up and make breakfast or not, put some music on a music player or not, go outside their home and wander around, etc. + - Unable to react to certain triggers + - When NPCs are punched, `mobs_redo` takes over and controls the NPC. Also, NPCs are unable to scan an area for certain things and perform actions continually based on it. + +The above are all playability issues and deficiencies. Some technical issues has to be addressed as well regarding the API. Given all these, the following is a proposal to move the mod towards the correct direction. + + +##Proposed changes: + - Unify the actions/tasks/schedule property change/schedule query API into a `commands` API + - Add new commands to bring the NPC interaction level closer to that of a player + - Rename `flags` to `properties` + - Allow registering scripts, or collections of commands for external mods to provide extra functionality + + +Unified Commands API +-------------------- + +The goal of this API is to provide consistency and extensibility to the actions a NPC can perform. First of all, rename actions/tasks/property change/query to `commands`. Each command will have the following properties that determines how it is to be executed and what it does: + + - Type: specifies the type of command. The following are valid types: + - `instruction`: Used for fundamental, atomic operations. This type maps directly to what are called now `actions`, which are for example, walk one step, dig, place, etc. + - `control`: Used for specific commands that are flow control statements. Example: If-else, for loops. The conditional statements is a Lua boolean expression. + - `script`: Used for collections of commands, executed on a sequential structure. This type maps directly to `tasks`. + - Execution: specifies how the command is to be executed. The following are valid valuesf for this parameter: + - `immediate`: Will execute this command immediately, without any enqueing. Very little commands should be able to do this. The `control` commands should be executed immediately as they need to enqueue certain commands depending on their conditions. + - `default`: Command will be enqueued and executed on the global command timer call. + - Interruptable: specifies whether the global command timer and/or the scheduler can interrupt the command. Boolean value, can be set to false or true. + - _Important_: Non-interruptable commands should be able to finish by themselves. The API will execute the default command once a non-interruptable command is done and if it doesn't executes another command. + - Parameters: a Lua table with all the parameters that the command requires. Depending on the type, some parameters are required. Below is a list of required parameters per type: + - `instruction`: Requires just the parameters required by the instruction to execute. + - `control`: Requires different parameters depending on the type of control. + - Required: + - `condition`: The condition to be evaluated. This is a Lua boolean expression. + - `match_commands`: A Lua array with the commands to be executed if condition evaluates to `true`. + - Dependent on type: + - `operation`: Only required in `for-loop` command. Operation to execute on the loop variable (e.g. increase/decrease) + - `repetition`: Optional for `for-loop` command. Can't be used together with `max` and `min`. + - `max`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `min`. Randomizes a loop execution and sets the upper bound of how many times the loop will execute. + - `min`: Optional for `for-loop` command. Can't be used together with `repetition`. Requires `max`. Randomizes a loop execution and sets the lower bound of how many times the loop will execute. + - `else_commands`: Only required in `if-else` command. A Lua array with the commands to be executed if condition evaluates to `false`. + - `script`: A Lua array of commands to execute, in order + +The following `instruction` commands will be added to the default set: + - `do_punch`: Executes the `on_punch` function of a node, object or player + - `do_rightclick`: Executes the `on_rightclick` function of a node, object or player + - `set_property`: Sets the value of a variable in the `self.properties` object. If the variable doesn't exists, it is created. This command is executed immediately and is not enqueued. + - Parameters: + - `key`: The property key-name. This is a variable in the `self.properties` object + - `value`: The property value. + - `get_property`: Returns the value of a given property. This command is executed immediately and is not enqueued. + - Parameters: + - `key`: The property key-name. + - `set_internal_property`: Sets the value of a limited set of internal properties related to the NPC trading and personality variables. + - `get_internal_property`: Gets the value of a limited set of internal properties related to the NPC trading and personality variables. + - `add_item_to_npc`: Adds an item to the NPC inventory, without any specific source. + - `remove_item_from_npc`: Removes a specific item from the NPC inventory. + - `query`: Executes a query for nodes or objects. Returns a Lua table with none, single or many positions. + +The following `control` commands will be added to the default set: + - `if-else`: An if-else control statement that will execute immediately. It will evaluate the given `condition` parameter and execute commands depending on the evaluation of the `condition`. + - Parameters: + - `condition`: A Lua boolean expression to be evaluated. + - `true-commands`: A Lua array of commands to be executed if `condition` evaluates to `true`. + - `else-commands`: A Lua array of commands to be executed if `condition` evaluates to `false`. + - `loop`: A flexible loop command. Supports for-loop and while-loops. The amount of loops done will be available in `npc.commands.current_loop_count`. Executes immediately, it is not enqueued. + - Parameters: + + +##Extensibility +Once the above commands has been added, it is possible to safely build scripts which don't touch directly many of the internal NPC mechanisms. An API will be provided for external mods to register scripts that let NPCs perform actions related to those mods, e.g. operating a node provided by the mod. The API for this will be: + +`npc.commands.register_script(name, script)` + +All registered scripts have the following properties: + - They are interruptable by the command queue/scheduler + - They are not immediately executed + +The `script` parameter is a Lua array of commands that will be executed when the script is executed. \ No newline at end of file diff --git a/npc.lua b/npc.lua index 91a6f48..c7f03bc 100755 --- a/npc.lua +++ b/npc.lua @@ -476,6 +476,8 @@ function npc.initialize(entity, pos, is_lua_entity, npc_stats, occupation_name) -- relative to the last position target_pos = {} }, + -- Execution context - map of user-declared variables when executing scripts + execution_context = {} } -- This flag is checked on every step. If it is true, the rest of @@ -886,6 +888,68 @@ function npc.unlock_actions(self) npc.log("DEBUG_ACTION", "Unlocked NPC "..dump(self.npc_id).." actions") end +-------------------------------------------- +-- Execution context management functions -- +-------------------------------------------- +-- These functions manage the execution context, where variables are +-- stored, whether internal (loops) or user-created. +-- The execution context is cleared at the end of each script. +npc.execution_context = {} + +-- This function adds a value to the execution context. +-- Readonly defaults to false. Returns false if failed due to +-- key-name conflict, or returns true if successful +function npc.execution_context.put(self, key, value, readonly) + -- Check if variable exists + if self.actions.execution_context[key] ~= nil then + npc.log("ERROR", "Attempt to create new variable with name "..key.." failed".. + "due to variable already existing: "..dump(self.actions.execution_context[key])) + return false + end + self.actions.execution_context[key] = {value = value, readonly = readonly} + return true +end + +-- Returns the value of a given key. If not found returns nil. +function npc.execution_context.get(self, key) + local result = self.actions.execution_context[key] + if result == nil then + return nil + else + return result.value + end +end + +-- This function updates a value in the execution context. +-- Returns false if the value is read-only or if key isn't found. +-- Returns true if able to update value +function npc.execution_context.set(self, key, new_value) + local var = self.actions.execution_context[key] + if var == nil then + return false + else + if var.readonly == true then + npc.log("ERROR", "Attempt to set value of readonly variable: "..key) + return false + end + var.value = new_value + end + return true +end + +-- This function removes a variable from the execution context. +-- If the key doesn't exist, returns nil, otherwise, returns +-- the value removed. +function npc.execution_context.remove(self, key) + local result = self.actions.execution_context[key] + if result == nil then + return nil + else + self.actions.execution_context[key] = nil + return result + end +end + --------------------------------------------------------------------------------------- -- Schedule functionality ---------------------------------------------------------------------------------------