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/
|
build/
|
||||||
|
*.o
|
||||||
|
|
||||||
|
# IDE & LSP
|
||||||
.kdev4/
|
.kdev4/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.user
|
*.user
|
||||||
*-swp
|
*-swp
|
||||||
*.kdev4
|
*.kdev4
|
||||||
|
.cache/
|
||||||
|
tags
|
||||||
|
|
||||||
|
# file produced by game
|
||||||
FreeKill
|
FreeKill
|
||||||
FreeKill.exe
|
FreeKill.exe
|
||||||
freekill-wrap.cxx
|
freekill-wrap.cxx
|
||||||
server/users.db
|
server/users.db
|
||||||
|
server/rsa
|
||||||
|
server/rsa_pub
|
||||||
|
freekill.client.config.json
|
||||||
|
freekill.server.config.json
|
||||||
|
flist.txt
|
||||||
|
|
||||||
|
# windeployqt
|
||||||
bearer/
|
bearer/
|
||||||
iconengines/
|
iconengines/
|
||||||
imageformats/
|
imageformats/
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "fkparse"]
|
[submodule "fkparse"]
|
||||||
path = 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
|
find_package(Qt6 REQUIRED COMPONENTS
|
||||||
Gui
|
Gui
|
||||||
Qml
|
Qml
|
||||||
|
Widgets
|
||||||
Network
|
Network
|
||||||
Multimedia
|
Multimedia
|
||||||
)
|
)
|
||||||
|
|
||||||
|
find_package(OpenSSL)
|
||||||
find_package(Lua)
|
find_package(Lua)
|
||||||
find_package(SQLite3)
|
find_package(SQLite3)
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,4 @@ ___
|
||||||
|
|
||||||
## 如何构建
|
## 如何构建
|
||||||
|
|
||||||
FreeKill使用Qt6.3,支持的运行平台有Windows、Linux、Android。
|
[编译教程](./doc/dev/compile.md)
|
||||||
|
|
||||||
欲编译FreeKill,首先得从Qt官网的安装工具安装Qt Creator和Qt 6.3.2。安装时需要勾选CMake,应该默认就是选上的状态。
|
|
||||||
|
|
||||||
然后下载swig,并为其配置环境变量,即可构建FreeKill。
|
|
||||||
|
|
||||||
对于Linux用户而言,还需要自己从包管理器安装lua5.4和sqlite。
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
assets/
|
assets/
|
||||||
res/
|
res/
|
||||||
|
build.sh
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
rm -rf res assets
|
||||||
|
|
||||||
if [ ! -e res/mipmap ]; then
|
if [ ! -e res/mipmap ]; then
|
||||||
mkdir -p res/mipmap
|
mkdir -p res/mipmap
|
||||||
fi
|
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。
|
FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在UI方面使用的是Qt Quick。
|
||||||
|
|
||||||
|
- [编译](./compile.md)
|
||||||
- [通信](./protocol.md)
|
- [通信](./protocol.md)
|
||||||
|
- [游戏逻辑](./gamelogic.md)
|
||||||
- [数据库](./database.md)
|
- [数据库](./database.md)
|
||||||
- [UI](./ui.md)
|
- [UI](./ui.md)
|
||||||
|
|
|
@ -36,8 +36,8 @@ $ ./FreeKill -s <port>
|
||||||
每当任何一个客户端连接上了之后,游戏会先进行以下流程:
|
每当任何一个客户端连接上了之后,游戏会先进行以下流程:
|
||||||
|
|
||||||
1. 检查IP是否被封禁。 // TODO: 数据库
|
1. 检查IP是否被封禁。 // TODO: 数据库
|
||||||
2. 检查客户端的延迟是否小于30秒。
|
2. 服务端将RSA公钥发给客户端,然后检查客户端的延迟是否小于30秒。
|
||||||
3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和密码,服务端检查这个字符串是否合法。
|
3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和RSA公钥加密后的密码,服务端检查这个字符串是否合法。如果合法,检查密码是否正确。
|
||||||
4. 上述检查都通过后,重连(TODO:)
|
4. 上述检查都通过后,重连(TODO:)
|
||||||
5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。
|
5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。
|
||||||
|
|
||||||
|
@ -51,11 +51,12 @@ ___
|
||||||
|
|
||||||
1. 只要房间被添加玩家,那么那名玩家就自动从大厅移除。
|
1. 只要房间被添加玩家,那么那名玩家就自动从大厅移除。
|
||||||
2. 当玩家离开房间时,玩家便会自动进入大厅。
|
2. 当玩家离开房间时,玩家便会自动进入大厅。
|
||||||
3. 当所有玩家都离开房间后,房间被销毁。
|
3. 当所有玩家都离开房间后,房间被“销毁”(其实是进入Server的空闲房间列表,毕竟新建lua_State的开销十分大)。
|
||||||
|
|
||||||
大厅的特点:
|
大厅的特点:
|
||||||
|
|
||||||
1. 只要有玩家进入,就刷新一次房间列表。
|
1. 只要有玩家进入,就刷新一次房间列表。
|
||||||
|
2. 只要玩家变动,就更新大厅内人数(TODO:)
|
||||||
|
|
||||||
> 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。
|
> 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。
|
||||||
|
|
||||||
|
@ -88,6 +89,41 @@ ___
|
||||||
|
|
||||||
但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。
|
但是为了[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)
|
## 旁观(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
|
||||||
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)
|
fk.client_callback["Setup"] = function(jsonData)
|
||||||
-- jsonData: [ int id, string screenName, string avatar ]
|
-- jsonData: [ int id, string screenName, string avatar ]
|
||||||
local data = json.decode(jsonData)
|
local data = json.decode(jsonData)
|
||||||
|
@ -103,8 +172,10 @@ fk.client_callback["RemovePlayer"] = function(jsonData)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
fk.ClientInstance:removePlayer(id)
|
if id ~= Self.id then
|
||||||
ClientInstance:notifyUI("RemovePlayer", jsonData)
|
fk.ClientInstance:removePlayer(id)
|
||||||
|
ClientInstance:notifyUI("RemovePlayer", jsonData)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
fk.client_callback["ArrangeSeats"] = function(jsonData)
|
fk.client_callback["ArrangeSeats"] = function(jsonData)
|
||||||
|
@ -163,6 +234,7 @@ local function separateMoves(moves)
|
||||||
to = move.to,
|
to = move.to,
|
||||||
toArea = move.toArea,
|
toArea = move.toArea,
|
||||||
fromArea = info.fromArea,
|
fromArea = info.fromArea,
|
||||||
|
moveReason = move.moveReason,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -182,7 +254,8 @@ local function mergeMoves(moves)
|
||||||
from = move.from,
|
from = move.from,
|
||||||
to = move.to,
|
to = move.to,
|
||||||
fromArea = move.fromArea,
|
fromArea = move.fromArea,
|
||||||
toArea = move.toArea
|
toArea = move.toArea,
|
||||||
|
moveReason = move.moveReason,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
table.insert(temp[info].ids, move.ids[1])
|
table.insert(temp[info].ids, move.ids[1])
|
||||||
|
@ -193,12 +266,33 @@ local function mergeMoves(moves)
|
||||||
return ret
|
return ret
|
||||||
end
|
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)
|
fk.client_callback["MoveCards"] = function(jsonData)
|
||||||
-- jsonData: CardsMoveStruct[]
|
-- jsonData: CardsMoveStruct[]
|
||||||
local raw_moves = json.decode(jsonData)
|
local raw_moves = json.decode(jsonData)
|
||||||
local separated = separateMoves(raw_moves)
|
local separated = separateMoves(raw_moves)
|
||||||
ClientInstance:moveCards(separated)
|
ClientInstance:moveCards(separated)
|
||||||
local merged = mergeMoves(separated)
|
local merged = mergeMoves(separated)
|
||||||
|
for _, move in ipairs(merged) do
|
||||||
|
sendMoveCardLog(move)
|
||||||
|
end
|
||||||
ClientInstance:notifyUI("MoveCards", json.encode(merged))
|
ClientInstance:notifyUI("MoveCards", json.encode(merged))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -237,6 +331,30 @@ fk.client_callback["AskForUseActiveSkill"] = function(jsonData)
|
||||||
ClientInstance:notifyUI("AskForUseActiveSkill", jsonData)
|
ClientInstance:notifyUI("AskForUseActiveSkill", jsonData)
|
||||||
end
|
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)
|
-- Create ClientInstance (used by Lua)
|
||||||
ClientInstance = Client:new()
|
ClientInstance = Client:new()
|
||||||
dofile "lua/client/client_util.lua"
|
dofile "lua/client/client_util.lua"
|
||||||
|
|
|
@ -256,4 +256,51 @@ Fk:loadTranslationTable{
|
||||||
["$Equip"] = "装备区",
|
["$Equip"] = "装备区",
|
||||||
["$Judge"] = "判定区",
|
["$Judge"] = "判定区",
|
||||||
["#AskForUseActiveSkill"] = "请使用技能 %1",
|
["#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
|
elseif suit == Card.Diamond then
|
||||||
return "diamond"
|
return "diamond"
|
||||||
else
|
else
|
||||||
return "unknown"
|
return "nosuit"
|
||||||
end
|
end
|
||||||
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
|
return Card
|
||||||
|
|
|
@ -9,7 +9,6 @@ package.path = package.path .. ";./lua/lib/?.lua"
|
||||||
class = require "middleclass"
|
class = require "middleclass"
|
||||||
json = require "json"
|
json = require "json"
|
||||||
|
|
||||||
dofile "lua/lib/sha256.lua"
|
|
||||||
local GroupUtils = require "core.util"
|
local GroupUtils = require "core.util"
|
||||||
TargetGroup, AimGroup = table.unpack(GroupUtils)
|
TargetGroup, AimGroup = table.unpack(GroupUtils)
|
||||||
dofile "lua/core/debug.lua"
|
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.BeforeCardEffect = 55
|
||||||
fk.CardEffecting = 56
|
fk.CardEffecting = 56
|
||||||
fk.CardEffectFinished = 57
|
fk.CardEffectFinished = 57
|
||||||
|
fk.CardEffectCancelledOut = 58
|
||||||
|
|
||||||
fk.NumOfEvents = 58
|
fk.NumOfEvents = 59
|
||||||
|
|
|
@ -127,8 +127,6 @@ function GameLogic:prepareForStart()
|
||||||
-- TODO: add skills to player
|
-- TODO: add skills to player
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: prepare drawPile
|
|
||||||
-- TODO: init cards in drawPile
|
|
||||||
local allCardIds = Fk:getAllCardIds()
|
local allCardIds = Fk:getAllCardIds()
|
||||||
table.shuffle(allCardIds)
|
table.shuffle(allCardIds)
|
||||||
room.draw_pile = allCardIds
|
room.draw_pile = allCardIds
|
||||||
|
@ -137,13 +135,15 @@ function GameLogic:prepareForStart()
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, p in ipairs(room.alive_players) do
|
for _, p in ipairs(room.alive_players) do
|
||||||
room:handleAddLoseSkills(p, "zhiheng")
|
room:handleAddLoseSkills(p, "zhiheng", nil, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
self:addTriggerSkill(GameRule)
|
self:addTriggerSkill(GameRule)
|
||||||
for _, trig in ipairs(Fk.global_trigger) do
|
for _, trig in ipairs(Fk.global_trigger) do
|
||||||
self:addTriggerSkill(trig)
|
self:addTriggerSkill(trig)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.room:sendLog{ type = "$GameStart" }
|
||||||
end
|
end
|
||||||
|
|
||||||
function GameLogic:action()
|
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
|
---@param _room fk.Room
|
||||||
function Room:initialize(_room)
|
function Room:initialize(_room)
|
||||||
self.room = _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)
|
self.room.startGame = function(_self)
|
||||||
Room.initialize(self, _room) -- clear old data
|
Room.initialize(self, _room) -- clear old data
|
||||||
|
@ -206,6 +198,32 @@ function Room:getNCards(num, from)
|
||||||
return cardIds
|
return cardIds
|
||||||
end
|
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
|
-- network functions, notify function
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
@ -257,11 +275,11 @@ end
|
||||||
|
|
||||||
---@param command string
|
---@param command string
|
||||||
---@param players ServerPlayer[]
|
---@param players ServerPlayer[]
|
||||||
function Room:doBroadcastRequest(command, players)
|
function Room:doBroadcastRequest(command, players, jsonData)
|
||||||
players = players or self.players
|
players = players or self.players
|
||||||
self:notifyMoveFocus(players, command)
|
self:notifyMoveFocus(players, command)
|
||||||
for _, p in ipairs(players) do
|
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
|
end
|
||||||
|
|
||||||
local remainTime = self.timeout
|
local remainTime = self.timeout
|
||||||
|
@ -269,8 +287,39 @@ function Room:doBroadcastRequest(command, players)
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
for _, p in ipairs(players) do
|
for _, p in ipairs(players) do
|
||||||
elapsed = os.time() - currentTime
|
elapsed = os.time() - currentTime
|
||||||
remainTime = remainTime - elapsed
|
p:waitForReply(remainTime - elapsed)
|
||||||
p:waitForReply(remainTime)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -328,6 +377,11 @@ function Room:notifyMoveFocus(players, command)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param log LogMessage
|
||||||
|
function Room:sendLog(log)
|
||||||
|
self:doBroadcastNotify("GameLog", json.encode(log))
|
||||||
|
end
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
-- interactive functions
|
-- interactive functions
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
@ -540,9 +594,9 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
|
||||||
---@type AimStruct
|
---@type AimStruct
|
||||||
local aimStruct
|
local aimStruct
|
||||||
local initialEvent = false
|
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 = {
|
aimStruct = {
|
||||||
from = cardUseEvent.from,
|
from = cardUseEvent.from,
|
||||||
cardId = cardUseEvent.cardId,
|
cardId = cardUseEvent.cardId,
|
||||||
|
@ -590,18 +644,20 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
|
||||||
if #cancelledTargets > 0 then
|
if #cancelledTargets > 0 then
|
||||||
for _, target in ipairs(cancelledTargets) do
|
for _, target in ipairs(cancelledTargets) do
|
||||||
aimEventCollaborators[target] = {}
|
aimEventCollaborators[target] = {}
|
||||||
collaboratorsIndex[target] = 0
|
collaboratorsIndex[target] = 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
aimStruct.tos[AimGroup.Cancelled] = {}
|
aimStruct.tos[AimGroup.Cancelled] = {}
|
||||||
|
|
||||||
aimEventCollaborators[toId] = aimEventCollaborators[toId] or {}
|
aimEventCollaborators[toId] = aimEventCollaborators[toId] or {}
|
||||||
if not room:getPlayerById(toId):isAlive() then
|
if room:getPlayerById(toId):isAlive() then
|
||||||
if initialEvent then
|
if initialEvent then
|
||||||
table.insert(aimEventCollaborators[toId], aimStruct)
|
table.insert(aimEventCollaborators[toId], aimStruct)
|
||||||
else
|
else
|
||||||
aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct
|
aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct
|
||||||
end
|
end
|
||||||
|
|
||||||
|
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
AimGroup:setTargetDone(aimStruct.tos, toId)
|
AimGroup:setTargetDone(aimStruct.tos, toId)
|
||||||
|
@ -639,7 +695,9 @@ function Room:useCard(cardUseEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do
|
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)
|
self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent)
|
||||||
if event == fk.CardUsing then
|
if event == fk.CardUsing then
|
||||||
|
@ -725,7 +783,59 @@ function Room:useCard(cardUseEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if Fk:getCardById(cardUseEvent.cardId).skill then
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -740,6 +850,80 @@ function Room:useCard(cardUseEvent)
|
||||||
end
|
end
|
||||||
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
|
-- move cards, and wrappers
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
@ -879,6 +1063,43 @@ function Room:drawCards(player, num, skillName, fromPlace)
|
||||||
return { table.unpack(topCards) }
|
return { table.unpack(topCards) }
|
||||||
end
|
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
|
-- some easier actions
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
@ -1087,11 +1308,13 @@ end
|
||||||
---@param player ServerPlayer
|
---@param player ServerPlayer
|
||||||
---@param skill_names string[] | string
|
---@param skill_names string[] | string
|
||||||
---@param source_skill string | Skill | nil
|
---@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
|
if type(skill_names) == "string" then
|
||||||
skill_names = skill_names:split("|")
|
skill_names = skill_names:split("|")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if sendlog == nil then sendlog = true end
|
||||||
|
|
||||||
if #skill_names == 0 then return end
|
if #skill_names == 0 then return end
|
||||||
local losts = {} ---@type boolean[]
|
local losts = {} ---@type boolean[]
|
||||||
local triggers = {} ---@type Skill[]
|
local triggers = {} ---@type Skill[]
|
||||||
|
@ -1105,7 +1328,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
|
||||||
player.id,
|
player.id,
|
||||||
s.name
|
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(losts, true)
|
||||||
table.insert(triggers, s)
|
table.insert(triggers, s)
|
||||||
end
|
end
|
||||||
|
@ -1122,7 +1353,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
|
||||||
player.id,
|
player.id,
|
||||||
s.name
|
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(losts, false)
|
||||||
table.insert(triggers, s)
|
table.insert(triggers, s)
|
||||||
end
|
end
|
||||||
|
@ -1138,6 +1377,35 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
|
||||||
end
|
end
|
||||||
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
|
-- other helpers
|
||||||
|
|
||||||
function Room:adjustSeats()
|
function Room:adjustSeats()
|
||||||
|
@ -1187,29 +1455,6 @@ function Room:gameOver()
|
||||||
self.room:gameOver()
|
self.room:gameOver()
|
||||||
end
|
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)
|
function CreateRoom(_room)
|
||||||
RoomInstance = Room:new(_room)
|
RoomInstance = Room:new(_room)
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,11 @@ function ServerPlayer:turnOver()
|
||||||
self.faceup = not self.faceup
|
self.faceup = not self.faceup
|
||||||
self.room:broadcastProperty(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)
|
self.room.logic:trigger(fk.TurnedOver, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -33,3 +33,5 @@ fk.ReasonResonpse = 10
|
||||||
fk.NormalDamage = 1
|
fk.NormalDamage = 1
|
||||||
fk.ThunderDamage = 2
|
fk.ThunderDamage = 2
|
||||||
fk.FireDamage = 3
|
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 target == nil then
|
||||||
if event == fk.GameStart then
|
if event == fk.GameStart then
|
||||||
print("Game started")
|
fk.qInfo("Game started")
|
||||||
RoomInstance.tag["FirstRound"] = true
|
RoomInstance.tag["FirstRound"] = true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
@ -35,6 +35,7 @@ GameRule = fk.CreateTriggerSkill{
|
||||||
move_to_notify.toArea = Card.PlayerHand
|
move_to_notify.toArea = Card.PlayerHand
|
||||||
move_to_notify.to = player.id
|
move_to_notify.to = player.id
|
||||||
move_to_notify.moveInfo = {}
|
move_to_notify.moveInfo = {}
|
||||||
|
move_to_notify.moveReason = fk.ReasonDraw
|
||||||
for _, id in ipairs(cardIds) do
|
for _, id in ipairs(cardIds) do
|
||||||
table.insert(move_to_notify.moveInfo,
|
table.insert(move_to_notify.moveInfo,
|
||||||
{ cardId = id, fromArea = Card.DrawPile })
|
{ cardId = id, fromArea = Card.DrawPile })
|
||||||
|
@ -55,7 +56,7 @@ GameRule = fk.CreateTriggerSkill{
|
||||||
player:setFlag("Global_FirstRound")
|
player:setFlag("Global_FirstRound")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: send log
|
room:sendLog{ type = "$AppendSeparator" }
|
||||||
|
|
||||||
player:addMark("Global_TurnCount")
|
player:addMark("Global_TurnCount")
|
||||||
if not player.faceup then
|
if not player.faceup then
|
||||||
|
|
|
@ -5,10 +5,35 @@ Fk:loadTranslationTable{
|
||||||
["standard_cards"] = "标+EX"
|
["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{
|
local slash = fk.CreateBasicCard{
|
||||||
name = "slash",
|
name = "slash",
|
||||||
number = 7,
|
number = 7,
|
||||||
suit = Card.Spade,
|
suit = Card.Spade,
|
||||||
|
skill = slashSkill,
|
||||||
}
|
}
|
||||||
Fk:loadTranslationTable{
|
Fk:loadTranslationTable{
|
||||||
["slash"] = "杀",
|
["slash"] = "杀",
|
||||||
|
@ -50,10 +75,19 @@ extension:addCards({
|
||||||
slash:clone(Card.Diamond, 13),
|
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{
|
local jink = fk.CreateBasicCard{
|
||||||
name = "jink",
|
name = "jink",
|
||||||
suit = Card.Heart,
|
suit = Card.Heart,
|
||||||
number = 2,
|
number = 2,
|
||||||
|
skill = jinkSkill,
|
||||||
}
|
}
|
||||||
Fk:loadTranslationTable{
|
Fk:loadTranslationTable{
|
||||||
["jink"] = "闪",
|
["jink"] = "闪",
|
||||||
|
@ -131,7 +165,7 @@ local snatchSkill = fk.CreateActiveSkill{
|
||||||
return #selected == 1
|
return #selected == 1
|
||||||
end,
|
end,
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = TargetGroup:getRealTargets(effect.tos)[1]
|
local to = effect.to
|
||||||
local from = effect.from
|
local from = effect.from
|
||||||
local cid = room:askForCardChosen(
|
local cid = room:askForCardChosen(
|
||||||
room:getPlayerById(from),
|
room:getPlayerById(from),
|
||||||
|
@ -201,7 +235,7 @@ local exNihiloSkill = fk.CreateActiveSkill{
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_effect = function(self, room, cardEffectEvent)
|
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
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,10 +256,19 @@ extension:addCards({
|
||||||
exNihilo:clone(Card.Heart, 11),
|
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{
|
local nullification = fk.CreateTrickCard{
|
||||||
name = "nullification",
|
name = "nullification",
|
||||||
suit = Card.Spade,
|
suit = Card.Spade,
|
||||||
number = 11,
|
number = 11,
|
||||||
|
skill = nullificationSkill,
|
||||||
}
|
}
|
||||||
Fk:loadTranslationTable{
|
Fk:loadTranslationTable{
|
||||||
["nullification"] = "无懈可击",
|
["nullification"] = "无懈可击",
|
||||||
|
|
|
@ -2,12 +2,36 @@ import QtQuick
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
// Client configuration
|
// Client configuration
|
||||||
|
property real winWidth
|
||||||
|
property real winHeight
|
||||||
|
property var conf: ({})
|
||||||
|
property string lastLoginServer
|
||||||
|
property var savedPassword: ({})
|
||||||
|
|
||||||
// Player property of client
|
// Player property of client
|
||||||
|
property string serverAddr
|
||||||
property string screenName: ""
|
property string screenName: ""
|
||||||
property string password: ""
|
property string password: ""
|
||||||
|
property string cipherText
|
||||||
|
|
||||||
// Client data
|
// Client data
|
||||||
property int roomCapacity: 0
|
property int roomCapacity: 0
|
||||||
property int roomTimeout: 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 = {};
|
var callbacks = {};
|
||||||
|
|
||||||
callbacks["NetworkDelayTest"] = function(jsonData) {
|
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([
|
ClientInstance.notifyServer("Setup", JSON.stringify([
|
||||||
config.screenName,
|
config.screenName, cipherText, md5sum
|
||||||
config.password
|
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +48,13 @@ callbacks["EnterLobby"] = function(jsonData) {
|
||||||
// depth == 1 means the lobby page is not present in mainStack
|
// depth == 1 means the lobby page is not present in mainStack
|
||||||
createClientPages();
|
createClientPages();
|
||||||
if (mainStack.depth === 1) {
|
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);
|
mainStack.push(lobby);
|
||||||
} else {
|
} else {
|
||||||
mainStack.pop();
|
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 {
|
Column {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
TextField {
|
ComboBox {
|
||||||
id: server_addr
|
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 {
|
TextField {
|
||||||
id: screenNameEdit
|
id: screenNameEdit
|
||||||
text: "player"
|
text: "player"
|
||||||
|
onTextChanged: {
|
||||||
|
passwordEdit.text = "";
|
||||||
|
let data = config.savedPassword[server_addr.editText];
|
||||||
|
if (data) {
|
||||||
|
if (text === data.username) {
|
||||||
|
passwordEdit.text = data.shorten_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*TextField {
|
/*TextField {
|
||||||
id: avatarEdit
|
id: avatarEdit
|
||||||
|
@ -35,16 +55,20 @@ Item {
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
text: "Join Server"
|
text: "Join Server"
|
||||||
|
enabled: passwordEdit.text !== ""
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
config.serverAddr = server_addr.editText;
|
||||||
config.screenName = screenNameEdit.text;
|
config.screenName = screenNameEdit.text;
|
||||||
config.password = passwordEdit.text;
|
config.password = passwordEdit.text;
|
||||||
mainWindow.busy = true;
|
mainWindow.busy = true;
|
||||||
Backend.joinServer(server_addr.text);
|
Backend.joinServer(server_addr.editText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
text: "Console start"
|
text: "Console start"
|
||||||
|
enabled: passwordEdit.text !== ""
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
config.serverAddr = "127.0.0.1";
|
||||||
config.screenName = screenNameEdit.text;
|
config.screenName = screenNameEdit.text;
|
||||||
config.password = passwordEdit.text;
|
config.password = passwordEdit.text;
|
||||||
mainWindow.busy = true;
|
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
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import "Common"
|
||||||
import "RoomElement"
|
import "RoomElement"
|
||||||
import "RoomLogic.js" as Logic
|
import "RoomLogic.js" as Logic
|
||||||
|
|
||||||
|
@ -15,6 +16,10 @@ Item {
|
||||||
|
|
||||||
property alias popupBox: popupBox
|
property alias popupBox: popupBox
|
||||||
property alias promptText: prompt.text
|
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: []
|
property var selected_targets: []
|
||||||
|
|
||||||
|
@ -30,7 +35,7 @@ Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
onClicked: {
|
onClicked: {
|
||||||
ClientInstance.clearPlayers();
|
// ClientInstance.clearPlayers();
|
||||||
ClientInstance.notifyServer("QuitRoom", "[]");
|
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 {
|
Dashboard {
|
||||||
id: dashboard
|
id: dashboard
|
||||||
width: roomScene.width
|
width: roomScene.width - dashboardBtn.width
|
||||||
anchors.top: roomArea.bottom
|
anchors.top: roomArea.bottom
|
||||||
|
anchors.left: dashboardBtn.right
|
||||||
|
|
||||||
self.playerid: dashboardModel.id
|
self.playerid: dashboardModel.id
|
||||||
self.general: dashboardModel.general
|
self.general: dashboardModel.general
|
||||||
|
@ -222,8 +247,10 @@ Item {
|
||||||
Text {
|
Text {
|
||||||
id: prompt
|
id: prompt
|
||||||
visible: progress.visible
|
visible: progress.visible
|
||||||
anchors.bottom: progress.top
|
anchors.top: progress.top
|
||||||
anchors.bottomMargin: 8
|
anchors.topMargin: -2
|
||||||
|
color: "white"
|
||||||
|
z: 1
|
||||||
anchors.horizontalCenter: progress.horizontalCenter
|
anchors.horizontalCenter: progress.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,11 +259,31 @@ Item {
|
||||||
width: parent.width * 0.6
|
width: parent.width * 0.6
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: okCancel.top
|
anchors.bottom: okCancel.top
|
||||||
anchors.bottomMargin: 8
|
anchors.bottomMargin: 4
|
||||||
from: 0.0
|
from: 0.0
|
||||||
to: 100.0
|
to: 100.0
|
||||||
|
|
||||||
visible: false
|
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 {
|
NumberAnimation on value {
|
||||||
running: progress.visible
|
running: progress.visible
|
||||||
from: 100.0
|
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: {
|
Component.onCompleted: {
|
||||||
toast.show(Backend.translate("$EnterRoom"));
|
toast.show(Backend.translate("$EnterRoom"));
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ Item {
|
||||||
state.x = parentPos.x;
|
state.x = parentPos.x;
|
||||||
state.y = parentPos.y;
|
state.y = parentPos.y;
|
||||||
state.opacity = 0;
|
state.opacity = 0;
|
||||||
card = component.createObject(roomScene, state);
|
card = component.createObject(roomScene.dynamicCardArea, state);
|
||||||
card.x -= card.width / 2;
|
card.x -= card.width / 2;
|
||||||
card.x += (i - outputs.length / 2) * 15;
|
card.x += (i - outputs.length / 2) * 15;
|
||||||
card.y -= card.height / 2;
|
card.y -= card.height / 2;
|
||||||
|
|
|
@ -371,10 +371,55 @@ Item {
|
||||||
anchors.centerIn: parent
|
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: {
|
onGeneralChanged: {
|
||||||
if (!roomScene.isStarted) return;
|
if (!roomScene.isStarted) return;
|
||||||
generalName.text = Backend.translate(general);
|
generalName.text = Backend.translate(general);
|
||||||
let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general]));
|
let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general]));
|
||||||
kingdom = data.kingdom;
|
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++) {
|
for (let i = 0; i < photoModel.count; i++) {
|
||||||
let item = photoModel.get(i);
|
let item = photoModel.get(i);
|
||||||
item.seatNumber = order.indexOf(item.id) + 1;
|
item.seatNumber = order.indexOf(item.id) + 1;
|
||||||
|
item.general = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardModel.seatNumber = order.indexOf(Self.id) + 1;
|
dashboardModel.seatNumber = order.indexOf(Self.id) + 1;
|
||||||
|
dashboardModel.general = "";
|
||||||
roomScene.dashboardModelChanged();
|
roomScene.dashboardModelChanged();
|
||||||
|
|
||||||
// make Self to the first of list, then reorder photomodel
|
// make Self to the first of list, then reorder photomodel
|
||||||
|
@ -476,7 +478,10 @@ callbacks["AskForSkillInvoke"] = function(jsonData) {
|
||||||
// jsonData: string name
|
// jsonData: string name
|
||||||
roomScene.promptText = Backend.translate("#AskForSkillInvoke")
|
roomScene.promptText = Backend.translate("#AskForSkillInvoke")
|
||||||
.arg(Backend.translate(jsonData));
|
.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) {
|
callbacks["AskForChoice"] = function(jsonData) {
|
||||||
|
@ -590,3 +595,11 @@ callbacks["AskForUseActiveSkill"] = function(jsonData) {
|
||||||
dashboard.startPending(skill_name);
|
dashboard.startPending(skill_name);
|
||||||
cancelButton.enabled = cancelable;
|
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"
|
import "Pages"
|
||||||
|
|
||||||
Window {
|
Window {
|
||||||
|
id: realMainWin
|
||||||
visible: true
|
visible: true
|
||||||
width: 960
|
width: 960
|
||||||
height: 540
|
height: 540
|
||||||
|
@ -19,6 +20,10 @@ Item {
|
||||||
scale: parent.width / width
|
scale: parent.width / width
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Config {
|
||||||
|
id: config
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: AppPath + "/image/background"
|
source: AppPath + "/image/background"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -52,10 +57,6 @@ Item {
|
||||||
visible: mainWindow.busy === true
|
visible: mainWindow.busy === true
|
||||||
}
|
}
|
||||||
|
|
||||||
Config {
|
|
||||||
id: config
|
|
||||||
}
|
|
||||||
|
|
||||||
// global popup. it is modal and just lower than toast
|
// global popup. it is modal and just lower than toast
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: globalPopupDim
|
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: {
|
onClosing: {
|
||||||
|
config.winWidth = width;
|
||||||
|
config.winHeight = height;
|
||||||
|
config.saveConf();
|
||||||
Backend.quitLobby();
|
Backend.quitLobby();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ CREATE TABLE userinfo (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name VARCHAR(255),
|
name VARCHAR(255),
|
||||||
password CHAR(64),
|
password CHAR(64),
|
||||||
|
salt CHAR(8),
|
||||||
avatar VARCHAR(64),
|
avatar VARCHAR(64),
|
||||||
lastLoginIp VARCHAR(64),
|
lastLoginIp VARCHAR(64),
|
||||||
banned BOOLEAN
|
banned BOOLEAN
|
||||||
|
|
|
@ -31,16 +31,21 @@ set(freekill_HEADERS
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll)
|
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll)
|
||||||
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.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)
|
elseif (ANDROID)
|
||||||
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/android/liblua54.so)
|
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/android/liblua54.so)
|
||||||
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/android/libsqlite3.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
|
set_target_properties(FreeKill PROPERTIES
|
||||||
QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android
|
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 ()
|
else ()
|
||||||
set(LUA_LIB lua5.4)
|
set(LUA_LIB lua5.4)
|
||||||
set(SQLITE3_LIB sqlite3)
|
set(SQLITE3_LIB sqlite3)
|
||||||
|
set(CRYPTO_LIB OpenSSL::Crypto)
|
||||||
|
set(READLINE_LIB readline)
|
||||||
|
list(APPEND freekill_SRCS "server/shell.cpp")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
source_group("Include" FILES ${freekill_HEADERS})
|
source_group("Include" FILES ${freekill_HEADERS})
|
||||||
|
@ -50,9 +55,12 @@ target_precompile_headers(FreeKill PRIVATE "pch.h")
|
||||||
target_link_libraries(FreeKill PRIVATE
|
target_link_libraries(FreeKill PRIVATE
|
||||||
${LUA_LIB}
|
${LUA_LIB}
|
||||||
${SQLITE3_LIB}
|
${SQLITE3_LIB}
|
||||||
|
${CRYPTO_LIB}
|
||||||
|
${READLINE_LIB}
|
||||||
fkparse
|
fkparse
|
||||||
Qt6::Qml
|
Qt6::Qml
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
|
Qt6::Widgets
|
||||||
Qt6::Network
|
Qt6::Network
|
||||||
Qt6::Multimedia
|
Qt6::Multimedia
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@ Client::~Client()
|
||||||
router->getSocket()->deleteLater();
|
router->getSocket()->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::connectToHost(const QHostAddress& server, ushort port)
|
void Client::connectToHost(const QString &server, ushort port)
|
||||||
{
|
{
|
||||||
router->getSocket()->connectToHost(server, port);
|
router->getSocket()->connectToHost(server, port);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ public:
|
||||||
Client(QObject *parent = nullptr);
|
Client(QObject *parent = nullptr);
|
||||||
~Client();
|
~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 replyToServer(const QString &command, const QString &jsonData);
|
||||||
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);
|
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include <qcryptographichash.h>
|
||||||
|
#include <qnamespace.h>
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
int luaopen_fk(lua_State *);
|
int luaopen_fk(lua_State *);
|
||||||
|
@ -24,7 +27,7 @@ bool DoLuaScript(lua_State *L, const char *script)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const char *error_msg = lua_tostring(L, -1);
|
const char *error_msg = lua_tostring(L, -1);
|
||||||
qDebug() << error_msg;
|
qCritical() << error_msg;
|
||||||
lua_pop(L, 2);
|
lua_pop(L, 2);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +68,7 @@ sqlite3 *OpenDatabase(const QString &filename)
|
||||||
if (!QFile::exists(filename)) {
|
if (!QFile::exists(filename)) {
|
||||||
QFile file("./server/init.sql");
|
QFile file("./server/init.sql");
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
qDebug() << "cannot open init.sql. Quit now.";
|
qFatal("cannot open init.sql. Quit now.");
|
||||||
qApp->exit(1);
|
qApp->exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +78,7 @@ sqlite3 *OpenDatabase(const QString &filename)
|
||||||
rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg);
|
rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg);
|
||||||
|
|
||||||
if (rc != SQLITE_OK ) {
|
if (rc != SQLITE_OK ) {
|
||||||
qDebug() << "sqlite error:" << err_msg;
|
qCritical() << "sqlite error:" << err_msg;
|
||||||
sqlite3_free(err_msg);
|
sqlite3_free(err_msg);
|
||||||
sqlite3_close(ret);
|
sqlite3_close(ret);
|
||||||
qApp->exit(1);
|
qApp->exit(1);
|
||||||
|
@ -83,7 +86,7 @@ sqlite3 *OpenDatabase(const QString &filename)
|
||||||
} else {
|
} else {
|
||||||
rc = sqlite3_open(filename.toLatin1().data(), &ret);
|
rc = sqlite3_open(filename.toLatin1().data(), &ret);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
qDebug() << "Cannot open database:" << sqlite3_errmsg(ret);
|
qCritical() << "Cannot open database:" << sqlite3_errmsg(ret);
|
||||||
sqlite3_close(ret);
|
sqlite3_close(ret);
|
||||||
qApp->exit(1);
|
qApp->exit(1);
|
||||||
}
|
}
|
||||||
|
@ -121,3 +124,75 @@ void ExecSQL(sqlite3 *db, const QString &sql) {
|
||||||
void CloseDatabase(sqlite3 *db) {
|
void CloseDatabase(sqlite3 *db) {
|
||||||
sqlite3_close(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 ExecSQL(sqlite3 *db, const QString &sql);
|
||||||
void CloseDatabase(sqlite3 *db);
|
void CloseDatabase(sqlite3 *db);
|
||||||
|
|
||||||
|
RSA *InitServerRSA();
|
||||||
|
|
||||||
|
QString calcFileMD5();
|
||||||
|
|
||||||
#endif // _GLOBAL_H
|
#endif // _GLOBAL_H
|
||||||
|
|
70
src/main.cpp
70
src/main.cpp
|
@ -1,6 +1,13 @@
|
||||||
#include "qmlbackend.h"
|
#include "qmlbackend.h"
|
||||||
#include "server.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
|
#ifdef Q_OS_ANDROID
|
||||||
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
|
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
|
||||||
{
|
{
|
||||||
|
@ -31,16 +38,37 @@ static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
|
||||||
}
|
}
|
||||||
#endif
|
#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[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
QThread::currentThread()->setObjectName("Main");
|
||||||
|
qInstallMessageHandler(fkMsgHandler);
|
||||||
QCoreApplication *app;
|
QCoreApplication *app;
|
||||||
QCoreApplication::setApplicationName("FreeKill");
|
QCoreApplication::setApplicationName("FreeKill");
|
||||||
QCoreApplication::setApplicationVersion("Alpha 0.0.1");
|
QCoreApplication::setApplicationVersion("Alpha 0.0.1");
|
||||||
|
|
||||||
#ifdef Q_OS_ANDROID
|
|
||||||
copyPath("assets:/res", QDir::currentPath());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription("FreeKill server");
|
parser.setApplicationDescription("FreeKill server");
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
@ -62,14 +90,38 @@ int main(int argc, char *argv[])
|
||||||
serverPort = parser.value("server").toInt();
|
serverPort = parser.value("server").toInt();
|
||||||
Server *server = new Server;
|
Server *server = new Server;
|
||||||
if (!server->listen(QHostAddress::Any, serverPort)) {
|
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);
|
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();
|
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;
|
QQmlApplicationEngine *engine = new QQmlApplicationEngine;
|
||||||
|
|
||||||
QmlBackend backend;
|
QmlBackend backend;
|
||||||
|
@ -83,10 +135,16 @@ int main(int argc, char *argv[])
|
||||||
bool debugging = false;
|
bool debugging = false;
|
||||||
#endif
|
#endif
|
||||||
engine->rootContext()->setContextProperty("Debugging", debugging);
|
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");
|
engine->load("qml/main.qml");
|
||||||
if (engine->rootObjects().isEmpty())
|
if (engine->rootObjects().isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
|
splash.close();
|
||||||
int ret = app->exec();
|
int ret = app->exec();
|
||||||
|
|
||||||
// delete the engine first
|
// delete the engine first
|
||||||
|
|
|
@ -26,7 +26,7 @@ void ClientSocket::init()
|
||||||
this, &ClientSocket::raiseError);
|
this, &ClientSocket::raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientSocket::connectToHost(const QHostAddress &address, ushort port)
|
void ClientSocket::connectToHost(const QString &address, ushort port)
|
||||||
{
|
{
|
||||||
socket->connectToHost(address, port);
|
socket->connectToHost(address, port);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ public:
|
||||||
// For server use
|
// For server use
|
||||||
ClientSocket(QTcpSocket *socket);
|
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 disconnectFromHost();
|
||||||
void send(const QByteArray& msg);
|
void send(const QByteArray& msg);
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "client_socket.h"
|
#include "client_socket.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "serverplayer.h"
|
#include "serverplayer.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
|
Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
@ -142,6 +143,60 @@ void Router::abortRequest()
|
||||||
|
|
||||||
void Router::handlePacket(const QByteArray& rawPacket)
|
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);
|
QJsonDocument packet = QJsonDocument::fromJson(rawPacket);
|
||||||
if (packet.isNull() || !packet.isArray())
|
if (packet.isNull() || !packet.isArray())
|
||||||
return;
|
return;
|
||||||
|
@ -156,12 +211,19 @@ void Router::handlePacket(const QByteArray& rawPacket)
|
||||||
ClientInstance->callLua(command, jsonData);
|
ClientInstance->callLua(command, jsonData);
|
||||||
} else {
|
} else {
|
||||||
ServerPlayer *player = qobject_cast<ServerPlayer *>(parent());
|
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 *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) {
|
else if (type & TYPE_REQUEST) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
// core gui qml
|
// core gui qml
|
||||||
#include <QtCore>
|
#include <QtCore>
|
||||||
#include <QGuiApplication>
|
#include <QApplication>
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
|
||||||
// network
|
// network
|
||||||
|
@ -15,4 +15,10 @@ typedef int LuaFunction;
|
||||||
#include "lua.hpp"
|
#include "lua.hpp"
|
||||||
#include "sqlite3.h"
|
#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
|
#endif // _PCH_H
|
||||||
|
|
|
@ -5,27 +5,26 @@
|
||||||
|
|
||||||
Room::Room(Server* server)
|
Room::Room(Server* server)
|
||||||
{
|
{
|
||||||
|
setObjectName("Room");
|
||||||
id = server->nextRoomId;
|
id = server->nextRoomId;
|
||||||
server->nextRoomId++;
|
server->nextRoomId++;
|
||||||
this->server = server;
|
this->server = server;
|
||||||
setParent(server);
|
setParent(server);
|
||||||
|
m_abandoned = false;
|
||||||
owner = nullptr;
|
owner = nullptr;
|
||||||
gameStarted = false;
|
gameStarted = false;
|
||||||
robot_id = -1;
|
robot_id = -2; // -1 is reserved in UI logic
|
||||||
timeout = 15;
|
timeout = 15;
|
||||||
|
L = NULL;
|
||||||
if (!isLobby()) {
|
if (!isLobby()) {
|
||||||
connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer);
|
connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer);
|
||||||
connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer);
|
connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer);
|
||||||
}
|
|
||||||
|
|
||||||
L = CreateLuaState();
|
L = CreateLuaState();
|
||||||
DoLuaScript(L, "lua/freekill.lua");
|
DoLuaScript(L, "lua/freekill.lua");
|
||||||
if (isLobby()) {
|
|
||||||
DoLuaScript(L, "lua/server/lobby.lua");
|
|
||||||
} else {
|
|
||||||
DoLuaScript(L, "lua/server/room.lua");
|
DoLuaScript(L, "lua/server/room.lua");
|
||||||
|
initLua();
|
||||||
}
|
}
|
||||||
initLua();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Room::~Room()
|
Room::~Room()
|
||||||
|
@ -35,7 +34,7 @@ Room::~Room()
|
||||||
terminate();
|
terminate();
|
||||||
wait();
|
wait();
|
||||||
}
|
}
|
||||||
lua_close(L);
|
if (L) lua_close(L);
|
||||||
}
|
}
|
||||||
|
|
||||||
Server *Room::getServer() const
|
Server *Room::getServer() const
|
||||||
|
@ -48,6 +47,11 @@ int Room::getId() const
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Room::setId(int id)
|
||||||
|
{
|
||||||
|
this->id = id;
|
||||||
|
}
|
||||||
|
|
||||||
bool Room::isLobby() const
|
bool Room::isLobby() const
|
||||||
{
|
{
|
||||||
return id == 0;
|
return id == 0;
|
||||||
|
@ -80,6 +84,9 @@ bool Room::isFull() const
|
||||||
|
|
||||||
bool Room::isAbandoned() const
|
bool Room::isAbandoned() const
|
||||||
{
|
{
|
||||||
|
if (isLobby())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (players.isEmpty())
|
if (players.isEmpty())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
@ -90,6 +97,10 @@ bool Room::isAbandoned() const
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Room::setAbandoned(bool abandoned) {
|
||||||
|
m_abandoned = abandoned;
|
||||||
|
}
|
||||||
|
|
||||||
ServerPlayer *Room::getOwner() const
|
ServerPlayer *Room::getOwner() const
|
||||||
{
|
{
|
||||||
return owner;
|
return owner;
|
||||||
|
@ -201,10 +212,11 @@ void Room::removePlayer(ServerPlayer *player)
|
||||||
server->addPlayer(runner);
|
server->addPlayer(runner);
|
||||||
|
|
||||||
emit playerRemoved(runner);
|
emit playerRemoved(runner);
|
||||||
runner->abortRequest();
|
player->abortRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAbandoned()) {
|
if (isAbandoned() && !m_abandoned) {
|
||||||
|
m_abandoned = true;
|
||||||
emit abandoned();
|
emit abandoned();
|
||||||
} else if (player == owner) {
|
} else if (player == owner) {
|
||||||
setOwner(players.first());
|
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()
|
void Room::gameOver()
|
||||||
{
|
{
|
||||||
gameStarted = false;
|
gameStarted = false;
|
||||||
|
@ -275,6 +298,8 @@ void Room::gameOver()
|
||||||
p->deleteLater();
|
p->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
players.clear();
|
||||||
|
owner = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Room::run()
|
void Room::run()
|
||||||
|
|
|
@ -14,6 +14,7 @@ public:
|
||||||
// ==================================={
|
// ==================================={
|
||||||
Server *getServer() const;
|
Server *getServer() const;
|
||||||
int getId() const;
|
int getId() const;
|
||||||
|
void setId(int id);
|
||||||
bool isLobby() const;
|
bool isLobby() const;
|
||||||
QString getName() const;
|
QString getName() const;
|
||||||
void setName(const QString &name);
|
void setName(const QString &name);
|
||||||
|
@ -21,6 +22,7 @@ public:
|
||||||
void setCapacity(int capacity);
|
void setCapacity(int capacity);
|
||||||
bool isFull() const;
|
bool isFull() const;
|
||||||
bool isAbandoned() const;
|
bool isAbandoned() const;
|
||||||
|
void setAbandoned(bool abandoned); // never use this function
|
||||||
|
|
||||||
ServerPlayer *getOwner() const;
|
ServerPlayer *getOwner() const;
|
||||||
void setOwner(ServerPlayer *owner);
|
void setOwner(ServerPlayer *owner);
|
||||||
|
@ -46,12 +48,11 @@ public:
|
||||||
const QString &command,
|
const QString &command,
|
||||||
const QString &jsonData
|
const QString &jsonData
|
||||||
);
|
);
|
||||||
|
void chat(ServerPlayer *sender, const QString &jsonData);
|
||||||
|
|
||||||
void gameOver();
|
void gameOver();
|
||||||
|
|
||||||
void initLua();
|
void initLua();
|
||||||
void callLua(const QString &command, const QString &jsonData);
|
|
||||||
LuaFunction callback;
|
|
||||||
|
|
||||||
void roomStart();
|
void roomStart();
|
||||||
LuaFunction startGame;
|
LuaFunction startGame;
|
||||||
|
|
|
@ -13,6 +13,13 @@ Server::Server(QObject* parent)
|
||||||
{
|
{
|
||||||
ServerInstance = this;
|
ServerInstance = this;
|
||||||
db = OpenDatabase();
|
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 = new ServerSocket();
|
||||||
server->setParent(this);
|
server->setParent(this);
|
||||||
connect(server, &ServerSocket::new_connection,
|
connect(server, &ServerSocket::new_connection,
|
||||||
|
@ -30,6 +37,7 @@ Server::~Server()
|
||||||
ServerInstance = nullptr;
|
ServerInstance = nullptr;
|
||||||
m_lobby->deleteLater();
|
m_lobby->deleteLater();
|
||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
|
RSA_free(rsa);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Server::listen(const QHostAddress& address, ushort port)
|
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)
|
void Server::createRoom(ServerPlayer* owner, const QString &name, int capacity)
|
||||||
{
|
{
|
||||||
Room *room = new Room(this);
|
Room *room;
|
||||||
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
|
if (!idle_rooms.isEmpty()) {
|
||||||
if (room->isLobby())
|
room = idle_rooms.pop();
|
||||||
m_lobby = room;
|
room->setId(nextRoomId);
|
||||||
else
|
nextRoomId++;
|
||||||
|
room->setAbandoned(false);
|
||||||
rooms.insert(room->getId(), room);
|
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->setName(name);
|
||||||
room->setCapacity(capacity);
|
room->setCapacity(capacity);
|
||||||
|
@ -105,11 +122,11 @@ sqlite3 *Server::getDatabase() {
|
||||||
|
|
||||||
void Server::processNewConnection(ClientSocket* client)
|
void Server::processNewConnection(ClientSocket* client)
|
||||||
{
|
{
|
||||||
qDebug() << client->peerAddress() << "connected";
|
qInfo() << client->peerAddress() << "connected";
|
||||||
// version check, file check, ban IP, reconnect, etc
|
// version check, file check, ban IP, reconnect, etc
|
||||||
|
|
||||||
connect(client, &ClientSocket::disconnected, this, [client](){
|
connect(client, &ClientSocket::disconnected, this, [client](){
|
||||||
qDebug() << client->peerAddress() << "disconnected";
|
qInfo() << client->peerAddress() << "disconnected";
|
||||||
});
|
});
|
||||||
|
|
||||||
// network delay test
|
// network delay test
|
||||||
|
@ -117,7 +134,7 @@ void Server::processNewConnection(ClientSocket* client)
|
||||||
body << -2;
|
body << -2;
|
||||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
|
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
|
||||||
body << "NetworkDelayTest";
|
body << "NetworkDelayTest";
|
||||||
body << "[]";
|
body << public_key;
|
||||||
client->send(QJsonDocument(body).toJson(QJsonDocument::Compact));
|
client->send(QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||||
// Note: the client should send a setup string next
|
// Note: the client should send a setup string next
|
||||||
connect(client, &ClientSocket::message_got, this, &Server::processRequest);
|
connect(client, &ClientSocket::message_got, this, &Server::processRequest);
|
||||||
|
@ -142,11 +159,11 @@ void Server::processRequest(const QByteArray& msg)
|
||||||
)
|
)
|
||||||
valid = false;
|
valid = false;
|
||||||
else
|
else
|
||||||
valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 2);
|
valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
qDebug() << "Invalid setup string:" << msg;
|
qWarning() << "Invalid setup string:" << msg;
|
||||||
QJsonArray body;
|
QJsonArray body;
|
||||||
body << -2;
|
body << -2;
|
||||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
|
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();
|
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());
|
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
|
// First check the name and password
|
||||||
// Matches a string that does not contain special characters
|
// Matches a string that does not contain special characters
|
||||||
QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
|
static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
|
||||||
QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex();
|
|
||||||
|
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;
|
bool passed = false;
|
||||||
QString error_msg;
|
QString error_msg;
|
||||||
QJsonObject result;
|
QJsonObject result;
|
||||||
|
|
||||||
if (!nameExp.match(name).hasMatch()) {
|
if (!nameExp.match(name).hasMatch() && !name.isEmpty()) {
|
||||||
// Then we check the database,
|
// Then we check the database,
|
||||||
QString sql_find = QString("SELECT * FROM userinfo \
|
QString sql_find = QString("SELECT * FROM userinfo \
|
||||||
WHERE name='%1';").arg(name);
|
WHERE name='%1';").arg(name);
|
||||||
result = SelectFromDatabase(db, sql_find);
|
result = SelectFromDatabase(db, sql_find);
|
||||||
QJsonArray arr = result["password"].toArray();
|
QJsonArray arr = result["password"].toArray();
|
||||||
if (arr.isEmpty()) {
|
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
|
// not present in database, register
|
||||||
QString sql_reg = QString("INSERT INTO userinfo (name,password,\
|
QString sql_reg = QString("INSERT INTO userinfo (name,password,salt,\
|
||||||
avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4',%5);")
|
avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4','%5',%6);")
|
||||||
.arg(name)
|
.arg(name)
|
||||||
.arg(QString(passwordHash))
|
.arg(QString(passwordHash))
|
||||||
|
.arg(salt)
|
||||||
.arg("liubei")
|
.arg("liubei")
|
||||||
.arg(client->peerAddress())
|
.arg(client->peerAddress())
|
||||||
.arg("FALSE");
|
.arg("FALSE");
|
||||||
|
@ -194,6 +234,9 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
|
||||||
int id = result["id"].toArray()[0].toString().toInt();
|
int id = result["id"].toArray()[0].toString().toInt();
|
||||||
if (!players.value(id)) {
|
if (!players.value(id)) {
|
||||||
// check if password is the same
|
// 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());
|
passed = (passwordHash == arr[0].toString());
|
||||||
if (!passed) error_msg = "username or password error";
|
if (!passed) error_msg = "username or password error";
|
||||||
} else {
|
} else {
|
||||||
|
@ -226,7 +269,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
|
||||||
|
|
||||||
lobby()->addPlayer(player);
|
lobby()->addPlayer(player);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << client->peerAddress() << "lost connection:" << error_msg;
|
qInfo() << client->peerAddress() << "lost connection:" << error_msg;
|
||||||
QJsonArray body;
|
QJsonArray body;
|
||||||
body << -2;
|
body << -2;
|
||||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
|
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
|
||||||
|
@ -244,13 +287,22 @@ void Server::onRoomAbandoned()
|
||||||
room->gameOver();
|
room->gameOver();
|
||||||
rooms.remove(room->getId());
|
rooms.remove(room->getId());
|
||||||
updateRoomList();
|
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()
|
void Server::onUserDisconnected()
|
||||||
{
|
{
|
||||||
ServerPlayer *player = qobject_cast<ServerPlayer *>(sender());
|
ServerPlayer *player = qobject_cast<ServerPlayer *>(sender());
|
||||||
qDebug() << "Player" << player->getId() << "disconnected";
|
qInfo() << "Player" << player->getId() << "disconnected";
|
||||||
Room *room = player->getRoom();
|
Room *room = player->getRoom();
|
||||||
if (room->isStarted()) {
|
if (room->isStarted()) {
|
||||||
player->setState(Player::Offline);
|
player->setState(Player::Offline);
|
||||||
|
|
|
@ -42,14 +42,19 @@ public slots:
|
||||||
void onUserStateChanged();
|
void onUserStateChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class Shell;
|
||||||
ServerSocket *server;
|
ServerSocket *server;
|
||||||
Room *m_lobby;
|
Room *m_lobby;
|
||||||
QMap<int, Room *> rooms;
|
QMap<int, Room *> rooms;
|
||||||
|
QStack<Room *> idle_rooms;
|
||||||
int nextRoomId;
|
int nextRoomId;
|
||||||
friend Room::Room(Server *server);
|
friend Room::Room(Server *server);
|
||||||
QHash<int, ServerPlayer *> players;
|
QHash<int, ServerPlayer *> players;
|
||||||
|
|
||||||
|
RSA *rsa;
|
||||||
|
QString public_key;
|
||||||
sqlite3 *db;
|
sqlite3 *db;
|
||||||
|
QString md5;
|
||||||
|
|
||||||
void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password);
|
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) {
|
if (error) {
|
||||||
const char *error_msg = lua_tostring(L, -1);
|
const char *error_msg = lua_tostring(L, -1);
|
||||||
qDebug() << error_msg;
|
qCritical() << error_msg;
|
||||||
lua_pop(L, 2);
|
lua_pop(L, 2);
|
||||||
}
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
|
@ -46,3 +46,8 @@ static int GetMicroSecond(lua_State *L) {
|
||||||
return 1;
|
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();
|
void gameOver();
|
||||||
|
|
||||||
LuaFunction callback;
|
|
||||||
LuaFunction startGame;
|
LuaFunction startGame;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,33 +67,10 @@ void Room::initLua()
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
if (error) {
|
if (error) {
|
||||||
const char *error_msg = lua_tostring(L, -1);
|
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() {
|
void Room::roomStart() {
|
||||||
Q_ASSERT(startGame);
|
Q_ASSERT(startGame);
|
||||||
|
|
||||||
|
@ -109,7 +85,7 @@ void Room::roomStart() {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const char *error_msg = lua_tostring(L, -1);
|
const char *error_msg = lua_tostring(L, -1);
|
||||||
qDebug() << error_msg;
|
qCritical() << error_msg;
|
||||||
lua_pop(L, 2);
|
lua_pop(L, 2);
|
||||||
}
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "qmlbackend.h"
|
#include "qmlbackend.h"
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
#include "client.h"
|
#include "client.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
QmlBackend *Backend;
|
QmlBackend *Backend;
|
||||||
|
|
||||||
|
@ -9,12 +10,14 @@ QmlBackend::QmlBackend(QObject* parent)
|
||||||
{
|
{
|
||||||
Backend = this;
|
Backend = this;
|
||||||
engine = nullptr;
|
engine = nullptr;
|
||||||
|
rsa = RSA_new();
|
||||||
parser = fkp_new_parser();
|
parser = fkp_new_parser();
|
||||||
}
|
}
|
||||||
|
|
||||||
QmlBackend::~QmlBackend()
|
QmlBackend::~QmlBackend()
|
||||||
{
|
{
|
||||||
Backend = nullptr;
|
Backend = nullptr;
|
||||||
|
RSA_free(rsa);
|
||||||
fkp_close(parser);
|
fkp_close(parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +63,7 @@ void QmlBackend::joinServer(QString address)
|
||||||
addr = address;
|
addr = address;
|
||||||
}
|
}
|
||||||
|
|
||||||
client->connectToHost(QHostAddress(addr), port);
|
client->connectToHost(addr, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlBackend::quitLobby()
|
void QmlBackend::quitLobby()
|
||||||
|
@ -101,7 +104,7 @@ QString QmlBackend::translate(const QString &src) {
|
||||||
int err = lua_pcall(L, 1, 1, 0);
|
int err = lua_pcall(L, 1, 1, 0);
|
||||||
const char *result = lua_tostring(L, -1);
|
const char *result = lua_tostring(L, -1);
|
||||||
if (err) {
|
if (err) {
|
||||||
qDebug() << result;
|
qCritical() << result;
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -135,7 +138,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
qDebug() << "cannot handle QVariant type" << v.typeId();
|
qCritical() << "cannot handle QVariant type" << v.typeId();
|
||||||
lua_pushnil(L);
|
lua_pushnil(L);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +157,7 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
|
||||||
int err = lua_pcall(L, params.length(), 1, 0);
|
int err = lua_pcall(L, params.length(), 1, 0);
|
||||||
const char *result = lua_tostring(L, -1);
|
const char *result = lua_tostring(L, -1);
|
||||||
if (err) {
|
if (err) {
|
||||||
qDebug() << result;
|
qCritical() << result;
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -162,6 +165,46 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
|
||||||
return QString(result);
|
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) {
|
void QmlBackend::parseFkp(const QString &fileName) {
|
||||||
if (!QFile::exists(fileName)) {
|
if (!QFile::exists(fileName)) {
|
||||||
// errorEdit->setText(tr("File does not exist!"));
|
// errorEdit->setText(tr("File does not exist!"));
|
||||||
|
@ -215,3 +258,8 @@ void QmlBackend::readHashFromParser() {
|
||||||
copyFkpHash2QHash(skills, parser->skills);
|
copyFkpHash2QHash(skills, parser->skills);
|
||||||
copyFkpHash2QHash(marks, parser->marks);
|
copyFkpHash2QHash(marks, parser->marks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString QmlBackend::calcFileMD5() {
|
||||||
|
return ::calcFileMD5();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define _QMLBACKEND_H
|
#define _QMLBACKEND_H
|
||||||
|
|
||||||
#include "fkparse.h"
|
#include "fkparse.h"
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
class QmlBackend : public QObject {
|
class QmlBackend : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -32,14 +33,21 @@ public:
|
||||||
Q_INVOKABLE QString translate(const QString &src);
|
Q_INVOKABLE QString translate(const QString &src);
|
||||||
Q_INVOKABLE QString callLuaFunction(const QString &func_name,
|
Q_INVOKABLE QString callLuaFunction(const QString &func_name,
|
||||||
QVariantList params);
|
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
|
// support fkp
|
||||||
Q_INVOKABLE void parseFkp(const QString &filename);
|
Q_INVOKABLE void parseFkp(const QString &filename);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString calcFileMD5();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void notifyUI(const QString &command, const QString &jsonData);
|
void notifyUI(const QString &command, const QString &jsonData);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QQmlApplicationEngine *engine;
|
QQmlApplicationEngine *engine;
|
||||||
|
RSA *rsa;
|
||||||
fkp_parser *parser;
|
fkp_parser *parser;
|
||||||
QHash<QString, QString> generals;
|
QHash<QString, QString> generals;
|
||||||
QHash<QString, QString> skills;
|
QHash<QString, QString> skills;
|
||||||
|
|
Loading…
Reference in New Issue