package factions import ( "context" "fmt" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseAdapter implements the factions.Database interface using sqlitex.Pool type DatabaseAdapter struct { pool *sqlitex.Pool } // NewDatabaseAdapter creates a new database adapter for factions func NewDatabaseAdapter(pool *sqlitex.Pool) *DatabaseAdapter { return &DatabaseAdapter{pool: pool} } // LoadAllFactions loads all factions from the database func (da *DatabaseAdapter) LoadAllFactions() ([]*Faction, error) { conn, err := da.pool.Take(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) // Create factions table if it doesn't exist err = sqlitex.Execute(conn, ` CREATE TABLE IF NOT EXISTS factions ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT, description TEXT, negative_change INTEGER DEFAULT 0, positive_change INTEGER DEFAULT 0, default_value INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `, nil) if err != nil { return nil, fmt.Errorf("failed to create factions table: %w", err) } var factions []*Faction err = sqlitex.Execute(conn, "SELECT id, name, type, description, negative_change, positive_change, default_value FROM factions", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { faction := &Faction{ ID: int32(stmt.ColumnInt64(0)), Name: stmt.ColumnText(1), Type: stmt.ColumnText(2), Description: stmt.ColumnText(3), NegativeChange: int16(stmt.ColumnInt64(4)), PositiveChange: int16(stmt.ColumnInt64(5)), DefaultValue: int32(stmt.ColumnInt64(6)), } factions = append(factions, faction) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load factions: %w", err) } return factions, nil } // SaveFaction saves a faction to the database func (da *DatabaseAdapter) SaveFaction(faction *Faction) error { if faction == nil { return fmt.Errorf("faction is nil") } conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) // Use INSERT OR REPLACE to handle both insert and update err = sqlitex.Execute(conn, ` INSERT OR REPLACE INTO factions (id, name, type, description, negative_change, positive_change, default_value, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, &sqlitex.ExecOptions{ Args: []any{faction.ID, faction.Name, faction.Type, faction.Description, faction.NegativeChange, faction.PositiveChange, faction.DefaultValue, time.Now().Unix()}, }) if err != nil { return fmt.Errorf("failed to save faction %d: %w", faction.ID, err) } return nil } // DeleteFaction deletes a faction from the database func (da *DatabaseAdapter) DeleteFaction(factionID int32) error { conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) err = sqlitex.Execute(conn, "DELETE FROM factions WHERE id = ?", &sqlitex.ExecOptions{ Args: []any{factionID}, }) if err != nil { return fmt.Errorf("failed to delete faction %d: %w", factionID, err) } return nil } // LoadHostileFactionRelations loads all hostile faction relations func (da *DatabaseAdapter) LoadHostileFactionRelations() ([]*FactionRelation, error) { conn, err := da.pool.Take(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) // Create faction_relations table if it doesn't exist err = sqlitex.Execute(conn, ` CREATE TABLE IF NOT EXISTS faction_relations ( faction_id INTEGER NOT NULL, related_faction_id INTEGER NOT NULL, is_hostile INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (faction_id, related_faction_id), FOREIGN KEY (faction_id) REFERENCES factions(id), FOREIGN KEY (related_faction_id) REFERENCES factions(id) ) `, nil) if err != nil { return nil, fmt.Errorf("failed to create faction_relations table: %w", err) } var relations []*FactionRelation err = sqlitex.Execute(conn, "SELECT faction_id, related_faction_id FROM faction_relations WHERE is_hostile = 1", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { relation := &FactionRelation{ FactionID: int32(stmt.ColumnInt64(0)), HostileFactionID: int32(stmt.ColumnInt64(1)), } relations = append(relations, relation) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load hostile faction relations: %w", err) } return relations, nil } // LoadFriendlyFactionRelations loads all friendly faction relations func (da *DatabaseAdapter) LoadFriendlyFactionRelations() ([]*FactionRelation, error) { conn, err := da.pool.Take(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) var relations []*FactionRelation err = sqlitex.Execute(conn, "SELECT faction_id, related_faction_id FROM faction_relations WHERE is_hostile = 0", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { relation := &FactionRelation{ FactionID: int32(stmt.ColumnInt64(0)), FriendlyFactionID: int32(stmt.ColumnInt64(1)), } relations = append(relations, relation) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load friendly faction relations: %w", err) } return relations, nil } // SaveFactionRelation saves a faction relation to the database func (da *DatabaseAdapter) SaveFactionRelation(relation *FactionRelation) error { if relation == nil { return fmt.Errorf("faction relation is nil") } conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) var relatedFactionID int32 var isHostile int if relation.HostileFactionID != 0 { relatedFactionID = relation.HostileFactionID isHostile = 1 } else if relation.FriendlyFactionID != 0 { relatedFactionID = relation.FriendlyFactionID isHostile = 0 } else { return fmt.Errorf("faction relation has no related faction ID") } err = sqlitex.Execute(conn, ` INSERT OR REPLACE INTO faction_relations (faction_id, related_faction_id, is_hostile) VALUES (?, ?, ?) `, &sqlitex.ExecOptions{ Args: []any{relation.FactionID, relatedFactionID, isHostile}, }) if err != nil { return fmt.Errorf("failed to save faction relation %d -> %d: %w", relation.FactionID, relatedFactionID, err) } return nil } // DeleteFactionRelation deletes a faction relation from the database func (da *DatabaseAdapter) DeleteFactionRelation(factionID, relatedFactionID int32, isHostile bool) error { conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) hostileFlag := 0 if isHostile { hostileFlag = 1 } err = sqlitex.Execute(conn, "DELETE FROM faction_relations WHERE faction_id = ? AND related_faction_id = ? AND is_hostile = ?", &sqlitex.ExecOptions{ Args: []any{factionID, relatedFactionID, hostileFlag}, }) if err != nil { return fmt.Errorf("failed to delete faction relation %d -> %d: %w", factionID, relatedFactionID, err) } return nil } // LoadPlayerFactions loads player faction values from the database func (da *DatabaseAdapter) LoadPlayerFactions(playerID int32) (map[int32]int32, error) { conn, err := da.pool.Take(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) // Create player_factions table if it doesn't exist err = sqlitex.Execute(conn, ` CREATE TABLE IF NOT EXISTS player_factions ( player_id INTEGER NOT NULL, faction_id INTEGER NOT NULL, faction_value INTEGER NOT NULL DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (player_id, faction_id), FOREIGN KEY (faction_id) REFERENCES factions(id) ) `, nil) if err != nil { return nil, fmt.Errorf("failed to create player_factions table: %w", err) } factionValues := make(map[int32]int32) err = sqlitex.Execute(conn, "SELECT faction_id, faction_value FROM player_factions WHERE player_id = ?", &sqlitex.ExecOptions{ Args: []any{playerID}, ResultFunc: func(stmt *sqlite.Stmt) error { factionID := int32(stmt.ColumnInt64(0)) factionValue := int32(stmt.ColumnInt64(1)) factionValues[factionID] = factionValue return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load player factions for player %d: %w", playerID, err) } return factionValues, nil } // SavePlayerFaction saves a player's faction value to the database func (da *DatabaseAdapter) SavePlayerFaction(playerID, factionID, factionValue int32) error { conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) err = sqlitex.Execute(conn, ` INSERT OR REPLACE INTO player_factions (player_id, faction_id, faction_value, updated_at) VALUES (?, ?, ?, ?) `, &sqlitex.ExecOptions{ Args: []any{playerID, factionID, factionValue, time.Now().Unix()}, }) if err != nil { return fmt.Errorf("failed to save player faction %d/%d: %w", playerID, factionID, err) } return nil } // SaveAllPlayerFactions saves all faction values for a player func (da *DatabaseAdapter) SaveAllPlayerFactions(playerID int32, factionValues map[int32]int32) error { conn, err := da.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %w", err) } defer da.pool.Put(conn) // Use a transaction for atomic updates err = sqlitex.Execute(conn, "BEGIN", nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer sqlitex.Execute(conn, "ROLLBACK", nil) // Clear existing faction values for this player err = sqlitex.Execute(conn, "DELETE FROM player_factions WHERE player_id = ?", &sqlitex.ExecOptions{ Args: []any{playerID}, }) if err != nil { return fmt.Errorf("failed to clear player factions: %w", err) } // Insert all current faction values for factionID, factionValue := range factionValues { err = sqlitex.Execute(conn, ` INSERT INTO player_factions (player_id, faction_id, faction_value, updated_at) VALUES (?, ?, ?, ?) `, &sqlitex.ExecOptions{ Args: []any{playerID, factionID, factionValue, time.Now().Unix()}, }) if err != nil { return fmt.Errorf("failed to insert player faction %d/%d: %w", playerID, factionID, err) } } return sqlitex.Execute(conn, "COMMIT", nil) }