eq2go/internal/player/database.go
2025-08-06 17:55:41 -05:00

227 lines
5.6 KiB
Go

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{})
}