mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
18
Commits
No files matched your search
Generated
-1
@@ -1584,7 +1584,6 @@ dependencies = [
|
||||
"horizon-client-input",
|
||||
"horizon-command",
|
||||
"horizon-core",
|
||||
"horizon-cosmetics",
|
||||
"horizon-hdr",
|
||||
"horizon-hooks",
|
||||
"horizon-items",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -280,25 +280,18 @@ impl Game<'_> {
|
||||
self.spin_attack_geometry(mob, camera, alpha as f32, packed, out);
|
||||
self.stuck_in_body_geometry(mob, mob.id as i32, camera, alpha as f32, packed, out);
|
||||
}
|
||||
// Cosmetics (game/cosmetics.rs), posed on the GPU with the mob's own jobs: culled
|
||||
// with it.
|
||||
// What crates draw on players and mannequins (`game/player_hooks.rs`: cosmetics),
|
||||
// posed on the GPU with the mob's own jobs: culled with it.
|
||||
if !layers_only
|
||||
&& super::super::cosmetics::enabled()
|
||||
&& horizon_hooks::engine::any()
|
||||
&& (is_player || &*mob.kind.id == "minecraft:mannequin")
|
||||
{
|
||||
let mark = self.skin_frame.borrow().mark();
|
||||
let base = horizon_client::mobs::render::base_transform(mob, alpha as f32, camera);
|
||||
let gpu = skinning;
|
||||
if let Some((lo, hi)) = self.cosmetics_geometry(
|
||||
mob,
|
||||
false,
|
||||
base,
|
||||
Some(camera),
|
||||
alpha as f32,
|
||||
packed,
|
||||
gpu,
|
||||
out,
|
||||
) {
|
||||
if let Some((lo, hi)) =
|
||||
self.player_layers_geometry(mob, base, camera, alpha as f32, packed, gpu, out)
|
||||
{
|
||||
skinned = Some(match skinned {
|
||||
Some((first, Some((l, h)))) => (first, Some((l.min(lo), h.max(hi)))),
|
||||
Some((first, None)) => (first, Some((lo, hi))),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -4,21 +4,22 @@
|
||||
//! registered nothing is built.
|
||||
|
||||
use super::Game;
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
use horizon_client::join_seq::JoinList;
|
||||
use horizon_client::mobs::Mob;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
use horizon_client::mesher::Vertex;
|
||||
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::{
|
||||
CameraView, Frame, Gear, Kind, 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}");
|
||||
}
|
||||
}
|
||||
@@ -146,7 +147,367 @@ impl SpriteStore for HookStore<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The crates' meshes as the renderer takes them, by the mesh's address (each entry holds its
|
||||
/// mesh, so the address isn't reused); render thread only.
|
||||
static MESHES: std::cell::RefCell<FxHashMap<usize, (Arc<Mesh>, Arc<Part>)>> =
|
||||
Default::default();
|
||||
}
|
||||
|
||||
/// The game's model of `mesh`, made once.
|
||||
fn part_of(mesh: &Arc<Mesh>) -> Arc<Part> {
|
||||
fn part(m: &Mesh) -> Part {
|
||||
Part {
|
||||
name: m.name.clone(),
|
||||
pose: m.pose,
|
||||
polygons: m
|
||||
.quads
|
||||
.iter()
|
||||
.map(|q| Polygon {
|
||||
normal: q.normal,
|
||||
vertices: q.vertices,
|
||||
cull_back: false,
|
||||
})
|
||||
.collect(),
|
||||
children: m.children.iter().map(part).collect(),
|
||||
}
|
||||
}
|
||||
MESHES.with_borrow_mut(|meshes| {
|
||||
let (_, part_of) = meshes
|
||||
.entry(Arc::as_ptr(mesh) as usize)
|
||||
.or_insert_with(|| (mesh.clone(), Arc::new(part(mesh))));
|
||||
part_of.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
m.borrow_mut()
|
||||
.retain(|_, (mesh, _)| Arc::strong_count(mesh) > 1)
|
||||
});
|
||||
}
|
||||
|
||||
/// `Poses` as a crate poses a mesh's parts.
|
||||
struct PosesOf<'a>(&'a mut Poses);
|
||||
|
||||
impl PartPoses for PosesOf<'_> {
|
||||
fn pose(&mut self, name: &str) -> &mut [f32; 9] {
|
||||
self.0.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
game: &'a Game<'g>,
|
||||
m: &'a Mob,
|
||||
pass: Pass,
|
||||
/// The model's matrix (`render::base_transform`).
|
||||
base: glam::Mat4,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
out: &'a mut Vec<Vertex>,
|
||||
bounds: Option<(glam::Vec3, glam::Vec3)>,
|
||||
/// The body as it is drawn, posed on the first `bone`.
|
||||
body: Option<PooledPoses>,
|
||||
poses: PooledPoses,
|
||||
}
|
||||
|
||||
impl DrawPlayer<'_, '_> {
|
||||
fn body(&self) -> &Part {
|
||||
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<'_, '_> {
|
||||
fn pass(&self) -> Pass {
|
||||
self.pass
|
||||
}
|
||||
|
||||
fn ticks(&self) -> f32 {
|
||||
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());
|
||||
let data = self.game.res.mob_data();
|
||||
let state = anim::state(self.m, self.alpha);
|
||||
anim::setup(self.m, &state, &mut poses, &data.animations);
|
||||
self.body = Some(poses);
|
||||
}
|
||||
let poses = self.body.as_ref()?;
|
||||
let at = render::part_matrix(self.body(), self.base, poses, bone.part(), 0.0)?;
|
||||
Some(at.to_cols_array_2d())
|
||||
}
|
||||
|
||||
fn body_pose(&self) -> Option<&Arc<dyn BodyPose>> {
|
||||
self.m.emote.as_ref()
|
||||
}
|
||||
|
||||
fn sprite(&self, id: u64) -> Option<[f32; 4]> {
|
||||
HookSprites(self.game).get(id)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 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();
|
||||
let mut skin = horizon_client::mobs::skin::Skin::new(&mut models, &mut frame);
|
||||
skin.bounds = self.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) {
|
||||
self.bounds = skin.bounds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
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`): see
|
||||
/// [`DrawPlayer`]. The bounds of what went to the GPU.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn player_layers_geometry(
|
||||
&self,
|
||||
m: &Mob,
|
||||
base: glam::Mat4,
|
||||
camera: glam::DVec3,
|
||||
alpha: f32,
|
||||
light: u32,
|
||||
gpu: bool,
|
||||
out: &mut Vec<Vertex>,
|
||||
) -> Option<(glam::Vec3, glam::Vec3)> {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
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::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::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.
|
||||
pub(super) fn client_hooks_frame(&mut self) {
|
||||
debug_assert!(
|
||||
@@ -160,6 +521,8 @@ impl Game<'_> {
|
||||
let connection = self.remote.as_ref().map_or(0, |r| r.session.serial());
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,13 @@ pub trait Hooks: Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -204,6 +211,20 @@ pub mod engine {
|
||||
all().iter().find_map(|h| h.player_mark(uuid))
|
||||
}
|
||||
|
||||
/// [`Hooks::player_layers`].
|
||||
pub fn player_layers(model: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
for hooks in all() {
|
||||
hooks.player_layers(model, draw);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`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))
|
||||
@@ -266,6 +287,21 @@ mod tests {
|
||||
fn camera(&self, own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
Some(players::CameraOverride::new(own.age / 10.0))
|
||||
}
|
||||
/// A hat on the head, its brim turned by the model's age.
|
||||
fn player_layers(&self, _: &players::PlayerModel, draw: &mut dyn players::PlayerDraw) {
|
||||
let (Some(head), Some(uv)) = (draw.bone(players::Bone::Head), draw.sprite(1)) else {
|
||||
return;
|
||||
};
|
||||
let turn = draw.ticks() / 100.0;
|
||||
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();
|
||||
models.for_each(&mut |m| ids.push(m.id));
|
||||
@@ -330,6 +366,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
static HAT: std::sync::LazyLock<std::sync::Arc<players::Mesh>> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
let brim = players::Mesh::new("brim".into(), [0.0; 9], Vec::new(), Vec::new());
|
||||
std::sync::Arc::new(players::Mesh::new(
|
||||
"hat".into(),
|
||||
[0.0; 9],
|
||||
Vec::new(),
|
||||
vec![brim],
|
||||
))
|
||||
});
|
||||
|
||||
/// 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)>, Vec<f32>);
|
||||
impl players::PlayerDraw for Canvas {
|
||||
fn pass(&self) -> players::Pass {
|
||||
players::Pass::World
|
||||
}
|
||||
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])
|
||||
}
|
||||
fn body_pose(&self) -> Option<&std::sync::Arc<dyn players::BodyPose>> {
|
||||
None
|
||||
}
|
||||
fn sprite(&self, id: u64) -> Option<[f32; 4]> {
|
||||
(id == 1).then_some([0.0, 0.0, 0.5, 0.5])
|
||||
}
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &std::sync::Arc<players::Mesh>,
|
||||
matrix: [[f32; 4]; 4],
|
||||
look: players::Look,
|
||||
pose: &mut dyn FnMut(&mut dyn players::PartPoses),
|
||||
) {
|
||||
struct Rest([f32; 9]);
|
||||
impl players::PartPoses for Rest {
|
||||
fn pose(&mut self, _: &str) -> &mut [f32; 9] {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
let mut brim = Rest(mesh.children[0].pose);
|
||||
pose(&mut brim);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements nothing: the defaults do nothing.
|
||||
struct Silent;
|
||||
impl Hooks for Silent {}
|
||||
@@ -364,6 +462,19 @@ mod tests {
|
||||
assert_eq!(engine::player_mark(8), None);
|
||||
let camera = engine::camera(&players::CameraView::new(5.0));
|
||||
assert_eq!(camera.map(|c| c.distance), Some(0.5));
|
||||
let mut canvas = Canvas::default();
|
||||
engine::player_layers(&model(players::Kind::Player), &mut canvas);
|
||||
assert_eq!(
|
||||
canvas.0,
|
||||
[(
|
||||
"hat".to_owned(),
|
||||
[[1.0, 0.0, 0.0, 0.0]; 4],
|
||||
[0.0, 0.0, 0.5, 0.5],
|
||||
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());
|
||||
|
||||
@@ -210,3 +210,247 @@ impl CameraOverride {
|
||||
Self { distance }
|
||||
}
|
||||
}
|
||||
|
||||
/// A model a crate draws on players, as the game's own models are: a tree of parts, 16 units a
|
||||
/// block, y down. Build it once and keep the `Arc`: the engine prepares it for the GPU once, by
|
||||
/// the `Arc`'s address, while the crate holds it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct Mesh {
|
||||
/// What [`PartPoses::pose`] names it by.
|
||||
pub name: String,
|
||||
/// At rest: x, y, z, x/y/z rotation (radians), x/y/z scale.
|
||||
pub pose: [f32; 9],
|
||||
pub quads: Vec<Quad>,
|
||||
pub children: Vec<Mesh>,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
pub fn new(name: String, pose: [f32; 9], quads: Vec<Quad>, children: Vec<Mesh>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
pose,
|
||||
quads,
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One face of a [`Mesh`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Quad {
|
||||
pub normal: [f32; 3],
|
||||
/// Its corners: x, y, z, then u, v across the sprite (0 to 1).
|
||||
pub vertices: [[f32; 5]; 4],
|
||||
}
|
||||
|
||||
impl Quad {
|
||||
pub fn new(normal: [f32; 3], vertices: [[f32; 5]; 4]) -> Self {
|
||||
Self { normal, vertices }
|
||||
}
|
||||
}
|
||||
|
||||
/// A part of the player's body a crate draws on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Bone {
|
||||
Head,
|
||||
Body,
|
||||
RightArm,
|
||||
LeftArm,
|
||||
RightLeg,
|
||||
LeftLeg,
|
||||
}
|
||||
|
||||
impl Bone {
|
||||
/// Its name among [`BODY_PARTS`].
|
||||
pub fn part(self) -> &'static str {
|
||||
BODY_PARTS[self as usize]
|
||||
}
|
||||
}
|
||||
|
||||
/// Which picture of a player is drawn.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Pass {
|
||||
/// 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.
|
||||
pub trait PartPoses {
|
||||
/// Part `name`'s pose, to change (see [`Mesh::pose`]); a name the mesh lacks gets a pose that
|
||||
/// is not used.
|
||||
fn pose(&mut self, name: &str) -> &mut [f32; 9];
|
||||
}
|
||||
|
||||
/// Where a crate draws on a player model ([`crate::Hooks::player_layers`]). Matrices are
|
||||
/// column-major, relative to the camera, in blocks.
|
||||
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;
|
||||
/// 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` as `look` says; `pose` poses its parts first.
|
||||
fn draw(
|
||||
&mut self,
|
||||
mesh: &Arc<Mesh>,
|
||||
matrix: [[f32; 4]; 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);
|
||||
}
|
||||
@@ -48,10 +48,11 @@ pub use horizon_server_files as server_files;
|
||||
pub mod hooks {
|
||||
/// The players the game shows (`horizon_hooks::players`): the views [`Hooks::frame`] and
|
||||
/// [`Hooks::tick`] get, the pose a crate lays over a player's body, what goes by their names,
|
||||
/// the crate's sprites and the camera.
|
||||
/// the crate's sprites, the camera, and what a crate draws on them.
|
||||
pub use horizon_hooks::players::{
|
||||
BODY_PARTS, Badge, BodyPose, CameraOverride, CameraView, Frame, Gear, Kind, Mark,
|
||||
NameDecoration, PlayerModel, PlayerModels, 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};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user