chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 0acef64f48
200 changed files with 35546 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
import type {Bot} from '@wp2508/mineflayer';
import type {log} from '../types/utils/logger.ts';
import zhCn from '../data/runtime/language/zh_cn.json';
import {MinecraftJsonParser} from './message_json_parser.ts';
export type ItemValue = null | boolean | number | string | ItemValue[] | {[key: string]: ItemValue};
export interface InventoryItem {
slot: number | null;
type: number;
name: string;
i18n: {lang: string; name: string; description: string}[];
count: number;
metadata: number;
displayName: log[];
lore: log[][];
enchants: {id?: number; name?: string; i18n: InventoryItem['i18n']; level: number; hidden: boolean}[];
durability: {used: number; max: number; remaining: number} | null;
unbreakable: boolean;
glint: boolean | null;
book: {title: string; author: string; pages: log[][]} | null;
details: Record<string, ItemValue>;
truncated: boolean;
}
const tags = new Set(['compound', 'list', 'string', 'byte', 'short', 'int', 'long', 'float', 'double', 'byteArray', 'intArray', 'longArray']);
const record = (value: unknown): Record<string, unknown> => value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown> : {};
const array = (value: unknown): unknown[] => Array.isArray(value) ? value : [];
const number = (value: unknown, fallback = 0): number => typeof value === 'number' && Number.isFinite(value) ? value : fallback;
const identifier = (value: unknown): string => typeof value === 'string' ? value.replace(/^minecraft:/, '') : '';
/** Projects both legacy NBT and modern components; never returns raw tag wrappers or live Item objects. */
export function parse_inventory_item(input: unknown, slot: number | null, registry: Bot['registry']): InventoryItem | null {
if (!input || typeof input !== 'object') return null;
let truncated = false;
let nodes = 0;
let text_left = 24_576;
const ancestors = new Set<object>();
const normalize = (value: unknown, depth = 0): ItemValue => {
if (++nodes > 8192 || depth > 32) { truncated = true; return null; }
if (value == null) return null;
if (typeof value === 'bigint') return value.toString();
if (typeof value === 'string') {
let text = '';
for (const character of value) {
const bytes = Buffer.byteLength(character);
if (bytes > text_left) break;
text += character;
text_left -= bytes;
}
if (text !== value) truncated = true;
return text;
}
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
if (typeof value === 'boolean') return value;
if (typeof value !== 'object') { truncated = true; return null; }
if (ancestors.has(value)) { truncated = true; return null; }
ancestors.add(value);
try {
if (Array.isArray(value) || Buffer.isBuffer(value)) {
if (value.length > 1024) truncated = true;
return Array.from(value as unknown[]).slice(0, 1024).map(entry => normalize(entry, depth + 1));
}
const descriptors = Object.getOwnPropertyDescriptors(value);
const type = descriptors.type?.value;
if (typeof type === 'string' && tags.has(type) && descriptors.value && 'value' in descriptors.value
&& Object.keys(descriptors).every(key => ['type', 'value', 'name'].includes(key))) {
return normalize(descriptors.value.value, depth + 1);
}
const entries = Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable);
if (entries.length > 256) truncated = true;
return Object.fromEntries(entries.slice(0, 256).map(([key, descriptor]) => {
if (!('value' in descriptor)) { truncated = true; return [key, null]; }
return [key, normalize(descriptor.value, depth + 1)];
}));
} finally { ancestors.delete(value); }
};
const text = (value: unknown): log[] => {
let component = value;
if (typeof component === 'string' && /^[\s]*[\[{\"]/.test(component)) {
try { component = JSON.parse(component); } catch { /* Plain item names need not be JSON. */ }
}
const logs = MinecraftJsonParser.toLog(component as Parameters<typeof MinecraftJsonParser.toLog>[0], false);
const colors = ['#000000', '#0000AA', '#00AA00', '#00AAAA', '#AA0000', '#AA00AA', '#FFAA00', '#AAAAAA',
'#555555', '#5555FF', '#55FF55', '#55FFFF', '#FF5555', '#FF55FF', '#FFFF55', '#FFFFFF'];
return logs.flatMap(segment => {
let color = segment.font_color;
let styles = segment.font_style.filter(style => style !== 'normal');
const result: log[] = [];
const pieces = String(segment.message).split(/(§[0-9a-fk-or])/gi);
for (const piece of pieces) {
if (/^§[0-9a-fk-or]$/i.test(piece)) {
const code = piece[1].toLowerCase();
const color_index = '0123456789abcdef'.indexOf(code);
if (color_index >= 0) { color = colors[color_index]; styles = []; }
else if (code === 'r') { color = '#FFFFFF'; styles = []; }
else {
const style = ({l: 'bold', o: 'italic', n: 'underline'} as const)[code as 'l' | 'o' | 'n'];
if (style && !styles.includes(style)) styles = [...styles, style];
}
} else if (piece) result.push({message: piece, font_color: color, font_style: styles.length ? [...styles] : ['normal']});
}
return result;
});
};
const parse = (raw: unknown, item_slot: number | null, nesting: number): InventoryItem | null => {
const item = record(raw);
const raw_id = item.type ?? item.itemId ?? item.id;
const lookup = typeof raw_id === 'string' ? registry?.itemsByName?.[identifier(raw_id)]
: registry?.items?.[number(raw_id, -1)];
const type = typeof raw_id === 'number' ? raw_id : lookup?.id;
const count = number(item.count ?? item.itemCount ?? item.Count, 1);
if (type == null || type < 0 || count <= 0) return null;
const legacy = record(normalize(item.nbt ?? item.tag));
const raw_components = new Map<string, unknown>();
for (const entry of array(item.components)) {
const component = record(entry);
const key = identifier(component.type);
if (key) raw_components.set(key, component.data);
}
// Item setters can update componentMap before rebuilding the components array.
if (item.componentMap instanceof Map) for (const [key, component] of item.componentMap) {
raw_components.set(identifier(key), record(component).data);
}
const components = Object.fromEntries(Array.from(raw_components, ([key, value]) => [key, normalize(value)]));
const display = record(legacy.display);
const name = typeof item.name === 'string' ? item.name : lookup?.name ?? (identifier(raw_id) || 'unknown');
let displayName: log[] = [];
for (const candidate of [components.custom_name, display.Name, components.item_name, item.displayName, lookup?.displayName, name]) {
if (candidate == null) continue;
displayName = text(candidate);
if (displayName.length) break;
}
const lore = array(components.lore ?? display.Lore).slice(0, 128).map(text);
if (array(components.lore ?? display.Lore).length > 128) truncated = true;
const flags = number(legacy.HideFlags);
const tooltip = record(components.tooltip_display);
const hidden_components = array(tooltip.hiddenComponents);
const component_types = registry?.protocol?.types?.SlotComponentType as unknown as [string, {mappings?: Record<string, string>}] | undefined;
const hidden_names = hidden_components.map(value => typeof value === 'number'
? component_types?.[1]?.mappings?.[String(value)] ?? value : identifier(value));
const hidden_tooltip = Object.hasOwn(components, 'hide_tooltip') || tooltip.hideTooltip === true;
const enchants: InventoryItem['enchants'] = [];
const enchant_sources = [
{source: components.enchantments ?? legacy.Enchantments ?? legacy.ench, hidden: Boolean(flags & 1)},
{source: components.stored_enchantments ?? legacy.StoredEnchantments, hidden: Boolean(flags & 32)},
];
for (const {source, hidden} of enchant_sources) {
const data = record(source);
const entries = Array.isArray(source) ? source : Array.isArray(data.enchantments) ? data.enchantments
: Object.entries(record(data.levels)).map(([id, level]) => ({id, level}));
for (const entry of entries) {
const enchant = record(entry);
const raw_id = enchant.id ?? enchant.name;
const id = typeof raw_id === 'number' ? raw_id : undefined;
const name = typeof raw_id === 'string' ? identifier(raw_id) : registry?.enchantments?.[id ?? -1]?.name;
enchants.push({...(id === undefined ? {} : {id}), ...(name ? {name} : {}),
i18n: [{lang: 'zh-CN', name: (zhCn as Record<string, string>)[`enchantment.minecraft.${name}`]
|| name || String(id ?? 'unknown'), description: ''}],
level: number(enchant.level ?? enchant.lvl),
hidden: hidden_tooltip || hidden || hidden_names.includes('enchantments') || hidden_names.includes('stored_enchantments')
|| data.showTooltip === false || data.showInTooltip === false});
}
}
const max = number(components.max_damage ?? item.maxDurability ?? lookup?.maxDurability);
let used = number(components.damage ?? legacy.Damage ?? item.metadata);
if (!Object.hasOwn(components, 'damage') && legacy.Damage == null && max > 0) {
try { used = number(item.durabilityUsed, used); } catch { truncated = true; }
}
const book_data = record(components.written_book_content ?? components.writable_book_content
?? (['written_book', 'writable_book'].includes(name) ? legacy : null));
const written = name === 'written_book' || Object.hasOwn(components, 'written_book_content');
const book = Array.isArray(book_data.pages) ? {
title: String(book_data.rawTitle ?? book_data.title ?? ''), author: String(book_data.author ?? ''),
pages: book_data.pages.slice(0, 100).map(page => {
const content = record(page).content ?? page;
// Writable books contain literal text, even when a page looks like JSON.
return written ? text(content) : text({text: String(content ?? '')});
}),
} : null;
if (array(book_data.pages).length > 100) truncated = true;
const details: Record<string, ItemValue> = {};
if (components.potion_contents != null || legacy.Potion != null || legacy.CustomPotionEffects != null) {
const potion = record(components.potion_contents);
details.potion = {
type: (potion.potionId ?? legacy.Potion ?? null) as ItemValue,
color: (potion.customColor ?? legacy.CustomPotionColor ?? null) as ItemValue,
effects: array(potion.customEffects ?? legacy.CustomPotionEffects).map(entry => {
const effect = record(entry);
const id = effect.id ?? effect.Id;
const data = record(effect.details ?? effect);
const name = typeof id === 'string' ? identifier(id) : registry?.effects?.[number(id, -1)]?.name;
return {id: id as ItemValue ?? null, name: name ?? null, amplifier: number(data.amplifier ?? data.Amplifier),
duration: number(data.duration ?? data.Duration), ambient: Boolean(data.ambient ?? data.Ambient),
showParticles: (data.showParticles ?? data.ShowParticles ?? true) as ItemValue};
}),
};
}
if (components.attribute_modifiers != null || legacy.AttributeModifiers != null) {
const modifiers = record(components.attribute_modifiers);
details.attributes = {hidden: hidden_tooltip || Boolean(flags & 2) || hidden_names.includes('attribute_modifiers') || modifiers.showTooltip === false,
modifiers: array(Array.isArray(components.attribute_modifiers) ? components.attribute_modifiers : modifiers.attributes ?? legacy.AttributeModifiers).map(entry => {
const attribute = record(entry);
return {
type: (attribute.typeId ?? attribute.type ?? attribute.AttributeName ?? null) as ItemValue,
name: String(attribute.name ?? attribute.Name ?? attribute.id ?? ''),
amount: number(attribute.value ?? attribute.amount ?? attribute.Amount),
operation: (attribute.operation ?? attribute.Operation ?? null) as ItemValue,
slot: (attribute.slot ?? attribute.Slot ?? null) as ItemValue,
hidden: record(attribute.display).type === 'hidden',
displayName: text(record(attribute.display).component) as unknown as ItemValue,
};
})};
}
const nested = (entries: unknown[], preserve_slot: boolean): ItemValue[] => {
if (nesting >= 4) { truncated = true; return []; }
if (entries.length > 256) truncated = true;
return entries.slice(0, 256).map((entry, index) => {
const wrapper = record(entry);
const raw = wrapper.item ?? entry;
return parse(raw, preserve_slot ? number(wrapper.slot ?? wrapper.Slot, index) : null, nesting + 1) as unknown as ItemValue;
});
};
const container = components.container ?? record(legacy.BlockEntityTag).Items;
if (container != null) details.container = nested(array(record(container).contents ?? container), true);
if (components.bundle_contents != null || name === 'bundle' && legacy.Items != null) {
const contents = components.bundle_contents ?? legacy.Items;
details.bundle = nested(array(record(contents).contents ?? contents), false);
}
const known_components = new Set(['custom_name', 'item_name', 'lore', 'enchantments', 'stored_enchantments', 'damage', 'max_damage',
'unbreakable', 'enchantment_glint_override', 'written_book_content', 'writable_book_content', 'potion_contents', 'attribute_modifiers',
'container', 'bundle_contents', 'tooltip_display', 'hide_tooltip', 'hide_additional_tooltip']);
const known_legacy = new Set(['display', 'Enchantments', 'ench', 'StoredEnchantments', 'Damage', 'Unbreakable', 'HideFlags',
'Potion', 'CustomPotionEffects', 'CustomPotionColor', 'AttributeModifiers']);
if (book) for (const key of ['pages', 'title', 'author']) known_legacy.add(key);
if (name === 'bundle') known_legacy.add('Items');
const extra_legacy = Object.fromEntries(Object.entries(legacy).filter(([key]) => !known_legacy.has(key)));
if (container != null) {
const block = {...record(extra_legacy.BlockEntityTag)};
delete block.Items;
if (Object.keys(block).length) extra_legacy.BlockEntityTag = block;
else delete extra_legacy.BlockEntityTag;
}
const extra_display = Object.fromEntries(Object.entries(display).filter(([key]) => !['Name', 'Lore'].includes(key)));
if (Object.keys(extra_display).length) extra_legacy.display = extra_display;
const extra: Record<string, unknown> = Object.fromEntries(Object.entries(components).filter(([key]) => !known_components.has(key)));
if (Object.keys(extra_legacy).length) extra.legacy = extra_legacy;
if (Object.keys(extra).length) details.extra = extra as ItemValue;
if (flags || hidden_tooltip || Object.hasOwn(components, 'hide_additional_tooltip') || hidden_components.length) {
details.tooltip = {hideAll: hidden_tooltip, hideAdditional: Object.hasOwn(components, 'hide_additional_tooltip'),
hiddenComponents: hidden_names as ItemValue[], legacyFlags: flags};
}
return {
slot: item_slot, type, name, count, metadata: number(item.metadata), displayName, lore, enchants,
i18n: [{lang: 'zh-CN', name: (zhCn as Record<string, string>)[`block.minecraft.${name}`]
|| (zhCn as Record<string, string>)[`item.minecraft.${name}`] || name, description: ''}],
durability: max > 0 ? {used, max, remaining: Math.max(0, max - used)} : null,
unbreakable: Object.hasOwn(components, 'unbreakable') ? components.unbreakable !== false : Boolean(legacy.Unbreakable),
glint: typeof components.enchantment_glint_override === 'boolean' ? components.enchantment_glint_override : null,
book, details, truncated,
};
};
const result = parse(input, slot, 0);
if (result) result.truncated = truncated;
return result;
}