Merge pull request #3589 from myk002/myk_dwarfvet

[dwarfvet] update for v50
develop
Myk 2023-07-21 14:35:06 -07:00 committed by GitHub
commit d95ba2f93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 363 additions and 714 deletions

@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
## New Plugins ## New Plugins
- `3dveins`: reinstated for v50, this plugin replaces vanilla DF's blobby vein generation with veins that flow smoothly and naturally between z-levels - `3dveins`: reinstated for v50, this plugin replaces vanilla DF's blobby vein generation with veins that flow smoothly and naturally between z-levels
- `zone`: new searchable, sortable, filterable screen for assigning units to pastures - `zone`: new searchable, sortable, filterable screen for assigning units to pastures
- `dwarfvet`: reinstated and updated for v50's new hospital mechanics; allow your animals to have their wounds treated at hospitals
- `dig`: new ``dig.asciiwarmdamp`` overlay that highlights warm and damp tiles when in ASCII mode. there is no effect in graphics mode since the tiles are already highlighted there - `dig`: new ``dig.asciiwarmdamp`` overlay that highlights warm and damp tiles when in ASCII mode. there is no effect in graphics mode since the tiles are already highlighted there
## Fixes ## Fixes

@ -1589,6 +1589,11 @@ Units module
Currently only one dream per unit is supported by Dwarf Fortress. Currently only one dream per unit is supported by Dwarf Fortress.
Support for multiple dreams may be added in future versions of Dwarf Fortress. Support for multiple dreams may be added in future versions of Dwarf Fortress.
* ``dfhack.units.getReadableName(unit)``
Returns a string that includes the language name of the unit (if any), the
race of the unit, and any syndrome-given descriptions (such as "necromancer").
* ``dfhack.units.getStressCategory(unit)`` * ``dfhack.units.getStressCategory(unit)``
Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least.

@ -2,22 +2,34 @@ dwarfvet
======== ========
.. dfhack-tool:: .. dfhack-tool::
:summary: Allows animals to be treated at animal hospitals. :summary: Allow animals to be treated at hospitals.
:tags: unavailable fort gameplay animals :tags: fort gameplay animals
Annoyed that your dragons become useless after a minor injury? Well, with Annoyed that your dragons become useless after a minor injury? Well, with
dwarfvet, injured animals will be treated at an animal hospital, which is simply dwarfvet, injured animals will be treated at a hospital. Dwarfs with the Animal
a hospital that is also an animal training zone. Dwarfs with the Animal Caretaker labor enabled will come to the hospital to treat the animals. Normal
Caretaker labor enabled will come to the hospital to treat animals. Normal
medical skills are used (and trained), but no experience is given to the Animal medical skills are used (and trained), but no experience is given to the Animal
Caretaker skill itself. Caretaker skill itself.
You can enable ``dwarfvet`` in `gui/control-panel`, and you can choose to start
``dwarfvet`` automatically in new forts in the ``Autostart`` tab.
Usage Usage
----- -----
``enable dwarfvet`` ::
Enables the plugin.
``dwarfvet report`` enable dwarfvet
Reports all zones that the game considers animal hospitals. dwarfvet [status]
``dwarfvet report-usage`` dwarfvet now
Reports on animals currently being treated.
Examples
--------
``dwarfvet``
Report on how many animals are being treated and how many are in need of
treatment.
``dwarfvet now``
Assign injured animals to a free floor spot in a nearby hospital,
regardless of whether the plugin is enabled.

@ -1824,6 +1824,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceBabyNameById),
WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildName),
WRAPM(Units, getRaceChildNameById), WRAPM(Units, getRaceChildNameById),
WRAPM(Units, getReadableName),
WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialActivity),
WRAPM(Units, getMainSocialEvent), WRAPM(Units, getMainSocialEvent),
WRAPM(Units, getStressCategory), WRAPM(Units, getStressCategory),

@ -192,7 +192,7 @@ DFHACK_EXPORT std::string getRaceBabyNameById(int32_t race_id);
DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit); DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit);
DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id);
DFHACK_EXPORT std::string getRaceChildName(df::unit* unit); DFHACK_EXPORT std::string getRaceChildName(df::unit* unit);
DFHACK_EXPORT std::string getReadableName(df::unit* unit);
DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
DFHACK_EXPORT int getKillCount(df::unit *unit); DFHACK_EXPORT int getKillCount(df::unit *unit);

@ -53,6 +53,7 @@ using namespace std;
#include "df/activity_entry.h" #include "df/activity_entry.h"
#include "df/burrow.h" #include "df/burrow.h"
#include "df/caste_raw.h" #include "df/caste_raw.h"
#include "df/creature_interaction_effect_display_namest.h"
#include "df/creature_raw.h" #include "df/creature_raw.h"
#include "df/curse_attr_change.h" #include "df/curse_attr_change.h"
#include "df/entity_position.h" #include "df/entity_position.h"
@ -74,11 +75,13 @@ using namespace std;
#include "df/nemesis_record.h" #include "df/nemesis_record.h"
#include "df/tile_occupancy.h" #include "df/tile_occupancy.h"
#include "df/plotinfost.h" #include "df/plotinfost.h"
#include "df/syndrome.h"
#include "df/unit_inventory_item.h" #include "df/unit_inventory_item.h"
#include "df/unit_misc_trait.h" #include "df/unit_misc_trait.h"
#include "df/unit_relationship_type.h" #include "df/unit_relationship_type.h"
#include "df/unit_skill.h" #include "df/unit_skill.h"
#include "df/unit_soul.h" #include "df/unit_soul.h"
#include "df/unit_syndrome.h"
#include "df/unit_wound.h" #include "df/unit_wound.h"
#include "df/world.h" #include "df/world.h"
#include "df/world_data.h" #include "df/world_data.h"
@ -1218,6 +1221,41 @@ string Units::getRaceChildName(df::unit* unit)
return getRaceChildNameById(unit->race); return getRaceChildNameById(unit->race);
} }
static string get_caste_name(df::unit* unit) {
int32_t id = unit->race;
if (id < 0 || (size_t)id >= world->raws.creatures.all.size())
return "";
df::creature_raw* raw = world->raws.creatures.all[id];
int16_t caste = unit->caste;
if (!raw || caste < 0 || (size_t)caste >= raw->caste.size())
return "";
return raw->caste[caste]->caste_name[0];
}
string Units::getReadableName(df::unit* unit) {
string race_name = isChild(unit) ? getRaceChildName(unit) : get_caste_name(unit);
string name = Translation::TranslateName(getVisibleName(unit));
if (name.empty()) {
name = race_name;
} else {
name += ", ";
name += race_name;
}
for (auto unit_syndrome : unit->syndromes.active) {
auto syndrome = df::syndrome::find(unit_syndrome->type);
if (!syndrome)
continue;
for (auto effect : syndrome->ce) {
auto cie = strict_virtual_cast<df::creature_interaction_effect_display_namest>(effect);
if (!cie)
continue;
name += " ";
name += cie->name;
break;
}
}
return name;
}
double Units::getAge(df::unit *unit, bool true_age) double Units::getAge(df::unit *unit, bool true_age)
{ {

@ -106,7 +106,7 @@ dfhack_plugin(dig dig.cpp)
dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua)
#dfhack_plugin(digFlood digFlood.cpp) #dfhack_plugin(digFlood digFlood.cpp)
#add_subdirectory(diggingInvaders) #add_subdirectory(diggingInvaders)
#dfhack_plugin(dwarfvet dwarfvet.cpp) dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua)
#dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua)
#add_subdirectory(embark-assistant) #add_subdirectory(embark-assistant)
#dfhack_plugin(embark-tools embark-tools.cpp) #dfhack_plugin(embark-tools embark-tools.cpp)

@ -20,735 +20,170 @@
* THE SOFTWARE. * THE SOFTWARE.
**/ **/
#include "Console.h" #include "Debug.h"
#include "Core.h" #include "LuaTools.h"
#include "DataDefs.h"
#include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/EventManager.h"
#include "modules/Persistence.h"
#include "modules/Units.h" #include "modules/Units.h"
#include "modules/Buildings.h" #include "modules/World.h"
#include "modules/Maps.h"
#include "modules/Job.h"
#include "df/animal_training_level.h"
#include "df/building_type.h"
#include "df/caste_raw.h"
#include "df/caste_raw_flags.h"
#include "df/creature_raw.h"
#include "df/job.h"
#include "df/general_ref_unit_workerst.h"
#include "df/profession.h"
#include "df/plotinfost.h"
#include "df/unit.h"
#include "df/unit_health_info.h"
#include "df/unit_health_flags.h"
#include "df/world.h" #include "df/world.h"
#include <map>
#include <unordered_set>
#include <vector>
using namespace DFHack; using namespace DFHack;
using namespace DFHack::Units; using std::string;
using namespace DFHack::Buildings; using std::vector;
using namespace std;
DFHACK_PLUGIN("dwarfvet"); DFHACK_PLUGIN("dwarfvet");
DFHACK_PLUGIN_IS_ENABLED(dwarfvet_enabled); DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
static unordered_set<int32_t> tracked_units; namespace DFHack {
static int32_t howOften = 100; // for configuration-related logging
DBG_DECLARE(dwarfvet, status, DebugCategory::LINFO);
struct hospital_spot { // for logging during the periodic scan
int32_t x; DBG_DECLARE(dwarfvet, cycle, DebugCategory::LINFO);
int32_t y;
int32_t z;
};
class Patient {
public:
// Constructor/Deconstrctor
Patient(int32_t id, size_t spot_index, int32_t x, int32_t y, int32_t z);
int32_t getID() { return this->id; };
size_t getSpotIndex() { return this->spot_index; };
int32_t returnX() { return this->spot_in_hospital.x; };
int32_t returnY() { return this->spot_in_hospital.y; };
int32_t returnZ() { return this->spot_in_hospital.z; };
private:
struct hospital_spot spot_in_hospital;
int32_t id;
size_t spot_index;
};
Patient::Patient(int32_t id, size_t spot_index, int32_t x, int32_t y, int32_t z){
this->id = id;
this->spot_index = spot_index;
this->spot_in_hospital.x = x;
this->spot_in_hospital.y = y;
this->spot_in_hospital.z = z;
} }
class AnimalHospital { static const string CONFIG_KEY = string(plugin_name) + "/config";
static PersistentDataItem config;
public: enum ConfigValues {
// Constructor CONFIG_IS_ENABLED = 0,
AnimalHospital(df::building *, color_ostream &out);
~AnimalHospital();
int32_t getID() { return id; }
bool acceptPatient(int32_t id, color_ostream&);
void processPatients(color_ostream &out);
void dischargePatient(Patient * patient, color_ostream &out);
void calculateHospital(bool force, color_ostream &out);
void reportUsage(color_ostream &out);
// GC
bool to_be_deleted;
private:
int spots_open;
int32_t id;
int32_t x1;
int32_t x2;
int32_t y1;
int32_t y2;
int32_t z;
int height;
int length;
// Doing an actual array in C++ is *annoying*, bloody copy constructors */
vector<bool> spots_in_use;
vector<int32_t> building_in_hospital_notification; /* If present, we already notified about this */
vector<Patient*> accepted_patients;
}; };
static int get_config_val(int index) {
AnimalHospital::AnimalHospital(df::building * building, color_ostream &out) { if (!config.isValid())
// Copy in what we need to know return -1;
id = building->id; return config.ival(index);
x1 = building->x1;
x2 = building->x2;
y1 = building->y1;
y2 = building->y2;
z = building->z;
// Determine how many spots we have for animals
this->length = x2-x1+1;
this->height = y2-y1+1;
// And calculate the hospital!
this->calculateHospital(true, out);
} }
static bool get_config_bool(int index) {
AnimalHospital::~AnimalHospital() { return get_config_val(index) == 1;
// Go through and delete all the patients
for (Patient* accepted_patient : this->accepted_patients) {
delete accepted_patient;
}
} }
static void set_config_val(int index, int value) {
bool AnimalHospital::acceptPatient(int32_t id, color_ostream &out) { if (config.isValid())
// This function determines if we can accept a patient, and if we will. config.ival(index) = value;
this->calculateHospital(true, out);
// First, do we have room?
if (!spots_open) return false;
// Yup, let's find the next open spot,
// and give it to our patient
int spot_cur = 0;
for (vector<bool>::iterator spot = this->spots_in_use.begin(); spot != this->spots_in_use.end(); spot++) {
if (*spot == false) {
*spot = true;
break;
}
spot_cur++;
}
spots_open--;
// Convert the spot into x/y/z cords.
int offset_y = spot_cur/length;
int offset_x = spot_cur%length;
// Create the patient!
Patient * patient = new Patient(id,
spot_cur,
this->x1+offset_x,
this->y1+offset_y,
this->z
);
accepted_patients.push_back(patient);
return true;
} }
static void set_config_bool(int index, bool value) {
// Before any use of the hospital, we need to make calculate open spots set_config_val(index, value ? 1 : 0);
// and such. This can change (i.e. stuff built in hospital) and
// such so it should be called on each function.
void AnimalHospital::reportUsage(color_ostream &out) {
// Debugging tool to see parts of the hospital in use
int length_cursor = this->length;
for (bool spot : this->spots_in_use) {
if (spot) out.print("X");
else out.print("-");
length_cursor--;
if (length_cursor <= 0) {
out.print("\n");
length_cursor = this->length;
}
}
out.print("\n");
} }
void AnimalHospital::calculateHospital(bool force, color_ostream &out) { static const int32_t CYCLE_TICKS = 2459; // a prime number that's around 2 days
// Only calculate out the hospital if we actually have a patient in it static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
// (acceptPatient will forcibly rerun this to make sure everything OK
// Should reduce FPS impact of each calculation tick when the hospitals
// are not in use
//if (!force || (spots_open == length*height)) {
// Hospital is idle, don't recalculate
// return;
//}
// Calculate out the total area of the hospital static command_result do_command(color_ostream &out, vector<string> &parameters);
// This can change if a hospital has been resized static void dwarfvet_cycle(color_ostream &out);
this->spots_open = length*height;
this->spots_in_use.assign(this->spots_open, false);
// The spots_in_use maps one to one with a spot
// starting at the upper-left hand corner, then
// across, then down. i.e.
//
// given hospital zone:
//
// UU
// uU
//
// where U is in use, and u isn't, the array
// would be t,t,f,t
// Walk the building array and see what stuff is in the hospital,
// then walk the patient array and remark those spots as used.
// If a patient is in an invalid spot, reassign it
for (df::building *building : world->buildings.all) {
// Check that we're not comparing ourselves;
if (building->id == this->id) {
continue;
}
// Check if the building is on our z level, if it isn't
// then it can't overlap the hospital (until Toady implements
// multi-z buildings
if (building->z != this->z) {
continue;
}
// DF defines activity zones multiple times in the building structure
// If axises agree with each other, we're looking at a reflection of
// ourselves
if (building->x1 == this->x1 &&
building->x2 == this->x2 &&
building->y1 == this->y1 &&
building->y2 == this->y2) {
continue;
}
// Check for X/Y overlap
// I can't believe I had to look this up -_-;
// http://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
if ((this->x1 > building->x2 ||
building->x1 > this->x2 ||
this->y1 > building->y2 ||
building->y1 > this->y2)) {
continue;
}
// Crap, building overlaps, we need to figure out where it is in the hospital
// NOTE: under some conditions, this generates a false warning. Not a lot I can do about it
// Mark spots used by that building as used; FIXME: handle special logic for traction benches and such
int overlap_x1 = std::max(building->x1, this->x1);
int overlap_y1 = std::max(building->y1, this->y1);
int overlap_x2 = std::min(building->x2, this->x2);
int overlap_y2 = std::min(building->y2, this->y2);
for (int x = overlap_x1; x <= overlap_x2; x++) {
for (int y = overlap_y1; y <= overlap_y2; y++) {
int spot_index = (x - this->x1) + (this->length * (y - this->y1));
spots_in_use.at(spot_index) = true;
}
}
}
DFhackCExport command_result plugin_init(color_ostream &out, vector <PluginCommand> &commands) {
commands.push_back(PluginCommand(
plugin_name,
"Allow animals to be treated at hospitals.",
do_command));
return CR_OK;
} }
// Self explanatory DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
void AnimalHospital::dischargePatient(Patient * patient, color_ostream &out) { if (!Core::getInstance().isWorldLoaded()) {
int32_t id = patient->getID(); out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
// Remove them from the hospital
// We can safely iterate here because once we delete the unit
// we no longer use the iterator
for (vector<Patient*>::iterator accepted_patient = this->accepted_patients.begin(); accepted_patient != this->accepted_patients.end(); accepted_patient++) {
if ( (*accepted_patient)->getID() == id) {
out.print("Discharging unit %d from hospital %d\n", id, this->id);
// Reclaim their spot
spots_in_use.at((*accepted_patient)->getSpotIndex()) = false;
this->spots_open++;
delete (*accepted_patient);
this->accepted_patients.erase(accepted_patient);
break;
}
} }
// And the master list if (enable != is_enabled) {
tracked_units.erase(id); is_enabled = enable;
DEBUG(status,out).print("%s from the API; persisting\n",
return; is_enabled ? "enabled" : "disabled");
} set_config_bool(CONFIG_IS_ENABLED, is_enabled);
} else {
void AnimalHospital::processPatients(color_ostream &out) { DEBUG(status,out).print("%s from the API, but already %s; no action\n",
// Where the magic happens is_enabled ? "enabled" : "disabled",
for (Patient *patient : this->accepted_patients) { is_enabled ? "enabled" : "disabled");
int id = patient->getID();
df::unit *real_unit = df::unit::find(id);
// Check to make sure the unit hasn't expired before assigning a job, or if they've been healed
if (!real_unit || !Units::isActive(real_unit) || !real_unit->health->flags.bits.needs_healthcare) {
// discharge the patient from the hospital
this->dischargePatient(patient, out);
return;
}
// Give the unit a job if they don't have any
if (!real_unit->job.current_job) {
// Create REST struct
df::job * job = new df::job;
DFHack::Job::linkIntoWorld(job);
job->pos.x = patient->returnX();
job->pos.y = patient->returnY();
job->pos.z = patient->returnZ();
job->flags.bits.special = 1;
job->job_type = df::enums::job_type::Rest;
df::general_ref *ref = df::allocate<df::general_ref_unit_workerst>();
ref->setID(real_unit->id);
job->general_refs.push_back(ref);
real_unit->job.current_job = job;
job->wait_timer = 1600;
out.print("Telling intelligent unit %d to report to the hospital!\n", real_unit->id);
}
} }
return CR_OK;
} }
DFhackCExport command_result plugin_load_data(color_ostream &out) {
cycle_timestamp = 0;
config = World::GetPersistentData(CONFIG_KEY);
static vector<AnimalHospital*> animal_hospital_zones; if (!config.isValid()) {
DEBUG(status,out).print("no config found in this save; initializing\n");
void delete_animal_hospital_vector(color_ostream &out) { config = World::AddPersistentData(CONFIG_KEY);
if (dwarfvet_enabled) { set_config_bool(CONFIG_IS_ENABLED, is_enabled);
out.print("Clearing all animal hospitals\n");
}
for (AnimalHospital *animal_hospital : animal_hospital_zones) {
delete animal_hospital;
} }
animal_hospital_zones.clear();
}
command_result dwarfvet(color_ostream &out, std::vector <std::string> & parameters);
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand(
"dwarfvet",
"Allows animals to be treated at animal hospitals",
dwarfvet));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out ) is_enabled = get_config_bool(CONFIG_IS_ENABLED);
{ DEBUG(status,out).print("loading persisted enabled state: %s\n",
is_enabled ? "true" : "false");
return CR_OK; return CR_OK;
} }
bool isActiveAnimalHospital(df::building * building) { DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (Buildings::isHospital(building) && Buildings::isAnimalTraining(building) && Buildings::isActive(building)) { if (event == DFHack::SC_WORLD_UNLOADED) {
return true; if (is_enabled) {
DEBUG(status,out).print("world unloaded; disabling %s\n",
plugin_name);
is_enabled = false;
}
} }
return CR_OK;
return false;
} }
bool compareAnimalHospitalZones(df::building * hospital1, df::building * hospital2) { DFhackCExport command_result plugin_onupdate(color_ostream &out) {
// We compare hospitals by checking if positions are identical, not by ID if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS)
// since activity zones can easily be changed in size dwarfvet_cycle(out);
return CR_OK;
if ( hospital1->x1 == hospital2->x1 &&
hospital1->x2 == hospital2->x2 &&
hospital1->y1 == hospital2->y1 &&
hospital1->y2 == hospital2->y2 &&
hospital1->z == hospital2->z) {
return true;
}
return false;
} }
void tickHandler(color_ostream& out, void* data) { static bool call_dwarfvet_lua(color_ostream *out, const char *fn_name,
if ( !dwarfvet_enabled ) int nargs = 0, int nres = 0,
return; Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
CoreSuspender suspend; Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
int32_t own_race_id = df::global::plotinfo->race_id; DEBUG(status).print("calling dwarfvet lua function: '%s'\n", fn_name);
/**
* Generate a list of animal hospitals on the map
*
* Since activity zones can change any instant given user interaction
* we need to be constantly on the lookout for changed zones, and update
* our cached list on the fly if necessary.
**/
vector<df::building*> hospitals_on_map;
// Because C++ iterators suck, we're going to build a temporary vector with the AHZ, and then
// copy it for my own bloody sanity (and compilance with the STL spec)
vector<AnimalHospital*> ahz_scratch;
// Holding area for things to be added to the scratch
vector<df::building*> to_be_added;
// Walk the building tree, and generate a list of animal hospitals on the map
for (df::building* building : df::building::get_vector()) {
if (isActiveAnimalHospital(building)) {
hospitals_on_map.push_back(building);
}
}
int count_of_hospitals = hospitals_on_map.size();
int hospitals_cached = animal_hospital_zones.size();
//out.print ("count_of_Hospitals: %d, hospitals_cached: %d\n", count_of_hospitals, hospitals_cached);
// It's possible our hospital cache is empty, if so, simply copy it, and jump to the main logic
if (!hospitals_cached && count_of_hospitals) {
out.print("Populating hospital cache:\n");
for (df::building *current_hospital : hospitals_on_map) {
AnimalHospital * hospital = new AnimalHospital(current_hospital, out);
out.print(" Found animal hospital %d at x1: %d, y1: %d, z: %d from valid hospital list\n",
hospital->getID(),
current_hospital->x1,
current_hospital->y1,
current_hospital->z
);
animal_hospital_zones.push_back(hospital);
}
goto processUnits;
}
if (!count_of_hospitals && !hospitals_cached) {
// No hospitals found, cache is empty, just return
goto cleanup;
}
// Now walk our list of known hospitals, do a bit of checking, then compare
// TODO: this doesn't handle zone resizes at all
for (AnimalHospital *animal_hospital : animal_hospital_zones) {
// If a zone is changed at all, DF seems to reallocate it.
//
// Each AnimalHospital has a "to_be_deleted" bool. We're going to set that to true, and clear it if we can't
// find a matching hospital. This limits the number of times we need to walk through the AHZ list to twice, and
// lets us cleanly report it later
//
// Surviving hospitals will be copied to scratch which will become the new AHZ vector
animal_hospital->to_be_deleted = true;
for (df::building *current_hospital : hospitals_on_map) {
/* Keep the hospital if its still valid */
if (animal_hospital->getID() == current_hospital->id) {
ahz_scratch.push_back(animal_hospital);
animal_hospital->to_be_deleted = false;
break;
}
}
}
// Report what we're deleting by checking the to_be_deleted bool.
//
// Whatsever left is added to the pending add list
for (AnimalHospital *animal_hospital : animal_hospital_zones) {
if (animal_hospital->to_be_deleted) {
out.print("Hospital #%d removed\n", animal_hospital->getID());
delete animal_hospital;
}
}
/* Now we need to walk the scratch and add anything that is a hospital and wasn't in the vector */ CoreSuspender guard;
for (df::building *current_hospital : hospitals_on_map) { auto L = Lua::Core::State;
bool new_hospital = true; Lua::StackUnwinder top(L);
for (AnimalHospital *animal_hospital : ahz_scratch) {
if (animal_hospital->getID() == current_hospital->id) {
// Next if we're already here
new_hospital = false;
break;
}
}
// Add it if its new
if (new_hospital == true) to_be_added.push_back(current_hospital);
}
/* Now add it to the scratch AHZ */
for (df::building *current_hospital : to_be_added) {
// Add it to the vector
out.print("Adding new hospital #id: %d at x1 %d y1: %d z: %d\n",
current_hospital->id,
current_hospital->x1,
current_hospital->y1,
current_hospital->z
);
AnimalHospital * hospital = new AnimalHospital(current_hospital, out);
ahz_scratch.push_back(hospital);
}
/* Copy the scratch to the AHZ */ if (!out)
animal_hospital_zones = ahz_scratch; out = &Core::getInstance().getConsole();
// We always recheck the cache instead of counts because someone might have removed then added a hospital return Lua::CallLuaModuleFunction(*out, L, "plugins.dwarfvet", fn_name,
/* if (hospitals_cached != count_of_hospitals) { nargs, nres,
out.print("Hospitals on the map changed, rebuilding cache\n"); std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
for (vector<df::building*>::iterator current_hospital = hospitals_on_map.begin(); current_hospital != hospitals_on_map.end(); current_hospital++) {
bool add_hospital = true;
for (vector<AnimalHospital*>::iterator map_hospital = animal_hospital_zones.begin(); map_hospital != animal_hospital_zones.end(); map_hospital++) {
if (compareAnimalHospitalZones(*map_hospital, *current_hospital)) {
// Same hospital, we're good
add_hospital = false;
break;
}
}
// Add it to the list
if (add_hospital) {
out.print("Adding zone at x1: %d, y1: %d to valid hospital list\n", (*current_hospital)->x1, (*current_hospital)->y1);
animal_hospital_zones.push_back(*current_hospital);
}
}
}
*/
processUnits:
/* Code borrowed from petcapRemover.cpp */
for (df::unit *unit : df::unit::get_vector()) {
/* As hilarious as it would be, lets not treat FB :) */
if ( !Units::isActive(unit) || unit->flags1.bits.active_invader || unit->flags2.bits.underworld || unit->flags2.bits.visitor_uninvited || unit->flags2.bits.visitor ) {
continue;
}
if ( !Units::isTamable(unit)) {
continue;
}
/**
* So, for a unit to be elligable for the hospital, all the following must be true
*
* 1. It must be a member of our civilization
* 2. It must be tame (semi-wild counts for this)
* 2.1 If its not a dwarf, AND untame clear its civ out so traps work
* 3. It must have a health struct (which is generated by combat)
* 4. health->needs_healthcare must be set to true
* 5. If health->requires_recovery is set, the creature can't move under its own power
* and a Recover Wounded or Pen/Pasture job MUST be created by hand - TODO
* 6. An open spot in the "Animal Hospital" (activity zone with hospital+animal training set)
* must be available
*
* I apologize if this excessively verbose, but the healthcare system is stupidly conplex
* and there's tons of edgecases to watch out for, and I want someone else to ACTUALLY
* beside me able to understand what's going on
*/
// 1. Make sure its our own civ
if (!Units::isOwnCiv(unit)) {
continue;
}
// 2. Check for tameness
if (unit->training_level == df::animal_training_level::WildUntamed) {
// We don't IMMEDIATELY continue here, if the unit is
// part of our civ, it indiciates it WAS tamed, and reverted
// from SemiWild. Clear its civ flag so it looses TRAPAVOID
//
// Unfortunately, dwarves (or whatever is CIV_SELECTABLE)
// also have a default taming level of WildUntamed so
// check for this case
//
// Furthermore, it MIGHT be a werebeast, so check THAT too
// and exclude those as well.
//
// Finally, this breaks makeown. I might need to write a patch
// to set the tameness of "makeowned" units so dwarfvet can notice
// it
if (unit->race == own_race_id || unit->enemy.normal_race == own_race_id) {
continue;
} else {
unit->civ_id = -1;
out.print ("Clearing civ on unit: %d", unit->id);
}
}
// 3. Check for health struct
if (!unit->health) {
// Unit has not been injured ever; health struct MIA
continue;
}
// 4. Check the healthcare flags
if (unit->health->flags.bits.needs_healthcare) {
/**
* So, for dwarves to care for a unit it must be resting in
* in a hospital zone. Since non-dwarves never take jobs
* this why animal healthcare doesn't work for animals despite
* animal caretaker being coded in DF itself
*
* How a unit gets there is dependent on several factors. If
* a unit can move under its own power, it will take the rest
* job, with a position of a bed in the hospital, then move
* into that bed and fall asleep. This triggers a doctor to
* treat the unit.
*
* If a unit *can't* move, it will set needs_recovery, which
* creates a "Recover Wounded" job in the job list, and then
* create the "Rest" job as listed above. Another dwarf with
* the right labors will go recover the unit, then the above
* logic kicks off.
*
* The necessary flags seem to be properly set for all units
* on the map, so in theory, we just need to make the jobs and
* we're in business, but from a realism POV, I don't think
* non-sentient animals would be smart enough to go to the
* hospital on their own, so instead, we're going to do the following
*
* If a unit CAN_THINK, and can move let it act like a dwarf,
* it will try and find an open spot in the hospital, and if so,
* go there to be treated. In vanilla, the only tamable animal
* with CAN_THINK are Gremlins, so this is actually an edge case
* but its the easiest to code.
*
* TODO: figure out exact logic for non-thinking critters.
*/
// Now we need to find if this unit can be accepted at a hospital
bool awareOfUnit = tracked_units.count(unit->id);
// New unit for dwarfvet to be aware of!
if (!awareOfUnit) {
// The master list handles all patients which are accepted
// Check if this is a unit we're already aware of
for (auto animal_hospital : animal_hospital_zones) {
if (animal_hospital->acceptPatient(unit->id, out)) {
out.print("Accepted patient %d at hospital %d\n", unit->id, animal_hospital->getID());
tracked_units.insert(unit->id);
break;
}
}
}
}
}
// The final step, process all patients!
for (AnimalHospital *animal_hospital : animal_hospital_zones) {
animal_hospital->calculateHospital(true, out);
animal_hospital->processPatients(out);
}
cleanup:
EventManager::unregisterAll(plugin_self);
EventManager::EventHandler handle(tickHandler, howOften);
EventManager::registerTick(handle, howOften, plugin_self);
} }
command_result dwarfvet (color_ostream &out, std::vector <std::string> & parameters) static command_result do_command(color_ostream &out, vector<string> &parameters) {
{
CoreSuspender suspend; CoreSuspender suspend;
for ( size_t a = 0; a < parameters.size(); a++ ) { if (!Core::getInstance().isWorldLoaded()) {
if ( parameters[a] == "enable" ) { out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
out.print("dwarfvet enabled!\n"); return CR_FAILURE;
dwarfvet_enabled = true;
}
if ( parameters[a] == "disable") {
out.print("dwarvet disabled!\n");
dwarfvet_enabled = false;
}
if ( parameters[a] == "report") {
out.print("Current animal hospitals are:\n");
for (df::building *building : df::building::get_vector()) {
if (isActiveAnimalHospital(building)) {
out.print(" at x1: %d, x2: %d, y1: %d, y2: %d, z: %d\n", building->x1, building->x2, building->y1, building->y2, building->z);
}
}
return CR_OK;
}
if ( parameters[a] == "report-usage") {
out.print("Current animal hospitals are:\n");
for (AnimalHospital *animal_hospital : animal_hospital_zones) {
animal_hospital->calculateHospital(true, out);
animal_hospital->reportUsage(out);
}
return CR_OK;
}
} }
if ( !dwarfvet_enabled ) { bool show_help = false;
return CR_OK; if (!call_dwarfvet_lua(&out, "parse_commandline", 1, 1,
[&](lua_State *L) {
Lua::PushVector(L, parameters);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
} }
EventManager::unregisterAll(plugin_self); return show_help ? CR_WRONG_USAGE : CR_OK;
EventManager::EventHandler handle(tickHandler, howOften);
EventManager::registerTick(handle, howOften, plugin_self);
return CR_OK;
} }
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) static void dwarfvet_cycle(color_ostream &out) {
{ // mark that we have recently run
if (enable && !dwarfvet_enabled) { cycle_timestamp = world->frame_counter;
dwarfvet_enabled = true;
}
else if (!enable && dwarfvet_enabled) {
delete_animal_hospital_vector(out);
dwarfvet_enabled = false;
}
return CR_OK; DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
call_dwarfvet_lua(&out, "checkup");
} }
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) DFHACK_PLUGIN_LUA_FUNCTIONS {
{ DFHACK_LUA_FUNCTION(dwarfvet_cycle),
switch (event) DFHACK_LUA_END
{ };
case DFHack::SC_MAP_LOADED:
break;
case DFHack::SC_MAP_UNLOADED:
delete_animal_hospital_vector(out);
dwarfvet_enabled = false;
break;
default:
break;
}
return CR_OK;
}

@ -0,0 +1,180 @@
local _ENV = mkmodule('plugins.dwarfvet')
local argparse = require('argparse')
local utils = require('utils')
local function is_valid_animal(unit)
return unit and
dfhack.units.isActive(unit) and
dfhack.units.isAnimal(unit) and
dfhack.units.isFortControlled(unit) and
dfhack.units.isTame(unit) and
not dfhack.units.isDead(unit)
end
local function get_cur_patients()
local cur_patients = {}
for _,job in utils.listpairs(df.global.world.jobs.list) do
if job.job_type ~= df.job_type.Rest then goto continue end
local unit = dfhack.job.getWorker(job)
print(unit.id)
if is_valid_animal(unit) then
cur_patients[unit] = true
end
::continue::
end
return cur_patients
end
local function get_new_patients(cur_patients)
cur_patients = cur_patients or get_cur_patients()
local new_patients = {}
for _,unit in ipairs(df.global.world.units.active) do
if unit.job.current_job then goto continue end
if cur_patients[unit] or not is_valid_animal(unit) then goto continue end
if not unit.health or not unit.health.flags.needs_healthcare then goto continue end
table.insert(new_patients, unit)
::continue::
end
return new_patients
end
local function print_status()
print(('dwarfvet is %srunning'):format(isEnabled() and '' or 'not '))
print()
print('The following animals are receiving treatment:')
local cur_patients = get_cur_patients()
if not next(cur_patients) then
print(' None')
else
for unit in pairs(cur_patients) do
print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id))
end
end
print()
print('The following animals are injured and need treatment:')
local new_patients = get_new_patients(cur_patients)
if #new_patients == 0 then
print(' None')
else
for _,unit in ipairs(new_patients) do
print((' %s (%d)'):format(dfhack.units.getReadableName(unit), unit.id))
end
end
end
HospitalZone = defclass(HospitalZone)
HospitalZone.ATTRS{
zone=DEFAULT_NIL,
}
local ONE_TILE = xy2pos(1, 1)
function HospitalZone:find_spot(unit_pos)
self.x = self.x or self.zone.x1
self.y = self.y or self.zone.y1
local zone = self.zone
for y=self.y,zone.y2 do
for x=self.x,zone.x2 do
local pos = xyz2pos(x, y, zone.z)
if dfhack.maps.canWalkBetween(unit_pos, pos) and
dfhack.buildings.containsTile(zone, x, y) and
dfhack.buildings.checkFreeTiles(pos, ONE_TILE)
then
return pos
end
end
end
end
-- TODO: If health.requires_recovery is set, the creature can't move under its own power
-- and a Recover Wounded or Pen/Pasture job must be created by hand
function HospitalZone:assign_spot(unit, unit_pos)
local pos = self:find_spot(unit_pos)
if not pos then return false end
local job = df.new(df.job)
dfhack.job.linkIntoWorld(job)
job.pos.x = pos.x
job.pos.y = pos.y
job.pos.z = pos.z
job.flags.special = true
job.job_type = df.job_type.Rest
job.wait_timer = 1600
local gref = df.new(df.general_ref_unit_workerst)
gref.unit_id = unit.id
job.general_refs:insert('#', gref)
unit.job.current_job = job
return true
end
local function get_hospital_zones()
local hospital_zones = {}
local site = df.global.world.world_data.active_site[0]
for _,location in ipairs(site.buildings) do
if not df.abstract_building_hospitalst:is_instance(location) then goto continue end
for _,bld_id in ipairs(location.contents.building_ids) do
local zone = df.building.find(bld_id)
if zone then
table.insert(hospital_zones, HospitalZone{zone=zone})
end
end
::continue::
end
return hospital_zones
end
local function distance(zone, pos)
return math.abs(zone.x1 - pos.x) + math.abs(zone.y1 - pos.y) + 50*math.abs(zone.z - pos.z)
end
function checkup()
local new_patients = get_new_patients()
if #new_patients == 0 then return end
local hospital_zones = get_hospital_zones()
local assigned = 0
for _,unit in ipairs(new_patients) do
local unit_pos = xyz2pos(dfhack.units.getPosition(unit))
table.sort(hospital_zones,
function(a, b) return distance(a.zone, unit_pos) < distance(b.zone, unit_pos) end)
for _,hospital_zone in ipairs(hospital_zones) do
if hospital_zone:assign_spot(unit, unit_pos) then
assigned = assigned + 1
break
end
end
end
print(('dwarfvet scheduled treatment for %d of %d injured animals'):format(assigned, #new_patients))
end
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
})
end
function parse_commandline(args)
local opts = {}
local positionals = process_args(opts, args)
if opts.help or not positionals then
return false
end
local command = positionals[1]
if not command or command == 'status' then
print_status()
elseif command == 'now' then
dwarfvet_cycle()
else
return false
end
return true
end
return _ENV

@ -422,29 +422,6 @@ local function make_choice_text(data)
} }
end end
local function get_unit_description(unit, raw)
local race = dfhack.units.isChild(unit) and raw.general_child_name[0] or raw.caste[unit.caste].caste_name[0]
local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit))
if name and #name > 0 then
name = ('%s, %s'):format(name, race)
else
name = race
end
if #unit.syndromes.active > 0 then
for _, unit_syndrome in ipairs(unit.syndromes.active) do
local syndrome = df.syndrome.find(unit_syndrome.type)
if not syndrome then goto continue end
for _, effect in ipairs(syndrome.ce) do
if df.creature_interaction_effect_display_namest:is_instance(effect) then
return name .. ' ' .. effect.name
end
end
::continue::
end
end
return name
end
local function get_cage_ref(unit) local function get_cage_ref(unit)
return dfhack.units.getGeneralRef(unit, df.general_ref_type.CONTAINED_IN_ITEM) return dfhack.units.getGeneralRef(unit, df.general_ref_type.CONTAINED_IN_ITEM)
end end
@ -518,9 +495,9 @@ function Pasture:cache_choices()
local raw = df.creature_raw.find(unit.race) local raw = df.creature_raw.find(unit.race)
local data = { local data = {
unit=unit, unit=unit,
desc=get_unit_description(unit, raw), desc=dfhack.units.getReadableName(unit),
gender=unit.sex, gender=unit.sex,
race=raw.creature_id, race=raw and raw.creature_id or -1,
status=get_status(unit), status=get_status(unit),
disposition=get_disposition(unit), disposition=get_disposition(unit),
egg=dfhack.units.isEggLayerRace(unit), egg=dfhack.units.isEggLayerRace(unit),