utils/infinitecraft/merger.go
2025-05-02 18:22:16 -05:00

378 lines
8.3 KiB
Go

package infinitecraft
import (
"compress/gzip"
"context"
"database/sql"
"encoding/json"
"errors"
"os"
"slices"
_ "github.com/glebarez/go-sqlite"
"github.com/jmoiron/sqlx"
)
type Merger struct {
files []*os.File
games []Game
parsed bool
db *sqlx.DB
gameMap map[int64]Game
itemMap map[int64]Item
}
func (m *Merger) Games() map[int64]Game {
return m.gameMap
}
func (m *Merger) Items() map[int64]Item {
return m.itemMap
}
func (m *Merger) Close() error {
errs := make([]error, 0, len(m.files)+1)
for _, f := range m.files {
errs = append(errs, f.Close())
}
errs = append(errs, m.db.Close())
return errors.Join(errs...)
}
func (m *Merger) ParseFiles() error {
m.games = make([]Game, len(m.files))
errs := make([]error, len(m.files))
for idx, f := range m.files {
unz, err := gzip.NewReader(f)
if err != nil {
errs[idx] = err
continue
}
dec := json.NewDecoder(unz)
errs[idx] = dec.Decode(&(m.games[idx]))
}
err := errors.Join(errs...)
if err == nil {
m.parsed = true
}
return err
}
const schema string = `
CREATE TABLE IF NOT EXISTS games (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
version TEXT NOT NULL,
created INTEGER NOT NULL,
updated INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
emoji TEXT NOT NULL,
discovery BOOLEAN NOT NULL
);
CREATE TABLE IF NOT EXISTS games_items (
id INTEGER PRIMARY KEY,
game_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
orig_id INTEGER NOT NULL,
FOREIGN KEY(game_id) REFERENCES games(id),
FOREIGN KEY(item_id) REFERENCES items(id)
);
CREATE TABLE IF NOT EXISTS recipes (
id INTEGER PRIMARY KEY,
item_id INTEGER NOT NULL,
FOREIGN KEY(item_id) REFERENCES items(id)
);
CREATE TABLE IF NOT EXISTS recipes_items (
id INTEGER PRIMARY KEY,
recipe_id INTEGER NOT NULL,
parent_id INTEGER NOT NULL,
FOREIGN KEY(recipe_id) REFERENCES recipes(id),
FOREIGN KEY(parent_id) REFERENCES items(id)
);
`
// select i.name as text, group_concat(par.name) as ingredients from items i join recipes r on r.item_id = i.id join recipes_items ri on ri.recipe_id = r.id join items par on ri.parent_id = par.id group by ri.recipe_id ;
func (m *Merger) ReadData(ctx context.Context) (err error) {
//nolint:gocritic // temp stuff
// m.db, err = sqlx.Open("sqlite", "foo.db")
m.db, err = sqlx.Open("sqlite", ":memory:")
if err != nil {
return
}
_, err = m.db.ExecContext(ctx, schema)
if err != nil {
return
}
m.gameMap, err = m.insertGames(ctx)
if err != nil {
return
}
m.itemMap = make(map[int64]Item)
for gameID, game := range m.gameMap {
var itM map[int64]Item
itM, err = m.insertItems(ctx, gameID, game.Items)
if err != nil {
return
}
for itemID, it := range itM {
m.itemMap[itemID] = it
err = m.insertRecipes(ctx, gameID, itemID, it.Recipes)
if err != nil {
return
}
}
}
return
}
func (m *Merger) getGameDetails(ctx context.Context, g *Game) error {
return m.db.GetContext(ctx, g, `SELECT name, version FROM games ORDER BY id ASC LIMIT 1`)
}
func (m *Merger) getGameCreated(ctx context.Context, date *int64) error {
return m.db.GetContext(ctx, date, `SELECT MIN(created) FROM games`)
}
func (m *Merger) getGameUpdated(ctx context.Context, date *int64) error {
return m.db.GetContext(ctx, date, `SELECT MAX(updated) FROM games`)
}
func (m *Merger) getItems(ctx context.Context, items *[]Item) error {
return m.db.SelectContext(ctx, items, `
SELECT
id-1 AS id,
name,
emoji,
discovery
FROM items
`)
}
func (m *Merger) getRecipes(ctx context.Context, items []Item) (err error) {
var rows *sqlx.Rows
if rows, err = m.db.QueryxContext(ctx, `
SELECT
r.item_id-1 AS itemid,
ri.recipe_id AS recipeid,
ri.parent_id-1 AS parentid
FROM recipes_items ri
JOIN recipes r ON ri.recipe_id = r.id
`); err != nil {
return
}
defer rows.Close()
rm := make(map[int32]map[int32][]int32)
var found bool
var recRow struct {
ItemID int32
RecipeID int32
ParentID int32
}
for rows.Next() {
if err = rows.StructScan(&recRow); err != nil {
return
}
if _, found = rm[recRow.ItemID]; !found {
rm[recRow.ItemID] = make(map[int32][]int32)
}
if _, found = rm[recRow.ItemID][recRow.RecipeID]; !found {
rm[recRow.ItemID][recRow.RecipeID] = make([]int32, 0, 2)
}
rm[recRow.ItemID][recRow.RecipeID] = append(rm[recRow.ItemID][recRow.RecipeID], recRow.ParentID)
}
var recs map[int32][]int32
for idx := range items {
if recs, found = rm[items[idx].Id]; found {
items[idx].Recipes = make([][]int32, 0, len(recs))
for _, rec := range recs {
addRecs(&(items[idx].Recipes), rec)
}
}
}
return
}
func addRecs(recs *[][]int32, rec []int32) {
slices.Sort(rec)
if !slices.ContainsFunc(*recs, recSorter(rec)) {
*recs = append(*recs, rec)
}
}
func recSorter(rec []int32) func([]int32) bool {
return func(v []int32) bool {
slices.Sort(v)
if !(len(v) == len(rec)) {
return false
}
for idx, i := range v {
if !(i == rec[idx]) {
return false
}
}
return true
}
}
func (m *Merger) Merge(ctx context.Context) (g Game, err error) {
if err = m.getGameDetails(ctx, &g); err != nil {
return
}
if err = m.getGameCreated(ctx, &g.Created); err != nil {
return
}
if err = m.getGameUpdated(ctx, &g.Updated); err != nil {
return
}
if err = m.getItems(ctx, &g.Items); err != nil {
return
}
err = m.getRecipes(ctx, g.Items)
return
}
func (m *Merger) insertGames(ctx context.Context) (mp map[int64]Game, err error) {
mp = make(map[int64]Game, len(m.games))
var res sql.Result
var stmt *sqlx.NamedStmt
stmt, err = m.db.PrepareNamedContext(ctx, `
INSERT INTO games
(name, version, created, updated)
VALUES (:name, :version, :created, :updated)
`)
if err != nil {
return
}
defer stmt.Close()
for _, g := range m.games {
res, err = stmt.ExecContext(ctx, g)
if err != nil {
return
}
var id int64
id, err = res.LastInsertId()
if err != nil {
return
}
mp[id] = g
}
return
}
func (m *Merger) insertItems(ctx context.Context, gameID int64, items []Item) (itM map[int64]Item, err error) {
itM = make(map[int64]Item, len(items))
var stmt *sqlx.NamedStmt
var fetchIDStmt *sqlx.Stmt
var relStmt *sqlx.Stmt
stmt, err = m.db.PrepareNamedContext(ctx, `
INSERT OR IGNORE INTO items
(name, emoji, discovery)
VALUES (:name, :emoji, :discovery)
ON CONFLICT(name) DO UPDATE SET
emoji=excluded.emoji,
discovery=excluded.discovery
`)
if err != nil {
return
}
defer stmt.Close()
fetchIDStmt, err = m.db.PreparexContext(ctx, `SELECT id FROM items WHERE name = ?`)
if err != nil {
return
}
defer fetchIDStmt.Close()
relStmt, err = m.db.PreparexContext(ctx, `
INSERT INTO games_items
(game_id, item_id, orig_id)
VALUES (?, ?, ?)
`)
if err != nil {
return
}
defer relStmt.Close()
for _, it := range items {
_, err = stmt.ExecContext(ctx, it)
if err != nil {
return
}
var id int64
err = fetchIDStmt.GetContext(ctx, &id, it.Text)
if err != nil {
return
}
_, err = relStmt.ExecContext(ctx, gameID, id, it.Id)
if err != nil {
return
}
itM[id] = it
}
return
}
func (m *Merger) insertRecipes(ctx context.Context, gameID, itemID int64, recipes [][]int32) (err error) {
var res sql.Result
var insertStmt *sqlx.Stmt
var selStmt *sqlx.Stmt
var insertRecItemStmt *sqlx.Stmt
insertStmt, err = m.db.PreparexContext(ctx, `
INSERT INTO recipes (item_id) VALUES (?)
`)
if err != nil {
return
}
defer insertStmt.Close()
insertRecItemStmt, err = m.db.PreparexContext(ctx, `
INSERT INTO recipes_items
(recipe_id, parent_id)
VALUES (?, ?)
`)
if err != nil {
return
}
defer insertRecItemStmt.Close()
selStmt, err = m.db.PreparexContext(ctx, `
SELECT item_id FROM games_items
WHERE game_id = ? AND orig_id = ?
`)
if err != nil {
return
}
defer selStmt.Close()
for _, recipe := range recipes {
var recipeID int64
res, err = insertStmt.ExecContext(ctx, itemID)
if err != nil {
return
}
recipeID, err = res.LastInsertId()
if err != nil {
return
}
for _, origID := range recipe {
var parentID int64
err = selStmt.GetContext(ctx, &parentID, gameID, origID)
if err != nil {
return
}
_, err = insertRecItemStmt.ExecContext(ctx, recipeID, parentID)
if err != nil {
return
}
}
}
return
}