package player import ( "fmt" "sync" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // PlayerDatabase manages player data persistence using SQLite type PlayerDatabase struct { conn *sqlite.Conn mutex sync.RWMutex } // NewPlayerDatabase creates a new player database instance func NewPlayerDatabase(conn *sqlite.Conn) *PlayerDatabase { return &PlayerDatabase{ conn: conn, } } // LoadPlayer loads a player from the database func (pdb *PlayerDatabase) LoadPlayer(characterID int32) (*Player, error) { pdb.mutex.RLock() defer pdb.mutex.RUnlock() player := NewPlayer() player.SetCharacterID(characterID) found := false query := `SELECT name, level, race, class, zone_id, x, y, z, heading FROM characters WHERE id = ?` err := sqlitex.Execute(pdb.conn, query, &sqlitex.ExecOptions{ Args: []any{characterID}, ResultFunc: func(stmt *sqlite.Stmt) error { player.SetName(stmt.ColumnText(0)) player.SetLevel(int16(stmt.ColumnInt(1))) player.SetRace(int8(stmt.ColumnInt(2))) player.SetClass(int8(stmt.ColumnInt(3))) player.SetZone(int32(stmt.ColumnInt(4))) player.SetX(float32(stmt.ColumnFloat(5))) player.SetY(float32(stmt.ColumnFloat(6)), false) player.SetZ(float32(stmt.ColumnFloat(7))) player.SetHeadingFromFloat(float32(stmt.ColumnFloat(8))) found = true return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load player %d: %w", characterID, err) } if !found { return nil, fmt.Errorf("player %d not found", characterID) } return player, nil } // SavePlayer saves a player to the database func (pdb *PlayerDatabase) SavePlayer(player *Player) error { if player == nil { return fmt.Errorf("cannot save nil player") } pdb.mutex.Lock() defer pdb.mutex.Unlock() characterID := player.GetCharacterID() if characterID == 0 { // Insert new player return pdb.insertPlayer(player) } // Try to update existing player first err := pdb.updatePlayer(player) if err == nil { // Check if any rows were affected changes := pdb.conn.Changes() if changes == 0 { // No rows updated, record doesn't exist - insert it return pdb.insertPlayerWithID(player) } } return err } // insertPlayer inserts a new player record func (pdb *PlayerDatabase) insertPlayer(player *Player) error { query := `INSERT INTO characters (name, level, race, class, zone_id, x, y, z, heading, created_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))` err := sqlitex.Execute(pdb.conn, query, &sqlitex.ExecOptions{ Args: []any{ player.GetName(), player.GetLevel(), player.GetRace(), player.GetClass(), player.GetZone(), player.GetX(), player.GetY(), player.GetZ(), player.GetHeading(), }, }) if err != nil { return fmt.Errorf("failed to insert player %s: %w", player.GetName(), err) } // Get the new character ID characterID := pdb.conn.LastInsertRowID() player.SetCharacterID(int32(characterID)) return nil } // insertPlayerWithID inserts a player with a specific ID func (pdb *PlayerDatabase) insertPlayerWithID(player *Player) error { query := `INSERT INTO characters (id, name, level, race, class, zone_id, x, y, z, heading, created_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))` err := sqlitex.Execute(pdb.conn, query, &sqlitex.ExecOptions{ Args: []any{ player.GetCharacterID(), player.GetName(), player.GetLevel(), player.GetRace(), player.GetClass(), player.GetZone(), player.GetX(), player.GetY(), player.GetZ(), player.GetHeading(), }, }) if err != nil { return fmt.Errorf("failed to insert player %s with ID %d: %w", player.GetName(), player.GetCharacterID(), err) } return nil } // updatePlayer updates an existing player record func (pdb *PlayerDatabase) updatePlayer(player *Player) error { query := `UPDATE characters SET name = ?, level = ?, race = ?, class = ?, zone_id = ?, x = ?, y = ?, z = ?, heading = ?, last_save = datetime('now') WHERE id = ?` err := sqlitex.Execute(pdb.conn, query, &sqlitex.ExecOptions{ Args: []any{ player.GetName(), player.GetLevel(), player.GetRace(), player.GetClass(), player.GetZone(), player.GetX(), player.GetY(), player.GetZ(), player.GetHeading(), player.GetCharacterID(), }, }) if err != nil { return fmt.Errorf("failed to update player %d: %w", player.GetCharacterID(), err) } return nil } // DeletePlayer deletes a player from the database func (pdb *PlayerDatabase) DeletePlayer(characterID int32) error { pdb.mutex.Lock() defer pdb.mutex.Unlock() query := `DELETE FROM characters WHERE id = ?` err := sqlitex.Execute(pdb.conn, query, &sqlitex.ExecOptions{ Args: []any{characterID}, }) if err != nil { return fmt.Errorf("failed to delete player %d: %w", characterID, err) } return nil } // CreateSchema creates the database schema for player data func (pdb *PlayerDatabase) CreateSchema() error { pdb.mutex.Lock() defer pdb.mutex.Unlock() schema := ` CREATE TABLE IF NOT EXISTS characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, level INTEGER DEFAULT 1, race INTEGER DEFAULT 1, class INTEGER DEFAULT 1, zone_id INTEGER DEFAULT 1, x REAL DEFAULT 0, y REAL DEFAULT 0, z REAL DEFAULT 0, heading REAL DEFAULT 0, hp INTEGER DEFAULT 100, power INTEGER DEFAULT 100, created_date TEXT, last_save TEXT, account_id INTEGER DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_characters_name ON characters(name); CREATE INDEX IF NOT EXISTS idx_characters_account ON characters(account_id); ` return sqlitex.ExecuteScript(pdb.conn, schema, &sqlitex.ExecOptions{}) }