diff --git a/docs/Lua API.rst b/docs/Lua API.rst index cfebdd792..89a318776 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1188,6 +1188,187 @@ Job module Units module ------------ +* ``dfhack.units.isUnitInBox(unit,x1,y1,z1,x2,y2,z2)`` + + The unit is within the specified coordinates. + +* ``dfhack.units.isActive(unit)`` + + The unit is active (alive and on the map). + +* ``dfhack.units.isVisible(unit)`` + + The unit is visible on the map. + +* ``dfhack.units.isCitizen(unit[,ignore_sanity])`` + + The unit is an alive sane citizen of the fortress; wraps the + same checks the game uses to decide game-over by extinction, + with an additional sanity check. You can identify citizens, + regardless of their sanity, by passing ``true`` as the optional + second parameter. + +* ``dfhack.units.isFortControlled(unit)`` + + Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks + for units hidden in ambush, and includes tame animals. Returns *false* + if not in fort mode. + +* ``dfhack.units.isOwnCiv(unit)`` + + The unit belongs to the player's civilization. + +* ``dfhack.units.isOwnGroup(unit)`` + + The unit belongs to the player's group. + +* ``dfhack.units.isOwnRace(unit)`` + + The unit belongs to the player's race. + +* ``dfhack.units.isAlive(unit)`` + + The unit isn't dead or undead. + +* ``dfhack.units.isDead(unit)`` + + The unit is completely dead and passive, or a ghost. Equivalent to + ``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``. + +* ``dfhack.units.isKilled(unit)`` + + The unit has been killed. + +* ``dfhack.units.isSane(unit)`` + + The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf. + +* ``dfhack.units.isCrazed`` + + The unit is berserk and will attack all other creatures except members of its own species + that are also crazed. (can be modified by curses) + +* ``dfhack.units.isGhost(unit)`` + + The unit is a ghost. + +* ``dfhack.units.isHidden(unit)`` + + The unit is hidden to the player, accounting for sneaking. Works for any game mode. + +* ``dfhack.units.isHidingCurse(unit)`` + + The unit is hiding a curse. + + +* ``dfhack.units.isMale(unit)`` +* ``dfhack.units.isFemale(unit)`` +* ``dfhack.units.isBaby(unit)`` +* ``dfhack.units.isChild(unit)`` +* ``dfhack.units.isAdult(unit)`` +* ``dfhack.units.isGay(unit)`` +* ``dfhack.units.isNaked(unit)`` + + Simple unit property checks + +* ``dfhack.units.isVisiting(unit)`` + + The unit is visiting. eg. Merchants, Diplomatics, travelers. + + +* ``dfhack.units.isTrainableHunting(unit)`` + + The unit is trainable for hunting. + +* ``dfhack.units.isTrainableWar(unit)`` + + The unit is trainable for war. + +* ``dfhack.units.isTrained(unit)`` + + The unit is trained. + +* ``dfhack.units.isHunter(unit)`` + + The unit is a trained hunter. + +* ``dfhack.units.isWar(unit)`` + + The unit is trained for war. + +* ``dfhack.units.isTame(unit)`` +* ``dfhack.units.isTamable(unit)`` +* ``dfhack.units.isDomesticated(unit)`` +* ``dfhack.units.isMarkedForSlaughter(unit)`` +* ``dfhack.units.isGelded(unit)`` +* ``dfhack.units.isEggLayer(unit)`` +* ``dfhack.units.isGrazer(unit)`` +* ``dfhack.units.isMilkable(unit)`` + + Simple unit property checks. + +* ``dfhack.units.isForest(unit)`` + + The unit is of the forest. + +* ``dfhack.units.isMischievous(unit)`` + + The unit is mischievous. + +* ``dfhack.units.isAvailableForAdoption(unit)`` + + The unit is available for adoption. + + +* ``dfhack.units.isOpposedToLife(unit)`` +* ``dfhack.units.hasExtravision(unit)`` +* ``dfhack.units.isBloodsucker(unit)`` + + Simple checks of caste attributes that can be modified by curses. + + +* ``dfhack.units.isDwarf(unit)`` + + The unit is of the correct race for the fortress. + +* ``dfhack.units.isAnimal(unit)`` +* ``dfhack.units.isMerchant(unit)`` +* ``dfhack.units.isDiplomat(unit)`` + + Simple unit type checks. + +* ``dfhack.units.isVisitor(unit)`` + + The unit is a regular visitor with no special purpose (eg. merchant). + +* ``dfhack.units.isInvader(unit)`` + + The unit is an active invader or marauder. + +* ``dfhack.units.isUndead(unit[,include_vamps])`` + + The unit is undead. Pass ``true`` as the optional second parameter to + count vampires as undead. + +* ``dfhack.units.isNightCreature(unit)`` +* ``dfhack.units.isSemiMegabeast(unit)`` +* ``dfhack.units.isMegabeast(unit)`` +* ``dfhack.units.isTitan(unit)`` +* ``dfhack.units.isDemon(unit)`` + + Simple enemy type checks. + +* ``dfhack.units.isDanger(unit)`` + + The unit is dangerous, and probably hostile. This includes + Great Dangers (see below), semi-megabeasts, night creatures, + undead, invaders, and crazed units. + +* ``dfhack.units.isGreatDanger(unit)`` + + The unit is of Great Danger. This include demons, titans, and megabeasts. + + * ``dfhack.units.getPosition(unit)`` Returns true *x,y,z* of the unit, or *nil* if invalid; may be not equal to unit.pos if caged. @@ -1238,72 +1419,15 @@ Units module Returns the nemesis record of the unit if it has one, or *nil*. -* ``dfhack.units.isHidingCurse(unit)`` - - Checks if the unit hides improved attributes from its curse. - * ``dfhack.units.getPhysicalAttrValue(unit, attr_type)`` * ``dfhack.units.getMentalAttrValue(unit, attr_type)`` Computes the effective attribute value, including curse effect. -* ``dfhack.units.isCrazed(unit)`` -* ``dfhack.units.isOpposedToLife(unit)`` -* ``dfhack.units.hasExtravision(unit)`` -* ``dfhack.units.isBloodsucker(unit)`` - - Simple checks of caste attributes that can be modified by curses. - * ``dfhack.units.getMiscTrait(unit, type[, create])`` Finds (or creates if requested) a misc trait object with the given id. -* ``dfhack.units.isActive(unit)`` - - The unit is active (alive and on the map). - -* ``dfhack.units.isAlive(unit)`` - - The unit isn't dead or undead. - -* ``dfhack.units.isDead(unit)`` - - The unit is completely dead and passive, or a ghost. Equivalent to - ``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``. - -* ``dfhack.units.isKilled(unit)`` - - The unit has been killed. - -* ``dfhack.units.isGhost(unit)`` - - The unit is a ghost. - -* ``dfhack.units.isSane(unit)`` - - The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf. - -* ``dfhack.units.isDwarf(unit)`` - - The unit is of the correct race of the fortress. - -* ``dfhack.units.isCitizen(unit)`` - - The unit is an alive sane citizen of the fortress; wraps the - same checks the game uses to decide game-over by extinction. - -* ``dfhack.units.isFortControlled(unit)`` - - Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks for units hidden in ambush, and includes tame animals. Returns *false* if not in fort mode. - -* ``dfhack.units.isVisible(unit)`` - - The unit is visible on the map. - -* ``dfhack.units.isHidden(unit)`` - - The unit is hidden to the player, accounting for sneaking. Works for any game mode. - * ``dfhack.units.getAge(unit[,true_age])`` Returns the age of the unit in years as a floating-point value. diff --git a/docs/changelog.txt b/docs/changelog.txt index 68b12dc77..f89849bf7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -77,6 +77,22 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Gui::anywhere_hotkey``: for plugin commands bound to keybindings that can be invoked on any screen - ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets - ``Lua::Push()``: now handles maps with otherwise supported keys and values +- Units module: added new checks + - ``isUnitInBox()`` + - ``isAnimal()`` + - ``isVisiting()`` any visiting unit (diplomat, merchant, visitor) + - ``isVisitor()`` ie. not merchants or diplomats + - ``isInvader()`` + - ``isDemon()`` returns true for unique/regular demons + - ``isTitan()`` + - ``isMegabeast()`` + - ``isGreatDanger()`` returns true if unit is a demon, titan, or megabeast + - ``isSemiMegabeast()`` + - ``isNightCreature()`` + - ``isDanger()`` returns true if is a 'GreatDanger', semi-megabeast, night creature, undead, or invader +- Units module: modifies existing checks + - ``isUndead(df::unit* unit)`` => ``isUndead(df::unit* unit, bool ignore_vamps = true)`` isUndead used to always ignore vamps, now it does it by default and includes them when false is passed + - ``isCitizen(df::unit* unit)`` => ``isCitizen(df::unit* unit, bool ignore_sanity = false)`` isCitizen used to always check sanity, now it does it by default and ignores sanity when true is passed - Constructions module: added ``insert()`` to insert constructions into the game's sorted list. - MiscUtils: moved the following string transformation functions from ``uicommon.h``: ``int_to_string``, ``ltrim``, ``rtrim``, and ``trim``; added ``string_to_int`` diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 9f713244d..ed59786a6 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1559,6 +1559,63 @@ static const luaL_Reg dfhack_job_funcs[] = { /***** Units module *****/ static const LuaWrapper::FunctionReg dfhack_units_module[] = { + WRAPM(Units, isUnitInBox), + WRAPM(Units, isActive), + WRAPM(Units, isVisible), + WRAPM(Units, isCitizen), + WRAPM(Units, isFortControlled), + WRAPM(Units, isOwnCiv), + WRAPM(Units, isOwnGroup), + WRAPM(Units, isOwnRace), + WRAPM(Units, isAlive), + WRAPM(Units, isDead), + WRAPM(Units, isKilled), + WRAPM(Units, isSane), + WRAPM(Units, isCrazed), + WRAPM(Units, isGhost), + WRAPM(Units, isHidden), + WRAPM(Units, isHidingCurse), + WRAPM(Units, isMale), + WRAPM(Units, isFemale), + WRAPM(Units, isBaby), + WRAPM(Units, isChild), + WRAPM(Units, isAdult), + WRAPM(Units, isGay), + WRAPM(Units, isNaked), + WRAPM(Units, isVisiting), + WRAPM(Units, isTrainableHunting), + WRAPM(Units, isTrainableWar), + WRAPM(Units, isTrained), + WRAPM(Units, isHunter), + WRAPM(Units, isWar), + WRAPM(Units, isTame), + WRAPM(Units, isTamable), + WRAPM(Units, isDomesticated), + WRAPM(Units, isMarkedForSlaughter), + WRAPM(Units, isGelded), + WRAPM(Units, isEggLayer), + WRAPM(Units, isGrazer), + WRAPM(Units, isMilkable), + WRAPM(Units, isForest), + WRAPM(Units, isMischievous), + WRAPM(Units, isAvailableForAdoption), + WRAPM(Units, hasExtravision), + WRAPM(Units, isOpposedToLife), + WRAPM(Units, isBloodsucker), + WRAPM(Units, isDwarf), + WRAPM(Units, isAnimal), + WRAPM(Units, isMerchant), + WRAPM(Units, isDiplomat), + WRAPM(Units, isVisitor), + WRAPM(Units, isInvader), + WRAPM(Units, isUndead), + WRAPM(Units, isNightCreature), + WRAPM(Units, isSemiMegabeast), + WRAPM(Units, isMegabeast), + WRAPM(Units, isTitan), + WRAPM(Units, isDemon), + WRAPM(Units, isDanger), + WRAPM(Units, isGreatDanger), WRAPM(Units, teleport), WRAPM(Units, getGeneralRef), WRAPM(Units, getSpecificRef), @@ -1567,23 +1624,9 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getVisibleName), WRAPM(Units, getIdentity), WRAPM(Units, getNemesis), - WRAPM(Units, isHidingCurse), WRAPM(Units, getPhysicalAttrValue), WRAPM(Units, getMentalAttrValue), - WRAPM(Units, isCrazed), - WRAPM(Units, isOpposedToLife), - WRAPM(Units, hasExtravision), - WRAPM(Units, isBloodsucker), - WRAPM(Units, isMischievous), WRAPM(Units, getMiscTrait), - WRAPM(Units, isDead), - WRAPM(Units, isAlive), - WRAPM(Units, isSane), - WRAPM(Units, isDwarf), - WRAPM(Units, isCitizen), - WRAPM(Units, isFortControlled), - WRAPM(Units, isVisible), - WRAPM(Units, isHidden), WRAPM(Units, getAge), WRAPM(Units, getKillCount), WRAPM(Units, getNominalSkill), @@ -1601,12 +1644,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getGoalName), WRAPM(Units, isGoalAchieved), WRAPM(Units, getSquadName), - WRAPM(Units, isWar), - WRAPM(Units, isHunter), - WRAPM(Units, isAvailableForAdoption), - WRAPM(Units, isOwnCiv), - WRAPM(Units, isOwnGroup), - WRAPM(Units, isOwnRace), WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), WRAPM(Units, getRaceNamePlural), @@ -1615,31 +1652,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildNameById), - WRAPM(Units, isBaby), - WRAPM(Units, isChild), - WRAPM(Units, isAdult), - WRAPM(Units, isEggLayer), - WRAPM(Units, isGrazer), - WRAPM(Units, isMilkable), - WRAPM(Units, isTrainableWar), - WRAPM(Units, isTrainableHunting), - WRAPM(Units, isTamable), - WRAPM(Units, isMale), - WRAPM(Units, isFemale), - WRAPM(Units, isMerchant), - WRAPM(Units, isDiplomat), - WRAPM(Units, isForest), - WRAPM(Units, isMarkedForSlaughter), - WRAPM(Units, isTame), - WRAPM(Units, isTrained), - WRAPM(Units, isGay), - WRAPM(Units, isNaked), - WRAPM(Units, isUndead), - WRAPM(Units, isGhost), - WRAPM(Units, isActive), - WRAPM(Units, isKilled), - WRAPM(Units, isGelded), - WRAPM(Units, isDomesticated), WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialEvent), WRAPM(Units, getStressCategory), @@ -1705,6 +1717,8 @@ static int units_getUnitsInBox(lua_State *state) { luaL_checktype(state, 7, LUA_TFUNCTION); units.erase(std::remove_if(units.begin(), units.end(), [&state](df::unit *unit) -> bool { + // todo: merging this filter into the base function would be welcomed by plugins + // (it would also be faster, and less obfuscated than this [ie. erase(remove_if)]) lua_dup(state); // copy function Lua::PushDFObject(state, unit); lua_call(state, 1, 1); diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 4dfa9f937..be630b802 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -68,6 +68,73 @@ static const int MAX_COLORS = 15; * The Units module - allows reading all non-vermin units and their properties */ +DFHACK_EXPORT bool isUnitInBox(df::unit* u, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2); + +DFHACK_EXPORT bool isActive(df::unit *unit); +DFHACK_EXPORT bool isVisible(df::unit* unit); +DFHACK_EXPORT bool isCitizen(df::unit *unit, bool ignore_sanity = false); +DFHACK_EXPORT bool isFortControlled(df::unit *unit); +DFHACK_EXPORT bool isOwnCiv(df::unit* unit); +DFHACK_EXPORT bool isOwnGroup(df::unit* unit); +DFHACK_EXPORT bool isOwnRace(df::unit* unit); + +DFHACK_EXPORT bool isAlive(df::unit *unit); +DFHACK_EXPORT bool isDead(df::unit *unit); +DFHACK_EXPORT bool isKilled(df::unit *unit); +DFHACK_EXPORT bool isSane(df::unit *unit); +DFHACK_EXPORT bool isCrazed(df::unit *unit); +DFHACK_EXPORT bool isGhost(df::unit *unit); +/// is unit hidden to the player? accounts for ambushing +DFHACK_EXPORT bool isHidden(df::unit *unit); +DFHACK_EXPORT bool isHidingCurse(df::unit *unit); + +DFHACK_EXPORT bool isMale(df::unit* unit); +DFHACK_EXPORT bool isFemale(df::unit* unit); +DFHACK_EXPORT bool isBaby(df::unit* unit); +DFHACK_EXPORT bool isChild(df::unit* unit); +DFHACK_EXPORT bool isAdult(df::unit* unit); +DFHACK_EXPORT bool isGay(df::unit* unit); +DFHACK_EXPORT bool isNaked(df::unit* unit); +DFHACK_EXPORT bool isVisiting(df::unit* unit); + +DFHACK_EXPORT bool isTrainableHunting(df::unit* unit); +DFHACK_EXPORT bool isTrainableWar(df::unit* unit); +DFHACK_EXPORT bool isTrained(df::unit* unit); +DFHACK_EXPORT bool isHunter(df::unit* unit); +DFHACK_EXPORT bool isWar(df::unit* unit); +DFHACK_EXPORT bool isTame(df::unit* unit); +DFHACK_EXPORT bool isTamable(df::unit* unit); +DFHACK_EXPORT bool isDomesticated(df::unit* unit); +DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); +DFHACK_EXPORT bool isGelded(df::unit* unit); +DFHACK_EXPORT bool isEggLayer(df::unit* unit); +DFHACK_EXPORT bool isGrazer(df::unit* unit); +DFHACK_EXPORT bool isMilkable(df::unit* unit); +DFHACK_EXPORT bool isForest(df::unit* unit); +DFHACK_EXPORT bool isMischievous(df::unit *unit); +DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit); + +DFHACK_EXPORT bool hasExtravision(df::unit *unit); +DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); +DFHACK_EXPORT bool isBloodsucker(df::unit *unit); + +DFHACK_EXPORT bool isDwarf(df::unit *unit); +DFHACK_EXPORT bool isAnimal(df::unit* unit); +DFHACK_EXPORT bool isMerchant(df::unit* unit); +DFHACK_EXPORT bool isDiplomat(df::unit* unit); +DFHACK_EXPORT bool isVisitor(df::unit* unit); +DFHACK_EXPORT bool isInvader(df::unit* unit); +DFHACK_EXPORT bool isUndead(df::unit* unit, bool include_vamps = false); +DFHACK_EXPORT bool isNightCreature(df::unit* unit); +DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit); +DFHACK_EXPORT bool isMegabeast(df::unit* unit); +DFHACK_EXPORT bool isTitan(df::unit* unit); +DFHACK_EXPORT bool isDemon(df::unit* unit); +DFHACK_EXPORT bool isDanger(df::unit* unit); +DFHACK_EXPORT bool isGreatDanger(df::unit* unit); + /* Read Functions */ // Read units in a box, starting with index. Returns -1 if no more units // found. Call repeatedly do get all units in a specified box (uses tile coords) @@ -99,35 +166,12 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit); DFHACK_EXPORT df::identity *getIdentity(df::unit *unit); DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit); -DFHACK_EXPORT bool isHidingCurse(df::unit *unit); DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr); DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr); DFHACK_EXPORT bool casteFlagSet(int race, int caste, df::caste_raw_flags flag); -DFHACK_EXPORT bool isCrazed(df::unit *unit); -DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); -DFHACK_EXPORT bool hasExtravision(df::unit *unit); -DFHACK_EXPORT bool isBloodsucker(df::unit *unit); -DFHACK_EXPORT bool isMischievous(df::unit *unit); - DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); -DFHACK_EXPORT bool isDead(df::unit *unit); -DFHACK_EXPORT bool isAlive(df::unit *unit); -DFHACK_EXPORT bool isSane(df::unit *unit); -DFHACK_EXPORT bool isCitizen(df::unit *unit); -DFHACK_EXPORT bool isFortControlled(df::unit *unit); -DFHACK_EXPORT bool isDwarf(df::unit *unit); -DFHACK_EXPORT bool isWar(df::unit* unit); -DFHACK_EXPORT bool isHunter(df::unit* unit); -DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit); -DFHACK_EXPORT bool isOwnCiv(df::unit* unit); -DFHACK_EXPORT bool isOwnGroup(df::unit* unit); -DFHACK_EXPORT bool isOwnRace(df::unit* unit); -DFHACK_EXPORT bool isVisible(df::unit* unit); -/// is unit hidden to the player? accounts for ambushing -DFHACK_EXPORT bool isHidden(df::unit *unit); - DFHACK_EXPORT std::string getRaceNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceName(df::unit* unit); DFHACK_EXPORT std::string getPhysicalDescription(df::unit* unit); @@ -138,31 +182,6 @@ 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 bool isBaby(df::unit* unit); -DFHACK_EXPORT bool isChild(df::unit* unit); -DFHACK_EXPORT bool isAdult(df::unit* unit); -DFHACK_EXPORT bool isEggLayer(df::unit* unit); -DFHACK_EXPORT bool isGrazer(df::unit* unit); -DFHACK_EXPORT bool isMilkable(df::unit* unit); -DFHACK_EXPORT bool isTrainableWar(df::unit* unit); -DFHACK_EXPORT bool isTrainableHunting(df::unit* unit); -DFHACK_EXPORT bool isTamable(df::unit* unit); -DFHACK_EXPORT bool isMale(df::unit* unit); -DFHACK_EXPORT bool isFemale(df::unit* unit); -DFHACK_EXPORT bool isMerchant(df::unit* unit); -DFHACK_EXPORT bool isDiplomat(df::unit* unit); -DFHACK_EXPORT bool isForest(df::unit* unit); -DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); -DFHACK_EXPORT bool isTame(df::unit* unit); -DFHACK_EXPORT bool isTrained(df::unit* unit); -DFHACK_EXPORT bool isGay(df::unit* unit); -DFHACK_EXPORT bool isNaked(df::unit* unit); -DFHACK_EXPORT bool isUndead(df::unit* unit); -DFHACK_EXPORT bool isGhost(df::unit *unit); -DFHACK_EXPORT bool isActive(df::unit *unit); -DFHACK_EXPORT bool isKilled(df::unit *unit); -DFHACK_EXPORT bool isGelded(df::unit* unit); -DFHACK_EXPORT bool isDomesticated(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 32b9f28e5..0395044ce 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -88,239 +88,233 @@ using df::global::ui; using df::global::gamemode; using df::global::gametype; -int32_t Units::getNumUnits() -{ - return world->units.all.size(); -} - -df::unit *Units::getUnit (const int32_t index) -{ - return vector_get(world->units.all, index); -} - -// returns index of creature actually read or -1 if no creature can be found -bool Units::getUnitsInBox (std::vector &units, - int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) -{ - if (!world) - return false; +bool Units::isUnitInBox(df::unit* u, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) { if (x1 > x2) swap(x1, x2); if (y1 > y2) swap(y1, y2); if (z1 > z2) swap(z1, z2); - - units.clear(); - for (df::unit *u : world->units.all) - { - if (u->pos.x >= x1 && u->pos.x <= x2) - { - if (u->pos.y >= y1 && u->pos.y <= y2) - { - if (u->pos.z >= z1 && u->pos.z <= z2) - { - units.push_back(u); - } + if (u->pos.x >= x1 && u->pos.x <= x2) { + if (u->pos.y >= y1 && u->pos.y <= y2) { + if (u->pos.z >= z1 && u->pos.z <= z2) { + return true; } } } - return true; + return false; } -int32_t Units::findIndexById(int32_t creature_id) +bool Units::isActive(df::unit *unit) { - return df::unit::binsearch_index(world->units.all, creature_id); + CHECK_NULL_POINTER(unit); + + return !unit->flags1.bits.inactive; } -df::coord Units::getPosition(df::unit *unit) +bool Units::isVisible(df::unit* unit) { CHECK_NULL_POINTER(unit); + return Maps::isTileVisible(unit->pos); +} - if (unit->flags1.bits.caged) - { - auto cage = getContainer(unit); - if (cage) - return Items::getPosition(cage); - } +bool Units::isCitizen(df::unit *unit, bool ignore_sanity) +{ + CHECK_NULL_POINTER(unit); - return unit->pos; + // Copied from the conditions used to decide game over, + // except that the game appears to let melancholy/raving + // dwarves count as citizens. + + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; + + if (!ignore_sanity && !isSane(unit)) + return false; + + return isOwnGroup(unit); } -bool Units::teleport(df::unit *unit, df::coord target_pos) -{ - // make sure source and dest map blocks are valid - auto old_occ = Maps::getTileOccupancy(unit->pos); - auto new_occ = Maps::getTileOccupancy(target_pos); - if (!old_occ || !new_occ) +bool Units::isFortControlled(df::unit *unit) +{ // Reverse-engineered from ambushing unit code + CHECK_NULL_POINTER(unit); + + if (*gamemode != game_mode::DWARF) return false; - // clear appropriate occupancy flags at old tile - if (unit->flags1.bits.on_ground) - // this is potentially wrong, but the game will recompute this as needed - old_occ->bits.unit_grounded = 0; - else - old_occ->bits.unit = 0; + if (unit->mood == mood_type::Berserk || + Units::isCrazed(unit) || + Units::isOpposedToLife(unit) || + unit->enemy.undead || + unit->flags3.bits.ghostly) + return false; - // if there's already somebody standing at the destination, then force the - // unit to lay down - if (new_occ->bits.unit) - unit->flags1.bits.on_ground = 1; + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat) + return false; - // set appropriate occupancy flags at new tile - if (unit->flags1.bits.on_ground) - new_occ->bits.unit_grounded = 1; - else - new_occ->bits.unit = 1; + if (unit->flags1.bits.tame) + return true; - // move unit to destination - unit->pos = target_pos; - unit->idle_area = target_pos; + if (unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; - // move unit's riders (including babies) to destination - if (unit->flags1.bits.ridden) + return unit->civ_id != -1 && unit->civ_id == ui->civ_id; +} + +// check if creature belongs to the player's civilization +// (don't try to pasture/slaughter random untame animals) +bool Units::isOwnCiv(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->civ_id == ui->civ_id; +} + +// check if creature belongs to the player's group +bool Units::isOwnGroup(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto histfig = df::historical_figure::find(unit->hist_figure_id); + if (!histfig) + return false; + for (size_t i = 0; i < histfig->entity_links.size(); i++) { - for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) - { - df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; - if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) - rider->pos = unit->pos; - } + auto link = histfig->entity_links[i]; + if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) + return true; } + return false; +} - return true; +// check if creature belongs to the player's race +// (in combination with check for civ helps to filter out own dwarves) +bool Units::isOwnRace(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->race == ui->race_id; } -df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) + +bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findRef(unit->general_refs, type); + return !unit->flags2.bits.killed && + !unit->flags3.bits.ghostly && + !unit->curse.add_tags1.bits.NOT_LIVING; } -df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) +bool Units::isDead(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findRef(unit->specific_refs, type); + return unit->flags2.bits.killed || + unit->flags3.bits.ghostly; } -df::item *Units::getContainer(df::unit *unit) +bool Units::isKilled(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); + return unit->flags2.bits.killed; } -void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) +bool Units::isSane(df::unit *unit) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - if (init_ref) - { - spec_ref.type = specific_ref_type::UNIT; - spec_ref.data.unit = unit; - } + if (isDead(unit) || + isOpposedToLife(unit) || + unit->enemy.undead) + return false; - if (unit->flags1.bits.caged) + if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) + return false; + + switch (unit->mood) { - df::item *cage = getContainer(unit); - if (cage) - return Items::getOuterContainerRef(spec_ref, cage); + case mood_type::Melancholy: + case mood_type::Raving: + case mood_type::Berserk: + return false; + default: + break; } - return; + + return true; } -static df::identity *getFigureIdentity(df::historical_figure *figure) +bool Units::isCrazed(df::unit *unit) { - if (figure && figure->info && figure->info->reputation) - return df::identity::find(figure->info->reputation->cur_identity); - - return NULL; + CHECK_NULL_POINTER(unit); + if (unit->flags3.bits.scuttle) + return false; + if (unit->curse.rem_tags1.bits.CRAZED) + return false; + if (unit->curse.add_tags1.bits.CRAZED) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); } -df::identity *Units::getIdentity(df::unit *unit) +bool Units::isGhost(df::unit *unit) { CHECK_NULL_POINTER(unit); - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - - return getFigureIdentity(figure); + return unit->flags3.bits.ghostly; } -void Units::setNickname(df::unit *unit, std::string nick) +bool Units::isHidden(df::unit *unit) { CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code - // There are >=3 copies of the name, and the one - // in the unit is not the authoritative one. - // This is the reason why military units often - // lose nicknames set from Dwarf Therapist. - Translation::setNickname(&unit->name, nick); - - if (unit->status.current_soul) - Translation::setNickname(&unit->status.current_soul->name, nick); + if (*df::global::debug_showambush) + return false; - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - if (figure) + if (*gamemode == game_mode::ADVENTURE) { - Translation::setNickname(&figure->name, nick); + if (unit == world->units.active[0]) + return false; + else if (unit->flags1.bits.hidden_in_ambush) + return true; + } + else + { + if (*gametype == game_type::DWARF_ARENA) + return false; + else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) + return true; + } - if (auto identity = getFigureIdentity(figure)) - { - df::historical_figure *id_hfig = NULL; + if (unit->flags1.bits.caged) + { + auto spec_ref = getOuterContainerRef(unit); + if (spec_ref.type == specific_ref_type::UNIT) + return isHidden(spec_ref.data.unit); + } - switch (identity->type) { - case df::identity_type::None: - case df::identity_type::HidingCurse: - case df::identity_type::FalseIdentity: - case df::identity_type::InfiltrationIdentity: - case df::identity_type::Identity: - break; // We want the nickname to end up in the identity - - case df::identity_type::Impersonating: - case df::identity_type::TrueName: - id_hfig = df::historical_figure::find(identity->histfig_id); - break; - } - - if (id_hfig) - { - Translation::setNickname(&id_hfig->name, nick); - } - else - Translation::setNickname(&identity->name, nick); - } - } -} - -df::language_name *Units::getVisibleName(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - // as of 0.44.11, identity names take precedence over associated histfig names - if (auto identity = getIdentity(unit)) - return &identity->name; - - return &unit->name; -} - -df::nemesis_record *Units::getNemesis(df::unit *unit) -{ - if (!unit) - return NULL; - - for (unsigned i = 0; i < unit->general_refs.size(); i++) - { - df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); - if (rv && rv->unit == unit) - return rv; - } - - return NULL; + if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) + return false; + else + return !Maps::isTileVisible(Units::getPosition(unit)); } - bool Units::isHidingCurse(df::unit *unit) { if (!unit->job.hunt_target) @@ -333,232 +327,294 @@ bool Units::isHidingCurse(df::unit *unit) return false; } -int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) -{ - auto &aobj = unit->body.physical_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - return std::max(0, value); +bool Units::isMale(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->sex == 1; } -int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +bool Units::isFemale(df::unit* unit) { - auto soul = unit->status.current_soul; - if (!soul) return 0; - - auto &aobj = soul->mental_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - - return std::max(0, value); + CHECK_NULL_POINTER(unit); + return unit->sex == 0; } -bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +bool Units::isBaby(df::unit* unit) { - auto creature = df::creature_raw::find(race); - if (!creature) - return false; - - auto craw = vector_get(creature->caste, caste); - if (!craw) - return false; - - return craw->flags.is_set(flag); + CHECK_NULL_POINTER(unit); + return unit->profession == df::profession::BABY; } -bool Units::isCrazed(df::unit *unit) +bool Units::isChild(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->flags3.bits.scuttle) - return false; - if (unit->curse.rem_tags1.bits.CRAZED) - return false; - if (unit->curse.add_tags1.bits.CRAZED) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); + return unit->profession == df::profession::CHILD; } -bool Units::isOpposedToLife(df::unit *unit) +bool Units::isAdult(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) - return false; - if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); + return !isBaby(unit) && !isChild(unit); } -bool Units::hasExtravision(df::unit *unit) +bool Units::isGay(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.EXTRAVISION) + if (!unit->status.current_soul) return false; - if (unit->curse.add_tags1.bits.EXTRAVISION) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); + df::orientation_flags orientation = unit->status.current_soul->orientation_flags; + return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) + && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); } -bool Units::isBloodsucker(df::unit *unit) +bool Units::isNaked(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.BLOODSUCKER) - return false; - if (unit->curse.add_tags1.bits.BLOODSUCKER) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); + // TODO(kazimuth): is this correct? + return (unit->inventory.empty()); } -bool Units::isMischievous(df::unit *unit) +bool Units::isVisiting(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited; +} + + +bool Units::isTrainableHunting(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.MISCHIEVOUS) - return false; - if (unit->curse.add_tags1.bits.MISCHIEVOUS) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); } -df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +bool Units::isTrainableWar(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); +} - auto &vec = unit->status.misc_traits; - for (size_t i = 0; i < vec.size(); i++) - if (vec[i]->id == type) - return vec[i]; +bool Units::isTrained(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + // case a: trained for war/hunting (those don't have a training level, strangely) + if(Units::isWar(unit) || Units::isHunter(unit)) + return true; - if (create) + // case b: tamed and trained wild creature, gets a training level + bool trained = false; + switch (unit->training_level) { - auto obj = new df::unit_misc_trait(); - obj->id = type; - vec.push_back(obj); - return obj; + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + //case df::animal_training_level::Domesticated: + trained = true; + break; + default: + break; } + return trained; +} - return NULL; +// check for profession "hunting creature" +bool Units::isHunter(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->profession == df::profession::TRAINED_HUNTER + || unit->profession2 == df::profession::TRAINED_HUNTER; } -bool Units::isDead(df::unit *unit) +// check for profession "war creature" +bool Units::isWar(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed || - unit->flags3.bits.ghostly; + return unit->profession == df::profession::TRAINED_WAR + || unit->profession2 == df::profession::TRAINED_WAR; } -bool Units::isAlive(df::unit *unit) +bool Units::isTame(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return !unit->flags2.bits.killed && - !unit->flags3.bits.ghostly && - !unit->curse.add_tags1.bits.NOT_LIVING; + bool tame = false; + if(unit->flags1.bits.tame) + { + switch (unit->training_level) + { + case df::animal_training_level::SemiWild: //?? + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + case df::animal_training_level::Domesticated: + tame=true; + break; + case df::animal_training_level::Unk8: //?? + case df::animal_training_level::WildUntamed: + default: + tame=false; + break; + } + } + return tame; } -bool Units::isSane(df::unit *unit) +bool Units::isTamable(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::PET) + || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); +} - if (isDead(unit) || - isOpposedToLife(unit) || - unit->enemy.undead) - return false; - - if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) - return false; - - switch (unit->mood) +// check if creature is domesticated +// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) +bool Units::isDomesticated(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + bool tame = false; + if(unit->flags1.bits.tame) { - case mood_type::Melancholy: - case mood_type::Raving: - case mood_type::Berserk: - return false; - default: - break; + switch (unit->training_level) + { + case df::animal_training_level::Domesticated: + tame=true; + break; + default: + tame=false; + break; + } } - - return true; + return tame; } -bool Units::isCitizen(df::unit *unit) +bool Units::isMarkedForSlaughter(df::unit* unit) { CHECK_NULL_POINTER(unit); + return unit->flags2.bits.slaughter == 1; +} - // Copied from the conditions used to decide game over, - // except that the game appears to let melancholy/raving - // dwarves count as citizens. +bool Units::isGelded(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto wounds = unit->body.wounds; + for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) + { + auto parts = (*wound)->parts; + for (auto part = parts.begin(); part != parts.end(); ++part) + { + if ((*part)->flags2.bits.gelded) + return true; + } + } + return false; +} - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat || - unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) - return false; +bool Units::isEggLayer(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) + || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); +} - if (!isSane(unit)) - return false; +bool Units::isGrazer(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::GRAZER); +} - return isOwnGroup(unit); +bool Units::isMilkable(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::MILKABLE); } -bool Units::isFortControlled(df::unit *unit) -{ // Reverse-engineered from ambushing unit code +bool Units::isForest(df::unit* unit) +{ CHECK_NULL_POINTER(unit); + return unit->flags1.bits.forest == 1; +} - if (*gamemode != game_mode::DWARF) +bool Units::isMischievous(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.MISCHIEVOUS) return false; + if (unit->curse.add_tags1.bits.MISCHIEVOUS) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); +} - if (unit->mood == mood_type::Berserk || - Units::isCrazed(unit) || - Units::isOpposedToLife(unit) || - unit->enemy.undead || - unit->flags3.bits.ghostly) - return false; +// check if unit is marked as available for adoption +bool Units::isAvailableForAdoption(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto refs = unit->specific_refs; + for(size_t i=0; itype; + if( reftype == df::specific_ref_type::PETINFO_PET ) + { + //df::pet_info* pet = ref->pet; + return true; + } + } - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat) - return false; + return false; +} - if (unit->flags1.bits.tame) + +bool Units::hasExtravision(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.EXTRAVISION) + return false; + if (unit->curse.add_tags1.bits.EXTRAVISION) return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); +} - if (unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) +bool Units::isOpposedToLife(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) return false; + if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); +} - return unit->civ_id != -1 && unit->civ_id == ui->civ_id; +bool Units::isBloodsucker(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.BLOODSUCKER) + return false; + if (unit->curse.add_tags1.bits.BLOODSUCKER) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); } + bool Units::isDwarf(df::unit *unit) { CHECK_NULL_POINTER(unit); @@ -567,113 +623,393 @@ bool Units::isDwarf(df::unit *unit) unit->enemy.normal_race == ui->race_id; } -// check for profession "war creature" -bool Units::isWar(df::unit* unit) +bool Units::isAnimal(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NATURAL_ANIMAL); +} + +bool Units::isMerchant(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::TRAINED_WAR - || unit->profession2 == df::profession::TRAINED_WAR; + + return unit->flags1.bits.merchant == 1; } -// check for profession "hunting creature" -bool Units::isHunter(df::unit* unit) +bool Units::isDiplomat(df::unit* unit) { - CHECK_NULL_POINTER(unit) - return unit->profession == df::profession::TRAINED_HUNTER - || unit->profession2 == df::profession::TRAINED_HUNTER; + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.diplomat == 1; } -// check if unit is marked as available for adoption -bool Units::isAvailableForAdoption(df::unit* unit) +bool Units::isVisitor(df::unit* unit) { CHECK_NULL_POINTER(unit); - auto refs = unit->specific_refs; - for(size_t i=0; iflags2.bits.visitor || unit->flags2.bits.visitor_uninvited; +} + +bool Units::isInvader(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return !isOwnGroup(unit) && + (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader); +} + +bool Units::isUndead(df::unit* unit, bool include_vamps) +{ + CHECK_NULL_POINTER(unit); + + const auto &cb = unit->curse.add_tags1.bits; + return unit->flags3.bits.ghostly || + ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (include_vamps || !cb.BLOODSUCKER)); +} + +bool Units::isNightCreature(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); +} + +bool Units::isSemiMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::SEMIMEGABEAST); +} + +bool Units::isMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); +} + +bool Units::isTitan(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::TITAN); +} + +bool Units::isDemon(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + using namespace df::enums::caste_raw_flags; + const auto &cf = unit->enemy.caste_flags; + return cf.is_set(DEMON) || cf.is_set(UNIQUE_DEMON); +} + +bool Units::isDanger(df::unit* unit) { + CHECK_NULL_POINTER(unit); + return isCrazed(unit) || + isInvader(unit) || + isUndead(unit, true) || + isSemiMegabeast(unit) || + isNightCreature(unit) || + isGreatDanger(unit); +} + +bool Units::isGreatDanger(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return isDemon(unit) || isTitan(unit) || isMegabeast(unit); +} + + + +int32_t Units::getNumUnits() +{ + return world->units.all.size(); +} + +df::unit *Units::getUnit (const int32_t index) +{ + return vector_get(world->units.all, index); +} + +// returns index of creature actually read or -1 if no creature can be found +bool Units::getUnitsInBox (std::vector &units, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) +{ + if (!world) + return false; + + units.clear(); + for (df::unit *u : world->units.all) { - auto ref = refs[i]; - auto reftype = ref->type; - if( reftype == df::specific_ref_type::PETINFO_PET ) + if (isUnitInBox(u, x1, y1, z1, x2, y2, z2)) { - //df::pet_info* pet = ref->pet; - return true; + units.push_back(u); } } + return true; +} - return false; +int32_t Units::findIndexById(int32_t creature_id) +{ + return df::unit::binsearch_index(world->units.all, creature_id); } -// check if creature belongs to the player's civilization -// (don't try to pasture/slaughter random untame animals) -bool Units::isOwnCiv(df::unit* unit) +df::coord Units::getPosition(df::unit *unit) { CHECK_NULL_POINTER(unit); - return unit->civ_id == ui->civ_id; + + if (unit->flags1.bits.caged) + { + auto cage = getContainer(unit); + if (cage) + return Items::getPosition(cage); + } + + return unit->pos; } -// check if creature belongs to the player's group -bool Units::isOwnGroup(df::unit* unit) +bool Units::teleport(df::unit *unit, df::coord target_pos) { - CHECK_NULL_POINTER(unit); - auto histfig = df::historical_figure::find(unit->hist_figure_id); - if (!histfig) + // make sure source and dest map blocks are valid + auto old_occ = Maps::getTileOccupancy(unit->pos); + auto new_occ = Maps::getTileOccupancy(target_pos); + if (!old_occ || !new_occ) return false; - for (size_t i = 0; i < histfig->entity_links.size(); i++) + + // clear appropriate occupancy flags at old tile + if (unit->flags1.bits.on_ground) + // this is potentially wrong, but the game will recompute this as needed + old_occ->bits.unit_grounded = 0; + else + old_occ->bits.unit = 0; + + // if there's already somebody standing at the destination, then force the + // unit to lay down + if (new_occ->bits.unit) + unit->flags1.bits.on_ground = 1; + + // set appropriate occupancy flags at new tile + if (unit->flags1.bits.on_ground) + new_occ->bits.unit_grounded = 1; + else + new_occ->bits.unit = 1; + + // move unit to destination + unit->pos = target_pos; + unit->idle_area = target_pos; + + // move unit's riders (including babies) to destination + if (unit->flags1.bits.ridden) { - auto link = histfig->entity_links[i]; - if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) - return true; + for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) + { + df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; + if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) + rider->pos = unit->pos; + } } - return false; + + return true; } -// check if creature belongs to the player's race -// (in combination with check for civ helps to filter out own dwarves) -bool Units::isOwnRace(df::unit* unit) +df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) { CHECK_NULL_POINTER(unit); - return unit->race == ui->race_id; + + return findRef(unit->general_refs, type); } -bool Units::isVisible(df::unit* unit) +df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) { CHECK_NULL_POINTER(unit); - return Maps::isTileVisible(unit->pos); + + return findRef(unit->specific_refs, type); +} + +df::item *Units::getContainer(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); +} + +void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) +{ + CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code + + if (init_ref) + { + spec_ref.type = specific_ref_type::UNIT; + spec_ref.data.unit = unit; + } + + if (unit->flags1.bits.caged) + { + df::item *cage = getContainer(unit); + if (cage) + return Items::getOuterContainerRef(spec_ref, cage); + } + return; +} + +static df::identity *getFigureIdentity(df::historical_figure *figure) +{ + if (figure && figure->info && figure->info->reputation) + return df::identity::find(figure->info->reputation->cur_identity); + + return NULL; +} + +df::identity *Units::getIdentity(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + + return getFigureIdentity(figure); +} + +void Units::setNickname(df::unit *unit, std::string nick) +{ + CHECK_NULL_POINTER(unit); + + // There are >=3 copies of the name, and the one + // in the unit is not the authoritative one. + // This is the reason why military units often + // lose nicknames set from Dwarf Therapist. + Translation::setNickname(&unit->name, nick); + + if (unit->status.current_soul) + Translation::setNickname(&unit->status.current_soul->name, nick); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + if (figure) + { + Translation::setNickname(&figure->name, nick); + + if (auto identity = getFigureIdentity(figure)) + { + df::historical_figure *id_hfig = NULL; + + switch (identity->type) { + case df::identity_type::None: + case df::identity_type::HidingCurse: + case df::identity_type::FalseIdentity: + case df::identity_type::InfiltrationIdentity: + case df::identity_type::Identity: + break; // We want the nickname to end up in the identity + + case df::identity_type::Impersonating: + case df::identity_type::TrueName: + id_hfig = df::historical_figure::find(identity->histfig_id); + break; + } + + if (id_hfig) + { + Translation::setNickname(&id_hfig->name, nick); + } + else + Translation::setNickname(&identity->name, nick); + } + } +} + +df::language_name *Units::getVisibleName(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + // as of 0.44.11, identity names take precedence over associated histfig names + if (auto identity = getIdentity(unit)) + return &identity->name; + + return &unit->name; +} + +df::nemesis_record *Units::getNemesis(df::unit *unit) +{ + if (!unit) + return NULL; + + for (unsigned i = 0; i < unit->general_refs.size(); i++) + { + df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); + if (rv && rv->unit == unit) + return rv; + } + + return NULL; +} + + +int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) +{ + auto &aobj = unit->body.physical_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +{ + auto soul = unit->status.current_soul; + if (!soul) return 0; + + auto &aobj = soul->mental_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +{ + auto creature = df::creature_raw::find(race); + if (!creature) + return false; + + auto craw = vector_get(creature->caste, caste); + if (!craw) + return false; + + return craw->flags.is_set(flag); } -bool Units::isHidden(df::unit *unit) +df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - - if (*df::global::debug_showambush) - return false; - if (*gamemode == game_mode::ADVENTURE) - { - if (unit == world->units.active[0]) - return false; - else if (unit->flags1.bits.hidden_in_ambush) - return true; - } - else - { - if (*gametype == game_type::DWARF_ARENA) - return false; - else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) - return true; - } + auto &vec = unit->status.misc_traits; + for (size_t i = 0; i < vec.size(); i++) + if (vec[i]->id == type) + return vec[i]; - if (unit->flags1.bits.caged) + if (create) { - auto spec_ref = getOuterContainerRef(unit); - if (spec_ref.type == specific_ref_type::UNIT) - return isHidden(spec_ref.data.unit); + auto obj = new df::unit_misc_trait(); + obj->id = type; + vec.push_back(obj); + return obj; } - if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) - return false; - else - return !Maps::isTileVisible(Units::getPosition(unit)); + return NULL; } // get race name by id or unit pointer @@ -752,86 +1088,6 @@ string Units::getRaceChildName(df::unit* unit) return getRaceChildNameById(unit->race); } -bool Units::isBaby(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::BABY; -} - -bool Units::isChild(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::CHILD; -} - -bool Units::isAdult(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return !isBaby(unit) && !isChild(unit); -} - -bool Units::isEggLayer(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) - || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); -} - -bool Units::isGrazer(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::GRAZER); -} - -bool Units::isMilkable(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::MILKABLE); -} - -bool Units::isTrainableWar(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); -} - -bool Units::isTrainableHunting(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); -} - -bool Units::isTamable(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::PET) - || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); -} - -bool Units::isMale(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->sex == 1; -} - -bool Units::isFemale(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->sex == 0; -} - double Units::getAge(df::unit *unit, bool true_age) { @@ -1660,170 +1916,6 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) return entry->events[entry->events.size() - 1]; } -bool Units::isMerchant(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.merchant == 1; -} - -bool Units::isDiplomat(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.diplomat == 1; -} - -bool Units::isForest(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags1.bits.forest == 1; -} - -bool Units::isMarkedForSlaughter(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags2.bits.slaughter == 1; -} - -bool Units::isTame(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::SemiWild: //?? - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - case df::animal_training_level::Domesticated: - tame=true; - break; - case df::animal_training_level::Unk8: //?? - case df::animal_training_level::WildUntamed: - default: - tame=false; - break; - } - } - return tame; -} - -bool Units::isTrained(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // case a: trained for war/hunting (those don't have a training level, strangely) - if(Units::isWar(unit) || Units::isHunter(unit)) - return true; - - // case b: tamed and trained wild creature, gets a training level - bool trained = false; - switch (unit->training_level) - { - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - //case df::animal_training_level::Domesticated: - trained = true; - break; - default: - break; - } - return trained; -} - -bool Units::isGay(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - if (!unit->status.current_soul) - return false; - df::orientation_flags orientation = unit->status.current_soul->orientation_flags; - return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) - && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); -} - -bool Units::isNaked(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // TODO(kazimuth): is this correct? - return (unit->inventory.empty()); -} - -bool Units::isUndead(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // ignore vampires, they should be treated like normal dwarves - return (unit->flags3.bits.ghostly || - ( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER )); -} - -bool Units::isGhost(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags3.bits.ghostly; -} - -bool Units::isActive(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return !unit->flags1.bits.inactive; -} - -bool Units::isKilled(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed; -} - -bool Units::isGelded(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - auto wounds = unit->body.wounds; - for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) - { - auto parts = (*wound)->parts; - for (auto part = parts.begin(); part != parts.end(); ++part) - { - if ((*part)->flags2.bits.gelded) - return true; - } - } - return false; -} - -// check if creature is domesticated -// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) -bool Units::isDomesticated(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::Domesticated: - tame=true; - break; - default: - tame=false; - break; - } - } - return tame; -} - // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000};