Skip to content

Commit 4ef4184

Browse files
committed
Add LTEX trimming and replacing
1 parent 8c397ee commit 4ef4184

File tree

5 files changed

+156
-7
lines changed

5 files changed

+156
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "StandardsValidator"
3-
version = "2.23.1"
3+
version = "2.24.0"
44
edition = "2021"
55

66
[dependencies]

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ This check computes the Levenshtein distance between NPC names. It also checks i
3939
`StandardsValidator.exe --names [mode] Morrowind.esm Tribunal.esm Bloodmoon.esm Tamriel_Data.esm file.esp`
4040

4141
Like `--extended` above, this mode attempts to load master files automatically.
42+
43+
# Land texture cleaner
44+
To remove unused LTEX from a plugin:
45+
46+
`StandardsValidator.exe [mode] inputfile.esp --trim-ltex outputfile.esp`
47+
48+
To also replace all uses of the `daedric stone` texture with the `ma_lavaridge` texture and all uses of `gl_grass_05` with `daedric stone`:
49+
50+
`StandardsValidator.exe [mode] inputfile.esp --trim-ltex outputfile.esp --replace-ltex "daedric stone" "ma_lavaridge" --replace-ltex gl_grass_05 "daedric stone"`

src/ltex.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use tes3::esp::{Landscape, LandscapeTexture, Plugin, TES3Object};
4+
5+
fn build_replacement_table(
6+
replacements: HashMap<String, String>,
7+
textures: &[LandscapeTexture],
8+
) -> Result<HashMap<u32, u32>, String> {
9+
let mut replace = HashMap::new();
10+
for (current, replacement) in replacements {
11+
if let Some(current_ltex) = textures
12+
.iter()
13+
.find(|ltex| ltex.id.eq_ignore_ascii_case(&current))
14+
{
15+
if let Some(replacement_ltex) = textures
16+
.iter()
17+
.find(|ltex| ltex.id.eq_ignore_ascii_case(&replacement))
18+
{
19+
if replace
20+
.insert(current_ltex.index + 1, replacement_ltex.index + 1)
21+
.is_some()
22+
{
23+
return Err(format!("Found multiple replacements for LTEX {}", current));
24+
}
25+
} else {
26+
println!(
27+
"Not replacing LTEX {} with {} because the latter doesn't exist",
28+
current, replacement
29+
);
30+
}
31+
} else {
32+
println!("Not replacing LTEX {} because it doesn't exist", current);
33+
}
34+
}
35+
Ok(replace)
36+
}
37+
38+
fn replace_textures(plugin: &mut Plugin, replace: HashMap<u32, u32>) -> HashSet<u32> {
39+
let mut used = HashSet::new();
40+
for landscape in plugin.objects_of_type_mut::<Landscape>() {
41+
for row in landscape.texture_indices.data.as_mut_slice() {
42+
for index in row {
43+
if let Some(replacement) = replace.get(&(*index as u32)) {
44+
*index = *replacement as u16;
45+
}
46+
if *index > 0 {
47+
used.insert((*index as u32) - 1);
48+
}
49+
}
50+
}
51+
}
52+
used
53+
}
54+
55+
fn is_ltex(object: &TES3Object) -> bool {
56+
matches!(object, TES3Object::LandscapeTexture(_))
57+
}
58+
59+
pub fn deduplicate_ltex(
60+
plugin: &mut Plugin,
61+
replacements: HashMap<String, String>,
62+
) -> Result<(), String> {
63+
let mut textures: Vec<LandscapeTexture> = plugin
64+
.objects_of_type::<LandscapeTexture>()
65+
.cloned()
66+
.collect();
67+
if textures.is_empty() {
68+
return Err("Plugin did not contain any landscape textures".to_string());
69+
}
70+
let replace = build_replacement_table(replacements, &textures)?;
71+
let used = replace_textures(plugin, replace);
72+
73+
let original_size = textures.len();
74+
textures.retain(|ltex| used.contains(&ltex.index));
75+
let removed = original_size - textures.len();
76+
println!("Found {} unused landscape textures", removed);
77+
78+
let mut new_indices = HashMap::new();
79+
for (index, texture) in textures.iter_mut().enumerate() {
80+
let current = texture.index;
81+
texture.index = index as u32;
82+
new_indices.insert(current + 1, texture.index + 1);
83+
}
84+
replace_textures(plugin, new_indices);
85+
86+
let (first_ltex, _) = plugin
87+
.objects
88+
.iter()
89+
.enumerate()
90+
.find(|(_, object)| is_ltex(object))
91+
.unwrap();
92+
plugin.objects.retain(|object| !is_ltex(object));
93+
plugin.objects.splice(
94+
first_ltex..first_ltex,
95+
textures.into_iter().map(TES3Object::LandscapeTexture),
96+
);
97+
98+
Ok(())
99+
}

src/main.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
use clap::{crate_version, Arg, ArgGroup, ArgMatches, Command};
1+
use clap::{crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command};
22
use context::{Context, Mode};
33
use extended::ExtendedValidator;
44
use oob::fix_oob;
5-
use std::{error::Error, fs, path::Path};
5+
use std::{collections::HashMap, error::Error, fs, path::Path};
66
use tes3::esp::Plugin;
77
use toml::{Table, Value};
88
use validators::Validator;
99

10+
use crate::ltex::deduplicate_ltex;
11+
1012
mod context;
1113
mod extended;
1214
mod handlers;
15+
mod ltex;
1316
mod oob;
1417
mod util;
1518
mod validators;
@@ -20,6 +23,10 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
2023
fn main() -> Result<(), Box<dyn Error>> {
2124
let args = Command::new("StandardsValidator")
2225
.args(&[
26+
Arg::new("ltexdedup")
27+
.long("trim-ltex")
28+
.value_name("output file")
29+
.help("Remove unused landscape textures and save the trimmed output to a new file. Warning: overwrites the output file!"),
2330
Arg::new("ooboutput")
2431
.long("fix-out-of-bounds")
2532
.value_name("output file")
@@ -60,6 +67,13 @@ fn main() -> Result<(), Box<dyn Error>> {
6067
"Squared distance at which two objects with the same id, \
6168
scale, and orientation are considered duplicates.",
6269
),
70+
Arg::new("replaceltex")
71+
.long("replace-ltex")
72+
.num_args(2)
73+
.action(ArgAction::Append)
74+
.requires("ltexdedup")
75+
.value_names(["original", "new"])
76+
.help("Replaces all uses of landscape textures with the original id with the new one"),
6377
Arg::new("mode")
6478
.required(true)
6579
.value_parser(["PT", "TD", "TR", "Vanilla"])
@@ -70,16 +84,22 @@ fn main() -> Result<(), Box<dyn Error>> {
7084
.help("C:/path/to/plugin.esp"),
7185
])
7286
.groups([
73-
ArgGroup::new("g_validator").args(["duplicatethreshold"]),
87+
ArgGroup::new("g_ltex").args(["ltexdedup"]),
88+
ArgGroup::new("g_ltexreplace")
89+
.args(["replaceltex"])
90+
.requires("g_ltex"),
91+
ArgGroup::new("g_validator")
92+
.args(["duplicatethreshold"])
93+
.conflicts_with("g_ltex"),
7494
ArgGroup::new("g_extended")
7595
.args(["extended", "names"])
76-
.conflicts_with("g_validator"),
96+
.conflicts_with_all(["g_validator", "g_ltex"]),
7797
ArgGroup::new("g_autoload")
7898
.arg("dontautoload")
7999
.requires("g_extended"),
80100
ArgGroup::new("g_oob")
81101
.arg("ooboutput")
82-
.conflicts_with_all(["g_validator", "g_extended"]),
102+
.conflicts_with_all(["g_validator", "g_extended", "g_ltex"]),
83103
])
84104
.version(crate_version!())
85105
.get_matches();
@@ -94,6 +114,9 @@ fn main() -> Result<(), Box<dyn Error>> {
94114
if let Some(output) = args.get_one::<String>("ooboutput") {
95115
return run_oob_fixes(paths.next().unwrap(), output);
96116
}
117+
if let Some(output) = args.get_one::<String>("ltexdedup") {
118+
return run_ltex_dedup(paths.next().unwrap(), output, &args);
119+
}
97120

98121
validate(paths.next().unwrap(), &args)
99122
}
@@ -212,3 +235,21 @@ fn run_oob_fixes(input: &str, output: &str) -> Result<(), Box<dyn Error>> {
212235
plugin.save_path(output)?;
213236
Ok(())
214237
}
238+
239+
fn run_ltex_dedup(input: &str, output: &str, args: &ArgMatches) -> Result<(), Box<dyn Error>> {
240+
let mut replacements = HashMap::new();
241+
if args.contains_id("replaceltex") {
242+
let vals: Vec<Vec<&String>> = args
243+
.get_occurrences("replaceltex")
244+
.unwrap()
245+
.map(Iterator::collect)
246+
.collect();
247+
for pair in vals {
248+
replacements.insert(pair[0].clone(), pair[1].clone());
249+
}
250+
}
251+
let mut plugin = load_plugin(input, None)?;
252+
deduplicate_ltex(&mut plugin, replacements)?;
253+
plugin.save_path(output)?;
254+
Ok(())
255+
}

0 commit comments

Comments
 (0)