Compare commits

...

4 Commits

25 changed files with 698 additions and 352 deletions

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
debug = true
smtp_host = smtp.foobar.com
smtp_port = 546
smtp_encryption = tls
smtp_username = foo
smtp_password = bar123

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.installed
database.db
database.db-*
.env
logs

View File

@ -8,7 +8,11 @@ html {
}
body {
background-image: url('/img/background.jpg');
background-image: url('/img/backgrounds/classic.jpg');
&.skin-1 {
background-image: url('/img/backgrounds/snowstorm.jpg');
}
}
div#game-container {
@ -179,3 +183,19 @@ div.town-content {
div.town-content div.options, div.town-content div.news {
grid-column: span 2;
}
div.stat-table div.stat-row {
display: flex;
justify-content: space-around;
gap: 0.5rem;
}
div.stat-bar {
position: relative;
}
div.stat-bar > div {
width: 100%;
position: absolute;
bottom: 0;
}

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

View File

@ -7,20 +7,18 @@ require_once '../src/bootstrap.php';
$r = new Router;
$r->get('/', function() {
global $userrow;
if ($userrow["currentaction"] == "In Town") {
if (user()->currentaction === "In Town") {
$page = dotown();
$title = "In Town";
} elseif ($userrow["currentaction"] == "Exploring") {
} elseif (user()->currentaction === "Exploring") {
$page = doexplore();
$title = "Exploring";
} elseif ($userrow["currentaction"] == "Fighting") {
} elseif (user()->currentaction === "Fighting") {
redirect('/fight');
}
display($page, $title);
})->middleware('auth_only');
});
$r->get('/ninja', function() {
exit('NINJA! 🥷');
@ -51,15 +49,13 @@ $l['handler'](...$l['params'] ?? []);
function donothing()
{
global $userrow;
if ($userrow["currentaction"] == "In Town") {
if (user()->currentaction == "In Town") {
$page = dotown();
$title = "In Town";
} elseif ($userrow["currentaction"] == "Exploring") {
} elseif (user()->currentaction == "Exploring") {
$page = doexplore();
$title = "Exploring";
} elseif ($userrow["currentaction"] == "Fighting") {
} elseif (user()->currentaction == "Fighting") {
redirect('/fight');
}
@ -71,9 +67,9 @@ function donothing()
*/
function dotown()
{
global $userrow, $controlrow;
global $controlrow;
$townrow = get_town_by_xy($userrow['longitude'], $userrow['latitude']);
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("There is an error with your user account, or with the town data. Please try again.","Error");
$townrow["news"] = "";
@ -89,7 +85,12 @@ function dotown()
// Who's Online. Currently just members. Guests maybe later.
if ($controlrow["showonline"] == 1) {
$onlinequery = db()->query("SELECT id, username FROM users WHERE strftime('%s', onlinetime) >= strftime('%s', 'now') - 600 ORDER BY username");
$onlinequery = db()->query(<<<SQL
SELECT id, username
FROM users
WHERE onlinetime >= datetime('now', '-600 seconds')
ORDER BY username;
SQL);
$online_count = 0;
$online_rows = [];
@ -114,6 +115,11 @@ function dotown()
HTML;
}
$u = User::find(1);
$u->gold += 100;
$u->save();
var_dump($u->gold);
return render('towns', ['town' => $townrow]);
}
@ -140,7 +146,7 @@ function show_character_info(int $id = 0): void
{
global $controlrow, $userrow;
$userrow = ($id === 0) ? $userrow : get_user_by_id($id);
$userrow = ($id === 0) ? $userrow : get_user($id);
if ($userrow === false) exit('Failed to show info for user ID '.$id);
$levelrow = db()->query("SELECT `{$userrow["charclass"]}_exp` FROM levels WHERE id=? LIMIT 1;", [$userrow['level'] + 1])->fetchArray(SQLITE3_ASSOC);
@ -166,12 +172,10 @@ function show_character_info(int $id = 0): void
function showmap()
{
global $userrow;
$pos = sprintf(
'<div style="position: absolute; width: 5px; height: 5px; border-radius: 1000px; border: solid 1px black; background-color: red; left: %dpx; top: %dpx;"></div>',
round(258 + $userrow['longitude'] * (500 / 500) - 3),
round(258 - $userrow['latitude'] * (500 / 500) - 3)
round(258 + user()->longitude * (500 / 500) - 3),
round(258 - user()->latitude * (500 / 500) - 3)
);
echo render('minimal', [
@ -186,13 +190,11 @@ function showmap()
*/
function babblebox()
{
global $userrow;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$safecontent = make_safe($_POST["babble"]);
if (!empty($safecontent)) {
db()->query('INSERT INTO babble (posttime, author, babble) VALUES (CURRENT_TIMESTAMP, ?, ?);',
[$userrow['username'], $safecontent]);
[user()->username, $safecontent]);
}
redirect('/babblebox');
}

View File

@ -8,8 +8,7 @@ use Router;
function register_routes(Router $r): Router
{
global $userrow;
if (isset($userrow) && $userrow !== false && $userrow['authlevel'] === 1) {
if (user('authlevel') === 1) {
$r->get('/admin', 'Admin\donothing');
$r->form('/admin/main', 'Admin\primary');

56
src/actions/explore.php Normal file
View File

@ -0,0 +1,56 @@
<?php
// explore.php :: Handles all map exploring, chances to fight, etc.
function move() {
global $controlrow;
// Early exit if fighting
if (user()->currentaction == 'Fighting') redirect('/fight');
// Validate direction
$form = validate($_POST, ['direction' => ['in:north,west,east,south']]);
if (!$form['valid']) display(ul_from_validate_errors($form['errors']), 'Move Error');
// Current game state
$game_size = $controlrow['gamesize'];
$latitude = user('latitude');
$longitude = user('longitude');
$direction = $form['data']['direction'];
// Calculate new coordinates with boundary checks
switch ($direction) {
case 'north':
$latitude = min($latitude + 1, $game_size);
break;
case 'south':
$latitude = max($latitude - 1, -$game_size);
break;
case 'east':
$longitude = min($longitude + 1, $game_size);
break;
case 'west':
$longitude = max($longitude - 1, -$game_size);
break;
}
// Check for town
$town = get_town_by_xy($longitude, $latitude);
if ($town !== false) {
Towns\travelto($town['id'], false);
return;
}
// Determine action (1 in 5 chance of fighting)
$action = (rand(1, 5) === 1)
? "currentaction='Fighting', currentfight='1',"
: "currentaction='Exploring',";
// Update user's position
db()->query(
"UPDATE users SET $action latitude = ?, longitude = ?, dropcode = 0 WHERE id = ?;",
[$latitude, $longitude, user()->id]
);
redirect('/');
}

View File

@ -41,6 +41,8 @@ function first()
*/
function second()
{
if (file_exists('../database.db')) unlink('../database.db');
echo "<html><head><title>Dragon Knight Installation</title></head><body><b>Dragon Knight Installation: Page Two</b><br><br>";
$query = db()->exec(<<<SQL
@ -52,7 +54,7 @@ function second()
);
SQL);
echo $query === true ? 'Babble Box table created.<br>' : 'Error creating Babble Box table.';
echo table_status_msg($query === true, 'Babble', 'create');
$query = db()->exec(<<<SQL
CREATE TABLE control (
@ -72,11 +74,14 @@ function second()
);
SQL);
echo $query === true ? 'Control table created.<br>' : 'Error creating Control table.';
echo table_status_msg($query === true, 'Control', 'create');
$query = db()->exec("INSERT INTO control VALUES (1, 'Dragon Knight', 250, 1, '', '', 'Mage', 'Warrior', 'Paladin', 1, 1, 1, 1);");
$query = db()->query("INSERT INTO control VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
1, 'Dragon Knight', 250, 1, $_SERVER['SERVER_NAME'], 'noreply@'.$_SERVER['SERVER_NAME'],
'Mage', 'Warrior', 'Paladin', 1, 1, 1, 1
]);
echo $query === true ? 'Control table populated.<br>' : 'Error populating Control table.';
echo table_status_msg($query !== false, 'Control', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE drops (
@ -89,7 +94,7 @@ function second()
);
SQL);
echo $query == true ? 'Drops table created.<br>' : 'Error creating Drops table.';
echo table_status_msg($query === true, 'Drops', 'create');
$query = db()->exec(<<<SQL
INSERT INTO drops VALUES
@ -127,7 +132,7 @@ function second()
(32, 'Fortune Drop', 5, 1, 'goldbonus,10', 'X');
SQL);
echo $query === true ? 'Drops table populated.<br>' : 'Error populating Drops table.';
echo table_status_msg($query === true, 'Drops', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE forum (
@ -142,7 +147,7 @@ function second()
);
SQL);
echo $query === true ? 'Forum table created.<br>' : 'Error creating Forum table.';
echo table_status_msg($query === true, 'Forum', 'create');
$query = db()->exec(<<<SQL
CREATE TABLE items (
@ -155,7 +160,7 @@ function second()
);
SQL);
echo $query === true ? 'Items table created.<br>' : 'Error creating Items table.';
echo table_status_msg($query === true, 'Items', 'create');
$query = db()->exec(<<<SQL
INSERT INTO items VALUES
@ -194,7 +199,7 @@ function second()
(33, 3, 'Destiny Aegis', 25000, 100, 'maxhp,50');
SQL);
echo $query === true ? 'Items table populated.<br>' : 'Error populating Items table.';
echo table_status_msg($query === true, 'Drops', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE levels (
@ -223,7 +228,7 @@ function second()
);
SQL);
echo $query === true ? 'Levels table created.<br>' : 'Error creating Levels table.';
echo table_status_msg($query === true, 'Levels', 'create');
$query = db()->exec(<<<SQL
INSERT INTO levels VALUES
@ -329,7 +334,7 @@ function second()
(100, 16777215, 0, 0, 0, 0, 0, 0, 16777215, 0, 0, 0, 0, 0, 0, 16777215, 0, 0, 0, 0, 0, 0);
SQL);
echo $query === true ? 'Levels table populated.<br>' : 'Error populating Levels table.';
echo table_status_msg($query === true, 'Levels', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE monsters (
@ -345,7 +350,7 @@ function second()
);
SQL);
echo $query === true ? 'Monsters table created.<br>' : 'Error creating Monsters table.';
echo table_status_msg($query === true, 'Monsters', 'create');
$query = db()->exec(<<<SQL
INSERT INTO monsters VALUES
@ -502,7 +507,7 @@ function second()
(151, 'Lucifuge', 600, 600, 400, 50, 10000, 10000, 2);
SQL);
echo $query === true ? 'Monsters table populated.<br>' : 'Error populating Monsters table.';
echo table_status_msg($query === true, 'Monsters', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE news (
@ -513,11 +518,11 @@ function second()
);
SQL);
echo $query === true ? 'News table created.<br>' : 'Error creating News table.';
echo table_status_msg($query === true, 'News', 'create');
$query = db()->exec("INSERT INTO news (content) VALUES ('This is the first news post. Please use the admin control panel to add another one and make this one go away.');");
echo $query === true ? 'News table populated.<br>' : 'Error populating News table.';
echo table_status_msg($query === true, 'News', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE spells (
@ -529,7 +534,7 @@ function second()
);
SQL);
echo $query === true ? 'Spells table created.<br>' : 'Error creating Spells table.';
echo table_status_msg($query === true, 'Spells', 'create');
$query = db()->exec(<<<SQL
INSERT INTO spells VALUES
@ -554,7 +559,7 @@ function second()
(19, 'Barrier', 30, 50, 5);
SQL);
echo $query === true ? 'Spells table populated.<br>' : 'Error populating Spells table.';
echo table_status_msg($query === true, 'Spells', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE towns (
@ -569,7 +574,7 @@ function second()
);
SQL);
echo $query === true ? 'Towns table created.<br>' : 'Error creating Towns table.';
echo table_status_msg($query === true, 'Towns', 'create');
$query = db()->exec(<<<SQL
INSERT INTO towns VALUES
@ -583,7 +588,7 @@ function second()
(8, 'Endworld', -250, -250, 125, 9000, 160, '16,27,33');
SQL);
echo $query === true ? 'Towns table populated.<br>' : 'Error populating Towns table.';
echo table_status_msg($query === true, 'Towns', 'populate');
$query = db()->exec(<<<SQL
CREATE TABLE users (
@ -635,11 +640,12 @@ function second()
`slot3name` TEXT NOT NULL default 'None',
`dropcode` INTEGER NOT NULL default 0,
`spells` TEXT NOT NULL default '0',
`towns` TEXT NOT NULL default '0'
`towns` TEXT NOT NULL default '0',
`game_skin` INTEGER NOT NULL DEFAULT 0
);
SQL);
echo $query === true ? 'Users table created.<br>' : 'Error creating Users table.';
echo table_status_msg($query === true, 'Users', 'create');
$time = round((microtime(true) - START), 4);
echo "<br>Database setup complete in $time seconds.<br><br><a href=\"/install/third\">Click here to continue with installation.</a></body></html>";
@ -691,7 +697,7 @@ function fourth()
$form = $form['data'];
if (db()->query(
"INSERT INTO users (username, password, email, verify, charclass, authlevel) VALUES (?, ?, ?, 1, ?, 1)",
"INSERT INTO users (username, password, email, verify, charclass, authlevel) VALUES (?, ?, ?, 'g2g', ?, 1)",
[$form['username'], password_hash($form['password'], PASSWORD_ARGON2ID), $form['email'], $form['charclass']]
) === false) {
exit("Failed to create user.");
@ -742,3 +748,17 @@ function fifth()
</html>
HTML;
}
function table_status_msg(bool $condition, string $table_name, string $verb): string
{
$verb = match($verb) {
'create' => ['created', 'creating'],
'populate' => ['populated', 'populating']
};
if ($condition === false) {
return "Error {$verb[1]} $table_name table. (".db()->lastErrorMsg().")<br>";
}
return "$table_name table {$verb[0]}.<br>";
}

View File

@ -25,20 +25,18 @@ function register_routes(Router $r): Router
*/
function inn()
{
global $userrow;
$townrow = get_town_by_xy($userrow["longitude"], $userrow["latitude"]);
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) { display("Cheat attempt detected.<br><br>Get a life, loser.", "Error"); }
if ($userrow["gold"] < $townrow["innprice"]) {
if (user()->gold < $townrow["innprice"]) {
display("You do not have enough gold to stay at this Inn tonight.<br><br>You may return to <a href=\"/\">town</a>, or use the direction buttons on the left to start exploring.", "Inn");
}
if (isset($_POST["submit"])) {
$newgold = $userrow["gold"] - $townrow["innprice"];
$newgold = user()->gold - $townrow["innprice"];
db()->query(
'UPDATE users SET gold=?, currenthp=?, currentmp=?, currenttp=? WHERE id=?',
[$newgold, $userrow['maxhp'], $userrow['maxmp'], $userrow['maxtp'], $userrow['id']
[$newgold, user()->maxhp, user()->maxmp, user()->maxtp, user()->id
]);
$title = "Inn";
$page = "You wake up feeling refreshed and ready for action.<br><br>You may return to <a href=\"/\">town</a>, or use the direction buttons on the left to start exploring.";
@ -63,9 +61,7 @@ function inn()
*/
function buy()
{
global $userrow;
$townrow = get_town_by_xy($userrow['longitude'], $userrow['latitude']);
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error");
$items = db()->query("SELECT * FROM items WHERE id IN ({$townrow["itemslist"]});");
@ -79,7 +75,7 @@ function buy()
2 => '<img src="/img/icon_armor.gif" alt="armor" /></td>',
3 => '<img src="/img/icon_shield.gif" alt="shield" /></td>'
};
if ($userrow["weaponid"] == $itemsrow["id"] || $userrow["armorid"] == $itemsrow["id"] || $userrow["shieldid"] == $itemsrow["id"]) {
if (user()->weaponid == $itemsrow["id"] || user()->armorid == $itemsrow["id"] || user()->shieldid == $itemsrow["id"]) {
$page .= "<td width=\"32%\"><span class=\"light\">".$itemsrow["name"]."</span></td><td width=\"32%\"><span class=\"light\">$attrib ".$itemsrow["attribute"]."</span></td><td width=\"32%\"><span class=\"light\">Already purchased</span></td></tr>\n";
} else {
if ($itemsrow["special"] != "X") { $specialdot = "<span class=\"highlight\">&#42;</span>"; } else { $specialdot = ""; }
@ -98,21 +94,19 @@ function buy()
*/
function buy2($id)
{
global $userrow;
$townrow = get_town_by_xy($userrow['longitude'], $userrow['latitude']);
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error");
$townitems = explode(",", $townrow["itemslist"]);
if (!in_array($id, $townitems)) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error");
$item = get_item($id);
if ($userrow["gold"] < $item["buycost"]) {
if (user()->gold < $item["buycost"]) {
display("You do not have enough gold to buy this item.<br><br>You may return to <a href=\"/\">town</a>, <a href=\"/buy\">store</a>, or use the direction buttons on the left to start exploring.", "Buy Items");
}
$type_to_row_mapping = [1 => 'weaponid', 2 => 'armorid', 3 => 'shieldid'];
$current_equipped_id = $userrow[$type_to_row_mapping[$item['type']] ?? 0];
$current_equipped_id = user()[$type_to_row_mapping[$item['type']] ?? 0];
if ($current_equipped_id != 0) {
$item2 = get_item($current_equipped_id);
@ -132,16 +126,14 @@ function buy3($id)
if (isset($_POST["cancel"])) redirect('/');
global $userrow;
$townrow = get_town_by_xy($userrow['longitude'], $userrow['latitude']);
$townrow = get_town_by_xy(user()->longitude, user()->latitude);
if ($townrow === false) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error");
$townitems = explode(",", $townrow["itemslist"]);
if (!in_array($id, $townitems)) display("Cheat attempt detected.<br><br>Get a life, loser.", "Error");
$item = get_item($id);
if ($userrow["gold"] < $item["buycost"]) {
if (user()->gold < $item["buycost"]) {
display("You do not have enough gold to buy this item.<br><br>You may return to <a href=\"/\">town</a>, <a href=\"/buy\">store</a>, or use the direction buttons on the left to start exploring.", "Buy Items");
}
@ -157,7 +149,7 @@ function buy3($id)
}
// Retrieve current equipped item or create a default
$current_equip_id = $userrow[$type_mapping[$item["type"]]['id']];
$current_equip_id = user()[$type_mapping[$item["type"]]['id']];
if ($current_equip_id != 0) {
$item2 = get_item($current_equip_id);
} else {
@ -175,9 +167,9 @@ function buy3($id)
$toChange = $special[0];
$changeAmount = $index === 0 ? $special[1] : -$special[1];
$userrow[$toChange] += $changeAmount;
user()[$toChange] += $changeAmount;
$specialFields[] = "$toChange = ?";
$specialValues[] = $userrow[$toChange];
$specialValues[] = user()[$toChange];
// Adjust attack or defense power
if ($toChange == "strength" || $toChange == "dexterity") {
@ -190,15 +182,15 @@ function buy3($id)
// Determine power and type-specific updates
$currentType = $type_mapping[$item["type"]];
$powerField = $currentType['power'];
$newPower = $userrow[$powerField] + $item["attribute"] - $item2["attribute"];
$newPower = user()[$powerField] + $item["attribute"] - $item2["attribute"];
// Calculate new gold with trade-in value
$newGold = $userrow["gold"] + ceil($item2["buycost"]/2) - $item["buycost"];
$newGold = user()->gold + ceil($item2["buycost"]/2) - $item["buycost"];
// Ensure current HP/MP/TP don't exceed max values
$newhp = min($userrow["currenthp"], $userrow["maxhp"]);
$newmp = min($userrow["currentmp"], $userrow["maxmp"]);
$newtp = min($userrow["currenttp"], $userrow["maxtp"]);
$newhp = min(user()->currenthp, user()->maxhp);
$newmp = min(user()->currentmp, user()->maxmp);
$newtp = min(user()->currenttp, user()->maxtp);
$updateFields = array_merge(
$specialFields,
@ -223,7 +215,7 @@ function buy3($id)
$newhp,
$newmp,
$newtp,
$userrow["id"]
user()->id
]
);
@ -238,9 +230,7 @@ function buy3($id)
*/
function maps()
{
global $userrow;
$mappedtowns = explode(",", $userrow["towns"]);
$mappedtowns = explode(",", user()->towns);
$page = "Buying maps will put the town in your Travel To box, and it won't cost you as many TP to get there.<br><br>\n";
$page .= "Click a town name to purchase its map.<br><br>\n";
@ -272,11 +262,9 @@ function maps()
*/
function maps2($id)
{
global $userrow;
$townrow = get_town_by_id($id);
if ($userrow["gold"] < $townrow["mapprice"]) {
if (user()->gold < $townrow["mapprice"]) {
display("You do not have enough gold to buy this map.<br><br>You may return to <a href=\"/\">town</a>, <a href=\"/maps\">store</a>, or use the direction buttons on the left to start exploring.", "Buy Maps");
}
@ -292,18 +280,16 @@ function maps3($id)
{
if (isset($_POST["cancel"])) redirect('/');
global $userrow;
$townrow = get_town_by_id($id);
if ($userrow["gold"] < $townrow["mapprice"]) {
if (user()->gold < $townrow["mapprice"]) {
display("You do not have enough gold to buy this map.<br><br>You may return to <a href=\"/\">town</a>, <a href=\"/maps\">store</a>, or use the direction buttons on the left to start exploring.", "Buy Maps");
}
$mappedtowns = $userrow["towns"].",$id";
$newgold = $userrow["gold"] - $townrow["mapprice"];
$mappedtowns = user()->towns.",$id";
$newgold = user()->gold - $townrow["mapprice"];
db()->query('UPDATE users SET towns=?, gold=? WHERE id=?;', [$mappedtowns, $newgold, $userrow['id']]);
db()->query('UPDATE users SET towns=?, gold=? WHERE id=?;', [$mappedtowns, $newgold, user()->id]);
display("Thank you for purchasing this map.<br><br>You may return to <a href=\"/\">town</a>, <a href=\"/maps\">store</a>, or use the direction buttons on the left to start exploring.", "Buy Maps");
}
@ -313,32 +299,30 @@ function maps3($id)
*/
function travelto($id, bool $usepoints = true)
{
global $userrow;
if ($userrow["currentaction"] == "Fighting") redirect('/fight');
if (user()->currentaction == "Fighting") redirect('/fight');
$townrow = get_town_by_id($id);
if ($usepoints) {
if ($userrow["currenttp"] < $townrow["travelpoints"]) {
if (user()->currenttp < $townrow["travelpoints"]) {
display("You do not have enough TP to travel here. Please go back and try again when you get more TP.", "Travel To");
}
$mapped = explode(",",$userrow["towns"]);
$mapped = explode(",",user()->towns);
if (!in_array($id, $mapped)) { display("Cheat attempt detected.<br><br>Get a life, loser.", "Error"); }
}
if (($userrow["latitude"] == $townrow["latitude"]) && ($userrow["longitude"] == $townrow["longitude"])) {
if ((user()->latitude == $townrow["latitude"]) && (user()->longitude == $townrow["longitude"])) {
display("You are already in this town. <a href=\"/\">Click here</a> to return to the main town screen.", "Travel To");
}
$newtp = ($usepoints) ? $userrow["currenttp"] - $townrow["travelpoints"] : $userrow["currenttp"];
$newtp = ($usepoints) ? user()->currenttp - $townrow["travelpoints"] : user()->currenttp;
$newlat = $townrow["latitude"];
$newlon = $townrow["longitude"];
$newid = $userrow["id"];
$newid = user()->id;
// If they got here by exploring, add this town to their map.
$mapped = explode(",",$userrow["towns"]);
$mapped = explode(",",user()->towns);
$town = false;
foreach($mapped as $b) if ($b == $id) $town = true;
$mapped = implode(",", $mapped);

View File

@ -12,6 +12,7 @@ function register_routes(Router $r): Router
$r->form('/lostpassword', 'Users\lostpassword');
$r->form('/changepassword', 'Users\changepassword');
$r->form('/verify', 'Users\verify');
$r->form('/settings', 'Users\settings');
return $r;
}
@ -20,7 +21,7 @@ function register_routes(Router $r): Router
*/
function login()
{
if (checkcookies() !== false) redirect('/');
global $auth;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form = validate($_POST, [
@ -34,11 +35,8 @@ function login()
}
$form = $form['data'];
$query = db()->query('SELECT id, username, password FROM users WHERE username = ? COLLATE NOCASE LIMIT 1;', [$form['username']]);
$row = $query ? $query->fetchArray(SQLITE3_ASSOC) : false;
if ($row === false || !password_verify($_POST['password'] ?? '', $row['password']))
$row = get_user($form['username']);
if ($row === false || !$auth->login($form['username'], $form['password']))
die("Invalid username or password. Please go back and try again.");
$expiretime = $form['remember'] ? time() + 31536000 : 0;
@ -57,6 +55,8 @@ function login()
*/
function logout()
{
global $auth;
$auth->logout();
set_cookie("dkgame", "", -3600);
redirect('/login');
}
@ -185,75 +185,58 @@ function changepassword()
display(render('changepassword'), "Change Password", true, false, false);
}
function settings()
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form = validate($_POST, [
'game_skin' => ['in:0,1']
]);
if (!$form['valid']) exit(ul_from_validate_errors($form['errors']));
$form = $form['data'];
user()->game_skin = $form['game_skin'];
user()->save();
$alert = '<div class="alert">Settings updated</div>';
display($alert . render('settings'), "Account Settings");
}
display(render('settings'), "Account Settings");
}
function sendpassemail($emailaddress, $password)
{
global $controlrow;
extract($controlrow);
$email = <<<HTML
You or someone using your email address submitted a Lost Password application on the $gamename server, located at $gameurl.
You or someone using your email address submitted a Lost Password application on the $gamename server, located at $gameurl.
We have issued you a new password so you can log back into the game.
We have issued you a new password so you can log back into the game.
Your new password is: $password
Your new password is: $password
Thanks for playing.
Thanks for playing.
HTML;
$status = mymail($emailaddress, "$gamename Lost Password", $email);
return $status;
return send_email($emailaddress, "$gamename Lost Password", $email);
}
function sendregmail($emailaddress, $vercode)
{
global $controlrow;
extract($controlrow);
$verurl = $gameurl . "?do=verify";
$verurl = $gameurl . "/verify";
$email = <<<HTML
You or someone using your email address recently signed up for an account on the $gamename server, located at $gameurl.
You or someone using your email address recently signed up for an account on the $gamename server, located at $gameurl.
This email is sent to verify your registration email. In order to begin using your account, you must verify your email address.
Please visit the Verification Page ($verurl) and enter the code below to activate your account.
Verification code: $vercode
This email is sent to verify your registration email. In order to begin using your account, you must verify your email address.
Please visit the Verification Page ($verurl) and enter the code below to activate your account.
Verification code: $vercode
If you were not the person who signed up for the game, please disregard this message. You will not be emailed again.
If you were not the person who signed up for the game, please disregard this message. You will not be emailed again.
HTML;
$status = mymail($emailaddress, "$gamename Account Verification", $email);
return $status;
}
/**
* thanks to arto dot PLEASE dot DO dot NOT dot SPAM at artoaaltonen dot fi.
*/
function mymail($to, $title, $body, $from = '')
{
global $controlrow;
extract($controlrow);
$from = trim($from);
if (!$from) $from = '<'.$controlrow["adminemail"].'>';
$rp = $controlrow["adminemail"];
$org = '$gameurl';
$mailer = 'PHP';
$head = '';
$head .= "Content-Type: text/plain \r\n";
$head .= "Date: ". date('r'). " \r\n";
$head .= "Return-Path: $rp \r\n";
$head .= "From: $from \r\n";
$head .= "Sender: $from \r\n";
$head .= "Reply-To: $from \r\n";
$head .= "Organization: $org \r\n";
$head .= "X-Sender: $from \r\n";
$head .= "X-Priority: 3 \r\n";
$head .= "X-Mailer: $mailer \r\n";
$body = str_replace("\r\n", "\n", $body);
$body = str_replace("\n", "\r\n", $body);
return mail($to, $title, $body, $head);
return send_email($emailaddress, "$gamename Account Verification", $email);
}

104
src/auth.php Normal file
View File

@ -0,0 +1,104 @@
<?php
/*
This is an experimental new class for handling user auth. The idea is to rely as much as possible on PHP's native
session handling. When authenticated, the class will store use data in GLOBALS state.
*/
class Auth
{
/**
* Set up the auth manager; adjust PHP session settings on the fly to improve security. Starts the session for
* this request.
*/
public function __construct()
{
// enhance the native session's sessid cryptography
ini_set('session.sid_length', 64);
ini_set('session.sid_bits_per_character', 6);
session_set_cookie_params([
'lifetime' => 2592000, // 30 days
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
$this->validate();
}
private function validate(): void
{
// Check for IP address change
if (!isset($_SESSION['ip_address'])) {
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
} elseif ($_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
$this->destroy(); // Possible hijacking
exit;
}
// Check for User-Agent change
if (!isset($_SESSION['user_agent'])) {
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
$this->destroy(); // Possible hijacking
exit;
}
// Regenerate session ID periodically for security
if (!isset($_SESSION['last_regeneration'])) {
$_SESSION['last_regeneration'] = time();
} elseif (time() - $_SESSION['last_regeneration'] > 300) { // Every 5 minutes
$this->regenerate();
}
}
public function login(string $username, string $password): bool
{
$user = get_user($username);
if ($user === false) return false;
if (password_verify($password, $user['password'])) {
$_SESSION['authenticated'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['login_time'] = time();
$this->regenerate();
return true;
}
return false;
}
public function good(): bool
{
return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
}
public function logout(): void
{
$this->destroy();
}
private function regenerate(): void
{
session_regenerate_id(true);
$_SESSION['last_regeneration'] = time();
}
public function destroy(): void
{
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
}
}

View File

@ -2,8 +2,10 @@
require_once 'lib.php';
require_once 'router.php';
require_once 'explore.php';
require_once 'heal.php';
require_once 'auth.php';
require_once 'mail.php';
require_once 'actions/explore.php';
require_once 'actions/heal.php';
require_once 'actions/users.php';
require_once 'actions/help.php';
require_once 'actions/towns.php';
@ -11,8 +13,14 @@ require_once 'actions/fight.php';
require_once 'actions/forum.php';
require_once 'actions/install.php';
require_once 'actions/admin.php';
require_once 'models/model.php';
require_once 'models/user.php';
env_load('../.env');
$uri = uri();
$GLOBALS['cache'] = [];
$GLOBALS['state'] = [];
if (!file_exists('../.installed') && $uri[0] !== 'install') {
redirect('/install');
@ -25,26 +33,26 @@ if (!file_exists('../.installed') && $uri[0] !== 'install') {
display("The game is currently closed for maintanence. Please check back later.", "Game Closed");
}
$auth = new Auth;
// Login (or verify) if not logged in.
if (($userrow = checkcookies()) === false) {
if (user() === false) {
if (!in_array($uri[0], ['login', 'register', 'verify', 'lostpassword', 'help'])) {
redirect('/login');
}
} else {
// Block user if he/she has been banned.
if ($userrow["authlevel"] === 2) {
if (user()->authlevel === 2) {
exit("Your account has been banned.");
}
// Force verify if the user isn't verified yet.
if ($controlrow["verifyemail"] && (bool) $userrow["verify"] === false) {
if ($controlrow['verifyemail'] && user()->verify !== 'g2g' && !in_array($uri[0], ['verify', 'logout'])) {
redirect('/verify');
header("Location: users.php?do=verify");
exit;
}
// Ensure the user can't use the admin panel.
if ($userrow['authlevel'] !== 1 && $uri[0] === 'admin') {
if (user()->authlevel !== 1 && $uri[0] === 'admin') {
redirect('/');
}
}

View File

@ -28,12 +28,18 @@ class Database extends SQLite3
public function query(string $query, array $params = []): SQLite3Result|false
{
$p = strpos($query, '?') !== false;
$stmt = $this->prepare($query);
foreach ($params ?? [] as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, $this->getSQLiteType($v));
try {
$stmt = $this->prepare($query);
foreach ($params ?? [] as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, $this->getSQLiteType($v));
} catch (Exception $e) {
exit("Failed to prepare query ($query): ".$this->lastErrorMsg());
}
$start = microtime(true);
$r = $stmt->execute();
$this->log($query, microtime(true) - $start);
$error = '';
$r = $stmt->execute();
$this->log($query, microtime(true) - $start, $error);
return $r;
}
@ -68,11 +74,11 @@ class Database extends SQLite3
/**
* Log the query, including the time it took. Increment the query counter.
*/
private function log(string $query, float $time_taken): void
private function log(string $query, float $time_taken, string $error = ''): void
{
$this->count++;
$this->query_time += $time_taken;
$this->log[] = [$query, $time_taken];
$this->log[] = [$query, $time_taken, $error];
}
/**

View File

@ -1,36 +0,0 @@
<?php
// explore.php :: Handles all map exploring, chances to fight, etc.
function move()
{
global $userrow, $controlrow;
if ($userrow["currentaction"] == "Fighting") { redirect('/fight'); }
$latitude = $userrow["latitude"];
$longitude = $userrow["longitude"];
$form = validate($_POST, [
'direction' => ['in:north,west,east,south']
]);
if (!$form['valid']) display(ul_from_validate_errors($form['errors']), 'Move Error');
$d = $form['data']['direction'];
if ($d === 'north') { $latitude++; if ($latitude > $controlrow["gamesize"]) { $latitude = $controlrow["gamesize"]; } }
if ($d === 'south') { $latitude--; if ($latitude < ($controlrow["gamesize"]*-1)) { $latitude = ($controlrow["gamesize"]*-1); } }
if ($d === 'east') { $longitude++; if ($longitude > $controlrow["gamesize"]) { $longitude = $controlrow["gamesize"]; } }
if ($d === 'west') { $longitude--; if ($longitude < ($controlrow["gamesize"]*-1)) { $longitude = ($controlrow["gamesize"]*-1); } }
$town = get_town_by_xy($longitude, $latitude);
if ($town !== false) {
Towns\travelto($town['id'], false);
return;
}
$chancetofight = rand(1, 5);
$action = $chancetofight === 1 ? "currentaction='Fighting', currentfight='1'," : "currentaction='Exploring',";
db()->query("UPDATE users SET $action latitude = ?, longitude = ?, dropcode = 0 WHERE id = ?;", [$latitude, $longitude, $userrow['id']]);
redirect('/');
}

View File

@ -5,7 +5,6 @@ require_once __DIR__ . '/database.php';
define('VERSION', '1.2.5');
define('BUILD', 'Reawaken');
define('START', microtime(true));
define('DEBUG', false);
/**
* Open or get SQLite database connection.
@ -100,10 +99,10 @@ function display_admin($content, $title)
*/
function display($content, $title, bool $topnav = true, bool $leftnav = true, bool $rightnav = true): void
{
global $userrow, $controlrow;
global $controlrow;
if ($topnav == true) {
if ($userrow !== false) { // user should be logged in
if (user() !== false) { // user should be logged in
$topnav = <<<HTML
<a href='/logout'><img src='/img/button_logout.gif' alt='Log Out' title='Log Out'></a>
<a href='/help'><img src='/img/button_help.gif' alt='Help' title='Help'></a>
@ -119,101 +118,66 @@ function display($content, $title, bool $topnav = true, bool $leftnav = true, bo
$topnav = '';
}
if (isset($userrow) && $userrow !== false) {
// Get userrow again, in case something has been updated.
$userquery = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$userrow['id']]);
unset($userrow);
$userrow = $userquery->fetchArray(SQLITE3_ASSOC);
// Current town name.
if ($userrow["currentaction"] == "In Town") {
$townquery = db()->query('SELECT * FROM towns WHERE latitude = ? AND longitude = ? LIMIT 1;', [$userrow["latitude"], $userrow["longitude"]]);
$townrow = $townquery->fetchArray(SQLITE3_ASSOC);
$userrow["currenttown"] = "Welcome to <b>".$townrow["name"]."</b>.<br><br>";
if (user() !== false) {
if (user()->currentaction == 'In Town') {
$town = get_town_by_xy(user()->latitude, user()->longitude);
$current_town = "Welcome to <b>{$town['name']}</b>.<br><br>";
} else {
$userrow["currenttown"] = "";
$current_town = '';
}
$userrow["forumslink"] = '<a href="/forum">Forum</a><br>';
// Format various userrow stuffs...
if ($userrow["latitude"] < 0) { $userrow["latitude"] = $userrow["latitude"] * -1 . "S"; } else { $userrow["latitude"] .= "N"; }
if ($userrow["longitude"] < 0) { $userrow["longitude"] = $userrow["longitude"] * -1 . "W"; } else { $userrow["longitude"] .= "E"; }
$userrow["experience"] = number_format($userrow["experience"]);
$userrow["gold"] = number_format($userrow["gold"]);
if ($userrow["authlevel"] == 1) { $userrow["adminlink"] = "<a href=\"/admin\">Admin</a><br>"; } else { $userrow["adminlink"] = ""; }
// HP/MP/TP bars.
$stathp = ceil($userrow["currenthp"] / $userrow["maxhp"] * 100);
if ($userrow["maxmp"] != 0) { $statmp = ceil($userrow["currentmp"] / $userrow["maxmp"] * 100); } else { $statmp = 0; }
$stattp = ceil($userrow["currenttp"] / $userrow["maxtp"] * 100);
$stattable = "<table width=\"100\"><tr><td width=\"33%\">\n";
$stattable .= "<table cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding:0px; width:15px; height:100px; border:solid 1px black; vertical-align:bottom;\">\n";
if ($stathp >= 66) { $stattable .= "<div style=\"padding:0px; height:".$stathp."px; border-top:solid 1px black; background-image:url(/img/bars_green.gif);\"><img src=\"/img/bars_green.gif\" alt=\"\" /></div>"; }
if ($stathp < 66 && $stathp >= 33) { $stattable .= "<div style=\"padding:0px; height:".$stathp."px; border-top:solid 1px black; background-image:url(/img/bars_yellow.gif);\"><img src=\"/img/bars_yellow.gif\" alt=\"\" /></div>"; }
if ($stathp < 33) { $stattable .= "<div style=\"padding:0px; height:".$stathp."px; border-top:solid 1px black; background-image:url(/img/bars_red.gif);\"><img src=\"/img/bars_red.gif\" alt=\"\" /></div>"; }
$stattable .= "</td></tr></table></td><td width=\"33%\">\n";
$stattable .= "<table cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding:0px; width:15px; height:100px; border:solid 1px black; vertical-align:bottom;\">\n";
if ($statmp >= 66) { $stattable .= "<div style=\"padding:0px; height:".$statmp."px; border-top:solid 1px black; background-image:url(/img/bars_green.gif);\"><img src=\"/img/bars_green.gif\" alt=\"\" /></div>"; }
if ($statmp < 66 && $statmp >= 33) { $stattable .= "<div style=\"padding:0px; height:".$statmp."px; border-top:solid 1px black; background-image:url(/img/bars_yellow.gif);\"><img src=\"/img/bars_yellow.gif\" alt=\"\" /></div>"; }
if ($statmp < 33) { $stattable .= "<div style=\"padding:0px; height:".$statmp."px; border-top:solid 1px black; background-image:url(/img/bars_red.gif);\"><img src=\"/img/bars_red.gif\" alt=\"\" /></div>"; }
$stattable .= "</td></tr></table></td><td width=\"33%\">\n";
$stattable .= "<table cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding:0px; width:15px; height:100px; border:solid 1px black; vertical-align:bottom;\">\n";
if ($stattp >= 66) { $stattable .= "<div style=\"padding:0px; height:".$stattp."px; border-top:solid 1px black; background-image:url(/img/bars_green.gif);\"><img src=\"/img/bars_green.gif\" alt=\"\" /></div>"; }
if ($stattp < 66 && $stattp >= 33) { $stattable .= "<div style=\"padding:0px; height:".$stattp."px; border-top:solid 1px black; background-image:url(/img/bars_yellow.gif);\"><img src=\"/img/bars_yellow.gif\" alt=\"\" /></div>"; }
if ($stattp < 33) { $stattable .= "<div style=\"padding:0px; height:".$stattp."px; border-top:solid 1px black; background-image:url(/img/bars_red.gif);\"><img src=\"/img/bars_red.gif\" alt=\"\" /></div>"; }
$stattable .= "</td></tr></table></td>\n";
$stattable .= "</tr><tr><td>HP</td><td>MP</td><td>TP</td></tr></table>\n";
$userrow["statbars"] = $stattable;
if (user()->latitude < 0) { user()->latitude = user()->latitude * -1 . "S"; } else { user()->latitude .= "N"; }
if (user()->longitude < 0) { user()->longitude = user()->longitude * -1 . "W"; } else { user()->longitude .= "E"; }
user()->experience = number_format(user()->experience);
user()->gold = number_format(user()->gold);
// Now make numbers stand out if they're low.
if ($userrow["currenthp"] <= ($userrow["maxhp"]/5)) { $userrow["currenthp"] = "<blink><span class=\"highlight\"><b>*".$userrow["currenthp"]."*</b></span></blink>"; }
if ($userrow["currentmp"] <= ($userrow["maxmp"]/5)) { $userrow["currentmp"] = "<blink><span class=\"highlight\"><b>*".$userrow["currentmp"]."*</b></span></blink>"; }
if (user()->currenthp <= (user()->maxhp/5)) { user()->currenthp = "<blink><span class=\"highlight\"><b>*".user()->currenthp."*</b></span></blink>"; }
if (user()->currentmp <= (user()->maxmp/5)) { user()->currentmp = "<blink><span class=\"highlight\"><b>*".user()->currentmp."*</b></span></blink>"; }
$spellquery = db()->query('SELECT id, name, type FROM spells;');
$userspells = explode(",",$userrow["spells"]);
$userrow["magiclist"] = "";
while ($spellrow = $spellquery->fetchArray(SQLITE3_ASSOC)) {
$user_spells = explode(',', user()->spells);
$spellquery = get_spells_from_list($user_spells);
user()->magiclist = '';
while ($spell = $spellquery->fetchArray(SQLITE3_ASSOC)) {
$spell = false;
foreach($userspells as $a => $b) {
if ($b == $spellrow["id"] && $spellrow["type"] == 1) { $spell = true; }
foreach($user_spells as $id) {
if ($id === $spell['id'] && $spell['type'] == 1) $spell = true;
}
if ($spell == true) {
$userrow["magiclist"] .= "<a href=\"/spell/{$spellrow["id"]}\">".$spellrow["name"]."</a><br>";
user()->magiclist .= "<a href=\"/spell/{$spell['id']}\">".$spell['name']."</a><br>";
}
}
if ($userrow["magiclist"] == "") { $userrow["magiclist"] = "None"; }
if (user()->magiclist == "") { user()->magiclist = "None"; }
// Travel To list.
$townslist = explode(",",$userrow["towns"]);
$townslist = explode(",",user()->towns);
$townquery2 = db()->query('SELECT * FROM towns ORDER BY id;');
$userrow["townslist"] = "";
$town_list_html = '';
while ($townrow2 = $townquery2->fetchArray(SQLITE3_ASSOC)) {
$town = false;
foreach($townslist as $a => $b) {
if ($b == $townrow2["id"]) { $town = true; }
foreach($townslist as $id) {
if ($id == $townrow2["id"]) { $town = true; }
}
if ($town == true) {
$userrow["townslist"] .= "<a href=\"/gotown/{$townrow2["id"]}\">".$townrow2["name"]."</a><br>\n";
$town_list_html .= "<a href=\"/gotown/{$townrow2["id"]}\">".$townrow2["name"]."</a><br>\n";
}
}
} else {
$userrow = [];
}
echo render('primary', [
"dkgamename" => $controlrow["gamename"],
"title" => $title,
"content" => $content,
'rightnav' => $rightnav ? render('rightnav', ['user' => $userrow]) : '',
"leftnav" => $leftnav ? render('leftnav', ['user' => $userrow]) : '',
"game_skin" => user()->game_skin ??= '0',
'rightnav' => $rightnav ? render('rightnav', ['statbars' => create_stat_table(user())]) : '',
"leftnav" => $leftnav ? render('leftnav', ['town_list' => $town_list_html, 'current_town' => $current_town]) : '',
"topnav" => $topnav,
"totaltime" => round(microtime(true) - START, 4),
"numqueries" => db()->count,
"version" => VERSION,
"build" => BUILD,
"querylog" => DEBUG ? db()->log : []
"querylog" => env('debug', false) ? db()->log : []
]);
exit;
@ -270,9 +234,15 @@ function get_control_row(): array|false
*/
function get_town_by_xy(int $x, int $y): array|false
{
$query = db()->query('SELECT * FROM towns WHERE longitude = ? AND latitude = ? LIMIT 1;', [$x, $y]);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
$cache_tag = "town-$x-$y";
if (!isset($GLOBALS['cache'][$cache_tag])) {
$query = db()->query('SELECT * FROM towns WHERE longitude = ? AND latitude = ? LIMIT 1;', [$x, $y]);
if ($query === false) return false;
$GLOBALS['cache'][$cache_tag] = $query->fetchArray(SQLITE3_ASSOC);
}
return $GLOBALS['cache'][$cache_tag];
}
/**
@ -286,11 +256,14 @@ function get_town_by_id(int $id): array|false
}
/**
* Get a user's data by their ID.
* Get a user's data by their ID, username or email.
*/
function get_user_by_id(int $id): array|false
function get_user(int|string $id, string $data = '*'): array|false
{
$query = db()->query('SELECT * FROM users WHERE id = ? LIMIT 1;', [$id]);
$query = db()->query(
"SELECT $data FROM users WHERE id=? OR username=? COLLATE NOCASE OR email=? COLLATE NOCASE LIMIT 1;",
[$id, $id, $id]
);
if ($query === false) return false;
return $query->fetchArray(SQLITE3_ASSOC);
}
@ -554,18 +527,91 @@ function uri(): array
}
/**
* Redirect to login if not authenticated.
* Load the environment variables from the .env file.
*/
function auth_only(): void
function env_load(string $filePath): void
{
if (!checkcookies()) redirect('/login');
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
// Skip lines that are empty after trimming or are comments
if ($line === '' || str_starts_with($line, '#')) continue;
// Skip lines without an '=' character
if (strpos($line, '=') === false) continue;
[$name, $value] = explode('=', $line, 2);
$name = trim($name);
$value = trim($value, " \t\n\r\0\x0B\"'"); // Trim whitespace and quotes
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
putenv("$name=$value");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
/**
* Retrieve an environment variable.
*/
function env(string $key, mixed $default = null): mixed
{
$v = $_ENV[$key] ?? $_SERVER[$key] ?? (getenv($key) ?: $default);
return match(true) {
$v === 'true' => true,
$v === 'false' => false,
is_numeric($v) => (int) $v,
is_float($v) => (float) $v,
default => $v
};
}
/**
* Redirect to home if authenticated.
* Get the data on spells from a given list of IDs.
*/
function guest_only(): void
function get_spells_from_list(array|string $spell_ids): SQLite3Result|false
{
if (checkcookies()) redirect('/login');
if (is_string($spell_ids)) $spell_ids = explode(',', $spell_ids);
$placeholders = implode(',', array_fill(0, count($spell_ids), '?'));
$query = db()->query("SELECT id, name, type FROM spells WHERE id IN($placeholders)", $spell_ids);
if ($query === false) return false;
return $query;
}
function generate_stat_bar($current, $max)
{
$percent = ($max === 0) ? 0 : ceil($current / $max * 100);
$color = $percent >= 66 ? 'green' : ($percent >= 33 ? 'yellow' : 'red');
return '<div class="stat-bar" style="width: 15px; height: 100px; border: solid 1px black;">' .
'<div style="height: ' . $percent . 'px; background-image: url(/img/bars_' . $color . '.gif);"></div>' .
'</div>';
}
function create_stat_table($userrow)
{
$stat_table = '<div class="stat-table">' .
'<div class="stat-row">' .
'<div class="stat-col">' . generate_stat_bar(user()->currenthp, user()->maxhp) . '<div>HP</div></div>' .
'<div class="stat-col">' . generate_stat_bar(user()->currentmp, user()->maxmp) . '<div>MP</div></div>' .
'<div class="stat-col">' . generate_stat_bar(user()->currenttp, user()->maxtp) . '<div>TP</div></div>' .
'</div>' .
'</div>';
return $stat_table;
}
/**
* Returns the user in the GLOBALS state, if there is one. If not, populates it if there is a SESSION user_id.
*/
function user(): User|false
{
$GLOBALS['state']['user'] ??= ($_SESSION['user_id'] ? User::find($_SESSION['user_id']) : false);
return $GLOBALS['state']['user'];
}

113
src/mail.php Normal file
View File

@ -0,0 +1,113 @@
<?php
/**
* Send an email or log email details
*
* @param string $to Recipient email address
* @param string $subject Email subject
* @param string $message Email body
* @param array $options Optional configuration options
* @return bool Success status of email sending or logging
*/
function send_email(string $to, string $subject, string $message, array $options = []): bool
{
global $controlrow;
$from_addr = empty($controlrow['adminemail']) ? 'noreply@'.$_SERVER['SERVER_NAME'] : $controlrow['adminemail'];
// Default configuration
$config = array_merge([
'from' => $from_addr,
'log_path' => '../logs/email.log',
'method' => 'smtp', // 'smtp' or 'log'
'smtp_host' => env('smtp_host', 'localhost'),
'smtp_port' => env('smtp_port', 25),
'smtp_username' => env('smtp_username', null),
'smtp_password' => env('smtp_password', null),
'smtp_encryption' => env('smtp_encryption', null)
], $options);
// Always send to log during debug
if (env('debug', false)) $config['method'] = 'log';
// Validate input
if (empty($to) || empty($subject) || empty($message)) {
error_log('Email sending failed: Missing required parameters');
return false;
}
// Prepare email headers
$headers = [
'From: ' . $config['from'],
'X-Mailer: PHP/' . phpversion()
];
// Choose sending method
switch ($config['method']) {
case 'log':
// Log email details to file
$logMessage = sprintf(
"[%s] To: %s, Subject: %s, Message:\n\n %s\n\n\n\n",
date('Y-m-d H:i:s'),
$to,
$subject,
$message
);
// Attempt to log to file
if (file_put_contents($config['log_path'], $logMessage, FILE_APPEND) === false) {
error_log('Failed to write to log file: ' . $config['log_path']);
return false;
}
return true;
case 'smtp':
default:
// Attempt to send via SMTP
try {
// Prepare SMTP connection
$smtpConfig = [
'host' => $config['smtp_host'],
'port' => $config['smtp_port'],
'username' => $config['smtp_username'],
'password' => $config['smtp_password'],
'encryption' => $config['smtp_encryption']
];
// Send email using PHP's mail function (basic SMTP)
$result = mail(
$to,
$subject,
$message,
implode("\r\n", $headers)
);
if (!$result) {
error_log('SMTP email sending failed');
return false;
}
return true;
} catch (Exception $e) {
error_log('Email sending error: ' . $e->getMessage());
return false;
}
}
}
// Example usage:
// Send via SMTP
// send_email('recipient@example.com', 'Test Subject', 'Email body text');
// Send via log
// send_email('recipient@example.com', 'Test Subject', 'Email body text', ['method' => 'log']);
// Customize SMTP settings
// send_email('recipient@example.com', 'Test Subject', 'Email body text', [
// 'method' => 'smtp',
// 'smtp_host' => 'smtp.yourserver.com',
// 'smtp_port' => 587,
// 'smtp_username' => 'your_username',
// 'smtp_password' => 'your_password',
// 'smtp_encryption' => 'tls'
// ]);

42
src/models/model.php Normal file
View File

@ -0,0 +1,42 @@
<?php
class Model
{
protected string $table_name = '';
protected array $original_data = [];
protected array $changes = [];
public function __construct(array $data)
{
$this->original_data = $data;
$this->changes = [];
}
public function __get(string $key): mixed
{
return array_key_exists($key, $this->changes) ? $this->changes[$key] : $this->original_data[$key] ?? false;
}
public function __set(string $key, mixed $value): void
{
if (array_key_exists($key, $this->original_data)) $this->changes[$key] = $value;
}
public function save(): bool
{
if (empty($this->changes)) return true;
$placeholders = [];
$values = [];
foreach ($this->changes as $key => $value) {
$placeholders[] = "$key=?";
$values[] = $value;
}
$values[] = $this->id;
$query = 'UPDATE ' . $this->table_name . ' SET ' . implode(', ', $placeholders) . ' WHERE id = ?;';
$result = db()->query($query, $values);
return $result === false ? false : true;
}
}

21
src/models/user.php Normal file
View File

@ -0,0 +1,21 @@
<?php
class User extends Model
{
protected string $table_name = 'users';
/**
* Find a user by their ID, username or email. Returns false on any failure.
*/
public static function find(int|string $id): User|false
{
$query = db()->query(
"SELECT * FROM users WHERE id=? OR username=? COLLATE NOCASE OR email=? COLLATE NOCASE LIMIT 1;",
[$id, $id, $id]
);
if ($query === false) return false;
$data = $query->fetchArray(SQLITE3_ASSOC);
if ($data === false) return false;
return new User($data);
}
}

View File

@ -1,8 +1,8 @@
<section>
<div class="title"><img src="/img/button_location.gif" alt="Location" title="Location"></div>
Currently: <?= $user['currentaction'] ?><br>
Latitude: <?= $user['latitude'] ?><br>
Longitude: <?= $user['longitude'] ?><br>
Currently: <?= user()->currentaction ?><br>
Latitude: <?= user()->latitude ?><br>
Longitude: <?= user()->longitude ?><br>
<a href="javascript:openmappopup()">View Map</a><br>
<form action="/move" method="post" class="move-compass">
<button type="submit" name="direction" value="north" class="north">North</button>
@ -16,17 +16,25 @@
<section>
<div class="title"><img src="/img/button_towns.gif" alt="Towns" title="Towns"></div>
<?= $user['currenttown'] ?>
<?php
if (user()->currentaction == 'In Town') {
$town = get_town_by_xy((int) user()->latitude, (int) user()->longitude);
echo "Welcome to <b>{$town['name']}</b>.<br><br>";
}
?>
Travel To:<br>
<?= $user['townslist'] ?>
<?= $town_list ?>
</section>
<section>
<div class="title"><img src="/img/button_functions.gif" alt="Functions" title="Functions"></div>
<a href="/">Home</a><br>
<?= $user['forumslink'] ?>
<?= $user['adminlink'] ?>
<a href="/forum">Forum</a><br>
<a href="/settings">Settings</a><br>
<a href="/changepassword">Change Password</a><br>
<a href="/logout">Log Out</a><br>
<?php if (user()->authlevel === 1): ?>
<a href="/admin">Admin</a><br>
<?php endif; ?>
<a href="/help">Help</a>
</section>

View File

@ -1,63 +1,9 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?></title>
<style type="text/css">
body {
background-image: url('/img/background.jpg');
color: black;
font: 11px verdana;
}
table {
border-style: none;
padding: 0px;
font: 11px verdana;
}
td {
border-style: none;
padding: 3px;
vertical-align: top;
}
td.top {
border-bottom: solid 2px black;
}
td.left {
width: 150px;
border-right: solid 2px black;
}
td.right {
width: 150px;
border-left: solid 2px black;
}
a {
color: #663300;
text-decoration: none;
font-weight: bold;
}
a:hover {
color: #330000;
}
.small {
font: 10px verdana;
}
.highlight {
color: red;
}
.light {
color: #999999;
}
.title {
border: solid 1px black;
background-color: #eeeeee;
font-weight: bold;
padding: 5px;
margin: 3px;
}
.copyright {
border: solid 1px black;
background-color: #eeeeee;
font: 10px verdana;
}
</style>
<link rel="stylesheet" href="/css/dk.css">
</head>
<body>
<?= $content ?>

View File

@ -19,7 +19,7 @@
}
</script>
</head>
<body>
<body class="skin-<?= $game_skin ?>">
<div id="game-container">
<header>
<img id="logo" src="/img/logo.gif" alt="<?= $dkgamename ?>" title="<?= $dkgamename ?>">
@ -42,7 +42,11 @@
<?php
if (!empty($querylog)) {
echo '<pre>';
foreach ($querylog as $record) echo '<div>['.round($record[1], 2)."s] {$record[0]}</div>";
foreach ($querylog as $record) {
$query_string = str_replace(["\r\n", "\n", "\r"], ' ', $record[0]);
$error_string = !empty($record[2]) ? '// '.$record[2] : '';
echo '<div>['.round($record[1], 2)."s] {$query_string}{$error_string}</div>";
}
echo '</pre>';
}
?>

View File

@ -1,13 +1,13 @@
<section>
<div class="title"><img src="/img/button_character.gif" alt="Character" title="Character"></div>
<b><?= $user['username'] ?></b><br>
Level: <?= $user['level'] ?><br>
Exp: <?= $user['experience'] ?><br>
Gold: <?= $user['gold'] ?><br>
HP: <?= $user['currenthp'] ?><br>
MP: <?= $user['currentmp'] ?><br>
TP: <?= $user['currenttp'] ?><br>
<?= $user['statbars'] ?><br>
<b><?= user()->username ?></b><br>
Level: <?= user()->level ?><br>
Exp: <?= user()->experience ?><br>
Gold: <?= user()->gold ?><br>
HP: <?= user()->currenthp ?><br>
MP: <?= user()->currentmp ?><br>
TP: <?= user()->currenttp ?><br><br>
<?= $statbars ?><br>
<a href="javascript:opencharpopup()">Extended Stats</a>
</section>

12
templates/settings.php Normal file
View File

@ -0,0 +1,12 @@
<h1>Account Settings</h1>
<p>Here you can change some basic settings for your account.</p>
<form action="/settings" method="post">
<label for="game_skin">Game Skin</label>
<select id="game_skin" name="game_skin">
<option value="0">Default</option>
<option value="1">Snowstorm</option>
</select>
<button type="submit">Save</button>
</form>