package database import ( "database/sql" "fmt" "sync" _ "modernc.org/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // Database wraps the SQL database connection type Database struct { db *sql.DB pool *sqlitex.Pool // For achievements system compatibility dbPath string // Store path for pool creation mutex sync.RWMutex } // New creates a new database connection func New(path string) (*Database, error) { db, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } // Set connection pool settings db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) // Create sqlitex pool for achievements system pool, err := sqlitex.NewPool(path, sqlitex.PoolOptions{ PoolSize: 5, }) if err != nil { return nil, fmt.Errorf("failed to create sqlite pool: %w", err) } d := &Database{ db: db, pool: pool, dbPath: path, } // Initialize schema if err := d.initSchema(); err != nil { return nil, fmt.Errorf("failed to initialize schema: %w", err) } return d, nil } // Close closes the database connection func (d *Database) Close() error { if d.pool != nil { d.pool.Close() } return d.db.Close() } // GetPool returns the sqlitex pool for achievements system compatibility func (d *Database) GetPool() *sqlitex.Pool { return d.pool } // initSchema creates the database schema if it doesn't exist func (d *Database) initSchema() error { schemas := []string{ // Rules table `CREATE TABLE IF NOT EXISTS rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL, description TEXT, UNIQUE(category, name) )`, // Accounts table `CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, email TEXT, admin_level INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME )`, // Characters table `CREATE TABLE IF NOT EXISTS characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, name TEXT UNIQUE NOT NULL, race_id INTEGER NOT NULL, class_id INTEGER NOT NULL, level INTEGER DEFAULT 1, x REAL DEFAULT 0, y REAL DEFAULT 0, z REAL DEFAULT 0, heading REAL DEFAULT 0, zone_id INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_played DATETIME, FOREIGN KEY(account_id) REFERENCES accounts(id) )`, // Zones table `CREATE TABLE IF NOT EXISTS zones ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, file TEXT NOT NULL, description TEXT, motd TEXT, min_level INTEGER DEFAULT 0, max_level INTEGER DEFAULT 100, min_version INTEGER DEFAULT 0, xp_modifier REAL DEFAULT 1.0, city_zone INTEGER DEFAULT 0, weather_allowed INTEGER DEFAULT 1, safe_x REAL DEFAULT 0, safe_y REAL DEFAULT 0, safe_z REAL DEFAULT 0, safe_heading REAL DEFAULT 0 )`, // Server statistics table `CREATE TABLE IF NOT EXISTS server_stats ( stat_id INTEGER PRIMARY KEY, stat_value INTEGER, stat_date INTEGER, save_needed INTEGER DEFAULT 0 )`, // Merchant tables `CREATE TABLE IF NOT EXISTS merchants ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, merchant_type INTEGER DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS merchant_items ( merchant_id INTEGER NOT NULL, item_id INTEGER NOT NULL, quantity INTEGER DEFAULT -1, price_coins INTEGER DEFAULT 0, price_status INTEGER DEFAULT 0, PRIMARY KEY(merchant_id, item_id), FOREIGN KEY(merchant_id) REFERENCES merchants(id) )`, // Achievement tables `CREATE TABLE IF NOT EXISTS achievements ( achievement_id INTEGER PRIMARY KEY, title TEXT NOT NULL, uncompleted_text TEXT, completed_text TEXT, category TEXT, expansion TEXT, icon INTEGER DEFAULT 0, point_value INTEGER DEFAULT 0, qty_req INTEGER DEFAULT 1, hide_achievement INTEGER DEFAULT 0, unknown3a INTEGER DEFAULT 0, unknown3b INTEGER DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS achievements_requirements ( achievement_id INTEGER NOT NULL, name TEXT NOT NULL, qty_req INTEGER DEFAULT 1, PRIMARY KEY(achievement_id, name), FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS achievements_rewards ( achievement_id INTEGER NOT NULL, reward TEXT NOT NULL, PRIMARY KEY(achievement_id, reward), FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_achievements ( char_id INTEGER NOT NULL, achievement_id INTEGER NOT NULL, completed_date INTEGER, PRIMARY KEY(char_id, achievement_id), FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE, FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_achievements_items ( char_id INTEGER NOT NULL, achievement_id INTEGER NOT NULL, items INTEGER DEFAULT 0, PRIMARY KEY(char_id, achievement_id), FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE, FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE )`, // Title tables `CREATE TABLE IF NOT EXISTS titles ( title_id INTEGER PRIMARY KEY, text TEXT NOT NULL, category INTEGER DEFAULT 0, rarity INTEGER DEFAULT 0, position INTEGER DEFAULT 0, description TEXT, is_unique INTEGER DEFAULT 0, is_hidden INTEGER DEFAULT 0, color_code TEXT, requirements TEXT, source_type INTEGER DEFAULT 0, source_id INTEGER DEFAULT 0, created_date INTEGER, expire_date INTEGER )`, `CREATE TABLE IF NOT EXISTS character_titles ( char_id INTEGER NOT NULL, title_id INTEGER NOT NULL, source_achievement_id INTEGER DEFAULT 0, source_quest_id INTEGER DEFAULT 0, granted_date INTEGER, expire_date INTEGER, PRIMARY KEY(char_id, title_id), FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE, FOREIGN KEY(title_id) REFERENCES titles(title_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_active_titles ( char_id INTEGER PRIMARY KEY, prefix_title_id INTEGER DEFAULT 0, suffix_title_id INTEGER DEFAULT 0, FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE, FOREIGN KEY(prefix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL, FOREIGN KEY(suffix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL )`, } for _, schema := range schemas { if _, err := d.db.Exec(schema); err != nil { return fmt.Errorf("failed to create schema: %w", err) } } return nil } // Query executes a query that returns rows func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) { return d.db.Query(query, args...) } // QueryRow executes a query that returns a single row func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row { return d.db.QueryRow(query, args...) } // Exec executes a query that doesn't return rows func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) { return d.db.Exec(query, args...) } // Begin starts a transaction func (d *Database) Begin() (*sql.Tx, error) { return d.db.Begin() } // LoadRules loads all rules from the database func (d *Database) LoadRules() (map[string]map[string]string, error) { rows, err := d.Query("SELECT category, name, value FROM rules") if err != nil { return nil, err } defer rows.Close() rules := make(map[string]map[string]string) for rows.Next() { var category, name, value string if err := rows.Scan(&category, &name, &value); err != nil { return nil, err } if rules[category] == nil { rules[category] = make(map[string]string) } rules[category][name] = value } return rules, rows.Err() } // SaveRule saves a rule to the database func (d *Database) SaveRule(category, name, value, description string) error { _, err := d.Exec(` INSERT OR REPLACE INTO rules (category, name, value, description) VALUES (?, ?, ?, ?) `, category, name, value, description) return err } // GetZones retrieves all zones from the database func (d *Database) GetZones() ([]map[string]interface{}, error) { rows, err := d.Query(` SELECT id, name, file, description, motd, min_level, max_level, min_version, xp_modifier, city_zone, weather_allowed, safe_x, safe_y, safe_z, safe_heading FROM zones ORDER BY name `) if err != nil { return nil, err } defer rows.Close() var zones []map[string]interface{} for rows.Next() { zone := make(map[string]interface{}) var id, minLevel, maxLevel, minVersion int var name, file, description, motd string var xpModifier, safeX, safeY, safeZ, safeHeading float64 var cityZone, weatherAllowed bool err := rows.Scan(&id, &name, &file, &description, &motd, &minLevel, &maxLevel, &minVersion, &xpModifier, &cityZone, &weatherAllowed, &safeX, &safeY, &safeZ, &safeHeading) if err != nil { return nil, err } zone["id"] = id zone["name"] = name zone["file"] = file zone["description"] = description zone["motd"] = motd zone["min_level"] = minLevel zone["max_level"] = maxLevel zone["min_version"] = minVersion zone["xp_modifier"] = xpModifier zone["city_zone"] = cityZone zone["weather_allowed"] = weatherAllowed zone["safe_x"] = safeX zone["safe_y"] = safeY zone["safe_z"] = safeZ zone["safe_heading"] = safeHeading zones = append(zones, zone) } return zones, rows.Err() }