Merge branch 'develop' into cmake-cleanup

Conflicts:
	depends/lua/CMakeLists.txt
	plugins/CMakeLists.txt
	plugins/devel/CMakeLists.txt
develop
lethosor 2020-01-18 22:28:43 -05:00
commit 815821eb93
50 changed files with 2300 additions and 114 deletions

@ -96,7 +96,8 @@ add_library(lua SHARED ${SRC_LIBLUA})
target_link_libraries(lua ${LIBS})
if(MSVC)
target_compile_options(lua PRIVATE /FI dfhack_llimits.h)
# need no space to prevent /FI from being stripped: https://github.com/DFHack/dfhack/issues/1455
target_compile_options(lua PRIVATE "/FIdfhack_llimits.h")
else()
target_compile_options(lua PRIVATE -include dfhack_llimits.h)
endif()

@ -262,6 +262,9 @@ enable \
# enable mouse controls and sand indicator in embark screen
embark-tools enable sticky sand mouse
# enable option to enter embark assistant
enable embark-assistant
###########
# Scripts #
###########

@ -56,8 +56,12 @@ IndigoFenix
James Logsdon jlogsdon
Japa JapaMala
Jared Adams
Jeremy Apthorp nornagon
Jim Lisi stonetoad
Jimbo Whales jimbowhales
jimcarreer jimcarreer
jj jjyg jj``
Joel Meador janxious
John Beisley huin
John Shade gsvslto
Jonas Ask

@ -1480,7 +1480,7 @@ Some widgets support additional options:
.. _dwarfvet:
dwarfvet
============
========
Enables Animal Caretaker functionality
Always annoyed your dragons become useless after a minor injury? Well, with
@ -1829,7 +1829,63 @@ nestboxes
=========
Automatically scan for and forbid fertile eggs incubating in a nestbox.
Toggle status with `enable` or `disable`.
Toggle status with `enable` or `disable <disable>`.
.. _tailor:
tailor
======
Whenever the bookkeeper updates stockpile records, this plugin will scan every unit in the fort,
count up the number that are worn, and then order enough more made to replace all worn items.
If there are enough replacement items in inventory to replace all worn items, the units wearing them
will have the worn items confiscated (in the same manner as the `cleanowned` plugin) so that they'll
reeequip with replacement items.
Use the `enable` and `disable <disable>` commands to toggle this plugin's status, or run
``tailor status`` to check its current status.
.. _autoclothing:
autoclothing
============
Automatically manage clothing work orders, allowing the user to set how many of
each clothing type every citizen should have. Usage::
autoclothing <material> <item> [number]
Examples:
* ``autoclothing cloth "short skirt" 10``:
Sets the desired number of cloth short skirts available per citizen to 10.
* ``autoclothing cloth dress``:
Displays the currently set number of cloth dresses chosen per citizen.
.. _autofarm:
autofarm
========
Automatically handles crop selection in farm plots based on current plant
stocks, and selects crops for planting if current stock is below a threshold.
Selected crops are dispatched on all farmplots. (Note that this plugin replaces
an older Ruby script of the same name.)
Use the `enable` or `disable <disable>` commands to change whether this plugin is
enabled.
Usage:
* ``autofarm runonce``:
Updates all farm plots once, without enabling the plugin
* ``autofarm status``:
Prints status information, including any applied limits
* ``autofarm default 30``:
Sets the default threshold
* ``autofarm threshold 150 helmet_plump tail_pig``:
Sets thresholds of individual plants
================
Map modification
@ -2242,16 +2298,27 @@ by spaces.
Options:
:-t: Select trees only (exclude shrubs)
:-s: Select shrubs only (exclude trees)
:-c: Clear designations instead of setting them
:-x: Apply selected action to all plants except those specified (invert
:``-t``: Select trees only (exclude shrubs)
:``-s``: Select shrubs only (exclude trees)
:``-c``: Clear designations instead of setting them
:``-x``: Apply selected action to all plants except those specified (invert
selection)
:-a: Select every type of plant (obeys ``-t``/``-s``)
:``-a``: Select every type of plant (obeys ``-t``/``-s``)
:``-v``: Lists the number of (un)designations per plant
Specifying both ``-t`` and ``-s`` will have no effect. If no plant IDs are specified,
all valid plant IDs will be listed.
.. note::
DF is capable of determining that a shrub has already been picked, leaving
an unusable structure part behind. This plugin does not perform such a check
(as the location of the required information has not yet been identified).
This leads to some shrubs being designated when they shouldn't be, causing a
plant gatherer to walk there and do nothing (except clearing the
designation). See :issue:`1479` for details.
.. _infiniteSky:
infiniteSky
@ -2266,8 +2333,10 @@ Usage:
``infiniteSky enable/disable``
Enables/disables monitoring of constructions. If you build anything in the second to highest z-level, it will allocate one more sky level. This is so you can continue to build stairs upward.
:issue:`Sometimes <254>` new z-levels disappear and cause cave-ins.
Saving and loading after creating new z-levels should fix the problem.
.. warning::
:issue:`Sometimes <254>` new z-levels disappear and cause cave-ins.
Saving and loading after creating new z-levels should fix the problem.
.. _liquids:

@ -52,6 +52,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `embark-assistant`:
- fixed bug causing crash on worlds without generated metals (as well as pruning vectors as originally intended).
- fixed bug causing mineral matching to fail to cut off at the magma sea, reporting presence of things that aren't (like DF does currently).
- fixed bug causing half of the river tiles not to be recognized.
- added logic to detect some river tiles DF doesn't generate data for (but are definitely present).
- `getplants`: fixed designation of plants out of season and added verbose flag, but failed to identify picked plants (which are still designated incorrectly)
- `gui/autogems`: fixed error when no world is loaded
- `gui/companion-order`:
- fixed error when resetting group leaders
@ -85,6 +88,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- changed matching to take incursions, i.e. parts of other biomes, into consideration when evaluating tiles. This allows for e.g. finding multiple biomes on single tile embarks.
- changed overlay display to show when incursion surveying is incomplete
- changed overlay display to show evil weather
- added optional parameter "fileresult" for crude external harness automated match support
- improved focus movement logic to go to only required world tiles, increasing speed of subsequent searches considerably
- `exportlegends`: added rivers to custom XML export
- `exterminate`: added support for a special ``enemy`` caste
- `modtools/create-unit`:

@ -1,3 +1,4 @@
#include "Core.h"
#include "Internal.h"
#include "DataDefs.h"
#include "MiscUtils.h"

@ -1,5 +1,7 @@
#include <stddef.h>
#include <fstream>
#ifndef STATIC_FIELDS_GROUP
#include "DataDefs.h"
#endif

@ -1611,6 +1611,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, isOwnCiv),
WRAPM(Units, isOwnGroup),
WRAPM(Units, isOwnRace),
WRAPM(Units, getPhysicalDescription),
WRAPM(Units, getRaceName),
WRAPM(Units, getRaceNamePlural),
WRAPM(Units, getRaceBabyName),

@ -26,6 +26,7 @@ distribution.
#include "TileTypes.h"
#include "Export.h"
#include <cassert>
#include <map>
using namespace DFHack;

@ -32,7 +32,6 @@ distribution.
#include <utility>
#include <vector>
#include "Core.h"
#include "BitArray.h"
// Stop some MS stupidity
@ -48,6 +47,7 @@ typedef struct lua_State lua_State;
namespace DFHack
{
class Core;
class virtual_class {};
enum identity_type {

@ -32,6 +32,10 @@ distribution.
#include "DataIdentity.h"
#include "LuaWrapper.h"
namespace DFHack {
class color_ostream;
}
namespace df {
// A very simple and stupid implementation of some stuff from boost
template<class U, class V> struct is_same_type { static const bool value = false; };

@ -24,6 +24,7 @@ distribution.
#pragma once
#include <deque>
#include <string>
#include <sstream>
#include <vector>

@ -30,6 +30,7 @@ distribution.
#include <map>
#include <type_traits>
#include "ColorText.h"
#include "DataDefs.h"
#include <lua.h>

@ -45,6 +45,17 @@ using std::endl;
#define DFHACK_FUNCTION_SIG __func__
#endif
#ifdef _WIN32
// On x86 MSVC, __thiscall passes |this| in ECX. On x86_64, __thiscall is the
// same as the standard calling convention.
// See https://docs.microsoft.com/en-us/cpp/cpp/thiscall for more info.
#define THISCALL __thiscall
#else
// On other platforms, there's no special calling convention for calling member
// functions.
#define THISCALL
#endif
namespace DFHack {
class color_ostream;
}

@ -25,6 +25,8 @@ distribution.
#pragma once
#include <algorithm>
#include "Pragma.h"
#include "Export.h"

@ -53,6 +53,9 @@ namespace df
namespace DFHack
{
class color_ostream;
namespace Buildings
{
/**

@ -47,6 +47,8 @@ namespace df
namespace DFHack
{
class color_ostream;
namespace Job {
// Duplicate the job structure. It is not linked into any DF lists.
DFHACK_EXPORT df::job *cloneJobStruct(df::job *job, bool keepEverything=false);

@ -120,6 +120,7 @@ DFHACK_EXPORT bool isVisible(df::unit* unit);
DFHACK_EXPORT std::string getRaceNameById(int32_t race_id);
DFHACK_EXPORT std::string getRaceName(df::unit* unit);
DFHACK_EXPORT std::string getPhysicalDescription(df::unit* unit);
DFHACK_EXPORT std::string getRaceNamePluralById(int32_t race_id);
DFHACK_EXPORT std::string getRaceNamePlural(df::unit* unit);
DFHACK_EXPORT std::string getRaceBabyNameById(int32_t race_id);

@ -765,14 +765,15 @@ function dfhack.run_command_silent(...)
end
function dfhack.run_command(...)
local output, status = _run_command(...)
for i, fragment in pairs(output) do
if type(fragment) == 'table' then
dfhack.color(fragment[1])
dfhack.print(fragment[2])
local result = _run_command(...)
for i, f in pairs(result) do
if type(f) == 'table' then
dfhack.color(f[1])
dfhack.print(f[2])
end
end
dfhack.color(COLOR_RESET)
return result.status
end
-- Per-save init file

@ -716,13 +716,16 @@ end
function FilteredList:onInput(keys)
if self.edit_key and keys[self.edit_key] and not self.edit.active then
self.edit.active = true
return true
elseif keys.LEAVESCREEN and self.edit.active then
self.edit.active = false
return true
else
self:inputToSubviews(keys)
return self:inputToSubviews(keys)
end
end
function FilteredList:getChoices()
return self.choices
end

@ -1199,7 +1199,7 @@ void Buildings::clearBuildings(color_ostream& out) {
void Buildings::updateBuildings(color_ostream& out, void* ptr)
{
int32_t id = *((int32_t*)ptr);
int32_t id = (int32_t)(intptr_t)ptr;
auto building = df::building::find(id);
if (building)

@ -273,7 +273,7 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event
}
for ( size_t a = 0; a < df::global::world->buildings.all.size(); a++ ) {
df::building* b = df::global::world->buildings.all[a];
Buildings::updateBuildings(out, (void*)&(b->id));
Buildings::updateBuildings(out, (void*)intptr_t(b->id));
buildings.insert(b->id);
}
lastSyndromeTime = -1;
@ -609,7 +609,7 @@ static void manageBuildingEvent(color_ostream& out) {
buildings.insert(a);
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler bob = (*b).second;
bob.eventHandler(out, (void*)&a);
bob.eventHandler(out, (void*)intptr_t(a));
}
}
nextBuilding = *df::global::building_next_id;
@ -625,7 +625,7 @@ static void manageBuildingEvent(color_ostream& out) {
for ( auto b = copy.begin(); b != copy.end(); b++ ) {
EventHandler bob = (*b).second;
bob.eventHandler(out, (void*)&id);
bob.eventHandler(out, (void*)intptr_t(id));
}
a = buildings.erase(a);
}

@ -541,6 +541,25 @@ string Units::getRaceName(df::unit* unit)
return getRaceNameById(unit->race);
}
void df_unit_get_physical_description(df::unit* unit, string* out_str)
{
static auto* const fn =
reinterpret_cast<void(THISCALL *)(df::unit*, string*)>(
Core::getInstance().vinfo->getAddress("unit_get_physical_description"));
if (fn)
fn(unit, out_str);
else
*out_str = "";
}
string Units::getPhysicalDescription(df::unit* unit)
{
CHECK_NULL_POINTER(unit);
string str;
df_unit_get_physical_description(unit, &str);
return str;
}
// get plural of race name (used for display in autobutcher UI and for sorting the watchlist)
string Units::getRaceNamePluralById(int32_t id)
{
@ -1549,8 +1568,8 @@ bool Units::isGay(df::unit* unit)
if (!unit->status.current_soul)
return false;
df::orientation_flags orientation = unit->status.current_soul->orientation_flags;
return (Units::isFemale(unit) && ! (orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male)))
|| (!Units::isFemale(unit) && ! (orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female)));
return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male)))
&& (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female)));
}
bool Units::isNaked(df::unit* unit)

@ -1 +1 @@
Subproject commit 4388fbfb8f51be41777406c6e7c518f738c195c7
Subproject commit 4053321b202a29f667d64d824ba8339ec1b1df4f

@ -11,6 +11,12 @@ else
export DYLD_FALLBACK_FRAMEWORK_PATH="./hack:./libs:./hack/libs"
fi
# attempt to remove quarantine flag: https://github.com/DFHack/dfhack/issues/1465
if ! test -f hack/quarantine-removed; then
find hack/ libs/ dwarfort.exe -name '*.dylib' -or -name '*.exe' -print0 | xargs -0 xattr -d com.apple.quarantine 2>&1 | grep -iv 'no such xattr'
echo "quarantine flag removed on $(date); remove this file to re-run" > hack/quarantine-removed
fi
old_tty_settings=$(stty -g)
DYLD_INSERT_LIBRARIES=./hack/libdfhack.dylib ./dwarfort.exe "$@"
stty "$old_tty_settings"

@ -69,6 +69,16 @@ fi
PRELOAD_LIB="${PRELOAD_LIB:+$PRELOAD_LIB:}${LIBSAN}${LIB}"
setarch_arch=$(cat hack/dfhack_setarch.txt || printf i386)
if ! setarch "$setarch_arch" -R true 2>/dev/null; then
echo "warn: architecture '$setarch_arch' not supported by setarch" >&2
if [ "$setarch_arch" = "i386" ]; then
setarch_arch=linux32
else
setarch_arch=linux64
fi
echo "using '$setarch_arch' instead. To silence this warning, edit" >&2
echo "hack/dfhack_setarch.txt to contain an architecture that works on your system." >&2
fi
case "$1" in
-g | --gdb)

@ -84,7 +84,9 @@ if(BUILD_SUPPORTED)
dfhack_plugin(add-spatter add-spatter.cpp)
# dfhack_plugin(advtools advtools.cpp)
dfhack_plugin(autochop autochop.cpp)
dfhack_plugin(autoclothing autoclothing.cpp)
dfhack_plugin(autodump autodump.cpp)
dfhack_plugin(autofarm autofarm.cpp)
dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static)
dfhack_plugin(autohauler autohauler.cpp)
dfhack_plugin(autolabor autolabor.cpp)
@ -164,6 +166,7 @@ if(BUILD_SUPPORTED)
add_subdirectory(stockpiles)
dfhack_plugin(stocks stocks.cpp)
dfhack_plugin(strangemood strangemood.cpp)
dfhack_plugin(tailor tailor.cpp)
dfhack_plugin(tiletypes tiletypes.cpp Brushes.h)
dfhack_plugin(title-folder title-folder.cpp)
dfhack_plugin(title-version title-version.cpp)

@ -0,0 +1,672 @@
// some headers required for a plugin. Nothing special, just the basics.
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <map>
// DF data structure definition headers
#include "DataDefs.h"
#include "modules/Items.h"
#include "modules/Maps.h"
#include "modules/Materials.h"
#include "modules/Units.h"
#include "modules/World.h"
#include "df/itemdef_armorst.h"
#include "df/itemdef_glovesst.h"
#include "df/itemdef_shoesst.h"
#include "df/itemdef_helmst.h"
#include "df/itemdef_pantsst.h"
#include "df/manager_order.h"
#include "df/creature_raw.h"
#include "df/world.h"
using namespace DFHack;
using namespace DFHack::Items;
using namespace DFHack::Units;
using namespace df::enums;
// A plugin must be able to return its name and version.
// The name string provided must correspond to the filename -
// skeleton.plug.so, skeleton.plug.dylib, or skeleton.plug.dll in this case
DFHACK_PLUGIN("autoclothing");
// Any globals a plugin requires (e.g. world) should be listed here.
// For example, this line expands to "using df::global::world" and prevents the
// plugin from being loaded if df::global::world is null (i.e. missing from symbols.xml):
//
REQUIRE_GLOBAL(world);
// Only run if this is enabled
DFHACK_PLUGIN_IS_ENABLED(autoclothing_enabled);
// Here go all the command declarations...
// mostly to allow having the mandatory stuff on top of the file and commands on the bottom
struct ClothingRequirement;
command_result autoclothing(color_ostream &out, std::vector <std::string> & parameters);
static void init_state(color_ostream &out);
static void save_state(color_ostream &out);
static void cleanup_state(color_ostream &out);
static void do_autoclothing();
static bool validateMaterialCategory(ClothingRequirement * requirement);
static bool setItem(std::string name, ClothingRequirement* requirement);
std::vector<ClothingRequirement>clothingOrders;
struct ClothingRequirement
{
df::job_type jobType;
df::item_type itemType;
int16_t item_subtype;
df::job_material_category material_category;
int16_t needed_per_citizen;
std::map<int16_t, int32_t> total_needed_per_race;
bool matches(ClothingRequirement * b)
{
if (b->jobType != this->jobType)
return false;
if (b->itemType != this->itemType)
return false;
if (b->item_subtype != this->item_subtype)
return false;
if (b->material_category.whole != this->material_category.whole)
return false;
return true;
}
std::string Serialize()
{
stringstream stream;
stream << ENUM_KEY_STR(job_type, jobType) << " ";
stream << ENUM_KEY_STR(item_type,itemType) << " ";
stream << item_subtype << " ";
stream << material_category.whole << " ";
stream << needed_per_citizen;
return stream.str();
}
void Deserialize(std::string s)
{
stringstream stream(s);
std::string loadedJob;
stream >> loadedJob;
FOR_ENUM_ITEMS(job_type, job)
{
if (ENUM_KEY_STR(job_type, job) == loadedJob)
{
jobType = job;
break;
}
}
std::string loadedItem;
stream >> loadedItem;
FOR_ENUM_ITEMS(item_type, item)
{
if (ENUM_KEY_STR(item_type, item) == loadedItem)
{
itemType = item;
break;
}
}
stream >> item_subtype;
stream >> material_category.whole;
stream >> needed_per_citizen;
}
bool SetFromParameters(color_ostream &out, std::vector <std::string> & parameters)
{
if (!set_bitfield_field(&material_category, parameters[0], 1))
{
out << "Unrecognized material type: " << parameters[0] << endl;
}
if (!setItem(parameters[1], this))
{
out << "Unrecognized item name or token: " << parameters[1] << endl;
return false;
}
if (!validateMaterialCategory(this))
{
out << parameters[0] << " is not a valid material category for " << parameters[1] << endl;
return false;
}
return true;
}
std::string ToReadableLabel()
{
stringstream stream;
stream << bitfield_to_string(material_category) << " ";
std::string adjective = "";
std::string name = "";
switch (itemType)
{
case df::enums::item_type::ARMOR:
adjective = world->raws.itemdefs.armor[item_subtype]->adjective;
name = world->raws.itemdefs.armor[item_subtype]->name;
break;
case df::enums::item_type::SHOES:
adjective = world->raws.itemdefs.shoes[item_subtype]->adjective;
name = world->raws.itemdefs.shoes[item_subtype]->name;
break;
case df::enums::item_type::HELM:
adjective = world->raws.itemdefs.helms[item_subtype]->adjective;
name = world->raws.itemdefs.helms[item_subtype]->name;
break;
case df::enums::item_type::GLOVES:
adjective = world->raws.itemdefs.gloves[item_subtype]->adjective;
name = world->raws.itemdefs.gloves[item_subtype]->name;
break;
case df::enums::item_type::PANTS:
adjective = world->raws.itemdefs.pants[item_subtype]->adjective;
name = world->raws.itemdefs.pants[item_subtype]->name;
break;
default:
break;
}
if (!adjective.empty())
stream << adjective << " ";
stream << name << " ";
stream << needed_per_citizen;
return stream.str();
}
};
// Mandatory init function. If you have some global state, create it here.
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
{
// Fill the command list with your commands.
commands.push_back(PluginCommand(
"autoclothing", "Automatically manage clothing work orders",
autoclothing, false, /* true means that the command can't be used from non-interactive user interface */
// Extended help string. Used by CR_WRONG_USAGE and the help command:
" autoclothing <material> <item> [number]\n"
"Example:\n"
" autoclothing cloth \"short skirt\" 10\n"
" Sets the desired number of cloth short skirts available per citizen to 10.\n"
" autoclothing cloth dress\n"
" Displays the currently set number of cloth dresses chosen per citizen.\n"
));
return CR_OK;
}
// This is called right before the plugin library is removed from memory.
DFhackCExport command_result plugin_shutdown(color_ostream &out)
{
// You *MUST* kill all threads you created before this returns.
// If everything fails, just return CR_FAILURE. Your plugin will be
// in a zombie state, but things won't crash.
cleanup_state(out);
return CR_OK;
}
// Called to notify the plugin about important state changes.
// Invoked with DF suspended, and always before the matching plugin_onupdate.
// More event codes may be added in the future.
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_WORLD_LOADED:
init_state(out);
break;
case SC_WORLD_UNLOADED:
cleanup_state(out);
break;
default:
break;
}
return CR_OK;
}
// Whatever you put here will be done in each game step. Don't abuse it.
// It's optional, so you can just comment it out like this if you don't need it.
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!autoclothing_enabled)
return CR_OK;
if (!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if ((world->frame_counter + 500) % 1200 != 0) // Check every day, but not the same day as other things
return CR_OK;
do_autoclothing();
return CR_OK;
}
static bool setItemFromName(std::string name, ClothingRequirement* requirement)
{
#define SEARCH_ITEM_RAWS(rawType, job, item) \
for (auto& itemdef : world->raws.itemdefs.rawType) \
{ \
std::string fullName = itemdef->adjective.empty() ? itemdef->name : itemdef->adjective + " " + itemdef->name; \
if (fullName == name) \
{ \
requirement->jobType = job_type::job; \
requirement->itemType = item_type::item; \
requirement->item_subtype = itemdef->subtype; \
return true; \
} \
}
SEARCH_ITEM_RAWS(armor, MakeArmor, ARMOR);
SEARCH_ITEM_RAWS(gloves, MakeGloves, GLOVES);
SEARCH_ITEM_RAWS(shoes, MakeShoes, SHOES);
SEARCH_ITEM_RAWS(helms, MakeHelm, HELM);
SEARCH_ITEM_RAWS(pants, MakePants, PANTS);
return false;
}
static bool setItemFromToken(std::string token, ClothingRequirement* requirement)
{
ItemTypeInfo itemInfo;
if (!itemInfo.find(token))
return false;
switch (itemInfo.type)
{
case item_type::ARMOR:
requirement->jobType = job_type::MakeArmor;
break;
case item_type::GLOVES:
requirement->jobType = job_type::MakeGloves;
break;
case item_type::SHOES:
requirement->jobType = job_type::MakeShoes;
break;
case item_type::HELM:
requirement->jobType = job_type::MakeHelm;
break;
case item_type::PANTS:
requirement->jobType = job_type::MakePants;
break;
default:
return false;
}
requirement->itemType = itemInfo.type;
requirement->item_subtype = itemInfo.subtype;
return true;
}
static bool setItem(std::string name, ClothingRequirement* requirement)
{
if (setItemFromName(name, requirement))
return true;
if (setItemFromToken(name, requirement))
return true;
return false;
}
static bool armorFlagsMatch(BitArray<df::armor_general_flags> * flags, df::job_material_category * category)
{
if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.cloth)
return true;
if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.yarn)
return true;
if (flags->is_set(df::armor_general_flags::SOFT) && category->bits.silk)
return true;
if (flags->is_set(df::armor_general_flags::BARRED) && category->bits.bone)
return true;
if (flags->is_set(df::armor_general_flags::SCALED) && category->bits.shell)
return true;
if (flags->is_set(df::armor_general_flags::LEATHER) && category->bits.leather)
return true;
return false;
}
static bool validateMaterialCategory(ClothingRequirement * requirement)
{
auto itemDef = getSubtypeDef(requirement->itemType, requirement->item_subtype);
switch (requirement->itemType)
{
case item_type::ARMOR:
if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_armorst, itemDef))
return armorFlagsMatch(&armor->props.flags, &requirement->material_category);
case item_type::GLOVES:
if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_glovesst, itemDef))
return armorFlagsMatch(&armor->props.flags, &requirement->material_category);
case item_type::SHOES:
if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_shoesst, itemDef))
return armorFlagsMatch(&armor->props.flags, &requirement->material_category);
case item_type::HELM:
if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_helmst, itemDef))
return armorFlagsMatch(&armor->props.flags, &requirement->material_category);
case item_type::PANTS:
if (STRICT_VIRTUAL_CAST_VAR(armor, df::itemdef_pantsst, itemDef))
return armorFlagsMatch(&armor->props.flags, &requirement->material_category);
default:
return false;
}
}
// A command! It sits around and looks pretty. And it's nice and friendly.
command_result autoclothing(color_ostream &out, std::vector <std::string> & parameters)
{
// It's nice to print a help message you get invalid options
// from the user instead of just acting strange.
// This can be achieved by adding the extended help string to the
// PluginCommand registration as show above, and then returning
// CR_WRONG_USAGE from the function. The same string will also
// be used by 'help your-command'.
if (parameters.size() == 0)
{
out << "Currently set " << clothingOrders.size() << " automatic clothing orders" << endl;
for (size_t i = 0; i < clothingOrders.size(); i++)
{
out << clothingOrders[i].ToReadableLabel() << endl;
}
return CR_OK;
}
else if (parameters.size() < 2 || parameters.size() > 3)
{
out << "Wrong number of arguments." << endl;
return CR_WRONG_USAGE;
}
// Commands are called from threads other than the DF one.
// Suspend this thread until DF has time for us. If you
// use CoreSuspender, it'll automatically resume DF when
// execution leaves the current scope.
CoreSuspender suspend;
// Create a new requirement from the available parameters.
ClothingRequirement newRequirement;
if (!newRequirement.SetFromParameters(out, parameters))
return CR_WRONG_USAGE;
//all checks are passed. Now we either show or set the amount.
bool settingSize = false;
bool matchedExisting = false;
if (parameters.size() > 2)
{
try
{
newRequirement.needed_per_citizen = std::stoi(parameters[2]);
}
catch (const std::exception&)
{
out << parameters[2] << " is not a valid number." << endl;
return CR_WRONG_USAGE;
}
settingSize = true;
}
for (size_t i = 0; i < clothingOrders.size(); i++)
{
if (!clothingOrders[i].matches(&newRequirement))
continue;
matchedExisting = true;
if (settingSize)
{
if (newRequirement.needed_per_citizen == 0)
{
clothingOrders.erase(clothingOrders.begin() + i);
out << "Unset " << parameters[0] << " " << parameters[1] << endl;
}
else
{
clothingOrders[i] = newRequirement;
out << "Set " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl;
}
}
else
{
out << parameters[0] << " " << parameters[1] << " is set to " << clothingOrders[i].needed_per_citizen << endl;
}
break;
}
if (!matchedExisting)
{
if (settingSize)
{
if (newRequirement.needed_per_citizen == 0)
{
out << parameters[0] << " " << parameters[1] << " already unset." << endl;
}
else
{
clothingOrders.push_back(newRequirement);
out << "Added order for " << parameters[0] << " " << parameters[1] << " to " << parameters[2] << endl;
}
}
else
{
out << parameters[0] << " " << parameters[1] << " is not set." << endl;
}
}
if (settingSize)
{
if (!autoclothing_enabled)
{
out << "Enabling automatic clothing management" << endl;
autoclothing_enabled = true;
}
do_autoclothing();
}
save_state(out);
// Give control back to DF.
return CR_OK;
}
static void find_needed_clothing_items()
{
for (auto& unit : world->units.active)
{
//obviously we don't care about illegal aliens.
if (!isCitizen(unit))
continue;
//now check each clothing order to see what the unit might be missing.
for (auto& clothingOrder : clothingOrders)
{
int alreadyOwnedAmount = 0;
//looping through the items first, then clothing order might be a little faster, but this way is cleaner.
for (auto& ownedItem : unit->owned_items)
{
auto item = findItemByID(ownedItem);
if (item->getType() != clothingOrder.itemType)
continue;
if (item->getSubtype() != clothingOrder.item_subtype)
continue;
MaterialInfo matInfo;
matInfo.decode(item);
if (!matInfo.matches(clothingOrder.material_category))
continue;
alreadyOwnedAmount++;
}
int neededAmount = clothingOrder.needed_per_citizen - alreadyOwnedAmount;
if (neededAmount <= 0)
continue;
//technically, there's some leeway in sizes, but only caring about exact sizes is simpler.
clothingOrder.total_needed_per_race[unit->race] += neededAmount;
}
}
}
static void remove_available_clothing()
{
for (auto& item : world->items.all)
{
//skip any owned items.
if (getOwner(item))
continue;
//again, for each item, find if any clothing order matches
for (auto& clothingOrder : clothingOrders)
{
if (item->getType() != clothingOrder.itemType)
continue;
if (item->getSubtype() != clothingOrder.item_subtype)
continue;
MaterialInfo matInfo;
matInfo.decode(item);
if (!matInfo.matches(clothingOrder.material_category))
continue;
clothingOrder.total_needed_per_race[item->getMakerRace()] --;
}
}
}
static void add_clothing_orders()
{
for (auto& clothingOrder : clothingOrders)
{
for (auto& orderNeeded : clothingOrder.total_needed_per_race)
{
auto race = orderNeeded.first;
auto amount = orderNeeded.second;
orderNeeded.second = 0; //once we get what we need, set it back to zero so we don't add it to further counts.
//Previous operations can easily make this negative. That jus means we have more than we need already.
if (amount <= 0)
continue;
bool orderExistedAlready = false;
for (auto& managerOrder : world->manager_orders)
{
//Annoyingly, the manager orders store the job type for clothing orders, and actual item type is left at -1;
if (managerOrder->job_type != clothingOrder.jobType)
continue;
if (managerOrder->item_subtype != clothingOrder.item_subtype)
continue;
if (managerOrder->hist_figure_id != race)
continue;
//We found a work order, that means we don't need to make a new one.
orderExistedAlready = true;
amount -= managerOrder->amount_left;
if (amount > 0)
{
managerOrder->amount_left += amount;
managerOrder->amount_total += amount;
}
}
//if it wasn't there, we need to make a new one.
if (!orderExistedAlready)
{
df::manager_order * newOrder = new df::manager_order();
newOrder->id = world->manager_order_next_id;
world->manager_order_next_id++;
newOrder->job_type = clothingOrder.jobType;
newOrder->item_subtype = clothingOrder.item_subtype;
newOrder->hist_figure_id = race;
newOrder->material_category = clothingOrder.material_category;
newOrder->amount_left = amount;
newOrder->amount_total = amount;
world->manager_orders.push_back(newOrder);
}
}
}
}
static void do_autoclothing()
{
if (clothingOrders.size() == 0)
return;
//first we look through all the units on the map to see who needs new clothes.
find_needed_clothing_items();
//Now we go through all the items in the map to see how many clothing items we have but aren't owned yet.
remove_available_clothing();
//Finally loop through the clothing orders to find ones that need more made.
add_clothing_orders();
}
static void cleanup_state(color_ostream &out)
{
clothingOrders.clear();
autoclothing_enabled = false;
}
static void init_state(color_ostream &out)
{
auto enabled = World::GetPersistentData("autoclothing/enabled");
if (enabled.isValid() && enabled.ival(0) == 1)
{
out << "autoclothing enabled" << endl;
autoclothing_enabled = true;
}
else
{
autoclothing_enabled = false;
}
// Parse constraints
std::vector<PersistentDataItem> items;
World::GetPersistentData(&items, "autoclothing/clothingItems");
for (auto& item : items)
{
if (!item.isValid())
continue;
ClothingRequirement req;
req.Deserialize(item.val());
clothingOrders.push_back(req);
out << "autoclothing added " << req.ToReadableLabel() << endl;
}
}
static void save_state(color_ostream &out)
{
auto enabled = World::GetPersistentData("autoclothing/enabled");
if (!enabled.isValid())
enabled = World::AddPersistentData("autoclothing/enabled");
enabled.ival(0) = autoclothing_enabled;
for (auto& order : clothingOrders)
{
auto orderSave = World::AddPersistentData("autoclothing/clothingItems");
orderSave.val() = order.Serialize();
}
// Parse constraints
std::vector<PersistentDataItem> items;
World::GetPersistentData(&items, "autoclothing/clothingItems");
for (size_t i = 0; i < items.size(); i++)
{
if (i < clothingOrders.size())
{
items[i].val() = clothingOrders[i].Serialize();
}
else
{
World::DeletePersistentData(items[i]);
}
}
for (size_t i = items.size(); i < clothingOrders.size(); i++)
{
auto item = World::AddPersistentData("autoclothing/clothingItems");
item.val() = clothingOrders[i].Serialize();
}
}

@ -0,0 +1,436 @@
#include "Core.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "DataDefs.h"
#include "df/world.h"
#include "df/ui.h"
#include "df/building_type.h"
#include "df/building_farmplotst.h"
#include "df/buildings_other_id.h"
#include "df/global_objects.h"
#include "df/item.h"
#include "df/item_plantst.h"
#include "df/items_other_id.h"
#include "df/unit.h"
#include "df/building.h"
#include "df/plant_raw.h"
#include "df/plant_raw_flags.h"
#include "df/biome_type.h"
#include "modules/Items.h"
#include "modules/Maps.h"
#include "modules/World.h"
#include <queue>
using std::vector;
using std::string;
using std::map;
using std::set;
using std::queue;
using std::endl;
using namespace DFHack;
using namespace df::enums;
using df::global::world;
using df::global::ui;
static command_result autofarm(color_ostream &out, vector <string> & parameters);
DFHACK_PLUGIN("autofarm");
DFHACK_PLUGIN_IS_ENABLED(enabled);
const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks.";
const char *usage = (
"``enable autofarm``: Enables the plugin\n"
"``autofarm runonce``: Updates farm plots (one-time only)\n"
"``autofarm status``: Prints status information\n"
"``autofarm default 30``: Sets the default threshold\n"
"``autofarm threshold 150 helmet_plump tail_pig``: Sets thresholds\n"
);
class AutoFarm {
private:
map<int, int> thresholds;
int defaultThreshold = 50;
map<int, int> lastCounts;
public:
void initialize()
{
thresholds.clear();
defaultThreshold = 50;
lastCounts.clear();
}
void setThreshold(int id, int val)
{
thresholds[id] = val;
}
int getThreshold(int id)
{
return (thresholds.count(id) > 0) ? thresholds[id] : defaultThreshold;
}
void setDefault(int val)
{
defaultThreshold = val;
}
private:
const df::plant_raw_flags seasons[4] = { df::plant_raw_flags::SPRING, df::plant_raw_flags::SUMMER, df::plant_raw_flags::AUTUMN, df::plant_raw_flags::WINTER };
public:
bool is_plantable(df::plant_raw* plant)
{
bool has_seed = plant->flags.is_set(df::plant_raw_flags::SEED);
bool is_tree = plant->flags.is_set(df::plant_raw_flags::TREE);
int8_t season = *df::global::cur_season;
int harvest = (*df::global::cur_season_tick) + plant->growdur * 10;
bool can_plant = has_seed && !is_tree && plant->flags.is_set(seasons[season]);
while (can_plant && harvest >= 10080) {
season = (season + 1) % 4;
harvest -= 10080;
can_plant = can_plant && plant->flags.is_set(seasons[season]);
}
return can_plant;
}
private:
map<int, set<df::biome_type>> plantable_plants;
const map<df::plant_raw_flags, df::biome_type> biomeFlagMap = {
{ df::plant_raw_flags::BIOME_MOUNTAIN, df::biome_type::MOUNTAIN },
{ df::plant_raw_flags::BIOME_GLACIER, df::biome_type::GLACIER },
{ df::plant_raw_flags::BIOME_TUNDRA, df::biome_type::TUNDRA },
{ df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_FRESHWATER, df::biome_type::SWAMP_TEMPERATE_FRESHWATER },
{ df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_SALTWATER, df::biome_type::SWAMP_TEMPERATE_SALTWATER },
{ df::plant_raw_flags::BIOME_MARSH_TEMPERATE_FRESHWATER, df::biome_type::MARSH_TEMPERATE_FRESHWATER },
{ df::plant_raw_flags::BIOME_MARSH_TEMPERATE_SALTWATER, df::biome_type::MARSH_TEMPERATE_SALTWATER },
{ df::plant_raw_flags::BIOME_SWAMP_TROPICAL_FRESHWATER, df::biome_type::SWAMP_TROPICAL_FRESHWATER },
{ df::plant_raw_flags::BIOME_SWAMP_TROPICAL_SALTWATER, df::biome_type::SWAMP_TROPICAL_SALTWATER },
{ df::plant_raw_flags::BIOME_SWAMP_MANGROVE, df::biome_type::SWAMP_MANGROVE },
{ df::plant_raw_flags::BIOME_MARSH_TROPICAL_FRESHWATER, df::biome_type::MARSH_TROPICAL_FRESHWATER },
{ df::plant_raw_flags::BIOME_MARSH_TROPICAL_SALTWATER, df::biome_type::MARSH_TROPICAL_SALTWATER },
{ df::plant_raw_flags::BIOME_FOREST_TAIGA, df::biome_type::FOREST_TAIGA },
{ df::plant_raw_flags::BIOME_FOREST_TEMPERATE_CONIFER, df::biome_type::FOREST_TEMPERATE_CONIFER },
{ df::plant_raw_flags::BIOME_FOREST_TEMPERATE_BROADLEAF, df::biome_type::FOREST_TEMPERATE_BROADLEAF },
{ df::plant_raw_flags::BIOME_FOREST_TROPICAL_CONIFER, df::biome_type::FOREST_TROPICAL_CONIFER },
{ df::plant_raw_flags::BIOME_FOREST_TROPICAL_DRY_BROADLEAF, df::biome_type::FOREST_TROPICAL_DRY_BROADLEAF },
{ df::plant_raw_flags::BIOME_FOREST_TROPICAL_MOIST_BROADLEAF, df::biome_type::FOREST_TROPICAL_MOIST_BROADLEAF },
{ df::plant_raw_flags::BIOME_GRASSLAND_TEMPERATE, df::biome_type::GRASSLAND_TEMPERATE },
{ df::plant_raw_flags::BIOME_SAVANNA_TEMPERATE, df::biome_type::SAVANNA_TEMPERATE },
{ df::plant_raw_flags::BIOME_SHRUBLAND_TEMPERATE, df::biome_type::SHRUBLAND_TEMPERATE },
{ df::plant_raw_flags::BIOME_GRASSLAND_TROPICAL, df::biome_type::GRASSLAND_TROPICAL },
{ df::plant_raw_flags::BIOME_SAVANNA_TROPICAL, df::biome_type::SAVANNA_TROPICAL },
{ df::plant_raw_flags::BIOME_SHRUBLAND_TROPICAL, df::biome_type::SHRUBLAND_TROPICAL },
{ df::plant_raw_flags::BIOME_DESERT_BADLAND, df::biome_type::DESERT_BADLAND },
{ df::plant_raw_flags::BIOME_DESERT_ROCK, df::biome_type::DESERT_ROCK },
{ df::plant_raw_flags::BIOME_DESERT_SAND, df::biome_type::DESERT_SAND },
{ df::plant_raw_flags::BIOME_OCEAN_TROPICAL, df::biome_type::OCEAN_TROPICAL },
{ df::plant_raw_flags::BIOME_OCEAN_TEMPERATE, df::biome_type::OCEAN_TEMPERATE },
{ df::plant_raw_flags::BIOME_OCEAN_ARCTIC, df::biome_type::OCEAN_ARCTIC },
{ df::plant_raw_flags::BIOME_POOL_TEMPERATE_FRESHWATER, df::biome_type::POOL_TEMPERATE_FRESHWATER },
{ df::plant_raw_flags::BIOME_POOL_TEMPERATE_BRACKISHWATER, df::biome_type::POOL_TEMPERATE_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_POOL_TEMPERATE_SALTWATER, df::biome_type::POOL_TEMPERATE_SALTWATER },
{ df::plant_raw_flags::BIOME_POOL_TROPICAL_FRESHWATER, df::biome_type::POOL_TROPICAL_FRESHWATER },
{ df::plant_raw_flags::BIOME_POOL_TROPICAL_BRACKISHWATER, df::biome_type::POOL_TROPICAL_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_POOL_TROPICAL_SALTWATER, df::biome_type::POOL_TROPICAL_SALTWATER },
{ df::plant_raw_flags::BIOME_LAKE_TEMPERATE_FRESHWATER, df::biome_type::LAKE_TEMPERATE_FRESHWATER },
{ df::plant_raw_flags::BIOME_LAKE_TEMPERATE_BRACKISHWATER, df::biome_type::LAKE_TEMPERATE_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_LAKE_TEMPERATE_SALTWATER, df::biome_type::LAKE_TEMPERATE_SALTWATER },
{ df::plant_raw_flags::BIOME_LAKE_TROPICAL_FRESHWATER, df::biome_type::LAKE_TROPICAL_FRESHWATER },
{ df::plant_raw_flags::BIOME_LAKE_TROPICAL_BRACKISHWATER, df::biome_type::LAKE_TROPICAL_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_LAKE_TROPICAL_SALTWATER, df::biome_type::LAKE_TROPICAL_SALTWATER },
{ df::plant_raw_flags::BIOME_RIVER_TEMPERATE_FRESHWATER, df::biome_type::RIVER_TEMPERATE_FRESHWATER },
{ df::plant_raw_flags::BIOME_RIVER_TEMPERATE_BRACKISHWATER, df::biome_type::RIVER_TEMPERATE_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_RIVER_TEMPERATE_SALTWATER, df::biome_type::RIVER_TEMPERATE_SALTWATER },
{ df::plant_raw_flags::BIOME_RIVER_TROPICAL_FRESHWATER, df::biome_type::RIVER_TROPICAL_FRESHWATER },
{ df::plant_raw_flags::BIOME_RIVER_TROPICAL_BRACKISHWATER, df::biome_type::RIVER_TROPICAL_BRACKISHWATER },
{ df::plant_raw_flags::BIOME_RIVER_TROPICAL_SALTWATER, df::biome_type::RIVER_TROPICAL_SALTWATER },
{ df::plant_raw_flags::BIOME_SUBTERRANEAN_WATER, df::biome_type::SUBTERRANEAN_WATER },
{ df::plant_raw_flags::BIOME_SUBTERRANEAN_CHASM, df::biome_type::SUBTERRANEAN_CHASM },
{ df::plant_raw_flags::BIOME_SUBTERRANEAN_LAVA, df::biome_type::SUBTERRANEAN_LAVA }
};
public:
void find_plantable_plants()
{
plantable_plants.clear();
map<int, int> counts;
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_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);
#undef F
for (auto ii : world->items.other[df::items_other_id::SEEDS])
{
df::item_plantst* i = (df::item_plantst*)ii;
if ((i->flags.whole & bad_flags.whole) == 0)
counts[i->mat_index] += i->stack_size;
}
for (auto ci : counts)
{
if (df::global::ui->tasks.discovered_plants[ci.first])
{
df::plant_raw* plant = world->raws.plants.all[ci.first];
if (is_plantable(plant))
for (auto flagmap : biomeFlagMap)
if (plant->flags.is_set(flagmap.first))
plantable_plants[plant->index].insert(flagmap.second);
}
}
}
void set_farms(color_ostream& out, set<int> plants, vector<df::building_farmplotst*> farms)
{
// this algorithm attempts to change as few farms as possible, while ensuring that
// the number of farms planting each eligible plant is "as equal as possible"
if (farms.empty() || plants.empty())
return; // do nothing if there are no farms or no plantable plants
int season = *df::global::cur_season;
int min = farms.size() / plants.size(); // the number of farms that should plant each eligible plant, rounded down
int extra = farms.size() - min * plants.size(); // the remainder that cannot be evenly divided
map<int, int> counters;
counters.empty();
queue<df::building_farmplotst*> toChange;
toChange.empty();
for (auto farm : farms)
{
int o = farm->plant_id[season];
if (plants.count(o)==0 || counters[o] > min || (counters[o] == min && extra == 0))
toChange.push(farm); // this farm is an excess instance for the plant it is currently planting
else
{
if (counters[o] == min)
extra--; // allocate off one of the remainder farms
counters[o]++;
}
}
for (auto n : plants)
{
int c = counters[n];
while (toChange.size() > 0 && (c < min || (c == min && extra > 0)))
{
// pick one of the excess farms and change it to plant this plant
df::building_farmplotst* farm = toChange.front();
int o = farm->plant_id[season];
farm->plant_id[season] = n;
out << "autofarm: changing farm #" << farm->id <<
" from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) <<
" to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl;
toChange.pop();
if (c++ == min)
extra--;
}
}
}
void process(color_ostream& out)
{
if (!enabled)
return;
find_plantable_plants();
lastCounts.clear();
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_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);
#undef F
for (auto ii : world->items.other[df::items_other_id::PLANT])
{
df::item_plantst* i = (df::item_plantst*)ii;
if ((i->flags.whole & bad_flags.whole) == 0 &&
plantable_plants.count(i->mat_index) > 0)
{
lastCounts[i->mat_index] += i->stack_size;
}
}
map<df::biome_type, set<int>> plants;
plants.clear();
for (auto plantable : plantable_plants)
{
df::plant_raw* plant = world->raws.plants.all[plantable.first];
if (lastCounts[plant->index] < getThreshold(plant->index))
for (auto biome : plantable.second)
{
plants[biome].insert(plant->index);
}
}
map<df::biome_type, vector<df::building_farmplotst*>> farms;
farms.clear();
for (auto bb : world->buildings.other[df::buildings_other_id::FARM_PLOT])
{
df::building_farmplotst* farm = (df::building_farmplotst*) bb;
if (farm->flags.bits.exists)
{
df::biome_type biome;
if (Maps::getTileDesignation(bb->centerx, bb->centery, bb->z)->bits.subterranean)
biome = biome_type::SUBTERRANEAN_WATER;
else {
df::coord2d region(Maps::getTileBiomeRgn(df::coord(bb->centerx, bb->centery, bb->z)));
biome = Maps::GetBiomeType(region.x, region.y);
}
farms[biome].push_back(farm);
}
}
for (auto ff : farms)
{
set_farms(out, plants[ff.first], ff.second);
}
}
void status(color_ostream& out)
{
out << (enabled ? "Running." : "Stopped.") << endl;
for (auto lc : lastCounts)
{
auto plant = world->raws.plants.all[lc.first];
out << plant->id << " limit " << getThreshold(lc.first) << " current " << lc.second << endl;
}
for (auto th : thresholds)
{
if (lastCounts[th.first] > 0)
continue;
auto plant = world->raws.plants.all[th.first];
out << plant->id << " limit " << getThreshold(th.first) << " current 0" << endl;
}
out << "Default: " << defaultThreshold << endl;
}
};
static AutoFarm* autofarmInstance;
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands)
{
if (world && ui) {
commands.push_back(
PluginCommand("autofarm", tagline,
autofarm, false, usage
)
);
}
autofarmInstance = new AutoFarm();
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
delete autofarmInstance;
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!autofarmInstance)
return CR_OK;
if (!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter % 50 != 0) // Check every hour
return CR_OK;
{
CoreSuspender suspend;
autofarmInstance->process(out);
}
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
enabled = enable;
return CR_OK;
}
static command_result setThresholds(color_ostream& out, vector<string> & parameters)
{
int val = atoi(parameters[1].c_str());
for (int i = 2; i < parameters.size(); i++)
{
string id = parameters[i];
transform(id.begin(), id.end(), id.begin(), ::toupper);
bool ok = false;
for (auto plant : world->raws.plants.all)
{
if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id))
{
autofarmInstance->setThreshold(plant->index, val);
ok = true;
break;
}
}
if (!ok)
{
out << "Cannot find plant with id " << id << endl;
return CR_WRONG_USAGE;
}
}
return CR_OK;
}
static command_result autofarm(color_ostream &out, vector <string> & parameters)
{
CoreSuspender suspend;
if (parameters.size() == 1 && parameters[0] == "runonce")
autofarmInstance->process(out);
else if (parameters.size() == 1 && parameters[0] == "enable")
plugin_enable(out, true);
else if (parameters.size() == 1 && parameters[0] == "disable")
plugin_enable(out, false);
else if (parameters.size() == 2 && parameters[0] == "default")
autofarmInstance->setDefault(atoi(parameters[1].c_str()));
else if (parameters.size() >= 3 && parameters[0] == "threshold")
return setThresholds(out, parameters);
else if (parameters.size() == 0 || parameters.size() == 1 && parameters[0] == "status")
autofarmInstance->status(out);
else
return CR_WRONG_USAGE;
return CR_OK;
}

@ -303,7 +303,12 @@ command_result df_createitem (color_ostream &out, vector <string> & parameters)
case item_type::PET:
case item_type::EGG:
split_string(&tokens, material_str, ":");
if (tokens.size() != 2)
if (tokens.size() == 1)
{
// default to empty caste to display a list of valid castes later
tokens.push_back("");
}
else if (tokens.size() != 2)
{
out.printerr("You must specify a creature ID and caste for this item type!\n");
return CR_FAILURE;
@ -311,12 +316,14 @@ command_result df_createitem (color_ostream &out, vector <string> & parameters)
for (size_t i = 0; i < world->raws.creatures.all.size(); i++)
{
string castes = "";
df::creature_raw *creature = world->raws.creatures.all[i];
if (creature->creature_id == tokens[0])
{
for (size_t j = 0; j < creature->caste.size(); j++)
{
df::caste_raw *caste = creature->caste[j];
castes += " " + creature->caste[j]->caste_id;
if (creature->caste[j]->caste_id == tokens[1])
{
mat_type = i;
@ -326,7 +333,15 @@ command_result df_createitem (color_ostream &out, vector <string> & parameters)
}
if (mat_type == -1)
{
out.printerr("The creature you specified has no such caste!\n");
if (tokens[1].empty())
{
out.printerr("You must also specify a caste.\n");
}
else
{
out.printerr("The creature you specified has no such caste!\n");
}
out.printerr("Valid castes:%s\n", castes.c_str());
return CR_FAILURE;
}
}

@ -2,6 +2,8 @@ if(UNIX)
dfhack_plugin(vectors vectors.cpp)
endif()
include(FindThreads)
add_definitions(-DDEV_PLUGIN)
dfhack_plugin(buildprobe buildprobe.cpp)
dfhack_plugin(color-dfhack-text color-dfhack-text.cpp)

@ -175,8 +175,15 @@ void unitAttack(color_ostream& out, void* ptr) {
EventManager::UnitAttackData* data = (EventManager::UnitAttackData*)ptr;
out.print("unit %d attacks unit %d\n", data->attacker, data->defender);
df::unit* defender = df::unit::find(data->defender);
if (!defender) {
out.printerr("defender %d does not exist\n", data->defender);
return;
}
int32_t woundIndex = df::unit_wound::binsearch_index(defender->body.wounds, data->wound);
df::unit_wound* wound = defender->body.wounds[woundIndex];
df::unit_wound* wound = vector_get(defender->body.wounds, woundIndex);
if (!wound) {
return;
}
set<int32_t> parts;
for ( auto a = wound->parts.begin(); a != wound->parts.end(); a++ ) {
parts.insert((*a)->body_part_id);

@ -163,7 +163,6 @@ namespace dm_lua {
delete out;
out = NULL;
}
lua_close(state);
}
bool init_call (const char *func)
{
@ -1873,6 +1872,12 @@ static bool set_monitoring_mode(const string &mode, const bool &state)
if (!is_enabled)
return false;
/*
NOTE: although we are not touching DF directly but there might be
code running that uses these values. So this could use another mutex
or just suspend the core while we edit our values.
*/
CoreSuspender guard;
if (mode == "work" || mode == "all")
{
@ -1908,7 +1913,10 @@ static bool load_config()
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable)
{
CoreSuspender guard;
load_config();
}
if (is_enabled != enable)
{
if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable))
@ -1963,16 +1971,19 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & par
}
else if (cmd == 's' || cmd == 'S')
{
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
}
else if (cmd == 'p' || cmd == 'P')
{
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
}
else if (cmd == 'r' || cmd == 'R')
{
CoreSuspender guard;
load_config();
}
else
@ -2024,7 +2035,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
" Reload configuration file (dfhack-config/dwarfmonitor.json)\n"
));
dm_lua::state = Lua::Open(out);
dm_lua::state=Lua::Core::State;
if (dm_lua::state == NULL)
return CR_FAILURE;

@ -11,6 +11,8 @@ using std::ostringstream;
using std::string;
using std::vector;
#define fileresult_file_name "./data/init/embark_assistant_fileresult.txt"
namespace embark_assist {
namespace defs {
// Survey types
@ -315,6 +317,8 @@ namespace embark_assist {
bool y_down;
bool inhibit_x_turn;
bool inhibit_y_turn;
uint16_t target_location_x;
uint16_t target_location_y;
uint16_t count;
finders finder;
};

@ -1,11 +1,13 @@
#include <time.h>
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include <time.h>
#include <modules/Gui.h>
#include <modules/Screen.h>
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "../uicommon.h"
#include "DataDefs.h"
#include "df/coord2d.h"
@ -27,6 +29,7 @@
#include "survey.h"
DFHACK_PLUGIN("embark-assistant");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
using namespace DFHack;
using namespace df::enums;
@ -134,11 +137,43 @@ command_result embark_assistant (color_ostream &out, std::vector <std::string> &
//=======================================================================================
struct start_site_hook : df::viewscreen_choose_start_sitest {
typedef df::viewscreen_choose_start_sitest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (embark_assist::main::state)
return;
auto dims = Screen::getWindowSize();
int x = 60;
int y = dims.y - 2;
OutputString(COLOR_LIGHTRED, x, y, " " + Screen::getKeyDisplay(interface_key::CUSTOM_A));
OutputString(COLOR_WHITE, x, y, ": Embark ");
OutputString(COLOR_WHITE, x, y, dims.x > 82 ? "Assistant" : "Asst.");
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set<df::interface_key> *input))
{
if (!embark_assist::main::state && input->count(interface_key::CUSTOM_A))
{
Core::getInstance().setHotkeyCmd("embark-assistant");
return;
}
INTERPOSE_NEXT(feed)(input);
}
};
IMPLEMENT_VMETHOD_INTERPOSE(start_site_hook, render);
IMPLEMENT_VMETHOD_INTERPOSE(start_site_hook, feed);
//=======================================================================================
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand(
"embark-assistant", "Embark site selection support.",
embark_assistant, true, /* true means that the command can't be used from non-interactive user interface */
embark_assistant, false, /* false means that the command can be used from non-interactive user interface */
// Extended help string. Used by CR_WRONG_USAGE and the help command:
" This command starts the embark-assist plugin that provides embark site\n"
" selection help. It has to be called while the pre-embark screen is\n"
@ -160,6 +195,22 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out)
//=======================================================================================
DFhackCExport command_result plugin_enable (color_ostream &out, bool enable)
{
if (is_enabled != enable)
{
if (!INTERPOSE_HOOK(start_site_hook, render).apply(enable) ||
!INTERPOSE_HOOK(start_site_hook, feed).apply(enable))
{
return CR_FAILURE;
}
is_enabled = enable;
}
return CR_OK;
}
//=======================================================================================
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
@ -202,8 +253,15 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
command_result embark_assistant(color_ostream &out, std::vector <std::string> & parameters)
{
if (!parameters.empty())
bool fileresult = false;
if (parameters.size() == 1 &&
parameters[0] == "fileresult") {
remove(fileresult_file_name);
fileresult = true;
} else if (!parameters.empty()) {
return CR_WRONG_USAGE;
}
CoreSuspender suspend;
@ -226,8 +284,11 @@ command_result embark_assistant(color_ostream &out, std::vector <std::string> &
// Find the end of the normal inorganic definitions.
embark_assist::main::state->max_inorganic = 0;
for (uint16_t i = 0; i < world->raws.inorganics.size(); i++) {
if (!world->raws.inorganics[i]->flags.is_set(df::inorganic_flags::GENERATED)) embark_assist::main::state->max_inorganic = i;
for (uint16_t i = world->raws.inorganics.size() - 1; i >= 0 ; i--) {
if (!world->raws.inorganics[i]->flags.is_set(df::inorganic_flags::GENERATED)) {
embark_assist::main::state->max_inorganic = i;
break;
}
}
embark_assist::main::state->max_inorganic++; // To allow it to be used as size() replacement
@ -297,5 +358,9 @@ command_result embark_assistant(color_ostream &out, std::vector <std::string> &
embark_assist::survey::survey_embark(&mlt, &embark_assist::main::state->survey_results, &embark_assist::main::state->site_info, false);
embark_assist::overlay::set_embark(&embark_assist::main::state->site_info);
if (fileresult) {
embark_assist::overlay::fileresult();
}
return CR_OK;
}

@ -1557,11 +1557,18 @@ namespace embark_assist {
// Exported operations
//===============================================================================
void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic) {
void embark_assist::finder_ui::init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic, bool fileresult) {
if (!embark_assist::finder_ui::state) { // First call. Have to do the setup
embark_assist::finder_ui::ui_setup(find_callback, max_inorganic);
}
Screen::show(dts::make_unique<ViewscreenFindUi>(), plugin_self);
if (!fileresult) {
Screen::show(dts::make_unique<ViewscreenFindUi>(), plugin_self);
}
else
{
load_profile();
find();
}
}
//===============================================================================

@ -10,7 +10,7 @@ using namespace DFHack;
namespace embark_assist {
namespace finder_ui {
void init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic);
void init(DFHack::Plugin *plugin_self, embark_assist::defs::find_callbacks find_callback, uint16_t max_inorganic, bool fileresult);
void activate();
void shutdown();
}

@ -167,25 +167,25 @@ namespace embark_assist{
help_text.push_back("DF's display of resources in the region DF currently displays. Secondly, the");
help_text.push_back("DF display doesn't take elevation based soil erosion or the magma sea depth");
help_text.push_back("into consideration, so it can display resources that actually are cut away.");
help_text.push_back("Thirdly, it takes 'intrusions', i.e. small sections of neighboring tiles'");
help_text.push_back("Thirdly, it takes 'incursions', i.e. small sections of neighboring tiles'");
help_text.push_back("biomes into consideration for many fields.");
help_text.push_back("(It can be noted that the DFHack Sand indicator does take the first two");
help_text.push_back("elements into account).");
help_text.push_back("The info the Embark Assistant displays is:");
help_text.push_back("Incompl. Survey if all intrusions couldn't be examined because that requires");
help_text.push_back("Incompl. Survey if all incursions couldn't be examined because that requires");
help_text.push_back("info from neighboring world tiles that haven't been surveyed.");
help_text.push_back("Sand, if present, including through intrusions.");
help_text.push_back("Clay, if present, including thorugh intrusions.");
help_text.push_back("Min and Max soil depth in the embark rectangle, including intrusions.");
help_text.push_back("Flat indicator if all the tiles and intrusions have the same elevation.");
help_text.push_back("Aquifer indicator: Part(ial) or Full, when present, including intrusions.");
help_text.push_back("Sand, if present, including through incursions.");
help_text.push_back("Clay, if present, including thorugh incursions.");
help_text.push_back("Min and Max soil depth in the embark rectangle, including incursions.");
help_text.push_back("Flat indicator if all the tiles and incursions have the same elevation.");
help_text.push_back("Aquifer indicator: Part(ial) or Full, when present, including incursions.");
help_text.push_back("Waterfall and largest Z level drop if the river has elevation differences");
help_text.push_back("Evil weather, when present: BR = Blood Rain, TS = Temporary Syndrome");
help_text.push_back("PS = Permanent Syndrome, Re = Reanimating, and Th = Thralling. Intrusions.");
help_text.push_back("Flux, if present. NOT allowing for small intrusion bits.");
help_text.push_back("A list of all metals present in the embark. Not intrusions.");
help_text.push_back("PS = Permanent Syndrome, Re = Reanimating, and Th = Thralling. Incursions.");
help_text.push_back("Flux, if present. NOT allowing for small incursion bits.");
help_text.push_back("A list of all metals present in the embark. Not incursions.");
help_text.push_back("A list of all economic minerals present in the embark. Both clays and flux");
help_text.push_back("stones are economic, so they show up here as well. Not intrusions.");
help_text.push_back("stones are economic, so they show up here as well. Not incursions.");
help_text.push_back("In addition to the above, the Find functionality can also produce blinking");
help_text.push_back("overlays over the Local, Region, and World maps to indicate where");
help_text.push_back("matching embarks are found. The Local display marks the top left corner of");
@ -256,7 +256,7 @@ namespace embark_assist{
help_text.push_back("block) at a time, and the results are displayed as green inverted X on");
help_text.push_back("the same map (replacing or erasing the yellow ones). Local map overlay");
help_text.push_back("data is generated as well.");
help_text.push_back("Since 'intrusion' processing requires that the neighboring tiles that may");
help_text.push_back("Since 'incursion' processing requires that the neighboring tiles that may");
help_text.push_back("provide them are surveyed before the current tile and tiles have to be");
help_text.push_back("surveyed in some order, the find function can not perform a complete");
help_text.push_back("survey of prospective embarks that border world tiles yet to be surveyed");
@ -266,6 +266,19 @@ namespace embark_assist{
help_text.push_back("ones.");
help_text.push_back("");
help_text.push_back("Caveats & technical stuff:");
help_text.push_back("- The plugin does in fact allow for a single, optional case sensitive");
help_text.push_back(" parameter when invoked: 'fileresult'. When this parameter is provided");
help_text.push_back(" The plugin will read the search profile stored to file and immediately");
help_text.push_back(" initiate a search for matches. This search is performed twice to ensure");
help_text.push_back(" incursions are handled correctly, and then the number of matching world");
help_text.push_back(" is written to the file <DF>/data/init/embark_assistant_fileresult.txt.");
help_text.push_back(" It can be noted that this file is deleted before the first search is");
help_text.push_back(" started. The purpose of this mode is to allow external harnesses to");
help_text.push_back(" generate worlds, search them for matches, and use the file results to");
help_text.push_back(" to determine which worlds to keep. It can be noted that after the search");
help_text.push_back(" the plugin continues to work essentially as usual, including external");
help_text.push_back(" to terminate, and that the author plugin can provide no help when it comes");
help_text.push_back(" to setting up any kind of harness using the plugin functionality.");
help_text.push_back("- The Find searching uses simulated cursor movement input to DF to get it");
help_text.push_back(" to load feature shells and detailed region data, and this costs the");
help_text.push_back(" least when done one feature shell at a time.");
@ -274,14 +287,17 @@ namespace embark_assist{
help_text.push_back(" set of preliminary matches (yellow tiles) than a previous search.");
help_text.push_back(" Note that the first search can miss a fair number of matches for");
help_text.push_back(" technical reasons discussed above and below.");
break;
case pages::Caveats_2:
Screen::drawBorder(" Embark Assistant Help/Info Caveats 2 Page ");
help_text.push_back("- The site info is deduced by the author, so there may be errors and");
help_text.push_back(" there are probably site types that end up not being identified.");
help_text.push_back("- Aquifer indications are based on the author's belief that they occur");
help_text.push_back(" whenever an aquifer supporting layer is present at a depth of 3 or");
help_text.push_back(" more.");
help_text.push_back("- The biome determination logic comes from code provided by Ragundo,");
help_text.push_back(" with only marginal changes by the author. References can be found in");
help_text.push_back(" the source file.");
help_text.push_back("- Thralling is determined by whether material interactions causes");
help_text.push_back(" blinking, which the author believes is one of 4 thralling changes.");
help_text.push_back("- The geo information is gathered by code which is essentially a");
@ -293,12 +309,6 @@ namespace embark_assist{
help_text.push_back(" reaching caverns that have been removed at world gen to fail to be");
help_text.push_back(" generated at all. It's likely this bug also affects magma pools.");
help_text.push_back(" This plugin does not address this but scripts can correct it.");
break;
case pages::Caveats_2:
Screen::drawBorder(" Embark Assistant Help/Info Caveats 2 Page ");
help_text.push_back("- The plugin detects 'incursions' of neighboring tiles into embarks, but");
help_text.push_back(" this functionality is incomplete when the incursion comes from a");
help_text.push_back(" neighboring tile that hasn't been surveyed yet. The embark info displays");
@ -314,9 +324,9 @@ namespace embark_assist{
help_text.push_back(" economics/minerals (including Flux and Coal) as any volumes are typically");
help_text.push_back(" too small to be of interest. Rivers, Waterfalls, Spires, and Magma Pools");
help_text.push_back(" are not incursion related features.");
help_text.push_back("- There are special rules for handing of intrusions from Lakes and Oceans,");
help_text.push_back("- There are special rules for handing of incursions from Lakes and Oceans,");
help_text.push_back(" as well as Mountains into everything that isn't a Lake or Ocean, and the");
help_text.push_back(" rules state that these intrusions should be reversed (i.e. 'normal' biomes");
help_text.push_back(" rules state that these incursions should be reversed (i.e. 'normal' biomes");
help_text.push_back(" should push into Lakes, Oceans, and Mountains, even when the indicators");
help_text.push_back(" say otherwise). This rule is clear for edges, but not for corners, as it");
help_text.push_back(" does not specify which of the potentially multiple 'superior' biomes");
@ -325,7 +335,7 @@ namespace embark_assist{
help_text.push_back(" the N, followed by the one to the W, and lastly the one acting as the");
help_text.push_back(" reference. This means there's a risk embarks with such 'trouble' corners");
help_text.push_back(" may get affected corner(s) evaluated incorrectly.");
help_text.push_back("Version 0.9 2019-07-12");
help_text.push_back("Version 0.10 2019-09-21");
break;
}

@ -2,6 +2,7 @@
#include <modules/Gui.h>
#include "Core.h"
#include "DataDefs.h"
#include "df/biome_type.h"
#include "df/inorganic_raw.h"
@ -176,8 +177,8 @@ namespace embark_assist {
result->sand_found = true;
}
// Flux. N/A for intrusions.
// Coal. N/A for intrusions
// Flux. N/A for incursions.
// Coal. N/A for incursions
// Min Soil
if (finder->soil_min != embark_assist::defs::soil_ranges::NA &&
@ -270,8 +271,8 @@ namespace embark_assist {
result->thralling_found = true;
}
// Spires. N/A for intrusions
// Magma. N/A for intrusions
// Spires. N/A for incursions
// Magma. N/A for incursions
// Biomes
result->biomes[survey_results->at(x).at(y).biome[mlt->biome_offset]] = true;
@ -279,8 +280,8 @@ namespace embark_assist {
// Region Type
result->region_types[world_data->regions[survey_results->at(x).at(y).biome_index[mlt->biome_offset]]->type] = true;
// Metals. N/A for intrusions
// Economics. N/A for intrusions
// Metals. N/A for incursions
// Economics. N/A for incursions
}
//=======================================================================================
@ -2076,7 +2077,7 @@ namespace embark_assist {
uint32_t preliminary_world_match(embark_assist::defs::world_tile_data *survey_results,
embark_assist::defs::finders *finder,
embark_assist::defs::match_results *match_results) {
// color_ostream_proxy out(Core::getInstance().getConsole());
// color_ostream_proxy out(Core::getInstance().getConsole());
uint32_t count = 0;
for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) {
for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) {
@ -2299,6 +2300,8 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter
while (screen->location.region_pos.x != 0 || screen->location.region_pos.y != 0) {
screen->feed_key(df::interface_key::CURSOR_UPLEFT_FAST);
}
iterator->target_location_x = 0;
iterator->target_location_y = 0;
iterator->active = true;
iterator->i = 0;
iterator->k = 0;
@ -2327,21 +2330,24 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter
for (uint16_t l = 0; l <= x_end; l++) {
for (uint16_t m = 0; m <= y_end; m++) {
// This is where the payload goes
if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).preliminary_match) {
if (!survey_results->at(iterator->target_location_x).at(iterator->target_location_y).surveyed ||
match_results->at(iterator->target_location_x).at(iterator->target_location_y).preliminary_match) {
move_cursor(iterator->target_location_x, iterator->target_location_y);
match_world_tile(geo_summary,
survey_results,
&iterator->finder,
match_results,
screen->location.region_pos.x,
screen->location.region_pos.y);
if (match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).contains_match) {
iterator->target_location_x,
iterator->target_location_y);
if (match_results->at(iterator->target_location_x).at(iterator->target_location_y).contains_match) {
iterator->count++;
}
}
else {
for (uint16_t n = 0; n < 16; n++) {
for (uint16_t p = 0; p < 16; p++) {
match_results->at(screen->location.region_pos.x).at(screen->location.region_pos.y).mlt_match[n][p] = false;
match_results->at(iterator->target_location_x).at(iterator->target_location_y).mlt_match[n][p] = false;
}
}
}
@ -2349,15 +2355,15 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter
if (m != y_end) {
if (iterator->y_down) {
screen->feed_key(df::interface_key::CURSOR_DOWN);
if (iterator->target_location_y < world->worldgen.worldgen_parms.dim_y - 1) iterator->target_location_y++;
}
else {
screen->feed_key(df::interface_key::CURSOR_UP);
if (iterator->target_location_y > 0) iterator->target_location_y--;
}
}
else {
if (screen->location.region_pos.x != 0 &&
screen->location.region_pos.x != world->worldgen.worldgen_parms.dim_x - 1) {
if (iterator->target_location_x != 0 &&
iterator->target_location_x != world->worldgen.worldgen_parms.dim_x - 1) {
turn = true;
}
else {
@ -2370,24 +2376,24 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter
}
else {
if (iterator->y_down) {
screen->feed_key(df::interface_key::CURSOR_DOWN);
if (iterator->target_location_y < world->worldgen.worldgen_parms.dim_y - 1) iterator->target_location_y++;
}
else {
screen->feed_key(df::interface_key::CURSOR_UP);
if (iterator->target_location_y > 0) iterator->target_location_y--;
}
}
}
}
if (iterator->x_right) { // Won't do anything at the edge, so we don't bother filter those cases.
screen->feed_key(df::interface_key::CURSOR_RIGHT);
if (iterator->target_location_x < world->worldgen.worldgen_parms.dim_x - 1) iterator->target_location_x++;
}
else {
screen->feed_key(df::interface_key::CURSOR_LEFT);
if (iterator->target_location_x > 0) iterator->target_location_x--;
}
if (!iterator->x_right &&
screen->location.region_pos.x == 0) {
iterator->target_location_x == 0) {
turn = !turn;
if (turn) {
@ -2395,7 +2401,7 @@ uint16_t embark_assist::matcher::find(embark_assist::defs::match_iterators *iter
}
}
else if (iterator->x_right &&
screen->location.region_pos.x == world->worldgen.worldgen_parms.dim_x - 1) {
iterator->target_location_x == world->worldgen.worldgen_parms.dim_x - 1) {
turn = !turn;
if (turn) {

@ -54,6 +54,9 @@ namespace embark_assist {
uint16_t match_count = 0;
uint16_t max_inorganic;
bool fileresult = false;
uint8_t fileresult_pass = 0;
};
static states *state = nullptr;
@ -113,7 +116,7 @@ namespace embark_assist {
}
else if (input->count(df::interface_key::CUSTOM_F)) {
if (!state->match_active && !state->matching) {
embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic);
embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, false);
}
}
else if (input->count(df::interface_key::CUSTOM_I)) {
@ -311,6 +314,7 @@ void embark_assist::overlay::match_progress(uint16_t count, embark_assist::defs:
// color_ostream_proxy out(Core::getInstance().getConsole());
state->matching = !done;
state->match_count = count;
for (uint16_t i = 0; i < world->worldgen.worldgen_parms.dim_x; i++) {
for (uint16_t k = 0; k < world->worldgen.worldgen_parms.dim_y; k++) {
if (match_results->at(i).at(k).preliminary_match) {
@ -324,6 +328,18 @@ void embark_assist::overlay::match_progress(uint16_t count, embark_assist::defs:
}
}
}
if (done && state->fileresult) {
state->fileresult_pass++;
if (state->fileresult_pass == 1) {
embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, true);
}
else {
FILE* outfile = fopen(fileresult_file_name, "w");
fprintf(outfile, "%i\n", count);
fclose(outfile);
}
}
}
//====================================================================
@ -463,6 +479,14 @@ void embark_assist::overlay::clear_match_results() {
//====================================================================
void embark_assist::overlay::fileresult() {
// Have to search twice, as the first pass cannot be complete due to mutual dependencies.
state->fileresult = true;
embark_assist::finder_ui::init(embark_assist::overlay::plugin_self, state->find_callback, state->max_inorganic, true);
}
//====================================================================
void embark_assist::overlay::shutdown() {
if (state &&
state->world_match_grid) {

@ -32,6 +32,7 @@ namespace embark_assist {
void set_embark(embark_assist::defs::site_infos *site_info);
void set_mid_level_tile_match(embark_assist::defs::mlt_matches mlt_matches);
void clear_match_results();
void fileresult();
void shutdown();
}
}

@ -1043,11 +1043,11 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data
mlt->at(i).at(k).river_present = false;
mlt->at(i).at(k).river_elevation = 100;
if (details->rivers_vertical.active[i][k] == 1) {
if (details->rivers_vertical.active[i][k] != 0) {
mlt->at(i).at(k).river_present = true;
mlt->at(i).at(k).river_elevation = details->rivers_vertical.elevation[i][k];
}
else if (details->rivers_horizontal.active[i][k] == 1) {
else if (details->rivers_horizontal.active[i][k] != 0) {
mlt->at(i).at(k).river_present = true;
mlt->at(i).at(k).river_elevation = details->rivers_horizontal.elevation[i][k];
}
@ -1179,6 +1179,51 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data
}
}
// This is messy. DF has some weird logic to leave out river bends with a South and an East connection, as well
// as river sources (and presumably sinks) that are to the North or the West of the connecting river.
// Experiments indicate these implicit river bends inherit their River Elevation from the lower of the two
// "parents", and it's assumed river sources and sinks similarly inherit it from their sole "parent".
// Two issues are known:
// - Lake and Ocean tiles may be marked as having a river when DF doesn't. However, DF does allow for rivers to
// exist in Ocean/Lake tiles, as well as sources/sinks.
// - DF generates rivers on/under glaciers, but does not display them (as they're frozen), nor are their names
// displayed.
//
for (uint8_t i = 1; i < 16; i++) {
for (uint8_t k = 0; k < 15; k++) {
if (details->rivers_horizontal.active[i][k] != 0 &&
details->rivers_vertical.active[i - 1][k + 1] != 0 &&
!mlt->at(i - 1).at(k).river_present) { // Probably never true
mlt->at(i - 1).at(k).river_present = true;
mlt->at(i - 1).at(k).river_elevation = mlt->at(i).at(k).river_elevation;
if (mlt->at(i - 1).at(k).river_elevation > mlt->at(i - 1).at(k + 1).river_elevation) {
mlt->at(i - 1).at(k).river_elevation = mlt->at(i - 1).at(k + 1).river_elevation;
}
}
}
}
for (uint8_t i = 0; i < 16; i++) {
for (uint8_t k = 1; k < 16; k++) {
if (details->rivers_vertical.active[i][k] != 0 &&
!mlt->at(i).at(k - 1).river_present) {
mlt->at(i).at(k - 1).river_present = true;
mlt->at(i).at(k - 1).river_elevation = mlt->at(i).at(k).river_elevation;
}
}
}
for (uint8_t i = 1; i < 16; i++) {
for (uint8_t k = 0; k < 16; k++) {
if (details->rivers_horizontal.active[i][k] != 0 &&
!mlt->at(i - 1).at(k).river_present) {
mlt->at(i - 1).at(k).river_present = true;
mlt->at(i - 1).at(k).river_elevation = mlt->at(i).at(k).river_elevation;
}
}
}
survey_results->at(x).at(y).aquifer_count = 0;
survey_results->at(x).at(y).clay_count = 0;
survey_results->at(x).at(y).sand_count = 0;
@ -1366,7 +1411,7 @@ void embark_assist::survey::survey_mid_level_tile(embark_assist::defs::geo_data
for (uint8_t i = 0; i < 16; i++) {
for (uint8_t k = 0; k < 16; k++) {
tile->region_type[i][k] = world_data->regions[tile->biome[mlt->at(i).at(k).biome_offset]]->type;
tile->region_type[i][k] = world_data->regions[tile->biome_index[mlt->at(i).at(k).biome_offset]]->type;
}
}

@ -1,5 +1,4 @@
// (un)designate matching plants for gathering/cutting
#include <set>
#include "Core.h"
@ -11,12 +10,14 @@
#include "df/map_block.h"
#include "df/plant.h"
#include "df/plant_growth.h"
#include "df/plant_raw.h"
#include "df/tile_dig_designation.h"
#include "df/world.h"
#include "modules/Designations.h"
#include "modules/Maps.h"
#include "modules/Materials.h"
using std::string;
using std::vector;
@ -27,15 +28,136 @@ using namespace df::enums;
DFHACK_PLUGIN("getplants");
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(cur_year_tick);
enum class selectability {
Selectable,
Grass,
Nonselectable,
OutOfSeason,
Unselected
};
//selectability selectablePlant(color_ostream &out, const df::plant_raw *plant)
selectability selectablePlant(const df::plant_raw *plant)
{
const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type_basic_mat, plant->material_defs.idx_basic_mat);
bool outOfSeason = false;
if (plant->flags.is_set(plant_raw_flags::TREE))
{
// out.print("%s is a selectable tree\n", plant->id.c_str());
return selectability::Selectable;
}
else if (plant->flags.is_set(plant_raw_flags::GRASS))
{
// out.print("%s is a non selectable Grass\n", plant->id.c_str());
return selectability::Grass;
}
if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) ||
basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))
{
// out.print("%s is edible\n", plant->id.c_str());
return selectability::Selectable;
}
if (plant->flags.is_set(plant_raw_flags::THREAD) ||
plant->flags.is_set(plant_raw_flags::MILL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL))
{
// out.print("%s is thread/mill/extract\n", plant->id.c_str());
return selectability::Selectable;
}
if (basic_mat.material->reaction_product.id.size() > 0 ||
basic_mat.material->reaction_class.size() > 0)
{
// out.print("%s has a reaction\n", plant->id.c_str());
return selectability::Selectable;
}
for (size_t i = 0; i < plant->growths.size(); i++)
{
if (plant->growths[i]->item_type == df::item_type::SEEDS || // Only trees have seed growths in vanilla, but raws can be modded...
plant->growths[i]->item_type == df::item_type::PLANT_GROWTH)
{
const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant->growths[i]->mat_type, plant->growths[i]->mat_index);
if ((plant->growths[i]->item_type == df::item_type::SEEDS &&
(growth_mat.material->flags.is_set(material_flags::EDIBLE_COOKED) ||
growth_mat.material->flags.is_set(material_flags::EDIBLE_RAW))) ||
(plant->growths[i]->item_type == df::item_type::PLANT_GROWTH &&
growth_mat.material->flags.is_set(material_flags::LEAF_MAT))) // Will change name to STOCKPILE_PLANT_GROWTH any day now...
{
if (*cur_year_tick >= plant->growths[i]->timing_1 &&
(plant->growths[i]->timing_2 == -1 ||
*cur_year_tick <= plant->growths[i]->timing_2))
{
// out.print("%s has an edible seed or a stockpile growth\n", plant->id.c_str());
return selectability::Selectable;
}
else
{
outOfSeason = true;
}
}
}
/* else if (plant->growths[i]->behavior.bits.has_seed) // This code designates beans, etc. when DF doesn't, but plant gatherers still fail to collect anything, so it's useless: bug #0006940.
{
const DFHack::MaterialInfo seed_mat = DFHack::MaterialInfo(plant->material_defs.type_seed, plant->material_defs.idx_seed);
if (seed_mat.material->flags.is_set(material_flags::EDIBLE_RAW) ||
seed_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))
{
if (*cur_year_tick >= plant->growths[i]->timing_1 &&
(plant->growths[i]->timing_2 == -1 ||
*cur_year_tick <= plant->growths[i]->timing_2))
{
return selectability::Selectable;
}
else
{
outOfSeason = true;
}
}
} */
}
if (outOfSeason)
{
// out.print("%s has an out of season growth\n", plant->id.c_str());
return selectability::OutOfSeason;
}
else
{
// out.printerr("%s cannot be gathered\n", plant->id.c_str());
return selectability::Nonselectable;
}
}
command_result df_getplants (color_ostream &out, vector <string> & parameters)
{
string plantMatStr = "";
set<int> plantIDs;
std::vector<selectability> plantSelections;
std::vector<size_t> collectionCount;
set<string> plantNames;
bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false;
bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false, verbose = false;
int count = 0;
plantSelections.resize(world->raws.plants.all.size());
collectionCount.resize(world->raws.plants.all.size());
for (size_t i = 0; i < plantSelections.size(); i++)
{
plantSelections[i] = selectability::Unselected;
collectionCount[i] = 0;
}
bool anyPlantsSelected = false;
for (size_t i = 0; i < parameters.size(); i++)
{
if(parameters[i] == "help" || parameters[i] == "?")
@ -50,6 +172,8 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
exclude = true;
else if(parameters[i] == "-a")
all = true;
else if(parameters[i] == "-v")
verbose = true;
else
plantNames.insert(parameters[i]);
}
@ -75,11 +199,35 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
{
df::plant_raw *plant = world->raws.plants.all[i];
if (all)
plantIDs.insert(i);
else if (plantNames.find(plant->id) != plantNames.end())
{
// plantSelections[i] = selectablePlant(out, plant);
plantSelections[i] = selectablePlant(plant);
}
else if (plantNames.find(plant->id) != plantNames.end())
{
plantNames.erase(plant->id);
plantIDs.insert(i);
// plantSelections[i] = selectablePlant(out, plant);
plantSelections[i] = selectablePlant(plant);
switch (plantSelections[i])
{
case selectability::Grass:
out.printerr("%s is a grass and cannot be gathered\n", plant->id.c_str());
break;
case selectability::Nonselectable:
out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str());
break;
case selectability::OutOfSeason:
out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str());
break;
case selectability::Selectable:
break;
case selectability::Unselected:
break; // We won't get to this option
}
}
}
if (plantNames.size() > 0)
@ -91,15 +239,44 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
return CR_FAILURE;
}
if (plantIDs.size() == 0)
for (size_t i = 0; i < plantSelections.size(); i++)
{
if (plantSelections[i] == selectability::OutOfSeason ||
plantSelections[i] == selectability::Selectable)
{
anyPlantsSelected = true;
break;
}
}
if (!anyPlantsSelected)
{
out.print("Valid plant IDs:\n");
for (size_t i = 0; i < world->raws.plants.all.size(); i++)
{
df::plant_raw *plant = world->raws.plants.all[i];
if (plant->flags.is_set(plant_raw_flags::GRASS))
// switch (selectablePlant(out, plant))
switch (selectablePlant(plant))
{
case selectability::Grass:
case selectability::Nonselectable:
continue;
out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str());
case selectability::OutOfSeason:
{
out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str());
break;
}
case selectability::Selectable:
{
out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str());
break;
}
case selectability::Unselected: // Should never get this alternative
break;
}
}
return CR_OK;
}
@ -113,9 +290,11 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
int x = plant->pos.x % 16;
int y = plant->pos.y % 16;
if (plantIDs.find(plant->material) != plantIDs.end())
if (plantSelections[plant->material] == selectability::OutOfSeason ||
plantSelections[plant->material] == selectability::Selectable)
{
if (exclude)
if (exclude ||
plantSelections[plant->material] == selectability::OutOfSeason)
continue;
}
else
@ -134,15 +313,30 @@ command_result df_getplants (color_ostream &out, vector <string> & parameters)
continue;
if (deselect && Designations::unmarkPlant(plant))
{
collectionCount[plant->material]++;
++count;
}
if (!deselect && Designations::markPlant(plant))
{
// out.print("Designated %s at (%i, %i, %i), %d\n", world->raws.plants.all[plant->material]->id.c_str(), plant->pos.x, plant->pos.y, plant->pos.z, (int)i);
collectionCount[plant->material]++;
++count;
}
}
if (count)
out.print("Updated %d plant designations.\n", count);
{
if (verbose)
{
for (size_t i = 0; i < plantSelections.size(); i++)
{
if (collectionCount[i] > 0)
out.print("Updated %d %s designations.\n", (int)collectionCount[i], world->raws.plants.all[i]->id.c_str());
}
out.print("\n");
}
}
out.print("Updated %d plant designations.\n", (int)count);
return CR_OK;
}
@ -154,11 +348,12 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCom
" Specify the types of trees to cut down and/or shrubs to gather by their\n"
" plant IDs, separated by spaces.\n"
"Options:\n"
" -t - Select trees only (exclude shrubs)\n"
" -s - Select shrubs only (exclude trees)\n"
" -c - Clear designations instead of setting them\n"
" -x - Apply selected action to all plants except those specified\n"
" -a - Select every type of plant (obeys -t/-s)\n"
" -t - Tree: Select trees only (exclude shrubs)\n"
" -s - Shrub: Select shrubs only (exclude trees)\n"
" -c - Clear: Clear designations instead of setting them\n"
" -x - eXcept: Apply selected action to all plants except those specified\n"
" -a - All: Select every type of plant (obeys -t/-s)\n"
" -v - Verbose: lists the number of (un)designations per plant\n"
"Specifying both -t and -s will have no effect.\n"
"If no plant IDs are specified, all valid plant IDs will be listed.\n"
));

@ -457,6 +457,7 @@ message UnitAppearance
optional Hair beard = 6;
optional Hair moustache = 7;
optional Hair sideburns = 8;
optional string physical_description = 9;
}
message InventoryItem

@ -1,5 +1,5 @@
#include "df_version_int.h"
#define RFR_VERSION "0.20.2"
#define RFR_VERSION "0.20.3"
#include <cstdio>
#include <time.h>
@ -1670,9 +1670,9 @@ void GetWounds(df::unit_wound * wound, UnitWound * send_wound)
static command_result GetUnitListInside(color_ostream &stream, const BlockRequest *in, UnitList *out)
{
auto world = df::global::world;
for (size_t i = 0; i < world->units.all.size(); i++)
for (size_t i = 0; i < world->units.active.size(); i++)
{
df::unit * unit = world->units.all[i];
df::unit * unit = world->units.active[i];
auto send_unit = out->add_creature_list();
send_unit->set_id(unit->id);
send_unit->set_pos_x(unit->pos.x);
@ -1734,6 +1734,8 @@ static command_result GetUnitListInside(color_ostream &stream, const BlockReques
appearance->add_colors(unit->appearance.colors[j]);
appearance->set_size_modifier(unit->appearance.size_modifier);
appearance->set_physical_description(Units::getPhysicalDescription(unit));
send_unit->set_profession_id(unit->profession);
std::vector<Units::NoblePosition> pvec;

@ -30,6 +30,8 @@
// protobuf
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <fstream>
using std::endl;
using namespace DFHack;
using namespace df::enums;

@ -1 +1 @@
Subproject commit 03e96477ca84e42c87db93bd2d781c73687795a8
Subproject commit 4fdb2be54365442b8abea86f21746795f83fbdc2

@ -0,0 +1,510 @@
/*
* Tailor plugin. Automatically manages keeping your dorfs clothed.
* For best effect, place "tailor enable" in your dfhack.init configuration,
* or set AUTOENABLE to true.
*/
#include "Core.h"
#include "DataDefs.h"
#include "PluginManager.h"
#include "df/creature_raw.h"
#include "df/global_objects.h"
#include "df/historical_entity.h"
#include "df/itemdef_armorst.h"
#include "df/itemdef_glovesst.h"
#include "df/itemdef_helmst.h"
#include "df/itemdef_pantsst.h"
#include "df/itemdef_shoesst.h"
#include "df/items_other_id.h"
#include "df/job.h"
#include "df/job_type.h"
#include "df/manager_order.h"
#include "df/ui.h"
#include "df/world.h"
#include "modules/Maps.h"
#include "modules/Units.h"
#include "modules/Translation.h"
#include "modules/World.h"
using namespace DFHack;
using namespace std;
using df::global::world;
using df::global::ui;
DFHACK_PLUGIN("tailor");
#define AUTOENABLE false
DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui);
const char *tagline = "Allow the bookkeeper to queue jobs to keep dwarfs in adequate clothing.";
const char *usage = (
" tailor enable\n"
" Enable the plugin.\n"
" tailor disable\n"
" Disable the plugin.\n"
" tailor status\n"
" Display plugin status\n"
"\n"
"Whenever the bookkeeper updates stockpile records, this plugin will scan every unit in the fort,\n"
"count up the number that are worn, and then order enough more made to replace all worn items.\n"
"If there are enough replacement items in inventory to replace all worn items, the units wearing them\n"
"will have the worn items confiscated (in the same manner as the _cleanowned_ plugin) so that they'll\n"
"reeequip with replacement items.\n"
);
// ARMOR, SHOES, HELM, GLOVES, PANTS
// ah, if only STL had a bimap
static map<df::job_type, df::item_type> jobTypeMap = {
{ df::job_type::MakeArmor, df::item_type::ARMOR },
{ df::job_type::MakePants, df::item_type::PANTS },
{ df::job_type::MakeHelm, df::item_type::HELM },
{ df::job_type::MakeGloves, df::item_type::GLOVES },
{ df::job_type::MakeShoes, df::item_type::SHOES }
};
static map<df::item_type, df::job_type> itemTypeMap = {
{ df::item_type::ARMOR, df::job_type::MakeArmor },
{ df::item_type::PANTS, df::job_type::MakePants },
{ df::item_type::HELM, df::job_type::MakeHelm},
{ df::item_type::GLOVES, df::job_type::MakeGloves},
{ df::item_type::SHOES, df::job_type::MakeShoes}
};
void do_scan(color_ostream& out)
{
map<pair<df::item_type, int>, int> available; // key is item type & size
map<pair<df::item_type, int>, int> needed; // same
map<pair<df::item_type, int>, int> queued; // same
map<int, int> sizes; // this maps body size to races
map<tuple<df::job_type, int, int>, int> orders; // key is item type, item subtype, size
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_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(owned);
#undef F
available.empty();
needed.empty();
queued.empty();
orders.empty();
int silk = 0, yarn = 0, cloth = 0, leather = 0;
// scan for useable clothing
for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing"
{
if (i->flags.whole & bad_flags.whole)
continue;
if (i->flags.bits.owned)
continue;
if (i->getWear() >= 1)
continue;
df::item_type t = i->getType();
int size = world->raws.creatures.all[i->getMakerRace()]->adultsize;
available[make_pair(t, size)] += 1;
}
// scan for clothing raw materials
for (auto i : world->items.other[df::items_other_id::CLOTH])
{
if (i->flags.whole & bad_flags.whole)
continue;
if (!i->hasImprovements()) // only count dyed
continue;
MaterialInfo mat(i);
int ss = i->getStackSize();
if (mat.material)
{
if (mat.material->flags.is_set(df::material_flags::SILK))
silk += ss;
else if (mat.material->flags.is_set(df::material_flags::THREAD_PLANT))
cloth += ss;
else if (mat.material->flags.is_set(df::material_flags::YARN))
yarn += ss;
}
}
for (auto i : world->items.other[df::items_other_id::SKIN_TANNED])
{
if (i->flags.whole & bad_flags.whole)
continue;
leather += i->getStackSize();
}
out.print("available: silk %d yarn %d cloth %d leather %d\n", silk, yarn, cloth, leather);
// scan for units who need replacement clothing
for (auto u : world->units.active)
{
if (!Units::isOwnCiv(u) ||
!Units::isOwnGroup(u) ||
!Units::isActive(u) ||
Units::isBaby(u))
continue; // skip units we don't control
set <df::item_type> wearing;
wearing.empty();
deque<df::item*> worn;
worn.empty();
for (auto inv : u->inventory)
{
if (inv->mode != df::unit_inventory_item::Worn)
continue;
if (inv->item->getWear() > 0)
worn.push_back(inv->item);
else
wearing.insert(inv->item->getType());
}
int size = world->raws.creatures.all[u->race]->adultsize;
sizes[size] = u->race;
for (auto ty : set<df::item_type>{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES })
{
if (wearing.count(ty) == 0)
needed[make_pair(ty, size)] += 1;
}
for (auto w : worn)
{
auto ty = w->getType();
auto oo = itemTypeMap.find(ty);
if (oo == itemTypeMap.end())
continue;
df::job_type o = oo->second;
int size = world->raws.creatures.all[w->getMakerRace()]->adultsize;
std::string description;
w->getItemDescription(&description, 0);
if (available[make_pair(ty, size)] > 0)
{
if (w->flags.bits.owned)
{
bool confiscated = Items::setOwner(w, NULL);
out.print(
"%s %s from %s.\n",
(confiscated ? "Confiscated" : "Could not confiscate"),
description.c_str(),
Translation::TranslateName(&u->name, false).c_str()
);
}
if (wearing.count(ty) == 0)
available[make_pair(ty, size)] -= 1;
if (w->getWear() > 1)
w->flags.bits.dump = true;
}
else
{
// out.print("%s worn by %s needs replacement\n",
// description.c_str(),
// Translation::TranslateName(&u->name, false).c_str()
// );
orders[make_tuple(o, w->getSubtype(), size)] += 1;
}
}
}
auto entity = world->entities.all[ui->civ_id];
for (auto a : needed)
{
df::item_type ty = a.first.first;
int size = a.first.second;
int count = a.second;
int sub = 0;
vector<int16_t> v;
switch (ty) {
case df::item_type::ARMOR: v = entity->resources.armor_type; break;
case df::item_type::GLOVES: v = entity->resources.gloves_type; break;
case df::item_type::HELM: v = entity->resources.helm_type; break;
case df::item_type::PANTS: v = entity->resources.pants_type; break;
case df::item_type::SHOES: v = entity->resources.shoes_type; break;
default: break;
}
for (auto vv : v) {
bool isClothing = false;
switch (ty) {
case df::item_type::ARMOR: isClothing = world->raws.itemdefs.armor[vv] ->armorlevel == 0; break;
case df::item_type::GLOVES: isClothing = world->raws.itemdefs.gloves[vv]->armorlevel == 0; break;
case df::item_type::HELM: isClothing = world->raws.itemdefs.helms[vv] ->armorlevel == 0; break;
case df::item_type::PANTS: isClothing = world->raws.itemdefs.pants[vv] ->armorlevel == 0; break;
case df::item_type::SHOES: isClothing = world->raws.itemdefs.shoes[vv] ->armorlevel == 0; break;
default: break;
}
if (isClothing)
{
sub = vv;
break;
}
}
orders[make_tuple(itemTypeMap[ty], sub, size)] += count;
}
// scan orders
for (auto o : world->manager_orders)
{
auto f = jobTypeMap.find(o->job_type);
if (f == jobTypeMap.end())
continue;
auto sub = o->item_subtype;
int race = o->hist_figure_id;
if (race == -1)
continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs
int size = world->raws.creatures.all[race]->adultsize;
orders[make_tuple(o->job_type, sub, size)] -= o->amount_left;
}
// place orders
for (auto o : orders)
{
df::job_type ty;
int sub;
int size;
tie(ty, sub, size) = o.first;
int count = o.second;
if (count > 0)
{
vector<int16_t> v;
BitArray<df::armor_general_flags>* fl;
string name_s, name_p;
switch (ty) {
case df::job_type::MakeArmor:
name_s = world->raws.itemdefs.armor[sub]->name;
name_p = world->raws.itemdefs.armor[sub]->name_plural;
v = entity->resources.armor_type;
fl = &world->raws.itemdefs.armor[sub]->props.flags;
break;
case df::job_type::MakeGloves:
name_s = world->raws.itemdefs.gloves[sub]->name;
name_p = world->raws.itemdefs.gloves[sub]->name_plural;
v = entity->resources.gloves_type;
fl = &world->raws.itemdefs.gloves[sub]->props.flags;
break;
case df::job_type::MakeHelm:
name_s = world->raws.itemdefs.helms[sub]->name;
name_p = world->raws.itemdefs.helms[sub]->name_plural;
v = entity->resources.helm_type;
fl = &world->raws.itemdefs.helms[sub]->props.flags;
break;
case df::job_type::MakePants:
name_s = world->raws.itemdefs.pants[sub]->name;
name_p = world->raws.itemdefs.pants[sub]->name_plural;
v = entity->resources.pants_type;
fl = &world->raws.itemdefs.pants[sub]->props.flags;
break;
case df::job_type::MakeShoes:
name_s = world->raws.itemdefs.shoes[sub]->name;
name_p = world->raws.itemdefs.shoes[sub]->name_plural;
v = entity->resources.shoes_type;
fl = &world->raws.itemdefs.shoes[sub]->props.flags;
break;
default:
break;
}
bool can_make = false;
for (auto vv : v)
{
if (vv == sub)
{
can_make = true;
break;
}
}
if (!can_make)
{
out.print("Cannot make %s, skipped\n", name_p.c_str());
continue; // this civilization does not know how to make this item, so sorry
}
switch (ty) {
case df::item_type::ARMOR: break;
case df::item_type::GLOVES: break;
case df::item_type::HELM: break;
case df::item_type::PANTS: break;
case df::item_type::SHOES: break;
default: break;
}
df::job_material_category mat;
if (silk > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_silk;
silk -= count;
}
else if (cloth > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_cloth;
cloth -= count;
}
else if (yarn > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_yarn;
yarn -= count;
}
else if (leather > count + 10 && fl->is_set(df::armor_general_flags::LEATHER)) {
mat.whole = df::job_material_category::mask_leather;
leather -= count;
}
else // not enough appropriate material available
continue;
auto order = new df::manager_order();
order->job_type = ty;
order->item_type = df::item_type::NONE;
order->item_subtype = sub;
order->mat_type = -1;
order->mat_index = -1;
order->amount_left = count;
order->amount_total = count;
order->status.bits.validated = false;
order->status.bits.active = false;
order->id = world->manager_order_next_id++;
order->hist_figure_id = sizes[size];
order->material_category = mat;
world->manager_orders.push_back(order);
out.print("Added order #%d for %d %s %s (sized for %s)\n",
order->id,
count,
bitfield_to_string(order->material_category).c_str(),
(count > 1) ? name_p.c_str() : name_s.c_str(),
world->raws.creatures.all[order->hist_figure_id]->name[1].c_str()
);
}
}
}
#define DELTA_TICKS 600
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!enabled)
return CR_OK;
if (!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
bool found = false;
for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next)
{
if (link->item == NULL) continue;
if (link->item->job_type == df::enums::job_type::UpdateStockpileRecords)
{
found = true;
break;
}
}
if (found)
{
do_scan(out);
}
return CR_OK;
}
static command_result tailor_cmd(color_ostream &out, vector <string> & parameters) {
bool desired = enabled;
if (parameters.size() == 1)
{
if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")
{
desired = true;
}
else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")
{
desired = false;
}
else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?")
{
out.print("%s: %s\nUsage:\n%s", plugin_name, tagline, usage);
return CR_OK;
}
else if (parameters[0] == "test")
{
do_scan(out);
return CR_OK;
}
else if (parameters[0] != "status")
{
return CR_WRONG_USAGE;
}
}
else
return CR_WRONG_USAGE;
out.print("Tailor is %s %s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled");
enabled = desired;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable)
{
enabled = enable;
return CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
{
if (AUTOENABLE) {
enabled = true;
}
commands.push_back(PluginCommand(plugin_name, tagline, tailor_cmd, false, usage));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
return plugin_enable(out, false);
}

@ -5,11 +5,16 @@
#include "MemAccess.h"
#include "PluginManager.h"
#include "df/init.h"
using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("title-folder");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(init);
// SDL frees the old window title when changed
static std::string original_title;
@ -36,6 +41,12 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out);
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands)
{
if (init->display.flag.is_set(init_display_flags::TEXT))
{
// Don't bother initializing in text mode.
return CR_OK;
}
for (auto it = sdl_libs.begin(); it != sdl_libs.end(); ++it)
{
if ((sdl_handle = OpenPlugin(it->c_str())))
@ -92,6 +103,12 @@ DFhackCExport command_result plugin_enable (color_ostream &out, bool state)
if (state)
{
if (init->display.flag.is_set(init_display_flags::TEXT))
{
out.printerr("title-folder: cannot enable with PRINT_MODE:TEXT.\n");
return CR_FAILURE;
}
std::string path = Core::getInstance().p->getPath();
std::string folder;
size_t pos = path.find_last_of('/');

@ -1 +1 @@
Subproject commit 8ef283377c9830fb932ea888d89b551873af36cf
Subproject commit 61d8e935ffd3c705ac13082874e2a3cb0363251b