diff --git a/.gitignore b/.gitignore
index 1282cf31..586b5bf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ tags
# file produced by game
/FreeKill
+/mymod
FreeKill.exe
freekill-wrap.cxx
server/users.db
diff --git a/Fk/ModMaker/CreateSomething.qml b/Fk/ModMaker/CreateSomething.qml
new file mode 100644
index 00000000..ff2fb692
--- /dev/null
+++ b/Fk/ModMaker/CreateSomething.qml
@@ -0,0 +1,50 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ColumnLayout {
+ id: root
+ anchors.fill: parent
+ anchors.margins: 16
+ signal finished()
+ signal accepted(string result)
+
+ property string head
+ property string hint
+
+ Text {
+ text: qsTr(head)
+ font.pixelSize: 20
+ font.bold: true
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ }
+
+ Text {
+ text: qsTr(hint)
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ }
+
+ Text {
+ text: qsTr("validator_hint")
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ }
+
+ TextField {
+ id: edit
+ font.pixelSize: 18
+ Layout.fillWidth: true
+ validator: RegularExpressionValidator { regularExpression: /[0-9A-Za-z_]+/ }
+ }
+
+ Button {
+ text: "OK"
+ enabled: edit.text.length >= 4
+ onClicked: {
+ accepted(edit.text);
+ finished();
+ }
+ }
+}
diff --git a/Fk/ModMaker/ModConfig.qml b/Fk/ModMaker/ModConfig.qml
new file mode 100644
index 00000000..51bfc119
--- /dev/null
+++ b/Fk/ModMaker/ModConfig.qml
@@ -0,0 +1,36 @@
+import QtQuick
+
+QtObject {
+ property var conf
+
+ property string userName
+ property string email
+ property var modList: []
+
+ function loadConf() {
+ conf = JSON.parse(ModBackend.readFile("mymod/config.json"));
+ userName = conf.userName ?? "";
+ email = conf.email ?? "";
+ modList = conf.modList ?? [];
+ }
+
+ function saveConf() {
+ conf.userName = userName;
+ conf.email = email;
+ conf.modList = modList;
+
+ ModBackend.saveToFile("mymod/config.json", JSON.stringify(conf, undefined, 2));
+ }
+
+ function addMod(mod) {
+ modList.push(mod);
+ saveConf();
+ modListChanged();
+ }
+
+ function removeMod(mod) {
+ modList.splice(modList.indexOf(mod), 1);
+ saveConf();
+ modListChanged();
+ }
+}
diff --git a/Fk/ModMaker/ModInit.qml b/Fk/ModMaker/ModInit.qml
new file mode 100644
index 00000000..c8a2ad3e
--- /dev/null
+++ b/Fk/ModMaker/ModInit.qml
@@ -0,0 +1,113 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+ id: root
+ property bool configOK: modConfig.userName !== "" && modConfig.email !== ""
+
+ ToolBar {
+ id: bar
+ width: parent.width
+ RowLayout {
+ anchors.fill: parent
+ ToolButton {
+ icon.source: AppPath + "/image/modmaker/back"
+ onClicked: mainStack.pop();
+ }
+ Label {
+ text: qsTr("ModMaker")
+ horizontalAlignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ }
+ ToolButton {
+ icon.source: AppPath + "/image/modmaker/menu"
+ onClicked: {
+ dialog.source = "UserInfo.qml";
+ drawer.open();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: parent.height - bar.height
+ anchors.top: bar.bottom
+ color: "snow"
+ opacity: 0.75
+
+ Text {
+ anchors.centerIn: parent
+ text: root.configOK ? "" : qsTr("config is incomplete")
+ }
+
+ ListView {
+ anchors.fill: parent
+ model: modConfig.modList
+ delegate: Text { text: modelData }
+ }
+ }
+
+ RoundButton {
+ visible: root.configOK
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: 40
+ scale: 2
+ icon.source: AppPath + "/image/modmaker/add"
+ onClicked: {
+ dialog.source = "CreateSomething.qml";
+ dialog.item.head = "create_mod";
+ dialog.item.hint = "create_mod_hint";
+ drawer.open();
+ dialog.item.accepted.connect((name) => {
+ createNewMod(name);
+ });
+ }
+ }
+
+ Drawer {
+ id: drawer
+ width: parent.width * 0.4 / mainWindow.scale
+ height: parent.height / mainWindow.scale
+ dim: false
+ clip: true
+ dragMargin: 0
+ scale: mainWindow.scale
+ transformOrigin: Item.TopLeft
+
+ Loader {
+ id: dialog
+ anchors.fill: parent
+ onSourceChanged: {
+ if (item === null)
+ return;
+ item.finished.connect(() => {
+ sourceComponent = undefined;
+ drawer.close();
+ });
+ }
+ onSourceComponentChanged: sourceChanged();
+ }
+ }
+
+ function createNewMod(name) {
+ const banned = [ "test", "standard", "standard_cards", "maneuvering" ];
+ if (banned.indexOf(name) !== -1 || modConfig.modList.indexOf(name) !== -1) {
+ toast.show(qsTr("cannot use this mod name"));
+ return;
+ }
+ ModBackend.createMod(name);
+ const modInfo = {
+ name: name,
+ descrption: "",
+ author: modConfig.userName,
+ };
+ ModBackend.saveToFile(`mymod/${name}/mod.json`, JSON.stringify(modInfo, undefined, 2));
+ ModBackend.saveToFile(`mymod/${name}/.gitignore`, "init.lua");
+ ModBackend.stageFiles(name);
+ ModBackend.commitChanges(name, "Initial commit", modConfig.userName, modConfig.email);
+ modConfig.addMod(name);
+ }
+}
diff --git a/Fk/ModMaker/UserInfo.qml b/Fk/ModMaker/UserInfo.qml
new file mode 100644
index 00000000..3bc9bb32
--- /dev/null
+++ b/Fk/ModMaker/UserInfo.qml
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 16
+ signal finished()
+
+ Text {
+ text: qsTr("help_text")
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ }
+
+ RowLayout {
+ anchors.rightMargin: 8
+ spacing: 16
+ Text {
+ text: qsTr("username")
+ }
+ TextField {
+ id: userName
+ font.pixelSize: 18
+ text: modConfig.userName
+ Layout.fillWidth: true
+ onTextChanged: {
+ modConfig.userName = text;
+ modConfig.saveConf();
+ }
+ }
+ }
+
+ RowLayout {
+ anchors.rightMargin: 8
+ spacing: 16
+ Text {
+ text: qsTr("email")
+ }
+ TextField {
+ id: emailAddr
+ font.pixelSize: 18
+ Layout.fillWidth: true
+ text: modConfig.email
+ onTextChanged: {
+ modConfig.email = text;
+ modConfig.saveConf();
+ }
+ }
+ }
+
+ Text {
+ text: qsTr("key_help_text")
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ textFormat: Text.RichText
+ onLinkActivated: Qt.openUrlExternally(link);
+ }
+
+ Button {
+ text: qsTr("copy pubkey")
+ Layout.fillWidth: true
+ onClicked: {
+ const key = "mymod/id_rsa.pub";
+ if (!Backend.exists(key)) {
+ ModBackend.initKey();
+ }
+ const pubkey = ModBackend.readFile(key);
+ Backend.copyToClipboard(pubkey);
+ toast.show(qsTr("pubkey copied"));
+ }
+ }
+}
diff --git a/Fk/ModMaker/main.qml b/Fk/ModMaker/main.qml
new file mode 100644
index 00000000..44793ad7
--- /dev/null
+++ b/Fk/ModMaker/main.qml
@@ -0,0 +1,55 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+ Component { id: modInit; ModInit {} }
+
+ StackView {
+ id: modStack
+ anchors.fill: parent
+ pushEnter: Transition {
+ PropertyAnimation {
+ property: "opacity"
+ from: 0
+ to:1
+ duration: 200
+ }
+ }
+ pushExit: Transition {
+ PropertyAnimation {
+ property: "opacity"
+ from: 1
+ to:0
+ duration: 200
+ }
+ }
+ popEnter: Transition {
+ PropertyAnimation {
+ property: "opacity"
+ from: 0
+ to:1
+ duration: 200
+ }
+ }
+ popExit: Transition {
+ PropertyAnimation {
+ property: "opacity"
+ from: 1
+ to:0
+ duration: 200
+ }
+ }
+ }
+
+ ModConfig {
+ id: modConfig
+ }
+
+ Component.onCompleted: {
+ if (!ModBackend) {
+ Backend.createModBackend();
+ }
+ modConfig.loadConf();
+ modStack.push(modInit);
+ }
+}
diff --git a/Fk/ModMaker/qmldir b/Fk/ModMaker/qmldir
new file mode 100644
index 00000000..4a0e4264
--- /dev/null
+++ b/Fk/ModMaker/qmldir
@@ -0,0 +1,2 @@
+module Fk.ModMaker
+ModMaker 1.0 main.qml
diff --git a/Fk/Pages/About.qml b/Fk/Pages/About.qml
index 22edbb8b..19e16c21 100644
--- a/Fk/Pages/About.qml
+++ b/Fk/Pages/About.qml
@@ -54,6 +54,7 @@ Item {
wrapMode: Text.WordWrap
textFormat: Text.MarkdownText
font.pixelSize: 18
+ onLinkActivated: Qt.openUrlExternally(link);
}
}
}
diff --git a/Fk/Pages/Init.qml b/Fk/Pages/Init.qml
index 626353cd..47a1f70d 100644
--- a/Fk/Pages/Init.qml
+++ b/Fk/Pages/Init.qml
@@ -183,6 +183,16 @@ Item {
}
}
+ // Temp
+ Button {
+ text: qsTr("Mod Making")
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ onClicked: {
+ mainStack.push(modMaker);
+ }
+ }
+
function downloadComplete() {
toast.show(qsTr("updated packages for md5"));
}
diff --git a/Fk/Pages/ModMaker.qml b/Fk/Pages/ModMaker.qml
new file mode 100644
index 00000000..b9d6360f
--- /dev/null
+++ b/Fk/Pages/ModMaker.qml
@@ -0,0 +1,3 @@
+import Fk.ModMaker as Md
+
+Md.ModMaker {}
diff --git a/Fk/Pages/qmldir b/Fk/Pages/qmldir
index 63044414..f473a9ec 100644
--- a/Fk/Pages/qmldir
+++ b/Fk/Pages/qmldir
@@ -10,3 +10,4 @@ ModesOverview 1.0 ModesOverview.qml
PackageManage 1.0 PackageManage.qml
Room 1.0 Room.qml
TileButton 1.0 TileButton.qml
+ModMaker 1.0 ModMaker.qml
diff --git a/Fk/main.qml b/Fk/main.qml
index 8decf09c..b95ee963 100644
--- a/Fk/main.qml
+++ b/Fk/main.qml
@@ -51,6 +51,7 @@ Item {
Component { id: init; Init {} }
Component { id: packageManage; PackageManage {} }
+ Component { id: modMaker; ModMaker {} }
Component { id: lobby; Lobby {} }
Component { id: generalsOverview; GeneralsOverview {} }
Component { id: cardsOverview; CardsOverview {} }
diff --git a/image/modmaker/add.png b/image/modmaker/add.png
new file mode 100644
index 00000000..46c8336a
Binary files /dev/null and b/image/modmaker/add.png differ
diff --git a/image/modmaker/back.png b/image/modmaker/back.png
new file mode 100644
index 00000000..fdce1585
Binary files /dev/null and b/image/modmaker/back.png differ
diff --git a/image/modmaker/menu.png b/image/modmaker/menu.png
new file mode 100644
index 00000000..19e8e87e
Binary files /dev/null and b/image/modmaker/menu.png differ
diff --git a/image/modmaker/ok.png b/image/modmaker/ok.png
new file mode 100644
index 00000000..1e3b259c
Binary files /dev/null and b/image/modmaker/ok.png differ
diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts
index 86dfabb7..bee2550c 100644
--- a/lang/zh_CN.ts
+++ b/lang/zh_CN.ts
@@ -79,6 +79,10 @@
管理拓展包
+
+
+ 制作Mod
+
欢迎回来!
@@ -212,4 +216,67 @@
已复制。
+
+
+ ModInit
+
+
+ 新月杀Mod制作器 - 首页
+
+
+
+
+ Mod制作器还未正确配置!
+请点击右上角配置好用户名和邮箱
+
+
+
+ 不能给mod取这个名字
+
+
+
+
+ UserInfo
+
+
+ 用户名和邮箱需要填入和git服务器上相同的名字和邮箱。
+
+
+
+ 用户名
+
+
+
+ 邮箱
+
+
+
+ 公钥是你向git服务器证明身份的手段。请点击按钮复制公钥,然后在网页中添加SSH密钥。详见新月杀Mod制作器教程。
+
+
+
+ 复制公钥
+
+
+
+ 公钥已经复制到剪贴板。
+
+
+
+
+ CreateSomething
+
+
+ 注意:你仅可以输入大小写字母、数字和下划线,且长度至少为4。这里输入的只是内部名称,它的中文名你可以稍后指定。
+
+
+
+ 新建Mod
+
+
+
+ 请输入mod的名称。
+
+
+
diff --git a/lua/core/card.lua b/lua/core/card.lua
index 52e1e504..88118f8f 100644
--- a/lua/core/card.lua
+++ b/lua/core/card.lua
@@ -86,13 +86,13 @@ function Card:initialize(name, suit, number, color)
self.color = Card.NoColor
end
- self.package = nil
+ -- self.package = nil
self.id = 0
self.type = 0
self.sub_type = Card.SubTypeNone
- self.skill = nil
+ -- self.skill = nil
self.subcards = {}
- self.skillName = nil -- ""
+ -- self.skillName = nil
self._skillName = ""
self.skillNames = {}
self.mark = {}
@@ -101,31 +101,21 @@ function Card:initialize(name, suit, number, color)
self.name = string.sub(name, 2, #name)
self.is_derived = true
end
+end
- local mt = table.simpleClone(getmetatable(self))
- local newidx = mt.__newindex or rawset
- mt.__newindex = function(t, k, v)
- if k == "skillName" then
- table.insertIfNeed(self.skillNames, v)
- t._skillName = v
- else
- return newidx(t, k, v)
- end
+function Card:__index(k)
+ if k == "skillName" then
+ return self._skillName
end
+end
- local idx = mt.__index or rawget
- mt.__index = function(t, k)
- if k == "skillName" then
- return t._skillName
- end
- if type(idx) == "table" then
- return idx[k]
- end
- if type(idx) == "function" then
- return idx(t, k)
- end
+function Card:__newindex(k, v)
+ if k == "skillName" then
+ table.insertIfNeed(self.skillNames, v)
+ self._skillName = v
+ else
+ rawset(self, k, v)
end
- setmetatable(self, mt)
end
function Card:__tostring()
diff --git a/lua/server/request.lua b/lua/server/request.lua
index afa85f03..c5c6cca8 100644
--- a/lua/server/request.lua
+++ b/lua/server/request.lua
@@ -90,6 +90,7 @@ request_handlers["luckcard"] = function(room, id, reqlist)
local p = room:getPlayerById(id)
local cancel = reqlist[3] == "false"
local luck_data = room:getTag("LuckCardData")
+ if not (p and luck_data) then return end
local pdata = luck_data[id]
if not cancel then
diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua
index 36bf646e..b024d758 100644
--- a/lua/server/serverplayer.lua
+++ b/lua/server/serverplayer.lua
@@ -243,7 +243,11 @@ function ServerPlayer:marshal(player)
room:notifyMoveCards({ player }, card_moves)
end
- -- TODO: pile, mark
+ -- TODO: pile
+
+ for k, v in pairs(self.mark) do
+ player:doNotify("SetPlayerMark", json.encode{self.id, k, v})
+ end
for _, s in ipairs(self.player_skills) do
player:doNotify("AddSkill", json.encode{self.id, s.name})
diff --git a/packages/standard/init.lua b/packages/standard/init.lua
index cb32619f..1ab74b47 100644
--- a/packages/standard/init.lua
+++ b/packages/standard/init.lua
@@ -1043,7 +1043,7 @@ local wushuang = fk.CreateTriggerSkill{
end
if event == fk.TargetSpecified then
- return target == player and table.contains({ "slash", "duel" }, data.card.name)
+ return target == player and table.contains({ "slash", "duel" }, data.card.trueName)
else
return data.to == player.id and data.card.name == "duel"
end
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e3c7b8fd..cd1da7a0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,6 +19,7 @@ if (NOT DEFINED FK_SERVER_ONLY)
list(APPEND freekill_SRCS
"client/client.cpp"
"client/clientplayer.cpp"
+ "ui/mod.cpp"
)
endif ()
diff --git a/src/core/util.cpp b/src/core/util.cpp
index 9a1c52ac..ed679644 100644
--- a/src/core/util.cpp
+++ b/src/core/util.cpp
@@ -139,33 +139,6 @@ void ExecSQL(sqlite3 *db, const QString &sql) {
void CloseDatabase(sqlite3 *db) { sqlite3_close(db); }
-#ifndef Q_OS_WASM
-RSA *InitServerRSA() {
- RSA *rsa = RSA_new();
- if (!QFile::exists("server/rsa_pub")) {
- BIGNUM *bne = BN_new();
- BN_set_word(bne, RSA_F4);
- RSA_generate_key_ex(rsa, 2048, bne, NULL);
-
- BIO *bp_pub = BIO_new_file("server/rsa_pub", "w+");
- PEM_write_bio_RSAPublicKey(bp_pub, rsa);
- BIO *bp_pri = BIO_new_file("server/rsa", "w+");
- PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL);
-
- BIO_free_all(bp_pub);
- BIO_free_all(bp_pri);
- BN_free(bne);
- }
- FILE *keyFile = fopen("server/rsa_pub", "r");
- PEM_read_RSAPublicKey(keyFile, &rsa, NULL, NULL);
- fclose(keyFile);
- keyFile = fopen("server/rsa", "r");
- PEM_read_RSAPrivateKey(keyFile, &rsa, NULL, NULL);
- fclose(keyFile);
- return rsa;
-}
-#endif
-
static void writeFileMD5(QFile &dest, const QString &fname) {
QFile f(fname);
if (!f.open(QIODevice::ReadOnly)) {
@@ -173,6 +146,7 @@ static void writeFileMD5(QFile &dest, const QString &fname) {
}
auto data = f.readAll();
+ f.close();
data.replace(QByteArray("\r\n"), QByteArray("\n"));
auto hash = QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex();
dest.write(fname.toUtf8() + '=' + hash + ';');
diff --git a/src/core/util.h b/src/core/util.h
index 5773b328..1a18ad60 100644
--- a/src/core/util.h
+++ b/src/core/util.h
@@ -16,10 +16,6 @@ QString SelectFromDb(sqlite3 *db, const QString &sql);
void ExecSQL(sqlite3 *db, const QString &sql);
void CloseDatabase(sqlite3 *db);
-#ifndef Q_OS_WASM
-RSA *InitServerRSA();
-#endif
-
QString calcFileMD5();
QByteArray JsonArray2Bytes(const QJsonArray &arr);
QJsonDocument String2Json(const QString &str);
diff --git a/src/main.cpp b/src/main.cpp
index ac98457f..3188818a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -260,6 +260,7 @@ int main(int argc, char *argv[]) {
// 向 Qml 中先定义几个全局变量
engine->rootContext()->setContextProperty("FkVersion", FK_VERSION);
engine->rootContext()->setContextProperty("Backend", &backend);
+ engine->rootContext()->setContextProperty("ModBackend", nullptr);
engine->rootContext()->setContextProperty("Pacman", Pacman);
#ifdef QT_DEBUG
diff --git a/src/pch.h b/src/pch.h
index f73e304b..f1189708 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -14,9 +14,7 @@
typedef int LuaFunction;
#include "lua.hpp"
#include "sqlite3.h"
-
-#include
-#include
+#define OPENSSL_API_COMPAT 0x10101000L
#if !defined (Q_OS_ANDROID) && !defined (Q_OS_WASM)
#define DESKTOP_BUILD
diff --git a/src/server/server.cpp b/src/server/server.cpp
index 3aa87567..41e7a55e 100644
--- a/src/server/server.cpp
+++ b/src/server/server.cpp
@@ -22,7 +22,7 @@ Server *ServerInstance;
Server::Server(QObject *parent) : QObject(parent) {
ServerInstance = this;
db = OpenDatabase();
- rsa = InitServerRSA();
+ rsa = initServerRSA();
QFile file("server/rsa_pub");
file.open(QIODevice::ReadOnly);
QTextStream in(&file);
@@ -431,6 +431,32 @@ void Server::onUserDisconnected() {
void Server::onUserStateChanged() {}
+RSA *Server::initServerRSA() {
+ RSA *rsa = RSA_new();
+ if (!QFile::exists("server/rsa_pub")) {
+ BIGNUM *bne = BN_new();
+ BN_set_word(bne, RSA_F4);
+ RSA_generate_key_ex(rsa, 2048, bne, NULL);
+
+ BIO *bp_pub = BIO_new_file("server/rsa_pub", "w+");
+ PEM_write_bio_RSAPublicKey(bp_pub, rsa);
+ BIO *bp_pri = BIO_new_file("server/rsa", "w+");
+ PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL);
+
+ BIO_free_all(bp_pub);
+ BIO_free_all(bp_pri);
+ QFile("server/rsa").setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
+ BN_free(bne);
+ }
+ FILE *keyFile = fopen("server/rsa_pub", "r");
+ PEM_read_RSAPublicKey(keyFile, &rsa, NULL, NULL);
+ fclose(keyFile);
+ keyFile = fopen("server/rsa", "r");
+ PEM_read_RSAPrivateKey(keyFile, &rsa, NULL, NULL);
+ fclose(keyFile);
+ return rsa;
+}
+
void Server::readConfig() {
QFile file("freekill.server.config.json");
if (!file.open(QIODevice::ReadOnly)) {
diff --git a/src/server/server.h b/src/server/server.h
index 701d3a68..015176bb 100644
--- a/src/server/server.h
+++ b/src/server/server.h
@@ -3,6 +3,9 @@
#ifndef _SERVER_H
#define _SERVER_H
+#include
+#include
+
#include
#include
class ServerSocket;
@@ -67,6 +70,8 @@ private:
sqlite3 *db;
QString md5;
+ static RSA *initServerRSA();
+
QJsonObject config;
void readConfig();
diff --git a/src/ui/mod.cpp b/src/ui/mod.cpp
new file mode 100644
index 00000000..3271a92e
--- /dev/null
+++ b/src/ui/mod.cpp
@@ -0,0 +1,230 @@
+#include "mod.h"
+#include "git2.h"
+#include "util.h"
+#include
+#include
+#include
+#include
+
+ModMaker::ModMaker(QObject *parent) : QObject(parent) {
+ git_libgit2_init();
+#ifdef Q_OS_ANDROID
+ git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, "./certs");
+#endif
+
+ if (!QDir("mymod").exists()) {
+ QDir(".").mkdir("mymod");
+ }
+
+ db = OpenDatabase("mymod/packages.db", "packages/mymod.sql");
+}
+
+ModMaker::~ModMaker() {
+ // git_libgit2_shutdown();
+ sqlite3_close(db);
+}
+
+// copied from https://stackoverflow.com/questions/1011572/convert-pem-key-to-ssh-rsa-format
+static unsigned char pSshHeader[11] = { 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2D, 0x72, 0x73, 0x61};
+
+static int SshEncodeBuffer(unsigned char *pEncoding, int bufferLen, unsigned char* pBuffer) {
+ int adjustedLen = bufferLen, index;
+ if (*pBuffer & 0x80) {
+ adjustedLen++;
+ pEncoding[4] = 0;
+ index = 5;
+ } else {
+ index = 4;
+ }
+
+ pEncoding[0] = (unsigned char) (adjustedLen >> 24);
+ pEncoding[1] = (unsigned char) (adjustedLen >> 16);
+ pEncoding[2] = (unsigned char) (adjustedLen >> 8);
+ pEncoding[3] = (unsigned char) (adjustedLen );
+ memcpy(&pEncoding[index], pBuffer, bufferLen);
+ return index + bufferLen;
+}
+
+static void initSSHKeyPair() {
+ if (!QFile::exists("mymod/id_rsa.pub")) {
+ RSA *rsa = RSA_new();
+ BIGNUM *bne = BN_new();
+ BN_set_word(bne, RSA_F4);
+ RSA_generate_key_ex(rsa, 3072, bne, NULL);
+
+ BIO *bp_pri = BIO_new_file("mymod/id_rsa", "w");
+ PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL);
+ BIO_free_all(bp_pri);
+ QFile("mymod/id_rsa").setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
+
+ auto n = RSA_get0_n(rsa);
+ auto e = RSA_get0_e(rsa);
+ auto nLen = BN_num_bytes(n);
+ auto eLen = BN_num_bytes(e);
+ auto nBytes = (unsigned char *)malloc(nLen);
+ auto eBytes = (unsigned char *)malloc(eLen);
+ BN_bn2bin(n, nBytes);
+ BN_bn2bin(e, eBytes);
+
+ auto encodingLength = 11 + 4 + eLen + 4 + nLen;
+ // correct depending on the MSB of e and N
+ if (eBytes[0] & 0x80)
+ encodingLength++;
+ if (nBytes[0] & 0x80)
+ encodingLength++;
+
+ auto pEncoding = (unsigned char *)malloc(encodingLength);
+ memcpy(pEncoding, pSshHeader, 11);
+ int index = 0;
+ index = SshEncodeBuffer(&pEncoding[11], eLen, eBytes);
+ index = SshEncodeBuffer(&pEncoding[11 + index], nLen, nBytes);
+
+ auto b64 = BIO_new(BIO_f_base64());
+ BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
+ auto bio = BIO_new_file("mymod/id_rsa.pub", "w");
+ BIO_printf(bio, "ssh-rsa ");
+ bio = BIO_push(b64, bio);
+ BIO_write(bio, pEncoding, encodingLength);
+ BIO_flush(bio);
+ bio = BIO_pop(b64);
+ BIO_printf(bio, " FreeKill\n");
+ BIO_flush(bio);
+
+ BIO_free_all(bio);
+ BIO_free_all(b64);
+ BN_free(bne);
+ RSA_free(rsa);
+ }
+}
+
+void ModMaker::initKey() { initSSHKeyPair(); }
+
+QString ModMaker::readFile(const QString &fileName) {
+ QFile conf(fileName);
+ if (!conf.exists()) {
+ conf.open(QIODevice::WriteOnly);
+ static const char *init_conf = "{}";
+ conf.write(init_conf);
+ conf.close();
+ return init_conf;
+ }
+ conf.open(QIODevice::ReadOnly);
+ QString ret = conf.readAll();
+ conf.close();
+ return ret;
+}
+
+void ModMaker::saveToFile(const QString &fName, const QString &content) {
+ QFile c(fName);
+ c.open(QIODevice::WriteOnly);
+ c.write(content.toUtf8());
+ c.close();
+}
+
+void ModMaker::createMod(const QString &name) {
+ init(name);
+}
+
+void ModMaker::commitChanges(const QString &name, const QString &msg,
+ const QString &user, const QString &email)
+{
+ auto userBytes = user.toUtf8();
+ auto emailBytes = email.toUtf8();
+ commit(name, msg, userBytes, emailBytes);
+}
+
+#define GIT_FAIL \
+ const git_error *e = git_error_last(); \
+ qCritical("Error %d/%d: %s\n", error, e->klass, e->message)
+
+#define GIT_CHK(s) do { \
+ error = (s); \
+ if (error < 0) { \
+ GIT_FAIL; \
+ goto clean; \
+ }} while (0)
+
+static int fk_cred_cb(git_cred **out, const char *url, const char *name,
+ unsigned int allowed_types, void *payload)
+{
+ initSSHKeyPair();
+ return git_cred_ssh_key_new(out, "git", "mymod/id_rsa.pub", "mymod/id_rsa", "");
+}
+
+int ModMaker::init(const QString &pkg) {
+ QString path = "mymod/" + pkg;
+ int error;
+ git_repository *repo = NULL;
+ git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
+ opts.flags |= GIT_REPOSITORY_INIT_MKPATH; /* mkdir as needed to create repo */
+ error = git_repository_init_ext(&repo, path.toLatin1().constData(), &opts);
+ if (error < 0) {
+ GIT_FAIL;
+ }
+ git_repository_free(repo);
+ return error;
+}
+
+int ModMaker::add(const QString &pkg) {
+ QString path = "mymod/" + pkg;
+ int error;
+ git_repository *repo = NULL;
+ git_index *index = NULL;
+
+ GIT_CHK(git_repository_open(&repo, path.toLatin1()));
+ GIT_CHK(git_repository_index(&index, repo));
+ GIT_CHK(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL));
+ GIT_CHK(git_index_write(index));
+
+clean:
+ git_repository_free(repo);
+ git_index_free(index);
+ return error;
+}
+
+int ModMaker::commit(const QString &pkg, const QString &msg, const char *user, const char *email) {
+ QString path = "mymod/" + pkg;
+ int error;
+ git_repository *repo = NULL;
+ git_oid commit_oid,tree_oid;
+ git_tree *tree;
+ git_index *index;
+ git_object *parent = NULL;
+ git_reference *ref = NULL;
+ git_signature *signature;
+
+ GIT_CHK(git_repository_open(&repo, path.toLatin1()));
+ error = git_revparse_ext(&parent, &ref, repo, "HEAD");
+ if (error == GIT_ENOTFOUND) {
+ // printf("HEAD not found. Creating first commit\n");
+ error = 0;
+ } else if (error != 0) {
+ GIT_FAIL;
+ goto clean;
+ }
+
+ GIT_CHK(git_repository_index(&index, repo));
+ GIT_CHK(git_index_write_tree(&tree_oid, index));
+ GIT_CHK(git_index_write(index));
+ GIT_CHK(git_tree_lookup(&tree, repo, &tree_oid));
+ GIT_CHK(git_signature_now(&signature, user, email));
+ GIT_CHK(git_commit_create_v(
+ &commit_oid,
+ repo,
+ "HEAD",
+ signature,
+ signature,
+ NULL,
+ msg.toUtf8(),
+ tree,
+ parent ? 1 : 0, parent));
+
+clean:
+ git_repository_free(repo);
+ git_index_free(index);
+ git_signature_free(signature);
+ git_tree_free(tree);
+ git_object_free(parent);
+ git_reference_free(ref);
+ return error;
+}
diff --git a/src/ui/mod.h b/src/ui/mod.h
new file mode 100644
index 00000000..9e21679c
--- /dev/null
+++ b/src/ui/mod.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef _DIY_H
+#define _DIY_H
+
+#include
+class ModMaker : public QObject {
+ Q_OBJECT
+public:
+ ModMaker(QObject *parent = nullptr);
+ ~ModMaker();
+
+ Q_INVOKABLE void initKey();
+
+ Q_INVOKABLE QString readFile(const QString &fileName);
+ Q_INVOKABLE void saveToFile(const QString &fileName, const QString &content);
+
+ Q_INVOKABLE void createMod(const QString &name);
+ Q_INVOKABLE void stageFiles(const QString &name) { add(name); }
+ Q_INVOKABLE void commitChanges(const QString &name, const QString &msg,
+ const QString &user, const QString &email);
+
+private:
+ sqlite3 *db;
+
+ // git functions
+ int init(const QString &pkg);
+ int add(const QString &pkg);
+ int commit(const QString &pkg, const QString &msg, const char *user, const char *email);
+};
+
+#endif
diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp
index 3c5c41c6..53b2b2f4 100644
--- a/src/ui/qmlbackend.cpp
+++ b/src/ui/qmlbackend.cpp
@@ -9,6 +9,7 @@
#include
#include
+#include "mod.h"
#endif
#include
@@ -227,10 +228,13 @@ QString QmlBackend::loadConf() {
conf.open(QIODevice::WriteOnly);
static const char *init_conf = "{}";
conf.write(init_conf);
+ conf.close();
return init_conf;
}
conf.open(QIODevice::ReadOnly);
- return conf.readAll();
+ auto ret = conf.readAll();
+ conf.close();
+ return ret;
}
QString QmlBackend::loadTips() {
@@ -239,16 +243,20 @@ QString QmlBackend::loadTips() {
conf.open(QIODevice::WriteOnly);
static const char *init_conf = "转啊~ 转啊~";
conf.write(init_conf);
+ conf.close();
return init_conf;
}
conf.open(QIODevice::ReadOnly);
- return conf.readAll();
+ auto ret = conf.readAll();
+ conf.close();
+ return ret;
}
void QmlBackend::saveConf(const QString &conf) {
QFile c("freekill.client.config.json");
c.open(QIODevice::WriteOnly);
c.write(conf.toUtf8());
+ c.close();
}
void QmlBackend::replyDelayTest(const QString &screenName,
@@ -308,4 +316,8 @@ void QmlBackend::installAESKey() {
ClientInstance->installAESKey(aes_key.toLatin1());
}
+void QmlBackend::createModBackend() {
+ engine->rootContext()->setContextProperty("ModBackend", new ModMaker);
+}
+
#endif
diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h
index 3c49dc5a..46d21820 100644
--- a/src/ui/qmlbackend.h
+++ b/src/ui/qmlbackend.h
@@ -3,6 +3,9 @@
#ifndef _QMLBACKEND_H
#define _QMLBACKEND_H
+#include
+#include
+
#include
class QmlBackend : public QObject {
Q_OBJECT
@@ -50,6 +53,8 @@ public:
Q_INVOKABLE QString getAESKey() const;
Q_INVOKABLE void installAESKey();
+ Q_INVOKABLE void createModBackend();
+
qreal volume() const { return m_volume; }
void setVolume(qreal v) { m_volume = v; }