游戏时长统计 (#302)

* 修复和完善qml mark
* 修复国战野心家放副将
* [需要编译] 统计游戏时长功能
* 后台也开始记录注册时间和上次上线的时间
* 现在会将屏蔽玩家保存到本地并标红提示
This commit is contained in:
notify 2023-12-28 12:11:24 +08:00 committed by GitHub
parent 94b7493e2e
commit 278e7ce4c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 573 additions and 130 deletions

View File

@ -92,6 +92,7 @@ Flickable {
config.blockedUsersChanged(); config.blockedUsersChanged();
} }
} }
MetroButton { MetroButton {
text: Backend.translate("Kick From Room") text: Backend.translate("Kick From Room")
visible: !roomScene.isStarted && roomScene.isOwner visible: !roomScene.isStarted && roomScene.isOwner
@ -168,11 +169,20 @@ Flickable {
const total = gamedata[0]; const total = gamedata[0];
const win = gamedata[1]; const win = gamedata[1];
const run = gamedata[2]; const run = gamedata[2];
const totalTime = gamedata[3];
const winRate = (win / total) * 100; const winRate = (win / total) * 100;
const runRate = (run / total) * 100; const runRate = (run / total) * 100;
playerGameData.text = total === 0 ? Backend.translate("Newbie") : playerGameData.text = total === 0 ? Backend.translate("Newbie") :
Backend.translate("Win=%1 Run=%2 Total=%3").arg(winRate.toFixed(2)) Backend.translate("Win=%1 Run=%2 Total=%3").arg(winRate.toFixed(2))
.arg(runRate.toFixed(2)).arg(total); .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])); const data = JSON.parse(Backend.callLuaFunction("GetPlayerSkills", [id]));

View File

@ -50,6 +50,7 @@ QtObject {
property bool observing: false property bool observing: false
property bool replaying: false property bool replaying: false
property list<string> blockedUsers: [] property list<string> blockedUsers: []
property int totalTime: 0 // FIXME: only for notifying
onDisabledGeneralsChanged: { onDisabledGeneralsChanged: {
disableGeneralSchemes[disableSchemeIdx] = disabledGenerals; disableGeneralSchemes[disableSchemeIdx] = disabledGenerals;
@ -89,6 +90,7 @@ QtObject {
disabledGenerals = conf.disabledGenerals ?? []; disabledGenerals = conf.disabledGenerals ?? [];
disableGeneralSchemes = conf.disableGeneralSchemes ?? [ disabledGenerals ]; disableGeneralSchemes = conf.disableGeneralSchemes ?? [ disabledGenerals ];
disableSchemeIdx = conf.disableSchemeIdx ?? 0; disableSchemeIdx = conf.disableSchemeIdx ?? 0;
blockedUsers = conf.blockedUsers ?? [];
} }
function saveConf() { function saveConf() {
@ -117,6 +119,7 @@ QtObject {
conf.disabledGenerals = disabledGenerals; conf.disabledGenerals = disabledGenerals;
conf.disableGeneralSchemes = disableGeneralSchemes; conf.disableGeneralSchemes = disableGeneralSchemes;
conf.disableSchemeIdx = disableSchemeIdx; conf.disableSchemeIdx = disableSchemeIdx;
conf.blockedUsers = blockedUsers;
Backend.saveConf(JSON.stringify(conf, undefined, 2)); Backend.saveConf(JSON.stringify(conf, undefined, 2));
} }

View File

@ -10,6 +10,37 @@ Item {
width: bg.width width: bg.width
height: bg.height 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 { Image {
id: bg id: bg
x: -32 x: -32

View File

@ -194,3 +194,7 @@ callbacks["ServerMessage"] = (jsonData) => {
callbacks["ShowToast"] = (j) => toast.show(j); callbacks["ShowToast"] = (j) => toast.show(j);
callbacks["InstallKey"] = (j) => Backend.installAESKey(); callbacks["InstallKey"] = (j) => Backend.installAESKey();
callbacks["AddTotalGameTime"] = (jsonData) => {
config.totalTime++;
}

View File

@ -1276,7 +1276,7 @@ Item {
gameData = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [item.id])); gameData = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [item.id]));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
gameData = [0, 0, 0]; gameData = [0, 0, 0, 0];
} }
if (item.id > 0) { if (item.id > 0) {
datalist.push({ datalist.push({

View File

@ -75,10 +75,10 @@ Item {
if (close_br === -1) return; if (close_br === -1) return;
const mark_type = mark_name.slice(2, close_br); const mark_type = mark_name.slice(2, close_br);
const _data = (mark_extra); const _data = mark_extra;
let data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark_name, JSON.stringify(_data)])); let data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark_name, _data, root.parent?.playerid]));
if (data && data.qml_path) { if (data && data.qml_path) {
params.data = _data; params.data = JSON.parse(_data);
roomScene.startCheat("../../" + data.qml_path, params); roomScene.startCheat("../../" + data.qml_path, params);
} }
return; return;
@ -122,7 +122,8 @@ Item {
const close_br = mark.indexOf(']'); const close_br = mark.indexOf(']');
if (close_br !== -1) { if (close_br !== -1) {
const mark_type = mark.slice(2, close_br); 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) { if (_data && _data.text) {
special_value = _data.text; special_value = _data.text;
} }

View File

@ -227,6 +227,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
y: 90 y: 90
scale: 1.25 scale: 1.25
z: 1
} }
Rectangle { Rectangle {
@ -234,6 +235,7 @@ Item {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5) color: Qt.rgba(0, 0, 0, 0.5)
opacity: 0.7 opacity: 0.7
z: 2
} }
Text { Text {

View File

@ -228,7 +228,8 @@ GraphicsBox {
} }
root.choicesChanged(); 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++) { for (i = 0; i < generalCardList.count; i++) {
item = generalCardList.itemAt(i); item = generalCardList.itemAt(i);

View File

@ -29,14 +29,14 @@ CardItem {
suit: "" suit: ""
number: 0 number: 0
footnote: "" footnote: ""
card.source: SkinBank.getGeneralPicture(name) card.source: known ? SkinBank.getGeneralPicture(name) : (SkinBank.GENERALCARD_DIR + 'card-back')
glow.color: "white" //Engine.kingdomColor[kingdom] glow.color: "white" //Engine.kingdomColor[kingdom]
// FIXME: // FIXME:
property bool heg: name.startsWith('hs__') || name.startsWith('ld__') || name.includes('heg__') property bool heg: name.startsWith('hs__') || name.startsWith('ld__') || name.includes('heg__')
Image { Image {
source: SkinBank.GENERALCARD_DIR + "border" source: known ? (SkinBank.GENERALCARD_DIR + "border") : ""
} }
Image { Image {
@ -44,7 +44,7 @@ CardItem {
width: 34; fillMode: Image.PreserveAspectFit width: 34; fillMode: Image.PreserveAspectFit
transformOrigin: Item.TopLeft transformOrigin: Item.TopLeft
source: SkinBank.getGeneralCardDir(kingdom) + kingdom source: SkinBank.getGeneralCardDir(kingdom) + kingdom
visible: detailed visible: detailed && known
} }
Image { Image {
@ -52,14 +52,14 @@ CardItem {
transformOrigin: Item.TopLeft transformOrigin: Item.TopLeft
width: 34; fillMode: Image.PreserveAspectFit width: 34; fillMode: Image.PreserveAspectFit
source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom : "" source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom : ""
visible: detailed visible: detailed && known
} }
Row { Row {
x: 34 x: 34
y: 4 y: 4
spacing: 1 spacing: 1
visible: detailed && !heg visible: detailed && known && !heg
Repeater { Repeater {
id: hpRepeater id: hpRepeater
model: (!heg) ? ((hp > 5 || hp !== maxHp) ? 1 : hp) : 0 model: (!heg) ? ((hp > 5 || hp !== maxHp) ? 1 : hp) : 0
@ -106,7 +106,7 @@ CardItem {
x: 34 x: 34
y: 3 y: 3
spacing: 0 spacing: 0
visible: detailed && heg visible: detailed && known && heg
Repeater { Repeater {
id: hegHpRepeater id: hegHpRepeater
model: heg ? ((hp > 7 || hp !== maxHp) ? 1 : Math.ceil(hp / 2)) : 0 model: heg ? ((hp > 7 || hp !== maxHp) ? 1 : Math.ceil(hp / 2)) : 0
@ -140,7 +140,7 @@ CardItem {
} }
Shield { Shield {
visible: shieldNum > 0 && detailed visible: shieldNum > 0 && detailed && known
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: hpRepeater.model > 4 ? 16 : 0 anchors.topMargin: hpRepeater.model > 4 ? 16 : 0
@ -154,7 +154,7 @@ CardItem {
x: 2 x: 2
y: lineCount > 6 ? 30 : 34 y: lineCount > 6 ? 30 : 34
text: name !== "" ? Backend.translate(name) : "nil" text: name !== "" ? Backend.translate(name) : "nil"
visible: Backend.translate(name).length <= 6 && detailed visible: Backend.translate(name).length <= 6 && detailed && known
color: "white" color: "white"
font.family: fontLibian.name font.family: fontLibian.name
font.pixelSize: 18 font.pixelSize: 18
@ -169,7 +169,7 @@ CardItem {
rotation: 90 rotation: 90
transformOrigin: Item.BottomLeft transformOrigin: Item.BottomLeft
text: Backend.translate(name) text: Backend.translate(name)
visible: Backend.translate(name).length > 6 && detailed visible: Backend.translate(name).length > 6 && detailed && known
color: "white" color: "white"
font.family: fontLibian.name font.family: fontLibian.name
font.pixelSize: 18 font.pixelSize: 18
@ -177,7 +177,7 @@ CardItem {
} }
Rectangle { Rectangle {
visible: pkgName !== "" && detailed visible: pkgName !== "" && detailed && known
height: 16 height: 16
width: childrenRect.width + 4 width: childrenRect.width + 4
anchors.bottom: parent.bottom anchors.bottom: parent.bottom

View File

@ -531,7 +531,7 @@ Item {
anchors.topMargin: 2 anchors.topMargin: 2
font.pixelSize: 16 font.pixelSize: 16
text: screenName text: (config.blockedUsers && config.blockedUsers.includes(screenName) ? Backend.translate("<Blocked> ") : "") + screenName
glow.radius: 8 glow.radius: 8
} }

29
docs/misc/calcDailyLogin.sh Executable file
View 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
View 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日nyutanislavskynyutan下称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年
=====================

View 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
View 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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -286,8 +286,9 @@ fk.client_callback["AddPlayer"] = function(jsonData)
-- jsonData: [ int id, string screenName, string avatar ] -- jsonData: [ int id, string screenName, string avatar ]
-- when other player enter the room, we create clientplayer(C and lua) for them -- when other player enter the room, we create clientplayer(C and lua) for them
local data = json.decode(jsonData) 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) local player = fk.ClientInstance:addPlayer(id, name, avatar)
player:addTotalGameTime(time)
local p = ClientPlayer:new(player) local p = ClientPlayer:new(player)
table.insert(ClientInstance.players, p) table.insert(ClientInstance.players, p)
table.insert(ClientInstance.alive_players, p) table.insert(ClientInstance.alive_players, p)
@ -920,6 +921,18 @@ fk.client_callback["UpdateGameData"] = function(jsonData)
ClientInstance:notifyUI("UpdateGameData", jsonData) ClientInstance:notifyUI("UpdateGameData", jsonData)
end 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) fk.client_callback["StartGame"] = function(jsonData)
local c = ClientInstance local c = ClientInstance
c.record = { c.record = {
@ -946,6 +959,7 @@ fk.client_callback["StartGame"] = function(jsonData)
p.player:getScreenName(), p.player:getScreenName(),
p.player:getAvatar(), p.player:getAvatar(),
true, true,
p.player:getTotalGameTime(),
}, },
}) })
end end

View File

@ -650,19 +650,21 @@ end
function GetPlayerGameData(pid) function GetPlayerGameData(pid)
local c = ClientInstance local c = ClientInstance
local p = c:getPlayerById(pid) 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 raw = p.player:getGameData()
local ret = {} local ret = {}
for _, i in fk.qlist(raw) do for _, i in fk.qlist(raw) do
table.insert(ret, i) table.insert(ret, i)
end end
table.insert(ret, p.player:getTotalGameTime())
return json.encode(ret) return json.encode(ret)
end end
function SetPlayerGameData(pid, data) function SetPlayerGameData(pid, data)
local c = ClientInstance local c = ClientInstance
local p = c:getPlayerById(pid) 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) table.insert(data, 1, pid)
ClientInstance:notifyUI("UpdateGameData", json.encode(data)) ClientInstance:notifyUI("UpdateGameData", json.encode(data))
end end
@ -735,13 +737,14 @@ function PoxiFeasible(poxi_type, selected, data, extra_data)
return json.encode(poxi.feasible(selected, data, extra_data)) return json.encode(poxi.feasible(selected, data, extra_data))
end end
function GetQmlMark(mtype, name, value) function GetQmlMark(mtype, name, value, p)
local spec = Fk.qml_marks[mtype] local spec = Fk.qml_marks[mtype]
if not spec then return "{}" end if not spec then return "{}" end
p = ClientInstance:getPlayerById(p)
value = json.decode(value) value = json.decode(value)
return json.encode { return json.encode {
qml_path = spec.qml_path, 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) text = spec.how_to_show(name, value, p)
} }
end end

View File

@ -68,6 +68,8 @@ Fk:loadTranslationTable({
--["Newbie"] = "新手保护ing", --["Newbie"] = "新手保护ing",
["Win=%1 Run=%2 Total=%3"] = "Win=%1% Run=%2% Total=%3", ["Win=%1 Run=%2 Total=%3"] = "Win=%1% Run=%2% Total=%3",
["Win=%1\nRun=%2\nTotal=%3"] = "Win: %1%\nRun: %2%\nTotal: %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", ["Ban List"] = "Ban character scheme",
["List"] = "Scheme", ["List"] = "Scheme",

View File

@ -65,10 +65,13 @@ Fk:loadTranslationTable{
["Give Shoe"] = "拖鞋", ["Give Shoe"] = "拖鞋",
["Block Chatter"] = "屏蔽发言", ["Block Chatter"] = "屏蔽发言",
["Unblock Chatter"] = "解除屏蔽", ["Unblock Chatter"] = "解除屏蔽",
["<Blocked> "] = '<font color="red">[已屏蔽]</font> ',
["Kick From Room"] = "踢出房间", ["Kick From Room"] = "踢出房间",
["Newbie"] = "新手保护ing", ["Newbie"] = "新手保护ing",
["Win=%1 Run=%2 Total=%3"] = "胜率%1% 逃率%2% 总场次%3", ["Win=%1 Run=%2 Total=%3"] = "胜率%1% 逃率%2% 总场次%3",
["Win=%1\nRun=%2\nTotal=%3"] = "胜率: %1%\n逃率: %2%\n总场次: %3", ["Win=%1\nRun=%2\nTotal=%3"] = "胜率: %1%\n逃率: %2%\n总场次: %3",
["TotalGameTime: %1 min"] = "已游玩: %1 分钟",
["TotalGameTime: %1 h"] = "已游玩: %1 小时",
["Ban List"] = "禁将方案", ["Ban List"] = "禁将方案",
["List"] = "方案", ["List"] = "方案",

View File

@ -942,6 +942,8 @@ end
---@param ignoreFromKong? boolean ---@param ignoreFromKong? boolean
---@param ignoreToKong? boolean ---@param ignoreToKong? boolean
function Player:canPindian(to, ignoreFromKong, ignoreToKong) function Player:canPindian(to, ignoreFromKong, ignoreToKong)
if self == to then return false end
if self:isKongcheng() and not ignoreFromKong then if self:isKongcheng() and not ignoreFromKong then
return false return false
end end

View File

@ -606,5 +606,5 @@ end
---@class QmlMarkSpec ---@class QmlMarkSpec
---@field name string ---@field name string
---@field qml_path string ---@field qml_path string | fun(name: string, value?: any, player?: Player): string
---@field how_to_show function(name: string, value?: any): string? ---@field how_to_show fun(name: string, value?: any, player?: Player): string?

View File

@ -18,6 +18,8 @@ local function tellRoomToObserver(self, player)
p.id, p.id,
p._splayer:getScreenName(), p._splayer:getScreenName(),
p._splayer:getAvatar(), p._splayer:getAvatar(),
false,
p._splayer:getTotalGameTime(),
}) })
end end

View File

@ -321,11 +321,6 @@ function ServerPlayer:reconnect()
local room = self.room local room = self.room
self.serverplayer:setState(fk.Player_Online) self.serverplayer:setState(fk.Player_Online)
self:doNotify("Setup", json.encode{
self.id,
self._splayer:getScreenName(),
self._splayer:getAvatar(),
})
self:doNotify("EnterLobby", "") self:doNotify("EnterLobby", "")
self:doNotify("EnterRoom", json.encode{ self:doNotify("EnterRoom", json.encode{
#room.players, room.timeout, room.settings, #room.players, room.timeout, room.settings,
@ -339,6 +334,8 @@ function ServerPlayer:reconnect()
p.id, p.id,
p._splayer:getScreenName(), p._splayer:getScreenName(),
p._splayer:getAvatar(), p._splayer:getAvatar(),
false,
p._splayer:getTotalGameTime(),
}) })
end end
self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() }) self:doNotify("RoomOwner", json.encode{ room.room:getOwner():getId() })

View File

@ -25,6 +25,13 @@ CREATE TABLE IF NOT EXISTS banuuid (
uuid VARCHAR(32) 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 ( CREATE TABLE IF NOT EXISTS friendinfo (
id1 INTEGER, id1 INTEGER,
id2 INTEGER, id2 INTEGER,

View File

@ -3,7 +3,7 @@
#include "player.h" #include "player.h"
Player::Player(QObject *parent) 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) {} totalGames(0), winCount(0), runCount(0) {}
Player::~Player() {} Player::~Player() {}
@ -26,6 +26,12 @@ void Player::setAvatar(const QString &avatar) {
emit avatarChanged(); emit avatarChanged();
} }
int Player::getTotalGameTime() const { return totalGameTime; }
void Player::addTotalGameTime(int toAdd) {
totalGameTime += toAdd;
}
Player::State Player::getState() const { return state; } Player::State Player::getState() const { return state; }
QString Player::getStateString() const { QString Player::getStateString() const {

View File

@ -31,6 +31,9 @@ public:
QString getAvatar() const; QString getAvatar() const;
void setAvatar(const QString &avatar); void setAvatar(const QString &avatar);
int getTotalGameTime() const;
void addTotalGameTime(int toAdd);
State getState() const; State getState() const;
QString getStateString() const; QString getStateString() const;
void setState(State state); void setState(State state);
@ -58,6 +61,7 @@ private:
int id; int id;
QString screenName; // screenName should not be same. QString screenName; // screenName should not be same.
QString avatar; QString avatar;
int totalGameTime;
State state; State state;
bool ready; bool ready;
bool died; bool died;

View File

@ -151,6 +151,7 @@ void Room::addPlayer(ServerPlayer *player) {
jsonData << player->getScreenName(); jsonData << player->getScreenName();
jsonData << player->getAvatar(); jsonData << player->getAvatar();
jsonData << player->isReady(); jsonData << player->isReady();
jsonData << player->getTotalGameTime();
doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData)); doBroadcastNotify(getPlayers(), "AddPlayer", JsonArray2Bytes(jsonData));
} }
@ -179,6 +180,7 @@ void Room::addPlayer(ServerPlayer *player) {
jsonData << p->getScreenName(); jsonData << p->getScreenName();
jsonData << p->getAvatar(); jsonData << p->getAvatar();
jsonData << p->isReady(); jsonData << p->isReady();
jsonData << p->getTotalGameTime();
player->doNotify("AddPlayer", JsonArray2Bytes(jsonData)); player->doNotify("AddPlayer", JsonArray2Bytes(jsonData));
jsonData = QJsonArray(); jsonData = QJsonArray();
@ -274,6 +276,7 @@ void Room::removePlayer(ServerPlayer *player) {
runner->setId(player->getId()); runner->setId(player->getId());
auto gamedata = player->getGameData(); auto gamedata = player->getGameData();
runner->setGameData(gamedata[0], gamedata[1], gamedata[2]); 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() { void Room::gameOver() {
if (!gameStarted) return;
gameStarted = false; gameStarted = false;
runned_players.clear(); runned_players.clear();
// 清理所有状态不是“在线”的玩家 // 清理所有状态不是“在线”的玩家,增加逃率、游戏时长
auto settings = QJsonDocument::fromJson(this->settings); auto settings = QJsonDocument::fromJson(this->settings);
auto mode = settings["gameMode"].toString(); auto mode = settings["gameMode"].toString();
foreach (ServerPlayer *p, players) { 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::Online) {
if (p->getState() == Player::Offline) { if (p->getState() == Player::Offline) {
auto pid = p->getId();
addRunRate(pid, mode); addRunRate(pid, mode);
// addRunRate(pid, mode); // addRunRate(pid, mode);
server->temporarilyBan(pid); server->temporarilyBan(pid);
@ -572,6 +595,7 @@ void Room::manuallyStart() {
foreach (auto p, players) { foreach (auto p, players) {
p->setReady(false); p->setReady(false);
p->setDied(false); p->setDied(false);
p->startGameTimer();
} }
gameStarted = true; gameStarted = true;
m_thread->pushRequest(QString("-1,%1,newroom").arg(QString::number(id))); m_thread->pushRequest(QString("-1,%1,newroom").arg(QString::number(id)));

View File

@ -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) { void Server::processNewConnection(ClientSocket *client) {
auto addr = client->peerAddress(); auto addr = client->peerAddress();
qInfo() << addr << "connected"; qInfo() << addr << "connected";
// check ban ip
auto result = SelectFromDatabase( auto result = SelectFromDatabase(
db, QString("SELECT * FROM banip WHERE ip='%1';").arg(addr)); db, QString("SELECT * FROM banip WHERE ip='%1';").arg(addr));
@ -229,13 +279,7 @@ void Server::processNewConnection(ClientSocket *client) {
} }
if (!errmsg.isEmpty()) { if (!errmsg.isEmpty()) {
QJsonArray body; sendEarlyPacket(client, "ErrorMsg", errmsg);
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "ErrorMsg";
body << errmsg;
client->send(JsonArray2Bytes(body));
qInfo() << "Refused banned IP:" << addr; qInfo() << "Refused banned IP:" << addr;
client->disconnectFromHost(); client->disconnectFromHost();
return; return;
@ -245,13 +289,7 @@ void Server::processNewConnection(ClientSocket *client) {
[client]() { qInfo() << client->peerAddress() << "disconnected"; }); [client]() { qInfo() << client->peerAddress() << "disconnected"; });
// network delay test // network delay test
QJsonArray body; sendEarlyPacket(client, "NetworkDelayTest", public_key);
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "NetworkDelayTest";
body << public_key;
client->send(JsonArray2Bytes(body));
// Note: the client should send a setup string next // Note: the client should send a setup string next
connect(client, &ClientSocket::message_got, this, &Server::processRequest); connect(client, &ClientSocket::message_got, this, &Server::processRequest);
client->timerSignup.start(30000); client->timerSignup.start(30000);
@ -278,40 +316,14 @@ void Server::processRequest(const QByteArray &msg) {
if (!valid) { if (!valid) {
qWarning() << "Invalid setup string:" << msg; qWarning() << "Invalid setup string:" << msg;
QJsonArray body; sendEarlyPacket(client, "ErrorMsg", "INVALID SETUP STRING");
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "ErrorMsg";
body << "INVALID SETUP STRING";
client->send(JsonArray2Bytes(body));
client->disconnectFromHost(); client->disconnectFromHost();
return; return;
} }
QJsonArray arr = String2Json(doc[3].toString()).array(); QJsonArray arr = String2Json(doc[3].toString()).array();
auto client_ver = QVersionNumber::fromString(arr[3].toString()); if (!checkClientVersion(client, arr[3].toString())) return;
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;
}
auto uuid = arr[4].toString(); auto uuid = arr[4].toString();
auto result2 = QJsonArray({1}); auto result2 = QJsonArray({1});
@ -321,13 +333,7 @@ void Server::processRequest(const QByteArray &msg) {
} }
if (!result2.isEmpty()) { if (!result2.isEmpty()) {
QJsonArray body; sendEarlyPacket(client, "ErrorMsg", "you have been banned!");
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "ErrorMsg";
body << "you have been banned!";
client->send(JsonArray2Bytes(body));
qInfo() << "Refused banned UUID:" << uuid; qInfo() << "Refused banned UUID:" << uuid;
client->disconnectFromHost(); client->disconnectFromHost();
return; return;
@ -352,13 +358,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
auto aes_bytes = decrypted_pw.first(32); auto aes_bytes = decrypted_pw.first(32);
// tell client to install aes key // tell client to install aes key
QJsonArray body; sendEarlyPacket(client, "InstallKey", "");
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "InstallKey";
body << "";
client->send(JsonArray2Bytes(body));
client->installAESKey(aes_bytes); client->installAESKey(aes_bytes);
decrypted_pw.remove(0, 32); decrypted_pw.remove(0, 32);
@ -367,20 +367,8 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
} }
if (md5 != md5_str) { if (md5 != md5_str) {
QJsonArray body; sendEarlyPacket(client, "ErrorMsg", "MD5 check failed!");
body << -2; sendEarlyPacket(client, "UpdatePackage", Pacman->getPackSummary());
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));
client->disconnectFromHost(); client->disconnectFromHost();
return; return;
} }
@ -415,6 +403,10 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
ExecSQL(db, sql_reg); ExecSQL(db, sql_reg);
result = SelectFromDatabase(db, sql_find); // refresh result result = SelectFromDatabase(db, sql_find); // refresh result
obj = result[0].toObject(); 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; passed = true;
} else { } else {
obj = result[0].toObject(); obj = result[0].toObject();
@ -454,11 +446,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
} }
if (room && !room->isLobby()) { if (room && !room->isLobby()) {
player->doNotify("SetServerSettings", JsonArray2Bytes({ setupPlayer(player, true);
getConfig("motd"),
getConfig("hiddenPacks"),
getConfig("enableBots"),
}));
room->pushRequest(QString("%1,reconnect").arg(id)); room->pushRequest(QString("%1,reconnect").arg(id));
} else { } else {
// 懒得处理掉线玩家在大厅了!踢掉得了 // 懒得处理掉线玩家在大厅了!踢掉得了
@ -479,15 +467,23 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
if (passed) { if (passed) {
// update lastLoginIp // update lastLoginIp
int id = obj["id"].toString().toInt();
beginTransaction();
auto sql_update = auto sql_update =
QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;") QString("UPDATE userinfo SET lastLoginIp='%1' WHERE id=%2;")
.arg(client->peerAddress()) .arg(client->peerAddress())
.arg(obj["id"].toString().toInt()); .arg(id);
ExecSQL(db, sql_update); 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, 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 // create new ServerPlayer and setup
ServerPlayer *player = new ServerPlayer(lobby()); ServerPlayer *player = new ServerPlayer(lobby());
player->setSocket(client); player->setSocket(client);
@ -497,35 +493,23 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
connect(player, &Player::stateChanged, this, &Server::onUserStateChanged); connect(player, &Player::stateChanged, this, &Server::onUserStateChanged);
player->setScreenName(name); player->setScreenName(name);
player->setAvatar(obj["avatar"].toString()); player->setAvatar(obj["avatar"].toString());
player->setId(obj["id"].toString().toInt()); player->setId(id);
if (players.count() <= 10) { if (players.count() <= 10) {
broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName())); broadcast("ServerMessage", tr("%1 logged in").arg(player->getScreenName()));
} }
players.insert(player->getId(), player); players.insert(player->getId(), player);
// tell the lobby player's basic property setupPlayer(player);
QJsonArray arr;
arr << player->getId();
arr << player->getScreenName();
arr << player->getAvatar();
player->doNotify("Setup", JsonArray2Bytes(arr));
player->doNotify("SetServerSettings", JsonArray2Bytes({ auto result = SelectFromDatabase(db, QString("SELECT totalGameTime FROM usergameinfo WHERE id=%1;").arg(id));
getConfig("motd"), auto time = result[0].toObject()["totalGameTime"].toString().toInt();
getConfig("hiddenPacks"), player->addTotalGameTime(time);
getConfig("enableBots"), player->doNotify("AddTotalGameTime", JsonArray2Bytes({ id, time }));
}));
lobby()->addPlayer(player); lobby()->addPlayer(player);
} else { } else {
qInfo() << client->peerAddress() << "lost connection:" << error_msg; qInfo() << client->peerAddress() << "lost connection:" << error_msg;
QJsonArray body; sendEarlyPacket(client, "ErrorMsg", error_msg);
body << -2;
body << (Router::TYPE_NOTIFICATION | Router::SRC_SERVER |
Router::DEST_CLIENT);
body << "ErrorMsg";
body << error_msg;
client->send(JsonArray2Bytes(body));
client->disconnectFromHost(); client->disconnectFromHost();
return; return;
} }
@ -572,8 +556,15 @@ void Server::onUserStateChanged() {
if (!room || room->isLobby() || room->isAbandoned()) { if (!room || room->isLobby() || room->isAbandoned()) {
return; return;
} }
auto state = player->getState();
room->doBroadcastNotify(room->getPlayers(), "NetStateChanged", room->doBroadcastNotify(room->getPlayers(), "NetStateChanged",
QString("[%1,\"%2\"]").arg(player->getId()).arg(player->getStateString())); QString("[%1,\"%2\"]").arg(player->getId()).arg(player->getStateString()));
if (state == Player::Online) {
player->resumeGameTimer();
} else {
player->pauseGameTimer();
}
} }
RSA *Server::initServerRSA() { RSA *Server::initServerRSA() {

View File

@ -67,7 +67,7 @@ public slots:
private: private:
friend class Shell; friend class Shell;
ServerSocket *server; ServerSocket *server;
QUdpSocket *udpSocket; QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用
Room *m_lobby; Room *m_lobby;
QMap<int, Room *> rooms; QMap<int, Room *> rooms;
@ -89,6 +89,12 @@ private:
QJsonObject config; QJsonObject config;
void readConfig(); 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, void handleNameAndPassword(ClientSocket *client, const QString &name,
const QString &password, const QString &md5_str, const QString &uuid_str); const QString &password, const QString &md5_str, const QString &uuid_str);
void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port); void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port);

View File

@ -124,3 +124,20 @@ void ServerPlayer::setThinking(bool t) {
m_thinking = t; m_thinking = t;
m_thinking_mutex.unlock(); 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);
}

View File

@ -43,6 +43,12 @@ public:
bool thinking(); bool thinking();
void setThinking(bool t); void setThinking(bool t);
void startGameTimer();
void pauseGameTimer();
void resumeGameTimer();
int getGameTime();
signals: signals:
void disconnected(); void disconnected();
void kicked(); void kicked();
@ -58,6 +64,9 @@ private:
QString requestCommand; QString requestCommand;
QString requestData; QString requestData;
int gameTime; // 在这个房间的有效游戏时长(秒)
QElapsedTimer gameTimer;
}; };
#endif // _SERVERPLAYER_H #endif // _SERVERPLAYER_H

View File

@ -23,6 +23,9 @@ public:
QString getAvatar() const; QString getAvatar() const;
void setAvatar(const QString &avatar); void setAvatar(const QString &avatar);
int getTotalGameTime() const;
void addTotalGameTime(int toAdd);
State getState() const; State getState() const;
void setState(State state); void setState(State state);