Sign in Sign up
kretrod/lodestone Public
Branches
master
455 lines (429 loc) · 17.2 KB Raw
//! Three things 26.2 exposes that nothing has used yet.
//!
//! * **Gizmos.** The game ships a public 3D debug-drawing API — cuboids, lines,
//!   billboard text — collected by `LevelExtractor` and drawn by the game's own
//!   renderer. Drawing through it means real depth, real interpolation, and a
//!   `setAlwaysOnTop()` flag that is see-through-walls for free. Everything
//!   below replaces hand-rolled screen projection with the renderer the game
//!   already runs.
//! * **Chunk rate.** The client measures how fast it absorbs chunks and tells
//!   the server, which throttles to that number. The measurement is one double.
//! * **Waypoints.** The locator bar's data is a map of player id to position,
//!   handed over by the server for players you cannot see.

use jni_sys::{jclass, jfieldID, jmethodID, jobject, jvalue};

use crate::jni::Jni;
use crate::mc::Mc;

pub struct Extras {
    // --- gizmos ---
    f_level_extractor: Option<jfieldID>,
    f_main_gizmos: Option<jfieldID>,
    gizmos_class: Option<jclass>,
    m_with_collector: Option<jmethodID>,
    m_cuboid: Option<jmethodID>,
    m_line: Option<jmethodID>,
    m_close: Option<jmethodID>,
    style_class: Option<jclass>,
    m_style_stroke: Option<jmethodID>,
    m_always_on_top: Option<jmethodID>,
    m_persist: Option<jmethodID>,
    aabb_class: Option<jclass>,
    m_aabb_init: Option<jmethodID>,
    vec3_class: Option<jclass>,
    m_vec3_init: Option<jmethodID>,

    // --- chunk rate ---
    f_connection: Option<jfieldID>,
    f_batch_calc: Option<jfieldID>,
    f_nanos_per_chunk: Option<jfieldID>,

    // --- waypoints ---
    f_waypoint_manager: Option<jfieldID>,
    f_waypoints: Option<jfieldID>,
    m_map_values: Option<jmethodID>,
    m_iterator: Option<jmethodID>,
    m_has_next: Option<jmethodID>,
    m_next: Option<jmethodID>,
    vec3i_waypoint_class: Option<jclass>,
    chunk_waypoint_class: Option<jclass>,
    f_wp_vector: Option<jfieldID>,
    f_wp_chunk: Option<jfieldID>,
    m_vec3i_x: Option<jmethodID>,
    m_vec3i_y: Option<jmethodID>,
    m_vec3i_z: Option<jmethodID>,
}

/// A player the server told us about without showing us.
#[derive(Clone, Copy)]
pub struct Blip {
    pub x: f64,
    pub y: f64,
    pub z: f64,
    /// Chunk-resolution blips are a 16-block box, not a point.
    pub coarse: bool,
}

impl Extras {
    pub fn resolve(j: &Jni, mc: &Mc, missing: &mut Vec<String>) -> Extras {
        let class = |name: &str| -> Option<jclass> {
            let local = j.find_class(name)?;
            j.global(local).map(|g| g as jclass)
        };
        let note = |missing: &mut Vec<String>, what: &str, ok: bool| {
            if !ok {
                missing.push(what.to_string());
            }
        };

        let extractor = class("net/minecraft/client/renderer/extract/LevelExtractor");
        let gizmos_class = class("net/minecraft/gizmos/Gizmos");
        let collector = class("net/minecraft/gizmos/GizmoCollector");
        let temp = class("net/minecraft/gizmos/Gizmos$TemporaryCollection");
        let style_class = class("net/minecraft/gizmos/GizmoStyle");
        let props = class("net/minecraft/gizmos/GizmoProperties");
        let aabb_class = class("net/minecraft/world/phys/AABB");
        let vec3_class = class("net/minecraft/world/phys/Vec3");
        let listener = class("net/minecraft/client/multiplayer/ClientPacketListener");
        let calc = class("net/minecraft/client/multiplayer/ChunkBatchSizeCalculator");
        let manager = class("net/minecraft/client/waypoints/ClientWaypointManager");
        let map = class("java/util/Map");
        let collection = class("java/util/Collection");
        let iterator = class("java/util/Iterator");
        let vec3i = class("net/minecraft/core/Vec3i");

        let out = Extras {
            f_level_extractor: j.field(
                mc.minecraft,
                "levelExtractor",
                "Lnet/minecraft/client/renderer/extract/LevelExtractor;",
            ),
            f_main_gizmos: extractor.and_then(|c| {
                j.field(c, "mainThreadGizmos", "Lnet/minecraft/gizmos/SimpleGizmoCollector;")
            }),
            m_with_collector: gizmos_class.and_then(|c| {
                j.static_method(
                    c,
                    "withCollector",
                    "(Lnet/minecraft/gizmos/GizmoCollector;)Lnet/minecraft/gizmos/Gizmos$TemporaryCollection;",
                )
            }),
            m_cuboid: gizmos_class.and_then(|c| {
                j.static_method(
                    c,
                    "cuboid",
                    "(Lnet/minecraft/world/phys/AABB;Lnet/minecraft/gizmos/GizmoStyle;)Lnet/minecraft/gizmos/GizmoProperties;",
                )
            }),
            m_line: gizmos_class.and_then(|c| {
                j.static_method(
                    c,
                    "line",
                    "(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/phys/Vec3;IF)Lnet/minecraft/gizmos/GizmoProperties;",
                )
            }),
            m_close: temp.and_then(|c| j.method(c, "close", "()V")),
            m_style_stroke: style_class.and_then(|c| {
                j.static_method(c, "stroke", "(I)Lnet/minecraft/gizmos/GizmoStyle;")
            }),
            m_always_on_top: props.and_then(|c| {
                j.method(c, "setAlwaysOnTop", "()Lnet/minecraft/gizmos/GizmoProperties;")
            }),
            m_persist: props.and_then(|c| {
                j.method(c, "persistForMillis", "(I)Lnet/minecraft/gizmos/GizmoProperties;")
            }),
            m_aabb_init: aabb_class.and_then(|c| j.method(c, "<init>", "(DDDDDD)V")),
            m_vec3_init: vec3_class.and_then(|c| j.method(c, "<init>", "(DDD)V")),
            aabb_class,
            vec3_class,
            gizmos_class,
            style_class,

            f_connection: j.field(
                mc.local_player,
                "connection",
                "Lnet/minecraft/client/multiplayer/ClientPacketListener;",
            ),
            f_batch_calc: listener.and_then(|c| {
                j.field(
                    c,
                    "chunkBatchSizeCalculator",
                    "Lnet/minecraft/client/multiplayer/ChunkBatchSizeCalculator;",
                )
            }),
            f_nanos_per_chunk: calc.and_then(|c| j.field(c, "aggregatedNanosPerChunk", "D")),

            f_waypoint_manager: listener.and_then(|c| {
                j.field(
                    c,
                    "waypointManager",
                    "Lnet/minecraft/client/waypoints/ClientWaypointManager;",
                )
            }),
            f_waypoints: manager.and_then(|c| j.field(c, "waypoints", "Ljava/util/Map;")),
            m_map_values: map.and_then(|c| j.method(c, "values", "()Ljava/util/Collection;")),
            m_iterator: collection
                .and_then(|c| j.method(c, "iterator", "()Ljava/util/Iterator;")),
            m_has_next: iterator.and_then(|c| j.method(c, "hasNext", "()Z")),
            m_next: iterator.and_then(|c| j.method(c, "next", "()Ljava/lang/Object;")),
            vec3i_waypoint_class: class("net/minecraft/world/waypoints/TrackedWaypoint$Vec3iWaypoint"),
            chunk_waypoint_class: class("net/minecraft/world/waypoints/TrackedWaypoint$ChunkWaypoint"),
            f_wp_vector: class("net/minecraft/world/waypoints/TrackedWaypoint$Vec3iWaypoint")
                .and_then(|c| j.field(c, "vector", "Lnet/minecraft/core/Vec3i;")),
            f_wp_chunk: class("net/minecraft/world/waypoints/TrackedWaypoint$ChunkWaypoint")
                .and_then(|c| j.field(c, "chunkPos", "Lnet/minecraft/world/level/ChunkPos;")),
            m_vec3i_x: vec3i.and_then(|c| j.method(c, "getX", "()I")),
            m_vec3i_y: vec3i.and_then(|c| j.method(c, "getY", "()I")),
            m_vec3i_z: vec3i.and_then(|c| j.method(c, "getZ", "()I")),
        };

        note(missing, "Gizmos.cuboid", out.m_cuboid.is_some());
        note(missing, "LevelExtractor.mainThreadGizmos", out.f_main_gizmos.is_some());
        note(
            missing,
            "ChunkBatchSizeCalculator.aggregatedNanosPerChunk",
            out.f_nanos_per_chunk.is_some(),
        );
        note(missing, "ClientWaypointManager.waypoints", out.f_waypoints.is_some());
        out
    }

    // ---- chunk rate ------------------------------------------------------

    /// What the client will tell the server it can absorb, in chunks per tick.
    ///
    /// `getDesiredChunksPerTick` is literally `7_000_000 / aggregatedNanosPerChunk`,
    /// and that number is the client's own measurement of how long a chunk took
    /// to process. Writing it is how you change the answer without touching the
    /// packet: claim each chunk costs less, and the server sends more of them.
    pub fn set_chunk_rate(&self, j: &Jni, player: jobject, chunks_per_tick: f32) {
        let (Some(fc), Some(fb), Some(fn_)) =
            (self.f_connection, self.f_batch_calc, self.f_nanos_per_chunk)
        else {
            return;
        };
        let Some(connection) = j.obj_field(player, fc) else {
            return;
        };
        let Some(calc) = j.obj_field(connection, fb) else {
            return;
        };
        let nanos = 7_000_000.0 / (chunks_per_tick.max(0.1) as f64);
        j.set_double(calc, fn_, nanos);
    }

    pub fn chunk_rate(&self, j: &Jni, player: jobject) -> Option<f32> {
        let connection = j.obj_field(player, self.f_connection?)?;
        let calc = j.obj_field(connection, self.f_batch_calc?)?;
        let nanos = j.double_field(calc, self.f_nanos_per_chunk?)?;
        Some((7_000_000.0 / nanos.max(1.0)) as f32)
    }

    // ---- waypoints -------------------------------------------------------

    /// Players the server is tracking for the locator bar. Beyond render
    /// distance, through terrain, whether or not the entity is loaded.
    pub fn waypoints(&self, j: &Jni, player: jobject, out: &mut Vec<Blip>) {
        out.clear();
        let (Some(fc), Some(fm), Some(fw)) =
            (self.f_connection, self.f_waypoint_manager, self.f_waypoints)
        else {
            return;
        };
        let (Some(values), Some(iter), Some(has_next), Some(next)) =
            (self.m_map_values, self.m_iterator, self.m_has_next, self.m_next)
        else {
            return;
        };
        let Some(connection) = j.obj_field(player, fc) else {
            return;
        };
        let Some(manager) = j.obj_field(connection, fm) else {
            return;
        };
        let Some(map) = j.obj_field(manager, fw) else {
            return;
        };
        let Some(collection) = j.call_obj(map, values, &[]) else {
            return;
        };
        let it = j.call_obj(collection, iter, &[]);
        j.delete_local(collection);
        let Some(it) = it else { return };

        let mut guard = 0;
        while guard < 512 {
            guard += 1;
            if !j.call_bool(it, has_next, &[]).unwrap_or(false) {
                break;
            }
            let Some(wp) = j.call_obj(it, next, &[]) else {
                break;
            };
            if let Some(blip) = self.read_waypoint(j, wp) {
                out.push(blip);
            }
            j.delete_local(wp);
        }
        j.delete_local(it);
    }

    fn read_waypoint(&self, j: &Jni, wp: jobject) -> Option<Blip> {
        // Exact position when the server chose to send one.
        if let (Some(cls), Some(f)) = (self.vec3i_waypoint_class, self.f_wp_vector) {
            if j.is_instance(wp, cls) {
                let v = j.obj_field(wp, f)?;
                let x = j.call_int(v, self.m_vec3i_x?, &[])? as f64;
                let y = j.call_int(v, self.m_vec3i_y?, &[])? as f64;
                let z = j.call_int(v, self.m_vec3i_z?, &[])? as f64;
                j.delete_local(v);
                return Some(Blip { x: x + 0.5, y, z: z + 0.5, coarse: false });
            }
        }
        // Otherwise the server may still have given a chunk, which is a
        // sixteen-block box rather than a point — worth drawing as one.
        if let (Some(cls), Some(f)) = (self.chunk_waypoint_class, self.f_wp_chunk) {
            if j.is_instance(wp, cls) {
                let c = j.obj_field(wp, f)?;
                // ChunkPos exposes x and z as plain ints.
                let x = j.int_field(c, self.chunk_field(j, "x")?)? as f64;
                let z = j.int_field(c, self.chunk_field(j, "z")?)? as f64;
                j.delete_local(c);
                return Some(Blip {
                    x: x * 16.0 + 8.0,
                    y: f64::NAN,
                    z: z * 16.0 + 8.0,
                    coarse: true,
                });
            }
        }
        None
    }

    fn chunk_field(&self, j: &Jni, name: &str) -> Option<jfieldID> {
        let cls = j.find_class("net/minecraft/world/level/ChunkPos")?;
        j.field(cls, name, "I")
    }

    // ---- gizmos ----------------------------------------------------------

    /// Borrow the extractor's collector for the duration of `draw`.
    ///
    /// `Gizmos`' static helpers post to a thread-local collector that the game
    /// only installs while it is extracting a frame. Installing it ourselves —
    /// on the same thread, and putting it back afterwards — lets the same
    /// helpers be used from here.
    pub fn with_gizmos<R>(
        &self,
        j: &Jni,
        instance: jobject,
        draw: impl FnOnce(&GizmoPen) -> R,
    ) -> Option<R> {
        let extractor = j.obj_field(instance, self.f_level_extractor?)?;
        let collector = j.obj_field(extractor, self.f_main_gizmos?)?;
        let temp = j.call_static_obj(
            self.gizmos_class?,
            self.m_with_collector?,
            &[jvalue { l: collector }],
        )?;
        let pen = GizmoPen { extras: self, j };
        let out = draw(&pen);
        if let Some(close) = self.m_close {
            j.call_void(temp, close, &[]);
        }
        j.delete_local(temp);
        Some(out)
    }
}

/// Drawing handle, valid only while the collector is installed.
pub struct GizmoPen<'a> {
    extras: &'a Extras,
    j: &'a Jni,
}

impl GizmoPen<'_> {
    /// A box in world space, drawn by the game at the right depth.
    pub fn box_at(
        &self,
        min: (f64, f64, f64),
        max: (f64, f64, f64),
        colour: i32,
        through_walls: bool,
    ) {
        let e = self.extras;
        let j = self.j;
        let (Some(aabb_cls), Some(aabb_init), Some(style_cls), Some(stroke), Some(cuboid)) = (
            e.aabb_class,
            e.m_aabb_init,
            e.style_class,
            e.m_style_stroke,
            e.m_cuboid,
        ) else {
            return;
        };
        let Some(aabb) = j.new_object(
            aabb_cls,
            aabb_init,
            &[
                jvalue { d: min.0 },
                jvalue { d: min.1 },
                jvalue { d: min.2 },
                jvalue { d: max.0 },
                jvalue { d: max.1 },
                jvalue { d: max.2 },
            ],
        ) else {
            return;
        };
        let style = j.call_static_obj(style_cls, stroke, &[jvalue { i: colour }]);
        let Some(style) = style else {
            j.delete_local(aabb);
            return;
        };
        let props = j.call_static_obj(
            e.gizmos_class.unwrap(),
            cuboid,
            &[jvalue { l: aabb }, jvalue { l: style }],
        );
        j.delete_local(aabb);
        j.delete_local(style);
        if let Some(props) = props {
            self.finish(props, through_walls);
        }
    }

    /// A line in world space — a tracer that is actually in the world.
    pub fn line(&self, from: (f64, f64, f64), to: (f64, f64, f64), colour: i32, width: f32) {
        let e = self.extras;
        let j = self.j;
        let (Some(vec_cls), Some(vec_init), Some(line)) =
            (e.vec3_class, e.m_vec3_init, e.m_line)
        else {
            return;
        };
        let make = |p: (f64, f64, f64)| {
            j.new_object(
                vec_cls,
                vec_init,
                &[jvalue { d: p.0 }, jvalue { d: p.1 }, jvalue { d: p.2 }],
            )
        };
        let (Some(a), Some(b)) = (make(from), make(to)) else {
            return;
        };
        let props = j.call_static_obj(
            e.gizmos_class.unwrap(),
            line,
            &[
                jvalue { l: a },
                jvalue { l: b },
                jvalue { i: colour },
                jvalue { f: width },
            ],
        );
        j.delete_local(a);
        j.delete_local(b);
        if let Some(props) = props {
            self.finish(props, true);
        }
    }

    /// Gizmos are drained once per frame, so anything added after this frame's
    /// drain needs to survive until the next one.
    fn finish(&self, props: jobject, through_walls: bool) {
        let e = self.extras;
        let j = self.j;
        let mut current = props;
        if through_walls {
            if let Some(m) = e.m_always_on_top {
                if let Some(next) = j.call_obj(current, m, &[]) {
                    j.delete_local(current);
                    current = next;
                }
            }
        }
        if let Some(m) = e.m_persist {
            if let Some(next) = j.call_obj(current, m, &[jvalue { i: 120 }]) {
                j.delete_local(current);
                current = next;
            }
        }
        j.delete_local(current);
    }
}