mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-10 02:21:33 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ff33fd853 | ||
|
|
536c0f938b | ||
|
|
398bec3d6e |
No files matched your search
Generated
-1
@@ -1584,7 +1584,6 @@ dependencies = [
|
||||
"horizon-client-input",
|
||||
"horizon-command",
|
||||
"horizon-core",
|
||||
"horizon-cosmetics",
|
||||
"horizon-hdr",
|
||||
"horizon-hooks",
|
||||
"horizon-items",
|
||||
|
||||
@@ -9,7 +9,6 @@ description = "The client's world data, rendering and screens: chunks, meshing,
|
||||
[dependencies]
|
||||
# The settings stores of the crates above the engine (docs/layers.md, "Platform registries").
|
||||
horizon-hooks = { path = "../horizon-hooks" }
|
||||
horizon-cosmetics = { path = "../horizon-cosmetics" }
|
||||
# The GUI atlas's pixels, kept compressed once uploaded (hud.rs `pack_pixels`).
|
||||
lz4_flex = { version = "0.14", default-features = false, features = ["std", "safe-encode", "safe-decode", "checked-decode"] }
|
||||
horizon-assets = { path = "../horizon-assets" }
|
||||
|
||||
@@ -166,24 +166,6 @@ pub struct Extras {
|
||||
/// built-in track and leaving the off ones out (`musicPage`, off by
|
||||
/// default: vanilla plays exactly as before).
|
||||
pub music_page: bool,
|
||||
/// Cosmetics (`horizon_cosmetics`, off unless enabled), the lines `horizon_cosmetics::prefs`
|
||||
/// shares with the launcher's Wardrobe: Show my cosmetics (the account's, sent to the service;
|
||||
/// on by default), how much of other players' to show (no line until chosen:
|
||||
/// `effective_others`), and whether their trails and auras are hidden (off by default).
|
||||
pub cosmetics_mine: bool,
|
||||
pub cosmetics_others: Option<horizon_cosmetics::prefs::Others>,
|
||||
pub cosmetics_hide_fx: bool,
|
||||
/// My cosmetics in single player too (`cosmeticsSolo`, off by default): the window's own server
|
||||
/// names the player by their offline profile, which the service doesn't know, so their own
|
||||
/// cosmetics and name tag style show only with this on.
|
||||
pub cosmetics_solo: bool,
|
||||
/// Whether the signed-in account owns a cosmetic, as the service said (never saved; the game
|
||||
/// sets it): with no choice made, others' cosmetics show only then.
|
||||
pub cosmetics_owner: bool,
|
||||
/// Horizon's mark before the names of other Horizon players (name tags, player list), and
|
||||
/// whether the service may say this player plays Horizon: both on by default.
|
||||
pub horizon_marks: bool,
|
||||
pub horizon_listed: bool,
|
||||
/// 3D skin layers (`threeDSkinLayers`, Options → Skin Customization): each
|
||||
/// painted texel of a player's outer layer drawn as a slab
|
||||
/// (`horizon_client::mobs::skin_layers`).
|
||||
@@ -238,13 +220,6 @@ impl Default for Extras {
|
||||
wheel_cycles_settings: false,
|
||||
music_off: Vec::new(),
|
||||
music_page: false,
|
||||
cosmetics_mine: true,
|
||||
cosmetics_others: None,
|
||||
cosmetics_hide_fx: false,
|
||||
cosmetics_solo: false,
|
||||
cosmetics_owner: false,
|
||||
horizon_marks: true,
|
||||
horizon_listed: true,
|
||||
three_d_skin_layers: false,
|
||||
pvp: Default::default(),
|
||||
}
|
||||
@@ -254,11 +229,6 @@ fn bool_text(v: bool) -> &'static str {
|
||||
if v { "true" } else { "false" }
|
||||
}
|
||||
impl Extras {
|
||||
/// How much of other players' cosmetics shows: the player's choice, else all of it for an
|
||||
/// account that owns a cosmetic and none for any other (a vanilla game).
|
||||
pub fn effective_others(&self) -> horizon_cosmetics::prefs::Others {
|
||||
horizon_cosmetics::prefs::Others::effective(self.cosmetics_others, self.cosmetics_owner)
|
||||
}
|
||||
pub fn load(dir: &Path) -> Self {
|
||||
fs::read_to_string(dir.join(FILE))
|
||||
.map(|t| Self::parse(&t))
|
||||
@@ -353,15 +323,6 @@ impl Extras {
|
||||
.collect()
|
||||
}
|
||||
"musicPage" => e.music_page = flag(e.music_page),
|
||||
"cosmeticsMine" => e.cosmetics_mine = flag(e.cosmetics_mine),
|
||||
"cosmeticsOthers" => {
|
||||
e.cosmetics_others =
|
||||
horizon_cosmetics::prefs::Others::parse(value).or(e.cosmetics_others)
|
||||
}
|
||||
"cosmeticsHideFx" => e.cosmetics_hide_fx = flag(e.cosmetics_hide_fx),
|
||||
"cosmeticsSolo" => e.cosmetics_solo = flag(e.cosmetics_solo),
|
||||
horizon_cosmetics::prefs::MARKS => e.horizon_marks = flag(e.horizon_marks),
|
||||
horizon_cosmetics::prefs::LISTED => e.horizon_listed = flag(e.horizon_listed),
|
||||
SKIN_LAYERS_KEY => e.three_d_skin_layers = flag(e.three_d_skin_layers),
|
||||
key => {
|
||||
e.pvp.parse_line(key, value);
|
||||
@@ -372,7 +333,7 @@ impl Extras {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
let mut text = format!(
|
||||
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsHideFx:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
|
||||
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\n{}:{}\n",
|
||||
bool_text(self.pack_browser),
|
||||
bool_text(self.pregen),
|
||||
bool_text(self.fullbright),
|
||||
@@ -421,16 +382,6 @@ impl Extras {
|
||||
bool_text(self.wheel_cycles_settings),
|
||||
self.music_off.join(","),
|
||||
bool_text(self.music_page),
|
||||
bool_text(self.cosmetics_mine),
|
||||
// Written once the player chose.
|
||||
self.cosmetics_others
|
||||
.map_or(String::new(), |o| format!("cosmeticsOthers:{}\n", o.name())),
|
||||
bool_text(self.cosmetics_hide_fx),
|
||||
bool_text(self.cosmetics_solo),
|
||||
horizon_cosmetics::prefs::MARKS,
|
||||
bool_text(self.horizon_marks),
|
||||
horizon_cosmetics::prefs::LISTED,
|
||||
bool_text(self.horizon_listed),
|
||||
SKIN_LAYERS_KEY,
|
||||
bool_text(self.three_d_skin_layers),
|
||||
);
|
||||
@@ -616,13 +567,6 @@ pub enum Item {
|
||||
/// The Select Resource Packs screen's Add button (Modrinth).
|
||||
PackBrowser,
|
||||
Pregen,
|
||||
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
|
||||
CosmeticsMine,
|
||||
CosmeticsOthers,
|
||||
CosmeticsHideFx,
|
||||
CosmeticsSolo,
|
||||
HorizonMarks,
|
||||
HorizonMe,
|
||||
/// On the PvP & HUD page (`pvp::rows`).
|
||||
Pvp(crate::pvp::Opt),
|
||||
}
|
||||
@@ -630,8 +574,10 @@ pub enum Item {
|
||||
pub fn rows() -> Vec<Result<Item, &'static str>> {
|
||||
use Item::*;
|
||||
// Fullbright, Dynamic lights and Hide block-break particles change the picture: they
|
||||
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here.
|
||||
let mut rows = vec![
|
||||
// are on the graphics screen's Quality page (`graphics_ui::rows`), not here. The Cosmetics
|
||||
// group comes last, from the cosmetics' own section (`pages::Place::Extras`) while they are
|
||||
// offered.
|
||||
vec![
|
||||
Err("horizon.extras.header.third_person"),
|
||||
Ok(OwnNameTag),
|
||||
Err("horizon.extras.header.inventory"),
|
||||
@@ -652,19 +598,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
|
||||
Err("horizon.extras.header.menus"),
|
||||
Ok(WheelCyclesSettings),
|
||||
Ok(PackBrowser),
|
||||
];
|
||||
if horizon_cosmetics::enabled() {
|
||||
rows.extend([
|
||||
Err("horizon.extras.header.cosmetics"),
|
||||
Ok(CosmeticsMine),
|
||||
Ok(CosmeticsSolo),
|
||||
Ok(CosmeticsOthers),
|
||||
Ok(CosmeticsHideFx),
|
||||
Ok(HorizonMarks),
|
||||
Ok(HorizonMe),
|
||||
]);
|
||||
}
|
||||
rows
|
||||
]
|
||||
}
|
||||
impl Item {
|
||||
/// The language key of the caption (`horizon.*`, assets/horizon/lang).
|
||||
@@ -690,12 +624,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_settings",
|
||||
Item::PackBrowser => "horizon.extras.pack_browser",
|
||||
Item::Pregen => "horizon.extras.pregen",
|
||||
Item::CosmeticsMine => "horizon.extras.cosmetics_mine",
|
||||
Item::CosmeticsOthers => "horizon.extras.cosmetics_others",
|
||||
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx",
|
||||
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo",
|
||||
Item::HorizonMarks => "horizon.extras.horizon_marks",
|
||||
Item::HorizonMe => "horizon.extras.horizon_me",
|
||||
Item::Pvp(o) => o.caption_key(),
|
||||
}
|
||||
}
|
||||
@@ -722,12 +650,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_list_settings.tooltip",
|
||||
Item::PackBrowser => "horizon.extras.pack_browser.tooltip",
|
||||
Item::Pregen => "horizon.extras.pregen.tooltip",
|
||||
Item::CosmeticsMine => "horizon.extras.cosmetics_mine.tooltip",
|
||||
Item::CosmeticsOthers => "horizon.extras.cosmetics_others.tooltip",
|
||||
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx.tooltip",
|
||||
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo.tooltip",
|
||||
Item::HorizonMarks => "horizon.extras.horizon_marks.tooltip",
|
||||
Item::HorizonMe => "horizon.extras.horizon_me.tooltip",
|
||||
Item::Pvp(o) => o.description_key(),
|
||||
}
|
||||
}
|
||||
@@ -763,12 +685,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => "Scroll wheel changes settings",
|
||||
Item::PackBrowser => "Resource pack browser",
|
||||
Item::Pregen => "Chunk pregeneration",
|
||||
Item::CosmeticsMine => "My cosmetics",
|
||||
Item::CosmeticsOthers => "Other players' cosmetics",
|
||||
Item::CosmeticsHideFx => "Hide other players' trails and auras",
|
||||
Item::CosmeticsSolo => "In single player",
|
||||
Item::HorizonMarks => "Horizon logos",
|
||||
Item::HorizonMe => "Show me as Horizon",
|
||||
Item::Pvp(o) => o.caption(),
|
||||
}
|
||||
}
|
||||
@@ -783,8 +699,7 @@ impl Item {
|
||||
Pvp(o) => return o.parent().map(Pvp),
|
||||
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
|
||||
| AutoReconnect | NoAi | AgentControl | ReproKeys | Splitscreen | ChatAnimation
|
||||
| WheelCyclesSettings | MusicPage | PackBrowser | CosmeticsMine | CosmeticsOthers
|
||||
| CosmeticsHideFx | CosmeticsSolo | HorizonMarks | HorizonMe | Pregen => {
|
||||
| WheelCyclesSettings | MusicPage | PackBrowser | Pregen => {
|
||||
return None;
|
||||
}
|
||||
})
|
||||
@@ -842,24 +757,6 @@ impl Item {
|
||||
Item::ReproKeys => {
|
||||
"F3+R starts or stops a repro recording and F3+E writes its bundle. Off by default: a recording copies the world."
|
||||
}
|
||||
Item::CosmeticsMine => {
|
||||
"Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe."
|
||||
}
|
||||
Item::CosmeticsOthers => {
|
||||
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
|
||||
}
|
||||
Item::CosmeticsHideFx => {
|
||||
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
|
||||
}
|
||||
Item::CosmeticsSolo => {
|
||||
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
|
||||
}
|
||||
Item::HorizonMarks => {
|
||||
"Show the Horizon logo next to Horizon players: a small logo before the names of other players who use Horizon, above their heads and in the player list. Only Horizon players see it: servers receive nothing."
|
||||
}
|
||||
Item::HorizonMe => {
|
||||
"Show me as a Horizon player: other Horizon players see the logo by your name. On, the service answers anyone who asks about you that you played Horizon in the last 3 hours; off, it never reports you."
|
||||
}
|
||||
Item::Pvp(o) => o.description(),
|
||||
Item::WheelCyclesSettings => {
|
||||
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
|
||||
@@ -886,12 +783,6 @@ impl Item {
|
||||
| Item::Splitscreen
|
||||
| Item::WheelCyclesSettings
|
||||
| Item::PackBrowser => Some(Cost::None),
|
||||
Item::CosmeticsMine
|
||||
| Item::CosmeticsOthers
|
||||
| Item::CosmeticsHideFx
|
||||
| Item::CosmeticsSolo
|
||||
| Item::HorizonMarks
|
||||
| Item::HorizonMe => Some(Cost::Low),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -917,10 +808,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => e.wheel_cycles_settings,
|
||||
Item::PackBrowser => e.pack_browser,
|
||||
Item::Pregen => e.pregen,
|
||||
Item::CosmeticsHideFx => e.cosmetics_hide_fx,
|
||||
Item::CosmeticsSolo => e.cosmetics_solo,
|
||||
Item::HorizonMarks => e.horizon_marks,
|
||||
Item::HorizonMe => e.horizon_listed,
|
||||
Item::Pvp(o) => return o.switch(&e.pvp),
|
||||
_ => return None,
|
||||
})
|
||||
@@ -930,30 +817,14 @@ impl Item {
|
||||
match self {
|
||||
Item::SortKey => crate::keybinds::display_name(&e.sort_key).plain(),
|
||||
Item::Pvp(o) if o.switch(&e.pvp).is_none() => o.value(&e.pvp).unwrap_or_default(),
|
||||
Item::CosmeticsMine => match e.cosmetics_mine {
|
||||
true => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
|
||||
false => crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden"),
|
||||
},
|
||||
Item::CosmeticsOthers => {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
match e.effective_others() {
|
||||
Others::Shown => crate::lang::get_or("horizon.extras.cosmetics.shown", "Shown"),
|
||||
Others::NameTags => {
|
||||
crate::lang::get_or("horizon.extras.cosmetics.name_tags", "Name tags only")
|
||||
}
|
||||
Others::Hidden => {
|
||||
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
other => match other.switch(e) {
|
||||
Some(true) => crate::lang::get_or("On", "On"),
|
||||
_ => crate::lang::get_or("Off", "Off"),
|
||||
},
|
||||
}
|
||||
}
|
||||
/// A click (`back` for the right button: it turns the three-way cosmetics button the other
|
||||
/// way; every other item left is a switch, so both flip it). The sort key binds instead.
|
||||
/// A click (`back` for the right button: a PvP & HUD choice steps back; a switch flips
|
||||
/// either way). The sort key binds instead.
|
||||
pub fn cycle(self, e: &mut Extras, back: bool) {
|
||||
match self {
|
||||
Item::Fullbright => e.fullbright = !e.fullbright,
|
||||
@@ -975,16 +846,6 @@ impl Item {
|
||||
Item::WheelCyclesSettings => e.wheel_cycles_settings = !e.wheel_cycles_settings,
|
||||
Item::PackBrowser => e.pack_browser = !e.pack_browser,
|
||||
Item::Pregen => e.pregen = !e.pregen,
|
||||
Item::CosmeticsMine => e.cosmetics_mine = !e.cosmetics_mine,
|
||||
Item::CosmeticsOthers => {
|
||||
// From what the button shows; the result is the player's choice from now on.
|
||||
let o = e.effective_others();
|
||||
e.cosmetics_others = Some(if back { o.next().next() } else { o.next() });
|
||||
}
|
||||
Item::CosmeticsHideFx => e.cosmetics_hide_fx = !e.cosmetics_hide_fx,
|
||||
Item::CosmeticsSolo => e.cosmetics_solo = !e.cosmetics_solo,
|
||||
Item::HorizonMarks => e.horizon_marks = !e.horizon_marks,
|
||||
Item::HorizonMe => e.horizon_listed = !e.horizon_listed,
|
||||
Item::Pvp(o) => o.cycle(&mut e.pvp, back),
|
||||
Item::SortKey => {}
|
||||
}
|
||||
@@ -1003,73 +864,6 @@ impl Item {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn other_players_cosmetics_cycle_three_ways_and_read_the_old_switch() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut e = Extras {
|
||||
cosmetics_owner: true,
|
||||
..Extras::default()
|
||||
};
|
||||
assert_eq!(
|
||||
(e.effective_others(), e.cosmetics_hide_fx, e.cosmetics_mine),
|
||||
(Others::Shown, false, true)
|
||||
);
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::Hidden));
|
||||
// The right button goes back.
|
||||
Item::CosmeticsOthers.cycle(&mut e, true);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::NameTags));
|
||||
// Ownership is the service's, never written.
|
||||
assert_eq!(
|
||||
Extras::parse(&e.serialize()),
|
||||
Extras {
|
||||
cosmetics_owner: false,
|
||||
..e.clone()
|
||||
}
|
||||
);
|
||||
// The launcher's Wardrobe writes the same lines.
|
||||
assert_eq!(
|
||||
horizon_cosmetics::prefs::Prefs::parse(&e.serialize()).others,
|
||||
Some(Others::NameTags)
|
||||
);
|
||||
// Before this, the line was on or off.
|
||||
assert_eq!(
|
||||
Extras::parse("cosmeticsOthers:false").cosmetics_others,
|
||||
Some(Others::Hidden)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn others_cosmetics_are_off_for_a_player_who_owns_none_until_chosen() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut e = Extras::default();
|
||||
// Nothing chosen, nothing owned: the button shows Hidden and no line is written.
|
||||
assert_eq!(e.effective_others(), Others::Hidden);
|
||||
assert_eq!(
|
||||
Item::CosmeticsOthers.value(&e),
|
||||
crate::lang::get_or("horizon.extras.cosmetics.hidden", "Hidden")
|
||||
);
|
||||
assert!(!e.serialize().contains("cosmeticsOthers"));
|
||||
// The service says the account owns one: shown, still not a choice.
|
||||
e.cosmetics_owner = true;
|
||||
assert_eq!(e.effective_others(), Others::Shown);
|
||||
assert!(!e.serialize().contains("cosmeticsOthers"));
|
||||
// A non-owner pressing the button goes on from Hidden, and that choice is kept.
|
||||
e.cosmetics_owner = false;
|
||||
Item::CosmeticsOthers.cycle(&mut e, false);
|
||||
assert_eq!(e.cosmetics_others, Some(Others::Shown));
|
||||
let saved = Extras::parse(&e.serialize());
|
||||
assert_eq!(saved.effective_others(), Others::Shown);
|
||||
// An owner who chose Hidden keeps it.
|
||||
let hidden = Extras {
|
||||
cosmetics_owner: true,
|
||||
..Extras::parse("cosmeticsOthers:hidden\n")
|
||||
};
|
||||
assert_eq!(hidden.effective_others(), Others::Hidden);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_wheel_setting_from_before_the_default_changed_is_dropped() {
|
||||
assert!(!Extras::parse("wheelCyclesSettings:true\n").wheel_cycles_settings);
|
||||
|
||||
@@ -206,14 +206,13 @@ impl Screen for ExtrasScreen {
|
||||
fn note_lines(&self) -> usize {
|
||||
NOTE_LINES
|
||||
}
|
||||
/// The rows read the extras, the mods, whether cosmetics are offered and the sections' rows.
|
||||
/// The rows read the extras, the mods and the sections' rows.
|
||||
fn stamp(&self, s: &Settings) -> u64 {
|
||||
let mut added = Vec::new();
|
||||
for (_, section) in sections() {
|
||||
section.rows(&mut added);
|
||||
}
|
||||
let cosmetics = horizon_cosmetics::enabled();
|
||||
shell::stamp_of(&(&s.extras, &s.mods, cosmetics, added))
|
||||
shell::stamp_of(&(&s.extras, &s.mods, added))
|
||||
}
|
||||
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
|
||||
let mut groups = vec![Group {
|
||||
@@ -321,22 +320,6 @@ mod tests {
|
||||
assert_ne!(s.extras.mouse_tweaks_wheel, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_three_way_cosmetics_row_turns_both_ways() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut s = Settings::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::CosmeticsOthers);
|
||||
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
|
||||
assert_eq!(s.extras.effective_others(), Others::Hidden);
|
||||
screen.press(&mut s, row, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
|
||||
}
|
||||
|
||||
/// The sort key binds instead of cycling; the wheel leaves it be.
|
||||
#[test]
|
||||
fn the_key_row_waits_for_a_key() {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
pub(super) mod emote_wheel;
|
||||
pub(super) mod emotes;
|
||||
mod fx;
|
||||
mod settings;
|
||||
|
||||
use super::Game;
|
||||
use horizon_client::fast_hash::FxHashMap;
|
||||
@@ -244,16 +245,13 @@ impl Cosmetics {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the player whose UUID `uuid` gives wears, if the game shows it: the player's own model
|
||||
/// (`own`) while Show my cosmetics is on, another player unless others' cosmetics are hidden (by
|
||||
/// choice, or by owning none). Hidden, the service is never asked about them.
|
||||
/// What the player whose UUID `uuid` gives wears, if the game shows it (`shown`:
|
||||
/// [`Seat::shows`]). Hidden, the service is never asked about them.
|
||||
fn worn_of(
|
||||
client: &horizon_cosmetics::Cosmetics,
|
||||
said: &Said,
|
||||
own: bool,
|
||||
shown: bool,
|
||||
uuid: impl FnOnce() -> Option<u128>,
|
||||
) -> Option<horizon_cosmetics::Worn> {
|
||||
let shown = if own { said.mine } else { !said.others_hidden };
|
||||
if !shown {
|
||||
return None;
|
||||
}
|
||||
@@ -363,6 +361,8 @@ struct Seat {
|
||||
wanted: Vec<u64>,
|
||||
/// Companions, trails and auras (`cosmetics/fx.rs`).
|
||||
fx: fx::Fx,
|
||||
/// The player's settings (`cosmetics/settings.rs`).
|
||||
settings: settings::Settings,
|
||||
}
|
||||
|
||||
/// An item the hooks show: its sprite's id (never reused) and a mesh per bone it rides, held
|
||||
@@ -397,22 +397,10 @@ fn bone(b: horizon_cosmetics::Bone) -> Bone {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the game tells the hooks before each call, until the settings move here (spec #14091
|
||||
/// step 5a) and the player's emote with them (step 4).
|
||||
/// What the game tells the hooks before each call: the player's emote, until it moves here
|
||||
/// (spec #14091 step 4).
|
||||
#[derive(Default)]
|
||||
struct Said {
|
||||
mine: bool,
|
||||
listed: bool,
|
||||
/// Other players' cosmetics are shown (not "name tags only", not hidden).
|
||||
others_shown: bool,
|
||||
/// Other players' cosmetics are hidden (not even name tag styles).
|
||||
others_hidden: bool,
|
||||
/// Horizon's mark by Horizon players' names.
|
||||
marks: bool,
|
||||
/// Other players' trails and auras are hidden.
|
||||
hide_fx: 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.
|
||||
@@ -421,19 +409,6 @@ struct Said {
|
||||
first: Option<Arc<Item>>,
|
||||
}
|
||||
|
||||
impl Said {
|
||||
/// What the game's settings say.
|
||||
fn settings(&mut self, extras: &horizon_client::extras::Extras) {
|
||||
self.mine = extras.cosmetics_mine;
|
||||
self.listed = extras.horizon_listed;
|
||||
self.others_shown = extras.effective_others() == Others::Shown;
|
||||
self.others_hidden = extras.effective_others() == Others::Hidden;
|
||||
self.marks = extras.horizon_marks;
|
||||
self.hide_fx = extras.cosmetics_hide_fx;
|
||||
self.solo = extras.cosmetics_solo;
|
||||
}
|
||||
}
|
||||
|
||||
/// One [`Seat`] per splitscreen player.
|
||||
static SERVICES: Mutex<Vec<Seat>> = Mutex::new(Vec::new());
|
||||
|
||||
@@ -448,8 +423,19 @@ fn service<R>(f: impl FnOnce(&mut Seat) -> R) -> R {
|
||||
}
|
||||
|
||||
/// The cosmetics' hooks, registered when [`enabled`].
|
||||
pub(super) struct Hooks;
|
||||
pub(super) static HOOKS: Hooks = Hooks;
|
||||
struct Hooks;
|
||||
static HOOKS: Hooks = Hooks;
|
||||
|
||||
/// Registers the cosmetics' settings lines, and while they are [`enabled`] their hooks and their
|
||||
/// rows on the Extras page. The lines always: a game without cosmetics keeps the Wardrobe's.
|
||||
pub(super) fn register() -> Result<(), horizon_hooks::TooLate> {
|
||||
horizon_hooks::settings::register(&settings::STORE)?;
|
||||
if enabled() {
|
||||
horizon_hooks::register(&HOOKS)?;
|
||||
horizon_hooks::pages::register(&settings::SECTION)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl horizon_hooks::Hooks for Hooks {
|
||||
fn frame(&self, frame: &Frame, sprites: &mut dyn SpriteStore) {
|
||||
@@ -475,7 +461,7 @@ impl horizon_hooks::Hooks for Hooks {
|
||||
|
||||
fn player_mark(&self, uuid: u128) -> Option<Mark> {
|
||||
service(|s| {
|
||||
let client = s.client.as_ref().filter(|_| s.said.marks)?;
|
||||
let client = s.client.as_ref().filter(|_| s.settings.marks)?;
|
||||
client.frame().horizon(uuid).then_some(MARK)
|
||||
})
|
||||
}
|
||||
@@ -498,6 +484,22 @@ impl horizon_hooks::Hooks for Hooks {
|
||||
}
|
||||
|
||||
impl Seat {
|
||||
/// How much of other players' cosmetics shows: the player's choice, else all of it for an
|
||||
/// account that owns a cosmetic and none for any other (a vanilla game).
|
||||
fn others(&self) -> Others {
|
||||
Others::effective(self.settings.others, self.owner.owns)
|
||||
}
|
||||
|
||||
/// Whether the cosmetics of the player's own model (`own`) or of another player show at all:
|
||||
/// mine with Show my cosmetics on, others' unless hidden.
|
||||
fn shows(&self, own: bool) -> bool {
|
||||
if own {
|
||||
self.settings.mine
|
||||
} else {
|
||||
self.others() != Others::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
/// Once a frame: starts the service's client on first use, asks whether the account owns a
|
||||
/// cosmetic, and tells the service what the account shows.
|
||||
fn frame(&mut self) {
|
||||
@@ -570,7 +572,9 @@ impl Seat {
|
||||
// My emote: only while I show myself as a Horizon player (playing one says so).
|
||||
let playing = said.playing.as_ref();
|
||||
let looping = playing.is_some_and(|(_, looping)| *looping);
|
||||
let wish = playing.map(|(id, _)| id.as_str()).filter(|_| said.listed);
|
||||
let wish = playing
|
||||
.map(|(id, _)| id.as_str())
|
||||
.filter(|_| self.settings.listed);
|
||||
let o = &mut self.emote_out;
|
||||
let changed = o.sent.as_ref().map(Option::as_deref) != Some(wish);
|
||||
let again = looping && o.again.is_some_and(|at| Instant::now() >= at);
|
||||
@@ -594,10 +598,10 @@ impl Seat {
|
||||
let _ = tx.send(send_emote(&kept, wish.as_deref()));
|
||||
})
|
||||
.ok();
|
||||
} else if self.mine.due(said.mine) {
|
||||
self.mine.start(said.mine, send_mine);
|
||||
} else if self.listed.due(said.listed) {
|
||||
self.listed.start(said.listed, send_listed);
|
||||
} else if self.mine.due(self.settings.mine) {
|
||||
self.mine.start(self.settings.mine, send_mine);
|
||||
} else if self.listed.due(self.settings.listed) {
|
||||
self.listed.start(self.settings.listed, send_listed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,18 +613,17 @@ impl Seat {
|
||||
m: &PlayerModel,
|
||||
sprites: &dyn Sprites,
|
||||
) -> Option<NameDecoration> {
|
||||
let said = &self.said;
|
||||
let own = m.kind == Kind::Own;
|
||||
let styles = if own { said.mine } else { !said.others_hidden };
|
||||
if !(styles || said.marks) {
|
||||
let (styles, marks) = (self.shows(own), self.settings.marks);
|
||||
if !(styles || marks) {
|
||||
return None;
|
||||
}
|
||||
let uuid = self.uuid_of(m)?;
|
||||
let mut frame = self.client.as_ref()?.frame();
|
||||
let mark = (said.marks && frame.horizon(uuid)).then_some(MARK);
|
||||
let mark = (marks && frame.horizon(uuid)).then_some(MARK);
|
||||
let worn = if styles { frame.worn(uuid) } else { None };
|
||||
drop(frame);
|
||||
let others = if said.others_shown {
|
||||
let others = if self.others() == Others::Shown {
|
||||
Others::Shown
|
||||
} else {
|
||||
Others::NameTags
|
||||
@@ -634,7 +637,7 @@ impl Seat {
|
||||
/// The UUID the service knows `m` by: a player's, or a mannequin's id with local items.
|
||||
fn uuid_of(&self, m: &PlayerModel) -> Option<u128> {
|
||||
match m.kind {
|
||||
Kind::Own => own_uuid(m.uuid, self.solo, self.said.solo, || {
|
||||
Kind::Own => own_uuid(m.uuid, self.solo, self.settings.solo, || {
|
||||
horizon_client::auth::current().map(|s| s.uuid)
|
||||
}),
|
||||
Kind::Mannequin if self.local => Some(u128::from(m.id)),
|
||||
@@ -704,10 +707,9 @@ impl Seat {
|
||||
if m.invisible {
|
||||
return;
|
||||
}
|
||||
let said = &self.said;
|
||||
let own = m.kind == Kind::Own;
|
||||
// "Name tags only": other players' styles beside their names, nothing on their bodies.
|
||||
let bodies = own || said.others_shown;
|
||||
let bodies = own || self.others() == Others::Shown;
|
||||
let ticks = draw.ticks();
|
||||
// An emote's props: its time and weight while it plays.
|
||||
let emote = emotes::pose_playing(draw.body_pose()).and_then(|p| {
|
||||
@@ -715,7 +717,7 @@ impl Seat {
|
||||
(p.item.vertices > 0).then(|| (p.item.clone(), t, w))
|
||||
});
|
||||
let client = self.client.as_ref().filter(|_| bodies);
|
||||
let worn = client.and_then(|c| worn_of(c, &self.said, own, || self.uuid_of(m)));
|
||||
let worn = client.and_then(|c| worn_of(c, self.shows(own), || self.uuid_of(m)));
|
||||
// Items on one bone (a back item, a shoulder pet and a shirt ride the body) share its
|
||||
// matrix: the head, the body and the two arms at most.
|
||||
let mut bones: [Option<(Bone, [[f32; 4]; 4])>; 4] = [None; 4];
|
||||
@@ -771,7 +773,7 @@ impl Seat {
|
||||
let Some(client) = self.client.as_ref().filter(|_| !m.invisible) else {
|
||||
return;
|
||||
};
|
||||
let Some(worn) = worn_of(client, &self.said, true, || self.uuid_of(m)) else {
|
||||
let Some(worn) = worn_of(client, self.shows(true), || self.uuid_of(m)) else {
|
||||
return;
|
||||
};
|
||||
if let Some(item) = worn.iter().find(|i| i.slot == Slot::Companion) {
|
||||
@@ -795,7 +797,7 @@ impl Seat {
|
||||
}
|
||||
|
||||
fn play_others(&self, models: &mut dyn PlayerModels, view: &mut Vec<PlayerModel>) {
|
||||
let shown = self.said.others_shown;
|
||||
let shown = self.others() == Others::Shown;
|
||||
// Items from a folder (the art, the bench): every mannequin plays its first emote, again
|
||||
// and again.
|
||||
if self.local && shown {
|
||||
@@ -860,8 +862,7 @@ impl Seat {
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Hands the hooks what the game's settings and the player's emote say (allocating only when
|
||||
/// the emote changes).
|
||||
/// Hands the hooks what the player's emote says (allocating only when it changes).
|
||||
pub(super) fn cosmetics_say(&mut self) {
|
||||
if !enabled() {
|
||||
return;
|
||||
@@ -873,12 +874,10 @@ impl Game<'_> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let extras = &self.settings.extras;
|
||||
let playing = self.cosmetics.emotes.sending();
|
||||
let pull = self.cosmetics.emotes.pull();
|
||||
service(|s| {
|
||||
let said = &mut s.said;
|
||||
said.settings(extras);
|
||||
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));
|
||||
@@ -889,16 +888,13 @@ impl Game<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Once a frame, after the hooks: what the game reads of them (the settings' owner flag,
|
||||
/// whether items come from a folder).
|
||||
/// Once a frame, after the hooks: what the game reads of them (whether items come from a
|
||||
/// folder).
|
||||
pub(super) fn cosmetics_frame(&mut self) {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let (owns, local) = service(|s| (s.owner.owns, s.local));
|
||||
// Read every frame: the settings button shows it, and it is never saved.
|
||||
self.settings.extras.cosmetics_owner = owns;
|
||||
self.cosmetics.local = local;
|
||||
self.cosmetics.local = service(|s| s.local);
|
||||
}
|
||||
|
||||
/// Asks the service which emotes the account owns, for the wheel: when it opens, at most
|
||||
@@ -929,7 +925,6 @@ impl Game<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use horizon_client::extras::Extras;
|
||||
use horizon_client::mobs::anim::BodyPose;
|
||||
|
||||
/// A name tag style with a 1×1 mark.
|
||||
@@ -990,31 +985,32 @@ mod tests {
|
||||
|
||||
const OTHER: u128 = 0x1234;
|
||||
|
||||
/// What `extras` tell the hooks.
|
||||
fn said_of(extras: &Extras) -> Said {
|
||||
let mut said = Said::default();
|
||||
said.settings(extras);
|
||||
said
|
||||
/// A seat whose player chose `others` (None: no choice) and whose account `owns` a cosmetic.
|
||||
fn seat_of(others: Option<Others>, owns: bool) -> Seat {
|
||||
let mut seat = Seat::default();
|
||||
seat.settings.others = others;
|
||||
seat.owner.owns = owns;
|
||||
seat
|
||||
}
|
||||
|
||||
/// What another player wears as `extras` shows it, drawn frame after frame for up to
|
||||
/// What another player wears as `seat` shows it, drawn frame after frame for up to
|
||||
/// `patience` (the service answers on its own thread), and what was asked of the service.
|
||||
fn other_player(
|
||||
extras: &Extras,
|
||||
seat: &Seat,
|
||||
patience: Duration,
|
||||
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
|
||||
other_player_from(Service::default(), extras, patience)
|
||||
other_player_from(Service::default(), seat, patience)
|
||||
}
|
||||
|
||||
fn other_player_from(
|
||||
service: Service,
|
||||
extras: &Extras,
|
||||
seat: &Seat,
|
||||
patience: Duration,
|
||||
) -> (Option<horizon_cosmetics::Worn>, Vec<String>) {
|
||||
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
|
||||
let deadline = Instant::now() + patience;
|
||||
let worn = loop {
|
||||
let worn = worn_of(&client, &said_of(extras), false, || Some(OTHER));
|
||||
let worn = worn_of(&client, seat.shows(false), || Some(OTHER));
|
||||
if worn.is_some() || Instant::now() >= deadline {
|
||||
break worn;
|
||||
}
|
||||
@@ -1026,18 +1022,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_player_who_owns_none_sees_and_asks_nothing_until_they_choose() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
// No line, owns nothing: a vanilla game, and no request about the other player.
|
||||
let (short, long) = (Duration::from_millis(300), Duration::from_secs(10));
|
||||
let (worn, asked) = other_player(&Extras::default(), short);
|
||||
let (worn, asked) = other_player(&seat_of(None, false), short);
|
||||
assert!(worn.is_none());
|
||||
assert_eq!(asked, Vec::<String>::new());
|
||||
// The service says this account owns a cosmetic: the other player's style shows.
|
||||
let owner = Extras {
|
||||
cosmetics_owner: true,
|
||||
..Extras::default()
|
||||
};
|
||||
let (worn, asked) = other_player(&owner, long);
|
||||
let (worn, asked) = other_player(&seat_of(None, true), long);
|
||||
assert_eq!(
|
||||
worn.map(|w| w.iter().map(|i| i.id.clone()).collect::<Vec<_>>()),
|
||||
Some(vec!["tag".to_owned()])
|
||||
@@ -1047,16 +1038,12 @@ mod tests {
|
||||
format!("POST /v1/worn {{\"players\":[\"{OTHER:032x}\"]}}")
|
||||
);
|
||||
// A choice wins over ownership both ways.
|
||||
let chose = Extras {
|
||||
cosmetics_others: Some(Others::Shown),
|
||||
..Extras::default()
|
||||
};
|
||||
assert!(other_player(&chose, long).0.is_some());
|
||||
let hid = Extras {
|
||||
cosmetics_others: Some(Others::Hidden),
|
||||
..owner
|
||||
};
|
||||
let (worn, asked) = other_player(&hid, short);
|
||||
assert!(
|
||||
other_player(&seat_of(Some(Others::Shown), false), long)
|
||||
.0
|
||||
.is_some()
|
||||
);
|
||||
let (worn, asked) = other_player(&seat_of(Some(Others::Hidden), true), short);
|
||||
assert!(worn.is_none() && asked.is_empty());
|
||||
}
|
||||
|
||||
@@ -1068,14 +1055,12 @@ mod tests {
|
||||
let mine = |solo: bool, shown: bool, patience: Duration| {
|
||||
let service = Service::default();
|
||||
let client = horizon_cosmetics::Cosmetics::new(service.clone(), None);
|
||||
let extras = Extras {
|
||||
cosmetics_solo: shown,
|
||||
..Extras::default()
|
||||
};
|
||||
let uuid = || own_uuid(Some(OFFLINE), solo, extras.cosmetics_solo, || Some(ACCOUNT));
|
||||
let mut seat = Seat::default();
|
||||
seat.settings.solo = shown;
|
||||
let uuid = || own_uuid(Some(OFFLINE), solo, seat.settings.solo, || Some(ACCOUNT));
|
||||
let deadline = Instant::now() + patience;
|
||||
let worn = loop {
|
||||
let worn = worn_of(&client, &said_of(&extras), true, uuid);
|
||||
let worn = worn_of(&client, seat.shows(true), uuid);
|
||||
if worn.is_some() || Instant::now() >= deadline {
|
||||
break worn;
|
||||
}
|
||||
@@ -1108,7 +1093,6 @@ mod tests {
|
||||
#[test]
|
||||
fn a_tall_cosmetic_lifts_the_name_tag_only_while_it_is_drawn() {
|
||||
use super::super::names::{NAME_TAG_UNIT, cosmetic_lift};
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
// Vanilla: EntityAttachment.NAME_TAG of a standing player, 1.8 + 0.5 above the feet;
|
||||
// the tag's lowest pixel 10 text pixels under it.
|
||||
const VANILLA: f64 = 2.3;
|
||||
@@ -1118,25 +1102,17 @@ mod tests {
|
||||
// by the avatar's 0.9375.
|
||||
let star = 0.9375 * (1.501 + 20.0 / 16.0);
|
||||
let both: &'static [(&str, &str)] = &[("tag", TAG), ("crown", CROWN)];
|
||||
let shown = Extras {
|
||||
cosmetics_others: Some(Others::Shown),
|
||||
..Extras::default()
|
||||
};
|
||||
let shown = seat_of(Some(Others::Shown), false);
|
||||
let (short, long) = (Duration::from_millis(300), Duration::from_secs(10));
|
||||
let top_of = |extras: &Extras, patience| {
|
||||
let (worn, _) = other_player_from(Service(Arc::default(), both), extras, patience);
|
||||
body_top(extras.effective_others(), false, worn.as_ref(), |_| false)
|
||||
let top_of = |seat: &Seat, patience| {
|
||||
let (worn, _) = other_player_from(Service(Arc::default(), both), seat, patience);
|
||||
body_top(seat.others(), false, worn.as_ref(), |_| false)
|
||||
};
|
||||
// No cosmetics drawn on the body (a name tag style only): vanilla's height exactly.
|
||||
let (worn, _) = other_player(&shown, long);
|
||||
assert!(worn.is_some());
|
||||
assert_eq!(
|
||||
tag(body_top(
|
||||
shown.effective_others(),
|
||||
false,
|
||||
worn.as_ref(),
|
||||
|_| false
|
||||
)),
|
||||
tag(body_top(shown.others(), false, worn.as_ref(), |_| false)),
|
||||
VANILLA
|
||||
);
|
||||
// A small hat under the tag: vanilla's height too.
|
||||
@@ -1150,22 +1126,19 @@ mod tests {
|
||||
);
|
||||
// A helmet over it: vanilla's height.
|
||||
let (worn, _) = other_player_from(Service(Arc::default(), both), &shown, long);
|
||||
let helmet = body_top(shown.effective_others(), false, worn.as_ref(), |slot| {
|
||||
let helmet = body_top(shown.others(), false, worn.as_ref(), |slot| {
|
||||
slot == Slot::Head
|
||||
});
|
||||
assert_eq!(tag(helmet), VANILLA);
|
||||
// Others' cosmetics on name tags only, or hidden: nothing on the body, vanilla's height.
|
||||
for others in [Others::NameTags, Others::Hidden] {
|
||||
let extras = Extras {
|
||||
cosmetics_others: Some(others),
|
||||
..Extras::default()
|
||||
};
|
||||
let seat = seat_of(Some(others), false);
|
||||
let patience = if others == Others::Hidden {
|
||||
short
|
||||
} else {
|
||||
long
|
||||
};
|
||||
assert_eq!(tag(top_of(&extras, patience)), VANILLA, "{others:?}");
|
||||
assert_eq!(tag(top_of(&seat, patience)), VANILLA, "{others:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1202,11 +1175,10 @@ mod tests {
|
||||
let mut service = Seat {
|
||||
local: true,
|
||||
said: Said {
|
||||
others_shown: true,
|
||||
first: Some(item.clone()),
|
||||
..Said::default()
|
||||
},
|
||||
..Seat::default()
|
||||
..seat_of(Some(Others::Shown), false)
|
||||
};
|
||||
let model = |id, kind, uuid| (PlayerModel::new(id, kind, uuid, 50, false), None);
|
||||
let mut view = View(vec![
|
||||
@@ -1225,7 +1197,7 @@ mod tests {
|
||||
service.tick(&mut view);
|
||||
assert!(Arc::ptr_eq(&first, view.0[1].1.as_ref().unwrap()));
|
||||
// Others' cosmetics not shown: a new mannequin plays nothing.
|
||||
service.said.others_shown = false;
|
||||
service.settings.others = Some(Others::NameTags);
|
||||
view.0.push(model(3, Kind::Mannequin, None));
|
||||
service.tick(&mut view);
|
||||
assert!(view.0[3].1.is_none());
|
||||
@@ -1334,13 +1306,7 @@ mod tests {
|
||||
..Canvas::default()
|
||||
};
|
||||
// Others' cosmetics hidden, no service: the player's own emote still shows.
|
||||
let mut seat = Seat {
|
||||
said: Said {
|
||||
others_hidden: true,
|
||||
..Said::default()
|
||||
},
|
||||
..Seat::default()
|
||||
};
|
||||
let mut seat = seat_of(Some(Others::Hidden), false);
|
||||
// Its texture not in the atlas yet: nothing drawn, the sprite asked for.
|
||||
seat.player_layers(&player, &mut canvas);
|
||||
assert!(canvas.drawn.is_empty());
|
||||
@@ -1393,7 +1359,7 @@ mod tests {
|
||||
assert_eq!(canvas.looks.len(), 1);
|
||||
// Others' trails and auras hidden: theirs go, the player's own stays.
|
||||
canvas.base = Some(glam::Mat4::from_translation(at));
|
||||
seat.said.hide_fx = true;
|
||||
seat.settings.hide_fx = true;
|
||||
seat.fx(&other, &ring, &mut canvas);
|
||||
assert_eq!(canvas.looks.len(), 1);
|
||||
seat.fx(&own, &ring, &mut canvas);
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Seat {
|
||||
/// trails and auras stay hidden with "Hide other players' trails and auras".
|
||||
pub(super) fn fx(&mut self, m: &PlayerModel, item: &Arc<Item>, draw: &mut dyn PlayerDraw) {
|
||||
let own = m.kind == Kind::Own;
|
||||
if !own && item.slot.is_effect() && self.said.hide_fx {
|
||||
if !own && item.slot.is_effect() && self.settings.hide_fx {
|
||||
return;
|
||||
}
|
||||
// Past `FAR` a companion, trail or aura is a few pixels across for the cost of a model:
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//! The cosmetics' settings (spec #14091 step 5a): their lines of `horizon-extras.txt`, the ones
|
||||
//! the launcher's Wardrobe shares (`horizon_cosmetics::prefs`), through the settings store
|
||||
//! ([`STORE`]), and their rows in the Extras page's Cosmetics group ([`SECTION`]). Each
|
||||
//! splitscreen player has their own, as their own settings file: they live in the player's
|
||||
//! [`Seat`].
|
||||
|
||||
use super::{Seat, service};
|
||||
use horizon_cosmetics::prefs::{self, Others};
|
||||
use horizon_hooks::pages::{Act, Label, Page, Place, Row};
|
||||
use horizon_hooks::settings::Store;
|
||||
|
||||
/// My cosmetics in single player too.
|
||||
const SOLO: &str = "cosmeticsSolo";
|
||||
|
||||
/// The settings, each with its line in `horizon-extras.txt`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) struct Settings {
|
||||
/// Show my cosmetics (the account's, told to the service; `prefs::MINE`).
|
||||
pub mine: bool,
|
||||
/// How much of other players' cosmetics shows: no line until the player chooses
|
||||
/// (`Seat::others`).
|
||||
pub others: Option<Others>,
|
||||
/// Other players' trails and auras hidden.
|
||||
pub hide_fx: bool,
|
||||
/// My cosmetics in single player too: the window's own server names the player by their
|
||||
/// offline profile, which the service doesn't know, so they show only with this on.
|
||||
pub solo: bool,
|
||||
/// Horizon's mark before Horizon players' names (name tags, player list).
|
||||
pub marks: bool,
|
||||
/// The service may say this player plays Horizon.
|
||||
pub listed: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mine: true,
|
||||
others: None,
|
||||
hide_fx: false,
|
||||
solo: false,
|
||||
marks: true,
|
||||
listed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KEYS: [&str; 6] = [
|
||||
prefs::MINE,
|
||||
prefs::OTHERS,
|
||||
prefs::HIDE_FX,
|
||||
SOLO,
|
||||
prefs::MARKS,
|
||||
prefs::LISTED,
|
||||
];
|
||||
|
||||
impl Settings {
|
||||
/// One line; a value it can't read keeps the setting.
|
||||
fn load(&mut self, key: &str, value: &str) {
|
||||
let flag = |was: bool| match value {
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => was,
|
||||
};
|
||||
match key {
|
||||
prefs::MINE => self.mine = flag(self.mine),
|
||||
// `Others::parse` reads the old two-way switch too.
|
||||
prefs::OTHERS => self.others = Others::parse(value).or(self.others),
|
||||
prefs::HIDE_FX => self.hide_fx = flag(self.hide_fx),
|
||||
SOLO => self.solo = flag(self.solo),
|
||||
prefs::MARKS => self.marks = flag(self.marks),
|
||||
prefs::LISTED => self.listed = flag(self.listed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Their lines; others' only once chosen.
|
||||
fn save(&self, line: &mut dyn FnMut(&str, &str)) {
|
||||
let text = |on: bool| if on { "true" } else { "false" };
|
||||
line(prefs::MINE, text(self.mine));
|
||||
if let Some(others) = self.others {
|
||||
line(prefs::OTHERS, others.name());
|
||||
}
|
||||
line(prefs::HIDE_FX, text(self.hide_fx));
|
||||
line(SOLO, text(self.solo));
|
||||
line(prefs::MARKS, text(self.marks));
|
||||
line(prefs::LISTED, text(self.listed));
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings' lines of `horizon-extras.txt`, the player's own file in splitscreen: the game
|
||||
/// loads and saves it on the player's turn, so they are that seat's.
|
||||
pub(super) static STORE: Lines = Lines;
|
||||
|
||||
pub(super) struct Lines;
|
||||
|
||||
impl Store for Lines {
|
||||
fn file(&self) -> &'static str {
|
||||
prefs::FILE
|
||||
}
|
||||
fn owns(&self, key: &str) -> bool {
|
||||
KEYS.contains(&key)
|
||||
}
|
||||
fn load(&self, key: &str, value: &str) {
|
||||
service(|s| s.settings.load(key, value));
|
||||
}
|
||||
fn save(&self, line: &mut dyn FnMut(&str, &str)) {
|
||||
service(|s| s.settings).save(line);
|
||||
}
|
||||
fn is_default(&self) -> bool {
|
||||
service(|s| s.settings) == Settings::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The rows, top to bottom.
|
||||
const ROWS: [&str; 6] = [
|
||||
"horizon.extras.cosmetics_mine",
|
||||
"horizon.extras.cosmetics_solo",
|
||||
"horizon.extras.cosmetics_others",
|
||||
"horizon.extras.cosmetics_hide_fx",
|
||||
"horizon.extras.horizon_marks",
|
||||
"horizon.extras.horizon_me",
|
||||
];
|
||||
|
||||
const TOOLTIPS: [&str; 6] = [
|
||||
"horizon.extras.cosmetics_mine.tooltip",
|
||||
"horizon.extras.cosmetics_solo.tooltip",
|
||||
"horizon.extras.cosmetics_others.tooltip",
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip",
|
||||
"horizon.extras.horizon_marks.tooltip",
|
||||
"horizon.extras.horizon_me.tooltip",
|
||||
];
|
||||
|
||||
/// The rows in the Extras page's Cosmetics group, while cosmetics are offered
|
||||
/// ([`super::enabled`]).
|
||||
pub(super) static SECTION: Section = Section;
|
||||
|
||||
pub(super) struct Section;
|
||||
|
||||
impl Section {
|
||||
fn rows_of(seat: &Seat, rows: &mut Vec<Row>) {
|
||||
let s = &seat.settings;
|
||||
let shown = |on: bool| {
|
||||
Label::Key(if on {
|
||||
"horizon.extras.cosmetics.shown"
|
||||
} else {
|
||||
"horizon.extras.cosmetics.hidden"
|
||||
})
|
||||
};
|
||||
let others = Label::Key(match seat.others() {
|
||||
Others::Shown => "horizon.extras.cosmetics.shown",
|
||||
Others::NameTags => "horizon.extras.cosmetics.name_tags",
|
||||
Others::Hidden => "horizon.extras.cosmetics.hidden",
|
||||
});
|
||||
let toggle = |label, on| Row::Toggle { label, on };
|
||||
let cycle = |label, value| Row::Cycle { label, value };
|
||||
rows.extend([
|
||||
cycle(ROWS[0], shown(s.mine)),
|
||||
toggle(ROWS[1], s.solo),
|
||||
cycle(ROWS[2], others),
|
||||
toggle(ROWS[3], s.hide_fx),
|
||||
toggle(ROWS[4], s.marks),
|
||||
toggle(ROWS[5], s.listed),
|
||||
]);
|
||||
}
|
||||
|
||||
/// A press on row `row`; `back` turns the three-way row the other way.
|
||||
fn act_on(seat: &mut Seat, row: usize, back: bool) {
|
||||
let s = &mut seat.settings;
|
||||
match row {
|
||||
0 => s.mine = !s.mine,
|
||||
1 => s.solo = !s.solo,
|
||||
2 => {
|
||||
// From what the row shows; the result is the player's choice from now on.
|
||||
let o = seat.others();
|
||||
seat.settings.others = Some(if back { o.next().next() } else { o.next() });
|
||||
}
|
||||
3 => s.hide_fx = !s.hide_fx,
|
||||
4 => s.marks = !s.marks,
|
||||
5 => s.listed = !s.listed,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Page for Section {
|
||||
fn id(&self) -> &'static str {
|
||||
"horizon-cosmetics.extras"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"horizon.extras.header.cosmetics"
|
||||
}
|
||||
fn place(&self) -> Place {
|
||||
Place::Extras
|
||||
}
|
||||
fn rows(&self, rows: &mut Vec<Row>) {
|
||||
if super::enabled() {
|
||||
service(|seat| Self::rows_of(seat, rows));
|
||||
}
|
||||
}
|
||||
fn act(&self, row: usize, act: Act) {
|
||||
let back = match act {
|
||||
Act::Press => false,
|
||||
Act::PressBack => true,
|
||||
_ => return,
|
||||
};
|
||||
service(|seat| Self::act_on(seat, row, back));
|
||||
}
|
||||
fn tooltip(&self, row: usize) -> Option<Label> {
|
||||
TOOLTIPS.get(row).copied().map(Label::Key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn loaded(text: &str) -> Settings {
|
||||
let mut s = Settings::default();
|
||||
for (key, value) in text.lines().filter_map(|l| l.split_once(':')) {
|
||||
s.load(key.trim(), value.trim());
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn saved(s: &Settings) -> String {
|
||||
let mut text = String::new();
|
||||
s.save(&mut |k, v| text.push_str(&format!("{k}:{v}\n")));
|
||||
text
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lines_round_trip_and_the_wardrobe_reads_them() {
|
||||
let s = Settings {
|
||||
mine: false,
|
||||
others: Some(Others::NameTags),
|
||||
hide_fx: true,
|
||||
solo: true,
|
||||
marks: false,
|
||||
listed: false,
|
||||
};
|
||||
let text = saved(&s);
|
||||
assert_eq!(loaded(&text), s);
|
||||
// The launcher's Wardrobe reads the same lines.
|
||||
let wardrobe = prefs::Prefs::parse(&text);
|
||||
assert_eq!(
|
||||
(wardrobe.mine, wardrobe.others, wardrobe.hide_fx),
|
||||
(false, Some(Others::NameTags), true)
|
||||
);
|
||||
// No choice made: no `cosmeticsOthers` line; the old switch still reads.
|
||||
assert!(!saved(&Settings::default()).contains(prefs::OTHERS));
|
||||
assert_eq!(
|
||||
loaded("cosmeticsOthers:false\n").others,
|
||||
Some(Others::Hidden)
|
||||
);
|
||||
// A value it can't read keeps the setting.
|
||||
assert!(loaded("cosmeticsMine:maybe\n").mine);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn others_cycle_three_ways_from_what_the_row_shows() {
|
||||
let mut seat = Seat::default();
|
||||
// Not chosen, nothing owned: Hidden, and forward wraps to Shown.
|
||||
assert_eq!(seat.others(), Others::Hidden);
|
||||
Section::act_on(&mut seat, 2, false);
|
||||
assert_eq!(seat.settings.others, Some(Others::Shown));
|
||||
Section::act_on(&mut seat, 2, true);
|
||||
assert_eq!(seat.settings.others, Some(Others::Hidden));
|
||||
Section::act_on(&mut seat, 2, true);
|
||||
assert_eq!(seat.settings.others, Some(Others::NameTags));
|
||||
// Owning one shows them by default, until the player chooses.
|
||||
let mut owner = Seat::default();
|
||||
owner.owner.owns = true;
|
||||
assert_eq!(owner.others(), Others::Shown);
|
||||
let mut rows = Vec::new();
|
||||
Section::rows_of(&owner, &mut rows);
|
||||
assert_eq!(
|
||||
rows[2],
|
||||
Row::Cycle {
|
||||
label: ROWS[2],
|
||||
value: Label::Key("horizon.extras.cosmetics.shown")
|
||||
}
|
||||
);
|
||||
assert_eq!(rows.len(), TOOLTIPS.len());
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,7 @@ use std::sync::Arc;
|
||||
/// Registers the hooks of the Horizon parts still in this crate (cosmetics: spec #14091 step 5
|
||||
/// moves them out). Before the hooks are sealed.
|
||||
pub(crate) fn register() {
|
||||
if super::cosmetics::enabled()
|
||||
&& let Err(e) = horizon_hooks::register(&super::cosmetics::HOOKS)
|
||||
{
|
||||
if let Err(e) = super::cosmetics::register() {
|
||||
log::error!("Cosmetics: their hooks came too late: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"horizon-client-lib -> horizon-shader-api",
|
||||
"horizon-client-lib -> horizon-shaders",
|
||||
"horizon-client-lib -> horizon-telemetry",
|
||||
"horizon-client-render -> horizon-cosmetics",
|
||||
"horizon-client-render -> horizon-hdr",
|
||||
"horizon-client-render -> horizon-optifine",
|
||||
"horizon-client-render -> horizon-polytone",
|
||||
|
||||
Reference in new issue
Block a user