Options > Horizon Settings > Shader Packs: pick a pack of either kind
The page lists the packs in shaderpacks/: Horizon's own, marked (Horizon) and named by their manifest, then packs in the shaders/ format. One runs at a time: clicking a pack picks it, clicking the picked one turns it off, and Open Pack Folder opens the folder. The pick is saved in horizon-graphics.txt under a new key (pickedShaderPack), so the shaderPack value older versions saved stays ignored. A new pick drops the running pack and loads the new one in the next frame; HORIZON_SHADER_PACK still wins, for development. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,7 @@ const PAGES: Ty = Ty::Enum(&[
|
||||
"font",
|
||||
"packs",
|
||||
"mods",
|
||||
"shader-packs",
|
||||
"graphics",
|
||||
"graphics-advanced",
|
||||
"extras",
|
||||
|
||||
@@ -612,6 +612,7 @@ impl Game<'_> {
|
||||
"font" => Page::Font,
|
||||
"packs" => Page::Packs,
|
||||
"mods" => Page::Mods,
|
||||
"shader-packs" => Page::ShaderPacks,
|
||||
"graphics" => Page::Graphics,
|
||||
"graphics-advanced" => Page::GraphicsAdvanced,
|
||||
"extras" => Page::Extras,
|
||||
@@ -625,6 +626,10 @@ impl Game<'_> {
|
||||
}
|
||||
self.open_options(self.title.is_some());
|
||||
self.ui.options.show(page);
|
||||
if page == Page::ShaderPacks {
|
||||
self.settings.shader_packs =
|
||||
horizon_client::shader_pack_list::scan(&Self::shader_packs_dir());
|
||||
}
|
||||
ok()
|
||||
}
|
||||
"chat-log" => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Horizon's own shader packs in the game (docs/shaders.md), for development until the Shader
|
||||
//! Packs page exists: `HORIZON_SHADER_PACK=<name>` naming one in `shaderpacks/` (a folder with
|
||||
//! `horizon-shaders.toml`, or a `.hshader`) runs it once it passes `horizon_shader_api::check`.
|
||||
//! The pack is looked at twice a second: a saved file is checked and the pack rebuilt, and a
|
||||
//! Horizon's own shader packs in the game (docs/shaders.md): the pick in `shaderpacks/` (a
|
||||
//! folder with `horizon-shaders.toml`, or a `.hshader`; `game/shader_packs.rs`) runs once it
|
||||
//! passes `horizon_shader_api::check`. The pack is looked at twice a second: a saved file is checked and the pack rebuilt, and a
|
||||
//! pack that fails is refused with its problems in the chat while the last good one keeps
|
||||
//! drawing.
|
||||
|
||||
@@ -21,11 +20,22 @@ const RED: u32 = 0xFF5555;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct NativeShaders {
|
||||
/// The pack's name in `shaderpacks/`, of either kind (`game/shader_packs.rs`); empty: none.
|
||||
pub picked: String,
|
||||
/// The pack's files when it was last read (`fingerprint`).
|
||||
stamp: Option<u64>,
|
||||
looked: Option<Instant>,
|
||||
}
|
||||
|
||||
impl NativeShaders {
|
||||
pub fn picking(name: &str) -> Self {
|
||||
Self {
|
||||
picked: name.to_owned(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// Builds the pack at `path` when its files changed since it was last read.
|
||||
pub(super) fn tick_native_shader_pack(&mut self, path: &Path) {
|
||||
|
||||
@@ -85,6 +85,10 @@ impl Game<'_> {
|
||||
.unwrap_or_default()
|
||||
.join("options.txt")
|
||||
}
|
||||
/// `shaderpacks/` beside `options.txt`.
|
||||
pub(super) fn shader_packs_dir() -> std::path::PathBuf {
|
||||
Self::options_path().with_file_name(horizon_client::shader_pack_list::DIR)
|
||||
}
|
||||
pub(super) fn gui_size(&self) -> (f32, f32) {
|
||||
let size = self
|
||||
.renderer
|
||||
@@ -237,6 +241,17 @@ impl Game<'_> {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
super::packs::open_folder(&dir);
|
||||
}
|
||||
Event::ListShaderPacks => {
|
||||
self.settings.shader_packs =
|
||||
horizon_client::shader_pack_list::scan(&Self::shader_packs_dir());
|
||||
}
|
||||
// Saved; the next frame's `shader_pack_inputs` loads the pick.
|
||||
Event::ShaderPack => self.settings.save(&Self::options_path()),
|
||||
Event::OpenShaderPackFolder => {
|
||||
let dir = Self::shader_packs_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
super::packs::open_folder(&dir);
|
||||
}
|
||||
Event::ModSettings(i) => self.open_mod_screen(i, true),
|
||||
Event::Credits => self.open_win_screen(false, super::win::Finish::Options),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Shader packs, for development until the Shader Packs page exists: `HORIZON_SHADER_PACK=<name>`
|
||||
//! runs `shaderpacks/<name>` (a folder or a zip beside `options.txt`) over the world; Horizon's
|
||||
//! own packs go to `game/native_shaders.rs`. The
|
||||
//! programs of the current dimension's folder are loaded when the dimension changes (spec 1.2).
|
||||
//! Shader packs: the one picked on Options > Horizon Settings > Shader Packs, or
|
||||
//! `HORIZON_SHADER_PACK=<name>` (development; it wins over the pick), runs `shaderpacks/<name>`
|
||||
//! (a folder or a zip beside `options.txt`) over the world; Horizon's own packs go to
|
||||
//! `game/native_shaders.rs`. The programs of the current dimension's folder are loaded when the
|
||||
//! dimension changes (spec 1.2).
|
||||
|
||||
use horizon_client::camera::CameraFluid;
|
||||
use horizon_client::gfx::pack_uniforms::PackInputs;
|
||||
@@ -34,25 +35,34 @@ impl Game<'_> {
|
||||
/// Loads the pack for the current dimension when it changed, and returns this frame's
|
||||
/// inputs to the pack's uniforms.
|
||||
pub(super) fn shader_pack_inputs(&mut self, camera: glam::DVec3, partial: f32) -> PackInputs {
|
||||
let Some(name) = std::env::var_os("HORIZON_SHADER_PACK") else {
|
||||
static DEV_PACK: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||
let dev = DEV_PACK.get_or_init(|| std::env::var("HORIZON_SHADER_PACK").ok());
|
||||
let picked = dev.as_deref().unwrap_or(&self.settings.horizon.shader_pack);
|
||||
if self.native_shaders.picked != picked {
|
||||
// Another pack, or none: drop what ran.
|
||||
self.native_shaders = super::native_shaders::NativeShaders::picking(picked);
|
||||
self.pack_ids = None;
|
||||
if let Some(renderer) = &mut self.renderer {
|
||||
renderer.drop_shader_packs();
|
||||
}
|
||||
}
|
||||
if picked.is_empty() {
|
||||
return PackInputs::default();
|
||||
};
|
||||
}
|
||||
let name = picked.to_owned();
|
||||
let dimension = match self.dim {
|
||||
Dim::Overworld => 0,
|
||||
Dim::Nether => -1,
|
||||
Dim::End => 1,
|
||||
};
|
||||
let path = std::env::current_dir()
|
||||
.unwrap_or_default()
|
||||
.join("shaderpacks")
|
||||
.join(&name);
|
||||
let path = Self::shader_packs_dir().join(&name);
|
||||
if horizon_shader_api::package::is_pack(&path) {
|
||||
// One of Horizon's own packs (game/native_shaders.rs).
|
||||
self.tick_native_shader_pack(&path);
|
||||
} else if let Some(renderer) = &mut self.renderer
|
||||
&& renderer.shader_pack_dimension() != Some(dimension)
|
||||
{
|
||||
let name = name.to_string_lossy();
|
||||
let name = name.as_str();
|
||||
let started = std::time::Instant::now();
|
||||
match Pack::open(&path) {
|
||||
Ok(pack) => {
|
||||
|
||||
@@ -366,6 +366,16 @@ impl Loading {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
packs::open_folder(&dir);
|
||||
}
|
||||
Event::ListShaderPacks => {
|
||||
self.settings.shader_packs =
|
||||
horizon_client::shader_pack_list::scan(&Game::shader_packs_dir());
|
||||
}
|
||||
Event::ShaderPack => self.save_menu_settings(),
|
||||
Event::OpenShaderPackFolder => {
|
||||
let dir = Game::shader_packs_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
packs::open_folder(&dir);
|
||||
}
|
||||
Event::Credits => {
|
||||
if let Some(gui) = &self.gui {
|
||||
let name = self.options.as_ref().map_or_else(
|
||||
|
||||
@@ -2928,6 +2928,12 @@ impl Renderer {
|
||||
}
|
||||
report
|
||||
}
|
||||
/// Drops the running shader pack, of either kind.
|
||||
pub fn drop_shader_packs(&mut self) {
|
||||
self.pack_post = None;
|
||||
self.native_post = None;
|
||||
self.pack_atlas = None;
|
||||
}
|
||||
/// Runs the post passes of one of Horizon's own shader packs over the world from now on
|
||||
/// (`None`: no pack), with its options' values; replaces a `shaders/` pack. Returns the
|
||||
/// passes' labels; on an error the passes that ran keep running.
|
||||
|
||||
@@ -261,6 +261,10 @@ pub struct Graphics {
|
||||
pub battery: bool,
|
||||
/// The graphics API (Windows only; see `GraphicsApi`).
|
||||
pub api: GraphicsApi,
|
||||
/// The shader pack picked in `shaderpacks/` (`shader_pack_list`); empty: none. Its key is
|
||||
/// not `shaderPack`, which versions before the clean-room rewrite saved and which stays
|
||||
/// ignored.
|
||||
pub shader_pack: String,
|
||||
}
|
||||
impl Default for Graphics {
|
||||
fn default() -> Self {
|
||||
@@ -284,6 +288,7 @@ impl Default for Graphics {
|
||||
fxaa: false,
|
||||
battery: false,
|
||||
api: GraphicsApi::Auto,
|
||||
shader_pack: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +339,7 @@ impl Graphics {
|
||||
"fxaa" => g.fxaa = flag(g.fxaa),
|
||||
"batteryMode" => g.battery = flag(g.battery),
|
||||
"graphicsApi" => g.api = GraphicsApi::from_name(value).unwrap_or_default(),
|
||||
"pickedShaderPack" => g.shader_pack = value.to_owned(),
|
||||
"overlay" => {
|
||||
g.overlay = match value {
|
||||
"fps" => Overlay::Fps,
|
||||
@@ -348,7 +354,7 @@ impl Graphics {
|
||||
}
|
||||
pub fn serialize(&self) -> String {
|
||||
format!(
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\nbatteryMode:{}\ngraphicsApi:{}\n",
|
||||
"preset:{}\nlodDistance:{}\nfarDetail:{}\ncaveCulling:{}\nentityCulling:{}\nchunkUpdates:{}\nuploadBudgetKiB:{}\nsky:{}\nfog:{}\nweather:{}\nanimatedTextures:{}\npresentMode:{}\nframePacing:{}\nworkers:{}\noverlay:{}\nresolutionScale:{}\nfxaa:{}\nbatteryMode:{}\ngraphicsApi:{}\npickedShaderPack:{}\n",
|
||||
self.preset.name(),
|
||||
self.lod_distance,
|
||||
self.far_detail.name(),
|
||||
@@ -375,6 +381,7 @@ impl Graphics {
|
||||
bool_text(self.fxaa),
|
||||
bool_text(self.battery),
|
||||
self.api.name(),
|
||||
self.shader_pack,
|
||||
)
|
||||
}
|
||||
pub fn save(&self, dir: &Path) {
|
||||
@@ -471,6 +478,7 @@ mod tests {
|
||||
fxaa: true,
|
||||
battery: true,
|
||||
api: GraphicsApi::Dx12,
|
||||
shader_pack: "Golden Hour".into(),
|
||||
};
|
||||
assert_eq!(Graphics::parse(&g.serialize()), g);
|
||||
assert_eq!(Graphics::parse(""), Graphics::default());
|
||||
|
||||
@@ -167,6 +167,7 @@ pub mod sculk;
|
||||
pub use horizon_server_files as server;
|
||||
pub mod server_packs;
|
||||
pub mod settings;
|
||||
pub mod shader_pack_list;
|
||||
pub mod shadows;
|
||||
pub mod signs;
|
||||
pub mod skins;
|
||||
|
||||
@@ -863,6 +863,8 @@ pub enum Page {
|
||||
Font,
|
||||
Packs,
|
||||
Mods,
|
||||
/// Horizon's shader packs, either kind (`shader_pack_list`).
|
||||
ShaderPacks,
|
||||
/// Horizon's graphics settings (`graphics_ui`).
|
||||
Graphics,
|
||||
GraphicsAdvanced,
|
||||
@@ -893,6 +895,9 @@ enum Act {
|
||||
ResetPadKey(&'static str),
|
||||
ResetPadKeys,
|
||||
ToggleMod(usize),
|
||||
/// Pick `s.shader_packs[i]`, or turn it off when it is the pick.
|
||||
PickShaderPack(usize),
|
||||
OpenShaderPackFolder,
|
||||
/// The installed mod's own settings page (capability `ui`), shown as a
|
||||
/// screen: `Event::ModSettings` has the game open it.
|
||||
ModSettings(usize),
|
||||
@@ -947,6 +952,11 @@ pub enum Event {
|
||||
Packs(Vec<String>),
|
||||
OpenPackFolder,
|
||||
OpenModFolder,
|
||||
/// The Shader Packs page opened: list `shaderpacks/` into `shader_packs`.
|
||||
ListShaderPacks,
|
||||
/// A shader pack was picked or turned off: save and apply it.
|
||||
ShaderPack,
|
||||
OpenShaderPackFolder,
|
||||
/// Open the mod's own settings page (capability `ui`): the installed
|
||||
/// mod's index in `mods.installed`.
|
||||
ModSettings(usize),
|
||||
@@ -1092,6 +1102,9 @@ fn horizon_page_tooltip(page: Page) -> Option<&'static str> {
|
||||
"Optional additions, all off by default: the game stays vanilla until you turn one on."
|
||||
}
|
||||
Page::Agent => "Let your own AI agent play: horizon-ctl and MCP clients on this computer.",
|
||||
Page::ShaderPacks => {
|
||||
"Shader packs from the shaderpacks folder: Horizon's own and those in the shaders/ format. One runs at a time."
|
||||
}
|
||||
Page::Mods => {
|
||||
"Mods made for Horizon, from the horizon-mods folder. Each runs in a sandbox and stays off until you turn it on."
|
||||
}
|
||||
@@ -1183,6 +1196,7 @@ impl OptionsUi {
|
||||
Page::Font => "options.font.title",
|
||||
Page::Packs => "resourcePack.title",
|
||||
Page::Mods => "Mods",
|
||||
Page::ShaderPacks => "Shader Packs",
|
||||
Page::Graphics | Page::GraphicsAdvanced => "options.videoTitle",
|
||||
Page::Controller => "Controller Settings",
|
||||
Page::PadBinds => "Controller Binds",
|
||||
@@ -1299,23 +1313,30 @@ impl OptionsUi {
|
||||
Some(open("Advanced Graphics...", Page::GraphicsAdvanced)),
|
||||
),
|
||||
Row::Buttons(
|
||||
open("Controller Settings...", Page::Controller),
|
||||
Some(self.extra_button(s, crate::extras::Item::DiscordPresence)),
|
||||
open("Shader Packs...", Page::ShaderPacks),
|
||||
Some(open("Controller Settings...", Page::Controller)),
|
||||
),
|
||||
Row::Buttons(
|
||||
open("Extras...", Page::Extras),
|
||||
Some(open("Mods...", Page::Mods)),
|
||||
),
|
||||
// No AI hides the AI Agent page and everything AI.
|
||||
if s.extras.no_ai {
|
||||
Row::Buttons(self.extra_button(s, crate::extras::Item::NoAi), None)
|
||||
} else {
|
||||
Row::Buttons(
|
||||
open("AI Agent...", Page::Agent),
|
||||
Some(self.extra_button(s, crate::extras::Item::NoAi)),
|
||||
)
|
||||
},
|
||||
Row::Buttons(
|
||||
self.extra_button(s, crate::extras::Item::DiscordPresence),
|
||||
Some(if s.extras.no_ai {
|
||||
self.extra_button(s, crate::extras::Item::NoAi)
|
||||
} else {
|
||||
open("AI Agent...", Page::Agent)
|
||||
}),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.chain(
|
||||
(!s.extras.no_ai).then(|| {
|
||||
Row::Buttons(self.extra_button(s, crate::extras::Item::NoAi), None)
|
||||
}),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
Page::Agent => {
|
||||
let mut rows = vec![Row::Buttons(
|
||||
@@ -1532,6 +1553,26 @@ impl OptionsUi {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Page::ShaderPacks => {
|
||||
if s.shader_packs.is_empty() {
|
||||
return vec![Row::Header(
|
||||
"No shader packs: put them in the shaderpacks folder",
|
||||
)];
|
||||
}
|
||||
s.shader_packs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, pack)| {
|
||||
let kind = match pack.kind {
|
||||
crate::shader_pack_list::Kind::Horizon => " (Horizon)",
|
||||
crate::shader_pack_list::Kind::Classic => "",
|
||||
};
|
||||
let label = Text::literal(format!("{}{kind}: ", pack.title))
|
||||
.append(on_off(s.horizon.shader_pack == pack.file));
|
||||
Row::Buttons(button(0.0, 0.0, 310.0, label, Act::PickShaderPack(i)), None)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => {
|
||||
let _ = s;
|
||||
Vec::new()
|
||||
@@ -2017,13 +2058,18 @@ impl OptionsUi {
|
||||
}
|
||||
content = y + self.scroll - top + 2.0;
|
||||
out[list_start..].iter_mut().for_each(|w| w.list = true);
|
||||
if page == Page::Mods {
|
||||
let folder = match page {
|
||||
Page::Mods => Some(("Open Mod Folder", Act::OpenModFolder)),
|
||||
Page::ShaderPacks => Some(("Open Pack Folder", Act::OpenShaderPackFolder)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((label, act)) = folder {
|
||||
out.push(button(
|
||||
cx - 154.0,
|
||||
h - 27.0,
|
||||
150.0,
|
||||
Text::literal("Open Mod Folder"),
|
||||
Act::OpenModFolder,
|
||||
Text::literal(label),
|
||||
act,
|
||||
));
|
||||
out.push(button(cx + 4.0, h - 27.0, 150.0, tr("gui.done"), Act::Done));
|
||||
} else {
|
||||
@@ -2475,7 +2521,12 @@ impl OptionsUi {
|
||||
} => {
|
||||
events.push(Event::ButtonClick);
|
||||
match act {
|
||||
Act::Push(p) => self.push(p),
|
||||
Act::Push(p) => {
|
||||
if p == Page::ShaderPacks {
|
||||
events.push(Event::ListShaderPacks);
|
||||
}
|
||||
self.push(p)
|
||||
}
|
||||
Act::Done => events.extend(self.back(s)),
|
||||
Act::Cycle(opt) => {
|
||||
opt.cycle(s, right, framebuffer);
|
||||
@@ -2520,6 +2571,17 @@ impl OptionsUi {
|
||||
events.push(Event::ModSettings(i));
|
||||
}
|
||||
Act::OpenModFolder => events.push(Event::OpenModFolder),
|
||||
Act::PickShaderPack(i) => {
|
||||
if let Some(pack) = s.shader_packs.get(i) {
|
||||
s.horizon.shader_pack = if s.horizon.shader_pack == pack.file {
|
||||
String::new()
|
||||
} else {
|
||||
pack.file.clone()
|
||||
};
|
||||
events.push(Event::ShaderPack);
|
||||
}
|
||||
}
|
||||
Act::OpenShaderPackFolder => events.push(Event::OpenShaderPackFolder),
|
||||
Act::Credits => events.push(Event::Credits),
|
||||
Act::CopyAgentDetails => events.push(Event::CopyAgentDetails),
|
||||
Act::None => {}
|
||||
@@ -4064,6 +4126,7 @@ mod tests {
|
||||
[
|
||||
Page::Graphics,
|
||||
Page::GraphicsAdvanced,
|
||||
Page::ShaderPacks,
|
||||
Page::Controller,
|
||||
Page::Extras,
|
||||
Page::Mods,
|
||||
|
||||
@@ -182,6 +182,9 @@ pub struct Settings {
|
||||
pub extras: crate::extras::Extras,
|
||||
/// The Horizon mods turned on (`horizon-mods.txt`), all off by default.
|
||||
pub mods: crate::mod_list::ModList,
|
||||
/// The packs in `shaderpacks/`, listed when the Shader Packs page opens; the pick is
|
||||
/// `horizon.shader_pack`.
|
||||
pub shader_packs: Vec<crate::shader_pack_list::Entry>,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
@@ -255,6 +258,7 @@ impl Default for Settings {
|
||||
controller: Default::default(),
|
||||
extras: Default::default(),
|
||||
mods: Default::default(),
|
||||
shader_packs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,6 +419,7 @@ impl Settings {
|
||||
controller: Default::default(),
|
||||
extras: Default::default(),
|
||||
mods: Default::default(),
|
||||
shader_packs: Vec::new(),
|
||||
auto_jump: f.bool("autoJump"),
|
||||
language: f.string("lang"),
|
||||
force_unicode: f.bool("forceUnicodeFont"),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//! The shader packs in the `shaderpacks` folder beside `options.txt`, as Options > Horizon
|
||||
//! Settings > Shader Packs lists them: Horizon's own (docs/shaders.md) and those in the
|
||||
//! `shaders/` format, side by side.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub const DIR: &str = "shaderpacks";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
/// A folder with `horizon-shaders.toml`, or a `.hshader`.
|
||||
Horizon,
|
||||
/// A folder with a `shaders` folder, or a `.zip`.
|
||||
Classic,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
/// The folder's or file's name in `shaderpacks`: what the pick saves.
|
||||
pub file: String,
|
||||
/// The pack's own name for Horizon's packs, else `file`.
|
||||
pub title: String,
|
||||
pub kind: Kind,
|
||||
}
|
||||
|
||||
/// The packs in `dir`, Horizon's first, each kind by title. A missing folder has none.
|
||||
pub fn scan(dir: &Path) -> Vec<Entry> {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut packs: Vec<Entry> = entries.flatten().filter_map(|e| entry(&e.path())).collect();
|
||||
packs.sort_by(|a, b| {
|
||||
(a.kind == Kind::Classic, a.title.to_lowercase())
|
||||
.cmp(&(b.kind == Kind::Classic, b.title.to_lowercase()))
|
||||
});
|
||||
packs
|
||||
}
|
||||
|
||||
fn entry(path: &Path) -> Option<Entry> {
|
||||
let file = path.file_name()?.to_str()?.to_owned();
|
||||
if file.starts_with('.') {
|
||||
return None;
|
||||
}
|
||||
if horizon_shader_api::package::is_pack(path) {
|
||||
// Its manifest's name; a pack that can't be read is still listed, under its file
|
||||
// name: picking it says why in the chat.
|
||||
let manifest = if path.is_dir() {
|
||||
std::fs::read_to_string(path.join(horizon_shader_api::MANIFEST))
|
||||
.ok()
|
||||
.and_then(|t| horizon_shader_api::Manifest::parse(&t).ok())
|
||||
} else {
|
||||
horizon_shader_api::Pack::read(path)
|
||||
.ok()
|
||||
.map(|p| p.manifest)
|
||||
};
|
||||
let title = manifest.map_or_else(|| file.clone(), |m| m.name);
|
||||
return Some(Entry {
|
||||
file,
|
||||
title,
|
||||
kind: Kind::Horizon,
|
||||
});
|
||||
}
|
||||
let zip = path.is_file()
|
||||
&& path
|
||||
.extension()
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("zip"));
|
||||
(zip || path.join("shaders").is_dir()).then(|| Entry {
|
||||
title: file.clone(),
|
||||
file,
|
||||
kind: Kind::Classic,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lists_both_kinds_horizon_first_and_skips_the_rest() {
|
||||
let dir = std::env::temp_dir().join(format!("horizon-shader-list-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let d = dir.as_path();
|
||||
let manifest = "schema = 1\nid = \"warm\"\nname = \"Warm Light\"\nversion = \"1\"\n\
|
||||
api = \"0.1\"\n[[post]]\nfile = \"p.wgsl\"\n";
|
||||
std::fs::create_dir(d.join("warm")).unwrap();
|
||||
std::fs::write(d.join("warm").join(horizon_shader_api::MANIFEST), manifest).unwrap();
|
||||
std::fs::create_dir_all(d.join("Classic Folder/shaders")).unwrap();
|
||||
std::fs::write(d.join("archive.zip"), b"").unwrap();
|
||||
std::fs::write(d.join("notes.txt"), b"").unwrap();
|
||||
std::fs::create_dir(d.join("empty")).unwrap();
|
||||
std::fs::create_dir_all(d.join(".hidden/shaders")).unwrap();
|
||||
let packs = scan(d);
|
||||
let listed: Vec<(&str, &str, Kind)> = packs
|
||||
.iter()
|
||||
.map(|e| (e.file.as_str(), e.title.as_str(), e.kind))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
listed,
|
||||
[
|
||||
("warm", "Warm Light", Kind::Horizon),
|
||||
("archive.zip", "archive.zip", Kind::Classic),
|
||||
("Classic Folder", "Classic Folder", Kind::Classic),
|
||||
]
|
||||
);
|
||||
assert!(scan(&d.join("missing")).is_empty());
|
||||
std::fs::remove_dir_all(d).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launch
|
||||
| `respawn` | `respawn` | Respawn from the death screen | none |
|
||||
| `pause` | `pause` | Open the pause menu | none |
|
||||
| `resume` | `resume` | Leave the pause menu | none |
|
||||
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, graphics, graphics-advanced, extras, horizon, agent): the page (default: the main options) |
|
||||
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, shader-packs, graphics, graphics-advanced, extras, horizon, agent): the page (default: the main options) |
|
||||
| `book [next\|back\|take\|done]` | `book` | The open book or lectern: its page text, or press one of its buttons | `action` (next, back, take, done): a button to press |
|
||||
| `signedit X Y Z [front\|back]` | `signedit` | Open the sign editor of the sign at X Y Z (then type and close) | `x` (integer, required): block X<br>`y` (integer, required): block Y<br>`z` (integer, required): block Z<br>`side` (front, back): which side |
|
||||
| `mount-menu` | `mount_menu` | Open the inventory of the horse, donkey, mule or llama being ridden | none |
|
||||
|
||||
+7
-6
@@ -13,7 +13,7 @@ Horizon's own format.
|
||||
| Authoring tool | `horizon-shader` (same crate) | `new`, `check`, `pack` |
|
||||
| Templates | `crates/horizon-shader-api/templates/` | post, sky, material |
|
||||
| Example pack | `crates/horizon-shader-api/packs/golden_hour` | a warm sky and two post passes |
|
||||
| In the game: the post stage | `HORIZON_SHADER_PACK=<name>` (for development, until the Shader Packs page) | post passes, hot reload |
|
||||
| In the game: the post stage | Options > Horizon Settings > Shader Packs | post passes, hot reload |
|
||||
| In the game: sky and material; the launcher and the registry | | not yet |
|
||||
|
||||
## Quick start
|
||||
@@ -46,9 +46,11 @@ my_pack 0.1.0 (shader API 0.1): FAILED
|
||||
|
||||
## Trying a pack in the game
|
||||
|
||||
Until the Shader Packs page lists them, put the pack folder (or its `.hshader`) in `shaderpacks/` beside `options.txt`
|
||||
and start the game with `HORIZON_SHADER_PACK=<its folder or file name>`. The game checks the pack first, and refuses
|
||||
one that fails, with its problems in the chat. While it runs, the game looks at the pack twice a second: save a file
|
||||
Put the pack folder (or its `.hshader`) in `shaderpacks/` beside `options.txt` (Open Pack Folder opens it) and pick
|
||||
it on Options > Horizon Settings > Shader Packs, which lists Horizon's packs, marked (Horizon), beside packs in the
|
||||
`shaders/` format; one runs at a time, and clicking it again turns it off. `HORIZON_SHADER_PACK=<its folder or file
|
||||
name>` runs a pack whatever the page says, for development. The game checks the pack first, and refuses one that
|
||||
fails, with its problems in the chat. While it runs, the game looks at the pack twice a second: save a file
|
||||
and the pack is checked and rebuilt in place ("Shader pack ... reloaded"); if the new version fails, the chat says
|
||||
where and the last good version keeps drawing. The post passes run today; the sky and material stages are checked
|
||||
but not drawn yet.
|
||||
@@ -133,5 +135,4 @@ open, each pass is timed on the GPU and the overlay shows the pack's total and i
|
||||
|
||||
## Next
|
||||
|
||||
Options > Shader packs listing these packs, then the material and sky stages, and the registry's `shader` kind with
|
||||
installs from the launcher.
|
||||
The material and sky stages in the game, and the registry's `shader` kind with installs from the launcher.
|
||||
|
||||
Reference in New Issue
Block a user