Skip to main content

rakata_extract/
resolver.rs

1//! Shared resource resolver for ordered source families.
2//!
3//! The resolver composes multiple source families and performs first-match
4//! lookups for `(resref, type)` in the provided precedence order.
5//!
6//! ## Initial Source Families
7//! - Override (in-memory source model)
8//! - Composite module (`.rim`, `_s.rim`, `_dlg.erf`)
9//! - KEY/BIF archive pair
10//!
11//! ## Texture Sidecar Helper
12//! - `Resolver::resolve_texture_with_txi` resolves a texture resource and then
13//!   resolves an optional same-`resref` `TXI` sidecar using independent global
14//!   source precedence ordering (no same-container/source coupling).
15//! - `Resolver::resolve_tpc_with_txi_handoff` resolves and parses TPC container
16//!   data, exposing header/payload/footer plus conservative embedded-body hints.
17//!
18//! ## Model Companion Helper
19//! - `Resolver::resolve_mdl_with_mdx` resolves an MDL model resource and then
20//!   resolves an optional same-`resref` MDX vertex data companion using
21//!   independent global source precedence.
22//! - `Resolver::resolve_mdl_with_mdx_handoff` resolves and parses the MDL model
23//!   (with MDX vertex data applied if available).
24//!
25//! ## Case Sensitivity Policy
26//! - Queries are case-insensitive via canonical `ResRef` normalization.
27//! - Override collision handling is deterministic:
28//! - canonical key: `(lowercase_resref, resource_type)`
29//! - tie-break: keep first entry after deterministic source-label sorting
30//! - later duplicates are retained in collision metadata and ignored for lookup
31
32use std::borrow::Cow;
33use std::collections::HashMap;
34
35use rakata_core::{ResRef, ResRefError};
36use rakata_core::{ResourceId, ResourceType, ResourceTypeCode};
37use rakata_formats::{
38    read_dds_from_bytes, read_mdl_from_bytes, read_tga_from_bytes, read_tpc_from_bytes, Bif, Key,
39    Mdl, MdlError, Tpc, TpcBinaryError,
40};
41use thiserror::Error;
42
43use crate::composite_module::{CompositeModule, CompositeModuleSource};
44use crate::keyfile::KeyFile;
45
46use crate::util::{cmp_ascii_case_insensitive, trace_debug};
47
48/// High-level source family used by the shared resolver.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum ResolverSourceFamily {
51    /// Override source family.
52    Override,
53    /// Composite module source family.
54    CompositeModule,
55    /// KEY/BIF source family.
56    KeyBif,
57}
58
59/// Concrete provenance for a resolved resource.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum ResolutionProvenance {
62    /// Resource came from an override entry.
63    Override,
64    /// Resource came from one composite module part.
65    Composite(CompositeModuleSource),
66    /// Resource came from KEY/BIF lookup.
67    KeyBif {
68        /// BIF index selected from KEY packed resource ID.
69        bif_index: u32,
70        /// Packed resource ID selected from KEY table.
71        resource_id: ResourceId,
72    },
73}
74
75impl ResolutionProvenance {
76    /// Returns the high-level source family for this provenance.
77    pub fn family(self) -> ResolverSourceFamily {
78        match self {
79            Self::Override => ResolverSourceFamily::Override,
80            Self::Composite(_) => ResolverSourceFamily::CompositeModule,
81            Self::KeyBif { .. } => ResolverSourceFamily::KeyBif,
82        }
83    }
84}
85
86/// Resolver output view.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ResolverResult<'a> {
89    /// Concrete source provenance.
90    pub provenance: ResolutionProvenance,
91    /// Canonical query resref.
92    pub resref: ResRef,
93    /// Raw on-disk resource type code.
94    pub resource_type: ResourceTypeCode,
95    /// Payload bytes (borrowed from in-memory sources, owned for seek-based).
96    pub data: Cow<'a, [u8]>,
97}
98
99/// Resolver output for a texture resource and optional TXI sidecar.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct TextureWithTxiResult<'a> {
102    /// Resolved texture resource.
103    pub texture: ResolverResult<'a>,
104    /// Optional resolved TXI sidecar for the same `resref`, resolved through
105    /// independent global precedence.
106    pub txi: Option<ResolverResult<'a>>,
107}
108
109/// Resolver output for a TPC resource, parsed container payload, and optional
110/// external TXI sidecar.
111#[derive(Debug, Clone, PartialEq)]
112pub struct TpcWithTxiHandoffResult<'a> {
113    /// Resolved TPC resource plus optional external TXI sidecar.
114    pub resolved: TextureWithTxiResult<'a>,
115    /// Parsed TPC container with separated header/payload/footer.
116    pub tpc: Tpc,
117}
118
119/// Conservative interpretation hint for TPC embedded payload bytes.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum TpcEmbeddedPayloadHint {
122    /// Payload parses as a legacy DDS stream and starts with `DDS ` magic.
123    DdsByMagic,
124    /// Payload parses as TGA bytes.
125    LikelyTga,
126    /// Payload does not match currently recognized hints.
127    Unknown,
128}
129
130impl<'a> TpcWithTxiHandoffResult<'a> {
131    /// Returns a conservative hint for interpreting embedded payload bytes.
132    pub fn embedded_payload_hint(&self) -> TpcEmbeddedPayloadHint {
133        classify_tpc_embedded_payload(&self.tpc.payload)
134    }
135}
136
137/// Resolver output for an MDL model resource and optional MDX vertex data.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct MdlWithMdxResult<'a> {
140    /// Resolved MDL model resource.
141    pub mdl: ResolverResult<'a>,
142    /// Optional resolved MDX vertex data for the same `resref`, resolved
143    /// through independent global source precedence.
144    pub mdx: Option<ResolverResult<'a>>,
145}
146
147/// Resolver output for a parsed MDL model with optional MDX vertex data.
148#[derive(Debug, Clone, PartialEq)]
149pub struct MdlWithMdxHandoffResult<'a> {
150    /// Resolved MDL resource plus optional MDX companion.
151    pub resolved: MdlWithMdxResult<'a>,
152    /// Parsed MDL model (with MDX vertex data applied if MDX was resolved).
153    pub model: Mdl,
154}
155
156/// One override entry.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct OverrideEntry {
159    /// Resource name.
160    pub resref: ResRef,
161    /// Resource type.
162    pub resource_type: ResourceTypeCode,
163    /// Payload bytes.
164    pub data: Vec<u8>,
165    /// Stable source label (for deterministic collision tie-breaking).
166    pub source_label: String,
167}
168
169/// Input model used to build deterministic override sources.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct OverrideInput {
172    /// Resource name (validated to [`ResRef`]).
173    pub resref: String,
174    /// Resource type.
175    pub resource_type: ResourceTypeCode,
176    /// Payload bytes.
177    pub data: Vec<u8>,
178    /// Stable source label used for deterministic ordering.
179    pub source_label: String,
180}
181
182/// Collision metadata for duplicate canonical override keys.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct OverrideCollision {
185    /// Canonical key `resref`.
186    pub resref: ResRef,
187    /// Canonical key resource type.
188    pub resource_type: ResourceTypeCode,
189    /// Label of kept entry.
190    pub kept_source_label: String,
191    /// Label of ignored duplicate entry.
192    pub ignored_source_label: String,
193}
194
195/// In-memory override source with deterministic duplicate handling.
196#[derive(Debug, Clone, Default, PartialEq, Eq)]
197pub struct OverrideSource {
198    entries: Vec<OverrideEntry>,
199    key_index: HashMap<(ResRef, ResourceTypeCode), usize>,
200    collisions: Vec<OverrideCollision>,
201}
202
203impl OverrideSource {
204    /// Creates an empty override source.
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Builds an override source from unsorted inputs.
210    ///
211    /// Inputs are sorted by source label (case-insensitive, then original text)
212    /// before insertion to keep collision outcomes deterministic.
213    #[cfg_attr(
214        feature = "tracing",
215        tracing::instrument(level = "debug", skip(inputs))
216    )]
217    pub fn from_inputs(mut inputs: Vec<OverrideInput>) -> Result<Self, ResolverError> {
218        trace_debug!(input_count = inputs.len(), "building override source");
219        inputs.sort_by(|a, b| {
220            cmp_ascii_case_insensitive(&a.source_label, &b.source_label)
221                .then(a.source_label.cmp(&b.source_label))
222        });
223
224        let mut source = Self::new();
225        for input in inputs {
226            source.add_entry(
227                &input.resref,
228                input.resource_type,
229                input.data,
230                input.source_label,
231            )?;
232        }
233        trace_debug!(
234            entry_count = source.entries.len(),
235            collision_count = source.collisions.len(),
236            "built override source"
237        );
238        Ok(source)
239    }
240
241    /// Adds one override entry.
242    ///
243    /// Duplicate canonical keys are ignored after the first match and recorded
244    /// in [`Self::collisions`].
245    pub fn add_entry(
246        &mut self,
247        resref: &str,
248        resource_type: ResourceTypeCode,
249        data: Vec<u8>,
250        source_label: impl Into<String>,
251    ) -> Result<(), ResolverError> {
252        #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(this, data, source_label), fields(resref = resref, resource_type = resource_type.raw_id())))]
253        fn add_entry_inner(
254            this: &mut OverrideSource,
255            resref: &str,
256            resource_type: ResourceTypeCode,
257            data: Vec<u8>,
258            source_label: impl Into<String>,
259        ) -> Result<(), ResolverError> {
260            let source_label = source_label.into();
261            let canonical = ResRef::new(resref).map_err(|source| ResolverError::InvalidResRef {
262                value: resref.to_string(),
263                source,
264            })?;
265            let key = (canonical, resource_type);
266
267            if let Some(kept_index) = this.key_index.get(&key).copied() {
268                trace_debug!(
269                    kept_source = this.entries[kept_index].source_label.as_str(),
270                    ignored_source = source_label.as_str(),
271                    "override collision: deterministic keep-first rule applied"
272                );
273                this.collisions.push(OverrideCollision {
274                    resref: canonical,
275                    resource_type,
276                    kept_source_label: this.entries[kept_index].source_label.clone(),
277                    ignored_source_label: source_label,
278                });
279                return Ok(());
280            }
281
282            let index = this.entries.len();
283            this.entries.push(OverrideEntry {
284                resref: canonical,
285                resource_type,
286                data,
287                source_label,
288            });
289            this.key_index.insert(key, index);
290            Ok(())
291        }
292        add_entry_inner(self, resref, resource_type, data, source_label)
293    }
294
295    /// Returns all collision records.
296    pub fn collisions(&self) -> &[OverrideCollision] {
297        &self.collisions
298    }
299
300    /// Resolves one resource from the override source.
301    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
302    pub fn resolve(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> Option<&[u8]> {
303        let resolved = self
304            .key_index
305            .get(&(*resref, resource_type))
306            .and_then(|index| self.entries.get(*index))
307            .map(|entry| entry.data.as_slice());
308        trace_debug!(resolved = resolved.is_some(), "override lookup complete");
309        resolved
310    }
311}
312
313/// One BIF binding used by [`KeyBifSource`].
314#[derive(Debug, Clone, Copy)]
315pub struct KeyBifBinding<'a> {
316    /// BIF index from KEY `resource_id`.
317    pub bif_index: u32,
318    /// BIF archive value for that index.
319    pub bif: &'a Bif,
320}
321
322/// KEY/BIF resolver source family.
323///
324/// This adapter resolves `(resref, type)` by:
325/// 1. looking up the entry in KEY (case-insensitive on resref),
326/// 2. extracting `(bif_index, resource_id)` from that key entry,
327/// 3. reading payload bytes from the matching BIF.
328#[derive(Debug, Clone)]
329pub struct KeyBifSource<'a> {
330    key: &'a Key,
331    bifs: Vec<KeyBifBinding<'a>>,
332    /// O(1) KEY resource index: `(lowercase_resref, type) -> Vec index`.
333    key_resource_index: HashMap<(ResRef, ResourceTypeCode), usize>,
334    /// O(1) BIF binding index: `bif_index -> Vec index` in `self.bifs`.
335    bif_binding_index: HashMap<u32, usize>,
336}
337
338impl<'a> KeyBifSource<'a> {
339    /// Creates a KEY/BIF source bound to one KEY index.
340    pub fn new(key: &'a Key) -> Self {
341        let key_resource_index = build_key_resource_index_from_key(key);
342        Self {
343            key,
344            bifs: Vec::new(),
345            key_resource_index,
346            bif_binding_index: HashMap::new(),
347        }
348    }
349
350    /// Returns the KEY index bound to this source.
351    pub fn key(&self) -> &'a Key {
352        self.key
353    }
354
355    /// Returns all registered BIF bindings.
356    pub fn bifs(&self) -> &[KeyBifBinding<'a>] {
357        &self.bifs
358    }
359
360    /// Appends one BIF binding.
361    pub fn push_bif(&mut self, bif_index: u32, bif: &'a Bif) {
362        let vec_index = self.bifs.len();
363        self.bifs.push(KeyBifBinding { bif_index, bif });
364        self.bif_binding_index.entry(bif_index).or_insert(vec_index);
365    }
366
367    /// Appends one BIF binding and returns `self`.
368    pub fn with_bif(mut self, bif_index: u32, bif: &'a Bif) -> Self {
369        self.push_bif(bif_index, bif);
370        self
371    }
372
373    fn bif_for_index(&self, bif_index: u32) -> Option<&'a Bif> {
374        let &vec_index = self.bif_binding_index.get(&bif_index)?;
375        self.bifs.get(vec_index).map(|binding| binding.bif)
376    }
377
378    /// Resolves one resource using KEY/BIF metadata.
379    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
380    pub fn resolve(
381        &self,
382        resref: &ResRef,
383        resource_type: ResourceTypeCode,
384    ) -> Option<(u32, ResourceId, &'a [u8])> {
385        let &key_index = self.key_resource_index.get(&(*resref, resource_type))?;
386        let key_entry = self.key.resources.get(key_index)?;
387        let bif_index = key_entry.bif_index();
388        let bif = self.bif_for_index(bif_index)?;
389        let data = bif.resource_by_id(key_entry.resource_id)?;
390        trace_debug!(
391            bif_index,
392            resource_id = key_entry.resource_id.raw(),
393            data_len = data.len(),
394            "key/bif lookup resolved"
395        );
396        Some((bif_index, key_entry.resource_id, data))
397    }
398}
399
400/// Borrowed resolver source reference.
401#[derive(Debug, Clone, Copy)]
402pub enum ResolverSourceRef<'a> {
403    /// Override source reference.
404    Override(&'a OverrideSource),
405    /// Composite module source reference.
406    CompositeModule(&'a CompositeModule),
407    /// KEY/BIF in-memory source reference (requires all BIFs loaded in memory).
408    KeyBif(&'a KeyBifSource<'a>),
409    /// KEY/BIF seek-based source reference (reads individual resources from
410    /// disk on demand, without loading entire BIF files into memory).
411    KeyFile(&'a KeyFile),
412}
413
414/// Shared ordered resolver.
415#[derive(Debug, Clone, Default)]
416pub struct Resolver<'a> {
417    sources: Vec<ResolverSourceRef<'a>>,
418}
419
420impl<'a> Resolver<'a> {
421    /// Creates an empty resolver.
422    pub fn new() -> Self {
423        Self {
424            sources: Vec::new(),
425        }
426    }
427
428    /// Returns all configured source references in precedence order.
429    pub fn sources(&self) -> &[ResolverSourceRef<'a>] {
430        &self.sources
431    }
432
433    /// Appends one source reference.
434    pub fn push_source(&mut self, source: ResolverSourceRef<'a>) {
435        self.sources.push(source);
436    }
437
438    /// Appends one source and returns `self`.
439    pub fn with_source(mut self, source: ResolverSourceRef<'a>) -> Self {
440        self.push_source(source);
441        self
442    }
443
444    fn resolve_from_source(
445        source: ResolverSourceRef<'a>,
446        resref: &ResRef,
447        resource_type: ResourceTypeCode,
448    ) -> Option<ResolverResult<'a>> {
449        match source {
450            ResolverSourceRef::Override(override_source) => {
451                let data = override_source.resolve(resref, resource_type)?;
452                trace_debug!(source = "override", data_len = data.len(), "resolver hit");
453                Some(ResolverResult {
454                    provenance: ResolutionProvenance::Override,
455                    resref: *resref,
456                    resource_type,
457                    data: Cow::Borrowed(data),
458                })
459            }
460            ResolverSourceRef::CompositeModule(composite) => {
461                let found = composite.resolve(resref, resource_type)?;
462                trace_debug!(
463                    source = ?found.source,
464                    data_len = found.data.len(),
465                    "resolver hit"
466                );
467                Some(ResolverResult {
468                    provenance: ResolutionProvenance::Composite(found.source),
469                    resref: found.resref,
470                    resource_type: found.resource_type,
471                    data: Cow::Borrowed(found.data),
472                })
473            }
474            ResolverSourceRef::KeyBif(key_bif) => {
475                let (bif_index, resource_id, data) = key_bif.resolve(resref, resource_type)?;
476                trace_debug!(
477                    source = "key_bif",
478                    bif_index,
479                    resource_id = resource_id.raw(),
480                    data_len = data.len(),
481                    "resolver hit"
482                );
483                Some(ResolverResult {
484                    provenance: ResolutionProvenance::KeyBif {
485                        bif_index,
486                        resource_id,
487                    },
488                    resref: *resref,
489                    resource_type,
490                    data: Cow::Borrowed(data),
491                })
492            }
493            ResolverSourceRef::KeyFile(key_file) => {
494                let entry = key_file.resource_entry(resref, resource_type)?;
495                let bif_index = entry.resource_id.bif_index();
496                let resource_id = entry.resource_id;
497                match key_file.read_resource_by_seek(bif_index, resource_id) {
498                    Ok(data) => {
499                        trace_debug!(
500                            source = "key_file",
501                            bif_index,
502                            resource_id = resource_id.raw(),
503                            data_len = data.len(),
504                            "resolver hit"
505                        );
506                        Some(ResolverResult {
507                            provenance: ResolutionProvenance::KeyBif {
508                                bif_index,
509                                resource_id,
510                            },
511                            resref: *resref,
512                            resource_type,
513                            data: Cow::Owned(data),
514                        })
515                    }
516                    Err(err) => {
517                        #[cfg(feature = "tracing")]
518                        tracing::debug!(
519                            error = %err,
520                            "key_file BIF seek read failed, treating as miss"
521                        );
522                        let _ = err;
523                        None
524                    }
525                }
526            }
527        }
528    }
529
530    fn resolve_with_source_index(
531        &self,
532        resref: &ResRef,
533        resource_type: ResourceTypeCode,
534    ) -> Option<(usize, ResolverResult<'a>)> {
535        for (index, source) in self.sources.iter().copied().enumerate() {
536            if let Some(result) = Self::resolve_from_source(source, resref, resource_type) {
537                return Some((index, result));
538            }
539        }
540        None
541    }
542
543    /// Resolves one resource using configured source precedence.
544    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, resource_type = resource_type.raw_id())))]
545    pub fn resolve(
546        &self,
547        resref: &ResRef,
548        resource_type: ResourceTypeCode,
549    ) -> Option<ResolverResult<'_>> {
550        let resolved = self.resolve_with_source_index(resref, resource_type);
551        if resolved.is_none() {
552            trace_debug!("resolver miss");
553        }
554        resolved.map(|(_, result)| result)
555    }
556
557    /// Resolves one texture resource and optional `TXI` sidecar.
558    ///
559    /// Resolution behavior:
560    /// 1. Resolve the texture using normal source precedence.
561    /// 2. Resolve `TXI` with the same `resref` using independent global source
562    ///    precedence (not bound to the texture's winning source).
563    ///
564    /// If texture resolution fails, returns `None` even if a `TXI` exists.
565    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref, texture_type = texture_type.raw_id())))]
566    pub fn resolve_texture_with_txi(
567        &self,
568        resref: &ResRef,
569        texture_type: ResourceTypeCode,
570    ) -> Option<TextureWithTxiResult<'_>> {
571        let texture = self.resolve(resref, texture_type)?;
572        let txi_type = ResourceTypeCode::from(ResourceType::Txi);
573        let txi = self.resolve(resref, txi_type);
574        trace_debug!(
575            texture_source = ?texture.provenance.family(),
576            txi_resolved = txi.is_some(),
577            "texture+txi resolution complete"
578        );
579        Some(TextureWithTxiResult { texture, txi })
580    }
581
582    /// Resolves one resource from raw query values.
583    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = resref, resource_type = resource_type_id)))]
584    #[doc(hidden)]
585    pub fn resolve_raw(
586        &self,
587        resref: &str,
588        resource_type_id: u16,
589    ) -> Result<Option<ResolverResult<'_>>, ResolverError> {
590        let resref = ResRef::new(resref).map_err(|source| ResolverError::InvalidResRef {
591            value: resref.to_string(),
592            source,
593        })?;
594        Ok(self.resolve(&resref, ResourceTypeCode::from_raw_id(resource_type_id)))
595    }
596
597    /// Resolves one texture and optional `TXI` sidecar from raw query values.
598    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = resref, texture_type = texture_type_id)))]
599    pub fn resolve_texture_with_txi_raw(
600        &self,
601        resref: &str,
602        texture_type_id: u16,
603    ) -> Result<Option<TextureWithTxiResult<'_>>, ResolverError> {
604        let resref = ResRef::new(resref).map_err(|source| ResolverError::InvalidResRef {
605            value: resref.to_string(),
606            source,
607        })?;
608        Ok(self.resolve_texture_with_txi(&resref, ResourceTypeCode::from_raw_id(texture_type_id)))
609    }
610
611    /// Resolves one `TPC` resource, parses it, and returns handoff-ready
612    /// container parts plus an optional external `TXI` sidecar.
613    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref)))]
614    pub fn resolve_tpc_with_txi_handoff(
615        &self,
616        resref: &ResRef,
617    ) -> Result<Option<TpcWithTxiHandoffResult<'_>>, TpcHandoffError> {
618        let resolved = match self
619            .resolve_texture_with_txi(resref, ResourceTypeCode::from(ResourceType::Tpc))
620        {
621            Some(resolved) => resolved,
622            None => {
623                trace_debug!("tpc handoff miss");
624                return Ok(None);
625            }
626        };
627        let tpc = read_tpc_from_bytes(&resolved.texture.data)
628            .map_err(|source| TpcHandoffError::ParseTpc { source })?;
629        trace_debug!(
630            payload_len = tpc.payload.len(),
631            txi_footer_len = tpc.txi_footer.len(),
632            "tpc handoff parse complete"
633        );
634        Ok(Some(TpcWithTxiHandoffResult { resolved, tpc }))
635    }
636
637    /// Resolves one `TPC` resource and optional `TXI` sidecar from a raw query
638    /// `resref`, returning parsed handoff-ready container parts.
639    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = resref)))]
640    pub fn resolve_tpc_with_txi_handoff_raw(
641        &self,
642        resref: &str,
643    ) -> Result<Option<TpcWithTxiHandoffResult<'_>>, TpcHandoffError> {
644        let resref = ResRef::new(resref).map_err(|source| TpcHandoffError::InvalidResRef {
645            value: resref.to_string(),
646            source,
647        })?;
648        self.resolve_tpc_with_txi_handoff(&resref)
649    }
650
651    /// Resolves one MDL model resource and optional MDX vertex data companion.
652    ///
653    /// Resolution behavior:
654    /// 1. Resolve the MDL using normal source precedence.
655    /// 2. Resolve MDX with the same `resref` using independent global source
656    ///    precedence (not bound to the MDL's winning source).
657    ///
658    /// If MDL resolution fails, returns `None` even if an MDX exists.
659    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref)))]
660    pub fn resolve_mdl_with_mdx(&self, resref: &ResRef) -> Option<MdlWithMdxResult<'_>> {
661        let mdl = self.resolve(resref, ResourceTypeCode::from(ResourceType::Mdl))?;
662        let mdx_type = ResourceTypeCode::from(ResourceType::Mdx);
663        let mdx = self.resolve(resref, mdx_type);
664        trace_debug!(
665            mdl_source = ?mdl.provenance.family(),
666            mdx_resolved = mdx.is_some(),
667            "mdl+mdx resolution complete"
668        );
669        Some(MdlWithMdxResult { mdl, mdx })
670    }
671
672    /// Resolves one MDL model and optional MDX companion from raw query values.
673    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = resref)))]
674    pub fn resolve_mdl_with_mdx_raw(
675        &self,
676        resref: &str,
677    ) -> Result<Option<MdlWithMdxResult<'_>>, ResolverError> {
678        let resref = ResRef::new(resref).map_err(|source| ResolverError::InvalidResRef {
679            value: resref.to_string(),
680            source,
681        })?;
682        Ok(self.resolve_mdl_with_mdx(&resref))
683    }
684
685    /// Resolves one MDL model resource, parses it (with MDX vertex data if
686    /// available), and returns a handoff-ready parsed model.
687    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = %resref)))]
688    pub fn resolve_mdl_with_mdx_handoff(
689        &self,
690        resref: &ResRef,
691    ) -> Result<Option<MdlWithMdxHandoffResult<'_>>, MdlHandoffError> {
692        let resolved = match self.resolve_mdl_with_mdx(resref) {
693            Some(resolved) => resolved,
694            None => {
695                trace_debug!("mdl handoff miss");
696                return Ok(None);
697            }
698        };
699        let mdx_data = resolved.mdx.as_ref().map(|r| r.data.as_ref());
700        let model = read_mdl_from_bytes(&resolved.mdl.data, mdx_data)
701            .map_err(|source| MdlHandoffError::ParseMdl { source })?;
702        trace_debug!(
703            node_count = model.node_count,
704            root_name = model.root_node.name.as_str(),
705            has_mdx = mdx_data.is_some(),
706            "mdl handoff parse complete"
707        );
708        Ok(Some(MdlWithMdxHandoffResult { resolved, model }))
709    }
710
711    /// Resolves one MDL model and optional MDX companion from a raw query
712    /// `resref`, returning a parsed handoff-ready model.
713    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(resref = resref)))]
714    pub fn resolve_mdl_with_mdx_handoff_raw(
715        &self,
716        resref: &str,
717    ) -> Result<Option<MdlWithMdxHandoffResult<'_>>, MdlHandoffError> {
718        let resref = ResRef::new(resref).map_err(|source| MdlHandoffError::InvalidResRef {
719            value: resref.to_string(),
720            source,
721        })?;
722        self.resolve_mdl_with_mdx_handoff(&resref)
723    }
724}
725
726/// Errors produced by resolver setup/query operations.
727#[derive(Debug, Error)]
728pub enum ResolverError {
729    /// Query/entry resref failed validation.
730    #[error("invalid resref `{value}`: {source}")]
731    InvalidResRef {
732        /// Original invalid value.
733        value: String,
734        /// Validation details.
735        #[source]
736        source: ResRefError,
737    },
738}
739
740/// Errors produced by TPC handoff helper operations.
741#[derive(Debug, Error)]
742pub enum TpcHandoffError {
743    /// Query `resref` failed validation.
744    #[error("invalid resref `{value}`: {source}")]
745    InvalidResRef {
746        /// Original invalid value.
747        value: String,
748        /// Validation details.
749        #[source]
750        source: ResRefError,
751    },
752    /// TPC container parsing failed after resource resolution.
753    #[error("failed to parse resolved TPC: {source}")]
754    ParseTpc {
755        /// Underlying TPC parse/validation error.
756        #[source]
757        source: TpcBinaryError,
758    },
759}
760
761/// Errors produced by MDL handoff helper operations.
762#[derive(Debug, Error)]
763pub enum MdlHandoffError {
764    /// Query `resref` failed validation.
765    #[error("invalid resref `{value}`: {source}")]
766    InvalidResRef {
767        /// Original invalid value.
768        value: String,
769        /// Validation details.
770        #[source]
771        source: ResRefError,
772    },
773    /// MDL parsing failed after resource resolution.
774    #[error("failed to parse resolved MDL: {source}")]
775    ParseMdl {
776        /// Underlying MDL parse error.
777        #[source]
778        source: MdlError,
779    },
780}
781
782/// Builds a `(ResRef, ResourceTypeCode) -> index` HashMap from KEY resources.
783///
784/// First-match wins for duplicate keys, matching engine linear-scan semantics.
785fn build_key_resource_index_from_key(key: &Key) -> HashMap<(ResRef, ResourceTypeCode), usize> {
786    let mut index = HashMap::with_capacity(key.resources.len());
787    for (i, entry) in key.resources.iter().enumerate() {
788        index
789            .entry((entry.resref, entry.resource_type))
790            .or_insert(i);
791    }
792    index
793}
794
795fn classify_tpc_embedded_payload(payload: &[u8]) -> TpcEmbeddedPayloadHint {
796    if payload.starts_with(b"DDS ") && read_dds_from_bytes(payload).is_ok() {
797        return TpcEmbeddedPayloadHint::DdsByMagic;
798    }
799    if read_tga_from_bytes(payload).is_ok() {
800        return TpcEmbeddedPayloadHint::LikelyTga;
801    }
802    TpcEmbeddedPayloadHint::Unknown
803}
804
805#[cfg(test)]
806mod tests {
807    use rakata_core::ResRef;
808    use rakata_core::{ResourceId, ResourceType, ResourceTypeCode};
809    use rakata_formats::ErfFileType;
810
811    use crate::composite_module::{CompositeModule, CompositeModuleSource};
812
813    use super::{
814        classify_tpc_embedded_payload, KeyBifSource, OverrideInput, OverrideSource,
815        ResolutionProvenance, Resolver, ResolverError, ResolverSourceRef, TpcEmbeddedPayloadHint,
816        TpcHandoffError,
817    };
818
819    fn sample_composite(payload: &[u8]) -> CompositeModule {
820        let mut erf = rakata_formats::Erf::new(ErfFileType::Erf);
821        erf.push_resource(
822            ResRef::new("module").expect("valid resref"),
823            ResourceTypeCode::from(ResourceType::Dlg),
824            payload.to_vec(),
825        );
826        CompositeModule::from_archives("m01aa", None, None, Some(erf)).expect("composite")
827    }
828
829    fn sample_key_bif(payload: &[u8]) -> (rakata_formats::Key, rakata_formats::Bif) {
830        let mut key = rakata_formats::Key::new();
831        key.push_bif_entry("chitin\\test.bif", 0, 0);
832        let resource_id = ResourceId::from_parts(0, 1).expect("resource id");
833        key.push_resource(
834            ResRef::new("MoDuLe").expect("valid resref"),
835            ResourceTypeCode::from(ResourceType::Dlg),
836            resource_id,
837        );
838
839        let mut bif = rakata_formats::Bif::new();
840        bif.push_resource(
841            resource_id,
842            ResourceTypeCode::from(ResourceType::Dlg),
843            payload.to_vec(),
844        );
845
846        (key, bif)
847    }
848
849    fn sample_tpc_bytes(payload: &[u8], txi_footer: &[u8]) -> Vec<u8> {
850        let header = rakata_formats::TpcHeader {
851            data_size: 0,
852            alpha_test: 0.5,
853            width: 2,
854            height: 2,
855            pixel_type: 4,
856            mipmap_count: 1,
857            reserved: [0_u8; 114],
858        };
859        let tpc = rakata_formats::Tpc::new(header, payload.to_vec(), txi_footer.to_vec());
860        rakata_formats::write_tpc_to_vec(&tpc).expect("tpc bytes")
861    }
862
863    fn sample_tpc_bytes_exact_payload(payload: &[u8]) -> Vec<u8> {
864        let width = u16::try_from(payload.len()).expect("payload len fits u16");
865        let header = rakata_formats::TpcHeader {
866            data_size: 0,
867            alpha_test: 0.0,
868            width,
869            height: 1,
870            pixel_type: 1,
871            mipmap_count: 1,
872            reserved: [0_u8; 114],
873        };
874        let tpc = rakata_formats::Tpc::new(header, payload.to_vec(), Vec::new());
875        rakata_formats::write_tpc_to_vec(&tpc).expect("tpc bytes")
876    }
877
878    fn sample_dds_bytes() -> Vec<u8> {
879        let mut dds = rakata_formats::Dds::new_d3d(rakata_formats::DdsNewD3dParams {
880            height: 4,
881            width: 4,
882            depth: None,
883            format: rakata_formats::DdsD3dFormat::DXT1,
884            mipmap_levels: Some(1),
885            caps2: None,
886        })
887        .expect("valid dds");
888        dds.data.fill(0x11);
889        rakata_formats::write_dds_to_vec(&dds).expect("dds bytes")
890    }
891
892    fn sample_tga_bytes() -> Vec<u8> {
893        let tga =
894            rakata_formats::Tga::new_rgba(1, 1, vec![0x11, 0x22, 0x33, 0x44]).expect("valid tga");
895        rakata_formats::write_tga_to_vec(&tga).expect("tga bytes")
896    }
897
898    #[test]
899    fn resolver_prefers_sources_in_configured_order() {
900        let composite = sample_composite(b"composite");
901        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
902            resref: "module".into(),
903            resource_type: ResourceTypeCode::from(ResourceType::Dlg),
904            data: b"override".to_vec(),
905            source_label: "override/Module.dlg".into(),
906        }])
907        .expect("override source");
908
909        let resolver = Resolver::new()
910            .with_source(ResolverSourceRef::Override(&override_source))
911            .with_source(ResolverSourceRef::CompositeModule(&composite));
912
913        let resref = ResRef::new("module").expect("resref");
914        let resolved = resolver
915            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
916            .expect("must resolve");
917        assert_eq!(resolved.provenance, ResolutionProvenance::Override);
918        assert_eq!(&*resolved.data, b"override");
919    }
920
921    #[test]
922    fn resolver_falls_back_to_key_bif_when_higher_sources_miss() {
923        let composite = sample_composite(b"composite");
924        let override_source = OverrideSource::new();
925        let mut key = rakata_formats::Key::new();
926        key.push_bif_entry("chitin\\test.bif", 0, 0);
927        let resource_id = ResourceId::from_parts(0, 2).expect("resource id");
928        key.push_resource(
929            ResRef::new("keyonly").expect("valid resref"),
930            ResourceTypeCode::from(ResourceType::Dlg),
931            resource_id,
932        );
933        let mut bif = rakata_formats::Bif::new();
934        bif.push_resource(
935            resource_id,
936            ResourceTypeCode::from(ResourceType::Dlg),
937            b"keybif".to_vec(),
938        );
939        let key_bif = KeyBifSource::new(&key).with_bif(0, &bif);
940
941        let resolver = Resolver::new()
942            .with_source(ResolverSourceRef::Override(&override_source))
943            .with_source(ResolverSourceRef::CompositeModule(&composite))
944            .with_source(ResolverSourceRef::KeyBif(&key_bif));
945
946        let resref = ResRef::new("keyonly").expect("resref");
947        let resolved = resolver
948            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
949            .expect("must resolve");
950        assert_eq!(
951            resolved.provenance,
952            ResolutionProvenance::KeyBif {
953                bif_index: 0,
954                resource_id,
955            }
956        );
957        assert_eq!(&*resolved.data, b"keybif");
958
959        let missing_resref = ResRef::new("only_in_key").expect("resref");
960        assert!(resolver
961            .resolve(&missing_resref, ResourceTypeCode::from(ResourceType::Dlg))
962            .is_none());
963    }
964
965    #[test]
966    fn resolver_falls_back_to_composite_when_override_missing() {
967        let composite = sample_composite(b"composite");
968        let override_source = OverrideSource::new();
969
970        let resolver = Resolver::new()
971            .with_source(ResolverSourceRef::Override(&override_source))
972            .with_source(ResolverSourceRef::CompositeModule(&composite));
973
974        let resref = ResRef::new("module").expect("resref");
975        let resolved = resolver
976            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
977            .expect("must resolve");
978        assert_eq!(
979            resolved.provenance,
980            ResolutionProvenance::Composite(CompositeModuleSource::DialogErf)
981        );
982        assert_eq!(&*resolved.data, b"composite");
983    }
984
985    #[test]
986    fn key_bif_lookup_is_case_insensitive_for_resref() {
987        let (key, bif) = sample_key_bif(b"keybif");
988        let key_bif = KeyBifSource::new(&key).with_bif(0, &bif);
989        let resolver = Resolver::new().with_source(ResolverSourceRef::KeyBif(&key_bif));
990
991        let resref = ResRef::new("module").expect("resref");
992        let resolved = resolver
993            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
994            .expect("must resolve");
995        assert_eq!(
996            resolved.provenance,
997            ResolutionProvenance::KeyBif {
998                bif_index: 0,
999                resource_id: ResourceId::from_parts(0, 1).expect("resource id"),
1000            }
1001        );
1002        assert_eq!(&*resolved.data, b"keybif");
1003    }
1004
1005    #[test]
1006    fn key_bif_lookup_returns_none_when_bif_binding_missing() {
1007        let mut key = rakata_formats::Key::new();
1008        key.push_bif_entry("chitin\\missing.bif", 0, 0);
1009        let resource_id = ResourceId::from_parts(1, 0).expect("resource id");
1010        key.push_resource(
1011            ResRef::new("module").expect("valid resref"),
1012            ResourceTypeCode::from(ResourceType::Dlg),
1013            resource_id,
1014        );
1015        let bif = rakata_formats::Bif::new();
1016        let key_bif = KeyBifSource::new(&key).with_bif(0, &bif);
1017        let resolver = Resolver::new().with_source(ResolverSourceRef::KeyBif(&key_bif));
1018
1019        let resref = ResRef::new("module").expect("resref");
1020        assert!(resolver
1021            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1022            .is_none());
1023    }
1024
1025    #[test]
1026    fn key_bif_lookup_prefers_first_duplicate_key_entry() {
1027        let mut key = rakata_formats::Key::new();
1028        key.push_bif_entry("chitin\\a.bif", 0, 0);
1029        key.push_bif_entry("chitin\\b.bif", 0, 0);
1030        let first_id = ResourceId::from_parts(0, 1).expect("resource id");
1031        let second_id = ResourceId::from_parts(1, 2).expect("resource id");
1032        key.push_resource(
1033            ResRef::new("module").expect("valid resref"),
1034            ResourceTypeCode::from(ResourceType::Dlg),
1035            first_id,
1036        );
1037        key.push_resource(
1038            ResRef::new("module").expect("valid resref"),
1039            ResourceTypeCode::from(ResourceType::Dlg),
1040            second_id,
1041        );
1042
1043        let mut bif_a = rakata_formats::Bif::new();
1044        bif_a.push_resource(
1045            first_id,
1046            ResourceTypeCode::from(ResourceType::Dlg),
1047            b"first".to_vec(),
1048        );
1049        let mut bif_b = rakata_formats::Bif::new();
1050        bif_b.push_resource(
1051            second_id,
1052            ResourceTypeCode::from(ResourceType::Dlg),
1053            b"second".to_vec(),
1054        );
1055
1056        let key_bif = KeyBifSource::new(&key)
1057            .with_bif(0, &bif_a)
1058            .with_bif(1, &bif_b);
1059        let resolver = Resolver::new().with_source(ResolverSourceRef::KeyBif(&key_bif));
1060        let resref = ResRef::new("module").expect("resref");
1061        let resolved = resolver
1062            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1063            .expect("must resolve");
1064
1065        assert_eq!(
1066            resolved.provenance,
1067            ResolutionProvenance::KeyBif {
1068                bif_index: 0,
1069                resource_id: first_id,
1070            }
1071        );
1072        assert_eq!(&*resolved.data, b"first");
1073    }
1074
1075    #[test]
1076    fn resolver_prefers_override_then_composite_then_key_bif() {
1077        let composite = sample_composite(b"composite");
1078        let (key, bif) = sample_key_bif(b"keybif");
1079        let key_bif = KeyBifSource::new(&key).with_bif(0, &bif);
1080
1081        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1082            resref: "module".into(),
1083            resource_type: ResourceTypeCode::from(ResourceType::Dlg),
1084            data: b"override".to_vec(),
1085            source_label: "override/module.dlg".into(),
1086        }])
1087        .expect("override source");
1088        let resolver = Resolver::new()
1089            .with_source(ResolverSourceRef::Override(&override_source))
1090            .with_source(ResolverSourceRef::CompositeModule(&composite))
1091            .with_source(ResolverSourceRef::KeyBif(&key_bif));
1092        let resref = ResRef::new("module").expect("resref");
1093        let resolved = resolver
1094            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1095            .expect("must resolve");
1096        assert_eq!(&*resolved.data, b"override");
1097        assert_eq!(resolved.provenance, ResolutionProvenance::Override);
1098
1099        let resolver_without_override = Resolver::new()
1100            .with_source(ResolverSourceRef::CompositeModule(&composite))
1101            .with_source(ResolverSourceRef::KeyBif(&key_bif));
1102        let resolved = resolver_without_override
1103            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1104            .expect("must resolve");
1105        assert_eq!(&*resolved.data, b"composite");
1106        assert_eq!(
1107            resolved.provenance,
1108            ResolutionProvenance::Composite(CompositeModuleSource::DialogErf)
1109        );
1110
1111        let resolver_key_only = Resolver::new().with_source(ResolverSourceRef::KeyBif(&key_bif));
1112        let resolved = resolver_key_only
1113            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1114            .expect("must resolve");
1115        assert_eq!(&*resolved.data, b"keybif");
1116        assert_eq!(
1117            resolved.provenance,
1118            ResolutionProvenance::KeyBif {
1119                bif_index: 0,
1120                resource_id: ResourceId::from_parts(0, 1).expect("resource id"),
1121            }
1122        );
1123    }
1124
1125    #[test]
1126    fn resolver_raw_query_validates_resref() {
1127        let resolver = Resolver::new();
1128        // Characters with no Windows-1252 mapping (CJK, emoji) are
1129        // the only resref inputs the validator still rejects post
1130        // engine audit. See resref.rs for the full rule set.
1131        let err = resolver
1132            .resolve_raw("名前", ResourceType::Dlg.type_id())
1133            .expect_err("invalid resref should fail");
1134        assert!(matches!(err, ResolverError::InvalidResRef { .. }));
1135    }
1136
1137    #[test]
1138    fn override_lookup_is_case_insensitive() {
1139        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1140            resref: "MoDuLe".into(),
1141            resource_type: ResourceTypeCode::from(ResourceType::Dlg),
1142            data: b"override".to_vec(),
1143            source_label: "override/Module.dlg".into(),
1144        }])
1145        .expect("override source");
1146        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1147
1148        let resref = ResRef::new("module").expect("resref");
1149        let resolved = resolver
1150            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1151            .expect("must resolve");
1152        assert_eq!(&*resolved.data, b"override");
1153    }
1154
1155    #[test]
1156    fn override_collisions_are_deterministic_from_unsorted_inputs() {
1157        let source = OverrideSource::from_inputs(vec![
1158            OverrideInput {
1159                resref: "module".into(),
1160                resource_type: ResourceTypeCode::from(ResourceType::Dlg),
1161                data: b"late".to_vec(),
1162                source_label: "z_override/module.dlg".into(),
1163            },
1164            OverrideInput {
1165                resref: "MoDuLe".into(),
1166                resource_type: ResourceTypeCode::from(ResourceType::Dlg),
1167                data: b"early".to_vec(),
1168                source_label: "a_override/MODULE.dlg".into(),
1169            },
1170        ])
1171        .expect("override source");
1172
1173        assert_eq!(source.collisions().len(), 1);
1174
1175        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&source));
1176        let resref = ResRef::new("module").expect("resref");
1177        let resolved = resolver
1178            .resolve(&resref, ResourceTypeCode::from(ResourceType::Dlg))
1179            .expect("must resolve");
1180        assert_eq!(&*resolved.data, b"early");
1181    }
1182
1183    #[test]
1184    fn texture_with_txi_returns_none_when_texture_is_missing() {
1185        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1186            resref: "module".into(),
1187            resource_type: ResourceTypeCode::from(ResourceType::Txi),
1188            data: b"mipmap 0".to_vec(),
1189            source_label: "override/module.txi".into(),
1190        }])
1191        .expect("override source");
1192        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1193
1194        let resref = ResRef::new("module").expect("resref");
1195        assert!(resolver
1196            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Tpc))
1197            .is_none());
1198    }
1199
1200    #[test]
1201    fn texture_with_txi_is_case_insensitive_for_pairing() {
1202        let override_source = OverrideSource::from_inputs(vec![
1203            OverrideInput {
1204                resref: "MoDuLe".into(),
1205                resource_type: ResourceTypeCode::from(ResourceType::Tpc),
1206                data: b"texture".to_vec(),
1207                source_label: "override/module.tpc".into(),
1208            },
1209            OverrideInput {
1210                resref: "module".into(),
1211                resource_type: ResourceTypeCode::from(ResourceType::Txi),
1212                data: b"mipmap 0".to_vec(),
1213                source_label: "override/module.txi".into(),
1214            },
1215        ])
1216        .expect("override source");
1217        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1218
1219        let resref = ResRef::new("MODULE").expect("resref");
1220        let resolved = resolver
1221            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Tpc))
1222            .expect("must resolve");
1223        assert_eq!(&*resolved.texture.data, b"texture");
1224        assert_eq!(&*resolved.txi.expect("txi").data, b"mipmap 0");
1225    }
1226
1227    #[test]
1228    fn texture_with_txi_uses_independent_global_source_order() {
1229        let mut dialog_erf = rakata_formats::Erf::new(ErfFileType::Erf);
1230        dialog_erf.push_resource(
1231            ResRef::new("module").expect("valid resref"),
1232            ResourceTypeCode::from(ResourceType::Tpc),
1233            b"composite-texture".to_vec(),
1234        );
1235        dialog_erf.push_resource(
1236            ResRef::new("module").expect("valid resref"),
1237            ResourceTypeCode::from(ResourceType::Txi),
1238            b"composite-txi".to_vec(),
1239        );
1240        let composite =
1241            CompositeModule::from_archives("m01aa", None, None, Some(dialog_erf)).expect("module");
1242        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1243            resref: "module".into(),
1244            resource_type: ResourceTypeCode::from(ResourceType::Tpc),
1245            data: b"override-texture".to_vec(),
1246            source_label: "override/module.tpc".into(),
1247        }])
1248        .expect("override source");
1249
1250        let resolver = Resolver::new()
1251            .with_source(ResolverSourceRef::Override(&override_source))
1252            .with_source(ResolverSourceRef::CompositeModule(&composite));
1253        let resref = ResRef::new("module").expect("resref");
1254        let resolved = resolver
1255            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Tpc))
1256            .expect("must resolve");
1257        assert_eq!(resolved.texture.provenance, ResolutionProvenance::Override);
1258        assert_eq!(&*resolved.texture.data, b"override-texture");
1259        let txi = resolved.txi.expect("txi");
1260        assert_eq!(
1261            txi.provenance,
1262            ResolutionProvenance::Composite(CompositeModuleSource::DialogErf)
1263        );
1264        assert_eq!(&*txi.data, b"composite-txi");
1265    }
1266
1267    #[test]
1268    fn texture_with_txi_supports_tpc_tga_and_dds_queries() {
1269        let mut inputs = Vec::new();
1270        for (label, ty) in [
1271            ("tpc", ResourceType::Tpc),
1272            ("tga", ResourceType::Tga),
1273            ("dds", ResourceType::Dds),
1274        ] {
1275            inputs.push(OverrideInput {
1276                resref: "module".into(),
1277                resource_type: ResourceTypeCode::from(ty),
1278                data: label.as_bytes().to_vec(),
1279                source_label: format!("override/module.{label}"),
1280            });
1281        }
1282        inputs.push(OverrideInput {
1283            resref: "module".into(),
1284            resource_type: ResourceTypeCode::from(ResourceType::Txi),
1285            data: b"mipmap 0".to_vec(),
1286            source_label: "override/module.txi".into(),
1287        });
1288
1289        let override_source = OverrideSource::from_inputs(inputs).expect("override");
1290        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1291        let resref = ResRef::new("module").expect("resref");
1292
1293        let resolved_tpc = resolver
1294            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Tpc))
1295            .expect("tpc");
1296        assert_eq!(&*resolved_tpc.texture.data, b"tpc");
1297        assert_eq!(&*resolved_tpc.txi.expect("txi").data, b"mipmap 0");
1298
1299        let resolved_tga = resolver
1300            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Tga))
1301            .expect("tga");
1302        assert_eq!(&*resolved_tga.texture.data, b"tga");
1303        assert_eq!(&*resolved_tga.txi.expect("txi").data, b"mipmap 0");
1304
1305        let resolved_dds = resolver
1306            .resolve_texture_with_txi(&resref, ResourceTypeCode::from(ResourceType::Dds))
1307            .expect("dds");
1308        assert_eq!(&*resolved_dds.texture.data, b"dds");
1309        assert_eq!(&*resolved_dds.txi.expect("txi").data, b"mipmap 0");
1310    }
1311
1312    #[test]
1313    fn tpc_handoff_parses_container_and_preserves_boundaries() {
1314        let payload = vec![0xAA; 16];
1315        let embedded_txi = b"cube 1\n".to_vec();
1316        let tpc_bytes = sample_tpc_bytes(&payload, &embedded_txi);
1317
1318        let override_source = OverrideSource::from_inputs(vec![
1319            OverrideInput {
1320                resref: "module".into(),
1321                resource_type: ResourceTypeCode::from(ResourceType::Tpc),
1322                data: tpc_bytes.clone(),
1323                source_label: "override/module.tpc".into(),
1324            },
1325            OverrideInput {
1326                resref: "module".into(),
1327                resource_type: ResourceTypeCode::from(ResourceType::Txi),
1328                data: b"external 1".to_vec(),
1329                source_label: "override/module.txi".into(),
1330            },
1331        ])
1332        .expect("override source");
1333        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1334
1335        let resref = ResRef::new("module").expect("resref");
1336        let handoff = resolver
1337            .resolve_tpc_with_txi_handoff(&resref)
1338            .expect("handoff parse")
1339            .expect("resolved");
1340        assert_eq!(&*handoff.resolved.texture.data, &*tpc_bytes);
1341        assert_eq!(handoff.tpc.payload, payload);
1342        assert_eq!(handoff.tpc.txi_footer, embedded_txi);
1343        assert_eq!(&*handoff.resolved.txi.expect("txi").data, b"external 1");
1344    }
1345
1346    #[test]
1347    fn tpc_handoff_reports_parse_errors_for_invalid_tpc_data() {
1348        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1349            resref: "module".into(),
1350            resource_type: ResourceTypeCode::from(ResourceType::Tpc),
1351            data: b"bad".to_vec(),
1352            source_label: "override/module.tpc".into(),
1353        }])
1354        .expect("override source");
1355        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1356        let resref = ResRef::new("module").expect("resref");
1357
1358        let err = resolver
1359            .resolve_tpc_with_txi_handoff(&resref)
1360            .expect_err("invalid tpc should fail");
1361        assert!(matches!(err, TpcHandoffError::ParseTpc { .. }));
1362    }
1363
1364    #[test]
1365    fn tpc_handoff_exposes_embedded_payload_hint() {
1366        let payload = sample_dds_bytes();
1367        let tpc_bytes = sample_tpc_bytes_exact_payload(&payload);
1368
1369        let override_source = OverrideSource::from_inputs(vec![OverrideInput {
1370            resref: "module".into(),
1371            resource_type: ResourceTypeCode::from(ResourceType::Tpc),
1372            data: tpc_bytes,
1373            source_label: "override/module.tpc".into(),
1374        }])
1375        .expect("override source");
1376        let resolver = Resolver::new().with_source(ResolverSourceRef::Override(&override_source));
1377        let resref = ResRef::new("module").expect("resref");
1378
1379        let handoff = resolver
1380            .resolve_tpc_with_txi_handoff(&resref)
1381            .expect("handoff parse")
1382            .expect("resolved");
1383        assert_eq!(
1384            handoff.embedded_payload_hint(),
1385            TpcEmbeddedPayloadHint::DdsByMagic
1386        );
1387    }
1388
1389    #[test]
1390    fn tpc_handoff_raw_query_validates_resref() {
1391        let resolver = Resolver::new();
1392        // Characters with no Windows-1252 mapping (CJK, emoji) are
1393        // the only resref inputs the validator still rejects post
1394        // engine audit. See resref.rs for the full rule set.
1395        let err = resolver
1396            .resolve_tpc_with_txi_handoff_raw("名前")
1397            .expect_err("invalid resref should fail");
1398        assert!(matches!(err, TpcHandoffError::InvalidResRef { .. }));
1399    }
1400
1401    #[test]
1402    fn tpc_embedded_payload_hint_detects_dds_magic() {
1403        let payload = sample_dds_bytes();
1404        assert_eq!(
1405            classify_tpc_embedded_payload(payload.as_slice()),
1406            TpcEmbeddedPayloadHint::DdsByMagic
1407        );
1408    }
1409
1410    #[test]
1411    fn tpc_embedded_payload_hint_detects_likely_tga() {
1412        let payload = sample_tga_bytes();
1413        assert_eq!(
1414            classify_tpc_embedded_payload(payload.as_slice()),
1415            TpcEmbeddedPayloadHint::LikelyTga
1416        );
1417    }
1418
1419    #[test]
1420    fn tpc_embedded_payload_hint_is_unknown_when_no_signature_matches() {
1421        let payload = [0xFF_u8; 12];
1422        assert_eq!(
1423            classify_tpc_embedded_payload(&payload),
1424            TpcEmbeddedPayloadHint::Unknown
1425        );
1426    }
1427
1428    #[test]
1429    fn tpc_embedded_payload_hint_requires_valid_dds_parse() {
1430        let payload = b"DDS not-a-valid-dds-container";
1431        assert_eq!(
1432            classify_tpc_embedded_payload(payload),
1433            TpcEmbeddedPayloadHint::Unknown
1434        );
1435    }
1436}