Make export own its playback and centralize session exits (#89)
Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
+93
-61
@@ -20,6 +20,7 @@ mod map;
|
||||
mod film;
|
||||
mod monitor;
|
||||
mod panel;
|
||||
mod presentation;
|
||||
mod radar;
|
||||
mod replay;
|
||||
mod smoke;
|
||||
@@ -48,10 +49,8 @@ struct AirDef {
|
||||
grid_ids: Vec<u32>,
|
||||
/// The raid the wave menu orders.
|
||||
wave: wave::WaveConfig,
|
||||
/// A replay playing (it takes the screen and the mouse wheel).
|
||||
replay: Option<replay::Player>,
|
||||
/// An MP4 export in progress (`/airdef replay export`).
|
||||
export: Option<Export>,
|
||||
/// Playback lifecycle. Export owns the exact replay player it is capturing.
|
||||
presentation: presentation::Session<replay::Player, Export>,
|
||||
/// The effects preset a film replaced: (shader, hd).
|
||||
film_prev: Option<(bool, bool)>,
|
||||
/// Counts the replay/film sessions started; an export belongs to the one it began with.
|
||||
@@ -465,7 +464,7 @@ impl AirDef {
|
||||
let shown = self.shown();
|
||||
// A replay lights with the recording's searchlights, where and as they pointed.
|
||||
let beams: Vec<sim::lights::Beam> = if shown.replay {
|
||||
match self.replay.as_ref().and_then(|r| self.sim.replay(r.rec).map(|rec| rec.beams(f64::from(shown.now)))) {
|
||||
match self.presentation.playback().and_then(|r| self.sim.replay(r.rec).map(|rec| rec.beams(f64::from(shown.now)))) {
|
||||
Some(b) => b,
|
||||
None => Vec::new(),
|
||||
}
|
||||
@@ -575,15 +574,14 @@ impl AirDef {
|
||||
|
||||
/// The smoke volumes the renderer draws and the clock they are aged by: the live ones, or (in a
|
||||
/// replay) the blasts' billows at the replay's moment, never the live raid's trails and fires.
|
||||
/// The billows are the live ones' own function (`Smoke::segments`), run on the recording's
|
||||
/// blasts and time with the live state set aside for the call.
|
||||
fn shown_smoke(&mut self) -> (f64, Vec<render::SmokeSegment>) {
|
||||
/// The recorded scene is read directly; live simulation state is never replaced.
|
||||
fn shown_smoke(&self) -> (f64, Vec<render::SmokeSegment>) {
|
||||
let shown = self.shown();
|
||||
if !shown.replay {
|
||||
return (self.sim.tick as f64, self.smoke.segments(&self.sim));
|
||||
}
|
||||
// The recording alone, as a function of its time: the live Sim is not touched.
|
||||
let segments = match self.sim.replay(self.replay.as_ref().map_or(0, |r| r.rec)) {
|
||||
let segments = match self.sim.replay(self.presentation.playback().map_or(0, |r| r.rec)) {
|
||||
Some(rec) => smoke::replay_smoke(rec, f64::from(shown.now)),
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -592,7 +590,7 @@ impl AirDef {
|
||||
|
||||
/// Where the picture is seen from: the replay's camera, or the player's eyes.
|
||||
fn viewer(&self, shown: &Shown) -> Option<DVec3> {
|
||||
match &self.replay {
|
||||
match self.presentation.playback() {
|
||||
Some(r) if shown.replay => Some(r.eye()),
|
||||
_ => player().map(|pl| eye(&pl)),
|
||||
}
|
||||
@@ -601,7 +599,7 @@ impl AirDef {
|
||||
/// The blasts the effects show and the clock they run on: the live ones, or (in a replay) the
|
||||
/// recording's at the replay's moment, never the live world's.
|
||||
fn shown(&self) -> Shown {
|
||||
if let Some(r) = &self.replay
|
||||
if let Some(r) = self.presentation.playback()
|
||||
&& r.filming()
|
||||
&& let Some(rec) = self.sim.replay(r.rec)
|
||||
{
|
||||
@@ -645,7 +643,7 @@ impl AirDef {
|
||||
/// The engines the simulation or the replay declared become persistent sound sources (#95):
|
||||
/// started, followed, stopped. The one-shots below stay as they were.
|
||||
fn step_engines(&mut self, out: &mut Out) {
|
||||
let replay = self.replay.as_ref().is_some_and(|r| r.filming());
|
||||
let replay = self.presentation.playback().is_some_and(|r| r.filming());
|
||||
let cut = std::mem::take(&mut out.engine_cut) || replay != self.engines_replay;
|
||||
self.engines_replay = replay;
|
||||
let generation = render::sound_source_generation();
|
||||
@@ -719,7 +717,7 @@ impl AirDef {
|
||||
self.send_models(&out.models);
|
||||
}
|
||||
if !out.particles.is_empty() {
|
||||
let hd_smoke = self.smoke.hd && !self.replay.as_ref().is_some_and(|r| r.filming());
|
||||
let hd_smoke = self.smoke.hd && !self.presentation.playback().is_some_and(|r| r.filming());
|
||||
let spawns: Vec<ParticleSpawn> = out
|
||||
.particles
|
||||
.into_iter()
|
||||
@@ -799,7 +797,7 @@ impl AirDef {
|
||||
fn draw_hud(&mut self, player: DVec3, yaw: f32) {
|
||||
let pic = self.sim.air_picture();
|
||||
// The command post's grid and replays cover the screen: the other panels wait under them.
|
||||
if self.grid || self.replay.is_some() || (pic.tracks.is_empty() && self.sim.sams.is_empty() && self.sim.missiles.is_empty()) {
|
||||
if self.grid || self.presentation.playback().is_some() || (pic.tracks.is_empty() && self.sim.sams.is_empty() && self.sim.missiles.is_empty()) {
|
||||
if self.hud_shown {
|
||||
hud::clear();
|
||||
render::clear_corner();
|
||||
@@ -1077,9 +1075,7 @@ impl AirDef {
|
||||
if self.world.is_some() && self.dirty {
|
||||
self.save();
|
||||
}
|
||||
if self.replay.is_some() {
|
||||
self.stop_replay();
|
||||
}
|
||||
self.end_session(presentation::ExitCause::WorldChange);
|
||||
let shader = self.sim.shader;
|
||||
let seed = game::get_clock().map_or(0x5eed, |c| c.game_time.wrapping_mul(2654435761) ^ 0x9e37);
|
||||
self.sim = Sim::new(seed);
|
||||
@@ -1141,7 +1137,7 @@ impl AirDef {
|
||||
|
||||
/// Plays the last recorded raid: "map", "full", "reel" or (with `event`) a clip.
|
||||
fn start_replay(&mut self, what: &str, event: Option<usize>) -> Result<(), String> {
|
||||
switch_allowed(self.export.is_some())?;
|
||||
switch_allowed(self.presentation.is_exporting())?;
|
||||
let n = self.sim.replays.len();
|
||||
if n == 0 {
|
||||
return Err("Повторов пока нет: запишется первый же налёт (когда небо очистится)".into());
|
||||
@@ -1162,30 +1158,34 @@ impl AirDef {
|
||||
self.say_block_log(rec);
|
||||
let player = replay::Player::new(rec.number, mode, &self.sim);
|
||||
// A film (its quality and effects) or another replay ends properly before this one.
|
||||
self.end_session();
|
||||
self.end_session(presentation::ExitCause::Replaced);
|
||||
self.session += 1;
|
||||
self.replay = Some(player);
|
||||
self.presentation.replay(player);
|
||||
self.grid = false;
|
||||
input::claim_wheel(true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_replay(&mut self) {
|
||||
if self.export.take().is_some() {
|
||||
render::capture_stop(true);
|
||||
let was_export = self.presentation.is_exporting();
|
||||
self.end_session(presentation::ExitCause::UserStop);
|
||||
if was_export {
|
||||
say("Экспорт отменён");
|
||||
}
|
||||
self.end_session();
|
||||
}
|
||||
|
||||
/// Ends whatever replay or film shows, on every path: the film quality, the effects, the
|
||||
/// clean HUD, the views and the time and camera overrides go back to the live game's.
|
||||
fn end_session(&mut self) {
|
||||
fn end_session(&mut self, cause: presentation::ExitCause) {
|
||||
let cleanup = self.presentation.end(cause);
|
||||
if cleanup.cancel_capture {
|
||||
render::capture_stop(true);
|
||||
}
|
||||
self.pending.clear();
|
||||
render::set_film_quality(false);
|
||||
if let Some((shader, hd)) = self.film_prev.take() {
|
||||
self.set_effects(shader, hd);
|
||||
}
|
||||
self.replay = None;
|
||||
self.show_world_at(0, 0.0, false);
|
||||
render::set_time_override(None);
|
||||
render::set_main_view(None);
|
||||
@@ -1198,7 +1198,7 @@ impl AirDef {
|
||||
/// Plays the director's film of the last raid at the best quality: HD effects on (restored after).
|
||||
fn start_film(&mut self, which: &str, export: Option<&ExportOpts>) -> Result<(), String> {
|
||||
if export.is_none() {
|
||||
switch_allowed(self.export.is_some())?;
|
||||
switch_allowed(self.presentation.is_exporting())?;
|
||||
}
|
||||
let n = self.sim.replays.len();
|
||||
if n == 0 {
|
||||
@@ -1219,9 +1219,9 @@ impl AirDef {
|
||||
player.limit = o.seconds;
|
||||
}
|
||||
// What was shown ends first (its effects are remembered below, not lost).
|
||||
self.end_session();
|
||||
self.end_session(presentation::ExitCause::Replaced);
|
||||
self.session += 1;
|
||||
self.replay = Some(player);
|
||||
self.presentation.replay(player);
|
||||
self.grid = false;
|
||||
input::claim_wheel(true);
|
||||
// The best picture: HD on, the renderer's film quality (full-size world, more cloud and fog steps).
|
||||
@@ -1254,7 +1254,7 @@ impl AirDef {
|
||||
o.name = a.chars().filter(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')).collect();
|
||||
}
|
||||
}
|
||||
if self.export.is_some() {
|
||||
if self.presentation.is_exporting() {
|
||||
return Err("Экспорт уже идёт (/airdef replay stop — отмена)".into());
|
||||
}
|
||||
if o.name.is_empty() {
|
||||
@@ -1263,20 +1263,26 @@ impl AirDef {
|
||||
}
|
||||
let which = o.which.clone();
|
||||
self.start_film(&which, Some(&o))?;
|
||||
let state = self.presentation.take();
|
||||
let playback = match state {
|
||||
presentation::State::Replay(player) => player,
|
||||
other => {
|
||||
self.presentation.restore(other);
|
||||
self.end_session(presentation::ExitCause::Failed);
|
||||
return Err("Не удалось передать фильм в захват".into());
|
||||
}
|
||||
};
|
||||
say(&format!("Экспорт «{}»: {} fps, {}x{}; планирую кадры…", o.name, o.fps, o.size.0, o.size.1));
|
||||
self.export = Some(Export { session: self.session, opts: o, stage: Stage::Planning, ticks: 0 });
|
||||
self.presentation.export(playback, Export { session: self.session, opts: o, stage: Stage::Planning, ticks: 0 });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drives an export: plan, start the recorder, wait for its first frame, report, stop at the end.
|
||||
fn export_step(&mut self, r: &mut replay::Player) {
|
||||
let Some(ex) = &mut self.export else { return };
|
||||
fn export_step(&mut self, r: &mut replay::Player, ex: &mut Export) -> Option<Result<(), String>> {
|
||||
if ex.session != self.session {
|
||||
// The film this export recorded was replaced: the recorder stops, nothing else drives it.
|
||||
render::capture_stop(true);
|
||||
self.export = None;
|
||||
say("Экспорт отменён: показ заменён другим");
|
||||
return;
|
||||
return Some(Err("показ заменён другим".into()));
|
||||
}
|
||||
ex.ticks += 1;
|
||||
let st = render::capture_status();
|
||||
@@ -1334,13 +1340,14 @@ impl AirDef {
|
||||
_ => {}
|
||||
}
|
||||
if let Some(res) = finish {
|
||||
self.export = None;
|
||||
match res {
|
||||
match &res {
|
||||
Ok(m) => say(&format!("Экспорт готов: {m}")),
|
||||
Err(e) => say(&format!("Экспорт не удался: {e}")),
|
||||
}
|
||||
r.done = true;
|
||||
return Some(res.map(|_| ()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// What a raid strikes: the marked objects, else where the player stands.
|
||||
@@ -1653,10 +1660,9 @@ impl Mod for AirDef {
|
||||
grid: false,
|
||||
grid_ids: Vec::new(),
|
||||
wave: wave::WaveConfig::default(),
|
||||
replay: None,
|
||||
presentation: presentation::Session::default(),
|
||||
block_log: None,
|
||||
time_view: false,
|
||||
export: None,
|
||||
film_prev: None,
|
||||
session: 0,
|
||||
smoke: Default::default(),
|
||||
@@ -1704,23 +1710,23 @@ impl Mod for AirDef {
|
||||
|
||||
fn tick(&mut self, tick: u64) {
|
||||
self.sim.engine_tick = tick;
|
||||
let Some(p) = player() else { return };
|
||||
// Each world keeps its own systems, objects and settings: loaded when it opens.
|
||||
let world = world_identity();
|
||||
if self.world.as_ref() != Some(&world) {
|
||||
self.open_world(world);
|
||||
}
|
||||
let Some(p) = player() else { return };
|
||||
for event in input::poll() {
|
||||
match event {
|
||||
Event::UseStart(u) => self.use_item(&u.item.id),
|
||||
Event::KeyDown(k) if k == "map" => self.map_toggle(),
|
||||
Event::KeyDown(k) if k == "menu" => {
|
||||
if self.replay.is_some() {
|
||||
if self.presentation.playback().is_some() {
|
||||
self.stop_replay();
|
||||
}
|
||||
self.open_menu(Page::Main)
|
||||
}
|
||||
Event::KeyDown(k) if k == "rp_exit" && self.replay.is_some() => self.stop_replay(),
|
||||
Event::KeyDown(k) if k == "rp_exit" && self.presentation.playback().is_some() => self.stop_replay(),
|
||||
Event::KeyDown(k) if k.starts_with("rp_") => {
|
||||
let c = match k.as_str() {
|
||||
"rp_back" => replay::Control::Back,
|
||||
@@ -1729,7 +1735,7 @@ impl Mod for AirDef {
|
||||
"rp_slower" => replay::Control::Slower,
|
||||
_ => replay::Control::Pause,
|
||||
};
|
||||
if let Some(r) = &mut self.replay
|
||||
if let Some(r) = self.presentation.playback_mut()
|
||||
&& let Some(rec) = self.sim.replay(r.rec)
|
||||
{
|
||||
r.control(c, rec);
|
||||
@@ -1739,14 +1745,14 @@ impl Mod for AirDef {
|
||||
Event::KeyDown(k) if k == "grid" => self.grid = !self.grid,
|
||||
Event::KeyDown(k) if k == "ew" => self.toggle_ew(),
|
||||
Event::KeyDown(k) if k == "replay" => {
|
||||
if self.replay.is_some() {
|
||||
if self.presentation.playback().is_some() {
|
||||
self.stop_replay();
|
||||
} else if let Err(e) = self.start_replay("map", None) {
|
||||
say(&e);
|
||||
}
|
||||
}
|
||||
Event::Wheel(w) => {
|
||||
if let Some(r) = &mut self.replay
|
||||
if let Some(r) = self.presentation.playback_mut()
|
||||
&& let Some(rec) = self.sim.replay(r.rec)
|
||||
{
|
||||
r.wheel(w, rec);
|
||||
@@ -1819,22 +1825,48 @@ impl Mod for AirDef {
|
||||
if !sections.is_empty() {
|
||||
render::query_sections(render::Policy::Collision, §ions);
|
||||
}
|
||||
if let Some(mut r) = self.replay.take() {
|
||||
// A replay shows the recording alone: not the live raid's models, particles, sounds or jolts.
|
||||
if r.filming() {
|
||||
out.models.clear();
|
||||
out.particles.clear();
|
||||
out.sounds.clear();
|
||||
out.engines.clear();
|
||||
out.shake = None;
|
||||
match self.presentation.take() {
|
||||
presentation::State::Live => {}
|
||||
presentation::State::Replay(mut playback) => {
|
||||
if playback.filming() {
|
||||
out.models.clear();
|
||||
out.particles.clear();
|
||||
out.sounds.clear();
|
||||
out.engines.clear();
|
||||
out.shake = None;
|
||||
}
|
||||
playback.tick(&self.sim, &mut self.ground, self.listener, &mut out);
|
||||
if playback.done {
|
||||
self.presentation.restore(presentation::State::Replay(playback));
|
||||
self.end_session(presentation::ExitCause::Finished);
|
||||
} else {
|
||||
self.presentation.restore(presentation::State::Replay(playback));
|
||||
}
|
||||
}
|
||||
r.tick(&self.sim, &mut self.ground, self.listener, &mut out);
|
||||
self.export_step(&mut r);
|
||||
if r.done {
|
||||
self.replay = Some(r);
|
||||
self.stop_replay();
|
||||
} else {
|
||||
self.replay = Some(r);
|
||||
presentation::State::Export { mut playback, mut export } => {
|
||||
let progress = if export.session != self.session {
|
||||
Some(Err("показ заменён другим".into()))
|
||||
} else {
|
||||
if playback.filming() {
|
||||
out.models.clear();
|
||||
out.particles.clear();
|
||||
out.sounds.clear();
|
||||
out.engines.clear();
|
||||
out.shake = None;
|
||||
}
|
||||
playback.tick(&self.sim, &mut self.ground, self.listener, &mut out);
|
||||
self.export_step(&mut playback, &mut export)
|
||||
};
|
||||
let cause = match progress {
|
||||
Some(Ok(())) => Some(presentation::ExitCause::Finished),
|
||||
Some(Err(_)) => Some(presentation::ExitCause::Failed),
|
||||
None if playback.done => Some(presentation::ExitCause::Failed),
|
||||
None => None,
|
||||
};
|
||||
self.presentation.restore(presentation::State::Export { playback, export });
|
||||
if let Some(cause) = cause {
|
||||
self.end_session(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.emit(out);
|
||||
@@ -1843,7 +1875,7 @@ impl Mod for AirDef {
|
||||
self.map_frame();
|
||||
self.draw_hud(DVec3::new(p.x, p.y, p.z), p.yaw);
|
||||
// The replay takes the screen; else the monitors and the cameras they show.
|
||||
if let Some(r) = &self.replay {
|
||||
if let Some(r) = self.presentation.playback() {
|
||||
let size = render::gui_size();
|
||||
render::set_hud(&r.hud(&self.sim, size));
|
||||
render::set_views(&r.views(size));
|
||||
|
||||
@@ -573,7 +573,7 @@ impl AirDef {
|
||||
self.map_close();
|
||||
return;
|
||||
}
|
||||
if self.replay.is_some() {
|
||||
if self.presentation.playback().is_some() {
|
||||
say("Карта: сначала закройте повтор");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Presentation playback ownership and lifecycle transitions.
|
||||
|
||||
/// The one active presentation mode. Export owns its playback so it cannot outlive its film.
|
||||
pub enum State<P, E> {
|
||||
Live,
|
||||
Replay(P),
|
||||
Export { playback: P, export: E },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ExitCause {
|
||||
UserStop,
|
||||
WorldChange,
|
||||
Replaced,
|
||||
Finished,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Cleanup {
|
||||
pub was_export: bool,
|
||||
pub cancel_capture: bool,
|
||||
}
|
||||
|
||||
pub struct Session<P, E> {
|
||||
state: State<P, E>,
|
||||
}
|
||||
|
||||
impl<P, E> Default for Session<P, E> {
|
||||
fn default() -> Self {
|
||||
Self { state: State::Live }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, E> Session<P, E> {
|
||||
pub fn is_live(&self) -> bool {
|
||||
matches!(self.state, State::Live)
|
||||
}
|
||||
|
||||
pub fn is_exporting(&self) -> bool {
|
||||
matches!(self.state, State::Export { .. })
|
||||
}
|
||||
|
||||
pub fn playback(&self) -> Option<&P> {
|
||||
match &self.state {
|
||||
State::Live => None,
|
||||
State::Replay(player)
|
||||
| State::Export {
|
||||
playback: player, ..
|
||||
} => Some(player),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn playback_mut(&mut self) -> Option<&mut P> {
|
||||
match &mut self.state {
|
||||
State::Live => None,
|
||||
State::Replay(player)
|
||||
| State::Export {
|
||||
playback: player, ..
|
||||
} => Some(player),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replay(&mut self, playback: P) {
|
||||
assert!(
|
||||
self.is_live(),
|
||||
"end the current presentation before replacing it"
|
||||
);
|
||||
self.state = State::Replay(playback);
|
||||
}
|
||||
|
||||
pub fn export(&mut self, playback: P, export: E) {
|
||||
assert!(
|
||||
self.is_live(),
|
||||
"end the current presentation before starting export"
|
||||
);
|
||||
self.state = State::Export { playback, export };
|
||||
}
|
||||
|
||||
/// Temporarily move the active state out while a tick mutates the player and export together.
|
||||
pub fn take(&mut self) -> State<P, E> {
|
||||
std::mem::replace(&mut self.state, State::Live)
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, state: State<P, E>) {
|
||||
assert!(self.is_live(), "restore only into a detached live session");
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
/// Every exit converges here; a completed capture is already drained and must not be cancelled.
|
||||
pub fn end(&mut self, cause: ExitCause) -> Cleanup {
|
||||
let was_export = self.is_exporting();
|
||||
self.state = State::Live;
|
||||
Cleanup {
|
||||
was_export,
|
||||
cancel_capture: was_export && cause != ExitCause::Finished,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ExitCause, Session, State};
|
||||
|
||||
#[test]
|
||||
fn export_always_owns_its_playback_and_user_stop_cleans_capture() {
|
||||
let mut session = Session::<u8, &str>::default();
|
||||
session.export(7, "capture");
|
||||
assert!(matches!(
|
||||
session.take(),
|
||||
State::Export {
|
||||
playback: 7,
|
||||
export: "capture"
|
||||
}
|
||||
));
|
||||
session.export(7, "capture");
|
||||
assert_eq!(session.end(ExitCause::UserStop).cancel_capture, true);
|
||||
assert!(session.is_live());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn world_change_and_failure_share_cleanup_while_success_drains() {
|
||||
for cause in [ExitCause::WorldChange, ExitCause::Failed] {
|
||||
let mut session = Session::<(), ()>::default();
|
||||
session.export((), ());
|
||||
assert_eq!(session.end(cause).cancel_capture, true);
|
||||
assert!(session.is_live());
|
||||
}
|
||||
let mut session = Session::<(), ()>::default();
|
||||
session.export((), ());
|
||||
assert_eq!(session.end(ExitCause::Finished).cancel_capture, false);
|
||||
assert!(session.is_live());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_replacement_is_not_an_export_and_stop_is_idempotent() {
|
||||
let mut session = Session::<u8, ()>::default();
|
||||
session.replay(1);
|
||||
assert!(session.playback().is_some());
|
||||
assert_eq!(session.end(ExitCause::Replaced).was_export, false);
|
||||
assert_eq!(session.end(ExitCause::UserStop).was_export, false);
|
||||
assert!(session.is_live());
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl AirDef {
|
||||
}
|
||||
// The whole body is read and checked before anything is touched (#86).
|
||||
let layout = layout::Layout::parse(text).map_err(|e| format!("Сценарий не загружен, сцена осталась прежней: {e}"))?;
|
||||
if self.replay.is_some() {
|
||||
if self.presentation.playback().is_some() {
|
||||
self.stop_replay();
|
||||
}
|
||||
// Everything of the last layout goes: its systems with their jobs and targets, the sky,
|
||||
|
||||
Reference in New Issue
Block a user