mod charmap;
mod decode;
use std::fmt::Write as _;
use serde::Serialize;
use crate::Result;
use crate::formats::narc::Narc;
use crate::io::ByteSource;
pub(super) const PLATINUM_BANKS: &str = include_str!("platinum_banks.txt");
pub struct Text<'a, A> {
archive: A,
narc: &'a Narc,
names: &'static str,
}
impl<'a, A: ByteSource> Text<'a, A> {
pub fn new(archive: A, narc: &'a Narc, names: &'static str) -> Self {
Self {
archive,
narc,
names,
}
}
pub fn len(&self) -> usize {
self.narc.len()
}
pub fn is_empty(&self) -> bool {
self.narc.is_empty()
}
pub fn name(&self, bank: usize) -> Option<&'static str> {
self.names.lines().nth(bank)
}
pub fn find(&self, name: &str) -> Option<usize> {
self.names.lines().position(|known| known == name)
}
pub fn bank(&self, bank: usize) -> Result<Vec<Message>> {
decode::bank(&self.narc.member(&self.archive, bank)?)
}
pub fn message(&self, bank: usize, index: usize) -> Result<Option<Message>> {
decode::message(&self.narc.member_source(&self.archive, bank)?, index)
}
pub fn strings(&self, bank: usize) -> Result<Vec<String>> {
Ok(self.bank(bank)?.iter().map(Message::plain).collect())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct Message {
pub tokens: Vec<Token>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Token {
Text(String),
LineBreak,
ScrollLine,
ClearBox,
Command {
id: u16,
args: Vec<u16>,
},
Unknown(u16),
}
const STRING_VAR_GROUPS: [u8; 5] = [0x01, 0x03, 0x04, 0x06, 0x34];
fn command_name(id: u16) -> Option<&'static str> {
Some(match id {
0x0200 => "YESNO",
0x0201 => "PAUSE",
0x0202 => "WAIT",
0x0203 => "CURSOR_X",
0x0204 => "CURSOR_Y",
0x0205 => "ALN_CENTER",
0x0206 => "ALN_RIGHT",
0xFF00 => "COLOR",
0xFF01 => "SIZE",
_ => return None,
})
}
impl Message {
pub fn plain(&self) -> String {
let mut out = String::new();
for token in &self.tokens {
match token {
Token::Text(text) => out.push_str(text),
Token::LineBreak | Token::ScrollLine | Token::ClearBox => out.push('\n'),
Token::Command { .. } | Token::Unknown(_) => {}
}
}
out
}
pub fn markup(&self) -> String {
let mut out = String::new();
for token in &self.tokens {
match token {
Token::Text(text) => {
for ch in text.chars() {
if matches!(ch, '\\' | '{') {
out.push('\\');
}
out.push(ch);
}
}
Token::LineBreak => out.push('\n'),
Token::ScrollLine => out.push('\r'),
Token::ClearBox => out.push('\x0C'),
Token::Command { id, args } => write_command(&mut out, *id, args),
Token::Unknown(code) => _ = write!(out, "{{UNK_CHAR {code:#06X}}}"),
}
}
out
}
}
fn write_command(out: &mut String, id: u16, args: &[u16]) {
let [group, kind] = id.to_be_bytes();
let (name, kind) = match command_name(id) {
Some(name) => (name.to_owned(), None),
None if STRING_VAR_GROUPS.contains(&group) => {
(format!("STRVAR_{group:X}"), Some(kind.into()))
}
None => (format!("CMD_{id:04X}"), None),
};
out.push('{');
out.push_str(&name);
for (index, value) in kind.into_iter().chain(args.iter().copied()).enumerate() {
out.push_str(if index == 0 { " " } else { ", " });
_ = write!(out, "{value}");
}
out.push('}');
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn markup_is_lossless_and_plain_is_text_only() {
let message = Message {
tokens: vec![
Token::Text("Hi {there}".into()),
Token::LineBreak,
Token::Command {
id: 0x0101,
args: vec![0],
},
Token::Command {
id: 0x0200,
args: vec![1, 2],
},
Token::Command {
id: 0x7777,
args: vec![],
},
Token::ScrollLine,
Token::Unknown(0x0DEF),
],
};
assert_eq!(
message.markup(),
"Hi \\{there}\n{STRVAR_1 1, 0}{YESNO 1, 2}{CMD_7777}\r{UNK_CHAR 0x0DEF}"
);
assert_eq!(message.plain(), "Hi {there}\n\n");
}
}