use crate::Host;
use crate::abi::{ManifestFlags, ModInfo, Side, Str};
pub const LOADER_ID: &str = "enshwick";
pub const STUB_FILE: &str = "powrprof.dll";
pub const LOADER_FILE: &str = "mods/enshwick/enshwick.dll";
pub const LOADER_FILES: [&str; 2] = [STUB_FILE, LOADER_FILE];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModEntry {
pub id: String,
pub version: String,
pub side: Side,
pub join: bool,
pub running: bool,
pub file: String,
pub name: String,
pub description: String,
pub authors: Vec<String>,
pub license: String,
pub homepage: String,
pub replaced_by: String,
}
impl Default for ModEntry {
fn default() -> Self {
Self {
id: String::new(),
version: String::new(),
side: Side(0),
join: false,
running: false,
file: String::new(),
name: String::new(),
description: String::new(),
authors: Vec::new(),
license: String::new(),
homepage: String::new(),
replaced_by: String::new(),
}
}
}
impl ModEntry {
#[must_use]
pub fn label(&self) -> String {
label(&self.name, &self.id)
}
}
#[doc(hidden)]
#[must_use]
pub fn label(name: &str, id: &str) -> String {
if name.is_empty() { id } else { name }.to_owned()
}
#[must_use]
pub fn all() -> Vec<ModEntry> {
crate::host()
.map(|host| host.listing(host.api().mods))
.unwrap_or_default()
}
#[must_use]
pub fn join_missing() -> Vec<ModEntry> {
crate::host()
.map(|host| host.listing(host.api().join_missing))
.unwrap_or_default()
}
#[must_use]
pub fn replaced() -> Vec<ModEntry> {
crate::host()
.map(|host| host.listing(host.api().replaced))
.unwrap_or_default()
}
impl Host {
fn listing(&self, list: Listing) -> Vec<ModEntry> {
let entry_size = u32::try_from(size_of::<ModInfo>()).unwrap_or(u32::MAX);
let mut count = 0u32;
let status = list(
self.handle(),
core::ptr::null_mut(),
entry_size,
0,
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
let mut infos = vec![ModInfo::EMPTY; count as usize];
let capacity = u32::try_from(infos.len()).unwrap_or(u32::MAX);
let status = list(
self.handle(),
infos.as_mut_ptr(),
entry_size,
capacity,
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
infos.truncate(count as usize);
infos
.iter()
.filter_map(|info| unsafe { entry(info) })
.collect()
}
}
type Listing = extern "system" fn(
me: crate::abi::ModHandle,
out: *mut ModInfo,
entry_size: u32,
capacity: u32,
count: *mut u32,
) -> crate::abi::Status;
unsafe fn entry(info: &ModInfo) -> Option<ModEntry> {
let id = unsafe { info.id.as_str() }?;
let version = unsafe { info.version.as_str() }?;
let file = unsafe { info.file.as_str() }?;
let text = |s: &Str| unsafe { s.as_str() }.unwrap_or_default().to_owned();
let authors = if info.authors.is_null() {
&[][..]
} else {
unsafe { core::slice::from_raw_parts(info.authors, info.author_count) }
};
let (name, description, authors, license, homepage) = (
text(&info.name),
text(&info.description),
authors.iter().map(text).collect(),
text(&info.license),
text(&info.homepage),
);
let replaced_by = text(&info.replaced_by);
Some(ModEntry {
id: id.to_owned(),
version: version.to_owned(),
side: info.side,
join: info.flags.contains(ManifestFlags::JOIN),
running: info.off == 0,
file: file.to_owned(),
name,
description,
authors,
license,
homepage,
replaced_by,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::Header;
#[test]
fn entries_read_back() {
let info = ModInfo {
header: Header::of::<ModInfo>(),
id: Str::new("you.trading-post"),
version: Str::new("0.1.0"),
side: Side::BOTH,
flags: ManifestFlags::JOIN,
off: 1,
file: Str::new("mods/trading_post.dll"),
name: Str::new("Trading Post"),
description: Str::new("A chest only its owner opens"),
authors: AUTHORS.as_ptr(),
author_count: AUTHORS.len(),
license: Str::new("MIT"),
homepage: Str::EMPTY,
replaced_by: Str::EMPTY,
};
let entry = unsafe { entry(&info) }.unwrap();
assert!(entry.join);
assert!(!entry.running);
assert_eq!(entry.file, "mods/trading_post.dll");
assert_eq!(entry.label(), "Trading Post");
assert_eq!(entry.authors, ["themaiby", "someone"]);
assert_eq!(entry.license, "MIT");
}
static AUTHORS: [Str; 2] = [Str::new("themaiby"), Str::new("someone")];
}