Skip to content

Commit bc176c4

Browse files
committed
Report (heuristic) vampires not using the right vampire head
1 parent e09aea5 commit bc176c4

File tree

6 files changed

+79
-27
lines changed

6 files changed

+79
-27
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.1.0"
3+
version = "2.2.0"
44
edition = "2021"
55

66
[dependencies]

WARNINGS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ Slaves should generally only wear a single slave bracer.
125125
### Knows spell
126126
This NPC knows a spell that is culturally or geographically inappropriate.
127127

128+
### Is a vampire but uses head
129+
This NPC is a vampire but does not use the correct vampire head for its race.
130+
NPC vampires that do not need to switch to a mortal appearance should use their race's vampire head as their default head to ensure they always look like vampires.
131+
128132
## Keys
129133
A misc item is a key if it has the key flag. This is a property of the record and determines if it can be sold to merchants and detected by Detect Key.
130134

src/util.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@ pub fn ci_starts_with(s: &str, prefix: &str) -> bool {
111111
false
112112
}
113113

114+
pub fn ci_ends_with(s: &str, suffix: &str) -> bool {
115+
if s.len() >= suffix.len() {
116+
let start = s.len() - suffix.len();
117+
return s.as_bytes()[start..].eq_ignore_ascii_case(suffix.as_bytes());
118+
}
119+
false
120+
}
121+
122+
pub fn is_correct_vampire_head(head: &str, race: &str, female: bool) -> bool {
123+
let prefix = "b_v_";
124+
if !ci_starts_with(head, prefix) {
125+
return false;
126+
}
127+
let without_prefix = &head[prefix.len()..];
128+
if !ci_starts_with(without_prefix, race) {
129+
return false;
130+
}
131+
let without_race = &without_prefix[race.len()..];
132+
if female {
133+
return "_f_head_01".eq_ignore_ascii_case(without_race);
134+
}
135+
"_m_head_01".eq_ignore_ascii_case(without_race)
136+
}
137+
114138
pub fn update_or_insert<K, V: Default, F>(map: &mut HashMap<K, V>, key: K, f: F)
115139
where
116140
K: PartialEq,

src/validators/ids.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashMap;
22

33
use super::Context;
4-
use crate::{context::Mode, handlers::Handler};
4+
use crate::{context::Mode, handlers::Handler, util::is_correct_vampire_head};
55
use tes3::esp::{Bodypart, BodypartFlags, BodypartId, EditorId, TES3Object, TypeInfo};
66

77
const VANILLA_FACTIONS: [&str; 27] = [
@@ -70,13 +70,13 @@ impl Handler<'_> for IdValidator {
7070
match record {
7171
TES3Object::Bodypart(part) => {
7272
if is_vampire_head(part) {
73-
let id = format!(
74-
"b_v_{}_{}_head_01",
75-
part.race,
76-
if is_female(part) { "f" } else { "m" }
77-
);
78-
if !part.id.eq_ignore_ascii_case(&id) {
79-
println!("Bodypart {} should have id {}", part.id, id);
73+
if !is_correct_vampire_head(&part.id, &part.race, is_female(part)) {
74+
println!(
75+
"Bodypart {} should have id b_v_{}_{}_head_01",
76+
part.id,
77+
part.race,
78+
if is_female(part) { "f" } else { "m" }
79+
);
8080
}
8181
} else {
8282
check_id(context, record);

src/validators/scripts.rs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ use super::Context;
44
use crate::{
55
context::Mode,
66
handlers::Handler,
7-
util::{ci_starts_with, Actor},
7+
util::{ci_ends_with, ci_starts_with, is_correct_vampire_head, Actor},
88
};
99
use codegen::{get_joined_commands, get_khajiit_script};
1010
use regex::{Regex, RegexBuilder};
11-
use tes3::esp::{Dialogue, Npc, Script, TES3Object};
11+
use tes3::esp::{Dialogue, Npc, NpcFlags, Script, TES3Object};
1212

1313
pub struct ScriptValidator {
1414
scripts: HashMap<String, ScriptInfo>,
1515
npc: Regex,
1616
khajiit: Regex,
1717
nolore: Regex,
18+
vampire: Regex,
1819
commands: Regex,
1920
khajiit_script: Regex,
2021
projects: Vec<(&'static str, Regex)>,
@@ -29,17 +30,19 @@ struct ScriptInfo {
2930
npc: bool,
3031
khajiit: bool,
3132
nolore: bool,
33+
vampire: bool,
3234
projects: Vec<&'static str>,
3335
}
3436

3537
impl ScriptInfo {
36-
fn new(npc: bool, khajiit: bool, nolore: bool) -> Self {
38+
fn new(npc: bool, khajiit: bool, nolore: bool, vampire: bool) -> Self {
3739
Self {
3840
used: false,
3941
used_by_khajiit: false,
4042
npc,
4143
khajiit,
4244
nolore,
45+
vampire,
4346
projects: Vec::new(),
4447
}
4548
}
@@ -56,6 +59,7 @@ impl Handler<'_> for ScriptValidator {
5659
self.npc.is_match(text),
5760
self.khajiit.is_match(text),
5861
self.nolore.is_match(text),
62+
self.vampire.is_match(text),
5963
);
6064
for (local, regex) in &self.projects {
6165
if regex.is_match(text) {
@@ -75,12 +79,11 @@ impl Handler<'_> for ScriptValidator {
7579
}
7680
} else if let TES3Object::Npc(npc) = record {
7781
if !npc.is_dead() {
78-
if !npc.script.is_empty() {
79-
let id = npc.script.to_ascii_lowercase();
80-
self.check_npc_script(npc, id);
81-
return;
82+
if npc.script.is_empty() {
83+
println!("Npc {} does not have a script", npc.id);
84+
} else {
85+
self.check_npc_script(npc);
8286
}
83-
println!("Npc {} does not have a script", npc.id);
8487
}
8588
}
8689
}
@@ -133,6 +136,7 @@ impl ScriptValidator {
133136
let npc = get_variable("T_Local_NPC", "short")?;
134137
let khajiit = get_variable("T_Local_Khajiit", "short")?;
135138
let nolore = get_variable("NoLore", "short")?;
139+
let vampire = get_variable("T_Local_Vampire", "short")?;
136140
let commands = get_variable(get_joined_commands!(), "(short|long|float)")?;
137141
let khajiit_script = RegexBuilder::new(get_khajiit_script!())
138142
.case_insensitive(true)
@@ -157,6 +161,7 @@ impl ScriptValidator {
157161
npc,
158162
khajiit,
159163
nolore,
164+
vampire,
160165
commands,
161166
khajiit_script,
162167
projects,
@@ -166,19 +171,21 @@ impl ScriptValidator {
166171
})
167172
}
168173

169-
fn check_npc_script(&mut self, npc: &Npc, id: String) {
170-
if let Some(script) = self.scripts.get_mut(&id) {
174+
fn check_npc_script(&mut self, npc: &Npc) {
175+
let vampire;
176+
if let Some(script) = self.scripts.get_mut(&npc.script.to_ascii_lowercase()) {
171177
script.used = true;
178+
vampire = script.vampire;
172179
if !script.npc {
173180
println!(
174181
"Npc {} uses script {} which does not define T_Local_NPC",
175-
npc.id, id
182+
npc.id, npc.script
176183
);
177184
}
178185
if !script.nolore {
179186
println!(
180187
"Npc {} uses script {} which does not define NoLore",
181-
npc.id, id
188+
npc.id, npc.script
182189
);
183190
}
184191
let race = &npc.race;
@@ -187,17 +194,17 @@ impl ScriptValidator {
187194
if !script.khajiit {
188195
println!(
189196
"Npc {} uses script {} which does not define T_Local_Khajiit",
190-
npc.id, id
197+
npc.id, npc.script
191198
);
192199
}
193200
}
194201
if script.projects.is_empty() {
195-
println!("Npc {} uses script {} which does not define any province specific local variables", npc.id, id);
202+
println!("Npc {} uses script {} which does not define any province specific local variables", npc.id, npc.script);
196203
} else if script.projects.len() > 1 {
197204
println!(
198205
"Npc {} uses script {} which defines {}",
199206
npc.id,
200-
id,
207+
npc.script,
201208
script
202209
.projects
203210
.iter()
@@ -206,8 +213,25 @@ impl ScriptValidator {
206213
.join(", ")
207214
);
208215
}
209-
} else if !ci_starts_with(&id, "t_scnpc_") {
210-
println!("Npc {} uses unknown script {}", npc.id, id);
216+
} else if ci_starts_with(&npc.script, "t_scvamp_") && ci_ends_with(&npc.script, "_npc") {
217+
vampire = true;
218+
} else if !ci_starts_with(&npc.script, "t_scnpc_") {
219+
println!("Npc {} uses unknown script {}", npc.id, npc.script);
220+
return;
221+
} else {
222+
vampire = npc.script.contains("Vamp");
223+
}
224+
if vampire {
225+
let has_vampire_head = is_correct_vampire_head(
226+
&npc.head,
227+
&npc.race,
228+
npc.npc_flags.contains(NpcFlags::FEMALE),
229+
);
230+
let is_sneaky = npc.faction.eq_ignore_ascii_case("T_Cyr_VampirumOrder")
231+
|| ci_starts_with(&npc.script, "T_ScNpc_Cyr_");
232+
if !has_vampire_head && !is_sneaky {
233+
println!("Npc {} is a vampire but uses head {}", npc.id, npc.head);
234+
}
211235
}
212236
}
213237

0 commit comments

Comments
 (0)