diff --git a/LUA_API.rst b/LUA_API.rst
index 4d9170d6e..48d2c19e6 100644
--- a/LUA_API.rst
+++ b/LUA_API.rst
@@ -868,6 +868,17 @@ Units module
Returns the nemesis record of the unit if it has one, or *nil*.
+* ``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.isDead(unit)``
The unit is completely dead and passive, or a ghost.
@@ -894,6 +905,10 @@ Units module
Returns the age of the unit in years as a floating-point value.
If ``true_age`` is true, ignores false identities.
+* ``dfhack.units.getEffectiveSkill(unit, skill)``
+
+ Computes the effective rating for the given skill, taking into account exhaustion, pain etc.
+
* ``dfhack.units.getNoblePositions(unit)``
Returns a list of tables describing noble position assignments, or *nil*.
diff --git a/Lua API.html b/Lua API.html
index dc9c8d73e..c302c29f7 100644
--- a/Lua API.html
+++ b/Lua API.html
@@ -1106,6 +1106,18 @@ a lua list containing them.
dfhack.units.getNemesis(unit)
Returns the nemesis record of the unit if it has one, or nil.
+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.isDead(unit)
The unit is completely dead and passive, or a ghost.
@@ -1126,6 +1138,9 @@ same checks the game uses to decide game-over by extinction.
Returns the age of the unit in years as a floating-point value.
If true_age is true, ignores false identities.
+dfhack.units.getEffectiveSkill(unit, skill)
+Computes the effective rating for the given skill, taking into account exhaustion, pain etc.
+
dfhack.units.getNoblePositions(unit)
Returns a list of tables describing noble position assignments, or nil.
Every table has fields entity, assignment and position.
diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp
index 807cbf539..6caf45575 100644
--- a/library/LuaApi.cpp
+++ b/library/LuaApi.cpp
@@ -79,6 +79,7 @@ distribution.
#include "df/building_civzonest.h"
#include "df/region_map_entry.h"
#include "df/flow_info.h"
+#include "df/unit_misc_trait.h"
#include
#include
@@ -813,12 +814,18 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getVisibleName),
WRAPM(Units, getIdentity),
WRAPM(Units, getNemesis),
+ WRAPM(Units, isCrazed),
+ WRAPM(Units, isOpposedToLife),
+ WRAPM(Units, hasExtravision),
+ WRAPM(Units, isBloodsucker),
+ WRAPM(Units, getMiscTrait),
WRAPM(Units, isDead),
WRAPM(Units, isAlive),
WRAPM(Units, isSane),
WRAPM(Units, isDwarf),
WRAPM(Units, isCitizen),
WRAPM(Units, getAge),
+ WRAPM(Units, getEffectiveSkill),
WRAPM(Units, getProfessionName),
WRAPM(Units, getCasteProfessionName),
WRAPM(Units, getProfessionColor),
diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h
index 9003dc3af..ece151127 100644
--- a/library/include/modules/Units.h
+++ b/library/include/modules/Units.h
@@ -32,6 +32,8 @@ distribution.
#include "modules/Items.h"
#include "DataDefs.h"
#include "df/unit.h"
+#include "df/misc_trait_type.h"
+#include "df/job_skill.h"
namespace df
{
@@ -41,6 +43,7 @@ namespace df
struct historical_entity;
struct entity_position_assignment;
struct entity_position;
+ struct unit_misc_trait;
}
/**
@@ -208,6 +211,13 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit);
DFHACK_EXPORT df::assumed_identity *getIdentity(df::unit *unit);
DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit);
+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 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);
@@ -216,6 +226,8 @@ DFHACK_EXPORT bool isDwarf(df::unit *unit);
DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
+DFHACK_EXPORT int getEffectiveSkill(df::unit *unit, df::job_skill skill_id);
+
struct NoblePosition {
df::historical_entity *entity;
df::entity_position_assignment *assignment;
diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp
index 874dabc3d..6a672b585 100644
--- a/library/modules/Units.cpp
+++ b/library/modules/Units.cpp
@@ -63,11 +63,15 @@ using namespace std;
#include "df/burrow.h"
#include "df/creature_raw.h"
#include "df/caste_raw.h"
+#include "df/game_mode.h"
+#include "df/unit_misc_trait.h"
+#include "df/unit_skill.h"
using namespace DFHack;
using namespace df::enums;
using df::global::world;
using df::global::ui;
+using df::global::gamemode;
bool Units::isValid()
{
@@ -626,8 +630,9 @@ static bool casteFlagSet(int race, int caste, df::caste_raw_flags flag)
return craw->flags.is_set(flag);
}
-static bool isCrazed(df::unit *unit)
+bool Units::isCrazed(df::unit *unit)
{
+ CHECK_NULL_POINTER(unit);
if (unit->flags3.bits.scuttle)
return false;
if (unit->curse.rem_tags1.bits.CRAZED)
@@ -637,13 +642,54 @@ static bool isCrazed(df::unit *unit)
return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED);
}
-static bool isOpposedToLife(df::unit *unit)
+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::CANNOT_UNDEAD);
+ return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE);
+}
+
+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);
+}
+
+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);
+}
+
+df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create)
+{
+ CHECK_NULL_POINTER(unit);
+
+ auto &vec = unit->status.misc_traits;
+ for (size_t i = 0; i < vec.size(); i++)
+ if (vec[i]->id == type)
+ return vec[i];
+
+ if (create)
+ {
+ auto obj = new df::unit_misc_trait();
+ obj->id = type;
+ vec.push_back(obj);
+ return obj;
+ }
+
+ return NULL;
}
bool DFHack::Units::isDead(df::unit *unit)
@@ -753,6 +799,113 @@ double DFHack::Units::getAge(df::unit *unit, bool true_age)
return cur_time - birth_time;
}
+inline int adjust_skill_rating(int &rating, bool is_adventure, int value, int dwarf3_4, int dwarf1_2, int adv9_10, int adv3_4, int adv1_2)
+{
+ if (is_adventure)
+ {
+ if (value >= adv1_2) rating >>= 1;
+ else if (value >= adv3_4) rating = rating*3/4;
+ else if (value >= adv9_10) rating = rating*9/10;
+ }
+ else
+ {
+ if (value >= dwarf1_2) rating >>= 1;
+ else if (value >= dwarf3_4) return rating*3/4;
+ }
+}
+
+int Units::getEffectiveSkill(df::unit *unit, df::job_skill skill_id)
+{
+ CHECK_NULL_POINTER(unit);
+
+ /*
+ * This is 100% reverse-engineered from DF code.
+ */
+
+ if (!unit->status.current_soul)
+ return 0;
+
+ // Retrieve skill from unit soul:
+
+ df::enum_field key(skill_id);
+ auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key);
+
+ int rating = 0;
+ if (skill)
+ rating = std::max(0, int(skill->rating) - skill->rusty);
+
+ // Apply special states
+
+ if (unit->counters.soldier_mood == df::unit::T_counters::None)
+ {
+ if (unit->counters.nausea > 0) rating >>= 1;
+ if (unit->counters.winded > 0) rating >>= 1;
+ if (unit->counters.stunned > 0) rating >>= 1;
+ if (unit->counters.dizziness > 0) rating >>= 1;
+ if (unit->counters2.fever > 0) rating >>= 1;
+ }
+
+ if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
+ {
+ if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle &&
+ !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged &&
+ !hasExtravision(unit))
+ {
+ rating >>= 2;
+ }
+ if (unit->counters.pain >= 100 && unit->mood == -1)
+ {
+ rating >>= 1;
+ }
+ if (unit->counters2.exhaustion >= 2000)
+ {
+ rating = rating*3/4;
+ if (unit->counters2.exhaustion >= 4000)
+ {
+ rating = rating*3/4;
+ if (unit->counters2.exhaustion >= 6000)
+ rating = rating*3/4;
+ }
+ }
+ }
+
+ // Hunger etc timers
+
+ bool is_adventure = (gamemode && *gamemode == game_mode::ADVENTURE);
+
+ if (!unit->flags3.bits.scuttle && isBloodsucker(unit))
+ {
+ using namespace df::enums::misc_trait_type;
+
+ if (auto trait = getMiscTrait(unit, TimeSinceSuckedBlood))
+ {
+ adjust_skill_rating(
+ rating, is_adventure, trait->value,
+ 302400, 403200, // dwf 3/4; 1/2
+ 1209600, 1209600, 2419200 // adv 9/10; 3/4; 1/2
+ );
+ }
+ }
+
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.thirst_timer,
+ 50000, 50000, 115200, 172800, 345600
+ );
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.hunger_timer,
+ 75000, 75000, 172800, 1209600, 2592000
+ );
+ if (is_adventure && unit->counters2.sleepiness_timer >= 846000)
+ rating >>= 2;
+ else
+ adjust_skill_rating(
+ rating, is_adventure, unit->counters2.sleepiness_timer,
+ 150000, 150000, 172800, 259200, 345600
+ );
+
+ return rating;
+}
+
static bool noble_pos_compare(const Units::NoblePosition &a, const Units::NoblePosition &b)
{
if (a.position->precedence < b.position->precedence)
diff --git a/library/xml b/library/xml
index 18e76d8bd..db765a65b 160000
--- a/library/xml
+++ b/library/xml
@@ -1 +1 @@
-Subproject commit 18e76d8bdd3d7e604c8bb40e62cd1fd7c4647e36
+Subproject commit db765a65b17099dbec115812b40a19b46ad59431
diff --git a/plugins/devel/siege-engine.cpp b/plugins/devel/siege-engine.cpp
index 60035b276..8b5010194 100644
--- a/plugins/devel/siege-engine.cpp
+++ b/plugins/devel/siege-engine.cpp
@@ -501,82 +501,6 @@ static void paintAimScreen(df::building_siegeenginest *bld, df::coord view, df::
* Unit tracking
*/
-static bool 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);
-}
-
-static bool hasExtravision(df::unit *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);
-}
-
-int getEffectiveSkill(df::unit *unit, df::job_skill skill_id)
-{
- CHECK_NULL_POINTER(unit);
-
- if (!unit->status.current_soul)
- return 0;
-
- int rating = 0;
-
- df::enum_field key(skill_id);
- auto skill = binsearch_in_vector(unit->status.current_soul->skills, &df::unit_skill::id, key);
- if (skill)
- rating = std::max(0, int(skill->rating) - skill->rusty);
-
- if (unit->counters.soldier_mood == df::unit::T_counters::None)
- {
- if (unit->counters.nausea > 0) rating >>= 1;
- if (unit->counters.winded > 0) rating >>= 1;
- if (unit->counters.stunned > 0) rating >>= 1;
- if (unit->counters.dizziness > 0) rating >>= 1;
- if (unit->counters2.fever > 0) rating >>= 1;
- }
-
- if (unit->counters.soldier_mood != df::unit::T_counters::MartialTrance)
- {
- if (unit->counters.pain >= 100 && unit->mood == -1) rating >>= 1;
-
- if (!unit->flags3.bits.ghostly && !unit->flags3.bits.scuttle &&
- !unit->flags2.bits.vision_good && !unit->flags2.bits.vision_damaged &&
- !hasExtravision(unit))
- {
- rating >>= 2;
- }
- if (unit->counters2.exhaustion >= 2000)
- {
- rating = rating*3/4;
- if (unit->counters2.exhaustion >= 4000)
- {
- rating = rating*3/4;
- if (unit->counters2.exhaustion >= 6000)
- rating = rating*3/4;
- }
- }
- }
-
- // TODO: bloodsucker; advmode
-
- if (unit->counters2.thirst_timer >= 50000) rating >>= 1;
- if (unit->counters2.hunger_timer >= 75000) rating >>= 1;
- if (unit->counters2.sleepiness_timer >= 150000) rating >>= 1;
-
- return rating;
-}
-
static int getAttrValue(const df::unit_attribute &attr)
{
return std::max(0, attr.value - attr.soft_demotion);
@@ -635,7 +559,7 @@ int getSpeedRating(df::unit *unit)
speed *= 2;
if (craw->flags.is_set(caste_raw_flags::SWIMS_LEARNED))
{
- int skill = getEffectiveSkill(unit, job_skill::SWIMMING);
+ int skill = Units::getEffectiveSkill(unit, job_skill::SWIMMING);
if (skill > 1)
skill = skill * std::max(6, 21-skill) / 20;
}
@@ -693,7 +617,7 @@ int getSpeedRating(df::unit *unit)
speed += 2000;
else if (unit->flags3.bits.on_crutch)
{
- int skill = getEffectiveSkill(unit, job_skill::CRUTCH_WALK);
+ int skill = Units::getEffectiveSkill(unit, job_skill::CRUTCH_WALK);
speed += 2000 - 100*std::min(20, skill);
}
@@ -739,7 +663,7 @@ int getSpeedRating(df::unit *unit)
if (unit->mood == mood_type::Melancholy) speed += 8000;
// Inventory encumberance
- int armor_skill = getEffectiveSkill(unit, job_skill::ARMOR);
+ int armor_skill = Units::getEffectiveSkill(unit, job_skill::ARMOR);
armor_skill = std::min(15, armor_skill);
int inv_weight = 0, inv_weight_fraction = 0;