parent
1fcd63ddeb
commit
59c25c583c
|
@ -40,6 +40,7 @@ QtObject {
|
|||
property int roomTimeout: 0
|
||||
property bool enableFreeAssign: false
|
||||
property bool observing: false
|
||||
property bool replaying: false
|
||||
property var blockedUsers: []
|
||||
|
||||
function loadConf() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/*
|
||||
var generalsOverviewPage, cardsOverviewPage;
|
||||
var clientPageCreated = false;
|
||||
function createClientPages() {
|
||||
|
@ -13,6 +14,7 @@ function createClientPages() {
|
|||
mainWindow.cardsOverviewPage = cardsOverviewPage;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
var callbacks = {};
|
||||
let sheduled_download = "";
|
||||
|
@ -98,7 +100,7 @@ callbacks["BackToStart"] = (jsonData) => {
|
|||
|
||||
callbacks["EnterLobby"] = (jsonData) => {
|
||||
// depth == 1 means the lobby page is not present in mainStack
|
||||
createClientPages();
|
||||
// createClientPages();
|
||||
if (mainStack.depth === 1) {
|
||||
// we enter the lobby successfully, so save password now.
|
||||
config.lastLoginServer = config.serverAddr;
|
||||
|
|
|
@ -168,6 +168,7 @@ Item {
|
|||
lobby_dialog.sourceComponent = Qt.createComponent("../LobbyElement/CreateRoom.qml");
|
||||
lobby_drawer.open();
|
||||
config.observing = false;
|
||||
config.replaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,6 +198,9 @@ Item {
|
|||
}
|
||||
Button {
|
||||
text: Backend.translate("Replay")
|
||||
onClicked: {
|
||||
mainStack.push(mainWindow.replayPage);
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: Backend.translate("About")
|
||||
|
@ -279,6 +283,7 @@ Item {
|
|||
}
|
||||
|
||||
function enterRoom(roomId, playerNum, capacity, pw) {
|
||||
config.replaying = false;
|
||||
if (playerNum < capacity) {
|
||||
config.observing = false;
|
||||
Backend.callLuaFunction("SetObserving", [false]);
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Fk
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ToolBar {
|
||||
id: bar
|
||||
width: parent.width
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
ToolButton {
|
||||
icon.source: AppPath + "/image/modmaker/back"
|
||||
onClicked: mainStack.pop();
|
||||
}
|
||||
Label {
|
||||
text: Backend.translate("Replay Manager")
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ToolButton {
|
||||
icon.source: AppPath + "/image/modmaker/menu"
|
||||
onClicked: menu.open()
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
y: bar.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - bar.height
|
||||
anchors.top: bar.bottom
|
||||
color: "snow"
|
||||
opacity: 0.75
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
clip: true
|
||||
anchors.fill: parent
|
||||
model: ListModel {
|
||||
id: model
|
||||
}
|
||||
delegate: Item {
|
||||
width: root.width
|
||||
height: 64
|
||||
|
||||
Image {
|
||||
id: generalPic
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 8
|
||||
width: 48
|
||||
height: 48
|
||||
source: SkinBank.getGeneralExtraPic(general, "avatar/") ?? SkinBank.getGeneralPicture(general)
|
||||
sourceClipRect: sourceSize.width > 200 ? Qt.rect(61, 0, 128, 128) : undefined
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: generalPic.right
|
||||
anchors.margins: 8
|
||||
Text {
|
||||
text: {
|
||||
const win = winner.split("+").indexOf(role) !== -1;
|
||||
const winStr = win ? Backend.translate("Game Win") : Backend.translate("Game Lose");
|
||||
return "<b>" + Backend.translate(general) + "</b> " + Backend.translate(role) + " " + winStr;
|
||||
}
|
||||
font.pixelSize: 20
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
Text {
|
||||
text: {
|
||||
const y = repDate.slice(0,4);
|
||||
const month = repDate.slice(4,6);
|
||||
const d = repDate.slice(6,8);
|
||||
const h = repDate.slice(8,10);
|
||||
const m = repDate.slice(10,12);
|
||||
const s = repDate.slice(12,14);
|
||||
const dateStr = y + "-" + month + "-" + d + " " + h + ":" + m + ":" + s;
|
||||
|
||||
return playerName + " " + Backend.translate(gameMode) + " " + dateStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: replayBtn
|
||||
text: Backend.translate("Play the Replay")
|
||||
anchors.right: delBtn.left
|
||||
anchors.rightMargin: 8
|
||||
onClicked: {
|
||||
config.observing = true;
|
||||
config.replaying = true;
|
||||
Backend.playRecord(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: delBtn
|
||||
text: Backend.translate("Delete Replay")
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
onClicked: {
|
||||
Backend.removeRecord(fileName);
|
||||
removeModel(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateList() {
|
||||
model.clear();
|
||||
const data = Backend.ls("recording");
|
||||
data.reverse();
|
||||
data.forEach(s => {
|
||||
const d = s.split(".");
|
||||
if (d.length !== 8) return;
|
||||
// s: <time>.<screenName>.<mode>.<general>.<role>.<winner>.fk.rep
|
||||
const [t, name, mode, general, role, winner] = d;
|
||||
|
||||
model.append({
|
||||
fileName: s,
|
||||
repDate: t,
|
||||
playerName: name,
|
||||
gameMode: mode,
|
||||
general: general,
|
||||
role: role,
|
||||
winner: winner,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function removeModel(index) {
|
||||
model.remove(index);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateList();
|
||||
}
|
||||
}
|
|
@ -44,6 +44,10 @@ Item {
|
|||
property var extra_data: ({})
|
||||
property var skippedUseEventId: []
|
||||
|
||||
property real replayerSpeed
|
||||
property int replayerElapsed
|
||||
property int replayerDuration
|
||||
|
||||
Image {
|
||||
source: config.roomBg
|
||||
anchors.fill: parent
|
||||
|
@ -100,7 +104,7 @@ Item {
|
|||
|
||||
Button {
|
||||
id: surrenderButton
|
||||
enabled: !config.observing
|
||||
enabled: !config.observing && !config.replaying
|
||||
text: Backend.translate("Surrender")
|
||||
onClicked: {
|
||||
if (isStarted && !getPhoto(Self.id).dead) {
|
||||
|
@ -143,7 +147,10 @@ Item {
|
|||
id: quitButton
|
||||
text: Backend.translate("Quit")
|
||||
onClicked: {
|
||||
if (config.observing) {
|
||||
if (config.replaying) {
|
||||
Backend.controlReplayer("shutdown");
|
||||
mainStack.pop();
|
||||
} else if (config.observing) {
|
||||
ClientInstance.notifyServer("QuitRoom", "[]");
|
||||
} else {
|
||||
quitDialog.open();
|
||||
|
@ -437,12 +444,72 @@ Item {
|
|||
|
||||
GlowText {
|
||||
text: Backend.translate("Observing ...")
|
||||
visible: config.observing
|
||||
visible: config.observing && !config.replaying
|
||||
color: "#4B83CD"
|
||||
font.family: fontLi2.name
|
||||
font.pixelSize: 48
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: replayControls
|
||||
visible: config.replaying
|
||||
anchors.bottom: dashboard.top
|
||||
anchors.bottomMargin: -60
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: childrenRect.width + 8
|
||||
height: childrenRect.height + 8
|
||||
|
||||
color: "#88EEEEEE"
|
||||
radius: 4
|
||||
|
||||
RowLayout {
|
||||
x: 4; y: 4
|
||||
Text {
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
text: {
|
||||
const elapsedMin = Math.floor(replayerElapsed / 60);
|
||||
const elapsedSec = replayerElapsed % 60;
|
||||
const totalMin = Math.floor(replayerDuration / 60);
|
||||
const totalSec = replayerDuration % 60;
|
||||
|
||||
return elapsedMin.toString() + ":" + elapsedSec + "/" + totalMin + ":" + totalSec;
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
text: "匀速"
|
||||
checked: false
|
||||
onCheckedChanged: Backend.controlReplayer("uniform");
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "减速"
|
||||
onClicked: Backend.controlReplayer("slowdown");
|
||||
}
|
||||
|
||||
Text {
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
text: "x" + replayerSpeed;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "加速"
|
||||
onClicked: Backend.controlReplayer("speedup");
|
||||
}
|
||||
|
||||
Button {
|
||||
property bool running: true
|
||||
text: running ? "暂停" : "继续"
|
||||
onClicked: {
|
||||
running = !running;
|
||||
Backend.controlReplayer("toggle");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: controls
|
||||
anchors.bottom: dashboard.top
|
||||
|
@ -721,6 +788,7 @@ Item {
|
|||
}
|
||||
}
|
||||
Item {
|
||||
visible: !config.replaying
|
||||
ChatBox {
|
||||
id: chat
|
||||
anchors.fill: parent
|
||||
|
|
|
@ -1383,6 +1383,7 @@ callbacks["ChangeSelf"] = (j) => {
|
|||
|
||||
callbacks["AskForLuckCard"] = (j) => {
|
||||
// jsonData: int time
|
||||
if (config.replaying) return;
|
||||
const time = parseInt(j);
|
||||
roomScene.setPrompt(Backend.translate("#AskForLuckCard").arg(time), true);
|
||||
roomScene.state = "replying";
|
||||
|
@ -1398,3 +1399,15 @@ callbacks["AskForLuckCard"] = (j) => {
|
|||
callbacks["CancelRequest"] = (jsonData) => {
|
||||
ClientInstance.replyToServer("", "__cancel")
|
||||
}
|
||||
|
||||
callbacks["ReplayerDurationSet"] = (j) => {
|
||||
roomScene.replayerDuration = parseInt(j);
|
||||
}
|
||||
|
||||
callbacks["ReplayerElapsedChange"] = (j) => {
|
||||
roomScene.replayerElapsed = parseInt(j);
|
||||
}
|
||||
|
||||
callbacks["ReplayerSpeedChange"] = (j) => {
|
||||
roomScene.replayerSpeed = parseFloat(j);
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@ MetroButton 1.0 MetroButton.qml
|
|||
ModesOverview 1.0 ModesOverview.qml
|
||||
PackageManage 1.0 PackageManage.qml
|
||||
Room 1.0 Room.qml
|
||||
Replay 1.0 Replay.qml
|
||||
TileButton 1.0 TileButton.qml
|
||||
ModMaker 1.0 ModMaker.qml
|
||||
|
|
|
@ -38,8 +38,26 @@ GraphicsBox {
|
|||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
onClicked: {
|
||||
if (config.replaying) {
|
||||
mainStack.pop();
|
||||
Backend.controlReplayer("shutdown");
|
||||
} else {
|
||||
ClientInstance.notifyServer("QuitRoom", "[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MetroButton {
|
||||
id: repBtn
|
||||
text: Backend.translate("Save Replay")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !config.replaying
|
||||
|
||||
onClicked: {
|
||||
repBtn.visible = false;
|
||||
Backend.callLuaFunction("SaveRecord", []);
|
||||
toast.show("OK.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,13 +57,15 @@ Window {
|
|||
Component { id: generalsOverview; GeneralsOverview {} }
|
||||
Component { id: cardsOverview; CardsOverview {} }
|
||||
Component { id: modesOverview; ModesOverview {} }
|
||||
Component { id: replay; Replay {} }
|
||||
Component { id: room; Room {} }
|
||||
Component { id: aboutPage; About {} }
|
||||
|
||||
property var generalsOverviewPage
|
||||
property var cardsOverviewPage
|
||||
property alias generalsOverviewPage: generalsOverview
|
||||
property alias cardsOverviewPage: cardsOverview
|
||||
property alias modesOverviewPage: modesOverview
|
||||
property alias aboutPage: aboutPage
|
||||
property alias replayPage: replay
|
||||
property bool busy: false
|
||||
property string busyText: ""
|
||||
onBusyChanged: busyText = "";
|
||||
|
|
|
@ -30,8 +30,8 @@ function Client:initialize()
|
|||
fk.Backend:emitNotifyUI(command, jsonData)
|
||||
end
|
||||
self.client.callback = function(_self, command, jsonData, isRequest)
|
||||
if self.recording and not self.observing then
|
||||
table.insert(self.record, {os.getms() / 1000, isRequest, command, jsonData})
|
||||
if self.recording then
|
||||
table.insert(self.record, {math.floor(os.getms() / 1000), isRequest, command, jsonData})
|
||||
end
|
||||
|
||||
local cb = fk.client_callback[command]
|
||||
|
@ -256,7 +256,9 @@ fk.client_callback["EnterRoom"] = function(jsonData)
|
|||
ClientInstance.alive_players = {Self}
|
||||
ClientInstance.discard_pile = {}
|
||||
|
||||
local data = json.decode(jsonData)[3]
|
||||
local _data = json.decode(jsonData)
|
||||
local data = _data[3]
|
||||
ClientInstance.enter_room_data = jsonData;
|
||||
ClientInstance.room_settings = data
|
||||
ClientInstance.disabled_packs = data.disabledPack
|
||||
ClientInstance.disabled_generals = data.disabledGenerals
|
||||
|
@ -793,11 +795,23 @@ end
|
|||
|
||||
fk.client_callback["StartGame"] = function(jsonData)
|
||||
local c = ClientInstance
|
||||
c.record = { fk.FK_VER, os.date("%Y%m%d%H%M%S") }
|
||||
c.record = {
|
||||
fk.FK_VER,
|
||||
os.date("%Y%m%d%H%M%S"),
|
||||
c.enter_room_data,
|
||||
json.encode { Self.id, fk.Self:getScreenName(), fk.Self:getAvatar() },
|
||||
-- RESERVED
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
}
|
||||
for _, p in ipairs(c.players) do
|
||||
if p.id ~= Self.id then
|
||||
table.insert(c.record, {
|
||||
os.getms() / 100,
|
||||
math.floor(os.getms() / 1000),
|
||||
false,
|
||||
"AddPlayer",
|
||||
json.encode {
|
||||
|
@ -815,7 +829,7 @@ end
|
|||
|
||||
fk.client_callback["GameOver"] = function(jsonData)
|
||||
local c = ClientInstance
|
||||
if c.recording and not c.observing then
|
||||
if c.recording then
|
||||
c.recording = false
|
||||
c.record[2] = table.concat({
|
||||
c.record[2],
|
||||
|
@ -832,6 +846,7 @@ end
|
|||
|
||||
fk.client_callback["EnterLobby"] = function(jsonData)
|
||||
local c = ClientInstance
|
||||
--[[
|
||||
if c.recording and not c.observing then
|
||||
c.recording = false
|
||||
c.record[2] = table.concat({
|
||||
|
@ -844,6 +859,7 @@ fk.client_callback["EnterLobby"] = function(jsonData)
|
|||
}, ".")
|
||||
-- c.client:saveRecord(json.encode(c.record), c.record[2])
|
||||
end
|
||||
--]]
|
||||
c:notifyUI("EnterLobby", jsonData)
|
||||
end
|
||||
|
||||
|
|
|
@ -640,4 +640,9 @@ function CheckSurrenderAvailable(playedTime)
|
|||
return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime))
|
||||
end
|
||||
|
||||
function SaveRecord()
|
||||
local c = ClientInstance
|
||||
c.client:saveRecord(json.encode(c.record), c.record[2])
|
||||
end
|
||||
|
||||
dofile "lua/client/i18n/init.lua"
|
||||
|
|
|
@ -72,6 +72,11 @@ Fk:loadTranslationTable{
|
|||
["Every suit & number:"] = "<b>所有的花色和点数:</b>",
|
||||
["Scenarios Overview"] = "玩法一览",
|
||||
["Replay"] = "录像",
|
||||
["Replay Manager"] = "来欣赏潇洒的录像吧!",
|
||||
["Game Win"] = "胜利",
|
||||
["Game Lose"] = "失败",
|
||||
["Play the Replay"] = "重放",
|
||||
["Delete Replay"] = "删除",
|
||||
["About"] = "关于",
|
||||
["about_freekill_description"] = [[
|
||||
# 关于新月杀
|
||||
|
@ -237,6 +242,7 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下
|
|||
["$NoWinner"] = "平局!",
|
||||
["Back To Room"] = "回到房间",
|
||||
["Back To Lobby"] = "返回大厅",
|
||||
["Save Replay"] = "保存录像",
|
||||
|
||||
["Bulletin Info"] = [==[
|
||||
## v0.2.7
|
||||
|
|
|
@ -20,6 +20,7 @@ if (NOT DEFINED FK_SERVER_ONLY)
|
|||
list(APPEND freekill_SRCS
|
||||
"client/client.cpp"
|
||||
"client/clientplayer.cpp"
|
||||
"client/replayer.cpp"
|
||||
"ui/mod.cpp"
|
||||
)
|
||||
endif ()
|
||||
|
|
|
@ -89,3 +89,7 @@ void Client::saveRecord(const QString &json, const QString &fname) {
|
|||
c.write(qCompress(json.toUtf8()));
|
||||
c.close();
|
||||
}
|
||||
|
||||
void Client::processReplay(const QString &c, const QString &j) {
|
||||
callLua(c, j);
|
||||
}
|
||||
|
|
|
@ -33,9 +33,13 @@ public:
|
|||
void installAESKey(const QByteArray &key);
|
||||
|
||||
void saveRecord(const QString &json, const QString &fname);
|
||||
|
||||
signals:
|
||||
void error_message(const QString &msg);
|
||||
|
||||
public slots:
|
||||
void processReplay(const QString &, const QString &);
|
||||
|
||||
private:
|
||||
Router *router;
|
||||
QMap<int, ClientPlayer *> players;
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "replayer.h"
|
||||
#include "client.h"
|
||||
#include "qmlbackend.h"
|
||||
#include "util.h"
|
||||
|
||||
Replayer::Replayer(QObject *parent, const QString &filename) :
|
||||
QThread(parent), fileName(filename), roomSettings(""), origPlayerInfo(""),
|
||||
playing(true), killed(false), speed(1.0), uniformRunning(false)
|
||||
{
|
||||
setObjectName("Replayer");
|
||||
|
||||
QFile file("recording/" + filename);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
|
||||
auto data = qUncompress(raw);
|
||||
|
||||
auto doc = QJsonDocument::fromJson(data);
|
||||
auto arr = doc.array();
|
||||
if (arr.count() < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ver = arr[0].toString();
|
||||
if (ver != FK_VERSION) {
|
||||
Backend->showToast("Warning: Mismatch version of replay detected, which may cause crashes.");
|
||||
}
|
||||
|
||||
roomSettings = arr[2].toString();
|
||||
|
||||
foreach (auto v, arr) {
|
||||
if (!v.isArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto a = v.toArray();
|
||||
Pair *pair = new Pair;
|
||||
pair->elapsed = a[0].toInteger();
|
||||
pair->isRequest = a[1].toBool();
|
||||
pair->cmd = a[2].toString();
|
||||
pair->jsonData = a[3].toString();
|
||||
pairs << pair;
|
||||
}
|
||||
|
||||
connect(this, &Replayer::command_parsed, ClientInstance, &Client::processReplay);
|
||||
|
||||
auto playerInfoRaw = arr[3].toString();
|
||||
auto playerInfo = QJsonDocument::fromJson(playerInfoRaw.toUtf8()).array();
|
||||
if (playerInfo[0].toInt() != Self->getId()) {
|
||||
origPlayerInfo = JsonArray2Bytes({ Self->getId(), Self->getScreenName(), Self->getAvatar() });
|
||||
emit command_parsed("Setup", playerInfoRaw);
|
||||
}
|
||||
}
|
||||
|
||||
Replayer::~Replayer() {
|
||||
if (origPlayerInfo != "") {
|
||||
emit command_parsed("Setup", origPlayerInfo);
|
||||
}
|
||||
Backend->setReplayer(nullptr);
|
||||
foreach (auto e, pairs) {
|
||||
delete e;
|
||||
}
|
||||
}
|
||||
|
||||
int Replayer::getDuration() const {
|
||||
long ret = (pairs.last()->elapsed - pairs.first()->elapsed) / 1000.0;
|
||||
return (int)ret;
|
||||
}
|
||||
|
||||
qreal Replayer::getSpeed() {
|
||||
qreal speed;
|
||||
mutex.lock();
|
||||
speed = this->speed;
|
||||
mutex.unlock();
|
||||
return speed;
|
||||
}
|
||||
|
||||
void Replayer::uniform() {
|
||||
mutex.lock();
|
||||
|
||||
uniformRunning = !uniformRunning;
|
||||
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
void Replayer::speedUp() {
|
||||
mutex.lock();
|
||||
|
||||
if (speed < 16.0) {
|
||||
qreal inc = speed >= 2.0 ? 1.0 : 0.5;
|
||||
speed += inc;
|
||||
emit speed_changed(speed);
|
||||
}
|
||||
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
void Replayer::slowDown() {
|
||||
mutex.lock();
|
||||
|
||||
if (speed >= 1.0) {
|
||||
qreal dec = speed > 2.0 ? 1.0 : 0.5;
|
||||
speed -= dec;
|
||||
emit speed_changed(speed);
|
||||
}
|
||||
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
void Replayer::toggle() {
|
||||
playing = !playing;
|
||||
if (playing)
|
||||
play_sem.release();
|
||||
}
|
||||
|
||||
void Replayer::shutdown() {
|
||||
killed = true;
|
||||
}
|
||||
|
||||
void Replayer::run() {
|
||||
long last = 0;
|
||||
long start = 0;
|
||||
|
||||
if (roomSettings == "") {
|
||||
Backend->showToast("Invalid replay file.");
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
emit command_parsed("EnterRoom", roomSettings);
|
||||
emit command_parsed("StartGame", "");
|
||||
|
||||
emit speed_changed(getSpeed());
|
||||
emit duration_set(getDuration());
|
||||
|
||||
foreach (auto pair, pairs) {
|
||||
if (killed) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (pair->isRequest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long delay = pair->elapsed - last;
|
||||
if (uniformRunning) {
|
||||
delay = qMin(delay, 2000);
|
||||
if (delay > 500)
|
||||
delay = 2000;
|
||||
} else if (last == 0) {
|
||||
delay = 100;
|
||||
}
|
||||
last = pair->elapsed;
|
||||
if (start == 0) start = last;
|
||||
|
||||
bool delayed = true;
|
||||
|
||||
if (!pair->isRequest) {
|
||||
delay /= getSpeed();
|
||||
|
||||
msleep(delay);
|
||||
emit elasped((pair->elapsed - start) / 1000);
|
||||
|
||||
emit command_parsed(pair->cmd, pair->jsonData);
|
||||
|
||||
if (!playing)
|
||||
play_sem.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
deleteLater();
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef _REPLAYER_H
|
||||
#define _REPLAYER_H
|
||||
|
||||
class Replayer : public QThread {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Replayer(QObject *parent, const QString &filename);
|
||||
~Replayer();
|
||||
|
||||
int getDuration() const;
|
||||
qreal getSpeed();
|
||||
|
||||
signals:
|
||||
void duration_set(int secs);
|
||||
void elasped(int secs);
|
||||
void speed_changed(qreal speed);
|
||||
void command_parsed(const QString &cmd, const QString &j);
|
||||
|
||||
public slots:
|
||||
void uniform();
|
||||
void toggle();
|
||||
void speedUp();
|
||||
void slowDown();
|
||||
void shutdown();
|
||||
|
||||
protected:
|
||||
virtual void run();
|
||||
|
||||
private:
|
||||
QString fileName;
|
||||
qreal speed;
|
||||
bool playing;
|
||||
bool killed;
|
||||
bool uniformRunning;
|
||||
QString roomSettings;
|
||||
QString origPlayerInfo;
|
||||
QMutex mutex;
|
||||
QSemaphore play_sem;
|
||||
|
||||
struct Pair {
|
||||
long elapsed;
|
||||
bool isRequest;
|
||||
QString cmd;
|
||||
QString jsonData;
|
||||
};
|
||||
QList<Pair *> pairs;
|
||||
};
|
||||
|
||||
#endif // _REPLAYER_H
|
|
@ -107,7 +107,7 @@ sqlite3 *OpenDatabase(const QString &filename, const QString &initSql) {
|
|||
}
|
||||
|
||||
bool CheckSqlString(const QString &str) {
|
||||
static const QRegularExpression exp("['\";#]+|(--)|(/\\*)|(\\*/)|(--\\+)");
|
||||
static const QRegularExpression exp("['\";#* /\\\\?<>|:]+|(--)|(/\\*)|(\\*/)|(--\\+)");
|
||||
return (!exp.match(str).hasMatch() && !str.isEmpty());
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#endif
|
||||
#include "client.h"
|
||||
#include "util.h"
|
||||
#include "replayer.h"
|
||||
|
||||
QmlBackend *Backend = nullptr;
|
||||
|
||||
|
@ -27,6 +28,7 @@ QmlBackend::QmlBackend(QObject *parent) : QObject(parent) {
|
|||
Backend = this;
|
||||
#ifndef FK_SERVER_ONLY
|
||||
engine = nullptr;
|
||||
replayer = nullptr;
|
||||
rsa = RSA_new();
|
||||
udpSocket = new QUdpSocket(this);
|
||||
udpSocket->bind(0);
|
||||
|
@ -95,6 +97,9 @@ void QmlBackend::joinServer(QString address) {
|
|||
return;
|
||||
Client *client = new Client(this);
|
||||
connect(client, &Client::error_message, this, [=](const QString &msg) {
|
||||
if (replayer) {
|
||||
emit replayerShutdown();
|
||||
}
|
||||
client->deleteLater();
|
||||
emit notifyUI("ErrorMsg", msg);
|
||||
emit notifyUI("BackToStart", "[]");
|
||||
|
@ -181,6 +186,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
|
|||
QString QmlBackend::callLuaFunction(const QString &func_name,
|
||||
QVariantList params) {
|
||||
if (!ClientInstance) return "{}";
|
||||
|
||||
lua_State *L = ClientInstance->getLuaState();
|
||||
lua_getglobal(L, func_name.toLatin1().data());
|
||||
|
||||
|
@ -196,6 +202,7 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
|
|||
return "";
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
return QString(result);
|
||||
}
|
||||
|
||||
|
@ -373,4 +380,57 @@ void QmlBackend::readPendingDatagrams() {
|
|||
}
|
||||
}
|
||||
|
||||
void QmlBackend::removeRecord(const QString &fname) {
|
||||
QFile::remove("recording/" + fname);
|
||||
}
|
||||
|
||||
void QmlBackend::playRecord(const QString &fname) {
|
||||
auto replayer = new Replayer(this, fname);
|
||||
setReplayer(replayer);
|
||||
replayer->start();
|
||||
}
|
||||
|
||||
Replayer *QmlBackend::getReplayer() const {
|
||||
return replayer;
|
||||
}
|
||||
|
||||
void QmlBackend::setReplayer(Replayer *rep) {
|
||||
auto r = replayer;
|
||||
if (r) {
|
||||
r->disconnect(this);
|
||||
disconnect(r);
|
||||
}
|
||||
replayer = rep;
|
||||
if (rep) {
|
||||
connect(rep, &Replayer::duration_set, this, [this](int sec) {
|
||||
this->emitNotifyUI("ReplayerDurationSet", QString::number(sec));
|
||||
});
|
||||
connect(rep, &Replayer::elasped, this, [this](int sec) {
|
||||
this->emitNotifyUI("ReplayerElapsedChange", QString::number(sec));
|
||||
});
|
||||
connect(rep, &Replayer::speed_changed, this, [this](qreal speed) {
|
||||
this->emitNotifyUI("ReplayerSpeedChange", QString::number(speed));
|
||||
});
|
||||
connect(this, &QmlBackend::replayerToggle, rep, &Replayer::toggle);
|
||||
connect(this, &QmlBackend::replayerSlowDown, rep, &Replayer::slowDown);
|
||||
connect(this, &QmlBackend::replayerSpeedUp, rep, &Replayer::speedUp);
|
||||
connect(this, &QmlBackend::replayerUniform, rep, &Replayer::uniform);
|
||||
connect(this, &QmlBackend::replayerShutdown, rep, &Replayer::shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
void QmlBackend::controlReplayer(QString type) {
|
||||
if (type == "toggle") {
|
||||
emit replayerToggle();
|
||||
} else if (type == "speedup") {
|
||||
emit replayerSpeedUp();
|
||||
} else if (type == "slowdown") {
|
||||
emit replayerSlowDown();
|
||||
} else if (type == "uniform") {
|
||||
emit replayerUniform();
|
||||
} else if (type == "shutdown") {
|
||||
emit replayerShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include <openssl/rsa.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
class Replayer;
|
||||
|
||||
#include <qtmetamacros.h>
|
||||
class QmlBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
@ -63,9 +65,20 @@ public:
|
|||
|
||||
void showToast(const QString &s) { emit notifyUI("ShowToast", s); }
|
||||
|
||||
Q_INVOKABLE void removeRecord(const QString &);
|
||||
Q_INVOKABLE void playRecord(const QString &);
|
||||
Replayer *getReplayer() const;
|
||||
void setReplayer(Replayer *rep);
|
||||
Q_INVOKABLE void controlReplayer(QString type);
|
||||
|
||||
signals:
|
||||
void notifyUI(const QString &command, const QString &jsonData);
|
||||
void volumeChanged(qreal);
|
||||
void replayerToggle();
|
||||
void replayerSpeedUp();
|
||||
void replayerSlowDown();
|
||||
void replayerUniform();
|
||||
void replayerShutdown();
|
||||
|
||||
private slots:
|
||||
void readPendingDatagrams();
|
||||
|
@ -80,6 +93,8 @@ private:
|
|||
QString aes_key;
|
||||
qreal m_volume;
|
||||
|
||||
Replayer *replayer;
|
||||
|
||||
void pushLuaValue(lua_State *L, QVariant v);
|
||||
#endif
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue