package infinitecraft import ( "compress/gzip" "context" "database/sql" "encoding/json" "errors" "os" _ "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 *uint64) error { return m.db.GetContext(ctx, date, `SELECT MIN(created) FROM games`) } func (m *Merger) getGameUpdated(ctx context.Context, date *uint64) 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 { items[idx].Recipes = append(items[idx].Recipes, rec) } } } return } 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 } type GameItem struct { Game string `db:"game"` GameID int64 `db:"game_id"` Text string `db:"name"` Emoji string Id int64 `db:"orig_id"` } func (m *Merger) GameItems(ctx context.Context) (gits []GameItem, err error) { err = m.db.SelectContext(ctx, &gits, ` SELECT g.name AS game, g.id AS game_id, it.name, it.emoji, it.id - 1 AS orig_id FROM games_items gi JOIN items it ON gi.item_id = it.id JOIN games g ON gi.game_id = g.id ORDER BY gi.id ASC `) 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 }