package tradeskills import ( "database/sql" "fmt" "log" ) // DatabaseService handles database operations for the tradeskills system. type DatabaseService interface { // LoadTradeskillEvents loads all tradeskill events from the database LoadTradeskillEvents(masterList *MasterTradeskillEventsList) error // CreateTradeskillTables creates the required database tables CreateTradeskillTables() error // SaveTradeskillEvent saves a tradeskill event to the database SaveTradeskillEvent(event *TradeskillEvent) error // DeleteTradeskillEvent removes a tradeskill event from the database DeleteTradeskillEvent(name string, technique uint32) error } // SQLiteTradeskillDatabase implements DatabaseService for SQLite. type SQLiteTradeskillDatabase struct { db *sql.DB } // NewSQLiteTradeskillDatabase creates a new SQLite database service. func NewSQLiteTradeskillDatabase(db *sql.DB) *SQLiteTradeskillDatabase { return &SQLiteTradeskillDatabase{ db: db, } } // CreateTradeskillTables creates the required database tables for tradeskills. func (db *SQLiteTradeskillDatabase) CreateTradeskillTables() error { createTableSQL := ` CREATE TABLE IF NOT EXISTS tradeskillevents ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, icon INTEGER NOT NULL, technique INTEGER NOT NULL, success_progress INTEGER NOT NULL DEFAULT 0, success_durability INTEGER NOT NULL DEFAULT 0, success_hp INTEGER NOT NULL DEFAULT 0, success_power INTEGER NOT NULL DEFAULT 0, success_spell_id INTEGER NOT NULL DEFAULT 0, success_item_id INTEGER NOT NULL DEFAULT 0, fail_progress INTEGER NOT NULL DEFAULT 0, fail_durability INTEGER NOT NULL DEFAULT 0, fail_hp INTEGER NOT NULL DEFAULT 0, fail_power INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(name, technique) ); CREATE INDEX IF NOT EXISTS idx_tradeskillevents_technique ON tradeskillevents(technique); CREATE INDEX IF NOT EXISTS idx_tradeskillevents_name ON tradeskillevents(name); ` _, err := db.db.Exec(createTableSQL) if err != nil { return fmt.Errorf("failed to create tradeskillevents table: %w", err) } log.Printf("Created tradeskillevents table") return nil } // LoadTradeskillEvents loads all tradeskill events from the database into the master list. func (db *SQLiteTradeskillDatabase) LoadTradeskillEvents(masterList *MasterTradeskillEventsList) error { if masterList == nil { return fmt.Errorf("masterList cannot be nil") } query := ` SELECT name, icon, technique, success_progress, success_durability, success_hp, success_power, success_spell_id, success_item_id, fail_progress, fail_durability, fail_hp, fail_power FROM tradeskillevents ORDER BY technique, name ` rows, err := db.db.Query(query) if err != nil { return fmt.Errorf("failed to query tradeskillevents: %w", err) } defer rows.Close() eventsLoaded := 0 for rows.Next() { event := &TradeskillEvent{} err := rows.Scan( &event.Name, &event.Icon, &event.Technique, &event.SuccessProgress, &event.SuccessDurability, &event.SuccessHP, &event.SuccessPower, &event.SuccessSpellID, &event.SuccessItemID, &event.FailProgress, &event.FailDurability, &event.FailHP, &event.FailPower, ) if err != nil { log.Printf("Warning: Failed to scan tradeskill event row: %v", err) continue } // Validate the event if event.Name == "" { log.Printf("Warning: Skipping tradeskill event with empty name") continue } if !IsValidTechnique(event.Technique) { log.Printf("Warning: Skipping tradeskill event '%s' with invalid technique %d", event.Name, event.Technique) continue } masterList.AddEvent(event) eventsLoaded++ log.Printf("Loaded tradeskill event: %s (technique: %d)", event.Name, event.Technique) } if err = rows.Err(); err != nil { return fmt.Errorf("error iterating tradeskill events: %w", err) } log.Printf("Loaded %d tradeskill events", eventsLoaded) return nil } // SaveTradeskillEvent saves a tradeskill event to the database. func (db *SQLiteTradeskillDatabase) SaveTradeskillEvent(event *TradeskillEvent) error { if event == nil { return fmt.Errorf("event cannot be nil") } if event.Name == "" { return fmt.Errorf("event name cannot be empty") } if !IsValidTechnique(event.Technique) { return fmt.Errorf("invalid technique: %d", event.Technique) } insertSQL := ` INSERT OR REPLACE INTO tradeskillevents ( name, icon, technique, success_progress, success_durability, success_hp, success_power, success_spell_id, success_item_id, fail_progress, fail_durability, fail_hp, fail_power, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ` _, err := db.db.Exec(insertSQL, event.Name, event.Icon, event.Technique, event.SuccessProgress, event.SuccessDurability, event.SuccessHP, event.SuccessPower, event.SuccessSpellID, event.SuccessItemID, event.FailProgress, event.FailDurability, event.FailHP, event.FailPower, ) if err != nil { return fmt.Errorf("failed to save tradeskill event '%s': %w", event.Name, err) } log.Printf("Saved tradeskill event: %s", event.Name) return nil } // DeleteTradeskillEvent removes a tradeskill event from the database. func (db *SQLiteTradeskillDatabase) DeleteTradeskillEvent(name string, technique uint32) error { if name == "" { return fmt.Errorf("name cannot be empty") } deleteSQL := `DELETE FROM tradeskillevents WHERE name = ? AND technique = ?` result, err := db.db.Exec(deleteSQL, name, technique) if err != nil { return fmt.Errorf("failed to delete tradeskill event '%s': %w", name, err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected for event '%s': %w", name, err) } if rowsAffected == 0 { return fmt.Errorf("tradeskill event '%s' with technique %d not found", name, technique) } log.Printf("Deleted tradeskill event: %s (technique: %d)", name, technique) return nil } // GetTradeskillEventsByTechnique retrieves all events for a specific technique from the database. func (db *SQLiteTradeskillDatabase) GetTradeskillEventsByTechnique(technique uint32) ([]*TradeskillEvent, error) { if !IsValidTechnique(technique) { return nil, fmt.Errorf("invalid technique: %d", technique) } query := ` SELECT name, icon, technique, success_progress, success_durability, success_hp, success_power, success_spell_id, success_item_id, fail_progress, fail_durability, fail_hp, fail_power FROM tradeskillevents WHERE technique = ? ORDER BY name ` rows, err := db.db.Query(query, technique) if err != nil { return nil, fmt.Errorf("failed to query events for technique %d: %w", technique, err) } defer rows.Close() var events []*TradeskillEvent for rows.Next() { event := &TradeskillEvent{} err := rows.Scan( &event.Name, &event.Icon, &event.Technique, &event.SuccessProgress, &event.SuccessDurability, &event.SuccessHP, &event.SuccessPower, &event.SuccessSpellID, &event.SuccessItemID, &event.FailProgress, &event.FailDurability, &event.FailHP, &event.FailPower, ) if err != nil { log.Printf("Warning: Failed to scan event row: %v", err) continue } events = append(events, event) } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating events for technique %d: %w", technique, err) } return events, nil } // GetTradeskillEventByName retrieves a specific event by name and technique. func (db *SQLiteTradeskillDatabase) GetTradeskillEventByName(name string, technique uint32) (*TradeskillEvent, error) { if name == "" { return nil, fmt.Errorf("name cannot be empty") } if !IsValidTechnique(technique) { return nil, fmt.Errorf("invalid technique: %d", technique) } query := ` SELECT name, icon, technique, success_progress, success_durability, success_hp, success_power, success_spell_id, success_item_id, fail_progress, fail_durability, fail_hp, fail_power FROM tradeskillevents WHERE name = ? AND technique = ? ` row := db.db.QueryRow(query, name, technique) event := &TradeskillEvent{} err := row.Scan( &event.Name, &event.Icon, &event.Technique, &event.SuccessProgress, &event.SuccessDurability, &event.SuccessHP, &event.SuccessPower, &event.SuccessSpellID, &event.SuccessItemID, &event.FailProgress, &event.FailDurability, &event.FailHP, &event.FailPower, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("tradeskill event '%s' with technique %d not found", name, technique) } return nil, fmt.Errorf("failed to get tradeskill event '%s': %w", name, err) } return event, nil } // CountTradeskillEvents returns the total number of events in the database. func (db *SQLiteTradeskillDatabase) CountTradeskillEvents() (int32, error) { query := `SELECT COUNT(*) FROM tradeskillevents` var count int32 err := db.db.QueryRow(query).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to count tradeskill events: %w", err) } return count, nil } // GetTechniqueCounts returns the number of events per technique. func (db *SQLiteTradeskillDatabase) GetTechniqueCounts() (map[uint32]int32, error) { query := ` SELECT technique, COUNT(*) as count FROM tradeskillevents GROUP BY technique ORDER BY technique ` rows, err := db.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query technique counts: %w", err) } defer rows.Close() counts := make(map[uint32]int32) for rows.Next() { var technique uint32 var count int32 err := rows.Scan(&technique, &count) if err != nil { log.Printf("Warning: Failed to scan technique count row: %v", err) continue } counts[technique] = count } if err = rows.Err(); err != nil { return nil, fmt.Errorf("error iterating technique counts: %w", err) } return counts, nil } // InsertDefaultTradeskillEvents adds some default tradeskill events to the database. func (db *SQLiteTradeskillDatabase) InsertDefaultTradeskillEvents() error { defaultEvents := []*TradeskillEvent{ { Name: "Blazing Heat", Icon: 1234, Technique: TechniqueSkillAlchemy, SuccessProgress: 25, SuccessDurability: 0, SuccessHP: 0, SuccessPower: -10, SuccessSpellID: 0, SuccessItemID: 0, FailProgress: -10, FailDurability: -25, FailHP: 0, FailPower: 0, }, { Name: "Precise Cut", Icon: 5678, Technique: TechniqueSkillFletching, SuccessProgress: 30, SuccessDurability: 5, SuccessHP: 0, SuccessPower: -5, SuccessSpellID: 0, SuccessItemID: 0, FailProgress: 0, FailDurability: -30, FailHP: 0, FailPower: 0, }, { Name: "Perfect Stitch", Icon: 9012, Technique: TechniqueSkillTailoring, SuccessProgress: 20, SuccessDurability: 10, SuccessHP: 0, SuccessPower: -8, SuccessSpellID: 0, SuccessItemID: 0, FailProgress: -5, FailDurability: -15, FailHP: 0, FailPower: 0, }, } for _, event := range defaultEvents { err := db.SaveTradeskillEvent(event) if err != nil { log.Printf("Warning: Failed to insert default event '%s': %v", event.Name, err) } } log.Printf("Inserted %d default tradeskill events", len(defaultEvents)) return nil }