use std::io::Read;
use vfs::VfsPath;
use crate::data::Version;
use crate::game_params::types::CrewSkillName;
use crate::game_params::types::Species;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShipIconState {
Alive,
Dead,
Invisible,
LastVisible,
}
impl ShipIconState {
fn suffix(self) -> &'static str {
match self {
ShipIconState::Alive => "",
ShipIconState::Dead => "_dead",
ShipIconState::Invisible => "_invisible",
ShipIconState::LastVisible => "_last_visible",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Relation {
Ally,
Enemy,
Neutral,
}
impl Relation {
fn as_str(self) -> &'static str {
match self {
Relation::Ally => "ally",
Relation::Enemy => "enemy",
Relation::Neutral => "neutral",
}
}
}
pub fn crew_skill_icon_slug(name: &CrewSkillName) -> String {
use convert_case::Case;
use convert_case::Casing;
name.as_str().to_case(Case::Snake)
}
#[derive(Clone, Copy, Debug)]
pub enum GuiAsset<'a> {
ShipClassIcon { species: Species, state: ShipIconState },
SelfShipIcon { species: Species, alive: bool },
Consumable(&'a str),
Achievement(&'a str),
Ribbon(&'a str),
SubRibbon(&'a str),
Modernization(&'a str),
SignalFlag(&'a str),
NationFlag(&'a str),
ShipSilhouette(&'a str),
CapturePointFlag(Relation),
CrewSkill { name: &'a CrewSkillName },
}
impl GuiAsset<'_> {
pub fn candidate_paths(&self, version: Option<&Version>) -> Vec<String> {
let _ = version;
match *self {
GuiAsset::ShipClassIcon { species, state } => {
let s = species.name().to_ascii_lowercase();
vec![format!("gui/fla/minimap/ship_icons/minimap_{s}{}.svg", state.suffix())]
}
GuiAsset::SelfShipIcon { species, alive } => {
let s = species.name().to_ascii_lowercase();
let life = if alive { "alive" } else { "dead" };
vec![
format!("gui/fla/minimap/ship_icons_self/minimap_self_{life}_{s}.svg"),
format!("gui/fla/minimap/ship_icons_self/minimap_self_{life}.svg"),
]
}
GuiAsset::Consumable(pcy) => vec![format!("gui/consumables/consumable_{pcy}.png")],
GuiAsset::Achievement(key) => vec![format!("gui/achievements/icon_achievement_{key}.png")],
GuiAsset::Ribbon(name) => vec![format!("gui/ribbons/{name}.png")],
GuiAsset::SubRibbon(name) => vec![format!("gui/ribbons/subribbons/{name}.png")],
GuiAsset::Modernization(name) => vec![format!("gui/modernization_icons/icon_modernization_{name}.png")],
GuiAsset::SignalFlag(name) => vec![format!("gui/signal_flags/{name}.png")],
GuiAsset::NationFlag(nation) => vec![format!("gui/nation_flags/tiny/flag_{nation}.png")],
GuiAsset::ShipSilhouette(index) => vec![format!("gui/ships_silhouettes/{index}.png")],
GuiAsset::CapturePointFlag(rel) => {
vec![format!("gui/battle_hud/markers/capture_point/icon_base_{}_flag.png", rel.as_str())]
}
GuiAsset::CrewSkill { name } => {
let slug = crew_skill_icon_slug(name);
let internal = name.as_str();
vec![
format!("gui/crew_commander/skills/{slug}.png"),
format!("gui/crew_commander/skills/big/icon_perk_{internal}.png"),
format!("gui/crew_commander/skills/small/icon_perk_{internal}.png"),
format!("gui/crew_commander/skills/small/icon_perk_small_{internal}.png"),
]
}
}
}
pub fn resolve(&self, vfs: &VfsPath, version: Option<&Version>) -> Option<VfsPath> {
for path in self.candidate_paths(version) {
if let Ok(entry) = vfs.join(&path)
&& entry.exists().unwrap_or(false)
{
return Some(entry);
}
}
None
}
pub fn read(&self, vfs: &VfsPath, version: Option<&Version>) -> Option<Vec<u8>> {
let entry = self.resolve(vfs, version)?;
let mut buf = Vec::new();
entry.open_file().ok()?.read_to_end(&mut buf).ok()?;
(!buf.is_empty()).then_some(buf)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PlaneMarkerKind {
Consumables,
Controllable,
AirSupport,
}
impl PlaneMarkerKind {
fn dir(self) -> &'static str {
match self {
PlaneMarkerKind::Consumables => "consumables",
PlaneMarkerKind::Controllable => "controllable",
PlaneMarkerKind::AirSupport => "airsupport",
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum GuiAssetDir {
ShipClassIcons,
SelfShipIcons,
PlaneMarkers(PlaneMarkerKind),
BuildingIcons,
Consumables,
CrewSkills,
Modernizations,
SignalFlags,
DeathCauseIcons,
PowerupDrops,
CapturePoint,
Ribbons,
SubRibbons,
Achievements,
NationFlags,
Silhouettes,
}
impl GuiAssetDir {
pub fn candidate_paths(&self, version: Option<&Version>) -> Vec<String> {
let _ = version;
let dir = match *self {
GuiAssetDir::ShipClassIcons => "gui/fla/minimap/ship_icons".to_string(),
GuiAssetDir::SelfShipIcons => "gui/fla/minimap/ship_icons_self".to_string(),
GuiAssetDir::PlaneMarkers(kind) => {
format!("gui/battle_hud/markers_minimap/plane/{}", kind.dir())
}
GuiAssetDir::BuildingIcons => "gui/battle_hud/markers/building_icons/normal".to_string(),
GuiAssetDir::Consumables => "gui/consumables".to_string(),
GuiAssetDir::CrewSkills => "gui/crew_commander/skills".to_string(),
GuiAssetDir::Modernizations => "gui/modernization_icons".to_string(),
GuiAssetDir::SignalFlags => "gui/signal_flags".to_string(),
GuiAssetDir::DeathCauseIcons => "gui/battle_hud/icon_frag".to_string(),
GuiAssetDir::PowerupDrops => "gui/powerups/drops".to_string(),
GuiAssetDir::CapturePoint => "gui/battle_hud/markers/capture_point".to_string(),
GuiAssetDir::Ribbons => "gui/ribbons".to_string(),
GuiAssetDir::SubRibbons => "gui/ribbons/subribbons".to_string(),
GuiAssetDir::Achievements => "gui/achievements".to_string(),
GuiAssetDir::NationFlags => "gui/nation_flags/tiny".to_string(),
GuiAssetDir::Silhouettes => "gui/ships_silhouettes".to_string(),
};
vec![dir]
}
pub fn resolve(&self, vfs: &VfsPath, version: Option<&Version>) -> Option<VfsPath> {
for path in self.candidate_paths(version) {
if let Ok(entry) = vfs.join(&path)
&& entry.exists().unwrap_or(false)
{
return Some(entry);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_params::types::CrewSkillName;
const VERSION: Version = Version { major: 15, minor: 0, patch: 0, build: 11791718 };
#[test]
fn crew_skill_icon_slug_is_snake_case() {
assert_eq!(crew_skill_icon_slug(&CrewSkillName::from("TriggerSpreading")), "trigger_spreading");
}
#[test]
fn crew_skill_candidate_paths_cover_modern_and_prerework() {
let paths = GuiAsset::CrewSkill { name: &CrewSkillName::from("HePenetration") }.candidate_paths(Some(&VERSION));
assert_eq!(paths[0], "gui/crew_commander/skills/he_penetration.png");
assert_eq!(paths[1], "gui/crew_commander/skills/big/icon_perk_HePenetration.png");
assert_eq!(paths[2], "gui/crew_commander/skills/small/icon_perk_HePenetration.png");
assert_eq!(paths[3], "gui/crew_commander/skills/small/icon_perk_small_HePenetration.png");
}
#[test]
fn self_icon_falls_back_to_generic() {
let paths = GuiAsset::SelfShipIcon { species: Species::Destroyer, alive: true }.candidate_paths(Some(&VERSION));
assert_eq!(paths[0], "gui/fla/minimap/ship_icons_self/minimap_self_alive_destroyer.svg");
assert_eq!(paths[1], "gui/fla/minimap/ship_icons_self/minimap_self_alive.svg");
}
#[test]
fn consumable_and_capture_point_paths() {
assert_eq!(
GuiAsset::Consumable("PCY009_CrashCrewPremium").candidate_paths(Some(&VERSION))[0],
"gui/consumables/consumable_PCY009_CrashCrewPremium.png"
);
assert_eq!(
GuiAsset::CapturePointFlag(Relation::Enemy).candidate_paths(Some(&VERSION))[0],
"gui/battle_hud/markers/capture_point/icon_base_enemy_flag.png"
);
}
#[test]
fn modernization_path_has_icon_prefix() {
assert_eq!(
GuiAsset::Modernization("PCM082_SpecialBonus_Mod_I").candidate_paths(Some(&VERSION))[0],
"gui/modernization_icons/icon_modernization_PCM082_SpecialBonus_Mod_I.png"
);
}
#[test]
fn signal_flag_path_is_bare() {
assert_eq!(
GuiAsset::SignalFlag("PCEF019_JW1_SignalFlag").candidate_paths(Some(&VERSION))[0],
"gui/signal_flags/PCEF019_JW1_SignalFlag.png"
);
}
}