diff --git a/database/auth.db b/database/auth.db index 414a797..5c2b22d 100644 Binary files a/database/auth.db and b/database/auth.db differ diff --git a/database/blueprints.db b/database/blueprints.db index 16c4108..553cf72 100644 Binary files a/database/blueprints.db and b/database/blueprints.db differ diff --git a/database/fights.db b/database/fights.db index a6c2a17..70b3acf 100644 Binary files a/database/fights.db and b/database/fights.db differ diff --git a/database/live.db b/database/live.db index 8720f4a..a18505c 100644 Binary files a/database/live.db and b/database/live.db differ diff --git a/database/scripts/create.php b/database/scripts/create.php index 89b0a88..b379939 100644 --- a/database/scripts/create.php +++ b/database/scripts/create.php @@ -28,7 +28,7 @@ if (!isset($argv[1])) { } // make sure it's a valid database -if (!in_array($argv[1], [AUTH, LIVE, FIGHTS, BPS])) { +if (!in_array($argv[1], [AUTH, LIVE, FIGHTS, BPS, 'reset'])) { eln(red('Invalid database: ') . $argv[1]); exit(1); } @@ -47,8 +47,8 @@ $drop = isset($argv[2]) && $argv[2] === '-d'; The Auth database is used to store user information - not player info, but user info. Usernames, passwords, email, session tokens, etc. */ -if ($database === AUTH) { - if ($drop) { +if ($database === AUTH || $database === 'reset') { + if ($drop || $database === 'reset') { unlink(__DIR__ . '/../' . AUTH); eln(red('Dropped database: ') . 'auth.db'); } @@ -76,6 +76,7 @@ if ($database === AUTH) { token TEXT NOT NULL UNIQUE, expires INTEGER NOT NULL )'); + $db->exec('CREATE INDEX idx_sessions_user_id ON sessions (user_id)'); created_or_error($r, 'sessions'); @@ -86,20 +87,21 @@ if ($database === AUTH) { token TEXT NOT NULL UNIQUE, created INTEGER NOT NULL )'); + $db->exec('CREATE INDEX idx_tokens_user_id ON tokens (user_id)'); created_or_error($r, 'tokens'); eln(green('Created database: ') . 'auth.db'); - exit(0); + if ($database !== 'reset') exit(0); } /* The Fights database is used to store information about fights. A fight is a battle between two characters; players or NPCs. */ -if ($database === FIGHTS) { - if ($drop) { +if ($database === FIGHTS || $database === 'reset') { + if ($drop || $database === 'reset') { unlink(__DIR__ . '/../' . FIGHTS); eln(red('Dropped database: ') . 'fights.db'); } @@ -148,6 +150,8 @@ if ($database === FIGHTS) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // create an index for char_id + $db->exec('CREATE INDEX idx_pve_char_id ON pve (char_id)'); created_or_error($r, 'pve'); @@ -191,6 +195,10 @@ if ($database === FIGHTS) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for char1_id + $db->exec('CREATE INDEX idx_pvp_char1_id ON pvp (char1_id)'); + // Create an index for char2_id + $db->exec('CREATE INDEX idx_pvp_char2_id ON pvp (char2_id)'); created_or_error($r, 'pvp'); @@ -201,6 +209,8 @@ if ($database === FIGHTS) { fight_id INTEGER NOT NULL, info TEXT NOT NULL )'); + // Create an index for fight_id + $db->exec('CREATE INDEX idx_pve_logs_fight_id ON pve_logs (fight_id)'); created_or_error($r, 'pve_logs'); @@ -211,19 +221,21 @@ if ($database === FIGHTS) { fight_id INTEGER NOT NULL, info TEXT NOT NULL )'); + // Create an index for fight_id + $db->exec('CREATE INDEX idx_pvp_logs_fight_id ON pvp_logs (fight_id)'); created_or_error($r, 'pvp_logs'); eln(green('Created database: ') . 'fights.db'); - exit(0); + if ($database !== 'reset') exit(0); } /* The Blueprints database is used to store information about items, weapons, armor, etc. */ -if ($database === BPS) { - if ($drop) { +if ($database === BPS || $database === 'reset') { + if ($drop || $database === 'reset') { unlink(__DIR__ . '/../' . BPS); eln(red('Dropped database: ') . 'blueprints.db'); } @@ -294,20 +306,38 @@ if ($database === BPS) { eln(green('Created database: ') . 'blueprints.db'); - exit(0); + if ($database !== 'reset') exit(0); } /* The Live database is used to store information about players, NPCs, guilds, etc. */ -if ($database === LIVE) { - if ($drop) { +if ($database === LIVE || $database === 'reset') { + if ($drop || $database === 'reset') { unlink(__DIR__ . '/../' . LIVE); eln(red('Dropped database: ') . 'live.db'); } $db = new SQLite3(__DIR__ . '/../' . LIVE); + // Blog posts + $db->exec('DROP TABLE IF EXISTS blog'); + $r = $db->exec('CREATE TABLE blog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + title TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + content TEXT NOT NULL, + created DATETIME DEFAULT CURRENT_TIMESTAMP, + updated DATETIME DEFAULT CURRENT_TIMESTAMP + )'); + // Create an index for author_id + $db->exec('CREATE INDEX idx_blog_author_id ON blog (author_id)'); + // Create an index for the slug + $db->exec('CREATE INDEX idx_blog_slug ON blog (slug)'); + + created_or_error($r, 'blog'); + // Characters $db->exec('DROP TABLE IF EXISTS characters'); $r = $db->exec('CREATE TABLE characters ( @@ -322,8 +352,8 @@ if ($database === LIVE) { max_hp INTEGER NOT NULL DEFAULT 20, current_mp INTEGER NOT NULL DEFAULT 10, max_mp INTEGER NOT NULL DEFAULT 10, - current_tp INTEGER NOT NULL DEFAULT 0, - max_tp INTEGER NOT NULL DEFAULT 0, + current_tp INTEGER NOT NULL DEFAULT 1, + max_tp INTEGER NOT NULL DEFAULT 1, power INTEGER NOT NULL DEFAULT 0, accuracy INTEGER NOT NULL DEFAULT 0, penetration INTEGER NOT NULL DEFAULT 0, @@ -337,6 +367,8 @@ if ($database === LIVE) { inv_slots INTEGER NOT NULL DEFAULT 10, attrib_points INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for user_id + $db->exec('CREATE INDEX idx_characters_user_id ON characters (user_id)'); created_or_error($r, 'characters'); @@ -367,6 +399,8 @@ if ($database === LIVE) { max_hp INTEGER NOT NULL DEFAULT 0, max_mp INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_char_gear_char_id ON char_gear (char_id)'); created_or_error($r, 'char_gear'); @@ -376,18 +410,22 @@ if ($database === LIVE) { char_id INTEGER NOT NULL, item_id INTEGER NOT NULL )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_inventory_char_id ON inventory (char_id)'); created_or_error($r, 'inventory'); // Player wallet - $db->exec('DROP TABLE IF EXISTS char_wallets'); - $r = $db->exec('CREATE TABLE char_wallets ( - char_id INTEGER NOT NULL, + $db->exec('DROP TABLE IF EXISTS wallets'); + $r = $db->exec('CREATE TABLE wallets ( + user_id INTEGER NOT NULL, silver INTEGER NOT NULL DEFAULT 10, stargem INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for user_id + $db->exec('CREATE INDEX idx_wallets_user_id ON wallets (user_id)'); - created_or_error($r, 'char_wallets'); + created_or_error($r, 'wallets'); // Player bank $db->exec('DROP TABLE IF EXISTS char_bank'); @@ -399,6 +437,8 @@ if ($database === LIVE) { can_collect INTEGER NOT NULL DEFAULT 1, last_collected DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_bank_char_id ON bank (char_id)'); created_or_error($r, 'bank'); @@ -408,6 +448,10 @@ if ($database === LIVE) { char_id INTEGER NOT NULL, item_id INTEGER NOT NULL )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_banked_items_char_id ON banked_items (char_id)'); + // Create an index for item_id + $db->exec('CREATE INDEX idx_banked_items_item_id ON banked_items (item_id)'); created_or_error($r, 'banked_items'); @@ -423,6 +467,8 @@ if ($database === LIVE) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for the x, y location + $db->exec('CREATE INDEX idx_towns_location ON towns (x, y)'); created_or_error($r, 'towns'); @@ -443,6 +489,8 @@ if ($database === LIVE) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for the x, y location + $db->exec('CREATE INDEX idx_shops_location ON shops (x, y)'); created_or_error($r, 'shops'); @@ -459,6 +507,8 @@ if ($database === LIVE) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for the x, y location + $db->exec('CREATE INDEX idx_inns_location ON inns (x, y)'); created_or_error($r, 'inns'); @@ -474,6 +524,8 @@ if ($database === LIVE) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for leader_id + $db->exec('CREATE INDEX idx_guilds_leader_id ON guilds (leader_id)'); created_or_error($r, 'guilds'); @@ -485,6 +537,8 @@ if ($database === LIVE) { name TEXT NOT NULL, permissions TEXT NOT NULL )'); + // Create an index for guild_id + $db->exec('CREATE INDEX idx_guild_ranks_guild_id ON guild_ranks (guild_id)'); created_or_error($r, 'guild_ranks'); @@ -498,6 +552,10 @@ if ($database === LIVE) { donated INTEGER NOT NULL DEFAULT 0, joined DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for guild_id + $db->exec('CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id)'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_guild_members_char_id ON guild_members (char_id)'); created_or_error($r, 'guild_members'); @@ -514,6 +572,8 @@ if ($database === LIVE) { created DATETIME DEFAULT CURRENT_TIMESTAMP, updated DATETIME DEFAULT CURRENT_TIMESTAMP )'); + // Create an index for the x, y location + $db->exec('CREATE INDEX idx_npcs_location ON npcs (x, y)'); created_or_error($r, 'npcs'); @@ -524,6 +584,8 @@ if ($database === LIVE) { town_id INTEGER NOT NULL, rep INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_char_town_rep_char_id ON char_town_rep (char_id)'); created_or_error($r, 'char_town_rep'); @@ -568,6 +630,8 @@ if ($database === LIVE) { trait_id INTEGER NOT NULL, value INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_char_traits_char_id ON char_traits (char_id)'); created_or_error($r, 'char_traits'); @@ -579,12 +643,16 @@ if ($database === LIVE) { y INTEGER NOT NULL, currently INTEGER NOT NULL DEFAULT 0 )'); + // Create an index for char_id + $db->exec('CREATE INDEX idx_char_locations_char_id ON char_locations (char_id)'); + // Create an index for x, y location + $db->exec('CREATE INDEX idx_char_locations_location ON char_locations (x, y)'); created_or_error($r, 'char_locations'); eln(green('Created database: ') . 'live.db'); - exit(0); + if ($database !== 'reset') exit(0); } function created_or_error(bool $result, string $table): void diff --git a/public/assets/css/dragon.css b/public/assets/css/dragon.css index a408291..8a5e3f7 100644 --- a/public/assets/css/dragon.css +++ b/public/assets/css/dragon.css @@ -258,3 +258,10 @@ span.badge { top: 0; left: 0; } + +#debug-query-log { + padding: 2rem; + font-size: 14px; + color: #666; + font-family: monospace; +} diff --git a/src/auth.php b/src/auth.php index 11b8f8c..9b503d4 100644 --- a/src/auth.php +++ b/src/auth.php @@ -63,8 +63,7 @@ function must_have_character(): void { // If there is a character selected, make sure the session is up to date. if ($_SESSION['user']['char_id'] !== 0) { - $char = db_query(db_live(), 'SELECT * FROM characters WHERE id = :c', [':c' => $_SESSION['user']['char_id']])->fetchArray(SQLITE3_ASSOC); - $_SESSION['char'] = $char; + char(); return; } diff --git a/src/components.php b/src/components.php index 5e6445f..dd29d1f 100644 --- a/src/components.php +++ b/src/components.php @@ -19,7 +19,7 @@ function c_logout_button(): string */ function c_char_bar(): string { - if (!char()) return ''; + if (char() === false) return ''; return render('components/char_bar', ['char' => char()]); } @@ -30,3 +30,11 @@ function c_left_nav(int $activeTab): string { return render('components/left_nav', ['activeTab' => $activeTab]); } + +/** + * Render the debug query log. + */ +function c_debug_query_log(): string +{ + return render('components/debug_query_log'); +} diff --git a/src/controllers/auth.php b/src/controllers/auth.php index 2633d32..ff89057 100644 --- a/src/controllers/auth.php +++ b/src/controllers/auth.php @@ -84,6 +84,7 @@ function auth_controller_register_post(): void if ($user === false) router_error(400); $_SESSION['user'] = user_find($u); + wallet_create($_SESSION['user']['id']); redirect('/character/create-first'); } diff --git a/src/controllers/char.php b/src/controllers/char.php index e961e87..02e82f8 100644 --- a/src/controllers/char.php +++ b/src/controllers/char.php @@ -67,7 +67,6 @@ function char_controller_create_post(): void // Create the auxiliary tables char_location_create($char); - char_wallet_create($char); char_gear_create($char); // Set the character as the user's selected character diff --git a/src/database.php b/src/database.php index 1b89641..5382ae3 100644 --- a/src/database.php +++ b/src/database.php @@ -1,11 +1,28 @@ exec('PRAGMA cache_size = 32000'); + // Enable WAL mode + $db->exec('PRAGMA journal_mode = WAL'); + // Move temp store to memory + $db->exec('PRAGMA temp_store = MEMORY'); + + return $db; +} + /** * Return a connection to the auth database. */ function db_auth(): SQLite3 { - return $GLOBALS['db_auth'] ??= new SQLite3(__DIR__ . '/../database/auth.db'); + return $GLOBALS['db_auth'] ??= db_open(__DIR__ . '/../database/auth.db'); } /** @@ -13,7 +30,7 @@ function db_auth(): SQLite3 */ function db_live(): SQLite3 { - return $GLOBALS['db_live'] ??= new SQLite3(__DIR__ . '/../database/live.db'); + return $GLOBALS['db_live'] ??= db_open(__DIR__ . '/../database/live.db'); } @@ -22,7 +39,7 @@ function db_live(): SQLite3 */ function db_fights(): SQLite3 { - return $GLOBALS['db_fights'] ??= new SQLite3(__DIR__ . '/../database/fights.db'); + return $GLOBALS['db_fights'] ??= db_open(__DIR__ . '/../database/fights.db'); } @@ -31,7 +48,7 @@ function db_fights(): SQLite3 */ function db_blueprints(): SQLite3 { - return $GLOBALS['db_blueprints'] ??= new SQLite3(__DIR__ . '/../database/blueprints.db'); + return $GLOBALS['db_blueprints'] ??= db_open(__DIR__ . '/../database/blueprints.db'); } /** @@ -43,6 +60,7 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result $stmt = $db->prepare($query); if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value)); $GLOBALS['queries']++; + db_log($query); return $stmt->execute(); } @@ -52,6 +70,7 @@ function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result function db_exec(SQLite3 $db, string $query): bool { $GLOBALS['queries']++; + db_log($query); return $db->exec($query); } @@ -61,7 +80,9 @@ function db_exec(SQLite3 $db, string $query): bool */ function db_exists(SQLite3 $db, string $table, string $column, mixed $value): bool { - $result = db_query($db, "SELECT 1 FROM $table WHERE $column = :v LIMIT 1", [':v' => $value]); + $query = "SELECT 1 FROM $table WHERE $column = :v LIMIT 1"; + $result = db_query($db, $query, [':v' => $value]); + db_log($query); return $result->fetchArray(SQLITE3_NUM) !== false; } @@ -77,3 +98,11 @@ function getSQLiteType(mixed $value): int default => SQLITE3_TEXT }; } + +/** + * Log the given query string to the db debug log. + */ +function db_log(string $query): void +{ + if (env('debug', false)) $GLOBALS['query_log'][] = $query; +} diff --git a/src/helpers.php b/src/helpers.php index e1b4a85..ebc4dd3 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -129,13 +129,23 @@ function user_selected_char(): int /** * If the current user has a selected char and the data is in the session, retrieve either the full array of data - * or a specific field. + * or a specific field. If there is no character data, populate it. */ function char(string $field = ''): mixed { - if (empty($_SESSION['char'])) return false; - if ($field === '') return $_SESSION['char']; - return $_SESSION['char'][$field] ?? false; + // If there is no user, return false + if (empty($_SESSION['user'])) return false; + + if (empty($GLOBALS['char'])) { + $GLOBALS['char'] = db_query( + db_live(), + "SELECT * FROM characters WHERE id = :c", + [':c' => user_selected_char()] + )->fetchArray(SQLITE3_ASSOC); + } + + if ($field === '') return $GLOBALS['char']; + return $GLOBALS['char'][$field] ?? false; } /** @@ -145,7 +155,7 @@ function change_user_character(int $char_id): void { $_SESSION['user']['char_id'] = $char_id; db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]); - $_SESSION['char'] = char_find($char_id); + $GLOBALS['char'] = char_find($char_id); } /** @@ -157,3 +167,22 @@ function percent(int $num, int $denom, int $precision = 4): int $p = ($num / $denom) * 100; return $p < 0 ? 0 : round($p, $precision); } + +/** + * Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way + * the data is up to date with every request without having to query the database every use within, for example, a + * template. Will return false if the field does not exist, or the entire wallet array if no field is specified. + */ +function wallet(string $field = ''): array|int|false +{ + if (empty($GLOBALS['wallet'])) { + $GLOBALS['wallet'] = db_query( + db_live(), + "SELECT * FROM wallets WHERE user_id = :u", + [':u' => user('id')] + )->fetchArray(SQLITE3_ASSOC); + } + + if ($field === '') return $GLOBALS['wallet']; + return $GLOBALS['wallet'][$field] ?? false; +} diff --git a/src/models/char.php b/src/models/char.php index 76502b0..a10da4c 100644 --- a/src/models/char.php +++ b/src/models/char.php @@ -61,22 +61,6 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current } } -/** - * Creates a character's wallet. A character's wallet is where they store their currencies. Can optionally specify the - * starting balances of the wallet. Returns the created wallet's ID. If a currency is set to -1, the starting_silver - * or starting_star_gems fields from the env will be used. - */ -function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1): void -{ - if (db_query(db_live(), "INSERT INTO char_wallets (char_id, silver, stargem) VALUES (:p, :s, :sg)", [ - ':p' => $char_id, - ':s' => $silver === -1 ? env('start_silver', 10) : $silver, - ':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems - ]) === false) { - throw new Exception('Failed to create character wallet. (cwc)'); - } -} - /** * Create the character's gear table. A character's gear is where they store their equipped items. * @TODO: implement initial gear @@ -152,16 +136,6 @@ function char_get_location(int $char_id): array return $location; } -/** - * Get a character's wallet by their character ID. Returns the wallet's data as an associative array. - */ -function char_get_wallet(int $char_id): array -{ - $wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); - if ($wallet === false) throw new Exception('Wallet not found. (cgw)'); - return $wallet; -} - /** * See if a character name exists. */ diff --git a/src/models/user.php b/src/models/user.php index 0245406..80a7506 100644 --- a/src/models/user.php +++ b/src/models/user.php @@ -35,3 +35,19 @@ function user_delete(string|int $user): SQLite3Result|false { return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]); } + +/** + * Creates a character's wallet. A character's wallet is where they store their currencies. Can optionally specify the + * starting balances of the wallet. Returns the created wallet's ID. If a currency is set to -1, the starting_silver + * or starting_star_gems fields from the env will be used. + */ +function wallet_create(int $user_id, int $silver = -1, int $starGems = -1): void +{ + if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [ + ':u' => $user_id, + ':s' => $silver === -1 ? env('start_silver', 10) : $silver, + ':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems + ]) === false) { + throw new Exception('Failed to create wallet. (wc)'); + } +} diff --git a/templates/components/char_bar.php b/templates/components/char_bar.php index 0b29357..b826f44 100644 --- a/templates/components/char_bar.php +++ b/templates/components/char_bar.php @@ -27,4 +27,8 @@
+ += $GLOBALS['queries'] ?> queries were executed.
+ $query"; ?> +v= env('version') ?>
+ +