diff --git a/doc/dev/protocol.md b/doc/dev/protocol.md index 8ac5372a..1b8a65b5 100644 --- a/doc/dev/protocol.md +++ b/doc/dev/protocol.md @@ -83,9 +83,9 @@ ___ ___ -## 断线重连(TODO) +## 断线重连 -根据用户名找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。 +根据用户id找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。 但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。 @@ -109,20 +109,30 @@ ___ 3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。 4. 玩家的信息就更多了,武将、身份、血量、id... -信息要怎么发呢: +所以Lua要在某时候让出一段时间,处理重连等其他内容,可能还会处理一下AI。 -- 一步一步的告诉重连中的玩家。 -- 全部汇总成字符串或者别的什么,然后可以压缩并发送。 -- 但以上两种都有问题:许多信息保存在Lua中,而Lua的运行是绝对不容其他线程打搅的。 -- 而且粗略一想,这些东西都应该非常耗时,而如今的线程只有Main线程和各大Room线程。有必要给Room加个子线程专门处理掉线这块的,然后Room该怎么跑继续怎么跑。 +这种让出时间处理的东西时间要尽可能的短,不要在里面搞个大循环。 -或者换个思路: +会阻塞住lua代码的函数有: -1. 首先EnterRoom消息,需要**人数**和**操作时长**。 -2. 服务端将这个客户端的*录像信息*发给客户端,客户端满速且不影响UI的播放录像。 -3. 在“播放录像”的过程中,客户端对于正在被收到的消息需进行特殊处理。 -4. 一个录像文件的体积会非常大。所以服务端所保存的客户端录像应该和真正的录像有差别才行。比如聊天、战报这种数据量大但又无关紧要的东西就不保存。 -5. 顺便这样也解决了多视角录像的问题,服务端给每个视角都录像就行了。 +- ServerPlayer:waitForReplay() +- Room:delay() + +在这里让出主线程,然后调度函数查找目前的请求列表。事实上,整个Room的游戏主流程就是一个协程: + +```lua +-- room.lua:53 +local co_func = function() + self:run() +end +local co = coroutine.create(co_func) +while not self.game_finished do + local ret, err_msg = coroutine.resume(co) + ... +end +``` + +如果在游戏流程中调用yield的话,那么这里的resume会返回true,然后可以带有额外的返回值。不过只要返回true就好了,这时候lua就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。 ___ diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 0275241a..62af5956 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -47,6 +47,7 @@ function GameLogic:assignRoles() local p = room.players[i] p.role = roles[i] if p.role == "lord" then + p.role_shown = true room:broadcastProperty(p, "role") else room:notifyProperty(p, p, "role") @@ -168,7 +169,7 @@ function GameLogic:action() if room.game_finished then break end room.current = room.current:getNextAlive() if checkNoHuman() then - room:gameOver() + room:gameOver("") end end end diff --git a/lua/server/room.lua b/lua/server/room.lua index 36278644..5b49e872 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -50,17 +50,26 @@ function Room:initialize(_room) self.room.startGame = function(_self) Room.initialize(self, _room) -- clear old data - local co_func = function() + local main_co = coroutine.create(function() self:run() - end - local co = coroutine.create(co_func) + end) + local request_co = coroutine.create(function() + self:requestLoop() + end) while not self.game_finished do - local ret, err_msg = coroutine.resume(co) + local ret, err_msg = coroutine.resume(main_co) -- handle error if ret == false then fk.qCritical(err_msg) - print(debug.traceback(co)) + print(debug.traceback(main_co)) + break + end + + ret, err_msg = coroutine.resume(request_co) + if ret == false then + fk.qCritical(err_msg) + print(debug.traceback(request_co)) break end end @@ -383,6 +392,33 @@ function Room:doRaceRequest(command, players, jsonData) end end +-- main loop for the request handling coroutine +function Room:requestLoop() + while true do + local request = self.room:fetchRequest() + if request ~= "" then + local id, command = table.unpack(request:split(",")) + id = tonumber(id) + if command == "reconnect" then + self:getPlayerById(id):reconnect() + end + end + coroutine.yield() + end +end + +-- delay function, should only be used in main coroutine +---@param ms integer @ millisecond to be delayed +function Room:delay(ms) + local start = fk.GetMicroSecond() + while true do + if fk.GetMicroSecond() - start >= ms * 1000 then + break + end + coroutine.yield() + end +end + ---@param players ServerPlayer[] ---@param card_moves CardsMoveStruct[] ---@param forceVisible boolean diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index b41db5d0..31a1050a 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -11,6 +11,7 @@ ---@field skipped_phases Phase[] ---@field phase_state table[] ---@field phase_index integer +---@field role_shown boolean local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) @@ -50,18 +51,28 @@ function ServerPlayer:doRequest(command, jsonData, timeout) self.serverplayer:doRequest(command, jsonData, timeout) end +local function _waitForReply(player, timeout) + local result + local start = fk.GetMicroSecond() + while true do + result = player.serverplayer:waitForReply(0) + if result ~= "__notready" then + return result + end + if timeout and (fk.GetMicroSecond() - start) / 1000 >= timeout * 1000 then + return "" + end + coroutine.yield() + end +end + --- Wait for at most *timeout* seconds for reply from client. --- --- If *timeout* is negative or **nil**, the function will wait forever until get reply. ---@param timeout integer @ seconds to wait ---@return string @ JSON data function ServerPlayer:waitForReply(timeout) - local result = "" - if timeout == nil then - result = self.serverplayer:waitForReply() - else - result = self.serverplayer:waitForReply(timeout) - end + local result = _waitForReply(self, timeout) self.request_data = "" self.client_reply = result if result == "__cancel" then @@ -72,6 +83,130 @@ function ServerPlayer:waitForReply(timeout) return result end +---@param player ServerPlayer +function ServerPlayer:marshal(player) + local room = self.room + room:notifyProperty(player, self, "maxHp") + room:notifyProperty(player, self, "hp") + -- TODO + --room:notifyProperty(player, self, "gender") + + if self.kingdom ~= Fk.generals[self.general].kingdom then + room:notifyProperty(player, self, "kingdom") + end + + if self.dead then + room:notifyProperty(player, self, "dead") + room:notifyProperty(player, self, "role") + else + room:notifyProperty(player, self, "seat") + room:notifyProperty(player, self, "phase") + end + + if not self.faceup then + room:notifyProperty(player, self, "faceup") + end + + if self.chained then + room:notifyProperty(player, self, "chained") + end + + local card_moves = {} + if #self.player_cards[Player.Hand] ~= 0 then + local info = {} + for _, i in ipairs(self.player_cards[Player.Hand]) do + table.insert(info, { cardId = i, fromArea = Card.DrawPile }) + end + local move = { + moveInfo = info, + to = self.id, + toArea = Card.PlayerHand + } + table.insert(card_moves, move) + end + if #self.player_cards[Player.Equip] ~= 0 then + local info = {} + for _, i in ipairs(self.player_cards[Player.Equip]) do + table.insert(info, { cardId = i, fromArea = Card.DrawPile }) + end + local move = { + moveInfo = info, + to = self.id, + toArea = Card.PlayerEquip + } + table.insert(card_moves, move) + end + if #self.player_cards[Player.Judge] ~= 0 then + local info = {} + for _, i in ipairs(self.player_cards[Player.Judge]) do + table.insert(info, { cardId = i, fromArea = Card.DrawPile }) + end + local move = { + moveInfo = info, + to = self.id, + toArea = Card.PlayerJudge + } + table.insert(card_moves, move) + end + if #card_moves > 0 then + room:notifyMoveCards({ player }, card_moves) + end + + -- TODO: pile, mark + + for _, s in ipairs(self.player_skills) do + player:doNotify("AddSkill", json.encode{self.id, s.name}) + end + + for k, v in pairs(self.cardUsedHistory) do + player:doNotify("AddCardUseHistory", json.encode{k, v}) + end + + if self.role_shown then + room:notifyProperty(player, self, "role") + end +end + +function ServerPlayer:reconnect() + local room = self.room + self.serverplayer:setStateString("online") + + self:doNotify("Setup", json.encode{ + self.id, + self.serverplayer:getScreenName(), + self.serverplayer:getAvatar(), + }) + self:doNotify("EnterLobby", "") + self:doNotify("EnterRoom", json.encode{ + #room.players, room.timeout, + }) + room:notifyProperty(self, self, "role") + + -- send player data + for _, p in ipairs(room:getOtherPlayers(self)) do + self:doNotify("AddPlayer", json.encode{ + p.id, + p.serverplayer:getScreenName(), + p.serverplayer:getAvatar(), + }) + end + + local player_circle = {} + for i = 1, #room.players do + table.insert(player_circle, room.players[i].id) + end + self:doNotify("ArrangeSeats", json.encode(player_circle)) + + for _, p in ipairs(room.players) do + room:notifyProperty(self, p, "general") + p:marshal(self) + end + + -- TODO: tell drawPile + + room:broadcastProperty(self, "state") +end + function ServerPlayer:isAlive() return self.dead == false end diff --git a/src/network/client_socket.cpp b/src/network/client_socket.cpp index 73af6dac..57fbee8a 100644 --- a/src/network/client_socket.cpp +++ b/src/network/client_socket.cpp @@ -35,8 +35,11 @@ void ClientSocket::connectToHost(const QString &address, ushort port) void ClientSocket::getMessage() { while (socket->canReadLine()) { - char msg[16000]; // buffer - socket->readLine(msg, sizeof(msg)); + auto msg = socket->readLine(); + if (msg.startsWith("Compressed")) { + msg = msg.sliced(10); + msg = qUncompress(QByteArray::fromBase64(msg)); + } emit message_got(msg); } } @@ -48,6 +51,12 @@ void ClientSocket::disconnectFromHost() void ClientSocket::send(const QByteArray &msg) { + if (msg.length() >= 1024) { + auto comp = qCompress(msg); + auto _msg = "Compressed" + comp.toBase64() + "\n"; + socket->write(_msg); + socket->flush(); + } socket->write(msg); if (!msg.endsWith("\n")) socket->write("\n"); diff --git a/src/network/router.cpp b/src/network/router.cpp index 221d82e4..11f7d306 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -70,7 +70,7 @@ void Router::request(int type, const QString& command, expectedReplyId = requestId; replyTimeout = timeout; requestStartTime = QDateTime::currentDateTime(); - m_reply = QString(); + m_reply = "__notready"; replyMutex.unlock(); QJsonArray body; @@ -126,14 +126,6 @@ void Router::cancelRequest() #endif } -QString Router::waitForReply() -{ -#ifndef Q_OS_WASM - replyReadySemaphore.acquire(); - return m_reply; -#endif -} - QString Router::waitForReply(int timeout) { #ifndef Q_OS_WASM diff --git a/src/network/router.h b/src/network/router.h index ff575cb2..a7440a7c 100644 --- a/src/network/router.h +++ b/src/network/router.h @@ -43,7 +43,6 @@ public: void cancelRequest(); void abortRequest(); - QString waitForReply(); QString waitForReply(int timeout); signals: diff --git a/src/server/room.cpp b/src/server/room.cpp index 50de1c2c..53cccf17 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -29,9 +29,7 @@ Room::Room(Server* server) Room::~Room() { - // TODO if (isRunning()) { - terminate(); wait(); } if (L) lua_close(L); @@ -259,16 +257,6 @@ bool Room::isStarted() const return gameStarted; } -void Room::doRequest(const QList targets, int timeout) -{ - // TODO -} - -void Room::doNotify(const QList targets, int timeout) -{ - // TODO -} - void Room::doBroadcastNotify(const QList targets, const QString& command, const QString& jsonData) { @@ -302,6 +290,22 @@ void Room::gameOver() owner = nullptr; } +QString Room::fetchRequest() { + request_queue_mutex.lock(); + QString ret = ""; + if (!request_queue.isEmpty()) { + ret = request_queue.dequeue(); + } + request_queue_mutex.unlock(); + return ret; +} + +void Room::pushRequest(const QString &req) { + request_queue_mutex.lock(); + request_queue.enqueue(req); + request_queue_mutex.unlock(); +} + void Room::run() { gameStarted = true; diff --git a/src/server/room.h b/src/server/room.h index 76c4e452..0426413b 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -40,9 +40,6 @@ public: bool isStarted() const; // ====================================} - void doRequest(const QList targets, int timeout); - void doNotify(const QList targets, int timeout); - void doBroadcastNotify( const QList targets, const QString &command, @@ -57,6 +54,9 @@ public: void roomStart(); LuaFunction startGame; + QString fetchRequest(); + void pushRequest(const QString &req); + signals: void abandoned(); @@ -82,7 +82,8 @@ private: int timeout; lua_State *L; - QMutex lua_mutex; + QMutex request_queue_mutex; + QQueue request_queue; // json string }; #endif // _ROOM_H diff --git a/src/server/server.cpp b/src/server/server.cpp index d7c9caa4..5f2e5f87 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -240,8 +240,16 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co passed = (passwordHash == arr[0].toString()); if (!passed) error_msg = "username or password error"; } else { - // TODO: reconnect here - error_msg = "others logged in with this name"; + auto player = players.value(id); + if (player->getState() == Player::Offline) { + auto room = player->getRoom(); + player->setSocket(client); + client->disconnect(this); + room->pushRequest(QString("%1,reconnect").arg(id)); + return; + } else { + error_msg = "others logged in with this name"; + } } } } else { @@ -289,7 +297,6 @@ void Server::onRoomAbandoned() updateRoomList(); //room->deleteLater(); if (room->isRunning()) { - room->terminate(); room->wait(); } idle_rooms.push(room); @@ -306,6 +313,7 @@ void Server::onUserDisconnected() Room *room = player->getRoom(); if (room->isStarted()) { player->setState(Player::Offline); + player->setSocket(nullptr); // TODO: add a robot } else { player->deleteLater(); diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index f6417f79..7d7c4363 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -80,20 +80,6 @@ void ServerPlayer::abortRequest() router->abortRequest(); } -QString ServerPlayer::waitForReply() -{ - QString ret; - Player::State state = getState(); - if (state != Player::Online) { - if (state != Player::Run) - QThread::sleep(1); - ret = QString("__state=%1").arg(getStateString()); - } else { - ret = router->waitForReply(); - } - return ret; -} - QString ServerPlayer::waitForReply(int timeout) { QString ret; diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index a8e34300..80432d6b 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -27,7 +27,6 @@ public: const QString &jsonData, int timeout = -1); void abortRequest(); QString waitForReply(int timeout); - QString waitForReply(); void doNotify(const QString &command, const QString &jsonData); void prepareForRequest(const QString &command, diff --git a/src/swig/server.i b/src/swig/server.i index e3e75aa0..763b1d40 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -42,8 +42,6 @@ public: bool isStarted() const; // ====================================} - void doRequest(const QList targets, int timeout); - void doNotify(const QList targets, int timeout); void doBroadcastNotify( const QList targets, const QString &command, @@ -53,6 +51,7 @@ public: void gameOver(); LuaFunction startGame; + QString fetchRequest(); }; %{ @@ -105,7 +104,6 @@ public: 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);