Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ where
&plugin_mmap,
plugin_name,
&game_plugins_path,
&load_order,
&mut load_order,
)?;

log::debug!(
Expand Down
95 changes: 76 additions & 19 deletions src/load_order.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,102 @@
use ahash::AHashMap;
use anyhow::anyhow;
use itertools::{EitherOrBoth::*, Itertools};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;

// FIXME: make case insensitive and instead use Skyrim.ccc to determine what is or is not CC content
fn is_creation_club_light_master<S: AsRef<str>>(mod_name: S) -> bool {
let mod_name = mod_name.as_ref();
mod_name.starts_with("cc") && mod_name.ends_with(".esl")
}

// FIXME: need to read esl/esm status from plugin directly, not base on extension. See Arthmoor post

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoadOrder {
load_order: Vec<String>,
// TODO: masters != plugins
masters: Vec<String>,
light_masters: Vec<String>,
}

impl LoadOrder {
pub fn new(load_order: Vec<String>) -> Self {
let (creation_club_light_masters, masters): (Vec<_>, Vec<_>) = load_order
.into_iter()
.partition(|entry| is_creation_club_light_master(entry));

// FIXME: save games contain lists of loaded plugins. I might(?) need to use these to correctly parse the form IDs. Are these lists in order?
// FIXME: the wiki is wrong. Arthmoor and mod organizer are right https://www.afkmods.com/index.php?/topic/5079-plugin-files-and-you-esmeslesp/ -- it's not alphabetical, but determined by the Skyrim.ccc file
// FIXME: read and impl this https://www.afkmods.com/index.php?/topic/5079-plugin-files-and-you-esmeslesp/page/5/#comment-174631
// Creation Club .esl files are sorted alphabetically to determine their load order
// See https://en.uesp.net/wiki/Skyrim:Form_ID#Creation_ClubCC
creation_club_light_masters.sort();

Self {
load_order: load_order.into_iter().collect::<Vec<_>>(),
// Note: additional light masters may be identified as they are parsed
light_masters: creation_club_light_masters,
masters,
}
}

// FIXME: handle "ESL flagged ESPs". They maintain the load order but are loaded in ESL space
// FIXME: mod organizer seems to maintain that (generally) file time is the actual load order https://github.com/ModOrganizer2/modorganizer-game_gamebryo/blob/3abd56d555c65c9b1d0d547a33cdc0e66d54b61a/src/gamebryo/gamebryogameplugins.cpp#L182

/// Marks the plugin with the specified `mod_name` as a light master
///
/// **Note**: assumes plugins are iterated in original load order!
pub fn plugin_is_esl_flagged(&mut self, mod_name: &str) -> Result<(), anyhow::Error> {
if is_esl(mod_name) {
// We don't need to do anything for .esl files, they were handled in Self::new
return Ok(());
}

let index = self.find_masters_index(mod_name).ok_or_else(|| {
anyhow!(
"failed to mark plugin \"{}\" as light master: not found in masters",
mod_name
)
})?;
// Move from masters to light_masters
let entry = self.masters.remove(index);
self.light_masters.push(entry);
Ok(())
}

pub fn find_index(&self, mod_name: &str) -> Option<u16> {
self.load_order
.iter()
.enumerate()
.find_map(|(index, name)| {
if matches!(cmp_ignore_case_ascii(name, mod_name), Ordering::Equal) {
Some(index as u16)
} else {
None
}
})
/// Finds the index of `mod_name` in the `masters` Vec
fn find_masters_index(&self, mod_name: &str) -> Option<usize> {
self.masters.iter().enumerate().find_map(|(index, name)| {
if matches!(cmp_ignore_case_ascii(name, mod_name), Ordering::Equal) {
Some(index)
} else {
None
}
})
}

pub fn get_form_id_prefix(&self, mod_name: &str) -> Option<u32> {
self.masters.iter().enumerate().find_map(|(index, name)| {
if matches!(cmp_ignore_case_ascii(name, mod_name), Ordering::Equal) {
Some(index as u32)
} else {
None
}
})
}

pub fn get(&self, index: u16) -> Option<&str> {
self.load_order.get(index as usize).map(|x| x.as_str())
self.masters.get(index as usize).map(|x| x.as_str())
}

pub fn is_empty(&self) -> bool {
self.load_order.is_empty()
self.masters.is_empty()
}

// FIXME: need to clone this data
pub fn iter(&self) -> impl Iterator<Item = &String> + '_ {
self.load_order.iter()
self.masters.iter()
}

/// Removes unused entries from the LoadOrder based on the used indexes returned by the iterator
Expand All @@ -57,7 +114,7 @@ impl LoadOrder {
.collect::<AHashMap<String, u16>>();

let num_removed = self
.load_order
.masters
.drain_filter(|entry| !used_entries_with_old_indexes.contains_key(entry))
.count();

Expand All @@ -70,7 +127,7 @@ impl LoadOrder {
used_entries_with_old_indexes
.iter()
// Create map from old index to new index
.map(|(entry, old_index)| (*old_index, self.find_index(entry).unwrap()))
.map(|(entry, old_index)| (*old_index, self.get_form_id_prefix(entry).unwrap()))
.collect(),
)
}
Expand All @@ -81,7 +138,7 @@ impl Display for LoadOrder {
write!(
f,
"{}",
self.load_order
self.masters
.iter()
.enumerate()
.map(|(index, entry)| format!("{:04}: {}", index, entry))
Expand Down
46 changes: 10 additions & 36 deletions src/plugin_parser/form_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,33 @@ use std::{fmt::Display, str::FromStr};

use serde_with::{DeserializeFromStr, SerializeDisplay};

use crate::load_order::LoadOrder;

#[derive(
Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, DeserializeFromStr, SerializeDisplay,
)]
pub struct GlobalFormId {
pub load_order_index: u16,
pub id: u32,
}
pub struct GlobalFormId(u32);

impl GlobalFormId {
pub fn new(load_order_index: u16, id: u32) -> Self {
GlobalFormId {
load_order_index,
id,
}
}

/// Sets the load order index to the specified value
pub fn set_load_order_index(&mut self, load_order_index: u16) {
self.load_order_index = load_order_index;
pub fn new(form_id: u32) -> Self {
GlobalFormId(form_id)
}
}

impl Display for GlobalFormId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:04}:{:06x}", self.load_order_index, self.id)
write!(f, "{:08x}", self.0)
}
}

impl FromStr for GlobalFormId {
type Err = String;

/// Parse a value like `0004:3F0001`
/// Parse a value like `043F0001`
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(':');

let load_order_index = {
let part = parts
.next()
.ok_or_else(|| "Missing first value".to_string())?;
part.parse::<u16>().map_err(|err| err.to_string())?
};

let id = {
let part = parts
.next()
.ok_or_else(|| "Missing second value".to_string())?;
u32::from_str_radix(part, 16).map_err(|err| err.to_string())?
};

Ok(Self {
load_order_index,
id,
})
let form_id = u32::from_str_radix(s, 16).map_err(|err| err.to_string())?;

Ok(Self(form_id))
}
}

Expand Down
23 changes: 19 additions & 4 deletions src/plugin_parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub fn parse_plugin<'a>(
input: &'a [u8],
plugin_name: &str,
game_plugins_path: &Path,
load_order: &LoadOrder,
load_order: &mut LoadOrder,
) -> Result<(Vec<Ingredient>, Vec<MagicEffect>), anyhow::Error> {
log::trace!("Parsing plugin {}", plugin_name);

Expand All @@ -55,14 +55,21 @@ pub fn parse_plugin<'a>(

let is_localized = (header_record.header().flags() & 0x80) != 0;

let is_light_master = (header_record.header().flags() & 0x200) != 0;
if is_light_master {
load_order.plugin_is_esl_flagged(plugin_name);
}

log::trace!("Plugin masters: {:#?}", masters);
log::trace!("Plugin is_localized: {:?}", is_localized);
log::trace!("Plugin is_light_master: {:?}", is_light_master);

let strings_table = match is_localized {
true => StringsTable::new(plugin_name, game_plugins_path),
false => None,
};

// Converts plugin-local form ID to proper (global) form ID
let globalize_form_id = |form_id: NonZeroU32| -> Result<GlobalFormId, anyhow::Error> {
// See https://en.uesp.net/wiki/Skyrim:Form_ID
let mod_id = (u32::from(form_id) >> 24) as usize;
Expand All @@ -82,14 +89,22 @@ pub fn parse_plugin<'a>(
}
}?;

// TODO: ESL

let is_esl = mod_name.ends_with(".esl");

// The last six hex digits are the ID of the record itself
let id = u32::from(form_id) & 0x00FFFFFF;
let record_id = u32::from(form_id) & 0x00FFFFFF;

let load_order_index = load_order
.find_index(&mod_name)
.get_form_id_prefix(&mod_name)
.ok_or_else(|| anyhow!("plugin {} not found in load order!", &mod_name))?;

Ok(GlobalFormId::new(load_order_index, id))
let load_order_index = load_order_index & 0xFF000000;

let form_id = load_order_index as u32 | record_id;

Ok(GlobalFormId::new(form_id))
};

let parse_lstring =
Expand Down