mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-15 19:22:25 +08:00
游戏时长统计 (#302)
* 修复和完善qml mark * 修复国战野心家放副将 * [需要编译] 统计游戏时长功能 * 后台也开始记录注册时间和上次上线的时间 * 现在会将屏蔽玩家保存到本地并标红提示
This commit is contained in:
parent
94b7493e2e
commit
278e7ce4c6
|
@ -92,6 +92,7 @@ Flickable {
|
|||
config.blockedUsersChanged();
|
||||
}
|
||||
}
|
||||
|
||||
MetroButton {
|
||||
text: Backend.translate("Kick From Room")
|
||||
visible: !roomScene.isStarted && roomScene.isOwner
|
||||
|
@ -168,11 +169,20 @@ Flickable {
|
|||
const total = gamedata[0];
|
||||
const win = gamedata[1];
|
||||
const run = gamedata[2];
|
||||
const totalTime = gamedata[3];
|
||||
const winRate = (win / total) * 100;
|
||||
const runRate = (run / total) * 100;
|
||||
playerGameData.text = total === 0 ? Backend.translate("Newbie") :
|
||||
Backend.translate("Win=%1 Run=%2 Total=%3").arg(winRate.toFixed(2))
|
||||
.arg(runRate.toFixed(2)).arg(total);
|
||||
|
||||
const h = (totalTime / 3600).toFixed(2);
|
||||
const m = Math.floor(totalTime / 60);
|
||||
if (m < 100) {
|
||||
playerGameData.text += " " + Backend.translate("TotalGameTime: %1 min").arg(m);
|
||||
} else {
|
||||
playerGameData.text += " " + Backend.translate("TotalGameTime: %1 h").arg(h);
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(Backend.callLuaFunction("GetPlayerSkills", [id]));
|
||||
|
|
|
@ -50,6 +50,7 @@ QtObject {
|
|||
property bool observing: false
|
||||
property bool replaying: false
|
||||
property list<string> blockedUsers: []
|
||||
property int totalTime: 0 // FIXME: only for notifying
|
||||
|
||||
onDisabledGeneralsChanged: {
|
||||
disableGeneralSchemes[disableSchemeIdx] = disabledGenerals;
|
||||
|
@ -89,6 +90,7 @@ QtObject {
|
|||
disabledGenerals = conf.disabledGenerals ?? [];
|
||||
disableGeneralSchemes = conf.disableGeneralSchemes ?? [ disabledGenerals ];
|
||||
disableSchemeIdx = conf.disableSchemeIdx ?? 0;
|
||||
blockedUsers = conf.blockedUsers ?? [];
|
||||
}
|
||||
|
||||
function saveConf() {
|
||||
|
@ -117,6 +119,7 @@ QtObject {
|
|||
conf.disabledGenerals = disabledGenerals;
|
||||
conf.disableGeneralSchemes = disableGeneralSchemes;
|
||||
conf.disableSchemeIdx = disableSchemeIdx;
|
||||
conf.blockedUsers = blockedUsers;
|
||||
|
||||
Backend.saveConf(JSON.stringify(conf, undefined, 2));
|
||||
}
|
||||
|
|
|
@ -10,6 +10,37 @@ Item {
|
|||
width: bg.width
|
||||
height: bg.height
|
||||
|
||||
Rectangle {
|
||||
x: 84; y: 31.6
|
||||
height: 20
|
||||
width: childrenRect.width + 48
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.7; color: "#AA3598E8" }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
Text {
|
||||
text: {
|
||||
config.totalTime;
|
||||
const gamedata = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [Self.id]));
|
||||
const totalTime = gamedata[3];
|
||||
const h = (totalTime / 3600).toFixed(2);
|
||||
const m = Math.floor(totalTime / 60);
|
||||
if (m < 100) {
|
||||
return Backend.translate("TotalGameTime: %1 min").arg(m);
|
||||
} else {
|
||||
return Backend.translate("TotalGameTime: %1 h").arg(h);
|
||||
}
|
||||
}
|
||||
x: 12; y: 1
|
||||
font.family: fontLibian.name
|
||||
font.pixelSize: 16
|
||||
color: "white"
|
||||
//style: Text.Outline
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: bg
|
||||
x: -32
|
||||
|
|
|
@ -194,3 +194,7 @@ callbacks["ServerMessage"] = (jsonData) => {
|
|||
|
||||
callbacks["ShowToast"] = (j) => toast.show(j);
|
||||
callbacks["InstallKey"] = (j) => Backend.installAESKey();
|
||||
|
||||
callbacks["AddTotalGameTime"] = (jsonData) => {
|
||||
config.totalTime++;
|
||||
}
|
||||
|
|
|
@ -1276,7 +1276,7 @@ Item {
|
|||
gameData = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [item.id]));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
gameData = [0, 0, 0];
|
||||
gameData = [0, 0, 0, 0];
|
||||
}
|
||||
if (item.id > 0) {
|
||||
datalist.push({
|
||||
|
|
|
@ -75,10 +75,10 @@ Item {
|
|||
if (close_br === -1) return;
|
||||
|
||||
const mark_type = mark_name.slice(2, close_br);
|
||||
const _data = (mark_extra);
|
||||
let data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark_name, JSON.stringify(_data)]));
|
||||
const _data = mark_extra;
|
||||
let data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark_name, _data, root.parent?.playerid]));
|
||||
if (data && data.qml_path) {
|
||||
params.data = _data;
|
||||
params.data = JSON.parse(_data);
|
||||
roomScene.startCheat("../../" + data.qml_path, params);
|
||||
}
|
||||
return;
|
||||
|
@ -122,7 +122,8 @@ Item {
|
|||
const close_br = mark.indexOf(']');
|
||||
if (close_br !== -1) {
|
||||
const mark_type = mark.slice(2, close_br);
|
||||
const _data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark, JSON.stringify(data)]));
|
||||
data = JSON.stringify(data);
|
||||
const _data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark, data, root.parent?.playerid]));
|
||||
if (_data && _data.text) {
|
||||
special_value = _data.text;
|
||||
}
|
||||
|
|
|
@ -227,6 +227,7 @@ Item {
|
|||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 90
|
||||
scale: 1.25
|
||||
z: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -234,6 +235,7 @@ Item {
|
|||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: 0.7
|
||||
z: 2
|
||||
}
|
||||
|
||||
Text {
|
||||
|
|
|
@ -228,7 +228,8 @@ GraphicsBox {
|
|||
}
|
||||
root.choicesChanged();
|
||||
|
||||
fightButton.enabled = (choices.length == choiceNum);
|
||||
fightButton.enabled = (choices.length == choiceNum) &&
|
||||
(needSameKingdom ? isHegPair(selectedItem[0], selectedItem[1]) : true);
|
||||
|
||||
for (i = 0; i < generalCardList.count; i++) {
|
||||
item = generalCardList.itemAt(i);
|
||||
|
|
|
@ -29,14 +29,14 @@ CardItem {
|
|||
suit: ""
|
||||
number: 0
|
||||
footnote: ""
|
||||
card.source: SkinBank.getGeneralPicture(name)
|
||||
card.source: known ? SkinBank.getGeneralPicture(name) : (SkinBank.GENERALCARD_DIR + 'card-back')
|
||||
glow.color: "white" //Engine.kingdomColor[kingdom]
|
||||
|
||||
// FIXME: 藕!!
|
||||
property bool heg: name.startsWith('hs__') || name.startsWith('ld__') || name.includes('heg__')
|
||||
|
||||
Image {
|
||||
source: SkinBank.GENERALCARD_DIR + "border"
|
||||
source: known ? (SkinBank.GENERALCARD_DIR + "border") : ""
|
||||
}
|
||||
|
||||
Image {
|
||||
|
@ -44,7 +44,7 @@ CardItem {
|
|||
width: 34; fillMode: Image.PreserveAspectFit
|
||||
transformOrigin: Item.TopLeft
|
||||
source: SkinBank.getGeneralCardDir(kingdom) + kingdom
|
||||
visible: detailed
|
||||
visible: detailed && known
|
||||
}
|
||||
|
||||
Image {
|
||||
|
@ -52,14 +52,14 @@ CardItem {
|
|||
transformOrigin: Item.TopLeft
|
||||
width: 34; fillMode: Image.PreserveAspectFit
|
||||
source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom : ""
|
||||
visible: detailed
|
||||
visible: detailed && known
|
||||
}
|
||||
|
||||
Row {
|
||||
x: 34
|
||||
y: 4
|
||||
spacing: 1
|
||||
visible: detailed && !heg
|
||||
visible: detailed && known && !heg
|
||||
Repeater {
|
||||
id: hpRepeater
|
||||
model: (!heg) ? ((hp > 5 || hp !== maxHp) ? 1 : hp) : 0
|
||||
|
@ -106,7 +106,7 @@ CardItem {
|
|||
x: 34
|
||||
y: 3
|
||||
spacing: 0
|
||||
visible: detailed && heg
|
||||
visible: detailed && known && heg
|
||||
Repeater {
|
||||
id: hegHpRepeater
|
||||
model: heg ? ((hp > 7 || hp !== maxHp) ? 1 : Math.ceil(hp / 2)) : 0
|
||||
|
@ -140,7 +140,7 @@ CardItem {
|
|||
}
|
||||
|
||||
Shield {
|
||||
visible: shieldNum > 0 && detailed
|
||||
visible: shieldNum > 0 && detailed && known
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: hpRepeater.model > 4 ? 16 : 0
|
||||
|
@ -154,7 +154,7 @@ CardItem {
|
|||
x: 2
|
||||
y: lineCount > 6 ? 30 : 34
|
||||
text: name !== "" ? Backend.translate(name) : "nil"
|
||||
visible: Backend.translate(name).length <= 6 && detailed
|
||||
visible: Backend.translate(name).length <= 6 && detailed && known
|
||||
color: "white"
|
||||
font.family: fontLibian.name
|
||||
font.pixelSize: 18
|
||||
|
@ -169,7 +169,7 @@ CardItem {
|
|||
rotation: 90
|
||||
transformOrigin: Item.BottomLeft
|
||||
text: Backend.translate(name)
|
||||
visible: Backend.translate(name).length > 6 && detailed
|
||||
visible: Backend.translate(name).length > 6 && detailed && known
|
||||
color: "white"
|
||||
font.family: fontLibian.name
|
||||
font.pixelSize: 18
|
||||
|
@ -177,7 +177,7 @@ CardItem {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
visible: pkgName !== "" && detailed
|
||||
visible: pkgName !== "" && detailed && known
|
||||
height: 16
|
||||
width: childrenRect.width + 4
|
||||
anchors.bottom: parent.bottom
|
||||
|
|
|
@ -531,7 +531,7 @@ Item {
|
|||
anchors.topMargin: 2
|
||||
|
||||
font.pixelSize: 16
|
||||
text: screenName
|
||||
text: (config.blockedUsers && config.blockedUsers.includes(screenName) ? Backend.translate("<Blocked> ") : "") + screenName
|
||||
|
||||
glow.radius: 8
|
||||
}
|
||||
|
|
29
docs/misc/calcDailyLogin.sh
Executable file
29
docs/misc/calcDailyLogin.sh
Executable file
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
# 用于统计新月杀日活的脚本,可以写入定时任务。
|
||||
|
||||
# 我自己是把数据库文件在这个目录创了符号链接,总之确保这里存在那个数据库,可以手动cd
|
||||
# cd ~
|
||||
|
||||
SQLITE_CMD="sqlite3 users.db -readonly -list -batch -bail -cmd "
|
||||
SEL_REG="SELECT count() FROM usergameinfo WHERE date(registerTime, 'unixepoch', 'localtime') >= date('now', 'localtime', 'start of day') AND date(registerTime, 'unixepoch', 'localtime') < date('now', 'localtime', 'start of day', '+1 days');"
|
||||
SEL_LOG="SELECT count() FROM usergameinfo WHERE date(lastLoginTime, 'unixepoch', 'localtime') >= date('now', 'localtime', 'start of day') AND date(lastLoginTime, 'unixepoch', 'localtime') < date('now', 'localtime', 'start of day', '+1 days');"
|
||||
|
||||
i=0
|
||||
# 数据库可能被锁定,需要循环
|
||||
false # 令$?为1,不知道怎么写do while循环
|
||||
while [ 0 -ne $? ]; do
|
||||
sleep 0.3
|
||||
i=$[i+1]
|
||||
if [ $i -ge 30 ]; then exit; fi
|
||||
REG_COUNT=$($SQLITE_CMD "$SEL_REG" < /dev/null)
|
||||
done
|
||||
|
||||
false
|
||||
while [ 0 -ne $? ]; do
|
||||
sleep 0.3
|
||||
i=$[i+1]
|
||||
if [ $i -ge 30 ]; then exit; fi
|
||||
LOG_COUNT=$($SQLITE_CMD "$SEL_LOG" < /dev/null)
|
||||
done
|
||||
|
||||
echo "$(date +'%Y-%m-%d'),${REG_COUNT},${LOG_COUNT}" >> loginInfo.csv
|
127
docs/misc/history.txt
Normal file
127
docs/misc/history.txt
Normal file
|
@ -0,0 +1,127 @@
|
|||
新月杀记事本 by Notify
|
||||
|
||||
在协力开发新月杀时(当然了,包括创文件夹之前)我也有在考据神杀的各种历史。总感觉以后新月杀也会自然而然走到需要考据的那一步呢,不如直接把一些琐事记载于此,方便想了解的人直接观看。
|
||||
|
||||
总而言之,开写吧,我还要整合一下从立项至今的各种事情。以后这里估计成月度总结.txt了。
|
||||
|
||||
2022年
|
||||
=======================
|
||||
|
||||
* 1月~2月
|
||||
|
||||
在此之前我主要在制作太阳神三国杀2015版的lua拓展包。在几经重构后终于放弃了神杀,和 Ho-spair(下称惑神) 共商新的Qt三国杀大计。当时提出的就是继承太阳神三国杀的精神,继续开源并使用Qt和Lua开发。当然了都是用最新版,太阳神卡在Qt 5.5了才导致维护困难。
|
||||
|
||||
新的三国杀游戏名曰FreeKill。因为目标是做便于各种拓展的平台,所以一开始就决定把各种拓展包排除到本体仓库之外,游戏只自带标准包。
|
||||
|
||||
* 12月
|
||||
|
||||
在与惑神协力了一年后,新游戏大体框架初步落成。两人分工明确,我做Qt通信、UI以及一些别的底层细节相关,惑神对规则集比较熟悉,做了游戏逻辑的主体。同时解决了Windows和安卓平台的编译问题。这个月内也完成了绝大多数卡牌。为了便于吸引神杀Luaer,拓展格式也是采用非常接近神杀的语法。
|
||||
|
||||
- 五谷丰登妥协成所有人各摸一张。属于是最初的特色(
|
||||
|
||||
当时差不多就是白天做完了之后晚上和惑神开1v1测试。刚好我双11捡了个新客服务器,就拿来搭建联机用了。因为有了安卓版所以联机游玩也方便多了。游戏卡牌都是慢慢开发的,很多卡牌连效果都没有,但是可以用出去(除了少一张手牌外无事发生)。
|
||||
|
||||
2023年
|
||||
=======================
|
||||
|
||||
* 1月~2月
|
||||
|
||||
继续开发,基本完成标准包武将技能、标准包装备卡等。给Linux写了个PKGBUILD,当然估计只有我自己在用。以及新月杀最重要的在线更新功能也是此时做的,这个功能出来之后游戏基本也称之为挺好拓展的了。最后差不多告成,在大约2月中旬时候发布了v0.0.1版本(因为种种原因该版本不在git tag中了)。
|
||||
|
||||
最初的宣传贴发在太阳神三国杀吧 (https://tieba.baidu.com/p/8265967671) 。不知为何(可能因为手机能玩吧)这个几乎啥也没有的版本吸引了一些无名杀玩家,于是有了第一波小引流。
|
||||
|
||||
在不断的催促之下做了一个完全随机的AI系统来糊弄玩家。(为什么都不来联机呢,服务器就在那啊)并在月底踩点最后一天发出v0.0.2版。
|
||||
|
||||
v0.0.1版本使用小米logo生成器做了个橘色的“fk”图标,开屏欢迎图是凰舞九天*关银屏(FreeKill 自由-开放-拓展)
|
||||
|
||||
* 3月
|
||||
|
||||
没人联机的原因也很简单,只有标包谁玩啊!刚好游戏不是有在线更新拓展包功能吗,于是第一个拓展包joy (https://gitee.com/notify-ctrl/joy) 产生。当时游戏虽然本身功能非常不齐全,但是做点简单的武将还是可以的。
|
||||
|
||||
首先加入战场的是:刘焉、留赞、戏志才、界徐盛。这四位打标包属于是难以评价了。过几天后v0.0.3发布,随之惑神在joy中加入了麴义和王基。
|
||||
|
||||
借助joy包和自动更新机制,我进一步在神杀群宣传。终于3月24日RalphR((=゚ω゚)ノ R神!为R神欢呼!)加入了新月杀拓展的开发中。后面的不用多说,R神成为最核心的拓展开发者之一,为FreeKill拓展更多官方武将。
|
||||
|
||||
- 顺便一提,在joy时期,他最先添加的是周宣、朱建平、曹金玉、张嫙。然而当时joy里面的都是手杀和OL的强将,新杀阴间一进来属于是降维打击了,可谓是早早奠定了FreeKill阴间横行的基调呀……
|
||||
|
||||
3月26日nyutanislavsky(nyutan,下称Nyu神。膜Nyu神!)提交了一个海外服的仓库链接。与joy包自由散漫的添加热门武将不同,海外服包专注于海外服。
|
||||
|
||||
因为人太少了,就做了斗地主模式。2月底引来的玩家也基本只是看个热闹,此时的基调就是偶尔联机打打斗地主了,在线峰值3人。除了开发者们外,最活跃的玩家当属星空之遥了。(其实也没多活跃,服务器基本没人玩)
|
||||
|
||||
为了进一步拉拢新人参与拓展开发,FreeKill项目文档启动。(是的就是这个docs文件夹)然而我的文笔实在让R神吐槽看不懂 = =
|
||||
|
||||
* 4月
|
||||
|
||||
超级妖梦厨加入开发中,主要是做自己的mod,但也会来帮忙做一些本体代码。
|
||||
|
||||
vup杀选择了FreeKill来继续发展。这也成为了计划转向FreeKill的第一个神杀mod。正因为是第一个导致我经常干涉拓展开发者他的进度如何。
|
||||
|
||||
这个月基本没啥,联机仍处于日活不足10人的状态,大家也都在慢慢开发新功能、新代码。这个月凑得出军八了,我记得江水澄秋和马蹄糕是很活跃的玩家。
|
||||
|
||||
因为人数不多,这阶段无论是版本更新还是重启服务器都挺随心所欲的。
|
||||
|
||||
* 5月
|
||||
|
||||
还是维持着不温不火的状态直到v0.1.9版本。这期间人少,对局风格偏向于群内局。(阴间全ban,对局算比较平衡,除了国际服李彦和呼除泉这俩阴间我因为不认识忘记ban了)
|
||||
|
||||
为了更加便于吸引新玩家,我们决定给项目取一个中文名字,几经讨论最后确定为新月杀。与此同时还添加了密码房、观战之类的设施,修了很多恶性bug,在R神和Nyu神的爆肝之下武将进度也十分喜人,最终做出了感觉适合引流的v0.2.0版本。
|
||||
|
||||
果不其然,视频发出去之后就吸引了不少路人,服务器达到二三十多、后来乃至上百人在线。然而这也给v0.2.0暴露了一大堆问题:
|
||||
- 房间列表自动刷新导致根本点不到想进入的房间
|
||||
- 进房没有准备机制,人满自动开
|
||||
- 逃跑不惩罚,一桌军八6个逃跑(最开始的话人很少,大家都挺守规矩)
|
||||
- 之前把阴间全ban了,而路人爱玩阴间,导致那些将根本没怎么测试bug巨多
|
||||
- 防误触的长按Quit按钮对于路人而言属于是反人类了(怎么全都在半路Quit啊)
|
||||
|
||||
问题归问题,服务器刚刚满20在线的时候,开发组里面简单像是庆祝一样。后来人越来越多,暴露的问题也使开发组从庆祝转向忙碌的修bug。
|
||||
|
||||
> 辛苦R神了,被各种反馈张嫙的bug改到吐
|
||||
|
||||
ken神(神杀幻天漫杀设计开发者)加入FreeKill并着手制作自己的拓展包。
|
||||
|
||||
v0.2.0的logo是白底上一个黑色的“殺”字,右上角蓝色的“新月”。开屏图片改为一舞倾城*貂蝉(新月杀 自由-开放-拓展)
|
||||
|
||||
* 6月
|
||||
|
||||
因为涌入的无素质玩家太多,一些老玩家气的暂时不玩了。同时0.2.0在人很多的情况下体验特别差,此时急需一个船新版本修复种种问题。
|
||||
|
||||
在几天赶工之下一个舒适很多的0.2.1版本发出来了。进房准备、踢人、ban人啥的都有了,然而并没有逃跑惩罚(但是有逃跑提示),导致逃跑过多的问题暴露的更严重了。此时引流也停止了(视频被拿下),当时图一热闹的玩家也各玩各的去了,服务器在线人数在四五十人左右。
|
||||
|
||||
月初这几天值得一提的玩家就是荀令君了,凭一己之力在群里了发起了“菜鸟玩家配不配玩游戏”大讨论。讨论以我赶工开发出ban人功能并把他ban了告终。
|
||||
|
||||
> 其实我最开始的时候还是想珍惜每个活跃玩家的,荀令君虽然态度跋扈了点但玩的比较多,所以我很多地方偏袒着他。然而他最后开始对R神进行人身攻击了,那我也只好手下不留情了。玩免费游戏还辱骂用爱发电的开发者是绝对无法忍受的。
|
||||
|
||||
不断涌入的玩家也加大了服务器的负担,因此了然提供了服务器用来分担压力,后来发展为私服。
|
||||
|
||||
原本为国战做准备的双将被玩家发现可以在身份局使用。令人绝望的双阴风潮就此展开了。双阴没启动多久就被朱建平+东海龙王的组合毁掉了,但其实没多少人在意。
|
||||
|
||||
在v0.2.3后我也意识到必须优化一下RAM占用了,于是大改了底层逻辑。妖梦厨也发力做了很多新功能。然而由于缺乏测试导致接下来推出的0.2.4版出了毁灭般的巨大bug,上线1小时后不得不回退到0.2.3。受到指责并进一步挂测试服仔仔细细测试之后(笑死根本没几个人测,主服多好玩)推出终于稳定的0.2.5版。
|
||||
|
||||
ken神离开。
|
||||
|
||||
* 7月
|
||||
|
||||
逃跑惩罚终于上线,武将也越来越接近全扩,是时候引一波新人了。有玩家自发的发视频进行了引流。这下引得更大了,服务器峰值在线达到450人左右,加群的玩家就更不必说了。(然而我时不时就说服务器最多塞300人下去)在如此大的人流面前视频被连忙删除,与此同时二群建立,一群很快就满了。
|
||||
|
||||
玩家“闪”达到了三千多局斗地主,成为全服第一个大将军。
|
||||
|
||||
* 8月
|
||||
|
||||
0.3.0上线。
|
||||
|
||||
0.3.0又改了logo和开屏图画,logo是蓝色渐变底和白色“杀”字,开屏图片改为乐蔡文姬。
|
||||
|
||||
* 9月
|
||||
|
||||
* 10月
|
||||
|
||||
* 11月
|
||||
|
||||
* 12月
|
||||
|
||||
时隔一个多月终于更新了0.4.0版本。UI大改,增加更多利好联机的功能。
|
||||
|
||||
R神离开。
|
||||
|
||||
2024年
|
||||
=====================
|
122
docs/misc/qsgs/01-history.txt
Normal file
122
docs/misc/qsgs/01-history.txt
Normal file
|
@ -0,0 +1,122 @@
|
|||
太阳神三国杀的历史整合 by Notify
|
||||
|
||||
参考:git提交记录、萌娘百科、贴吧(地址能贴就贴) 鸣谢:蛇履虫
|
||||
|
||||
太阳神三国杀作为老牌开源三国杀和新月杀的启发项目,截止到新月杀新建文件夹开始(2022),已经有十多年的历史了。在这漫漫时间长河之中,随着……算了不知道写啥,总之关于其发展史的资料似乎很有欠缺,只能从各种只言片语中搜寻到少数。或许新月杀也会有这么被遗忘的一天吧,故创建此文件整合一下太阳神三国杀的历史,以及记载新月杀的发展史。
|
||||
|
||||
纵观神杀十来年的历史,大致可以分为以下几个阶段:初代(v1)、v2、v2国战、v3。本文姑且尝试按照编年(尽量精确到月份)来对这些事情进行一些梳理。
|
||||
|
||||
v1篇
|
||||
======================
|
||||
|
||||
* 2010年
|
||||
|
||||
原作者Moligaloo(亦称太阳神上,下称神上)于6月13日启动了QSanguosha项目。9月27日,神上在三国杀吧发布了“三国杀Q”的首个版本。并同时公布了github上的源码地址。在这一版中,已经实现了全部的标准版武将、部分风包武将和火包武将、军争扩展。震惊三国杀界。然而美中不足的是无法单机游戏(没有AI),只能和其他小伙伴联机对战。
|
||||
|
||||
2010年10月1日,为了和英文名QSanguosha对应,名称改为“Q三国杀”。新增四张“屎”牌,成为神杀的经典之作。
|
||||
|
||||
2010年10月4日,Q三国杀改名为太阳神三国杀,英文名保持不变,建立了官方QQ群,招贤纳士。同时hypercross等大拿开始参与源码维护。
|
||||
|
||||
2010年11月3日,太阳神三国杀新增了剧情模式:官渡之战,以及当时开始流行的双将模式。新增“欢乐”卡牌包,将“屎”独立出来。
|
||||
|
||||
2010年11月14日,新增林包武将和剩余火包武将。此时风林火仅剩风包的坑爹三将()未完成。
|
||||
|
||||
2010年12月9日,因为新美工苦力的加盟,神杀迎来了全新的界面,同时作者对系统功能和部分结算流程进行了优化。新增武将小乔(测试版)和全部神武将。
|
||||
|
||||
2010年12月26日,圣诞版发布。这是神杀首个正式命名的版本。主要更新:创建了神杀特色DIY武将包“倚天”,收录一些作者喜欢的设计和玩家投稿的特色设计。本次新增两将:初版神曹操魏武帝和张儁乂(倚天包武将用姓+字来避免和官方重复)。同时还有测试版杨修(和风包坑爹三将齐名的坑爹将)。
|
||||
|
||||
* 2011年
|
||||
|
||||
2011年1月31日,贺岁版发布。因为AI师donle等人的加盟,本版首次实现了单机AI,玩家终于可以独自虐电脑了群里求联机的也开始减少了。倚天包新增贺岁两将:曹冲和夏侯涓。欢乐包新增四大天灾牌(各种效果的延时锦囊)。为了接下来的比赛,推出竞赛模式。然而就用了一次
|
||||
|
||||
2011年3月8日,红颜版发布。新增武将周泰、倚天包新增红颜三将:(待补充)。神杀举办第一次大型活动也是目前唯一一次“红颜百合赛”,参赛选手联机对战“红颜模式”。第一名奖励30QB第一名获得者为宇文天启。
|
||||
|
||||
2011年3月12日,植树节补丁发布。
|
||||
|
||||
2011年4月5日,清明版发布。新增武将SP公孙瓒、SP袁术、一将成名,倚天包新增清明三将:(待补充)。卡牌新增带技能坐骑猴子一只。
|
||||
|
||||
2011年5月8日,春晖版发布。
|
||||
|
||||
2011年6月6日,端午版发布。
|
||||
|
||||
2011年7月18日,归心版发布。
|
||||
|
||||
2011年8月6日,鹊桥版发布。
|
||||
|
||||
2011年9月12日,中秋版发布。
|
||||
|
||||
2011年11月13日,赤壁版发布。新增武将包“智包”(神杀爱好者共同设计、严格测试后发布的第一个武将包之前的倚天包武将那都是走关系出的)
|
||||
|
||||
* 2012年
|
||||
|
||||
2012年1月22日,除夕版发布。
|
||||
|
||||
2012年4月5日,踏青版发布。
|
||||
|
||||
2012年7月15日,涅槃版发布。因新程序员加盟导致此版架构发生重大变动,开发组内部争议颇大,原作者调停无果停止更新。
|
||||
|
||||
* 2013年
|
||||
|
||||
2013年1月12日,雪霁版发布。代码架构衔接踏青版,由宇文天启接手开发,天子会工作室负责配音工作,将大批配音更换为独家版(而不是照搬三国杀OL)。但因为开发仓促,新开发者对架构不熟等原因,出现了很多问题和bug,以至于接下来一段时间几乎每天都在更新修复补丁。
|
||||
|
||||
2013年2月8日,金蛇版发布。更新了三国杀新出的武将和模式,修复了上一版的绝大部分bug,程序开始趋于稳定。
|
||||
|
||||
2013年7月1日,鬼隐系列从其之一更到其之六。鬼隐谐音归隐,预示太阳神三国杀停止更新。
|
||||
|
||||
v2篇
|
||||
======================
|
||||
|
||||
神杀于2012年分裂为两派开发团队。后被称作V1和V2,他们之间的分歧主要在于创作理念和发展方向上。原创派(V1)倾向于将神杀逐步打造成一个脱离游卡三国杀的原创游戏,也就是不再和三国杀官方推出的氪金将、坑爹设计妥协,并发展自己的原创武将、原创游戏模式等。官方派(V2,最初称为新神杀)则正好相反,他们倾向于将包括界面、武将、游戏风格全部朝着官方(三国杀OL)靠拢,打造成一个高仿的无氪金版的三国杀游戏。
|
||||
|
||||
当然结果也很明显,原创派虽然愿望很美好,但操作难度大,人员流失严重,不久就死了。而官方派因为素材获取容易,制作相对简单,玩家更容易吸引,所以只要三国杀不死,则新神杀就不死。时代证明了这条路的正确性。
|
||||
|
||||
神杀两派之间没有什么深仇大恨,而且随着时光的流逝,他们最开始起争端的那些人都陆续结婚生子,淡出业界了。留下的,还有一些依然在努力为神杀续命的人。
|
||||
|
||||
从此神杀身份版的主线就是v2了,之后也不断有老人淡出、新人加入,直到今天仍保持着活力。
|
||||
|
||||
程序上而言,v2是基于v1的Fork版本,二者朝着不同的方向发展。
|
||||
|
||||
* 2012~2015
|
||||
|
||||
Mogara进行开发维护。于20150926版停止更新。
|
||||
|
||||
* 2015~2018
|
||||
|
||||
2015之后就是由各路网友自发维护了,难以尽述。
|
||||
|
||||
这几年主要由youko1316(亦称ZY)(膜拜)基于Lua拓展功能为神杀陆续添加新武将,代表性的就是extra.lua。
|
||||
|
||||
ZY: https://tieba.baidu.com/p/4186274993
|
||||
Ho-spair: https://tieba.baidu.com/p/5991537372
|
||||
|
||||
* 2019~2020
|
||||
|
||||
ZY在2020年6月1日更新了最后一版(熊孩子版),从此遁入无尽的鸽中~
|
||||
|
||||
zeroOna的更新: https://tieba.baidu.com/p/6256073021
|
||||
还记得一位C开头的大神更了个几千楼,但找不到了
|
||||
|
||||
* 2020~2022
|
||||
|
||||
继ZY之后由叫什么啊你妹(妹神!)重新基于v2最终版(即20150926版)的源码,使用C++语言在源码层面重新增加新武将。
|
||||
|
||||
妹神: https://tieba.baidu.com/p/7004744263
|
||||
|
||||
* 2023
|
||||
|
||||
|
||||
|
||||
v2国战篇
|
||||
======================
|
||||
|
||||
https://tieba.baidu.com/p/3166370825
|
||||
|
||||
v3
|
||||
======================
|
||||
|
||||
积弊已久的神杀v2终于在2015年被开发组宣布停止更新,但Mogara同时也提出了新的神杀v3开发企划,让玩家继续充满期待。
|
||||
|
||||
当初的通告贴: https://tieba.baidu.com/p/4041729081
|
||||
我的考据贴: https://tieba.baidu.com/p/8620367665
|
||||
|
||||
最终于2016年不了了之。
|
23
docs/misc/server_admin.md
Normal file
23
docs/misc/server_admin.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
服主小技巧
|
||||
=====================
|
||||
|
||||
本文件夹下的 calcDailyLogin.sh 是统计日活的脚本,可以将其设为定时任务。
|
||||
|
||||
注意时不时备份数据库。数据库只是单个sqlite文件而已,直接cp即可。
|
||||
|
||||
使用sqlite命令行之前可以使用.mode markdown命令将sqlite输出设为markdown格式,方便复制粘贴到配置中。
|
||||
|
||||
常用sql语句
|
||||
-----------------
|
||||
|
||||
```sql
|
||||
-- 统计某个模式胜率前20名的玩家
|
||||
SELECT * FROM playerWinRate WHERE mode="m_1v2_mode" AND total > 400 ORDER BY winRate DESC LIMIT 20;
|
||||
|
||||
-- 统计某个模式胜率前20名的武将
|
||||
SELECT * FROM generalWinRate WHERE mode="m_1v2_mode" AND total > 400 ORDER BY winRate DESC LIMIT 20;
|
||||
|
||||
-- 统计游玩时长排行
|
||||
SELECT usergameinfo.id, totalGameTime AS 'Time (sec)', round(totalGameTime/3600.0, 2)||" h" AS ' ', name AS Name FROM usergameinfo, userinfo WHERE userinfo.id = usergameinfo.id GROUP BY usergameinfo.id ORDER BY totalGameTime DESC LIMIT 10;
|
||||
```
|
||||
|
BIN
image/card/general/card-back.png
Normal file
BIN
image/card/general/card-back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
|
@ -286,8 +286,9 @@ fk.client_callback["AddPlayer"] = function(jsonData)
|
|||
-- jsonData: [ int id, string screenName, string avatar ]
|
||||
-- when other player enter the room, we create clientplayer(C and lua) for them
|
||||
local data = json.decode(jsonData)
|
||||
local id, name, avatar = data[1], data[2], data[3]
|
||||
local id, name, avatar, time = data[1], data[2], data[3], data[5]
|
||||
local player = fk.ClientInstance:addPlayer(id, name, avatar)
|
||||
player:addTotalGameTime(time)
|
||||
local p = ClientPlayer:new(player)
|
||||
table.insert(ClientInstance.players, p)
|
||||
table.insert(ClientInstance.alive_players, p)
|
||||
|
@ -920,6 +921,18 @@ fk.client_callback["UpdateGameData"] = function(jsonData)
|
|||
ClientInstance:notifyUI("UpdateGameData", jsonData)
|
||||
end
|
||||
|
||||
fk.client_callback["AddTotalGameTime"] = function(jsonData)
|
||||
local data = json.decode(jsonData)
|
||||
local player, time = data[1], data[2]
|
||||
player = ClientInstance:getPlayerById(player)
|
||||
if player then
|
||||
player.player:addTotalGameTime(time)
|
||||
if player == Self then
|
||||
ClientInstance:notifyUI("AddTotalGameTime", jsonData)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
fk.client_callback["StartGame"] = function(jsonData)
|
||||
local c = ClientInstance
|
||||
c.record = {
|
||||
|
@ -946,6 +959,7 @@ fk.client_callback["StartGame"] = function(jsonData)
|
|||
p.player:getScreenName(),
|
||||
p.player:getAvatar(),
|
||||
true,
|
||||
p.player:getTotalGameTime(),
|
||||
},
|
||||
})
|
||||
end
|
||||
|
|
|
@ -650,19 +650,21 @@ end
|
|||
function GetPlayerGameData(pid)
|
||||
local c = ClientInstance
|
||||
local p = c:getPlayerById(pid)
|
||||
if not p then return "[0, 0, 0]" end
|
||||
if not p then return "[0, 0, 0, 0]" end
|
||||
local raw = p.player:getGameData()
|
||||
local ret = {}
|
||||
for _, i in fk.qlist(raw) do
|
||||
table.insert(ret, i)
|
||||
end
|
||||
table.insert(ret, p.player:getTotalGameTime())
|
||||
return json.encode(ret)
|
||||
end
|
||||
|
||||
function SetPlayerGameData(pid, data)
|
||||
local c = ClientInstance
|
||||
local p = c:getPlayerById(pid)
|
||||
p.player:setGameData(table.unpack(data))
|
||||
local total, win, run = table.unpack(data)
|
||||
p.player:setGameData(total, win, run)
|
||||
table.insert(data, 1, pid)
|
||||
ClientInstance:notifyUI("UpdateGameData", json.encode(data))
|
||||
end
|
||||
|
@ -735,13 +737,14 @@ function PoxiFeasible(poxi_type, selected, data, extra_data)
|
|||
return json.encode(poxi.feasible(selected, data, extra_data))
|
||||
end
|
||||
|
||||
function GetQmlMark(mtype, name, value)
|
||||
function GetQmlMark(mtype, name, value, p)
|
||||
local spec = Fk.qml_marks[mtype]
|
||||
if not spec then return "{}" end
|
||||
p = ClientInstance:getPlayerById(p)
|
||||
value = json.decode(value)
|
||||
return json.encode {
|
||||
qml_path = spec.qml_path,
|
||||
text = spec.how_to_show(name, value)
|
||||
qml_path = type(spec.qml_path) == "function" and spec.qml_path(name, value, p) or spec.qml_path,
|
||||
text = spec.how_to_show(name, value, p)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -68,6 +68,8 @@ Fk:loadTranslationTable({
|
|||
--["Newbie"] = "新手保护ing",
|
||||
["Win=%1 Run=%2 Total=%3"] = "Win=%1% Run=%2% Total=%3",
|
||||
["Win=%1\nRun=%2\nTotal=%3"] = "Win: %1%\nRun: %2%\nTotal: %3",
|
||||
["TotalGameTime: %1 min"] = "Played: %1 minutes",
|
||||
["TotalGameTime: %1 h"] = "Played: %1 hours",
|
||||
|
||||
["Ban List"] = "Ban character scheme",
|
||||
["List"] = "Scheme",
|
||||
|
|
|
@ -65,10 +65,13 @@ Fk:loadTranslationTable{
|
|||
["Give Shoe"] = "拖鞋",
|
||||
["Block Chatter"] = "屏蔽发言",
|
||||
["Unblock Chatter"] = "解除屏蔽",
|
||||
["<Blocked> "] = '<font color="red">[已屏蔽]</font> ',
|
||||
["Kick From Room"] = "踢出房间",
|
||||
["Newbie"] = "新手保护ing",
|
||||
["Win=%1 Run=%2 Total=%3"] = "胜率%1% 逃率%2% 总场次%3",
|
||||
["Win=%1\nRun=%2\nTotal=%3"] = "胜率: %1%\n逃率: %2%\n总场次: %3",
|
||||
["TotalGameTime: %1 min"] = "已游玩: %1 分钟",
|
||||
["TotalGameTime: %1 h"] = "已游玩: %1 小时",
|
||||
|
||||
["Ban List"] = "禁将方案",
|
||||
["List"] = "方案",
|
||||
|
|
|
@ -942,6 +942,8 @@ end
|
|||
---@param ignoreFromKong? boolean
|
||||
---@param ignoreToKong? boolean
|
||||
function Player:canPindian(to, ignoreFromKong, ignoreToKong)
|
||||
if self == to then return false end
|
||||
|
||||
if self:isKongcheng() and not ignoreFromKong then
|
||||
return false
|
||||
end
|
||||
|
|
|
@ -606,5 +606,5 @@ end
|
|||
|
||||
---@class QmlMarkSpec
|
||||
---@field name string
|
||||
---@field qml_path string
|
||||
---@field how_to_show function(name: string, value?: any): string?
|
||||
---@field qml_path string | fun(name: string, value?: any, player?: Player): string
|
||||
---@field how_to_show fun(name: string, value?: any, player?: Player): string?
|
||||
|
|
|
@ -18,6 +18,8 @@ local function tellRoomToObserver(self, player)
|
|||
p.id,
|
||||
p._splayer:getScreenName(),
|
||||
p._splayer:getAvatar(),
|
||||
false,
|
||||
p._splayer:getTotalGameTime(),
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -321,11 +321,6 @@ function ServerPlayer:reconnect()
|
|||
local room = self.room
|
||||
self.serverplayer:setState(fk.Player_Online)
|
||||
|
||||
self:doNotify("Setup", json.encode{
|
||||
self.id,
|
||||
self._splayer:getScreenName(),
|
||||
self._splayer:getAvatar(),
|
||||
})
|
||||
self:doNotify("EnterLobby", "")
|
||||
self:doNotify("EnterRoom", json.encode{
|
||||
#room.players, room.timeout, room.settings,
|
||||
|
@ -339,6 +334,8 @@ function ServerPlayer:reconnect()
|
|||
p.id,
|
||||
p._splayer:getScreenName(),
|
||||
p._splayer:getAvatar(),
|
||||
false,
|
||||
p._splayer:getTotalGameTime(),
|
||||
})
|
||||
end
|
||||
self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() })
|
||||
|
|
|
@ -25,6 +25,13 @@ CREATE TABLE IF NOT EXISTS banuuid (
|
|||
uuid VARCHAR(32)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usergameinfo (
|
||||
id INTEGER PRIMARY KEY,
|
||||
registerTime INTEGER, -- 时间戳
|
||||
lastLoginTime INTEGER, -- 时间戳
|
||||
totalGameTime INTEGER -- 单位:秒
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS friendinfo (
|
||||
id1 INTEGER,
|
||||
id2 INTEGER,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
#include "player.h"
|
||||
|
||||
Player::Player(QObject *parent)
|
||||
: QObject(parent), id(0), state(Player::Invalid), ready(false),
|
||||
: QObject(parent), id(0), state(Player::Invalid), totalGameTime(0), ready(false),
|
||||
totalGames(0), winCount(0), runCount(0) {}
|
||||
|
||||
Player::~Player() {}
|
||||
|
@ -26,6 +26,12 @@ void Player::setAvatar(const QString &avatar) {
|
|||
emit avatarChanged();
|
||||
}
|
||||
|
||||
int Player::getTotalGameTime() const { return totalGameTime; }
|
||||
|
||||
void Player::addTotalGameTime(int toAdd) {
|
||||
totalGameTime += toAdd;
|
||||
}
|
||||
|
||||
Player::State Player::getState() const { return state; }
|
||||
|
||||
QString Player::getStateString() const {
|
||||
|
|
|
@ -31,6 +31,9 @@ public:
|
|||
QString getAvatar() const;
|
||||
void setAvatar(const QString &avatar);
|
||||
|
||||
int getTotalGameTime() const;
|
||||
void addTotalGameTime(int toAdd);
|
||||
|
||||
State getState() const;
|
||||
QString getStateString() const;
|
||||
void setState(State state);
|
||||
|
@ -58,6 +61,7 @@ private:
|
|||
int id;
|
||||
QString screenName; // screenName should not be same.
|
||||
QString avatar;
|
||||
int totalGameTime;
|
||||
State state;
|
||||
bool ready;
|
||||
bool died;
|
||||
|
|
|
@ -151,6 +151,7 @@ void Room::addPlayer(ServerPlayer *player) {
|
|||
jsonData << player->getScreenName();
|
||||
jsonData << player->getAvatar();
|
||||
jsonData << player->isReady();
|
||||
jsonData << player->getTotalGameTime();
|
||||
doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData));
|
||||
}
|
||||
|
||||
|
@ -179,6 +180,7 @@ void Room::addPlayer(ServerPlayer *player) {
|
|||
jsonData << p->getScreenName();
|
||||
jsonData << p->getAvatar();
|
||||
jsonData << p->isReady();
|
||||
jsonData << p->getTotalGameTime();
|
||||
player->doNotify("AddPlayer", JsonArray2Bytes(jsonData));
|
||||
|
||||
jsonData = QJsonArray();
|
||||
|
@ -274,6 +276,7 @@ void Room::removePlayer(ServerPlayer *player) {
|
|||
runner->setId(player->getId());
|
||||
auto gamedata = player->getGameData();
|
||||
runner->setGameData(gamedata[0], gamedata[1], gamedata[2]);
|
||||
runner->addTotalGameTime(player->getTotalGameTime());
|
||||
|
||||
// 最后向服务器玩家列表中增加这个人
|
||||
// 原先的跑路机器人会在游戏结束后自动销毁掉
|
||||
|
@ -543,15 +546,35 @@ void Room::updatePlayerGameData(int id, const QString &mode) {
|
|||
}
|
||||
|
||||
void Room::gameOver() {
|
||||
if (!gameStarted) return;
|
||||
gameStarted = false;
|
||||
runned_players.clear();
|
||||
// 清理所有状态不是“在线”的玩家
|
||||
// 清理所有状态不是“在线”的玩家,增加逃率、游戏时长
|
||||
auto settings = QJsonDocument::fromJson(this->settings);
|
||||
auto mode = settings["gameMode"].toString();
|
||||
foreach (ServerPlayer *p, players) {
|
||||
auto pid = p->getId();
|
||||
|
||||
if (pid > 0) {
|
||||
int time = p->getGameTime();
|
||||
auto bytes = JsonArray2Bytes({ pid, time });
|
||||
doBroadcastNotify(getOtherPlayers(p), "AddTotalGameTime", bytes);
|
||||
|
||||
// 考虑到阵亡已离开啥的,时间得给真实玩家增加
|
||||
auto realPlayer = server->findPlayer(pid);
|
||||
if (realPlayer) {
|
||||
realPlayer->addTotalGameTime(time);
|
||||
realPlayer->doNotify("AddTotalGameTime", bytes);
|
||||
}
|
||||
|
||||
// 摸了,这么写总之不会有问题
|
||||
auto info_update = QString("UPDATE usergameinfo SET totalGameTime = "
|
||||
"IIF(totalGameTime IS NULL, %2, totalGameTime + %2) WHERE id = %1;").arg(pid).arg(time);
|
||||
ExecSQL(server->getDatabase(), info_update);
|
||||
}
|
||||
|
||||
if (p->getState() != Player::Online) {
|
||||
if (p->getState() == Player::Offline) {
|
||||
auto pid = p->getId();
|
||||
addRunRate(pid, mode);
|
||||
// addRunRate(pid, mode);
|
||||
server->temporarilyBan(pid);
|
||||
|
@ -572,6 +595,7 @@ void Room::manuallyStart() {
|
|||
foreach (auto p, players) {
|
||||
p->setReady(false);
|
||||
p->setDied(false);
|
||||
p->startGameTimer();
|
||||
}
|
||||
gameStarted = true;
|
||||
m_thread->pushRequest(QString("-1,%1,newroom").arg(QString::number(id)));
|
||||
|
|
|
@ -212,9 +212,59 @@ void Server::broadcast(const QString &command, const QString &jsonData) {
|
|||
}
|
||||
}
|
||||
|
||||
void Server::sendEarlyPacket(ClientSocket *client, const QString &type, const QString &msg) {
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << type;
|
||||
body << msg;
|
||||
client->send(JsonArray2Bytes(body));
|
||||
}
|
||||
|
||||
bool Server::checkClientVersion(ClientSocket *client, const QString &cver) {
|
||||
auto client_ver = QVersionNumber::fromString(cver);
|
||||
auto ver = QVersionNumber::fromString(FK_VERSION);
|
||||
int cmp = QVersionNumber::compare(ver, client_ver);
|
||||
if (cmp != 0) {
|
||||
auto errmsg = QString();
|
||||
if (cmp < 0) {
|
||||
errmsg = QString("[\"server is still on version %%2\",\"%1\"]")
|
||||
.arg(FK_VERSION, "1");
|
||||
} else {
|
||||
errmsg = QString("[\"server is using version %%2, please update\",\"%1\"]")
|
||||
.arg(FK_VERSION, "1");
|
||||
}
|
||||
|
||||
sendEarlyPacket(client, "ErrorMsg", errmsg);
|
||||
client->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server::setupPlayer(ServerPlayer *player, bool all_info) {
|
||||
// tell the lobby player's basic property
|
||||
QJsonArray arr;
|
||||
arr << player->getId();
|
||||
arr << player->getScreenName();
|
||||
arr << player->getAvatar();
|
||||
player->doNotify("Setup", JsonArray2Bytes(arr));
|
||||
|
||||
if (all_info) {
|
||||
player->doNotify("SetServerSettings", JsonArray2Bytes({
|
||||
getConfig("motd"),
|
||||
getConfig("hiddenPacks"),
|
||||
getConfig("enableBots"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void Server::processNewConnection(ClientSocket *client) {
|
||||
auto addr = client->peerAddress();
|
||||
qInfo() << addr << "connected";
|
||||
|
||||
// check ban ip
|
||||
auto result = SelectFromDatabase(
|
||||
db, QString("SELECT * FROM banip WHERE ip='%1';").arg(addr));
|
||||
|
||||
|
@ -229,13 +279,7 @@ void Server::processNewConnection(ClientSocket *client) {
|
|||
}
|
||||
|
||||
if (!errmsg.isEmpty()) {
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body << errmsg;
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "ErrorMsg", errmsg);
|
||||
qInfo() << "Refused banned IP:" << addr;
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
|
@ -245,13 +289,7 @@ void Server::processNewConnection(ClientSocket *client) {
|
|||
[client]() { qInfo() << client->peerAddress() << "disconnected"; });
|
||||
|
||||
// network delay test
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "NetworkDelayTest";
|
||||
body << public_key;
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "NetworkDelayTest", public_key);
|
||||
// Note: the client should send a setup string next
|
||||
connect(client, &ClientSocket::message_got, this, &Server::processRequest);
|
||||
client->timerSignup.start(30000);
|
||||
|
@ -278,40 +316,14 @@ void Server::processRequest(const QByteArray &msg) {
|
|||
|
||||
if (!valid) {
|
||||
qWarning() << "Invalid setup string:" << msg;
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body << "INVALID SETUP STRING";
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "ErrorMsg", "INVALID SETUP STRING");
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray arr = String2Json(doc[3].toString()).array();
|
||||
|
||||
auto client_ver = QVersionNumber::fromString(arr[3].toString());
|
||||
auto ver = QVersionNumber::fromString(FK_VERSION);
|
||||
int cmp = QVersionNumber::compare(ver, client_ver);
|
||||
if (cmp != 0) {
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body
|
||||
<< (cmp < 0
|
||||
? QString("[\"server is still on version %%2\",\"%1\"]")
|
||||
.arg(FK_VERSION, "1")
|
||||
: QString(
|
||||
"[\"server is using version %%2, please update\",\"%1\"]")
|
||||
.arg(FK_VERSION, "1"));
|
||||
|
||||
client->send(JsonArray2Bytes(body));
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
if (!checkClientVersion(client, arr[3].toString())) return;
|
||||
|
||||
auto uuid = arr[4].toString();
|
||||
auto result2 = QJsonArray({1});
|
||||
|
@ -321,13 +333,7 @@ void Server::processRequest(const QByteArray &msg) {
|
|||
}
|
||||
|
||||
if (!result2.isEmpty()) {
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body << "you have been banned!";
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "ErrorMsg", "you have been banned!");
|
||||
qInfo() << "Refused banned UUID:" << uuid;
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
|
@ -352,13 +358,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
auto aes_bytes = decrypted_pw.first(32);
|
||||
|
||||
// tell client to install aes key
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "InstallKey";
|
||||
body << "";
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "InstallKey", "");
|
||||
|
||||
client->installAESKey(aes_bytes);
|
||||
decrypted_pw.remove(0, 32);
|
||||
|
@ -367,20 +367,8 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
}
|
||||
|
||||
if (md5 != md5_str) {
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body << "MD5 check failed!";
|
||||
client->send(JsonArray2Bytes(body));
|
||||
|
||||
body.removeLast();
|
||||
body.removeLast();
|
||||
body << "UpdatePackage";
|
||||
body << Pacman->getPackSummary();
|
||||
client->send(JsonArray2Bytes(body));
|
||||
|
||||
sendEarlyPacket(client, "ErrorMsg", "MD5 check failed!");
|
||||
sendEarlyPacket(client, "UpdatePackage", Pacman->getPackSummary());
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
|
@ -415,6 +403,10 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
ExecSQL(db, sql_reg);
|
||||
result = SelectFromDatabase(db, sql_find); // refresh result
|
||||
obj = result[0].toObject();
|
||||
|
||||
auto info_update = QString("INSERT INTO usergameinfo (id, registerTime) VALUES (%1, %2);").arg(obj["id"].toString().toInt()).arg(QDateTime::currentSecsSinceEpoch());
|
||||
ExecSQL(db, info_update);
|
||||
|
||||
passed = true;
|
||||
} else {
|
||||
obj = result[0].toObject();
|
||||
|
@ -454,11 +446,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
}
|
||||
|
||||
if (room && !room->isLobby()) {
|
||||
player->doNotify("SetServerSettings", JsonArray2Bytes({
|
||||
getConfig("motd"),
|
||||
getConfig("hiddenPacks"),
|
||||
getConfig("enableBots"),
|
||||
}));
|
||||
setupPlayer(player, true);
|
||||
room->pushRequest(QString("%1,reconnect").arg(id));
|
||||
} else {
|
||||
// 懒得处理掉线玩家在大厅了!踢掉得了
|
||||
|
@ -479,15 +467,23 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
|
||||
if (passed) {
|
||||
// update lastLoginIp
|
||||
int id = obj["id"].toString().toInt();
|
||||
beginTransaction();
|
||||
auto sql_update =
|
||||
QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;")
|
||||
.arg(client->peerAddress())
|
||||
.arg(obj["id"].toString().toInt());
|
||||
.arg(id);
|
||||
ExecSQL(db, sql_update);
|
||||
|
||||
auto uuid_update = QString("REPLACE INTO uuidinfo (id, uuid) VALUES (%1, '%2');").arg(obj["id"].toString().toInt()).arg(uuid_str);
|
||||
auto uuid_update = QString("REPLACE INTO uuidinfo (id, uuid) VALUES (%1, '%2');").arg(id).arg(uuid_str);
|
||||
ExecSQL(db, uuid_update);
|
||||
|
||||
// 来晚了,有很大可能存在已经注册但是表里面没数据的人
|
||||
ExecSQL(db, QString("INSERT OR IGNORE INTO usergameinfo (id) VALUES (%1);").arg(id));
|
||||
auto info_update = QString("UPDATE usergameinfo SET lastLoginTime=%2 where id=%1;").arg(id).arg(QDateTime::currentSecsSinceEpoch());
|
||||
ExecSQL(db, info_update);
|
||||
endTransaction();
|
||||
|
||||
// create new ServerPlayer and setup
|
||||
ServerPlayer *player = new ServerPlayer(lobby());
|
||||
player->setSocket(client);
|
||||
|
@ -497,35 +493,23 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
|
|||
connect(player, &Player::stateChanged, this, &Server::onUserStateChanged);
|
||||
player->setScreenName(name);
|
||||
player->setAvatar(obj["avatar"].toString());
|
||||
player->setId(obj["id"].toString().toInt());
|
||||
player->setId(id);
|
||||
if (players.count() <= 10) {
|
||||
broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName()));
|
||||
}
|
||||
players.insert(player->getId(), player);
|
||||
|
||||
// tell the lobby player's basic property
|
||||
QJsonArray arr;
|
||||
arr << player->getId();
|
||||
arr << player->getScreenName();
|
||||
arr << player->getAvatar();
|
||||
player->doNotify("Setup", JsonArray2Bytes(arr));
|
||||
setupPlayer(player);
|
||||
|
||||
player->doNotify("SetServerSettings", JsonArray2Bytes({
|
||||
getConfig("motd"),
|
||||
getConfig("hiddenPacks"),
|
||||
getConfig("enableBots"),
|
||||
}));
|
||||
auto result = SelectFromDatabase(db, QString("SELECT totalGameTime FROM usergameinfo WHERE id=%1;").arg(id));
|
||||
auto time = result[0].toObject()["totalGameTime"].toString().toInt();
|
||||
player->addTotalGameTime(time);
|
||||
player->doNotify("AddTotalGameTime", JsonArray2Bytes({ id, time }));
|
||||
|
||||
lobby()->addPlayer(player);
|
||||
} else {
|
||||
qInfo() << client->peerAddress() << "lost connection:" << error_msg;
|
||||
QJsonArray body;
|
||||
body << -2;
|
||||
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
|
||||
Router::DEST_CLIENT);
|
||||
body << "ErrorMsg";
|
||||
body << error_msg;
|
||||
client->send(JsonArray2Bytes(body));
|
||||
sendEarlyPacket(client, "ErrorMsg", error_msg);
|
||||
client->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
|
@ -572,8 +556,15 @@ void Server::onUserStateChanged() {
|
|||
if (!room || room->isLobby() || room->isAbandoned()) {
|
||||
return;
|
||||
}
|
||||
auto state = player->getState();
|
||||
room->doBroadcastNotify(room->getPlayers(), "NetStateChanged",
|
||||
QString("[%1,\"%2\"]").arg(player->getId()).arg(player->getStateString()));
|
||||
|
||||
if (state == Player::Online) {
|
||||
player->resumeGameTimer();
|
||||
} else {
|
||||
player->pauseGameTimer();
|
||||
}
|
||||
}
|
||||
|
||||
RSA *Server::initServerRSA() {
|
||||
|
|
|
@ -67,7 +67,7 @@ public slots:
|
|||
private:
|
||||
friend class Shell;
|
||||
ServerSocket *server;
|
||||
QUdpSocket *udpSocket;
|
||||
QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用
|
||||
|
||||
Room *m_lobby;
|
||||
QMap<int, Room *> rooms;
|
||||
|
@ -89,6 +89,12 @@ private:
|
|||
QJsonObject config;
|
||||
void readConfig();
|
||||
|
||||
// 用于确定建立连接之前与客户端通信,连接后用doNotify
|
||||
void sendEarlyPacket(ClientSocket *client, const QString &type, const QString &msg);
|
||||
bool checkClientVersion(ClientSocket *client, const QString &ver);
|
||||
|
||||
// 某玩家刚刚连入之后,服务器告诉他关于他的一些基本信息
|
||||
void setupPlayer(ServerPlayer *player, bool all_info = true);
|
||||
void handleNameAndPassword(ClientSocket *client, const QString &name,
|
||||
const QString &password, const QString &md5_str, const QString &uuid_str);
|
||||
void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port);
|
||||
|
|
|
@ -124,3 +124,20 @@ void ServerPlayer::setThinking(bool t) {
|
|||
m_thinking = t;
|
||||
m_thinking_mutex.unlock();
|
||||
}
|
||||
|
||||
void ServerPlayer::startGameTimer() {
|
||||
gameTime = 0;
|
||||
gameTimer.start();
|
||||
}
|
||||
|
||||
void ServerPlayer::pauseGameTimer() {
|
||||
gameTime += gameTimer.elapsed() / 1000;
|
||||
}
|
||||
|
||||
void ServerPlayer::resumeGameTimer() {
|
||||
gameTimer.start();
|
||||
}
|
||||
|
||||
int ServerPlayer::getGameTime() {
|
||||
return gameTime + (getState() == Player::Online ? gameTimer.elapsed() / 1000 : 0);
|
||||
}
|
||||
|
|
|
@ -43,6 +43,12 @@ public:
|
|||
|
||||
bool thinking();
|
||||
void setThinking(bool t);
|
||||
|
||||
void startGameTimer();
|
||||
void pauseGameTimer();
|
||||
void resumeGameTimer();
|
||||
int getGameTime();
|
||||
|
||||
signals:
|
||||
void disconnected();
|
||||
void kicked();
|
||||
|
@ -58,6 +64,9 @@ private:
|
|||
|
||||
QString requestCommand;
|
||||
QString requestData;
|
||||
|
||||
int gameTime; // 在这个房间的有效游戏时长(秒)
|
||||
QElapsedTimer gameTimer;
|
||||
};
|
||||
|
||||
#endif // _SERVERPLAYER_H
|
||||
|
|
|
@ -23,6 +23,9 @@ public:
|
|||
QString getAvatar() const;
|
||||
void setAvatar(const QString &avatar);
|
||||
|
||||
int getTotalGameTime() const;
|
||||
void addTotalGameTime(int toAdd);
|
||||
|
||||
State getState() const;
|
||||
void setState(State state);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user