mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Creative inventory: the Operator Utilities tab for operators
The Operator Items Tab option was greyed out and nothing read it: the creative export was built without permissions, so the operator tab had no stacks and was dropped. VanillaCreativeTabsExporter now builds the tabs with permissions and marks the stacks only that build lists (permissions_only). Creative keeps every exported stack and rebuilds its tabs and search tab for hasPermissions (canUseGameMasterBlocks and the option), as CreativeModeInventoryScreen does on opening and in containerTick (tryRefreshInvalidatedTabs): a selected operator tab left empty gives way to the default tab. The option is switchable again. test_creative_parity.py runs with and without permissions. Part of #3393. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e8ecd77cc8
commit
d4a973f65f
9 files changed
+337
-81
No files matched your search
@@ -141,9 +141,7 @@ impl Opt {
|
||||
| Opt::ChunkFade
|
||||
| Opt::ImprovedTransparency
|
||||
| Opt::ChunkBuilder
|
||||
// Saved, but the creative tabs, the quit keys and the mouse
|
||||
// don't read them yet.
|
||||
| Opt::OperatorItemsTab
|
||||
// Saved, but the quit keys and the mouse don't read them yet.
|
||||
| Opt::QuitShortcuts
|
||||
| Opt::CtrlClickEmulatesRightClick
|
||||
)
|
||||
@@ -3336,6 +3334,16 @@ mod tests {
|
||||
assert!(loaded.toggle_attack && loaded.toggle_use);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_items_tab_option_can_be_switched() {
|
||||
// The creative screen reads it (CreativeModeInventoryScreen.hasPermissions).
|
||||
assert!(Opt::OperatorItemsTab.available());
|
||||
let mut s = Settings::default();
|
||||
assert!(!s.operator_items_tab);
|
||||
Opt::OperatorItemsTab.cycle(&mut s, false, [854, 480]);
|
||||
assert!(s.operator_items_tab);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_distance_slider_round_trips_through_extended_options_range() {
|
||||
let mut s = Settings::default();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! The creative inventory, for tools/test_creative_parity.py: loads an
|
||||
//! exported `creative-inventory.json` into `horizon_client::creative::Creative`
|
||||
//! and prints the tabs it shows (canonical stacks) and the search tab's
|
||||
//! results for every query of the vanilla oracle's output.
|
||||
//! results for every query of the vanilla oracle's output; with `permissions`,
|
||||
//! as an operator with the Operator Items Tab option on.
|
||||
//!
|
||||
//! usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT
|
||||
//! usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT [permissions]
|
||||
use horizon_client::{
|
||||
creative::{Creative, Names},
|
||||
item_data::canon_stack,
|
||||
@@ -13,11 +14,13 @@ use std::{fs, path::Path};
|
||||
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT";
|
||||
let usage =
|
||||
"usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT [permissions]";
|
||||
let names = Names::load(Path::new(args.get(1).ok_or(usage)?));
|
||||
let creative = Creative::from_json(&serde_json::from_slice(&fs::read(
|
||||
let mut creative = Creative::from_json(&serde_json::from_slice(&fs::read(
|
||||
args.get(2).ok_or(usage)?,
|
||||
)?)?)?;
|
||||
creative.set_permissions(args.get(5).is_some_and(|a| a == "permissions"), &names);
|
||||
let vanilla: Value = serde_json::from_slice(&fs::read(args.get(3).ok_or(usage)?)?)?;
|
||||
let tabs: Vec<Value> = creative
|
||||
.tabs
|
||||
|
||||
@@ -21,6 +21,30 @@ pub struct Tab {
|
||||
pub search: bool,
|
||||
pub inventory: bool,
|
||||
}
|
||||
impl Tab {
|
||||
/// The tab without its stacks.
|
||||
fn header(&self) -> Self {
|
||||
Self {
|
||||
id: self.id.clone(),
|
||||
title: self.title.clone(),
|
||||
icon: self.icon.clone(),
|
||||
items: Vec::new(),
|
||||
top: self.top,
|
||||
column: self.column,
|
||||
search: self.search,
|
||||
inventory: self.inventory,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A tab as exported, built with permissions: `operator` marks the stacks a
|
||||
/// build without them leaves out (the export's `permissions_only`).
|
||||
#[derive(Clone, Debug)]
|
||||
struct Exported {
|
||||
tab: Tab,
|
||||
/// `CreativeModeTab.Type.CATEGORY`: shown only with a stack (`shouldDisplay`).
|
||||
category: bool,
|
||||
operator: Vec<bool>,
|
||||
}
|
||||
/// What the client's creative search trees (SessionSearchTrees) index for one
|
||||
/// stack of the search tab: its creative tooltip lines and its item tags.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -34,6 +58,8 @@ struct SearchEntry {
|
||||
tags: Vec<(String, String)>,
|
||||
}
|
||||
pub struct Creative {
|
||||
/// The tabs shown (`CreativeModeTabs.tabs()`), with the stacks the current
|
||||
/// permissions put in them.
|
||||
pub tabs: Vec<Tab>,
|
||||
pub selected: usize,
|
||||
/// Scroll position 0..1 (scrollOffs).
|
||||
@@ -45,6 +71,13 @@ pub struct Creative {
|
||||
/// The search tab's stacks, in its display order.
|
||||
search_items: Vec<ItemStack>,
|
||||
search_index: Vec<SearchEntry>,
|
||||
/// Every exported tab, and the search tab's stacks with their entries and
|
||||
/// whether only permissions list them: `rebuild` keeps what is shown.
|
||||
exported: Vec<Exported>,
|
||||
exported_search: Vec<(ItemStack, SearchEntry, bool)>,
|
||||
/// The `hasPermissions` the tabs were last built with
|
||||
/// (`CreativeModeTabs.tryRebuildTabContents`).
|
||||
permissions: bool,
|
||||
/// ItemPickerMenu.items: what the grid shows, filled when the tab or the
|
||||
/// search changes (selectTab, refreshSearchResults), not on every frame.
|
||||
shown: Mutex<Option<Shown>>,
|
||||
@@ -199,14 +232,22 @@ impl Creative {
|
||||
}
|
||||
pub fn from_json(raw: &Value) -> Result<Self> {
|
||||
let mut tabs = Vec::new();
|
||||
let mut search_items = Vec::new();
|
||||
let mut search_index = Vec::new();
|
||||
let mut search = Vec::new();
|
||||
for t in raw["tabs"].as_array().into_iter().flatten() {
|
||||
// The stacks only a build with permissions lists, by index.
|
||||
let only: Vec<u64> = t["permissions_only"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_u64)
|
||||
.collect();
|
||||
let mut items = Vec::new();
|
||||
for v in t["items"].as_array().into_iter().flatten() {
|
||||
let mut operator = Vec::new();
|
||||
for (i, v) in t["items"].as_array().into_iter().flatten().enumerate() {
|
||||
let snbt = v.as_str().unwrap_or("-");
|
||||
if let Some(stack) = crate::item_data::parse_stack(snbt)? {
|
||||
items.push(stack);
|
||||
operator.push(only.contains(&(i as u64)));
|
||||
}
|
||||
}
|
||||
let kind = t["type"].as_str().unwrap_or("");
|
||||
@@ -220,33 +261,37 @@ impl Creative {
|
||||
};
|
||||
for (i, stack) in items.iter().enumerate() {
|
||||
let (namespace, path) = split_id(&java_lower(&stack.item));
|
||||
search_index.push(SearchEntry {
|
||||
let entry = SearchEntry {
|
||||
namespace,
|
||||
path,
|
||||
lines: strings(&t["lines"][i]),
|
||||
tags: strings(&t["tags"][i]).iter().map(|t| split_id(t)).collect(),
|
||||
});
|
||||
};
|
||||
search.push((stack.clone(), entry, operator[i]));
|
||||
}
|
||||
search_items = items.clone();
|
||||
}
|
||||
if kind == "HOTBAR" || t["visible"] == false {
|
||||
if kind == "HOTBAR" {
|
||||
continue;
|
||||
}
|
||||
let icon = t["icon"]
|
||||
.as_str()
|
||||
.and_then(|s| crate::item_data::parse_stack(s).ok().flatten());
|
||||
tabs.push(Tab {
|
||||
id: t["id"].as_str().unwrap_or("").to_owned(),
|
||||
title: t["title"].as_str().unwrap_or("").to_owned(),
|
||||
icon: icon.map_or_else(|| "minecraft:stone".to_owned(), |s| s.item.to_string()),
|
||||
items,
|
||||
top: t["row"] == "TOP",
|
||||
column: t["column"].as_u64().unwrap_or(0) as u32,
|
||||
search: kind == "SEARCH",
|
||||
inventory: kind == "INVENTORY",
|
||||
tabs.push(Exported {
|
||||
tab: Tab {
|
||||
id: t["id"].as_str().unwrap_or("").to_owned(),
|
||||
title: t["title"].as_str().unwrap_or("").to_owned(),
|
||||
icon: icon.map_or_else(|| "minecraft:stone".to_owned(), |s| s.item.to_string()),
|
||||
items,
|
||||
top: t["row"] == "TOP",
|
||||
column: t["column"].as_u64().unwrap_or(0) as u32,
|
||||
search: kind == "SEARCH",
|
||||
inventory: kind == "INVENTORY",
|
||||
},
|
||||
category: kind == "CATEGORY",
|
||||
operator,
|
||||
});
|
||||
}
|
||||
Ok(Self::new(tabs, search_items, search_index))
|
||||
Ok(Self::new(tabs, search))
|
||||
}
|
||||
/// The old export: item ids per tab, no components, tooltips or tags.
|
||||
fn from_legacy(raw: &Value) -> Result<Self> {
|
||||
@@ -268,33 +313,38 @@ impl Creative {
|
||||
}
|
||||
}
|
||||
let kind = t["type"].as_str().unwrap_or("");
|
||||
tabs.push(Tab {
|
||||
id: t["id"].as_str().unwrap_or("").to_owned(),
|
||||
title: t["title"].as_str().unwrap_or("").to_owned(),
|
||||
icon: t["icon"].as_str().unwrap_or("minecraft:stone").to_owned(),
|
||||
items,
|
||||
top: t["row"] == "TOP",
|
||||
column: t["column"].as_u64().unwrap_or(0) as u32,
|
||||
search: kind == "SEARCH",
|
||||
inventory: kind == "INVENTORY",
|
||||
tabs.push(Exported {
|
||||
operator: vec![false; items.len()],
|
||||
tab: Tab {
|
||||
id: t["id"].as_str().unwrap_or("").to_owned(),
|
||||
title: t["title"].as_str().unwrap_or("").to_owned(),
|
||||
icon: t["icon"].as_str().unwrap_or("minecraft:stone").to_owned(),
|
||||
items,
|
||||
top: t["row"] == "TOP",
|
||||
column: t["column"].as_u64().unwrap_or(0) as u32,
|
||||
search: kind == "SEARCH",
|
||||
inventory: kind == "INVENTORY",
|
||||
},
|
||||
category: false,
|
||||
});
|
||||
}
|
||||
let search_index = search_items
|
||||
.iter()
|
||||
let search = search_items
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let (namespace, path) = split_id(&java_lower(&s.item));
|
||||
SearchEntry {
|
||||
let entry = SearchEntry {
|
||||
namespace,
|
||||
path,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
(s, entry, false)
|
||||
})
|
||||
.collect();
|
||||
Ok(Self::new(tabs, search_items, search_index))
|
||||
Ok(Self::new(tabs, search))
|
||||
}
|
||||
fn new(tabs: Vec<Tab>, search_items: Vec<ItemStack>, search_index: Vec<SearchEntry>) -> Self {
|
||||
Self {
|
||||
tabs,
|
||||
fn new(exported: Vec<Exported>, exported_search: Vec<(ItemStack, SearchEntry, bool)>) -> Self {
|
||||
let mut creative = Self {
|
||||
tabs: Vec::new(),
|
||||
selected: 0,
|
||||
scroll: 0.0,
|
||||
scrolling: false,
|
||||
@@ -305,10 +355,81 @@ impl Creative {
|
||||
search.text_color = u32::MAX;
|
||||
search
|
||||
},
|
||||
search_items,
|
||||
search_index,
|
||||
search_items: Vec::new(),
|
||||
search_index: Vec::new(),
|
||||
exported,
|
||||
exported_search,
|
||||
permissions: false,
|
||||
shown: Mutex::new(None),
|
||||
};
|
||||
creative.rebuild();
|
||||
creative
|
||||
}
|
||||
/// `CreativeModeTabs.buildAllTabContents` with `permissions`: the operator
|
||||
/// tab's display items generator lists its stacks only with them
|
||||
/// (`hasPermissions`), and the search tab with it; a category tab is
|
||||
/// shown with a stack (`CreativeModeTab.shouldDisplay`).
|
||||
fn rebuild(&mut self) {
|
||||
let permissions = self.permissions;
|
||||
let keep = |operator: bool| permissions || !operator;
|
||||
self.tabs = self
|
||||
.exported
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
let items: Vec<ItemStack> = (e.tab.items.iter().zip(&e.operator))
|
||||
.filter(|(_, o)| keep(**o))
|
||||
.map(|(s, _)| s.clone())
|
||||
.collect();
|
||||
(!e.category || !items.is_empty()).then(|| Tab {
|
||||
items,
|
||||
..e.tab.header()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
(self.search_items, self.search_index) = self
|
||||
.exported_search
|
||||
.iter()
|
||||
.filter(|(_, _, o)| keep(*o))
|
||||
.map(|(s, e, _)| (s.clone(), e.clone()))
|
||||
.unzip();
|
||||
self.forget_items();
|
||||
}
|
||||
/// `CreativeModeInventoryScreen.tryRefreshInvalidatedTabs`, which its
|
||||
/// constructor and `containerTick` run with `hasPermissions`
|
||||
/// (`Player.canUseGameMasterBlocks` and the Operator Items Tab option):
|
||||
/// when that changed, the tabs are built again. A selected tab left
|
||||
/// without stacks (the operator tab) gives way to the default tab; any
|
||||
/// other keeps its first row (`refreshCurrentTabContents`). True when the
|
||||
/// tabs were rebuilt.
|
||||
pub fn set_permissions(&mut self, permissions: bool, names: &Names) -> bool {
|
||||
if permissions == self.permissions {
|
||||
return false;
|
||||
}
|
||||
let selected = self.tab().id.clone();
|
||||
let row = self.first_row(names);
|
||||
self.permissions = permissions;
|
||||
self.rebuild();
|
||||
match self.tabs.iter().position(|t| t.id == selected) {
|
||||
Some(tab) => {
|
||||
self.selected = tab;
|
||||
// ItemPickerMenu.getScrollForRowIndex.
|
||||
let hidden = self.hidden_rows(names);
|
||||
self.scroll = if hidden > 0 {
|
||||
(row as f32 / hidden as f32).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
None => {
|
||||
// CreativeModeTabs.getDefaultTab.
|
||||
let default = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|t| t.id == "minecraft:building_blocks");
|
||||
self.select(default.unwrap_or(0));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Where a stack comes in the creative tabs (the search tab lists them
|
||||
/// all, in tab order): the same stack's place, else its item's first.
|
||||
@@ -322,19 +443,7 @@ impl Creative {
|
||||
}
|
||||
/// The tabs without their stacks: what the screen draws of them each frame.
|
||||
pub fn tab_headers(&self) -> Vec<Tab> {
|
||||
self.tabs
|
||||
.iter()
|
||||
.map(|t| Tab {
|
||||
id: t.id.clone(),
|
||||
title: t.title.clone(),
|
||||
icon: t.icon.clone(),
|
||||
items: Vec::new(),
|
||||
top: t.top,
|
||||
column: t.column,
|
||||
search: t.search,
|
||||
inventory: t.inventory,
|
||||
})
|
||||
.collect()
|
||||
self.tabs.iter().map(Tab::header).collect()
|
||||
}
|
||||
pub fn tab(&self) -> &Tab {
|
||||
&self.tabs[self.selected.min(self.tabs.len() - 1)]
|
||||
@@ -578,7 +687,7 @@ impl Creative {
|
||||
pub fn tab_position(tab: &Tab) -> (f32, f32) {
|
||||
let mut x = tab.column as f32 * 27.0;
|
||||
if tab.column >= 5 {
|
||||
// Right-aligned tabs (search, inventory).
|
||||
// Right-aligned tabs (search, operator, inventory).
|
||||
x = Self::WIDTH - 27.0 * (7 - tab.column) as f32 + 1.0;
|
||||
}
|
||||
let y = if tab.top { -28.0 } else { Self::HEIGHT - 4.0 };
|
||||
@@ -1047,6 +1156,84 @@ mod tests {
|
||||
assert_eq!(c.search.value, "");
|
||||
}
|
||||
|
||||
/// An export built with permissions: the operator tab's stacks, and their
|
||||
/// place at the end of the search tab, only with them.
|
||||
fn with_operator_tab() -> Creative {
|
||||
let stack = |id: &str| format!("{{count:1,id:\"minecraft:{id}\"}}");
|
||||
let raw = serde_json::json!({"schema": 3, "tabs": [
|
||||
{"id": "minecraft:building_blocks", "type": "CATEGORY", "row": "TOP", "column": 0, "title": "Building Blocks",
|
||||
"icon": stack("bricks"), "visible": true, "items": [stack("stone")]},
|
||||
{"id": "minecraft:op_blocks", "type": "CATEGORY", "row": "BOTTOM", "column": 5, "title": "Operator Utilities",
|
||||
"icon": stack("command_block"), "visible": false,
|
||||
"items": [stack("command_block"), stack("barrier")], "permissions_only": [0, 1]},
|
||||
{"id": "minecraft:search", "type": "SEARCH", "row": "TOP", "column": 6, "title": "Search Items",
|
||||
"icon": stack("compass"), "visible": true,
|
||||
"items": [stack("stone"), stack("command_block"), stack("barrier")],
|
||||
"lines": [["Stone"], ["Command Block"], ["Barrier"]], "tags": [[], [], []],
|
||||
"permissions_only": [1, 2]},
|
||||
{"id": "minecraft:inventory", "type": "INVENTORY", "row": "BOTTOM", "column": 6, "title": "Survival Inventory",
|
||||
"icon": stack("chest"), "visible": true, "items": []}
|
||||
]});
|
||||
Creative::from_json(&raw).unwrap()
|
||||
}
|
||||
|
||||
fn tab_ids(c: &Creative) -> Vec<&str> {
|
||||
c.tabs.iter().map(|t| t.id.as_str()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_stacks_stay_out_without_permissions() {
|
||||
let c = with_operator_tab();
|
||||
let names = Names::default();
|
||||
assert_eq!(
|
||||
tab_ids(&c),
|
||||
[
|
||||
"minecraft:building_blocks",
|
||||
"minecraft:search",
|
||||
"minecraft:inventory"
|
||||
]
|
||||
);
|
||||
assert_eq!(c.search_items().len(), 1);
|
||||
assert!(c.search_results("command", &names).is_empty());
|
||||
assert_eq!(c.search_results("", &names), [0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operator_tab_comes_and_goes_with_permissions() {
|
||||
let mut c = with_operator_tab();
|
||||
let names = Names::default();
|
||||
assert!(!c.set_permissions(false, &names));
|
||||
assert!(c.set_permissions(true, &names));
|
||||
assert!(!c.set_permissions(true, &names));
|
||||
assert_eq!(
|
||||
tab_ids(&c),
|
||||
[
|
||||
"minecraft:building_blocks",
|
||||
"minecraft:op_blocks",
|
||||
"minecraft:search",
|
||||
"minecraft:inventory"
|
||||
]
|
||||
);
|
||||
let op: Vec<&str> = c.tabs[1].items.iter().map(|s| s.item.as_ref()).collect();
|
||||
assert_eq!(op, ["minecraft:command_block", "minecraft:barrier"]);
|
||||
assert_eq!(c.search_results("command", &names), [1]);
|
||||
assert_eq!(c.search_results("", &names), [0, 1, 2]);
|
||||
// The search tab keeps its place across the rebuild.
|
||||
c.select(2);
|
||||
c.search.set_value("block");
|
||||
assert!(c.set_permissions(false, &names));
|
||||
assert!(c.tab().search);
|
||||
assert_eq!(c.search.value, "block");
|
||||
assert!(c.items(&names).is_empty());
|
||||
// The selected operator tab is left empty: the default tab instead.
|
||||
c.set_permissions(true, &names);
|
||||
c.select(1);
|
||||
assert_eq!(c.items(&names).len(), 2);
|
||||
c.set_permissions(false, &names);
|
||||
assert_eq!(c.tab().id, "minecraft:building_blocks");
|
||||
assert_eq!(c.items(&names)[0].item.as_ref(), "minecraft:stone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_matches_vanilla_trees() {
|
||||
let c = creative(0);
|
||||
|
||||
@@ -5291,6 +5291,9 @@ impl Game<'_> {
|
||||
self.tick_debug_keys()?;
|
||||
self.tick_debug_charts();
|
||||
self.tick_tutorial();
|
||||
if self.screen == Screen::Creative {
|
||||
self.refresh_creative_tabs();
|
||||
}
|
||||
if self.remote.is_some() {
|
||||
return self.tick_remote();
|
||||
}
|
||||
@@ -6519,6 +6522,14 @@ impl Game<'_> {
|
||||
}
|
||||
batch
|
||||
}
|
||||
/// `CreativeModeInventoryScreen.hasPermissions` (`canUseGameMasterBlocks`
|
||||
/// and the Operator Items Tab option) given to `tryRefreshInvalidatedTabs`,
|
||||
/// as the screen's constructor and `containerTick` do.
|
||||
fn refresh_creative_tabs(&mut self) {
|
||||
let permissions = self.can_use_game_master_blocks() && self.settings.operator_items_tab;
|
||||
self.creative_screen
|
||||
.set_permissions(permissions, &self.pack.names);
|
||||
}
|
||||
fn creative_inventory_open(&self) -> bool {
|
||||
self.screen == Screen::Creative && self.creative_screen.tab().inventory
|
||||
}
|
||||
@@ -12661,6 +12672,7 @@ impl Game<'_> {
|
||||
}
|
||||
if self.creative {
|
||||
self.screen = Screen::Creative;
|
||||
self.refresh_creative_tabs();
|
||||
self.creative_screen.reopen();
|
||||
} else {
|
||||
self.screen = Screen::Inventory;
|
||||
|
||||
@@ -5844,6 +5844,7 @@ impl Game<'_> {
|
||||
self.recipe_ui.reset_screen();
|
||||
if self.creative {
|
||||
self.screen = Screen::Creative;
|
||||
self.refresh_creative_tabs();
|
||||
self.creative_screen.reopen();
|
||||
} else {
|
||||
self.screen = Screen::Inventory;
|
||||
|
||||
+8
-3
@@ -1527,15 +1527,20 @@ suggestion list and clipboard keys.
|
||||
vanilla's own `CreativeModeTabs.tryRebuildTabContents` over the vanilla data
|
||||
pack's registries: every stack with its components, and for each stack of the
|
||||
search tab the creative tooltip lines and item tags that the client's search
|
||||
trees index. The search ports `refreshSearchResults` and `SessionSearchTrees`
|
||||
trees index. The tabs are built with permissions, and the export marks the
|
||||
stacks only that build lists (`permissions_only`): the operator tab and its
|
||||
stacks at the end of the search tab, shown while the player
|
||||
`canUseGameMasterBlocks` with the Operator Items Tab option on
|
||||
(`CreativeModeInventoryScreen.hasPermissions`, checked on opening and every
|
||||
tick as `tryRefreshInvalidatedTabs` does). The search ports `refreshSearchResults` and `SessionSearchTrees`
|
||||
(substring matches over tooltip lines; after a colon the item id's namespace,
|
||||
then its path or a line; tags after `#`), and the scrollbar follows
|
||||
`mouseClicked`/`mouseDragged`/`mouseScrolled`. `tools/test_creative_parity.py`
|
||||
compares every shown tab's stacks, the search tab and 6,793 searches (fixed
|
||||
queries, then every stack's item path and tooltip lines and every tag, whole
|
||||
and halved) with the vanilla client's own search trees
|
||||
(`VanillaCreativeSearchOracle`). Not compared: the operator tab (off by
|
||||
default, `operatorItemsTab`) and the saved hotbars tab.
|
||||
(`VanillaCreativeSearchOracle`), then again with permissions (13 tabs, 2,034
|
||||
search stacks, 6,845 searches). Not compared: the saved hotbars tab.
|
||||
|
||||
## Pillager patrols and raids
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import net.minecraft.world.item.ItemStack;
|
||||
* (FullTextSearchTree over creative tooltip lines and item ids, IdSearchTree
|
||||
* over item tags after "#").
|
||||
*
|
||||
* VanillaCreativeSearchOracle QUERIES.json EXPORT.json OUT.json
|
||||
* VanillaCreativeSearchOracle QUERIES.json EXPORT.json OUT.json [permissions]
|
||||
*
|
||||
* With "permissions", the screen's tabs are built as for an operator in creative
|
||||
* with the Operator Items Tab option on (CreativeModeInventoryScreen.hasPermissions).
|
||||
*
|
||||
* The queries are the file's list, then every search stack's item path and
|
||||
* tooltip lines and every item tag, each also as a prefix and with "#".
|
||||
@@ -29,11 +32,12 @@ public final class VanillaCreativeSearchOracle {
|
||||
public static void main(String[] args) throws Exception {
|
||||
ParityData.init();
|
||||
var lookup = ParityData.lookup;
|
||||
boolean hasPermissions = args.length > 3 && args[3].equals("permissions");
|
||||
Files.writeString(Path.of(args[1]),
|
||||
VanillaCreativeTabsExporter.export(FeatureFlags.DEFAULT_FLAGS, VanillaCreativeTabsExporter.HAS_PERMISSIONS, lookup).toString(),
|
||||
VanillaCreativeTabsExporter.export(FeatureFlags.DEFAULT_FLAGS, lookup).toString(),
|
||||
StandardCharsets.UTF_8);
|
||||
// CreativeModeInventoryScreen.tryRebuildTabContents, as the screen calls it.
|
||||
CreativeModeTabs.tryRebuildTabContents(FeatureFlags.DEFAULT_FLAGS, VanillaCreativeTabsExporter.HAS_PERMISSIONS, lookup);
|
||||
CreativeModeTabs.tryRebuildTabContents(FeatureFlags.DEFAULT_FLAGS, hasPermissions, lookup);
|
||||
List<ItemStack> searchItems = List.copyOf(CreativeModeTabs.searchTab().getDisplayItems());
|
||||
var trees = new SessionSearchTrees();
|
||||
trees.updateCreativeTooltips(lookup, searchItems);
|
||||
|
||||
@@ -3,6 +3,9 @@ package dev.horizonlab.parity;
|
||||
import com.google.gson.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@@ -24,20 +27,30 @@ import net.minecraft.world.item.TooltipFlag;
|
||||
* of the search tab the inputs of the client's creative search trees
|
||||
* (SessionSearchTrees): its creative tooltip lines and its item tags.
|
||||
*
|
||||
* The tabs are built with permissions (CreativeModeInventoryScreen.hasPermissions:
|
||||
* canUseGameMasterBlocks and the operatorItemsTab option), so the operator tab and
|
||||
* its stacks in the search tab are listed; "permissions_only" gives the indices of
|
||||
* the stacks a build without permissions leaves out, and "visible" is
|
||||
* shouldDisplay without them.
|
||||
*
|
||||
* VanillaCreativeTabsExporter OUT.json
|
||||
*/
|
||||
public final class VanillaCreativeTabsExporter {
|
||||
/** The operator tab and its items need permissions and the operatorItemsTab option (off by default). */
|
||||
public static final boolean HAS_PERMISSIONS = false;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ParityData.init();
|
||||
Files.writeString(Path.of(args[0]), export(FeatureFlags.DEFAULT_FLAGS, HAS_PERMISSIONS, ParityData.lookup).toString(),
|
||||
Files.writeString(Path.of(args[0]), export(FeatureFlags.DEFAULT_FLAGS, ParityData.lookup).toString(),
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static JsonObject export(FeatureFlagSet features, boolean hasPermissions, HolderLookup.Provider lookup) {
|
||||
CreativeModeTabs.tryRebuildTabContents(features, hasPermissions, lookup);
|
||||
public static JsonObject export(FeatureFlagSet features, HolderLookup.Provider lookup) {
|
||||
CreativeModeTabs.tryRebuildTabContents(features, false, lookup);
|
||||
var without = new HashMap<CreativeModeTab, List<String>>();
|
||||
var visible = new HashMap<CreativeModeTab, Boolean>();
|
||||
for (CreativeModeTab tab : BuiltInRegistries.CREATIVE_MODE_TAB) {
|
||||
without.put(tab, tab.getDisplayItems().stream().map(VanillaCreativeTabsExporter::snbt).toList());
|
||||
visible.put(tab, tab.shouldDisplay());
|
||||
}
|
||||
CreativeModeTabs.tryRebuildTabContents(features, true, lookup);
|
||||
var tabs = new JsonArray();
|
||||
for (CreativeModeTab tab : BuiltInRegistries.CREATIVE_MODE_TAB) {
|
||||
var o = new JsonObject();
|
||||
@@ -48,10 +61,21 @@ public final class VanillaCreativeTabsExporter {
|
||||
o.addProperty("column", tab.column());
|
||||
o.addProperty("title", tab.getDisplayName().getString());
|
||||
o.addProperty("icon", snbt(tab.getIconItem()));
|
||||
o.addProperty("visible", tab.shouldDisplay());
|
||||
o.addProperty("visible", visible.get(tab));
|
||||
var items = new JsonArray();
|
||||
for (ItemStack stack : tab.getDisplayItems()) items.add(snbt(stack));
|
||||
var kept = new ArrayList<String>();
|
||||
var permissionsOnly = new JsonArray();
|
||||
var base = new HashSet<>(without.get(tab));
|
||||
for (ItemStack stack : tab.getDisplayItems()) {
|
||||
String s = snbt(stack);
|
||||
if (base.contains(s)) kept.add(s);
|
||||
else permissionsOnly.add(items.size());
|
||||
items.add(s);
|
||||
}
|
||||
// The client leaves these indices out without permissions: what remains must be that build, in order.
|
||||
if (!kept.equals(without.get(tab))) throw new IllegalStateException("permissions reorder " + key);
|
||||
o.add("items", items);
|
||||
if (!permissionsOnly.isEmpty()) o.add("permissions_only", permissionsOnly);
|
||||
if (tab.getType() == CreativeModeTab.Type.SEARCH) {
|
||||
var context = Item.TooltipContext.of(lookup);
|
||||
var flag = TooltipFlag.Default.NORMAL.asCreative();
|
||||
@@ -67,8 +91,7 @@ public final class VanillaCreativeTabsExporter {
|
||||
tabs.add(o);
|
||||
}
|
||||
var out = new JsonObject();
|
||||
out.addProperty("schema", 2);
|
||||
out.addProperty("permissions", hasPermissions);
|
||||
out.addProperty("schema", 3);
|
||||
out.add("tabs", tabs);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ horizon_client::creative::Creative. The tabs it shows (id, title, icon,
|
||||
position, every stack with its components), the search tab's stacks and the
|
||||
results of every query must agree. The queries are QUERIES below, then every
|
||||
search stack's item path and tooltip lines, every item tag with and without
|
||||
"#", and half of each as a prefix.
|
||||
"#", and half of each as a prefix. Both run twice: as a player without
|
||||
permissions, then as an operator with the Operator Items Tab option on
|
||||
(CreativeModeInventoryScreen.hasPermissions), where the operator tab and its
|
||||
stacks in the search tab appear.
|
||||
|
||||
test_creative_parity.py [--detail N]
|
||||
"""
|
||||
@@ -63,12 +66,24 @@ def main():
|
||||
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-nowarn', '-cp', dependencies, '-d', classes,
|
||||
*(PARITY / f'{n}.java' for n in ('ParityData', 'VanillaCreativeTabsExporter', 'VanillaCreativeSearchOracle'))],
|
||||
'compile-java')
|
||||
bad = 0
|
||||
for mode in ('', 'permissions'):
|
||||
print(f'== {mode or "no permissions"}')
|
||||
bad += compare(args, build, classes, dependencies, queries, run, mode)
|
||||
if bad:
|
||||
sys.exit(1)
|
||||
print('creative parity ok')
|
||||
|
||||
|
||||
def compare(args, build, classes, dependencies, queries, run, mode):
|
||||
suffix = '-' + mode if mode else ''
|
||||
run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaCreativeSearchOracle',
|
||||
queries, build / 'creative-inventory.json', build / 'vanilla.json'], 'vanilla')
|
||||
queries, build / 'creative-inventory.json', build / f'vanilla{suffix}.json', *([mode] if mode else [])],
|
||||
'vanilla' + suffix)
|
||||
lib.cargo_example('horizon-client', 'creative_search_fixture', lib.RESOURCES, build / 'creative-inventory.json',
|
||||
build / 'vanilla.json', build / 'rust.json')
|
||||
vanilla = json.loads((build / 'vanilla.json').read_text())
|
||||
rust = json.loads((build / 'rust.json').read_text())
|
||||
build / f'vanilla{suffix}.json', build / f'rust{suffix}.json', *([mode] if mode else []))
|
||||
vanilla = json.loads((build / f'vanilla{suffix}.json').read_text())
|
||||
rust = json.loads((build / f'rust{suffix}.json').read_text())
|
||||
bad = 0
|
||||
shown = [{k: t[k] for k in ('id', 'title', 'icon', 'row', 'column', 'items')} for t in vanilla['tabs'] if t['shown']]
|
||||
if [t['id'] for t in shown] != [t['id'] for t in rust['tabs']]:
|
||||
@@ -101,9 +116,7 @@ def main():
|
||||
for i in hits[:6]))
|
||||
print(f'{len(vanilla["searches"]) - len(wrong)}/{len(vanilla["searches"])} searches match; {len(shown)} tabs, '
|
||||
f'{len(search)} search stacks')
|
||||
if bad:
|
||||
sys.exit(1)
|
||||
print('creative parity ok')
|
||||
return bad
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user