mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00aba43d81 | ||
|
|
e893fd3eac | ||
|
|
ee986c3e62 | ||
|
|
4848a9eff5 | ||
|
|
95b0cb05e7 | ||
|
|
8902d94a19 | ||
|
|
ca6140d05e |
No files matched your search
@@ -20,14 +20,17 @@ 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::{Frame, Kind, PlayerModel, PlayerModels};
|
||||
use horizon_hooks::players::{
|
||||
Badge, CameraOverride, CameraView, Frame, Gear, Kind, Mark, NameDecoration, PlayerModel,
|
||||
PlayerModels, SpriteStore, Sprites,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Horizon's mark before a Horizon player's name, over their name tag and in the player list.
|
||||
pub(super) const MARK: horizon_client::tab_list::NameMark = &horizon_cosmetics::mark::MARK;
|
||||
const MARK: Mark = &horizon_cosmetics::mark::MARK;
|
||||
|
||||
/// An item as the renderer takes it.
|
||||
struct Baked {
|
||||
@@ -397,17 +400,13 @@ fn slot_image(item: &Item) -> Vec<u8> {
|
||||
|
||||
/// Which slots an avatar's gear covers: a helmet or head item hides a hat, an elytra or a cape
|
||||
/// what is worn on the back. A name tag style is never drawn on the body.
|
||||
fn covered(m: &Mob, slot: Slot) -> bool {
|
||||
fn covered(gear: Gear, slot: Slot) -> bool {
|
||||
match slot {
|
||||
Slot::Head => m.extra.head.is_some(),
|
||||
Slot::Back => m.cape.is_some() || m.extra.chest.as_deref() == Some("minecraft:elytra"),
|
||||
Slot::Head => gear.head,
|
||||
Slot::Back => gear.back,
|
||||
Slot::Shoulder => false,
|
||||
// A chestplate, not an elytra.
|
||||
Slot::Torso => m
|
||||
.extra
|
||||
.chest
|
||||
.as_deref()
|
||||
.is_some_and(|c| c != "minecraft:elytra"),
|
||||
Slot::Torso => gear.chest,
|
||||
Slot::NameTag | Slot::Emote => true,
|
||||
// On no bone: drawn by a pass of their own, never on the body.
|
||||
Slot::Companion | Slot::Trail | Slot::Aura => true,
|
||||
@@ -419,12 +418,12 @@ fn covered(m: &Mob, slot: Slot) -> bool {
|
||||
/// (`worn` is None), another player's at "name tags only" or hidden, or every item `covered` (a
|
||||
/// helmet over a hat). One max over at most five items, read when the item was parsed.
|
||||
fn body_top(
|
||||
extras: &horizon_client::extras::Extras,
|
||||
others: Others,
|
||||
own: bool,
|
||||
worn: Option<&horizon_cosmetics::Worn>,
|
||||
covered: impl Fn(Slot) -> bool,
|
||||
) -> Option<f32> {
|
||||
if !own && extras.effective_others() != Others::Shown {
|
||||
if !own && others != Others::Shown {
|
||||
return None;
|
||||
}
|
||||
worn?
|
||||
@@ -462,6 +461,14 @@ struct Seat {
|
||||
said: Said,
|
||||
/// Kept between ticks so a tick allocates nothing: the player models in view.
|
||||
models: Vec<PlayerModel>,
|
||||
/// The world is the window's own single-player one (`Frame::solo`).
|
||||
solo: bool,
|
||||
/// Name tag badges' sprites, by the item's address: their id (never reused) and the item,
|
||||
/// held so the address isn't reused while it is there.
|
||||
sprites: FxHashMap<usize, (u64, Arc<Item>)>,
|
||||
last_sprite: u64,
|
||||
/// Sprites drawing missed, put in the atlas next frame.
|
||||
wanted: Vec<u64>,
|
||||
}
|
||||
|
||||
/// What the game tells the hooks before each call, until the settings move here (spec #14091
|
||||
@@ -472,6 +479,14 @@ struct Said {
|
||||
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.
|
||||
playing: Option<(String, bool)>,
|
||||
/// A folder's first emote, which mannequins play.
|
||||
@@ -496,13 +511,39 @@ pub(super) struct Hooks;
|
||||
pub(super) static HOOKS: Hooks = Hooks;
|
||||
|
||||
impl horizon_hooks::Hooks for Hooks {
|
||||
fn frame(&self, _frame: &Frame) {
|
||||
service(Seat::frame);
|
||||
fn frame(&self, frame: &Frame, sprites: &mut dyn SpriteStore) {
|
||||
service(|s| {
|
||||
s.solo = frame.solo;
|
||||
s.frame();
|
||||
s.put_sprites(sprites);
|
||||
});
|
||||
}
|
||||
|
||||
fn tick(&self, models: &mut dyn PlayerModels) {
|
||||
service(|s| s.tick(models));
|
||||
}
|
||||
|
||||
fn name_decoration(
|
||||
&self,
|
||||
model: &PlayerModel,
|
||||
sprites: &dyn Sprites,
|
||||
) -> Option<NameDecoration> {
|
||||
service(|s| s.name_decoration(model, sprites))
|
||||
}
|
||||
|
||||
fn player_mark(&self, uuid: u128) -> Option<Mark> {
|
||||
service(|s| {
|
||||
let client = s.client.as_ref().filter(|_| s.said.marks)?;
|
||||
client.frame().horizon(uuid).then_some(MARK)
|
||||
})
|
||||
}
|
||||
|
||||
/// An emote pulls the camera back: it eases out over the emote's first quarter second.
|
||||
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);
|
||||
Some(CameraOverride::new(0.35 + 0.65 * t * t * (3.0 - 2.0 * t)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Seat {
|
||||
@@ -609,6 +650,81 @@ impl Seat {
|
||||
}
|
||||
}
|
||||
|
||||
/// What goes by `m`'s name: Horizon's mark if they play Horizon (and the mark is on), the
|
||||
/// name tag style they wear if cosmetics are shown for them (name tags only counts), and how
|
||||
/// high what is drawn on their body gets (`body_top`). One look in the cache for all three.
|
||||
fn name_decoration(
|
||||
&mut self,
|
||||
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) {
|
||||
return None;
|
||||
}
|
||||
let uuid = match m.kind {
|
||||
Kind::Own => own_uuid(m.uuid, self.solo, said.solo, || {
|
||||
horizon_client::auth::current().map(|s| s.uuid)
|
||||
}),
|
||||
Kind::Mannequin if self.local => Some(u128::from(m.id)),
|
||||
Kind::Player => m.uuid,
|
||||
_ => None,
|
||||
}?;
|
||||
let mut frame = self.client.as_ref()?.frame();
|
||||
let mark = (said.marks && frame.horizon(uuid)).then_some(MARK);
|
||||
let worn = if styles { frame.worn(uuid) } else { None };
|
||||
drop(frame);
|
||||
let others = if said.others_shown {
|
||||
Others::Shown
|
||||
} else {
|
||||
Others::NameTags
|
||||
};
|
||||
let top = body_top(others, own, worn.as_ref(), |slot| covered(m.gear, slot))
|
||||
.filter(|_| !m.invisible);
|
||||
let badge = worn.and_then(|w| self.badge(&w, sprites));
|
||||
Some(NameDecoration::new(mark, badge, top))
|
||||
}
|
||||
|
||||
/// The name tag style in `worn`: its badge's place in the atlas and its line's colour. None
|
||||
/// until the badge is in the atlas (it is put there next frame).
|
||||
fn badge(&mut self, worn: &horizon_cosmetics::Worn, sprites: &dyn Sprites) -> Option<Badge> {
|
||||
let item = worn.iter().find(|i| i.slot == Slot::NameTag)?;
|
||||
let [r, g, b] = item.tag?.line;
|
||||
let next = self.last_sprite + 1;
|
||||
let (id, _) = *self
|
||||
.sprites
|
||||
.entry(Arc::as_ptr(item) as usize)
|
||||
.or_insert_with(|| (next, item.clone()));
|
||||
self.last_sprite = self.last_sprite.max(id);
|
||||
let Some([u0, v0, u1, v1]) = sprites.get(id) else {
|
||||
if !self.wanted.contains(&id) {
|
||||
self.wanted.push(id);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
// The mark sits in the slot's top left corner.
|
||||
let u1 = u0 + (u1 - u0) * item.texture.width as f32 / 64.0;
|
||||
let v1 = v0 + (v1 - v0) * item.texture.height as f32 / 64.0;
|
||||
Some(Badge::new(
|
||||
[u0, v0, u1, v1],
|
||||
u32::from_be_bytes([0, r, g, b]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Puts the sprites drawing missed in the atlas; forgets those of items no one wears.
|
||||
fn put_sprites(&mut self, store: &mut dyn SpriteStore) {
|
||||
for id in self.wanted.drain(..) {
|
||||
let item = self.sprites.values().find(|(i, _)| *i == id);
|
||||
if let Some((_, item)) = item.filter(|_| !store.has(id)) {
|
||||
store.put(id, slot_image(item));
|
||||
}
|
||||
}
|
||||
self.sprites
|
||||
.retain(|_, (_, item)| Arc::strong_count(item) > 1);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -705,11 +821,16 @@ impl Game<'_> {
|
||||
};
|
||||
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));
|
||||
}
|
||||
@@ -775,81 +896,6 @@ impl Game<'_> {
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Items from a folder (`HORIZON_COSMETICS_LOCAL`): mannequins are dressed and marked too.
|
||||
pub(super) fn cosmetics_local(&self) -> bool {
|
||||
self.cosmetics.local
|
||||
}
|
||||
|
||||
/// What goes by `m`'s name: Horizon's mark if they play Horizon (and the mark is on), the
|
||||
/// name tag style they wear if cosmetics are shown for them (name tags only counts), and how
|
||||
/// high what is drawn on their body gets (`body_top`). One look in the cache for all three.
|
||||
pub(super) fn cosmetics_marks(&self, m: &Mob, own: bool) -> super::names::Marks {
|
||||
let mut marks = super::names::Marks::default();
|
||||
let extras = &self.settings.extras;
|
||||
let styles = if own {
|
||||
extras.cosmetics_mine
|
||||
} else {
|
||||
extras.effective_others() != Others::Hidden
|
||||
};
|
||||
if !(styles || extras.horizon_marks) {
|
||||
return marks;
|
||||
}
|
||||
let (Some(client), Some(uuid)) = (&self.cosmetics.client, self.cosmetics_uuid(m, own))
|
||||
else {
|
||||
return marks;
|
||||
};
|
||||
let mut frame = client.frame();
|
||||
marks.horizon = extras.horizon_marks && frame.horizon(uuid);
|
||||
let worn = if styles { frame.worn(uuid) } else { None };
|
||||
drop(frame);
|
||||
if !horizon_client::mobs::trader::is_invisible(m) {
|
||||
marks.top = body_top(extras, own, worn.as_ref(), |slot| covered(m, slot));
|
||||
}
|
||||
marks.style = worn.and_then(|w| self.cosmetics_tag(&w));
|
||||
marks
|
||||
}
|
||||
|
||||
/// Those of `uuids` who play Horizon, with the mark on (the player list): one look each.
|
||||
pub(super) fn horizon_players(
|
||||
&self,
|
||||
uuids: impl Iterator<Item = u128>,
|
||||
) -> std::collections::HashSet<u128> {
|
||||
match &self.cosmetics.client {
|
||||
Some(c) if self.settings.extras.horizon_marks => {
|
||||
let mut frame = c.frame();
|
||||
uuids.filter(|u| frame.horizon(*u)).collect()
|
||||
}
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The name tag style in `worn`: its badge's place in the atlas and its line's colour. None
|
||||
/// until the badge is in the atlas.
|
||||
fn cosmetics_tag(&self, worn: &horizon_cosmetics::Worn) -> Option<super::names::TagStyle> {
|
||||
let item = worn.iter().find(|i| i.slot == Slot::NameTag)?;
|
||||
let [r, g, b] = item.tag?.line;
|
||||
let frame = self.cosmetics.frame.get();
|
||||
let mut baked = self.cosmetics.baked.borrow_mut();
|
||||
let entry = baked
|
||||
.entry(Arc::as_ptr(item) as usize)
|
||||
.or_insert_with(|| Baked::new(item, frame));
|
||||
let Some(slot) = self.gui.atlas.sprites.get(&entry.location) else {
|
||||
self.cosmetics.uploads.borrow_mut().push(item.clone());
|
||||
return None;
|
||||
};
|
||||
if entry.marked.replace(frame) != frame {
|
||||
self.skin_downloads.borrow_mut().drawn(&entry.location);
|
||||
}
|
||||
// The mark sits in the slot's top left corner.
|
||||
let mut mark = *slot;
|
||||
mark.u1 = mark.u0 + (slot.u1 - slot.u0) * item.texture.width as f32 / 64.0;
|
||||
mark.v1 = mark.v0 + (slot.v1 - slot.v0) * item.texture.height as f32 / 64.0;
|
||||
Some(super::names::TagStyle {
|
||||
mark,
|
||||
line: u32::from_be_bytes([0, r, g, b]),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
@@ -941,7 +987,7 @@ impl Game<'_> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if emoting.is_none() && covered(m, item.slot) {
|
||||
if emoting.is_none() && covered(super::player_hooks::gear(m), item.slot) {
|
||||
continue;
|
||||
}
|
||||
let Some((models, uv)) = self.cosmetics_baked(item) else {
|
||||
@@ -1247,13 +1293,18 @@ mod tests {
|
||||
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, false, worn.as_ref(), |_| false)
|
||||
body_top(extras.effective_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, false, worn.as_ref(), |_| false)),
|
||||
tag(body_top(
|
||||
shown.effective_others(),
|
||||
false,
|
||||
worn.as_ref(),
|
||||
|_| false
|
||||
)),
|
||||
VANILLA
|
||||
);
|
||||
// A small hat under the tag: vanilla's height too.
|
||||
@@ -1267,7 +1318,9 @@ 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, false, worn.as_ref(), |slot| slot == Slot::Head);
|
||||
let helmet = body_top(shown.effective_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] {
|
||||
|
||||
@@ -43,6 +43,11 @@ impl Emotes {
|
||||
let looping = p.item.emote.as_ref().is_some_and(|e| e.looping);
|
||||
Some((&p.item.id, looping))
|
||||
}
|
||||
|
||||
/// When the emote that pulled the camera back started (the own model's age), while it plays.
|
||||
pub(in crate::game) fn pull(&self) -> Option<f32> {
|
||||
self.camera_from.and(self.playing.as_ref()).map(|p| p.start)
|
||||
}
|
||||
}
|
||||
|
||||
/// An emote on a player's model: the engine's pose hook (`Mob::emote`), which names no cosmetics
|
||||
@@ -277,19 +282,6 @@ impl Game<'_> {
|
||||
}
|
||||
self.tick_emote();
|
||||
}
|
||||
|
||||
/// How far the camera stands back while an emote pulls it (1: the usual distance): it eases
|
||||
/// out over the emote's first quarter second.
|
||||
pub(in crate::game) fn emote_camera_pull(&self, partial: f32) -> f32 {
|
||||
let (Some(p), Some(m)) = (&self.cosmetics.emotes.playing, &self.player_model) else {
|
||||
return 1.0;
|
||||
};
|
||||
if self.cosmetics.emotes.camera_from.is_none() {
|
||||
return 1.0;
|
||||
}
|
||||
let t = ((m.age as f32 + partial - p.start) / 5.0).clamp(0.0, 1.0);
|
||||
0.35 + 0.65 * t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -300,11 +300,7 @@ impl Game<'_> {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let marks = if player || self.cosmetics_local() {
|
||||
self.cosmetics_marks(m, Some(m.id) == own)
|
||||
} else {
|
||||
Marks::default()
|
||||
};
|
||||
let marks = self.hooks_name_marks(m, Some(m.id) == own);
|
||||
let at = match marks.top {
|
||||
Some(top) => p + glam::DVec3::Y * cosmetic_lift(top, m.model_scale()),
|
||||
None => p,
|
||||
@@ -770,10 +766,10 @@ impl Game<'_> {
|
||||
if background { BACKGROUND } else { 0 },
|
||||
),
|
||||
};
|
||||
if n == last && marks.horizon {
|
||||
if let Some(mark) = marks.mark.filter(|_| n == last) {
|
||||
let x = -self.gui.atlas.font.width_text(text) / 2.0 - 10.0;
|
||||
// 8 text pixels square, a pixel left of the background.
|
||||
for &(mx, my, w, h, argb) in super::cosmetics::MARK {
|
||||
for &(mx, my, w, h, argb) in mark {
|
||||
let a = text_alpha * (argb >> 24) / 255;
|
||||
batch.fill(
|
||||
&self.gui.atlas,
|
||||
@@ -937,10 +933,11 @@ struct NameTag {
|
||||
marks: Marks,
|
||||
}
|
||||
|
||||
/// What Horizon draws by a player's name: its mark (they play Horizon) and their name tag style.
|
||||
/// What a hook draws by a player's name (`horizon_hooks::players::NameDecoration`): a mark
|
||||
/// before it (Horizon's: they play Horizon) and a name tag style.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct Marks {
|
||||
pub horizon: bool,
|
||||
pub mark: Option<horizon_hooks::players::Mark>,
|
||||
pub style: Option<TagStyle>,
|
||||
/// How high the cosmetics drawn on their body get above the neck, in model pixels: the tag
|
||||
/// goes up over a tall one (`cosmetic_lift`).
|
||||
|
||||
@@ -311,8 +311,8 @@ impl Game<'_> {
|
||||
}
|
||||
};
|
||||
let mut view = self.view;
|
||||
// An emote pulls the camera back from the player's face (`game/cosmetics/emotes.rs`).
|
||||
let pull = self.emote_camera_pull(partial);
|
||||
// A hook may pull the camera back (an emote, from the player's face).
|
||||
let pull = self.hooks_camera_distance(partial);
|
||||
let kind = match self.camera_type {
|
||||
CameraType::FirstPerson => horizon_client::camera::CameraType::FirstPerson,
|
||||
CameraType::ThirdPersonBack => horizon_client::camera::CameraType::ThirdPersonBack,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Where the game hands the player models to crates above the engine
|
||||
//! (`horizon_hooks::Hooks::frame` and `tick`, spec #14091): only a window's game calls them,
|
||||
//! never the integrated server's thread or a dedicated server. With no hook registered nothing is
|
||||
//! built.
|
||||
//! Where the game hands the player models to crates above the engine (`horizon_hooks::Hooks`'s
|
||||
//! `frame`, `tick`, `name_decoration`, `player_mark`, `camera`; spec #14091): only a window's
|
||||
//! game calls them, never the integrated server's thread or a dedicated server. With no hook
|
||||
//! registered nothing is built.
|
||||
|
||||
use super::Game;
|
||||
use horizon_client::join_seq::JoinList;
|
||||
use horizon_client::mobs::Mob;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
use horizon_hooks::engine;
|
||||
use horizon_hooks::players::{Frame, Kind, PlayerModel, PlayerModels};
|
||||
use horizon_hooks::players::{
|
||||
CameraView, Frame, Gear, Kind, PlayerModel, PlayerModels, SpriteStore, Sprites,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Registers the hooks of the Horizon parts still in this crate (cosmetics: spec #14091 step 5
|
||||
@@ -49,9 +51,20 @@ fn kind(m: &Mob) -> Option<Kind> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What `m` wears where a crate may draw: a helmet or head item, a chestplate, a cape or elytra.
|
||||
pub(super) fn gear(m: &Mob) -> Gear {
|
||||
let elytra = m.extra.chest.as_deref() == Some("minecraft:elytra");
|
||||
Gear::new(
|
||||
m.extra.head.is_some(),
|
||||
m.extra.chest.is_some() && !elytra,
|
||||
m.cape.is_some() || elytra,
|
||||
)
|
||||
}
|
||||
|
||||
fn model(m: &Mob, kind: Kind, uuid: Option<u128>) -> PlayerModel {
|
||||
let (dx, dz) = (m.position.x - m.previous.x, m.position.z - m.previous.z);
|
||||
PlayerModel::new(m.id, kind, uuid, m.age, dx * dx + dz * dz > 1e-4)
|
||||
.looks(gear(m), horizon_client::mobs::trader::is_invisible(m))
|
||||
}
|
||||
|
||||
impl PlayerModels for Models<'_> {
|
||||
@@ -61,7 +74,7 @@ impl PlayerModels for Models<'_> {
|
||||
|
||||
fn for_each(&self, f: &mut dyn FnMut(PlayerModel)) {
|
||||
if let Some(m) = self.own.as_deref() {
|
||||
f(model(m, Kind::Own, None));
|
||||
f(model(m, Kind::Own, Some(self.remote.profile())));
|
||||
}
|
||||
for m in self.mobs.iter() {
|
||||
if let Some(kind) = kind(m) {
|
||||
@@ -92,6 +105,47 @@ impl PlayerModels for Models<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sprite `id`'s place in the atlas's names (`hooks/<hex>`), written into `buf`: no allocation.
|
||||
fn location(id: u64, buf: &mut [u8; 22]) -> &str {
|
||||
use std::io::Write;
|
||||
let mut out = std::io::Cursor::new(&mut buf[..]);
|
||||
let _ = write!(out, "hooks/{id:x}");
|
||||
let len = out.position() as usize;
|
||||
std::str::from_utf8(&buf[..len]).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The crates' sprites in the GUI atlas, in the skin slots: read while drawing.
|
||||
struct HookSprites<'a, 'g>(&'a Game<'g>);
|
||||
|
||||
impl Sprites for HookSprites<'_, '_> {
|
||||
fn get(&self, id: u64) -> Option<[f32; 4]> {
|
||||
let mut buf = [0; 22];
|
||||
let at = location(id, &mut buf);
|
||||
let r = self.0.gui.atlas.sprites.get(at)?;
|
||||
// Kept this frame (`skin_slots.next_frame`).
|
||||
self.0.skin_downloads.borrow_mut().drawn(at);
|
||||
Some([r.u0, r.v0, r.u1, r.v1])
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a crate's frame hook puts its sprites.
|
||||
struct HookStore<'a, 'g>(&'a mut Game<'g>);
|
||||
|
||||
impl SpriteStore for HookStore<'_, '_> {
|
||||
fn has(&self, id: u64) -> bool {
|
||||
self.0
|
||||
.gui
|
||||
.atlas
|
||||
.sprites
|
||||
.contains_key(location(id, &mut [0; 22]))
|
||||
}
|
||||
|
||||
fn put(&mut self, id: u64, rgba: Vec<u8>) {
|
||||
let at = location(id, &mut [0; 22]).to_owned();
|
||||
self.0.store_skin(at, rgba);
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Once a frame, before drawing.
|
||||
pub(super) fn client_hooks_frame(&mut self) {
|
||||
@@ -104,7 +158,8 @@ impl Game<'_> {
|
||||
}
|
||||
self.cosmetics_say();
|
||||
let connection = self.remote.as_ref().map_or(0, |r| r.session.serial());
|
||||
engine::frame(&Frame::new(connection));
|
||||
let frame = Frame::new(connection, self.integrated.is_some());
|
||||
engine::frame(&frame, &mut HookStore(self));
|
||||
self.cosmetics_frame();
|
||||
}
|
||||
|
||||
@@ -128,4 +183,68 @@ impl Game<'_> {
|
||||
remote,
|
||||
});
|
||||
}
|
||||
|
||||
/// What goes by `m`'s name (`own`: the window's own player), from the hooks.
|
||||
pub(super) fn hooks_name_marks(&self, m: &Mob, own: bool) -> super::names::Marks {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
let mut marks = super::names::Marks::default();
|
||||
if !engine::any() {
|
||||
return marks;
|
||||
}
|
||||
let model = match (own, kind(m)) {
|
||||
(true, _) => model(m, Kind::Own, self.remote.as_ref().map(|r| r.profile())),
|
||||
(false, Some(kind)) => model(m, kind, self.remote_player_uuid(m.id)),
|
||||
(false, None) => return marks,
|
||||
};
|
||||
if let Some(d) = engine::name_decoration(&model, &HookSprites(self)) {
|
||||
marks.mark = d.mark;
|
||||
marks.top = d.top;
|
||||
marks.style = d.badge.map(|b| {
|
||||
let [u0, v0, u1, v1] = b.uv;
|
||||
super::names::TagStyle {
|
||||
mark: horizon_client::hud::Rect {
|
||||
u0,
|
||||
v0,
|
||||
u1,
|
||||
v1,
|
||||
w: 8,
|
||||
h: 8,
|
||||
},
|
||||
line: b.line,
|
||||
}
|
||||
});
|
||||
}
|
||||
marks
|
||||
}
|
||||
|
||||
/// How far the third-person camera stands back, as a share of the usual distance: a hook's
|
||||
/// (an emote pulls it back), else 1.
|
||||
pub(super) fn hooks_camera_distance(&self, partial: f32) -> f32 {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
match &self.player_model {
|
||||
Some(m) if engine::any() => {
|
||||
engine::camera(&CameraView::new(m.age as f32 + partial)).map_or(1.0, |c| c.distance)
|
||||
}
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sprite_locations_fit_the_buffer_and_differ_by_id() {
|
||||
let mut a = [0; 22];
|
||||
let mut b = [0; 22];
|
||||
assert_eq!(location(u64::MAX, &mut a), "hooks/ffffffffffffffff");
|
||||
assert_eq!(location(10, &mut b), "hooks/a");
|
||||
}
|
||||
}
|
||||
@@ -900,10 +900,12 @@ impl Game<'_> {
|
||||
let board = &self.score.board;
|
||||
let objective = board.display_objective(sb::SLOT_LIST);
|
||||
let up = |id: u128| upside_down.contains(&id);
|
||||
// Horizon's mark before the names of Horizon players (the cosmetics service's cache).
|
||||
let horizon = self.horizon_players(players.iter().map(|p| p.uuid));
|
||||
let horizon_mark = super::cosmetics::MARK;
|
||||
let marked = |id: u128| horizon.contains(&id).then_some(horizon_mark);
|
||||
// A hook's mark before a name (Horizon's: they play Horizon).
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
let marked = horizon_hooks::engine::player_mark;
|
||||
let inputs = TabInputs {
|
||||
players: &players,
|
||||
board,
|
||||
|
||||
@@ -44,14 +44,36 @@ pub trait Hooks: Sync {
|
||||
/// The game is ending normally: its window closed and the world is saved. Not after a crash.
|
||||
fn shutdown(&self) {}
|
||||
|
||||
/// Once a frame, before drawing. Only a window's game calls it (each splitscreen player in
|
||||
/// turn, [`seat`]), never the integrated server's thread or a dedicated server.
|
||||
fn frame(&self, _frame: &players::Frame) {}
|
||||
/// Once a frame, before drawing: `sprites` takes the pictures the crate's
|
||||
/// [`players::Sprites`] lacked. Only a window's game calls it (each splitscreen player in
|
||||
/// turn, [`seat`]), never the integrated server's thread or a dedicated server; so are the
|
||||
/// player methods below (not [`Hooks::tunnel`]).
|
||||
fn frame(&self, _frame: &players::Frame, _sprites: &mut dyn players::SpriteStore) {}
|
||||
|
||||
/// Once a client tick while a world is shown: the player models in view. Called as
|
||||
/// [`Hooks::frame`] is.
|
||||
/// Once a client tick while a world is shown: the player models in view.
|
||||
fn tick(&self, _models: &mut dyn players::PlayerModels) {}
|
||||
|
||||
/// What goes by a player model's name over its head, each frame its name tag is drawn. The
|
||||
/// first hook that answers is used.
|
||||
fn name_decoration(
|
||||
&self,
|
||||
_model: &players::PlayerModel,
|
||||
_sprites: &dyn players::Sprites,
|
||||
) -> Option<players::NameDecoration> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The mark before the name of the player `uuid` in the player list, each frame it shows.
|
||||
/// The first hook that answers is used.
|
||||
fn player_mark(&self, _uuid: u128) -> Option<players::Mark> {
|
||||
None
|
||||
}
|
||||
|
||||
/// How the camera stands in third person, each frame. The first hook that answers is used.
|
||||
fn camera(&self, _own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
None
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -163,12 +185,30 @@ pub mod engine {
|
||||
}
|
||||
|
||||
/// [`Hooks::frame`].
|
||||
pub fn frame(frame: &players::Frame) {
|
||||
pub fn frame(frame: &players::Frame, sprites: &mut dyn players::SpriteStore) {
|
||||
for hooks in all() {
|
||||
hooks.frame(frame);
|
||||
hooks.frame(frame, sprites);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::name_decoration`].
|
||||
pub fn name_decoration(
|
||||
model: &players::PlayerModel,
|
||||
sprites: &dyn players::Sprites,
|
||||
) -> Option<players::NameDecoration> {
|
||||
all().iter().find_map(|h| h.name_decoration(model, sprites))
|
||||
}
|
||||
|
||||
/// [`Hooks::player_mark`].
|
||||
pub fn player_mark(uuid: u128) -> Option<players::Mark> {
|
||||
all().iter().find_map(|h| h.player_mark(uuid))
|
||||
}
|
||||
|
||||
/// [`Hooks::camera`].
|
||||
pub fn camera(own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
all().iter().find_map(|h| h.camera(own))
|
||||
}
|
||||
|
||||
/// [`Hooks::tick`].
|
||||
pub fn tick(models: &mut dyn players::PlayerModels) {
|
||||
for hooks in all() {
|
||||
@@ -202,11 +242,29 @@ mod tests {
|
||||
fn published(&self, open: bool) {
|
||||
self.0.lock().unwrap().push(format!("published {open}"));
|
||||
}
|
||||
fn frame(&self, frame: &players::Frame) {
|
||||
fn frame(&self, frame: &players::Frame, sprites: &mut dyn players::SpriteStore) {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("frame {}", frame.connection));
|
||||
if !sprites.has(1) {
|
||||
sprites.put(1, vec![0; 64 * 64 * 4]);
|
||||
}
|
||||
}
|
||||
fn name_decoration(
|
||||
&self,
|
||||
model: &players::PlayerModel,
|
||||
sprites: &dyn players::Sprites,
|
||||
) -> Option<players::NameDecoration> {
|
||||
let badge = sprites.get(1).map(|uv| players::Badge::new(uv, 0xFF0000));
|
||||
(model.kind == players::Kind::Player)
|
||||
.then(|| players::NameDecoration::new(Some(MARK), badge, Some(2.0)))
|
||||
}
|
||||
fn player_mark(&self, uuid: u128) -> Option<players::Mark> {
|
||||
(uuid == 9).then_some(MARK)
|
||||
}
|
||||
fn camera(&self, own: &players::CameraView) -> Option<players::CameraOverride> {
|
||||
Some(players::CameraOverride::new(own.age / 10.0))
|
||||
}
|
||||
fn tick(&self, models: &mut dyn players::PlayerModels) {
|
||||
let mut ids = Vec::new();
|
||||
@@ -253,6 +311,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
const MARK: players::Mark = &[(0, 0, 8, 8, 0xFF00_00FF)];
|
||||
|
||||
/// One sprite slot, as the game's atlas has many.
|
||||
#[derive(Default)]
|
||||
struct Atlas(Option<(u64, usize)>);
|
||||
impl players::SpriteStore for Atlas {
|
||||
fn has(&self, id: u64) -> bool {
|
||||
self.0.is_some_and(|(i, _)| i == id)
|
||||
}
|
||||
fn put(&mut self, id: u64, rgba: Vec<u8>) {
|
||||
self.0 = Some((id, rgba.len()));
|
||||
}
|
||||
}
|
||||
impl players::Sprites for Atlas {
|
||||
fn get(&self, id: u64) -> Option<[f32; 4]> {
|
||||
players::SpriteStore::has(self, id).then_some([0.0, 0.0, 0.5, 0.5])
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements nothing: the defaults do nothing.
|
||||
struct Silent;
|
||||
impl Hooks for Silent {}
|
||||
@@ -270,7 +347,23 @@ mod tests {
|
||||
}
|
||||
engine::shutdown();
|
||||
assert!(engine::any());
|
||||
engine::frame(&players::Frame::new(4));
|
||||
let mut atlas = Atlas::default();
|
||||
let model = |kind| players::PlayerModel::new(5, kind, Some(9), 0, false);
|
||||
// Before the frame put the sprite in: the decoration without its badge.
|
||||
let before = engine::name_decoration(&model(players::Kind::Player), &atlas).unwrap();
|
||||
assert!(before.badge.is_none() && before.top == Some(2.0));
|
||||
engine::frame(&players::Frame::new(4, false), &mut atlas);
|
||||
assert_eq!(atlas.0, Some((1, 64 * 64 * 4)));
|
||||
let after = engine::name_decoration(&model(players::Kind::Player), &atlas).unwrap();
|
||||
assert_eq!(
|
||||
after.badge.map(|b| (b.uv, b.line)),
|
||||
Some(([0.0, 0.0, 0.5, 0.5], 0xFF0000))
|
||||
);
|
||||
assert!(engine::name_decoration(&model(players::Kind::Mannequin), &atlas).is_none());
|
||||
assert_eq!(engine::player_mark(9), Some(MARK));
|
||||
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 models = Two(Default::default());
|
||||
engine::tick(&mut models);
|
||||
assert!(models.body_pose(3).is_some() && models.body_pose(7).is_some());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The players the game shows, as a crate above the engine sees them (Horizon's emotes, spec
|
||||
//! #14091): [`PlayerModels`] in [`crate::Hooks::tick`], [`Frame`] in [`crate::Hooks::frame`], and
|
||||
//! [`BodyPose`], a pose laid over a player's body after its vanilla animation. Plain types only:
|
||||
//! no engine type crosses this line.
|
||||
//! #14091): [`PlayerModels`] in [`crate::Hooks::tick`], [`Frame`] in [`crate::Hooks::frame`],
|
||||
//! [`BodyPose`], a pose laid over a player's body after its vanilla animation, what goes by their
|
||||
//! names, and the crate's [`Sprites`]. Plain types only: no engine type crosses this line.
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The player model's parts a [`BodyPose`] poses, in its order.
|
||||
@@ -44,12 +44,17 @@ pub struct PlayerModel {
|
||||
/// Its entity id on this connection ([`PlayerModels::connection`]).
|
||||
pub id: u32,
|
||||
pub kind: Kind,
|
||||
/// The player's UUID as the server gave it; None for the window's own model and mannequins.
|
||||
/// The player's UUID as the server gave it (the window's own: its profile); None for
|
||||
/// mannequins.
|
||||
pub uuid: Option<u128>,
|
||||
/// Its age in ticks, the clock a [`BodyPose`] is timed on.
|
||||
pub age: u32,
|
||||
/// It moved sideways since the last tick (more than 0.01 blocks).
|
||||
pub moved: bool,
|
||||
/// What it wears that covers a crate's drawing.
|
||||
pub gear: Gear,
|
||||
/// It is drawn invisible (the effect, or a trader's disguise).
|
||||
pub invisible: bool,
|
||||
}
|
||||
|
||||
impl PlayerModel {
|
||||
@@ -60,8 +65,37 @@ impl PlayerModel {
|
||||
uuid,
|
||||
age,
|
||||
moved,
|
||||
gear: Gear::default(),
|
||||
invisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// With what it wears and whether it is invisible.
|
||||
pub fn looks(self, gear: Gear, invisible: bool) -> Self {
|
||||
Self {
|
||||
gear,
|
||||
invisible,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a player model wears where a crate may draw.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct Gear {
|
||||
/// A helmet or a head item.
|
||||
pub head: bool,
|
||||
/// A chestplate (an elytra is on the back).
|
||||
pub chest: bool,
|
||||
/// A cape or an elytra.
|
||||
pub back: bool,
|
||||
}
|
||||
|
||||
impl Gear {
|
||||
pub fn new(head: bool, chest: bool, back: bool) -> Self {
|
||||
Self { head, chest, back }
|
||||
}
|
||||
}
|
||||
|
||||
/// The player models of the world this window shows, once a tick ([`crate::Hooks::tick`]).
|
||||
@@ -83,10 +117,96 @@ pub trait PlayerModels {
|
||||
pub struct Frame {
|
||||
/// As [`PlayerModels::connection`]; 0 with no world.
|
||||
pub connection: u64,
|
||||
/// The world is the window's own single-player one: its server names the player by an
|
||||
/// offline profile.
|
||||
pub solo: bool,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(connection: u64) -> Self {
|
||||
Self { connection }
|
||||
pub fn new(connection: u64, solo: bool) -> Self {
|
||||
Self { connection, solo }
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangles drawn before a name: x, y, width, height in text pixels from the name line's top
|
||||
/// left corner (a pixel left of its background), and `0xAARRGGBB`.
|
||||
pub type Mark = &'static [(u8, u8, u8, u8, u32)];
|
||||
|
||||
/// What a crate draws by a player's name over their head ([`crate::Hooks::name_decoration`]).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct NameDecoration {
|
||||
/// Before the name.
|
||||
pub mark: Option<Mark>,
|
||||
/// After the name, and a line under it.
|
||||
pub badge: Option<Badge>,
|
||||
/// How high the crate's drawing on the body gets above the neck, in model pixels: the tag
|
||||
/// goes up over it.
|
||||
pub top: Option<f32>,
|
||||
}
|
||||
|
||||
impl NameDecoration {
|
||||
pub fn new(mark: Option<Mark>, badge: Option<Badge>, top: Option<f32>) -> Self {
|
||||
Self { mark, badge, top }
|
||||
}
|
||||
}
|
||||
|
||||
/// A badge after a name: 8 text pixels square, a pixel right of the background.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Badge {
|
||||
/// Its picture: a part of a [`Sprites`] sprite, in atlas coordinates (u0, v0, u1, v1).
|
||||
pub uv: [f32; 4],
|
||||
/// The line under the name, `0xRRGGBB`.
|
||||
pub line: u32,
|
||||
}
|
||||
|
||||
impl Badge {
|
||||
pub fn new(uv: [f32; 4], line: u32) -> Self {
|
||||
Self { uv, line }
|
||||
}
|
||||
}
|
||||
|
||||
/// A crate's pictures in the game's atlas, by ids of its own: 64×64 RGBA each, in the slots skins
|
||||
/// use (one no frame drew for a while may go). Read while drawing.
|
||||
pub trait Sprites {
|
||||
/// Where sprite `id` is (u0, v0, u1, v1), and it is kept this frame. None until
|
||||
/// [`SpriteStore::put`] put it there.
|
||||
fn get(&self, id: u64) -> Option<[f32; 4]>;
|
||||
}
|
||||
|
||||
/// Where a crate puts its [`Sprites`], in [`crate::Hooks::frame`].
|
||||
pub trait SpriteStore {
|
||||
/// Sprite `id` is in the atlas.
|
||||
fn has(&self, id: u64) -> bool;
|
||||
/// Puts sprite `id` in the atlas (64×64 RGBA), if a slot is free.
|
||||
fn put(&mut self, id: u64, rgba: Vec<u8>);
|
||||
}
|
||||
|
||||
/// The window's own model, as [`crate::Hooks::camera`] sees it.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct CameraView {
|
||||
/// Its age in ticks, with the frame's part of a tick.
|
||||
pub age: f32,
|
||||
}
|
||||
|
||||
impl CameraView {
|
||||
pub fn new(age: f32) -> Self {
|
||||
Self { age }
|
||||
}
|
||||
}
|
||||
|
||||
/// How a crate moves the camera in third person.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct CameraOverride {
|
||||
/// How far it stands back, as a share of the usual distance.
|
||||
pub distance: f32,
|
||||
}
|
||||
|
||||
impl CameraOverride {
|
||||
pub fn new(distance: f32) -> Self {
|
||||
Self { distance }
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,11 @@ pub use horizon_server_files as server_files;
|
||||
/// in"). The engine's dispatch side is not part of the API.
|
||||
pub mod hooks {
|
||||
/// The players the game shows (`horizon_hooks::players`): the views [`Hooks::frame`] and
|
||||
/// [`Hooks::tick`] get, and the pose a crate lays over a player's body.
|
||||
/// [`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.
|
||||
pub use horizon_hooks::players::{
|
||||
BODY_PARTS, BodyPose, Frame, Kind, PlayerModel, PlayerModels,
|
||||
BODY_PARTS, Badge, BodyPose, CameraOverride, CameraView, Frame, Gear, Kind, Mark,
|
||||
NameDecoration, PlayerModel, PlayerModels, SpriteStore, Sprites,
|
||||
};
|
||||
pub use horizon_hooks::{Hooks, Started, TooLate, register, seat, tunnel};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user