Füge Unterstützung für Würfelspiele hinzu und verbessere Debugging-Optionen

- Implementiere neue Funktionen in der ChatRoom-Klasse für das Starten, Rollen und Beenden von Würfelspielen.
- Füge eine Option zur Aktivierung von Debug-Logging in CMake hinzu, um die Entwicklung zu erleichtern.
- Aktualisiere die ChatUser-Klasse, um die Interaktion mit dem Würfelspiel zu ermöglichen.
- Verbessere die Socket-Verwaltung im Server, um WebSocket-Verbindungen zu unterstützen und die Handhabung von Anfragen zu optimieren.
- Aktualisiere die Konfiguration, um die neue Funktionalität zu unterstützen und die Benutzererfahrung zu verbessern.
This commit is contained in:
Torsten Schulz (local)
2025-08-16 22:43:08 +02:00
parent 864d86aa09
commit 7338f1a740
15 changed files with 1556 additions and 135 deletions

View File

@@ -3,6 +3,7 @@
#include "chat_user.h"
#include "server.h"
#include "lib/tools.h"
#include "lib/base.h"
#include <json/json.h>
#include <unistd.h>
#include <sys/socket.h>
@@ -12,12 +13,22 @@
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sstream>
#include <cctype>
namespace Yc
{
namespace Lib
{
static bool _isValidHexColor(const std::string &c) {
if (c.size() != 7 || c[0] != '#') return false;
for (size_t i = 1; i < c.size(); ++i) {
char ch = c[i];
if (!std::isxdigit(static_cast<unsigned char>(ch))) return false;
}
return true;
}
ChatUser::ChatUser(std::shared_ptr<ChatRoom> parent, std::string name, std::string color, int socket)
: _parent(std::move(parent)),
_name(name),
@@ -79,14 +90,20 @@ namespace Yc
userJson["rights"] = rights;
}
_user = Yc::Object::User(userJson);
// Prefer DB color if available
if (_user.id() != 0 && !_user.color().empty()) {
_color = _user.color();
}
_token = Yc::Lib::Tools::generateRandomString(32);
sendMsg(token, _token, "", "");
thread = std::make_unique<std::thread>(&ChatUser::checkerTask, this);
// Beim Initial-Token direkt Name und aktuelle Farbe mitsenden, damit der Client "ich" korrekt färben kann
sendMsg(token, _token, _name, _color);
// Thread-Start erfolgt jetzt explizit per start(), nicht im Konstruktor
}
ChatUser::~ChatUser()
{
_parent->addMessage(ChatUser::system, std::string("leaved_chat"), std::string(_name), std::string(_color));
// Hinweis: Thread wird nicht im Destruktor gejoint, um Deadlocks zu vermeiden!
// Der Thread muss explizit von außen gestoppt und gejoint werden (z.B. im ChatRoom beim Entfernen des Users).
}
std::string ChatUser::name() const
@@ -111,6 +128,9 @@ namespace Yc
void ChatUser::sendMsg(MsgType type, std::string message, std::string userName, std::string color)
{
// Standardwerte für leere Felder setzen
if (userName.empty()) userName = _name;
if (color.empty()) color = _color;
Json::Value sendMessage;
sendMessage["type"] = type;
sendMessage["message"] = message;
@@ -121,6 +141,9 @@ namespace Yc
void ChatUser::sendMsg(MsgType type, Json::Value message, std::string userName, std::string color)
{
// Standardwerte für leere Felder setzen
if (userName.empty()) userName = _name;
if (color.empty()) color = _color;
Json::Value sendMessage;
sendMessage["type"] = type;
sendMessage["message"] = message;
@@ -131,28 +154,44 @@ namespace Yc
void ChatUser::checkerTask()
{
while (!_stop)
{
fd_set readSd;
FD_ZERO(&readSd);
FD_SET(_socket, &readSd);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv);
if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1)
try {
while (!_stop)
{
_parent->removeUser(_token);
_stop = true;
delete this;
return;
fd_set readSd;
FD_ZERO(&readSd);
FD_SET(_socket, &readSd);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv);
if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1)
{
char peek;
ssize_t r = recv(_socket, &peek, 1, MSG_PEEK);
if (r == 0) {
#ifdef YC_DEBUG
std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: <hidden>)" << std::endl;
#endif
_parent->removeUser(_token);
_stop = true;
if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) {
thread.detach();
}
return;
}
}
std::string msg = readSocket(_socket);
if (msg == "")
{
// Kein Inhalt gelesen; kann Idle/Keepalive sein
continue;
}
handleMessage(msg);
}
std::string msg = readSocket(_socket);
if (msg == "")
{
continue;
}
handleMessage(msg);
} catch (const std::exception& ex) {
// swallow, optional hook
} catch (...) {
// swallow, optional hook
}
}
@@ -171,19 +210,29 @@ namespace Yc
_parent = std::move(parent);
}
void ChatUser::send(std::string out)
{
void ChatUser::send(std::string out) {
// Entferne ggf. Token-Felder aus JSON-Strings und sende über Socket/WebSocket
Base::sanitizeTokensInString(out);
Base::send(_socket, out);
}
void ChatUser::send(Json::Value out)
{
void ChatUser::send(Json::Value out) {
// Entferne rekursiv alle Token-Felder und sende über Socket/WebSocket
Base::sanitizeTokens(out);
Base::send(_socket, out);
}
void ChatUser::handleMessage(std::string message)
{
Json::Value jsonTree = getJsonTree(message);
// Eingehende Nachricht (nur im Debug-Mode)
#ifdef YC_DEBUG
{
Json::Value toLog = jsonTree;
if (toLog.isMember("token")) toLog["token"] = "<hidden>";
std::cout << "[Debug] <- Client(fd=" << _socket << ", user='" << _name << "'): " << getJsonString(toLog) << std::endl;
}
#endif
if (jsonTree["token"].asString() != _token)
{
return;
@@ -194,7 +243,38 @@ namespace Yc
}
else if (jsonTree["type"].asString() == "dice")
{
doDice();
// Neue Würfel-Behandlung für das Würfelspiel
if (jsonTree.isMember("value")) {
int diceValue = jsonTree["value"].asInt();
if (!_parent->rollDice(shared_from_this(), diceValue)) {
sendMsg(ChatUser::error, "dice_roll_failed", "", "");
}
} else {
// Fallback: Einfacher Würfelwurf
doDice();
}
}
else if (jsonTree["type"].asString() == "start_dice_game")
{
// Würfelspiel starten (nur Admin)
if (jsonTree.isMember("rounds")) {
int rounds = jsonTree["rounds"].asInt();
if (rounds < 1 || rounds > 10) {
sendMsg(ChatUser::error, "invalid_rounds", "", "");
return;
}
if (!_parent->startDiceGame(rounds, shared_from_this())) {
sendMsg(ChatUser::error, "dice_game_start_failed", "", "");
}
} else {
sendMsg(ChatUser::error, "missing_rounds", "", "");
}
}
else if (jsonTree["type"].asString() == "end_dice_game")
{
// Würfelspiel beenden (nur Admin)
_parent->endDiceGame();
}
else if (jsonTree["type"].asString() == "scream")
{
@@ -202,7 +282,68 @@ namespace Yc
}
else if (jsonTree["type"].asString() == "do")
{
_parent->addMessage(ChatUser::dosomething, jsonTree["message"].asString(), std::string(_name), std::string(_color));
// "do" erwartet "value" (Aktion) und optional "to" (Ziel-User)
std::string action = jsonTree.isMember("value") ? jsonTree["value"].asString() : "";
std::string targetUser = jsonTree.isMember("to") ? jsonTree["to"].asString() : "";
if (action.empty()) {
sendMsg(ChatUser::error, "missing_action", "", "");
return;
}
// Erstelle strukturierte Nachricht mit Ziel-User und Aktion
Json::Value doMsg = Json::objectValue;
doMsg["tr"] = "user_action";
doMsg["action"] = action;
if (!targetUser.empty()) {
doMsg["to"] = targetUser;
// Suche den Ziel-User und füge dessen Informationen hinzu
auto targetUserObj = _parent->findUserByName(targetUser);
if (targetUserObj) {
doMsg["targetName"] = targetUserObj->name();
doMsg["targetColor"] = targetUserObj->color();
}
}
// Debug-Ausgabe für "do"-Nachrichten
#ifdef YC_DEBUG
std::cout << "[Debug] Sending do message: type=" << ChatUser::dosomething << ", action=" << action << ", to=" << (targetUser.empty() ? "all" : targetUser) << std::endl;
#endif
_parent->addMessage(ChatUser::dosomething, doMsg, std::string(_name), std::string(_color));
}
else if (jsonTree["type"].asString() == "color")
{
std::string newColor = jsonTree.isMember("value") ? jsonTree["value"].asString() : "";
std::string oldColor = _color;
if (!_isValidHexColor(newColor)) {
sendMsg(ChatUser::error, "invalid_color", "", "");
return;
}
_color = newColor;
_user.set_color(newColor);
// Persistieren, falls DB-ID vorhanden
try {
if (_user.id() != 0) {
auto server = _parent->getServer();
auto db = server->_database;
std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";";
(void)db->exec(query);
}
} catch (...) {
// Ignoriere DB-Fehler still
}
// Bestätigung an User
sendMsg(ChatUser::system, "color_changed", "", newColor);
// Broadcast an andere: alte und neue Farbe mitsenden
{
Json::Value msg = Json::objectValue;
msg["tr"] = "user_color_changed";
msg["from"] = oldColor;
msg["to"] = newColor;
_parent->addMessage(ChatUser::system, msg, _name, newColor);
}
}
else if (jsonTree["type"].asString() == "join")
{
@@ -230,10 +371,90 @@ namespace Yc
changeRoom(room, password);
}
}
else if (message.substr(0, 7) == "/color ")
{
std::string newColor = message.substr(7);
std::string oldColor = _color;
if (!_isValidHexColor(newColor)) {
sendMsg(ChatUser::error, "invalid_color", "", "");
return;
}
_color = newColor;
_user.set_color(newColor);
try {
if (_user.id() != 0) {
auto server = _parent->getServer();
auto db = server->_database;
std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";";
(void)db->exec(query);
}
} catch (...) {
// ignore
}
sendMsg(ChatUser::system, "color_changed", "", newColor);
{
Json::Value msg = Json::objectValue;
msg["tr"] = "user_color_changed";
msg["from"] = oldColor;
msg["to"] = newColor;
_parent->addMessage(ChatUser::system, msg, _name, newColor);
}
}
else if (message == "/dice")
{
doDice();
}
else if (message.substr(0, 15) == "/start_dice_game")
{
// Format: /start_dice_game <runden>
if (message.length() > 16) {
std::string roundsStr = message.substr(16);
try {
int rounds = std::stoi(roundsStr);
if (rounds >= 1 && rounds <= 10) {
if (!_parent->startDiceGame(rounds, shared_from_this())) {
sendMsg(ChatUser::error, "dice_game_start_failed", "", "");
}
} else {
sendMsg(ChatUser::error, "invalid_rounds", "", "");
}
} catch (...) {
sendMsg(ChatUser::error, "invalid_rounds", "", "");
}
} else {
sendMsg(ChatUser::error, "missing_rounds", "", "");
}
}
else if (message == "/end_dice_game")
{
_parent->endDiceGame();
}
else if (message == "/reload_rooms")
{
// Raumliste neu laden
_parent->reloadRoomList();
}
else if (message.substr(0, 5) == "/roll ")
{
// Format: /roll <würfelwert>
if (message.length() > 6) {
std::string diceStr = message.substr(6);
try {
int diceValue = std::stoi(diceStr);
if (diceValue >= 1 && diceValue <= 6) {
if (!_parent->rollDice(shared_from_this(), diceValue)) {
sendMsg(ChatUser::error, "dice_roll_failed", "", "");
}
} else {
sendMsg(ChatUser::error, "invalid_dice_value", "", "");
}
} catch (...) {
sendMsg(ChatUser::error, "invalid_dice_value", "", "");
}
} else {
sendMsg(ChatUser::error, "missing_dice_value", "", "");
}
}
else
{
_parent->addMessage(ChatUser::message, std::string(message), std::string(_name), std::string(_color));
@@ -271,5 +492,10 @@ namespace Yc
}
}
void ChatUser::start() {
auto self = shared_from_this();
thread = std::thread([self]() { self->checkerTask(); });
}
} // namespace Lib
} // namespace Yc