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:
Ludovic Marie authored and GitHub committed 2026-10-29 16:37:10 +01:00
commit 62a23ba025
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);
}
}
+72 -4
View File
@@ -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,
+21 -6
View File
@@ -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,
+16
View File
@@ -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
+34 -1
View File
@@ -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,
)
})
+20 -3
View File
@@ -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")
+6 -3
View File
@@ -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")}')