Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,9 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
|
||||
Радар игрока и мониторы показывают только то, что держат РЛС; в сводке видно «Вне видимости РЛС: N».
|
||||
Мёртвые зоны — ближе минимальной дальности ракет (Patriot 10 м, Панцирь 25, С-400 30, С-500 40).
|
||||
|
||||
**Сохранение.** Комплексы, объекты, их повреждения и настройки сохраняются отдельно для каждого мира (и для каждого
|
||||
сервера); уничтоженный комплекс остаётся остовом и после перезахода, пока его не отремонтируют.
|
||||
|
||||
**Повреждения.** Взрыв рядом с комплексом повреждает его: он хуже поворачивается, хуже видит и попадает,
|
||||
дымит, ниже 35% горит. Уничтоженный остаётся горящим остовом, его боекомплект детонирует: чем больше ракет
|
||||
оставалось, тем мощнее взрыв и дольше вторичные (у Гепарда рвутся снаряды); соседние комплексы тоже могут
|
||||
@@ -105,6 +108,7 @@ CI: `.gitea/workflows/build.yml` на каждый пуш собирает мо
|
||||
Команды:
|
||||
- `/airdef wave <тип> [кол-во] [дальность]` — налёт на ближайшую цель. Типы: `shahed`, `geran3`, `lancet`, `gerbera`, `kh101`, `kalibr`, `kh22`, `zircon`, `iskander`, `kinzhal`, `oreshnik` и `mix` (комбинированный удар двумя секторами: «Герберы», шахеды, Герани-3, Х-101, «Калибры», Х-22, Циркон, «Искандер», «Кинжал», при 30+ — «Орешник»);
|
||||
- `/airdef replay [map|full|reel|stop]` — повтор последнего налёта;
|
||||
- `/airdef import` — перенести в текущий мир комплексы и цели из старой версии мода (до 0.2.x они были общими для всех миров);
|
||||
- `/airdef raid` — налёт, настроенный в меню; `/airdef target here` — отметить объект под ногами; `/airdef repair` — ремонт;
|
||||
- `/airdef roles` — показать, по каким классам целей работает каждый комплекс; `/airdef roles <patriot|gepard|pantsir|s400|s500|all> <классы|default>` — задать (классы через запятую: `drone`, `jet`, `cruise`, `supersonic`, `ballistic` или `all`), например `/airdef roles s400 cruise,ballistic`;
|
||||
- `/airdef deploy <patriot|gepard|pantsir|s400|s500>` — поставить комплекс в 8 блоках перед собой;
|
||||
|
||||
+73
-4
@@ -58,6 +58,10 @@ struct AirDef {
|
||||
replay: Option<replay::Player>,
|
||||
/// The world drawn beyond the render distance (replays and cameras see far off).
|
||||
far: bool,
|
||||
/// The world whose state is loaded (`game.world-id`), once one is.
|
||||
world: Option<Option<String>>,
|
||||
/// The systems' and objects' health as last saved, to save again when it changes.
|
||||
health_mark: u64,
|
||||
/// The menu's page while it is open.
|
||||
menu: Option<Page>,
|
||||
/// Where the player is, for the sounds' delay and far variants.
|
||||
@@ -362,14 +366,18 @@ impl AirDef {
|
||||
for (kind, roles) in &self.sim.roles {
|
||||
text += &format!("roles {} {}\n", kind.key(), roles);
|
||||
}
|
||||
if let Err(e) = storage::set("state", text.as_bytes()) {
|
||||
if let Err(e) = storage::set(&self.state_key(), text.as_bytes()) {
|
||||
horizon_mod_sdk::warn(&format!("could not save: {e}"));
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
fn load(&mut self) {
|
||||
let Some(bytes) = storage::get("state") else { return };
|
||||
self.load_from(&self.state_key());
|
||||
}
|
||||
|
||||
fn load_from(&mut self, key: &str) {
|
||||
let Some(bytes) = storage::get(key) else { return };
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
for line in text.lines() {
|
||||
let w: Vec<&str> = line.split_whitespace().collect();
|
||||
@@ -417,6 +425,42 @@ impl AirDef {
|
||||
}
|
||||
|
||||
impl AirDef {
|
||||
/// Where the open world's state is kept: a key of its own per world.
|
||||
fn state_key(&self) -> String {
|
||||
match self.world.as_ref().and_then(|w| w.as_ref()) {
|
||||
Some(w) => {
|
||||
// FNV-1a: storage keys are short, lowercase and plain.
|
||||
let h = w.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x100_0000_01b3));
|
||||
format!("state-{h:016x}")
|
||||
}
|
||||
None => "state".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Another world opened: the last one's state saved, a clean simulation with this one's loaded.
|
||||
fn open_world(&mut self, world: Option<String>) {
|
||||
if self.world.is_some() && self.dirty {
|
||||
self.save();
|
||||
}
|
||||
if self.replay.is_some() {
|
||||
self.stop_replay();
|
||||
}
|
||||
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);
|
||||
self.sim.shader = shader;
|
||||
self.ground = Ground::default();
|
||||
self.monitor = None;
|
||||
self.grid = false;
|
||||
self.grid_ids.clear();
|
||||
self.far = true;
|
||||
self.world = Some(world);
|
||||
self.load();
|
||||
self.apply_far();
|
||||
self.dirty = false;
|
||||
self.health_mark = 0;
|
||||
}
|
||||
|
||||
/// Distant terrain (48 chunks, coarse) while `far` is on: the raid's whole area has land,
|
||||
/// in the world and in the cameras.
|
||||
fn apply_far(&self) {
|
||||
@@ -640,7 +684,7 @@ impl AirDef {
|
||||
}
|
||||
}
|
||||
|
||||
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
|
||||
const HELP: &str = "/airdef menu | monitor | grid | replay [map|full|reel|stop] | raid | repair | import | target here | give | deploy <комплекс> | wave <тип|mix> [кол-во] [дальность] | clear | targets clear | damage on|off | shader on|off | hold on|off | roles [комплекс классы] | stats";
|
||||
|
||||
impl Mod for AirDef {
|
||||
fn init() -> Result<(Self, Registration), String> {
|
||||
@@ -657,6 +701,8 @@ impl Mod for AirDef {
|
||||
wave: wave::WaveConfig::default(),
|
||||
replay: None,
|
||||
far: true,
|
||||
world: None,
|
||||
health_mark: 0,
|
||||
menu: None,
|
||||
listener: DVec3::ZERO,
|
||||
pending: Vec::new(),
|
||||
@@ -673,7 +719,6 @@ impl Mod for AirDef {
|
||||
// Not Enter: it opens the chat.
|
||||
let _ = input::register_key("rp_pause", "Повтор: пауза", "key.keyboard.space");
|
||||
let _ = input::register_key("rp_exit", "Повтор: выход", "key.keyboard.backspace");
|
||||
me.load();
|
||||
input::claim_items(&ITEMS.map(String::from));
|
||||
render::set_post(include_str!("../shaders/effects.wgsl"));
|
||||
me.sim.shader = true;
|
||||
@@ -683,6 +728,11 @@ impl Mod for AirDef {
|
||||
|
||||
fn tick(&mut self, _tick: u64) {
|
||||
let Some(p) = player() else { return };
|
||||
// Each world keeps its own systems, objects and settings: loaded when it opens.
|
||||
let world = game::world_id();
|
||||
if self.world.as_ref() != Some(&world) {
|
||||
self.open_world(world);
|
||||
}
|
||||
for event in input::poll() {
|
||||
match event {
|
||||
Event::UseStart(u) => self.use_item(&u.item.id),
|
||||
@@ -779,6 +829,18 @@ impl Mod for AirDef {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Damage is saved too: a destroyed system stays destroyed.
|
||||
let mark = self
|
||||
.sim
|
||||
.sams
|
||||
.iter()
|
||||
.map(|s| (s.health * 1000.0) as u64)
|
||||
.chain(self.sim.target_health.iter().map(|h| (h * 10.0) as u64))
|
||||
.fold(self.sim.sams.len() as u64, |a, h| a.wrapping_mul(31).wrapping_add(h));
|
||||
if mark != self.health_mark {
|
||||
self.health_mark = mark;
|
||||
self.dirty = true;
|
||||
}
|
||||
if self.dirty {
|
||||
self.save();
|
||||
}
|
||||
@@ -988,6 +1050,13 @@ impl Mod for AirDef {
|
||||
self.launch_wave(&mut out)?;
|
||||
None
|
||||
}
|
||||
["import"] => {
|
||||
// The state the mod kept before worlds had their own: brought into this world.
|
||||
let before = self.sim.sams.len();
|
||||
self.load_from("state");
|
||||
self.dirty = true;
|
||||
Some(format!("Перенесено комплексов из старой версии: {}", self.sim.sams.len() - before))
|
||||
}
|
||||
["repair"] => {
|
||||
let n = self.sim.repair();
|
||||
self.dirty = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:37:45 +0300
|
||||
Subject: [PATCH 1/8] Mod API (AirDef fork): world models, client particles,
|
||||
Subject: [PATCH 1/9] Mod API (AirDef fork): world models, client particles,
|
||||
sounds, ground heights
|
||||
|
||||
render.set-models draws block models with an interpolated pose per id;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:39:45 +0300
|
||||
Subject: [PATCH 2/8] cargo-horizon-mod: report particles and sounds
|
||||
Subject: [PATCH 2/9] cargo-horizon-mod: report particles and sounds
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 13:33:10 +0300
|
||||
Subject: [PATCH 3/8] Mod API (AirDef fork): full-screen post pass for mods
|
||||
Subject: [PATCH 3/9] Mod API (AirDef fork): full-screen post pass for mods
|
||||
|
||||
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
|
||||
time, 256 vec4 of params, the world's colour and depth) and runs it after
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:09:30 +0300
|
||||
Subject: [PATCH 4/8] Mod models: draw far models as scaled impostors past the
|
||||
Subject: [PATCH 4/9] Mod models: draw far models as scaled impostors past the
|
||||
far plane, cull at 4096
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:31:00 +0300
|
||||
Subject: [PATCH 5/8] Mod cameras: render.set-views draws the world from other
|
||||
Subject: [PATCH 5/9] Mod cameras: render.set-views draws the world from other
|
||||
points into pictures that HUD view shapes show (API level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:08:21 +0300
|
||||
Subject: [PATCH 6/8] Mods: more fuel per tick and per event (AirDef plans
|
||||
Subject: [PATCH 6/9] Mods: more fuel per tick and per event (AirDef plans
|
||||
raids)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:25:29 +0300
|
||||
Subject: [PATCH 7/8] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
Subject: [PATCH 7/9] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:15:11 +0300
|
||||
Subject: [PATCH 8/8] Mod cameras: distant terrain tiles, fog measured from the
|
||||
Subject: [PATCH 8/9] Mod cameras: distant terrain tiles, fog measured from the
|
||||
camera, no impostors while one is shown
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:25:45 +0300
|
||||
Subject: [PATCH 9/9] Mods: game.world-id, so mods keep state per world (API
|
||||
level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/game.rs | 1 +
|
||||
crates/horizon-client/src/game/mods.rs | 3 +++
|
||||
crates/horizon-client/src/game/remote.rs | 4 ++++
|
||||
crates/horizon-mod-sdk/wit/horizon.wit | 4 ++++
|
||||
crates/horizon-mods/src/api_level.rs | 3 ++-
|
||||
crates/horizon-mods/src/runtime.rs | 5 +++++
|
||||
6 files changed, 19 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs
|
||||
index e5ffb30c4..c6b642345 100644
|
||||
--- a/crates/horizon-client/src/game.rs
|
||||
+++ b/crates/horizon-client/src/game.rs
|
||||
@@ -8918,6 +8918,7 @@ impl Game<'_> {
|
||||
create: Option<&horizon_client::title::WorldOptions>,
|
||||
) -> Result<()> {
|
||||
world_loading::world_data_used();
|
||||
+ self.mods.world_id = Some(format!("world:{folder}"));
|
||||
if self.defer_world_entry(PendingWorldEntry::Local {
|
||||
folder: folder.to_owned(),
|
||||
name: name.map(str::to_owned),
|
||||
diff --git a/crates/horizon-client/src/game/mods.rs b/crates/horizon-client/src/game/mods.rs
|
||||
index 3eef96aa3..ebdd18a24 100644
|
||||
--- a/crates/horizon-client/src/game/mods.rs
|
||||
+++ b/crates/horizon-client/src/game/mods.rs
|
||||
@@ -38,6 +38,8 @@ use {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ModsState {
|
||||
+ /// The open world, for `game.world-id` (AirDef fork): its folder, or the server's address.
|
||||
+ pub(super) world_id: Option<String>,
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) mods: Mods,
|
||||
/// Capability `input`: a claimed item's use while the button is down.
|
||||
@@ -482,6 +484,7 @@ impl Game<'_> {
|
||||
known_recipes,
|
||||
placing: self.mod_recipe_screen(),
|
||||
creative: self.creative,
|
||||
+ world: self.player.is_some().then(|| self.mods.world_id.clone()).flatten(),
|
||||
}
|
||||
}
|
||||
/// The first block along the player's view within
|
||||
diff --git a/crates/horizon-client/src/game/remote.rs b/crates/horizon-client/src/game/remote.rs
|
||||
index 22b7c13df..89df0616b 100644
|
||||
--- a/crates/horizon-client/src/game/remote.rs
|
||||
+++ b/crates/horizon-client/src/game/remote.rs
|
||||
@@ -342,6 +342,10 @@ impl Game<'_> {
|
||||
cookies: Option<horizon_client::net::Cookies>,
|
||||
) -> Result<()> {
|
||||
self.perf.world_opening();
|
||||
+ // A server's own world; joining the world this window hosts keeps its folder.
|
||||
+ if local.is_none() {
|
||||
+ self.mods.world_id = Some(format!("server:{address}"));
|
||||
+ }
|
||||
let root = self.options.resources.clone();
|
||||
let builtin = Builtin::find(&root)
|
||||
.and_then(|p| Builtin::load(&p).ok())
|
||||
diff --git a/crates/horizon-mod-sdk/wit/horizon.wit b/crates/horizon-mod-sdk/wit/horizon.wit
|
||||
index e30f3a30e..b6758d9fb 100644
|
||||
--- a/crates/horizon-mod-sdk/wit/horizon.wit
|
||||
+++ b/crates/horizon-mod-sdk/wit/horizon.wit
|
||||
@@ -47,6 +47,10 @@ interface game {
|
||||
get-player: func() -> option<player>;
|
||||
/// None on the title screen.
|
||||
get-clock: func() -> option<clock>;
|
||||
+ /// Which world is open (AirDef fork): `world:<folder>` for a saved
|
||||
+ /// world, `server:<address>` on a server; none on the title screen.
|
||||
+ /// Mods keep their state apart by it.
|
||||
+ world-id: func() -> option<string>;
|
||||
/// A block along the player's view.
|
||||
record hit {
|
||||
/// The block hit.
|
||||
diff --git a/crates/horizon-mods/src/api_level.rs b/crates/horizon-mods/src/api_level.rs
|
||||
index 9d15851b4..cf2993186 100644
|
||||
--- a/crates/horizon-mods/src/api_level.rs
|
||||
+++ b/crates/horizon-mods/src/api_level.rs
|
||||
@@ -13,7 +13,7 @@ pub const LEVEL: u32 = 9;
|
||||
/// level. Level 0 is the contract of Horizon 0.2.67 to 0.2.69. A new
|
||||
/// function takes the next level and raises [`LEVEL`] (the `api_reference`
|
||||
/// test fails until it is listed here).
|
||||
-pub const FUNCTIONS: [(&str, u32); 55] = [
|
||||
+pub const FUNCTIONS: [(&str, u32); 56] = [
|
||||
("log.write", 0),
|
||||
("game.get-player", 0),
|
||||
("game.get-clock", 0),
|
||||
@@ -70,6 +70,7 @@ pub const FUNCTIONS: [(&str, u32); 55] = [
|
||||
("render.clear-post", 8),
|
||||
("render.set-params", 8),
|
||||
("render.set-views", 9),
|
||||
+ ("game.world-id", 9),
|
||||
];
|
||||
|
||||
/// What the game's program carries so the launcher can read its level
|
||||
diff --git a/crates/horizon-mods/src/runtime.rs b/crates/horizon-mods/src/runtime.rs
|
||||
index 1389407be..abed5b31f 100644
|
||||
--- a/crates/horizon-mods/src/runtime.rs
|
||||
+++ b/crates/horizon-mods/src/runtime.rs
|
||||
@@ -168,6 +168,8 @@ pub struct Snapshot {
|
||||
pub placing: bool,
|
||||
/// The player is in creative mode (`recipes.give`).
|
||||
pub creative: bool,
|
||||
+ /// The open world (`game.world-id`, AirDef fork).
|
||||
+ pub world: Option<String>,
|
||||
}
|
||||
|
||||
/// Capability `recipes`'s tables: `items` in the registry's order, which
|
||||
@@ -1475,6 +1477,9 @@ impl game::Host for State {
|
||||
fn get_clock(&mut self) -> Option<Clock> {
|
||||
self.snapshot.clock
|
||||
}
|
||||
+ fn world_id(&mut self) -> Option<String> {
|
||||
+ self.snapshot.world.clone()
|
||||
+ }
|
||||
fn raycast(&mut self, distance: f32) -> Option<Hit> {
|
||||
let (at, hit) = self.snapshot.view_hit.as_ref()?;
|
||||
// NaN compares false: no hit.
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
Reference in New Issue
Block a user