Files
airdef/airdef-mod/src/capabilities.rs
T
chexit 910f02ca92
build / mod (push) Failing after 19s
feat: consume scoped engine capabilities and preserve safe fallbacks
Pin API27, diagnose prepared states, gate capture ownership and keep far-sound fallback until acoustics is prepared. Retain engine changes in build provenance and cancel invalid return reservations after cell loss.

Co-authored-by: Codex <codex@openai.com>
2026-11-03 00:24:46 +03:00

148 lines
4.7 KiB
Rust

//! Host milestones, not promises of audible output or completed GPU/file work.
use horizon_mod_sdk::render::{self, CapabilityReason, CapabilityState};
pub fn ready(s: &CapabilityState) -> bool {
s.generation != 0
&& s.available
&& s.enabled
&& s.request_accepted
&& s.prepared
&& s.reason == CapabilityReason::Ready
}
pub fn capture_problem(s: &CapabilityState) -> Option<&'static str> {
if !s.available {
Some("Движок не поддерживает подтверждение захвата")
} else if !s.enabled || s.generation == 0 {
Some("Захват недоступен без текущего мира")
} else if !s.request_accepted {
Some("Захват больше не принадлежит этой сессии мода")
} else if s.reason == CapabilityReason::Failed {
Some("Ошибка пути захвата движка")
} else {
None
}
}
/// Adopt the existing global preference once in this world, without switching it.
/// A configured request and actual backend preparation are still separate milestones.
pub fn acoustics() -> CapabilityState {
let s = render::acoustics_status();
if s.available && s.generation != 0 && !s.request_accepted {
render::set_acoustics(s.enabled);
render::acoustics_status()
} else {
s
}
}
fn reason(r: CapabilityReason) -> &'static str {
match r {
CapabilityReason::AwaitingWorld => "awaiting-world",
CapabilityReason::Disabled => "disabled",
CapabilityReason::NotRequested => "not-requested",
CapabilityReason::AwaitingBackend => "awaiting-backend",
CapabilityReason::Failed => "failed",
CapabilityReason::Ready => "ready",
}
}
fn line(
name: &str,
generation: u64,
available: bool,
enabled: bool,
accepted: bool,
prepared: bool,
why: &str,
) -> String {
format!("{name}: API={available}, включено={enabled}, запрос={accepted}, подготовлено={prepared}, поколение={generation}, {why}")
}
pub fn diagnostics() -> String {
let smoke = render::smoke_status();
let acoustic = acoustics();
let capture = render::capture_owner_status();
let smoke_reason = match smoke.reason {
render::SmokeReason::AwaitingFrame => "awaiting-frame",
render::SmokeReason::Disabled => "disabled",
render::SmokeReason::PathUnavailable => "path-unavailable",
render::SmokeReason::Ready => "ready",
};
[
"Возможности движка (API 27; подготовка не означает готовый GPU-кадр или файл):".into(),
line(
"Дым",
smoke.generation,
smoke.available,
smoke.enabled,
smoke.request_accepted,
smoke.backend_ready,
smoke_reason,
),
line(
"Акустика",
acoustic.generation,
acoustic.available,
acoustic.enabled,
acoustic.request_accepted,
acoustic.prepared,
reason(acoustic.reason),
),
line(
"Захват этого мода",
capture.generation,
capture.available,
capture.enabled,
capture.request_accepted,
capture.prepared,
reason(capture.reason),
),
]
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn state() -> CapabilityState {
CapabilityState {
generation: 5,
available: true,
enabled: true,
request_accepted: true,
prepared: true,
reason: CapabilityReason::Ready,
}
}
#[test]
fn acceptance_alone_never_disables_the_working_fallback() {
assert!(ready(&state()));
for missing in 0..6 {
let mut s = state();
match missing {
0 => s.generation = 0,
1 => s.available = false,
2 => s.enabled = false,
3 => s.request_accepted = false,
4 => s.prepared = false,
_ => s.reason = CapabilityReason::AwaitingBackend,
}
assert!(!ready(&s));
}
}
#[test]
fn a_foreign_capture_is_not_success_and_preparing_is_not_failure() {
let mut s = state();
s.request_accepted = false;
assert!(capture_problem(&s).is_some());
s = state();
s.prepared = false;
s.reason = CapabilityReason::AwaitingBackend;
assert!(capture_problem(&s).is_none());
assert!(!ready(&s));
s.reason = CapabilityReason::Failed;
assert!(capture_problem(&s).is_some());
}
}