423 lines
11 KiB
Go
423 lines
11 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"sync"
|
|
|
|
_ "modernc.org/sqlite"
|
|
"zombiezen.com/go/sqlite/sqlitex"
|
|
)
|
|
|
|
// Database wraps the SQL database connection
|
|
type Database struct {
|
|
db *sql.DB
|
|
pool *sqlitex.Pool // For achievements system compatibility
|
|
dbPath string // Store path for pool creation
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// New creates a new database connection
|
|
func New(path string) (*Database, error) {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
}
|
|
|
|
// Test connection
|
|
if err := db.Ping(); err != nil {
|
|
return nil, fmt.Errorf("failed to ping database: %w", err)
|
|
}
|
|
|
|
// Set connection pool settings
|
|
db.SetMaxOpenConns(25)
|
|
db.SetMaxIdleConns(5)
|
|
|
|
// Create sqlitex pool for achievements system
|
|
pool, err := sqlitex.NewPool(path, sqlitex.PoolOptions{
|
|
PoolSize: 5,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
|
|
}
|
|
|
|
d := &Database{
|
|
db: db,
|
|
pool: pool,
|
|
dbPath: path,
|
|
}
|
|
|
|
// Initialize schema
|
|
if err := d.initSchema(); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (d *Database) Close() error {
|
|
if d.pool != nil {
|
|
d.pool.Close()
|
|
}
|
|
return d.db.Close()
|
|
}
|
|
|
|
// GetPool returns the sqlitex pool for achievements system compatibility
|
|
func (d *Database) GetPool() *sqlitex.Pool {
|
|
return d.pool
|
|
}
|
|
|
|
// initSchema creates the database schema if it doesn't exist
|
|
func (d *Database) initSchema() error {
|
|
schemas := []string{
|
|
// Rules table
|
|
`CREATE TABLE IF NOT EXISTS rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
category TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
description TEXT,
|
|
UNIQUE(category, name)
|
|
)`,
|
|
|
|
// Accounts table
|
|
`CREATE TABLE IF NOT EXISTS accounts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
email TEXT,
|
|
admin_level INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_login DATETIME
|
|
)`,
|
|
|
|
// Characters table
|
|
`CREATE TABLE IF NOT EXISTS characters (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
account_id INTEGER NOT NULL,
|
|
name TEXT UNIQUE NOT NULL,
|
|
race_id INTEGER NOT NULL,
|
|
class_id INTEGER NOT NULL,
|
|
level INTEGER DEFAULT 1,
|
|
x REAL DEFAULT 0,
|
|
y REAL DEFAULT 0,
|
|
z REAL DEFAULT 0,
|
|
heading REAL DEFAULT 0,
|
|
zone_id INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_played DATETIME,
|
|
FOREIGN KEY(account_id) REFERENCES accounts(id)
|
|
)`,
|
|
|
|
// Zones table
|
|
`CREATE TABLE IF NOT EXISTS zones (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
file TEXT NOT NULL,
|
|
description TEXT,
|
|
motd TEXT,
|
|
min_level INTEGER DEFAULT 0,
|
|
max_level INTEGER DEFAULT 100,
|
|
min_version INTEGER DEFAULT 0,
|
|
xp_modifier REAL DEFAULT 1.0,
|
|
city_zone INTEGER DEFAULT 0,
|
|
weather_allowed INTEGER DEFAULT 1,
|
|
safe_x REAL DEFAULT 0,
|
|
safe_y REAL DEFAULT 0,
|
|
safe_z REAL DEFAULT 0,
|
|
safe_heading REAL DEFAULT 0
|
|
)`,
|
|
|
|
// Server statistics table
|
|
`CREATE TABLE IF NOT EXISTS server_stats (
|
|
stat_id INTEGER PRIMARY KEY,
|
|
stat_value INTEGER,
|
|
stat_date INTEGER,
|
|
save_needed INTEGER DEFAULT 0
|
|
)`,
|
|
|
|
// Merchant tables
|
|
`CREATE TABLE IF NOT EXISTS merchants (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
merchant_type INTEGER DEFAULT 0
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS merchant_items (
|
|
merchant_id INTEGER NOT NULL,
|
|
item_id INTEGER NOT NULL,
|
|
quantity INTEGER DEFAULT -1,
|
|
price_coins INTEGER DEFAULT 0,
|
|
price_status INTEGER DEFAULT 0,
|
|
PRIMARY KEY(merchant_id, item_id),
|
|
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
|
)`,
|
|
|
|
// Achievement tables
|
|
`CREATE TABLE IF NOT EXISTS achievements (
|
|
achievement_id INTEGER PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
uncompleted_text TEXT,
|
|
completed_text TEXT,
|
|
category TEXT,
|
|
expansion TEXT,
|
|
icon INTEGER DEFAULT 0,
|
|
point_value INTEGER DEFAULT 0,
|
|
qty_req INTEGER DEFAULT 1,
|
|
hide_achievement INTEGER DEFAULT 0,
|
|
unknown3a INTEGER DEFAULT 0,
|
|
unknown3b INTEGER DEFAULT 0
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS achievements_requirements (
|
|
achievement_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
qty_req INTEGER DEFAULT 1,
|
|
PRIMARY KEY(achievement_id, name),
|
|
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS achievements_rewards (
|
|
achievement_id INTEGER NOT NULL,
|
|
reward TEXT NOT NULL,
|
|
PRIMARY KEY(achievement_id, reward),
|
|
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS character_achievements (
|
|
char_id INTEGER NOT NULL,
|
|
achievement_id INTEGER NOT NULL,
|
|
completed_date INTEGER,
|
|
PRIMARY KEY(char_id, achievement_id),
|
|
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS character_achievements_items (
|
|
char_id INTEGER NOT NULL,
|
|
achievement_id INTEGER NOT NULL,
|
|
items INTEGER DEFAULT 0,
|
|
PRIMARY KEY(char_id, achievement_id),
|
|
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(achievement_id) REFERENCES achievements(achievement_id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
// Title tables
|
|
`CREATE TABLE IF NOT EXISTS titles (
|
|
title_id INTEGER PRIMARY KEY,
|
|
text TEXT NOT NULL,
|
|
category INTEGER DEFAULT 0,
|
|
rarity INTEGER DEFAULT 0,
|
|
position INTEGER DEFAULT 0,
|
|
description TEXT,
|
|
is_unique INTEGER DEFAULT 0,
|
|
is_hidden INTEGER DEFAULT 0,
|
|
color_code TEXT,
|
|
requirements TEXT,
|
|
source_type INTEGER DEFAULT 0,
|
|
source_id INTEGER DEFAULT 0,
|
|
created_date INTEGER,
|
|
expire_date INTEGER
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS character_titles (
|
|
char_id INTEGER NOT NULL,
|
|
title_id INTEGER NOT NULL,
|
|
source_achievement_id INTEGER DEFAULT 0,
|
|
source_quest_id INTEGER DEFAULT 0,
|
|
granted_date INTEGER,
|
|
expire_date INTEGER,
|
|
PRIMARY KEY(char_id, title_id),
|
|
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(title_id) REFERENCES titles(title_id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS character_active_titles (
|
|
char_id INTEGER PRIMARY KEY,
|
|
prefix_title_id INTEGER DEFAULT 0,
|
|
suffix_title_id INTEGER DEFAULT 0,
|
|
FOREIGN KEY(char_id) REFERENCES characters(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(prefix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL,
|
|
FOREIGN KEY(suffix_title_id) REFERENCES titles(title_id) ON DELETE SET NULL
|
|
)`,
|
|
|
|
// NPC tables
|
|
`CREATE TABLE IF NOT EXISTS npcs (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
level INTEGER DEFAULT 1,
|
|
max_level INTEGER DEFAULT 1,
|
|
race INTEGER DEFAULT 0,
|
|
model_type INTEGER DEFAULT 0,
|
|
size INTEGER DEFAULT 32,
|
|
hp INTEGER DEFAULT 100,
|
|
power INTEGER DEFAULT 100,
|
|
x REAL DEFAULT 0,
|
|
y REAL DEFAULT 0,
|
|
z REAL DEFAULT 0,
|
|
heading REAL DEFAULT 0,
|
|
respawn_time INTEGER DEFAULT 300,
|
|
zone_id INTEGER DEFAULT 0,
|
|
aggro_radius REAL DEFAULT 10,
|
|
ai_strategy INTEGER DEFAULT 0,
|
|
loot_table_id INTEGER DEFAULT 0,
|
|
merchant_type INTEGER DEFAULT 0,
|
|
randomize_appearance INTEGER DEFAULT 0,
|
|
show_name INTEGER DEFAULT 1,
|
|
show_level INTEGER DEFAULT 1,
|
|
targetable INTEGER DEFAULT 1,
|
|
show_command_icon INTEGER DEFAULT 1,
|
|
display_hand_icon INTEGER DEFAULT 0,
|
|
faction_id INTEGER DEFAULT 0,
|
|
created_date INTEGER,
|
|
last_modified INTEGER
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS npc_spells (
|
|
npc_id INTEGER NOT NULL,
|
|
spell_id INTEGER NOT NULL,
|
|
tier INTEGER DEFAULT 1,
|
|
hp_percentage INTEGER DEFAULT 100,
|
|
priority INTEGER DEFAULT 1,
|
|
cast_type INTEGER DEFAULT 0,
|
|
recast_delay INTEGER DEFAULT 5,
|
|
PRIMARY KEY(npc_id, spell_id),
|
|
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS npc_skills (
|
|
npc_id INTEGER NOT NULL,
|
|
skill_name TEXT NOT NULL,
|
|
skill_value INTEGER DEFAULT 0,
|
|
max_value INTEGER DEFAULT 0,
|
|
PRIMARY KEY(npc_id, skill_name),
|
|
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
)`,
|
|
|
|
`CREATE TABLE IF NOT EXISTS npc_loot (
|
|
npc_id INTEGER NOT NULL,
|
|
item_id INTEGER NOT NULL,
|
|
probability REAL DEFAULT 100.0,
|
|
min_level INTEGER DEFAULT 0,
|
|
max_level INTEGER DEFAULT 100,
|
|
PRIMARY KEY(npc_id, item_id),
|
|
FOREIGN KEY(npc_id) REFERENCES npcs(id) ON DELETE CASCADE
|
|
)`,
|
|
}
|
|
|
|
for _, schema := range schemas {
|
|
if _, err := d.db.Exec(schema); err != nil {
|
|
return fmt.Errorf("failed to create schema: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Query executes a query that returns rows
|
|
func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
|
return d.db.Query(query, args...)
|
|
}
|
|
|
|
// QueryRow executes a query that returns a single row
|
|
func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row {
|
|
return d.db.QueryRow(query, args...)
|
|
}
|
|
|
|
// Exec executes a query that doesn't return rows
|
|
func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) {
|
|
return d.db.Exec(query, args...)
|
|
}
|
|
|
|
// Begin starts a transaction
|
|
func (d *Database) Begin() (*sql.Tx, error) {
|
|
return d.db.Begin()
|
|
}
|
|
|
|
// LoadRules loads all rules from the database
|
|
func (d *Database) LoadRules() (map[string]map[string]string, error) {
|
|
rows, err := d.Query("SELECT category, name, value FROM rules")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
rules := make(map[string]map[string]string)
|
|
|
|
for rows.Next() {
|
|
var category, name, value string
|
|
if err := rows.Scan(&category, &name, &value); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rules[category] == nil {
|
|
rules[category] = make(map[string]string)
|
|
}
|
|
rules[category][name] = value
|
|
}
|
|
|
|
return rules, rows.Err()
|
|
}
|
|
|
|
// SaveRule saves a rule to the database
|
|
func (d *Database) SaveRule(category, name, value, description string) error {
|
|
_, err := d.Exec(`
|
|
INSERT OR REPLACE INTO rules (category, name, value, description)
|
|
VALUES (?, ?, ?, ?)
|
|
`, category, name, value, description)
|
|
return err
|
|
}
|
|
|
|
// GetZones retrieves all zones from the database
|
|
func (d *Database) GetZones() ([]map[string]interface{}, error) {
|
|
rows, err := d.Query(`
|
|
SELECT id, name, file, description, motd, min_level, max_level,
|
|
min_version, xp_modifier, city_zone, weather_allowed,
|
|
safe_x, safe_y, safe_z, safe_heading
|
|
FROM zones
|
|
ORDER BY name
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var zones []map[string]interface{}
|
|
|
|
for rows.Next() {
|
|
zone := make(map[string]interface{})
|
|
var id, minLevel, maxLevel, minVersion int
|
|
var name, file, description, motd string
|
|
var xpModifier, safeX, safeY, safeZ, safeHeading float64
|
|
var cityZone, weatherAllowed bool
|
|
|
|
err := rows.Scan(&id, &name, &file, &description, &motd,
|
|
&minLevel, &maxLevel, &minVersion, &xpModifier,
|
|
&cityZone, &weatherAllowed,
|
|
&safeX, &safeY, &safeZ, &safeHeading)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zone["id"] = id
|
|
zone["name"] = name
|
|
zone["file"] = file
|
|
zone["description"] = description
|
|
zone["motd"] = motd
|
|
zone["min_level"] = minLevel
|
|
zone["max_level"] = maxLevel
|
|
zone["min_version"] = minVersion
|
|
zone["xp_modifier"] = xpModifier
|
|
zone["city_zone"] = cityZone
|
|
zone["weather_allowed"] = weatherAllowed
|
|
zone["safe_x"] = safeX
|
|
zone["safe_y"] = safeY
|
|
zone["safe_z"] = safeZ
|
|
zone["safe_heading"] = safeHeading
|
|
|
|
zones = append(zones, zone)
|
|
}
|
|
|
|
return zones, rows.Err()
|
|
} |