parent
1fcd63ddeb
commit
59c25c583c
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -38,8 +38,26 @@ GraphicsBox {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
if (config.replaying) {
|
||||||
|
mainStack.pop();
|
||||||
|
Backend.controlReplayer("shutdown");
|
||||||
|
} else {
|
||||||
ClientInstance.notifyServer("QuitRoom", "[]");
|
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: 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 = "";
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ()
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue