Merge pull request #580 from lethosor/lua-enable

Allow Lua scripts to implement enable/disable commands and specify other behavior (e.g. "module mode") that they implement
develop
Lethosor 2015-05-09 09:27:40 -04:00
commit aa7297b7af
7 changed files with 163 additions and 26 deletions

@ -3457,6 +3457,30 @@ Note that this function lets errors propagate to the caller.
This will not be an issue in most cases. This will not be an issue in most cases.
This function also permits circular dependencies of scripts. This function also permits circular dependencies of scripts.
* ``dfhack.reqscript(name)`` or ``reqscript(name)``
Nearly identical to script_environment() but requires scripts being loaded to
include a line similar to::
--@ module = true
This is intended to only allow scripts that take appropriate action when used
as a module to be loaded.
Enabling and disabling scripts
==============================
Scripts can choose to recognize the built-in ``enable`` and ``disable`` commands
by including the following line anywhere in their file::
--@ enable = true
When the ``enable`` and ``disable`` commands are invoked, a ``dfhack_flags``
table will be passed to the script with the following fields set:
* ``enable``: Always true if the script is being enabled *or* disabled
* ``enable_state``: True if the script is being enabled, false otherwise
Save init script Save init script
================ ================

@ -4,6 +4,8 @@ DFHack Future
Developer plugins can be ignored on startup by setting the DFHACK_NO_DEV_PLUGINS environment variable Developer plugins can be ignored on startup by setting the DFHACK_NO_DEV_PLUGINS environment variable
The console on Linux and OS X now recognizes keyboard input between prompts The console on Linux and OS X now recognizes keyboard input between prompts
Lua Lua
Scripts can be enabled with the built-in enable/disable commands
A new function, reqscript(), is available as a safer alternative to script_environment()
New internal commands New internal commands
New plugins New plugins
New scripts New scripts

@ -288,6 +288,10 @@ namespace {
const string *pcmd; const string *pcmd;
vector<string> *pargs; vector<string> *pargs;
}; };
struct ScriptEnableState {
const string *pcmd;
bool pstate;
};
} }
static bool init_run_script(color_ostream &out, lua_State *state, void *info) static bool init_run_script(color_ostream &out, lua_State *state, void *info)
@ -315,6 +319,30 @@ static command_result runLuaScript(color_ostream &out, std::string name, vector<
return ok ? CR_OK : CR_FAILURE; return ok ? CR_OK : CR_FAILURE;
} }
static bool init_enable_script(color_ostream &out, lua_State *state, void *info)
{
auto args = (ScriptEnableState*)info;
if (!lua_checkstack(state, 4))
return false;
Lua::PushDFHack(state);
lua_getfield(state, -1, "enable_script");
lua_remove(state, -2);
lua_pushstring(state, args->pcmd->c_str());
lua_pushboolean(state, args->pstate);
return true;
}
static command_result enableLuaScript(color_ostream &out, std::string name, bool state)
{
ScriptEnableState data;
data.pcmd = &name;
data.pstate = state;
bool ok = Lua::RunCoreQueryLoop(out, Lua::Core::State, init_enable_script, &data);
return ok ? CR_OK : CR_FAILURE;
}
static command_result runRubyScript(color_ostream &out, PluginManager *plug_mgr, std::string name, vector<string> &args) static command_result runRubyScript(color_ostream &out, PluginManager *plug_mgr, std::string name, vector<string> &args)
{ {
if (!plug_mgr->ruby || !plug_mgr->ruby->is_enabled()) if (!plug_mgr->ruby || !plug_mgr->ruby->is_enabled())
@ -633,24 +661,43 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
for (size_t i = 0; i < parts.size(); i++) for (size_t i = 0; i < parts.size(); i++)
{ {
Plugin * plug = plug_mgr->getPluginByName(parts[i]); std::string part = parts[i];
if (part.find('\\') != std::string::npos)
{
con.printerr("Replacing backslashes with forward slashes in \"%s\"\n", part.c_str());
for (size_t j = 0; j < part.size(); j++)
{
if (part[j] == '\\')
part[j] = '/';
}
}
Plugin * plug = plug_mgr->getPluginByName(part);
if(!plug) if(!plug)
{ {
res = CR_NOT_FOUND; std::string lua = findScript(this->p->getPath(), part + ".lua");
con.printerr("No such plugin: %s\n", parts[i].c_str()); if (lua.size())
{
res = enableLuaScript(con, part, enable);
}
else
{
res = CR_NOT_FOUND;
con.printerr("No such plugin or Lua script: %s\n", part.c_str());
}
} }
else if (!plug->can_set_enabled()) else if (!plug->can_set_enabled())
{ {
res = CR_NOT_IMPLEMENTED; res = CR_NOT_IMPLEMENTED;
con.printerr("Cannot %s plugin: %s\n", first.c_str(), parts[i].c_str()); con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str());
} }
else else
{ {
res = plug->set_enabled(con, enable); res = plug->set_enabled(con, enable);
if (res != CR_OK || plug->is_enabled() != enable) if (res != CR_OK || plug->is_enabled() != enable)
con.printerr("Could not %s plugin: %s\n", first.c_str(), parts[i].c_str()); con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str());
} }
} }

@ -386,15 +386,33 @@ end
local internal = dfhack.internal local internal = dfhack.internal
internal.scripts = internal.scripts or {} Script = defclass(Script)
internal.scriptPath = internal.scriptPath or {} function Script:init(path)
internal.scriptMtime = internal.scriptMtime or {} self.path = path
internal.scriptRun = internal.scriptRun or {} self.mtime = dfhack.filesystem.mtime(path)
self._flags = {}
end
function Script:get_flags()
if self.flags_mtime ~= dfhack.filesystem.mtime(self.path) then
self.flags_mtime = dfhack.filesystem.mtime(self.path)
self._flags = {}
local f = io.open(self.path)
local contents = f:read('*all')
f:close()
for line in contents:gmatch('%-%-@([^\n]+)') do
local chunk = load(line, self.path, 't', self._flags)
if chunk then
chunk()
else
dfhack.printerr('Parse error: ' .. line)
end
end
end
return self._flags
end
internal.scripts = internal.scripts or {}
local scripts = internal.scripts local scripts = internal.scripts
local scriptPath = internal.scriptPath
local scriptMtime = internal.scriptMtime
local scriptRun = internal.scriptRun
local hack_path = dfhack.getHackPath() local hack_path = dfhack.getHackPath()
@ -419,28 +437,69 @@ function dfhack.findScript(name)
return nil return nil
end end
local valid_script_flags = {
enable = {required = true, error = 'Does not recognize enable/disable commands'},
enable_state = {required = false},
module = {
required = function(flags)
if flags.module_strict == false then return false end
return true
end,
error = 'Cannot be used as a module'
},
module_strict = {required = false},
}
function dfhack.run_script(name,...) function dfhack.run_script(name,...)
return dfhack.run_script_with_env(nil,name,...) return dfhack.run_script_with_env(nil, name, nil, ...)
end end
function dfhack.enable_script(name, state)
local res, err = dfhack.pcall(dfhack.run_script_with_env, nil, name, {enable=true, enable_state=state})
if not res then
dfhack.printerr(err.message)
qerror(('Cannot %s Lua script: %s'):format(state and 'enable' or 'disable', name))
end
end
function dfhack.reqscript(name)
_, env = dfhack.run_script_with_env(nil, name, {module=true})
return env
end
reqscript = dfhack.reqscript
function dfhack.script_environment(name) function dfhack.script_environment(name)
_, env = dfhack.run_script_with_env({moduleMode=true}, name) _, env = dfhack.run_script_with_env(nil, name, {module=true, module_strict=false})
return env return env
end end
function dfhack.run_script_with_env(envVars,name,...) function dfhack.run_script_with_env(envVars, name, flags, ...)
if type(flags) ~= 'table' then flags = {} end
local file = dfhack.findScript(name) local file = dfhack.findScript(name)
if not file then if not file then
error('Could not find script ' .. name) error('Could not find script ' .. name)
end end
if scriptPath[name] and scriptPath[name] ~= file then
--new file path: must have loaded a different save or unloaded if scripts[file] == nil then
scriptPath[name] = file scripts[file] = Script(file)
scriptMtime[file] = dfhack.filesystem.mtime(file) end
--it is the responsibility of the script to clear its own data on unload so it's safe for us to not delete it here local script_flags = scripts[file]:get_flags()
for flag, value in pairs(flags) do
if value then
local v = valid_script_flags[flag]
if not v then
error('Invalid flag: ' .. flag)
elseif ((type(v.required) == 'boolean' and v.required) or
(type(v.required) == 'function' and v.required(flags))) then
if not script_flags[flag] then
local msg = v.error or 'Flag "' .. flag .. '" not recognized'
error(name .. ': ' .. msg)
end
end
end
end end
local env = scripts[file] local env = scripts[file].env
if env == nil then if env == nil then
env = {} env = {}
setmetatable(env, { __index = base_env }) setmetatable(env, { __index = base_env })
@ -448,11 +507,13 @@ function dfhack.run_script_with_env(envVars,name,...)
for x,y in pairs(envVars or {}) do for x,y in pairs(envVars or {}) do
env[x] = y env[x] = y
end end
env.dfhack_flags = flags
env.moduleMode = flags.module
local f local f
local perr local perr
local time = dfhack.filesystem.mtime(file) local time = dfhack.filesystem.mtime(file)
if time == scriptMtime[file] and scriptRun[file] then if time == scripts[file].mtime and scripts[file].run then
f = scriptRun[file] f = scripts[file].run
else else
--reload --reload
f, perr = loadfile(file, 't', env) f, perr = loadfile(file, 't', env)
@ -460,10 +521,10 @@ function dfhack.run_script_with_env(envVars,name,...)
error(perr) error(perr)
end end
-- avoid updating mtime if the script failed to load -- avoid updating mtime if the script failed to load
scriptMtime[file] = time scripts[file].mtime = time
end end
scripts[file] = env scripts[file].env = env
scriptRun[file] = f scripts[file].run = f
return f(...), env return f(...), env
end end

@ -1,4 +1,5 @@
-- Adds emotions to creatures. -- Adds emotions to creatures.
--@ module = true
local utils=require('utils') local utils=require('utils')

@ -1,6 +1,7 @@
-- scripts/modtools/reaction-product-trigger.lua -- scripts/modtools/reaction-product-trigger.lua
-- author expwnent -- author expwnent
-- trigger commands just before and after custom reactions produce items -- trigger commands just before and after custom reactions produce items
--@ module = true
local eventful = require 'plugins.eventful' local eventful = require 'plugins.eventful'
local utils = require 'utils' local utils = require 'utils'

@ -1,6 +1,7 @@
-- scripts/modtools/reaction-trigger.lua -- scripts/modtools/reaction-trigger.lua
-- author expwnent -- author expwnent
-- replaces autoSyndrome: trigger commands when custom reactions are completed -- replaces autoSyndrome: trigger commands when custom reactions are completed
--@ module = true
local eventful = require 'plugins.eventful' local eventful = require 'plugins.eventful'
local syndromeUtil = require 'syndrome-util' local syndromeUtil = require 'syndrome-util'