diff --git a/Fk/Cheat/PlayerDetail.qml b/Fk/Cheat/PlayerDetail.qml index 38088397..517c60ec 100644 --- a/Fk/Cheat/PlayerDetail.qml +++ b/Fk/Cheat/PlayerDetail.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Fk.Common import Fk.Pages import Fk.RoomElement @@ -22,18 +23,29 @@ Flickable { width: parent.width - 40 x: 20 - // TODO: player details - Text { - id: screenName - font.pixelSize: 18 - color: "#E4D5A0" - } + RowLayout { + spacing: 8 + Avatar { + id: avatar + Layout.preferredWidth: 56 + Layout.preferredHeight: 56 + general: "diaochan" + } - Text { - id: playerGameData - Layout.fillWidth: true - font.pixelSize: 18 - color: "#E4D5A0" + ColumnLayout { + Text { + id: screenName + font.pixelSize: 18 + color: "#E4D5A0" + } + + Text { + id: playerGameData + Layout.fillWidth: true + font.pixelSize: 18 + color: "#E4D5A0" + } + } } RowLayout { @@ -163,6 +175,7 @@ Flickable { if (id === 0) return; root.pid = id; + avatar.general = extra_data.photo.avatar; screenName.text = extra_data.photo.screenName; mainChara.name = extra_data.photo.general; deputyChara.name = extra_data.photo.deputyGeneral; @@ -182,9 +195,9 @@ Flickable { const h = (totalTime / 3600).toFixed(2); const m = Math.floor(totalTime / 60); if (m < 100) { - playerGameData.text += " " + luatr("TotalGameTime: %1 min").arg(m); + screenName.text += " (" + luatr("TotalGameTime: %1 min").arg(m) + ")"; } else { - playerGameData.text += " " + luatr("TotalGameTime: %1 h").arg(h); + screenName.text += " (" + luatr("TotalGameTime: %1 h").arg(h) + ")"; } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 60ce0e88..b2766c3a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,25 +1,29 @@ -FROM debian -USER root +FROM linuxcontainers/debian-slim:latest +# install dependencies +RUN apt update -y && apt upgrade -y && \ + apt install -y \ + gcc g++ cmake \ + liblua5.4-dev libsqlite3-dev libreadline-dev libssl-dev libgit2-dev swig qt6-base-dev qt6-tools-dev-tools \ + gosu && \ + apt clean -y && \ + rm -rf /var/lib/apt/lists/* + +# prepare source code COPY . /FreeKill -#update apt dependencies -RUN apt update -y && apt upgrade -y +# compile and install +RUN mkdir -p /FreeKill/build && \ + cd /FreeKill/build && cp -r /usr/include/lua5.4/* ../include && cmake .. -DFK_SERVER_ONLY= && make && \ + cd /FreeKill && cmake --install build --config Release && \ + cp /FreeKill/docker/docker-entrypoint.sh / && chmod +x /docker-entrypoint.sh && \ + mkdir /data && \ + cd / && rm -rf /FreeKill -#install compile tools -RUN apt install -y gcc g++ cmake -RUN apt install -y liblua5.4-dev libsqlite3-dev libreadline-dev libssl-dev libgit2-dev swig qt6-base-dev qt6-tools-dev-tools - -#change workdir to FreeKill -WORKDIR /FreeKill - -#compile source code -RUN mkdir build && cd build && cp -r /usr/include/lua5.4/* ../include && cmake .. -DFK_SERVER_ONLY= -RUN cd build && make - -#build soft link -RUN ln -s build/FreeKill +WORKDIR /data EXPOSE 9527 -ENTRYPOINT ["/FreeKill/FreeKill", "-s"] +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["FreeKill", "-s"] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 00000000..b2ed6b45 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +USER_ID=${LOCAL_USER_ID:-1000} + +if [ "${1#-}" != "$1" ]; then + set -- FreeKill -s "$@" +fi + +if [ "$1" = 'FreeKill' -a "$(id -u)" = '0' ]; then + id -u freekill >&/dev/null + if [ $? -ne 0 ]; then + useradd --shell /bin/bash -u $USER_ID -o -c "" -m freekill + usermod -aG root freekill + export HOME=/home/freekill + mkdir -p $HOME/.local/share + ln -s /data $HOME/.local/share/FreeKill + chown -R freekill:freekill $HOME + if [ ! -d "/data/server" ]; then + cp -r /usr/local/share/FreeKill/server /data + fi + if [ ! -d "/data/packages" ]; then + cp -r /usr/local/share/FreeKill/packages /data + fi + fi + chown -R freekill /data + exec gosu freekill "$0" "$@" +fi + +exec "$@" diff --git a/image/card/delayedTrick/unknown.png b/image/card/delayedTrick/unknown.png index 0c0f0c37..4731fd9b 100644 Binary files a/image/card/delayedTrick/unknown.png and b/image/card/delayedTrick/unknown.png differ diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index a92531eb..27cbc345 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -784,4 +784,8 @@ function GetMiniGame(gtype, p, data) } end +function ReloadPackage(path) + Fk:reloadPackage(path) +end + dofile "lua/client/i18n/init.lua" diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 1585aa3d..b3023764 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -47,7 +47,6 @@ function Engine:initialize() end Fk = self - self.extensions = { ["standard"] = { "standard" }, ["standard_cards"] = { "standard_cards" }, @@ -123,6 +122,76 @@ function Engine:loadPackage(pack) self:addGameModes(pack.game_modes) end +-- Don't do this +local package = package +function Engine:reloadPackage(path) + path = path:sub(1, #path - 4) + local oldPkg = package.loaded[path] + package.loaded[path] = nil + local ok, err = pcall(require, path) + if not ok then + package.loaded[path] = oldPkg + print("reload failed:", err) + return + end + + -- 阉割版重载机制,反正单机用 + local function replace(t, skill) + if not t then return end + for k, s in pairs(t) do + if s.name == skill.name then + t[k] = skill + break + end + end + end + + local function f(p) + self.packages[p.name] = p + local room = Fk:currentRoom() + local skills = p:getSkills() + local related = {} + for _, skill in ipairs(skills) do + table.insertTableIfNeed(related, skill.related_skills) + end + table.insertTableIfNeed(skills, related) + + for _, skill in ipairs(skills) do + if self.skills[skill.name].class ~= skill.class then + fk.qCritical("cannot change class of skill: " .. skill.name) + goto CONTINUE + end + self.skills[skill.name] = skill + if skill:isInstanceOf(TriggerSkill) and RoomInstance then + local logic = room.logic + for _, event in ipairs(skill.refresh_events) do + replace(logic.refresh_skill_table[event], skill) + end + for _, event in ipairs(skill.events) do + replace(logic.skill_table[event], skill) + end + end + if skill:isInstanceOf(StatusSkill) then + replace(room.status_skills[skill.class], skill) + end + + for _, p in ipairs(room.players) do + replace(p.player_skills, skill) + end + ::CONTINUE:: + end + end + + local pkg = package.loaded[path] + if type(pkg) ~= "table" then return end + if pkg.class and pkg:isInstanceOf(Package) then + f(pkg) + elseif path:endsWith("init") and not path:find("/ai/") then + for _, p in ipairs(pkg) do f(p) end + end +end + + --- 加载所有拓展包。 --- --- Engine会在packages/下搜索所有含有init.lua的文件夹,并把它们作为拓展包加载进来。 @@ -215,7 +284,7 @@ end function Engine:addSkill(skill) assert(skill.class:isSubclassOf(Skill)) if self.skills[skill.name] ~= nil then - error(string.format("Duplicate skill %s detected", skill.name)) + fk.qWarning(string.format("Duplicate skill %s detected", skill.name)) end self.skills[skill.name] = skill diff --git a/lua/server/ai/random_ai.lua b/lua/server/ai/random_ai.lua index cfb12ac9..6a5ca098 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -10,6 +10,8 @@ function RandomAI:useActiveSkill(skill, card) local room = self.room local player = self.player + if skill:isInstanceOf(ViewAsSkill) then return "" end + local filter_func = skill.cardFilter if card then filter_func = Util.FalseFunc @@ -68,6 +70,7 @@ function RandomAI:useVSSkill(skill, pattern, cancelable, extra_data) local player = self.player local room = self.room local precondition + if not skill then return nil end if self.command == "PlayCard" then precondition = skill:enabledAtPlay(player) diff --git a/lua/server/request.lua b/lua/server/request.lua index 2d36437d..1b9ff419 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -181,6 +181,12 @@ request_handlers["newroom"] = function(s, id) s:registerRoom(id) end +request_handlers["reloadpackage"] = function(room, id, reqlist) + if not IsConsoleStart() then return end + local path = reqlist[3] + Fk:reloadPackage(path) +end + -- 处理异步请求的协程,本身也是个死循环就是了。 -- 为了适应调度器,目前又暂且将请求分为“耗时请求”和不耗时请求。 -- 耗时请求处理后会立刻挂起。不耗时的请求则会不断处理直到请求队列空后再挂起。 diff --git a/lua/server/scheduler.lua b/lua/server/scheduler.lua index 2c1a16dd..e1bf22d3 100644 --- a/lua/server/scheduler.lua +++ b/lua/server/scheduler.lua @@ -154,3 +154,7 @@ function InitScheduler(_thread) requestRoom.thread = _thread Pcall(mainLoop) end + +function IsConsoleStart() + return requestRoom.thread:isConsoleStart() +end diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c74fbadc..f63910b2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,3 +97,25 @@ target_link_libraries(FreeKill PRIVATE ) install(TARGETS FreeKill DESTINATION bin) +install(DIRECTORY + ${PROJECT_SOURCE_DIR}/audio + ${PROJECT_SOURCE_DIR}/fonts + ${PROJECT_SOURCE_DIR}/image + ${PROJECT_SOURCE_DIR}/lua + ${PROJECT_SOURCE_DIR}/packages + ${PROJECT_SOURCE_DIR}/Fk + ${PROJECT_SOURCE_DIR}/server + DESTINATION share/FreeKill +) +install(FILES + ${PROJECT_SOURCE_DIR}/fk_ver + DESTINATION share/FreeKill +) + +if (NOT DEFINED FK_SERVER_ONLY) + install(FILES + ${CMAKE_BINARY_DIR}/zh_CN.qm + ${CMAKE_BINARY_DIR}/en_US.qm + DESTINATION share/FreeKill + ) +endif() diff --git a/src/client/client.cpp b/src/client/client.cpp index e4cdf03f..f2dc5e4c 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -3,7 +3,11 @@ #include "client.h" #include "client_socket.h" #include "clientplayer.h" +#include "qmlbackend.h" #include "util.h" +#include "server.h" +#include +#include Client *ClientInstance = nullptr; ClientPlayer *Self = nullptr; @@ -78,7 +82,10 @@ void Client::changeSelf(int id) { lua_State *Client::getLuaState() { return L; } -void Client::installAESKey(const QByteArray &key) { router->installAESKey(key); } +void Client::installAESKey(const QByteArray &key) { + startWatchFiles(); + router->installAESKey(key); +} void Client::saveRecord(const QString &json, const QString &fname) { if (!QDir("recording").exists()) { @@ -90,6 +97,48 @@ void Client::saveRecord(const QString &json, const QString &fname) { c.close(); } +bool Client::isConsoleStart() const { + if (!ClientInstance || !ServerInstance) { + return false; + } + + return router->isConsoleStart(); +} + +void Client::startWatchFiles() { + if (!isConsoleStart()) return; + if (!fsWatcher.files().empty()) return; + QFile flist("flist.txt"); + if (!flist.open(QIODevice::ReadOnly)) { + qCritical("Cannot open flist.txt. Won't watch files."); + fsWatcher.addPath("fk_ver"); // dummy + } + auto md5pairs = flist.readAll().split(';'); + foreach (auto md5, md5pairs) { + if (md5.isEmpty()) continue; + auto fname = md5.split('=')[0]; + if (fname.startsWith("packages") && fname.endsWith(".lua")) { + fsWatcher.addPath(fname); + } + } + connect(&fsWatcher, &QFileSystemWatcher::fileChanged, this, + &Client::updateLuaFiles); +} + void Client::processReplay(const QString &c, const QString &j) { callLua(c, j); } + +void Client::updateLuaFiles(const QString &path) { + if (!isConsoleStart()) return; + Backend->showToast(tr("File %1 changed, reloading...").arg(path)); + QThread::msleep(100); + Backend->callLuaFunction("ReloadPackage", { path }); + ClientInstance->notifyServer("PushRequest", + QString("reloadpackage,%1").arg(path)); + + // according to QT documentation + if (!fsWatcher.files().contains(path) && QFile::exists(path)) { + fsWatcher.addPath(path); + } +} diff --git a/src/client/client.h b/src/client/client.h index a98e4ba1..f5d0d7ab 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -5,6 +5,7 @@ #include "router.h" #include "clientplayer.h" +#include #ifndef FK_SERVER_ONLY #include "qmlbackend.h" @@ -34,18 +35,24 @@ public: void saveRecord(const QString &json, const QString &fname); + bool isConsoleStart() const; + void startWatchFiles(); signals: void error_message(const QString &msg); public slots: void processReplay(const QString &, const QString &); +private slots: + void updateLuaFiles(const QString &path); + private: Router *router; QMap players; ClientPlayer *self; lua_State *L; + QFileSystemWatcher fsWatcher; }; extern Client *ClientInstance; diff --git a/src/main.cpp b/src/main.cpp index fb2fd807..e9c7a453 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -173,6 +173,16 @@ void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, int main(int argc, char *argv[]) { // 初始化一下各种杂项信息 QThread::currentThread()->setObjectName("Main"); + + qInstallMessageHandler(fkMsgHandler); + QCoreApplication *app; + QCoreApplication::setApplicationName("FreeKill"); + QCoreApplication::setApplicationVersion(FK_VERSION); + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + prepareForLinux(); +#endif + if (!info_log) { info_log = fopen("freekill.server.info.log", "w+"); if (!info_log) { @@ -186,15 +196,6 @@ int main(int argc, char *argv[]) { } } - qInstallMessageHandler(fkMsgHandler); - QCoreApplication *app; - QCoreApplication::setApplicationName("FreeKill"); - QCoreApplication::setApplicationVersion(FK_VERSION); - -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - prepareForLinux(); -#endif - #ifndef FK_CLIENT_ONLY // 分析命令行,如果有 -s 或者 --server 就在命令行直接开服务器 QCommandLineParser parser; diff --git a/src/network/router.cpp b/src/network/router.cpp index 38c96437..4de18c85 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -53,6 +53,10 @@ void Router::installAESKey(const QByteArray &key) { socket->installAESKey(key); } +bool Router::isConsoleStart() const { + return socket->peerAddress() == "127.0.0.1"; +} + #ifndef FK_CLIENT_ONLY void Router::setReplyReadySemaphore(QSemaphore *semaphore) { extraReplyReadySemaphore = semaphore; diff --git a/src/network/router.h b/src/network/router.h index 62048a6b..7c8841b7 100644 --- a/src/network/router.h +++ b/src/network/router.h @@ -32,6 +32,7 @@ public: void setSocket(ClientSocket *socket); void removeSocket(); void installAESKey(const QByteArray &key); + bool isConsoleStart() const; #ifndef FK_CLIENT_ONLY void setReplyReadySemaphore(QSemaphore *semaphore); diff --git a/src/server/roomthread.cpp b/src/server/roomthread.cpp index a193576a..104d592c 100644 --- a/src/server/roomthread.cpp +++ b/src/server/roomthread.cpp @@ -4,6 +4,10 @@ #include "server.h" #include "util.h" +#ifndef FK_SERVER_ONLY +#include "client.h" +#endif + RoomThread::RoomThread(Server *m_server) { setObjectName("Room"); this->m_server = m_server; @@ -105,3 +109,12 @@ void RoomThread::tryTerminate() { bool RoomThread::isTerminated() const { return terminated; } + +bool RoomThread::isConsoleStart() const { +#ifndef FK_SERVER_ONLY + if (!ClientInstance) return false; + return ClientInstance->isConsoleStart(); +#else + return false; +#endif +} diff --git a/src/server/roomthread.h b/src/server/roomthread.h index f8baf4c3..15cd1964 100644 --- a/src/server/roomthread.h +++ b/src/server/roomthread.h @@ -31,6 +31,7 @@ class RoomThread : public QThread { void tryTerminate(); bool isTerminated() const; + bool isConsoleStart() const; protected: virtual void run(); diff --git a/src/swig/server.i b/src/swig/server.i index 3f73d8c6..82908a2b 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -39,6 +39,8 @@ public: void trySleep(int ms); bool isTerminated() const; + + bool isConsoleStart() const; }; %{