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:
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user