4515 lines
142 KiB
C++
4515 lines
142 KiB
C++
// Intention: help with activity zone management (auto-pasture animals, auto-pit goblins, ...)
|
|
//
|
|
// the following things would be nice:
|
|
// - dump info about pastures, pastured animals, count non-pastured tame animals, print gender info
|
|
// - help finding caged dwarves? (maybe even allow to build their cages for fast release)
|
|
// - dump info about caged goblins, animals, ...
|
|
// - count grass tiles on pastures, move grazers to new pasture if old pasture is empty
|
|
// move hungry unpastured grazers to pasture with grass
|
|
//
|
|
// What is working so far:
|
|
// - print detailed info about activity zone and units under cursor (mostly for checking refs and stuff)
|
|
// - mark a zone which is used for future assignment commands
|
|
// - assign single selected creature to a zone
|
|
// - mass-assign creatures using filters
|
|
// - unassign single creature under cursor from current zone
|
|
// - pitting own dwarves :)
|
|
// - full automation of handling mini-pastures over nestboxes:
|
|
// go through all pens, check if they are empty and placed over a nestbox
|
|
// find female tame egg-layer who is not assigned to another pen and assign it to nestbox pasture
|
|
// maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds
|
|
// state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used)
|
|
// - full automation of marking live-stock for slaughtering
|
|
// races can be added to a watchlist and it can be set how many male/female kids/adults are left alive
|
|
// adding to the watchlist can be automated as well.
|
|
// config for autobutcher (state and sleep setting) is saved the first time autobutcher is started
|
|
// config for watchlist entries is saved when they are created or modified
|
|
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <climits>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <ctime>
|
|
#include <cstdio>
|
|
using namespace std;
|
|
|
|
#include "Core.h"
|
|
#include "Console.h"
|
|
#include "Export.h"
|
|
#include "PluginManager.h"
|
|
#include "MiscUtils.h"
|
|
|
|
#include "LuaTools.h"
|
|
#include "DataFuncs.h"
|
|
|
|
#include "modules/Units.h"
|
|
#include "modules/Maps.h"
|
|
#include "modules/Gui.h"
|
|
#include "modules/Materials.h"
|
|
#include "modules/MapCache.h"
|
|
#include "modules/Buildings.h"
|
|
#include "modules/World.h"
|
|
#include "modules/Screen.h"
|
|
#include "MiscUtils.h"
|
|
#include <VTableInterpose.h>
|
|
|
|
#include "df/ui.h"
|
|
#include "df/world.h"
|
|
#include "df/world_raws.h"
|
|
#include "df/building_def.h"
|
|
#include "df/building_civzonest.h"
|
|
#include "df/building_cagest.h"
|
|
#include "df/building_chainst.h"
|
|
#include "df/building_nest_boxst.h"
|
|
#include "df/general_ref_building_civzone_assignedst.h"
|
|
#include <df/creature_raw.h>
|
|
#include <df/caste_raw.h>
|
|
#include "df/viewscreen_dwarfmodest.h"
|
|
#include "modules/Translation.h"
|
|
|
|
using std::vector;
|
|
using std::string;
|
|
using namespace DFHack;
|
|
using namespace df::enums;
|
|
using df::global::world;
|
|
using df::global::cursor;
|
|
using df::global::ui;
|
|
using df::global::ui_build_selector;
|
|
using df::global::gps;
|
|
|
|
using namespace DFHack::Gui;
|
|
|
|
command_result df_zone (color_ostream &out, vector <string> & parameters);
|
|
command_result df_autonestbox (color_ostream &out, vector <string> & parameters);
|
|
command_result df_autobutcher(color_ostream &out, vector <string> & parameters);
|
|
|
|
DFHACK_PLUGIN("zone");
|
|
|
|
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
|
|
|
|
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable);
|
|
|
|
const string zone_help =
|
|
"Allows easier management of pens/pastures, pits and cages.\n"
|
|
"Options:\n"
|
|
" set - set zone under cursor as default for future assigns\n"
|
|
" assign - assign creature(s) to a pen or pit\n"
|
|
" if no filters are used, a single unit must be selected.\n"
|
|
" can be followed by valid building id which will then be set.\n"
|
|
" building must be a pen/pasture, pit or cage.\n"
|
|
" slaughter - mark creature(s) for slaughter\n"
|
|
" if no filters are used, a single unit must be selected.\n"
|
|
" with filters named units are ignored unless specified.\n"
|
|
" unassign - unassign selected creature(s) from it's zone or cage\n"
|
|
" nick - give unit(s) nicknames (e.g. all units in a cage)\n"
|
|
" remnick - remove nicknames\n"
|
|
" tocages - assign to (multiple) built cages inside a pen/pasture\n"
|
|
" spreads creatures evenly among cages for faster hauling.\n"
|
|
" uinfo - print info about selected unit\n"
|
|
" zinfo - print info about zone(s) under cursor\n"
|
|
" verbose - print some more info, mostly useless debug stuff\n"
|
|
" filters - print list of supported filters\n"
|
|
" examples - print some usage examples\n"
|
|
;
|
|
|
|
const string zone_help_filters =
|
|
"Filters (to be used in combination with 'all' or 'count'):\n"
|
|
"These filters can not be used with the prefix 'not':"
|
|
" all - in combinations with zinfo/uinfo: print all zones/units\n"
|
|
" in combination with assign: process all units\n"
|
|
" should be used in combination with further filters\n"
|
|
" count - must be followed by number. process X units\n"
|
|
" should be used in combination with further filters\n"
|
|
" unassigned - not assigned to zone, chain or built cage\n"
|
|
" age - exact age. must be followed by number\n"
|
|
" minage - minimum age. must be followed by number\n"
|
|
" maxage - maximum age. must be followed by number\n"
|
|
"These filters can be used with the prefix 'not' (e.g. 'not own'):"
|
|
" race - must be followed by a race raw id (e.g. BIRD_TURKEY)\n"
|
|
" caged - in a built cage\n"
|
|
" own - from own civilization\n"
|
|
" war - trained war creature\n"
|
|
" tamed - tamed\n"
|
|
" named - has name or nickname\n"
|
|
" can be used to mark named units for slaughter\n"
|
|
" merchant - is a merchant / belongs to a merchant\n"
|
|
" can be used to pit merchants and slaughter their animals\n"
|
|
" (could have weird effects during trading, be careful)\n"
|
|
" trained - obvious\n"
|
|
" trainablewar - can be trained for war (and is not already trained)\n"
|
|
" trainablehunt- can be trained for hunting (and is not already trained)\n"
|
|
" male - obvious\n"
|
|
" female - obvious\n"
|
|
" egglayer - race lays eggs (use together with 'female')\n"
|
|
" grazer - is a grazer\n"
|
|
" milkable - race is milkable (use together with 'female')\n"
|
|
;
|
|
|
|
const string zone_help_examples =
|
|
"Example for assigning single units:\n"
|
|
" (ingame) move cursor to a pen/pasture or pit zone\n"
|
|
" (dfhack) 'zone set' to use this zone for future assignments\n"
|
|
" (dfhack) map 'zone assign' to a hotkey of your choice\n"
|
|
" (ingame) select unit with 'v', 'k' or from unit list or inside a cage\n"
|
|
" (ingame) press hotkey to assign unit to it's new home (or pit)\n"
|
|
"Examples for assigning with filters:\n"
|
|
" (this assumes you have already set up a target zone)\n"
|
|
" zone assign all own grazer maxage 10\n"
|
|
" zone assign all own milkable not grazer\n"
|
|
" zone assign count 5 own female milkable\n"
|
|
" zone assign all own race DWARF maxage 2\n"
|
|
" throw all useless kids into a pit :)\n"
|
|
"Notes:\n"
|
|
" Unassigning per filters ignores built cages and chains currently. Usually you\n"
|
|
" should always use the filter 'own' (which implies tame) unless you want to\n"
|
|
" use the zone tool for pitting hostiles. 'own' ignores own dwarves unless you\n"
|
|
" specify 'race DWARF' and it ignores merchants and their animals unless you\n"
|
|
" specify 'merchant' (so it's safe to use 'assign all own' to one big pasture\n"
|
|
" if you want to have all your animals at the same place).\n"
|
|
" 'egglayer' and 'milkable' should be used together with 'female'\n"
|
|
" well, unless you have a mod with egg-laying male elves who give milk...\n";
|
|
|
|
|
|
const string autonestbox_help =
|
|
"Assigns unpastured female egg-layers to nestbox zones.\n"
|
|
"Requires that you create pen/pasture zones above nestboxes.\n"
|
|
"If the pen is bigger than 1x1 the nestbox must be in the top left corner.\n"
|
|
"Only 1 unit will be assigned per pen, regardless of the size.\n"
|
|
"The age of the units is currently not checked, most birds grow up quite fast.\n"
|
|
"When called without options autonestbox will instantly run once.\n"
|
|
"Options:\n"
|
|
" start - run every X frames (df simulation ticks)\n"
|
|
" default: X=6000 (~60 seconds at 100fps)\n"
|
|
" stop - stop running automatically\n"
|
|
" sleep X - change timer to sleep X frames between runs.\n";
|
|
|
|
const string autobutcher_help =
|
|
"Assigns your lifestock for slaughter once it reaches a specific count. Requires\n"
|
|
"that you add the target race(s) to a watch list. Only tame units will be\n"
|
|
"processed. Named units will be completely ignored (you can give animals\n"
|
|
"nicknames with the tool 'rename unit' to protect them from getting slaughtered\n"
|
|
"automatically. Trained war or hunting pets will be ignored.\n"
|
|
"Once you have too much adults, the oldest will be butchered first.\n"
|
|
"Once you have too much kids, the youngest will be butchered first.\n"
|
|
"If you don't set a target count the following default will be used:\n"
|
|
"1 male kid, 5 female kids, 1 male adult, 5 female adults.\n"
|
|
"Options:\n"
|
|
" start - run every X frames (df simulation ticks)\n"
|
|
" default: X=6000 (~60 seconds at 100fps)\n"
|
|
" stop - stop running automatically\n"
|
|
" sleep X - change timer to sleep X frames between runs.\n"
|
|
" watch R - start watching race(s)\n"
|
|
" R = valid race RAW id (ALPACA, BIRD_TURKEY, etc)\n"
|
|
" or a list of RAW ids seperated by spaces\n"
|
|
" or the keyword 'all' which affects your whole current watchlist.\n"
|
|
" unwatch R - stop watching race(s)\n"
|
|
" the current target settings will be remembered\n"
|
|
" forget R - unwatch race(s) and forget target settings for it/them\n"
|
|
" autowatch - automatically adds all new races (animals you buy\n"
|
|
" from merchants, tame yourself or get from migrants)\n"
|
|
" to the watch list using default target count\n"
|
|
" noautowatch - stop auto-adding new races to the watch list\n"
|
|
" list - print status and watchlist\n"
|
|
" list_export - print status and watchlist in batchfile format\n"
|
|
" can be used to copy settings into another savegame\n"
|
|
" usage: 'dfhack-run autobutcher list_export > xyz.bat' \n"
|
|
" target fk mk fa ma R\n"
|
|
" - set target count for specified race:\n"
|
|
" fk = number of female kids\n"
|
|
" mk = number of male kids\n"
|
|
" fa = number of female adults\n"
|
|
" ma = number of female adults\n"
|
|
" R = 'all' sets count for all races on the current watchlist\n"
|
|
" including the races which are currenly set to 'unwatched'\n"
|
|
" and sets the new default for future watch commands\n"
|
|
" R = 'new' sets the new default for future watch commands\n"
|
|
" without changing your current watchlist\n"
|
|
" example - print some usage examples\n";
|
|
|
|
const string autobutcher_help_example =
|
|
"Examples:\n"
|
|
" autobutcher target 4 3 2 1 ALPACA BIRD_TURKEY\n"
|
|
" autobutcher watch ALPACA BIRD_TURKEY\n"
|
|
" autobutcher start\n"
|
|
" This means you want to have max 7 kids (4 female, 3 male) and max 3 adults\n"
|
|
" (2 female, 1 male) of the races alpaca and turkey. Once the kids grow up the\n"
|
|
" oldest adults will get slaughtered. Excess kids will get slaughtered starting\n"
|
|
" the the youngest to allow that the older ones grow into adults.\n"
|
|
" autobutcher target 0 0 0 0 new\n"
|
|
" autobutcher autowatch\n"
|
|
" autobutcher start\n"
|
|
" This tells autobutcher to automatically put all new races onto the watchlist\n"
|
|
" and mark unnamed tame units for slaughter as soon as they arrive in your\n"
|
|
" fortress. Settings already made for some races will be left untouched.\n";
|
|
|
|
command_result init_autobutcher(color_ostream &out);
|
|
command_result cleanup_autobutcher(color_ostream &out);
|
|
command_result start_autobutcher(color_ostream &out);
|
|
|
|
command_result init_autonestbox(color_ostream &out);
|
|
command_result cleanup_autonestbox(color_ostream &out);
|
|
command_result start_autonestbox(color_ostream &out);
|
|
|
|
|
|
///////////////
|
|
// stuff for autonestbox and autobutcher
|
|
// should be moved to own plugin once the tool methods it shares with the zone plugin are moved to Unit.h / Building.h
|
|
|
|
command_result autoNestbox( color_ostream &out, bool verbose );
|
|
command_result autoButcher( color_ostream &out, bool verbose );
|
|
|
|
static bool enable_autonestbox = false;
|
|
static bool enable_autobutcher = false;
|
|
static bool enable_autobutcher_autowatch = false;
|
|
static size_t sleep_autonestbox = 6000;
|
|
static size_t sleep_autobutcher = 6000;
|
|
static bool autonestbox_did_complain = false; // avoids message spam
|
|
|
|
static PersistentDataItem config_autobutcher;
|
|
static PersistentDataItem config_autonestbox;
|
|
|
|
|
|
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
|
|
{
|
|
switch (event)
|
|
{
|
|
case DFHack::SC_MAP_LOADED:
|
|
// initialize from the world just loaded
|
|
init_autobutcher(out);
|
|
init_autonestbox(out);
|
|
break;
|
|
case DFHack::SC_MAP_UNLOADED:
|
|
enable_autonestbox = false;
|
|
enable_autobutcher = false;
|
|
// cleanup
|
|
cleanup_autobutcher(out);
|
|
cleanup_autonestbox(out);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
|
|
{
|
|
static size_t ticks_autonestbox = 0;
|
|
static size_t ticks_autobutcher = 0;
|
|
|
|
if(enable_autonestbox)
|
|
{
|
|
if(++ticks_autonestbox >= sleep_autonestbox)
|
|
{
|
|
ticks_autonestbox = 0;
|
|
autoNestbox(out, false);
|
|
}
|
|
}
|
|
|
|
if(enable_autobutcher)
|
|
{
|
|
if(++ticks_autobutcher >= sleep_autobutcher)
|
|
{
|
|
ticks_autobutcher= 0;
|
|
autoButcher(out, false);
|
|
}
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
|
|
///////////////
|
|
// Various small tool functions
|
|
// probably many of these should be moved to Unit.h and Building.h sometime later...
|
|
|
|
int32_t getUnitAge(df::unit* unit);
|
|
bool isTame(df::unit* unit);
|
|
bool isTrained(df::unit* unit);
|
|
bool isWar(df::unit* unit);
|
|
bool isHunter(df::unit* unit);
|
|
bool isOwnCiv(df::unit* unit);
|
|
bool isMerchant(df::unit* unit);
|
|
bool isForest(df::unit* unit);
|
|
|
|
bool isActivityZone(df::building * building);
|
|
bool isPenPasture(df::building * building);
|
|
bool isPitPond(df::building * building);
|
|
bool isActive(df::building * building);
|
|
|
|
int32_t findBuildingIndexById(int32_t id);
|
|
int32_t findPenPitAtCursor();
|
|
int32_t findCageAtCursor();
|
|
int32_t findChainAtCursor();
|
|
|
|
df::general_ref_building_civzone_assignedst * createCivzoneRef();
|
|
bool unassignUnitFromBuilding(df::unit* unit);
|
|
command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose);
|
|
void unitInfo(color_ostream & out, df::unit* creature, bool verbose);
|
|
void zoneInfo(color_ostream & out, df::building* building, bool verbose);
|
|
void cageInfo(color_ostream & out, df::building* building, bool verbose);
|
|
void chainInfo(color_ostream & out, df::building* building, bool verbose);
|
|
bool isBuiltCageAtPos(df::coord pos);
|
|
bool isInBuiltCageRoom(df::unit*);
|
|
bool isNaked(df::unit *);
|
|
bool isTamable(df::unit *);
|
|
|
|
int32_t getUnitAge(df::unit* unit)
|
|
{
|
|
// If the birthday this year has not yet passed, subtract one year.
|
|
// ASSUMPTION: birth_time is on the same scale as cur_year_tick
|
|
int32_t yearDifference = *df::global::cur_year - unit->relations.birth_year;
|
|
if (unit->relations.birth_time >= *df::global::cur_year_tick)
|
|
yearDifference--;
|
|
return yearDifference;
|
|
}
|
|
|
|
bool isDead(df::unit* unit)
|
|
{
|
|
return unit->flags1.bits.dead;
|
|
}
|
|
|
|
// ignore vampires, they should be treated like normal dwarves
|
|
bool isUndead(df::unit* unit)
|
|
{
|
|
return (unit->flags3.bits.ghostly ||
|
|
( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING)
|
|
&& !unit->curse.add_tags1.bits.BLOODSUCKER ));
|
|
}
|
|
|
|
bool isMerchant(df::unit* unit)
|
|
{
|
|
return unit->flags1.bits.merchant;
|
|
}
|
|
|
|
bool isForest(df::unit* unit)
|
|
{
|
|
return unit->flags1.bits.forest;
|
|
}
|
|
|
|
bool isMarkedForSlaughter(df::unit* unit)
|
|
{
|
|
return unit->flags2.bits.slaughter;
|
|
}
|
|
|
|
void doMarkForSlaughter(df::unit* unit)
|
|
{
|
|
unit->flags2.bits.slaughter = 1;
|
|
}
|
|
|
|
// check if creature is tame
|
|
bool isTame(df::unit* creature)
|
|
{
|
|
bool tame = false;
|
|
if(creature->flags1.bits.tame)
|
|
{
|
|
switch (creature->training_level)
|
|
{
|
|
case df::animal_training_level::SemiWild: //??
|
|
case df::animal_training_level::Trained:
|
|
case df::animal_training_level::WellTrained:
|
|
case df::animal_training_level::SkilfullyTrained:
|
|
case df::animal_training_level::ExpertlyTrained:
|
|
case df::animal_training_level::ExceptionallyTrained:
|
|
case df::animal_training_level::MasterfullyTrained:
|
|
case df::animal_training_level::Domesticated:
|
|
tame=true;
|
|
break;
|
|
case df::animal_training_level::Unk8: //??
|
|
case df::animal_training_level::WildUntamed:
|
|
default:
|
|
tame=false;
|
|
break;
|
|
}
|
|
}
|
|
return tame;
|
|
}
|
|
|
|
// check if creature is domesticated
|
|
// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert)
|
|
bool isDomesticated(df::unit* creature)
|
|
{
|
|
bool tame = false;
|
|
if(creature->flags1.bits.tame)
|
|
{
|
|
switch (creature->training_level)
|
|
{
|
|
case df::animal_training_level::Domesticated:
|
|
tame=true;
|
|
break;
|
|
default:
|
|
tame=false;
|
|
break;
|
|
}
|
|
}
|
|
return tame;
|
|
}
|
|
|
|
// check if trained (might be useful if pasturing war dogs etc)
|
|
bool isTrained(df::unit* unit)
|
|
{
|
|
// case a: trained for war/hunting (those don't have a training level, strangely)
|
|
if(isWar(unit) || isHunter(unit))
|
|
return true;
|
|
|
|
// case b: tamed and trained wild creature, gets a training level
|
|
bool trained = false;
|
|
switch (unit->training_level)
|
|
{
|
|
case df::animal_training_level::Trained:
|
|
case df::animal_training_level::WellTrained:
|
|
case df::animal_training_level::SkilfullyTrained:
|
|
case df::animal_training_level::ExpertlyTrained:
|
|
case df::animal_training_level::ExceptionallyTrained:
|
|
case df::animal_training_level::MasterfullyTrained:
|
|
//case df::animal_training_level::Domesticated:
|
|
trained = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return trained;
|
|
}
|
|
|
|
// check for profession "war creature"
|
|
bool isWar(df::unit* unit)
|
|
{
|
|
if( unit->profession == df::profession::TRAINED_WAR
|
|
|| unit->profession2 == df::profession::TRAINED_WAR)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// check for profession "hunting creature"
|
|
bool isHunter(df::unit* unit)
|
|
{
|
|
if( unit->profession == df::profession::TRAINED_HUNTER
|
|
|| unit->profession2 == df::profession::TRAINED_HUNTER)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
// check if unit is marked as available for adoption
|
|
bool isAvailableForAdoption(df::unit* unit)
|
|
{
|
|
auto refs = unit->specific_refs;
|
|
for(int i=0; i<refs.size(); i++)
|
|
{
|
|
auto ref = refs[i];
|
|
auto reftype = ref->type;
|
|
if( reftype == df::specific_ref_type::PETINFO_PET )
|
|
{
|
|
//df::pet_info* pet = ref->pet;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// check if creature belongs to the player's civilization
|
|
// (don't try to pasture/slaughter random untame animals)
|
|
bool isOwnCiv(df::unit* unit)
|
|
{
|
|
return unit->civ_id == ui->civ_id;
|
|
}
|
|
|
|
// check if creature belongs to the player's race
|
|
// (in combination with check for civ helps to filter out own dwarves)
|
|
bool isOwnRace(df::unit* unit)
|
|
{
|
|
return unit->race == ui->race_id;
|
|
}
|
|
|
|
// get race name by id or unit pointer
|
|
// todo: rename these two functions to "getRaceToken" since the output is more of a token
|
|
string getRaceName(int32_t id)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[id];
|
|
return raw->creature_id;
|
|
}
|
|
string getRaceName(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
return raw->creature_id;
|
|
}
|
|
|
|
// get plural of race name (used for display in autobutcher UI and for sorting the watchlist)
|
|
string getRaceNamePlural(int32_t id)
|
|
{
|
|
//WatchedRace * w = watched_races[idx];
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[id];
|
|
return raw->name[1]; // second field is plural of race name
|
|
}
|
|
|
|
string getRaceBabyName(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
return raw->general_baby_name[0];
|
|
}
|
|
string getRaceChildName(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
return raw->general_child_name[0];
|
|
}
|
|
|
|
bool isBaby(df::unit* unit)
|
|
{
|
|
return unit->profession == df::profession::BABY;
|
|
}
|
|
|
|
bool isChild(df::unit* unit)
|
|
{
|
|
return unit->profession == df::profession::CHILD;
|
|
}
|
|
|
|
bool isAdult(df::unit* unit)
|
|
{
|
|
return !isBaby(unit) && !isChild(unit);
|
|
}
|
|
|
|
bool isEggLayer(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if( caste->flags.is_set(caste_raw_flags::LAYS_EGGS)
|
|
|| caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isGrazer(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if(caste->flags.is_set(caste_raw_flags::GRAZER))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isMilkable(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if(caste->flags.is_set(caste_raw_flags::MILKABLE))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isTrainableWar(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if(caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isTrainableHunting(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if(caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isTamable(df::unit* unit)
|
|
{
|
|
df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race];
|
|
size_t sizecas = raw->caste.size();
|
|
for (size_t j = 0; j < sizecas;j++)
|
|
{
|
|
df::caste_raw *caste = raw->caste[j];
|
|
if(caste->flags.is_set(caste_raw_flags::PET) ||
|
|
caste->flags.is_set(caste_raw_flags::PET_EXOTIC))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isMale(df::unit* unit)
|
|
{
|
|
return unit->sex == 1;
|
|
}
|
|
|
|
bool isFemale(df::unit* unit)
|
|
{
|
|
return unit->sex == 0;
|
|
}
|
|
|
|
// found a unit with weird position values on one of my maps (negative and in the thousands)
|
|
// it didn't appear in the animal stocks screen, but looked completely fine otherwise (alive, tame, own, etc)
|
|
// maybe a rare bug, but better avoid assigning such units to zones or slaughter etc.
|
|
bool hasValidMapPos(df::unit* unit)
|
|
{
|
|
if( unit->pos.x >=0 && unit->pos.y >= 0 && unit->pos.z >= 0
|
|
&& unit->pos.x < world->map.x_count
|
|
&& unit->pos.y < world->map.y_count
|
|
&& unit->pos.z < world->map.z_count)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool isNaked(df::unit* unit)
|
|
{
|
|
return (unit->inventory.empty());
|
|
}
|
|
|
|
|
|
int getUnitIndexFromId(df::unit* unit_)
|
|
{
|
|
for (size_t i=0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit* unit = world->units.all[i];
|
|
if(unit->id == unit_->id)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// dump some unit info
|
|
void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false)
|
|
{
|
|
out.print("Unit %d ", unit->id); //race %d, civ %d,", creature->race, creature->civ_id
|
|
if(unit->name.has_name)
|
|
{
|
|
// units given a nick with the rename tool might not have a first name (animals etc)
|
|
string firstname = unit->name.first_name;
|
|
if(firstname.size() > 0)
|
|
{
|
|
firstname[0] = toupper(firstname[0]);
|
|
out << "Name: " << firstname;
|
|
}
|
|
if(unit->name.nickname.size() > 0)
|
|
out << " '" << unit->name.nickname << "'";
|
|
out << ", ";
|
|
}
|
|
|
|
if(isAdult(unit))
|
|
out << "adult";
|
|
else if(isBaby(unit))
|
|
out << "baby";
|
|
else if(isChild(unit))
|
|
out << "child";
|
|
out << " ";
|
|
// sometimes empty even in vanilla RAWS, sometimes contains full race name (e.g. baby alpaca)
|
|
// all animals I looked at don't have babies anyways, their offspring starts as CHILD
|
|
//out << getRaceBabyName(unit);
|
|
//out << getRaceChildName(unit);
|
|
|
|
out << getRaceName(unit) << " (";
|
|
switch(unit->sex)
|
|
{
|
|
case 0:
|
|
out << "female";
|
|
break;
|
|
case 1:
|
|
out << "male";
|
|
break;
|
|
case -1:
|
|
default:
|
|
out << "n/a";
|
|
break;
|
|
}
|
|
out << ")";
|
|
out << ", age: " << getUnitAge(unit);
|
|
|
|
if(isTame(unit))
|
|
out << ", tame";
|
|
if(isOwnCiv(unit))
|
|
out << ", owned";
|
|
if(isWar(unit))
|
|
out << ", war";
|
|
if(isHunter(unit))
|
|
out << ", hunter";
|
|
if(isMerchant(unit))
|
|
out << ", merchant";
|
|
if(isForest(unit))
|
|
out << ", forest";
|
|
if(isEggLayer(unit))
|
|
out << ", egglayer";
|
|
if(isGrazer(unit))
|
|
out << ", grazer";
|
|
if(isMilkable(unit))
|
|
out << ", milkable";
|
|
|
|
if(verbose)
|
|
{
|
|
out << ". Pos: ("<<unit->pos.x << "/"<< unit->pos.y << "/" << unit->pos.z << ") " << endl;
|
|
out << "index in units vector: " << getUnitIndexFromId(unit) << endl;
|
|
}
|
|
out << endl;
|
|
|
|
if(!verbose)
|
|
return;
|
|
|
|
//out << "number of refs: " << creature->general_refs.size() << endl;
|
|
for(size_t r = 0; r<unit->general_refs.size(); r++)
|
|
{
|
|
df::general_ref* ref = unit->general_refs.at(r);
|
|
df::general_ref_type refType = ref->getType();
|
|
out << " ref#" << r <<" refType#" << refType << " "; //endl;
|
|
switch(refType)
|
|
{
|
|
case df::general_ref_type::BUILDING_CIVZONE_ASSIGNED:
|
|
{
|
|
out << "assigned to zone";
|
|
df::building_civzonest * civAss = (df::building_civzonest *) ref->getBuilding();
|
|
out << " #" << civAss->id;
|
|
}
|
|
break;
|
|
case df::general_ref_type::CONTAINED_IN_ITEM:
|
|
out << "contained in item";
|
|
break;
|
|
case df::general_ref_type::BUILDING_CAGED:
|
|
out << "caged";
|
|
break;
|
|
case df::general_ref_type::BUILDING_CHAIN:
|
|
out << "chained";
|
|
break;
|
|
default:
|
|
//out << "unhandled reftype";
|
|
break;
|
|
}
|
|
out << endl;
|
|
}
|
|
if(isInBuiltCageRoom(unit))
|
|
{
|
|
out << "in a room." << endl;
|
|
}
|
|
}
|
|
|
|
bool isActivityZone(df::building * building)
|
|
{
|
|
if( building->getType() == building_type::Civzone
|
|
&& building->getSubtype() == (short)civzone_type::ActivityZone)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool isPenPasture(df::building * building)
|
|
{
|
|
if(!isActivityZone(building))
|
|
return false;
|
|
|
|
df::building_civzonest * civ = (df::building_civzonest *) building;
|
|
|
|
if(civ->zone_flags.bits.pen_pasture)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool isPitPond(df::building * building)
|
|
{
|
|
if(!isActivityZone(building))
|
|
return false;
|
|
|
|
df::building_civzonest * civ = (df::building_civzonest *) building;
|
|
|
|
if(civ->zone_flags.bits.pit_pond) // && civ->pit_flags==0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool isCage(df::building * building)
|
|
{
|
|
return building->getType() == building_type::Cage;
|
|
}
|
|
|
|
bool isChain(df::building * building)
|
|
{
|
|
return building->getType() == building_type::Chain;
|
|
}
|
|
|
|
bool isActive(df::building * building)
|
|
{
|
|
if(!isActivityZone(building))
|
|
return false;
|
|
|
|
df::building_civzonest * civ = (df::building_civzonest *) building;
|
|
if(civ->zone_flags.bits.active)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
int32_t findBuildingIndexById(int32_t id)
|
|
{
|
|
for (size_t b = 0; b < world->buildings.all.size(); b++)
|
|
{
|
|
if(world->buildings.all.at(b)->id == id)
|
|
return b;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int32_t findUnitIndexById(int32_t id)
|
|
{
|
|
for (size_t i = 0; i < world->units.all.size(); i++)
|
|
{
|
|
if(world->units.all.at(i)->id == id)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
df::unit* findUnitById(int32_t id)
|
|
{
|
|
int32_t index = findUnitIndexById(id);
|
|
if(index != -1)
|
|
return world->units.all[index];
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
// returns id of pen/pit at cursor position (-1 if nothing found)
|
|
int32_t findPenPitAtCursor()
|
|
{
|
|
int32_t foundID = -1;
|
|
|
|
if(cursor->x == -30000)
|
|
return -1;
|
|
|
|
for (size_t b = 0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
|
|
// find zone under cursor
|
|
if (!(building->x1 <= cursor->x && cursor->x <= building->x2 &&
|
|
building->y1 <= cursor->y && cursor->y <= building->y2 &&
|
|
building->z == cursor->z))
|
|
continue;
|
|
|
|
if(isPenPasture(building) || isPitPond(building))
|
|
{
|
|
foundID = building->id;
|
|
break;
|
|
}
|
|
}
|
|
return foundID;
|
|
}
|
|
|
|
// returns id of cage at cursor position (-1 if nothing found)
|
|
int32_t findCageAtCursor()
|
|
{
|
|
int32_t foundID = -1;
|
|
|
|
if(cursor->x == -30000)
|
|
return -1;
|
|
|
|
for (size_t b = 0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
|
|
if (!(building->x1 <= cursor->x && cursor->x <= building->x2 &&
|
|
building->y1 <= cursor->y && cursor->y <= building->y2 &&
|
|
building->z == cursor->z))
|
|
continue;
|
|
|
|
// don't set id if cage is not constructed yet
|
|
if(building->getBuildStage()!=building->getMaxBuildStage())
|
|
break;
|
|
|
|
if(isCage(building))
|
|
{
|
|
foundID = building->id;
|
|
break;
|
|
}
|
|
}
|
|
return foundID;
|
|
}
|
|
|
|
int32_t findChainAtCursor()
|
|
{
|
|
int32_t foundID = -1;
|
|
|
|
if(cursor->x == -30000)
|
|
return -1;
|
|
|
|
for (size_t b = 0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
|
|
// find zone under cursor
|
|
if (!(building->x1 <= cursor->x && cursor->x <= building->x2 &&
|
|
building->y1 <= cursor->y && cursor->y <= building->y2 &&
|
|
building->z == cursor->z))
|
|
continue;
|
|
|
|
if(isChain(building))
|
|
{
|
|
foundID = building->id;
|
|
break;
|
|
}
|
|
}
|
|
return foundID;
|
|
}
|
|
|
|
df::general_ref_building_civzone_assignedst * createCivzoneRef()
|
|
{
|
|
static bool vt_initialized = false;
|
|
df::general_ref_building_civzone_assignedst* newref = NULL;
|
|
|
|
// after having run successfully for the first time it's safe to simply create the object
|
|
if(vt_initialized)
|
|
{
|
|
newref = (df::general_ref_building_civzone_assignedst*)
|
|
df::general_ref_building_civzone_assignedst::_identity.instantiate();
|
|
return newref;
|
|
}
|
|
|
|
// being called for the first time, need to initialize the vtable
|
|
for(size_t i = 0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit * creature = world->units.all[i];
|
|
for(size_t r = 0; r<creature->general_refs.size(); r++)
|
|
{
|
|
df::general_ref* ref;
|
|
ref = creature->general_refs.at(r);
|
|
if(ref->getType() == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED)
|
|
{
|
|
if (strict_virtual_cast<df::general_ref_building_civzone_assignedst>(ref))
|
|
{
|
|
// !! calling new() doesn't work, need _identity.instantiate() instead !!
|
|
newref = (df::general_ref_building_civzone_assignedst*)
|
|
df::general_ref_building_civzone_assignedst::_identity.instantiate();
|
|
vt_initialized = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(vt_initialized)
|
|
break;
|
|
}
|
|
return newref;
|
|
}
|
|
|
|
bool isInBuiltCage(df::unit* unit);
|
|
|
|
// check if assigned to pen, pit, (built) cage or chain
|
|
// note: BUILDING_CAGED is not set for animals (maybe it's used for dwarves who get caged as sentence)
|
|
// animals in cages (no matter if built or on stockpile) get the ref CONTAINED_IN_ITEM instead
|
|
// removing them from cages on stockpiles is no problem even without clearing the ref
|
|
// and usually it will be desired behavior to do so.
|
|
bool isAssigned(df::unit* unit)
|
|
{
|
|
bool assigned = false;
|
|
for (size_t r=0; r < unit->general_refs.size(); r++)
|
|
{
|
|
df::general_ref * ref = unit->general_refs[r];
|
|
auto rtype = ref->getType();
|
|
if( rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED
|
|
|| rtype == df::general_ref_type::BUILDING_CAGED
|
|
|| rtype == df::general_ref_type::BUILDING_CHAIN
|
|
|| (rtype == df::general_ref_type::CONTAINED_IN_ITEM && isInBuiltCage(unit))
|
|
)
|
|
{
|
|
assigned = true;
|
|
break;
|
|
}
|
|
}
|
|
return assigned;
|
|
}
|
|
|
|
bool isAssignedToZone(df::unit* unit)
|
|
{
|
|
bool assigned = false;
|
|
for (size_t r=0; r < unit->general_refs.size(); r++)
|
|
{
|
|
df::general_ref * ref = unit->general_refs[r];
|
|
auto rtype = ref->getType();
|
|
if(rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED)
|
|
{
|
|
assigned = true;
|
|
break;
|
|
}
|
|
}
|
|
return assigned;
|
|
}
|
|
|
|
// check if assigned to a chain or built cage
|
|
// (need to check if the ref needs to be removed, until then touching them is forbidden)
|
|
bool isChained(df::unit* unit)
|
|
{
|
|
bool contained = false;
|
|
for (size_t r=0; r < unit->general_refs.size(); r++)
|
|
{
|
|
df::general_ref * ref = unit->general_refs[r];
|
|
auto rtype = ref->getType();
|
|
if(rtype == df::general_ref_type::BUILDING_CHAIN)
|
|
{
|
|
contained = true;
|
|
break;
|
|
}
|
|
}
|
|
return contained;
|
|
}
|
|
|
|
// check if contained in item (e.g. animals in cages)
|
|
bool isContainedInItem(df::unit* unit)
|
|
{
|
|
bool contained = false;
|
|
for (size_t r=0; r < unit->general_refs.size(); r++)
|
|
{
|
|
df::general_ref * ref = unit->general_refs[r];
|
|
auto rtype = ref->getType();
|
|
if(rtype == df::general_ref_type::CONTAINED_IN_ITEM)
|
|
{
|
|
contained = true;
|
|
break;
|
|
}
|
|
}
|
|
return contained;
|
|
}
|
|
|
|
bool isInBuiltCage(df::unit* unit)
|
|
{
|
|
bool caged = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( building->getType() == building_type::Cage)
|
|
{
|
|
df::building_cagest* cage = (df::building_cagest*) building;
|
|
for(size_t c=0; c<cage->assigned_units.size(); c++)
|
|
{
|
|
if(cage->assigned_units[c] == unit->id)
|
|
{
|
|
caged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(caged)
|
|
break;
|
|
}
|
|
return caged;
|
|
}
|
|
|
|
// built cage defined as room (supposed to detect zoo cages)
|
|
bool isInBuiltCageRoom(df::unit* unit)
|
|
{
|
|
bool caged_room = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
|
|
// !!! building->isRoom() returns true if the building can be made a room but currently isn't
|
|
// !!! except for coffins/tombs which always return false
|
|
// !!! using the bool is_room however gives the correct state/value
|
|
if(!building->is_room)
|
|
continue;
|
|
|
|
if(building->getType() == building_type::Cage)
|
|
{
|
|
df::building_cagest* cage = (df::building_cagest*) building;
|
|
for(size_t c=0; c<cage->assigned_units.size(); c++)
|
|
{
|
|
if(cage->assigned_units[c] == unit->id)
|
|
{
|
|
caged_room = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(caged_room)
|
|
break;
|
|
}
|
|
return caged_room;
|
|
}
|
|
|
|
// check a map position for a built cage
|
|
// animals in cages are CONTAINED_IN_ITEM, no matter if they are on a stockpile or inside a built cage
|
|
// if they are on animal stockpiles they should count as unassigned to allow pasturing them
|
|
// if they are inside built cages they should be ignored in case the cage is a zoo or linked to a lever or whatever
|
|
bool isBuiltCageAtPos(df::coord pos)
|
|
{
|
|
bool cage = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( building->getType() == building_type::Cage
|
|
&& building->x1 == pos.x
|
|
&& building->y1 == pos.y
|
|
&& building->z == pos.z )
|
|
{
|
|
cage = true;
|
|
break;
|
|
}
|
|
}
|
|
return cage;
|
|
}
|
|
|
|
df::building * getBuiltCageAtPos(df::coord pos)
|
|
{
|
|
df::building* cage = NULL;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( building->getType() == building_type::Cage
|
|
&& building->x1 == pos.x
|
|
&& building->y1 == pos.y
|
|
&& building->z == pos.z )
|
|
{
|
|
// don't set pointer if not constructed yet
|
|
if(building->getBuildStage()!=building->getMaxBuildStage())
|
|
break;
|
|
|
|
cage = building;
|
|
break;
|
|
}
|
|
}
|
|
return cage;
|
|
}
|
|
|
|
bool isNestboxAtPos(int32_t x, int32_t y, int32_t z)
|
|
{
|
|
bool found = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( building->getType() == building_type::NestBox
|
|
&& building->x1 == x
|
|
&& building->y1 == y
|
|
&& building->z == z )
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool isFreeNestboxAtPos(int32_t x, int32_t y, int32_t z)
|
|
{
|
|
bool found = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( building->getType() == building_type::NestBox
|
|
&& building->x1 == x
|
|
&& building->y1 == y
|
|
&& building->z == z )
|
|
{
|
|
df::building_nest_boxst* nestbox = (df::building_nest_boxst*) building;
|
|
if(nestbox->claimed_by == -1 && nestbox->contained_items.size() == 1)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool isEmptyPasture(df::building* building)
|
|
{
|
|
if(!isPenPasture(building))
|
|
return false;
|
|
df::building_civzonest * civ = (df::building_civzonest *) building;
|
|
if(civ->assigned_units.size() == 0)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
df::building* findFreeNestboxZone()
|
|
{
|
|
df::building * free_building = NULL;
|
|
bool cage = false;
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building* building = world->buildings.all[b];
|
|
if( isEmptyPasture(building) &&
|
|
isActive(building) &&
|
|
isFreeNestboxAtPos(building->x1, building->y1, building->z))
|
|
{
|
|
free_building = building;
|
|
break;
|
|
}
|
|
}
|
|
return free_building;
|
|
}
|
|
|
|
bool isFreeEgglayer(df::unit * unit)
|
|
{
|
|
if( !isDead(unit) && !isUndead(unit)
|
|
&& isFemale(unit)
|
|
&& isTame(unit)
|
|
&& isOwnCiv(unit)
|
|
&& isEggLayer(unit)
|
|
&& !isAssigned(unit)
|
|
&& !isGrazer(unit) // exclude grazing birds because they're messy
|
|
&& !isMerchant(unit) // don't steal merchant mounts
|
|
&& !isForest(unit) // don't steal birds from traders, they hate that
|
|
)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
df::unit * findFreeEgglayer()
|
|
{
|
|
df::unit* free_unit = NULL;
|
|
for (size_t i=0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit* unit = world->units.all[i];
|
|
if(isFreeEgglayer(unit))
|
|
{
|
|
free_unit = unit;
|
|
break;
|
|
}
|
|
}
|
|
return free_unit;
|
|
}
|
|
|
|
size_t countFreeEgglayers()
|
|
{
|
|
size_t count = 0;
|
|
for (size_t i=0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit* unit = world->units.all[i];
|
|
if(isFreeEgglayer(unit))
|
|
count ++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// check if unit is already assigned to a zone, remove that ref from unit and old zone
|
|
// check if unit is already assigned to a cage, remove that ref from the cage
|
|
// returns false if no cage or pasture information was found
|
|
// helps as workaround for http://www.bay12games.com/dwarves/mantisbt/view.php?id=4475 by the way
|
|
// (pastured animals assigned to chains will get hauled back and forth because the pasture ref is not deleted)
|
|
bool unassignUnitFromBuilding(df::unit* unit)
|
|
{
|
|
bool success = false;
|
|
for (std::size_t idx = 0; idx < unit->general_refs.size(); idx++)
|
|
{
|
|
df::general_ref * oldref = unit->general_refs[idx];
|
|
switch(oldref->getType())
|
|
{
|
|
case df::general_ref_type::BUILDING_CIVZONE_ASSIGNED:
|
|
{
|
|
unit->general_refs.erase(unit->general_refs.begin() + idx);
|
|
df::building_civzonest * oldciv = (df::building_civzonest *) oldref->getBuilding();
|
|
for(size_t oc=0; oc<oldciv->assigned_units.size(); oc++)
|
|
{
|
|
if(oldciv->assigned_units[oc] == unit->id)
|
|
{
|
|
oldciv->assigned_units.erase(oldciv->assigned_units.begin() + oc);
|
|
break;
|
|
}
|
|
}
|
|
delete oldref;
|
|
success = true;
|
|
break;
|
|
}
|
|
|
|
case df::general_ref_type::CONTAINED_IN_ITEM:
|
|
{
|
|
// game does not erase the ref until creature gets removed from cage
|
|
//unit->general_refs.erase(unit->general_refs.begin() + idx);
|
|
|
|
// walk through buildings, check cages for inhabitants, compare ids
|
|
for (size_t b=0; b < world->buildings.all.size(); b++)
|
|
{
|
|
bool found = false;
|
|
df::building* building = world->buildings.all[b];
|
|
if(isCage(building))
|
|
{
|
|
df::building_cagest* oldcage = (df::building_cagest*) building;
|
|
for(size_t oc=0; oc<oldcage->assigned_units.size(); oc++)
|
|
{
|
|
if(oldcage->assigned_units[oc] == unit->id)
|
|
{
|
|
oldcage->assigned_units.erase(oldcage->assigned_units.begin() + oc);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if(found)
|
|
break;
|
|
}
|
|
success = true;
|
|
break;
|
|
}
|
|
|
|
case df::general_ref_type::BUILDING_CHAIN:
|
|
{
|
|
// try not erasing the ref and see what happens
|
|
//unit->general_refs.erase(unit->general_refs.begin() + idx);
|
|
// probably need to delete chain reference here
|
|
//success = true;
|
|
break;
|
|
}
|
|
|
|
case df::general_ref_type::BUILDING_CAGED:
|
|
{
|
|
// not sure what to do here, doesn't seem to get used by the game
|
|
//unit->general_refs.erase(unit->general_refs.begin() + idx);
|
|
//success = true;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
// some reference which probably shouldn't get deleted
|
|
// (animals who are historical figures and have a NEMESIS reference or whatever)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// assign to pen or pit
|
|
command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose = false)
|
|
{
|
|
// building must be a pen/pasture or pit
|
|
if(!isPenPasture(building) && !isPitPond(building))
|
|
{
|
|
out << "Invalid building type. This is not a pen/pasture or pit/pond." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// try to get a fresh civzone ref
|
|
df::general_ref_building_civzone_assignedst * ref = createCivzoneRef();
|
|
if(!ref)
|
|
{
|
|
out << "Could not find a clonable activity zone reference" << endl
|
|
<< "You need to pen/pasture/pit at least one creature" << endl
|
|
<< "before using 'assign' for the first time." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// check if unit is already pastured, remove that ref from unit and old pasture
|
|
// testing showed that removing the ref from the unit only seems to be necessary for pastured creatures
|
|
// if they are in cages on stockpiles the game unassigns them automatically
|
|
// if they are in built cages the pointer to the creature needs to be removed from the cage
|
|
// TODO: check what needs to be done for chains
|
|
bool cleared_old = unassignUnitFromBuilding(unit);
|
|
|
|
if(verbose)
|
|
{
|
|
if(cleared_old)
|
|
out << "old zone info cleared.";
|
|
else
|
|
out << "no old zone info found.";
|
|
}
|
|
|
|
ref->building_id = building->id;
|
|
unit->general_refs.push_back(ref);
|
|
|
|
df::building_civzonest * civz = (df::building_civzonest *) building;
|
|
civz->assigned_units.push_back(unit->id);
|
|
|
|
out << "Unit " << unit->id
|
|
<< "(" << getRaceName(unit) << ")"
|
|
<< " assigned to zone " << building->id;
|
|
if(isPitPond(building))
|
|
out << " (pit/pond).";
|
|
if(isPenPasture(building))
|
|
out << " (pen/pasture).";
|
|
out << endl;
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result assignUnitToCage(color_ostream& out, df::unit* unit, df::building* building, bool verbose)
|
|
{
|
|
// building must be a pen/pasture or pit
|
|
if(!isCage(building))
|
|
{
|
|
out << "Invalid building type. This is not a cage." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// don't assign owned pets to a cage. the owner will release them, resulting into infinite hauling (df bug)
|
|
if(unit->relations.pet_owner_id != -1)
|
|
return CR_OK;
|
|
|
|
// check if unit is already pastured or caged, remove refs where necessary
|
|
bool cleared_old = unassignUnitFromBuilding(unit);
|
|
if(verbose)
|
|
{
|
|
if(cleared_old)
|
|
out << "old zone info cleared.";
|
|
else
|
|
out << "no old zone info found.";
|
|
}
|
|
|
|
//ref->building_id = building->id;
|
|
//unit->general_refs.push_back(ref);
|
|
|
|
df::building_cagest* civz = (df::building_cagest*) building;
|
|
civz->assigned_units.push_back(unit->id);
|
|
|
|
out << "Unit " << unit->id
|
|
<< "(" << getRaceName(unit) << ")"
|
|
<< " assigned to cage " << building->id;
|
|
out << endl;
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result assignUnitToChain(color_ostream& out, df::unit* unit, df::building* building, bool verbose)
|
|
{
|
|
out << "sorry. assigning to chains is not possible yet." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
command_result assignUnitToBuilding(color_ostream& out, df::unit* unit, df::building* building, bool verbose)
|
|
{
|
|
command_result result = CR_WRONG_USAGE;
|
|
|
|
if(isActivityZone(building))
|
|
result = assignUnitToZone(out, unit, building, verbose);
|
|
else if(isCage(building))
|
|
result = assignUnitToCage(out, unit, building, verbose);
|
|
else if(isChain(building))
|
|
result = assignUnitToChain(out, unit, building, verbose);
|
|
else
|
|
out << "Cannot assign units to this type of building!" << endl;
|
|
|
|
return result;
|
|
}
|
|
|
|
command_result assignUnitsToCagezone(color_ostream& out, vector<df::unit*> units, df::building* building, bool verbose)
|
|
{
|
|
command_result result = CR_WRONG_USAGE;
|
|
|
|
if(!isPenPasture(building))
|
|
{
|
|
out << "A cage zone needs to be a pen/pasture containing at least one cage!" << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
int32_t x1 = building->x1;
|
|
int32_t x2 = building->x2;
|
|
int32_t y1 = building->y1;
|
|
int32_t y2 = building->y2;
|
|
int32_t z = building->z;
|
|
vector <df::building_cagest*> cages;
|
|
for (int32_t x = x1; x<=x2; x++)
|
|
{
|
|
for (int32_t y = y1; y<=y2; y++)
|
|
{
|
|
df::building* cage = getBuiltCageAtPos(df::coord(x,y,z));
|
|
if(cage)
|
|
{
|
|
df::building_cagest* cagest = (df::building_cagest*) cage;
|
|
cages.push_back(cagest);
|
|
}
|
|
}
|
|
}
|
|
if(!cages.size())
|
|
{
|
|
out << "No cages found in this zone!" << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
out << "Number of cages: " << cages.size() << endl;
|
|
}
|
|
|
|
while(units.size())
|
|
{
|
|
// hrm, better use sort() instead?
|
|
df::building_cagest * bestcage = cages[0];
|
|
size_t lowest = cages[0]->assigned_units.size();
|
|
for(size_t i=1; i<cages.size(); i++)
|
|
{
|
|
if(cages[i]->assigned_units.size()<lowest)
|
|
{
|
|
lowest = cages[i]->assigned_units.size();
|
|
bestcage = cages[i];
|
|
}
|
|
}
|
|
df::unit* unit = units.back();
|
|
units.pop_back();
|
|
command_result result = assignUnitToCage(out, unit, (df::building*) bestcage, verbose);
|
|
if(result!=CR_OK)
|
|
return result;
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result nickUnitsInZone(color_ostream& out, df::building* building, string nick)
|
|
{
|
|
// building must be a pen/pasture or pit
|
|
if(!isPenPasture(building) && !isPitPond(building))
|
|
{
|
|
out << "Invalid building type. This is not a pen/pasture or pit/pond." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
df::building_civzonest * civz = (df::building_civzonest *) building;
|
|
for(size_t i = 0; i < civz->assigned_units.size(); i++)
|
|
{
|
|
df::unit* unit = findUnitById(civz->assigned_units[i]);
|
|
if(unit)
|
|
Units::setNickname(unit, nick);
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result nickUnitsInCage(color_ostream& out, df::building* building, string nick)
|
|
{
|
|
// building must be a pen/pasture or pit
|
|
if(!isCage(building))
|
|
{
|
|
out << "Invalid building type. This is not a cage." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
df::building_cagest* cage = (df::building_cagest*) building;
|
|
for(size_t i=0; i<cage->assigned_units.size(); i++)
|
|
{
|
|
df::unit* unit = findUnitById(cage->assigned_units[i]);
|
|
if(unit)
|
|
Units::setNickname(unit, nick);
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result nickUnitsInChain(color_ostream& out, df::building* building, string nick)
|
|
{
|
|
out << "sorry. nicknaming chained units is not possible yet." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// give all units inside a pasture or cage the same nickname
|
|
// (usage example: protect them from being autobutchered)
|
|
command_result nickUnitsInBuilding(color_ostream& out, df::building* building, string nick)
|
|
{
|
|
command_result result = CR_WRONG_USAGE;
|
|
|
|
if(isActivityZone(building))
|
|
result = nickUnitsInZone(out, building, nick);
|
|
else if(isCage(building))
|
|
result = nickUnitsInCage(out, building, nick);
|
|
else if(isChain(building))
|
|
result = nickUnitsInChain(out, building, nick);
|
|
else
|
|
out << "Cannot nickname units in this type of building!" << endl;
|
|
|
|
return result;
|
|
}
|
|
|
|
// dump some zone info
|
|
void zoneInfo(color_ostream & out, df::building* building, bool verbose)
|
|
{
|
|
if(building->getType()!= building_type::Civzone)
|
|
return;
|
|
|
|
if(building->getSubtype() != (short)civzone_type::ActivityZone)
|
|
return;
|
|
|
|
string name;
|
|
building->getName(&name);
|
|
out.print("Building %i - \"%s\" - type %s (%i)",
|
|
building->id,
|
|
name.c_str(),
|
|
ENUM_KEY_STR(building_type, building->getType()).c_str(),
|
|
building->getType());
|
|
out.print(", subtype %s (%i)",
|
|
ENUM_KEY_STR(civzone_type, (df::civzone_type)building->getSubtype()).c_str(),
|
|
building->getSubtype());
|
|
out.print("\n");
|
|
|
|
df::building_civzonest * civ = (df::building_civzonest *) building;
|
|
if(civ->zone_flags.bits.active)
|
|
out << "active";
|
|
else
|
|
out << "not active";
|
|
|
|
if(civ->zone_flags.bits.pen_pasture)
|
|
out << ", pen/pasture";
|
|
else if (civ->zone_flags.bits.pit_pond)
|
|
{
|
|
out << " (pit flags:" << civ->pit_flags.whole << ")";
|
|
if(civ->pit_flags.bits.is_pond)
|
|
out << ", pond";
|
|
else
|
|
out << ", pit";
|
|
}
|
|
out << endl;
|
|
out << "x1:" <<building->x1
|
|
<< " x2:" <<building->x2
|
|
<< " y1:" <<building->y1
|
|
<< " y2:" <<building->y2
|
|
<< " z:" <<building->z
|
|
<< endl;
|
|
|
|
int32_t creaturecount = civ->assigned_units.size();
|
|
out << "Creatures in this zone: " << creaturecount << endl;
|
|
for(size_t c = 0; c < creaturecount; c++)
|
|
{
|
|
int32_t cindex = civ->assigned_units.at(c);
|
|
|
|
// print list of all units assigned to that zone
|
|
for(size_t i = 0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit * creature = world->units.all[i];
|
|
if(creature->id != cindex)
|
|
continue;
|
|
|
|
unitInfo(out, creature, verbose);
|
|
}
|
|
}
|
|
}
|
|
|
|
// dump some cage info
|
|
void cageInfo(color_ostream & out, df::building* building, bool verbose)
|
|
{
|
|
if(!isCage(building))
|
|
return;
|
|
|
|
string name;
|
|
building->getName(&name);
|
|
out.print("Building %i - \"%s\" - type %s (%i)",
|
|
building->id,
|
|
name.c_str(),
|
|
ENUM_KEY_STR(building_type, building->getType()).c_str(),
|
|
building->getType());
|
|
out.print("\n");
|
|
|
|
out << "x:" << building->x1
|
|
<< " y:" << building->y1
|
|
<< " z:" << building->z
|
|
<< endl;
|
|
|
|
df::building_cagest * cage = (df::building_cagest*) building;
|
|
|
|
int32_t creaturecount = cage->assigned_units.size();
|
|
out << "Creatures in this cage: " << creaturecount << endl;
|
|
for(size_t c = 0; c < creaturecount; c++)
|
|
{
|
|
int32_t cindex = cage->assigned_units.at(c);
|
|
|
|
// print list of all units assigned to that cage
|
|
for(size_t i = 0; i < world->units.all.size(); i++)
|
|
{
|
|
df::unit * creature = world->units.all[i];
|
|
if(creature->id != cindex)
|
|
continue;
|
|
|
|
unitInfo(out, creature, verbose);
|
|
}
|
|
}
|
|
}
|
|
|
|
// dump some chain/restraint info
|
|
void chainInfo(color_ostream & out, df::building* building, bool list_refs = false)
|
|
{
|
|
if(!isChain(building))
|
|
return;
|
|
|
|
string name;
|
|
building->getName(&name);
|
|
out.print("Building %i - \"%s\" - type %s (%i)",
|
|
building->id,
|
|
name.c_str(),
|
|
ENUM_KEY_STR(building_type, building->getType()).c_str(),
|
|
building->getType());
|
|
out.print("\n");
|
|
|
|
df::building_chainst* chain = (df::building_chainst*) building;
|
|
if(chain->assigned)
|
|
{
|
|
out << "assigned: ";
|
|
unitInfo(out, chain->assigned, true);
|
|
}
|
|
if(chain->chained)
|
|
{
|
|
out << "chained: ";
|
|
unitInfo(out, chain->chained, true);
|
|
}
|
|
}
|
|
|
|
command_result df_zone (color_ostream &out, vector <string> & parameters)
|
|
{
|
|
CoreSuspender suspend;
|
|
|
|
bool need_cursor = false; // for zone_info, zone_assign, ...
|
|
bool unit_info = false;
|
|
bool zone_info = false;
|
|
//bool cage_info = false;
|
|
//bool chain_info = false;
|
|
|
|
bool invert_filter = false;
|
|
bool find_unassigned = false;
|
|
bool find_caged = false;
|
|
bool find_not_caged = false;
|
|
bool find_trainable_war = false;
|
|
bool find_not_trainable_war = false;
|
|
bool find_trainable_hunting = false;
|
|
bool find_not_trainable_hunting = false;
|
|
bool find_trained = false;
|
|
bool find_not_trained = false;
|
|
bool find_war = false;
|
|
bool find_not_war = false;
|
|
bool find_hunter = false;
|
|
bool find_not_hunter = false;
|
|
bool find_own = false;
|
|
bool find_not_own = false;
|
|
bool find_tame = false;
|
|
bool find_not_tame = false;
|
|
bool find_merchant = false;
|
|
bool find_not_merchant = false;
|
|
bool find_male = false;
|
|
bool find_not_male = false;
|
|
bool find_female = false;
|
|
bool find_not_female = false;
|
|
bool find_egglayer = false;
|
|
bool find_not_egglayer = false;
|
|
bool find_grazer = false;
|
|
bool find_not_grazer = false;
|
|
bool find_milkable = false;
|
|
bool find_not_milkable = false;
|
|
bool find_named = false;
|
|
bool find_not_named = false;
|
|
bool find_naked = false;
|
|
bool find_not_naked = false;
|
|
bool find_tamable = false;
|
|
bool find_not_tamable = false;
|
|
|
|
bool find_agemin = false;
|
|
bool find_agemax = false;
|
|
int target_agemin = 0;
|
|
int target_agemax = 0;
|
|
|
|
bool find_count = false;
|
|
size_t target_count = 0;
|
|
|
|
bool find_race = false;
|
|
bool find_not_race = false;
|
|
string target_race = "";
|
|
|
|
bool building_assign = false;
|
|
bool building_unassign = false;
|
|
bool building_set = false;
|
|
bool cagezone_assign = false;
|
|
bool verbose = false;
|
|
bool all = false;
|
|
bool unit_slaughter = false;
|
|
static int target_building = -1;
|
|
bool nick_set = false;
|
|
string target_nick = "";
|
|
|
|
for (size_t i = 0; i < parameters.size(); i++)
|
|
{
|
|
string & p = parameters[i];
|
|
|
|
if (p == "help" || p == "?")
|
|
{
|
|
out << zone_help << endl;
|
|
return CR_OK;
|
|
}
|
|
if (p == "filters")
|
|
{
|
|
out << zone_help_filters << endl;
|
|
return CR_OK;
|
|
}
|
|
if (p == "examples")
|
|
{
|
|
out << zone_help_examples << endl;
|
|
return CR_OK;
|
|
}
|
|
else if(p == "zinfo")
|
|
{
|
|
zone_info = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "uinfo")
|
|
{
|
|
unit_info = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "verbose")
|
|
{
|
|
verbose = true;
|
|
if(invert_filter)
|
|
{
|
|
verbose = false;
|
|
invert_filter=false;
|
|
}
|
|
}
|
|
else if(p == "unassign")
|
|
{
|
|
if(invert_filter)
|
|
{
|
|
out << "'not unassign' makes no sense." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
building_unassign = true;
|
|
}
|
|
else if(p == "assign")
|
|
{
|
|
if(invert_filter)
|
|
{
|
|
out << "'not assign' makes no sense. (did you want to use unassign?)" << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// if followed by another parameter, check if it's numeric
|
|
if(i < parameters.size()-1)
|
|
{
|
|
auto & str = parameters[i+1];
|
|
if(str.size() > 0 && str[0] >= '0' && str[0] <= '9')
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
int new_building = -1;
|
|
ss >> new_building;
|
|
if(new_building != -1)
|
|
{
|
|
i++;
|
|
target_building = new_building;
|
|
out << "Assign selected unit(s) to building #" << target_building <<std::endl;
|
|
}
|
|
}
|
|
}
|
|
if(target_building == -1)
|
|
{
|
|
out.printerr("No building id specified and current one is invalid!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
out << "No buiding id specified. Will try to use #" << target_building << endl;
|
|
building_assign = true;
|
|
}
|
|
}
|
|
else if(p == "tocages")
|
|
{
|
|
if(invert_filter)
|
|
{
|
|
out << "'not tocages' makes no sense." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
// if followed by another parameter, check if it's numeric
|
|
if(i < parameters.size()-1)
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
int new_building = -1;
|
|
ss >> new_building;
|
|
if(new_building != -1)
|
|
{
|
|
i++;
|
|
target_building = new_building;
|
|
out << "Assign selected unit(s) to cagezone #" << target_building <<std::endl;
|
|
}
|
|
}
|
|
if(target_building == -1)
|
|
{
|
|
out.printerr("No building id specified and current one is invalid!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
out << "No buiding id specified. Will try to use #" << target_building << endl;
|
|
cagezone_assign = true;
|
|
}
|
|
}
|
|
else if(p == "race" && !invert_filter)
|
|
{
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No race id specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
target_race = parameters[i+1];
|
|
i++;
|
|
out << "Filter by race " << target_race << endl;
|
|
find_race = true;
|
|
}
|
|
}
|
|
else if(p == "race" && invert_filter)
|
|
{
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No race id specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
target_race = parameters[i+1];
|
|
i++;
|
|
out << "Excluding race: " << target_race << endl;
|
|
find_not_race = true;
|
|
}
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "not")
|
|
{
|
|
invert_filter = true;
|
|
}
|
|
else if(p == "unassigned")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
out << "Filter by 'unassigned'." << endl;
|
|
find_unassigned = true;
|
|
}
|
|
else if(p == "caged" && !invert_filter)
|
|
{
|
|
out << "Filter by 'caged' (ignores built cages)." << endl;
|
|
find_caged = true;
|
|
}
|
|
else if(p == "caged" && invert_filter)
|
|
{
|
|
out << "Filter by 'not caged'." << endl;
|
|
find_not_caged = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "trained" && !invert_filter)
|
|
{
|
|
out << "Filter by 'trained'." << endl;
|
|
find_trained = true;
|
|
}
|
|
else if(p == "trained" && invert_filter)
|
|
{
|
|
out << "Filter by 'untrained'." << endl;
|
|
find_not_trained = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "trainablewar" && !invert_filter)
|
|
{
|
|
out << "Filter by 'trainable for war'." << endl;
|
|
find_trainable_war = true;
|
|
}
|
|
else if(p == "trainablewar" && invert_filter)
|
|
{
|
|
out << "Filter by 'not trainable for war'." << endl;
|
|
find_not_trainable_war = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "trainablehunt"&& !invert_filter)
|
|
{
|
|
out << "Filter by 'trainable for hunting'." << endl;
|
|
find_trainable_hunting = true;
|
|
}
|
|
else if(p == "trainablehunt"&& invert_filter)
|
|
{
|
|
out << "Filter by 'not trainable for hunting'." << endl;
|
|
find_not_trainable_hunting = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "war" && !invert_filter)
|
|
{
|
|
out << "Filter by 'trained war creature'." << endl;
|
|
find_war = true;
|
|
}
|
|
else if(p == "war" && invert_filter)
|
|
{
|
|
out << "Filter by 'not a trained war creature'." << endl;
|
|
find_not_war = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "hunting" && !invert_filter)
|
|
{
|
|
out << "Filter by 'trained hunting creature'." << endl;
|
|
find_hunter = true;
|
|
}
|
|
else if(p == "hunting" && invert_filter)
|
|
{
|
|
out << "Filter by 'not a trained hunting creature'." << endl;
|
|
find_not_hunter = true;
|
|
invert_filter = false;
|
|
}else if(p == "own"&& !invert_filter)
|
|
{
|
|
out << "Filter by 'own civilization'." << endl;
|
|
find_own = true;
|
|
}
|
|
else if(p == "own" && invert_filter)
|
|
{
|
|
out << "Filter by 'not own' (i.e. not from the fortress civ, can be a dwarf)." << endl;
|
|
find_not_own = true;
|
|
invert_filter = false;
|
|
}
|
|
else if(p == "tame" && !invert_filter)
|
|
{
|
|
out << "Filter by 'tame'." << endl;
|
|
find_tame = true;
|
|
}
|
|
else if(p == "tame" && invert_filter)
|
|
{
|
|
out << "Filter by 'not tame'." << endl;
|
|
find_not_tame = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "named" && !invert_filter)
|
|
{
|
|
out << "Filter by 'has name or nickname'." << endl;
|
|
find_named = true;
|
|
}
|
|
else if(p == "named" && invert_filter)
|
|
{
|
|
out << "Filter by 'has no name or nickname'." << endl;
|
|
find_not_named = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "slaughter")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
out << "Assign animals for slaughter." << endl;
|
|
unit_slaughter = true;
|
|
}
|
|
else if(p == "count")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No count specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
i++;
|
|
ss >> target_count;
|
|
if(target_count <= 0)
|
|
{
|
|
out.printerr("Invalid count specified (must be > 0)!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
find_count = true;
|
|
out << "Process up to " << target_count << " units." << endl;
|
|
}
|
|
}
|
|
else if(p == "age")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No age specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
i++;
|
|
ss >> target_agemin;
|
|
ss >> target_agemax;
|
|
find_agemin = true;
|
|
find_agemax = true;
|
|
out << "Filter by exact age of " << target_agemin << endl;
|
|
}
|
|
}
|
|
else if(p == "minage")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No age specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
i++;
|
|
ss >> target_agemin;
|
|
find_agemin = true;
|
|
out << "Filter by minimum age of " << target_agemin << endl;
|
|
}
|
|
}
|
|
else if(p == "maxage")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No age specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
stringstream ss(parameters[i+1]);
|
|
i++;
|
|
ss >> target_agemax;
|
|
find_agemax = true;
|
|
out << "Filter by maximum age of " << target_agemax << endl;
|
|
}
|
|
}
|
|
else if(p == "male" && !invert_filter)
|
|
{
|
|
find_male = true;
|
|
}
|
|
else if(p == "female" && !invert_filter)
|
|
{
|
|
find_female = true;
|
|
}
|
|
else if(p == "egglayer" && !invert_filter)
|
|
{
|
|
find_egglayer = true;
|
|
}
|
|
else if(p == "egglayer" && invert_filter)
|
|
{
|
|
find_not_egglayer = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "naked" && !invert_filter)
|
|
{
|
|
find_naked = true;
|
|
}
|
|
else if(p == "naked" && invert_filter)
|
|
{
|
|
find_not_naked = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "tamable" && !invert_filter)
|
|
{
|
|
find_tamable = true;
|
|
}
|
|
else if(p == "tamable" && invert_filter)
|
|
{
|
|
find_not_tamable = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "grazer" && !invert_filter)
|
|
{
|
|
find_grazer = true;
|
|
}
|
|
else if(p == "grazer" && invert_filter)
|
|
{
|
|
find_not_grazer = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "merchant" && !invert_filter)
|
|
{
|
|
find_merchant = true;
|
|
}
|
|
else if(p == "merchant" && invert_filter)
|
|
{
|
|
// actually 'not merchant' is pointless since merchant units are ignored by default
|
|
find_not_merchant = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "milkable" && !invert_filter)
|
|
{
|
|
find_milkable = true;
|
|
}
|
|
else if(p == "milkable" && invert_filter)
|
|
{
|
|
find_not_milkable = true;
|
|
invert_filter=false;
|
|
}
|
|
else if(p == "set")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
building_set = true;
|
|
}
|
|
else if(p == "nick")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No nickname specified! Use 'remnick' to remove nicknames!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
nick_set = true;
|
|
target_nick = parameters[i+1];
|
|
i++;
|
|
out << "Set nickname to: " << target_nick << endl;
|
|
}
|
|
else if(p == "remnick")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
|
|
nick_set = true;
|
|
target_nick = "";
|
|
i++;
|
|
out << "Remove nickname." << endl;
|
|
}
|
|
else if(p == "all")
|
|
{
|
|
if(invert_filter)
|
|
return CR_WRONG_USAGE;
|
|
out << "Filter: all" << endl;
|
|
all = true;
|
|
}
|
|
else
|
|
{
|
|
out << "Unknown command: " << p << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
}
|
|
|
|
if (!Maps::IsValid())
|
|
{
|
|
out.printerr("Map is not available!\n");
|
|
return CR_FAILURE;
|
|
}
|
|
|
|
if((zone_info && !all) || building_set)
|
|
need_cursor = true;
|
|
|
|
if(need_cursor && cursor->x == -30000)
|
|
{
|
|
out.printerr("No cursor; place cursor over activity zone.\n");
|
|
return CR_FAILURE;
|
|
}
|
|
|
|
// search for male and not male is exclusive, so drop the flags if both are specified
|
|
if(find_male && find_not_male)
|
|
{
|
|
find_male=false;
|
|
find_not_male=false;
|
|
}
|
|
|
|
// search for female and not female is exclusive, so drop the flags if both are specified
|
|
if(find_female && find_not_female)
|
|
{
|
|
find_female=false;
|
|
find_not_female=false;
|
|
}
|
|
|
|
// search for male and female is exclusive, so drop the flags if both are specified
|
|
if(find_male && find_female)
|
|
{
|
|
find_male=false;
|
|
find_female=false;
|
|
}
|
|
|
|
// search for trained and untrained is exclusive, so drop the flags if both are specified
|
|
if(find_trained && find_not_trained)
|
|
{
|
|
find_trained=false;
|
|
find_not_trained=false;
|
|
}
|
|
|
|
// search for grazer and nograzer is exclusive, so drop the flags if both are specified
|
|
if(find_grazer && find_not_grazer)
|
|
{
|
|
find_grazer=false;
|
|
find_not_grazer=false;
|
|
}
|
|
|
|
// todo: maybe add this type of sanity check for all remaining bools, maybe not (lots of code just to avoid parsing dumb input)
|
|
|
|
// try to cope with user dumbness
|
|
if(target_agemin > target_agemax)
|
|
{
|
|
out << "Invalid values for minage/maxage specified! I'll swap them." << endl;
|
|
int oldmin = target_agemin;
|
|
target_agemin = target_agemax;
|
|
target_agemax = oldmin;
|
|
}
|
|
|
|
// give info on zone(s), chain or cage under cursor
|
|
// (doesn't use the findXyzAtCursor() methods because zones might overlap and contain a cage or chain)
|
|
if(zone_info) // || chain_info || cage_info)
|
|
{
|
|
for (size_t b = 0; b < world->buildings.all.size(); b++)
|
|
{
|
|
df::building * building = world->buildings.all[b];
|
|
|
|
// find building under cursor
|
|
if (!all &&
|
|
!(building->x1 <= cursor->x && cursor->x <= building->x2 &&
|
|
building->y1 <= cursor->y && cursor->y <= building->y2 &&
|
|
building->z == cursor->z))
|
|
continue;
|
|
|
|
zoneInfo(out, building, verbose);
|
|
chainInfo(out, building, verbose);
|
|
cageInfo(out, building, verbose);
|
|
}
|
|
return CR_OK;
|
|
}
|
|
|
|
// set building at cursor position to be new target building
|
|
if(building_set)
|
|
{
|
|
// cagezone wants a pen/pit as starting point
|
|
if(!cagezone_assign)
|
|
target_building = findCageAtCursor();
|
|
if(target_building != -1)
|
|
{
|
|
out << "Target building type: cage." << endl;
|
|
}
|
|
else
|
|
{
|
|
target_building = findPenPitAtCursor();
|
|
if(target_building == -1)
|
|
{
|
|
out << "No pen/pasture or pit under cursor!" << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
out << "Target building type: pen/pasture or pit." << endl;
|
|
}
|
|
}
|
|
out << "Current building set to #" << target_building << endl;
|
|
return CR_OK;
|
|
}
|
|
|
|
if(building_assign || cagezone_assign || unit_info || unit_slaughter || nick_set)
|
|
{
|
|
df::building * building;
|
|
if(building_assign || cagezone_assign || (nick_set && !all && !find_count))
|
|
{
|
|
// try to get building index from the id
|
|
int32_t index = findBuildingIndexById(target_building);
|
|
if(index == -1)
|
|
{
|
|
out << "Invalid building id." << endl;
|
|
target_building = -1;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
building = world->buildings.all.at(index);
|
|
|
|
if(nick_set && !building_assign)
|
|
{
|
|
out << "Renaming all units in target building." << endl;
|
|
return nickUnitsInBuilding(out, building, target_nick);
|
|
}
|
|
}
|
|
|
|
if(all || find_count)
|
|
{
|
|
vector <df::unit*> units_for_cagezone;
|
|
size_t count = 0;
|
|
for(size_t c = 0; c < world->units.all.size(); c++)
|
|
{
|
|
df::unit *unit = world->units.all[c];
|
|
|
|
// ignore dead and undead units
|
|
if (isDead(unit) || isUndead(unit))
|
|
continue;
|
|
|
|
// ignore merchant units by default
|
|
if (!find_merchant && (isMerchant(unit) || isForest(unit)))
|
|
continue;
|
|
// but allow pitting them and stealing from them if specified :)
|
|
if (find_merchant && !isMerchant(unit) && !isForest(unit))
|
|
continue;
|
|
|
|
if(find_race && getRaceName(unit) != target_race)
|
|
continue;
|
|
if(find_not_race && getRaceName(unit) == target_race)
|
|
continue;
|
|
// ignore own dwarves by default
|
|
if(isOwnCiv(unit) && isOwnRace(unit))
|
|
{
|
|
if(!find_race)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if( (find_unassigned && isAssigned(unit))
|
|
// avoid tampering with creatures who are currently being hauled to a built cage
|
|
|| (isContainedInItem(unit) && (find_not_caged || isInBuiltCage(unit)))
|
|
|| (isChained(unit))
|
|
|| (find_caged && !isContainedInItem(unit))
|
|
|| (find_not_caged && isContainedInItem(unit))
|
|
|| (find_own && !isOwnCiv(unit))
|
|
|| (find_not_own && isOwnCiv(unit))
|
|
|| (find_tame && !isTame(unit))
|
|
|| (find_not_tame && isTame(unit))
|
|
|| (find_trained && !isTrained(unit))
|
|
|| (find_not_trained && isTrained(unit))
|
|
|| (find_war && !isWar(unit))
|
|
|| (find_not_war && isWar(unit))
|
|
|| (find_hunter && !isHunter(unit))
|
|
|| (find_not_hunter && isHunter(unit))
|
|
|| (find_agemin && getUnitAge(unit)<target_agemin)
|
|
|| (find_agemax && getUnitAge(unit)>target_agemax)
|
|
|| (find_grazer && !isGrazer(unit))
|
|
|| (find_not_grazer && isGrazer(unit))
|
|
|| (find_egglayer && !isEggLayer(unit))
|
|
|| (find_not_egglayer && isEggLayer(unit))
|
|
|| (find_milkable && !isMilkable(unit))
|
|
|| (find_not_milkable && isMilkable(unit))
|
|
|| (find_male && !isMale(unit))
|
|
|| (find_not_male && isMale(unit))
|
|
|| (find_female && !isFemale(unit))
|
|
|| (find_not_female && isFemale(unit))
|
|
|| (find_named && !unit->name.has_name)
|
|
|| (find_not_named && unit->name.has_name)
|
|
|| (find_naked && !isNaked(unit))
|
|
|| (find_not_naked && isNaked(unit))
|
|
|| (find_tamable && !isTamable(unit))
|
|
|| (find_not_tamable && isTamable(unit))
|
|
|| (find_trainable_war && (isWar(unit) || isHunter(unit) || !isTrainableWar(unit)))
|
|
|| (find_not_trainable_war && isTrainableWar(unit)) // hm, is this check enough?
|
|
|| (find_trainable_hunting && (isWar(unit) || isHunter(unit) || !isTrainableHunting(unit)))
|
|
|| (find_not_trainable_hunting && isTrainableHunting(unit)) // hm, is this check enough?
|
|
)
|
|
continue;
|
|
|
|
// animals bought in cages have an invalid map pos until they are freed for the first time
|
|
// but if they are not in a cage and have an invalid pos it's better not to touch them
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
{
|
|
if(verbose)
|
|
{
|
|
out << "----------"<<endl;
|
|
out << "invalid unit pos but not in cage either. will skip this unit." << endl;
|
|
unitInfo(out, unit, verbose);
|
|
out << "----------"<<endl;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if(unit_info)
|
|
{
|
|
unitInfo(out, unit, verbose);
|
|
continue;
|
|
}
|
|
|
|
if(nick_set)
|
|
{
|
|
Units::setNickname(unit, target_nick);
|
|
}
|
|
|
|
if(cagezone_assign)
|
|
{
|
|
units_for_cagezone.push_back(unit);
|
|
}
|
|
else if(building_assign)
|
|
{
|
|
command_result result = assignUnitToBuilding(out, unit, building, verbose);
|
|
if(result != CR_OK)
|
|
return result;
|
|
}
|
|
|
|
if(unit_slaughter)
|
|
{
|
|
// don't slaughter named creatures unless told to do so
|
|
if(!unit->name.has_name || find_named)
|
|
doMarkForSlaughter(unit);
|
|
}
|
|
|
|
count++;
|
|
if(find_count && count >= target_count)
|
|
break;
|
|
}
|
|
if(cagezone_assign)
|
|
{
|
|
command_result result = assignUnitsToCagezone(out, units_for_cagezone, building, verbose);
|
|
if(result != CR_OK)
|
|
return result;
|
|
}
|
|
|
|
out << "Processed creatures: " << count << endl;
|
|
}
|
|
else
|
|
{
|
|
// must have unit selected
|
|
df::unit *unit = getSelectedUnit(out, true);
|
|
if (!unit)
|
|
return CR_WRONG_USAGE;
|
|
|
|
if(unit_info)
|
|
{
|
|
unitInfo(out, unit, verbose);
|
|
return CR_OK;
|
|
}
|
|
else if(building_assign)
|
|
{
|
|
return assignUnitToBuilding(out, unit, building, verbose);
|
|
}
|
|
else if(unit_slaughter)
|
|
{
|
|
// by default behave like the game? only allow slaughtering of named war/hunting pets
|
|
//if(unit->name.has_name && !find_named && !(isWar(unit)||isHunter(unit)))
|
|
//{
|
|
// out << "Slaughter of named unit denied. Use the filter 'named' to override this check." << endl;
|
|
// return CR_OK;
|
|
//}
|
|
|
|
doMarkForSlaughter(unit);
|
|
return CR_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
// de-assign from pen or pit
|
|
// using the zone tool to free creatures from cages or chains
|
|
// is pointless imo since that is already quite easy using the ingame UI.
|
|
// but it's easy to implement so I might as well add it later
|
|
if(building_unassign)
|
|
{
|
|
// must have unit selected
|
|
df::unit *unit = getSelectedUnit(out, true);
|
|
if (!unit)
|
|
return CR_WRONG_USAGE;
|
|
|
|
// remove assignment reference from unit and old zone
|
|
if(unassignUnitFromBuilding(unit))
|
|
out << "Unit unassigned." << endl;
|
|
else
|
|
out << "Unit is not assigned to an activity zone!" << endl;
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
////////////////////
|
|
// autonestbox stuff
|
|
|
|
command_result df_autonestbox(color_ostream &out, vector <string> & parameters)
|
|
{
|
|
CoreSuspender suspend;
|
|
|
|
bool verbose = false;
|
|
|
|
for (size_t i = 0; i < parameters.size(); i++)
|
|
{
|
|
string & p = parameters[i];
|
|
|
|
if (p == "help" || p == "?")
|
|
{
|
|
out << autonestbox_help << endl;
|
|
return CR_OK;
|
|
}
|
|
if (p == "start")
|
|
{
|
|
autonestbox_did_complain = false;
|
|
start_autonestbox(out);
|
|
return autoNestbox(out, verbose);
|
|
}
|
|
if (p == "stop")
|
|
{
|
|
enable_autonestbox = false;
|
|
if(config_autonestbox.isValid())
|
|
config_autonestbox.ival(0) = 0;
|
|
out << "Autonestbox stopped." << endl;
|
|
return CR_OK;
|
|
}
|
|
else if(p == "verbose")
|
|
{
|
|
verbose = true;
|
|
}
|
|
else if(p == "sleep")
|
|
{
|
|
if(i == parameters.size()-1)
|
|
{
|
|
out.printerr("No duration specified!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
size_t ticks = 0;
|
|
stringstream ss(parameters[i+1]);
|
|
i++;
|
|
ss >> ticks;
|
|
if(ticks <= 0)
|
|
{
|
|
out.printerr("Invalid duration specified (must be > 0)!");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
sleep_autonestbox = ticks;
|
|
if(config_autonestbox.isValid())
|
|
config_autonestbox.ival(1) = sleep_autonestbox;
|
|
out << "New sleep timer for autonestbox: " << ticks << " ticks." << endl;
|
|
return CR_OK;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
out << "Unknown command: " << p << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
}
|
|
return autoNestbox(out, verbose);
|
|
}
|
|
|
|
command_result autoNestbox( color_ostream &out, bool verbose = false )
|
|
{
|
|
bool stop = false;
|
|
size_t processed = 0;
|
|
|
|
if (!Maps::IsValid())
|
|
{
|
|
out.printerr("Map is not available!\n");
|
|
enable_autonestbox = false;
|
|
return CR_FAILURE;
|
|
}
|
|
|
|
do
|
|
{
|
|
df::building * free_building = findFreeNestboxZone();
|
|
df::unit * free_unit = findFreeEgglayer();
|
|
if(free_building && free_unit)
|
|
{
|
|
command_result result = assignUnitToBuilding(out, free_unit, free_building, verbose);
|
|
if(result != CR_OK)
|
|
return result;
|
|
processed ++;
|
|
}
|
|
else
|
|
{
|
|
stop = true;
|
|
if(free_unit && !free_building)
|
|
{
|
|
static size_t old_count = 0;
|
|
size_t freeEgglayers = countFreeEgglayers();
|
|
// avoid spamming the same message
|
|
if(old_count != freeEgglayers)
|
|
autonestbox_did_complain = false;
|
|
old_count = freeEgglayers;
|
|
if(!autonestbox_did_complain)
|
|
{
|
|
stringstream ss;
|
|
ss << freeEgglayers;
|
|
string announce = "Not enough free nestbox zones found! You need " + ss.str() + " more.";
|
|
Gui::showAnnouncement(announce, 6, true);
|
|
out << announce << endl;
|
|
autonestbox_did_complain = true;
|
|
}
|
|
}
|
|
}
|
|
} while (!stop);
|
|
if(processed > 0)
|
|
{
|
|
stringstream ss;
|
|
ss << processed;
|
|
string announce;
|
|
announce = ss.str() + " nestboxes were assigned.";
|
|
Gui::showAnnouncement(announce, 2, false);
|
|
out << announce << endl;
|
|
// can complain again
|
|
// (might lead to spamming the same message twice, but catches the case
|
|
// where for example 2 new egglayers hatched right after 2 zones were created and assigned)
|
|
autonestbox_did_complain = false;
|
|
}
|
|
return CR_OK;
|
|
}
|
|
|
|
////////////////////
|
|
// autobutcher stuff
|
|
|
|
// getUnitAge() returns 0 if born in current year, therefore the look at birth_time in that case
|
|
// (assuming that the value from there indicates in which tick of the current year the unit was born)
|
|
bool compareUnitAgesYounger(df::unit* i, df::unit* j)
|
|
{
|
|
int32_t age_i = getUnitAge(i);
|
|
int32_t age_j = getUnitAge(j);
|
|
if(age_i == 0 && age_j == 0)
|
|
{
|
|
age_i = i->relations.birth_time;
|
|
age_j = j->relations.birth_time;
|
|
}
|
|
return (age_i < age_j);
|
|
}
|
|
bool compareUnitAgesOlder(df::unit* i, df::unit* j)
|
|
{
|
|
int32_t age_i = getUnitAge(i);
|
|
int32_t age_j = getUnitAge(j);
|
|
if(age_i == 0 && age_j == 0)
|
|
{
|
|
age_i = i->relations.birth_time;
|
|
age_j = j->relations.birth_time;
|
|
}
|
|
return (age_i > age_j);
|
|
}
|
|
|
|
|
|
|
|
//enum WatchedRaceSubtypes
|
|
//{
|
|
// femaleKid=0,
|
|
// maleKid,
|
|
// femaleAdult,
|
|
// maleAdult
|
|
//};
|
|
|
|
struct WatchedRace
|
|
{
|
|
public:
|
|
PersistentDataItem rconfig;
|
|
|
|
bool isWatched; // if true, autobutcher will process this race
|
|
int raceId;
|
|
|
|
// target amounts
|
|
int fk; // max female kids
|
|
int mk; // max male kids
|
|
int fa; // max female adults
|
|
int ma; // max male adults
|
|
|
|
// amounts of protected (not butcherable) units
|
|
int fk_prot;
|
|
int fa_prot;
|
|
int mk_prot;
|
|
int ma_prot;
|
|
|
|
// bah, this should better be an array of 4 vectors
|
|
// that way there's no need for the 4 ugly process methods
|
|
vector <df::unit*> fk_ptr;
|
|
vector <df::unit*> mk_ptr;
|
|
vector <df::unit*> fa_ptr;
|
|
vector <df::unit*> ma_ptr;
|
|
|
|
WatchedRace(bool watch, int id, int _fk, int _mk, int _fa, int _ma)
|
|
{
|
|
isWatched = watch;
|
|
raceId = id;
|
|
fk = _fk;
|
|
mk = _mk;
|
|
fa = _fa;
|
|
ma = _ma;
|
|
fk_prot = fa_prot = mk_prot = ma_prot = 0;
|
|
}
|
|
|
|
~WatchedRace()
|
|
{
|
|
ClearUnits();
|
|
}
|
|
|
|
void UpdateConfig(color_ostream & out)
|
|
{
|
|
if(!rconfig.isValid())
|
|
{
|
|
string keyname = "autobutcher/watchlist/" + getRaceName(raceId);
|
|
rconfig = World::GetPersistentData(keyname, NULL);
|
|
}
|
|
if(rconfig.isValid())
|
|
{
|
|
rconfig.ival(0) = raceId;
|
|
rconfig.ival(1) = isWatched;
|
|
rconfig.ival(2) = fk;
|
|
rconfig.ival(3) = mk;
|
|
rconfig.ival(4) = fa;
|
|
rconfig.ival(5) = ma;
|
|
}
|
|
else
|
|
{
|
|
// this should never happen
|
|
string keyname = "autobutcher/watchlist/" + getRaceName(raceId);
|
|
out << "Something failed, could not find/create config key " << keyname << "!" << endl;
|
|
}
|
|
}
|
|
|
|
void RemoveConfig(color_ostream & out)
|
|
{
|
|
if(!rconfig.isValid())
|
|
return;
|
|
World::DeletePersistentData(rconfig);
|
|
}
|
|
|
|
void SortUnitsByAge()
|
|
{
|
|
sort(fk_ptr.begin(), fk_ptr.end(), compareUnitAgesOlder);
|
|
sort(mk_ptr.begin(), mk_ptr.end(), compareUnitAgesOlder);
|
|
sort(fa_ptr.begin(), fa_ptr.end(), compareUnitAgesYounger);
|
|
sort(ma_ptr.begin(), ma_ptr.end(), compareUnitAgesYounger);
|
|
}
|
|
|
|
void PushUnit(df::unit * unit)
|
|
{
|
|
if(isFemale(unit))
|
|
{
|
|
if(isBaby(unit) || isChild(unit))
|
|
fk_ptr.push_back(unit);
|
|
else
|
|
fa_ptr.push_back(unit);
|
|
}
|
|
else //treat sex n/a like it was male
|
|
{
|
|
if(isBaby(unit) || isChild(unit))
|
|
mk_ptr.push_back(unit);
|
|
else
|
|
ma_ptr.push_back(unit);
|
|
}
|
|
}
|
|
|
|
void PushProtectedUnit(df::unit * unit)
|
|
{
|
|
if(isFemale(unit))
|
|
{
|
|
if(isBaby(unit) || isChild(unit))
|
|
fk_prot++;
|
|
else
|
|
fa_prot++;
|
|
}
|
|
else //treat sex n/a like it was male
|
|
{
|
|
if(isBaby(unit) || isChild(unit))
|
|
mk_prot++;
|
|
else
|
|
ma_prot++;
|
|
}
|
|
}
|
|
|
|
void ClearUnits()
|
|
{
|
|
fk_prot = fa_prot = mk_prot = ma_prot = 0;
|
|
fk_ptr.clear();
|
|
mk_ptr.clear();
|
|
fa_ptr.clear();
|
|
ma_ptr.clear();
|
|
}
|
|
|
|
int ProcessUnits_fk()
|
|
{
|
|
int subcount = 0;
|
|
while(fk_ptr.size() && (fk_ptr.size() + fk_prot > fk) )
|
|
{
|
|
df::unit* unit = fk_ptr.back();
|
|
doMarkForSlaughter(unit);
|
|
fk_ptr.pop_back();
|
|
subcount++;
|
|
}
|
|
return subcount;
|
|
}
|
|
|
|
int ProcessUnits_mk()
|
|
{
|
|
int subcount = 0;
|
|
while(mk_ptr.size() && (mk_ptr.size() + mk_prot > mk) )
|
|
{
|
|
df::unit* unit = mk_ptr.back();
|
|
doMarkForSlaughter(unit);
|
|
mk_ptr.pop_back();
|
|
subcount++;
|
|
}
|
|
return subcount;
|
|
}
|
|
|
|
int ProcessUnits_fa()
|
|
{
|
|
int subcount = 0;
|
|
while(fa_ptr.size() && (fa_ptr.size() + fa_prot > fa) )
|
|
{
|
|
df::unit* unit = fa_ptr.back();
|
|
doMarkForSlaughter(unit);
|
|
fa_ptr.pop_back();
|
|
subcount++;
|
|
}
|
|
return subcount;
|
|
}
|
|
|
|
int ProcessUnits_ma()
|
|
{
|
|
int subcount = 0;
|
|
while(ma_ptr.size() && (ma_ptr.size() + ma_prot > ma) )
|
|
{
|
|
df::unit* unit = ma_ptr.back();
|
|
doMarkForSlaughter(unit);
|
|
ma_ptr.pop_back();
|
|
subcount++;
|
|
}
|
|
return subcount;
|
|
}
|
|
|
|
int ProcessUnits()
|
|
{
|
|
SortUnitsByAge();
|
|
int slaughter_count = 0;
|
|
slaughter_count += ProcessUnits_fk();
|
|
slaughter_count += ProcessUnits_mk();
|
|
slaughter_count += ProcessUnits_fa();
|
|
slaughter_count += ProcessUnits_ma();
|
|
ClearUnits();
|
|
return slaughter_count;
|
|
}
|
|
};
|
|
// vector of races handled by autobutcher
|
|
// the name is a bit misleading since entries can be set to 'unwatched'
|
|
// to ignore them for a while but still keep the target count settings
|
|
std::vector<WatchedRace*> watched_races;
|
|
|
|
// helper for sorting the watchlist alphabetically
|
|
bool compareRaceNames(WatchedRace* i, WatchedRace* j)
|
|
{
|
|
string name_i = getRaceNamePlural(i->raceId);
|
|
string name_j = getRaceNamePlural(j->raceId);
|
|
|
|
return (name_i < name_j);
|
|
}
|
|
|
|
static void autobutcher_sortWatchList(color_ostream &out);
|
|
|
|
// default target values for autobutcher
|
|
static int default_fk = 5;
|
|
static int default_mk = 1;
|
|
static int default_fa = 5;
|
|
static int default_ma = 1;
|
|
|
|
command_result df_autobutcher(color_ostream &out, vector <string> & parameters)
|
|
{
|
|
CoreSuspender suspend;
|
|
|
|
bool verbose = false;
|
|
bool watch_race = false;
|
|
bool unwatch_race = false;
|
|
bool forget_race = false;
|
|
bool list_watched = false;
|
|
bool list_export = false;
|
|
bool change_target = false;
|
|
vector <string> target_racenames;
|
|
vector <int> target_raceids;
|
|
|
|
int target_fk = default_fk;
|
|
int target_mk = default_mk;
|
|
int target_fa = default_fa;
|
|
int target_ma = default_ma;
|
|
|
|
int32_t target_raceid = -1;
|
|
|
|
if(!parameters.size())
|
|
{
|
|
out << "You must specify a command!" << endl;
|
|
out << autobutcher_help << endl;
|
|
return CR_OK;
|
|
}
|
|
|
|
// parse main command
|
|
string & p = parameters[0];
|
|
if (p == "help" || p == "?")
|
|
{
|
|
out << autobutcher_help << endl;
|
|
return CR_OK;
|
|
}
|
|
if (p == "example")
|
|
{
|
|
out << autobutcher_help_example << endl;
|
|
return CR_OK;
|
|
}
|
|
else if (p == "start")
|
|
{
|
|
plugin_enable(out, true);
|
|
enable_autobutcher = true;
|
|
start_autobutcher(out);
|
|
return autoButcher(out, verbose);
|
|
}
|
|
else if (p == "stop")
|
|
{
|
|
enable_autobutcher = false;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(0) = enable_autobutcher;
|
|
out << "Autobutcher stopped." << endl;
|
|
return CR_OK;
|
|
}
|
|
else if(p == "sleep")
|
|
{
|
|
parameters.erase(parameters.begin());
|
|
if(!parameters.size())
|
|
{
|
|
out.printerr("No duration specified!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
size_t ticks = 0;
|
|
stringstream ss(parameters.back());
|
|
ss >> ticks;
|
|
if(ticks <= 0)
|
|
{
|
|
out.printerr("Invalid duration specified (must be > 0)!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
sleep_autobutcher = ticks;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(1) = sleep_autobutcher;
|
|
out << "New sleep timer for autobutcher: " << ticks << " ticks." << endl;
|
|
return CR_OK;
|
|
}
|
|
}
|
|
else if(p == "watch")
|
|
{
|
|
parameters.erase(parameters.begin());
|
|
watch_race = true;
|
|
out << "Start watching race(s): "; // << endl;
|
|
}
|
|
else if(p == "unwatch")
|
|
{
|
|
parameters.erase(parameters.begin());
|
|
unwatch_race = true;
|
|
out << "Stop watching race(s): "; // << endl;
|
|
}
|
|
else if(p == "forget")
|
|
{
|
|
parameters.erase(parameters.begin());
|
|
forget_race = true;
|
|
out << "Removing race(s) from watchlist: "; // << endl;
|
|
}
|
|
else if(p == "target")
|
|
{
|
|
// needs at least 5 more parameters:
|
|
// fk mk fa ma R (can have more than 1 R)
|
|
if(parameters.size() < 6)
|
|
{
|
|
out.printerr("Not enough parameters!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
else
|
|
{
|
|
stringstream fk(parameters[1]);
|
|
stringstream mk(parameters[2]);
|
|
stringstream fa(parameters[3]);
|
|
stringstream ma(parameters[4]);
|
|
fk >> target_fk;
|
|
mk >> target_mk;
|
|
fa >> target_fa;
|
|
ma >> target_ma;
|
|
parameters.erase(parameters.begin(), parameters.begin()+5);
|
|
change_target = true;
|
|
out << "Setting new target count for race(s): "; // << endl;
|
|
}
|
|
}
|
|
else if(p == "autowatch")
|
|
{
|
|
out << "Auto-adding to watchlist started." << endl;
|
|
enable_autobutcher_autowatch = true;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
return CR_OK;
|
|
}
|
|
else if(p == "noautowatch")
|
|
{
|
|
out << "Auto-adding to watchlist stopped." << endl;
|
|
enable_autobutcher_autowatch = false;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
return CR_OK;
|
|
}
|
|
else if(p == "list")
|
|
{
|
|
list_watched = true;
|
|
}
|
|
else if(p == "list_export")
|
|
{
|
|
list_export = true;
|
|
}
|
|
else
|
|
{
|
|
out << "Unknown command: " << p << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
|
|
if(list_watched)
|
|
{
|
|
out << "Autobutcher status: ";
|
|
|
|
if(enable_autobutcher)
|
|
out << "enabled,";
|
|
else
|
|
out << "not enabled,";
|
|
|
|
if (enable_autobutcher_autowatch)
|
|
out << " autowatch,";
|
|
else
|
|
out << " noautowatch,";
|
|
|
|
out << " sleep: " << sleep_autobutcher << endl;
|
|
|
|
out << "Default setting for new races:"
|
|
<< " fk=" << default_fk
|
|
<< " mk=" << default_mk
|
|
<< " fa=" << default_fa
|
|
<< " ma=" << default_ma
|
|
<< endl;
|
|
|
|
if(!watched_races.size())
|
|
{
|
|
out << "The autobutcher race list is empty." << endl;
|
|
return CR_OK;
|
|
}
|
|
|
|
out << "Races on autobutcher list: " << endl;
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
df::creature_raw * raw = df::global::world->raws.creatures.all[w->raceId];
|
|
string name = raw->creature_id;
|
|
if(w->isWatched)
|
|
out << "watched: ";
|
|
else
|
|
out << "not watched: ";
|
|
out << name
|
|
<< " fk=" << w->fk
|
|
<< " mk=" << w->mk
|
|
<< " fa=" << w->fa
|
|
<< " ma=" << w->ma
|
|
<< endl;
|
|
}
|
|
return CR_OK;
|
|
}
|
|
|
|
if(list_export)
|
|
{
|
|
string run = "dfhack-run autobutcher ";
|
|
#ifdef LINUX_BUILD
|
|
run = "./dfhack-run autobutcher ";
|
|
#endif
|
|
// force creation of config
|
|
out << run << "start" << endl;
|
|
|
|
if(!enable_autobutcher)
|
|
out << run << "stop" << endl;
|
|
|
|
if (enable_autobutcher_autowatch)
|
|
out << run << "autowatch" << endl;
|
|
|
|
out << run << "sleep " << sleep_autobutcher << endl;
|
|
out << run << "target"
|
|
<< " " << default_fk
|
|
<< " " << default_mk
|
|
<< " " << default_fa
|
|
<< " " << default_ma
|
|
<< " new" << endl;
|
|
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
df::creature_raw * raw = df::global::world->raws.creatures.all[w->raceId];
|
|
string name = raw->creature_id;
|
|
|
|
out << run << "target"
|
|
<< " " << w->fk
|
|
<< " " << w->mk
|
|
<< " " << w->fa
|
|
<< " " << w->ma
|
|
<< " " << name << endl;
|
|
|
|
if(w->isWatched)
|
|
out << run << "watch " << name << endl;
|
|
}
|
|
return CR_OK;
|
|
}
|
|
|
|
// parse rest of parameters for commands followed by a list of races
|
|
if( watch_race
|
|
|| unwatch_race
|
|
|| forget_race
|
|
|| change_target )
|
|
{
|
|
if(!parameters.size())
|
|
{
|
|
out.printerr("No race(s) specified!\n");
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
while(parameters.size())
|
|
{
|
|
string tr = parameters.back();
|
|
target_racenames.push_back(tr);
|
|
parameters.pop_back();
|
|
out << tr << " ";
|
|
}
|
|
out << endl;
|
|
}
|
|
|
|
if(change_target && target_racenames.size() && target_racenames[0] == "all")
|
|
{
|
|
out << "Setting target count for all races on watchlist." << endl;
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
w->fk = target_fk;
|
|
w->mk = target_mk;
|
|
w->fa = target_fa;
|
|
w->ma = target_ma;
|
|
w->UpdateConfig(out);
|
|
}
|
|
}
|
|
|
|
if(target_racenames.size() && (target_racenames[0] == "all" || target_racenames[0] == "new"))
|
|
{
|
|
if(change_target)
|
|
{
|
|
out << "Setting target count for the future." << endl;
|
|
default_fk = target_fk;
|
|
default_mk = target_mk;
|
|
default_fa = target_fa;
|
|
default_ma = target_ma;
|
|
if(config_autobutcher.isValid())
|
|
{
|
|
config_autobutcher.ival(3) = default_fk;
|
|
config_autobutcher.ival(4) = default_mk;
|
|
config_autobutcher.ival(5) = default_fa;
|
|
config_autobutcher.ival(6) = default_ma;
|
|
}
|
|
return CR_OK;
|
|
}
|
|
else if(target_racenames[0] == "new")
|
|
{
|
|
out << "The only valid usage of 'new' is in combination when setting a target count!" << endl;
|
|
|
|
// hm, maybe instead of complaining start/stop autowatch instead? and get rid of the autowatch option?
|
|
if(unwatch_race)
|
|
out << "'unwatch new' makes no sense! Use 'noautowatch' instead." << endl;
|
|
else if(forget_race)
|
|
out << "'forget new' makes no sense, 'forget' is only for existing watchlist entries! Use 'noautowatch' instead." << endl;
|
|
else if(watch_race)
|
|
out << "'watch new' makes no sense! Use 'autowatch' instead." << endl;
|
|
return CR_WRONG_USAGE;
|
|
}
|
|
}
|
|
|
|
if(target_racenames.size() && target_racenames[0] == "all")
|
|
{
|
|
// fill with race ids from watchlist
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
target_raceids.push_back(w->raceId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// map race names from parameter list to ids
|
|
size_t num_races = df::global::world->raws.creatures.all.size();
|
|
while(target_racenames.size())
|
|
{
|
|
bool found_race = false;
|
|
for(size_t i=0; i<num_races; i++)
|
|
{
|
|
if(getRaceName(i) == target_racenames.back())
|
|
{
|
|
target_raceids.push_back(i);
|
|
target_racenames.pop_back();
|
|
found_race = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!found_race)
|
|
{
|
|
out << "Race not found: " << target_racenames.back() << endl;
|
|
return CR_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
while(target_raceids.size())
|
|
{
|
|
bool entry_found = false;
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
if(w->raceId == target_raceids.back())
|
|
{
|
|
if(unwatch_race)
|
|
{
|
|
w->isWatched=false;
|
|
w->UpdateConfig(out);
|
|
}
|
|
else if(forget_race)
|
|
{
|
|
w->RemoveConfig(out);
|
|
watched_races.erase(watched_races.begin()+i);
|
|
}
|
|
else if(watch_race)
|
|
{
|
|
w->isWatched = true;
|
|
w->UpdateConfig(out);
|
|
}
|
|
else if(change_target)
|
|
{
|
|
w->fk = target_fk;
|
|
w->mk = target_mk;
|
|
w->fa = target_fa;
|
|
w->ma = target_ma;
|
|
w->UpdateConfig(out);
|
|
}
|
|
entry_found = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!entry_found && (watch_race||change_target))
|
|
{
|
|
WatchedRace * w = new WatchedRace(watch_race, target_raceids.back(), target_fk, target_mk, target_fa, target_ma);
|
|
w->UpdateConfig(out);
|
|
watched_races.push_back(w);
|
|
autobutcher_sortWatchList(out);
|
|
}
|
|
target_raceids.pop_back();
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
// check watched_races vector for a race id, return -1 if nothing found
|
|
// calling method needs to check itself if the race is currently being watched or ignored
|
|
int getWatchedIndex(int id)
|
|
{
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
if(w->raceId == id) // && w->isWatched)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
command_result autoButcher( color_ostream &out, bool verbose = false )
|
|
{
|
|
// don't run if not supposed to
|
|
if(!Maps::IsValid())
|
|
return CR_OK;
|
|
|
|
// check if there is anything to watch before walking through units vector
|
|
if(!enable_autobutcher_autowatch)
|
|
{
|
|
bool watching = false;
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
if(w->isWatched)
|
|
{
|
|
watching = true;
|
|
break;
|
|
}
|
|
}
|
|
if(!watching)
|
|
return CR_OK;
|
|
}
|
|
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
// this check is now divided into two steps, squeezed autowatch into the middle
|
|
// first one ignores completely inappropriate units (dead, undead, not belonging to the fort, ...)
|
|
// then let autowatch add units to the watchlist which will probably start breeding (owned pets, war animals, ...)
|
|
// then process units counting those which can't be butchered (war animals, named pets, ...)
|
|
// so that they are treated as "own stock" as well and count towards the target quota
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMarkedForSlaughter(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
|| !isTame(unit)
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
WatchedRace * w = NULL;
|
|
int watched_index = getWatchedIndex(unit->race);
|
|
if(watched_index != -1)
|
|
{
|
|
w = watched_races[watched_index];
|
|
}
|
|
else if(enable_autobutcher_autowatch)
|
|
{
|
|
w = new WatchedRace(true, unit->race, default_fk, default_mk, default_fa, default_ma);
|
|
w->UpdateConfig(out);
|
|
watched_races.push_back(w);
|
|
|
|
string announce;
|
|
announce = "New race added to autobutcher watchlist: " + getRaceNamePlural(w->raceId);
|
|
Gui::showAnnouncement(announce, 2, false);
|
|
autobutcher_sortWatchList(out);
|
|
}
|
|
|
|
if(w && w->isWatched)
|
|
{
|
|
// don't butcher protected units, but count them as stock as well
|
|
// this way they count towards target quota, so if you order that you want 1 female adult cat
|
|
// and have 2 cats, one of them being a pet, the other gets butchered
|
|
if( isWar(unit) // ignore war dogs etc
|
|
|| isHunter(unit) // ignore hunting dogs etc
|
|
// ignore creatures in built cages which are defined as rooms to leave zoos alone
|
|
// (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher)
|
|
|| (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom()
|
|
|| isAvailableForAdoption(unit)
|
|
|| unit->name.has_name )
|
|
w->PushProtectedUnit(unit);
|
|
else
|
|
w->PushUnit(unit);
|
|
}
|
|
}
|
|
|
|
int slaughter_count = 0;
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
int slaughter_subcount = w->ProcessUnits();
|
|
slaughter_count += slaughter_subcount;
|
|
if(slaughter_subcount)
|
|
{
|
|
stringstream ss;
|
|
ss << slaughter_subcount;
|
|
string announce;
|
|
announce = getRaceNamePlural(w->raceId) + " marked for slaughter: " + ss.str();
|
|
Gui::showAnnouncement(announce, 2, false);
|
|
}
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
////////////////////////////////////////////////////
|
|
// autobutcher and autonestbox start/init/cleanup
|
|
|
|
command_result start_autobutcher(color_ostream &out)
|
|
{
|
|
plugin_enable(out, true);
|
|
enable_autobutcher = true;
|
|
|
|
if (!config_autobutcher.isValid())
|
|
{
|
|
config_autobutcher = World::AddPersistentData("autobutcher/config");
|
|
|
|
if (!config_autobutcher.isValid())
|
|
{
|
|
out << "Cannot enable autobutcher without a world!" << endl;
|
|
return CR_OK;
|
|
}
|
|
|
|
config_autobutcher.ival(1) = sleep_autobutcher;
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
config_autobutcher.ival(3) = default_fk;
|
|
config_autobutcher.ival(4) = default_mk;
|
|
config_autobutcher.ival(5) = default_fa;
|
|
config_autobutcher.ival(6) = default_ma;
|
|
}
|
|
|
|
config_autobutcher.ival(0) = enable_autobutcher;
|
|
|
|
out << "Starting autobutcher." << endl;
|
|
init_autobutcher(out);
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result init_autobutcher(color_ostream &out)
|
|
{
|
|
cleanup_autobutcher(out);
|
|
|
|
config_autobutcher = World::GetPersistentData("autobutcher/config");
|
|
if(config_autobutcher.isValid())
|
|
{
|
|
if (config_autobutcher.ival(0) == -1)
|
|
{
|
|
config_autobutcher.ival(0) = enable_autobutcher;
|
|
config_autobutcher.ival(1) = sleep_autobutcher;
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
config_autobutcher.ival(3) = default_fk;
|
|
config_autobutcher.ival(4) = default_mk;
|
|
config_autobutcher.ival(5) = default_fa;
|
|
config_autobutcher.ival(6) = default_ma;
|
|
out << "Autobutcher's persistent config object was invalid!" << endl;
|
|
}
|
|
else
|
|
{
|
|
enable_autobutcher = config_autobutcher.ival(0);
|
|
sleep_autobutcher = config_autobutcher.ival(1);
|
|
enable_autobutcher_autowatch = config_autobutcher.ival(2);
|
|
default_fk = config_autobutcher.ival(3);
|
|
default_mk = config_autobutcher.ival(4);
|
|
default_fa = config_autobutcher.ival(5);
|
|
default_ma = config_autobutcher.ival(6);
|
|
}
|
|
}
|
|
|
|
if(!enable_autobutcher)
|
|
return CR_OK;
|
|
|
|
plugin_enable(out, true);
|
|
// read watchlist from save
|
|
|
|
std::vector<PersistentDataItem> items;
|
|
World::GetPersistentData(&items, "autobutcher/watchlist/", true);
|
|
for (auto p = items.begin(); p != items.end(); p++)
|
|
{
|
|
string key = p->key();
|
|
out << "Reading from save: " << key << endl;
|
|
//out << " raceid: " << p->ival(0) << endl;
|
|
//out << " watched: " << p->ival(1) << endl;
|
|
//out << " fk: " << p->ival(2) << endl;
|
|
//out << " mk: " << p->ival(3) << endl;
|
|
//out << " fa: " << p->ival(4) << endl;
|
|
//out << " ma: " << p->ival(5) << endl;
|
|
WatchedRace * w = new WatchedRace(p->ival(1), p->ival(0), p->ival(2), p->ival(3),p->ival(4),p->ival(5));
|
|
w->rconfig = *p;
|
|
watched_races.push_back(w);
|
|
}
|
|
autobutcher_sortWatchList(out);
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result cleanup_autobutcher(color_ostream &out)
|
|
{
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
delete watched_races[i];
|
|
}
|
|
watched_races.clear();
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result start_autonestbox(color_ostream &out)
|
|
{
|
|
plugin_enable(out, true);
|
|
enable_autonestbox = true;
|
|
|
|
if (!config_autonestbox.isValid())
|
|
{
|
|
config_autonestbox = World::AddPersistentData("autonestbox/config");
|
|
|
|
if (!config_autonestbox.isValid())
|
|
{
|
|
out << "Cannot enable autonestbox without a world!" << endl;
|
|
return CR_OK;
|
|
}
|
|
|
|
config_autonestbox.ival(1) = sleep_autonestbox;
|
|
}
|
|
|
|
config_autonestbox.ival(0) = enable_autonestbox;
|
|
|
|
out << "Starting autonestbox." << endl;
|
|
init_autonestbox(out);
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result init_autonestbox(color_ostream &out)
|
|
{
|
|
cleanup_autonestbox(out);
|
|
|
|
config_autonestbox = World::GetPersistentData("autonestbox/config");
|
|
if(config_autonestbox.isValid())
|
|
{
|
|
if (config_autonestbox.ival(0) == -1)
|
|
{
|
|
config_autonestbox.ival(0) = enable_autonestbox;
|
|
config_autonestbox.ival(1) = sleep_autonestbox;
|
|
out << "Autonestbox's persistent config object was invalid!" << endl;
|
|
}
|
|
else
|
|
{
|
|
enable_autonestbox = config_autonestbox.ival(0);
|
|
sleep_autonestbox = config_autonestbox.ival(1);
|
|
}
|
|
}
|
|
if (enable_autonestbox)
|
|
plugin_enable(out, true);
|
|
return CR_OK;
|
|
}
|
|
|
|
command_result cleanup_autonestbox(color_ostream &out)
|
|
{
|
|
// nothing to cleanup currently
|
|
// (future version of autonestbox could store info about cages for useless male kids)
|
|
return CR_OK;
|
|
}
|
|
|
|
// abuse WatchedRace struct for counting stocks (since it sorts by gender and age)
|
|
// calling method must delete pointer!
|
|
WatchedRace * checkRaceStocksTotal(int race)
|
|
{
|
|
WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma);
|
|
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
w->PushUnit(unit);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
WatchedRace * checkRaceStocksProtected(int race)
|
|
{
|
|
WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma);
|
|
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
if( !isTame(unit)
|
|
|| isWar(unit) // ignore war dogs etc
|
|
|| isHunter(unit) // ignore hunting dogs etc
|
|
// ignore creatures in built cages which are defined as rooms to leave zoos alone
|
|
// (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher)
|
|
|| (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom()
|
|
|| isAvailableForAdoption(unit)
|
|
|| unit->name.has_name )
|
|
w->PushUnit(unit);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
WatchedRace * checkRaceStocksButcherable(int race)
|
|
{
|
|
WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma);
|
|
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
|| !isTame(unit)
|
|
|| isWar(unit) // ignore war dogs etc
|
|
|| isHunter(unit) // ignore hunting dogs etc
|
|
// ignore creatures in built cages which are defined as rooms to leave zoos alone
|
|
// (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher)
|
|
|| (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom()
|
|
|| isAvailableForAdoption(unit)
|
|
|| unit->name.has_name
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
w->PushUnit(unit);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
WatchedRace * checkRaceStocksButcherFlag(int race)
|
|
{
|
|
WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma);
|
|
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
if(isMarkedForSlaughter(unit))
|
|
w->PushUnit(unit);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
void butcherRace(int race)
|
|
{
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| isMerchant(unit) // ignore merchants' draught animals
|
|
|| isForest(unit) // ignore merchants' caged animals
|
|
|| !isOwnCiv(unit)
|
|
|| !isTame(unit)
|
|
|| isWar(unit) // ignore war dogs etc
|
|
|| isHunter(unit) // ignore hunting dogs etc
|
|
// ignore creatures in built cages which are defined as rooms to leave zoos alone
|
|
// (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher)
|
|
|| (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom()
|
|
|| isAvailableForAdoption(unit)
|
|
|| unit->name.has_name
|
|
)
|
|
continue;
|
|
|
|
// found a bugged unit which had invalid coordinates but was not in a cage.
|
|
// marking it for slaughter didn't seem to have negative effects, but you never know...
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
unit->flags2.bits.slaughter = true;
|
|
}
|
|
}
|
|
|
|
// remove butcher flag for all units of a given race
|
|
void unbutcherRace(int race)
|
|
{
|
|
for(size_t i=0; i<world->units.all.size(); i++)
|
|
{
|
|
df::unit * unit = world->units.all[i];
|
|
|
|
if(unit->race != race)
|
|
continue;
|
|
|
|
if( isDead(unit)
|
|
|| isUndead(unit)
|
|
|| !isMarkedForSlaughter(unit)
|
|
)
|
|
continue;
|
|
|
|
if(!isContainedInItem(unit) && !hasValidMapPos(unit))
|
|
continue;
|
|
|
|
unit->flags2.bits.slaughter = false;
|
|
}
|
|
}
|
|
|
|
|
|
/////////////////////////////////////
|
|
// API functions to control autobutcher with a lua script
|
|
|
|
static bool autobutcher_isEnabled() { return enable_autobutcher; }
|
|
static bool autowatch_isEnabled() { return enable_autobutcher_autowatch; }
|
|
|
|
static unsigned autobutcher_getSleep(color_ostream &out)
|
|
{
|
|
return sleep_autobutcher;
|
|
}
|
|
|
|
static void autobutcher_setSleep(color_ostream &out, unsigned ticks)
|
|
{
|
|
sleep_autobutcher = ticks;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(1) = sleep_autobutcher;
|
|
}
|
|
|
|
static void autobutcher_setEnabled(color_ostream &out, bool enable)
|
|
{
|
|
if(enable)
|
|
{
|
|
enable_autobutcher = true;
|
|
start_autobutcher(out);
|
|
autoButcher(out, false);
|
|
}
|
|
else
|
|
{
|
|
enable_autobutcher = false;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(0) = enable_autobutcher;
|
|
out << "Autobutcher stopped." << endl;
|
|
}
|
|
|
|
if (enable)
|
|
plugin_enable(out, true);
|
|
}
|
|
|
|
static void autowatch_setEnabled(color_ostream &out, bool enable)
|
|
{
|
|
if(enable)
|
|
{
|
|
out << "Auto-adding to watchlist started." << endl;
|
|
enable_autobutcher_autowatch = true;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
}
|
|
else
|
|
{
|
|
out << "Auto-adding to watchlist stopped." << endl;
|
|
enable_autobutcher_autowatch = false;
|
|
if(config_autobutcher.isValid())
|
|
config_autobutcher.ival(2) = enable_autobutcher_autowatch;
|
|
}
|
|
}
|
|
|
|
// set all data for a watchlist race in one go
|
|
// if race is not already on watchlist it will be added
|
|
// params: (id, fk, mk, fa, ma, watched)
|
|
static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsigned fk, unsigned mk, unsigned fa, unsigned ma, bool watched)
|
|
{
|
|
int watched_index = getWatchedIndex(id);
|
|
if(watched_index != -1)
|
|
{
|
|
out << "updating watchlist entry" << endl;
|
|
WatchedRace * w = watched_races[watched_index];
|
|
w->fk = fk;
|
|
w->mk = mk;
|
|
w->fa = fa;
|
|
w->ma = ma;
|
|
w->isWatched = watched;
|
|
w->UpdateConfig(out);
|
|
}
|
|
else
|
|
{
|
|
out << "creating new watchlist entry" << endl;
|
|
WatchedRace * w = new WatchedRace(watched, id, fk, mk, fa, ma); //default_fk, default_mk, default_fa, default_ma);
|
|
w->UpdateConfig(out);
|
|
watched_races.push_back(w);
|
|
|
|
string announce;
|
|
announce = "New race added to autobutcher watchlist: " + getRaceNamePlural(w->raceId);
|
|
Gui::showAnnouncement(announce, 2, false);
|
|
autobutcher_sortWatchList(out);
|
|
}
|
|
}
|
|
|
|
// remove entry from watchlist
|
|
static void autobutcher_removeFromWatchList(color_ostream &out, unsigned id)
|
|
{
|
|
int watched_index = getWatchedIndex(id);
|
|
if(watched_index != -1)
|
|
{
|
|
out << "updating watchlist entry" << endl;
|
|
WatchedRace * w = watched_races[watched_index];
|
|
w->RemoveConfig(out);
|
|
watched_races.erase(watched_races.begin() + watched_index);
|
|
}
|
|
}
|
|
|
|
// sort watchlist alphabetically
|
|
static void autobutcher_sortWatchList(color_ostream &out)
|
|
{
|
|
sort(watched_races.begin(), watched_races.end(), compareRaceNames);
|
|
}
|
|
|
|
// set default target values for new races
|
|
static void autobutcher_setDefaultTargetNew(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma)
|
|
{
|
|
default_fk = fk;
|
|
default_mk = mk;
|
|
default_fa = fa;
|
|
default_ma = ma;
|
|
if(config_autobutcher.isValid())
|
|
{
|
|
config_autobutcher.ival(3) = default_fk;
|
|
config_autobutcher.ival(4) = default_mk;
|
|
config_autobutcher.ival(5) = default_fa;
|
|
config_autobutcher.ival(6) = default_ma;
|
|
}
|
|
}
|
|
|
|
// set default target values for ALL races (update watchlist and set new default)
|
|
static void autobutcher_setDefaultTargetAll(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma)
|
|
{
|
|
for(unsigned i=0; i<watched_races.size(); i++)
|
|
{
|
|
WatchedRace * w = watched_races[i];
|
|
w->fk = fk;
|
|
w->mk = mk;
|
|
w->fa = fa;
|
|
w->ma = ma;
|
|
w->UpdateConfig(out);
|
|
}
|
|
autobutcher_setDefaultTargetNew(out, fk, mk, fa, ma);
|
|
}
|
|
|
|
static void autobutcher_butcherRace(color_ostream &out, unsigned id)
|
|
{
|
|
butcherRace(id);
|
|
}
|
|
|
|
static void autobutcher_unbutcherRace(color_ostream &out, unsigned id)
|
|
{
|
|
unbutcherRace(id);
|
|
}
|
|
|
|
// push autobutcher settings on lua stack
|
|
static int autobutcher_getSettings(lua_State *L)
|
|
{
|
|
color_ostream &out = *Lua::GetOutput(L);
|
|
lua_newtable(L);
|
|
int ctable = lua_gettop(L);
|
|
Lua::SetField(L, enable_autobutcher, ctable, "enable_autobutcher");
|
|
Lua::SetField(L, enable_autobutcher_autowatch, ctable, "enable_autowatch");
|
|
Lua::SetField(L, default_fk, ctable, "fk");
|
|
Lua::SetField(L, default_mk, ctable, "mk");
|
|
Lua::SetField(L, default_fa, ctable, "fa");
|
|
Lua::SetField(L, default_ma, ctable, "ma");
|
|
Lua::SetField(L, sleep_autobutcher, ctable, "sleep");
|
|
return 1;
|
|
}
|
|
|
|
// push the watchlist vector as nested table on the lua stack
|
|
static int autobutcher_getWatchList(lua_State *L)
|
|
{
|
|
color_ostream &out = *Lua::GetOutput(L);
|
|
lua_newtable(L);
|
|
|
|
for(size_t i=0; i<watched_races.size(); i++)
|
|
{
|
|
lua_newtable(L);
|
|
int ctable = lua_gettop(L);
|
|
WatchedRace * w = watched_races[i];
|
|
Lua::SetField(L, w->raceId, ctable, "id");
|
|
Lua::SetField(L, w->isWatched, ctable, "watched");
|
|
Lua::SetField(L, getRaceNamePlural(w->raceId), ctable, "name");
|
|
Lua::SetField(L, w->fk, ctable, "fk");
|
|
Lua::SetField(L, w->mk, ctable, "mk");
|
|
Lua::SetField(L, w->fa, ctable, "fa");
|
|
Lua::SetField(L, w->ma, ctable, "ma");
|
|
|
|
int id = w->raceId;
|
|
|
|
w = checkRaceStocksTotal(id);
|
|
Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_total");
|
|
Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_total");
|
|
Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_total");
|
|
Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_total");
|
|
delete w;
|
|
|
|
w = checkRaceStocksProtected(id);
|
|
Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_protected");
|
|
Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_protected");
|
|
Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_protected");
|
|
Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_protected");
|
|
delete w;
|
|
|
|
w = checkRaceStocksButcherable(id);
|
|
Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_butcherable");
|
|
Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_butcherable");
|
|
Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_butcherable");
|
|
Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_butcherable");
|
|
delete w;
|
|
|
|
w = checkRaceStocksButcherFlag(id);
|
|
Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_butcherflag");
|
|
Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_butcherflag");
|
|
Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_butcherflag");
|
|
Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_butcherflag");
|
|
delete w;
|
|
|
|
lua_rawseti(L, -2, i+1);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
DFHACK_PLUGIN_LUA_FUNCTIONS {
|
|
DFHACK_LUA_FUNCTION(autobutcher_isEnabled),
|
|
DFHACK_LUA_FUNCTION(autowatch_isEnabled),
|
|
DFHACK_LUA_FUNCTION(autobutcher_setEnabled),
|
|
DFHACK_LUA_FUNCTION(autowatch_setEnabled),
|
|
DFHACK_LUA_FUNCTION(autobutcher_getSleep),
|
|
DFHACK_LUA_FUNCTION(autobutcher_setSleep),
|
|
DFHACK_LUA_FUNCTION(autobutcher_setWatchListRace),
|
|
DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetNew),
|
|
DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetAll),
|
|
DFHACK_LUA_FUNCTION(autobutcher_butcherRace),
|
|
DFHACK_LUA_FUNCTION(autobutcher_unbutcherRace),
|
|
DFHACK_LUA_FUNCTION(autobutcher_removeFromWatchList),
|
|
DFHACK_LUA_FUNCTION(autobutcher_sortWatchList),
|
|
DFHACK_LUA_END
|
|
};
|
|
|
|
DFHACK_PLUGIN_LUA_COMMANDS {
|
|
DFHACK_LUA_COMMAND(autobutcher_getSettings),
|
|
DFHACK_LUA_COMMAND(autobutcher_getWatchList),
|
|
DFHACK_LUA_END
|
|
};
|
|
|
|
// end lua API
|
|
|
|
|
|
|
|
//START zone filters
|
|
using df::global::ui_building_item_cursor;
|
|
using df::global::ui_building_assign_type;
|
|
using df::global::ui_building_assign_is_marked;
|
|
using df::global::ui_building_assign_units;
|
|
using df::global::ui_building_assign_items;
|
|
|
|
using df::global::ui_building_in_assign;
|
|
|
|
void OutputString(int8_t color, int &x, int y, const std::string &text)
|
|
{
|
|
Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
|
|
x += text.length();
|
|
}
|
|
|
|
class zone_filter
|
|
{
|
|
public:
|
|
zone_filter()
|
|
{
|
|
initialized = false;
|
|
}
|
|
|
|
void initialize(const df::ui_sidebar_mode &mode)
|
|
{
|
|
if (!initialized)
|
|
{
|
|
this->mode = mode;
|
|
saved_ui_building_assign_type.clear();
|
|
saved_ui_building_assign_units.clear();
|
|
saved_ui_building_assign_items.clear();
|
|
saved_ui_building_assign_is_marked.clear();
|
|
saved_indexes.clear();
|
|
|
|
for (size_t i = 0; i < ui_building_assign_units->size(); i++)
|
|
{
|
|
saved_ui_building_assign_type.push_back(ui_building_assign_type->at(i));
|
|
saved_ui_building_assign_units.push_back(ui_building_assign_units->at(i));
|
|
saved_ui_building_assign_items.push_back(ui_building_assign_items->at(i));
|
|
saved_ui_building_assign_is_marked.push_back(ui_building_assign_is_marked->at(i));
|
|
}
|
|
|
|
search_string.clear();
|
|
show_non_grazers = show_pastured = show_noncaged = show_male = show_female = show_other_zones = true;
|
|
entry_mode = false;
|
|
|
|
initialized = true;
|
|
}
|
|
}
|
|
|
|
void deinitialize()
|
|
{
|
|
initialized = false;
|
|
}
|
|
|
|
void apply_filters()
|
|
{
|
|
if (saved_indexes.size() > 0)
|
|
{
|
|
bool list_has_been_sorted = (ui_building_assign_units->size() == reference_list.size()
|
|
&& *ui_building_assign_units != reference_list);
|
|
|
|
for (size_t i = 0; i < saved_indexes.size(); i++)
|
|
{
|
|
int adjusted_item_index = i;
|
|
if (list_has_been_sorted)
|
|
{
|
|
for (int j = 0; j < ui_building_assign_units->size(); j++)
|
|
{
|
|
if (ui_building_assign_units->at(j) == reference_list[i])
|
|
{
|
|
adjusted_item_index = j;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
saved_ui_building_assign_is_marked[saved_indexes[i]] = ui_building_assign_is_marked->at(adjusted_item_index);
|
|
}
|
|
}
|
|
|
|
string search_string_l = toLower(search_string);
|
|
saved_indexes.clear();
|
|
ui_building_assign_type->clear();
|
|
ui_building_assign_is_marked->clear();
|
|
ui_building_assign_units->clear();
|
|
ui_building_assign_items->clear();
|
|
|
|
for (size_t i = 0; i < saved_ui_building_assign_units.size(); i++)
|
|
{
|
|
df::unit *curr_unit = saved_ui_building_assign_units[i];
|
|
|
|
if (!curr_unit)
|
|
continue;
|
|
|
|
if (!show_non_grazers && !isGrazer(curr_unit))
|
|
continue;
|
|
|
|
if (!show_pastured && isAssignedToZone(curr_unit))
|
|
continue;
|
|
|
|
if (!show_noncaged)
|
|
{
|
|
// must be in a container
|
|
if(!isContainedInItem(curr_unit))
|
|
continue;
|
|
// but exclude built cages (zoos, traps, ...) to avoid "accidental" pitting of creatures you'd prefer to keep
|
|
if (isInBuiltCage(curr_unit))
|
|
continue;
|
|
}
|
|
|
|
if (!show_male && isMale(curr_unit))
|
|
continue;
|
|
|
|
if (!show_female && isFemale(curr_unit))
|
|
continue;
|
|
|
|
if (!search_string_l.empty())
|
|
{
|
|
string desc = Translation::TranslateName(
|
|
Units::getVisibleName(curr_unit), false);
|
|
|
|
desc += Units::getProfessionName(curr_unit);
|
|
desc = toLower(desc);
|
|
|
|
if (desc.find(search_string_l) == string::npos)
|
|
continue;
|
|
}
|
|
|
|
ui_building_assign_type->push_back(saved_ui_building_assign_type[i]);
|
|
ui_building_assign_units->push_back(curr_unit);
|
|
ui_building_assign_items->push_back(saved_ui_building_assign_items[i]);
|
|
ui_building_assign_is_marked->push_back(saved_ui_building_assign_is_marked[i]);
|
|
|
|
saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed
|
|
}
|
|
|
|
reference_list = *ui_building_assign_units;
|
|
*ui_building_item_cursor = 0;
|
|
}
|
|
|
|
bool handle_input(const set<df::interface_key> *input)
|
|
{
|
|
if (!initialized)
|
|
return false;
|
|
|
|
bool key_processed = true;
|
|
|
|
if (entry_mode)
|
|
{
|
|
// Query typing mode
|
|
|
|
if (input->count(interface_key::SECONDSCROLL_UP) || input->count(interface_key::SECONDSCROLL_DOWN) ||
|
|
input->count(interface_key::SECONDSCROLL_PAGEUP) || input->count(interface_key::SECONDSCROLL_PAGEDOWN))
|
|
{
|
|
// Arrow key pressed. Leave entry mode and allow screen to process key
|
|
entry_mode = false;
|
|
return false;
|
|
}
|
|
|
|
df::interface_key last_token = *input->rbegin();
|
|
int charcode = Screen::keyToChar(last_token);
|
|
if (charcode >= 32 && charcode <= 126)
|
|
{
|
|
// Standard character
|
|
search_string += char(charcode);
|
|
apply_filters();
|
|
}
|
|
else if (last_token == interface_key::STRING_A000)
|
|
{
|
|
// Backspace
|
|
if (search_string.length() > 0)
|
|
{
|
|
search_string.erase(search_string.length()-1);
|
|
apply_filters();
|
|
}
|
|
}
|
|
else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN))
|
|
{
|
|
// ENTER or ESC: leave typing mode
|
|
entry_mode = false;
|
|
}
|
|
}
|
|
// Not in query typing mode
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_G) &&
|
|
(mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding))
|
|
{
|
|
show_non_grazers = !show_non_grazers;
|
|
apply_filters();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_C) &&
|
|
(mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding))
|
|
{
|
|
show_noncaged = !show_noncaged;
|
|
apply_filters();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_P) &&
|
|
(mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding))
|
|
{
|
|
show_pastured = !show_pastured;
|
|
apply_filters();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_M) &&
|
|
(mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding))
|
|
{
|
|
show_male = !show_male;
|
|
apply_filters();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_F) &&
|
|
(mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding))
|
|
{
|
|
show_female = !show_female;
|
|
apply_filters();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_S))
|
|
{
|
|
// Hotkey pressed, enter typing mode
|
|
entry_mode = true;
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_S))
|
|
{
|
|
// Shift + Hotkey pressed, clear query
|
|
search_string.clear();
|
|
apply_filters();
|
|
}
|
|
else
|
|
{
|
|
// Not a key for us, pass it on to the screen
|
|
key_processed = false;
|
|
}
|
|
|
|
return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode
|
|
}
|
|
|
|
void do_render()
|
|
{
|
|
if (!initialized)
|
|
return;
|
|
|
|
int left_margin = gps->dimx - 30;
|
|
int8_t a = *df::global::ui_menu_width;
|
|
int8_t b = *df::global::ui_area_map_width;
|
|
if ((a == 1 && b > 1) || (a == 2 && b == 2))
|
|
left_margin -= 24;
|
|
|
|
int x = left_margin;
|
|
int y = 24;
|
|
|
|
OutputString(COLOR_BROWN, x, y, "DFHack Filtering");
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "s");
|
|
OutputString(COLOR_WHITE, x, y, ": Search");
|
|
if (!search_string.empty() || entry_mode)
|
|
{
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
if (!search_string.empty())
|
|
OutputString(COLOR_WHITE, x, y, search_string);
|
|
if (entry_mode)
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "_");
|
|
}
|
|
|
|
if (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding)
|
|
{
|
|
x = left_margin;
|
|
y += 2;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "G");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_non_grazers) ? COLOR_WHITE : COLOR_GREY, x, y, "Non-Grazing");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "C");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "P");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "F");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "M");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male");
|
|
}
|
|
|
|
// pits don't have grazer filter because it seems pointless
|
|
if (mode == ui_sidebar_mode::ZonesPitInfo)
|
|
{
|
|
x = left_margin;
|
|
y += 2;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "C");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "P");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "F");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female");
|
|
|
|
x = left_margin;
|
|
++y;
|
|
OutputString(COLOR_LIGHTGREEN, x, y, "M");
|
|
OutputString(COLOR_WHITE, x, y, ": ");
|
|
OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male");
|
|
}
|
|
}
|
|
|
|
private:
|
|
df::ui_sidebar_mode mode;
|
|
string search_string;
|
|
bool initialized;
|
|
bool entry_mode;
|
|
bool show_non_grazers, show_pastured, show_noncaged, show_male, show_female, show_other_zones;
|
|
|
|
std::vector<int8_t> saved_ui_building_assign_type;
|
|
std::vector<df::unit*> saved_ui_building_assign_units, reference_list;
|
|
std::vector<df::item*> saved_ui_building_assign_items;
|
|
std::vector<char> saved_ui_building_assign_is_marked;
|
|
|
|
vector <int> saved_indexes;
|
|
|
|
};
|
|
|
|
struct zone_hook : public df::viewscreen_dwarfmodest
|
|
{
|
|
typedef df::viewscreen_dwarfmodest interpose_base;
|
|
static zone_filter filter;
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
|
|
{
|
|
if (!filter.handle_input(input))
|
|
INTERPOSE_NEXT(feed)(input);
|
|
}
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(void, render, ())
|
|
{
|
|
if ( ( (ui->main.mode == ui_sidebar_mode::ZonesPenInfo || ui->main.mode == ui_sidebar_mode::ZonesPitInfo) &&
|
|
ui_building_assign_type && ui_building_assign_units &&
|
|
ui_building_assign_is_marked && ui_building_assign_items &&
|
|
ui_building_assign_type->size() == ui_building_assign_units->size() &&
|
|
ui_building_item_cursor)
|
|
// allow mode QueryBuilding, but only for cages (bedrooms will crash DF with this code, chains don't work either etc)
|
|
||
|
|
( ui->main.mode == ui_sidebar_mode::QueryBuilding &&
|
|
ui_building_in_assign && *ui_building_in_assign &&
|
|
ui_building_assign_type && ui_building_assign_units &&
|
|
ui_building_assign_type->size() == ui_building_assign_units->size() &&
|
|
ui_building_item_cursor &&
|
|
world->selected_building && isCage(world->selected_building) )
|
|
)
|
|
{
|
|
if (vector_get(*ui_building_assign_units, *ui_building_item_cursor))
|
|
filter.initialize(ui->main.mode);
|
|
}
|
|
else
|
|
{
|
|
filter.deinitialize();
|
|
}
|
|
|
|
INTERPOSE_NEXT(render)();
|
|
|
|
filter.do_render();
|
|
|
|
}
|
|
};
|
|
|
|
zone_filter zone_hook::filter;
|
|
|
|
IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, feed);
|
|
IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, render);
|
|
//END zone filters
|
|
|
|
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
|
|
{
|
|
if (!gps)
|
|
return CR_FAILURE;
|
|
|
|
if (enable != is_enabled)
|
|
{
|
|
if (!INTERPOSE_HOOK(zone_hook, feed).apply(enable) ||
|
|
!INTERPOSE_HOOK(zone_hook, render).apply(enable))
|
|
return CR_FAILURE;
|
|
|
|
is_enabled = enable;
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
|
|
{
|
|
commands.push_back(PluginCommand(
|
|
"zone", "manage activity zones.",
|
|
df_zone, false,
|
|
zone_help.c_str()
|
|
));
|
|
commands.push_back(PluginCommand(
|
|
"autonestbox", "auto-assign nestbox zones.",
|
|
df_autonestbox, false,
|
|
autonestbox_help.c_str()
|
|
));
|
|
commands.push_back(PluginCommand(
|
|
"autobutcher", "auto-assign lifestock for butchering.",
|
|
df_autobutcher, false,
|
|
autobutcher_help.c_str()
|
|
));
|
|
init_autobutcher(out);
|
|
init_autonestbox(out);
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
|
|
{
|
|
cleanup_autobutcher(out);
|
|
cleanup_autonestbox(out);
|
|
return CR_OK;
|
|
}
|