Skip to main content

wows_core/
game_constants.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::sync::LazyLock;
4
5use crate::game_types::BuoyancyState;
6use crate::game_types::CameraMode;
7use crate::game_types::CollisionType;
8use crate::game_types::Consumable;
9use crate::game_types::ControlPointType;
10use crate::game_types::DamageStatCategory;
11use crate::game_types::DamageStatWeapon;
12use crate::game_types::DeathCause;
13use crate::game_types::FinishType;
14use crate::game_types::InteractiveZoneType;
15use crate::game_types::ShellHitType;
16use crate::game_types::WeaponType;
17
18/// Default battle constants (hardcoded, no game files needed).
19pub static DEFAULT_BATTLE_CONSTANTS: LazyLock<BattleConstants> = LazyLock::new(BattleConstants::defaults);
20
21/// Default ships constants (hardcoded, no game files needed).
22pub static DEFAULT_SHIPS_CONSTANTS: LazyLock<ShipsConstants> = LazyLock::new(ShipsConstants::defaults);
23
24/// Default common constants (hardcoded, no game files needed).
25pub static DEFAULT_COMMON_CONSTANTS: LazyLock<CommonConstants> = LazyLock::new(CommonConstants::defaults);
26
27/// Constants parsed from `gui/data/constants/battle.xml`.
28#[derive(Clone)]
29pub struct BattleConstants {
30    camera_modes: HashMap<i32, Cow<'static, str>>,
31    death_reasons: HashMap<i32, Cow<'static, str>>,
32    game_modes: HashMap<i32, Cow<'static, str>>,
33    battle_results: HashMap<i32, Cow<'static, str>>,
34    player_relations: HashMap<i32, Cow<'static, str>>,
35    damage_modules: HashMap<i32, Cow<'static, str>>,
36    finish_types: HashMap<i32, Cow<'static, str>>,
37    consumable_states: HashMap<i32, Cow<'static, str>>,
38    planes_types: HashMap<i32, Cow<'static, str>>,
39    diplomacy_relations: HashMap<i32, Cow<'static, str>>,
40    modules_states: HashMap<i32, Cow<'static, str>>,
41    entity_types: HashMap<i32, Cow<'static, str>>,
42    entity_states: HashMap<i32, Cow<'static, str>>,
43    battery_states: HashMap<i32, Cow<'static, str>>,
44    depth_states: HashMap<i32, Cow<'static, str>>,
45    building_types: HashMap<i32, Cow<'static, str>>,
46    torpedo_marker_types: HashMap<i32, Cow<'static, str>>,
47    interactive_zone_types: HashMap<i32, Cow<'static, str>>,
48    control_point_types: HashMap<i32, Cow<'static, str>>,
49    damage_stat_weapons: HashMap<i32, Cow<'static, str>>,
50    damage_stat_categories: HashMap<i32, Cow<'static, str>>,
51}
52
53impl BattleConstants {
54    /// Load from game files, falling back to defaults if the file can't be read.
55    /// Parse from raw XML bytes. Falls back to defaults per-field on parse failure.
56    #[cfg(feature = "parsing")]
57    pub fn from_xml(xml: &[u8]) -> Self {
58        let xml_str = match std::str::from_utf8(xml) {
59            Ok(s) => s,
60            Err(_) => return Self::defaults(),
61        };
62
63        let defaults = Self::defaults();
64        Self {
65            camera_modes: parse_integer_enum(xml_str, "CAMERA_MODE").unwrap_or(defaults.camera_modes),
66            death_reasons: parse_positional_enum(xml_str, "DEATH_REASON_NAME").unwrap_or(defaults.death_reasons),
67            game_modes: parse_integer_enum(xml_str, "GAME_MODE").unwrap_or(defaults.game_modes),
68            battle_results: parse_integer_enum(xml_str, "BATTLE_RESULT").unwrap_or(defaults.battle_results),
69            player_relations: parse_integer_enum(xml_str, "PLAYER_RELATION").unwrap_or(defaults.player_relations),
70            damage_modules: parse_integer_enum(xml_str, "DAMAGE_MODULES").unwrap_or(defaults.damage_modules),
71            finish_types: parse_integer_enum(xml_str, "FINISH_TYPE").unwrap_or(defaults.finish_types),
72            consumable_states: parse_integer_enum(xml_str, "CONSUMABLE_STATES").unwrap_or(defaults.consumable_states),
73            planes_types: parse_integer_enum(xml_str, "PLANES_TYPES").unwrap_or(defaults.planes_types),
74            diplomacy_relations: parse_integer_enum(xml_str, "DIPLOMACY_RELATIONS")
75                .unwrap_or(defaults.diplomacy_relations),
76            modules_states: parse_integer_enum(xml_str, "MODULES_STATES").unwrap_or(defaults.modules_states),
77            entity_types: parse_integer_enum(xml_str, "ENTITY_TYPES").unwrap_or(defaults.entity_types),
78            entity_states: parse_integer_enum(xml_str, "ENTITY_STATES").unwrap_or(defaults.entity_states),
79            battery_states: parse_integer_enum(xml_str, "BATTERY_STATE").unwrap_or(defaults.battery_states),
80            depth_states: parse_integer_enum(xml_str, "DEPTH_STATE").unwrap_or(defaults.depth_states),
81            building_types: parse_positional_enum(xml_str, "BUILDING_TYPES").unwrap_or(defaults.building_types),
82            torpedo_marker_types: parse_positional_enum(xml_str, "TORPEDO_MARKER_TYPE")
83                .unwrap_or(defaults.torpedo_marker_types),
84            interactive_zone_types: defaults.interactive_zone_types,
85            control_point_types: defaults.control_point_types,
86            damage_stat_weapons: defaults.damage_stat_weapons,
87            damage_stat_categories: defaults.damage_stat_categories,
88        }
89    }
90
91    /// Hardcoded defaults matching known game versions (v15.0/v15.1).
92    pub fn defaults() -> Self {
93        Self {
94            camera_modes: HashMap::from([
95                (1, Cow::Borrowed(CameraMode::Airplanes.name())),
96                (2, Cow::Borrowed(CameraMode::Dock.name())),
97                (3, Cow::Borrowed(CameraMode::OverheadMap.name())),
98                (4, Cow::Borrowed(CameraMode::DevFree.name())),
99                (5, Cow::Borrowed(CameraMode::FollowingShells.name())),
100                (6, Cow::Borrowed(CameraMode::FollowingPlanes.name())),
101                (7, Cow::Borrowed(CameraMode::DockModule.name())),
102                (8, Cow::Borrowed(CameraMode::FollowingShip.name())),
103                (9, Cow::Borrowed(CameraMode::FreeFlying.name())),
104                (10, Cow::Borrowed(CameraMode::ReplayFpc.name())),
105                (11, Cow::Borrowed(CameraMode::FollowingSubmarine.name())),
106                (12, Cow::Borrowed(CameraMode::TacticalConsumables.name())),
107                (13, Cow::Borrowed(CameraMode::RespawnMap.name())),
108                (19, Cow::Borrowed(CameraMode::DockFlags.name())),
109                (20, Cow::Borrowed(CameraMode::DockEnsign.name())),
110                (21, Cow::Borrowed(CameraMode::DockLootbox.name())),
111                (22, Cow::Borrowed(CameraMode::DockNavalFlag.name())),
112                (23, Cow::Borrowed(CameraMode::IdleGame.name())),
113            ]),
114            death_reasons: HashMap::from([
115                (0, Cow::Borrowed(DeathCause::None.name())),
116                (1, Cow::Borrowed(DeathCause::Artillery.name())),
117                (2, Cow::Borrowed(DeathCause::Secondaries.name())),
118                (3, Cow::Borrowed(DeathCause::Torpedo.name())),
119                (4, Cow::Borrowed(DeathCause::DiveBomber.name())),
120                (5, Cow::Borrowed(DeathCause::AerialTorpedo.name())),
121                (6, Cow::Borrowed(DeathCause::Fire.name())),
122                (7, Cow::Borrowed(DeathCause::Ramming.name())),
123                (8, Cow::Borrowed(DeathCause::Terrain.name())),
124                (9, Cow::Borrowed(DeathCause::Flooding.name())),
125                (10, Cow::Borrowed(DeathCause::Mirror.name())),
126                (11, Cow::Borrowed(DeathCause::SeaMine.name())),
127                (12, Cow::Borrowed(DeathCause::Special.name())),
128                (13, Cow::Borrowed(DeathCause::DepthCharge.name())),
129                (14, Cow::Borrowed(DeathCause::AerialRocket.name())),
130                (15, Cow::Borrowed(DeathCause::Detonation.name())),
131                (16, Cow::Borrowed(DeathCause::Health.name())),
132                (17, Cow::Borrowed(DeathCause::ApShell.name())),
133                (18, Cow::Borrowed(DeathCause::HeShell.name())),
134                (19, Cow::Borrowed(DeathCause::CsShell.name())),
135                (20, Cow::Borrowed(DeathCause::Fel.name())),
136                (21, Cow::Borrowed(DeathCause::Portal.name())),
137                (22, Cow::Borrowed(DeathCause::SkipBombs.name())),
138                (23, Cow::Borrowed(DeathCause::SectorWave.name())),
139                (24, Cow::Borrowed(DeathCause::Acid.name())),
140                (25, Cow::Borrowed(DeathCause::Laser.name())),
141                (26, Cow::Borrowed(DeathCause::Match.name())),
142                (27, Cow::Borrowed(DeathCause::Timer.name())),
143                (28, Cow::Borrowed(DeathCause::AerialDepthCharge.name())),
144                (29, Cow::Borrowed(DeathCause::Event1.name())),
145                (30, Cow::Borrowed(DeathCause::Event2.name())),
146                (31, Cow::Borrowed(DeathCause::Event3.name())),
147                (32, Cow::Borrowed(DeathCause::Event4.name())),
148                (33, Cow::Borrowed(DeathCause::Event5.name())),
149                (34, Cow::Borrowed(DeathCause::Event6.name())),
150                (35, Cow::Borrowed(DeathCause::Missile.name())),
151            ]),
152            game_modes: HashMap::from([
153                (-1, Cow::Borrowed("INVALID")),
154                (0, Cow::Borrowed("TEST")),
155                (1, Cow::Borrowed("STANDART")),
156                (2, Cow::Borrowed("SINGLEBASE")),
157                (7, Cow::Borrowed("DOMINATION")),
158                (8, Cow::Borrowed("TUTORIAL")),
159                (9, Cow::Borrowed("MEGABASE")),
160                (10, Cow::Borrowed("FORTS")),
161                (11, Cow::Borrowed("STANDARD_DOMINATION")),
162                (12, Cow::Borrowed("EPICENTER")),
163                (13, Cow::Borrowed("ASSAULT_DEFENSE")),
164                (14, Cow::Borrowed("PVE")),
165                (15, Cow::Borrowed("ARMS_RACE")),
166                (16, Cow::Borrowed("EPICENTER_RING")),
167                (17, Cow::Borrowed("ANTI_STANDARD")),
168                (18, Cow::Borrowed("ATTACK_DEFENSE")),
169                (19, Cow::Borrowed("TORPEDO_BEAT")),
170                (20, Cow::Borrowed("TEAM_BATTLE_ROYALE")),
171                (21, Cow::Borrowed("ESCAPE_TO_PORTAL")),
172                (22, Cow::Borrowed("DOMINATION_ASYMM")),
173                (23, Cow::Borrowed("KEY_BATTLE")),
174                (24, Cow::Borrowed("PORTAL_2021")),
175                (25, Cow::Borrowed("TEAM_BATTLE_ROYALE_2021")),
176                (26, Cow::Borrowed("CONVOY_EVENT")),
177                (27, Cow::Borrowed("CONVOY_AIRSHIP")),
178                (28, Cow::Borrowed("TWO_TEAMS_BATTLE_ROYALE")),
179                (29, Cow::Borrowed("PINATA_EVENT")),
180                (30, Cow::Borrowed("RESPAWNS")),
181                (31, Cow::Borrowed("RESPAWNS_SECTORS")),
182            ]),
183            battle_results: HashMap::from([
184                (0, Cow::Borrowed("DEFEAT")),
185                (1, Cow::Borrowed("VICTORY")),
186                (2, Cow::Borrowed("DRAW")),
187                (3, Cow::Borrowed("SUCCESS")),
188                (4, Cow::Borrowed("FAILURE")),
189                (5, Cow::Borrowed("PORTAL")),
190                (6, Cow::Borrowed("MATCH")),
191                (7, Cow::Borrowed("DEATH")),
192                (8, Cow::Borrowed("TEAM_LADDER_WINNER")),
193                (9, Cow::Borrowed("TEAM_LADDER_LOSER")),
194            ]),
195            player_relations: HashMap::from([
196                (0, Cow::Borrowed("SELF")),
197                (1, Cow::Borrowed("ALLY")),
198                (2, Cow::Borrowed("ENEMY")),
199                (3, Cow::Borrowed("NEUTRAL")),
200            ]),
201            damage_modules: HashMap::from([
202                (0, Cow::Borrowed("ENGINE")),
203                (1, Cow::Borrowed("MAIN_CALIBER")),
204                (2, Cow::Borrowed("ATBA_GUN")),
205                (3, Cow::Borrowed("AVIATION")),
206                (4, Cow::Borrowed("AIR_DEFENSE")),
207                (5, Cow::Borrowed("OBSERVATION")),
208                (6, Cow::Borrowed("TORPEDO_TUBE")),
209                (7, Cow::Borrowed("PATH_CONTROL")),
210                (8, Cow::Borrowed("DEPTH_CHARGE_GUN")),
211                (9, Cow::Borrowed("BURN")),
212                (10, Cow::Borrowed("FLOOD")),
213                (11, Cow::Borrowed("ACID")),
214                (12, Cow::Borrowed("HEATED")),
215                (13, Cow::Borrowed("WAVED")),
216                (14, Cow::Borrowed("PINGER")),
217                (15, Cow::Borrowed("OIL_LEAK")),
218                (16, Cow::Borrowed("OIL_LEAK_PENDING")),
219                (17, Cow::Borrowed("WILD_FIRE")),
220            ]),
221            finish_types: HashMap::from([
222                (0, Cow::Borrowed(FinishType::Unknown.name())),
223                (1, Cow::Borrowed(FinishType::Extermination.name())),
224                (2, Cow::Borrowed(FinishType::BaseCaptured.name())),
225                (3, Cow::Borrowed(FinishType::Timeout.name())),
226                (4, Cow::Borrowed(FinishType::Failure.name())),
227                (5, Cow::Borrowed(FinishType::Technical.name())),
228                (8, Cow::Borrowed(FinishType::Score.name())),
229                (9, Cow::Borrowed(FinishType::ScoreOnTimeout.name())),
230                (10, Cow::Borrowed(FinishType::PveMainTaskSucceeded.name())),
231                (11, Cow::Borrowed(FinishType::PveMainTaskFailed.name())),
232                (12, Cow::Borrowed(FinishType::ScoreZero.name())),
233                (13, Cow::Borrowed(FinishType::ScoreExcess.name())),
234            ]),
235            consumable_states: HashMap::from([
236                (0, Cow::Borrowed("READY")),
237                (1, Cow::Borrowed("SELECTED")),
238                (2, Cow::Borrowed("AT_WORK")),
239                (3, Cow::Borrowed("RELOAD")),
240                (4, Cow::Borrowed("NO_AMMO")),
241                (5, Cow::Borrowed("PREPARATION")),
242                (6, Cow::Borrowed("REGENERATION")),
243            ]),
244            planes_types: HashMap::from([
245                (0, Cow::Borrowed("SCOUT")),
246                (1, Cow::Borrowed("DIVEBOMBER")),
247                (2, Cow::Borrowed("TORPEDOBOMBER")),
248                (3, Cow::Borrowed("FIGHTER")),
249                (4, Cow::Borrowed("AUXILIARY")),
250                (5, Cow::Borrowed("SKIP_BOMBER")),
251                (6, Cow::Borrowed("AIR_SUPPORT")),
252                (7, Cow::Borrowed("AIRSHIP")),
253            ]),
254            diplomacy_relations: HashMap::from([
255                (0, Cow::Borrowed("SELF")),
256                (1, Cow::Borrowed("ALLY")),
257                (2, Cow::Borrowed("NEUTRAL")),
258                (3, Cow::Borrowed("ENEMY")),
259                (4, Cow::Borrowed("AGGRESSOR")),
260            ]),
261            modules_states: HashMap::from([
262                (0, Cow::Borrowed("NORMAL")),
263                (1, Cow::Borrowed("DAMAGED")),
264                (2, Cow::Borrowed("CRIT")),
265                (3, Cow::Borrowed("BROKEN")),
266                (4, Cow::Borrowed("DEAD")),
267            ]),
268            entity_types: HashMap::from([
269                (-1, Cow::Borrowed("INVALID")),
270                (0, Cow::Borrowed("SHIP")),
271                (1, Cow::Borrowed("PLANE")),
272                (2, Cow::Borrowed("TORPEDO")),
273                (3, Cow::Borrowed("BUILDING")),
274                (11, Cow::Borrowed("CAPTURE_POINT")),
275                (12, Cow::Borrowed("PLAYER")),
276                (13, Cow::Borrowed("EPICENTER")),
277                (14, Cow::Borrowed("SCENARIO_OBJECT")),
278                (15, Cow::Borrowed("DROP_ZONE")),
279                (16, Cow::Borrowed("ATTENTION_POINT")),
280                (17, Cow::Borrowed("KEY_OBJECT")),
281                (18, Cow::Borrowed("INTERACTIVE_ZONE")),
282                (27, Cow::Borrowed("PINATA_SHIP")),
283                (28, Cow::Borrowed("STARTREK_SCRAP")),
284                (29, Cow::Borrowed("MISSILE")),
285                (99, Cow::Borrowed("NAVPOINT")),
286                (100, Cow::Borrowed("EMPTY")),
287            ]),
288            entity_states: HashMap::from([
289                (0, Cow::Borrowed("EMPTY")),
290                (1, Cow::Borrowed("REPAIR")),
291                (2, Cow::Borrowed("GATHERING_SURVIVORS")),
292                (3, Cow::Borrowed("FILTH")),
293                (4, Cow::Borrowed("FROZEN")),
294                (5, Cow::Borrowed("UNLOADING_MARINES")),
295                (6, Cow::Borrowed("INSIDE_WEATHER")),
296                (7, Cow::Borrowed("NEAR_WEATHER")),
297                (8, Cow::Borrowed("ILLUMINATED")),
298                (9, Cow::Borrowed("BY_NIGHT")),
299                (10, Cow::Borrowed("INSIDE_MINEFIELD")),
300                (11, Cow::Borrowed("NEAR_MINEFIELD")),
301                (12, Cow::Borrowed("CAPTURING_LOCKED")),
302                (13, Cow::Borrowed("DANGER")),
303                (14, Cow::Borrowed("TEAM_01")),
304                (15, Cow::Borrowed("TEAM_02")),
305                (16, Cow::Borrowed("TEAM_03")),
306                (17, Cow::Borrowed("TEAM_11")),
307                (18, Cow::Borrowed("TEAM_12")),
308                (19, Cow::Borrowed("TEAM_13")),
309                (20, Cow::Borrowed("TEAM_21")),
310                (21, Cow::Borrowed("TEAM_22")),
311                (22, Cow::Borrowed("TEAM_23")),
312                (23, Cow::Borrowed("TEAM_31")),
313                (24, Cow::Borrowed("TEAM_32")),
314                (25, Cow::Borrowed("TEAM_33")),
315                (26, Cow::Borrowed("SPOTTED_BY_ENEMY")),
316                (27, Cow::Borrowed("INVULNERABLE")),
317                (28, Cow::Borrowed("FLAGSHIP")),
318                (29, Cow::Borrowed("MIGNON")),
319                (30, Cow::Borrowed("REPAIR_SHIP")),
320                (100, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_BROKEN_MODULES")),
321                (101, Cow::Borrowed("BURN")),
322                (102, Cow::Borrowed("FLOOD")),
323                (103, Cow::Borrowed("HOLD_RESOURCE")),
324                (104, Cow::Borrowed("HOLD_RESOURCE_FILTHIOR_TEAM_0")),
325                (105, Cow::Borrowed("HOLD_RESOURCE_FILTHIOR_TEAM_1")),
326                (106, Cow::Borrowed("HOLD_RESOURCE_FILTHIOR_TEAM_2")),
327                (107, Cow::Borrowed("HOLD_RESOURCE_FILTHIOR_TEAM_3")),
328                (108, Cow::Borrowed("HOLD_RESOURCE_POINTS")),
329                (109, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_CRIT_MODULES")),
330                (110, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_SPECIAL_MODULES")),
331                (111, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_MODIFIERS")),
332                (112, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_TALENTS")),
333                (113, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_ATBA_ACCURACY_PERK")),
334                (114, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_PERKS")),
335                (115, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_BUFFS")),
336                (116, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_WEATHER")),
337                (117, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_INTERACTIVE_ZONE")),
338                (118, Cow::Borrowed("EMERGENCY_SURFACING")),
339                (119, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_BATTERY_STATE")),
340                (120, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_DEPTH")),
341                (121, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_ARTILLERY_FIRE_MODE")),
342                (122, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_RAGE_MODE")),
343                (123, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_CONSUMABLES")),
344                (124, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_NOT_SPECIAL_MODULES")),
345                (125, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_CONSUMABLE_LOCKER")),
346                (126, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_ANTI_ABUSE_SYSTEM")),
347                (127, Cow::Borrowed("BATTLE_CARD_SELECTOR_AVAILABLE")),
348                (128, Cow::Borrowed("SHIP_PARAMS_CHANGE_BY_INNATE_SKILLS")),
349            ]),
350            battery_states: HashMap::from([
351                (0, Cow::Borrowed("IDLE")),
352                (1, Cow::Borrowed("CHARGING")),
353                (2, Cow::Borrowed("FROZEN")),
354                (3, Cow::Borrowed("SPENDING_NORMAL")),
355                (4, Cow::Borrowed("SPENDING_WARNING")),
356                (5, Cow::Borrowed("SPENDING_CRITICAL")),
357                (6, Cow::Borrowed("BURNING")),
358                (7, Cow::Borrowed("EMPTY")),
359            ]),
360            depth_states: HashMap::from([
361                (-1, Cow::Borrowed(BuoyancyState::Invalid.name())),
362                (0, Cow::Borrowed(BuoyancyState::Surface.name())),
363                (1, Cow::Borrowed(BuoyancyState::Periscope.name())),
364                (2, Cow::Borrowed(BuoyancyState::SemiDeepWater.name())),
365                (3, Cow::Borrowed(BuoyancyState::DeepWater.name())),
366                (4, Cow::Borrowed(BuoyancyState::DeepWaterInvul.name())),
367            ]),
368            building_types: HashMap::from([
369                (0, Cow::Borrowed("ANTI_AIRCRAFT")),
370                (1, Cow::Borrowed("AIR_BASE")),
371                (2, Cow::Borrowed("COASTAL_ARTILLERY")),
372                (3, Cow::Borrowed("MILITARY")),
373                (4, Cow::Borrowed("SENSOR_TOWER")),
374                (5, Cow::Borrowed("COMPLEX")),
375                (6, Cow::Borrowed("RAY_TOWER")),
376                (7, Cow::Borrowed("GENERATOR")),
377                (8, Cow::Borrowed("SPACE_STATION")),
378            ]),
379            torpedo_marker_types: HashMap::from([
380                (0, Cow::Borrowed("NORMAL")),
381                (1, Cow::Borrowed("DEEP_WATER")),
382                (2, Cow::Borrowed("ACOUSTIC")),
383                (3, Cow::Borrowed("MAGNETIC")),
384                (4, Cow::Borrowed("NOT_DANGEROUS")),
385            ]),
386            interactive_zone_types: HashMap::from([
387                (0, Cow::Borrowed(InteractiveZoneType::NoType.name())),
388                (1, Cow::Borrowed(InteractiveZoneType::ResourceZone.name())),
389                (2, Cow::Borrowed(InteractiveZoneType::ConvoyZone.name())),
390                (3, Cow::Borrowed(InteractiveZoneType::RepairZone.name())),
391                (4, Cow::Borrowed(InteractiveZoneType::FelZone.name())),
392                (5, Cow::Borrowed(InteractiveZoneType::WeatherZone.name())),
393                (6, Cow::Borrowed(InteractiveZoneType::DropZone.name())),
394                (7, Cow::Borrowed(InteractiveZoneType::ConsumableZone.name())),
395                (8, Cow::Borrowed(InteractiveZoneType::ColoredByRelation.name())),
396                (9, Cow::Borrowed(InteractiveZoneType::ControlPoint.name())),
397                (10, Cow::Borrowed(InteractiveZoneType::RescueZone.name())),
398                (11, Cow::Borrowed(InteractiveZoneType::OrbitalStrikeZone.name())),
399            ]),
400            control_point_types: HashMap::from([
401                (1, Cow::Borrowed(ControlPointType::Control.name())),
402                (2, Cow::Borrowed(ControlPointType::Base.name())),
403                (3, Cow::Borrowed(ControlPointType::MegaBase.name())),
404                (4, Cow::Borrowed(ControlPointType::BuildingCp.name())),
405                (5, Cow::Borrowed(ControlPointType::BaseWithPoints.name())),
406                (6, Cow::Borrowed(ControlPointType::EpicenterCp.name())),
407            ]),
408            // DamageStatWeapon: from mc15a2792.pyc enum_weapon = idGenerator(0), game version 15.1
409            damage_stat_weapons: HashMap::from([
410                (0, Cow::Borrowed(DamageStatWeapon::Default.name())),
411                (1, Cow::Borrowed(DamageStatWeapon::MainAp.name())),
412                (2, Cow::Borrowed(DamageStatWeapon::MainHe.name())),
413                (3, Cow::Borrowed(DamageStatWeapon::AtbaAp.name())),
414                (4, Cow::Borrowed(DamageStatWeapon::AtbaHe.name())),
415                (5, Cow::Borrowed(DamageStatWeapon::MainAiAp.name())),
416                (6, Cow::Borrowed(DamageStatWeapon::MainAiHe.name())),
417                (7, Cow::Borrowed(DamageStatWeapon::Torpedo.name())),
418                (8, Cow::Borrowed(DamageStatWeapon::Antiair.name())),
419                (9, Cow::Borrowed(DamageStatWeapon::Scout.name())),
420                (10, Cow::Borrowed(DamageStatWeapon::BomberAp.name())),
421                (11, Cow::Borrowed(DamageStatWeapon::BomberHe.name())),
422                (12, Cow::Borrowed(DamageStatWeapon::TBomber.name())),
423                (13, Cow::Borrowed(DamageStatWeapon::Fighter.name())),
424                (14, Cow::Borrowed(DamageStatWeapon::SFighter.name())),
425                (15, Cow::Borrowed(DamageStatWeapon::Turret.name())),
426                (16, Cow::Borrowed(DamageStatWeapon::Spot.name())),
427                (17, Cow::Borrowed(DamageStatWeapon::Burn.name())),
428                (18, Cow::Borrowed(DamageStatWeapon::Ram.name())),
429                (19, Cow::Borrowed(DamageStatWeapon::Terrain.name())),
430                (20, Cow::Borrowed(DamageStatWeapon::Flood.name())),
431                (21, Cow::Borrowed(DamageStatWeapon::Mirror.name())),
432                (22, Cow::Borrowed(DamageStatWeapon::Radar.name())),
433                (23, Cow::Borrowed(DamageStatWeapon::Xray.name())),
434                (24, Cow::Borrowed(DamageStatWeapon::ConsSpot.name())),
435                (25, Cow::Borrowed(DamageStatWeapon::SeaMine.name())),
436                (26, Cow::Borrowed(DamageStatWeapon::Fel.name())),
437                (27, Cow::Borrowed(DamageStatWeapon::DepthCharge.name())),
438                (28, Cow::Borrowed(DamageStatWeapon::RocketHe.name())),
439                (29, Cow::Borrowed(DamageStatWeapon::AaNear.name())),
440                (30, Cow::Borrowed(DamageStatWeapon::AaMedium.name())),
441                (31, Cow::Borrowed(DamageStatWeapon::AaFar.name())),
442                (32, Cow::Borrowed(DamageStatWeapon::MainCs.name())),
443                (33, Cow::Borrowed(DamageStatWeapon::AtbaCs.name())),
444                (34, Cow::Borrowed(DamageStatWeapon::Portal.name())),
445                (35, Cow::Borrowed(DamageStatWeapon::TorpedoAcc.name())),
446                (36, Cow::Borrowed(DamageStatWeapon::TorpedoMag.name())),
447                (37, Cow::Borrowed(DamageStatWeapon::Ping.name())),
448                (38, Cow::Borrowed(DamageStatWeapon::PingSlow.name())),
449                (39, Cow::Borrowed(DamageStatWeapon::PingFast.name())),
450                (40, Cow::Borrowed(DamageStatWeapon::TorpedoAccOff.name())),
451                (41, Cow::Borrowed(DamageStatWeapon::RocketAp.name())),
452                (42, Cow::Borrowed(DamageStatWeapon::SkipHe.name())),
453                (43, Cow::Borrowed(DamageStatWeapon::SkipAp.name())),
454                (44, Cow::Borrowed(DamageStatWeapon::Acid.name())),
455                (45, Cow::Borrowed(DamageStatWeapon::SectorWave.name())),
456                (46, Cow::Borrowed(DamageStatWeapon::Match.name())),
457                (47, Cow::Borrowed(DamageStatWeapon::Timer.name())),
458                (48, Cow::Borrowed(DamageStatWeapon::ChargeLaser.name())),
459                (49, Cow::Borrowed(DamageStatWeapon::PulseLaser.name())),
460                (50, Cow::Borrowed(DamageStatWeapon::AxisLaser.name())),
461                (51, Cow::Borrowed(DamageStatWeapon::BomberApAsup.name())),
462                (52, Cow::Borrowed(DamageStatWeapon::BomberHeAsup.name())),
463                (53, Cow::Borrowed(DamageStatWeapon::TBomberAsup.name())),
464                (54, Cow::Borrowed(DamageStatWeapon::RocketHeAsup.name())),
465                (55, Cow::Borrowed(DamageStatWeapon::RocketApAsup.name())),
466                (56, Cow::Borrowed(DamageStatWeapon::SkipHeAsup.name())),
467                (57, Cow::Borrowed(DamageStatWeapon::SkipApAsup.name())),
468                (58, Cow::Borrowed(DamageStatWeapon::DepthChargeAsup.name())),
469                (59, Cow::Borrowed(DamageStatWeapon::TorpedoDeep.name())),
470                (60, Cow::Borrowed(DamageStatWeapon::TorpedoAlter.name())),
471                (61, Cow::Borrowed(DamageStatWeapon::AirSupport.name())),
472                (62, Cow::Borrowed(DamageStatWeapon::BomberApAlter.name())),
473                (63, Cow::Borrowed(DamageStatWeapon::BomberHeAlter.name())),
474                (64, Cow::Borrowed(DamageStatWeapon::TBomberAlter.name())),
475                (65, Cow::Borrowed(DamageStatWeapon::RocketHeAlter.name())),
476                (66, Cow::Borrowed(DamageStatWeapon::RocketApAlter.name())),
477                (67, Cow::Borrowed(DamageStatWeapon::SkipHeAlter.name())),
478                (68, Cow::Borrowed(DamageStatWeapon::SkipApAlter.name())),
479                (69, Cow::Borrowed(DamageStatWeapon::DepthChargeAlter.name())),
480                (70, Cow::Borrowed(DamageStatWeapon::Recon.name())),
481                (71, Cow::Borrowed(DamageStatWeapon::BomberApTc.name())),
482                (72, Cow::Borrowed(DamageStatWeapon::BomberHeTc.name())),
483                (73, Cow::Borrowed(DamageStatWeapon::TBomberTc.name())),
484                (74, Cow::Borrowed(DamageStatWeapon::RocketHeTc.name())),
485                (75, Cow::Borrowed(DamageStatWeapon::RocketApTc.name())),
486                (76, Cow::Borrowed(DamageStatWeapon::SkipHeTc.name())),
487                (77, Cow::Borrowed(DamageStatWeapon::SkipApTc.name())),
488                (78, Cow::Borrowed(DamageStatWeapon::DepthChargeTc.name())),
489                (79, Cow::Borrowed(DamageStatWeapon::PhaserLaser.name())),
490                (80, Cow::Borrowed(DamageStatWeapon::Event1.name())),
491                (81, Cow::Borrowed(DamageStatWeapon::Event2.name())),
492                (82, Cow::Borrowed(DamageStatWeapon::TorpedoPhoton.name())),
493                (83, Cow::Borrowed(DamageStatWeapon::Missile.name())),
494                (84, Cow::Borrowed(DamageStatWeapon::AntiMissile.name())),
495            ]),
496            // DamageStatCategory: from DamageStatsType in mc15a2792.pyc, game version 15.1
497            damage_stat_categories: HashMap::from([
498                (0, Cow::Borrowed(DamageStatCategory::Enemy.name())),
499                (1, Cow::Borrowed(DamageStatCategory::Ally.name())),
500                (2, Cow::Borrowed(DamageStatCategory::Spot.name())),
501                (3, Cow::Borrowed(DamageStatCategory::Agro.name())),
502            ]),
503        }
504    }
505
506    pub fn camera_mode(&self, id: i32) -> Option<&str> {
507        self.camera_modes.get(&id).map(|s| s.as_ref())
508    }
509
510    pub fn death_reason(&self, id: i32) -> Option<&str> {
511        self.death_reasons.get(&id).map(|s| s.as_ref())
512    }
513
514    pub fn game_mode(&self, id: i32) -> Option<&str> {
515        self.game_modes.get(&id).map(|s| s.as_ref())
516    }
517
518    pub fn battle_result(&self, id: i32) -> Option<&str> {
519        self.battle_results.get(&id).map(|s| s.as_ref())
520    }
521
522    pub fn player_relation(&self, id: i32) -> Option<&str> {
523        self.player_relations.get(&id).map(|s| s.as_ref())
524    }
525
526    pub fn damage_module(&self, id: i32) -> Option<&str> {
527        self.damage_modules.get(&id).map(|s| s.as_ref())
528    }
529
530    pub fn finish_type(&self, id: i32) -> Option<&str> {
531        self.finish_types.get(&id).map(|s| s.as_ref())
532    }
533
534    pub fn consumable_state(&self, id: i32) -> Option<&str> {
535        self.consumable_states.get(&id).map(|s| s.as_ref())
536    }
537
538    pub fn planes_type(&self, id: i32) -> Option<&str> {
539        self.planes_types.get(&id).map(|s| s.as_ref())
540    }
541
542    pub fn diplomacy_relation(&self, id: i32) -> Option<&str> {
543        self.diplomacy_relations.get(&id).map(|s| s.as_ref())
544    }
545
546    pub fn modules_state(&self, id: i32) -> Option<&str> {
547        self.modules_states.get(&id).map(|s| s.as_ref())
548    }
549
550    pub fn entity_type(&self, id: i32) -> Option<&str> {
551        self.entity_types.get(&id).map(|s| s.as_ref())
552    }
553
554    pub fn entity_state(&self, id: i32) -> Option<&str> {
555        self.entity_states.get(&id).map(|s| s.as_ref())
556    }
557
558    pub fn battery_state(&self, id: i32) -> Option<&str> {
559        self.battery_states.get(&id).map(|s| s.as_ref())
560    }
561
562    pub fn depth_state(&self, id: i32) -> Option<&str> {
563        self.depth_states.get(&id).map(|s| s.as_ref())
564    }
565
566    pub fn building_type(&self, id: i32) -> Option<&str> {
567        self.building_types.get(&id).map(|s| s.as_ref())
568    }
569
570    pub fn torpedo_marker_type(&self, id: i32) -> Option<&str> {
571        self.torpedo_marker_types.get(&id).map(|s| s.as_ref())
572    }
573
574    pub fn interactive_zone_type(&self, id: i32) -> Option<&str> {
575        self.interactive_zone_types.get(&id).map(|s| s.as_ref())
576    }
577
578    pub fn control_point_type(&self, id: i32) -> Option<&str> {
579        self.control_point_types.get(&id).map(|s| s.as_ref())
580    }
581
582    pub fn camera_modes_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
583        &mut self.camera_modes
584    }
585
586    pub fn death_reasons_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
587        &mut self.death_reasons
588    }
589
590    pub fn game_modes_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
591        &mut self.game_modes
592    }
593
594    pub fn battle_results_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
595        &mut self.battle_results
596    }
597
598    pub fn player_relations_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
599        &mut self.player_relations
600    }
601
602    pub fn damage_modules_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
603        &mut self.damage_modules
604    }
605
606    pub fn finish_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
607        &mut self.finish_types
608    }
609
610    pub fn consumable_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
611        &mut self.consumable_states
612    }
613
614    pub fn planes_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
615        &mut self.planes_types
616    }
617
618    pub fn diplomacy_relations_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
619        &mut self.diplomacy_relations
620    }
621
622    pub fn modules_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
623        &mut self.modules_states
624    }
625
626    pub fn entity_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
627        &mut self.entity_types
628    }
629
630    pub fn entity_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
631        &mut self.entity_states
632    }
633
634    pub fn battery_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
635        &mut self.battery_states
636    }
637
638    pub fn depth_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
639        &mut self.depth_states
640    }
641
642    pub fn building_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
643        &mut self.building_types
644    }
645
646    pub fn torpedo_marker_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
647        &mut self.torpedo_marker_types
648    }
649
650    pub fn interactive_zone_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
651        &mut self.interactive_zone_types
652    }
653
654    pub fn control_point_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
655        &mut self.control_point_types
656    }
657
658    pub fn damage_stat_weapon(&self, id: i32) -> Option<&str> {
659        self.damage_stat_weapons.get(&id).map(|s| s.as_ref())
660    }
661
662    pub fn damage_stat_weapons(&self) -> &HashMap<i32, Cow<'static, str>> {
663        &self.damage_stat_weapons
664    }
665
666    pub fn damage_stat_weapons_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
667        &mut self.damage_stat_weapons
668    }
669
670    pub fn damage_stat_category(&self, id: i32) -> Option<&str> {
671        self.damage_stat_categories.get(&id).map(|s| s.as_ref())
672    }
673
674    pub fn damage_stat_categories(&self) -> &HashMap<i32, Cow<'static, str>> {
675        &self.damage_stat_categories
676    }
677
678    pub fn damage_stat_categories_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
679        &mut self.damage_stat_categories
680    }
681
682    pub fn camera_modes(&self) -> &HashMap<i32, Cow<'static, str>> {
683        &self.camera_modes
684    }
685
686    pub fn death_reasons(&self) -> &HashMap<i32, Cow<'static, str>> {
687        &self.death_reasons
688    }
689
690    pub fn game_modes(&self) -> &HashMap<i32, Cow<'static, str>> {
691        &self.game_modes
692    }
693
694    pub fn battle_results(&self) -> &HashMap<i32, Cow<'static, str>> {
695        &self.battle_results
696    }
697
698    pub fn player_relations(&self) -> &HashMap<i32, Cow<'static, str>> {
699        &self.player_relations
700    }
701
702    pub fn damage_modules(&self) -> &HashMap<i32, Cow<'static, str>> {
703        &self.damage_modules
704    }
705
706    pub fn finish_types(&self) -> &HashMap<i32, Cow<'static, str>> {
707        &self.finish_types
708    }
709
710    pub fn consumable_states(&self) -> &HashMap<i32, Cow<'static, str>> {
711        &self.consumable_states
712    }
713
714    pub fn planes_types(&self) -> &HashMap<i32, Cow<'static, str>> {
715        &self.planes_types
716    }
717
718    pub fn diplomacy_relations(&self) -> &HashMap<i32, Cow<'static, str>> {
719        &self.diplomacy_relations
720    }
721
722    pub fn modules_states(&self) -> &HashMap<i32, Cow<'static, str>> {
723        &self.modules_states
724    }
725
726    pub fn entity_types(&self) -> &HashMap<i32, Cow<'static, str>> {
727        &self.entity_types
728    }
729
730    pub fn entity_states(&self) -> &HashMap<i32, Cow<'static, str>> {
731        &self.entity_states
732    }
733
734    pub fn battery_states(&self) -> &HashMap<i32, Cow<'static, str>> {
735        &self.battery_states
736    }
737
738    pub fn depth_states(&self) -> &HashMap<i32, Cow<'static, str>> {
739        &self.depth_states
740    }
741
742    pub fn building_types(&self) -> &HashMap<i32, Cow<'static, str>> {
743        &self.building_types
744    }
745
746    pub fn torpedo_marker_types(&self) -> &HashMap<i32, Cow<'static, str>> {
747        &self.torpedo_marker_types
748    }
749
750    pub fn interactive_zone_types(&self) -> &HashMap<i32, Cow<'static, str>> {
751        &self.interactive_zone_types
752    }
753
754    pub fn control_point_types(&self) -> &HashMap<i32, Cow<'static, str>> {
755        &self.control_point_types
756    }
757}
758
759/// Constants parsed from `gui/data/constants/ships.xml`.
760#[derive(Clone)]
761pub struct ShipsConstants {
762    weapon_types: HashMap<i32, Cow<'static, str>>,
763    module_types: HashMap<i32, Cow<'static, str>>,
764    shell_hit_types: HashMap<i32, Cow<'static, str>>,
765    collision_types: HashMap<i32, Cow<'static, str>>,
766}
767
768impl ShipsConstants {
769    /// Load from game files, falling back to defaults if the file can't be read.
770    /// Parse from raw XML bytes. Falls back to defaults on parse failure.
771    #[cfg(feature = "parsing")]
772    pub fn from_xml(xml: &[u8]) -> Self {
773        let xml_str = match std::str::from_utf8(xml) {
774            Ok(s) => s,
775            Err(_) => return Self::defaults(),
776        };
777
778        let defaults = Self::defaults();
779        Self {
780            weapon_types: parse_integer_enum(xml_str, "SHIP_WEAPON_TYPES").unwrap_or(defaults.weapon_types),
781            module_types: parse_integer_enum(xml_str, "SHIP_MODULE_TYPES").unwrap_or(defaults.module_types),
782            shell_hit_types: defaults.shell_hit_types,
783            collision_types: defaults.collision_types,
784        }
785    }
786
787    /// Hardcoded defaults matching known game versions (v15.0/v15.1).
788    pub fn defaults() -> Self {
789        Self {
790            weapon_types: HashMap::from([
791                (-1, Cow::Borrowed("NONE")),
792                (0, Cow::Borrowed(WeaponType::Artillery.name())),
793                (1, Cow::Borrowed(WeaponType::Secondaries.name())),
794                (2, Cow::Borrowed(WeaponType::Torpedoes.name())),
795                (3, Cow::Borrowed(WeaponType::Planes.name())),
796                (4, Cow::Borrowed("AIRDEFENCE")),
797                (5, Cow::Borrowed("DEPTH_CHARGES")),
798                (6, Cow::Borrowed(WeaponType::Pinger.name())),
799                (7, Cow::Borrowed("CHARGE_LASER")),
800                (8, Cow::Borrowed("IMPULSE_LASER")),
801                (9, Cow::Borrowed("AXIS_LASER")),
802                (10, Cow::Borrowed("PHASER_LASER")),
803                (11, Cow::Borrowed("WAVES")),
804                (12, Cow::Borrowed("AIR_SUPPORT")),
805                (13, Cow::Borrowed("ANTI_MISSILE")),
806                (14, Cow::Borrowed("MISSILES")),
807                (100, Cow::Borrowed("SQUADRON")),
808                (200, Cow::Borrowed("PULSE_PHASERS")),
809            ]),
810            module_types: HashMap::from([
811                (0, Cow::Borrowed("ARTILLERY")),
812                (1, Cow::Borrowed("HULL")),
813                (2, Cow::Borrowed("TORPEDOES")),
814                (3, Cow::Borrowed("SUO")),
815                (4, Cow::Borrowed("ENGINE")),
816                (5, Cow::Borrowed("TORPEDO_BOMBER")),
817                (6, Cow::Borrowed("DIVE_BOMBER")),
818                (7, Cow::Borrowed("FIGHTER")),
819                (8, Cow::Borrowed("FLIGHT_CONTROLL")),
820                (9, Cow::Borrowed("HYDROPHONE")),
821                (10, Cow::Borrowed("SKIP_BOMBER")),
822                (11, Cow::Borrowed("PRIMARY_WEAPONS")),
823                (12, Cow::Borrowed("SECONDARY_WEAPONS")),
824                (13, Cow::Borrowed("ABILITIES")),
825            ]),
826            shell_hit_types: HashMap::from([
827                (0, Cow::Borrowed(ShellHitType::Normal.name())),
828                (1, Cow::Borrowed(ShellHitType::Ricochet.name())),
829                (2, Cow::Borrowed(ShellHitType::MajorHit.name())),
830                (3, Cow::Borrowed(ShellHitType::NoPenetration.name())),
831                (4, Cow::Borrowed(ShellHitType::Overpenetration.name())),
832                (5, Cow::Borrowed(ShellHitType::None.name())),
833                (6, Cow::Borrowed(ShellHitType::ExitOverpenetration.name())),
834                (7, Cow::Borrowed(ShellHitType::Underwater.name())),
835            ]),
836            collision_types: HashMap::from([
837                (0, Cow::Borrowed(CollisionType::NoHit.name())),
838                (1, Cow::Borrowed(CollisionType::HitWater.name())),
839                (2, Cow::Borrowed(CollisionType::HitGround.name())),
840                (3, Cow::Borrowed(CollisionType::HitEntity.name())),
841                (4, Cow::Borrowed(CollisionType::HitEntityBB.name())),
842                (5, Cow::Borrowed(CollisionType::HitWave.name())),
843            ]),
844        }
845    }
846
847    pub fn weapon_type(&self, id: i32) -> Option<&str> {
848        self.weapon_types.get(&id).map(|s| s.as_ref())
849    }
850
851    pub fn module_type(&self, id: i32) -> Option<&str> {
852        self.module_types.get(&id).map(|s| s.as_ref())
853    }
854
855    pub fn weapon_types(&self) -> &HashMap<i32, Cow<'static, str>> {
856        &self.weapon_types
857    }
858
859    pub fn module_types(&self) -> &HashMap<i32, Cow<'static, str>> {
860        &self.module_types
861    }
862
863    pub fn weapon_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
864        &mut self.weapon_types
865    }
866
867    pub fn module_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
868        &mut self.module_types
869    }
870
871    pub fn shell_hit_type(&self, id: i32) -> Option<&str> {
872        self.shell_hit_types.get(&id).map(|s| s.as_ref())
873    }
874
875    pub fn collision_type(&self, id: i32) -> Option<&str> {
876        self.collision_types.get(&id).map(|s| s.as_ref())
877    }
878
879    pub fn shell_hit_types(&self) -> &HashMap<i32, Cow<'static, str>> {
880        &self.shell_hit_types
881    }
882
883    pub fn collision_types(&self) -> &HashMap<i32, Cow<'static, str>> {
884        &self.collision_types
885    }
886
887    pub fn shell_hit_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
888        &mut self.shell_hit_types
889    }
890
891    pub fn collision_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
892        &mut self.collision_types
893    }
894}
895
896/// Constants parsed from `gui/data/constants/weapons.xml`.
897#[derive(Clone)]
898pub struct WeaponsConstants {
899    gun_states: HashMap<i32, Cow<'static, str>>,
900}
901
902impl WeaponsConstants {
903    /// Load from game files, falling back to defaults if the file can't be read.
904    /// Parse from raw XML bytes. Falls back to defaults on parse failure.
905    #[cfg(feature = "parsing")]
906    pub fn from_xml(xml: &[u8]) -> Self {
907        let xml_str = match std::str::from_utf8(xml) {
908            Ok(s) => s,
909            Err(_) => return Self::defaults(),
910        };
911
912        let defaults = Self::defaults();
913        Self { gun_states: parse_integer_enum(xml_str, "GUN_STATE").unwrap_or(defaults.gun_states) }
914    }
915
916    /// Hardcoded defaults matching known game versions (v15.0/v15.1).
917    pub fn defaults() -> Self {
918        Self {
919            gun_states: HashMap::from([
920                (1, Cow::Borrowed("READY")),
921                (2, Cow::Borrowed("WORK")),
922                (3, Cow::Borrowed("RELOAD")),
923                (4, Cow::Borrowed("SWITCHING_AMMO")),
924                (5, Cow::Borrowed("RELOAD_STOPPED")),
925                (6, Cow::Borrowed("CHARGE")),
926                (7, Cow::Borrowed("CRITICAL")),
927                (8, Cow::Borrowed("DESTROYED")),
928                (9, Cow::Borrowed("SWITCHING_CRITICAL")),
929                (10, Cow::Borrowed("DISABLED")),
930                (11, Cow::Borrowed("BROKEN")),
931            ]),
932        }
933    }
934
935    pub fn gun_state(&self, id: i32) -> Option<&str> {
936        self.gun_states.get(&id).map(|s| s.as_ref())
937    }
938
939    pub fn gun_states(&self) -> &HashMap<i32, Cow<'static, str>> {
940        &self.gun_states
941    }
942
943    pub fn gun_states_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
944        &mut self.gun_states
945    }
946}
947
948/// Constants parsed from `gui/data/constants/common.xml`.
949#[derive(Clone)]
950pub struct CommonConstants {
951    plane_ammo_types: HashMap<i32, Cow<'static, str>>,
952    torpedo_types: HashMap<i32, Cow<'static, str>>,
953    consumable_types: HashMap<i32, Cow<'static, str>>,
954    battle_stages: HashMap<i32, crate::game_types::BattleStage>,
955}
956
957impl CommonConstants {
958    /// Load from game files, falling back to defaults if the file can't be read.
959    /// Parse from raw XML bytes. Falls back to defaults on parse failure.
960    #[cfg(feature = "parsing")]
961    pub fn from_xml(xml: &[u8]) -> Self {
962        let xml_str = match std::str::from_utf8(xml) {
963            Ok(s) => s,
964            Err(_) => return Self::defaults(),
965        };
966
967        let defaults = Self::defaults();
968        Self {
969            plane_ammo_types: parse_integer_enum(xml_str, "PLANE_AMMO_TYPES").unwrap_or(defaults.plane_ammo_types),
970            torpedo_types: parse_integer_enum(xml_str, "TORPEDO_TYPE").unwrap_or(defaults.torpedo_types),
971            consumable_types: defaults.consumable_types,
972            battle_stages: defaults.battle_stages,
973        }
974    }
975
976    /// Hardcoded defaults matching known game versions (v15.0/v15.1).
977    pub fn defaults() -> Self {
978        Self {
979            plane_ammo_types: HashMap::from([
980                (-1, Cow::Borrowed("NONE")),
981                (0, Cow::Borrowed("PROJECTILE")),
982                (1, Cow::Borrowed("BOMB_HE")),
983                (2, Cow::Borrowed("BOMB_AP")),
984                (3, Cow::Borrowed("SKIP_BOMB_HE")),
985                (4, Cow::Borrowed("SKIP_BOMB_AP")),
986                (5, Cow::Borrowed("TORPEDO")),
987                (6, Cow::Borrowed("TORPEDO_DEEPWATER")),
988                (7, Cow::Borrowed("PROJECTILE_AP")),
989                (8, Cow::Borrowed("DEPTH_CHARGE")),
990                (9, Cow::Borrowed("MINE")),
991                (10, Cow::Borrowed("SMOKE")),
992            ]),
993            torpedo_types: HashMap::from([
994                (0, Cow::Borrowed("COMMON")),
995                (1, Cow::Borrowed("SUBMARINE")),
996                (2, Cow::Borrowed("PHOTON")),
997            ]),
998            consumable_types: HashMap::from([
999                (0, Cow::Borrowed(Consumable::DamageControl.name())),
1000                (1, Cow::Borrowed(Consumable::SpottingAircraft.name())),
1001                (2, Cow::Borrowed(Consumable::DefensiveAntiAircraft.name())),
1002                (3, Cow::Borrowed(Consumable::SpeedBoost.name())),
1003                (4, Cow::Borrowed(Consumable::MainBatteryReloadBooster.name())),
1004                (6, Cow::Borrowed(Consumable::Smoke.name())),
1005                (8, Cow::Borrowed(Consumable::RepairParty.name())),
1006                (9, Cow::Borrowed(Consumable::CatapultFighter.name())),
1007                (10, Cow::Borrowed(Consumable::HydroacousticSearch.name())),
1008                (11, Cow::Borrowed(Consumable::TorpedoReloadBooster.name())),
1009                (12, Cow::Borrowed(Consumable::Radar.name())),
1010                (13, Cow::Borrowed(Consumable::Trigger1.name())),
1011                (14, Cow::Borrowed(Consumable::Trigger2.name())),
1012                (15, Cow::Borrowed(Consumable::Trigger3.name())),
1013                (16, Cow::Borrowed(Consumable::Trigger4.name())),
1014                (17, Cow::Borrowed(Consumable::Trigger5.name())),
1015                (18, Cow::Borrowed(Consumable::Trigger6.name())),
1016                (19, Cow::Borrowed(Consumable::Invulnerable.name())),
1017                (20, Cow::Borrowed(Consumable::HealForsage.name())),
1018                (21, Cow::Borrowed(Consumable::CallFighters.name())),
1019                (22, Cow::Borrowed(Consumable::RegenerateHealth.name())),
1020                (23, Cow::Borrowed(Consumable::SubsOxygenRegen.name())),
1021                (24, Cow::Borrowed(Consumable::SubsWaveGunBoost.name())),
1022                (25, Cow::Borrowed(Consumable::SubsFourthState.name())),
1023                (26, Cow::Borrowed(Consumable::DepthCharges.name())),
1024                (27, Cow::Borrowed(Consumable::Trigger7.name())),
1025                (28, Cow::Borrowed(Consumable::Trigger8.name())),
1026                (29, Cow::Borrowed(Consumable::Trigger9.name())),
1027                (30, Cow::Borrowed(Consumable::Buff.name())),
1028                (31, Cow::Borrowed(Consumable::BuffsShift.name())),
1029                (32, Cow::Borrowed(Consumable::CircleWave.name())),
1030                (33, Cow::Borrowed(Consumable::GoDeep.name())),
1031                (34, Cow::Borrowed(Consumable::WeaponReloadBooster.name())),
1032                (35, Cow::Borrowed(Consumable::Hydrophone.name())),
1033                (36, Cow::Borrowed(Consumable::EnhancedRudders.name())),
1034                (37, Cow::Borrowed(Consumable::ReserveBattery.name())),
1035                (38, Cow::Borrowed(Consumable::GroupAuraBuff.name())),
1036                (39, Cow::Borrowed(Consumable::AffectedBuffAura.name())),
1037                (40, Cow::Borrowed(Consumable::InvisibilityExtraBuff.name())),
1038                (41, Cow::Borrowed(Consumable::SubmarineSurveillance.name())),
1039                (42, Cow::Borrowed(Consumable::PlaneSmokeGenerator.name())),
1040                (44, Cow::Borrowed(Consumable::Minefield.name())),
1041                (45, Cow::Borrowed(Consumable::TacticalTrigger1.name())),
1042                (46, Cow::Borrowed(Consumable::TacticalTrigger2.name())),
1043                (47, Cow::Borrowed(Consumable::TacticalTrigger3.name())),
1044                (48, Cow::Borrowed(Consumable::TacticalTrigger4.name())),
1045                (49, Cow::Borrowed(Consumable::TacticalTrigger5.name())),
1046                (50, Cow::Borrowed(Consumable::TacticalTrigger6.name())),
1047                (51, Cow::Borrowed(Consumable::ReconnaissanceSquad.name())),
1048                (52, Cow::Borrowed(Consumable::SmokePlane.name())),
1049                (53, Cow::Borrowed(Consumable::TacticalBuff.name())),
1050                (54, Cow::Borrowed(Consumable::PlaneTrigger1.name())),
1051                (55, Cow::Borrowed(Consumable::PlaneTrigger2.name())),
1052                (56, Cow::Borrowed(Consumable::PlaneTrigger3.name())),
1053                (57, Cow::Borrowed(Consumable::PlaneBuff.name())),
1054                (58, Cow::Borrowed(Consumable::Any.name())),
1055                (59, Cow::Borrowed(Consumable::All.name())),
1056                (60, Cow::Borrowed(Consumable::Special.name())),
1057            ]),
1058            battle_stages: HashMap::from([
1059                (0, crate::game_types::BattleStage::Waiting),
1060                (1, crate::game_types::BattleStage::Battle),
1061                (2, crate::game_types::BattleStage::Results),
1062                (3, crate::game_types::BattleStage::Finishing),
1063                (4, crate::game_types::BattleStage::Ended),
1064            ]),
1065        }
1066    }
1067
1068    pub fn plane_ammo_type(&self, id: i32) -> Option<&str> {
1069        self.plane_ammo_types.get(&id).map(|s| s.as_ref())
1070    }
1071
1072    pub fn torpedo_type(&self, id: i32) -> Option<&str> {
1073        self.torpedo_types.get(&id).map(|s| s.as_ref())
1074    }
1075
1076    pub fn consumable_type(&self, id: i32) -> Option<&str> {
1077        self.consumable_types.get(&id).map(|s| s.as_ref())
1078    }
1079
1080    pub fn plane_ammo_types(&self) -> &HashMap<i32, Cow<'static, str>> {
1081        &self.plane_ammo_types
1082    }
1083
1084    pub fn torpedo_types(&self) -> &HashMap<i32, Cow<'static, str>> {
1085        &self.torpedo_types
1086    }
1087
1088    pub fn consumable_types(&self) -> &HashMap<i32, Cow<'static, str>> {
1089        &self.consumable_types
1090    }
1091
1092    pub fn plane_ammo_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
1093        &mut self.plane_ammo_types
1094    }
1095
1096    pub fn torpedo_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
1097        &mut self.torpedo_types
1098    }
1099
1100    pub fn consumable_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
1101        &mut self.consumable_types
1102    }
1103
1104    pub fn battle_stage(&self, id: i32) -> Option<&crate::game_types::BattleStage> {
1105        self.battle_stages.get(&id)
1106    }
1107
1108    pub fn battle_stages(&self) -> &HashMap<i32, crate::game_types::BattleStage> {
1109        &self.battle_stages
1110    }
1111
1112    pub fn battle_stages_mut(&mut self) -> &mut HashMap<i32, crate::game_types::BattleStage> {
1113        &mut self.battle_stages
1114    }
1115}
1116
1117/// Constants parsed from `gui/data/constants/channel.xml`.
1118#[derive(Clone)]
1119pub struct ChannelConstants {
1120    battle_chat_channel_types: HashMap<i32, Cow<'static, str>>,
1121    channel_type_idents: HashMap<i32, Cow<'static, str>>,
1122}
1123
1124impl ChannelConstants {
1125    /// Load from game files, falling back to defaults if the file can't be read.
1126    /// Parse from raw XML bytes. Falls back to defaults on parse failure.
1127    #[cfg(feature = "parsing")]
1128    pub fn from_xml(xml: &[u8]) -> Self {
1129        let xml_str = match std::str::from_utf8(xml) {
1130            Ok(s) => s,
1131            Err(_) => return Self::defaults(),
1132        };
1133
1134        let defaults = Self::defaults();
1135        Self {
1136            battle_chat_channel_types: parse_integer_enum(xml_str, "BATTLE_CHAT_CHANNEL_TYPE")
1137                .unwrap_or(defaults.battle_chat_channel_types),
1138            channel_type_idents: parse_positional_enum(xml_str, "CHANNEL_TYPE_IDENT_VALUE")
1139                .unwrap_or(defaults.channel_type_idents),
1140        }
1141    }
1142
1143    /// Hardcoded defaults matching known game versions (v15.0/v15.1).
1144    pub fn defaults() -> Self {
1145        Self {
1146            battle_chat_channel_types: HashMap::from([
1147                (0, Cow::Borrowed("GENERAL")),
1148                (1, Cow::Borrowed("TEAM")),
1149                (2, Cow::Borrowed("DIVISION")),
1150                (3, Cow::Borrowed("SYSTEM")),
1151            ]),
1152            channel_type_idents: HashMap::from([
1153                (0, Cow::Borrowed("UNKNOWN")),
1154                (1, Cow::Borrowed("GROUP_OPEN")),
1155                (2, Cow::Borrowed("GROUP_CLOSED")),
1156                (3, Cow::Borrowed("PREBATTLE")),
1157                (4, Cow::Borrowed("COMMON")),
1158                (5, Cow::Borrowed("PRIVATE")),
1159                (6, Cow::Borrowed("CLAN")),
1160                (7, Cow::Borrowed("TRAINING_ROOM")),
1161            ]),
1162        }
1163    }
1164
1165    pub fn battle_chat_channel_type(&self, id: i32) -> Option<&str> {
1166        self.battle_chat_channel_types.get(&id).map(|s| s.as_ref())
1167    }
1168
1169    pub fn channel_type_ident(&self, id: i32) -> Option<&str> {
1170        self.channel_type_idents.get(&id).map(|s| s.as_ref())
1171    }
1172
1173    pub fn battle_chat_channel_types(&self) -> &HashMap<i32, Cow<'static, str>> {
1174        &self.battle_chat_channel_types
1175    }
1176
1177    pub fn channel_type_idents(&self) -> &HashMap<i32, Cow<'static, str>> {
1178        &self.channel_type_idents
1179    }
1180
1181    pub fn battle_chat_channel_types_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
1182        &mut self.battle_chat_channel_types
1183    }
1184
1185    pub fn channel_type_idents_mut(&mut self) -> &mut HashMap<i32, Cow<'static, str>> {
1186        &mut self.channel_type_idents
1187    }
1188}
1189
1190/// Parse an `<enum type="Integer">` block from XML.
1191#[cfg(feature = "parsing")]
1192fn parse_integer_enum(xml: &str, enum_name: &str) -> Option<HashMap<i32, Cow<'static, str>>> {
1193    let doc = roxmltree::Document::parse(xml).ok()?;
1194    let enum_node = doc.descendants().find(|n| n.has_tag_name("enum") && n.attribute("name") == Some(enum_name))?;
1195
1196    let mut map = HashMap::new();
1197    for child in enum_node.children() {
1198        if child.has_tag_name("const")
1199            && let (Some(name), Some(value_str)) = (child.attribute("name"), child.attribute("value"))
1200            && let Ok(value) = value_str.trim().parse::<i32>()
1201        {
1202            map.insert(value, Cow::Owned(name.to_string()));
1203        }
1204    }
1205
1206    if map.is_empty() { None } else { Some(map) }
1207}
1208
1209/// Parse an `<enum type="String">` block from XML (positional indexing).
1210#[cfg(feature = "parsing")]
1211fn parse_positional_enum(xml: &str, enum_name: &str) -> Option<HashMap<i32, Cow<'static, str>>> {
1212    let doc = roxmltree::Document::parse(xml).ok()?;
1213    let enum_node = doc.descendants().find(|n| n.has_tag_name("enum") && n.attribute("name") == Some(enum_name))?;
1214
1215    let mut map = HashMap::new();
1216    let mut index = 0i32;
1217    for child in enum_node.children() {
1218        if child.has_tag_name("const") {
1219            if let Some(name) = child.attribute("name") {
1220                map.insert(index, Cow::Owned(name.to_string()));
1221            }
1222            index += 1;
1223        }
1224    }
1225
1226    if map.is_empty() { None } else { Some(map) }
1227}
1228
1229/// The file path within the game's `res/` directory for battle constants.
1230pub const BATTLE_CONSTANTS_PATH: &str = "gui/data/constants/battle.xml";
1231
1232/// The file path within the game's `res/` directory for ship constants.
1233pub const SHIPS_CONSTANTS_PATH: &str = "gui/data/constants/ships.xml";
1234
1235/// The file path within the game's `res/` directory for weapons constants.
1236pub const WEAPONS_CONSTANTS_PATH: &str = "gui/data/constants/weapons.xml";
1237
1238/// The file path within the game's `res/` directory for common constants.
1239pub const COMMON_CONSTANTS_PATH: &str = "gui/data/constants/common.xml";
1240
1241/// The file path within the game's `res/` directory for channel constants.
1242pub const CHANNEL_CONSTANTS_PATH: &str = "gui/data/constants/channel.xml";