update seedwatch

develop
Myk Taylor 2023-01-09 17:31:49 -08:00
parent 0ff7ad0cc4
commit be40d55e64
No known key found for this signature in database
7 changed files with 452 additions and 362 deletions

@ -5,42 +5,47 @@ seedwatch
:summary: Manages seed and plant cooking based on seed stock levels. :summary: Manages seed and plant cooking based on seed stock levels.
:tags: fort auto plants :tags: fort auto plants
Each seed type can be assigned a target. If the number of seeds of that type Unlike brewing and other kinds of processing, cooking plants does not produce
falls below that target, then the plants and seeds of that type will be excluded a usable seed. By default, all plants are allowed to be cooked. This often leads
from cookery. If the number rises above the target + 20, then cooking will be to the situation where dwarves have no seeds left to plant crops with because
allowed. they cooked all the relevant plants. Seedwatch protects you from this problem.
The plugin needs a fortress to be loaded and will deactivate automatically Each seed type can be assigned a target stock amount. If the number of seeds of
otherwise. You have to reactivate with ``enable seedwatch`` after you load a that type falls below that target, then the plants and seeds of that type will
fort. be protected from cookery. If the number rises above the target + 20, then
cooking will be allowed again.
Usage Usage
----- -----
``enable seedwatch`` ``enable seedwatch``
Start managing seed and plant cooking. By default, no types are watched. Start managing seed and plant cooking. By default, all types are watched
You have to add them with further ``seedwatch`` commands. with a target of ``30``, but you can adjust the list or even
``seedwatch clear`` it and start your own if you like.
``seedwatch [status]``
Display whether seedwatch is enabled and prints out the watch list, along
with the current seed counts.
``seedwatch <type> <target>`` ``seedwatch <type> <target>``
Adds the specified type to the watchlist (if it's not already there) and Adds the specified type to the watchlist (if it's not already there) and
sets the target number of seeds to the specified number. You can pass the sets the target number of seeds to the specified number. You can pass the
keyword ``all`` instead of a specific type to set the target for all types. keyword ``all`` instead of a specific type to set the target for all types.
``seedwatch <type>`` If you set the target to ``0``, it removes the specified type from the
Removes the specified type from the watch list. watch list.
``seedwatch clear`` ``seedwatch clear``
Clears all types from the watch list. Clears all types from the watch list. Same as ``seedwatch all 0``.
``seedwatch info``
Display whether seedwatch is enabled and prints out the watch list.
To print out a list of all plant types, you can run this command:: To see a list of all plant types that you might want to set targets for, you can
run this command::
devel/query --table df.global.world.raws.plants.all --search ^id --maxdepth 1 devel/query --table df.global.world.raws.plants.all --search ^id --maxdepth 1
Examples Examples
-------- --------
``seedwatch all 30`` ``enable seedwatch``
Adds all seeds to the watch list and sets the targets to 30. Adds all seeds to the watch list, sets the targets to 30, and starts
monitoring your seed stock levels.
``seedwatch MUSHROOM_HELMET_PLUMP 50`` ``seedwatch MUSHROOM_HELMET_PLUMP 50``
Add Plump Helmets to the watch list and sets the target to 50. Add Plump Helmets to the watch list and sets the target to 50.
``seedwatch MUSHROOM_HELMET_PLUMP`` ``seedwatch MUSHROOM_HELMET_PLUMP 0``
removes Plump Helmets from the watch list. removes Plump Helmets from the watch list.

@ -30,6 +30,7 @@ distribution.
#include <vector> #include <vector>
#include <map> #include <map>
#include <type_traits> #include <type_traits>
#include <unordered_map>
#include "df/interfacest.h" #include "df/interfacest.h"
@ -378,6 +379,13 @@ namespace DFHack {namespace Lua {
TableInsert(L, entry.first, entry.second); TableInsert(L, entry.first, entry.second);
} }
template<typename T_Key, typename T_Value>
void Push(lua_State *L, const std::unordered_map<T_Key, T_Value> &pmap) {
lua_createtable(L, 0, pmap.size());
for (auto &entry : pmap)
TableInsert(L, entry.first, entry.second);
}
DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true); DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true);
DFHACK_EXPORT bool IsCoreContext(lua_State *state); DFHACK_EXPORT bool IsCoreContext(lua_State *state);

@ -27,11 +27,6 @@ distribution.
* kitchen settings * kitchen settings
*/ */
#include "Export.h" #include "Export.h"
#include "Module.h"
#include "Types.h"
#include "VersionInfo.h"
#include "Core.h"
#include "modules/Items.h"
#include "df/kitchen_exc_type.h" #include "df/kitchen_exc_type.h"
/** /**
@ -43,38 +38,21 @@ namespace DFHack
{ {
namespace Kitchen namespace Kitchen
{ {
// print the exclusion list, with the material index also translated into its token (for organics) - for debug really
DFHACK_EXPORT void debug_print(color_ostream &out);
/** /**
* Kitchen exclusions manipulator. Currently geared towards plants and seeds. * Kitchen exclusions manipulator. Currently geared towards plants and seeds.
* \ingroup grp_modules * \ingroup grp_modules
* \ingroup grp_kitchen * \ingroup grp_kitchen
*/ */
// print the exclusion list, with the material index also translated into its token (for organics) - for debug really
DFHACK_EXPORT void debug_print(color_ostream &out);
// remove this plant from the exclusion list if it is in it // remove this plant from the exclusion list if it is in it
DFHACK_EXPORT void allowPlantSeedCookery(int32_t plant_id); DFHACK_EXPORT void allowPlantSeedCookery(int32_t plant_id);
// add this plant to the exclusion list, if it is not already in it // add this plant to the exclusion list, if it is not already in it
DFHACK_EXPORT void denyPlantSeedCookery(int32_t plant_id); DFHACK_EXPORT void denyPlantSeedCookery(int32_t plant_id);
// fills a map with info from the limit info storage entries in the exclusion list
DFHACK_EXPORT void fillWatchMap(std::map<int32_t, int16_t>& watchMap);
// Finds the index of a limit info storage entry. Returns -1 if not found.
DFHACK_EXPORT int findLimit(int32_t plant_id);
// removes a limit info storage entry from the exclusion list if it's present
DFHACK_EXPORT bool removeLimit(int32_t plant_id);
// add a limit info storage item to the exclusion list, or alters an existing one
DFHACK_EXPORT bool setLimit(int32_t plant_id, int16_t limit);
// clears all limit info storage items from the exclusion list
DFHACK_EXPORT void clearLimits();
DFHACK_EXPORT std::size_t size();
// Finds the index of a kitchen exclusion in plotinfo.kitchen.exc_types. Returns -1 if not found. // Finds the index of a kitchen exclusion in plotinfo.kitchen.exc_types. Returns -1 if not found.
DFHACK_EXPORT int findExclusion(df::kitchen_exc_type type, DFHACK_EXPORT int findExclusion(df::kitchen_exc_type type,
df::item_type item_type, int16_t item_subtype, df::item_type item_type, int16_t item_subtype,
@ -90,5 +68,7 @@ DFHACK_EXPORT bool removeExclusion(df::kitchen_exc_type type,
df::item_type item_type, int16_t item_subtype, df::item_type item_type, int16_t item_subtype,
int16_t mat_type, int32_t mat_index); int16_t mat_type, int32_t mat_index);
DFHACK_EXPORT std::size_t size();
} }
} }

@ -78,84 +78,6 @@ void Kitchen::denyPlantSeedCookery(int32_t plant_id)
type->material_defs.idx[plant_material_def::basic_mat]); type->material_defs.idx[plant_material_def::basic_mat]);
} }
void Kitchen::fillWatchMap(std::map<int32_t, int16_t>& watchMap)
{
watchMap.clear();
for (std::size_t i = 0; i < size(); ++i)
{
if (plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMTYPE &&
plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE &&
plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE)
{
watchMap[plotinfo->kitchen.mat_indices[i]] = plotinfo->kitchen.mat_types[i];
}
}
}
int Kitchen::findLimit(int32_t plant_id)
{
for (size_t i = 0; i < size(); ++i)
{
if (plotinfo->kitchen.item_types[i] == SEEDLIMIT_ITEMTYPE &&
plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE &&
plotinfo->kitchen.mat_indices[i] == plant_id &&
plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE)
{
return int(i);
}
}
return -1;
}
bool Kitchen::removeLimit(int32_t plant_id)
{
int i = findLimit(plant_id);
if (i < 0)
return false;
plotinfo->kitchen.item_types.erase(plotinfo->kitchen.item_types.begin() + i);
plotinfo->kitchen.item_subtypes.erase(plotinfo->kitchen.item_subtypes.begin() + i);
plotinfo->kitchen.mat_types.erase(plotinfo->kitchen.mat_types.begin() + i);
plotinfo->kitchen.mat_indices.erase(plotinfo->kitchen.mat_indices.begin() + i);
plotinfo->kitchen.exc_types.erase(plotinfo->kitchen.exc_types.begin() + i);
return true;
}
bool Kitchen::setLimit(int32_t plant_id, int16_t limit)
{
if (limit > SEEDLIMIT_MAX)
limit = SEEDLIMIT_MAX;
int i = findLimit(plant_id);
if (i < 0)
{
plotinfo->kitchen.item_types.push_back(SEEDLIMIT_ITEMTYPE);
plotinfo->kitchen.item_subtypes.push_back(SEEDLIMIT_ITEMSUBTYPE);
plotinfo->kitchen.mat_types.push_back(limit);
plotinfo->kitchen.mat_indices.push_back(plant_id);
plotinfo->kitchen.exc_types.push_back(SEEDLIMIT_EXCTYPE);
}
else
{
plotinfo->kitchen.mat_types[i] = limit;
}
return true;
}
void Kitchen::clearLimits()
{
for (size_t i = 0; i < size(); ++i)
{
if (plotinfo->kitchen.item_types[i] == SEEDLIMIT_ITEMTYPE &&
plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE &&
plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE)
{
removeLimit(plotinfo->kitchen.mat_indices[i]);
--i;
}
}
}
size_t Kitchen::size() size_t Kitchen::size()
{ {
return plotinfo->kitchen.item_types.size(); return plotinfo->kitchen.item_types.size();

@ -149,7 +149,7 @@ add_subdirectory(remotefortressreader)
#add_subdirectory(rendermax) #add_subdirectory(rendermax)
dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua)
#dfhack_plugin(search search.cpp) #dfhack_plugin(search search.cpp)
dfhack_plugin(seedwatch seedwatch.cpp) dfhack_plugin(seedwatch seedwatch.cpp LINK_LIBRARIES lua)
dfhack_plugin(showmood showmood.cpp) dfhack_plugin(showmood showmood.cpp)
#dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua)
#dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) #dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua)

@ -0,0 +1,67 @@
local _ENV = mkmodule('plugins.seedwatch')
local argparse = require('argparse')
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 print_status()
print(('seedwatch is %s'):format(isEnabled() and "enabled" or "disabled"))
print()
print('usable seed counts and current targets:')
local watch_map, seed_counts = seedwatch_getData()
local sum = 0
local plants = df.global.world.raws.plants.all
for k,v in pairs(seed_counts) do
print((' %4d/%d %s'):format(v, watch_map[k] or 0, plants[k].id))
sum = sum + v
end
print()
print(('total usable seeds: %d'):format(sum))
end
local function set_target(name, num)
if not name or #name == 0 then
qerror('must specify "all" or plant name')
end
num = tonumber(num)
num = num and math.floor(num) or nil
if not num or num < 0 then
qerror('target must be a non-negative integer')
end
seedwatch_setTarget(name, num)
end
function parse_commandline(...)
local args, opts = {...}, {}
local positionals = process_args(opts, args)
if opts.help then
return false
end
local command = positionals[1]
if not command or command == 'status' then
print_status()
elseif command == 'clear' then
set_target('all', 0)
elseif positionals[2] and positionals[3] then
set_target(positionals[2], positionals[3])
else
return false
end
return true
end
return _ENV

@ -4,218 +4,113 @@
// With thanks to peterix for DFHack and Quietust for information // With thanks to peterix for DFHack and Quietust for information
// http://www.bay12forums.com/smf/index.php?topic=91166.msg2605147#msg2605147 // http://www.bay12forums.com/smf/index.php?topic=91166.msg2605147#msg2605147
#include <map> #include "Debug.h"
#include <string> #include "LuaTools.h"
#include <vector>
#include "Console.h"
#include "Core.h"
#include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/World.h" #include "TileTypes.h"
#include "modules/Materials.h"
#include "modules/Kitchen.h" #include "modules/Kitchen.h"
#include "VersionInfo.h" #include "modules/Maps.h"
#include "df/world.h" #include "modules/Persistence.h"
#include "df/plant_raw.h" #include "modules/Units.h"
#include "modules/World.h"
#include "df/item_flags.h" #include "df/item_flags.h"
#include "df/items_other_id.h" #include "df/items_other_id.h"
#include "df/plant_raw.h"
#include "df/world.h"
#include <unordered_map>
using namespace std;
using namespace DFHack; using namespace DFHack;
using namespace df::enums; using namespace df::enums;
using std::map;
using std::string;
using std::unordered_map;
using std::vector;
DFHACK_PLUGIN("seedwatch"); DFHACK_PLUGIN("seedwatch");
DFHACK_PLUGIN_IS_ENABLED(running); // whether seedwatch is counting the seeds or not DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
const int buffer = 20; // seed number buffer - 20 is reasonable namespace DFHack {
// for configuration-related logging
DBG_DECLARE(seedwatch, config, DebugCategory::LINFO);
// for logging during the periodic scan
DBG_DECLARE(seedwatch, cycle, DebugCategory::LINFO);
}
// abbreviations for the standard plants // abbreviations for the standard plants
map<string, string> abbreviations; static unordered_map<string, const char *> abbreviations;
static map<string, int32_t> world_plant_ids;
bool ignoreSeeds(df::item_flags& f) // seeds with the following flags should not be counted static const int DEFAULT_TARGET = 30;
{ static const int TARGET_BUFFER = 20; // seed number buffer; 20 is reasonable
return
f.bits.dump ||
f.bits.forbid ||
f.bits.garbage_collect ||
f.bits.hidden ||
f.bits.hostile ||
f.bits.on_fire ||
f.bits.rotten ||
f.bits.trader ||
f.bits.in_building ||
f.bits.in_job;
};
// searches abbreviations, returns expansion if so, returns original if not static const string CONFIG_KEY = string(plugin_name) + "/config";
string searchAbbreviations(string in) static const string SEED_CONFIG_KEY_PREFIX = string(plugin_name) + "/seed/";
{ static PersistentDataItem config;
if(abbreviations.count(in) > 0) static unordered_map<int, PersistentDataItem> watched_seeds;
{
return abbreviations[in]; enum ConfigValues {
} CONFIG_IS_ENABLED = 0,
else
{
return in;
}
}; };
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) enum SeedConfigValues {
{ SEED_CONFIG_ID = 0,
if(enable == true) SEED_CONFIG_TARGET = 1,
{ };
if(Core::getInstance().isWorldLoaded())
{
running = true;
out.print("seedwatch supervision started.\n");
} else {
out.printerr(
"This plugin needs a fortress to be loaded and will deactivate automatically otherwise.\n"
"Activate with 'seedwatch start' after you load the game.\n"
);
}
} else {
running = false;
out.print("seedwatch supervision stopped.\n");
}
return CR_OK; static int get_config_val(PersistentDataItem &c, int index) {
if (!c.isValid())
return -1;
return c.ival(index);
}
static bool get_config_bool(PersistentDataItem &c, int index) {
return get_config_val(c, index) == 1;
}
static void set_config_val(PersistentDataItem &c, int index, int value) {
if (c.isValid())
c.ival(index) = value;
}
static void set_config_bool(PersistentDataItem &c, int index, bool value) {
set_config_val(c, index, value ? 1 : 0);
} }
command_result df_seedwatch(color_ostream &out, vector<string>& parameters) static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) {
{ if (watched_seeds.count(id))
CoreSuspender suspend; return watched_seeds[id];
string keyname = SEED_CONFIG_KEY_PREFIX + int_to_string(id);
map<string, int32_t> plantIDs; DEBUG(config,out).print("creating new persistent key for seed type %d\n", id);
for(size_t i = 0; i < world->raws.plants.all.size(); ++i) watched_seeds.emplace(id, World::GetPersistentData(keyname, NULL));
{ return watched_seeds[id];
auto & plant = world->raws.plants.all[i]; }
if (plant->material_defs.type[plant_material_def::seed] != -1) static void remove_seed_config(color_ostream &out, int id) {
plantIDs[plant->id] = i; if (!watched_seeds.count(id))
} return;
DEBUG(config,out).print("removing persistent key for seed type %d\n", id);
t_gamemodes gm; World::DeletePersistentData(watched_seeds[id]);
World::ReadGameMode(gm);// FIXME: check return value watched_seeds.erase(id);
}
// if game mode isn't fortress mode
if(gm.g_mode != game_mode::DWARF || !World::isFortressMode(gm.g_type))
{
// just print the help
return CR_WRONG_USAGE;
}
string par; static const int32_t CYCLE_TICKS = 1200;
int limit; static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
switch(parameters.size())
{
case 0:
return CR_WRONG_USAGE;
case 1:
par = parameters[0];
if ((par == "help") || (par == "?"))
{
return CR_WRONG_USAGE;
}
else if(par == "start")
{
plugin_enable(out, true);
} static command_result do_command(color_ostream &out, vector<string> &parameters);
else if(par == "stop") static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds, int32_t *num_disabled_seeds);
{ static void seedwatch_setTarget(color_ostream &out, string name, int32_t num);
plugin_enable(out, false);
}
else if(par == "clear")
{
Kitchen::clearLimits();
out.print("seedwatch watchlist cleared\n");
}
else if(par == "info")
{
out.print("seedwatch Info:\n");
if(running)
{
out.print("seedwatch is supervising. Use 'disable seedwatch' to stop supervision.\n");
}
else
{
out.print("seedwatch is not supervising. Use 'enable seedwatch' to start supervision.\n");
}
map<int32_t, int16_t> watchMap;
Kitchen::fillWatchMap(watchMap);
if(watchMap.empty())
{
out.print("The watch list is empty.\n");
}
else
{
out.print("The watch list is:\n");
for(auto i = watchMap.begin(); i != watchMap.end(); ++i)
{
out.print("%s : %u\n", world->raws.plants.all[i->first]->id.c_str(), i->second);
}
}
}
else if(par == "debug")
{
map<int32_t, int16_t> watchMap;
Kitchen::fillWatchMap(watchMap);
Kitchen::debug_print(out);
}
else
{
string token = searchAbbreviations(par);
if(plantIDs.count(token) > 0)
{
Kitchen::removeLimit(plantIDs[token]);
out.print("%s is not being watched\n", token.c_str());
}
else
{
out.print("%s has not been found as a material.\n", token.c_str());
}
}
break;
case 2:
limit = atoi(parameters[1].c_str());
if(limit < 0) limit = 0;
if(parameters[0] == "all")
{
for(auto & entry : plantIDs)
Kitchen::setLimit(entry.second, limit);
}
else
{
string token = searchAbbreviations(parameters[0]);
if(plantIDs.count(token) > 0)
{
Kitchen::setLimit(plantIDs[token], limit);
out.print("%s is being watched.\n", token.c_str());
}
else
{
out.print("%s has not been found as a material.\n", token.c_str());
}
}
break;
default:
return CR_WRONG_USAGE;
break;
}
return CR_OK; DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
} DEBUG(config,out).print("initializing %s\n", plugin_name);
DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand>& commands) // provide a configuration interface for the plugin
{
commands.push_back(PluginCommand( commands.push_back(PluginCommand(
"seedwatch", plugin_name,
"Toggles seed cooking based on quantity available.", "Automatically toggle seed cooking based on quantity available.",
df_seedwatch)); do_command));
// fill in the abbreviations map, with abbreviations for the standard plants
// fill in the abbreviations map
abbreviations["bs"] = "SLIVER_BARB"; abbreviations["bs"] = "SLIVER_BARB";
abbreviations["bt"] = "TUBER_BLOATED"; abbreviations["bt"] = "TUBER_BLOATED";
abbreviations["bw"] = "WEED_BLADE"; abbreviations["bw"] = "WEED_BLADE";
@ -237,72 +132,285 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginComman
abbreviations["vh"] = "HERB_VALLEY"; abbreviations["vh"] = "HERB_VALLEY";
abbreviations["ws"] = "BERRIES_STRAW"; abbreviations["ws"] = "BERRIES_STRAW";
abbreviations["wv"] = "VINE_WHIP"; abbreviations["wv"] = "VINE_WHIP";
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
{ if (!Core::getInstance().isWorldLoaded()) {
if (event == SC_MAP_UNLOADED) { out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
if (running) return CR_FAILURE;
out.print("seedwatch deactivated due to game unload\n");
running = false;
} }
if (enable != is_enabled) {
is_enabled = enable;
DEBUG(config,out).print("%s from the API; persisting\n",
is_enabled ? "enabled" : "disabled");
set_config_bool(config, CONFIG_IS_ENABLED, is_enabled);
if (enable)
seedwatch_setTarget(out, "all", DEFAULT_TARGET);
} else {
DEBUG(config,out).print("%s from the API, but already %s; no action\n",
is_enabled ? "enabled" : "disabled",
is_enabled ? "enabled" : "disabled");
}
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_onupdate(color_ostream &out) DFhackCExport command_result plugin_shutdown (color_ostream &out) {
{ DEBUG(config,out).print("shutting down %s\n", plugin_name);
if (running)
{ return CR_OK;
// reduce processing rate }
static int counter = 0;
if (++counter < 500) DFhackCExport command_result plugin_load_data (color_ostream &out) {
return CR_OK; world_plant_ids.clear();
counter = 0; for (size_t i = 0; i < world->raws.plants.all.size(); ++i) {
auto & plant = world->raws.plants.all[i];
t_gamemodes gm; if (plant->material_defs.type[plant_material_def::seed] != -1)
World::ReadGameMode(gm);// FIXME: check return value world_plant_ids[plant->id] = i;
// if game mode isn't fortress mode }
if(gm.g_mode != game_mode::DWARF || !World::isFortressMode(gm.g_type))
{ config = World::GetPersistentData(CONFIG_KEY);
// stop running.
running = false; if (!config.isValid()) {
out.printerr("seedwatch deactivated due to game mode switch\n"); DEBUG(config,out).print("no config found in this save; initializing\n");
return CR_OK; config = World::AddPersistentData(CONFIG_KEY);
} set_config_bool(config, CONFIG_IS_ENABLED, is_enabled);
// this is dwarf mode, continue }
map<int32_t, int16_t> seedCount; // the number of seeds
is_enabled = get_config_bool(config, CONFIG_IS_ENABLED);
// count all seeds and plants by RAW material DEBUG(config,out).print("loading persisted enabled state: %s\n",
for(size_t i = 0; i < world->items.other[items_other_id::SEEDS].size(); ++i) is_enabled ? "true" : "false");
{ watched_seeds.clear();
df::item *item = world->items.other[items_other_id::SEEDS][i]; vector<PersistentDataItem> seed_configs;
MaterialInfo mat(item); World::GetPersistentData(&seed_configs, SEED_CONFIG_KEY_PREFIX, true);
if (!mat.isPlant()) const size_t num_seed_configs = seed_configs.size();
continue; for (size_t idx = 0; idx < num_seed_configs; ++idx) {
if (!ignoreSeeds(item->flags)) auto &c = seed_configs[idx];
++seedCount[mat.plant->index]; watched_seeds.emplace(get_config_val(c, SEED_CONFIG_ID), c);
} }
map<int32_t, int16_t> watchMap; return CR_OK;
Kitchen::fillWatchMap(watchMap); }
for(auto i = watchMap.begin(); i != watchMap.end(); ++i)
{ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if(seedCount[i->first] <= i->second) if (event == DFHack::SC_WORLD_UNLOADED) {
{ if (is_enabled) {
Kitchen::denyPlantSeedCookery(i->first); DEBUG(config,out).print("world unloaded; disabling %s\n",
} plugin_name);
else if(i->second + buffer < seedCount[i->first]) is_enabled = false;
{
Kitchen::allowPlantSeedCookery(i->first);
}
} }
} }
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_shutdown(Core* pCore) DFhackCExport command_result plugin_onupdate(color_ostream &out) {
{ if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) {
int32_t num_enabled_seeds, num_disabled_seeds;
do_cycle(out, &num_enabled_seeds, &num_disabled_seeds);
if (0 < num_enabled_seeds)
out.print("%s: enabled %d seed types for cooking\n",
plugin_name, num_enabled_seeds);
if (0 < num_disabled_seeds)
out.print("%s: protected %d seed types from cooking\n",
plugin_name, num_disabled_seeds);
}
return CR_OK; return CR_OK;
} }
static bool call_seedwatch_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(config,*out).print("calling %s lua function: '%s'\n", plugin_name, fn_name);
return Lua::CallLuaModuleFunction(*out, L, "plugins.seedwatch", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
CoreSuspender suspend;
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
bool show_help = false;
if (!call_seedwatch_lua(&out, "parse_commandline", parameters.size(), 1,
[&](lua_State *L) {
for (const string &param : parameters)
Lua::Push(L, param);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
}
return show_help ? CR_WRONG_USAGE : CR_OK;
}
/////////////////////////////////////////////////////
// cycle logic
//
struct BadFlags {
uint32_t whole;
// TODO: maybe don't filter out seeds that are in_building. that would
// allow us to count seeds that are in workshops. are there any negative
// consequences?
BadFlags() {
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(artifact);
F(in_job); F(owned); F(in_chest); F(removed);
F(encased); F(spider_web);
#undef F
whole = flags.whole;
}
};
static bool is_accessible_item(const df::coord &pos, const vector<df::unit *> &citizens) {
for (auto &unit : citizens) {
if (Maps::canWalkBetween(unit->pos, pos))
return true;
}
return false;
}
static void scan_seeds(color_ostream &out, unordered_map<int32_t, int32_t> *accessible_counts,
unordered_map<int32_t, int32_t> *inaccessible_counts = NULL) {
static const BadFlags bad_flags;
vector<df::unit *> citizens;
Units::getCitizens(citizens);
for (auto &item : world->items.other[items_other_id::SEEDS]) {
MaterialInfo mat(item);
if (!mat.isPlant())
continue;
if ((bad_flags.whole & item->flags.whole) || !is_accessible_item(item->pos, citizens)) {
if (inaccessible_counts)
++(*inaccessible_counts)[mat.plant->index];
} else {
if (accessible_counts)
++(*accessible_counts)[mat.plant->index];
}
}
}
static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_t *num_disabled_seed_types) {
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
// mark that we have recently run
cycle_timestamp = world->frame_counter;
if (num_enabled_seed_types)
*num_enabled_seed_types = 0;
if (num_disabled_seed_types)
*num_disabled_seed_types = 0;
unordered_map<int32_t, int32_t> accessible_counts;
scan_seeds(out, &accessible_counts);
for (auto &entry : watched_seeds) {
int32_t id = entry.first;
int32_t target = get_config_val(entry.second, SEED_CONFIG_TARGET);
if (accessible_counts[id] <= target) {
DEBUG(cycle,out).print("disabling seed mat: %d\n", id);
if (num_disabled_seed_types)
++*num_disabled_seed_types;
Kitchen::denyPlantSeedCookery(id);
} else if (target + TARGET_BUFFER < accessible_counts[id]) {
DEBUG(cycle,out).print("enabling seed mat: %d\n", id);
if (num_enabled_seed_types)
++*num_enabled_seed_types;
Kitchen::allowPlantSeedCookery(id);
}
}
}
/////////////////////////////////////////////////////
// Lua API
// core will already be suspended when coming in through here
//
static void set_target(color_ostream &out, int32_t id, int32_t target) {
if (target == 0)
remove_seed_config(out, id);
else {
PersistentDataItem &c = ensure_seed_config(out, id);
set_config_val(c, SEED_CONFIG_TARGET, target);
}
}
// searches abbreviations, returns expansion if so, returns original if not
static string searchAbbreviations(string in) {
if(abbreviations.count(in) > 0)
return abbreviations[in];
return in;
};
static void seedwatch_setTarget(color_ostream &out, string name, int32_t num) {
DEBUG(config,out).print("entering seedwatch_setTarget\n");
if (num < 0) {
out.printerr("target must be at least 0\n");
return;
}
if (name == "all") {
for (auto &entry : world_plant_ids) {
set_target(out, entry.second, num);
}
return;
}
string token = searchAbbreviations(name);
if (!world_plant_ids.count(token)) {
out.printerr("%s has not been found as a material.\n", token.c_str());
return;
}
set_target(out, world_plant_ids[token], num);
}
static int seedwatch_getData(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(config,*out).print("entering seedwatch_getData\n");
unordered_map<int32_t, int32_t> watch_map, accessible_counts;
scan_seeds(*out, &accessible_counts);
for (auto &entry : watched_seeds) {
watch_map.emplace(entry.first, get_config_val(entry.second, SEED_CONFIG_TARGET));
}
Lua::Push(L, watch_map);
Lua::Push(L, accessible_counts);
return 2;
}
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(seedwatch_setTarget),
DFHACK_LUA_END
};
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(seedwatch_getData),
DFHACK_LUA_END
};