feat(smart-crafting): add smart crafting table using private chest inventory
All checks were successful
Deploy Addons / deploy (push) Successful in 14s

Adds an upgraded crafting block that scans the player's owned private chests
and aggregates their contents with the personal inventory when deciding which
recipes are craftable. Ingredients are consumed from the player first then
from chests; the result goes to the player (or drops at their feet).

Also redraws the post_office and mailbox block textures via a new
scripts/build-textures.py generator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:16:19 +01:00
parent f7aa71e9eb
commit d6bafb9d16
17 changed files with 802 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "silverlabs:smart_crafting_table",
"menu_category": {
"category": "items",
"group": "itemGroup.name.crafting"
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.5
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 20.0
},
"minecraft:map_color": "#D4AF37",
"minecraft:material_instances": {
"*": {
"texture": "smart_crafting_table",
"render_method": "opaque"
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
{
"format_version": 2,
"header": {
"name": "Smart Crafting Table",
"description": "Upgraded crafting table that uses items stored in your private chests",
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a91",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a92",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a93",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.17.0"
},
{
"module_name": "@minecraft/server-ui",
"version": "1.3.0"
},
{
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a94",
"version": [1, 0, 0]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1,19 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:smart_crafting_table_recipe"
},
"tags": ["crafting_table"],
"unlock": { "context": "AlwaysUnlocked" },
"ingredients": [
{ "item": "minecraft:crafting_table" },
{ "item": "minecraft:gold_ingot" },
{ "item": "minecraft:redstone" }
],
"result": {
"item": "silverlabs:smart_crafting_table",
"count": 1
}
}
}

View File

@@ -0,0 +1,116 @@
import { world } from "@minecraft/server";
// Read-only peek into the private-chest addon's world dynamic property.
const PRIVATE_CHESTS_PROP = "private_chests_v1";
function loadRegistry() {
try {
const raw = world.getDynamicProperty(PRIVATE_CHESTS_PROP);
if (raw && typeof raw === "string") return JSON.parse(raw);
} catch (_) {}
return {};
}
// Returns [{ container, label }, ...] for every reachable private chest owned by player.
// Chests in unloaded chunks or that no longer exist are silently skipped.
export function readOwnedChests(player) {
const registry = loadRegistry();
const sources = [];
const skipped = [];
for (const [key, entry] of Object.entries(registry)) {
if (!entry || entry.ownerId !== player.id) continue;
const parts = key.split(",");
if (parts.length < 4) continue;
const x = parseInt(parts[0], 10);
const y = parseInt(parts[1], 10);
const z = parseInt(parts[2], 10);
const dimId = parts.slice(3).join(",");
let dim;
try { dim = world.getDimension(dimId); } catch (_) { continue; }
let block;
try { block = dim.getBlock({ x, y, z }); } catch (_) { block = null; }
if (!block) { skipped.push({ x, y, z, dim: dimId }); continue; }
if (block.typeId !== "minecraft:chest") continue;
let inv;
try { inv = block.getComponent("inventory"); } catch (_) { continue; }
const container = inv?.container;
if (!container) continue;
sources.push({ container, label: `${x},${y},${z}` });
}
return { sources, skipped };
}
// Build a Map<typeId, totalCount> across the player inventory and chest containers.
export function tallyItems(playerInv, chestSources) {
const totals = new Map();
const addContainer = (c) => {
if (!c) return;
for (let i = 0; i < c.size; i++) {
const item = c.getItem(i);
if (!item) continue;
totals.set(item.typeId, (totals.get(item.typeId) || 0) + item.amount);
}
};
addContainer(playerInv);
for (const src of chestSources) addContainer(src.container);
return totals;
}
// How many times can a recipe be crafted given the totals map.
export function craftableCount(recipe, totals) {
let min = Infinity;
for (const ing of recipe.ingredients) {
let available = 0;
for (const typeId of ing.any) available += totals.get(typeId) || 0;
const times = Math.floor(available / ing.count);
if (times < min) min = times;
if (min === 0) return 0;
}
return min === Infinity ? 0 : min;
}
// Remove `count` items matching any of `typeIds` from the sources in order.
// Returns the number actually removed (caller should verify == count).
// `sources` is an ordered list of containers: typically [playerInv, ...chestContainers].
function drain(typeIds, count, sources) {
let remaining = count;
for (const container of sources) {
if (!container) continue;
for (let i = 0; i < container.size && remaining > 0; i++) {
const item = container.getItem(i);
if (!item) continue;
if (!typeIds.includes(item.typeId)) continue;
if (item.amount <= remaining) {
remaining -= item.amount;
container.setItem(i, undefined);
} else {
const clone = item.clone();
clone.amount = item.amount - remaining;
container.setItem(i, clone);
remaining = 0;
}
}
if (remaining === 0) break;
}
return count - remaining;
}
// Consume ingredients for a single craft. Returns true on success, false if anything
// was short (in which case nothing has been consumed — caller should pre-verify).
export function consumeIngredients(recipe, playerInv, chestSources) {
const containers = [playerInv, ...chestSources.map((s) => s.container)];
for (const ing of recipe.ingredients) {
const removed = drain(ing.any, ing.count, containers);
if (removed < ing.count) return false;
}
return true;
}

View File

@@ -0,0 +1,110 @@
import { world, system, ItemStack } from "@minecraft/server";
import { ActionFormData } from "@minecraft/server-ui";
import { RECIPES } from "./recipes.js";
import { readOwnedChests, tallyItems, craftableCount, consumeIngredients } from "./chest-scan.js";
const SMART_TABLE = "silverlabs:smart_crafting_table";
function getPlayerInventory(player) {
try { return player.getComponent("inventory")?.container ?? null; }
catch (_) { return null; }
}
function giveOrDrop(player, itemStack) {
const inv = getPlayerInventory(player);
if (inv) {
const leftover = inv.addItem(itemStack);
if (!leftover) return;
// inventory full — drop whatever didn't fit at player's feet
try { player.dimension.spawnItem(leftover, player.location); } catch (_) {}
return;
}
try { player.dimension.spawnItem(itemStack, player.location); } catch (_) {}
}
async function openCraftingUI(player) {
const playerInv = getPlayerInventory(player);
if (!playerInv) {
player.sendMessage(`§c[Smart Crafting] §7Couldn't access your inventory.`);
return;
}
const { sources, skipped } = readOwnedChests(player);
const totals = tallyItems(playerInv, sources);
const candidates = [];
for (const recipe of RECIPES) {
const count = craftableCount(recipe, totals);
if (count > 0) candidates.push({ recipe, count });
}
const bodyLines = [
`§7Scanned §f${sources.length}§7 of your chests.`,
];
if (skipped.length > 0) bodyLines.push(`§8(${skipped.length} chest${skipped.length === 1 ? "" : "s"} unreachable — chunk not loaded)`);
if (candidates.length === 0) {
bodyLines.push("", "§cNo recipes available with your current items.");
} else {
bodyLines.push(`§7§o${candidates.length} recipe${candidates.length === 1 ? "" : "s"} craftable.`);
}
const form = new ActionFormData()
.title("§6Smart Crafting")
.body(bodyLines.join("\n"));
for (const c of candidates) {
form.button(`${c.recipe.name} §7(x${c.count})`);
}
form.button("§cClose");
let response;
try { response = await form.show(player); }
catch (_) { return; }
if (response.canceled || response.selection === undefined) return;
if (response.selection >= candidates.length) return; // Close button
const chosen = candidates[response.selection].recipe;
// Re-scan to ensure items didn't change while the form was open.
const freshInv = getPlayerInventory(player);
if (!freshInv) { player.sendMessage(`§c[Smart Crafting] §7Inventory became unavailable.`); return; }
const fresh = readOwnedChests(player);
const freshTotals = tallyItems(freshInv, fresh.sources);
if (craftableCount(chosen, freshTotals) < 1) {
player.sendMessage(`§c[Smart Crafting] §7Ingredients for §f${chosen.name}§7 are no longer available.`);
return;
}
const ok = consumeIngredients(chosen, freshInv, fresh.sources);
if (!ok) {
player.sendMessage(`§c[Smart Crafting] §7Something went wrong consuming ingredients.`);
return;
}
try {
const result = new ItemStack(chosen.result.item, chosen.result.count);
giveOrDrop(player, result);
} catch (e) {
player.sendMessage(`§c[Smart Crafting] §7Couldn't create result item: ${e.message}`);
return;
}
player.sendMessage(`§6[Smart Crafting] §7Crafted §f${chosen.name}§7.`);
}
// ─── Interact: open the crafting UI ─────────────────────────
try {
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block || block.typeId !== SMART_TABLE) return;
event.cancel = true;
const playerRef = event.player;
system.run(() => openCraftingUI(playerRef));
});
} catch (e) {
console.warn(`[Smart Crafting] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
}
system.run(() => {
world.sendMessage("§6[Smart Crafting] §7Smart crafting table loaded.");
});

View File

@@ -0,0 +1,215 @@
// Recipe catalogue for the Smart Crafting Table.
// Each recipe lists ingredients as { any: [...itemIds], count }. The "any" variant
// lets a single slot accept any matching wood-type etc.
const PLANKS = [
"minecraft:oak_planks",
"minecraft:spruce_planks",
"minecraft:birch_planks",
"minecraft:jungle_planks",
"minecraft:acacia_planks",
"minecraft:dark_oak_planks",
"minecraft:mangrove_planks",
"minecraft:cherry_planks",
"minecraft:bamboo_planks",
"minecraft:crimson_planks",
"minecraft:warped_planks",
];
const LOGS = [
"minecraft:oak_log",
"minecraft:spruce_log",
"minecraft:birch_log",
"minecraft:jungle_log",
"minecraft:acacia_log",
"minecraft:dark_oak_log",
"minecraft:mangrove_log",
"minecraft:cherry_log",
"minecraft:crimson_stem",
"minecraft:warped_stem",
];
const WOOL = [
"minecraft:white_wool",
"minecraft:orange_wool",
"minecraft:magenta_wool",
"minecraft:light_blue_wool",
"minecraft:yellow_wool",
"minecraft:lime_wool",
"minecraft:pink_wool",
"minecraft:gray_wool",
"minecraft:light_gray_wool",
"minecraft:cyan_wool",
"minecraft:purple_wool",
"minecraft:blue_wool",
"minecraft:brown_wool",
"minecraft:green_wool",
"minecraft:red_wool",
"minecraft:black_wool",
];
const STICK = ["minecraft:stick"];
const COBBLE = ["minecraft:cobblestone"];
const IRON = ["minecraft:iron_ingot"];
const GOLD = ["minecraft:gold_ingot"];
const DIAMOND = ["minecraft:diamond"];
export const RECIPES = [
// ── Basics ─────────────────────────────────────────────────
{ id: "stick", name: "Stick",
result: { item: "minecraft:stick", count: 4 },
ingredients: [{ any: PLANKS, count: 2 }] },
{ id: "planks", name: "Wooden Planks (oak)",
result: { item: "minecraft:oak_planks", count: 4 },
ingredients: [{ any: ["minecraft:oak_log"], count: 1 }] },
{ id: "crafting_table", name: "Crafting Table",
result: { item: "minecraft:crafting_table", count: 1 },
ingredients: [{ any: PLANKS, count: 4 }] },
{ id: "chest", name: "Chest",
result: { item: "minecraft:chest", count: 1 },
ingredients: [{ any: PLANKS, count: 8 }] },
{ id: "furnace", name: "Furnace",
result: { item: "minecraft:furnace", count: 1 },
ingredients: [{ any: COBBLE, count: 8 }] },
{ id: "torch", name: "Torch",
result: { item: "minecraft:torch", count: 4 },
ingredients: [
{ any: ["minecraft:coal", "minecraft:charcoal"], count: 1 },
{ any: STICK, count: 1 },
] },
{ id: "ladder", name: "Ladder",
result: { item: "minecraft:ladder", count: 3 },
ingredients: [{ any: STICK, count: 7 }] },
{ id: "bucket", name: "Bucket",
result: { item: "minecraft:bucket", count: 1 },
ingredients: [{ any: IRON, count: 3 }] },
{ id: "bed", name: "Bed (red)",
result: { item: "minecraft:red_bed", count: 1 },
ingredients: [
{ any: WOOL, count: 3 },
{ any: PLANKS, count: 3 },
] },
// ── Tools: wood ────────────────────────────────────────────
{ id: "wood_pickaxe", name: "Wooden Pickaxe",
result: { item: "minecraft:wooden_pickaxe", count: 1 },
ingredients: [{ any: PLANKS, count: 3 }, { any: STICK, count: 2 }] },
{ id: "wood_axe", name: "Wooden Axe",
result: { item: "minecraft:wooden_axe", count: 1 },
ingredients: [{ any: PLANKS, count: 3 }, { any: STICK, count: 2 }] },
{ id: "wood_shovel", name: "Wooden Shovel",
result: { item: "minecraft:wooden_shovel", count: 1 },
ingredients: [{ any: PLANKS, count: 1 }, { any: STICK, count: 2 }] },
{ id: "wood_sword", name: "Wooden Sword",
result: { item: "minecraft:wooden_sword", count: 1 },
ingredients: [{ any: PLANKS, count: 2 }, { any: STICK, count: 1 }] },
{ id: "wood_hoe", name: "Wooden Hoe",
result: { item: "minecraft:wooden_hoe", count: 1 },
ingredients: [{ any: PLANKS, count: 2 }, { any: STICK, count: 2 }] },
// ── Tools: stone ───────────────────────────────────────────
{ id: "stone_pickaxe", name: "Stone Pickaxe",
result: { item: "minecraft:stone_pickaxe", count: 1 },
ingredients: [{ any: COBBLE, count: 3 }, { any: STICK, count: 2 }] },
{ id: "stone_axe", name: "Stone Axe",
result: { item: "minecraft:stone_axe", count: 1 },
ingredients: [{ any: COBBLE, count: 3 }, { any: STICK, count: 2 }] },
{ id: "stone_shovel", name: "Stone Shovel",
result: { item: "minecraft:stone_shovel", count: 1 },
ingredients: [{ any: COBBLE, count: 1 }, { any: STICK, count: 2 }] },
{ id: "stone_sword", name: "Stone Sword",
result: { item: "minecraft:stone_sword", count: 1 },
ingredients: [{ any: COBBLE, count: 2 }, { any: STICK, count: 1 }] },
{ id: "stone_hoe", name: "Stone Hoe",
result: { item: "minecraft:stone_hoe", count: 1 },
ingredients: [{ any: COBBLE, count: 2 }, { any: STICK, count: 2 }] },
// ── Tools: iron ────────────────────────────────────────────
{ id: "iron_pickaxe", name: "Iron Pickaxe",
result: { item: "minecraft:iron_pickaxe", count: 1 },
ingredients: [{ any: IRON, count: 3 }, { any: STICK, count: 2 }] },
{ id: "iron_axe", name: "Iron Axe",
result: { item: "minecraft:iron_axe", count: 1 },
ingredients: [{ any: IRON, count: 3 }, { any: STICK, count: 2 }] },
{ id: "iron_shovel", name: "Iron Shovel",
result: { item: "minecraft:iron_shovel", count: 1 },
ingredients: [{ any: IRON, count: 1 }, { any: STICK, count: 2 }] },
{ id: "iron_sword", name: "Iron Sword",
result: { item: "minecraft:iron_sword", count: 1 },
ingredients: [{ any: IRON, count: 2 }, { any: STICK, count: 1 }] },
{ id: "iron_hoe", name: "Iron Hoe",
result: { item: "minecraft:iron_hoe", count: 1 },
ingredients: [{ any: IRON, count: 2 }, { any: STICK, count: 2 }] },
// ── Tools: gold ────────────────────────────────────────────
{ id: "gold_pickaxe", name: "Golden Pickaxe",
result: { item: "minecraft:golden_pickaxe", count: 1 },
ingredients: [{ any: GOLD, count: 3 }, { any: STICK, count: 2 }] },
{ id: "gold_sword", name: "Golden Sword",
result: { item: "minecraft:golden_sword", count: 1 },
ingredients: [{ any: GOLD, count: 2 }, { any: STICK, count: 1 }] },
// ── Tools: diamond ─────────────────────────────────────────
{ id: "diamond_pickaxe", name: "Diamond Pickaxe",
result: { item: "minecraft:diamond_pickaxe", count: 1 },
ingredients: [{ any: DIAMOND, count: 3 }, { any: STICK, count: 2 }] },
{ id: "diamond_axe", name: "Diamond Axe",
result: { item: "minecraft:diamond_axe", count: 1 },
ingredients: [{ any: DIAMOND, count: 3 }, { any: STICK, count: 2 }] },
{ id: "diamond_shovel", name: "Diamond Shovel",
result: { item: "minecraft:diamond_shovel", count: 1 },
ingredients: [{ any: DIAMOND, count: 1 }, { any: STICK, count: 2 }] },
{ id: "diamond_sword", name: "Diamond Sword",
result: { item: "minecraft:diamond_sword", count: 1 },
ingredients: [{ any: DIAMOND, count: 2 }, { any: STICK, count: 1 }] },
// ── Armor: iron ────────────────────────────────────────────
{ id: "iron_helmet", name: "Iron Helmet",
result: { item: "minecraft:iron_helmet", count: 1 },
ingredients: [{ any: IRON, count: 5 }] },
{ id: "iron_chestplate", name: "Iron Chestplate",
result: { item: "minecraft:iron_chestplate", count: 1 },
ingredients: [{ any: IRON, count: 8 }] },
{ id: "iron_leggings", name: "Iron Leggings",
result: { item: "minecraft:iron_leggings", count: 1 },
ingredients: [{ any: IRON, count: 7 }] },
{ id: "iron_boots", name: "Iron Boots",
result: { item: "minecraft:iron_boots", count: 1 },
ingredients: [{ any: IRON, count: 4 }] },
// ── Armor: diamond ─────────────────────────────────────────
{ id: "diamond_helmet", name: "Diamond Helmet",
result: { item: "minecraft:diamond_helmet", count: 1 },
ingredients: [{ any: DIAMOND, count: 5 }] },
{ id: "diamond_chestplate", name: "Diamond Chestplate",
result: { item: "minecraft:diamond_chestplate", count: 1 },
ingredients: [{ any: DIAMOND, count: 8 }] },
{ id: "diamond_leggings", name: "Diamond Leggings",
result: { item: "minecraft:diamond_leggings", count: 1 },
ingredients: [{ any: DIAMOND, count: 7 }] },
{ id: "diamond_boots", name: "Diamond Boots",
result: { item: "minecraft:diamond_boots", count: 1 },
ingredients: [{ any: DIAMOND, count: 4 }] },
// ── Utility ────────────────────────────────────────────────
{ id: "shears", name: "Shears",
result: { item: "minecraft:shears", count: 1 },
ingredients: [{ any: IRON, count: 2 }] },
{ id: "flint_and_steel", name: "Flint and Steel",
result: { item: "minecraft:flint_and_steel", count: 1 },
ingredients: [{ any: IRON, count: 1 }, { any: ["minecraft:flint"], count: 1 }] },
{ id: "compass", name: "Compass",
result: { item: "minecraft:compass", count: 1 },
ingredients: [{ any: IRON, count: 4 }, { any: ["minecraft:redstone"], count: 1 }] },
{ id: "clock", name: "Clock",
result: { item: "minecraft:clock", count: 1 },
ingredients: [{ any: GOLD, count: 4 }, { any: ["minecraft:redstone"], count: 1 }] },
];

View File

@@ -0,0 +1,4 @@
{
"format_version": [1, 1, 0],
"silverlabs:smart_crafting_table": { "sound": "wood" }
}

View File

@@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Smart Crafting Table Resources",
"description": "Texture and lang for the silverlabs:smart_crafting_table block",
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a94",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "resources",
"uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a95",
"version": [1, 0, 0]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1 @@
tile.silverlabs:smart_crafting_table.name=Smart Crafting Table

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,11 @@
{
"resource_pack_name": "smart_crafting_RP",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {
"smart_crafting_table": {
"textures": "textures/blocks/smart_crafting_table"
}
}
}