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:
notify 2022-12-18 12:52:52 +08:00 committed by GitHub
parent 7b12d82683
commit a02410c282
58 changed files with 1808 additions and 431 deletions

15
.gitignore vendored
View File

@ -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/

2
.gitmodules vendored
View File

@ -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

View File

@ -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)

View File

@ -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。

2
android/.gitignore vendored
View File

@ -1,2 +1,4 @@
assets/ assets/
res/ res/
build.sh

View File

@ -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

90
doc/dev/compile.md Normal file
View File

@ -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就能编译了。

23
doc/dev/gamelogic.md Normal file
View File

@ -0,0 +1,23 @@
# 游戏逻辑
> [dev](./index.md) > 游戏逻辑
___
## 概述
FreeKill的游戏相关处理逻辑完全使用lua实现。在服务端上每个Room都有自己的lua_State并且只会在Room线程启动后才会去调用lua函数进行游戏逻辑处理。
本文档将简要介绍几个最为复杂的逻辑实现。
___
## 触发技
___
## 移动牌
___
## 使用牌

View File

@ -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)

View File

@ -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

BIN
image/splash.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

View File

@ -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"

View File

@ -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"] = "背面朝上",
} }

View File

@ -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

View File

@ -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"

68
lua/lib/fkparser.lua Normal file
View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,
@ -581,7 +635,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
cardUseEvent.from = aimStruct.from cardUseEvent.from = aimStruct.from
cardUseEvent.tos = aimEventTargetGroup cardUseEvent.tos = aimEventTargetGroup
cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets
if #AimGroup:getAllTargets(aimStruct.tos) == 0 then if #AimGroup:getAllTargets(aimStruct.tos) == 0 then
return false return false
end end
@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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"] = "无懈可击",

View File

@ -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));
}
} }

View File

@ -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}`);
}

View File

@ -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 = "";
}
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
} }

View File

@ -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"));

View File

@ -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;

View File

@ -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();
}
} }

View File

@ -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)
}

View File

@ -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();
} }
} }

View File

@ -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

View File

@ -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
) )

View File

@ -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);
} }

View File

@ -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);

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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);
} }

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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()

View File

@ -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;

View File

@ -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);

View File

@ -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);
}; };

116
src/server/shell.cpp Normal file
View File

@ -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

36
src/server/shell.h Normal file
View File

@ -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

View File

@ -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);

View File

@ -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, ...);

View File

@ -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);

View File

@ -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();
}

View File

@ -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;