Preserve engine type side view cohorts
This commit is contained in:
parent
af21b83300
commit
7741dc2087
3 changed files with 265 additions and 12 deletions
|
|
@ -168,6 +168,7 @@ pub struct EngineTypesInspectionReport {
|
|||
pub car_side_view_resource_pk4_match_count: usize,
|
||||
pub car_side_view_resource_pk4_missing_count: usize,
|
||||
pub car_side_view_resource_pk4_missing_counts: BTreeMap<String, usize>,
|
||||
pub car_side_view_resource_pk4_missing_families: Vec<String>,
|
||||
pub car_auxiliary_stem_counts: BTreeMap<String, usize>,
|
||||
pub car_auxiliary_stem_relation_counts: BTreeMap<String, usize>,
|
||||
pub car_auxiliary_stem_distinct_pair_counts: BTreeMap<String, usize>,
|
||||
|
|
@ -184,6 +185,7 @@ pub struct EngineTypesInspectionReport {
|
|||
pub internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap<String, usize>,
|
||||
pub lco_companion_stem_counts: BTreeMap<String, usize>,
|
||||
pub lco_body_type_label_counts: BTreeMap<String, usize>,
|
||||
pub lco_companion_profile_family_stems: BTreeMap<String, Vec<String>>,
|
||||
pub lco_low_cardinality_lane_counts: BTreeMap<String, BTreeMap<String, usize>>,
|
||||
pub cgo_scalar_value_counts: BTreeMap<String, usize>,
|
||||
pub cgo_scalar_ladder_counts: BTreeMap<String, usize>,
|
||||
|
|
@ -404,7 +406,7 @@ pub fn inspect_engine_types_dir(
|
|||
);
|
||||
let car_side_view_resource_profile_summaries = side_view_imb_pk4_lookup
|
||||
.as_ref()
|
||||
.map(|lookup| lookup.car_side_view_profile_summaries.clone())
|
||||
.map(|lookup| build_side_view_resource_profile_summaries(&family_entries, lookup))
|
||||
.unwrap_or_default();
|
||||
let car_side_view_resource_pk4_match_count = family_entries
|
||||
.iter()
|
||||
|
|
@ -420,6 +422,8 @@ pub fn inspect_engine_types_dir(
|
|||
.filter(|family| family.side_view_resource_found_in_pk4 == Some(false))
|
||||
.filter_map(|family| family.side_view_resource.as_deref()),
|
||||
);
|
||||
let car_side_view_resource_pk4_missing_families =
|
||||
build_car_side_view_resource_pk4_missing_families(&family_entries);
|
||||
let car_auxiliary_stem_counts = count_named_values(
|
||||
family_entries
|
||||
.iter()
|
||||
|
|
@ -523,6 +527,8 @@ pub fn inspect_engine_types_dir(
|
|||
.iter()
|
||||
.filter_map(|family| family.body_type_label.as_deref()),
|
||||
);
|
||||
let lco_companion_profile_family_stems =
|
||||
build_lco_companion_profile_family_stems(&family_entries);
|
||||
let lco_low_cardinality_lane_counts =
|
||||
build_lco_low_cardinality_lane_counts(lco_reports.values());
|
||||
let cgo_scalar_value_counts = count_owned_values(
|
||||
|
|
@ -601,6 +607,7 @@ pub fn inspect_engine_types_dir(
|
|||
car_side_view_resource_pk4_match_count,
|
||||
car_side_view_resource_pk4_missing_count,
|
||||
car_side_view_resource_pk4_missing_counts,
|
||||
car_side_view_resource_pk4_missing_families,
|
||||
car_auxiliary_stem_counts,
|
||||
car_auxiliary_stem_relation_counts,
|
||||
car_auxiliary_stem_distinct_pair_counts,
|
||||
|
|
@ -617,6 +624,7 @@ pub fn inspect_engine_types_dir(
|
|||
internal_ne_profile_max_percent_of_interface_vram_counts,
|
||||
lco_companion_stem_counts,
|
||||
lco_body_type_label_counts,
|
||||
lco_companion_profile_family_stems,
|
||||
lco_low_cardinality_lane_counts,
|
||||
cgo_scalar_value_counts,
|
||||
cgo_scalar_ladder_counts,
|
||||
|
|
@ -642,7 +650,7 @@ struct EngineTypeFamilyBuilder {
|
|||
struct SideViewImbPk4Lookup {
|
||||
path: String,
|
||||
entry_names: BTreeSet<String>,
|
||||
car_side_view_profile_summaries: BTreeMap<String, EngineTypeImbProfileSummary>,
|
||||
imb_profile_summaries_by_entry_name: BTreeMap<String, EngineTypeImbProfileSummary>,
|
||||
internal_ne_profile_texture_size_counts: BTreeMap<String, usize>,
|
||||
internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap<String, usize>,
|
||||
internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap<String, usize>,
|
||||
|
|
@ -712,24 +720,22 @@ fn load_side_view_imb_pk4_lookup(
|
|||
let bytes = fs::read(&pk4_path)?;
|
||||
let inspection = inspect_pk4_file(&pk4_path)?;
|
||||
let mut entry_names = BTreeSet::new();
|
||||
let mut car_side_view_profile_summaries = BTreeMap::new();
|
||||
let mut imb_profile_summaries_by_entry_name = BTreeMap::new();
|
||||
let mut internal_ne_profile_texture_size_counts = BTreeMap::new();
|
||||
let mut internal_ne_profile_horizontal_scale_modifier_counts = BTreeMap::new();
|
||||
let mut internal_ne_profile_max_percent_of_interface_vram_counts = BTreeMap::new();
|
||||
|
||||
for entry in inspection.entries {
|
||||
entry_names.insert(entry.name.clone());
|
||||
let is_car_side_view = entry.name.starts_with("CarSideView_");
|
||||
let is_imb_profile = entry.name.ends_with(".imb");
|
||||
let is_internal_ne = entry.name.ends_with("_NE.imb");
|
||||
if !(is_car_side_view || is_internal_ne) {
|
||||
if !is_imb_profile {
|
||||
continue;
|
||||
}
|
||||
|
||||
let payload = &bytes[entry.payload_absolute_offset..entry.payload_end_offset];
|
||||
let imb_profile = summarize_imb_profile(&inspect_imb_bytes(payload)?);
|
||||
if is_car_side_view {
|
||||
car_side_view_profile_summaries.insert(entry.name.clone(), imb_profile.clone());
|
||||
}
|
||||
imb_profile_summaries_by_entry_name.insert(entry.name.clone(), imb_profile.clone());
|
||||
if is_internal_ne {
|
||||
if let (Some(width), Some(height)) =
|
||||
(imb_profile.texture_width, imb_profile.texture_height)
|
||||
|
|
@ -754,7 +760,7 @@ fn load_side_view_imb_pk4_lookup(
|
|||
Ok(Some(SideViewImbPk4Lookup {
|
||||
path: pk4_path.display().to_string(),
|
||||
entry_names,
|
||||
car_side_view_profile_summaries,
|
||||
imb_profile_summaries_by_entry_name,
|
||||
internal_ne_profile_texture_size_counts,
|
||||
internal_ne_profile_horizontal_scale_modifier_counts,
|
||||
internal_ne_profile_max_percent_of_interface_vram_counts,
|
||||
|
|
@ -1110,6 +1116,61 @@ fn build_lco_low_cardinality_lane_counts<'a>(
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn build_car_side_view_resource_pk4_missing_families(
|
||||
families: &[EngineTypeFamilyEntry],
|
||||
) -> Vec<String> {
|
||||
let mut stems = families
|
||||
.iter()
|
||||
.filter(|family| family.side_view_resource_found_in_pk4 == Some(false))
|
||||
.map(|family| family.canonical_stem.clone())
|
||||
.collect::<Vec<_>>();
|
||||
stems.sort();
|
||||
stems
|
||||
}
|
||||
|
||||
fn build_lco_companion_profile_family_stems(
|
||||
families: &[EngineTypeFamilyEntry],
|
||||
) -> BTreeMap<String, Vec<String>> {
|
||||
let mut grouped = BTreeMap::<String, Vec<String>>::new();
|
||||
for family in families {
|
||||
let Some(companion_stem) = family.companion_stem.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let body_type_label = family.body_type_label.as_deref().unwrap_or("(none)");
|
||||
let side_view_resource = family.side_view_resource.as_deref().unwrap_or("(none)");
|
||||
grouped
|
||||
.entry(format!(
|
||||
"companion={companion_stem} / body={body_type_label} / side_view={side_view_resource}"
|
||||
))
|
||||
.or_default()
|
||||
.push(family.canonical_stem.clone());
|
||||
}
|
||||
for stems in grouped.values_mut() {
|
||||
stems.sort();
|
||||
}
|
||||
grouped
|
||||
}
|
||||
|
||||
fn build_side_view_resource_profile_summaries(
|
||||
families: &[EngineTypeFamilyEntry],
|
||||
lookup: &SideViewImbPk4Lookup,
|
||||
) -> BTreeMap<String, EngineTypeImbProfileSummary> {
|
||||
let mut summaries = BTreeMap::new();
|
||||
for resource_name in families
|
||||
.iter()
|
||||
.filter_map(|family| family.side_view_resource.as_deref())
|
||||
{
|
||||
let Some(summary) = lookup
|
||||
.imb_profile_summaries_by_entry_name
|
||||
.get(resource_name)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
summaries.insert(resource_name.to_string(), summary.clone());
|
||||
}
|
||||
summaries
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -1438,6 +1499,122 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collects_missing_side_view_resource_family_stems() {
|
||||
let missing = build_car_side_view_resource_pk4_missing_families(&[
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "zephyrl".to_string(),
|
||||
side_view_resource_found_in_pk4: Some(false),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "gp35l".to_string(),
|
||||
side_view_resource_found_in_pk4: Some(false),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "gp7".to_string(),
|
||||
side_view_resource_found_in_pk4: Some(true),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(missing, vec!["gp35l".to_string(), "zephyrl".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groups_lco_companion_profiles_by_companion_body_and_side_view() {
|
||||
let grouped = build_lco_companion_profile_family_stems(&[
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "gp35l".to_string(),
|
||||
companion_stem: Some("Zephyr".to_string()),
|
||||
side_view_resource: Some("CarSideView_3.imb".to_string()),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "u1l".to_string(),
|
||||
companion_stem: Some("Zephyr".to_string()),
|
||||
side_view_resource: Some("CarSideView_3.imb".to_string()),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "gp7".to_string(),
|
||||
companion_stem: Some("VL80T".to_string()),
|
||||
body_type_label: Some("Loco".to_string()),
|
||||
side_view_resource: Some("CarSideView_1.imb".to_string()),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
grouped.get("companion=Zephyr / body=(none) / side_view=CarSideView_3.imb"),
|
||||
Some(&vec!["gp35l".to_string(), "u1l".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
grouped.get("companion=VL80T / body=Loco / side_view=CarSideView_1.imb"),
|
||||
Some(&vec!["gp7".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_profile_summaries_for_all_referenced_side_view_resources() {
|
||||
let summaries = build_side_view_resource_profile_summaries(
|
||||
&[
|
||||
EngineTypeFamilyEntry {
|
||||
side_view_resource: Some("CarSideView_1.imb".to_string()),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
EngineTypeFamilyEntry {
|
||||
side_view_resource: Some("GP7D_Profile.imb".to_string()),
|
||||
..minimal_family_entry()
|
||||
},
|
||||
],
|
||||
&SideViewImbPk4Lookup {
|
||||
path: "rt3_2IMB.PK4".to_string(),
|
||||
entry_names: BTreeSet::new(),
|
||||
imb_profile_summaries_by_entry_name: BTreeMap::from([
|
||||
(
|
||||
"CarSideView_1.imb".to_string(),
|
||||
EngineTypeImbProfileSummary {
|
||||
tga_name: Some("CarSideView_1".to_string()),
|
||||
texture_width: Some(512),
|
||||
texture_height: Some(512),
|
||||
target_screen_width: None,
|
||||
target_screen_height: None,
|
||||
horizontal_scale_modifier: None,
|
||||
max_percent_of_interface_vram: Some(0.04),
|
||||
image_rect_scaled: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"GP7D_Profile.imb".to_string(),
|
||||
EngineTypeImbProfileSummary {
|
||||
tga_name: Some("GP7D_Profile".to_string()),
|
||||
texture_width: Some(256),
|
||||
texture_height: Some(128),
|
||||
target_screen_width: None,
|
||||
target_screen_height: None,
|
||||
horizontal_scale_modifier: Some(0.75),
|
||||
max_percent_of_interface_vram: Some(0.09),
|
||||
image_rect_scaled: None,
|
||||
},
|
||||
),
|
||||
]),
|
||||
internal_ne_profile_texture_size_counts: BTreeMap::new(),
|
||||
internal_ne_profile_horizontal_scale_modifier_counts: BTreeMap::new(),
|
||||
internal_ne_profile_max_percent_of_interface_vram_counts: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(summaries.len(), 2);
|
||||
assert_eq!(
|
||||
summaries
|
||||
.get("GP7D_Profile.imb")
|
||||
.and_then(|summary| summary.tga_name.as_deref()),
|
||||
Some("GP7D_Profile")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_cgo_content_stems_to_matching_cct_values() {
|
||||
let families = vec![
|
||||
|
|
@ -1559,4 +1736,27 @@ mod tests {
|
|||
assert_eq!(report.entries[0].primary_display_name, "2-D-2");
|
||||
assert!(report.entries[0].matches_grounded_prefix_name);
|
||||
}
|
||||
|
||||
fn minimal_family_entry() -> EngineTypeFamilyEntry {
|
||||
EngineTypeFamilyEntry {
|
||||
canonical_stem: "family".to_string(),
|
||||
car_file: None,
|
||||
lco_file: None,
|
||||
cgo_file: None,
|
||||
cct_file: None,
|
||||
primary_display_name: None,
|
||||
content_name: None,
|
||||
internal_stem: None,
|
||||
auxiliary_stem: None,
|
||||
side_view_resource: None,
|
||||
side_view_resource_found_in_pk4: None,
|
||||
companion_stem: None,
|
||||
body_type_label: None,
|
||||
internal_ne_profile_name: None,
|
||||
internal_ne_profile_found_in_pk4: None,
|
||||
cct_identifier: None,
|
||||
cct_value: None,
|
||||
has_matched_locomotive_pair: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ This file is the short active queue for the current runtime and reverse-engineer
|
|||
The checked 1.05 corpus now also splits `.car` auxiliary stems into `126` direct matches, `14` role-neutral roots, and only `5` truly distinct cases, with those five exact internal-to-auxiliary pairs now preserved directly in the report surface, while `.cgo` collapses into five stable scalar ladders instead of arbitrary floats.
|
||||
The early `.lco` lane block is now partially partitioned too: only offsets `0x20`, `0x34`, `0x38`, `0x3c`, `0x44`, `0x48`, and `0x54` behave like low-cardinality buckets, while the other early lanes still look high-variance.
|
||||
The side-view resource path is now grounded into `Data/2D/rt3_2IMB.PK4`, and the `.imb` parser now decodes shipped comment-suffixed numeric rows plus `_NE` profile fields such as `HorizontalScaleModifier` and `ImageWHScaled`.
|
||||
The checked PK4 linkage split is now explicit too: `132 / 145` side-view resource names resolve directly, but the remaining `13` are the missing `CarSideView_3.imb` cohort and that hole exists in both checked installs, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs.
|
||||
The checked PK4 linkage split is now explicit too: `132 / 145` side-view resource names resolve directly in 1.05, but the remaining `13` are the missing `CarSideView_3.imb` cohort and that hole exists in both checked installs, while `43 / 145` derived `{internal_stem}_NE.imb` names resolve and all of those hits belong to matched locomotive pairs.
|
||||
The parser now preserves that `CarSideView_3` miss cohort exactly, and it also preserves the tiny conditional `.lco` companion-profile seam directly: in 1.05 the padded `.lco` companion/body slots collapse to `Zephyr / (none) / CarSideView_3` for `242_a1_l`, `gp35l`, `u1l`, and `zephyrl`, and to `VL80T / Loco / CarSideView_{1,2}` for `be 5-7`, `f3 loco`, and `gp7`.
|
||||
The classic install widens the same `0xc0` seam further: the side-view slot is not only `CarSideView_*` but also a larger unresolved `*_Profile.imb` family, and every checked classic `*_Profile.imb` reference currently misses the packaged `rt3_2IMB.PK4` surface too.
|
||||
The packaged profile metadata is stable enough to summarize: `CarSideView_1` is `512x512` at `0.04` VRAM, `CarSideView_2` is `512x256` at `0.02`, and every packaged `_NE` profile is `512x128` with `HorizontalScaleModifier = 0.75` and `MaxPercentOfInterfaceVRAM = 0.09`.
|
||||
The `_NE` split is now aligned with the locomotive display census too: all `43` packaged `_NE` hits live inside the grounded display prefix, and all `5` unmatched display-tail families are still missing packaged `_NE` profiles.
|
||||
The cargo side is partially linked now as well: the `.cgo` ladder families and `.cct` sidecar identifiers share the same cargo-family keys for ten checked families, with `Troop` left as the only `.cct`-only outlier.
|
||||
The next honest static work is to keep promoting those fixed lanes into stable parser fields, explain the five remaining distinct auxiliary-stem cases, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting.
|
||||
The next honest static work is to keep promoting those fixed lanes into stable parser fields, explain the five remaining distinct auxiliary-stem cases, decide whether the classic `*_Profile.imb` side-view references are dead loose-file dependencies or a still-unmapped package family, and decide how far the `.cgo` ladders plus the low-cardinality `.lco` lanes can be grounded without overclaiming semantics. The latest corpus check did narrow one point already: the low-cardinality `.lco` lanes do not split cleanly on `_NE` presence, so that branch now wants binary/code correlation rather than more aggregate-only counting.
|
||||
Preserved checked parser detail now lives in [EngineTypes parser semantics](rehost-queue/engine-types-parser-semantics-2026-04-21.md).
|
||||
Preserved checked format inventory detail now lives in [RT3 format inventory](rehost-queue/format-inventory-2026-04-21.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,54 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
|
|||
- no freight-car or tender family currently resolves to a packaged `{internal_stem}_NE.imb`
|
||||
- That `CarSideView_3.imb` hole is now confirmed across both checked installs (`rt3` and
|
||||
`rt3_105`): neither shipped `rt3_2IMB.PK4` contains a `CarSideView_3.imb` entry.
|
||||
- The parser now preserves the exact 1.05 `CarSideView_3.imb` miss cohort directly:
|
||||
- `242_a1_l`
|
||||
- `242_a1_t`
|
||||
- `class_460`
|
||||
- `class_a1l`
|
||||
- `class_a1t`
|
||||
- `class_p8l`
|
||||
- `class_p8t`
|
||||
- `class_qjl`
|
||||
- `class_qjt`
|
||||
- `gp35l`
|
||||
- `u1l`
|
||||
- `u1t`
|
||||
- `zephyrl`
|
||||
- The conditional `.lco` companion/body seam is also narrow enough to preserve exactly in 1.05:
|
||||
- `companion=Zephyr / body=(none) / side_view=CarSideView_3.imb`
|
||||
- `242_a1_l`
|
||||
- `gp35l`
|
||||
- `u1l`
|
||||
- `zephyrl`
|
||||
- `companion=VL80T / body=Loco / side_view=CarSideView_1.imb`
|
||||
- `f3 loco`
|
||||
- `gp7`
|
||||
- `companion=VL80T / body=Loco / side_view=CarSideView_2.imb`
|
||||
- `be 5-7`
|
||||
- The classic install widens the `0xc0` side-view seam beyond `CarSideView_*`.
|
||||
The parser now confirms a larger checked classic `*_Profile.imb` reference family at `0xc0`,
|
||||
and every one of those checked classic profile names currently misses the packaged
|
||||
`rt3_2IMB.PK4` entry set too:
|
||||
- `264T_Profile.imb`
|
||||
- `Black5_Profile.imb`
|
||||
- `BR39_Profile.imb`
|
||||
- `ClassS_Profile.imb`
|
||||
- `Daylight484_Profile.imb`
|
||||
- `E111_Profile.imb`
|
||||
- `Eurostar_Profile.imb`
|
||||
- `G10_Profile.imb`
|
||||
- `G4_Profile.imb`
|
||||
- `Goods_Profile.imb`
|
||||
- `GP40-2_Profile.imb`
|
||||
- `GP7D_Profile.imb`
|
||||
- `ICE_Profile.imb`
|
||||
- `Mogul_Profile.imb`
|
||||
- `P-2_Profile.imb`
|
||||
- `Sm2_Profile.imb`
|
||||
- `TenWheeler_Profile.imb`
|
||||
- `V2Class_Profile.imb`
|
||||
- `Vittorio_Profile.imb`
|
||||
- The packaged profile metadata is now bounded too:
|
||||
- `CarSideView_1.imb`: `512x512`, `MaxPercentOfInterfaceVRAM = 0.04`
|
||||
- `CarSideView_2.imb`: `512x256`, `MaxPercentOfInterfaceVRAM = 0.02`
|
||||
|
|
@ -123,9 +171,12 @@ first `.car` / `.lco` / `.cgo` / `.cct` inspector pass landed.
|
|||
- what the `0xa2` auxiliary stem really represents in the five remaining distinct cases:
|
||||
alternate content root, paired tender/loco image root, or a narrower foreign-display alias
|
||||
- whether the trailing side-view resource can be tied cleanly to the PK4-backed `CarSideView_*`
|
||||
metadata and the engine-specific `_NE.imb` profiles without inventing frontend semantics
|
||||
metadata, the classic `*_Profile.imb` reference family, and the engine-specific `_NE.imb`
|
||||
profiles without inventing frontend semantics
|
||||
- whether the `CarSideView_3.imb` missing cohort is a classic-only asset dependency, an omitted
|
||||
1.05 package, or just a dead resource reference in the shipped `EngineTypes` data
|
||||
- whether the classic `*_Profile.imb` references are dead loose-file dependencies, a separate
|
||||
package family outside `rt3_2IMB.PK4`, or a still-unmapped authoring convention
|
||||
- `.lco`
|
||||
- whether the guarded companion-stem slot is a tender/fallback display family, a foreign reuse
|
||||
key, or only a subset authoring convenience
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue