From aa362bde074895f376a9f4141d35754a9869a6c2 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sun, 3 Aug 2025 20:45:54 -0500 Subject: [PATCH] fix ground_spawn database file --- internal/ground_spawn/database.go | 359 +++++++++++++++++++++--------- 1 file changed, 258 insertions(+), 101 deletions(-) diff --git a/internal/ground_spawn/database.go b/internal/ground_spawn/database.go index 9337d57..c384f85 100644 --- a/internal/ground_spawn/database.go +++ b/internal/ground_spawn/database.go @@ -1,25 +1,34 @@ package ground_spawn import ( + "context" "fmt" - "eq2emu/internal/database" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) -// DatabaseAdapter implements the Database interface using the internal database wrapper +// DatabaseAdapter implements the Database interface using zombiezen.com/go/sqlite type DatabaseAdapter struct { - db *database.DB + pool *sqlitex.Pool } -// NewDatabaseAdapter creates a new database adapter using the database wrapper -func NewDatabaseAdapter(db *database.DB) *DatabaseAdapter { +// NewDatabaseAdapter creates a new database adapter using the sqlite pool +func NewDatabaseAdapter(pool *sqlitex.Pool) *DatabaseAdapter { return &DatabaseAdapter{ - db: db, + pool: pool, } } // LoadGroundSpawnEntries loads harvest entries for a ground spawn func (da *DatabaseAdapter) LoadGroundSpawnEntries(groundspawnID int32) ([]*GroundSpawnEntry, error) { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` SELECT min_skill_level, min_adventure_level, bonus_table, harvest_1, harvest_3, harvest_5, harvest_imbue, harvest_rare, harvest_10, harvest_coin @@ -29,27 +38,25 @@ func (da *DatabaseAdapter) LoadGroundSpawnEntries(groundspawnID int32) ([]*Groun ` var entries []*GroundSpawnEntry - - err := da.db.Query(query, func(row *database.Row) error { - entry := &GroundSpawnEntry{} - var bonusTable int32 - - entry.MinSkillLevel = int16(row.Int(0)) - entry.MinAdventureLevel = int16(row.Int(1)) - bonusTable = int32(row.Int(2)) - entry.Harvest1 = float32(row.Float(3)) - entry.Harvest3 = float32(row.Float(4)) - entry.Harvest5 = float32(row.Float(5)) - entry.HarvestImbue = float32(row.Float(6)) - entry.HarvestRare = float32(row.Float(7)) - entry.Harvest10 = float32(row.Float(8)) - entry.HarvestCoin = float32(row.Float(9)) - - entry.BonusTable = bonusTable == 1 - entries = append(entries, entry) - - return nil - }, groundspawnID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{groundspawnID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + entry := &GroundSpawnEntry{ + MinSkillLevel: int16(stmt.ColumnInt64(0)), + MinAdventureLevel: int16(stmt.ColumnInt64(1)), + BonusTable: stmt.ColumnInt64(2) == 1, + Harvest1: float32(stmt.ColumnFloat(3)), + Harvest3: float32(stmt.ColumnFloat(4)), + Harvest5: float32(stmt.ColumnFloat(5)), + HarvestImbue: float32(stmt.ColumnFloat(6)), + HarvestRare: float32(stmt.ColumnFloat(7)), + Harvest10: float32(stmt.ColumnFloat(8)), + HarvestCoin: float32(stmt.ColumnFloat(9)), + } + entries = append(entries, entry) + return nil + }, + }) if err != nil { return nil, fmt.Errorf("failed to query groundspawn entries: %w", err) @@ -60,6 +67,13 @@ func (da *DatabaseAdapter) LoadGroundSpawnEntries(groundspawnID int32) ([]*Groun // LoadGroundSpawnItems loads harvest items for a ground spawn func (da *DatabaseAdapter) LoadGroundSpawnItems(groundspawnID int32) ([]*GroundSpawnEntryItem, error) { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` SELECT item_id, is_rare, grid_id, quantity FROM groundspawn_items @@ -68,19 +82,19 @@ func (da *DatabaseAdapter) LoadGroundSpawnItems(groundspawnID int32) ([]*GroundS ` var items []*GroundSpawnEntryItem - - err := da.db.Query(query, func(row *database.Row) error { - item := &GroundSpawnEntryItem{} - - item.ItemID = int32(row.Int(0)) - item.IsRare = int8(row.Int(1)) - item.GridID = int32(row.Int(2)) - item.Quantity = int16(row.Int(3)) - - items = append(items, item) - - return nil - }, groundspawnID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{groundspawnID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + item := &GroundSpawnEntryItem{ + ItemID: int32(stmt.ColumnInt64(0)), + IsRare: int8(stmt.ColumnInt64(1)), + GridID: int32(stmt.ColumnInt64(2)), + Quantity: int16(stmt.ColumnInt64(3)), + } + items = append(items, item) + return nil + }, + }) if err != nil { return nil, fmt.Errorf("failed to query groundspawn items: %w", err) @@ -95,6 +109,13 @@ func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error { return fmt.Errorf("ground spawn cannot be nil") } + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` INSERT OR REPLACE INTO groundspawns ( id, name, x, y, z, heading, respawn_timer, collection_skill, @@ -112,21 +133,23 @@ func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error { // TODO: Get actual zone ID from spawn zoneID := int32(1) - err := da.db.Exec(query, - gs.GetID(), - gs.GetName(), - gs.GetX(), - gs.GetY(), - gs.GetZ(), - int16(gs.GetHeading()), - 300, // Default 5 minutes respawn timer - gs.GetCollectionSkill(), - gs.GetNumberHarvests(), - gs.GetAttemptsPerHarvest(), - gs.GetGroundSpawnEntryID(), - randomizeHeading, - zoneID, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + gs.GetID(), + gs.GetName(), + gs.GetX(), + gs.GetY(), + gs.GetZ(), + int16(gs.GetHeading()), + 300, // Default 5 minutes respawn timer + gs.GetCollectionSkill(), + gs.GetNumberHarvests(), + gs.GetAttemptsPerHarvest(), + gs.GetGroundSpawnEntryID(), + randomizeHeading, + zoneID, + }, + }) if err != nil { return fmt.Errorf("failed to save ground spawn %d: %w", gs.GetID(), err) @@ -137,6 +160,13 @@ func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error { // LoadAllGroundSpawns loads all ground spawns from the database func (da *DatabaseAdapter) LoadAllGroundSpawns() ([]*GroundSpawn, error) { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` SELECT id, name, x, y, z, heading, collection_skill, number_harvests, attempts_per_harvest, groundspawn_entry_id, randomize_heading @@ -149,44 +179,45 @@ func (da *DatabaseAdapter) LoadAllGroundSpawns() ([]*GroundSpawn, error) { zoneID := int32(1) var groundSpawns []*GroundSpawn + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{zoneID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + id := int32(stmt.ColumnInt64(0)) + name := stmt.ColumnText(1) + x := float32(stmt.ColumnFloat(2)) + y := float32(stmt.ColumnFloat(3)) + z := float32(stmt.ColumnFloat(4)) + heading := float32(stmt.ColumnFloat(5)) + collectionSkill := stmt.ColumnText(6) + numberHarvests := int8(stmt.ColumnInt64(7)) + attemptsPerHarvest := int8(stmt.ColumnInt64(8)) + groundspawnEntryID := int32(stmt.ColumnInt64(9)) + randomizeHeading := stmt.ColumnInt64(10) == 1 - err := da.db.Query(query, func(row *database.Row) error { - id := int32(row.Int(0)) - name := row.Text(1) - x := float32(row.Float(2)) - y := float32(row.Float(3)) - z := float32(row.Float(4)) - heading := float32(row.Float(5)) - collectionSkill := row.Text(6) - numberHarvests := int8(row.Int(7)) - attemptsPerHarvest := int8(row.Int(8)) - groundspawnEntryID := int32(row.Int(9)) - randomizeHeading := int32(row.Int(10)) + config := GroundSpawnConfig{ + GroundSpawnID: groundspawnEntryID, + CollectionSkill: collectionSkill, + NumberHarvests: numberHarvests, + AttemptsPerHarvest: attemptsPerHarvest, + RandomizeHeading: randomizeHeading, + Location: SpawnLocation{ + X: x, + Y: y, + Z: z, + Heading: heading, + GridID: 1, // TODO: Load from database + }, + Name: name, + Description: fmt.Sprintf("A %s node", collectionSkill), + } - config := GroundSpawnConfig{ - GroundSpawnID: groundspawnEntryID, - CollectionSkill: collectionSkill, - NumberHarvests: numberHarvests, - AttemptsPerHarvest: attemptsPerHarvest, - RandomizeHeading: randomizeHeading == 1, - Location: SpawnLocation{ - X: x, - Y: y, - Z: z, - Heading: heading, - GridID: 1, // TODO: Load from database - }, - Name: name, - Description: fmt.Sprintf("A %s node", collectionSkill), - } + gs := NewGroundSpawn(config) + gs.SetID(id) - gs := NewGroundSpawn(config) - gs.SetID(id) - - groundSpawns = append(groundSpawns, gs) - - return nil - }, zoneID) + groundSpawns = append(groundSpawns, gs) + return nil + }, + }) if err != nil { return nil, fmt.Errorf("failed to query groundspawns: %w", err) @@ -197,9 +228,18 @@ func (da *DatabaseAdapter) LoadAllGroundSpawns() ([]*GroundSpawn, error) { // DeleteGroundSpawn deletes a ground spawn from the database func (da *DatabaseAdapter) DeleteGroundSpawn(id int32) error { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := `DELETE FROM groundspawns WHERE id = ?` - err := da.db.Exec(query, id) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{id}, + }) if err != nil { return fmt.Errorf("failed to delete ground spawn %d: %w", id, err) } @@ -209,6 +249,13 @@ func (da *DatabaseAdapter) DeleteGroundSpawn(id int32) error { // LoadPlayerHarvestStatistics loads harvest statistics for a player func (da *DatabaseAdapter) LoadPlayerHarvestStatistics(playerID int32) (map[string]int64, error) { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` SELECT skill_name, harvest_count FROM player_harvest_stats @@ -216,15 +263,15 @@ func (da *DatabaseAdapter) LoadPlayerHarvestStatistics(playerID int32) (map[stri ` stats := make(map[string]int64) - - err := da.db.Query(query, func(row *database.Row) error { - skillName := row.Text(0) - harvestCount := row.Int64(1) - - stats[skillName] = harvestCount - - return nil - }, playerID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{playerID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + skillName := stmt.ColumnText(0) + harvestCount := stmt.ColumnInt64(1) + stats[skillName] = harvestCount + return nil + }, + }) if err != nil { return nil, fmt.Errorf("failed to query player harvest stats: %w", err) @@ -235,15 +282,125 @@ func (da *DatabaseAdapter) LoadPlayerHarvestStatistics(playerID int32) (map[stri // SavePlayerHarvestStatistic saves a player's harvest statistic func (da *DatabaseAdapter) SavePlayerHarvestStatistic(playerID int32, skillName string, count int64) error { + ctx := context.Background() + conn, err := da.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + query := ` INSERT OR REPLACE INTO player_harvest_stats (player_id, skill_name, harvest_count, updated_at) VALUES (?, ?, ?, datetime('now')) ` - err := da.db.Exec(query, playerID, skillName, count) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{playerID, skillName, count}, + }) if err != nil { return fmt.Errorf("failed to save player harvest stat: %w", err) } return nil } + +// EnsureGroundSpawnTables creates the necessary database tables if they don't exist +func (da *DatabaseAdapter) EnsureGroundSpawnTables(ctx context.Context) error { + conn, err := da.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer da.pool.Put(conn) + + // Create groundspawns table + createGroundSpawnsTable := ` + CREATE TABLE IF NOT EXISTS groundspawns ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL, + z REAL NOT NULL, + heading REAL NOT NULL, + respawn_timer INTEGER NOT NULL DEFAULT 300, + collection_skill TEXT NOT NULL, + number_harvests INTEGER NOT NULL DEFAULT 1, + attempts_per_harvest INTEGER NOT NULL DEFAULT 1, + groundspawn_entry_id INTEGER NOT NULL, + randomize_heading INTEGER NOT NULL DEFAULT 0, + zone_id INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ` + + // Create groundspawn_entries table + createGroundSpawnEntriesTable := ` + CREATE TABLE IF NOT EXISTS groundspawn_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + groundspawn_id INTEGER NOT NULL, + min_skill_level INTEGER NOT NULL DEFAULT 0, + min_adventure_level INTEGER NOT NULL DEFAULT 0, + bonus_table INTEGER NOT NULL DEFAULT 0, + harvest_1 REAL NOT NULL DEFAULT 0.0, + harvest_3 REAL NOT NULL DEFAULT 0.0, + harvest_5 REAL NOT NULL DEFAULT 0.0, + harvest_imbue REAL NOT NULL DEFAULT 0.0, + harvest_rare REAL NOT NULL DEFAULT 0.0, + harvest_10 REAL NOT NULL DEFAULT 0.0, + harvest_coin REAL NOT NULL DEFAULT 0.0 + ) + ` + + // Create groundspawn_items table + createGroundSpawnItemsTable := ` + CREATE TABLE IF NOT EXISTS groundspawn_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + groundspawn_id INTEGER NOT NULL, + item_id INTEGER NOT NULL, + is_rare INTEGER NOT NULL DEFAULT 0, + grid_id INTEGER NOT NULL DEFAULT 0, + quantity INTEGER NOT NULL DEFAULT 1 + ) + ` + + // Create player_harvest_stats table + createPlayerHarvestStatsTable := ` + CREATE TABLE IF NOT EXISTS player_harvest_stats ( + player_id INTEGER NOT NULL, + skill_name TEXT NOT NULL, + harvest_count INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (player_id, skill_name) + ) + ` + + // Execute table creation statements + tables := []string{ + createGroundSpawnsTable, + createGroundSpawnEntriesTable, + createGroundSpawnItemsTable, + createPlayerHarvestStatsTable, + } + + for _, tableSQL := range tables { + if err := sqlitex.Execute(conn, tableSQL, nil); err != nil { + return fmt.Errorf("failed to create table: %w", err) + } + } + + // Create indexes + indexes := []string{ + `CREATE INDEX IF NOT EXISTS idx_groundspawn_entries_groundspawn_id ON groundspawn_entries(groundspawn_id)`, + `CREATE INDEX IF NOT EXISTS idx_groundspawn_items_groundspawn_id ON groundspawn_items(groundspawn_id)`, + `CREATE INDEX IF NOT EXISTS idx_groundspawns_zone_id ON groundspawns(zone_id)`, + `CREATE INDEX IF NOT EXISTS idx_player_harvest_stats_player_id ON player_harvest_stats(player_id)`, + } + + for _, indexSQL := range indexes { + if err := sqlitex.Execute(conn, indexSQL, nil); err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + } + + return nil +} \ No newline at end of file