use super::*;
pub(super) fn build_position_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut entries = Vec::new();
if let Some(cover_fid) = ctx.cover_fragment_id {
let mut contains_list = vec![IonValue::Int(cover_fid as i64)];
if let Some(content_id) = ctx.cover_content_id {
contains_list.push(IonValue::Int(content_id as i64));
}
let entry = IonValue::Struct(vec![
(KfxSymbol::Contains as u64, IonValue::List(contains_list)),
(
KfxSymbol::SectionName as u64,
IonValue::Symbol(ctx.section_ids[0]),
),
]);
entries.push(entry);
}
for &(section_sym, chapter_id) in &ctx.spine_section_chapters {
let Some(&fragment_id) = ctx.chapter_fragments.get(&chapter_id) else {
continue;
};
let mut eid_list = Vec::new();
eid_list.push(IonValue::Int(fragment_id as i64));
if let Some(content_ids) = ctx.content_ids_by_chapter.get(&chapter_id) {
for &content_id in content_ids {
eid_list.push(IonValue::Int(content_id as i64));
}
}
let entry = IonValue::Struct(vec![
(KfxSymbol::Contains as u64, IonValue::List(eid_list)),
(KfxSymbol::SectionName as u64, IonValue::Symbol(section_sym)),
]);
entries.push(entry);
}
let ion = IonValue::List(entries);
KfxFragment::singleton(KfxSymbol::PositionMap, ion)
}
pub(super) fn build_position_id_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut entries = Vec::new();
let mut pid = 0i64;
if let Some(cover_id) = ctx.cover_content_id {
let content_len = ctx
.content_id_lengths
.get(&cover_id)
.copied()
.unwrap_or(1)
.max(1) as i64;
let entry = IonValue::Struct(vec![
(KfxSymbol::Eid as u64, IonValue::Int(cover_id as i64)),
(KfxSymbol::Pid as u64, IonValue::Int(pid)),
]);
entries.push(entry);
pid += content_len;
}
let mut chapter_entries: Vec<_> = ctx.chapter_fragments.iter().collect();
chapter_entries.sort_by_key(|(_, fid)| **fid);
for (chapter_id, _) in &chapter_entries {
if let Some(content_ids) = ctx.content_ids_by_chapter.get(chapter_id) {
for &eid in content_ids {
let content_len = ctx
.content_id_lengths
.get(&eid)
.copied()
.unwrap_or(1)
.max(1) as i64;
let entry = IonValue::Struct(vec![
(KfxSymbol::Eid as u64, IonValue::Int(eid as i64)),
(KfxSymbol::Pid as u64, IonValue::Int(pid)),
]);
entries.push(entry);
pid += content_len;
}
}
}
let terminator = IonValue::Struct(vec![
(KfxSymbol::Eid as u64, IonValue::Int(0)),
(KfxSymbol::Pid as u64, IonValue::Int(pid)),
]);
entries.push(terminator);
let ion = IonValue::List(entries);
KfxFragment::singleton(KfxSymbol::PositionIdMap, ion)
}
pub(super) fn build_location_map_fragment(ctx: &ExportContext) -> KfxFragment {
let mut location_entries = Vec::new();
let mut process_content_id = |content_id: u64| {
let entry = IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Int(content_id as i64)),
(KfxSymbol::Offset as u64, IonValue::Int(0)),
]);
location_entries.push(entry);
};
if let Some(cover_id) = ctx.cover_content_id {
process_content_id(cover_id);
}
let mut chapter_entries: Vec<_> = ctx.chapter_fragments.iter().collect();
chapter_entries.sort_by_key(|(_, fid)| **fid);
for (chapter_id, _) in &chapter_entries {
if let Some(content_ids) = ctx.content_ids_by_chapter.get(chapter_id) {
for &content_id in content_ids {
process_content_id(content_id);
}
}
}
let ion = IonValue::List(vec![IonValue::Struct(vec![(
KfxSymbol::Locations as u64,
IonValue::List(location_entries),
)])]);
KfxFragment::singleton(KfxSymbol::LocationMap, ion)
}
pub(super) fn build_container_entity_map_fragment(
container_id: &str,
fragments: &[KfxFragment],
ctx: &ExportContext,
) -> KfxFragment {
let mut entity_names: Vec<IonValue> = Vec::new();
for frag in fragments {
if frag.fid.starts_with('$') {
continue;
}
if let Some(symbol_id) = ctx.symbols.get(&frag.fid) {
entity_names.push(IonValue::Symbol(symbol_id));
}
}
let container_entry = IonValue::Struct(vec![
(
KfxSymbol::Id as u64,
IonValue::String(container_id.to_string()),
),
(KfxSymbol::Contains as u64, IonValue::List(entity_names)),
]);
let mut dependencies: Vec<IonValue> = Vec::new();
for (section_name, short_names) in &ctx.section_resource_deps {
if short_names.is_empty() {
continue;
}
let Some(section_sym) = ctx.symbols.get(section_name) else {
continue;
};
let deps: Vec<IonValue> = short_names
.iter()
.filter_map(|n| ctx.symbols.get(n).map(IonValue::Symbol))
.collect();
if deps.is_empty() {
continue;
}
dependencies.push(IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Symbol(section_sym)),
(
KfxSymbol::MandatoryDependencies as u64,
IonValue::List(deps),
),
]));
}
let mut all_short_names: BTreeSet<&String> = BTreeSet::new();
for short_names in ctx.section_resource_deps.values() {
for n in short_names {
all_short_names.insert(n);
}
}
for short_name in all_short_names {
let Some(resource_sym) = ctx.symbols.get(short_name) else {
continue;
};
let raw_name = format!("resource/{short_name}");
let Some(raw_sym) = ctx.symbols.get(&raw_name) else {
continue;
};
dependencies.push(IonValue::Struct(vec![
(KfxSymbol::Id as u64, IonValue::Symbol(resource_sym)),
(
KfxSymbol::MandatoryDependencies as u64,
IonValue::List(vec![IonValue::Symbol(raw_sym)]),
),
]));
}
let mut ion_fields = vec![(
KfxSymbol::ContainerList as u64,
IonValue::List(vec![container_entry]),
)];
if !dependencies.is_empty() {
ion_fields.push((
KfxSymbol::EntityDependencies as u64,
IonValue::List(dependencies),
));
}
let ion = IonValue::Struct(ion_fields);
KfxFragment::singleton(KfxSymbol::ContainerEntityMap, ion)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ChapterId;
use crate::kfx::fragment::FragmentData;
fn extract_entries(frag: &KfxFragment) -> Vec<(u64, Vec<i64>)> {
let FragmentData::Ion(IonValue::List(entries)) = &frag.data else {
panic!("expected Ion list fragment data");
};
entries
.iter()
.map(|entry| {
let IonValue::Struct(fields) = entry else {
panic!("expected struct entry");
};
let mut section = None;
let mut eids = Vec::new();
for (id, value) in fields {
if *id == KfxSymbol::SectionName as u64 {
if let IonValue::Symbol(s) = value {
section = Some(*s);
}
} else if *id == KfxSymbol::Contains as u64
&& let IonValue::List(list) = value
{
for item in list {
if let IonValue::Int(i) = item {
eids.push(*i);
}
}
}
}
(section.expect("entry must have section_name"), eids)
})
.collect()
}
#[test]
fn position_map_pairs_sections_with_chapters_by_key() {
let mut ctx = ExportContext::new();
let (ch1, ch2, ch3) = (ChapterId(1), ChapterId(2), ChapterId(3));
let c0 = ctx.register_spine_section("c0", ch1);
let c1 = ctx.register_spine_section("c1", ch2);
let c2 = ctx.register_spine_section("c2", ch3);
ctx.chapter_fragments.insert(ch1, 90);
ctx.chapter_fragments.insert(ch2, 95);
ctx.chapter_fragments.insert(ch3, 100);
ctx.content_ids_by_chapter.insert(ch1, vec![91]);
ctx.content_ids_by_chapter.insert(ch2, vec![96, 97]);
ctx.content_ids_by_chapter.insert(ch3, vec![101]);
let frag = build_position_map_fragment(&ctx);
let entries = extract_entries(&frag);
assert_eq!(
entries,
vec![
(c0, vec![90, 91]),
(c1, vec![95, 96, 97]),
(c2, vec![100, 101]),
]
);
}
#[test]
fn position_map_skips_failed_chapter_without_shifting_sections() {
let mut ctx = ExportContext::new();
let (ch1, ch2, ch3) = (ChapterId(1), ChapterId(2), ChapterId(3));
let c0 = ctx.register_spine_section("c0", ch1);
let _c1 = ctx.register_spine_section("c1", ch2);
let c2 = ctx.register_spine_section("c2", ch3);
ctx.chapter_fragments.insert(ch1, 90);
ctx.chapter_fragments.insert(ch3, 100);
ctx.content_ids_by_chapter.insert(ch1, vec![91]);
ctx.content_ids_by_chapter.insert(ch3, vec![101]);
let frag = build_position_map_fragment(&ctx);
let entries = extract_entries(&frag);
assert_eq!(entries, vec![(c0, vec![90, 91]), (c2, vec![100, 101])]);
}
}