录像保存. 重放.
This commit is contained in:
notify 2023-08-01 21:01:01 +08:00 committed by GitHub
parent 1fcd63ddeb
commit 59c25c583c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 617 additions and 14 deletions

View File

@ -40,6 +40,7 @@ QtObject {
property int roomTimeout: 0 property int roomTimeout: 0
property bool enableFreeAssign: false property bool enableFreeAssign: false
property bool observing: false property bool observing: false
property bool replaying: false
property var blockedUsers: [] property var blockedUsers: []
function loadConf() { function loadConf() {

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
/*
var generalsOverviewPage, cardsOverviewPage; var generalsOverviewPage, cardsOverviewPage;
var clientPageCreated = false; var clientPageCreated = false;
function createClientPages() { function createClientPages() {
@ -13,6 +14,7 @@ function createClientPages() {
mainWindow.cardsOverviewPage = cardsOverviewPage; mainWindow.cardsOverviewPage = cardsOverviewPage;
} }
} }
*/
var callbacks = {}; var callbacks = {};
let sheduled_download = ""; let sheduled_download = "";
@ -98,7 +100,7 @@ callbacks["BackToStart"] = (jsonData) => {
callbacks["EnterLobby"] = (jsonData) => { callbacks["EnterLobby"] = (jsonData) => {
// depth == 1 means the lobby page is not present in mainStack // depth == 1 means the lobby page is not present in mainStack
createClientPages(); // createClientPages();
if (mainStack.depth === 1) { if (mainStack.depth === 1) {
// we enter the lobby successfully, so save password now. // we enter the lobby successfully, so save password now.
config.lastLoginServer = config.serverAddr; config.lastLoginServer = config.serverAddr;

View File

@ -168,6 +168,7 @@ Item {
lobby_dialog.sourceComponent = Qt.createComponent("../LobbyElement/CreateRoom.qml"); lobby_dialog.sourceComponent = Qt.createComponent("../LobbyElement/CreateRoom.qml");
lobby_drawer.open(); lobby_drawer.open();
config.observing = false; config.observing = false;
config.replaying = false;
} }
} }
@ -197,6 +198,9 @@ Item {
} }
Button { Button {
text: Backend.translate("Replay") text: Backend.translate("Replay")
onClicked: {
mainStack.push(mainWindow.replayPage);
}
} }
Button { Button {
text: Backend.translate("About") text: Backend.translate("About")
@ -279,6 +283,7 @@ Item {
} }
function enterRoom(roomId, playerNum, capacity, pw) { function enterRoom(roomId, playerNum, capacity, pw) {
config.replaying = false;
if (playerNum < capacity) { if (playerNum < capacity) {
config.observing = false; config.observing = false;
Backend.callLuaFunction("SetObserving", [false]); Backend.callLuaFunction("SetObserving", [false]);

155
Fk/Pages/Replay.qml Normal file
View File

@ -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();
}
}

View File

@ -44,6 +44,10 @@ Item {
property var extra_data: ({}) property var extra_data: ({})
property var skippedUseEventId: [] property var skippedUseEventId: []
property real replayerSpeed
property int replayerElapsed
property int replayerDuration
Image { Image {
source: config.roomBg source: config.roomBg
anchors.fill: parent anchors.fill: parent
@ -100,7 +104,7 @@ Item {
Button { Button {
id: surrenderButton id: surrenderButton
enabled: !config.observing enabled: !config.observing && !config.replaying
text: Backend.translate("Surrender") text: Backend.translate("Surrender")
onClicked: { onClicked: {
if (isStarted && !getPhoto(Self.id).dead) { if (isStarted && !getPhoto(Self.id).dead) {
@ -143,7 +147,10 @@ Item {
id: quitButton id: quitButton
text: Backend.translate("Quit") text: Backend.translate("Quit")
onClicked: { onClicked: {
if (config.observing) { if (config.replaying) {
Backend.controlReplayer("shutdown");
mainStack.pop();
} else if (config.observing) {
ClientInstance.notifyServer("QuitRoom", "[]"); ClientInstance.notifyServer("QuitRoom", "[]");
} else { } else {
quitDialog.open(); quitDialog.open();
@ -437,12 +444,72 @@ Item {
GlowText { GlowText {
text: Backend.translate("Observing ...") text: Backend.translate("Observing ...")
visible: config.observing visible: config.observing && !config.replaying
color: "#4B83CD" color: "#4B83CD"
font.family: fontLi2.name font.family: fontLi2.name
font.pixelSize: 48 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 { Item {
id: controls id: controls
anchors.bottom: dashboard.top anchors.bottom: dashboard.top
@ -721,6 +788,7 @@ Item {
} }
} }
Item { Item {
visible: !config.replaying
ChatBox { ChatBox {
id: chat id: chat
anchors.fill: parent anchors.fill: parent

View File

@ -1383,6 +1383,7 @@ callbacks["ChangeSelf"] = (j) => {
callbacks["AskForLuckCard"] = (j) => { callbacks["AskForLuckCard"] = (j) => {
// jsonData: int time // jsonData: int time
if (config.replaying) return;
const time = parseInt(j); const time = parseInt(j);
roomScene.setPrompt(Backend.translate("#AskForLuckCard").arg(time), true); roomScene.setPrompt(Backend.translate("#AskForLuckCard").arg(time), true);
roomScene.state = "replying"; roomScene.state = "replying";
@ -1398,3 +1399,15 @@ callbacks["AskForLuckCard"] = (j) => {
callbacks["CancelRequest"] = (jsonData) => { callbacks["CancelRequest"] = (jsonData) => {
ClientInstance.replyToServer("", "__cancel") ClientInstance.replyToServer("", "__cancel")
} }
callbacks["ReplayerDurationSet"] = (j) => {
roomScene.replayerDuration = parseInt(j);
}
callbacks["ReplayerElapsedChange"] = (j) => {
roomScene.replayerElapsed = parseInt(j);
}
callbacks["ReplayerSpeedChange"] = (j) => {
roomScene.replayerSpeed = parseFloat(j);
}

View File

@ -9,5 +9,6 @@ MetroButton 1.0 MetroButton.qml
ModesOverview 1.0 ModesOverview.qml ModesOverview 1.0 ModesOverview.qml
PackageManage 1.0 PackageManage.qml PackageManage 1.0 PackageManage.qml
Room 1.0 Room.qml Room 1.0 Room.qml
Replay 1.0 Replay.qml
TileButton 1.0 TileButton.qml TileButton 1.0 TileButton.qml
ModMaker 1.0 ModMaker.qml ModMaker 1.0 ModMaker.qml

View File

@ -38,7 +38,25 @@ GraphicsBox {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onClicked: { onClicked: {
ClientInstance.notifyServer("QuitRoom", "[]"); 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.");
} }
} }
} }

View File

@ -57,13 +57,15 @@ Window {
Component { id: generalsOverview; GeneralsOverview {} } Component { id: generalsOverview; GeneralsOverview {} }
Component { id: cardsOverview; CardsOverview {} } Component { id: cardsOverview; CardsOverview {} }
Component { id: modesOverview; ModesOverview {} } Component { id: modesOverview; ModesOverview {} }
Component { id: replay; Replay {} }
Component { id: room; Room {} } Component { id: room; Room {} }
Component { id: aboutPage; About {} } Component { id: aboutPage; About {} }
property var generalsOverviewPage property alias generalsOverviewPage: generalsOverview
property var cardsOverviewPage property alias cardsOverviewPage: cardsOverview
property alias modesOverviewPage: modesOverview property alias modesOverviewPage: modesOverview
property alias aboutPage: aboutPage property alias aboutPage: aboutPage
property alias replayPage: replay
property bool busy: false property bool busy: false
property string busyText: "" property string busyText: ""
onBusyChanged: busyText = ""; onBusyChanged: busyText = "";

View File

@ -30,8 +30,8 @@ function Client:initialize()
fk.Backend:emitNotifyUI(command, jsonData) fk.Backend:emitNotifyUI(command, jsonData)
end end
self.client.callback = function(_self, command, jsonData, isRequest) self.client.callback = function(_self, command, jsonData, isRequest)
if self.recording and not self.observing then if self.recording then
table.insert(self.record, {os.getms() / 1000, isRequest, command, jsonData}) table.insert(self.record, {math.floor(os.getms() / 1000), isRequest, command, jsonData})
end end
local cb = fk.client_callback[command] local cb = fk.client_callback[command]
@ -256,7 +256,9 @@ fk.client_callback["EnterRoom"] = function(jsonData)
ClientInstance.alive_players = {Self} ClientInstance.alive_players = {Self}
ClientInstance.discard_pile = {} 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.room_settings = data
ClientInstance.disabled_packs = data.disabledPack ClientInstance.disabled_packs = data.disabledPack
ClientInstance.disabled_generals = data.disabledGenerals ClientInstance.disabled_generals = data.disabledGenerals
@ -793,11 +795,23 @@ end
fk.client_callback["StartGame"] = function(jsonData) fk.client_callback["StartGame"] = function(jsonData)
local c = ClientInstance 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 for _, p in ipairs(c.players) do
if p.id ~= Self.id then if p.id ~= Self.id then
table.insert(c.record, { table.insert(c.record, {
os.getms() / 100, math.floor(os.getms() / 1000),
false, false,
"AddPlayer", "AddPlayer",
json.encode { json.encode {
@ -815,7 +829,7 @@ end
fk.client_callback["GameOver"] = function(jsonData) fk.client_callback["GameOver"] = function(jsonData)
local c = ClientInstance local c = ClientInstance
if c.recording and not c.observing then if c.recording then
c.recording = false c.recording = false
c.record[2] = table.concat({ c.record[2] = table.concat({
c.record[2], c.record[2],
@ -832,6 +846,7 @@ end
fk.client_callback["EnterLobby"] = function(jsonData) fk.client_callback["EnterLobby"] = function(jsonData)
local c = ClientInstance local c = ClientInstance
--[[
if c.recording and not c.observing then if c.recording and not c.observing then
c.recording = false c.recording = false
c.record[2] = table.concat({ 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]) -- c.client:saveRecord(json.encode(c.record), c.record[2])
end end
--]]
c:notifyUI("EnterLobby", jsonData) c:notifyUI("EnterLobby", jsonData)
end end

View File

@ -640,4 +640,9 @@ function CheckSurrenderAvailable(playedTime)
return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime)) return json.encode(Fk.game_modes[curMode]:surrenderFunc(playedTime))
end end
function SaveRecord()
local c = ClientInstance
c.client:saveRecord(json.encode(c.record), c.record[2])
end
dofile "lua/client/i18n/init.lua" dofile "lua/client/i18n/init.lua"

View File

@ -72,6 +72,11 @@ Fk:loadTranslationTable{
["Every suit & number:"] = "<b>所有的花色和点数:</b>", ["Every suit & number:"] = "<b>所有的花色和点数:</b>",
["Scenarios Overview"] = "玩法一览", ["Scenarios Overview"] = "玩法一览",
["Replay"] = "录像", ["Replay"] = "录像",
["Replay Manager"] = "来欣赏潇洒的录像吧!",
["Game Win"] = "胜利",
["Game Lose"] = "失败",
["Play the Replay"] = "重放",
["Delete Replay"] = "删除",
["About"] = "关于", ["About"] = "关于",
["about_freekill_description"] = [[ ["about_freekill_description"] = [[
# #
@ -237,6 +242,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["$NoWinner"] = "平局!", ["$NoWinner"] = "平局!",
["Back To Room"] = "回到房间", ["Back To Room"] = "回到房间",
["Back To Lobby"] = "返回大厅", ["Back To Lobby"] = "返回大厅",
["Save Replay"] = "保存录像",
["Bulletin Info"] = [==[ ["Bulletin Info"] = [==[
## v0.2.7 ## v0.2.7

View File

@ -20,6 +20,7 @@ if (NOT DEFINED FK_SERVER_ONLY)
list(APPEND freekill_SRCS list(APPEND freekill_SRCS
"client/client.cpp" "client/client.cpp"
"client/clientplayer.cpp" "client/clientplayer.cpp"
"client/replayer.cpp"
"ui/mod.cpp" "ui/mod.cpp"
) )
endif () endif ()

View File

@ -89,3 +89,7 @@ void Client::saveRecord(const QString &json, const QString &fname) {
c.write(qCompress(json.toUtf8())); c.write(qCompress(json.toUtf8()));
c.close(); c.close();
} }
void Client::processReplay(const QString &c, const QString &j) {
callLua(c, j);
}

View File

@ -33,9 +33,13 @@ public:
void installAESKey(const QByteArray &key); void installAESKey(const QByteArray &key);
void saveRecord(const QString &json, const QString &fname); void saveRecord(const QString &json, const QString &fname);
signals: signals:
void error_message(const QString &msg); void error_message(const QString &msg);
public slots:
void processReplay(const QString &, const QString &);
private: private:
Router *router; Router *router;
QMap<int, ClientPlayer *> players; QMap<int, ClientPlayer *> players;

175
src/client/replayer.cpp Normal file
View File

@ -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();
}

52
src/client/replayer.h Normal file
View File

@ -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

View File

@ -107,7 +107,7 @@ sqlite3 *OpenDatabase(const QString &filename, const QString &initSql) {
} }
bool CheckSqlString(const QString &str) { bool CheckSqlString(const QString &str) {
static const QRegularExpression exp("['\";#]+|(--)|(/\\*)|(\\*/)|(--\\+)"); static const QRegularExpression exp("['\";#* /\\\\?<>|:]+|(--)|(/\\*)|(\\*/)|(--\\+)");
return (!exp.match(str).hasMatch() && !str.isEmpty()); return (!exp.match(str).hasMatch() && !str.isEmpty());
} }

View File

@ -20,6 +20,7 @@
#endif #endif
#include "client.h" #include "client.h"
#include "util.h" #include "util.h"
#include "replayer.h"
QmlBackend *Backend = nullptr; QmlBackend *Backend = nullptr;
@ -27,6 +28,7 @@ QmlBackend::QmlBackend(QObject *parent) : QObject(parent) {
Backend = this; Backend = this;
#ifndef FK_SERVER_ONLY #ifndef FK_SERVER_ONLY
engine = nullptr; engine = nullptr;
replayer = nullptr;
rsa = RSA_new(); rsa = RSA_new();
udpSocket = new QUdpSocket(this); udpSocket = new QUdpSocket(this);
udpSocket->bind(0); udpSocket->bind(0);
@ -95,6 +97,9 @@ void QmlBackend::joinServer(QString address) {
return; return;
Client *client = new Client(this); Client *client = new Client(this);
connect(client, &Client::error_message, this, [=](const QString &msg) { connect(client, &Client::error_message, this, [=](const QString &msg) {
if (replayer) {
emit replayerShutdown();
}
client->deleteLater(); client->deleteLater();
emit notifyUI("ErrorMsg", msg); emit notifyUI("ErrorMsg", msg);
emit notifyUI("BackToStart", "[]"); emit notifyUI("BackToStart", "[]");
@ -181,6 +186,7 @@ void QmlBackend::pushLuaValue(lua_State *L, QVariant v) {
QString QmlBackend::callLuaFunction(const QString &func_name, QString QmlBackend::callLuaFunction(const QString &func_name,
QVariantList params) { QVariantList params) {
if (!ClientInstance) return "{}"; if (!ClientInstance) return "{}";
lua_State *L = ClientInstance->getLuaState(); lua_State *L = ClientInstance->getLuaState();
lua_getglobal(L, func_name.toLatin1().data()); lua_getglobal(L, func_name.toLatin1().data());
@ -196,6 +202,7 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
return ""; return "";
} }
lua_pop(L, 1); lua_pop(L, 1);
return QString(result); 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 #endif

View File

@ -6,6 +6,8 @@
#include <openssl/rsa.h> #include <openssl/rsa.h>
#include <openssl/pem.h> #include <openssl/pem.h>
class Replayer;
#include <qtmetamacros.h> #include <qtmetamacros.h>
class QmlBackend : public QObject { class QmlBackend : public QObject {
Q_OBJECT Q_OBJECT
@ -63,9 +65,20 @@ public:
void showToast(const QString &s) { emit notifyUI("ShowToast", s); } 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: signals:
void notifyUI(const QString &command, const QString &jsonData); void notifyUI(const QString &command, const QString &jsonData);
void volumeChanged(qreal); void volumeChanged(qreal);
void replayerToggle();
void replayerSpeedUp();
void replayerSlowDown();
void replayerUniform();
void replayerShutdown();
private slots: private slots:
void readPendingDatagrams(); void readPendingDatagrams();
@ -80,6 +93,8 @@ private:
QString aes_key; QString aes_key;
qreal m_volume; qreal m_volume;
Replayer *replayer;
void pushLuaValue(lua_State *L, QVariant v); void pushLuaValue(lua_State *L, QVariant v);
#endif #endif
}; };