diff --git a/lua/client/client.lua b/lua/client/client.lua index ac33aec7..65ca82e5 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -538,13 +538,20 @@ fk.client_callback["SetPlayerMark"] = function(jsonData) end fk.client_callback["Chat"] = function(jsonData) - -- jsonData: { int type, string msg } + -- jsonData: { int type, int sender, string msg } local data = json.decode(jsonData) - local p = ClientInstance:getPlayerById(data.type) + if data.type == 1 then + data.general = "" + data.time = os.date("%H:%M:%S") + ClientInstance:notifyUI("Chat", json.encode(data)) + return + end + + local p = ClientInstance:getPlayerById(data.sender) -- TODO: observer chatting if not p then for _, pl in ipairs(ClientInstance.observers) do - if pl.id == data.type then + if pl.id == data.sender then p = pl; break end end @@ -618,6 +625,10 @@ fk.client_callback["RemoveVirtualEquip"] = function(jsonData) player:removeVirtualEquip(data.id) end +fk.client_callback["Heartbeat"] = function() + ClientInstance.client:notifyServer("Heartbeat", "") +end + -- Create ClientInstance (used by Lua) ClientInstance = Client:new() dofile "lua/client/client_util.lua" diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 792d22ef..60d0db59 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -28,6 +28,8 @@ Fk:loadTranslationTable{ ["General Packages"] = "武将拓展包", ["Card Packages"] = "卡牌拓展包", + ["$OnlineInfo"] = "大厅人数:%1,总在线人数:%2", + ["Generals Overview"] = "武将一览", ["Cards Overview"] = "卡牌一览", ["Scenarios Overview"] = "玩法一览", diff --git a/image/card/equipIcon/horse.png b/packages/standard_cards/image/card/equipIcon/horse.png similarity index 100% rename from image/card/equipIcon/horse.png rename to packages/standard_cards/image/card/equipIcon/horse.png diff --git a/qml/Logic.js b/qml/Logic.js index 7b8f32ad..719b22a9 100644 --- a/qml/Logic.js +++ b/qml/Logic.js @@ -101,11 +101,20 @@ callbacks["UpdateRoomList"] = function(jsonData) { }); } +callbacks["UpdatePlayerNum"] = (j) => { + let current = mainStack.currentItem; // should be lobby + let data = JSON.parse(j); + let l = data[0]; + let s = data[1]; + current.lobbyPlayerNum = l; + current.serverPlayerNum = s; +} + callbacks["Chat"] = function(jsonData) { // jsonData: { string userName, string general, string time, string msg } let current = mainStack.currentItem; // lobby(TODO) or room let data = JSON.parse(jsonData); - let pid = data.type; + let pid = data.sender; let userName = data.userName; let general = Backend.translate(data.general); let time = data.time; diff --git a/qml/Pages/Common/ChatBox.qml b/qml/Pages/Common/ChatBox.qml index 732d3012..bfc4654d 100644 --- a/qml/Pages/Common/ChatBox.qml +++ b/qml/Pages/Common/ChatBox.qml @@ -44,7 +44,7 @@ Rectangle { ClientInstance.notifyServer( "Chat", JSON.stringify({ - type: isLobby, + type: isLobby ? 1 : 2, msg: text }) ); diff --git a/qml/Pages/Lobby.qml b/qml/Pages/Lobby.qml index 143b7489..9dbf3569 100644 --- a/qml/Pages/Lobby.qml +++ b/qml/Pages/Lobby.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Window import QtQuick.Layouts import "LobbyElement" +import "Common" import "Logic.js" as Logic Item { @@ -107,69 +108,6 @@ Item { } } } -/* - GridLayout { - flow: GridLayout.TopToBottom - rows: 4 - TileButton { - iconSource: "configure" - text: Backend.translate("Edit Profile") - onClicked: { - lobby_dialog.source = "LobbyElement/EditProfile.qml"; - lobby_drawer.open(); - } - } - TileButton { - iconSource: "create_room" - text: Backend.translate("Create Room") - onClicked: { - lobby_dialog.source = "LobbyElement/CreateRoom.qml"; - lobby_drawer.open(); - config.observing = false; - } - } - TileButton { - iconSource: "general_overview" - text: Backend.translate("Generals Overview") - onClicked: { - mainStack.push(mainWindow.generalsOverviewPage); - mainStack.currentItem.loadPackages(); - } - } - TileButton { - iconSource: "card_overview" - text: Backend.translate("Cards Overview") - onClicked: { - mainStack.push(mainWindow.cardsOverviewPage); - mainStack.currentItem.loadPackages(); - } - } - TileButton { - iconSource: "rule_summary" - text: Backend.translate("Scenarios Overview") - } - TileButton { - iconSource: "replay" - text: Backend.translate("Replay") - } - TileButton { - iconSource: "about" - text: Backend.translate("About") - onClicked: { - mainStack.push(mainWindow.aboutPage); - } - } - TileButton { - iconSource: "quit" - text: Backend.translate("Exit Lobby") - onClicked: { - toast.show("Goodbye."); - Backend.quitLobby(); - mainStack.pop(); - } - } - } -*/ } Button { @@ -257,6 +195,49 @@ Item { } } + property int lobbyPlayerNum: 0 + property int serverPlayerNum: 0 + + function updateOnlineInfo() { + } + + onLobbyPlayerNumChanged: updateOnlineInfo(); + onServerPlayerNumChanged: updateOnlineInfo(); + + Rectangle { + id: info + color: "#88EEEEEE" + width: childrenRect.width + 8 + height: childrenRect.height + 4 + anchors.bottom: parent.bottom + anchors.left: parent.left + radius: 4 + + Text { + x: 4; y: 2 + font.pixelSize: 16 + text: Backend.translate("$OnlineInfo") + .arg(lobbyPlayerNum).arg(serverPlayerNum) + "\n" + + "Powered by FreeKill " + FkVersion + } + } + + ChatBox { + id: lobbyChat + anchors.bottom: info.top + width: info.width + height: root.height * 0.6 + isLobby: true + color: "#88EEEEEE" + radius: 4 + } + + function addToChat(pid, raw, msg) { + if (raw.type !== 1) return; + lobbyChat.append(msg); + toast.show("" + raw.userName + ": " + raw.msg); + } + Component.onCompleted: { toast.show(Backend.translate("$WelcomeToLobby")); } diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index b9e59edf..51d491db 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -669,6 +669,7 @@ Item { } function addToChat(pid, raw, msg) { + if (raw.type === 1) return; chat.append(msg); let photo = Logic.getPhotoOrSelf(pid); if (photo === undefined) diff --git a/src/network/router.cpp b/src/network/router.cpp index fe72c5bc..ad5d5e7c 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -162,7 +162,7 @@ void Router::handlePacket(const QByteArray& rawPacket) lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender, const QString &jsonData){ auto arr = String2Json(jsonData).array(); auto avatar = arr[0].toString(); - static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); + static QRegularExpression nameExp("['\";#]+|(--)|(/\\*)|(\\*/)|(--\\+)"); if (!nameExp.match(avatar).hasMatch()) { auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;") .arg(avatar).arg(sender->getId()); @@ -238,6 +238,10 @@ void Router::handlePacket(const QByteArray& rawPacket) else { ServerPlayer *player = qobject_cast(parent()); + if (command == "Heartbeat") { + player->alive = true; + return; + } Room *room = player->getRoom(); if (room->isLobby() && lobby_actions.contains(command)) diff --git a/src/server/room.cpp b/src/server/room.cpp index 34868e39..8a3d353f 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -314,9 +314,11 @@ void Room::doBroadcastNotify(const QList targets, void Room::chat(ServerPlayer *sender, const QString &jsonData) { auto doc = String2Json(jsonData).object(); auto type = doc["type"].toInt(); - doc["type"] = sender->getId(); + doc["sender"] = sender->getId(); if (type == 1) { - // TODO: server chatting + doc["userName"] = sender->getScreenName(); + auto json = QJsonDocument(doc).toJson(QJsonDocument::Compact); + doBroadcastNotify(players, "Chat", json); } else { auto json = QJsonDocument(doc).toJson(QJsonDocument::Compact); doBroadcastNotify(players, "Chat", json); diff --git a/src/server/server.cpp b/src/server/server.cpp index 48067ea7..4014521b 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,4 +1,5 @@ #include "server.h" +#include "player.h" #include "server_socket.h" #include "client_socket.h" #include "room.h" @@ -7,6 +8,7 @@ #include "util.h" #include "parser.h" #include "packman.h" +#include Server *ServerInstance; @@ -33,6 +35,28 @@ Server::Server(QObject* parent) createRoom(nullptr, "Lobby", INT32_MAX); connect(lobby(), &Room::playerAdded, this, &Server::updateRoomList); connect(lobby(), &Room::playerRemoved, this, &Server::updateRoomList); + + auto heartbeatThread = QThread::create([=](){ + while (true) { + foreach (auto p, this->players.values()) { + if (p->getState() == Player::Online) { + p->alive = false; + p->doNotify("Heartbeat", ""); + } + } + + // wait for reply + QThread::sleep(5); + + foreach (auto p, this->players.values()) { + if (p->getState() == Player::Online && !p->alive) { + p->kicked(); + } + } + } + }); + heartbeatThread->setObjectName("Heartbeat"); + heartbeatThread->start(); } Server::~Server() @@ -119,6 +143,15 @@ void Server::updateRoomList() "UpdateRoomList", QString(jsonData) ); + + lobby()->doBroadcastNotify( + lobby()->getPlayers(), + "UpdatePlayerNum", + QString(JsonArray2Bytes(QJsonArray({ + lobby()->getPlayers().length(), + this->players.count(), + }))) + ); } sqlite3 *Server::getDatabase() { @@ -206,7 +239,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co { // First check the name and password // Matches a string that does not contain special characters - static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); + static QRegularExpression nameExp("['\";#]+|(--)|(/\\*)|(\\*/)|(--\\+)"); auto encryted_pw = QByteArray::fromBase64(password.toLatin1()); unsigned char buf[4096] = {0}; diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index 6e61334d..b47d8117 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -11,6 +11,9 @@ ServerPlayer::ServerPlayer(Room *room) setState(Player::Online); this->room = room; server = room->getServer(); + connect(this, &ServerPlayer::kicked, this, &ServerPlayer::kick); + + alive = true; } ServerPlayer::~ServerPlayer() @@ -112,3 +115,10 @@ void ServerPlayer::prepareForRequest(const QString& command, const QString& data requestCommand = command; requestData = data; } + +void ServerPlayer::kick() { + if (socket != nullptr) { + socket->disconnectFromHost(); + } + setSocket(nullptr); +} diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index 626c0089..89df7327 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -33,8 +33,12 @@ public: void prepareForRequest(const QString &command, const QString &data); + volatile bool alive; // For heartbeat + void kick(); + signals: void disconnected(); + void kicked(); private: ClientSocket *socket; // socket for communicating with client diff --git a/src/server/shell.cpp b/src/server/shell.cpp index 89af9a26..b8944a97 100644 --- a/src/server/shell.cpp +++ b/src/server/shell.cpp @@ -27,6 +27,7 @@ void Shell::helpCommand(QStringList &) { qInfo("%s: Enable a package.", "enable"); qInfo("%s: Disable a package.", "disable"); qInfo("%s: Upgrade a package.", "upgrade"); + qInfo("%s: Kick a player by his id.", "kick"); qInfo("For more commands, check the documentation."); } @@ -115,6 +116,24 @@ void Shell::lspkgCommand(QStringList &) { } } +void Shell::kickCommand(QStringList &list) { + if (list.isEmpty()) { + qWarning("The 'kick' command needs a player id."); + return; + } + + auto pid = list[0]; + bool ok; + int id = pid.toInt(&ok); + if (!ok) return; + + auto p = ServerInstance->findPlayer(id); + if (p) { + p->kicked(); + qInfo("Success"); + } +} + Shell::Shell() { setObjectName("Shell"); signal(SIGINT, sigintHandler); @@ -131,6 +150,7 @@ Shell::Shell() { handlers["lspkg"] = &Shell::lspkgCommand; handlers["enable"] = &Shell::enableCommand; handlers["disable"] = &Shell::disableCommand; + handlers["kick"] = &Shell::kickCommand; } handler_map = handlers; } diff --git a/src/server/shell.h b/src/server/shell.h index 229e81db..e283702c 100644 --- a/src/server/shell.h +++ b/src/server/shell.h @@ -36,6 +36,7 @@ private: void lspkgCommand(QStringList &); void enableCommand(QStringList &); void disableCommand(QStringList &); + void kickCommand(QStringList &); }; #endif