Compare commits
28 Commits
auto/re-is
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6dbcfead | ||
|
|
69291a315f | ||
|
|
4734797e50 | ||
|
|
ecbd70f69a | ||
|
|
b5a193839c | ||
|
|
5cdff5e515 | ||
|
|
77cd58202b | ||
|
|
306a8a5661 | ||
|
|
fb70511242 | ||
|
|
47e4e43310 | ||
|
|
1054d43676 | ||
|
|
e1dcc689bc | ||
|
|
03ece95c06 | ||
|
|
1fce3f9c71 | ||
|
|
8124b85fc7 | ||
|
|
245b73243b | ||
|
|
0cbef2023f | ||
|
|
50625b5a9e | ||
|
|
e5c6c27e6a | ||
|
|
a7d5a3bd12 | ||
|
|
1f4524f9fe | ||
|
|
3b67a451c2 | ||
|
|
fa3818c58c | ||
|
|
1ba16da9a4 | ||
|
|
e9c06e2e87 | ||
|
|
eb75b62f82 | ||
|
|
e1e4f7bc1d | ||
|
|
8af6a3f5d5 |
@@ -67,6 +67,10 @@ pub mod movie_manifest;
|
|||||||
|
|
||||||
pub mod movie_voice;
|
pub mod movie_voice;
|
||||||
|
|
||||||
|
/// Assembling media whose bytes are not one archive entry — segment-spanning
|
||||||
|
/// reads, multi-sub-wave banks, and the continuous cutscene-voice stream.
|
||||||
|
pub mod media;
|
||||||
|
|
||||||
pub mod game_data;
|
pub mod game_data;
|
||||||
|
|
||||||
pub mod localization;
|
pub mod localization;
|
||||||
|
|||||||
305
crates/sylpheed-formats/src/media.rs
Normal file
305
crates/sylpheed-formats/src/media.rs
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
//! Assembling media that does **not** sit in one place on the disc.
|
||||||
|
//!
|
||||||
|
//! Most assets are one archive entry and are read with [`crate::pak`] alone.
|
||||||
|
//! Audio is not, and this module owns every case where the bytes of one playable
|
||||||
|
//! thing have to be gathered from somewhere other than a single entry:
|
||||||
|
//!
|
||||||
|
//! * **An entry spans segment files.** A `.pak` TOC offset addresses the
|
||||||
|
//! *concatenated* `.p00….pNN` stream, so one entry routinely straddles two
|
||||||
|
//! files on disc. [`DiscSource::read_segment_range`] is the seam for that.
|
||||||
|
//! * **A bank holds several sub-waves.** A `.slb` is an XACT bank; its sub-waves
|
||||||
|
//! are either alternate takes or sequential segments of one line, and only
|
||||||
|
//! concatenating them all and clamping to the known length gets both right.
|
||||||
|
//! * **A cutscene voice is not in its own bank.** The movie voices are one
|
||||||
|
//! continuous XMA stream chunked into `VOICE_*.slb` TOC entries whose
|
||||||
|
//! boundaries do **not** match the cutscene cues. A cue routinely spans two
|
||||||
|
//! chunks, so *a `.slb` need not hold the track its name claims*.
|
||||||
|
//! [`resolve_movie_voice_region`] resolves a movie to a byte region of the
|
||||||
|
//! stream instead, which is the only reading that produces the right audio.
|
||||||
|
//!
|
||||||
|
//! ## Why this lives in `sylpheed-formats` and not in a viewer
|
||||||
|
//!
|
||||||
|
//! It used to live in the Bevy viewer, which meant the one piece of logic most
|
||||||
|
//! likely to be re-derived incorrectly was in the crate least likely to be
|
||||||
|
//! reused. Anything that reads the disc — the viewer, a CLI, an asset exporter
|
||||||
|
//! for a port — needs the same answers, and there must be one implementation of
|
||||||
|
//! them.
|
||||||
|
//!
|
||||||
|
//! ## What deliberately stays out
|
||||||
|
//!
|
||||||
|
//! Decoding. This module returns **XMA `RIFF`s**, not PCM: turning XMA into
|
||||||
|
//! samples means shelling out to FFmpeg, which is a native-only dependency and
|
||||||
|
//! a policy decision for the consumer. The seam is "here are the bytes that
|
||||||
|
//! belong together" — everything up to that point is disc knowledge, everything
|
||||||
|
//! after it is a codec choice.
|
||||||
|
|
||||||
|
use crate::pak::PakArchive;
|
||||||
|
use crate::slb::VoiceLang;
|
||||||
|
|
||||||
|
/// Where disc bytes come from. Implemented over an extracted directory, an ISO,
|
||||||
|
/// or anything else that can serve the same three questions.
|
||||||
|
///
|
||||||
|
/// It is a trait rather than a concrete type because the callers differ in ways
|
||||||
|
/// this module should not know about: a viewer reads from whichever source the
|
||||||
|
/// user opened, a headless exporter reads from a fixed extract, and a test reads
|
||||||
|
/// from a fixture.
|
||||||
|
pub trait DiscSource {
|
||||||
|
/// Read a whole file by disc-relative path, e.g. `dat/sound.pak`.
|
||||||
|
fn read_file(&self, path: &str) -> Result<Vec<u8>, String>;
|
||||||
|
|
||||||
|
/// Open an IPFB archive by disc-relative path, with its `.pNN` segments.
|
||||||
|
fn open_pak(&self, path: &str) -> Result<PakArchive, String>;
|
||||||
|
|
||||||
|
/// Read `len` bytes at `offset` into the concatenated `<stem>.p00….pNN`
|
||||||
|
/// stream, where `stem` is a disc-relative path without extension
|
||||||
|
/// (`dat/sound`). The range may cross a segment boundary; that is the point.
|
||||||
|
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read one `sound.pak` bank by name-hash, taking only its byte range from the
|
||||||
|
/// segments rather than inflating the 1.07 GB archive.
|
||||||
|
pub fn read_sound_bank<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
name_hash: u32,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let toc = source.read_file("dat/sound.pak")?;
|
||||||
|
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
|
||||||
|
let idx = entries
|
||||||
|
.binary_search_by_key(&name_hash, |e| e.name_hash)
|
||||||
|
.map_err(|_| "not present in sound.pak".to_string())?;
|
||||||
|
let e = &entries[idx];
|
||||||
|
source.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The XMA `RIFF`s of one named bank, in the order they must be concatenated.
|
||||||
|
///
|
||||||
|
/// Every sub-wave is returned, not just the first. The two bank shapes need
|
||||||
|
/// this for opposite reasons: a **segment** bank (`VOICE_RT07A` = 24 s + 14 s +
|
||||||
|
/// 11 s ≈ the 50 s movie) is only complete when all of them are joined, and an
|
||||||
|
/// **alternate-take** bank (`VOICE_S00A`, whose sub-wave 0 already spans the
|
||||||
|
/// whole movie) is trimmed by the caller's length clamp. Taking sub-wave 0 alone
|
||||||
|
/// dropped two thirds of the dialogue on segment banks — that was a real bug.
|
||||||
|
pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
clip_name: &str,
|
||||||
|
) -> Result<Vec<Vec<u8>>, String> {
|
||||||
|
let bytes = read_sound_bank(source, crate::hash::name_hash(clip_name))?;
|
||||||
|
Ok(riffs_of(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
|
||||||
|
/// by [`resolve_movie_voice_region`].
|
||||||
|
pub fn voice_region_riffs<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
start: u64,
|
||||||
|
end: u64,
|
||||||
|
) -> Result<Vec<Vec<u8>>, String> {
|
||||||
|
let bytes = source.read_segment_range("dat/sound", start, (end - start) as usize)?;
|
||||||
|
Ok(riffs_of(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sub-wave `RIFF`s of a bank's bytes, with the single-stream fallback.
|
||||||
|
///
|
||||||
|
/// Some banks — the data-before-header `\etc\` radio clips — defeat the
|
||||||
|
/// multi-sub-wave scanner, and the robust single-stream reader handles them. An
|
||||||
|
/// empty result here means genuinely undecodable, not "scanner confused".
|
||||||
|
fn riffs_of(bytes: &[u8]) -> Vec<Vec<u8>> {
|
||||||
|
let riffs = crate::slb::to_xma_riffs(bytes);
|
||||||
|
if riffs.is_empty() {
|
||||||
|
crate::slb::to_xma_riff_best(bytes).into_iter().collect()
|
||||||
|
} else {
|
||||||
|
riffs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a movie's voice bank **name** through the manifest in `tables.pak`.
|
||||||
|
///
|
||||||
|
/// Only the manifest's DIRECT bindings are trusted. Extending this to unbound
|
||||||
|
/// resupply movies by shared demo line was tried and verified WRONG — it played
|
||||||
|
/// the wrong recording — so an unbound movie stays unvoiced rather than play a
|
||||||
|
/// guess. `None` therefore means "this cutscene has no voice-over", which is a
|
||||||
|
/// real answer for most `hokyu_*` movies.
|
||||||
|
pub fn resolve_movie_voice_clip<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
movie: &str,
|
||||||
|
lang: VoiceLang,
|
||||||
|
) -> Option<String> {
|
||||||
|
let pak = source.open_pak("dat/tables.pak").ok()?;
|
||||||
|
let manifest = find_manifest(&pak)?;
|
||||||
|
let sounds = pak
|
||||||
|
.read_by_name(&format!("{}\\sounds.tbl", lang.code_pub()))?
|
||||||
|
.ok()?;
|
||||||
|
crate::movie_manifest::resolve_voice_entry(&manifest, &sounds, movie, lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The manifest has no stable name, so it is found by shape among the entries.
|
||||||
|
fn find_manifest(pak: &PakArchive) -> Option<Vec<u8>> {
|
||||||
|
pak.entries().iter().find_map(|e| {
|
||||||
|
pak.read(e)
|
||||||
|
.ok()
|
||||||
|
.filter(|b| crate::movie_manifest::is_manifest(b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Voice token for a hokyu (resupply) cutscene the manifest leaves unbound.
|
||||||
|
///
|
||||||
|
/// Only 5 of the 18 hokyu movies carry an explicit `VOICETRACK`; the rest reuse
|
||||||
|
/// those recordings. The selector is the cutscene's **demo id** (from its
|
||||||
|
/// subtitle track), NOT the ship category: `hokyu_LS_s02A` and `hokyu_LS_s11A`
|
||||||
|
/// are both LS/carrier but use demos 600 vs 601, whose lines differ. So the map
|
||||||
|
/// is derived from the 5 bound hokyu — each of which has both a subtitle demo id
|
||||||
|
/// and a `VOICETRACK` — and the target movie's demo id is looked up in it.
|
||||||
|
pub fn hokyu_voice_token<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
movie: &str,
|
||||||
|
lang: VoiceLang,
|
||||||
|
manifest: &[u8],
|
||||||
|
) -> Option<String> {
|
||||||
|
use crate::movie_subtitle as ms;
|
||||||
|
if !movie.starts_with("hokyu_") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let lang_pak = source
|
||||||
|
.open_pak(&format!("dat/movie/{}.pak", lang.code_pub()))
|
||||||
|
.ok()?;
|
||||||
|
let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?;
|
||||||
|
crate::movie_manifest::parse(manifest)
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|e| {
|
||||||
|
let tok = e.voice_token.filter(|_| e.movie.starts_with("hokyu_"))?;
|
||||||
|
ms::track_voice_cues(&lang_pak, &e.movie)
|
||||||
|
.iter()
|
||||||
|
.any(|&(d, _)| d == want)
|
||||||
|
.then_some(tok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a movie's cutscene voice to a continuous `[start, end)` byte region
|
||||||
|
/// of the voice stream — **the reading that produces the right audio**.
|
||||||
|
///
|
||||||
|
/// The chain is movie → cue token (manifest) → sound id (master registry) →
|
||||||
|
/// region (scan the stream for two trailers). Each cue ends at an inline
|
||||||
|
/// `(sound_id, 0x11, …)` trailer, so cue *N* is the bytes between trailer *N-1*
|
||||||
|
/// and trailer *N*.
|
||||||
|
///
|
||||||
|
/// Returns `None` for movies whose voice is not a `\Movie\` bank — the hokyu
|
||||||
|
/// `\etc\` clips — which the caller then resolves the per-clip way via
|
||||||
|
/// [`resolve_movie_voice_clip`].
|
||||||
|
pub fn resolve_movie_voice_region<S: DiscSource + ?Sized>(
|
||||||
|
source: &S,
|
||||||
|
movie: &str,
|
||||||
|
lang: VoiceLang,
|
||||||
|
) -> Option<(u64, u64)> {
|
||||||
|
use crate::{hash::name_hash, movie_manifest, movie_voice};
|
||||||
|
let code = lang.code_pub();
|
||||||
|
let tpak = source.open_pak("dat/tables.pak").ok()?;
|
||||||
|
let manifest = find_manifest(&tpak)?;
|
||||||
|
let token = movie_manifest::voice_token(&manifest, movie)
|
||||||
|
.or_else(|| hokyu_voice_token(source, movie, lang, &manifest))?;
|
||||||
|
|
||||||
|
// token → sound id, via the large per-language IDXD entry carrying the
|
||||||
|
// `<lang>\Movie\VOICE_*.slb` paths. Located by content, like the manifest.
|
||||||
|
let marker = format!("{code}\\Movie\\VOICE_ADV.slb");
|
||||||
|
let registry = tpak.entries().iter().find_map(|e| {
|
||||||
|
tpak.read(e)
|
||||||
|
.ok()
|
||||||
|
.filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
|
||||||
|
})?;
|
||||||
|
let id = *movie_voice::registry_voice_ids(®istry).get(&token)?;
|
||||||
|
|
||||||
|
// Physical anchor: the TOC offset of this token's own `.slb` chunk. That is a
|
||||||
|
// start point NEAR the cue's trailers, not the cue itself — the cue may sit
|
||||||
|
// before or after it, which is the whole reason a region is needed. The
|
||||||
|
// token's subdirectory varies by kind.
|
||||||
|
let stoc = source.read_file("dat/sound.pak").ok()?;
|
||||||
|
let entries = PakArchive::parse_toc(&stoc).ok()?;
|
||||||
|
let anchor = ["Movie", "etc", "Voice"].iter().find_map(|dir| {
|
||||||
|
let h = name_hash(&format!("{code}\\{dir}\\{token}.slb"));
|
||||||
|
entries
|
||||||
|
.binary_search_by_key(&h, |e| e.name_hash)
|
||||||
|
.ok()
|
||||||
|
.map(|i| entries[i].offset as u64)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Scan both directions from the anchor. The window must span the largest
|
||||||
|
// bank (ADV ≈ 3.6 MB) or the predecessor trailer falls outside it.
|
||||||
|
let win_start = anchor.saturating_sub(2 * 1024 * 1024) & !3;
|
||||||
|
let window = source
|
||||||
|
.read_segment_range("dat/sound", win_start, 8 * 1024 * 1024)
|
||||||
|
.ok()?;
|
||||||
|
let end_local = movie_voice::find_descriptor(&window, id)?;
|
||||||
|
let end = win_start + end_local as u64;
|
||||||
|
|
||||||
|
// Start = the predecessor trailer. Prefer the exact `id-1`; where the id
|
||||||
|
// sequence has a gap (VOICE_D_453 → 454) fall back to the nearest trailer
|
||||||
|
// below — but only within one bank (~1.5 MB), else this is the first cue in
|
||||||
|
// its block and the audio starts at the anchor itself.
|
||||||
|
let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1))
|
||||||
|
.or_else(|| movie_voice::find_descriptor_before(&window, end_local))
|
||||||
|
.map(|o| win_start + o as u64)
|
||||||
|
.filter(|&s| s < end && end - s < 1_500_000)
|
||||||
|
.unwrap_or(anchor);
|
||||||
|
Some((start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`DiscSource`] over an **extracted** disc directory.
|
||||||
|
///
|
||||||
|
/// Provided here rather than left to each caller because every headless
|
||||||
|
/// consumer — the CLI, the disc tests, an asset exporter for a port — wants
|
||||||
|
/// exactly this and would otherwise re-derive the segment-spanning read, which
|
||||||
|
/// is the part that is easy to get subtly wrong.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub struct DirectorySource {
|
||||||
|
root: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl DirectorySource {
|
||||||
|
pub fn new(root: impl Into<std::path::PathBuf>) -> Self {
|
||||||
|
Self { root: root.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl DiscSource for DirectorySource {
|
||||||
|
fn read_file(&self, path: &str) -> Result<Vec<u8>, String> {
|
||||||
|
std::fs::read(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_pak(&self, path: &str) -> Result<PakArchive, String> {
|
||||||
|
PakArchive::open(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walks `<stem>.p00`, `.p01`, … skipping whole segments until the offset is
|
||||||
|
/// inside one, then reads across as many as the length needs. A range that
|
||||||
|
/// straddles a boundary is the normal case, not an edge case.
|
||||||
|
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String> {
|
||||||
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
let mut out = Vec::with_capacity(len);
|
||||||
|
let (mut skip, mut need) = (offset, len);
|
||||||
|
for i in 0..100u32 {
|
||||||
|
if need == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let path = self.root.join(format!("{stem}.p{i:02}"));
|
||||||
|
let Ok(meta) = std::fs::metadata(&path) else { break };
|
||||||
|
let seg_len = meta.len();
|
||||||
|
if skip >= seg_len {
|
||||||
|
skip -= seg_len;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut f = std::fs::File::open(&path).map_err(|e| e.to_string())?;
|
||||||
|
f.seek(SeekFrom::Start(skip)).map_err(|e| e.to_string())?;
|
||||||
|
let take = need.min((seg_len - skip) as usize);
|
||||||
|
let start = out.len();
|
||||||
|
out.resize(start + take, 0);
|
||||||
|
f.read_exact(&mut out[start..]).map_err(|e| e.to_string())?;
|
||||||
|
need -= take;
|
||||||
|
skip = 0;
|
||||||
|
}
|
||||||
|
if need != 0 {
|
||||||
|
return Err(format!("segment range short by {need} bytes"));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,10 @@ use crate::pak::PakArchive;
|
|||||||
|
|
||||||
/// Subtitle language. `pak_code` selects `dat/movie/<code>.pak`; `game_code`
|
/// Subtitle language. `pak_code` selects `dat/movie/<code>.pak`; `game_code`
|
||||||
/// selects `dat/GP_MAIN_GAME_<code>.pak` (the caption text).
|
/// selects `dat/GP_MAIN_GAME_<code>.pak` (the caption text).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum SubLang {
|
pub enum SubLang {
|
||||||
|
/// Default only because the disc's own default is English.
|
||||||
|
#[default]
|
||||||
English,
|
English,
|
||||||
Japanese,
|
Japanese,
|
||||||
German,
|
German,
|
||||||
|
|||||||
@@ -225,12 +225,30 @@ impl PakArchive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The raw stored bytes for an entry (still `"Z1"`-wrapped / compressed).
|
/// The raw stored bytes for an entry (still `"Z1"`-wrapped / compressed).
|
||||||
|
///
|
||||||
|
/// One entry on the retail disc declares more bytes than the segments hold:
|
||||||
|
/// `sound.pak`'s `Static.slb` (the SFX bank) claims 8 970 240 bytes at the
|
||||||
|
/// highest offset in the archive, 616 768 past the end of `sound.p04`. It is
|
||||||
|
/// not corruption and it is not our extraction — `sound.p04` is byte-for-byte
|
||||||
|
/// the size the ISO's own directory record gives, and a sweep of **every**
|
||||||
|
/// `.pak` on the disc finds this one entry and no other. So the last entry's
|
||||||
|
/// `comp_size` is an allocation size, not a stored size.
|
||||||
|
///
|
||||||
|
/// A short read is therefore allowed **only** for the highest-offset entry,
|
||||||
|
/// which is the shape the evidence supports. Any other overrun is still an
|
||||||
|
/// error: that would be real damage, and clamping it would hide the damage
|
||||||
|
/// behind a half-decoded asset.
|
||||||
pub fn stored_bytes(&self, entry: &PakEntry) -> Result<&[u8], PakError> {
|
pub fn stored_bytes(&self, entry: &PakEntry) -> Result<&[u8], PakError> {
|
||||||
let start = entry.offset as usize;
|
let start = entry.offset as usize;
|
||||||
let end = start + entry.comp_size as usize;
|
let end = start + entry.comp_size as usize;
|
||||||
self.data
|
if let Some(b) = self.data.get(start..end) {
|
||||||
.get(start..end)
|
return Ok(b);
|
||||||
.ok_or(PakError::OffsetOutOfRange {
|
}
|
||||||
|
let is_tail = self.entries.iter().all(|e| e.offset <= entry.offset);
|
||||||
|
if is_tail && start < self.data.len() {
|
||||||
|
return Ok(&self.data[start..]);
|
||||||
|
}
|
||||||
|
Err(PakError::OffsetOutOfRange {
|
||||||
offset: entry.offset,
|
offset: entry.offset,
|
||||||
size: entry.comp_size,
|
size: entry.comp_size,
|
||||||
data_len: self.data.len(),
|
data_len: self.data.len(),
|
||||||
|
|||||||
@@ -29,13 +29,25 @@ pub const XMA1_PACKET: usize = 2048;
|
|||||||
|
|
||||||
/// Voice language for cutscene audio. Only English and Japanese voice exist on
|
/// Voice language for cutscene audio. Only English and Japanese voice exist on
|
||||||
/// the disc (subtitles cover more languages, voice does not).
|
/// the disc (subtitles cover more languages, voice does not).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum VoiceLang {
|
pub enum VoiceLang {
|
||||||
|
/// The default only because the disc's own default audio track is English;
|
||||||
|
/// nothing else about the code should assume it.
|
||||||
|
#[default]
|
||||||
English,
|
English,
|
||||||
Japanese,
|
Japanese,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VoiceLang {
|
impl VoiceLang {
|
||||||
|
pub const ALL: [VoiceLang; 2] = [VoiceLang::English, VoiceLang::Japanese];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
VoiceLang::English => "English",
|
||||||
|
VoiceLang::Japanese => "Japanese",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn code(self) -> &'static str {
|
fn code(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
VoiceLang::English => "eng",
|
VoiceLang::English => "eng",
|
||||||
@@ -67,14 +79,122 @@ pub struct VoiceClip {
|
|||||||
pub display: String,
|
pub display: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate the voice/dialog clips named in a decompressed `sounds.tbl` (the
|
/// What kind of audio a `sounds.tbl` entry names.
|
||||||
/// IDXD in `tables.pak`). Extracts every `<lang>\{Voice,etc,Movie,Briefing}\…`
|
///
|
||||||
/// path ending in `.slb` for `lang`, parsed into `(name, speaker, display)`.
|
/// The split is the on-disc path shape, not a guess: the 36 language-independent
|
||||||
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
/// banks sit at the table root (`BGM_###.slb`, `JNGL_00#.slb`, `Static.slb`),
|
||||||
|
/// while everything else is under `<lang>\<dir>\`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum AudioCategory {
|
||||||
|
/// `BGM_###.slb` — 32 music tracks, language-independent.
|
||||||
|
Music,
|
||||||
|
/// `JNGL_00#.slb` — 3 short jingles (mission clear / fail stings).
|
||||||
|
Jingle,
|
||||||
|
/// `Static.slb` — the sound-effect bank, one 9 MB multi-wave bank.
|
||||||
|
Sfx,
|
||||||
|
/// `<lang>\Voice\` — in-mission radio chatter, by speaker.
|
||||||
|
Radio,
|
||||||
|
/// `<lang>\etc\` — the other spoken lines (cutscene dialogue, system).
|
||||||
|
Dialogue,
|
||||||
|
/// `<lang>\Movie\VOICE_<movie>.slb` — a cutscene's continuous voice track.
|
||||||
|
MovieVoice,
|
||||||
|
/// `<lang>\Briefing\BR<NN>_<MM>.slb` — mission briefing lines.
|
||||||
|
Briefing,
|
||||||
|
/// A `.slb` whose path matched no known shape.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioCategory {
|
||||||
|
pub const ALL: [AudioCategory; 8] = [
|
||||||
|
AudioCategory::Music,
|
||||||
|
AudioCategory::Jingle,
|
||||||
|
AudioCategory::Sfx,
|
||||||
|
AudioCategory::Radio,
|
||||||
|
AudioCategory::Dialogue,
|
||||||
|
AudioCategory::MovieVoice,
|
||||||
|
AudioCategory::Briefing,
|
||||||
|
AudioCategory::Other,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AudioCategory::Music => "Music",
|
||||||
|
AudioCategory::Jingle => "Jingles",
|
||||||
|
AudioCategory::Sfx => "Sound effects",
|
||||||
|
AudioCategory::Radio => "Radio",
|
||||||
|
AudioCategory::Dialogue => "Dialogue",
|
||||||
|
AudioCategory::MovieVoice => "Movie voice",
|
||||||
|
AudioCategory::Briefing => "Briefing",
|
||||||
|
AudioCategory::Other => "Other",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True for the categories that are spoken lines — the set
|
||||||
|
/// [`list_voice_clips`] returns.
|
||||||
|
pub fn is_voice(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
AudioCategory::Radio
|
||||||
|
| AudioCategory::Dialogue
|
||||||
|
| AudioCategory::MovieVoice
|
||||||
|
| AudioCategory::Briefing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the bank is language-independent, so it appears whichever
|
||||||
|
/// `<lang>\sounds.tbl` is read.
|
||||||
|
pub fn is_shared(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
AudioCategory::Music | AudioCategory::Jingle | AudioCategory::Sfx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify(name: &str) -> AudioCategory {
|
||||||
|
let leaf = name.rsplit('\\').next().unwrap_or(name);
|
||||||
|
if !name.contains('\\') {
|
||||||
|
return if leaf.starts_with("BGM_") {
|
||||||
|
AudioCategory::Music
|
||||||
|
} else if leaf.starts_with("JNGL_") {
|
||||||
|
AudioCategory::Jingle
|
||||||
|
} else if leaf.eq_ignore_ascii_case("Static.slb") {
|
||||||
|
AudioCategory::Sfx
|
||||||
|
} else {
|
||||||
|
AudioCategory::Other
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match name.rsplit('\\').nth(1) {
|
||||||
|
Some("Voice") => AudioCategory::Radio,
|
||||||
|
Some("etc") => AudioCategory::Dialogue,
|
||||||
|
Some("Movie") => AudioCategory::MovieVoice,
|
||||||
|
Some("Briefing") => AudioCategory::Briefing,
|
||||||
|
_ => AudioCategory::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One playable bank named in `sounds.tbl`, with the category its path implies.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AudioEntry {
|
||||||
|
pub clip: VoiceClip,
|
||||||
|
pub category: AudioCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerate **every** `.slb` bank named in a decompressed `sounds.tbl` (the
|
||||||
|
/// IDXD in `tables.pak`): the language-independent music/jingle/SFX banks at
|
||||||
|
/// the table root, plus every `<lang>\…` spoken line.
|
||||||
|
///
|
||||||
|
/// Measured on the retail disc: `eng\sounds.tbl` names 4 418 banks (36 shared +
|
||||||
|
/// 2 382 Radio + 1 821 Dialogue + 101 Briefing + 78 Movie voice) and
|
||||||
|
/// `jpn\sounds.tbl` names 5 136 (the same 36 shared + 5 100 Japanese lines).
|
||||||
|
/// Every one of the 36 shared names resolves to a `sound.pak` TOC entry under
|
||||||
|
/// [`crate::hash::name_hash`], which is the check that they are real banks and
|
||||||
|
/// not stale table text.
|
||||||
|
pub fn list_audio_entries(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<AudioEntry> {
|
||||||
let prefix = format!("{}\\", lang.code());
|
let prefix = format!("{}\\", lang.code());
|
||||||
let mut seen = std::collections::BTreeSet::new();
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
// Scan for printable-ASCII runs; keep those that look like a voice path.
|
// Scan for printable-ASCII runs; keep those that name a `.slb`.
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < sounds_tbl.len() {
|
while i < sounds_tbl.len() {
|
||||||
let start = i;
|
let start = i;
|
||||||
@@ -83,15 +203,14 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
|||||||
}
|
}
|
||||||
if i - start >= 6 {
|
if i - start >= 6 {
|
||||||
if let Ok(s) = std::str::from_utf8(&sounds_tbl[start..i]) {
|
if let Ok(s) = std::str::from_utf8(&sounds_tbl[start..i]) {
|
||||||
// Every spoken-line category, so the standalone player covers them
|
// Take this language's entries plus the root (shared) banks; a
|
||||||
// all: in-mission radio (`\Voice\`, `\etc\`) and bound movie voices
|
// path under the OTHER language would be a table artefact.
|
||||||
// (`\Movie\`) all carry `VOICE_`; mission-briefing lines live in
|
let mine = s.starts_with(&prefix) || !s.contains('\\');
|
||||||
// `\Briefing\` as `BR<NN>_<MM>.slb` (no `VOICE` in the name).
|
if mine && s.ends_with(".slb") && seen.insert(s.to_string()) {
|
||||||
let is_voice = s.contains("VOICE") || s.contains("\\Briefing\\");
|
out.push(AudioEntry {
|
||||||
if s.starts_with(&prefix) && s.ends_with(".slb") && is_voice {
|
category: AudioCategory::classify(s),
|
||||||
if seen.insert(s.to_string()) {
|
clip: parse_voice_clip(s),
|
||||||
out.push(parse_voice_clip(s));
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,6 +219,21 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enumerate just the spoken-line clips — [`list_audio_entries`] restricted to
|
||||||
|
/// [`AudioCategory::is_voice`].
|
||||||
|
///
|
||||||
|
/// In-mission radio (`\Voice\`, `\etc\`) and bound movie voices (`\Movie\`) all
|
||||||
|
/// carry `VOICE_`; mission-briefing lines live in `\Briefing\` as
|
||||||
|
/// `BR<NN>_<MM>.slb` and carry no `VOICE` at all, which is why the category —
|
||||||
|
/// i.e. the directory — decides this and not the filename.
|
||||||
|
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
||||||
|
list_audio_entries(sounds_tbl, lang)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| e.category.is_voice())
|
||||||
|
.map(|e| e.clip)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_voice_clip(name: &str) -> VoiceClip {
|
fn parse_voice_clip(name: &str) -> VoiceClip {
|
||||||
// `<lang>\<cat>\VOICE_<SPK>_<NNN>.slb` or `..\VOICE_<movie>.slb`.
|
// `<lang>\<cat>\VOICE_<SPK>_<NNN>.slb` or `..\VOICE_<movie>.slb`.
|
||||||
let stem = name
|
let stem = name
|
||||||
@@ -525,6 +659,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_audio_entries_categorises_root_banks_and_keeps_them_language_shared() {
|
||||||
|
// The three root banks carry no language component, so BOTH sounds.tbl
|
||||||
|
// files name them; a language filter that only accepted `<lang>\` would
|
||||||
|
// silently drop all the music, which is what it used to do.
|
||||||
|
let mut tbl = Vec::new();
|
||||||
|
for s in [
|
||||||
|
"BGM_001.slb",
|
||||||
|
"JNGL_002.slb",
|
||||||
|
"Static.slb",
|
||||||
|
"eng\\Voice\\VOICE_ADAN_010.slb",
|
||||||
|
"eng\\etc\\VOICE_D_450.slb",
|
||||||
|
"eng\\Movie\\VOICE_S13A.slb",
|
||||||
|
"eng\\Briefing\\BR01_01.slb",
|
||||||
|
] {
|
||||||
|
tbl.extend_from_slice(s.as_bytes());
|
||||||
|
tbl.push(0);
|
||||||
|
}
|
||||||
|
let by = |lang| {
|
||||||
|
list_audio_entries(&tbl, lang)
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| (e.clip.name, e.category))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
let eng = by(VoiceLang::English);
|
||||||
|
let want = [
|
||||||
|
("BGM_001.slb", AudioCategory::Music),
|
||||||
|
("JNGL_002.slb", AudioCategory::Jingle),
|
||||||
|
("Static.slb", AudioCategory::Sfx),
|
||||||
|
("eng\\Voice\\VOICE_ADAN_010.slb", AudioCategory::Radio),
|
||||||
|
("eng\\etc\\VOICE_D_450.slb", AudioCategory::Dialogue),
|
||||||
|
("eng\\Movie\\VOICE_S13A.slb", AudioCategory::MovieVoice),
|
||||||
|
("eng\\Briefing\\BR01_01.slb", AudioCategory::Briefing),
|
||||||
|
];
|
||||||
|
assert_eq!(eng.len(), want.len());
|
||||||
|
for (n, c) in want {
|
||||||
|
assert!(
|
||||||
|
eng.iter().any(|(en, ec)| en == n && *ec == c),
|
||||||
|
"{n} not categorised as {c:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Reading the Japanese table yields the shared banks and none of the
|
||||||
|
// English lines.
|
||||||
|
let jpn = by(VoiceLang::Japanese);
|
||||||
|
assert_eq!(jpn.len(), 3, "only the shared banks: {jpn:?}");
|
||||||
|
assert!(jpn.iter().all(|(_, c)| c.is_shared()));
|
||||||
|
// And the voice view is exactly the non-shared half.
|
||||||
|
assert_eq!(list_voice_clips(&tbl, VoiceLang::English).len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rebuilds_riff_from_headerless() {
|
fn rebuilds_riff_from_headerless() {
|
||||||
let mut slb = vec![0u8; HEADERLESS_DATA_OFFSET];
|
let mut slb = vec![0u8; HEADERLESS_DATA_OFFSET];
|
||||||
|
|||||||
88
crates/sylpheed-formats/tests/media_disc.rs
Normal file
88
crates/sylpheed-formats/tests/media_disc.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//! Real-disc tests for media assembly. Skipped without `SYLPHEED_DISC`.
|
||||||
|
//!
|
||||||
|
//! This logic used to live in the Bevy viewer, where it had no test at all. It
|
||||||
|
//! is the trickiest reading on the disc — a cutscene's voice is a byte region of
|
||||||
|
//! a continuous stream, not the bank its name points at — so it gets pinned here
|
||||||
|
//! before anything else is built on top of it.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||||
|
use sylpheed_formats::slb::VoiceLang;
|
||||||
|
|
||||||
|
fn disc() -> Option<DirectorySource> {
|
||||||
|
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
|
||||||
|
p.join("dat").is_dir().then(|| DirectorySource::new(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A segment-spanning read returns the same bytes as slicing the whole archive.
|
||||||
|
///
|
||||||
|
/// The control that matters: `sound.pak`'s data is five segments, so a TOC
|
||||||
|
/// offset late in the archive addresses a position no single file has. If the
|
||||||
|
/// walk were off by a segment this would return plausible-looking wrong bytes
|
||||||
|
/// rather than fail, which is exactly why it is asserted against the archive's
|
||||||
|
/// own read rather than against a length.
|
||||||
|
#[test]
|
||||||
|
fn segment_range_matches_the_archive_read() {
|
||||||
|
let Some(src) = disc() else {
|
||||||
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let name = "BGM_020.slb";
|
||||||
|
let hash = sylpheed_formats::hash::name_hash(name);
|
||||||
|
let via_range = media::read_sound_bank(&src, hash).expect("segment range read");
|
||||||
|
|
||||||
|
let toc = src.read_file("dat/sound.pak").unwrap();
|
||||||
|
let entries = sylpheed_formats::PakArchive::parse_toc(&toc).unwrap();
|
||||||
|
let e = entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.name_hash == hash)
|
||||||
|
.expect("BGM_020 in the TOC");
|
||||||
|
assert_eq!(via_range.len(), e.comp_size as usize);
|
||||||
|
|
||||||
|
// And it decodes, which a misaligned read would not do.
|
||||||
|
let riffs = media::sound_bank_riffs(&src, name).expect("riffs");
|
||||||
|
assert!(!riffs.is_empty(), "no sub-waves recovered");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A movie's voice resolves to a byte region, and the region is sane.
|
||||||
|
///
|
||||||
|
/// `RT01A` is one of the cutscenes whose voice spans more than one `.slb`
|
||||||
|
/// chunk — the case that motivated regions over per-bank reads in the first
|
||||||
|
/// place. The assertions are deliberately about *shape* (ordered, non-empty,
|
||||||
|
/// smaller than one bank) rather than exact offsets, because the offsets are
|
||||||
|
/// disc facts we have no independent oracle for here; a regression that
|
||||||
|
/// reversed or emptied the region would still be caught.
|
||||||
|
#[test]
|
||||||
|
fn movie_voice_resolves_to_a_region_that_decodes() {
|
||||||
|
let Some(src) = disc() else {
|
||||||
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (start, end) = media::resolve_movie_voice_region(&src, "RT01A", VoiceLang::English)
|
||||||
|
.expect("RT01A has a bound voice track");
|
||||||
|
assert!(start < end, "region is inverted: {start}..{end}");
|
||||||
|
assert!(end - start > 4096, "region is implausibly small");
|
||||||
|
assert!(end - start < 1_500_000, "region spans more than one bank");
|
||||||
|
|
||||||
|
let riffs = media::voice_region_riffs(&src, start, end).expect("region riffs");
|
||||||
|
assert!(!riffs.is_empty(), "region decoded to no audio");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unbound movie stays unvoiced rather than borrowing a neighbour's clip.
|
||||||
|
///
|
||||||
|
/// This is a *negative* the corpus paid for: extending resolution to unbound
|
||||||
|
/// resupply movies by shared demo line played the WRONG recording. The guard
|
||||||
|
/// keeps that door shut.
|
||||||
|
#[test]
|
||||||
|
fn manifest_binding_is_the_only_route() {
|
||||||
|
let Some(src) = disc() else {
|
||||||
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// A movie the manifest does not bind must resolve to nothing, not to a guess.
|
||||||
|
assert_eq!(
|
||||||
|
media::resolve_movie_voice_clip(&src, "no_such_movie_xyz", VoiceLang::English),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -167,3 +167,62 @@ fn manifest_slot_and_movie_counts() {
|
|||||||
// Every id names a real record, so nothing dangles.
|
// Every id names a real record, so nothing dangles.
|
||||||
assert!(entries.iter().all(|e| !e.slot.is_empty() && !e.movie.is_empty()));
|
assert!(entries.iter().all(|e| !e.slot.is_empty() && !e.movie.is_empty()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Everything the Cutscenes browser shows, asserted against the disc.
|
||||||
|
///
|
||||||
|
/// The window's value is that it answers "which cutscenes exist, which mission
|
||||||
|
/// is each one in, and can I read it without playing it" — so the test checks
|
||||||
|
/// exactly those three, including the **negative**: five manifest-bound movies
|
||||||
|
/// have no `.wmv`, and nine more have no English transcript. A browser that
|
||||||
|
/// quietly omitted them would look complete and be wrong, so the counts are
|
||||||
|
/// pinned here rather than left to the eye.
|
||||||
|
#[test]
|
||||||
|
fn cutscene_catalog_binds_movies_and_transcripts() {
|
||||||
|
let Some(root) = disc_root() else {
|
||||||
|
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
use sylpheed_formats::movie_subtitle as ms;
|
||||||
|
let (manifest, _) = load_manifest_and_sounds(&root);
|
||||||
|
let rows = movie_manifest::parse(&manifest);
|
||||||
|
assert_eq!(rows.len(), 104, "manifest slots");
|
||||||
|
|
||||||
|
let movies: std::collections::BTreeSet<&str> =
|
||||||
|
rows.iter().map(|r| r.movie.as_str()).collect();
|
||||||
|
assert_eq!(movies.len(), 101, "distinct movies");
|
||||||
|
assert_eq!(rows.iter().filter(|r| r.subtitle.is_some()).count(), 99);
|
||||||
|
assert_eq!(rows.iter().filter(|r| r.voice_token.is_some()).count(), 99);
|
||||||
|
assert_eq!(rows.iter().filter(|r| r.telop.is_some()).count(), 22);
|
||||||
|
|
||||||
|
// Bound-but-absent: the four boot logos and one encoder test clip are named
|
||||||
|
// by the manifest and are not on the disc. The browser marks these in red
|
||||||
|
// rather than offering a Play button that would fail.
|
||||||
|
let on_disc: std::collections::BTreeSet<String> = std::fs::read_dir(root.join("dat/movie"))
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|e| {
|
||||||
|
let p = e.ok()?.path();
|
||||||
|
(p.extension()?.to_str()? == "wmv")
|
||||||
|
.then(|| p.file_stem()?.to_str().map(|s| s.to_ascii_lowercase()))?
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut absent: Vec<&str> = movies
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|m| !on_disc.contains(&m.to_ascii_lowercase()))
|
||||||
|
.collect();
|
||||||
|
absent.sort_unstable();
|
||||||
|
assert_eq!(
|
||||||
|
absent,
|
||||||
|
["SYLPH_HD720p_8M-CBR_2ch", "logo1", "logo2", "logo3", "logo4"],
|
||||||
|
"manifest-bound movies with no .wmv on the disc"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transcripts, which is what makes a cutscene readable without playback.
|
||||||
|
let lang_pak = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap();
|
||||||
|
let text_pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||||
|
let resolved = movies
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !ms::load(m, &lang_pak, &text_pak).is_empty())
|
||||||
|
.count();
|
||||||
|
assert_eq!(resolved, 92, "movies with an English transcript");
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,12 @@ use bevy::prelude::*;
|
|||||||
use bevy_egui::{egui, EguiContexts};
|
use bevy_egui::{egui, EguiContexts};
|
||||||
|
|
||||||
use crate::iso_loader::{
|
use crate::iso_loader::{
|
||||||
AudioPreview, FileInfo, FileSelected, GameCategory, GameData, ImageRgba, IsoState, ModelPreview,
|
AudioLibrary, AudioPreview, CutsceneBrowser, FileInfo, FileSelected, GameCategory, GameData,
|
||||||
MovieSubtitles, MovieVoice, PakContent, PakView, RequestAudio, RequestGameData, RequestOpenDir,
|
ImageRgba, IsoLoaderSystemSet, IsoState, ModelPreview, MovieSubtitles, MovieVoice, PakContent,
|
||||||
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose,
|
PakView, RequestAudio, RequestAudioLibrary, RequestCutscenes, RequestGameData, RequestOpenDir,
|
||||||
RequestShipCatalog, RequestShipRender, RequestSubtitles, RequestVoiceLibrary, SaveBrowser,
|
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose, RequestShipCatalog,
|
||||||
ScreenBrowser, ShipBrowser, SkyboxPreview, TextPreview, TexturePreview,
|
RequestShipRender, RequestSubtitles, SaveBrowser, ScreenBrowser, ShipBrowser, SkyboxPreview,
|
||||||
VideoPreview, VoiceLibrary, IsoLoaderSystemSet,
|
TextPreview, TexturePreview, VideoPreview,
|
||||||
};
|
};
|
||||||
use crate::ViewerState;
|
use crate::ViewerState;
|
||||||
use sylpheed_formats::SubLang;
|
use sylpheed_formats::SubLang;
|
||||||
@@ -37,6 +37,7 @@ impl Plugin for ViewerUiPlugin {
|
|||||||
app.add_systems(Update, draw_ships_ui.after(IsoLoaderSystemSet));
|
app.add_systems(Update, draw_ships_ui.after(IsoLoaderSystemSet));
|
||||||
app.add_systems(Update, draw_screens_ui.after(IsoLoaderSystemSet));
|
app.add_systems(Update, draw_screens_ui.after(IsoLoaderSystemSet));
|
||||||
app.add_systems(Update, draw_save_ui.after(IsoLoaderSystemSet));
|
app.add_systems(Update, draw_save_ui.after(IsoLoaderSystemSet));
|
||||||
|
app.add_systems(Update, draw_cutscenes_ui.after(IsoLoaderSystemSet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,11 +149,12 @@ struct UiEvents<'w> {
|
|||||||
file_selected: EventWriter<'w, FileSelected>,
|
file_selected: EventWriter<'w, FileSelected>,
|
||||||
subtitles: EventWriter<'w, RequestSubtitles>,
|
subtitles: EventWriter<'w, RequestSubtitles>,
|
||||||
audio: EventWriter<'w, RequestAudio>,
|
audio: EventWriter<'w, RequestAudio>,
|
||||||
voice_lib: EventWriter<'w, RequestVoiceLibrary>,
|
audio_lib: EventWriter<'w, RequestAudioLibrary>,
|
||||||
game_data: EventWriter<'w, RequestGameData>,
|
game_data: EventWriter<'w, RequestGameData>,
|
||||||
ships: EventWriter<'w, RequestShipCatalog>,
|
ships: EventWriter<'w, RequestShipCatalog>,
|
||||||
screens: EventWriter<'w, RequestScreenCatalog>,
|
screens: EventWriter<'w, RequestScreenCatalog>,
|
||||||
save: EventWriter<'w, RequestSaveOpen>,
|
save: EventWriter<'w, RequestSaveOpen>,
|
||||||
|
cutscenes: EventWriter<'w, RequestCutscenes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_viewer_ui(
|
fn draw_viewer_ui(
|
||||||
@@ -170,7 +172,7 @@ fn draw_viewer_ui(
|
|||||||
mut subtitles: ResMut<MovieSubtitles>,
|
mut subtitles: ResMut<MovieSubtitles>,
|
||||||
mut movie_voice: ResMut<MovieVoice>,
|
mut movie_voice: ResMut<MovieVoice>,
|
||||||
mut audio: ResMut<AudioPreview>,
|
mut audio: ResMut<AudioPreview>,
|
||||||
mut voice_lib: ResMut<VoiceLibrary>,
|
mut audio_lib: ResMut<AudioLibrary>,
|
||||||
mut events: UiEvents,
|
mut events: UiEvents,
|
||||||
) {
|
) {
|
||||||
let ctx = contexts.ctx_mut();
|
let ctx = contexts.ctx_mut();
|
||||||
@@ -202,11 +204,11 @@ fn draw_viewer_ui(
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui.menu_button("View", |ui| {
|
ui.menu_button("View", |ui| {
|
||||||
if ui.button("🎙 Voice Lines…").clicked() {
|
if ui.button("🔊 Audio Library…").clicked() {
|
||||||
voice_lib.open = true;
|
audio_lib.open = true;
|
||||||
if !voice_lib.loaded && !voice_lib.loading {
|
if !audio_lib.loaded && !audio_lib.loading {
|
||||||
voice_lib.loading = true;
|
audio_lib.loading = true;
|
||||||
events.voice_lib.send_default();
|
events.audio_lib.send_default();
|
||||||
}
|
}
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
@@ -228,6 +230,12 @@ fn draw_viewer_ui(
|
|||||||
events.screens.send_default();
|
events.screens.send_default();
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
|
if ui.button("🎬 Cutscenes…").clicked() {
|
||||||
|
// The manifest binds every cutscene slot to its movie,
|
||||||
|
// subtitle track, voice token and telop overlay.
|
||||||
|
events.cutscenes.send_default();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
if ui.button("💾 Save File…").clicked() {
|
if ui.button("💾 Save File…").clicked() {
|
||||||
// A save is not on the disc — it lives in the emulator's
|
// A save is not on the disc — it lives in the emulator's
|
||||||
// content tree, so this opens a file dialog.
|
// content tree, so this opens a file dialog.
|
||||||
@@ -248,98 +256,134 @@ fn draw_viewer_ui(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Standalone voice-line browser (floating window) ───────────────────
|
// ── Standalone voice-line browser (floating window) ───────────────────
|
||||||
if voice_lib.open {
|
if audio_lib.open {
|
||||||
let mut open = true;
|
let mut open = true;
|
||||||
egui::Window::new("🎙 Voice Lines")
|
egui::Window::new("🔊 Audio Library")
|
||||||
.default_width(360.0)
|
.default_width(400.0)
|
||||||
.default_height(480.0)
|
.default_height(520.0)
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
if voice_lib.loading {
|
if audio_lib.loading {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
ui.label("Reading sounds.tbl…");
|
ui.label("Reading sounds.tbl…");
|
||||||
});
|
});
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
} else if !voice_lib.loaded {
|
} else if !audio_lib.loaded {
|
||||||
ui.label("Open a game source first.");
|
ui.label("Open a game source first.");
|
||||||
} else {
|
} else {
|
||||||
|
use sylpheed_formats::slb::VoiceLang;
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Voice:");
|
||||||
|
// The table name IS the selector: there is no language
|
||||||
|
// field inside sounds.tbl, so switching re-reads the
|
||||||
|
// other file. Music/jingles/SFX are shared and stay.
|
||||||
|
for lang in VoiceLang::ALL {
|
||||||
|
if ui
|
||||||
|
.selectable_label(audio_lib.lang == lang, lang.label())
|
||||||
|
.clicked()
|
||||||
|
&& audio_lib.lang != lang
|
||||||
|
{
|
||||||
|
audio_lib.lang = lang;
|
||||||
|
audio_lib.loaded = false;
|
||||||
|
audio_lib.entries.clear();
|
||||||
|
audio_lib.reload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
ui.label("Filter:");
|
ui.label("Filter:");
|
||||||
ui.text_edit_singleline(&mut voice_lib.filter);
|
ui.text_edit_singleline(&mut audio_lib.filter);
|
||||||
});
|
});
|
||||||
let f = voice_lib.filter.to_lowercase();
|
let f = audio_lib.filter.to_lowercase();
|
||||||
// Group the thousands of entries as directory → speaker so the
|
// Group category → speaker. The category comes from the path
|
||||||
// list is navigable (e.g. browse `Voice` by character to find a
|
// shape, so the root banks (music/jingles/SFX) get real
|
||||||
// cutscene's radio line). `name` is `<lang>\<dir>\<file>.slb`.
|
// headings instead of the "?" a directory split gave them.
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
let mut groups: BTreeMap<&str, BTreeMap<&str, Vec<&sylpheed_formats::slb::VoiceClip>>> =
|
type Group<'a> = BTreeMap<&'a str, Vec<&'a sylpheed_formats::slb::AudioEntry>>;
|
||||||
BTreeMap::new();
|
let mut groups: BTreeMap<
|
||||||
for c in &voice_lib.clips {
|
sylpheed_formats::slb::AudioCategory,
|
||||||
if !f.is_empty() && !c.name.to_lowercase().contains(&f) {
|
Group<'_>,
|
||||||
|
> = BTreeMap::new();
|
||||||
|
for e in &audio_lib.entries {
|
||||||
|
if !f.is_empty() && !e.clip.name.to_lowercase().contains(&f) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let dir = c.name.rsplit('\\').nth(1).unwrap_or("?");
|
|
||||||
groups
|
groups
|
||||||
.entry(dir)
|
.entry(e.category)
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(c.speaker.as_str())
|
.entry(e.clip.speaker.as_str())
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(c);
|
.push(e);
|
||||||
}
|
}
|
||||||
let shown: usize = groups.values().flat_map(|s| s.values()).map(Vec::len).sum();
|
let shown: usize = groups.values().flat_map(|s| s.values()).map(Vec::len).sum();
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(format!("{shown} / {} clips", voice_lib.clips.len()))
|
egui::RichText::new(format!(
|
||||||
|
"{shown} / {} banks",
|
||||||
|
audio_lib.entries.len()
|
||||||
|
))
|
||||||
.weak()
|
.weak()
|
||||||
.small(),
|
.small(),
|
||||||
);
|
);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
let filtering = !f.is_empty();
|
let filtering = !f.is_empty();
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
for (dir, speakers) in &groups {
|
for (cat, speakers) in &groups {
|
||||||
let dtotal: usize = speakers.values().map(Vec::len).sum();
|
let ctotal: usize = speakers.values().map(Vec::len).sum();
|
||||||
egui::CollapsingHeader::new(format!("📁 {dir} ({dtotal})"))
|
|
||||||
.id_salt(("vdir", *dir))
|
|
||||||
.default_open(filtering)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
for (speaker, clips) in speakers {
|
|
||||||
egui::CollapsingHeader::new(format!(
|
egui::CollapsingHeader::new(format!(
|
||||||
"{speaker} ({})",
|
"{} ({ctotal})",
|
||||||
clips.len()
|
cat.label()
|
||||||
))
|
))
|
||||||
.id_salt(("vspk", *dir, *speaker))
|
.id_salt(("acat", *cat))
|
||||||
.default_open(filtering || clips.len() <= 6)
|
.default_open(filtering || ctotal <= 40)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for c in clips {
|
for (speaker, entries) in speakers {
|
||||||
|
// A single-bank group (Static.slb) would be a
|
||||||
|
// pointless nested header.
|
||||||
|
let flat = speakers.len() == 1 || entries.len() == 1;
|
||||||
|
let mut row = |ui: &mut egui::Ui| {
|
||||||
|
for e in entries {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui
|
if ui
|
||||||
.button("▶")
|
.button("▶")
|
||||||
.on_hover_text(&c.name)
|
.on_hover_text(&e.clip.name)
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
audio.generation =
|
audio.generation =
|
||||||
audio.generation.wrapping_add(1);
|
audio.generation.wrapping_add(1);
|
||||||
audio.loading = true;
|
audio.loading = true;
|
||||||
audio.active = true;
|
audio.active = true;
|
||||||
audio.name = c.display.clone();
|
audio.error = None;
|
||||||
|
audio.name = e.clip.display.clone();
|
||||||
events.audio.send(RequestAudio {
|
events.audio.send(RequestAudio {
|
||||||
clip: c.name.clone(),
|
clip: e.clip.name.clone(),
|
||||||
display: c.display.clone(),
|
display: e.clip.display.clone(),
|
||||||
movie: None,
|
movie: None,
|
||||||
|
mono: e.category.is_voice(),
|
||||||
generation: audio.generation,
|
generation: audio.generation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ui.label(&c.display);
|
ui.label(&e.clip.display);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if flat {
|
||||||
|
row(ui);
|
||||||
|
} else {
|
||||||
|
egui::CollapsingHeader::new(format!(
|
||||||
|
"{speaker} ({})",
|
||||||
|
entries.len()
|
||||||
|
))
|
||||||
|
.id_salt(("aspk", *cat, *speaker))
|
||||||
|
.default_open(filtering || entries.len() <= 6)
|
||||||
|
.show(ui, &mut row);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
audio_lib.open = open;
|
||||||
});
|
|
||||||
voice_lib.open = open;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Left panel: file browser ──────────────────────────────────────────
|
// ── Left panel: file browser ──────────────────────────────────────────
|
||||||
@@ -481,11 +525,13 @@ fn draw_viewer_ui(
|
|||||||
audio.generation = audio.generation.wrapping_add(1);
|
audio.generation = audio.generation.wrapping_add(1);
|
||||||
audio.loading = true;
|
audio.loading = true;
|
||||||
audio.active = true; // show the panel immediately (spinner)
|
audio.active = true; // show the panel immediately (spinner)
|
||||||
|
audio.error = None;
|
||||||
audio.name = format!("VOICE_{movie}");
|
audio.name = format!("VOICE_{movie}");
|
||||||
events.audio.send(RequestAudio {
|
events.audio.send(RequestAudio {
|
||||||
clip: String::new(),
|
clip: String::new(),
|
||||||
display: format!("VOICE_{movie}"),
|
display: format!("VOICE_{movie}"),
|
||||||
movie: Some((movie.clone(), movie_voice.lang)),
|
movie: Some((movie.clone(), movie_voice.lang)),
|
||||||
|
mono: true, // a cutscene voice track
|
||||||
generation: audio.generation,
|
generation: audio.generation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1348,8 +1394,10 @@ fn draw_audio_player(ui: &mut egui::Ui, audio: &mut AudioPreview) {
|
|||||||
if audio.loading {
|
if audio.loading {
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
ui.label("decoding…");
|
ui.label("decoding…");
|
||||||
|
} else if let Some(err) = &audio.error {
|
||||||
|
ui.colored_label(egui::Color32::from_rgb(224, 86, 122), format!("⚠ {err}"));
|
||||||
} else {
|
} else {
|
||||||
ui.label("voice track");
|
ui.label("sound bank");
|
||||||
}
|
}
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if ui.button("✖ Close").clicked() {
|
if ui.button("✖ Close").clicked() {
|
||||||
@@ -1876,6 +1924,21 @@ fn draw_screens_ui(
|
|||||||
if !screens.filter.is_empty() && ui.small_button("✖").clicked() {
|
if !screens.filter.is_empty() && ui.small_button("✖").clicked() {
|
||||||
screens.filter.clear();
|
screens.filter.clear();
|
||||||
}
|
}
|
||||||
|
ui.separator();
|
||||||
|
// Widens the enumeration to everything `compose` can draw — the
|
||||||
|
// same rule the pak browser's inline preview uses. Costs a
|
||||||
|
// re-scan, so it is a deliberate click rather than the default.
|
||||||
|
if ui
|
||||||
|
.checkbox(&mut screens.include_fragments, "Fragments")
|
||||||
|
.on_hover_text(
|
||||||
|
"Also list RATC bundles with no .rat layout child: the developer-logo \
|
||||||
|
splash, and ~1 786 two-element fragments (a button beside its glow). \
|
||||||
|
These are what the PAK browser preview draws but this list omits.",
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
screens.rescan = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
@@ -1895,6 +1958,12 @@ fn draw_screens_ui(
|
|||||||
egui::CollapsingHeader::new(&pak.label)
|
egui::CollapsingHeader::new(&pak.label)
|
||||||
.default_open(pak.builds.len() == 1)
|
.default_open(pak.builds.len() == 1)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
|
if pak.truncated {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(224, 168, 86),
|
||||||
|
"⚠ list truncated (decode budget)",
|
||||||
|
);
|
||||||
|
}
|
||||||
for (bi, (entry, size)) in pak.builds.iter().enumerate() {
|
for (bi, (entry, size)) in pak.builds.iter().enumerate() {
|
||||||
let selected = screens.selected == Some(pi)
|
let selected = screens.selected == Some(pi)
|
||||||
&& screens.build == bi;
|
&& screens.build == bi;
|
||||||
@@ -1922,6 +1991,32 @@ fn draw_screens_ui(
|
|||||||
{
|
{
|
||||||
recompose = true;
|
recompose = true;
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
|
.checkbox(&mut screens.black_backdrop, "Black backdrop")
|
||||||
|
.on_hover_text(
|
||||||
|
"Composite over black instead of the default dim slate. The slate \
|
||||||
|
stands in for the PRMD dim quad plus the live 3D scene behind an \
|
||||||
|
in-mission screen; black is what the game composites over on a \
|
||||||
|
screen carrying its own background — and what a framebuffer \
|
||||||
|
capture must be compared against.",
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
recompose = true;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.checkbox(&mut screens.show_primitives, "Primitives")
|
||||||
|
.on_hover_text(
|
||||||
|
"Draw the untextured .prm fade/dim/flash quads. Decoded, but their \
|
||||||
|
paint order is unsolved: a primitive has no T8aD header and so no \
|
||||||
|
layer key, and the derived order forces keyless elements last. \
|
||||||
|
That is measurably wrong — expect an opaque quad to wipe some \
|
||||||
|
screens (32 of GP_DIALOG's builds).",
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
recompose = true;
|
||||||
|
}
|
||||||
if screens.composing {
|
if screens.composing {
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
@@ -2081,8 +2176,17 @@ fn draw_screens_ui(
|
|||||||
compose.send(RequestScreenCompose {
|
compose.send(RequestScreenCompose {
|
||||||
pak: pak.path.clone(),
|
pak: pak.path.clone(),
|
||||||
build: screens.build,
|
build: screens.build,
|
||||||
|
// The catalog stored (pak entry index, size) per build; the
|
||||||
|
// entry index is what lets the worker read one entry.
|
||||||
|
entry: pak
|
||||||
|
.builds
|
||||||
|
.get(screens.build)
|
||||||
|
.map(|(e, _)| *e)
|
||||||
|
.unwrap_or(0),
|
||||||
focus: screens.show_focus,
|
focus: screens.show_focus,
|
||||||
animated: screens.show_animated,
|
animated: screens.show_animated,
|
||||||
|
black_backdrop: screens.black_backdrop,
|
||||||
|
primitives: screens.show_primitives,
|
||||||
hidden: screens.hidden.clone(),
|
hidden: screens.hidden.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2295,3 +2399,236 @@ fn draw_save_ui(
|
|||||||
}
|
}
|
||||||
saves.open &= open;
|
saves.open &= open;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cutscene browser (View ▸ Cutscenes) ──────────────────────────────────────
|
||||||
|
|
||||||
|
/// The cutscene catalog: every slot the manifest binds, with its movie,
|
||||||
|
/// subtitle track, voice token and telop overlay — and a transcript pane that
|
||||||
|
/// resolves the captions to text **without playing the video**.
|
||||||
|
///
|
||||||
|
/// Before this the manifest was invisible plumbing: it resolved a voice bank and
|
||||||
|
/// was never rendered, so a cutscene could only be found by hunting `.wmv` in
|
||||||
|
/// the ISO tree, where nothing says which mission a file belongs to.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn draw_cutscenes_ui(
|
||||||
|
mut contexts: EguiContexts,
|
||||||
|
mut cut: ResMut<CutsceneBrowser>,
|
||||||
|
mut requests: EventReader<RequestCutscenes>,
|
||||||
|
mut file_selected: EventWriter<FileSelected>,
|
||||||
|
mut browser: ResMut<FileBrowserState>,
|
||||||
|
) {
|
||||||
|
if requests.read().next().is_some() {
|
||||||
|
cut.open = true;
|
||||||
|
}
|
||||||
|
if !cut.open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ctx = contexts.ctx_mut().clone();
|
||||||
|
let mut open = true;
|
||||||
|
let mut pick: Option<usize> = None;
|
||||||
|
let mut play: Option<String> = None;
|
||||||
|
|
||||||
|
egui::Window::new("🎬 Cutscenes")
|
||||||
|
.default_width(880.0)
|
||||||
|
.default_height(560.0)
|
||||||
|
.open(&mut open)
|
||||||
|
.show(&ctx, |ui| {
|
||||||
|
if cut.loading {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spinner();
|
||||||
|
ui.label("Reading the cutscene manifest…");
|
||||||
|
});
|
||||||
|
ctx.request_repaint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !cut.loaded {
|
||||||
|
ui.label("Open a game source first (File ▸ Open…).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("🔍");
|
||||||
|
ui.text_edit_singleline(&mut cut.filter);
|
||||||
|
if !cut.filter.is_empty() && ui.small_button("✖").clicked() {
|
||||||
|
cut.filter.clear();
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.label("Subtitles:");
|
||||||
|
let before = cut.lang;
|
||||||
|
egui::ComboBox::from_id_salt("cutscene_lang")
|
||||||
|
.selected_text(cut.lang.label())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for l in sylpheed_formats::movie_subtitle::SubLang::ALL {
|
||||||
|
ui.selectable_value(&mut cut.lang, l, l.label());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if cut.lang != before && cut.selected.is_some() {
|
||||||
|
cut.want_cues = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let missing = cut.rows.iter().filter(|r| !r.present).count();
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(format!(
|
||||||
|
"{} slots · {} distinct movies{}",
|
||||||
|
cut.rows.len(),
|
||||||
|
cut.rows
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.movie.as_str())
|
||||||
|
.collect::<std::collections::BTreeSet<_>>()
|
||||||
|
.len(),
|
||||||
|
if missing > 0 {
|
||||||
|
format!(" · ⚠ {missing} bound to a movie not on the disc")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.weak()
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
let filter = cut.filter.to_lowercase();
|
||||||
|
egui::SidePanel::left("cutscene_list")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(330.0)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
egui::Grid::new("cutscene_grid")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (i, r) in cut.rows.iter().enumerate() {
|
||||||
|
let hay = format!("{} {} {}", r.slot, r.movie, r.kind)
|
||||||
|
.to_lowercase();
|
||||||
|
if !filter.is_empty() && !hay.contains(&filter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let label = match (r.mission, r.phase) {
|
||||||
|
(Some(m), Some(p)) => format!("S{m:02} ph{p} {}", r.movie),
|
||||||
|
(Some(m), None) => format!("S{m:02} {}", r.movie),
|
||||||
|
_ => format!(" {}", r.movie),
|
||||||
|
};
|
||||||
|
let mut text = egui::RichText::new(label).monospace();
|
||||||
|
if !r.present {
|
||||||
|
text = text.color(egui::Color32::from_rgb(224, 86, 122));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(cut.selected == Some(i), text)
|
||||||
|
.on_hover_text(&r.slot)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
pick = Some(i);
|
||||||
|
}
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(r.kind).weak().small(),
|
||||||
|
);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
|
let Some(row) = cut.selected.and_then(|i| cut.rows.get(i)) else {
|
||||||
|
ui.label("Pick a cutscene on the left.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading(&row.slot);
|
||||||
|
if row.present {
|
||||||
|
if ui.button("▶ Play").clicked() {
|
||||||
|
play = Some(format!("dat/movie/{}.wmv", row.movie));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(224, 86, 122),
|
||||||
|
"⚠ not on the disc",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
egui::Grid::new("cutscene_detail")
|
||||||
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let mut kv = |k: &str, v: String| {
|
||||||
|
ui.label(egui::RichText::new(k).weak().small());
|
||||||
|
ui.label(egui::RichText::new(v).monospace());
|
||||||
|
ui.end_row();
|
||||||
|
};
|
||||||
|
kv("kind", row.kind.to_string());
|
||||||
|
kv("movie", format!("dat/movie/{}.wmv", row.movie));
|
||||||
|
if let (Some(m), Some(p)) = (row.mission, row.phase) {
|
||||||
|
kv("mission", format!("S{m:02}, phase {p}"));
|
||||||
|
} else if let Some(m) = row.mission {
|
||||||
|
kv("mission", format!("S{m:02}"));
|
||||||
|
}
|
||||||
|
kv(
|
||||||
|
"subtitle",
|
||||||
|
row.subtitle.clone().unwrap_or_else(|| "—".into()),
|
||||||
|
);
|
||||||
|
kv(
|
||||||
|
"voice track",
|
||||||
|
row.voice_token.clone().unwrap_or_else(|| "—".into()),
|
||||||
|
);
|
||||||
|
// The .prt overlay is bound here but we have no parser,
|
||||||
|
// so name the reference and say the content is not read.
|
||||||
|
kv(
|
||||||
|
"telop (.prt)",
|
||||||
|
match &row.telop {
|
||||||
|
Some(t) => format!("{t} (not decoded)"),
|
||||||
|
None => "—".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.label(egui::RichText::new("Transcript").strong());
|
||||||
|
if cut.cues_loading {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.spinner();
|
||||||
|
ui.label("resolving captions…");
|
||||||
|
});
|
||||||
|
ctx.request_repaint();
|
||||||
|
} else if cut.cues.is_empty() {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(
|
||||||
|
"no captions resolved for this movie in this language",
|
||||||
|
)
|
||||||
|
.weak(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.id_salt("cue_scroll")
|
||||||
|
.show(ui, |ui| {
|
||||||
|
egui::Grid::new("cue_grid").num_columns(2).striped(true).show(
|
||||||
|
ui,
|
||||||
|
|ui| {
|
||||||
|
for c in &cut.cues {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(fmt_time(c.start))
|
||||||
|
.monospace()
|
||||||
|
.weak()
|
||||||
|
.small(),
|
||||||
|
);
|
||||||
|
ui.label(&c.text);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(i) = pick {
|
||||||
|
cut.selected = Some(i);
|
||||||
|
cut.want_cues = true;
|
||||||
|
}
|
||||||
|
if let Some(path) = play {
|
||||||
|
// Route through the normal file-open path, so the existing video player
|
||||||
|
// handles it exactly as it would from the tree.
|
||||||
|
browser.loading = true;
|
||||||
|
browser.selected = browser.files.iter().position(|f| f.eq_ignore_ascii_case(&path));
|
||||||
|
file_selected.send(FileSelected(path));
|
||||||
|
}
|
||||||
|
cut.open = open;
|
||||||
|
}
|
||||||
|
|||||||
67
docker/agent/bin/push-work
Executable file
67
docker/agent/bin/push-work
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Push the current topic branch to origin — the ONLY sanctioned way out of the
|
||||||
|
# container.
|
||||||
|
#
|
||||||
|
# Why a wrapper instead of plain `git push`:
|
||||||
|
#
|
||||||
|
# * **`main` and shared branches are refused.** The agent commits to
|
||||||
|
# `auto/<topic>`; a human merges. A token that can push anywhere is one
|
||||||
|
# confused iteration away from rewriting the consolidated line.
|
||||||
|
# * **Force-push is refused**, always. Nothing here needs it, and history
|
||||||
|
# rewriting is the one mistake that cannot be undone by merging.
|
||||||
|
# * It pushes the CURRENT branch only, by name, so a stray `--all` cannot
|
||||||
|
# publish another agent's worktree branch mid-experiment.
|
||||||
|
#
|
||||||
|
# Credentials come from a file mounted read-only at ~/.git-credentials (see
|
||||||
|
# `sylph-agent`). They are never printed, never logged, and never passed on a
|
||||||
|
# command line.
|
||||||
|
#
|
||||||
|
# push-work push the current branch
|
||||||
|
# push-work --dry-run say what it would do
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DRY=0
|
||||||
|
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||||
|
|
||||||
|
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
||||||
|
echo "push-work: not inside a git repository" >&2; exit 1; }
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
if [ "$branch" = "HEAD" ]; then
|
||||||
|
echo "push-work: detached HEAD — check out a branch first" >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$branch" in
|
||||||
|
auto/*) ;;
|
||||||
|
*)
|
||||||
|
echo "push-work: refusing to push '$branch'." >&2
|
||||||
|
echo " Only auto/* topic branches may leave the container; a human merges" >&2
|
||||||
|
echo " them into main. Move your work: git switch -c auto/<topic>" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ ! -s "$HOME/.git-credentials" ]; then
|
||||||
|
echo "push-work: no credentials mounted at ~/.git-credentials." >&2
|
||||||
|
echo " The host must start the container with SYLPH_GIT_CREDENTIALS pointing" >&2
|
||||||
|
echo " at a file containing one line:" >&2
|
||||||
|
echo " https://<user>:<token>@git.mc02.dev" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `store` reads the file we mounted; nothing is written back (it is read-only).
|
||||||
|
git config --local credential.helper "store --file=$HOME/.git-credentials"
|
||||||
|
|
||||||
|
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
|
||||||
|
echo "push-work: $branch — $ahead commit(s) to publish"
|
||||||
|
|
||||||
|
if [ "$DRY" = 1 ]; then
|
||||||
|
echo "push-work: --dry-run, stopping here"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --force-with-lease is deliberately NOT offered. If this is rejected as
|
||||||
|
# non-fast-forward, someone else moved the branch: fetch and merge, do not
|
||||||
|
# overwrite.
|
||||||
|
git push --set-upstream origin "$branch"
|
||||||
|
echo "push-work: pushed $branch"
|
||||||
@@ -1,48 +1,122 @@
|
|||||||
Work the Project Sylpheed reverse-engineering backlog, one item at a time.
|
Answer the open questions the Godot menu port is blocked on, one at a time.
|
||||||
|
|
||||||
Read `Syplheed-Reborn/docker/agent/AGENT.md` first — it has the container's
|
## Your objective
|
||||||
tooling and, more importantly, the method rules this corpus is built on.
|
|
||||||
|
`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It lists the
|
||||||
|
open questions Q1…Q9 plus a gated probe S1, and the gate each one must pass.
|
||||||
|
|
||||||
|
**You do not build the port.** A separate agent does that, from what you produce.
|
||||||
|
Your deliverable is decoded, verified, written-down answers with the evidence.
|
||||||
|
If you find yourself designing an export schema or writing GDScript, you have
|
||||||
|
crossed the line — go back to the question you were answering.
|
||||||
|
|
||||||
|
This is still reverse engineering. What changed is what earns attention: an item
|
||||||
|
is worth doing when the menu port is blocked on it.
|
||||||
|
|
||||||
|
## Read these first, every iteration
|
||||||
|
|
||||||
|
Short on purpose, and the reason this prompt is short:
|
||||||
|
|
||||||
|
1. `docs/port/MISSION.md` — the open questions, their gates, what is out of scope.
|
||||||
|
2. `docs/port/HANDOFF.md` — what the port agent has been told so far. **Update it
|
||||||
|
when you answer something.** An answer not reachable from that page has not
|
||||||
|
been delivered.
|
||||||
|
3. `docs/re/REFUTED.md` — claims already tested and dead. Grep it for your nouns
|
||||||
|
before designing anything.
|
||||||
|
4. `docs/re/METHOD.md` — the traps this corpus has already paid for.
|
||||||
|
5. `docs/re/INDEX.md` — what is already decoded. Re-deriving a ✅ row is not a
|
||||||
|
finding; asking whether its values *resolve* is.
|
||||||
|
6. `docker/agent/AGENT.md` — the container's tooling.
|
||||||
|
|
||||||
|
`docs/re/disc-atlas.html` maps how the assets reference each other — useful when
|
||||||
|
you need to find what feeds what.
|
||||||
|
|
||||||
|
**These files are the memory.** A finding that lives only in your context is lost
|
||||||
|
when the container dies.
|
||||||
|
|
||||||
## Each iteration
|
## Each iteration
|
||||||
|
|
||||||
1. **Pick one item.** Take the next open entry from
|
1. **Pick one question** from MISSION.md, preferring the one that blocks the port
|
||||||
`Syplheed-Reborn/docs/re/BACKLOG.md`, preferring the one whose "first step"
|
earliest and whose first step is cheapest. If you are mid-question, continue it
|
||||||
is cheapest and most decisive. If you are mid-item from a previous
|
rather than starting another.
|
||||||
iteration, continue it rather than starting another.
|
2. **Do the smallest experiment that could settle it**, and try to *refute* your
|
||||||
2. **Do the smallest experiment that could settle it**, and try to *refute*
|
hypothesis before believing it. Run the known-positive through any new filter
|
||||||
your hypothesis before you believe it.
|
first; a filter that fails its own control is dead, not tuneable.
|
||||||
3. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with
|
3. **Classify the answer honestly.** Every answer is exactly one of:
|
||||||
the evidence. A withdrawn or refuted result is a real result — record it,
|
* **decoded** — the field, plus a disc-wide check;
|
||||||
with the reasoning, rather than deleting it.
|
* **measured** — not on the disc in any form you found, but here is what the
|
||||||
4. **Commit** to a topic branch (below), one logical change per commit.
|
running game does, and here is the capture;
|
||||||
5. **Say plainly what you did not settle**, and stop the iteration.
|
* **undecodable, with reach** — you looked here, here and here, and this is
|
||||||
|
why it is not there.
|
||||||
|
|
||||||
|
Never a fourth thing. *Measured* and *undecodable* mean the port agent will
|
||||||
|
author that value by hand, and it must know it is authoring rather than
|
||||||
|
transcribing. Labelling a guess as a decode puts it into the port wearing the
|
||||||
|
badge of a measurement.
|
||||||
|
4. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence
|
||||||
|
and the *reach* of any negative, then update the row in `docs/port/HANDOFF.md`.
|
||||||
|
* Refuted something → a line in `REFUTED.md`.
|
||||||
|
* Bitten by a general trap → a line in `METHOD.md`.
|
||||||
|
* Closed a format → update its `INDEX.md` row.
|
||||||
|
5. **Commit** to `auto/<topic>`, one logical change per commit.
|
||||||
|
6. **Publish**: `push-work`. Every iteration that produced a commit.
|
||||||
|
7. **Say plainly what you did not settle**, and stop.
|
||||||
|
|
||||||
## Hard rules
|
## Hard rules
|
||||||
|
|
||||||
* **Never commit to `main`.** Work on `auto/<topic>` in whichever repo you are
|
* **Do not build the port.** No Godot project, no GDScript, no asset pipeline, no
|
||||||
touching, branched from the current `main`. Create it if it does not exist.
|
export schema, no transcoding. Those belong to the port agent.
|
||||||
* **Never push.** No push credentials are mounted, and that is deliberate — a
|
* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool for
|
||||||
human reviews before anything leaves the box.
|
exploring and verifying the RE work; it keeps its static-data-only rule and the
|
||||||
* **One emulator at a time.** `run-canary` enforces this with a lockfile; do not
|
port does not depend on it.
|
||||||
work around it.
|
* **Never commit to `main`**, never rebase a shared branch, never delete a branch,
|
||||||
* **Do not edit `main`'s history**, do not rebase shared branches, and do not
|
never rewrite history.
|
||||||
delete branches.
|
* **Do not touch another agent's worktree.** `git worktree list` first; branches
|
||||||
* **Measure the oracle; never infer it.** An iteration that reasons about the
|
marked `+` are checked out elsewhere.
|
||||||
game without running it is a red flag unless it is a pure static-format task.
|
* **One emulator at a time** — `run-canary` enforces it with a lockfile.
|
||||||
* **Verify with an artifact**, not with "it compiles": `build-reborn test` (it
|
* **Measure the oracle; never infer it.** Most of the open questions are about
|
||||||
wires up `SYLPHEED_DISC` — without it the disc tests silently self-skip and a
|
*behaviour* — timing, transitions, what a button does, what a d-pad press does
|
||||||
green run means almost nothing), `sylpheed-cli mesh render`, `screen render`,
|
at the end of a list. Those cannot be answered from the file. An iteration that
|
||||||
`save info`, a screenshot.
|
reasons about the game without running it is a red flag unless the question is
|
||||||
|
a pure static-format one.
|
||||||
|
* **Do not improvise around a blocker.** If a question needs a decision only the
|
||||||
|
user can make, or the container cannot do it, write what you found, note it in
|
||||||
|
MISSION.md, and move to the next question you can actually finish.
|
||||||
|
|
||||||
## When you are blocked
|
## The S1 probe
|
||||||
|
|
||||||
If an item needs something the container cannot do — hardware Vulkan for a
|
One iteration, then **stop and write the go/no-go**. Do not start Ready Room work
|
||||||
rendering question, a push, a decision only the user can make — **do not
|
on your own authority — MISSION.md §S1 says why.
|
||||||
improvise around it**. Write what you found, note the blocker in `BACKLOG.md`,
|
|
||||||
and move to the next item.
|
## Verifying
|
||||||
|
|
||||||
|
* `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests
|
||||||
|
self-skip and a green run means almost nothing.
|
||||||
|
* Verify with an **artifact**, not with "it compiles": `sylpheed-cli screen
|
||||||
|
info` / `screen render` / `mesh render` / `save info`, a capture, a screenshot.
|
||||||
|
* Commit the reference data beside the finding, so the port can be built without
|
||||||
|
a disc in the loop during development.
|
||||||
|
* A regenerated artifact that comes out byte-identical is strong evidence a change
|
||||||
|
was additive. When it does change, check that every diff line pairs exactly.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
`push-work` pushes the current branch to origin. It refuses anything that is not
|
||||||
|
`auto/*` and never force-pushes, so the consolidated line stays a human's
|
||||||
|
decision. Run it **every iteration that produced a commit** — not at the end of
|
||||||
|
some longer arc, which is exactly when a container dies.
|
||||||
|
|
||||||
|
If it reports no credentials, say so in your reply and continue working. Do not
|
||||||
|
improvise another route out: no remote rewrite, no credential helper of your own,
|
||||||
|
no alternate transport. A push that is blocked is a blocked push.
|
||||||
|
|
||||||
## Pacing
|
## Pacing
|
||||||
|
|
||||||
Self-pace. A useful iteration is one experiment plus its write-up, not a
|
One experiment plus its write-up is a good iteration; a marathon is not. Stop with
|
||||||
marathon; stopping with a clean commit and an honest "here is what is still
|
a clean commit, a push, and an honest list of what is still open.
|
||||||
open" is the goal every time.
|
|
||||||
|
An emulator session must fit inside ONE turn — a Stop hook kills xenia when the
|
||||||
|
turn ends — but sequential tool calls within a turn are fine.
|
||||||
|
|
||||||
|
The loop runs on a fixed interval set by the harness, so you do **not** need to
|
||||||
|
arm the next wakeup yourself. Spend that attention on the write-up instead.
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
# SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists
|
# SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists
|
||||||
# SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`)
|
# SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`)
|
||||||
# SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent)
|
# SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent)
|
||||||
|
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
|
||||||
|
# (default: $HOME/.sylph-git-credentials)
|
||||||
|
# SYLPH_LOOP_INTERVAL fixed loop cadence, e.g. 30m (default: 45m)
|
||||||
# SYLPH_CPUS / SYLPH_MEM_GB override the computed half
|
# SYLPH_CPUS / SYLPH_MEM_GB override the computed half
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -125,6 +128,21 @@ docker_args() {
|
|||||||
echo " packaged SPIRV-Tools is too old. Running is unaffected." >&2
|
echo " packaged SPIRV-Tools is too old. Running is unaffected." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── git push ──
|
||||||
|
# Read-only, and only ever used by `push-work`, which refuses anything but an
|
||||||
|
# auto/* branch and never force-pushes. Without this the agent's work only
|
||||||
|
# exists inside the container and dies with it.
|
||||||
|
GITCRED="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
|
||||||
|
if [ -f "$GITCRED" ]; then
|
||||||
|
_out+=(-v "$GITCRED:/sylph-home/re/.git-credentials:ro")
|
||||||
|
else
|
||||||
|
echo "==> NOTE: no git credentials at $GITCRED — the agent cannot push," >&2
|
||||||
|
echo " so its work will be lost if the container is destroyed. Create it" >&2
|
||||||
|
echo " with a single line and chmod 600:" >&2
|
||||||
|
echo " https://<user>:<token>@git.mc02.dev" >&2
|
||||||
|
echo " or point SYLPH_GIT_CREDENTIALS elsewhere." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
|
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
|
||||||
[ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN")
|
[ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN")
|
||||||
[ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE")
|
[ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE")
|
||||||
@@ -191,7 +209,12 @@ case "${1:-}" in
|
|||||||
TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md."
|
TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
INTERVAL="${SYLPH_LOOP_INTERVAL:-}" # empty = let the model self-pace
|
# A FIXED interval by default, not self-pacing. Self-pacing requires the
|
||||||
|
# agent to call ScheduleWakeup itself at the end of every turn, and the one
|
||||||
|
# thing an agent deep in an experiment reliably forgets is the bookkeeping
|
||||||
|
# after it. With an interval the harness owns the cadence and a forgotten
|
||||||
|
# wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace.
|
||||||
|
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
|
||||||
declare -a ARGS; docker_args ARGS
|
declare -a ARGS; docker_args ARGS
|
||||||
ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT")
|
ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT")
|
||||||
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g"
|
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g"
|
||||||
@@ -206,8 +229,19 @@ case "${1:-}" in
|
|||||||
echo " ./sylph-agent attach chat with it locally (Ctrl-P Ctrl-Q to leave it running)"
|
echo " ./sylph-agent attach chat with it locally (Ctrl-P Ctrl-Q to leave it running)"
|
||||||
echo " ./sylph-agent stop stop it"
|
echo " ./sylph-agent stop stop it"
|
||||||
echo
|
echo
|
||||||
echo " It commits to auto/* branches and cannot push — no git credentials"
|
# Report what is actually true. This line used to claim unconditionally that
|
||||||
echo " are mounted, so review its work with: git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
|
# the agent could not push, which was written before credentials were
|
||||||
|
# supported and then went stale — telling the operator their work was at risk
|
||||||
|
# when it was not, which is the exact failure the credential mount fixes.
|
||||||
|
if [ -f "${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}" ]; then
|
||||||
|
echo " It commits to auto/* branches and publishes them with push-work,"
|
||||||
|
echo " which refuses any other branch and never force-pushes. Review with:"
|
||||||
|
echo " git -C '$PROJECT/Syplheed-Reborn' fetch origin && git log --oneline origin/auto/..."
|
||||||
|
else
|
||||||
|
echo " It commits to auto/* branches and CANNOT PUSH — no git credentials"
|
||||||
|
echo " are mounted, so its work dies with the container. Review it with:"
|
||||||
|
echo " git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
logs) shift; exec docker logs "$@" "$NAME" ;;
|
logs) shift; exec docker logs "$@" "$NAME" ;;
|
||||||
|
|||||||
99
docs/port/HANDOFF.md
Normal file
99
docs/port/HANDOFF.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Handoff — what the menu port needs, and where it stands
|
||||||
|
|
||||||
|
The single page the **port agent** reads. Everything here is produced by the
|
||||||
|
container agent's reverse engineering; nothing here is a design decision about
|
||||||
|
the port itself.
|
||||||
|
|
||||||
|
Keep it current. It is a summary with links into `docs/re/`, not a second copy of
|
||||||
|
the findings — but an answer that is not reachable from this page has not been
|
||||||
|
delivered.
|
||||||
|
|
||||||
|
## How to read an answer
|
||||||
|
|
||||||
|
Every row below is one of exactly three things, and the distinction is the point:
|
||||||
|
|
||||||
|
| | meaning | what the port should do |
|
||||||
|
|---|---|---|
|
||||||
|
| **decoded** | a field on the disc, with a disc-wide check | read it from the data |
|
||||||
|
| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page |
|
||||||
|
| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly |
|
||||||
|
|
||||||
|
There is no fourth kind. If a row says *measured* or *undecodable*, the port is
|
||||||
|
**authoring** that value, not transcribing it — and it should be kept somewhere a
|
||||||
|
human can see it is a human decision, so that when it is later decoded the
|
||||||
|
authored version can be deleted.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| | Question | State | Answer / link |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Q1 | keyframe time unit + ramp shape | ❔ open | blocks all animation |
|
||||||
|
| Q2 | which build is which screen state | 🟡 partial | build 4 title, 5 main menu, 6/8/9 submenus, `palogo` splash — unconfirmed against captures |
|
||||||
|
| Q3 | paint order for the six screens | ❔ open | runtime-solved only; declaration table is refuted |
|
||||||
|
| Q4 | button → GamePart | ❔ open | labels are baked into sprites |
|
||||||
|
| Q5 | navigation semantics | ❔ open | |
|
||||||
|
| Q6 | boot sequence + what drives it | 🟡 partial | order observed; the driver is not decoded |
|
||||||
|
| Q7 | transitions | ❔ open | |
|
||||||
|
| Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
|
||||||
|
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified |
|
||||||
|
| Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today |
|
||||||
|
| S1 | Ready Room go/no-go | ❔ open | probe not run |
|
||||||
|
|
||||||
|
## Already settled — the port can rely on these today
|
||||||
|
|
||||||
|
* **`GP_TITLE.pak` is the whole title-side tree.** Build 4 is the title with the
|
||||||
|
animating wordmarks, build 5 the five-button main menu, builds 6/8/9 are
|
||||||
|
submenus, and the developer splash is the `palogo` bundle in the same archive.
|
||||||
|
✅ decoded (enumeration), 🟡 the state labels are not yet capture-confirmed.
|
||||||
|
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
||||||
|
decoration, `0x10` = primitive. ✅ decoded.
|
||||||
|
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
||||||
|
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
||||||
|
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
||||||
|
(measured and refuted, see
|
||||||
|
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
||||||
|
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
||||||
|
naming convention that holds for all 54 real pairs, not a decoded field.
|
||||||
|
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
||||||
|
a keyframe is the **start of a ramp**.
|
||||||
|
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
||||||
|
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
|
||||||
|
executable's own registration strings. ✅ This is the screen vocabulary; which
|
||||||
|
button reaches which entry is Q4 and is *not* part of it.
|
||||||
|
* **The logo splash is a screen, not a video.** `logo1`–`logo4` are
|
||||||
|
manifest-bound with no `.wmv` on the disc. ✅
|
||||||
|
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
||||||
|
for this milestone. ✅
|
||||||
|
|
||||||
|
## Facts the port will trip over
|
||||||
|
|
||||||
|
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||||||
|
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
||||||
|
ours — but it is not optional.
|
||||||
|
* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game
|
||||||
|
intro are in scope.
|
||||||
|
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
||||||
|
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
||||||
|
reader must allow a short read there and only there.
|
||||||
|
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||||||
|
for spoken lines and discards half a music mix.
|
||||||
|
* **A music bank has several sub-waves and we glue them together.** `BGM_001`
|
||||||
|
is 10 KB + 4.47 MB + 4.67 MB, concatenated into one 347 s track. Nobody has
|
||||||
|
established whether those are intro + loop, two variations, or two halves —
|
||||||
|
see Q10. Do not build menu looping on the concatenated track until it is
|
||||||
|
answered.
|
||||||
|
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||||||
|
number of XMA1 packets from any known data offset.
|
||||||
|
|
||||||
|
## Reference data
|
||||||
|
|
||||||
|
Committed alongside the findings, so the port can be built without a disc in the
|
||||||
|
loop during development:
|
||||||
|
|
||||||
|
* `sylpheed-cli screen info --build <n> GP_TITLE.pak` — the element table, per
|
||||||
|
build, with pivots, kinds, focus links, keyframes and resting poses.
|
||||||
|
* `sylpheed-cli screen render` — the reference composite. When the port draws a
|
||||||
|
screen, this is what it should be diffed against; where they disagree, one of
|
||||||
|
them is wrong and the disagreement is worth reporting back.
|
||||||
|
* `docs/re/captures/` — framebuffer captures of the real screens, for anything
|
||||||
|
that has to be checked against the game rather than against our renderer.
|
||||||
132
docs/port/MISSION.md
Normal file
132
docs/port/MISSION.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Primary objective — answer everything the menu port needs
|
||||||
|
|
||||||
|
**Status:** active, set 2026-08-28. This replaces "work the RE backlog" as the
|
||||||
|
agent's primary objective. It does not change what the agent *does* — it is still
|
||||||
|
reverse engineering — it changes what earns attention: an item is worth doing
|
||||||
|
when the menu port is blocked on it.
|
||||||
|
|
||||||
|
## Who builds what
|
||||||
|
|
||||||
|
**You do not build the port.** A separate agent will build it, from what you
|
||||||
|
produce. Your deliverable is decoded, verified, written-down answers plus the
|
||||||
|
reference data that proves them.
|
||||||
|
|
||||||
|
| | container agent (you) | port agent |
|
||||||
|
|---|---|---|
|
||||||
|
| decodes the disc | ✅ | ❌ — consumes your answers |
|
||||||
|
| measures the running game | ✅ | ❌ — no emulator |
|
||||||
|
| writes `docs/re/` and `docs/port/HANDOFF.md` | ✅ | reads them |
|
||||||
|
| Godot project, asset pipeline, transcoding | ❌ | ✅ |
|
||||||
|
|
||||||
|
If you find yourself designing an export schema or writing GDScript, you have
|
||||||
|
crossed the line. Stop and go back to the question you were answering.
|
||||||
|
|
||||||
|
`crates/sylpheed-viewer` is also **not yours to change** for this objective. The
|
||||||
|
Explorer is the human's tool for exploring and verifying the RE work, it keeps its
|
||||||
|
static-data-only rule, and the port does not depend on it.
|
||||||
|
|
||||||
|
## The target
|
||||||
|
|
||||||
|
Someone else has to build this, from your answers alone:
|
||||||
|
|
||||||
|
```
|
||||||
|
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
|
||||||
|
```
|
||||||
|
|
||||||
|
No gameplay, no 3D, no HUD, no missions. If an answer is not needed to put those
|
||||||
|
five screens on a display and let a person move through them with a d-pad and Ⓐ,
|
||||||
|
it is not in this objective.
|
||||||
|
|
||||||
|
## What is already answered
|
||||||
|
|
||||||
|
Do not re-derive these. They are in `docs/re/` and the
|
||||||
|
[disc atlas](../re/disc-atlas.html):
|
||||||
|
|
||||||
|
* **The screen archive.** `GP_TITLE.pak` holds the whole title-side tree — build 4
|
||||||
|
the title with animating wordmarks, build 5 the five-button main menu, builds
|
||||||
|
6/8/9 submenus, and the developer splash as the `palogo` bundle in the same pak.
|
||||||
|
* **Buttons are identifiable as data.** Element kind `0x3002` is a button, `0x0`
|
||||||
|
decoration, `0x10` a primitive; buttons sort top-to-bottom by resting Y; each
|
||||||
|
pairs with an `f`-suffixed highlighted variant.
|
||||||
|
* **The screen vocabulary.** The GamePart id table, 29 entries at `.rdata
|
||||||
|
0x820A1630`, confirmed by the executable's own factory-registration strings.
|
||||||
|
* **The resting pose rule** — the hold, not the longest dwell — and that a
|
||||||
|
keyframe is the *start of a ramp*.
|
||||||
|
* **Screen composition**, pixel-accurate for the tutorial pause menu and the title
|
||||||
|
main menu, via `sylpheed-cli screen render`.
|
||||||
|
* **The logo splash is not a video.** `logo1`–`logo4` are manifest-bound with no
|
||||||
|
`.wmv` on the disc; the splash is the RATC screen, which already renders.
|
||||||
|
|
||||||
|
## The open questions — these are the objective
|
||||||
|
|
||||||
|
Ordered by what blocks the port earliest. Each is done when its **gate** exists:
|
||||||
|
a written `docs/re/` result with the evidence, and reference data committed
|
||||||
|
alongside it.
|
||||||
|
|
||||||
|
| | Question | Gate |
|
||||||
|
|---|---|---|
|
||||||
|
| **Q1** | **What is a keyframe time?** Values run 16…269. 60 Hz frames would make the title intro ~4.5 s — plausible and untested. Also: is the ramp linear, or eased? | A measured answer against the running game, not an inference. Everything animated downstream depends on this number |
|
||||||
|
| **Q2** | **Which build is which screen state?** Confirm build↔state for splash, title/PRESS Ⓐ, main menu and each submenu | A table, each row confirmed against a capture of the real screen |
|
||||||
|
| **Q3** | **Paint order for these six screens.** Solved at runtime, unsolved from the file — the declaration table is provably not it | Either a rule derived from the bundle, or six measured orders and a clear statement that no file-side rule was found |
|
||||||
|
| **Q4** | **What does each button do?** Labels are baked into the sprites; no decoded field says which GamePart a button opens | Button → GamePart id, from code or from driving the game. Say which |
|
||||||
|
| **Q5** | **Navigation semantics.** Initial focus, wrap-around at the ends, whether left/right does anything, what B does on each screen | Observed behaviour, per screen |
|
||||||
|
| **Q6** | **The boot sequence, and what drives it.** Order is observable; the *data or code* that sequences it is not decoded. Include the attract loop and what returns to the title | The sequence, plus whatever the game reads to decide it |
|
||||||
|
| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture |
|
||||||
|
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
||||||
|
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
||||||
|
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||||||
|
| **S1** | **Ready Room probe.** *Gated* — one iteration, then stop | A written go/no-go (see below) |
|
||||||
|
|
||||||
|
## Known unknowns — say so, do not fill them in
|
||||||
|
|
||||||
|
Some of these may turn out to be undecodable. That is a valid, useful answer, and
|
||||||
|
it is better than a guess, because the port agent will otherwise have to author
|
||||||
|
the mapping by hand and needs to know it is authoring rather than transcribing.
|
||||||
|
|
||||||
|
For each question, the answer is one of:
|
||||||
|
|
||||||
|
* **decoded** — here is the field, here is the disc-wide check;
|
||||||
|
* **measured** — not on the disc in any form we found, but here is what the
|
||||||
|
running game does, and here is the capture;
|
||||||
|
* **undecodable, with reach** — we looked here, here and here, and this is why it
|
||||||
|
is not there.
|
||||||
|
|
||||||
|
Never a fourth thing. In particular: if Q4 ends as "read the labels off the sprite
|
||||||
|
images by eye", say exactly that — it is then an authored mapping on the port
|
||||||
|
side, not a disc fact, and mislabelling it would put a guess into the port wearing
|
||||||
|
the badge of a measurement.
|
||||||
|
|
||||||
|
## The Ready Room probe (S1) — one iteration, then stop
|
||||||
|
|
||||||
|
`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and only
|
||||||
|
**6 of its names resolve**. It is also ISL-scripted. That is either a week or a
|
||||||
|
quarter, and one cheap test tells you which.
|
||||||
|
|
||||||
|
Our screen catalog enumerates bundles by **content**, not by name — `is_build` /
|
||||||
|
`is_composable` read the bytes — so unrecoverable *paths* do not necessarily mean
|
||||||
|
unrenderable *screens*.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak"
|
||||||
|
sylpheed-cli screen render --build <n> "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" /tmp/rr.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Report how many builds it finds, whether any composite looks like a Ready Room,
|
||||||
|
and **whether the room is 2D at all** or 3D with a UI overlay — if it is 3D the
|
||||||
|
answer is no-go by definition, not "try harder".
|
||||||
|
|
||||||
|
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
|
||||||
|
authority.
|
||||||
|
|
||||||
|
## Handing it over
|
||||||
|
|
||||||
|
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
|
||||||
|
current as you answer questions: it is a summary with links into `docs/re/`, not a
|
||||||
|
second copy of the findings. An answer that is not reachable from HANDOFF.md has
|
||||||
|
not been delivered.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Godot
|
||||||
|
project itself, any asset pipeline, and any archive outside `GP_TITLE`,
|
||||||
|
`tables.pak`, `sound.pak` and `dat/movie/` — except for the S1 probe.
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md).
|
Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md).
|
||||||
|
|
||||||
|
Also durable, and worth reading before proposing anything:
|
||||||
|
[`REFUTED.md`](REFUTED.md) — what has already been tested and died ·
|
||||||
|
[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for ·
|
||||||
|
[`BACKLOG.md`](BACKLOG.md) — what is still open.
|
||||||
|
|
||||||
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
|
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
|
||||||
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
|
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
|
||||||
Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout.
|
Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout.
|
||||||
@@ -13,6 +18,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
|||||||
| IPFB `.pak` archive | ✅ | `sylpheed-formats/src/pak.rs` + `tests/pak_idxd_disc.rs` | header + 12-byte TOC, Z1/zlib payloads |
|
| IPFB `.pak` archive | ✅ | `sylpheed-formats/src/pak.rs` + `tests/pak_idxd_disc.rs` | header + 12-byte TOC, Z1/zlib payloads |
|
||||||
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
|
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
|
||||||
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
|
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
|
||||||
|
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
|
||||||
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
|
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
|
||||||
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
|
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
|
||||||
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
|
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
|
||||||
@@ -94,7 +100,7 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`idxd-legacy-reader-audit.md`](idxd-legacy-reader-audit.md) | The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong | 🟡 shape CONFIRMED by hand (`FCSRange`, `ShieldRatio`, hangar `Model`); disc-wide rates are single-source |
|
| [`idxd-legacy-reader-audit.md`](idxd-legacy-reader-audit.md) | The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong | 🟡 shape CONFIRMED by hand (`FCSRange`, `ShieldRatio`, hangar `Model`); disc-wide rates are single-source |
|
||||||
| [`structures/idxd-container.md`](structures/idxd-container.md) | The IDXD/IXUD container — record/field table, and the two beliefs it withdraws | ✅ CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures |
|
| [`structures/idxd-container.md`](structures/idxd-container.md) | The IDXD/IXUD container — record/field table, and the two beliefs it withdraws | ✅ CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures |
|
||||||
| [`structures/hud-glyph-quad.md`](structures/hud-glyph-quad.md) | The HUD's glyph quad — vtable `0x820B2A64` | ✅ CONFIRMED for the object layout and the atlas size, read live off |
|
| [`structures/hud-glyph-quad.md`](structures/hud-glyph-quad.md) | The HUD's glyph quad — vtable `0x820B2A64` | ✅ CONFIRMED for the object layout and the atlas size, read live off |
|
||||||
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` leading-stream offset is `first_riff % 2048`, not the constant 1392 | ✅ CONFIRMED by decoding — 85 of 140 sampled banks yield more audio (median 70×), 54 identical controls. ⚠️ The *cause* is a segment-packing phase, not a header: `X = (cumulative .pNN start) mod 2048`. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
||||||
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
|
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
|
||||||
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
|
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
|
||||||
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
|
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
|
||||||
@@ -116,12 +122,13 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model — `Generic`, `Maneuver`, `Effect` | ✅ CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read; AA_/AV_ are one interleaved block (AV at X, AA at X+8), selection 🔴 BLOCKED for static RE (4 routes, all controlled) |
|
| [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model — `Generic`, `Maneuver`, `Effect` | ✅ CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read; AA_/AV_ are one interleaved block (AV at X, AA at X+8), selection 🔴 BLOCKED for static RE (4 routes, all controlled) |
|
||||||
| [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet — `Weapon`, `Shell`, `AssortMissileParam` | ✅ CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read |
|
| [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet — `Weapon`, `Shell`, `AssortMissileParam` | ✅ CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read |
|
||||||
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
|
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
|
||||||
|
| [`structures/isl-builtins.md`](structures/isl-builtins.md) | The 147 ISL built-ins — dispatch table, calling convention, and which one does what | ✅ for the table/ABI and ~135 handlers; ✅ 26 `set_unit_hp_pct`, 29 `set_unit_damage_taken_pct`, 101 `all_units_invulnerable`; 🟡 28 |
|
||||||
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
|
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
|
||||||
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
|
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
|
||||||
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |
|
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |
|
||||||
| [`structures/asteroid-fields.md`](structures/asteroid-fields.md) | `AsteroidGroup_00N` — the asteroid-field tables a `Phase_N.AsteroidDefinition` names | ✅ 10 objects/pack, 384 group records, 3 fields each (`AsteroidModelName`, `AsteroidFrameName`, `EnumAsteroid` = **the frame record's `FrameCount`, 57/57**, and the frame holds **`index, quaternion(x,y,z,w), position(x,y,z)`** per asteroid — 37 518 items, unit norm 37 518/37 518); `Enum<Thing>` counts while `Enumerate<Thing>` names a table (54/60); the join resolves as `name_hash("stage\" + name)` 9/9; 🔑 a tenth `Frame_Alpha_S01_*` object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 **S28.Phase_1 borrows `S14_p2_asteroid.tbl`** — the per-phase join shows 9 tables ↔ 9 volumes with one table shared |
|
| [`structures/asteroid-fields.md`](structures/asteroid-fields.md) | `AsteroidGroup_00N` — the asteroid-field tables a `Phase_N.AsteroidDefinition` names | ✅ 10 objects/pack, 384 group records, 3 fields each (`AsteroidModelName`, `AsteroidFrameName`, `EnumAsteroid` = **the frame record's `FrameCount`, 57/57**, and the frame holds **`index, quaternion(x,y,z,w), position(x,y,z)`** per asteroid — 37 518 items, unit norm 37 518/37 518); `Enum<Thing>` counts while `Enumerate<Thing>` names a table (54/60); the join resolves as `name_hash("stage\" + name)` 9/9; 🔑 a tenth `Frame_Alpha_S01_*` object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 **S28.Phase_1 borrows `S14_p2_asteroid.tbl`** — the per-phase join shows 9 tables ↔ 9 volumes with one table shared |
|
||||||
| [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == max−min` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ |
|
| [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == max−min` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ |
|
||||||
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a per-map spatial grid (and `MCOL` beside it) | ✅ CONFIRMED for the header, which self-checks on all 11 objects on |
|
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it) | ✅ CONFIRMED — tet mesh + face adjacency + portal costs + uniform grid; decoded from the file's own `POF0` fixup table |
|
||||||
| [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the |
|
| [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the |
|
||||||
| [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — |
|
| [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — |
|
||||||
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
|
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
|
||||||
|
|||||||
105
docs/re/METHOD.md
Normal file
105
docs/re/METHOD.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Method traps already paid for
|
||||||
|
|
||||||
|
Each line cost an iteration at least once. They are general — they are not about
|
||||||
|
Sylpheed, they are about how this kind of measurement goes wrong.
|
||||||
|
|
||||||
|
Like [`REFUTED.md`](REFUTED.md), this list had been living in the autonomous
|
||||||
|
agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
||||||
|
✅/🟡/❔ confidence convention itself.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
* **Every result needs a control. A control that fails kills the instrument.**
|
||||||
|
* **Run the known-positive through a new filter FIRST.** Three filters have been
|
||||||
|
killed by their own control. When one fails, **read the known-good's
|
||||||
|
disassembly** before assuming a shape.
|
||||||
|
* **A measured negative is a result** — but a negative is only as strong as the
|
||||||
|
route you ran, so **state its reach**.
|
||||||
|
* **A null result needs its cause shown to have happened.**
|
||||||
|
* **A result with NO unknowns is suspicious.**
|
||||||
|
* **Census the whole set; always run the other population as the control.**
|
||||||
|
**Zero partials is stronger than a majority.**
|
||||||
|
* **A 2×2 partition is the sharpest general tool** — both off-diagonals empty is
|
||||||
|
a law.
|
||||||
|
* **Re-derive a doc's own numbers as the control.**
|
||||||
|
|
||||||
|
## Inference
|
||||||
|
|
||||||
|
* **Never conclude from ONE sample.**
|
||||||
|
* **A law proved on one population is a hypothesis on the next.**
|
||||||
|
* **Finding one exception does not imply a family.**
|
||||||
|
* **Consistency is not proof. A suggestive coincidence is a coincidence until
|
||||||
|
measured.** **An analogy is not a measurement.**
|
||||||
|
* **Same layout ≠ same instance.** **Same record-name set ≠ same object.**
|
||||||
|
* **A marker is only proven by what it leaves out.**
|
||||||
|
* **A high-confidence SCORE is not a high-confidence MECHANISM.**
|
||||||
|
* **Knowing HOW MANY is not knowing WHICH.**
|
||||||
|
* **Round numbers matching is weak evidence — unless you read the constant.**
|
||||||
|
* **My own last-turn result is a hypothesis too.**
|
||||||
|
* **A global partition can understate a per-owner one.**
|
||||||
|
* **A residual is measured against a population — name it.**
|
||||||
|
|
||||||
|
## Searching and tooling
|
||||||
|
|
||||||
|
* **A search that returns thousands has no power; state the reach.**
|
||||||
|
* **A substring match is not a hit.** **A regex miss looks like a null result —
|
||||||
|
print one raw sample before believing a zero.**
|
||||||
|
* **A derived table can be a cross product — measure its shape first.**
|
||||||
|
* **After refuting an instrument, sweep everything that depended on it.**
|
||||||
|
* **Before measuring how wrong a tool is, read what the tool actually does.**
|
||||||
|
* **The instrument must pass its own control.**
|
||||||
|
* **Classify a bulk before mining it. The residual is the prize.**
|
||||||
|
* **Rank by similarity — the cliff is the finding.** But **read the values
|
||||||
|
before trusting the rank.**
|
||||||
|
* **Grep the nouns before designing the experiment — and believe it.**
|
||||||
|
* **Grep gives you a file list — read *every* file on it.**
|
||||||
|
* **The answer is often already in the doc that owns the subject — read it end
|
||||||
|
to end.** A 🟡 often names its own route.
|
||||||
|
* **Before re-trying a blocked idea, check whether the blocker's own doc already
|
||||||
|
tried it.**
|
||||||
|
* **Ship a regenerator with every artefact.** An artefact that moves by a pure
|
||||||
|
reorder is a tool bug.
|
||||||
|
* **Never print per-entry lines from a disc-wide sweep — aggregate.**
|
||||||
|
|
||||||
|
## Reading the data
|
||||||
|
|
||||||
|
* **Read what a loader NAMES, not where it stores.**
|
||||||
|
* **A field the disc never values still gets named by the loader.**
|
||||||
|
* **An indexed read beats a deduped-pool adjacency read.**
|
||||||
|
* **Check the whole string set, not the one matching word.**
|
||||||
|
* **A dict keyed by record name across a multi-entry pak is a lie.**
|
||||||
|
* **A set-difference over names hides reuse — join per USER.**
|
||||||
|
* **A self-index names records, not files.**
|
||||||
|
* **Case-insensitive hashing means two spellings can be one entry.**
|
||||||
|
* **An "unresolved" name may be the wrong kind, namespace or prefix — or part of
|
||||||
|
a cut asset.**
|
||||||
|
* **A garbled value may be a real string in another encoding.**
|
||||||
|
* **Two of my own counts disagreeing is a grammar clue.**
|
||||||
|
* **A game's own typo is a join key.**
|
||||||
|
* **A bias constant in the code is a join key.**
|
||||||
|
* **A prefix trap: enumerate maximal `[A-Za-z0-9_]` runs, not `startswith`.**
|
||||||
|
* **Re-deriving a format is not a finding — asking whether its values *resolve*
|
||||||
|
is.**
|
||||||
|
|
||||||
|
## Mechanics that have bitten
|
||||||
|
|
||||||
|
* **Never hand-convert a decimal VA — print `hex()`.**
|
||||||
|
* **`grep -c` counts LINES** — use `grep -o | wc -l`.
|
||||||
|
* **`Counter.most_common()` tie-breaks by insertion order — use `sorted()`.**
|
||||||
|
* **Raw grep cannot see inside compressed pak entries.**
|
||||||
|
* **Commit messages go in a file** (`git commit -F`); a literal `|` in a table
|
||||||
|
cell needs escaping; `git log --all -- <path>` can hang.
|
||||||
|
|
||||||
|
## Runtime / emulator
|
||||||
|
|
||||||
|
* **Look at the PNG** — and check its dimensions.
|
||||||
|
* **"Animating" is not "still in a mission".**
|
||||||
|
* **Dedup entity enumerations by position value.**
|
||||||
|
* **Do not diagnose timing or liveness under gdb.** `ps %cpu` is cumulative.
|
||||||
|
* **Classify screens by whole-image statistics, not named pixels** — a named
|
||||||
|
pixel is only valid while the image sits at a known place, and nothing errors
|
||||||
|
when it moves.
|
||||||
|
* **Do not poll faster than the guest updates** — it manufactures a clean curve
|
||||||
|
out of noise.
|
||||||
|
* **A probe that never performs the action will "prove" the action does not
|
||||||
|
exist.**
|
||||||
185
docs/re/REFUTED.md
Normal file
185
docs/re/REFUTED.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# Refuted — claims that were tested and died
|
||||||
|
|
||||||
|
**Read this before proposing a hypothesis.** Every line below was believed at
|
||||||
|
some point, measured, and found false. Reviving one costs a whole iteration and
|
||||||
|
produces nothing.
|
||||||
|
|
||||||
|
This file exists because the list had been living in the autonomous agent's
|
||||||
|
*loop prompt* — the only copy, lost the moment the container was. A refuted
|
||||||
|
result is a real result; it belongs in the corpus like any other.
|
||||||
|
|
||||||
|
**Format:** each entry is the claim as it was believed. Where the true answer is
|
||||||
|
known it follows after `→`. Grouped by subject so a grep for your noun finds the
|
||||||
|
neighbourhood, not just the line.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offsets, structs and the progress singleton
|
||||||
|
|
||||||
|
* `position = instance − 0x12c` → refuted.
|
||||||
|
* `+0x29d0` → refuted.
|
||||||
|
* "an offset intersection can find a struct's consumer" → **only for LARGE or
|
||||||
|
unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits).
|
||||||
|
* "a `+1956` store means a progress write" → writes go through the COPY, not
|
||||||
|
direct stores. 9 direct stores, none of them a progress write.
|
||||||
|
* "the singleton-global filter can find progress writers" → it fails its own
|
||||||
|
control.
|
||||||
|
* "the progress copy destination is an `r1`-relative stack local" → it is a
|
||||||
|
**frame register**. The `r1` assumption returned 0 for all 21 candidates
|
||||||
|
*including the known-good* — the filter was killed by its own control.
|
||||||
|
* "word B's writer also stores the Time/Points record" → it does not.
|
||||||
|
* "the debriefing records the metric with the clear bit" → 44 calls, exactly two
|
||||||
|
strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`.
|
||||||
|
* "`0x820AF030` holds live state" → all 384 words constant; it is a
|
||||||
|
spawned-entity record, not live state.
|
||||||
|
|
||||||
|
## Screens, classes and RTTI
|
||||||
|
|
||||||
|
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
|
||||||
|
`ANON_`, 0 `rtti_present`, 0 base classes.
|
||||||
|
* "the sibling vtable methods name the class" → they cannot.
|
||||||
|
* "`xrefs` can name the callers of a vtable method" → no.
|
||||||
|
* "the `ind_call` refutation voids existing corpus claims" → damage bounded,
|
||||||
|
4/4 caller claims verify. But **`xrefs.ind_call` is a CROSS PRODUCT** — always
|
||||||
|
filter `kind='call'`.
|
||||||
|
* "`BASE_INFO` marks the 5-slot screen family" → it discriminates
|
||||||
|
screen-config from table-read, 9/9 vs 10/10.
|
||||||
|
* "a high key count means a rich screen" → `sub_82297550` / `sub_822A2F00`'s 27
|
||||||
|
"keys" are coordinate pairs, i.e. a layout table.
|
||||||
|
* "`EX_` = the CHALLENGE-mission debriefing" → `EX_` is **EXTRA**, mission-kind
|
||||||
|
3.
|
||||||
|
* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`.
|
||||||
|
|
||||||
|
## Stages, missions and the challenge set
|
||||||
|
|
||||||
|
* "the disc's stages are numbered 1..28" → S01–S16 story, S17 **cut**, S18–S23
|
||||||
|
tutorials, S24–S29 challenge.
|
||||||
|
* "S24–S29 are story missions" → they are the challenge missions.
|
||||||
|
* "the challenge missions have their own maps" → they reuse
|
||||||
|
`GP_MAIN_GAME_E.pak`'s stage records.
|
||||||
|
* "the challenge `REQUIREMENT` values are 16,25,26,27,29" → the chain is
|
||||||
|
16→24→25→26→27→28.
|
||||||
|
* "the `Extra0n` family shares one leaderboard metric" → `RECORD_TYPE` is
|
||||||
|
per-stage: 3 Time / 3 Points.
|
||||||
|
* "stage = filled SHAB count + 1" → refuted.
|
||||||
|
* "the first TRIGGER is always the point of no return" → refuted.
|
||||||
|
* "`EnumUnit_S14.tbl` might be missing" / "S14's 13 are a manifest omission" /
|
||||||
|
"asteroids are exempt from the manifest" → S14's 13 are **dangling
|
||||||
|
deployments**. NEEDS-HUMAN: fly S14.
|
||||||
|
* "`StageMessageSet_S02.tbl` is not in the pak" → it is.
|
||||||
|
* "`S28_p1` has an asteroid volume with no definition" → refuted.
|
||||||
|
* "`test_s8p1_asteroid.tbl` is test-only" → refuted.
|
||||||
|
* "the settings family has 28 or 29 objects" → 24.
|
||||||
|
|
||||||
|
## ISL / mission scripting
|
||||||
|
|
||||||
|
* "the bytecode is in the `.embsec_` sections" → refuted.
|
||||||
|
* "only 31 built-ins take a unit" → refuted.
|
||||||
|
* "`sub_8230C398` is the message pump" → refuted.
|
||||||
|
* "`bus+8216` is the subscriber registry" → refuted.
|
||||||
|
* "the ScriptPhase vtable is ≥200 slots" → 113.
|
||||||
|
|
||||||
|
## IDXD, paks and naming
|
||||||
|
|
||||||
|
* "IDXD record keys are `name_hash`" → record keys are **`tag_hash`**
|
||||||
|
(case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys.
|
||||||
|
* "pak TOC order is stage order" / "TOC order is semantic order" → it is not.
|
||||||
|
* "the executable holds the asset names" → the image names **no data value at
|
||||||
|
all**; that route is powerless.
|
||||||
|
* "the image might name a data VALUE" → powerless.
|
||||||
|
* "the XPR2 manifest names hash to the DefTables tables" → refuted.
|
||||||
|
* "the `DefTables` model names are unreachable" → reachable via the `Enumerate`
|
||||||
|
declaration tables (1 413/1 425, 99.2 %).
|
||||||
|
* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted.
|
||||||
|
* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted.
|
||||||
|
* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted.
|
||||||
|
* "the `Enumeration` self-index can name objects" → a self-index names
|
||||||
|
**records, not files**.
|
||||||
|
* "a per-pak prefix might close the 2D blocker" → no.
|
||||||
|
* "the `+` paths might name the 2D or `GP_READY_ROOM` keys" → the `+`-dictionary
|
||||||
|
route is exhausted, 0 of 1 817.
|
||||||
|
* "the `game:\` paths are unresolved" → refuted.
|
||||||
|
* "a set-difference over file names can see reuse" → it cannot; **join per
|
||||||
|
USER**. Per-pak copies are ×6.
|
||||||
|
|
||||||
|
## Units, weapons, effects and assets
|
||||||
|
|
||||||
|
* "`Generic` (394) is the unit datasheet" → refuted.
|
||||||
|
* "a loadout's `Arm1` names an item" → it names a **hardpoint slot**
|
||||||
|
(`Turret_NNN`), 59/59.
|
||||||
|
* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not.
|
||||||
|
* "the roster is the `Generic.Model` set" → roster 40, `Generic.Model` 46,
|
||||||
|
`GameResourceID` 480 — three vocabularies.
|
||||||
|
* "every unit ID is `UN_<l>###_<FACTION>_<name>`" → the grammar is
|
||||||
|
`UN_<letter>###_[<subkind>_]<FACTION>_<name>`.
|
||||||
|
* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist.
|
||||||
|
* "the only two `_EX5` names on the disc are the AA gun and the DeltaSaber" →
|
||||||
|
refuted.
|
||||||
|
* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted.
|
||||||
|
* "the disc has exactly three `EnumWeapon` tables" → four.
|
||||||
|
* "the `wep_NN` package gaps are unshipped weapons" / "`wep_85` is the tip of a
|
||||||
|
family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26).
|
||||||
|
* "nothing is deployed without being declared" → refuted.
|
||||||
|
* "effects are one namespace" → refuted.
|
||||||
|
* "the 58 undeclared effect names are missing assets" → refuted.
|
||||||
|
* "all five orphan effects are unshipped" → refuted.
|
||||||
|
* "`eff_f0002` ships in `Base.xpr`" → refuted.
|
||||||
|
* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted.
|
||||||
|
* "the 34 unlocated are a scatter" → refuted.
|
||||||
|
* "the 9 unlocated might be under another prefix" → refuted.
|
||||||
|
* "`ptc_pack` has 532 names" → 727.
|
||||||
|
* "the `_e`/`_f` law is effect-FIELD-specific" → it is the **faction law**,
|
||||||
|
564/564.
|
||||||
|
* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot.
|
||||||
|
* "`rot_n001` is on the disc" → refuted.
|
||||||
|
* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`.
|
||||||
|
* "`parent` + `_all` + `_child` is the composite-model convention" → refuted.
|
||||||
|
`_hangar` **is** real (59 of 166, 59/59 with a bare twin); `_all`/`_child` is
|
||||||
|
not.
|
||||||
|
* "`Motion_guard_start` has no damage variants" → refuted.
|
||||||
|
* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted.
|
||||||
|
* "the 27 unresolved `NamePlate` values are missing objects" → refuted.
|
||||||
|
|
||||||
|
## LOD, background and misc tables
|
||||||
|
|
||||||
|
* "`EnumLODSet_*` is a per-stage family" → `EnumLODSet_test.tbl` serves 17
|
||||||
|
stages; 17+5+1 = 23.
|
||||||
|
* "there are 8 orphan LOD tables" → 6.
|
||||||
|
* "the orphans are stale copies of `_test`" → refuted.
|
||||||
|
* "S25 is absent from the `DefTables` LOD families" → refuted.
|
||||||
|
* "`BackGroundID` has no referent anywhere" → it is an **identity**.
|
||||||
|
* "`BackGroundPackage == BG_<id>.xpr`" → refuted.
|
||||||
|
* "`<X>ID` + `<X>Package` is a convention" → refuted.
|
||||||
|
* "`Placement_*` / `RouteTest_*` are unattached" → refuted.
|
||||||
|
* "the `AsteroidDefinition` join does not reproduce by hash" → it does.
|
||||||
|
* "the 8-value frame is a new finding" → it was already in the corpus.
|
||||||
|
|
||||||
|
## Loaders, config and tuning
|
||||||
|
|
||||||
|
* "the config reader is XML" → INI.
|
||||||
|
* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s.
|
||||||
|
* "`sub_822AE628` reads the main-game `Tweak`" → refuted.
|
||||||
|
* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader.
|
||||||
|
* "`sub_82286BC8`'s key list is new" → refuted.
|
||||||
|
* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted.
|
||||||
|
* "`Booster` is a new schema" / "`Booster` is the player craft's flight
|
||||||
|
envelope" → refuted; nothing selects `Booster`.
|
||||||
|
* "the `AnalogRevice`/`Tweak` block is unreachable" → reachable
|
||||||
|
(`sub_821A6CF0`, base `0x820A1630`).
|
||||||
|
* "a 0-xref string block has no reader" → refuted.
|
||||||
|
* "the AI table was NEEDS-HUMAN" → refuted.
|
||||||
|
* "the `PG*` HUD names are undocumented" → they are documented.
|
||||||
|
* "a base-solver row identifies a FUNCTION" → it does not.
|
||||||
|
* "a 64K-boundary base is low confidence" → **inverted**; it is high
|
||||||
|
confidence.
|
||||||
|
* "the 0x820B0000 cluster is a false positive" → refuted.
|
||||||
|
* "a pointer to a function in the image implies a registry" → refuted.
|
||||||
|
`.pdata` is not a registry.
|
||||||
|
* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted.
|
||||||
|
* "the 8 undeclared chatter tables are tutorial chatter" → refuted.
|
||||||
|
* "other datasheets ship a schema too" → refuted.
|
||||||
|
|
||||||
|
## Encoding and text
|
||||||
|
|
||||||
|
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
|
||||||
|
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.
|
||||||
7549
docs/re/data/idxd-unnamed-field-keys.txt
Normal file
7549
docs/re/data/idxd-unnamed-field-keys.txt
Normal file
File diff suppressed because it is too large
Load Diff
909
docs/re/data/isl-builtins-26-28-29-sites.txt
Normal file
909
docs/re/data/isl-builtins-26-28-29-sites.txt
Normal file
@@ -0,0 +1,909 @@
|
|||||||
|
# Every call site of ISL built-ins 26, 28, 29 and 101 across all 28 stage scripts.
|
||||||
|
#
|
||||||
|
# Regenerate with:
|
||||||
|
# python3 tools/re-capture/isl_builtin_sites.py 26,28,29,101 \
|
||||||
|
# --ssb <dir with StageNN.ssb> --craft <unitgroup.py --all output>
|
||||||
|
#
|
||||||
|
# Columns: stage offset builtin slot8 slot4-name slot4-raw symtype before after
|
||||||
|
# slot8 is the double operand; the handler multiplies it by 0.01 before sending it.
|
||||||
|
#
|
||||||
|
Stage01 0x0009C8 29 0 TCN001 0x1B type2 [35,118,12] [12,29,15]
|
||||||
|
Stage01 0x000A44 29 0 TCN002 0x1E type2 [12,29,12] [15,12,30]
|
||||||
|
Stage01 0x001A5C 29 100 TCN001 0x1B type2 [127,123,8] [35,11,58]
|
||||||
|
Stage01 0x0028A8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage01 0x002CF8 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage01 0x003210 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage01 0x007370 28 10 TCN002 0x1E type2 [1,1,1] [29,30,64]
|
||||||
|
Stage01 0x0073B0 29 0 TCN002 0x1E type2 [1,1,28] [30,64,64]
|
||||||
|
Stage01 0x00EDAC 29 0 TCN001 0x1B type2 [35,118,12] [12,15,29]
|
||||||
|
Stage01 0x00EE80 29 0 TCN002 0x1E type2 [29,12,15] [12,15,29]
|
||||||
|
Stage01 0x00EF54 29 0 TCN003 0x20 type2 [29,12,15] [12,15,30]
|
||||||
|
Stage01 0x00F094 28 100 ADT201 0x3 type2 [12,15,30] [12,15,30]
|
||||||
|
Stage01 0x00F1D4 28 100 ADT202 0x6 type2 [12,15,30] [12,3,15]
|
||||||
|
Stage01 0x00F348 28 100 ADS221 0x7 type2 [12,3,15] [12,3,15]
|
||||||
|
Stage01 0x00F4BC 28 200 ADS222 0xB type2 [12,3,15] [12,15,28]
|
||||||
|
Stage01 0x00F590 28 100 ADS223 0xE type2 [28,12,15] [12,15,12]
|
||||||
|
Stage01 0x0100B0 29 100 TCN001 0x1B type2 [127,123,8] [35,103,1]
|
||||||
|
Stage01 0x010398 28 10 TCN002 0x1E type2 [1,1,1] [29,30,11]
|
||||||
|
Stage01 0x0103D8 29 0 TCN002 0x1E type2 [1,1,28] [30,11,58]
|
||||||
|
Stage01 0x01060C 28 100 ADT203 0xA type2 [15,30,47] [103,69,63]
|
||||||
|
Stage01 0x0108B4 28 100 ADT204 0xD type2 [58,15,30] [47,1,1]
|
||||||
|
Stage01 0x010B40 28 100 ADS224 0x11 type2 [58,3,15] [1,11,58]
|
||||||
|
Stage01 0x010CE8 28 100 ADS225 0x13 type2 [58,3,15] [1,11,58]
|
||||||
|
Stage01 0x010DF0 28 100 ADT205 0x10 type2 [11,58,15] [30,47,20]
|
||||||
|
Stage01 0x01115C 28 200 ADS227 0x17 type2 [57,3,15] [58,15,28]
|
||||||
|
Stage01 0x011230 28 100 ADS226 0x15 type2 [28,58,15] [48,92,1]
|
||||||
|
Stage01 0x0115CC 28 100 ADS228 0x19 type2 [58,3,15] [1,11,58]
|
||||||
|
Stage01 0x011774 28 100 ADS229 0x1A type2 [58,3,15] [1,11,95]
|
||||||
|
Stage01 0x01184C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage01 0x011D58 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage01 0x0127AC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage02 0x000E6C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||||
|
Stage02 0x002D14 26 0 TCN131 0x57 type2 [14,77,11] [11,116,127]
|
||||||
|
Stage02 0x002E0C 29 100 TCN001 0x4B type2 [127,123,8] [35,103,30]
|
||||||
|
Stage02 0x003220 29 0 TCN002 0x4E type2 [1,1,1] [11,58,15]
|
||||||
|
Stage02 0x004DA8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage02 0x005274 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage02 0x0059BC 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage02 0x0060A8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage02 0x01586C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||||
|
Stage02 0x018544 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
|
||||||
|
Stage02 0x0186DC 29 0 TCN002 0x4E type2 [1,1,1] [11,58,3]
|
||||||
|
Stage02 0x019204 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage02 0x0196D0 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage02 0x01A21C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage02 0x01E114 26 0 ADN207 0x2A type2 [79,7,70] [26,11,47]
|
||||||
|
Stage02 0x01E154 26 0 TCN208 0x68 type2 [7,70,26] [11,47,7]
|
||||||
|
Stage02 0x01E2C4 26 0 ADN207 0x2A type2 [47,7,70] [26,11,47]
|
||||||
|
Stage02 0x01E304 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
|
||||||
|
Stage02 0x01E58C 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
|
||||||
|
Stage02 0x01E5CC 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
|
||||||
|
Stage02 0x01E854 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
|
||||||
|
Stage02 0x01E894 26 0 TCN208 0x68 type2 [7,70,26] [11,47,18]
|
||||||
|
Stage02 0x025B14 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||||
|
Stage02 0x025FCC 26 60 TCN324 0x67 type2 [15,12,15] [12,15,26]
|
||||||
|
Stage02 0x0260A0 26 80 TCN325 0x6A type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026174 26 60 TCN326 0x6D type2 [26,12,15] [12,15,12]
|
||||||
|
Stage02 0x0262DC 26 80 TCN328 0x74 type2 [15,12,15] [12,15,12]
|
||||||
|
Stage02 0x026AD8 26 80 ADN322 0x1F type2 [15,12,15] [12,15,26]
|
||||||
|
Stage02 0x026BAC 26 60 ADN323 0x25 type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026C80 26 80 ADN324 0x2C type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026D54 26 30 ADN325 0x32 type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026E28 26 80 ADN326 0x38 type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026EFC 26 30 ADN327 0x3D type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x026FD0 26 60 ADN328 0x42 type2 [26,12,15] [12,15,26]
|
||||||
|
Stage02 0x0270A4 26 80 ADN329 0x45 type2 [26,12,15] [12,15,12]
|
||||||
|
Stage02 0x02720C 26 80 ADN331 0x20 type2 [15,12,15] [12,15,26]
|
||||||
|
Stage02 0x0272E0 26 60 ADN332 0x26 type2 [26,12,15] [47,47,47]
|
||||||
|
Stage02 0x028B5C 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
|
||||||
|
Stage02 0x028E14 29 0 TCN002 0x4E type2 [1,1,1] [11,26,26]
|
||||||
|
Stage02 0x028E64 26 0 ADN313 0x1E type2 [1,29,11] [26,11,57]
|
||||||
|
Stage02 0x028EA4 26 0 TCN334 0x6B type2 [29,11,26] [11,57,30]
|
||||||
|
Stage02 0x02907C 29 100 ADT301 0x31 type2 [30,15,19] [57,30,15]
|
||||||
|
Stage02 0x029244 29 100 ADT302 0x37 type2 [30,15,19] [48,47,47]
|
||||||
|
Stage02 0x029B84 29 100 ADT303 0x3C type2 [30,15,19] [1,64,70]
|
||||||
|
Stage02 0x029ED0 29 100 ADT304 0x41 type2 [30,15,19] [57,30,15]
|
||||||
|
Stage02 0x02A098 29 100 ADT305 0x44 type2 [30,15,19] [58,15,48]
|
||||||
|
Stage02 0x02AAE8 29 100 ADT306 0x47 type2 [30,15,19] [57,30,15]
|
||||||
|
Stage02 0x02ACB0 29 100 ADT307 0x48 type2 [30,15,19] [57,30,15]
|
||||||
|
Stage02 0x02AE78 29 100 ADT308 0x49 type2 [30,15,19] [58,15,48]
|
||||||
|
Stage02 0x02B530 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage02 0x02B9FC 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage02 0x02C000 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage02 0x02C524 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage02 0x02D018 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage02 0x02D2DC 28 150 TCN004 0x56 type2 [20,105,115] [11,26,64]
|
||||||
|
Stage02 0x02D350 26 0 ADT301 0x31 type2 [115,28,11] [64,90,11]
|
||||||
|
Stage02 0x02D4D4 26 0 ADT302 0x37 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02D658 26 0 ADT303 0x3C type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02D7DC 26 0 ADT304 0x41 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02D960 26 0 ADT305 0x44 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02DAF0 26 0 ADT306 0x47 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02DC74 26 0 ADT307 0x48 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage02 0x02DDF8 26 0 ADT308 0x49 type2 [64,90,11] [64,90,11]
|
||||||
|
Stage03 0x0009F0 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage03 0x002514 26 0 TCN132 0x30 type2 [14,77,11] [11,116,127]
|
||||||
|
Stage03 0x00260C 29 100 TCN001 0x21 type2 [127,123,8] [35,103,1]
|
||||||
|
Stage03 0x002810 29 0 TCN002 0x24 type2 [30,91,47] [11,58,19]
|
||||||
|
Stage03 0x0034E8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage03 0x0039B4 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage03 0x00424C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage03 0x004720 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage03 0x00ABB4 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage03 0x00B0CC 29 0 TCT207 0x42 type2 [15,19,30] [12,15,19]
|
||||||
|
Stage03 0x00C720 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
|
||||||
|
Stage03 0x00DD7C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage03 0x00E248 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage03 0x00E84C 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage03 0x00EFF8 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage03 0x00F844 29 0 TCT207 0x42 type2 [64,19,11] [5,11,20]
|
||||||
|
Stage03 0x00FA58 29 0 TCT208 0x44 type2 [19,19,11] [5,11,20]
|
||||||
|
Stage03 0x00FC6C 29 0 TCT209 0x46 type2 [19,19,11] [5,11,20]
|
||||||
|
Stage03 0x00FE34 29 100 TCT207 0x42 type2 [11,20,20] [11,20,20]
|
||||||
|
Stage03 0x00FF50 29 100 TCT207 0x42 type2 [11,20,20] [11,20,105]
|
||||||
|
Stage03 0x01584C 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage03 0x0164CC 28 500 ADN301 0x4 type2 [117,12,15] [48,48,48]
|
||||||
|
Stage03 0x016FF8 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
|
||||||
|
Stage03 0x01713C 28 500 ADN304 0xB type2 [58,15,91] [58,15,30]
|
||||||
|
Stage03 0x01727C 28 1000 ADT302 0x19 type2 [58,15,30] [91,103,4]
|
||||||
|
Stage03 0x017C6C 28 500 ADN306 0x11 type2 [11,58,15] [91,69,47]
|
||||||
|
Stage03 0x0182F4 28 500 ADN308 0x18 type2 [11,58,15] [91,69,47]
|
||||||
|
Stage03 0x018A4C 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
|
||||||
|
Stage03 0x0191FC 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage03 0x0196C8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage03 0x019CCC 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage03 0x019ED4 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage03 0x01A744 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage03 0x01BC10 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
|
||||||
|
Stage03 0x01C358 28 150 TCN309 0x41 type2 [9,115,69] [64,11,64]
|
||||||
|
Stage04 0x000A38 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage04 0x002228 29 100 TCN001 0x15 type2 [127,123,8] [35,11,69]
|
||||||
|
Stage04 0x0026D0 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage04 0x002BCC 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage04 0x003390 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage04 0x003834 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage04 0x005ED0 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage04 0x006260 28 500 ADN201 0x0 type2 [15,12,15] [12,30,15]
|
||||||
|
Stage04 0x0063A0 28 1000 ADT202 0x9 type2 [12,30,15] [12,15,12]
|
||||||
|
Stage04 0x0074CC 29 100 TCN001 0x15 type2 [127,8,123] [35,11,58]
|
||||||
|
Stage04 0x007674 28 500 ADN206 0x4 type2 [58,15,3] [92,91,64]
|
||||||
|
Stage04 0x007990 28 500 ADN207 0x6 type2 [58,3,15] [47,1,11]
|
||||||
|
Stage04 0x007B8C 28 500 ADN208 0x8 type2 [58,15,3] [92,91,64]
|
||||||
|
Stage04 0x007FAC 28 2000 ADT203 0xD type2 [30,47,30] [64,64,8]
|
||||||
|
Stage04 0x008324 28 500 ADN209 0xC type2 [58,15,3] [92,91,1]
|
||||||
|
Stage04 0x0084AC 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage04 0x0089A8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage04 0x009030 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage04 0x0099DC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage04 0x009D40 28 150 TCN205 0x25 type2 [9,115,69] [64,11,20]
|
||||||
|
Stage04 0x00A004 28 2000 ADT203 0xD type2 [15,30,47] [64,64,64]
|
||||||
|
Stage05 0x000898 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
|
||||||
|
Stage05 0x00181C 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage05 0x002E6C 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage05 0x003BAC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage05 0x003F28 28 150 TCN003 0x19 type2 [9,115,70] [70,48,11]
|
||||||
|
Stage05 0x007F00 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
|
||||||
|
Stage05 0x0081C0 26 80 TCN004 0x1E type2 [12,15,30] [12,15,30]
|
||||||
|
Stage05 0x008300 28 150 ADT201 0x4 type2 [12,15,30] [12,19,15]
|
||||||
|
Stage05 0x0084C8 28 150 ADT202 0x6 type2 [19,15,30] [12,3,15]
|
||||||
|
Stage05 0x008FF8 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage05 0x009734 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage05 0x00A228 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage05 0x00B1A0 28 150 TCN001 0x13 type2 [64,9,115] [28,28,64]
|
||||||
|
Stage05 0x00B1E0 28 150 TCN002 0x16 type2 [9,115,28] [28,64,64]
|
||||||
|
Stage05 0x00B220 28 150 TCN003 0x19 type2 [115,28,28] [64,64,11]
|
||||||
|
Stage06 0x000924 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
|
||||||
|
Stage06 0x001460 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage06 0x00330C 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage06 0x003810 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage06 0x003E94 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage06 0x0040D4 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||||
|
Stage06 0x009B50 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
|
||||||
|
Stage06 0x00BEC4 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage06 0x00C168 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage06 0x00C66C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage06 0x00CCF0 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage06 0x00D274 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage06 0x0119AC 29 0 TCN001 0x26 type2 [118,12,15] [12,15,29]
|
||||||
|
Stage06 0x011A80 29 0 TCN002 0x29 type2 [29,12,15] [28,12,15]
|
||||||
|
Stage06 0x011AC0 28 50 TCN002 0x29 type2 [12,15,29] [12,15,12]
|
||||||
|
Stage06 0x011EFC 26 50 TCN306 0x3D type2 [12,15,3] [12,15,3]
|
||||||
|
Stage06 0x013A7C 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage06 0x013B60 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage06 0x01405C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage06 0x014724 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage06 0x014F64 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage06 0x01845C 26 0 TCN350 0x3A type2 [64,11,64] [1,1,1]
|
||||||
|
Stage06 0x01A7B0 26 80 TCN002 0x29 type2 [69,11,9] [64,64,1]
|
||||||
|
Stage06 0x01AAA0 26 50 TCN002 0x29 type2 [69,11,9] [11,64,1]
|
||||||
|
Stage06 0x01ACEC 26 30 TCN002 0x29 type2 [69,11,9] [11,64,11]
|
||||||
|
Stage07 0x000D3C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
|
||||||
|
Stage07 0x0026A4 29 100 TCN001 0x1C type2 [8,127,123] [35,119,11]
|
||||||
|
Stage07 0x002788 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage07 0x002BD0 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage07 0x00324C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage07 0x0034D4 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage07 0x003714 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||||
|
Stage07 0x00AB0C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
|
||||||
|
Stage07 0x00CCD4 29 100 TCN001 0x1C type2 [116,8,123] [35,119,11]
|
||||||
|
Stage07 0x00CDB8 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage07 0x00D200 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage07 0x00D87C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage07 0x00E4A8 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||||
|
Stage07 0x00ED78 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage08 0x001F44 29 0 TCN001 0x1E type2 [15,12,15] [15,15,15]
|
||||||
|
Stage08 0x0021C4 28 0.1 TCN104 0x30 type2 [92,15,30] [15,28,15]
|
||||||
|
Stage08 0x00225C 28 0.1 TCN105 0x35 type2 [30,28,15] [15,28,15]
|
||||||
|
Stage08 0x0022F4 28 0.1 TCN106 0x36 type2 [15,28,15] [15,30,28]
|
||||||
|
Stage08 0x0023F8 28 0.1 TCN012 0x29 type2 [28,15,30] [15,28,15]
|
||||||
|
Stage08 0x002490 28 0.1 TCN113 0x31 type2 [30,28,15] [15,92,30]
|
||||||
|
Stage08 0x003350 26 0 ADN126 0xF type2 [4,14,77] [11,26,11]
|
||||||
|
Stage08 0x0033A0 26 0 ADN126b 0x3D type2 [77,26,11] [11,26,11]
|
||||||
|
Stage08 0x0033F0 26 0 ADN126c 0x3F type2 [11,26,11] [11,116,8]
|
||||||
|
Stage08 0x0034E8 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
|
||||||
|
Stage08 0x0036D0 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage08 0x004294 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||||
|
Stage08 0x006C54 28 1000 ADS151a 0x45 type2 [15,47,30] [29,58,15]
|
||||||
|
Stage08 0x006C94 29 40 ADS151a 0x45 type2 [47,30,28] [58,15,47]
|
||||||
|
Stage08 0x006E28 28 1000 ADS151b 0x4B type2 [15,47,30] [29,64,8]
|
||||||
|
Stage08 0x006E68 29 40 ADS151b 0x4B type2 [47,30,28] [64,8,1]
|
||||||
|
Stage08 0x00BD3C 28 150 TCN001 0x1E type2 [105,9,115] [28,28,64]
|
||||||
|
Stage08 0x00BD7C 28 150 TCN002 0x21 type2 [9,115,28] [28,64,11]
|
||||||
|
Stage08 0x00BDBC 28 150 TCN003 0x24 type2 [115,28,28] [64,11,20]
|
||||||
|
Stage08 0x00D918 29 0 TCN001 0x1E type2 [12,12,12] [15,15,15]
|
||||||
|
Stage08 0x00E190 29 0 ADT201 0x11 type2 [15,92,15] [117,15,92]
|
||||||
|
Stage08 0x00EE0C 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
|
||||||
|
Stage08 0x00EFF4 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage08 0x00FD4C 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage09 0x000974 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage09 0x002958 29 100 TCN001 0x28 type2 [127,123,8] [35,11,58]
|
||||||
|
Stage09 0x003610 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage09 0x003A20 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage09 0x003C68 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage09 0x00426C 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage09 0x004A44 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage09 0x00851C 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage09 0x009E64 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
|
||||||
|
Stage09 0x00A1C0 28 1000 ADS251a 0x44 type2 [12,15,30] [29,12,15]
|
||||||
|
Stage09 0x00A200 29 40 ADS251a 0x44 type2 [15,30,28] [12,15,30]
|
||||||
|
Stage09 0x00A340 28 1000 ADS251b 0x46 type2 [12,15,30] [29,47,47]
|
||||||
|
Stage09 0x00A380 29 40 ADS251b 0x46 type2 [15,30,28] [47,47,1]
|
||||||
|
Stage09 0x00A6D0 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage09 0x00AAE0 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage09 0x00ACE8 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage09 0x00B2D0 101 - ? None typeNone [7,9,116] [100,124,93]
|
||||||
|
Stage09 0x00C38C 28 150 TCN001 0x28 type2 [115,9,115] [28,11,64]
|
||||||
|
Stage09 0x00C3CC 28 150 TCN002 0x2B type2 [9,115,28] [11,64,64]
|
||||||
|
Stage09 0x00FDC4 29 0 TCN001 0x28 type2 [114,114,12] [12,15,12]
|
||||||
|
Stage09 0x010684 28 500 ADT301 0x14 type2 [15,12,15] [12,15,12]
|
||||||
|
Stage09 0x011790 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
|
||||||
|
Stage09 0x011E84 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage09 0x012294 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage09 0x012B60 101 - ? None typeNone [69,69,116] [100,124,93]
|
||||||
|
Stage09 0x0131AC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage09 0x017BE4 28 500 ADT302 0x18 type2 [3,15,47] [64,1,1]
|
||||||
|
Stage10 0x0006DC 29 0 TCN001 0xE type2 [118,75,12] [12,3,15]
|
||||||
|
Stage10 0x000910 28 2000 ADT001 0x2 type2 [15,30,47] [12,15,47]
|
||||||
|
Stage10 0x000A38 28 500 ADS002 0x1 type2 [12,15,47] [12,15,47]
|
||||||
|
Stage10 0x000B60 28 500 ADS003 0x5 type2 [12,15,47] [1,1,1]
|
||||||
|
Stage10 0x000F38 29 100 TCN001 0xE type2 [8,127,123] [35,119,11]
|
||||||
|
Stage10 0x00101C 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage10 0x0013A8 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage10 0x001620 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage10 0x002394 28 500 ADS004 0x7 type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x002BA0 28 500 ADS005 0x9 type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x0033AC 28 500 ADS006 0xA type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x003BB8 28 500 ADS007 0xB type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x0043C4 28 500 ADS008 0xC type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x004BD0 28 500 ADS009 0xD type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x0053DC 28 500 ADS010 0x0 type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x005BE8 28 500 ADS011 0x4 type2 [64,128,47] [1,11,20]
|
||||||
|
Stage10 0x006554 28 500 ADS013 0x8 type2 [64,128,47] [1,11,20]
|
||||||
|
Stage11 0x000744 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage11 0x001610 29 100 TCN001 0x18 type2 [8,127,123] [35,11,58]
|
||||||
|
Stage11 0x0017D8 28 1000 ADS151a 0x27 type2 [15,47,30] [29,58,15]
|
||||||
|
Stage11 0x001818 29 40 ADS151a 0x27 type2 [47,30,28] [58,15,47]
|
||||||
|
Stage11 0x0019AC 28 1000 ADS151b 0x29 type2 [15,47,30] [29,1,1]
|
||||||
|
Stage11 0x0019EC 29 40 ADS151b 0x29 type2 [47,30,28] [1,1,1]
|
||||||
|
Stage11 0x001B0C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage11 0x001F54 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage11 0x0025D8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage11 0x002818 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage11 0x00B070 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
|
||||||
|
Stage11 0x00BFB8 29 100 TCN001 0x18 type2 [8,127,123] [35,1,1]
|
||||||
|
Stage11 0x00C0D8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage11 0x00C520 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage11 0x00CBA4 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage11 0x00CDE4 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage12 0x0012D0 29 0 TCN001 0x20 type2 [12,12,12] [75,8,11]
|
||||||
|
Stage12 0x001564 29 100 TCN001 0x20 type2 [8,127,123] [35,119,1]
|
||||||
|
Stage12 0x0033CC 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage12 0x00403C 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||||
|
Stage13 0x0012C4 29 0 TCN001 0x16 type2 [12,12,12] [15,15,15]
|
||||||
|
Stage13 0x001D74 26 50 TCN013 0x25 type2 [3,15,1] [3,15,1]
|
||||||
|
Stage13 0x0027DC 28 150 ADN004 0x8 type2 [47,92,3] [15,3,28]
|
||||||
|
Stage13 0x002914 28 150 ADN005 0xB type2 [28,15,3] [15,47,28]
|
||||||
|
Stage13 0x002A00 28 150 ADN012 0x6 type2 [28,15,47] [15,47,47]
|
||||||
|
Stage13 0x0031BC 29 100 TCN001 0x16 type2 [116,8,123] [35,119,11]
|
||||||
|
Stage13 0x003348 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage13 0x004EC4 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||||
|
Stage13 0x0098A0 28 150 ADN002 0x3 type2 [20,9,57] [15,47,47]
|
||||||
|
Stage13 0x00A68C 28 500 ADS051a 0x34 type2 [64,11,58] [15,47,92]
|
||||||
|
Stage13 0x00A894 28 500 ADS051b 0x36 type2 [92,30,58] [15,47,92]
|
||||||
|
Stage13 0x00AA9C 28 500 ADS051c 0x37 type2 [92,30,58] [15,47,92]
|
||||||
|
Stage13 0x00BA00 28 150 ADN015 0xE type2 [64,11,57] [15,92,64]
|
||||||
|
Stage13 0x00BE08 28 150 ADN017 0x12 type2 [64,11,57] [15,47,92]
|
||||||
|
Stage14 0x001230 29 0 TCN001 0x18 type2 [12,12,12] [15,15,92]
|
||||||
|
Stage14 0x001394 26 80 TCN003 0x24 type2 [15,15,92] [25,15,30]
|
||||||
|
Stage14 0x001548 28 500 ADT102 0x3 type2 [15,30,15] [30,47,92]
|
||||||
|
Stage14 0x001714 28 500 ADT103 0x6 type2 [47,92,15] [30,47,92]
|
||||||
|
Stage14 0x0018E0 28 500 ADT104 0x8 type2 [47,92,15] [30,47,92]
|
||||||
|
Stage14 0x00242C 29 100 TCN001 0x18 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage14 0x002510 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage14 0x002960 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage14 0x002D84 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage14 0x003784 28 500 ADT105 0xA type2 [64,58,15] [30,47,47]
|
||||||
|
Stage14 0x003E90 28 500 ADT108 0x10 type2 [64,58,15] [30,47,1]
|
||||||
|
Stage14 0x004548 28 500 ADT114 0xB type2 [64,58,15] [30,47,47]
|
||||||
|
Stage14 0x007334 29 0 TCN201 0x29 type2 [12,12,12] [15,30,47]
|
||||||
|
Stage14 0x00748C 28 500 ADT201 0x5 type2 [15,30,47] [15,15,15]
|
||||||
|
Stage14 0x007F18 29 100 TCN201 0x29 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage14 0x007FFC 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage14 0x0083C4 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage14 0x008644 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||||
|
Stage15 0x0014F8 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
|
||||||
|
Stage15 0x002390 29 0 TCN008 0x59 type2 [15,47,30] [1,15,48]
|
||||||
|
Stage15 0x002C68 28 150 ADN101 0x1 type2 [15,48,92] [15,28,15]
|
||||||
|
Stage15 0x002D00 28 150 ADN102 0x2 type2 [92,28,15] [15,3,1]
|
||||||
|
Stage15 0x002FC8 28 150 ADN103 0x5 type2 [15,48,92] [15,3,15]
|
||||||
|
Stage15 0x0032C0 28 150 ADN104 0xB type2 [48,47,92] [15,3,15]
|
||||||
|
Stage15 0x003564 28 150 ADN106 0x18 type2 [15,48,92] [15,28,15]
|
||||||
|
Stage15 0x0035FC 28 150 ADN109 0x2C type2 [92,28,15] [15,15,47]
|
||||||
|
Stage15 0x003F50 29 100 TCN001 0x3A type2 [116,8,123] [35,119,11]
|
||||||
|
Stage15 0x0040DC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage15 0x00563C 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||||
|
Stage15 0x00DE90 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
|
||||||
|
Stage15 0x00E970 29 0 TCN008 0x59 type2 [47,92,30] [15,48,92]
|
||||||
|
Stage15 0x00F198 28 150 ADT201 0x28 type2 [47,92,30] [15,1,30]
|
||||||
|
Stage15 0x00F2C0 28 150 ADT202 0x30 type2 [15,1,30] [15,1,28]
|
||||||
|
Stage15 0x00F37C 28 150 ADN203 0xF type2 [28,15,1] [15,28,15]
|
||||||
|
Stage15 0x00F414 28 150 ADN204 0x15 type2 [1,28,15] [15,28,15]
|
||||||
|
Stage15 0x00F4AC 28 150 ADN205 0x1C type2 [15,28,15] [15,28,15]
|
||||||
|
Stage15 0x00F544 28 150 ADN206 0x21 type2 [15,28,15] [15,28,15]
|
||||||
|
Stage15 0x00F5DC 28 150 ADN207 0x27 type2 [15,28,15] [15,28,15]
|
||||||
|
Stage15 0x00F674 28 150 ADN208 0x2F type2 [15,28,15] [15,28,15]
|
||||||
|
Stage15 0x00F70C 28 150 ADN209 0x36 type2 [15,28,15] [15,15,28]
|
||||||
|
Stage15 0x00F7FC 28 500 ADN210 0x4 type2 [28,15,15] [47,92,1]
|
||||||
|
Stage15 0x00F980 28 500 ADN211 0x9 type2 [92,1,15] [47,92,1]
|
||||||
|
Stage15 0x00FB04 28 500 ADN212 0x10 type2 [92,1,15] [47,47,92]
|
||||||
|
Stage15 0x00FCDC 28 500 ADN213 0x16 type2 [92,1,15] [47,47,92]
|
||||||
|
Stage15 0x00FEB4 28 500 ADN214 0x1D type2 [92,1,15] [47,47,92]
|
||||||
|
Stage15 0x0102DC 29 100 TCN001 0x3A type2 [8,127,123] [35,119,11]
|
||||||
|
Stage15 0x0104C4 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||||
|
Stage15 0x0110CC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||||
|
Stage15 0x013368 28 500 ADN215 0x22 type2 [20,58,15] [47,92,1]
|
||||||
|
Stage15 0x01358C 28 500 ADN216 0x2A type2 [20,58,15] [47,92,11]
|
||||||
|
Stage15 0x01378C 28 500 ADN217 0x32 type2 [20,58,15] [47,47,92]
|
||||||
|
Stage15 0x0139E0 28 500 ADN218 0x37 type2 [20,58,15] [47,47,92]
|
||||||
|
Stage15 0x013C34 28 500 ADN219 0x39 type2 [20,58,15] [47,47,92]
|
||||||
|
Stage15 0x013E88 28 500 ADN220 0xA type2 [20,58,15] [47,92,11]
|
||||||
|
Stage15 0x014088 28 500 ADN221 0x11 type2 [20,58,15] [47,92,11]
|
||||||
|
Stage15 0x014924 28 150 ADN222 0x17 type2 [64,11,57] [15,64,64]
|
||||||
|
Stage15 0x014D28 28 500 ADN223 0x1E type2 [11,58,15] [47,47,92]
|
||||||
|
Stage15 0x015170 28 150 ADN224 0x23 type2 [64,11,57] [15,64,64]
|
||||||
|
Stage15 0x01553C 28 500 ADN225 0x2B type2 [11,58,15] [47,92,64]
|
||||||
|
Stage15 0x0158E8 28 500 ADN226 0x33 type2 [11,58,15] [47,92,58]
|
||||||
|
Stage15 0x015A84 28 500 ADN227 0x38 type2 [92,58,15] [47,92,64]
|
||||||
|
Stage16 0x0004F4 29 100 TCN001 0x1 type2 [8,127,123] [35,119,11]
|
||||||
|
Stage16 0x0005D8 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||||
|
Stage16 0x000A28 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage16 0x000E4C 101 - ? None typeNone [140,140,116] [100,93,124]
|
||||||
|
Stage18 0x000488 28 0 TCN001 0xB type2 [35,8,12] [52,1,11]
|
||||||
|
Stage18 0x0009D4 26 0 ADT101 0x0 type2 [1,11,18] [13,0,97]
|
||||||
|
Stage18 0x000D2C 26 0 ADT102 0x2 type2 [1,11,18] [13,0,11]
|
||||||
|
Stage18 0x000E30 26 0 ADT103 0x4 type2 [0,11,18] [13,0,11]
|
||||||
|
Stage18 0x000F34 26 0 ADT104 0x5 type2 [0,11,18] [13,0,11]
|
||||||
|
Stage18 0x0013E4 26 0 ADT105 0x6 type2 [1,11,18] [13,0,11]
|
||||||
|
Stage18 0x0014E8 26 0 ADT106 0x7 type2 [0,11,18] [13,0,11]
|
||||||
|
Stage18 0x0015EC 26 0 ADT107 0x8 type2 [0,11,18] [13,0,11]
|
||||||
|
Stage18 0x001AB0 26 0 ADT108 0x9 type2 [1,11,18] [13,0,97]
|
||||||
|
Stage18 0x001E2C 28 100 TCN001 0xB type2 [4,1,11] [12,3,15]
|
||||||
|
Stage18 0x0029FC 26 100 TCN001 0xB type2 [6,11,20] []
|
||||||
|
Stage19 0x0003D4 28 0 TCN001 0x7 type2 [35,8,12] [52,1,11]
|
||||||
|
Stage19 0x000CD4 28 100 TCN001 0x7 type2 [11,104,4] [1,11,69]
|
||||||
|
Stage19 0x000F0C 26 0 ADS101 0x0 type2 [16,0,12] [26,84,4]
|
||||||
|
Stage19 0x000F4C 26 0 ADS102 0x1 type2 [0,12,26] [84,4,97]
|
||||||
|
Stage19 0x001010 28 0 TCN001 0x7 type2 [4,97,4] [12,3,15]
|
||||||
|
Stage19 0x0012F8 26 0 ADS104 0x2 type2 [4,16,0] [84,4,97]
|
||||||
|
Stage19 0x001394 28 100 TCN001 0x7 type2 [84,4,97] [12,3,15]
|
||||||
|
Stage19 0x0019F4 26 100 TCN001 0x7 type2 [6,11,20] []
|
||||||
|
Stage20 0x0026C0 26 100 TCN001 0xB type2 [6,11,20] []
|
||||||
|
Stage21 0x0002A8 28 0 TCN001 0x2 type2 [35,8,12] [26,52,1]
|
||||||
|
Stage21 0x0002E8 26 40 TCN001 0x2 type2 [8,12,28] [52,1,12]
|
||||||
|
Stage21 0x000B58 28 100 TCN001 0x2 type2 [0,84,4] [97,4,12]
|
||||||
|
Stage21 0x0012F4 26 100 TCN001 0x2 type2 [6,11,20] []
|
||||||
|
Stage22 0x00098C 26 0 ADT101 0x0 type2 [1,11,18] [0,11,69]
|
||||||
|
Stage22 0x000AD0 26 0 ADT102 0x2 type2 [69,11,18] [0,11,69]
|
||||||
|
Stage22 0x001384 26 0 ADS103 0x1 type2 [11,69,0] [97,1,11]
|
||||||
|
Stage22 0x001470 26 0 ADT104 0x3 type2 [1,11,18] [0,11,69]
|
||||||
|
Stage22 0x001868 26 0 ADT106 0x5 type2 [1,11,18] [0,11,69]
|
||||||
|
Stage22 0x001F1C 26 0 ADT107 0x6 type2 [1,11,18] [0,11,69]
|
||||||
|
Stage22 0x002130 26 100 TCN001 0x7 type2 [6,11,20] []
|
||||||
|
Stage23 0x0002A8 28 0 TCN001 0x3 type2 [35,8,12] [52,1,11]
|
||||||
|
Stage23 0x000898 26 0 ADS105 0x2 type2 [4,16,0] [84,4,97]
|
||||||
|
Stage23 0x000B98 28 100 TCN001 0x3 type2 [11,104,4] [1,11,69]
|
||||||
|
Stage23 0x0012AC 26 100 TCN001 0x3 type2 [6,11,20] []
|
||||||
|
Stage24 0x001274 29 0 ADT108 0x1E type2 [12,30,117] [12,12,12]
|
||||||
|
Stage24 0x001EE8 29 100 TCN001 0x2C type2 [127,123,8] [35,119,1]
|
||||||
|
Stage24 0x0029B0 29 100 ADT108 0x1E type2 [15,47,64] [20,64,11]
|
||||||
|
Stage24 0x0045F8 29 100 ADT109 0x22 type2 [15,48,47] [64,1,7]
|
||||||
|
Stage24 0x004A44 29 0 ADT109 0x22 type2 [58,30,117] [64,7,58]
|
||||||
|
Stage24 0x0051B8 29 100 ADT103 0x8 type2 [30,15,47] [64,64,1]
|
||||||
|
Stage24 0x0056B8 29 0 ADT103 0x8 type2 [58,30,117] [64,20,4]
|
||||||
|
Stage24 0x007094 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage24 0x007304 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage24 0x007710 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage24 0x007DBC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage25 0x001790 28 100 ADN355 0x21 type2 [15,12,15] [12,15,28]
|
||||||
|
Stage25 0x001864 28 100 ADN350 0xF type2 [28,12,15] [12,15,28]
|
||||||
|
Stage25 0x001938 28 100 ADN351 0x13 type2 [28,12,15] [12,15,28]
|
||||||
|
Stage25 0x001A0C 28 100 ADN352 0x17 type2 [28,12,15] [12,15,28]
|
||||||
|
Stage25 0x001AE0 28 100 ADN353 0x1B type2 [28,12,15] [12,15,28]
|
||||||
|
Stage25 0x001BB4 28 100 ADN354 0x1E type2 [28,12,15] [12,15,28]
|
||||||
|
Stage25 0x001C88 28 200 ADN310 0x1 type2 [28,12,15] [12,15,117]
|
||||||
|
Stage25 0x004218 29 100 TCN001 0x27 type2 [127,123,8] [35,119,1]
|
||||||
|
Stage25 0x00440C 28 300 ADN312 0x5 type2 [11,57,16] [1,11,57]
|
||||||
|
Stage25 0x0044F8 28 300 ADN313 0x9 type2 [11,57,16] [1,11,58]
|
||||||
|
Stage25 0x004600 28 600 ADN314 0xD type2 [11,58,15] [47,47,1]
|
||||||
|
Stage25 0x004964 28 600 ADN315 0x11 type2 [11,58,15] [47,47,1]
|
||||||
|
Stage25 0x005348 28 100 ADN350 0xF type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x005814 28 200 ADN356 0x23 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x005BAC 28 200 ADN356 0x23 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00709C 28 100 ADN351 0x13 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x0074C8 28 200 ADN357 0x24 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x007860 28 200 ADN357 0x24 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x0088E8 28 100 ADN352 0x17 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x008D14 28 200 ADN358 0x25 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x0090AC 28 200 ADN358 0x25 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00A59C 28 100 ADN353 0x1B type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00A9C8 28 200 ADN359 0x26 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x00AD60 28 200 ADN359 0x26 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00BC70 28 100 ADN354 0x1E type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00C09C 28 200 ADN360 0x14 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x00C434 28 200 ADN360 0x14 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00D924 28 100 ADN355 0x21 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00DD50 28 200 ADN361 0x18 type2 [5,57,15] [145,47,47]
|
||||||
|
Stage25 0x00E0E8 28 200 ADN361 0x18 type2 [143,57,15] [145,47,47]
|
||||||
|
Stage25 0x00ECD4 28 200 ADN310 0x1 type2 [143,57,15] [19,64,20]
|
||||||
|
Stage25 0x00F390 28 300 ADN311 0x2 type2 [5,57,15] [19,1,64]
|
||||||
|
Stage25 0x00F704 28 300 ADN311 0x2 type2 [143,57,15] [19,64,8]
|
||||||
|
Stage25 0x010EFC 28 300 ADN312 0x5 type2 [9,143,57] [16,64,69]
|
||||||
|
Stage25 0x0111A8 26 0 ADN320 0x3 type2 [69,20,18] [69,8,69]
|
||||||
|
Stage25 0x011450 28 300 ADN320 0x3 type2 [9,128,15] [47,47,1]
|
||||||
|
Stage25 0x011C0C 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
|
||||||
|
Stage25 0x011FFC 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
|
||||||
|
Stage25 0x012240 26 0 ADN321 0x7 type2 [69,20,18] [69,8,69]
|
||||||
|
Stage25 0x0124E8 28 300 ADN321 0x7 type2 [9,128,15] [47,47,1]
|
||||||
|
Stage25 0x012CA4 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
|
||||||
|
Stage25 0x013094 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
|
||||||
|
Stage25 0x0134C0 28 300 ADN328 0x20 type2 [9,128,15] [47,1,69]
|
||||||
|
Stage25 0x013BE8 28 300 ADN328 0x20 type2 [143,128,15] [47,1,69]
|
||||||
|
Stage25 0x0142CC 28 300 ADN313 0x9 type2 [9,143,57] [16,64,69]
|
||||||
|
Stage25 0x0146F4 28 300 ADN325 0x16 type2 [9,128,15] [47,1,69]
|
||||||
|
Stage25 0x014E1C 28 300 ADN325 0x16 type2 [143,128,15] [47,1,18]
|
||||||
|
Stage25 0x0151F4 28 300 ADN329 0x22 type2 [9,128,15] [47,1,69]
|
||||||
|
Stage25 0x01591C 28 300 ADN329 0x22 type2 [143,128,15] [47,1,69]
|
||||||
|
Stage25 0x016058 28 600 ADN314 0xD type2 [143,57,15] [47,47,145]
|
||||||
|
Stage25 0x016B84 28 600 ADN315 0x11 type2 [143,57,15] [47,47,145]
|
||||||
|
Stage25 0x017094 101 - ? None typeNone [64,95,116] [100,124,93]
|
||||||
|
Stage25 0x01748C 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage25 0x017AE8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage26 0x000EF0 29 0 TCN001 0x16 type2 [118,146,12] [15,12,15]
|
||||||
|
Stage26 0x001614 29 50 TCT012 0x28 type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x0017F4 29 50 TCT013 0x2A type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x0019D4 29 50 TCT014 0x2C type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x001BB4 29 50 TCT015 0x2E type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x001D94 29 50 TCT016 0x30 type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x001F74 29 50 TCT017 0x32 type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x002154 29 50 TCT018 0x34 type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x002334 29 50 TCT019 0x36 type2 [15,3,30] [12,15,3]
|
||||||
|
Stage26 0x003928 29 100 TCN001 0x16 type2 [127,123,8] [35,11,95]
|
||||||
|
Stage26 0x003A00 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage26 0x003E10 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage26 0x004414 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||||
|
Stage26 0x004664 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage26 0x008DD0 29 20 ADN128 0x14 type2 [47,11,57] [15,3,46]
|
||||||
|
Stage26 0x00AD0C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00AD4C 26 0 TCT012 0x28 type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00AF4C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00AF8C 26 0 TCT013 0x2A type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00B18C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00B1CC 26 0 TCT014 0x2C type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00B3CC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00B40C 26 0 TCT015 0x2E type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00B60C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00B64C 26 0 TCT016 0x30 type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00B84C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00B88C 26 0 TCT017 0x32 type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00BA8C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00BACC 26 0 TCT018 0x34 type2 [5,5,26] [4,64,11]
|
||||||
|
Stage26 0x00BCCC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||||
|
Stage26 0x00BD0C 26 0 TCT019 0x36 type2 [5,5,26] [4,64,11]
|
||||||
|
Stage27 0x000AAC 29 0 TCN001 0x12 type2 [118,146,12] [15,12,15]
|
||||||
|
Stage27 0x002088 29 100 TCN001 0x12 type2 [127,123,8] [35,11,95]
|
||||||
|
Stage27 0x002160 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage27 0x002570 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage27 0x002BBC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage27 0x00339C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage28 0x00101C 29 0 TCN001 0x13 type2 [118,146,12] [15,12,30]
|
||||||
|
Stage28 0x0020EC 29 100 TCN001 0x13 type2 [127,123,8] [35,11,95]
|
||||||
|
Stage28 0x0021C4 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||||
|
Stage28 0x00256C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||||
|
Stage28 0x002958 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||||
|
Stage28 0x002E4C 29 0 ADT102 0x2 type2 [12,12,12] [29,29,136]
|
||||||
|
Stage28 0x002E8C 29 0 ADT103 0x4 type2 [12,12,29] [29,136,13]
|
||||||
|
Stage28 0x002ECC 29 0 ADT104 0x6 type2 [12,29,29] [136,13,1]
|
||||||
|
Stage28 0x003BE8 29 0 ADT107 0xB type2 [12,12,12] [29,29,136]
|
||||||
|
Stage28 0x003C28 29 0 ADT108 0xC type2 [12,12,29] [29,136,13]
|
||||||
|
Stage28 0x003C68 29 0 ADT109 0xD type2 [12,29,29] [136,13,64]
|
||||||
|
Stage28 0x004848 29 0 ADT113 0x7 type2 [12,12,12] [29,29,136]
|
||||||
|
Stage28 0x004888 29 0 ADT114 0x8 type2 [12,12,29] [29,136,13]
|
||||||
|
Stage28 0x0048C8 29 0 ADT115 0xA type2 [12,29,29] [136,13,1]
|
||||||
|
Stage28 0x0050F8 26 0 ADT102 0x2 type2 [11,70,18] [13,70,136]
|
||||||
|
Stage28 0x005330 26 0 ADT103 0x4 type2 [11,70,18] [13,70,11]
|
||||||
|
Stage28 0x005520 26 0 ADT104 0x6 type2 [11,70,18] [13,11,70]
|
||||||
|
Stage28 0x005638 26 0 ADT107 0xB type2 [11,70,18] [13,70,136]
|
||||||
|
Stage28 0x005870 26 0 ADT108 0xC type2 [11,70,18] [13,70,11]
|
||||||
|
Stage28 0x005A60 26 0 ADT109 0xD type2 [11,70,18] [13,11,70]
|
||||||
|
Stage28 0x005B78 26 0 ADT113 0x7 type2 [11,70,18] [13,70,136]
|
||||||
|
Stage28 0x005DB0 26 0 ADT114 0x8 type2 [11,70,18] [13,70,11]
|
||||||
|
Stage28 0x005FA0 26 0 ADT115 0xA type2 [11,70,18] [13,11,64]
|
||||||
|
Stage29 0x0083A0 29 100 TCN001 0x11 type2 [12,12,12] [15,29,15]
|
||||||
|
Stage29 0x008438 29 100 TCN002 0x14 type2 [12,29,15] [15,29,15]
|
||||||
|
Stage29 0x0084D0 29 50 TCN003 0x18 type2 [15,29,15] [15,92,15]
|
||||||
|
Stage29 0x008740 29 75 TCN004 0x1F type2 [15,30,3] [15,3,29]
|
||||||
|
Stage29 0x008878 29 50 TCN005 0x21 type2 [29,15,3] [15,30,19]
|
||||||
|
Stage29 0x008AA4 29 75 TCT006 0x2C type2 [30,19,3] [15,3,3]
|
||||||
|
Stage29 0x008C7C 29 50 TCN007 0x25 type2 [15,3,3] [15,3,3]
|
||||||
|
Stage29 0x008E54 29 50 TCN110 0x17 type2 [15,3,3] [15,3,3]
|
||||||
|
Stage29 0x00902C 29 50 TCN111 0x1D type2 [15,3,3] [15,3,3]
|
||||||
|
Stage29 0x009204 29 50 TCN113 0x22 type2 [15,3,3] [15,30,29]
|
||||||
|
Stage29 0x009308 29 0 TCN008 0x26 type2 [29,15,30] [92,15,48]
|
||||||
|
Stage29 0x0094B4 29 50 TCN009 0x29 type2 [92,15,48] [15,92,29]
|
||||||
|
Stage29 0x0095C0 29 50 TCN114 0x24 type2 [29,15,92] [1,15,92]
|
||||||
|
Stage29 0x0096F0 29 50 TCN116 0x28 type2 [1,15,92] [1,15,48]
|
||||||
|
Stage29 0x00984C 29 50 TCN119 0x2B type2 [1,15,48] [1,1,75]
|
||||||
|
Stage29 0x00CCF0 29 100 TCN001 0x11 type2 [127,123,8] [35,119,11]
|
||||||
|
Stage29 0x010580 101 - ? None typeNone [69,95,116] [100,93,124]
|
||||||
|
Stage29 0x0109A8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage29 0x011004 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||||
|
Stage29 0x01128C 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||||
|
Stage29 0x01158C 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||||
|
Stage29 0x011D74 29 50 TCN120 0x1E type2 [128,15,92] [11,20,11]
|
||||||
|
Stage29 0x011FD0 29 50 TCN121 0x20 type2 [128,15,92] [11,69,57]
|
||||||
|
Stage29 0x012D0C 28 200 ADN101ea1 0x0 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x013294 28 200 ADN101ea2 0x1 type2 [1,128,15] [29,145,47]
|
||||||
|
Stage29 0x0132D4 29 50 ADN101ea2 0x1 type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x01381C 28 200 ADN102ea1 0x2 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x013DA4 28 200 ADN102ea2 0x3 type2 [1,128,15] [29,145,47]
|
||||||
|
Stage29 0x013DE4 29 50 ADN102ea2 0x3 type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x01432C 28 200 ADN103ea1 0x4 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x0148B4 28 200 ADN103ea2 0x6 type2 [1,128,15] [29,145,47]
|
||||||
|
Stage29 0x0148F4 29 50 ADN103ea2 0x6 type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x014E3C 28 200 ADN104ea1 0x7 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x0153C4 28 200 ADN104ea2 0xA type2 [1,128,15] [29,145,47]
|
||||||
|
Stage29 0x015404 29 50 ADN104ea2 0xA type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x01594C 28 200 ADN121ea1 0x5 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x015EB0 28 200 ADN121ea2 0x8 type2 [92,128,15] [29,145,47]
|
||||||
|
Stage29 0x015EF0 29 50 ADN121ea2 0x8 type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x016338 28 200 ADN122ea1 0x9 type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x01689C 28 200 ADN122ea2 0xB type2 [92,128,15] [29,145,47]
|
||||||
|
Stage29 0x0168DC 29 50 ADN122ea2 0xB type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x016D24 28 200 ADN123ea1 0xC type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x017288 28 200 ADN123ea2 0xD type2 [92,128,15] [29,145,47]
|
||||||
|
Stage29 0x0172C8 29 50 ADN123ea2 0xD type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x017710 28 200 ADN124ea1 0xE type2 [145,128,15] [145,47,47]
|
||||||
|
Stage29 0x017C74 28 200 ADN124ea2 0xF type2 [92,128,15] [29,145,47]
|
||||||
|
Stage29 0x017CB4 29 50 ADN124ea2 0xF type2 [128,15,28] [145,47,47]
|
||||||
|
Stage29 0x01C5FC 28 200 ADN101b 0x2F type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x01C71C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01C83C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01C95C 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01CA7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01CB9C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01CCBC 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01CDDC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01CEFC 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D01C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D13C 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D25C 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D37C 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D49C 28 120 ADN107a 0x56 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D5C4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D6E4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D804 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01D924 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DA44 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DB64 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DC84 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DDA4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DEC4 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01DFE4 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E104 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E224 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E344 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E464 28 200 ADN104b 0x42 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E58C 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E6AC 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E7CC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01E8EC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01EA0C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01EB2C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01EC4C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01ED6C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01EE8C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01EFAC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F0CC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F1EC 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F30C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F42C 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
|
||||||
|
Stage29 0x01F52C 28 200 ADN101c 0x32 type2 [1,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F64C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F76C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F88C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01F9AC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01FACC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01FBEC 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01FD0C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01FE2C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x01FF4C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02006C 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02018C 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0202AC 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0203CC 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0204F4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020614 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020734 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020854 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020974 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020A94 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020BB4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020CD4 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020DF4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x020F14 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021034 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021154 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021274 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021394 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0214BC 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0215DC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0216FC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02181C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02193C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021A5C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021B7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021C9C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021DBC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021EDC 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x021FFC 28 200 ADN101c 0x32 type2 [4,57,15] [1,57,15]
|
||||||
|
Stage29 0x0220F4 28 200 ADN102b 0x33 type2 [1,57,15] [1,4,57]
|
||||||
|
Stage29 0x022214 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022334 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
|
||||||
|
Stage29 0x022434 28 300 ADN101e 0x40 type2 [1,57,15] [1,4,57]
|
||||||
|
Stage29 0x022554 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022674 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022794 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0228B4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0229D4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022AF4 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022C14 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022D34 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022E54 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x022F74 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x023094 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0231B4 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0232D4 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,11]
|
||||||
|
Stage29 0x023764 28 200 ADN141a 0x3E type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x023EFC 28 200 ADN141b 0x48 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x023F3C 29 50 ADN141b 0x48 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x024420 28 200 ADN142a 0x49 type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x024BB8 28 200 ADN142b 0x52 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x024BF8 29 50 ADN142b 0x52 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x02517C 28 200 ADN143a 0x53 type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x025914 28 200 ADN143b 0x5A type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x025954 29 50 ADN143b 0x5A type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x025F78 28 200 ADN144a 0x5B type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x026710 28 200 ADN144b 0x61 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x026750 29 50 ADN144b 0x61 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x026CD4 28 200 ADN145a 0x62 type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x02746C 28 200 ADN145b 0x65 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x0274AC 29 50 ADN145b 0x65 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x02783C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x02795C 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x027A7C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x027B9C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x027CBC 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x027DDC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x027EFC 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02801C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02813C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02825C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02837C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02849C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0285BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0286DC 28 120 ADN127a 0x60 type2 [4,57,15] [1,4,64]
|
||||||
|
Stage29 0x0288A4 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x0289C4 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x028AE4 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x028C04 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x028D24 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x028E44 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x028F64 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029084 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0291A4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0292C4 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x0293E4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029504 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029624 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029744 28 200 ADN124b 0x50 type2 [4,57,15] [1,4,64]
|
||||||
|
Stage29 0x0299AC 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x029ACC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029BEC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029D0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029E2C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x029F4C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A06C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A18C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A2AC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A3CC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A4EC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A60C 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A72C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02A84C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
|
||||||
|
Stage29 0x02AA8C 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x02ABAC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02ACCC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02ADEC 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02AF0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B02C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B14C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B26C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B38C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B4AC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B5CC 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B6EC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B80C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02B92C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
|
||||||
|
Stage29 0x02BB94 28 300 ADN121d 0x44 type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x02BCB4 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02BDD4 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02BEF4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C014 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C134 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C254 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C374 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C494 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C5B4 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C6D4 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C7F4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02C914 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02CA34 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
|
||||||
|
Stage29 0x02CC9C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x02CDBC 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02CEDC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02CFFC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D11C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D23C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D35C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D47C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D59C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D6BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D7DC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02D8FC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02DA1C 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02DB3C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
|
||||||
|
Stage29 0x02DD7C 28 300 ADN121e 0x4E type2 [64,57,15] [1,4,57]
|
||||||
|
Stage29 0x02DE9C 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02DFBC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E0DC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E1FC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E31C 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E43C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E55C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E67C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E79C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E8BC 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02E9DC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02EAFC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||||
|
Stage29 0x02EC1C 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,64]
|
||||||
|
Stage29 0x02F1EC 28 200 ADN146a 0x66 type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x02F960 28 200 ADN146b 0x67 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x02F9A0 29 50 ADN146b 0x67 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x02FF24 28 200 ADN147a 0x68 type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x030698 28 200 ADN147b 0x69 type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x0306D8 29 50 ADN147b 0x69 type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x030C5C 28 200 ADN148a 0x6A type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x0313D0 28 200 ADN148b 0x6B type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x031410 29 50 ADN148b 0x6B type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x031994 28 200 ADN149a 0x6C type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x032108 28 200 ADN149b 0x6D type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x032148 29 50 ADN149b 0x6D type2 [16,16,28] [145,47,47]
|
||||||
|
Stage29 0x0326CC 28 200 ADN150a 0x3F type2 [16,16,16] [145,47,47]
|
||||||
|
Stage29 0x032E40 28 200 ADN150b 0x4A type2 [16,16,16] [29,145,47]
|
||||||
|
Stage29 0x032E80 29 50 ADN150b 0x4A type2 [16,16,28] [145,47,47]
|
||||||
|
|
||||||
|
# builtin 26: 97 sites, 7 distinct values
|
||||||
|
# values: 0 x69, 80 x10, 100 x6, 60 x5, 30 x3, 50 x3, 40 x1
|
||||||
|
# units: 66 distinct, top: ADN128 x8, TCN001 x7, ADN207 x4, TCN208 x4, TCN002 x3, ADT102 x3, ADT104 x3, ADT107 x3, ADT101 x2, ADT103 x2
|
||||||
|
# craft cross-tab (0 unresolved):
|
||||||
|
# UN_e201_ADAN_ISCMissile 17 0x17
|
||||||
|
# UN_n001_TTRL_Box 16 0x16
|
||||||
|
# UN_f202_TCAF_Cargo 9 0x9
|
||||||
|
# UN_e106_ADAN_Destroyer 8 0x4, 80x2, 30x1, 60x1
|
||||||
|
# UN_f106_TCAF_Destroyer 8 0x4, 60x2, 80x1, 50x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 6 100x5, 40x1
|
||||||
|
# UN_f105_TCAF_Cruiser 5 0x3, 80x1, 50x1
|
||||||
|
# UN_e015_ADAN_Puppy_2 5 0x5
|
||||||
|
# UN_e015_ADAN_Puppy 5 0x5
|
||||||
|
# UN_e108_ADAN_ASFrigate 4 80x2, 60x1, 30x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T 3 80x1, 50x1, 30x1
|
||||||
|
# UN_mn500_ADAN_FloatingMine 3 0x3
|
||||||
|
# UN_e105_ADAN_Cruiser 2 80x1, 60x1
|
||||||
|
# UN_e011_ADAN_Attacker_B 2 0x2
|
||||||
|
# UN_f101_TCAF_Acropolis 1 80x1
|
||||||
|
# UN_f002_TCAF_DeltaSaber_W 1 80x1
|
||||||
|
# UN_n001_TTRL_Box_move 1 0x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 1 100x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Ttrl 1 100x1
|
||||||
|
|
||||||
|
# builtin 28: 410 sites, 13 distinct values
|
||||||
|
# values: 200 x116, 120 x76, 300 x74, 500 x48, 150 x37, 100 x29, 1000 x8, 2000 x5, 0.1 x5, 0 x5, 600 x4, 10 x2, 50 x1
|
||||||
|
# units: 190 distinct, top: TCN001 x13, ADN101b x7, ADN101a x7, ADN102a x7, ADN102b x7, ADN103a x7, ADN104a x7, ADN101e x7, ADN102e x7, ADN121b x7
|
||||||
|
# craft cross-tab (0 unresolved):
|
||||||
|
# UN_e106_ADAN_Destroyer 105 120x76, 150x15, 100x12, 200x2
|
||||||
|
# UN_e104_ADAN_Carrier 55 300x54, 150x1
|
||||||
|
# UN_e105_ADAN_CruiserEX 52 200x52
|
||||||
|
# UN_e102_ADAN_BattleshipEX 26 200x26
|
||||||
|
# UN_e001_ADAN_Elan 24 200x18, 300x6
|
||||||
|
# UN_e010_ADAN_Attacker_S 21 200x18, 100x3
|
||||||
|
# UN_e004_ADAN_ElanPlus_N 19 500x19
|
||||||
|
# UN_e009_ADAN_Phantom 16 500x16
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 10 0x5, 100x5
|
||||||
|
# UN_e007_ADAN_Turret 9 100x9
|
||||||
|
# UN_e002_ADAN_Elan_N 9 500x9
|
||||||
|
# UN_e105_ADAN_Cruiser 8 150x8
|
||||||
|
# UN_e101_ADAN_SDBattleshipEX 8 300x8
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T 7 150x4, 10x2, 50x1
|
||||||
|
# UN_e011_ADAN_Attacker_B 6 300x6
|
||||||
|
# UN_f002_TCAF_DeltaSaber_W 5 150x5
|
||||||
|
# UN_e003_ADAN_ElanPlus_Margras 4 2000x4
|
||||||
|
# UN_e108_ADAN_ASFrigateEX 4 600x4
|
||||||
|
# UN_f106_TCAF_Destroyer_Inv 3 0.1x2, 150x1
|
||||||
|
# UN_e001_ADAN_Elan_GR_Violeta 3 1000x3
|
||||||
|
# UN_e001_ADAN_Elan_GR 3 1000x3
|
||||||
|
# UN_e101_ADAN_SDBattleship 3 150x3
|
||||||
|
# UN_f101_TCAF_Acropolis 2 150x1, 0.1x1
|
||||||
|
# UN_e013_ADAN_ElanPlus_Taskent 2 1000x2
|
||||||
|
# UN_f002_TCAF_DeltaSaber_W_Player 2 150x2
|
||||||
|
# UN_e011_ADAN_Attacker_B_HF 2 500x2
|
||||||
|
# UN_e102_ADAN_Battleship 2 150x2
|
||||||
|
# UN_f106_TCAF_Destroyer 1 150x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Player 1 150x1
|
||||||
|
# UN_f105_TCAF_Cruiser 1 0.1x1
|
||||||
|
# UN_f102_TCAF_LightCarrier_Inv 1 0.1x1
|
||||||
|
# UN_e005_ADAN_ElanTypeQ_Margras 1 2000x1
|
||||||
|
# UN_e108_ADAN_ASFrigate 1 150x1
|
||||||
|
# UN_e011_ADAN_Attacker_B_HF_Wayne 1 500x1
|
||||||
|
# UN_e006_ADAN_Vindicator_Margras 1 500x1
|
||||||
|
|
||||||
|
# builtin 29: 164 sites, 6 distinct values
|
||||||
|
# values: 0 x64, 100 x53, 50 x38, 40 x6, 75 x2, 20 x1
|
||||||
|
# units: 70 distinct, top: TCN001 x71, TCN002 x10, TCT207 x4, TCN008 x3, ADT108 x3, ADT109 x3, ADT103 x3, TCN003 x2, ADS151a x2, ADS151b x2
|
||||||
|
# craft cross-tab (0 unresolved):
|
||||||
|
# UN_f002_TCAF_DeltaSaber_W_Player 41 100x23, 0x18
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T 40 0x25, 100x15
|
||||||
|
# UN_f002_TCAF_DeltaSaber_W 38 100x21, 0x16, 50x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_Player 30 0x15, 100x15
|
||||||
|
# UN_e010_ADAN_Attacker_S 18 50x18
|
||||||
|
# UN_f202_TCAF_Cargo 14 50x8, 0x4, 100x2
|
||||||
|
# UN_e201_ADAN_ISCMissile 9 100x8, 20x1
|
||||||
|
# UN_n001_TTRL_Box 9 0x9
|
||||||
|
# UN_e104_ADAN_Carrier 6 0x3, 100x3
|
||||||
|
# UN_f003_TCAF_ArrowHead 5 50x5
|
||||||
|
# UN_e001_ADAN_Elan_GR_Violeta 3 40x3
|
||||||
|
# UN_e001_ADAN_Elan_GR 3 40x3
|
||||||
|
# UN_e006_ADAN_Vindicator_MargrasF 3 0x3
|
||||||
|
# UN_f004_TCAF_DeltaSaber_A_Player 2 0x1, 100x1
|
||||||
|
# UN_f001_TCAF_DeltaSaber_T_EX5_el 2 0x1, 100x1
|
||||||
|
# UN_f106_TCAF_Destroyer 2 50x2
|
||||||
|
# UN_f105_TCAF_Cruiser 2 50x2
|
||||||
|
# UN_be005_ADAN_SpaceFortress 1 0x1
|
||||||
|
# UN_f101_TCAF_Acropolis 1 75x1
|
||||||
|
# UN_f104_TCAF_Battleship 1 75x1
|
||||||
|
# UN_f102_TCAF_LightCarrier 1 50x1
|
||||||
|
# UN_e004_ADAN_ElanPlus_NF 1 50x1
|
||||||
|
|
||||||
|
# builtin 101: 133 sites, 1 distinct values
|
||||||
|
# values: - x133
|
||||||
|
# units: 1 distinct, top: ? x133
|
||||||
|
# craft cross-tab (133 unresolved):
|
||||||
921
docs/re/disc-atlas.html
Normal file
921
docs/re/disc-atlas.html
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
<title>Sylpheed Disc Atlas</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
Palette. Three of these hex values are literal constants in the codebase:
|
||||||
|
--ground is ComposeOptions::default().backdrop = [14,14,20], the colour the
|
||||||
|
UI compositor paints behind every reassembled screen; --warn and --refuted
|
||||||
|
are the viewer's own egui warning/error colours, rgb(224,168,86) and
|
||||||
|
rgb(224,86,122). --confirm is the cyan glow measured off the intro video.
|
||||||
|
Neutrals are biased blue to sit with the ground rather than against it.
|
||||||
|
─────────────────────────────────────────────────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
--ground: #0e0e14;
|
||||||
|
--confirm: #3fb6c6;
|
||||||
|
--warn: #b8792c;
|
||||||
|
--refuted: #c23f61;
|
||||||
|
--accent: #c23f61;
|
||||||
|
|
||||||
|
--bg: #eceef3;
|
||||||
|
--surface: #f6f7fa;
|
||||||
|
--surface-2: #e2e5ee;
|
||||||
|
--text: #16171f;
|
||||||
|
--text-muted: #5c6076;
|
||||||
|
--text-faint: #878ca3;
|
||||||
|
--rule: #c9cddb;
|
||||||
|
--rule-soft: #dbdfe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--confirm: #4fd0e0;
|
||||||
|
--warn: #e0a856;
|
||||||
|
--refuted: #e0567a;
|
||||||
|
--accent: #e0567a;
|
||||||
|
|
||||||
|
--bg: #0e0e14;
|
||||||
|
--surface: #16171f;
|
||||||
|
--surface-2: #1e2029;
|
||||||
|
--text: #e8e9ef;
|
||||||
|
--text-muted: #9296ab;
|
||||||
|
--text-faint: #6d7186;
|
||||||
|
--rule: #2b2e3a;
|
||||||
|
--rule-soft: #22242e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--confirm: #4fd0e0;
|
||||||
|
--warn: #e0a856;
|
||||||
|
--refuted: #e0567a;
|
||||||
|
--accent: #e0567a;
|
||||||
|
|
||||||
|
--bg: #0e0e14;
|
||||||
|
--surface: #16171f;
|
||||||
|
--surface-2: #1e2029;
|
||||||
|
--text: #e8e9ef;
|
||||||
|
--text-muted: #9296ab;
|
||||||
|
--text-faint: #6d7186;
|
||||||
|
--rule: #2b2e3a;
|
||||||
|
--rule-soft: #22242e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type roles: mono for every label and datum (this subject is hex and paths),
|
||||||
|
serif for prose so the two never blur into each other. */
|
||||||
|
:root {
|
||||||
|
--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo,
|
||||||
|
Consolas, "Liberation Mono", monospace;
|
||||||
|
--serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
|
||||||
|
Georgia, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prose stays near 66ch; panels break out to the full width. */
|
||||||
|
.col {
|
||||||
|
max-width: 66ch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Masthead ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
header.mast {
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
padding: 72px 0 34px;
|
||||||
|
margin-bottom: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: clamp(30px, 5.4vw, 52px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.04;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
h1 .dim { color: var(--text-faint); font-weight: 400; }
|
||||||
|
|
||||||
|
.standfirst {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 60ch;
|
||||||
|
margin: 0;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Stat strip ───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--rule-soft);
|
||||||
|
border: 1px solid var(--rule-soft);
|
||||||
|
margin: 40px 0 0;
|
||||||
|
}
|
||||||
|
.stat { background: var(--bg); padding: 16px 18px; }
|
||||||
|
.stat .n {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.stat .l {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-top: 7px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sections ─────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
section { margin: 78px 0 0; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 6px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
h2 .idx { color: var(--accent); font-weight: 400; }
|
||||||
|
h2 .rest { color: var(--text-faint); font-weight: 400; letter-spacing: 0.1em; }
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
margin: 42px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin: 0; }
|
||||||
|
.col > p + p { margin-top: 0; }
|
||||||
|
|
||||||
|
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
|
||||||
|
a:focus-visible, button:focus-visible { outline: 2px solid var(--confirm); outline-offset: 3px; }
|
||||||
|
|
||||||
|
code, .m {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.86em;
|
||||||
|
background: var(--surface-2);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong { font-weight: 700; }
|
||||||
|
em { font-style: italic; color: var(--text); }
|
||||||
|
|
||||||
|
/* ── Confidence chips ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.c-ok { color: var(--confirm); }
|
||||||
|
.c-part { color: var(--warn); }
|
||||||
|
.c-no { color: var(--refuted); }
|
||||||
|
.c-none { color: var(--text-faint); }
|
||||||
|
|
||||||
|
/* ── Panels (full-width breakouts) ────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin: 30px 0 0;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.panel-head {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
padding: 11px 16px;
|
||||||
|
border-bottom: 1px solid var(--rule-soft);
|
||||||
|
}
|
||||||
|
.panel-body { padding: 4px 0; overflow-x: auto; }
|
||||||
|
|
||||||
|
/* The schematic is a screen, not a page element: it keeps the game's own
|
||||||
|
ground in both themes, the way an instrument panel does. */
|
||||||
|
.screen {
|
||||||
|
background: var(--ground);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
margin: 30px 0 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.screen svg { display: block; min-width: 760px; width: 100%; height: auto; }
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11.5px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-top: 12px;
|
||||||
|
max-width: 76ch;
|
||||||
|
}
|
||||||
|
figure { margin: 0; }
|
||||||
|
|
||||||
|
/* ── Tables ───────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.tbl-scroll { overflow-x: auto; margin: 26px 0 0; }
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12.5px;
|
||||||
|
min-width: 620px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 14px 9px 0;
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 8px 14px 8px 0;
|
||||||
|
border-bottom: 1px solid var(--rule-soft);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; padding-right: 22px; }
|
||||||
|
tbody tr:hover { background: var(--surface); }
|
||||||
|
td .note { color: var(--text-muted); font-family: var(--serif); font-size: 13.5px; }
|
||||||
|
|
||||||
|
/* ── Mermaid ──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.dia {
|
||||||
|
margin: 24px 0 0;
|
||||||
|
padding: 18px 16px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--rule-soft);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.dia pre.mermaid { margin: 0; text-align: left; }
|
||||||
|
|
||||||
|
/* ── Callout ──────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.note-box {
|
||||||
|
border-left: 2px solid var(--accent);
|
||||||
|
padding: 2px 0 2px 18px;
|
||||||
|
margin: 26px 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 64ch;
|
||||||
|
}
|
||||||
|
.note-box .lbl {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.plain { margin: 0; padding-left: 1.15em; display: flex; flex-direction: column; gap: 0.55em; }
|
||||||
|
ul.plain li::marker { color: var(--text-faint); }
|
||||||
|
|
||||||
|
/* ── Chain (numbered hops — the numbering is the hop order, which is real) ─── */
|
||||||
|
|
||||||
|
.hops { display: flex; flex-direction: column; gap: 0; margin: 24px 0 0; }
|
||||||
|
.hop {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 34px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 13px 0;
|
||||||
|
border-bottom: 1px solid var(--rule-soft);
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.hop:first-child { border-top: 1px solid var(--rule); }
|
||||||
|
.hop .n {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.hop .b { font-family: var(--mono); font-size: 12.5px; line-height: 1.7; }
|
||||||
|
.hop .b .d { color: var(--text-muted); font-family: var(--serif); font-size: 14px; display: block; margin-top: 3px; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 96px;
|
||||||
|
padding-top: 26px;
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* { animation: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<header class="mast">
|
||||||
|
<p class="eyebrow">Project Sylpheed · Arc of Deception — reverse-engineering reference</p>
|
||||||
|
<h1>Sylpheed Disc Atlas</h1>
|
||||||
|
<p class="standfirst">
|
||||||
|
What is on the disc, and how every piece of it points at every other piece.
|
||||||
|
Four layers deep: the media, the archives, the container formats, and the
|
||||||
|
domain chains a port has to walk.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat"><span class="n">41</span><span class="l">IPFB archives</span></div>
|
||||||
|
<div class="stat"><span class="n">26 443</span><span class="l">archive entries</span></div>
|
||||||
|
<div class="stat"><span class="n">166</span><span class="l">.xpr packages</span></div>
|
||||||
|
<div class="stat"><span class="n">97</span><span class="l">.wmv cutscenes</span></div>
|
||||||
|
<div class="stat"><span class="n">9 519</span><span class="l">sound banks</span></div>
|
||||||
|
<div class="stat"><span class="n">3</span><span class="l">hash functions</span></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
Nothing on this disc is found by path at runtime. Every lookup is a
|
||||||
|
<em>hash</em>, and the whole disc is joined together by three of them with
|
||||||
|
different rules. Read that first, or the rest of the map reads as a pile of
|
||||||
|
unrelated tables.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Counts here were measured on the retail extract for this page, not copied
|
||||||
|
forward. Where a claim is contested or partial, the chip says so — and the
|
||||||
|
things that <em>do not</em> resolve get their own section, because a port
|
||||||
|
has to survive them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">01</span> <span>The join algebra</span> <span class="rest">three hashes, three rules</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
These are not interchangeable, and mixing them up is the single most
|
||||||
|
productive mistake this project has made. They differ in what they hash,
|
||||||
|
and crucially in <strong>case sensitivity</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbl-scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Hash</th><th>Keys</th><th>Case</th><th>Verified</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>name_hash</strong></td>
|
||||||
|
<td>IPFB archive TOC entries — a file path inside a <span class="m">.pak</span></td>
|
||||||
|
<td>insensitive</td>
|
||||||
|
<td class="num">recovers paths</td>
|
||||||
|
<td><span class="chip c-ok">confirmed</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>tag_hash</strong></td>
|
||||||
|
<td>IDXD record names and field keys</td>
|
||||||
|
<td><em>sensitive</em></td>
|
||||||
|
<td class="num">1 271 462 / 1 271 462</td>
|
||||||
|
<td><span class="chip c-ok">confirmed</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>ixud_hash</strong></td>
|
||||||
|
<td>IXUD localised-text record and field keys</td>
|
||||||
|
<td><em>sensitive</em></td>
|
||||||
|
<td class="num">628 165 / 628 165</td>
|
||||||
|
<td><span class="chip c-ok">confirmed</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note-box">
|
||||||
|
<span class="lbl">The trap that cost the most</span>
|
||||||
|
A 24-bit modulus cannot uniquely name an identifier of eight characters or
|
||||||
|
more. Exhaustive preimage search recovers <span class="m">"Stage01"</span>
|
||||||
|
from its own hash, but at seven characters one real target already has
|
||||||
|
<strong>1 176</strong> preimages. Forty-two field keys on this disc are
|
||||||
|
hash-only with no stored name, and they are <em>provably</em> unrecoverable
|
||||||
|
— not merely unrecovered. Everything else carries its name inline: an IDXD
|
||||||
|
field's middle word points at its own name string, which is why the
|
||||||
|
containers are self-describing and the hash almost never has to be inverted.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">02</span> <span>The four layers</span> <span class="rest">disc → archive → container → domain</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
Reading up from the bottom: a domain chain names an asset by string, that
|
||||||
|
string hashes into an archive TOC, the TOC yields a compressed blob, and
|
||||||
|
the blob's first four bytes say which container format it is. Every arrow
|
||||||
|
in this diagram is a hash lookup or a magic-byte test — there are no
|
||||||
|
directory scans.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<div class="screen">
|
||||||
|
<svg viewBox="0 0 1000 640" role="img" aria-label="Four-layer schematic of the Project Sylpheed disc: media, archives, container formats and domain chains, joined by three hash functions.">
|
||||||
|
<defs>
|
||||||
|
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4f5468"/>
|
||||||
|
</marker>
|
||||||
|
<style>
|
||||||
|
.bl { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; letter-spacing: .16em; fill: #6d7186; text-transform: uppercase; }
|
||||||
|
.bx { fill: #16171f; stroke: #2b2e3a; stroke-width: 1; }
|
||||||
|
.bxa { fill: #16171f; stroke: #4fd0e0; stroke-width: 1; }
|
||||||
|
.tt { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; fill: #e8e9ef; }
|
||||||
|
.ts { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; fill: #6d7186; }
|
||||||
|
.tn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; fill: #4fd0e0; }
|
||||||
|
.jn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10.5px; fill: #e0a856; letter-spacing: .06em; }
|
||||||
|
.ln { stroke: #4f5468; stroke-width: 1; }
|
||||||
|
.rl { stroke: #22242e; stroke-width: 1; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- band rules -->
|
||||||
|
<line class="rl" x1="0" y1="118" x2="1000" y2="118"/>
|
||||||
|
<line class="rl" x1="0" y1="268" x2="1000" y2="268"/>
|
||||||
|
<line class="rl" x1="0" y1="418" x2="1000" y2="418"/>
|
||||||
|
|
||||||
|
<!-- ── BAND 1 · disc media ── -->
|
||||||
|
<text class="bl" x="24" y="40">Disc</text>
|
||||||
|
<g>
|
||||||
|
<rect class="bx" x="130" y="30" width="160" height="58" rx="2"/>
|
||||||
|
<text class="tt" x="145" y="54">default.xex</text>
|
||||||
|
<text class="ts" x="145" y="70">executable + PE</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="302" y="30" width="160" height="58" rx="2"/>
|
||||||
|
<text class="tt" x="317" y="54">dat/</text>
|
||||||
|
<text class="ts" x="317" y="70">37 archives</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="474" y="30" width="160" height="58" rx="2"/>
|
||||||
|
<text class="tt" x="489" y="54">hidden/</text>
|
||||||
|
<text class="ts" x="489" y="70">DefTables · MiscBin</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="646" y="30" width="160" height="58" rx="2"/>
|
||||||
|
<text class="tt" x="661" y="54">dat/movie/</text>
|
||||||
|
<text class="ts" x="661" y="70">97 wmv · 6 paks</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="818" y="30" width="160" height="58" rx="2"/>
|
||||||
|
<text class="tt" x="833" y="54">resource3d/</text>
|
||||||
|
<text class="ts" x="833" y="70">166 xpr · 1.4 GB</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- join 1 -->
|
||||||
|
<line class="ln" x1="382" y1="88" x2="382" y2="176" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="554" y1="88" x2="554" y2="176" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="726" y1="88" x2="726" y2="176" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="898" y1="88" x2="898" y2="220" marker-end="url(#ar)"/>
|
||||||
|
<text class="jn" x="130" y="112">name_hash keys the TOC — case-insensitive</text>
|
||||||
|
|
||||||
|
<!-- ── BAND 2 · archives ── -->
|
||||||
|
<text class="bl" x="24" y="190">Archive</text>
|
||||||
|
<g>
|
||||||
|
<rect class="bxa" x="130" y="176" width="560" height="72" rx="2"/>
|
||||||
|
<text class="tt" x="146" y="200">IPFB · .pak index + .p00….pNN data segments</text>
|
||||||
|
<text class="ts" x="146" y="218">12-byte TOC entry { name_hash, offset, comp_size } · Z1/zlib payloads</text>
|
||||||
|
<text class="tn" x="146" y="236">41 archives · 26 443 entries</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="706" y="176" width="272" height="72" rx="2"/>
|
||||||
|
<text class="tt" x="722" y="200">raw files (not archived)</text>
|
||||||
|
<text class="ts" x="722" y="218">.wmv video · .xpr resource packages</text>
|
||||||
|
<text class="tn" x="722" y="236">263 files</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- join 2 -->
|
||||||
|
<line class="ln" x1="200" y1="248" x2="200" y2="292" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="410" y1="248" x2="410" y2="292" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="620" y1="248" x2="620" y2="292" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="842" y1="248" x2="842" y2="292" marker-end="url(#ar)"/>
|
||||||
|
<text class="jn" x="130" y="264">payload magic decides the format — nothing declares a type</text>
|
||||||
|
|
||||||
|
<!-- ── BAND 3 · containers ── -->
|
||||||
|
<text class="bl" x="24" y="330">Container</text>
|
||||||
|
<g>
|
||||||
|
<rect class="bxa" x="130" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="140" y="316">IDXD</text><text class="ts" x="140" y="332">tables</text><text class="tn" x="140" y="347">6325</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="215" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="225" y="316">T8aD</text><text class="ts" x="225" y="332">2D tex</text><text class="tn" x="225" y="347">4525</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="300" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="310" y="316">RATC</text><text class="ts" x="310" y="332">UI bundle</text><text class="tn" x="310" y="347">2985</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="385" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="395" y="316">IXUD</text><text class="ts" x="395" y="332">text</text><text class="tn" x="395" y="347">1104</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="470" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="480" y="316">LSTA</text><text class="ts" x="480" y="332">sprites</text><text class="tn" x="480" y="347">64</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="555" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="565" y="316">SLB</text><text class="ts" x="565" y="332">XMA1</text><text class="tn" x="565" y="347">9519</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="640" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="650" y="316">XBG7</text><text class="ts" x="650" y="332">mesh</text><text class="tn" x="650" y="347">6294</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="725" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="735" y="316">XPR2</text><text class="ts" x="735" y="332">tex pkg</text><text class="tn" x="735" y="347">166</text>
|
||||||
|
|
||||||
|
<rect class="bxa" x="810" y="292" width="77" height="62" rx="2"/>
|
||||||
|
<text class="tt" x="820" y="316">TTF</text><text class="ts" x="820" y="332">fonts</text><text class="tn" x="820" y="347">54</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="895" y="292" width="77" height="62" rx="2" stroke="#e0567a"/>
|
||||||
|
<text class="tt" x="905" y="316">ISB</text><text class="ts" x="905" y="332">PRT · BIN</text>
|
||||||
|
<text class="ts" x="905" y="347" fill="#e0567a">no parser</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- join 3 -->
|
||||||
|
<line class="ln" x1="200" y1="354" x2="200" y2="442" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="440" y1="354" x2="440" y2="442" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="680" y1="354" x2="680" y2="442" marker-end="url(#ar)"/>
|
||||||
|
<line class="ln" x1="900" y1="354" x2="900" y2="442" marker-end="url(#ar)"/>
|
||||||
|
<text class="jn" x="130" y="410">tag_hash / ixud_hash key records + fields — values are asset NAMES, hashed back up</text>
|
||||||
|
|
||||||
|
<!-- ── BAND 4 · domains ── -->
|
||||||
|
<text class="bl" x="24" y="480">Domain</text>
|
||||||
|
<g>
|
||||||
|
<rect class="bx" x="130" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="141" y="466">Stage</text><text class="ts" x="141" y="482">28 missions</text><text class="ts" x="141" y="496">10 sub-tables</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="252" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="263" y="466">Unit</text><text class="ts" x="263" y="482">159 fields</text><text class="ts" x="263" y="496">→ turret slot</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="374" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="385" y="466">Arsenal</text><text class="ts" x="385" y="482">59 items</text><text class="ts" x="385" y="496">4-hop chain</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="496" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="507" y="466">Audio</text><text class="ts" x="507" y="482">5798 cues</text><text class="ts" x="507" y="496">5 families</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="618" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="629" y="466">Cutscene</text><text class="ts" x="629" y="482">104 slots</text><text class="ts" x="629" y="496">4 bindings</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="740" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="751" y="466">UI screen</text><text class="ts" x="751" y="482">965 builds</text><text class="ts" x="751" y="496">decl + place</text>
|
||||||
|
|
||||||
|
<rect class="bx" x="862" y="442" width="112" height="66" rx="2"/>
|
||||||
|
<text class="tt" x="873" y="466">Save</text><text class="ts" x="873" y="482">545 bytes</text><text class="ts" x="873" y="496">GDHA + zlib</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- the cross-domain edge: the save indexes the arsenal's string order -->
|
||||||
|
<path class="ln" d="M 918 508 L 918 560 L 430 560 L 430 508" fill="none" stroke-dasharray="3 3" marker-end="url(#ar)"/>
|
||||||
|
<text class="jn" x="560" y="578">the save's 54-byte blob is indexed by strings.tbl order — not weapon.tbl</text>
|
||||||
|
|
||||||
|
<text class="ts" x="130" y="614" fill="#4f5468">Counts measured on the retail extract. Container counts exclude sound.pak and entries over 8 MB.</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<figcaption>
|
||||||
|
The three amber lines are the joins. Each is a hash lookup, and each is a
|
||||||
|
place a port can silently take the wrong branch: an archive TOC miss
|
||||||
|
returns nothing, and a field the disc never values reads as
|
||||||
|
<span class="m">0.0</span> rather than as an error.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">03</span> <span>Archives</span> <span class="rest">where the mass actually sits</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
Two archives hold most of the disc. <span class="m">sound.pak</span> is
|
||||||
|
1.07 GB across five data segments — more than every other archive
|
||||||
|
combined — and <span class="m">GP_HANGAR_ARSENAL.pak</span> is large for a
|
||||||
|
menu because it is <em>stage-scoped</em>: 168 of its objects are 28
|
||||||
|
missions × 6 languages, each a complete Hangar configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbl-scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Archive</th><th class="num">Entries</th><th class="num">Stored</th><th>What it is</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>sound.pak</td><td class="num">9 519</td><td class="num">1.07 GB</td><td><span class="note">every XMA1 bank — music, jingles, SFX, both voice languages</span></td></tr>
|
||||||
|
<tr><td>GP_HANGAR_ARSENAL.pak</td><td class="num">1 538</td><td class="num">67.2 MB</td><td><span class="note">28 stages × 6 languages of Hangar config, plus item text</span></td></tr>
|
||||||
|
<tr><td>GP_MAIN_GAME_{D,E,F,I,J,S}</td><td class="num">1 119 ea.</td><td class="num">4.6 MB ea.</td><td><span class="note">the mission data set — stages, units, routes, dialogue</span></td></tr>
|
||||||
|
<tr><td>GP_READY_ROOM.pak</td><td class="num">1 106</td><td class="num">67.6 MB</td><td><span class="note">largest UI pak; ISL script bytecode + link map</span></td></tr>
|
||||||
|
<tr><td>GP_MAIN_GAME_*2D <span class="chip c-no">unnamed</span></td><td class="num">711 ea.</td><td class="num">15.2 MB ea.</td><td><span class="note">six paks, 0 % of names recoverable — see §06</span></td></tr>
|
||||||
|
<tr><td>DefTables.pak <span class="m">hidden/</span></td><td class="num">1 465</td><td class="num">—</td><td><span class="note">definition tables; 804 of 1 465 names resolved</span></td></tr>
|
||||||
|
<tr><td>movie/{eng,jpn,deu,esp,fra,ita}</td><td class="num">117 ea.</td><td class="num">1.8 MB ea.</td><td><span class="note">subtitles, telop overlays and fonts — <em>not</em> voice</span></td></tr>
|
||||||
|
<tr><td>tables.pak</td><td class="num">79</td><td class="num">330 KB</td><td><span class="note">the registries: sound cues, the cutscene manifest</span></td></tr>
|
||||||
|
<tr><td>MiscBin.pak <span class="m">hidden/</span></td><td class="num">40</td><td class="num">—</td><td><span class="note">0 names resolved</span></td></tr>
|
||||||
|
<tr><td>fonts.pak</td><td class="num">3</td><td class="num">2.4 MB</td><td><span class="note">the three shipped typefaces</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note-box">
|
||||||
|
<span class="lbl">One entry lies about its size</span>
|
||||||
|
<span class="m">Static.slb</span>, the sound-effect bank, sits at the highest
|
||||||
|
offset in <span class="m">sound.pak</span> and declares
|
||||||
|
<strong>616 768 bytes more than the disc holds</strong>. This is not a bad
|
||||||
|
extract — <span class="m">sound.p04</span> matches the ISO's own directory
|
||||||
|
record — and a sweep of every archive finds this entry over-running and no
|
||||||
|
other. The last entry's size field is an allocation size. A reader must
|
||||||
|
allow a short read there, and <em>only</em> there.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">04</span> <span>Container formats</span> <span class="rest">what each blob turns into</span></h2>
|
||||||
|
|
||||||
|
<div class="tbl-scroll">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Magic</th><th>Content</th><th>State</th><th>Reach</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><strong>IDXD</strong></td><td><span class="note">self-describing record/field table — the game's whole data layer</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">7 750 / 7 750 objects</td></tr>
|
||||||
|
<tr><td><strong>IXUD</strong></td><td><span class="note">the same container, UTF-16BE, offsets in <em>chars</em></span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">1 104 / 1 104 objects</td></tr>
|
||||||
|
<tr><td><strong>T8aD</strong></td><td><span class="note">2D texture — a list of arbitrary sub-rectangles, not a tile grid</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">19 216 / 19 216</td></tr>
|
||||||
|
<tr><td><strong>RATC</strong></td><td><span class="note">UI bundle; children are sprites, layout records and primitives</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">10 144 / 10 148 refs</td></tr>
|
||||||
|
<tr><td><strong>LSTA</strong></td><td><span class="note">sprite display list — count covers T8aD <em>and</em> PRMD</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">64 / 64</td></tr>
|
||||||
|
<tr><td><strong>XBG7</strong></td><td><span class="note">mesh; index pool then vertex pool, layout declared per sub-mesh</span></td><td><span class="chip c-part">99.25 %</span></td><td class="num">6 247 / 6 294</td></tr>
|
||||||
|
<tr><td><strong>XPR2</strong></td><td><span class="note">texture package — de-tile, A8R8G8B8 and DXT1</span></td><td><span class="chip c-part">partial</span></td><td class="num">channel order confirmed</td></tr>
|
||||||
|
<tr><td><strong>SLB</strong></td><td><span class="note">XACT bank of XMA1 sub-waves; two layouts, one headerless</span></td><td><span class="chip c-part">35/36 shared</span></td><td class="num"><span class="m">JNGL_001</span> fails</td></tr>
|
||||||
|
<tr><td><strong>GDHA</strong></td><td><span class="note">save file — zlib payload, chunk stream, round-trips byte-identically</span></td><td><span class="chip c-part">~11 ❔ fields</span></td><td class="num">545 bytes</td></tr>
|
||||||
|
<tr><td>TTF / OTF</td><td><span class="note">stock OpenType</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">54</td></tr>
|
||||||
|
<tr><td><strong>ISB</strong></td><td><span class="note">ISL mission-script bytecode</span></td><td><span class="chip c-part">partial</span></td><td class="num">builtins documented</td></tr>
|
||||||
|
<tr><td><strong>PRT</strong></td><td><span class="note">telop — on-screen text overlay for cutscenes</span></td><td><span class="chip c-none">no parser</span></td><td class="num">22 bound</td></tr>
|
||||||
|
<tr><td><strong>BIN</strong></td><td><span class="note">collision meshes, <span class="m">CollisionSet_S<NN>.bin</span></span></td><td><span class="chip c-none">no parser</span></td><td class="num">documented only</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">05</span> <span>The domain chains</span> <span class="rest">what references what</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
Each of these starts from something the player can see and ends at bytes on
|
||||||
|
the disc. Where a chain has a hop that looks like it should be direct and
|
||||||
|
is not, that hop is called out — those are where a naive port breaks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Mission</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
One IDXD object per stage in every language pak. Its
|
||||||
|
<span class="m">StageResource</span> record is the hub: nineteen fields
|
||||||
|
naming the 3D packages, the enemy roster, the routes, the collision set
|
||||||
|
and the localised objective strings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="dia">
|
||||||
|
<pre class="mermaid">
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px','clusterBkg':'#16171f','clusterBorder':'#2b2e3a'}}}%%
|
||||||
|
flowchart LR
|
||||||
|
A["GP_MAIN_GAME_<lang>.pak"] --> B["Stage_S<NN> record"]
|
||||||
|
B --> C["StageResource"]
|
||||||
|
C --> D["Stage_S<NN>.xpr<br/>BG_<place>.xpr"]
|
||||||
|
C --> E["EnumUnit_S<NN>.tbl"]
|
||||||
|
C --> F["UnitGroup_S<NN>.tbl<br/>squadron roster"]
|
||||||
|
C --> G["Route_S<NN>.tbl<br/>FormationSet_S<NN>.tbl"]
|
||||||
|
C --> H["CollisionSet_S<NN>.bin"]
|
||||||
|
C --> I["EnumLocalString_S<NN>.tbl"]
|
||||||
|
B --> J["Stage\script.tbl"]
|
||||||
|
J --> K["StageNN.ssb<br/>ISL bytecode"]
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Arsenal — the four-hop trap</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
The Hangar lists 59 weapons; the disc's <span class="m">Weapon</span> table
|
||||||
|
has 131, and the two name sets overlap in <strong>zero</strong> values.
|
||||||
|
That is not a mismatch to be reconciled — it is the wrong join. An arsenal
|
||||||
|
item names a <em>hardpoint slot</em>, and the slot carries the weapon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="hops">
|
||||||
|
<div class="hop"><span class="n">01</span><span class="b">loadout record <span style="color:var(--text-faint)">— flight position × pilot, e.g. Rhino2-Ellen</span><span class="d">Names <code>Arm1</code> / <code>Arm2</code> / <code>Arm3</code> / <code>Nose</code> — which are not items either.</span></span></div>
|
||||||
|
<div class="hop"><span class="n">02</span><span class="b">per-slot allow-list record<span class="d">Its only <em>named</em> field is <code>Type</code>; the candidate items live in its positional, unnamed fields, in order.</span></span></div>
|
||||||
|
<div class="hop"><span class="n">03</span><span class="b">arsenal item <span style="color:var(--text-faint)">→ .PlayerWeapon</span><span class="d">Resolves to <code>Turret_050</code> — a slot on the player craft's own unit table. 59 of 59 do; 0 of 59 name a weapon.</span></span></div>
|
||||||
|
<div class="hop"><span class="n">04</span><span class="b">turret slot <span style="color:var(--text-faint)">→ .WeaponID</span><span class="d">And <em>this</em> is the <code>Weapon</code> record.</span></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Audio</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
One IDXD object in <span class="m">tables.pak</span> is the whole cue
|
||||||
|
index. Its <span class="m">SOUNDS</span> record is the only record found on
|
||||||
|
the disc that is entirely named fields — 5 798 of them — and the names are
|
||||||
|
the join key. Cue ids are partitioned by family with no overlap.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="dia">
|
||||||
|
<pre class="mermaid">
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
|
||||||
|
flowchart LR
|
||||||
|
M["script message id"] --> S["SOUNDS<br/>cue name → sound id"]
|
||||||
|
S --> F["FILES<br/>5 135 bank paths"]
|
||||||
|
F --> H{{"name_hash"}}
|
||||||
|
H --> P["sound.pak<br/>9 519 entries"]
|
||||||
|
P --> X["XMA1 sub-waves"]
|
||||||
|
S -.-> C1["VOICE 1500–7331"]
|
||||||
|
S -.-> C2["SE 1–901"]
|
||||||
|
S -.-> C3["DEMO 8000–8408"]
|
||||||
|
S -.-> C4["BR 8500–8600"]
|
||||||
|
S -.-> C5["BGM 1001–1109"]
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div class="note-box">
|
||||||
|
<span class="lbl">Two things the paths do not tell you</span>
|
||||||
|
The 36 music, jingle and SFX banks sit at the table <em>root</em> with no
|
||||||
|
language component, so both <span class="m">sounds.tbl</span> files name
|
||||||
|
them. And a <span class="m">.slb</span> need not hold the track its name
|
||||||
|
claims: the movie voices are one continuous stream chunked into TOC entries
|
||||||
|
whose boundaries do not line up with the cues.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Cutscene</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
One manifest record binds four assets per slot. Measured for this page:
|
||||||
|
104 slots over 101 distinct movies, 99 with a subtitle track, 99 with a
|
||||||
|
voice track, 22 with a telop overlay.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="dia">
|
||||||
|
<pre class="mermaid">
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
|
||||||
|
flowchart LR
|
||||||
|
MF["cutscene manifest<br/>tables.pak"] --> MV["MOVIE<br/>dat/movie/<name>.wmv"]
|
||||||
|
MF --> TL["TELOP<br/><lang>.pak+*.prt"]
|
||||||
|
MF --> SB["SUBTITLE<br/><lang>.pak+SUBTITLE_*.tbl"]
|
||||||
|
MF --> VT["VOICETRACK<br/>cue name"]
|
||||||
|
SB --> IX["IXUD caption keys"]
|
||||||
|
IX --> TX["GP_MAIN_GAME_<lang><br/>8 800 text keys"]
|
||||||
|
VT --> SC["sound cue → .slb region"]
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>UI screen</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
One archive per screen; each RATC bundle is one context × language build.
|
||||||
|
A bundle's declaration table lists every element with its parent and pivot;
|
||||||
|
the placement region right after it gives each element a keyframe group.
|
||||||
|
Both the tutorial pause menu and the title main menu rebuild
|
||||||
|
pixel-accurately from the disc alone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Two things are <em>not</em> what they look like. The resting position is
|
||||||
|
neither the first nor the last keyframe — it is the plateau. And the
|
||||||
|
declaration table is <strong>not</strong> the paint order: a per-draw
|
||||||
|
capture of the running title screen paints element 13 first and elements
|
||||||
|
0 and 1 late. The real order is a second, reordered child array the screen
|
||||||
|
object keeps at runtime — deriving it from the bundle is still open.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Save</h3>
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
545 bytes: a <span class="m">GDHA</span> container, a zlib payload, then a
|
||||||
|
chunk stream. The 54-byte develop blob has the alphabet <span class="m">0
|
||||||
|
locked / 2 developable / 4 developed</span>, and is indexed by
|
||||||
|
<span class="m">strings.tbl</span> order — the display order <em>plus</em>
|
||||||
|
the cut items only the localisation file lists. <span class="m">weapon.tbl</span>'s
|
||||||
|
id list is not the index space; that it is also 54 long is a coincidence,
|
||||||
|
and the two agree only to index 32.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="note-box">
|
||||||
|
<span class="lbl">The stale-summary trap</span>
|
||||||
|
The in-game Details panel reads a <em>summary copy</em> held in the container
|
||||||
|
header, not the payload. Edit the payload alone and the panel keeps showing
|
||||||
|
the old values — which reads exactly like a failed parse.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">06</span> <span>References that go nowhere</span> <span class="rest">a port has to survive these</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<p>
|
||||||
|
These are not gaps in the reverse engineering. They are properties of the
|
||||||
|
shipped disc, and each one was found by a check that expected the opposite.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbl-scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Reference</th><th>Where</th><th>Status</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td><span class="m">pmbase.t32</span></td><td><span class="note">named by <span class="m">pmbase.rat</span> in all four <span class="m">GP_STAGE_CLEAR</span> language builds</span></td><td><span class="chip c-no">nowhere on disc</span></td></tr>
|
||||||
|
<tr><td><span class="m">SUBTITLE_S12B.tbl</span></td><td><span class="note">bound by the manifest, in all six languages</span></td><td><span class="chip c-no">resolves in none</span></td></tr>
|
||||||
|
<tr><td><span class="m">logo1…logo4</span>, <span class="m">SYLPH_HD720p_8M-CBR_2ch</span></td><td><span class="note">manifest-bound movies</span></td><td><span class="chip c-no">no .wmv</span></td></tr>
|
||||||
|
<tr><td><span class="m">dat\GP_TEST\</span></td><td><span class="note">a debug archive the script manifest points at</span></td><td><span class="chip c-no">not shipped</span></td></tr>
|
||||||
|
<tr><td>6 × <span class="m">GP_MAIN_GAME_*2D</span></td><td><span class="note">711 entries each; eleven other paks are at 100 % by the same method</span></td><td><span class="chip c-no">0 % named</span></td></tr>
|
||||||
|
<tr><td><span class="m">GP_READY_ROOM.pak</span></td><td><span class="note">the largest UI pak on the disc</span></td><td><span class="chip c-no">6 of 1 106 named</span></td></tr>
|
||||||
|
<tr><td>42 ISL script-symbol keys</td><td><span class="note">hash-only field keys in <span class="m"><lang>\script\ID.tbl</span></span></td><td><span class="chip c-no">provably unrecoverable</span></td></tr>
|
||||||
|
<tr><td>9 of 101 cutscenes</td><td><span class="note">no English transcript resolves</span></td><td><span class="chip c-part">unexplained</span></td></tr>
|
||||||
|
<tr><td><span class="m">JNGL_001.slb</span></td><td><span class="note">headerless bank; payload is not a whole number of XMA1 packets</span></td><td><span class="chip c-part">does not decode</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note-box">
|
||||||
|
<span class="lbl">Why the 2D paks matter more than they look</span>
|
||||||
|
Six archives at <em>exactly</em> 0.0 %, all six, 711 entries each — while
|
||||||
|
eleven menu paks hit 100 % in the same run. That control is what makes it a
|
||||||
|
finding rather than a failed guess: the naming method works, and these
|
||||||
|
archives are outside it. Whatever names their contents is not a path hashed
|
||||||
|
the way every other archive's is.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||||
|
<section>
|
||||||
|
<h2><span class="idx">07</span> <span>Still open</span></h2>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<ul class="plain">
|
||||||
|
<li><strong>UI paint order from the bundle.</strong> Solved at runtime, not statically — and static is what a port needs.</li>
|
||||||
|
<li><strong>The <span class="m">.prt</span> telop format.</strong> 22 cutscenes bind one; no parser exists.</li>
|
||||||
|
<li><strong>Collision meshes.</strong> <span class="m">CollisionSet_S<NN>.bin</span> is documented and unparsed.</li>
|
||||||
|
<li><strong>47 XBG7 resources</strong> still miss — mostly pose/proxy composites and damage variants, not a threshold away.</li>
|
||||||
|
<li><strong>~11 GHAD save fields</strong> unnamed, and difficulty-versus-stage undecided: three fields hold the value 2.</li>
|
||||||
|
<li><strong>The naming of the 2D and Ready Room archives</strong>, which is the largest single block of unreachable content on the disc.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Counts measured on the retail extract (USA/Europe, En/Ja). Container-format
|
||||||
|
counts exclude <span class="m">sound.pak</span> and entries over 8 MB.<br/>
|
||||||
|
Confidence follows the corpus convention — confirmed · partial · refuted —
|
||||||
|
and is per-claim, never per-document.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
253
docs/re/structures/idxd-unnamed-keys.md
Normal file
253
docs/re/structures/idxd-unnamed-keys.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# IDXD field keys that carry no name
|
||||||
|
|
||||||
|
- **Confidence:** ✅ the census and the identification of the hash-keyed set ·
|
||||||
|
✅ the last characters of 30 of the 42 names · ❌ **no name was recovered**
|
||||||
|
- **Tool:** `tools/re-capture/idxd_unnamed_keys.py` (`census` / `idtbl` / `crack` / `selftest`)
|
||||||
|
- **Data:** [`docs/re/data/idxd-unnamed-field-keys.txt`](../data/idxd-unnamed-field-keys.txt)
|
||||||
|
- **Seen in:** every `dat/*.pak` + `hidden/*.pak` on the disc; the interesting 42
|
||||||
|
are in `dat/GP_READY_ROOM.pak`, file `<lang>\script\ID.tbl`
|
||||||
|
- **Depends on:** the IDXD record/field table and `tag_hash`, which live on branch
|
||||||
|
`auto/re-idxd-container` — **not on `main`**, so this note transcribes what it
|
||||||
|
needs and the tool carries its own copy of `tag_hash` rather than importing one.
|
||||||
|
|
||||||
|
An IDXD field entry is `(key, name_off, value_off)`. Usually `name_off` points at
|
||||||
|
the field's own name in the string pool and `key == tag_hash(name)`. When
|
||||||
|
`name_off` is `0xFFFFFFFF` the pool holds no name and the reader has to know what
|
||||||
|
`key` means from somewhere else. This note answers: how many such keys are there,
|
||||||
|
what are they, and can the names be inverted out of the hash?
|
||||||
|
|
||||||
|
**Short answer: 7 094 distinct keys, of which 7 052 are not hashes at all and 42
|
||||||
|
are. None of the 42 names was recovered — but they are now identified, their call
|
||||||
|
graph is reconstructed, and 30 of them have their last two-to-four characters
|
||||||
|
pinned algebraically.**
|
||||||
|
|
||||||
|
## ✅ The census
|
||||||
|
|
||||||
|
Measured by walking all 33 `dat/*.pak` plus `hidden/DefTables.pak` and
|
||||||
|
`hidden/MiscBin.pak` (`.pNN` segments joined, `Z1` payloads inflated):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| IDXD objects / records / field entries | 7 750 / 190 782 / **2 757 039** |
|
||||||
|
| named field entries | 1 271 462 |
|
||||||
|
| unnamed field entries (`name_off == 0xFFFFFFFF`) | 1 485 577 |
|
||||||
|
| distinct keys named *somewhere* on disc | 12 174 |
|
||||||
|
| **distinct keys never named anywhere** | **7 094** |
|
||||||
|
| … below `0x00010000` | 7 052 |
|
||||||
|
| … in `[0x10000, 0x01000000)` | **0** |
|
||||||
|
| … at or above `0x01000000` | **42** |
|
||||||
|
|
||||||
|
Parse failures: 0. `tag_hash` reproduces the key of **1 271 462 / 1 271 462**
|
||||||
|
named field entries disc-wide (`idxd_unnamed_keys.py selftest`), which is the
|
||||||
|
validation gate for everything below — far past the 200-pair bar this
|
||||||
|
investigation was asked to clear.
|
||||||
|
|
||||||
|
### ✅ "504 hash-keyed fields disc-wide" is 504 **entries**, not 504 keys
|
||||||
|
|
||||||
|
`INDEX.md` (on `auto/re-idxd-container`) records *504 fields disc-wide that are
|
||||||
|
hash-keyed with no name*. That number is exactly reproduced here — and it counts
|
||||||
|
**field entries**, not distinct keys:
|
||||||
|
|
||||||
|
```
|
||||||
|
42 distinct keys x 6 language copies of the same object x 2 records = 504
|
||||||
|
```
|
||||||
|
|
||||||
|
The three unnamed populations add up:
|
||||||
|
`504` (hash-keyed) `+ 1 380 324` (ordinal) `+ 104 749` (key `0x00000000`, i.e.
|
||||||
|
ordinal 0, held apart only because `tag_hash("") == 0` makes it look "named"
|
||||||
|
somewhere) `= 1 485 577`. So the brute-force target was never 504 names; it was
|
||||||
|
**42**.
|
||||||
|
|
||||||
|
### ✅ The gap at `[0x10000, 0x01000000)` is what separates hashes from ordinals
|
||||||
|
|
||||||
|
`tag_hash` puts the byte-sum checksum of the name in the **top byte**. A real
|
||||||
|
hash therefore falls below `0x01000000` only when the name's byte sum is `0 mod
|
||||||
|
256` — about 1 name in 256 — *and* its low 24 bits are also tiny. Of the 12 174
|
||||||
|
keys that do carry a name, 75 have a zero top byte, and the smallest of those is
|
||||||
|
`0x0002677C`; the never-named ordinal band tops out at `0x2198`. There is a clean
|
||||||
|
three-order-of-magnitude gap and **not one key in it**. The split is measured,
|
||||||
|
not assumed.
|
||||||
|
|
||||||
|
## ✅ The 7 052 ordinals are not hashes and never were
|
||||||
|
|
||||||
|
They are author-assigned element ids written straight into the tag slot. For
|
||||||
|
1 404 924 of the 1 485 577 unnamed entries the key **equals the field's index
|
||||||
|
within its record**; a further 42 750 are index + 1. The rest are hand-numbered
|
||||||
|
with deliberate gaps, e.g. the hangar `WEAPONS` record runs
|
||||||
|
`…0x30 0x31 … 0x35`, jumps to `0x50 … 0x55`, then `0x61` — an id space, not a
|
||||||
|
counter. `tables.pak`'s `FILES` record numbers 5 135 resources from `0x3E9`
|
||||||
|
(1001) upwards.
|
||||||
|
|
||||||
|
**Do not feed these to a reverse lookup.** Confirmed by the strongest possible
|
||||||
|
test: `tag_hash("BGM_001") = 0xC662435B`, while the key of the field whose value
|
||||||
|
is `BGM_001.slb` is `0x000003E9`. Zero of 5 135 `FILES` keys match a hash of
|
||||||
|
their own value under any stem/case/extension variant.
|
||||||
|
|
||||||
|
Full per-key listing: Part 3 of the data file. Per-schema value profiles: Part 2.
|
||||||
|
|
||||||
|
## ✅ What the 42 hash-shaped keys are
|
||||||
|
|
||||||
|
All 42 live in **six byte-identical copies** of one object — `eng jpn fra deu ita
|
||||||
|
esp \script\ID.tbl` inside `dat/GP_READY_ROOM.pak` (resolved through the IPFB TOC
|
||||||
|
by `name_hash("eng\\script\\ID.tbl") = 0xC40BC120`, and pointed at by the
|
||||||
|
`SCRIPT_PATH` field of `tables.pak`'s `BASE_INFO`, whose value is
|
||||||
|
`dat\GP_READY_ROOM.pak+eng\script\`).
|
||||||
|
|
||||||
|
The object has exactly two records, `FILE` and `OFFSET`, carrying **the same 42
|
||||||
|
keys in the same order** — it is a column store:
|
||||||
|
|
||||||
|
```
|
||||||
|
key(tag_hash of an ISL script symbol) -> FILE = the .isb it is defined in
|
||||||
|
OFFSET = where in that file
|
||||||
|
```
|
||||||
|
|
||||||
|
It is the artefact of the pass the executable announces as
|
||||||
|
`silph::GamePart_ReadyRoom::Impl::PrepareScript - isl script prescanning start.`
|
||||||
|
— a link map from symbol hash to definition site. The 34 `.isb` files it names
|
||||||
|
are all present in the same pak (`eng\script\stage01.isb` etc.).
|
||||||
|
|
||||||
|
### ✅ Independent confirmation: the keys appear in the bytecode
|
||||||
|
|
||||||
|
41 of the 42 keys occur verbatim as **little-endian 32-bit words** inside the
|
||||||
|
`.isb` bytecode, and where they occur reconstructs a coherent call graph
|
||||||
|
(Part 4 of the data file):
|
||||||
|
|
||||||
|
| defined in | referenced from |
|
||||||
|
|---|---|
|
||||||
|
| `stage01..16.isb`, `challenge01..06.isb`, `tutorial0N01.isb` (30 symbols) | `main.isb` only |
|
||||||
|
| `function.isb` (5 symbols) | the stage and challenge scripts |
|
||||||
|
| `function_tutorial.isb` (6 symbols) | the six tutorial scripts |
|
||||||
|
| `main.isb` (1 symbol) | **nothing** — the root entry point |
|
||||||
|
|
||||||
|
That is exactly the shape a launcher + shared-helper-library arrangement should
|
||||||
|
have, and it is the reason these keys can be called hashes with confidence rather
|
||||||
|
than "large ordinals": ordinals are not stored as call targets in code.
|
||||||
|
|
||||||
|
🟡 `OFFSET` is **base 36**. Every value uses only `[0-9a-z]`, and read as base 36
|
||||||
|
they are small ascending numbers per file: `function.isb` → 1, 9, 19, 35, 58;
|
||||||
|
`function_tutorial.isb` → 1, 12, 23, 34, 45, 50; `tutorial0101.isb` → 1, 61, 191.
|
||||||
|
The first symbol of every file is 1. Whether the unit is a statement index, a
|
||||||
|
line, or a word offset is ❔ — not settled here.
|
||||||
|
|
||||||
|
## ✅ 30 of the 42 names end in the digits of their own filename
|
||||||
|
|
||||||
|
This is *forced* by the hash, not guessed. `tag_hash`'s low 24 bits are a
|
||||||
|
base-256 polynomial mod `0x00FFFFDF`, so bumping the character `k` places from
|
||||||
|
the end changes the low bits by `256^k mod M` (1, `0x100`, `0x10000`, `0x21`, …)
|
||||||
|
and the top byte by 1. Measured:
|
||||||
|
|
||||||
|
| family | observed key deltas | forced conclusion |
|
||||||
|
|---|---|---|
|
||||||
|
| `stage01..09` | `+0x01000001` × 8 | last character increments by 1 |
|
||||||
|
| `stage10..16` | `+0x01000001` × 6 | ditto |
|
||||||
|
| `stage09 → stage10` | `0x8FA349EF → 0x87A34AE6` | low bits `+0xF7 = 0x100 − 9`, top `−8`: the last two characters go `"09" → "10"` |
|
||||||
|
| `challenge01..06` | `+0x01000001` × 5 | last character increments by 1 |
|
||||||
|
| `tutorial0101 → 0201 … 0601` | `+0x01010000` × 5 | the character **two** from the end increments |
|
||||||
|
| `tutorial0101`'s two siblings | `+0x01000001`, `+0x02000002` | last character `+1`, `+2` |
|
||||||
|
|
||||||
|
So the sixteen stage symbols end `"01" … "16"`, the six challenge symbols end
|
||||||
|
`"01" … "06"`, and the eight tutorial symbols end `"0101" "0102" "0103" "0201"
|
||||||
|
"0301" "0401" "0501" "0601"` — matching their `.isb` filenames exactly. The
|
||||||
|
remaining 12 (`function.isb` × 5, `function_tutorial.isb` × 6, `main.isb` × 1) are
|
||||||
|
unconstrained.
|
||||||
|
|
||||||
|
That is the whole of the recovered name information. **The prefixes are not
|
||||||
|
recovered.**
|
||||||
|
|
||||||
|
## ❌ Cracking: a null result, and the arithmetic that says why
|
||||||
|
|
||||||
|
`tag_hash` maps onto `256 × 0x00FFFFDF ≈ 2^32` values, so a search over `S`
|
||||||
|
candidates yields on average `S / 2^32` **false** preimages *per target*. That is
|
||||||
|
the number every attempt below is judged against.
|
||||||
|
|
||||||
|
| attack | candidates (space `S`) | E[false] per target | hits |
|
||||||
|
|---|---|---|---|
|
||||||
|
| every distinct disc string (IDXD names, record names, field values) | 105 393 | 0.000025 | **0** |
|
||||||
|
| every string in the executable (`.pe`) | 75 912 | 0.000018 | **0** |
|
||||||
|
| every ASCII run ≥3 in every pak payload except `sound.pak` (incl. `.ssb` symbol tables, XML, XPR) | 1 023 671 | 0.00024 | **0** |
|
||||||
|
| all three combined, hashed against all 42 | 1 204 976 | 0.00028 | **0** |
|
||||||
|
| `printf`-style substitution: 49 139 format strings × 80 numeric substitutions | 161 200 | 0.00004 | **0** |
|
||||||
|
| hand-built guess list (`main`/`Stage`/`Mission`/`ReadyRoom`/… × prefixes × decorations × numbers) | 506 520 | 0.00012 | **0** |
|
||||||
|
| two-token composition over a vocabulary mined from disc + exe identifiers (`t1 + sep + t2`, sep ∈ `"" _ -`) — this is what `idxd_unnamed_keys.py crack --pe …` runs | 3.48 × 10⁸ | 0.081 | **0** |
|
||||||
|
| the same, with each family's forced digit suffix appended | 1.18 × 10¹⁰ across the sweep | 2.8 total | 1 (`"Cj-format 01"` — noise, and the same string for all six challenge keys, i.e. **one** false prefix) |
|
||||||
|
| meet-in-the-middle exhaustive preimage, `[A-Za-z0-9_]`, ≤ 6 chars before the known suffix | 6.3 × 10¹⁰ | ~15 | noise only |
|
||||||
|
|
||||||
|
The last row is the important one. Exhaustive search **succeeds** — it is not
|
||||||
|
that the search is broken. Fed `tag_hash("Stage01")` it returns
|
||||||
|
`['F4D_kl01', 'Stage01', 'be_zT01', 'cd_Yu01']`: the right answer plus three
|
||||||
|
collisions, which is what `63^5 / 2^32 ≈ 0.2` … `63^6 / 2^32 ≈ 15` predicts. Fed
|
||||||
|
the real target `0x87A349E7` with suffix `"01"` it returns **nothing**, so the
|
||||||
|
prefix is **longer than 6 characters** over that alphabet.
|
||||||
|
|
||||||
|
And that is where it stops being useful. Measured, not extrapolated: at 7
|
||||||
|
characters target `0xA40C6DF7` has **1 176** preimages (`'1T1E1B6'`,
|
||||||
|
`'24A7O0G'`, `'4quqkkC'`, …). At 8 the expectation is ≈ 58 000. A hash with a
|
||||||
|
24-bit modulus simply does not have enough entropy to name an 8+ character
|
||||||
|
identifier uniquely, so **no amount of compute recovers these names from the hash
|
||||||
|
alone** — only a corpus containing the actual string can, and the disc does not
|
||||||
|
contain it.
|
||||||
|
|
||||||
|
### 🔴 Refuted along the way (kept, not deleted)
|
||||||
|
|
||||||
|
* **"The symbol is the filename stem, in some case/decoration."** Refuted
|
||||||
|
directly: `tag_hash` of `stage01`, `Stage01`, `STAGE01`, `stage_01`,
|
||||||
|
`mission01`, and 30-odd relatives is not `0x87A349E7`.
|
||||||
|
* **"The symbol is `<common prefix> + <filename stem> + <number>`."** Refuted
|
||||||
|
*algebraically*, without searching. If `name = A + "stage01"` and
|
||||||
|
`name = A + "challenge01"` share one `A`, then `A`'s byte sum is pinned twice
|
||||||
|
and the two values must agree. They do not, for lowercase, Capitalised or
|
||||||
|
UPPER stems, with or without a `_`/`-` separator — 9 pairs tested, 9
|
||||||
|
contradictions.
|
||||||
|
* **"The name is somewhere in the `.isb` files."** Refuted: the `.isb` payloads
|
||||||
|
contain no ASCII symbol names at all. They reference symbols purely by hash —
|
||||||
|
which is *why* the prescan link map exists.
|
||||||
|
* **"The hash constants might be in the executable next to a name table."**
|
||||||
|
Refuted: none of the 42 values occurs in the `.pe` image in either endianness.
|
||||||
|
* **"IXUD might hold more hash-keyed nameless fields."** Refuted by measurement:
|
||||||
|
534 IXUD objects, 624 488 field entries, **8** distinct unnamed tags — all of
|
||||||
|
them ordinals `0…7` (the weapon `CATEGORY_DESC` list). Zero hash-shaped.
|
||||||
|
* **⚠️ "There are ~504 keys to crack."** Withdrawn — 504 is the *entry* count.
|
||||||
|
See above.
|
||||||
|
|
||||||
|
## Coverage / limits
|
||||||
|
|
||||||
|
- The census covers every IDXD object the disc has (7 750). `sound.pak` holds no
|
||||||
|
IDXD objects and `dat/movie/*.wmv` and `hidden/resource3d/*.xpr` are not
|
||||||
|
containers; strings from `resource3d` were nevertheless folded into the corpus
|
||||||
|
attack.
|
||||||
|
- The 42 targets are fully characterised as *references*; their *spelling* is
|
||||||
|
unrecovered and, per the arithmetic above, unrecoverable from the disc.
|
||||||
|
|
||||||
|
## Evidence log (append-only; newest last)
|
||||||
|
|
||||||
|
- `2026-08-26` — Disc-wide walk of 7 750 IDXD objects / 2 757 039 field entries
|
||||||
|
with `tag_hash` self-check at 1 271 462/1 271 462. Census → confidence
|
||||||
|
`CONFIRMED` for the counts and for the ordinal/hash split.
|
||||||
|
- `2026-08-26` — 42 hash-shaped keys localised to six copies of
|
||||||
|
`<lang>\script\ID.tbl`; cross-checked against the `.isb` bytecode, where 41 of
|
||||||
|
42 appear as little-endian call targets forming a consistent call graph →
|
||||||
|
`CONFIRMED` that they are ISL script-symbol hashes.
|
||||||
|
- `2026-08-26` — Key-delta arithmetic pins the trailing digits of 30 of the 42
|
||||||
|
names → `CONFIRMED` (forced by the hash's polynomial structure, three
|
||||||
|
independent families agreeing).
|
||||||
|
- `2026-08-26` — Seven corpus/composition/brute-force attacks, none producing a
|
||||||
|
hit above the noise floor; exhaustive search validated on a known name and
|
||||||
|
shown to return nothing at ≤ 6 characters → the names are **not recovered**,
|
||||||
|
and the negative result is quantified rather than asserted.
|
||||||
|
|
||||||
|
## Open questions / what would raise confidence
|
||||||
|
|
||||||
|
- **The 12 unconstrained names** (`function.isb` ×5, `function_tutorial.isb` ×6,
|
||||||
|
`main.isb` ×1) have no digit structure to exploit at all.
|
||||||
|
- **What would actually work:** a build artefact containing ISL source or a symbol
|
||||||
|
list (none on the retail disc); or observing the game *construct* one of these
|
||||||
|
strings at runtime — `PrepareScript` builds the table from `.isb` contents, but
|
||||||
|
whatever `main.isb`'s root symbol is called, the name existed only in the
|
||||||
|
compiler's input. A `--mem-watch` on the ReadyRoom lookup path would show
|
||||||
|
whether any caller ever passes a literal name rather than a precomputed hash;
|
||||||
|
that is the one remaining honest route and it was **not** attempted here (this
|
||||||
|
was a static-only investigation).
|
||||||
|
- **`OFFSET`'s unit** — base-36 is settled, what it counts is not.
|
||||||
|
- The `.isb` instruction encoding is only sketched here (a length byte at
|
||||||
|
`byte[2]`, call payloads leading with the target hash). It is not the `.ssb`
|
||||||
|
ISL bytecode documented elsewhere and deserves its own note.
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
Status: ✅ table encoding, calling convention and the `ScriptPhase` state layout;
|
Status: ✅ table encoding, calling convention and the `ScriptPhase` state layout;
|
||||||
✅ ~135 of 147 handlers characterised from the disassembly; 🟡 three resolved
|
✅ ~135 of 147 handlers characterised from the disassembly; 🟡 three resolved
|
||||||
only partially; ❔ the interpreter-command table is only partly recovered.
|
only partially; ❔ the interpreter-command table is only partly recovered.
|
||||||
|
✅ built-ins **26 / 29 / 101** are now named end to end (HP %, damage-taken %,
|
||||||
|
broadcast invulnerability); 🟡 **28** writes the matching damage-**dealt** field
|
||||||
|
but its one consumer is a single weapon path — see below.
|
||||||
|
|
||||||
Companion to [isl-bytecode](isl-bytecode.md) (the instruction encoding) and
|
Companion to [isl-bytecode](isl-bytecode.md) (the instruction encoding) and
|
||||||
[mission-phase-advance](../mission-phase-advance.md) (why phases hinge on these).
|
[mission-phase-advance](../mission-phase-advance.md) (why phases hinge on these).
|
||||||
@@ -168,56 +171,269 @@ words**: a descriptor offset and the message id.
|
|||||||
That fixes the id format as `0xED08 nn DE`, and the ids already known from other
|
That fixes the id format as `0xED08 nn DE`, and the ids already known from other
|
||||||
work fit it: opcode 514 → `00DE`, 803 → `07DE`, 999 → `0FDE`.
|
work fit it: opcode 514 → `00DE`, 803 → `07DE`, 999 → `0FDE`.
|
||||||
|
|
||||||
🟡 The pump's arm for `0xED0802DE` does **not** apply an effect — it walks the
|
✅ **The pump's arm for `0xED0802DE` applies no effect itself** — it walks the
|
||||||
unit's child list at `[unit+320]`/`[unit+324]` and **rebroadcasts** to each child
|
unit's child list at `[unit+320]`/`[unit+324]` and **rebroadcasts** to each child
|
||||||
as `0xED0902DE`. So `0xED08…` is the to-unit family and `0xED09…` the to-child
|
as `0xED0902DE`. So `0xED08…` is the to-group family and `0xED09…` the
|
||||||
one, and the terminal effect is one link further on. ❌ Not followed; 26/28/29
|
to-individual one. That link is now followed all the way to the effect — see
|
||||||
remain unnamed.
|
below.
|
||||||
|
|
||||||
### 🟡 Built-ins 26 / 28 / 29 are one family — and `damage_unit` looks mis-named
|
### ✅ Built-ins 26 / 28 / 29 RESOLVED — HP, damage dealt, damage taken
|
||||||
|
|
||||||
Method-diffing put the structure beyond doubt but did not reach the semantics.
|
The chain was followed end to end for all three, from the dispatch table to the
|
||||||
|
field write. Nothing here is inferred from shape; every hop was read.
|
||||||
|
|
||||||
| built-in | vtable slot | method | opcode | sites |
|
| built-in | vtable slot | method | opcode | cmd word | interp thunk | opcode handler | to-group msg | to-child msg | arm | writes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
| 26 `damage_unit` | 76 | `sub_8226ACD0` (67) | **800** | 97 |
|
| **26** | 76 | `sub_8226ACD0` | 800 | `0xAB0320BA` | `0x822FF118` | `sub_823008C8` | `0xED0802DE` | `0xED0902DE` | `0x82398E8C` | `[unit+532]` = **HP** |
|
||||||
| **28** | 84 | `sub_82268F98` (69) | **801** | 410 |
|
| **28** | 84 | `sub_82268F98` | 801 | `0xAB0321BA` | `0x822FF120` | `sub_823009B8` | `0xED0803DE` | `0xED0903DE` | `0x8239926C` | `[unit+672]` |
|
||||||
| **29** | 88 | `sub_822690B0` (69) | **802** | 164 |
|
| **29** | 88 | `sub_822690B0` | 802 | `0xAB0322BA` | `0x822FF128` | `sub_82300AA8` | `0xED0804DE` | `0xED0904DE` | `0x823994C0` | `[unit+676]` |
|
||||||
| 101 | 276 | `sub_822691C8` (78) | 802 (broadcast) | 133 |
|
|
||||||
|
|
||||||
**28 and 29 differ in two words only** — the opcode (`0x21BA` vs `0x22BA`) and a
|
The cmd word decodes as `0xAB` `opcode:16` `0xBA`, which is the same encoding
|
||||||
descriptor pointer 8 bytes apart. Otherwise instruction-identical. All three take
|
already recorded for 256 (`0xAB0100BA`), 513 (`0xAB0201BA`) and 996
|
||||||
`(unit, double)`.
|
(`0xAB03E4BA`) — `0x320/0x321/0x322` = 800/801/802.
|
||||||
|
|
||||||
26 differs from both by one guard: it rejects only state 3, while **28 and 29
|
Two links that the previous attempt did not have:
|
||||||
reject states 1 and 3** (2 = active, 1/3/4 = gone/dead/invalid).
|
|
||||||
|
|
||||||
#### The operand distributions separate them
|
* the **to-group pump is `sub_8232C4C0`** (vtable `0x820AF1FC` slot 6), whose
|
||||||
|
arms at `0x8232C824` / `0x8232CB48` / `0x8232CC40` each walk the child list and
|
||||||
|
re-post with the `0xED09…` id, copying the float from `[msg+36]` unchanged;
|
||||||
|
* the **to-individual handler is `sub_82398CC0`** (vtable `0x820B192C` slot 6),
|
||||||
|
the entity base class. Its `0xED0902DE` arm is reached by a `sub.`/`cmplwi 0x8200`
|
||||||
|
pair rather than a `lis`+`ori` compare, which is why a constant scan for
|
||||||
|
`0xED0902DE` finds only the *sender*. That is worth remembering: **a
|
||||||
|
subtract-based compare hides the constant from any scan that pairs
|
||||||
|
`lis` with `ori`.**
|
||||||
|
|
||||||
| built-in | n | distinct | range | most common |
|
#### ✅ 26 is `set_unit_hp_pct` — NOT `damage_unit`
|
||||||
|---|---|---|---|---|
|
|
||||||
| 26 | 97 | 7 | **[0, 100]** | **0 ×69**, 80 ×10, 100 ×6 |
|
|
||||||
| 28 | 410 | 13 | **[0, 2000]** | 200 ×116, 120 ×76, 300 ×74 |
|
|
||||||
| 29 | 164 | 6 | **[0, 100]** | **0 ×64**, 100 ×53, 50 ×38 |
|
|
||||||
|
|
||||||
26 and 29 are percentage-shaped; 28 is an absolute quantity an order of magnitude
|
`sub_8226ACD0` multiplies the double operand by the constant at `0x820C5688`,
|
||||||
larger.
|
which is **0.01** — so the operand is a percentage and the message carries a
|
||||||
|
fraction. `0x82398E8C` then does, in its own terms:
|
||||||
|
|
||||||
#### 🟡 `damage_unit` (26) is doubtful
|
```
|
||||||
|
new = min( max(def.HP * frac, 0.0), def.HP ) ; def = [unit+496]
|
||||||
|
[unit+532] = new
|
||||||
|
if new < FLT_EPSILON and old > 0: ; crossed to zero
|
||||||
|
[unit+688] = 0.0 ; [unit+1340] = 1 ; [unit+692] = def.Delay
|
||||||
|
```
|
||||||
|
|
||||||
**69 of its 97 calls pass 0.** Dealing zero damage is a no-op, so 71 % of the
|
Three independent facts pin the field:
|
||||||
call sites would do nothing. *Setting* a percentage-valued property to 0 is a
|
|
||||||
perfectly natural thing to do 69 times, and 29 has the same shape (0 ×64 of 164).
|
|
||||||
The existing name predates this session and is not withdrawn, but it should not
|
|
||||||
be relied on.
|
|
||||||
|
|
||||||
#### ❌ Where this stopped
|
* `[unit+496]` is the **unit definition** — the entity constructor
|
||||||
|
`sub_82393868` sets it from `sub_82348830` (the per-member definition
|
||||||
|
`std::map::find` already identified for built-in 15) at `0x82393BE4`;
|
||||||
|
* `[def+84]` is **`HP`** in
|
||||||
|
`crates/sylpheed-formats/data/unit_definition_layout.txt`, and the same
|
||||||
|
constructor immediately does `[unit+532] = [def+84]` at `0x82393BF0` — so
|
||||||
|
`+532` *is* current HP, seeded from the datasheet maximum;
|
||||||
|
* the crossing branch loads **`[def+584]` = `Delay`** into `[unit+692]` and sets
|
||||||
|
a flag — the destruction sequence. So a percentage of **0 destroys the unit**,
|
||||||
|
with the definition's own death delay.
|
||||||
|
|
||||||
The three commands' descriptors sit at `0x820A8D10` / `+8` / `+16`. Following
|
So the operand is *percent of maximum HP*, the write is absolute (a **set**, not
|
||||||
them lands on data pointing into `0x8210E5xx`, which is **below the disassembly
|
a subtract), and `damage_unit` is withdrawn: 100 heals to full, which no
|
||||||
DB's range** (it starts at `0x82150000`) and contains no code — so that route
|
"damage" primitive does.
|
||||||
does not reach an execute method. Reaching opcodes 800–802's semantics needs the
|
|
||||||
interpreter's command table, not the command objects.
|
#### ✅ 29 is `set_unit_damage_taken_pct` — `[unit+676]` scales incoming damage
|
||||||
|
|
||||||
|
`[unit+672]` and `[unit+676]` are **both initialised to 1.0** by the entity
|
||||||
|
constructor (`0x8239395C`/`0x82393964`, from the float at `0x8208583C`), so both
|
||||||
|
are multipliers whose neutral value is 1.0 and which built-ins 28/29 set to
|
||||||
|
`operand/100`.
|
||||||
|
|
||||||
|
`[unit+676]` has **three independent readers**, and every one of them multiplies
|
||||||
|
a damage amount immediately before it is subtracted from `[unit+532]`:
|
||||||
|
|
||||||
|
| reader | what it computes |
|
||||||
|
|---|---|
|
||||||
|
| `0x8237B20C` in `sub_8237B020` (craft subclass `0x820B0A3C` slot 6, the `0xED0200DE` damage arm) | `dmg = [msg+36] × def.ResistanceToPlayer × [unit+676]` |
|
||||||
|
| `0x823803A4` in `sub_823800A8` (another subclass's damage arm) | the same product |
|
||||||
|
| `0x82399FFC` in `sub_82398CC0` (the base class's continuous-damage arm) | `dmg = def.HP × globalRate × t × [unit+676]`, then `[unit+532] -= dmg` |
|
||||||
|
|
||||||
|
`def+128` is `ResistanceToPlayer` in the layout file, which is the second
|
||||||
|
corroboration that the object is the entity and the quantity is damage.
|
||||||
|
|
||||||
|
#### 🟡 28 sets `[unit+672]`, and its one consumer scales damage DEALT
|
||||||
|
|
||||||
|
The write is certain and the field's default (1.0) is certain. The **label** is
|
||||||
|
one link weaker than 29's, and this is the honest state:
|
||||||
|
|
||||||
|
`[unit+672]` is read in **exactly one place** in the image —
|
||||||
|
`0x8237A44C`, inside `sub_823785A0`, which is vtable `0x820B0A3C` **slot 1**,
|
||||||
|
i.e. the craft subclass's per-frame update. There it is copied into a 240-byte
|
||||||
|
projectile spawn record at `+100`; `sub_8238F4D0` turns that record into a
|
||||||
|
projectile with `[proj+300] = [rec+100]`; and `sub_8238FE10` builds the
|
||||||
|
`0xED0200DE` damage message with **`[msg+36] = [proj+300] × charge × globalScale`**
|
||||||
|
— the same `[msg+36]` that the readers above multiply by the *target's* `+676`.
|
||||||
|
|
||||||
|
So the two fields are the two ends of one damage product: **`+672` on the
|
||||||
|
shooter, `+676` on the target.** That is a tidy story and it is exactly the
|
||||||
|
shape that has produced wrong names in this file before, so it is 🟡 and not ✅.
|
||||||
|
|
||||||
|
❔ **What is not established**: that `+672` reaches *every* weapon. The one read
|
||||||
|
site is on a charged-shot path (`[unit+2244]` is a charge accumulator compared
|
||||||
|
against thresholds, `[unit+2268]` the resulting power), and the other damage
|
||||||
|
sender in the same family, `sub_82388FF8`, computes
|
||||||
|
`dmg = ammo.base × [proj+268] × k` with **no `+672` term at all**. Either the
|
||||||
|
craft update funnels all firing through this one spawn, or `+672` scales only
|
||||||
|
some weapons. Not settled.
|
||||||
|
|
||||||
|
#### ✅ The operand-range test discriminates the three
|
||||||
|
|
||||||
|
All three built-ins have the identical signature `(unit, double)` and the
|
||||||
|
identical `× 0.01` conversion, so an operand ceiling is a real measurement rather
|
||||||
|
than a coincidence:
|
||||||
|
|
||||||
|
| built-in | n | distinct | min | max | inside [0, 100] |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 26 | 97 | 7 | 0 | **100** | **97 / 97** |
|
||||||
|
| 29 | 164 | 6 | 0 | **100** | **164 / 164** |
|
||||||
|
| 28 | 410 | 13 | 0.1 | **2000** | 42 / 410 |
|
||||||
|
|
||||||
|
26 and 29 respect a hard 100 ceiling — they are percentages of something with a
|
||||||
|
natural maximum (HP; and "no more damage than normal"). 28 does not: it is a
|
||||||
|
free multiplier that reaches 20×. 🟡 Note that 29 therefore **only ever makes a
|
||||||
|
unit tougher**, never more fragile.
|
||||||
|
|
||||||
|
#### ✅ The craft cross-tab — the same test that settled built-in 15
|
||||||
|
|
||||||
|
Joining every call site to its squadron's craft through `UnitGroup_S<NN>.tbl`
|
||||||
|
resolves **671 of 671** sites (26 + 28 + 29), none unknown. Full dump in
|
||||||
|
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt).
|
||||||
|
|
||||||
|
**26** splits into three groups with nothing in between:
|
||||||
|
|
||||||
|
| group | craft (sites) | values |
|
||||||
|
|---|---|---|
|
||||||
|
| disposable objects | `e201_ISCMissile` 17, `n001_TTRL_Box` 16, `f202_Cargo` 9, `e015_Puppy(_2)` 10, `mn500_FloatingMine` 3, `e011_Attacker_B` 2 | **0, and only 0** |
|
||||||
|
| warships | `e106_Destroyer` 8, `f106_Destroyer` 8, `f105_Cruiser` 5, `e108_ASFrigate` 4, `e105_Cruiser` 2, `f101_Acropolis` 1 | 30 / 50 / 60 / 80 |
|
||||||
|
| the player's craft in a tutorial | `..._Player_Ttrl1/2`, `..._T_Ttrl` (8) | **100** ×7, 40 ×1 |
|
||||||
|
|
||||||
|
Scripted removal of props, pre-damaged capital-ship spawns, and healing the
|
||||||
|
player to full at the start of a tutorial section — one field, three uses, and
|
||||||
|
no other reading covers all three.
|
||||||
|
|
||||||
|
**29** is dominated by the ships the mission must protect:
|
||||||
|
|
||||||
|
| craft (sites) | values |
|
||||||
|
|---|---|
|
||||||
|
| player + wingman `DeltaSaber` variants (`_T`, `_W`, `_A`, `_Player`) 113 | **0** and **100**, alternating |
|
||||||
|
| `n001_TTRL_Box` 9 | **0** only |
|
||||||
|
| escorted TCAF hulls — `f202_Cargo` 14, `f106_Destroyer` 2, `f105_Cruiser` 2, `f104_Battleship` 1, `f102_LightCarrier` 1, `f101_Acropolis` 1 | 50 / 75 |
|
||||||
|
| named ADAN aces — `e001_Elan_GR`, `_GR_Violeta` 6; `e010_Attacker_S` 18 | 40 / 50 |
|
||||||
|
|
||||||
|
**28** stratifies by combat class and is applied almost only to ADAN craft:
|
||||||
|
|
||||||
|
| class | craft (sites) | value |
|
||||||
|
|---|---|---|
|
||||||
|
| named bosses | `e003_ElanPlus_Margras` 4, `e005_ElanTypeQ_Margras` 1 | **2000** |
|
||||||
|
| named aces | `e001_Elan_GR(_Violeta)` 6, `e013_ElanPlus_Taskent` 2 | **1000** |
|
||||||
|
| elite fighters | `e004_ElanPlus_N` 19, `e009_Phantom` 16, `e002_Elan_N` 9, `e011_Attacker_B_HF` 3 | **500–600** |
|
||||||
|
| line fighters | `e001_Elan` 24, `e010_Attacker_S` 21, `e011_Attacker_B` 6 | 200–300 |
|
||||||
|
| capital hulls | `e106_Destroyer` 105, `e104_Carrier` 55, `e105_CruiserEX` 52, `e102_BattleshipEX` 26 | 100–300 |
|
||||||
|
| background props | `f106_Destroyer_Inv`, `f102_LightCarrier_Inv`, `f101_Acropolis`, `f105_Cruiser` (5) | **0.1** |
|
||||||
|
|
||||||
|
A monotone boss > ace > elite > line > prop ordering is what a firepower knob
|
||||||
|
looks like. 🟡 It is also what several other knobs would look like, which is why
|
||||||
|
the name stays PROBABLE and rests on the disassembly link above rather than on
|
||||||
|
this table.
|
||||||
|
|
||||||
|
#### ✅ The idiom that ties all three together
|
||||||
|
|
||||||
|
The per-unit setup block that follows every deployment reads, e.g. Stage 06
|
||||||
|
phase 3 at `0x0119AC` (from
|
||||||
|
[`data/isl-builtins-26-28-29-sites.txt`](../data/isl-builtins-26-28-29-sites.txt)):
|
||||||
|
|
||||||
|
```
|
||||||
|
activate_unit(TCN001) set_group_speed(TCN001, 400) 29(TCN001, 0)
|
||||||
|
activate_unit(TCN002) set_group_speed(TCN002, 400) 29(TCN002, 0) 28(TCN002, 50)
|
||||||
|
activate_unit(TCN003) set_group_speed(TCN003, 400)
|
||||||
|
... 26(TCN306, 50)
|
||||||
|
```
|
||||||
|
|
||||||
|
`15 / 29 / 28` is a **speed / toughness / firepower trio**, each a percentage
|
||||||
|
override of the craft's datasheet, applied immediately after `activate_unit`;
|
||||||
|
`26` joins it wherever a unit should arrive pre-damaged.
|
||||||
|
|
||||||
|
And the scripted-drama use is unambiguous. Stage 06 makes the wingman flight
|
||||||
|
TCN002 invulnerable at phase start (`29(TCN002, 0)`), then walks its HP down as
|
||||||
|
the voice lines fire:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x01A7B0 26(TCN002, 80) -> request_script_message(MSG_VOICE_D_065)
|
||||||
|
0x01AAA0 26(TCN002, 50) -> request_script_message(MSG_VOICE_D_069)
|
||||||
|
0x01ACEC 26(TCN002, 30)
|
||||||
|
```
|
||||||
|
|
||||||
|
You would only combine those two built-ins that way if they meant exactly "this
|
||||||
|
unit cannot be hurt by combat" and "set this unit's HP to N %".
|
||||||
|
|
||||||
|
#### ✅ Refutation attempts, and what they found
|
||||||
|
|
||||||
|
Recorded because two of them turned into confirmations.
|
||||||
|
|
||||||
|
* **"26 kills a unit the mission still needs."** Over all 28 stages there are
|
||||||
|
200 (stage, unit) pairs where a predicate on `u` follows a `26(u, 0)` in file
|
||||||
|
order. Every one inspected is a `unit_alive` / `unit_state` / `hp_pct_test`
|
||||||
|
**poll waiting for that death** — which is what a scripted kill implies, not a
|
||||||
|
contradiction. Not a counterexample, but the count is recorded rather than
|
||||||
|
hidden.
|
||||||
|
* **"29 makes a unit invulnerable that the player is required to destroy."**
|
||||||
|
The nine `n001_TTRL_Box` targets in Stage 28 do get `29(box, 0)`. They are
|
||||||
|
then destroyed by **`26(box, 0)`** later in the same file — nine boxes, nine
|
||||||
|
pairs, invulnerability first (offsets 0x2E4C–0x48C8) and the scripted removal
|
||||||
|
second (0x50F8–0x5FA0). The tutorial target cannot be shot down; the script
|
||||||
|
removes it when the lesson ends. The counterexample became a confirmation.
|
||||||
|
* **"28 is applied to something with no weapons."** Of the 35 craft classes it
|
||||||
|
touches, none is an unarmed prop — no asteroid, box, cargo or mine ever
|
||||||
|
receives 28, while 26 and 29 both do. The only near-miss is the four
|
||||||
|
`…_Inv` / background hulls at 0.1, i.e. deliberately harmless.
|
||||||
|
* **"`+672`/`+676` have another consumer that contradicts damage."** Searched
|
||||||
|
every `lfs`/`stfs`/`addi`/`lfsx` reference to offsets 672 and 676 across the
|
||||||
|
whole `.text`, then filtered by base register. **No further entity-side reader
|
||||||
|
of either exists.** ⚠️ The search also produced the trap this file keeps
|
||||||
|
warning about: `sub_8238E0F0` writes `676(r3)` and `sub_82389558` reads
|
||||||
|
`672(r31)`, but in both `r3`/`r31` is a **projectile**, which has its own
|
||||||
|
fields at those offsets. Three of the five "extra readers" the raw offset
|
||||||
|
search returned were that mistake.
|
||||||
|
* **What I could not refute and could not confirm**: that `+672` reaches every
|
||||||
|
weapon (see the ❔ above).
|
||||||
|
|
||||||
|
#### ✅ Bonus: built-in 101 is `all_units_invulnerable`
|
||||||
|
|
||||||
|
`sub_822691C8` is 29's **broadcast** twin — same descriptor `0x820A8D20`, same
|
||||||
|
opcode 802 — but it takes **no operand**: the float it sends is the constant at
|
||||||
|
`0x8209FD28`, which is **0.0**, and it loops over the phase's whole unit array
|
||||||
|
posting `0xED0804DE` to each live entry.
|
||||||
|
|
||||||
|
The usage confirms it. All **133** sites sit in one fixed phase-teardown idiom,
|
||||||
|
with no exceptions:
|
||||||
|
|
||||||
|
```
|
||||||
|
builtin116(0) -> builtin101 -> reset_phase_threads -> timer_stop -> clear_flag(-1) -> builtin118
|
||||||
|
```
|
||||||
|
|
||||||
|
133/133 preceded by 116 and 133/133 followed by 100. Freezing all damage is
|
||||||
|
exactly the first step of tearing a phase down, and no other reading of "post
|
||||||
|
0.0 to every unit" fits a teardown.
|
||||||
|
|
||||||
|
#### ❌ Two corrections to this file's earlier entries
|
||||||
|
|
||||||
|
* **The state guards were mis-stated.** Read directly: **26 rejects states 3 and
|
||||||
|
4**; **28 and 29 reject 1, 3 and 4**. This file said 26 rejects "only state 3"
|
||||||
|
and 28/29 "states 1 and 3". So 26 alone will still act on a state-1 unit.
|
||||||
|
* **"28 is an absolute quantity, 26 and 29 are percentage-shaped"** was half
|
||||||
|
right for the wrong reason. All three are percentages of *something*; only 26
|
||||||
|
and 29 are percentages of a thing with a ceiling.
|
||||||
|
|
||||||
|
#### 🗒️ The route that failed, kept
|
||||||
|
|
||||||
|
The three commands' descriptors at `0x820A8D10` / `+8` / `+16` are **vtables**,
|
||||||
|
not data: slot 0 of each is `0x82301C20` and slot 1 points into `0x8210E5xx`,
|
||||||
|
below the disassembly DB's range. Chasing them still leads nowhere. The route
|
||||||
|
that works is the one this file already recommended — the **interpreter command
|
||||||
|
table** → `0x822FF118/120/128` → the three opcode handlers — and from there the
|
||||||
|
**unit-message pump**, which is the part that was missing.
|
||||||
|
|
||||||
### ✅ Built-in 108 is `deploy_squadron_ex` — `deploy_squadron` plus a `1 << n` selector
|
### ✅ Built-in 108 is `deploy_squadron_ex` — `deploy_squadron` plus a `1 << n` selector
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
# `REGN` — a per-map spatial grid (and `MCOL` beside it)
|
# `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it)
|
||||||
|
|
||||||
**Status: ✅ `CONFIRMED` for the header**, which self-checks on all 11 objects on
|
**Status: ✅ `CONFIRMED`.** A `REGN` object is a **tetrahedral navigation mesh
|
||||||
the disc. ❔ the four data sections are undecoded. **New to this corpus** — no
|
of the whole play volume** — vertices, tetrahedra, faces with full adjacency,
|
||||||
document mentioned `REGN`, `MCOL` or `hidden/MiscBin.pak` before 2026-08-24.
|
per-tetrahedron portal costs — plus a uniform grid for point location. The
|
||||||
|
decode is complete except for two small fields; see
|
||||||
|
[the 2026-08-26 section](#-2026-08-26--the-reader-found-regn-is-a-tetrahedral-navigation-mesh),
|
||||||
|
which supersedes the offsets in the older sections below (they were read
|
||||||
|
**16 bytes early** — the loader's fixup base is `chunk + 0x10`). The older
|
||||||
|
sections are kept because their refutations are still instructive.
|
||||||
|
|
||||||
|
**New to this corpus** — no document mentioned `REGN`, `MCOL` or
|
||||||
|
`hidden/MiscBin.pak` before 2026-08-24.
|
||||||
|
|
||||||
## Where it is
|
## Where it is
|
||||||
|
|
||||||
@@ -131,23 +139,6 @@ thing is indexed *by*.
|
|||||||
partition and no more; the sections are unread. Treat this as the location of the
|
partition and no more; the sections are unread. Treat this as the location of the
|
||||||
world's spatial data, not as the wave table.
|
world's spatial data, not as the wave table.
|
||||||
|
|
||||||
> ❌ **SETTLED 2026-08-26 — `REGN` is not the wave scheduler, and cannot be.**
|
|
||||||
> It is a **tetrahedral navigation mesh**: vertices, faces with plane equations
|
|
||||||
> and adjacency, tetrahedra with portal costs between their face pairs, and a
|
|
||||||
> grid that indexes which tetrahedra fall in each cell. Every section is
|
|
||||||
> accounted for by that structure. There is nowhere in it for "what spawns, when,
|
|
||||||
> on what trigger" to live — no time field, no unit reference, no trigger.
|
|
||||||
>
|
|
||||||
> The hedge above was right to keep the reading provisional, but the reasoning
|
|
||||||
> it hedged ("a grid is what a scheduler would be indexed by") was a **guess from
|
|
||||||
> shape**, and the shape belonged to pathfinding. Worth noting because that guess
|
|
||||||
> is what pointed this whole investigation at `REGN` in the first place.
|
|
||||||
>
|
|
||||||
> The wave-scheduler search should now treat `REGN` as **excluded**, not as
|
|
||||||
> unread. The arrival timetable already found in `Route_S<NN>.tbl` — keyframed
|
|
||||||
> `(time, quat, pos)` per squadron per phase, with `t` in seconds — remains the
|
|
||||||
> only located part of that mechanism.
|
|
||||||
|
|
||||||
## ✅ Update: `REGN` is a stage's **MapPath**
|
## ✅ Update: `REGN` is a stage's **MapPath**
|
||||||
|
|
||||||
The per-stage definition record (see
|
The per-stage definition record (see
|
||||||
@@ -391,119 +382,338 @@ object in the executable and watch which fields it dereferences. That is static
|
|||||||
PE work (`/work/*.pe`, offset = VA − 0x82000000) of the same kind that cracked
|
PE work (`/work/*.pe`, offset = VA − 0x82000000) of the same kind that cracked
|
||||||
the `.slb` packing phase, and it is the honest next step rather than a
|
the `.slb` packing phase, and it is the honest next step rather than a
|
||||||
twenty-first correlation.
|
twenty-first correlation.
|
||||||
|
---
|
||||||
|
|
||||||
## ⚠️ Open conflict with `auto/regn-reader` over the `+0x10` base
|
# ✅ 2026-08-26 — the reader found: `REGN` is a **tetrahedral navigation mesh**
|
||||||
|
|
||||||
**2026-08-26.** Branch `auto/regn-reader` decodes `REGN` as a **tetrahedral
|
The previous section closed with "find what reads a `REGN` object in the
|
||||||
navigation mesh** and reports the cell→geometry link solved, with strong checks
|
executable and watch which fields it dereferences". That worked, and it did not
|
||||||
(faces passing through 3 of 4 tet vertices, 253 722/253 722; portal cost equal to
|
need the consumer: the **deserialiser** answers the question, because the file
|
||||||
the distance between face centroids, 380 460/380 460). Its load-bearing structural
|
carries its own pointer map and the deserialiser tells you how to read it.
|
||||||
claim is that the `POF0` fixup base is **`chunk + 0x10`**, and therefore that
|
|
||||||
"every offset previously recorded on that page was read 16 bytes early" — offered
|
|
||||||
as the reason the ~20 correlation tests on this page returned chance.
|
|
||||||
|
|
||||||
**I could not reproduce that as stated, on the one thing here that is
|
## ✅ The reader — `sub_82465110` / `sub_82465138` / `sub_82465200`
|
||||||
independently checkable.** Re-reading the plane list at both bases:
|
|
||||||
|
|
||||||
| | records | unit normals |
|
Three functions, all in the resource module around `0x82460000`:
|
||||||
|---|---|---|
|
|
||||||
| section at `chunk + offset` (what this page used) | 133 573 | **133 573 (100.00 %)** |
|
|
||||||
| section at `chunk + 16 + offset` | 133 573 | **0 (0.00 %)** |
|
|
||||||
|
|
||||||
And the plane identity `n·p + d = 0` holds to float round-off at the **unshifted**
|
```
|
||||||
base. A 16-byte shift destroys it completely. So for *this* record the unshifted
|
sub_82465110 find_pof0(chunk)
|
||||||
reading is right, and the blanket statement is not.
|
82465110 lwz r11, 4(r3) ; datasize
|
||||||
|
82465118 add r11, r11, r3
|
||||||
|
82465120 addi r3, r11, 16 ; -> chunk + 16 + datasize
|
||||||
|
82465124 lwz r11, 0(r3)
|
||||||
|
82465114 lis r10, 0x504F / 8246511c ori r10, r10, 0x4630 ; 'POF0'
|
||||||
|
82465128 cmplw cr6, r11, r10
|
||||||
|
8246512c beqlr cr6 ; else return 0
|
||||||
|
```
|
||||||
|
|
||||||
🟡 **The likely reconciliation is bookkeeping, not disagreement.** The other
|
```
|
||||||
branch describes a 48-byte **face** record whose plane fields sit at a different
|
sub_82465138 relocate_chunk_chain(chunk)
|
||||||
intra-record offset; `chunk+16` with the plane at `+0` addresses the same bytes as
|
82465164 lbz r11, 8(r31) / clrlwi r11,r11,31 ; already-relocated bit
|
||||||
`chunk+0` with the plane at `+16`, which is exactly what this page uses. If so
|
82465178 bl 0x82465110 ; find the POF0 chunk
|
||||||
both readings are correct and only the origin convention differs — but that is a
|
82465194 addi r4, r11, 16 ; POF0 payload
|
||||||
guess, and I am not adopting either page's wording until it is checked.
|
82465198 lwz r5, 4(r11) ; POF0 payload size
|
||||||
|
8246519c addi r3, r31, 16 ; ← THE FIXUP BASE = chunk + 0x10
|
||||||
|
824651a0 bl 0x82465200
|
||||||
|
824651a8 ori r11, r11, 0x1 / stb r11, 8(r31) ; set the bit
|
||||||
|
```
|
||||||
|
|
||||||
**Also confirmed here, incidentally**: my section-0 point test passes at **100 %
|
```
|
||||||
at both bases**, so it never had the power to distinguish them. That test could
|
sub_82465200 apply_pof0(base=r3, table=r4, size=r5)
|
||||||
not have caught a 16-byte error and should not be cited as if it validated the
|
82465224 clrrwi r8, r10, 6 ; top two bits of the lead byte select
|
||||||
offsets.
|
82465228 cmplwi cr6, r8, 0x40 ; 0x40 → 6-bit delta, 1 byte
|
||||||
|
82465230 cmplwi cr6, r8, 0x80 ; 0x80 → 14-bit delta, 2 bytes
|
||||||
|
82465238 cmplwi cr6, r8, 0xC0 ; 0xC0 → 22-bit delta, 3 bytes
|
||||||
|
82465258 add r11, r10, r11 ; running WORD index, never reset
|
||||||
|
8246525c slwi r8, r11, 2
|
||||||
|
82465260 lwzx r10, r8, r3 ; slot = base[word]
|
||||||
|
82465264 cmplwi cr6, r10, 0x0
|
||||||
|
8246526c add r10, r10, r3 ; *slot += base (skipped when *slot == 0)
|
||||||
|
82465270 stwx r10, r8, r3
|
||||||
|
```
|
||||||
|
|
||||||
❔ Until the conflict is resolved, treat the plane/face layout on this page as
|
Two things follow, and both are load-bearing:
|
||||||
verified by its own arithmetic and the tetrahedral decode on `auto/regn-reader`
|
|
||||||
as verified by its own — and do not merge the two offset conventions without
|
|
||||||
re-running one check from each.
|
|
||||||
|
|
||||||
## ✅ RESOLVED — the base IS `chunk + 0x10`, and my section-0 offsets were wrong
|
1. **The fixup base is `chunk + 0x10`, not `chunk + 0`** (`82465198`:
|
||||||
|
`addi r3, r31, 16`). Every stored "offset" in a `REGN` file is relative to
|
||||||
|
file offset `0x10`, and every **record origin** on this page above is
|
||||||
|
therefore 16 bytes early.
|
||||||
|
2. **The `POF0` table is an exact list of which words are pointers.** It is not
|
||||||
|
a heuristic — it is the data the retail loader itself walks. Decoding it
|
||||||
|
gives the pointer graph directly, with no guessing.
|
||||||
|
|
||||||
**2026-08-26, same day.** The conflict above is settled, and it settles against
|
### 🔴 A correction to how I first stated point 1
|
||||||
me on the boundary while leaving the plane arithmetic intact.
|
|
||||||
|
|
||||||
**The plane fields were never in dispute.** For object `3506e972`, the other
|
The first version of this section said "everything on this page above was read
|
||||||
branch's face record 0 begins at `0x1c700`, and its plane normal begins at the
|
16 bytes early", which is **too broad and I withdraw it**. For the plane fields
|
||||||
same address; my `chunk + offset_at_0x78 + 16` gives `0x1c700` too. **Same bytes,
|
it is *false*: the older reading put the section at `chunk + offset` and the
|
||||||
different bookkeeping** — exactly the reconciliation guessed above. The
|
normal at intra-record `+16`, which addresses **exactly the same bytes** as
|
||||||
`n·p + d = 0` result stands unchanged.
|
`chunk + 0x10 + offset` with the normal at `+0`. Its unit-normal and
|
||||||
|
`n·p + d = 0` results were measuring real planes and stand unchanged. On
|
||||||
|
`3506e972`, both conventions put face record 0's normal at file offset
|
||||||
|
**`0x1c700` (116 480)** = `0x10 + 0x1c6f0`, where the header word at `0x78` is
|
||||||
|
`0x1c6f0`.
|
||||||
|
|
||||||
**The base is `chunk + 0x10`**, on evidence that has power where my tests did
|
What is genuinely wrong is the **record boundary**, and it matters for one
|
||||||
not:
|
reason: it decides which 48-byte record the four integer words belong to — and
|
||||||
|
those integer words are the face adjacency, i.e. precisely the thing the
|
||||||
|
correlation search was hunting for. It also mis-resolves every stored pointer
|
||||||
|
by 16 bytes, twice over along the cell → item → ref → tetrahedron chain, which
|
||||||
|
is the mechanical reason that chain never landed on anything.
|
||||||
|
|
||||||
* the loader does it: `8246519c addi r3, r31, 16`;
|
### ✅ Which tests actually decide the origin — and which have no power
|
||||||
* at `+0x10` the six `POF0`-relocated slots land exactly on `0x70`–`0x84`, the six
|
|
||||||
section pointers. At `+0` they would land on `0x60`–`0x74` — relocating the
|
|
||||||
`u16` **counts** and leaving two section pointers unrelocated. Not merely
|
|
||||||
wrong, non-functional;
|
|
||||||
* section-0 record 0 reads as a bbox corner at `+0x10` and as garbage at `+0`.
|
|
||||||
|
|
||||||
### ❌ And my point-list evidence was vacuous
|
Most of the numbers on this page are **content** tests and address the same
|
||||||
|
bytes under either convention, so they cannot settle the origin. Stated
|
||||||
|
plainly, because the same trap caught the point-in-bbox test:
|
||||||
|
|
||||||
I reported "13 467/13 467 section-0 points lie inside the bbox" as the check that
|
| test | discriminates the base? |
|
||||||
made section 0 a decode. Read at the correct base, record 0 of four objects is:
|
|
||||||
|
|
||||||
base +0 (1.4e-41, 5.3e-41, 0) (2.2e-40, 2.7e-40, 0)
|
|
||||||
base +16 (-50000,-50000,-50000) (-250000, 250000, -250000)
|
|
||||||
|
|
||||||
So my offsets were 16 bytes early throughout. **Only 11 of 13 467 points read as
|
|
||||||
denormal at the wrong base** — the rest were still plausible coordinates inside
|
|
||||||
the box, because a 16-byte shift within a packed array of `f32` triples yields
|
|
||||||
*other floats from the same array*. That is the general form of the failure and
|
|
||||||
it is worth stating plainly:
|
|
||||||
|
|
||||||
> **A containment test cannot detect a shift inside a homogeneous array.** The
|
|
||||||
> shifted values are drawn from the same distribution as the correct ones, so the
|
|
||||||
> test passes at 100 % either way. It is not a weak check — for this class of
|
|
||||||
> error it is no check at all.
|
|
||||||
|
|
||||||
The conclusion "section 0 is a point list" happens to be right. The evidence I
|
|
||||||
gave for it was not evidence.
|
|
||||||
|
|
||||||
✅ **Adopt `chunk + 0x10`.** The plane *fields* need no change; the record
|
|
||||||
*boundary* moves, and the four integer words I described as "four zeros" move
|
|
||||||
with it into the record they describe — which is where the face adjacency, and
|
|
||||||
the answer to the cell→geometry question, was hiding. See branch
|
|
||||||
`auto/regn-reader` for the tetrahedral decode.
|
|
||||||
|
|
||||||
## ✅ Independent confirmation of the face decode
|
|
||||||
|
|
||||||
**2026-08-26.** I re-derived the other branch's central check with my own code,
|
|
||||||
my own reading of the record, and my own control — rather than accepting the
|
|
||||||
number.
|
|
||||||
|
|
||||||
Reading at base `chunk + 0x10`, taking the plane at intra-record `+0` and the
|
|
||||||
**three `u16` vertex indices at `+32`**, and asking whether each named vertex
|
|
||||||
satisfies its own face's plane equation:
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| faces where **all three** named vertices lie on the plane | **133 573 / 133 573 (100.00 %)** |
|
| section-5 targets on 96-byte boundaries; ref-array packing | ❌ no — `target − start` cancels the base |
|
||||||
| a **random** vertex lying on that plane (control) | 2 782 / 400 719 = **0.69 %** |
|
| face passes through 3 of 4 vertices; portal-cost identity | ❌ no — same absolute bytes either way |
|
||||||
|
| sphere reaches its cell (98.7 % vs 18–28 %) | ❌ no for the base; it discriminates the **axis order** |
|
||||||
|
| points inside the bbox (the older section) | ❌ no — a denormal reads as ≈ 0, which is inside a ±250 km box |
|
||||||
|
| **the loader's own `addi r3, r31, 16`** | ✅ direct |
|
||||||
|
| **where `POF0` says the header pointers are** | ✅ at `+0x10` the six relocated slots are exactly `0x70`–`0x84`, the six section pointers. At `+0` they would be `0x60`–`0x74`: the loader would relocate the `u16` **counts** and would leave the pointers at `0x78`–`0x84` unrelocated. Not merely wrong — non-functional. |
|
||||||
|
| **what section-0 record 0 contains** | ✅ at `+0x10` a real vertex (`-250000, 250000, -250000` on `d4c89536`); at `+0` the two remaining header pointer words read as floats — `2.2e-40, 2.7e-40, 0` |
|
||||||
|
| **the section-2 record boundary** | ✅ see below, 100.000 % against 0.000 % |
|
||||||
|
|
||||||
A 100 % against a 0.69 % baseline is not a fit — the face record genuinely names
|
### ✅ The record boundary, settled
|
||||||
the vertices of its own plane, and the `+0x10` base together with this layout is
|
|
||||||
right.
|
|
||||||
|
|
||||||
That also settles the record-boundary question from my side: the `u16`s that
|
A tetrahedron names face `f`; the plane at record `f` is independently verified
|
||||||
describe a plane sit **after** it in the same 48-byte record. My earlier reading
|
(three of the tet's four vertices lie on it, 100 %). The question is whether the
|
||||||
of "four zeros at the start of each record" was those integers seen 16 bytes out
|
adjacency block that says *"I am face `f`, my tetrahedra are A and B, at their
|
||||||
of position, one record late.
|
face slots i and j"* sits in the **same** 48-byte record as that plane.
|
||||||
|
|
||||||
This is worth having in the corpus independently of the other branch: two
|
Requiring `own == f` **and** `tet[A].faces[i] == f`:
|
||||||
different implementations, two different guesses at the intra-record layout, the
|
|
||||||
same 100 %.
|
adjacency block taken from record f (base + 0x10) : 100.000 %
|
||||||
|
adjacency block taken from record f + 1 (base + 0) : 0.000 %
|
||||||
|
|
||||||
|
11 of 11 objects, every face, both sides. The plane and the four integer words
|
||||||
|
that describe it are one record, and that record starts at `chunk + 0x10 +
|
||||||
|
offset + 48·f`.
|
||||||
|
|
||||||
|
`sub_82465138` is reached from exactly two callers, `sub_82461018` (vtable slot
|
||||||
|
9 of the class at `0x820af8bc`, the pak/resource file class) and
|
||||||
|
`sub_82461DE8`; both store `chunk + 16` as the object's data pointer, which
|
||||||
|
confirms the same `+0x10` base from the other side.
|
||||||
|
|
||||||
|
### 🔴 …and the magic is never compared
|
||||||
|
|
||||||
|
Worth recording because it is what sent the search to the fixup table:
|
||||||
|
**`'REGN'` and `'MCOL'` are not constructed anywhere in the executable.** The
|
||||||
|
title builds its four-character tags as `lis`/`ori` pairs, and a sweep of every
|
||||||
|
such pair recovers 156 tags — `RATC`, `T8aD`, `XBG7`, `IPFB`, `IDXD`, `LSTA`,
|
||||||
|
`POF0`, `PRMD`, `TBMD`, `WMV3` … — but neither of these. Nor does either half
|
||||||
|
appear as an immediate anywhere: `0x474E` (`'GN'`) occurs **zero** times in
|
||||||
|
1 865 751 instructions, and the flat PE image contains the byte string `REGN`
|
||||||
|
**zero** times. So no magic-dispatch site exists to find; `.rgn` objects are
|
||||||
|
handed to the map code by the stage record, not identified by their tag.
|
||||||
|
|
||||||
|
## ✅ The header is 0x70 bytes at `chunk + 0x10`, with **six** sections
|
||||||
|
|
||||||
|
Corrected, and re-derived from the `POF0` table, which relocates exactly six
|
||||||
|
header words (`0x70`, `0x74`, `0x78`, `0x7c`, `0x80`, `0x84`) on 11 of 11:
|
||||||
|
|
||||||
|
```
|
||||||
|
chunk +0x00 char[4] 'REGN'
|
||||||
|
+0x04 u32 data size (POF0 chunk at +0x04-value + 0x10)
|
||||||
|
+0x08 u8 flags; bit0 = "already relocated" (set by sub_82465138)
|
||||||
|
data = chunk + 0x10:
|
||||||
|
+0x00 f32[4] bbox min (w = 1.0)
|
||||||
|
+0x10 f32[4] bbox max
|
||||||
|
+0x20 f32[4] extent
|
||||||
|
+0x30 f32[4] cell size
|
||||||
|
+0x40 u32[4] grid dims
|
||||||
|
+0x50 u16[6] record counts, one per section
|
||||||
|
+0x60 ptr[6] section pointers
|
||||||
|
```
|
||||||
|
|
||||||
|
The old page had four sections because it read the last two pointers as data.
|
||||||
|
There are six, their strides are `12, 96, 48, 8, 32, 4`, and each section's
|
||||||
|
span divided by its stride is its `counts[]` entry exactly — 11 of 11, with the
|
||||||
|
only slack being 16-byte alignment padding and, for the face list, **two
|
||||||
|
all-zero sentinel records** (which is the unexplained "constant 96-byte tail"
|
||||||
|
from the section above: 2 × 48).
|
||||||
|
|
||||||
|
| # | contents | stride | count |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | vertices | 12 | `counts[0]` |
|
||||||
|
| 1 | **tetrahedra** | 96 | `counts[1]` |
|
||||||
|
| 2 | **faces** (plane + adjacency) | 48 | `counts[2]` |
|
||||||
|
| 3 | cell index, one per cell | 8 | `counts[3]` = cells |
|
||||||
|
| 4 | cell items, one per **occupied** cell | 32 | `counts[4]` |
|
||||||
|
| 5 | tetrahedron references | 4 | `counts[5]` |
|
||||||
|
|
||||||
|
## ✅ How a cell reaches its geometry — the question, answered
|
||||||
|
|
||||||
|
The `POF0` table places every pointer in the file, and there are only four
|
||||||
|
kinds. Verified on all 11 objects by `tools/re-capture/regn_decode.py verify`:
|
||||||
|
|
||||||
|
* the six header words, and nothing else in the header;
|
||||||
|
* **one pointer per occupied cell**, at section-3 record offset **`+4`**
|
||||||
|
— so a cell is `{ u32 count; item* }` and the *count* word is not a pointer;
|
||||||
|
* **exactly one pointer per section-4 record, at offset `+0x14`** — the 32-byte
|
||||||
|
cell item is `{ …, u32 n @+0x10, tetref* @+0x14, … }`;
|
||||||
|
* **every word of section 5**, all `counts[5]` of them.
|
||||||
|
|
||||||
|
So the chain is:
|
||||||
|
|
||||||
|
```
|
||||||
|
position ─▶ cell (x,y,z) index = (z·dimY + y)·dimX + x
|
||||||
|
─▶ sec3[index] {count, item*}
|
||||||
|
─▶ item {…, n @+0x10, refs* @+0x14, …}
|
||||||
|
─▶ refs[0 .. n) each a pointer into section 1
|
||||||
|
─▶ tetrahedron
|
||||||
|
```
|
||||||
|
|
||||||
|
Four independent checks, all 11 of 11 objects:
|
||||||
|
|
||||||
|
* **section-5 targets land on section-1 record boundaries** — every one of
|
||||||
|
**261 000** pointers is `sec1 + 96·k` with zero remainder;
|
||||||
|
* **the reference arrays are packed contiguously in cell order** —
|
||||||
|
`item[i].refs == sec5 + 4·Σ item[j<i].n`, exact, and the total equals
|
||||||
|
`counts[5]`;
|
||||||
|
* `counts[4]` equals the number of relocated section-3 pointers equals the
|
||||||
|
number of occupied cells;
|
||||||
|
* **the cell ordering is x-fastest, then y, then z.** A tetrahedron's bounding
|
||||||
|
sphere reaches the box of a cell that lists it in **98.7 – 100 %** of all
|
||||||
|
261 000 references; transposing the axes — the natural control — drops that
|
||||||
|
to **18 – 28 %**.
|
||||||
|
|
||||||
|
The list is a strict subset of "every cell the bounding sphere touches"
|
||||||
|
(Jaccard ≈ 0.4 – 1.0), i.e. the build used a tighter test than sphere-vs-box.
|
||||||
|
That is expected and is not claimed as decoded.
|
||||||
|
|
||||||
|
## ✅ Section 1 is a **tetrahedron** — 96 bytes
|
||||||
|
|
||||||
|
```
|
||||||
|
+0x00 f32[3] bounding-sphere centre
|
||||||
|
+0x0c f32 bounding-sphere radius
|
||||||
|
+0x10 u16[4] vertex indices → section 0
|
||||||
|
+0x18 u16[4] face indices → section 2
|
||||||
|
+0x20 6 × { f32 cost; f32 ❔ } one pair per face pair, in the order
|
||||||
|
(0,1) (0,2) (0,3) (1,2) (1,3) (2,3)
|
||||||
|
+0x50 u32 the record's own index
|
||||||
|
+0x54 u8[4] flags, values 0/1
|
||||||
|
```
|
||||||
|
|
||||||
|
**The check that makes this a decode, not a reading**: each of a tetrahedron's
|
||||||
|
four faces must pass through exactly three of its four vertices. Over
|
||||||
|
**253 722** (tetrahedron, face) pairs on the disc, the count of vertices lying
|
||||||
|
on the plane is **3 in 100.00 % of cases**. The control — the same test with a
|
||||||
|
randomly chosen face of the same object — gives 3 in **0.07 – 2.2 %**, and
|
||||||
|
touches no vertex at all 96 % of the time.
|
||||||
|
|
||||||
|
Two more, both 11 of 11: the bounding sphere encloses all four vertices in
|
||||||
|
100 % of the 63 410 records (with ~0.1 % padding), and the word at `+0x50` is
|
||||||
|
the record's own index in 100 %.
|
||||||
|
|
||||||
|
### ✅ The six floats at `+0x20` are the portal-graph edge costs
|
||||||
|
|
||||||
|
For face `i` of a tetrahedron, let `omit(i)` be the one vertex not on it. Then
|
||||||
|
|
||||||
|
cost[k] == | V[omit(i)] − V[omit(j)] | / 3 for the k-th pair (i<j)
|
||||||
|
|
||||||
|
which is exactly the **distance between the centroids of faces `i` and `j`** —
|
||||||
|
the A\* step cost of crossing the tetrahedron from one portal to another. It
|
||||||
|
holds to `1e-4` relative in **380 460 of 380 460** values, all 11 objects.
|
||||||
|
|
||||||
|
❔ The second float of each pair is unread. It is a genuine varied float, zero
|
||||||
|
in 8 – 26 % of slots. **🔴 Refuted**: "zero marks a hull edge" — `P(zero)` is
|
||||||
|
the same for edges on the hull as for interior ones (e.g. 2.9 % / 16.4 %
|
||||||
|
against 16.7 % / 64.0 % — no lift at all). A clearance width is the obvious
|
||||||
|
reading and is *not* established.
|
||||||
|
|
||||||
|
## ✅ Section 2 is a **face** — a plane plus the adjacency graph
|
||||||
|
|
||||||
|
```
|
||||||
|
+0x00 f32[3] unit normal
|
||||||
|
+0x0c f32 d
|
||||||
|
+0x10 f32[3] a point on the plane
|
||||||
|
+0x1c f32 1.0
|
||||||
|
+0x20 u16[3] the face's three vertex indices → section 0
|
||||||
|
+0x26 u16 the face's own index
|
||||||
|
+0x28 u16[2] the tetrahedra on either side; 0xFFFF = hull (no neighbour)
|
||||||
|
+0x2c u16[2] which of that tetrahedron's four face slots this is
|
||||||
|
```
|
||||||
|
|
||||||
|
The old page's plane reading was the same 16 bytes early, which is why it
|
||||||
|
reported "four zeros" at the front: those were the *previous* record's four
|
||||||
|
integer words, read as floats.
|
||||||
|
|
||||||
|
Verified over every face of every object (11 of 11, 100.00 % each):
|
||||||
|
|
||||||
|
* the word at `+0x26` is the record's own index;
|
||||||
|
* for each non-`0xFFFF` side, `tet[side].faces[slot[side]] == this face`;
|
||||||
|
* the face's three vertices are a subset of that tetrahedron's four.
|
||||||
|
|
||||||
|
And the Euler relation closes: `4·T == 2·(F − B) + B` exactly on every object
|
||||||
|
(e.g. `4 × 1172 = 4688`, `F = 2584`, `B = 480` hull halves).
|
||||||
|
|
||||||
|
## ✅ What a `REGN` object is
|
||||||
|
|
||||||
|
A stage's `MapPath` is a **tetrahedral navigation mesh of the whole play
|
||||||
|
volume**, plus a uniform grid for point location:
|
||||||
|
|
||||||
|
* vertices, tetrahedra, and faces form a conforming tet mesh with full
|
||||||
|
face adjacency and hull markers;
|
||||||
|
* each tetrahedron carries the six face-to-face traversal costs, so A\* over
|
||||||
|
the portal graph needs no geometry at query time;
|
||||||
|
* the uniform grid answers "which tetrahedra could contain this point" in O(1),
|
||||||
|
which is what a flight game needs to localise a ship into the mesh each frame.
|
||||||
|
|
||||||
|
The 100 km-cube smoke-test map (`e993b93e`, `counts = [8, 6, 18, …]`) is the
|
||||||
|
textbook **six-tetrahedron decomposition of a cube** — 8 corners, 6 tets, 18
|
||||||
|
distinct faces (24 faces less the 6 shared internally) — which is as strong a
|
||||||
|
confirmation of the reading as the statistics are.
|
||||||
|
|
||||||
|
## 🔴 Corrections to the sections above, kept rather than deleted
|
||||||
|
|
||||||
|
Every one of these was a real observation; each fails for the same single
|
||||||
|
reason, and that is worth having on the page.
|
||||||
|
|
||||||
|
| earlier claim | status |
|
||||||
|
|---|---|
|
||||||
|
| "four section offsets at `0x70`" | 🔴 there are **six**, `0x70`–`0x84` |
|
||||||
|
| "the first section offset is always `0x80`" | 🔴 the *value* is `0x80`; the section is at `0x90` — the base is `+0x10` |
|
||||||
|
| "`counts[4]` = occupied cells **+ 2**, unexplained" | 🔴 artefact of the base error; `counts[4]` = occupied cells exactly |
|
||||||
|
| "the constant 96-byte tail after the plane list" | ✅ explained: two all-zero sentinel face records |
|
||||||
|
| "record `+0x1c` is a bounding-sphere radius (ratio 1.001 on one object, 0.13–0.27 on the rest)" | 🟡 **half right** — `+0x0c` *is* a bounding-sphere radius, of the *tetrahedron*; the 1.001 was a coincidence of that map, where every tet shares the cube's circumsphere |
|
||||||
|
| "`(count, offset)` pairs point at leaf arrays of `count × 4` bytes" | 🟡 **right mechanism, wrong record** — it is the cell *item* at `+0x10`/`+0x14` pointing into section 5, not the cell index |
|
||||||
|
| "slots 8–11 are integers (denormal as float)" | ✅ correct, and they are the four vertex + four face `u16` indices |
|
||||||
|
| "section 2 records are 48 bytes, unit normal, `n·p + d = 0` in 133 573/133 573" | ✅ **stands** — the same bytes under either origin convention; only the record boundary moves, see the correction above |
|
||||||
|
| "section 0 points are inside the bbox, 13 467/13 467" | ⚠️ true but **uninformative** — it passes at either origin, because a denormal reads as ≈ 0 and 0 is inside the box |
|
||||||
|
| "slot 7 is a local scale, ~14× smaller than the inter-node distance" | ✅ correct as measured — it is a *tetrahedron's* bounding radius, which is exactly that small |
|
||||||
|
| "the BVH reading is refuted, no node sphere contains another" | ✅ stands, and is now explained: these are sibling tetrahedra, not a tree |
|
||||||
|
| "the cell payload holds section-1 indices" | ✅ stands — it holds a **pointer to an array of pointers**, and no index anywhere |
|
||||||
|
| "the static-correlation avenue is exhausted" | ✅ stands for correlations; the file's own fixup table was never a correlation |
|
||||||
|
|
||||||
|
## ❔ Still open
|
||||||
|
|
||||||
|
* the second float of each portal pair at tetrahedron `+0x24`, `+0x2c`, … ;
|
||||||
|
* the four flag bytes at tetrahedron `+0x54` (values 0/1);
|
||||||
|
* the exact predicate the tools used to decide cell membership (tighter than
|
||||||
|
sphere-vs-box);
|
||||||
|
* **the runtime consumer.** The deserialiser is found and proven; the code that
|
||||||
|
*queries* the grid is not. Searches that came up empty, so they are not
|
||||||
|
repeated: functions loading the header field groups off one base register
|
||||||
|
(only stack frames match); `vctsxs`/`vctuxs` float→int conversion (16
|
||||||
|
functions, only one outside the XDK ranges); and the magic-comparison route,
|
||||||
|
which cannot exist (above). The likely reason the field-offset search fails
|
||||||
|
is that a `float4`-aligned header is read with VMX loads, which carry no
|
||||||
|
useful displacement signature.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
`tools/re-capture/regn_decode.py` — standalone, stdlib only.
|
||||||
|
|
||||||
|
```
|
||||||
|
regn_decode.py list hidden/MiscBin.pak
|
||||||
|
regn_decode.py dump hidden/MiscBin.pak [name_hash]
|
||||||
|
regn_decode.py verify hidden/MiscBin.pak # every check quoted above
|
||||||
|
```
|
||||||
|
|
||||||
|
`verify` prints, per object: the `POF0` pointer-slot shape, the reference-array
|
||||||
|
packing, the face/vertex incidence with its random control, the sphere/cell
|
||||||
|
agreement with its transposed-axis control, and the portal-cost identity.
|
||||||
|
|||||||
@@ -149,3 +149,63 @@ into those five shapes.
|
|||||||
|
|
||||||
Two incidental facts fall out: **4 banks run at 44 100 Hz** where everything else
|
Two incidental facts fall out: **4 banks run at 44 100 Hz** where everything else
|
||||||
is 48 000, and the `BGM_*` tracks are the stereo ones.
|
is 48 000, and the `BGM_*` tracks are the stereo ones.
|
||||||
|
|
||||||
|
## The 36 shared banks, decoded end-to-end (2026-08-28)
|
||||||
|
|
||||||
|
The census above is arithmetic — durations from the `seek` table, no decoding.
|
||||||
|
This section is the decode itself, for the 36 language-independent banks
|
||||||
|
(32 `BGM_*` + 3 `JNGL_*` + `Static.slb`), because they were the ones no viewer
|
||||||
|
had ever played: the library enumerator kept only names containing `VOICE` or
|
||||||
|
`\Briefing\`, so every one of them was filtered out before it could be tried.
|
||||||
|
|
||||||
|
**35 of 36 decode to plausible audio**, and the shapes agree with the census:
|
||||||
|
the 32 `BGM_*` come out 75–555 s and **stereo**, matching "the `BGM_*` tracks
|
||||||
|
are the stereo ones"; `JNGL_002` decodes to 33.89 s against the manifest's
|
||||||
|
predicted 33.97 s.
|
||||||
|
|
||||||
|
Two things the census could not have caught, both found by decoding:
|
||||||
|
|
||||||
|
### `Static.slb`'s TOC entry over-declares its size
|
||||||
|
|
||||||
|
The SFX bank sits at the **highest offset in the archive** and claims
|
||||||
|
8 970 240 bytes — **616 768 past the end of `sound.p04`**. It is not our
|
||||||
|
extraction: `sound.p04` is byte-for-byte the size the ISO's own directory record
|
||||||
|
gives. A sweep of **every `.pak` on the disc** finds this one entry over-running
|
||||||
|
and no other, so the last entry's `comp_size` is an allocation size rather than a
|
||||||
|
stored size.
|
||||||
|
|
||||||
|
`PakArchive::stored_bytes` therefore allows a short read **only** for the
|
||||||
|
highest-offset entry. Any other overrun is still an error — that would be real
|
||||||
|
damage, and clamping it would hide the damage behind a half-decoded asset. With
|
||||||
|
the short read, `Static.slb` decodes to **514 s of mono**; before it, the SFX
|
||||||
|
bank could not be read at all.
|
||||||
|
|
||||||
|
### `JNGL_001.slb` does not decode — and that is a real gap, not a filter
|
||||||
|
|
||||||
|
It is one of the headerless banks (no `RIFF`), so it was already outside the
|
||||||
|
4 114-bank manifest above. Decoding it yields **0.01 s** — one frame, the
|
||||||
|
signature this corpus already records for a wrong channel count. But the usual
|
||||||
|
fixes do not apply:
|
||||||
|
|
||||||
|
* it is not a channel-count error the `RIFF+49` rule can repair, because there is
|
||||||
|
no `RIFF` to read the count from;
|
||||||
|
* its payload is **not a whole number of 2048-byte XMA1 packets** from any of the
|
||||||
|
four `DATA_OFFSET_CANDIDATES`, which a headerless XMA1 stream must be.
|
||||||
|
|
||||||
|
So `JNGL_001` is probably not a plain headerless XMA1 stream at all. The other
|
||||||
|
headerless root bank, `Static.slb`, decodes fine at 514 s, so the headerless path
|
||||||
|
is not broken in general — this is one bank in 9 519. It is listed in the viewer
|
||||||
|
and reports that it did not decode, rather than being hidden.
|
||||||
|
|
||||||
|
⚠️ Note on the offsets: `to_xma_riffs` still *scans* for the headerless data
|
||||||
|
offset, while [slb-data-offset.md](slb-data-offset.md) establishes the exact rule
|
||||||
|
(the cumulative `.pNN` segment start mod 2048, 8 783/8 783). Wiring the exact
|
||||||
|
rule into the decoder is open, and is the first thing to try on `JNGL_001`.
|
||||||
|
|
||||||
|
### Downmix is a per-category decision, not a constant
|
||||||
|
|
||||||
|
The decoder took the left channel unconditionally. That is right for **voice**,
|
||||||
|
whose content is mono however it is stored (some clips put the signal in the left
|
||||||
|
channel alone, others duplicate L=R) — and wrong for **music**, where the two
|
||||||
|
channels are a real stereo mix and taking one throws half of it away. The caller
|
||||||
|
now decides from the bank's category.
|
||||||
|
|||||||
536
tools/re-capture/idxd_unnamed_keys.py
Normal file
536
tools/re-capture/idxd_unnamed_keys.py
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
r"""Census -- and preimage attack -- for IDXD field keys that carry no name.
|
||||||
|
|
||||||
|
Pure static work: reads the extracted disc, runs no emulator.
|
||||||
|
|
||||||
|
python3 tools/re-capture/idxd_unnamed_keys.py census > docs/re/data/idxd-unnamed-field-keys.txt
|
||||||
|
python3 tools/re-capture/idxd_unnamed_keys.py crack # corpus + brute-force attack
|
||||||
|
python3 tools/re-capture/idxd_unnamed_keys.py idtbl # dump the 42 hash-shaped keys in context
|
||||||
|
|
||||||
|
An IDXD field entry is `(key, name_off, value_off)`. When `name_off` is
|
||||||
|
0xFFFFFFFF the field has no name in the pool, and the reader must know what the
|
||||||
|
`key` means from somewhere else. This tool answers "how many such keys are
|
||||||
|
there, what do they look like, and can the names be recovered from the hash?"
|
||||||
|
|
||||||
|
The headline measurement (see docs/re/structures/idxd-unnamed-keys.md):
|
||||||
|
|
||||||
|
2 757 039 field entries in 7 750 IDXD objects
|
||||||
|
1 271 462 named, 1 485 577 unnamed
|
||||||
|
7 094 distinct keys that are never named ANYWHERE on the disc
|
||||||
|
7 052 of them are < 0x10000 -- author-assigned ordinals / element ids
|
||||||
|
42 of them are >= 0x01000000 -- genuine tag_hash values
|
||||||
|
|
||||||
|
`SYLPHEED_DISC` (default /work/sylph_extract) points at the extracted disc.
|
||||||
|
"""
|
||||||
|
import argparse, collections, glob, os, re, struct, sys, zlib
|
||||||
|
|
||||||
|
DISC = os.environ.get('SYLPHEED_DISC', '/work/sylph_extract')
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- hashes
|
||||||
|
|
||||||
|
TAG_MODULUS = 0x00FFFFDF # 2^24 - 33, prime
|
||||||
|
TAG_MAGIC = 0x2101 # floor(2^56/M)+1, the guest's divide magic
|
||||||
|
|
||||||
|
|
||||||
|
def tag_hash(s):
|
||||||
|
"""IDXD record key / field tag -- `sub_82447DF0`.
|
||||||
|
|
||||||
|
Transcribed in docs/re/structures/idxd-tag-hash.md and implemented three
|
||||||
|
times in this tree (here, tools/re-capture/unitgroup.py, and
|
||||||
|
sylpheed_formats::hash::tag_hash). Case-SENSITIVE, bytes sign-extended.
|
||||||
|
"""
|
||||||
|
a = b = 0
|
||||||
|
for byte in s.encode('latin-1', 'replace'):
|
||||||
|
c = (byte - 256) if byte > 127 else byte # extsb
|
||||||
|
a = ((a << 8) & 0xFFFFFFFF)
|
||||||
|
a = (a + c) & 0xFFFFFFFF
|
||||||
|
b = (b + c) & 0xFFFFFFFF
|
||||||
|
hi = ((a * TAG_MAGIC) >> 32) & 0xFFFFFFFF # mulhwu
|
||||||
|
q = ((hi + (((a - hi) & 0xFFFFFFFF) >> 1)) & 0xFFFFFFFF) >> 23
|
||||||
|
a = (a - q * TAG_MODULUS) & 0xFFFFFFFF
|
||||||
|
return (((b << 24) & 0xFF000000) | (a & 0x00FFFFFF)) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
NAME_MODULUS, NAME_RECIP = 0x00FFF9D7, 0x80031493
|
||||||
|
|
||||||
|
|
||||||
|
def _rotl(v, n):
|
||||||
|
return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def name_hash(s):
|
||||||
|
"""IPFB TOC path hash -- lowercased, a different modulus. `sub_82455C78`."""
|
||||||
|
bs = bytearray(s.encode('latin-1', 'replace'))
|
||||||
|
for i, x in enumerate(bs):
|
||||||
|
if 65 <= x <= 90:
|
||||||
|
bs[i] = x + 0x20
|
||||||
|
a = b = 0
|
||||||
|
for byte in bs:
|
||||||
|
c = ((byte - 256) if byte > 127 else byte) & 0xFFFFFFFF
|
||||||
|
a = ((_rotl(a, 8) & 0xFFFFFF00) + c) & 0xFFFFFFFF
|
||||||
|
b = (b + c) & 0xFFFFFFFF
|
||||||
|
q = _rotl(((a * NAME_RECIP) >> 32) & 0xFFFFFFFF, 9) & 0x1FF
|
||||||
|
a = (a - (q * NAME_MODULUS)) & 0xFFFFFFFF
|
||||||
|
return (((b << 24) & 0xFF000000) | (a & 0x00FFFFFF)) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- pak / IDXD
|
||||||
|
|
||||||
|
def pak_entries(pak):
|
||||||
|
"""yield (toc_key, payload) for every entry of an IPFB pak + its .pNN segments."""
|
||||||
|
idx = open(pak, 'rb').read()
|
||||||
|
data = b''.join(open(p, 'rb').read()
|
||||||
|
for p in sorted(glob.glob(pak[:-4] + '.p[0-9][0-9]')))
|
||||||
|
for i in range(struct.unpack_from('>I', idx, 4)[0]):
|
||||||
|
k, off, csize = struct.unpack_from('>III', idx, 16 + i * 12)
|
||||||
|
s = data[off:off + csize]
|
||||||
|
if s[:2] == b'Z1':
|
||||||
|
try:
|
||||||
|
s = zlib.decompress(s[10:])
|
||||||
|
except zlib.error:
|
||||||
|
continue
|
||||||
|
yield k, s
|
||||||
|
|
||||||
|
|
||||||
|
def parse_idxd(b):
|
||||||
|
"""-> [(record_key, record_name, [(tag, name|None, value, field_index)])]"""
|
||||||
|
u = lambda o: struct.unpack_from('>I', b, o)[0]
|
||||||
|
nrec = u(0x04)
|
||||||
|
npool_off = 0x08 + nrec * 16
|
||||||
|
npool = u(npool_off)
|
||||||
|
pool = npool_off + 4
|
||||||
|
strsize_off = pool + npool * 12
|
||||||
|
STR = strsize_off + 4
|
||||||
|
strsize = u(strsize_off)
|
||||||
|
if STR + strsize != len(b):
|
||||||
|
raise ValueError('string-pool trailer mismatch')
|
||||||
|
|
||||||
|
def s(o):
|
||||||
|
if o >= strsize:
|
||||||
|
raise ValueError('string offset out of pool')
|
||||||
|
return b[STR + o:b.index(b'\x00', STR + o)].decode('latin-1')
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for i in range(nrec):
|
||||||
|
key, nm, lo, hi = (u(0x08 + i * 16 + j * 4) for j in range(4))
|
||||||
|
fields = []
|
||||||
|
for j in range(lo, hi):
|
||||||
|
o = pool + j * 12
|
||||||
|
tag, noff, voff = u(o), u(o + 4), u(o + 8)
|
||||||
|
fields.append((tag, s(noff) if noff != 0xFFFFFFFF else None, s(voff), j - lo))
|
||||||
|
out.append((key, s(nm), fields))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def all_paks():
|
||||||
|
return sorted(glob.glob(DISC + '/dat/*.pak') + glob.glob(DISC + '/hidden/*.pak'))
|
||||||
|
|
||||||
|
|
||||||
|
def walk(counter=None):
|
||||||
|
"""yield (pak_basename, obj_toc_key, record_key, record_name, fields)."""
|
||||||
|
for pak in all_paks():
|
||||||
|
base = os.path.basename(pak)
|
||||||
|
for h, payload in pak_entries(pak):
|
||||||
|
if payload[:4] != b'IDXD':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
recs = parse_idxd(payload)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print('PARSE FAIL %s %08x %s' % (base, h, exc), file=sys.stderr)
|
||||||
|
continue
|
||||||
|
if counter is not None:
|
||||||
|
counter['objects'] += 1
|
||||||
|
schema = recs[0][1] if recs else ''
|
||||||
|
for key, rname, fields in recs:
|
||||||
|
yield base, h, schema, key, rname, fields
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- value profile
|
||||||
|
|
||||||
|
_FLOAT = re.compile(r'^-?(\d+\.\d*|\.\d+|\d+)([eE][-+]?\d+)?$')
|
||||||
|
_INT = re.compile(r'^-?\d+$')
|
||||||
|
_HEX = re.compile(r'^(0[xX])?[0-9a-fA-F]+$')
|
||||||
|
_FILE = re.compile(r'^[\w\\/. -]+\.[A-Za-z0-9]{2,4}$')
|
||||||
|
|
||||||
|
|
||||||
|
def classify(values):
|
||||||
|
"""A coarse type label for a bag of field values."""
|
||||||
|
if not values:
|
||||||
|
return 'empty'
|
||||||
|
kinds = set()
|
||||||
|
for v in values:
|
||||||
|
if v == '':
|
||||||
|
kinds.add('empty')
|
||||||
|
elif _INT.match(v):
|
||||||
|
kinds.add('int')
|
||||||
|
elif _FLOAT.match(v):
|
||||||
|
kinds.add('float')
|
||||||
|
elif _FILE.match(v):
|
||||||
|
kinds.add('file')
|
||||||
|
elif v in ('True', 'False', 'Yes', 'No', 'ON', 'OFF'):
|
||||||
|
kinds.add('bool')
|
||||||
|
elif _HEX.match(v) and len(v) >= 6:
|
||||||
|
kinds.add('hex')
|
||||||
|
else:
|
||||||
|
kinds.add('id')
|
||||||
|
order = ['file', 'id', 'hex', 'bool', 'float', 'int', 'empty']
|
||||||
|
return '+'.join(k for k in order if k in kinds)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- census
|
||||||
|
|
||||||
|
def census():
|
||||||
|
named = collections.defaultdict(collections.Counter)
|
||||||
|
unnamed = collections.Counter()
|
||||||
|
key_objs = collections.defaultdict(set)
|
||||||
|
key_recs = collections.defaultdict(collections.Counter)
|
||||||
|
key_idx = collections.defaultdict(lambda: [1 << 30, -1])
|
||||||
|
key_vals = collections.defaultdict(list)
|
||||||
|
key_nval = collections.Counter()
|
||||||
|
# per (object-schema, record-name) family, for the ordinal population
|
||||||
|
fam = collections.defaultdict(lambda: {'keys': set(), 'n': 0, 'vals': [], 'recs': 0})
|
||||||
|
tot = collections.Counter()
|
||||||
|
schema_of = {}
|
||||||
|
|
||||||
|
for base, h, schema, rkey, rname, fields in walk(tot):
|
||||||
|
tot['records'] += 1
|
||||||
|
# the object's schema hash is the key of its first record; use record name
|
||||||
|
fam_id = None
|
||||||
|
for tag, name, val, fidx in fields:
|
||||||
|
tot['fields'] += 1
|
||||||
|
if name is not None:
|
||||||
|
tot['named'] += 1
|
||||||
|
named[tag][name] += 1
|
||||||
|
else:
|
||||||
|
tot['unnamed'] += 1
|
||||||
|
unnamed[tag] += 1
|
||||||
|
key_objs[tag].add((base, h))
|
||||||
|
key_recs[tag][rname] += 1
|
||||||
|
lohi = key_idx[tag]
|
||||||
|
lohi[0] = min(lohi[0], fidx)
|
||||||
|
lohi[1] = max(lohi[1], fidx)
|
||||||
|
key_nval[tag] += 1
|
||||||
|
if len(key_vals[tag]) < 40:
|
||||||
|
key_vals[tag].append(val)
|
||||||
|
if fam_id is None:
|
||||||
|
fam_id = (base, schema)
|
||||||
|
f = fam[fam_id]
|
||||||
|
f['keys'].add(tag)
|
||||||
|
f['n'] += 1
|
||||||
|
if len(f['vals']) < 60:
|
||||||
|
f['vals'].append(val)
|
||||||
|
if fam_id is not None:
|
||||||
|
fam[fam_id]['recs'] += 1
|
||||||
|
|
||||||
|
return dict(named=named, unnamed=unnamed, key_objs=key_objs, key_recs=key_recs,
|
||||||
|
key_idx=key_idx, key_vals=key_vals, key_nval=key_nval, fam=fam, tot=tot)
|
||||||
|
|
||||||
|
|
||||||
|
def report(c, out=sys.stdout):
|
||||||
|
named, unnamed = c['named'], c['unnamed']
|
||||||
|
never = sorted(set(unnamed) - set(named))
|
||||||
|
hashy = [k for k in never if k >= 0x01000000]
|
||||||
|
ordinal = [k for k in never if k < 0x01000000]
|
||||||
|
both = sorted(set(unnamed) & set(named))
|
||||||
|
w = out.write
|
||||||
|
|
||||||
|
w('# IDXD field keys that carry no name\n')
|
||||||
|
w('#\n')
|
||||||
|
w('# Generated by tools/re-capture/idxd_unnamed_keys.py census\n')
|
||||||
|
w('# Disc: %s (all dat/*.pak + hidden/*.pak, .pNN segments joined, Z1 inflated)\n' % DISC)
|
||||||
|
w('# Companion write-up: docs/re/structures/idxd-unnamed-keys.md\n')
|
||||||
|
w('#\n')
|
||||||
|
w('# A field entry is (key, name_off, value_off). name_off == 0xFFFFFFFF means the\n')
|
||||||
|
w('# pool holds no name for it, so the reader must already know what `key` means.\n')
|
||||||
|
w('\n')
|
||||||
|
w('== TOTALS ==\n')
|
||||||
|
w('IDXD objects walked : %d\n' % c['tot']['objects'])
|
||||||
|
w('IDXD records walked : %d\n' % c['tot']['records'])
|
||||||
|
w('field entries : %d\n' % c['tot']['fields'])
|
||||||
|
w(' named (name_off != 0xFFFFFFFF) : %d\n' % c['tot']['named'])
|
||||||
|
w(' unnamed (name_off == 0xFFFFFFFF) : %d\n' % c['tot']['unnamed'])
|
||||||
|
w('distinct keys seen named somewhere : %d\n' % len(named))
|
||||||
|
w('distinct keys seen unnamed somewhere : %d\n' % len(unnamed))
|
||||||
|
w('distinct keys NEVER named anywhere on disc : %d\n' % len(never))
|
||||||
|
w(' of those, < 0x00010000 (ordinal-shaped) : %d\n' % len([k for k in never if k < 0x10000]))
|
||||||
|
w(' of those, in [0x10000, 0x1000000) : %d\n' % len([k for k in never if 0x10000 <= k < 0x1000000]))
|
||||||
|
w(' of those, >= 0x01000000 (hash-shaped) : %d\n' % len(hashy))
|
||||||
|
w('keys that are named in one place and unnamed in another: %d %s\n'
|
||||||
|
% (len(both), ['%08x' % k for k in both]))
|
||||||
|
w('unnamed FIELD ENTRIES whose key is hash-shaped : %d\n'
|
||||||
|
% sum(c['key_nval'][k] for k in hashy))
|
||||||
|
w('unnamed FIELD ENTRIES whose key is an ordinal : %d\n'
|
||||||
|
% sum(c['key_nval'][k] for k in ordinal))
|
||||||
|
w('unnamed FIELD ENTRIES with key 0x00000000 : %d (ordinal 0; the key is\n'
|
||||||
|
% sum(c['key_nval'][k] for k in both))
|
||||||
|
w('# "named" elsewhere only because tag_hash("") == 0, so it is counted apart)\n')
|
||||||
|
w('# The first of those two is the "504 hash-keyed fields disc-wide" already in\n')
|
||||||
|
w('# INDEX.md. It is 504 ENTRIES, not 504 distinct keys: %d keys x 6 language\n' % len(hashy))
|
||||||
|
w('# copies of the same object x 2 records (FILE and OFFSET) = %d.\n'
|
||||||
|
% sum(c['key_nval'][k] for k in hashy))
|
||||||
|
w('\n')
|
||||||
|
w('# Why the split at 0x01000000 is not arbitrary: a tag_hash puts the byte-sum\n')
|
||||||
|
w('# checksum in the top byte, so a hash lands below 0x01000000 only when the name\n')
|
||||||
|
w('# byte-sum is 0 mod 256 (1/256 of names) AND its low 24 bits are also tiny. Of\n')
|
||||||
|
zerotop = sorted(k for k in named if (k >> 24) == 0 and k != 0)
|
||||||
|
w('# the %d keys that DO carry a name, %d have a zero top byte, and the smallest of\n'
|
||||||
|
% (len(named), len(zerotop)))
|
||||||
|
w('# those is 0x%06x -- far above the ordinal band, which tops out at 0x%x.\n'
|
||||||
|
% (zerotop[0], max(ordinal)))
|
||||||
|
w('\n')
|
||||||
|
|
||||||
|
w('== PART 1: the %d HASH-SHAPED never-named keys ==\n' % len(hashy))
|
||||||
|
w('# key n objects record field value\n')
|
||||||
|
for k in hashy:
|
||||||
|
recs = ','.join(sorted(c['key_recs'][k]))
|
||||||
|
lo, hi = c['key_idx'][k]
|
||||||
|
vals = sorted(set(c['key_vals'][k]))
|
||||||
|
w('%08x %3d %2d %-13s %2d-%-2d %s\n'
|
||||||
|
% (k, c['key_nval'][k], len(c['key_objs'][k]), recs, lo, hi, '|'.join(vals)))
|
||||||
|
w('\n')
|
||||||
|
w('# All %d live in the six copies of <lang>\\script\\ID.tbl inside\n' % len(hashy))
|
||||||
|
w('# dat/GP_READY_ROOM.pak (eng jpn fra deu ita esp). Each copy has two records,\n')
|
||||||
|
w('# FILE and OFFSET, with the SAME 42 keys in the same order -- i.e. the object is\n')
|
||||||
|
w('# a column store: key -> (which .isb file, what offset inside it).\n')
|
||||||
|
w('\n')
|
||||||
|
|
||||||
|
w('== PART 2: the ordinal population, by (pak, object schema) ==\n')
|
||||||
|
w('# The schema is the name of the object\'s FIRST record -- the string whose\n')
|
||||||
|
w('# tag_hash is the object header word. `recs` counts records with >=1 unnamed field.\n')
|
||||||
|
w('# pak schema recs fields keys keymin-keymax valuetype sample\n')
|
||||||
|
rows = []
|
||||||
|
for (base, rname), f in c['fam'].items():
|
||||||
|
ks = f['keys']
|
||||||
|
rows.append((base, rname, f['recs'], f['n'], len(ks), min(ks), max(ks),
|
||||||
|
classify(f['vals']), f['vals'][0] if f['vals'] else ''))
|
||||||
|
for r in sorted(rows, key=lambda r: (-r[3], r[0], r[1])):
|
||||||
|
w('%-28s %-17s %5d %7d %5d %6x-%-6x %-10s %s\n'
|
||||||
|
% (r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8][:40]))
|
||||||
|
w('\n')
|
||||||
|
|
||||||
|
w('== PART 3: every never-named ordinal key ==\n')
|
||||||
|
w('# key occurrences distinct-records fieldidx-lo-hi valuetype sample-value\n')
|
||||||
|
for k in ordinal:
|
||||||
|
lo, hi = c['key_idx'][k]
|
||||||
|
w('%08x %8d %6d %4d-%-4d %-10s %s\n'
|
||||||
|
% (k, c['key_nval'][k], len(c['key_recs'][k]), lo, hi,
|
||||||
|
classify(c['key_vals'][k]), (c['key_vals'][k] or [''])[0][:40]))
|
||||||
|
w('\n')
|
||||||
|
w('== PART 4: the ID.tbl script-symbol call graph ==\n')
|
||||||
|
try:
|
||||||
|
idtbl(out)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
w('# unavailable: %s\n' % exc)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- ID.tbl
|
||||||
|
|
||||||
|
READY_ROOM = DISC + '/dat/GP_READY_ROOM.pak'
|
||||||
|
LANGS = ('eng', 'jpn', 'fra', 'deu', 'ita', 'esp')
|
||||||
|
|
||||||
|
|
||||||
|
_RR_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
|
def readyroom_file(path):
|
||||||
|
"""Read one file out of GP_READY_ROOM.pak by its (lowercased) path."""
|
||||||
|
if not _RR_CACHE:
|
||||||
|
_RR_CACHE.update(dict(pak_entries(READY_ROOM)))
|
||||||
|
return _RR_CACHE.get(name_hash(path))
|
||||||
|
|
||||||
|
|
||||||
|
def idtbl(out=sys.stdout):
|
||||||
|
"""The 42 keys with their FILE/OFFSET columns, plus the .isb cross-check."""
|
||||||
|
payload = readyroom_file('eng\\script\\ID.tbl')
|
||||||
|
recs = {r[1]: r[2] for r in parse_idxd(payload)}
|
||||||
|
files = recs['FILE']
|
||||||
|
offs = recs['OFFSET']
|
||||||
|
out.write("# eng\\script\\ID.tbl -- key -> (defining .isb file, offset), plus every\n")
|
||||||
|
out.write('# .isb whose bytecode contains the key as a little-endian word (= a call site).\n')
|
||||||
|
out.write('# idx key defined-in offset referenced-from\n')
|
||||||
|
isb = {}
|
||||||
|
for name in sorted({v for _, _, v, _ in files}):
|
||||||
|
b = readyroom_file('eng\\script\\' + name)
|
||||||
|
if b:
|
||||||
|
isb[name] = {struct.unpack_from('<I', b, o)[0] for o in range(0, len(b) - 3)}
|
||||||
|
for i, (tag, _n, val, _f) in enumerate(files):
|
||||||
|
seen = sorted(f for f, s in isb.items() if tag in s)
|
||||||
|
out.write('%3d %08x %-24s %-6s %s\n'
|
||||||
|
% (i, tag, val, offs[i][2], ','.join(seen) or '-'))
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- attacks
|
||||||
|
|
||||||
|
def _poly(s):
|
||||||
|
a = 0
|
||||||
|
for ch in s.encode('latin-1', 'replace'):
|
||||||
|
a = (a * 256 + ch) % TAG_MODULUS
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def corpus_attack(targets, words):
|
||||||
|
"""Straight dictionary attack. Returns {tag: [names]} and the corpus size."""
|
||||||
|
hits = collections.defaultdict(list)
|
||||||
|
n = 0
|
||||||
|
for w in words:
|
||||||
|
n += 1
|
||||||
|
h = tag_hash(w)
|
||||||
|
if h in targets:
|
||||||
|
hits[h].append(w)
|
||||||
|
return hits, n
|
||||||
|
|
||||||
|
|
||||||
|
def compose_attack(targets, tokens, suffixes=('',), seps=('', '_', '-')):
|
||||||
|
"""Two-token composition attack: name = t1 + sep + t2 + suffix.
|
||||||
|
|
||||||
|
Returns (hits, effective search space). The space is what matters: with a
|
||||||
|
32-bit tag, expected FALSE hits per target is space / 2**32.
|
||||||
|
"""
|
||||||
|
right = collections.defaultdict(list)
|
||||||
|
for t in tokens:
|
||||||
|
for sep in seps:
|
||||||
|
for suf in suffixes:
|
||||||
|
r = sep + t + suf
|
||||||
|
right[(len(r), _poly(r))].append(r)
|
||||||
|
lens = sorted({k[0] for k in right})
|
||||||
|
p256 = {l: pow(256, l, TAG_MODULUS) for l in lens}
|
||||||
|
hits = collections.defaultdict(list)
|
||||||
|
for left in tokens:
|
||||||
|
pl = _poly(left)
|
||||||
|
for l in lens:
|
||||||
|
for tag in targets:
|
||||||
|
want = ((tag & 0xFFFFFF) - pl * p256[l]) % TAG_MODULUS
|
||||||
|
for r in right.get((l, want), ()):
|
||||||
|
cand = left + r
|
||||||
|
if tag_hash(cand) == tag:
|
||||||
|
hits[tag].append(cand)
|
||||||
|
space = len(tokens) * sum(len(v) for v in right.values())
|
||||||
|
return hits, space
|
||||||
|
|
||||||
|
|
||||||
|
def brute_force(target, suffix='', maxlen=6, alphabet=None):
|
||||||
|
"""Meet-in-the-middle preimage search: all P over `alphabet` with
|
||||||
|
tag_hash(P + suffix) == target and len(P) <= maxlen.
|
||||||
|
|
||||||
|
Needs numpy. The honest limit is maxlen 6: the expected number of FALSE
|
||||||
|
preimages is |alphabet|**maxlen / 2**32, which is ~15 at 63**6 and ~58 000
|
||||||
|
at 63**8, so anything past 6 returns noise, not names.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
if alphabet is None:
|
||||||
|
alphabet = ([chr(c) for c in range(48, 58)] + [chr(c) for c in range(65, 91)]
|
||||||
|
+ [chr(c) for c in range(97, 123)] + ['_'])
|
||||||
|
A = len(alphabet)
|
||||||
|
codes = np.array([ord(c) for c in alphabet], dtype=np.int64)
|
||||||
|
tabs = []
|
||||||
|
polys = np.zeros(1, dtype=np.int64)
|
||||||
|
sums = np.zeros(1, dtype=np.int64)
|
||||||
|
tabs.append((polys, sums))
|
||||||
|
for _ in range(maxlen // 2 + maxlen % 2):
|
||||||
|
polys = ((polys[:, None] * 256 + codes[None, :]) % TAG_MODULUS).ravel()
|
||||||
|
sums = (sums[:, None] + codes[None, :]).ravel()
|
||||||
|
tabs.append((polys, sums))
|
||||||
|
|
||||||
|
def decode(idx, k):
|
||||||
|
s = ''
|
||||||
|
for _ in range(k):
|
||||||
|
s = alphabet[idx % A] + s
|
||||||
|
idx //= A
|
||||||
|
return s
|
||||||
|
|
||||||
|
ps = _poly(suffix)
|
||||||
|
inv = pow(pow(256, len(suffix), TAG_MODULUS), -1, TAG_MODULUS)
|
||||||
|
need_sum = ((target >> 24) - sum(suffix.encode('latin-1'))) & 0xFF
|
||||||
|
out = []
|
||||||
|
for L in range(maxlen + 1):
|
||||||
|
v = L // 2
|
||||||
|
u = L - v
|
||||||
|
if u >= len(tabs) or v >= len(tabs):
|
||||||
|
continue
|
||||||
|
pu, su = tabs[u]
|
||||||
|
pv, sv = tabs[v]
|
||||||
|
order = np.argsort(pv, kind='stable')
|
||||||
|
pv_s = pv[order]
|
||||||
|
need = (((target & 0xFFFFFF) - ps) * inv) % TAG_MODULUS
|
||||||
|
want = (need - (pu * pow(256, v, TAG_MODULUS)) % TAG_MODULUS) % TAG_MODULUS
|
||||||
|
lo = np.searchsorted(pv_s, want, side='left')
|
||||||
|
hi = np.searchsorted(pv_s, want, side='right')
|
||||||
|
for i in np.nonzero(hi > lo)[0]:
|
||||||
|
for j in range(lo[i], hi[i]):
|
||||||
|
vi = order[j]
|
||||||
|
if ((su[i] + sv[vi]) & 0xFF) != need_sum:
|
||||||
|
continue
|
||||||
|
cand = decode(int(i), u) + decode(int(vi), v) + suffix
|
||||||
|
if tag_hash(cand) == target:
|
||||||
|
out.append(cand)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- main
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument('mode', choices=('census', 'idtbl', 'crack', 'selftest'))
|
||||||
|
ap.add_argument('--maxlen', type=int, default=6)
|
||||||
|
ap.add_argument('--pe', default=os.environ.get('SYLPHEED_PE'),
|
||||||
|
help='flat VA dump of default.xex; its strings widen the corpus')
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.mode == 'census':
|
||||||
|
report(census())
|
||||||
|
elif args.mode == 'idtbl':
|
||||||
|
idtbl()
|
||||||
|
elif args.mode == 'selftest':
|
||||||
|
# tag_hash must reproduce the key of every NAMED field on the disc.
|
||||||
|
ok = bad = 0
|
||||||
|
for _b, _h, _sc, _rk, _rn, fields in walk():
|
||||||
|
for tag, name, _v, _i in fields:
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
if tag_hash(name) == tag:
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
bad += 1
|
||||||
|
if bad < 5:
|
||||||
|
print('MISMATCH %r %08x != %08x' % (name, tag, tag_hash(name)))
|
||||||
|
print('tag_hash reproduces %d / %d named field keys (%d wrong)' % (ok, ok + bad, bad))
|
||||||
|
else:
|
||||||
|
c = census()
|
||||||
|
targets = {k for k in set(c['unnamed']) - set(c['named']) if k >= 0x01000000}
|
||||||
|
print('targets: %d hash-shaped never-named keys' % len(targets))
|
||||||
|
words = set()
|
||||||
|
for b, h, _sc, _rk, rname, fields in walk():
|
||||||
|
words.add(rname)
|
||||||
|
for _t, name, val, _i in fields:
|
||||||
|
if name:
|
||||||
|
words.add(name)
|
||||||
|
words.add(val)
|
||||||
|
if args.pe and os.path.exists(args.pe):
|
||||||
|
blob = open(args.pe, 'rb').read()
|
||||||
|
words |= {m.decode('latin-1') for m in re.findall(rb'[ -~]{3,}', blob)}
|
||||||
|
hits, n = corpus_attack(targets, words)
|
||||||
|
print('disc-string corpus : %d strings, %d hits, E[false]=%.4f'
|
||||||
|
% (n, len(hits), n * len(targets) / 2 ** 32))
|
||||||
|
for t, v in hits.items():
|
||||||
|
print(' %08x %s' % (t, v[:6]))
|
||||||
|
toks = collections.Counter()
|
||||||
|
for w in words:
|
||||||
|
for part in re.split(r'[^A-Za-z0-9]+', w):
|
||||||
|
for p in re.findall(r'[A-Z]+(?![a-z])|[A-Z][a-z]+|[a-z]+', part):
|
||||||
|
if 2 <= len(p) <= 14:
|
||||||
|
toks[p] += 1
|
||||||
|
tokens = [t for t, _ in toks.most_common()]
|
||||||
|
hits, space = compose_attack(targets, tokens)
|
||||||
|
print('2-token composition: space=%d, %d hits, E[false]=%.3f'
|
||||||
|
% (space, len(hits), space * len(targets) / 2 ** 32))
|
||||||
|
for t, v in hits.items():
|
||||||
|
print(' %08x %s' % (t, v[:6]))
|
||||||
|
for t in sorted(targets)[:3]:
|
||||||
|
r = brute_force(t, '', args.maxlen)
|
||||||
|
print('brute force <=%d chars %08x: %d candidates %s'
|
||||||
|
% (args.maxlen, t, len(r), r[:6]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -92,7 +92,11 @@ BUILTIN = {
|
|||||||
13: 'play_se', 14: 'play_bgm',
|
13: 'play_se', 14: 'play_bgm',
|
||||||
15: 'set_group_speed',
|
15: 'set_group_speed',
|
||||||
17: 'wait_frames', 18: 'dist_lt', 20: 'hp_pct_test', 24: 'squad_survival_pct',
|
17: 'wait_frames', 18: 'dist_lt', 20: 'hp_pct_test', 24: 'squad_survival_pct',
|
||||||
26: 'damage_unit', 30: 'objective_marker', 31: 'objective_marker_at_route',
|
26: 'set_unit_hp_pct',
|
||||||
|
# 28's field and default (1.0) are certain; the LABEL rests on a single
|
||||||
|
# consumer, so it is PROBABLE, not confirmed. See isl-builtins.md.
|
||||||
|
28: 'set_unit_damage_dealt_pct', 29: 'set_unit_damage_taken_pct',
|
||||||
|
30: 'objective_marker', 31: 'objective_marker_at_route',
|
||||||
33: 'global_counter0', 34: 'global_counter1', 36: 'screen_fade',
|
33: 'global_counter0', 34: 'global_counter1', 36: 'screen_fade',
|
||||||
39: 'MARK_LAST_PHASE', 40: 'mark_not_last', 43: 'play_voice',
|
39: 'MARK_LAST_PHASE', 40: 'mark_not_last', 43: 'play_voice',
|
||||||
45: 'play_voice_vol', 46: 'squadron_trace', 47: 'squadron_attack',
|
45: 'play_voice_vol', 46: 'squadron_trace', 47: 'squadron_attack',
|
||||||
@@ -109,8 +113,13 @@ BUILTIN = {
|
|||||||
77: 'banner_mission_start', 78: 'banner_mission_complete',
|
77: 'banner_mission_start', 78: 'banner_mission_complete',
|
||||||
81: 'banner_objective_update', 82: 'banner_mission_failed',
|
81: 'banner_objective_update', 82: 'banner_mission_failed',
|
||||||
135: 'banner_mission_restart',
|
135: 'banner_mission_restart',
|
||||||
|
# 93 is `stopwatch_stop`, not the older `clear_flag`: 8/9/93 were renamed to
|
||||||
|
# stopwatch_start/_elapsed/_stop once the reading landed (isl-timers.md says
|
||||||
|
# of the old trio "the names predate the reading"). The branch that still
|
||||||
|
# called it `clear_flag` simply forked before that rename.
|
||||||
93: 'stopwatch_stop', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'reset_phase_threads',
|
93: 'stopwatch_stop', 94: 'is_engaged', 95: 'unit_hp_pct', 100: 'reset_phase_threads',
|
||||||
102: 'prompt_yes_no', 104: 'request_next', 108: 'deploy_squadron_ex',
|
101: 'all_units_invulnerable', 102: 'prompt_yes_no', 104: 'request_next',
|
||||||
|
108: 'deploy_squadron_ex',
|
||||||
109: 'set_unit_flags', 115: 'named_event',
|
109: 'set_unit_flags', 115: 'named_event',
|
||||||
120: 'wait_cmds_drained', 123: 'timer_resume', 124: 'timer_stop',
|
120: 'wait_cmds_drained', 123: 'timer_resume', 124: 'timer_stop',
|
||||||
125: 'timer_reset', 126: 'timer_elapsed', 127: 'timer_set',
|
125: 'timer_reset', 126: 'timer_elapsed', 127: 'timer_set',
|
||||||
|
|||||||
171
tools/re-capture/isl_builtin_sites.py
Normal file
171
tools/re-capture/isl_builtin_sites.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Every call site of one or more ISL built-ins, across all `StageNN.ssb`.
|
||||||
|
|
||||||
|
Pure static work — reads the extracted scripts, runs no emulator.
|
||||||
|
|
||||||
|
python3 tools/re-capture/isl_builtin_sites.py 26,28,29 --ssb <dir>
|
||||||
|
python3 tools/re-capture/isl_builtin_sites.py 101 --ctx 3 --ssb <dir>
|
||||||
|
python3 tools/re-capture/isl_builtin_sites.py 28 --craft <unitgroup-dump>
|
||||||
|
|
||||||
|
Why this exists rather than `isl.py --calls`: naming a built-in needs the
|
||||||
|
OPERANDS and the NEIGHBOURS, not just a histogram of ids. `isl.py --calls`
|
||||||
|
scans on the encoding and so cannot see the `local[]` staging that carries a
|
||||||
|
call's arguments, and `isl.py dis` has to be re-synchronised by hand for each
|
||||||
|
site.
|
||||||
|
|
||||||
|
The walk here is a straight linear decode of the whole code region, from the
|
||||||
|
header's code offset (`+0x08`) to the symbol-table-1 offset (`+0x0C`, which is
|
||||||
|
where the code ends). That is safe: **measured on all 28 stages, the linear walk
|
||||||
|
lands on every call site the independent encoding scan finds** (Stage02:
|
||||||
|
2846/2846), so no call is skipped and no false instruction boundary is invented.
|
||||||
|
|
||||||
|
Columns are tab-separated:
|
||||||
|
|
||||||
|
stage offset builtin slot8 slot4-name slot4-raw symtype before after
|
||||||
|
|
||||||
|
`slot4` is the unit operand (a symbol-table-2 index; see isl.py for why the unit
|
||||||
|
sits at slot 4 and slot 0 is a tag word) and `slot8` is the second operand,
|
||||||
|
which is a bare double for the built-ins this was written for. `before`/`after`
|
||||||
|
are the ids of the neighbouring calls, which is what makes a fixed idiom —
|
||||||
|
`116 -> 101 -> 100 -> 124 -> 93` — visible.
|
||||||
|
|
||||||
|
With `--craft <file>` (the output of `unitgroup.py --all`) it also cross-tabs
|
||||||
|
value against the squadron's craft type, which is the test that separated
|
||||||
|
built-in 15's classes and separates these three.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import isl # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def walk(b):
|
||||||
|
"""Yield `(offset, builtin_id, staged)` for every call in the code region.
|
||||||
|
|
||||||
|
`staged` is the `local[]` slot -> value map as it stood when the call
|
||||||
|
executed, i.e. the call's arguments. Both staging forms matter:
|
||||||
|
`local[i] = special[0]` after an immediate, and the far commoner
|
||||||
|
`local[i] = immediate` written straight in.
|
||||||
|
"""
|
||||||
|
code_base = struct.unpack_from('>I', b, 0x08)[0]
|
||||||
|
code_end = struct.unpack_from('>I', b, 0x0C)[0]
|
||||||
|
off, staged, pending = code_base, {}, None
|
||||||
|
while off + 4 <= code_end:
|
||||||
|
w = struct.unpack_from('>I', b, off)[0]
|
||||||
|
op, ln = w & 0xFF, (w >> 8) & 0xFF
|
||||||
|
k1, k0 = (w >> 24) & 0xFF, (w >> 16) & 0xFF
|
||||||
|
if ln == 0 or ln % 4:
|
||||||
|
break
|
||||||
|
words = [struct.unpack_from('>I', b, off + i)[0]
|
||||||
|
for i in range(4, max(ln, 4), 4) if off + i + 4 <= len(b)]
|
||||||
|
if op in (0, 1) and len(words) >= 2:
|
||||||
|
if k0 == 2 and k1 == 1:
|
||||||
|
pending = _imm(op, words)
|
||||||
|
elif k0 == 3 and k1 == 2 and pending is not None:
|
||||||
|
staged[words[0]] = pending
|
||||||
|
elif k0 == 3 and k1 == 1:
|
||||||
|
staged[words[0]] = _imm(op, words)
|
||||||
|
if op == 19 and words:
|
||||||
|
yield off, words[0], dict(staged)
|
||||||
|
staged = {}
|
||||||
|
off += ln
|
||||||
|
|
||||||
|
|
||||||
|
def _imm(op, words):
|
||||||
|
"""An `op 1` immediate is a DOUBLE carried as two words, not a float."""
|
||||||
|
if op == 1:
|
||||||
|
lo = words[2] if len(words) > 2 else 0
|
||||||
|
return struct.unpack('>d', struct.pack('>II', words[1], lo))[0]
|
||||||
|
return words[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(v):
|
||||||
|
if v is None:
|
||||||
|
return '-'
|
||||||
|
return ('%g' % v) if isinstance(v, float) else '0x%X' % v
|
||||||
|
|
||||||
|
|
||||||
|
def load_craft(path):
|
||||||
|
"""{stage: {squadron: [craft, ...]}} from `unitgroup.py --all` output."""
|
||||||
|
out, stage, squad = collections.defaultdict(dict), None, None
|
||||||
|
for line in open(path):
|
||||||
|
m = re.match(r'^(S\d\d):', line)
|
||||||
|
if m:
|
||||||
|
stage = m.group(1)
|
||||||
|
continue
|
||||||
|
m = re.match(r'^ (\S+)\s+\S+\s+Count=', line)
|
||||||
|
if m:
|
||||||
|
squad = m.group(1)
|
||||||
|
out[stage][squad] = []
|
||||||
|
continue
|
||||||
|
m = re.match(r'^ unit=(\S+)', line)
|
||||||
|
if m and squad:
|
||||||
|
out[stage][squad].append(m.group(1))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('ids', help='comma-separated built-in ids')
|
||||||
|
ap.add_argument('--ssb', default='.', help='directory holding StageNN.ssb')
|
||||||
|
ap.add_argument('--ctx', type=int, default=2, help='neighbour calls to show')
|
||||||
|
ap.add_argument('--craft', help='unitgroup.py --all output, for the cross-tab')
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
ids = {int(x) for x in a.ids.split(',')}
|
||||||
|
craft = load_craft(a.craft) if a.craft else None
|
||||||
|
vals = collections.defaultdict(collections.Counter)
|
||||||
|
units = collections.defaultdict(collections.Counter)
|
||||||
|
cross = collections.defaultdict(lambda: collections.defaultdict(collections.Counter))
|
||||||
|
unresolved = collections.Counter()
|
||||||
|
|
||||||
|
for path in sorted(glob.glob(os.path.join(a.ssb, 'Stage*.ssb'))):
|
||||||
|
stage = os.path.basename(path)[:-4]
|
||||||
|
b = isl.load(path)
|
||||||
|
sym2 = isl.symbols(b, 2)
|
||||||
|
calls = list(walk(b))
|
||||||
|
for i, (off, bid, staged) in enumerate(calls):
|
||||||
|
if bid not in ids:
|
||||||
|
continue
|
||||||
|
uidx = staged.get(4)
|
||||||
|
typ, name = sym2.get(uidx, (None, '?')) if isinstance(uidx, int) \
|
||||||
|
else (None, '?')
|
||||||
|
val = _fmt(staged.get(8))
|
||||||
|
print('%s\t0x%06X\t%d\t%s\t%s\t%s\ttype%s\t[%s]\t[%s]' % (
|
||||||
|
stage, off, bid, val, name,
|
||||||
|
('0x%X' % uidx) if isinstance(uidx, int) else uidx, typ,
|
||||||
|
','.join(str(calls[j][1]) for j in range(max(0, i - a.ctx), i)),
|
||||||
|
','.join(str(calls[j][1])
|
||||||
|
for j in range(i + 1, min(len(calls), i + 1 + a.ctx)))))
|
||||||
|
vals[bid][val] += 1
|
||||||
|
units[bid][name] += 1
|
||||||
|
if craft is not None:
|
||||||
|
cs = craft.get('S' + stage[-2:], {}).get(name)
|
||||||
|
if cs:
|
||||||
|
for c in set(cs):
|
||||||
|
cross[bid][c][val] += 1
|
||||||
|
else:
|
||||||
|
unresolved[bid] += 1
|
||||||
|
|
||||||
|
for bid in sorted(ids):
|
||||||
|
n = sum(vals[bid].values())
|
||||||
|
print('\n# builtin %d: %d sites, %d distinct values' % (bid, n, len(vals[bid])))
|
||||||
|
print('# values: ' + ', '.join('%s x%d' % kv for kv in vals[bid].most_common()))
|
||||||
|
print('# units: %d distinct, top: %s' % (
|
||||||
|
len(units[bid]), ', '.join('%s x%d' % kv for kv in units[bid].most_common(10))))
|
||||||
|
if craft is not None:
|
||||||
|
print('# craft cross-tab (%d unresolved):' % unresolved[bid])
|
||||||
|
for c in sorted(cross[bid], key=lambda c: -sum(cross[bid][c].values())):
|
||||||
|
print('# %-42s %4d %s' % (
|
||||||
|
c, sum(cross[bid][c].values()),
|
||||||
|
', '.join('%sx%d' % kv for kv in cross[bid][c].most_common())))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
89
tools/re-capture/kf_time_probe.py
Executable file
89
tools/re-capture/kf_time_probe.py
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Per-frame quad geometry and vertex colour from a `log_ui_draws` capture.
|
||||||
|
|
||||||
|
`ui_draw_order.py` answers "in what order were these painted"; this answers
|
||||||
|
"what did this quad look like on frame N, and on N+1". That is what a keyframe
|
||||||
|
TIME unit has to be measured against: the bundle says an element ramps from
|
||||||
|
t=31 to t=34, and the only way to learn what a `t` is worth is to count the
|
||||||
|
rendered frames the same ramp takes in the running game.
|
||||||
|
|
||||||
|
kf_time_probe.py <capture.log> [--csv out.csv]
|
||||||
|
|
||||||
|
Emits one row per quad per frame: frame, draw index, pixel rect, whether the
|
||||||
|
quad is axis-aligned, and the per-vertex colour word (whose alpha byte is the
|
||||||
|
element's fade). Frame numbers are the emulator's VdSwap count, so they are
|
||||||
|
SUBMITTED FRAMES, not wall-clock — which is the point: an emulator that runs
|
||||||
|
at half speed does not move them.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
W, H = 1280, 720
|
||||||
|
VERT = re.compile(r"\[(-?\d+\.\d+),(-?\d+\.\d+),z=(-?\d+\.\d+)(?:,col=([0-9A-F]{8}))?\]")
|
||||||
|
|
||||||
|
|
||||||
|
def quads(log):
|
||||||
|
"""Yield (frame, draw, x0, y0, w, h, rot, col) for every quad in the log."""
|
||||||
|
frame, pending = None, None
|
||||||
|
for line in open(log):
|
||||||
|
if line.startswith("# every draw"):
|
||||||
|
frame = int(line.rstrip().split()[-1].split("..")[0])
|
||||||
|
continue
|
||||||
|
if line.startswith("--- frame"):
|
||||||
|
frame = int(line.split()[2])
|
||||||
|
continue
|
||||||
|
m = re.match(r"\s*(\d+) (prim=.*)", line)
|
||||||
|
if m:
|
||||||
|
pending = (int(m.group(1)), m.group(2))
|
||||||
|
continue
|
||||||
|
if "vb=0x" in line and pending:
|
||||||
|
hits = VERT.findall(line)
|
||||||
|
verts = [(float(a), float(b), c) for a, b, _z, c in hits]
|
||||||
|
if verts:
|
||||||
|
# Two conventions in one log: the UI sprite shader emits NDC,
|
||||||
|
# the full-screen pass emits pixels already.
|
||||||
|
if max(abs(v) for x, y, _c in verts for v in (x, y)) > 4.0:
|
||||||
|
pts = [(x, y, c) for x, y, c in verts]
|
||||||
|
else:
|
||||||
|
pts = [((x + 1) / 2 * W, (1 - y) / 2 * H, c) for x, y, c in verts]
|
||||||
|
per = 4 if "prim=13" in pending[1] else len(pts)
|
||||||
|
for q in range(0, len(pts), per):
|
||||||
|
chunk = pts[q:q + per]
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
xs = [p[0] for p in chunk]
|
||||||
|
ys = [p[1] for p in chunk]
|
||||||
|
x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
|
||||||
|
rot = all(
|
||||||
|
(abs(x - x0) < 1.0 or abs(x - x1) < 1.0)
|
||||||
|
and (abs(y - y0) < 1.0 or abs(y - y1) < 1.0)
|
||||||
|
for x, y in zip(xs, ys)
|
||||||
|
)
|
||||||
|
cols = {p[2] for p in chunk if p[2]}
|
||||||
|
col = sorted(cols)[0] if len(cols) == 1 else (
|
||||||
|
"/".join(sorted(cols)) if cols else "")
|
||||||
|
yield (frame, pending[0], round(x0), round(y0),
|
||||||
|
round(x1 - x0), round(y1 - y0), "" if rot else "ROT",
|
||||||
|
col)
|
||||||
|
pending = None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log = sys.argv[1]
|
||||||
|
out = None
|
||||||
|
if "--csv" in sys.argv:
|
||||||
|
out = open(sys.argv[sys.argv.index("--csv") + 1], "w")
|
||||||
|
out.write("frame,draw,x,y,w,h,rot,col\n")
|
||||||
|
for row in quads(log):
|
||||||
|
line = "%d,%d,%d,%d,%d,%d,%s,%s" % row
|
||||||
|
if out:
|
||||||
|
out.write(line + "\n")
|
||||||
|
else:
|
||||||
|
print(line)
|
||||||
|
if out:
|
||||||
|
out.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
0
tools/re-capture/regn_decode.py
Normal file → Executable file
0
tools/re-capture/regn_decode.py
Normal file → Executable file
87
tools/re-capture/screen_build_capture.sh
Executable file
87
tools/re-capture/screen_build_capture.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Catch a screen being BUILT, not sitting still.
|
||||||
|
#
|
||||||
|
# `log_ui_draws` armed at the title only ever sees the steady state, and the
|
||||||
|
# steady state is the one thing an animation question cannot be asked of. The
|
||||||
|
# build — the wordmarks zooming in, the effect sprites wiping across, the black
|
||||||
|
# fade lifting — happens in the seconds BEFORE the screen classifier can call it
|
||||||
|
# a title.
|
||||||
|
#
|
||||||
|
# So arm repeatedly and keep every log: each F10 opens a new numbered file and
|
||||||
|
# CLOSES the previous one (which stays on disk, complete). Re-arming every few
|
||||||
|
# seconds through the boot therefore tiles the whole approach to the title, and
|
||||||
|
# whichever file happens to straddle the build contains it. Stop re-arming the
|
||||||
|
# instant the title is classified, so a press cannot land mid-build and abandon
|
||||||
|
# the one file that matters.
|
||||||
|
#
|
||||||
|
# NO pad input at all. Ⓐ during the boot has ended a run on a permanent black
|
||||||
|
# screen (docs/re/canary-scripted-input-traps.md), and nothing here needs it.
|
||||||
|
#
|
||||||
|
# Usage: screen_build_capture.sh [out_dir] [timeout_s]
|
||||||
|
# FRAMES= frames per capture window (default 1200 ~ 20 s at 60 Hz)
|
||||||
|
# MAXDRAWS= hard stop per capture (default 80000)
|
||||||
|
# REARM= seconds between F10 presses (default 10)
|
||||||
|
set -u
|
||||||
|
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
|
||||||
|
SD="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
OUT="${1:-/sylph-home/re/buildcap}"
|
||||||
|
TIMEOUT="${2:-600}"
|
||||||
|
mkdir -p "$OUT"; rm -f "$OUT"/xenia_re_ui_draws_*.log
|
||||||
|
|
||||||
|
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
|
||||||
|
shot(){ screenshot "$1" >/dev/null 2>&1; }
|
||||||
|
wide(){ [ "$(identify -format '%w' "$1" 2>/dev/null || echo 0)" -gt 1000 ]; }
|
||||||
|
|
||||||
|
( cd "$OUT" && nohup run-canary \
|
||||||
|
--ui_draw_capture_frames="${FRAMES:-1200}" \
|
||||||
|
--ui_draw_capture_max="${MAXDRAWS:-80000}" \
|
||||||
|
--create_profile_if_none="${SYLPH_TAG:-SylphRE}" \
|
||||||
|
--logged_profile_slot_0_xuid="${SYLPH_XUID:-B13EBABEBABEBABE}" \
|
||||||
|
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
|
||||||
|
sleep "${LAUNCH_WAIT:-8}"
|
||||||
|
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
|
||||||
|
[ -n "$(alive)" ] || { echo "EMULATOR GONE before the window appeared"; exit 4; }
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
win="$(xdotool search --name "Xenia-canary" | tail -1)"
|
||||||
|
echo "WINDOW=$win"
|
||||||
|
|
||||||
|
arm(){
|
||||||
|
xdotool windowactivate "$win" 2>/dev/null
|
||||||
|
xdotool key --window "$win" F10 2>/dev/null
|
||||||
|
xdotool key F10 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
last_arm=-999
|
||||||
|
deadline=$(( SECONDS + TIMEOUT ))
|
||||||
|
s=none
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
|
||||||
|
shot /tmp/sbc.png
|
||||||
|
if [ -s /tmp/sbc.png ]; then
|
||||||
|
wide /tmp/sbc.png || { echo "GRAB IS NOT THE GAME SURFACE at ${SECONDS}s"; exit 6; }
|
||||||
|
s="$(python3 "$SD/screen_id.py" /tmp/sbc.png | awk '{print $1}')"
|
||||||
|
fi
|
||||||
|
if [ "$s" = "${WANT:-title}" ]; then
|
||||||
|
echo "t=${SECONDS}s $s <- STOP re-arming"
|
||||||
|
cp /tmp/sbc.png "$OUT/reached.png"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $(( SECONDS - last_arm )) -ge "${REARM:-10}" ]; then
|
||||||
|
arm; last_arm=$SECONDS
|
||||||
|
echo "t=${SECONDS}s $s (armed)"
|
||||||
|
else
|
||||||
|
echo "t=${SECONDS}s $s"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Let the straddling window run itself out rather than cutting it short.
|
||||||
|
echo "waiting for the in-flight capture to close..."
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
grep -q "UI-CAP. done" "$OUT/canary.stdout" 2>/dev/null && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -6
|
||||||
|
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO CAPTURE LOG"
|
||||||
|
echo "SCREEN BUILD CAPTURE DONE (screen=$s, emulator left running)"
|
||||||
297
tools/re-capture/slb_segment_phase.py
Executable file
297
tools/re-capture/slb_segment_phase.py
Executable file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Why a `.slb` bank's XMA data starts at 1392 / 1468 / 1600 / 1728.
|
||||||
|
|
||||||
|
Static, disc-only. No emulator, no decoder, no run-generated input: everything
|
||||||
|
here is read straight out of `dat/sound.pak` + `dat/sound.p00..p04`.
|
||||||
|
|
||||||
|
The answer (see docs/re/structures/slb-data-offset.md): the XMA packet grid is
|
||||||
|
2048-byte aligned *inside each individual `.pNN` segment file*, but the `.pak`
|
||||||
|
TOC addresses entries in the flat CONCATENATION of those files, at offsets that
|
||||||
|
are themselves multiples of 2048. The segment files are not multiples of 2048
|
||||||
|
long, so the grid phase seen inside an entry is
|
||||||
|
|
||||||
|
X = (cumulative start of the .pNN segment holding the wave) mod 2048
|
||||||
|
|
||||||
|
and the four disc-wide values are just the running sums of the segment sizes.
|
||||||
|
|
||||||
|
Subcommands
|
||||||
|
phases the segment table and where 1392/1468/1600/1728 come from
|
||||||
|
verify predicted vs measured X over every bank on the disc
|
||||||
|
bank <name> full structural dump of one bank entry
|
||||||
|
chain <name> [n] the tiling arithmetic across n consecutive entries
|
||||||
|
|
||||||
|
Usage
|
||||||
|
SYLPHEED_DISC=/work/sylph_extract python3 slb_segment_phase.py verify
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bisect
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
BLOCK = 2048 # XMA1 packet size and the archive's data-offset alignment unit
|
||||||
|
WAVE_HDR = 4096 # RIFF + 32-byte `fmt ` + `Dmmy` pad, always exactly two blocks
|
||||||
|
|
||||||
|
# --- IPFB name hash (crate `sylpheed_formats::hash`, from `sub_82455C78`) -----
|
||||||
|
MODULUS = 0x00FF_F9D7
|
||||||
|
RECIP = 0x8003_1493
|
||||||
|
|
||||||
|
|
||||||
|
def _rotl32(x, n):
|
||||||
|
x &= 0xFFFFFFFF
|
||||||
|
return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def name_hash(name):
|
||||||
|
a = b = 0
|
||||||
|
for byte in name.lower().encode("latin-1"):
|
||||||
|
c = (byte - 256) & 0xFFFFFFFF if byte >= 0x80 else byte
|
||||||
|
a = ((_rotl32(a, 8) & 0xFFFFFF00) + c) & 0xFFFFFFFF
|
||||||
|
b = (b + c) & 0xFFFFFFFF
|
||||||
|
q = _rotl32(((a * RECIP) >> 32) & 0xFFFFFFFF, 9) & 0x1FF
|
||||||
|
a = (a - q * MODULUS) & 0xFFFFFFFF
|
||||||
|
return ((b & 0xFF) << 24) | (a & 0xFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
|
class Pak:
|
||||||
|
"""Minimal IPFB reader: TOC plus raw access to the concatenated segments."""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
data = open(path, "rb").read()
|
||||||
|
assert data[:4] == b"IPFB", data[:4]
|
||||||
|
count, self.block, self.flags = struct.unpack_from(">III", data, 4)
|
||||||
|
self.toc = [struct.unpack_from(">III", data, 0x10 + 12 * i) for i in range(count)]
|
||||||
|
base, pos, self.segs = path[:-4], 0, []
|
||||||
|
i = 0
|
||||||
|
while os.path.exists("%s.p%02d" % (base, i)):
|
||||||
|
seg = "%s.p%02d" % (base, i)
|
||||||
|
n = os.path.getsize(seg)
|
||||||
|
self.segs.append((pos, pos + n, seg))
|
||||||
|
pos, i = pos + n, i + 1
|
||||||
|
self.seg_starts = [s for s, _, _ in self.segs]
|
||||||
|
|
||||||
|
def raw(self, off, size):
|
||||||
|
"""Bytes [off, off+size) of the flat concatenation of the .pNN files."""
|
||||||
|
out = b""
|
||||||
|
for s, e, path in self.segs:
|
||||||
|
if off < e and off + size > s:
|
||||||
|
lo, hi = max(off, s), min(off + size, e)
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
f.seek(lo - s)
|
||||||
|
out += f.read(hi - lo)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def read(self, idx):
|
||||||
|
_, off, csz = self.toc[idx]
|
||||||
|
raw = self.raw(off, csz)
|
||||||
|
return zlib.decompress(raw[10:]) if raw[:2] == b"Z1" else raw
|
||||||
|
|
||||||
|
def find(self, name):
|
||||||
|
h = name_hash(name)
|
||||||
|
for i, (hh, _, _) in enumerate(self.toc):
|
||||||
|
if hh == h:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
def phase_at(self, concat_off):
|
||||||
|
"""The 2048-grid phase in force at a flat-concatenation offset."""
|
||||||
|
k = bisect.bisect_right(self.seg_starts, concat_off) - 1
|
||||||
|
return self.seg_starts[k] % BLOCK
|
||||||
|
|
||||||
|
|
||||||
|
def entry_names(disc):
|
||||||
|
"""Every `.slb` path named in any `<lang>\\sounds.tbl` inside tables.pak."""
|
||||||
|
tbl = Pak(os.path.join(disc, "dat", "tables.pak"))
|
||||||
|
names = set()
|
||||||
|
for lang in ("eng", "jpn", "deu", "fra", "esp", "ita"):
|
||||||
|
i = tbl.find(lang + "\\sounds.tbl")
|
||||||
|
if i is None:
|
||||||
|
continue
|
||||||
|
blob = tbl.read(i)
|
||||||
|
for run in re.finditer(rb"[\x20-\x7e]{6,}", blob):
|
||||||
|
for m in re.finditer(r"[A-Za-z0-9_\\.]+\.slb", run.group().decode("latin-1")):
|
||||||
|
names.add(m.group())
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
# --- structure walkers --------------------------------------------------------
|
||||||
|
def bank_headers(data, phase):
|
||||||
|
"""Bank headers inside an entry. They sit on the grid, so only `phase` is scanned."""
|
||||||
|
out = []
|
||||||
|
for off in range(phase, max(0, len(data) - 56), BLOCK):
|
||||||
|
if data[off + 0x18 : off + 0x1C] != b"\x00\x00\x08\x00":
|
||||||
|
continue
|
||||||
|
if data[off : off + 4] != data[off + 0x20 : off + 0x24]:
|
||||||
|
continue
|
||||||
|
f = struct.unpack(">14I", data[off : off + 56])
|
||||||
|
out.append(
|
||||||
|
dict(at=off, id=f[0], data_size=f[7], hdr_blocks=f[9],
|
||||||
|
bits=f[10] >> 16, channels=f[10] & 0xFFFF)
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def waves(data):
|
||||||
|
out = []
|
||||||
|
for m in re.finditer(b"RIFF", data):
|
||||||
|
r = m.start()
|
||||||
|
if data[r + 8 : r + 12] != b"WAVE":
|
||||||
|
continue
|
||||||
|
dp = data.find(b"data", r)
|
||||||
|
if dp < 0:
|
||||||
|
continue
|
||||||
|
out.append(dict(at=r, data_at=dp + 8,
|
||||||
|
data_size=struct.unpack_from("<I", data, dp + 4)[0]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def seeks(data):
|
||||||
|
"""`seek` (XMA dpds) chunks: one u32 LE per emitted packet, so the entry count
|
||||||
|
is the wave's packet count and the chunk sits immediately after its data."""
|
||||||
|
out = []
|
||||||
|
for m in re.finditer(b"seek", data):
|
||||||
|
s = m.start()
|
||||||
|
if s + 16 > len(data):
|
||||||
|
continue
|
||||||
|
size, streams, n = struct.unpack_from("<III", data, s + 4)
|
||||||
|
if streams == 1 and size == 8 + 4 * n and n:
|
||||||
|
out.append(dict(at=s, packets=n, data_start=s - n * BLOCK))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- subcommands --------------------------------------------------------------
|
||||||
|
def cmd_phases(pak):
|
||||||
|
print("segment size size%2048 cum_start PHASE (= cum_start%2048)")
|
||||||
|
running = 0
|
||||||
|
for s, e, path in pak.segs:
|
||||||
|
n = e - s
|
||||||
|
print(" %-10s %11d %8d %12d %d" % (os.path.basename(path), n, n % BLOCK, s, s % BLOCK))
|
||||||
|
running = s % BLOCK
|
||||||
|
print()
|
||||||
|
print("The four disc-wide values are the running sums of `size % 2048`:")
|
||||||
|
acc = 0
|
||||||
|
for s, e, path in pak.segs:
|
||||||
|
print(" %-10s phase %4d" % (os.path.basename(path), s % BLOCK))
|
||||||
|
acc = (acc + (e - s)) % BLOCK
|
||||||
|
_ = running
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_verify(pak, disc):
|
||||||
|
names = entry_names(disc)
|
||||||
|
ok = bad = 0
|
||||||
|
seek_ok = seek_bad = 0
|
||||||
|
misses = []
|
||||||
|
for name in sorted(names):
|
||||||
|
i = pak.find(name)
|
||||||
|
if i is None:
|
||||||
|
continue
|
||||||
|
_, off, _ = pak.toc[i]
|
||||||
|
data = pak.read(i)
|
||||||
|
w = waves(data)
|
||||||
|
if w:
|
||||||
|
pred = pak.phase_at(off + w[0]["at"])
|
||||||
|
meas = w[0]["at"] % BLOCK
|
||||||
|
if pred == meas:
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
bad += 1
|
||||||
|
if len(misses) < 10:
|
||||||
|
misses.append((name, pred, meas))
|
||||||
|
else:
|
||||||
|
# RIFF-less entry: the prediction is still exact, cross-check on `seek`.
|
||||||
|
sk = seeks(data)
|
||||||
|
if sk:
|
||||||
|
pred = pak.phase_at(off + sk[0]["at"])
|
||||||
|
if sk[0]["at"] % BLOCK == pred:
|
||||||
|
seek_ok += 1
|
||||||
|
else:
|
||||||
|
seek_bad += 1
|
||||||
|
print("banks with a RIFF : predicted == measured %d, mismatches %d" % (ok, bad))
|
||||||
|
print("RIFF-less, via seek : agree %d, disagree %d" % (seek_ok, seek_bad))
|
||||||
|
for m in misses:
|
||||||
|
print(" MISMATCH", m)
|
||||||
|
return 1 if (bad or seek_bad) else 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_bank(pak, name):
|
||||||
|
i = pak.find(name)
|
||||||
|
if i is None:
|
||||||
|
sys.exit("no such entry: " + name)
|
||||||
|
_, off, csz = pak.toc[i]
|
||||||
|
data = pak.read(i)
|
||||||
|
phase = pak.phase_at(off)
|
||||||
|
print("%s\n pak offset %d (%%2048=%d) stored %d bytes phase %d"
|
||||||
|
% (name, off, off % BLOCK, csz, phase))
|
||||||
|
events = []
|
||||||
|
for b in bank_headers(data, phase):
|
||||||
|
events.append((b["at"], "BANK id=%d data_size=%d hdr=%d blocks (%d B) %dbit/%dch -> wave at %d"
|
||||||
|
% (b["id"], b["data_size"], b["hdr_blocks"], b["hdr_blocks"] * BLOCK,
|
||||||
|
b["bits"], b["channels"], b["at"] + b["hdr_blocks"] * BLOCK)))
|
||||||
|
for w in waves(data):
|
||||||
|
over = w["data_at"] + w["data_size"] - len(data)
|
||||||
|
events.append((w["at"], "RIFF data@%d size=%d (%d packets)%s"
|
||||||
|
% (w["data_at"], w["data_size"], w["data_size"] // BLOCK,
|
||||||
|
" OVERRUNS window by %d" % over if over > 0 else "")))
|
||||||
|
for s in seeks(data):
|
||||||
|
events.append((s["at"], "seek %d packets -> its data began at %d%s"
|
||||||
|
% (s["packets"], s["data_start"],
|
||||||
|
" (BEFORE this window)" if s["data_start"] < 0 else "")))
|
||||||
|
for at, text in sorted(events):
|
||||||
|
print(" %8d %%2048=%-5d %s" % (at, at % BLOCK, text))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_chain(pak, name, count):
|
||||||
|
"""Show that entry K's overrunning wave lands exactly on entry K+1's leading `seek`."""
|
||||||
|
i = pak.find(name)
|
||||||
|
if i is None:
|
||||||
|
sys.exit("no such entry: " + name)
|
||||||
|
order = sorted(range(len(pak.toc)), key=lambda k: pak.toc[k][1])
|
||||||
|
start = order.index(i)
|
||||||
|
prev = None
|
||||||
|
for k in order[start : start + count]:
|
||||||
|
_, off, csz = pak.toc[k]
|
||||||
|
data = pak.read(k)
|
||||||
|
padded = (len(data) + BLOCK - 1) // BLOCK * BLOCK
|
||||||
|
sk = seeks(data)
|
||||||
|
lead = sk[0] if sk and sk[0]["data_start"] < 0 else None
|
||||||
|
w = waves(data)
|
||||||
|
tail = w[-1] if w and w[-1]["data_at"] + w[-1]["data_size"] > len(data) else None
|
||||||
|
line = "off=%-11d len=%-7d padded=%-7d" % (off, len(data), padded)
|
||||||
|
if lead:
|
||||||
|
line += " leading seek@%-6d (%d packets)" % (lead["at"], lead["packets"])
|
||||||
|
if tail:
|
||||||
|
line += " tail wave ends at %d" % (tail["data_at"] + tail["data_size"])
|
||||||
|
print(line)
|
||||||
|
if prev and lead:
|
||||||
|
predicted = prev[0] - prev[1] # tail end minus previous padded length
|
||||||
|
mark = "OK " if predicted == lead["at"] else "!! "
|
||||||
|
print(" %s predecessor's wave ends %d past its padded window; leading seek is at %d"
|
||||||
|
% (mark, predicted, lead["at"]))
|
||||||
|
prev = ((tail["data_at"] + tail["data_size"]) if tail else None, padded)
|
||||||
|
if prev[0] is None:
|
||||||
|
prev = None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
disc = os.environ.get("SYLPHEED_DISC")
|
||||||
|
if not disc:
|
||||||
|
sys.exit("set SYLPHEED_DISC to the extracted disc root")
|
||||||
|
pak = Pak(os.path.join(disc, "dat", "sound.pak"))
|
||||||
|
argv = sys.argv[1:] or ["phases"]
|
||||||
|
cmd = argv[0]
|
||||||
|
if cmd == "phases":
|
||||||
|
cmd_phases(pak)
|
||||||
|
elif cmd == "verify":
|
||||||
|
sys.exit(cmd_verify(pak, disc))
|
||||||
|
elif cmd == "bank":
|
||||||
|
cmd_bank(pak, argv[1])
|
||||||
|
elif cmd == "chain":
|
||||||
|
cmd_chain(pak, argv[1], int(argv[2]) if len(argv) > 2 else 4)
|
||||||
|
else:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user