diff --git a/.gitignore b/.gitignore index 92a78abf..c50f7bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,28 @@ +# Compile output build/ +*.o + +# IDE & LSP .kdev4/ .vscode/ *.user *-swp *.kdev4 +.cache/ +tags + +# file produced by game FreeKill FreeKill.exe freekill-wrap.cxx server/users.db +server/rsa +server/rsa_pub +freekill.client.config.json +freekill.server.config.json +flist.txt + +# windeployqt bearer/ iconengines/ imageformats/ diff --git a/.gitmodules b/.gitmodules index f28279f0..d914762f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "fkparse"] path = fkparse - url = git@github.com:Notify-ctrl/fkparse + url = https://github.com/Notify-ctrl/fkparse diff --git a/CMakeLists.txt b/CMakeLists.txt index d9cf76b9..286fe4c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,10 +8,12 @@ add_subdirectory(fkparse) find_package(Qt6 REQUIRED COMPONENTS Gui Qml + Widgets Network Multimedia ) +find_package(OpenSSL) find_package(Lua) find_package(SQLite3) diff --git a/README.md b/README.md index 3b3f77b2..12886287 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,4 @@ ___ ## 如何构建 -FreeKill使用Qt6.3,支持的运行平台有Windows、Linux、Android。 - -欲编译FreeKill,首先得从Qt官网的安装工具安装Qt Creator和Qt 6.3.2。安装时需要勾选CMake,应该默认就是选上的状态。 - -然后下载swig,并为其配置环境变量,即可构建FreeKill。 - -对于Linux用户而言,还需要自己从包管理器安装lua5.4和sqlite。 +[编译教程](./doc/dev/compile.md) diff --git a/android/.gitignore b/android/.gitignore index 8982be2f..39a3a6b8 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,2 +1,4 @@ assets/ res/ +build.sh + diff --git a/android/copy_assets.sh b/android/copy_assets.sh index 126409c5..91f5e059 100755 --- a/android/copy_assets.sh +++ b/android/copy_assets.sh @@ -1,5 +1,7 @@ #!/bin/sh +rm -rf res assets + if [ ! -e res/mipmap ]; then mkdir -p res/mipmap fi diff --git a/doc/dev/compile.md b/doc/dev/compile.md new file mode 100644 index 00000000..0b472170 --- /dev/null +++ b/doc/dev/compile.md @@ -0,0 +1,90 @@ +# 编译 FreeKill + +> [dev](./index.md) > 编译 + +___ + +## 全平台通用步骤 + +FreeKill采用最新的Qt进行构建,因此需要先安装Qt6的开发环境。 + +无论是Win还是Linux,都建议用[Qt官方的下载器](https://download.qt.io/official_releases/online_installers/)进行安装。当然了,在一些软件更新很频繁的Linux发行版里面,可能已经能从包管理器安装Qt6,对此后文细说。这个环节介绍用Qt安装器安装的步骤。 + +Qt安装的流程不赘述。为了编译FreeKill,至少需要安装以下的组件: +- Qt 6: MinGW 11.2.0 64-bit (不支持MSVC) +- Qt 6: Qt5 Compat +- Qt 6: Multimedia +- QtCreator(这个是安装器强制要你安装的) +- CMake、Ninja +- OpenSSL 1.1.1j Source + +接下来根据平台的不同,步骤也稍有区别。 + +___ + +## Windows + +从网络上下载swig、flex、bison。swig在其官网可以下载,flex和bison可在[github](https://github.com/lexxmark/winflexbison/releases/)下载。 + +全都下载完成之后,将含有swig.exe、win_flex.exe、win_bison.exe的文件夹全部都设置到Path环境变量里面去。 + +之后,把/Tools/OpenSSL/src/include/openssl这个文件夹复制到/6.3.2/mingw_64/include。 + +接下来万事俱备,使用QtCreator打开项目,然后编译吧。 + +___ + +## Linux + +通过包管理器安装一些额外软件包方可编译。 + +Debian一家子: + +```sh +$ sudo apt install liblua5.4-dev libsqlite3-dev libssl-dev swig flex bison +``` + +Arch Linux: + +```sh +$ sudo pacman -Sy lua sqlite swig openssl swig flex bison +``` + +然后使用配置好的QtCreator环境即可编译。 + +如果你不想用Qt安装器的话,可以用包管理器安装依赖,下面仅举例Arch: + +```sh +$ sudo pacman -S qt6-base qt6-declarative qt6-5compat qt6-multimedia +$ sudo pacman -S cmake lua sqlite swig openssl swig flex bison +``` + +然后可以用命令行编译: + +```sh +$ mkdir build && cd build +$ cmake .. +$ make -j8 +``` + +___ + +## Linux服务器 + +一般来说Linux服务器的包管理器都没新到提供Qt6下载,这个时候想编译服务端的话,需要在尽可能安装完Qt5环境的情况下,对FreeKill的Qt版本降一下等级。 + +首先将根目录和src下面的两个CMakeLists.txt的Qt6都改成Qt5,然后试图进行编译。 + +编译器会报告大概不超过10处错误,将它们修改成Qt5可以接受的形式就行了。 + +___ + +## MacOS + +大致与Windows类似,但尚且缺少确切的方案。 + +___ + +## 编译安卓版 + +用Qt安装器装好Android库,然后配置一下android-sdk就能编译了。 diff --git a/doc/dev/gamelogic.md b/doc/dev/gamelogic.md new file mode 100644 index 00000000..a8e8aa93 --- /dev/null +++ b/doc/dev/gamelogic.md @@ -0,0 +1,23 @@ +# 游戏逻辑 + +> [dev](./index.md) > 游戏逻辑 + +___ + +## 概述 + +FreeKill的游戏相关处理逻辑完全使用lua实现。在服务端上,每个Room都有自己的lua_State,并且只会在Room线程启动后才会去调用lua函数进行游戏逻辑处理。 + +本文档将简要介绍几个最为复杂的逻辑实现。 + +___ + +## 触发技 + +___ + +## 移动牌 + +___ + +## 使用牌 diff --git a/doc/dev/index.md b/doc/dev/index.md index 56e5a8d8..2c0900d0 100644 --- a/doc/dev/index.md +++ b/doc/dev/index.md @@ -6,6 +6,8 @@ ___ FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在UI方面使用的是Qt Quick。 +- [编译](./compile.md) - [通信](./protocol.md) +- [游戏逻辑](./gamelogic.md) - [数据库](./database.md) - [UI](./ui.md) diff --git a/doc/dev/protocol.md b/doc/dev/protocol.md index 0521a5f0..8ac5372a 100644 --- a/doc/dev/protocol.md +++ b/doc/dev/protocol.md @@ -36,8 +36,8 @@ $ ./FreeKill -s 每当任何一个客户端连接上了之后,游戏会先进行以下流程: 1. 检查IP是否被封禁。 // TODO: 数据库 -2. 检查客户端的延迟是否小于30秒。 -3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和密码,服务端检查这个字符串是否合法。 +2. 服务端将RSA公钥发给客户端,然后检查客户端的延迟是否小于30秒。 +3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和RSA公钥加密后的密码,服务端检查这个字符串是否合法。如果合法,检查密码是否正确。 4. 上述检查都通过后,重连(TODO:) 5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。 @@ -51,11 +51,12 @@ ___ 1. 只要房间被添加玩家,那么那名玩家就自动从大厅移除。 2. 当玩家离开房间时,玩家便会自动进入大厅。 -3. 当所有玩家都离开房间后,房间被销毁。 +3. 当所有玩家都离开房间后,房间被“销毁”(其实是进入Server的空闲房间列表,毕竟新建lua_State的开销十分大)。 大厅的特点: 1. 只要有玩家进入,就刷新一次房间列表。 +2. 只要玩家变动,就更新大厅内人数(TODO:) > 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。 @@ -88,6 +89,41 @@ ___ 但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。 +重连的流程应为: + +1. 总之先新建`ServerPlayer`并加到大厅 +2. 在默认的处理流程中,此时会提醒玩家“已经有同名玩家加入”,然后断掉连接。 +3. 在这时可以改成:如果这个已经在线的玩家是Offline状态,那么就继续,否则断开。 +4. pass之后,走一遍流程,把玩家加到大厅里面先。 +5. 既然是Offline,那么掉线玩家肯定是在已经开始游戏的房间里面,而且其socket处于deleted但没有置为nullptr的状态。 +6. 那么在pass之后不要创建旧的SPlayer对象,而复用以前的。也不必走一次进lobby流程。 +7. 所以先手动发送Setup和EnterLobby消息。 +8. 发送Reconnect消息,内含房间的所有信息。Client据此加入房间并设定好信息。 + +房间应该有哪些信息? + +直接从UI着手: + +1. 首先EnterRoom消息,需要**人数**和**操作时长**。 +2. 既然需要人数了,那么就需要**所有玩家**。 +3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。 +4. 玩家的信息就更多了,武将、身份、血量、id... + +信息要怎么发呢: + +- 一步一步的告诉重连中的玩家。 +- 全部汇总成字符串或者别的什么,然后可以压缩并发送。 +- 但以上两种都有问题:许多信息保存在Lua中,而Lua的运行是绝对不容其他线程打搅的。 +- 而且粗略一想,这些东西都应该非常耗时,而如今的线程只有Main线程和各大Room线程。有必要给Room加个子线程专门处理掉线这块的,然后Room该怎么跑继续怎么跑。 + +或者换个思路: + +1. 首先EnterRoom消息,需要**人数**和**操作时长**。 +2. 服务端将这个客户端的*录像信息*发给客户端,客户端满速且不影响UI的播放录像。 +3. 在“播放录像”的过程中,客户端对于正在被收到的消息需进行特殊处理。 +4. 一个录像文件的体积会非常大。所以服务端所保存的客户端录像应该和真正的录像有差别才行。比如聊天、战报这种数据量大但又无关紧要的东西就不保存。 +5. 顺便这样也解决了多视角录像的问题,服务端给每个视角都录像就行了。 + ___ ## 旁观(TODO) diff --git a/image/splash.jpg b/image/splash.jpg new file mode 100644 index 00000000..06e1250a Binary files /dev/null and b/image/splash.jpg differ diff --git a/lib/android/libcrypto_1_1.so b/lib/android/libcrypto_1_1.so new file mode 100644 index 00000000..09d584fa Binary files /dev/null and b/lib/android/libcrypto_1_1.so differ diff --git a/lua/client/client.lua b/lua/client/client.lua index 2162dfce..c4ca6edc 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -62,6 +62,75 @@ function Client:moveCards(moves) end end +---@param msg LogMessage +function Client:appendLog(msg) + local data = msg + local function getPlayerStr(pid, color) + if not pid then + return "" + end + local ret = self:getPlayerById(pid) + ret = ret.general + ret = Fk:translate(ret) + ret = string.format('%s', ret) + return ret + end + + local from = getPlayerStr(data.from, "#0C8F0C") + + local to = data.to or {} + local to_str = {} + for _, id in ipairs(to) do + table.insert(to_str, getPlayerStr(id, "#CC3131")) + end + to = table.concat(to_str, ", ") + + local card = data.card or {} + local allUnknown = true + local unknownCount = 0 + for _, id in ipairs(card) do + if id ~= -1 then + allUnknown = false + else + unknownCount = unknownCount + 1 + end + end + + if allUnknown then + card = "" + else + local card_str = {} + for _, id in ipairs(card) do + table.insert(card_str, Fk:getCardById(id):toLogString()) + end + if unknownCount > 0 then + table.insert(card_str, Fk:translate("unknown_card") + .. unknownCount == 1 and "x" .. unknownCount or "") + end + card = table.concat(card_str, ", ") + end + + local function parseArg(arg) + arg = arg or "" + arg = Fk:translate(arg) + arg = string.format('%s', arg) + return arg + end + + local arg = parseArg(data.arg) + local arg2 = parseArg(data.arg2) + local arg3 = parseArg(data.arg3) + + local log = Fk:translate(data.type) + log = string.gsub(log, "%%from", from) + log = string.gsub(log, "%%to", to) + log = string.gsub(log, "%%card", card) + log = string.gsub(log, "%%arg2", arg2) + log = string.gsub(log, "%%arg3", arg3) + log = string.gsub(log, "%%arg", arg) + self:notifyUI("GameLog", log) +end + fk.client_callback["Setup"] = function(jsonData) -- jsonData: [ int id, string screenName, string avatar ] local data = json.decode(jsonData) @@ -103,8 +172,10 @@ fk.client_callback["RemovePlayer"] = function(jsonData) break end end - fk.ClientInstance:removePlayer(id) - ClientInstance:notifyUI("RemovePlayer", jsonData) + if id ~= Self.id then + fk.ClientInstance:removePlayer(id) + ClientInstance:notifyUI("RemovePlayer", jsonData) + end end fk.client_callback["ArrangeSeats"] = function(jsonData) @@ -163,6 +234,7 @@ local function separateMoves(moves) to = move.to, toArea = move.toArea, fromArea = info.fromArea, + moveReason = move.moveReason, }) end end @@ -182,7 +254,8 @@ local function mergeMoves(moves) from = move.from, to = move.to, fromArea = move.fromArea, - toArea = move.toArea + toArea = move.toArea, + moveReason = move.moveReason, } end table.insert(temp[info].ids, move.ids[1]) @@ -193,12 +266,33 @@ local function mergeMoves(moves) return ret end +local function sendMoveCardLog(move) + if move.moveReason == fk.ReasonDraw then + ClientInstance:appendLog{ + type = "$DrawCards", + from = move.to, + card = move.ids, + arg = #move.ids, + } + elseif move.moveReason == fk.ReasonDiscard then + ClientInstance:appendLog{ + type = "$DiscardCards", + from = move.from, + card = move.ids, + arg = #move.ids, + } + end +end + fk.client_callback["MoveCards"] = function(jsonData) -- jsonData: CardsMoveStruct[] local raw_moves = json.decode(jsonData) local separated = separateMoves(raw_moves) ClientInstance:moveCards(separated) local merged = mergeMoves(separated) + for _, move in ipairs(merged) do + sendMoveCardLog(move) + end ClientInstance:notifyUI("MoveCards", json.encode(merged)) end @@ -237,6 +331,30 @@ fk.client_callback["AskForUseActiveSkill"] = function(jsonData) ClientInstance:notifyUI("AskForUseActiveSkill", jsonData) end +fk.client_callback["SetPlayerMark"] = function(jsonData) + -- jsonData: [ int id, string mark, int value ] + local data = json.decode(jsonData) + local player, mark, value = data[1], data[2], data[3] + ClientInstance:getPlayerById(player):setMark(mark, value) + + -- TODO: if mark is visible, update the UI. +end + +fk.client_callback["Chat"] = function(jsonData) + -- jsonData: { int type, string msg } + local data = json.decode(jsonData) + local p = ClientInstance:getPlayerById(data.type) + data.userName = p.player:getScreenName() + data.general = p.general + data.time = os.date("%H:%M:%S") + ClientInstance:notifyUI("Chat", json.encode(data)) +end + +fk.client_callback["GameLog"] = function(jsonData) + local data = json.decode(jsonData) + ClientInstance:appendLog(data) +end + -- Create ClientInstance (used by Lua) ClientInstance = Client:new() dofile "lua/client/client_util.lua" diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 4afed62b..f4d5635f 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -256,4 +256,51 @@ Fk:loadTranslationTable{ ["$Equip"] = "装备区", ["$Judge"] = "判定区", ["#AskForUseActiveSkill"] = "请使用技能 %1", + + ["Trust"] = "托管", + ["Sort Cards"] = "牌序", + ["Chat"] = "聊天", + ["Log"] = "战报", +} + +-- related to sendLog +Fk:loadTranslationTable{ + -- game processing + ["$AppendSeparator"] = '------------------------------', + ["$GameStart"] = "== 游戏开始 ==", + ["$GameEnd"] = "== 游戏结束 ==", + + -- get/lose skill + ["#AcquireSkill"] = "%from 获得了技能“%arg”", + ["#LoseSkill"] = "%from 失去了技能“%arg”", + + -- moveCards (they are sent by notifyMoveCards) + ["unknown_card"] = '未知牌', + ["log_spade"] = "♠", + ["log_heart"] = '', + ["log_club"] = "♣", + ["log_diamond"] = '', + ["log_nosuit"] = "无花色", + ["nosuit"] = "无花色", + ["spade"] = "黑桃", + ["heart"] = "红桃", + ["club"] = "梅花", + ["diamond"] = "方块", + + ["$DrawCards"] = "%from 摸了 %arg 张牌 %card", + ["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card", + + -- useCard + ["#UseCard"] = "%from 使用了牌 %card", + ["#UseCardToTargets"] = "%from 使用了牌 %card,目标是 %to", + + -- judge + ["#InitialJudge"] = "%from 的判定牌为 %card", + ["#ChangedJudge"] = "%from 发动“%arg”把 %to 的判定牌改为 %card", + ["#JudgeResult"] = "%from 的判定结果为 %card", + + -- turnOver + ["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg", + ["face_up"] = "正面朝上", + ["face_down"] = "背面朝上", } diff --git a/lua/core/card.lua b/lua/core/card.lua index bc1261ee..56052e10 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -87,8 +87,32 @@ function Card:getSuitString() elseif suit == Card.Diamond then return "diamond" else - return "unknown" + return "nosuit" end end +local function getNumberStr(num) + if num == 1 then + return "A" + elseif num == 11 then + return "J" + elseif num == 12 then + return "Q" + elseif num == 13 then + return "K" + end + return tostring(num) +end + +-- for sendLog +function Card:toLogString() + local ret = string.format('%s', Fk:translate(self.name) .. "[") + ret = ret .. Fk:translate("log_" .. self:getSuitString()) + if self.number > 0 then + ret = ret .. string.format('%s', self.color == Card.Red and "#CC3131" or "black", getNumberStr(self.number)) + end + ret = ret .. ']' + return ret +end + return Card diff --git a/lua/freekill.lua b/lua/freekill.lua index 6fe2d094..9122bc7d 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -9,7 +9,6 @@ package.path = package.path .. ";./lua/lib/?.lua" class = require "middleclass" json = require "json" -dofile "lua/lib/sha256.lua" local GroupUtils = require "core.util" TargetGroup, AimGroup = table.unpack(GroupUtils) dofile "lua/core/debug.lua" diff --git a/lua/lib/fkparser.lua b/lua/lib/fkparser.lua new file mode 100644 index 00000000..047d3f3d --- /dev/null +++ b/lua/lib/fkparser.lua @@ -0,0 +1,68 @@ +-- FreeKill's fkparse interface +-- fkparse (FreeKill parser), a game code generator +-- For license information, check generated lua files. + +-- In most cases, fk's basic modules are loaded before extension calls +-- "require 'fkparser'", so we needn't to import lua modules here. + +fkp = { + functions = {}, + newlist = function(t) + t.length = function(self) + return #self + end, + + t.prepend = function(self, element) + if #self > 0 and type(self[1]) ~= type(element) then return end + for i = #self, 1, -1 do + self[i + 1] = self[i] + end + self[1] = element + end, + + t.append = function(self, element) + if #self > 0 and type(self[1]) ~= type(element) then return end + table.insert(self, element) + end, + + t.removeOne = function(self, element) + if #self == 0 or type(self[1]) ~= type(element) then return false end + + for i = 1, #self do + if self[i] == element then + table.remove(self, i) + return true + end + end + return false + end, + + t.at = function(self, index) + return self[index + 1] + end, + + t.replace = function(self, index, value) + self[index + 1] = value + end, + return t + end, +} + +fkp.functions.prepend = function(arr, e) + if arr:length() == 0 then + arr = fkp.newlist{e} + else + arr:prepend(e) + end + return arr +end, + +fkp.functions.append = function(arr, e) + if arr:length() == 0 then + arr = fkp.newlist{e} + else + arr:append(e) + end + return arr +end, + diff --git a/lua/lib/sha256.lua b/lua/lib/sha256.lua deleted file mode 100644 index 33867635..00000000 --- a/lua/lib/sha256.lua +++ /dev/null @@ -1,195 +0,0 @@ --- From http://pastebin.com/gsFrNjbt linked from http://www.computercraft.info/forums2/index.php?/topic/8169-sha-256-in-pure-lua/ - --- --- Adaptation of the Secure Hashing Algorithm (SHA-244/256) --- Found Here: http://lua-users.org/wiki/SecureHashAlgorithm --- --- Using an adapted version of the bit library --- Found Here: https://bitbucket.org/Boolsheet/bslf/src/1ee664885805/bit.lua --- - -local MOD = 2^32 -local MODM = MOD-1 - -local function memoize(f) - local mt = {} - local t = setmetatable({}, mt) - function mt:__index(k) - local v = f(k) - t[k] = v - return v - end - return t -end - -local function make_bitop_uncached(t, m) - local function bitop(a, b) - local res,p = 0,1 - while a ~= 0 and b ~= 0 do - local am, bm = a % m, b % m - res = res + t[am][bm] * p - a = (a - am) / m - b = (b - bm) / m - p = p*m - end - res = res + (a + b) * p - return res - end - return bitop -end - -local function make_bitop(t) - local op1 = make_bitop_uncached(t,2^1) - local op2 = memoize(function(a) return memoize(function(b) return op1(a, b) end) end) - return make_bitop_uncached(op2, 2 ^ (t.n or 1)) -end - -local bxor1 = make_bitop({[0] = {[0] = 0,[1] = 1}, [1] = {[0] = 1, [1] = 0}, n = 4}) - -local function bxor(a, b, c, ...) - local z = nil - if b then - a = a % MOD - b = b % MOD - z = bxor1(a, b) - if c then z = bxor(z, c, ...) end - return z - elseif a then return a % MOD - else return 0 end -end - -local function band(a, b, c, ...) - local z - if b then - a = a % MOD - b = b % MOD - z = ((a + b) - bxor1(a,b)) / 2 - if c then z = bit32_band(z, c, ...) end - return z - elseif a then return a % MOD - else return MODM end -end - -local function bnot(x) return (-1 - x) % MOD end - -local function rshift1(a, disp) - if disp < 0 then return lshift(a,-disp) end - return math.floor(a % 2 ^ 32 / 2 ^ disp) -end - -local function rshift(x, disp) - if disp > 31 or disp < -31 then return 0 end - return rshift1(x % MOD, disp) -end - -local function lshift(a, disp) - if disp < 0 then return rshift(a,-disp) end - return (a * 2 ^ disp) % 2 ^ 32 -end - -local function rrotate(x, disp) - x = x % MOD - disp = disp % 32 - local low = band(x, 2 ^ disp - 1) - return rshift(x, disp) + lshift(low, 32 - disp) -end - -local k = { - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, - 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, - 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, - 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, - 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, - 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, -} - -local function str2hexa(s) - return (string.gsub(s, ".", function(c) return string.format("%02x", string.byte(c)) end)) -end - -local function num2s(l, n) - local s = "" - for i = 1, n do - local rem = l % 256 - s = string.char(rem) .. s - l = (l - rem) / 256 - end - return s -end - -local function s232num(s, i) - local n = 0 - for i = i, i + 3 do n = n*256 + string.byte(s, i) end - return n -end - -local function preproc(msg, len) - local extra = 64 - ((len + 9) % 64) - len = num2s(8 * len, 8) - msg = msg .. "\128" .. string.rep("\0", extra) .. len - assert(#msg % 64 == 0) - return msg -end - -local function initH256(H) - H[1] = 0x6a09e667 - H[2] = 0xbb67ae85 - H[3] = 0x3c6ef372 - H[4] = 0xa54ff53a - H[5] = 0x510e527f - H[6] = 0x9b05688c - H[7] = 0x1f83d9ab - H[8] = 0x5be0cd19 - return H -end - -local function digestblock(msg, i, H) - local w = {} - for j = 1, 16 do w[j] = s232num(msg, i + (j - 1)*4) end - for j = 17, 64 do - local v = w[j - 15] - local s0 = bxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) - v = w[j - 2] - w[j] = w[j - 16] + s0 + w[j - 7] + bxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) - end - - local a, b, c, d, e, f, g, h = H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] - for i = 1, 64 do - local s0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) - local maj = bxor(band(a, b), band(a, c), band(b, c)) - local t2 = s0 + maj - local s1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) - local ch = bxor (band(e, f), band(bnot(e), g)) - local t1 = h + s1 + ch + k[i] + w[i] - h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2 - end - - H[1] = band(H[1] + a) - H[2] = band(H[2] + b) - H[3] = band(H[3] + c) - H[4] = band(H[4] + d) - H[5] = band(H[5] + e) - H[6] = band(H[6] + f) - H[7] = band(H[7] + g) - H[8] = band(H[8] + h) -end - --- Made this global -function sha256(msg) - msg = preproc(msg, #msg) - local H = initH256({}) - for i = 1, #msg, 64 do digestblock(msg, i, H) end - return str2hexa(num2s(H[1], 4) .. num2s(H[2], 4) .. num2s(H[3], 4) .. num2s(H[4], 4) .. - num2s(H[5], 4) .. num2s(H[6], 4) .. num2s(H[7], 4) .. num2s(H[8], 4)) -end - diff --git a/lua/server/event.lua b/lua/server/event.lua index 7e7afc88..9d0055e7 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -69,5 +69,6 @@ fk.PreCardEffect = 54 fk.BeforeCardEffect = 55 fk.CardEffecting = 56 fk.CardEffectFinished = 57 +fk.CardEffectCancelledOut = 58 -fk.NumOfEvents = 58 +fk.NumOfEvents = 59 diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 1a4c38e1..6decba99 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -127,8 +127,6 @@ function GameLogic:prepareForStart() -- TODO: add skills to player end - -- TODO: prepare drawPile - -- TODO: init cards in drawPile local allCardIds = Fk:getAllCardIds() table.shuffle(allCardIds) room.draw_pile = allCardIds @@ -137,13 +135,15 @@ function GameLogic:prepareForStart() end for _, p in ipairs(room.alive_players) do - room:handleAddLoseSkills(p, "zhiheng") + room:handleAddLoseSkills(p, "zhiheng", nil, false) end self:addTriggerSkill(GameRule) for _, trig in ipairs(Fk.global_trigger) do self:addTriggerSkill(trig) end + + self.room:sendLog{ type = "$GameStart" } end function GameLogic:action() diff --git a/lua/server/lobby.lua b/lua/server/lobby.lua deleted file mode 100644 index 103243db..00000000 --- a/lua/server/lobby.lua +++ /dev/null @@ -1,68 +0,0 @@ ----@class Lobby : Object ----@field lobby fk.Room -Lobby = class("Lobby") - -fk.lobby_callback = {} -local db = fk.ServerInstance:getDatabase() - -function Lobby:initialize(_lobby) - self.lobby = _lobby - self.lobby.callback = function(_self, command, jsonData) - local cb = fk.lobby_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - print("Lobby error: Unknown command " .. command); - end - end -end - -fk.lobby_callback["UpdateAvatar"] = function(jsonData) - -- jsonData: [ int uid, string newavatar ] - local data = json.decode(jsonData) - local id, avatar = data[1], data[2] - local sql = "UPDATE userinfo SET avatar='%s' WHERE id=%d;" - Sql.exec(db, string.format(sql, avatar, id)) - local player = fk.ServerInstance:findPlayer(id) - player:setAvatar(avatar) - player:doNotify("UpdateAvatar", avatar) -end - -fk.lobby_callback["UpdatePassword"] = function(jsonData) - -- jsonData: [ int uid, string oldpassword, int newpassword ] - local data = json.decode(jsonData) - local id, old, new = data[1], data[2], data[3] - local sql_find = "SELECT password FROM userinfo WHERE id=%d;" - local sql_update = "UPDATE userinfo SET password='%s' WHERE id=%d;" - - local passed = false - local result = Sql.exec_select(db, string.format(sql_find, id)) - passed = (result["password"][1] == sha256(old)) - if passed then - Sql.exec(db, string.format(sql_update, sha256(new), id)) - end - - local player = fk.ServerInstance:findPlayer(tonumber(id)) - player:doNotify("UpdatePassword", passed and "1" or "0") -end - -fk.lobby_callback["CreateRoom"] = function(jsonData) - -- jsonData: [ int uid, string name, int capacity ] - local data = json.decode(jsonData) - local owner = fk.ServerInstance:findPlayer(tonumber(data[1])) - local roomName = data[2] - local capacity = data[3] - fk.ServerInstance:createRoom(owner, roomName, capacity) -end - -fk.lobby_callback["EnterRoom"] = function(jsonData) - -- jsonData: [ int uid, int roomId ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = fk.ServerInstance:findRoom(tonumber(data[2])) - room:addPlayer(player) -end - -function CreateRoom(_room) - LobbyInstance = Lobby:new(_room) -end diff --git a/lua/server/room.lua b/lua/server/room.lua index 5068f595..6d2840e3 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -46,14 +46,6 @@ ServerPlayer = require "server.serverplayer" ---@param _room fk.Room function Room:initialize(_room) self.room = _room - self.room.callback = function(_self, command, jsonData) - local cb = fk.room_callback[command] - if (type(cb) == "function") then - cb(jsonData) - else - print("Lobby error: Unknown command " .. command); - end - end self.room.startGame = function(_self) Room.initialize(self, _room) -- clear old data @@ -206,6 +198,32 @@ function Room:getNCards(num, from) return cardIds end +---@param player ServerPlayer +---@param mark string +---@param value integer +function Room:setPlayerMark(player, mark, value) + player:setMark(mark, value) + self:doBroadcastNotify("SetPlayerMark", json.encode{ + player.id, + mark, + value + }) +end + +function Room:addPlayerMark(player, mark, count) + count = count or 1 + local num = player:getMark(mark) + num = num or 0 + self:setPlayerMark(player, mark, math.max(num + count, 0)) +end + +function Room:removePlayerMark(player, mark, count) + count = count or 1 + local num = player:getMark(mark) + num = num or 0 + self:setPlayerMark(player, mark, math.max(num - count, 0)) +end + ------------------------------------------------------------------------ -- network functions, notify function ------------------------------------------------------------------------ @@ -257,11 +275,11 @@ end ---@param command string ---@param players ServerPlayer[] -function Room:doBroadcastRequest(command, players) +function Room:doBroadcastRequest(command, players, jsonData) players = players or self.players self:notifyMoveFocus(players, command) for _, p in ipairs(players) do - self:doRequest(p, command, p.request_data, false) + self:doRequest(p, command, jsonData or p.request_data, false) end local remainTime = self.timeout @@ -269,8 +287,39 @@ function Room:doBroadcastRequest(command, players) local elapsed = 0 for _, p in ipairs(players) do elapsed = os.time() - currentTime - remainTime = remainTime - elapsed - p:waitForReply(remainTime) + p:waitForReply(remainTime - elapsed) + end +end + +---@param command string +---@param players ServerPlayer[] +function Room:doRaceRequest(command, players, jsonData) + players = players or self.players + self:notifyMoveFocus(players, command) + for _, p in ipairs(players) do + self:doRequest(p, command, jsonData or p.request_data, false) + end + + local remainTime = self.timeout + local currentTime = os.time() + local elapsed = 0 + local winner + while true do + elapsed = os.time() - currentTime + if remainTime - elapsed <= 0 then + return nil + end + for _, p in ipairs(players) do + p:waitForReply(0) + if p.reply_ready == true then + winner = p + break + end + end + if winner then + self:doBroadcastNotify("CancelRequest", "") + return winner + end end end @@ -328,6 +377,11 @@ function Room:notifyMoveFocus(players, command) }) end +---@param log LogMessage +function Room:sendLog(log) + self:doBroadcastNotify("GameLog", json.encode(log)) +end + ------------------------------------------------------------------------ -- interactive functions ------------------------------------------------------------------------ @@ -540,9 +594,9 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) ---@type AimStruct local aimStruct local initialEvent = false - collaboratorsIndex[toId] = collaboratorsIndex[toId] or 0 + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 - if not aimEventCollaborators[toId] or collaboratorsIndex[toId] >= #aimEventCollaborators[toId] then + if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then aimStruct = { from = cardUseEvent.from, cardId = cardUseEvent.cardId, @@ -581,7 +635,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) cardUseEvent.from = aimStruct.from cardUseEvent.tos = aimEventTargetGroup cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets - + if #AimGroup:getAllTargets(aimStruct.tos) == 0 then return false end @@ -590,18 +644,20 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) if #cancelledTargets > 0 then for _, target in ipairs(cancelledTargets) do aimEventCollaborators[target] = {} - collaboratorsIndex[target] = 0 + collaboratorsIndex[target] = 1 end end aimStruct.tos[AimGroup.Cancelled] = {} aimEventCollaborators[toId] = aimEventCollaborators[toId] or {} - if not room:getPlayerById(toId):isAlive() then + if room:getPlayerById(toId):isAlive() then if initialEvent then table.insert(aimEventCollaborators[toId], aimStruct) else aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct end + + collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 end AimGroup:setTargetDone(aimStruct.tos, toId) @@ -639,7 +695,9 @@ function Room:useCard(cardUseEvent) end for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do - -- TODO: need to complete the cards for response + if not cardUseEvent.toCardId and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then + break + end self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent) if event == fk.CardUsing then @@ -725,7 +783,59 @@ function Room:useCard(cardUseEvent) end if Fk:getCardById(cardUseEvent.cardId).skill then - Fk:getCardById(cardUseEvent.cardId).skill:onEffect(self, cardUseEvent) + ---@type CardEffectEvent + local cardEffectEvent = { + from = cardUseEvent.from, + tos = cardUseEvent.tos, + cardId = cardUseEvent.cardId, + toCardId = cardUseEvent.toCardId, + responseToEvent = cardUseEvent.responseToEvent, + nullifiedTargets = cardUseEvent.nullifiedTargets, + disresponsiveList = cardUseEvent.disresponsiveList, + unoffsetableList = cardUseEvent.unoffsetableList, + addtionalDamage = cardUseEvent.addtionalDamage, + cardIdsResponded = cardUseEvent.nullifiedTargets, + } + + if cardUseEvent.toCardId ~= nil then + self:doCardEffect(cardEffectEvent) + else + local collaboratorsIndex = {} + for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do + if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then + if aimEventCollaborators[toId] then + cardEffectEvent.to = toId + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 + local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]] + + cardEffectEvent.addtionalDamage = curAimEvent.additionalDamage + + if curAimEvent.disresponsiveList then + for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do + if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then + table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer) + end + end + end + + if curAimEvent.unoffsetableList then + for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do + if not table.contains(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer) then + table.insert(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer) + end + end + end + + cardEffectEvent.disresponsive = curAimEvent.disresponsive + cardEffectEvent.unoffsetable = curAimEvent.unoffsetable + + collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 + + self:doCardEffect(cardEffectEvent) + end + end + end + end end end end @@ -740,6 +850,80 @@ function Room:useCard(cardUseEvent) end end +---@param cardEffectEvent CardEffectEvent +function Room:doCardEffect(cardEffectEvent) + for _, event in ipairs({ fk.PreCardEffect, fk.BeforeCardEffect, fk.CardEffecting, fk.CardEffectFinished }) do + if cardEffectEvent.isCancellOut then + self.logic:trigger(fk.CardEffectCancelledOut, self:getPlayerById(cardEffectEvent.from), cardEffectEvent) + break + end + + if not cardEffectEvent.toCardId and (not (self:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or #self:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then + break + end + + if table.contains((cardEffectEvent.nullifiedTargets or {}), cardEffectEvent.to) then + break + end + + if self.logic:trigger(event, self:getPlayerById(cardEffectEvent.from), cardEffectEvent) then + return + end + + if event == fk.PreCardEffect then + -- TODO: use jink + + if Fk:getCardById(cardEffectEvent.cardId).name == 'slash' and + not ( + cardEffectEvent.disresponsive or + cardEffectEvent.unoffsetable or + table.contains(cardEffectEvent.disresponsiveList or {}, cardEffectEvent.to) or + table.contains(cardEffectEvent.unoffsetableList or {}, cardEffectEvent.to) + ) then + local result = self:doRequest(self:getPlayerById(cardEffectEvent.to), "PlayCard", cardEffectEvent.to) + if result ~= '' then + local data = json.decode(result) + local card = data.card + local targets = data.targets + if type(card) == "string" then + local card_data = json.decode(card) + local skill = Fk.skills[card_data.skill] + local selected_cards = card_data.subcards + skill:onEffect(self, { + from = cardEffectEvent.to, + cards = selected_cards, + tos = targets, + }) + else + local use = {} ---@type CardUseStruct + use.from = cardEffectEvent.to + use.toCardId = cardEffectEvent.cardId + use.responseToEvent = cardEffectEvent + use.cardId = card + self:useCard(use) + end + end + elseif Fk:getCardById(cardEffectEvent.cardId).type == Card.TypeTrick then + -- TODO: use nullification + + -- local use = {} ---@type CardUseStruct + -- use.from = cardEffectEvent.to + -- use.toCardId = cardEffectEvent.cardId + -- use.responseToEvent = cardEffectEvent + -- use.cardId = card + -- self:useCard(use) + end + end + + if event == fk.CardEffecting then + local cardEffecting = Fk:getCardById(cardEffectEvent.cardId) + if cardEffecting.skill then + cardEffecting.skill:onEffect(self, cardEffectEvent) + end + end + end +end + ------------------------------------------------------------------------ -- move cards, and wrappers ------------------------------------------------------------------------ @@ -879,6 +1063,43 @@ function Room:drawCards(player, num, skillName, fromPlace) return { table.unpack(topCards) } end +---@param card Card | Card[] +---@param to_place integer +---@param target ServerPlayer +---@param reason integer +---@param skill_name string +---@param special_name string +function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name) + reason = reason or fk.ReasonJustMove + skill_name = skill_name or "" + special_name = special_name or "" + local ids = {} + if card[1] ~= nil then + for i, cd in ipairs(card) do + ids[i] = cd.id + end + else + ids[1] = card.id + end + + local to + if table.contains( + {Card.PlayerEquip, Card.PlayerHand, + Card.PlayerJudge, Card.PlayerSpecial}, to_place) then + to = target.id + end + + self.moveCards{ + ids = ids, + from = self.owner_map[ids[1]], + to = to, + toArea = to_place, + moveReason = reason, + skillName = skill_name, + specialName = special_name + } +end + ------------------------------------------------------------------------ -- some easier actions ------------------------------------------------------------------------ @@ -1087,11 +1308,13 @@ end ---@param player ServerPlayer ---@param skill_names string[] | string ---@param source_skill string | Skill | nil -function Room:handleAddLoseSkills(player, skill_names, source_skill) +function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog) if type(skill_names) == "string" then skill_names = skill_names:split("|") end + if sendlog == nil then sendlog = true end + if #skill_names == 0 then return end local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] @@ -1105,7 +1328,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill) player.id, s.name }) - -- TODO: send a log here + + if sendlog then + self:sendLog{ + type = "#LoseSkill", + from = player.id, + arg = s.name + } + end + table.insert(losts, true) table.insert(triggers, s) end @@ -1122,7 +1353,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill) player.id, s.name }) - -- TODO: send log + + if sendlog then + self:sendLog{ + type = "#AcquireSkill", + from = player.id, + arg = s.name + } + end + table.insert(losts, false) table.insert(triggers, s) end @@ -1138,6 +1377,35 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill) end end +-- judge + +---@param data JudgeData +---@return Card +function Room:judge(data) + local who = data.who + self.logic:trigger(fk.StartJudge, who, data) + data.card = Fk:getCardById(self:getNCards(1)[1]) + self:sendLog{ + type = "#InitialJudge", + from = who.id, + card = {data.card}, + } + self:moveCardTo(data.card, Card.Processing, nil, fk.ReasonPrey) + + self.logic:trigger(fk.AskForRetrial, who, data) + self.logic:trigger(fk.FinishRetrial, who, data) + self:sendLog{ + type = "#JudgeResult", + from = who.id, + card = {data.card}, + } + + self.logic:trigger(fk.FinishJudge, who, data) + if self:getCardArea(data.card.id) == Card.Processing then + self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile) + end +end + -- other helpers function Room:adjustSeats() @@ -1187,29 +1455,6 @@ function Room:gameOver() self.room:gameOver() end -fk.room_callback = {} - -fk.room_callback["QuitRoom"] = function(jsonData) - -- jsonData: [ int uid ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = player:getRoom() - if not room:isLobby() then - room:removePlayer(player) - end -end - -fk.room_callback["AddRobot"] = function(jsonData) - -- jsonData: [ int uid ] - local data = json.decode(jsonData) - local player = fk.ServerInstance:findPlayer(tonumber(data[1])) - local room = player:getRoom() - - if not room:isLobby() then - room:addRobot(player) - end -end - function CreateRoom(_room) RoomInstance = Room:new(_room) end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 89d9333e..99e1f319 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -85,7 +85,11 @@ function ServerPlayer:turnOver() self.faceup = not self.faceup self.room:broadcastProperty(self, "faceup") - -- TODO: log + self.room:sendLog{ + type = "#TurnOver", + from = self.id, + arg = self.faceup and "face_up" or "face_down", + } self.room.logic:trigger(fk.TurnedOver, self) end diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index e7cdf2f9..34308656 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -33,3 +33,5 @@ fk.ReasonResonpse = 10 fk.NormalDamage = 1 fk.ThunderDamage = 2 fk.FireDamage = 3 + +---@alias LogMessage {type: string, from: integer, to: integer[], card: integer[], arg: any, arg2: any, arg3: any} diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index d72113b2..e4ea2188 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -18,7 +18,7 @@ GameRule = fk.CreateTriggerSkill{ if target == nil then if event == fk.GameStart then - print("Game started") + fk.qInfo("Game started") RoomInstance.tag["FirstRound"] = true end return false @@ -35,6 +35,7 @@ GameRule = fk.CreateTriggerSkill{ move_to_notify.toArea = Card.PlayerHand move_to_notify.to = player.id move_to_notify.moveInfo = {} + move_to_notify.moveReason = fk.ReasonDraw for _, id in ipairs(cardIds) do table.insert(move_to_notify.moveInfo, { cardId = id, fromArea = Card.DrawPile }) @@ -55,7 +56,7 @@ GameRule = fk.CreateTriggerSkill{ player:setFlag("Global_FirstRound") end - -- TODO: send log + room:sendLog{ type = "$AppendSeparator" } player:addMark("Global_TurnCount") if not player.faceup then diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index a8806648..c60a4205 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -5,10 +5,35 @@ Fk:loadTranslationTable{ ["standard_cards"] = "标+EX" } +local slashSkill = fk.CreateActiveSkill{ + name = "slash_skill", + target_filter = function(self, to_select, selected) + if #selected == 0 then + local player = Fk:currentRoom():getPlayerById(to_select) + return Self ~= player + end + end, + feasible = function(self, selected) + return #selected == 1 + end, + on_effect = function(self, room, effect) + local to = effect.to + local from = effect.from + local cid = room:askForCardChosen( + room:getPlayerById(from), + room:getPlayerById(to), + "hej", + "snatch" + ) + + room:obtainCard(from, cid) + end +} local slash = fk.CreateBasicCard{ name = "slash", number = 7, suit = Card.Spade, + skill = slashSkill, } Fk:loadTranslationTable{ ["slash"] = "杀", @@ -50,10 +75,19 @@ extension:addCards({ slash:clone(Card.Diamond, 13), }) +local jinkSkill = fk.CreateActiveSkill{ + name = "jink_skill", + on_effect = function(self, room, effect) + if effect.responseToEvent then + effect.responseToEvent.isCancellOut = true + end + end +} local jink = fk.CreateBasicCard{ name = "jink", suit = Card.Heart, number = 2, + skill = jinkSkill, } Fk:loadTranslationTable{ ["jink"] = "闪", @@ -131,7 +165,7 @@ local snatchSkill = fk.CreateActiveSkill{ return #selected == 1 end, on_effect = function(self, room, effect) - local to = TargetGroup:getRealTargets(effect.tos)[1] + local to = effect.to local from = effect.from local cid = room:askForCardChosen( room:getPlayerById(from), @@ -201,7 +235,7 @@ local exNihiloSkill = fk.CreateActiveSkill{ end end, on_effect = function(self, room, cardEffectEvent) - room:drawCards(room:getPlayerById(TargetGroup:getRealTargets(cardEffectEvent.tos)[1]), 2, "ex_nihilo") + room:drawCards(room:getPlayerById(cardEffectEvent.to), 2, "ex_nihilo") end } @@ -222,10 +256,19 @@ extension:addCards({ exNihilo:clone(Card.Heart, 11), }) +local nullificationSkill = fk.CreateActiveSkill{ + name = "nullification_skill", + on_effect = function(self, room, effect) + if effect.responseToEvent then + effect.responseToEvent.isCancellOut = true + end + end +} local nullification = fk.CreateTrickCard{ name = "nullification", suit = Card.Spade, number = 11, + skill = nullificationSkill, } Fk:loadTranslationTable{ ["nullification"] = "无懈可击", diff --git a/qml/Config.qml b/qml/Config.qml index 0f8f89cc..d05ba443 100644 --- a/qml/Config.qml +++ b/qml/Config.qml @@ -2,12 +2,36 @@ import QtQuick QtObject { // Client configuration + property real winWidth + property real winHeight + property var conf: ({}) + property string lastLoginServer + property var savedPassword: ({}) // Player property of client + property string serverAddr property string screenName: "" property string password: "" + property string cipherText // Client data property int roomCapacity: 0 property int roomTimeout: 0 + + function loadConf() { + conf = JSON.parse(Backend.loadConf()); + winWidth = conf.winWidth; + winHeight = conf.winHeight; + lastLoginServer = conf.lastLoginServer; + savedPassword = conf.savedPassword; + } + + function saveConf() { + conf.winWidth = realMainWin.width; + conf.winHeight = realMainWin.height; + conf.lastLoginServer = lastLoginServer; + conf.savedPassword = savedPassword; + + Backend.saveConf(JSON.stringify(conf, undefined, 2)); + } } diff --git a/qml/Logic.js b/qml/Logic.js index 6f4ac2b7..40e90f0b 100644 --- a/qml/Logic.js +++ b/qml/Logic.js @@ -15,9 +15,20 @@ function createClientPages() { var callbacks = {}; callbacks["NetworkDelayTest"] = function(jsonData) { + // jsonData: RSA pub key + let cipherText + if (config.savedPassword[config.serverAddr] !== undefined + && config.savedPassword[config.serverAddr].shorten_password === config.password) { + cipherText = config.savedPassword[config.serverAddr].password; + if (Debugging) + console.log("use remembered password", config.password); + } else { + cipherText = Backend.pubEncrypt(jsonData, config.password); + } + config.cipherText = cipherText; + let md5sum = Backend.calcFileMD5(); ClientInstance.notifyServer("Setup", JSON.stringify([ - config.screenName, - config.password + config.screenName, cipherText, md5sum ])); } @@ -37,6 +48,13 @@ callbacks["EnterLobby"] = function(jsonData) { // depth == 1 means the lobby page is not present in mainStack createClientPages(); if (mainStack.depth === 1) { + // we enter the lobby successfully, so save password now. + config.lastLoginServer = config.serverAddr; + config.savedPassword[config.serverAddr] = { + username: config.screenName, + password: config.cipherText, + shorten_password: config.cipherText.slice(0, 8) + } mainStack.push(lobby); } else { mainStack.pop(); @@ -66,3 +84,18 @@ callbacks["UpdateRoomList"] = function(jsonData) { }); }); } + +callbacks["Chat"] = function(jsonData) { + // jsonData: { string userName, string general, string time, string msg } + let current = mainStack.currentItem; // lobby(TODO) or room + let data = JSON.parse(jsonData); + let pid = data.type; + let userName = data.userName; + let general = Backend.translate(data.general); + let time = data.time; + let msg = data.msg; + if (general === "") + current.addToChat(pid, data, `[${time}] ${userName}: ${msg}`); + else + current.addToChat(pid, data, `[${time}] ${userName}(${general}): ${msg}`); +} diff --git a/qml/Pages/Common/ChatBox.qml b/qml/Pages/Common/ChatBox.qml new file mode 100644 index 00000000..732d3012 --- /dev/null +++ b/qml/Pages/Common/ChatBox.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Layouts + +Rectangle { + property bool isLobby: false + + function append(chatter) { + chatLogBox.append(chatter) + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + LogEdit { + id: chatLogBox + anchors.fill: parent + anchors.margins: 10 + font.pixelSize: 14 + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 28 + color: "#040403" + radius: 3 + border.width: 1 + border.color: "#A6967A" + + TextInput { + anchors.fill: parent + anchors.margins: 6 + color: "white" + clip: true + font.pixelSize: 14 + + onAccepted: { + if (text != "") { + ClientInstance.notifyServer( + "Chat", + JSON.stringify({ + type: isLobby, + msg: text + }) + ); + text = ""; + } + } + } + } + } +} diff --git a/qml/Pages/Common/LogEdit.qml b/qml/Pages/Common/LogEdit.qml new file mode 100644 index 00000000..3cf5f0ca --- /dev/null +++ b/qml/Pages/Common/LogEdit.qml @@ -0,0 +1,34 @@ +import QtQuick + +Flickable { + id: root + property alias font: textEdit.font + property alias text: textEdit.text + property alias color: textEdit.color + property alias textFormat: textEdit.textFormat + + flickableDirection: Flickable.VerticalFlick + contentWidth: textEdit.width + contentHeight: textEdit.height + clip: true + + TextEdit { + id: textEdit + + width: root.width + clip: true + readOnly: true + selectByKeyboard: true + selectByMouse: true + wrapMode: TextEdit.WrapAnywhere + textFormat: TextEdit.RichText + } + + function append(text) { + let autoScroll = atYEnd; + textEdit.append(text); + if (autoScroll && contentHeight > contentY + height) { + contentY = contentHeight - height; + } + } +} diff --git a/qml/Pages/Init.qml b/qml/Pages/Init.qml index e67cc14d..f76da5e2 100644 --- a/qml/Pages/Init.qml +++ b/qml/Pages/Init.qml @@ -15,13 +15,33 @@ Item { Column { spacing: 8 - TextField { + ComboBox { id: server_addr - text: "127.0.0.1" + model: [] + editable: true + + onEditTextChanged: { + if (model.indexOf(editText) === -1) { + passwordEdit.text = ""; + } else { + let data = config.savedPassword[editText]; + screenNameEdit.text = data.username; + passwordEdit.text = data.shorten_password; + } + } } TextField { id: screenNameEdit text: "player" + onTextChanged: { + passwordEdit.text = ""; + let data = config.savedPassword[server_addr.editText]; + if (data) { + if (text === data.username) { + passwordEdit.text = data.shorten_password; + } + } + } } /*TextField { id: avatarEdit @@ -35,16 +55,20 @@ Item { } Button { text: "Join Server" + enabled: passwordEdit.text !== "" onClicked: { + config.serverAddr = server_addr.editText; config.screenName = screenNameEdit.text; config.password = passwordEdit.text; mainWindow.busy = true; - Backend.joinServer(server_addr.text); + Backend.joinServer(server_addr.editText); } } Button { text: "Console start" + enabled: passwordEdit.text !== "" onClicked: { + config.serverAddr = "127.0.0.1"; config.screenName = screenNameEdit.text; config.password = passwordEdit.text; mainWindow.busy = true; @@ -54,4 +78,15 @@ Item { } } } + + Component.onCompleted: { + config.loadConf(); + server_addr.model = Object.keys(config.savedPassword); + server_addr.onModelChanged(); + server_addr.currentIndex = server_addr.model.indexOf(config.lastLoginServer); + + let data = config.savedPassword[config.lastLoginServer]; + screenNameEdit.text = data.username; + passwordEdit.text = data.shorten_password; + } } diff --git a/qml/Pages/Room.qml b/qml/Pages/Room.qml index 77c8bccc..7813597f 100644 --- a/qml/Pages/Room.qml +++ b/qml/Pages/Room.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import "Common" import "RoomElement" import "RoomLogic.js" as Logic @@ -15,6 +16,10 @@ Item { property alias popupBox: popupBox property alias promptText: prompt.text + property alias okCancel: okCancel + property alias okButton: okButton + property alias cancelButton: cancelButton + property alias dynamicCardArea: dynamicCardArea property var selected_targets: [] @@ -30,7 +35,7 @@ Item { anchors.top: parent.top anchors.right: parent.right onClicked: { - ClientInstance.clearPlayers(); + // ClientInstance.clearPlayers(); ClientInstance.notifyServer("QuitRoom", "[]"); } } @@ -183,10 +188,30 @@ Item { } } + Item { + id: dashboardBtn + width: childrenRect.width + height: childrenRect.height + anchors.bottom: parent.bottom + ColumnLayout { + MetroButton { + text: Backend.translate("Trust") + } + MetroButton { + text: Backend.translate("Sort Cards") + } + MetroButton { + text: Backend.translate("Chat") + onClicked: roomDrawer.open(); + } + } + } + Dashboard { id: dashboard - width: roomScene.width + width: roomScene.width - dashboardBtn.width anchors.top: roomArea.bottom + anchors.left: dashboardBtn.right self.playerid: dashboardModel.id self.general: dashboardModel.general @@ -222,8 +247,10 @@ Item { Text { id: prompt visible: progress.visible - anchors.bottom: progress.top - anchors.bottomMargin: 8 + anchors.top: progress.top + anchors.topMargin: -2 + color: "white" + z: 1 anchors.horizontalCenter: progress.horizontalCenter } @@ -232,11 +259,31 @@ Item { width: parent.width * 0.6 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: okCancel.top - anchors.bottomMargin: 8 + anchors.bottomMargin: 4 from: 0.0 to: 100.0 visible: false + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 14 + color: "black" + radius: 3 + } + + contentItem: Item { + implicitWidth: 200 + implicitHeight: 12 + + Rectangle { + width: progress.visualPosition * parent.width + height: parent.height + radius: 2 + color: "red" + } + } + NumberAnimation on value { running: progress.visible from: 100.0 @@ -315,6 +362,133 @@ Item { } } + Drawer { + id: roomDrawer + width: parent.width * 0.3 + height: parent.height + dim: false + clip: true + dragMargin: 0 + + ColumnLayout { + anchors.fill: parent + + SwipeView { + Layout.fillWidth: true + Layout.fillHeight: true + interactive: false + currentIndex: drawerBar.currentIndex + Item { + LogEdit { + id: log + anchors.fill: parent + } + } + Item { + ChatBox { + id: chat + anchors.fill: parent + } + } + } + + TabBar { + id: drawerBar + width: roomDrawer.width + TabButton { + width: roomDrawer.width / 2 + text: Backend.translate("Log") + } + TabButton { + width: roomDrawer.width / 2 + text: Backend.translate("Chat") + } + } + } + } + + Item { + id: dynamicCardArea + anchors.fill: parent + } + + Rectangle { + id: easyChat + width: parent.width + height: 28 + anchors.bottom: parent.bottom + visible: false + color: "#040403" + radius: 3 + border.width: 1 + border.color: "#A6967A" + + TextInput { + id: easyChatEdit + anchors.fill: parent + anchors.margins: 6 + color: "white" + clip: true + font.pixelSize: 14 + + onAccepted: { + if (text != "") { + ClientInstance.notifyServer( + "Chat", + JSON.stringify({ + type: 0, + msg: text + }) + ); + text = ""; + easyChat.visible = false; + easyChatEdit.enabled = false; + } + } + } + } + + Shortcut { + sequence: "T" + onActivated: { + easyChat.visible = true; + easyChatEdit.enabled = true; + easyChatEdit.forceActiveFocus(); + } + } + + Shortcut { + sequence: "Esc" + onActivated: { + easyChat.visible = false; + easyChatEdit.enabled = false; + } + } + + Shortcut { + sequence: "Return" + enabled: okButton.enabled + onActivated: Logic.doOkButton(); + } + + Shortcut { + sequence: "Space" + enabled: cancelButton.enabled + onActivated: Logic.doCancelButton(); + } + + function addToChat(pid, raw, msg) { + chat.append(msg); + let photo = Logic.getPhoto(pid); + if (photo === undefined) + photo = dashboard.self; + photo.chat(raw.msg); + } + + function addToLog(msg) { + log.append(msg); + } + Component.onCompleted: { toast.show(Backend.translate("$EnterRoom")); diff --git a/qml/Pages/RoomElement/InvisibleCardArea.qml b/qml/Pages/RoomElement/InvisibleCardArea.qml index 86956e00..6795cee4 100644 --- a/qml/Pages/RoomElement/InvisibleCardArea.qml +++ b/qml/Pages/RoomElement/InvisibleCardArea.qml @@ -61,7 +61,7 @@ Item { state.x = parentPos.x; state.y = parentPos.y; state.opacity = 0; - card = component.createObject(roomScene, state); + card = component.createObject(roomScene.dynamicCardArea, state); card.x -= card.width / 2; card.x += (i - outputs.length / 2) * 15; card.y -= card.height / 2; diff --git a/qml/Pages/RoomElement/Photo.qml b/qml/Pages/RoomElement/Photo.qml index 89c74ea5..8482fd5e 100644 --- a/qml/Pages/RoomElement/Photo.qml +++ b/qml/Pages/RoomElement/Photo.qml @@ -371,10 +371,55 @@ Item { anchors.centerIn: parent } + Rectangle { + id: chat + color: "#F2ECD7" + radius: 4 + opacity: 0 + width: parent.width + height: childrenRect.height + 8 + property string text: "" + visible: false + Text { + width: parent.width - 8 + x: 4 + y: 4 + text: parent.text + wrapMode: Text.WrapAnywhere + font.family: fontLibian.name + font.pixelSize: 20 + } + SequentialAnimation { + id: chatAnim + PropertyAnimation { + target: chat + property: "opacity" + to: 0.9 + duration: 200 + } + NumberAnimation { + duration: 2500 + } + PropertyAnimation { + target: chat + property: "opacity" + to: 0 + duration: 150 + } + onFinished: chat.visible = false; + } + } + onGeneralChanged: { if (!roomScene.isStarted) return; generalName.text = Backend.translate(general); let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])); kingdom = data.kingdom; } + + function chat(msg) { + chat.text = msg; + chat.visible = true; + chatAnim.restart(); + } } diff --git a/qml/Pages/RoomLogic.js b/qml/Pages/RoomLogic.js index ae887283..acaa32e0 100644 --- a/qml/Pages/RoomLogic.js +++ b/qml/Pages/RoomLogic.js @@ -379,9 +379,11 @@ callbacks["ArrangeSeats"] = function(jsonData) { for (let i = 0; i < photoModel.count; i++) { let item = photoModel.get(i); item.seatNumber = order.indexOf(item.id) + 1; + item.general = ""; } dashboardModel.seatNumber = order.indexOf(Self.id) + 1; + dashboardModel.general = ""; roomScene.dashboardModelChanged(); // make Self to the first of list, then reorder photomodel @@ -476,7 +478,10 @@ callbacks["AskForSkillInvoke"] = function(jsonData) { // jsonData: string name roomScene.promptText = Backend.translate("#AskForSkillInvoke") .arg(Backend.translate(jsonData)); - roomScene.state = "responding"; + roomScene.state = "replying"; + roomScene.okCancel.visible = true; + roomScene.okButton.enabled = true; + roomScene.cancelButton.enabled = true; } callbacks["AskForChoice"] = function(jsonData) { @@ -590,3 +595,11 @@ callbacks["AskForUseActiveSkill"] = function(jsonData) { dashboard.startPending(skill_name); cancelButton.enabled = cancelable; } + +callbacks["CancelRequest"] = function() { + roomScene.state = "notactive"; +} + +callbacks["GameLog"] = function(jsonData) { + roomScene.addToLog(jsonData) +} diff --git a/qml/main.qml b/qml/main.qml index a8f427db..12d20d14 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -5,6 +5,7 @@ import "Logic.js" as Logic import "Pages" Window { + id: realMainWin visible: true width: 960 height: 540 @@ -19,6 +20,10 @@ Item { scale: parent.width / width anchors.centerIn: parent + Config { + id: config + } + Image { source: AppPath + "/image/background" anchors.fill: parent @@ -52,10 +57,6 @@ Item { visible: mainWindow.busy === true } - Config { - id: config - } - // global popup. it is modal and just lower than toast Rectangle { id: globalPopupDim @@ -140,7 +141,27 @@ Item { } } + Shortcut { + sequences: [ StandardKey.FullScreen ] + onActivated: { + if (realMainWin.visibility === Window.FullScreen) + realMainWin.showNormal(); + else + realMainWin.showFullScreen(); + } + } + + Component.onCompleted: { + if (!Android) { + width = config.winWidth; + height = config.winHeight; + } + } + onClosing: { + config.winWidth = width; + config.winHeight = height; + config.saveConf(); Backend.quitLobby(); } } diff --git a/server/init.sql b/server/init.sql index 69afb583..14333fd6 100644 --- a/server/init.sql +++ b/server/init.sql @@ -2,6 +2,7 @@ CREATE TABLE userinfo ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), password CHAR(64), + salt CHAR(8), avatar VARCHAR(64), lastLoginIp VARCHAR(64), banned BOOLEAN diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 83a3d3ae..b15665dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,16 +31,21 @@ set(freekill_HEADERS if (WIN32) set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll) set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll) + set(CRYPTO_LIB ${PROJECT_SOURCE_DIR}/lib/win/libcrypto_1_1.dll) elseif (ANDROID) set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/android/liblua54.so) set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/android/libsqlite3.so) + set(CRYPTO_LIB ${PROJECT_SOURCE_DIR}/lib/android/libcrypto_1_1.so) set_target_properties(FreeKill PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android - QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB}" + QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB};${CRYPTO_LIB}" ) else () set(LUA_LIB lua5.4) set(SQLITE3_LIB sqlite3) + set(CRYPTO_LIB OpenSSL::Crypto) + set(READLINE_LIB readline) + list(APPEND freekill_SRCS "server/shell.cpp") endif () source_group("Include" FILES ${freekill_HEADERS}) @@ -50,9 +55,12 @@ target_precompile_headers(FreeKill PRIVATE "pch.h") target_link_libraries(FreeKill PRIVATE ${LUA_LIB} ${SQLITE3_LIB} + ${CRYPTO_LIB} + ${READLINE_LIB} fkparse Qt6::Qml Qt6::Gui + Qt6::Widgets Qt6::Network Qt6::Multimedia ) diff --git a/src/client/client.cpp b/src/client/client.cpp index 9194324b..0d7aa44c 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -32,7 +32,7 @@ Client::~Client() router->getSocket()->deleteLater(); } -void Client::connectToHost(const QHostAddress& server, ushort port) +void Client::connectToHost(const QString &server, ushort port) { router->getSocket()->connectToHost(server, port); } diff --git a/src/client/client.h b/src/client/client.h index 557a2398..6586947b 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -11,7 +11,7 @@ public: Client(QObject *parent = nullptr); ~Client(); - void connectToHost(const QHostAddress &server, ushort port); + void connectToHost(const QString &server, ushort port); Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData); Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData); diff --git a/src/core/util.cpp b/src/core/util.cpp index 9df01962..76a0003c 100644 --- a/src/core/util.cpp +++ b/src/core/util.cpp @@ -1,4 +1,7 @@ #include "util.h" +#include +#include +#include extern "C" { int luaopen_fk(lua_State *); @@ -24,7 +27,7 @@ bool DoLuaScript(lua_State *L, const char *script) if (error) { const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; + qCritical() << error_msg; lua_pop(L, 2); return false; } @@ -65,7 +68,7 @@ sqlite3 *OpenDatabase(const QString &filename) if (!QFile::exists(filename)) { QFile file("./server/init.sql"); if (!file.open(QIODevice::ReadOnly)) { - qDebug() << "cannot open init.sql. Quit now."; + qFatal("cannot open init.sql. Quit now."); qApp->exit(1); } @@ -75,7 +78,7 @@ sqlite3 *OpenDatabase(const QString &filename) rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg); if (rc != SQLITE_OK ) { - qDebug() << "sqlite error:" << err_msg; + qCritical() << "sqlite error:" << err_msg; sqlite3_free(err_msg); sqlite3_close(ret); qApp->exit(1); @@ -83,7 +86,7 @@ sqlite3 *OpenDatabase(const QString &filename) } else { rc = sqlite3_open(filename.toLatin1().data(), &ret); if (rc != SQLITE_OK) { - qDebug() << "Cannot open database:" << sqlite3_errmsg(ret); + qCritical() << "Cannot open database:" << sqlite3_errmsg(ret); sqlite3_close(ret); qApp->exit(1); } @@ -121,3 +124,75 @@ void ExecSQL(sqlite3 *db, const QString &sql) { void CloseDatabase(sqlite3 *db) { sqlite3_close(db); } + +RSA *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); + 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; +} + +static void writeFileMD5(QFile &dest, const QString &fname) { + QFile f(fname); + if (!f.open(QIODevice::ReadOnly)) { + return; + } + + auto data = f.readAll(); + auto hash = QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex(); + dest.write(fname.toUtf8() + '=' + hash + '\n'); +} + +static void writeDirMD5(QFile &dest, const QString &dir, const QString &filter) { + QDir d(dir); + auto entries = d.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); + auto re = QRegularExpression::fromWildcard(filter); + foreach (QFileInfo info, entries) { + if (info.isDir()) { + writeDirMD5(dest, info.filePath(), filter); + } else { + if (re.match(info.fileName()).hasMatch()) { + writeFileMD5(dest, info.filePath()); + } + } + } +} + +QString calcFileMD5() { + // First, generate flist.txt + // flist.txt is a file contains all md5sum for code files + QFile flist("flist.txt"); + if (!flist.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + qFatal("Cannot open flist.txt. Quitting."); + } + + writeDirMD5(flist, "lua", "*.lua"); + writeDirMD5(flist, "qml", "*.qml"); + writeDirMD5(flist, "qml", "*.js"); + + // then, return flist.txt's md5 + flist.close(); + flist.open(QIODevice::ReadOnly); + auto ret = QCryptographicHash::hash(flist.readAll(), QCryptographicHash::Md5); + flist.close(); + return ret.toHex(); +} + diff --git a/src/core/util.h b/src/core/util.h index 3f1676fa..f8257d92 100644 --- a/src/core/util.h +++ b/src/core/util.h @@ -13,4 +13,8 @@ QString SelectFromDb(sqlite3 *db, const QString &sql); void ExecSQL(sqlite3 *db, const QString &sql); void CloseDatabase(sqlite3 *db); +RSA *InitServerRSA(); + +QString calcFileMD5(); + #endif // _GLOBAL_H diff --git a/src/main.cpp b/src/main.cpp index f21d275f..7d8dc250 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,13 @@ #include "qmlbackend.h" #include "server.h" +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +#include "shell.h" +#endif + +#include +#include + #ifdef Q_OS_ANDROID static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) { @@ -31,16 +38,37 @@ static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) } #endif +void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + fprintf(stderr, "\r[%s] ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); + auto localMsg = msg.toUtf8().constData(); + auto threadName = QThread::currentThread()->objectName().toLatin1().constData(); + switch (type) { + case QtDebugMsg: + fprintf(stderr, "[%s/\e[1;30mDEBUG\e[0m] %s\n", threadName, localMsg); + break; + case QtInfoMsg: + fprintf(stderr, "[%s/\e[1;32mINFO\e[0m] %s\n", threadName, localMsg); + break; + case QtWarningMsg: + fprintf(stderr, "[%s/\e[1;33mWARNING\e[0m] %s\n", threadName, localMsg); + break; + case QtCriticalMsg: + fprintf(stderr, "[%s/\e[1;31mCRITICAL\e[0m] %s\n", threadName, localMsg); + break; + case QtFatalMsg: + fprintf(stderr, "[%s/\e[1;31mFATAL\e[0m] %s\n", threadName, localMsg); + break; + } +} + int main(int argc, char *argv[]) { + QThread::currentThread()->setObjectName("Main"); + qInstallMessageHandler(fkMsgHandler); QCoreApplication *app; QCoreApplication::setApplicationName("FreeKill"); QCoreApplication::setApplicationVersion("Alpha 0.0.1"); -#ifdef Q_OS_ANDROID - copyPath("assets:/res", QDir::currentPath()); -#endif - QCommandLineParser parser; parser.setApplicationDescription("FreeKill server"); parser.addHelpOption(); @@ -62,14 +90,38 @@ int main(int argc, char *argv[]) serverPort = parser.value("server").toInt(); Server *server = new Server; if (!server->listen(QHostAddress::Any, serverPort)) { - fprintf(stderr, "cannot listen on port %d!\n", serverPort); + qFatal("cannot listen on port %d!\n", serverPort); app->exit(1); + } else { + qInfo("Server is listening on port %d", serverPort); +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + auto shell = new Shell; + shell->start(); +#endif } return app->exec(); } - app = new QGuiApplication(argc, argv); + app = new QApplication(argc, argv); +#define SHOW_SPLASH_MSG(msg) \ + splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom); + +#ifdef Q_OS_ANDROID + QScreen *screen = qobject_cast(app)->primaryScreen(); + QRect screenGeometry = screen->geometry(); + int screenWidth = screenGeometry.width(); + int screenHeight = screenGeometry.height(); + QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg").scaled(screenWidth, screenHeight)); + splash.showFullScreen(); + SHOW_SPLASH_MSG("Copying resources..."); + copyPath("assets:/res", QDir::currentPath()); +#else + QSplashScreen splash(QPixmap("image/splash.jpg")); + splash.show(); +#endif + + SHOW_SPLASH_MSG("Loading qml files..."); QQmlApplicationEngine *engine = new QQmlApplicationEngine; QmlBackend backend; @@ -83,10 +135,16 @@ int main(int argc, char *argv[]) bool debugging = false; #endif engine->rootContext()->setContextProperty("Debugging", debugging); +#ifdef Q_OS_ANDROID + engine->rootContext()->setContextProperty("Android", true); +#else + engine->rootContext()->setContextProperty("Android", false); +#endif engine->load("qml/main.qml"); if (engine->rootObjects().isEmpty()) return -1; + splash.close(); int ret = app->exec(); // delete the engine first diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index e0ea33b6..78c51acc 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -26,7 +26,7 @@ void ClientSocket::init() this, &ClientSocket::raiseError); } -void ClientSocket::connectToHost(const QHostAddress &address, ushort port) +void ClientSocket::connectToHost(const QString &address, ushort port) { socket->connectToHost(address, port); } diff --git a/src/network/client_socket.h b/src/network/client_socket.h index 279bd85d..32034acb 100644 --- a/src/network/client_socket.h +++ b/src/network/client_socket.h @@ -9,7 +9,7 @@ public: // For server use ClientSocket(QTcpSocket *socket); - void connectToHost(const QHostAddress &address = QHostAddress::LocalHost, ushort port = 9527u); + void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u); void disconnectFromHost(); void send(const QByteArray& msg); bool isConnected() const; diff --git a/src/network/router.cpp b/src/network/router.cpp index 5db60c39..ab1db819 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -3,6 +3,7 @@ #include "client_socket.h" #include "server.h" #include "serverplayer.h" +#include "util.h" Router::Router(QObject *parent, ClientSocket *socket, RouterType type) : QObject(parent) @@ -142,6 +143,60 @@ void Router::abortRequest() void Router::handlePacket(const QByteArray& rawPacket) { + static QMap lobby_actions; + if (lobby_actions.size() <= 0) { + lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender, const QString &jsonData){ + auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto avatar = arr[0].toString(); + static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); + if (!nameExp.match(avatar).hasMatch()) { + 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); + } + }; + lobby_actions["UpdatePassword"] = [](ServerPlayer *sender, const QString &jsonData){ + auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).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 result = SelectFromDatabase(ServerInstance->getDatabase(), sql_find); + passed = (result["password"].toArray()[0].toString() == + QCryptographicHash::hash( + oldpw.append(result["salt"].toArray()[0].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"].toArray()[0].toString()).toLatin1(), + QCryptographicHash::Sha256).toHex()) + .arg(sender->getId()); + ExecSQL(ServerInstance->getDatabase(), sql_update); + } + + sender->doNotify("UpdatePassword", passed ? "1" : "0"); + }; + lobby_actions["CreateRoom"] = [](ServerPlayer *sender, const QString &jsonData){ + auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto name = arr[0].toString(); + auto capacity = arr[1].toInt(); + ServerInstance->createRoom(sender, name, capacity); + }; + lobby_actions["EnterRoom"] = [](ServerPlayer *sender, const QString &jsonData){ + auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); + auto roomId = arr[0].toInt(); + ServerInstance->findRoom(roomId)->addPlayer(sender); + }; + lobby_actions["Chat"] = [](ServerPlayer *sender, const QString &jsonData){ + sender->getRoom()->chat(sender, jsonData); + }; + } + QJsonDocument packet = QJsonDocument::fromJson(rawPacket); if (packet.isNull() || !packet.isArray()) return; @@ -156,12 +211,19 @@ void Router::handlePacket(const QByteArray& rawPacket) ClientInstance->callLua(command, jsonData); } else { ServerPlayer *player = qobject_cast(parent()); - // Add the uid of sender to jsonData - QJsonArray arr = QJsonDocument::fromJson(jsonData.toUtf8()).array(); - arr.prepend(player->getId()); Room *room = player->getRoom(); - room->callLua(command, QJsonDocument(arr).toJson()); + if (room->isLobby() && lobby_actions.contains(command)) + lobby_actions[command](player, jsonData); + else { + if (command == "QuitRoom") { + room->removePlayer(player); + } else if (command == "AddRobot") { + room->addRobot(player); + } else if (command == "Chat") { + room->chat(player, jsonData); + } + } } } else if (type & TYPE_REQUEST) { diff --git a/src/pch.h b/src/pch.h index 0422516c..a09b3d3a 100644 --- a/src/pch.h +++ b/src/pch.h @@ -3,7 +3,7 @@ // core gui qml #include -#include +#include #include // network @@ -15,4 +15,10 @@ typedef int LuaFunction; #include "lua.hpp" #include "sqlite3.h" + // Note: headers of openssl is too big, so they are not provided in git repo + // Please install openssl's src via Qt Installer, then copy headers + // (/Tools/OpenSSL/src/include/openssl) to /mingw_64/include +#include +#include + #endif // _PCH_H diff --git a/src/server/room.cpp b/src/server/room.cpp index c741bfe5..50de1c2c 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -5,27 +5,26 @@ Room::Room(Server* server) { + setObjectName("Room"); id = server->nextRoomId; server->nextRoomId++; this->server = server; setParent(server); + m_abandoned = false; owner = nullptr; gameStarted = false; - robot_id = -1; + robot_id = -2; // -1 is reserved in UI logic timeout = 15; + L = NULL; if (!isLobby()) { connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); - } - L = CreateLuaState(); - DoLuaScript(L, "lua/freekill.lua"); - if (isLobby()) { - DoLuaScript(L, "lua/server/lobby.lua"); - } else { + L = CreateLuaState(); + DoLuaScript(L, "lua/freekill.lua"); DoLuaScript(L, "lua/server/room.lua"); + initLua(); } - initLua(); } Room::~Room() @@ -35,7 +34,7 @@ Room::~Room() terminate(); wait(); } - lua_close(L); + if (L) lua_close(L); } Server *Room::getServer() const @@ -48,6 +47,11 @@ int Room::getId() const return id; } +void Room::setId(int id) +{ + this->id = id; +} + bool Room::isLobby() const { return id == 0; @@ -80,6 +84,9 @@ bool Room::isFull() const bool Room::isAbandoned() const { + if (isLobby()) + return false; + if (players.isEmpty()) return true; @@ -90,6 +97,10 @@ bool Room::isAbandoned() const return true; } +void Room::setAbandoned(bool abandoned) { + m_abandoned = abandoned; +} + ServerPlayer *Room::getOwner() const { return owner; @@ -201,10 +212,11 @@ void Room::removePlayer(ServerPlayer *player) server->addPlayer(runner); emit playerRemoved(runner); - runner->abortRequest(); + player->abortRequest(); } - if (isAbandoned()) { + if (isAbandoned() && !m_abandoned) { + m_abandoned = true; emit abandoned(); } else if (player == owner) { setOwner(players.first()); @@ -265,6 +277,17 @@ void Room::doBroadcastNotify(const QList targets, } } +void Room::chat(ServerPlayer *sender, const QString &jsonData) { + auto doc = QJsonDocument::fromJson(jsonData.toUtf8()).object(); + auto type = doc["type"].toInt(); + doc["type"] = sender->getId(); + if (type == 1) { + // TODO: server chatting + } else { + doBroadcastNotify(players, "Chat", QJsonDocument(doc).toJson(QJsonDocument::Compact)); + } +} + void Room::gameOver() { gameStarted = false; @@ -275,6 +298,8 @@ void Room::gameOver() p->deleteLater(); } } + players.clear(); + owner = nullptr; } void Room::run() diff --git a/src/server/room.h b/src/server/room.h index 0f8b9597..76c4e452 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -14,6 +14,7 @@ public: // ==================================={ Server *getServer() const; int getId() const; + void setId(int id); bool isLobby() const; QString getName() const; void setName(const QString &name); @@ -21,6 +22,7 @@ public: void setCapacity(int capacity); bool isFull() const; bool isAbandoned() const; + void setAbandoned(bool abandoned); // never use this function ServerPlayer *getOwner() const; void setOwner(ServerPlayer *owner); @@ -46,12 +48,11 @@ public: const QString &command, const QString &jsonData ); + void chat(ServerPlayer *sender, const QString &jsonData); void gameOver(); void initLua(); - void callLua(const QString &command, const QString &jsonData); - LuaFunction callback; void roomStart(); LuaFunction startGame; diff --git a/src/server/server.cpp b/src/server/server.cpp index 57b9dd7a..d7c9caa4 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -13,6 +13,13 @@ Server::Server(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(); + server = new ServerSocket(); server->setParent(this); connect(server, &ServerSocket::new_connection, @@ -30,6 +37,7 @@ Server::~Server() ServerInstance = nullptr; m_lobby->deleteLater(); sqlite3_close(db); + RSA_free(rsa); } bool Server::listen(const QHostAddress& address, ushort port) @@ -39,12 +47,21 @@ bool Server::listen(const QHostAddress& address, ushort port) void Server::createRoom(ServerPlayer* owner, const QString &name, int capacity) { - Room *room = new Room(this); - connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); - if (room->isLobby()) - m_lobby = room; - else + Room *room; + if (!idle_rooms.isEmpty()) { + room = idle_rooms.pop(); + room->setId(nextRoomId); + nextRoomId++; + room->setAbandoned(false); rooms.insert(room->getId(), room); + } else { + room = new Room(this); + connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); + if (room->isLobby()) + m_lobby = room; + else + rooms.insert(room->getId(), room); + } room->setName(name); room->setCapacity(capacity); @@ -105,11 +122,11 @@ sqlite3 *Server::getDatabase() { void Server::processNewConnection(ClientSocket* client) { - qDebug() << client->peerAddress() << "connected"; + qInfo() << client->peerAddress() << "connected"; // version check, file check, ban IP, reconnect, etc connect(client, &ClientSocket::disconnected, this, [client](){ - qDebug() << client->peerAddress() << "disconnected"; + qInfo() << client->peerAddress() << "disconnected"; }); // network delay test @@ -117,7 +134,7 @@ void Server::processNewConnection(ClientSocket* client) body << -2; body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); body << "NetworkDelayTest"; - body << "[]"; + body << public_key; client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); // Note: the client should send a setup string next connect(client, &ClientSocket::message_got, this, &Server::processRequest); @@ -142,11 +159,11 @@ void Server::processRequest(const QByteArray& msg) ) valid = false; else - valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 2); + valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 3); } if (!valid) { - qDebug() << "Invalid setup string:" << msg; + qWarning() << "Invalid setup string:" << msg; QJsonArray body; body << -2; body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); @@ -158,6 +175,19 @@ void Server::processRequest(const QByteArray& msg) } QJsonArray arr = QJsonDocument::fromJson(doc[3].toString().toUtf8()).array(); + + if (md5 != arr[2].toString()) { + qWarning() << "MD5 check failed!"; + QJsonArray body; + body << -2; + body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); + body << "ErrorMsg"; + body << "MD5 check failed!"; + client->send(QJsonDocument(body).toJson(QJsonDocument::Compact)); + client->disconnectFromHost(); + return; + } + handleNameAndPassword(client, arr[0].toString(), arr[1].toString()); } @@ -165,24 +195,34 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co { // First check the name and password // Matches a string that does not contain special characters - QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); - QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex(); + static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]"); + + 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)); bool passed = false; QString error_msg; QJsonObject result; - if (!nameExp.match(name).hasMatch()) { + if (!nameExp.match(name).hasMatch() && !name.isEmpty()) { // Then we check the database, QString sql_find = QString("SELECT * FROM userinfo \ WHERE name='%1';").arg(name); result = SelectFromDatabase(db, sql_find); QJsonArray arr = result["password"].toArray(); if (arr.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,\ - avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4',%5);") + 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"); @@ -194,6 +234,9 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co int id = result["id"].toArray()[0].toString().toInt(); if (!players.value(id)) { // check if password is the same + auto salt = result["salt"].toArray()[0].toString().toLatin1(); + decrypted_pw.append(salt); + auto passwordHash = QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256).toHex(); passed = (passwordHash == arr[0].toString()); if (!passed) error_msg = "username or password error"; } else { @@ -226,7 +269,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co lobby()->addPlayer(player); } else { - qDebug() << client->peerAddress() << "lost connection:" << error_msg; + qInfo() << client->peerAddress() << "lost connection:" << error_msg; QJsonArray body; body << -2; body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT); @@ -244,13 +287,22 @@ void Server::onRoomAbandoned() room->gameOver(); rooms.remove(room->getId()); updateRoomList(); - room->deleteLater(); + //room->deleteLater(); + if (room->isRunning()) { + room->terminate(); + room->wait(); + } + idle_rooms.push(room); +#ifdef QT_DEBUG + qDebug() << rooms.size() << "running room(s)," + << idle_rooms.size() << "idle room(s)."; +#endif } void Server::onUserDisconnected() { ServerPlayer *player = qobject_cast(sender()); - qDebug() << "Player" << player->getId() << "disconnected"; + qInfo() << "Player" << player->getId() << "disconnected"; Room *room = player->getRoom(); if (room->isStarted()) { player->setState(Player::Offline); diff --git a/src/server/server.h b/src/server/server.h index 2040e412..aa661336 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -42,14 +42,19 @@ public slots: void onUserStateChanged(); private: + friend class Shell; ServerSocket *server; Room *m_lobby; QMap rooms; + QStack idle_rooms; int nextRoomId; friend Room::Room(Server *server); QHash players; + RSA *rsa; + QString public_key; sqlite3 *db; + QString md5; void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password); }; diff --git a/src/server/shell.cpp b/src/server/shell.cpp new file mode 100644 index 00000000..01dc3b28 --- /dev/null +++ b/src/server/shell.cpp @@ -0,0 +1,116 @@ +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +#include "shell.h" +#include "server.h" +#include "serverplayer.h" +#include +#include +#include + +static void sigintHandler(int) { + fprintf(stderr, "\n"); + rl_reset_line_state(); + rl_replace_line("", 0); + rl_crlf(); + rl_redisplay(); +} + +const char *Shell::ColoredText(const char *input, Color color, TextType type) { + QString str(input); + str.append("\e[0m"); + QString header = "\e["; + switch (type) { + case NoType: + header.append("0"); + break; + case Bold: + header.append("1"); + break; + case UnderLine: + header.append("4"); + break; + } + header.append(";"); + header.append(QString::number(30 + color)); + header.append("m"); + header.append(str); + return header.toUtf8().constData(); +} + +void Shell::helpCommand(QStringList &) { + qInfo("Frequently used commands:"); + qInfo("%s: Display this help message.", ColoredText("help", Blue)); + qInfo("%s: Shut down the server.", ColoredText("quit", Blue)); + qInfo("%s: List all online players.", ColoredText("lsplayer", Blue)); + qInfo("%s: List all running rooms.", ColoredText("lsroom", Blue)); + qInfo("For more commands, check the documentation."); +} + +void Shell::lspCommand(QStringList &) { + if (ServerInstance->players.size() == 0) { + qInfo("No online player."); + return; + } + qInfo("Current %d online player(s) are:", ServerInstance->players.size()); + foreach (auto player, ServerInstance->players) { + qInfo() << player->getId() << "," << player->getScreenName(); + } +} + +void Shell::lsrCommand(QStringList &) { + if (ServerInstance->rooms.size() == 0) { + qInfo("No running room."); + return; + } + qInfo("Current %d running rooms are:", ServerInstance->rooms.size()); + foreach (auto room, ServerInstance->rooms) { + qInfo() << room->getId() << "," << room->getName(); + } +} + +Shell::Shell() { + setObjectName("Shell"); + signal(SIGINT, sigintHandler); + + static QHash handlers; + if (handlers.size() == 0) { + handlers["help"] = &Shell::helpCommand; + handlers["?"] = &Shell::helpCommand; + handlers["lsplayer"] = &Shell::lspCommand; + handlers["lsroom"] = &Shell::lsrCommand; + } + handler_map = handlers; +} + +void Shell::run() { + printf("\rFreeKill, Copyright (C) 2022, GNU GPL'd, by Notify et al.\n"); + printf("This program comes with ABSOLUTELY NO WARRANTY.\n"); + printf("This is free software, and you are welcome to redistribute it under\n"); + printf("certain conditions; For more information visit http://www.gnu.org/licenses.\n\n"); + printf("This is server cli. Enter \"help\" for usage hints.\n"); + + while (true) { + char *bytes = readline("fk> "); + if (!bytes || !strcmp(bytes, "quit")) { + qInfo("Server is shutting down."); + qApp->quit(); + return; + } + + if (*bytes) + add_history(bytes); + + auto command = QString(bytes); + auto command_list = command.split(' '); + auto func = handler_map[command_list.first()]; + if (!func) { + qWarning("Unknown command \"%s\". Type \"help\" for hints.", command_list.first().toUtf8().constData()); + } else { + command_list.removeFirst(); + (this->*func)(command_list); + } + + free(bytes); + } +} + +#endif diff --git a/src/server/shell.h b/src/server/shell.h new file mode 100644 index 00000000..1bcb1c3e --- /dev/null +++ b/src/server/shell.h @@ -0,0 +1,36 @@ +#ifndef _SHELL_H +#define _SHELL_H + +class Shell: public QThread { + Q_OBJECT +public: + Shell(); + + enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + }; + enum TextType { + NoType, + Bold, + UnderLine + }; + static const char *ColoredText(const char *input, Color color, TextType type = NoType); + +protected: + virtual void run(); + +private: + QHash handler_map; + void helpCommand(QStringList &); + void quitCommand(QStringList &); + void lspCommand(QStringList &); + void lsrCommand(QStringList &); +}; + +#endif diff --git a/src/swig/client.i b/src/swig/client.i index cf739cc2..6b626712 100644 --- a/src/swig/client.i +++ b/src/swig/client.i @@ -48,7 +48,7 @@ void Client::callLua(const QString& command, const QString& json_data) if (error) { const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; + qCritical() << error_msg; lua_pop(L, 2); } lua_pop(L, 1); diff --git a/src/swig/qt.i b/src/swig/qt.i index 195c79aa..d77a4e9d 100644 --- a/src/swig/qt.i +++ b/src/swig/qt.i @@ -46,3 +46,8 @@ static int GetMicroSecond(lua_State *L) { return 1; } %} + +void qDebug(const char *msg, ...); +void qInfo(const char *msg, ...); +void qWarning(const char *msg, ...); +void qCritical(const char *msg, ...); diff --git a/src/swig/server.i b/src/swig/server.i index a3c5f4cc..d3344b4e 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -52,7 +52,6 @@ public: void gameOver(); - LuaFunction callback; LuaFunction startGame; }; @@ -68,33 +67,10 @@ void Room::initLua() lua_pop(L, 1); if (error) { const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; + qCritical() << error_msg; } } -void Room::callLua(const QString& command, const QString& json_data) -{ - Q_ASSERT(callback); - - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); - - lua_rawgeti(L, LUA_REGISTRYINDEX, callback); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); - lua_pushstring(L, command.toUtf8()); - lua_pushstring(L, json_data.toUtf8()); - - int error = lua_pcall(L, 3, 0, -5); - - if (error) { - const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; - lua_pop(L, 2); - } - lua_pop(L, 1); -} - void Room::roomStart() { Q_ASSERT(startGame); @@ -109,7 +85,7 @@ void Room::roomStart() { if (error) { const char *error_msg = lua_tostring(L, -1); - qDebug() << error_msg; + qCritical() << error_msg; lua_pop(L, 2); } lua_pop(L, 1); diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index 75a53aef..8443ee20 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -1,6 +1,7 @@ #include "qmlbackend.h" #include "server.h" #include "client.h" +#include "util.h" QmlBackend *Backend; @@ -9,12 +10,14 @@ QmlBackend::QmlBackend(QObject* parent) { Backend = this; engine = nullptr; + rsa = RSA_new(); parser = fkp_new_parser(); } QmlBackend::~QmlBackend() { Backend = nullptr; + RSA_free(rsa); fkp_close(parser); } @@ -60,7 +63,7 @@ void QmlBackend::joinServer(QString address) addr = address; } - client->connectToHost(QHostAddress(addr), port); + client->connectToHost(addr, port); } void QmlBackend::quitLobby() @@ -101,7 +104,7 @@ QString QmlBackend::translate(const QString &src) { int err = lua_pcall(L, 1, 1, 0); const char *result = lua_tostring(L, -1); if (err) { - qDebug() << result; + qCritical() << result; lua_pop(L, 1); return ""; } @@ -135,7 +138,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) { } break; default: - qDebug() << "cannot handle QVariant type" << v.typeId(); + qCritical() << "cannot handle QVariant type" << v.typeId(); lua_pushnil(L); break; } @@ -154,7 +157,7 @@ QString QmlBackend::callLuaFunction(const QString &func_name, int err = lua_pcall(L, params.length(), 1, 0); const char *result = lua_tostring(L, -1); if (err) { - qDebug() << result; + qCritical() << result; lua_pop(L, 1); return ""; } @@ -162,6 +165,46 @@ QString QmlBackend::callLuaFunction(const QString &func_name, return QString(result); } +QString QmlBackend::pubEncrypt(const QString &key, const QString &data) { + BIO *keyio = BIO_new_mem_buf(key.toLatin1().data(), -1); + PEM_read_bio_RSAPublicKey(keyio, &rsa, NULL, NULL); + BIO_free_all(keyio); + + unsigned char buf[RSA_size(rsa)]; + RSA_public_encrypt(data.length(), (const unsigned char *)data.toUtf8().data(), + buf, rsa, RSA_PKCS1_PADDING); + return QByteArray::fromRawData((const char *)buf, RSA_size(rsa)).toBase64(); +} + +QString QmlBackend::loadConf() { + QFile conf("freekill.client.config.json"); + if (!conf.exists()) { + conf.open(QIODevice::WriteOnly); + static const char *init_conf = "{\ + \"winWidth\": 960,\ + \"winHeight\": 540,\ + \"lastLoginServer\": \"127.0.0.1\",\ + \"savedPassword\": {\ + \"127.0.0.1\": {\ + \"username\": \"player\",\ + \"password\": \"\",\ + \"shorten_password\": \"\"\ + }\ + }\ + }"; + conf.write(init_conf); + return init_conf; + } + conf.open(QIODevice::ReadOnly); + return conf.readAll(); +} + +void QmlBackend::saveConf(const QString &conf) { + QFile c("freekill.client.config.json"); + c.open(QIODevice::WriteOnly); + c.write(conf.toUtf8()); +} + void QmlBackend::parseFkp(const QString &fileName) { if (!QFile::exists(fileName)) { // errorEdit->setText(tr("File does not exist!")); @@ -215,3 +258,8 @@ void QmlBackend::readHashFromParser() { copyFkpHash2QHash(skills, parser->skills); copyFkpHash2QHash(marks, parser->marks); } + +QString QmlBackend::calcFileMD5() { + return ::calcFileMD5(); +} + diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h index 423168d8..fd573301 100644 --- a/src/ui/qmlbackend.h +++ b/src/ui/qmlbackend.h @@ -2,6 +2,7 @@ #define _QMLBACKEND_H #include "fkparse.h" +#include class QmlBackend : public QObject { Q_OBJECT @@ -32,14 +33,21 @@ public: Q_INVOKABLE QString translate(const QString &src); Q_INVOKABLE QString callLuaFunction(const QString &func_name, QVariantList params); + + Q_INVOKABLE QString pubEncrypt(const QString &key, const QString &data); + Q_INVOKABLE QString loadConf(); + Q_INVOKABLE void saveConf(const QString &conf); // support fkp Q_INVOKABLE void parseFkp(const QString &filename); + Q_INVOKABLE QString calcFileMD5(); + signals: void notifyUI(const QString &command, const QString &jsonData); private: QQmlApplicationEngine *engine; + RSA *rsa; fkp_parser *parser; QHash generals; QHash skills;