First git commit
This commit is contained in:
344
public_html/lib/db.php
Normal file
344
public_html/lib/db.php
Normal file
@ -0,0 +1,344 @@
|
||||
<?php
|
||||
// SQLite connection + schema bootstrap
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
function db(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo !== null) return $pdo;
|
||||
|
||||
if (!is_dir(DATA_DIR)) @mkdir(DATA_DIR, 0775, true);
|
||||
$fresh = !file_exists(DB_FILE);
|
||||
|
||||
$pdo = new PDO('sqlite:' . DB_FILE);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$pdo->exec('PRAGMA journal_mode = WAL');
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
$pdo->exec('PRAGMA busy_timeout = 5000');
|
||||
|
||||
db_migrate($pdo);
|
||||
if ($fresh) db_seed($pdo);
|
||||
db_seed_mud($pdo);
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function db_migrate(PDO $p): void {
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
pass_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
is_banned INTEGER NOT NULL DEFAULT 0,
|
||||
tagline TEXT DEFAULT '',
|
||||
location TEXT DEFAULT '',
|
||||
created_at INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL DEFAULT 0
|
||||
)");
|
||||
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
to_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
is_read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_msg_to ON messages(to_id, id DESC)");
|
||||
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS forums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
descr TEXT DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_locked INTEGER NOT NULL DEFAULT 0
|
||||
)");
|
||||
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS topics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
forum_id INTEGER NOT NULL REFERENCES forums(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
title TEXT NOT NULL,
|
||||
is_locked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
bumped_at INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_topic_forum ON topics(forum_id, bumped_at DESC)");
|
||||
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
topic_id INTEGER NOT NULL REFERENCES topics(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_post_topic ON posts(topic_id, id)");
|
||||
|
||||
// ---- games ----
|
||||
// single player high scores
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS scores (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
game TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
score INTEGER NOT NULL,
|
||||
detail TEXT DEFAULT '',
|
||||
created_at INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_score_game ON scores(game, score DESC)");
|
||||
|
||||
// generic multiplayer match table (tictactoe, nim)
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS matches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
game TEXT NOT NULL,
|
||||
p1 INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
p2 INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
turn INTEGER NOT NULL DEFAULT 1,
|
||||
state TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
winner INTEGER DEFAULT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_match_status ON matches(game, status)");
|
||||
|
||||
// single-player session state (guess the number)
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS sp_games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
game TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
state TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)");
|
||||
|
||||
// ---- MUD world ----
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_rooms (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
descr TEXT NOT NULL,
|
||||
exits TEXT NOT NULL DEFAULT '{}', -- JSON dir -> room_id
|
||||
safe INTEGER NOT NULL DEFAULT 0
|
||||
)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_mobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
key_name TEXT NOT NULL, -- keyword for 'kill <key>'
|
||||
name TEXT NOT NULL,
|
||||
descr TEXT NOT NULL,
|
||||
hp INTEGER NOT NULL,
|
||||
atk INTEGER NOT NULL,
|
||||
def INTEGER NOT NULL,
|
||||
xp INTEGER NOT NULL,
|
||||
gold INTEGER NOT NULL DEFAULT 0,
|
||||
loot TEXT NOT NULL DEFAULT '[]', -- JSON [item_key,...]
|
||||
respawn INTEGER NOT NULL DEFAULT 30
|
||||
)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_spawn (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mob_id INTEGER NOT NULL,
|
||||
room_id INTEGER NOT NULL,
|
||||
hp INTEGER NOT NULL,
|
||||
alive INTEGER NOT NULL DEFAULT 1,
|
||||
next_respawn INTEGER NOT NULL DEFAULT 0
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_mudspawn_room ON mud_spawn(room_id, alive)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key_name TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
descr TEXT NOT NULL,
|
||||
slot TEXT, -- weapon|armor|potion|misc
|
||||
atk INTEGER NOT NULL DEFAULT 0,
|
||||
def INTEGER NOT NULL DEFAULT 0,
|
||||
heal INTEGER NOT NULL DEFAULT 0,
|
||||
gold INTEGER NOT NULL DEFAULT 0,
|
||||
price INTEGER NOT NULL DEFAULT 0 -- shop price (0 = not sold)
|
||||
)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_ground (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
item_id INTEGER NOT NULL
|
||||
)");
|
||||
// de-dupe any pre-existing duplicate ground rows before the unique index
|
||||
$p->exec("DELETE FROM mud_ground WHERE id NOT IN (
|
||||
SELECT MIN(id) FROM mud_ground GROUP BY room_id, item_id)");
|
||||
$p->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_mudground_room_item ON mud_ground(room_id, item_id)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_chars (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
class TEXT NOT NULL DEFAULT 'fighter',
|
||||
room_id INTEGER NOT NULL DEFAULT 1,
|
||||
hp INTEGER NOT NULL,
|
||||
max_hp INTEGER NOT NULL,
|
||||
atk INTEGER NOT NULL,
|
||||
def INTEGER NOT NULL,
|
||||
xp INTEGER NOT NULL DEFAULT 0,
|
||||
level INTEGER NOT NULL DEFAULT 1,
|
||||
gold INTEGER NOT NULL DEFAULT 0,
|
||||
bank INTEGER NOT NULL DEFAULT 0,
|
||||
bounty INTEGER NOT NULL DEFAULT 0,
|
||||
weapon TEXT DEFAULT NULL,
|
||||
armor TEXT DEFAULT NULL,
|
||||
inv TEXT NOT NULL DEFAULT '[]', -- JSON [item_key,...]
|
||||
kills INTEGER NOT NULL DEFAULT 0,
|
||||
deaths INTEGER NOT NULL DEFAULT 0,
|
||||
last_cmd_at INTEGER NOT NULL DEFAULT 0
|
||||
)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_bounties (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_id INTEGER NOT NULL,
|
||||
by_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
ts INTEGER NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
room_id INTEGER NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
text TEXT NOT NULL
|
||||
)");
|
||||
$p->exec("CREATE INDEX IF NOT EXISTS idx_mudev_room ON mud_events(room_id, id DESC)");
|
||||
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS settings (
|
||||
k TEXT PRIMARY KEY,
|
||||
v TEXT NOT NULL
|
||||
)");
|
||||
}
|
||||
|
||||
function db_seed_mud(PDO $p): void {
|
||||
// --- migrate schema on an existing install (columns/tables may be new) ---
|
||||
$cols = [];
|
||||
foreach ($p->query("PRAGMA table_info(mud_chars)") as $row) $cols[$row['name']] = 1;
|
||||
$add = [
|
||||
'class' => "ALTER TABLE mud_chars ADD COLUMN class TEXT NOT NULL DEFAULT 'fighter'",
|
||||
'bank' => "ALTER TABLE mud_chars ADD COLUMN bank INTEGER NOT NULL DEFAULT 0",
|
||||
'bounty' => "ALTER TABLE mud_chars ADD COLUMN bounty INTEGER NOT NULL DEFAULT 0",
|
||||
'kills' => "ALTER TABLE mud_chars ADD COLUMN kills INTEGER NOT NULL DEFAULT 0",
|
||||
'deaths' => "ALTER TABLE mud_chars ADD COLUMN deaths INTEGER NOT NULL DEFAULT 0",
|
||||
];
|
||||
foreach ($add as $k => $sql) if (!isset($cols[$k])) $p->exec($sql);
|
||||
$p->exec("CREATE TABLE IF NOT EXISTS mud_bounties (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_id INTEGER NOT NULL, by_id INTEGER NOT NULL,
|
||||
amount INTEGER NOT NULL, ts INTEGER NOT NULL
|
||||
)");
|
||||
// mud_items.price may be new on older installs
|
||||
$icols = [];
|
||||
foreach ($p->query("PRAGMA table_info(mud_items)") as $row) $icols[$row['name']] = 1;
|
||||
if (!isset($icols['price'])) $p->exec("ALTER TABLE mud_items ADD COLUMN price INTEGER NOT NULL DEFAULT 0");
|
||||
|
||||
// --- build the base world only if empty ---
|
||||
if ($p->query("SELECT COUNT(*) FROM mud_rooms")->fetchColumn() == 0) {
|
||||
$now = time();
|
||||
$rooms = [
|
||||
1 => ['Village Square', 'A quiet cobbled square. Travellers set out from here.',
|
||||
'{"n":2,"e":5,"s":6,"w":9,"u":10,"d":11}', 1],
|
||||
2 => ['Forest Path', 'Dappled light, the smell of pine. Something rustles.',
|
||||
'{"s":1,"e":3}', 0],
|
||||
3 => ['Dark Cave', 'Dripping walls and the flap of leathery wings.',
|
||||
'{"w":2,"d":4}', 0],
|
||||
4 => ['Cave Depths', 'A vast chamber lit by glowing moss; something large lurks.',
|
||||
'{"u":3,"e":8}', 0],
|
||||
5 => ['River Bank', 'A clear stream teeming with silver fish.',
|
||||
'{"w":1,"n":7}', 0],
|
||||
6 => ['Old Ruins', 'Toppled columns and a broken altar.',
|
||||
'{"n":1,"e":7}', 0],
|
||||
7 => ['Ruins Altar', 'Cold air; a pale shape drifts between the stones.',
|
||||
'{"w":6,"s":5}', 0],
|
||||
8 => ['Mountain Pass', 'Thin air and a hulking figure blocks the path.',
|
||||
'{"w":4}', 0],
|
||||
9 => ['Trader\'s Shop', 'Shelves of blades and brews. "Buy or begone," grunts the trader.',
|
||||
'{"e":1}', 1],
|
||||
10 => ['The Vault', 'A thick-doored bank. Coins are safer here than on your person.',
|
||||
'{"d":1}', 1],
|
||||
11 => ['The Inn', 'Sir Joe Mollicone, the Taxman, watches the door. "Pay up, adventurer."',
|
||||
'{"u":1}', 1],
|
||||
];
|
||||
$ri = $p->prepare("INSERT INTO mud_rooms (id,name,descr,exits,safe) VALUES (?,?,?,?,?)");
|
||||
foreach ($rooms as $id => $r) $ri->execute([$id, $r[0], $r[1], $r[2], $r[3]]);
|
||||
|
||||
$mobs = [
|
||||
[2,'wolf','a Wolf','Mangy but quick.',18,5,2,8,3,'[]',40],
|
||||
[3,'bat','a Cave Bat','A squeaking blur.',12,4,1,5,2,'[]',30],
|
||||
[3,'goblin','a Goblin','Sharp teeth, sharper knife.',26,6,3,14,6,'["rusty_sword"]',45],
|
||||
[4,'orc','an Orc','Scarred and brutal.',46,9,5,40,15,'["leather_armor","gold"]',60],
|
||||
[5,'fish','a River Fish','Slippery and startled.',8,2,1,3,1,'[]',20],
|
||||
[6,'skeleton','a Skeleton','Rattling bones, rusted blade.',30,7,4,20,8,'[]',45],
|
||||
[7,'ghost','a Ghost','Cold, wailing, half-seen.',38,8,3,30,12,'["healing_potion"]',45],
|
||||
[8,'troll','a Troll','Twice your height and thick as a tree.',70,12,8,80,30,'["rusty_sword","leather_armor","gold"]',90],
|
||||
];
|
||||
$mi = $p->prepare("INSERT INTO mud_mobs (room_id,key_name,name,descr,hp,atk,def,xp,gold,loot,respawn)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$si = $p->prepare("INSERT INTO mud_spawn (mob_id,room_id,hp,alive,next_respawn) VALUES (?,?,?,1,0)");
|
||||
foreach ($mobs as $m) {
|
||||
$mi->execute($m);
|
||||
$mid = $p->lastInsertId();
|
||||
$si->execute([$mid, $m[0], $m[4]]);
|
||||
}
|
||||
} else {
|
||||
// existing world: make sure the shop/bank/taxman rooms and the village
|
||||
// links to them exist (idempotent).
|
||||
$new_rooms = [
|
||||
9 => ['Trader\'s Shop', 'Shelves of blades and brews. "Buy or begone," grunts the trader.', '{"e":1}', 1],
|
||||
10 => ['The Vault', 'A thick-doored bank. Coins are safer here than on your person.', '{"d":1}', 1],
|
||||
11 => ['The Inn', 'Sir Joe Mollicone, the Taxman, watches the door. "Pay up, adventurer."', '{"u":1}', 1],
|
||||
];
|
||||
$ri = $p->prepare("INSERT OR IGNORE INTO mud_rooms (id,name,descr,exits,safe) VALUES (?,?,?,?,?)");
|
||||
foreach ($new_rooms as $id => $r) $ri->execute([$id, $r[0], $r[1], $r[2], $r[3]]);
|
||||
// extend the village (room 1) exits to include w/u/d if not present
|
||||
$v = $p->query("SELECT exits FROM mud_rooms WHERE id=1")->fetchColumn();
|
||||
$vx = json_decode($v, true);
|
||||
$vx['w'] = 9; $vx['u'] = 10; $vx['d'] = 11;
|
||||
$p->prepare("UPDATE mud_rooms SET exits=? WHERE id=1")->execute([json_encode($vx)]);
|
||||
}
|
||||
|
||||
// --- shop stock (idempotent by key_name) ---
|
||||
$items = [
|
||||
['rusty_sword','Rusty Sword','A chipped but serviceable blade.','weapon',4,0,0,0,20],
|
||||
['leather_armor','Leather Armor','Cracked but protective.','armor',0,3,0,0,25],
|
||||
['healing_potion','Healing Potion','Bitter herbs that mend wounds.','potion',0,0,25,0,15],
|
||||
['steel_sword','Steel Sword','A keen edge for the serious.','weapon',8,0,0,0,60],
|
||||
['plate_armor','Plate Armor','Heavy and reassuring.','armor',0,7,0,0,70],
|
||||
['gold','Gold Coins','A small pile of coin.','misc',0,0,0,10,0],
|
||||
['torch','Torch','Flickering light against the dark.','misc',1,0,0,0,5],
|
||||
];
|
||||
$ii = $p->prepare("INSERT OR IGNORE INTO mud_items (key_name,name,descr,slot,atk,def,heal,gold,price)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)");
|
||||
foreach ($items as $it) $ii->execute($it);
|
||||
|
||||
// a couple of ground items to find
|
||||
$g1 = $p->prepare("INSERT OR IGNORE INTO mud_ground (room_id,item_id)
|
||||
SELECT 1, id FROM mud_items WHERE key_name='healing_potion'");
|
||||
$g1->execute();
|
||||
$g1 = $p->prepare("INSERT OR IGNORE INTO mud_ground (room_id,item_id)
|
||||
SELECT 3, id FROM mud_items WHERE key_name='torch'");
|
||||
$g1->execute();
|
||||
}
|
||||
|
||||
function db_seed(PDO $p): void {
|
||||
$now = time();
|
||||
$p->prepare("INSERT INTO users (username,pass_hash,is_admin,tagline,created_at)
|
||||
VALUES (?,?,1,?,?)")
|
||||
->execute([BOOTSTRAP_ADMIN, password_hash(BOOTSTRAP_ADMIN_PW, PASSWORD_DEFAULT),
|
||||
'site admin', $now]);
|
||||
$f = $p->prepare("INSERT INTO forums (name,descr,sort_order) VALUES (?,?,?)");
|
||||
$f->execute(['General', 'Anything goes', 1]);
|
||||
$f->execute(['Mobile', 'Phones, WAP, retro kit', 2]);
|
||||
$f->execute(['Games', 'Talk about the site games', 3]);
|
||||
$p->prepare("INSERT INTO settings (k,v) VALUES ('motd',?)")
|
||||
->execute(['Welcome to ' . SITE_NAME . '!']);
|
||||
}
|
||||
|
||||
function setting(string $k, string $default = ''): string {
|
||||
$r = db()->prepare("SELECT v FROM settings WHERE k=?");
|
||||
$r->execute([$k]);
|
||||
$v = $r->fetchColumn();
|
||||
return $v === false ? $default : (string)$v;
|
||||
}
|
||||
|
||||
function setting_set(string $k, string $v): void {
|
||||
db()->prepare("INSERT INTO settings (k,v) VALUES (?,?)
|
||||
ON CONFLICT(k) DO UPDATE SET v=excluded.v")->execute([$k, $v]);
|
||||
}
|
||||
Reference in New Issue
Block a user