mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98b1438cb |
No files matched your search
@@ -53,7 +53,7 @@ impl Game<'_> {
|
||||
state.stamp = Stamp::default();
|
||||
None
|
||||
} else {
|
||||
state.stamp = Stamp::of(&dir.join(&id));
|
||||
state.stamp = Stamp::of_pack(&dir, &id);
|
||||
// Java packs are told which GPU they run on.
|
||||
let (vendor, renderer) = self.renderer.as_ref().map_or(("Unknown", ""), |r| {
|
||||
let name = r.info.name.as_str();
|
||||
@@ -97,7 +97,7 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
state.checked = Some(Instant::now());
|
||||
if Stamp::of(&dir().join(&state.selected)) != state.stamp {
|
||||
if Stamp::of_pack(&dir(), &state.selected) != state.stamp {
|
||||
self.load_shader_pack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ pub enum HRow {
|
||||
ShaderPackFolder,
|
||||
/// Shader pack `i` of `OptionsUi::shader_packs` and its switch.
|
||||
ShaderPack(usize),
|
||||
/// Option `i` of the selected Horizon shader pack.
|
||||
ShaderPackOption(usize),
|
||||
}
|
||||
|
||||
/// The sidebar section listing the shader packs.
|
||||
@@ -298,6 +300,8 @@ enum Target {
|
||||
Search,
|
||||
Row(usize),
|
||||
Slider(Item, f32),
|
||||
/// The selected shader pack's option `i`, a slider at `v`.
|
||||
PackSlider(usize, f32),
|
||||
Style(usize),
|
||||
Level(usize),
|
||||
}
|
||||
@@ -315,6 +319,8 @@ impl HorizonScrollbar {
|
||||
}
|
||||
struct HorizonLayout {
|
||||
sliders: Vec<(Item, f32, f32)>,
|
||||
/// The selected shader pack's option sliders: option, x, width.
|
||||
pack_sliders: Vec<(usize, f32, f32)>,
|
||||
scrollbar: Option<HorizonScrollbar>,
|
||||
}
|
||||
fn horizon_batch(framebuffer: [i32; 2]) -> HudBatch {
|
||||
@@ -329,7 +335,15 @@ impl OptionsUi {
|
||||
"No shader packs: put a pack's folder or .zip in this folder",
|
||||
));
|
||||
}
|
||||
rows.extend((0..self.shader_packs.packs.len()).map(HRow::ShaderPack));
|
||||
for (p, (id, _)) in self.shader_packs.packs.iter().enumerate() {
|
||||
rows.push(HRow::ShaderPack(p));
|
||||
// The selected pack's options, under its row.
|
||||
if *id == s.horizon.shader_pack
|
||||
&& let Some(options) = self.shader_packs.options.get(id)
|
||||
{
|
||||
rows.extend((0..options.len()).map(HRow::ShaderPackOption));
|
||||
}
|
||||
}
|
||||
}
|
||||
let query = self.settings_search.value.to_lowercase();
|
||||
if query.is_empty() || !searchable_section(section) {
|
||||
@@ -412,6 +426,7 @@ impl OptionsUi {
|
||||
let hstart = ui.hits.len();
|
||||
let tstart = crate::widgets::targets_mark();
|
||||
let mut sliders = Vec::new();
|
||||
let mut pack_sliders = Vec::new();
|
||||
ui.divider(x, y, cw);
|
||||
let control_x = x + cw;
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
@@ -564,6 +579,39 @@ impl OptionsUi {
|
||||
let on = s.horizon.shader_pack == *id;
|
||||
ui.switch(ROW + i as u64, control_x, y + ROW_H / 2.0, on);
|
||||
}
|
||||
HRow::ShaderPackOption(k) => {
|
||||
let Some(o) = self.pack_option(s, k) else {
|
||||
continue;
|
||||
};
|
||||
let id = ROW + i as u64;
|
||||
let cy = y + ROW_H / 2.0;
|
||||
let value = crate::shader_pack::option_text(o);
|
||||
match o.kind {
|
||||
crate::shader_pack::OptionKind::Bool => {
|
||||
ui.row(x, y, &o.label, "", 0.0);
|
||||
ui.switch(id, control_x, cy, o.value != 0.0);
|
||||
}
|
||||
crate::shader_pack::OptionKind::Int => {
|
||||
ui.row(x, y, &o.label, "", 0.0);
|
||||
let bw = ui.button_width(&value, false).max(96.0);
|
||||
ui.button(
|
||||
id,
|
||||
[control_x - bw, y + 12.0, bw, CONTROL],
|
||||
&value,
|
||||
Button::Secondary,
|
||||
true,
|
||||
);
|
||||
}
|
||||
crate::shader_pack::OptionKind::Float { min, max, .. } => {
|
||||
let sw = 220.0_f32.min(cw - ROW_LABEL - 200.0).max(80.0);
|
||||
ui.row(x, y, &o.label, &value, cw - ROW_LABEL - 16.0 - sw - 24.0);
|
||||
let sx = control_x - sw - 8.0;
|
||||
let v = ((o.value - min) / (max - min).max(1e-9)) as f32;
|
||||
ui.slider(id, sx, cy, sw, v);
|
||||
pack_sliders.push((k, sx, sw));
|
||||
}
|
||||
}
|
||||
}
|
||||
HRow::OpenModFolder => {
|
||||
ui.row(x, y, "Mod folder", "", 0.0);
|
||||
let label = "Open Folder";
|
||||
@@ -664,7 +712,11 @@ impl OptionsUi {
|
||||
let _ = ox;
|
||||
ui.opacity = 1.0;
|
||||
ui.offset = (0.0, 0.0);
|
||||
HorizonLayout { sliders, scrollbar }
|
||||
HorizonLayout {
|
||||
sliders,
|
||||
pack_sliders,
|
||||
scrollbar,
|
||||
}
|
||||
}
|
||||
pub(crate) fn draw_horizon(
|
||||
&mut self,
|
||||
@@ -734,6 +786,12 @@ impl OptionsUi {
|
||||
let v = t::slider_value(&ui, sx, sw, x);
|
||||
return (Some(Target::Slider(item, v)), scrollbar, cursor);
|
||||
}
|
||||
if let Some(HRow::ShaderPackOption(k)) = rows.get(i)
|
||||
&& let Some(&(_, sx, sw)) = layout.pack_sliders.iter().find(|(o, ..)| o == k)
|
||||
{
|
||||
let v = t::slider_value(&ui, sx, sw, x);
|
||||
return (Some(Target::PackSlider(*k, v)), scrollbar, cursor);
|
||||
}
|
||||
(
|
||||
(i < rows.len()).then_some(Target::Row(i)),
|
||||
scrollbar,
|
||||
@@ -785,6 +843,12 @@ impl OptionsUi {
|
||||
set_item_slider(item, s, t::slider_value(&ui, sx, sw, x));
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
if let Some(k) = self.hdragging_pack
|
||||
&& let Some(&(_, sx, sw)) = layout.pack_sliders.iter().find(|(o, ..)| *o == k)
|
||||
{
|
||||
let v = t::slider_value(&ui, sx, sw, x);
|
||||
self.set_pack_option(s, k, |o| o.set_fraction(v));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
// Rebinding consumes the click before sidebar or widget actions.
|
||||
@@ -835,6 +899,10 @@ impl OptionsUi {
|
||||
set_item_slider(item, s, v);
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
Some(Target::PackSlider(k, v)) => {
|
||||
self.hdragging_pack = Some(k);
|
||||
self.set_pack_option(s, k, |o| o.set_fraction(v));
|
||||
}
|
||||
Some(Target::Row(i)) => {
|
||||
let section = self.section.min(SECTIONS.len() - 1);
|
||||
match self.horizon_rows(s, section).get(i) {
|
||||
@@ -877,6 +945,10 @@ impl OptionsUi {
|
||||
self.toggle_shader_pack(s, *p);
|
||||
events.push(Event::Changed);
|
||||
}
|
||||
Some(HRow::ShaderPackOption(k)) => {
|
||||
events.push(Event::ButtonClick);
|
||||
self.set_pack_option(s, *k, |o| o.cycle(right));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -884,6 +956,35 @@ impl OptionsUi {
|
||||
}
|
||||
events
|
||||
}
|
||||
/// The selected shader pack's option `k`.
|
||||
fn pack_option(&self, s: &Settings, k: usize) -> Option<&crate::shader_pack::PackOption> {
|
||||
self.shader_packs
|
||||
.options
|
||||
.get(&s.horizon.shader_pack)?
|
||||
.get(k)
|
||||
}
|
||||
/// Change the selected pack's option `k` and save the pack's values; the
|
||||
/// game sees the file change and loads the pack again with them.
|
||||
fn set_pack_option(
|
||||
&mut self,
|
||||
s: &Settings,
|
||||
k: usize,
|
||||
change: impl FnOnce(&mut crate::shader_pack::PackOption),
|
||||
) {
|
||||
let id = &s.horizon.shader_pack;
|
||||
let folder = &mut self.shader_packs;
|
||||
let Some(options) = folder.options.get_mut(id) else {
|
||||
return;
|
||||
};
|
||||
let Some(o) = options.get_mut(k) else { return };
|
||||
let before = o.value;
|
||||
change(o);
|
||||
if o.value != before
|
||||
&& let Err(e) = crate::shader_pack::save_values(&folder.dir, id, options)
|
||||
{
|
||||
log::warn!("Horizon: shader pack {id}: saving its options: {e}");
|
||||
}
|
||||
}
|
||||
fn select_section(&mut self, i: usize) {
|
||||
if searchable_section(self.section) || searchable_section(i) {
|
||||
self.settings_search.set_value("");
|
||||
@@ -1187,6 +1288,37 @@ mod tests {
|
||||
click(&mut options, &mut settings, ROW + 1);
|
||||
assert_eq!(settings.horizon.shader_pack, "");
|
||||
|
||||
// A Horizon pack's options show under it once it's picked, and a
|
||||
// change is saved where the game reads it.
|
||||
std::fs::create_dir_all(dir.join("Mine")).unwrap();
|
||||
std::fs::write(
|
||||
dir.join("Mine/pack.toml"),
|
||||
"[pack]\nname = \"Mine\"\napi = 1\n\n[[stage]]\nkind = \"composite\"\nshader = \"a.wgsl\"\n\n\
|
||||
[options.vignette]\nlabel = \"Vignette\"\ndefault = true\n\n\
|
||||
[options.strength]\nlabel = \"Strength\"\ndefault = 0.5\nmin = 0.0\nmax = 1.0\nstep = 0.25\n",
|
||||
)
|
||||
.unwrap();
|
||||
options.shader_packs = Default::default();
|
||||
options.shader_packs.poll(dir.clone());
|
||||
click(&mut options, &mut settings, ROW + 1);
|
||||
assert_eq!(settings.horizon.shader_pack, "Mine");
|
||||
assert!(matches!(
|
||||
options.horizon_rows(&settings, options.section)[..],
|
||||
[
|
||||
HRow::ShaderPackFolder,
|
||||
HRow::ShaderPack(0),
|
||||
HRow::ShaderPackOption(0),
|
||||
HRow::ShaderPackOption(1),
|
||||
HRow::ShaderPack(1)
|
||||
]
|
||||
));
|
||||
click(&mut options, &mut settings, ROW + 3);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dir.join("Mine.txt")).unwrap(),
|
||||
"strength=0.5\nvignette=false\n"
|
||||
);
|
||||
click(&mut options, &mut settings, ROW + 1);
|
||||
|
||||
let mut options = section_ui("Video");
|
||||
settings.horizon.advanced = true;
|
||||
let video = SECTIONS.iter().position(|s| s.name == "Video").unwrap();
|
||||
|
||||
@@ -977,6 +977,8 @@ pub struct OptionsUi {
|
||||
pub(crate) hscroll: f32,
|
||||
/// The slider being dragged on the Horizon options shell.
|
||||
pub(crate) hdragging: Option<crate::graphics_ui::Item>,
|
||||
/// The selected shader pack's option slider being dragged.
|
||||
pub(crate) hdragging_pack: Option<usize>,
|
||||
/// Key Binds: the mapping waiting for a key.
|
||||
pub selected_key: Option<&'static str>,
|
||||
/// Controller Binds: the action waiting for a pad button.
|
||||
@@ -1021,6 +1023,7 @@ impl Default for OptionsUi {
|
||||
section_shown: None,
|
||||
hscroll: 0.0,
|
||||
hdragging: None,
|
||||
hdragging_pack: None,
|
||||
selected_key: None,
|
||||
selected_pad_action: None,
|
||||
pad_style: GlyphStyle::Xbox,
|
||||
@@ -2541,6 +2544,7 @@ impl OptionsUi {
|
||||
// release, even if the pointer has left the widget or its value stayed.
|
||||
let slider = self.dragging.take().is_some();
|
||||
self.hdragging = None;
|
||||
self.hdragging_pack = None;
|
||||
let graphics_slider = self.graphics.release();
|
||||
if slider || graphics_slider {
|
||||
vec![Event::ButtonClick]
|
||||
|
||||
@@ -103,6 +103,48 @@ pub struct PackOption {
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl PackOption {
|
||||
/// A slider at `v` (0..1): the value there, on the option's steps.
|
||||
pub fn set_fraction(&mut self, v: f32) {
|
||||
if let OptionKind::Float { min, max, step } = self.kind {
|
||||
let mut value = min + f64::from(v.clamp(0.0, 1.0)) * (max - min);
|
||||
if step > 0.0 {
|
||||
value = min + ((value - min) / step).round() * step;
|
||||
}
|
||||
self.value = value.clamp(min, max);
|
||||
}
|
||||
}
|
||||
/// A click: a switch flips, a choice moves on (back with `back`), a
|
||||
/// slider moves one step and wraps.
|
||||
pub fn cycle(&mut self, back: bool) {
|
||||
match self.kind {
|
||||
OptionKind::Bool => self.value = if self.value != 0.0 { 0.0 } else { 1.0 },
|
||||
OptionKind::Int if !self.values.is_empty() => {
|
||||
let n = self.values.len();
|
||||
let i = self
|
||||
.values
|
||||
.iter()
|
||||
.position(|&v| v as f64 == self.value)
|
||||
.unwrap_or(0);
|
||||
let i = if back { (i + n - 1) % n } else { (i + 1) % n };
|
||||
self.value = self.values[i] as f64;
|
||||
}
|
||||
OptionKind::Int => {}
|
||||
OptionKind::Float { min, max, step } => {
|
||||
let step = if step > 0.0 { step } else { (max - min) / 20.0 };
|
||||
let next = self.value + if back { -step } else { step };
|
||||
self.value = if next > max + 1e-9 {
|
||||
min
|
||||
} else if next < min - 1e-9 {
|
||||
max
|
||||
} else {
|
||||
next
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShaderPack {
|
||||
/// The pack's folder name.
|
||||
pub id: String,
|
||||
@@ -120,6 +162,15 @@ pub struct ShaderPack {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Stamp(Vec<(PathBuf, u64, Option<SystemTime>)>);
|
||||
impl Stamp {
|
||||
/// Pack `id` in `shaderpacks/` and its saved option values.
|
||||
pub fn of_pack(folder: &Path, id: &str) -> Self {
|
||||
let mut stamp = Self::of(&folder.join(id));
|
||||
let values = folder.join(format!("{id}.txt"));
|
||||
if let Ok(meta) = fs::metadata(&values) {
|
||||
stamp.0.push((values, meta.len(), meta.modified().ok()));
|
||||
}
|
||||
stamp
|
||||
}
|
||||
pub fn of(dir: &Path) -> Self {
|
||||
let mut files = Vec::new();
|
||||
if let Ok(meta) = fs::metadata(dir)
|
||||
@@ -184,6 +235,8 @@ pub fn folder() -> PathBuf {
|
||||
pub struct Folder {
|
||||
pub dir: PathBuf,
|
||||
pub packs: Vec<(String, Result<Info, String>)>,
|
||||
/// Each Horizon pack's options with their saved values.
|
||||
pub options: BTreeMap<String, Vec<PackOption>>,
|
||||
read: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
@@ -196,6 +249,15 @@ impl Folder {
|
||||
return;
|
||||
}
|
||||
self.packs = list(&dir);
|
||||
self.options = self
|
||||
.packs
|
||||
.iter()
|
||||
.filter(|(id, info)| info.is_ok() && dir.join(id).join("pack.toml").is_file())
|
||||
.filter_map(|(id, _)| {
|
||||
let options = pack_options(&dir.join(id), &load_values(&dir, id)).ok()?;
|
||||
Some((id.clone(), options))
|
||||
})
|
||||
.collect();
|
||||
self.dir = dir;
|
||||
self.read = Some(std::time::Instant::now());
|
||||
}
|
||||
@@ -412,6 +474,37 @@ fn validate(source: &str, file: &str, entry: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A Horizon pack's options with `values` (the Shader Packs page's rows).
|
||||
pub fn pack_options(
|
||||
dir: &Path,
|
||||
values: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<PackOption>, String> {
|
||||
manifest(dir)?
|
||||
.options
|
||||
.into_iter()
|
||||
.map(|(key, entry)| option(key, entry, values))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An option's value as `shaderpacks/<id>.txt` keeps it.
|
||||
pub fn option_text(o: &PackOption) -> String {
|
||||
match o.kind {
|
||||
OptionKind::Bool => (o.value != 0.0).to_string(),
|
||||
OptionKind::Int => (o.value as i64).to_string(),
|
||||
OptionKind::Float { .. } => format!("{}", o.value as f32),
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a pack's option values; the game sees the file change and loads the
|
||||
/// pack again with them.
|
||||
pub fn save_values(dir: &Path, id: &str, options: &[PackOption]) -> std::io::Result<()> {
|
||||
let text: String = options
|
||||
.iter()
|
||||
.map(|o| format!("{}={}\n", o.key, option_text(o)))
|
||||
.collect();
|
||||
fs::write(dir.join(format!("{id}.txt")), text)
|
||||
}
|
||||
|
||||
/// The option values saved for a pack (`shaderpacks/<id>.txt`, `key=value`).
|
||||
pub fn load_values(dir: &Path, id: &str) -> BTreeMap<String, String> {
|
||||
fs::read_to_string(dir.join(format!("{id}.txt")))
|
||||
@@ -577,6 +670,17 @@ override mode: i32 = 1;
|
||||
assert_eq!(listed[1].1.as_ref().unwrap().name, "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_option_values_change_the_pack_stamp() {
|
||||
let dir = scratch();
|
||||
fs::create_dir_all(dir.join("p")).unwrap();
|
||||
fs::write(dir.join("p/pack.toml"), "").unwrap();
|
||||
let before = Stamp::of_pack(&dir, "p");
|
||||
fs::write(dir.join("p.txt"), "a=1\n").unwrap();
|
||||
assert!(Stamp::of_pack(&dir, "p") != before);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_values_are_read_per_pack() {
|
||||
let dir = scratch();
|
||||
|
||||
@@ -67,6 +67,9 @@ The lightmap, fog and sky colours are computed on the CPU (`environment.rs`, `fo
|
||||
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.
|
||||
- In the Horizon menus, the picked Horizon pack's options show under its row: a switch for `true`/`false`, a
|
||||
button cycling through `values`, a slider for numbers (a click beside it moves one `step`). A change is saved
|
||||
to `shaderpacks/<pack>.txt` at once, and the pack loads again with it (the file counts as one of the pack's).
|
||||
- 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.
|
||||
|
||||
Reference in new issue
Block a user