386 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			386 lines
		
	
	
	
		
			8.6 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
 | |
| 	}
 | |
| 
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	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
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	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
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	defer stmt.Close()
 | |
| 	fetchIDStmt, err = m.db.PreparexContext(ctx, `SELECT id FROM items WHERE name = ?`)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	defer fetchIDStmt.Close()
 | |
| 	relStmt, err = m.db.PreparexContext(ctx, `
 | |
| 	    INSERT INTO games_items
 | |
| 	    (game_id, item_id, orig_id)
 | |
| 	    VALUES (?, ?, ?)
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	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
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	defer insertStmt.Close()
 | |
| 	insertRecItemStmt, err = m.db.PreparexContext(ctx, `
 | |
| 	    INSERT INTO recipes_items
 | |
| 	    (recipe_id, parent_id)
 | |
| 	    VALUES (?, ?)
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	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
 | |
| 	}
 | |
| 	//nolint:errcheck // I don't care
 | |
| 	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
 | |
| }
 |