eq2go/internal/rules/database.go

465 lines
13 KiB
Go

package rules
import (
"database/sql"
"fmt"
"log"
"strconv"
"eq2emu/internal/database"
)
// DatabaseService handles rule database operations
// Converted from C++ WorldDatabase rule functions
type DatabaseService struct {
db *database.Database
}
// NewDatabaseService creates a new database service instance
func NewDatabaseService(db *database.Database) *DatabaseService {
return &DatabaseService{
db: db,
}
}
// LoadGlobalRuleSet loads the global rule set from database
// Converted from C++ WorldDatabase::LoadGlobalRuleSet()
func (ds *DatabaseService) LoadGlobalRuleSet(ruleManager *RuleManager) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
ruleSetID := int32(0)
// Get the default ruleset ID from variables table
query := "SELECT variable_value FROM variables WHERE variable_name = ?"
var variableValue string
err := ds.db.QueryRow(query, DefaultRuleSetIDVar).Scan(&variableValue)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("[Rules] Variables table is missing %s variable name, using code-default rules", DefaultRuleSetIDVar)
return nil
}
return fmt.Errorf("error querying default ruleset ID: %v", err)
}
if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil {
ruleSetID = int32(id)
log.Printf("[Rules] Loading Global Ruleset id %d", ruleSetID)
} else {
return fmt.Errorf("invalid ruleset ID format: %s", variableValue)
}
if ruleSetID > 0 {
if !ruleManager.SetGlobalRuleSet(ruleSetID) {
return fmt.Errorf("error loading global rule set - rule set with ID %d does not exist", ruleSetID)
}
}
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// LoadRuleSets loads all rule sets from database
// Converted from C++ WorldDatabase::LoadRuleSets()
func (ds *DatabaseService) LoadRuleSets(ruleManager *RuleManager, reload bool) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if reload {
ruleManager.Flush(true)
}
// First load the coded defaults into the global rule set
ruleManager.LoadCodedDefaultsIntoRuleSet(ruleManager.GetGlobalRuleSet())
// Load active rule sets from database
query := "SELECT ruleset_id, ruleset_name FROM rulesets WHERE ruleset_active > 0"
loadedCount := 0
rows, err := ds.db.Query(query)
if err != nil {
return fmt.Errorf("error querying rule sets: %v", err)
}
defer rows.Close()
for rows.Next() {
var ruleSetID int32
var ruleSetName string
err := rows.Scan(&ruleSetID, &ruleSetName)
if err != nil {
return fmt.Errorf("error scanning rule set row: %v", err)
}
ruleSet := NewRuleSet()
ruleSet.SetID(ruleSetID)
ruleSet.SetName(ruleSetName)
if ruleManager.AddRuleSet(ruleSet) {
log.Printf("[Rules] Loading rule set '%s' (%d)", ruleSet.GetName(), ruleSet.GetID())
err := ds.LoadRuleSetDetails(ruleManager, ruleSet)
if err != nil {
log.Printf("[Rules] Error loading rule set details for '%s': %v", ruleSetName, err)
continue // Continue with other rule sets
}
loadedCount++
} else {
log.Printf("[Rules] Unable to add rule set '%s' - ID %d already exists", ruleSetName, ruleSetID)
}
}
if err = rows.Err(); err != nil {
return fmt.Errorf("error iterating rule sets: %v", err)
}
log.Printf("[Rules] Loaded %d Rule Sets", loadedCount)
// Load global rule set
err = ds.LoadGlobalRuleSet(ruleManager)
if err != nil {
return fmt.Errorf("error loading global rule set: %v", err)
}
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// LoadRuleSetDetails loads the detailed rules for a specific rule set
// Converted from C++ WorldDatabase::LoadRuleSetDetails()
func (ds *DatabaseService) LoadRuleSetDetails(ruleManager *RuleManager, ruleSet *RuleSet) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if ruleSet == nil {
return fmt.Errorf("rule set is nil")
}
// Copy rules from global rule set (coded defaults) first
ruleSet.CopyRulesInto(ruleManager.GetGlobalRuleSet())
// Load rule overrides from database
query := "SELECT rule_category, rule_type, rule_value FROM ruleset_details WHERE ruleset_id = ?"
loadedRules := 0
rows, err := ds.db.Query(query, ruleSet.GetID())
if err != nil {
return fmt.Errorf("error querying rule set details: %v", err)
}
defer rows.Close()
for rows.Next() {
var categoryName, typeName, ruleValue string
err := rows.Scan(&categoryName, &typeName, &ruleValue)
if err != nil {
return fmt.Errorf("error scanning rule detail row: %v", err)
}
// Find the rule by name
rule := ruleSet.GetRuleByName(categoryName, typeName)
if rule == nil {
log.Printf("[Rules] Unknown rule with category '%s' and type '%s'", categoryName, typeName)
continue // Continue with other rules
}
log.Printf("[Rules] Setting rule category '%s', type '%s' to value: %s", categoryName, typeName, ruleValue)
rule.SetValue(ruleValue)
loadedRules++
}
if err = rows.Err(); err != nil {
return fmt.Errorf("error iterating rule set details: %v", err)
}
log.Printf("[Rules] Loaded %d rule overrides for rule set '%s'", loadedRules, ruleSet.GetName())
ruleManager.stats.IncrementDatabaseOperations()
return nil
}
// SaveRuleSet saves a rule set to the database
func (ds *DatabaseService) SaveRuleSet(ruleSet *RuleSet) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
if ruleSet == nil {
return fmt.Errorf("rule set is nil")
}
// Use transaction for atomicity
tx, err := ds.db.Begin()
if err != nil {
return fmt.Errorf("error beginning transaction: %v", err)
}
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
// Insert or update rule set using MySQL ON DUPLICATE KEY UPDATE
query := `INSERT INTO rulesets (ruleset_id, ruleset_name, ruleset_active)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE
ruleset_name = VALUES(ruleset_name),
ruleset_active = VALUES(ruleset_active)`
_, err = tx.Exec(query, ruleSet.GetID(), ruleSet.GetName())
if err != nil {
return fmt.Errorf("error saving rule set: %v", err)
}
// Delete existing rule details
deleteQuery := "DELETE FROM ruleset_details WHERE ruleset_id = ?"
_, err = tx.Exec(deleteQuery, ruleSet.GetID())
if err != nil {
return fmt.Errorf("error deleting existing rule details: %v", err)
}
// Insert rule details
insertQuery := "INSERT INTO ruleset_details (ruleset_id, rule_category, rule_type, rule_value) VALUES (?, ?, ?, ?)"
rules := ruleSet.GetRules()
for _, categoryMap := range rules {
for _, rule := range categoryMap {
if rule.IsValid() {
combined := rule.GetCombined()
parts := splitCombined(combined)
if len(parts) == 2 {
_, err = tx.Exec(insertQuery, ruleSet.GetID(), parts[0], parts[1], rule.GetValue())
if err != nil {
return fmt.Errorf("error saving rule detail: %v", err)
}
}
}
}
}
return nil
}
// DeleteRuleSet deletes a rule set from the database
func (ds *DatabaseService) DeleteRuleSet(ruleSetID int32) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
// Use transaction for atomicity
tx, err := ds.db.Begin()
if err != nil {
return fmt.Errorf("error beginning transaction: %v", err)
}
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
// Delete rule details first (foreign key constraint)
_, err = tx.Exec("DELETE FROM ruleset_details WHERE ruleset_id = ?", ruleSetID)
if err != nil {
return fmt.Errorf("error deleting rule details: %v", err)
}
// Delete rule set
_, err = tx.Exec("DELETE FROM rulesets WHERE ruleset_id = ?", ruleSetID)
if err != nil {
return fmt.Errorf("error deleting rule set: %v", err)
}
return nil
}
// SetDefaultRuleSet sets the default rule set ID in the variables table
func (ds *DatabaseService) SetDefaultRuleSet(ruleSetID int32) error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
query := `INSERT INTO variables (variable_name, variable_value, comment)
VALUES (?, ?, 'Default ruleset ID')
ON DUPLICATE KEY UPDATE
variable_value = VALUES(variable_value)`
_, err := ds.db.Exec(query, DefaultRuleSetIDVar, strconv.Itoa(int(ruleSetID)))
if err != nil {
return fmt.Errorf("error setting default rule set: %v", err)
}
return nil
}
// GetDefaultRuleSetID gets the default rule set ID from the variables table
func (ds *DatabaseService) GetDefaultRuleSetID() (int32, error) {
if ds.db == nil {
return 0, fmt.Errorf("database not initialized")
}
query := "SELECT variable_value FROM variables WHERE variable_name = ?"
var variableValue string
err := ds.db.QueryRow(query, DefaultRuleSetIDVar).Scan(&variableValue)
if err != nil {
if err == sql.ErrNoRows {
return 0, fmt.Errorf("default ruleset ID not found in variables table")
}
return 0, fmt.Errorf("error querying default ruleset ID: %v", err)
}
if id, err := strconv.ParseInt(variableValue, 10, 32); err == nil {
return int32(id), nil
}
return 0, fmt.Errorf("invalid ruleset ID format: %s", variableValue)
}
// GetRuleSetList returns a list of all rule sets
func (ds *DatabaseService) GetRuleSetList() ([]RuleSetInfo, error) {
if ds.db == nil {
return nil, fmt.Errorf("database not initialized")
}
query := "SELECT ruleset_id, ruleset_name, ruleset_active FROM rulesets ORDER BY ruleset_id"
var ruleSets []RuleSetInfo
rows, err := ds.db.Query(query)
if err != nil {
return nil, fmt.Errorf("error querying rule sets: %v", err)
}
defer rows.Close()
for rows.Next() {
var info RuleSetInfo
var active int
err := rows.Scan(&info.ID, &info.Name, &active)
if err != nil {
return nil, fmt.Errorf("error scanning rule set row: %v", err)
}
info.Active = active > 0 // Convert int to bool
ruleSets = append(ruleSets, info)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating rule sets: %v", err)
}
return ruleSets, nil
}
// ValidateDatabase ensures the required tables exist
func (ds *DatabaseService) ValidateDatabase() error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
tables := []string{"rulesets", "ruleset_details", "variables"}
query := "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?"
for _, table := range tables {
var count int
err := ds.db.QueryRow(query, table).Scan(&count)
if err != nil {
return fmt.Errorf("error checking %s table: %v", table, err)
}
if count == 0 {
return fmt.Errorf("%s table does not exist", table)
}
}
return nil
}
// RuleSetInfo contains basic information about a rule set
type RuleSetInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Active bool `json:"active"`
}
// Helper function to split combined rule string
func splitCombined(combined string) []string {
for i, char := range combined {
if char == ':' {
return []string{combined[:i], combined[i+1:]}
}
}
return []string{combined}
}
// CreateRulesTables creates the necessary database tables for rules
func (ds *DatabaseService) CreateRulesTables() error {
if ds.db == nil {
return fmt.Errorf("database not initialized")
}
// Create rulesets table
createRuleSets := `
CREATE TABLE IF NOT EXISTS rulesets (
ruleset_id INTEGER PRIMARY KEY,
ruleset_name VARCHAR(255) NOT NULL UNIQUE,
ruleset_active INTEGER NOT NULL DEFAULT 0
)`
_, err := ds.db.Exec(createRuleSets)
if err != nil {
return fmt.Errorf("error creating rulesets table: %v", err)
}
// Create ruleset_details table
createRuleSetDetails := `
CREATE TABLE IF NOT EXISTS ruleset_details (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
ruleset_id INTEGER NOT NULL,
rule_category VARCHAR(255) NOT NULL,
rule_type VARCHAR(255) NOT NULL,
rule_value TEXT NOT NULL,
description TEXT,
FOREIGN KEY (ruleset_id) REFERENCES rulesets(ruleset_id) ON DELETE CASCADE
)`
_, err = ds.db.Exec(createRuleSetDetails)
if err != nil {
return fmt.Errorf("error creating ruleset_details table: %v", err)
}
// Create variables table if it doesn't exist
createVariables := `
CREATE TABLE IF NOT EXISTS variables (
variable_name VARCHAR(255) PRIMARY KEY,
variable_value TEXT NOT NULL,
comment TEXT
)`
_, err = ds.db.Exec(createVariables)
if err != nil {
return fmt.Errorf("error creating variables table: %v", err)
}
// Create indexes for better performance
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_ruleset_details_ruleset_id ON ruleset_details(ruleset_id)",
"CREATE INDEX IF NOT EXISTS idx_ruleset_details_category_type ON ruleset_details(rule_category, rule_type)",
"CREATE INDEX IF NOT EXISTS idx_rulesets_active ON rulesets(ruleset_active)",
}
for _, indexSQL := range indexes {
_, err = ds.db.Exec(indexSQL)
if err != nil {
return fmt.Errorf("error creating index: %v", err)
}
}
return nil
}