added shadowrun database

This commit is contained in:
Lukas Forsberg 2025-06-01 21:19:54 +02:00
parent 71f771b428
commit 69f7f625f8
20 changed files with 738 additions and 103 deletions

88
.vscode/settings.json vendored
View File

@ -1,92 +1,6 @@
{
"files.associations": {
"any": "cpp",
"array": "cpp",
"atomic": "cpp",
"hash_map": "cpp",
"bit": "cpp",
"*.tcc": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"coroutine": "cpp",
"csignal": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"forward_list": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"source_location": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"format": "cpp",
"fstream": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"ranges": "cpp",
"semaphore": "cpp",
"shared_mutex": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stdfloat": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"text_encoding": "cpp",
"thread": "cpp",
"cfenv": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"valarray": "cpp",
"variant": "cpp",
"*.ipp": "cpp",
"expected": "cpp",
"queue": "cpp",
"stack": "cpp",
"strstream": "cpp",
"complex": "cpp",
"typeindex": "cpp"
"thread": "cpp"
}
}

View File

@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.10)
project(CrowHTMX)
set(TARGET_NAME lf-server-admin-panel )
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -30,9 +32,12 @@ foreach(file IN LISTS TEMPLATE_FILES)
endforeach()
# Use Crow from system include (installed via yay -S crow + asio)
include_directories(/usr/include)
include_directories(/usr/include src src/htmx src/shadowrun src/database)
add_executable(lf-server-admin-panel src/main.cpp
add_executable(${TARGET_NAME}
src/main.cpp
src/utils.hpp
src/utils.cpp
src/htmx/HtmxTable.cpp
src/htmx/HtmxTable.h
src/systemd.cpp
@ -46,9 +51,29 @@ add_executable(lf-server-admin-panel src/main.cpp
src/htmx_helper.h
src/json_settings.cpp
src/json_settings.h
src/json.hpp)
src/json.hpp
target_link_libraries(lf-server-admin-panel pthread)
src/database/database.cpp
src/database/database.hpp
# Shadowrun
src/shadowrun/HtmxShItemList.cpp
src/shadowrun/HtmxShItemList.hpp
src/shadowrun/HtmxShAttributeList.cpp
src/shadowrun/HtmxShAttributeList.hpp
src/shadowrun/HtmxShCondition.cpp
src/shadowrun/HtmxShCondition.hpp
src/shadowrun/ShadowrunCharacterForm.hpp
src/shadowrun/ShadowrunCharacterForm.cpp
src/shadowrun/ShadowrunApi.cpp
src/shadowrun/ShadowrunApi.hpp
)
target_compile_definitions(${TARGET_NAME} PRIVATE APPLICATION_NAME="${TARGET_NAME}")
target_link_libraries(${TARGET_NAME} pthread sqlite3)
# Optional: Print build type at configuration time
message(STATUS "Configuring build type: ${CMAKE_BUILD_TYPE}")

View File

@ -1,6 +1,6 @@
## Setup
pacman -S crow asio gdb gcc cmake make
pacman -S crow asio gdb gcc cmake make sqlite3
## Build

35
src/database/database.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "crow.h"
#include "database.hpp"
Database::Database() :
m_db(nullptr)
{}
Database::~Database() {
sqlite3_close(m_db);
}
bool Database::exec(const char* sqlQuery) {
char* errmsg = nullptr;
int rc = sqlite3_exec(m_db, sqlQuery, nullptr, nullptr, &errmsg);
if (rc != SQLITE_OK) {
CROW_LOG_ERROR << "SQL error: " << errmsg;
sqlite3_free(errmsg);
return false;
}
return true;
}
bool Database::exec(const std::string& sqlQuery)
{
exec(sqlQuery.c_str());
}
bool Database::open(){
int rc = sqlite3_open("example.db", &m_db);
if (rc) {
CROW_LOG_ERROR << "Can't open database: " << sqlite3_errmsg(m_db);
return false;
}
return true;
}

21
src/database/database.hpp Normal file
View File

@ -0,0 +1,21 @@
#ifndef __DATABASE_H__
#define __DATABASE_H__
#include "sqlite3.h"
#include <string>
class Database {
public:
Database();
~Database();
bool open();
bool exec(const char* sqlQuery);
bool exec(const std::string& sqlQuery);
private:
sqlite3* m_db;
};
#endif // __DATABASE_H__

View File

@ -5,6 +5,7 @@
#include "systemd.h"
#include "htmx_helper.h"
#include "json_settings.h"
#include "utils.hpp"
using namespace std;

View File

@ -1,6 +1,4 @@
#include <crow.h>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <string>
#include <print>
@ -8,16 +6,11 @@
#include "json_settings.h"
#include "htmx_helper.h"
#include "systemd.h"
#include "utils.hpp"
#include "ShadowrunApi.hpp"
using namespace std;
string load_file(const string& path) {
ifstream f(path);
stringstream buffer;
buffer << f.rdbuf();
return buffer.str();
}
optional<string> get_body_name(const string& body) {
const auto pos = body.find('=');
if (pos == std::string::npos) return {};
@ -34,11 +27,15 @@ int main() {
crow::SimpleApp app;
CROW_ROUTE(app, "/")([] {
return crow::response(load_file("templates/index.html"));
return crow::response(utils::loadFile("templates/index.html"));
});
CROW_ROUTE(app, "/static/<string>")([](const std::string& file) {
return crow::response(load_file("static/" + file));
return crow::response(utils::loadFile("static/" + file));
});
CROW_ROUTE(app, "/templates/<string>")([](const std::string& file) {
return crow::response(utils::loadFile("templates/" + file));
});
CROW_ROUTE(app, "/status")([] {
@ -80,8 +77,20 @@ int main() {
} else {
CROW_LOG_ERROR << "failed to load settings : " << opt_settings.error();
}
auto opt_isPortOpen = utils::isLocalPortOpen(httpPort);
if (opt_isPortOpen.has_value()){
if (opt_isPortOpen.value()){
CROW_LOG_ERROR << "Local port : " << httpPort << " is already open";
}
}
else {
CROW_LOG_ERROR << "failed to check if local port is open : " << opt_isPortOpen.error();
}
}
shadowrun::initApi(app);
app.loglevel(crow::LogLevel::INFO);
app.port(httpPort).multithreaded().run();
}

View File

@ -0,0 +1,26 @@
#include <format>
#include "HtmxShAttributeList.hpp"
#include "utils.hpp"
using namespace std;
HtmxShAttributeList::HtmxShAttributeList(const std::string& id, const vector<string>& itemList){
html += format("<h2>{}</h2>", id);
html += "<div class='grid'>";
for (auto& item : itemList){
string item_id = utils::to_id_format(id + "_" + item);
html += format("<label>{}:<input type='text' name='{}'></label>", item, item_id);
}
html += "</div>";
}
HtmxShAttributeList::HtmxShAttributeList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList){
html += format("<h2>{}</h2>", id);
html += "<div class='grid'>";
for (auto& item : itemValueList){
string item_id = utils::to_id_format(id + "_" + item.first);
html += format("<label>{}:<input type='text' name='{}' value='{}'></label>", item, item_id, item.second);
}
html += "</div>";
}

View File

@ -0,0 +1,18 @@
#ifndef HTMXSHATTRIBUTELIST_H
#define HTMXSHATTRIBUTELIST_H
#include "HtmxObject.h"
#include <vector>
#include <string>
class HtmxShAttributeList : public HtmxObject {
public:
// create new item list
HtmxShAttributeList(const std::string& id, const std::vector<std::string>& itemList);
// create new item list where each item as a value
HtmxShAttributeList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList);
};
#endif // HTMXSHATTRIBUTELIST_H

View File

@ -0,0 +1,28 @@
#include "HtmxShCondition.hpp"
#include "../utils.hpp"
#include <format>
using namespace std;
HtmxShCondition::HtmxShCondition(std::string id, size_t nbrOfBoxes)
{
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += "<div class='monitor-track'>";
int con_value = -1;
for (size_t i = 0; i < nbrOfBoxes; i++)
{
string item_id = utils::to_id_format(format("{}_{}",id, i));
html += format("<label class='monitor-box'><input type='checkbox' name='{}'></label>", item_id);
if ( ((i + 1) % 3 == 0) && (i != 0) )
{
html += format("<div class='monitor-number'>{}</div>", con_value);
con_value--;
}
}
html += "</div></div>";
}

View File

@ -0,0 +1,13 @@
#ifndef __HTMXSHCONDITION_H__
#define __HTMXSHCONDITION_H__
#include <string>
#include "HtmxObject.h"
class HtmxShCondition : public HtmxObject {
public:
HtmxShCondition(std::string id, size_t nbrOfBoxes);
};
#endif // __HTMXSHCONDITION_H__

View File

@ -0,0 +1,43 @@
#include <format>
#include "HtmxShItemList.hpp"
#include "../utils.hpp"
using namespace std;
HtmxShItemList::HtmxShItemList(const std::string& id, const std::vector<std::string>& columns, size_t size){
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += format("<div class='grid grid-{}'>", columns.size());
for (size_t i = 0; i < size; i++){
for (auto& col : columns){
string item_id = utils::to_id_format(format("{}_{}_{}", id, i, col));
html += format("<input type='text' name='{}' placeholder='{}'>", item_id, col);
}
}
html += "</div></div>";
}
/*
HtmxShItemList::HtmxShItemList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList){
html += format("<h2>{}</h2>", id);
html += "<div class='grid'>";
for (auto& item : itemValueList){
string item_id = utils::to_id_format(id + "_" + item.first);
html += format("<label>{}:<input type='text' name='{}' value='{}'></label>", item, item_id, item.second);
}
html += "</div>";
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += format("<div class='grid grid-{}'>", columns.size());
for (size_t i = 0; i < size; i++){
for (auto& col : columns){
string item_id = utils::to_id_format(format("{}_{}_{}", id, i, col));
html += format("<input type='text' name='{}' placeholder='{}' value='{}'>", item_id, col);
}
}
html += "</div></div>";
}
*/

View File

@ -0,0 +1,18 @@
#ifndef HTMXSHITEMLIST_H
#define HTMXSHITEMLIST_H
#include "HtmxObject.h"
#include <vector>
#include <string>
class HtmxShItemList : public HtmxObject {
public:
// create new item list,
HtmxShItemList(const std::string& id, const std::vector<std::string>& columns, size_t size);
// create new item list where each item as a value
HtmxShItemList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList);
};
#endif // HTMXSHITEMLIST_H

View File

@ -0,0 +1,106 @@
#include "ShadowrunApi.hpp"
#include "ShadowrunCharacterForm.hpp"
#include "database.hpp"
namespace shadowrun
{
bool initDb() {
auto db = Database();
if (!db.open()){
return false;
}
// Create a tables
const char* create_sql_chars = "CREATE TABLE IF NOT EXISTS shadowrun_characters ("
"id INTEGER PRIMARY KEY,"
"name TEXT,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP);";
if (!db.exec(create_sql_chars)){
CROW_LOG_ERROR << "Failed to create shadowrun_characters table";
return false;
}
const char* create_sql_data = "CREATE TABLE IF NOT EXISTS shadowrun_data ("
"id INTEGER PRIMARY KEY,"
"character_id INTEGER NOT NULL,"
"name TEXT NOT NULL,"
"value TEXT,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE);";
if (!db.exec(create_sql_data)){
CROW_LOG_ERROR << "Failed to create shadowrun_data table";
return false;
}
return true;
}
void initApi(crow::SimpleApp& app)
{
CROW_ROUTE(app, "/api/shadowrun/submit-character").methods("POST"_method)(
[](const crow::request& req) {
auto params = crow::query_string(req.body);
std::string name = params.get("name") ? params.get("name") : "";
std::string metatype = params.get("metatype") ? params.get("metatype") : "";
std::string age = params.get("age") ? params.get("age") : "";
// ... extract more fields as needed
// Optionally save to a DB or do logic here
// Return response HTML
std::ostringstream out;
out << "<div class='alert alert-success'>"
<< "Character " << name << " submitted successfully!"
<< "</div>";
return crow::response{out.str()};
});
CROW_ROUTE(app, "/api/shadowrun/character-form")
([](const crow::request& req) {
auto query = crow::query_string(req.url_params);
std::string name = query.get("name") ? query.get("name") : "";
// TODO: Load data from file or DB using `name`
std::string metatype = "Troll";
int age = 28;
return crow::response{ShadowrunCharacterForm().htmx()};
});
CROW_ROUTE(app, "/api/shadowrun/character-list")
([] {
std::ostringstream html;
// Simulated character database
std::vector<std::string> characters = { "Trogdor", "Alice", "Zigzag" };
html << "<form hx-get='/api/shadowrun/character-form' hx-target='#form-container' hx-params='*'>"
<< "<label>Character Name: "
<< "<select name='name'>";
for (const auto& name : characters) {
html << "<option value='" << name << "'>" << name << "</option>";
}
html << "</select></label>"
<< "<button type='submit'>Load Character</button>"
<< "</form>";
return crow::response{html.str()};
});
if(initDb()){
CROW_LOG_ERROR << "Failed to Init shadowrun database";
}
}
}

View File

@ -0,0 +1,11 @@
#ifndef __SHADOWRUNAPI_H__
#define __SHADOWRUNAPI_H__
#include <crow.h>
namespace shadowrun {
void initApi(crow::SimpleApp& app);
}
#endif // __SHADOWRUNAPI_H__

View File

@ -0,0 +1,122 @@
#include <string>
#include <vector>
#include "HtmxShItemList.hpp"
#include "HtmxShAttributeList.hpp"
#include "HtmxShCondition.hpp"
#include "ShadowrunCharacterForm.hpp"
using namespace std;
static const vector<string> cCharacterInfo = {
"Name",
"Metatype",
"Age",
"Sex",
"Nuyen",
"Lifestyle",
"Total Karma",
"Current Karma",
"Street Cred",
"Notoriety",
"Public Awareness"
};
static const vector<string> cAttributes = {
"Body",
"Agility",
"Reaction",
"Strength",
"Charisma",
"Intuition",
"Logic",
"Willpower",
"Edge",
"Essence",
"Initiative"
};
static const vector<string> cSkillParameters = {
"Name",
"RTG.",
"ATT.",
};
static const vector<string> cContactsParameters = {
"Name",
"Loyalty",
};
static const vector<string> cRangedWeaponsParameters = {
"Weapon",
"Damage",
"AP",
"Mode",
"RC",
"Ammo"
};
static const vector<string> cImplantParameters = {
"Implant",
"Rating",
"Essence",
"Notes",
};
static const vector<string> cMeleeWeaponParameters = {
"Weapon",
"Reach",
"Damage",
"AP",
};
static const vector<string> cArmorParamters = {
"Armor",
"Ballistic",
"Impact",
"Notes",
};
ShadowrunCharacterForm::ShadowrunCharacterForm() {
html.reserve(30000);
html += "<form hx-post='/api/shadowrun/submit-character' hx-target='#form-response' hx-swap='innerHTML'>";
html += HtmxShAttributeList("Character Info", cCharacterInfo).htmx();
html += HtmxShAttributeList("Attributes", cAttributes).htmx();
html += "<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 2em;'>";
html += HtmxShItemList("Active Skills", cSkillParameters, 6).htmx();
html += HtmxShItemList("Knowledge Skills", cSkillParameters, 6).htmx();
// add Qualities
html += "<div class='section'>"
"<h2>Qualities</h2>"
"<label>Positive Qualities:"
"<textarea name='positive_qualities' rows='4'></textarea>"
"</label>"
"<label>Negative Qualities:"
"<textarea name='negative_qualities' rows='4'></textarea>"
"</label>"
"</div>";
// add datapack notes
html += "<div class='section'>"
"<h2>Datajack / Commlink / Cyberdeck / Notes</h2>"
"<label>Notes:"
"<textarea name='datapack_notes' rows='6'></textarea>"
"</label>"
"</div>";
html += HtmxShCondition("Physical Condition", 18).htmx();
html += HtmxShCondition("Stun Condition", 12).htmx();
html += HtmxShItemList("Contacts", cContactsParameters, 6).htmx();
html += HtmxShItemList("Ranged Weapons", cRangedWeaponsParameters, 7).htmx();
html += HtmxShItemList("Cyberware and Bioware", cImplantParameters, 7).htmx();
html += HtmxShItemList("Melee Weapons", cMeleeWeaponParameters, 7).htmx();
html += HtmxShItemList("Armor", cArmorParamters , 3).htmx();
html += "</div>";
html += "<div style='text-align:center'>"
"<button type='submit'>Submit</button>"
"</div>"
"</form>";
}

View File

@ -0,0 +1,14 @@
#ifndef SHADOWRUN_CHARACTER_FORM_HPP
#define SHADOWRUN_CHARACTER_FORM_HPP
#include "htmx/HtmxObject.h"
class ShadowrunCharacterForm : public HtmxObject {
public:
ShadowrunCharacterForm();
private:
};
#endif // SHADOWRUN_CHARACTER_FORM_HPP

79
src/utils.cpp Normal file
View File

@ -0,0 +1,79 @@
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include "utils.hpp"
#include <algorithm>
#include <fstream>
#include <sstream>
using namespace std;
namespace utils {
expected<bool, string> isLocalPortOpen(uint16_t portno) {
const char *hostname = "localhost";
int sockfd;
bool ret;
struct sockaddr_in serv_addr;
struct hostent *server;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
close(sockfd);
return unexpected("ERROR opening socket");
}
server = gethostbyname(hostname);
if (server == NULL) {
close(sockfd);
return unexpected("ERROR, no such host");
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr,
(char *)&serv_addr.sin_addr.s_addr,
server->h_length);
serv_addr.sin_port = htons(portno);
if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) {
ret = false;
} else {
ret = true;
}
close(sockfd);
return ret;
}
string to_id_format(const string& s){
string new_s = s;
// transform(new_s.begin(), new_s.end(), new_s.begin(),
// [](unsigned char c){ return std::tolower(c); });
replace( new_s.begin(), new_s.end(), ' ', '-');
return new_s;
}
string loadFile(const string& path) {
ifstream f(path);
stringstream buffer;
buffer << f.rdbuf();
return buffer.str();
}
std::filesystem::path getDataDir(){
return std::getenv("XDG_DATA_HOME")
? std::filesystem::path(std::getenv("XDG_DATA_HOME")) / APPLICATION_NAME
: std::filesystem::path(std::getenv("HOME")) / ".local" / "share" / APPLICATION_NAME;
}
}

19
src/utils.hpp Normal file
View File

@ -0,0 +1,19 @@
#ifndef UTILS_HPP
#define UTILS_HPP
#include <expected>
#include <string>
#include <cstdint>
#include <filesystem>
namespace utils {
std::expected<bool, std::string> isLocalPortOpen(uint16_t portno);
std::string to_id_format(const std::string& s);
std::string loadFile(const std::string& path);
std::filesystem::path getDataDir();
}
#endif

133
templates/shadowrun.html Normal file
View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/static/htmx.min.js"></script>
<meta charset="UTF-8">
<title>Shadowrun Character Sheet</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 2em;
}
.container {
max-width: 900px;
margin: 0 auto;
background-color: white;
padding: 2em;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2 {
text-align: center;
border-bottom: 1px solid #ccc;
padding-bottom: 0.5em;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1em;
}
.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-4 {
grid-template-columns: repeat(4, 1fr);
}
.grid-6 {
grid-template-columns: repeat(6, 1fr);
}
.skill-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 0.5em;
margin-bottom: 0.5em;
}
h3 {
margin-bottom: 0.5em;
}
label {
display: flex;
flex-direction: column;
font-weight: bold;
}
input[type="text"], input[type="number"] {
padding: 0.4em;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}
button {
margin-top: 2em;
padding: 0.75em 2em;
background-color: #222;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background-color: #444;
}
.monitor-track {
display: grid;
grid-template-columns: repeat(4, auto);
gap: 0.25em;
max-width: 200px;
}
.monitor-box input[type="checkbox"] {
width: 20px;
height: 20px;
margin: 0;
cursor: pointer;
}
.monitor-number {
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Shadowrun Character Sheet</h1>
<div id="character-selector"
hx-get="/api/shadowrun/character-list"
hx-trigger="load"
hx-target="this"
hx-swap="outerHTML">
</div>
<div id="form-container"></div>
<div id="form-response"></div>
</div>
</body>
</html>