fix: accesscontrol toast; feature: autocraft basic; reconstruct: autocraft

fix:
- accesscontrol send toast failed
- advanced peripherals BlockDetailData wrong record type
feature:
- autocraft support multi package craft
- autocraft more fast craft speed
reconstruct:
- CraftManager algorithm
- autocraft logic
This commit is contained in:
2025-10-26 20:19:49 +08:00
parent 119bc1997a
commit 2ab091d939
5 changed files with 369 additions and 148 deletions

View File

@@ -124,9 +124,9 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat( const noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
config.usersGroups config.usersGroups
.filter((value) => value.isNotice) .filter((value) => value.isNotice)
.map((value) => value.groupUsers ?? []) .flatMap((value) => value.groupUsers ?? []),
.flat(),
); );
logger.debug(`noticeTargetPlayers: ${noticeTargetPlayers.join(", ")}`);
for (const targetPlayer of noticeTargetPlayers) { for (const targetPlayer of noticeTargetPlayers) {
if (!onlinePlayers.includes(targetPlayer)) continue; if (!onlinePlayers.includes(targetPlayer)) continue;
@@ -134,6 +134,7 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
name: player, name: player,
info: playerInfo, info: playerInfo,
}); });
sleep(1);
} }
releaser.release(); releaser.release();
} }

View File

@@ -1,22 +1,30 @@
import { CraftManager } from "@/lib/CraftManager"; import {
import { CCLog } from "@/lib/ccLog"; CraftManager,
CraftRecipe,
CreatePackageTag,
} from "@/lib/CraftManager";
import { CCLog, LogLevel } from "@/lib/ccLog";
import { Queue } from "@/lib/datatype/Queue";
const logger = new CCLog("autocraft.log"); const logger = new CCLog("autocraft.log", { outputMinLevel: LogLevel.Info });
const peripheralsNames = { const peripheralsNames = {
packagesContainer: "minecraft:chest_10", // packsInventory: "minecraft:chest_14",
itemsContainer: "minecraft:chest_9", // itemsInventory: "minecraft:chest_15",
packageExtractor: "create:packager_1", // packageExtractor: "create:packager_3",
blockReader: "front", blockReader: "bottom",
wiredModem: "back", wiredModem: "right",
redstone: "front", redstone: "left",
packsInventory: "minecraft:chest_1121",
itemsInventory: "minecraft:chest_1120",
packageExtractor: "create:packager_0",
}; };
const packagesContainer = peripheral.wrap( const packsInventory = peripheral.wrap(
peripheralsNames.packagesContainer, peripheralsNames.packsInventory,
) as InventoryPeripheral; ) as InventoryPeripheral;
const itemsContainer = peripheral.wrap( const itemsInventory = peripheral.wrap(
peripheralsNames.itemsContainer, peripheralsNames.itemsInventory,
) as InventoryPeripheral; ) as InventoryPeripheral;
const packageExtractor = peripheral.wrap( const packageExtractor = peripheral.wrap(
peripheralsNames.packageExtractor, peripheralsNames.packageExtractor,
@@ -31,87 +39,170 @@ const turtleLocalName = wiredModem.getNameLocal();
enum State { enum State {
IDLE, IDLE,
CHECK_PACK,
READ_RECIPE, READ_RECIPE,
PULL_ITEMS,
CRAFT_OUTPUT, CRAFT_OUTPUT,
} }
function main() { function main() {
const craftManager = new CraftManager(turtleLocalName); const craftManager = new CraftManager(turtleLocalName, itemsInventory);
const recipesQueue = new Queue<CraftRecipe>();
const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>();
let currentState = State.IDLE;
let nextState = State.IDLE;
let hasPackage = redstone.getInput(peripheralsNames.redstone); let hasPackage = redstone.getInput(peripheralsNames.redstone);
// let currentState = State.IDLE; while (hasPackage) {
// let nextState = State.IDLE; hasPackage = redstone.getInput(peripheralsNames.redstone);
logger.warn("redstone activated when init, please clear inventory");
sleep(1);
}
logger.info("AutoCraft init finished..."); logger.info("AutoCraft init finished...");
while (true) { while (true) {
if (!hasPackage) os.pullEvent("redstone"); // Switch state
hasPackage = redstone.getInput(peripheralsNames.redstone); switch (currentState) {
if (!hasPackage) { case State.IDLE: {
continue; nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
break;
}
case State.READ_RECIPE: {
nextState = hasPackage ? State.READ_RECIPE : State.CRAFT_OUTPUT;
break;
}
case State.CRAFT_OUTPUT: {
nextState =
recipesQueue.size() > 0
? State.CRAFT_OUTPUT
: hasPackage
? State.READ_RECIPE
: State.IDLE;
break;
}
default: {
logger.error(`Unknown state`);
nextState = hasPackage ? State.READ_RECIPE : State.IDLE;
break;
}
} }
logger.info(`Package detected`);
const itemsInfo = packagesContainer.list(); // State logic
for (const key in itemsInfo) { switch (currentState) {
const slot = parseInt(key); case State.IDLE: {
const item = itemsInfo[slot]; if (!hasPackage) os.pullEvent("redstone");
logger.info(`${item.count}x ${item.name} in slot ${key}`); hasPackage = redstone.getInput(peripheralsNames.redstone);
break;
// Get package NBT
packagesContainer.pushItems(turtleLocalName, slot);
const packageInfo = blockReader.getBlockData()!.Items[1];
// log.info(textutils.serialise(packageInfo));
// Get recipe
const packageRecipes = CraftManager.getPackageRecipe(packageInfo);
// No recipe, just extract package
if (packageRecipes.isNone()) {
packageExtractor.pullItems(turtleLocalName, 1);
logger.info(`No recipe, just pass`);
continue;
} }
// Extract package case State.READ_RECIPE: {
// log.info(`Get recipe ${textutils.serialise(recipe)}`); logger.info(`Package detected`);
packageExtractor.pullItems(turtleLocalName, 1); const packagesInfoRecord = packsInventory.list();
for (const key in packagesInfoRecord) {
const slotNum = parseInt(key);
packsInventory.pushItems(turtleLocalName, slotNum);
// Get package NBT
logger.debug(
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
);
const packageDetailInfo = blockReader.getBlockData()?.Items[1];
if (packageDetailInfo === undefined) {
logger.error(`Package detail info not found`);
continue;
}
// Get OrderId and isFinal
const packageOrderId = (packageDetailInfo.tag as CreatePackageTag)
.Fragment.OrderId;
const packageIsFinal =
(packageDetailInfo.tag as CreatePackageTag).Fragment.IsFinal > 0
? true
: false;
// Get recipe
const packageRecipes =
CraftManager.getPackageRecipe(packageDetailInfo);
if (packageRecipes.isSome()) {
if (packageIsFinal) recipesQueue.enqueue(packageRecipes.value);
else recipesWaitingMap.set(packageOrderId, packageRecipes.value);
} else {
if (packageIsFinal && recipesWaitingMap.has(packageOrderId)) {
recipesQueue.enqueue(recipesWaitingMap.get(packageOrderId)!);
recipesWaitingMap.delete(packageOrderId);
} else {
logger.debug(`No recipe, just pass`);
}
}
packageExtractor.pullItems(turtleLocalName, 1);
}
if (
currentState === State.READ_RECIPE &&
nextState === State.CRAFT_OUTPUT
) {
craftManager.initItemsMap();
}
break;
}
case State.CRAFT_OUTPUT: {
// Check recipe
const recipe = recipesQueue.dequeue();
if (recipe === undefined) break;
// Pull and craft multi recipe
for (const recipe of packageRecipes.value) {
let craftOutputItem: BlockItemDetailData | undefined = undefined;
let restCraftCnt = recipe.Count; let restCraftCnt = recipe.Count;
let maxSignleCraftCnt = restCraftCnt;
let craftItemDetail: ItemDetail | undefined = undefined;
do { do {
// Clear workbench // Clear workbench
craftManager.pushAll(itemsContainer); craftManager.clearTurtle();
logger.info(`Pull items according to a recipe`); logger.info(`Pull items according to a recipe`);
const craftCnt = craftManager const craftCnt = craftManager
.pullItems(recipe, itemsContainer, restCraftCnt) .pullItemsWithRecipe(recipe, maxSignleCraftCnt)
.unwrapOrElse((error) => { .unwrapOrElse((error) => {
logger.error(error.message); logger.error(error.message);
return 0; return 0;
}); });
if (craftCnt == 0) break; if (craftCnt == 0) break;
craftManager.craft(); if (craftCnt < maxSignleCraftCnt) maxSignleCraftCnt = craftCnt;
const craftRet = craftManager.craft(maxSignleCraftCnt);
craftItemDetail ??= craftRet;
logger.info(`Craft ${craftCnt} times`); logger.info(`Craft ${craftCnt} times`);
restCraftCnt -= craftCnt; restCraftCnt -= craftCnt;
// Get output item
craftOutputItem ??= blockReader.getBlockData()!.Items[1];
} while (restCraftCnt > 0); } while (restCraftCnt > 0);
// Finally output // Finally output
if (restCraftCnt > 0) { if (restCraftCnt > 0) {
logger.warn(`Only craft ${recipe.Count - restCraftCnt} times`); logger.warn(
`Only craft ${recipe.Count - restCraftCnt}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
} else { } else {
logger.info(`Finish craft ${recipe.Count}x ${craftOutputItem?.id}`); logger.info(
`Finish craft ${recipe.Count}x ${craftItemDetail?.name ?? "UnknownItem"}`,
);
} }
craftManager.pushAll(itemsContainer);
// Clear workbench and inventory
const turtleItemSlots = Object.values(
blockReader.getBlockData()!.Items,
).map((val) => val.Slot + 1);
craftManager.clearTurtle(turtleItemSlots);
break;
}
default: {
sleep(1);
break;
} }
} }
// Check packages
hasPackage = redstone.getInput(peripheralsNames.redstone);
// State update
currentState = nextState;
} }
} }

View File

@@ -8,23 +8,52 @@ import { Result, Ok, Err, Option, Some, None } from "./thirdparty/ts-result-es";
// 13, 14, 15, 16 // 13, 14, 15, 16
const TURTLE_SIZE = 16; const TURTLE_SIZE = 16;
const CRAFT_OUTPUT_SLOT = 4;
// const CRAFT_SLOT_CNT = 9; // const CRAFT_SLOT_CNT = 9;
const CRAFT_SLOT_TABLE: number[] = [1, 2, 3, 5, 6, 7, 9, 10, 11]; const CRAFT_SLOT_TABLE: number[] = [1, 2, 3, 5, 6, 7, 9, 10, 11];
// const REST_SLOT_CNT = 7; // const REST_SLOT_CNT = 7;
// const REST_SLOT_TABLE: number[] = [4, 8, 12, 13, 14, 15, 16]; // const REST_SLOT_TABLE: number[] = [4, 8, 12, 13, 14, 15, 16];
/**
* Represents the NBT data of a Create mod package. This data is used for managing crafting and logistics,
* especially in the context of multi-step crafting orders.
* The structure is inspired by the logic in Create's own packaging and repackaging helpers.
* @see https://github.com/Creators-of-Create/Create/blob/mc1.21.1/dev/src/main/java/com/simibubi/create/content/logistics/packager/repackager/PackageRepackageHelper.java
*/
interface CreatePackageTag { interface CreatePackageTag {
/**
* The items contained within this package.
*/
Items: { Items: {
/**
* A list of the items stored in the package.
*/
Items: { Items: {
id: string; id: string;
Count: number; Count: number;
Slot: number; Slot: number;
}[]; }[];
/**
* The number of slots in the package's inventory.
*/
Size: number; Size: number;
}; };
/**
* Information about this package's role as a fragment of a larger crafting order.
* This is used to track progress and manage dependencies in a distributed crafting system.
*/
Fragment: { Fragment: {
/**
* The index of this fragment within the larger order.
*/
Index: number; Index: number;
/**
* The context of the overall order this fragment belongs to.
*/
OrderContext: { OrderContext: {
/**
* A list of crafting recipes required for the order.
*/
OrderedCrafts: { OrderedCrafts: {
Pattern: { Pattern: {
Entries: { Entries: {
@@ -38,6 +67,9 @@ interface CreatePackageTag {
}; };
Count: number; Count: number;
}[]; }[];
/**
* A list of pre-existing item stacks required for the order.
*/
OrderedStacks: { OrderedStacks: {
Entries: { Entries: {
Item: { Item: {
@@ -48,11 +80,26 @@ interface CreatePackageTag {
}[]; }[];
}; };
}; };
/**
* Whether this is the final fragment in the sequence for this specific part of the order.
*/
IsFinal: number; IsFinal: number;
/**
* The unique identifier for the overall order.
*/
OrderId: number; OrderId: number;
/**
* The index of this package in a linked list of packages for the same order.
*/
LinkIndex: number; LinkIndex: number;
/**
* Whether this is the last package in the linked list.
*/
IsFinalLink: number; IsFinalLink: number;
}; };
/**
* The destination address for this package.
*/
Address: string; Address: string;
} }
@@ -71,17 +118,25 @@ interface CraftRecipe {
interface InventorySlotInfo { interface InventorySlotInfo {
name: string; name: string;
count: number; slotCountQueue: Queue<{
slotNum: number;
count: number;
}>;
maxCount: number; maxCount: number;
slotNum: number;
} }
type CraftMode = "keep" | "keepProduct" | "keepIngredient"; type CraftMode = "keep" | "keepProduct" | "keepIngredient";
class CraftManager { class CraftManager {
private localName: string; private localName: string;
private inventory: InventoryPeripheral;
constructor(modem: WiredModemPeripheral | string) { private inventoryItemsMap = new Map<string, InventorySlotInfo>();
constructor(
modem: WiredModemPeripheral | string,
srcInventory: InventoryPeripheral,
) {
if (turtle == undefined) { if (turtle == undefined) {
throw new Error("Script must be run in a turtle computer"); throw new Error("Script must be run in a turtle computer");
} }
@@ -105,20 +160,9 @@ class CraftManager {
} }
this.localName = name; this.localName = name;
// log.info(`Get turtle name : ${name}`); // log.info(`Get turtle name : ${name}`);
}
public pushAll(outputInventory: InventoryPeripheral): void { // Inventory
for (let i = 1; i <= TURTLE_SIZE; i++) { this.inventory = srcInventory;
outputInventory.pullItems(this.localName, i);
}
}
public craft(dstInventory?: InventoryPeripheral, limit?: number): void {
turtle.craft(limit);
if (dstInventory != undefined) {
dstInventory.pullItems(this.localName, 1, limit);
}
} }
public static getPackageRecipe( public static getPackageRecipe(
@@ -142,40 +186,152 @@ class CraftManager {
); );
} }
public pullItems( public initItemsMap() {
recipe: CraftRecipe, const ingredientList = this.inventory.list();
srcInventory: InventoryPeripheral,
craftCnt: number,
): Result<number> {
// Initialize hash map
const ingredientList = srcInventory.list();
const ingredientMap = new Map<string, Queue<InventorySlotInfo>>();
for (const key in ingredientList) { for (const key in ingredientList) {
const slotNum = parseInt(key); const slotNum = parseInt(key);
const item = srcInventory.getItemDetail(slotNum)!; const item = this.inventory.getItemDetail(slotNum)!;
if (ingredientMap.has(item.name)) { if (this.inventoryItemsMap.has(item.name)) {
ingredientMap.get(item.name)!.enqueue({ this.inventoryItemsMap.get(item.name)!.slotCountQueue.enqueue({
name: item.name,
slotNum: slotNum, slotNum: slotNum,
count: item.count, count: item.count,
maxCount: item.maxCount,
}); });
} else { } else {
ingredientMap.set( this.inventoryItemsMap.set(item.name, {
item.name, name: item.name,
new Queue<InventorySlotInfo>([ maxCount: item.maxCount,
{ slotCountQueue: new Queue<{ slotNum: number; count: number }>([
name: item.name, { slotNum: slotNum, count: item.count },
slotNum: slotNum,
count: item.count,
maxCount: item.maxCount,
},
]), ]),
); });
}
}
}
public pullFromInventory(
itemId: string,
count?: number,
toSlot?: number,
): Result<number> {
const item = this.inventoryItemsMap.get(itemId);
if (item === undefined || item.slotCountQueue.size() === 0)
return new Err(Error(`No item match ${itemId}`));
if (count === undefined) {
const itemSlot = item.slotCountQueue.dequeue()!;
const pullItemsCnt = this.inventory.pushItems(
this.localName,
itemSlot.slotNum,
itemSlot.count,
toSlot,
);
return new Ok(pullItemsCnt);
}
let restCount = count;
while (restCount > 0 && item.slotCountQueue.size() > 0) {
const itemSlot = item.slotCountQueue.dequeue()!;
const pullItemsCnt = this.inventory.pushItems(
this.localName,
itemSlot.slotNum,
Math.min(restCount, itemSlot.count),
toSlot,
);
if (pullItemsCnt < itemSlot.count) {
item.slotCountQueue.enqueue({
slotNum: itemSlot.slotNum,
count: itemSlot.count - pullItemsCnt,
});
}
restCount -= pullItemsCnt;
}
return new Ok(count - restCount);
}
public pushToInventoryEmpty(
fromSlot: number,
count?: number,
): Result<number> {
let emptySlot = 0;
for (let i = this.inventory.size(); i > 0; i--) {
const isEmpty = this.inventory.getItemDetail(i) === undefined;
if (isEmpty) {
emptySlot = i;
break;
} }
} }
if (emptySlot <= 0) return new Err(Error("No empty slot found"));
return new Ok(
this.inventory.pullItems(this.localName, fromSlot, count, emptySlot),
);
}
public pushToInventory(fromSlot: number): Result<number> {
const itemInfoDetail = turtle.getItemDetail(fromSlot) as
| SlotDetail
| undefined;
if (itemInfoDetail === undefined) return new Ok(0);
const inventoryItemInfo = this.inventoryItemsMap.get(itemInfoDetail.name);
if (inventoryItemInfo === undefined) {
return this.pushToInventoryEmpty(fromSlot, itemInfoDetail.count);
}
let restItemsCount = itemInfoDetail.count;
for (const slotInfo of inventoryItemInfo.slotCountQueue) {
const pullItemsCount = inventoryItemInfo.maxCount - slotInfo.count;
if (pullItemsCount > 0) {
this.inventory.pullItems(
this.localName,
fromSlot,
pullItemsCount,
slotInfo.slotNum,
);
restItemsCount -= pullItemsCount;
if (restItemsCount <= 0) break;
}
}
if (restItemsCount > 0) {
const pushRet = this.pushToInventoryEmpty(fromSlot, restItemsCount);
if (pushRet.isErr()) return pushRet;
}
return new Ok(itemInfoDetail.count);
}
public clearTurtle(slots?: number[]): void {
if (slots !== undefined) {
for (const slotNum of slots) {
this.pushToInventory(slotNum);
}
return;
}
for (let i = 1; i <= TURTLE_SIZE; i++) {
this.pushToInventory(i);
}
}
public craft(limit?: number, outputSlot = CRAFT_OUTPUT_SLOT): ItemDetail {
turtle.select(outputSlot);
turtle.craft(limit);
const craftItemDetail = turtle.getItemDetail(
outputSlot,
true,
) as ItemDetail;
return craftItemDetail;
}
public pullItemsWithRecipe(
recipe: CraftRecipe,
craftCnt: number,
): Result<number> {
let maxCraftCnt = craftCnt; let maxCraftCnt = craftCnt;
for (const index in recipe.PatternEntries) { for (const index in recipe.PatternEntries) {
const entry = recipe.PatternEntries[index]; const entry = recipe.PatternEntries[index];
@@ -183,58 +339,24 @@ class CraftManager {
continue; continue;
} }
if (!ingredientMap.has(entry.Item.id)) const ingredient = this.inventoryItemsMap.get(entry.Item.id);
if (ingredient === undefined)
return new Err(Error(`No ingredient match ${entry.Item.id}`)); return new Err(Error(`No ingredient match ${entry.Item.id}`));
const ingredient = ingredientMap.get(entry.Item.id)!; // Check item max stack count
let restCraftCnt = maxCraftCnt; if (ingredient.maxCount < maxCraftCnt) {
while (restCraftCnt > 0 && ingredient.size() > 0) { maxCraftCnt = ingredient.maxCount;
const slotItem = ingredient.dequeue()!;
// Check item max stack count
if (slotItem.maxCount < maxCraftCnt) {
maxCraftCnt = slotItem.maxCount;
restCraftCnt = maxCraftCnt;
}
if (slotItem.count >= restCraftCnt) {
const pushItemsCnt = srcInventory.pushItems(
this.localName,
slotItem.slotNum,
restCraftCnt,
CRAFT_SLOT_TABLE[index],
);
if (pushItemsCnt !== restCraftCnt)
return new Err(
Error(
`Try to get items ${restCraftCnt}x "${slotItem.name}" from inventory, but only get ${pushItemsCnt}x`,
),
);
if (slotItem.count > restCraftCnt) {
ingredient.enqueue({
...slotItem,
count: slotItem.count - restCraftCnt,
});
}
restCraftCnt = 0;
} else {
const pushItemsCnt = srcInventory.pushItems(
this.localName,
slotItem.slotNum,
slotItem.count,
CRAFT_SLOT_TABLE[index],
);
if (pushItemsCnt !== slotItem.count)
return new Err(
Error(
`Try to get items ${slotItem.count}x "${slotItem.name}" from inventory, but only get ${pushItemsCnt}x`,
),
);
restCraftCnt -= slotItem.count;
}
} }
if (restCraftCnt > 0) // Pull items
const pullItemsCnt = this.pullFromInventory(
ingredient.name,
maxCraftCnt,
CRAFT_SLOT_TABLE[index],
);
if (pullItemsCnt.isErr()) return pullItemsCnt;
if (pullItemsCnt.value < maxCraftCnt)
return new Err(Error("Not enough items in inventory")); return new Err(Error("Not enough items in inventory"));
} }

View File

@@ -26,7 +26,14 @@ export class Queue<T> {
} }
} }
public enqueue(data: T): void { public enqueue(data: T | T[]): void {
if (Array.isArray(data)) {
for (const val of data) {
this.enqueue(val);
}
return;
}
const node = new Node(data); const node = new Node(data);
if (this._head === undefined) { if (this._head === undefined) {

View File

@@ -6,7 +6,7 @@ declare interface BlockItemDetailData {
} }
declare interface BlockDetailData { declare interface BlockDetailData {
Items: Record<number, BlockItemDetailData>; Items: Record<string, BlockItemDetailData>;
} }
/** /**