package db import ( "database/sql" "fmt" "time" _ "github.com/go-sql-driver/mysql" ) type Database struct { conn *sql.DB } func New(dsn string) (*Database, error) { conn, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } conn.SetMaxOpenConns(25) conn.SetMaxIdleConns(5) conn.SetConnMaxLifetime(5 * time.Minute) if err := conn.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } db := &Database{conn: conn} if err := db.createTables(); err != nil { return nil, fmt.Errorf("failed to create tables: %w", err) } return db, nil } func (db *Database) createTables() error { queries := []string{ `CREATE TABLE IF NOT EXISTS players ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, INDEX idx_username (username) )`, `CREATE TABLE IF NOT EXISTS player_positions ( player_id INT PRIMARY KEY, x FLOAT NOT NULL DEFAULT 0, y FLOAT NOT NULL DEFAULT 0, z FLOAT NOT NULL DEFAULT 0, yaw FLOAT NOT NULL DEFAULT 0, pitch FLOAT NOT NULL DEFAULT 0, world VARCHAR(255) DEFAULT 'main', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS player_sessions ( id INT AUTO_INCREMENT PRIMARY KEY, player_id INT NOT NULL, token VARCHAR(255) UNIQUE NOT NULL, server_type VARCHAR(50) NOT NULL, server_id VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, INDEX idx_token (token), INDEX idx_player (player_id), FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE )`, } for _, query := range queries { if _, err := db.conn.Exec(query); err != nil { return fmt.Errorf("failed to execute query: %w", err) } } return nil } func (db *Database) Close() error { return db.conn.Close() } func (db *Database) CreatePlayer(username, passwordHash string) (int64, error) { result, err := db.conn.Exec( "INSERT INTO players (username, password_hash) VALUES (?, ?)", username, passwordHash, ) if err != nil { return 0, err } return result.LastInsertId() } func (db *Database) GetPlayerByUsername(username string) (*Player, error) { p := &Player{} err := db.conn.QueryRow( "SELECT id, username, password_hash FROM players WHERE username = ?", username, ).Scan(&p.ID, &p.Username, &p.PasswordHash) if err != nil { return nil, err } return p, nil } func (db *Database) GetPlayerByID(playerID int64) (*Player, error) { p := &Player{} err := db.conn.QueryRow( "SELECT id, username, password_hash FROM players WHERE id = ?", playerID, ).Scan(&p.ID, &p.Username, &p.PasswordHash) if err != nil { return nil, err } return p, nil } func (db *Database) UpdateLastLogin(playerID int64) error { _, err := db.conn.Exec( "UPDATE players SET last_login = CURRENT_TIMESTAMP WHERE id = ?", playerID, ) return err } func (db *Database) SavePlayerPosition(playerID int64, pos *Position) error { _, err := db.conn.Exec(` INSERT INTO player_positions (player_id, x, y, z, yaw, pitch, world) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE x = VALUES(x), y = VALUES(y), z = VALUES(z), yaw = VALUES(yaw), pitch = VALUES(pitch), world = VALUES(world) `, playerID, pos.X, pos.Y, pos.Z, pos.Yaw, pos.Pitch, pos.World) return err } func (db *Database) GetPlayerPosition(playerID int64) (*Position, error) { pos := &Position{} err := db.conn.QueryRow( "SELECT x, y, z, yaw, pitch, world FROM player_positions WHERE player_id = ?", playerID, ).Scan(&pos.X, &pos.Y, &pos.Z, &pos.Yaw, &pos.Pitch, &pos.World) if err == sql.ErrNoRows { return &Position{X: 0, Y: 0, Z: 0, Yaw: 0, Pitch: 0, World: "main"}, nil } return pos, err } func (db *Database) CreateSession(playerID int64, token, serverType, serverID string, duration time.Duration) error { expiresAt := time.Now().Add(duration) _, err := db.conn.Exec( `INSERT INTO player_sessions (player_id, token, server_type, server_id, expires_at) VALUES (?, ?, ?, ?, ?)`, playerID, token, serverType, serverID, expiresAt, ) return err } func (db *Database) ValidateSession(token string) (*Session, error) { s := &Session{} err := db.conn.QueryRow( `SELECT id, player_id, token, server_type, server_id, expires_at FROM player_sessions WHERE token = ? AND expires_at > NOW()`, token, ).Scan(&s.ID, &s.PlayerID, &s.Token, &s.ServerType, &s.ServerID, &s.ExpiresAt) if err != nil { return nil, err } return s, nil } func (db *Database) DeleteSession(token string) error { _, err := db.conn.Exec("DELETE FROM player_sessions WHERE token = ?", token) return err } func (db *Database) CleanExpiredSessions() error { _, err := db.conn.Exec("DELETE FROM player_sessions WHERE expires_at < NOW()") return err }