Files
ngzz-mc/internal/recipe/recipe.go

238 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package recipe 配方注册表与匹配(场景/工作台与合成表.md §2–§4)。
//
// 索引化匹配:启动时按输入物品建索引,运行时不做全表扫描。
package recipe
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
)
// Result 配方产物。
type Result struct {
Item string `json:"item"`
Count int `json:"count"`
}
// Recipe 配方条目(assets/config/recipes.json)。
type Recipe struct {
Type string `json:"type"` // crafting_shaped / crafting_shapeless / smelting
Pattern []string `json:"pattern"`
Key map[string]string `json:"key"`
Ingredients []string `json:"ingredients"`
Input string `json:"input"`
Result Result `json:"result"`
}
// Registry 配方注册表(含索引)。
type Registry struct {
shaped []*Recipe
shapeless []*Recipe
smelting map[string]*Recipe
index map[string][]*Recipe // 输入物品 → 候选配方(§3 索引)
}
// Load 解析 recipes.json 并构建索引。
func Load(path string) (*Registry, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("recipe.Load %s: %w", path, err)
}
var doc struct {
Recipes []Recipe `json:"recipes"`
}
if err := json.Unmarshal(b, &doc); err != nil {
return nil, fmt.Errorf("recipe.Load %s: %w", path, err)
}
r := &Registry{smelting: make(map[string]*Recipe), index: make(map[string][]*Recipe)}
for i := range doc.Recipes {
rc := &doc.Recipes[i]
switch rc.Type {
case "crafting_shaped":
r.shaped = append(r.shaped, rc)
for _, k := range rc.Key {
r.index[k] = append(r.index[k], rc)
}
case "crafting_shapeless":
r.shapeless = append(r.shapeless, rc)
for _, ing := range rc.Ingredients {
r.index[ing] = append(r.index[ing], rc)
}
case "smelting":
r.smelting[rc.Input] = rc
default:
return nil, fmt.Errorf("recipe.Load: 未知配方类型 %q", rc.Type)
}
}
return r, nil
}
// MatchSmelting 熔炼匹配(单输入键)。
func (r *Registry) MatchSmelting(input string) (*Recipe, bool) {
rc, ok := r.smelting[input]
return rc, ok
}
// MatchShapeless 无序匹配:多重集比对(工作台与合成表.md §4.2 计数不能只判包含)。
func (r *Registry) MatchShapeless(counts map[string]int) (*Recipe, bool) {
for _, rc := range r.shapeless {
if multisetEq(counts, rc.Ingredients) {
return rc, true
}
}
return nil, false
}
// multisetEq 输入多重集与配方配料多重集是否完全一致。
func multisetEq(counts map[string]int, ingredients []string) bool {
if len(counts) != len(ingredients) {
return false
}
want := make(map[string]int, len(ingredients))
for _, ing := range ingredients {
want[ing]++
}
for k, v := range want {
if counts[k] != v {
return false
}
}
return true
}
// MatchShaped 有序匹配:pattern 归一化后与 3×3 网格逐偏移比对(工作台与合成表.md §4.1)。
// grid 按行主序:grid[y][x](y=0 为最上行)。
func (r *Registry) MatchShaped(grid [3][3]string) (*Recipe, bool) {
// 网格归一化:去掉空行/空列
g := normalizeGrid(grid)
for _, rc := range r.shaped {
if shapedMatches(rc, g) {
return rc, true
}
}
return nil, false
}
// gridT 归一化后的网格(行 × 列,均为非空)。
type gridT struct {
w, h int
cell [][]string
}
// normalizeGrid 去除全空行与全空列。
func normalizeGrid(g [3][3]string) gridT {
var out gridT
for y := 0; y < 3; y++ {
var row []string
for x := 0; x < 3; x++ {
row = append(row, g[y][x])
}
out.cell = append(out.cell, row)
}
// 裁空行
var rows [][]string
for _, row := range out.cell {
nonEmpty := false
for _, c := range row {
if c != "" {
nonEmpty = true
}
}
if nonEmpty {
rows = append(rows, row)
}
}
if len(rows) == 0 {
return gridT{}
}
out.cell = rows
out.h = len(rows)
// 裁空列
minCol, maxCol := 3, -1
for x := 0; x < 3; x++ {
for y := 0; y < out.h; y++ {
if out.cell[y][x] != "" {
if x < minCol {
minCol = x
}
if x > maxCol {
maxCol = x
}
}
}
}
if minCol > maxCol {
return gridT{}
}
var trimmed [][]string
for y := 0; y < out.h; y++ {
trimmed = append(trimmed, out.cell[y][minCol:maxCol+1])
}
out.cell = trimmed
out.w = maxCol - minCol + 1
return out
}
// shapedMatches 配方 pattern(同样归一化)与网格全等。
func shapedMatches(rc *Recipe, g gridT) bool {
if rc == nil || len(rc.Pattern) == 0 {
return false
}
if len(rc.Pattern) != g.h {
return false
}
for y, row := range rc.Pattern {
if len(row) != g.w {
return false
}
for x := 0; x < g.w; x++ {
sym := string(row[x])
if sym == " " {
if g.cell[y][x] != "" {
return false
}
continue
}
if g.cell[y][x] != rc.Key[sym] {
return false
}
}
}
return true
}
// GridFromStrings 把行主序字符串切片转网格(" X " 表示一行)。
func GridFromStrings(rows []string) [3][3]string {
var g [3][3]string
for y := 0; y < 3 && y < len(rows); y++ {
row := rows[y]
for x := 0; x < 3 && x < len(row); x++ {
if row[x] != ' ' {
g[y][x] = string(row[x])
}
}
}
return g
}
// KeysSorted 返回已排序键列表(工具函数)。
func KeysSorted(m map[string]int) []string {
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
return ks
}
// TrimNS 去掉命名空间前缀(minecraft:stick → stick)。
func TrimNS(s string) string {
if i := strings.IndexByte(s, ':'); i >= 0 {
return s[i+1:]
}
return s
}