diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 07356fd4..1a008c74 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -86,6 +86,7 @@ jobs: cp -r /etc/ssl/certs . cp /usr/share/ca-certificates/mozilla/* certs/ echo ${FKVER%)} > fk_ver + ./genfkver.sh - name: Configure CMake Project working-directory: ${{github.workspace}} diff --git a/.gitignore b/.gitignore index 73076526..449381e9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /*.kdev4 /.cache/ /tags +/.luarc.json # file produced by game /FreeKill diff --git a/CMakeLists.txt b/CMakeLists.txt index c14f53b3..a55a906d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,11 +39,6 @@ include_directories(include/lua) include_directories(include) include_directories(include/libgit2) include_directories(src) -include_directories(src/client) -include_directories(src/core) -include_directories(src/network) -include_directories(src/server) -include_directories(src/ui) file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i") if (DEFINED FK_SERVER_ONLY) @@ -78,6 +73,7 @@ add_custom_command( POST_BUILD COMMENT "Generating version file fk_ver" COMMAND echo ${CMAKE_PROJECT_VERSION} > ${PROJECT_SOURCE_DIR}/fk_ver + COMMAND ${PROJECT_SOURCE_DIR}/genfkver.sh ) add_subdirectory(src) diff --git a/Fk/Config.qml b/Fk/Config.qml index fa47a54b..452a0e09 100644 --- a/Fk/Config.qml +++ b/Fk/Config.qml @@ -43,6 +43,8 @@ QtObject { property string password: "" property string cipherText property string aeskey + // string => { roomId => config } + property var roomConfigCache: ({}) // Client data property string serverMotd: "" diff --git a/Fk/Logic.js b/Fk/Logic.js index 97991935..b098e7ef 100644 --- a/Fk/Logic.js +++ b/Fk/Logic.js @@ -81,6 +81,25 @@ callbacks["ErrorMsg"] = (jsonData) => { } } +callbacks["ErrorDlg"] = (jsonData) => { + let log; + try { + const a = JSON.parse(jsonData); + log = qsTr(a[0]).arg(a[1]); + } catch (e) { + log = qsTr(jsonData); + } + + console.log("ERROR: " + log); + Backend.showDialog("warning", log, jsonData); + mainWindow.busy = false; + if (sheduled_download !== "") { + mainWindow.busy = true; + Pacman.loadSummary(JSON.stringify(sheduled_download), true); + sheduled_download = ""; + } +} + callbacks["UpdatePackage"] = (jsonData) => sheduled_download = jsonData; callbacks["UpdateBusyText"] = (jsonData) => { diff --git a/Fk/Pages/Init.qml b/Fk/Pages/Init.qml index ea636a9e..44d58183 100644 --- a/Fk/Pages/Init.qml +++ b/Fk/Pages/Init.qml @@ -183,17 +183,6 @@ Item { } } - // Temp - Button { - text: qsTr("Making Mod") - anchors.right: parent.right - anchors.bottom: parent.bottom - visible: Debugging - onClicked: { - mainStack.push(modMaker); - } - } - function downloadComplete() { toast.show(qsTr("updated packages for md5")); } diff --git a/Fk/Pages/Lobby.qml b/Fk/Pages/Lobby.qml index f376fd2d..4931978c 100644 --- a/Fk/Pages/Lobby.qml +++ b/Fk/Pages/Lobby.qml @@ -11,112 +11,151 @@ import "Logic.js" as Logic Item { id: root property alias roomModel: roomModel + property var roomInfoCache: ({}) property string password - Rectangle { - width: parent.width / 2 - roomListLayout.width / 2 - 50 - height: parent.height * 0.7 - anchors.top: exitButton.bottom - anchors.bottom: createRoomButton.top - anchors.right: parent.right - anchors.rightMargin: 20 - color: "#88EEEEEE" - radius: 6 + Component { + id: roomDelegate - Flickable { - id: flickableContainer - ScrollBar.vertical: ScrollBar {} - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 10 - flickableDirection: Flickable.VerticalFlick - width: parent.width - 10 - height: parent.height - 10 - contentHeight: bulletin_info.height - clip: true + Rectangle { + radius: 8 + height: 124 - 8 + width: 124 - 8 + color: outdated ? "#E2E2E2" : "lightgreen" Text { - id: bulletin_info - width: parent.width - wrapMode: TextEdit.WordWrap - textFormat: Text.MarkdownText - text: config.serverMotd + "\n___\n" + luatr('Bulletin Info') - onLinkActivated: Qt.openUrlExternally(link); + id: roomNameText + horizontalAlignment: Text.AlignLeft + width: parent.width - 16 + height: contentHeight + maximumLineCount: 2 + wrapMode: Text.WrapAnywhere + textFormat: Text.PlainText + text: roomName + // color: outdated ? "gray" : "black" + font.pixelSize: 16 + // elide: Label.ElideRight + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 8 + } + + Text { + id: roomIdText + text: luatr(gameMode) + ' #' + roomId + anchors.top: roomNameText.bottom + anchors.left: roomNameText.left + } + + Image { + source: AppPath + "/image/button/skill/locked.png" + visible: hasPassword + scale: 0.8 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: -4 + } + + Text { + color: (playerNum == capacity) ? "red" : "black" + text: playerNum + "/" + capacity + font.pixelSize: 18 + font.bold: true + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + } + + TapHandler { + gesturePolicy: TapHandler.WithinBounds + enabled: !opTimer.running && !outdated + + onTapped: { + lobby_dialog.sourceComponent = roomDetailDialog; + lobby_dialog.item.roomData = { + roomId, roomName, gameMode, playerNum, capacity, + hasPassword, outdated, + }; + lobby_dialog.item.roomConfig = config.roomConfigCache?.[config.serverAddr]?.[roomId] + lobby_drawer.open(); + } } } } Component { - id: roomDelegate + id: roomDetailDialog + ColumnLayout { + property var roomData: ({ + roomName: "", + hasPassword: true, + }) + property var roomConfig: undefined + signal finished() + anchors.fill: parent + anchors.margins: 16 - Item { - height: 48 - width: roomList.width + Text { + text: roomData.roomName + font.pixelSize: 18 + } + + Text { + font.pixelSize: 18 + text: { + let ret = luatr(roomData.gameMode); + ret += (' #' + roomData.roomId); + ret += (' ' + roomData.playerNum + '/' + roomData.capacity); + return ret; + } + } + + Item { Layout.fillHeight: true } + + // Dummy + Text { + text: "在未来的版本中这一块区域将增加更多实用的功能,
"+ + "例如直接查看房间的各种配置信息
"+ + "以及更多与禁将有关的实用功能!"+ + "注:绿色按钮为试做型UI 后面优化" + font.pixelSize: 18 + } RowLayout { - anchors.fill: parent - spacing: 16 + Layout.fillWidth: true Text { - text: roomId - color: "grey" + visible: roomData.hasPassword + text: luatr("Please input room's password") } - Text { - horizontalAlignment: Text.AlignLeft + TextField { + id: passwordEdit + visible: roomData.hasPassword Layout.fillWidth: true - text: { - let ret = roomName; - if (outdated) { - ret = '' + ret + ''; - } - return ret; - } - font.pixelSize: 20 - elide: Label.ElideRight + onTextChanged: root.password = text; } Item { - Layout.preferredWidth: 16 - Image { - source: AppPath + "/image/button/skill/locked.png" - visible: hasPassword - anchors.centerIn: parent - scale: 0.8 - } - } - - Text { - text: luatr(gameMode) - } - - Text { - color: (playerNum == capacity) ? "red" : "black" - text: playerNum + "/" + capacity - font.pixelSize: 20 - font.bold: true + visible: !roomData.hasPassword + Layout.fillWidth: true } Button { - text: (playerNum < capacity) ? luatr("Enter") : - luatr("Observe") - - enabled: !opTimer.running && !outdated - + // text: "OK" + text: (roomData.playerNum < roomData.capacity) ? luatr("Enter") : luatr("Observe") onClicked: { - opTimer.start(); - if (hasPassword) { - lobby_dialog.sourceComponent = enterPassword; - lobby_dialog.item.roomId = roomId; - lobby_dialog.item.playerNum = playerNum; - lobby_dialog.item.capacity = capacity; - lobby_drawer.open(); - } else { - enterRoom(roomId, playerNum, capacity, ""); - } + enterRoom(roomData.roomId, roomData.playerNum, roomData.capacity, + roomData.hasPassword ? root.password : ""); + lobby_dialog.item.finished(); } } } + + Component.onCompleted: { + passwordEdit.text = ""; + } } } @@ -124,8 +163,7 @@ Item { id: roomModel } - PersonalSettings { - } + PersonalSettings {} Timer { id: opTimer @@ -134,69 +172,132 @@ Item { ColumnLayout { id: roomListLayout - anchors.top: parent.top - anchors.topMargin: 10 - anchors.horizontalCenter: parent.horizontalCenter - width: root.width * 0.48 - height: root.height - 80 - Button { - Layout.alignment: Qt.AlignRight - text: luatr("Refresh Room List") - enabled: !opTimer.running - onClicked: { - opTimer.start(); - ClientInstance.notifyServer("RefreshRoomList", ""); + height: root.height - 72 + y: 16 + anchors.left: parent.left + anchors.leftMargin: root.width * 0.03 + root.width * 0.94 * 0.8 % 128 / 2 + width: { + let ret = root.width * 0.94 * 0.8; + ret -= ret % 128; + return ret; + } + clip: true + + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + Layout.alignment: Qt.AlignRight + text: luatr("Refresh Room List").arg(roomModel.count) + enabled: !opTimer.running + onClicked: { + opTimer.start(); + ClientInstance.notifyServer("RefreshRoomList", ""); + } + } + Button { + text: luatr("Create Room") + onClicked: { + lobby_dialog.sourceComponent = + Qt.createComponent("../LobbyElement/CreateRoom.qml"); + lobby_drawer.open(); + config.observing = false; + config.replaying = false; + } } } - Item { - Layout.fillWidth: true + + GridView { + id: roomList + cellWidth: 128 + cellHeight: 128 Layout.fillHeight: true - Rectangle { - anchors.fill: parent - anchors.centerIn: parent - color: "#88EEEEEE" - radius: 16 - Text { - width: parent.width - horizontalAlignment: Text.AlignHCenter - text: luatr("Room List").arg(roomModel.count) - } - ListView { - id: roomList - height: parent.height * 0.9 - width: parent.width * 0.95 - contentHeight: roomDelegate.height * count - ScrollBar.vertical: ScrollBar {} - anchors.centerIn: parent - delegate: roomDelegate - clip: true - model: roomModel - } - } + Layout.fillWidth: true + ScrollBar.vertical: ScrollBar {} + delegate: roomDelegate + clip: true + model: roomModel } } - Button { - id: createRoomButton - anchors.bottom: buttonRow.top + Rectangle { + id: serverInfoLayout + height: root.height - 112 + y: 56 + width: root.width * 0.94 * 0.2 anchors.right: parent.right - width: 120 - display: AbstractButton.TextUnderIcon - icon.name: "media-playback-start" - text: luatr("Create Room") - onClicked: { - lobby_dialog.sourceComponent = - Qt.createComponent("../LobbyElement/CreateRoom.qml"); - lobby_drawer.open(); - config.observing = false; - config.replaying = false; + anchors.rightMargin: root.width * 0.03 + // anchors.horizontalCenter: parent.horizontalCenter + color: "#88EEEEEE" + property bool chatShown: true + + Flickable { + ScrollBar.vertical: ScrollBar {} + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 10 + flickableDirection: Flickable.VerticalFlick + width: parent.width - 10 + height: parent.height - 10 - (parent.chatShown ? 200 : 0) + contentHeight: bulletin_info.height + clip: true + + Text { + id: bulletin_info + width: parent.width + wrapMode: TextEdit.WordWrap + textFormat: Text.MarkdownText + text: config.serverMotd + "\n\n___\n\n" + luatr('Bulletin Info') + onLinkActivated: Qt.openUrlExternally(link); + } + } + + MetroButton { + text: "🗨️" + (parent.chatShown ? "➖" : "➕") + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: lobbyChat.top + onClicked: { + parent.chatShown = !parent.chatShown + } + } + + ChatBox { + id: lobbyChat + width: parent.width + height: parent.chatShown ? 200 : 0 + Behavior on height { NumberAnimation { duration: 200 } } + anchors.bottom: parent.bottom + isLobby: true + color: "#88EEEEEE" + clip: true } } RowLayout { id: buttonRow - anchors.right: parent.right + anchors.left: parent.left anchors.bottom: parent.bottom + width: parent.width + + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: childrenRect.width + 48 + + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.8; color: "white" } + GradientStop { position: 1.0; color: "transparent" } + } + Text { + x: 16; y: 4 + font.pixelSize: 16 + text: luatr("$OnlineInfo") + .arg(lobbyPlayerNum).arg(serverPlayerNum) + "\n" + + "Powered by FreeKill " + FkVersion + } + } + + Item { Layout.fillWidth: true } Button { text: luatr("Generals Overview") onClicked: { @@ -276,39 +377,6 @@ Item { } } - Component { - id: enterPassword - ColumnLayout { - property int roomId - property int playerNum - property int capacity - signal finished() - anchors.fill: parent - anchors.margins: 16 - - Text { - text: luatr("Please input room's password") - } - - TextField { - id: passwordEdit - onTextChanged: root.password = text; - } - - Button { - text: "OK" - onClicked: { - enterRoom(roomId, playerNum, capacity, root.password); - parent.finished(); - } - } - - Component.onCompleted: { - passwordEdit.text = ""; - } - } - } - function enterRoom(roomId, playerNum, capacity, pw) { config.replaying = false; if (playerNum < capacity) { @@ -333,40 +401,15 @@ Item { property int lobbyPlayerNum: 0 property int serverPlayerNum: 0 + /* function updateOnlineInfo() { } onLobbyPlayerNumChanged: updateOnlineInfo(); onServerPlayerNumChanged: updateOnlineInfo(); - Rectangle { - id: info - color: "#88EEEEEE" - width: root.width * 0.23 // childrenRect.width + 8 - height: childrenRect.height + 4 - anchors.bottom: parent.bottom - anchors.left: parent.left - radius: 4 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - x: 4; y: 2 - font.pixelSize: 16 - text: luatr("$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 - } + /* + */ Danmaku { id: danmaku diff --git a/Fk/main.qml b/Fk/main.qml index 7e454912..469fea57 100644 --- a/Fk/main.qml +++ b/Fk/main.qml @@ -53,7 +53,6 @@ Window { Component { id: init; Init {} } Component { id: packageManage; PackageManage {} } - Component { id: modMaker; ModMaker {} } Component { id: lobby; Lobby {} } Component { id: generalsOverview; GeneralsOverview {} } Component { id: cardsOverview; CardsOverview {} } diff --git a/genfkver.sh b/genfkver.sh new file mode 100755 index 00000000..e7809bab --- /dev/null +++ b/genfkver.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# 为fk_ver文件追加编译时相关文件列表 +# 类似其他项目中flist.txt的功能 + +cd $(dirname $0) +sed -i '2,$d' ./fk_ver + +fn() { + for f in $(ls -1 $1); do + if [ -d $1/$f ]; then + fn $1/$f + else + echo $1/$f >> ./fk_ver + fi + done +} + +fn lua +fn Fk +cd - diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts index f7c58d6d..d033ea87 100644 --- a/lang/zh_CN.ts +++ b/lang/zh_CN.ts @@ -113,6 +113,38 @@ + + QmlBackend + + FreeKill + 新月杀 + + + help: others logged in again with this name + 提示:请检查密码是否泄漏 + + + help: unknown password error + 提示:请尝试重新启动程序 + + + help: you have been banned! + 提示:此为永久封禁,请联系管理员说明 + + + help: you have been temporarily banned! + 提示:此为暂时封禁,一般在约二十分钟后自动解禁 + + + help: user name not in whitelist + 提示:请联系服主解决 + + + help: username or password error + 提示:可能该用户名已被占用,或者密码错误,如果你是初次注册的话考虑用另一个用户名密码进行登入 + + + Init @@ -292,7 +324,15 @@ others logged in again with this name - 其他人用你的用户名和密码登陆到了服务器,请检查密码是否泄漏 + 其他人用你的用户名和密码登陆到了服务器 + + + unknown password error + 服务端解密密码时出现未知错误 + + + user name not in whitelist + 你不在该服务器的白名单中! invalid user name diff --git a/sgs b/sgs new file mode 100644 index 00000000..804335a2 --- /dev/null +++ b/sgs @@ -0,0 +1,10 @@ +{ + "banwords": [ "习近", "近平", "共产党", "介石", "刘少奇", "邓小平", "江泽民", "胡锦涛", "毛泽东" ], + "description": "新月杀 [0.4.15] 主力联机服务器!请素质交流、理性对局!交流请去贴吧[新月杀]吧", + "iconUrl": "http://175.178.66.93/ba-freekill.png", + "capacity": 800, + "tempBanTime": 15, + "motd": "6.5更新\n\n手杀测试服:司马孚、成济、SP毌丘俭、李昭焦伯;十周年(一将24获奖版初稿):宣公主、徐琨、令狐愚、司马孚\n\n6.3~6.4更新\n\nOL:界法正、蒋琬(注:暂不实现禁用手牌排序,且点击“牌序”按钮并不影响真实顺序,如不小心点击则通过点击武将上的“自若”标记查看真实顺序);十周年:韩嵩、马铁;线下:周姬、鄂焕\n\n5.31~6.1更新\n\n十周年:乐诸葛果、小孙权、乐邹氏、乐祢衡、谋张绣;\n\n\n\n请为新月杀的Github仓库点一个star吧!感谢! https://github.com/Notify-ctrl/FreeKill\n\n## 点此查看游玩教程: https://fkbook-all-in-one.readthedocs.io", + "hiddenPacks": [], + "enableBots": false +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5f83432e..2d6473b8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,10 +8,14 @@ set(freekill_SRCS "network/server_socket.cpp" "network/client_socket.cpp" "network/router.cpp" + "server/auth.cpp" "server/server.cpp" "server/serverplayer.cpp" + "server/roombase.cpp" + "server/lobby.cpp" "server/room.cpp" "server/roomthread.cpp" + "server/scheduler.cpp" "ui/qmlbackend.cpp" "swig/freekill-wrap.cxx" ) @@ -21,7 +25,7 @@ if (NOT DEFINED FK_SERVER_ONLY) "client/client.cpp" "client/clientplayer.cpp" "client/replayer.cpp" - "ui/mod.cpp" + # "ui/mod.cpp" ) endif () diff --git a/src/client/client.cpp b/src/client/client.cpp index a9578520..8b336abc 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "client.h" -#include "client_socket.h" -#include "clientplayer.h" -#include "qmlbackend.h" -#include "util.h" -#include "server.h" -#include -#include +#include "client/client.h" +#include "client/clientplayer.h" +#include "ui/qmlbackend.h" +#include "core/util.h" +#include "server/server.h" +#include "network/client_socket.h" Client *ClientInstance = nullptr; ClientPlayer *Self = nullptr; diff --git a/src/client/client.h b/src/client/client.h index f5d0d7ab..db362d19 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -3,12 +3,11 @@ #ifndef _CLIENT_H #define _CLIENT_H -#include "router.h" -#include "clientplayer.h" -#include +#include "network/router.h" +#include "client/clientplayer.h" #ifndef FK_SERVER_ONLY -#include "qmlbackend.h" +#include "ui/qmlbackend.h" #endif class Client : public QObject { diff --git a/src/client/clientplayer.cpp b/src/client/clientplayer.cpp index 462a1143..a1ca578f 100644 --- a/src/client/clientplayer.cpp +++ b/src/client/clientplayer.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "clientplayer.h" +#include "client/clientplayer.h" ClientPlayer::ClientPlayer(int id, QObject *parent) : Player(parent) { setId(id); diff --git a/src/client/clientplayer.h b/src/client/clientplayer.h index 31d5bea1..47a96f97 100644 --- a/src/client/clientplayer.h +++ b/src/client/clientplayer.h @@ -3,7 +3,7 @@ #ifndef _CLIENTPLAYER_H #define _CLIENTPLAYER_H -#include "player.h" +#include "core/player.h" class ClientPlayer : public Player { Q_OBJECT diff --git a/src/client/replayer.cpp b/src/client/replayer.cpp index d31800f3..a30b0c52 100644 --- a/src/client/replayer.cpp +++ b/src/client/replayer.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "replayer.h" -#include "client.h" -#include "qmlbackend.h" -#include "util.h" +#include "client/replayer.h" +#include "client/client.h" +#include "ui/qmlbackend.h" +#include "core/util.h" Replayer::Replayer(QObject *parent, const QString &filename) : QThread(parent), fileName(filename), roomSettings(""), origPlayerInfo(""), diff --git a/src/core/packman.cpp b/src/core/packman.cpp index 520a0378..cf30461d 100644 --- a/src/core/packman.cpp +++ b/src/core/packman.cpp @@ -1,10 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "packman.h" +#include "core/packman.h" #include "git2.h" -#include "util.h" -#include "qmlbackend.h" -#include +#include "core/util.h" +#include "ui/qmlbackend.h" PackMan *Pacman; @@ -70,13 +69,16 @@ void PackMan::loadSummary(const QString &jsonData, bool useThread) { auto obj = e.toObject(); auto name = obj["name"].toString(); auto url = obj["url"].toString(); -#ifndef FK_SERVER_ONLY - Backend->showToast(tr("[%1/%2] upgrading package '%3'").arg(i).arg(arr.count()).arg(name)); -#endif + bool toast_showed = false; if (SelectFromDatabase( db, QString("SELECT name FROM packages WHERE name='%1';").arg(name)) .isEmpty()) { +#ifndef FK_SERVER_ONLY + Backend->showToast(tr("[%1/%2] upgrading package '%3'") + .arg(i).arg(arr.count()).arg(name)); + toast_showed = true; +#endif downloadNewPack(url); } ExecSQL(db, QString("UPDATE packages SET hash='%1' WHERE name='%2'") @@ -85,6 +87,11 @@ void PackMan::loadSummary(const QString &jsonData, bool useThread) { enablePack(name); if (head(name) != obj["hash"].toString()) { +#ifndef FK_SERVER_ONLY + if (!toast_showed) + Backend->showToast(tr("[%1/%2] upgrading package '%3'") + .arg(i).arg(arr.count()).arg(name)); +#endif updatePack(name); } } @@ -171,7 +178,7 @@ void PackMan::updatePack(const QString &pack) { if (error != 0) { #ifndef FK_SERVER_ONLY if (Backend != nullptr) { - Backend->showToast(tr("packages/%1: some error occured.").arg(pack)); + Backend->dialog("critical", tr("packages/%1: some error occured.").arg(pack)); } #endif return; @@ -193,7 +200,7 @@ void PackMan::upgradePack(const QString &pack) { if (error != 0) { #ifndef FK_SERVER_ONLY if (Backend != nullptr) { - Backend->showToast(tr("packages/%1: some error occured.").arg(pack)); + Backend->showDialog("critical", tr("packages/%1: some error occured.").arg(pack)); } #endif return; diff --git a/src/core/packman.h b/src/core/packman.h index b1dd4b90..3269dcb3 100644 --- a/src/core/packman.h +++ b/src/core/packman.h @@ -3,8 +3,6 @@ #ifndef _PACKMAN_H #define _PACKMAN_H -#include - // 管理拓展包所需的类,本质上是libgit2接口的再封装。 class PackMan : public QObject { Q_OBJECT diff --git a/src/core/player.cpp b/src/core/player.cpp index 65685b4f..3f1f7cb1 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "player.h" +#include "core/player.h" Player::Player(QObject *parent) : QObject(parent), id(0), state(Player::Invalid), totalGameTime(0), ready(false), diff --git a/src/core/util.cpp b/src/core/util.cpp index c14c3293..6c517955 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -1,10 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "util.h" -#include "packman.h" -#include -#include -#include +#include "core/util.h" +#include "core/packman.h" #include extern "C" { @@ -172,6 +169,17 @@ static void writeDirMD5(QFile &dest, const QString &dir, } } +static void writeFkVerMD5(QFile &dest) { + QFile flist("fk_ver"); + if (flist.exists() && flist.open(QIODevice::ReadOnly)) { + while (true) { + QByteArray bytes = flist.readLine().simplified(); + if (bytes.isNull()) break; + writeFileMD5(dest, bytes); + } + } +} + QString calcFileMD5() { // First, generate flist.txt // flist.txt is a file contains all md5sum for code files @@ -180,12 +188,13 @@ QString calcFileMD5() { qFatal("Cannot open flist.txt. Quitting."); } + writeFkVerMD5(flist); writeDirMD5(flist, "packages", "*.lua"); writeDirMD5(flist, "packages", "*.qml"); writeDirMD5(flist, "packages", "*.js"); - writeDirMD5(flist, "lua", "*.lua"); - writeDirMD5(flist, "Fk", "*.qml"); - writeDirMD5(flist, "Fk", "*.js"); + // writeDirMD5(flist, "lua", "*.lua"); + // writeDirMD5(flist, "Fk", "*.qml"); + // writeDirMD5(flist, "Fk", "*.js"); // then, return flist.txt's md5 flist.close(); diff --git a/src/main.cpp b/src/main.cpp index 6ea696ce..aa8be159 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "client.h" -#include "util.h" +#include "client/client.h" +#include "core/util.h" using namespace fkShell; -#include "packman.h" -#include "server.h" +#include "core/packman.h" +#include "server/server.h" #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) -#include "shell.h" +#include "server/shell.h" #endif #if defined(Q_OS_WIN32) @@ -22,7 +22,7 @@ using namespace fkShell; #ifndef Q_OS_ANDROID #include #endif -#include "qmlbackend.h" +#include "ui/qmlbackend.h" #endif #if defined(Q_OS_ANDROID) @@ -113,10 +113,10 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, break; } - fprintf(stderr, "\r%02d/%02d ", date.month(), date.day()); + fprintf(stderr, "%02d/%02d ", date.month(), date.day()); fprintf(stderr, "%s ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); - fprintf(file, "\r%02d/%02d ", date.month(), date.day()); + fprintf(file, "%02d/%02d ", date.month(), date.day()); fprintf(file, "%s ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); @@ -150,8 +150,7 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, "C", localMsg.constData()); #ifndef FK_SERVER_ONLY if (Backend != nullptr) { - Backend->notifyUI( - "ErrorDialog", + Backend->notifyUI("ErrorDialog", QString("⛔ %1/Error occured!\n %2").arg(threadName).arg(localMsg)); } #endif @@ -329,6 +328,7 @@ int main(int argc, char *argv[]) { #if defined(Q_OS_ANDROID) system = "Android"; #elif defined(Q_OS_WIN32) + qputenv("QT_MEDIA_BACKEND", "windows"); system = "Win"; ::system("chcp 65001"); #elif defined(Q_OS_LINUX) diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index de033e4b..180fc31a 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "client_socket.h" +#include "network/client_socket.h" #include -#include -#include ClientSocket::ClientSocket() : socket(new QTcpSocket(this)) { aes_ready = false; @@ -35,7 +33,7 @@ void ClientSocket::connectToHost(const QString &address, ushort port) { void ClientSocket::getMessage() { while (socket->canReadLine()) { auto msg = socket->readLine(); - msg = aesDecrypt(msg); + msg = aesDec(msg); if (msg.startsWith("Compressed")) { msg = msg.sliced(10); msg = qUncompress(QByteArray::fromBase64(msg)); @@ -54,9 +52,9 @@ void ClientSocket::send(const QByteArray &msg) { if (msg.length() >= 1024) { auto comp = qCompress(msg); _msg = "Compressed" + comp.toBase64(); - _msg = aesEncrypt(_msg) + "\n"; + _msg = aesEnc(_msg) + "\n"; } else { - _msg = aesEncrypt(msg) + "\n"; + _msg = aesEnc(msg) + "\n"; } socket->write(_msg); @@ -156,7 +154,7 @@ void ClientSocket::installAESKey(const QByteArray &key) { aes_ready = true; } -QByteArray ClientSocket::aesEncrypt(const QByteArray &in) { +QByteArray ClientSocket::aesEnc(const QByteArray &in) { if (!aes_ready) { return in; } @@ -182,7 +180,7 @@ QByteArray ClientSocket::aesEncrypt(const QByteArray &in) { return iv + out.toBase64(); } -QByteArray ClientSocket::aesDecrypt(const QByteArray &in) { +QByteArray ClientSocket::aesDec(const QByteArray &in) { if (!aes_ready) { return in; } diff --git a/src/network/client_socket.h b/src/network/client_socket.h index 12dfc202..26ccd6ec 100644 --- a/src/network/client_socket.h +++ b/src/network/client_socket.h @@ -33,8 +33,8 @@ private slots: void raiseError(QAbstractSocket::SocketError error); private: - QByteArray aesEncrypt(const QByteArray &in); - QByteArray aesDecrypt(const QByteArray &out); + QByteArray aesEnc(const QByteArray &in); + QByteArray aesDec(const QByteArray &out); AES_KEY aes_key; bool aes_ready; QTcpSocket *socket; diff --git a/src/network/router.cpp b/src/network/router.cpp index fcbf71a5..2475ba60 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -1,13 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "router.h" -#include "client.h" -#include "client_socket.h" -#include "roomthread.h" -#include -#include "server.h" -#include "serverplayer.h" -#include "util.h" +#include "network/router.h" +#include "client/client.h" +#include "network/client_socket.h" +#include "server/roomthread.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "core/util.h" Router::Router(QObject *parent, ClientSocket *socket, RouterType type) : QObject(parent) { @@ -160,7 +159,7 @@ void Router::handlePacket(const QByteArray &rawPacket) { return; } - Room *room = player->getRoom(); + auto room = player->getRoom(); room->handlePacket(player, command, jsonData); } } else if (type & TYPE_REQUEST) { @@ -180,10 +179,13 @@ void Router::handlePacket(const QByteArray &rawPacket) { ServerPlayer *player = qobject_cast(parent()); player->setThinking(false); - // qDebug() << "wake up!"; - auto room = player->getRoom(); - if (room->getThread()) { - room->getThread()->wakeUp(); + auto _room = player->getRoom(); + if (!_room->isLobby()) { + auto room = qobject_cast(_room); + if (room->getThread()) { + room->getThread()->wakeUp(room->getId()); + // TODO: signal + } } if (requestId != this->expectedReplyId) diff --git a/src/network/server_socket.cpp b/src/network/server_socket.cpp index d6e7e722..989ab90e 100644 --- a/src/network/server_socket.cpp +++ b/src/network/server_socket.cpp @@ -1,15 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "server_socket.h" -#include "client_socket.h" +#include "network/server_socket.h" +#include "network/client_socket.h" +#include "server/server.h" +#include "core/util.h" -ServerSocket::ServerSocket() { +ServerSocket::ServerSocket(QObject *parent) : QObject(parent) { server = new QTcpServer(this); connect(server, &QTcpServer::newConnection, this, &ServerSocket::processNewConnection); + + udpSocket = new QUdpSocket(this); + connect(udpSocket, &QUdpSocket::readyRead, + this, &ServerSocket::readPendingDatagrams); } bool ServerSocket::listen(const QHostAddress &address, ushort port) { + udpSocket->bind(port); return server->listen(address, port); } @@ -20,3 +27,29 @@ void ServerSocket::processNewConnection() { [connection]() { connection->deleteLater(); }); emit new_connection(connection); } + +void ServerSocket::readPendingDatagrams() { + while (udpSocket->hasPendingDatagrams()) { + QNetworkDatagram datagram = udpSocket->receiveDatagram(); + if (datagram.isValid()) { + processDatagram(datagram.data(), datagram.senderAddress(), datagram.senderPort()); + } + } +} + +void ServerSocket::processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port) { + auto server = qobject_cast(parent()); + if (msg == "fkDetectServer") { + udpSocket->writeDatagram("me", addr, port); + } else if (msg.startsWith("fkGetDetail,")) { + udpSocket->writeDatagram(JsonArray2Bytes(QJsonArray({ + FK_VERSION, + server->getConfig("iconUrl"), + server->getConfig("description"), + server->getConfig("capacity"), + server->getPlayers().count(), + msg.sliced(12).constData(), + })), addr, port); + } + udpSocket->flush(); +} diff --git a/src/network/server_socket.h b/src/network/server_socket.h index 7cbd2fa2..543b017b 100644 --- a/src/network/server_socket.h +++ b/src/network/server_socket.h @@ -10,7 +10,7 @@ class ServerSocket : public QObject { Q_OBJECT public: - ServerSocket(); + ServerSocket(QObject *parent = nullptr); bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); @@ -20,9 +20,12 @@ signals: private slots: // 新建一个ClientSocket,然后立刻交给Server相关函数处理。 void processNewConnection(); + void readPendingDatagrams(); private: QTcpServer *server; + QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用 + void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port); }; #endif // _SERVER_SOCKET_H diff --git a/src/server/auth.cpp b/src/server/auth.cpp new file mode 100644 index 00000000..9fc70ff1 --- /dev/null +++ b/src/server/auth.cpp @@ -0,0 +1,195 @@ +#include "server/auth.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "core/util.h" +#include "network/client_socket.h" + +AuthManager::AuthManager(QObject *parent) : QObject(parent) { + rsa = initRSA(); + + QFile file("server/rsa_pub"); + file.open(QIODevice::ReadOnly); + QTextStream in(&file); + public_key = in.readAll(); +} + +AuthManager::~AuthManager() noexcept { + RSA_free(rsa); +} + +RSA *AuthManager::initRSA() { + RSA *rsa = RSA_new(); + if (!QFile::exists("server/rsa_pub")) { + BIGNUM *bne = BN_new(); + BN_set_word(bne, RSA_F4); + RSA_generate_key_ex(rsa, 2048, bne, NULL); + + BIO *bp_pub = BIO_new_file("server/rsa_pub", "w+"); + PEM_write_bio_RSAPublicKey(bp_pub, rsa); + BIO *bp_pri = BIO_new_file("server/rsa", "w+"); + PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL); + + BIO_free_all(bp_pub); + BIO_free_all(bp_pri); + QFile("server/rsa") + .setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + BN_free(bne); + } + FILE *keyFile = fopen("server/rsa_pub", "r"); + PEM_read_RSAPublicKey(keyFile, &rsa, NULL, NULL); + fclose(keyFile); + keyFile = fopen("server/rsa", "r"); + PEM_read_RSAPrivateKey(keyFile, &rsa, NULL, NULL); + fclose(keyFile); + return rsa; +} + +bool AuthManager::checkClientVersion(ClientSocket *client, const QString &cver) { + auto server = qobject_cast(parent()); + auto client_ver = QVersionNumber::fromString(cver); + auto ver = QVersionNumber::fromString(FK_VERSION); + int cmp = QVersionNumber::compare(ver, client_ver); + if (cmp != 0) { + auto errmsg = QString(); + if (cmp < 0) { + errmsg = QString("[\"server is still on version %%2\",\"%1\"]") + .arg(FK_VERSION, "1"); + } else { + errmsg = QString("[\"server is using version %%2, please update\",\"%1\"]") + .arg(FK_VERSION, "1"); + } + + server->sendEarlyPacket(client, "ErrorDlg", errmsg); + client->disconnectFromHost(); + return false; + } + return true; +} + +QJsonObject AuthManager::queryUserInfo(ClientSocket *client, const QString &name, + const QByteArray &password) { + auto server = qobject_cast(parent()); + auto db = server->getDatabase(); + auto pw = password; + + auto sql_find = QString("SELECT * FROM userinfo WHERE name='%1';") + .arg(name); + + auto result = SelectFromDatabase(db, sql_find); + if (result.isEmpty()) { + auto salt_gen = QRandomGenerator::securelySeeded(); + auto salt = QByteArray::number(salt_gen(), 16); + pw.append(salt); + auto passwordHash = + QCryptographicHash::hash(pw, QCryptographicHash::Sha256).toHex(); + + auto sql_reg = QString("INSERT INTO userinfo (name,password,salt,\ + avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4','%5',%6);") + .arg(name).arg(QString(passwordHash)) + .arg(salt).arg("liubei").arg(client->peerAddress()) + .arg("FALSE"); + + ExecSQL(db, sql_reg); + result = SelectFromDatabase(db, sql_find); // refresh result + auto obj = result[0].toObject(); + + auto info_update = QString("INSERT INTO usergameinfo (id, registerTime) VALUES (%1, %2);").arg(obj["id"].toString().toInt()).arg(QDateTime::currentSecsSinceEpoch()); + ExecSQL(db, info_update); + } + + return result[0].toObject(); +} + +QJsonObject AuthManager::checkPassword(ClientSocket *client, const QString &name, + const QString &password) { + + auto server = qobject_cast(parent()); + bool passed = false; + QString error_msg; + QJsonObject obj; + int id; + QByteArray salt; + QByteArray passwordHash; + auto players = server->getPlayers(); + + auto encryted_pw = QByteArray::fromBase64(password.toLatin1()); + unsigned char buf[4096] = {0}; + RSA_private_decrypt(RSA_size(rsa), (const unsigned char *)encryted_pw.data(), + buf, rsa, RSA_PKCS1_PADDING); + auto decrypted_pw = + QByteArray::fromRawData((const char *)buf, strlen((const char *)buf)); + + if (decrypted_pw.length() > 32) { + auto aes_bytes = decrypted_pw.first(32); + + // tell client to install aes key + server->sendEarlyPacket(client, "InstallKey", ""); + client->installAESKey(aes_bytes); + decrypted_pw.remove(0, 32); + } else { + // FIXME + // decrypted_pw = "\xFF"; + error_msg = "unknown password error"; + goto FAIL; + } + + if (!CheckSqlString(name) || !server->checkBanWord(name)) { + error_msg = "invalid user name"; + goto FAIL; + } + + if (server->getConfig("whitelist").isArray() && + !server->getConfig("whitelist").toArray().toVariantList().contains(name)) { + error_msg = "user name not in whitelist"; + goto FAIL; + } + + obj = queryUserInfo(client, name, decrypted_pw); + + // check ban account + id = obj["id"].toString().toInt(); + passed = obj["banned"].toString().toInt() == 0; + if (!passed) { + error_msg = "you have been banned!"; + goto FAIL; + } + + // check if password is the same + salt = obj["salt"].toString().toLatin1(); + decrypted_pw.append(salt); + passwordHash = + QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256).toHex(); + passed = (passwordHash == obj["password"].toString()); + if (!passed) { + error_msg = "username or password error"; + goto FAIL; + } + + if (players.value(id)) { + auto player = players.value(id); + // 顶号机制,如果在线的话就让他变成不在线 + if (player->getState() == Player::Online) { + player->doNotify("ErrorDlg", "others logged in again with this name"); + emit player->kicked(); + } + + if (player->getState() == Player::Offline) { + player->reconnect(client); + passed = true; + return QJsonObject(); + } else { + error_msg = "others logged in with this name"; + passed = false; + } + } + +FAIL: + if (!passed) { + qInfo() << client->peerAddress() << "lost connection:" << error_msg; + server->sendEarlyPacket(client, "ErrorDlg", error_msg); + client->disconnectFromHost(); + return QJsonObject(); + } + + return obj; +} diff --git a/src/server/auth.h b/src/server/auth.h new file mode 100644 index 00000000..7619cb01 --- /dev/null +++ b/src/server/auth.h @@ -0,0 +1,27 @@ +#ifndef _AUTH_H +#define _AUTH_H + +#include +#include + +class ClientSocket; + +class AuthManager : public QObject { + Q_OBJECT +public: + AuthManager(QObject *parent = nullptr); + ~AuthManager() noexcept; + auto getPublicKey() const { return public_key; } + + bool checkClientVersion(ClientSocket *client, const QString &ver); + QJsonObject checkPassword(ClientSocket *client, const QString &name, const QString &password); + +private: + RSA *rsa; + QString public_key; + + static RSA *initRSA(); + QJsonObject queryUserInfo(ClientSocket *client, const QString &name, const QByteArray &password); +}; + +#endif // _AUTH_H diff --git a/src/server/lobby.cpp b/src/server/lobby.cpp new file mode 100644 index 00000000..bddcb429 --- /dev/null +++ b/src/server/lobby.cpp @@ -0,0 +1,162 @@ +#include "server/lobby.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "core/util.h" + +Lobby::Lobby(Server *server) { + this->server = server; + setParent(server); +} + +void Lobby::addPlayer(ServerPlayer *player) { + if (!player) return; + + players.append(player); + player->setRoom(this); + + if (player->getState() == Player::Robot) { + removePlayer(player); + player->deleteLater(); + } else { + player->doNotify("EnterLobby", "[]"); + } + + server->updateOnlineInfo(); +} + +void Lobby::removePlayer(ServerPlayer *player) { + players.removeOne(player); + server->updateOnlineInfo(); +} + +void Lobby::updateAvatar(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto avatar = arr[0].toString(); + + if (CheckSqlString(avatar)) { + auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;") + .arg(avatar) + .arg(sender->getId()); + ExecSQL(ServerInstance->getDatabase(), sql); + sender->setAvatar(avatar); + sender->doNotify("UpdateAvatar", avatar); + } +} + +void Lobby::updatePassword(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto oldpw = arr[0].toString(); + auto newpw = arr[1].toString(); + auto sql_find = + QString("SELECT password, salt FROM userinfo WHERE id=%1;") + .arg(sender->getId()); + + auto passed = false; + auto arr2 = SelectFromDatabase(ServerInstance->getDatabase(), sql_find); + auto result = arr2[0].toObject(); + passed = (result["password"].toString() == + QCryptographicHash::hash( + oldpw.append(result["salt"].toString()).toLatin1(), + QCryptographicHash::Sha256) + .toHex()); + if (passed) { + auto sql_update = + QString("UPDATE userinfo SET password='%1' WHERE id=%2;") + .arg(QCryptographicHash::hash( + newpw.append(result["salt"].toString()).toLatin1(), + QCryptographicHash::Sha256) + .toHex()) + .arg(sender->getId()); + ExecSQL(ServerInstance->getDatabase(), sql_update); + } + + sender->doNotify("UpdatePassword", passed ? "1" : "0"); +} + +void Lobby::createRoom(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto name = arr[0].toString(); + auto capacity = arr[1].toInt(); + auto timeout = arr[2].toInt(); + auto settings = + QJsonDocument(arr[3].toObject()).toJson(QJsonDocument::Compact); + ServerInstance->createRoom(sender, name, capacity, timeout, settings); +} + +void Lobby::getRoomConfig(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto roomId = arr[0].toInt(); + auto room = ServerInstance->findRoom(roomId); + if (room) { + auto settings = room->getSettings(); + // 手搓JSON数组 跳过编码解码 + sender->doNotify("GetRoomConfig", QString("[%1,%2]").arg(roomId).arg(settings)); + } else { + sender->doNotify("ErrorMsg", "no such room"); + } +} + +void Lobby::enterRoom(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto roomId = arr[0].toInt(); + auto room = ServerInstance->findRoom(roomId); + if (room) { + auto settings = QJsonDocument::fromJson(room->getSettings()); + auto password = settings["password"].toString(); + if (password.isEmpty() || arr[1].toString() == password) { + if (room->isOutdated()) { + sender->doNotify("ErrorMsg", "room is outdated"); + } else { + room->addPlayer(sender); + } + } else { + sender->doNotify("ErrorMsg", "room password error"); + } + } else { + sender->doNotify("ErrorMsg", "no such room"); + } +} + +void Lobby::observeRoom(ServerPlayer *sender, const QString &jsonData) { + auto arr = String2Json(jsonData).array(); + auto roomId = arr[0].toInt(); + auto room = ServerInstance->findRoom(roomId); + if (room) { + auto settings = QJsonDocument::fromJson(room->getSettings()); + auto password = settings["password"].toString(); + if (password.isEmpty() || arr[1].toString() == password) { + if (room->isOutdated()) { + sender->doNotify("ErrorMsg", "room is outdated"); + } else { + room->addObserver(sender); + } + } else { + sender->doNotify("ErrorMsg", "room password error"); + } + } else { + sender->doNotify("ErrorMsg", "no such room"); + } +} + +void Lobby::refreshRoomList(ServerPlayer *sender, const QString &) { + ServerInstance->updateRoomList(sender); +}; + +typedef void (Lobby::*room_cb)(ServerPlayer *, const QString &); + +void Lobby::handlePacket(ServerPlayer *sender, const QString &command, + const QString &jsonData) { + static const QMap lobby_actions = { + {"UpdateAvatar", &Lobby::updateAvatar}, + {"UpdatePassword", &Lobby::updatePassword}, + {"CreateRoom", &Lobby::createRoom}, + {"GetRoomConfig", &Lobby::getRoomConfig}, + {"EnterRoom", &Lobby::enterRoom}, + {"ObserveRoom", &Lobby::observeRoom}, + {"RefreshRoomList", &Lobby::refreshRoomList}, + {"Chat", &Lobby::chat}, + }; + + auto func = lobby_actions[command]; + if (func) (this->*func)(sender, jsonData); +} diff --git a/src/server/lobby.h b/src/server/lobby.h new file mode 100644 index 00000000..dad70931 --- /dev/null +++ b/src/server/lobby.h @@ -0,0 +1,27 @@ +#ifndef _LOBBY_H +#define _LOBBY_H + +#include "server/roombase.h" + +class Lobby : public RoomBase { + Q_OBJECT + public: + Lobby(Server *server); + + void addPlayer(ServerPlayer *player); + void removePlayer(ServerPlayer *player); + + void handlePacket(ServerPlayer *sender, const QString &command, + const QString &jsonData); + private: + // for handle packet + void updateAvatar(ServerPlayer *, const QString &); + void updatePassword(ServerPlayer *, const QString &); + void createRoom(ServerPlayer *, const QString &); + void getRoomConfig(ServerPlayer *, const QString &); + void enterRoom(ServerPlayer *, const QString &); + void observeRoom(ServerPlayer *, const QString &); + void refreshRoomList(ServerPlayer *, const QString &); +}; + +#endif // _LOBBY_H diff --git a/src/server/room.cpp b/src/server/room.cpp index 9a27d8f3..60e20aed 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -1,28 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "room.h" - -#include -#include +#include "server/room.h" +#include "server/lobby.h" #ifdef FK_SERVER_ONLY static void *ClientInstance = nullptr; #else -#include "client.h" +#include "client/client.h" #endif -#include "client_socket.h" -#include "roomthread.h" -#include "server.h" -#include "serverplayer.h" -#include "util.h" +#include "network/client_socket.h" +#include "server/roomthread.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "core/util.h" Room::Room(RoomThread *m_thread) { auto server = ServerInstance; id = server->nextRoomId; server->nextRoomId++; this->server = server; - setThread(m_thread); if (m_thread) { // In case of lobby m_thread->addRoom(this); } @@ -36,14 +33,8 @@ Room::Room(RoomThread *m_thread) { m_ready = true; - // 如果是普通房间而不是大厅,就初始化Lua,否则置Lua为nullptr - if (!isLobby()) { - // 如果不是大厅,那么: - // * 只要房间添加人了,那么从大厅中移掉这个人 - // * 只要有人离开房间,那就把他加到大厅去 - connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); - connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); - } + connect(this, &Room::playerAdded, server->lobby(), &Lobby::removePlayer); + connect(this, &Room::playerRemoved, server->lobby(), &Lobby::addPlayer); } Room::~Room() { @@ -56,8 +47,6 @@ Room::~Room() { } } -Server *Room::getServer() const { return server; } - RoomThread *Room::getThread() const { return m_thread; } void Room::setThread(RoomThread *t) { @@ -71,8 +60,6 @@ int Room::getId() const { return id; } void Room::setId(int id) { this->id = id; } -bool Room::isLobby() const { return id == 0; } - QString Room::getName() const { return name; } void Room::setName(const QString &name) { this->name = name; } @@ -88,9 +75,6 @@ const QByteArray Room::getSettings() const { return settings; } void Room::setSettings(QByteArray settings) { this->settings = settings; } bool Room::isAbandoned() const { - if (isLobby()) - return false; - if (players.isEmpty()) return true; @@ -151,72 +135,60 @@ void Room::addPlayer(ServerPlayer *player) { auto mode = settings["gameMode"].toString(); // 告诉房里所有玩家有新人进来了 - if (!isLobby()) { - jsonData << player->getId(); - jsonData << player->getScreenName(); - jsonData << player->getAvatar(); - jsonData << player->isReady(); - jsonData << player->getTotalGameTime(); - doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData)); - } + jsonData << player->getId(); + jsonData << player->getScreenName(); + jsonData << player->getAvatar(); + jsonData << player->isReady(); + jsonData << player->getTotalGameTime(); + doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData)); players.append(player); player->setRoom(this); - if (isLobby()) { - // 有机器人进入大厅(可能因为被踢),那么改为销毁 - if (player->getState() == Player::Robot) { - removePlayer(player); - player->deleteLater(); - } else { - player->doNotify("EnterLobby", "[]"); - } - } else { - // Second, let the player enter room and add other players + // Second, let the player enter room and add other players + jsonData = QJsonArray(); + jsonData << this->capacity; + jsonData << this->timeout; + jsonData << QJsonDocument::fromJson(this->settings).object(); + player->doNotify("EnterRoom", JsonArray2Bytes(jsonData)); + + foreach (ServerPlayer *p, getOtherPlayers(player)) { jsonData = QJsonArray(); - jsonData << this->capacity; - jsonData << this->timeout; - jsonData << QJsonDocument::fromJson(this->settings).object(); - player->doNotify("EnterRoom", JsonArray2Bytes(jsonData)); + jsonData << p->getId(); + jsonData << p->getScreenName(); + jsonData << p->getAvatar(); + jsonData << p->isReady(); + jsonData << p->getTotalGameTime(); + player->doNotify("AddPlayer", JsonArray2Bytes(jsonData)); - foreach (ServerPlayer *p, getOtherPlayers(player)) { - jsonData = QJsonArray(); - jsonData << p->getId(); - jsonData << p->getScreenName(); - jsonData << p->getAvatar(); - jsonData << p->isReady(); - jsonData << p->getTotalGameTime(); - player->doNotify("AddPlayer", JsonArray2Bytes(jsonData)); - - jsonData = QJsonArray(); - jsonData << p->getId(); - foreach (int i, p->getGameData()) { - jsonData << i; - } - player->doNotify("UpdateGameData", JsonArray2Bytes(jsonData)); + jsonData = QJsonArray(); + jsonData << p->getId(); + foreach (int i, p->getGameData()) { + jsonData << i; } - - if (this->owner != nullptr) { - jsonData = QJsonArray(); - jsonData << this->owner->getId(); - player->doNotify("RoomOwner", JsonArray2Bytes(jsonData)); - } - - if (player->getLastGameMode() != mode) { - player->setLastGameMode(mode); - updatePlayerGameData(player->getId(), mode); - } else { - auto jsonData = QJsonArray(); - jsonData << player->getId(); - foreach (int i, player->getGameData()) { - jsonData << i; - } - doBroadcastNotify(getPlayers(), "UpdateGameData", JsonArray2Bytes(jsonData)); - } - // 玩家手动启动 - // if (isFull() && !gameStarted) - // start(); + player->doNotify("UpdateGameData", JsonArray2Bytes(jsonData)); } + + if (this->owner != nullptr) { + jsonData = QJsonArray(); + jsonData << this->owner->getId(); + player->doNotify("RoomOwner", JsonArray2Bytes(jsonData)); + } + + if (player->getLastGameMode() != mode) { + player->setLastGameMode(mode); + updatePlayerGameData(player->getId(), mode); + } else { + auto jsonData = QJsonArray(); + jsonData << player->getId(); + foreach (int i, player->getGameData()) { + jsonData << i; + } + doBroadcastNotify(getPlayers(), "UpdateGameData", JsonArray2Bytes(jsonData)); + } + // 玩家手动启动 + // if (isFull() && !gameStarted) + // start(); emit playerAdded(player); } @@ -251,12 +223,7 @@ void Room::removePlayer(ServerPlayer *player) { } emit playerRemoved(player); - if (isLobby()) - return; - - QJsonArray jsonData; - jsonData << player->getId(); - doBroadcastNotify(getPlayers(), "RemovePlayer", JsonArray2Bytes(jsonData)); + doBroadcastNotify(getPlayers(), "RemovePlayer", JsonArray2Bytes({ player->getId() })); } else { // 否则给跑路玩家召唤个AI代打 // TODO: if the player is died.. @@ -287,7 +254,7 @@ void Room::removePlayer(ServerPlayer *player) { // 原先的跑路机器人会在游戏结束后自动销毁掉 server->addPlayer(runner); - m_thread->wakeUp(); + // m_thread->wakeUp(); // 发出信号,让大厅添加这个人 emit playerRemoved(runner); @@ -312,22 +279,6 @@ void Room::removePlayer(ServerPlayer *player) { } } -QList Room::getPlayers() const { return players; } - -QList Room::getOtherPlayers(ServerPlayer *expect) const { - QList others = getPlayers(); - others.removeOne(expect); - return others; -} - -ServerPlayer *Room::findPlayer(int id) const { - foreach (ServerPlayer *p, players) { - if (p->getId() == id) - return p; - } - return nullptr; -} - void Room::addObserver(ServerPlayer *player) { // 首先只能旁观在运行的房间,因为旁观是由Lua处理的 if (!gameStarted) { @@ -371,6 +322,10 @@ int Room::getTimeout() const { return timeout; } void Room::setTimeout(int timeout) { this->timeout = timeout; } +void Room::delay(int ms) { + m_thread->delay(id, ms); +} + bool Room::isOutdated() { bool ret = md5 != server->getMd5(); if (ret) md5 = ""; @@ -379,42 +334,6 @@ bool Room::isOutdated() { bool Room::isStarted() const { return gameStarted; } -void Room::doBroadcastNotify(const QList targets, - const QString &command, const QString &jsonData) { - foreach (ServerPlayer *p, targets) { - p->doNotify(command, jsonData); - } -} - -void Room::chat(ServerPlayer *sender, const QString &jsonData) { - auto doc = String2Json(jsonData).object(); - auto type = doc["type"].toInt(); - doc["sender"] = sender->getId(); - - // 屏蔽.号,防止有人在HTML文本发链接,而正常发链接看不出来有啥改动 - auto msg = doc["msg"].toString(); - msg.replace(".", "․"); - // 300字限制,与客户端相同 - msg.erase(msg.begin() + 300, msg.end()); - doc["msg"] = msg; - if (!server->checkBanWord(msg)) { - return; - } - - if (type == 1) { - 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); - doBroadcastNotify(observers, "Chat", json); - } - - qInfo("[Chat] %s: %s", sender->getScreenName().toUtf8().constData(), - doc["msg"].toString().toUtf8().constData()); -} - static const QString findWinRate = QString("SELECT win, lose, draw " "FROM winRate WHERE id = %1 and general = '%2' and mode = '%3';"); @@ -551,18 +470,18 @@ void Room::updatePlayerGameData(int id, const QString &mode) { auto room = player->getRoom(); player->setGameData(total, win, run); auto data_arr = QJsonArray({ player->getId(), total, win, run }); - if (!room->isLobby()) { - room->doBroadcastNotify(room->getPlayers(), "UpdateGameData", JsonArray2Bytes(data_arr)); - } + room->doBroadcastNotify(room->getPlayers(), "UpdateGameData", JsonArray2Bytes(data_arr)); } void Room::gameOver() { if (!gameStarted) return; + insideGameOver = true; gameStarted = false; runned_players.clear(); // 清理所有状态不是“在线”的玩家,增加逃率、游戏时长 auto settings = QJsonDocument::fromJson(this->settings); auto mode = settings["gameMode"].toString(); + server->beginTransaction(); foreach (ServerPlayer *p, players) { auto pid = p->getId(); @@ -578,7 +497,7 @@ void Room::gameOver() { realPlayer->doNotify("AddTotalGameTime", bytes); } - // 摸了,这么写总之不会有问题 + // 将游戏时间更新到数据库中 auto info_update = QString("UPDATE usergameinfo SET totalGameTime = " "IIF(totalGameTime IS NULL, %2, totalGameTime + %2) WHERE id = %1;").arg(pid).arg(time); ExecSQL(server->getDatabase(), info_update); @@ -587,18 +506,13 @@ void Room::gameOver() { if (p->getState() != Player::Online) { if (p->getState() == Player::Offline) { addRunRate(pid, mode); - // addRunRate(pid, mode); server->temporarilyBan(pid); } p->deleteLater(); } } - // 旁观者不能在这清除,因为removePlayer逻辑不一样 - // observers.clear(); - // 玩家也不能在这里清除,因为要能返回原来房间继续玩呢 - // players.clear(); - // owner = nullptr; - // clearRequest(); + server->endTransaction(); + insideGameOver = true; } void Room::manuallyStart() { @@ -620,7 +534,6 @@ void Room::pushRequest(const QString &req) { } void Room::addRejectId(int id) { - if (isLobby()) return; rejected_players << id; } @@ -629,184 +542,84 @@ void Room::removeRejectId(int id) { } // ------------------------------------------------ -static void updateAvatar(ServerPlayer *sender, const QString &jsonData) { - auto arr = String2Json(jsonData).array(); - auto avatar = arr[0].toString(); - - if (CheckSqlString(avatar)) { - auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;") - .arg(avatar) - .arg(sender->getId()); - ExecSQL(ServerInstance->getDatabase(), sql); - sender->setAvatar(avatar); - sender->doNotify("UpdateAvatar", avatar); - } -} - -static void updatePassword(ServerPlayer *sender, const QString &jsonData) { - auto arr = String2Json(jsonData).array(); - auto oldpw = arr[0].toString(); - auto newpw = arr[1].toString(); - auto sql_find = - QString("SELECT password, salt FROM userinfo WHERE id=%1;") - .arg(sender->getId()); - - auto passed = false; - auto arr2 = SelectFromDatabase(ServerInstance->getDatabase(), sql_find); - auto result = arr2[0].toObject(); - passed = (result["password"].toString() == - QCryptographicHash::hash( - oldpw.append(result["salt"].toString()).toLatin1(), - QCryptographicHash::Sha256) - .toHex()); - if (passed) { - auto sql_update = - QString("UPDATE userinfo SET password='%1' WHERE id=%2;") - .arg(QCryptographicHash::hash( - newpw.append(result["salt"].toString()).toLatin1(), - QCryptographicHash::Sha256) - .toHex()) - .arg(sender->getId()); - ExecSQL(ServerInstance->getDatabase(), sql_update); - } - - sender->doNotify("UpdatePassword", passed ? "1" : "0"); -} - -static void createRoom(ServerPlayer *sender, const QString &jsonData) { - auto arr = String2Json(jsonData).array(); - auto name = arr[0].toString(); - auto capacity = arr[1].toInt(); - auto timeout = arr[2].toInt(); - auto settings = - QJsonDocument(arr[3].toObject()).toJson(QJsonDocument::Compact); - ServerInstance->createRoom(sender, name, capacity, timeout, settings); -} - -static void enterRoom(ServerPlayer *sender, const QString &jsonData) { - auto arr = String2Json(jsonData).array(); - auto roomId = arr[0].toInt(); - auto room = ServerInstance->findRoom(roomId); - if (room) { - auto settings = QJsonDocument::fromJson(room->getSettings()); - auto password = settings["password"].toString(); - if (password.isEmpty() || arr[1].toString() == password) { - if (room->isOutdated()) { - sender->doNotify("ErrorMsg", "room is outdated"); - } else { - room->addPlayer(sender); - } - } else { - sender->doNotify("ErrorMsg", "room password error"); - } - } else { - sender->doNotify("ErrorMsg", "no such room"); - } -} - -static void observeRoom(ServerPlayer *sender, const QString &jsonData) { - auto arr = String2Json(jsonData).array(); - auto roomId = arr[0].toInt(); - auto room = ServerInstance->findRoom(roomId); - if (room) { - auto settings = QJsonDocument::fromJson(room->getSettings()); - auto password = settings["password"].toString(); - if (password.isEmpty() || arr[1].toString() == password) { - if (room->isOutdated()) { - sender->doNotify("ErrorMsg", "room is outdated"); - } else { - room->addObserver(sender); - } - } else { - sender->doNotify("ErrorMsg", "room password error"); - } - } else { - sender->doNotify("ErrorMsg", "no such room"); - } -} - -static void refreshRoomList(ServerPlayer *sender, const QString &) { - ServerInstance->updateRoomList(sender); -}; - -static void quitRoom(ServerPlayer *player, const QString &) { - auto room = player->getRoom(); - room->removePlayer(player); - if (room->isOutdated()) { +void Room::quitRoom(ServerPlayer *player, const QString &) { + removePlayer(player); + if (isOutdated()) { player->kicked(); } } -static void addRobot(ServerPlayer *player, const QString &) { - auto room = player->getRoom(); +void Room::addRobotRequest(ServerPlayer *player, const QString &) { if (ServerInstance->getConfig("enableBots").toBool()) - room->addRobot(player); + addRobot(player); } -static void kickPlayer(ServerPlayer *player, const QString &jsonData) { - auto room = player->getRoom(); +void Room::kickPlayer(ServerPlayer *player, const QString &jsonData) { int i = jsonData.toInt(); - auto p = room->findPlayer(i); - if (p && !room->isStarted()) { - room->removePlayer(p); - room->addRejectId(i); - QTimer::singleShot(30000, room, [=]() { - room->removeRejectId(i); + auto p = findPlayer(i); + if (p && !isStarted()) { + removePlayer(p); + addRejectId(i); + QTimer::singleShot(30000, this, [=]() { + removeRejectId(i); }); } } -static void ready(ServerPlayer *player, const QString &) { - auto room = player->getRoom(); +void Room::ready(ServerPlayer *player, const QString &) { player->setReady(!player->isReady()); - room->doBroadcastNotify(room->getPlayers(), "ReadyChanged", + doBroadcastNotify(getPlayers(), "ReadyChanged", QString("[%1,%2]").arg(player->getId()).arg(player->isReady())); } -static void startGame(ServerPlayer *player, const QString &) { - auto room = player->getRoom(); - if (room->isOutdated()) { - foreach (auto p, room->getPlayers()) { +void Room::startGame(ServerPlayer *player, const QString &) { + if (isOutdated()) { + foreach (auto p, getPlayers()) { p->doNotify("ErrorMsg", "room is outdated"); p->kicked(); } } else { - room->manuallyStart(); + manuallyStart(); } } -typedef void (*room_cb)(ServerPlayer *, const QString &); -static const QMap lobby_actions = { - {"UpdateAvatar", updateAvatar}, - {"UpdatePassword", updatePassword}, - {"CreateRoom", createRoom}, - {"EnterRoom", enterRoom}, - {"ObserveRoom", observeRoom}, - {"RefreshRoomList", refreshRoomList}, -}; - -static const QMap room_actions = { - {"QuitRoom", quitRoom}, - {"AddRobot", addRobot}, - {"KickPlayer", kickPlayer}, - {"Ready", ready}, - {"StartGame", startGame}, -}; +typedef void (Room::*room_cb)(ServerPlayer *, const QString &); void Room::handlePacket(ServerPlayer *sender, const QString &command, const QString &jsonData) { - if (command == "Chat") { - chat(sender, jsonData); + static const QMap room_actions = { + {"QuitRoom", &Room::quitRoom}, + {"AddRobot", &Room::addRobotRequest}, + {"KickPlayer", &Room::kickPlayer}, + {"Ready", &Room::ready}, + {"StartGame", &Room::startGame}, + {"Chat", &Room::chat}, + }; + + if (command == "PushRequest") { + pushRequest(QString("%1,").arg(sender->getId()) + jsonData); return; - } else if (command == "PushRequest") { - if (!isLobby()) - pushRequest(QString("%1,").arg(sender->getId()) + jsonData); } - auto func_table = lobby_actions; - if (!isLobby()) func_table = room_actions; - auto func = func_table[command]; - if (func) { - func(sender, jsonData); - } + auto func = room_actions[command]; + if (func) (this->*func)(sender, jsonData); +} + +// Lua用:request之前设置计时器防止等到死。 +void Room::setRequestTimer(int ms) { + request_timer = new QTimer(); + request_timer->setSingleShot(true); + request_timer->setInterval(ms); + connect(request_timer, &QTimer::timeout, this, [=](){ + m_thread->wakeUp(id); + }); + request_timer->start(); +} + +// Lua用:当request完成后手动销毁计时器。 +void Room::destroyRequestTimer() { + if (!request_timer) return; + request_timer->stop(); + delete request_timer; + request_timer = nullptr; } diff --git a/src/server/room.h b/src/server/room.h index 221be5b8..ab671686 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -3,11 +3,13 @@ #ifndef _ROOM_H #define _ROOM_H +#include "server/roombase.h" + class Server; class ServerPlayer; class RoomThread; -class Room : public QObject { +class Room : public RoomBase { Q_OBJECT public: explicit Room(RoomThread *m_thread); @@ -15,12 +17,11 @@ class Room : public QObject { // Property reader & setter // ==================================={ - Server *getServer() const; RoomThread *getThread() const; void setThread(RoomThread *t); + int getId() const; void setId(int id); - bool isLobby() const; QString getName() const; void setName(const QString &name); int getCapacity() const; @@ -38,9 +39,6 @@ class Room : public QObject { void addPlayer(ServerPlayer *player); void addRobot(ServerPlayer *player); void removePlayer(ServerPlayer *player); - QList getPlayers() const; - QList getOtherPlayers(ServerPlayer *expect) const; - ServerPlayer *findPlayer(int id) const; void addObserver(ServerPlayer *player); void removeObserver(ServerPlayer *player); @@ -49,16 +47,13 @@ class Room : public QObject { int getTimeout() const; void setTimeout(int timeout); + void delay(int ms); bool isOutdated(); bool isStarted() const; // ====================================} - void doBroadcastNotify(const QList targets, - const QString &command, const QString &jsonData); - void chat(ServerPlayer *sender, const QString &jsonData); - void updateWinRate(int id, const QString &general, const QString &mode, int result, bool dead); void gameOver(); @@ -71,6 +66,13 @@ class Room : public QObject { // router用 void handlePacket(ServerPlayer *sender, const QString &command, const QString &jsonData); + + void setRequestTimer(int ms); + void destroyRequestTimer(); + + // FIXME + volatile bool insideGameOver = false; + signals: void abandoned(); @@ -78,8 +80,7 @@ class Room : public QObject { void playerRemoved(ServerPlayer *player); private: - Server *server; - RoomThread *m_thread; + RoomThread *m_thread = nullptr; int id; // Lobby's id is 0 QString name; // “阴间大乱斗” int capacity; // by default is 5, max is 8 @@ -87,8 +88,6 @@ class Room : public QObject { bool m_abandoned; // If room is empty, delete it ServerPlayer *owner; // who created this room? - QList players; - QList observers; QList runned_players; QList rejected_players; int robot_id; @@ -98,8 +97,17 @@ class Room : public QObject { int timeout; QString md5; + QTimer *request_timer = nullptr; + void addRunRate(int id, const QString &mode); void updatePlayerGameData(int id, const QString &mode); + + // handle packet + void quitRoom(ServerPlayer *, const QString &); + void addRobotRequest(ServerPlayer *, const QString &); + void kickPlayer(ServerPlayer *, const QString &); + void ready(ServerPlayer *, const QString &); + void startGame(ServerPlayer *, const QString &); }; #endif // _ROOM_H diff --git a/src/server/roombase.cpp b/src/server/roombase.cpp new file mode 100644 index 00000000..0abb85db --- /dev/null +++ b/src/server/roombase.cpp @@ -0,0 +1,62 @@ +#include "server/roombase.h" +#include "server/serverplayer.h" +#include "server/server.h" +#include "core/util.h" + +Server *RoomBase::getServer() const { return server; } + +bool RoomBase::isLobby() const { + return inherits("Lobby"); +} + +QList RoomBase::getPlayers() const { return players; } + +QList RoomBase::getOtherPlayers(ServerPlayer *expect) const { + QList others = getPlayers(); + others.removeOne(expect); + return others; +} + +ServerPlayer *RoomBase::findPlayer(int id) const { + foreach (ServerPlayer *p, players) { + if (p->getId() == id) + return p; + } + return nullptr; +} + +void RoomBase::doBroadcastNotify(const QList targets, + const QString &command, const QString &jsonData) { + foreach (ServerPlayer *p, targets) { + p->doNotify(command, jsonData); + } +} + +void RoomBase::chat(ServerPlayer *sender, const QString &jsonData) { + auto doc = String2Json(jsonData).object(); + auto type = doc["type"].toInt(); + doc["sender"] = sender->getId(); + + // 屏蔽.号,防止有人在HTML文本发链接,而正常发链接看不出来有啥改动 + auto msg = doc["msg"].toString(); + msg.replace(".", "․"); + // 300字限制,与客户端相同 + msg.erase(msg.begin() + 300, msg.end()); + doc["msg"] = msg; + if (!server->checkBanWord(msg)) { + return; + } + + if (type == 1) { + 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); + doBroadcastNotify(observers, "Chat", json); + } + + qInfo("[Chat] %s: %s", sender->getScreenName().toUtf8().constData(), + doc["msg"].toString().toUtf8().constData()); +} diff --git a/src/server/roombase.h b/src/server/roombase.h new file mode 100644 index 00000000..ef749949 --- /dev/null +++ b/src/server/roombase.h @@ -0,0 +1,30 @@ +#ifndef _ROOMBASE_H +#define _ROOMBASE_H + +class Server; +class ServerPlayer; + +class RoomBase : public QObject { + public: + Server *getServer() const; + bool isLobby() const; + QList getPlayers() const; + QList getOtherPlayers(ServerPlayer *expect) const; + ServerPlayer *findPlayer(int id) const; + + void doBroadcastNotify(const QList targets, + const QString &command, const QString &jsonData); + + void chat(ServerPlayer *sender, const QString &jsonData); + + virtual void addPlayer(ServerPlayer *player) = 0; + virtual void removePlayer(ServerPlayer *player) = 0; + virtual void handlePacket(ServerPlayer *sender, const QString &command, + const QString &jsonData) = 0; + protected: + Server *server; + QList players; + QList observers; +}; + +#endif // _ROOMBASE_H diff --git a/src/server/roomthread.cpp b/src/server/roomthread.cpp index cbbff968..10f1cd87 100644 --- a/src/server/roomthread.cpp +++ b/src/server/roomthread.cpp @@ -1,45 +1,42 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "roomthread.h" -#include "server.h" -#include "util.h" -#include +#include "server/roomthread.h" +#include "server/scheduler.h" +#include "server/server.h" #ifndef FK_SERVER_ONLY -#include "client.h" +#include "client/client.h" #endif RoomThread::RoomThread(Server *m_server) { setObjectName("Room"); this->m_server = m_server; m_capacity = 100; // TODO: server cfg - terminated = false; md5 = m_server->getMd5(); - L = CreateLuaState(); - if (QFile::exists("packages/freekill-core") && - !GetDisabledPacks().contains("freekill-core")) { - // 危险的cd操作,记得在lua中切回游戏根目录 - QDir::setCurrent("packages/freekill-core"); - } - - DoLuaScript(L, "lua/freekill.lua"); - DoLuaScript(L, "lua/server/scheduler.lua"); start(); } RoomThread::~RoomThread() { - tryTerminate(); if (isRunning()) { - wait(); + quit(); } - lua_close(L); + delete m_scheduler; m_server->removeThread(this); // foreach (auto room, room_list) { // room->deleteLater(); // } } +void RoomThread::run() { + // 在run中创建,这样就能在接下来的exec中处理事件了 + m_scheduler = new Scheduler(this); + connect(this, &RoomThread::pushRequest, m_scheduler, &Scheduler::handleRequest); + connect(this, &RoomThread::delay, m_scheduler, &Scheduler::doDelay); + connect(this, &RoomThread::wakeUp, m_scheduler, &Scheduler::resumeRoom); + exec(); +} + Server *RoomThread::getServer() const { return m_server; } @@ -56,7 +53,7 @@ Room *RoomThread::getRoom(int id) const { } void RoomThread::addRoom(Room *room) { - Q_UNUSED(room); + room->setThread(this); m_capacity--; } @@ -69,6 +66,7 @@ void RoomThread::removeRoom(Room *room) { } } +/* QString RoomThread::fetchRequest() { // if (!gameStarted) // return ""; @@ -124,6 +122,7 @@ void RoomThread::tryTerminate() { bool RoomThread::isTerminated() const { return terminated; } +*/ bool RoomThread::isConsoleStart() const { #ifndef FK_SERVER_ONLY diff --git a/src/server/roomthread.h b/src/server/roomthread.h index 25d0afe3..aa80e88a 100644 --- a/src/server/roomthread.h +++ b/src/server/roomthread.h @@ -3,9 +3,9 @@ #ifndef _ROOMTHREAD_H #define _ROOMTHREAD_H -#include class Room; class Server; +class Scheduler; class RoomThread : public QThread { Q_OBJECT @@ -21,21 +21,24 @@ class RoomThread : public QThread { void addRoom(Room *room); void removeRoom(Room *room); - QString fetchRequest(); - void pushRequest(const QString &req); - void clearRequest(); - bool hasRequest(); + //QString fetchRequest(); + //void clearRequest(); + //bool hasRequest(); - void trySleep(int ms); - void wakeUp(); + // void trySleep(int ms); - void tryTerminate(); - bool isTerminated() const; + // void tryTerminate(); + // bool isTerminated() const; bool isConsoleStart() const; bool isOutdated(); + signals: + void pushRequest(const QString &req); + void delay(int roomId, int ms); + void wakeUp(int roomId); + protected: virtual void run(); @@ -45,11 +48,11 @@ class RoomThread : public QThread { int m_capacity; QString md5; - lua_State *L; - QMutex request_queue_mutex; - QQueue request_queue; // json string - QSemaphore sema_wake; - volatile bool terminated; + Scheduler *m_scheduler; + // QMutex request_queue_mutex; + // QQueue request_queue; // json string + // QSemaphore sema_wake; + // volatile bool terminated; }; #endif // _ROOMTHREAD_H diff --git a/src/server/scheduler.cpp b/src/server/scheduler.cpp new file mode 100644 index 00000000..f0a9d211 --- /dev/null +++ b/src/server/scheduler.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "server/scheduler.h" +#include "server/roomthread.h" +#include "core/util.h" + +Scheduler::Scheduler(RoomThread *thread) { + m_thread = thread; + L = CreateLuaState(); + if (QFile::exists("packages/freekill-core") && + !GetDisabledPacks().contains("freekill-core")) { + // 危险的cd操作,记得在lua中切回游戏根目录 + QDir::setCurrent("packages/freekill-core"); + } + DoLuaScript(L, "lua/freekill.lua"); + DoLuaScript(L, "lua/server/scheduler.lua"); + tellThreadToLua(); +} + +Scheduler::~Scheduler() { + lua_close(L); +} + +void Scheduler::handleRequest(const QString &req) { + lua_getglobal(L, "HandleRequest"); + auto bytes = req.toUtf8(); + lua_pushstring(L, bytes.data()); + + int err = lua_pcall(L, 1, 1, 0); + const char *result = lua_tostring(L, -1); + if (err) { + qCritical() << result; + lua_pop(L, 1); + } + lua_pop(L, 1); +} + +void Scheduler::doDelay(int roomId, int ms) { + QTimer::singleShot(ms, [=](){ resumeRoom(roomId); }); +} + +bool Scheduler::resumeRoom(int roomId) { + lua_getglobal(L, "ResumeRoom"); + lua_pushnumber(L, roomId); + + int err = lua_pcall(L, 1, 1, 0); + const char *result = lua_tostring(L, -1); + if (err) { + qCritical() << result; + lua_pop(L, 1); + return true; + } + auto ret = lua_toboolean(L, -1); + return ret; +} diff --git a/src/server/scheduler.h b/src/server/scheduler.h new file mode 100644 index 00000000..c304e76f --- /dev/null +++ b/src/server/scheduler.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef _SCHEDULER_H +#define _SCHEDULER_H + +class Room; +class RoomThread; + +class Scheduler : public QObject { + Q_OBJECT + public: + explicit Scheduler(RoomThread *m_thread); + ~Scheduler(); + + void tellThreadToLua(); // 转swig + + public slots: + // 跨线程传递引用可能出问题! + void handleRequest(const QString &req); + void doDelay(int roomId, int ms); + bool resumeRoom(int roomId); + + private: + RoomThread *m_thread; + lua_State *L; + // QList room_list; +}; + +#endif // _ROOMTHREAD_H diff --git a/src/server/server.cpp b/src/server/server.cpp index 996157b1..6500f8ae 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,54 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "server.h" +#include "server/server.h" +#include "server/auth.h" +#include "server/room.h" +#include "server/lobby.h" +#include "server/roomthread.h" +#include "server/serverplayer.h" +#include "network/router.h" +#include "network/client_socket.h" +#include "network/server_socket.h" +#include "core/packman.h" +#include "core/util.h" -#include -#include -#include -#include -#include #include -#include - -#include "client_socket.h" -#include "packman.h" -#include "player.h" -#include "room.h" -#include "roomthread.h" -#include "router.h" -#include "server_socket.h" -#include "serverplayer.h" -#include "util.h" - Server *ServerInstance = nullptr; Server::Server(QObject *parent) : QObject(parent) { ServerInstance = this; db = OpenDatabase(); - rsa = initServerRSA(); - QFile file("server/rsa_pub"); - file.open(QIODevice::ReadOnly); - QTextStream in(&file); - public_key = in.readAll(); md5 = calcFileMD5(); readConfig(); - server = new ServerSocket(); - server->setParent(this); + auth = new AuthManager(this); + server = new ServerSocket(this); connect(server, &ServerSocket::new_connection, this, &Server::processNewConnection); - udpSocket = new QUdpSocket(this); - connect(udpSocket, &QUdpSocket::readyRead, - this, &Server::readPendingDatagrams); - - // 创建第一个房间,这个房间作为“大厅房间” - nextRoomId = 0; - createRoom(nullptr, "Lobby", INT32_MAX); - // 大厅只要发生人员变动,就向所有人广播一下房间列表 - connect(lobby(), &Room::playerAdded, this, &Server::updateOnlineInfo); - connect(lobby(), &Room::playerRemoved, this, &Server::updateOnlineInfo); + nextRoomId = 1; + m_lobby = new Lobby(this); // 启动心跳包线程 auto heartbeatThread = QThread::create([=]() { @@ -90,12 +70,10 @@ Server::~Server() { thread->deleteLater(); } sqlite3_close(db); - RSA_free(rsa); } bool Server::listen(const QHostAddress &address, ushort port) { bool ret = server->listen(address, port); - udpSocket->bind(port); isListening = ret; return ret; } @@ -118,7 +96,7 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity, } } - if (!thread && nextRoomId != 0) { + if (!thread) { thread = createThread(); } @@ -127,16 +105,12 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity, room->setId(nextRoomId); nextRoomId++; room->setAbandoned(false); - room->setThread(thread); thread->addRoom(room); rooms.insert(room->getId(), room); } else { room = new Room(thread); connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); - if (room->isLobby()) - m_lobby = room; - else - rooms.insert(room->getId(), room); + rooms.insert(room->getId(), room); } room->setName(name); @@ -144,13 +118,12 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity, room->setTimeout(timeout); room->setSettings(settings); room->addPlayer(owner); - if (!room->isLobby()) - room->setOwner(owner); + room->setOwner(owner); } Room *Server::findRoom(int id) const { return rooms.value(id); } -Room *Server::lobby() const { return m_lobby; } +Lobby *Server::lobby() const { return m_lobby; } RoomThread *Server::createThread() { RoomThread *thread = new RoomThread(this); @@ -234,27 +207,6 @@ void Server::sendEarlyPacket(ClientSocket *client, const QString &type, const QS client->send(JsonArray2Bytes(body)); } -bool Server::checkClientVersion(ClientSocket *client, const QString &cver) { - auto client_ver = QVersionNumber::fromString(cver); - auto ver = QVersionNumber::fromString(FK_VERSION); - int cmp = QVersionNumber::compare(ver, client_ver); - if (cmp != 0) { - auto errmsg = QString(); - if (cmp < 0) { - errmsg = QString("[\"server is still on version %%2\",\"%1\"]") - .arg(FK_VERSION, "1"); - } else { - errmsg = QString("[\"server is using version %%2, please update\",\"%1\"]") - .arg(FK_VERSION, "1"); - } - - sendEarlyPacket(client, "ErrorMsg", errmsg); - client->disconnectFromHost(); - return false; - } - return true; -} - void Server::setupPlayer(ServerPlayer *player, bool all_info) { // tell the lobby player's basic property QJsonArray arr; @@ -291,7 +243,7 @@ void Server::processNewConnection(ClientSocket *client) { } if (!errmsg.isEmpty()) { - sendEarlyPacket(client, "ErrorMsg", errmsg); + sendEarlyPacket(client, "ErrorDlg", errmsg); qInfo() << "Refused banned IP:" << addr; client->disconnectFromHost(); return; @@ -301,7 +253,7 @@ void Server::processNewConnection(ClientSocket *client) { [client]() { qInfo() << client->peerAddress() << "disconnected"; }); // network delay test - sendEarlyPacket(client, "NetworkDelayTest", public_key); + sendEarlyPacket(client, "NetworkDelayTest", auth->getPublicKey()); // Note: the client should send a setup string next connect(client, &ClientSocket::message_got, this, &Server::processRequest); client->timerSignup.start(30000); @@ -328,56 +280,30 @@ void Server::processRequest(const QByteArray &msg) { if (!valid) { qWarning() << "Invalid setup string:" << msg; - sendEarlyPacket(client, "ErrorMsg", "INVALID SETUP STRING"); + sendEarlyPacket(client, "ErrorDlg", "INVALID SETUP STRING"); client->disconnectFromHost(); return; } QJsonArray arr = String2Json(doc[3].toString()).array(); - if (!checkClientVersion(client, arr[3].toString())) return; + if (!auth->checkClientVersion(client, arr[3].toString())) return; - auto uuid = arr[4].toString(); + auto uuid_str = arr[4].toString(); auto result2 = QJsonArray({1}); - if (CheckSqlString(uuid)) { + if (CheckSqlString(uuid_str)) { result2 = SelectFromDatabase( - db, QString("SELECT * FROM banuuid WHERE uuid='%1';").arg(uuid)); + db, QString("SELECT * FROM banuuid WHERE uuid='%1';").arg(uuid_str)); } if (!result2.isEmpty()) { - sendEarlyPacket(client, "ErrorMsg", "you have been banned!"); - qInfo() << "Refused banned UUID:" << uuid; + sendEarlyPacket(client, "ErrorDlg", "you have been banned!"); + qInfo() << "Refused banned UUID:" << uuid_str; client->disconnectFromHost(); return; } - handleNameAndPassword(client, arr[0].toString(), arr[1].toString(), - arr[2].toString(), uuid); -} - -void Server::handleNameAndPassword(ClientSocket *client, const QString &name, - const QString &password, - const QString &md5_str, - const QString &uuid_str) { - auto encryted_pw = QByteArray::fromBase64(password.toLatin1()); - unsigned char buf[4096] = {0}; - RSA_private_decrypt(RSA_size(rsa), (const unsigned char *)encryted_pw.data(), - buf, rsa, RSA_PKCS1_PADDING); - auto decrypted_pw = - QByteArray::fromRawData((const char *)buf, strlen((const char *)buf)); - - if (decrypted_pw.length() > 32) { - auto aes_bytes = decrypted_pw.first(32); - - // tell client to install aes key - sendEarlyPacket(client, "InstallKey", ""); - - client->installAESKey(aes_bytes); - decrypted_pw.remove(0, 32); - } else { - decrypted_pw = "\xFF"; - } - + auto md5_str = arr[2].toString(); if (md5 != md5_str) { sendEarlyPacket(client, "ErrorMsg", "MD5 check failed!"); sendEarlyPacket(client, "UpdatePackage", Pacman->getPackSummary()); @@ -385,146 +311,53 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, return; } - bool passed = false; - QString error_msg; - QJsonArray result; - QJsonObject obj; + auto name = arr[0].toString(); + auto password = arr[1].toString(); + auto obj = auth->checkPassword(client, name, password); + if (obj.isEmpty()) return; - if (CheckSqlString(name) && checkBanWord(name)) { - // Then we check the database, - QString sql_find = QString("SELECT * FROM userinfo \ - WHERE name='%1';") - .arg(name); - result = SelectFromDatabase(db, sql_find); - if (result.isEmpty()) { - auto salt_gen = QRandomGenerator::securelySeeded(); - auto salt = QByteArray::number(salt_gen(), 16); - decrypted_pw.append(salt); - auto passwordHash = - QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256) - .toHex(); - // not present in database, register - QString sql_reg = QString("INSERT INTO userinfo (name,password,salt,\ - avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4','%5',%6);") - .arg(name) - .arg(QString(passwordHash)) - .arg(salt) - .arg("liubei") - .arg(client->peerAddress()) - .arg("FALSE"); - ExecSQL(db, sql_reg); - result = SelectFromDatabase(db, sql_find); // refresh result - obj = result[0].toObject(); + // update lastLoginIp + int id = obj["id"].toString().toInt(); + beginTransaction(); + auto sql_update = + QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;") + .arg(client->peerAddress()) + .arg(id); + ExecSQL(db, sql_update); - auto info_update = QString("INSERT INTO usergameinfo (id, registerTime) VALUES (%1, %2);").arg(obj["id"].toString().toInt()).arg(QDateTime::currentSecsSinceEpoch()); - ExecSQL(db, info_update); + auto uuid_update = QString("REPLACE INTO uuidinfo (id, uuid) VALUES (%1, '%2');") + .arg(id).arg(uuid_str); + ExecSQL(db, uuid_update); - passed = true; - } else { - obj = result[0].toObject(); + // 来晚了,有很大可能存在已经注册但是表里面没数据的人 + ExecSQL(db, QString("INSERT OR IGNORE INTO usergameinfo (id) VALUES (%1);").arg(id)); + auto info_update = QString("UPDATE usergameinfo SET lastLoginTime=%2 where id=%1;").arg(id).arg(QDateTime::currentSecsSinceEpoch()); + ExecSQL(db, info_update); + endTransaction(); - // check ban account - int id = obj["id"].toString().toInt(); - passed = obj["banned"].toString().toInt() == 0; - if (!passed) { - error_msg = "you have been banned!"; - } - - // check if password is the same - auto salt = obj["salt"].toString().toLatin1(); - decrypted_pw.append(salt); - auto passwordHash = - QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256) - .toHex(); - passed = (passwordHash == obj["password"].toString()); - - if (!passed) { - error_msg = "username or password error"; - } else if (players.value(id)) { - auto player = players.value(id); - // 顶号机制,如果在线的话就让他变成不在线 - if (player->getState() == Player::Online) { - player->doNotify("ErrorMsg", "others logged in again with this name"); - emit player->kicked(); - } - - if (player->getState() == Player::Offline) { - auto room = player->getRoom(); - player->setSocket(client); - player->alive = true; - client->disconnect(this); - if (players.count() <= 10) { - broadcast("ServerMessage", tr("%1 backed").arg(player->getScreenName())); - } - - if (room && !room->isLobby()) { - setupPlayer(player, true); - room->pushRequest(QString("%1,reconnect").arg(id)); - } else { - // 懒得处理掉线玩家在大厅了!踢掉得了 - player->doNotify("ErrorMsg", "Unknown Error"); - emit player->kicked(); - } - - return; - } else { - error_msg = "others logged in with this name"; - passed = false; - } - } - } - } else { - error_msg = "invalid user name"; + // create new ServerPlayer and setup + ServerPlayer *player = new ServerPlayer(lobby()); + player->setSocket(client); + client->disconnect(this); + connect(player, &ServerPlayer::disconnected, this, + &Server::onUserDisconnected); + connect(player, &Player::stateChanged, this, &Server::onUserStateChanged); + player->setScreenName(name); + player->setAvatar(obj["avatar"].toString()); + player->setId(id); + if (players.count() <= 10) { + broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName())); } + players.insert(player->getId(), player); - if (passed) { - // update lastLoginIp - int id = obj["id"].toString().toInt(); - beginTransaction(); - auto sql_update = - QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;") - .arg(client->peerAddress()) - .arg(id); - ExecSQL(db, sql_update); + setupPlayer(player); - auto uuid_update = QString("REPLACE INTO uuidinfo (id, uuid) VALUES (%1, '%2');").arg(id).arg(uuid_str); - ExecSQL(db, uuid_update); + auto result = SelectFromDatabase(db, QString("SELECT totalGameTime FROM usergameinfo WHERE id=%1;").arg(id)); + auto time = result[0].toObject()["totalGameTime"].toString().toInt(); + player->addTotalGameTime(time); + player->doNotify("AddTotalGameTime", JsonArray2Bytes({ id, time })); - // 来晚了,有很大可能存在已经注册但是表里面没数据的人 - ExecSQL(db, QString("INSERT OR IGNORE INTO usergameinfo (id) VALUES (%1);").arg(id)); - auto info_update = QString("UPDATE usergameinfo SET lastLoginTime=%2 where id=%1;").arg(id).arg(QDateTime::currentSecsSinceEpoch()); - ExecSQL(db, info_update); - endTransaction(); - - // create new ServerPlayer and setup - ServerPlayer *player = new ServerPlayer(lobby()); - player->setSocket(client); - client->disconnect(this); - connect(player, &ServerPlayer::disconnected, this, - &Server::onUserDisconnected); - connect(player, &Player::stateChanged, this, &Server::onUserStateChanged); - player->setScreenName(name); - player->setAvatar(obj["avatar"].toString()); - player->setId(id); - if (players.count() <= 10) { - broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName())); - } - players.insert(player->getId(), player); - - setupPlayer(player); - - auto result = SelectFromDatabase(db, QString("SELECT totalGameTime FROM usergameinfo WHERE id=%1;").arg(id)); - auto time = result[0].toObject()["totalGameTime"].toString().toInt(); - player->addTotalGameTime(time); - player->doNotify("AddTotalGameTime", JsonArray2Bytes({ id, time })); - - lobby()->addPlayer(player); - } else { - qInfo() << client->peerAddress() << "lost connection:" << error_msg; - sendEarlyPacket(client, "ErrorMsg", error_msg); - client->disconnectFromHost(); - return; - } + lobby()->addPlayer(player); } void Server::onRoomAbandoned() { @@ -537,37 +370,49 @@ void Server::onRoomAbandoned() { // FIXME: 但是这终归是内存泄漏!以后啥时候再改吧。 // room->deleteLater(); idle_rooms.push(room); + room->getThread()->wakeUp(room->getId()); room->getThread()->removeRoom(room); } void Server::onUserDisconnected() { - ServerPlayer *player = qobject_cast(sender()); + auto player = qobject_cast(sender()); qInfo() << "Player" << player->getId() << "disconnected"; if (players.count() <= 10) { broadcast("ServerMessage", tr("%1 logged out").arg(player->getScreenName())); } - Room *room = player->getRoom(); - if (room->isStarted()) { - if (room->getObservers().contains(player)) { - room->removeObserver(player); - player->deleteLater(); - return; - } - player->setState(Player::Offline); - player->setSocket(nullptr); - // TODO: add a robot - } else { + + auto _room = player->getRoom(); + if (_room->isLobby()) { player->setState(Player::Robot); // 大厅!然而又不能设Offline player->deleteLater(); + } else { + auto room = qobject_cast(_room); + if (room->isStarted()) { + if (room->getObservers().contains(player)) { + room->removeObserver(player); + player->deleteLater(); + return; + } + player->setState(Player::Offline); + player->setSocket(nullptr); + // TODO: add a robot + } else { + player->setState(Player::Robot); // 大厅!然而又不能设Offline + // 这里有一个多线程问题,可能与Room::gameOver同时deleteLater导致出事 + // FIXME: 这种解法肯定不安全 + if (!room->insideGameOver) + player->deleteLater(); + } } } void Server::onUserStateChanged() { ServerPlayer *player = qobject_cast(sender()); - auto room = player->getRoom(); - if (!room || room->isLobby() || room->isAbandoned()) { - return; - } + auto _room = player->getRoom(); + if (!_room || _room->isLobby()) return; + auto room = qobject_cast(_room); + if (room->isAbandoned()) return; + auto state = player->getState(); room->doBroadcastNotify(room->getPlayers(), "NetStateChanged", QString("[%1,\"%2\"]").arg(player->getId()).arg(player->getStateString())); @@ -579,33 +424,6 @@ void Server::onUserStateChanged() { } } -RSA *Server::initServerRSA() { - RSA *rsa = RSA_new(); - if (!QFile::exists("server/rsa_pub")) { - BIGNUM *bne = BN_new(); - BN_set_word(bne, RSA_F4); - RSA_generate_key_ex(rsa, 2048, bne, NULL); - - BIO *bp_pub = BIO_new_file("server/rsa_pub", "w+"); - PEM_write_bio_RSAPublicKey(bp_pub, rsa); - BIO *bp_pri = BIO_new_file("server/rsa", "w+"); - PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL); - - BIO_free_all(bp_pub); - BIO_free_all(bp_pri); - QFile("server/rsa") - .setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); - BN_free(bne); - } - FILE *keyFile = fopen("server/rsa_pub", "r"); - PEM_read_RSAPublicKey(keyFile, &rsa, NULL, NULL); - fclose(keyFile); - keyFile = fopen("server/rsa", "r"); - PEM_read_RSAPrivateKey(keyFile, &rsa, NULL, NULL); - fclose(keyFile); - return rsa; -} - #define SET_DEFAULT_CONFIG(k, v) do {\ if (config.value(k).isUndefined()) { \ config[k] = (v); \ @@ -705,28 +523,3 @@ void Server::refreshMd5() { emit p->kicked(); } } - -void Server::readPendingDatagrams() { - while (udpSocket->hasPendingDatagrams()) { - QNetworkDatagram datagram = udpSocket->receiveDatagram(); - if (datagram.isValid()) { - processDatagram(datagram.data(), datagram.senderAddress(), datagram.senderPort()); - } - } -} - -void Server::processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port) { - if (msg == "fkDetectServer") { - udpSocket->writeDatagram("me", addr, port); - } else if (msg.startsWith("fkGetDetail,")) { - udpSocket->writeDatagram(JsonArray2Bytes(QJsonArray({ - FK_VERSION, - getConfig("iconUrl"), - getConfig("description"), - getConfig("capacity"), - players.count(), - msg.sliced(12).constData(), - })), addr, port); - } - udpSocket->flush(); -} diff --git a/src/server/server.h b/src/server/server.h index f2baaa53..593195cb 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -3,17 +3,14 @@ #ifndef _SERVER_H #define _SERVER_H -#include -#include - -#include -#include +class AuthManager; class ServerSocket; class ClientSocket; class ServerPlayer; class RoomThread; +class Lobby; -#include "room.h" +#include "server/room.h" class Server : public QObject { Q_OBJECT @@ -29,7 +26,7 @@ public: int timeout = 15, const QByteArray &settings = "{}"); Room *findRoom(int id) const; - Room *lobby() const; + Lobby *lobby() const; RoomThread *createThread(); void removeThread(RoomThread *thread); @@ -37,6 +34,7 @@ public: ServerPlayer *findPlayer(int id) const; void addPlayer(ServerPlayer *player); void removePlayer(int id); + auto getPlayers() { return players; } void updateRoomList(ServerPlayer *teller); void updateOnlineInfo(); @@ -44,6 +42,8 @@ public: sqlite3 *getDatabase(); void broadcast(const QString &command, const QString &jsonData); + void sendEarlyPacket(ClientSocket *client, const QString &type, const QString &msg); + void setupPlayer(ServerPlayer *player, bool all_info = true); bool isListening; QJsonValue getConfig(const QString &command); @@ -64,7 +64,6 @@ signals: public slots: void processNewConnection(ClientSocket *client); void processRequest(const QByteArray &msg); - void readPendingDatagrams(); void onRoomAbandoned(); void onUserDisconnected(); @@ -73,9 +72,8 @@ public slots: private: friend class Shell; ServerSocket *server; - QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用 - Room *m_lobby; + Lobby *m_lobby; QMap rooms; QStack idle_rooms; QList threads; @@ -84,26 +82,13 @@ private: QHash players; QList temp_banlist; - RSA *rsa; - QString public_key; + AuthManager *auth; sqlite3 *db; QMutex transaction_mutex; QString md5; - static RSA *initServerRSA(); - QJsonObject config; void readConfig(); - - // 用于确定建立连接之前与客户端通信,连接后用doNotify - void sendEarlyPacket(ClientSocket *client, const QString &type, const QString &msg); - bool checkClientVersion(ClientSocket *client, const QString &ver); - - // 某玩家刚刚连入之后,服务器告诉他关于他的一些基本信息 - void setupPlayer(ServerPlayer *player, bool all_info = true); - void handleNameAndPassword(ClientSocket *client, const QString &name, - const QString &password, const QString &md5_str, const QString &uuid_str); - void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port); }; extern Server *ServerInstance; diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index 34089ad5..ac3825b4 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "serverplayer.h" -#include "client_socket.h" -#include "room.h" -#include "roomthread.h" -#include "router.h" -#include "server.h" +#include "server/serverplayer.h" +#include "network/client_socket.h" +#include "server/room.h" +#include "server/roomthread.h" +#include "network/router.h" +#include "server/server.h" -ServerPlayer::ServerPlayer(Room *room) { +ServerPlayer::ServerPlayer(RoomBase *room) { socket = nullptr; router = new Router(this, socket, Router::TYPE_SERVER); setState(Player::Online); @@ -61,9 +61,9 @@ void ServerPlayer::removeSocket() { Server *ServerPlayer::getServer() const { return server; } -Room *ServerPlayer::getRoom() const { return room; } +RoomBase *ServerPlayer::getRoom() const { return room; } -void ServerPlayer::setRoom(Room *room) { this->room = room; } +void ServerPlayer::setRoom(RoomBase *room) { this->room = room; } void ServerPlayer::speak(const QString &message) { ; } @@ -112,6 +112,24 @@ void ServerPlayer::kick() { setSocket(nullptr); } +void ServerPlayer::reconnect(ClientSocket *client) { + setSocket(client); + alive = true; + client->disconnect(this); + if (server->getPlayers().count() <= 10) { + server->broadcast("ServerMessage", tr("%1 backed").arg(getScreenName())); + } + + if (room && !room->isLobby()) { + server->setupPlayer(this, true); + qobject_cast(room)->pushRequest(QString("%1,reconnect").arg(getId())); + } else { + // 懒得处理掉线玩家在大厅了!踢掉得了 + doNotify("ErrorMsg", "Unknown Error"); + emit kicked(); + } +} + bool ServerPlayer::thinking() { m_thinking_mutex.lock(); bool ret = m_thinking; diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index c9b0a10a..9bbf8d2d 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -3,17 +3,18 @@ #ifndef _SERVERPLAYER_H #define _SERVERPLAYER_H -#include "player.h" +#include "core/player.h" class ClientSocket; class Router; class Server; class Room; +class RoomBase; class ServerPlayer : public Player { Q_OBJECT public: - explicit ServerPlayer(Room *room); + explicit ServerPlayer(RoomBase *room); ~ServerPlayer(); void setSocket(ClientSocket *socket); @@ -21,8 +22,8 @@ public: ClientSocket *getSocket() const; Server *getServer() const; - Room *getRoom() const; - void setRoom(Room *room); + RoomBase *getRoom() const; + void setRoom(RoomBase *room); void speak(const QString &message); @@ -37,6 +38,7 @@ public: volatile bool alive; // For heartbeat void kick(); + void reconnect(ClientSocket *socket); bool busy() const { return m_busy; } void setBusy(bool busy) { m_busy = busy; } @@ -57,7 +59,7 @@ private: ClientSocket *socket; // socket for communicating with client Router *router; Server *server; - Room *room; // Room that player is in, maybe lobby + RoomBase *room; // Room that player is in, maybe lobby bool m_busy; // (Lua专用) 是否有doRequest没处理完?见于神貂蝉这种一控多的 bool m_thinking; // 是否在烧条? QMutex m_thinking_mutex; // 注意setBusy只在Lua使用,所以不需要锁。 diff --git a/src/server/shell.cpp b/src/server/shell.cpp index c286701d..f1168adf 100644 --- a/src/server/shell.cpp +++ b/src/server/shell.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) -#include "shell.h" -#include "packman.h" -#include "server.h" -#include "serverplayer.h" -#include "util.h" +#include "server/shell.h" +#include "core/packman.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "core/util.h" #include #include #include diff --git a/src/swig/freekill-nogui.i b/src/swig/freekill-nogui.i index 726746cc..b7ebd97a 100644 --- a/src/swig/freekill-nogui.i +++ b/src/swig/freekill-nogui.i @@ -3,13 +3,13 @@ %module fk %{ -#include "server.h" -#include "serverplayer.h" -#include "clientplayer.h" -#include "room.h" -#include "roomthread.h" -#include "util.h" -#include "qmlbackend.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "client/clientplayer.h" +#include "server/room.h" +#include "server/roomthread.h" +#include "core/util.h" +#include "ui/qmlbackend.h" class ClientPlayer *Self = nullptr; %} diff --git a/src/swig/freekill.i b/src/swig/freekill.i index f8b95d85..962f9014 100644 --- a/src/swig/freekill.i +++ b/src/swig/freekill.i @@ -3,14 +3,14 @@ %module fk %{ -#include "client.h" -#include "server.h" -#include "serverplayer.h" -#include "clientplayer.h" -#include "room.h" -#include "roomthread.h" -#include "qmlbackend.h" -#include "util.h" +#include "client/client.h" +#include "server/server.h" +#include "server/serverplayer.h" +#include "client/clientplayer.h" +#include "server/room.h" +#include "server/roomthread.h" +#include "ui/qmlbackend.h" +#include "core/util.h" const char *FK_VER = FK_VERSION; %} diff --git a/src/swig/naturalvar.i b/src/swig/naturalvar.i index 9aff29f1..f83a4f84 100644 --- a/src/swig/naturalvar.i +++ b/src/swig/naturalvar.i @@ -5,7 +5,7 @@ // ------------------------------------------------------ %{ -#include +#include "ui/qmlbackend.h" %} // Lua 5.4 特有的不能pushnumber, swig迟迟不更只好手动调教 diff --git a/src/swig/server.i b/src/swig/server.i index ba72a769..97d546b5 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -1,5 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later +%nodefaultctor Server; +%nodefaultdtor Server; +class Server : public QObject { +public: + void beginTransaction(); + void endTransaction(); +}; +extern Server *ServerInstance; + %nodefaultctor Room; %nodefaultdtor Room; class Room : public QObject { @@ -14,11 +23,14 @@ public: QList getObservers() const; bool hasObserver(ServerPlayer *player) const; int getTimeout() const; + void delay(int ms); void checkAbandoned(); void updateWinRate(int id, const QString &general, const QString &mode, int result, bool dead); void gameOver(); + void setRequestTimer(int ms); + void destroyRequestTimer(); }; %extend Room { @@ -33,25 +45,26 @@ class RoomThread : public QThread { public: Room *getRoom(int id); - QString fetchRequest(); - void clearRequest(); - bool hasRequest(); + // QString fetchRequest(); + // void clearRequest(); + // bool hasRequest(); - void trySleep(int ms); - bool isTerminated() const; + // void trySleep(int ms); + // bool isTerminated() const; bool isConsoleStart() const; bool isOutdated(); }; %{ -void RoomThread::run() +#include "server/scheduler.h" +void Scheduler::tellThreadToLua() { lua_getglobal(L, "debug"); lua_getfield(L, -1, "traceback"); lua_replace(L, -2); lua_getglobal(L, "InitScheduler"); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_RoomThread, 0); + SWIG_NewPointerObj(L, m_thread, SWIGTYPE_p_RoomThread, 0); int error = lua_pcall(L, 1, 0, -2); lua_pop(L, 1); if (error) { diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index b86446a3..0f821901 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -1,27 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "qmlbackend.h" -#include -#include -#include +#include "ui/qmlbackend.h" #ifndef FK_SERVER_ONLY -#include -#include -#include +#include #include #include #include #include -#include "mod.h" +#include +// #include "mod.h" #endif #include -#include "server.h" -#include "client.h" -#include "util.h" -#include "replayer.h" +#include "server/server.h" +#include "client/client.h" +#include "core/util.h" +#include "client/replayer.h" QmlBackend *Backend = nullptr; @@ -35,6 +31,7 @@ QmlBackend::QmlBackend(QObject *parent) : QObject(parent) { udpSocket->bind(0); connect(udpSocket, &QUdpSocket::readyRead, this, &QmlBackend::readPendingDatagrams); + connect(this, &QmlBackend::dialog, this, &QmlBackend::showDialog); #endif } @@ -464,7 +461,7 @@ void QmlBackend::installAESKey() { } void QmlBackend::createModBackend() { - engine->rootContext()->setContextProperty("ModBackend", new ModMaker); + //engine->rootContext()->setContextProperty("ModBackend", new ModMaker); } @@ -549,6 +546,30 @@ void QmlBackend::readPendingDatagrams() { } } +void QmlBackend::showDialog(const QString &type, const QString &text, const QString &orig) { + static const QString title = tr("FreeKill") + " v" + FK_VERSION; + QMessageBox *box = nullptr; + if (type == "critical") { + box = new QMessageBox(QMessageBox::Critical, title, text, QMessageBox::Ok); + connect(box, &QMessageBox::buttonClicked, box, &QObject::deleteLater); + } else if (type == "info") { + box = new QMessageBox(QMessageBox::Information, title, text, QMessageBox::Ok); + connect(box, &QMessageBox::buttonClicked, box, &QObject::deleteLater); + } else if (type == "warning") { + box = new QMessageBox(QMessageBox::Warning, title, text, QMessageBox::Ok); + connect(box, &QMessageBox::buttonClicked, box, &QObject::deleteLater); + } + + if (box) { + if (!orig.isEmpty()) { + auto bytes = orig.toLocal8Bit().prepend("help: "); + if (tr(bytes) != bytes) box->setInformativeText(tr(bytes)); + } + box->setWindowModality(Qt::NonModal); + box->show(); + } +} + void QmlBackend::removeRecord(const QString &fname) { QFile::remove("recording/" + fname); } diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h index 0961c11b..53be74c4 100644 --- a/src/ui/qmlbackend.h +++ b/src/ui/qmlbackend.h @@ -64,6 +64,9 @@ public: Q_INVOKABLE void detectServer(); Q_INVOKABLE void getServerInfo(const QString &addr); + Q_INVOKABLE void showDialog(const QString &type, const QString &text, + const QString &orig = QString()); + qreal volume() const { return m_volume; } void setVolume(qreal v) { m_volume = v; } @@ -77,6 +80,7 @@ public: signals: void notifyUI(const QString &command, const QVariant &data); + void dialog(const QString &type, const QString &text, const QString &orig = QString()); void volumeChanged(qreal); void replayerToggle(); void replayerSpeedUp();