✨ Add ic-merge command
Still needs some work, like saving to a new file, but works well so far.
This commit is contained in:
parent
7aae44048a
commit
8a3da4c5fe
8 changed files with 499 additions and 0 deletions
5
infinitecraft/errors.go
Normal file
5
infinitecraft/errors.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package infinitecraft
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotParsed = errors.New("not yet parsed")
|
||||
10
infinitecraft/game.go
Normal file
10
infinitecraft/game.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package infinitecraft
|
||||
|
||||
type Game struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Created uint64 `json:"created"`
|
||||
Updated uint64 `json:"updated"`
|
||||
Instances []any `json:"instances" db:"-"`
|
||||
Items []Item `json:"items" db:"-"`
|
||||
}
|
||||
9
infinitecraft/item.go
Normal file
9
infinitecraft/item.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package infinitecraft
|
||||
|
||||
type Item struct {
|
||||
Id int32 `json:"id"`
|
||||
Text string `json:"text" db:"name"`
|
||||
Emoji string `json:"emoji"`
|
||||
Discovery bool `json:"discovery,omitempty"`
|
||||
Recipes [][]int32 `json:"recipes,omitempty" db:"-"`
|
||||
}
|
||||
379
infinitecraft/merger.go
Normal file
379
infinitecraft/merger.go
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
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
|
||||
}
|
||||
25
infinitecraft/new.go
Normal file
25
infinitecraft/new.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package infinitecraft
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func NewMerger(args []string) (*Merger, error) {
|
||||
m := Merger{}
|
||||
var err error
|
||||
if len(args) < 2 {
|
||||
//nolint:err113 // Don't care
|
||||
return nil, fmt.Errorf("expected at lease 2 arguments. Got %d", len(args))
|
||||
}
|
||||
fs := make([]*os.File, len(args))
|
||||
for idx, pth := range args {
|
||||
fs[idx], err = os.Open(pth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m.files = fs
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue