From 995b946dcb20792fc206cb0c3057b5689ad4fcbc Mon Sep 17 00:00:00 2001 From: p11 Date: Fri, 18 Apr 2025 21:11:38 +0200 Subject: [PATCH] Put vn rendering into a new vn module --- src/main.rs | 336 +--------------------------------------------------- src/vn.rs | 333 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+), 331 deletions(-) create mode 100644 src/vn.rs diff --git a/src/main.rs b/src/main.rs index bfe908d..c68a15e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,23 +18,18 @@ use data_stream::{ FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings, from_stream, to_stream, }; -use dialog::parse_map; -use dialogi::DialogParameter; use header_config::parse_config; -use indexmap::IndexMap; use maud::html; use percent_encoding::percent_decode_str; use pukram2html::{Settings, convert, convert_extended, convert_subheader}; use threadpool::ThreadPool; -mod request; -use request::Request; -use vn_settings::{ - ColorSettings, ImageSettings, LayoutSettings, Names, ObjectSettings, Parameter, PlayerSettings, - SettingsContext, TimingSettings, -}; - mod dialog; +mod request; +mod vn; + +use request::Request; +use vn::render_novel; fn main() -> ExitCode { let Ok(path) = env::current_dir() else { @@ -605,327 +600,6 @@ fn handle_relative_connection( .map(|parent| parse_config(&parent.with_extension("vng")).ok()) .unwrap_or_default(); - fn render_scene(settings: &PlayerSettings, name: &str, output: &mut Vec) { - for object in &settings.objects.objects { - let Some(image_set) = object.image.get(name) else { - continue; - }; - - for image in &settings.images.images[image_set] { - let Some(image) = image.get_ref(name) else { - continue; - }; - - let _ = writeln!( - output, - "" - ); - } - } - } - - fn render_novel( - mut config_map: IndexMap, Box>, - pk_path: &Path, - stream: &mut TcpStream, - start_level: usize, - ) -> std::result::Result<(), dialogi::ParsingError> { - let mut player_settings = PlayerSettings { - colors: ColorSettings::common(), - timing: TimingSettings::common(), - images: ImageSettings::common(), - objects: ObjectSettings::common(), - layout: LayoutSettings::common(), - names: Names::new(), - }; - - let mut layers = HashMap::new(); - - let keys: Vec<_> = config_map.keys().cloned().collect(); - for key in keys { - let Some(("Layer", name)) = key.split_once(':') else { - continue; - }; - - let value = config_map.shift_remove(&key).expect("Invalid layer"); - if !value.is_empty() { - eprintln!("Layers don't accept arguments!"); - } - layers.insert(name.into(), layers.len()); - } - - if layers.is_empty() { - layers.insert("Background".into(), 0); - layers.insert("Character".into(), 1); - } - - let mut settings_context = SettingsContext { - object_cache: HashMap::new(), - layers, - }; - - for (key, value) in config_map { - if let Some(parameter) = Parameter::create(&key, &mut settings_context) { - let setter = parameter.value_setter(&value, &settings_context); - player_settings.set_character_default(setter); - } - } - - let dialogs = parse_map(pk_path, &mut settings_context)?; - - let mut scenes = Vec::new(); - let mut sections = Vec::new(); - - for dialog_sequence in dialogs { - let changes = &dialog_sequence.changes; - let mut states = HashMap::new(); - for parameter in changes.keys() { - states.insert(parameter.clone(), 0); - } - - for block in &dialog_sequence.blocks { - let mut block_content = Vec::new(); - - for parameter in block - .lines - .iter() - .flat_map(|l| &l.actions) - .chain(&block.final_actions) - { - if let Some(state) = states.get_mut(parameter) { - let change = &dialog_sequence.changes[parameter][*state]; - player_settings.change(change); - *state += 1; - } - } - - let mut scene_data = Vec::new(); - render_scene(&player_settings, &block.name, &mut scene_data); - scenes.push(unsafe { String::from_utf8_unchecked(scene_data) }); - - let section_html = if block.lines.is_empty() { - html! {} - } else { - convert_subheader( - block.lines.iter().map(|l| l.text.as_ref()), - &mut block_content, - start_level, - ); - - html! { - fieldset class="visual-novel-box" { - @if !block.name.is_empty() { - legend class="character-name" { (block.name) } - } - div class="dialog-content" { - @if let Ok(content) = String::from_utf8(block_content) { - (maud::PreEscaped(content)) - } - } - } - } - }; - - sections.push(section_html); - } - - player_settings.reset(); - } - - let html = html! { - div id="story-container" { - div class="textbox-container" { - @for (index, scene) in scenes.iter().enumerate() { - div class="scene-section" data-section-index=(index) - style=(format!("display: {};", if index == 0 { "block" } else { "none" })) { - (maud::PreEscaped(scene)) - } - } - - div class="textbox-content" { - @for (index, section) in sections.iter().enumerate() { - div class="story-section" data-section-index=(index) - style=(format!("display: {};", if index == 0 { "block" } else { "none" })) { - (section) - } - } - } - } - - div class="nav-controls" { - button class="nav-button" onclick="prev()" { "←" } - span id="section-counter" { "1/" (sections.len()) } - button class="nav-button" onclick="next()" { "→" } - } - - style { - (maud::PreEscaped(r" - .nav-controls { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin: 2rem 0; - padding: 1rem 0; - } - - .nav-button { - width: 2.5rem; - height: 2.5rem; - border-radius: 50%; - border: none; - background: #f0f0f0; - cursor: pointer; - font-size: 1.2rem; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - margin: 0; - padding: 0; - } - - #section-counter { - font-family: system-ui, sans-serif; - font-size: 0.9rem; - color: #666; - margin: 0 1rem; - padding: 0; - line-height: 1; - } - - .nav-button:hover { - background: #e0e0e0; - transform: scale(1.1); - } - - .nav-button:active { - transform: scale(0.95); - } - - .textbox-container { - width: 75%; - margin: 0 auto; - position: relative; - padding-top: 42.1875%; /* 16:9 Aspect Ratio */ - background: #f8f8f8; - border-radius: 8px; - overflow: hidden; - } - - .textbox-content { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - flex-direction: column; - padding: 1rem; - justify-content: flex-end; - } - - .visual-novel-box { - border: 0.2vw solid #3a3a3a; - border-radius: 1vw; - padding: 2vw; - margin: 1vh 0; - background: rgba(255, 255, 255, 0.9); - box-shadow: 0 0.4vw 0.6vw rgba(0, 0, 0, 0.1); - width: 90%; - min-width: 85%; - font-size: 1.6vw; - position: absolute; - bottom: 3vh; - left: 50%; - transform: translateX(-50%); - } - - .character-name { - font-family: 'Georgia', serif; - font-size: 1.8vw; - font-weight: bold; - padding: 0 1.5vw; - background: #4a6b8a; - color: white; - border-radius: 0.5vw; - margin: 0; - transform: translateY(-60%); - position: absolute; - top: 0; - left: 1vw; - } - - .dialogue-content { - font-size: 1.6vw; - line-height: 1.4; - max-height: 20vh; - overflow-y: auto; - padding: 1vh 0.5vw; - } - - .visual-novel-box::after { - content: ''; - position: absolute; - bottom: 10px; - right: 10px; - width: 20px; - height: 20px; - opacity: 0.5; - } - ")) - } - - script { - (maud::PreEscaped(r" - let currentSection = 0; - const totalSections = ".to_owned() + §ions.len().to_string() + r"; - - function updateSection() { - document.querySelectorAll('.story-section').forEach((el, index) => { - el.style.display = index === currentSection ? 'block' : 'none'; - }); - document.querySelectorAll('.scene-section').forEach((el, index) => { - el.style.display = index === currentSection ? 'block' : 'none'; - }); - document.getElementById('section-counter').textContent = - `${currentSection + 1}/${totalSections}`; - } - - function prev() { - if (currentSection > 0) { - --currentSection; - updateSection(); - } - } - - function next() { - if (currentSection < totalSections - 1) { - ++currentSection; - updateSection(); - } - } - - document.addEventListener('keydown', (e) => { - switch(e.keyCode) { - case 37: - prev(); - break; - case 39: - next(); - break; - } - }); - ")) - } - } - }; - - let _ = write!(stream, "{}", html.into_string()); - - Ok(()) - } - if let Some(config_map) = config_map { if render_novel(config_map, file_paths.pk, &mut stream, start_level).is_err() { fail(stream); diff --git a/src/vn.rs b/src/vn.rs new file mode 100644 index 0000000..0d4d065 --- /dev/null +++ b/src/vn.rs @@ -0,0 +1,333 @@ +use std::{collections::HashMap, io::prelude::*, net::TcpStream, path::Path}; + +use dialogi::DialogParameter; +use indexmap::IndexMap; +use maud::html; +use pukram2html::convert_subheader; +use vn_settings::{ + ColorSettings, ImageSettings, LayoutSettings, Names, ObjectSettings, Parameter, PlayerSettings, + SettingsContext, TimingSettings, +}; + +use crate::dialog::parse_map; + +fn render_scene(settings: &PlayerSettings, name: &str, output: &mut Vec) { + for object in &settings.objects.objects { + let Some(image_set) = object.image.get(name) else { + continue; + }; + + for image in &settings.images.images[image_set] { + let Some(image) = image.get_ref(name) else { + continue; + }; + + let _ = writeln!( + output, + "" + ); + } + } +} + +pub fn render_novel( + mut config_map: IndexMap, Box>, + pk_path: &Path, + stream: &mut TcpStream, + start_level: usize, +) -> std::result::Result<(), dialogi::ParsingError> { + let mut player_settings = PlayerSettings { + colors: ColorSettings::common(), + timing: TimingSettings::common(), + images: ImageSettings::common(), + objects: ObjectSettings::common(), + layout: LayoutSettings::common(), + names: Names::new(), + }; + + let mut layers = HashMap::new(); + + let keys: Vec<_> = config_map.keys().cloned().collect(); + for key in keys { + let Some(("Layer", name)) = key.split_once(':') else { + continue; + }; + + let value = config_map.shift_remove(&key).expect("Invalid layer"); + if !value.is_empty() { + eprintln!("Layers don't accept arguments!"); + } + layers.insert(name.into(), layers.len()); + } + + if layers.is_empty() { + layers.insert("Background".into(), 0); + layers.insert("Character".into(), 1); + } + + let mut settings_context = SettingsContext { + object_cache: HashMap::new(), + layers, + }; + + for (key, value) in config_map { + if let Some(parameter) = Parameter::create(&key, &mut settings_context) { + let setter = parameter.value_setter(&value, &settings_context); + player_settings.set_character_default(setter); + } + } + + let dialogs = parse_map(pk_path, &mut settings_context)?; + + let mut scenes = Vec::new(); + let mut sections = Vec::new(); + + for dialog_sequence in dialogs { + let changes = &dialog_sequence.changes; + let mut states = HashMap::new(); + for parameter in changes.keys() { + states.insert(parameter.clone(), 0); + } + + for block in &dialog_sequence.blocks { + let mut block_content = Vec::new(); + + for parameter in block + .lines + .iter() + .flat_map(|l| &l.actions) + .chain(&block.final_actions) + { + if let Some(state) = states.get_mut(parameter) { + let change = &dialog_sequence.changes[parameter][*state]; + player_settings.change(change); + *state += 1; + } + } + + let mut scene_data = Vec::new(); + render_scene(&player_settings, &block.name, &mut scene_data); + scenes.push(unsafe { String::from_utf8_unchecked(scene_data) }); + + let section_html = if block.lines.is_empty() { + html! {} + } else { + convert_subheader( + block.lines.iter().map(|l| l.text.as_ref()), + &mut block_content, + start_level, + ); + + html! { + fieldset class="visual-novel-box" { + @if !block.name.is_empty() { + legend class="character-name" { (block.name) } + } + div class="dialog-content" { + @if let Ok(content) = String::from_utf8(block_content) { + (maud::PreEscaped(content)) + } + } + } + } + }; + + sections.push(section_html); + } + + player_settings.reset(); + } + + let html = html! { + div id="story-container" { + div class="textbox-container" { + @for (index, scene) in scenes.iter().enumerate() { + div class="scene-section" data-section-index=(index) + style=(format!("display: {};", if index == 0 { "block" } else { "none" })) { + (maud::PreEscaped(scene)) + } + } + + div class="textbox-content" { + @for (index, section) in sections.iter().enumerate() { + div class="story-section" data-section-index=(index) + style=(format!("display: {};", if index == 0 { "block" } else { "none" })) { + (section) + } + } + } + } + + div class="nav-controls" { + button class="nav-button" onclick="prev()" { "←" } + span id="section-counter" { "1/" (sections.len()) } + button class="nav-button" onclick="next()" { "→" } + } + + style { + (maud::PreEscaped(r" + .nav-controls { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + margin: 2rem 0; + padding: 1rem 0; + } + + .nav-button { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + border: none; + background: #f0f0f0; + cursor: pointer; + font-size: 1.2rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + margin: 0; + padding: 0; + } + + #section-counter { + font-family: system-ui, sans-serif; + font-size: 0.9rem; + color: #666; + margin: 0 1rem; + padding: 0; + line-height: 1; + } + + .nav-button:hover { + background: #e0e0e0; + transform: scale(1.1); + } + + .nav-button:active { + transform: scale(0.95); + } + + .textbox-container { + width: 75%; + margin: 0 auto; + position: relative; + padding-top: 42.1875%; /* 16:9 Aspect Ratio */ + background: #f8f8f8; + border-radius: 8px; + overflow: hidden; + } + + .textbox-content { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + padding: 1rem; + justify-content: flex-end; + } + + .visual-novel-box { + border: 0.2vw solid #3a3a3a; + border-radius: 1vw; + padding: 2vw; + margin: 1vh 0; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 0.4vw 0.6vw rgba(0, 0, 0, 0.1); + width: 90%; + min-width: 85%; + font-size: 1.6vw; + position: absolute; + bottom: 3vh; + left: 50%; + transform: translateX(-50%); + } + + .character-name { + font-family: 'Georgia', serif; + font-size: 1.8vw; + font-weight: bold; + padding: 0 1.5vw; + background: #4a6b8a; + color: white; + border-radius: 0.5vw; + margin: 0; + transform: translateY(-60%); + position: absolute; + top: 0; + left: 1vw; + } + + .dialogue-content { + font-size: 1.6vw; + line-height: 1.4; + max-height: 20vh; + overflow-y: auto; + padding: 1vh 0.5vw; + } + + .visual-novel-box::after { + content: ''; + position: absolute; + bottom: 10px; + right: 10px; + width: 20px; + height: 20px; + opacity: 0.5; + } + ")) + } + + script { + (maud::PreEscaped(r" + let currentSection = 0; + const totalSections = ".to_owned() + §ions.len().to_string() + r"; + + function updateSection() { + document.querySelectorAll('.story-section').forEach((el, index) => { + el.style.display = index === currentSection ? 'block' : 'none'; + }); + document.querySelectorAll('.scene-section').forEach((el, index) => { + el.style.display = index === currentSection ? 'block' : 'none'; + }); + document.getElementById('section-counter').textContent = + `${currentSection + 1}/${totalSections}`; + } + + function prev() { + if (currentSection > 0) { + --currentSection; + updateSection(); + } + } + + function next() { + if (currentSection < totalSections - 1) { + ++currentSection; + updateSection(); + } + } + + document.addEventListener('keydown', (e) => { + switch(e.keyCode) { + case 37: + prev(); + break; + case 39: + next(); + break; + } + }); + ")) + } + } + }; + + let _ = write!(stream, "{}", html.into_string()); + + Ok(()) +}