Skip to main content

rpfm_extensions/startpos/world/
agents.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Agent data of characters and factions: abilities (`CHARACTER_DETAILS/AGENT_ABILITIES`), attributes
12//! (`AGENT_ATTRIBUTES`), skill sets and starting skills (`CAMPAIGN_SKILLS`) and the agent types of the recruitment
13//! pools (`FACTION/CHARACTER_RECRUITMENT_POOL_MANAGER`).
14//!
15//! Verified on Attila's main campaign for every living character and every faction. The game stores the
16//! recruitment pools in hash order.
17
18use std::collections::HashMap;
19
20use rpfm_lib::error::Result;
21
22use super::{CampaignIndex, FactionEntry};
23use crate::startpos::tables::StartposTables;
24
25#[cfg(test)] #[path = "agents_test.rs"] mod agents_test;
26
27/// Agent abilities, in the order the startpos stores them.
28pub const ABILITIES: [&str; 9] = [
29    "command_force",
30    "assist_army",
31    "assist_province",
32    "hinder_army",
33    "hinder_character",
34    "hinder_agent",
35    "hinder_province",
36    "hinder_settlement",
37    "passive_ability",
38];
39
40/// Agent attributes, in the order the startpos stores them.
41pub const ATTRIBUTES: [&str; 3] = ["subterfuge", "zeal", "authority"];
42
43//---------------------------------------------------------------------------//
44//                              Implementations
45//---------------------------------------------------------------------------//
46
47/// Returns which abilities an agent type has, in [`ABILITIES`] order (`agent_to_agent_abilities`).
48///
49/// # Arguments
50///
51/// * `tables` - Tables with `agent_to_agent_abilities`.
52/// * `agent` - Agent type.
53///
54/// # Errors
55///
56/// Returns an error if a table or column is missing.
57pub fn agent_abilities(tables: &StartposTables, agent: &str) -> Result<[bool; ABILITIES.len()]> {
58    let mut abilities = [false; ABILITIES.len()];
59    for row in tables.table("agent_to_agent_abilities")?.rows() {
60        if row.str("agent")? == agent {
61            let ability = row.str("ability")?;
62            if let Some(index) = ABILITIES.iter().position(|known| *known == ability) {
63                abilities[index] = true;
64            }
65        }
66    }
67    Ok(abilities)
68}
69
70/// Returns the starting attributes of an agent type, in [`ATTRIBUTES`] order (`agent_to_agent_attributes`, 0 when
71/// the agent has no row for an attribute).
72///
73/// # Arguments
74///
75/// * `tables` - Tables with `agent_to_agent_attributes`.
76/// * `agent` - Agent type.
77///
78/// # Errors
79///
80/// Returns an error if a table or column is missing.
81pub fn agent_attributes(tables: &StartposTables, agent: &str) -> Result<[i64; ATTRIBUTES.len()]> {
82    let mut attributes = [0; ATTRIBUTES.len()];
83    for row in tables.table("agent_to_agent_attributes")?.rows() {
84        if row.str("agent")? == agent {
85            let attribute = row.str("attribute")?;
86            if let Some(index) = ATTRIBUTES.iter().position(|known| *known == attribute) {
87                attributes[index] = row.int("default_value")?;
88            }
89        }
90    }
91    Ok(attributes)
92}
93
94/// Returns the skill set of a character (`character_skill_node_sets` row for its agent type and subculture).
95///
96/// # Arguments
97///
98/// * `tables` - Tables with `character_skill_node_sets`.
99/// * `index` - The campaign's startpos rows.
100/// * `agent` - Agent type.
101/// * `faction` - The character's faction.
102///
103/// # Returns
104///
105/// The skill set key, or `None` if there isn't one (colonels).
106///
107/// # Errors
108///
109/// Returns an error if a table or column is missing.
110pub fn skill_set(tables: &StartposTables, index: &CampaignIndex, agent: &str, faction: &FactionEntry) -> Result<Option<String>> {
111    for row in tables.table("character_skill_node_sets")?.rows() {
112        let campaign = row.str("campaign_key")?;
113        let row_faction = row.str("faction_key")?;
114        if row.str("agent_key")? == agent
115            && row.str("subculture")? == *faction.subculture()
116            && (row_faction.is_empty() || row_faction == *faction.key())
117            && (campaign.is_empty() || campaign == *index.campaign()) {
118            return Ok(Some(row.str("key")?.into_owned()));
119        }
120    }
121    Ok(None)
122}
123
124/// A skill a character starts with: a node of its skill set with points on creation.
125#[derive(Debug, Clone, PartialEq)]
126pub struct StartingSkill {
127
128    /// Skill key.
129    pub skill: String,
130
131    /// Tier of the node in the skill set.
132    pub tier: u32,
133
134    /// Indent of the node in the skill set.
135    pub indent: f32,
136
137    /// Points the skill starts with.
138    pub points: u32,
139}
140
141/// Returns the skills a character starts with: the nodes of its skill set with points on creation, by tier.
142///
143/// # Arguments
144///
145/// * `tables` - Tables with `character_skill_nodes`.
146/// * `index` - The campaign's startpos rows.
147/// * `skill_set` - The character's skill set.
148/// * `faction` - The character's faction.
149///
150/// # Errors
151///
152/// Returns an error if a table or column is missing.
153pub fn starting_skills(tables: &StartposTables, index: &CampaignIndex, skill_set: &str, faction: &FactionEntry) -> Result<Vec<StartingSkill>> {
154    let mut skills = vec![];
155    for row in tables.table("character_skill_nodes")?.rows() {
156        let points = row.int("points_on_creation")?;
157        if points <= 0 || row.str("character_skill_node_set_key")? != skill_set {
158            continue;
159        }
160        let (campaign, row_faction, subculture) = (row.str("campaign_key")?, row.str("faction_key")?, row.str("subculture")?);
161        if (campaign.is_empty() || campaign == *index.campaign())
162            && (row_faction.is_empty() || row_faction == *faction.key())
163            && (subculture.is_empty() || subculture == *faction.subculture()) {
164            skills.push(StartingSkill {
165                skill: row.str("character_skill_key")?.into_owned(),
166                tier: row.f32("tier")? as u32,
167                indent: row.f32("indent")?,
168                points: u32::try_from(points).unwrap_or_default(),
169            });
170        }
171    }
172    skills.sort_by_key(|skill| skill.tier);
173    Ok(skills)
174}
175
176/// Returns the agent types with a recruitment pool in a faction.
177///
178/// They're the agent types of the faction's culture in `agent_culture_details` whose `agents.faction_total_cap`
179/// is above 0 (which leaves out colonels and ministers).
180///
181/// # Arguments
182///
183/// * `tables` - Tables with `agent_culture_details` and `agents`.
184/// * `faction` - The faction.
185///
186/// # Returns
187///
188/// The agent types, in `agent_culture_details` order, without repeats.
189///
190/// # Errors
191///
192/// Returns an error if a table or column is missing.
193pub fn recruitment_pool_agents(tables: &StartposTables, faction: &FactionEntry) -> Result<Vec<String>> {
194    let mut caps = HashMap::new();
195    for row in tables.table("agents")?.rows() {
196        caps.insert(row.str("key")?.into_owned(), row.int("faction_total_cap")?);
197    }
198
199    let mut agents = vec![];
200    for row in tables.table("agent_culture_details")?.rows() {
201        let agent = row.str("agent")?.into_owned();
202        if row.str("culture")? == *faction.culture() && caps.get(&agent).is_some_and(|&cap| cap > 0) && !agents.contains(&agent) {
203            agents.push(agent);
204        }
205    }
206
207    Ok(agents)
208}