* compile with wasm

* pack into qrc

* disable colorized msg for browser and cmd.exe

* run lua in wasm

* run the game

* font & pixmap anim

* special init page for web

* doc for compile wasm
This commit is contained in:
notify 2023-01-03 23:37:14 +08:00 committed by GitHub
parent f434e9ee9b
commit fb425e15fb
19 changed files with 282 additions and 48 deletions

View File

@ -2,8 +2,10 @@ cmake_minimum_required(VERSION 3.16)
project(FreeKill VERSION 0.0.1)
if (NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
include_directories(fkparse/src)
add_subdirectory(fkparse)
endif ()
find_package(Qt6 REQUIRED COMPONENTS
Gui
@ -31,15 +33,38 @@ include_directories(src/network)
include_directories(src/server)
include_directories(src/ui)
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
# Fix include problem
include_directories("/usr/include/openssl-1.1/")
endif()
file(GLOB SWIG_FILES "${PROJECT_SOURCE_DIR}/src/swig/*.i")
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill-wasm.i)
else ()
set(SWIG_SOURCE ${PROJECT_SOURCE_DIR}/src/swig/freekill.i)
endif ()
add_custom_command(
OUTPUT ${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
DEPENDS ${SWIG_FILES}
COMMENT "Generating freekill-wrap.cxx"
COMMAND swig -c++ -lua -Wall -o
${PROJECT_SOURCE_DIR}/src/swig/freekill-wrap.cxx
${PROJECT_SOURCE_DIR}/src/swig/freekill.i
${SWIG_SOURCE}
)
qt_add_executable(FreeKill)
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
file(GLOB_RECURSE FK_RESOURCE_FILES
RELATIVE ${PROJECT_SOURCE_DIR}
*.lua *.qml *.js *.png *.jpg *.mp3
)
list(APPEND FK_RESOURCE_FILES "fonts/FZLBGBK.ttf")
qt_add_resources(FreeKill "qrc"
PREFIX "/"
FILES ${FK_RESOURCE_FILES}
)
endif()
add_subdirectory(src)

View File

@ -91,3 +91,30 @@ ___
## 编译安卓版
用Qt安装器装好Android库然后配置一下android-sdk就能编译了。
___
## WASM下编译
WASM大概就是能在浏览器中跑C++。编译用Qt Creator即可。
### 1. 条件与局限性
如果程序运行在网页上的话那么理应只有客户端然后提供网页的服务器上自然也运行着一个后端服务器。所以说在编译时应该舍弃掉服务端相关的代码。因此依赖库就不再需要sqlite3。
总之是编译个纯客户端的FK。
### 2. 编译OpenSSL
进入OpenSSL的src目录然后
$ ./config -no-asm -no-engine -no-dso
$ emmake make -j8 build_generated libssl.a libcrypto.a
编译Lua的话直接emmake make就行了总之库已经传到仓库了。
### 3. 部署资源文件
由于CMake中`file(GLOB_RECURSE)`所带来的缺陷,每当资源文件变动时,需要手动更新。
把构建目录中的.rcc目录删掉然后重新执行CMake->make即可。每次编译资源文件总要消耗相当多的时间。

BIN
lib/wasm/libcrypto.a Normal file

Binary file not shown.

BIN
lib/wasm/liblua.a Normal file

Binary file not shown.

View File

@ -1,5 +1,4 @@
import QtQuick
import Qt.labs.folderlistmodel
import "../skin-bank.js" as SkinBank
Item {
@ -18,12 +17,8 @@ Item {
width: childrenRect.width
height: childrenRect.height
FolderListModel {
id: fileModel
folder: SkinBank.PIXANIM_DIR + source
nameFilters: ["*.png"]
showDirs: false
}
property string folder: SkinBank.PIXANIM_DIR + source
property int fileModel
Repeater {
id: frames
@ -35,7 +30,7 @@ Item {
onStatusChanged: {
if (status == Image.Ready) {
loadedFrameCount++;
if (loadedFrameCount == fileModel.count)
if (loadedFrameCount == fileModel)
root.loaded();
}
}
@ -52,8 +47,8 @@ Item {
interval: 50
repeat: true
onTriggered: {
if (currentFrame >= fileModel.count) {
frames.itemAt(fileModel.count - 1).visible = false;
if (currentFrame >= fileModel) {
frames.itemAt(fileModel - 1).visible = false;
if (loop) {
currentFrame = 0;
} else {
@ -73,7 +68,7 @@ Item {
function start()
{
if (loadedFrameCount == fileModel.count) {
if (loadedFrameCount == fileModel) {
timer.start();
} else {
root.loaded.connect(function(){
@ -86,4 +81,8 @@ Item {
{
timer.stop();
}
Component.onCompleted: {
fileModel = Backend.ls(folder).length;
}
}

View File

@ -170,7 +170,14 @@ function moveCards(moves) {
}
function setEmotion(id, emotion) {
let path = (SkinBank.PIXANIM_DIR + emotion).replace("file://", "");
let path;
if (OS === "Win") {
// Windows: file:/C:/xxx/xxxx
path = (SkinBank.PIXANIM_DIR + emotion).replace("file:/", "");
} else {
path = (SkinBank.PIXANIM_DIR + emotion).replace("file://", "");
}
if (!Backend.exists(path)) {
return;
}

61
qml/Pages/WebInit.qml Normal file
View File

@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Controls
Item {
id: root
scale: 2
// Change this to your server's IP or domain name
property string server_addr: "127.0.0.1:9530"
Frame {
id: join_server
anchors.centerIn: parent
background: Rectangle {
color: "#88888888"
radius: 2
}
Column {
spacing: 8
TextField {
id: screenNameEdit
text: "player"
onTextChanged: {
passwordEdit.text = "";
let data = config.savedPassword[server_addr.editText];
if (data) {
if (text === data.username) {
passwordEdit.text = data.shorten_password;
}
}
}
}
TextField {
id: passwordEdit
text: ""
echoMode: TextInput.Password
passwordCharacter: "*"
}
Button {
text: "Login"
enabled: passwordEdit.text !== ""
onClicked: {
config.serverAddr = server_addr;
config.screenName = screenNameEdit.text;
config.password = passwordEdit.text;
mainWindow.busy = true;
Backend.joinServer(server_addr);
}
}
}
}
Component.onCompleted: {
config.loadConf();
let data = config.savedPassword[config.lastLoginServer];
screenNameEdit.text = data.username;
passwordEdit.text = data.shorten_password;
}
}

View File

@ -37,11 +37,12 @@ Item {
StackView {
id: mainStack
visible: !mainWindow.busy
initialItem: init
initialItem: OS !== "Web" ? init : webinit
anchors.fill: parent
}
Component { id: init; Init {} }
Component { id: webinit; WebInit {} }
Component { id: lobby; Lobby {} }
Component { id: generalsOverview; GeneralsOverview {} }
Component { id: cardsOverview; CardsOverview {} }
@ -152,7 +153,7 @@ Item {
}
Component.onCompleted: {
if (!Android) {
if (OS !== "Android" && OS !== "Web") {
width = config.winWidth;
height = config.winHeight;
}

View File

@ -28,6 +28,7 @@ set(freekill_HEADERS
"ui/qmlbackend.h"
)
set(FKP_LIB fkparse)
if (WIN32)
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/win/lua54.dll)
set(SQLITE3_LIB ${PROJECT_SOURCE_DIR}/lib/win/sqlite3.dll)
@ -40,6 +41,19 @@ elseif (ANDROID)
QT_ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android
QT_ANDROID_EXTRA_LIBS "${LUA_LIB};${SQLITE3_LIB};${CRYPTO_LIB}"
)
elseif (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
# WASM
list(REMOVE_ITEM freekill_SRCS
"network/server_socket.cpp"
#"network/client_socket.cpp"
#"network/router.cpp"
"server/server.cpp"
"server/serverplayer.cpp"
"server/room.cpp"
)
set(FKP_LIB "")
set(LUA_LIB ${PROJECT_SOURCE_DIR}/lib/wasm/liblua.a)
set(CRYPTO_LIB ${PROJECT_SOURCE_DIR}/lib/wasm/libcrypto.a)
else ()
set(LUA_LIB lua5.4)
set(SQLITE3_LIB sqlite3)
@ -57,7 +71,7 @@ target_link_libraries(FreeKill PRIVATE
${SQLITE3_LIB}
${CRYPTO_LIB}
${READLINE_LIB}
fkparse
${FKP_LIB}
Qt6::Qml
Qt6::Gui
Qt6::Widgets

View File

@ -61,6 +61,7 @@ void Dumpstack(lua_State *L)
}
}
#ifndef Q_OS_WASM
sqlite3 *OpenDatabase(const QString &filename)
{
sqlite3 *ret;
@ -151,6 +152,7 @@ RSA *InitServerRSA() {
fclose(keyFile);
return rsa;
}
#endif
static void writeFileMD5(QFile &dest, const QString &fname) {
QFile f(fname);

View File

@ -6,6 +6,7 @@
lua_State *CreateLuaState();
bool DoLuaScript(lua_State *L, const char *script);
#ifndef Q_OS_WASM
sqlite3 *OpenDatabase(const QString &filename = "./server/users.db");
QJsonObject SelectFromDatabase(sqlite3 *db, const QString &sql);
// For Lua
@ -14,6 +15,7 @@ void ExecSQL(sqlite3 *db, const QString &sql);
void CloseDatabase(sqlite3 *db);
RSA *InitServerRSA();
#endif
QString calcFileMD5();

View File

@ -1,5 +1,7 @@
#include "qmlbackend.h"
#ifndef Q_OS_WASM
#include "server.h"
#endif
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
#include "shell.h"
@ -11,8 +13,9 @@
#include <QSplashScreen>
#include <QScreen>
#include <QFileDialog>
#ifdef Q_OS_ANDROID
#if defined(Q_OS_ANDROID) || defined(Q_OS_WASM)
static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath)
{
QFileInfo srcFileInfo(srcFilePath);
@ -48,19 +51,19 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, const QStri
auto threadName = QThread::currentThread()->objectName().toLatin1().constData();
switch (type) {
case QtDebugMsg:
fprintf(stderr, "[%s/\e[1;30mDEBUG\e[0m] %s\n", threadName, localMsg.constData());
fprintf(stderr, "[%s/DEBUG] %s\n", threadName, localMsg.constData());
break;
case QtInfoMsg:
fprintf(stderr, "[%s/\e[1;32mINFO\e[0m] %s\n", threadName, localMsg.constData());
fprintf(stderr, "[%s/INFO] %s\n", threadName, localMsg.constData());
break;
case QtWarningMsg:
fprintf(stderr, "[%s/\e[1;33mWARNING\e[0m] %s\n", threadName, localMsg.constData());
fprintf(stderr, "[%s/WARNING] %s\n", threadName, localMsg.constData());
break;
case QtCriticalMsg:
fprintf(stderr, "[%s/\e[1;31mCRITICAL\e[0m] %s\n", threadName, localMsg.constData());
fprintf(stderr, "[%s/CRITICAL] %s\n", threadName, localMsg.constData());
break;
case QtFatalMsg:
fprintf(stderr, "[%s/\e[1;31mFATAL\e[0m] %s\n", threadName, localMsg.constData());
fprintf(stderr, "[%s/FATAL] %s\n", threadName, localMsg.constData());
break;
}
}
@ -73,6 +76,7 @@ int main(int argc, char *argv[])
QCoreApplication::setApplicationName("FreeKill");
QCoreApplication::setApplicationVersion("Alpha 0.0.1");
#ifndef Q_OS_WASM
QCommandLineParser parser;
parser.setApplicationDescription("FreeKill server");
parser.addHelpOption();
@ -105,6 +109,9 @@ int main(int argc, char *argv[])
}
return app->exec();
}
#else
copyPath(":/", QDir::currentPath());
#endif
app = new QApplication(argc, argv);
@ -132,19 +139,32 @@ int main(int argc, char *argv[])
backend.setEngine(engine);
engine->rootContext()->setContextProperty("Backend", &backend);
engine->rootContext()->setContextProperty("AppPath", QUrl::fromLocalFile(QDir::currentPath()));
#ifdef QT_DEBUG
bool debugging = true;
#else
bool debugging = false;
#endif
engine->rootContext()->setContextProperty("Debugging", debugging);
#ifdef Q_OS_ANDROID
engine->rootContext()->setContextProperty("Android", true);
QString system;
#if defined(Q_OS_ANDROID)
system = "Android";
#elif defined(Q_OS_WASM)
system = "Web";
#elif defined(Q_OS_WIN32)
system = "Win";
#elif defined(Q_OS_LINUX)
system = "Linux";
#else
engine->rootContext()->setContextProperty("Android", false);
system = "Other";
#endif
engine->rootContext()->setContextProperty("OS", system);
engine->rootContext()->setContextProperty("AppPath", QUrl::fromLocalFile(QDir::currentPath()));
engine->load("qml/main.qml");
if (engine->rootObjects().isEmpty())
return -1;

View File

@ -1,8 +1,10 @@
#include "router.h"
#include "client.h"
#include "client_socket.h"
#ifndef Q_OS_WASM
#include "server.h"
#include "serverplayer.h"
#endif
#include "util.h"
Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
@ -13,7 +15,9 @@ Router::Router(QObject *parent, ClientSocket *socket, RouterType type)
setSocket(socket);
expectedReplyId = -1;
replyTimeout = 0;
#ifndef Q_OS_WASM
extraReplyReadySemaphore = nullptr;
#endif
}
Router::~Router()
@ -44,14 +48,17 @@ void Router::setSocket(ClientSocket *socket)
}
}
#ifndef Q_OS_WASM
void Router::setReplyReadySemaphore(QSemaphore *semaphore)
{
extraReplyReadySemaphore = semaphore;
}
#endif
void Router::request(int type, const QString& command,
const QString& jsonData, int timeout)
{
#ifndef Q_OS_WASM
// In case a request is called without a following waitForReply call
if (replyReadySemaphore.available() > 0)
replyReadySemaphore.acquire(replyReadySemaphore.available());
@ -74,6 +81,7 @@ void Router::request(int type, const QString& command,
body << timeout;
emit messageReady(QJsonDocument(body).toJson(QJsonDocument::Compact));
#endif
}
void Router::reply(int type, const QString& command, const QString& jsonData)
@ -106,6 +114,7 @@ int Router::getTimeout() const
// cancel last request from the sender
void Router::cancelRequest()
{
#ifndef Q_OS_WASM
replyMutex.lock();
expectedReplyId = -1;
replyTimeout = 0;
@ -114,22 +123,28 @@ void Router::cancelRequest()
if (replyReadySemaphore.available() > 0)
replyReadySemaphore.acquire(replyReadySemaphore.available());
#endif
}
QString Router::waitForReply()
{
#ifndef Q_OS_WASM
replyReadySemaphore.acquire();
return m_reply;
#endif
}
QString Router::waitForReply(int timeout)
{
#ifndef Q_OS_WASM
replyReadySemaphore.tryAcquire(1, timeout * 1000);
return m_reply;
#endif
}
void Router::abortRequest()
{
#ifndef Q_OS_WASM
replyMutex.lock();
if (expectedReplyId != -1) {
replyReadySemaphore.release();
@ -139,10 +154,12 @@ void Router::abortRequest()
extraReplyReadySemaphore = nullptr;
}
replyMutex.unlock();
#endif
}
void Router::handlePacket(const QByteArray& rawPacket)
{
#ifndef Q_OS_WASM
static QMap<QString, void (*)(ServerPlayer *, const QString &)> lobby_actions;
if (lobby_actions.size() <= 0) {
lobby_actions["UpdateAvatar"] = [](ServerPlayer *sender, const QString &jsonData){
@ -260,5 +277,28 @@ void Router::handlePacket(const QByteArray& rawPacket)
locker.unlock();
emit replyReady();
}
#else
QJsonDocument packet = QJsonDocument::fromJson(rawPacket);
if (packet.isNull() || !packet.isArray())
return;
int requestId = packet[0].toInt();
int type = packet[1].toInt();
QString command = packet[2].toString();
QString jsonData = packet[3].toString();
if (type & TYPE_NOTIFICATION) {
if (type & DEST_CLIENT) {
ClientInstance->callLua(command, jsonData);
}
} else if (type & TYPE_REQUEST) {
this->requestId = requestId;
this->requestTimeout = packet[4].toInt();
if (type & DEST_CLIENT) {
qobject_cast<Client *>(parent())->callLua(command, jsonData);
}
}
#endif
}

View File

@ -29,7 +29,9 @@ public:
ClientSocket *getSocket() const;
void setSocket(ClientSocket *socket);
#ifndef Q_OS_WASM
void setReplyReadySemaphore(QSemaphore *semaphore);
#endif
void request(int type, const QString &command,
const QString &jsonData, int timeout);
@ -66,8 +68,10 @@ private:
int expectedReplyId;
int replyTimeout;
QString m_reply; // should be json string
#ifndef Q_OS_WASM
QSemaphore replyReadySemaphore;
QSemaphore *extraReplyReadySemaphore;
#endif
// Two Lua global table for callbacks and interactions
// stored in the lua_State of the sender

15
src/swig/freekill-wasm.i Normal file
View File

@ -0,0 +1,15 @@
%module fk
%{
#include "client.h"
#include "serverplayer.h"
#include "clientplayer.h"
#include "room.h"
#include "qmlbackend.h"
#include "util.h"
%}
%include "naturalvar.i"
%include "qt.i"
%include "player.i"
%include "client.i"

View File

@ -34,22 +34,3 @@ public:
};
extern ClientPlayer *Self;
%nodefaultctor ServerPlayer;
%nodefaultdtor ServerPlayer;
class ServerPlayer : public Player {
public:
Server *getServer() const;
Room *getRoom() const;
void setRoom(Room *room);
void speak(const QString &message);
void doRequest(const QString &command,
const QString &json_data, int timeout);
QString waitForReply();
QString waitForReply(int timeout);
void doNotify(const QString &command, const QString &json_data);
void prepareForRequest(const QString &command, const QString &data);
};

View File

@ -93,3 +93,21 @@ void Room::roomStart() {
%}
%nodefaultctor ServerPlayer;
%nodefaultdtor ServerPlayer;
class ServerPlayer : public Player {
public:
Server *getServer() const;
Room *getRoom() const;
void setRoom(Room *room);
void speak(const QString &message);
void doRequest(const QString &command,
const QString &json_data, int timeout);
QString waitForReply();
QString waitForReply(int timeout);
void doNotify(const QString &command, const QString &json_data);
void prepareForRequest(const QString &command, const QString &data);
};

View File

@ -1,5 +1,7 @@
#include "qmlbackend.h"
#ifndef Q_OS_WASM
#include "server.h"
#endif
#include "client.h"
#include "util.h"
@ -11,14 +13,18 @@ QmlBackend::QmlBackend(QObject* parent)
Backend = this;
engine = nullptr;
rsa = RSA_new();
#ifndef Q_OS_WASM
parser = fkp_new_parser();
#endif
}
QmlBackend::~QmlBackend()
{
Backend = nullptr;
RSA_free(rsa);
#ifndef Q_OS_WASM
fkp_close(parser);
#endif
}
QQmlApplicationEngine *QmlBackend::getEngine() const
@ -33,6 +39,7 @@ void QmlBackend::setEngine(QQmlApplicationEngine *engine)
void QmlBackend::startServer(ushort port)
{
#ifndef Q_OS_WASM
if (!ServerInstance) {
Server *server = new Server(this);
@ -41,6 +48,7 @@ void QmlBackend::startServer(ushort port)
emit notifyUI("ErrorMsg", tr("Cannot start server!"));
}
}
#endif
}
void QmlBackend::joinServer(QString address)
@ -81,7 +89,7 @@ void QmlBackend::cd(const QString &path) {
}
QStringList QmlBackend::ls(const QString &dir) {
return QDir(dir).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
return QDir(QUrl(dir).path()).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
}
QString QmlBackend::pwd() {
@ -169,12 +177,14 @@ QString QmlBackend::callLuaFunction(const QString &func_name,
}
QString QmlBackend::pubEncrypt(const QString &key, const QString &data) {
BIO *keyio = BIO_new_mem_buf(key.toLatin1().data(), -1);
auto key_bytes = key.toLatin1();
BIO *keyio = BIO_new_mem_buf(key_bytes.constData(), -1);
PEM_read_bio_RSAPublicKey(keyio, &rsa, NULL, NULL);
BIO_free_all(keyio);
auto data_bytes = data.toUtf8();
unsigned char buf[RSA_size(rsa)];
RSA_public_encrypt(data.length(), (const unsigned char *)data.toUtf8().data(),
RSA_public_encrypt(data.length(), (const unsigned char *)data_bytes.constData(),
buf, rsa, RSA_PKCS1_PADDING);
return QByteArray::fromRawData((const char *)buf, RSA_size(rsa)).toBase64();
}
@ -209,6 +219,7 @@ void QmlBackend::saveConf(const QString &conf) {
}
void QmlBackend::parseFkp(const QString &fileName) {
#ifndef Q_OS_WASM
if (!QFile::exists(fileName)) {
// errorEdit->setText(tr("File does not exist!"));
return;
@ -245,8 +256,10 @@ void QmlBackend::parseFkp(const QString &fileName) {
}
*/
QDir::setCurrent(cwd);
#endif
}
#ifndef Q_OS_WASM
static void copyFkpHash2QHash(QHash<QString, QString> &dst, fkp_hash *from) {
dst.clear();
for (size_t i = 0; i < from->capacity; i++) {
@ -261,6 +274,7 @@ void QmlBackend::readHashFromParser() {
copyFkpHash2QHash(skills, parser->skills);
copyFkpHash2QHash(marks, parser->marks);
}
#endif
QString QmlBackend::calcFileMD5() {
return ::calcFileMD5();

View File

@ -1,7 +1,9 @@
#ifndef _QMLBACKEND_H
#define _QMLBACKEND_H
#ifndef Q_OS_WASM
#include "fkparse.h"
#endif
#include <qtmetamacros.h>
class QmlBackend : public QObject {
@ -48,7 +50,9 @@ signals:
private:
QQmlApplicationEngine *engine;
RSA *rsa;
#ifndef Q_OS_WASM
fkp_parser *parser;
#endif
QHash<QString, QString> generals;
QHash<QString, QString> skills;
QHash<QString, QString> marks;