|
| 1 | +#include "Debug.h" |
| 2 | +#include "PluginManager.h" |
| 3 | + |
| 4 | +#include "modules/Maps.h" |
| 5 | +#include "modules/Units.h" |
| 6 | +#include "modules/World.h" |
| 7 | + |
| 8 | +#include "df/unit.h" |
| 9 | +#include "df/world.h" |
| 10 | + |
| 11 | +#include <random> |
| 12 | + |
| 13 | +using namespace DFHack; |
| 14 | +using std::map; |
| 15 | +using std::string; |
| 16 | +using std::vector; |
| 17 | + |
| 18 | +DFHACK_PLUGIN("pet-uncapper"); |
| 19 | +DFHACK_PLUGIN_IS_ENABLED(is_enabled); |
| 20 | + |
| 21 | +REQUIRE_GLOBAL(world); |
| 22 | + |
| 23 | +namespace DFHack { |
| 24 | + // for configuration-related logging |
| 25 | + DBG_DECLARE(petuncapper, control, DebugCategory::LINFO); |
| 26 | + // for logging during the periodic scan |
| 27 | + DBG_DECLARE(petuncapper, cycle, DebugCategory::LINFO); |
| 28 | +} |
| 29 | + |
| 30 | +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle |
| 31 | + |
| 32 | +static const string CONFIG_KEY = string(plugin_name) + "/config"; |
| 33 | +static PersistentDataItem config; |
| 34 | + |
| 35 | +enum ConfigValues { |
| 36 | + CONFIG_IS_ENABLED = 0, |
| 37 | + CONFIG_FREQ = 1, |
| 38 | + CONFIG_POP_CAP = 2, |
| 39 | + CONFIG_PREG_TIME = 3, |
| 40 | +}; |
| 41 | + |
| 42 | +bool impregnate(df::unit* female, df::unit* male) { |
| 43 | + if (!female || !male) |
| 44 | + return false; |
| 45 | + if (female->pregnancy_genes) |
| 46 | + return false; |
| 47 | + |
| 48 | + df::unit_genes* preg = new df::unit_genes; |
| 49 | + *preg = male->appearance.genes; |
| 50 | + female->pregnancy_genes = preg; |
| 51 | + female->pregnancy_timer = config.get_int(CONFIG_PREG_TIME); |
| 52 | + female->pregnancy_caste = male->caste; |
| 53 | + return true; |
| 54 | +} |
| 55 | + |
| 56 | +void impregnateMany(color_ostream &out, bool verbose = false) { |
| 57 | + // mark that we have recently run |
| 58 | + cycle_timestamp = world->frame_counter; |
| 59 | + |
| 60 | + map<int32_t, vector<df::unit *>> males; |
| 61 | + map<int32_t, vector<df::unit *>> females; |
| 62 | + map<int32_t, int32_t> popcount; |
| 63 | + |
| 64 | + std::random_device seed; |
| 65 | + std::mt19937 gen{seed()}; |
| 66 | + |
| 67 | + const int popcap = config.get_int(CONFIG_POP_CAP); |
| 68 | + int pregnancies = 0; |
| 69 | + |
| 70 | + for (auto unit : world->units.active) { |
| 71 | + // not restricted to fort pets since merchant/wild animals can participate |
| 72 | + if (!Units::isActive(unit) || unit->flags1.bits.active_invader || unit->flags2.bits.underworld || unit->flags2.bits.visitor_uninvited || unit->flags2.bits.visitor) |
| 73 | + continue; |
| 74 | + popcount[unit->race]++; |
| 75 | + if (unit->pregnancy_genes) { |
| 76 | + // already pregnant -- if remaining time is less than the current setting, speed it up |
| 77 | + if ((int)unit->pregnancy_timer > config.get_int(CONFIG_PREG_TIME)) |
| 78 | + unit->pregnancy_timer = config.get_int(CONFIG_PREG_TIME); |
| 79 | + // for player convenience and population stability, count the fetus toward the population cap |
| 80 | + popcount[unit->race]++; |
| 81 | + continue; |
| 82 | + } |
| 83 | + if (unit->flags1.bits.caged) |
| 84 | + continue; |
| 85 | + // must have PET or PET_EXOTIC |
| 86 | + if (!Units::isTamable(unit)) |
| 87 | + continue; |
| 88 | + // check for adulthood |
| 89 | + if (Units::isBaby(unit) || Units::isChild(unit)) |
| 90 | + continue; |
| 91 | + if (Units::isMale(unit)) |
| 92 | + males[unit->race].push_back(unit); |
| 93 | + else if (Units::isFemale(unit)) |
| 94 | + females[unit->race].push_back(unit); |
| 95 | + } |
| 96 | + |
| 97 | + for (auto [race, femalesList] : females) { |
| 98 | + if (!males.contains(race)) |
| 99 | + continue; |
| 100 | + |
| 101 | + for (auto female : femalesList) { |
| 102 | + if (popcap > 0 && popcount[race] >= popcap) |
| 103 | + break; |
| 104 | + |
| 105 | + vector<df::unit *> compatibles; |
| 106 | + for (auto male : males[race]) { |
| 107 | + if (Maps::canWalkBetween(female->pos, male->pos) ) |
| 108 | + compatibles.push_back(male); |
| 109 | + } |
| 110 | + if (compatibles.empty()) |
| 111 | + continue; |
| 112 | + |
| 113 | + std::uniform_int_distribution<> dist{0, (int)compatibles.size() - 1}; |
| 114 | + if (impregnate(female, compatibles[dist(gen)])) { |
| 115 | + pregnancies++; |
| 116 | + popcount[race]++; |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + if (pregnancies || verbose) { |
| 122 | + INFO(cycle, out).print("%d pet pregnanc%s initiated\n", |
| 123 | + pregnancies, pregnancies == 1 ? "y" : "ies"); |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +command_result do_command(color_ostream &out, vector<string> & parameters) { |
| 128 | + CoreSuspender suspend; |
| 129 | + |
| 130 | + if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { |
| 131 | + out.printerr("Cannot run %s without a loaded fort.\n", plugin_name); |
| 132 | + return CR_FAILURE; |
| 133 | + } |
| 134 | + |
| 135 | + if (parameters.size() == 0 || parameters[0] == "status") { |
| 136 | + out.print("%s is %s\n\n", plugin_name, is_enabled ? "enabled" : "not enabled"); |
| 137 | + out.print("population cap per species: %d\n", config.get_int(CONFIG_POP_CAP)); |
| 138 | + out.print("updating pregnancies every %d ticks\n", config.get_int(CONFIG_FREQ)); |
| 139 | + out.print("pregancies last %d ticks\n", config.get_int(CONFIG_PREG_TIME)); |
| 140 | + } else if (parameters[0] == "now") { |
| 141 | + impregnateMany(out, true); |
| 142 | + } else { |
| 143 | + if (parameters.size() < 2) |
| 144 | + return CR_WRONG_USAGE; |
| 145 | + string command = parameters[0]; |
| 146 | + int val = std::max(0, string_to_int(parameters[1])); |
| 147 | + if (command == "cap") |
| 148 | + config.set_int(CONFIG_POP_CAP, val); |
| 149 | + else if (command == "every") |
| 150 | + config.set_int(CONFIG_FREQ, val); |
| 151 | + else if (command == "pregtime") |
| 152 | + config.set_int(CONFIG_PREG_TIME, val); |
| 153 | + else |
| 154 | + return CR_WRONG_USAGE; |
| 155 | + } |
| 156 | + |
| 157 | + return CR_OK; |
| 158 | +} |
| 159 | + |
| 160 | +DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand> &commands) { |
| 161 | + commands.push_back(PluginCommand( |
| 162 | + "pet-uncapper", |
| 163 | + "Modify the pet population cap.", |
| 164 | + do_command)); |
| 165 | + return CR_OK; |
| 166 | +} |
| 167 | + |
| 168 | +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { |
| 169 | + if (!Core::getInstance().isMapLoaded() || !World::IsSiteLoaded()) { |
| 170 | + out.printerr("Cannot enable %s without a loaded fort.\n", plugin_name); |
| 171 | + return CR_FAILURE; |
| 172 | + } |
| 173 | + |
| 174 | + if (enable != is_enabled) { |
| 175 | + is_enabled = enable; |
| 176 | + DEBUG(control,out).print("%s from the API; persisting\n", |
| 177 | + is_enabled ? "enabled" : "disabled"); |
| 178 | + config.set_bool(CONFIG_IS_ENABLED, is_enabled); |
| 179 | + if (enable) |
| 180 | + impregnateMany(out); |
| 181 | + } else { |
| 182 | + DEBUG(control,out).print("%s from the API, but already %s; no action\n", |
| 183 | + is_enabled ? "enabled" : "disabled", |
| 184 | + is_enabled ? "enabled" : "disabled"); |
| 185 | + } |
| 186 | + return CR_OK; |
| 187 | +} |
| 188 | + |
| 189 | +DFhackCExport command_result plugin_load_site_data (color_ostream &out) { |
| 190 | + cycle_timestamp = 0; |
| 191 | + |
| 192 | + config = World::GetPersistentSiteData(CONFIG_KEY); |
| 193 | + |
| 194 | + if (!config.isValid()) { |
| 195 | + DEBUG(control,out).print("no config found in this save; initializing\n"); |
| 196 | + config = World::AddPersistentSiteData(CONFIG_KEY); |
| 197 | + config.set_bool(CONFIG_IS_ENABLED, is_enabled); |
| 198 | + config.set_int(CONFIG_FREQ, 10000); |
| 199 | + config.set_int(CONFIG_POP_CAP, 100); |
| 200 | + config.set_int(CONFIG_PREG_TIME, 200000); |
| 201 | + } |
| 202 | + |
| 203 | + is_enabled = config.get_bool(CONFIG_IS_ENABLED); |
| 204 | + DEBUG(control,out).print("loading persisted enabled state: %s\n", |
| 205 | + is_enabled ? "true" : "false"); |
| 206 | + return CR_OK; |
| 207 | +} |
| 208 | + |
| 209 | +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { |
| 210 | + if (event == DFHack::SC_WORLD_UNLOADED) { |
| 211 | + if (is_enabled) { |
| 212 | + DEBUG(control,out).print("world unloaded; disabling %s\n", |
| 213 | + plugin_name); |
| 214 | + is_enabled = false; |
| 215 | + } |
| 216 | + } |
| 217 | + return CR_OK; |
| 218 | +} |
| 219 | + |
| 220 | +DFhackCExport command_result plugin_onupdate(color_ostream &out) { |
| 221 | + if (world->frame_counter - cycle_timestamp >= config.get_int(CONFIG_FREQ)) |
| 222 | + impregnateMany(out); |
| 223 | + return CR_OK; |
| 224 | +} |
0 commit comments