Merge branch 'develop' into docs
commit
03027b513a
@ -1 +1 @@
|
|||||||
Subproject commit a04727cc4ec350399ce4d2ded4425f33c50cea50
|
Subproject commit dc118c5e90aea6181a290e4bbf40e7f2974fb053
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,430 @@
|
|||||||
|
// - full automation of handling mini-pastures over nestboxes:
|
||||||
|
// go through all pens, check if they are empty and placed over a nestbox
|
||||||
|
// find female tame egg-layer who is not assigned to another pen and assign it to nestbox pasture
|
||||||
|
// maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds
|
||||||
|
// state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used)
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "df/building_cagest.h"
|
||||||
|
#include "df/building_civzonest.h"
|
||||||
|
#include "df/building_nest_boxst.h"
|
||||||
|
#include "df/general_ref_building_civzone_assignedst.h"
|
||||||
|
#include "df/world.h"
|
||||||
|
|
||||||
|
#include "Debug.h"
|
||||||
|
#include "LuaTools.h"
|
||||||
|
#include "PluginManager.h"
|
||||||
|
|
||||||
|
#include "modules/Buildings.h"
|
||||||
|
#include "modules/Gui.h"
|
||||||
|
#include "modules/Persistence.h"
|
||||||
|
#include "modules/Units.h"
|
||||||
|
#include "modules/World.h"
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::vector;
|
||||||
|
|
||||||
|
using namespace DFHack;
|
||||||
|
|
||||||
|
DFHACK_PLUGIN("autonestbox");
|
||||||
|
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
|
||||||
|
|
||||||
|
REQUIRE_GLOBAL(world);
|
||||||
|
|
||||||
|
static const string autonestbox_help =
|
||||||
|
"Assigns unpastured female egg-layers to nestbox zones.\n"
|
||||||
|
"Requires that you create pen/pasture zones above nestboxes.\n"
|
||||||
|
"If the pen is bigger than 1x1 the nestbox must be in the top left corner.\n"
|
||||||
|
"Only 1 unit will be assigned per pen, regardless of the size.\n"
|
||||||
|
"The age of the units is currently not checked, most birds grow up quite fast.\n"
|
||||||
|
"Usage:\n"
|
||||||
|
"\n"
|
||||||
|
"enable autonestbox\n"
|
||||||
|
" Start checking for unpastured egg-layers and assigning them to nestbox zones.\n"
|
||||||
|
"autonestbox\n"
|
||||||
|
" Print current status."
|
||||||
|
"autonestbox now\n"
|
||||||
|
" Run a scan and assignment cycle right now. Does not require that the plugin is enabled.\n"
|
||||||
|
"autonestbox ticks <ticks>\n"
|
||||||
|
" Change the number of ticks between scan and assignment cycles when the plugin is enabled.\n"
|
||||||
|
" The default is 6000 (about 8 days)\n";
|
||||||
|
|
||||||
|
namespace DFHack {
|
||||||
|
// for configuration-related logging
|
||||||
|
DBG_DECLARE(autonestbox, status, DebugCategory::LINFO);
|
||||||
|
// for logging during the periodic scan
|
||||||
|
DBG_DECLARE(autonestbox, cycle, DebugCategory::LINFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const string CONFIG_KEY = string(plugin_name) + "/config";
|
||||||
|
static PersistentDataItem config;
|
||||||
|
enum ConfigValues {
|
||||||
|
CONFIG_IS_ENABLED = 0,
|
||||||
|
CONFIG_CYCLE_TICKS = 1,
|
||||||
|
};
|
||||||
|
static int get_config_val(int index) {
|
||||||
|
if (!config.isValid())
|
||||||
|
return -1;
|
||||||
|
return config.ival(index);
|
||||||
|
}
|
||||||
|
static bool get_config_bool(int index) {
|
||||||
|
return get_config_val(index) == 1;
|
||||||
|
}
|
||||||
|
static void set_config_val(int index, int value) {
|
||||||
|
if (config.isValid())
|
||||||
|
config.ival(index) = value;
|
||||||
|
}
|
||||||
|
static void set_config_bool(int index, bool value) {
|
||||||
|
set_config_val(index, value ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool did_complain = false; // avoids message spam
|
||||||
|
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
|
||||||
|
|
||||||
|
static command_result df_autonestbox(color_ostream &out, vector<string> ¶meters);
|
||||||
|
static void autonestbox_cycle(color_ostream &out);
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
|
||||||
|
commands.push_back(PluginCommand(
|
||||||
|
plugin_name,
|
||||||
|
"Auto-assign egg-laying female pets to nestbox zones.",
|
||||||
|
df_autonestbox,
|
||||||
|
false,
|
||||||
|
autonestbox_help.c_str()));
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
|
||||||
|
if (!Core::getInstance().isWorldLoaded()) {
|
||||||
|
out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable != is_enabled) {
|
||||||
|
is_enabled = enable;
|
||||||
|
DEBUG(status,out).print("%s from the API; persisting\n",
|
||||||
|
is_enabled ? "enabled" : "disabled");
|
||||||
|
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
|
||||||
|
} else {
|
||||||
|
DEBUG(status,out).print("%s from the API, but already %s; no action\n",
|
||||||
|
is_enabled ? "enabled" : "disabled",
|
||||||
|
is_enabled ? "enabled" : "disabled");
|
||||||
|
}
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_load_data (color_ostream &out) {
|
||||||
|
config = World::GetPersistentData(CONFIG_KEY);
|
||||||
|
|
||||||
|
if (!config.isValid()) {
|
||||||
|
DEBUG(status,out).print("no config found in this save; initializing\n");
|
||||||
|
config = World::AddPersistentData(CONFIG_KEY);
|
||||||
|
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
|
||||||
|
set_config_val(CONFIG_CYCLE_TICKS, 6000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to copy our enabled flag into the global plugin variable, but
|
||||||
|
// all the other state we can directly read/modify from the persistent
|
||||||
|
// data structure.
|
||||||
|
is_enabled = get_config_bool(CONFIG_IS_ENABLED);
|
||||||
|
DEBUG(status,out).print("loading persisted enabled state: %s\n",
|
||||||
|
is_enabled ? "true" : "false");
|
||||||
|
did_complain = false;
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
|
||||||
|
if (event == DFHack::SC_WORLD_UNLOADED) {
|
||||||
|
if (is_enabled) {
|
||||||
|
DEBUG(status,out).print("world unloaded; disabling %s\n",
|
||||||
|
plugin_name);
|
||||||
|
is_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
|
||||||
|
if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS))
|
||||||
|
autonestbox_cycle(out);
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
// configuration interface
|
||||||
|
//
|
||||||
|
|
||||||
|
struct autonestbox_options {
|
||||||
|
// whether to display help
|
||||||
|
bool help = false;
|
||||||
|
|
||||||
|
// whether to run a cycle right now
|
||||||
|
bool now = false;
|
||||||
|
|
||||||
|
// how many ticks to wait between automatic cycles, -1 means unset
|
||||||
|
int32_t ticks = -1;
|
||||||
|
|
||||||
|
static struct_identity _identity;
|
||||||
|
};
|
||||||
|
static const struct_field_info autonestbox_options_fields[] = {
|
||||||
|
{ struct_field_info::PRIMITIVE, "help", offsetof(autonestbox_options, help), &df::identity_traits<bool>::identity, 0, 0 },
|
||||||
|
{ struct_field_info::PRIMITIVE, "now", offsetof(autonestbox_options, now), &df::identity_traits<bool>::identity, 0, 0 },
|
||||||
|
{ struct_field_info::PRIMITIVE, "ticks", offsetof(autonestbox_options, ticks), &df::identity_traits<int32_t>::identity, 0, 0 },
|
||||||
|
{ struct_field_info::END }
|
||||||
|
};
|
||||||
|
struct_identity autonestbox_options::_identity(sizeof(autonestbox_options), &df::allocator_fn<autonestbox_options>, NULL, "autonestbox_options", NULL, autonestbox_options_fields);
|
||||||
|
|
||||||
|
static bool get_options(color_ostream &out,
|
||||||
|
autonestbox_options &opts,
|
||||||
|
const vector<string> ¶meters)
|
||||||
|
{
|
||||||
|
auto L = Lua::Core::State;
|
||||||
|
Lua::StackUnwinder top(L);
|
||||||
|
|
||||||
|
if (!lua_checkstack(L, parameters.size() + 2) ||
|
||||||
|
!Lua::PushModulePublic(
|
||||||
|
out, L, "plugins.autonestbox", "parse_commandline")) {
|
||||||
|
out.printerr("Failed to load autonestbox Lua code\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lua::Push(L, &opts);
|
||||||
|
for (const string ¶m : parameters)
|
||||||
|
Lua::Push(L, param);
|
||||||
|
|
||||||
|
if (!Lua::SafeCall(out, L, parameters.size() + 1, 0))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static command_result df_autonestbox(color_ostream &out, vector<string> ¶meters) {
|
||||||
|
CoreSuspender suspend;
|
||||||
|
|
||||||
|
if (!Core::getInstance().isWorldLoaded()) {
|
||||||
|
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
|
||||||
|
return CR_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
autonestbox_options opts;
|
||||||
|
if (!get_options(out, opts, parameters) || opts.help)
|
||||||
|
return CR_WRONG_USAGE;
|
||||||
|
|
||||||
|
if (opts.ticks > -1) {
|
||||||
|
set_config_val(CONFIG_CYCLE_TICKS, opts.ticks);
|
||||||
|
INFO(status,out).print("New cycle timer: %d ticks.\n", opts.ticks);
|
||||||
|
}
|
||||||
|
else if (opts.now) {
|
||||||
|
autonestbox_cycle(out);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
out << "autonestbox is " << (is_enabled ? "" : "not ") << "running" << endl;
|
||||||
|
}
|
||||||
|
return CR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
// cycle logic
|
||||||
|
//
|
||||||
|
|
||||||
|
static bool isEmptyPasture(df::building *building) {
|
||||||
|
if (!Buildings::isPenPasture(building))
|
||||||
|
return false;
|
||||||
|
df::building_civzonest *civ = (df::building_civzonest *)building;
|
||||||
|
return (civ->assigned_units.size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isFreeNestboxAtPos(int32_t x, int32_t y, int32_t z) {
|
||||||
|
for (auto building : world->buildings.all) {
|
||||||
|
if (building->getType() == df::building_type::NestBox
|
||||||
|
&& building->x1 == x
|
||||||
|
&& building->y1 == y
|
||||||
|
&& building->z == z) {
|
||||||
|
df::building_nest_boxst *nestbox = (df::building_nest_boxst *)building;
|
||||||
|
if (nestbox->claimed_by == -1 && nestbox->contained_items.size() == 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static df::building* findFreeNestboxZone() {
|
||||||
|
for (auto building : world->buildings.all) {
|
||||||
|
if (isEmptyPasture(building) &&
|
||||||
|
Buildings::isActive(building) &&
|
||||||
|
isFreeNestboxAtPos(building->x1, building->y1, building->z)) {
|
||||||
|
return building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isInBuiltCage(df::unit *unit) {
|
||||||
|
for (auto building : world->buildings.all) {
|
||||||
|
if (building->getType() == df::building_type::Cage) {
|
||||||
|
df::building_cagest* cage = (df::building_cagest *)building;
|
||||||
|
for (auto unitid : cage->assigned_units) {
|
||||||
|
if (unitid == unit->id)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if assigned to pen, pit, (built) cage or chain
|
||||||
|
// note: BUILDING_CAGED is not set for animals (maybe it's used for dwarves who get caged as sentence)
|
||||||
|
// animals in cages (no matter if built or on stockpile) get the ref CONTAINED_IN_ITEM instead
|
||||||
|
// removing them from cages on stockpiles is no problem even without clearing the ref
|
||||||
|
// and usually it will be desired behavior to do so.
|
||||||
|
static bool isAssigned(df::unit *unit) {
|
||||||
|
for (auto ref : unit->general_refs) {
|
||||||
|
auto rtype = ref->getType();
|
||||||
|
if(rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED
|
||||||
|
|| rtype == df::general_ref_type::BUILDING_CAGED
|
||||||
|
|| rtype == df::general_ref_type::BUILDING_CHAIN
|
||||||
|
|| (rtype == df::general_ref_type::CONTAINED_IN_ITEM && isInBuiltCage(unit))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isFreeEgglayer(df::unit *unit)
|
||||||
|
{
|
||||||
|
return Units::isActive(unit) && !Units::isUndead(unit)
|
||||||
|
&& Units::isFemale(unit)
|
||||||
|
&& Units::isTame(unit)
|
||||||
|
&& Units::isOwnCiv(unit)
|
||||||
|
&& Units::isEggLayer(unit)
|
||||||
|
&& !isAssigned(unit)
|
||||||
|
&& !Units::isGrazer(unit) // exclude grazing birds because they're messy
|
||||||
|
&& !Units::isMerchant(unit) // don't steal merchant mounts
|
||||||
|
&& !Units::isForest(unit); // don't steal birds from traders, they hate that
|
||||||
|
}
|
||||||
|
|
||||||
|
static df::unit * findFreeEgglayer() {
|
||||||
|
for (auto unit : world->units.all) {
|
||||||
|
if (isFreeEgglayer(unit))
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static df::general_ref_building_civzone_assignedst * createCivzoneRef() {
|
||||||
|
static bool vt_initialized = false;
|
||||||
|
|
||||||
|
// after having run successfully for the first time it's safe to simply create the object
|
||||||
|
if (vt_initialized) {
|
||||||
|
return (df::general_ref_building_civzone_assignedst *)
|
||||||
|
df::general_ref_building_civzone_assignedst::_identity.instantiate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// being called for the first time, need to initialize the vtable
|
||||||
|
for (auto creature : world->units.all) {
|
||||||
|
for (auto ref : creature->general_refs) {
|
||||||
|
if (ref->getType() == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED) {
|
||||||
|
if (strict_virtual_cast<df::general_ref_building_civzone_assignedst>(ref)) {
|
||||||
|
vt_initialized = true;
|
||||||
|
// !! calling new() doesn't work, need _identity.instantiate() instead !!
|
||||||
|
return (df::general_ref_building_civzone_assignedst *)
|
||||||
|
df::general_ref_building_civzone_assignedst::_identity.instantiate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building *building) {
|
||||||
|
// try to get a fresh civzone ref
|
||||||
|
df::general_ref_building_civzone_assignedst *ref = createCivzoneRef();
|
||||||
|
if (!ref) {
|
||||||
|
ERR(cycle,out).print("Could not find a clonable activity zone reference!"
|
||||||
|
" You need to manually pen/pasture/pit at least one creature"
|
||||||
|
" before autonestbox can function.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref->building_id = building->id;
|
||||||
|
unit->general_refs.push_back(ref);
|
||||||
|
|
||||||
|
df::building_civzonest *civz = (df::building_civzonest *)building;
|
||||||
|
civz->assigned_units.push_back(unit->id);
|
||||||
|
|
||||||
|
INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n",
|
||||||
|
unit->id, Units::getRaceName(unit).c_str(),
|
||||||
|
building->id, building->name.c_str());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t countFreeEgglayers() {
|
||||||
|
size_t count = 0;
|
||||||
|
for (auto unit : world->units.all) {
|
||||||
|
if (isFreeEgglayer(unit))
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t assign_nestboxes(color_ostream &out) {
|
||||||
|
size_t processed = 0;
|
||||||
|
df::building *free_building = NULL;
|
||||||
|
df::unit *free_unit = NULL;
|
||||||
|
do {
|
||||||
|
free_building = findFreeNestboxZone();
|
||||||
|
free_unit = findFreeEgglayer();
|
||||||
|
if (free_building && free_unit) {
|
||||||
|
if (!assignUnitToZone(out, free_unit, free_building)) {
|
||||||
|
DEBUG(cycle,out).print("Failed to assign unit to building.\n");
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
DEBUG(cycle,out).print("assigned unit %d to zone %d\n",
|
||||||
|
free_unit->id, free_building->id);
|
||||||
|
++processed;
|
||||||
|
}
|
||||||
|
} while (free_unit && free_building);
|
||||||
|
|
||||||
|
if (free_unit && !free_building) {
|
||||||
|
static size_t old_count = 0;
|
||||||
|
size_t freeEgglayers = countFreeEgglayers();
|
||||||
|
// avoid spamming the same message
|
||||||
|
if (old_count != freeEgglayers)
|
||||||
|
did_complain = false;
|
||||||
|
old_count = freeEgglayers;
|
||||||
|
if (!did_complain) {
|
||||||
|
stringstream ss;
|
||||||
|
ss << freeEgglayers;
|
||||||
|
string announce = "Not enough free nestbox zones found! You need " + ss.str() + " more.";
|
||||||
|
Gui::showAnnouncement(announce, 6, true);
|
||||||
|
out << announce << endl;
|
||||||
|
did_complain = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void autonestbox_cycle(color_ostream &out) {
|
||||||
|
// mark that we have recently run
|
||||||
|
cycle_timestamp = world->frame_counter;
|
||||||
|
|
||||||
|
DEBUG(cycle,out).print("running autonestbox cycle\n");
|
||||||
|
|
||||||
|
size_t processed = assign_nestboxes(out);
|
||||||
|
if (processed > 0) {
|
||||||
|
stringstream ss;
|
||||||
|
ss << processed << " nestboxes were assigned.";
|
||||||
|
string announce = ss.str();
|
||||||
|
DEBUG(cycle,out).print("%s\n", announce.c_str());
|
||||||
|
Gui::showAnnouncement(announce, 2, false);
|
||||||
|
out << announce << endl;
|
||||||
|
// can complain again
|
||||||
|
// (might lead to spamming the same message twice, but catches the case
|
||||||
|
// where for example 2 new egglayers hatched right after 2 zones were created and assigned)
|
||||||
|
did_complain = false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
local _ENV = mkmodule('plugins.autobutcher')
|
||||||
|
|
||||||
|
local argparse = require('argparse')
|
||||||
|
|
||||||
|
local function is_int(val)
|
||||||
|
return val and val == math.floor(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_positive_int(val)
|
||||||
|
return is_int(val) and val > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check_nonnegative_int(str)
|
||||||
|
local val = tonumber(str)
|
||||||
|
if is_positive_int(val) or val == 0 then return val end
|
||||||
|
qerror('expecting a non-negative integer, but got: '..tostring(str))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function process_args(opts, args)
|
||||||
|
if args[1] == 'help' then
|
||||||
|
opts.help = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return argparse.processArgsGetopt(args, {
|
||||||
|
{'h', 'help', handler=function() opts.help = true end},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function process_races(opts, races, start_idx)
|
||||||
|
if #races < start_idx then
|
||||||
|
qerror('missing list of races (or "all" or "new" keywords)')
|
||||||
|
end
|
||||||
|
for i=start_idx,#races do
|
||||||
|
local race = races[i]
|
||||||
|
if race == 'all' then
|
||||||
|
opts.races_all = true
|
||||||
|
elseif race == 'new' then
|
||||||
|
opts.races_new = true
|
||||||
|
else
|
||||||
|
local str = df.new('string')
|
||||||
|
str.value = race
|
||||||
|
opts.races:insert('#', str)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function parse_commandline(opts, ...)
|
||||||
|
local positionals = process_args(opts, {...})
|
||||||
|
|
||||||
|
local command = positionals[1]
|
||||||
|
if command then opts.command = command end
|
||||||
|
|
||||||
|
if opts.help or not command or command == 'now' or
|
||||||
|
command == 'autowatch' or command == 'noautowatch' or
|
||||||
|
command == 'list' or command == 'list_export' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command == 'watch' or command == 'unwatch' or command == 'forget' then
|
||||||
|
process_races(opts, positionals, 2)
|
||||||
|
elseif command == 'target' then
|
||||||
|
opts.fk = check_nonnegative_int(positionals[2])
|
||||||
|
opts.mk = check_nonnegative_int(positionals[3])
|
||||||
|
opts.fa = check_nonnegative_int(positionals[4])
|
||||||
|
opts.ma = check_nonnegative_int(positionals[5])
|
||||||
|
process_races(opts, positionals, 6)
|
||||||
|
elseif command == 'ticks' then
|
||||||
|
local ticks = tonumber(positionals[2])
|
||||||
|
if not is_positive_int(arg) then
|
||||||
|
qerror('number of ticks must be a positive integer: ' .. ticks)
|
||||||
|
else
|
||||||
|
opts.ticks = ticks
|
||||||
|
end
|
||||||
|
else
|
||||||
|
qerror(('unrecognized command: "%s"'):format(command))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV
|
@ -0,0 +1,51 @@
|
|||||||
|
local _ENV = mkmodule('plugins.autonestbox')
|
||||||
|
|
||||||
|
local argparse = require('argparse')
|
||||||
|
|
||||||
|
local function is_int(val)
|
||||||
|
return val and val == math.floor(val)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_positive_int(val)
|
||||||
|
return is_int(val) and val > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function process_args(opts, args)
|
||||||
|
if args[1] == 'help' then
|
||||||
|
opts.help = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return argparse.processArgsGetopt(args, {
|
||||||
|
{'h', 'help', handler=function() opts.help = true end},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function parse_commandline(opts, ...)
|
||||||
|
local positionals = process_args(opts, {...})
|
||||||
|
|
||||||
|
if opts.help then return end
|
||||||
|
|
||||||
|
local in_ticks = false
|
||||||
|
for _,arg in ipairs(positionals) do
|
||||||
|
if in_ticks then
|
||||||
|
arg = tonumber(arg)
|
||||||
|
if not is_positive_int(arg) then
|
||||||
|
qerror('number of ticks must be a positive integer: ' .. arg)
|
||||||
|
else
|
||||||
|
opts.ticks = arg
|
||||||
|
end
|
||||||
|
in_ticks = false
|
||||||
|
elseif arg == 'ticks' then
|
||||||
|
in_ticks = true
|
||||||
|
elseif arg == 'now' then
|
||||||
|
opts.now = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if in_ticks then
|
||||||
|
qerror('missing number of ticks')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV
|
@ -1,12 +0,0 @@
|
|||||||
local _ENV = mkmodule('plugins.zone')
|
|
||||||
|
|
||||||
--[[
|
|
||||||
|
|
||||||
Native functions:
|
|
||||||
|
|
||||||
* autobutcher_isEnabled()
|
|
||||||
* autowatch_isEnabled()
|
|
||||||
|
|
||||||
--]]
|
|
||||||
|
|
||||||
return _ENV
|
|
File diff suppressed because it is too large
Load Diff
@ -1 +1 @@
|
|||||||
Subproject commit b9d49dfc36dae9b8b1bf91c2a8e8179ca554abdc
|
Subproject commit 6a66be4facd1d5a7b3ab4cb61c34c678d19e0795
|
Loading…
Reference in New Issue