sf_api/gamestate/
fortress.rs

1#![allow(clippy::module_name_repetitions)]
2use std::time::Duration;
3
4use chrono::{DateTime, Local};
5use enum_map::{Enum, EnumMap};
6use num_derive::FromPrimitive;
7use strum::{EnumCount, EnumIter, IntoEnumIterator};
8
9use super::{
10    ArrSkip, CCGet, CFPGet, CSTGet, SFError, ServerTime, items::GemType,
11};
12use crate::{
13    PlayerId,
14    gamestate::{CGet, EnumMapGet},
15    misc::soft_into,
16};
17
18/// The information about a characters fortress
19#[derive(Debug, Default, Clone)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct Fortress {
22    /// All the buildings, that a fortress can have. If they are not yet built,
23    /// they are level 0
24    pub buildings: EnumMap<FortressBuildingType, FortressBuilding>,
25    /// Information about all the buildable units in the fortress
26    pub units: EnumMap<FortressUnitType, FortressUnit>,
27    /// All information about resources in the fortress
28    pub resources: EnumMap<FortressResourceType, FortressResource>,
29    /// The `last_collectable` variable in `FortessProduction` is NOT
30    /// calculated whenever you did the last request, instead the server
31    /// calculates it at regular points in time and whenever you collect
32    /// resources. That point in time is this variable here. That means if
33    /// you want to know the exact current value, that you can collect, you
34    /// need to calculate that yourself based on the current time, this
35    /// time, the last collectable value and the per hour production of
36    /// whatever you are looking at
37    // TODO: Make such a function as a convenient helper
38    pub last_collectable_updated: Option<DateTime<Local>>,
39
40    /// The highest level buildings can be upgraded to
41    pub building_max_lvl: u8,
42    /// The level the fortress wall will have when defending against another
43    /// player
44    pub wall_combat_lvl: u16,
45
46    /// Information about the building, that is currently being upgraded
47    pub building_upgrade: FortressAction<FortressBuildingType>,
48
49    /// The upgrades count visible on the HOF screen for fortress. Should be
50    /// all building levels summed up
51    pub upgrades: u16,
52    /// The honor you have in the fortress Hall of Fame
53    pub honor: u32,
54    /// The rank you have in the fortress Hall of Fame if you have any
55    pub rank: Option<u32>,
56
57    /// Information about searching for gems
58    pub gem_search: FortressAction<GemType>,
59
60    /// The level of the hall of knights
61    pub hall_of_knights_level: u16,
62    /// The price to upgrade the hall of knights. Note, that the duration here
63    /// will be 0, as the game does not tell you how long it will take
64    pub hall_of_knights_upgrade_price: FortressCost,
65
66    /// The next enemy you can choose to battle. This should always be Some,
67    /// but there is the edge case of being the first player on a server to get
68    /// a fortress, which I can not even test for, so I just assume this could
69    /// be none then.
70    pub attack_target: Option<PlayerId>,
71    /// The time at which switching is free again
72    pub attack_free_reroll: Option<DateTime<Local>>,
73    /// The price in silver re-rolling costs
74    pub opponent_reroll_price: u64,
75
76    /// The amount of stone currently in your secret storage
77    pub secret_storage_stone: u64,
78    /// The amount of wood currently in your secret storage
79    pub secret_storage_wood: u64,
80}
81
82/// The price an upgrade, or building something in the fortress costs. These
83/// are always for one upgrade/build, which is important for unit builds
84#[derive(Debug, Default, Clone, Copy)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub struct FortressCost {
87    /// The time it takes to complete one build/upgrade
88    pub time: Duration,
89    /// The price in wood this costs
90    pub wood: u64,
91    /// The price in stone this costs
92    pub stone: u64,
93    /// The price in silver this costs
94    pub silver: u64,
95}
96
97impl FortressCost {
98    pub(crate) fn parse(data: &[i64]) -> Result<FortressCost, SFError> {
99        Ok(FortressCost {
100            time: Duration::from_secs(data.csiget(0, "fortress time", 0)?),
101            // Guessing here
102            silver: data.csiget(1, "silver cost", u64::MAX)?,
103            wood: data.csiget(2, "wood cost", u64::MAX)?,
104            stone: data.csiget(3, "stone cost", u64::MAX)?,
105        })
106    }
107}
108
109/// Information about one of the three resources, that the fortress can produce.
110#[derive(Debug, Default, Clone)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct FortressResource {
113    /// The amount of this resource you have available to spend on upgrades and
114    /// recruitment
115    pub current: u64,
116    /// The maximum amount of this resource, that you can store. If `current ==
117    /// limit`, you will not be able to collect resources from buildings
118    pub limit: u64,
119    /// Information about the production building, that produces this resource.
120    pub production: FortressProduction,
121}
122
123/// Information about the production of a resource in the fortress.  Note that
124/// experience will not have some of these fields
125#[derive(Debug, Default, Clone)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct FortressProduction {
128    /// The amount the production building has already produced, that you can
129    /// collect. Note that this value will be out of date by some amount of
130    /// time. If you need the exact current amount collectable, look at
131    /// `last_collectable_updated`
132    pub last_collectable: u64,
133    /// The maximum amount of this resource, that this building can store. If
134    /// `building_collectable == building_limit` the production stops
135    pub limit: u64,
136    /// The amount of this resource the corresponding production building
137    /// produces per hour
138    pub per_hour: u64,
139    /// The amount of this resource the building produces on the next level per
140    /// hour. If the resource is Experience, this will be 0
141    pub per_hour_next_lvl: u64,
142}
143
144/// The type of resource, that the fortress available in the fortress
145#[derive(Debug, Clone, Copy, EnumCount, EnumIter, PartialEq, Eq, Enum)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[allow(missing_docs)]
148pub enum FortressResourceType {
149    Wood = 0,
150    Stone = 1,
151    Experience = 2,
152}
153
154/// The type of building, that can be build in the fortress
155#[derive(
156    Debug, Clone, Copy, EnumCount, FromPrimitive, PartialEq, Eq, Enum, EnumIter,
157)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
159#[allow(missing_docs)]
160pub enum FortressBuildingType {
161    Fortress = 0,
162    LaborersQuarters = 1,
163    WoodcuttersHut = 2,
164    Quarry = 3,
165    GemMine = 4,
166    Academy = 5,
167    ArcheryGuild = 6,
168    Barracks = 7,
169    MagesTower = 8,
170    Treasury = 9,
171    Smithy = 10,
172    Wall = 11,
173}
174
175impl FortressBuildingType {
176    /// Minimal fortress level which is required to be allowed to build this
177    /// building
178    #[must_use]
179    pub fn required_min_fortress_level(&self) -> u16 {
180        match self {
181            FortressBuildingType::Fortress => 0,
182            FortressBuildingType::LaborersQuarters
183            | FortressBuildingType::Quarry
184            | FortressBuildingType::Smithy
185            | FortressBuildingType::WoodcuttersHut => 1,
186            FortressBuildingType::Treasury => 2,
187            FortressBuildingType::GemMine => 3,
188            FortressBuildingType::Barracks | FortressBuildingType::Wall => 4,
189            FortressBuildingType::ArcheryGuild => 5,
190            FortressBuildingType::Academy => 6,
191            FortressBuildingType::MagesTower => 7,
192        }
193    }
194
195    /// Get the unit type associated with this building type
196    #[must_use]
197    pub fn unit_produced(self) -> Option<FortressUnitType> {
198        match self {
199            FortressBuildingType::Barracks => Some(FortressUnitType::Soldier),
200            FortressBuildingType::MagesTower => {
201                Some(FortressUnitType::Magician)
202            }
203            FortressBuildingType::ArcheryGuild => {
204                Some(FortressUnitType::Archer)
205            }
206            _ => None,
207        }
208    }
209}
210
211/// Information about a single type of unit
212#[derive(Debug, Default, Clone)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct FortressUnit {
215    /// The level this unit has
216    pub level: u16,
217
218    /// The amount of this unit, that you have available for combat
219    pub count: u16,
220    /// The amount of this unit, that are currently being trained/build
221    pub in_training: u16,
222    /// The maximum `count + in_training` you have of this unit
223    pub limit: u16,
224    /// All information about training up new units of this type
225    pub training: FortressAction<()>,
226
227    /// The price to pay in stone for the next upgrade
228    pub upgrade_cost: FortressCost,
229    /// The level this unit will be at, when you upgrade it
230    pub upgrade_next_lvl: u64,
231}
232
233/// An action, that costs some amount of resources to do and will finish at a
234/// certain point in time
235#[derive(Debug, Clone, Copy)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237pub struct FortressAction<T> {
238    /// When this action was started. This can be months in the past, as this
239    /// will often not be cleared by the server
240    pub start: Option<DateTime<Local>>,
241    /// When this action will be finished
242    pub finish: Option<DateTime<Local>>,
243    /// The amount of resources it costs to do this
244    pub cost: FortressCost,
245    /// If it is not clear from the place where this is located, this will
246    /// contain the specific type, that this action will be applied to/for
247    pub target: Option<T>,
248}
249
250impl<T> Default for FortressAction<T> {
251    fn default() -> Self {
252        Self {
253            start: None,
254            finish: None,
255            cost: FortressCost::default(),
256            target: None,
257        }
258    }
259}
260
261/// The type of a unit usable in the fortress
262#[derive(Debug, Clone, Copy, EnumCount, PartialEq, Eq, Enum, EnumIter)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264#[allow(missing_docs)]
265pub enum FortressUnitType {
266    Soldier = 0,
267    Magician = 1,
268    Archer = 2,
269}
270
271/// Generic information about a building in the fortress. If you want
272/// information about a production building, you should look at the resources
273#[derive(Debug, Default, Clone, Copy)]
274#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
275pub struct FortressBuilding {
276    /// The current level of this building. If this is 0, it has not yet been
277    /// build
278    pub level: u16,
279    /// The amount of resources it costs to upgrade to the next level
280    pub upgrade_cost: FortressCost,
281}
282
283impl Fortress {
284    /// Check if units are being trained in the building (soldiers in barracks,
285    /// magicians in mages' tower, archers in archery guild), or gem mining is
286    /// in progress
287    #[must_use]
288    pub fn in_use(&self, building_type: FortressBuildingType) -> bool {
289        // Check if associated units are training
290        if let Some(unit_type) = building_type.unit_produced()
291            && let Some(finish) = self.units.get(unit_type).training.finish
292            && finish > Local::now()
293        {
294            return true;
295        }
296
297        // Check if gem mining is in progress
298        if building_type == FortressBuildingType::GemMine
299            && self.gem_search.finish.is_some()
300        {
301            return true;
302        }
303        false
304    }
305
306    /// Checks whether or not it is possible to build/upgrade a building
307    #[must_use]
308    pub fn can_build(
309        &self,
310        building_type: FortressBuildingType,
311        available_silver: u64,
312    ) -> bool {
313        let building_info = self.buildings.get(building_type);
314        let fortress_level =
315            self.buildings.get(FortressBuildingType::Fortress).level;
316        let smithy_required_buildings = [
317            FortressBuildingType::ArcheryGuild,
318            FortressBuildingType::Barracks,
319            FortressBuildingType::MagesTower,
320            FortressBuildingType::Wall,
321        ];
322
323        // Checks whether a building is in use, in such a way that it prevents
324        // upgrading (for example, if a unit is being trained, or gem mining is
325        // in progress)
326        if self.in_use(building_type) {
327            return false;
328        }
329
330        // Smithy can only be built if these buildings exist
331        let can_smithy_be_built = smithy_required_buildings
332            .map(|required_building| {
333                self.buildings.get(required_building).level
334            })
335            .iter()
336            .all(|level| *level > 0);
337
338        if matches!(building_type, FortressBuildingType::Smithy)
339            && !can_smithy_be_built
340        {
341            // Some buildings which are required to built Smithy do not exist
342            false
343        } else if !matches!(building_type, FortressBuildingType::Fortress)
344            && building_info.level == fortress_level
345        {
346            // It is not possible to upgrade a building to a higher level than
347            // the fortress level
348            false
349        } else {
350            let upgrade_cost = building_info.upgrade_cost;
351
352            // Check if required fortress level has been reached
353            building_type.required_min_fortress_level() <= fortress_level
354            // Check that no construction is in progress
355            && self.building_upgrade.target.is_none()
356            // Check if there are enough resources
357            && upgrade_cost.stone <= self.resources.get(FortressResourceType::Stone).current
358            && upgrade_cost.wood <= self.resources.get(FortressResourceType::Wood).current
359            && upgrade_cost.silver <= available_silver
360        }
361    }
362
363    pub(crate) fn update(
364        &mut self,
365        data: &[i64],
366        server_time: ServerTime,
367    ) -> Result<(), SFError> {
368        // Buildings
369        for (idx, typ) in FortressBuildingType::iter().enumerate() {
370            self.buildings.get_mut(typ).level =
371                data.csiget(524 + idx, "building lvl", 0)?;
372        }
373        self.hall_of_knights_level =
374            data.csiget(598, "hall of knights level", 0)?;
375
376        // Units
377        for (idx, typ) in FortressUnitType::iter().enumerate() {
378            let msg = "fortress unit training start";
379            self.units.get_mut(typ).training.start =
380                server_time.convert_to_local(data.cget(550 + idx, msg)?, msg);
381            let msg = "fortress unit training finish";
382            self.units.get_mut(typ).training.finish =
383                server_time.convert_to_local(data.cget(553 + idx, msg)?, msg);
384        }
385
386        #[allow(clippy::enum_glob_use)]
387        {
388            use FortressBuildingType::*;
389            use FortressUnitType::*;
390            self.units.get_mut(Soldier).limit = soft_into(
391                self.buildings.get_mut(Barracks).level * 3,
392                "soldier max count",
393                0,
394            );
395            self.units.get_mut(Magician).limit = soft_into(
396                self.buildings.get_mut(MagesTower).level,
397                "magician max count",
398                0,
399            );
400            self.units.get_mut(Archer).limit = soft_into(
401                self.buildings.get_mut(ArcheryGuild).level * 2,
402                "archer max count",
403                0,
404            );
405
406            self.units.get_mut(Soldier).count =
407                data.csimget(547, "soldier count", 0, |x| x & 0xFFFF)?;
408            self.units.get_mut(Soldier).in_training =
409                data.csimget(548, "soldier in que", 0, |x| x >> 16)?;
410
411            self.units.get_mut(Magician).count =
412                data.csimget(547, "magician count", 0, |x| x >> 16)?;
413            self.units.get_mut(Magician).in_training =
414                data.csimget(549, "magicians in que", 0, |x| x & 0xFFFF)?;
415
416            self.units.get_mut(Archer).count =
417                data.csimget(548, "archer count", 0, |x| x & 0xFFFF)?;
418            self.units.get_mut(Archer).in_training =
419                data.csimget(549, "archer in que", 0, |x| x >> 16)?;
420        }
421
422        // Items
423        for (idx, typ) in FortressResourceType::iter().enumerate() {
424            if typ != FortressResourceType::Experience {
425                self.resources.get_mut(typ).production.per_hour_next_lvl =
426                    data.csiget(584 + idx, "max saved next resource", 0)?;
427            }
428
429            self.resources.get_mut(typ).limit =
430                data.csiget(568 + idx, "resource max save", 0)?;
431            self.resources.get_mut(typ).production.last_collectable =
432                data.csiget(562 + idx, "resource in collectable", 0)?;
433            self.resources.get_mut(typ).production.limit =
434                data.csiget(565 + idx, "resource max in store", 0)?;
435            self.resources.get_mut(typ).production.per_hour =
436                data.csiget(574 + idx, "resource per hour", 0)?;
437        }
438
439        self.last_collectable_updated =
440            data.cstget(577, "fortress collection update", server_time)?;
441
442        self.building_upgrade = FortressAction {
443            start: data.cstget(573, "fortress upgrade begin", server_time)?,
444            finish: data.cstget(572, "fortress upgrade end", server_time)?,
445            cost: FortressCost::default(),
446            target: data.cfpget(571, "fortress building upgrade", |x| x - 1)?,
447        };
448
449        self.upgrades = data.csiget(581, "fortress lvl", 0)?;
450        self.honor = data.csiget(582, "fortress honor", 0)?;
451        let fortress_rank: i64 = data.csiget(583, "fortress rank", 0)?;
452        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
453        if fortress_rank > 0 {
454            self.rank = Some(fortress_rank as u32);
455        } else {
456            self.rank = None;
457        }
458
459        self.gem_search.start =
460            data.cstget(596, "gem search start", server_time)?;
461        self.gem_search.finish =
462            data.cstget(595, "gem search end", server_time)?;
463        self.gem_search.target =
464            GemType::parse(data.cget(594, "gem target")?, 0);
465
466        self.attack_target = data.cwiget(587, "fortress enemy")?;
467        self.attack_free_reroll =
468            data.cstget(586, "fortress attack reroll", server_time)?;
469
470        // Secret storage
471        self.secret_storage_wood =
472            data.csiget(698, "secret storage wood", 0)?;
473        self.secret_storage_stone =
474            data.csiget(700, "secret storage stone", 0)?;
475
476        Ok(())
477    }
478
479    pub(crate) fn update_unit_prices(
480        &mut self,
481        data: &[i64],
482    ) -> Result<(), SFError> {
483        for (i, typ) in FortressUnitType::iter().enumerate() {
484            self.units.get_mut(typ).training.cost =
485                FortressCost::parse(data.skip(i * 4, "unit prices")?)?;
486        }
487        Ok(())
488    }
489
490    pub(crate) fn update_unit_upgrade_info(
491        &mut self,
492        data: &[i64],
493    ) -> Result<(), SFError> {
494        for (i, typ) in FortressUnitType::iter().enumerate() {
495            self.units.get_mut(typ).upgrade_next_lvl =
496                data.csiget(i * 3, "unit next lvl", 0)?;
497            self.units.get_mut(typ).upgrade_cost.wood =
498                data.csiget(1 + i * 3, "wood price next unit lvl", 0)?;
499            self.units.get_mut(typ).upgrade_cost.stone =
500                data.csiget(2 + i * 3, "stone price next unit lvl", 0)?;
501        }
502        Ok(())
503    }
504
505    pub(crate) fn update_levels(
506        &mut self,
507        data: &[i64],
508    ) -> Result<(), SFError> {
509        self.units.get_mut(FortressUnitType::Soldier).level =
510            data.csiget(1, "soldier level", 0)?;
511        self.units.get_mut(FortressUnitType::Magician).level =
512            data.csiget(2, "magician level", 0)?;
513        self.units.get_mut(FortressUnitType::Archer).level =
514            data.csiget(3, "archer level", 0)?;
515        Ok(())
516    }
517
518    pub(crate) fn update_prices(
519        &mut self,
520        data: &[i64],
521    ) -> Result<(), SFError> {
522        for (i, typ) in FortressBuildingType::iter().enumerate() {
523            self.buildings.get_mut(typ).upgrade_cost =
524                FortressCost::parse(data.skip(i * 4, "fortress unit prices")?)?;
525        }
526        self.gem_search.cost =
527            FortressCost::parse(data.skip(48, "gem_search_cost")?)?;
528        Ok(())
529    }
530}