- 游戏结束时离线玩家增加逃率
- 退出房间时取消准备状态
- 副技能的 `main_skill`
- 预亮相关优化
- 自定义身份,图从拓展包随便找一张
- 无懈可击使用时带1200毫秒延迟
- 未开始的房间显示开启的所有牌堆,衍生牌灰色字体化
- 可以随意打开fk.rep文件并播放录像
- 服务器Shell新增重置密码命令
This commit is contained in:
notify 2023-08-09 22:25:15 +08:00 committed by GitHub
parent 4bee447327
commit 0745863863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 314 additions and 53 deletions

View File

@ -3,6 +3,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import Fk
Item {
@ -29,6 +30,12 @@ Item {
Menu {
id: menu
y: bar.height
MenuItem {
text: qsTr("Replay from file")
onTriggered: {
fdialog.open();
}
}
}
}
}
@ -104,7 +111,7 @@ Item {
onClicked: {
config.observing = true;
config.replaying = true;
Backend.playRecord(fileName);
Backend.playRecord("recording/" + fileName);
}
}
@ -122,6 +129,17 @@ Item {
}
}
FileDialog {
id: fdialog
nameFilters: ["FK Rep Files (*.fk.rep)"];
onAccepted: {
config.observing = true;
config.replaying = true;
let str = selectedFile.toString(); // QUrl -> string
Backend.playRecord(str);
}
}
function updateList() {
model.clear();
const data = Backend.ls("recording");

View File

@ -210,22 +210,55 @@ Item {
}
}
Rectangle {
x: parent.width / 2 + 80
y: parent.height / 2
x: parent.width / 2 + 60
y: parent.height / 2 - 30
color: "snow"
opacity: 0.8
radius: 6
height: childrenRect.height + 16
width: childrenRect.width + 16
visible: !isStarted
width: 280
height: 280
Flickable {
id: flickableContainer
ScrollBar.vertical: ScrollBar {}
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
flickableDirection: Flickable.VerticalFlick
width: parent.width - 10
height: parent.height - 10
contentHeight: roominfo.height
clip: true
Text {
x: 8; y: 8
id: roominfo
font.pixelSize: 16
width: parent.width
wrapMode: TextEdit.WordWrap
Component.onCompleted: {
const data = JSON.parse(Backend.callLuaFunction("GetRoomConfig", []));
text = Backend.translate("LuckCardNum") + data.luckTime + "<br />" + Backend.translate("ResponseTime") + config.roomTimeout
+ "<br />" + Backend.translate("GeneralBoxNum") + data.generalNum + (data.enableFreeAssign ? "<br />" + Backend.translate("IncludeFreeAssign") : "")
+ (data.enableDeputy ? "<br />" + Backend.translate("IncludeDeputy") : "")
let cardpack = JSON.parse(Backend.callLuaFunction("GetAllCardPack", []));
cardpack = cardpack.filter(p => !data.disabledPack.includes(p));
text = "游戏模式:" + Backend.translate(data.gameMode) + "<br />"
+ Backend.translate("LuckCardNum") + "<b>" + data.luckTime + "</b><br />"
+ Backend.translate("ResponseTime") + "<b>" + config.roomTimeout + "</b><br />"
+ Backend.translate("GeneralBoxNum") + "<b>" + data.generalNum + "</b>"
+ (data.enableFreeAssign ? "<br />" + Backend.translate("IncludeFreeAssign") : "")
+ (data.enableDeputy ? " " + Backend.translate("IncludeDeputy") : "")
+ '<br />使用牌堆:' + cardpack.map(e => {
let ret = Backend.translate(e);
if (ret.search(/特殊牌|衍生牌/) === -1) { // TODO: 西= =
ret = "<b>" + ret + "</b>";
} else {
ret = '<font color="grey"><i>' + ret + "</i></font>";
}
return ret;
}).join('')
//+ '<br /><b></b>' + data.disabledPack.map(e => Backend.translate(e)).join('')
//+ '<br /><b></b>' + data.disabledGenerals.map(e => Backend.translate(e)).join('')
}
}
}
}

View File

@ -22,6 +22,9 @@ ColumnLayout {
const data = skills.get(i);
if (data.skillname_ === skill) {
data.times = times;
if (times == -1) {
skills.remove(i);
}
return;
}
}

View File

@ -8,7 +8,7 @@ Image {
property var options: ["unknown", "loyalist", "rebel", "renegade"]
id: root
source: visible ? SkinBank.ROLE_DIR + value : ""
source: visible ? SkinBank.getRolePic(value) : ""
visible: value != "hidden"
Image {

View File

@ -55,6 +55,7 @@ function getCardPicture(cidOrName) {
return path;
} else {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/card/" + name + ".png";
if (Backend.exists(path)) return path;
}
@ -70,6 +71,7 @@ function getDelayedTrickPicture(name) {
return path;
} else {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/card/delayedTrick/" + name + ".png";
if (Backend.exists(path)) return path;
}
@ -87,6 +89,7 @@ function getEquipIcon(cid, icon) {
return path;
} else {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/card/equipIcon/" + name + ".png";
if (Backend.exists(path)) return path;
}
@ -98,6 +101,7 @@ function getPhotoBack(kingdom) {
let path = PHOTO_BACK_DIR + kingdom + ".png";
if (!Backend.exists(path)) {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/kingdom/" + kingdom + "-back.png";
if (Backend.exists(path)) return path;
}
@ -111,6 +115,7 @@ function getGeneralCardDir(kingdom) {
let path = GENERALCARD_DIR + kingdom + ".png";
if (!Backend.exists(path)) {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/kingdom/" + kingdom + "-back.png";
if (Backend.exists(path))
return AppPath + "/packages/" + dir + "/image/kingdom/";
@ -119,3 +124,17 @@ function getGeneralCardDir(kingdom) {
return GENERALCARD_DIR;
}
}
function getRolePic(role) {
let path = ROLE_DIR + role + ".png";
if (Backend.exists(path)) {
return path;
} else {
for (let dir of Backend.ls(AppPath + "/packages/")) {
if (dir.endsWith(".disabled")) continue;
path = AppPath + "/packages/" + dir + "/image/role/" + name + ".png";
if (Backend.exists(path)) return path;
}
}
return ROLE_DIR + "unknown.png";
}

View File

@ -585,21 +585,6 @@ fk.client_callback["ShowCard"] = function(jsonData)
})
end
fk.client_callback["LoseSkill"] = function(jsonData)
-- jsonData: [ int player_id, string skill_name ]
local data = json.decode(jsonData)
local id, skill_name, prelight = data[1], data[2], data[3]
local target = ClientInstance:getPlayerById(id)
local skill = Fk.skills[skill_name]
if not prelight then
target:loseSkill(skill)
end
if skill.visible then
ClientInstance:notifyUI("LoseSkill", jsonData)
end
end
-- 说是限定技,其实也适用于觉醒技、转换技、使命技
---@param skill Skill
---@param times integer
@ -613,16 +598,100 @@ local function updateLimitSkill(pid, skill, times)
end
end
fk.client_callback["LoseSkill"] = function(jsonData)
-- jsonData: [ int player_id, string skill_name ]
local data = json.decode(jsonData)
local id, skill_name, fake = data[1], data[2], data[3]
local target = ClientInstance:getPlayerById(id)
local skill = Fk.skills[skill_name]
if not fake then
target:loseSkill(skill)
if skill.visible then
ClientInstance:notifyUI("LoseSkill", jsonData)
end
elseif skill.visible then
-- 按理说能弄得更好的但还是复制粘贴舒服
local sks = { table.unpack(skill.related_skills) }
--[[ 需要大伙都适配好main_skill或者讨论出更好方案才行。不敢轻举妄动
local sks = table.filter(skill.related_skills, function(s)
return s.main_skill == skill
end)
--]]
table.insert(sks, skill)
table.removeOne(target.player_skills, skill)
local chk = false
if table.find(sks, function(s) return s:isInstanceOf(TriggerSkill) end) then
chk = true
ClientInstance:notifyUI("LoseSkill", jsonData)
end
local active = table.filter(sks, function(s)
return s:isInstanceOf(ActiveSkill) or s:isInstanceOf(ViewAsSkill)
end)
if #active > 0 then
chk = true
ClientInstance:notifyUI("LoseSkill", json.encode {
id, skill_name,
})
end
if not chk then
ClientInstance:notifyUI("LoseSkill", json.encode {
id, skill_name,
})
end
end
updateLimitSkill(id, skill, -1)
end
fk.client_callback["AddSkill"] = function(jsonData)
-- jsonData: [ int player_id, string skill_name ]
local data = json.decode(jsonData)
local id, skill_name = data[1], data[2]
local id, skill_name, fake = data[1], data[2], data[3]
local target = ClientInstance:getPlayerById(id)
local skill = Fk.skills[skill_name]
if not fake then
target:addSkill(skill)
if skill.visible then
ClientInstance:notifyUI("AddSkill", jsonData)
end
elseif skill.visible then
-- 添加假技能:服务器只会传一个主技能来。
-- 若有主动技则添加按钮,若有触发技则添加预亮按钮。
-- 无视状态技。
local sks = { table.unpack(skill.related_skills) }
table.insert(sks, skill)
table.insert(target.player_skills, skill)
local chk = false
if table.find(sks, function(s) return s:isInstanceOf(TriggerSkill) end) then
chk = true
ClientInstance:notifyUI("AddSkill", jsonData)
end
local active = table.filter(sks, function(s)
return s:isInstanceOf(ActiveSkill) or s:isInstanceOf(ViewAsSkill)
end)
if #active > 0 then
chk = true
ClientInstance:notifyUI("AddSkill", json.encode {
id, skill_name,
})
end
-- 面板上总得有点啥东西表明自己有技能吧 = =
if not chk then
ClientInstance:notifyUI("AddSkill", json.encode {
id, skill_name,
})
end
end
if skill.frequency == Skill.Quest then
return

View File

@ -678,6 +678,13 @@ function Player:hasSkill(skill, ignoreNullified, ignoreAlive)
return true
end
if self:isInstanceOf(ServerPlayer) and -- isInstanceOf(nil) will return false
table.contains(self._fake_skills, skill) and
table.contains(self.prelighted_skills, skill) then
return true
end
for _, v in pairs(self.derivative_skills) do
if table.contains(v, skill) then
return true

View File

@ -56,8 +56,15 @@ end
-- do cost and skill effect.
-- DO NOT modify this function
function TriggerSkill:doCost(event, target, player, data)
local start_time = os.getms()
local ret = self:cost(event, target, player, data)
local end_time = os.getms()
local room = player.room
-- 对于那种cost直接返回true的锁定技如果是预亮技那么还是询问一下好
if ret and player:isFakeSkill(self) and end_time - start_time < 10000 then
ret = room:askForSkillInvoke(player, self.name)
end
local cost_data_bak = self.cost_data
room.logic:trigger(fk.BeforeTriggerSkillUse, player, { skill = self, willUse = ret })

View File

@ -1,6 +1,7 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@class UsableSkill : Skill
---@field public main_skill UsableSkill
---@field public max_use_time integer[]
---@field public expand_pile string
local UsableSkill = Skill:subclass("UsableSkill")

View File

@ -42,6 +42,8 @@ end
local function readUsableSpecToSkill(skill, spec)
readCommonSpecToSkill(skill, spec)
assert(spec.main_skill == nil or spec.main_skill:isInstanceOf(UsableSkill))
skill.main_skill = spec.main_skill
skill.target_num = spec.target_num or skill.target_num
skill.min_target_num = spec.min_target_num or skill.min_target_num
skill.max_target_num = spec.max_target_num or skill.max_target_num

View File

@ -1,9 +1,14 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
GameEvent.functions[GameEvent.SkillEffect] = function(self)
local effect_cb, player, skill = table.unpack(self.data)
local effect_cb, player, _skill = table.unpack(self.data)
local room = self.room
local logic = room.logic
local skill = _skill.main_skill and _skill.main_skill or _skill
if player then
player:addSkillUseHistory(skill.name)
end
local cost_data_bak = skill.cost_data
logic:trigger(fk.SkillEffect, player, skill)

View File

@ -829,6 +829,15 @@ function Room:notifyMoveFocus(players, command)
table.insert(ids, p.id)
end
local tempSk = Fk.skills[command]
if tempSk and #players == 1 then
local p = players[1]
if p:isFakeSkill(tempSk) then
command = ""
ids = table.map(self.alive_players, Util.IdMapper)
end
end
self:doBroadcastNotify("MoveFocus", json.encode{
ids,
command
@ -2987,7 +2996,6 @@ function Room:useSkill(player, skill, effect_cb)
player:getSwitchSkillState(switchSkillName, true)
)
end
player:addSkillUseHistory(skill.name)
if effect_cb then
return execGameEvent(GameEvent.SkillEffect, effect_cb, player, skill)

View File

@ -14,6 +14,8 @@
---@field public phase_state table[]
---@field public phase_index integer
---@field public role_shown boolean
---@field private _fake_skills Skill[]
---@field public prelighted_skills Skill[]
---@field private _timewaste_count integer
---@field public ai AI
---@field public ai_data any
@ -35,6 +37,11 @@ function ServerPlayer:initialize(_self)
self.reply_cancel = false
self.phases = {}
self.skipped_phases = {}
self._fake_skills = {}
self.prelighted_skills = {}
self._prelighted_skills = {}
self._timewaste_count = 0
self.ai = RandomAI:new(self)
end
@ -702,13 +709,70 @@ end
-- Hegemony func
---@param skill Skill
function ServerPlayer:addFakeSkill(skill)
assert(skill:isInstanceOf(Skill))
if table.contains(self._fake_skills, skill) then return end
table.insert(self._fake_skills, skill)
for _, s in ipairs(skill.related_skills) do
-- if s.main_skill == skill then -- TODO: need more detailed
table.insert(self._fake_skills, s)
-- end
end
-- TODO
self:doNotify("AddSkill", json.encode{ self.id, skill.name, true })
end
---@param skill Skill
function ServerPlayer:loseFakeSkill(skill)
assert(skill:isInstanceOf(Skill))
if not table.contains(self._fake_skills, skill) then return end
table.removeOne(self._fake_skills, skill)
for _, s in ipairs(skill.related_skills) do
table.removeOne(self._fake_skills, s)
end
-- TODO
self:doNotify("LoseSkill", json.encode{ self.id, skill.name, true })
end
function ServerPlayer:isFakeSkill(skill)
if type(skill) == "string" then skill = Fk.skills[skill] end
assert(skill:isInstanceOf(Skill))
return table.contains(self._fake_skills, skill)
end
---@param skill string | Skill
---@param isPrelight boolean | nil
function ServerPlayer:prelightSkill(skill, isPrelight)
if isPrelight then
if type(skill) == "string" then skill = Fk.skills[skill] end
assert(skill:isInstanceOf(Skill))
if not self._prelighted_skills[skill] and not self:hasSkill(skill) then
self._prelighted_skills[skill] = true
-- to attach skill to room
self:addSkill(skill)
else
self:loseSkill(skill)
end
self:doNotify("PrelightSkill", json.encode{ skill, isPrelight })
if isPrelight then
-- self:addSkill(skill)
table.insert(self.prelighted_skills, skill)
for _, s in ipairs(skill.related_skills) do
table.insert(self.prelighted_skills, s)
end
else
-- self:loseSkill(skill)
table.removeOne(self.prelighted_skills, skill)
for _, s in ipairs(skill.related_skills) do
table.removeOne(self.prelighted_skills, s)
end
end
self:doNotify("PrelightSkill", json.encode{ skill.name, isPrelight })
end
function ServerPlayer:revealGeneral(isDeputy)
@ -725,10 +789,7 @@ function ServerPlayer:revealGeneral(isDeputy)
local general = Fk.generals[generalName]
for _, s in ipairs(general:getSkillNameList()) do
local skill = Fk.skills[s]
if skill:isInstanceOf(TriggerSkill) or table.find(skill.related_skills,
function(s) return s:isInstanceOf(TriggerSkill) end) then
self:doNotify("LoseSkill", json.encode{ self.id, s, true })
end
self:loseFakeSkill(skill)
end
local oldKingdom = self.kingdom

View File

@ -229,13 +229,7 @@ function HegLogic:attachSkillToPlayers()
return
end
-- room:handleAddLoseSkills(player, skillName, nil, false)
player:doNotify("AddSkill", json.encode{ player.id, skillName })
if skill:isInstanceOf(TriggerSkill) or table.find(skill.related_skills,
function(s) return s:isInstanceOf(TriggerSkill) end) then
player:doNotify("AddSkill", json.encode{ player.id, skillName, true })
end
player:addFakeSkill(skill)
end
for _, p in ipairs(room.alive_players) do

View File

@ -413,6 +413,7 @@ local nullificationSkill = fk.CreateActiveSkill{
can_use = function()
return false
end,
on_use = function() RoomInstance:delay(1200) end,
on_effect = function(self, room, effect)
if effect.responseToEvent then
effect.responseToEvent.isCancellOut = true

View File

@ -11,7 +11,12 @@ Replayer::Replayer(QObject *parent, const QString &filename) :
{
setObjectName("Replayer");
QFile file("recording/" + filename);
auto s = filename;
#ifdef Q_OS_WIN
if (s.startsWith("file:///"))
s.replace(0, 8, "file://");
#endif
QFile file(QUrl(s).path());
file.open(QIODevice::ReadOnly);
QByteArray raw = file.readAll();
file.close();

View File

@ -234,6 +234,7 @@ void Room::removePlayer(ServerPlayer *player) {
if (!gameStarted) {
// 游戏还没开始的话,直接删除这名玩家
if (players.contains(player) && !players.isEmpty()) {
player->setReady(false);
players.removeOne(player);
}
emit playerRemoved(player);
@ -533,10 +534,15 @@ void Room::gameOver() {
gameStarted = false;
runned_players.clear();
// 清理所有状态不是“在线”的玩家
auto settings = QJsonDocument::fromJson(this->settings);
auto mode = settings["gameMode"].toString();
foreach (ServerPlayer *p, players) {
if (p->getState() != Player::Online) {
if (p->getState() == Player::Offline) {
server->temporarilyBan(p->getId());
auto pid = p->getId();
addRunRate(pid, mode);
addRunRate(pid, mode);
server->temporarilyBan(pid);
}
p->deleteLater();
}

View File

@ -28,9 +28,9 @@ void Shell::helpCommand(QStringList &) {
HELP_MSG("%s: Crash the server. Useful when encounter dead loop.", "crash");
HELP_MSG("%s: List all online players.", "lsplayer");
HELP_MSG("%s: List all running rooms.", "lsroom");
HELP_MSG("%s: Reload server config file.", "reloadconf");
HELP_MSG("%s: Reload server config file.", "reloadconf/r");
HELP_MSG("%s: Kick a player by his <id>.", "kick");
HELP_MSG("%s: Broadcast message.", "msg");
HELP_MSG("%s: Broadcast message.", "msg/m");
HELP_MSG("%s: Ban 1 or more accounts, IP, UUID by their <name>.", "ban");
HELP_MSG("%s: Unban 1 or more accounts by their <name>.", "unban");
HELP_MSG(
@ -49,6 +49,7 @@ void Shell::helpCommand(QStringList &) {
"%s: Unban 1 or more UUID. "
"At least 1 <name> required.",
"unbanuuid");
HELP_MSG("%s: reset <name>'s password to 1234.", "resetpassword/rp");
qInfo();
qInfo("===== Package commands =====");
HELP_MSG("%s: Install a new package from <url>.", "install");
@ -56,7 +57,7 @@ void Shell::helpCommand(QStringList &) {
HELP_MSG("%s: List all packages.", "lspkg");
HELP_MSG("%s: Enable a package.", "enable");
HELP_MSG("%s: Disable a package.", "disable");
HELP_MSG("%s: Upgrade a package. Leave empty to upgrade all.", "upgrade");
HELP_MSG("%s: Upgrade a package. Leave empty to upgrade all.", "upgrade/u");
qInfo("For more commands, check the documentation.");
#undef HELP_MSG
@ -356,6 +357,21 @@ void Shell::reloadConfCommand(QStringList &) {
qInfo("Reloaded server config file.");
}
void Shell::resetPasswordCommand(QStringList &list) {
if (list.isEmpty()) {
qWarning("The 'resetpassword' command needs at least 1 <name>.");
return;
}
auto db = ServerInstance->getDatabase();
foreach (auto name, list) {
// 重置为1234
ExecSQL(db, QString("UPDATE userinfo SET password=\
'dbdc2ad3d9625407f55674a00b58904242545bfafedac67485ac398508403ade',\
salt='00000000' WHERE name='%1';").arg(name));
}
}
Shell::Shell() {
setObjectName("Shell");
signal(SIGINT, sigintHandler);
@ -369,11 +385,13 @@ Shell::Shell() {
handlers["install"] = &Shell::installCommand;
handlers["remove"] = &Shell::removeCommand;
handlers["upgrade"] = &Shell::upgradeCommand;
handlers["u"] = &Shell::upgradeCommand;
handlers["lspkg"] = &Shell::lspkgCommand;
handlers["enable"] = &Shell::enableCommand;
handlers["disable"] = &Shell::disableCommand;
handlers["kick"] = &Shell::kickCommand;
handlers["msg"] = &Shell::msgCommand;
handlers["m"] = &Shell::msgCommand;
handlers["ban"] = &Shell::banCommand;
handlers["unban"] = &Shell::unbanCommand;
handlers["banip"] = &Shell::banipCommand;
@ -381,6 +399,9 @@ Shell::Shell() {
handlers["banuuid"] = &Shell::banUuidCommand;
handlers["unbanuuid"] = &Shell::unbanUuidCommand;
handlers["reloadconf"] = &Shell::reloadConfCommand;
handlers["r"] = &Shell::reloadConfCommand;
handlers["resetpassword"] = &Shell::resetPasswordCommand;
handlers["rp"] = &Shell::resetPasswordCommand;
}
handler_map = handlers;
}

View File

@ -32,6 +32,7 @@ private:
void unbanipCommand(QStringList &);
void unbanUuidCommand(QStringList &);
void reloadConfCommand(QStringList &);
void resetPasswordCommand(QStringList &);
};
#endif