492 lines
16 KiB
Go
492 lines
16 KiB
Go
package collections
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"eq2emu/internal/database"
|
|
)
|
|
|
|
// DatabaseCollectionManager implements CollectionDatabase interface using the existing database wrapper
|
|
type DatabaseCollectionManager struct {
|
|
db *database.DB
|
|
}
|
|
|
|
// NewDatabaseCollectionManager creates a new database collection manager
|
|
func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager {
|
|
return &DatabaseCollectionManager{
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// LoadCollections retrieves all collections from database
|
|
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
|
|
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
|
|
|
|
rows, err := dcm.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query collections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var collections []CollectionData
|
|
for rows.Next() {
|
|
var collection CollectionData
|
|
err := rows.Scan(
|
|
&collection.ID,
|
|
&collection.Name,
|
|
&collection.Category,
|
|
&collection.Level,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan collection row: %w", err)
|
|
}
|
|
|
|
collections = append(collections, collection)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating collection rows: %w", err)
|
|
}
|
|
|
|
return collections, nil
|
|
}
|
|
|
|
// LoadCollectionItems retrieves items for a specific collection
|
|
func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) {
|
|
query := `SELECT item_id, item_index
|
|
FROM collection_details
|
|
WHERE collection_id = ?
|
|
ORDER BY item_index ASC`
|
|
|
|
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var items []CollectionItem
|
|
for rows.Next() {
|
|
var item CollectionItem
|
|
err := rows.Scan(
|
|
&item.ItemID,
|
|
&item.Index,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan collection item row: %w", err)
|
|
}
|
|
|
|
// Items start as not found
|
|
item.Found = ItemNotFound
|
|
items = append(items, item)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating collection item rows: %w", err)
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
// LoadCollectionRewards retrieves rewards for a specific collection
|
|
func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) {
|
|
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
|
|
FROM collection_rewards
|
|
WHERE collection_id = ?`
|
|
|
|
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var rewards []CollectionRewardData
|
|
for rows.Next() {
|
|
var reward CollectionRewardData
|
|
err := rows.Scan(
|
|
&reward.CollectionID,
|
|
&reward.RewardType,
|
|
&reward.RewardValue,
|
|
&reward.Quantity,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan collection reward row: %w", err)
|
|
}
|
|
|
|
rewards = append(rewards, reward)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating collection reward rows: %w", err)
|
|
}
|
|
|
|
return rewards, nil
|
|
}
|
|
|
|
// LoadPlayerCollections retrieves player's collection progress
|
|
func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) {
|
|
query := `SELECT char_id, collection_id, completed
|
|
FROM character_collections
|
|
WHERE char_id = ?`
|
|
|
|
rows, err := dcm.db.QueryContext(ctx, query, characterID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var collections []PlayerCollectionData
|
|
for rows.Next() {
|
|
var collection PlayerCollectionData
|
|
var completed int
|
|
err := rows.Scan(
|
|
&collection.CharacterID,
|
|
&collection.CollectionID,
|
|
&completed,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan player collection row: %w", err)
|
|
}
|
|
|
|
collection.Completed = completed == 1
|
|
collections = append(collections, collection)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating player collection rows: %w", err)
|
|
}
|
|
|
|
return collections, nil
|
|
}
|
|
|
|
// LoadPlayerCollectionItems retrieves player's found collection items
|
|
func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) {
|
|
query := `SELECT collection_item_id
|
|
FROM character_collection_items
|
|
WHERE char_id = ? AND collection_id = ?`
|
|
|
|
rows, err := dcm.db.QueryContext(ctx, query, characterID, collectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var itemIDs []int32
|
|
for rows.Next() {
|
|
var itemID int32
|
|
err := rows.Scan(&itemID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan player collection item row: %w", err)
|
|
}
|
|
|
|
itemIDs = append(itemIDs, itemID)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating player collection item rows: %w", err)
|
|
}
|
|
|
|
return itemIDs, nil
|
|
}
|
|
|
|
// SavePlayerCollection saves player collection completion status
|
|
func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error {
|
|
completedInt := 0
|
|
if completed {
|
|
completedInt = 1
|
|
}
|
|
|
|
query := `INSERT INTO character_collections (char_id, collection_id, completed)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(char_id, collection_id)
|
|
DO UPDATE SET completed = ?`
|
|
|
|
_, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, completedInt, completedInt)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SavePlayerCollectionItem saves a found collection item
|
|
func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error {
|
|
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
|
VALUES (?, ?, ?)`
|
|
|
|
_, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, itemID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SavePlayerCollections saves all modified player collections
|
|
func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error {
|
|
if len(collections) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Use a transaction for atomic updates
|
|
tx, err := dcm.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
for _, collection := range collections {
|
|
if !collection.GetSaveNeeded() {
|
|
continue
|
|
}
|
|
|
|
// Save collection completion status
|
|
if err := dcm.savePlayerCollectionTx(ctx, tx, characterID, collection); err != nil {
|
|
return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err)
|
|
}
|
|
|
|
// Save found items
|
|
if err := dcm.savePlayerCollectionItemsTx(ctx, tx, characterID, collection); err != nil {
|
|
return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err)
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// savePlayerCollectionTx saves a single collection within a transaction
|
|
func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error {
|
|
completedInt := 0
|
|
if collection.GetCompleted() {
|
|
completedInt = 1
|
|
}
|
|
|
|
query := `INSERT INTO character_collections (char_id, collection_id, completed)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(char_id, collection_id)
|
|
DO UPDATE SET completed = ?`
|
|
|
|
_, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), completedInt, completedInt)
|
|
return err
|
|
}
|
|
|
|
// savePlayerCollectionItemsTx saves collection items within a transaction
|
|
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error {
|
|
items := collection.GetCollectionItems()
|
|
|
|
for _, item := range items {
|
|
if item.Found == ItemFound {
|
|
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
|
|
VALUES (?, ?, ?)`
|
|
|
|
_, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), item.ItemID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save item %d: %w", item.ItemID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureCollectionTables creates the collection tables if they don't exist
|
|
func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context) error {
|
|
queries := []string{
|
|
`CREATE TABLE IF NOT EXISTS collections (
|
|
id INTEGER PRIMARY KEY,
|
|
collection_name TEXT NOT NULL,
|
|
collection_category TEXT NOT NULL DEFAULT '',
|
|
level INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS collection_details (
|
|
collection_id INTEGER NOT NULL,
|
|
item_id INTEGER NOT NULL,
|
|
item_index INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (collection_id, item_id),
|
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS collection_rewards (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
collection_id INTEGER NOT NULL,
|
|
reward_type TEXT NOT NULL,
|
|
reward_value TEXT NOT NULL,
|
|
reward_quantity INTEGER NOT NULL DEFAULT 1,
|
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS character_collections (
|
|
char_id INTEGER NOT NULL,
|
|
collection_id INTEGER NOT NULL,
|
|
completed INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (char_id, collection_id),
|
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE
|
|
)`,
|
|
`CREATE TABLE IF NOT EXISTS character_collection_items (
|
|
char_id INTEGER NOT NULL,
|
|
collection_id INTEGER NOT NULL,
|
|
collection_item_id INTEGER NOT NULL,
|
|
found_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (char_id, collection_id, collection_item_id),
|
|
FOREIGN KEY (char_id, collection_id) REFERENCES character_collections(char_id, collection_id) ON DELETE CASCADE
|
|
)`,
|
|
}
|
|
|
|
for i, query := range queries {
|
|
_, err := dcm.db.ExecContext(ctx, query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create collection table %d: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
// Create indexes for better performance
|
|
indexes := []string{
|
|
`CREATE INDEX IF NOT EXISTS idx_collection_details_collection_id ON collection_details(collection_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_collection_rewards_collection_id ON collection_rewards(collection_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_character_collections_char_id ON character_collections(char_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_character_collection_items_char_id ON character_collection_items(char_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_character_collection_items_collection_id ON character_collection_items(collection_id)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_collections_category ON collections(collection_category)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_collections_level ON collections(level)`,
|
|
}
|
|
|
|
for i, query := range indexes {
|
|
_, err := dcm.db.ExecContext(ctx, query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create collection index %d: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetCollectionCount returns the total number of collections in the database
|
|
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
|
|
query := "SELECT COUNT(*) FROM collections"
|
|
|
|
var count int
|
|
err := dcm.db.QueryRowContext(ctx, query).Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get collection count: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetPlayerCollectionCount returns the number of collections a player has
|
|
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
|
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
|
|
|
|
var count int
|
|
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetCompletedCollectionCount returns the number of completed collections for a player
|
|
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
|
|
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
|
|
|
|
var count int
|
|
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// DeletePlayerCollection removes a player's collection progress
|
|
func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error {
|
|
// Use a transaction to ensure both tables are updated atomically
|
|
tx, err := dcm.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete collection items first due to foreign key constraint
|
|
_, err = tx.ExecContext(ctx,
|
|
"DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?",
|
|
characterID, collectionID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete player collection items: %w", err)
|
|
}
|
|
|
|
// Delete collection
|
|
_, err = tx.ExecContext(ctx,
|
|
"DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?",
|
|
characterID, collectionID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete player collection: %w", err)
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetCollectionStatistics returns database-level collection statistics
|
|
func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Context) (CollectionStatistics, error) {
|
|
var stats CollectionStatistics
|
|
|
|
// Total collections
|
|
err := dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collections").Scan(&stats.TotalCollections)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get total collections: %w", err)
|
|
}
|
|
|
|
// Total collection items
|
|
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_details").Scan(&stats.TotalItems)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get total items: %w", err)
|
|
}
|
|
|
|
// Players with collections
|
|
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(DISTINCT char_id) FROM character_collections").Scan(&stats.PlayersWithCollections)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get players with collections: %w", err)
|
|
}
|
|
|
|
// Completed collections across all players
|
|
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collections WHERE completed = 1").Scan(&stats.CompletedCollections)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get completed collections: %w", err)
|
|
}
|
|
|
|
// Active collections (incomplete with at least one item found) across all players
|
|
query := `SELECT COUNT(DISTINCT cc.char_id, cc.collection_id)
|
|
FROM character_collections cc
|
|
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
|
|
WHERE cc.completed = 0`
|
|
err = dcm.db.QueryRowContext(ctx, query).Scan(&stats.ActiveCollections)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get active collections: %w", err)
|
|
}
|
|
|
|
// Found items across all players
|
|
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collection_items").Scan(&stats.FoundItems)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get found items: %w", err)
|
|
}
|
|
|
|
// Total rewards
|
|
err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_rewards").Scan(&stats.TotalRewards)
|
|
if err != nil {
|
|
return stats, fmt.Errorf("failed to get total rewards: %w", err)
|
|
}
|
|
|
|
return stats, nil
|
|
}
|