Reconnect (#46)
* 1 * room:delay and other coroutine * yield in waitForReply * fix stack overflow bug * compress large message when transmiting * delete useless func * reconnect * update doc for reconnect
This commit is contained in:
parent
2ea7acfb7a
commit
a8cd7431ef
|
@ -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就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。
|
||||
|
||||
___
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -43,7 +43,6 @@ public:
|
|||
void cancelRequest();
|
||||
void abortRequest();
|
||||
|
||||
QString waitForReply();
|
||||
QString waitForReply(int timeout);
|
||||
|
||||
signals:
|
||||
|
|
|
@ -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<ServerPlayer *> targets, int timeout)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void Room::doNotify(const QList<ServerPlayer *> targets, int timeout)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void Room::doBroadcastNotify(const QList<ServerPlayer *> 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;
|
||||
|
|
|
@ -40,9 +40,6 @@ public:
|
|||
bool isStarted() const;
|
||||
// ====================================}
|
||||
|
||||
void doRequest(const QList<ServerPlayer *> targets, int timeout);
|
||||
void doNotify(const QList<ServerPlayer *> targets, int timeout);
|
||||
|
||||
void doBroadcastNotify(
|
||||
const QList<ServerPlayer *> 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<QString> request_queue; // json string
|
||||
};
|
||||
|
||||
#endif // _ROOM_H
|
||||
|
|
|
@ -240,10 +240,18 @@ 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
|
||||
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 {
|
||||
error_msg = "invalid user name";
|
||||
}
|
||||
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -42,8 +42,6 @@ public:
|
|||
bool isStarted() const;
|
||||
// ====================================}
|
||||
|
||||
void doRequest(const QList<ServerPlayer *> targets, int timeout);
|
||||
void doNotify(const QList<ServerPlayer *> targets, int timeout);
|
||||
void doBroadcastNotify(
|
||||
const QList<ServerPlayer *> 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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue