Merge Dev (#31)
* splash screen when app is loading * doRaceRequest * prepare to add fkparse feature * player mark operation * dont call lua in regular room * dont call lua in lobby * clean up * idle_room in Cpp's class Server * fix many small bugs * Security enhancement (#27) * use RSA encryption when sending password * update fkp's url so other can clone it * add salt to password * save password * fix default config bug * fix room reuse bug * disable empty usr name * how to compile (#28) * add some doc * how to compile * update readme * Actions (#29) * judge(not tested) * logic of chat * sendlog at most scenario * adjust ui, add shortcuts * ui, z axis of cardArea * create server cli, improve logging * basic shell using * use gnu readline instead * use static QRegularExp * fix android build * fix automoc problem * MD5 check * md5 check bugfix * cardEffectEvent (#30) * cardEffectEvent * add TODOs * thinking Co-authored-by: Ho-spair <62695577+Ho-spair@users.noreply.github.com>
This commit is contained in:
parent
7b12d82683
commit
a02410c282
|
@ -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/
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[submodule "fkparse"]
|
||||
path = fkparse
|
||||
url = git@github.com:Notify-ctrl/fkparse
|
||||
url = https://github.com/Notify-ctrl/fkparse
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
assets/
|
||||
res/
|
||||
build.sh
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
rm -rf res assets
|
||||
|
||||
if [ ! -e res/mipmap ]; then
|
||||
mkdir -p res/mipmap
|
||||
fi
|
||||
|
|
|
@ -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环境变量里面去。
|
||||
|
||||
之后,把<Qt_root>/Tools/OpenSSL/src/include/openssl这个文件夹复制到<Qt_root>/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就能编译了。
|
|
@ -0,0 +1,23 @@
|
|||
# 游戏逻辑
|
||||
|
||||
> [dev](./index.md) > 游戏逻辑
|
||||
|
||||
___
|
||||
|
||||
## 概述
|
||||
|
||||
FreeKill的游戏相关处理逻辑完全使用lua实现。在服务端上,每个Room都有自己的lua_State,并且只会在Room线程启动后才会去调用lua函数进行游戏逻辑处理。
|
||||
|
||||
本文档将简要介绍几个最为复杂的逻辑实现。
|
||||
|
||||
___
|
||||
|
||||
## 触发技
|
||||
|
||||
___
|
||||
|
||||
## 移动牌
|
||||
|
||||
___
|
||||
|
||||
## 使用牌
|
|
@ -6,6 +6,8 @@ ___
|
|||
|
||||
FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在UI方面使用的是Qt Quick。
|
||||
|
||||
- [编译](./compile.md)
|
||||
- [通信](./protocol.md)
|
||||
- [游戏逻辑](./gamelogic.md)
|
||||
- [数据库](./database.md)
|
||||
- [UI](./ui.md)
|
||||
|
|
|
@ -36,8 +36,8 @@ $ ./FreeKill -s <port>
|
|||
每当任何一个客户端连接上了之后,游戏会先进行以下流程:
|
||||
|
||||
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)
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
@ -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('<font color="' .. color .. '"><b>%s</b></font>', 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('<font color="#0598BC"><b>%s</b></font>', 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"
|
||||
|
|
|
@ -256,4 +256,51 @@ Fk:loadTranslationTable{
|
|||
["$Equip"] = "装备区",
|
||||
["$Judge"] = "判定区",
|
||||
["#AskForUseActiveSkill"] = "请使用技能 %1",
|
||||
|
||||
["Trust"] = "托管",
|
||||
["Sort Cards"] = "牌序",
|
||||
["Chat"] = "聊天",
|
||||
["Log"] = "战报",
|
||||
}
|
||||
|
||||
-- related to sendLog
|
||||
Fk:loadTranslationTable{
|
||||
-- game processing
|
||||
["$AppendSeparator"] = '<font color="grey">------------------------------</font>',
|
||||
["$GameStart"] = "== 游戏开始 ==",
|
||||
["$GameEnd"] = "== 游戏结束 ==",
|
||||
|
||||
-- get/lose skill
|
||||
["#AcquireSkill"] = "%from 获得了技能“%arg”",
|
||||
["#LoseSkill"] = "%from 失去了技能“%arg”",
|
||||
|
||||
-- moveCards (they are sent by notifyMoveCards)
|
||||
["unknown_card"] = '<font color="#B5BA00"><b>未知牌</b></font>',
|
||||
["log_spade"] = "♠",
|
||||
["log_heart"] = '<font color="#CC3131">♥</font>',
|
||||
["log_club"] = "♣",
|
||||
["log_diamond"] = '<font color="#CC3131">♦</font>',
|
||||
["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"] = "背面朝上",
|
||||
}
|
||||
|
|
|
@ -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('<font color="#0598BC"><b>%s</b></font>', Fk:translate(self.name) .. "[")
|
||||
ret = ret .. Fk:translate("log_" .. self:getSuitString())
|
||||
if self.number > 0 then
|
||||
ret = ret .. string.format('<font color="%s"><b>%s</b></font>', self.color == Card.Red and "#CC3131" or "black", getNumberStr(self.number))
|
||||
end
|
||||
ret = ret .. '<font color="#0598BC"><b>]</b></font>'
|
||||
return ret
|
||||
end
|
||||
|
||||
return Card
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"] = "无懈可击",
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
37
qml/Logic.js
37
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}`);
|
||||
}
|
||||
|
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
29
qml/main.qml
29
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
#include "util.h"
|
||||
#include <qcryptographichash.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qregularexpression.h>
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
70
src/main.cpp
70
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 <QSplashScreen>
|
||||
#include <QScreen>
|
||||
|
||||
#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<QApplication *>(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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<QString, void (*)(ServerPlayer *, const QString &)> 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<ServerPlayer *>(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) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
// core gui qml
|
||||
#include <QtCore>
|
||||
#include <QGuiApplication>
|
||||
#include <QApplication>
|
||||
#include <QtQml>
|
||||
|
||||
// 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
|
||||
// (<Qt_root>/Tools/OpenSSL/src/include/openssl) to <Qt6_dir>/mingw_64/include
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
#endif // _PCH_H
|
||||
|
|
|
@ -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<ServerPlayer *> 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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ServerPlayer *>(sender());
|
||||
qDebug() << "Player" << player->getId() << "disconnected";
|
||||
qInfo() << "Player" << player->getId() << "disconnected";
|
||||
Room *room = player->getRoom();
|
||||
if (room->isStarted()) {
|
||||
player->setState(Player::Offline);
|
||||
|
|
|
@ -42,14 +42,19 @@ public slots:
|
|||
void onUserStateChanged();
|
||||
|
||||
private:
|
||||
friend class Shell;
|
||||
ServerSocket *server;
|
||||
Room *m_lobby;
|
||||
QMap<int, Room *> rooms;
|
||||
QStack<Room *> idle_rooms;
|
||||
int nextRoomId;
|
||||
friend Room::Room(Server *server);
|
||||
QHash<int, ServerPlayer *> players;
|
||||
|
||||
RSA *rsa;
|
||||
QString public_key;
|
||||
sqlite3 *db;
|
||||
QString md5;
|
||||
|
||||
void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
#include "shell.h"
|
||||
#include "server.h"
|
||||
#include "serverplayer.h"
|
||||
#include <signal.h>
|
||||
#include <readline/readline.h>
|
||||
#include <readline/history.h>
|
||||
|
||||
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<QString, void (Shell::*)(QStringList &)> 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
|
|
@ -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<QString, void (Shell::*)(QStringList &)> handler_map;
|
||||
void helpCommand(QStringList &);
|
||||
void quitCommand(QStringList &);
|
||||
void lspCommand(QStringList &);
|
||||
void lsrCommand(QStringList &);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -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);
|
||||
|
|
|
@ -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, ...);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define _QMLBACKEND_H
|
||||
|
||||
#include "fkparse.h"
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
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<QString, QString> generals;
|
||||
QHash<QString, QString> skills;
|
||||
|
|
Loading…
Reference in New Issue