package player import ( "database/sql" "fmt" "sync" "eq2emu/internal/database" ) // PlayerDatabase manages player data persistence using MySQL type PlayerDatabase struct { db *database.Database mutex sync.RWMutex } // NewPlayerDatabase creates a new player database instance func NewPlayerDatabase(db *database.Database) *PlayerDatabase { return &PlayerDatabase{ db: db, } } // 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) query := `SELECT name, level, race, class, zone_id, x, y, z, heading FROM characters WHERE id = ?` row := pdb.db.QueryRow(query, characterID) var name string var level int16 var race, class int8 var zoneID int32 var x, y, z, heading float32 err := row.Scan(&name, &level, &race, &class, &zoneID, &x, &y, &z, &heading) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("player not found: %d", characterID) } return nil, fmt.Errorf("failed to load player: %w", err) } player.SetName(name) player.SetLevel(level) player.SetRace(race) player.SetClass(class) player.SetZone(zoneID) player.SetX(x) player.SetY(y, false) player.SetZ(z) player.SetHeadingFromFloat(heading) 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 return pdb.updatePlayer(player) } // 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())` result, err := pdb.db.Exec(query, 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: %w", err) } // Get the inserted character ID characterID, err := result.LastInsertId() if err != nil { return fmt.Errorf("failed to get inserted character ID: %w", err) } player.SetCharacterID(int32(characterID)) 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=? WHERE id=?` _, err := pdb.db.Exec(query, 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: %w", err) } return nil } // DeletePlayer soft-deletes a player (marks as deleted) func (pdb *PlayerDatabase) DeletePlayer(characterID int32) error { pdb.mutex.Lock() defer pdb.mutex.Unlock() query := `UPDATE characters SET deleted_date = NOW() WHERE id = ?` _, err := pdb.db.Exec(query, characterID) if err != nil { return fmt.Errorf("failed to delete player %d: %w", characterID, err) } return nil } // PlayerExists checks if a player exists in the database func (pdb *PlayerDatabase) PlayerExists(characterID int32) (bool, error) { pdb.mutex.RLock() defer pdb.mutex.RUnlock() var count int query := `SELECT COUNT(*) FROM characters WHERE id = ? AND (deleted_date IS NULL OR deleted_date = 0)` err := pdb.db.QueryRow(query, characterID).Scan(&count) if err != nil { return false, fmt.Errorf("failed to check player existence: %w", err) } return count > 0, nil }