mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-10 02:21:33 -10:00
Merge pull request #3325 from ThatCakeIsAPie/feat/create-world-data-packs
🤖 Agent: verified by Ludovic's AI agent (Claude): follows CreateWorldScreen's temp data pack folder flow; main merged, clippy and the datapack/title/pack screen tests pass, verify green. Queued for the merger.
This commit is contained in:
13 files changed
+367
-24
No files matched your search
@@ -125,6 +125,114 @@ pub fn discover(world: &Path) -> Vec<DataPack> {
|
||||
packs
|
||||
}
|
||||
|
||||
// ---- Create World ------------------------------------------------------------
|
||||
|
||||
/// Create World's Data Packs screen (`PackSelectionScreen` on vanilla's
|
||||
/// `mcworld-` folder): the folder's packs (`PackSource.WORLD`) and the
|
||||
/// built-in one (`resources` holds its `pack.png`), which can be unselected
|
||||
/// and stays at the bottom. Horizon has no experiments, so their feature
|
||||
/// packs are not offered.
|
||||
pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::PackInfo> {
|
||||
use crate::{
|
||||
pack_screen::{Compat, PackInfo, Source, load_icon},
|
||||
packs::Position,
|
||||
text::{Style, Text},
|
||||
};
|
||||
let format = horizon_core::version::DATA_PACK_FORMAT;
|
||||
let mut infos: Vec<PackInfo> = crate::packs::folder(dir, format)
|
||||
.into_iter()
|
||||
.map(|p| PackInfo {
|
||||
icon: p.icon_png().as_deref().and_then(load_icon),
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
description: p.raw_description,
|
||||
description_style: p.description_style,
|
||||
source: Source::World,
|
||||
compat: p.compatibility,
|
||||
required: false,
|
||||
position: Position::Top,
|
||||
fixed: false,
|
||||
})
|
||||
.collect();
|
||||
infos.push(PackInfo {
|
||||
id: "vanilla".into(),
|
||||
title: Text::translate("dataPack.vanilla.name", &[]),
|
||||
description: Text::translate("dataPack.vanilla.description", &[]),
|
||||
description_style: Style::default(),
|
||||
source: Source::BuiltIn,
|
||||
compat: Compat::Compatible,
|
||||
icon: std::fs::read(resources.join("pack.png"))
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(load_icon),
|
||||
required: false,
|
||||
position: Position::Bottom,
|
||||
fixed: false,
|
||||
});
|
||||
infos
|
||||
}
|
||||
/// A new empty folder for Create World's packs
|
||||
/// (`Files.createTempDirectory("mcworld-")`).
|
||||
pub fn temp_folder() -> std::io::Result<PathBuf> {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("mcworld-{}-{nanos}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
/// What's in a folder of packs, to notice packs added or removed while the
|
||||
/// screen shows it (`PackSelectionScreen.Watcher`).
|
||||
pub fn folder_stamp(dir: &Path) -> Vec<(String, Option<std::time::SystemTime>)> {
|
||||
let mut stamp: Vec<_> = std::fs::read_dir(dir)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.map(|e| {
|
||||
let modified = e.metadata().and_then(|m| m.modified()).ok();
|
||||
(e.file_name().to_string_lossy().into_owned(), modified)
|
||||
})
|
||||
.collect();
|
||||
stamp.sort();
|
||||
stamp
|
||||
}
|
||||
/// Create World's packs into the new world before it opens
|
||||
/// (`copyTempDataPackDirToNewWorld`): every pack of `dir` goes to
|
||||
/// `<world>/datapacks`, and `datapacks.json` enables those `selected`
|
||||
/// (lowest priority first) and disables the rest.
|
||||
pub fn install(dir: &Path, world: &Path, selected: &[String]) -> Result<(), String> {
|
||||
let err = |p: &Path, e: std::io::Error| format!("{}: {e}", p.display());
|
||||
let target = world.join("datapacks");
|
||||
std::fs::create_dir_all(&target).map_err(|e| err(&target, e))?;
|
||||
let mut disabled = Vec::new();
|
||||
for pack in crate::packs::folder(dir, horizon_core::version::DATA_PACK_FORMAT) {
|
||||
let (crate::packs::Source::Dir(from) | crate::packs::Source::Zip(from)) = &pack.source;
|
||||
copy(from, &target.join(pack.id.trim_start_matches("file/")))?;
|
||||
if !selected.contains(&pack.id) {
|
||||
disabled.push(pack.id);
|
||||
}
|
||||
}
|
||||
if !selected.iter().any(|id| id == "vanilla") {
|
||||
disabled.push("vanilla".into());
|
||||
}
|
||||
let json = serde_json::json!({ "enabled": selected, "disabled": disabled });
|
||||
let file = world.join("datapacks.json");
|
||||
std::fs::write(&file, json.to_string()).map_err(|e| err(&file, e))
|
||||
}
|
||||
/// A file, or a folder and everything in it.
|
||||
fn copy(from: &Path, to: &Path) -> Result<(), String> {
|
||||
let err = |e: std::io::Error| format!("{} -> {}: {e}", from.display(), to.display());
|
||||
if !from.is_dir() {
|
||||
return std::fs::copy(from, to).map(drop).map_err(err);
|
||||
}
|
||||
std::fs::create_dir_all(to).map_err(err)?;
|
||||
for e in std::fs::read_dir(from).map_err(err)?.flatten() {
|
||||
copy(&e.path(), &to.join(e.file_name()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- functions ---------------------------------------------------------------
|
||||
|
||||
/// A `.mcfunction`: plain command lines and macro lines.
|
||||
@@ -566,4 +674,51 @@ mod tests {
|
||||
assert_eq!(p.list_kinds(&kinds), wanted);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_world_installs_its_packs() {
|
||||
let root = std::env::temp_dir().join(format!("hz-datapack-install-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let (dir, world) = (root.join("mcworld"), root.join("world"));
|
||||
let write = |pack: &str, p: &str, t: &str| {
|
||||
let path = dir.join(pack).join(p);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, t).unwrap();
|
||||
};
|
||||
write("fns", "pack.mcmeta", r#"{"pack":{"pack_format":121}}"#);
|
||||
write("fns", "data/x/function/f.mcfunction", "say hi");
|
||||
write("off", "pack.mcmeta", r#"{"pack":{"pack_format":121}}"#);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
|
||||
zip.start_file("pack.mcmeta", zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
std::io::Write::write_all(&mut zip, br#"{"pack":{"pack_format":48}}"#).unwrap();
|
||||
zip.finish().unwrap();
|
||||
let infos = screen_packs(&dir, &root);
|
||||
let ids: Vec<&str> = infos.iter().map(|p| p.id.as_str()).collect();
|
||||
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip", "vanilla"]);
|
||||
let compat: Vec<_> = infos.iter().map(|p| p.compat).collect();
|
||||
use crate::pack_screen::Compat;
|
||||
assert_eq!(
|
||||
compat,
|
||||
[
|
||||
Compat::Compatible,
|
||||
Compat::Compatible,
|
||||
Compat::TooOld,
|
||||
Compat::Compatible
|
||||
]
|
||||
);
|
||||
let selected = ["vanilla", "file/old.zip", "file/fns"].map(String::from);
|
||||
install(&dir, &world, &selected).unwrap();
|
||||
let packs = discover(&world);
|
||||
let ids: Vec<&str> = packs.iter().map(|p| p.id.as_str()).collect();
|
||||
assert_eq!(ids, ["file/fns", "file/off", "file/old.zip"]);
|
||||
let saved: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(world.join("datapacks.json")).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
saved,
|
||||
serde_json::json!({"enabled": selected, "disabled": ["file/off"]})
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
@@ -1012,7 +1012,11 @@ pub enum Event {
|
||||
Language(String),
|
||||
/// The pack selection changed (reload resources).
|
||||
Packs(Vec<String>),
|
||||
/// Create World's Data Packs screen closed with a new selection.
|
||||
DataPacks(Vec<String>),
|
||||
OpenPackFolder,
|
||||
/// Data Packs' Open Pack Folder: create the folder if needed and open it.
|
||||
OpenDataPackFolder(std::path::PathBuf),
|
||||
OpenModFolder,
|
||||
/// The Shader Packs page opened: list `shaderpacks/` into `shader_packs`.
|
||||
ListShaderPacks,
|
||||
@@ -1078,6 +1082,13 @@ pub struct OptionsUi {
|
||||
pub pack_selection: Vec<String>,
|
||||
/// The open pack screen, made at its first frame.
|
||||
pub pack_screen: Option<crate::pack_screen::PackScreen>,
|
||||
/// The Packs page is Create World's Data Packs screen on this folder.
|
||||
data_pack_dir: Option<std::path::PathBuf>,
|
||||
/// The game's resources, for the built-in data pack's icon.
|
||||
data_pack_resources: std::path::PathBuf,
|
||||
/// The folder's entries when its packs were listed, and when they were
|
||||
/// last compared with it.
|
||||
data_pack_stamp: (Vec<(String, Option<std::time::SystemTime>)>, Instant),
|
||||
/// A reload failure, dismissed before returning to the current page.
|
||||
pub resource_error: Option<String>,
|
||||
/// When the pack screen's search box took the focus (its cursor blink).
|
||||
@@ -1112,6 +1123,9 @@ impl Default for OptionsUi {
|
||||
pack_infos: Vec::new(),
|
||||
pack_selection: Vec::new(),
|
||||
pack_screen: None,
|
||||
data_pack_dir: None,
|
||||
data_pack_resources: Default::default(),
|
||||
data_pack_stamp: (Vec::new(), Instant::now()),
|
||||
resource_error: None,
|
||||
pack_focus: Instant::now(),
|
||||
in_world: false,
|
||||
@@ -1219,9 +1233,48 @@ impl OptionsUi {
|
||||
pub fn open(&mut self, in_world: bool) {
|
||||
self.open_at(in_world, Page::Main);
|
||||
}
|
||||
/// Create World's Data Packs (`openDataPackSelectionScreen`): the Packs
|
||||
/// page on the new world's pack folder `dir`, `selected` chosen;
|
||||
/// `resources` holds the built-in pack's icon.
|
||||
pub fn open_data_packs(
|
||||
&mut self,
|
||||
dir: std::path::PathBuf,
|
||||
selected: Vec<String>,
|
||||
resources: std::path::PathBuf,
|
||||
) {
|
||||
self.open_at(false, Page::Packs);
|
||||
self.pack_infos = crate::datapack::screen_packs(&dir, &resources);
|
||||
self.data_pack_resources = resources;
|
||||
self.pack_selection = selected;
|
||||
self.data_pack_stamp = (crate::datapack::folder_stamp(&dir), Instant::now());
|
||||
self.data_pack_dir = Some(dir);
|
||||
}
|
||||
/// `PackSelectionScreen.tick`'s watcher: once a second, the Data Packs
|
||||
/// folder's packs are listed again if it changed, the selection kept
|
||||
/// (the search text and scroll start over).
|
||||
fn watch_data_packs(&mut self, atlas: &HudAtlas, size: (f32, f32)) {
|
||||
let Some(dir) = &self.data_pack_dir else {
|
||||
return;
|
||||
};
|
||||
if self.data_pack_stamp.1.elapsed().as_secs() < 1 {
|
||||
return;
|
||||
}
|
||||
let stamp = crate::datapack::folder_stamp(dir);
|
||||
let changed = stamp != self.data_pack_stamp.0;
|
||||
self.data_pack_stamp = (stamp, Instant::now());
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
self.pack_infos = crate::datapack::screen_packs(dir, &self.data_pack_resources);
|
||||
if let Some(p) = self.pack_screen.take() {
|
||||
self.pack_selection = p.commit();
|
||||
self.packs(atlas, size).changed = p.changed;
|
||||
}
|
||||
}
|
||||
/// Open a page whose Done/Escape returns directly to the calling screen.
|
||||
pub fn open_at(&mut self, in_world: bool, page: Page) {
|
||||
self.stack.clear();
|
||||
self.data_pack_dir = None;
|
||||
self.push(page);
|
||||
self.in_world = in_world;
|
||||
self.selected_key = None;
|
||||
@@ -2273,6 +2326,7 @@ impl OptionsUi {
|
||||
return;
|
||||
}
|
||||
if self.page() == Page::Packs {
|
||||
self.watch_data_packs(atlas, (w, h));
|
||||
ui::menu_background(batch, atlas, 0.0, 0.0, w, h, self.in_world);
|
||||
let blink = self.pack_focus.elapsed().as_millis() as u64;
|
||||
let m = crate::screen_draw::Measure(&atlas.font);
|
||||
@@ -2568,7 +2622,10 @@ impl OptionsUi {
|
||||
self.pack_focus = Instant::now();
|
||||
}
|
||||
match action {
|
||||
crate::pack_screen::Action::OpenFolder => events.push(Event::OpenPackFolder),
|
||||
crate::pack_screen::Action::OpenFolder => events.push(match &self.data_pack_dir {
|
||||
Some(dir) => Event::OpenDataPackFolder(dir.clone()),
|
||||
None => Event::OpenPackFolder,
|
||||
}),
|
||||
crate::pack_screen::Action::Done => events.extend(self.back(s)),
|
||||
crate::pack_screen::Action::None => {}
|
||||
}
|
||||
@@ -2820,8 +2877,14 @@ impl OptionsUi {
|
||||
}
|
||||
None => {
|
||||
self.pack_focus = Instant::now();
|
||||
let title = if self.data_pack_dir.is_some() {
|
||||
"dataPack.title"
|
||||
} else {
|
||||
"resourcePack.title"
|
||||
};
|
||||
self.pack_screen = Some(crate::pack_screen::PackScreen::new(
|
||||
&m,
|
||||
crate::text::Text::translate(title, &[]),
|
||||
w,
|
||||
h,
|
||||
self.in_world,
|
||||
@@ -2853,13 +2916,18 @@ impl OptionsUi {
|
||||
}
|
||||
Page::Packs => {
|
||||
// PackSelectionScreen.onClose: PackSelectionModel.commit.
|
||||
let data = self.data_pack_dir.take();
|
||||
if let Some(p) = self.pack_screen.take()
|
||||
&& p.changed
|
||||
{
|
||||
let ids = p.commit();
|
||||
s.resource_packs = ids.clone();
|
||||
self.pack_selection = ids.clone();
|
||||
events.push(Event::Packs(ids));
|
||||
if data.is_some() {
|
||||
events.push(Event::DataPacks(ids));
|
||||
} else {
|
||||
s.resource_packs = ids.clone();
|
||||
self.pack_selection = ids.clone();
|
||||
events.push(Event::Packs(ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -318,6 +318,8 @@ pub enum Action {
|
||||
/// The pack screen and its state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PackScreen {
|
||||
/// `resourcePack.title`, or `dataPack.title` from Create World.
|
||||
pub title: Text,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub in_world: bool,
|
||||
@@ -342,6 +344,7 @@ impl PackScreen {
|
||||
/// `PackSelectionScreen.init`: the lists filled, the search box focused.
|
||||
pub fn new(
|
||||
m: &Measure,
|
||||
title: Text,
|
||||
width: i32,
|
||||
height: i32,
|
||||
in_world: bool,
|
||||
@@ -360,6 +363,7 @@ impl PackScreen {
|
||||
.filter_map(|p| Some((icon_texture(&p.id), p.icon.clone()?)))
|
||||
.collect();
|
||||
let mut s = Self {
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
in_world,
|
||||
@@ -382,7 +386,7 @@ impl PackScreen {
|
||||
}
|
||||
/// The header layout: title, drop info and search box, centred.
|
||||
fn header(&self, m: &Measure) -> [(i32, i32); 3] {
|
||||
let tw = m.width(&tr("resourcePack.title"));
|
||||
let tw = m.width(&self.title);
|
||||
let dw = m.width(&tr("pack.dropInfo"));
|
||||
let col = tw.max(dw).max(200);
|
||||
let frame = self.width.max(col);
|
||||
@@ -837,7 +841,7 @@ impl PackScreen {
|
||||
return out;
|
||||
}
|
||||
let [(tx, ty), (dx, dy), _] = self.header(m);
|
||||
d::string_widget(&mut out, m, &tr("resourcePack.title"), tx, ty, 0);
|
||||
d::string_widget(&mut out, m, &self.title, tx, ty, 0);
|
||||
d::string_widget(
|
||||
&mut out,
|
||||
m,
|
||||
@@ -1105,7 +1109,7 @@ mod icon_tests {
|
||||
pack("high_contrast", Some(contrast.clone())),
|
||||
pack("file/Iconless", None),
|
||||
];
|
||||
let screen = PackScreen::new(&m, 480, 320, false, packs, &[]);
|
||||
let screen = PackScreen::new(&m, tr("resourcePack.title"), 480, 320, false, packs, &[]);
|
||||
let mut draws = screen.draw(&m, (0, 0), 0);
|
||||
let mut patches = Vec::new();
|
||||
screen.bind_icons(&mut draws, &atlas, &mut patches);
|
||||
@@ -1152,7 +1156,8 @@ mod click_sound_tests {
|
||||
crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets"))
|
||||
.unwrap();
|
||||
let m = Measure(&atlas.font);
|
||||
let mut screen = PackScreen::new(&m, 480, 320, false, Vec::new(), &[]);
|
||||
let title = tr("resourcePack.title");
|
||||
let mut screen = PackScreen::new(&m, title, 480, 320, false, Vec::new(), &[]);
|
||||
assert!(!screen.button_at(&m, 0.0, 0.0));
|
||||
assert!(!screen.button_at(
|
||||
&m,
|
||||
|
||||
@@ -181,9 +181,15 @@ pub fn compatible(meta: &serde_json::Value) -> bool {
|
||||
/// `PackCompatibility.forVersion`: too old, too new, compatible, or unknown
|
||||
/// when the metadata names no format.
|
||||
pub fn compatibility(meta: &serde_json::Value) -> crate::pack_screen::Compat {
|
||||
compatibility_for(meta, RESOURCE_FORMAT)
|
||||
}
|
||||
/// `compatibility` against `current`, the resource or the data pack format.
|
||||
pub fn compatibility_for(
|
||||
meta: &serde_json::Value,
|
||||
current: (i64, i64),
|
||||
) -> crate::pack_screen::Compat {
|
||||
use crate::pack_screen::Compat;
|
||||
let pack = &meta["pack"];
|
||||
let current = RESOURCE_FORMAT;
|
||||
let range = if let (Some(min), Some(max)) = (
|
||||
format_of(&pack["min_format"]),
|
||||
format_of(&pack["max_format"]),
|
||||
@@ -275,6 +281,10 @@ fn read_meta(source: &Source) -> Option<serde_json::Value> {
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
fn pack(id: String, title: Text, source: Source) -> Option<Pack> {
|
||||
pack_for(id, title, source, RESOURCE_FORMAT)
|
||||
}
|
||||
/// A pack whose compatibility is against `format`.
|
||||
fn pack_for(id: String, title: Text, source: Source, format: (i64, i64)) -> Option<Pack> {
|
||||
let meta = read_meta(&source)?;
|
||||
let (raw_description, description_style) = component_root(&meta["pack"]["description"]);
|
||||
Some(Pack {
|
||||
@@ -284,7 +294,7 @@ fn pack(id: String, title: Text, source: Source) -> Option<Pack> {
|
||||
raw_description,
|
||||
description_style,
|
||||
compatible: compatible(&meta),
|
||||
compatibility: compatibility(&meta),
|
||||
compatibility: compatibility_for(&meta, format),
|
||||
overlays: overlays_of(&meta),
|
||||
filter: filter_of(&meta),
|
||||
source,
|
||||
@@ -305,7 +315,13 @@ pub fn discover(resources: &Path, packs_dir: &Path) -> Vec<Pack> {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
let mut user: Vec<Pack> = fs::read_dir(packs_dir)
|
||||
out.extend(folder(packs_dir, RESOURCE_FORMAT));
|
||||
out
|
||||
}
|
||||
/// The packs (folders and zips) in `dir`, by name, as `FolderRepositorySource`
|
||||
/// finds them; their compatibility is against `format`.
|
||||
pub fn folder(dir: &Path, format: (i64, i64)) -> Vec<Pack> {
|
||||
let mut user: Vec<Pack> = fs::read_dir(dir)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
@@ -319,12 +335,11 @@ pub fn discover(resources: &Path, packs_dir: &Path) -> Vec<Pack> {
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
pack(format!("file/{name}"), Text::literal(name), source)
|
||||
pack_for(format!("file/{name}"), Text::literal(name), source, format)
|
||||
})
|
||||
.collect();
|
||||
user.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
out.extend(user);
|
||||
out
|
||||
user
|
||||
}
|
||||
/// A file a pack provides: its path (folder packs) and its name inside the pack.
|
||||
type Provided = (Option<PathBuf>, String);
|
||||
|
||||
@@ -126,6 +126,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
.collect();
|
||||
let mut screen = PackScreen::new(
|
||||
&m,
|
||||
Text::translate(c["title"].as_str().unwrap_or("resourcePack.title"), &[]),
|
||||
int(&c["width"]),
|
||||
int(&c["height"]),
|
||||
c["inWorld"] == true,
|
||||
|
||||
@@ -9145,6 +9145,15 @@ impl Game<'_> {
|
||||
return Ok(());
|
||||
}
|
||||
self.join_world_rest()?;
|
||||
// Create World's data packs, in the world before it opens.
|
||||
if let Some(dir) = create.and_then(|o| o.pack_dir.as_deref()) {
|
||||
let world = std::env::current_dir()?.join("saves").join(folder);
|
||||
let packs = create.map_or(&[][..], |o| &o.data_packs[..]);
|
||||
if let Err(e) = horizon_client::datapack::install(dir, &world, packs) {
|
||||
log::warn!("Horizon: data packs for {folder}: {e}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
if self
|
||||
.options
|
||||
.execution_role
|
||||
@@ -13260,6 +13269,13 @@ impl ApplicationHandler for Game<'_> {
|
||||
Action::Connect { address } => {
|
||||
self.connect_server(&address, &self.options.player_name())?
|
||||
}
|
||||
Action::DataPacks { dir, selected } => {
|
||||
self.keys.clear();
|
||||
self.screen = Screen::Options;
|
||||
self.options_from_title = true;
|
||||
let resources = self.vanilla_root();
|
||||
self.ui.options.open_data_packs(dir, selected, resources);
|
||||
}
|
||||
Action::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,15 @@ impl Game<'_> {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
super::packs::open_folder(&dir);
|
||||
}
|
||||
Event::DataPacks(ids) => {
|
||||
if let Some(title) = &mut self.title {
|
||||
title.set_data_packs(ids);
|
||||
}
|
||||
}
|
||||
Event::OpenDataPackFolder(dir) => {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
super::packs::open_folder(&dir);
|
||||
}
|
||||
Event::OpenModFolder => {
|
||||
let dir = Self::options_path().with_file_name(horizon_client::mod_list::DIR);
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
|
||||
@@ -365,6 +365,15 @@ impl Loading {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
packs::open_folder(&dir);
|
||||
}
|
||||
Event::DataPacks(ids) => {
|
||||
if let Some(title) = &mut self.menu.title {
|
||||
title.set_data_packs(ids);
|
||||
}
|
||||
}
|
||||
Event::OpenDataPackFolder(dir) => {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
packs::open_folder(&dir);
|
||||
}
|
||||
Event::OpenModFolder => {
|
||||
let dir = Game::options_path().with_file_name(horizon_client::mod_list::DIR);
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
@@ -426,6 +435,11 @@ impl Loading {
|
||||
Action::Options => self.open_menu_options(Page::Main),
|
||||
Action::Accessibility => self.open_menu_options(Page::Accessibility),
|
||||
Action::Language => self.open_menu_options(Page::Language),
|
||||
Action::DataPacks { dir, selected } => {
|
||||
let resources = self.vanilla_root.clone();
|
||||
self.menu.options.open_data_packs(dir, selected, resources);
|
||||
self.menu.options_open = true;
|
||||
}
|
||||
action => {
|
||||
let name = self
|
||||
.options
|
||||
|
||||
@@ -150,6 +150,11 @@ pub enum Action {
|
||||
Connect {
|
||||
address: String,
|
||||
},
|
||||
/// Create World's Data Packs screen on the new world's pack folder.
|
||||
DataPacks {
|
||||
dir: PathBuf,
|
||||
selected: Vec<String>,
|
||||
},
|
||||
}
|
||||
pub struct Title {
|
||||
pub mode: Mode,
|
||||
@@ -384,6 +389,12 @@ impl Title {
|
||||
row
|
||||
}
|
||||
fn cancel_create(&mut self) {
|
||||
// removeTempDataPackDir.
|
||||
if let Mode::Create { options, .. } = &self.mode
|
||||
&& let Some(dir) = &options.pack_dir
|
||||
{
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
if self.create_return_to_main {
|
||||
self.mode = Mode::Main;
|
||||
} else {
|
||||
@@ -448,6 +459,13 @@ impl Title {
|
||||
};
|
||||
}
|
||||
}
|
||||
/// The Data Packs screen's Done (`applyNewPackConfig`): its selection for
|
||||
/// the new world.
|
||||
pub fn set_data_packs(&mut self, ids: Vec<String>) {
|
||||
if let Mode::Create { options, .. } = &mut self.mode {
|
||||
options.data_packs = ids;
|
||||
}
|
||||
}
|
||||
fn matches_search(&self, world: &WorldEntry) -> bool {
|
||||
let filter = self.search.to_lowercase();
|
||||
world.name.to_lowercase().contains(&filter) || world.folder.to_lowercase().contains(&filter)
|
||||
@@ -1669,11 +1687,26 @@ impl Title {
|
||||
creative, options, ..
|
||||
},
|
||||
Some(id),
|
||||
) if id != 21 && id != 22 => {
|
||||
) if !matches!(id, 21 | 22 | 31) => {
|
||||
options.press(id, creative);
|
||||
Action::None
|
||||
}
|
||||
(Mode::Create { .. }, Some(21)) => self.create_action(),
|
||||
(Mode::Create { options, .. }, Some(31)) => {
|
||||
if options.pack_dir.is_none() {
|
||||
match crate::datapack::temp_folder() {
|
||||
Ok(dir) => options.pack_dir = Some(dir),
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: no folder for data packs: {e}");
|
||||
return Action::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::DataPacks {
|
||||
dir: options.pack_dir.clone().unwrap_or_default(),
|
||||
selected: options.data_packs.clone(),
|
||||
}
|
||||
}
|
||||
(Mode::Create { .. }, Some(22)) => {
|
||||
self.cancel_create();
|
||||
Action::None
|
||||
|
||||
@@ -38,6 +38,11 @@ pub struct WorldOptions {
|
||||
pub bonus_chest: bool,
|
||||
/// `getGameRules`: the Game Rules screen's values, saved with the world.
|
||||
pub rules: crate::gamerules::GameRules,
|
||||
/// The Data Packs screen's selection, lowest priority first.
|
||||
pub data_packs: Vec<String>,
|
||||
/// The folder of packs for the new world (vanilla's `mcworld-`
|
||||
/// temporary folder), made when Data Packs first opens.
|
||||
pub pack_dir: Option<std::path::PathBuf>,
|
||||
}
|
||||
impl Default for WorldOptions {
|
||||
fn default() -> Self {
|
||||
@@ -51,6 +56,8 @@ impl Default for WorldOptions {
|
||||
structures: true,
|
||||
bonus_chest: false,
|
||||
rules: crate::gamerules::GameRules::default(),
|
||||
data_packs: vec!["vanilla".into()],
|
||||
pack_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +293,7 @@ pub(super) fn buttons(w: f32, h: f32, creative: bool, o: &WorldOptions) -> Vec<B
|
||||
28,
|
||||
),
|
||||
],
|
||||
// Experiments and data packs have no screens yet.
|
||||
// Experiments have no screen yet.
|
||||
_ => [
|
||||
"selectWorld.gameRules",
|
||||
"selectWorld.experiments",
|
||||
@@ -300,7 +307,7 @@ pub(super) fn buttons(w: f32, h: f32, creative: bool, o: &WorldOptions) -> Vec<B
|
||||
gy + 28.0 * i as f32,
|
||||
210.0,
|
||||
tr(key),
|
||||
i == 0,
|
||||
i != 1,
|
||||
29 + i as u8,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -850,12 +850,29 @@ fn create_world_tabs_follow_vanilla_and_their_choices_reach_the_action() {
|
||||
bonus_chest: true
|
||||
}
|
||||
);
|
||||
// The More tab's Experiments and Data Packs screens don't exist yet.
|
||||
// The More tab: Experiments has no screen yet; Data
|
||||
// Packs opens on a new pack folder, and its selection reaches the action.
|
||||
f.title.click(54.0 + 248.0 + 10.0, 10.0, size);
|
||||
let more = labels(&f);
|
||||
assert_eq!(
|
||||
labels(&f)[..3].iter().map(|b| b.3).collect::<Vec<_>>(),
|
||||
[true, false, false]
|
||||
more[..3].iter().map(|b| b.3).collect::<Vec<_>>(),
|
||||
[true, false, true]
|
||||
);
|
||||
let (action, sound) = f.title.click(more[2].1 + 10.0, more[2].2 + 10.0, size);
|
||||
let Action::DataPacks { dir, selected } = action else {
|
||||
panic!("{action:?}")
|
||||
};
|
||||
assert!(sound && dir.is_dir() && selected == ["vanilla"]);
|
||||
f.title
|
||||
.set_data_packs(vec!["vanilla".into(), "file/ttm.zip".into()]);
|
||||
let Action::Create { options, .. } = f.title.create_action() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(options.data_packs, ["vanilla", "file/ttm.zip"]);
|
||||
assert_eq!(options.pack_dir.as_ref(), Some(&dir));
|
||||
// Cancel removes the folder (removeTempDataPackDir).
|
||||
f.title.escape();
|
||||
assert!(!dir.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -309,7 +309,7 @@ public final class VanillaPackScreenOracle {
|
||||
List<String> initial = new ArrayList<>();
|
||||
for (var s : c.getAsJsonArray("selected")) initial.add(s.getAsString());
|
||||
repository.setSelected(initial);
|
||||
var screen = new PackSelectionScreen(repository, r -> { }, Path.of(c.get("dir").getAsString()), Component.translatable("resourcePack.title"));
|
||||
var screen = new PackSelectionScreen(repository, r -> { }, Path.of(c.get("dir").getAsString()), Component.translatable(c.get("title").getAsString()));
|
||||
gui.setScreen(screen);
|
||||
// PackSelectionScreen.loadPackIcon: a pack with a pack.png gets its own texture.
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the resource pack screen's drawing with the vanilla client's.
|
||||
|
||||
Each case is a pack repository (titles and descriptions as text components,
|
||||
Each case is a title (the resource pack screen's, or the data pack screen's
|
||||
Create World opens), a pack repository (titles and descriptions as text components,
|
||||
with legacy formatting codes, colours and styles; pack sources; compatible,
|
||||
too old, too new or unknown packs; required, fixed and positioned packs;
|
||||
icons), an initial selection, a GUI size, in a world or not, and what the
|
||||
@@ -119,7 +120,9 @@ def cases(count):
|
||||
(rng.randint(cx - 154, cx + 154), height - 26 + rng.randint(0, 19)),
|
||||
(rng.randint(cx - 160, cx + 160), rng.randint(height // 2 - 30, height // 2 + 60))])
|
||||
out.append({'width': width, 'height': height, 'inWorld': rng.random() < 0.3, 'packs': packs, 'selected': selected,
|
||||
'actions': actions, 'mouse': list(mouse), 'blink': rng.choice([0, 100, 400, 650])})
|
||||
'actions': actions, 'mouse': list(mouse), 'blink': rng.choice([0, 100, 400, 650]),
|
||||
# Create World's Data Packs screen is the same screen under its own title.
|
||||
'title': 'dataPack.title' if n % 4 == 3 else 'resourcePack.title'})
|
||||
return out
|
||||
|
||||
|
||||
@@ -161,7 +164,7 @@ def main():
|
||||
for e in sorted(set(errors))[:5]:
|
||||
print('ORACLE ERROR', e[:1500])
|
||||
for c, v, r in bad[:args.detail]:
|
||||
brief = {k: c[k] for k in ('width', 'height', 'inWorld', 'selected', 'actions', 'mouse', 'blink')}
|
||||
brief = {k: c[k] for k in ('title', 'width', 'height', 'inWorld', 'selected', 'actions', 'mouse', 'blink')}
|
||||
print('MISMATCH', json.dumps(brief)[:1500])
|
||||
if v.get('screen') != r.get('screen'):
|
||||
print(f' screen: vanilla {v.get("screen")} | horizon {r.get("screen")}')
|
||||
|
||||
Reference in new issue
Block a user