mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
feat(mods): draw item stacks on screens
Co-Authored-By: Codex gpt-6.1-terra <[email protected]>
This commit is contained in:
1 parent
ce8d86cb23
commit
ff5551084a
9 files changed
+126
-2
No files matched your search
@@ -450,4 +450,7 @@ impl Host for super::Game<'_> {
|
||||
) -> u32 {
|
||||
self.native_move_items(dimension, from, from_face, to, to_face, item, max)
|
||||
}
|
||||
fn item_stack(&self, batch: &mut HudBatch, stack: &ItemStack, at: [f32; 2], size: f32) -> bool {
|
||||
self.native_item_stack(batch, stack, at, size)
|
||||
}
|
||||
}
|
||||
@@ -6932,6 +6932,24 @@ impl Game<'_> {
|
||||
batch.scale_since(start, at[0], at[1], size / 16.0, size / 16.0);
|
||||
true
|
||||
}
|
||||
/// Draw a stack and its slot decorations, except the count, at `at`,
|
||||
/// `size` GUI pixels square. False for an unknown item. API @API:item_stack@.
|
||||
pub(super) fn native_item_stack(
|
||||
&self,
|
||||
batch: &mut HudBatch,
|
||||
stack: &ItemStack,
|
||||
at: [f32; 2],
|
||||
size: f32,
|
||||
) -> bool {
|
||||
if self.res.items().get(&stack.item).is_none() {
|
||||
return false;
|
||||
}
|
||||
let start = batch.vertices.len();
|
||||
self.draw_item_model(batch, stack, at[0], at[1]);
|
||||
self.draw_item_decorations(batch, stack, at[0], at[1], Some(String::new()));
|
||||
batch.scale_since(start, at[0], at[1], size / 16.0, size / 16.0);
|
||||
true
|
||||
}
|
||||
/// Item `item`'s icon drawn with model `model` (`minecraft:paper` as
|
||||
/// `mymod:wheel_icon`, an `item_model` of a content mod's), as `native_item`
|
||||
/// draws one; false for an unknown item. API 111.
|
||||
|
||||
@@ -664,4 +664,7 @@ pub trait Host {
|
||||
item: Option<&str>,
|
||||
max: u32,
|
||||
) -> u32;
|
||||
/// Draw a stack and its slot decorations, except the count, at `at`,
|
||||
/// `size` GUI pixels square. False for an unknown item. API @API:item_stack@.
|
||||
fn item_stack(&self, batch: &mut HudBatch, stack: &ItemStack, at: [f32; 2], size: f32) -> bool;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Exercise the screen stack helper without a game window."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_world_input_capture import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NativeItemStackTests(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("rustc"), "requires Rust")
|
||||
def test_stack_and_decorations_are_forwarded_and_scaled(self):
|
||||
source = (ROOT / "crates/horizon-client/src/game/native_hooks.rs").read_text(encoding="utf-8")
|
||||
actual = method(source.replace("pub(super) fn", "fn"), "native_item_stack")
|
||||
fixture = r"""
|
||||
struct ItemStack { item: String, count: u32, damage: u32 }
|
||||
struct Items;
|
||||
impl Items { fn get(&self, id: &str) -> Option<()> { (id == "known").then_some(()) } }
|
||||
struct Resources;
|
||||
impl Resources { fn items(&self) -> Items { Items } }
|
||||
struct HudBatch { vertices: Vec<u32>, scaled: Option<(usize, f32, f32, f32, f32)> }
|
||||
impl HudBatch {
|
||||
fn scale_since(&mut self, start: usize, x: f32, y: f32, sx: f32, sy: f32) {
|
||||
self.scaled = Some((start, x, y, sx, sy));
|
||||
}
|
||||
}
|
||||
struct Game { res: Resources }
|
||||
impl Game {
|
||||
fn draw_item_model(&self, batch: &mut HudBatch, stack: &ItemStack, x: f32, y: f32) {
|
||||
assert_eq!((stack.count, stack.damage, x, y), (42, 13, 2.0, 3.0));
|
||||
batch.vertices.push(1);
|
||||
}
|
||||
fn draw_item_decorations(&self, batch: &mut HudBatch, stack: &ItemStack,
|
||||
x: f32, y: f32, count: Option<String>) {
|
||||
assert_eq!((stack.count, stack.damage, x, y, count.as_deref()), (42, 13, 2.0, 3.0, Some("")));
|
||||
batch.vertices.push(2);
|
||||
}
|
||||
METHOD
|
||||
}
|
||||
#[test]
|
||||
fn stack_and_unknown_item() {
|
||||
let game = Game { res: Resources };
|
||||
let mut batch = HudBatch { vertices: vec![0], scaled: None };
|
||||
let mut stack = ItemStack { item: "unknown".into(), count: 42, damage: 13 };
|
||||
assert!(!game.native_item_stack(&mut batch, &stack, [2.0, 3.0], 32.0));
|
||||
assert_eq!(batch.vertices, vec![0]);
|
||||
assert_eq!(batch.scaled, None);
|
||||
stack.item = "known".into();
|
||||
assert!(game.native_item_stack(&mut batch, &stack, [2.0, 3.0], 32.0));
|
||||
assert_eq!(batch.vertices, vec![0, 1, 2]);
|
||||
assert_eq!(batch.scaled, Some((1, 2.0, 3.0, 2.0, 2.0)));
|
||||
}
|
||||
""".replace("METHOD", actual)
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
src, exe = Path(folder) / "item.rs", Path(folder) / "item.exe"
|
||||
src.write_text(fixture, encoding="utf-8")
|
||||
subprocess.run(["rustc", "--edition=2024", "--test", str(src), "-o", str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -79,10 +79,12 @@ class StampTests(unittest.TestCase):
|
||||
path = self.fragment("helper")
|
||||
fragment = json.loads(path.read_text())
|
||||
fragment["hooks"] = []
|
||||
fragment["helpers"] = [{"name": "helper", "target": "target::helper"}]
|
||||
path.write_text(json.dumps(fragment))
|
||||
self.assertFalse(stamp.stamp(False))
|
||||
self.assertIn("NATIVE_HOOKS_API: u32 = 8;", (self.root / stamp.SOURCE).read_text())
|
||||
self.assertEqual(json.loads((self.root / stamp.CATALOG).read_text())["hooks"], [])
|
||||
self.assertEqual(json.loads((self.root / stamp.CATALOG).read_text())["helpers"], fragment["helpers"])
|
||||
|
||||
def test_catalog_facts_travel_with_the_hook(self):
|
||||
self.git("add", ".")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A hook PR adds a uniquely named JSON file here instead of editing the API number,
|
||||
the API histories or `native-hooks.json`. Helper-only additions use an empty
|
||||
`hooks` array. Example:
|
||||
`hooks` array and list new Host helpers in `helpers` as catalog entries. Example:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{"name": "item_stack", "target": "horizon_client::game::native_hooks::Game<'_>::native_item_stack"}
|
||||
],
|
||||
"rust_summary": "drawing a component-bearing item stack and its decorations on a screen (`native_item_stack`).",
|
||||
"doc_summary": "drawing an item stack on a screen: `native_item_stack(batch, stack, at, size)` draws its model, glint, damage bar, cooldown and mod decorations, but not its count. It returns false for an unknown item. Bundles need \"hooks\": @API:item_stack@."
|
||||
}
|
||||
@@ -50,6 +50,7 @@ def stamp(check):
|
||||
raise ValueError("missing numeric NATIVE_HOOKS_API")
|
||||
version = int(match.group(1))
|
||||
names = {hook["name"] for hook in catalog["hooks"]}
|
||||
helper_names = {helper["name"] for helper in catalog.get("helpers", [])}
|
||||
extra = {}
|
||||
for path in ordered:
|
||||
fragment = json.loads(path.read_text())
|
||||
@@ -63,6 +64,11 @@ def stamp(check):
|
||||
# The catalog's optional facts about a hook travel with it.
|
||||
entry.update({k: hook[k] for k in ("neoforge", "side", "cancellable", "mod_api") if k in hook})
|
||||
catalog["hooks"].append(entry)
|
||||
for helper in fragment.get("helpers", []):
|
||||
if helper["name"] in helper_names:
|
||||
raise ValueError(f"duplicate helper: {helper['name']}")
|
||||
helper_names.add(helper["name"])
|
||||
catalog.setdefault("helpers", []).append(helper)
|
||||
rust_summary = fragment["rust_summary"].replace(marker, str(version))
|
||||
doc_summary = fragment["doc_summary"].replace(marker, str(version))
|
||||
if not rust_summary or not doc_summary:
|
||||
|
||||
+24
-1
@@ -880,10 +880,33 @@ fn generate(root: &Path, check: bool) -> Result<(), String> {
|
||||
let mut host_traits = String::new();
|
||||
let mut host_impl = String::new();
|
||||
let mut names = HashSet::new();
|
||||
for helper in catalog["helpers"]
|
||||
let mut helpers = catalog["helpers"]
|
||||
.as_array()
|
||||
.ok_or("catalog helpers must be an array")?
|
||||
.clone();
|
||||
let mut fragments = fs::read_dir(root.join("tools/native-hook-fragments"))
|
||||
.map_err(|e| e.to_string())?
|
||||
.map(|entry| entry.map(|entry| entry.path()).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
fragments.sort();
|
||||
for path in fragments
|
||||
.into_iter()
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
|
||||
{
|
||||
let fragment: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&path).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
if let Some(pending) = fragment.get("helpers") {
|
||||
helpers.extend(
|
||||
pending
|
||||
.as_array()
|
||||
.ok_or("fragment helpers must be an array")?
|
||||
.iter()
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for helper in &helpers {
|
||||
let name = helper["name"].as_str().ok_or("helper name missing")?;
|
||||
if !names.insert(name) {
|
||||
return Err(format!("duplicate helper {name}"));
|
||||
|
||||
Reference in new issue
Block a user