ccc3e20902
Smoke: one rule by engine, no heat on trails, no trail on P1-SUN. Searchlights: two lamps a target, off with an empty sky. Halos fade before their reach and their sum is limited. Launch sites load chunks through the server thread, go off-map as a last resort, and reach 4000 blocks. Air defence: ranges x1.5, shots grow with urgency, claims released, /airdef lastchance, long shots on a big radar's tracks. Build: rust-toolchain.toml, airdef.py check, generator manifest and --check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
643 lines
28 KiB
Diff
643 lines
28 KiB
Diff
From 35ffbaa26dd91855c8b93446646ba8c58448d3d0 Mon Sep 17 00:00:00 2001
|
|
From: chexit <chexit@duck.com>
|
|
Date: Fri, 30 Oct 2026 03:59:41 +0300
|
|
Subject: [PATCH 31/40] Mods: render.set-main-view (API level 15) renders the
|
|
screen's world from a mod camera with the full pipeline, so replays get HD
|
|
and the post effects; small mod views get the post pass with their own camera
|
|
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
---
|
|
crates/horizon-client/src/game.rs | 31 +++-
|
|
crates/horizon-client/src/game/mod_models.rs | 9 ++
|
|
crates/horizon-client/src/gfx.rs | 39 ++++-
|
|
crates/horizon-client/src/gfx/mod_post.rs | 152 +++++++++++++++----
|
|
crates/horizon-client/src/gfx/mod_views.rs | 69 ++++++---
|
|
crates/horizon-mod-sdk/wit/horizon.wit | 10 ++
|
|
crates/horizon-mods/src/api_level.rs | 6 +-
|
|
crates/horizon-mods/src/runtime.rs | 12 ++
|
|
docs/api/mods-reference.md | 1 +
|
|
9 files changed, 269 insertions(+), 60 deletions(-)
|
|
|
|
diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs
|
|
index f21b3dcdd..0e779aaa0 100644
|
|
--- a/crates/horizon-client/src/game.rs
|
|
+++ b/crates/horizon-client/src/game.rs
|
|
@@ -5950,7 +5950,7 @@ impl Game<'_> {
|
|
// the sleep overlay, debug screen and open screens still draw.
|
|
let spectator = self.spectator();
|
|
let (wi, hi) = (w as i32, h as i32);
|
|
- let show_hud = !self.ui.hide_gui;
|
|
+ let show_hud = !self.ui.hide_gui && self.mod_main_view(self.clock.fraction()).is_none();
|
|
// max(Hud.leftHeight, Hud.rightHeight) after the bars: the item name and
|
|
// the action bar sit above it.
|
|
let mut bar_height = 39;
|
|
@@ -11572,6 +11572,13 @@ impl Game<'_> {
|
|
let (view_yaw, view_pitch) = self.shaken_view(view_yaw, view_pitch);
|
|
self.audio
|
|
.set_listener([camera.x, camera.y, camera.z], view_yaw, view_pitch);
|
|
+ // AirDef fork: a mod's camera as the screen's own view (replays): the listener stays the
|
|
+ // player's, everything seen is drawn from the camera with the full pipeline.
|
|
+ let main_view = self.mod_main_view(alpha);
|
|
+ let (camera, underwater, view_yaw, view_pitch) = match &main_view {
|
|
+ Some(v) => (v.camera, false, v.yaw, v.pitch),
|
|
+ None => (camera, underwater, view_yaw, view_pitch),
|
|
+ };
|
|
let block = [
|
|
camera.x.floor() as i32,
|
|
camera.y.floor() as i32,
|
|
@@ -11579,6 +11586,7 @@ impl Game<'_> {
|
|
];
|
|
// The camera inside an opaque block sees nothing through it: disable.
|
|
let cave_culling = self.settings.horizon.cave_culling
|
|
+ && main_view.is_none()
|
|
&& !self
|
|
.res
|
|
.facts()
|
|
@@ -11670,7 +11678,11 @@ impl Game<'_> {
|
|
// player who may build. Adventure players only get one with a CAN_BREAK or
|
|
// CAN_PLACE_ON item and spectators with a menu block; Horizon reads neither the
|
|
// components nor opens menus for spectators, so those never show it.
|
|
- let outline = if self.screen == Screen::Playing && !self.ui.hide_gui && self.may_build() {
|
|
+ let outline = if self.screen == Screen::Playing
|
|
+ && !self.ui.hide_gui
|
|
+ && main_view.is_none()
|
|
+ && self.may_build()
|
|
+ {
|
|
self.pick()
|
|
.map(|h| {
|
|
let state = self.world.block(h.pos[0], h.pos[1], h.pos[2]);
|
|
@@ -11733,17 +11745,19 @@ impl Game<'_> {
|
|
};
|
|
let fov_degrees = if title {
|
|
85.0
|
|
+ } else if let Some(v) = &main_view {
|
|
+ v.fov
|
|
} else {
|
|
self.fov_degrees(camera_alpha as f32, camera.to_array())
|
|
};
|
|
self.locator_camera =
|
|
(!title).then_some((camera.to_array(), view_yaw, view_pitch, fov_degrees));
|
|
- let bob = if title {
|
|
+ let bob = if title || main_view.is_some() {
|
|
glam::Mat4::IDENTITY
|
|
} else {
|
|
self.bob_matrix(camera_alpha as f32)
|
|
};
|
|
- let spin = if title {
|
|
+ let spin = if title || main_view.is_some() {
|
|
glam::Mat4::IDENTITY
|
|
} else {
|
|
self.spinning_matrix(camera_alpha as f32)
|
|
@@ -11791,7 +11805,7 @@ impl Game<'_> {
|
|
} else {
|
|
(Vec::new(), None)
|
|
};
|
|
- let hand = if title || self.scoping() {
|
|
+ let hand = if title || self.scoping() || main_view.is_some() {
|
|
Vec::new()
|
|
} else {
|
|
self.hand_geometry(camera_alpha)
|
|
@@ -11829,7 +11843,12 @@ impl Game<'_> {
|
|
hud.patches.append(&mut self.map_patches);
|
|
let entity_spans = std::mem::take(self.entity_spans.get_mut());
|
|
let glowing = std::mem::take(self.glowing_vertices.get_mut());
|
|
- let mod_views = self.mod_views(alpha);
|
|
+ // The main view is the frame itself, not a picture of its own.
|
|
+ let mod_views: Vec<_> = self
|
|
+ .mod_views(alpha)
|
|
+ .into_iter()
|
|
+ .filter(|v| main_view.as_ref().is_none_or(|m| m.id != v.id))
|
|
+ .collect();
|
|
let hd_lights = self.mod_lights(camera, alpha);
|
|
let (hd_smoke, hd_smoke_clock) = self.mod_smoke(camera, alpha);
|
|
let frame = Frame {
|
|
diff --git a/crates/horizon-client/src/game/mod_models.rs b/crates/horizon-client/src/game/mod_models.rs
|
|
index f5e68566a..7a4d742df 100644
|
|
--- a/crates/horizon-client/src/game/mod_models.rs
|
|
+++ b/crates/horizon-client/src/game/mod_models.rs
|
|
@@ -71,6 +71,9 @@ impl Game<'_> {
|
|
pub(super) fn mod_views(&self, _alpha: f64) -> Vec<horizon_client::gfx::ModView> {
|
|
Vec::new()
|
|
}
|
|
+ pub(super) fn mod_main_view(&self, _alpha: f64) -> Option<horizon_client::gfx::ModView> {
|
|
+ None
|
|
+ }
|
|
pub(super) fn mod_hd(&self) -> Option<horizon_client::gfx::HdSettings> {
|
|
None
|
|
}
|
|
@@ -300,6 +303,12 @@ impl Game<'_> {
|
|
.collect()
|
|
}
|
|
|
|
+ /// The camera a mod asked the screen's world from (`render.set-main-view`), for this frame.
|
|
+ pub(super) fn mod_main_view(&self, alpha: f64) -> Option<horizon_client::gfx::ModView> {
|
|
+ let id = self.mods.mods.main_view()?;
|
|
+ self.mod_views(alpha).into_iter().find(|v| v.id == id)
|
|
+ }
|
|
+
|
|
/// Draws every mod model between its last two poses, lit by the world
|
|
/// (or at full brightness), its faces shaded by its own rotation.
|
|
pub(super) fn mod_model_geometry(&self, camera: DVec3, alpha: f64, out: &mut Vec<Vertex>) {
|
|
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
|
index 93a2f7c63..1a4e68b88 100644
|
|
--- a/crates/horizon-client/src/gfx.rs
|
|
+++ b/crates/horizon-client/src/gfx.rs
|
|
@@ -3363,7 +3363,8 @@ impl Renderer {
|
|
view: &t.depth,
|
|
depth_ops: Some(wgpu::Operations {
|
|
load: wgpu::LoadOp::Clear(0.0),
|
|
- store: wgpu::StoreOp::Discard,
|
|
+ // The mod's post pass reads it.
|
|
+ store: wgpu::StoreOp::Store,
|
|
}),
|
|
stencil_ops: None,
|
|
}),
|
|
@@ -3411,6 +3412,33 @@ impl Renderer {
|
|
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
|
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
|
|
}
|
|
+ drop(pass);
|
|
+ // The mod's post pass with this camera: its blasts, flashes and glows in the picture,
|
|
+ // as much as the picture's size is worth. The picture's own camera and world are
|
|
+ // lit by the pass itself (no HD lighting here: it is the world's, not a picture's).
|
|
+ t.post = false;
|
|
+ if let Some(post) = &self.mod_post
|
|
+ && let Some((blasts, lights)) = mod_views::post_budget(t.size)
|
|
+ {
|
|
+ let mut params = self.mod_params.0.clone();
|
|
+ if params.len() >= 4 {
|
|
+ // Mode 0: the pass lights the land and draws the smoke itself.
|
|
+ params[3] = 0.0;
|
|
+ params[1] = params[1].min(blasts as f32);
|
|
+ params[2] = params[2].min(lights as f32);
|
|
+ }
|
|
+ post.write_to(
|
|
+ &self.queue,
|
|
+ &t.post_buffer,
|
|
+ (d.view, d.projection, d.camera),
|
|
+ frame.sky.sun_angle,
|
|
+ t.size,
|
|
+ ¶ms,
|
|
+ self.mod_params.1,
|
|
+ );
|
|
+ post.run_view(&self.device, encoder, &t.post_buffer, (&t.color, &t.depth), &t.fx);
|
|
+ t.post = true;
|
|
+ }
|
|
}
|
|
encoder.copy_buffer_to_buffer(&views.main, 0, &self.globals, 0, size);
|
|
}
|
|
@@ -3703,7 +3731,12 @@ impl Renderer {
|
|
} else {
|
|
self.config.format.remove_srgb_suffix()
|
|
};
|
|
- self.mod_post = Some(mod_post::ModPost::new(&self.device, source, format)?);
|
|
+ self.mod_post = Some(mod_post::ModPost::new(
|
|
+ &self.device,
|
|
+ source,
|
|
+ format,
|
|
+ self.config.format.remove_srgb_suffix(),
|
|
+ )?);
|
|
self.mod_post_source = Some(source.to_owned());
|
|
}
|
|
}
|
|
@@ -4424,6 +4457,8 @@ impl Renderer {
|
|
id: v.id,
|
|
view_proj,
|
|
view,
|
|
+ projection: world_projection(v.fov, aspect, frame.depth_far),
|
|
+ camera: v.camera,
|
|
aspect,
|
|
fov: v.fov,
|
|
visible: seen,
|
|
diff --git a/crates/horizon-client/src/gfx/mod_post.rs b/crates/horizon-client/src/gfx/mod_post.rs
|
|
index d1f54a4bb..3d67ff25b 100644
|
|
--- a/crates/horizon-client/src/gfx/mod_post.rs
|
|
+++ b/crates/horizon-client/src/gfx/mod_post.rs
|
|
@@ -106,6 +106,8 @@ fn horizon_mod_post_fs(in: HorizonModPostOut) -> @location(0) vec4<f32> {
|
|
|
|
pub struct ModPost {
|
|
pipeline: wgpu::RenderPipeline,
|
|
+ /// The same pass onto the mod cameras' pictures (the surface's format).
|
|
+ view_pipeline: wgpu::RenderPipeline,
|
|
layout: wgpu::BindGroupLayout,
|
|
buffer: wgpu::Buffer,
|
|
sampler: wgpu::Sampler,
|
|
@@ -144,6 +146,7 @@ impl ModPost {
|
|
device: &wgpu::Device,
|
|
source: &str,
|
|
format: wgpu::TextureFormat,
|
|
+ view_format: wgpu::TextureFormat,
|
|
) -> Result<Self, String> {
|
|
let module = compile(source)?;
|
|
let texture = |binding, sample_type| wgpu::BindGroupLayoutEntry {
|
|
@@ -189,31 +192,35 @@ impl ModPost {
|
|
label: Some("Mod post"),
|
|
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
|
|
});
|
|
- let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
- label: Some("Mod post"),
|
|
- layout: Some(&pipeline_layout),
|
|
- vertex: wgpu::VertexState {
|
|
- module: &shader,
|
|
- entry_point: Some("horizon_mod_post_vs"),
|
|
- compilation_options: Default::default(),
|
|
- buffers: &[],
|
|
- },
|
|
- primitive: Default::default(),
|
|
- depth_stencil: None,
|
|
- multisample: Default::default(),
|
|
- fragment: Some(wgpu::FragmentState {
|
|
- module: &shader,
|
|
- entry_point: Some("horizon_mod_post_fs"),
|
|
- compilation_options: Default::default(),
|
|
- targets: &[Some(wgpu::ColorTargetState {
|
|
- format,
|
|
- blend: None,
|
|
- write_mask: wgpu::ColorWrites::ALL,
|
|
- })],
|
|
- }),
|
|
- multiview_mask: None,
|
|
- cache: None,
|
|
- });
|
|
+ let make = |format: wgpu::TextureFormat| {
|
|
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
+ label: Some("Mod post"),
|
|
+ layout: Some(&pipeline_layout),
|
|
+ vertex: wgpu::VertexState {
|
|
+ module: &shader,
|
|
+ entry_point: Some("horizon_mod_post_vs"),
|
|
+ compilation_options: Default::default(),
|
|
+ buffers: &[],
|
|
+ },
|
|
+ primitive: Default::default(),
|
|
+ depth_stencil: None,
|
|
+ multisample: Default::default(),
|
|
+ fragment: Some(wgpu::FragmentState {
|
|
+ module: &shader,
|
|
+ entry_point: Some("horizon_mod_post_fs"),
|
|
+ compilation_options: Default::default(),
|
|
+ targets: &[Some(wgpu::ColorTargetState {
|
|
+ format,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ })],
|
|
+ }),
|
|
+ multiview_mask: None,
|
|
+ cache: None,
|
|
+ })
|
|
+ };
|
|
+ let pipeline = make(format);
|
|
+ let view_pipeline = make(view_format);
|
|
if let Some(e) = pollster::block_on(scope.pop()) {
|
|
return Err(e.to_string());
|
|
}
|
|
@@ -231,6 +238,7 @@ impl ModPost {
|
|
});
|
|
Ok(Self {
|
|
pipeline,
|
|
+ view_pipeline,
|
|
layout,
|
|
buffer,
|
|
sampler,
|
|
@@ -253,7 +261,30 @@ impl ModPost {
|
|
let projection = super::world_projection(frame.fov_degrees, aspect, frame.depth_far)
|
|
* frame.bob
|
|
* frame.spin;
|
|
- let a = frame.sky.sun_angle.to_radians();
|
|
+ self.write_to(
|
|
+ queue,
|
|
+ &self.buffer,
|
|
+ (view, projection, frame.camera),
|
|
+ frame.sky.sun_angle,
|
|
+ size,
|
|
+ params,
|
|
+ partial,
|
|
+ );
|
|
+ }
|
|
+
|
|
+ /// The same for another camera (a mod view's picture) into its own `buffer`.
|
|
+ #[allow(clippy::too_many_arguments)]
|
|
+ pub fn write_to(
|
|
+ &self,
|
|
+ queue: &wgpu::Queue,
|
|
+ buffer: &wgpu::Buffer,
|
|
+ (view, projection, camera): (Mat4, Mat4, glam::DVec3),
|
|
+ sun_angle: f32,
|
|
+ size: [u32; 2],
|
|
+ params: &[f32],
|
|
+ partial: f32,
|
|
+ ) {
|
|
+ let a = sun_angle.to_radians();
|
|
let sun = [-a.sin(), a.cos(), 0.0, 0.0];
|
|
let mut p = [[0.0f32; 4]; PARAMS / 4];
|
|
for (i, v) in params.iter().take(PARAMS).enumerate() {
|
|
@@ -264,12 +295,7 @@ impl ModPost {
|
|
view_proj: m(projection * view),
|
|
view_inverse: m(view.inverse()),
|
|
projection_inverse: m(projection.inverse()),
|
|
- camera: [
|
|
- frame.camera.x as f32,
|
|
- frame.camera.y as f32,
|
|
- frame.camera.z as f32,
|
|
- 0.0,
|
|
- ],
|
|
+ camera: [camera.x as f32, camera.y as f32, camera.z as f32, 0.0],
|
|
resolution: [
|
|
size[0] as f32,
|
|
size[1] as f32,
|
|
@@ -285,7 +311,67 @@ impl ModPost {
|
|
sun,
|
|
params: p,
|
|
};
|
|
- queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(&inputs));
|
|
+ queue.write_buffer(buffer, 0, bytemuck::bytes_of(&inputs));
|
|
+ }
|
|
+
|
|
+ /// A buffer for one more camera's `write_to`.
|
|
+ pub fn new_buffer(device: &wgpu::Device) -> wgpu::Buffer {
|
|
+ device.create_buffer(&wgpu::BufferDescriptor {
|
|
+ label: Some("Mod post frame (view)"),
|
|
+ size: size_of::<ModFrame>() as u64,
|
|
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
+ mapped_at_creation: false,
|
|
+ })
|
|
+ }
|
|
+
|
|
+ /// Draws the pass over a mod camera's picture (`world`: colour and depth, `buffer`: what
|
|
+ /// `write_to` filled) onto `target`, in the surface's format.
|
|
+ pub fn run_view(
|
|
+ &self,
|
|
+ device: &wgpu::Device,
|
|
+ encoder: &mut wgpu::CommandEncoder,
|
|
+ buffer: &wgpu::Buffer,
|
|
+ world: (&wgpu::TextureView, &wgpu::TextureView),
|
|
+ target: &wgpu::TextureView,
|
|
+ ) {
|
|
+ let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
+ label: Some("Mod post (view)"),
|
|
+ layout: &self.layout,
|
|
+ entries: &[
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: buffer.as_entire_binding(),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::TextureView(world.0),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 2,
|
|
+ resource: wgpu::BindingResource::TextureView(world.1),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 3,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ },
|
|
+ ],
|
|
+ });
|
|
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
+ label: Some("Mod post (view)"),
|
|
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
+ view: target,
|
|
+ resolve_target: None,
|
|
+ depth_slice: None,
|
|
+ ops: wgpu::Operations {
|
|
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
|
+ store: wgpu::StoreOp::Store,
|
|
+ },
|
|
+ })],
|
|
+ ..Default::default()
|
|
+ });
|
|
+ pass.set_pipeline(&self.view_pipeline);
|
|
+ pass.set_bind_group(0, &group, &[]);
|
|
+ pass.draw(0..3, 0..1);
|
|
}
|
|
|
|
/// Draws the pass over the world target `world` (colour, depth) onto `screen`.
|
|
diff --git a/crates/horizon-client/src/gfx/mod_views.rs b/crates/horizon-client/src/gfx/mod_views.rs
|
|
index aa2c5b971..bb65480ea 100644
|
|
--- a/crates/horizon-client/src/gfx/mod_views.rs
|
|
+++ b/crates/horizon-client/src/gfx/mod_views.rs
|
|
@@ -26,15 +26,34 @@ pub(super) const PER_FRAME: usize = 3;
|
|
pub(super) const RADIUS: i32 = 12;
|
|
|
|
pub(super) struct Target {
|
|
- size: [u32; 2],
|
|
+ pub size: [u32; 2],
|
|
pub color: wgpu::TextureView,
|
|
pub depth: wgpu::TextureView,
|
|
pub globals: wgpu::Buffer,
|
|
blit: wgpu::BindGroup,
|
|
+ /// The picture after the mod's post pass (`post`), and what shows it.
|
|
+ pub fx: wgpu::TextureView,
|
|
+ blit_fx: wgpu::BindGroup,
|
|
+ /// The post pass's uniform for this camera.
|
|
+ pub post_buffer: wgpu::Buffer,
|
|
+ /// The picture shown went through the post pass this time.
|
|
+ pub post: bool,
|
|
/// Drawn at least once: the HUD shows nothing before.
|
|
pub drawn: bool,
|
|
}
|
|
|
|
+/// How much of the mod's post pass a picture of `size` pays for: (blasts, lights) the pass
|
|
+/// marches, or None for a picture too small to see them in. Full-screen pictures get all.
|
|
+pub(super) fn post_budget(size: [u32; 2]) -> Option<(usize, usize)> {
|
|
+ let area = size[0] as u64 * size[1] as u64;
|
|
+ match area {
|
|
+ 0..4096 => None,
|
|
+ 4096..150_000 => Some((4, 12)),
|
|
+ 150_000..600_000 => Some((12, 32)),
|
|
+ _ => Some((48, 79)),
|
|
+ }
|
|
+}
|
|
+
|
|
/// A camera drawn this frame.
|
|
pub(super) struct Due {
|
|
pub id: u32,
|
|
@@ -42,6 +61,9 @@ pub(super) struct Due {
|
|
/// world's other geometry are.
|
|
pub view_proj: Mat4,
|
|
pub view: Mat4,
|
|
+ /// Its own projection and place, for the mod's post pass.
|
|
+ pub projection: Mat4,
|
|
+ pub camera: DVec3,
|
|
pub aspect: f32,
|
|
pub fov: f32,
|
|
pub visible: Vec<super::resident::Visible>,
|
|
@@ -239,22 +261,31 @@ impl Views {
|
|
let depth = texture(
|
|
"Mod view depth",
|
|
wgpu::TextureFormat::Depth32Float,
|
|
- wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
+ wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
|
);
|
|
- let blit = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
- label: Some("Mod view picture"),
|
|
- layout: &self.layout,
|
|
- entries: &[
|
|
- wgpu::BindGroupEntry {
|
|
- binding: 0,
|
|
- resource: wgpu::BindingResource::TextureView(&color),
|
|
- },
|
|
- wgpu::BindGroupEntry {
|
|
- binding: 1,
|
|
- resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
- },
|
|
- ],
|
|
- });
|
|
+ let fx = texture(
|
|
+ "Mod view post",
|
|
+ self.format,
|
|
+ wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
|
+ );
|
|
+ let group = |view: &wgpu::TextureView| {
|
|
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
+ label: Some("Mod view picture"),
|
|
+ layout: &self.layout,
|
|
+ entries: &[
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 0,
|
|
+ resource: wgpu::BindingResource::TextureView(view),
|
|
+ },
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 1,
|
|
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
|
|
+ },
|
|
+ ],
|
|
+ })
|
|
+ };
|
|
+ let blit = group(&color);
|
|
+ let blit_fx = group(&fx);
|
|
let globals = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Mod view globals"),
|
|
size: globals_size,
|
|
@@ -267,6 +298,10 @@ impl Views {
|
|
depth,
|
|
globals,
|
|
blit,
|
|
+ fx,
|
|
+ blit_fx,
|
|
+ post_buffer: super::mod_post::ModPost::new_buffer(device),
|
|
+ post: false,
|
|
drawn: false,
|
|
}
|
|
}
|
|
@@ -306,7 +341,7 @@ impl Views {
|
|
pass.set_viewport(x, y, w, h, 0.0, 1.0);
|
|
pass.set_scissor_rect(x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32);
|
|
pass.set_pipeline(&self.pipeline);
|
|
- pass.set_bind_group(0, &t.blit, &[]);
|
|
+ pass.set_bind_group(0, if t.post { &t.blit_fx } else { &t.blit }, &[]);
|
|
let mode = view.mode.min(2);
|
|
pass.draw(0..3, mode..mode + 1);
|
|
}
|
|
diff --git a/crates/horizon-mod-sdk/wit/horizon.wit b/crates/horizon-mod-sdk/wit/horizon.wit
|
|
index fba8f162d..bae0d2134 100644
|
|
--- a/crates/horizon-mod-sdk/wit/horizon.wit
|
|
+++ b/crates/horizon-mod-sdk/wit/horizon.wit
|
|
@@ -664,6 +664,16 @@ interface render {
|
|
/// Replaces the mod's cameras: up to 8. A `view` shape shows one; the
|
|
/// game renders up to three a frame, in turn.
|
|
set-views: func(views: list<camera-view>);
|
|
+ /// Renders the screen's world from one of the `set-views` cameras
|
|
+ /// (AirDef fork), with everything the player's own view has: the HD
|
|
+ /// lighting, sky, clouds, fog, smoke, TAA and the post passes, at the
|
|
+ /// window's resolution. The picture size of that camera is ignored, the
|
|
+ /// player's hand, block outline and the game's HUD are not drawn (the mod's
|
|
+ /// own `set-hud` shapes are); a `view` shape of that camera draws nothing.
|
|
+ /// For a full-screen replay or a director's camera; the small pictures of
|
|
+ /// monitors stay `view` shapes. None (or an unknown id) is the player's
|
|
+ /// own view. The chunks stay those round the player.
|
|
+ set-main-view: func(view: option<u32>);
|
|
/// A light the mod's things give off (AirDef fork): rocket flames,
|
|
/// flares, blasts. Drawn by the HD lighting (`set-lighting`).
|
|
record point-light {
|
|
diff --git a/crates/horizon-mods/src/api_level.rs b/crates/horizon-mods/src/api_level.rs
|
|
index f438eadbd..e578de688 100644
|
|
--- a/crates/horizon-mods/src/api_level.rs
|
|
+++ b/crates/horizon-mods/src/api_level.rs
|
|
@@ -7,13 +7,13 @@
|
|
//! version's level so the launcher can tell before it installs.
|
|
|
|
/// The newest level this Horizon serves.
|
|
-pub const LEVEL: u32 = 14;
|
|
+pub const LEVEL: u32 = 15;
|
|
|
|
/// Every function the game offers mods, as `interface.function`, with its
|
|
/// 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); 68] = [
|
|
+pub const FUNCTIONS: [(&str, u32); 69] = [
|
|
("log.write", 0),
|
|
("game.get-player", 0),
|
|
("game.get-clock", 0),
|
|
@@ -86,6 +86,8 @@ pub const FUNCTIONS: [(&str, u32); 68] = [
|
|
("render.set-time-override", 13),
|
|
// AirDef fork: volumetric smoke and exhaust trails.
|
|
("render.set-smoke", 14),
|
|
+ // AirDef fork: a mod camera as the screen's own view.
|
|
+ ("render.set-main-view", 15),
|
|
];
|
|
|
|
/// 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 495ef6cae..74da34673 100644
|
|
--- a/crates/horizon-mods/src/runtime.rs
|
|
+++ b/crates/horizon-mods/src/runtime.rs
|
|
@@ -506,6 +506,8 @@ struct State {
|
|
post_params: Vec<f32>,
|
|
/// `render.set-views`' cameras (AirDef fork).
|
|
views: Vec<CameraView>,
|
|
+ /// `render.set-main-view`: the camera the screen's world is rendered from.
|
|
+ main_view: Option<u32>,
|
|
/// `render.set-lights` and `render.set-lighting` (AirDef fork).
|
|
lights: Vec<PointLight>,
|
|
lighting: Option<ModLighting>,
|
|
@@ -754,6 +756,7 @@ impl Mods {
|
|
post: None,
|
|
post_params: Vec::new(),
|
|
views: Vec::new(),
|
|
+ main_view: None,
|
|
lights: Vec::new(),
|
|
lighting: None,
|
|
time_override: None,
|
|
@@ -1363,6 +1366,12 @@ impl Mods {
|
|
t.smoke_active = on;
|
|
}
|
|
}
|
|
+ /// The camera the first running mod asked the screen's world from, if any.
|
|
+ pub fn main_view(&self) -> Option<u32> {
|
|
+ self.entries
|
|
+ .iter()
|
|
+ .find_map(|e| e.run.as_ref()?.store.data().main_view)
|
|
+ }
|
|
/// The time of day the first running mod asked to show, if any.
|
|
pub fn time_override(&self) -> Option<u64> {
|
|
self.entries
|
|
@@ -2230,6 +2239,9 @@ impl render::Host for State {
|
|
})
|
|
.collect();
|
|
}
|
|
+ fn set_main_view(&mut self, view: Option<u32>) {
|
|
+ self.main_view = view;
|
|
+ }
|
|
fn set_lights(&mut self, lights: Vec<PointLight>) {
|
|
let finite = |v: f64| v.is_finite() && v.abs() < 30_000_000.0;
|
|
let unit = |v: f32| {
|
|
diff --git a/docs/api/mods-reference.md b/docs/api/mods-reference.md
|
|
index fefc4903f..1bd2c7eae 100644
|
|
--- a/docs/api/mods-reference.md
|
|
+++ b/docs/api/mods-reference.md
|
|
@@ -667,6 +667,7 @@ A piece of smoke or exhaust trail (AirDef fork): a tube from `a` to `b` (equal e
|
|
| `clear-post` | `func()` | Removes this mod's post pass: the world is shown as the game draws it. |
|
|
| `set-params` | `func(params: list<f32>)` | The pass's `frame.params` (up to 1024 floats, as 256 vec4), kept until replaced; call it once a tick (the pass gets the partial tick in `frame.time.y`). |
|
|
| `set-views` | `func(views: list<camera-view>)` | Replaces the mod's cameras: up to 8. A `view` shape shows one; the game renders up to three a frame, in turn. |
|
|
+| `set-main-view` | `func(view: option<u32>)` | Renders the screen's world from one of the `set-views` cameras (AirDef fork), with everything the player's own view has: the HD lighting, sky, clouds, fog, smoke, TAA and the post passes, at the window's resolution. The picture size of that camera is ignored, the player's hand, block outline and the game's HUD are not drawn (the mod's own `set-hud` shapes are); a `view` shape of that camera draws nothing. For a full-screen replay or a director's camera; the small pictures of monitors stay `view` shapes. None (or an unknown id) is the player's own view. The chunks stay those round the player. |
|
|
| `set-lights` | `func(lights: list<point-light>)` | Replaces the mod's lights: up to 512, interpolated per id between the last two calls. |
|
|
| `set-lighting` | `func(lighting: lighting)` | Switches the HD lighting on or off for this mod's world, with its exposure and bloom; off, the world is drawn as the game draws it. |
|
|
| `set-time-override` | `func(day-time: option<u64>)` | Shows the world at this time of day (AirDef fork): the sky, the sun and moon, the sky light and the fog, as `day-time` ticks (24000 a day, as `clock`). Only what the player sees changes: the world's own clock, its saves and its rules do not. None shows the world's own time. |
|
|
--
|
|
2.54.0 (Apple Git-157)
|
|
|