mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 10:39:46 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19aabc08c2 | ||
|
|
ca3649534d | ||
|
|
a7f07a08de | ||
|
|
0e528f7287 | ||
|
|
7ca4fd68bd | ||
|
|
547b1d4959 | ||
|
|
e2918de5aa | ||
|
|
e6b0a03270 | ||
|
|
8f198d9cbd | ||
|
|
bd90908a72 | ||
|
|
a8d445f56c |
No files matched your search
@@ -337,6 +337,13 @@ impl Settings {
|
||||
.and_then(|mode| self.video_modes.iter().position(|m| *m == mode))
|
||||
.map_or(-1, |i| i as i32)
|
||||
}
|
||||
/// Loads only the crates' settings stores (`horizon_hooks::settings`) from the files next to
|
||||
/// `path`, into the seat whose turn it is (`horizon_hooks::seat`): a splitscreen player whose
|
||||
/// settings were made from P1's rather than loaded.
|
||||
pub fn load_stores(path: &Path) {
|
||||
let dir = path.parent();
|
||||
horizon_hooks::settings::engine::load(&mut |file| fs::read_to_string(dir?.join(file)).ok());
|
||||
}
|
||||
pub fn load(path: &Path) -> Self {
|
||||
let mut horizon = path
|
||||
.parent()
|
||||
|
||||
@@ -23,8 +23,9 @@ pub fn main_with(mods: crate::mod_api::Mods) {
|
||||
horizon_client::telemetry::mark_start();
|
||||
// Opt-in telemetry keeps each crash report (docs/telemetry.md), whatever the mode.
|
||||
crash::on_report(horizon_client::telemetry::keep_crash);
|
||||
// Crates above the engine registered their hooks before this (horizon-official's `main`);
|
||||
// none registers after this point.
|
||||
// Crates above the engine registered their hooks before this (horizon-official's `main`),
|
||||
// then the Horizon parts still in this crate; none registers after this point.
|
||||
game::player_hooks::register();
|
||||
horizon_hooks::engine::seal();
|
||||
horizon_client::spans::startup::init(std::time::Instant::now());
|
||||
horizon_client::spans::trace::start_from_env();
|
||||
|
||||
@@ -373,6 +373,7 @@ mod place_feature_tests;
|
||||
mod play_input;
|
||||
mod play_keys;
|
||||
mod player_equip;
|
||||
pub(crate) mod player_hooks;
|
||||
mod player_spawn;
|
||||
mod pointer_capture;
|
||||
#[cfg(test)]
|
||||
@@ -7492,7 +7493,6 @@ impl Game<'_> {
|
||||
self.spawn_phantoms();
|
||||
self.tick_advancements();
|
||||
self.tick_player_model();
|
||||
self.tick_emotes();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,10 @@ 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 std::cell::{Cell, RefCell};
|
||||
use std::sync::Arc;
|
||||
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.
|
||||
@@ -55,7 +56,8 @@ impl Baked {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Cosmetics {
|
||||
client: Option<horizon_cosmetics::Cosmetics>,
|
||||
/// The hooks' client (`Seat`), taken once it started.
|
||||
client: Option<Arc<horizon_cosmetics::Cosmetics>>,
|
||||
/// By the item's address; each entry holds its item, so the address isn't reused.
|
||||
baked: RefCell<FxHashMap<usize, Baked>>,
|
||||
/// Items drawn whose texture isn't in the atlas yet.
|
||||
@@ -65,16 +67,6 @@ pub(super) struct Cosmetics {
|
||||
poses: RefCell<(Poses, Poses)>,
|
||||
/// Item files from a folder (`HORIZON_COSMETICS_LOCAL`): mannequins wear them too.
|
||||
local: bool,
|
||||
/// Show my cosmetics on its way to the service.
|
||||
mine: AccountSync,
|
||||
/// Show me as a Horizon player: sent at start, on each change and every hour while on.
|
||||
listed: AccountSync,
|
||||
/// Whether the signed-in account owns a cosmetic.
|
||||
owner: OwnerCheck,
|
||||
/// Which other players play an emote (`horizon_cosmetics::Feed`), started with the client.
|
||||
feed: Option<horizon_cosmetics::Feed>,
|
||||
/// My emote on its way to the service.
|
||||
emote_out: EmoteOut,
|
||||
/// Companions, trails and auras.
|
||||
fx: RefCell<Fx>,
|
||||
/// Emotes: those the wheel offers and the one playing (`cosmetics/emotes.rs`).
|
||||
@@ -449,115 +441,302 @@ pub(super) fn enabled() -> bool {
|
||||
cfg!(feature = "horizon-extras") && horizon_cosmetics::enabled()
|
||||
}
|
||||
|
||||
/// What the cosmetics' hooks keep for one splitscreen player (`horizon_hooks::seat`): the
|
||||
/// service's client, the emote feed and what the account tells the service.
|
||||
#[derive(Default)]
|
||||
struct Seat {
|
||||
client: Option<Arc<horizon_cosmetics::Cosmetics>>,
|
||||
/// Item files from a folder (`HORIZON_COSMETICS_LOCAL`).
|
||||
local: bool,
|
||||
/// Show my cosmetics on its way to the service.
|
||||
mine: AccountSync,
|
||||
/// Show me as a Horizon player: sent at start, on each change and every hour while on.
|
||||
listed: AccountSync,
|
||||
/// Whether the signed-in account owns a cosmetic.
|
||||
owner: OwnerCheck,
|
||||
/// Which other players play an emote (`horizon_cosmetics::Feed`), started with the client.
|
||||
feed: Option<horizon_cosmetics::Feed>,
|
||||
/// My emote on its way to the service.
|
||||
emote_out: EmoteOut,
|
||||
/// What the game's settings and the player's emote say (`Game::cosmetics_say`).
|
||||
said: Said,
|
||||
/// Kept between ticks so a tick allocates nothing: the player models in view.
|
||||
models: Vec<PlayerModel>,
|
||||
}
|
||||
|
||||
/// What the game tells the hooks before each call, until the settings move here (spec #14091
|
||||
/// step 5a) and the player's emote with them (step 4).
|
||||
#[derive(Default)]
|
||||
struct Said {
|
||||
mine: bool,
|
||||
listed: bool,
|
||||
/// Other players' cosmetics are shown (not "name tags only", not hidden).
|
||||
others_shown: bool,
|
||||
/// My emote while it plays: its id and whether it loops.
|
||||
playing: Option<(String, bool)>,
|
||||
/// A folder's first emote, which mannequins play.
|
||||
first: Option<Arc<Item>>,
|
||||
}
|
||||
|
||||
/// One [`Seat`] per splitscreen player.
|
||||
static SERVICES: Mutex<Vec<Seat>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Runs `f` on this seat's [`Seat`]. Never called inside itself.
|
||||
fn service<R>(f: impl FnOnce(&mut Seat) -> R) -> R {
|
||||
let seat = horizon_hooks::seat();
|
||||
let mut all = SERVICES.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if all.len() <= seat {
|
||||
all.resize_with(seat + 1, Seat::default);
|
||||
}
|
||||
f(&mut all[seat])
|
||||
}
|
||||
|
||||
/// The cosmetics' hooks, registered when [`enabled`].
|
||||
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 tick(&self, models: &mut dyn PlayerModels) {
|
||||
service(|s| s.tick(models));
|
||||
}
|
||||
}
|
||||
|
||||
impl Seat {
|
||||
/// Once a frame: starts the service's client on first use, asks whether the account owns a
|
||||
/// cosmetic, and tells the service what the account shows.
|
||||
fn frame(&mut self) {
|
||||
if self.client.is_none() {
|
||||
let (client, local) = Cosmetics::client();
|
||||
self.client = Some(Arc::new(client));
|
||||
self.local = local;
|
||||
// Others' emotes come from the service only (a local folder has no one playing).
|
||||
if !local {
|
||||
self.feed = Some(horizon_cosmetics::Feed::new(http()));
|
||||
}
|
||||
}
|
||||
// Item files from a folder (the art, the bench): shown as to an owner, nothing asked.
|
||||
if self.local {
|
||||
self.owner.owns = true;
|
||||
return;
|
||||
}
|
||||
// Whether the account owns a cosmetic: what the last run learnt, then the service.
|
||||
let o = &mut self.owner;
|
||||
if !o.started {
|
||||
o.started = true;
|
||||
o.owns = account()
|
||||
.is_some_and(|(_, uuid, home)| prefs::owned(&home, &uuid).unwrap_or(false));
|
||||
}
|
||||
if let Some(done) = o.asking.as_ref().and_then(|rx| rx.try_recv().ok()) {
|
||||
o.asking = None;
|
||||
match done {
|
||||
Ok(owns) => (o.owns, o.known, o.retry) = (owns, true, None),
|
||||
Err(_) => o.retry = backoff(o.retry),
|
||||
}
|
||||
}
|
||||
// One sign-in at a time: Mojang keeps one joined server id per account.
|
||||
let due = !o.known && o.retry.is_none_or(|(at, _)| Instant::now() >= at);
|
||||
if due && o.asking.is_none() && self.mine.sending.is_none() && self.listed.sending.is_none()
|
||||
{
|
||||
let (tx, rx) = channel();
|
||||
o.asking = Some(rx);
|
||||
std::thread::Builder::new()
|
||||
.name("Cosmetics account".into())
|
||||
.spawn(move || {
|
||||
let _ = tx.send(check_owner());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
// Show my cosmetics: sent once per change of the wish, then retried until confirmed.
|
||||
// Show me as a Horizon player: also said again every hour while on.
|
||||
let every = horizon_cosmetics::account::PRESENCE_EVERY;
|
||||
self.mine.settle(None);
|
||||
self.listed.settle(Some(every));
|
||||
// One sign-in at a time, as for the ownership check above.
|
||||
let busy = self.owner.asking.is_some()
|
||||
|| self.mine.sending.is_some()
|
||||
|| self.listed.sending.is_some();
|
||||
let said = &self.said;
|
||||
if let Some(done) = self
|
||||
.emote_out
|
||||
.sending
|
||||
.as_ref()
|
||||
.and_then(|rx| rx.try_recv().ok())
|
||||
{
|
||||
self.emote_out.sending = None;
|
||||
match done {
|
||||
Ok(()) => self.emote_out.retry = None,
|
||||
Err(_) => {
|
||||
self.emote_out.retry = backoff(self.emote_out.retry);
|
||||
self.emote_out.sent = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// My emote: only while I show myself as a Horizon player (playing one says so).
|
||||
let playing = said.playing.as_ref();
|
||||
let looping = playing.is_some_and(|(_, looping)| *looping);
|
||||
let wish = playing.map(|(id, _)| id.as_str()).filter(|_| said.listed);
|
||||
let o = &mut self.emote_out;
|
||||
let changed = o.sent.as_ref().map(Option::as_deref) != Some(wish);
|
||||
let again = looping && o.again.is_some_and(|at| Instant::now() >= at);
|
||||
let waited = o.retry.is_none_or(|(at, _)| Instant::now() >= at);
|
||||
// Nothing sent yet and nothing playing: nothing to say (a failed send forgets what
|
||||
// was sent: the service ends a one-shot by itself, a loop within a minute).
|
||||
let quiet = wish.is_none() && o.sent.is_none();
|
||||
let emote_due = !quiet && waited && (changed || again);
|
||||
// One sign-in at a time, the emote's included.
|
||||
if busy || o.sending.is_some() {
|
||||
} else if emote_due {
|
||||
let wish = wish.map(str::to_owned);
|
||||
o.sent = Some(wish.clone());
|
||||
o.again = looping.then(|| Instant::now() + LOOP_AGAIN);
|
||||
let (tx, rx) = channel();
|
||||
o.sending = Some(rx);
|
||||
let kept = o.kept.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("Cosmetics emote".into())
|
||||
.spawn(move || {
|
||||
let _ = tx.send(send_emote(&kept, wish.as_deref()));
|
||||
})
|
||||
.ok();
|
||||
} else if self.mine.due(said.mine) {
|
||||
self.mine.start(said.mine, send_mine);
|
||||
} else if self.listed.due(said.listed) {
|
||||
self.listed.start(said.listed, send_listed);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn tick(&mut self, models: &mut dyn PlayerModels) {
|
||||
let mut seen = std::mem::take(&mut self.models);
|
||||
seen.clear();
|
||||
models.for_each(&mut |m| {
|
||||
if m.kind != Kind::Own {
|
||||
seen.push(m);
|
||||
}
|
||||
});
|
||||
self.play_others(models, &mut seen);
|
||||
self.models = seen;
|
||||
}
|
||||
|
||||
fn play_others(&self, models: &mut dyn PlayerModels, view: &mut Vec<PlayerModel>) {
|
||||
let shown = self.said.others_shown;
|
||||
// Items from a folder (the art, the bench): every mannequin plays its first emote, again
|
||||
// and again.
|
||||
if self.local && shown {
|
||||
let Some(first) = &self.said.first else {
|
||||
return;
|
||||
};
|
||||
for m in view.iter().filter(|m| m.kind == Kind::Mannequin) {
|
||||
let age = m.age as f32;
|
||||
let now = emotes::pose_playing(models.body_pose(m.id));
|
||||
if now.is_none_or(|p| p.at(age).is_none()) {
|
||||
let playing = horizon_cosmetics::Playing {
|
||||
item: first.clone(),
|
||||
// Out of step, so a crowd doesn't move as one.
|
||||
start: age - (m.id % 40) as f32,
|
||||
stop: None,
|
||||
};
|
||||
emotes::set_model_playing(models, m.id, Some(playing));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let (Some(feed), Some(client)) = (&self.feed, &self.client) else {
|
||||
return;
|
||||
};
|
||||
if !shown {
|
||||
feed.ask([]);
|
||||
for m in view.iter() {
|
||||
emotes::set_model_playing(models, m.id, None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut frame = client.frame();
|
||||
view.retain(|m| m.kind == Kind::Player && m.uuid.is_some_and(|u| frame.horizon(u)));
|
||||
drop(frame);
|
||||
feed.ask(view.iter().filter_map(|m| m.uuid));
|
||||
let now = Instant::now();
|
||||
for m in view.iter() {
|
||||
let Some(uuid) = m.uuid else { continue };
|
||||
let seen = feed.playing(uuid);
|
||||
let age = m.age as f32;
|
||||
let mut next = emotes::pose_playing(models.body_pose(m.id)).cloned();
|
||||
match (seen, &mut next) {
|
||||
(Some(seen), Some(p)) if Arc::ptr_eq(&p.item, &seen.item) => {
|
||||
if m.moved && p.stop.is_none() {
|
||||
p.stop = Some(age);
|
||||
}
|
||||
}
|
||||
(Some(seen), slot) if !m.moved => {
|
||||
let ticks = now.duration_since(seen.started).as_secs_f32() * 20.0;
|
||||
*slot = Some(horizon_cosmetics::Playing {
|
||||
item: seen.item,
|
||||
start: age - ticks,
|
||||
stop: None,
|
||||
});
|
||||
}
|
||||
(_, Some(p)) if p.stop.is_none() => p.stop = Some(age),
|
||||
_ => {}
|
||||
}
|
||||
emotes::set_model_playing(models, m.id, next.filter(|p| p.at(age).is_some()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Once a frame, before drawing: starts the service's client on first use and puts the
|
||||
/// Hands the hooks what the game's settings and the player's emote say (allocating only when
|
||||
/// the emote changes).
|
||||
pub(super) fn cosmetics_say(&mut self) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let first = if self.cosmetics.local {
|
||||
service(|s| s.said.first.is_none())
|
||||
.then(|| self.owned_emotes().first().cloned())
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let extras = &self.settings.extras;
|
||||
let playing = self.cosmetics.emotes.sending();
|
||||
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;
|
||||
if said.playing.as_ref().map(|(id, l)| (id.as_str(), *l)) != playing {
|
||||
said.playing = playing.map(|(id, l)| (id.to_owned(), l));
|
||||
}
|
||||
if first.is_some() {
|
||||
said.first = first;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Once a frame, after the hooks: takes the service's client for drawing and puts the
|
||||
/// textures the last frame wanted into the atlas.
|
||||
pub(super) fn cosmetics_frame(&mut self) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let c = &mut self.cosmetics;
|
||||
if c.client.is_none() {
|
||||
let (client, local) = Cosmetics::client();
|
||||
c.client = Some(client);
|
||||
c.local = local;
|
||||
// Others' emotes come from the service only (a local folder has no one playing).
|
||||
if !local {
|
||||
c.feed = Some(horizon_cosmetics::Feed::new(http()));
|
||||
let owns = service(|s| {
|
||||
if c.client.is_none() {
|
||||
c.client = s.client.clone();
|
||||
c.local = s.local;
|
||||
}
|
||||
}
|
||||
s.owner.owns
|
||||
});
|
||||
// Read every frame: the settings button shows it, and it is never saved.
|
||||
self.settings.extras.cosmetics_owner = owns;
|
||||
c.frame.set(c.frame.get().wrapping_add(1));
|
||||
c.fx.get_mut().frame(c.frame.get());
|
||||
// Item files from a folder (the art, the bench): shown as to an owner, nothing asked.
|
||||
if c.local {
|
||||
c.owner.owns = true;
|
||||
} else {
|
||||
// Whether the account owns a cosmetic: what the last run learnt, then the service.
|
||||
let o = &mut c.owner;
|
||||
if !o.started {
|
||||
o.started = true;
|
||||
o.owns = account()
|
||||
.is_some_and(|(_, uuid, home)| prefs::owned(&home, &uuid).unwrap_or(false));
|
||||
}
|
||||
if let Some(done) = o.asking.as_ref().and_then(|rx| rx.try_recv().ok()) {
|
||||
o.asking = None;
|
||||
match done {
|
||||
Ok(owns) => (o.owns, o.known, o.retry) = (owns, true, None),
|
||||
Err(_) => o.retry = backoff(o.retry),
|
||||
}
|
||||
}
|
||||
// One sign-in at a time: Mojang keeps one joined server id per account.
|
||||
let due = !o.known && o.retry.is_none_or(|(at, _)| Instant::now() >= at);
|
||||
if due && o.asking.is_none() && c.mine.sending.is_none() && c.listed.sending.is_none() {
|
||||
let (tx, rx) = channel();
|
||||
o.asking = Some(rx);
|
||||
std::thread::Builder::new()
|
||||
.name("Cosmetics account".into())
|
||||
.spawn(move || {
|
||||
let _ = tx.send(check_owner());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
// Read every frame: the settings button shows it, and it is never saved.
|
||||
self.settings.extras.cosmetics_owner = c.owner.owns;
|
||||
// Show my cosmetics: sent once per change of the wish, then retried until confirmed.
|
||||
// Show me as a Horizon player: also said again every hour while on.
|
||||
if !c.local {
|
||||
let every = horizon_cosmetics::account::PRESENCE_EVERY;
|
||||
c.mine.settle(None);
|
||||
c.listed.settle(Some(every));
|
||||
// One sign-in at a time, as for the ownership check above.
|
||||
let busy =
|
||||
c.owner.asking.is_some() || c.mine.sending.is_some() || c.listed.sending.is_some();
|
||||
let extras = &self.settings.extras;
|
||||
if let Some(done) = c
|
||||
.emote_out
|
||||
.sending
|
||||
.as_ref()
|
||||
.and_then(|rx| rx.try_recv().ok())
|
||||
{
|
||||
c.emote_out.sending = None;
|
||||
match done {
|
||||
Ok(()) => c.emote_out.retry = None,
|
||||
Err(_) => {
|
||||
c.emote_out.retry = backoff(c.emote_out.retry);
|
||||
c.emote_out.sent = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// My emote: only while I show myself as a Horizon player (playing one says so).
|
||||
let playing = c.emotes.sending();
|
||||
let looping = playing.as_ref().is_some_and(|(_, looping)| *looping);
|
||||
let wish = playing.map(|(id, _)| id).filter(|_| extras.horizon_listed);
|
||||
let o = &mut c.emote_out;
|
||||
let changed = o.sent.as_ref() != Some(&wish);
|
||||
let again = looping && o.again.is_some_and(|at| Instant::now() >= at);
|
||||
let waited = o.retry.is_none_or(|(at, _)| Instant::now() >= at);
|
||||
// Nothing sent yet and nothing playing: nothing to say (a failed send forgets what
|
||||
// was sent: the service ends a one-shot by itself, a loop within a minute).
|
||||
let quiet = wish.is_none() && o.sent.is_none();
|
||||
let emote_due = !quiet && waited && (changed || again);
|
||||
// One sign-in at a time, the emote's included.
|
||||
if busy || o.sending.is_some() {
|
||||
} else if emote_due {
|
||||
o.sent = Some(wish.clone());
|
||||
o.again = looping.then(|| Instant::now() + LOOP_AGAIN);
|
||||
let (tx, rx) = channel();
|
||||
o.sending = Some(rx);
|
||||
let kept = o.kept.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("Cosmetics emote".into())
|
||||
.spawn(move || {
|
||||
let _ = tx.send(send_emote(&kept, wish.as_deref()));
|
||||
})
|
||||
.ok();
|
||||
} else if c.mine.due(extras.cosmetics_mine) {
|
||||
c.mine.start(extras.cosmetics_mine, send_mine);
|
||||
} else if c.listed.due(extras.horizon_listed) {
|
||||
c.listed.start(extras.horizon_listed, send_listed);
|
||||
}
|
||||
}
|
||||
let uploads = std::mem::take(&mut *c.uploads.borrow_mut());
|
||||
for item in uploads {
|
||||
let location = format!("cosmetics/{:x}", Arc::as_ptr(&item) as usize);
|
||||
@@ -572,88 +751,6 @@ impl Game<'_> {
|
||||
.retain(|_, b| Arc::strong_count(&b.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.
|
||||
pub(super) fn tick_others_emotes(&mut self) {
|
||||
let shown = self.settings.extras.effective_others() == Others::Shown;
|
||||
// Items from a folder (the art, the bench): every mannequin plays its first emote, again
|
||||
// and again.
|
||||
if self.cosmetics.local && shown {
|
||||
let Some(first) = self.owned_emotes().first().cloned() else {
|
||||
return;
|
||||
};
|
||||
for m in self.mobs.iter_mut() {
|
||||
let age = m.age as f32;
|
||||
if &*m.kind.id == "minecraft:mannequin"
|
||||
&& emotes::playing(m).is_none_or(|p| p.at(age).is_none())
|
||||
{
|
||||
let playing = horizon_cosmetics::Playing {
|
||||
item: first.clone(),
|
||||
// Out of step, so a crowd doesn't move as one.
|
||||
start: age - (m.id % 40) as f32,
|
||||
stop: None,
|
||||
};
|
||||
emotes::set_playing(m, Some(playing));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let (Some(feed), Some(client)) = (&self.cosmetics.feed, &self.cosmetics.client) else {
|
||||
return;
|
||||
};
|
||||
if !shown {
|
||||
feed.ask([]);
|
||||
for m in self.mobs.iter_mut() {
|
||||
emotes::set_playing(m, None);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut frame = client.frame();
|
||||
let mut horizon = Vec::new();
|
||||
for m in self.mobs.iter() {
|
||||
if &*m.kind.id == "minecraft:player"
|
||||
&& let Some(uuid) = self.remote_player_uuid(m.id)
|
||||
&& frame.horizon(uuid)
|
||||
{
|
||||
horizon.push((m.id, uuid));
|
||||
}
|
||||
}
|
||||
drop(frame);
|
||||
feed.ask(horizon.iter().map(|(_, u)| *u));
|
||||
let now = Instant::now();
|
||||
for (id, uuid) in horizon {
|
||||
let seen = feed.playing(uuid);
|
||||
let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) else {
|
||||
continue;
|
||||
};
|
||||
let age = m.age as f32;
|
||||
let moved = {
|
||||
let (dx, dz) = (m.position.x - m.previous.x, m.position.z - m.previous.z);
|
||||
dx * dx + dz * dz > 1e-4
|
||||
};
|
||||
let mut next = emotes::playing(m).cloned();
|
||||
match (seen, &mut next) {
|
||||
(Some(seen), Some(p)) if Arc::ptr_eq(&p.item, &seen.item) => {
|
||||
if moved && p.stop.is_none() {
|
||||
p.stop = Some(age);
|
||||
}
|
||||
}
|
||||
(Some(seen), slot) if !moved => {
|
||||
let ticks = now.duration_since(seen.started).as_secs_f32() * 20.0;
|
||||
*slot = Some(horizon_cosmetics::Playing {
|
||||
item: seen.item,
|
||||
start: age - ticks,
|
||||
stop: None,
|
||||
});
|
||||
}
|
||||
(_, Some(p)) if p.stop.is_none() => p.stop = Some(age),
|
||||
_ => {}
|
||||
}
|
||||
emotes::set_playing(m, next.filter(|p| p.at(age).is_some()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the service which emotes the account owns, for the wheel: when it opens, at most
|
||||
/// every 10 minutes (a folder's emotes need no asking; an offline account has none).
|
||||
pub(super) fn refresh_owned_emotes(&mut self) {
|
||||
@@ -669,7 +766,7 @@ impl Game<'_> {
|
||||
}
|
||||
let (tx, rx) = channel();
|
||||
e.loading = Some(rx);
|
||||
let kept = self.cosmetics.emote_out.kept.clone();
|
||||
let kept = service(|s| s.emote_out.kept.clone());
|
||||
std::thread::Builder::new()
|
||||
.name("Cosmetics emotes owned".into())
|
||||
.spawn(move || {
|
||||
@@ -962,7 +1059,7 @@ impl Game<'_> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use horizon_client::extras::Extras;
|
||||
use std::sync::Mutex;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
|
||||
/// A name tag style with a 1×1 mark.
|
||||
const TAG: &str = r##"{"format": 1, "id": "tag", "slot": "name_tag", "parts": [],
|
||||
@@ -1186,4 +1283,66 @@ mod tests {
|
||||
assert_eq!(tag(top_of(&extras, patience)), VANILLA, "{others:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Player models as the hooks see them, with the pose laid over each.
|
||||
struct View(Vec<(PlayerModel, Option<Arc<dyn BodyPose>>)>);
|
||||
|
||||
impl PlayerModels for View {
|
||||
fn connection(&self) -> u64 {
|
||||
1
|
||||
}
|
||||
fn for_each(&self, f: &mut dyn FnMut(PlayerModel)) {
|
||||
self.0.iter().for_each(|(m, _)| f(*m));
|
||||
}
|
||||
fn body_pose(&self, id: u32) -> Option<&Arc<dyn BodyPose>> {
|
||||
self.0.iter().find(|(m, _)| m.id == id)?.1.as_ref()
|
||||
}
|
||||
fn set_body_pose(&mut self, id: u32, pose: Option<Arc<dyn BodyPose>>) {
|
||||
if let Some((_, p)) = self.0.iter_mut().find(|(m, _)| m.id == id) {
|
||||
*p = pose;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_a_folder_mannequins_play_its_first_emote_out_of_step_and_players_do_not() {
|
||||
let item = Arc::new(
|
||||
Item::parse(
|
||||
br#"{"format": 1, "id": "up", "slot": "emote", "parts": [],
|
||||
"emote": {"length": 20, "blend": 0, "tracks": [
|
||||
{"part": "right_arm", "keys": [{"t": 10, "rotation": [0, 0, 90]}]}]}}"#,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let mut service = Seat {
|
||||
local: true,
|
||||
said: Said {
|
||||
others_shown: true,
|
||||
first: Some(item.clone()),
|
||||
..Said::default()
|
||||
},
|
||||
..Seat::default()
|
||||
};
|
||||
let model = |id, kind, uuid| (PlayerModel::new(id, kind, uuid, 50, false), None);
|
||||
let mut view = View(vec![
|
||||
model(1, Kind::Own, None),
|
||||
model(41, Kind::Mannequin, None),
|
||||
model(2, Kind::Player, Some(7)),
|
||||
]);
|
||||
service.tick(&mut view);
|
||||
let playing = emotes::pose_playing(view.0[1].1.as_ref()).expect("the mannequin plays");
|
||||
assert!(Arc::ptr_eq(&playing.item, &item));
|
||||
// Out of step by its id: 50 - 41 % 40.
|
||||
assert_eq!(playing.start, 49.0);
|
||||
assert!(view.0[0].1.is_none() && view.0[2].1.is_none());
|
||||
// Still playing: the same pose stays.
|
||||
let first = view.0[1].1.clone().unwrap();
|
||||
service.tick(&mut view);
|
||||
assert!(Arc::ptr_eq(&first, view.0[1].1.as_ref().unwrap()));
|
||||
// Others' cosmetics not shown: a new mannequin plays nothing.
|
||||
service.said.others_shown = false;
|
||||
view.0.push(model(3, Kind::Mannequin, None));
|
||||
service.tick(&mut view);
|
||||
assert!(view.0[3].1.is_none());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use horizon_client::mobs::Mob;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
use horizon_cosmetics::account::OwnedEmote;
|
||||
use horizon_cosmetics::{Item, Playing, Slot};
|
||||
use horizon_hooks::players::PlayerModels;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// How far the player may drift (blocks) before the emote stops: a nudge, not a step.
|
||||
@@ -37,10 +38,10 @@ pub(in crate::game) struct Emotes {
|
||||
|
||||
impl Emotes {
|
||||
/// The emote to tell the service about, while it plays: its id and whether it loops.
|
||||
pub(in crate::game) fn sending(&self) -> Option<(String, bool)> {
|
||||
pub(in crate::game) fn sending(&self) -> Option<(&str, bool)> {
|
||||
let p = self.playing.as_ref().filter(|p| p.stop.is_none())?;
|
||||
let looping = p.item.emote.as_ref().is_some_and(|e| e.looping);
|
||||
Some((p.item.id.clone(), looping))
|
||||
Some((&p.item.id, looping))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,23 +60,48 @@ impl BodyPose for EmotePose {
|
||||
}
|
||||
}
|
||||
|
||||
/// The emote `m` plays, if any.
|
||||
pub(in crate::game) fn playing(m: &Mob) -> Option<&Playing> {
|
||||
let any: &dyn std::any::Any = &**m.emote.as_ref()?;
|
||||
/// The emote a model's pose plays, if it is one.
|
||||
pub(in crate::game) fn pose_playing(pose: Option<&Arc<dyn BodyPose>>) -> Option<&Playing> {
|
||||
let any: &dyn std::any::Any = &**pose?;
|
||||
any.downcast_ref::<EmotePose>().map(|e| &e.0)
|
||||
}
|
||||
|
||||
/// Makes `m` play `next`, unless it plays that already (a new one allocates).
|
||||
pub(in crate::game) fn set_playing(m: &mut Mob, next: Option<Playing>) {
|
||||
let same = match (playing(m), &next) {
|
||||
/// 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(
|
||||
now: Option<&Arc<dyn BodyPose>>,
|
||||
next: Option<Playing>,
|
||||
) -> Option<Option<Arc<dyn BodyPose>>> {
|
||||
let same = match (pose_playing(now), &next) {
|
||||
(None, None) => true,
|
||||
(Some(a), Some(b)) => {
|
||||
Arc::ptr_eq(&a.item, &b.item) && a.start == b.start && a.stop == b.stop
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !same {
|
||||
m.emote = next.map(|p| Arc::new(EmotePose(p)) as Arc<dyn BodyPose>);
|
||||
(!same).then(|| next.map(|p| Arc::new(EmotePose(p)) as Arc<dyn BodyPose>))
|
||||
}
|
||||
|
||||
/// Makes `m` play `next`, unless it plays that already.
|
||||
pub(in crate::game) fn set_playing(m: &mut Mob, next: Option<Playing>) {
|
||||
if let Some(pose) = replaced(m.emote.as_ref(), next) {
|
||||
m.emote = pose;
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes model `id` play `next`, unless it plays that already.
|
||||
pub(in crate::game) fn set_model_playing(
|
||||
models: &mut dyn PlayerModels,
|
||||
id: u32,
|
||||
next: Option<Playing>,
|
||||
) {
|
||||
if let Some(pose) = replaced(models.body_pose(id), next) {
|
||||
models.set_body_pose(id, pose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +255,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Once a tick: my emote, then the other players'.
|
||||
/// Once a tick: my emote (the others' are `cosmetics::Seat::tick`'s, a hook's).
|
||||
pub(in crate::game) fn tick_emotes(&mut self) {
|
||||
if let Some(done) = self
|
||||
.cosmetics
|
||||
@@ -250,7 +276,6 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.tick_emote();
|
||||
self.tick_others_emotes();
|
||||
}
|
||||
|
||||
/// How far the camera stands back while an emote pulls it (1: the usual distance): it eases
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! 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.
|
||||
|
||||
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 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)
|
||||
{
|
||||
log::error!("Cosmetics: their hooks came too late: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The models of the world a window shows: its own first, then the other players and mannequins.
|
||||
struct Models<'a> {
|
||||
own: Option<&'a mut Mob>,
|
||||
mobs: &'a mut JoinList<Mob>,
|
||||
remote: &'a super::remote::Remote,
|
||||
}
|
||||
|
||||
impl Models<'_> {
|
||||
fn find(&mut self, id: u32) -> Option<&mut Mob> {
|
||||
match self.own.as_deref_mut() {
|
||||
Some(m) if m.id == id => Some(m),
|
||||
_ => self
|
||||
.mobs
|
||||
.iter_mut()
|
||||
.find(|m| m.id == id && kind(m).is_some()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What kind of player model `m` is, if it is one (the window's own model aside).
|
||||
fn kind(m: &Mob) -> Option<Kind> {
|
||||
match &*m.kind.id {
|
||||
"minecraft:player" => Some(Kind::Player),
|
||||
"minecraft:mannequin" => Some(Kind::Mannequin),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
impl PlayerModels for Models<'_> {
|
||||
fn connection(&self) -> u64 {
|
||||
self.remote.session.serial()
|
||||
}
|
||||
|
||||
fn for_each(&self, f: &mut dyn FnMut(PlayerModel)) {
|
||||
if let Some(m) = self.own.as_deref() {
|
||||
f(model(m, Kind::Own, None));
|
||||
}
|
||||
for m in self.mobs.iter() {
|
||||
if let Some(kind) = kind(m) {
|
||||
let uuid = (kind == Kind::Player)
|
||||
.then(|| self.remote.player_uuid(m.id))
|
||||
.flatten();
|
||||
f(model(m, kind, uuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_pose(&self, id: u32) -> Option<&Arc<dyn BodyPose>> {
|
||||
match self.own.as_deref() {
|
||||
Some(m) if m.id == id => m.emote.as_ref(),
|
||||
_ => self
|
||||
.mobs
|
||||
.iter()
|
||||
.find(|m| m.id == id && kind(m).is_some())?
|
||||
.emote
|
||||
.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_body_pose(&mut self, id: u32, pose: Option<Arc<dyn BodyPose>>) {
|
||||
if let Some(m) = self.find(id) {
|
||||
m.emote = pose;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Once a frame, before drawing.
|
||||
pub(super) fn client_hooks_frame(&mut self) {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
if !engine::any() {
|
||||
return;
|
||||
}
|
||||
self.cosmetics_say();
|
||||
let connection = self.remote.as_ref().map_or(0, |r| r.session.serial());
|
||||
engine::frame(&Frame::new(connection));
|
||||
self.cosmetics_frame();
|
||||
}
|
||||
|
||||
/// Once a client tick in a world: the player's own emote, then the hooks.
|
||||
pub(super) fn client_hooks_tick(&mut self) {
|
||||
debug_assert!(
|
||||
self.serving.is_none(),
|
||||
"a server thread calls no client hook"
|
||||
);
|
||||
self.tick_emotes();
|
||||
if !engine::any() {
|
||||
return;
|
||||
}
|
||||
self.cosmetics_say();
|
||||
let Some(remote) = self.remote.as_ref() else {
|
||||
return;
|
||||
};
|
||||
engine::tick(&mut Models {
|
||||
own: self.player_model.as_mut(),
|
||||
mobs: &mut self.mobs,
|
||||
remote,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6731,7 +6731,7 @@ impl Game<'_> {
|
||||
if self.studio_replay.is_none() {
|
||||
self.tick_player_model();
|
||||
}
|
||||
self.tick_emotes();
|
||||
self.client_hooks_tick();
|
||||
if !dead {
|
||||
self.remote_tick_using();
|
||||
}
|
||||
@@ -10714,17 +10714,11 @@ impl Game<'_> {
|
||||
/// The profile name of another player the server sent, by entity id.
|
||||
/// The UUID of another player's entity in the level.
|
||||
pub(super) fn remote_player_uuid(&self, id: u32) -> Option<u128> {
|
||||
let r = self.remote.as_ref()?;
|
||||
let uuid = *r.level_players.get(&(id as i32))?;
|
||||
(uuid != r.profile).then_some(uuid)
|
||||
self.remote.as_ref()?.player_uuid(id)
|
||||
}
|
||||
pub(super) fn remote_player_name(&self, id: u32) -> Option<&str> {
|
||||
let r = self.remote.as_ref()?;
|
||||
let uuid = r.level_players.get(&(id as i32))?;
|
||||
if *uuid == r.profile {
|
||||
return None;
|
||||
}
|
||||
r.level_player_name(id as i32, uuid)
|
||||
r.level_player_name(id as i32, &r.player_uuid(id)?)
|
||||
}
|
||||
/// Status for `horizon-ctl status`.
|
||||
/// The other players' entities in the level and their names (their
|
||||
@@ -11505,6 +11499,11 @@ impl Remote {
|
||||
pub(super) fn profile(&self) -> u128 {
|
||||
self.profile
|
||||
}
|
||||
/// Another player's profile UUID, by their entity id (None for the local player).
|
||||
pub(super) fn player_uuid(&self, id: u32) -> Option<u128> {
|
||||
let uuid = *self.level_players.get(&(id as i32))?;
|
||||
(uuid != self.profile).then_some(uuid)
|
||||
}
|
||||
/// The entity id of a player in the level, by profile UUID.
|
||||
pub(super) fn player_entity(&self, uuid: u128) -> Option<u32> {
|
||||
self.level_players
|
||||
|
||||
@@ -823,7 +823,7 @@ impl Game<'_> {
|
||||
for (location, rgba) in ready {
|
||||
self.store_skin(location, rgba);
|
||||
}
|
||||
self.cosmetics_frame();
|
||||
self.client_hooks_frame();
|
||||
}
|
||||
/// `Hud.extractTabList`: the player list while the player-list key is
|
||||
/// held. A local world shows it with a joined LAN guest or a `list`
|
||||
|
||||
@@ -261,6 +261,8 @@ impl<'a> Split<'a> {
|
||||
.get(i.wrapping_sub(1))
|
||||
.map(|g| g.options.clone()),
|
||||
);
|
||||
let seat = self.guests.get(i.wrapping_sub(1)).map_or(1, |g| g.seat);
|
||||
horizon_hooks::engine::set_seat(seat - 1);
|
||||
self.turn = i;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ impl Split<'_> {
|
||||
// Until it has a profile, a scratch options file: never P1's.
|
||||
let options = Self::folder()?.join(".joining/options.txt");
|
||||
OPTIONS.set(Some(options.clone()));
|
||||
horizon_hooks::engine::set_seat(seat - 1);
|
||||
let mut settings = self.host.settings.clone();
|
||||
if pad.is_some() {
|
||||
// The Controller setting is P1's own: a player who pressed a pad to join plays
|
||||
@@ -135,6 +136,7 @@ impl Split<'_> {
|
||||
}
|
||||
let game = self.start_guest(seat, &options, settings);
|
||||
OPTIONS.set(None);
|
||||
horizon_hooks::engine::set_seat(0);
|
||||
let mut game = game?;
|
||||
if let Some(pad) = pad {
|
||||
// The press that joined it doesn't act in its game.
|
||||
@@ -299,16 +301,31 @@ impl Split<'_> {
|
||||
}
|
||||
let profile = profiles.join(&name);
|
||||
let options = profile.join("options.txt");
|
||||
let mut settings = if options.exists() {
|
||||
horizon_client::settings::Settings::load(&options)
|
||||
let made = if options.exists() {
|
||||
None
|
||||
} else {
|
||||
std::fs::create_dir_all(&profile)?;
|
||||
let mut new = self.host.settings.clone();
|
||||
// The Controller setting is P1's, as on the profile page (`join`).
|
||||
new.controller.enabled |= self.guests[i].pad.is_some();
|
||||
// On P1's seat, so the crates' settings stores write P1's values too.
|
||||
let turn = horizon_hooks::seat();
|
||||
horizon_hooks::engine::set_seat(0);
|
||||
new.save(&options);
|
||||
new
|
||||
horizon_hooks::engine::set_seat(turn);
|
||||
Some(new)
|
||||
};
|
||||
// The crates' stores load into the guest's seat, before its turn starts below.
|
||||
let turn = horizon_hooks::seat();
|
||||
horizon_hooks::engine::set_seat(self.guests[i].seat - 1);
|
||||
let mut settings = match made {
|
||||
Some(new) => {
|
||||
horizon_client::settings::Settings::load_stores(&options);
|
||||
new
|
||||
}
|
||||
None => horizon_client::settings::Settings::load(&options),
|
||||
};
|
||||
horizon_hooks::engine::set_seat(turn);
|
||||
// A profile an earlier join saved with P1's off, or the player's pad would be dead.
|
||||
settings.controller.enabled |= self.guests[i].pad.is_some();
|
||||
// P1's own world through memory (`Ok`), else P1's server over the network (`Err`).
|
||||
|
||||
@@ -771,26 +771,9 @@ fn swim(p: &mut Poses, pos: f32, h: &Hum) {
|
||||
lerp(amount, x, 0.3 * (pos * 0.333_333_34).mcos()),
|
||||
);
|
||||
}
|
||||
/// The player model's parts a [`BodyPose`] poses, in its order.
|
||||
pub const BODY_PARTS: [&str; 6] = [
|
||||
"head",
|
||||
"body",
|
||||
"right_arm",
|
||||
"left_arm",
|
||||
"right_leg",
|
||||
"left_leg",
|
||||
];
|
||||
|
||||
/// A pose a higher layer lays over the player model's body after its vanilla animation (Horizon's
|
||||
/// emotes, docs/cosmetics.md), handed to the model through [`Mob::emote`]. The engine names no
|
||||
/// type of that layer (docs/layers.md); it downcasts its own back through [`std::any::Any`].
|
||||
pub trait BodyPose: std::any::Any + std::fmt::Debug + Send + Sync {
|
||||
/// Poses the [`BODY_PARTS`] at `age` (the mob's ticks): `at_rest` holds each part before the
|
||||
/// animation, `poses` as the animation left it; a part the model lacks holds the identity in
|
||||
/// both and is not written back. False when it poses nothing at `age` (`poses` is then
|
||||
/// ignored).
|
||||
fn pose(&self, age: f32, at_rest: &[[f32; 9]; 6], poses: &mut [[f32; 9]; 6]) -> bool;
|
||||
}
|
||||
/// A pose laid over the player model's body after its vanilla animation, and the parts it poses:
|
||||
/// the hook's own types (`horizon_hooks::players`), kept here under their old path.
|
||||
pub use horizon_hooks::players::{BODY_PARTS, BodyPose};
|
||||
|
||||
/// Apply the mob's model animation to `p` (at rest), then the pose laid over it, if any.
|
||||
pub fn setup(
|
||||
|
||||
@@ -149,8 +149,20 @@ pub struct Session {
|
||||
far: Arc<crate::lod_net::Inbox>,
|
||||
/// The recording running, if any.
|
||||
recorder: RecorderSlot,
|
||||
/// This connection's number, new for each one ([`Session::serial`]).
|
||||
serial: u64,
|
||||
}
|
||||
/// The next [`Session::serial`].
|
||||
static NEXT_SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
fn next_serial() -> u64 {
|
||||
NEXT_SERIAL.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
impl Session {
|
||||
/// A number of its own for each connection, never 0: entity ids are per connection, so what
|
||||
/// is kept by id goes when it changes (`horizon_hooks::players::PlayerModels::connection`).
|
||||
pub fn serial(&self) -> u64 {
|
||||
self.serial
|
||||
}
|
||||
/// The negotiated wire release, once the connection has been established.
|
||||
pub fn server_version(&self) -> horizon_protocol::adapter::ServerVersion {
|
||||
self.writer.lock().unwrap().as_ref().map_or(
|
||||
@@ -256,6 +268,7 @@ impl Session {
|
||||
debug: false,
|
||||
far: Arc::new(crate::lod_net::Inbox::default()),
|
||||
recorder: Arc::new(Mutex::new(None)),
|
||||
serial: next_serial(),
|
||||
}
|
||||
}
|
||||
fn start(
|
||||
@@ -333,6 +346,7 @@ impl Session {
|
||||
debug,
|
||||
far,
|
||||
recorder,
|
||||
serial: next_serial(),
|
||||
}
|
||||
}
|
||||
/// Everything received since the last poll.
|
||||
@@ -1829,6 +1843,7 @@ mod tests {
|
||||
debug: false,
|
||||
far: Arc::default(),
|
||||
recorder: Arc::new(Mutex::new(None)),
|
||||
serial: next_serial(),
|
||||
};
|
||||
let started = std::time::Instant::now();
|
||||
if explicit_disconnect {
|
||||
@@ -1932,6 +1947,7 @@ mod tests {
|
||||
debug: false,
|
||||
far: Arc::default(),
|
||||
recorder: Arc::new(Mutex::new(None)),
|
||||
serial: next_serial(),
|
||||
};
|
||||
let (thread_writer, thread_configuring, thread_recorder) = (
|
||||
session.writer.clone(),
|
||||
@@ -2215,6 +2231,7 @@ mod tests {
|
||||
debug: false,
|
||||
far: Arc::default(),
|
||||
recorder: Arc::new(Mutex::new(None)),
|
||||
serial: next_serial(),
|
||||
};
|
||||
let recorder = TestRecorder::default();
|
||||
session.start_recording(recorder.clone(), |_| {});
|
||||
|
||||
@@ -70,7 +70,8 @@ struct Shared {
|
||||
/// The game's handle. Dropping it stops the thread.
|
||||
pub struct Cosmetics {
|
||||
shared: Arc<Shared>,
|
||||
worker: Option<Box<dyn FnMut() + Send>>,
|
||||
/// Behind a lock so one client can be shared (`Arc<Cosmetics>` is `Send + Sync`).
|
||||
worker: Mutex<Option<Box<dyn FnMut() + Send>>>,
|
||||
}
|
||||
|
||||
impl Cosmetics {
|
||||
@@ -78,7 +79,12 @@ impl Cosmetics {
|
||||
/// `cache` when given.
|
||||
pub fn new(transport: impl Transport, cache: Option<PathBuf>) -> Self {
|
||||
let mut c = Self::manual(transport, cache, Arc::new(Instant::now));
|
||||
let mut work = c.worker.take().expect("worker");
|
||||
let mut work = c
|
||||
.worker
|
||||
.get_mut()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.take()
|
||||
.expect("worker");
|
||||
let shared = c.shared.clone();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("Cosmetics".into())
|
||||
@@ -125,7 +131,7 @@ impl Cosmetics {
|
||||
});
|
||||
Self {
|
||||
shared,
|
||||
worker: Some(worker),
|
||||
worker: Mutex::new(Some(worker)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ fn clock() -> (Clock, Arc<AtomicU64>) {
|
||||
}
|
||||
|
||||
fn run(c: &mut Cosmetics) {
|
||||
(c.worker.as_mut().unwrap())();
|
||||
(c.worker.get_mut().unwrap().as_mut().unwrap())();
|
||||
}
|
||||
|
||||
fn ids(worn: Option<Worn>) -> Vec<String> {
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
pub mod hud;
|
||||
pub mod pages;
|
||||
pub mod players;
|
||||
mod registry;
|
||||
pub mod settings;
|
||||
pub mod tunnel;
|
||||
|
||||
use registry::Registry;
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// What a crate above the engine hears from the game. Every method has an empty default:
|
||||
@@ -42,6 +44,14 @@ 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 client tick while a world is shown: the player models in view. Called as
|
||||
/// [`Hooks::frame`] is.
|
||||
fn tick(&self, _models: &mut dyn players::PlayerModels) {}
|
||||
|
||||
/// 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.
|
||||
@@ -89,10 +99,27 @@ fn all() -> &'static [&'static dyn Hooks] {
|
||||
HOOKS.all()
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SEAT: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The splitscreen player whose turn it is: 0 for P1 (and without splitscreen), 1 for P2...
|
||||
/// Splitscreen players are one process, each `Game` in turn on the game's thread, so a crate's
|
||||
/// statics are shared by them: a crate that keeps per-player state (settings, pages, HUD) indexes
|
||||
/// it by this. A player keeps its seat while it plays. Other threads always see 0.
|
||||
pub fn seat() -> usize {
|
||||
SEAT.get()
|
||||
}
|
||||
|
||||
/// The engine's side: where it calls the hooks. Only the engine calls these.
|
||||
pub mod engine {
|
||||
use super::*;
|
||||
|
||||
/// The player whose turn starts on this thread ([`seat`]).
|
||||
pub fn set_seat(seat: usize) {
|
||||
SEAT.set(seat);
|
||||
}
|
||||
|
||||
/// Seals every list (hooks and the registries); their first use does it anyway. Call where
|
||||
/// registration ends.
|
||||
pub fn seal() {
|
||||
@@ -130,6 +157,25 @@ pub mod engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any hook is registered: without one the engine builds no view to hand them.
|
||||
pub fn any() -> bool {
|
||||
!all().is_empty()
|
||||
}
|
||||
|
||||
/// [`Hooks::frame`].
|
||||
pub fn frame(frame: &players::Frame) {
|
||||
for hooks in all() {
|
||||
hooks.frame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::tick`].
|
||||
pub fn tick(models: &mut dyn players::PlayerModels) {
|
||||
for hooks in all() {
|
||||
hooks.tick(models);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hooks::tunnel`]: the first registered hook's resolver.
|
||||
pub fn tunnel() -> Option<&'static dyn tunnel::Resolver> {
|
||||
all().iter().find_map(|hooks| hooks.tunnel())
|
||||
@@ -139,6 +185,7 @@ pub mod engine {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use players::PlayerModels as _;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
/// Records what it heard. One process-wide list, as the game has: this test is the only
|
||||
@@ -155,6 +202,55 @@ mod tests {
|
||||
fn published(&self, open: bool) {
|
||||
self.0.lock().unwrap().push(format!("published {open}"));
|
||||
}
|
||||
fn frame(&self, frame: &players::Frame) {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("frame {}", frame.connection));
|
||||
}
|
||||
fn tick(&self, models: &mut dyn players::PlayerModels) {
|
||||
let mut ids = Vec::new();
|
||||
models.for_each(&mut |m| ids.push(m.id));
|
||||
for id in ids {
|
||||
models.set_body_pose(id, Some(std::sync::Arc::new(Still)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Still;
|
||||
impl players::BodyPose for Still {
|
||||
fn pose(&self, _: f32, _: &[[f32; 9]; 6], _: &mut [[f32; 9]; 6]) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Two models, poses by id.
|
||||
struct Two(std::collections::BTreeMap<u32, std::sync::Arc<dyn players::BodyPose>>);
|
||||
impl players::PlayerModels for Two {
|
||||
fn connection(&self) -> u64 {
|
||||
1
|
||||
}
|
||||
fn for_each(&self, f: &mut dyn FnMut(players::PlayerModel)) {
|
||||
for id in [3, 7] {
|
||||
f(players::PlayerModel::new(
|
||||
id,
|
||||
players::Kind::Player,
|
||||
None,
|
||||
0,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn body_pose(&self, id: u32) -> Option<&std::sync::Arc<dyn players::BodyPose>> {
|
||||
self.0.get(&id)
|
||||
}
|
||||
fn set_body_pose(&mut self, id: u32, pose: Option<std::sync::Arc<dyn players::BodyPose>>) {
|
||||
match pose {
|
||||
Some(p) => self.0.insert(id, p),
|
||||
None => self.0.remove(&id),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements nothing: the defaults do nothing.
|
||||
@@ -173,9 +269,19 @@ mod tests {
|
||||
engine::published(open);
|
||||
}
|
||||
engine::shutdown();
|
||||
assert!(engine::any());
|
||||
engine::frame(&players::Frame::new(4));
|
||||
let mut models = Two(Default::default());
|
||||
engine::tick(&mut models);
|
||||
assert!(models.body_pose(3).is_some() && models.body_pose(7).is_some());
|
||||
assert_eq!(
|
||||
*RECORDER.0.lock().unwrap(),
|
||||
["started 0.2.31", "published true", "published false"]
|
||||
[
|
||||
"started 0.2.31",
|
||||
"published true",
|
||||
"published false",
|
||||
"frame 4"
|
||||
]
|
||||
);
|
||||
assert_eq!(register(&Silent), Err(TooLate));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! 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.
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The player model's parts a [`BodyPose`] poses, in its order.
|
||||
pub const BODY_PARTS: [&str; 6] = [
|
||||
"head",
|
||||
"body",
|
||||
"right_arm",
|
||||
"left_arm",
|
||||
"right_leg",
|
||||
"left_leg",
|
||||
];
|
||||
|
||||
/// A pose a crate lays over a player model's body after its vanilla animation (Horizon's emotes,
|
||||
/// docs/cosmetics.md), handed to the model with [`PlayerModels::set_body_pose`]. The crate
|
||||
/// downcasts its own back through [`std::any::Any`].
|
||||
pub trait BodyPose: std::any::Any + std::fmt::Debug + Send + Sync {
|
||||
/// Poses the [`BODY_PARTS`] at `age` (the model's ticks): `at_rest` holds each part before the
|
||||
/// animation, `poses` as the animation left it (x, y, z, x/y/z rotation, x/y/z scale); a part
|
||||
/// the model lacks holds the identity in both and is not written back. False when it poses
|
||||
/// nothing at `age` (`poses` is then ignored).
|
||||
fn pose(&self, age: f32, at_rest: &[[f32; 9]; 6], poses: &mut [[f32; 9]; 6]) -> bool;
|
||||
}
|
||||
|
||||
/// What a player model is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Kind {
|
||||
/// The player of this window, seen from outside (third person, the inventory picture).
|
||||
Own,
|
||||
/// Another player.
|
||||
Player,
|
||||
/// A mannequin: a player model with no player.
|
||||
Mannequin,
|
||||
}
|
||||
|
||||
/// One player model, as [`PlayerModels::for_each`] shows it.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
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.
|
||||
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,
|
||||
}
|
||||
|
||||
impl PlayerModel {
|
||||
pub fn new(id: u32, kind: Kind, uuid: Option<u128>, age: u32, moved: bool) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind,
|
||||
uuid,
|
||||
age,
|
||||
moved,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The player models of the world this window shows, once a tick ([`crate::Hooks::tick`]).
|
||||
pub trait PlayerModels {
|
||||
/// Changes on every join: entity ids are per connection, so what a crate keeps by id goes
|
||||
/// when this changes.
|
||||
fn connection(&self) -> u64;
|
||||
/// Each player model in view (other players and mannequins; the window's own model first).
|
||||
fn for_each(&self, f: &mut dyn FnMut(PlayerModel));
|
||||
/// The pose laid over model `id`, if any.
|
||||
fn body_pose(&self, id: u32) -> Option<&Arc<dyn BodyPose>>;
|
||||
/// Lays `pose` over model `id` (None: its vanilla animation alone). Unknown ids are ignored.
|
||||
fn set_body_pose(&mut self, id: u32, pose: Option<Arc<dyn BodyPose>>);
|
||||
}
|
||||
|
||||
/// What [`crate::Hooks::frame`] gets, once a frame before drawing.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Frame {
|
||||
/// As [`PlayerModels::connection`]; 0 with no world.
|
||||
pub connection: u64,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(connection: u64) -> Self {
|
||||
Self { connection }
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,12 @@ pub use horizon_server_files as server_files;
|
||||
/// [`hooks::register`] it before the game starts (docs/layers.md, "How a Horizon crate plugs
|
||||
/// in"). The engine's dispatch side is not part of the API.
|
||||
pub mod hooks {
|
||||
pub use horizon_hooks::{Hooks, Started, TooLate, register, tunnel};
|
||||
/// 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.
|
||||
pub use horizon_hooks::players::{
|
||||
BODY_PARTS, BodyPose, Frame, Kind, PlayerModel, PlayerModels,
|
||||
};
|
||||
pub use horizon_hooks::{Hooks, Started, TooLate, register, seat, tunnel};
|
||||
}
|
||||
|
||||
/// The settings store (`horizon_hooks::settings`): a crate's own `key:value` lines in the game
|
||||
|
||||
Reference in new issue
Block a user