Merge pull request #1158 from ludovic111/fix/shader-pack-folder

🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
This commit is contained in:
Ludovic Marie
2026-10-22 12:51:30 +02:00
committed by GitHub
8 changed files with 465 additions and 41 deletions
@@ -230,6 +230,11 @@ impl Game<'_> {
let _ = std::fs::create_dir_all(&dir);
super::packs::open_folder(&dir);
}
Event::OpenShaderPackFolder => {
let dir = horizon_client::shader_pack::folder();
let _ = std::fs::create_dir_all(&dir);
super::packs::open_folder(&dir);
}
}
}
Ok(())
@@ -37,11 +37,8 @@ impl ShaderPacks {
}
}
/// `shaderpacks/` beside `options.txt`.
pub(super) fn dir() -> PathBuf {
std::env::current_dir()
.unwrap_or_default()
.join(shader_pack::DIR)
shader_pack::folder()
}
impl Game<'_> {
@@ -77,6 +74,7 @@ impl Game<'_> {
return self.shader_pack_error(&id, &e.to_string());
}
self.shader_packs.pack = pack;
self.ui.options.shader_pack_error = None;
}
/// Give the loaded pack to the renderer (also after it is rebuilt).
pub(super) fn apply_shader_pack(&mut self) -> horizon_client::Result<()> {
@@ -105,6 +103,11 @@ impl Game<'_> {
}
fn shader_pack_error(&mut self, id: &str, error: &str) {
log::warn!("Horizon: shader pack {id}: {error}");
// The Shader Packs page says why on the pack's row: the first line,
// since translator errors go on with a drawn source excerpt.
let first = error.lines().map(str::trim).find(|l| !l.is_empty());
self.ui.options.shader_pack_error =
Some((id.to_owned(), first.unwrap_or(error).to_owned()));
self.system_message(Text::literal(format!("Shader pack {id}: {error}")));
}
}
@@ -373,6 +373,11 @@ impl Loading {
let _ = std::fs::create_dir_all(&dir);
packs::open_folder(&dir);
}
Event::OpenShaderPackFolder => {
let dir = horizon_client::shader_pack::folder();
let _ = std::fs::create_dir_all(&dir);
packs::open_folder(&dir);
}
Event::ButtonClick => {}
}
}
+30 -17
View File
@@ -237,7 +237,7 @@ pub fn rows(level: Level) -> Vec<Row> {
h(
H::ShaderPack,
High,
"Redraws the world with a pack from shaderpacks/. Off is vanilla.",
"Redraws the world with a pack from the shaderpacks folder. Off is vanilla.",
),
Row::Header("Preferences"),
v(Opt::ViewBobbing, None, "Sways the view while walking."),
@@ -422,17 +422,6 @@ impl H {
H::Antialiasing => g.fxaa = !g.fxaa,
H::BatteryMode => g.battery = !g.battery,
H::GraphicsApi => g.api = step(&GraphicsApi::ALL, g.api, back),
H::ShaderPack => {
// Off, then every pack in shaderpacks/ by folder name.
let dir = std::env::current_dir()
.unwrap_or_default()
.join(crate::shader_pack::DIR);
let mut all = vec![String::new()];
all.extend(crate::shader_pack::list(&dir).into_iter().map(|(id, _)| id));
let i = all.iter().position(|x| *x == g.shader_pack).unwrap_or(0);
let n = all.len();
g.shader_pack = all[if back { (i + n - 1) % n } else { (i + 1) % n }].clone();
}
H::Pacing => {
g.pacing = match g.pacing {
Pacing::Efficient => Pacing::Precise,
@@ -559,6 +548,8 @@ pub enum Action {
Changed,
OpenAdvanced,
OpenSimple,
/// The Shader Pack row: the Shader Packs page (where packs are picked).
OpenShaderPacks,
Done,
}
@@ -784,6 +775,9 @@ impl GraphicsUi {
}
return match wd.target {
Target::Button(action) => vec![Action::ButtonClick, action],
Target::Item(Item::Horizon(H::ShaderPack)) => {
vec![Action::ButtonClick, Action::OpenShaderPacks]
}
Target::Item(item) => {
if item.slider(s).is_some() {
self.dragging = Some(item);
@@ -1003,11 +997,30 @@ mod tests {
);
s.horizon.shader_pack = "horizon-sample".into();
assert_eq!(row.label(&s).plain(), "Shader Pack: horizon-sample");
// Picking a pack keeps the preset.
Item::Horizon(H::Preset).cycle(&mut s, false, [854, 480]);
let preset = s.horizon.preset;
row.cycle(&mut s, false, [854, 480]);
assert_eq!(s.horizon.preset, preset);
// The row opens the Shader Packs page, where packs are picked.
let mut ui = GraphicsUi::default();
let find = |ui: &mut GraphicsUi| {
let (widgets, ..) = ui.layout(Level::Advanced, 854.0, 480.0);
widgets
.into_iter()
.find(|w| matches!(w.target, Target::Item(i) if i == row))
.unwrap()
};
ui.scroll = find(&mut ui).y - GraphicsUi::bounds(480.0).0 - 4.0;
let w = find(&mut ui);
assert_eq!(
ui.click(
Level::Advanced,
&mut s,
(w.x + 1.0, w.y + 1.0),
(854.0, 480.0),
[854, 480],
false,
false
),
[Action::ButtonClick, Action::OpenShaderPacks]
);
assert_eq!(s.horizon.shader_pack, "horizon-sample");
}
#[test]
+159 -8
View File
@@ -38,7 +38,14 @@ pub enum HRow {
Mod(usize),
/// Opens the directory where Horizon mods are installed.
OpenModFolder,
/// `shaderpacks/`'s path and a button opening it.
ShaderPackFolder,
/// Shader pack `i` of `OptionsUi::shader_packs` and its switch.
ShaderPack(usize),
}
/// The sidebar section listing the shader packs.
pub const SHADER_PACKS: &str = "Shader Packs";
/// A sidebar section: name, description, rows.
pub struct Section {
pub name: &'static str,
@@ -172,6 +179,12 @@ pub const SECTIONS: &[Section] = &[
.to_vec()
},
},
Section {
name: SHADER_PACKS,
blurb: "Packs that redraw the world. Off is the vanilla look.",
// The packs follow, from the folder (`horizon_rows`).
rows: |_| vec![HRow::ShaderPackFolder],
},
Section {
name: "Interface",
blurb: "Menus and resource packs.",
@@ -309,7 +322,15 @@ fn horizon_batch(framebuffer: [i32; 2]) -> HudBatch {
}
impl OptionsUi {
fn horizon_rows(&self, s: &Settings, section: usize) -> Vec<HRow> {
let rows = (SECTIONS[section].rows)(s);
let mut rows = (SECTIONS[section].rows)(s);
if SECTIONS[section].name == SHADER_PACKS {
if self.shader_packs.packs.is_empty() {
rows.push(HRow::Header(
"No shader packs: put a pack's folder or .zip in this folder",
));
}
rows.extend((0..self.shader_packs.packs.len()).map(HRow::ShaderPack));
}
let query = self.settings_search.value.to_lowercase();
if query.is_empty() || !searchable_section(section) {
return rows;
@@ -511,6 +532,38 @@ impl OptionsUi {
ui.described_row(x, y, &installed.title, "", 0.0, cost, text, cw - 60.0);
ui.switch(ROW + i as u64, control_x, y + ROW_H / 2.0, s.mods.is_on(m));
}
HRow::ShaderPackFolder => {
let label = "Open Folder";
let bw = ui.button_width(label, false).max(96.0);
let dir = self.shader_packs.dir.display().to_string();
ui.described_row(x, y, "Folder", "", 0.0, (0, ""), &dir, cw - bw - 24.0);
ui.button(
ROW + i as u64,
[control_x - bw, y + 12.0, bw, CONTROL],
label,
Button::Secondary,
true,
);
}
HRow::ShaderPack(p) => {
let (id, info) = &self.shader_packs.packs[p];
let error = match info {
Err(e) => Some(e.as_str()),
Ok(_) => self
.shader_pack_error
.as_ref()
.filter(|(failed, _)| failed == id)
.map(|(_, e)| e.as_str()),
};
let (cost, text) = match (error, info) {
(Some(e), _) => ((3, "Failed"), e),
(None, Ok(info)) => ((0, ""), info.description.as_str()),
(None, Err(_)) => unreachable!(),
};
ui.described_row(x, y, id, "", 0.0, cost, text, cw - 60.0);
let on = s.horizon.shader_pack == *id;
ui.switch(ROW + i as u64, control_x, y + ROW_H / 2.0, on);
}
HRow::OpenModFolder => {
ui.row(x, y, "Mod folder", "", 0.0);
let label = "Open Folder";
@@ -756,13 +809,7 @@ impl OptionsUi {
return events;
}
match target {
Some(Target::Nav(i)) => {
if searchable_section(self.section) || searchable_section(i) {
self.settings_search.set_value("");
}
self.section = i;
self.hscroll = 0.0;
}
Some(Target::Nav(i)) => self.select_section(i),
Some(Target::Search) => {
self.settings_search.focused = true;
self.settings_search.move_cursor_to_end(false);
@@ -798,6 +845,10 @@ impl OptionsUi {
}
events.push(Event::Changed);
}
Some(HRow::Graphics(Item::Horizon(graphics_ui::H::ShaderPack), ..)) => {
events.push(Event::ButtonClick);
self.open_section(SHADER_PACKS);
}
Some(HRow::Graphics(item, ..)) => {
item.cycle(s, right, framebuffer);
events.push(Event::Changed);
@@ -818,6 +869,14 @@ impl OptionsUi {
events.push(Event::ButtonClick);
events.push(Event::OpenModFolder);
}
Some(HRow::ShaderPackFolder) if !right => {
events.push(Event::ButtonClick);
events.push(Event::OpenShaderPackFolder);
}
Some(HRow::ShaderPack(p)) => {
self.toggle_shader_pack(s, *p);
events.push(Event::Changed);
}
_ => {}
}
}
@@ -825,6 +884,19 @@ impl OptionsUi {
}
events
}
fn select_section(&mut self, i: usize) {
if searchable_section(self.section) || searchable_section(i) {
self.settings_search.set_value("");
}
self.section = i;
self.hscroll = 0.0;
}
/// Show the sidebar section called `name`.
fn open_section(&mut self, name: &str) {
if let Some(i) = SECTIONS.iter().position(|s| s.name == name) {
self.select_section(i);
}
}
pub(crate) fn scroll_horizon(&mut self, amount: f32) {
self.hscroll -= amount * 40.0;
}
@@ -1057,6 +1129,85 @@ mod tests {
assert_eq!(ui.hscroll, 0.0);
}
/// The Shader Packs section: the folder's path and Open Folder, a switch
/// per pack; the Video section's Shader Pack row leads here (#941, #925).
#[test]
fn shader_packs_section_opens_the_folder_and_picks_a_pack() {
let atlas = atlas();
let framebuffer = [2560, 1440];
let dir =
std::env::temp_dir().join(format!("horizon-shader-section-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut options = section_ui(SHADER_PACKS);
let mut settings = Settings::default();
options.shader_packs.poll(dir.clone());
assert!(matches!(
options.horizon_rows(&settings, options.section)[..],
[HRow::ShaderPackFolder, HRow::Header(_)]
));
std::fs::write(dir.join("Pack.zip"), b"").unwrap();
options.shader_packs = Default::default();
options.shader_packs.poll(dir.clone());
// Click the control with hit id `id`, scrolling until it shows.
let click = |options: &mut OptionsUi, settings: &mut Settings, id: u64| {
for _ in 0..40 {
let mut batch = horizon_batch(framebuffer);
let mut ui = Ui::dry(&atlas, &mut batch, (0.0, 0.0));
options.horizon_layout(settings, &mut ui, 1.0);
if let Some(hit) = ui.hits.iter().find(|hit| hit.id == id) {
let r = hit.rect;
let scale = ui.density / ui.batch.scale;
let at = ((r[0] + r[2]) * 0.5 * scale, (r[1] + r[3]) * 0.5 * scale);
drop(ui);
return options.click_horizon(
settings,
&atlas,
at.0,
at.1,
framebuffer,
false,
false,
);
}
drop(ui);
options.hscroll += 200.0;
}
panic!("no control {id}");
};
assert_eq!(
click(&mut options, &mut settings, ROW),
[Event::ButtonClick, Event::OpenShaderPackFolder]
);
assert_eq!(
click(&mut options, &mut settings, ROW + 1),
[Event::Changed]
);
assert_eq!(settings.horizon.shader_pack, "Pack.zip");
click(&mut options, &mut settings, ROW + 1);
assert_eq!(settings.horizon.shader_pack, "");
let mut options = section_ui("Video");
settings.horizon.advanced = true;
let video = SECTIONS.iter().position(|s| s.name == "Video").unwrap();
let row = options
.horizon_rows(&settings, video)
.iter()
.position(|r| {
matches!(
r,
HRow::Graphics(Item::Horizon(graphics_ui::H::ShaderPack), ..)
)
})
.unwrap();
assert_eq!(
click(&mut options, &mut settings, ROW + row as u64),
[Event::ButtonClick]
);
assert_eq!(SECTIONS[options.section].name, SHADER_PACKS);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn mods_section_opens_folder_from_its_button() {
let atlas = atlas();
+216 -3
View File
@@ -862,6 +862,8 @@ pub enum Page {
Font,
Packs,
Mods,
/// The shader packs in `shaderpacks/`: pick one, open the folder.
ShaderPacks,
/// Horizon's graphics settings (`graphics_ui`).
Graphics,
GraphicsAdvanced,
@@ -888,6 +890,9 @@ enum Act {
HorizonMenus,
ToggleMod(usize),
OpenModFolder,
/// Pick shader pack `i` of `OptionsUi::shader_packs` (again: off).
ToggleShaderPack(usize),
OpenShaderPackFolder,
}
enum WKind {
Button {
@@ -935,6 +940,8 @@ pub enum Event {
Packs(Vec<String>),
OpenPackFolder,
OpenModFolder,
/// Create `shaderpacks/` if needed and open it.
OpenShaderPackFolder,
}
/// `SliderableValueSet.applyValueImmediately`: these apply 600 ms after
/// the last change (`OptionInstanceSliderButton.delayedApplyAt`).
@@ -993,6 +1000,10 @@ pub struct OptionsUi {
/// Scroll the selected language into the middle on the next layout.
center_language: bool,
pub graphics: crate::graphics_ui::GraphicsUi,
/// `shaderpacks/` and its packs, read while the Shader Packs page shows.
pub shader_packs: crate::shader_pack::Folder,
/// The pack that failed to load last and why (set by the game).
pub shader_pack_error: Option<(String, String)>,
}
impl Default for OptionsUi {
fn default() -> Self {
@@ -1025,6 +1036,8 @@ impl Default for OptionsUi {
in_world: false,
center_language: false,
graphics: Default::default(),
shader_packs: Default::default(),
shader_pack_error: None,
}
}
}
@@ -1073,6 +1086,8 @@ enum Row {
Big(Opt),
Pair(Opt, Option<Opt>),
Header(&'static str),
/// A line of grey text (a path).
Note(String),
Buttons(Widget, Option<Widget>),
}
fn pairs(opts: &[Opt]) -> Vec<Row> {
@@ -1131,6 +1146,33 @@ impl OptionsUi {
self.graphics.reset();
}
}
/// Pick shader pack `i` of the folder, or turn it off when it is the
/// one running.
pub(crate) fn toggle_shader_pack(&self, s: &mut Settings, i: usize) {
let Some((id, Ok(_))) = self.shader_packs.packs.get(i) else {
return;
};
let g = &mut s.horizon;
g.shader_pack = if g.shader_pack == *id {
String::new()
} else {
id.clone()
};
}
pub(crate) fn shader_pack_failed(&self, id: &str) -> bool {
self.shader_pack_error
.as_ref()
.is_some_and(|(e, _)| e == id)
}
/// Whether the Shader Packs page (or the Horizon menus' section) shows.
fn shader_packs_shown(&self) -> bool {
self.page() == Page::ShaderPacks
|| crate::ui_theme::horizon()
&& self.page() == Page::Main
&& crate::options_horizon::SECTIONS
.get(self.section)
.is_some_and(|s| s.name == crate::options_horizon::SHADER_PACKS)
}
/// The graphics screen level of `page`, if it is one.
fn graphics_level(page: Page) -> Option<crate::graphics_ui::Level> {
match page {
@@ -1153,6 +1195,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",
@@ -1262,6 +1305,16 @@ impl OptionsUi {
Act::Push(Page::GraphicsAdvanced),
)),
)];
rows.push(Row::Buttons(
button(
0.0,
0.0,
310.0,
Text::literal("Shader Packs..."),
Act::Push(Page::ShaderPacks),
),
None,
));
// The way back from Classic menus. Horizon-only like the row
// above, here rather than on the main Options screen so that
// one keeps vanilla's exact layout; vanilla's interface
@@ -1450,6 +1503,32 @@ impl OptionsUi {
})
.collect()
}
Page::ShaderPacks => {
let folder = &self.shader_packs;
let mut rows = vec![Row::Note(folder.dir.display().to_string())];
if folder.packs.is_empty() {
rows.push(Row::Header(
"No shader packs: put a pack's folder or .zip in this folder",
));
}
for (i, (id, info)) in folder.packs.iter().enumerate() {
let on = s.horizon.shader_pack == *id;
let mut label = Text::literal(format!("{id}: ")).append(on_off(on));
if info.is_err() || self.shader_pack_failed(id) {
label = label.append(" (Failed)");
}
let widget = button(0.0, 0.0, 310.0, label, Act::ToggleShaderPack(i));
rows.push(Row::Buttons(
if info.is_ok() {
widget
} else {
inactive(widget)
},
None,
));
}
rows
}
_ => {
let _ = s;
Vec::new()
@@ -1849,6 +1928,21 @@ impl OptionsUi {
});
y += padding + 18.0;
}
Row::Note(text) => {
out.push(Widget {
x: 0.0,
y: y + 4.0,
w,
h: 9.0,
kind: WKind::Text {
text: Text::literal(text),
color: 0xFFA0A0A0,
centered: true,
},
list: false,
});
y += 16.0;
}
Row::Big(opt) => {
out.push(option_widget(s, opt, cx - 155.0, y + 2.0, 310.0));
y += 25.0;
@@ -1875,13 +1969,20 @@ 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 Shader 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 {
@@ -1911,6 +2012,9 @@ impl OptionsUi {
atlas: &HudAtlas,
cursor: (f32, f32),
) {
if self.shader_packs_shown() {
self.shader_packs.poll(crate::shader_pack::folder());
}
let (w, h) = (batch.width, batch.height);
if let Some(error) = &self.resource_error {
ui::menu_background(batch, atlas, 0.0, 0.0, w, h, self.in_world);
@@ -2210,6 +2314,7 @@ impl OptionsUi {
Action::ButtonClick => events.push(Event::ButtonClick),
Action::Changed => events.push(Event::Changed),
Action::OpenAdvanced => self.push(Page::GraphicsAdvanced),
Action::OpenShaderPacks => self.push(Page::ShaderPacks),
Action::OpenSimple => {
if self.stack.len() >= 2
&& self.stack[self.stack.len() - 2] == Page::Graphics
@@ -2377,6 +2482,11 @@ impl OptionsUi {
events.push(Event::Changed);
}
Act::OpenModFolder => events.push(Event::OpenModFolder),
Act::ToggleShaderPack(i) => {
self.toggle_shader_pack(s, i);
events.push(Event::Changed);
}
Act::OpenShaderPackFolder => events.push(Event::OpenShaderPackFolder),
Act::None => {}
}
}
@@ -2831,6 +2941,109 @@ mod tests {
assert!(ui.resource_error.is_none());
}
/// The Shader Packs page (Video Settings > Shader Packs..., or the
/// Shader Pack row of Advanced graphics): the folder's path, Open Shader
/// Pack Folder, and a switch per pack (#941, #925).
#[test]
fn classic_shader_packs_page_shows_the_folder_and_picks_a_pack() {
let atlas = atlas();
let mut s = Settings::default();
let mut ui = OptionsUi::default();
let size = (480.0, 240.0);
let dir = std::env::temp_dir().join(format!("horizon-shaderpacks-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let find = |ui: &mut OptionsUi, s: &Settings, act: &dyn Fn(&Act) -> bool| {
ui.layout(s, size.0, size.1)
.0
.into_iter()
.find(|w| matches!(&w.kind, WKind::Button { act: a, active: true, .. } if act(a)))
};
ui.show(Page::Video);
let open = find(&mut ui, &s, &|a| *a == Act::Push(Page::ShaderPacks)).unwrap();
ui.click(
&mut s,
&atlas,
open.x + 1.0,
open.y + 1.0,
size,
[480, 240],
false,
false,
);
assert_eq!(ui.page(), Page::ShaderPacks);
ui.shader_packs.poll(dir.clone());
let empty = ui.layout(&s, size.0, size.1).0;
let path = dir.display().to_string();
for line in [path.as_str(), "No shader packs"] {
assert!(
empty.iter().any(
|w| matches!(&w.kind, WKind::Text { text, .. } if text.plain().contains(line))
),
"{line}"
);
}
let folder = find(&mut ui, &s, &|a| *a == Act::OpenShaderPackFolder).unwrap();
assert_eq!(
ui.click(
&mut s,
&atlas,
folder.x + 1.0,
folder.y + 1.0,
size,
[480, 240],
false,
false
),
vec![Event::ButtonClick, Event::OpenShaderPackFolder]
);
std::fs::write(dir.join("Pack.zip"), b"").unwrap();
ui.shader_packs = Default::default();
ui.shader_packs.poll(dir.clone());
let pack = find(&mut ui, &s, &|a| *a == Act::ToggleShaderPack(0)).unwrap();
let events = ui.click(
&mut s,
&atlas,
pack.x + 1.0,
pack.y + 1.0,
size,
[480, 240],
false,
false,
);
assert!(events.contains(&Event::Changed));
assert_eq!(s.horizon.shader_pack, "Pack.zip");
ui.shader_pack_error = Some(("Pack.zip".into(), "broken".into()));
let label = |ui: &mut OptionsUi, s: &Settings| match find(ui, s, &|a| {
*a == Act::ToggleShaderPack(0)
})
.unwrap()
.kind
{
WKind::Button { label, .. } => label.plain(),
_ => unreachable!(),
};
assert!(
label(&mut ui, &s).ends_with("(Failed)"),
"{}",
label(&mut ui, &s)
);
ui.click(
&mut s,
&atlas,
pack.x + 1.0,
pack.y + 1.0,
size,
[480, 240],
false,
false,
);
assert_eq!(s.horizon.shader_pack, "", "a second click turns it off");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn classic_mods_page_toggles_mods_and_keeps_dev_and_invalid_entries_inactive() {
let atlas = atlas();
+31 -1
View File
@@ -171,11 +171,41 @@ pub fn list(dir: &Path) -> Vec<(String, Result<Info, String>)> {
packs
}
/// `shaderpacks/` beside `options.txt` (the game's working folder: the game
/// home, not the launcher's version folder).
pub fn folder() -> PathBuf {
std::env::current_dir().unwrap_or_default().join(DIR)
}
/// `shaderpacks/` and its packs as the Shader Packs page lists them, read
/// again at most once a second while the page is open, so a pack dropped
/// into the folder shows up.
#[derive(Debug, Default)]
pub struct Folder {
pub dir: PathBuf,
pub packs: Vec<(String, Result<Info, String>)>,
read: Option<std::time::Instant>,
}
impl Folder {
pub fn poll(&mut self, dir: PathBuf) {
let fresh = self
.read
.is_some_and(|t| t.elapsed() < std::time::Duration::from_secs(1));
if fresh && self.dir == dir {
return;
}
self.packs = list(&dir);
self.dir = dir;
self.read = Some(std::time::Instant::now());
}
}
/// A Java pack has no manifest: its name is its file name.
fn java_info(name: String) -> Info {
Info {
name,
description: String::new(),
description: "Iris/OptiFine pack: its full-screen passes run.".into(),
authors: Vec::new(),
version: String::new(),
license: String::new(),
+12 -8
View File
@@ -13,7 +13,7 @@ Both are off by default. With no pack selected the renderer draws exactly what i
| Part | State |
| --- | --- |
| Renderer layout (below) | today's code |
| Horizon shader packs: `composite` stage, uniforms, hot reload, Shader Packs screen | first slice planned |
| Horizon shader packs: `composite` stage, uniforms, hot reload, Shader Packs page | done (the page: list, pick, open the folder) |
| Horizon shader packs: `deferred`, `gbuffer`, `shadow` stages, custom textures | planned |
| Java shader packs: preprocessing + GLSL parse coverage | spike done: 450 of 470 programs reach valid WGSL (below) |
| Java shader packs: preprocessor (`crates/horizon-shaderpack`: includes, options, Iris's macros, JCPP's `#if` and `#define` rules) | done: every program of the three test packs preprocesses; ten packs tested (Compatibility, below) |
@@ -56,15 +56,19 @@ The lightmap, fog and sky colours are computed on the CPU (`environment.rs`, `fo
### Where packs live and how one is picked
- Packs go in `shaderpacks/` beside `options.txt` in the game folder (the same folder Iris uses, so one folder
holds both kinds). A pack is a folder (zips planned). A pack with a `pack.toml` at its root is a Horizon pack; a pack
with a `shaders/` folder is a Java pack.
- **Options → Video Settings → Shader Packs...** (Classic screens, laid out like Iris's: the packs on the left with
"(off)" first, Apply and Done, *Open Shader Pack Folder*, *Shader Pack Settings...*) and a **Shader Packs** row in
the Horizon menus' Video section open the same list.
holds both kinds). That is the game home the launcher starts the game in (`%APPDATA%\horizon` on Windows,
`~/Library/Application Support/horizon` on macOS, `~/.local/share/horizon` on Linux), not the launcher's version
folder. A pack with a `pack.toml` at its root is a Horizon pack; a folder with `shaders/`, or a `.zip`, is a Java
pack.
- The **Shader Packs** page lists them: a sidebar section in the Horizon menus, **Video Settings → Shader Packs...**
in the Classic ones, and the Shader Pack row of the graphics settings leads to it. It shows the folder's full path
and an *Open Folder* button (the folder is made if it is missing), then one switch per pack: turning one on picks
it, turning it off is the vanilla picture. With no pack, it says so under the path. The list is read again every
second while the page is open, so a pack dropped into the folder shows up.
- The choice is kept in `horizon-graphics.txt` (`shaderPack:<folder or zip name>`, empty = off). A pack's option
values are kept in `shaderpacks/<pack>.txt`, as Iris does.
- A pack that fails to load or compile leaves the vanilla picture on and shows why, in red, on its row (and in the
log). Nothing a pack does can stop the game from starting.
- A pack that fails to load or compile leaves the vanilla picture (or the last pack that worked) on and says why on
its row ("Failed", with the error), in chat and in the log. Nothing a pack does can stop the game from starting.
- Benchmarks (`--benchmark`) and the parity harnesses never load a pack.
### Layout