diff --git a/docs/changelog.txt b/docs/changelog.txt index 97bc202fe..9c5d04a25 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -36,6 +36,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## 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 - `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 ## Fixes - Fix extra keys appearing in DFHack text boxes when shift (or any other modifier) is released before the other key you were pressing diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 7aea41c55..cd35d9465 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1589,6 +1589,11 @@ Units module Currently only one dream per unit is supported by 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)`` Returns a number from 0-6 indicating stress. 0 is most stressed; 6 is least. diff --git a/docs/plugins/dwarfvet.rst b/docs/plugins/dwarfvet.rst index b4dfe0ada..70f92cc79 100644 --- a/docs/plugins/dwarfvet.rst +++ b/docs/plugins/dwarfvet.rst @@ -2,22 +2,19 @@ dwarfvet ======== .. dfhack-tool:: - :summary: Allows animals to be treated at animal hospitals. - :tags: unavailable fort gameplay animals + :summary: Allow animals to be treated at hospitals. + :tags: fort gameplay animals 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 -a hospital that is also an animal training zone. Dwarfs with the Animal -Caretaker labor enabled will come to the hospital to treat animals. Normal +dwarfvet, injured animals will be treated at a hospital. Dwarfs with the Animal +Caretaker labor enabled will come to the hospital to treat the animals. Normal medical skills are used (and trained), but no experience is given to the Animal Caretaker skill itself. Usage ----- -``enable dwarfvet`` - Enables the plugin. -``dwarfvet report`` - Reports all zones that the game considers animal hospitals. -``dwarfvet report-usage`` - Reports on animals currently being treated. +:: + + enable dwarfvet + dwarfvet [status] diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 050c99b86..c19c10197 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1824,6 +1824,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildNameById), + WRAPM(Units, getReadableName), WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialEvent), WRAPM(Units, getStressCategory), diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 23b4647b2..19af4ec9a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -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 getRaceChildNameById(int32_t race_id); 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 int getKillCount(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index f517a07b5..1605a3d06 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -53,6 +53,7 @@ using namespace std; #include "df/activity_entry.h" #include "df/burrow.h" #include "df/caste_raw.h" +#include "df/creature_interaction_effect_display_namest.h" #include "df/creature_raw.h" #include "df/curse_attr_change.h" #include "df/entity_position.h" @@ -74,11 +75,13 @@ using namespace std; #include "df/nemesis_record.h" #include "df/tile_occupancy.h" #include "df/plotinfost.h" +#include "df/syndrome.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" #include "df/unit_relationship_type.h" #include "df/unit_skill.h" #include "df/unit_soul.h" +#include "df/unit_syndrome.h" #include "df/unit_wound.h" #include "df/world.h" #include "df/world_data.h" @@ -1218,6 +1221,41 @@ string Units::getRaceChildName(df::unit* unit) 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(effect); + if (!cie) + continue; + name += " "; + name += cie->name; + break; + } + } + return name; +} double Units::getAge(df::unit *unit, bool true_age) { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c037fc4a0..345263f1b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -106,7 +106,7 @@ dfhack_plugin(dig dig.cpp) dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) #dfhack_plugin(digFlood digFlood.cpp) #add_subdirectory(diggingInvaders) -#dfhack_plugin(dwarfvet dwarfvet.cpp) +dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) #dfhack_plugin(embark-tools embark-tools.cpp) diff --git a/plugins/dwarfvet.cpp b/plugins/dwarfvet.cpp index 75f015975..dcd8e0f8c 100644 --- a/plugins/dwarfvet.cpp +++ b/plugins/dwarfvet.cpp @@ -20,735 +20,170 @@ * THE SOFTWARE. **/ -#include "Console.h" -#include "Core.h" -#include "DataDefs.h" -#include "Export.h" +#include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" -#include "modules/EventManager.h" + +#include "modules/Persistence.h" #include "modules/Units.h" -#include "modules/Buildings.h" -#include "modules/Maps.h" -#include "modules/Job.h" +#include "modules/World.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 -#include -#include - using namespace DFHack; -using namespace DFHack::Units; -using namespace DFHack::Buildings; - -using namespace std; +using std::string; +using std::vector; DFHACK_PLUGIN("dwarfvet"); -DFHACK_PLUGIN_IS_ENABLED(dwarfvet_enabled); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(world); -static unordered_set tracked_units; -static int32_t howOften = 100; - -struct hospital_spot { - int32_t x; - 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; +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(dwarfvet, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(dwarfvet, cycle, DebugCategory::LINFO); } -class AnimalHospital { - - public: - // Constructor - 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 spots_in_use; - vector building_in_hospital_notification; /* If present, we already notified about this */ - vector accepted_patients; +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static PersistentDataItem config; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, }; - -AnimalHospital::AnimalHospital(df::building * building, color_ostream &out) { - // Copy in what we need to know - id = building->id; - 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 int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); } - -AnimalHospital::~AnimalHospital() { - // Go through and delete all the patients - for (Patient* accepted_patient : this->accepted_patients) { - delete accepted_patient; - } +static bool get_config_bool(int index) { + return get_config_val(index) == 1; } - -bool AnimalHospital::acceptPatient(int32_t id, color_ostream &out) { - // This function determines if we can accept a patient, and if we will. - 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::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_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; } - -// Before any use of the hospital, we need to make calculate open spots -// 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"); - +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); } -void AnimalHospital::calculateHospital(bool force, color_ostream &out) { - // Only calculate out the hospital if we actually have a patient in it - // (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; - //} +static const int32_t CYCLE_TICKS = 2459; // a prime number that's around 2 days +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - // Calculate out the total area of the hospital - // This can change if a hospital has been resized - 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; - } - } - } +static command_result do_command(color_ostream &out, vector ¶meters); +static void dwarfvet_cycle(color_ostream &out); +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { + commands.push_back(PluginCommand( + plugin_name, + "Allow animals to be treated at hospitals.", + do_command)); + return CR_OK; } -// Self explanatory -void AnimalHospital::dischargePatient(Patient * patient, color_ostream &out) { - int32_t id = patient->getID(); - - // Remove them from the hospital - - // We can safely iterate here because once we delete the unit - // we no longer use the iterator - for (vector::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; - } +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - // And the master list - tracked_units.erase(id); - - return; -} - -void AnimalHospital::processPatients(color_ostream &out) { - // Where the magic happens - for (Patient *patient : this->accepted_patients) { - 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(); - 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); - } + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); } + return CR_OK; } +DFhackCExport command_result plugin_load_data(color_ostream &out) { + cycle_timestamp = 0; + config = World::GetPersistentData(CONFIG_KEY); -static vector animal_hospital_zones; - -void delete_animal_hospital_vector(color_ostream &out) { - if (dwarfvet_enabled) { - out.print("Clearing all animal hospitals\n"); - } - for (AnimalHospital *animal_hospital : animal_hospital_zones) { - delete animal_hospital; + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); } - animal_hospital_zones.clear(); -} - -command_result dwarfvet(color_ostream &out, std::vector & parameters); - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &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; } -bool isActiveAnimalHospital(df::building * building) { - if (Buildings::isHospital(building) && Buildings::isAnimalTraining(building) && Buildings::isActive(building)) { - return true; +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(status,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } } - - return false; + return CR_OK; } -bool compareAnimalHospitalZones(df::building * hospital1, df::building * hospital2) { - // We compare hospitals by checking if positions are identical, not by ID - // since activity zones can easily be changed in size - - 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; +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + dwarfvet_cycle(out); + return CR_OK; } -void tickHandler(color_ostream& out, void* data) { - if ( !dwarfvet_enabled ) - return; - CoreSuspender suspend; - int32_t own_race_id = df::global::plotinfo->race_id; - - /** - * 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 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 ahz_scratch; - - // Holding area for things to be added to the scratch - vector 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; - } - } +static bool call_dwarfvet_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + DEBUG(status).print("calling dwarfvet lua function: '%s'\n", fn_name); - /* 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) { - bool new_hospital = true; - - 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); - } + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - /* Copy the scratch to the AHZ */ - animal_hospital_zones = ahz_scratch; + if (!out) + out = &Core::getInstance().getConsole(); - // We always recheck the cache instead of counts because someone might have removed then added a hospital -/* if (hospitals_cached != count_of_hospitals) { - out.print("Hospitals on the map changed, rebuilding cache\n"); - - for (vector::iterator current_hospital = hospitals_on_map.begin(); current_hospital != hospitals_on_map.end(); current_hospital++) { - bool add_hospital = true; - - for (vector::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); + return Lua::CallLuaModuleFunction(*out, L, "plugins.dwarfvet", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } -command_result dwarfvet (color_ostream &out, std::vector & parameters) -{ +static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; - for ( size_t a = 0; a < parameters.size(); a++ ) { - if ( parameters[a] == "enable" ) { - out.print("dwarfvet enabled!\n"); - 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 (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; } - if ( !dwarfvet_enabled ) { - return CR_OK; + bool show_help = false; + 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); - EventManager::EventHandler handle(tickHandler, howOften); - EventManager::registerTick(handle, howOften, plugin_self); - - return CR_OK; + return show_help ? CR_WRONG_USAGE : CR_OK; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if (enable && !dwarfvet_enabled) { - dwarfvet_enabled = true; - } - else if (!enable && dwarfvet_enabled) { - delete_animal_hospital_vector(out); - dwarfvet_enabled = false; - } +static void dwarfvet_cycle(color_ostream &out) { + // mark that we have recently run + cycle_timestamp = world->frame_counter; - 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) -{ - switch (event) - { - 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; -} +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(dwarfvet_cycle), + DFHACK_LUA_END +}; diff --git a/plugins/lua/dwarfvet.lua b/plugins/lua/dwarfvet.lua new file mode 100644 index 000000000..64538bac0 --- /dev/null +++ b/plugins/lua/dwarfvet.lua @@ -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 diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua index e8a943381..636f3938e 100644 --- a/plugins/lua/zone.lua +++ b/plugins/lua/zone.lua @@ -422,29 +422,6 @@ local function make_choice_text(data) } 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) return dfhack.units.getGeneralRef(unit, df.general_ref_type.CONTAINED_IN_ITEM) end @@ -518,9 +495,9 @@ function Pasture:cache_choices() local raw = df.creature_raw.find(unit.race) local data = { unit=unit, - desc=get_unit_description(unit, raw), + desc=dfhack.units.getReadableName(unit), gender=unit.sex, - race=raw.creature_id, + race=raw and raw.creature_id or -1, status=get_status(unit), disposition=get_disposition(unit), egg=dfhack.units.isEggLayerRace(unit),