a-planet-alive/mods/mobs/mobs_mobs/advanced_npc/npc.lua

2354 lines
80 KiB
Lua
Executable File

-- Advanced NPC by Zorman2000
-- Based on original NPC by Tenplus1
local S = mobs.intllib
npc = {}
-- Constants
npc.FEMALE = "female"
npc.MALE = "male"
npc.age = {
adult = "adult",
child = "child"
}
npc.INVENTORY_ITEM_MAX_STACK = 99
npc.ANIMATION_STAND_START = 0
npc.ANIMATION_STAND_END = 79
npc.ANIMATION_SIT_START = 81
npc.ANIMATION_SIT_END = 160
npc.ANIMATION_LAY_START = 162
npc.ANIMATION_LAY_END = 166
npc.ANIMATION_WALK_START = 168
npc.ANIMATION_WALK_END = 187
npc.ANIMATION_MINE_START = 189
npc.ANIMATION_MINE_END =198
npc.direction = {
north = 0,
east = 1,
south = 2,
west = 3,
north_east = 4,
north_west = 5,
south_east = 6,
south_west = 7
}
npc.action_state = {
none = 0,
executing = 1,
interrupted = 2
}
npc.log_level = {
INFO = true,
WARNING = true,
ERROR = true,
DEBUG = false,
DEBUG_ACTION = false,
DEBUG_SCHEDULE = false,
EXECUTION = false
}
npc.texture_check = {
timer = 0,
interval = 2
}
---------------------------------------------------------------------------------------
-- General functions
---------------------------------------------------------------------------------------
-- Logging
function npc.log(level, message)
if npc.log_level[level] then
minetest.log("info","[advanced_npc] "..level..": "..message)
end
end
-- NPC chat
function npc.chat(npc_name, player_name, message)
minetest.chat_send_player(player_name, npc_name..": "..message)
end
-- Simple wrapper over minetest.add_particle()
-- Copied from mobs_redo/api.lua
function npc.effect(pos, amount, texture, min_size, max_size, radius, gravity, glow)
radius = radius or 2
min_size = min_size or 0.5
max_size = max_size or 1
gravity = gravity or -10
glow = glow or 0
minetest.add_particlespawner({
amount = amount,
time = 0.25,
minpos = pos,
maxpos = pos,
minvel = {x = -radius, y = -radius, z = -radius},
maxvel = {x = radius, y = radius, z = radius},
minacc = {x = 0, y = gravity, z = 0},
maxacc = {x = 0, y = gravity, z = 0},
minexptime = 0.1,
maxexptime = 1,
minsize = min_size,
maxsize = max_size,
texture = texture,
glow = glow,
})
end
-- Gets name of player or NPC
function npc.get_entity_name(entity)
if entity:is_player() then
return entity:get_player_name()
else
return entity:get_luaentity().name
end
end
-- Returns the item "wielded" by player or NPC
-- TODO: Implement NPC
function npc.get_entity_wielded_item(entity)
if entity:is_player() then
return entity:get_wielded_item()
end
end
---------------------------------------------------------------------------------------
-- Spawning functions
---------------------------------------------------------------------------------------
-- These functions are used at spawn time to determine several
-- random attributes for the NPC in case they are not already
-- defined. On a later phase, pre-defining many of the NPC values
-- will be allowed.
local function get_random_name(gender, tags)
local search_tags = {gender}
if tags then
search_tags = { gender, unpack(tags) }
end
local names = npc.info.get_names(search_tags, "all_match")
if next(names) ~= nil then
local i = math.random(#names)
return names[i]
else
-- Return a default name if no name was found
return "Anonymous"
end
end
local function initialize_inventory()
return {
[1] = "", [2] = "", [3] = "", [4] = "",
[5] = "", [6] = "", [7] = "", [8] = "",
[9] = "", [10] = "", [11] = "", [12] = "",
[13] = "", [14] = "", [15] = "", [16] = "",
}
end
-- This function checks for "female" text on the texture name
local function is_female_texture(textures)
for i = 1, #textures do
if string.find(textures[i], "female") ~= nil then
return true
end
end
return false
end
function npc.assign_gender_from_texture(self)
if is_female_texture(self.base_texture) then
return npc.FEMALE
else
return npc.MALE
end
end
local function get_random_texture(gender, age)
local textures = npc.info.get_textures({gender, age}, "all_match")
if next(textures) ~= nil then
local i = math.random(#textures)
return {textures[i]}
else
return {"default_"..gender..".png"}
end
-- local textures = {}
-- local filtered_textures = {}
-- -- Find textures by gender and age
-- if age == npc.age.adult then
-- --minetest.log("Registered: "..dump(minetest.registered_entities["advanced_npc:npc"]))
-- textures = minetest.registered_entities["advanced_npc:npc"].texture_list
-- elseif age == npc.age.child then
-- textures = minetest.registered_entities["advanced_npc:npc"].child_texture
-- end
--
-- for i = 1, #textures do
-- local current_texture = textures[i][1]
-- if (gender == npc.MALE
-- and string.find(current_texture, gender)
-- and not string.find(current_texture, npc.FEMALE))
-- or (gender == npc.FEMALE
-- and string.find(current_texture, gender)) then
-- table.insert(filtered_textures, current_texture)
-- end
-- end
--
-- -- Check if filtered textures is empty
-- if filtered_textures == {} then
-- return textures[1][1]
-- end
--
-- return filtered_textures[math.random(1,#filtered_textures)]
end
--function npc.get_random_texture_from_array(age, gender, textures)
-- local filtered_textures = {}
--
-- for i = 1, #textures do
-- local current_texture = textures[i]
-- -- Filter by age
-- if (gender == npc.MALE
-- and string.find(current_texture, gender)
-- and not string.find(current_texture, npc.FEMALE)
-- and ((age == npc.age.adult
-- and not string.find(current_texture, npc.age.child))
-- or (age == npc.age.child
-- and string.find(current_texture, npc.age.child))
-- )
-- )
-- or (gender == npc.FEMALE
-- and string.find(current_texture, gender)
-- and ((age == npc.age.adult
-- and not string.find(current_texture, npc.age.child))
-- or (age == npc.age.child
-- and string.find(current_texture, npc.age.child))
-- )
-- ) then
-- table.insert(filtered_textures, current_texture)
-- end
-- end
--
-- -- Check if there are no textures
-- if #filtered_textures == 0 then
-- -- Return whole array for re-evaluation
-- npc.log("DEBUG", "No textures found, returning original array")
-- return textures
-- end
--
-- return filtered_textures[math.random(1, #filtered_textures)]
--end
-- Choose whether NPC can have relationships. Only 30% of NPCs
-- cannot have relationships
local function can_have_relationships(is_child)
-- Children can't have relationships
if is_child then
return false
end
local chance = math.random(1,10)
return chance > 3
end
-- Choose a maximum of two items that the NPC will have at spawn time
-- These items are chosen from the favorite items list.
local function choose_spawn_items(self)
local number_of_items_to_add = math.random(1, 2)
-- local number_of_items = #npc.FAVORITE_ITEMS[self.gender].phase1
--
-- for i = 1, number_of_items_to_add do
-- npc.add_item_to_inventory(
-- self,
-- npc.FAVORITE_ITEMS[self.gender].phase1[math.random(1, number_of_items)].item,
-- math.random(1,5)
-- )
-- end
-- Add currency to the items spawned with. Will add 5-10 tier 3
-- currency items
local currency_item_count = math.random(5, 10)
npc.add_item_to_inventory(self, npc.trade.prices.get_currency_itemstring("tier3"), currency_item_count)
-- For test
--npc.add_item_to_inventory(self, "default:tree", 10)
--npc.add_item_to_inventory(self, "default:cobble", 10)
--npc.add_item_to_inventory(self, "default:diamond", 2)
--npc.add_item_to_inventory(self, "default:mese_crystal", 2)
--npc.add_item_to_inventory(self, "flowers:rose", 2)
--npc.add_item_to_inventory(self, "advanced_npc:marriage_ring", 2)
--npc.add_item_to_inventory(self, "flowers:geranium", 2)
--npc.add_item_to_inventory(self, "mobs:meat", 2)
--npc.add_item_to_inventory(self, "mobs:leather", 2)
--npc.add_item_to_inventory(self, "default:sword_stone", 2)
--npc.add_item_to_inventory(self, "default:shovel_stone", 2)
--npc.add_item_to_inventory(self, "default:axe_stone", 2)
--minetest.log("Initial inventory: "..dump(self.inventory))
end
-- Spawn function. Initializes all variables that the
-- NPC will have and choose random, starting values
function npc.initialize(entity, pos, is_lua_entity, npc_stats, npc_info)
npc.log("INFO", "Initializing NPC at "..minetest.pos_to_string(pos))
-- Get variables
local ent = entity
if not is_lua_entity then
ent = entity:get_luaentity()
end
local occupation_name
if npc_info then
occupation_name = npc_info.occupation_name
end
-- Avoid NPC to be removed by mobs_redo API
ent.remove_ok = false
-- Flag that enables/disables right-click interaction - good for moments where NPC
-- can't be disturbed
ent.enable_rightclick_interaction = true
-- Determine gender and age
-- If there's no previous NPC data, gender and age will be randomly chosen.
-- - Sex: Female or male will have each 50% of spawning
-- - Age: 90% chance of spawning adults, 10% chance of spawning children.
-- If there is previous data then:
-- - Sex: The unbalanced gender will get a 75% chance of spawning
-- - Example: If there's one male, then female will have 75% spawn chance.
-- - If there's male and female, then each have 50% spawn chance.
-- - Age: For each two adults, the chance of spawning a child next will be 50%
-- If there's a child for two adults, the chance of spawning a child goes to
-- 40% and keeps decreasing unless two adults have no child.
-- Use NPC stats if provided
if npc_stats then
-- Default chances
local male_s, male_e = 0, 50
local female_s, female_e = 51, 100
local adult_s, adult_e = 0, 85
local child_s, child_e = 86, 100
-- Determine gender probabilities
if npc_stats[npc.FEMALE].total > npc_stats[npc.MALE].total then
male_e = 75
female_s, female_e = 76, 100
elseif npc_stats[npc.FEMALE].total < npc_stats[npc.MALE].total then
male_e = 25
female_s, female_e = 26, 100
end
-- Determine age probabilities
if npc_stats["adult_total"] >= 2 then
if npc_stats["adult_total"] % 2 == 0
and (npc_stats["adult_total"] / 2 > npc_stats["child_total"]) then
child_s,child_e = 26, 100
adult_e = 25
else
child_s, child_e = 61, 100
adult_e = 60
end
end
-- Get gender and age based on the probabilities
local gender_chance = math.random(1, 100)
local age_chance = math.random(1, 100)
local selected_gender = ""
local selected_age = ""
-- Select gender
if male_s <= gender_chance and gender_chance <= male_e then
selected_gender = npc.MALE
elseif female_s <= gender_chance and gender_chance <= female_e then
selected_gender = npc.FEMALE
end
-- Set gender for NPC
ent.gender = selected_gender
-- Select age
if adult_s <= age_chance and age_chance <= adult_e then
selected_age = npc.age.adult
elseif child_s <= age_chance and age_chance <= child_e then
selected_age = npc.age.child
ent.visual_size = {
x = 0.65,
y = 0.65
}
ent.collisionbox = {-0.10,-0.50,-0.10, 0.10,0.40,0.10}
ent.is_child = true
-- For mobs_redo
ent.child = true
end
-- Store the selected age
ent.age = selected_age
-- Set texture accordingly
local selected_texture = get_random_texture(selected_gender, selected_age)
--minetest.log("Selected texture: "..dump(selected_texture))
-- Store selected texture due to the need to restore it later
ent.selected_texture = selected_texture
-- Set texture and base texture
ent.textures = {selected_texture}
ent.base_texture = {selected_texture}
elseif npc_info then
-- Attempt to assign gender from npc_info
if npc_info.gender then
ent.gender = npc_info.gender
else
local gender_chance = math.random(1,2)
ent.gender = npc.FEMALE
if gender_chance == 1 then
ent.gender = npc.MALE
end
end
-- Attempt to assign age from npc_info
if npc_info.age then
ent.age = npc_info.age
else
ent.age = npc.age.adult
end
else
-- Randomly choose gender, and spawn as adult
local gender_chance = math.random(1,2)
ent.gender = npc.FEMALE
if gender_chance == 1 then
ent.gender = npc.MALE
end
ent.age = npc.age.adult
end
-- Initialize all gift data
ent.gift_data = {
-- Choose favorite items. Choose phase1 per default
favorite_items = npc.relationships.select_random_favorite_items(ent.gender, "phase1"),
-- Choose disliked items. Choose phase1 per default
disliked_items = npc.relationships.select_random_disliked_items(ent.gender),
-- Enable/disable gift item hints dialogue lines
enable_gift_items_hints = true
}
-- Flag that determines if NPC can have a relationship
ent.can_have_relationship = can_have_relationships(ent.is_child)
--ent.infotext = "Interested in relationships: "..dump(ent.can_have_relationship)
-- Flag to determine if NPC can receive gifts
ent.can_receive_gifts = ent.can_have_relationship
-- Initialize relationships object
ent.relationships = {}
-- Determines if NPC is married or not
ent.is_married_to = nil
-- Initialize dialogues
ent.dialogues = npc.dialogue.select_random_dialogues_for_npc(ent, "phase1")
-- Declare NPC inventory
ent.inventory = initialize_inventory()
-- Choose items to spawn with
choose_spawn_items(ent)
-- Flags: generic booleans or functions that help drive functionality
ent.flags = {}
-- Declare trade data
ent.trader_data = {
-- Type of trader
trader_status = npc.trade.get_random_trade_status(),
-- Current buy offers
buy_offers = {},
-- Current sell offers
sell_offers = {},
-- Items to buy change timer
change_offers_timer = 0,
-- Items to buy change timer interval
change_offers_timer_interval = 60,
-- Trading list: a list of item names the trader is expected to trade in.
-- It is mostly related to its occupation.
-- If empty, the NPC will revert to casual trading
-- If not, it will try to sell those that it have, and buy the ones it not.
trade_list = {},
-- Custom trade allows to specify more than one payment
-- and a custom prompt (instead of the usual buy or sell prompts)
custom_trades = {}
}
-- To model and control behavior of a NPC, advanced_npc follows an OS model
-- where it allows developers to create processes. These processes executes
-- programs, or a group of instructions that together make the NPC do something,
-- e.g. follow a player, use a furnace, etc. The model is:
-- - Each process has:
-- - An `execution context`, which is memory to store variables
-- - An `instruction queue`, which is a queue with the program instructions
-- to execute
-- - A `state`, whether the process is running or is paused
-- - Processes can specify whether they allow interruptions or not. They also
-- can opt to handle the interruption with a callback. The possible
-- interruptions are:
-- - Punch interruption
-- - Rightclick interruption
-- - Schedule interruption
-- - Only one process can run at a time. If another process is executed,
-- the currently running process is paused, and restored when the other ends.
-- - Processes can be enqueued, so once the executing process finishes, the
-- next one in the queue can be started.
-- - One process, called the `state` process, will run by default when no
-- processes are executing.
ent.execution = {
process_id = 0,
-- Queue of processes
process_queue = {},
-- State process
state_process = {},
-- Whether state process was changed or not
state_process_changed = false,
-- Whether to enable process execution or not
enable = true,
-- Interval to run process queue scheduler
scheduler_interval = 1,
-- Timer for next scheduler interval
scheduler_timer = 0,
-- Monitor environment executes timers and registered callbacks
monitor = {
timer = {},
callback = {
to_execute = {}
},
enabled = true
}
}
-- NPC permanent storage for data
ent.data = {}
-- State date
ent.npc_state = {
-- This table defines the types of interaction the NPC is performing
interaction = {
dialogues = {
is_in_dialogue = false,
in_dialogue_with = "",
in_dialogue_with_name = ""
},
yaw_before_interaction = 0
},
punch = {
last_punch_time = 0,
},
movement = {
is_idle = false,
is_sitting = false,
is_laying = false,
walking = {
is_walking = false,
path = {},
target_pos = {},
}
},
following = {
is_following = false,
following_obj = "",
following_obj_name = ""
}
}
-- This flag is checked on every step. If it is true, the rest of
-- Mobs Redo API is not executed
ent.freeze = nil
-- This map will hold all the places for the NPC
-- Map entries should be like: "bed" = {x=1, y=1, z=1}
ent.places_map = {}
-- Schedule data
ent.schedules = {
-- Flag to enable or disable the schedules functionality
enabled = true,
-- Lock for when executing a schedule
lock = -1,
-- Queue of programs in schedule to be enqueued
-- Used to calculate dependencies
dependency_queue = {},
-- An array of schedules, meant to be one per day at some point
-- when calendars are implemented. Allows for only 7 schedules,
-- one for each day of the week
generic = {},
-- An array of schedules, meant to be for specific dates in the
-- year. Can contain as many as possible. The keys will be strings
-- in the format MM:DD
date_based = {},
-- The following holds the check parameters provided by the
-- current schedule
current_check_params = {}
}
-- If occupation name given, override properties with
-- occupation values and initialize schedules
if occupation_name and occupation_name ~= "" and ent.age == npc.age.adult then
-- Set occupation name
ent.occupation_name = occupation_name
-- Override relevant values
npc.occupations.initialize_occupation_values(ent, occupation_name)
end
-- Nametag is initialized to blank
ent.nametag = ""
-- Set name
if npc_info and npc_info.name then
if npc_info.name.value then
ent.npc_name = npc_info.name.value
elseif npc_info.name.tags then
ent.npc_name = get_random_name(ent.gender, npc_info.name.tags)
else
ent.npc_name = get_random_name(ent.gender)
end
else
ent.npc_name = get_random_name(ent.gender)
end
-- Set ID
ent.npc_id = tostring(math.random(1000, 9999))..":"..ent.npc_name
-- Generate trade offers
npc.trade.generate_trade_offers_by_status(ent)
-- Set initialized flag on
ent.initialized = true
--npc.log("WARNING", "Spawned entity: "..dump(ent))
npc.log("INFO", "Successfully initialized NPC with name "..dump(ent.npc_name)
..", gender: "..ent.gender..", is child: "..dump(ent.is_child)
..", texture: "..dump(ent.textures))
-- Refreshes entity
ent.object:set_properties(ent)
end
---------------------------------------------------------------------------------------
-- Trading functions
---------------------------------------------------------------------------------------
function npc.generate_trade_list_from_inventory(self)
local list = {}
for i = 1, #self.inventory do
list[npc.get_item_name(self.inventory[i])] = {}
end
self.trader_data.trade_list = list
end
function npc.set_trading_status(self, status)
-- Stop, if any, the casual offer regeneration timer
npc.monitor.timer.stop(self, "advanced_npc:trade:casual_offer_regeneration")
--minetest.log("Trader_data: "..dump(self.trader_data))
-- Set status
self.trader_data.trader_status = status
-- Check if status is casual
if status == npc.trade.CASUAL then
-- Register timer for changing casual trade offers
local timer_reg_success = npc.monitor.timer.register(self, "advanced_npc:trade:casual_offer_regeneration", 60,
function(self)
-- Re-select casual trade offers
npc.trade.generate_trade_offers_by_status(self)
end)
if timer_reg_success == false then
-- Activate timer instead
npc.monitor.timer.start(self, "advanced_npc:trade:casual_offer_regeneration")
end
end
-- Re-generate trade offers
npc.trade.generate_trade_offers_by_status(self)
end
---------------------------------------------------------------------------------------
-- Inventory functions
---------------------------------------------------------------------------------------
-- NPCs inventories are restrained to 16 slots.
-- Each slot can hold one item up to 99 count.
-- Utility function to get item name from a string
function npc.get_item_name(item_string)
return ItemStack(item_string):get_name()
end
-- Utility function to get item count from a string
function npc.get_item_count(item_string)
return ItemStack(item_string):get_count()
end
-- Add an item to inventory. Returns true if add successful
-- These function can be used to give items to other NPCs
-- given that the "self" variable can be any NPC
function npc.add_item_to_inventory(self, item_name, count)
-- Check if NPC already has item
local existing_item = npc.inventory_contains(self, item_name)
if existing_item ~= nil and existing_item.item_string ~= nil then
-- NPC already has item. Get count and see
local existing_count = npc.get_item_count(existing_item.item_string)
if (existing_count + count) < npc.INVENTORY_ITEM_MAX_STACK then
-- Set item here
self.inventory[existing_item.slot] =
npc.get_item_name(existing_item.item_string).." "..tostring(existing_count + count)
return true
else
--Find next free slot
for i = 1, #self.inventory do
if self.inventory[i] == "" then
-- Found slot, set item
self.inventory[i] =
item_name.." "..tostring((existing_count + count) - npc.INVENTORY_ITEM_MAX_STACK)
return true
end
end
-- No free slot found
return false
end
else
-- Find a free slot
for i = 1, #self.inventory do
if self.inventory[i] == "" then
-- Found slot, set item
self.inventory[i] = item_name.." "..tostring(count)
return true
end
end
-- No empty slot found
return false
end
end
-- Same add method but with itemstring for convenience
function npc.add_item_to_inventory_itemstring(self, item_string)
local item_name = npc.get_item_name(item_string)
local item_count = npc.get_item_count(item_string)
npc.add_item_to_inventory(self, item_name, item_count)
end
-- Checks if an item is contained in the inventory. Returns
-- the item string or nil if not found
function npc.inventory_contains(self, item_name)
for key,value in pairs(self.inventory) do
if value ~= "" and string.find(value, item_name) then
return {slot=key, item_string=value}
end
end
-- Item not found
return nil
end
-- Removes the item from an NPC inventory and returns the item
-- with its count (as a string, e.g. "default:apple 2"). Returns
-- nil if unable to get the item.
function npc.take_item_from_inventory(self, item_name, count)
local existing_item = npc.inventory_contains(self, item_name)
if existing_item ~= nil then
-- Found item
local existing_count = npc.get_item_count(existing_item.item_string)
local new_count = existing_count
if existing_count - count < 0 then
-- Remove item first
self.inventory[existing_item.slot] = ""
-- TODO: Support for retrieving from next stack. Too complicated
-- and honestly might be unecessary.
return item_name.." "..tostring(new_count)
else
new_count = existing_count - count
if new_count == 0 then
self.inventory[existing_item.slot] = ""
else
self.inventory[existing_item.slot] = item_name.." "..new_count
end
return item_name.." "..tostring(count)
end
else
-- Not able to take item because not found
return nil
end
end
-- Same take method but with itemstring for convenience
function npc.take_item_from_inventory_itemstring(self, item_string)
local item_name = npc.get_item_name(item_string)
local item_count = npc.get_item_count(item_string)
npc.take_item_from_inventory(self, item_name, item_count)
end
---------------------------------------------------------------------------------------
-- Flag functionality
---------------------------------------------------------------------------------------
-- TODO: Consider removing them as they are pretty simple and straight forward.
-- Generic variables or function that help drive some functionality for the NPC.
function npc.add_flag(self, flag_name, value)
self.flags[flag_name] = value
end
function npc.update_flag(self, flag_name, value)
self.flags[flag_name] = value
end
function npc.get_flag(self, flag_name)
return self.flags[flag_name]
end
---------------------------------------------------------------------------------------
-- Dialogue functionality
---------------------------------------------------------------------------------------
function npc.start_dialogue(self, clicker, show_married_dialogue)
-- Call dialogue function as normal
npc.dialogue.start_dialogue(self, clicker, show_married_dialogue)
-- Check and update relationship if needed
npc.relationships.dialogue_relationship_update(self, clicker)
end
---------------------------------------------------------------------------------------
-- State functionality
---------------------------------------------------------------------------------------
-- All the self.npc_state variables are used to track the state of the NPC, and
-- if necessary, restore it back in case of changes. The following functions allow
-- to set different aspects of the state.
function npc.set_movement_state(self, args)
self.npc_state.movement.is_idle = args.is_idle or false
self.npc_state.movement.is_sitting = args.is_sitting or false
self.npc_state.movement.is_laying = args.is_laying or false
self.npc_state.movement.walking.is_walking = args.is_walking or false
end
---------------------------------------------------------------------------------------
-- Execution API
---------------------------------------------------------------------------------------
-- Methods for:
-- - Enqueue a program
-- - Set a program as the `state` process
-- - Execute next process in queue
-- - Pause/restore current process
-- - Process scheduling
-- - Get the current process data
-- - Create, read, write and update variables in current process
-- - Enqueue and execute instructions for the current process
-- Global namespace
npc.exec = {
var = {},
proc = {
instr = {}
}
}
-- Private namespace
local _exec = {
proc = {}
}
-- Process states
npc.exec.proc.state = {
INACTIVE = "inactive",
RUNNING = "running",
EXECUTING = "executing",
PAUSED = "paused",
WAITING_USER_INPUT = "waiting_user_input",
READY = "ready"
}
npc.exec.proc.instr.state = {
INACTIVE = "inactive",
EXECUTING = "executing",
INTERRUPTED = "interrupted"
}
-- This function sets the interrupt options as given from the `interrupt_options`
-- table. This table can have the following values:
-- - allow_punch, boolean
-- - allow_rightclick, boolean
-- - allow_schedule, boolean
function npc.exec.create_interrupt_options(interrupt_options)
local interrupt_options = interrupt_options or {}
if next(interrupt_options) ~= nil then
local allow_punch = interrupt_options.allow_punch
local allow_rightclick = interrupt_options.allow_rightclick
local allow_schedule = interrupt_options.allow_schedule
-- Set defaults
if allow_punch == nil then allow_punch = true end
if allow_rightclick == nil then allow_rightclick = true end
if allow_schedule == nil then allow_schedule = true end
return {
allow_punch = allow_punch,
allow_rightclick = allow_rightclick,
allow_schedule = allow_schedule
}
else
return {
allow_punch = true,
allow_rightclick = true,
allow_schedule = true
}
end
end
function _exec.get_new_process_id(self)
self.execution.process_id = self.execution.process_id + 1
if self.execution.process_id > 10000 then
self.execution.process_id = 0
end
return self.execution.process_id
end
function _exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, process_id)
return {
id = process_id,
program_name = program_name,
arguments = arguments,
state = npc.exec.proc.state.INACTIVE,
execution_context = {
data = {},
instr_interval = 1,
instr_timer = 0
},
instruction_queue = {},
current_instruction = {
entry = {},
state = npc.exec.proc.instr.state.INACTIVE,
pos = {}
},
interrupt_options = npc.exec.create_interrupt_options(interrupt_options),
interrupted_process = {},
is_state_process = is_state_program
}
end
-- This function creates a process for the given program, and
-- places it into the process queue.
function npc.exec.enqueue_program(self, program_name, arguments, interrupt_options, is_state_program)
if is_state_program == nil then
is_state_program = false
end
if is_state_program == true then
npc.exec.set_state_program(self, program_name, arguments, interrupt_options)
-- Enqueue state process
self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process
else
-- Enqueue process
self.execution.process_queue[#self.execution.process_queue + 1] =
_exec.create_process_entry(program_name, arguments, interrupt_options, is_state_program, _exec.get_new_process_id(self))
end
end
-- This function creates a state process. The state process will execute
-- everytime there's no other process executing
function npc.exec.set_state_program(self, program_name, arguments, interrupt_options)
-- Disable monitor - give a chance to this state process to do what it has to do
self.execution.monitor.enabled = false
-- This flag signals the state process was changed and scheduler needs to consume
self.execution.state_process_changed = true
self.execution.state_process = {
program_name = program_name,
arguments = arguments,
state = npc.exec.proc.state.INACTIVE,
execution_context = {
data = {},
instr_interval = 1,
instr_timer = 0
},
instruction_queue = {},
current_instruction = {
entry = {},
state = npc.exec.proc.instr.state.INACTIVE,
pos = {}
},
interrupt_options = npc.exec.create_interrupt_options(interrupt_options),
is_state_process = true,
state_process_id = os.time()
}
end
-- Convenience function that returns first process in the queue
function npc.exec.get_current_process(self)
local result = self.execution.process_queue[1]
if result then
if next(result) == 0 then
return nil
end
end
return result
end
-- This function always execute the process at the start of the process
-- queue. When a process is stopped (because its instruction queue is empty
-- or because the process itself stops), the entry is removed from the
-- process queue, and thus the next process to execute will be the first one
-- in the queue.
function npc.exec.execute_process(self)
local current_process = self.execution.process_queue[1]
-- Execute current process
if current_process then
-- Restore scheduler interval
self.execution.scheduler_interval = 1
if not current_process.is_state_process then
npc.log("EXECUTION", "NPC "..dump(self.npc_name).." is executing: "..dump(current_process.program_name))
end
current_process.state = npc.exec.proc.state.EXECUTING
npc.programs.execute(self, current_process.program_name, current_process.arguments)
current_process.state = npc.exec.proc.state.RUNNING
-- Re-enable monitor
if current_process.is_state_process then
self.execution.monitor.enabled = true
end
end
end
---------------------------------------------------------------------------------------
-- Interruption algorithm
---------------------------------------------------------------------------------------
-- Interruption of an executing process can come from three sources:
-- - NPC is left-clicked (or punch)
-- - NPC is right-clicked (or rightclick)
-- - Job scheduler has identified it is time to start a process
-- When an interrupt happens, and another process needs to be executed, the
-- workflow should be the following:
-- 1. Enqueue the new process to be scheduled.
-- a. If for some reason the process queue *has more than one* process,
-- then the process will have to be enqueued with high priority,
-- meaning next to the current process.
-- 2. Pause the current executing process using `npc.exec.pause_process(self)`
-- The new process will be executed by `npc.exec.pause_process()`.
-- 3. The process finishes execution successfully, in which the scheduler
-- will notice that and restore the interrupted process properly
--
-- It is very important that a process is enqueued before pausing the current
-- process. The pause will not work itself if that condition is not met
-- This function enqueues an array of processes right after the current process
-- Each element in `program_entries` is a Lua table with three parameters:
-- - program_name
-- - arguments
-- - interrupt_options
function _exec.priority_enqueue(self, program_entries)
-- minetest.log("BEGIN PRIORITY ENQUEUE")
-- minetest.log("Initial queue: "..dump(self.execution.process_queue))
-- Check if the queue has more than one (current) process
if #self.execution.process_queue > 1 then
npc.log("EXECUTION", "More than One: "..dump(#self.execution.process_queue))
-- Get current process entry
--local current_process = self.execution.process_queue[1]
-- Backup the current process queue
-- local backup_queue = self.execution.process_queue
-- minetest.log("Backup queue size: "..dump(#backup_queue))
-- -- Remove current process from backup_queue
-- table.remove(backup_queue, 1)
-- minetest.log("Backup queue size after dequeue: "..dump(#backup_queue))
-- -- Recreate queue, re-enqueue first process
-- minetest.log("ENqueue")
-- self.execution.process_queue[#self.execution.process_queue + 1] = current_process
npc.log("EXECUTION", "Queue size after enqueue: "..dump(#self.execution.process_queue))
-- Enqueue the next processes with high priority (next to the current)
npc.log("EXECUTION", "Enqueue all new")
for i = 1, #program_entries do
if program_entries[i].is_state_program == true then
npc.exec.set_state_program(self,
program_entries[i].program_name,
program_entries[i].arguments,
program_entries[i].interrupt_options)
-- Enqueue state process
table.insert(self.execution.process_queue, i + 1, self.execution.state_process)
else
-- Enqueue normal process
table.insert(
self.execution.process_queue, i + 1, _exec.create_process_entry(
program_entries[i].program_name,
program_entries[i].arguments,
program_entries[i].interrupt_options,
program_entries[i].is_state_program,
_exec.get_new_process_id(self)))
end
end
--minetest.log("Backup queue after all new: "..dump(#backup_queue))
else
npc.log("EXECUTION", "Only one process in queue")
-- There is only one process, therefore just enqueue every process
for i = 1, #program_entries do
if program_entries[i].is_state_program == true then
npc.exec.set_state_program(self,
program_entries[i].program_name,
program_entries[i].arguments,
program_entries[i].interrupt_options)
-- Enqueue state process
self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process
else
-- Enqueue normal process
self.execution.process_queue[#self.execution.process_queue + 1] = _exec.create_process_entry(
program_entries[i].program_name,
program_entries[i].arguments,
program_entries[i].interrupt_options,
program_entries[i].is_state_program,
_exec.get_new_process_id(self))
end
end
end
end
-- This function handles a new process called by an interrupt.
-- Will execute steps 1 and 2 of the above algorithm. The scheduler
-- will take care of handling step 3.
function npc.exec.interrupt(self, new_program, new_arguments, interrupt_options)
-- Enqueue process with priority
_exec.priority_enqueue(self,
{[1] = {program_name=new_program, arguments=new_arguments, interrupt_options=interrupt_options}})
--minetest.log("Pause")
minetest.log("info","Interrupted process: "..dump(self.execution.process_queue[1]))
-- Check process - if the instruction queue is empty, do not store
-- Pause current process
_exec.pause_process(self)
local interrupted_process = self.execution.process_queue[1]
-- Dequeue process
table.remove(self.execution.process_queue, 1)
-- Find if interrupted process has more instructions to execute
local has_more_instructions = next(interrupted_process.instruction_queue) ~= nil
--minetest.log("Process has more instructions: "..dump())
if has_more_instructions then
-- Store interrupted process
local current_process = self.execution.process_queue[1]
current_process.interrupted_process = interrupted_process
end
-- Restore process scheduler interval
self.execution.scheduler_interval = 1
--minetest.log("Execute")
-- Execute current process
npc.exec.execute_process(self)
end
-- This function pauses a process and sets its state as waiting for user input.
-- The process scheduler and instruction executer will skip any process in this state.
-- Once the process is ready to run again, the `npc.exec.set_ready_state()` function
-- should be called, and execution will continue.
function npc.exec.set_input_wait_state(self)
npc.log("EXECUTION", "Setting input wait...")
if self.execution.process_queue[1] then
-- Call pause to do the instruction interruption
_exec.pause_process(self)
-- Change process state
self.execution.process_queue[1].state = npc.exec.proc.state.WAITING_USER_INPUT
end
end
function npc.exec.set_ready_state(self)
if self.execution.process_queue[1] then
-- Change process state
self.execution.process_queue[1].state = npc.exec.proc.state.READY
end
end
-- If there is another process in the queue, this function pauses a
-- currently executing process, then executes the
function _exec.pause_process(self, set_instruction_as_interrupted)
if #self.execution.process_queue == 1 then
npc.log("WARNING", "Unable to pause current process without anoher process in queue.\nCurrent queue: "
..dump(self.execution.process_queue))
return
end
local current_process = self.execution.process_queue[1]
if current_process then
-- Check if there are instructions in the instruction queue
if next(current_process.instruction_queue) ~= nil then
-- If the instruction is interrupt, then dequeue that instruction :)
if current_process.instruction_queue[1].name == "advanced_npc:interrupt" then
-- Dequeue instruction
table.remove(current_process.instruction_queue, 1)
-- Check if there are more instructions
if next(current_process.instruction_queue) ~= nil then
-- Set entry
current_process.current_instruction.entry = current_process.instruction_queue[1]
-- Set state
current_process.current_instruction.state = npc.exec.proc.instr.state.INTERRUPTED
else
-- Set entry to blank as there is no other instruction
current_process.current_instruction.entry = {}
-- Set state
current_process.current_instruction.state = npc.exec.proc.instr.state.INACTIVE
end
else
-- Check current instruction
if current_process.current_instruction.entry
and current_process.current_instruction.state == npc.exec.proc.instr.state.EXECUTING then
-- This condition shouldn't become true
--and set_instruction_as_interrupted == true then
-- Change instruction state
current_process.current_instruction.state = npc.exec.proc.instr.state.INTERRUPTED
-- The following flow has been commented out as it doesn't gets executed.
--elseif set_instruction_as_interrupted == nil or set_instruction_as_interrupted == false then
-- current_process.current_instruction.state = npc.exec.proc.instr.state.INACTIVE
end
end
end
--minetest.log("Process after pausing: "..dump(current_process))
-- Change process state
current_process.state = npc.exec.proc.state.PAUSED
end
end
-- This function restores the process that was running before the
-- current one (the interrupted process).
-- As it can only be runned with the interrupted process being enqueued
-- before calling this function, this function is private and only
-- used by the scheduler (which will enqueue the interrupted process before
-- calling this)
function _exec.restore_process(self)
local current_process = self.execution.process_queue[1]
if current_process then
minetest.log("info","Restoring process: "..dump(current_process.program_name))
-- Change process state
current_process.state = npc.exec.proc.state.RUNNING
-- Check if any instruction was interrupted
if current_process.current_instruction.entry
and current_process.current_instruction.state == npc.exec.proc.instr.state.INTERRUPTED then
-- TODO: Do we really want to restore position?
-- Restore position
--self.object:setpos(current_process.current_instruction.pos)
-- Execute instruction
minetest.log("info","Re-executing instruction: "..dump(current_process.current_instruction.entry.name))
_exec.proc.execute(self, current_process.current_instruction.entry)
end
end
end
---------------------------------------------------------------------------------------
-- Scheduler algorithm
---------------------------------------------------------------------------------------
-- This function will manage how processes are executed. This function needs
-- to be called on a one second interval. The function will check:
-- - If the process queue is emtpy and there is a state process, enqueue the
-- the state process and execute
-- - If the current process' instruction queue is empty:
-- - If the process is a `state` process, and no other process is in queue,
-- re-execute `state` process.
-- - If the process is a `state` process and there is a process in queue,
-- - Remove current process from queue
-- - Store the current process entry into the `interrupted_process` field of
-- the next process in queue.
-- - Execute next process in queue
-- - If the process is *not* a `state` process and there is a process entry in
-- the `interrupted_process` field:
-- - Remove current process from queue
-- - Enqueue the entry in the `interrupted_process` field
-- - Execute next process in the queue
-- - If the instruction queue is not empty, continue
function npc.exec.process_scheduler(self)
npc.log("EXECUTION", "Current process queue size: "..dump(#self.execution.process_queue))
-- minetest.log("Queue for "..dump(self.npc_name))
-- for i = 1, #self.execution.process_queue do
-- minetest.log("["..dump(self.execution.process_queue[i].program_name).."]")
-- end
-- Check current process
local current_process = self.execution.process_queue[1]
if current_process then
-- Check current process state
if current_process.state == npc.exec.proc.state.EXECUTING then
-- Do not interrupt process while the process is enqueuing instructions
return
elseif current_process.state == npc.exec.proc.state.INACTIVE then
-- Execute process
npc.exec.execute_process(self)
elseif current_process.state == npc.exec.proc.state.READY then
-- Change state to running
current_process.state = npc.exec.proc.state.RUNNING
elseif current_process.state == npc.exec.proc.state.PAUSED then
-- Restore process
_exec.restore_process(self)
end
-- Check if instruction queue is empty
if current_process.instruction_queue and #current_process.instruction_queue == 0
and current_process.state == npc.exec.proc.state.RUNNING then
-- Check if this is a state process
if current_process.is_state_process == true then
-- Check if the process queue only has this process
if #self.execution.process_queue == 1 then
-- Check if state process was changed
if self.execution.state_process_changed == true then
npc.log("EXECUTION", "Switching from state process "
..dump(self.execution.process_queue[1].program_name)
.." to "
..dump(self.execution.state_process.program_name))
-- Dequeue this process, enqueue new one
self.execution.process_queue[1] = self.execution.state_process
-- Change flag back
self.execution.state_process_changed = false
end
-- Since this is a state process, re-execute
npc.log("EXECUTION", "Hi, executing state process "..dump(self.execution.process_queue[1].program_name))
npc.exec.execute_process(self)
else
-- Changed state process check - an old state process could be enqueued,
-- but the state process was changed. If this is is true, ignore old
-- entry in the process queue.
local next_enqueued_process = self.execution.process_queue[2]
if self.execution.state_process_changed == true
and next_enqueued_process.id ~= current_process.id
and next_enqueued_process.is_state_process == true then
-- Assume every enqueued state process is old and discard
table.remove(self.execution.process_queue, 2)
-- Change flag back
self.execution.state_process_changed = false
else
-- Next process is not a state process, interrupt current state process
npc.log("EXECUTION", "Current process queue size: "..dump(#self.execution.process_queue))
-- Pause current process
current_process.state = npc.exec.proc.state.PAUSED
-- Dequeue process
table.remove(self.execution.process_queue, 1)
-- Get next process in queue
local next_process = self.execution.process_queue[1]
-- Store the interrupted process in the next process
next_process.interrupted_process = current_process
end
-- Execute next process
npc.exec.execute_process(self)
end
else
npc.log("EXECUTION", "Current process name: "..dump(current_process.program_name))
npc.log("EXECUTION", "Process queue size: "..dump(#self.execution.process_queue))
npc.log("EXECUTION", "Current instrcution queue size: "..dump(#current_process.instruction_queue))
npc.log("EXECUTION", "Current process state: "..dump(current_process.state))
-- This is not a state process, check the interrupted process field
if next(current_process.interrupted_process) ~= nil then
npc.log("EXECUTION", "There is an interrupted process: "..dump(current_process.interrupted_process.program_name))
npc.log("EXECUTION", "------------------------------")
npc.log("EXECUTION", "Is state process? "..dump(current_process.interrupted_process.is_state_process))
npc.log("EXECUTION", "State process ID: "..dump(current_process.interrupted_process.state_process_id))
npc.log("EXECUTION", "Valid state process ID: "..dump(self.execution.state_process.state_process_id))
if current_process.interrupted_process.is_state_process == true
and current_process.interrupted_process.state_process_id
and (current_process.interrupted_process.state_process_id < self.execution.state_process.state_process_id) then
-- Do nothing, just dequeue process
npc.log("EXECUTION", "Found an old state process that was interrupted.\n"
..dump(current_process.interrupted_process.program_name).." WILL NOT be re-enqueued")
npc.log("EXECUTION", "Process "..dump(self.execution.process_queue[1].program_name)
.." is finished execution and will be dequeued")
-- Dequeue process
table.remove(self.execution.process_queue, 1)
-- Check if there are more processes
if #self.execution.process_queue > 0 then
-- Execute new process
npc.exec.execute_process(self)
end
return
end
-- Dequeue process
table.remove(self.execution.process_queue, 1)
-- Re-enqueue the interrupted process
self.execution.process_queue[#self.execution.process_queue + 1] = current_process.interrupted_process
if #self.execution.process_queue > 1 then
-- Execute next process in queue
npc.exec.execute_process(self)
else
-- Execute next process in queue which is interrupted
_exec.restore_process(self)
end
else
npc.log("EXECUTION", "Process "..dump(self.execution.process_queue[1].program_name).." is finished execution")
-- Dequeue process
table.remove(self.execution.process_queue, 1)
-- Check if there are more processes
if #self.execution.process_queue > 0 then
-- Execute new process
npc.exec.execute_process(self)
end
end
end
end
else
-- Process queue is empty, enqueue state process if it is defined
if next(self.execution.state_process) ~= nil then
npc.log("EXECUTION", "NPC "..dump(self.npc_name).." is executing: "..dump(self.execution.state_process.program_name))
self.execution.process_queue[#self.execution.process_queue + 1] = self.execution.state_process
-- Execute state process
npc.exec.execute_process(self)
end
end
end
---------------------------------------------------------------------------------------
-- Process instructions functionality - enqueue and execute instructions
-- for the currently executing process
---------------------------------------------------------------------------------------
-- This function enqueues a given instruction with its arguments
-- in the current process' instruction queue. If var_name is given,
-- results of this function are stored in the execution context with that
-- var_key
function npc.exec.proc.enqueue(self, name, args, var_name)
local current_process = self.execution.process_queue[1]
if current_process then
current_process.instruction_queue[#current_process.instruction_queue + 1] =
{name=name, args=args, var_name=var_name}
end
end
-- Private function to execute a given instruction entry
function _exec.proc.execute(self, entry)
if entry ~= nil and next(entry) ~= nil then
local current_process = self.execution.process_queue[1]
if current_process then
-- Set current instruction params
current_process.current_instruction.entry = entry
current_process.current_instruction.pos = self.object:getpos()
current_process.current_instruction.state = npc.exec.proc.instr.state.EXECUTING
-- Execute current instruction
npc.log("EXECUTION", "Executing instruction: "..dump(entry.name))
local result = npc.programs.instr.execute(self, entry.name, entry.args)
if entry.name == "advanced_npc:interrupt" then
-- Do not do anything else, the interrupt instruction was already
-- dequeued.
return
end
-- Check if var_name was given
if entry.var_name then
if npc.exec.var.get(self, entry.var_name) then
-- Update the value
npc.exec.var.set(self, entry.var_name, result)
else
-- Create new var with value
npc.exec.var.put(self, entry.var_name, result, false)
end
end
-- Dequeue from instruction queue
table.remove(current_process.instruction_queue, 1)
end
end
-- minetest.log("END PRIVATE PROC EXEC")
end
-- This function executes the next instruction entry in the current
-- process' instruction queue
function npc.exec.proc.execute(self)
--minetest.log("PROCESS EXECUTE BEGIN")
local current_process = self.execution.process_queue[1]
if current_process then
-- Get next instruction entry in queue
local entry = current_process.instruction_queue[1]
-- Execute instruction
_exec.proc.execute(self, entry)
end
--minetest.log("PROCESS EXECUTE END")
end
---------------------------------------------------------------------------------------
-- Execution routine
---------------------------------------------------------------------------------------
-- This function is to be executed on each step() of the Lua entity
-- Algorithm:
-- 1. Increase the timer with dtime
-- 2. If the timer has reached the interval, then:
-- a. Reset the timer and execute `npc.exec.process_scheduler(self)`
-- 3. Increase the current process' instruction timer with dtime
-- 4. If the instruction timer has reached the interval, then:
-- a. Reset the instruction timer and execute `noc.exec.proc.execute(self)`
function npc.exec.execution_routine(self, dtime)
local execution = self.execution
-- Increase process scheduler timer
execution.scheduler_timer = execution.scheduler_timer + dtime
-- Check if timer reached interval
if execution.scheduler_timer >= execution.scheduler_interval then
-- Reset timer
execution.scheduler_timer = 0
npc.log("EXECUTION", "Executing scheduler for NPC "..dump(self.npc_name))
-- Execute process scheduler
npc.exec.process_scheduler(self)
end
-- Get current process
local current_process = execution.process_queue[1]
if current_process ~= nil and current_process.execution_context ~= nil then
--minetest.log("STATE: "..dump(self.execution.process_queue[1].state))
--minetest.log("PROCESS: "..dump(self.execution.process_queue[1]))
if current_process.state == npc.exec.proc.state.RUNNING then
-- Increase timer
current_process.execution_context.instr_timer =
current_process.execution_context.instr_timer + dtime
-- Check if timer reached interval
if current_process.execution_context.instr_timer
>= current_process.execution_context.instr_interval then
-- Reset timer
--minetest.log("HI, RESET")
current_process.execution_context.instr_timer = 0
-- Check if NPC is walking
if self.npc_state.movement.walking.is_walking == true then
-- Move NPC to expected position to ensure not getting lost
local pos = self.npc_state.movement.walking.target_pos
if vector.distance(self.object:getpos(), pos) > 0.2 then
npc.log("INFO", "Corrected position for walking NPC "..dump(self.npc_name).." to "..minetest.pos_to_string(pos))
self.object:moveto({x=pos.x, y=pos.y, z=pos.z})
end
end
-- Execute next instruction in process' queue
npc.exec.proc.execute(self)
end
end
end
end
---------------------------------------------------------------------------------------
-- Variable functionality - create, read, update and delete variables in the
-- current process.
-- IMPORTANT: These variables are deleted when the process is finished execution.
-- For permanent storage, use npc.data.* functions.
---------------------------------------------------------------------------------------
-- This function adds a value to the execution context of the
-- current process.
-- Readonly defaults to false. Returns false if failed due to
-- key-name conflict, or returns true if successful
function npc.exec.var.put(self, name, value, readonly)
-- Retrieve current process execution context
local current_process = self.execution.process_queue[1]
if current_process then
local context = current_process.execution_context
-- Check if variable exists
if context[name] ~= nil then
npc.log("ERROR", "Attempt to create new variable with name "..name.." failed"..
"due to variable already existing: "..dump(context[name]))
return false
end
context[name] = {value = value, readonly = readonly}
return true
end
end
-- Returns the value of a given key. If not found returns nil.
function npc.exec.var.get(self, name)
-- Retrieve current process execution context
local current_process = self.execution.process_queue[1]
if current_process then
local context = current_process.execution_context
local result = context[name]
if result == nil then
return nil
else
return result.value
end
end
end
function npc.exec.var.get_or_put_if_nil(self, name, initial_value)
local var = npc.exec.var.get(self, name)
if var == nil then
npc.exec.var.put(self, name, initial_value)
return initial_value
else
return var
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.exec.var.set(self, name, new_value)
-- Retrieve current process execution context
local current_process = self.execution.process_queue[1]
if current_process then
local context = current_process.execution_context
local var = context[name]
if var == nil then
return false
else
if var.readonly == true then
npc.log("ERROR", "Attempt to set value of readonly variable: "..name)
return false
end
var.value = new_value
end
return true
end
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.exec.var.remove(self, name)
-- Retrieve current process execution context
local current_process = self.execution.process_queue[1]
if current_process then
local context = current_process.execution_context
local result = context[name]
if result == nil then
return nil
else
-- Clear variable
npc.exec.get_current_process(self).execution_context[name] = nil
return result
end
end
end
---------------------------------------------------------------------------------------
-- Permanent storage functionality - create, read, update and delete variables
-- in the NPC's permnanent storage.
-- IMPORTANT: These variables are *NOT* deleted. Be careful what you store on it or
-- the NPC object can grow in size very quickly.
-- For temporary storage, use npc.exec.var.* functions.
---------------------------------------------------------------------------------------
-- Namespace
npc.data = {}
-- This function adds a value to the execution context of the
-- current process.
-- Readonly defaults to false. Returns false if failed due to
-- key-name conflict, or returns true if successful
function npc.data.put(self, name, value, readonly)
-- Check if variable exists
if self.data[name] ~= nil then
npc.log("ERROR", "Attempt to create new variable with name "..name.." failed"..
"due to variable already existing: "..dump(self.data[name]))
return false
end
self.data[name] = {value = value, readonly = readonly}
return true
end
-- Returns the value of a given key. If not found returns nil.
function npc.data.get(self, name)
local result = self.data[name]
if result == nil then
return nil
else
return result.value
end
end
-- Convenience function for initializing a variable if nil
function npc.data.get_or_put_if_nil(self, name, initial_value)
local var = npc.data.get(self, name)
if var == nil then
npc.data.put(self, name, initial_value, false)
return initial_value
else
return var
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.data.set(self, name, new_value)
local var = self.data[name]
if var == nil then
return false
else
if var.readonly == true then
npc.log("ERROR", "Attempt to set value of readonly variable: "..name)
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.data.remove(self, name)
local result = self.data[name]
if result == nil then
return nil
else
-- Clear variable
self.data[name] = nil
return result
end
end
---------------------------------------------------------------------------------------
-- Monitor API: API that executes timers and registered callbacks.
-- - Timers can be registered by programs or by code in general, and can
-- have a callback which is executed when the timer reaches the interval.
-- - Callbacks are for programs, instructions and for interrupts (punch, right-click,
-- and scheduled entries). The callback is executed after a program,
-- instruction or interrupt is executed.
-- IMPORTANT: Please, keep *all your callbacks* as light as possible. While useful,
-- too many timers or callbacks can deteriorate performance, as all could
-- run on NPC steps.
---------------------------------------------------------------------------------------
-- Namespace
npc.monitor = {
timer = {
registered = {}
},
callback = {
registered = {},
-- Constant values
type = {
program = "program",
instruction = "instruction",
interaction = "interaction",
},
subtype = {
on_punch = "on_punch",
on_rightclick = "on_rightclick",
on_schedule = "on_schedule",
}
}
}
-- Register a timer. The timer can have the following arguments:
-- - name: unique identifier for timer
-- - interval: when timer reaches this value, callback will be executed
-- - callback: function to be executed when timer reaches interval
-- - initial_value: default is 0. Give this to start with a specific value
function npc.monitor.timer.register(name, interval, callback)
if npc.monitor.timer.registered[name] ~= nil then
npc.log("DEBUG", "Attempt to register an existing timer: "..dump(name))
return false
else
local timer = {
interval = interval,
callback = callback
}
npc.monitor.timer.registered[name] = timer
end
return true
end
function npc.monitor.timer.start(self, name, interval, args)
if self.execution.monitor.timer[name] then
npc.log("DEBUG", "Attempted to start already started timer: "..dump(name))
return
end
local timer = npc.monitor.timer.registered[name]
if timer then
-- Activate timer by moving it into the active timer array
self.execution.monitor.timer[name] = {
value = 0,
interval = interval or timer.interval,
args = args
}
else
npc.log("DEBUG", "Attempted to start non-existent timer: "..dump(name))
end
end
function npc.monitor.timer.stop(self, name)
if self.execution.monitor.timer[name] == nil then
npc.log("DEBUG", "Attempted to stop already stopped timer: "..dump(name))
return
end
local timer = self.execution.monitor.timer[name]
if timer then
-- Set timer for removal on next monitor execution routine
self.execution.monitor.timer[name].remove = true
else
npc.log("DEBUG", "Attempted to stop non-existent timer: "..dump(name))
end
end
-- Name is the name of function for which callback is being registered.
-- Use program or instruction name for corresponding programs or instructions,
-- and "on_punch", "on_rightclick", "on_activate", "on_schedule" for interrupts
function npc.monitor.callback.register(name, type, subtype, callback)
-- Initialize type and subtype if they don't exist
if npc.monitor.callback.registered[type] == nil then
npc.monitor.callback.registered[type] = {}
end
if npc.monitor.callback.registered[type][subtype] == nil then
npc.monitor.callback.registered[type][subtype] = {}
end
-- Check if callback already exists
if npc.monitor.callback.registered[type][subtype][name] ~= nil then
npc.log("DEBUG", "Attempt to register an existing callback: "..dump(name))
return
else
-- Register callback
npc.monitor.callback.registered[type][subtype][name] = callback
end
end
function npc.monitor.callback.exists(type, subtype)
if npc.monitor.callback.registered[type] ~= nil then
if npc.monitor.callback.registered[type][subtype] ~= nil then
return next(npc.monitor.callback.registered[type][subtype]) ~= nil
end
end
return false
end
function npc.monitor.callback.enqueue(self, type, subtype, name)
self.execution.monitor.callback.to_execute[#self.execution.monitor.callback.to_execute + 1] = {
name = name,
type = type,
subtype = subtype
}
end
function npc.monitor.callback.enqueue_all(self, type, subtype)
for name,_ in pairs(npc.monitor.callback.registered[type][subtype]) do
self.execution.monitor.callback.to_execute[#self.execution.monitor.callback.to_execute + 1] = {
name = name,
type = type,
subtype = subtype
}
end
end
function npc.monitor.execution_routine(self, dtime)
if self.execution.monitor.enabled == false then
return
end
-- Execute timers - traverse the array of active timers and increase
-- their respective values
for name,timer in pairs(self.execution.monitor.timer) do
if timer.remove == true then
self.execution.monitor.timer[name] = nil
else
-- Increase value
timer.value = timer.value + dtime
-- Check if interval is met
if timer.value >= timer.interval then
-- Reset value
timer.value = 0
-- Execute callback
npc.monitor.timer.registered[name].callback(self, timer.args)
end
end
end
-- Execute callbacks - traverse array of callbacks to execute
for i = #self.execution.monitor.callback.to_execute, 1, -1 do
local callback = self.execution.monitor.callback.to_execute[i]
-- Execute callback
npc.monitor.callback.registered[callback.type][callback.subtype][callback.name](self)
-- Remove callback from the execute array
self.execution.monitor.callback.to_execute[i] = nil
end
end
---------------------------------------------------------------------------------------
-- Schedule functionality
---------------------------------------------------------------------------------------
-- Schedules allow the NPC to do different things depending on the time of the day.
-- The time of the day is in 24 hours and is consistent with the Minetest
-- /time command. Hours will be written as numbers: 1 for 1:00, 13 for 13:00 or 1:00 PM
-- The API is as following: a schedule can be created for a specific date or for a
-- day of the week. A date is a string in the format MM:DD
npc.schedule = {
const = {
types = {
generic = "generic",
date_based = "date_based"
}
},
entry = {}
}
npc.schedule_properties = {
put_item = "put_item",
put_multiple_items = "put_multiple_items",
take_item = "take_item",
trader_status = "trader_status",
can_receive_gifts = "can_receive_gifts",
flag = "flag",
enable_gift_items_hints = "enable_gift_items_hints",
set_trade_list = "set_trade_list"
}
local function get_time_in_hours()
return minetest.get_timeofday() * 24
end
-- Create a schedule on a NPC.
-- Schedule types:
-- - Generic: Returns nil if there are already
-- seven schedules, one for each day of the
-- week or if the schedule attempting to add
-- already exists. The date parameter is the
-- day of the week it represents as follows:
-- - 1: Monday
-- - 2: Tuesday
-- - 3: Wednesday
-- - 4: Thursday
-- - 5: Friday
-- - 6: Saturday
-- - 7: Sunday
-- - Date-based: The date parameter should be a
-- string of the format "MM:DD". If it already
-- exists, function retuns nil
function npc.schedule.create(self, schedule_type, date)
if schedule_type == npc.schedule.const.types.generic then
-- Check that there are no more than 7 schedules
if #self.schedules.generic == 7 then
-- Unable to add schedule
return nil
elseif #self.schedules.generic < 7 then
-- Check schedule doesn't exists already
if self.schedules.generic[date] == nil then
-- Add schedule
self.schedules.generic[date] = {}
else
-- Schedule already present
return nil
end
end
elseif schedule_type == npc.schedule.const.types.date then
-- Check schedule doesn't exists already
if self.schedules.date_based[date] == nil then
-- Add schedule
self.schedules.date_based[date] = {}
else
-- Schedule already present
return nil
end
end
end
function npc.schedule.delete(self, schedule_type, date)
-- Delete schedule by setting entry to nil
self.schedules[schedule_type][date] = nil
end
-- Schedule entries API
-- Allows to add, get, update and delete entries from each
-- schedule. Attempts to be as safe-fail as possible to avoid crashes.
-- Actions is an array of actions and tasks that the NPC
-- will perform at the scheduled time on the scheduled date
function npc.schedule.entry.put(self, schedule_type, date, time, check, actions)
-- Check that schedule for date exists
if self.schedules[schedule_type][date] ~= nil then
-- Add schedule entry
if check == nil then
self.schedules[schedule_type][date][time] = actions
else
self.schedules[schedule_type][date][time].check = check
end
else
-- No schedule found, need to be created for date
return nil
end
end
function npc.schedule.entry.get(self, schedule_type, date, time)
-- Check if schedule for date exists
if self.schedules[schedule_type][date] ~= nil then
-- Return schedule
return self.schedules[schedule_type][date][time]
else
-- Schedule for date not found
return nil
end
end
function npc.schedule.entry.set(self, schedule_type, date, time, check, actions)
-- Check schedule for date exists
if self.schedules[schedule_type][date] ~= nil then
-- Check that a schedule entry for that time exists
if self.schedules[schedule_type][date][time] ~= nil then
-- Set the new actions
if check == nil then
self.schedules[schedule_type][date][time] = actions
else
self.schedules[schedule_type][date][time].check = check
end
else
-- Schedule not found for specified time
return nil
end
else
-- Schedule not found for date
return nil
end
end
function npc.schedule.entry.remove(self, schedule_type, date, time)
-- Check schedule for date exists
if self.schedules[schedule_type][date] ~= nil then
-- Remove schedule entry by setting to nil
self.schedules[schedule_type][date][time] = nil
else
-- Schedule not found for date
return nil
end
end
-- Execution routine for schedules
-- For now, only one program per hour should be created by schedule
function npc.schedule.execution_routine(self, dtime)
if self.schedules.enabled == true then
-- Get time of day
local time = get_time_in_hours()
-- Check if time is an hour
if ((time % 1) < dtime) then
-- Get integer part of time
time = (time) - (time % 1)
if not(time > self.schedules.lock or (time == 0 and self.schedules.lock == 23)) then
return
end
npc.log("INFO", "Time: "..dump(time))
-- Activate lock to avoid more than one entry to this code
self.schedules.lock = time
-- Check if there is a schedule entry for this time
-- Note: Currently only one schedule is supported, for day 0
local schedule = self.schedules.generic[0]
if schedule ~= nil then
-- Check if schedule for this time exists
if schedule[time] ~= nil then
-- Check if schedules are enabled, and interruptions by scheduler allowed by
-- current state/executing script
local current_process = self.execution.process_queue[1]
--minetest.log("CURRENT PROCESS name: "..dump(current_process.program_name))
if current_process and current_process.interrupt_options.allow_scheduler == false then
-- Don't check schedules any further
return
end
-- Hold the programs to be enqueued
local programs_to_enqueue = {}
local entries_to_enqueue = {}
-- Check if a program should be enqueued or not
for i = 1, #schedule[time] do
-- Check chance
local execution_chance = math.random(1, 100)
if not schedule[time][i].chance or
(schedule[time][i].chance and execution_chance <= schedule[time][i].chance) then
-- Check if entry has dependency on other entry
local dependencies_met
if schedule[time][i].depends then
-- TODO: Fix dependency check issue
-- minetest.log("Programs to enqueue size: "..dump(programs_to_enqueue))
-- minetest.log("i: "..dump(i))
-- minetest.log("Dependency: "..dump(schedule[time][i].depends[1]))
-- minetest.log("programs to enqueue[1]: "..dump(programs_to_enqueue[1]))
-- for key,var in pairs(programs_to_enqueue) do
-- minetest.log("Key: "..dump(key))
-- end
-- minetest.log("entries to enqueue[i]: "..dump(entries_to_enqueue[schedule[time][i].depends[1]]))
if entries_to_enqueue[schedule[time][i].depends[1]] ~= nil then
dependencies_met = true
else
dependencies_met = false
end
end
-- minetest.log("Dependencies met for entry with name: "..dump(schedule[time][i].program_name)..": "..dump(dependencies_met))
-- Check for dependencies being met
if dependencies_met == nil or dependencies_met == true then
programs_to_enqueue[#programs_to_enqueue + 1] = schedule[time][i]
entries_to_enqueue[i] = i
else
npc.log("DEBUG", "Skipping schedule entry for time "..dump(time)..": "..dump(schedule[time][i]))
end
end
end
-- Enqueue all programs in programs_to_enqueue
if #programs_to_enqueue > 0 then
npc.log("INFO", "Enqueueing the following programs into process queue for time: "..dump(time).."\n"
..dump(programs_to_enqueue))
_exec.priority_enqueue(self, programs_to_enqueue)
end
-- Clear programs_to_enqueue
programs_to_enqueue = nil
entries_to_enqueue = nil
end
end
-- else
-- -- Check if lock can be released
-- if (time % 1) > dtime + 0.1 then
-- -- Release lock
-- self.schedules.lock = false
-- end
end
end
end
---------------------------------------------------------------------------------------
-- NPC Lua object functions
---------------------------------------------------------------------------------------
-- The following functions make up the definitions of on_rightclick(), do_custom()
-- and other functions that are assigned to the Lua entity definition
-- This function is executed each time the NPC is loaded
function npc.after_activate(self)
--minetest.log("Self: "..dump(self))
-- Reset animation
if self.npc_state then
if self.npc_state.movement then
if self.npc_state.movement.is_sitting == true then
npc.programs.instr.execute(self, npc.programs.instr.default.SIT, {pos=self.object:getpos()})
elseif self.npc_state.movement.is_laying == true then
npc.programs.instr.execute(self, npc.programs.instr.default.LAY, {pos=self.object:getpos()})
end
-- Reset yaw if available
if self.yaw_before_interaction then
self.object:setyaw(self.yaw_before_interaction)
end
end
end
end
-- This function is executed on right-click
function npc.rightclick_interaction(self, clicker)
-- Disable right click interaction per execution options
local current_process = self.execution.process_queue[1]
if current_process then
if current_process.interrupt_options.allow_rightclick == false then
npc.log("WARNING", "Attempted to right-click a NPC with disabled rightlick interaction")
return
end
end
-- Enqueue callback if any
if npc.monitor.callback.exists(npc.monitor.callback.type.interaction, npc.monitor.callback.subtype.on_rightclick) then
-- Enqueue all right-click callbacks for execution
npc.monitor.callback.enqueue_all(self,
npc.monitor.callback.type.interaction,
npc.monitor.callback.subtype.on_rightclick)
end
-- Store original yaw
self.yaw_before_interaction = self.object:getyaw()
-- Rotate NPC toward its clicker
npc.dialogue.rotate_npc_to_player(self)
-- Get information from clicker
local item = clicker:get_wielded_item()
local name = clicker:get_player_name()
npc.log("INFO", "Right-clicked NPC: "..dump(self))
-- Receive gift or start chat. If player has no item in hand
-- then it is going to start chat directly
--minetest.log("self.can_have_relationship: "..dump(self.can_have_relationship)..", self.can_receive_gifts: "..dump(self.can_receive_gifts)..", table: "..dump(item:to_table()))
if self.can_have_relationship
and self.can_receive_gifts
and item:to_table() ~= nil then
-- Get item name
local item = minetest.registered_items[item:get_name()]
local item_name = item.description
-- Show dialogue to confirm that player is giving item as gift
npc.dialogue.show_yes_no_dialogue(
self,
"Do you want to give "..item_name.." to "..self.npc_name.."?",
npc.dialogue.POSITIVE_GIFT_ANSWER_PREFIX..item_name,
function()
npc.relationships.receive_gift(self, clicker)
end,
npc.dialogue.NEGATIVE_ANSWER_LABEL,
function()
npc.start_dialogue(self, clicker, true)
end,
name
)
else
npc.start_dialogue(self, clicker, true)
end
end
function npc.step(self, dtime)
if self.initialized == nil or self.initialized == false then
-- Initialize NPC if spawned using the spawn egg built in from
-- mobs_redo. This functionality will be removed in the future in
-- favor of a better manual spawning method with customization
npc.log("WARNING", "Initializing NPC from entity step. This message should only be appearing if an NPC is being spawned from inventory with egg!")
npc.initialize(self, self.object:getpos(), true)
self.tamed = false
self.owner = nil
else
-- NPC is initialized, check other variables
-- Check child texture issues
if self.is_child then
-- Check texture
npc.texture_check.timer = npc.texture_check.timer + dtime
if npc.texture_check.timer > npc.texture_check.interval then
-- Reset timer
npc.texture_check.timer = 0
-- Set hornytimer to zero every 60 seconds so that children
-- don't grow automatically
self.hornytimer = 0
-- Set correct textures
self.texture = {self.selected_texture}
self.base_texture = {self.selected_texture}
self.object:set_properties(self)
npc.log("WARNING", "Corrected textures on NPC child "..dump(self.npc_name))
-- Set interval to large interval so this code isn't called frequently
npc.texture_check.interval = 60
end
end
end
-- Timer function for gifts
for i = 1, #self.relationships do
local relationship = self.relationships[i]
-- Gift timer check
if relationship.gift_timer_value < relationship.gift_interval then
relationship.gift_timer_value = relationship.gift_timer_value + dtime
elseif relationship.talk_timer_value < relationship.gift_interval then
-- Relationship talk timer - only allows players to increase relationship
-- by talking on the same intervals as gifts
relationship.talk_timer_value = relationship.talk_timer_value + dtime
else
-- Relationship decrease timer
if relationship.relationship_decrease_timer_value
< relationship.relationship_decrease_interval then
relationship.relationship_decrease_timer_value =
relationship.relationship_decrease_timer_value + dtime
else
-- Check if married to decrease half
if relationship.phase == "phase6" then
-- Avoid going below the marriage phase limit
if (relationship.points - 0.5) >=
npc.relationships.RELATIONSHIP_PHASE["phase5"].limit then
relationship.points = relationship.points - 0.5
end
else
relationship.points = relationship.points - 1
end
relationship.relationship_decrease_timer_value = 0
end
end
end
-- Execute monitor
npc.monitor.execution_routine(self, dtime)
-- Execute process scheduler
npc.exec.execution_routine(self, dtime)
-- Schedule timer
npc.schedule.execution_routine(self, dtime)
return false--self.freeze
end
---------------------------------------------------------------------------------------
-- NPC Definition
---------------------------------------------------------------------------------------
--mobs:register_mob("advanced_npc:npc", {
-- type = "npc",
-- passive = false,
-- damage = 3,
-- attack_type = "dogfight",
-- attacks_monsters = true,
-- -- Added group attack
-- group_attack = true,
-- -- Pathfinder = 2 to make NPCs more smart when attacking
-- pathfinding = 2,
-- hp_min = 10,
-- hp_max = 20,
-- armor = 100,
-- collisionbox = {-0.20,0,-0.20, 0.20,1.8,0.20},
-- --collisionbox = {-0.20,-1.0,-0.20, 0.20,0.8,0.20},
-- --collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35},
-- visual = "mesh",
-- mesh = "character.b3d",
-- drawtype = "front",
-- textures = {
-- {"npc_male1.png"},
-- {"npc_male2.png"},
-- {"npc_male3.png"},
-- {"npc_male4.png"},
-- {"npc_male5.png"},
-- {"npc_male6.png"},
-- {"npc_male7.png"},
-- {"npc_male8.png"},
-- {"npc_male9.png"},
-- {"npc_male10.png"},
-- {"npc_male11.png"},
-- {"npc_male12.png"},
-- {"npc_male13.png"},
-- {"npc_male14.png"},
-- {"npc_female1.png"}, -- female by nuttmeg20
-- {"npc_female2.png"},
-- {"npc_female3.png"},
-- {"npc_female4.png"},
-- {"npc_female5.png"},
-- {"npc_female6.png"},
-- {"npc_female7.png"},
-- {"npc_female8.png"},
-- {"npc_female9.png"},
-- {"npc_female10.png"},
-- {"npc_female11.png"},
-- },
-- child_texture = {
-- {"npc_child_male1.png"},
-- {"npc_child_female1.png"},
-- },
-- makes_footstep_sound = true,
-- sounds = {},
-- -- Added walk chance
-- walk_chance = 20,
-- -- Added stepheight
-- stepheight = 0.6,
-- walk_velocity = 1,
-- run_velocity = 3,
-- jump = false,
-- drops = {
-- {name = "default:wood", chance = 1, min = 1, max = 3},
-- {name = "default:apple", chance = 2, min = 1, max = 2},
-- {name = "default:axe_stone", chance = 5, min = 1, max = 1},
-- },
-- water_damage = 0,
-- lava_damage = 2,
-- light_damage = 0,
-- --follow = {"farming:bread", "mobs:meat", "default:diamond"},
-- view_range = 15,
-- owner = "",
-- order = "follow",
-- --order = "stand",
-- fear_height = 3,
-- animation = {
-- speed_normal = 30,
-- speed_run = 30,
-- stand_start = 0,
-- stand_end = 79,
-- walk_start = 168,
-- walk_end = 187,
-- run_start = 168,
-- run_end = 187,
-- punch_start = 200,
-- punch_end = 219,
-- },
-- after_activate = function(self, staticdata, def, dtime)
-- npc.after_activate(self)
-- end,
-- on_rightclick = function(self, clicker)
-- -- Check if right-click interaction is enabled
-- if self.enable_rightclick_interaction == true then
-- npc.rightclick_interaction(self, clicker)
-- end
-- end,
-- do_custom = function(self, dtime)
-- return npc.step(self, dtime)
-- end
--})
-------------------------------------------------------------------------
-- Item definitions
-------------------------------------------------------------------------
--mobs:register_egg("advanced_npc:npc", S("NPC"), "default_brick.png", 1)
-- compatibility
--mobs:alias_mob("mobs:npc", "advanced_npc:npc")
-- Marriage ring
minetest.register_craftitem("advanced_npc:marriage_ring", {
description = S("Marriage Ring"),
inventory_image = "marriage_ring.png",
})
-- Marriage ring craft recipe
minetest.register_craft({
output = "advanced_npc:marriage_ring",
recipe = { {"", "", ""},
{"", "default:diamond", ""},
{"", "default:gold_ingot", ""} },
})