diff --git a/docs/changelog.txt b/docs/changelog.txt index 92d6aae4d..e8fc5725c 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 - `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 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..12d77fef8 100644 --- a/docs/plugins/dwarfvet.rst +++ b/docs/plugins/dwarfvet.rst @@ -2,22 +2,34 @@ 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. +You can enable ``dwarfvet`` in `gui/control-panel`, and you can choose to start +``dwarfvet`` automatically in new forts in the ``Autostart`` tab. + 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] + dwarfvet now + +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. 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),