2025-05-02 11:33:23 -05:00
package infinitecraft
import (
"compress/gzip"
"context"
"database/sql"
"encoding/json"
"errors"
"os"
2025-05-02 18:22:16 -05:00
"slices"
2025-05-02 11:33:23 -05:00
_ "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 ` )
}
2025-05-02 15:16:04 -05:00
func ( m * Merger ) getGameCreated ( ctx context . Context , date * int64 ) error {
2025-05-02 11:33:23 -05:00
return m . db . GetContext ( ctx , date , ` SELECT MIN(created) FROM games ` )
}
2025-05-02 15:16:04 -05:00
func ( m * Merger ) getGameUpdated ( ctx context . Context , date * int64 ) error {
2025-05-02 11:33:23 -05:00
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
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
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 {
2025-05-02 18:22:16 -05:00
addRecs ( & ( items [ idx ] . Recipes ) , rec )
2025-05-02 11:33:23 -05:00
}
}
}
return
}
2025-05-02 18:22:16 -05:00
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 )
2025-07-16 11:13:53 -05:00
if len ( v ) != len ( rec ) {
2025-05-02 18:22:16 -05:00
return false
}
for idx , i := range v {
2025-07-16 11:13:53 -05:00
if i != rec [ idx ] {
2025-05-02 18:22:16 -05:00
return false
}
}
return true
}
}
2025-05-02 11:33:23 -05:00
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
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
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
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
defer stmt . Close ( )
fetchIDStmt , err = m . db . PreparexContext ( ctx , ` SELECT id FROM items WHERE name = ? ` )
if err != nil {
return
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
defer fetchIDStmt . Close ( )
relStmt , err = m . db . PreparexContext ( ctx , `
INSERT INTO games_items
( game_id , item_id , orig_id )
VALUES ( ? , ? , ? )
` )
if err != nil {
return
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
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
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
defer insertStmt . Close ( )
insertRecItemStmt , err = m . db . PreparexContext ( ctx , `
INSERT INTO recipes_items
( recipe_id , parent_id )
VALUES ( ? , ? )
` )
if err != nil {
return
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
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
}
2025-07-16 11:13:53 -05:00
//nolint:errcheck // I don't care
2025-05-02 11:33:23 -05:00
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
}