package ground_spawn import ( "context" "fmt" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseAdapter implements the Database interface using zombiezen.com/go/sqlite type DatabaseAdapter struct { pool *sqlitex.Pool } // NewDatabaseAdapter creates a new database adapter using the sqlite pool func NewDatabaseAdapter(pool *sqlitex.Pool) *DatabaseAdapter { return &DatabaseAdapter{ 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 FROM groundspawn_entries WHERE groundspawn_id = ? ORDER BY min_skill_level ASC ` var entries []*GroundSpawnEntry 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) } return entries, nil } // 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 WHERE groundspawn_id = ? ORDER BY item_id ASC ` var items []*GroundSpawnEntryItem 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) } return items, nil } // SaveGroundSpawn saves a ground spawn to the database func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error { if gs == nil { 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, number_harvests, attempts_per_harvest, groundspawn_entry_id, randomize_heading, zone_id, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) ` randomizeHeading := 0 if gs.GetRandomizeHeading() { randomizeHeading = 1 } // TODO: Get actual zone ID from spawn zoneID := int32(1) 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) } return nil } // 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 FROM groundspawns WHERE zone_id = ? ORDER BY id ASC ` // TODO: Support multiple zones 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 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), } gs := NewGroundSpawn(config) gs.SetID(id) groundSpawns = append(groundSpawns, gs) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query groundspawns: %w", err) } return groundSpawns, nil } // 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 = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{id}, }) if err != nil { return fmt.Errorf("failed to delete ground spawn %d: %w", id, err) } return nil } // 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 WHERE player_id = ? ` stats := make(map[string]int64) 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) } return stats, nil } // 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 = 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 }