feat(mods): expose computed world field of view

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
[email protected]andCodex committed 2026-11-01 14:49:50 +01:00
1 parent e1ac85c149
commit 5c8fbc0c3e
4 files changed
+80 -1

No files matched your search

+8 -1
View File
@@ -124,7 +124,14 @@ impl Game<'_> {
self.camera_fluid(camera),
false,
);
self.zoomed_fov_degrees(fov)
let vanilla = self.zoomed_fov_degrees(fov);
let mut degrees = vanilla;
self.native_compute_fov(partial, camera, &mut degrees);
if degrees.is_finite() && (1.0..=179.0).contains(&degrees) {
degrees
} else {
vanilla
}
}
/// `Camera.calculateHudFov`: the first-person hand's field of view.
pub(super) fn hud_fov_degrees(&self, partial: f32, camera: [f64; 3]) -> f32 {
@@ -1325,6 +1325,12 @@ impl Game<'_> {
pub(super) fn native_commands(&mut self, commands: &mut Vec<&'static str>) {
let _ = commands;
}
/// World field of view, after normal calculation and zoom and before projection/Nausea.
/// Called for world rendering and near-plane overlays; hand/HUD FOV uses its own path.
/// Only finite values in 1..=179 degrees replace the computed FOV.
pub(super) fn native_compute_fov(&self, partial: f32, camera: [f64; 3], degrees: &mut f32) {
let _ = (partial, camera, degrees);
}
/// The zoom a native mod asks for, applied while playing (the spyglass's
/// own view wins): set `zoom` when it is none yet.
pub(super) fn native_zoom(&self, zoom: &mut Option<NativeZoom>) {
+54
View File
@@ -0,0 +1,54 @@
"""Run the actual FOV method with a fake mod, without opening an OS window."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from test_world_input_capture import method
ROOT = Path(__file__).resolve().parents[1]
class ComputeFovHookTests(unittest.TestCase):
@unittest.skipUnless(shutil.which("rustc"), "requires Rust")
def test_computed_fov_override_and_invalid_fallback(self):
source = (ROOT / "crates/horizon-client/src/game/fov.rs").read_text(encoding="utf-8")
actual = method(source.replace("pub(super) fn", "fn"), "fov_degrees")
fixture = r"""
struct View;
impl View {
fn calculate_fov(&self, partial: f32, setting: i32, scale: f32,
death: Option<i32>, fluid: (), panorama: bool) -> f32 {
assert_eq!((partial, setting, scale, death, fluid, panorama), (0.25, 70, 1.0, None, (), false));
70.0
}
}
struct Settings { fov: f32 }
struct Game { view: View, settings: Settings, override_fov: Option<f32> }
impl Game {
fn fov_effect_scale(&self) -> f32 { 1.0 }
fn death_time(&self) -> Option<i32> { None }
fn camera_fluid(&self, camera: [f64; 3]) { assert_eq!(camera, [1.0, 2.0, 3.0]); }
fn zoomed_fov_degrees(&self, fov: f32) -> f32 { fov / 2.0 }
fn native_compute_fov(&self, partial: f32, camera: [f64; 3], degrees: &mut f32) {
assert_eq!(partial, 0.25); assert_eq!(camera, [1.0, 2.0, 3.0]); assert_eq!(*degrees, 35.0);
if let Some(value) = self.override_fov { *degrees = value; }
}
METHOD
}
#[test]
fn unchanged_narrow_wide_and_invalid() {
for (value, expected) in [(None, 35.0), (Some(15.0), 15.0), (Some(120.0), 120.0),
(Some(1.0), 1.0), (Some(179.0), 179.0), (Some(0.0), 35.0), (Some(180.0), 35.0),
(Some(-1.0), 35.0), (Some(f32::NAN), 35.0), (Some(f32::INFINITY), 35.0)] {
let game = Game { view: View, settings: Settings { fov: 70.0 }, override_fov: value };
assert_eq!(game.fov_degrees(0.25, [1.0, 2.0, 3.0]), expected);
}
}
""".replace("METHOD", actual)
with tempfile.TemporaryDirectory() as folder:
src, exe = Path(folder) / "fov.rs", Path(folder) / "fov.exe"
src.write_text(fixture, encoding="utf-8")
subprocess.run(["rustc", "--edition=2024", "--test", str(src), "-o", str(exe)], check=True)
subprocess.run([str(exe)], check=True)
@@ -0,0 +1,12 @@
{
"hooks": [
{
"name": "compute_fov",
"target": "horizon_client::game::native_hooks::Game<'_>::native_compute_fov",
"neoforge": "ViewportEvent.ComputeFov",
"side": "client"
}
],
"rust_summary": "a world field-of-view hook (`native_compute_fov`).",
"doc_summary": "`native_compute_fov(partial, camera, degrees)` receives the computed world FOV after settings, ordinary FOV effects and zoom, before projection/Nausea. `camera` is its world position and `partial` the interpolation fraction. Mods can narrow or widen the angle; only finite values in 1..=179 degrees are accepted, otherwise the original computed angle is used. It runs for world rendering and near-plane overlays, not just once per frame. Hand/HUD FOV and the title panorama keep their existing paths. Bundles need \"hooks\": @API:compute_fov@."
}