diff --git a/.gitignore b/.gitignore index addb66a9..2af8dd74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Compile output /build/ /docs/build +/docs/.venv /*.o /zh_CN.qm /fk_ver diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 9df202de..094d10e3 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -19,3 +19,4 @@ ui.rst hegemony.rst shendiaochan.rst + schedule.rst diff --git a/docs/dev/schedule.rst b/docs/dev/schedule.rst new file mode 100644 index 00000000..04404c59 --- /dev/null +++ b/docs/dev/schedule.rst @@ -0,0 +1,102 @@ +.. SPDX-License-Identifier: GFDL-1.3-or-later + +关于Fk在同一个Lua中运行多个游戏房间的思考 +========================================= + +目前Fk的实现中,每个游戏房间是一根线程,这就出现了一个问题: +当同时游戏的房间很多的时候,会给RAM带来相当大的负担,毕竟lua_State太多了, +每个Room都有一个。 目前比较流行的模式是只需三人就可进行的斗地主, +有时候同时运行着30多桌斗地主,直接把内存推向1GB占用,云服务器那才多少点资源。 + +如果这么多房间都在同一个Lua运行的话,情况应该会有很大改观。 + +多个房间如何调度? +------------------ + +在当前阶段已经实现了重连和观战机制,这是借助于协程机制实现的。 +当Lua正在执行delay或者等待用户答复时,Lua会从主协程中切换出来, +然后去另一个协程处理诸如旁观、重连等游戏逻辑之外的请求。 + +如果把这种空白时间更加充分的利用起来的话,或许就能同时执行多个房间了。 + +现在每个游戏房间在Lua中都是一个Room对象的实例,而Room +本身是通过一个协程执行着主游戏逻辑。 +考虑修改一下,使得同一个Lua中能执行多个Room协程。 + +暂且考虑开个数组保存所有正在运行中的房间。 + +就绪队列 +-------- + +调度器除了维护运行中房间的数组之外,还维护一个就绪队列。 + +当房间不处于阻塞状态时,或者已经可以脱离阻塞状态,那么就认为他就绪。 + +此外还有一个特殊的协程,他用来处理托管、旁观等请求。 +当他的这个请求队列为空的时候,也视为他未就绪。 + +.. note:: + + 所谓阻塞状态目前指的是正在delay或者正在等候答复时。自然而然的, + 脱离阻塞状态就是delay的时间已经结束或者已收到答复。 + +当所有房间全部未就绪时,调度器调用sleep睡一段时间,让出CPU。 + +当房间因为delay而延时时,可以知道他恢复就绪的确切等待时间。但如果是等待答复、 +等待新请求这种依赖玩家操作的协程,其就绪时间就完全不可预测了。 +正是这种不可预测的等待时间才使得我们调度器只能小睡个几毫秒。 + +如何多睡一段时间? +------------------ + +假设有多个房处于delay中,那么能睡的最长时间就是其中所有delay剩余时间的最小值。 +但问题在于那些不可预测的就绪时间。如果另一个线程能告诉Lua答复已经就绪的话, +那么睡眠问题就好解决的多了。 + +实际上,可以借助信号量机制。当调度器开始睡觉时,肯定是调用了cpp函数。 +这个cpp函数里面先将一个信号量置为0,然后再tryAcquire这个信号量一段时间。 +当收到答复/收到请求时,把这个信号量+1。这样调度器就知道自己该醒过来啦。 + +如何避免房间等待太久? +---------------------- + +目前的调度思路如下: + +- 若就绪队列为空,那么从所有运行中的房间进行一遍筛选。 +- 若还为空,再筛选。 +- 假如第二遍还空,那就睡觉。 +- 假如就绪队列不空,那么弹出第一个,然后把控制权交给这个协程,等他自己让出。 + +在这个过程中,由于有些房间在等待就绪队列变空的过程中满足了就绪的条件, +那么再次筛选时,有可能重复的房间又会再次位于就绪队列靠前的位置。 +这虽然不会导致某个房间永久等待下去的情况,但却可能让一个房间等待太长时间, +那我就又要被艾特了。 + +一种或许可行的解法是,在再次刷新队列的时候,优先选出上次队列里面没有的房间。 +不知道这样如何呢? + +怎么把现有的改成这样? +---------------------- + +CPP代码方面,首先Room已经不再是一个线程了,自然不能再当QThread的子类。 +另外开个什么类来占据线程吧。 + +既然不是QThread的子类的话,那Room::run就得改了。原本是启动线程,现在还是改成 +pushRequest吧。新请求立刻唤醒调度器,然后调度器切到请求处理协程处理新房间请求, +然后就开战! + +既然要pushRequest,那么原先的请求队列也要从Room中移走了,这个得直接归属于线程。 +当然,lua_State也要从Room撤走。 + +Lua代码方面,首先由于Engine成了所有房间共用的变量,必须杜绝所有对Engine +内变量的修改。点名cost_data和card.mark,这两个东西得通过__index大法托管给 +Room挂着。剩下的没啥好说的,当然还有实现调度器。 + +协程池 +------ + +虽然题文无关,但是FK确实在运行过程中不断的产生着协程。 +为了避免因为不断产生新的协程导致的开销,可以使用协程池来管理。 + +用完的协程就存在某个数组留待下次使用。当需要启动一个新协程的时候, +先从协程池找,没有的话就新建一个。 diff --git a/lua/core/debug.lua b/lua/core/debug.lua index 423aa5f2..daa70fa9 100644 --- a/lua/core/debug.lua +++ b/lua/core/debug.lua @@ -4,5 +4,36 @@ inspect = require "inspect" dbg = require "debugger" +function PrintWhere() + local info = debug.getinfo(2) + local name = info.name + local line = info.currentline + local namewhat = info.namewhat + local shortsrc = info.short_src + if (namewhat == "method") and + (shortsrc ~= "[C]") and + (not string.find(shortsrc, "/lib")) then + print(shortsrc .. ":" .. line .. ": " .. name) + end +end +--debug.sethook(PrintWhere, "l") + +function Traceback() + print(debug.traceback()) +end + +local msgh = function(err) + fk.qCritical(err) + print(debug.traceback(nil, 2)) +end + +function Pcall(f, ...) + local ret = { xpcall(f, msgh, ...) } + local err = table.remove(ret, 1) + if err ~= false then + return table.unpack(ret) + end +end + function p(v) print(inspect(v)) end function pt(t) for k, v in pairs(t) do print(k, v) end end diff --git a/lua/core/player.lua b/lua/core/player.lua index 724454b4..635e09df 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -77,7 +77,6 @@ function Player:initialize() self.chained = false self.dying = false self.dead = false - self.state = "" self.drank = 0 self.player_skills = {} diff --git a/lua/core/skill.lua b/lua/core/skill.lua index 33db303d..5128e3e9 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -57,6 +57,20 @@ function Skill:initialize(name, frequency) self.attached_equip = nil end +function Skill:__index(k) + if k == "cost_data" then + return Fk:currentRoom().skill_costs[self.name] + end +end + +function Skill:__newindex(k, v) + if k == "cost_data" then + Fk:currentRoom().skill_costs[self.name] = v + else + rawset(self, k, v) + end +end + function Skill:__tostring() return "" end diff --git a/lua/core/util.lua b/lua/core/util.lua index cdbd7ca9..a90511e0 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -26,7 +26,9 @@ Util.lockTable = function(t) return setmetatable({}, new_mt) end -function printf(fmt, ...) print(string.format(fmt, ...)) end +function printf(fmt, ...) + print(string.format(fmt, ...)) +end -- the iterator of QList object local qlist_iterator = function(list, n) @@ -307,34 +309,6 @@ function string:endsWith(e) return e == "" or self:sub(-#e) == e end ----@class Sql -Sql = { - ---@param filename string - open = function(filename) - return fk.OpenDatabase(filename) - end, - - ---@param db fk.SQLite3 - close = function(db) - fk.CloseDatabase(db) - end, - - --- Execute an SQL statement. - ---@param db fk.SQLite3 - ---@param sql string - exec = function(db, sql) - fk.ExecSQL(db, sql) - end, - - --- Execute a `SELECT` SQL statement. - ---@param db fk.SQLite3 - ---@param sql string - ---@return table[] @ Array of Json object, the key is column name and value is row value - exec_select = function(db, sql) - return json.decode(fk.SelectFromDb(db, sql)) - end, -} - FileIO = { pwd = fk.QmlBackend_pwd, ls = function(filename) diff --git a/lua/lsp/player.lua b/lua/lsp/player.lua index 2dc130e1..39a7e886 100644 --- a/lua/lsp/player.lua +++ b/lua/lsp/player.lua @@ -8,42 +8,12 @@ FPlayer = {} ---@return integer id function FPlayer:getId()end ----@param id integer -function FPlayer:setId(id)end - ---@return string name function FPlayer:getScreenName()end ----@param name string -function FPlayer:setScreenName(name)end - ---@return string avatar function FPlayer:getAvatar()end ----@param avatar string -function FPlayer:setAvatar(avatar)end - ----@return string state -function FPlayer:getStateString()end - ----@param state string -function FPlayer:setStateString(state)end - ----@class fk.ServerPlayer : fk.Player -FServerPlayer = {} - ----@return fk.Server -function FServerPlayer:getServer()end - ----@return fk.Room -function FServerPlayer:getRoom()end - ----@param room fk.Room -function FServerPlayer:setRoom(room)end - ----@param msg string -function FServerPlayer:speak(msg)end - --- Send a request to client, and allow client to reply within *timeout* seconds. --- --- *timeout* must not be negative or **nil**. diff --git a/lua/lsp/server.lua b/lua/lsp/server.lua index 7f99a263..2205e73a 100644 --- a/lua/lsp/server.lua +++ b/lua/lsp/server.lua @@ -1,52 +1 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ----@meta - ----@class fk.Server -FServer = {} - ----@type fk.Server -fk.ServerInstance = {} - ----@class fk.Room ---- Room (C++) -FRoom = {} - ----@param owner fk.ServerPlayer ----@param name string ----@param capacity integer -function FServer:createRoom(owner,name,capacity)end - ----@param id integer ----@return fk.Room -function FServer:findRoom(id)end - ----@return fk.Room -function FServer:lobby()end - ----@param id integer ----@return fk.ServerPlayer -function FServer:findPlayer(id)end - ----@return fk.SQLite3 -function FServer:getDatabase()end - -function FRoom:getServer()end -function FRoom:getId()end -function FRoom:isLobby()end -function FRoom:getName()end -function FRoom:setName(name)end -function FRoom:getCapacity()end -function FRoom:setCapacity(capacity)end -function FRoom:isFull()end -function FRoom:isAbandoned()end -function FRoom:addPlayer(player)end -function FRoom:removePlayer(player)end -function FRoom:getOwner()end -function FRoom:setOwner(owner)end -function FRoom:getPlayers()end -function FRoom:findPlayer(id)end -function FRoom:getTimeout()end -function FRoom:isStarted()end -function FRoom:doBroadcastNotify(targets,command,jsonData)end -function FRoom:gameOver()end diff --git a/lua/lsp/sqlite.lua b/lua/lsp/sqlite.lua index 23145ce7..2205e73a 100644 --- a/lua/lsp/sqlite.lua +++ b/lua/lsp/sqlite.lua @@ -1,22 +1 @@ -- SPDX-License-Identifier: GPL-3.0-or-later - ----@meta - ----@class fk.SQLite3 -SQLite3 = {} - ----@param filename string ----@return fk.SQLite3 -function fk.OpenDatabase(filename)end - ----@param db fk.SQLite3 ----@param sql string ----@return string jsonData -function fk.SelectFromDb(db, sql)end - ----@param db fk.SQLite3 ----@param sql string -function fk.ExecSQL(db, sql)end - ----@param db fk.SQLite3 -function fk.CloseDatabase(db)end diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index f9e7b9be..76151365 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -47,16 +47,6 @@ local function discardInit(room, player) end end -local function checkNoHuman(room) - for _, p in ipairs(room.players) do - -- TODO: trust - if p.serverplayer:getStateString() == "online" then - return - end - end - room:gameOver("") -end - GameEvent.functions[GameEvent.DrawInitial] = function(self) local room = self.room @@ -96,6 +86,13 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self) local currentTime = os.time() local elapsed = 0 + for _, id in ipairs(luck_data.playerList) do + local pl = room:getPlayerById(id) + if luck_data[id].luckTime > 0 then + pl.serverplayer:setThinking(true) + end + end + while true do elapsed = os.time() - currentTime if remainTime - elapsed <= 0 then @@ -112,14 +109,16 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self) end for _, id in ipairs(ldata.playerList) do - if room:getPlayerById(id)._splayer:getStateString() ~= "online" then + local pl = room:getPlayerById(id) + if pl._splayer:getState() ~= fk.Player_Online then ldata[id].luckTime = 0 + pl.serverplayer:setThinking(false) end end -- room:setTag("LuckCardData", ldata) - checkNoHuman(room) + room:checkNoHuman() coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000) end diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index b3ac7b60..6c4d9bfd 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -186,13 +186,10 @@ function GameEvent:clear() logic.game_event_stack:pop() - local err, msg - err, msg = xpcall(self.exit_func, debug.traceback, self) - if err == false then fk.qCritical(msg) end + Pcall(self.exit_func, self) for _, f in ipairs(self.extra_exit_funcs) do if type(f) == "function" then - err, msg = xpcall(f, debug.traceback, self) - if err == false then fk.qCritical(msg) end + Pcall(f, self) end end end diff --git a/lua/server/request.lua b/lua/server/request.lua index 051caa62..dadc7823 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -105,6 +105,8 @@ request_handlers["luckcard"] = function(room, id, reqlist) if pdata.luckTime > 0 then p:doNotify("AskForLuckCard", pdata.luckTime) + else + p.serverplayer:setThinking(false) end room:setTag("LuckCardData", luck_data) @@ -132,23 +134,34 @@ request_handlers["changeself"] = function(room, id, reqlist) }) end +request_handlers["newroom"] = function(s, id) + s:registerRoom(id) +end + +-- 处理异步请求的协程,本身也是个死循环就是了。 +-- 为了适应调度器,目前又暂且将请求分为“耗时请求”和不耗时请求。 +-- 耗时请求处理后会立刻挂起。不耗时的请求则会不断处理直到请求队列空后再挂起。 local function requestLoop(self) - local rest_time = 0 while true do local ret = false - local request = self.room:fetchRequest() + local request = self.thread:fetchRequest() if request ~= "" then ret = true local reqlist = request:split(",") - local id = tonumber(reqlist[1]) - local command = reqlist[2] - request_handlers[command](self, id, reqlist) - elseif rest_time > 10 then - -- let current thread sleep 10ms - -- otherwise CPU usage will be 100% (infinite yield <-> resume loop) - fk.QThread_msleep(10) + local roomId = tonumber(table.remove(reqlist, 1)) + local room = self:getRoom(roomId) + + if room then + RoomInstance = room + local id = tonumber(reqlist[1]) + local command = reqlist[2] + request_handlers[command](room, id, reqlist) + RoomInstance = nil + end + end + if not ret then + coroutine.yield() end - rest_time = coroutine.yield(ret) end end diff --git a/lua/server/room.lua b/lua/server/room.lua index 7f150c9d..90713596 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -5,6 +5,8 @@ --- 一个房间中只有一个Room实例,保存在RoomInstance全局变量中。 ---@class Room : Object ---@field public room fk.Room @ C++层面的Room类实例,别管他就是了,用不着 +---@field public id integer @ 房间的id +---@field private main_co any @ 本房间的主协程 ---@field public players ServerPlayer[] @ 这个房间中所有参战玩家 ---@field public alive_players ServerPlayer[] @ 所有还活着的玩家 ---@field public observers fk.ServerPlayer[] @ 旁观者清单,这是c++玩家列表,别乱动 @@ -24,6 +26,7 @@ ---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动 ---@field public request_queue table ---@field public request_self table +---@field public skill_costs table @ 存放skill.cost_data用 local Room = class("Room") -- load classes used by the game @@ -62,46 +65,7 @@ dofile "lua/server/ai/init.lua" ---@param _room fk.Room function Room:initialize(_room) self.room = _room - _room.startGame = function(_self) - Room.initialize(self, _room) -- clear old data - self.settings = json.decode(_room:settings()) - Fk.disabled_packs = self.settings.disabledPack - Fk.disabled_generals = self.settings.disabledGenerals - local main_co = coroutine.create(function() - self:run() - end) - local request_co = coroutine.create(function() - self:requestLoop() - end) - local ret, err_msg, rest_time = true, true - while not self.game_finished do - ret, err_msg, rest_time = coroutine.resume(main_co, err_msg) - - -- handle error - if ret == false then - fk.qCritical(err_msg) - print(debug.traceback(main_co)) - break - end - - ret, err_msg = coroutine.resume(request_co, rest_time) - if ret == false then - fk.qCritical(err_msg) - print(debug.traceback(request_co)) - break - end - - -- If ret == true, then when err_msg is true, that means no request - end - - coroutine.close(main_co) - coroutine.close(request_co) - - if not self.game_finished then - self:doBroadcastNotify("GameOver", "") - self.room:gameOver() - end - end + self.id = _room:getId() self.players = {} self.alive_players = {} @@ -123,8 +87,119 @@ function Room:initialize(_room) end self.request_queue = {} self.request_self = {} + self.skill_costs = {} + + self.settings = json.decode(self.room:settings()) + Fk.disabled_packs = self.settings.disabledPack + Fk.disabled_generals = self.settings.disabledGenerals end +-- 供调度器使用的函数。能让房间开始运行/从挂起状态恢复。 +function Room:resume() + -- 如果还没运行的话就先创建自己的主协程 + if not self.main_co then + self.main_co = coroutine.create(function() + self:run() + end) + end + + local ret, err_msg, rest_time = true, true, nil + local main_co = self.main_co + + if self:checkNoHuman() then + return true + end + + if not self.game_finished then + ret, err_msg, rest_time = coroutine.resume(main_co, err_msg) + + -- handle error + if ret == false then + fk.qCritical(err_msg) + print(debug.traceback(main_co)) + goto GAME_OVER + end + + if rest_time == "over" then + goto GAME_OVER + end + + return false, rest_time + end + + ::GAME_OVER:: + coroutine.close(main_co) + self.main_co = nil + return true +end + +-- 供调度器使用的函数,用来指示房间是否就绪。 +-- 如果没有就绪的话,可能会返回第二个值来告诉调度器自己还有多久就绪。 +function Room:isReady() + -- 没有活人了?那就告诉调度器我就绪了,恢复时候就会自己杀掉 + if self:checkNoHuman(true) then + return true + end + + -- 因为delay函数而延时:判断延时是否已经结束。 + -- 注意整个delay函数的实现都搬到这来了,delay本身只负责挂起协程了。 + if self.in_delay then + local rest = self.delay_duration - (os.getms() - self.delay_start) / 1000 + if rest <= 0 then + self.in_delay = false + return true + end + return false, rest + end + + -- 剩下的就是因为等待应答而未就绪了 + -- 检查所有正在等回答的玩家,如果已经过了烧条时间 + -- 那么就不认为他还需要时间就绪了 + -- 然后在调度器第二轮刷新的时候就应该能返回自己已就绪 + local ret = true + local rest + for _, p in ipairs(self.players) do + -- 这里判断的话需要用_splayer了,不然一控多的情况下会导致重复判断 + if p._splayer:thinking() then + ret = false + -- 烧条烧光了的话就把thinking设为false + rest = p.request_timeout * 1000 - (os.getms() - + p.request_start) / 1000 + + if rest <= 0 then + p._splayer:setThinking(false) + end + end + end + return ret, (rest and rest > 1) and rest or nil +end + +function Room:checkNoHuman(chkOnly) + if #self.players == 0 then return end + + for _, p in ipairs(self.players) do + -- TODO: trust + if p.serverplayer:getState() == fk.Player_Online then + return + end + end + + if not chkOnly then + self:gameOver("") + end + return true +end + +function Room:__tostring() + return string.format("", self.id) +end + +--[[ 敢删就寄,算了 +function Room:__gc() + self.room:checkAbandoned() +end +--]] + --- 正式在这个房间中开始游戏。 --- --- 当这个函数返回之后,整个Room线程也宣告结束。 @@ -636,6 +711,12 @@ function Room:doRaceRequest(command, players, jsonData) table.removeOne(players, p) table.insertIfNeed(canceled_players, p) end + + -- 骗过调度器让他以为自己尚未就绪 + if p.id > 0 then + p.request_timeout = remainTime - elapsed + p.serverplayer:setThinking(true) + end end if winner then self:doBroadcastNotify("CancelRequest", "") @@ -657,21 +738,17 @@ function Room:doRaceRequest(command, players, jsonData) return ret end -Room.requestLoop = require "server.request" --- 延迟一段时间。 --- ---- 这个函数只应该在主协程中使用。 +--- 这个函数不应该在请求处理协程中使用。 ---@param ms integer @ 要延迟的毫秒数 function Room:delay(ms) local start = os.getms() - while true do - local rest = ms - (os.getms() - start) / 1000 - if rest <= 0 then - break - end - coroutine.yield("__handleRequest", rest) - end + self.delay_start = start + self.delay_duration = ms + self.in_delay = true + coroutine.yield("__handleRequest", ms) end --- 向多名玩家告知一次移牌行为。 @@ -1158,7 +1235,7 @@ function Room:askForGeneral(player, generals, n) if #generals == n then return n == 1 and generals[1] or generals end local defaultChoice = table.random(generals, n) - if (player.state == "online") then + if (player.serverplayer:getState() == fk.Player_Online) then local result = self:doRequest(player, command, json.encode{ generals, n }) local choices if result == "" then @@ -2766,6 +2843,8 @@ end --- 结束一局游戏。 ---@param winner string @ 获胜的身份,空字符串表示平局 function Room:gameOver(winner) + if not self.game_started then return end + self.logic:trigger(fk.GameFinished, nil, winner) self.game_started = false self.game_finished = true @@ -2794,7 +2873,16 @@ function Room:gameOver(winner) end self.room:gameOver() - coroutine.yield("__handleRequest", 0) + + if table.contains( + { "running", "normal" }, + coroutine.status(self.main_co) + ) then + coroutine.yield("__handleRequest", "over") + else + coroutine.close(self.main_co) + self.main_co = nil + end end ---@param card Card @@ -2913,6 +3001,4 @@ function Room:updateQuestSkillState(player, skillName, failed) }) end -function CreateRoom(_room) - RoomInstance = Room:new(_room) -end +return Room diff --git a/lua/server/scheduler.lua b/lua/server/scheduler.lua new file mode 100644 index 00000000..57a97694 --- /dev/null +++ b/lua/server/scheduler.lua @@ -0,0 +1,149 @@ +-- SPDX-License-Identifier: GPL-3.0-or-later + +local Room = require "server.room" + +--[[ +local verbose = function(...) + printf(...) +end +--]] + +-- 所有当前正在运行的房间(即游戏尚未结束的房间) +---@type table +local runningRooms = {} + +-- 所有处于就绪态的房间,以及request协程(如果就绪的话) +---@type Room[] +local readyRooms = {} + +local requestCo = coroutine.create(function(room) + require "server.request"(room) +end) + +-- 仿照Room接口编写的request协程处理器 +local requestRoom = setmetatable({ + + -- minDelayTime 是当没有任何就绪房间时,可以睡眠的时间。 + -- 因为这个时间是所有房间预期就绪用时的最小值,故称为minDelayTime。 + minDelayTime = -1, + + getRoom = function(_, roomId) + return runningRooms[roomId] + end, + + resume = function(self) + local err, msg = coroutine.resume(requestCo, self) + if err == false then + fk.qCritical(msg) + print(debug.traceback(requestCo)) + end + return nil, 0 + end, + + isReady = function(self) + return self.thread:hasRequest() + end, + + registerRoom = function(self, id) + local cRoom = self.thread:getRoom(id) + local room = Room:new(cRoom) + runningRooms[room.id] = room + end, + +}, { + __tostring = function() + return "" + end, +}) + +runningRooms[-1] = requestRoom + +-- 从所有运行中房间中挑出就绪的房间。 +-- 方法暂时就是最简单的遍历。 +local function refreshReadyRooms() + -- verbose '[+] Refreshing ready queue...' + for k, v in pairs(runningRooms) do + local ready, rest = v:isReady() + if ready then + table.insertIfNeed(readyRooms, v) + elseif rest and rest >= 0 then + local time = requestRoom.minDelayTime + time = math.min((time <= 0 and 9999999 or time), rest) + requestRoom.minDelayTime = math.ceil(time) + end + end + -- verbose('[+] now have %d ready rooms...', #readyRooms) +end + +-- 主循环。只要线程没有被杀掉,就一直循环下去。 +-- 函数每轮循环会从队列中取一个元素并交给控制权, +-- 如果没有,则尝试刷新队列,无法刷新则开始睡眠。 +local function mainLoop() + -- request协程的专用特判变量。因为处理request不应当重置睡眠时长 + local rest_sleep_time + + while not requestRoom.thread:isTerminated() do + local room = table.remove(readyRooms, 1) + if room then + -- verbose '============= LOOP ==============' + -- verbose('[*] Switching to %s...', tostring(room)) + + RoomInstance = (room ~= requestRoom and room or nil) + local over, rest = room:resume() + RoomInstance = nil + + if over then + -- verbose('[#] %s is finished, removing ...', tostring(room)) + room.logic = nil + runningRooms[room.id] = nil + else + local time = requestRoom.minDelayTime + if room == requestRoom then + rest = rest_sleep_time + end + + if rest and rest >= 0 then + time = math.min((time <= 0 and 9999999 or time), rest) + else + time = -1 + end + requestRoom.minDelayTime = math.ceil(time) + -- verbose("[+] minDelay is %d ms...", requestRoom.minDelayTime) + -- verbose('[-] %s successfully yielded, %d ready rooms left...', + -- tostring(room), #readyRooms) + end + else + refreshReadyRooms() + if #readyRooms == 0 then + refreshReadyRooms() + if #readyRooms == 0 then + local time = requestRoom.minDelayTime + -- verbose('[.] Sleeping for %d ms...', time) + local cur = os.getms() + + -- 调用RoomThread的trySleep函数开始真正的睡眠。会被wakeUp(c++)唤醒。 + requestRoom.thread:trySleep(time) + + -- verbose('[!] Waked up after %f ms...', (os.getms() - cur) / 1000) + + if time > 0 then + rest_sleep_time = math.floor(time - (os.getms() - cur) / 1000) + else + rest_sleep_time = -1 + end + + requestRoom.minDelayTime = -1 + end + end + end + end + -- verbose '=========== LOOP END ============' + -- verbose '[:)] Goodbye!' +end + +-- 当Cpp侧的RoomThread运行时,以下这个函数就是这个线程的主函数。 +-- 而这个函数里面又调用了上面的mainLoop。 +function InitScheduler(_thread) + requestRoom.thread = _thread + Pcall(mainLoop) +end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index b5c74599..bb54813a 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -24,7 +24,6 @@ function ServerPlayer:initialize(_self) self._splayer = _self -- 真正在玩的玩家 self._observers = { _self } -- "旁观"中的玩家,然而不包括真正的旁观者 self.id = _self:getId() - self.state = _self:getStateString() self.room = nil -- Below are for doBroadcastRequest @@ -86,46 +85,36 @@ function ServerPlayer:doRequest(command, jsonData, timeout) self.serverplayer:doRequest(command, jsonData, timeout) end -local function checkNoHuman(room) - for _, p in ipairs(room.players) do - -- TODO: trust - if p.serverplayer:getStateString() == "online" then - return - end - end - room:gameOver("") -end - - local function _waitForReply(player, timeout) local result local start = os.getms() - local state = player.serverplayer:getStateString() - if state ~= "online" then - if state ~= "robot" then - checkNoHuman(player.room) + local state = player.serverplayer:getState() + player.request_timeout = timeout + player.request_start = start + if state ~= fk.Player_Online then + if state ~= fk.Player_Robot then + player.room:checkNoHuman() player.room:delay(500) return "__cancel" end -- Let AI make reply. First handle request - local ret_msg = true - while ret_msg do - -- when ret_msg is false, that means there is no request in the queue - ret_msg = coroutine.yield("__handleRequest", 1) - end + -- coroutine.yield("__handleRequest", 0) - checkNoHuman(player.room) + player.room:checkNoHuman() player.ai:readRequestData() local reply = player.ai:makeReply() return reply end while true do + player.serverplayer:setThinking(true) result = player.serverplayer:waitForReply(0) if result ~= "__notready" then + player.serverplayer:setThinking(false) return result end local rest = timeout * 1000 - (os.getms() - start) / 1000 if timeout and rest <= 0 then + player.serverplayer:setThinking(false) return "" end coroutine.yield("__handleRequest", rest) @@ -277,7 +266,7 @@ end function ServerPlayer:reconnect() local room = self.room - self.serverplayer:setStateString("online") + self.serverplayer:setState(fk.Player_Online) self:doNotify("Setup", json.encode{ self.id, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3f97f135..1310d230 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ set(freekill_SRCS "server/server.cpp" "server/serverplayer.cpp" "server/room.cpp" + "server/roomthread.cpp" "ui/qmlbackend.cpp" "swig/freekill-wrap.cxx" ) diff --git a/src/network/router.cpp b/src/network/router.cpp index 91f73109..cf0f50e2 100644 --- a/src/network/router.cpp +++ b/src/network/router.cpp @@ -3,6 +3,7 @@ #include "router.h" #include "client.h" #include "client_socket.h" +#include "roomthread.h" #include #ifndef FK_CLIENT_ONLY #include "server.h" @@ -311,6 +312,11 @@ void Router::handlePacket(const QByteArray &rawPacket) { else if (type & TYPE_REPLY) { QMutexLocker locker(&replyMutex); + ServerPlayer *player = qobject_cast(parent()); + player->setThinking(false); + // qDebug() << "wake up!"; + player->getRoom()->getThread()->wakeUp(); + if (requestId != this->expectedReplyId) return; @@ -328,6 +334,7 @@ void Router::handlePacket(const QByteArray &rawPacket) { extraReplyReadySemaphore->release(); extraReplyReadySemaphore = nullptr; } + locker.unlock(); emit replyReady(); } diff --git a/src/server/room.cpp b/src/server/room.cpp index e0a93e36..0d309eab 100644 --- a/src/server/room.cpp +++ b/src/server/room.cpp @@ -5,48 +5,56 @@ #include #include +#include "roomthread.h" #include "server.h" #include "serverplayer.h" #include "util.h" -Room::Room(Server *server) { - setObjectName("Room"); +Room::Room(RoomThread *m_thread) { + auto server = ServerInstance; id = server->nextRoomId; server->nextRoomId++; this->server = server; - setParent(server); + this->m_thread = m_thread; + if (m_thread) { // In case of lobby + m_thread->addRoom(this); + } + // setParent(server); + m_abandoned = false; owner = nullptr; gameStarted = false; robot_id = -2; // -1 is reserved in UI logic timeout = 15; + m_ready = true; + // 如果是普通房间而不是大厅,就初始化Lua,否则置Lua为nullptr - L = nullptr; if (!isLobby()) { // 如果不是大厅,那么: // * 只要房间添加人了,那么从大厅中移掉这个人 // * 只要有人离开房间,那就把他加到大厅去 connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer); connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer); - - L = CreateLuaState(); - DoLuaScript(L, "lua/freekill.lua"); - DoLuaScript(L, "lua/server/room.lua"); - initLua(); } } Room::~Room() { - if (isRunning()) { - wait(); + if (gameStarted) { + gameOver(); + } + + if (m_thread) { + m_thread->removeRoom(this); } - if (L) - lua_close(L); } Server *Room::getServer() const { return server; } +RoomThread *Room::getThread() const { return m_thread; } + +void Room::setThread(RoomThread *t) { m_thread = t; } + int Room::getId() const { return id; } void Room::setId(int id) { this->id = id; } @@ -81,7 +89,20 @@ bool Room::isAbandoned() const { return true; } -void Room::setAbandoned(bool abandoned) { m_abandoned = abandoned; } +// Lua专用,lua room销毁时检查c++的Room是不是也差不多可以销毁了 +void Room::checkAbandoned() { + if (isAbandoned()) { + bool tmp = m_abandoned; + m_abandoned = true; + if (!tmp) { + emit abandoned(); + } else { + deleteLater(); + } + } +} + +void Room::setAbandoned(bool a) { m_abandoned = a; } ServerPlayer *Room::getOwner() const { return owner; } @@ -218,6 +239,8 @@ void Room::removePlayer(ServerPlayer *player) { // 原先的跑路机器人会在游戏结束后自动销毁掉 server->addPlayer(runner); + m_thread->wakeUp(); + // 发出信号,让大厅添加这个人 emit playerRemoved(runner); } @@ -397,7 +420,7 @@ void Room::gameOver() { // 玩家也不能在这里清除,因为要能返回原来房间继续玩呢 // players.clear(); // owner = nullptr; - clearRequest(); + // clearRequest(); } void Room::manuallyStart() { @@ -405,43 +428,11 @@ void Room::manuallyStart() { foreach (auto p, players) { p->setReady(false); } - start(); + gameStarted = true; + m_thread->pushRequest(QString("-1,%1,newroom").arg(QString::number(id))); } } -QString Room::fetchRequest() { - if (!gameStarted) - return ""; - 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) { - if (!gameStarted) - return; - request_queue_mutex.lock(); - request_queue.enqueue(req); - request_queue_mutex.unlock(); -} - -void Room::clearRequest() { - request_queue_mutex.lock(); - request_queue.clear(); - request_queue_mutex.unlock(); -} - -bool Room::hasRequest() const { return !request_queue.isEmpty(); } - -void Room::run() { - gameStarted = true; - - clearRequest(); - - // 此处调用了Lua Room:run()函数 - roomStart(); + m_thread->pushRequest(QString("%1,%2").arg(QString::number(id), req)); } diff --git a/src/server/room.h b/src/server/room.h index 6f33135f..95fd40a9 100644 --- a/src/server/room.h +++ b/src/server/room.h @@ -5,16 +5,19 @@ class Server; class ServerPlayer; +class RoomThread; -class Room : public QThread { +class Room : public QObject { Q_OBJECT public: - explicit Room(Server *m_server); + explicit Room(RoomThread *m_thread); ~Room(); // Property reader & setter // ==================================={ Server *getServer() const; + RoomThread *getThread() const; + void setThread(RoomThread *t); int getId() const; void setId(int id); bool isLobby() const; @@ -26,7 +29,8 @@ class Room : public QThread { const QByteArray getSettings() const; void setSettings(QByteArray settings); bool isAbandoned() const; - void setAbandoned(bool abandoned); // never use this function + void checkAbandoned(); + void setAbandoned(bool a); ServerPlayer *getOwner() const; void setOwner(ServerPlayer *owner); @@ -55,17 +59,8 @@ class Room : public QThread { void updateWinRate(int id, const QString &general, const QString &mode, int result); void gameOver(); - - void initLua(); - - void roomStart(); void manuallyStart(); - LuaFunction startGame; - - QString fetchRequest(); void pushRequest(const QString &req); - void clearRequest(); - bool hasRequest() const; signals: void abandoned(); @@ -73,11 +68,9 @@ class Room : public QThread { void playerAdded(ServerPlayer *player); void playerRemoved(ServerPlayer *player); - protected: - virtual void run(); - private: Server *server; + RoomThread *m_thread; int id; // Lobby's id is 0 QString name; // “阴间大乱斗” int capacity; // by default is 5, max is 8 @@ -90,12 +83,9 @@ class Room : public QThread { QList runned_players; int robot_id; bool gameStarted; + bool m_ready; int timeout; - - lua_State *L; - QMutex request_queue_mutex; - QQueue request_queue; // json string }; #endif // _ROOM_H diff --git a/src/server/roomthread.cpp b/src/server/roomthread.cpp new file mode 100644 index 00000000..1ca74e46 --- /dev/null +++ b/src/server/roomthread.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "roomthread.h" +#include "server.h" +#include "util.h" +#include + +RoomThread::RoomThread(Server *m_server) { + setObjectName("Room"); + this->m_server = m_server; + m_capacity = 100; // TODO: server cfg + terminated = false; + + L = CreateLuaState(); + DoLuaScript(L, "lua/freekill.lua"); + DoLuaScript(L, "lua/server/scheduler.lua"); + start(); +} + +RoomThread::~RoomThread() { + tryTerminate(); + if (isRunning()) { + wait(); + } + lua_close(L); + // foreach (auto room, room_list) { + // room->deleteLater(); + // } +} + +Server *RoomThread::getServer() const { + return m_server; +} + +bool RoomThread::isFull() const { + // return room_list.count() >= m_capacity; + return m_capacity <= 0; +} + +Room *RoomThread::getRoom(int id) const { + return m_server->findRoom(id); +} + +void RoomThread::addRoom(Room *room) { + Q_UNUSED(room); + m_capacity--; +} + +void RoomThread::removeRoom(Room *room) { + room->setThread(nullptr); + m_capacity++; +} + +QString RoomThread::fetchRequest() { + // if (!gameStarted) + // return ""; + request_queue_mutex.lock(); + QString ret = ""; + if (!request_queue.isEmpty()) { + ret = request_queue.dequeue(); + } + request_queue_mutex.unlock(); + return ret; +} + +void RoomThread::pushRequest(const QString &req) { + // if (!gameStarted) + // return; + request_queue_mutex.lock(); + request_queue.enqueue(req); + request_queue_mutex.unlock(); + wakeUp(); +} + +void RoomThread::clearRequest() { + request_queue_mutex.lock(); + request_queue.clear(); + request_queue_mutex.unlock(); +} + +bool RoomThread::hasRequest() { + request_queue_mutex.lock(); + auto ret = !request_queue.isEmpty(); + request_queue_mutex.unlock(); + return ret; +} + +void RoomThread::trySleep(int ms) { + if (sema_wake.available() > 0) { + sema_wake.acquire(sema_wake.available()); + } + + sema_wake.tryAcquire(1, ms); +} + +void RoomThread::wakeUp() { + sema_wake.release(1); +} + +void RoomThread::tryTerminate() { + terminated = true; + wakeUp(); +} + +bool RoomThread::isTerminated() const { + return terminated; +} diff --git a/src/server/roomthread.h b/src/server/roomthread.h new file mode 100644 index 00000000..f8baf4c3 --- /dev/null +++ b/src/server/roomthread.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef _ROOMTHREAD_H +#define _ROOMTHREAD_H + +#include +class Room; +class Server; + +class RoomThread : public QThread { + Q_OBJECT + public: + explicit RoomThread(Server *m_server); + ~RoomThread(); + + Server *getServer() const; + bool isFull() const; + + Room *getRoom(int id) const; + void addRoom(Room *room); + void removeRoom(Room *room); + + QString fetchRequest(); + void pushRequest(const QString &req); + void clearRequest(); + bool hasRequest(); + + void trySleep(int ms); + void wakeUp(); + + void tryTerminate(); + bool isTerminated() const; + + protected: + virtual void run(); + + private: + Server *m_server; + // QList room_list; + int m_capacity; + + lua_State *L; + QMutex request_queue_mutex; + QQueue request_queue; // json string + QSemaphore sema_wake; + volatile bool terminated; +}; + +#endif // _ROOMTHREAD_H diff --git a/src/server/server.cpp b/src/server/server.cpp index e6379c11..b3d1a2ed 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -14,6 +14,7 @@ #include "packman.h" #include "player.h" #include "room.h" +#include "roomthread.h" #include "router.h" #include "server_socket.h" #include "serverplayer.h" @@ -44,6 +45,8 @@ Server::Server(QObject *parent) : QObject(parent) { connect(lobby(), &Room::playerAdded, this, &Server::updateRoomList); connect(lobby(), &Room::playerRemoved, this, &Server::updateRoomList); + threads.append(new RoomThread(this)); + // 启动心跳包线程 auto heartbeatThread = QThread::create([=]() { while (true) { @@ -63,7 +66,7 @@ Server::Server(QObject *parent) : QObject(parent) { foreach (auto p, this->players.values()) { if (p->getState() == Player::Online && !p->alive) { - p->kicked(); + emit p->kicked(); } } } @@ -77,11 +80,11 @@ Server::~Server() { isListening = false; ServerInstance = nullptr; m_lobby->deleteLater(); - foreach (auto room, idle_rooms) { - room->deleteLater(); - } - foreach (auto room, rooms) { - room->deleteLater(); +// foreach (auto room, idle_rooms) { +// room->deleteLater(); +// } + foreach (auto thread, threads) { + thread->deleteLater(); } sqlite3_close(db); RSA_free(rsa); @@ -102,14 +105,27 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity, return; } Room *room; + RoomThread *thread = nullptr; + + foreach (auto t, threads) { + if (!t->isFull()) { + thread = t; + } + } + if (!thread && nextRoomId != 0) { + thread = new RoomThread(this); + threads.append(thread); + } + if (!idle_rooms.isEmpty()) { room = idle_rooms.pop(); room->setId(nextRoomId); nextRoomId++; room->setAbandoned(false); + room->setThread(thread); rooms.insert(room->getId(), room); } else { - room = new Room(this); + room = new Room(thread); connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); if (room->isLobby()) m_lobby = room; @@ -270,12 +286,10 @@ void Server::processRequest(const QByteArray &msg) { body << (cmp < 0 ? QString("[\"server is still on version %%2\",\"%1\"]") - .arg(FK_VERSION) - .arg("1") + .arg(FK_VERSION, "1") : QString( "[\"server is using version %%2, please update\",\"%1\"]") - .arg(FK_VERSION) - .arg("1")); + .arg(FK_VERSION, "1")); client->send(JsonArray2Bytes(body)); client->disconnectFromHost(); @@ -464,26 +478,15 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, void Server::onRoomAbandoned() { Room *room = qobject_cast(sender()); - if (room->isRunning()) { - room->wait(); - } room->gameOver(); rooms.remove(room->getId()); updateRoomList(); + // 按理说这时候就可以删除了,但是这里肯定比Lua先检测到。 + // 倘若在Lua的Room:gameOver时C++的Room被删除了问题就大了。 + // FIXME: 但是这终归是内存泄漏!以后啥时候再改吧。 // room->deleteLater(); idle_rooms.push(room); - // 懒得改了! - // 这里出bug的原因还是在于room的销毁工作没做好 - // room销毁这块bug很多 - // if (idle_rooms.length() > 10) { - // auto junk = idle_rooms[0]; - // idle_rooms.removeFirst(); - // junk->deleteLater(); - // } -#ifdef QT_DEBUG - qDebug() << rooms.size() << "running room(s)," << idle_rooms.size() - << "idle room(s)."; -#endif + room->getThread()->removeRoom(room); } void Server::onUserDisconnected() { diff --git a/src/server/server.h b/src/server/server.h index 564771ae..05bc5588 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -11,6 +11,7 @@ class ServerSocket; class ClientSocket; class ServerPlayer; +class RoomThread; #include "room.h" @@ -62,8 +63,9 @@ private: Room *m_lobby; QMap rooms; QStack idle_rooms; + QList threads; int nextRoomId; - friend Room::Room(Server *server); + friend Room::Room(RoomThread *m_thread); QHash players; RSA *rsa; diff --git a/src/server/serverplayer.cpp b/src/server/serverplayer.cpp index a35dce2c..6e88973c 100644 --- a/src/server/serverplayer.cpp +++ b/src/server/serverplayer.cpp @@ -3,6 +3,7 @@ #include "serverplayer.h" #include "client_socket.h" #include "room.h" +#include "roomthread.h" #include "router.h" #include "server.h" @@ -16,6 +17,7 @@ ServerPlayer::ServerPlayer(Room *room) { alive = true; m_busy = false; + m_thinking = false; } ServerPlayer::~ServerPlayer() { @@ -108,3 +110,16 @@ void ServerPlayer::kick() { } setSocket(nullptr); } + +bool ServerPlayer::thinking() { + m_thinking_mutex.lock(); + bool ret = m_thinking; + m_thinking_mutex.unlock(); + return ret; +} + +void ServerPlayer::setThinking(bool t) { + m_thinking_mutex.lock(); + m_thinking = t; + m_thinking_mutex.unlock(); +} diff --git a/src/server/serverplayer.h b/src/server/serverplayer.h index 30737e02..16361c01 100644 --- a/src/server/serverplayer.h +++ b/src/server/serverplayer.h @@ -40,6 +40,9 @@ public: bool busy() const { return m_busy; } void setBusy(bool busy) { m_busy = busy; } + + bool thinking(); + void setThinking(bool t); signals: void disconnected(); void kicked(); @@ -49,7 +52,9 @@ private: Router *router; Server *server; Room *room; // Room that player is in, maybe lobby - bool m_busy; + bool m_busy; // (Lua专用) 是否有doRequest没处理完?见于神貂蝉这种一控多的 + bool m_thinking; // 是否在烧条? + QMutex m_thinking_mutex; // 注意setBusy只在Lua使用,所以不需要锁。 QString requestCommand; QString requestData; diff --git a/src/swig/freekill-nogui.i b/src/swig/freekill-nogui.i index 06dff76c..d0f647b4 100644 --- a/src/swig/freekill-nogui.i +++ b/src/swig/freekill-nogui.i @@ -7,6 +7,7 @@ #include "serverplayer.h" #include "clientplayer.h" #include "room.h" +#include "roomthread.h" #include "util.h" #include "qmlbackend.h" class ClientPlayer *Self = nullptr; @@ -17,4 +18,3 @@ class ClientPlayer *Self = nullptr; %include "qml-nogui.i" %include "player.i" %include "server.i" -%include "sqlite3.i" diff --git a/src/swig/freekill.i b/src/swig/freekill.i index e8c4dd12..4b9521a5 100644 --- a/src/swig/freekill.i +++ b/src/swig/freekill.i @@ -8,6 +8,7 @@ #include "serverplayer.h" #include "clientplayer.h" #include "room.h" +#include "roomthread.h" #include "qmlbackend.h" #include "util.h" %} @@ -17,4 +18,3 @@ %include "player.i" %include "client.i" %include "server.i" -%include "sqlite3.i" diff --git a/src/swig/player.i b/src/swig/player.i index 8dae1942..85c9cbbb 100644 --- a/src/swig/player.i +++ b/src/swig/player.i @@ -8,6 +8,8 @@ public: Invalid, Online, Trust, + Run, + Robot, // only for real robot Offline }; @@ -21,12 +23,7 @@ public: void setAvatar(const QString &avatar); State getState() const; - QString getStateString() const; void setState(State state); - void setStateString(const QString &state); - - bool isReady() const; - void setReady(bool ready); }; %nodefaultctor ClientPlayer; diff --git a/src/swig/server.i b/src/swig/server.i index 00ef7659..f698add1 100644 --- a/src/swig/server.i +++ b/src/swig/server.i @@ -1,63 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later -%nodefaultctor Server; -%nodefaultdtor Server; -class Server : public QObject { -public: - Room *lobby() const; - void createRoom(ServerPlayer *owner, const QString &name, int capacity); - Room *findRoom(int id) const; - ServerPlayer *findPlayer(int id) const; - - sqlite3 *getDatabase(); -}; - -extern Server *ServerInstance; - %nodefaultctor Room; %nodefaultdtor Room; -class Room : public QThread { +class Room : public QObject { public: // Property reader & setter // ==================================={ - Server *getServer() const; int getId() const; - bool isLobby() const; - QString getName() const; - void setName(const QString &name); - int getCapacity() const; - void setCapacity(int capacity); - bool isFull() const; - bool isAbandoned() const; - ServerPlayer *getOwner() const; - void setOwner(ServerPlayer *owner); - - void addPlayer(ServerPlayer *player); - void addRobot(ServerPlayer *player); - void removePlayer(ServerPlayer *player); QList getPlayers() const; - ServerPlayer *findPlayer(int id) const; QList getObservers() const; int getTimeout() const; - - bool isStarted() const; - // ====================================} - - void doBroadcastNotify( - const QList targets, - const QString &command, - const QString &jsonData - ); + void checkAbandoned(); void updateWinRate(int id, const QString &general, const QString &mode, int result); void gameOver(); - - LuaFunction startGame; - QString fetchRequest(); - bool hasRequest() const; }; %extend Room { @@ -66,14 +25,28 @@ public: } } +%nodefaultctor RoomThread; +%nodefaultdtor RoomThread; +class RoomThread : public QThread { +public: + Room *getRoom(int id); + + QString fetchRequest(); + void clearRequest(); + bool hasRequest(); + + void trySleep(int ms); + bool isTerminated() const; +}; + %{ -void Room::initLua() +void RoomThread::run() { lua_getglobal(L, "debug"); lua_getfield(L, -1, "traceback"); lua_replace(L, -2); - lua_getglobal(L, "CreateRoom"); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); + lua_getglobal(L, "InitScheduler"); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_RoomThread, 0); int error = lua_pcall(L, 1, 0, -2); lua_pop(L, 1); if (error) { @@ -81,46 +54,20 @@ void Room::initLua() qCritical() << error_msg; } } - -void Room::roomStart() { - Q_ASSERT(startGame); - - lua_getglobal(L, "debug"); - lua_getfield(L, -1, "traceback"); - lua_replace(L, -2); - - lua_rawgeti(L, LUA_REGISTRYINDEX, startGame); - SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); - - int error = lua_pcall(L, 1, 0, -3); - - if (error) { - const char *error_msg = lua_tostring(L, -1); - qCritical() << error_msg; - lua_pop(L, 2); - } - lua_pop(L, 1); -} - %} %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(int timeout); void doNotify(const QString &command, const QString &json_data); - void prepareForRequest(const QString &command, const QString &data); - bool busy() const; void setBusy(bool busy); + + bool thinking(); + void setThinking(bool t); }; diff --git a/src/swig/sqlite3.i b/src/swig/sqlite3.i deleted file mode 100644 index 435a18fb..00000000 --- a/src/swig/sqlite3.i +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -struct sqlite3; - -sqlite3 *OpenDatabase(const QString &filename); -QString SelectFromDb(sqlite3 *db, const QString &sql); -void ExecSQL(sqlite3 *db, const QString &sql); -void CloseDatabase(sqlite3 *db);