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}