#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TilePos {
pub x: u16,
pub y: u16,
}
impl TilePos {
pub fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileEntry {
pub tile_id: u16,
pub ink: u8,
}
impl Default for TileEntry {
fn default() -> Self {
Self { tile_id: 0, ink: 0 }
}
}
pub struct TileBuffer {
pub tiles: Vec<TileEntry>,
pub width_tiles: u16,
pub height_tiles: u16,
pub cursor: TilePos,
}
impl TileBuffer {
pub fn new(width_tiles: u16, height_tiles: u16) -> Self {
let size = (width_tiles as usize) * (height_tiles as usize);
Self {
tiles: vec![TileEntry::default(); size],
width_tiles,
height_tiles,
cursor: TilePos::new(0, 0),
}
}
#[inline]
fn index(&self, x: u16, y: u16) -> usize {
(y * self.width_tiles + x) as usize
}
pub fn clear(&mut self) {
self.tiles.fill(TileEntry::default());
self.cursor = TilePos::new(0, 0);
}
pub fn set_tile(&mut self, pos: TilePos, tile_id: u16, ink: u8) {
if pos.x < self.width_tiles && pos.y < self.height_tiles {
let idx = self.index(pos.x, pos.y);
self.tiles[idx] = TileEntry { tile_id, ink };
}
}
pub fn newline(&mut self) {
self.cursor.x = 0;
self.cursor.y += 1;
}
pub fn scroll(&mut self) {
for y in 1..self.height_tiles {
for x in 0..self.width_tiles {
let src = self.index(x, y);
let dst = self.index(x, y - 1);
self.tiles[dst] = self.tiles[src];
}
}
for x in 0..self.width_tiles {
let idx = self.index(x, self.height_tiles - 1);
self.tiles[idx] = TileEntry::default();
}
}
}
impl Default for TileBuffer {
fn default() -> Self {
Self::new(20, 18)
}
}
pub struct TextStream<C> {
pub chars: Vec<C>,
pub pos: usize,
}
impl<C> TextStream<C> {
pub fn new(chars: Vec<C>) -> Self {
Self { chars, pos: 0 }
}
pub fn next(&mut self) -> Option<&C> {
let c = self.chars.get(self.pos);
if c.is_some() {
self.pos += 1;
}
c
}
pub fn peek(&self) -> Option<&C> {
self.chars.get(self.pos)
}
pub fn skip(&mut self, n: usize) {
self.pos = (self.pos + n).min(self.chars.len());
}
pub fn remaining(&self) -> &[C] {
&self.chars[self.pos..]
}
pub fn is_at_end(&self) -> bool {
self.pos >= self.chars.len()
}
pub fn position(&self) -> usize {
self.pos
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlAction {
None,
Newline,
PageBreak,
Done,
SetSpeed(u8),
Clear,
CallScript,
WaitInput,
MoveCursor { x: u16, y: u16 },
Scroll,
Pause(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DialogMode {
Typing,
Paused,
WaitingForInput,
Scrolling,
Done,
}
#[derive(Debug, Clone)]
pub struct DialogState {
pub mode: DialogMode,
pub page_index: usize,
pub char_index: usize,
pub scroll_offset: u16,
}
impl Default for DialogState {
fn default() -> Self {
Self {
mode: DialogMode::Typing,
page_index: 0,
char_index: 0,
scroll_offset: 0,
}
}
}
pub trait TextProvider {
type Char: Clone + std::fmt::Debug;
fn decode_byte(&self, byte: u8) -> Option<Self::Char>;
fn decode_stream(&self, bytes: &[u8]) -> TextStream<Self::Char> {
let chars: Vec<Self::Char> = bytes
.iter()
.filter_map(|&b| self.decode_byte(b))
.collect();
TextStream::new(chars)
}
fn render_char(&self, c: &Self::Char, buffer: &mut TileBuffer);
fn string_width(&self, text: &[Self::Char]) -> u16;
fn is_control_code(&self, c: &Self::Char) -> bool;
fn process_control(&self, c: &Self::Char, state: &mut DialogState) -> ControlAction;
}
pub struct DialogEngine<P: TextProvider> {
pub provider: P,
pub stream: Option<TextStream<P::Char>>,
pub state: DialogState,
}
impl<P: TextProvider> DialogEngine<P> {
pub fn new(provider: P) -> Self {
Self {
provider,
stream: None,
state: DialogState::default(),
}
}
pub fn open_dialog(&mut self, text: &[u8]) {
self.stream = Some(self.provider.decode_stream(text));
self.state = DialogState::default();
}
pub fn update(&mut self, buffer: &mut TileBuffer) {
if self.state.mode != DialogMode::Typing {
return;
}
let stream = match &mut self.stream {
Some(s) => s,
None => return,
};
let c = match stream.next() {
Some(c) => c.clone(),
None => {
self.state.mode = DialogMode::Done;
return;
}
};
if self.provider.is_control_code(&c) {
let action = self.provider.process_control(&c, &mut self.state);
match action {
ControlAction::Newline => buffer.newline(),
ControlAction::Done => {
self.state.mode = DialogMode::Done;
}
ControlAction::Clear => buffer.clear(),
ControlAction::PageBreak => {
self.state.mode = DialogMode::WaitingForInput;
}
ControlAction::WaitInput => {
self.state.mode = DialogMode::WaitingForInput;
}
ControlAction::MoveCursor { x, y } => {
buffer.cursor = TilePos::new(x, y);
}
ControlAction::Scroll => buffer.scroll(),
ControlAction::Pause(_frames) => {
self.state.mode = DialogMode::Paused;
}
_ => {}
}
} else {
self.provider.render_char(&c, buffer);
}
if stream.is_at_end() && self.state.mode == DialogMode::Typing {
self.state.mode = DialogMode::Done;
}
}
pub fn advance(&mut self) {
match self.state.mode {
DialogMode::Paused => {
self.state.mode = DialogMode::Typing;
}
DialogMode::WaitingForInput | DialogMode::Scrolling => {
self.state.mode = DialogMode::Typing;
}
DialogMode::Done => {
self.stream = None;
self.state = DialogState::default();
}
_ => {}
}
}
pub fn is_active(&self) -> bool {
self.stream.is_some() && self.state.mode != DialogMode::Done
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MockChar {
Ascii(char),
Digit(u8),
Newline,
Done,
}
struct MockAsciiProvider;
impl TextProvider for MockAsciiProvider {
type Char = MockChar;
fn decode_byte(&self, byte: u8) -> Option<Self::Char> {
match byte {
b @ 0x00..=0x09 => Some(MockChar::Digit(b)),
0x41..=0x5A => Some(MockChar::Ascii(byte as char)),
0xFE => Some(MockChar::Newline),
0xFF => Some(MockChar::Done),
_ => None,
}
}
fn render_char(&self, c: &Self::Char, buffer: &mut TileBuffer) {
let tile_id = match c {
MockChar::Ascii(ch) => *ch as u16,
MockChar::Digit(d) => (b'0' + d) as u16,
MockChar::Newline | MockChar::Done => return,
};
let pos = buffer.cursor;
buffer.set_tile(pos, tile_id, 0);
buffer.cursor.x += 1;
}
fn string_width(&self, text: &[Self::Char]) -> u16 {
text.len() as u16 * 8
}
fn is_control_code(&self, c: &Self::Char) -> bool {
matches!(c, MockChar::Newline | MockChar::Done)
}
fn process_control(&self, c: &Self::Char, _state: &mut DialogState) -> ControlAction {
match c {
MockChar::Newline => ControlAction::Newline,
MockChar::Done => ControlAction::Done,
_ => ControlAction::None,
}
}
}
#[test]
fn test_decode_hello_world() {
let provider = MockAsciiProvider;
let bytes = [0x48, 0x45, 0x4C, 0x4C, 0x4F]; let stream = provider.decode_stream(&bytes);
assert_eq!(stream.chars.len(), 5);
assert_eq!(stream.chars[0], MockChar::Ascii('H'));
assert_eq!(stream.chars[1], MockChar::Ascii('E'));
assert_eq!(stream.chars[2], MockChar::Ascii('L'));
assert_eq!(stream.chars[3], MockChar::Ascii('L'));
assert_eq!(stream.chars[4], MockChar::Ascii('O'));
}
#[test]
fn test_decode_done_control() {
let provider = MockAsciiProvider;
assert_eq!(provider.decode_byte(0xFF), Some(MockChar::Done));
assert!(provider.is_control_code(&MockChar::Done));
}
#[test]
fn test_dialog_engine_hello() {
let provider = MockAsciiProvider;
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
engine.open_dialog(&[0x48, 0x45, 0x4C, 0x4C, 0x4F, 0xFF]);
assert!(engine.is_active());
for _ in 0..6 {
engine.update(&mut buffer);
}
let tiles = &buffer.tiles;
assert_eq!(tiles[0].tile_id, b'H' as u16);
assert_eq!(tiles[1].tile_id, b'E' as u16);
assert_eq!(tiles[2].tile_id, b'L' as u16);
assert_eq!(tiles[3].tile_id, b'L' as u16);
assert_eq!(tiles[4].tile_id, b'O' as u16);
assert_eq!(tiles[5].tile_id, 0);
}
#[test]
fn test_dialog_engine_done_deactivates() {
let provider = MockAsciiProvider;
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
engine.open_dialog(&[0x48, 0xFF]);
engine.update(&mut buffer); engine.update(&mut buffer); assert!(!engine.is_active());
}
#[test]
fn test_advance_after_done() {
let provider = MockAsciiProvider;
let mut engine = DialogEngine::new(provider);
let mut buffer = TileBuffer::new(20, 18);
engine.open_dialog(&[0x48, 0xFF]);
engine.update(&mut buffer); engine.update(&mut buffer);
assert!(!engine.is_active());
assert_eq!(engine.stream.is_some(), true);
engine.advance();
assert!(!engine.is_active());
assert!(engine.stream.is_none());
}
#[test]
fn test_tile_buffer_clear() {
let mut buffer = TileBuffer::new(20, 18);
buffer.set_tile(TilePos::new(5, 5), 0x42, 1);
assert_eq!(buffer.tiles[buffer.index(5, 5)].tile_id, 0x42);
buffer.clear();
assert_eq!(buffer.tiles[buffer.index(5, 5)].tile_id, 0);
assert_eq!(buffer.cursor, TilePos::new(0, 0));
}
#[test]
fn test_tile_buffer_newline() {
let mut buffer = TileBuffer::new(20, 18);
buffer.cursor = TilePos::new(12, 5);
buffer.newline();
assert_eq!(buffer.cursor.x, 0);
assert_eq!(buffer.cursor.y, 6);
}
#[test]
fn test_tile_buffer_scroll() {
let mut buffer = TileBuffer::new(20, 18);
buffer.set_tile(TilePos::new(0, 1), 0xAB, 0);
buffer.set_tile(TilePos::new(0, 0), 0xCD, 0);
buffer.scroll();
assert_eq!(buffer.tiles[buffer.index(0, 0)].tile_id, 0xAB);
assert_eq!(
buffer.tiles[buffer.index(0, buffer.height_tiles - 1)].tile_id,
0
);
}
#[test]
fn test_text_stream_operations() {
let mut stream = TextStream::new(vec![1, 2, 3, 4, 5]);
assert_eq!(stream.peek(), Some(&1));
assert_eq!(stream.next(), Some(&1));
assert_eq!(stream.next(), Some(&2));
assert_eq!(stream.position(), 2);
stream.skip(1); assert_eq!(stream.peek(), Some(&4));
assert_eq!(stream.remaining(), &[4, 5]);
assert_eq!(stream.next(), Some(&4));
assert_eq!(stream.next(), Some(&5));
assert!(stream.is_at_end());
assert_eq!(stream.next(), None);
}
#[test]
fn test_control_action_equality() {
assert_eq!(ControlAction::None, ControlAction::None);
assert_ne!(ControlAction::Newline, ControlAction::Done);
assert_eq!(ControlAction::SetSpeed(3), ControlAction::SetSpeed(3));
assert_ne!(ControlAction::SetSpeed(1), ControlAction::SetSpeed(2));
assert_eq!(
ControlAction::MoveCursor { x: 5, y: 3 },
ControlAction::MoveCursor { x: 5, y: 3 }
);
assert_ne!(
ControlAction::MoveCursor { x: 1, y: 1 },
ControlAction::MoveCursor { x: 2, y: 2 }
);
assert_eq!(ControlAction::Scroll, ControlAction::Scroll);
assert_eq!(ControlAction::Pause(30), ControlAction::Pause(30));
assert_ne!(ControlAction::Pause(10), ControlAction::Pause(30));
}
#[test]
fn test_dialog_state_default() {
let state = DialogState::default();
assert_eq!(state.mode, DialogMode::Typing);
assert_eq!(state.page_index, 0);
assert_eq!(state.char_index, 0);
assert_eq!(state.scroll_offset, 0);
}
#[test]
fn test_string_width() {
let provider = MockAsciiProvider;
let chars = vec![
MockChar::Ascii('H'),
MockChar::Ascii('I'),
];
assert_eq!(provider.string_width(&chars), 16);
assert_eq!(provider.string_width(&[]), 0);
}
#[test]
fn test_decode_digit() {
let provider = MockAsciiProvider;
assert_eq!(provider.decode_byte(0x00), Some(MockChar::Digit(0)));
assert_eq!(provider.decode_byte(0x05), Some(MockChar::Digit(5)));
assert_eq!(provider.decode_byte(0x09), Some(MockChar::Digit(9)));
}
}