Skip to content

Commit c44fbc6

Browse files
committed
Report missing destination aid markers
1 parent 8ff12c3 commit c44fbc6

5 files changed

Lines changed: 144 additions & 7 deletions

File tree

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.13.0"
3+
version = "2.14.0"
44
edition = "2021"
55

66
[dependencies]

WARNINGS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ This script doesn't implement the standardized Khajiit script, but does set the
287287
### Contains unexpected line set T_Local_Khajiit to X
288288
This script sets the variable to an unexpected value.
289289

290+
### Lacks a comment
291+
`PlaceItem[Cell]`, `Position[Cell]`, `AiEscort[Cell]`, `AiTravel[Cell]`, and `AiFollow[Cell]` to places other than (0, 0, 0) require a comment explaining the destination.
292+
This comment can include a marker ID (an ID containing `_MARK_`) if so, the marker needs to be a used NPC marker.
293+
294+
### Refers to marker which is not a book
295+
No book matching the detected marker ID exists.
296+
297+
### Refers to book which is not a(n NPC) marker
298+
A book matching the detected marker ID exists, but it's not using the correct mesh.
299+
300+
### Refers to marker which has no references
301+
No instances of the detected marker exist in this file.
302+
290303
## Magic
291304

292305
### Uses effect

src/util.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,12 @@ pub fn cannot_sleep(cell: &Cell) -> bool {
202202
cell.data.flags.contains(CellFlags::RESTING_IS_ILLEGAL)
203203
}
204204

205+
pub const NPC_MARKER: &str = "tr\\tr_editormarker_npc.nif";
206+
205207
pub fn is_marker(book: &Book) -> bool {
206208
let mesh = &book.mesh;
207209
mesh.eq_ignore_ascii_case("tr\\tr_note_pin.nif")
208-
|| mesh.eq_ignore_ascii_case("tr\\tr_editormarker_npc.nif")
210+
|| mesh.eq_ignore_ascii_case(NPC_MARKER)
209211
|| mesh.eq_ignore_ascii_case("tr\\tr_editormarker_landmark.nif")
210212
|| mesh.eq_ignore_ascii_case("editormarker.nif")
211213
}

src/validators/scripts.rs

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ use super::Context;
44
use crate::{
55
context::Mode,
66
handlers::Handler,
7-
util::{ci_ends_with, ci_starts_with, is_correct_vampire_head, is_khajiit, Actor},
7+
util::{
8+
ci_ends_with, ci_starts_with, is_correct_vampire_head, is_khajiit, is_marker, Actor,
9+
NPC_MARKER,
10+
},
811
};
912
use codegen::{get_joined_commands, get_khajiit_script};
1013
use regex::{Regex, RegexBuilder};
11-
use tes3::esp::{Dialogue, Npc, NpcFlags, Script, TES3Object};
14+
use tes3::esp::{Cell, Dialogue, Npc, NpcFlags, Reference, Script, TES3Object};
15+
16+
enum PositionMarkerType {
17+
Unknown,
18+
Book,
19+
Marker,
20+
NpcMarker,
21+
}
1222

1323
pub struct ScriptValidator {
1424
unique_heads: HashSet<&'static str>,
@@ -23,6 +33,9 @@ pub struct ScriptValidator {
2333
set_khajiit_neg1: Regex,
2434
set_khajiit_var: Regex,
2535
position: Regex,
36+
markers: HashMap<String, (String, PositionMarkerType, bool, i32)>,
37+
needs_marker: Regex,
38+
marker_id: Regex,
2639
}
2740

2841
struct ScriptInfo {
@@ -86,15 +99,31 @@ impl Handler<'_> for ScriptValidator {
8699
self.check_npc_script(npc);
87100
}
88101
}
102+
} else if let TES3Object::Book(book) = record {
103+
let id = book.id.to_ascii_lowercase();
104+
let marker = if book.mesh.eq_ignore_ascii_case(NPC_MARKER) {
105+
PositionMarkerType::NpcMarker
106+
} else if is_marker(book) {
107+
PositionMarkerType::Marker
108+
} else {
109+
PositionMarkerType::Book
110+
};
111+
if let Some((_, marker_type, _, _)) = self.markers.get_mut(&id) {
112+
*marker_type = marker;
113+
} else if let Some(found) = self.marker_id.find(&book.id) {
114+
if found.len() == book.id.len() {
115+
self.markers.insert(id, (String::new(), marker, false, 0));
116+
}
117+
}
89118
}
90119
}
91120

92121
fn on_scriptline(
93122
&mut self,
94-
_: &Context,
123+
context: &Context,
95124
record: &TES3Object,
96125
code: &str,
97-
_: &str,
126+
comment: &str,
98127
topic: &Dialogue,
99128
) {
100129
if !code.is_empty() && self.position.is_match(code) {
@@ -107,6 +136,50 @@ impl Handler<'_> for ScriptValidator {
107136
println!("Script {} uses Position instead of PositionCell", script.id);
108137
}
109138
}
139+
if context.mode != Mode::Vanilla && self.needs_marker.is_match(code) {
140+
if comment.is_empty() {
141+
if let TES3Object::DialogueInfo(info) = record {
142+
println!(
143+
"Info {} in topic {} lacks a comment for {}",
144+
info.id, topic.id, code
145+
);
146+
} else if let TES3Object::Script(script) = record {
147+
println!("Script {} lacks a comment for {}", script.id, code);
148+
}
149+
} else if let Some(capture) = self.marker_id.captures(comment) {
150+
if let Some(group) = capture.get(2) {
151+
let description = if let TES3Object::DialogueInfo(info) = record {
152+
format!("Info {} in topic {}", info.id, topic.id)
153+
} else if let TES3Object::Script(script) = record {
154+
format!("Script {}", script.id)
155+
} else {
156+
String::new()
157+
};
158+
let id = group.as_str().to_ascii_lowercase();
159+
if let Some((desc, _, used, _)) = self.markers.get_mut(&id) {
160+
*desc = description;
161+
*used = true;
162+
} else {
163+
self.markers
164+
.insert(id, (description, PositionMarkerType::Unknown, false, 0));
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
fn on_cellref(
172+
&mut self,
173+
_: &Context,
174+
_: &'_ Cell,
175+
_: &Reference,
176+
id: &str,
177+
_: &[&Reference],
178+
_: usize,
179+
) {
180+
if let Some((_, _, _, count)) = self.markers.get_mut(id) {
181+
*count += 1;
182+
}
110183
}
111184

112185
fn on_end(&mut self, context: &Context) {
@@ -120,6 +193,38 @@ impl Handler<'_> for ScriptValidator {
120193
}
121194
}
122195
}
196+
for (id, (description, is_book, used, count)) in &self.markers {
197+
if !used {
198+
continue;
199+
}
200+
match *is_book {
201+
PositionMarkerType::Unknown => {
202+
println!(
203+
"{} refers to marker {} which is not a book",
204+
description, id
205+
);
206+
}
207+
PositionMarkerType::Book => {
208+
println!(
209+
"{} refers to book {} which is not a marker",
210+
description, id
211+
);
212+
}
213+
PositionMarkerType::Marker => {
214+
println!(
215+
"{} refers to book {} which is not an NPC marker",
216+
description, id
217+
);
218+
}
219+
_ => {}
220+
}
221+
if *count == 0 {
222+
println!(
223+
"{} refers to marker {} which has no references",
224+
description, id
225+
);
226+
}
227+
}
123228
}
124229
}
125230

@@ -160,6 +265,20 @@ impl ScriptValidator {
160265
.case_insensitive(true)
161266
.build()?;
162267
let position = Regex::new(r"^([,\s]*|.*?->[,\s]*)position[,\s]+")?;
268+
let needs_marker = Regex::new(
269+
r#"^([,\s]*|.*?->[,\s]*)((position|aitravel|aiescort|placeitem)(cell)?[,\s])|(aifollow(cell[,\s]+("[^"]+"|[^,\s]+))?[,\s]+("[^"]+"|[^,\s]+)[,\s]+[0-9]+([,\s][0.]+){3,})"#,
270+
)?;
271+
let marker_id_pattern = r"(^|[,\s])((".to_string()
272+
+ &context
273+
.projects
274+
.iter()
275+
.map(|project| project.prefix)
276+
.collect::<Vec<_>>()
277+
.join("|")
278+
+ r#")[a-z0-9-_']*_mark_[a-z0-9-_']*)"#;
279+
let marker_id = RegexBuilder::new(&marker_id_pattern)
280+
.case_insensitive(true)
281+
.build()?;
163282
Ok(Self {
164283
unique_heads,
165284
scripts: HashMap::new(),
@@ -173,6 +292,9 @@ impl ScriptValidator {
173292
set_khajiit_neg1,
174293
set_khajiit_var,
175294
position,
295+
needs_marker,
296+
marker_id,
297+
markers: HashMap::new(),
176298
})
177299
}
178300

0 commit comments

Comments
 (0)