Post-Effekte über Action::SetPost statt Engine-Zustand
Die PostChain lag als engine::post im Kern, nur damit die Konsole
`post` durch den exec-Trichter schalten konnte — Zustand, der allein
das Render-Frontend angeht und nichts mit der Simulation zu tun hat.
Jetzt kennt der Kern Post-Effekte nur als deferred Action (wie
play_sound): das `post <name> on|off`-Verb queued Action::SetPost,
die Session reicht sie als take_post()-Queue durch, das Fenster wendet
sie auf seine eigene PostChain an. Damit schalten Trigger und Ink-Tags
Effekte, die vorher nur der Konsolenbefehl erreichte.
- engine/post.rs entfällt; PostChain wandert nach render::post
- Session verliert das post-Feld, bekommt pending_post + take_post
- der `post`-Konsolenbefehl ist ein Shim wie `mode`; Ketten-Listing
und nackter Toggle fallen weg (letzterer bewusst, vgl. 8bee832)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196AM4UyAJEXVtPvp5DdKr8
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b3d08d4a8c
commit
247f7ef9fe
@@ -29,6 +29,10 @@ pub enum Action {
|
|||||||
/// Anzeige-/Eingabemodus wechseln (Spiel, Noclip, Menü). Der Dialog-Modus
|
/// Anzeige-/Eingabemodus wechseln (Spiel, Noclip, Menü). Der Dialog-Modus
|
||||||
/// ist bewusst nicht wählbar — er entsteht nur aus dem Story-Ablauf.
|
/// ist bewusst nicht wählbar — er entsteht nur aus dem Story-Ablauf.
|
||||||
SetMode(ModeTarget),
|
SetMode(ModeTarget),
|
||||||
|
/// Renderer-Post-Effekt schalten. Der Kern kennt weder die Kette noch die
|
||||||
|
/// Effektnamen — das Fenster löst `name` gegen seine Kette auf (unbekannt
|
||||||
|
/// → Meldung), die CLI reicht nur die `[action]`-Zeile.
|
||||||
|
SetPost { name: String, on: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frontend-neutrales Ziel eines Moduswechsels: die nicht-Dialog-Varianten von
|
/// Frontend-neutrales Ziel eines Moduswechsels: die nicht-Dialog-Varianten von
|
||||||
|
|||||||
+3
-4
@@ -16,9 +16,9 @@
|
|||||||
//!
|
//!
|
||||||
//! `gltf`, `tga` und `wav` sind reine Decoder (Bytes → owned Daten); `gltf`
|
//! `gltf`, `tga` und `wav` sind reine Decoder (Bytes → owned Daten); `gltf`
|
||||||
//! ist DER 3D-Pfad und produziert das neutrale `model::Model`. `audio` ist
|
//! ist DER 3D-Pfad und produziert das neutrale `model::Model`. `audio` ist
|
||||||
//! die Emitter-Logik zum Frontend-Treiber render::audio, `post` analog die
|
//! die Emitter-Logik zum Frontend-Treiber render::audio, `player` die
|
||||||
//! Effekt-Schalter zu render::post, `player` die First-Person-Physik auf der
|
//! First-Person-Physik auf der Welt aus `collision`. Post-Effekte kennt der
|
||||||
//! Welt aus `collision`.
|
//! Kern nur als `game::Action::SetPost` — die Kette lebt in render::post.
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
@@ -29,7 +29,6 @@ pub mod ink;
|
|||||||
pub mod kv;
|
pub mod kv;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod post;
|
|
||||||
pub mod signals;
|
pub mod signals;
|
||||||
pub mod story_ctrl;
|
pub mod story_ctrl;
|
||||||
pub mod tga;
|
pub mod tga;
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
//! Post-Effekt-Schalter: welches Kettenglied des Renderers ist an?
|
|
||||||
//!
|
|
||||||
//! Headless-Gegenstück zu `render::post`, so wie `audio` zum Audio-Treiber.
|
|
||||||
//! Hier liegen nur die *Namen* und ihr An/Aus-Zustand — damit die Konsole sie
|
|
||||||
//! durch denselben `exec`-Trichter schalten kann wie alles andere, ohne dass
|
|
||||||
//! der Kern eine GPU kennt. Was ein Effekt tut und in welcher Reihenfolge er
|
|
||||||
//! läuft, weiß allein das Render-Frontend; es meldet seine Effekte beim Start
|
|
||||||
//! per [`PostChain::register`] an. Ohne Fenster-Frontend bleibt die Liste leer.
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PostChain {
|
|
||||||
/// In Ketten-Reihenfolge, wie vom Frontend angemeldet.
|
|
||||||
effects: Vec<(String, bool)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PostChain {
|
|
||||||
pub fn new() -> Self { Self::default() }
|
|
||||||
|
|
||||||
/// Einen Effekt anmelden (Frontend beim Start, in Ketten-Reihenfolge).
|
|
||||||
pub fn register(&mut self, name: &str, on: bool) {
|
|
||||||
self.effects.push((name.to_string(), on));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled(&self, name: &str) -> bool {
|
|
||||||
self.effects.iter().any(|(n, on)| n == name && *on)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schalten; `on = None` togglet. Gibt den neuen Zustand zurück, `None`
|
|
||||||
/// bei unbekanntem Namen.
|
|
||||||
pub fn set(&mut self, name: &str, on: Option<bool>) -> Option<bool> {
|
|
||||||
let e = self.effects.iter_mut().find(|(n, _)| n == name)?;
|
|
||||||
e.1 = on.unwrap_or(!e.1);
|
|
||||||
Some(e.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alle Effekte in Ketten-Reihenfolge.
|
|
||||||
pub fn list(&self) -> impl Iterator<Item = (&str, bool)> {
|
|
||||||
self.effects.iter().map(|(n, on)| (n.as_str(), *on))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn chain() -> PostChain {
|
|
||||||
let mut c = PostChain::new();
|
|
||||||
c.register("cel", false);
|
|
||||||
c.register("dither", true);
|
|
||||||
c
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keeps_registration_order() {
|
|
||||||
let c = chain();
|
|
||||||
let names: Vec<&str> = c.list().map(|(n, _)| n).collect();
|
|
||||||
assert_eq!(names, vec!["cel", "dither"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn set_toggles_and_reports_unknown() {
|
|
||||||
let mut c = chain();
|
|
||||||
assert_eq!(c.set("cel", Some(true)), Some(true));
|
|
||||||
assert!(c.enabled("cel"));
|
|
||||||
assert_eq!(c.set("cel", None), Some(false), "ohne Argument togglen");
|
|
||||||
assert!(!c.enabled("cel"));
|
|
||||||
assert_eq!(c.set("bloom", Some(true)), None, "unbekannt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unregistered_effect_is_off() {
|
|
||||||
assert!(!PostChain::new().enabled("dither"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
//! `assets/sounds/{name}.wav`
|
//! `assets/sounds/{name}.wav`
|
||||||
//! `mode <play|free|menu>` → Action::SetMode (deferred): Anzeige-/
|
//! `mode <play|free|menu>` → Action::SetMode (deferred): Anzeige-/
|
||||||
//! Eingabemodus wechseln
|
//! Eingabemodus wechseln
|
||||||
|
//! `post <name> <on|off>` → Action::SetPost (deferred): Renderer-
|
||||||
|
//! Post-Effekt schalten (Name = Kettenglied
|
||||||
|
//! im Fenster-Frontend)
|
||||||
//!
|
//!
|
||||||
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
||||||
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
||||||
@@ -101,11 +104,29 @@ fn execute(cmd: &str, ctx: &mut ActionCtx) -> bool {
|
|||||||
"mode" => if let Some(t) = ModeTarget::parse(args) {
|
"mode" => if let Some(t) = ModeTarget::parse(args) {
|
||||||
ctx.actions.push(Action::SetMode(t));
|
ctx.actions.push(Action::SetMode(t));
|
||||||
},
|
},
|
||||||
|
"post" => if let Some((name, on)) = parse_post_arg(args) {
|
||||||
|
ctx.actions.push(Action::SetPost { name, on });
|
||||||
|
},
|
||||||
_ => return false, // Unbekanntes Verb — der Aufrufer meldet ggf.
|
_ => return false, // Unbekanntes Verb — der Aufrufer meldet ggf.
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `"<name> on"` / `"<name> off"` → `(name, bool)`; sonst `None`. Kein
|
||||||
|
/// nackter Toggle — eine geskriptete Schaltung soll lesbar sagen, wohin sie
|
||||||
|
/// schaltet. Geteilt von der Dispatch-Pipeline (`post`-Verb) und dem
|
||||||
|
/// `post`-Konsolenbefehl in `session`.
|
||||||
|
pub fn parse_post_arg(args: &str) -> Option<(String, bool)> {
|
||||||
|
let (name, state) = args.trim().split_once(char::is_whitespace)?;
|
||||||
|
let on = match state.trim() {
|
||||||
|
"on" => true,
|
||||||
|
"off" => false,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let name = name.trim();
|
||||||
|
(!name.is_empty()).then(|| (name.to_string(), on))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parst das signals.toml-Subset: `[signal]`-Header gefolgt von einem
|
/// Parst das signals.toml-Subset: `[signal]`-Header gefolgt von einem
|
||||||
/// `actions = [ "…", … ]`-Array. Bewusst handgerollt statt toml-Crate —
|
/// `actions = [ "…", … ]`-Array. Bewusst handgerollt statt toml-Crate —
|
||||||
/// das Format ist trivial und die Engine bleibt dependency-arm.
|
/// das Format ist trivial und die Engine bleibt dependency-arm.
|
||||||
@@ -208,6 +229,23 @@ mod tests {
|
|||||||
assert!(game.actions.is_empty());
|
assert!(game.actions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn post_verb_queues_setpost_action() {
|
||||||
|
let mut game = Game::new(Signals::new());
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
dispatch("post outline off", &mut ctx);
|
||||||
|
assert_eq!(game.actions, vec![
|
||||||
|
Action::SetPost { name: "outline".into(), on: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Fehlendes/ungültiges on|off → kein Action (stilles No-Op wie `mode`).
|
||||||
|
game.actions.clear();
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
dispatch("post outline", &mut ctx);
|
||||||
|
dispatch("post outline wat", &mut ctx);
|
||||||
|
assert!(game.actions.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn typo_verb_in_table_is_tolerated_and_reported() {
|
fn typo_verb_in_table_is_tolerated_and_reported() {
|
||||||
// Ein Tippfehler-Verb in einer Table-Action wird gemeldet (stderr) und
|
// Ein Tippfehler-Verb in einer Table-Action wird gemeldet (stderr) und
|
||||||
|
|||||||
+1
-2
@@ -16,10 +16,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
use crate::engine::post::PostChain;
|
|
||||||
use crate::engine::tga::Image;
|
use crate::engine::tga::Image;
|
||||||
use crate::render::math::Mat4;
|
use crate::render::math::Mat4;
|
||||||
use crate::render::post::Post;
|
use crate::render::post::{Post, PostChain};
|
||||||
use crate::render::scene::{Mesh, ScenePass};
|
use crate::render::scene::{Mesh, ScenePass};
|
||||||
use crate::render::sprite::SpritePass;
|
use crate::render::sprite::SpritePass;
|
||||||
use crate::render::ui::Ui;
|
use crate::render::ui::Ui;
|
||||||
|
|||||||
+17
-5
@@ -45,10 +45,6 @@ const EYE_SMOOTH_TAU: f32 = 0.06;
|
|||||||
const EYE_MAX_LAG: f32 = 0.6;
|
const EYE_MAX_LAG: f32 = 0.6;
|
||||||
|
|
||||||
pub fn run(mut session: Session) {
|
pub fn run(mut session: Session) {
|
||||||
// Post-Effekte anmelden: die Kette gehört dem Renderer, geschaltet wird
|
|
||||||
// sie über denselben exec-Trichter wie alles andere (`post`).
|
|
||||||
for (name, on) in post::effects() { session.post.register(name, on); }
|
|
||||||
|
|
||||||
// Init-Signal feuern, bevor das Fenster steht (kann bereits einen
|
// Init-Signal feuern, bevor das Fenster steht (kann bereits einen
|
||||||
// Dialog öffnen — dann startet die Welt eben pausiert).
|
// Dialog öffnen — dann startet die Welt eben pausiert).
|
||||||
for line in session.start() { println!("{line}"); }
|
for line in session.start() { println!("{line}"); }
|
||||||
@@ -242,6 +238,9 @@ struct App {
|
|||||||
/// melden, nicht jeden Klick).
|
/// melden, nicht jeden Klick).
|
||||||
audio: Option<audio::Audio>,
|
audio: Option<audio::Audio>,
|
||||||
sfx_cache: HashMap<String, Option<audio::Sound>>,
|
sfx_cache: HashMap<String, Option<audio::Sound>>,
|
||||||
|
/// Post-Effekt-Kette (Renderer-Zustand, nicht im Kern). Pro Frame aus
|
||||||
|
/// `session.take_post()` nachgeführt, dann an `gpu.frame`.
|
||||||
|
post: post::PostChain,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -271,6 +270,10 @@ impl App {
|
|||||||
fonts,
|
fonts,
|
||||||
audio,
|
audio,
|
||||||
sfx_cache: HashMap::new(),
|
sfx_cache: HashMap::new(),
|
||||||
|
// Die Post-Kette gehört dem Renderer, nicht dem Kern: geschaltet
|
||||||
|
// wird sie über `Action::SetPost` (Trigger/Ink/Konsole), die die
|
||||||
|
// Session als `take_post`-Queue durchreicht.
|
||||||
|
post: post::PostChain::defaults(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +440,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-Effekt-Schaltungen (SetPost-Actions von Trigger/Ink/Konsole)
|
||||||
|
// auf die eigene Kette anwenden; unbekannter Name wird gemeldet, wie
|
||||||
|
// ein fehlendes WAV bei play_sound.
|
||||||
|
for (name, on) in self.session.take_post() {
|
||||||
|
if !self.post.set(&name, on) {
|
||||||
|
eprintln!("[post] unbekannter Effekt {name:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Am Boden gleitet die Augenhöhe sanft nach (siehe EYE_SMOOTH_TAU); in
|
// Am Boden gleitet die Augenhöhe sanft nach (siehe EYE_SMOOTH_TAU); in
|
||||||
// der Luft und außerhalb des Spiels folgt sie exakt.
|
// der Luft und außerhalb des Spiels folgt sie exakt.
|
||||||
let target_eye = self.session.player.pos[1] + player::EYE_HEIGHT;
|
let target_eye = self.session.player.pos[1] + player::EYE_HEIGHT;
|
||||||
@@ -494,7 +506,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(gpu) = &mut self.gpu {
|
if let Some(gpu) = &mut self.gpu {
|
||||||
gpu.frame(&view, &screen.ui, &self.session.post);
|
gpu.frame(&view, &screen.ui, &self.post);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-7
@@ -13,15 +13,14 @@
|
|||||||
//! Effekt über den Frame wissen kann; beide darf ein Effekt ignorieren. Kein
|
//! Effekt über den Frame wissen kann; beide darf ein Effekt ignorieren. Kein
|
||||||
//! Effekt kennt einen anderen, keiner darf einen anderen voraussetzen. Ein
|
//! Effekt kennt einen anderen, keiner darf einen anderen voraussetzen. Ein
|
||||||
//! Effekt wird hinzugefügt, indem er in [`EFFECTS`] auftaucht; geschaltet
|
//! Effekt wird hinzugefügt, indem er in [`EFFECTS`] auftaucht; geschaltet
|
||||||
//! wird er über `engine::post` (Konsole: `post`).
|
//! wird er über [`PostChain`], die Trigger, Ink-Tags und die Konsole per
|
||||||
|
//! `game::Action::SetPost` erreichen. Der Kern kennt die Kette nicht.
|
||||||
//!
|
//!
|
||||||
//! Gerechnet wird auf zwei Ping-Pong-Targets in interner Auflösung. Bei
|
//! Gerechnet wird auf zwei Ping-Pong-Targets in interner Auflösung. Bei
|
||||||
//! 640×480 kostet ein zusätzlicher Fullscreen-Pass praktisch nichts —
|
//! 640×480 kostet ein zusätzlicher Fullscreen-Pass praktisch nichts —
|
||||||
//! deshalb darf jeder Effekt seinen eigenen behalten, statt sie zu einem
|
//! deshalb darf jeder Effekt seinen eigenen behalten, statt sie zu einem
|
||||||
//! Über-Shader zu verschmelzen.
|
//! Über-Shader zu verschmelzen.
|
||||||
|
|
||||||
use crate::engine::post::PostChain;
|
|
||||||
|
|
||||||
/// Die Kette in Ausführungsreihenfolge, mit ihrem Startzustand. `dither`
|
/// Die Kette in Ausführungsreihenfolge, mit ihrem Startzustand. `dither`
|
||||||
/// quantisiert die Farbtiefe und muss deshalb zuletzt laufen.
|
/// quantisiert die Farbtiefe und muss deshalb zuletzt laufen.
|
||||||
const EFFECTS: &[(&str, bool, &str)] = &[
|
const EFFECTS: &[(&str, bool, &str)] = &[
|
||||||
@@ -30,10 +29,34 @@ const EFFECTS: &[(&str, bool, &str)] = &[
|
|||||||
("dither", true, include_str!("dither.wgsl")),
|
("dither", true, include_str!("dither.wgsl")),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Effektnamen samt Startzustand — das Frontend meldet sie beim Start in der
|
/// An/Aus je Post-Effekt, in Ketten-Reihenfolge. Das Fenster hält sie als
|
||||||
/// Session an.
|
/// eigenen Zustand (nicht der Kern), wendet die per `Session::take_post`
|
||||||
pub fn effects() -> impl Iterator<Item = (&'static str, bool)> {
|
/// aufgelaufenen `SetPost`-Schaltungen jeden Frame an und gibt die Kette an
|
||||||
EFFECTS.iter().map(|(n, on, _)| (*n, *on))
|
/// [`Post::run`].
|
||||||
|
pub struct PostChain {
|
||||||
|
effects: Vec<(String, bool)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostChain {
|
||||||
|
/// Die Kette aus [`EFFECTS`] mit ihren Startzuständen.
|
||||||
|
pub fn defaults() -> Self {
|
||||||
|
Self {
|
||||||
|
effects: EFFECTS.iter().map(|(n, on, _)| ((*n).to_string(), *on)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled(&self, name: &str) -> bool {
|
||||||
|
self.effects.iter().any(|(n, on)| n == name && *on)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effekt `name` auf `on` setzen. Rückgabe: `false`, wenn kein Effekt so
|
||||||
|
/// heißt — der Aufrufer meldet das (wie ein fehlendes WAV bei `play_sound`).
|
||||||
|
pub fn set(&mut self, name: &str, on: bool) -> bool {
|
||||||
|
match self.effects.iter_mut().find(|(n, _)| n == name) {
|
||||||
|
Some(e) => { e.1 = on; true }
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spiegelt `Uniforms` in den Effekt-Shadern. `near`/`far` müssen zur
|
/// Spiegelt `Uniforms` in den Effekt-Shadern. `near`/`far` müssen zur
|
||||||
@@ -304,3 +327,32 @@ fn make_bind(
|
|||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_follow_effects_table() {
|
||||||
|
let c = PostChain::defaults();
|
||||||
|
// Startzustände aus EFFECTS.
|
||||||
|
assert!(!c.enabled("normals"));
|
||||||
|
assert!(c.enabled("outline"));
|
||||||
|
assert!(c.enabled("dither"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_switches_and_reports_unknown() {
|
||||||
|
let mut c = PostChain::defaults();
|
||||||
|
assert!(c.set("outline", false));
|
||||||
|
assert!(!c.enabled("outline"));
|
||||||
|
assert!(c.set("outline", true));
|
||||||
|
assert!(c.enabled("outline"));
|
||||||
|
assert!(!c.set("bloom", true), "unbekannter Effekt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_effect_is_off() {
|
||||||
|
assert!(!PostChain::defaults().enabled("bloom"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+48
-50
@@ -3,7 +3,6 @@ use crate::engine::collision::CollisionWorld;
|
|||||||
use crate::engine::game::{Action, Game, ModeTarget};
|
use crate::engine::game::{Action, Game, ModeTarget};
|
||||||
use crate::engine::ink::StoryState;
|
use crate::engine::ink::StoryState;
|
||||||
use crate::engine::player::{self, Player};
|
use crate::engine::player::{self, Player};
|
||||||
use crate::engine::post::PostChain;
|
|
||||||
use crate::engine::trigger::Triggers;
|
use crate::engine::trigger::Triggers;
|
||||||
use crate::engine::{kv, model, signals, story_ctrl};
|
use crate::engine::{kv, model, signals, story_ctrl};
|
||||||
|
|
||||||
@@ -44,13 +43,14 @@ pub struct Session {
|
|||||||
/// Ambient-Emitter aus den Blender-Modellen (`sound`-Property auf
|
/// Ambient-Emitter aus den Blender-Modellen (`sound`-Property auf
|
||||||
/// Empties, siehe engine::audio).
|
/// Empties, siehe engine::audio).
|
||||||
emitters: Emitters,
|
emitters: Emitters,
|
||||||
/// An/Aus der Render-Post-Effekte. Das Fenster-Frontend meldet seine Kette
|
|
||||||
/// beim Start an und liest sie jeden Frame; die CLI lässt sie leer.
|
|
||||||
pub post: PostChain,
|
|
||||||
/// Von `Action::PlaySound` aufgelaufene Einmal-SFX — das Fenster-Frontend
|
/// Von `Action::PlaySound` aufgelaufene Einmal-SFX — das Fenster-Frontend
|
||||||
/// zieht sie per [`Session::take_sounds`]; der CLI reicht die
|
/// zieht sie per [`Session::take_sounds`]; der CLI reicht die
|
||||||
/// `[action]`-Textzeile.
|
/// `[action]`-Textzeile.
|
||||||
pending_sounds: Vec<String>,
|
pending_sounds: Vec<String>,
|
||||||
|
/// Von `Action::SetPost` aufgelaufene Post-Effekt-Schaltungen (Name,
|
||||||
|
/// Zielzustand) — das Fenster zieht sie per [`Session::take_post`] und
|
||||||
|
/// wendet sie auf seine Kette an; der CLI reicht die `[action]`-Textzeile.
|
||||||
|
pending_post: Vec<(String, bool)>,
|
||||||
signals_path: String,
|
signals_path: String,
|
||||||
/// Aufgelaufene, noch nicht simulierte Zeit (s) für den Fixed-Timestep.
|
/// Aufgelaufene, noch nicht simulierte Zeit (s) für den Fixed-Timestep.
|
||||||
sim_accum: f32,
|
sim_accum: f32,
|
||||||
@@ -97,8 +97,8 @@ impl Session {
|
|||||||
collision: CollisionWorld::empty(),
|
collision: CollisionWorld::empty(),
|
||||||
triggers: Triggers::new(),
|
triggers: Triggers::new(),
|
||||||
emitters: Emitters::new(),
|
emitters: Emitters::new(),
|
||||||
post: PostChain::new(),
|
|
||||||
pending_sounds: Vec::new(),
|
pending_sounds: Vec::new(),
|
||||||
|
pending_post: Vec::new(),
|
||||||
signals_path,
|
signals_path,
|
||||||
sim_accum: 0.0,
|
sim_accum: 0.0,
|
||||||
pending_jump: false,
|
pending_jump: false,
|
||||||
@@ -138,6 +138,13 @@ impl Session {
|
|||||||
std::mem::take(&mut self.pending_sounds)
|
std::mem::take(&mut self.pending_sounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Aufgelaufene Post-Effekt-Schaltungen (`SetPost`-Actions) abholen — jeden
|
||||||
|
/// Frame vom Fenster-Frontend, das sie auf seine Kette anwendet. Headless
|
||||||
|
/// bleibt die Queue liegen (die CLI hat keine Kette) und wird nie gezogen.
|
||||||
|
pub fn take_post(&mut self) -> Vec<(String, bool)> {
|
||||||
|
std::mem::take(&mut self.pending_post)
|
||||||
|
}
|
||||||
|
|
||||||
/// Einen Simulationsschritt treiben — das kontinuierliche Gegenstück zu
|
/// Einen Simulationsschritt treiben — das kontinuierliche Gegenstück zu
|
||||||
/// `exec`. Im Spielmodus läuft die Physik, im Noclip fliegt derselbe
|
/// `exec`. Im Spielmodus läuft die Physik, im Noclip fliegt derselbe
|
||||||
/// Spieler frei; Menü und Dialog pausieren die Welt. Rückgabe:
|
/// Spieler frei; Menü und Dialog pausieren die Welt. Rückgabe:
|
||||||
@@ -255,7 +262,16 @@ impl Session {
|
|||||||
}
|
}
|
||||||
None => ExecResult::lines(vec!["Modus: play | free | menu".into()]),
|
None => ExecResult::lines(vec!["Modus: play | free | menu".into()]),
|
||||||
},
|
},
|
||||||
("post", args) => ExecResult::lines(self.post_cmd(args)),
|
// Post-Effekt über dieselbe Action wie das `post`-Verb aus
|
||||||
|
// signals.toml/Ink-Tags. Das Fenster löst den Namen gegen seine
|
||||||
|
// Kette auf; headless bleibt es bei der `[action]`-Zeile.
|
||||||
|
("post", args) => match signals::parse_post_arg(args) {
|
||||||
|
Some((name, on)) => {
|
||||||
|
self.game.actions.push(Action::SetPost { name, on });
|
||||||
|
ExecResult::lines(self.drain_actions())
|
||||||
|
}
|
||||||
|
None => ExecResult::lines(vec!["post <name> on|off".into()]),
|
||||||
|
},
|
||||||
("signal", sig) => ExecResult::lines(self.fire(sig, None)),
|
("signal", sig) => ExecResult::lines(self.fire(sig, None)),
|
||||||
// Objekt-Interaktion (Klick wie Konsole): Klick-Ziele feuern ihre
|
// Objekt-Interaktion (Klick wie Konsole): Klick-Ziele feuern ihre
|
||||||
// `signal`-Property, sonst den suffix-befreiten Objektnamen.
|
// `signal`-Property, sonst den suffix-befreiten Objektnamen.
|
||||||
@@ -272,32 +288,6 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `post` listet die Kette, `post <name> [on|off]` schaltet sie (ohne
|
|
||||||
/// Zustand: toggle). Die Namen kommen vom Frontend — die Session validiert
|
|
||||||
/// nur gegen das, was angemeldet wurde.
|
|
||||||
fn post_cmd(&mut self, args: &str) -> Vec<String> {
|
|
||||||
if args.is_empty() {
|
|
||||||
let lines: Vec<String> = self.post.list()
|
|
||||||
.map(|(n, on)| format!(" {n:<10} {}", on_off(on)))
|
|
||||||
.collect();
|
|
||||||
return match lines.is_empty() {
|
|
||||||
true => vec!["keine Post-Effekte (nur im Fenster-Frontend)".into()],
|
|
||||||
false => lines,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let (name, state) = args.split_once(char::is_whitespace).unwrap_or((args, ""));
|
|
||||||
let on = match state.trim() {
|
|
||||||
"" => None,
|
|
||||||
"on" | "an" => Some(true),
|
|
||||||
"off" | "aus" => Some(false),
|
|
||||||
_ => return vec!["post <name> [on|off]".into()],
|
|
||||||
};
|
|
||||||
match self.post.set(name, on) {
|
|
||||||
Some(now) => vec![format!("{name}: {}", on_off(now))],
|
|
||||||
None => vec![format!("unbekannter Effekt: {name:?} — `post` listet sie")],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dialog_input(&mut self, line: &str) -> ExecResult {
|
fn dialog_input(&mut self, line: &str) -> ExecResult {
|
||||||
let Mode::Dialog(d) = &self.mode else { return ExecResult::empty(); };
|
let Mode::Dialog(d) = &self.mode else { return ExecResult::empty(); };
|
||||||
let sel = if d.choices.is_empty() {
|
let sel = if d.choices.is_empty() {
|
||||||
@@ -378,6 +368,10 @@ impl Session {
|
|||||||
self.set_mode(t);
|
self.set_mode(t);
|
||||||
out.push(format!("[action] mode {}", t.label()));
|
out.push(format!("[action] mode {}", t.label()));
|
||||||
}
|
}
|
||||||
|
Action::SetPost { name, on } => {
|
||||||
|
out.push(format!("[action] post {name} {}", if on { "on" } else { "off" }));
|
||||||
|
self.pending_post.push((name, on));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -404,10 +398,6 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_off(on: bool) -> &'static str {
|
|
||||||
if on { "an" } else { "aus" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn help() -> Vec<String> {
|
fn help() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
" signal <s> Signal feuern oder Action direkt ausführen".into(),
|
" signal <s> Signal feuern oder Action direkt ausführen".into(),
|
||||||
@@ -417,7 +407,7 @@ fn help() -> Vec<String> {
|
|||||||
" kv KV-Store anzeigen".into(),
|
" kv KV-Store anzeigen".into(),
|
||||||
" menu Menü öffnen / ins Spiel zurück".into(),
|
" menu Menü öffnen / ins Spiel zurück".into(),
|
||||||
" mode <m> Modus setzen: play | free (Noclip) | menu".into(),
|
" mode <m> Modus setzen: play | free (Noclip) | menu".into(),
|
||||||
" post [<e> on|off] Post-Effekte anzeigen / schalten".into(),
|
" post <e> on|off Post-Effekt schalten (Fenster-Frontend)".into(),
|
||||||
" reload signals.toml neu laden".into(),
|
" reload signals.toml neu laden".into(),
|
||||||
" quit beenden".into(),
|
" quit beenden".into(),
|
||||||
]
|
]
|
||||||
@@ -643,21 +633,29 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn post_command_switches_registered_effects() {
|
fn post_command_queues_setpost_for_the_frontend() {
|
||||||
let mut s = empty_session();
|
let mut s = empty_session();
|
||||||
// Ohne Frontend ist die Kette leer — der Befehl sagt das, statt zu lügen.
|
s.mode = Mode::Free;
|
||||||
assert_eq!(s.exec("post").output.len(), 1);
|
// Der Befehl kennt die Kette nicht — er reiht die Schaltung ein, das
|
||||||
assert!(!s.post.enabled("dither"));
|
// Fenster zieht sie per take_post().
|
||||||
|
let r = s.exec("post outline off");
|
||||||
|
assert_eq!(r.output, vec!["[action] post outline off"]);
|
||||||
|
assert_eq!(s.take_post(), vec![("outline".to_string(), false)]);
|
||||||
|
assert!(s.take_post().is_empty(), "abgeholt = geleert");
|
||||||
|
|
||||||
s.post.register("dither", true);
|
// Fehlendes/ungültiges on|off → Nutzungshinweis, nichts eingereiht.
|
||||||
assert_eq!(s.exec("post").output.len(), 1, "eine Zeile je Effekt");
|
assert_eq!(s.exec("post outline").output, vec!["post <name> on|off"]);
|
||||||
s.exec("post dither off");
|
assert_eq!(s.exec("post outline wat").output, vec!["post <name> on|off"]);
|
||||||
assert!(!s.post.enabled("dither"));
|
assert!(s.take_post().is_empty());
|
||||||
s.exec("post dither");
|
}
|
||||||
assert!(s.post.enabled("dither"), "ohne Zustand togglet es");
|
|
||||||
// Unbekannter Effekt meldet sich und lässt die Kette in Ruhe.
|
#[test]
|
||||||
assert_eq!(s.exec("post bloom on").output.len(), 1);
|
fn post_signal_from_table_queues_setpost() {
|
||||||
assert!(s.post.enabled("dither"));
|
let mut s = empty_session();
|
||||||
|
s.mode = Mode::Free;
|
||||||
|
// Ink-Tag-/signals.toml-Pfad: `post`-Verb direkt als Action.
|
||||||
|
s.exec("signal post dither on");
|
||||||
|
assert_eq!(s.take_post(), vec![("dither".to_string(), true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user