Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions tools/test-tiling.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,23 @@ function loadRules() {
{ pattern: "*_side.svg", category: "x-only" },
{ pattern: "*_overlay.svg", category: "exempt" },
{ pattern: "short_grass*.svg", category: "exempt" },
{ pattern: "tall_grass_*.svg", category: "exempt" }
{ pattern: "tall_grass_*.svg", category: "exempt" },
{ pattern: "grass.svg", category: "exempt" }
],
itemIds: [
"cooked_beef", "golden_apple", "compass_nexus", "plot_compass", "spiral_core", "ninja6_token",
"wooden_sword", "wooden_pickaxe", "wooden_axe", "wooden_shovel", "wooden_hoe",
"stone_sword", "stone_pickaxe", "stone_axe", "stone_shovel", "stone_hoe",
"iron_sword", "iron_pickaxe", "iron_axe", "iron_shovel", "iron_hoe",
"golden_sword", "golden_pickaxe", "golden_axe", "golden_shovel", "golden_hoe",
"diamond_sword", "diamond_pickaxe", "diamond_axe", "diamond_shovel", "diamond_hoe",
"netherite_sword", "netherite_pickaxe", "netherite_axe", "netherite_shovel", "netherite_hoe",
"bow", "bow_pulling_0", "bow_pulling_1", "bow_pulling_2",
"crossbow_standby", "crossbow_pulling_0", "crossbow_pulling_1", "crossbow_pulling_2",
"crossbow_arrow", "crossbow_firework",
"shield_base", "shield_base_nopattern", "trident", "mace",
"golden_carrot", "baked_potato", "bread", "apple", "cooked_porkchop"
],
itemIds: ["cooked_beef", "golden_apple", "compass_nexus", "plot_compass", "spiral_core", "ninja6_token"],
overrides: {}
};
}
Expand Down Expand Up @@ -100,7 +114,10 @@ export function categorizeTexture(filename, rules, axisOverride = null) {
// this function is exported and the CLI's `--texture` path can carry a directory, so
// normalize once here rather than trusting every caller to have stripped it.
const base = basenameOf(filename);
const stem = base.replace(/\.svg$/i, "");
// Patterns are written against `<stem>.svg`, so a `.png`, upper-case or extensionless
// id is rebuilt from its stem and resolves exactly as its `.svg` master does.
const stem = base.replace(/\.(svg|png)$/i, "");
const nameWithExt = stem + ".svg";

if (axisOverride) {
const axes = axisOverride.toLowerCase() === "x" ? ["x"] :
Expand All @@ -115,8 +132,8 @@ export function categorizeTexture(filename, rules, axisOverride = null) {
}

// 1. Explicit overrides
if (rules.overrides && (rules.overrides[filename] || rules.overrides[base])) {
const ovr = rules.overrides[filename] || rules.overrides[base];
if (rules.overrides && (rules.overrides[filename] || rules.overrides[base] || rules.overrides[nameWithExt] || rules.overrides[stem])) {
const ovr = rules.overrides[filename] || rules.overrides[base] || rules.overrides[nameWithExt] || rules.overrides[stem];
const catConfig = rules.categories[ovr.category] || { testAxes: ["x", "y"], tolerance: 0 };
return {
category: ovr.category,
Expand All @@ -139,7 +156,7 @@ export function categorizeTexture(filename, rules, axisOverride = null) {
// 3. Glob patterns
if (rules.patterns) {
for (const pat of rules.patterns) {
if (matchGlob(base, pat.pattern)) {
if (matchGlob(nameWithExt, pat.pattern)) {
const catConfig = rules.categories[pat.category] || { testAxes: [], tolerance: 0 };
return {
category: pat.category,
Expand All @@ -162,6 +179,14 @@ export function categorizeTexture(filename, rules, axisOverride = null) {
};
}

/**
* Resolves a texture identifier or filename directly to its tiling category name
* ('toroidal', 'x-only', 'y-only', 'exempt').
*/
export function resolveTilingCategory(filename, rules) {
return categorizeTexture(filename, rules).category;
}

// --------------------------------------------------------------------------
// Resvg Rasterization & 3x3 Grid Compiler
// --------------------------------------------------------------------------
Expand Down
128 changes: 127 additions & 1 deletion tools/test/tiling-rules.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { matchGlob, globToRegExp, basenameOf, categorizeTexture } from "../test-tiling.mjs";
import { matchGlob, globToRegExp, basenameOf, categorizeTexture, resolveTilingCategory } from "../test-tiling.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down Expand Up @@ -323,12 +323,138 @@ console.log("\n[11] The shipped rules exempt exactly the intended masters");

// Every rules-file pattern must be reachable. A pattern matching nothing is either a
// typo or dead weight, and a dead pattern is exactly how #199 hid for as long as it did.
// A pattern lands in the same change as the first master it matches; there is no
// allowlist for rules written ahead of their textures.
const unreachable = rules.patterns
.map(p => p.pattern)
.filter(p => p !== "grass.svg" && !shipped.some(f => matchGlob(f, p)));
assertEqual(unreachable.join(", "), "", "Every pattern in tiling-rules.json matches at least one shipped master");
}

// --------------------------------------------------------------------------
console.log("\n[12] Extension and case variants resolve like the `.svg` master");
// --------------------------------------------------------------------------
{
// Every pattern is written against `<stem>.svg`. A `.png`, upper-case or bare id must
// give the answer its master gives, never fall through to the default category.
const variants = [
["oak_log.svg", ["oak_log", "oak_log.png", "oak_log.SVG", "block/oak_log.png"]],
["grass_block_side.svg", ["grass_block_side", "grass_block_side.png", "grass_block_side.PNG"]],
["short_grass_1.svg", ["short_grass_1", "short_grass_1.png", "block\\short_grass_1.png"]],
["tall_grass_top.svg", ["tall_grass_top", "tall_grass_top.SVG"]],
["grass_block_side_overlay.svg", ["grass_block_side_overlay", "grass_block_side_overlay.png"]]
];

for (const [master, ids] of variants) {
const expected = categorizeTexture(master, rules);
for (const id of ids) {
const got = categorizeTexture(id, rules);
assertEqual(got.category, expected.category, `'${id}' resolves to ${expected.category}, like ${master}`);
assertEqual(got.testAxes.join(","), expected.testAxes.join(","), `'${id}' is audited on the same axes as ${master}`);
assertEqual(resolveTilingCategory(id, rules), got.category, `resolveTilingCategory('${id}') agrees with categorizeTexture`);
}
}

// An unknown extension is not stripped: it stays part of the name.
assertEqual(
resolveTilingCategory("grass_block_side.txt", rules),
"toroidal",
"Only .svg and .png are stripped, so 'grass_block_side.txt' does not match *_side.svg"
);
}

// --------------------------------------------------------------------------
console.log("\n[13] Pillar-shaped vanilla names keep their full audit");
// --------------------------------------------------------------------------
{
// #88 requires oak_log's toroidal tiling to be verified: logs placed side by side show
// the X seam, so the shipped master is audited on both axes, not Y only.
const oakLog = categorizeTexture("oak_log.svg", rules);
assertEqual(oakLog.category, "toroidal", "The shipped oak_log.svg is audited as toroidal");
assertEqual(oakLog.testAxes.join(","), "x,y", "oak_log.svg is audited on both X and Y");
assertEqual(categorizeTexture("oak_log_top.svg", rules).testAxes.join(","), "x,y", "oak_log_top.svg is audited on both X and Y");

// Real vanilla 1.21 textures ending in `_stem` that are not pillars: crop stems are
// cutouts and mushroom_stem is a full cube. None may be narrowed to a Y-only audit by a
// broad `*_stem.svg` rule.
for (const f of ["pumpkin_stem.svg", "melon_stem.svg", "attached_pumpkin_stem.svg",
"attached_melon_stem.svg", "mushroom_stem.svg"]) {
assert(resolveTilingCategory(f, rules) !== "y-only", `${f} is not audited as a Y-only pillar`);
}
}

// --------------------------------------------------------------------------
console.log("\n[14] Epic 13 item textures are exempt by their vanilla texture stems");
// --------------------------------------------------------------------------
{
// Stems are the vanilla 1.21 `textures/item/` names (and `textures/entity/` for the
// shield, which has no item texture), not the in-game display names.
const toolSets = [];
for (const material of ["wooden", "stone", "iron", "golden", "diamond", "netherite"]) {
for (const tool of ["sword", "pickaxe", "axe", "shovel", "hoe"]) {
toolSets.push(`${material}_${tool}`);
}
}

const epic13Items = [
// #175 survival tools: six materials x sword, pickaxe, axe, shovel, hoe
...toolSets,
// #176 ranged combat and defense, including every animation-state texture
"bow", "bow_pulling_0", "bow_pulling_1", "bow_pulling_2",
"crossbow_standby", "crossbow_pulling_0", "crossbow_pulling_1", "crossbow_pulling_2",
"crossbow_arrow", "crossbow_firework",
"shield_base", "shield_base_nopattern", "trident", "mace",
// #177 food and sustenance
"golden_apple", "cooked_beef", "golden_carrot", "baked_potato", "bread", "apple", "cooked_porkchop"
];
assertEqual(toolSets.length, 30, "The tool suite covers 6 materials x 5 tool types");

for (const itemId of epic13Items) {
assert(rules.itemIds.includes(itemId), `'${itemId}' is listed in itemIds`);
for (const id of [itemId, `${itemId}.svg`, `item/${itemId}.svg`, `${itemId}.png`]) {
const c = categorizeTexture(id, rules);
assertEqual(c.category, "exempt", `'${id}' resolves to exempt`);
assertEqual(c.testAxes.length, 0, `'${id}' is audited on no axis`);
}
}

// Ids that are not vanilla texture stems must not be listed: `steak` is `cooked_beef`,
// `crossbow` has only state textures, `shield` has no item texture, and `porkchop` is
// the raw item while #177 ships `cooked_porkchop`.
for (const bogus of ["steak", "crossbow", "shield", "porkchop"]) {
assert(!rules.itemIds.includes(bogus), `'${bogus}' is not listed in itemIds`);
}

for (const itemId of ["compass_nexus", "plot_compass", "spiral_core", "ninja6_token"]) {
assertEqual(resolveTilingCategory(itemId, rules), "exempt", `Pre-existing item '${itemId}' resolves to exempt`);
}
}

// --------------------------------------------------------------------------
console.log("\n[15] itemIds match exact stems, never substrings");
// --------------------------------------------------------------------------
{
// Real vanilla item textures that contain, or are contained in, a listed id without
// being listed themselves. Each resolves on its own stem.
const cases = [
["bowl.svg", "contains 'bow'"],
["enchanted_golden_apple.svg", "contains 'golden_apple'"],
["carrot.svg", "is contained in 'golden_carrot'"],
["potato.svg", "is contained in 'baked_potato'"],
["porkchop.svg", "is contained in 'cooked_porkchop'"]
];
for (const [f, why] of cases) {
assertEqual(resolveTilingCategory(f, rules), "toroidal", `${f} is not exempted (${why})`);
}

// Standard terrain blocks still fall through to toroidal, and sides stay X only.
for (const f of ["stone.svg", "deepslate.svg", "dirt.svg", "sand.svg", "gravel.svg"]) {
assertEqual(resolveTilingCategory(f, rules), "toroidal", `${f} is toroidal`);
}
assertEqual(resolveTilingCategory("grass_block_side.svg", rules), "x-only", "grass_block_side is x-only");
assertEqual(resolveTilingCategory("dirt_path_side.svg", rules), "x-only", "dirt_path_side is x-only");
}

console.log("\n=======================================================");
console.log(` RESULTS: ${passedTests}/${totalTests} tests passed`);
console.log("=======================================================\n");
Expand Down
51 changes: 50 additions & 1 deletion tools/tiling-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,56 @@
"compass_nexus",
"plot_compass",
"spiral_core",
"ninja6_token"
"ninja6_token",
"wooden_sword",
"wooden_pickaxe",
"wooden_axe",
"wooden_shovel",
"wooden_hoe",
"stone_sword",
"stone_pickaxe",
"stone_axe",
"stone_shovel",
"stone_hoe",
"iron_sword",
"iron_pickaxe",
"iron_axe",
"iron_shovel",
"iron_hoe",
"golden_sword",
"golden_pickaxe",
"golden_axe",
"golden_shovel",
"golden_hoe",
"diamond_sword",
"diamond_pickaxe",
"diamond_axe",
"diamond_shovel",
"diamond_hoe",
"netherite_sword",
"netherite_pickaxe",
"netherite_axe",
"netherite_shovel",
"netherite_hoe",
"bow",
"bow_pulling_0",
"bow_pulling_1",
"bow_pulling_2",
"crossbow_standby",
"crossbow_pulling_0",
"crossbow_pulling_1",
"crossbow_pulling_2",
"crossbow_arrow",
"crossbow_firework",
"shield_base",
"shield_base_nopattern",
"trident",
"mace",
"golden_carrot",
"baked_potato",
"bread",
"apple",
"cooked_porkchop"
],
"overrides": {}
}
Loading