Compare commits

..
Author SHA1 Message Date
Horizon agent 536c0f938b Merge branch 'split/cosmetics-fx' into split/cosmetics-settings 2026-11-09 21:13:54 +01:00
Horizon agentandClaude Opus 5.5 e138304d2e Put the aura test's item at the feet
It reused the crown's 18-pixel attach offset, which an aura may not have
(at most 6 pixels above the feet), so the item failed to parse.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 21:10:39 +01:00
Horizon agentandClaude Opus 5.5 398bec3d6e Move the cosmetics settings out of the engine's Extras (spec #14091 step 5a)
The six cosmetics settings (my cosmetics, in single player, other players'
cosmetics, hide others' trails and auras, Horizon logos, show me as Horizon)
leave horizon-client-render's Extras for game/cosmetics/settings.rs:

- their horizon-extras.txt lines go through a settings Store (same keys, so
  players and the launcher's Wardrobe keep their choice; others' line still
  written only once chosen), always registered so a game without cosmetics
  keeps the Wardrobe's lines;
- their rows are a Place::Extras page section, shown while cosmetics are
  offered, in the same Cosmetics group at the end of the Extras page;
- the values live in the per-seat Seat, and the owner flag is read there
  instead of being copied into Extras each frame.

horizon-client-render no longer depends on horizon-cosmetics: the allowed
entry "horizon-client-render -> horizon-cosmetics" is removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:37:10 +01:00
Horizon agentandClaude Opus 5.5 4be9343eca Pass trail corners by value to to_array
Vec3::to_array takes &self, so it can't be handed to array::map directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:32:08 +01:00
Horizon agentandClaude Opus 5.5 03fd6705a1 Drop the unused shown binding in Seat::player_layers
worn_of decides what shows since the fx moved onto the hooks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:27:38 +01:00
Horizon agent a5a60598e4 Merge branch 'split/cosmetics-fx-api' into split/cosmetics-fx 2026-11-09 20:20:28 +01:00
Horizon agent 0fd4e22139 Merge branch 'split/cosmetics-own' into split/cosmetics-fx-api
# Conflicts:
#	crates/horizon-hooks/src/lib.rs
2026-11-09 20:20:25 +01:00
Horizon agent ec19b6320a Merge branch 'split/cosmetics-layers' into split/cosmetics-own 2026-11-09 20:20:01 +01:00
Horizon agentandClaude Opus 5.5 3d9df204e9 Cosmetics: companions, trails and auras through the hooks; the game's own path removed (spec #14091 step 2b-iii)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:08:05 +01:00
Horizon agentandClaude Opus 5.5 984b7d8635 Hooks: PlayerDraw gets motion, view, looks and particles, and first_person (spec #14091 step 2b-ii)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 20:02:47 +01:00
Horizon agentandClaude Opus 5.5 afa60454b2 Cosmetics: the own body and the inventory picture through PlayerDraw (spec #14091 step 2b-i)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 19:56:59 +01:00
15 changed files with 1061 additions and 1032 deletions

No files matched your search

Generated
-1
View File
@@ -1584,7 +1584,6 @@ dependencies = [
"horizon-client-input",
"horizon-command",
"horizon-core",
"horizon-cosmetics",
"horizon-hdr",
"horizon-hooks",
"horizon-items",
-1
View File
@@ -9,7 +9,6 @@ description = "The client's world data, rendering and screens: chunks, meshing,
[dependencies]
# The settings stores of the crates above the engine (docs/layers.md, "Platform registries").
horizon-hooks = { path = "../horizon-hooks" }
horizon-cosmetics = { path = "../horizon-cosmetics" }
# The GUI atlas's pixels, kept compressed once uploaded (hud.rs `pack_pixels`).
lz4_flex = { version = "0.14", default-features = false, features = ["std", "safe-encode", "safe-decode", "checked-decode"] }
horizon-assets = { path = "../horizon-assets" }
+9 -216
View File
@@ -168,24 +168,6 @@ pub struct Extras {
/// built-in track and leaving the off ones out (`musicPage`, off by
/// default: vanilla plays exactly as before).
pub music_page: bool,
/// Cosmetics (`horizon_cosmetics`, off unless enabled), the lines `horizon_cosmetics::prefs`
/// shares with the launcher's Wardrobe: Show my cosmetics (the account's, sent to the service;
/// on by default), how much of other players' to show (no line until chosen:
/// `effective_others`), and whether their trails and auras are hidden (off by default).
pub cosmetics_mine: bool,
pub cosmetics_others: Option<horizon_cosmetics::prefs::Others>,
pub cosmetics_hide_fx: bool,
/// My cosmetics in single player too (`cosmeticsSolo`, off by default): the window's own server
/// names the player by their offline profile, which the service doesn't know, so their own
/// cosmetics and name tag style show only with this on.
pub cosmetics_solo: bool,
/// Whether the signed-in account owns a cosmetic, as the service said (never saved; the game
/// sets it): with no choice made, others' cosmetics show only then.
pub cosmetics_owner: bool,
/// Horizon's mark before the names of other Horizon players (name tags, player list), and
/// whether the service may say this player plays Horizon: both on by default.
pub horizon_marks: bool,
pub horizon_listed: bool,
/// 3D skin layers (`threeDSkinLayers`, Options → Skin Customization): each
/// painted texel of a player's outer layer drawn as a slab
/// (`horizon_client::mobs::skin_layers`).
@@ -241,13 +223,6 @@ impl Default for Extras {
wheel_cycles_settings: false,
music_off: Vec::new(),
music_page: false,
cosmetics_mine: true,
cosmetics_others: None,
cosmetics_hide_fx: false,
cosmetics_solo: false,
cosmetics_owner: false,
horizon_marks: true,
horizon_listed: true,
three_d_skin_layers: false,
pvp: Default::default(),
}
@@ -257,11 +232,6 @@ fn bool_text(v: bool) -> &'static str {
if v { "true" } else { "false" }
}
impl Extras {
/// How much of other players' cosmetics shows: the player's choice, else all of it for an
/// account that owns a cosmetic and none for any other (a vanilla game).
pub fn effective_others(&self) -> horizon_cosmetics::prefs::Others {
horizon_cosmetics::prefs::Others::effective(self.cosmetics_others, self.cosmetics_owner)
}
pub fn load(dir: &Path) -> Self {
fs::read_to_string(dir.join(FILE))
.map(|t| Self::parse(&t))
@@ -357,15 +327,6 @@ impl Extras {
.collect()
}
"musicPage" => e.music_page = flag(e.music_page),
"cosmeticsMine" => e.cosmetics_mine = flag(e.cosmetics_mine),
"cosmeticsOthers" => {
e.cosmetics_others =
horizon_cosmetics::prefs::Others::parse(value).or(e.cosmetics_others)
}
"cosmeticsHideFx" => e.cosmetics_hide_fx = flag(e.cosmetics_hide_fx),
"cosmeticsSolo" => e.cosmetics_solo = flag(e.cosmetics_solo),
horizon_cosmetics::prefs::MARKS => e.horizon_marks = flag(e.horizon_marks),
horizon_cosmetics::prefs::LISTED => e.horizon_listed = flag(e.horizon_listed),
SKIN_LAYERS_KEY => e.three_d_skin_layers = flag(e.three_d_skin_layers),
key => {
e.pvp.parse_line(key, value);
@@ -376,7 +337,7 @@ impl Extras {
}
pub fn serialize(&self) -> String {
let mut text = format!(
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsHideFx:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\n{}:{}\n",
bool_text(self.pack_browser),
bool_text(self.pregen),
bool_text(self.fullbright),
@@ -426,16 +387,6 @@ impl Extras {
bool_text(self.wheel_cycles_settings),
self.music_off.join(","),
bool_text(self.music_page),
bool_text(self.cosmetics_mine),
// Written once the player chose.
self.cosmetics_others
.map_or(String::new(), |o| format!("cosmeticsOthers:{}\n", o.name())),
bool_text(self.cosmetics_hide_fx),
bool_text(self.cosmetics_solo),
horizon_cosmetics::prefs::MARKS,
bool_text(self.horizon_marks),
horizon_cosmetics::prefs::LISTED,
bool_text(self.horizon_listed),
SKIN_LAYERS_KEY,
bool_text(self.three_d_skin_layers),
);
@@ -623,13 +574,6 @@ pub enum Item {
/// The Select Resource Packs screen's Add button (Modrinth).
PackBrowser,
Pregen,
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
CosmeticsMine,
CosmeticsOthers,
CosmeticsHideFx,
CosmeticsSolo,
HorizonMarks,
HorizonMe,
/// On the PvP & HUD page (`pvp::rows`).
Pvp(crate::pvp::Opt),
}
@@ -637,8 +581,10 @@ pub enum Item {
pub fn rows() -> Vec<Result<Item, &'static str>> {
use Item::*;
// Fullbright, Dynamic lights and Hide block-break particles change the picture: they
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here.
let mut rows = vec![
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here. The Cosmetics
// group comes last, from the cosmetics' own section (`pages::Place::Extras`) while they are
// offered.
vec![
Err("horizon.extras.header.third_person"),
Ok(OwnNameTag),
Err("horizon.extras.header.inventory"),
@@ -659,19 +605,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
Err("horizon.extras.header.menus"),
Ok(WheelCyclesSettings),
Ok(PackBrowser),
];
if horizon_cosmetics::enabled() {
rows.extend([
Err("horizon.extras.header.cosmetics"),
Ok(CosmeticsMine),
Ok(CosmeticsSolo),
Ok(CosmeticsOthers),
Ok(CosmeticsHideFx),
Ok(HorizonMarks),
Ok(HorizonMe),
]);
}
rows
]
}
impl Item {
/// The language key of the caption (`horizon.*`, assets/horizon/lang).
@@ -698,12 +632,6 @@ impl Item {
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_settings",
Item::PackBrowser => "horizon.extras.pack_browser",
Item::Pregen => "horizon.extras.pregen",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo",
Item::HorizonMarks => "horizon.extras.horizon_marks",
Item::HorizonMe => "horizon.extras.horizon_me",
Item::Pvp(o) => o.caption_key(),
}
}
@@ -731,12 +659,6 @@ impl Item {
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_list_settings.tooltip",
Item::PackBrowser => "horizon.extras.pack_browser.tooltip",
Item::Pregen => "horizon.extras.pregen.tooltip",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine.tooltip",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others.tooltip",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx.tooltip",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo.tooltip",
Item::HorizonMarks => "horizon.extras.horizon_marks.tooltip",
Item::HorizonMe => "horizon.extras.horizon_me.tooltip",
Item::Pvp(o) => o.description_key(),
}
}
@@ -773,12 +695,6 @@ impl Item {
Item::WheelCyclesSettings => "Scroll wheel changes settings",
Item::PackBrowser => "Resource pack browser",
Item::Pregen => "Chunk pregeneration",
Item::CosmeticsMine => "My cosmetics",
Item::CosmeticsOthers => "Other players' cosmetics",
Item::CosmeticsHideFx => "Hide other players' trails and auras",
Item::CosmeticsSolo => "In single player",
Item::HorizonMarks => "Horizon logos",
Item::HorizonMe => "Show me as Horizon",
Item::Pvp(o) => o.caption(),
}
}
@@ -793,9 +709,7 @@ impl Item {
Pvp(o) => return o.parent().map(Pvp),
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
| AutoReconnect | NoAi | AgentControl | ReproKeys | DiscordPresence | Splitscreen
| ChatAnimation | WheelCyclesSettings | MusicPage | PackBrowser | CosmeticsMine
| CosmeticsOthers | CosmeticsHideFx | CosmeticsSolo | HorizonMarks | HorizonMe
| Pregen => {
| ChatAnimation | WheelCyclesSettings | MusicPage | PackBrowser | Pregen => {
return None;
}
})
@@ -856,24 +770,6 @@ impl Item {
Item::ReproKeys => {
"F3+R starts or stops a repro recording and F3+E writes its bundle. Off by default: a recording copies the world."
}
Item::CosmeticsMine => {
"Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe."
}
Item::CosmeticsOthers => {
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsHideFx => {
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
}
Item::CosmeticsSolo => {
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
}
Item::HorizonMarks => {
"Show the Horizon logo next to Horizon players: a small logo before the names of other players who use Horizon, above their heads and in the player list. Only Horizon players see it: servers receive nothing."
}
Item::HorizonMe => {
"Show me as a Horizon player: other Horizon players see the logo by your name. On, the service answers anyone who asks about you that you played Horizon in the last 3 hours; off, it never reports you."
}
Item::Pvp(o) => o.description(),
Item::WheelCyclesSettings => {
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
@@ -900,12 +796,6 @@ impl Item {
| Item::Splitscreen
| Item::WheelCyclesSettings
| Item::PackBrowser => Some(Cost::None),
Item::CosmeticsMine
| Item::CosmeticsOthers
| Item::CosmeticsHideFx
| Item::CosmeticsSolo
| Item::HorizonMarks
| Item::HorizonMe => Some(Cost::Low),
_ => None,
}
}
@@ -932,10 +822,6 @@ impl Item {
Item::WheelCyclesSettings => e.wheel_cycles_settings,
Item::PackBrowser => e.pack_browser,
Item::Pregen => e.pregen,
Item::CosmeticsHideFx => e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks,
Item::HorizonMe => e.horizon_listed,
Item::Pvp(o) => return o.switch(&e.pvp),
_ => return None,
})
@@ -945,30 +831,14 @@ impl Item {
match self {
Item::SortKey => crate::keybinds::display_name(&e.sort_key).plain(),
Item::Pvp(o) if o.switch(&e.pvp).is_none() => o.value(&e.pvp).unwrap_or_default(),
Item::CosmeticsMine => match e.cosmetics_mine {
true => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
false => crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden"),
},
Item::CosmeticsOthers => {
use horizon_cosmetics::prefs::Others;
match e.effective_others() {
Others::Shown => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
Others::NameTags => {
crate::lang::get_or("horizon.extras.cosmetics.name_tags", "Name tags only")
}
Others::Hidden => {
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
}
}
}
other => match other.switch(e) {
Some(true) => crate::lang::get_or("On", "On"),
_ => crate::lang::get_or("Off", "Off"),
},
}
}
/// A click (`back` for the right button: it turns the three-way cosmetics button the other
/// way; every other item left is a switch, so both flip it). The sort key binds instead.
/// A click (`back` for the right button: a PvP & HUD choice steps back; a switch flips
/// either way). The sort key binds instead.
pub fn cycle(self, e: &mut Extras, back: bool) {
match self {
Item::Fullbright => e.fullbright = !e.fullbright,
@@ -991,16 +861,6 @@ impl Item {
Item::WheelCyclesSettings => e.wheel_cycles_settings = !e.wheel_cycles_settings,
Item::PackBrowser => e.pack_browser = !e.pack_browser,
Item::Pregen => e.pregen = !e.pregen,
Item::CosmeticsMine => e.cosmetics_mine = !e.cosmetics_mine,
Item::CosmeticsOthers => {
// From what the button shows; the result is the player's choice from now on.
let o = e.effective_others();
e.cosmetics_others = Some(if back { o.next().next() } else { o.next() });
}
Item::CosmeticsHideFx => e.cosmetics_hide_fx = !e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo = !e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks = !e.horizon_marks,
Item::HorizonMe => e.horizon_listed = !e.horizon_listed,
Item::Pvp(o) => o.cycle(&mut e.pvp, back),
Item::SortKey => {}
}
@@ -1019,73 +879,6 @@ impl Item {
mod tests {
use super::*;
#[test]
fn other_players_cosmetics_cycle_three_ways_and_read_the_old_switch() {
use horizon_cosmetics::prefs::Others;
let mut e = Extras {
cosmetics_owner: true,
..Extras::default()
};
assert_eq!(
(e.effective_others(), e.cosmetics_hide_fx, e.cosmetics_mine),
(Others::Shown, false, true)
);
Item::CosmeticsOthers.cycle(&mut e, false);
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
Item::CosmeticsOthers.cycle(&mut e, false);
assert_eq!(e.cosmetics_others, Some(Others::Hidden));
// The right button goes back.
Item::CosmeticsOthers.cycle(&mut e, true);
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
// Ownership is the service's, never written.
assert_eq!(
Extras::parse(&e.serialize()),
Extras {
cosmetics_owner: false,
..e.clone()
}
);
// The launcher's Wardrobe writes the same lines.
assert_eq!(
horizon_cosmetics::prefs::Prefs::parse(&e.serialize()).others,
Some(Others::NameTags)
);
// Before this, the line was on or off.
assert_eq!(
Extras::parse("cosmeticsOthers:false").cosmetics_others,
Some(Others::Hidden)
);
}
#[test]
fn others_cosmetics_are_off_for_a_player_who_owns_none_until_chosen() {
use horizon_cosmetics::prefs::Others;
let mut e = Extras::default();
// Nothing chosen, nothing owned: the button shows Hidden and no line is written.
assert_eq!(e.effective_others(), Others::Hidden);
assert_eq!(
Item::CosmeticsOthers.value(&e),
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
);
assert!(!e.serialize().contains("cosmeticsOthers"));
// The service says the account owns one: shown, still not a choice.
e.cosmetics_owner = true;
assert_eq!(e.effective_others(), Others::Shown);
assert!(!e.serialize().contains("cosmeticsOthers"));
// A non-owner pressing the button goes on from Hidden, and that choice is kept.
e.cosmetics_owner = false;
Item::CosmeticsOthers.cycle(&mut e, false);
assert_eq!(e.cosmetics_others, Some(Others::Shown));
let saved = Extras::parse(&e.serialize());
assert_eq!(saved.effective_others(), Others::Shown);
// An owner who chose Hidden keeps it.
let hidden = Extras {
cosmetics_owner: true,
..Extras::parse("cosmeticsOthers:hidden\n")
};
assert_eq!(hidden.effective_others(), Others::Hidden);
}
#[test]
fn a_saved_wheel_setting_from_before_the_default_changed_is_dropped() {
assert!(!Extras::parse("wheelCyclesSettings:true\n").wheel_cycles_settings);
+2 -19
View File
@@ -202,14 +202,13 @@ impl Screen for ExtrasScreen {
fn note_lines(&self) -> usize {
NOTE_LINES
}
/// The rows read the extras, the mods, whether cosmetics are offered and the sections' rows.
/// The rows read the extras, the mods and the sections' rows.
fn stamp(&self, s: &Settings) -> u64 {
let mut added = Vec::new();
for (_, section) in sections() {
section.rows(&mut added);
}
let cosmetics = horizon_cosmetics::enabled();
shell::stamp_of(&(&s.extras, &s.mods, cosmetics, added))
shell::stamp_of(&(&s.extras, &s.mods, added))
}
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
let mut groups = vec![Group {
@@ -317,22 +316,6 @@ mod tests {
assert_ne!(s.extras.mouse_tweaks_wheel, before);
}
#[test]
fn the_three_way_cosmetics_row_turns_both_ways() {
use horizon_cosmetics::prefs::Others;
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::CosmeticsOthers);
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
assert_eq!(s.extras.effective_others(), Others::Hidden);
screen.press(&mut s, row, false);
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
}
/// The sort key binds instead of cycling; the wheel leaves it be.
#[test]
fn the_key_row_waits_for_a_key() {
+2 -2
View File
@@ -6773,8 +6773,8 @@ impl Game<'_> {
.extend(out.drain(player_start..));
}
self.mark_motion(out.len(), 0);
// Companions in first person, trails and auras (game/cosmetics/fx.rs).
self.cosmetics_fx_geometry(camera, alpha, &mut out);
// What crates draw around players: first person, see-through (game/player_hooks.rs).
self.players_after_geometry(camera, alpha, &mut out);
self.mark_entity(out.len(), "minecraft:evoker_fangs");
self.fangs_geometry(camera, alpha, &mut out);
self.block_item_display_geometry(camera, alpha, &mut out);
+202 -463
View File
@@ -10,21 +10,16 @@
pub(super) mod emote_wheel;
pub(super) mod emotes;
mod fx;
mod settings;
use super::Game;
use horizon_client::fast_hash::FxHashMap;
use horizon_client::mesher::Vertex;
use horizon_client::mobs::anim::Poses;
use horizon_client::mobs::{Mob, Part, Polygon};
use horizon_cosmetics::follow::Follow;
use horizon_cosmetics::prefs::{self, Others};
use horizon_cosmetics::trail::{Budget, Emitter};
use horizon_cosmetics::{Item, Slot};
use horizon_hooks::players::{
Badge, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Mark, Mesh, NameDecoration,
PlayerDraw, PlayerModel, PlayerModels, Quad, SpriteStore, Sprites,
Badge, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Look, Mark, Mesh, NameDecoration,
Pass, PlayerDraw, PlayerModel, PlayerModels, Quad, SpriteStore, Sprites,
};
use std::cell::{Cell, RefCell};
use std::sync::mpsc::{Receiver, channel};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
@@ -32,46 +27,10 @@ use std::time::{Duration, Instant};
/// Horizon's mark before a Horizon player's name, over their name tag and in the player list.
const MARK: Mark = &horizon_cosmetics::mark::MARK;
/// An item as the renderer takes it.
struct Baked {
item: Arc<Item>,
/// A model per bone it rides (a shirt's sleeves are on the arms).
models: Vec<(horizon_cosmetics::Bone, Arc<Part>)>,
/// Its texture's sprite (a 64×64 skin slot of the atlas).
location: String,
/// The frame it was last marked drawn in (its slot is kept).
marked: Cell<u32>,
}
impl Baked {
fn new(item: &Arc<Item>, frame: u32) -> Self {
Self {
item: item.clone(),
models: item
.roots()
.map(|(bone, root)| (bone, Arc::new(part(root))))
.collect(),
location: format!("cosmetics/{:x}", Arc::as_ptr(item) as usize),
marked: Cell::new(frame.wrapping_sub(1)),
}
}
}
#[derive(Default)]
pub(super) struct Cosmetics {
/// The hooks' client (`Seat`), taken once it started.
client: Option<Arc<horizon_cosmetics::Cosmetics>>,
/// By the item's address; each entry holds its item, so the address isn't reused.
baked: RefCell<FxHashMap<usize, Baked>>,
/// Items drawn whose texture isn't in the atlas yet.
uploads: RefCell<Vec<Arc<Item>>>,
frame: Cell<u32>,
/// Kept between players so a frame of avatars allocates none: the wearer's body, an item.
poses: RefCell<(Poses, Poses)>,
/// Item files from a folder (`HORIZON_COSMETICS_LOCAL`): mannequins wear them too.
local: bool,
/// Companions, trails and auras.
fx: RefCell<Fx>,
/// Emotes: those the wheel offers and the one playing (`cosmetics/emotes.rs`).
emotes: emotes::Emotes,
/// The emote wheel (`cosmetics/emote_wheel.rs`).
@@ -130,49 +89,6 @@ fn load_owned_emotes(
)
}
/// The key of the player's own model in `Fx`'s maps (others go by entity id).
const OWN: u32 = u32::MAX;
/// Full light, for what glows.
const FULL_LIGHT: u32 = 240 | 240 << 8;
/// What companions and trails keep between frames, per wearer (marked with the frame they were
/// last drawn in; the others are dropped), and the frame's trails and auras, drawn after the
/// players (`cosmetics_fx_geometry`).
#[derive(Default)]
struct Fx {
companions: FxHashMap<u32, (Follow, u32)>,
trails: FxHashMap<u32, (Emitter, u32)>,
budget: Budget,
clock: Option<(Instant, Instant)>,
/// Ticks since the first frame, and the last frame's length in seconds.
now: f64,
dt: f32,
/// Trails and auras: see-through.
clear: Vec<Vertex>,
}
impl Fx {
/// A new frame: the clock moves on and what no frame drew since the last one goes.
fn frame(&mut self, frame: u32) {
let now = Instant::now();
let (start, last) = self.clock.get_or_insert((now, now));
self.dt = now.duration_since(*last).as_secs_f32().min(0.25);
self.now = now.duration_since(*start).as_secs_f64() * 20.0;
*last = now;
let seen = |mark: u32| frame.wrapping_sub(mark) <= 1;
self.companions.retain(|_, (_, mark)| seen(*mark));
let budget = &mut self.budget;
self.trails.retain(|_, (e, mark)| {
let keep = seen(*mark);
if !keep {
e.clear(budget);
}
keep
});
self.clear.clear();
}
}
/// Whether the signed-in account owns a cosmetic: the answer kept from the last run first
/// (`prefs::owned`), then the service's, asked once a run and retried after a failure like Show my
/// cosmetics. Not known yet, or no session: not owning.
@@ -329,20 +245,13 @@ impl Cosmetics {
}
}
/// What the player whose UUID `uuid` gives wears, if the game shows it: the player's own model
/// (`own`) while Show my cosmetics is on, another player unless others' cosmetics are hidden (by
/// choice, or by owning none). Hidden, the service is never asked about them.
/// What the player whose UUID `uuid` gives wears, if the game shows it (`shown`:
/// [`Seat::shows`]). Hidden, the service is never asked about them.
fn worn_of(
client: &horizon_cosmetics::Cosmetics,
extras: &horizon_client::extras::Extras,
own: bool,
shown: bool,
uuid: impl FnOnce() -> Option<u128>,
) -> Option<horizon_cosmetics::Worn> {
let shown = if own {
extras.cosmetics_mine
} else {
extras.effective_others() != Others::Hidden
};
if !shown {
return None;
}
@@ -369,24 +278,6 @@ fn own_uuid(
/// How often a looping emote is said again (the service keeps one a minute).
const LOOP_AGAIN: Duration = Duration::from_secs(30);
/// The game's model of an item's part.
fn part(p: &horizon_cosmetics::Part) -> Part {
Part {
name: p.name.clone(),
pose: p.pose,
polygons: p
.faces
.iter()
.map(|f| Polygon {
normal: f.normal,
vertices: f.vertices,
cull_back: false,
})
.collect(),
children: p.children.iter().map(part).collect(),
}
}
/// The texture in the top-left corner of a skin slot's 64×64 RGBA.
fn slot_image(item: &Item) -> Vec<u8> {
let t = &item.texture;
@@ -468,6 +359,10 @@ struct Seat {
last_sprite: u64,
/// Sprites drawing missed, put in the atlas next frame.
wanted: Vec<u64>,
/// Companions, trails and auras (`cosmetics/fx.rs`).
fx: fx::Fx,
/// The player's settings (`cosmetics/settings.rs`).
settings: settings::Settings,
}
/// An item the hooks show: its sprite's id (never reused) and a mesh per bone it rides, held
@@ -502,20 +397,10 @@ fn bone(b: horizon_cosmetics::Bone) -> Bone {
}
}
/// What the game tells the hooks before each call, until the settings move here (spec #14091
/// step 5a) and the player's emote with them (step 4).
/// What the game tells the hooks before each call: the player's emote, until it moves here
/// (spec #14091 step 4).
#[derive(Default)]
struct Said {
mine: bool,
listed: bool,
/// Other players' cosmetics are shown (not "name tags only", not hidden).
others_shown: bool,
/// Other players' cosmetics are hidden (not even name tag styles).
others_hidden: bool,
/// Horizon's mark by Horizon players' names.
marks: bool,
/// Cosmetics in single player: the signed-in account's on the player's own model.
solo: bool,
/// My emote pulled the camera back: when it started (the own model's age).
pull: Option<f32>,
/// My emote while it plays: its id and whether it loops.
@@ -538,14 +423,26 @@ fn service<R>(f: impl FnOnce(&mut Seat) -> R) -> R {
}
/// The cosmetics' hooks, registered when [`enabled`].
pub(super) struct Hooks;
pub(super) static HOOKS: Hooks = Hooks;
struct Hooks;
static HOOKS: Hooks = Hooks;
/// Registers the cosmetics' settings lines, and while they are [`enabled`] their hooks and their
/// rows on the Extras page. The lines always: a game without cosmetics keeps the Wardrobe's.
pub(super) fn register() -> Result<(), horizon_hooks::TooLate> {
horizon_hooks::settings::register(&settings::STORE)?;
if enabled() {
horizon_hooks::register(&HOOKS)?;
horizon_hooks::pages::register(&settings::SECTION)?;
}
Ok(())
}
impl horizon_hooks::Hooks for Hooks {
fn frame(&self, frame: &Frame, sprites: &mut dyn SpriteStore) {
service(|s| {
s.solo = frame.solo;
s.frame();
s.fx.frame();
s.put_sprites(sprites);
});
}
@@ -564,7 +461,7 @@ impl horizon_hooks::Hooks for Hooks {
fn player_mark(&self, uuid: u128) -> Option<Mark> {
service(|s| {
let client = s.client.as_ref().filter(|_| s.said.marks)?;
let client = s.client.as_ref().filter(|_| s.settings.marks)?;
client.frame().horizon(uuid).then_some(MARK)
})
}
@@ -574,6 +471,11 @@ impl horizon_hooks::Hooks for Hooks {
service(|s| s.player_layers(model, draw));
}
/// The player's own companion in first person.
fn first_person(&self, own: &PlayerModel, draw: &mut dyn PlayerDraw) {
service(|s| s.first_person(own, draw));
}
fn camera(&self, own: &CameraView) -> Option<CameraOverride> {
let start = service(|s| s.said.pull)?;
let t = ((own.age - start) / 5.0).clamp(0.0, 1.0);
@@ -582,6 +484,22 @@ impl horizon_hooks::Hooks for Hooks {
}
impl Seat {
/// How much of other players' cosmetics shows: the player's choice, else all of it for an
/// account that owns a cosmetic and none for any other (a vanilla game).
fn others(&self) -> Others {
Others::effective(self.settings.others, self.owner.owns)
}
/// Whether the cosmetics of the player's own model (`own`) or of another player show at all:
/// mine with Show my cosmetics on, others' unless hidden.
fn shows(&self, own: bool) -> bool {
if own {
self.settings.mine
} else {
self.others() != Others::Hidden
}
}
/// Once a frame: starts the service's client on first use, asks whether the account owns a
/// cosmetic, and tells the service what the account shows.
fn frame(&mut self) {
@@ -654,7 +572,9 @@ impl Seat {
// My emote: only while I show myself as a Horizon player (playing one says so).
let playing = said.playing.as_ref();
let looping = playing.is_some_and(|(_, looping)| *looping);
let wish = playing.map(|(id, _)| id.as_str()).filter(|_| said.listed);
let wish = playing
.map(|(id, _)| id.as_str())
.filter(|_| self.settings.listed);
let o = &mut self.emote_out;
let changed = o.sent.as_ref().map(Option::as_deref) != Some(wish);
let again = looping && o.again.is_some_and(|at| Instant::now() >= at);
@@ -678,10 +598,10 @@ impl Seat {
let _ = tx.send(send_emote(&kept, wish.as_deref()));
})
.ok();
} else if self.mine.due(said.mine) {
self.mine.start(said.mine, send_mine);
} else if self.listed.due(said.listed) {
self.listed.start(said.listed, send_listed);
} else if self.mine.due(self.settings.mine) {
self.mine.start(self.settings.mine, send_mine);
} else if self.listed.due(self.settings.listed) {
self.listed.start(self.settings.listed, send_listed);
}
}
@@ -693,18 +613,17 @@ impl Seat {
m: &PlayerModel,
sprites: &dyn Sprites,
) -> Option<NameDecoration> {
let said = &self.said;
let own = m.kind == Kind::Own;
let styles = if own { said.mine } else { !said.others_hidden };
if !(styles || said.marks) {
let (styles, marks) = (self.shows(own), self.settings.marks);
if !(styles || marks) {
return None;
}
let uuid = self.uuid_of(m)?;
let mut frame = self.client.as_ref()?.frame();
let mark = (said.marks && frame.horizon(uuid)).then_some(MARK);
let mark = (marks && frame.horizon(uuid)).then_some(MARK);
let worn = if styles { frame.worn(uuid) } else { None };
drop(frame);
let others = if said.others_shown {
let others = if self.others() == Others::Shown {
Others::Shown
} else {
Others::NameTags
@@ -718,7 +637,7 @@ impl Seat {
/// The UUID the service knows `m` by: a player's, or a mannequin's id with local items.
fn uuid_of(&self, m: &PlayerModel) -> Option<u128> {
match m.kind {
Kind::Own => own_uuid(m.uuid, self.solo, self.said.solo, || {
Kind::Own => own_uuid(m.uuid, self.solo, self.settings.solo, || {
horizon_client::auth::current().map(|s| s.uuid)
}),
Kind::Mannequin if self.local => Some(u128::from(m.id)),
@@ -782,34 +701,37 @@ impl Seat {
self.items.retain(|_, k| Arc::strong_count(&k.item) > 1);
}
/// The cosmetics `m` wears, on its bones, and an emote's props (companions, trails and auras
/// are still the game's: `cosmetics_geometry`).
/// The cosmetics `m` wears, on its bones and around it in the world (companions, trails,
/// auras: `cosmetics/fx.rs`), and an emote's props.
fn player_layers(&mut self, m: &PlayerModel, draw: &mut dyn PlayerDraw) {
if m.invisible {
return;
}
let said = &self.said;
let own = m.kind == Kind::Own;
let shown = if own { said.mine } else { !said.others_hidden };
// "Name tags only": other players' styles beside their names, nothing on their bodies.
let bodies = own || said.others_shown;
let bodies = own || self.others() == Others::Shown;
let ticks = draw.ticks();
// An emote's props: its time and weight while it plays.
let emote = emotes::pose_playing(draw.body_pose()).and_then(|p| {
let (t, w) = p.at(ticks)?;
(p.item.vertices > 0).then(|| (p.item.clone(), t, w))
});
let worn = match (&self.client, self.uuid_of(m)) {
(Some(client), Some(uuid)) if shown && bodies => client.frame().worn(uuid),
_ => None,
};
let client = self.client.as_ref().filter(|_| bodies);
let worn = client.and_then(|c| worn_of(c, self.shows(own), || self.uuid_of(m)));
// Items on one bone (a back item, a shoulder pet and a shirt ride the body) share its
// matrix: the head, the body and the two arms at most.
let mut bones: [Option<(Bone, [[f32; 4]; 4])>; 4] = [None; 4];
let worn_items = worn.iter().flat_map(|w| w.iter()).map(|i| (i, None));
let emote_items = emote.iter().map(|(i, t, w)| (i, Some((*t, *w))));
for (item, emoting) in worn_items.chain(emote_items) {
// Covered by gear, or drawn by no bone (a name tag style, companions, trails, auras).
// On no bone, around the model in the world.
if let (Slot::Companion | Slot::Trail | Slot::Aura, None) = (item.slot, emoting) {
if draw.pass() == Pass::World {
self.fx(m, item, draw);
}
continue;
}
// Covered by gear, or drawn by no bone (a name tag style).
if emoting.is_none() && covered(m.gear, item.slot) {
continue;
}
@@ -831,7 +753,7 @@ impl Seat {
matrix
}
};
draw.draw(&mesh, matrix, uv, &mut |p| {
draw.draw(&mesh, matrix, Look::new(uv), &mut |p| {
item.animate(ticks, |name, pose| *p.pose(name) = pose);
if let (Some((t, w)), Some(e)) = (emoting, &item.emote) {
e.pose_props(t, |n| item.rest(n), |n, pose| *p.pose(n) = pose);
@@ -846,6 +768,19 @@ impl Seat {
}
}
/// The window's own companion while its body is out of view.
fn first_person(&mut self, m: &PlayerModel, draw: &mut dyn PlayerDraw) {
let Some(client) = self.client.as_ref().filter(|_| !m.invisible) else {
return;
};
let Some(worn) = worn_of(client, self.shows(true), || self.uuid_of(m)) else {
return;
};
if let Some(item) = worn.iter().find(|i| i.slot == Slot::Companion) {
self.fx(m, item, draw);
}
}
/// Once a tick: names the Horizon players in view to the emote feed and plays what it says
/// they play on their models; one that moved ends theirs here at once. Others' emotes show
/// with "Other players' cosmetics" on Shown only; otherwise nothing is asked.
@@ -862,7 +797,7 @@ impl Seat {
}
fn play_others(&self, models: &mut dyn PlayerModels, view: &mut Vec<PlayerModel>) {
let shown = self.said.others_shown;
let shown = self.others() == Others::Shown;
// Items from a folder (the art, the bench): every mannequin plays its first emote, again
// and again.
if self.local && shown {
@@ -927,8 +862,7 @@ impl Seat {
}
impl Game<'_> {
/// Hands the hooks what the game's settings and the player's emote say (allocating only when
/// the emote changes).
/// Hands the hooks what the player's emote says (allocating only when it changes).
pub(super) fn cosmetics_say(&mut self) {
if !enabled() {
return;
@@ -940,17 +874,10 @@ impl Game<'_> {
} else {
None
};
let extras = &self.settings.extras;
let playing = self.cosmetics.emotes.sending();
let pull = self.cosmetics.emotes.pull();
service(|s| {
let said = &mut s.said;
said.mine = extras.cosmetics_mine;
said.listed = extras.horizon_listed;
said.others_shown = extras.effective_others() == Others::Shown;
said.others_hidden = extras.effective_others() == Others::Hidden;
said.marks = extras.horizon_marks;
said.solo = extras.cosmetics_solo;
said.pull = pull;
if said.playing.as_ref().map(|(id, l)| (id.as_str(), *l)) != playing {
said.playing = playing.map(|(id, l)| (id.to_owned(), l));
@@ -961,36 +888,13 @@ impl Game<'_> {
});
}
/// Once a frame, after the hooks: takes the service's client for drawing and puts the
/// textures the last frame wanted into the atlas.
/// Once a frame, after the hooks: what the game reads of them (whether items come from a
/// folder).
pub(super) fn cosmetics_frame(&mut self) {
if !enabled() {
return;
}
let c = &mut self.cosmetics;
let owns = service(|s| {
if c.client.is_none() {
c.client = s.client.clone();
c.local = s.local;
}
s.owner.owns
});
// Read every frame: the settings button shows it, and it is never saved.
self.settings.extras.cosmetics_owner = owns;
c.frame.set(c.frame.get().wrapping_add(1));
c.fx.get_mut().frame(c.frame.get());
let uploads = std::mem::take(&mut *c.uploads.borrow_mut());
for item in uploads {
let location = format!("cosmetics/{:x}", Arc::as_ptr(&item) as usize);
if !self.gui.atlas.sprites.contains_key(&location) {
self.store_skin(location, slot_image(&item));
}
}
// Items no player wears any more.
self.cosmetics
.baked
.borrow_mut()
.retain(|_, b| Arc::strong_count(&b.item) > 1);
self.cosmetics.local = service(|s| s.local);
}
/// Asks the service which emotes the account owns, for the wheel: when it opens, at most
@@ -1016,223 +920,11 @@ impl Game<'_> {
})
.ok();
}
/// What `m` wears, if cosmetics are shown for it: `own` for the player's own model.
fn worn(&self, m: &Mob, own: bool) -> Option<horizon_cosmetics::Worn> {
let client = self.cosmetics.client.as_ref()?;
worn_of(client, &self.settings.extras, own, || {
self.cosmetics_uuid(m, own)
})
}
/// The UUID the service knows `m` by: a player's, or a mannequin's id with local items.
fn cosmetics_uuid(&self, m: &Mob, own: bool) -> Option<u128> {
if own {
own_uuid(
self.remote.as_ref().map(|r| r.profile()),
self.integrated.is_some(),
self.settings.extras.cosmetics_solo,
|| horizon_client::auth::current().map(|s| s.uuid),
)
} else if &*m.kind.id == "minecraft:player" {
self.remote_player_uuid(m.id)
} else if self.cosmetics.local && &*m.kind.id == "minecraft:mannequin" {
Some(u128::from(m.id))
} else {
None
}
}
/// The cosmetics `m` wears, on its bones. `gpu`: posed on the GPU (the mob pass's skin
/// frame, begun); the bounds of what went there come back, for culling. Else the vertices
/// go to `out`. `camera`: where the world view's camera is; None in a picture of the player
/// (the inventory), which shows no companion, trail or aura.
#[allow(clippy::too_many_arguments)]
pub(super) fn cosmetics_geometry(
&self,
m: &Mob,
own: bool,
base: glam::Mat4,
camera: Option<glam::DVec3>,
alpha: f32,
light: u32,
gpu: bool,
out: &mut Vec<horizon_client::mesher::Vertex>,
) -> Option<(glam::Vec3, glam::Vec3)> {
use horizon_client::mobs::{anim, render};
let worn = self.worn(m, own);
// An emote's props: its time and weight while it plays.
let ticks = m.age as f32 + alpha;
let emote = emotes::playing(m).and_then(|p| {
let (t, w) = p.at(ticks)?;
(p.item.vertices > 0).then_some((&p.item, t, w))
});
if worn.is_none() && emote.is_none() || horizon_client::mobs::trader::is_invisible(m) {
return None;
}
// The avatar's body, posed as it is drawn.
let (key, _) = render::body(m);
let body = m.kind.layer(&key).unwrap_or(&m.kind.model);
let mut scratch = self.cosmetics.poses.borrow_mut();
let (body_poses, poses) = &mut *scratch;
// Others' items on the body are the hook's (`Seat::player_layers`): their companions,
// trails and auras alone are drawn here, on no bone.
if own {
body_poses.reset(body);
let data = self.res.mob_data();
anim::setup(m, &anim::state(m, alpha), body_poses, &data.animations);
}
let mut bounds = None;
// Items on one bone (a back item, a shoulder pet and a shirt ride the body) share its
// matrix: the head, the body and the two arms at most.
let mut bones: [Option<(horizon_cosmetics::Bone, glam::Mat4)>; 4] = [None; 4];
// "Name tags only": other players' styles beside their names, nothing on their bodies.
let bodies = own || self.settings.extras.effective_others() == Others::Shown;
let worn_items = worn.iter().flat_map(|w| w.iter()).map(|i| (i, None));
let emote_items = emote.map(|(i, t, w)| (i, Some((t, w))));
for (item, emoting) in worn_items.chain(emote_items) {
if emoting.is_none() && !bodies {
continue;
}
if let (Some(camera), Slot::Companion | Slot::Trail | Slot::Aura) = (camera, item.slot)
{
// The player's own in first person: only its companion, drawn after the drain
// of the first-person model (`cosmetics_fx_geometry`).
if !(own && !self.own_body_in_view()) {
let at = fx::Wearer {
m,
own,
key: if own { OWN } else { m.id },
base,
camera,
alpha,
light,
};
bounds = self.cosmetics_fx(&at, item, gpu, bounds, poses, out);
}
continue;
}
if !own {
continue;
}
if emoting.is_none() && covered(super::player_hooks::gear(m), item.slot) {
continue;
}
let Some((models, uv)) = self.cosmetics_baked(item) else {
continue;
};
for (bone_of, model) in &models {
let known = bones.iter().flatten().find(|(b, _)| b == bone_of);
let bone = match known {
Some(&(_, matrix)) => matrix,
None => {
let Some(matrix) =
render::part_matrix(body, base, body_poses, bone_of.part(), 0.0)
else {
continue;
};
if let Some(free) = bones.iter_mut().find(|b| b.is_none()) {
*free = Some((*bone_of, matrix));
}
matrix
}
};
poses.reset(model);
item.animate(ticks, |name, pose| *poses.get(name) = pose);
if let (Some((t, w)), Some(e)) = (emoting, &item.emote) {
e.pose_props(t, |n| item.rest(n), |n, pose| *poses.get(n) = pose);
// Props grow in and shrink out with the emote's blend.
let root = poses.get(&model.name);
for s in &mut root[6..9] {
*s *= w;
}
}
bounds = self.cosmetics_model(model, bone, poses, uv, light, gpu, bounds, out);
}
}
bounds
}
/// An item's models and its texture's corners in the atlas, once its texture is there (it is
/// asked for meanwhile).
#[allow(clippy::type_complexity)]
fn cosmetics_baked(
&self,
item: &Arc<Item>,
) -> Option<(Vec<(horizon_cosmetics::Bone, Arc<Part>)>, [f32; 4])> {
let frame = self.cosmetics.frame.get();
let mut baked = self.cosmetics.baked.borrow_mut();
let b = baked
.entry(Arc::as_ptr(item) as usize)
.or_insert_with(|| Baked::new(item, frame));
let Some(rect) = self.gui.atlas.sprites.get(&b.location) else {
self.cosmetics.uploads.borrow_mut().push(item.clone());
return None;
};
if b.marked.replace(frame) != frame {
self.skin_downloads.borrow_mut().drawn(&b.location);
}
// The texture fills the top-left of its 64×64 slot.
let (w, h) = (
item.texture.width as f32 / 64.0,
item.texture.height as f32 / 64.0,
);
let uv = [
rect.u0,
rect.v0,
rect.u0 + (rect.u1 - rect.u0) * w,
rect.v0 + (rect.v1 - rect.v0) * h,
];
Some((b.models.clone(), uv))
}
/// One model at `matrix`, posed by `poses`: on the GPU's skin path when `gpu` (the bounds
/// grow to hold it), else its vertices go to `out`.
#[allow(clippy::too_many_arguments)]
fn cosmetics_model(
&self,
model: &Arc<Part>,
matrix: glam::Mat4,
poses: &horizon_client::mobs::anim::Poses,
uv: [f32; 4],
light: u32,
gpu: bool,
bounds: Option<(glam::Vec3, glam::Vec3)>,
out: &mut Vec<Vertex>,
) -> Option<(glam::Vec3, glam::Vec3)> {
if gpu {
let mut models = self.skin_models.borrow_mut();
let mut frame = self.skin_frame.borrow_mut();
let mut skin = horizon_client::mobs::skin::Skin::new(&mut models, &mut frame);
skin.bounds = bounds;
let paint = horizon_client::mobs::skin::Paint {
uv,
scroll: 0.0,
color: 0xFFFF_FFFF,
light,
overlay: 0,
};
if skin.draw(model, matrix, poses, paint) {
return skin.bounds;
}
}
horizon_client::mobs::render_model(
model,
matrix,
&|p: &Part| poses.pose(p),
uv,
light,
0,
out,
);
bounds
}
}
#[cfg(test)]
mod tests {
use super::*;
use horizon_client::extras::Extras;
use horizon_client::mobs::anim::BodyPose;
/// A name tag style with a 1×1 mark.
@@ -1293,24 +985,32 @@ mod tests {
const OTHER: u128 = 0x1234;
/// What another player wears as `extras` shows it, drawn frame after frame for up to
/// A seat whose player chose `others` (None: no choice) and whose account `owns` a cosmetic.
fn seat_of(others: Option<Others>, owns: bool) -> Seat {
let mut seat = Seat::default();
seat.settings.others = others;
seat.owner.owns = owns;
seat
}
/// What another player wears as `seat` shows it, drawn frame after frame for up to
/// `patience` (the service answers on its own thread), and what was asked of the service.
fn other_player(
extras: &Extras,
seat: &Seat,
patience: Duration,
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
other_player_from(Service::default(), extras, patience)
other_player_from(Service::default(), seat, patience)
}
fn other_player_from(
service: Service,
extras: &Extras,
seat: &Seat,
patience: Duration,
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
let deadline = Instant::now() + patience;
let worn = loop {
let worn = worn_of(&client, extras, false, || Some(OTHER));
let worn = worn_of(&client, seat.shows(false), || Some(OTHER));
if worn.is_some() || Instant::now() >= deadline {
break worn;
}
@@ -1322,18 +1022,13 @@ mod tests {
#[test]
fn a_player_who_owns_none_sees_and_asks_nothing_until_they_choose() {
use horizon_cosmetics::prefs::Others;
// No line, owns nothing: a vanilla game, and no request about the other player.
let (short, long) = (Duration::from_millis(300), Duration::from_secs(10));
let (worn, asked) = other_player(&Extras::default(), short);
let (worn, asked) = other_player(&seat_of(None, false), short);
assert!(worn.is_none());
assert_eq!(asked, Vec::<String>::new());
// The service says this account owns a cosmetic: the other player's style shows.
let owner = Extras {
cosmetics_owner: true,
..Extras::default()
};
let (worn, asked) = other_player(&owner, long);
let (worn, asked) = other_player(&seat_of(None, true), long);
assert_eq!(
worn.map(|w| w.iter().map(|i| i.id.clone()).collect::<Vec<_>>()),
Some(vec!["tag".to_owned()])
@@ -1343,16 +1038,12 @@ mod tests {
format!("POST /v1/worn {{\"players\":[\"{OTHER:032x}\"]}}")
);
// A choice wins over ownership both ways.
let chose = Extras {
cosmetics_others: Some(Others::Shown),
..Extras::default()
};
assert!(other_player(&chose, long).0.is_some());
let hid = Extras {
cosmetics_others: Some(Others::Hidden),
..owner
};
let (worn, asked) = other_player(&hid, short);
assert!(
other_player(&seat_of(Some(Others::Shown), false), long)
.0
.is_some()
);
let (worn, asked) = other_player(&seat_of(Some(Others::Hidden), true), short);
assert!(worn.is_none() && asked.is_empty());
}
@@ -1364,14 +1055,12 @@ mod tests {
let mine = |solo: bool, shown: bool, patience: Duration| {
let service = Service::default();
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
let extras = Extras {
cosmetics_solo: shown,
..Extras::default()
};
let uuid = || own_uuid(Some(OFFLINE), solo, extras.cosmetics_solo, || Some(ACCOUNT));
let mut seat = Seat::default();
seat.settings.solo = shown;
let uuid = || own_uuid(Some(OFFLINE), solo, seat.settings.solo, || Some(ACCOUNT));
let deadline = Instant::now() + patience;
let worn = loop {
let worn = worn_of(&client, &extras, true, uuid);
let worn = worn_of(&client, seat.shows(true), uuid);
if worn.is_some() || Instant::now() >= deadline {
break worn;
}
@@ -1404,7 +1093,6 @@ mod tests {
#[test]
fn a_tall_cosmetic_lifts_the_name_tag_only_while_it_is_drawn() {
use super::super::names::{NAME_TAG_UNIT, cosmetic_lift};
use horizon_cosmetics::prefs::Others;
// Vanilla: EntityAttachment.NAME_TAG of a standing player, 1.8 + 0.5 above the feet;
// the tag's lowest pixel 10 text pixels under it.
const VANILLA: f64 = 2.3;
@@ -1414,25 +1102,17 @@ mod tests {
// by the avatar's 0.9375.
let star = 0.9375 * (1.501 + 20.0 / 16.0);
let both: &'static [(&str, &str)] = &[("tag", TAG), ("crown", CROWN)];
let shown = Extras {
cosmetics_others: Some(Others::Shown),
..Extras::default()
};
let shown = seat_of(Some(Others::Shown), false);
let (short, long) = (Duration::from_millis(300), Duration::from_secs(10));
let top_of = |extras: &Extras, patience| {
let (worn, _) = other_player_from(Service(Arc::default(), both), extras, patience);
body_top(extras.effective_others(), false, worn.as_ref(), |_| false)
let top_of = |seat: &Seat, patience| {
let (worn, _) = other_player_from(Service(Arc::default(), both), seat, patience);
body_top(seat.others(), false, worn.as_ref(), |_| false)
};
// No cosmetics drawn on the body (a name tag style only): vanilla's height exactly.
let (worn, _) = other_player(&shown, long);
assert!(worn.is_some());
assert_eq!(
tag(body_top(
shown.effective_others(),
false,
worn.as_ref(),
|_| false
)),
tag(body_top(shown.others(), false, worn.as_ref(), |_| false)),
VANILLA
);
// A small hat under the tag: vanilla's height too.
@@ -1446,22 +1126,19 @@ mod tests {
);
// A helmet over it: vanilla's height.
let (worn, _) = other_player_from(Service(Arc::default(), both), &shown, long);
let helmet = body_top(shown.effective_others(), false, worn.as_ref(), |slot| {
let helmet = body_top(shown.others(), false, worn.as_ref(), |slot| {
slot == Slot::Head
});
assert_eq!(tag(helmet), VANILLA);
// Others' cosmetics on name tags only, or hidden: nothing on the body, vanilla's height.
for others in [Others::NameTags, Others::Hidden] {
let extras = Extras {
cosmetics_others: Some(others),
..Extras::default()
};
let seat = seat_of(Some(others), false);
let patience = if others == Others::Hidden {
short
} else {
long
};
assert_eq!(tag(top_of(&extras, patience)), VANILLA, "{others:?}");
assert_eq!(tag(top_of(&seat, patience)), VANILLA, "{others:?}");
}
}
@@ -1498,11 +1175,10 @@ mod tests {
let mut service = Seat {
local: true,
said: Said {
others_shown: true,
first: Some(item.clone()),
..Said::default()
},
..Seat::default()
..seat_of(Some(Others::Shown), false)
};
let model = |id, kind, uuid| (PlayerModel::new(id, kind, uuid, 50, false), None);
let mut view = View(vec![
@@ -1521,7 +1197,7 @@ mod tests {
service.tick(&mut view);
assert!(Arc::ptr_eq(&first, view.0[1].1.as_ref().unwrap()));
// Others' cosmetics not shown: a new mannequin plays nothing.
service.said.others_shown = false;
service.settings.others = Some(Others::NameTags);
view.0.push(model(3, Kind::Mannequin, None));
service.tick(&mut view);
assert!(view.0[3].1.is_none());
@@ -1529,10 +1205,16 @@ mod tests {
/// A player model as `player_layers` draws on it: one bone, the sprites it is given; logs
/// each draw (mesh, matrix, uv, its root's scale once posed).
#[derive(Default)]
struct Canvas {
pose: Option<Arc<dyn BodyPose>>,
sprite: Option<[f32; 4]>,
drawn: Vec<(String, [[f32; 4]; 4], [f32; 4], [f32; 3])>,
/// The model's matrix (`LEFT_ARM` if None) and how it moves.
base: Option<glam::Mat4>,
motion: horizon_hooks::players::Motion,
/// Each draw's look: see-through alpha and light.
looks: Vec<(Option<f32>, horizon_hooks::players::Light)>,
}
const LEFT_ARM: [[f32; 4]; 4] = [[2.0, 0.0, 0.0, 0.0]; 4];
@@ -1544,6 +1226,22 @@ mod tests {
fn ticks(&self) -> f32 {
102.0
}
fn base(&self) -> [[f32; 4]; 4] {
self.base.map_or(LEFT_ARM, |b| b.to_cols_array_2d())
}
fn motion(&self) -> horizon_hooks::players::Motion {
self.motion
}
fn view(&self) -> horizon_hooks::players::View {
Default::default()
}
fn particles(
&mut self,
_: [f32; 4],
_: &[horizon_hooks::players::Particle],
_: horizon_hooks::players::Light,
) {
}
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]> {
(bone == Bone::LeftArm).then_some(LEFT_ARM)
}
@@ -1557,7 +1255,7 @@ mod tests {
&mut self,
mesh: &Arc<Mesh>,
matrix: [[f32; 4]; 4],
uv: [f32; 4],
look: Look,
pose: &mut dyn FnMut(&mut dyn horizon_hooks::players::PartPoses),
) {
struct Rest(std::collections::HashMap<String, [f32; 9]>);
@@ -1569,8 +1267,13 @@ mod tests {
let mut rest = Rest([(mesh.name.clone(), mesh.pose)].into());
pose(&mut rest);
let root = rest.0[&mesh.name];
self.drawn
.push((mesh.name.clone(), matrix, uv, [root[6], root[7], root[8]]));
self.looks.push((look.see_through, look.light));
self.drawn.push((
mesh.name.clone(),
matrix,
look.uv,
[root[6], root[7], root[8]],
));
}
}
@@ -1600,17 +1303,10 @@ mod tests {
emotes::set_model_playing(&mut view, 9, Some(playing));
let mut canvas = Canvas {
pose: view.0[0].1.clone(),
sprite: None,
drawn: Vec::new(),
..Canvas::default()
};
// Others' cosmetics hidden, no service: the player's own emote still shows.
let mut seat = Seat {
said: Said {
others_hidden: true,
..Said::default()
},
..Seat::default()
};
let mut seat = seat_of(Some(Others::Hidden), false);
// Its texture not in the atlas yet: nothing drawn, the sprite asked for.
seat.player_layers(&player, &mut canvas);
assert!(canvas.drawn.is_empty());
@@ -1626,4 +1322,47 @@ mod tests {
assert!((s - weight).abs() < 1e-6, "{scale:?} by {weight}");
}
}
#[test]
fn an_aura_goes_see_through_at_the_feet_unless_swimming_far_or_hidden() {
use horizon_hooks::players::{Light, Motion};
let ring = CROWN
.replace(r#""slot": "head", "bone": "head""#, r#""slot": "aura""#)
// At the feet: an aura reaches at most 6 pixels above them.
.replace(r#""attach": {"offset": [0, -18, 0]},"#, "")
.replace(
r#""uv": [0, 0]}]}]}"#,
r#""uv": [0, 0]}]}], "aura": {"at": "feet", "alpha": 0.5}}"#,
);
let ring = Arc::new(Item::parse(ring.as_bytes()).unwrap());
let at = glam::Vec3::new(3.0, 0.0, 4.0);
let mut canvas = Canvas {
sprite: Some([0.0, 0.0, 0.5, 0.5]),
base: Some(glam::Mat4::from_translation(at)),
..Canvas::default()
};
let other = PlayerModel::new(9, Kind::Player, Some(5), 102, false);
let own = PlayerModel::new(1, Kind::Own, None, 102, false);
let mut seat = Seat::default();
seat.fx(&other, &ring, &mut canvas);
// Half see-through, lit as the wearer, 24 pixels under the neck.
assert_eq!(canvas.looks, [(Some(0.5), Light::Model)]);
let feet = glam::Mat4::from_cols_array_2d(&canvas.drawn[0].1).w_axis;
assert_eq!(feet.truncate(), at + glam::Vec3::new(0.0, 1.5, 0.0));
// Swimming, it would tip over with the body: none.
canvas.motion = Motion::default().poses(false, false, false, true);
seat.fx(&other, &ring, &mut canvas);
canvas.motion = Motion::default();
// Past 48 blocks: none.
canvas.base = Some(glam::Mat4::from_translation(glam::Vec3::X * 49.0));
seat.fx(&other, &ring, &mut canvas);
assert_eq!(canvas.looks.len(), 1);
// Others' trails and auras hidden: theirs go, the player's own stays.
canvas.base = Some(glam::Mat4::from_translation(at));
seat.settings.hide_fx = true;
seat.fx(&other, &ring, &mut canvas);
assert_eq!(canvas.looks.len(), 1);
seat.fx(&own, &ring, &mut canvas);
assert_eq!(canvas.looks.len(), 2);
}
}
@@ -71,11 +71,6 @@ pub(in crate::game) fn pose_playing(pose: Option<&Arc<dyn BodyPose>>) -> Option<
any.downcast_ref::<EmotePose>().map(|e| &e.0)
}
/// The emote `m` plays, if any.
pub(in crate::game) fn playing(m: &Mob) -> Option<&Playing> {
pose_playing(m.emote.as_ref())
}
/// The pose that plays `next` where `now` is laid, unless it plays that already (None: keep
/// it; a new one allocates).
fn replaced(
+160 -255
View File
@@ -1,47 +1,78 @@
//! Companions, trails and auras ([docs/cosmetics.md](../../../../../docs/cosmetics.md)): worn
//! like the other cosmetics but on no bone. A companion floats beside its wearer and follows with
//! a lag (`horizon_cosmetics::follow`), drawn with the wearer; a trail's particles
//! (`horizon_cosmetics::trail`) and an aura blend, so they go after every player in one
//! see-through span (`cosmetics_fx_geometry`). Nothing of them is an entity, collides or is sent
//! to a server.
//! (`horizon_cosmetics::trail`) and an aura blend, so they are drawn see-through after every
//! player (`PlayerDraw::particles`, `Look::see_through`). Nothing of them is an entity, collides
//! or is sent to a server.
use super::{FULL_LIGHT, OWN};
use crate::game::Game;
use super::Seat;
use glam::{DVec3, Mat4, Vec3};
use horizon_client::mesher::Vertex;
use horizon_client::mobs::anim::Poses;
use horizon_client::mobs::{Mob, Part};
use horizon_client::fast_hash::FxHashMap;
use horizon_cosmetics::follow::{self, Follow};
use horizon_cosmetics::trail::Emitter;
use horizon_cosmetics::trail::{Budget, Emitter};
use horizon_cosmetics::{Item, Slot};
use horizon_hooks::players::{Kind, Light, Look, Particle, Pass, PlayerDraw, PlayerModel};
use std::sync::Arc;
use std::time::Instant;
/// How far from the camera (blocks) a wearer's companion, trail and aura are drawn.
const FAR: f32 = 48.0;
/// Who wears it, as drawn this frame.
pub(super) struct Wearer<'a> {
pub m: &'a Mob,
/// The player's own model.
pub own: bool,
/// `OWN`, or the entity id.
pub key: u32,
/// The wearer's model matrix (`render::base_transform`), relative to `camera`.
pub base: Mat4,
pub camera: DVec3,
pub alpha: f32,
pub light: u32,
/// The key of the player's own model in `Fx`'s maps (others go by entity id).
const OWN: u32 = u32::MAX;
/// What companions and trails keep between frames, per wearer (marked with the frame they were
/// last drawn in; the others are dropped).
#[derive(Default)]
pub(super) struct Fx {
companions: FxHashMap<u32, (Follow, u32)>,
trails: FxHashMap<u32, (Emitter, u32)>,
budget: Budget,
clock: Option<(Instant, Instant)>,
/// Ticks since the first frame, and the last frame's length in seconds.
now: f64,
dt: f32,
frame: u32,
/// Kept so a frame of trails allocates none.
particles: Vec<Particle>,
}
/// `m`'s head yaw this frame (degrees).
fn head_yaw(m: &Mob, alpha: f32) -> f32 {
let turn = horizon_client::mobs::living::wrap(m.head_yaw - m.previous_head_yaw);
m.previous_head_yaw + turn * alpha
}
impl Fx {
/// A new frame: the clock moves on and what no frame drew since the last one goes.
pub(super) fn frame(&mut self) {
self.frame = self.frame.wrapping_add(1);
let frame = self.frame;
let now = Instant::now();
let (start, last) = self.clock.get_or_insert((now, now));
self.dt = now.duration_since(*last).as_secs_f32().min(0.25);
self.now = now.duration_since(*start).as_secs_f64() * 20.0;
*last = now;
let seen = |mark: u32| frame.wrapping_sub(mark) <= 1;
self.companions.retain(|_, (_, mark)| seen(*mark));
let budget = &mut self.budget;
self.trails.retain(|_, (e, mark)| {
let keep = seen(*mark);
if !keep {
e.clear(budget);
}
keep
});
}
/// Whether `m` lies in a bed.
fn asleep(m: &Mob) -> bool {
m.extra.avatar_pose == horizon_client::pose::Pose::Sleeping
/// The companion of wearer `key` moved toward `target` (once a frame, however many times it
/// is drawn).
fn follow(&mut self, key: u32, target: DVec3, yaw: f32, lag: f32) -> Follow {
let (frame, dt) = (self.frame, self.dt);
let (f, mark) = self
.companions
.entry(key)
.or_insert((Follow::at(target, yaw), frame));
if *mark != frame {
f.step(target, yaw, lag, dt);
*mark = frame;
}
*f
}
}
/// Where a companion floats: `at` (relative to the camera) facing `yaw`, at the wearer's scale,
@@ -52,270 +83,144 @@ fn companion_matrix(at: Vec3, yaw: f32, scale: f32) -> Mat4 {
* Mat4::from_scale(Vec3::new(-scale, -scale, scale))
}
impl Game<'_> {
/// A companion, trail or aura `at` wears: a companion goes where the wearer's other items go
/// (`gpu` and `out`, `bounds` grown), trails and auras to the frame's see-through vertices.
/// Other players' trails and auras stay hidden with "Hide other players' trails and auras".
pub(super) fn cosmetics_fx(
&self,
at: &Wearer,
item: &Arc<Item>,
gpu: bool,
bounds: Option<(Vec3, Vec3)>,
poses: &mut Poses,
out: &mut Vec<Vertex>,
) -> Option<(Vec3, Vec3)> {
if !at.own && item.slot.is_effect() && self.settings.extras.cosmetics_hide_fx {
return bounds;
/// Full light for what glows, else `otherwise`.
fn glowing(glow: bool, otherwise: Light) -> Light {
if glow { Light::Full } else { otherwise }
}
impl Seat {
/// A companion, trail or aura `m` wears, around its model as `draw` draws it. Other players'
/// trails and auras stay hidden with "Hide other players' trails and auras".
pub(super) fn fx(&mut self, m: &PlayerModel, item: &Arc<Item>, draw: &mut dyn PlayerDraw) {
let own = m.kind == Kind::Own;
if !own && item.slot.is_effect() && self.settings.hide_fx {
return;
}
// Past `FAR` a companion, trail or aura is a few pixels across for the cost of a model:
// not drawn, and a trail that isn't drawn doesn't spawn (its particles go back to the
// budget).
if at.base.w_axis.truncate().length_squared() > FAR * FAR {
return bounds;
let base = Mat4::from_cols_array_2d(&draw.base());
if base.w_axis.truncate().length_squared() > FAR * FAR {
return;
}
let Some(uv) = self.uv(item, |id| draw.sprite(id)) else {
return;
};
let key = if own { OWN } else { m.id };
match item.slot {
Slot::Companion => {
let Some((model, uv, matrix)) = self.companion_at(at, item, None) else {
return bounds;
};
self.pose_companion(item, at.m, at.alpha, poses, &model);
let glow = item.companion.as_ref().is_some_and(|c| c.glow);
let light = if glow { FULL_LIGHT } else { at.light };
self.cosmetics_model(&model, matrix, poses, uv, light, gpu, bounds, out)
}
Slot::Trail => {
self.trail(at, item);
bounds
}
Slot::Aura => {
self.aura(at, item, poses);
bounds
}
_ => bounds,
Slot::Companion => self.companion(key, item, uv, base, draw),
Slot::Trail => self.trail(key, item, uv, draw),
Slot::Aura => self.aura(item, uv, base, draw),
_ => {}
}
}
/// The companion's model, texture and matrix this frame, its follow moved on once a frame.
/// `first_person`: the camera's forward and left, for the player's own in first person: the
/// companion keeps out of the middle of the view.
fn companion_at(
&self,
at: &Wearer,
/// The companion where it floats, its follow moved on once a frame. In first person it keeps
/// out of the middle of the view, lit where it floats.
fn companion(
&mut self,
key: u32,
item: &Arc<Item>,
first_person: Option<(Vec3, Vec3)>,
) -> Option<(Arc<Part>, [f32; 4], Mat4)> {
let c = item.companion.as_ref()?;
let (models, uv) = self.cosmetics_baked(item)?;
let model = models.first()?.1.clone();
let scale = at.base.x_axis.truncate().length();
let target = at.base.transform_point3(Vec3::from(c.anchor) / 16.0);
let head = at
.base
.transform_point3(Vec3::from(horizon_cosmetics::HEAD) / 16.0);
let yaw = head_yaw(at.m, at.alpha);
let follow = self.companion_follow(at.key, at.camera + target.as_dvec3(), yaw, c.lag);
uv: [f32; 4],
base: Mat4,
draw: &mut dyn PlayerDraw,
) {
let Some(c) = item.companion.as_ref() else {
return;
};
let Some((_, mesh)) = self.kept(item).meshes.first().cloned() else {
return;
};
let first_person = draw.pass() == Pass::FirstPerson;
let (motion, view) = (draw.motion(), draw.view());
let camera = DVec3::from(view.camera);
let scale = base.x_axis.truncate().length();
let target = base.transform_point3(Vec3::from(c.anchor) / 16.0);
let head = base.transform_point3(Vec3::from(horizon_cosmetics::HEAD) / 16.0);
let follow = self
.fx
.follow(key, camera + target.as_dvec3(), motion.head_yaw, c.lag);
let clearance = (horizon_cosmetics::HEAD_CLEARANCE + c.reach) / 16.0 * scale;
let mut rel = (follow.pos - at.camera).as_vec3();
if let Some((forward, left)) = first_person {
rel = follow::out_of_view(rel, forward, left);
let mut rel = (follow.pos - camera).as_vec3();
if first_person {
rel = follow::out_of_view(rel, Vec3::from(view.forward), Vec3::from(view.left));
}
rel = follow::clear_of(rel, head, clearance);
Some((model, uv, companion_matrix(rel, follow.yaw, scale)))
}
/// The companion of wearer `key` moved toward `target` (once a frame, however many times it
/// is drawn).
fn companion_follow(&self, key: u32, target: DVec3, yaw: f32, lag: f32) -> Follow {
let frame = self.cosmetics.frame.get();
let mut fx = self.cosmetics.fx.borrow_mut();
let dt = fx.dt;
let (f, mark) = fx
.companions
.entry(key)
.or_insert((Follow::at(target, yaw), frame));
if *mark != frame {
f.step(target, yaw, lag, dt);
*mark = frame;
}
*f
}
/// The companion's animations, and its `look` part turned with the wearer's pitch.
fn pose_companion(&self, item: &Item, m: &Mob, alpha: f32, poses: &mut Poses, model: &Part) {
poses.reset(model);
item.animate(m.age as f32 + alpha, |name, pose| *poses.get(name) = pose);
if let Some(look) = item.companion.as_ref().and_then(|c| c.look.as_deref()) {
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * alpha;
poses.get(look)[3] += (pitch * 0.6).clamp(-30.0, 30.0).to_radians();
}
let matrix = companion_matrix(rel, follow.yaw, scale).to_cols_array_2d();
let near = if first_person {
Light::AtPlace
} else {
Light::Model
};
let look = Look::new(uv).lit(glowing(c.glow, near));
let ticks = draw.ticks();
// Its animations, and its `look` part turned with the wearer's pitch.
let pitch = (motion.head_pitch * 0.6).clamp(-30.0, 30.0).to_radians();
draw.draw(&mesh, matrix, look, &mut |p| {
item.animate(ticks, |name, pose| *p.pose(name) = pose);
if let Some(part) = c.look.as_deref() {
p.pose(part)[3] += pitch;
}
});
}
/// The trail's particles: spawned once a frame by the distance walked (not while sneaking or
/// asleep), facing the camera.
fn trail(&self, at: &Wearer, item: &Arc<Item>) {
fn trail(&mut self, key: u32, item: &Arc<Item>, uv: [f32; 4], draw: &mut dyn PlayerDraw) {
let Some(t) = &item.trail else {
return;
};
let Some((_, uv)) = self.cosmetics_baked(item) else {
return;
};
let m = at.m;
let a = f64::from(at.alpha);
let feet = DVec3::new(
m.previous.x + (m.position.x - m.previous.x) * a,
m.previous.y + (m.position.y - m.previous.y) * a,
m.previous.z + (m.position.z - m.previous.z) * a,
);
let moves = !m.extra.crouching && !asleep(m);
let q = self.camera_orientation();
let (right, up) = (q * Vec3::X, q * Vec3::Y);
let light = if t.glow { FULL_LIGHT } else { at.light };
let frame = self.cosmetics.frame.get();
let mut fx = self.cosmetics.fx.borrow_mut();
let super::Fx {
let (motion, view) = (draw.motion(), draw.view());
let moves = !motion.crouching && !motion.asleep;
let Fx {
trails,
budget,
now,
clear,
frame,
particles,
..
} = &mut *fx;
} = &mut self.fx;
let (e, mark) = trails
.entry(at.key)
.entry(key)
.or_insert_with(|| (Emitter::default(), frame.wrapping_sub(1)));
if *mark != frame {
e.step(t, feet, moves, *now, budget);
*mark = frame;
}
for quad in e.quads(t, *now, at.camera, right, up) {
let color = ((quad.alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
for (corner, [u, v]) in quad.corners.iter().zip(quad.uv) {
clear.push(Vertex {
pos: corner.to_array(),
uv: [uv[0] + (uv[2] - uv[0]) * u, uv[1] + (uv[3] - uv[1]) * v],
layer: 0,
color,
light,
// Unlit: a particle keeps its colour whichever way it faces.
normal: [0, 0, 0, 0],
glint_uv: [0.0, 0.0],
});
}
if *mark != *frame {
e.step(t, DVec3::from(motion.feet), moves, *now, budget);
*mark = *frame;
}
let (camera, right, up) = (
DVec3::from(view.camera),
Vec3::from(view.right),
Vec3::from(view.up),
);
particles.clear();
particles.extend(
e.quads(t, *now, camera, right, up)
.map(|q| Particle::new(q.corners.map(|c| c.to_array()), q.uv, q.alpha)),
);
draw.particles(uv, particles, glowing(t.glow, Light::Model));
}
/// The aura at the wearer's feet, see-through. None while they fly, swim or sleep: it would
/// tip over with the body.
fn aura(&self, at: &Wearer, item: &Arc<Item>, poses: &mut Poses) {
let (m, Some(a)) = (at.m, &item.aura) else {
fn aura(&mut self, item: &Arc<Item>, uv: [f32; 4], base: Mat4, draw: &mut dyn PlayerDraw) {
let Some(a) = &item.aura else {
return;
};
if m.extra.fall_flying || asleep(m) || m.extra.swim.amount(at.alpha) > 0.0 {
let motion = draw.motion();
if motion.flying || motion.asleep || motion.swimming {
return;
}
let Some((models, uv)) = self.cosmetics_baked(item) else {
return;
};
let Some((_, model)) = models.first() else {
let Some((_, mesh)) = self.kept(item).meshes.first().cloned() else {
return;
};
// The model's origin is the middle of the feet: 24 pixels under the neck.
let matrix = at.base * Mat4::from_translation(Vec3::new(0.0, 24.0 / 16.0, 0.0));
poses.reset(model);
item.animate(m.age as f32 + at.alpha, |name, pose| {
*poses.get(name) = pose
let matrix = base * Mat4::from_translation(Vec3::new(0.0, 24.0 / 16.0, 0.0));
let look = Look::new(uv)
.lit(glowing(a.glow, Light::Model))
.see_through(a.alpha);
let ticks = draw.ticks();
draw.draw(&mesh, matrix.to_cols_array_2d(), look, &mut |p| {
item.animate(ticks, |name, pose| *p.pose(name) = pose)
});
let light = if a.glow { FULL_LIGHT } else { at.light };
let mut fx = self.cosmetics.fx.borrow_mut();
let start = fx.clear.len();
horizon_client::mobs::render_model(
model,
matrix,
&|p: &Part| poses.pose(p),
uv,
light,
0,
&mut fx.clear,
);
let color = ((a.alpha * 255.0) as u32) << 24 | 0x00FF_FFFF;
for v in &mut fx.clear[start..] {
v.color = color;
// Seen through, a face turned away would add to the near one (and a card's two
// faces would double it): `entity.wgsl` drops back faces, so it is `alpha` at most.
v.normal[3] = horizon_client::mesher::SHADED_CULL_W;
}
}
/// After every player: the player's own companion in first person (out of the middle of the
/// view, after the first-person model went to the shadow pass only), then the frame's trails
/// and auras in one see-through span.
pub(in crate::game) fn cosmetics_fx_geometry(
&self,
camera: DVec3,
alpha: f64,
out: &mut Vec<Vertex>,
) {
if !super::enabled() {
return;
}
if !self.own_body_in_view()
&& let Some(m) = self.player_model.as_ref()
{
self.mark_entity(out.len(), "minecraft:player");
self.own_companion_first_person(m, camera, alpha as f32, out);
}
let fx = self.cosmetics.fx.borrow();
if !fx.clear.is_empty() {
self.mark_translucent_entity(out.len(), "minecraft:player");
out.extend_from_slice(&fx.clear);
}
}
fn own_companion_first_person(
&self,
m: &Mob,
camera: DVec3,
alpha: f32,
out: &mut Vec<Vertex>,
) {
if horizon_client::mobs::trader::is_invisible(m) || self.own_invisible() {
return;
}
let Some(worn) = self.worn(m, true) else {
return;
};
let Some(item) = worn.iter().find(|i| i.slot == Slot::Companion) else {
return;
};
let at = Wearer {
m,
own: true,
key: OWN,
base: horizon_client::mobs::render::base_transform(m, alpha, camera),
camera,
alpha,
light: FULL_LIGHT,
};
let (yaw, pitch) = (self.yaw.to_radians(), self.pitch.to_radians());
let forward = Vec3::new(
-yaw.sin() * pitch.cos(),
-pitch.sin(),
yaw.cos() * pitch.cos(),
);
let left = Vec3::new(yaw.cos(), 0.0, yaw.sin());
let Some((model, uv, matrix)) = self.companion_at(&at, item, Some((forward, left))) else {
return;
};
// Lit where it floats.
let p = (camera + matrix.w_axis.truncate().as_dvec3()).floor();
let light = self.world.light(p.x as i32, p.y as i32, p.z as i32);
let mut light = (u32::from(light >> 4) * 16) | ((u32::from(light & 15) * 16) << 8);
if item.companion.as_ref().is_some_and(|c| c.glow) {
light = FULL_LIGHT;
}
let mut poses = self.cosmetics.poses.borrow_mut();
let poses = &mut poses.1;
self.pose_companion(item, m, alpha, poses, &model);
self.cosmetics_model(&model, matrix, poses, uv, light, false, None, out);
}
}
@@ -0,0 +1,285 @@
//! The cosmetics' settings (spec #14091 step 5a): their lines of `horizon-extras.txt`, the ones
//! the launcher's Wardrobe shares (`horizon_cosmetics::prefs`), through the settings store
//! ([`STORE`]), and their rows in the Extras page's Cosmetics group ([`SECTION`]). Each
//! splitscreen player has their own, as their own settings file: they live in the player's
//! [`Seat`].
use super::{Seat, service};
use horizon_cosmetics::prefs::{self, Others};
use horizon_hooks::pages::{Act, Label, Page, Place, Row};
use horizon_hooks::settings::Store;
/// My cosmetics in single player too.
const SOLO: &str = "cosmeticsSolo";
/// The settings, each with its line in `horizon-extras.txt`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Settings {
/// Show my cosmetics (the account's, told to the service; `prefs::MINE`).
pub mine: bool,
/// How much of other players' cosmetics shows: no line until the player chooses
/// (`Seat::others`).
pub others: Option<Others>,
/// Other players' trails and auras hidden.
pub hide_fx: bool,
/// My cosmetics in single player too: the window's own server names the player by their
/// offline profile, which the service doesn't know, so they show only with this on.
pub solo: bool,
/// Horizon's mark before Horizon players' names (name tags, player list).
pub marks: bool,
/// The service may say this player plays Horizon.
pub listed: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
mine: true,
others: None,
hide_fx: false,
solo: false,
marks: true,
listed: true,
}
}
}
const KEYS: [&str; 6] = [
prefs::MINE,
prefs::OTHERS,
prefs::HIDE_FX,
SOLO,
prefs::MARKS,
prefs::LISTED,
];
impl Settings {
/// One line; a value it can't read keeps the setting.
fn load(&mut self, key: &str, value: &str) {
let flag = |was: bool| match value {
"true" => true,
"false" => false,
_ => was,
};
match key {
prefs::MINE => self.mine = flag(self.mine),
// `Others::parse` reads the old two-way switch too.
prefs::OTHERS => self.others = Others::parse(value).or(self.others),
prefs::HIDE_FX => self.hide_fx = flag(self.hide_fx),
SOLO => self.solo = flag(self.solo),
prefs::MARKS => self.marks = flag(self.marks),
prefs::LISTED => self.listed = flag(self.listed),
_ => {}
}
}
/// Their lines; others' only once chosen.
fn save(&self, line: &mut dyn FnMut(&str, &str)) {
let text = |on: bool| if on { "true" } else { "false" };
line(prefs::MINE, text(self.mine));
if let Some(others) = self.others {
line(prefs::OTHERS, others.name());
}
line(prefs::HIDE_FX, text(self.hide_fx));
line(SOLO, text(self.solo));
line(prefs::MARKS, text(self.marks));
line(prefs::LISTED, text(self.listed));
}
}
/// The settings' lines of `horizon-extras.txt`, the player's own file in splitscreen: the game
/// loads and saves it on the player's turn, so they are that seat's.
pub(super) static STORE: Lines = Lines;
pub(super) struct Lines;
impl Store for Lines {
fn file(&self) -> &'static str {
prefs::FILE
}
fn owns(&self, key: &str) -> bool {
KEYS.contains(&key)
}
fn load(&self, key: &str, value: &str) {
service(|s| s.settings.load(key, value));
}
fn save(&self, line: &mut dyn FnMut(&str, &str)) {
service(|s| s.settings).save(line);
}
fn is_default(&self) -> bool {
service(|s| s.settings) == Settings::default()
}
}
/// The rows, top to bottom.
const ROWS: [&str; 6] = [
"horizon.extras.cosmetics_mine",
"horizon.extras.cosmetics_solo",
"horizon.extras.cosmetics_others",
"horizon.extras.cosmetics_hide_fx",
"horizon.extras.horizon_marks",
"horizon.extras.horizon_me",
];
const TOOLTIPS: [&str; 6] = [
"horizon.extras.cosmetics_mine.tooltip",
"horizon.extras.cosmetics_solo.tooltip",
"horizon.extras.cosmetics_others.tooltip",
"horizon.extras.cosmetics_hide_fx.tooltip",
"horizon.extras.horizon_marks.tooltip",
"horizon.extras.horizon_me.tooltip",
];
/// The rows in the Extras page's Cosmetics group, while cosmetics are offered
/// ([`super::enabled`]).
pub(super) static SECTION: Section = Section;
pub(super) struct Section;
impl Section {
fn rows_of(seat: &Seat, rows: &mut Vec<Row>) {
let s = &seat.settings;
let shown = |on: bool| {
Label::Key(if on {
"horizon.extras.cosmetics.shown"
} else {
"horizon.extras.cosmetics.hidden"
})
};
let others = Label::Key(match seat.others() {
Others::Shown => "horizon.extras.cosmetics.shown",
Others::NameTags => "horizon.extras.cosmetics.name_tags",
Others::Hidden => "horizon.extras.cosmetics.hidden",
});
let toggle = |label, on| Row::Toggle { label, on };
let cycle = |label, value| Row::Cycle { label, value };
rows.extend([
cycle(ROWS[0], shown(s.mine)),
toggle(ROWS[1], s.solo),
cycle(ROWS[2], others),
toggle(ROWS[3], s.hide_fx),
toggle(ROWS[4], s.marks),
toggle(ROWS[5], s.listed),
]);
}
/// A press on row `row`; `back` turns the three-way row the other way.
fn act_on(seat: &mut Seat, row: usize, back: bool) {
let s = &mut seat.settings;
match row {
0 => s.mine = !s.mine,
1 => s.solo = !s.solo,
2 => {
// From what the row shows; the result is the player's choice from now on.
let o = seat.others();
seat.settings.others = Some(if back { o.next().next() } else { o.next() });
}
3 => s.hide_fx = !s.hide_fx,
4 => s.marks = !s.marks,
5 => s.listed = !s.listed,
_ => {}
}
}
}
impl Page for Section {
fn id(&self) -> &'static str {
"horizon-cosmetics.extras"
}
fn title(&self) -> &'static str {
"horizon.extras.header.cosmetics"
}
fn place(&self) -> Place {
Place::Extras
}
fn rows(&self, rows: &mut Vec<Row>) {
if super::enabled() {
service(|seat| Self::rows_of(seat, rows));
}
}
fn act(&self, row: usize, act: Act) {
let back = match act {
Act::Press => false,
Act::PressBack => true,
_ => return,
};
service(|seat| Self::act_on(seat, row, back));
}
fn tooltip(&self, row: usize) -> Option<Label> {
TOOLTIPS.get(row).copied().map(Label::Key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn loaded(text: &str) -> Settings {
let mut s = Settings::default();
for (key, value) in text.lines().filter_map(|l| l.split_once(':')) {
s.load(key.trim(), value.trim());
}
s
}
fn saved(s: &Settings) -> String {
let mut text = String::new();
s.save(&mut |k, v| text.push_str(&format!("{k}:{v}\n")));
text
}
#[test]
fn the_lines_round_trip_and_the_wardrobe_reads_them() {
let s = Settings {
mine: false,
others: Some(Others::NameTags),
hide_fx: true,
solo: true,
marks: false,
listed: false,
};
let text = saved(&s);
assert_eq!(loaded(&text), s);
// The launcher's Wardrobe reads the same lines.
let wardrobe = prefs::Prefs::parse(&text);
assert_eq!(
(wardrobe.mine, wardrobe.others, wardrobe.hide_fx),
(false, Some(Others::NameTags), true)
);
// No choice made: no `cosmeticsOthers` line; the old switch still reads.
assert!(!saved(&Settings::default()).contains(prefs::OTHERS));
assert_eq!(
loaded("cosmeticsOthers:false\n").others,
Some(Others::Hidden)
);
// A value it can't read keeps the setting.
assert!(loaded("cosmeticsMine:maybe\n").mine);
}
#[test]
fn others_cycle_three_ways_from_what_the_row_shows() {
let mut seat = Seat::default();
// Not chosen, nothing owned: Hidden, and forward wraps to Shown.
assert_eq!(seat.others(), Others::Hidden);
Section::act_on(&mut seat, 2, false);
assert_eq!(seat.settings.others, Some(Others::Shown));
Section::act_on(&mut seat, 2, true);
assert_eq!(seat.settings.others, Some(Others::Hidden));
Section::act_on(&mut seat, 2, true);
assert_eq!(seat.settings.others, Some(Others::NameTags));
// Owning one shows them by default, until the player chooses.
let mut owner = Seat::default();
owner.owner.owns = true;
assert_eq!(owner.others(), Others::Shown);
let mut rows = Vec::new();
Section::rows_of(&owner, &mut rows);
assert_eq!(
rows[2],
Row::Cycle {
label: ROWS[2],
value: Label::Key("horizon.extras.cosmetics.shown")
}
);
assert_eq!(rows.len(), TOOLTIPS.len());
}
}
+4 -17
View File
@@ -800,19 +800,8 @@ impl Game<'_> {
packed,
out,
);
if super::cosmetics::enabled() {
let base = horizon_client::mobs::render::base_transform(m, alpha as f32, camera);
self.cosmetics_geometry(
m,
true,
base,
Some(camera),
alpha as f32,
packed,
false,
out,
);
}
let world = horizon_hooks::players::Pass::World;
self.own_layers_geometry(m, world, camera, alpha as f32, packed, out);
self.player_elytra_geometry(m, chest, camera, alpha as f32, packed, out);
self.spin_attack_geometry(m, camera, alpha as f32, packed, out);
self.shoulder_parrots_geometry(m, camera, alpha as f32, packed, out);
@@ -926,10 +915,8 @@ impl Game<'_> {
240 | 240 << 8,
&mut verts,
);
if super::cosmetics::enabled() {
let base = horizon_client::mobs::render::base_transform(&m, 1.0, camera);
self.cosmetics_geometry(&m, true, base, None, 1.0, 240 | 240 << 8, false, &mut verts);
}
let picture = horizon_hooks::players::Pass::Picture;
self.own_layers_geometry(&m, picture, camera, 1.0, 240 | 240 << 8, &mut verts);
// `WingsLayer` draws through `RenderTypes.armorCutoutNoCull`: the
// wings' faces show from both sides (the elytra texture is empty on
// the face that looks at the body).
+204 -39
View File
@@ -11,17 +11,15 @@ use horizon_client::mobs::anim::{self, BodyPose, PooledPoses, Poses};
use horizon_client::mobs::{Mob, Part, Polygon, render};
use horizon_hooks::engine;
use horizon_hooks::players::{
Bone, CameraView, Frame, Gear, Kind, Mesh, PartPoses, Pass, PlayerDraw, PlayerModel,
PlayerModels, SpriteStore, Sprites,
Bone, CameraView, Frame, Gear, Kind, Light, Look, Mesh, Motion, PartPoses, Particle, Pass,
PlayerDraw, PlayerModel, PlayerModels, SpriteStore, Sprites, View,
};
use std::sync::Arc;
/// Registers the hooks of the Horizon parts still in this crate (cosmetics: spec #14091 step 5
/// moves them out). Before the hooks are sealed.
pub(crate) fn register() {
if super::cosmetics::enabled()
&& let Err(e) = horizon_hooks::register(&super::cosmetics::HOOKS)
{
if let Err(e) = super::cosmetics::register() {
log::error!("Cosmetics: their hooks came too late: {e}");
}
}
@@ -182,6 +180,14 @@ fn part_of(mesh: &Arc<Mesh>) -> Arc<Part> {
})
}
/// Full light, packed as the vertices carry it.
const FULL: u32 = 240 | 240 << 8;
thread_local! {
/// What crates draw see-through this frame, added after every player.
static SEE_THROUGH: std::cell::RefCell<Vec<Vertex>> = const { std::cell::RefCell::new(Vec::new()) };
}
/// Forgets the meshes no crate holds any more (once a frame).
fn forget_meshes() {
let _ = MESHES.try_with(|m| {
@@ -199,7 +205,7 @@ impl PartPoses for PosesOf<'_> {
}
}
/// A player model drawn in the world pass, as a crate draws on it: posed on the GPU's skin path
/// A player model drawn, as a crate draws on it: posed on the GPU's skin path
/// when `gpu` (the mob pass's skin frame, begun; the bounds of what went there grow, for
/// culling), else its vertices go to `out`.
struct DrawPlayer<'a, 'g> {
@@ -208,6 +214,7 @@ struct DrawPlayer<'a, 'g> {
pass: Pass,
/// The model's matrix (`render::base_transform`).
base: glam::Mat4,
camera: glam::DVec3,
alpha: f32,
light: u32,
gpu: bool,
@@ -223,6 +230,19 @@ impl DrawPlayer<'_, '_> {
let (key, _) = render::body(self.m);
self.m.kind.layer(&key).unwrap_or(&self.m.kind.model)
}
/// `light` packed; `at`: the place `Light::AtPlace` reads, relative to the camera.
fn packed(&self, light: Light, at: glam::Vec3) -> u32 {
match light {
Light::Full => FULL,
Light::AtPlace => {
let p = (self.camera + at.as_dvec3()).floor();
let l = self.game.world.light(p.x as i32, p.y as i32, p.z as i32);
(u32::from(l >> 4) * 16) | ((u32::from(l & 15) * 16) << 8)
}
_ => self.light,
}
}
}
impl PlayerDraw for DrawPlayer<'_, '_> {
@@ -234,7 +254,52 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
self.m.age as f32 + self.alpha
}
fn base(&self) -> [[f32; 4]; 4] {
self.base.to_cols_array_2d()
}
fn motion(&self) -> Motion {
let (m, a) = (self.m, self.alpha);
let t = f64::from(a);
let (p, n) = (m.previous, m.position);
let feet = [
p.x + (n.x - p.x) * t,
p.y + (n.y - p.y) * t,
p.z + (n.z - p.z) * t,
];
let turn = horizon_client::mobs::living::wrap(m.head_yaw - m.previous_head_yaw);
let yaw = m.previous_head_yaw + turn * a;
let pitch = m.previous_head_pitch + (m.head_pitch - m.previous_head_pitch) * a;
let asleep = m.extra.avatar_pose == horizon_client::pose::Pose::Sleeping;
let swimming = m.extra.swim.amount(a) > 0.0;
Motion::new(feet, yaw, pitch).poses(
m.extra.crouching,
asleep,
m.extra.fall_flying,
swimming,
)
}
fn view(&self) -> View {
let q = self.game.camera_orientation();
let (right, up) = (
(q * glam::Vec3::X).to_array(),
(q * glam::Vec3::Y).to_array(),
);
let (yaw, pitch) = (self.game.yaw.to_radians(), self.game.pitch.to_radians());
let forward = [
-yaw.sin() * pitch.cos(),
-pitch.sin(),
yaw.cos() * pitch.cos(),
];
let left = [yaw.cos(), 0.0, yaw.sin()];
View::new(self.camera.to_array(), right, up).looking(forward, left)
}
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]> {
if self.pass == Pass::FirstPerson {
return None;
}
if self.body.is_none() {
let mut poses = Poses::pooled();
poses.reset(self.body());
@@ -260,13 +325,30 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
&mut self,
mesh: &Arc<Mesh>,
matrix: [[f32; 4]; 4],
uv: [f32; 4],
look: Look,
pose: &mut dyn FnMut(&mut dyn PartPoses),
) {
let model = part_of(mesh);
let matrix = glam::Mat4::from_cols_array_2d(&matrix);
let (uv, light) = (look.uv, self.packed(look.light, matrix.w_axis.truncate()));
self.poses.reset(&model);
pose(&mut PosesOf(&mut self.poses));
let poses = &self.poses;
if let Some(alpha) = look.see_through {
let color = ((alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
let pose_of = |p: &Part| poses.pose(p);
SEE_THROUGH.with_borrow_mut(|out| {
let start = out.len();
horizon_client::mobs::render_model(&model, matrix, &pose_of, uv, light, 0, out);
for v in &mut out[start..] {
v.color = color;
// Seen through, a face turned away would add to the near one: `entity.wgsl`
// drops back faces, so it is `alpha` at most.
v.normal[3] = horizon_client::mesher::SHADED_CULL_W;
}
});
return;
}
if self.gpu {
let mut models = self.game.skin_models.borrow_mut();
let mut frame = self.game.skin_frame.borrow_mut();
@@ -276,30 +358,43 @@ impl PlayerDraw for DrawPlayer<'_, '_> {
uv,
scroll: 0.0,
color: 0xFFFF_FFFF,
light: self.light,
light,
overlay: 0,
};
if skin.draw(&model, matrix, &self.poses, paint) {
if skin.draw(&model, matrix, poses, paint) {
self.bounds = skin.bounds;
return;
}
}
let poses = &self.poses;
horizon_client::mobs::render_model(
&model,
matrix,
&|p: &Part| poses.pose(p),
uv,
self.light,
0,
self.out,
);
let pose_of = |p: &Part| poses.pose(p);
horizon_client::mobs::render_model(&model, matrix, &pose_of, uv, light, 0, self.out);
}
fn particles(&mut self, uv: [f32; 4], particles: &[Particle], light: Light) {
let light = self.packed(light, glam::Vec3::ZERO);
SEE_THROUGH.with_borrow_mut(|out| {
for q in particles {
let color = ((q.alpha.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF;
for (corner, [u, v]) in q.corners.iter().zip(q.uv) {
out.push(Vertex {
pos: *corner,
uv: [uv[0] + (uv[2] - uv[0]) * u, uv[1] + (uv[3] - uv[1]) * v],
layer: 0,
color,
light,
// Unlit: a particle keeps its colour whichever way it faces.
normal: [0, 0, 0, 0],
glint_uv: [0.0, 0.0],
});
}
}
});
}
}
impl Game<'_> {
/// What crates draw on player model `m` in the world pass (`creatures.rs`), then the
/// cosmetics still drawn here: see [`DrawPlayer`]. The bounds of what went to the GPU.
/// What crates draw on player model `m` in the world pass (`creatures.rs`): see
/// [`DrawPlayer`]. The bounds of what went to the GPU.
#[allow(clippy::too_many_arguments)]
pub(super) fn player_layers_geometry(
&self,
@@ -315,33 +410,102 @@ impl Game<'_> {
self.serving.is_none(),
"a server thread calls no client hook"
);
let kind = kind(m)?;
let mut bounds = None;
if engine::any() {
let model = model(m, kind, self.remote_player_uuid(m.id));
let kind = kind(m).filter(|_| engine::any())?;
let model = model(m, kind, self.remote_player_uuid(m.id));
let mut draw = DrawPlayer {
game: self,
m,
pass: Pass::World,
base,
camera,
alpha,
light,
gpu,
out,
bounds: None,
body: None,
poses: Poses::pooled(),
};
engine::player_layers(&model, &mut draw);
draw.bounds
}
/// What crates draw on the window's own model `m` (its vertices to `out`): in the world in
/// third person (`Pass::World`), or in the inventory's picture of it (`Pass::Picture`).
/// `camera`: the world view's camera, or the picture's.
pub(super) fn own_layers_geometry(
&self,
m: &Mob,
pass: Pass,
camera: glam::DVec3,
alpha: f32,
light: u32,
out: &mut Vec<Vertex>,
) {
debug_assert!(
self.serving.is_none(),
"a server thread calls no client hook"
);
if !engine::any() {
return;
}
let model = model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile()));
let mut draw = DrawPlayer {
game: self,
m,
pass,
base: render::base_transform(m, alpha, camera),
camera,
alpha,
light,
gpu: false,
out,
bounds: None,
body: None,
poses: Poses::pooled(),
};
engine::player_layers(&model, &mut draw);
}
/// After every player (`game.rs`): what crates draw around the window's own model in first
/// person, then what was drawn see-through, in one span.
pub(super) fn players_after_geometry(
&self,
camera: glam::DVec3,
alpha: f64,
out: &mut Vec<Vertex>,
) {
if engine::any()
&& !self.own_body_in_view()
&& let Some(m) = self.player_model.as_ref()
{
let own = model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile()));
let own = own.looks(own.gear, own.invisible || self.own_invisible());
self.mark_entity(out.len(), "minecraft:player");
let mut draw = DrawPlayer {
game: self,
m,
pass: Pass::World,
base,
alpha,
light,
gpu,
pass: Pass::FirstPerson,
base: render::base_transform(m, alpha as f32, camera),
camera,
alpha: alpha as f32,
light: FULL,
gpu: false,
out: &mut *out,
bounds: None,
body: None,
poses: Poses::pooled(),
};
engine::player_layers(&model, &mut draw);
bounds = draw.bounds;
}
let worn = super::cosmetics::enabled()
.then(|| self.cosmetics_geometry(m, false, base, Some(camera), alpha, light, gpu, out))
.flatten();
match (bounds, worn) {
(Some((l, h)), Some((lo, hi))) => Some((l.min(lo), h.max(hi))),
(b, w) => b.or(w),
engine::first_person(&own, &mut draw);
}
let _ = SEE_THROUGH.try_with(|v| {
let mut v = v.borrow_mut();
if !v.is_empty() {
self.mark_translucent_entity(out.len(), "minecraft:player");
out.extend_from_slice(&v);
v.clear();
}
});
}
/// Once a frame, before drawing.
@@ -358,6 +522,7 @@ impl Game<'_> {
let frame = Frame::new(connection, self.integrated.is_some());
engine::frame(&frame, &mut HookStore(self));
forget_meshes();
let _ = SEE_THROUGH.try_with(|v| v.borrow_mut().clear());
self.cosmetics_frame();
}
+38 -5
View File
@@ -77,6 +77,10 @@ pub trait Hooks: Sync {
/// Draws on a player model, after its vanilla layers, each time it is drawn.
fn player_layers(&self, _model: &players::PlayerModel, _draw: &mut dyn players::PlayerDraw) {}
/// Draws around the window's own model while its body is out of view (first person), once a
/// frame after the first-person model: [`players::Pass::FirstPerson`].
fn first_person(&self, _own: &players::PlayerModel, _draw: &mut dyn players::PlayerDraw) {}
/// The resolver of `friend:` addresses this crate offers ([`tunnel`]); the first hook that
/// offers one is used. Asked on the network thread when joining and on the game thread when
/// an address is shown, never per frame.
@@ -214,6 +218,13 @@ pub mod engine {
}
}
/// [`Hooks::first_person`].
pub fn first_person(own: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
for hooks in all() {
hooks.first_person(own, draw);
}
}
/// [`Hooks::camera`].
pub fn camera(own: &players::CameraView) -> Option<players::CameraOverride> {
all().iter().find_map(|h| h.camera(own))
@@ -282,7 +293,14 @@ mod tests {
return;
};
let turn = draw.ticks() / 100.0;
draw.draw(&HAT, head, uv, &mut |p| p.pose("brim")[4] = turn);
let look = players::Look::new(uv);
draw.draw(&HAT, head, look, &mut |p| p.pose("brim")[4] = turn);
}
/// A spark at the eyes, as bright as the head turns.
fn first_person(&self, _: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
let alpha = draw.motion().head_yaw / 90.0;
let spark = players::Particle::new([[0.0; 3]; 4], [[0.0; 2]; 4], alpha);
draw.particles([0.0; 4], &[spark], players::Light::Full);
}
fn tick(&self, models: &mut dyn players::PlayerModels) {
let mut ids = Vec::new();
@@ -359,9 +377,10 @@ mod tests {
))
});
/// A player drawn at age 50, its head where the camera is; logs each draw.
/// A player drawn at age 50, its head where the camera is, turned 45 degrees; logs each draw
/// and its particles' alphas.
#[derive(Default)]
struct Canvas(Vec<(String, [[f32; 4]; 4], [f32; 4], f32)>);
struct Canvas(Vec<(String, [[f32; 4]; 4], [f32; 4], f32)>, Vec<f32>);
impl players::PlayerDraw for Canvas {
fn pass(&self) -> players::Pass {
players::Pass::World
@@ -369,6 +388,15 @@ mod tests {
fn ticks(&self) -> f32 {
50.0
}
fn base(&self) -> [[f32; 4]; 4] {
[[1.0, 0.0, 0.0, 0.0]; 4]
}
fn motion(&self) -> players::Motion {
players::Motion::new([0.0; 3], 45.0, 0.0)
}
fn view(&self) -> players::View {
players::View::default()
}
fn bone(&mut self, bone: players::Bone) -> Option<[[f32; 4]; 4]> {
(bone == players::Bone::Head).then_some([[1.0, 0.0, 0.0, 0.0]; 4])
}
@@ -382,7 +410,7 @@ mod tests {
&mut self,
mesh: &std::sync::Arc<players::Mesh>,
matrix: [[f32; 4]; 4],
uv: [f32; 4],
look: players::Look,
pose: &mut dyn FnMut(&mut dyn players::PartPoses),
) {
struct Rest([f32; 9]);
@@ -393,7 +421,10 @@ mod tests {
}
let mut brim = Rest(mesh.children[0].pose);
pose(&mut brim);
self.0.push((mesh.name.clone(), matrix, uv, brim.0[4]));
self.0.push((mesh.name.clone(), matrix, look.uv, brim.0[4]));
}
fn particles(&mut self, _: [f32; 4], particles: &[players::Particle], _: players::Light) {
self.1.extend(particles.iter().map(|p| p.alpha));
}
}
@@ -442,6 +473,8 @@ mod tests {
0.5
)]
);
engine::first_person(&model(players::Kind::Own), &mut canvas);
assert_eq!(canvas.1, [0.5]);
let mut models = Two(Default::default());
engine::tick(&mut models);
assert!(models.body_pose(3).is_some() && models.body_pose(7).is_some());
+152 -5
View File
@@ -274,8 +274,149 @@ impl Bone {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Pass {
/// In the world, seen by the camera.
/// In the world, seen by the camera (the window's own model too, in third person).
World,
/// The inventory's picture of the window's own model.
Picture,
/// The window's own model while its body is out of view (first person): no bone, only what
/// floats around it ([`crate::Hooks::first_person`]).
FirstPerson,
}
/// How a player model moves this frame ([`PlayerDraw::motion`]), with the frame's part of a tick.
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct Motion {
/// Where its feet are, in the world.
pub feet: [f64; 3],
/// Where its head turns (degrees, the game's yaw and pitch).
pub head_yaw: f32,
pub head_pitch: f32,
pub crouching: bool,
/// Lies in a bed.
pub asleep: bool,
/// Flies with an elytra.
pub flying: bool,
/// Swims (any part of the swimming pose).
pub swimming: bool,
}
impl Motion {
pub fn new(feet: [f64; 3], head_yaw: f32, head_pitch: f32) -> Self {
Self {
feet,
head_yaw,
head_pitch,
..Self::default()
}
}
/// With its poses: crouching, asleep, flying, swimming.
pub fn poses(self, crouching: bool, asleep: bool, flying: bool, swimming: bool) -> Self {
Self {
crouching,
asleep,
flying,
swimming,
..self
}
}
}
/// The camera a player model is drawn for ([`PlayerDraw::view`]).
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct View {
/// Where it is, in the world: matrices and [`Particle`] corners are relative to it.
pub camera: [f64; 3],
/// Its right and up, for what faces it.
pub right: [f32; 3],
pub up: [f32; 3],
/// Where the window's player looks, and their level left (first person).
pub forward: [f32; 3],
pub left: [f32; 3],
}
impl View {
pub fn new(camera: [f64; 3], right: [f32; 3], up: [f32; 3]) -> Self {
Self {
camera,
right,
up,
..Self::default()
}
}
/// With where the window's player looks and their level left.
pub fn looking(self, forward: [f32; 3], left: [f32; 3]) -> Self {
Self {
forward,
left,
..self
}
}
}
/// How a mesh or particles are lit.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Light {
/// As the player model is.
Model,
/// Fully lit: it glows.
Full,
/// By the world where the mesh's origin is (as particles: like [`Light::Model`]).
AtPlace,
}
/// How [`PlayerDraw::draw`] draws a mesh.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Look {
/// The sprite part it is textured by (u0, v0, u1, v1).
pub uv: [f32; 4],
pub light: Light,
/// Drawn see-through at this alpha, after every player, its back faces dropped.
pub see_through: Option<f32>,
}
impl Look {
/// Opaque, lit as the model.
pub fn new(uv: [f32; 4]) -> Self {
Self {
uv,
light: Light::Model,
see_through: None,
}
}
pub fn lit(self, light: Light) -> Self {
Self { light, ..self }
}
pub fn see_through(self, alpha: f32) -> Self {
Self {
see_through: Some(alpha),
..self
}
}
}
/// A quad that faces the camera ([`PlayerDraw::particles`]).
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Particle {
/// Its corners, relative to the camera.
pub corners: [[f32; 3]; 4],
/// Each corner's place across the sprite part (0 to 1).
pub uv: [[f32; 2]; 4],
pub alpha: f32,
}
impl Particle {
pub fn new(corners: [[f32; 3]; 4], uv: [[f32; 2]; 4], alpha: f32) -> Self {
Self { corners, uv, alpha }
}
}
/// The poses of a [`Mesh`]'s parts for one [`PlayerDraw::draw`], each at rest to begin with.
@@ -291,19 +432,25 @@ pub trait PlayerDraw {
fn pass(&self) -> Pass;
/// The model's age in ticks, with the frame's part of a tick.
fn ticks(&self) -> f32;
/// Where `bone` is as the body is drawn this frame; None if the model has no such part.
/// The model's own matrix, its origin at the neck (y down, 16 units a block).
fn base(&self) -> [[f32; 4]; 4];
fn motion(&self) -> Motion;
fn view(&self) -> View;
/// Where `bone` is as the body is drawn this frame; None if the model has no such part (or
/// no body is drawn: [`Pass::FirstPerson`]).
fn bone(&mut self, bone: Bone) -> Option<[[f32; 4]; 4]>;
/// The pose laid over the model's body ([`PlayerModels::set_body_pose`]), if any.
fn body_pose(&self) -> Option<&Arc<dyn BodyPose>>;
/// As [`Sprites::get`].
fn sprite(&self, id: u64) -> Option<[f32; 4]>;
/// Draws `mesh` at `matrix`, textured by the sprite part `uv` (u0, v0, u1, v1) and lit as the
/// model is; `pose` poses its parts first.
/// Draws `mesh` at `matrix` as `look` says; `pose` poses its parts first.
fn draw(
&mut self,
mesh: &Arc<Mesh>,
matrix: [[f32; 4]; 4],
uv: [f32; 4],
look: Look,
pose: &mut dyn FnMut(&mut dyn PartPoses),
);
/// Draws `particles`, textured by the sprite part `uv`, see-through after every player.
fn particles(&mut self, uv: [f32; 4], particles: &[Particle], light: Light);
}
+3 -3
View File
@@ -50,9 +50,9 @@ pub mod hooks {
/// [`Hooks::tick`] get, the pose a crate lays over a player's body, what goes by their names,
/// the crate's sprites, the camera, and what a crate draws on them.
pub use horizon_hooks::players::{
BODY_PARTS, Badge, BodyPose, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Mark,
Mesh, NameDecoration, PartPoses, Pass, PlayerDraw, PlayerModel, PlayerModels, Quad,
SpriteStore, Sprites,
BODY_PARTS, Badge, BodyPose, Bone, CameraOverride, CameraView, Frame, Gear, Kind, Light,
Look, Mark, Mesh, Motion, NameDecoration, PartPoses, Particle, Pass, PlayerDraw,
PlayerModel, PlayerModels, Quad, SpriteStore, Sprites, View,
};
pub use horizon_hooks::{Hooks, Started, TooLate, register, seat, tunnel};
}
-1
View File
@@ -106,7 +106,6 @@
"horizon-client-lib -> horizon-shaders",
"horizon-client-lib -> horizon-telemetry",
"horizon-client-lib -> horizon-theme",
"horizon-client-render -> horizon-cosmetics",
"horizon-client-render -> horizon-hdr",
"horizon-client-render -> horizon-optifine",
"horizon-client-render -> horizon-polytone",