mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
20
Commits
No files matched your search
@@ -186,7 +186,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
|
||||
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; G: `packs.rs` |
|
||||
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
|
||||
| Mods | crate `horizon-mods`; G: `mods.rs`; L: `mod_content.rs`, `mod_list.rs` |
|
||||
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs`, `tweaks.rs`, `options.rs` |
|
||||
| Horizon extras, settings | L: `extras.rs`, `graphics.rs`, `settings.rs`, `minimap.rs`; G: `extras.rs` + `extras/` (each extra's glue: `mouse_tweaks.rs`, `sorting.rs`, `pvp.rs`, `reconnect.rs`), `options.rs` |
|
||||
|
||||
## How `Game` is organised
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -430,7 +430,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -1001,8 +1001,8 @@ pub struct Frame<'a> {
|
||||
pub outline_after_terrain: bool,
|
||||
/// `BlockOutlineRenderState.highContrast`: a thick black outline under a bright one.
|
||||
pub outline_high_contrast: bool,
|
||||
/// The PvP & HUD page's outline and hit colour (`pvp::Frame`; default: vanilla's).
|
||||
pub pvp: crate::pvp::Frame,
|
||||
/// The outline, hurt overlay and glint colours a crate above set (`horizon_hooks::look`).
|
||||
pub look: horizon_hooks::look::Frame,
|
||||
/// `lines` with a colour and pixel width (`gizmos::camera_relative`):
|
||||
/// the fishing line and debug gizmos, depth tested, then gizmos drawn
|
||||
/// over everything.
|
||||
@@ -8506,9 +8506,9 @@ impl Renderer {
|
||||
colored_count: [
|
||||
frame.colored_lights.len().min(crate::dynamic_light::MAX) as u32,
|
||||
// PvP & HUD's hit colour: entity.wgsl's hurt texel, 0 for vanilla's.
|
||||
frame.pvp.hurt_texel,
|
||||
frame.look.hurt_texel,
|
||||
// PvP & HUD's glint colour: entity.wgsl's `tinted_glint`, 0 for vanilla's.
|
||||
frame.pvp.glint,
|
||||
frame.look.glint,
|
||||
0,
|
||||
],
|
||||
emissive_layers: {
|
||||
@@ -9495,7 +9495,7 @@ impl Renderer {
|
||||
}
|
||||
};
|
||||
// A custom outline (PvP & HUD) takes the bright stroke's colour, scales both widths.
|
||||
let (custom, scale) = frame.pvp.outline.map_or((None, 1.0), |(c, s)| (Some(c), s));
|
||||
let (custom, scale) = frame.look.outline.map_or((None, 1.0), |o| (Some(o.0), o.1));
|
||||
if frame.outline_high_contrast {
|
||||
stroke(0xff00_0000, 7.0 * scale, lines);
|
||||
stroke(custom.unwrap_or(0xff57_ffe1), width * scale, lines);
|
||||
|
||||
@@ -524,7 +524,7 @@ impl Scene {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -602,7 +602,7 @@ pub(super) fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -440,7 +440,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -409,7 +409,7 @@ fn frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -2664,7 +2664,7 @@ pub(super) mod initial_history_tests {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! Horizon Settings > PvP & HUD. Every option is off by default, so the game looks like
|
||||
//! vanilla until the player changes one, and none of them shows anything vanilla doesn't
|
||||
//! already tell the player.
|
||||
use horizon_hooks::look::Look;
|
||||
|
||||
/// Colours the PvP options pick from. `Inverted` is vanilla's crosshair blend: the colour
|
||||
/// under it, inverted.
|
||||
@@ -263,15 +264,6 @@ impl ViewWeather {
|
||||
Self::ALL.into_iter().find(|c| c.name() == s)
|
||||
}
|
||||
}
|
||||
/// The day time this player sees with `fixed` set: the same day (the moon's phase follows the
|
||||
/// world's), at that time.
|
||||
pub fn view_day_time(world: i64, fixed: Option<ViewTime>) -> i64 {
|
||||
match fixed {
|
||||
Some(t) => world.div_euclid(24000) * 24000 + t.day_time(),
|
||||
None => world,
|
||||
}
|
||||
}
|
||||
|
||||
/// The crosshair's look. Its defaults draw vanilla's: a 15-pixel cross, one pixel thick,
|
||||
/// inverting what is under it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -528,24 +520,36 @@ impl Hit {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the renderer reads of these options each frame (`FrameInputs::pvp`); the default is
|
||||
/// vanilla's look.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// The block outline's stroke ([`Outline::stroke`]).
|
||||
pub outline: Option<(u32, f32)>,
|
||||
/// The hurt overlay texel ([`Hit::texel`]), 0 for vanilla's.
|
||||
pub hurt_texel: u32,
|
||||
/// The glint's colour ([`Tweaks::glint_texel`]), 0 for vanilla's.
|
||||
pub glint: u32,
|
||||
}
|
||||
impl Pvp {
|
||||
pub fn frame(&self) -> Frame {
|
||||
Frame {
|
||||
outline: self.outline.stroke(),
|
||||
hurt_texel: self.hit.texel(),
|
||||
glint: self.tweaks.glint_texel(),
|
||||
}
|
||||
/// What the engine reads of these options (`horizon_hooks::look`): all it draws differently.
|
||||
/// Sets PvP & HUD's fields of `look`, the others' stay.
|
||||
pub fn apply(&self, look: &mut Look) {
|
||||
let (c, t) = (self.combat, self.tweaks);
|
||||
look.legacy_pose = c.legacy;
|
||||
look.hide_attack_indicator = c.legacy && !c.indicator;
|
||||
look.mute_attack_sounds = c.legacy && c.sounds;
|
||||
look.hide_attack_particles = c.legacy;
|
||||
look.frame.outline = self.outline.stroke();
|
||||
look.frame.hurt_texel = self.hit.texel();
|
||||
look.frame.glint = t.glint_texel();
|
||||
look.shield_size = t.shield_size;
|
||||
look.totem_scale = f32::from(t.totem_size) / 100.0;
|
||||
look.fire_lower = f32::from(t.fire_lower) / 100.0;
|
||||
look.clear_lava = t.clear_lava;
|
||||
look.clear_water = t.clear_water;
|
||||
look.chat.timestamps = self.chat.timestamps;
|
||||
look.chat.compact = self.chat.compact;
|
||||
look.chat.copy = self.chat.copy;
|
||||
look.chat.search = self.chat.search;
|
||||
look.hide_effect_icons = self.hud.effects;
|
||||
look.time_of_day = t.time.map(ViewTime::day_time);
|
||||
look.weather = t.weather.map(ViewWeather::levels);
|
||||
}
|
||||
/// [`Pvp::apply`] on vanilla's look.
|
||||
pub fn look(&self) -> Look {
|
||||
let mut look = Look::default();
|
||||
self.apply(&mut look);
|
||||
look
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,15 +745,6 @@ pub struct ChatOptions {
|
||||
/// Ctrl+F in the chat searches the chat's messages.
|
||||
pub search: bool,
|
||||
}
|
||||
impl ChatOptions {
|
||||
/// What `chat::Chat` lays its lines out with.
|
||||
pub fn qol(&self) -> crate::chat::ChatQol {
|
||||
crate::chat::ChatQol {
|
||||
timestamps: self.timestamps,
|
||||
compact: self.compact,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Combat feel (`docs/pvp.md`): how fights look. Legacy draws them as 1.8.9 did; what the
|
||||
/// game sends to a server never changes, so a vanilla server and its anticheat see 26.3.
|
||||
@@ -766,21 +761,6 @@ pub struct Combat {
|
||||
/// (`horizon_client::legacy_combat`). Never used as a client of a server.
|
||||
pub rules: bool,
|
||||
}
|
||||
impl Combat {
|
||||
/// Whether this client leaves out sound `name` a server sent: under Legacy with its
|
||||
/// sounds, the `entity.player.attack.*` family (strong, weak, crit, sweep, knockback,
|
||||
/// nodamage), which 1.9 brought with the attack cooldown.
|
||||
pub fn mutes_sound(&self, name: &str) -> bool {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
self.legacy && self.sounds && name.starts_with("entity.player.attack.")
|
||||
}
|
||||
/// Whether this client leaves out particles of `ty` a server sent: under Legacy, the
|
||||
/// sweep and the damage hearts, which 1.9 brought (1.8.9's hits show crits only).
|
||||
pub fn hides_particle(&self, ty: crate::particles::ParticleType) -> bool {
|
||||
use crate::particles::ParticleType as T;
|
||||
self.legacy && matches!(ty, T::SweepAttack | T::DamageIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pvp {
|
||||
@@ -1707,18 +1687,6 @@ mod tests {
|
||||
assert_eq!(fresh.extras.pvp.tweaks.fire_lower, PRESET_FIRE_LOWER);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_time_keeps_the_worlds_day() {
|
||||
// Day 9 at 5000: the moon's phase (the day number) stays, the time of day is the chosen.
|
||||
let world = 9 * 24000 + 5000;
|
||||
assert_eq!(view_day_time(world, None), world);
|
||||
assert_eq!(
|
||||
view_day_time(world, Some(ViewTime::Midnight)),
|
||||
9 * 24000 + 18000
|
||||
);
|
||||
assert_eq!(view_day_time(-100, Some(ViewTime::Noon)), -24000 + 6000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_glint_colour_reaches_the_shaders_as_rgb_bytes() {
|
||||
let mut t = Tweaks::default();
|
||||
@@ -1805,31 +1773,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy leaves out what 1.9 added to a hit: its sounds only with the sounds option, the
|
||||
/// sweep and hearts always; the indicator stays when kept. Modern changes nothing.
|
||||
#[test]
|
||||
fn legacy_leaves_out_what_1_9_added_to_a_hit() {
|
||||
use crate::particles::ParticleType as T;
|
||||
let modern = Combat::default();
|
||||
let legacy = Combat {
|
||||
legacy: true,
|
||||
..Combat::default()
|
||||
};
|
||||
let sounds = Combat {
|
||||
sounds: true,
|
||||
..legacy
|
||||
};
|
||||
for name in [
|
||||
"minecraft:entity.player.attack.strong",
|
||||
"entity.player.attack.sweep",
|
||||
"minecraft:entity.player.attack.nodamage",
|
||||
] {
|
||||
assert!(sounds.mutes_sound(name), "{name}");
|
||||
assert!(!legacy.mutes_sound(name), "only with the sounds option");
|
||||
assert!(!modern.mutes_sound(name));
|
||||
}
|
||||
// 1.8.9 had the hurt sound: it stays.
|
||||
assert!(!sounds.mutes_sound("minecraft:entity.player.hurt"));
|
||||
assert!(legacy.hides_particle(T::SweepAttack) && legacy.hides_particle(T::DamageIndicator));
|
||||
assert!(!legacy.hides_particle(T::Crit) && !legacy.hides_particle(T::EnchantedHit));
|
||||
assert!(!modern.hides_particle(T::SweepAttack));
|
||||
fn combat_feel_reaches_the_look() {
|
||||
let mut pvp = Pvp::default();
|
||||
assert_eq!(pvp.look(), Look::default(), "vanilla's look");
|
||||
let strong = "minecraft:entity.player.attack.strong";
|
||||
pvp.combat.legacy = true;
|
||||
let look = pvp.look();
|
||||
assert!(look.legacy_pose && look.hide_attack_particles && look.hide_attack_indicator);
|
||||
assert!(!look.mutes_sound(strong), "only with the sounds option");
|
||||
pvp.combat.sounds = true;
|
||||
pvp.combat.indicator = true;
|
||||
let look = pvp.look();
|
||||
assert!(look.mutes_sound(strong) && !look.hide_attack_indicator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_time_weather_and_sizes_reach_the_look() {
|
||||
let mut pvp = Pvp::default();
|
||||
pvp.tweaks.time = Some(ViewTime::Midnight);
|
||||
pvp.tweaks.weather = Some(ViewWeather::Thunder);
|
||||
pvp.tweaks.totem_size = 50;
|
||||
pvp.tweaks.fire_lower = 40;
|
||||
let look = pvp.look();
|
||||
assert_eq!(look.day_time(9 * 24000 + 5000), 9 * 24000 + 18000);
|
||||
assert_eq!(look.weather, Some((1.0, 1.0)));
|
||||
assert_eq!((look.totem_scale, look.fire_lower), (0.5, 0.4));
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ pub mod item_tooltip;
|
||||
pub mod join_seq;
|
||||
pub mod lan;
|
||||
pub mod landing;
|
||||
pub mod last_server;
|
||||
pub mod legacy_combat;
|
||||
pub mod locator_bar;
|
||||
pub mod lod_net;
|
||||
|
||||
@@ -21,6 +21,11 @@ pub fn main_with(mods: crate::mod_api::Mods) {
|
||||
horizon_client::telemetry::mark_start();
|
||||
// Opt-in telemetry keeps each crash report (docs/telemetry.md), whatever the mode.
|
||||
crash::on_report(horizon_client::telemetry::keep_crash);
|
||||
// Auto-reconnect, a Horizon extra still in the engine crates, runs on the connection hooks as
|
||||
// the crates above do (horizon_client::reconnect; docs/layers.md, plan M5).
|
||||
if cfg!(feature = "horizon-extras") {
|
||||
let _ = horizon_hooks::register(&horizon_client::reconnect::HOOKS);
|
||||
}
|
||||
// Crates above the engine registered their hooks before this (horizon-official's `main`);
|
||||
// none registers after this point.
|
||||
horizon_hooks::engine::seal();
|
||||
|
||||
@@ -189,6 +189,52 @@ impl hud::Canvas for Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws on an open container screen (`horizon_hooks::containers`): `draw` gets a canvas whose
|
||||
/// (0, 0) is the panel's top left corner, at `origin` on the screen. It has no corners: `place`
|
||||
/// answers `None`.
|
||||
pub fn draw_on_panel(
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
origin: (f32, f32),
|
||||
draw: impl FnOnce(&mut dyn hud::Canvas),
|
||||
) {
|
||||
draw(&mut Panel {
|
||||
batch,
|
||||
atlas,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
|
||||
struct Panel<'a> {
|
||||
batch: &'a mut HudBatch,
|
||||
atlas: &'a HudAtlas,
|
||||
origin: (f32, f32),
|
||||
}
|
||||
|
||||
impl hud::Canvas for Panel<'_> {
|
||||
fn size(&self) -> (f32, f32) {
|
||||
(self.batch.width, self.batch.height)
|
||||
}
|
||||
fn tr(&self, key: &str) -> String {
|
||||
crate::lang::get(key)
|
||||
}
|
||||
fn text_width(&self, text: &str) -> f32 {
|
||||
HudBatch::text_width(self.atlas, text)
|
||||
}
|
||||
fn text(&mut self, x: f32, y: f32, text: &str, argb: u32, shadow: bool) {
|
||||
let (x, y) = (self.origin.0 + x, self.origin.1 + y);
|
||||
self.batch
|
||||
.text(self.atlas, text, x, y, argb_color(argb), shadow);
|
||||
}
|
||||
fn fill(&mut self, x: f32, y: f32, w: f32, h: f32, argb: u32) {
|
||||
let (x, y) = (self.origin.0 + x, self.origin.1 + y);
|
||||
self.batch.fill(self.atlas, x, y, w, h, argb_color(argb));
|
||||
}
|
||||
fn place(&mut self, _: hud::Corner, _: f32, _: f32) -> Option<(f32, f32)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -237,6 +237,7 @@ mod llamas;
|
||||
mod loading_screen;
|
||||
mod loading_window;
|
||||
mod locator_bar;
|
||||
mod look;
|
||||
mod loom;
|
||||
mod mace;
|
||||
mod machines;
|
||||
@@ -383,7 +384,6 @@ mod potions;
|
||||
mod powder_snow;
|
||||
mod prediction;
|
||||
mod pumpkin;
|
||||
mod pvp;
|
||||
mod quick_actions;
|
||||
mod raids;
|
||||
mod random_textures;
|
||||
@@ -467,7 +467,6 @@ mod trial_spawner;
|
||||
mod trial_visuals;
|
||||
mod trident;
|
||||
mod tutorial;
|
||||
mod tweaks;
|
||||
mod ui_control;
|
||||
mod use_hand;
|
||||
mod using_item;
|
||||
@@ -2222,7 +2221,7 @@ struct Game<'a> {
|
||||
/// `AbstractContainerScreen`'s quick craft and skip-release state.
|
||||
inv_clicks: horizon_client::container_screen::Clicks,
|
||||
/// The Mouse Tweaks extra's drag and wheel (`tweaks`).
|
||||
tweaks: tweaks::Tweaks,
|
||||
tweaks: extras::mouse_tweaks::Tweaks,
|
||||
/// The wheel and hovered slot of `BundleMouseActions`.
|
||||
bundle_mouse: bundles::BundleMouse,
|
||||
/// The last inventory click, for the double click that gathers a stack.
|
||||
@@ -2811,10 +2810,9 @@ struct Game<'a> {
|
||||
pending_bench: Option<(Instant, std::sync::mpsc::Sender<String>)>,
|
||||
/// Recent frame times for the performance overlay.
|
||||
frame_graph: horizon_client::perf_overlay::FrameGraph,
|
||||
/// Horizon's extras (minimap, auto-reconnect; mods' counters): `game/extras.rs`.
|
||||
/// Horizon's extras (mods' minimap and counters; PvP & HUD's): `game/extras.rs`.
|
||||
extras: extras::ExtrasState,
|
||||
/// The PvP & HUD options' state (`game/pvp.rs`).
|
||||
pvp: pvp::PvpState,
|
||||
pvp: extras::pvp::PvpState,
|
||||
/// Horizon mods (`game/mods.rs`), none running unless turned on.
|
||||
mods: mods::ModsState,
|
||||
/// Every item and recipe, for mods (`game/native_recipes.rs`), made once.
|
||||
@@ -2879,9 +2877,9 @@ struct Game<'a> {
|
||||
entity_attachments: HashMap<[i32; 4], horizon_world::attachments::Attachments>,
|
||||
/// Connected to a server: the world comes from the network (game/remote.rs).
|
||||
remote: Option<remote::Remote>,
|
||||
/// The open connection joined from Realms: `ServerData.isRealm` for the friends
|
||||
/// service's presence status.
|
||||
realm_session: bool,
|
||||
/// The multiplayer server connected to last, and whether it is a Realm; the connection's
|
||||
/// lifecycle reaches the hooks through it (`last_server.rs`).
|
||||
last_server: horizon_client::last_server::LastServer,
|
||||
/// Active packet replay session (game/studio.rs).
|
||||
pub(super) studio_replay: Option<horizon_client::studio::ReplayDriver>,
|
||||
pub(super) studio_freecam: horizon_client::studio::StudioFreecam,
|
||||
@@ -5532,7 +5530,7 @@ impl Game<'_> {
|
||||
}
|
||||
} else if self.remote.is_some() {
|
||||
// `getCurrentServer().isRealm()`: the connection came from Realms.
|
||||
if self.realm_session {
|
||||
if self.last_server.realm {
|
||||
PresenceStatus::PlayingRealms
|
||||
} else {
|
||||
PresenceStatus::PlayingServer
|
||||
@@ -5875,7 +5873,7 @@ impl Game<'_> {
|
||||
fn hud_layers(&mut self, size: [u32; 2]) -> HudBatch {
|
||||
self.pad.guide_shown.clear();
|
||||
let mut batch = HudBatch::new(size);
|
||||
batch.glint_colour = self.settings.extras.pvp.tweaks.glint_texel();
|
||||
batch.glint_colour = self.glint_texel();
|
||||
// GlobalSettingsUniform's GameTime, for a resource pack's text shader.
|
||||
batch.game_time = ((self.world.game_time.rem_euclid(24000)) as f32
|
||||
+ self.clock.fraction() as f32)
|
||||
@@ -5988,8 +5986,7 @@ impl Game<'_> {
|
||||
// Hud.extractCrosshair / extractItemHotbar / extractPlayerHealth /
|
||||
// extractVehicleHealth and the experience bar, laid out by hud_layout.
|
||||
// Combat feel Legacy hides the attack indicator unless the player keeps it.
|
||||
let combat = self.settings.extras.pvp.combat;
|
||||
let indicator = if combat.legacy && !combat.indicator {
|
||||
let indicator = if self.attack_indicator_hidden() {
|
||||
0
|
||||
} else {
|
||||
self.settings.attack_indicator
|
||||
@@ -6053,7 +6050,7 @@ impl Game<'_> {
|
||||
});
|
||||
self.gui_layer("minecraft:effects", &mut batch, |g, batch| {
|
||||
// The PvP & HUD effects widget takes the icons' place.
|
||||
if !g.pvp_effects_shown() {
|
||||
if !g.effect_icons_hidden() {
|
||||
g.draw_effect_icons(batch);
|
||||
}
|
||||
});
|
||||
@@ -6488,7 +6485,7 @@ impl Game<'_> {
|
||||
self.draw_hovered_slot_highlight(batch, false, ox, oy);
|
||||
self.draw_inventory_slots(batch, ox, oy);
|
||||
self.draw_hovered_slot_highlight(batch, true, ox, oy);
|
||||
self.draw_sort_buttons(batch, ox, oy);
|
||||
self.draw_container_extras(batch, ox, oy);
|
||||
self.draw_recipe_book(batch);
|
||||
let effect_tip = if self.container.is_none() && self.craft_size == 2 && !self.mount_active()
|
||||
{
|
||||
@@ -9898,7 +9895,7 @@ impl Game<'_> {
|
||||
outline,
|
||||
outline_after_terrain,
|
||||
outline_high_contrast: self.settings.file.bool("highContrastBlockOutline"),
|
||||
pvp: self.settings.extras.pvp.frame(),
|
||||
look: self.look_frame(),
|
||||
debug_lines: &debug_lines,
|
||||
debug_lines_on_top: &debug_lines_on_top,
|
||||
debug_crosshair: &debug_crosshair,
|
||||
@@ -10709,7 +10706,7 @@ impl Game<'_> {
|
||||
if self.screen == Screen::Inventory
|
||||
&& d != 0.0
|
||||
&& !bundle
|
||||
&& !self.tweak_scroll(d, self.inventory_target().0)
|
||||
&& !self.container_scroll(d)
|
||||
{
|
||||
self.station_scroll(d);
|
||||
}
|
||||
@@ -10957,7 +10954,7 @@ impl Game<'_> {
|
||||
return Ok(());
|
||||
}
|
||||
if pressed
|
||||
&& (self.sort_mouse_press(button)
|
||||
&& (self.container_mouse_press(button)
|
||||
|| self.hud_editor_key_press(&horizon_client::keybinds::mouse_name(
|
||||
button,
|
||||
)))
|
||||
@@ -11499,7 +11496,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.extras_frame(elapsed);
|
||||
self.extras_retry()?;
|
||||
self.reconnect_when_due()?;
|
||||
self.apply_mouse_look();
|
||||
self.pad_look(elapsed.as_secs_f32());
|
||||
self.advance_flight();
|
||||
@@ -11610,7 +11607,7 @@ impl Game<'_> {
|
||||
self.settings.simulation_distance,
|
||||
self.settings.entity_distance,
|
||||
);
|
||||
server.set_legacy_rules(self.settings.extras.pvp.combat.rules);
|
||||
server.set_legacy_rules(self.pvp_legacy_rules());
|
||||
}
|
||||
self.clock.end_frame(self.pause);
|
||||
let mut consumed = false;
|
||||
@@ -12210,7 +12207,7 @@ impl<'a> Game<'a> {
|
||||
persist: Default::default(),
|
||||
entity_attachments: Default::default(),
|
||||
remote: None,
|
||||
realm_session: false,
|
||||
last_server: Default::default(),
|
||||
studio_replay: None,
|
||||
studio_freecam: Default::default(),
|
||||
studio_timeline: Default::default(),
|
||||
|
||||
@@ -1278,7 +1278,7 @@ impl Game<'_> {
|
||||
let ctrl = self.has_control_down_with_quirk();
|
||||
let shift = self.has_shift_down();
|
||||
// PvP & HUD's chat search: Ctrl+F starts and stops it, Enter stops it (sends nothing).
|
||||
if self.settings.extras.pvp.chat.search
|
||||
if self.chat_look().search
|
||||
&& match key {
|
||||
KeyCode::KeyF => ctrl,
|
||||
KeyCode::Enter | KeyCode::NumpadEnter => self.pvp.chat_search,
|
||||
@@ -1487,7 +1487,7 @@ impl Game<'_> {
|
||||
cursor.1 as f64,
|
||||
);
|
||||
// PvP & HUD's copy on click: a message where the click has nothing else to do.
|
||||
if self.settings.extras.pvp.chat.copy
|
||||
if self.chat_look().copy
|
||||
&& !self.has_shift_down()
|
||||
&& style.as_ref().is_none_or(|s| s.click.is_none())
|
||||
&& let Some(message) =
|
||||
@@ -1665,8 +1665,8 @@ impl Game<'_> {
|
||||
/// Chat lines over the HUD (focused while the chat screen is open).
|
||||
pub(super) fn draw_chat(&mut self, batch: &mut HudBatch) {
|
||||
let focused = self.screen == Screen::Chat;
|
||||
let options = self.settings.extras.pvp.chat;
|
||||
self.ui.chat.set_qol(&self.gui.atlas, options.qol());
|
||||
let options = self.chat_look();
|
||||
self.ui.chat.set_qol(&self.gui.atlas, self.chat_qol());
|
||||
// A search ends with its option; a chat closed some other way ends it in keep_chat_draft.
|
||||
if self.pvp.chat_search && self.ui.chat_screen.is_none() {
|
||||
// The chat went without closing (leaving the world): nothing to give back.
|
||||
|
||||
@@ -1249,7 +1249,7 @@ impl Game<'_> {
|
||||
self.sync_ops(&lists);
|
||||
let anticheat = load_anticheat();
|
||||
let chat_filter = load_chat_filter();
|
||||
self.settings.extras.pvp.combat.rules = load_combat();
|
||||
self.set_pvp_legacy_rules(load_combat());
|
||||
// Horizon's far ring (docs/hosting.md): server.toml's `max_view_distance`.
|
||||
let far = props
|
||||
.raw
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
//! (`native_minimap`: its tiles, image and HUD) and zoom (`native_zoom`) on the camera; clicks
|
||||
//! and frame times for mods (`native_clicks`, `native_frame_rate`).
|
||||
//! Each hook does nothing while its extra is off and no mod uses it.
|
||||
//!
|
||||
//! Each Horizon extra's game glue is a child module here, so it leaves the engine in one move
|
||||
//! (docs/layers.md, plan M5): `reconnect` (auto-reconnect), `pvp` (the PvP & HUD options),
|
||||
//! `mouse_tweaks` and `sorting` (the inventory extras).
|
||||
pub(in crate::game) mod mouse_tweaks;
|
||||
pub(in crate::game) mod pvp;
|
||||
mod reconnect;
|
||||
mod sorting;
|
||||
|
||||
use super::{Dim, Game, Screen, native_hooks::NativeMinimap};
|
||||
use horizon_client::{
|
||||
extras::{Corner, Cps, FpsCounter, fov_target},
|
||||
extras_hud::{self, Corners},
|
||||
hud::{HudBatch, MINIMAP_SPRITE},
|
||||
minimap::{self, Layer, Minimap, View},
|
||||
multiplayer,
|
||||
reconnect::{Retry, Tracker},
|
||||
title::Mode,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use winit::event::MouseButton;
|
||||
@@ -24,8 +30,6 @@ pub(super) struct ExtrasState {
|
||||
minimap: Minimap,
|
||||
/// The dimension the minimap's tiles belong to.
|
||||
minimap_dim: Option<Dim>,
|
||||
/// Auto-reconnect's attempts at the current server.
|
||||
reconnect: Tracker,
|
||||
}
|
||||
/// CPU time the minimap's tiles may take per frame.
|
||||
const MINIMAP_BUDGET: Duration = Duration::from_micros(300);
|
||||
@@ -57,6 +61,7 @@ impl Game<'_> {
|
||||
if !playing {
|
||||
self.extras.mouse = [false; 2];
|
||||
}
|
||||
self.sync_reconnect_setting();
|
||||
self.update_minimap();
|
||||
// For mods (`render.frame-stats`, `native_frame_rate`).
|
||||
let now = self.extras_now();
|
||||
@@ -78,74 +83,6 @@ impl Game<'_> {
|
||||
let cps = (l.per_second(now) as u32, r.per_second(now) as u32);
|
||||
(cps, self.extras.mouse)
|
||||
}
|
||||
/// Auto-reconnect: a connection to `address` as `name` starts.
|
||||
pub(super) fn extras_connecting(&mut self, address: &str, name: &str, lan: bool) {
|
||||
self.extras.reconnect.connecting(address, name, lan);
|
||||
}
|
||||
/// The server this game connected to last.
|
||||
pub(super) fn server_address(&self) -> Option<&str> {
|
||||
self.extras.reconnect.address()
|
||||
}
|
||||
/// The server this game connected to last was found on the local network.
|
||||
pub(super) fn server_is_lan(&self) -> bool {
|
||||
self.extras.reconnect.lan()
|
||||
}
|
||||
/// Auto-reconnect: the server let the player in.
|
||||
pub(super) fn extras_joined(&mut self) {
|
||||
self.extras.reconnect.joined(std::time::Instant::now());
|
||||
}
|
||||
/// Auto-reconnect: the player left the server on purpose.
|
||||
pub(super) fn extras_left(&mut self) {
|
||||
self.extras.reconnect.reset();
|
||||
}
|
||||
/// Auto-reconnect's next attempt after a disconnect, when it is on and
|
||||
/// the reason allows one.
|
||||
pub(super) fn reconnect_after(
|
||||
&mut self,
|
||||
reason: &str,
|
||||
key: Option<&str>,
|
||||
from_server: bool,
|
||||
) -> Option<Retry> {
|
||||
if !self.settings.extras.auto_reconnect {
|
||||
self.extras.reconnect.reset();
|
||||
return None;
|
||||
}
|
||||
let now = std::time::Instant::now();
|
||||
self.extras.reconnect.ended(reason, key, from_server, now)
|
||||
}
|
||||
/// Show the attempt's countdown on the Disconnected screen.
|
||||
pub(super) fn offer_reconnect(&mut self, retry: Option<Retry>) {
|
||||
let Some(retry) = retry else {
|
||||
return;
|
||||
};
|
||||
if let Some(title) = self.title.as_mut()
|
||||
&& let Mode::Multiplayer(multiplayer::Screen::Disconnected { retry: slot, .. }) =
|
||||
&mut title.mode
|
||||
{
|
||||
log::info!(
|
||||
"Horizon: reconnecting to {} in {} s",
|
||||
horizon_client::net::shown_address(&retry.address),
|
||||
retry.seconds_left(std::time::Instant::now())
|
||||
);
|
||||
*slot = Some(retry);
|
||||
}
|
||||
}
|
||||
/// Auto-reconnect: connect again once the countdown is over. An
|
||||
/// online-mode server gets the signed-in account again: `connect_server`
|
||||
/// takes it from `auth::current()`, held in memory for the whole run
|
||||
/// (the launcher's session file is gone after the first read).
|
||||
pub(super) fn extras_retry(&mut self) -> horizon_client::Result<()> {
|
||||
if let Some(retry) = self.title.as_mut().and_then(|t| t.due_retry()) {
|
||||
self.connect_server_with_options(
|
||||
&retry.address,
|
||||
&retry.name,
|
||||
None,
|
||||
retry.lan,
|
||||
self.realm_session,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// The minimap a mod shows (`native_minimap`): its corner, its side
|
||||
/// in GUI pixels and its view of the player.
|
||||
fn minimap_shown(&self) -> Option<(Corner, f32, View)> {
|
||||
@@ -284,7 +221,7 @@ impl Game<'_> {
|
||||
/// (`Hud.extractEffects`: beneficial at y 1, harmful at y 26).
|
||||
fn effect_rows_height(&self) -> f32 {
|
||||
let effects = &self.potions.effects.list;
|
||||
if effects.is_empty() || self.screen != Screen::Playing || self.pvp_effects_shown() {
|
||||
if effects.is_empty() || self.screen != Screen::Playing || self.effect_icons_hidden() {
|
||||
return 0.0;
|
||||
}
|
||||
let harmful = effects.iter().any(|e| {
|
||||
|
||||
+19
-166
@@ -17,26 +17,12 @@
|
||||
//! other side. Down over a result slot takes one craft or the whole furnace
|
||||
//! output to the inventory; nothing ever goes into a result slot.
|
||||
//!
|
||||
//! Sorting draws a small button by a menu's storage slots and one by the
|
||||
//! inventory's (an optional key sorts the part under the cursor); the
|
||||
//! clicks come from `inv_sort::plan`.
|
||||
use super::{Creative, Game, Screen};
|
||||
use horizon_client::{
|
||||
containers::BlockEntity,
|
||||
hud::{HudBatch, rgba},
|
||||
inv_sort,
|
||||
items::ItemStack,
|
||||
keybinds,
|
||||
menu::Input,
|
||||
stations::hover_name,
|
||||
};
|
||||
use winit::{event::MouseButton, keyboard::KeyCode};
|
||||
|
||||
/// A sort button's side in GUI pixels.
|
||||
const SORT_BUTTON: f32 = 9.0;
|
||||
//! Sorting, which clicks through `tweak_click` too, is in `sorting.rs`.
|
||||
use crate::game::{Creative, Game, Screen};
|
||||
use horizon_client::{containers::BlockEntity, items::ItemStack, menu::Input};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Tweaks {
|
||||
pub(in crate::game) struct Tweaks {
|
||||
drag: Option<Drag>,
|
||||
/// Wheel steps not yet acted on (a trackpad scrolls in fractions).
|
||||
wheel: f32,
|
||||
@@ -44,7 +30,7 @@ pub(super) struct Tweaks {
|
||||
/// A server's menu the tweaks run in: their clicks are vanilla's in these,
|
||||
/// and the menu engine knows which slot takes what. Not the menus whose slots
|
||||
/// cost or trigger something (anvils, enchanting, merchants, beacons, crafters).
|
||||
pub(super) fn tweak_menu(name: &str) -> bool {
|
||||
pub(in crate::game) fn tweak_menu(name: &str) -> bool {
|
||||
name == "inventory"
|
||||
|| name.starts_with("minecraft:generic_9x")
|
||||
|| matches!(
|
||||
@@ -72,7 +58,7 @@ struct Drag {
|
||||
|
||||
impl Game<'_> {
|
||||
/// The open screen is one the tweaks run in, with the extra on.
|
||||
pub(super) fn tweaks_on(&self) -> bool {
|
||||
pub(in crate::game) fn tweaks_on(&self) -> bool {
|
||||
if !self.settings.extras.mouse_tweaks
|
||||
|| self.screen != Screen::Inventory
|
||||
|| self.station.is_some()
|
||||
@@ -87,7 +73,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
/// One ordinary click on a game slot. False when nothing took it.
|
||||
pub(super) fn tweak_click(&mut self, slot: usize, button: i32, input: Input) -> bool {
|
||||
pub(in crate::game) fn tweak_click(&mut self, slot: usize, button: i32, input: Input) -> bool {
|
||||
if self.remote.is_some() {
|
||||
return self.remote_menu_ops(&[(Some(slot), button, input)]);
|
||||
}
|
||||
@@ -103,7 +89,7 @@ impl Game<'_> {
|
||||
/// drag (`QUICK_CRAFT`) and not a click: a server that has the hand empty
|
||||
/// (the stack it was to come from just burned as fuel) ignores a drag, but
|
||||
/// a click would pick up what's in `slot`.
|
||||
fn tweak_put(&mut self, slot: usize, right: bool) -> bool {
|
||||
pub(in crate::game) fn tweak_put(&mut self, slot: usize, right: bool) -> bool {
|
||||
let kind = i32::from(right) << 2;
|
||||
let ops = [
|
||||
(None, kind, Input::QuickCraft),
|
||||
@@ -123,7 +109,12 @@ impl Game<'_> {
|
||||
}
|
||||
/// A press on `slot`: true when the tweaks took it (a right-drag
|
||||
/// placing), else vanilla's press runs and the tweaks watch the drag.
|
||||
pub(super) fn tweak_press(&mut self, slot: Option<usize>, right: bool, shift: bool) -> bool {
|
||||
pub(in crate::game) fn tweak_press(
|
||||
&mut self,
|
||||
slot: Option<usize>,
|
||||
right: bool,
|
||||
shift: bool,
|
||||
) -> bool {
|
||||
self.tweaks.drag = None;
|
||||
let Some(slot) = slot.filter(|&s| s != 200 && s < Creative::DISPLAY) else {
|
||||
return false;
|
||||
@@ -174,7 +165,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// The cursor moved while a button is held: true when the tweaks own
|
||||
/// the drag (vanilla's quick craft drag doesn't grow).
|
||||
pub(super) fn tweak_drag_move(&mut self, slot: Option<usize>) -> bool {
|
||||
pub(in crate::game) fn tweak_drag_move(&mut self, slot: Option<usize>) -> bool {
|
||||
let Some(mut drag) = self.tweaks.drag.take() else {
|
||||
return false;
|
||||
};
|
||||
@@ -222,11 +213,11 @@ impl Game<'_> {
|
||||
owned
|
||||
}
|
||||
/// The button went up: the drag ends.
|
||||
pub(super) fn tweak_release(&mut self) {
|
||||
pub(in crate::game) fn tweak_release(&mut self) {
|
||||
self.tweaks.drag = None;
|
||||
}
|
||||
/// The wheel over an inventory screen: true when the tweaks used it.
|
||||
pub(super) fn tweak_scroll(&mut self, delta: f32, slot: Option<usize>) -> bool {
|
||||
pub(in crate::game) fn tweak_scroll(&mut self, delta: f32, slot: Option<usize>) -> bool {
|
||||
if !self.settings.extras.mouse_tweaks_wheel || !self.tweaks_on() || self.carried.is_some() {
|
||||
return false;
|
||||
}
|
||||
@@ -256,7 +247,7 @@ impl Game<'_> {
|
||||
result
|
||||
}
|
||||
/// `slot` takes one more of `stack`, by the open menu's rules.
|
||||
fn tweak_takes_one(&mut self, slot: usize, stack: &ItemStack) -> bool {
|
||||
pub(in crate::game) fn tweak_takes_one(&mut self, slot: usize, stack: &ItemStack) -> bool {
|
||||
let mut takes = false;
|
||||
self.menu_session(|menu, env, to_menu| {
|
||||
takes = to_menu(slot).is_some_and(|i| menu.takes_one(env, i, stack));
|
||||
@@ -266,7 +257,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// Where vanilla's quick move would put `slot`'s items, tried on a copy
|
||||
/// of the menu that's dropped (`Some(false)` keeps nothing).
|
||||
fn tweak_landing(&mut self, slot: usize) -> Vec<usize> {
|
||||
pub(in crate::game) fn tweak_landing(&mut self, slot: usize) -> Vec<usize> {
|
||||
let slots = self.slots();
|
||||
let mut landing = Vec::new();
|
||||
self.menu_session(|menu, env, to_menu| {
|
||||
@@ -356,144 +347,6 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The sort buttons of the open screen, with the extra on: their
|
||||
/// corner (from the screen's) and the slots each sorts. Any menu screen
|
||||
/// gets them, by its slots' roles (`native_slot_roles`): one by the top
|
||||
/// right of its storage slots (a chest's title line) when it has some,
|
||||
/// one by the main inventory's (the "Inventory" label's line).
|
||||
fn sort_buttons(&self) -> Vec<((f32, f32), Vec<usize>)> {
|
||||
use super::NativeSlotRole as Role;
|
||||
if !self.settings.extras.sort {
|
||||
return Vec::new();
|
||||
}
|
||||
let roles = self.native_slot_roles();
|
||||
let of = |role| -> Vec<usize> {
|
||||
roles
|
||||
.iter()
|
||||
.filter(|(_, r)| *r == role)
|
||||
.map(|(s, _)| *s)
|
||||
.collect()
|
||||
};
|
||||
// Over the section's last column: (160, 5) over a chest's slots.
|
||||
let right = |slots: &[usize]| {
|
||||
slots
|
||||
.iter()
|
||||
.map(|&s| self.slot_position(s).0 + 8.0)
|
||||
.fold(f32::MIN, f32::max)
|
||||
};
|
||||
let mut buttons = Vec::new();
|
||||
let storage = of(Role::Storage);
|
||||
if !storage.is_empty() {
|
||||
let top = storage
|
||||
.iter()
|
||||
.map(|&s| self.slot_position(s).1 - 13.0)
|
||||
.fold(f32::MAX, f32::min);
|
||||
buttons.push(((right(&storage), top), storage));
|
||||
}
|
||||
// On the "Inventory" label's line (`inventoryLabelY`, height - 94, 12
|
||||
// above the first row): a mod's screen places its inventory itself.
|
||||
let main = of(Role::Inventory);
|
||||
if !main.is_empty() {
|
||||
let y = self
|
||||
.native_screen_inventory_y()
|
||||
.map_or(self.screen_height() - 95.0, |y| y - 13.0);
|
||||
buttons.push(((right(&main), y), main));
|
||||
}
|
||||
buttons
|
||||
}
|
||||
/// The sort buttons: a bordered square with three bars, lighter under
|
||||
/// the cursor.
|
||||
pub(super) fn draw_sort_buttons(&self, batch: &mut HudBatch, ox: f32, oy: f32) {
|
||||
let atlas = &self.gui.atlas;
|
||||
for ((x, y), _) in self.sort_buttons() {
|
||||
let (x, y) = (ox + x, oy + y);
|
||||
let (cx, cy) = self.cursor;
|
||||
let hovered = cx >= x && cx < x + SORT_BUTTON && cy >= y && cy < y + SORT_BUTTON;
|
||||
let face = if hovered {
|
||||
rgba(220, 220, 220, 255)
|
||||
} else {
|
||||
rgba(160, 160, 160, 255)
|
||||
};
|
||||
batch.fill(atlas, x, y, SORT_BUTTON, SORT_BUTTON, rgba(55, 55, 55, 255));
|
||||
batch.fill(
|
||||
atlas,
|
||||
x + 1.0,
|
||||
y + 1.0,
|
||||
SORT_BUTTON - 2.0,
|
||||
SORT_BUTTON - 2.0,
|
||||
face,
|
||||
);
|
||||
for (i, w) in [5.0, 4.0, 3.0].into_iter().enumerate() {
|
||||
let by = y + 2.0 + 2.0 * i as f32;
|
||||
batch.fill(atlas, x + 2.0, by, w, 1.0, rgba(55, 55, 55, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A left press on a sort button sorts its slots (true).
|
||||
pub(super) fn sort_press(&mut self) -> bool {
|
||||
let Some(r) = self.renderer.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let batch = HudBatch::new([r.config.width, r.config.height]);
|
||||
let (ox, oy) = self.inventory_origin(&batch);
|
||||
let (cx, cy) = self.cursor;
|
||||
let hit = self.sort_buttons().into_iter().find(|((x, y), _)| {
|
||||
let (x, y) = (ox + x, oy + y);
|
||||
cx >= x && cx < x + SORT_BUTTON && cy >= y && cy < y + SORT_BUTTON
|
||||
});
|
||||
let Some((_, slots)) = hit else {
|
||||
return false;
|
||||
};
|
||||
self.sort_slots(&slots);
|
||||
true
|
||||
}
|
||||
/// The sort key: sorts the part under the cursor (the container when
|
||||
/// the cursor is over no slot).
|
||||
pub(super) fn sort_key_press(&mut self, key: KeyCode) -> bool {
|
||||
keybinds::key_name(key).is_some_and(|name| self.sort_input(name))
|
||||
}
|
||||
pub(super) fn sort_mouse_press(&mut self, button: MouseButton) -> bool {
|
||||
self.sort_input(&keybinds::mouse_name(button))
|
||||
}
|
||||
fn sort_input(&mut self, name: &str) -> bool {
|
||||
let e = &self.settings.extras;
|
||||
if !e.sort || e.sort_key != name || self.recipe_search_focused() {
|
||||
return false;
|
||||
}
|
||||
let buttons = self.sort_buttons();
|
||||
let hovered = self.inventory_target().0;
|
||||
let slots = buttons
|
||||
.iter()
|
||||
.find(|(_, slots)| hovered.is_some_and(|h| slots.contains(&h)))
|
||||
.or(buttons.first())
|
||||
.map(|(_, slots)| slots.clone());
|
||||
let Some(slots) = slots else {
|
||||
return false;
|
||||
};
|
||||
self.sort_slots(&slots);
|
||||
true
|
||||
}
|
||||
/// Sorts `slots` with ordinary left clicks (needs an empty hand).
|
||||
fn sort_slots(&mut self, slots: &[usize]) {
|
||||
if self.carried.is_some() {
|
||||
return;
|
||||
}
|
||||
let views: Vec<Option<ItemStack>> = slots.iter().map(|&s| self.slot_view(s)).collect();
|
||||
let clicks = {
|
||||
let rank = |s: &ItemStack| {
|
||||
let order = self.creative_screen.order_of(s).unwrap_or(usize::MAX);
|
||||
(order, hover_name(&self.pack.names, s))
|
||||
};
|
||||
let max = |s: &ItemStack| self.max_stack(s);
|
||||
inv_sort::plan(&views, &rank, &max)
|
||||
};
|
||||
for i in clicks {
|
||||
self.tweak_click(slots[i], 0, Input::Pickup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::tweak_menu;
|
||||
+27
-40
@@ -1,6 +1,6 @@
|
||||
//! The PvP & HUD page's options in the game (`horizon_client::pvp`, `docs/pvp.md`). Each
|
||||
//! does nothing while it is off.
|
||||
use super::{Dim, Game};
|
||||
use crate::game::Game;
|
||||
use horizon_client::{
|
||||
extras::Corner,
|
||||
extras_hud::Corners,
|
||||
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
||||
use winit::event::MouseButton;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct PvpState {
|
||||
pub(in crate::game) struct PvpState {
|
||||
crosshair: CrosshairCache,
|
||||
/// Each armour slot's item, damage and maximum damage, and whether durability is in percent.
|
||||
armour: Widget<([Option<(Arc<str>, u32, u32)>; 4], bool)>,
|
||||
@@ -41,13 +41,13 @@ pub(super) struct PvpState {
|
||||
speed: pvp_hud::Speed,
|
||||
speed_shown: Widget<u32>,
|
||||
/// Each widget drawn last frame and its box on screen, for the HUD editor.
|
||||
pub(super) boxes: Vec<(pvp_hud::WidgetId, [f32; 4])>,
|
||||
pub(in crate::game) boxes: Vec<(pvp_hud::WidgetId, [f32; 4])>,
|
||||
/// The HUD editor, while it is open.
|
||||
pub(super) editor: Option<super::hud_editor::HudEditor>,
|
||||
pub(in crate::game) editor: Option<crate::game::hud_editor::HudEditor>,
|
||||
/// The open chat's box holds a chat search's query (Ctrl+F).
|
||||
pub(super) chat_search: bool,
|
||||
pub(in crate::game) chat_search: bool,
|
||||
/// What the chat box held when the search borrowed it.
|
||||
pub(super) chat_search_aside: String,
|
||||
pub(in crate::game) chat_search_aside: String,
|
||||
}
|
||||
/// `GuiGraphicsExtractor`'s grey for a detail line (an effect's time left).
|
||||
const DETAIL: u32 = 0xFF7F_7F7F;
|
||||
@@ -78,13 +78,13 @@ const ARROWS: [&str; 3] = [
|
||||
|
||||
impl Game<'_> {
|
||||
/// Whether the custom crosshair replaces vanilla's sprite.
|
||||
pub(super) fn custom_crosshair(&self) -> bool {
|
||||
pub(in crate::game) fn custom_crosshair(&self) -> bool {
|
||||
self.settings.extras.pvp.crosshair.custom
|
||||
}
|
||||
/// The custom crosshair, centred where vanilla's 15x15 sprite is (`Hud.extractCrosshair`):
|
||||
/// its outline in black, then the shape, inverting what is under it when its colour is
|
||||
/// `Inverted`. The runs are cached, so a frame only adds their quads.
|
||||
pub(super) fn draw_custom_crosshair(&mut self, batch: &mut HudBatch, w: i32, h: i32) {
|
||||
pub(in crate::game) fn draw_custom_crosshair(&mut self, batch: &mut HudBatch, w: i32, h: i32) {
|
||||
let mut c = self.settings.extras.pvp.crosshair;
|
||||
if let Some(hover) = c.hover
|
||||
&& self.crosshair_living() == Some(true)
|
||||
@@ -114,7 +114,7 @@ impl Game<'_> {
|
||||
/// the outline's edges come from) as translucent faces drawn after translucent terrain,
|
||||
/// pushed out a little so they don't fight the block's own faces. Only while the outline
|
||||
/// shows (`outline` is its edges).
|
||||
pub(super) fn outline_fill(
|
||||
pub(in crate::game) fn outline_fill(
|
||||
&self,
|
||||
outline: &[[f64; 6]],
|
||||
camera: glam::DVec3,
|
||||
@@ -176,42 +176,33 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Once per frame: the options that live outside the game's state.
|
||||
pub(super) fn pvp_frame(&mut self) {
|
||||
/// Once per frame: the options that live outside the game's state, and the look the engine
|
||||
/// draws with (`game/look.rs`), the only way it reads them.
|
||||
pub(in crate::game) fn pvp_frame(&mut self) {
|
||||
let tweaks = self.settings.extras.pvp.tweaks;
|
||||
horizon_client::enchant::set_numeric_levels(tweaks.numeric_enchants);
|
||||
horizon_hooks::look::update(|look| self.settings.extras.pvp.apply(look));
|
||||
}
|
||||
/// Whether this window may draw PvP & HUD's own time and weather: only a client of a server
|
||||
/// (single player's included) draws them, never the server that ticks the world.
|
||||
fn own_view(&self) -> bool {
|
||||
self.remote.is_some() && self.dim == Dim::Overworld
|
||||
/// Legacy rules: the hits a server resolves follow 1.8.9 (`legacy_rules.rs`).
|
||||
pub(in crate::game) fn pvp_legacy_rules(&self) -> bool {
|
||||
self.settings.extras.pvp.combat.rules
|
||||
}
|
||||
/// The day time the frame draws (sky, sun, moon, the sky's light): PvP & HUD's time of day,
|
||||
/// else the world's. The world's own `day_time` and everything that plays by it stay.
|
||||
pub(super) fn view_day_time(&self) -> i64 {
|
||||
let fixed = self.settings.extras.pvp.tweaks.time;
|
||||
horizon_client::pvp::view_day_time(self.world.day_time, fixed.filter(|_| self.own_view()))
|
||||
}
|
||||
/// The rain and thunder levels the frame draws (rain, the sky's colour and light, fog): PvP &
|
||||
/// HUD's weather, else the world's. Gameplay (riptide, mobs, crops) keeps the world's.
|
||||
pub(super) fn view_weather(&self, partial: f32) -> (f32, f32) {
|
||||
match self.settings.extras.pvp.tweaks.weather {
|
||||
Some(w) if self.own_view() && self.can_have_weather() => w.levels(),
|
||||
_ => self.weather_levels(partial),
|
||||
}
|
||||
/// The server's legacy rules, from server.toml or the window's options.
|
||||
pub(in crate::game) fn set_pvp_legacy_rules(&mut self, on: bool) {
|
||||
self.settings.extras.pvp.combat.rules = on;
|
||||
}
|
||||
/// This player attacked entity `id`, `distance` from the eyes to where the crosshair's
|
||||
/// ray met its box (the combo counter waits for the server's report; the reach widget
|
||||
/// shows the distance).
|
||||
pub(super) fn pvp_attacked(&mut self, id: i32, distance: f64) {
|
||||
pub(in crate::game) fn pvp_attacked(&mut self, id: i32, distance: f64) {
|
||||
let now = self.started.elapsed().as_secs_f64();
|
||||
self.pvp.combo.attacked(id, now);
|
||||
self.pvp.reach.hit(distance, now);
|
||||
}
|
||||
/// A mouse press, counted for clicks per second while playing with a widget that shows them.
|
||||
pub(super) fn pvp_click(&mut self, button: MouseButton) {
|
||||
pub(in crate::game) fn pvp_click(&mut self, button: MouseButton) {
|
||||
let hud = &self.settings.extras.pvp.hud;
|
||||
if !(hud.cps || hud.keys) || self.screen != super::Screen::Playing {
|
||||
if !(hud.cps || hud.keys) || self.screen != crate::game::Screen::Playing {
|
||||
return;
|
||||
}
|
||||
let i = match button {
|
||||
@@ -232,7 +223,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// The server reports entity `id` hurt; `own` when it is this player,
|
||||
/// `by_me` whether this player was the damage's direct entity (if named).
|
||||
pub(super) fn pvp_hurt(&mut self, id: i32, own: bool, by_me: Option<bool>) {
|
||||
pub(in crate::game) fn pvp_hurt(&mut self, id: i32, own: bool, by_me: Option<bool>) {
|
||||
if own {
|
||||
self.pvp.combo.broken();
|
||||
} else {
|
||||
@@ -240,13 +231,9 @@ impl Game<'_> {
|
||||
self.pvp.combo.hurt(id, by_me, now);
|
||||
}
|
||||
}
|
||||
/// Whether the effects widget replaces vanilla's effect icons (`Hud.extractEffects`).
|
||||
pub(super) fn pvp_effects_shown(&self) -> bool {
|
||||
self.settings.extras.pvp.hud.effects
|
||||
}
|
||||
/// The HUD widgets that are on, after the minimap and before mods' boxes. Each rebuilds its
|
||||
/// lines only when what it shows changed.
|
||||
pub(super) fn draw_pvp_hud(&mut self, batch: &mut HudBatch, corners: &mut Corners) {
|
||||
pub(in crate::game) fn draw_pvp_hud(&mut self, batch: &mut HudBatch, corners: &mut Corners) {
|
||||
let hud = self.settings.extras.pvp.hud;
|
||||
if self.player.is_none() || !hud.any() {
|
||||
// The editor frames only what was drawn.
|
||||
@@ -267,8 +254,8 @@ impl Game<'_> {
|
||||
let at = [p.position.x, p.position.y, p.position.z].map(|v| v.floor() as i32);
|
||||
(at, pvp_hud::facing(self.yaw))
|
||||
});
|
||||
// The address is copied only when it changed (`server_address` borrows the whole game).
|
||||
let server = self.server_address().filter(|_| multiplayer);
|
||||
// The address is copied only when it changed.
|
||||
let server = self.last_server.address().filter(|_| multiplayer);
|
||||
let server = (hud.server && self.pvp.server.next.as_deref() != server)
|
||||
.then(|| server.map(str::to_owned));
|
||||
let now = self.started.elapsed().as_secs_f64();
|
||||
@@ -691,7 +678,7 @@ impl Game<'_> {
|
||||
false => s.item == held.item,
|
||||
};
|
||||
let slots = self.inventory.iter().map(Option::as_ref).enumerate();
|
||||
let offhand = std::iter::once((super::shield::OFFHAND_SLOT, self.offhand.as_ref()));
|
||||
let offhand = std::iter::once((crate::game::shield::OFFHAND_SLOT, self.offhand.as_ref()));
|
||||
let mut found = slots
|
||||
.chain(offhand)
|
||||
.filter_map(|(i, s)| Some((i, s?)))
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Auto-reconnect (`autoReconnect`, docs/extras.md) runs on the engine's connection hooks
|
||||
//! (`horizon_client::reconnect::HOOKS`, registered in `entry.rs`). Its setting still lives in
|
||||
//! the engine's extras, so it is handed over here; it leaves with the extras page (plan M5).
|
||||
use crate::game::Game;
|
||||
|
||||
impl Game<'_> {
|
||||
/// Once per frame: the `autoReconnect` setting, for the hook to read when a connection ends.
|
||||
pub(in crate::game) fn sync_reconnect_setting(&self) {
|
||||
horizon_client::reconnect::HOOKS.set_enabled(self.settings.extras.auto_reconnect);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Sorting (`sort`, `sortKey`; #332, `docs/extras.md`), off by default: a small button by a
|
||||
//! menu's storage slots and one by the inventory's (an optional key sorts the part under the
|
||||
//! cursor). The clicks come from `inv_sort::plan` and go through the Mouse Tweaks' ordinary slot
|
||||
//! clicks (`mouse_tweaks.rs`), so they work wherever vanilla's do.
|
||||
use crate::game::Game;
|
||||
use horizon_client::{
|
||||
hud::{HudBatch, rgba},
|
||||
inv_sort,
|
||||
items::ItemStack,
|
||||
keybinds,
|
||||
menu::Input,
|
||||
stations::hover_name,
|
||||
};
|
||||
use winit::{event::MouseButton, keyboard::KeyCode};
|
||||
|
||||
/// A sort button's side in GUI pixels.
|
||||
const SORT_BUTTON: f32 = 9.0;
|
||||
|
||||
impl Game<'_> {
|
||||
/// The sort buttons of the open screen, with the extra on: their
|
||||
/// corner (from the screen's) and the slots each sorts. Any menu screen
|
||||
/// gets them, by its slots' roles (`native_slot_roles`): one by the top
|
||||
/// right of its storage slots (a chest's title line) when it has some,
|
||||
/// one by the main inventory's (the "Inventory" label's line).
|
||||
fn sort_buttons(&self) -> Vec<((f32, f32), Vec<usize>)> {
|
||||
use crate::game::NativeSlotRole as Role;
|
||||
if !self.settings.extras.sort {
|
||||
return Vec::new();
|
||||
}
|
||||
let roles = self.native_slot_roles();
|
||||
let of = |role| -> Vec<usize> {
|
||||
roles
|
||||
.iter()
|
||||
.filter(|(_, r)| *r == role)
|
||||
.map(|(s, _)| *s)
|
||||
.collect()
|
||||
};
|
||||
// Over the section's last column: (160, 5) over a chest's slots.
|
||||
let right = |slots: &[usize]| {
|
||||
slots
|
||||
.iter()
|
||||
.map(|&s| self.slot_position(s).0 + 8.0)
|
||||
.fold(f32::MIN, f32::max)
|
||||
};
|
||||
let mut buttons = Vec::new();
|
||||
let storage = of(Role::Storage);
|
||||
if !storage.is_empty() {
|
||||
let top = storage
|
||||
.iter()
|
||||
.map(|&s| self.slot_position(s).1 - 13.0)
|
||||
.fold(f32::MAX, f32::min);
|
||||
buttons.push(((right(&storage), top), storage));
|
||||
}
|
||||
// On the "Inventory" label's line (`inventoryLabelY`, height - 94, 12
|
||||
// above the first row): a mod's screen places its inventory itself.
|
||||
let main = of(Role::Inventory);
|
||||
if !main.is_empty() {
|
||||
let y = self
|
||||
.native_screen_inventory_y()
|
||||
.map_or(self.screen_height() - 95.0, |y| y - 13.0);
|
||||
buttons.push(((right(&main), y), main));
|
||||
}
|
||||
buttons
|
||||
}
|
||||
/// The sort buttons: a bordered square with three bars, lighter under
|
||||
/// the cursor.
|
||||
pub(in crate::game) fn draw_sort_buttons(&self, batch: &mut HudBatch, ox: f32, oy: f32) {
|
||||
let atlas = &self.gui.atlas;
|
||||
for ((x, y), _) in self.sort_buttons() {
|
||||
let (x, y) = (ox + x, oy + y);
|
||||
let (cx, cy) = self.cursor;
|
||||
let hovered = cx >= x && cx < x + SORT_BUTTON && cy >= y && cy < y + SORT_BUTTON;
|
||||
let face = if hovered {
|
||||
rgba(220, 220, 220, 255)
|
||||
} else {
|
||||
rgba(160, 160, 160, 255)
|
||||
};
|
||||
batch.fill(atlas, x, y, SORT_BUTTON, SORT_BUTTON, rgba(55, 55, 55, 255));
|
||||
batch.fill(
|
||||
atlas,
|
||||
x + 1.0,
|
||||
y + 1.0,
|
||||
SORT_BUTTON - 2.0,
|
||||
SORT_BUTTON - 2.0,
|
||||
face,
|
||||
);
|
||||
for (i, w) in [5.0, 4.0, 3.0].into_iter().enumerate() {
|
||||
let by = y + 2.0 + 2.0 * i as f32;
|
||||
batch.fill(atlas, x + 2.0, by, w, 1.0, rgba(55, 55, 55, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A left press on a sort button sorts its slots (true).
|
||||
pub(in crate::game) fn sort_press(&mut self) -> bool {
|
||||
let Some(r) = self.renderer.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let batch = HudBatch::new([r.config.width, r.config.height]);
|
||||
let (ox, oy) = self.inventory_origin(&batch);
|
||||
let (cx, cy) = self.cursor;
|
||||
let hit = self.sort_buttons().into_iter().find(|((x, y), _)| {
|
||||
let (x, y) = (ox + x, oy + y);
|
||||
cx >= x && cx < x + SORT_BUTTON && cy >= y && cy < y + SORT_BUTTON
|
||||
});
|
||||
let Some((_, slots)) = hit else {
|
||||
return false;
|
||||
};
|
||||
self.sort_slots(&slots);
|
||||
true
|
||||
}
|
||||
/// The sort key: sorts the part under the cursor (the container when
|
||||
/// the cursor is over no slot).
|
||||
pub(in crate::game) fn sort_key_press(&mut self, key: KeyCode) -> bool {
|
||||
keybinds::key_name(key).is_some_and(|name| self.sort_input(name))
|
||||
}
|
||||
pub(in crate::game) fn sort_mouse_press(&mut self, button: MouseButton) -> bool {
|
||||
self.sort_input(&keybinds::mouse_name(button))
|
||||
}
|
||||
fn sort_input(&mut self, name: &str) -> bool {
|
||||
let e = &self.settings.extras;
|
||||
if !e.sort || e.sort_key != name || self.recipe_search_focused() {
|
||||
return false;
|
||||
}
|
||||
let buttons = self.sort_buttons();
|
||||
let hovered = self.inventory_target().0;
|
||||
let slots = buttons
|
||||
.iter()
|
||||
.find(|(_, slots)| hovered.is_some_and(|h| slots.contains(&h)))
|
||||
.or(buttons.first())
|
||||
.map(|(_, slots)| slots.clone());
|
||||
let Some(slots) = slots else {
|
||||
return false;
|
||||
};
|
||||
self.sort_slots(&slots);
|
||||
true
|
||||
}
|
||||
/// Sorts `slots` with ordinary left clicks (needs an empty hand).
|
||||
fn sort_slots(&mut self, slots: &[usize]) {
|
||||
if self.carried.is_some() {
|
||||
return;
|
||||
}
|
||||
let views: Vec<Option<ItemStack>> = slots.iter().map(|&s| self.slot_view(s)).collect();
|
||||
let clicks = {
|
||||
let rank = |s: &ItemStack| {
|
||||
let order = self.creative_screen.order_of(s).unwrap_or(usize::MAX);
|
||||
(order, hover_name(&self.pack.names, s))
|
||||
};
|
||||
let max = |s: &ItemStack| self.max_stack(s);
|
||||
inv_sort::plan(&views, &rank, &max)
|
||||
};
|
||||
for i in clicks {
|
||||
self.tweak_click(slots[i], 0, Input::Pickup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ impl Game<'_> {
|
||||
let off_hand_empty = items[1].is_none();
|
||||
// Combat feel Legacy: a sword is held up while use is held, as 1.8.9's block looked.
|
||||
// Only the picture changes: the clicks and what they send stay 26.3's.
|
||||
let legacy_block = self.settings.extras.pvp.combat.legacy
|
||||
let legacy_block = self.legacy_pose()
|
||||
&& self.right
|
||||
&& self.screen == Screen::Playing
|
||||
&& items[0]
|
||||
@@ -426,7 +426,7 @@ impl Game<'_> {
|
||||
let mut m = to_world
|
||||
* horizon_client::first_person::item_pose(left, inverse_height, attack, pose);
|
||||
// PvP & HUD's shield size (100% is vanilla's), toward the hand holding it.
|
||||
let shield_size = self.settings.extras.pvp.tweaks.shield_size;
|
||||
let shield_size = self.shield_size();
|
||||
if shield_size != 100 && &*stack.item == "minecraft:shield" {
|
||||
m *= glam::Mat4::from_scale(glam::Vec3::splat(f32::from(shield_size) / 100.0));
|
||||
}
|
||||
@@ -449,7 +449,7 @@ impl Game<'_> {
|
||||
};
|
||||
let state = self.resolve_item_in_hand(&stack, context, Some(&owner), seed, cit_hand);
|
||||
// Combat feel Legacy: 1.8.9's pose for a flat item drawn with vanilla's display.
|
||||
if self.settings.extras.pvp.combat.legacy
|
||||
if self.legacy_pose()
|
||||
&& !state.layers.is_empty()
|
||||
&& state
|
||||
.layers
|
||||
|
||||
@@ -568,7 +568,7 @@ impl Game<'_> {
|
||||
self.hands_busy(),
|
||||
self.hand_strength.swap_scale(1.0, speed),
|
||||
changed,
|
||||
self.settings.extras.pvp.combat.legacy,
|
||||
self.legacy_pose(),
|
||||
);
|
||||
for h in 0..2 {
|
||||
if replace[h] {
|
||||
|
||||
@@ -477,7 +477,7 @@ impl Game<'_> {
|
||||
.store(settings.entity_distance.to_bits(), Ordering::Relaxed);
|
||||
flags
|
||||
.legacy_rules
|
||||
.store(settings.extras.pvp.combat.rules, Ordering::Relaxed);
|
||||
.store(self.pvp_legacy_rules(), Ordering::Relaxed);
|
||||
let (ready_tx, ready) = channel();
|
||||
let (tasks, server_tasks) = channel();
|
||||
let server_flags = flags.clone();
|
||||
@@ -1095,7 +1095,7 @@ impl Game<'_> {
|
||||
// IntegratedServer.getScaledTrackingDistance reads the option each time.
|
||||
self.settings.entity_distance =
|
||||
f32::from_bits(flags.entity_distance.load(Ordering::Relaxed));
|
||||
self.settings.extras.pvp.combat.rules = flags.legacy_rules.load(Ordering::Relaxed);
|
||||
self.set_pvp_legacy_rules(flags.legacy_rules.load(Ordering::Relaxed));
|
||||
let Some(host) = self.lan.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
//! drags, throws and the press/release of a slot click.
|
||||
use super::*;
|
||||
|
||||
/// What crates above the engine get of these screens (`horizon_hooks::containers`).
|
||||
mod containers;
|
||||
|
||||
/// Q throws the whole stack with Control down: the same modifier word the
|
||||
/// screens read (`has_control_down`), never a latched sprint toggle.
|
||||
pub(super) fn drop_whole_stack(modifiers: winit::keyboard::ModifiersState) -> bool {
|
||||
@@ -161,7 +164,7 @@ impl Game<'_> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.tweak_press(slot, right, shift) {
|
||||
if self.tweak_press(slot, right, shift) || self.container_press(right) {
|
||||
return;
|
||||
}
|
||||
if let Some(slot) = slot
|
||||
@@ -263,7 +266,9 @@ impl Game<'_> {
|
||||
}
|
||||
/// Mouse movement while a quick craft drag is running.
|
||||
pub(super) fn inventory_drag_move(&mut self) {
|
||||
if self.tweaks_on() && self.tweak_drag_move(self.inventory_target().0) {
|
||||
if self.tweaks_on() && self.tweak_drag_move(self.inventory_target().0)
|
||||
|| self.container_drag()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some((ty, slots)) = self.inv_drag.clone() else {
|
||||
@@ -426,6 +431,7 @@ impl Game<'_> {
|
||||
pub(super) fn inventory_mouse_release(&mut self, right: bool) {
|
||||
use horizon_client::container_screen::{Release, Target};
|
||||
self.tweak_release();
|
||||
self.container_release();
|
||||
let (slot, outside) = self.inventory_target();
|
||||
let dragged = self.inv_drag.as_ref().is_some_and(|(_, s)| !s.is_empty());
|
||||
let release = self.inv_clicks.release(
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
//! The engine's side of the container screen API (`horizon_hooks::containers`, docs/layers.md,
|
||||
//! "Container screens"): the open menu screen as a [`Container`] for the length of one hook call,
|
||||
//! and the input and drawing that reach the hooks. With no hook registered nothing here is built.
|
||||
use crate::game::{Game, NativeSlotRole, Screen};
|
||||
use horizon_client::{
|
||||
extras_hud, hud::HudBatch, items::ItemStack, keybinds, menu::Input, stations::hover_name,
|
||||
};
|
||||
use horizon_hooks::{
|
||||
containers::{Click, Container, Role, Slot, SlotId, Stack},
|
||||
engine,
|
||||
};
|
||||
use winit::{event::MouseButton, keyboard::KeyCode};
|
||||
|
||||
/// The open screen's menu, over the game for one call. `origin`: the panel's top left corner in
|
||||
/// GUI pixels.
|
||||
struct Open<'g, 'a> {
|
||||
game: &'g mut Game<'a>,
|
||||
origin: (f32, f32),
|
||||
}
|
||||
|
||||
/// The same, read only: what drawing gets.
|
||||
struct View<'g, 'a> {
|
||||
game: &'g Game<'a>,
|
||||
origin: (f32, f32),
|
||||
}
|
||||
|
||||
fn role(role: NativeSlotRole) -> Role {
|
||||
match role {
|
||||
NativeSlotRole::Hotbar => Role::Hotbar,
|
||||
NativeSlotRole::Inventory => Role::Inventory,
|
||||
NativeSlotRole::Storage => Role::Storage,
|
||||
NativeSlotRole::CraftingGrid | NativeSlotRole::Input => Role::Input,
|
||||
NativeSlotRole::Result => Role::Result,
|
||||
NativeSlotRole::Equipment => Role::Equipment,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stacks that merge (`same_item_same_components`) share a kind.
|
||||
fn kind(stack: &ItemStack) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
stack.item.hash(&mut h);
|
||||
stack.damage.hash(&mut h);
|
||||
format!("{:?}", stack.extra).hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn slot_id(slot: usize) -> SlotId {
|
||||
SlotId(u32::try_from(slot).unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
fn game_slot(slot: SlotId) -> usize {
|
||||
slot.0 as usize
|
||||
}
|
||||
|
||||
impl<'a> Game<'a> {
|
||||
/// The open menu's type: the server's, else the menu this window opened itself.
|
||||
fn container_menu(&self) -> &str {
|
||||
if let Some(menu) = self.remote_menu_type() {
|
||||
return menu;
|
||||
}
|
||||
use horizon_client::containers::BlockEntity;
|
||||
if self.mount_active() {
|
||||
return "horizon:mount";
|
||||
}
|
||||
if self.station.is_some() {
|
||||
return "horizon:station";
|
||||
}
|
||||
match self.container_kind() {
|
||||
None if self.craft_size == 3 => "minecraft:crafting",
|
||||
None => "inventory",
|
||||
Some(BlockEntity::Chest(_)) => "minecraft:generic_9x3",
|
||||
Some(BlockEntity::Furnace(_)) => "minecraft:furnace",
|
||||
Some(_) => "horizon:block",
|
||||
}
|
||||
}
|
||||
fn container_slots(&self) -> Vec<Slot> {
|
||||
self.native_slot_roles()
|
||||
.into_iter()
|
||||
.map(|(s, r)| {
|
||||
let (x, y) = self.slot_position(s);
|
||||
Slot {
|
||||
id: slot_id(s),
|
||||
role: role(r),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn container_stack(&self, stack: &ItemStack) -> Stack {
|
||||
Stack {
|
||||
item: stack.item.to_string(),
|
||||
count: stack.count,
|
||||
max: self.max_stack(stack),
|
||||
kind: kind(stack),
|
||||
}
|
||||
}
|
||||
/// The stack of `kind` the screen shows, on the cursor or in a slot.
|
||||
fn container_find(&mut self, of: &Stack) -> Option<ItemStack> {
|
||||
if let Some(c) = self.carried.as_ref().filter(|c| kind(c) == of.kind) {
|
||||
return Some(c.clone());
|
||||
}
|
||||
self.slots()
|
||||
.into_iter()
|
||||
.filter_map(|s| self.slot_view(s))
|
||||
.find(|s| kind(s) == of.kind)
|
||||
}
|
||||
/// The panel's top left corner, None without a window.
|
||||
fn container_origin(&self) -> Option<(f32, f32)> {
|
||||
let r = self.renderer.as_ref()?;
|
||||
Some(self.inventory_origin(&HudBatch::new([r.config.width, r.config.height])))
|
||||
}
|
||||
/// The open screen for the hooks' input, None when no hook may take it.
|
||||
fn container_open(&mut self) -> Option<Open<'_, 'a>> {
|
||||
if self.screen != Screen::Inventory || !engine::containers() {
|
||||
return None;
|
||||
}
|
||||
let origin = self.container_origin()?;
|
||||
Some(Open { game: self, origin })
|
||||
}
|
||||
fn hovered(&self) -> Option<SlotId> {
|
||||
self.inventory_target().0.map(slot_id)
|
||||
}
|
||||
/// A press over the panel: true when a hook took it, and vanilla's drag doesn't start.
|
||||
pub(in crate::game) fn container_press(&mut self, right: bool) -> bool {
|
||||
let slot = self.hovered();
|
||||
let taken = self
|
||||
.container_open()
|
||||
.is_some_and(|mut c| engine::container_press(&mut c, slot, right));
|
||||
if taken {
|
||||
self.inv_drag = None;
|
||||
self.inv_clicks.skip_release();
|
||||
}
|
||||
taken
|
||||
}
|
||||
/// The cursor moved with a button held: true when a hook owns the drag, which ends vanilla's.
|
||||
pub(in crate::game) fn container_drag(&mut self) -> bool {
|
||||
let slot = self.hovered();
|
||||
let taken = self
|
||||
.container_open()
|
||||
.is_some_and(|mut c| engine::container_drag(&mut c, slot));
|
||||
if taken {
|
||||
self.inv_drag = None;
|
||||
self.inv_clicks.skip_release();
|
||||
}
|
||||
taken
|
||||
}
|
||||
pub(in crate::game) fn container_release(&mut self) {
|
||||
if let Some(mut c) = self.container_open() {
|
||||
engine::container_release(&mut c);
|
||||
}
|
||||
}
|
||||
/// The wheel over an inventory screen: true when the Mouse Tweaks or a hook used it.
|
||||
pub(in crate::game) fn container_scroll(&mut self, steps: f32) -> bool {
|
||||
let slot = self.inventory_target().0;
|
||||
if self.tweak_scroll(steps, slot) {
|
||||
return true;
|
||||
}
|
||||
let slot = slot.map(slot_id);
|
||||
self.container_open()
|
||||
.is_some_and(|mut c| engine::container_wheel(&mut c, slot, steps))
|
||||
}
|
||||
/// A key the screen got: sorting's key, else a hook's.
|
||||
pub(in crate::game) fn container_key_press(&mut self, key: KeyCode) -> bool {
|
||||
self.sort_key_press(key)
|
||||
|| keybinds::key_name(key).is_some_and(|name| self.container_key(name))
|
||||
}
|
||||
/// A mouse button the screen got, as a key.
|
||||
pub(in crate::game) fn container_mouse_press(&mut self, button: MouseButton) -> bool {
|
||||
self.sort_mouse_press(button) || self.container_key(&keybinds::mouse_name(button))
|
||||
}
|
||||
fn container_key(&mut self, name: &str) -> bool {
|
||||
if self.recipe_search_focused() {
|
||||
return false;
|
||||
}
|
||||
let slot = self.hovered();
|
||||
self.container_open()
|
||||
.is_some_and(|mut c| engine::container_key(&mut c, slot, name))
|
||||
}
|
||||
/// Draws the sort buttons and the hooks' drawing over the panel at (`ox`, `oy`).
|
||||
pub(in crate::game) fn draw_container_extras(&self, batch: &mut HudBatch, ox: f32, oy: f32) {
|
||||
self.draw_sort_buttons(batch, ox, oy);
|
||||
if !engine::containers() {
|
||||
return;
|
||||
}
|
||||
let view = View {
|
||||
game: self,
|
||||
origin: (ox, oy),
|
||||
};
|
||||
extras_hud::draw_on_panel(batch, &self.gui.atlas, (ox, oy), |canvas| {
|
||||
engine::container_draw(&view, canvas);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// What both readings answer alike.
|
||||
macro_rules! reads {
|
||||
() => {
|
||||
fn menu(&self) -> &str {
|
||||
self.game.container_menu()
|
||||
}
|
||||
fn slots(&self) -> Vec<Slot> {
|
||||
self.game.container_slots()
|
||||
}
|
||||
fn carried(&self) -> Option<Stack> {
|
||||
let c = self.game.carried.as_ref()?;
|
||||
Some(self.game.container_stack(c))
|
||||
}
|
||||
fn spread(&self) -> usize {
|
||||
self.game.inv_drag.as_ref().map_or(0, |(_, s)| s.len())
|
||||
}
|
||||
fn cursor(&self) -> (f32, f32) {
|
||||
let (x, y) = self.game.cursor;
|
||||
(x - self.origin.0, y - self.origin.1)
|
||||
}
|
||||
fn shift(&self) -> bool {
|
||||
self.game.has_shift_down()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Container for Open<'_, '_> {
|
||||
reads!();
|
||||
fn stack(&mut self, slot: SlotId) -> Option<Stack> {
|
||||
let s = self.game.slot_view(game_slot(slot))?;
|
||||
Some(self.game.container_stack(&s))
|
||||
}
|
||||
fn takes_one(&mut self, slot: SlotId, of: &Stack) -> bool {
|
||||
self.game
|
||||
.container_find(of)
|
||||
.is_some_and(|s| self.game.tweak_takes_one(game_slot(slot), &s))
|
||||
}
|
||||
fn landing(&mut self, slot: SlotId) -> Vec<SlotId> {
|
||||
let landing = self.game.tweak_landing(game_slot(slot));
|
||||
landing.into_iter().map(slot_id).collect()
|
||||
}
|
||||
fn click(&mut self, slot: SlotId, click: Click) -> bool {
|
||||
let slot = game_slot(slot);
|
||||
match click {
|
||||
Click::Pick { right } => self.game.tweak_click(slot, i32::from(right), Input::Pickup),
|
||||
Click::QuickMove => self.game.tweak_click(slot, 0, Input::QuickMove),
|
||||
Click::Put { one } => self.game.tweak_put(slot, one),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn order(&mut self, slot: SlotId) -> (u32, String) {
|
||||
let Some(s) = self.game.slot_view(game_slot(slot)) else {
|
||||
return (u32::MAX, String::new());
|
||||
};
|
||||
let order = self.game.creative_screen.order_of(&s);
|
||||
let order = order.map_or(u32::MAX, |o| u32::try_from(o).unwrap_or(u32::MAX));
|
||||
(order, hover_name(&self.game.pack.names, &s))
|
||||
}
|
||||
}
|
||||
|
||||
/// Drawing gets `&dyn Container`, which can't call the `&mut self` reads.
|
||||
impl Container for View<'_, '_> {
|
||||
reads!();
|
||||
fn stack(&mut self, _: SlotId) -> Option<Stack> {
|
||||
None
|
||||
}
|
||||
fn takes_one(&mut self, _: SlotId, _: &Stack) -> bool {
|
||||
false
|
||||
}
|
||||
fn landing(&mut self, _: SlotId) -> Vec<SlotId> {
|
||||
Vec::new()
|
||||
}
|
||||
fn click(&mut self, _: SlotId, _: Click) -> bool {
|
||||
false
|
||||
}
|
||||
fn order(&mut self, _: SlotId) -> (u32, String) {
|
||||
(u32::MAX, String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use horizon_client::items::ItemStack;
|
||||
|
||||
fn stack(item: &str, count: u32, damage: u32) -> ItemStack {
|
||||
ItemStack {
|
||||
item: item.into(),
|
||||
count,
|
||||
damage,
|
||||
extra: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacks_that_merge_share_a_kind() {
|
||||
let a = kind(&stack("minecraft:stone", 1, 0));
|
||||
assert_eq!(
|
||||
a,
|
||||
kind(&stack("minecraft:stone", 64, 0)),
|
||||
"the count is no part"
|
||||
);
|
||||
assert_ne!(a, kind(&stack("minecraft:dirt", 1, 0)));
|
||||
let worn = stack("minecraft:iron_sword", 1, 3);
|
||||
assert_ne!(kind(&worn), kind(&stack("minecraft:iron_sword", 1, 0)));
|
||||
}
|
||||
}
|
||||
@@ -571,7 +571,7 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
// Combat feel Legacy draws a hit as 1.8.9 did: no sweep, no damage hearts.
|
||||
if self.settings.extras.pvp.combat.hides_particle(options.ty) {
|
||||
if self.particle_hidden(options.ty) {
|
||||
return;
|
||||
}
|
||||
let (d, s) = (p.dist.map(f64::from), p.max_speed.map(f64::from));
|
||||
|
||||
@@ -8,7 +8,7 @@ impl Game<'_> {
|
||||
/// Whether the hits this game resolves follow 1.8.9: legacy rules are on and this game
|
||||
/// is the server (its own server thread, a LAN host, a dedicated server).
|
||||
pub(super) fn legacy_rules(&self) -> bool {
|
||||
self.remote.is_none() && self.settings.extras.pvp.combat.rules
|
||||
self.remote.is_none() && self.pvp_legacy_rules()
|
||||
}
|
||||
|
||||
/// `melee_damage` under legacy rules: (damage, attack strength, critical, enchanted).
|
||||
|
||||
@@ -200,7 +200,7 @@ pub(super) fn loading_frame(hud: &HudBatch) -> Frame<'_> {
|
||||
outline: Vec::new(),
|
||||
outline_after_terrain: false,
|
||||
outline_high_contrast: false,
|
||||
pvp: Default::default(),
|
||||
look: Default::default(),
|
||||
debug_lines: &[],
|
||||
debug_lines_on_top: &[],
|
||||
debug_crosshair: &[],
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! What a crate above may change in how the game draws (`horizon_hooks::look`): the engine reads
|
||||
//! it here only, and each reading is vanilla's while nothing set a look. PvP & HUD sets it today
|
||||
//! (`game/extras/pvp.rs`).
|
||||
use super::{Dim, Game};
|
||||
use horizon_client::{chat::ChatQol, particles::ParticleType};
|
||||
use horizon_hooks::look::{Chat, Frame, engine::get as look};
|
||||
|
||||
impl Game<'_> {
|
||||
/// 1.8.9's pictures of a fight: the sword block, the flat item pose and the hand swing.
|
||||
pub(super) fn legacy_pose(&self) -> bool {
|
||||
look().legacy_pose
|
||||
}
|
||||
pub(super) fn attack_indicator_hidden(&self) -> bool {
|
||||
look().hide_attack_indicator
|
||||
}
|
||||
/// A sound a server sent that this player doesn't hear (`Look::mutes_sound`).
|
||||
pub(super) fn sound_muted(&self, name: &str) -> bool {
|
||||
look().mutes_sound(name)
|
||||
}
|
||||
/// A particle a server sent that this player doesn't see: with the attack particles hidden,
|
||||
/// the sweep and the damage hearts, which 1.9 added to a hit (1.8.9's show crits only).
|
||||
pub(super) fn particle_hidden(&self, ty: ParticleType) -> bool {
|
||||
let attack = matches!(
|
||||
ty,
|
||||
ParticleType::SweepAttack | ParticleType::DamageIndicator
|
||||
);
|
||||
attack && look().hide_attack_particles
|
||||
}
|
||||
/// The block outline, hurt overlay and glint colours the renderer draws with.
|
||||
pub(super) fn look_frame(&self) -> Frame {
|
||||
look().frame
|
||||
}
|
||||
/// The enchantment glint's colour texel, 0 for vanilla's.
|
||||
pub(super) fn glint_texel(&self) -> u32 {
|
||||
look().frame.glint
|
||||
}
|
||||
/// The held shield's size in percent (100 is vanilla's).
|
||||
pub(super) fn shield_size(&self) -> u8 {
|
||||
look().shield_size
|
||||
}
|
||||
/// The totem animation's scale (1 is vanilla's).
|
||||
pub(super) fn totem_scale(&self) -> f32 {
|
||||
look().totem_scale
|
||||
}
|
||||
/// How far the first-person fire overlay is lowered, as a fraction (0 is vanilla's).
|
||||
pub(super) fn fire_lower(&self) -> f32 {
|
||||
look().fire_lower
|
||||
}
|
||||
/// Lava's fog as with fire resistance, and water's as fully adapted.
|
||||
pub(super) fn clear_fluids(&self) -> (bool, bool) {
|
||||
let look = look();
|
||||
(look.clear_lava, look.clear_water)
|
||||
}
|
||||
/// The chat's extras: Ctrl+F search, copy on click, timestamps, repeats counted.
|
||||
pub(super) fn chat_look(&self) -> Chat {
|
||||
look().chat
|
||||
}
|
||||
/// What the chat lays its lines out with.
|
||||
pub(super) fn chat_qol(&self) -> ChatQol {
|
||||
let chat = look().chat;
|
||||
ChatQol {
|
||||
timestamps: chat.timestamps,
|
||||
compact: chat.compact,
|
||||
}
|
||||
}
|
||||
/// Whether vanilla's status effect icons are left out (`Hud.extractEffects`).
|
||||
pub(super) fn effect_icons_hidden(&self) -> bool {
|
||||
look().hide_effect_icons
|
||||
}
|
||||
/// Whether this window may draw its own time and weather: only a client of a server (single
|
||||
/// player's included) draws them, never the server that ticks the world.
|
||||
fn own_view(&self) -> bool {
|
||||
self.remote.is_some() && self.dim == Dim::Overworld
|
||||
}
|
||||
/// The day time the frame draws (sky, sun, moon, the sky's light): the look's time of day,
|
||||
/// else the world's. The world's own `day_time` and everything that plays by it stay.
|
||||
pub(super) fn view_day_time(&self) -> i64 {
|
||||
if !self.own_view() {
|
||||
return self.world.day_time;
|
||||
}
|
||||
look().day_time(self.world.day_time)
|
||||
}
|
||||
/// The rain and thunder levels the frame draws (rain, the sky's colour and light, fog): the
|
||||
/// look's weather, else the world's. Gameplay (riptide, mobs, crops) keeps the world's.
|
||||
pub(super) fn view_weather(&self, partial: f32) -> (f32, f32) {
|
||||
match look().weather {
|
||||
Some(levels) if self.own_view() && self.can_have_weather() => levels,
|
||||
_ => self.weather_levels(partial),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ impl Game<'_> {
|
||||
/// address isn't, even on this computer or the local network: an address can be a proxy, a
|
||||
/// tunnel or a friend's relay as well.
|
||||
fn direct_craft_free_server(&self) -> bool {
|
||||
self.server_is_lan()
|
||||
self.last_server.lan()
|
||||
|| self
|
||||
.remote
|
||||
.as_ref()
|
||||
|
||||
@@ -62,7 +62,7 @@ impl Game<'_> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if self.sort_key_press(key)
|
||||
if self.container_key_press(key)
|
||||
|| horizon_client::keybinds::key_name(key)
|
||||
.is_some_and(|name| self.hud_editor_key_press(name))
|
||||
{
|
||||
|
||||
@@ -979,7 +979,7 @@ impl Game<'_> {
|
||||
lan: bool,
|
||||
realm: bool,
|
||||
) -> Result<()> {
|
||||
self.realm_session = realm;
|
||||
self.last_server.realm = realm;
|
||||
self.reset_tutorial();
|
||||
self.perf.world_opening();
|
||||
// A server's own biomes come over the connection; the last local
|
||||
@@ -1027,7 +1027,7 @@ impl Game<'_> {
|
||||
root.clone(),
|
||||
),
|
||||
None => {
|
||||
self.extras_connecting(address, name, lan);
|
||||
self.last_server.connecting(address, name, lan);
|
||||
match cookies {
|
||||
Some(cookies) => Session::connect_transferred(
|
||||
address,
|
||||
@@ -1522,7 +1522,7 @@ impl Game<'_> {
|
||||
self.finish_perf();
|
||||
if reason.is_none() {
|
||||
// Leaving on purpose: nothing for auto-reconnect to retry.
|
||||
self.extras_left();
|
||||
self.last_server.left();
|
||||
}
|
||||
self.clear_waypoints();
|
||||
self.hud_disconnected();
|
||||
@@ -2288,7 +2288,7 @@ impl Game<'_> {
|
||||
uuid,
|
||||
} => {
|
||||
self.set_registries(registries);
|
||||
self.extras_joined();
|
||||
self.last_server.joined();
|
||||
// handleLogin: the level loading screen replaces any dialog.
|
||||
self.close_dialogs();
|
||||
if let Some(r) = self.remote.as_mut() {
|
||||
@@ -2430,9 +2430,9 @@ impl Game<'_> {
|
||||
self.connect_server_with_cookies(&address, &name, Some(cookies))?;
|
||||
return Ok(());
|
||||
}
|
||||
let retry = self.reconnect_after(&reason, key.as_deref(), from_server);
|
||||
let plan = self.last_server.ended(&reason, key.as_deref(), from_server);
|
||||
self.leave_server(Some(reason))?;
|
||||
self.offer_reconnect(retry);
|
||||
horizon_client::last_server::offer(self.title.as_mut(), plan);
|
||||
return Ok(());
|
||||
}
|
||||
Event::Packet(packet) => self.handle_packet(*packet)?,
|
||||
@@ -3873,7 +3873,7 @@ impl Game<'_> {
|
||||
C::Explode(p) => self.remote_explode(&p),
|
||||
C::Sound(p) => {
|
||||
if let Some(name) = self.remote_sound_name(&p.sound)
|
||||
&& !self.settings.extras.pvp.combat.mutes_sound(&name)
|
||||
&& !self.sound_muted(&name)
|
||||
{
|
||||
let at = [
|
||||
f64::from(p.x) / 8.0,
|
||||
@@ -3895,7 +3895,7 @@ impl Game<'_> {
|
||||
}
|
||||
C::SoundEntity(p) => {
|
||||
if let Some(name) = self.remote_sound_name(&p.sound)
|
||||
&& !self.settings.extras.pvp.combat.mutes_sound(&name)
|
||||
&& !self.sound_muted(&name)
|
||||
&& let Some(state) = self.remote_sound_entity_state(p.id)
|
||||
{
|
||||
let source = usize::try_from(p.source)
|
||||
@@ -6746,7 +6746,7 @@ impl Game<'_> {
|
||||
// (`KeyboardInput.tick`: 8 directions, full speed). The server only hears those keys
|
||||
// (`ServerboundPlayerInputPacket`), and an anticheat that predicts from them pulls back
|
||||
// a player moving at the stick's own angle and speed.
|
||||
if self.integrated.is_none() && self.server_address().is_some() {
|
||||
if self.integrated.is_none() && self.last_server.address().is_some() {
|
||||
keys.analog = None;
|
||||
}
|
||||
let [mut strafe, mut forward] = keys.direction();
|
||||
|
||||
@@ -164,8 +164,8 @@ impl Game<'_> {
|
||||
boss_world_fog: self.boss_overlay.should_create_world_fog(),
|
||||
spectator: self.spectator(),
|
||||
fire_resistance: self.potions.effects.get("fire_resistance").is_some()
|
||||
|| self.settings.extras.pvp.tweaks.clear_lava,
|
||||
full_water_vision: self.settings.extras.pvp.tweaks.clear_water,
|
||||
|| self.clear_fluids().0,
|
||||
full_water_vision: self.clear_fluids().1,
|
||||
night_vision: self.fog_night_vision(),
|
||||
environment: self.pack.polytone_colors.env,
|
||||
};
|
||||
@@ -405,7 +405,7 @@ impl Game<'_> {
|
||||
on_fire: self.camera_type.first_person()
|
||||
&& !self.spectator()
|
||||
&& self.player.as_ref().is_some_and(|p| p.fire_ticks > 0),
|
||||
fire_lower: f32::from(self.settings.extras.pvp.tweaks.fire_lower) / 100.0,
|
||||
fire_lower: self.fire_lower(),
|
||||
gui_overlays: &gui_overlays,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ impl Game<'_> {
|
||||
}
|
||||
/// The server's name: a local world's name, a server's entry in the list.
|
||||
fn social_server_name(&self) -> String {
|
||||
match (&self.integrated, self.server_address()) {
|
||||
match (&self.integrated, self.last_server.address()) {
|
||||
(Some(server), _) => server.world_name.clone(),
|
||||
(None, Some(address)) if self.remote.is_some() => address.to_owned(),
|
||||
_ => self.level.name.clone(),
|
||||
|
||||
@@ -515,7 +515,7 @@ impl<'a> Split<'a> {
|
||||
if self.host.integrated.is_some() || self.host.remote.is_none() {
|
||||
return None;
|
||||
}
|
||||
self.host.server_address().map(str::to_owned)
|
||||
self.host.last_server.address().map(str::to_owned)
|
||||
}
|
||||
|
||||
/// The pads' players who accepted, and the testing players (`HORIZON_SPLIT`, one a frame),
|
||||
|
||||
@@ -206,7 +206,7 @@ impl Game<'_> {
|
||||
to_world.transform_vector3(b).normalize(),
|
||||
);
|
||||
// PvP & HUD's totem animation size (100% is vanilla's), about the item's centre.
|
||||
let size = f32::from(self.settings.extras.pvp.tweaks.totem_size) / 100.0;
|
||||
let size = self.totem_scale();
|
||||
let pose = pose * glam::Mat4::from_scale(glam::Vec3::splat(size));
|
||||
self.submit_item_lit(&state, to_world * pose, vertex_light(0xFF), lights, out);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
//! Opening and closing a local world from the client: the title screen's actions, entering a
|
||||
//! world on the server thread, and saving and leaving it on exit.
|
||||
//! world on the server thread, and saving and leaving it on exit. Also connecting again to a
|
||||
//! server when an attempt a hook planned is due (`last_server.rs`).
|
||||
use super::*;
|
||||
|
||||
impl Game<'_> {
|
||||
/// Connects again once a planned attempt's countdown is over. An online-mode server gets the
|
||||
/// signed-in account again: `connect_server` takes it from `auth::current()`, held in memory
|
||||
/// for the whole run (the launcher's session file is gone after the first read).
|
||||
pub(super) fn reconnect_when_due(&mut self) -> Result<()> {
|
||||
if let Some(retry) = self.title.as_mut().and_then(|t| t.due_retry()) {
|
||||
self.connect_server_with_options(
|
||||
&retry.address,
|
||||
&retry.name,
|
||||
None,
|
||||
retry.lan,
|
||||
self.last_server.realm,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `onAbort`: the confirm screen's Back or Escape reopens the Data
|
||||
/// Packs screen with the selection it had.
|
||||
pub(super) fn reopen_data_packs(&mut self, event_loop: &dyn EventLoopCtl) -> Result<()> {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! The multiplayer server this window connected to last, which splitscreen players join and the
|
||||
//! friends service's presence names, and the connection's lifecycle as the hooks of the layers
|
||||
//! above hear it (`horizon_hooks`: connecting, joined, left, ended). A hook may plan another
|
||||
//! attempt when a connection ends; the Disconnected screen shows its countdown ([`offer`]).
|
||||
use crate::{multiplayer, net, title};
|
||||
use horizon_hooks::{Ended, Reconnect, Server, engine};
|
||||
|
||||
/// What the window knows of its last server. Forgotten when the player leaves it, or when a
|
||||
/// connection ends with no attempt planned.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LastServer {
|
||||
address: Option<String>,
|
||||
lan: bool,
|
||||
/// The open connection joined from Realms: `ServerData.isRealm` for the friends service's
|
||||
/// presence status.
|
||||
pub realm: bool,
|
||||
}
|
||||
|
||||
impl LastServer {
|
||||
/// A connection to a multiplayer server starts.
|
||||
pub fn connecting(&mut self, address: &str, name: &str, lan: bool) {
|
||||
self.address = Some(address.to_owned());
|
||||
self.lan = lan;
|
||||
engine::connecting(&Server::new(address, name, lan));
|
||||
}
|
||||
/// The server let the player in.
|
||||
pub fn joined(&self) {
|
||||
engine::joined();
|
||||
}
|
||||
/// The player left on purpose.
|
||||
pub fn left(&mut self) {
|
||||
self.forget();
|
||||
engine::left();
|
||||
}
|
||||
/// The connection ended: the attempt a hook plans, if any.
|
||||
pub fn ended(
|
||||
&mut self,
|
||||
reason: &str,
|
||||
key: Option<&str>,
|
||||
from_server: bool,
|
||||
) -> Option<Reconnect> {
|
||||
let plan = engine::ended(&Ended::new(reason, key, from_server));
|
||||
if plan.is_none() {
|
||||
self.forget();
|
||||
}
|
||||
plan
|
||||
}
|
||||
fn forget(&mut self) {
|
||||
self.address = None;
|
||||
self.lan = false;
|
||||
}
|
||||
/// The server's address, while the window keeps it.
|
||||
pub fn address(&self) -> Option<&str> {
|
||||
self.address.as_deref()
|
||||
}
|
||||
/// That server was found on the local network.
|
||||
pub fn lan(&self) -> bool {
|
||||
self.lan
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a planned attempt's countdown on the Disconnected screen.
|
||||
pub fn offer(title: Option<&mut title::Title>, plan: Option<Reconnect>) {
|
||||
let Some(plan) = plan else {
|
||||
return;
|
||||
};
|
||||
if let Some(title) = title
|
||||
&& let title::Mode::Multiplayer(multiplayer::Screen::Disconnected { retry, .. }) =
|
||||
&mut title.mode
|
||||
{
|
||||
log::info!(
|
||||
"Horizon: reconnecting to {} in {} s",
|
||||
net::shown_address(&plan.address),
|
||||
plan.seconds_left(std::time::Instant::now())
|
||||
);
|
||||
*retry = Some(plan);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_server_is_forgotten_when_left_or_lost_without_a_plan() {
|
||||
let mut last = LastServer::default();
|
||||
last.connecting("example.org", "Player", true);
|
||||
assert_eq!((last.address(), last.lan()), (Some("example.org"), true));
|
||||
// No hook plans an attempt in this test process.
|
||||
assert_eq!(last.ended("Timed out", None, false), None);
|
||||
assert_eq!((last.address(), last.lan()), (None, false));
|
||||
last.connecting("example.org:25566", "Player", false);
|
||||
last.left();
|
||||
assert_eq!(last.address(), None);
|
||||
}
|
||||
}
|
||||
@@ -140,8 +140,8 @@ pub enum Screen {
|
||||
},
|
||||
Disconnected {
|
||||
reason: String,
|
||||
/// Auto-reconnect's next attempt, until it is cancelled.
|
||||
retry: Option<crate::reconnect::Retry>,
|
||||
/// The next attempt a hook planned (`last_server`), until it is cancelled.
|
||||
retry: Option<horizon_hooks::Reconnect>,
|
||||
/// Wrapped reason lines, set by the last draw; the buttons sit below them.
|
||||
lines: std::cell::Cell<usize>,
|
||||
},
|
||||
@@ -1970,7 +1970,7 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
/// Auto-reconnect's attempt, once its countdown is over.
|
||||
pub fn due_retry(&mut self) -> Option<crate::reconnect::Retry> {
|
||||
pub fn due_retry(&mut self) -> Option<horizon_hooks::Reconnect> {
|
||||
let Self::Disconnected { retry, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -2,8 +2,18 @@
|
||||
//! connection may be retried, and when. Only lost connections and the
|
||||
//! server's own shutdown or restart are retried, never kicks, bans or
|
||||
//! refusals; attempts back off and stop after a few.
|
||||
//!
|
||||
//! It runs on the engine's connection hooks ([`HOOKS`], `horizon_hooks`): the engine
|
||||
//! reports a connection's start, join, leave and end, and shows and runs the attempt
|
||||
//! this plans. Nothing else in the engine reads it, so it leaves for a Horizon crate
|
||||
//! as a file move once its setting can leave too (docs/layers.md, plan M5).
|
||||
use horizon_hooks::{Ended, Hooks, Server, seat};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A planned attempt.
|
||||
pub use horizon_hooks::Reconnect as Retry;
|
||||
|
||||
/// Seconds before each attempt; there are no more after the last.
|
||||
pub const DELAYS: [u64; 5] = [5, 10, 20, 40, 60];
|
||||
/// Staying connected this long counts the next drop as a fresh one.
|
||||
@@ -111,43 +121,7 @@ pub struct Tracker {
|
||||
attempts: usize,
|
||||
joined: Option<Instant>,
|
||||
}
|
||||
/// A planned attempt.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Retry {
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub lan: bool,
|
||||
pub at: Instant,
|
||||
/// 1-based, of `DELAYS.len()`.
|
||||
pub attempt: usize,
|
||||
}
|
||||
impl Retry {
|
||||
/// Whole seconds left, rounded up.
|
||||
pub fn seconds_left(&self, now: Instant) -> u64 {
|
||||
let left = self.at.saturating_duration_since(now);
|
||||
left.as_secs() + u64::from(left.subsec_nanos() > 0)
|
||||
}
|
||||
pub fn due(&self, now: Instant) -> bool {
|
||||
now >= self.at
|
||||
}
|
||||
pub fn text(&self, now: Instant) -> String {
|
||||
format!(
|
||||
"Reconnecting in {} s (attempt {} of {})",
|
||||
self.seconds_left(now),
|
||||
self.attempt,
|
||||
DELAYS.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
impl Tracker {
|
||||
/// The server connected to last (splitscreen players join the same one).
|
||||
pub fn address(&self) -> Option<&str> {
|
||||
self.address.as_deref()
|
||||
}
|
||||
/// That server was found on the local network.
|
||||
pub fn lan(&self) -> bool {
|
||||
self.lan
|
||||
}
|
||||
/// A connection to `address` as `name` starts: a new address starts
|
||||
/// counting anew.
|
||||
pub fn connecting(&mut self, address: &str, name: &str, lan: bool) {
|
||||
@@ -196,6 +170,65 @@ impl Tracker {
|
||||
lan: self.lan,
|
||||
at: now + delay,
|
||||
attempt: self.attempts,
|
||||
attempts: DELAYS.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-reconnect on the engine's connection hooks: registered at startup with the other
|
||||
/// extras (`entry.rs`); it plans nothing while its setting is off. Splitscreen players share
|
||||
/// the hooks, so each seat (`horizon_hooks::seat`) has its own tracker and setting, as each
|
||||
/// player's game had before.
|
||||
pub static HOOKS: AutoReconnect = AutoReconnect {
|
||||
seats: Mutex::new(Vec::new()),
|
||||
};
|
||||
|
||||
pub struct AutoReconnect {
|
||||
seats: Mutex<Vec<Seat>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Seat {
|
||||
tracker: Tracker,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl AutoReconnect {
|
||||
/// The `autoReconnect` setting of the player whose turn it is.
|
||||
pub fn set_enabled(&self, on: bool) {
|
||||
self.with(|s| s.enabled = on);
|
||||
}
|
||||
fn with<T>(&self, f: impl FnOnce(&mut Seat) -> T) -> T {
|
||||
let mut seats = self.seats.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let i = seat();
|
||||
if seats.len() <= i {
|
||||
seats.resize_with(i + 1, Seat::default);
|
||||
}
|
||||
f(&mut seats[i])
|
||||
}
|
||||
}
|
||||
|
||||
impl Hooks for AutoReconnect {
|
||||
fn connecting(&self, server: &Server<'_>) {
|
||||
self.with(|s| {
|
||||
s.tracker
|
||||
.connecting(server.address, server.name, server.lan)
|
||||
});
|
||||
}
|
||||
fn joined(&self) {
|
||||
self.with(|s| s.tracker.joined(Instant::now()));
|
||||
}
|
||||
fn left(&self) {
|
||||
self.with(|s| s.tracker.reset());
|
||||
}
|
||||
fn ended(&self, end: &Ended<'_>) -> Option<Retry> {
|
||||
self.with(|s| {
|
||||
if !s.enabled {
|
||||
s.tracker.reset();
|
||||
return None;
|
||||
}
|
||||
s.tracker
|
||||
.ended(end.reason, end.key, end.from_server, Instant::now())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -458,6 +491,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_splitscreen_seat_keeps_its_own_attempts_and_setting() {
|
||||
use horizon_hooks::engine::set_seat;
|
||||
let hooks = AutoReconnect {
|
||||
seats: Mutex::new(Vec::new()),
|
||||
};
|
||||
let lost = Ended::new("Timed out", Some("disconnect.timeout"), false);
|
||||
for (seat, on) in [(0, false), (1, true)] {
|
||||
set_seat(seat);
|
||||
hooks.set_enabled(on);
|
||||
hooks.connecting(&Server::new("example.org", "Player", false));
|
||||
}
|
||||
// P2 has it on: an attempt in 5 s. P1 has it off: none, and P2's state stays.
|
||||
set_seat(1);
|
||||
assert_eq!(hooks.ended(&lost).map(|r| r.attempt), Some(1));
|
||||
set_seat(0);
|
||||
assert_eq!(hooks.ended(&lost), None);
|
||||
set_seat(1);
|
||||
hooks.connecting(&Server::new("example.org", "Player", false));
|
||||
assert_eq!(hooks.ended(&lost).map(|r| r.attempt), Some(2));
|
||||
// P2 leaving on purpose starts it over.
|
||||
hooks.left();
|
||||
hooks.connecting(&Server::new("example.org", "Player", false));
|
||||
assert_eq!(hooks.ended(&lost).map(|r| r.attempt), Some(1));
|
||||
set_seat(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_countdown_rounds_up() {
|
||||
let now = Instant::now();
|
||||
@@ -467,6 +527,7 @@ mod tests {
|
||||
lan: false,
|
||||
at: now + Duration::from_millis(4200),
|
||||
attempt: 2,
|
||||
attempts: DELAYS.len(),
|
||||
};
|
||||
assert_eq!(r.seconds_left(now), 5);
|
||||
assert_eq!(r.text(now), "Reconnecting in 5 s (attempt 2 of 5)");
|
||||
|
||||
@@ -4707,7 +4707,7 @@ Would you like to enable the Narrator or visit the Accessibility Settings?",
|
||||
}
|
||||
}
|
||||
/// The server auto-reconnect retries now, once its countdown is over.
|
||||
pub fn due_retry(&mut self) -> Option<crate::reconnect::Retry> {
|
||||
pub fn due_retry(&mut self) -> Option<horizon_hooks::Reconnect> {
|
||||
match &mut self.mode {
|
||||
Mode::Multiplayer(screen) => screen.due_retry(),
|
||||
_ => None,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Container screens (docs/layers.md, "Container screens"): input and drawing on an open container
|
||||
//! screen (the inventory, chests, furnaces, crafting tables...) for a crate above the engine, as
|
||||
//! Mouse Tweaks and sorting need. The engine implements [`Container`] for the length of one call
|
||||
//! of a hook (`Hooks::container_press` and the others); everything in it is plain data.
|
||||
|
||||
/// A slot of the open screen: opaque, stable while the screen is open.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SlotId(pub u32);
|
||||
|
||||
/// What a slot is for.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Role {
|
||||
/// The player's hotbar.
|
||||
Hotbar,
|
||||
/// The player's main inventory, above the hotbar.
|
||||
Inventory,
|
||||
/// A slot that only holds items: a chest's, a hopper's, a mount's chest.
|
||||
Storage,
|
||||
/// A slot a recipe or a furnace reads: a crafting grid's, a furnace's input and fuel.
|
||||
Input,
|
||||
/// A slot that only gives: a crafting or furnace result.
|
||||
Result,
|
||||
/// The player's armour and off hand, a mount's saddle and armour.
|
||||
Equipment,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// A slot drawn on the screen.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Slot {
|
||||
pub id: SlotId,
|
||||
pub role: Role,
|
||||
/// Its top left corner in GUI pixels, from the panel's.
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
/// The items in a slot or on the cursor.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Stack {
|
||||
/// The item's id (`minecraft:stone`).
|
||||
pub item: String,
|
||||
pub count: u32,
|
||||
/// How many one slot holds of it.
|
||||
pub max: u32,
|
||||
/// Equal for stacks of the same item with the same components: those merge.
|
||||
pub kind: u64,
|
||||
}
|
||||
|
||||
/// One ordinary slot click, as vanilla's screen sends it.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Click {
|
||||
/// Picks up, puts down or swaps (`PICKUP`), the left or right button.
|
||||
Pick { right: bool },
|
||||
/// The Shift click (`QUICK_MOVE`).
|
||||
QuickMove,
|
||||
/// Puts the cursor's stack, or one of it, in the slot as a one-slot drag (`QUICK_CRAFT`): a
|
||||
/// server whose cursor is already empty ignores it, where a click would pick up the slot.
|
||||
Put { one: bool },
|
||||
}
|
||||
|
||||
/// The open screen's menu, for the length of one call. The engine implements it. The reads that
|
||||
/// ask the menu's rules take `&mut self`, as the engine's do; drawing gets the others only.
|
||||
pub trait Container {
|
||||
/// Its menu type: `inventory` for the player's own, else the registry name the server opened
|
||||
/// (`minecraft:generic_9x3`, `minecraft:furnace`...).
|
||||
fn menu(&self) -> &str;
|
||||
/// Every slot drawn, in the screen's order, in every menu: a crate keeps to the menus it acts
|
||||
/// in, by [`Container::menu`] (Mouse Tweaks stays out of those whose slots cost or trigger
|
||||
/// something: anvils, enchanting, merchants, beacons, crafters). Creative's tabs are no
|
||||
/// container screen.
|
||||
fn slots(&self) -> Vec<Slot>;
|
||||
fn stack(&mut self, slot: SlotId) -> Option<Stack>;
|
||||
/// The stack on the cursor.
|
||||
fn carried(&self) -> Option<Stack>;
|
||||
/// Whether `slot` takes one more of `of`, by the menu's rules (`mayPlace`, the slot's limit).
|
||||
fn takes_one(&mut self, slot: SlotId, of: &Stack) -> bool;
|
||||
/// Where a Shift click on `slot` would put its items (`quickMoveStack`), first first.
|
||||
fn landing(&mut self, slot: SlotId) -> Vec<SlotId>;
|
||||
/// One ordinary click. False when nothing took it.
|
||||
fn click(&mut self, slot: SlotId, click: Click) -> bool;
|
||||
/// How many slots vanilla's drag of the cursor's stack spreads over now (0 while none runs).
|
||||
fn spread(&self) -> usize;
|
||||
/// The cursor, in GUI pixels from the panel's corner.
|
||||
fn cursor(&self) -> (f32, f32);
|
||||
/// Whether Shift is held.
|
||||
fn shift(&self) -> bool;
|
||||
/// The creative tabs' order of the stack in `slot` (`u32::MAX` when no tab lists it) and the
|
||||
/// name it shows: how sorting ranks it.
|
||||
fn order(&mut self, slot: SlotId) -> (u32, String);
|
||||
}
|
||||
@@ -17,14 +17,18 @@
|
||||
//! pages, [`hud`] for what it draws on the HUD.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod containers;
|
||||
pub mod hud;
|
||||
pub mod look;
|
||||
pub mod pages;
|
||||
mod registry;
|
||||
pub mod settings;
|
||||
|
||||
use containers::{Container, SlotId};
|
||||
use registry::Registry;
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Instant;
|
||||
|
||||
/// What a crate above the engine hears from the game. Every method has an empty default:
|
||||
/// implement only those you need. Hooks run on the game's thread (the render thread) unless a
|
||||
@@ -41,6 +45,49 @@ pub trait Hooks: Sync {
|
||||
|
||||
/// The game is ending normally: its window closed and the world is saved. Not after a crash.
|
||||
fn shutdown(&self) {}
|
||||
|
||||
/// This window starts connecting to a multiplayer server (the server list, a direct
|
||||
/// connection, a Realm, a LAN world). Not single player's own server.
|
||||
fn connecting(&self, _server: &Server<'_>) {}
|
||||
|
||||
/// The server let the player in.
|
||||
fn joined(&self) {}
|
||||
|
||||
/// The player left the server on purpose: nothing to retry.
|
||||
fn left(&self) {}
|
||||
|
||||
/// The connection ended without the player leaving it (lost, closed, kicked, refused). A hook
|
||||
/// may plan another attempt: the engine shows its countdown on the Disconnected screen, with
|
||||
/// Cancel, and connects again when it is due. Every hook hears the end; the first plan wins.
|
||||
fn ended(&self, _end: &Ended<'_>) -> Option<Reconnect> {
|
||||
None
|
||||
}
|
||||
|
||||
// An open container screen (`containers`): each input hook returns whether it took the
|
||||
// input, and then vanilla's handling of it doesn't run. The first hook that takes it wins.
|
||||
|
||||
/// A mouse button went down over the panel, on `slot` or none.
|
||||
fn container_press(&self, _c: &mut dyn Container, _slot: Option<SlotId>, _right: bool) -> bool {
|
||||
false
|
||||
}
|
||||
/// The cursor moved with a button held, now over `slot` or none.
|
||||
fn container_drag(&self, _c: &mut dyn Container, _slot: Option<SlotId>) -> bool {
|
||||
false
|
||||
}
|
||||
/// The button went up. Every hook hears it.
|
||||
fn container_release(&self, _c: &mut dyn Container) {}
|
||||
/// The wheel turned `steps` notches over `slot` or none (up is positive; a trackpad turns it
|
||||
/// in fractions).
|
||||
fn container_wheel(&self, _c: &mut dyn Container, _slot: Option<SlotId>, _steps: f32) -> bool {
|
||||
false
|
||||
}
|
||||
/// A key or a mouse button the screen got, by its name (`key.keyboard.r`, `key.mouse.middle`),
|
||||
/// with the cursor over `slot` or none.
|
||||
fn container_key(&self, _c: &mut dyn Container, _slot: Option<SlotId>, _key: &str) -> bool {
|
||||
false
|
||||
}
|
||||
/// Draws over the panel, from its top left corner, after the slots and before the tooltip.
|
||||
fn container_draw(&self, _c: &dyn Container, _canvas: &mut dyn hud::Canvas) {}
|
||||
}
|
||||
|
||||
/// What [`Hooks::started`] gets.
|
||||
@@ -56,6 +103,77 @@ impl<'a> Started<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`Hooks::connecting`] gets.
|
||||
#[non_exhaustive]
|
||||
pub struct Server<'a> {
|
||||
/// As the player gave it (`host[:port]`).
|
||||
pub address: &'a str,
|
||||
/// The player name the connection uses.
|
||||
pub name: &'a str,
|
||||
/// Found on the local network.
|
||||
pub lan: bool,
|
||||
}
|
||||
|
||||
impl<'a> Server<'a> {
|
||||
pub fn new(address: &'a str, name: &'a str, lan: bool) -> Self {
|
||||
Self { address, name, lan }
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`Hooks::ended`] gets.
|
||||
#[non_exhaustive]
|
||||
pub struct Ended<'a> {
|
||||
/// The reason shown on the Disconnected screen.
|
||||
pub reason: &'a str,
|
||||
/// Its translation key, when it has one (`disconnect.timeout`, `multiplayer.disconnect.kicked`).
|
||||
pub key: Option<&'a str>,
|
||||
/// The server sent it (a disconnect packet), rather than the connection failing.
|
||||
pub from_server: bool,
|
||||
}
|
||||
|
||||
impl<'a> Ended<'a> {
|
||||
pub fn new(reason: &'a str, key: Option<&'a str>, from_server: bool) -> Self {
|
||||
Self {
|
||||
reason,
|
||||
key,
|
||||
from_server,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Another attempt at a server, planned by [`Hooks::ended`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Reconnect {
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub lan: bool,
|
||||
/// When to connect.
|
||||
pub at: Instant,
|
||||
/// This attempt, 1-based, of `attempts`.
|
||||
pub attempt: usize,
|
||||
pub attempts: usize,
|
||||
}
|
||||
|
||||
impl Reconnect {
|
||||
/// Whole seconds left, rounded up.
|
||||
pub fn seconds_left(&self, now: Instant) -> u64 {
|
||||
let left = self.at.saturating_duration_since(now);
|
||||
left.as_secs() + u64::from(left.subsec_nanos() > 0)
|
||||
}
|
||||
pub fn due(&self, now: Instant) -> bool {
|
||||
now >= self.at
|
||||
}
|
||||
/// The Disconnected screen's countdown line.
|
||||
pub fn text(&self, now: Instant) -> String {
|
||||
format!(
|
||||
"Reconnecting in {} s (attempt {} of {})",
|
||||
self.seconds_left(now),
|
||||
self.attempt,
|
||||
self.attempts
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static HOOKS: Registry<dyn Hooks> = Registry::new();
|
||||
|
||||
/// Why [`register`] refused a hook.
|
||||
@@ -139,6 +257,79 @@ pub mod engine {
|
||||
hooks.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::connecting`].
|
||||
pub fn connecting(server: &Server<'_>) {
|
||||
for hooks in all() {
|
||||
hooks.connecting(server);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::joined`].
|
||||
pub fn joined() {
|
||||
for hooks in all() {
|
||||
hooks.joined();
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::left`].
|
||||
pub fn left() {
|
||||
for hooks in all() {
|
||||
hooks.left();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a container screen's input reaches a hook: false while none is registered, and
|
||||
/// the engine builds no `Container` then.
|
||||
pub fn containers() -> bool {
|
||||
!all().is_empty()
|
||||
}
|
||||
|
||||
/// [`Hooks::container_press`]: true when a hook took it.
|
||||
pub fn container_press(c: &mut dyn Container, slot: Option<SlotId>, right: bool) -> bool {
|
||||
all().iter().any(|h| h.container_press(c, slot, right))
|
||||
}
|
||||
|
||||
/// [`Hooks::container_drag`].
|
||||
pub fn container_drag(c: &mut dyn Container, slot: Option<SlotId>) -> bool {
|
||||
all().iter().any(|h| h.container_drag(c, slot))
|
||||
}
|
||||
|
||||
/// [`Hooks::container_release`], to every hook.
|
||||
pub fn container_release(c: &mut dyn Container) {
|
||||
for hooks in all() {
|
||||
hooks.container_release(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::container_wheel`].
|
||||
pub fn container_wheel(c: &mut dyn Container, slot: Option<SlotId>, steps: f32) -> bool {
|
||||
all().iter().any(|h| h.container_wheel(c, slot, steps))
|
||||
}
|
||||
|
||||
/// [`Hooks::container_key`].
|
||||
pub fn container_key(c: &mut dyn Container, slot: Option<SlotId>, key: &str) -> bool {
|
||||
all().iter().any(|h| h.container_key(c, slot, key))
|
||||
}
|
||||
|
||||
/// [`Hooks::container_draw`].
|
||||
pub fn container_draw(c: &dyn Container, canvas: &mut dyn crate::hud::Canvas) {
|
||||
for hooks in all() {
|
||||
hooks.container_draw(c, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::ended`]: every hook hears it, and the first plan is the one kept.
|
||||
pub fn ended(end: &Ended<'_>) -> Option<Reconnect> {
|
||||
let mut plan = None;
|
||||
for hooks in all() {
|
||||
let mine = hooks.ended(end);
|
||||
if plan.is_none() {
|
||||
plan = mine;
|
||||
}
|
||||
}
|
||||
plan
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -160,27 +351,126 @@ mod tests {
|
||||
fn published(&self, open: bool) {
|
||||
self.0.lock().unwrap().push(format!("published {open}"));
|
||||
}
|
||||
fn ended(&self, end: &Ended<'_>) -> Option<Reconnect> {
|
||||
self.0.lock().unwrap().push(format!("ended {}", end.reason));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Plans an attempt numbered by its field.
|
||||
struct Planner(usize);
|
||||
impl Hooks for Planner {
|
||||
fn ended(&self, end: &Ended<'_>) -> Option<Reconnect> {
|
||||
Some(Reconnect {
|
||||
address: end.reason.to_owned(),
|
||||
name: String::new(),
|
||||
lan: false,
|
||||
at: Instant::now(),
|
||||
attempt: self.0,
|
||||
attempts: 5,
|
||||
})
|
||||
}
|
||||
}
|
||||
static FIRST: Planner = Planner(1);
|
||||
static SECOND: Planner = Planner(2);
|
||||
|
||||
/// Implements nothing: the defaults do nothing.
|
||||
struct Silent;
|
||||
impl Hooks for Silent {}
|
||||
|
||||
static RECORDER: Recorder = Recorder(StdMutex::new(Vec::new()));
|
||||
|
||||
/// Takes its key on a container screen; notes what reached it.
|
||||
struct Keys(&'static str, StdMutex<Vec<String>>);
|
||||
impl Hooks for Keys {
|
||||
fn container_key(&self, _: &mut dyn Container, _: Option<SlotId>, key: &str) -> bool {
|
||||
self.1.lock().unwrap().push(format!("key {key}"));
|
||||
key == self.0
|
||||
}
|
||||
fn container_release(&self, _: &mut dyn Container) {
|
||||
self.1.lock().unwrap().push("release".into());
|
||||
}
|
||||
}
|
||||
static SORT: Keys = Keys("key.keyboard.r", StdMutex::new(Vec::new()));
|
||||
static ALSO_R: Keys = Keys("key.keyboard.r", StdMutex::new(Vec::new()));
|
||||
|
||||
/// A screen with nothing in it.
|
||||
struct Empty;
|
||||
impl Container for Empty {
|
||||
fn menu(&self) -> &str {
|
||||
"inventory"
|
||||
}
|
||||
fn slots(&self) -> Vec<containers::Slot> {
|
||||
Vec::new()
|
||||
}
|
||||
fn stack(&mut self, _: SlotId) -> Option<containers::Stack> {
|
||||
None
|
||||
}
|
||||
fn carried(&self) -> Option<containers::Stack> {
|
||||
None
|
||||
}
|
||||
fn takes_one(&mut self, _: SlotId, _: &containers::Stack) -> bool {
|
||||
false
|
||||
}
|
||||
fn landing(&mut self, _: SlotId) -> Vec<SlotId> {
|
||||
Vec::new()
|
||||
}
|
||||
fn click(&mut self, _: SlotId, _: containers::Click) -> bool {
|
||||
false
|
||||
}
|
||||
fn spread(&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn cursor(&self) -> (f32, f32) {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
fn shift(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn order(&mut self, _: SlotId) -> (u32, String) {
|
||||
(u32::MAX, String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_hooks_hear_the_game_and_late_ones_are_refused() {
|
||||
register(&RECORDER).unwrap();
|
||||
register(&Silent).unwrap();
|
||||
register(&FIRST).unwrap();
|
||||
register(&SECOND).unwrap();
|
||||
register(&SORT).unwrap();
|
||||
register(&ALSO_R).unwrap();
|
||||
engine::started(&Started::new("0.2.31"));
|
||||
// Changes only: the same state every frame is said once.
|
||||
for open in [false, true, true, true, false, false] {
|
||||
engine::published(open);
|
||||
}
|
||||
// Every hook hears the end; the first plan registered is the one kept.
|
||||
let plan = engine::ended(&Ended::new("lost", None, false)).unwrap();
|
||||
assert_eq!(plan.attempt, 1);
|
||||
// A container screen's key goes to the first hook that takes it, its release to all.
|
||||
assert!(engine::containers());
|
||||
assert!(!engine::container_key(&mut Empty, None, "key.keyboard.e"));
|
||||
assert!(engine::container_key(&mut Empty, None, "key.keyboard.r"));
|
||||
engine::container_release(&mut Empty);
|
||||
assert_eq!(
|
||||
*SORT.1.lock().unwrap(),
|
||||
["key key.keyboard.e", "key key.keyboard.r", "release"]
|
||||
);
|
||||
assert_eq!(
|
||||
*ALSO_R.1.lock().unwrap(),
|
||||
["key key.keyboard.e", "release"],
|
||||
"taken before it"
|
||||
);
|
||||
engine::shutdown();
|
||||
assert_eq!(
|
||||
*RECORDER.0.lock().unwrap(),
|
||||
["started 0.2.31", "published true", "published false"]
|
||||
[
|
||||
"started 0.2.31",
|
||||
"published true",
|
||||
"published false",
|
||||
"ended lost"
|
||||
]
|
||||
);
|
||||
assert_eq!(register(&Silent), Err(TooLate));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
//! How the engine draws a few things a crate above may change (docs/layers.md, plan M5): PvP &
|
||||
//! HUD's look today. Every field's default is vanilla's, so with nothing set the game looks and
|
||||
//! sounds as vanilla does.
|
||||
//!
|
||||
//! Plain data, pushed: a crate changes its own fields of the look of the player whose turn it is
|
||||
//! ([`update`]; splitscreen players each have theirs: [`crate::seat`]) when its settings load or
|
||||
//! change, and the engine reads it where it draws ([`engine::get`]). Several crates share the
|
||||
//! look, each keeping to its fields. Nothing is asked of a crate per frame.
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// What the engine reads. Built from `Look::default()`, then its fields set.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Look {
|
||||
/// 1.8.9's pictures of a fight: the sword block, the flat item pose and the hand swing.
|
||||
pub legacy_pose: bool,
|
||||
/// No attack indicator under the crosshair or beside the hotbar.
|
||||
pub hide_attack_indicator: bool,
|
||||
/// The `entity.player.attack.*` sounds a server sends are left out (1.9 added them with the
|
||||
/// attack cooldown; 1.8.9 played none on a hit).
|
||||
pub mute_attack_sounds: bool,
|
||||
/// The sweep and damage-heart particles a server sends are left out (1.9 added them).
|
||||
pub hide_attack_particles: bool,
|
||||
/// The block outline, hurt overlay and glint colours the renderer draws with.
|
||||
pub frame: Frame,
|
||||
/// The first-person shield's size, in percent.
|
||||
pub shield_size: u8,
|
||||
/// The totem animation's scale.
|
||||
pub totem_scale: f32,
|
||||
/// How far down the first-person fire overlay moves, as a fraction of its height.
|
||||
pub fire_lower: f32,
|
||||
/// Lava's fog as with Fire Resistance, without the effect.
|
||||
pub clear_lava: bool,
|
||||
/// Water's fog as after 30 seconds under water, from the first moment.
|
||||
pub clear_water: bool,
|
||||
pub chat: Chat,
|
||||
/// The status effect icons at the top right are left out (a crate shows the effects itself).
|
||||
pub hide_effect_icons: bool,
|
||||
/// The time of day this player sees, as `/time set` takes it (0 to 23999); `None` for the
|
||||
/// world's. Only what is drawn follows it, never what plays by the world's time.
|
||||
pub time_of_day: Option<i64>,
|
||||
/// The rain and thunder levels this player sees (0 to 1); `None` for the world's.
|
||||
pub weather: Option<(f32, f32)>,
|
||||
}
|
||||
impl Default for Look {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
legacy_pose: false,
|
||||
hide_attack_indicator: false,
|
||||
mute_attack_sounds: false,
|
||||
hide_attack_particles: false,
|
||||
frame: Frame::default(),
|
||||
shield_size: 100,
|
||||
totem_scale: 1.0,
|
||||
fire_lower: 0.0,
|
||||
clear_lava: false,
|
||||
clear_water: false,
|
||||
chat: Chat::default(),
|
||||
hide_effect_icons: false,
|
||||
time_of_day: None,
|
||||
weather: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Look {
|
||||
/// Whether the sound `name` a server sent is left out ([`Look::mute_attack_sounds`]).
|
||||
pub fn mutes_sound(&self, name: &str) -> bool {
|
||||
let name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
self.mute_attack_sounds && name.starts_with("entity.player.attack.")
|
||||
}
|
||||
/// The day time drawn while the world's is `world`: the same day (the moon's phase follows the
|
||||
/// world's), at [`Look::time_of_day`].
|
||||
pub fn day_time(&self, world: i64) -> i64 {
|
||||
match self.time_of_day {
|
||||
Some(t) => world.div_euclid(24000) * 24000 + t,
|
||||
None => world,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the renderer reads each frame.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Frame {
|
||||
/// The block outline's colour (`0xAARRGGBB`) and its stroke's scale; `None` for vanilla's.
|
||||
pub outline: Option<(u32, f32)>,
|
||||
/// The hurt overlay's texel, 0 for vanilla's.
|
||||
pub hurt_texel: u32,
|
||||
/// The enchantment glint's texel (RGB bytes then 0xFF, `unpack4x8unorm`'s order), 0 for
|
||||
/// vanilla's.
|
||||
pub glint: u32,
|
||||
}
|
||||
|
||||
/// The chat's extras. Each is off in vanilla.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Chat {
|
||||
/// The local time before each message.
|
||||
pub timestamps: bool,
|
||||
/// A message repeated in a row shows once with its count.
|
||||
pub compact: bool,
|
||||
/// A click on a message (where it has no click action) copies it.
|
||||
pub copy: bool,
|
||||
/// Ctrl+F in the chat searches its messages.
|
||||
pub search: bool,
|
||||
}
|
||||
|
||||
/// Each seat's look; a seat that never set one has vanilla's.
|
||||
static LOOKS: Mutex<Vec<Look>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Changes the look of the player whose turn it is: `change` sets the fields its crate owns and
|
||||
/// leaves the others' be.
|
||||
pub fn update(change: impl FnOnce(&mut Look)) {
|
||||
let mut looks = LOOKS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let seat = crate::seat();
|
||||
if looks.len() <= seat {
|
||||
looks.resize(seat + 1, Look::default());
|
||||
}
|
||||
change(&mut looks[seat]);
|
||||
}
|
||||
|
||||
/// The engine's side. Only the engine calls it.
|
||||
pub mod engine {
|
||||
use super::*;
|
||||
|
||||
/// The look of the player whose turn it is: vanilla's until something set one.
|
||||
pub fn get() -> Look {
|
||||
let looks = LOOKS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
looks.get(crate::seat()).copied().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn each_seat_sees_its_own_look_and_vanillas_until_set() {
|
||||
// Seats no other test plays, as the looks are shared.
|
||||
crate::engine::set_seat(5);
|
||||
assert_eq!(engine::get(), Look::default());
|
||||
update(|look| look.legacy_pose = true);
|
||||
// Another crate's change keeps the first one's.
|
||||
update(|look| look.clear_water = true);
|
||||
let look = engine::get();
|
||||
assert!(look.legacy_pose && look.clear_water);
|
||||
crate::engine::set_seat(4);
|
||||
assert_eq!(engine::get(), Look::default());
|
||||
crate::engine::set_seat(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attack_sounds_go_only_when_muted() {
|
||||
let mut look = Look::default();
|
||||
let attacks = [
|
||||
"minecraft:entity.player.attack.strong",
|
||||
"entity.player.attack.sweep",
|
||||
"minecraft:entity.player.attack.nodamage",
|
||||
];
|
||||
assert!(!attacks.iter().any(|name| look.mutes_sound(name)));
|
||||
look.mute_attack_sounds = true;
|
||||
assert!(attacks.iter().all(|name| look.mutes_sound(name)));
|
||||
// 1.8.9 had the hurt sound: it stays.
|
||||
assert!(!look.mutes_sound("minecraft:entity.player.hurt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_time_keeps_the_worlds_day() {
|
||||
// Day 9 at 5000: the moon's phase (the day number) stays, the time of day is the chosen.
|
||||
let world = 9 * 24000 + 5000;
|
||||
let mut look = Look::default();
|
||||
assert_eq!(look.day_time(world), world);
|
||||
look.time_of_day = Some(18000);
|
||||
assert_eq!(look.day_time(world), 9 * 24000 + 18000);
|
||||
look.time_of_day = Some(6000);
|
||||
assert_eq!(look.day_time(-100), -24000 + 6000);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,18 @@ pub mod pages {
|
||||
pub use horizon_hooks::pages::{Act, Label, Page, Place, Row, register};
|
||||
}
|
||||
|
||||
/// How the game draws a few things a crate may change, per player (`horizon_hooks::look`):
|
||||
/// vanilla's until set.
|
||||
pub mod look {
|
||||
pub use horizon_hooks::look::{Chat, Frame, Look, update};
|
||||
}
|
||||
|
||||
/// Input and drawing on an open container screen (docs/layers.md, "Container screens"): the
|
||||
/// `container_*` methods of [`hooks::Hooks`] get the screen as a [`containers::Container`].
|
||||
pub mod containers {
|
||||
pub use horizon_hooks::containers::{Click, Container, Role, Slot, SlotId, Stack};
|
||||
}
|
||||
|
||||
/// Drawing on the HUD (docs/layers.md, "Platform registries").
|
||||
pub mod hud {
|
||||
pub use horizon_hooks::hud::{Canvas, Corner, Layer, register};
|
||||
|
||||
+3
-3
@@ -14,9 +14,9 @@ again restores vanilla's behaviour completely.
|
||||
| Dynamic lights: item glow, nearest sources | `crates/horizon-client-render/src/dynamic_light.rs` (unit tested), `game/dynamic_lights.rs`, `terrain.wgsl` and `entity.wgsl` (`dynamic_block_light`) |
|
||||
| HUD drawing (a native mod's minimap, corner stacking) | `crates/horizon-client/src/extras_hud.rs` |
|
||||
| Minimap colours, tiles and image (for the Minimap native mod) | `crates/horizon-client-render/src/minimap.rs` |
|
||||
| Reconnect policy and backoff | `crates/horizon-client/src/reconnect.rs` |
|
||||
| Game glue (input hooks, per-frame update) | `crates/horizon-client/src/game/extras.rs` |
|
||||
| Mouse Tweaks-style controls, sort buttons | `crates/horizon-client/src/game/tweaks.rs` |
|
||||
| Reconnect policy and backoff | `crates/horizon-client/src/reconnect.rs`, a hook on the engine's connection lifecycle (`horizon_hooks`: `connecting`, `joined`, `left`, `ended`), registered in `entry.rs`; the engine shows and runs the attempt it plans (`last_server.rs`, `game/world_session.rs`); `game/extras/reconnect.rs` hands it the setting |
|
||||
| Game glue (input hooks, per-frame update) | `crates/horizon-client/src/game/extras.rs`; each extra's own glue in a child module, `game/extras/` |
|
||||
| Mouse Tweaks-style controls, sort buttons | `crates/horizon-client/src/game/extras/mouse_tweaks.rs`, `game/extras/sorting.rs` |
|
||||
| Sort order and its clicks | `crates/horizon-client/src/inv_sort.rs` (pure, unit tested) |
|
||||
| The music page's rows, and the pools it filters | `options_ui.rs` (`Page::Music`), `crates/horizon-client/src/music.rs` (`track_groups`, `MusicManager::set_off`), `sound_events.rs` (`sound_except`) |
|
||||
| Cosmetics (show mine, show others'), off with their flag | `extras.rs`, `crates/horizon-client/src/game/cosmetics.rs` ([cosmetics.md](cosmetics.md)) |
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ What changes in that build:
|
||||
|---|---|---|
|
||||
| Cosmetics, emotes and the emote wheel, "Show me as a Horizon player" | `game/cosmetics.rs` (+ `cosmetics/fx.rs`, `cosmetics/emotes.rs`, `cosmetics/emote_wheel.rs`), crate `horizon-cosmetics` | `game::cosmetics::enabled()`: the service is never asked, nothing is drawn, B opens no wheel |
|
||||
| The official server added to the server list | `multiplayer.rs` (`add_official_server`) | their call at startup (`game.rs`, `fn run`) |
|
||||
| Horizon Settings (Extras, PvP & HUD, Splitscreen, Studio, AI Agent, Discord) and Mods (with the registry browser) | `options_ui.rs` (`Page::Horizon` and below), `extras_ui.rs`, `extras.rs`, `pvp.rs`, `game/extras.rs`, `game/tweaks.rs`, `extras_hud.rs` | the Options screen shows vanilla's greyed Telemetry Data instead (`options_ui::HORIZON_EXTRAS`, set in `entry.rs`) |
|
||||
| Horizon Settings (Extras, PvP & HUD, Splitscreen, Studio, AI Agent, Discord) and Mods (with the registry browser) | `options_ui.rs` (`Page::Horizon` and below), `extras_ui.rs`, `extras.rs`, `pvp.rs`, `game/extras.rs` and `game/extras/`, `extras_hud.rs` | the Options screen shows vanilla's greyed Telemetry Data instead (`options_ui::HORIZON_EXTRAS`, set in `entry.rs`) |
|
||||
| Discord Rich Presence | `discord_presence.rs`, `game/discord.rs` | the `discord` feature, which `horizon-extras` turns on |
|
||||
| Play with Friends | `game/friends.rs`, `friends_screen.rs`, crate `horizon-friends` | the `friends` feature (`game/friends_off.rs` stands in) |
|
||||
| The anonymous "I'm playing" ping | crate `horizon-heartbeat` | not in horizon-client at all: only the official executable (`horizon-official`) registers it ([layers.md](layers.md#how-a-horizon-crate-plugs-in)) |
|
||||
|
||||
+7
-3
@@ -157,7 +157,7 @@ Horizon's own extras and its product, and every built-in address of an outside s
|
||||
- **Examples:** `horizon-addresses`, `horizon-cosmetics`, `horizon-friends`, `horizon-relay`, `horizon-telemetry`,
|
||||
`horizon-bench`, `horizon-release-sign`, `horizon-server`, `horizon-mod-registry`, `horizon-heartbeat`,
|
||||
`horizon-official`, `launcher/`. Still in engine crates today: `game/cosmetics.rs`, `game/emotes.rs`,
|
||||
`game/discord.rs`, `game/extras.rs`, `game/tweaks.rs`, `multiplayer.rs` (`add_official_server`), `extras_ui.rs`,
|
||||
`game/discord.rs`, `game/extras.rs` and `game/extras/`, `multiplayer.rs` (`add_official_server`), `extras_ui.rs`,
|
||||
`pvp.rs`.
|
||||
- **Rules:**
|
||||
- It uses the platform's public API only, never the engine.
|
||||
@@ -287,8 +287,12 @@ otherwise, and must return quickly. With nothing registered a dispatch is one lo
|
||||
build pays nothing.
|
||||
|
||||
The hooks today: `started` (once, when someone plays: not a dedicated server, a benchmark or an automated run),
|
||||
`published` (the world opened to or closed from LAN or friends, on change) and `shutdown` (the window closed, the
|
||||
world saved). **A hook you need and don't find:** add a method with an empty default to `Hooks` and a function in
|
||||
`published` (the world opened to or closed from LAN or friends, on change), `shutdown` (the window closed, the
|
||||
world saved), and a multiplayer connection's lifecycle: `connecting` (address, player name, LAN), `joined`, `left` (on
|
||||
purpose) and `ended` (reason, its translation key, whether the server sent it). `ended` may return a `Reconnect`
|
||||
(address, name, LAN, when, attempt of attempts): the engine shows its countdown on the Disconnected screen, with
|
||||
Cancel, and connects when it is due; every hook hears the end and the first plan wins. Auto-reconnect is built on
|
||||
them (`reconnect.rs`); the engine keeps the last server itself (`last_server.rs`). **A hook you need and don't find:** add a method with an empty default to `Hooks` and a function in
|
||||
`horizon_hooks::engine` that the engine calls where vanilla does the matching thing; give it plain data or a type
|
||||
the engine owns, never one from above. A hook that needs engine types the platform doesn't expose yet (a draw
|
||||
context for HUD and chat overlays, a settings page) comes with that type's platform API (M6). An extension point
|
||||
|
||||
+4
-3
@@ -21,7 +21,8 @@ Each feature is built from what it visibly does, never from another client's or
|
||||
|---|---|
|
||||
| Settings, parsing, the page's rows | `crates/horizon-client-render/src/pvp.rs` (stored in `horizon-extras.txt`, see [extras.md](extras.md)) |
|
||||
| The page | `options_ui.rs` (`Page::Pvp`), the rows of `pvp::rows()` drawn like the Extras page |
|
||||
| Game glue | `crates/horizon-client/src/game/pvp.rs` |
|
||||
| Game glue | `crates/horizon-client/src/game/extras/pvp.rs` |
|
||||
| What the engine draws differently | `horizon_hooks::look` (`Look`, PvP & HUD's fields set each frame from the options by `Pvp::apply`); the engine reads it only in `game/look.rs` |
|
||||
|
||||
## Crosshair (`crosshair`, default off)
|
||||
|
||||
@@ -53,7 +54,7 @@ Each feature is built from what it visibly does, never from another client's or
|
||||
|
||||
Boxes in the screen corners, placed like the minimap and mods' boxes (`extras_hud::Corners`), each in a corner of
|
||||
its own, with an optional dark background (on by default). F3's debug screen takes the top corners as it does from
|
||||
mods' boxes. Code: `crates/horizon-client-render/src/pvp_hud.rs` (lines, cache, layout) and `game/pvp.rs`.
|
||||
mods' boxes. Code: `crates/horizon-client-render/src/pvp_hud.rs` (lines, cache, layout) and `game/extras/pvp.rs`.
|
||||
|
||||
- **Armour** (bottom right): each piece worn, head first, with the uses left as a number or a percent, coloured
|
||||
like the item's durability bar (green to red). Only what the inventory already shows.
|
||||
@@ -136,7 +137,7 @@ There is no "no hurt camera" option: vanilla's Accessibility → Damage Tilt alr
|
||||
|
||||
The time of day (morning, noon, sunset, midnight) and the weather (clear, rain, thunder) this player sees. Only
|
||||
the frame changes: the sky's colours, sun, moon and stars, the sky's light in the lightmap, the rain and its
|
||||
fog (`Game::view_day_time`, `Game::view_weather` in `game/pvp.rs`). The world's `day_time` and weather, and all
|
||||
fog (`Game::view_day_time`, `Game::view_weather` in `game/look.rs`). The world's `day_time` and weather, and all
|
||||
that plays by them (mobs, crops, beds, riptide, rain sounds), stay the server's, and nothing is sent to it.
|
||||
The moon keeps the world's phase. Only a client of a server draws it (single player's window is one), in the
|
||||
Overworld.
|
||||
|
||||
Reference in new issue
Block a user