Compare commits
26 Commits
stable
...
4cf1e550b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cf1e550b7 | ||
|
|
66ac81de65 | ||
|
|
ea29136f25 | ||
|
|
641a317873 | ||
|
|
5dd9bd9344 | ||
|
|
b7427aa973 | ||
|
|
48ca088a2c | ||
|
|
b9081f26a8 | ||
|
|
f10108bd48 | ||
|
|
f1e418ad83 | ||
|
|
9a02b350c2 | ||
|
|
c390b5291b | ||
|
|
b2d55feb98 | ||
|
|
54cad8b92b | ||
|
|
b3a69c6797 | ||
|
|
62a9ab811d | ||
|
|
df436ff84d | ||
|
|
1606d60a06 | ||
|
|
f327f82677 | ||
|
|
2c99169ce9 | ||
|
|
9ca46dc29d | ||
|
|
3f79645bb8 | ||
|
|
8468134919 | ||
|
|
d9638cdc69 | ||
|
|
2d2b8835b1 | ||
|
|
22836dafb2 |
110
README.md
110
README.md
@@ -47,7 +47,9 @@ A Minecraft inventory management system built on [CC:Tweaked](https://tweaked.cc
|
||||
| `craftingTurtle.lua` | **Crafting Worker** | Runs on a Crafting Turtle. When the master places ingredients in its grid, it auto-crafts and reports results. Must be connected to the master via wired network. |
|
||||
| `dropperController.lua` | **Dropper Driver** | Pulses redstone to fire items out of a dropper block until empty. Runs on a computer adjacent to the dropper. |
|
||||
| `inventoryWebBridge.lua` | **Web Bridge** | Bridges the in-game modem network to the external web server over HTTP/WebSocket. Forwards state from the master and relays commands from the web dashboard back into the game. |
|
||||
| `miningTurtle.lua` | **Mining Turtle** | Runs on a mining turtle connected to the wired network. Continuously mines downward, auto-dumps inventory to networked storage, auto-refuels from storage, and triggers master scans after dumping. |
|
||||
| `listDevicesByType.lua` | **Diagnostic Utility** | Lists all peripherals on the wired network grouped by type. Useful for discovering connected chests, furnaces, etc. |
|
||||
| `autorun/startup.lua` | **Opus Autorun** | Auto-detects the computer's role from config files and launches the appropriate program. For use with [Opus OS](https://github.com/kepler155c/opus). |
|
||||
|
||||
### Web Stack (Docker)
|
||||
|
||||
@@ -62,54 +64,27 @@ A Minecraft inventory management system built on [CC:Tweaked](https://tweaked.cc
|
||||
### Prerequisites
|
||||
|
||||
- Minecraft with [CC:Tweaked](https://tweaked.cc/) installed
|
||||
- [Opus OS](https://github.com/kepler155c/opus) installed on all CC:Tweaked computers (required for the UI framework)
|
||||
- A CC:Tweaked computer with a wired modem and monitor attached
|
||||
- HTTP access enabled in the CC:Tweaked config (for the web bridge)
|
||||
|
||||
### 1. Install the Master Controller
|
||||
### 1. Install via Opus Package Manager (Recommended)
|
||||
|
||||
On your main CC:Tweaked computer (with the monitor and wired modem), open the terminal and run:
|
||||
On any CC:Tweaked computer running Opus, open the shell and run:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/inventoryManager.lua startup.lua
|
||||
package install inventory-manager
|
||||
```
|
||||
|
||||
This downloads `inventoryManager.lua` and saves it as `startup.lua` so it runs automatically on boot. Reboot the computer or run `startup.lua` to start.
|
||||
An interactive setup wizard will guide you through role selection (Manager, Client, Web Bridge, or Mining Turtle) and peripheral configuration. The package installs all required files and an autorun script that launches the correct program on boot. Reboot after installation.
|
||||
|
||||
### 2. Install a Display Client (Optional)
|
||||
|
||||
On a separate CC:Tweaked computer with a monitor:
|
||||
To update later:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/inventoryClient.lua startup.lua
|
||||
package update inventory-manager
|
||||
```
|
||||
|
||||
### 3. Install the Crafting Turtle (Optional)
|
||||
|
||||
On a **Crafting Turtle** connected to the wired network:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/craftingTurtle.lua startup.lua
|
||||
```
|
||||
|
||||
### 4. Install the Dropper Controller (Optional)
|
||||
|
||||
On a computer adjacent to a dropper block:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/dropperController.lua startup.lua
|
||||
```
|
||||
|
||||
### 5. Install the Web Bridge (Optional)
|
||||
|
||||
On any CC:Tweaked computer with a wired modem and HTTP access:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/inventoryWebBridge.lua startup.lua
|
||||
```
|
||||
|
||||
Configure the web server URL by editing `.webbridge_config` on the computer, or it will default to `http://localhost`.
|
||||
|
||||
### 6. Start the Web Dashboard (Optional)
|
||||
### 2. Start the Web Dashboard (Optional)
|
||||
|
||||
On the host machine, navigate to the `web/` directory and run:
|
||||
|
||||
@@ -119,14 +94,79 @@ docker compose up -d --build
|
||||
|
||||
The dashboard will be available at `http://localhost` on port 80.
|
||||
|
||||
### Manual Install (Without Opus)
|
||||
|
||||
If you are not using Opus, you can install individual components with `wget`. Note that the monitor dashboard requires the Opus UI framework (`opus.ui`), so Opus is still needed on computers with monitors.
|
||||
|
||||
<details>
|
||||
<summary>Click to expand manual install instructions</summary>
|
||||
|
||||
#### Master Controller
|
||||
|
||||
On your main CC:Tweaked computer (with the monitor and wired modem):
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/startup/manager.lua startup.lua
|
||||
```
|
||||
|
||||
#### Display Client
|
||||
|
||||
On a separate CC:Tweaked computer with a monitor:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/startup/client.lua startup.lua
|
||||
```
|
||||
|
||||
#### Crafting Turtle
|
||||
|
||||
On a **Crafting Turtle** connected to the wired network:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/startup/turtle.lua startup.lua
|
||||
```
|
||||
|
||||
#### Mining Turtle
|
||||
|
||||
On a **Mining Turtle** connected to the wired network:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/startup/miner.lua startup.lua
|
||||
```
|
||||
|
||||
#### Web Bridge
|
||||
|
||||
On any CC:Tweaked computer with a wired modem and HTTP access:
|
||||
|
||||
```
|
||||
wget https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/startup/bridge.lua startup.lua
|
||||
```
|
||||
|
||||
These startup scripts auto-download all required files from the repository before launching.
|
||||
|
||||
</details>
|
||||
|
||||
## Modem Channels
|
||||
|
||||
| Channel | Purpose |
|
||||
|---------|---------|
|
||||
| 4200 | Master → Clients/Bridge (state broadcast) |
|
||||
| 4201 | Clients/Bridge → Master (orders & commands) |
|
||||
| 4202 | Master → Client (order/craft result replies) |
|
||||
| 4203 | Master → Crafting Turtle (craft requests) |
|
||||
| 4204 | Crafting Turtle → Master (craft results) |
|
||||
| 4205 | System channel (remote reboot all computers) |
|
||||
|
||||
## Startup Scripts
|
||||
|
||||
The `startup/` directory contains self-updating startup scripts that download the latest code from the git repository before launching. These are used by the manual install method above and can also be used standalone:
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `startup/manager.lua` | Master controller with auto-update for all manager modules |
|
||||
| `startup/client.lua` | Client with auto-update, first-run setup wizard, and optional dropper |
|
||||
| `startup/turtle.lua` | Crafting turtle with auto-update |
|
||||
| `startup/bridge.lua` | Web bridge with auto-update |
|
||||
| `startup/miner.lua` | Mining turtle with auto-update |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -180,30 +180,56 @@ local function handleCraftCommand(message)
|
||||
|
||||
for turtleSlotStr, info in pairs(slots) do
|
||||
local turtleSlot = tonumber(turtleSlotStr)
|
||||
local chestName = info.chestName
|
||||
local chestSlot = info.chestSlot
|
||||
local itemName = info.itemName
|
||||
local count = info.count or 1
|
||||
local placed = 0
|
||||
|
||||
print(string.format("[CRAFT] Pulling %s from %s slot %d -> turtle slot %d",
|
||||
itemName, chestName, chestSlot, turtleSlot))
|
||||
|
||||
local chest = peripheral.wrap(chestName)
|
||||
if not chest then
|
||||
print(string.format("[CRAFT] Cannot wrap chest: %s", chestName))
|
||||
allPlaced = false
|
||||
break
|
||||
end
|
||||
local ok, n = pcall(chest.pushItems, selfName, chestSlot, count, turtleSlot)
|
||||
if ok and n and n > 0 then
|
||||
placedItems[turtleSlot] = itemName
|
||||
print(string.format("[CRAFT] Placed %s x%d in slot %d", itemName, n, turtleSlot))
|
||||
else
|
||||
if not ok then
|
||||
print(string.format("[CRAFT] pullItems error: %s", tostring(n)))
|
||||
else
|
||||
print(string.format("[CRAFT] pullItems returned %s (expected %d)", tostring(n), count))
|
||||
-- Try the suggested chest+slot first
|
||||
local chest = peripheral.wrap(info.chestName)
|
||||
if chest then
|
||||
local ok, n = pcall(chest.pushItems, selfName, info.chestSlot, count, turtleSlot)
|
||||
if ok and n and n > 0 then
|
||||
placed = n
|
||||
end
|
||||
end
|
||||
|
||||
-- If we didn't get enough, search ALL chests on the network for this item
|
||||
if placed < count then
|
||||
local remaining = count - placed
|
||||
if placed == 0 then
|
||||
print(string.format("[CRAFT] %s not at %s:%d, searching network...",
|
||||
itemName, info.chestName, info.chestSlot))
|
||||
else
|
||||
print(string.format("[CRAFT] Got %d/%d %s from hint, searching for %d more...",
|
||||
placed, count, itemName, remaining))
|
||||
end
|
||||
|
||||
for _, chestName in ipairs(returnChests) do
|
||||
if remaining <= 0 then break end
|
||||
local ch = peripheral.wrap(chestName)
|
||||
if ch then
|
||||
local contents = ch.list()
|
||||
if contents then
|
||||
for slot, slotItem in pairs(contents) do
|
||||
if slotItem.name == itemName then
|
||||
local ok, n = pcall(ch.pushItems, selfName, slot, remaining, turtleSlot)
|
||||
if ok and n and n > 0 then
|
||||
placed = placed + n
|
||||
remaining = remaining - n
|
||||
if remaining <= 0 then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if placed > 0 then
|
||||
placedItems[turtleSlot] = itemName
|
||||
print(string.format("[CRAFT] Placed %s x%d in slot %d", itemName, placed, turtleSlot))
|
||||
else
|
||||
print(string.format("[CRAFT] Could not find %s anywhere on network!", itemName))
|
||||
allPlaced = false
|
||||
break
|
||||
end
|
||||
|
||||
14
data/auto_craft.lua
Normal file
14
data/auto_craft.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Auto-craft rules: items that should be automatically crafted when stock exceeds reserve.
|
||||
-- The system will keep `reserve` of the input item and craft all excess into the output.
|
||||
-- Requires a crafting turtle to be connected.
|
||||
--
|
||||
-- Format: { input = "mod:item", reserve = N, output = "mod:output_item" }
|
||||
-- The output item must have a recipe in data/craftable.lua (or learned via recipeBook).
|
||||
-- Recursive crafting is used, so intermediate steps are handled automatically.
|
||||
|
||||
return {
|
||||
-- Convert excess bamboo into planks (keeps 128 raw bamboo as reserve)
|
||||
{ input = "minecraft:bamboo", reserve = 128, output = "minecraft:bamboo_planks" },
|
||||
-- Convert excess bamboo blocks into planks too (keeps 64 blocks as reserve)
|
||||
{ input = "minecraft:bamboo_block", reserve = 64, output = "minecraft:bamboo_planks" },
|
||||
}
|
||||
@@ -31,6 +31,24 @@ return {
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:bamboo_block",
|
||||
count = 1,
|
||||
grid = {
|
||||
"minecraft:bamboo", "minecraft:bamboo", nil,
|
||||
"minecraft:bamboo", "minecraft:bamboo", nil,
|
||||
"minecraft:bamboo", "minecraft:bamboo", nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:bamboo_planks",
|
||||
count = 2,
|
||||
grid = {
|
||||
"minecraft:bamboo_block", nil, nil,
|
||||
nil, nil, nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:stick",
|
||||
count = 4,
|
||||
@@ -105,6 +123,114 @@ return {
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone",
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:cobblestone_slab",
|
||||
count = 6,
|
||||
grid = {
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone",
|
||||
nil, nil, nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:cobblestone_stairs",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:cobblestone", nil, nil,
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", nil,
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone",
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:cobblestone_wall",
|
||||
count = 6,
|
||||
grid = {
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone",
|
||||
"minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone",
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:stone_bricks",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:stone", "minecraft:stone", nil,
|
||||
"minecraft:stone", "minecraft:stone", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:stone_slab",
|
||||
count = 6,
|
||||
grid = {
|
||||
"minecraft:stone", "minecraft:stone", "minecraft:stone",
|
||||
nil, nil, nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:stone_stairs",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:stone", nil, nil,
|
||||
"minecraft:stone", "minecraft:stone", nil,
|
||||
"minecraft:stone", "minecraft:stone", "minecraft:stone",
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:smooth_stone_slab",
|
||||
count = 6,
|
||||
grid = {
|
||||
"minecraft:smooth_stone", "minecraft:smooth_stone", "minecraft:smooth_stone",
|
||||
nil, nil, nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:polished_andesite",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:andesite", "minecraft:andesite", nil,
|
||||
"minecraft:andesite", "minecraft:andesite", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:polished_diorite",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:diorite", "minecraft:diorite", nil,
|
||||
"minecraft:diorite", "minecraft:diorite", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:polished_granite",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:granite", "minecraft:granite", nil,
|
||||
"minecraft:granite", "minecraft:granite", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:polished_deepslate",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:cobbled_deepslate", "minecraft:cobbled_deepslate", nil,
|
||||
"minecraft:cobbled_deepslate", "minecraft:cobbled_deepslate", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:polished_tuff",
|
||||
count = 4,
|
||||
grid = {
|
||||
"minecraft:tuff", "minecraft:tuff", nil,
|
||||
"minecraft:tuff", "minecraft:tuff", nil,
|
||||
nil, nil, nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
output = "minecraft:ladder",
|
||||
count = 3,
|
||||
|
||||
20
data/stock_limits.lua
Normal file
20
data/stock_limits.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Stock limits: maximum quantities to keep in storage.
|
||||
-- When an item exceeds its limit, the excess is pushed to the trash dropper.
|
||||
-- Set up a dropper facing lava, cactus, or void to destroy excess items.
|
||||
--
|
||||
-- Format: ["mod:item"] = maxCount
|
||||
-- Only items listed here are subject to auto-discard; everything else is unlimited.
|
||||
|
||||
return {
|
||||
["minecraft:cobblestone"] = 8192, -- 128 slots (~5 chests)
|
||||
["minecraft:stone"] = 4096,
|
||||
["minecraft:smooth_stone"] = 4096,
|
||||
["minecraft:cobbled_deepslate"] = 4096,
|
||||
["minecraft:netherrack"] = 2048,
|
||||
["minecraft:dirt"] = 2048,
|
||||
["minecraft:gravel"] = 2048,
|
||||
["minecraft:andesite"] = 2048,
|
||||
["minecraft:diorite"] = 2048,
|
||||
["minecraft:granite"] = 2048,
|
||||
["minecraft:tuff"] = 1024,
|
||||
}
|
||||
@@ -42,7 +42,6 @@ end
|
||||
|
||||
-- Override dofile to load modules into our _ENV so they inherit
|
||||
-- Opus's require/package (CC:Tweaked dofile uses _G instead).
|
||||
local _ccDofile = dofile
|
||||
local function dofile(path) -- luacheck: ignore
|
||||
local fn, err = loadfile(path, nil, _ENV)
|
||||
if fn then return fn()
|
||||
@@ -56,6 +55,7 @@ local CLIENT_CONFIG_FILE = _configPath(".client_config")
|
||||
-------------------------------------------------
|
||||
|
||||
local log = dofile(_path("lib/log.lua"))
|
||||
local ui = dofile(_path("lib/ui.lua"))
|
||||
|
||||
local function loadConfig()
|
||||
if not fs.exists(CLIENT_CONFIG_FILE) then return end
|
||||
@@ -120,7 +120,6 @@ end
|
||||
local state = {}
|
||||
|
||||
state.cache = {
|
||||
catalogue = {},
|
||||
itemList = {},
|
||||
itemListDirty = false,
|
||||
grandTotal = 0,
|
||||
@@ -185,46 +184,11 @@ local function getItemTotal(itemName)
|
||||
return 0
|
||||
end
|
||||
|
||||
function ops.getRecipeIngredients(recipe)
|
||||
local ingredients = {}
|
||||
for _, item in ipairs(recipe.grid) do
|
||||
if item then
|
||||
ingredients[item] = (ingredients[item] or 0) + 1
|
||||
end
|
||||
end
|
||||
return ingredients
|
||||
end
|
||||
|
||||
function ops.canCraftRecipe(recipe)
|
||||
local ingredients = ops.getRecipeIngredients(recipe)
|
||||
for itemName, needed in pairs(ingredients) do
|
||||
if (getItemTotal(itemName) or 0) < needed then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ops.maxCraftBatches(recipe)
|
||||
local ingredients = ops.getRecipeIngredients(recipe)
|
||||
local minBatches = math.huge
|
||||
for itemName, needed in pairs(ingredients) do
|
||||
local batches = math.floor((getItemTotal(itemName) or 0) / needed)
|
||||
if batches < minBatches then minBatches = batches end
|
||||
end
|
||||
if minBatches == math.huge then return 0 end
|
||||
return minBatches
|
||||
end
|
||||
|
||||
function ops.getMissingIngredients(recipe)
|
||||
local ingredients = ops.getRecipeIngredients(recipe)
|
||||
local missing = {}
|
||||
for itemName, needed in pairs(ingredients) do
|
||||
local have = getItemTotal(itemName) or 0
|
||||
if have < needed then
|
||||
table.insert(missing, { name = itemName, have = have, need = needed })
|
||||
end
|
||||
end
|
||||
return missing
|
||||
end
|
||||
-- Delegate recipe helpers to shared lib/ui.lua with local stock lookup.
|
||||
function ops.getRecipeIngredients(recipe) return ui.getRecipeIngredients(recipe) end
|
||||
function ops.canCraftRecipe(recipe) return ui.canCraftRecipe(recipe, getItemTotal) end
|
||||
function ops.maxCraftBatches(recipe) return ui.maxCraftBatches(recipe, getItemTotal) end
|
||||
function ops.getMissingIngredients(recipe) return ui.getMissingIngredients(recipe, getItemTotal) end
|
||||
|
||||
function ops.orderItem(itemName, amount)
|
||||
sendToMaster({
|
||||
@@ -392,10 +356,8 @@ local function main()
|
||||
c.barrelOk = message.cache.barrelOk
|
||||
c.furnaceCount = message.cache.furnaceCount ~= nil and message.cache.furnaceCount or c.furnaceCount
|
||||
c.furnaceStatus = message.cache.furnaceStatus or c.furnaceStatus
|
||||
-- Also build catalogue from itemList so display.lua
|
||||
-- smelter tab can look up stock by item name
|
||||
if message.cache.catalogue then
|
||||
c.catalogue = message.cache.catalogue
|
||||
if message.cache.droppers then
|
||||
c.droppers = message.cache.droppers
|
||||
end
|
||||
end
|
||||
if message.activity then
|
||||
|
||||
@@ -39,7 +39,6 @@ local ok, err = xpcall(function()
|
||||
|
||||
-- Override dofile to load modules into our _ENV so they inherit
|
||||
-- Opus's require/package (CC:Tweaked dofile uses _G instead).
|
||||
local _ccDofile = dofile
|
||||
local function dofile(path) -- luacheck: ignore
|
||||
local fn, err = loadfile(path, nil, _ENV)
|
||||
if fn then return fn()
|
||||
@@ -52,8 +51,6 @@ end
|
||||
|
||||
local log = dofile(_path("lib/log.lua"))
|
||||
local ui = dofile(_path("lib/ui.lua"))
|
||||
local itemDB = dofile(_path("lib/itemDB.lua"))
|
||||
itemDB.init(_configPath(".item_names.db"))
|
||||
|
||||
-------------------------------------------------
|
||||
-- Load modules (factory pattern → shared context)
|
||||
@@ -85,7 +82,6 @@ ctx.display = display
|
||||
local craftEngine = dofile(_path("lib/craft.lua"))
|
||||
craftEngine.init(cfg.recipeBook, ops.getItemTotal)
|
||||
ctx.craftEngine = craftEngine
|
||||
ctx.itemDB = itemDB
|
||||
|
||||
-- Convenience aliases
|
||||
local cache = state.cache
|
||||
@@ -151,9 +147,12 @@ local function broadcastState()
|
||||
-- Keep ctx in sync so display.lua can check ctx.craftTurtleOk directly
|
||||
ctx.craftTurtleOk = payload.craftTurtleOk
|
||||
|
||||
payload.smeltable = cfg.SMELTABLE
|
||||
payload.craftable = cfg.CRAFTABLE
|
||||
state.configDirty = false
|
||||
-- Only include recipe tables when config has changed (they're large).
|
||||
if state.configDirty then
|
||||
payload.smeltable = cfg.SMELTABLE
|
||||
payload.craftable = cfg.CRAFTABLE
|
||||
state.configDirty = false
|
||||
end
|
||||
|
||||
ctx.networkModem.transmit(cfg.BROADCAST_CHANNEL, cfg.ORDER_CHANNEL, payload)
|
||||
state.lastBroadcastVersion = state.stateVersion
|
||||
@@ -350,13 +349,35 @@ local function main()
|
||||
end
|
||||
print("")
|
||||
|
||||
-----------------------------------------------
|
||||
-- Resilient task wrapper: if a task crashes, it
|
||||
-- restarts after a brief delay instead of killing
|
||||
-- all other parallel tasks.
|
||||
-----------------------------------------------
|
||||
local function resilient(name, fn)
|
||||
return function()
|
||||
while true do
|
||||
local ok, err = pcall(fn)
|
||||
if not ok then
|
||||
log.error("TASK", "%s crashed: %s", name, tostring(err))
|
||||
sleep(5)
|
||||
log.info("TASK", "%s restarting...", name)
|
||||
else
|
||||
-- Task returned normally (shouldn't happen)
|
||||
log.warn("TASK", "%s exited unexpectedly, restarting...", name)
|
||||
sleep(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------
|
||||
-- Parallel tasks
|
||||
-----------------------------------------------
|
||||
|
||||
parallel.waitForAny(
|
||||
-- Task 1: Background inventory scanner
|
||||
function()
|
||||
resilient("Scanner", function()
|
||||
if cacheLoaded then
|
||||
pcall(ops.refreshCache)
|
||||
pcall(ops.checkAlerts)
|
||||
@@ -368,23 +389,22 @@ local function main()
|
||||
sleep(cfg.SCAN_INTERVAL)
|
||||
pcall(ops.refreshCache)
|
||||
pcall(ops.checkAlerts)
|
||||
pcall(function() itemDB.flush() end)
|
||||
pcall(function() cfg.recipeBook.flush() end)
|
||||
state.needsRedraw = true
|
||||
state.smelterNeedsRedraw = true
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 2: Barrel auto-sort
|
||||
function()
|
||||
resilient("Barrel-sort", function()
|
||||
while true do
|
||||
pcall(ops.sortBarrel)
|
||||
sleep(cfg.POLL_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 3: Auto-smelt
|
||||
function()
|
||||
resilient("Auto-smelt", function()
|
||||
while true do
|
||||
local ok, didWork = pcall(ops.autoSmelt)
|
||||
if ok and didWork then
|
||||
@@ -398,10 +418,10 @@ local function main()
|
||||
state.smelterNeedsRedraw = true
|
||||
sleep(cfg.SMELT_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 4: Defrag (consolidate partial stacks)
|
||||
function()
|
||||
resilient("Defrag", function()
|
||||
sleep(10)
|
||||
while true do
|
||||
activity.defragging = true
|
||||
@@ -411,10 +431,10 @@ local function main()
|
||||
state.needsRedraw = true
|
||||
sleep(cfg.DEFRAG_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 5: Auto-compost
|
||||
function()
|
||||
resilient("Auto-compost", function()
|
||||
while true do
|
||||
activity.composting = true
|
||||
state.needsRedraw = true
|
||||
@@ -424,10 +444,57 @@ local function main()
|
||||
pcall(ops.checkAlerts)
|
||||
sleep(cfg.COMPOST_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 5b: Auto-discard excess stock
|
||||
resilient("Auto-discard", function()
|
||||
if #cfg.TRASH_DROPPERS == 0 then
|
||||
while true do sleep(3600) end
|
||||
end
|
||||
sleep(8) -- let initial scan finish first
|
||||
log.info("DISCARD", "Auto-discard active with %d trash dropper(s)", #cfg.TRASH_DROPPERS)
|
||||
while true do
|
||||
activity.discarding = true
|
||||
state.needsRedraw = true
|
||||
pcall(ops.discardExcess)
|
||||
activity.discarding = false
|
||||
state.needsRedraw = true
|
||||
sleep(cfg.DISCARD_INTERVAL)
|
||||
end
|
||||
end),
|
||||
|
||||
-- Task 5c: Auto-craft excess items into target products
|
||||
resilient("Auto-craft", function()
|
||||
sleep(12) -- let initial scan + discard settle first
|
||||
log.info("AUTOCRAFT", "Auto-craft active (%d explicit rule(s), smart=%s)",
|
||||
#cfg.AUTO_CRAFT_RULES, tostring(cfg.AUTO_CRAFT_FROM_EXCESS))
|
||||
while true do
|
||||
if ctx.craftTurtleName then
|
||||
activity.autocrafting = true
|
||||
state.needsRedraw = true
|
||||
pcall(ops.autoCraft)
|
||||
activity.autocrafting = false
|
||||
state.needsRedraw = true
|
||||
end
|
||||
sleep(cfg.AUTO_CRAFT_INTERVAL)
|
||||
end
|
||||
end),
|
||||
|
||||
-- Task 5d: Collection hopper emptying (egg spawner, etc.)
|
||||
resilient("Hopper-collect", function()
|
||||
if #cfg.COLLECTION_HOPPERS == 0 then
|
||||
while true do sleep(3600) end
|
||||
end
|
||||
sleep(3)
|
||||
log.info("COLLECT", "Hopper collection active for %d hopper(s)", #cfg.COLLECTION_HOPPERS)
|
||||
while true do
|
||||
pcall(ops.collectHoppers)
|
||||
sleep(cfg.COLLECTION_INTERVAL)
|
||||
end
|
||||
end),
|
||||
|
||||
-- Task 6: Low-stock alert checker
|
||||
function()
|
||||
resilient("Alert-checker", function()
|
||||
sleep(5)
|
||||
pcall(ops.checkAlerts)
|
||||
state.needsRedraw = true
|
||||
@@ -436,10 +503,10 @@ local function main()
|
||||
pcall(ops.checkAlerts)
|
||||
state.needsRedraw = true
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 7: Main dashboard redraw (event-driven, polls 0.1s)
|
||||
function()
|
||||
resilient("Dashboard", function()
|
||||
state.needsRedraw = true
|
||||
while true do
|
||||
if state.needsRedraw then
|
||||
@@ -459,10 +526,10 @@ local function main()
|
||||
end
|
||||
sleep(0.1)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 8: Smelter dashboard redraw
|
||||
function()
|
||||
resilient("Smelter-dashboard", function()
|
||||
state.smelterNeedsRedraw = true
|
||||
while true do
|
||||
if state.smelterNeedsRedraw then
|
||||
@@ -472,10 +539,10 @@ local function main()
|
||||
end
|
||||
sleep(0.1)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 9: Touch event listener (both monitors)
|
||||
function()
|
||||
resilient("Touch-listener", function()
|
||||
while true do
|
||||
local event, side, x, y = os.pullEvent("monitor_touch")
|
||||
if display.smelterMonName and side == display.smelterMonName then
|
||||
@@ -486,20 +553,20 @@ local function main()
|
||||
display.handleTouch(x, y)
|
||||
end
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 10: Network state broadcast (skips if nothing changed)
|
||||
function()
|
||||
resilient("Broadcast", function()
|
||||
while true do
|
||||
if state.stateVersion ~= state.lastBroadcastVersion then
|
||||
pcall(broadcastState)
|
||||
end
|
||||
sleep(cfg.BROADCAST_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 11: Peripheral detach handler
|
||||
function()
|
||||
resilient("Detach-handler", function()
|
||||
while true do
|
||||
local event, name = os.pullEvent("peripheral_detach")
|
||||
if name then
|
||||
@@ -512,10 +579,10 @@ local function main()
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 11b: Peripheral attach handler (auto-detect crafting turtle)
|
||||
function()
|
||||
resilient("Attach-handler", function()
|
||||
while true do
|
||||
local event, name = os.pullEvent("peripheral_attach")
|
||||
if name and name:match("^turtle_") and not ctx.craftTurtleName then
|
||||
@@ -524,10 +591,10 @@ local function main()
|
||||
pcall(broadcastState)
|
||||
end
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 12: Supply chest (builder / manifest-based stocking)
|
||||
function()
|
||||
resilient("Supply-chest", function()
|
||||
if cfg.SUPPLY_CHEST == "" or #cfg.SUPPLY_MANIFEST == 0 then
|
||||
while true do sleep(3600) end
|
||||
end
|
||||
@@ -536,10 +603,10 @@ local function main()
|
||||
pcall(ops.supplyChest)
|
||||
sleep(cfg.SUPPLY_INTERVAL)
|
||||
end
|
||||
end,
|
||||
end),
|
||||
|
||||
-- Task 13: Network order/command listener
|
||||
function()
|
||||
resilient("Network-listener", function()
|
||||
if not ctx.networkModem then
|
||||
while true do sleep(3600) end
|
||||
end
|
||||
@@ -798,7 +865,7 @@ local function main()
|
||||
end -- idempotency else
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ local POLL_INTERVAL = 0.5 -- seconds between command polls
|
||||
local STATE_INTERVAL = 1 -- seconds between state forwards
|
||||
|
||||
-- Modem channels (must match inventoryManager.lua)
|
||||
local BROADCAST_CHANNEL = 4200
|
||||
local ORDER_CHANNEL = 4201
|
||||
local BROADCAST_CHANNEL = 4200
|
||||
local ORDER_CHANNEL = 4201
|
||||
local BRIDGE_REPLY_CHANNEL = 4206 -- dedicated reply channel for bridge
|
||||
|
||||
-------------------------------------------------
|
||||
-- Load config from file if present
|
||||
@@ -72,6 +73,7 @@ local function findModem()
|
||||
modem = peripheral.wrap(name)
|
||||
modemName = name
|
||||
modem.open(BROADCAST_CHANNEL)
|
||||
modem.open(BRIDGE_REPLY_CHANNEL)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -100,6 +102,7 @@ local function httpPost(path, body)
|
||||
if ok then
|
||||
return result
|
||||
end
|
||||
print(string.format("[ERR] HTTP POST %s: %s", path, tostring(result)))
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -119,6 +122,7 @@ local function httpGet(path)
|
||||
if ok then
|
||||
return result
|
||||
end
|
||||
print(string.format("[ERR] HTTP GET %s: %s", path, tostring(result)))
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -144,7 +148,7 @@ local function processCommand(cmd)
|
||||
print(string.format("[CMD] %s", action))
|
||||
|
||||
if action == "order" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "order",
|
||||
commandId = cmd.commandId,
|
||||
itemName = cmd.itemName,
|
||||
@@ -152,45 +156,81 @@ local function processCommand(cmd)
|
||||
dropperName = cmd.dropperName,
|
||||
})
|
||||
elseif action == "scan" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "scan",
|
||||
commandId = cmd.commandId,
|
||||
})
|
||||
elseif action == "toggle_pause" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "toggle_pause",
|
||||
commandId = cmd.commandId,
|
||||
})
|
||||
elseif action == "toggle_recipe" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "toggle_recipe",
|
||||
commandId = cmd.commandId,
|
||||
recipe = cmd.recipe,
|
||||
})
|
||||
elseif action == "enable_all" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "enable_all",
|
||||
commandId = cmd.commandId,
|
||||
})
|
||||
elseif action == "disable_all" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "disable_all",
|
||||
commandId = cmd.commandId,
|
||||
})
|
||||
elseif action == "sort_barrel" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "sort_barrel",
|
||||
commandId = cmd.commandId,
|
||||
barrelName = cmd.barrelName,
|
||||
})
|
||||
elseif action == "craft" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "craft",
|
||||
commandId = cmd.commandId,
|
||||
recipeIdx = cmd.recipeIdx,
|
||||
})
|
||||
elseif action == "recursive_craft" then
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "recursive_craft",
|
||||
commandId = cmd.commandId,
|
||||
itemName = cmd.itemName,
|
||||
count = cmd.count,
|
||||
})
|
||||
elseif action == "learn_crafting_recipe" then
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "learn_crafting_recipe",
|
||||
commandId = cmd.commandId,
|
||||
output = cmd.output,
|
||||
count = cmd.count,
|
||||
grid = cmd.grid,
|
||||
})
|
||||
elseif action == "learn_smelting_recipe" then
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "learn_smelting_recipe",
|
||||
commandId = cmd.commandId,
|
||||
input = cmd.input,
|
||||
result = cmd.result,
|
||||
furnaces = cmd.furnaces,
|
||||
})
|
||||
elseif action == "forget_recipe" then
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "forget_recipe",
|
||||
commandId = cmd.commandId,
|
||||
recipe = cmd.recipe,
|
||||
})
|
||||
elseif action == "sync_disabled_recipes" then
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "sync_disabled_recipes",
|
||||
commandId = cmd.commandId,
|
||||
disabledRecipes = cmd.disabledRecipes,
|
||||
smeltingPaused = cmd.smeltingPaused,
|
||||
})
|
||||
elseif action == "reboot" then
|
||||
modem.transmit(ORDER_CHANNEL, BROADCAST_CHANNEL, {
|
||||
modem.transmit(ORDER_CHANNEL, BRIDGE_REPLY_CHANNEL, {
|
||||
type = "reboot",
|
||||
commandId = cmd.commandId,
|
||||
target = cmd.target or "all",
|
||||
@@ -212,6 +252,22 @@ local function modemListener()
|
||||
if message.type == "state" then
|
||||
latestState = message
|
||||
end
|
||||
elseif channel == BRIDGE_REPLY_CHANNEL and type(message) == "table" then
|
||||
-- Forward command results back to web server
|
||||
local resultType = message.type
|
||||
if resultType == "order_result" or resultType == "craft_result"
|
||||
or resultType == "recursive_craft_result" or resultType == "find_item_result" then
|
||||
local fwdOk, fwdErr = pcall(httpPost, "/api/bridge/result", {
|
||||
action = resultType,
|
||||
commandId = message.commandId,
|
||||
success = message.success,
|
||||
message = message.message,
|
||||
error = message.error,
|
||||
})
|
||||
if not fwdOk then
|
||||
print(string.format("[ERR] Forward result %s: %s", resultType, tostring(fwdErr)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -221,7 +277,7 @@ local function stateForwarder()
|
||||
while running do
|
||||
local ok, err = pcall(forwardState)
|
||||
if not ok then
|
||||
-- Connection error, will retry
|
||||
print(string.format("[ERR] State forward: %s", tostring(err)))
|
||||
end
|
||||
sleep(STATE_INTERVAL)
|
||||
end
|
||||
@@ -240,7 +296,10 @@ local function commandPoller()
|
||||
for _, cmd in ipairs(result.commands) do
|
||||
local cmdId = cmd.id or 0
|
||||
if cmdId > lastProcessedId then
|
||||
pcall(processCommand, cmd)
|
||||
local cmdOk, cmdErr = pcall(processCommand, cmd)
|
||||
if not cmdOk then
|
||||
print(string.format("[ERR] Process cmd %s: %s", tostring(cmd.action), tostring(cmdErr)))
|
||||
end
|
||||
if cmdId > maxId then maxId = cmdId end
|
||||
end
|
||||
end
|
||||
@@ -251,6 +310,9 @@ local function commandPoller()
|
||||
end
|
||||
end
|
||||
end)
|
||||
if not ok then
|
||||
print(string.format("[ERR] Command poll: %s", tostring(err)))
|
||||
end
|
||||
sleep(POLL_INTERVAL)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -168,7 +168,8 @@ end
|
||||
-- @param recipe crafting recipe table
|
||||
-- @param ctx context table with ops, cfg, state, log, craftTurtleName, networkModem
|
||||
-- @return success, error_message
|
||||
function craft.executeSingleCraft(recipe, ctx)
|
||||
function craft.executeSingleCraft(recipe, ctx, batches)
|
||||
batches = batches or 1
|
||||
local ops = ctx.ops
|
||||
local cfg = ctx.cfg
|
||||
local st = ctx.state
|
||||
@@ -177,11 +178,14 @@ function craft.executeSingleCraft(recipe, ctx)
|
||||
if not ctx.networkModem then return false, "No modem" end
|
||||
if not peripheral.isPresent(ctx.craftTurtleName) then return false, "Turtle offline" end
|
||||
|
||||
-- Clamp batches to 64 (max stack size per slot)
|
||||
batches = math.min(batches, 64)
|
||||
|
||||
local chests = ops.getChests()
|
||||
local slotMap = {}
|
||||
local reserved = {}
|
||||
|
||||
-- Map each grid position to a chest slot
|
||||
-- Map each grid position to a chest slot, pulling `batches` items per slot
|
||||
for gridPos = 1, 9 do
|
||||
local itemName = recipe.grid[gridPos]
|
||||
if itemName then
|
||||
@@ -195,11 +199,12 @@ function craft.executeSingleCraft(recipe, ctx)
|
||||
for slot, si in pairs(chest.list()) do
|
||||
local key = src.chest .. ":" .. slot
|
||||
if si.name == itemName and not reserved[key] then
|
||||
local pullCount = math.min(batches, si.count)
|
||||
slotMap[tostring(tSlot)] = {
|
||||
chestName = src.chest,
|
||||
chestSlot = slot,
|
||||
itemName = itemName,
|
||||
count = 1,
|
||||
count = pullCount,
|
||||
}
|
||||
reserved[key] = true
|
||||
found = true
|
||||
@@ -298,16 +303,20 @@ function craft.executeChain(targetItem, count, ctx)
|
||||
ctx.state.needsRedraw = true
|
||||
ctx.state.smelterNeedsRedraw = true
|
||||
|
||||
for batch = 1, step.count do
|
||||
local ok, batchErr = craft.executeSingleCraft(step.recipe, ctx)
|
||||
-- Batch in chunks of 64 (max stack per turtle slot)
|
||||
local remaining = step.count
|
||||
while remaining > 0 do
|
||||
local chunk = math.min(remaining, 64)
|
||||
local ok, batchErr = craft.executeSingleCraft(step.recipe, ctx, chunk)
|
||||
if not ok then
|
||||
ctx.state.activity.crafting = false
|
||||
ctx.state.needsRedraw = true
|
||||
ctx.log.error("CRAFT", "Chain failed at step %d batch %d: %s", i, batch, batchErr)
|
||||
ctx.log.error("CRAFT", "Chain failed at step %d: %s", i, batchErr)
|
||||
return false, string.format("Step %d/%d failed: %s", i, #steps, batchErr)
|
||||
end
|
||||
-- Brief pause between batches to let turtle finish
|
||||
if batch < step.count then os.sleep(0.3) end
|
||||
remaining = remaining - chunk
|
||||
-- Brief pause between chunks to let turtle finish
|
||||
if remaining > 0 then os.sleep(0.3) end
|
||||
end
|
||||
|
||||
ctx.log.info("CRAFT", "Step %d/%d complete: %s x%d", i, #steps, step.output, step.outputCount)
|
||||
|
||||
@@ -245,4 +245,22 @@ function recipeBook.count()
|
||||
return cc, sc
|
||||
end
|
||||
|
||||
--- Find all crafting recipes that use a given item as an ingredient.
|
||||
-- @param ingredientName string — the input item to search for
|
||||
-- @return array of recipe tables
|
||||
function recipeBook.findRecipesUsing(ingredientName)
|
||||
local results = {}
|
||||
for _, recipe in pairs(recipes.crafting) do
|
||||
if recipe.grid then
|
||||
for _, item in ipairs(recipe.grid) do
|
||||
if item == ingredientName then
|
||||
table.insert(results, recipe)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
return recipeBook
|
||||
|
||||
@@ -53,6 +53,26 @@ C.COMPOST_RESERVE = 128
|
||||
C.COMPOST_DROPPER = "minecraft:dropper_10"
|
||||
C.COMPOST_HOPPER = "minecraft:hopper_0"
|
||||
|
||||
-- Collection hoppers: periodically emptied into storage (e.g. egg spawner, mob farm)
|
||||
C.COLLECTION_HOPPERS = { "minecraft:hopper_1" }
|
||||
C.COLLECTION_INTERVAL = 3 -- seconds between hopper collection
|
||||
|
||||
-- Stock limits / auto-discard (overridable via config file)
|
||||
C.TRASH_DROPPERS = { -- droppers facing lava/void for destroying excess items
|
||||
"minecraft:dropper_11",
|
||||
"minecraft:dropper_12",
|
||||
"minecraft:dropper_13",
|
||||
"minecraft:dropper_14",
|
||||
"minecraft:dropper_15",
|
||||
"minecraft:dropper_16",
|
||||
}
|
||||
C.DISCARD_INTERVAL = 5 -- seconds between discard checks
|
||||
|
||||
-- Auto-craft (overridable via config file)
|
||||
C.AUTO_CRAFT_INTERVAL = 10 -- seconds between auto-craft checks
|
||||
C.AUTO_CRAFT_OUTPUT_CAP = 512 -- max items of any output before smart-craft stops crafting it
|
||||
C.AUTO_CRAFT_FROM_EXCESS = true -- auto-discover recipes for over-stocked items
|
||||
|
||||
-- Peripheral
|
||||
C.PERIPHERAL_CACHE_TTL = 5
|
||||
|
||||
@@ -113,6 +133,18 @@ function C.loadConfig()
|
||||
if cfg.compostReserve then C.COMPOST_RESERVE = cfg.compostReserve end
|
||||
if cfg.compostDropper then C.COMPOST_DROPPER = cfg.compostDropper end
|
||||
if cfg.compostHopper then C.COMPOST_HOPPER = cfg.compostHopper end
|
||||
if cfg.collectionHoppers then C.COLLECTION_HOPPERS = cfg.collectionHoppers end
|
||||
if cfg.collectionInterval then C.COLLECTION_INTERVAL = cfg.collectionInterval end
|
||||
if cfg.trashDroppers then C.TRASH_DROPPERS = cfg.trashDroppers end
|
||||
if cfg.discardInterval then C.DISCARD_INTERVAL = cfg.discardInterval end
|
||||
if cfg.autoCraftInterval then C.AUTO_CRAFT_INTERVAL = cfg.autoCraftInterval end
|
||||
if cfg.autoCraftOutputCap then C.AUTO_CRAFT_OUTPUT_CAP = cfg.autoCraftOutputCap end
|
||||
if cfg.autoCraftFromExcess ~= nil then C.AUTO_CRAFT_FROM_EXCESS = cfg.autoCraftFromExcess end
|
||||
if cfg.stockLimits then
|
||||
for item, limit in pairs(cfg.stockLimits) do
|
||||
C.STOCK_LIMITS[item] = limit -- merge / override per-item
|
||||
end
|
||||
end
|
||||
if cfg.parallelScanChunks then C.PARALLEL_SCAN_CHUNKS = cfg.parallelScanChunks end
|
||||
if cfg.chestPriority then C.CHEST_PRIORITY = cfg.chestPriority end
|
||||
if cfg.supplyChest then C.SUPPLY_CHEST = cfg.supplyChest end
|
||||
@@ -130,6 +162,8 @@ C.FUEL_LIST = dofile(_path("data/fuel.lua"))
|
||||
local _compostData = dofile(_path("data/compostable.lua"))
|
||||
C.COMPOSTABLE = _compostData.items
|
||||
C.COMPOST_TRASH = _compostData.trash
|
||||
C.STOCK_LIMITS = dofile(_path("data/stock_limits.lua"))
|
||||
C.AUTO_CRAFT_RULES = dofile(_path("data/auto_craft.lua"))
|
||||
C.LOW_STOCK_ALERTS = dofile(_path("data/alerts.lua"))
|
||||
|
||||
-- Recipe book: merges built-in recipes + user-learned recipes
|
||||
@@ -184,6 +218,10 @@ for _, f in ipairs(C.FUEL_LIST) do C.FUEL_SET[f.name] = true end
|
||||
C.COMPOSTABLE_SET = {}
|
||||
for _, name in ipairs(C.COMPOSTABLE) do C.COMPOSTABLE_SET[name] = true end
|
||||
|
||||
-- Build stock limits set for quick lookup
|
||||
C.STOCK_LIMITS_SET = {}
|
||||
for name, _ in pairs(C.STOCK_LIMITS) do C.STOCK_LIMITS_SET[name] = true end
|
||||
|
||||
return C
|
||||
|
||||
end
|
||||
|
||||
@@ -78,7 +78,9 @@ local function getActivityString()
|
||||
if activity.smelting then table.insert(parts, "SMELTING") end
|
||||
if activity.scanning then table.insert(parts, "SCANNING") end
|
||||
if activity.defragging then table.insert(parts, "DEFRAG") end
|
||||
if activity.composting then table.insert(parts, "COMPOST") end
|
||||
if activity.composting then table.insert(parts, "COMPOST") end
|
||||
if activity.discarding then table.insert(parts, "DISCARD") end
|
||||
if activity.autocrafting then table.insert(parts, "AUTOCRAFT") end
|
||||
if #parts > 0 then
|
||||
return table.concat(parts, " | ")
|
||||
end
|
||||
@@ -91,6 +93,8 @@ local function getBottomMessage()
|
||||
elseif activity.sorting then return "SORTING BARREL..."
|
||||
elseif activity.defragging then return "DEFRAGMENTING..."
|
||||
elseif activity.composting then return "COMPOSTING..."
|
||||
elseif activity.discarding then return "DISCARDING EXCESS..."
|
||||
elseif activity.autocrafting then return "AUTO-CRAFTING..."
|
||||
end
|
||||
return "Tap item to order"
|
||||
end
|
||||
|
||||
@@ -765,6 +765,279 @@ function O.autoCompost()
|
||||
return didWork
|
||||
end
|
||||
|
||||
-------------------------------------------------
|
||||
-- Collection hopper emptying (egg spawner, mob farm, etc.)
|
||||
-------------------------------------------------
|
||||
|
||||
function O.collectHoppers()
|
||||
if #cfg.COLLECTION_HOPPERS == 0 then return false end
|
||||
|
||||
local didWork = false
|
||||
local chests = O.getChestsByPriority()
|
||||
local catalogue = cache.catalogue
|
||||
|
||||
for _, hopperName in ipairs(cfg.COLLECTION_HOPPERS) do
|
||||
local hopper = O.wrapCached(hopperName)
|
||||
if hopper then
|
||||
local contents = hopper.list()
|
||||
if contents and next(contents) then
|
||||
for slot, item in pairs(contents) do
|
||||
local moved = 0
|
||||
local triedChests = {}
|
||||
|
||||
-- Prefer chests where item already exists
|
||||
if catalogue[item.name] then
|
||||
for _, entry in ipairs(catalogue[item.name]) do
|
||||
triedChests[entry.chest] = true
|
||||
local n = hopper.pushItems(entry.chest, slot)
|
||||
if n and n > 0 then
|
||||
moved = moved + n
|
||||
state.adjustCache(item.name, entry.chest, n)
|
||||
didWork = true
|
||||
log.info("COLLECT", "%s x%d -> %s (from %s)", item.name, n, entry.chest, hopperName)
|
||||
end
|
||||
if moved >= item.count then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- Overflow to any chest with space
|
||||
if moved < item.count then
|
||||
for _, chest in ipairs(chests) do
|
||||
if not triedChests[chest] then
|
||||
local n = hopper.pushItems(chest, slot)
|
||||
if n and n > 0 then
|
||||
moved = moved + n
|
||||
state.adjustCache(item.name, chest, n)
|
||||
didWork = true
|
||||
log.info("COLLECT", "%s x%d -> %s (from %s)", item.name, n, chest, hopperName)
|
||||
end
|
||||
if moved >= item.count then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return didWork
|
||||
end
|
||||
|
||||
-------------------------------------------------
|
||||
-- Auto-discard excess stock
|
||||
-------------------------------------------------
|
||||
|
||||
function O.discardExcess()
|
||||
if #cfg.TRASH_DROPPERS == 0 then return false end
|
||||
|
||||
local catalogue = cache.catalogue
|
||||
local didWork = false
|
||||
|
||||
-- Build a flat list of dropper handles + free capacity
|
||||
local droppers = {}
|
||||
for _, dName in ipairs(cfg.TRASH_DROPPERS) do
|
||||
local d = O.wrapCached(dName)
|
||||
if d then
|
||||
local used = 0
|
||||
local contents = d.list()
|
||||
if contents then
|
||||
for _, item in pairs(contents) do used = used + item.count end
|
||||
end
|
||||
local free = (d.size() * 64) - used
|
||||
if free > 0 then
|
||||
table.insert(droppers, { name = dName, handle = d, free = free })
|
||||
else
|
||||
log.debug("DISCARD", "Dropper %s is full, skipping", dName)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #droppers == 0 then
|
||||
log.debug("DISCARD", "All trash droppers full or offline, cannot discard")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Round-robin index across droppers
|
||||
local dIdx = 1
|
||||
|
||||
for itemName, maxCount in pairs(cfg.STOCK_LIMITS) do
|
||||
if catalogue[itemName] then
|
||||
local totalInStorage = 0
|
||||
for _, src in ipairs(catalogue[itemName]) do
|
||||
totalInStorage = totalInStorage + src.total
|
||||
end
|
||||
|
||||
local excess = totalInStorage - maxCount
|
||||
if excess > 0 then
|
||||
log.info("DISCARD", "%s: %d in stock, limit %d, discarding %d",
|
||||
itemName, totalInStorage, maxCount, excess)
|
||||
local discarded = 0
|
||||
local srcSnapshot = { table.unpack(catalogue[itemName]) }
|
||||
for _, source in ipairs(srcSnapshot) do
|
||||
if discarded >= excess then break end
|
||||
if source.total > 0 then
|
||||
local chest = O.wrapCached(source.chest)
|
||||
if chest then
|
||||
for slot, slotItem in pairs(chest.list()) do
|
||||
if slotItem.name == itemName then
|
||||
-- Find a dropper with free space (round-robin)
|
||||
local startIdx = dIdx
|
||||
local dropper = nil
|
||||
repeat
|
||||
if droppers[dIdx].free > 0 then
|
||||
dropper = droppers[dIdx]
|
||||
end
|
||||
dIdx = (dIdx % #droppers) + 1
|
||||
until dropper or dIdx == startIdx
|
||||
if not dropper then break end -- all droppers full
|
||||
|
||||
local batch = math.min(slotItem.count, excess - discarded, dropper.free)
|
||||
local n = chest.pushItems(dropper.name, slot, batch)
|
||||
if n and n > 0 then
|
||||
state.adjustCache(itemName, source.chest, -n)
|
||||
dropper.free = dropper.free - n
|
||||
discarded = discarded + n
|
||||
didWork = true
|
||||
end
|
||||
if discarded >= excess then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if discarded > 0 then
|
||||
log.info("DISCARD", "Discarded %s x%d", itemName, discarded)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return didWork
|
||||
end
|
||||
|
||||
-------------------------------------------------
|
||||
-- Auto-craft excess stock into target items
|
||||
-------------------------------------------------
|
||||
|
||||
function O.autoCraft()
|
||||
if not ctx.craftEngine then return false end
|
||||
if not ctx.craftTurtleName then return false end
|
||||
|
||||
local catalogue = cache.catalogue
|
||||
local didWork = false
|
||||
|
||||
-- Phase 1: Explicit rules from auto_craft.lua
|
||||
for _, rule in ipairs(cfg.AUTO_CRAFT_RULES) do
|
||||
local inputName = rule.input
|
||||
local reserve = rule.reserve or 0
|
||||
local outputName = rule.output
|
||||
|
||||
if catalogue[inputName] then
|
||||
local totalInStorage = 0
|
||||
for _, src in ipairs(catalogue[inputName]) do
|
||||
totalInStorage = totalInStorage + src.total
|
||||
end
|
||||
|
||||
local excess = totalInStorage - reserve
|
||||
if excess > 0 then
|
||||
local recipe = cfg.recipeBook.getCraftingRecipe(outputName)
|
||||
if recipe then
|
||||
local ingredients = cfg.recipeBook.getIngredients(recipe)
|
||||
local inputPerCraft = ingredients[inputName] or 0
|
||||
if inputPerCraft > 0 then
|
||||
local batches = math.floor(excess / inputPerCraft)
|
||||
batches = math.min(batches, 64)
|
||||
if batches > 0 then
|
||||
local craftCount = batches * recipe.count
|
||||
log.info("AUTOCRAFT", "%s: %d excess (reserve %d), crafting %d x %s",
|
||||
inputName, excess, reserve, craftCount, outputName)
|
||||
local ok, err = O.recursiveCraft(outputName, craftCount)
|
||||
if ok then
|
||||
didWork = true
|
||||
log.info("AUTOCRAFT", "Crafted %s x%d", outputName, craftCount)
|
||||
else
|
||||
log.warn("AUTOCRAFT", "Failed to craft %s: %s", outputName, tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
log.warn("AUTOCRAFT", "No recipe found for output: %s", outputName)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Phase 2: Smart excess-to-craft — auto-discover recipes for over-stocked items
|
||||
if cfg.AUTO_CRAFT_FROM_EXCESS then
|
||||
-- Track outputs we've already handled via explicit rules to avoid duplicates
|
||||
local handledOutputs = {}
|
||||
for _, rule in ipairs(cfg.AUTO_CRAFT_RULES) do
|
||||
handledOutputs[rule.output] = true
|
||||
end
|
||||
|
||||
for itemName, maxCount in pairs(cfg.STOCK_LIMITS) do
|
||||
if catalogue[itemName] then
|
||||
local totalInStorage = 0
|
||||
for _, src in ipairs(catalogue[itemName]) do
|
||||
totalInStorage = totalInStorage + src.total
|
||||
end
|
||||
|
||||
local excess = totalInStorage - maxCount
|
||||
if excess > 0 then
|
||||
-- Find all crafting recipes that use this item
|
||||
local usingRecipes = cfg.recipeBook.findRecipesUsing(itemName)
|
||||
for _, recipe in ipairs(usingRecipes) do
|
||||
if not handledOutputs[recipe.output] then
|
||||
-- Check how much of the output we already have
|
||||
local outputTotal = O.getItemTotal(recipe.output)
|
||||
if outputTotal < cfg.AUTO_CRAFT_OUTPUT_CAP then
|
||||
local ingredients = cfg.recipeBook.getIngredients(recipe)
|
||||
local inputPerCraft = ingredients[itemName] or 0
|
||||
if inputPerCraft > 0 then
|
||||
-- Only craft up to the output cap
|
||||
local outputRoom = cfg.AUTO_CRAFT_OUTPUT_CAP - outputTotal
|
||||
local maxBatches = math.floor(excess / inputPerCraft)
|
||||
local batchesByRoom = math.ceil(outputRoom / recipe.count)
|
||||
local batches = math.min(maxBatches, batchesByRoom, 64)
|
||||
if batches > 0 then
|
||||
-- Check all other ingredients are available
|
||||
local canCraft = true
|
||||
for ingr, needed in pairs(ingredients) do
|
||||
if ingr ~= itemName then
|
||||
local have = O.getItemTotal(ingr)
|
||||
if have < needed * batches then
|
||||
canCraft = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if canCraft then
|
||||
local craftCount = batches * recipe.count
|
||||
log.info("AUTOCRAFT", "Smart: %s over limit, crafting %d x %s",
|
||||
itemName, craftCount, recipe.output)
|
||||
local ok, err = O.recursiveCraft(recipe.output, craftCount)
|
||||
if ok then
|
||||
didWork = true
|
||||
handledOutputs[recipe.output] = true
|
||||
log.info("AUTOCRAFT", "Smart-crafted %s x%d", recipe.output, craftCount)
|
||||
-- Recalculate excess after crafting
|
||||
break
|
||||
else
|
||||
log.warn("AUTOCRAFT", "Smart craft failed %s: %s", recipe.output, tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return didWork
|
||||
end
|
||||
|
||||
-------------------------------------------------
|
||||
-- Low-stock alert checker
|
||||
-------------------------------------------------
|
||||
@@ -952,7 +1225,8 @@ function O.getMissingIngredients(recipe)
|
||||
return ui.getMissingIngredients(recipe, O.getItemTotal)
|
||||
end
|
||||
|
||||
function O.craftItem(recipeIdx)
|
||||
function O.craftItem(recipeIdx, batches)
|
||||
batches = batches or 1
|
||||
local recipe = cfg.CRAFTABLE[recipeIdx]
|
||||
if not recipe then
|
||||
log.error("CRAFT", "Invalid recipe index: %s", tostring(recipeIdx))
|
||||
@@ -971,7 +1245,11 @@ function O.craftItem(recipeIdx)
|
||||
return false, "Turtle offline"
|
||||
end
|
||||
|
||||
log.info("CRAFT", "Starting craft: %s (turtle: %s)", recipe.output, ctx.craftTurtleName)
|
||||
-- Clamp batches to 64 (max stack size per slot)
|
||||
batches = math.min(batches, 64)
|
||||
|
||||
log.info("CRAFT", "Starting craft: %s x%d (%d batches, turtle: %s)",
|
||||
recipe.output, batches * recipe.count, batches, ctx.craftTurtleName)
|
||||
|
||||
activity.crafting = true
|
||||
state.needsRedraw = true
|
||||
@@ -981,6 +1259,27 @@ function O.craftItem(recipeIdx)
|
||||
local slotMap = {}
|
||||
local reservedSlots = {}
|
||||
|
||||
-- Sum how many of each ingredient we need total
|
||||
local ingredientTotals = {}
|
||||
for gridPos = 1, 9 do
|
||||
local itemName = recipe.grid[gridPos]
|
||||
if itemName then
|
||||
ingredientTotals[itemName] = (ingredientTotals[itemName] or 0) + batches
|
||||
end
|
||||
end
|
||||
|
||||
-- Check we have enough of each ingredient
|
||||
for itemName, needed in pairs(ingredientTotals) do
|
||||
local have = O.getItemTotal(itemName)
|
||||
if have < needed then
|
||||
log.error("CRAFT", "Not enough %s: have %d, need %d", itemName, have, needed)
|
||||
activity.crafting = false
|
||||
state.needsRedraw = true
|
||||
state.smelterNeedsRedraw = true
|
||||
return false, string.format("Need %d %s, have %d", needed, itemName, have)
|
||||
end
|
||||
end
|
||||
|
||||
for gridPos = 1, 9 do
|
||||
local itemName = recipe.grid[gridPos]
|
||||
if itemName then
|
||||
@@ -994,11 +1293,12 @@ function O.craftItem(recipeIdx)
|
||||
for slot, slotItem in pairs(chest.list()) do
|
||||
local key = source.chest .. ":" .. slot
|
||||
if slotItem.name == itemName and not reservedSlots[key] then
|
||||
local pullCount = math.min(batches, slotItem.count)
|
||||
slotMap[tostring(turtleSlot)] = {
|
||||
chestName = source.chest,
|
||||
chestSlot = slot,
|
||||
itemName = itemName,
|
||||
count = 1,
|
||||
count = pullCount,
|
||||
}
|
||||
reservedSlots[key] = true
|
||||
found = true
|
||||
|
||||
@@ -41,6 +41,8 @@ S.activity = {
|
||||
defragging = false,
|
||||
composting = false,
|
||||
crafting = false,
|
||||
discarding = false,
|
||||
autocrafting = false,
|
||||
}
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
@@ -12,11 +12,16 @@ local FILES = {
|
||||
["manager/display.lua"] = "manager/display.lua",
|
||||
["lib/log.lua"] = "lib/log.lua",
|
||||
["lib/ui.lua"] = "lib/ui.lua",
|
||||
["lib/itemDB.lua"] = "lib/itemDB.lua",
|
||||
["lib/craft.lua"] = "lib/craft.lua",
|
||||
["lib/recipeBook.lua"] = "lib/recipeBook.lua",
|
||||
["data/smeltable.lua"] = "data/smeltable.lua",
|
||||
["data/fuel.lua"] = "data/fuel.lua",
|
||||
["data/compostable.lua"] = "data/compostable.lua",
|
||||
["data/craftable.lua"] = "data/craftable.lua",
|
||||
["data/alerts.lua"] = "data/alerts.lua",
|
||||
["data/stock_limits.lua"] = "data/stock_limits.lua",
|
||||
["data/auto_craft.lua"] = "data/auto_craft.lua",
|
||||
}
|
||||
|
||||
-------------------------------------------------
|
||||
|
||||
@@ -55,4 +55,28 @@ print("")
|
||||
print("Starting miningTurtle...")
|
||||
sleep(1)
|
||||
|
||||
shell.run("miningTurtle.lua")
|
||||
-- Reboot listener: reboots this turtle on remote command
|
||||
local SYSTEM_CHANNEL = 4205
|
||||
local ROLE = "miner"
|
||||
|
||||
local function rebootListener()
|
||||
local m = peripheral.find("modem")
|
||||
if not m then return end
|
||||
m.open(SYSTEM_CHANNEL)
|
||||
while true do
|
||||
local _, _, channel, _, message = os.pullEvent("modem_message")
|
||||
if channel == SYSTEM_CHANNEL and type(message) == "table" and message.type == "reboot" then
|
||||
local target = message.target or "all"
|
||||
if target == "all" or target == ROLE or target == tostring(os.getComputerID()) then
|
||||
print("[SYSTEM] Reboot command received. Rebooting...")
|
||||
sleep(0.5)
|
||||
os.reboot()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parallel.waitForAny(
|
||||
function() shell.run("miningTurtle.lua") end,
|
||||
rebootListener
|
||||
)
|
||||
|
||||
@@ -588,6 +588,122 @@ app.post('/api/craft', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Recursive craft (multi-step crafting chain)
|
||||
app.post('/api/recursive-craft', (req, res) => {
|
||||
try {
|
||||
const { itemName, count, commandId } = req.body;
|
||||
|
||||
const cached = checkIdempotent(commandId);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
if (!itemName || typeof itemName !== 'string' || itemName.length > 200) {
|
||||
return res.status(400).json({ error: 'Missing or invalid itemName' });
|
||||
}
|
||||
const parsedCount = parseInt(count);
|
||||
if (!Number.isFinite(parsedCount) || parsedCount < 1 || parsedCount > 100000) {
|
||||
return res.status(400).json({ error: 'Invalid count (1–100000)' });
|
||||
}
|
||||
|
||||
pushCommandToBridge({ type: 'command', action: 'recursive_craft', commandId, itemName, count: parsedCount });
|
||||
const result = { success: true, commandId, message: `Recursive craft sent: ${itemName} x${count}` };
|
||||
recordCommand(commandId, result);
|
||||
console.log(`🔨 Recursive craft: ${itemName} x${count}`);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Learn a crafting recipe
|
||||
app.post('/api/recipes/learn-crafting', (req, res) => {
|
||||
try {
|
||||
const { output, count, grid, commandId } = req.body;
|
||||
|
||||
const cached = checkIdempotent(commandId);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
if (!output || typeof output !== 'string' || output.length > 200) {
|
||||
return res.status(400).json({ error: 'Missing or invalid output' });
|
||||
}
|
||||
if (!grid || !Array.isArray(grid) || grid.length !== 9) {
|
||||
return res.status(400).json({ error: 'grid must be an array of 9 items' });
|
||||
}
|
||||
|
||||
pushCommandToBridge({ type: 'command', action: 'learn_crafting_recipe', commandId, output, count: count || 1, grid });
|
||||
const result = { success: true, commandId, message: `Learned crafting recipe: ${output}` };
|
||||
recordCommand(commandId, result);
|
||||
console.log(`📖 Learn crafting recipe: ${output}`);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Learn a smelting recipe
|
||||
app.post('/api/recipes/learn-smelting', (req, res) => {
|
||||
try {
|
||||
const { input, result: recipeResult, furnaces, commandId } = req.body;
|
||||
|
||||
const cached = checkIdempotent(commandId);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
if (!input || typeof input !== 'string' || input.length > 200) {
|
||||
return res.status(400).json({ error: 'Missing or invalid input' });
|
||||
}
|
||||
if (!recipeResult || typeof recipeResult !== 'string' || recipeResult.length > 200) {
|
||||
return res.status(400).json({ error: 'Missing or invalid result' });
|
||||
}
|
||||
|
||||
pushCommandToBridge({ type: 'command', action: 'learn_smelting_recipe', commandId, input, result: recipeResult, furnaces });
|
||||
const apiResult = { success: true, commandId, message: `Learned smelting recipe: ${input} → ${recipeResult}` };
|
||||
recordCommand(commandId, apiResult);
|
||||
console.log(`📖 Learn smelting recipe: ${input} → ${recipeResult}`);
|
||||
res.json(apiResult);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Forget a recipe
|
||||
app.post('/api/recipes/forget', (req, res) => {
|
||||
try {
|
||||
const { recipe, commandId } = req.body;
|
||||
|
||||
const cached = checkIdempotent(commandId);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
if (!recipe || typeof recipe !== 'string' || recipe.length > 200) {
|
||||
return res.status(400).json({ error: 'Missing or invalid recipe name' });
|
||||
}
|
||||
|
||||
pushCommandToBridge({ type: 'command', action: 'forget_recipe', commandId, recipe });
|
||||
const result = { success: true, commandId, message: `Forget recipe: ${recipe}` };
|
||||
recordCommand(commandId, result);
|
||||
console.log(`🗑️ Forget recipe: ${recipe}`);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Sync disabled recipes state
|
||||
app.post('/api/recipes/sync', (req, res) => {
|
||||
try {
|
||||
const { disabledRecipes, smeltingPaused, commandId } = req.body;
|
||||
|
||||
const cached = checkIdempotent(commandId);
|
||||
if (cached) return res.json(cached);
|
||||
|
||||
pushCommandToBridge({ type: 'command', action: 'sync_disabled_recipes', commandId, disabledRecipes, smeltingPaused });
|
||||
const result = { success: true, commandId, message: 'Synced recipe state' };
|
||||
recordCommand(commandId, result);
|
||||
console.log('🔄 Sync disabled recipes');
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Bridge Endpoints (for HTTP polling fallback) ==========
|
||||
|
||||
// Bridge sends inventory state
|
||||
|
||||
Reference in New Issue
Block a user