diff --git a/.gitignore b/.gitignore index 9f7c107f..4e7279dd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ freekill-wrap.cxx /server/rsa_pub /freekill.client.config.json /freekill.server.config.json +/freekill.server.error.log +/freekill.server.info.log /flist.txt # windeployqt diff --git a/Fk/Cheat/PlayerDetail.qml b/Fk/Cheat/PlayerDetail.qml index 55681380..e281fb50 100644 --- a/Fk/Cheat/PlayerDetail.qml +++ b/Fk/Cheat/PlayerDetail.qml @@ -43,12 +43,22 @@ Flickable { Button { text: Backend.translate("Give Shoe") - enabled: Math.random() < 0.5 + enabled: Math.random() < 0.3 onClicked: { root.givePresent("Shoe"); root.finish(); } } + + Button { + text: Backend.translate("Kick From Room") + visible: !roomScene.isStarted && roomScene.isOwner + enabled: pid !== Self.id + onClicked: { + ClientInstance.notifyServer("KickPlayer", pid.toString()); + root.finish(); + } + } } // TODO: player details diff --git a/Fk/LobbyElement/RoomGeneralSettings.qml b/Fk/LobbyElement/RoomGeneralSettings.qml index f0b676a4..b49a5297 100644 --- a/Fk/LobbyElement/RoomGeneralSettings.qml +++ b/Fk/LobbyElement/RoomGeneralSettings.qml @@ -4,194 +4,201 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -ColumnLayout { - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Room Name") - } - TextField { - id: roomName - maximumLength: 64 - font.pixelSize: 18 - text: Backend.translate("$RoomName").arg(Self.screenName) - } - } +Flickable { + flickableDirection: Flickable.AutoFlickIfNeeded + clip: true + contentHeight: layout.height - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Player num") - } - SpinBox { - id: playerNum - from: 2 - to: 8 - value: config.preferedPlayerNum - - onValueChanged: { - config.preferedPlayerNum = value; + ColumnLayout { + id: layout + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Room Name") + } + TextField { + id: roomName + maximumLength: 64 + font.pixelSize: 18 + text: Backend.translate("$RoomName").arg(Self.screenName) } } - } - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Game Mode") - } - ComboBox { - id: gameModeCombo - textRole: "name" - model: ListModel { - id: gameModeList + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Player num") } + SpinBox { + id: playerNum + from: 2 + to: 8 + value: config.preferedPlayerNum - onCurrentIndexChanged: { - let data = gameModeList.get(currentIndex); - playerNum.from = data.minPlayer; - playerNum.to = data.maxPlayer; - - config.preferedMode = data.orig_name; + onValueChanged: { + config.preferedPlayerNum = value; + } } } - } - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Select general num") - } - SpinBox { - id: generalNum - from: 3 - to: 18 - value: config.preferredGeneralNum - - onValueChanged: { - config.preferredGeneralNum = value; + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Game Mode") } - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Operation timeout") - } - SpinBox { - from: 10 - to: 60 - editable: true - value: config.preferredTimeout - - onValueChanged: { - config.preferredTimeout = value; - } - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Luck Card Times") - } - SpinBox { - from: 0 - to: 8 - value: config.preferredLuckTime - - onValueChanged: { - config.preferredLuckTime = value; - } - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: Backend.translate("Room Password") - } - TextField { - id: roomPassword - maximumLength: 16 - font.pixelSize: 18 - } - } - - Switch { - id: freeAssignCheck - checked: Debugging ? true : false - text: Backend.translate("Enable free assign") - } - - Switch { - id: deputyCheck - checked: Debugging ? true : false - text: Backend.translate("Enable deputy general") - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Button { - text: Backend.translate("OK") - onClicked: { - root.finished(); - mainWindow.busy = true; - - let disabledGenerals = config.disabledGenerals.slice(); - if (disabledGenerals.length) { - const availablePack = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])). - filter((pack) => !config.disabledPack.includes(pack)); - disabledGenerals = disabledGenerals.filter((general) => { - return availablePack.find((pack) => JSON.parse(Backend.callLuaFunction("GetGenerals", [pack])).includes(general)); - }); - - disabledGenerals = Array.from(new Set(disabledGenerals)); + ComboBox { + id: gameModeCombo + textRole: "name" + model: ListModel { + id: gameModeList } - ClientInstance.notifyServer( - "CreateRoom", - JSON.stringify([roomName.text, playerNum.value, config.preferredTimeout, { - enableFreeAssign: freeAssignCheck.checked, - enableDeputy: deputyCheck.checked, - gameMode: config.preferedMode, - disabledPack: config.disabledPack, - generalNum: config.preferredGeneralNum, - luckTime: config.preferredLuckTime, - password: roomPassword.text, - disabledGenerals, - }]) - ); - } - } - Button { - text: Backend.translate("Cancel") - onClicked: { - root.finished(); - } - } - } + onCurrentIndexChanged: { + let data = gameModeList.get(currentIndex); + playerNum.from = data.minPlayer; + playerNum.to = data.maxPlayer; - Component.onCompleted: { - let mode_data = JSON.parse(Backend.callLuaFunction("GetGameModes", [])); - let i = 0; - for (let d of mode_data) { - gameModeList.append(d); - if (d.orig_name == config.preferedMode) { - gameModeCombo.currentIndex = i; + config.preferedMode = data.orig_name; + } } - i += 1; } - playerNum.value = config.preferedPlayerNum; + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Select general num") + } + SpinBox { + id: generalNum + from: 3 + to: 18 + value: config.preferredGeneralNum + + onValueChanged: { + config.preferredGeneralNum = value; + } + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Operation timeout") + } + SpinBox { + from: 10 + to: 60 + editable: true + value: config.preferredTimeout + + onValueChanged: { + config.preferredTimeout = value; + } + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Luck Card Times") + } + SpinBox { + from: 0 + to: 8 + value: config.preferredLuckTime + + onValueChanged: { + config.preferredLuckTime = value; + } + } + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Text { + text: Backend.translate("Room Password") + } + TextField { + id: roomPassword + maximumLength: 16 + font.pixelSize: 18 + } + } + + Switch { + id: freeAssignCheck + checked: Debugging ? true : false + text: Backend.translate("Enable free assign") + } + + Switch { + id: deputyCheck + checked: Debugging ? true : false + text: Backend.translate("Enable deputy general") + } + + RowLayout { + anchors.rightMargin: 8 + spacing: 16 + Button { + text: Backend.translate("OK") + onClicked: { + root.finished(); + mainWindow.busy = true; + + let disabledGenerals = config.disabledGenerals.slice(); + if (disabledGenerals.length) { + const availablePack = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])). + filter((pack) => !config.disabledPack.includes(pack)); + disabledGenerals = disabledGenerals.filter((general) => { + return availablePack.find((pack) => JSON.parse(Backend.callLuaFunction("GetGenerals", [pack])).includes(general)); + }); + + disabledGenerals = Array.from(new Set(disabledGenerals)); + } + + ClientInstance.notifyServer( + "CreateRoom", + JSON.stringify([roomName.text, playerNum.value, config.preferredTimeout, { + enableFreeAssign: freeAssignCheck.checked, + enableDeputy: deputyCheck.checked, + gameMode: config.preferedMode, + disabledPack: config.disabledPack, + generalNum: config.preferredGeneralNum, + luckTime: config.preferredLuckTime, + password: roomPassword.text, + disabledGenerals, + }]) + ); + } + } + Button { + text: Backend.translate("Cancel") + onClicked: { + root.finished(); + } + } + } + + Component.onCompleted: { + let mode_data = JSON.parse(Backend.callLuaFunction("GetGameModes", [])); + let i = 0; + for (let d of mode_data) { + gameModeList.append(d); + if (d.orig_name == config.preferedMode) { + gameModeCombo.currentIndex = i; + } + i += 1; + } + + playerNum.value = config.preferedPlayerNum; + } } } diff --git a/Fk/Pages/PackageManage.qml b/Fk/Pages/PackageManage.qml index 6c15ab13..d008250d 100644 --- a/Fk/Pages/PackageManage.qml +++ b/Fk/Pages/PackageManage.qml @@ -6,195 +6,170 @@ import QtQuick.Layouts Item { id: root - Button { - text: qsTr("Quit") - anchors.right: parent.right - onClicked: { - mainStack.pop(); - } - } - Component { - id: packageDelegate - - Item { - height: 22 - width: packageList.width - - RowLayout { - anchors.fill: parent - spacing: 16 - Text { - font.pixelSize: 20 - text: pkgName - } - - Text { - font.pixelSize: 20 - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: pkgURL - } - - Text { - font.pixelSize: 20 - text: pkgVersion - } - - Text { - font.pixelSize: 20 - color: pkgEnabled === "1" ? "green" : "red" - text: pkgEnabled === "1" ? qsTr("Enabled") : qsTr("Disabled") - } + ToolBar { + id: bar + width: parent.width + RowLayout { + anchors.fill: parent + ToolButton { + icon.source: AppPath + "/image/modmaker/back" + onClicked: mainStack.pop(); } + Label { + text: qsTr("Package Manager") + horizontalAlignment: Qt.AlignHCenter + Layout.fillWidth: true + } + ToolButton { + icon.source: AppPath + "/image/modmaker/menu" + onClicked: menu.open() - TapHandler { - onTapped: { - if (packageList.currentIndex === index) { - packageList.currentIndex = -1; - } else { - packageList.currentIndex = index; + Menu { + id: menu + y: bar.height + + MenuItem { + text: qsTr("Enable All") + onTriggered: { + for (let i = 0; i < packageModel.count; i++) { + let name = packageModel.get(i).pkgName; + Pacman.enablePack(name); + } + updatePackageList(); + } + } + MenuItem { + text: qsTr("Disable All") + onTriggered: { + for (let i = 0; i < packageModel.count; i++) { + let name = packageModel.get(i).pkgName; + Pacman.disablePack(name); + } + updatePackageList(); + } + } + MenuItem { + text: qsTr("Upgrade All") + onTriggered: { + for (let i = 0; i < packageModel.count; i++) { + let name = packageModel.get(i).pkgName; + Pacman.upgradePack(name); + } + updatePackageList(); + } } } } } } - ListModel { - id: packageModel - } + Rectangle { + width: parent.width + height: parent.height - bar.height - urlInstaller.height + anchors.top: bar.bottom + color: "snow" + opacity: 0.75 + clip: true - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.fillHeight: true - Layout.alignment: Qt.AlignHCenter - Item { - Layout.preferredWidth: root.width * 0.9 - Layout.fillHeight: true - Rectangle { - anchors.fill: parent - color: "#88EEEEEE" + ListView { + id: packageList + anchors.fill: parent + model: ListModel { + id: packageModel } - ListView { - id: packageList - anchors.fill: parent + delegate: ItemDelegate { + width: root.width + height: 64 - contentHeight: packageDelegate.height * count - ScrollBar.vertical: ScrollBar {} - header: RowLayout { - height: 22 - width: packageList.width - spacing: 16 + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 Text { - font.pixelSize: 20 - text: qsTr("Name") + text: "" + pkgName + " (" + pkgVersion + ")" + font.pixelSize: 18 + textFormat: Text.RichText + color: pkgEnabled === "1" ? "black" : "grey" } - Text { - font.pixelSize: 20 - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: "URL" - } - - Text { - font.pixelSize: 20 - text: qsTr("Version") - } - - Text { - font.pixelSize: 20 - text: qsTr("Enable") + text: pkgURL + color: pkgEnabled === "1" ? "black" : "grey" } } - delegate: packageDelegate - model: packageModel - highlight: Rectangle { color: "lightsteelblue"; radius: 5 } - Component.onCompleted: { currentIndex = -1; } - } - } - ColumnLayout { - Button { - enabled: packageList.currentItem - text: qsTr("Enable") - onClicked: { - let idx = packageList.currentIndex; - let name = packageModel.get(idx).pkgName; - Pacman.enablePack(name); - updatePackageList(); - packageList.currentIndex = idx; + Button { + id: enableBtn + text: pkgEnabled === "0" ? qsTr("Enable") : qsTr("Disable") + anchors.right: upgradeBtn.left + anchors.rightMargin: 8 + onClicked: { + if (pkgEnabled === "0") { + Pacman.enablePack(pkgName); + } else { + Pacman.disablePack(pkgName); + } + updatePackageList(); + } } - } - Button { - enabled: packageList.currentItem - text: qsTr("Disable") - onClicked: { - let idx = packageList.currentIndex; - let name = packageModel.get(idx).pkgName; - Pacman.disablePack(name); - updatePackageList(); - packageList.currentIndex = idx; + + Button { + id: upgradeBtn + text: qsTr("Upgrade") + anchors.right: delBtn.left + anchors.rightMargin: 8 + onClicked: { + Pacman.upgradePack(pkgName); + updatePackageList(); + } } - } - Button { - enabled: packageList.currentItem - text: qsTr("Upgrade") - onClicked: { - let idx = packageList.currentIndex; - let name = packageModel.get(idx).pkgName; - Pacman.upgradePack(name); - updatePackageList(); - packageList.currentIndex = idx; + + Button { + id: delBtn + text: qsTr("Remove") + anchors.right: parent.right + anchors.rightMargin: 8 + onClicked: { + Pacman.removePack(pkgName); + updatePackageList(); + } } - } - Button { - enabled: packageList.currentItem - text: qsTr("Remove") + onClicked: { - let idx = packageList.currentIndex; - let name = packageModel.get(idx).pkgName; - Pacman.removePack(name); - updatePackageList(); - packageList.currentIndex = idx; - } - } - Button { - enabled: packageList.currentItem - text: qsTr("Copy URL") - onClicked: { - let idx = packageList.currentIndex; - let name = packageModel.get(idx).pkgURL; - Backend.copyToClipboard(name); - toast.show(qsTr("Copied.")); + Backend.copyToClipboard(pkgURL); + toast.show(qsTr("Copied %1.").arg(pkgURL)); } } } } - RowLayout { - Layout.fillWidth: true - TextField { - id: urlEdit - Layout.fillWidth: true - clip: true - } + Rectangle { + id: urlInstaller + width: parent.width + height: childrenRect.height + color: "snow" + opacity: 0.75 + anchors.bottom: parent.bottom - Button { - text: qsTr("Install From URL") - enabled: urlEdit.text !== "" - onClicked: { - let url = urlEdit.text; - mainWindow.busy = true; - Pacman.downloadNewPack(url, true); + RowLayout { + width: parent.width + TextField { + id: urlEdit + Layout.fillWidth: true + clip: true + } + + Button { + text: qsTr("Install From URL") + enabled: urlEdit.text !== "" + onClicked: { + let url = urlEdit.text; + mainWindow.busy = true; + Pacman.downloadNewPack(url, true); + } } } } - } - function updatePackageList() { packageModel.clear(); let data = JSON.parse(Pacman.listPackages()); diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 921022ab..1c0e4d9a 100644 --- a/Fk/Pages/Room.qml +++ b/Fk/Pages/Room.qml @@ -17,6 +17,9 @@ Item { property bool isOwner: false property bool isStarted: false + property bool isFull: false + property bool isAllReady: false + property bool isReady: false property alias popupBox: popupBox property alias manualBox: manualBox @@ -79,13 +82,36 @@ Item { } } Button { - text: "add robot" - visible: isOwner && !isStarted + text: Backend.translate("Add Robot") + visible: isOwner && !isStarted && !isFull anchors.centerIn: parent onClicked: { ClientInstance.notifyServer("AddRobot", "[]"); } } + Button { + text: Backend.translate("Start Game") + visible: isOwner && !isStarted && isFull + enabled: isAllReady + anchors.centerIn: parent + onClicked: { + ClientInstance.notifyServer("StartGame", "[]"); + } + } + Timer { + id: opTimer + interval: 1000 + } + Button { + text: isReady ? Backend.translate("Cancel Ready") : Backend.translate("Ready") + visible: !isOwner && !isStarted + enabled: !opTimer.running + anchors.centerIn: parent + onClicked: { + opTimer.start(); + ClientInstance.notifyServer("Ready", ""); + } + } states: [ State { name: "notactive" }, // Normal status @@ -195,6 +221,7 @@ Item { Photo { playerid: model.id general: model.general + avatar: model.avatar deputyGeneral: model.deputyGeneral screenName: model.screenName role: model.role @@ -210,6 +237,7 @@ Item { chained: model.chained drank: model.drank isOwner: model.isOwner + ready: model.ready onSelectedChanged: { Logic.updateSelectedTargets(playerid, selected); @@ -830,6 +858,36 @@ Item { cheatDrawer.open(); } + function resetToInit() { + let datalist = []; + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (item.id > 0) { + datalist.push({ + id: item.id, + avatar: item.avatar, + name: item.screenName, + isOwner: item.isOwner, + ready: item.ready, + }); + } + } + mainStack.pop(); + mainStack.push(room); + mainStack.currentItem.loadPlayerData(datalist); + } + + function loadPlayerData(datalist) { + datalist.forEach(d => { + if (d.id == Self.id) { + roomScene.isOwner = d.isOwner; + } else { + callbacks["AddPlayer"](JSON.stringify([d.id, d.name, d.avatar, d.ready])); + } + Logic.getPhotoModel(d.id).isOwner = d.isOwner; + }); + } + Component.onCompleted: { toast.show(Backend.translate("$EnterRoom")); playerNum = config.roomCapacity; @@ -839,6 +897,7 @@ Item { id: i ? -1 : Self.id, index: i, // For animating seat swap general: i ? "" : Self.avatar, + avatar: i ? "" : Self.avatar, deputyGeneral: "", screenName: i ? "" : Self.screenName, role: "unknown", @@ -853,7 +912,8 @@ Item { faceup: true, chained: false, drank: 0, - isOwner: false + isOwner: false, + ready: false, }); } diff --git a/Fk/Pages/RoomLogic.js b/Fk/Pages/RoomLogic.js index e3334230..61befd6e 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -334,7 +334,7 @@ function changeSelf(id) { } callbacks["AddPlayer"] = function(jsonData) { - // jsonData: int id, string screenName, string avatar + // jsonData: int id, string screenName, string avatar, bool ready for (let i = 0; i < photoModel.count; i++) { let item = photoModel.get(i); if (item.id === -1) { @@ -342,9 +342,22 @@ callbacks["AddPlayer"] = function(jsonData) { let uid = data[0]; let name = data[1]; let avatar = data[2]; + let ready = data[3]; + item.id = uid; item.screenName = name; item.general = avatar; + item.avatar = avatar; + item.ready = ready; + + checkAllReady(); + + if (getPhoto(-1)) { + roomScene.isFull = false; + } else { + roomScene.isFull = true; + } + return; } } @@ -485,6 +498,8 @@ callbacks["RemovePlayer"] = function(jsonData) { model.id = -1; model.screenName = ""; model.general = ""; + model.isOwner = false; + roomScene.isFull = false; } } @@ -492,9 +507,7 @@ callbacks["RoomOwner"] = function(jsonData) { // jsonData: int uid of the owner let uid = JSON.parse(jsonData)[0]; - if (Self.id === uid) { - roomScene.isOwner = true; - } + roomScene.isOwner = (Self.id === uid); let model = getPhotoModel(uid); if (typeof(model) !== "undefined") { @@ -502,6 +515,46 @@ callbacks["RoomOwner"] = function(jsonData) { } } +function checkAllReady() { + let allReady = true; + for (let i = 0; i < photoModel.count; i++) { + let item = photoModel.get(i); + if (!item.isOwner && !item.ready) { + allReady = false; + break; + } + } + roomScene.isAllReady = allReady; +} + +callbacks["ReadyChanged"] = (j) => { + const data = JSON.parse(j); + const id = data[0]; + const ready = data[1]; + + if (id === Self.id) { + roomScene.isReady = ready === 1; + } + + let model = getPhotoModel(id); + if (typeof(model) !== "undefined") { + model.ready = ready ? true : false; + checkAllReady(); + } +} + +callbacks["NetStateChanged"] = (j) => { + const data = JSON.parse(j); + const id = data[0]; + let state = data[1]; + + let model = getPhotoModel(id); + if (state == "run" && model.dead) { + state = "leave"; + } + model.netstate = state; +} + callbacks["PropertyUpdate"] = function(jsonData) { // jsonData: int id, string property_name, value let data = JSON.parse(jsonData); @@ -510,6 +563,7 @@ callbacks["PropertyUpdate"] = function(jsonData) { let value = data[2]; let model = getPhotoModel(uid); + if (typeof(model) !== "undefined") { model[property_name] = value; } @@ -520,6 +574,7 @@ callbacks["StartGame"] = function(jsonData) { for (let i = 0; i < photoModel.count; i++) { let item = photoModel.get(i); + item.ready = false; item.general = ""; } } @@ -1013,6 +1068,12 @@ callbacks["LogEvent"] = function(jsonData) { Backend.playSound("./audio/system/losehp"); break; } + case "ChangeMaxHp": { + if (data.num < 0) { + Backend.playSound("./audio/system/losemaxhp"); + } + break; + } case "PlaySkillSound": { let skill = data.name; let extension = data.extension; @@ -1042,7 +1103,7 @@ callbacks["GameOver"] = function(jsonData) { roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GameOverBox.qml"); let box = roomScene.popupBox.item; box.winner = jsonData; - roomScene.isStarted = false; + // roomScene.isStarted = false; } callbacks["FillAG"] = (j) => { diff --git a/Fk/PhotoElement/DelayedTrickArea.qml b/Fk/PhotoElement/DelayedTrickArea.qml index b1db9b20..37a630f5 100644 --- a/Fk/PhotoElement/DelayedTrickArea.qml +++ b/Fk/PhotoElement/DelayedTrickArea.qml @@ -25,7 +25,7 @@ Item { Image { height: 55 * 0.8 width: 47 * 0.8 - source: SkinBank.DELAYED_TRICK_DIR + name + source: SkinBank.getDelayedTrickPicture(name) // SkinBank.DELAYED_TRICK_DIR + name } } } diff --git a/Fk/RoomElement/ChooseGeneralBox.qml b/Fk/RoomElement/ChooseGeneralBox.qml index bbf4c924..71cf7f9c 100644 --- a/Fk/RoomElement/ChooseGeneralBox.qml +++ b/Fk/RoomElement/ChooseGeneralBox.qml @@ -18,7 +18,8 @@ GraphicsBox { } id: root - title.text: Backend.translate("$ChooseGeneral").arg(choiceNum) + title.text: Backend.translate("$ChooseGeneral").arg(choiceNum) + + (config.enableFreeAssign ? "(" + Backend.translate("Enable free assign") + ")" : "") width: generalArea.width + body.anchors.leftMargin + body.anchors.rightMargin height: body.implicitHeight + body.anchors.topMargin + body.anchors.bottomMargin diff --git a/Fk/RoomElement/GameOverBox.qml b/Fk/RoomElement/GameOverBox.qml index 79af281e..d7b74e34 100644 --- a/Fk/RoomElement/GameOverBox.qml +++ b/Fk/RoomElement/GameOverBox.qml @@ -22,6 +22,16 @@ GraphicsBox { color: "#E4D5A0" } + MetroButton { + text: Backend.translate("Back To Room") + anchors.horizontalCenter: parent.horizontalCenter + + onClicked: { + roomScene.resetToInit(); + finished(); + } + } + MetroButton { text: Backend.translate("Back To Lobby") anchors.horizontalCenter: parent.horizontalCenter diff --git a/Fk/RoomElement/Photo.qml b/Fk/RoomElement/Photo.qml index c8792fab..30831920 100644 --- a/Fk/RoomElement/Photo.qml +++ b/Fk/RoomElement/Photo.qml @@ -13,6 +13,7 @@ Item { scale: 0.75 property int playerid: 0 property string general: "" + property string avatar: "" property string deputyGeneral: "" property string screenName: "" property string role: "unknown" @@ -29,6 +30,7 @@ Item { property bool chained: false property int drank: 0 property bool isOwner: false + property bool ready: false property int distance: 0 property string status: "normal" property int maxCard: 0 @@ -249,7 +251,7 @@ Item { anchors.right: parent.right anchors.bottomMargin: 8 anchors.rightMargin: 4 - source: SkinBank.PHOTO_DIR + (isOwner ? "owner" : "ready") + source: SkinBank.PHOTO_DIR + (isOwner ? "owner" : (ready ? "ready" : "notready")) visible: screenName != "" && !roomScene.isStarted } @@ -342,6 +344,8 @@ Item { source: SkinBank.STATE_DIR + root.netstate x: photoMask.x y: photoMask.y + scale: 0.9 + transformOrigin: Item.TopLeft } Image { @@ -596,6 +600,10 @@ Item { } function showDetail() { + if (playerid === 0 || playerid === -1) { + return; + } + roomScene.startCheat("PlayerDetail", { photo: this }); } } diff --git a/Fk/skin-bank.js b/Fk/skin-bank.js index b7db7a3b..f1c7c875 100644 --- a/Fk/skin-bank.js +++ b/Fk/skin-bank.js @@ -53,6 +53,22 @@ function getCardPicture(cidOrName) { return CARD_DIR + "unknown.png"; } +function getDelayedTrickPicture(name) { + let extension = Backend.callLuaFunction("GetCardExtensionByName", [name]); + + let path = AppPath + "/packages/" + extension + "/image/card/delayedTrick/" + name + ".png"; + if (Backend.exists(path)) { + return path; + } else { + for (let dir of Backend.ls(AppPath + "/packages/")) { + path = AppPath + "/packages/" + dir + "/image/card/delayedTrick/" + name + ".png"; + if (Backend.exists(path)) return path; + } + } + return DELAYED_TRICK_DIR + "unknown.png"; +} + + function getEquipIcon(cid, icon) { let data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); let extension = data.extension; diff --git a/audio/system/losemaxhp.mp3 b/audio/system/losemaxhp.mp3 new file mode 100644 index 00000000..e28f1296 Binary files /dev/null and b/audio/system/losemaxhp.mp3 differ diff --git a/image/card/delayedTrick/lightning.png b/image/card/delayedTrick/unknown.png similarity index 100% rename from image/card/delayedTrick/lightning.png rename to image/card/delayedTrick/unknown.png diff --git a/image/photo/notready.png b/image/photo/notready.png new file mode 100644 index 00000000..c0774756 Binary files /dev/null and b/image/photo/notready.png differ diff --git a/image/photo/state/run.png b/image/photo/state/run.png new file mode 100644 index 00000000..d04b605a Binary files /dev/null and b/image/photo/state/run.png differ diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts index b6d68107..fb4ccdf5 100644 --- a/lang/zh_CN.ts +++ b/lang/zh_CN.ts @@ -15,7 +15,59 @@ Socket access error - 套接字访问错误 + 无权访问套接字文件 + + + Socket resource error + 系统资源不足 + + + Socket timeout error + 连接超时 + + + Datagram too large error + 报文过长 + + + Network error + 网络错误 + + + Unsupprted socket operation + 不支持的套接字操作 + + + Unfinished socket operation + 未完成的套接字操作 + + + Proxy auth error + 代理服务器认证失败 + + + Proxy refused + 代理服务器拒绝连接 + + + Proxy closed + 代理服务器已关闭连接 + + + Proxy timeout + 代理服务器连接超时 + + + Proxy protocol error + 代理服务器协议错误 + + + Operation error + 不允许的操作 + + + Temporary error + 网络暂时出现故障,请稍后重试 Unknown error @@ -187,14 +239,14 @@ no such room 房间不存在 + + you have been banned! + 你已经被该服务器封禁! + PackageManage - - Quit - 退出 - Install From URL 从URL安装 @@ -207,14 +259,6 @@ Disable 禁用 - - Enabled - 已启用 - - - Disabled - 已禁用 - Remove 删除 @@ -224,20 +268,24 @@ 更新 - Name - 名称 + Copied %1. + 已复制到剪贴板。(%1) - Version - 版本 + Package Manager + 新月杀拓展包管理器 - Copy URL - 复制URL + Enable All + 全部启用 - Copied. - 已复制。 + Disable All + 全部禁用 + + + Upgrade All + 全部更新 diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 864153a8..219d6d21 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -34,6 +34,10 @@ Fk:loadTranslationTable{ ["Luck Card Times"] = "手气卡次数", ["Room Password"] = "房间密码", ["Please input room's password"] = "请输入房间的密码", + ["Add Robot"] = "添加机器人", + ["Start Game"] = "开始游戏", + ["Ready"] = "准备", + ["Cancel Ready"] = "取消准备", ["Game Mode"] = "游戏模式", ["Enable free assign"] = "自由选将", ["Enable deputy general"] = "启用副将机制", @@ -45,6 +49,7 @@ Fk:loadTranslationTable{ ["Give Flower"] = "送花", ["Give Egg"] = "砸蛋", ["Give Shoe"] = "拖鞋", + ["Kick From Room"] = "踢出房间", ["$OnlineInfo"] = "大厅人数:%1,总在线人数:%2", @@ -206,6 +211,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["$GameOver"] = "游戏结束", ["$Winner"] = "%1 获胜", ["$NoWinner"] = "平局!", + ["Back To Room"] = "回到房间", ["Back To Lobby"] = "返回大厅", ["Bulletin Info"] = [==[

v0.2.0 更新说明

diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index 95c4b6c5..9be010a2 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -102,12 +102,23 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self) break end - if table.every(room:getTag("LuckCardData").playerList, function(id) - return room:getTag("LuckCardData")[id].luckTime == 0 + -- local ldata = room:getTag("LuckCardData") + local ldata = luck_data + + if table.every(ldata.playerList, function(id) + return ldata[id].luckTime == 0 end) then break end + for _, id in ipairs(ldata.playerList) do + if room:getPlayerById(id)._splayer:getStateString() ~= "online" then + ldata[id].luckTime = 0 + end + end + + -- room:setTag("LuckCardData", ldata) + checkNoHuman(room) coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) diff --git a/lua/server/events/hp.lua b/lua/server/events/hp.lua index c01bdc71..5bb04226 100644 --- a/lua/server/events/hp.lua +++ b/lua/server/events/hp.lua @@ -242,6 +242,10 @@ GameEvent.functions[GameEvent.ChangeMaxHp] = function(self) player.maxHp = math.max(player.maxHp + num, 0) self:broadcastProperty(player, "maxHp") + self:sendLogEvent("ChangeMaxHp", { + player = player.id, + num = num, + }) if player.maxHp == 0 then self:killPlayer({ who = player.id }) end diff --git a/lua/server/request.lua b/lua/server/request.lua index c5c6cca8..051caa62 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -70,7 +70,9 @@ end local request_handlers = {} request_handlers["reconnect"] = function(room, id, reqlist) local p = room:getPlayerById(id) - p:reconnect() + if p then + p:reconnect() + end end request_handlers["observe"] = function(room, id, reqlist) diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 7930407a..b78f3be3 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -102,6 +102,11 @@ local function _waitForReply(player, timeout) local start = os.getms() local state = player.serverplayer:getStateString() if state ~= "online" then + if state ~= "robot" then + checkNoHuman(player.room) + player.room:delay(500) + return "__cancel" + end -- Let AI make reply. First handle request local ret_msg = true while ret_msg do @@ -259,6 +264,12 @@ function ServerPlayer:marshal(player) end end + for k, v in pairs(self.skillUsedHistory) do + if v[1] > 0 then + player:doNotify("AddSkillUseHistory", json.encode{self.id, k, v[1]}) + end + end + if self.role_shown then room:notifyProperty(player, self, "role") end diff --git a/image/card/delayedTrick/supply_shortage.png b/packages/maneuvering/image/card/delayedTrick/supply_shortage.png similarity index 100% rename from image/card/delayedTrick/supply_shortage.png rename to packages/maneuvering/image/card/delayedTrick/supply_shortage.png diff --git a/image/card/delayedTrick/indulgence.png b/packages/standard_cards/image/card/delayedTrick/indulgence.png similarity index 100% rename from image/card/delayedTrick/indulgence.png rename to packages/standard_cards/image/card/delayedTrick/indulgence.png diff --git a/packages/standard_cards/image/card/delayedTrick/lightning.png b/packages/standard_cards/image/card/delayedTrick/lightning.png new file mode 100644 index 00000000..0c0f0c37 Binary files /dev/null and b/packages/standard_cards/image/card/delayedTrick/lightning.png differ diff --git a/src/client/client.cpp b/src/client/client.cpp index 7b0afc9e..909d0186 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -5,8 +5,8 @@ #include "clientplayer.h" #include "util.h" -Client *ClientInstance; -ClientPlayer *Self; +Client *ClientInstance = nullptr; +ClientPlayer *Self = nullptr; static ClientPlayer dummyPlayer(0, nullptr); diff --git a/src/core/packman.cpp b/src/core/packman.cpp index e9be22fc..2658038a 100644 --- a/src/core/packman.cpp +++ b/src/core/packman.cpp @@ -82,10 +82,16 @@ void PackMan::loadSummary(const QString &jsonData, bool useThread) { } void PackMan::downloadNewPack(const QString &url, bool useThread) { + static auto sql_select = QString("SELECT name FROM packages \ + WHERE name = '%1';"); + static auto sql_update = QString("INSERT INTO packages (name,url,hash,enabled) \ + VALUES ('%1','%2','%3',1);"); + auto threadFunc = [=]() { int error = clone(url); - if (error < 0) - return; + // if (error < 0) + // return; + auto u = url; while (u.endsWith('/')) { u.chop(1); @@ -94,15 +100,11 @@ void PackMan::downloadNewPack(const QString &url, bool useThread) { if (fileName.endsWith(".git")) fileName.chop(4); - auto result = SelectFromDatabase(db, QString("SELECT name FROM packages \ - WHERE name = '%1';") - .arg(fileName)); + auto result = SelectFromDatabase(db, sql_select.arg(fileName)); if (result.isEmpty()) { - ExecSQL(db, QString("INSERT INTO packages (name,url,hash,enabled) \ - VALUES ('%1','%2','%3',1);") - .arg(fileName) + ExecSQL(db, sql_update.arg(fileName) .arg(url) - .arg(head(fileName))); + .arg(error < 0 ? "XXXXXXXX" : head(fileName))); } }; if (useThread) { diff --git a/src/main.cpp b/src/main.cpp index 3188818a..2c1c5612 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -97,12 +97,32 @@ static void prepareForLinux() { } #endif +static FILE *info_log = nullptr; +static FILE *err_log = nullptr; + void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { auto date = QDate::currentDate(); + + FILE *file; + switch (type) { + case QtDebugMsg: + case QtInfoMsg: + file = info_log; + break; + case QtWarningMsg: + case QtCriticalMsg: + case QtFatalMsg: + file = err_log; + break; + } + fprintf(stderr, "\r%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, "%s ", + QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); auto localMsg = msg.toUtf8(); auto threadName = QThread::currentThread()->objectName().toLatin1(); @@ -111,19 +131,27 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, case QtDebugMsg: fprintf(stderr, "%s[D] %s\n", threadName.constData(), localMsg.constData()); + fprintf(file, "%s[D] %s\n", threadName.constData(), + localMsg.constData()); break; case QtInfoMsg: fprintf(stderr, "%s[%s] %s\n", threadName.constData(), Color("I", Green).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "I", localMsg.constData()); break; case QtWarningMsg: fprintf(stderr, "%s[%s] %s\n", threadName.constData(), Color("W", Yellow, Bold).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "W", localMsg.constData()); break; case QtCriticalMsg: fprintf(stderr, "%s[%s] %s\n", threadName.constData(), Color("C", Red, Bold).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "C", localMsg.constData()); #ifndef FK_SERVER_ONLY if (Backend != nullptr) { Backend->notifyUI( @@ -135,6 +163,8 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, case QtFatalMsg: fprintf(stderr, "%s[%s] %s\n", threadName.constData(), Color("E", Red, Bold).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "E", localMsg.constData()); break; } } @@ -143,6 +173,19 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, int main(int argc, char *argv[]) { // 初始化一下各种杂项信息 QThread::currentThread()->setObjectName("Main"); + if (!info_log) { + info_log = fopen("freekill.server.info.log", "a+"); + if (!info_log) { + qFatal("Cannot open info.log"); + } + } + if (!err_log) { + err_log = fopen("freekill.server.error.log", "a+"); + if (!err_log) { + qFatal("Cannot open error.log"); + } + } + qInstallMessageHandler(fkMsgHandler); QCoreApplication *app; QCoreApplication::setApplicationName("FreeKill"); @@ -313,6 +356,15 @@ int main(int argc, char *argv[]) { ); #endif + if (info_log) { + fclose(info_log); + info_log = nullptr; + } + if (err_log) { + fclose(err_log); + info_log = nullptr; + } + return ret; #endif } diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index b5bfcd7b..de033e4b 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -2,6 +2,7 @@ #include "client_socket.h" #include +#include #include ClientSocket::ClientSocket() : socket(new QTcpSocket(this)) { @@ -96,8 +97,45 @@ void ClientSocket::raiseError(QAbstractSocket::SocketError socket_error) { case QAbstractSocket::SocketAccessError: reason = tr("Socket access error"); break; + case QAbstractSocket::SocketResourceError: + reason = tr("Socket resource error"); + break; + case QAbstractSocket::SocketTimeoutError: + reason = tr("Socket timeout error"); + break; + case QAbstractSocket::DatagramTooLargeError: + reason = tr("Datagram too large error"); + break; case QAbstractSocket::NetworkError: - return; // this error is ignored ... + reason = tr("Network error"); + break; + case QAbstractSocket::UnsupportedSocketOperationError: + reason = tr("Unsupprted socket operation"); + break; + case QAbstractSocket::UnfinishedSocketOperationError: + reason = tr("Unfinished socket operation"); + break; + case QAbstractSocket::ProxyAuthenticationRequiredError: + reason = tr("Proxy auth error"); + break; + case QAbstractSocket::ProxyConnectionRefusedError: + reason = tr("Proxy refused"); + break; + case QAbstractSocket::ProxyConnectionClosedError: + reason = tr("Proxy closed"); + break; + case QAbstractSocket::ProxyConnectionTimeoutError: + reason = tr("Proxy timeout"); + break; + case QAbstractSocket::ProxyProtocolError: + reason = tr("Proxy protocol error"); + break; + case QAbstractSocket::OperationError: + reason = tr("Operation error"); + break; + case QAbstractSocket::TemporaryError: + reason = tr("Temporary error"); + break; default: reason = tr("Unknown error"); break; diff --git a/src/network/router.cpp b/src/network/router.cpp index d18e1e39..91f73109 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -276,6 +276,16 @@ void Router::handlePacket(const QByteArray &rawPacket) { room->removePlayer(player); } else if (command == "AddRobot") { room->addRobot(player); + } else if (command == "KickPlayer") { + int i = jsonData.toInt(); + auto p = room->findPlayer(i); + if (p) room->removePlayer(p); + } else if (command == "Ready") { + player->setReady(!player->isReady()); + room->doBroadcastNotify(room->getPlayers(), "ReadyChanged", + QString("[%1,%2]").arg(player->getId()).arg(player->isReady())); + } else if (command == "StartGame") { + room->manuallyStart(); } else if (command == "Chat") { room->chat(player, jsonData); } else if (command == "PushRequest") { diff --git a/src/server/room.cpp b/src/server/room.cpp index 6b7281d8..4ef74600 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -114,6 +114,7 @@ void Room::addPlayer(ServerPlayer *player) { jsonData << player->getId(); jsonData << player->getScreenName(); jsonData << player->getAvatar(); + jsonData << player->isReady(); doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData)); } @@ -121,7 +122,13 @@ void Room::addPlayer(ServerPlayer *player) { player->setRoom(this); if (isLobby()) { - player->doNotify("EnterLobby", "[]"); + // 有机器人进入大厅(可能因为被踢),那么改为销毁 + if (player->getState() == Player::Robot) { + removePlayer(player); + player->deleteLater(); + } else { + player->doNotify("EnterLobby", "[]"); + } } else { // Second, let the player enter room and add other players jsonData = QJsonArray(); @@ -135,6 +142,7 @@ void Room::addPlayer(ServerPlayer *player) { jsonData << p->getId(); jsonData << p->getScreenName(); jsonData << p->getAvatar(); + jsonData << p->isReady(); player->doNotify("AddPlayer", JsonArray2Bytes(jsonData)); } @@ -144,8 +152,9 @@ void Room::addPlayer(ServerPlayer *player) { player->doNotify("RoomOwner", JsonArray2Bytes(jsonData)); } - if (isFull() && !gameStarted) - start(); + // 玩家手动启动 + // if (isFull() && !gameStarted) + // start(); } emit playerAdded(player); } @@ -159,6 +168,7 @@ void Room::addRobot(ServerPlayer *player) { robot->setId(robot_id); robot->setAvatar("guanyu"); robot->setScreenName(QString("COMP-%1").arg(robot_id)); + robot->setReady(true); robot_id--; // FIXME: 会触发Lobby:removePlayer @@ -174,7 +184,7 @@ void Room::removePlayer(ServerPlayer *player) { if (!gameStarted) { // 游戏还没开始的话,直接删除这名玩家 - if (players.contains(player)) { + if (players.contains(player) && !players.isEmpty()) { players.removeOne(player); } emit playerRemoved(player); @@ -213,9 +223,13 @@ void Room::removePlayer(ServerPlayer *player) { } // 如果房间空了,就把房间标为废弃,Server有信号处理函数的 - if (isAbandoned() && !m_abandoned) { + if (isAbandoned()) { + bool tmp = m_abandoned; m_abandoned = true; - emit abandoned(); + // 只释放一次信号就行了,他销毁机器人的时候会多次调用removePlayer + if (!tmp) { + emit abandoned(); + } } else if (player == owner) { setOwner(players.first()); } @@ -377,9 +391,19 @@ void Room::gameOver() { } // 旁观者不能在这清除,因为removePlayer逻辑不一样 // observers.clear(); - players.clear(); + // 玩家也不能在这里清除,因为要能返回原来房间继续玩呢 + // players.clear(); + // owner = nullptr; clearRequest(); - owner = nullptr; +} + +void Room::manuallyStart() { + if (isFull() && !gameStarted) { + foreach (auto p, players) { + p->setReady(false); + } + start(); + } } QString Room::fetchRequest() { diff --git a/src/server/room.h b/src/server/room.h index 3a28269e..6f33135f 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -59,6 +59,7 @@ class Room : public QThread { void initLua(); void roomStart(); + void manuallyStart(); LuaFunction startGame; QString fetchRequest(); diff --git a/src/server/server.cpp b/src/server/server.cpp index b5942485..cad23589 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -19,7 +19,7 @@ #include "serverplayer.h" #include "util.h" -Server *ServerInstance; +Server *ServerInstance = nullptr; Server::Server(QObject *parent) : QObject(parent) { ServerInstance = this; @@ -134,22 +134,36 @@ void Server::addPlayer(ServerPlayer *player) { players.insert(id, player); } -void Server::removePlayer(int id) { players.remove(id); } +void Server::removePlayer(int id) { + if (players[id]) { + players.remove(id); + } +} void Server::updateRoomList() { QJsonArray arr; + QJsonArray avail_arr; foreach (Room *room, rooms) { QJsonArray obj; auto settings = QJsonDocument::fromJson(room->getSettings()); auto password = settings["password"].toString(); + auto count = room->getPlayers().count(); // playerNum + auto cap = room->getCapacity(); // capacity - obj << room->getId(); // roomId - obj << room->getName(); // roomName - obj << settings["gameMode"]; // gameMode - obj << room->getPlayers().count(); // playerNum - obj << room->getCapacity(); // capacity + obj << room->getId(); // roomId + obj << room->getName(); // roomName + obj << settings["gameMode"]; // gameMode + obj << count; + obj << cap; obj << !password.isEmpty(); - arr << obj; + + if (count == cap) + arr << obj; + else + avail_arr << obj; + } + foreach (auto v, avail_arr) { + arr.prepend(v); } auto jsonData = JsonArray2Bytes(arr); lobby()->doBroadcastNotify(lobby()->getPlayers(), "UpdateRoomList", @@ -171,8 +185,22 @@ void Server::broadcast(const QString &command, const QString &jsonData) { } void Server::processNewConnection(ClientSocket *client) { - qInfo() << client->peerAddress() << "connected"; - // version check, file check, ban IP, reconnect, etc + auto addr = client->peerAddress(); + qInfo() << addr << "connected"; + auto result = SelectFromDatabase( + db, QString("SELECT * FROM banip WHERE ip='%1';").arg(addr)); + if (!result.isEmpty()) { + QJsonArray body; + body << -2; + body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | + Router::DEST_CLIENT); + body << "ErrorMsg"; + body << "you have been banned!"; + client->send(JsonArray2Bytes(body)); + qInfo() << "Refused banned IP:" << addr; + client->disconnectFromHost(); + return; + } connect(client, &ClientSocket::disconnected, this, [client]() { qInfo() << client->peerAddress() << "disconnected"; }); @@ -334,7 +362,10 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, obj = result[0].toObject(); // check if this username already login int id = obj["id"].toString().toInt(); - if (!players.value(id)) { + passed = obj["banned"].toString().toInt() == 0; + if (!passed) { + error_msg = "you have been banned!"; + } else if (!players.value(id)) { // check if password is the same auto salt = obj["salt"].toString().toLatin1(); decrypted_pw.append(salt); @@ -351,12 +382,21 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, player->setSocket(client); player->alive = true; client->disconnect(this); - broadcast("ServerMessage", - tr("%1 backed").arg(player->getScreenName())); - room->pushRequest(QString("%1,reconnect").arg(id)); + // broadcast("ServerMessage", + // tr("%1 backed").arg(player->getScreenName())); + + if (room && !room->isLobby()) { + room->pushRequest(QString("%1,reconnect").arg(id)); + } else { + // 懒得处理掉线玩家在大厅了!踢掉得了 + player->doNotify("ErrorMsg", "Unknown Error"); + player->kicked(); + } + return; } else { error_msg = "others logged in with this name"; + passed = false; } } } @@ -365,6 +405,13 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, } if (passed) { + // update lastLoginIp + auto sql_update = + QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;") + .arg(client->peerAddress()) + .arg(obj["id"].toString().toInt()); + ExecSQL(db, sql_update); + // create new ServerPlayer and setup ServerPlayer *player = new ServerPlayer(lobby()); player->setSocket(client); @@ -375,7 +422,8 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, player->setScreenName(name); player->setAvatar(obj["avatar"].toString()); player->setId(obj["id"].toString().toInt()); - broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName())); + // broadcast("ServerMessage", tr("%1 logged + // in").arg(player->getScreenName())); players.insert(player->getId(), player); // tell the lobby player's basic property @@ -410,6 +458,14 @@ void Server::onRoomAbandoned() { updateRoomList(); // room->deleteLater(); idle_rooms.push(room); + // 懒得改了! + // 这里出bug的原因还是在于room的销毁工作没做好 + // room销毁这块bug很多 + // if (idle_rooms.length() > 10) { + // auto junk = idle_rooms[0]; + // idle_rooms.removeFirst(); + // junk->deleteLater(); + // } #ifdef QT_DEBUG qDebug() << rooms.size() << "running room(s)," << idle_rooms.size() << "idle room(s)."; @@ -419,7 +475,8 @@ void Server::onRoomAbandoned() { void Server::onUserDisconnected() { ServerPlayer *player = qobject_cast(sender()); qInfo() << "Player" << player->getId() << "disconnected"; - broadcast("ServerMessage", tr("%1 logged out").arg(player->getScreenName())); + // broadcast("ServerMessage", tr("%1 logged + // out").arg(player->getScreenName())); Room *room = player->getRoom(); if (room->isStarted()) { if (room->getObservers().contains(player)) { @@ -435,7 +492,15 @@ void Server::onUserDisconnected() { } } -void Server::onUserStateChanged() {} +void Server::onUserStateChanged() { + ServerPlayer *player = qobject_cast(sender()); + auto room = player->getRoom(); + if (!room || room->isLobby() || room->isAbandoned()) { + return; + } + room->doBroadcastNotify(room->getPlayers(), "NetStateChanged", + QString("[%1,\"%2\"]").arg(player->getId()).arg(player->getStateString())); +} RSA *Server::initServerRSA() { RSA *rsa = RSA_new(); @@ -451,7 +516,8 @@ RSA *Server::initServerRSA() { BIO_free_all(bp_pub); BIO_free_all(bp_pri); - QFile("server/rsa").setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + QFile("server/rsa") + .setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); BN_free(bne); } FILE *keyFile = fopen("server/rsa_pub", "r"); diff --git a/src/server/shell.cpp b/src/server/shell.cpp index 94ef3cf5..7fc9a7b5 100644 --- a/src/server/shell.cpp +++ b/src/server/shell.cpp @@ -20,15 +20,25 @@ static void sigintHandler(int) { void Shell::helpCommand(QStringList &) { qInfo("Frequently used commands:"); -#define HELP_MSG(a, b) \ +#define HELP_MSG(a, b) \ qInfo((a), Color((b), fkShell::Cyan).toUtf8().constData()); HELP_MSG("%s: Display this help message.", "help"); HELP_MSG("%s: Shut down the server.", "quit"); HELP_MSG("%s: List all online players.", "lsplayer"); HELP_MSG("%s: List all running rooms.", "lsroom"); - HELP_MSG("%s: Kick a player by his id.", "kick"); + HELP_MSG("%s: Kick a player by his .", "kick"); HELP_MSG("%s: Broadcast message.", "msg"); + HELP_MSG("%s: Ban 1 or more accounts by their .", "ban"); + HELP_MSG("%s: Unban 1 or more accounts by their .", "unban"); + HELP_MSG( + "%s: Ban 1 or more IP address according to somebody's 'lastLoginIp'. " + "At least 1 required.", + "banip"); + HELP_MSG( + "%s: Unban 1 or more IP address according to somebody's 'lastLoginIp'. " + "At least 1 required.", + "unbanip"); qInfo(); qInfo("===== Package commands ====="); HELP_MSG("%s: Install a new package from .", "install"); @@ -36,7 +46,7 @@ void Shell::helpCommand(QStringList &) { HELP_MSG("%s: List all packages.", "lspkg"); HELP_MSG("%s: Enable a package.", "enable"); HELP_MSG("%s: Disable a package.", "disable"); - HELP_MSG("%s: Upgrade a package.", "upgrade"); + HELP_MSG("%s: Upgrade a package. Leave empty to upgrade all.", "upgrade"); qInfo("For more commands, check the documentation."); #undef HELP_MSG @@ -86,7 +96,12 @@ void Shell::removeCommand(QStringList &list) { void Shell::upgradeCommand(QStringList &list) { if (list.isEmpty()) { - qWarning("The 'upgrade' command need a package name to upgrade."); + // qWarning("The 'upgrade' command need a package name to upgrade."); + auto arr = QJsonDocument::fromJson(Pacman->listPackages().toUtf8()).array(); + foreach (auto a, arr) { + auto obj = a.toObject(); + Pacman->upgradePack(obj["name"].toString()); + } return; } @@ -155,6 +170,111 @@ void Shell::msgCommand(QStringList &list) { ServerInstance->broadcast("ServerMessage", msg); } +static void banAccount(sqlite3 *db, const QString &name, bool banned) { + if (!CheckSqlString(name)) + return; + QString sql_find = QString("SELECT * FROM userinfo \ + WHERE name='%1';") + .arg(name); + auto result = SelectFromDatabase(db, sql_find); + if (result.isEmpty()) + return; + auto obj = result[0].toObject(); + int id = obj["id"].toString().toInt(); + ExecSQL(db, QString("UPDATE userinfo SET banned=%2 WHERE id=%1;") + .arg(id) + .arg(banned ? 1 : 0)); + + if (banned) { + auto p = ServerInstance->findPlayer(id); + if (p) { + p->kicked(); + } + qInfo("Banned %s.", name.toUtf8().constData()); + } else { + qInfo("Unbanned %s.", name.toUtf8().constData()); + } +} + +void Shell::banCommand(QStringList &list) { + if (list.isEmpty()) { + qWarning("The 'ban' command needs at least 1 ."); + return; + } + + auto db = ServerInstance->getDatabase(); + + foreach (auto name, list) { + banAccount(db, name, true); + } +} + +void Shell::unbanCommand(QStringList &list) { + if (list.isEmpty()) { + qWarning("The 'unban' command needs at least 1 ."); + return; + } + + auto db = ServerInstance->getDatabase(); + + foreach (auto name, list) { + banAccount(db, name, false); + } +} +static void banIPByName(sqlite3 *db, const QString &name, bool banned) { + if (!CheckSqlString(name)) + return; + + QString sql_find = QString("SELECT * FROM userinfo \ + WHERE name='%1';") + .arg(name); + auto result = SelectFromDatabase(db, sql_find); + if (result.isEmpty()) + return; + auto obj = result[0].toObject(); + int id = obj["id"].toString().toInt(); + auto addr = obj["lastLoginIp"].toString(); + + if (banned) { + ExecSQL(db, QString("INSERT INTO banip VALUES('%1');").arg(addr)); + + auto p = ServerInstance->findPlayer(id); + if (p) { + p->kicked(); + } + qInfo("Banned IP %s.", addr.toUtf8().constData()); + } else { + ExecSQL(db, QString("DELETE FROM banip WHERE ip='%1';").arg(addr)); + qInfo("Unbanned IP %s.", addr.toUtf8().constData()); + } +} + +void Shell::banipCommand(QStringList &list) { + if (list.isEmpty()) { + qWarning("The 'banip' command needs at least 1 ."); + return; + } + + auto db = ServerInstance->getDatabase(); + + foreach (auto name, list) { + banIPByName(db, name, true); + } +} + +void Shell::unbanipCommand(QStringList &list) { + if (list.isEmpty()) { + qWarning("The 'unbanip' command needs at least 1 ."); + return; + } + + auto db = ServerInstance->getDatabase(); + + foreach (auto name, list) { + banIPByName(db, name, false); + } +} + Shell::Shell() { setObjectName("Shell"); signal(SIGINT, sigintHandler); @@ -173,6 +293,10 @@ Shell::Shell() { handlers["disable"] = &Shell::disableCommand; handlers["kick"] = &Shell::kickCommand; handlers["msg"] = &Shell::msgCommand; + handlers["ban"] = &Shell::banCommand; + handlers["unban"] = &Shell::unbanCommand; + handlers["banip"] = &Shell::banipCommand; + handlers["unbanip"] = &Shell::unbanipCommand; } handler_map = handlers; } diff --git a/src/server/shell.h b/src/server/shell.h index 149d6d36..0e4b1564 100644 --- a/src/server/shell.h +++ b/src/server/shell.h @@ -25,6 +25,10 @@ private: void disableCommand(QStringList &); void kickCommand(QStringList &); void msgCommand(QStringList &); + void banCommand(QStringList &); + void banipCommand(QStringList &); + void unbanCommand(QStringList &); + void unbanipCommand(QStringList &); }; #endif