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/
*.o
# IDE & LSP
.kdev4/
.vscode/
*.user
*-swp
*.kdev4
.cache/
tags
# file produced by game
FreeKill
FreeKill.exe
freekill-wrap.cxx
server/users.db
server/rsa
server/rsa_pub
freekill.client.config.json
freekill.server.config.json
flist.txt
# windeployqt
bearer/
iconengines/
imageformats/

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "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
Gui
Qml
Widgets
Network
Multimedia
)
find_package(OpenSSL)
find_package(Lua)
find_package(SQLite3)

View File

@ -10,10 +10,4 @@ ___
## 如何构建
FreeKill使用Qt6.3支持的运行平台有Windows、Linux、Android。
欲编译FreeKill首先得从Qt官网的安装工具安装Qt Creator和Qt 6.3.2。安装时需要勾选CMake应该默认就是选上的状态。
然后下载swig并为其配置环境变量即可构建FreeKill。
对于Linux用户而言还需要自己从包管理器安装lua5.4和sqlite。
[编译教程](./doc/dev/compile.md)

2
android/.gitignore vendored
View File

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

View File

@ -1,5 +1,7 @@
#!/bin/sh
rm -rf res assets
if [ ! -e res/mipmap ]; then
mkdir -p res/mipmap
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。
- [编译](./compile.md)
- [通信](./protocol.md)
- [游戏逻辑](./gamelogic.md)
- [数据库](./database.md)
- [UI](./ui.md)

View File

@ -36,8 +36,8 @@ $ ./FreeKill -s <port>
每当任何一个客户端连接上了之后,游戏会先进行以下流程:
1. 检查IP是否被封禁。 // TODO: 数据库
2. 检查客户端的延迟是否小于30秒。
3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和密码,服务端检查这个字符串是否合法。
2. 服务端将RSA公钥发给客户端然后检查客户端的延迟是否小于30秒。
3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和RSA公钥加密后的密码,服务端检查这个字符串是否合法。如果合法,检查密码是否正确。
4. 上述检查都通过后重连TODO:
5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。
@ -51,11 +51,12 @@ ___
1. 只要房间被添加玩家,那么那名玩家就自动从大厅移除。
2. 当玩家离开房间时,玩家便会自动进入大厅。
3. 当所有玩家都离开房间后,房间被销毁。
3. 当所有玩家都离开房间后,房间被销毁其实是进入Server的空闲房间列表毕竟新建lua_State的开销十分大
大厅的特点:
1. 只要有玩家进入,就刷新一次房间列表。
2. 只要玩家变动就更新大厅内人数TODO:
> 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。
@ -88,6 +89,41 @@ ___
但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。
重连的流程应为:
1. 总之先新建`ServerPlayer`并加到大厅
2. 在默认的处理流程中,此时会提醒玩家“已经有同名玩家加入”,然后断掉连接。
3. 在这时可以改成如果这个已经在线的玩家是Offline状态那么就继续否则断开。
4. pass之后走一遍流程把玩家加到大厅里面先。
5. 既然是Offline那么掉线玩家肯定是在已经开始游戏的房间里面而且其socket处于deleted但没有置为nullptr的状态。
6. 那么在pass之后不要创建旧的SPlayer对象而复用以前的。也不必走一次进lobby流程。
7. 所以先手动发送Setup和EnterLobby消息。
8. 发送Reconnect消息内含房间的所有信息。Client据此加入房间并设定好信息。
房间应该有哪些信息?
直接从UI着手
1. 首先EnterRoom消息需要**人数**和**操作时长**。
2. 既然需要人数了,那么就需要**所有玩家**。
3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。
4. 玩家的信息就更多了武将、身份、血量、id...
信息要怎么发呢:
- 一步一步的告诉重连中的玩家。
- 全部汇总成字符串或者别的什么,然后可以压缩并发送。
- 但以上两种都有问题许多信息保存在Lua中而Lua的运行是绝对不容其他线程打搅的。
- 而且粗略一想这些东西都应该非常耗时而如今的线程只有Main线程和各大Room线程。有必要给Room加个子线程专门处理掉线这块的然后Room该怎么跑继续怎么跑。
或者换个思路:
1. 首先EnterRoom消息需要**人数**和**操作时长**。
2. 服务端将这个客户端的*录像信息*发给客户端客户端满速且不影响UI的播放录像。
3. 在“播放录像”的过程中,客户端对于正在被收到的消息需进行特殊处理。
4. 一个录像文件的体积会非常大。所以服务端所保存的客户端录像应该和真正的录像有差别才行。比如聊天、战报这种数据量大但又无关紧要的东西就不保存。
5. 顺便这样也解决了多视角录像的问题,服务端给每个视角都录像就行了。
___
## 旁观TODO

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
---@param msg LogMessage
function Client:appendLog(msg)
local data = msg
local function getPlayerStr(pid, color)
if not pid then
return ""
end
local ret = self:getPlayerById(pid)
ret = ret.general
ret = Fk:translate(ret)
ret = string.format('<font color="' .. color .. '"><b>%s</b></font>', ret)
return ret
end
local from = getPlayerStr(data.from, "#0C8F0C")
local to = data.to or {}
local to_str = {}
for _, id in ipairs(to) do
table.insert(to_str, getPlayerStr(id, "#CC3131"))
end
to = table.concat(to_str, ", ")
local card = data.card or {}
local allUnknown = true
local unknownCount = 0
for _, id in ipairs(card) do
if id ~= -1 then
allUnknown = false
else
unknownCount = unknownCount + 1
end
end
if allUnknown then
card = ""
else
local card_str = {}
for _, id in ipairs(card) do
table.insert(card_str, Fk:getCardById(id):toLogString())
end
if unknownCount > 0 then
table.insert(card_str, Fk:translate("unknown_card")
.. unknownCount == 1 and "x" .. unknownCount or "")
end
card = table.concat(card_str, ", ")
end
local function parseArg(arg)
arg = arg or ""
arg = Fk:translate(arg)
arg = string.format('<font color="#0598BC"><b>%s</b></font>', arg)
return arg
end
local arg = parseArg(data.arg)
local arg2 = parseArg(data.arg2)
local arg3 = parseArg(data.arg3)
local log = Fk:translate(data.type)
log = string.gsub(log, "%%from", from)
log = string.gsub(log, "%%to", to)
log = string.gsub(log, "%%card", card)
log = string.gsub(log, "%%arg2", arg2)
log = string.gsub(log, "%%arg3", arg3)
log = string.gsub(log, "%%arg", arg)
self:notifyUI("GameLog", log)
end
fk.client_callback["Setup"] = function(jsonData)
-- jsonData: [ int id, string screenName, string avatar ]
local data = json.decode(jsonData)
@ -103,8 +172,10 @@ fk.client_callback["RemovePlayer"] = function(jsonData)
break
end
end
fk.ClientInstance:removePlayer(id)
ClientInstance:notifyUI("RemovePlayer", jsonData)
if id ~= Self.id then
fk.ClientInstance:removePlayer(id)
ClientInstance:notifyUI("RemovePlayer", jsonData)
end
end
fk.client_callback["ArrangeSeats"] = function(jsonData)
@ -163,6 +234,7 @@ local function separateMoves(moves)
to = move.to,
toArea = move.toArea,
fromArea = info.fromArea,
moveReason = move.moveReason,
})
end
end
@ -182,7 +254,8 @@ local function mergeMoves(moves)
from = move.from,
to = move.to,
fromArea = move.fromArea,
toArea = move.toArea
toArea = move.toArea,
moveReason = move.moveReason,
}
end
table.insert(temp[info].ids, move.ids[1])
@ -193,12 +266,33 @@ local function mergeMoves(moves)
return ret
end
local function sendMoveCardLog(move)
if move.moveReason == fk.ReasonDraw then
ClientInstance:appendLog{
type = "$DrawCards",
from = move.to,
card = move.ids,
arg = #move.ids,
}
elseif move.moveReason == fk.ReasonDiscard then
ClientInstance:appendLog{
type = "$DiscardCards",
from = move.from,
card = move.ids,
arg = #move.ids,
}
end
end
fk.client_callback["MoveCards"] = function(jsonData)
-- jsonData: CardsMoveStruct[]
local raw_moves = json.decode(jsonData)
local separated = separateMoves(raw_moves)
ClientInstance:moveCards(separated)
local merged = mergeMoves(separated)
for _, move in ipairs(merged) do
sendMoveCardLog(move)
end
ClientInstance:notifyUI("MoveCards", json.encode(merged))
end
@ -237,6 +331,30 @@ fk.client_callback["AskForUseActiveSkill"] = function(jsonData)
ClientInstance:notifyUI("AskForUseActiveSkill", jsonData)
end
fk.client_callback["SetPlayerMark"] = function(jsonData)
-- jsonData: [ int id, string mark, int value ]
local data = json.decode(jsonData)
local player, mark, value = data[1], data[2], data[3]
ClientInstance:getPlayerById(player):setMark(mark, value)
-- TODO: if mark is visible, update the UI.
end
fk.client_callback["Chat"] = function(jsonData)
-- jsonData: { int type, string msg }
local data = json.decode(jsonData)
local p = ClientInstance:getPlayerById(data.type)
data.userName = p.player:getScreenName()
data.general = p.general
data.time = os.date("%H:%M:%S")
ClientInstance:notifyUI("Chat", json.encode(data))
end
fk.client_callback["GameLog"] = function(jsonData)
local data = json.decode(jsonData)
ClientInstance:appendLog(data)
end
-- Create ClientInstance (used by Lua)
ClientInstance = Client:new()
dofile "lua/client/client_util.lua"

View File

@ -256,4 +256,51 @@ Fk:loadTranslationTable{
["$Equip"] = "装备区",
["$Judge"] = "判定区",
["#AskForUseActiveSkill"] = "请使用技能 %1",
["Trust"] = "托管",
["Sort Cards"] = "牌序",
["Chat"] = "聊天",
["Log"] = "战报",
}
-- related to sendLog
Fk:loadTranslationTable{
-- game processing
["$AppendSeparator"] = '<font color="grey">------------------------------</font>',
["$GameStart"] = "== 游戏开始 ==",
["$GameEnd"] = "== 游戏结束 ==",
-- get/lose skill
["#AcquireSkill"] = "%from 获得了技能“%arg”",
["#LoseSkill"] = "%from 失去了技能“%arg”",
-- moveCards (they are sent by notifyMoveCards)
["unknown_card"] = '<font color="#B5BA00"><b>未知牌</b></font>',
["log_spade"] = "",
["log_heart"] = '<font color="#CC3131">♥</font>',
["log_club"] = "",
["log_diamond"] = '<font color="#CC3131">♦</font>',
["log_nosuit"] = "无花色",
["nosuit"] = "无花色",
["spade"] = "黑桃",
["heart"] = "红桃",
["club"] = "梅花",
["diamond"] = "方块",
["$DrawCards"] = "%from 摸了 %arg 张牌 %card",
["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card",
-- useCard
["#UseCard"] = "%from 使用了牌 %card",
["#UseCardToTargets"] = "%from 使用了牌 %card目标是 %to",
-- judge
["#InitialJudge"] = "%from 的判定牌为 %card",
["#ChangedJudge"] = "%from 发动“%arg”把 %to 的判定牌改为 %card",
["#JudgeResult"] = "%from 的判定结果为 %card",
-- turnOver
["#TurnOver"] = "%from 将武将牌翻面,现在是 %arg",
["face_up"] = "正面朝上",
["face_down"] = "背面朝上",
}

View File

@ -87,8 +87,32 @@ function Card:getSuitString()
elseif suit == Card.Diamond then
return "diamond"
else
return "unknown"
return "nosuit"
end
end
local function getNumberStr(num)
if num == 1 then
return "A"
elseif num == 11 then
return "J"
elseif num == 12 then
return "Q"
elseif num == 13 then
return "K"
end
return tostring(num)
end
-- for sendLog
function Card:toLogString()
local ret = string.format('<font color="#0598BC"><b>%s</b></font>', Fk:translate(self.name) .. "[")
ret = ret .. Fk:translate("log_" .. self:getSuitString())
if self.number > 0 then
ret = ret .. string.format('<font color="%s"><b>%s</b></font>', self.color == Card.Red and "#CC3131" or "black", getNumberStr(self.number))
end
ret = ret .. '<font color="#0598BC"><b>]</b></font>'
return ret
end
return Card

View File

@ -9,7 +9,6 @@ package.path = package.path .. ";./lua/lib/?.lua"
class = require "middleclass"
json = require "json"
dofile "lua/lib/sha256.lua"
local GroupUtils = require "core.util"
TargetGroup, AimGroup = table.unpack(GroupUtils)
dofile "lua/core/debug.lua"

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.CardEffecting = 56
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
end
-- TODO: prepare drawPile
-- TODO: init cards in drawPile
local allCardIds = Fk:getAllCardIds()
table.shuffle(allCardIds)
room.draw_pile = allCardIds
@ -137,13 +135,15 @@ function GameLogic:prepareForStart()
end
for _, p in ipairs(room.alive_players) do
room:handleAddLoseSkills(p, "zhiheng")
room:handleAddLoseSkills(p, "zhiheng", nil, false)
end
self:addTriggerSkill(GameRule)
for _, trig in ipairs(Fk.global_trigger) do
self:addTriggerSkill(trig)
end
self.room:sendLog{ type = "$GameStart" }
end
function GameLogic:action()

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
function Room:initialize(_room)
self.room = _room
self.room.callback = function(_self, command, jsonData)
local cb = fk.room_callback[command]
if (type(cb) == "function") then
cb(jsonData)
else
print("Lobby error: Unknown command " .. command);
end
end
self.room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data
@ -206,6 +198,32 @@ function Room:getNCards(num, from)
return cardIds
end
---@param player ServerPlayer
---@param mark string
---@param value integer
function Room:setPlayerMark(player, mark, value)
player:setMark(mark, value)
self:doBroadcastNotify("SetPlayerMark", json.encode{
player.id,
mark,
value
})
end
function Room:addPlayerMark(player, mark, count)
count = count or 1
local num = player:getMark(mark)
num = num or 0
self:setPlayerMark(player, mark, math.max(num + count, 0))
end
function Room:removePlayerMark(player, mark, count)
count = count or 1
local num = player:getMark(mark)
num = num or 0
self:setPlayerMark(player, mark, math.max(num - count, 0))
end
------------------------------------------------------------------------
-- network functions, notify function
------------------------------------------------------------------------
@ -257,11 +275,11 @@ end
---@param command string
---@param players ServerPlayer[]
function Room:doBroadcastRequest(command, players)
function Room:doBroadcastRequest(command, players, jsonData)
players = players or self.players
self:notifyMoveFocus(players, command)
for _, p in ipairs(players) do
self:doRequest(p, command, p.request_data, false)
self:doRequest(p, command, jsonData or p.request_data, false)
end
local remainTime = self.timeout
@ -269,8 +287,39 @@ function Room:doBroadcastRequest(command, players)
local elapsed = 0
for _, p in ipairs(players) do
elapsed = os.time() - currentTime
remainTime = remainTime - elapsed
p:waitForReply(remainTime)
p:waitForReply(remainTime - elapsed)
end
end
---@param command string
---@param players ServerPlayer[]
function Room:doRaceRequest(command, players, jsonData)
players = players or self.players
self:notifyMoveFocus(players, command)
for _, p in ipairs(players) do
self:doRequest(p, command, jsonData or p.request_data, false)
end
local remainTime = self.timeout
local currentTime = os.time()
local elapsed = 0
local winner
while true do
elapsed = os.time() - currentTime
if remainTime - elapsed <= 0 then
return nil
end
for _, p in ipairs(players) do
p:waitForReply(0)
if p.reply_ready == true then
winner = p
break
end
end
if winner then
self:doBroadcastNotify("CancelRequest", "")
return winner
end
end
end
@ -328,6 +377,11 @@ function Room:notifyMoveFocus(players, command)
})
end
---@param log LogMessage
function Room:sendLog(log)
self:doBroadcastNotify("GameLog", json.encode(log))
end
------------------------------------------------------------------------
-- interactive functions
------------------------------------------------------------------------
@ -540,9 +594,9 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
---@type AimStruct
local aimStruct
local initialEvent = false
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 0
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
if not aimEventCollaborators[toId] or collaboratorsIndex[toId] >= #aimEventCollaborators[toId] then
if not aimEventCollaborators[toId] or collaboratorsIndex[toId] > #aimEventCollaborators[toId] then
aimStruct = {
from = cardUseEvent.from,
cardId = cardUseEvent.cardId,
@ -581,7 +635,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
cardUseEvent.from = aimStruct.from
cardUseEvent.tos = aimEventTargetGroup
cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets
if #AimGroup:getAllTargets(aimStruct.tos) == 0 then
return false
end
@ -590,18 +644,20 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators)
if #cancelledTargets > 0 then
for _, target in ipairs(cancelledTargets) do
aimEventCollaborators[target] = {}
collaboratorsIndex[target] = 0
collaboratorsIndex[target] = 1
end
end
aimStruct.tos[AimGroup.Cancelled] = {}
aimEventCollaborators[toId] = aimEventCollaborators[toId] or {}
if not room:getPlayerById(toId):isAlive() then
if room:getPlayerById(toId):isAlive() then
if initialEvent then
table.insert(aimEventCollaborators[toId], aimStruct)
else
aimEventCollaborators[toId][collaboratorsIndex[toId]] = aimStruct
end
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
end
AimGroup:setTargetDone(aimStruct.tos, toId)
@ -639,7 +695,9 @@ function Room:useCard(cardUseEvent)
end
for _, event in ipairs({ fk.AfterCardUseDeclared, fk.AfterCardTargetDeclared, fk.BeforeCardUseEffect, fk.CardUsing }) do
-- TODO: need to complete the cards for response
if not cardUseEvent.toCardId and #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then
break
end
self.logic:trigger(event, self:getPlayerById(cardUseEvent.from), cardUseEvent)
if event == fk.CardUsing then
@ -725,7 +783,59 @@ function Room:useCard(cardUseEvent)
end
if Fk:getCardById(cardUseEvent.cardId).skill then
Fk:getCardById(cardUseEvent.cardId).skill:onEffect(self, cardUseEvent)
---@type CardEffectEvent
local cardEffectEvent = {
from = cardUseEvent.from,
tos = cardUseEvent.tos,
cardId = cardUseEvent.cardId,
toCardId = cardUseEvent.toCardId,
responseToEvent = cardUseEvent.responseToEvent,
nullifiedTargets = cardUseEvent.nullifiedTargets,
disresponsiveList = cardUseEvent.disresponsiveList,
unoffsetableList = cardUseEvent.unoffsetableList,
addtionalDamage = cardUseEvent.addtionalDamage,
cardIdsResponded = cardUseEvent.nullifiedTargets,
}
if cardUseEvent.toCardId ~= nil then
self:doCardEffect(cardEffectEvent)
else
local collaboratorsIndex = {}
for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do
if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then
if aimEventCollaborators[toId] then
cardEffectEvent.to = toId
collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1
local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]]
cardEffectEvent.addtionalDamage = curAimEvent.additionalDamage
if curAimEvent.disresponsiveList then
for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do
if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then
table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer)
end
end
end
if curAimEvent.unoffsetableList then
for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do
if not table.contains(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer) then
table.insert(cardEffectEvent.unoffsetablePlayer, unoffsetablePlayer)
end
end
end
cardEffectEvent.disresponsive = curAimEvent.disresponsive
cardEffectEvent.unoffsetable = curAimEvent.unoffsetable
collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1
self:doCardEffect(cardEffectEvent)
end
end
end
end
end
end
end
@ -740,6 +850,80 @@ function Room:useCard(cardUseEvent)
end
end
---@param cardEffectEvent CardEffectEvent
function Room:doCardEffect(cardEffectEvent)
for _, event in ipairs({ fk.PreCardEffect, fk.BeforeCardEffect, fk.CardEffecting, fk.CardEffectFinished }) do
if cardEffectEvent.isCancellOut then
self.logic:trigger(fk.CardEffectCancelledOut, self:getPlayerById(cardEffectEvent.from), cardEffectEvent)
break
end
if not cardEffectEvent.toCardId and (not (self:getPlayerById(cardEffectEvent.to):isAlive() and cardEffectEvent.to) or #self:deadPlayerFilter(TargetGroup:getRealTargets(cardEffectEvent.tos)) == 0) then
break
end
if table.contains((cardEffectEvent.nullifiedTargets or {}), cardEffectEvent.to) then
break
end
if self.logic:trigger(event, self:getPlayerById(cardEffectEvent.from), cardEffectEvent) then
return
end
if event == fk.PreCardEffect then
-- TODO: use jink
if Fk:getCardById(cardEffectEvent.cardId).name == 'slash' and
not (
cardEffectEvent.disresponsive or
cardEffectEvent.unoffsetable or
table.contains(cardEffectEvent.disresponsiveList or {}, cardEffectEvent.to) or
table.contains(cardEffectEvent.unoffsetableList or {}, cardEffectEvent.to)
) then
local result = self:doRequest(self:getPlayerById(cardEffectEvent.to), "PlayCard", cardEffectEvent.to)
if result ~= '' then
local data = json.decode(result)
local card = data.card
local targets = data.targets
if type(card) == "string" then
local card_data = json.decode(card)
local skill = Fk.skills[card_data.skill]
local selected_cards = card_data.subcards
skill:onEffect(self, {
from = cardEffectEvent.to,
cards = selected_cards,
tos = targets,
})
else
local use = {} ---@type CardUseStruct
use.from = cardEffectEvent.to
use.toCardId = cardEffectEvent.cardId
use.responseToEvent = cardEffectEvent
use.cardId = card
self:useCard(use)
end
end
elseif Fk:getCardById(cardEffectEvent.cardId).type == Card.TypeTrick then
-- TODO: use nullification
-- local use = {} ---@type CardUseStruct
-- use.from = cardEffectEvent.to
-- use.toCardId = cardEffectEvent.cardId
-- use.responseToEvent = cardEffectEvent
-- use.cardId = card
-- self:useCard(use)
end
end
if event == fk.CardEffecting then
local cardEffecting = Fk:getCardById(cardEffectEvent.cardId)
if cardEffecting.skill then
cardEffecting.skill:onEffect(self, cardEffectEvent)
end
end
end
end
------------------------------------------------------------------------
-- move cards, and wrappers
------------------------------------------------------------------------
@ -879,6 +1063,43 @@ function Room:drawCards(player, num, skillName, fromPlace)
return { table.unpack(topCards) }
end
---@param card Card | Card[]
---@param to_place integer
---@param target ServerPlayer
---@param reason integer
---@param skill_name string
---@param special_name string
function Room:moveCardTo(card, to_place, target, reason, skill_name, special_name)
reason = reason or fk.ReasonJustMove
skill_name = skill_name or ""
special_name = special_name or ""
local ids = {}
if card[1] ~= nil then
for i, cd in ipairs(card) do
ids[i] = cd.id
end
else
ids[1] = card.id
end
local to
if table.contains(
{Card.PlayerEquip, Card.PlayerHand,
Card.PlayerJudge, Card.PlayerSpecial}, to_place) then
to = target.id
end
self.moveCards{
ids = ids,
from = self.owner_map[ids[1]],
to = to,
toArea = to_place,
moveReason = reason,
skillName = skill_name,
specialName = special_name
}
end
------------------------------------------------------------------------
-- some easier actions
------------------------------------------------------------------------
@ -1087,11 +1308,13 @@ end
---@param player ServerPlayer
---@param skill_names string[] | string
---@param source_skill string | Skill | nil
function Room:handleAddLoseSkills(player, skill_names, source_skill)
function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog)
if type(skill_names) == "string" then
skill_names = skill_names:split("|")
end
if sendlog == nil then sendlog = true end
if #skill_names == 0 then return end
local losts = {} ---@type boolean[]
local triggers = {} ---@type Skill[]
@ -1105,7 +1328,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
player.id,
s.name
})
-- TODO: send a log here
if sendlog then
self:sendLog{
type = "#LoseSkill",
from = player.id,
arg = s.name
}
end
table.insert(losts, true)
table.insert(triggers, s)
end
@ -1122,7 +1353,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
player.id,
s.name
})
-- TODO: send log
if sendlog then
self:sendLog{
type = "#AcquireSkill",
from = player.id,
arg = s.name
}
end
table.insert(losts, false)
table.insert(triggers, s)
end
@ -1138,6 +1377,35 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill)
end
end
-- judge
---@param data JudgeData
---@return Card
function Room:judge(data)
local who = data.who
self.logic:trigger(fk.StartJudge, who, data)
data.card = Fk:getCardById(self:getNCards(1)[1])
self:sendLog{
type = "#InitialJudge",
from = who.id,
card = {data.card},
}
self:moveCardTo(data.card, Card.Processing, nil, fk.ReasonPrey)
self.logic:trigger(fk.AskForRetrial, who, data)
self.logic:trigger(fk.FinishRetrial, who, data)
self:sendLog{
type = "#JudgeResult",
from = who.id,
card = {data.card},
}
self.logic:trigger(fk.FinishJudge, who, data)
if self:getCardArea(data.card.id) == Card.Processing then
self:moveCardTo(data.card, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile)
end
end
-- other helpers
function Room:adjustSeats()
@ -1187,29 +1455,6 @@ function Room:gameOver()
self.room:gameOver()
end
fk.room_callback = {}
fk.room_callback["QuitRoom"] = function(jsonData)
-- jsonData: [ int uid ]
local data = json.decode(jsonData)
local player = fk.ServerInstance:findPlayer(tonumber(data[1]))
local room = player:getRoom()
if not room:isLobby() then
room:removePlayer(player)
end
end
fk.room_callback["AddRobot"] = function(jsonData)
-- jsonData: [ int uid ]
local data = json.decode(jsonData)
local player = fk.ServerInstance:findPlayer(tonumber(data[1]))
local room = player:getRoom()
if not room:isLobby() then
room:addRobot(player)
end
end
function CreateRoom(_room)
RoomInstance = Room:new(_room)
end

View File

@ -85,7 +85,11 @@ function ServerPlayer:turnOver()
self.faceup = not self.faceup
self.room:broadcastProperty(self, "faceup")
-- TODO: log
self.room:sendLog{
type = "#TurnOver",
from = self.id,
arg = self.faceup and "face_up" or "face_down",
}
self.room.logic:trigger(fk.TurnedOver, self)
end

View File

@ -33,3 +33,5 @@ fk.ReasonResonpse = 10
fk.NormalDamage = 1
fk.ThunderDamage = 2
fk.FireDamage = 3
---@alias LogMessage {type: string, from: integer, to: integer[], card: integer[], arg: any, arg2: any, arg3: any}

View File

@ -18,7 +18,7 @@ GameRule = fk.CreateTriggerSkill{
if target == nil then
if event == fk.GameStart then
print("Game started")
fk.qInfo("Game started")
RoomInstance.tag["FirstRound"] = true
end
return false
@ -35,6 +35,7 @@ GameRule = fk.CreateTriggerSkill{
move_to_notify.toArea = Card.PlayerHand
move_to_notify.to = player.id
move_to_notify.moveInfo = {}
move_to_notify.moveReason = fk.ReasonDraw
for _, id in ipairs(cardIds) do
table.insert(move_to_notify.moveInfo,
{ cardId = id, fromArea = Card.DrawPile })
@ -55,7 +56,7 @@ GameRule = fk.CreateTriggerSkill{
player:setFlag("Global_FirstRound")
end
-- TODO: send log
room:sendLog{ type = "$AppendSeparator" }
player:addMark("Global_TurnCount")
if not player.faceup then

View File

@ -5,10 +5,35 @@ Fk:loadTranslationTable{
["standard_cards"] = "标+EX"
}
local slashSkill = fk.CreateActiveSkill{
name = "slash_skill",
target_filter = function(self, to_select, selected)
if #selected == 0 then
local player = Fk:currentRoom():getPlayerById(to_select)
return Self ~= player
end
end,
feasible = function(self, selected)
return #selected == 1
end,
on_effect = function(self, room, effect)
local to = effect.to
local from = effect.from
local cid = room:askForCardChosen(
room:getPlayerById(from),
room:getPlayerById(to),
"hej",
"snatch"
)
room:obtainCard(from, cid)
end
}
local slash = fk.CreateBasicCard{
name = "slash",
number = 7,
suit = Card.Spade,
skill = slashSkill,
}
Fk:loadTranslationTable{
["slash"] = "",
@ -50,10 +75,19 @@ extension:addCards({
slash:clone(Card.Diamond, 13),
})
local jinkSkill = fk.CreateActiveSkill{
name = "jink_skill",
on_effect = function(self, room, effect)
if effect.responseToEvent then
effect.responseToEvent.isCancellOut = true
end
end
}
local jink = fk.CreateBasicCard{
name = "jink",
suit = Card.Heart,
number = 2,
skill = jinkSkill,
}
Fk:loadTranslationTable{
["jink"] = "",
@ -131,7 +165,7 @@ local snatchSkill = fk.CreateActiveSkill{
return #selected == 1
end,
on_effect = function(self, room, effect)
local to = TargetGroup:getRealTargets(effect.tos)[1]
local to = effect.to
local from = effect.from
local cid = room:askForCardChosen(
room:getPlayerById(from),
@ -201,7 +235,7 @@ local exNihiloSkill = fk.CreateActiveSkill{
end
end,
on_effect = function(self, room, cardEffectEvent)
room:drawCards(room:getPlayerById(TargetGroup:getRealTargets(cardEffectEvent.tos)[1]), 2, "ex_nihilo")
room:drawCards(room:getPlayerById(cardEffectEvent.to), 2, "ex_nihilo")
end
}
@ -222,10 +256,19 @@ extension:addCards({
exNihilo:clone(Card.Heart, 11),
})
local nullificationSkill = fk.CreateActiveSkill{
name = "nullification_skill",
on_effect = function(self, room, effect)
if effect.responseToEvent then
effect.responseToEvent.isCancellOut = true
end
end
}
local nullification = fk.CreateTrickCard{
name = "nullification",
suit = Card.Spade,
number = 11,
skill = nullificationSkill,
}
Fk:loadTranslationTable{
["nullification"] = "无懈可击",

View File

@ -2,12 +2,36 @@ import QtQuick
QtObject {
// Client configuration
property real winWidth
property real winHeight
property var conf: ({})
property string lastLoginServer
property var savedPassword: ({})
// Player property of client
property string serverAddr
property string screenName: ""
property string password: ""
property string cipherText
// Client data
property int roomCapacity: 0
property int roomTimeout: 0
function loadConf() {
conf = JSON.parse(Backend.loadConf());
winWidth = conf.winWidth;
winHeight = conf.winHeight;
lastLoginServer = conf.lastLoginServer;
savedPassword = conf.savedPassword;
}
function saveConf() {
conf.winWidth = realMainWin.width;
conf.winHeight = realMainWin.height;
conf.lastLoginServer = lastLoginServer;
conf.savedPassword = savedPassword;
Backend.saveConf(JSON.stringify(conf, undefined, 2));
}
}

View File

@ -15,9 +15,20 @@ function createClientPages() {
var callbacks = {};
callbacks["NetworkDelayTest"] = function(jsonData) {
// jsonData: RSA pub key
let cipherText
if (config.savedPassword[config.serverAddr] !== undefined
&& config.savedPassword[config.serverAddr].shorten_password === config.password) {
cipherText = config.savedPassword[config.serverAddr].password;
if (Debugging)
console.log("use remembered password", config.password);
} else {
cipherText = Backend.pubEncrypt(jsonData, config.password);
}
config.cipherText = cipherText;
let md5sum = Backend.calcFileMD5();
ClientInstance.notifyServer("Setup", JSON.stringify([
config.screenName,
config.password
config.screenName, cipherText, md5sum
]));
}
@ -37,6 +48,13 @@ callbacks["EnterLobby"] = function(jsonData) {
// depth == 1 means the lobby page is not present in mainStack
createClientPages();
if (mainStack.depth === 1) {
// we enter the lobby successfully, so save password now.
config.lastLoginServer = config.serverAddr;
config.savedPassword[config.serverAddr] = {
username: config.screenName,
password: config.cipherText,
shorten_password: config.cipherText.slice(0, 8)
}
mainStack.push(lobby);
} else {
mainStack.pop();
@ -66,3 +84,18 @@ callbacks["UpdateRoomList"] = function(jsonData) {
});
});
}
callbacks["Chat"] = function(jsonData) {
// jsonData: { string userName, string general, string time, string msg }
let current = mainStack.currentItem; // lobby(TODO) or room
let data = JSON.parse(jsonData);
let pid = data.type;
let userName = data.userName;
let general = Backend.translate(data.general);
let time = data.time;
let msg = data.msg;
if (general === "")
current.addToChat(pid, data, `[${time}] ${userName}: ${msg}`);
else
current.addToChat(pid, data, `[${time}] ${userName}(${general}): ${msg}`);
}

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 {
spacing: 8
TextField {
ComboBox {
id: server_addr
text: "127.0.0.1"
model: []
editable: true
onEditTextChanged: {
if (model.indexOf(editText) === -1) {
passwordEdit.text = "";
} else {
let data = config.savedPassword[editText];
screenNameEdit.text = data.username;
passwordEdit.text = data.shorten_password;
}
}
}
TextField {
id: screenNameEdit
text: "player"
onTextChanged: {
passwordEdit.text = "";
let data = config.savedPassword[server_addr.editText];
if (data) {
if (text === data.username) {
passwordEdit.text = data.shorten_password;
}
}
}
}
/*TextField {
id: avatarEdit
@ -35,16 +55,20 @@ Item {
}
Button {
text: "Join Server"
enabled: passwordEdit.text !== ""
onClicked: {
config.serverAddr = server_addr.editText;
config.screenName = screenNameEdit.text;
config.password = passwordEdit.text;
mainWindow.busy = true;
Backend.joinServer(server_addr.text);
Backend.joinServer(server_addr.editText);
}
}
Button {
text: "Console start"
enabled: passwordEdit.text !== ""
onClicked: {
config.serverAddr = "127.0.0.1";
config.screenName = screenNameEdit.text;
config.password = passwordEdit.text;
mainWindow.busy = true;
@ -54,4 +78,15 @@ Item {
}
}
}
Component.onCompleted: {
config.loadConf();
server_addr.model = Object.keys(config.savedPassword);
server_addr.onModelChanged();
server_addr.currentIndex = server_addr.model.indexOf(config.lastLoginServer);
let data = config.savedPassword[config.lastLoginServer];
screenNameEdit.text = data.username;
passwordEdit.text = data.shorten_password;
}
}

View File

@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "Common"
import "RoomElement"
import "RoomLogic.js" as Logic
@ -15,6 +16,10 @@ Item {
property alias popupBox: popupBox
property alias promptText: prompt.text
property alias okCancel: okCancel
property alias okButton: okButton
property alias cancelButton: cancelButton
property alias dynamicCardArea: dynamicCardArea
property var selected_targets: []
@ -30,7 +35,7 @@ Item {
anchors.top: parent.top
anchors.right: parent.right
onClicked: {
ClientInstance.clearPlayers();
// ClientInstance.clearPlayers();
ClientInstance.notifyServer("QuitRoom", "[]");
}
}
@ -183,10 +188,30 @@ Item {
}
}
Item {
id: dashboardBtn
width: childrenRect.width
height: childrenRect.height
anchors.bottom: parent.bottom
ColumnLayout {
MetroButton {
text: Backend.translate("Trust")
}
MetroButton {
text: Backend.translate("Sort Cards")
}
MetroButton {
text: Backend.translate("Chat")
onClicked: roomDrawer.open();
}
}
}
Dashboard {
id: dashboard
width: roomScene.width
width: roomScene.width - dashboardBtn.width
anchors.top: roomArea.bottom
anchors.left: dashboardBtn.right
self.playerid: dashboardModel.id
self.general: dashboardModel.general
@ -222,8 +247,10 @@ Item {
Text {
id: prompt
visible: progress.visible
anchors.bottom: progress.top
anchors.bottomMargin: 8
anchors.top: progress.top
anchors.topMargin: -2
color: "white"
z: 1
anchors.horizontalCenter: progress.horizontalCenter
}
@ -232,11 +259,31 @@ Item {
width: parent.width * 0.6
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: okCancel.top
anchors.bottomMargin: 8
anchors.bottomMargin: 4
from: 0.0
to: 100.0
visible: false
background: Rectangle {
implicitWidth: 200
implicitHeight: 14
color: "black"
radius: 3
}
contentItem: Item {
implicitWidth: 200
implicitHeight: 12
Rectangle {
width: progress.visualPosition * parent.width
height: parent.height
radius: 2
color: "red"
}
}
NumberAnimation on value {
running: progress.visible
from: 100.0
@ -315,6 +362,133 @@ Item {
}
}
Drawer {
id: roomDrawer
width: parent.width * 0.3
height: parent.height
dim: false
clip: true
dragMargin: 0
ColumnLayout {
anchors.fill: parent
SwipeView {
Layout.fillWidth: true
Layout.fillHeight: true
interactive: false
currentIndex: drawerBar.currentIndex
Item {
LogEdit {
id: log
anchors.fill: parent
}
}
Item {
ChatBox {
id: chat
anchors.fill: parent
}
}
}
TabBar {
id: drawerBar
width: roomDrawer.width
TabButton {
width: roomDrawer.width / 2
text: Backend.translate("Log")
}
TabButton {
width: roomDrawer.width / 2
text: Backend.translate("Chat")
}
}
}
}
Item {
id: dynamicCardArea
anchors.fill: parent
}
Rectangle {
id: easyChat
width: parent.width
height: 28
anchors.bottom: parent.bottom
visible: false
color: "#040403"
radius: 3
border.width: 1
border.color: "#A6967A"
TextInput {
id: easyChatEdit
anchors.fill: parent
anchors.margins: 6
color: "white"
clip: true
font.pixelSize: 14
onAccepted: {
if (text != "") {
ClientInstance.notifyServer(
"Chat",
JSON.stringify({
type: 0,
msg: text
})
);
text = "";
easyChat.visible = false;
easyChatEdit.enabled = false;
}
}
}
}
Shortcut {
sequence: "T"
onActivated: {
easyChat.visible = true;
easyChatEdit.enabled = true;
easyChatEdit.forceActiveFocus();
}
}
Shortcut {
sequence: "Esc"
onActivated: {
easyChat.visible = false;
easyChatEdit.enabled = false;
}
}
Shortcut {
sequence: "Return"
enabled: okButton.enabled
onActivated: Logic.doOkButton();
}
Shortcut {
sequence: "Space"
enabled: cancelButton.enabled
onActivated: Logic.doCancelButton();
}
function addToChat(pid, raw, msg) {
chat.append(msg);
let photo = Logic.getPhoto(pid);
if (photo === undefined)
photo = dashboard.self;
photo.chat(raw.msg);
}
function addToLog(msg) {
log.append(msg);
}
Component.onCompleted: {
toast.show(Backend.translate("$EnterRoom"));

View File

@ -61,7 +61,7 @@ Item {
state.x = parentPos.x;
state.y = parentPos.y;
state.opacity = 0;
card = component.createObject(roomScene, state);
card = component.createObject(roomScene.dynamicCardArea, state);
card.x -= card.width / 2;
card.x += (i - outputs.length / 2) * 15;
card.y -= card.height / 2;

View File

@ -371,10 +371,55 @@ Item {
anchors.centerIn: parent
}
Rectangle {
id: chat
color: "#F2ECD7"
radius: 4
opacity: 0
width: parent.width
height: childrenRect.height + 8
property string text: ""
visible: false
Text {
width: parent.width - 8
x: 4
y: 4
text: parent.text
wrapMode: Text.WrapAnywhere
font.family: fontLibian.name
font.pixelSize: 20
}
SequentialAnimation {
id: chatAnim
PropertyAnimation {
target: chat
property: "opacity"
to: 0.9
duration: 200
}
NumberAnimation {
duration: 2500
}
PropertyAnimation {
target: chat
property: "opacity"
to: 0
duration: 150
}
onFinished: chat.visible = false;
}
}
onGeneralChanged: {
if (!roomScene.isStarted) return;
generalName.text = Backend.translate(general);
let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general]));
kingdom = data.kingdom;
}
function chat(msg) {
chat.text = msg;
chat.visible = true;
chatAnim.restart();
}
}

View File

@ -379,9 +379,11 @@ callbacks["ArrangeSeats"] = function(jsonData) {
for (let i = 0; i < photoModel.count; i++) {
let item = photoModel.get(i);
item.seatNumber = order.indexOf(item.id) + 1;
item.general = "";
}
dashboardModel.seatNumber = order.indexOf(Self.id) + 1;
dashboardModel.general = "";
roomScene.dashboardModelChanged();
// make Self to the first of list, then reorder photomodel
@ -476,7 +478,10 @@ callbacks["AskForSkillInvoke"] = function(jsonData) {
// jsonData: string name
roomScene.promptText = Backend.translate("#AskForSkillInvoke")
.arg(Backend.translate(jsonData));
roomScene.state = "responding";
roomScene.state = "replying";
roomScene.okCancel.visible = true;
roomScene.okButton.enabled = true;
roomScene.cancelButton.enabled = true;
}
callbacks["AskForChoice"] = function(jsonData) {
@ -590,3 +595,11 @@ callbacks["AskForUseActiveSkill"] = function(jsonData) {
dashboard.startPending(skill_name);
cancelButton.enabled = cancelable;
}
callbacks["CancelRequest"] = function() {
roomScene.state = "notactive";
}
callbacks["GameLog"] = function(jsonData) {
roomScene.addToLog(jsonData)
}

View File

@ -5,6 +5,7 @@ import "Logic.js" as Logic
import "Pages"
Window {
id: realMainWin
visible: true
width: 960
height: 540
@ -19,6 +20,10 @@ Item {
scale: parent.width / width
anchors.centerIn: parent
Config {
id: config
}
Image {
source: AppPath + "/image/background"
anchors.fill: parent
@ -52,10 +57,6 @@ Item {
visible: mainWindow.busy === true
}
Config {
id: config
}
// global popup. it is modal and just lower than toast
Rectangle {
id: globalPopupDim
@ -140,7 +141,27 @@ Item {
}
}
Shortcut {
sequences: [ StandardKey.FullScreen ]
onActivated: {
if (realMainWin.visibility === Window.FullScreen)
realMainWin.showNormal();
else
realMainWin.showFullScreen();
}
}
Component.onCompleted: {
if (!Android) {
width = config.winWidth;
height = config.winHeight;
}
}
onClosing: {
config.winWidth = width;
config.winHeight = height;
config.saveConf();
Backend.quitLobby();
}
}

View File

@ -2,6 +2,7 @@ CREATE TABLE userinfo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255),
password CHAR(64),
salt CHAR(8),
avatar VARCHAR(64),
lastLoginIp VARCHAR(64),
banned BOOLEAN

View File

@ -31,16 +31,21 @@ set(freekill_HEADERS
if (WIN32)
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll)
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll)
set(CRYPTO_LIB ${PROJECT_SOURCE_DIR}/lib/win/libcrypto_1_1.dll)
elseif (ANDROID)
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/android/liblua54.so)
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/android/libsqlite3.so)
set(CRYPTO_LIB ${PROJECT_SOURCE_DIR}/lib/android/libcrypto_1_1.so)
set_target_properties(FreeKill PROPERTIES
QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android
QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB}"
QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB};${CRYPTO_LIB}"
)
else ()
set(LUA_LIB lua5.4)
set(SQLITE3_LIB sqlite3)
set(CRYPTO_LIB OpenSSL::Crypto)
set(READLINE_LIB readline)
list(APPEND freekill_SRCS "server/shell.cpp")
endif ()
source_group("Include" FILES ${freekill_HEADERS})
@ -50,9 +55,12 @@ target_precompile_headers(FreeKill PRIVATE "pch.h")
target_link_libraries(FreeKill PRIVATE
${LUA_LIB}
${SQLITE3_LIB}
${CRYPTO_LIB}
${READLINE_LIB}
fkparse
Qt6::Qml
Qt6::Gui
Qt6::Widgets
Qt6::Network
Qt6::Multimedia
)

View File

@ -32,7 +32,7 @@ Client::~Client()
router->getSocket()->deleteLater();
}
void Client::connectToHost(const QHostAddress& server, ushort port)
void Client::connectToHost(const QString &server, ushort port)
{
router->getSocket()->connectToHost(server, port);
}

View File

@ -11,7 +11,7 @@ public:
Client(QObject *parent = nullptr);
~Client();
void connectToHost(const QHostAddress &server, ushort port);
void connectToHost(const QString &server, ushort port);
Q_INVOKABLE void replyToServer(const QString &command, const QString &jsonData);
Q_INVOKABLE void notifyServer(const QString &command, const QString &jsonData);

View File

@ -1,4 +1,7 @@
#include "util.h"
#include <qcryptographichash.h>
#include <qnamespace.h>
#include <qregularexpression.h>
extern "C" {
int luaopen_fk(lua_State *);
@ -24,7 +27,7 @@ bool DoLuaScript(lua_State *L, const char *script)
if (error) {
const char *error_msg = lua_tostring(L, -1);
qDebug() << error_msg;
qCritical() << error_msg;
lua_pop(L, 2);
return false;
}
@ -65,7 +68,7 @@ sqlite3 *OpenDatabase(const QString &filename)
if (!QFile::exists(filename)) {
QFile file("./server/init.sql");
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "cannot open init.sql. Quit now.";
qFatal("cannot open init.sql. Quit now.");
qApp->exit(1);
}
@ -75,7 +78,7 @@ sqlite3 *OpenDatabase(const QString &filename)
rc = sqlite3_exec(ret, in.readAll().toLatin1().data(), nullptr, nullptr, &err_msg);
if (rc != SQLITE_OK ) {
qDebug() << "sqlite error:" << err_msg;
qCritical() << "sqlite error:" << err_msg;
sqlite3_free(err_msg);
sqlite3_close(ret);
qApp->exit(1);
@ -83,7 +86,7 @@ sqlite3 *OpenDatabase(const QString &filename)
} else {
rc = sqlite3_open(filename.toLatin1().data(), &ret);
if (rc != SQLITE_OK) {
qDebug() << "Cannot open database:" << sqlite3_errmsg(ret);
qCritical() << "Cannot open database:" << sqlite3_errmsg(ret);
sqlite3_close(ret);
qApp->exit(1);
}
@ -121,3 +124,75 @@ void ExecSQL(sqlite3 *db, const QString &sql) {
void CloseDatabase(sqlite3 *db) {
sqlite3_close(db);
}
RSA *InitServerRSA() {
RSA *rsa = RSA_new();
if (!QFile::exists("server/rsa_pub")) {
BIGNUM *bne = BN_new();
BN_set_word(bne, RSA_F4);
RSA_generate_key_ex(rsa, 2048, bne, NULL);
BIO *bp_pub = BIO_new_file("server/rsa_pub", "w+");
PEM_write_bio_RSAPublicKey(bp_pub, rsa);
BIO *bp_pri = BIO_new_file("server/rsa", "w+");
PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL);
BIO_free_all(bp_pub);
BIO_free_all(bp_pri);
BN_free(bne);
}
FILE *keyFile = fopen("server/rsa_pub", "r");
PEM_read_RSAPublicKey(keyFile, &rsa, NULL, NULL);
fclose(keyFile);
keyFile = fopen("server/rsa", "r");
PEM_read_RSAPrivateKey(keyFile, &rsa, NULL, NULL);
fclose(keyFile);
return rsa;
}
static void writeFileMD5(QFile &dest, const QString &fname) {
QFile f(fname);
if (!f.open(QIODevice::ReadOnly)) {
return;
}
auto data = f.readAll();
auto hash = QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex();
dest.write(fname.toUtf8() + '=' + hash + '\n');
}
static void writeDirMD5(QFile &dest, const QString &dir, const QString &filter) {
QDir d(dir);
auto entries = d.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
auto re = QRegularExpression::fromWildcard(filter);
foreach (QFileInfo info, entries) {
if (info.isDir()) {
writeDirMD5(dest, info.filePath(), filter);
} else {
if (re.match(info.fileName()).hasMatch()) {
writeFileMD5(dest, info.filePath());
}
}
}
}
QString calcFileMD5() {
// First, generate flist.txt
// flist.txt is a file contains all md5sum for code files
QFile flist("flist.txt");
if (!flist.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
qFatal("Cannot open flist.txt. Quitting.");
}
writeDirMD5(flist, "lua", "*.lua");
writeDirMD5(flist, "qml", "*.qml");
writeDirMD5(flist, "qml", "*.js");
// then, return flist.txt's md5
flist.close();
flist.open(QIODevice::ReadOnly);
auto ret = QCryptographicHash::hash(flist.readAll(), QCryptographicHash::Md5);
flist.close();
return ret.toHex();
}

View File

@ -13,4 +13,8 @@ QString SelectFromDb(sqlite3 *db, const QString &sql);
void ExecSQL(sqlite3 *db, const QString &sql);
void CloseDatabase(sqlite3 *db);
RSA *InitServerRSA();
QString calcFileMD5();
#endif // _GLOBAL_H

View File

@ -1,6 +1,13 @@
#include "qmlbackend.h"
#include "server.h"
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include "shell.h"
#endif
#include <QSplashScreen>
#include <QScreen>
#ifdef Q_OS_ANDROID
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
{
@ -31,16 +38,37 @@ static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
}
#endif
void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
fprintf(stderr, "\r[%s] ", QTime::currentTime().toString("hh:mm:ss").toLatin1().constData());
auto localMsg = msg.toUtf8().constData();
auto threadName = QThread::currentThread()->objectName().toLatin1().constData();
switch (type) {
case QtDebugMsg:
fprintf(stderr, "[%s/\e[1;30mDEBUG\e[0m] %s\n", threadName, localMsg);
break;
case QtInfoMsg:
fprintf(stderr, "[%s/\e[1;32mINFO\e[0m] %s\n", threadName, localMsg);
break;
case QtWarningMsg:
fprintf(stderr, "[%s/\e[1;33mWARNING\e[0m] %s\n", threadName, localMsg);
break;
case QtCriticalMsg:
fprintf(stderr, "[%s/\e[1;31mCRITICAL\e[0m] %s\n", threadName, localMsg);
break;
case QtFatalMsg:
fprintf(stderr, "[%s/\e[1;31mFATAL\e[0m] %s\n", threadName, localMsg);
break;
}
}
int main(int argc, char *argv[])
{
QThread::currentThread()->setObjectName("Main");
qInstallMessageHandler(fkMsgHandler);
QCoreApplication *app;
QCoreApplication::setApplicationName("FreeKill");
QCoreApplication::setApplicationVersion("Alpha 0.0.1");
#ifdef Q_OS_ANDROID
copyPath("assets:/res", QDir::currentPath());
#endif
QCommandLineParser parser;
parser.setApplicationDescription("FreeKill server");
parser.addHelpOption();
@ -62,14 +90,38 @@ int main(int argc, char *argv[])
serverPort = parser.value("server").toInt();
Server *server = new Server;
if (!server->listen(QHostAddress::Any, serverPort)) {
fprintf(stderr, "cannot listen on port %d!\n", serverPort);
qFatal("cannot listen on port %d!\n", serverPort);
app->exit(1);
} else {
qInfo("Server is listening on port %d", serverPort);
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
auto shell = new Shell;
shell->start();
#endif
}
return app->exec();
}
app = new QGuiApplication(argc, argv);
app = new QApplication(argc, argv);
#define SHOW_SPLASH_MSG(msg) \
splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom);
#ifdef Q_OS_ANDROID
QScreen *screen = qobject_cast<QApplication *>(app)->primaryScreen();
QRect screenGeometry = screen->geometry();
int screenWidth = screenGeometry.width();
int screenHeight = screenGeometry.height();
QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg").scaled(screenWidth, screenHeight));
splash.showFullScreen();
SHOW_SPLASH_MSG("Copying resources...");
copyPath("assets:/res", QDir::currentPath());
#else
QSplashScreen splash(QPixmap("image/splash.jpg"));
splash.show();
#endif
SHOW_SPLASH_MSG("Loading qml files...");
QQmlApplicationEngine *engine = new QQmlApplicationEngine;
QmlBackend backend;
@ -83,10 +135,16 @@ int main(int argc, char *argv[])
bool debugging = false;
#endif
engine->rootContext()->setContextProperty("Debugging", debugging);
#ifdef Q_OS_ANDROID
engine->rootContext()->setContextProperty("Android", true);
#else
engine->rootContext()->setContextProperty("Android", false);
#endif
engine->load("qml/main.qml");
if (engine->rootObjects().isEmpty())
return -1;
splash.close();
int ret = app->exec();
// delete the engine first

View File

@ -26,7 +26,7 @@ void ClientSocket::init()
this, &ClientSocket::raiseError);
}
void ClientSocket::connectToHost(const QHostAddress &address, ushort port)
void ClientSocket::connectToHost(const QString &address, ushort port)
{
socket->connectToHost(address, port);
}

View File

@ -9,7 +9,7 @@ public:
// For server use
ClientSocket(QTcpSocket *socket);
void connectToHost(const QHostAddress &address = QHostAddress::LocalHost, ushort port = 9527u);
void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u);
void disconnectFromHost();
void send(const QByteArray& msg);
bool isConnected() const;

View File

@ -3,6 +3,7 @@
#include "client_socket.h"
#include "server.h"
#include "serverplayer.h"
#include "util.h"
Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
: QObject(parent)
@ -142,6 +143,60 @@ void Router::abortRequest()
void Router::handlePacket(const QByteArray& rawPacket)
{
static QMap<QString, void (*)(ServerPlayer *, const QString &)> lobby_actions;
if (lobby_actions.size() <= 0) {
lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender, const QString &jsonData){
auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array();
auto avatar = arr[0].toString();
static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
if (!nameExp.match(avatar).hasMatch()) {
auto sql = QString("UPDATE userinfo SET avatar='%1' WHERE id=%2;")
.arg(avatar).arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql);
sender->setAvatar(avatar);
sender->doNotify("UpdateAvatar", avatar);
}
};
lobby_actions["UpdatePassword"] = [](ServerPlayer *sender, const QString &jsonData){
auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array();
auto oldpw = arr[0].toString();
auto newpw = arr[1].toString();
auto sql_find = QString("SELECT password, salt FROM userinfo WHERE id=%1;")
.arg(sender->getId());
auto passed = false;
auto result = SelectFromDatabase(ServerInstance->getDatabase(), sql_find);
passed = (result["password"].toArray()[0].toString() ==
QCryptographicHash::hash(
oldpw.append(result["salt"].toArray()[0].toString()).toLatin1(),
QCryptographicHash::Sha256).toHex());
if (passed) {
auto sql_update = QString("UPDATE userinfo SET password='%1' WHERE id=%2;")
.arg(QCryptographicHash::hash(
newpw.append(result["salt"].toArray()[0].toString()).toLatin1(),
QCryptographicHash::Sha256).toHex())
.arg(sender->getId());
ExecSQL(ServerInstance->getDatabase(), sql_update);
}
sender->doNotify("UpdatePassword", passed ? "1" : "0");
};
lobby_actions["CreateRoom"] = [](ServerPlayer *sender, const QString &jsonData){
auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array();
auto name = arr[0].toString();
auto capacity = arr[1].toInt();
ServerInstance->createRoom(sender, name, capacity);
};
lobby_actions["EnterRoom"] = [](ServerPlayer *sender, const QString &jsonData){
auto arr = QJsonDocument::fromJson(jsonData.toUtf8()).array();
auto roomId = arr[0].toInt();
ServerInstance->findRoom(roomId)->addPlayer(sender);
};
lobby_actions["Chat"] = [](ServerPlayer *sender, const QString &jsonData){
sender->getRoom()->chat(sender, jsonData);
};
}
QJsonDocument packet = QJsonDocument::fromJson(rawPacket);
if (packet.isNull() || !packet.isArray())
return;
@ -156,12 +211,19 @@ void Router::handlePacket(const QByteArray& rawPacket)
ClientInstance->callLua(command, jsonData);
} else {
ServerPlayer *player = qobject_cast<ServerPlayer *>(parent());
// Add the uid of sender to jsonData
QJsonArray arr = QJsonDocument::fromJson(jsonData.toUtf8()).array();
arr.prepend(player->getId());
Room *room = player->getRoom();
room->callLua(command, QJsonDocument(arr).toJson());
if (room->isLobby() && lobby_actions.contains(command))
lobby_actions[command](player, jsonData);
else {
if (command == "QuitRoom") {
room->removePlayer(player);
} else if (command == "AddRobot") {
room->addRobot(player);
} else if (command == "Chat") {
room->chat(player, jsonData);
}
}
}
}
else if (type & TYPE_REQUEST) {

View File

@ -3,7 +3,7 @@
// core gui qml
#include <QtCore>
#include <QGuiApplication>
#include <QApplication>
#include <QtQml>
// network
@ -15,4 +15,10 @@ typedef int LuaFunction;
#include "lua.hpp"
#include "sqlite3.h"
// Note: headers of openssl is too big, so they are not provided in git repo
// Please install openssl's src via Qt Installer, then copy headers
// (<Qt_root>/Tools/OpenSSL/src/include/openssl) to <Qt6_dir>/mingw_64/include
#include <openssl/rsa.h>
#include <openssl/pem.h>
#endif // _PCH_H

View File

@ -5,27 +5,26 @@
Room::Room(Server* server)
{
setObjectName("Room");
id = server->nextRoomId;
server->nextRoomId++;
this->server = server;
setParent(server);
m_abandoned = false;
owner = nullptr;
gameStarted = false;
robot_id = -1;
robot_id = -2; // -1 is reserved in UI logic
timeout = 15;
L = NULL;
if (!isLobby()) {
connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer);
connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer);
}
L = CreateLuaState();
DoLuaScript(L, "lua/freekill.lua");
if (isLobby()) {
DoLuaScript(L, "lua/server/lobby.lua");
} else {
L = CreateLuaState();
DoLuaScript(L, "lua/freekill.lua");
DoLuaScript(L, "lua/server/room.lua");
initLua();
}
initLua();
}
Room::~Room()
@ -35,7 +34,7 @@ Room::~Room()
terminate();
wait();
}
lua_close(L);
if (L) lua_close(L);
}
Server *Room::getServer() const
@ -48,6 +47,11 @@ int Room::getId() const
return id;
}
void Room::setId(int id)
{
this->id = id;
}
bool Room::isLobby() const
{
return id == 0;
@ -80,6 +84,9 @@ bool Room::isFull() const
bool Room::isAbandoned() const
{
if (isLobby())
return false;
if (players.isEmpty())
return true;
@ -90,6 +97,10 @@ bool Room::isAbandoned() const
return true;
}
void Room::setAbandoned(bool abandoned) {
m_abandoned = abandoned;
}
ServerPlayer *Room::getOwner() const
{
return owner;
@ -201,10 +212,11 @@ void Room::removePlayer(ServerPlayer *player)
server->addPlayer(runner);
emit playerRemoved(runner);
runner->abortRequest();
player->abortRequest();
}
if (isAbandoned()) {
if (isAbandoned() && !m_abandoned) {
m_abandoned = true;
emit abandoned();
} else if (player == owner) {
setOwner(players.first());
@ -265,6 +277,17 @@ void Room::doBroadcastNotify(const QList<ServerPlayer *> targets,
}
}
void Room::chat(ServerPlayer *sender, const QString &jsonData) {
auto doc = QJsonDocument::fromJson(jsonData.toUtf8()).object();
auto type = doc["type"].toInt();
doc["type"] = sender->getId();
if (type == 1) {
// TODO: server chatting
} else {
doBroadcastNotify(players, "Chat", QJsonDocument(doc).toJson(QJsonDocument::Compact));
}
}
void Room::gameOver()
{
gameStarted = false;
@ -275,6 +298,8 @@ void Room::gameOver()
p->deleteLater();
}
}
players.clear();
owner = nullptr;
}
void Room::run()

View File

@ -14,6 +14,7 @@ public:
// ==================================={
Server *getServer() const;
int getId() const;
void setId(int id);
bool isLobby() const;
QString getName() const;
void setName(const QString &name);
@ -21,6 +22,7 @@ public:
void setCapacity(int capacity);
bool isFull() const;
bool isAbandoned() const;
void setAbandoned(bool abandoned); // never use this function
ServerPlayer *getOwner() const;
void setOwner(ServerPlayer *owner);
@ -46,12 +48,11 @@ public:
const QString &command,
const QString &jsonData
);
void chat(ServerPlayer *sender, const QString &jsonData);
void gameOver();
void initLua();
void callLua(const QString &command, const QString &jsonData);
LuaFunction callback;
void roomStart();
LuaFunction startGame;

View File

@ -13,6 +13,13 @@ Server::Server(QObject* parent)
{
ServerInstance = this;
db = OpenDatabase();
rsa = InitServerRSA();
QFile file("server/rsa_pub");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);
public_key = in.readAll();
md5 = calcFileMD5();
server = new ServerSocket();
server->setParent(this);
connect(server, &ServerSocket::new_connection,
@ -30,6 +37,7 @@ Server::~Server()
ServerInstance = nullptr;
m_lobby->deleteLater();
sqlite3_close(db);
RSA_free(rsa);
}
bool Server::listen(const QHostAddress& address, ushort port)
@ -39,12 +47,21 @@ bool Server::listen(const QHostAddress& address, ushort port)
void Server::createRoom(ServerPlayer* owner, const QString &name, int capacity)
{
Room *room = new Room(this);
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
if (room->isLobby())
m_lobby = room;
else
Room *room;
if (!idle_rooms.isEmpty()) {
room = idle_rooms.pop();
room->setId(nextRoomId);
nextRoomId++;
room->setAbandoned(false);
rooms.insert(room->getId(), room);
} else {
room = new Room(this);
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
if (room->isLobby())
m_lobby = room;
else
rooms.insert(room->getId(), room);
}
room->setName(name);
room->setCapacity(capacity);
@ -105,11 +122,11 @@ sqlite3 *Server::getDatabase() {
void Server::processNewConnection(ClientSocket* client)
{
qDebug() << client->peerAddress() << "connected";
qInfo() << client->peerAddress() << "connected";
// version check, file check, ban IP, reconnect, etc
connect(client, &ClientSocket::disconnected, this, [client](){
qDebug() << client->peerAddress() << "disconnected";
qInfo() << client->peerAddress() << "disconnected";
});
// network delay test
@ -117,7 +134,7 @@ void Server::processNewConnection(ClientSocket* client)
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
body << "NetworkDelayTest";
body << "[]";
body << public_key;
client->send(QJsonDocument(body).toJson(QJsonDocument::Compact));
// Note: the client should send a setup string next
connect(client, &ClientSocket::message_got, this, &Server::processRequest);
@ -142,11 +159,11 @@ void Server::processRequest(const QByteArray& msg)
)
valid = false;
else
valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 2);
valid = (QJsonDocument::fromJson(doc[3].toString().toUtf8()).array().size() == 3);
}
if (!valid) {
qDebug() << "Invalid setup string:" << msg;
qWarning() << "Invalid setup string:" << msg;
QJsonArray body;
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
@ -158,6 +175,19 @@ void Server::processRequest(const QByteArray& msg)
}
QJsonArray arr = QJsonDocument::fromJson(doc[3].toString().toUtf8()).array();
if (md5 != arr[2].toString()) {
qWarning() << "MD5 check failed!";
QJsonArray body;
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
body << "ErrorMsg";
body << "MD5 check failed!";
client->send(QJsonDocument(body).toJson(QJsonDocument::Compact));
client->disconnectFromHost();
return;
}
handleNameAndPassword(client, arr[0].toString(), arr[1].toString());
}
@ -165,24 +195,34 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
{
// First check the name and password
// Matches a string that does not contain special characters
QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
QByteArray passwordHash = QCryptographicHash::hash(password.toLatin1(), QCryptographicHash::Sha256).toHex();
static QRegularExpression nameExp("[\\000-\\057\\072-\\100\\133-\\140\\173-\\177]");
auto encryted_pw = QByteArray::fromBase64(password.toLatin1());
unsigned char buf[4096] = {0};
RSA_private_decrypt(RSA_size(rsa), (const unsigned char *)encryted_pw.data(),
buf, rsa, RSA_PKCS1_PADDING);
auto decrypted_pw = QByteArray::fromRawData((const char *)buf, strlen((const char *)buf));
bool passed = false;
QString error_msg;
QJsonObject result;
if (!nameExp.match(name).hasMatch()) {
if (!nameExp.match(name).hasMatch() && !name.isEmpty()) {
// Then we check the database,
QString sql_find = QString("SELECT * FROM userinfo \
WHERE name='%1';").arg(name);
result = SelectFromDatabase(db, sql_find);
QJsonArray arr = result["password"].toArray();
if (arr.isEmpty()) {
auto salt_gen = QRandomGenerator::securelySeeded();
auto salt = QByteArray::number(salt_gen(), 16);
decrypted_pw.append(salt);
auto passwordHash = QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256).toHex();
// not present in database, register
QString sql_reg = QString("INSERT INTO userinfo (name,password,\
avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4',%5);")
QString sql_reg = QString("INSERT INTO userinfo (name,password,salt,\
avatar,lastLoginIp,banned) VALUES ('%1','%2','%3','%4','%5',%6);")
.arg(name)
.arg(QString(passwordHash))
.arg(salt)
.arg("liubei")
.arg(client->peerAddress())
.arg("FALSE");
@ -194,6 +234,9 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
int id = result["id"].toArray()[0].toString().toInt();
if (!players.value(id)) {
// check if password is the same
auto salt = result["salt"].toArray()[0].toString().toLatin1();
decrypted_pw.append(salt);
auto passwordHash = QCryptographicHash::hash(decrypted_pw, QCryptographicHash::Sha256).toHex();
passed = (passwordHash == arr[0].toString());
if (!passed) error_msg = "username or password error";
} else {
@ -226,7 +269,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
lobby()->addPlayer(player);
} else {
qDebug() << client->peerAddress() << "lost connection:" << error_msg;
qInfo() << client->peerAddress() << "lost connection:" << error_msg;
QJsonArray body;
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER | Router::DEST_CLIENT);
@ -244,13 +287,22 @@ void Server::onRoomAbandoned()
room->gameOver();
rooms.remove(room->getId());
updateRoomList();
room->deleteLater();
//room->deleteLater();
if (room->isRunning()) {
room->terminate();
room->wait();
}
idle_rooms.push(room);
#ifdef QT_DEBUG
qDebug() << rooms.size() << "running room(s),"
<< idle_rooms.size() << "idle room(s).";
#endif
}
void Server::onUserDisconnected()
{
ServerPlayer *player = qobject_cast<ServerPlayer *>(sender());
qDebug() << "Player" << player->getId() << "disconnected";
qInfo() << "Player" << player->getId() << "disconnected";
Room *room = player->getRoom();
if (room->isStarted()) {
player->setState(Player::Offline);

View File

@ -42,14 +42,19 @@ public slots:
void onUserStateChanged();
private:
friend class Shell;
ServerSocket *server;
Room *m_lobby;
QMap<int, Room *> rooms;
QStack<Room *> idle_rooms;
int nextRoomId;
friend Room::Room(Server *server);
QHash<int, ServerPlayer *> players;
RSA *rsa;
QString public_key;
sqlite3 *db;
QString md5;
void handleNameAndPassword(ClientSocket *client, const QString &name, const QString &password);
};

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) {
const char *error_msg = lua_tostring(L, -1);
qDebug() << error_msg;
qCritical() << error_msg;
lua_pop(L, 2);
}
lua_pop(L, 1);

View File

@ -46,3 +46,8 @@ static int GetMicroSecond(lua_State *L) {
return 1;
}
%}
void qDebug(const char *msg, ...);
void qInfo(const char *msg, ...);
void qWarning(const char *msg, ...);
void qCritical(const char *msg, ...);

View File

@ -52,7 +52,6 @@ public:
void gameOver();
LuaFunction callback;
LuaFunction startGame;
};
@ -68,33 +67,10 @@ void Room::initLua()
lua_pop(L, 1);
if (error) {
const char *error_msg = lua_tostring(L, -1);
qDebug() << error_msg;
qCritical() << error_msg;
}
}
void Room::callLua(const QString& command, const QString& json_data)
{
Q_ASSERT(callback);
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_replace(L, -2);
lua_rawgeti(L, LUA_REGISTRYINDEX, callback);
SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0);
lua_pushstring(L, command.toUtf8());
lua_pushstring(L, json_data.toUtf8());
int error = lua_pcall(L, 3, 0, -5);
if (error) {
const char *error_msg = lua_tostring(L, -1);
qDebug() << error_msg;
lua_pop(L, 2);
}
lua_pop(L, 1);
}
void Room::roomStart() {
Q_ASSERT(startGame);
@ -109,7 +85,7 @@ void Room::roomStart() {
if (error) {
const char *error_msg = lua_tostring(L, -1);
qDebug() << error_msg;
qCritical() << error_msg;
lua_pop(L, 2);
}
lua_pop(L, 1);

View File

@ -1,6 +1,7 @@
#include "qmlbackend.h"
#include "server.h"
#include "client.h"
#include "util.h"
QmlBackend *Backend;
@ -9,12 +10,14 @@ QmlBackend::QmlBackend(QObject* parent)
{
Backend = this;
engine = nullptr;
rsa = RSA_new();
parser = fkp_new_parser();
}
QmlBackend::~QmlBackend()
{
Backend = nullptr;
RSA_free(rsa);
fkp_close(parser);
}
@ -60,7 +63,7 @@ void QmlBackend::joinServer(QString address)
addr = address;
}
client->connectToHost(QHostAddress(addr), port);
client->connectToHost(addr, port);
}
void QmlBackend::quitLobby()
@ -101,7 +104,7 @@ QString QmlBackend::translate(const QString &src) {
int err = lua_pcall(L, 1, 1, 0);
const char *result = lua_tostring(L, -1);
if (err) {
qDebug() << result;
qCritical() << result;
lua_pop(L, 1);
return "";
}
@ -135,7 +138,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
}
break;
default:
qDebug() << "cannot handle QVariant type" << v.typeId();
qCritical() << "cannot handle QVariant type" << v.typeId();
lua_pushnil(L);
break;
}
@ -154,7 +157,7 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
int err = lua_pcall(L, params.length(), 1, 0);
const char *result = lua_tostring(L, -1);
if (err) {
qDebug() << result;
qCritical() << result;
lua_pop(L, 1);
return "";
}
@ -162,6 +165,46 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
return QString(result);
}
QString QmlBackend::pubEncrypt(const QString &key, const QString &data) {
BIO *keyio = BIO_new_mem_buf(key.toLatin1().data(), -1);
PEM_read_bio_RSAPublicKey(keyio, &rsa, NULL, NULL);
BIO_free_all(keyio);
unsigned char buf[RSA_size(rsa)];
RSA_public_encrypt(data.length(), (const unsigned char *)data.toUtf8().data(),
buf, rsa, RSA_PKCS1_PADDING);
return QByteArray::fromRawData((const char *)buf, RSA_size(rsa)).toBase64();
}
QString QmlBackend::loadConf() {
QFile conf("freekill.client.config.json");
if (!conf.exists()) {
conf.open(QIODevice::WriteOnly);
static const char *init_conf = "{\
\"winWidth\": 960,\
\"winHeight\": 540,\
\"lastLoginServer\": \"127.0.0.1\",\
\"savedPassword\": {\
\"127.0.0.1\": {\
\"username\": \"player\",\
\"password\": \"\",\
\"shorten_password\": \"\"\
}\
}\
}";
conf.write(init_conf);
return init_conf;
}
conf.open(QIODevice::ReadOnly);
return conf.readAll();
}
void QmlBackend::saveConf(const QString &conf) {
QFile c("freekill.client.config.json");
c.open(QIODevice::WriteOnly);
c.write(conf.toUtf8());
}
void QmlBackend::parseFkp(const QString &fileName) {
if (!QFile::exists(fileName)) {
// errorEdit->setText(tr("File does not exist!"));
@ -215,3 +258,8 @@ void QmlBackend::readHashFromParser() {
copyFkpHash2QHash(skills, parser->skills);
copyFkpHash2QHash(marks, parser->marks);
}
QString QmlBackend::calcFileMD5() {
return ::calcFileMD5();
}

View File

@ -2,6 +2,7 @@
#define _QMLBACKEND_H
#include "fkparse.h"
#include <qtmetamacros.h>
class QmlBackend : public QObject {
Q_OBJECT
@ -32,14 +33,21 @@ public:
Q_INVOKABLE QString translate(const QString &src);
Q_INVOKABLE QString callLuaFunction(const QString &func_name,
QVariantList params);
Q_INVOKABLE QString pubEncrypt(const QString &key, const QString &data);
Q_INVOKABLE QString loadConf();
Q_INVOKABLE void saveConf(const QString &conf);
// support fkp
Q_INVOKABLE void parseFkp(const QString &filename);
Q_INVOKABLE QString calcFileMD5();
signals:
void notifyUI(const QString &command, const QString &jsonData);
private:
QQmlApplicationEngine *engine;
RSA *rsa;
fkp_parser *parser;
QHash<QString, QString> generals;
QHash<QString, QString> skills;