From 713bbca17adcc2ead1949beb987c320f01c157ed Mon Sep 17 00:00:00 2001 From: notify Date: Fri, 9 Jun 2023 01:10:16 +0800 Subject: [PATCH] Recorder (#178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供了一个简单的事件记录器机制和一个功能简单的查询函数。 在GameEvent的clear环节中,先执行默认的clear函数,再执行用户自订的clear函数。 --- lang/zh_CN.ts | 8 +- lua/client/i18n/zh_CN.lua | 1 + lua/core/general.lua | 4 + lua/core/player.lua | 12 +++ lua/core/util.lua | 30 ++++++- lua/lib/middleclass.lua | 3 + lua/server/event.lua | 5 +- lua/server/events/init.lua | 3 + lua/server/events/skill.lua | 14 +++- lua/server/gameevent.lua | 158 ++++++++++++++++++++++++++++++++---- lua/server/gamelogic.lua | 58 +++++++++++-- lua/server/room.lua | 9 +- lua/server/serverplayer.lua | 57 ++++++++----- 13 files changed, 306 insertions(+), 56 deletions(-) diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts index adb7ddce..1f401669 100644 --- a/lang/zh_CN.ts +++ b/lang/zh_CN.ts @@ -167,10 +167,6 @@ updated packages for md5 已为您与服务器同步拓展包,请尝试再次连入 - - Are you sure to exit? - 是否确认退出? - @@ -179,6 +175,10 @@ FreeKill 新月杀 + + Are you sure to exit? + 是否确认退出? + diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 6ce4d89d..22959182 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -303,6 +303,7 @@ Fk:loadTranslationTable{ -- phase ["#PhaseSkipped"] = "%from 跳过了 %arg", ["#GainAnExtraTurn"] = "%from 开始进行一个额外的回合", + ["#GainAnExtraPhase"] = "%from 开始进行一个额外的 %arg", -- useCard ["#UseCard"] = "%from 使用了牌 %card", diff --git a/lua/core/general.lua b/lua/core/general.lua index 1bb43d0a..70c81da1 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -59,6 +59,10 @@ function General:initialize(package, name, kingdom, hp, maxHp, gender) package:addGeneral(self) end +function General:__tostring() + return string.format("", self.name) +end + --- 为武将增加技能,需要注意增加其他武将技能时的处理方式。 ---@param skill Skill @ (单个)武将技能 function General:addSkill(skill) diff --git a/lua/core/player.lua b/lua/core/player.lua index 5ed8dd1b..1422e88c 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -444,6 +444,18 @@ function Player:inMyAttackRange(other, fixLimit) return self:distanceTo(other) <= (baseAttackRange + fixLimit) end +function Player:getNextAlive() + if Fk:currentRoom().alive_players == 0 then + return self + end + + local ret = self.next + while ret.dead do + ret = ret.next + end + return ret +end + --- 增加玩家使用特定牌的历史次数。 ---@param cardName string @ 牌名 ---@param num integer @ 次数 diff --git a/lua/core/util.lua b/lua/core/util.lua index dc26ebcb..cdbd7ca9 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -1,6 +1,32 @@ -- SPDX-License-Identifier: GPL-3.0-or-later local Util = {} +Util.DummyFunc = function() end +Util.DummyTable = setmetatable({}, { + __newindex = function() error("Cannot assign to dummy table") end +}) + +local metamethods = { + "__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv", + "__band", "__bor", "__bxor", "__bnot", "__shl", "__shr", + "__concat", "__len", "__eq", "__lt", "__le", "__call", + -- "__index", "__newindex", +} +-- 别对类用 暂且会弄坏isSubclassOf 懒得研究先 +Util.lockTable = function(t) + local mt = getmetatable(t) or Util.DummyTable + local new_mt = { + __index = t, + __newindex = function() error("Cannot assign to locked table") end, + __metatable = false, + } + for _, e in ipairs(metamethods) do + new_mt[e] = mt[e] + end + return setmetatable({}, new_mt) +end + +function printf(fmt, ...) print(string.format(fmt, ...)) end -- the iterator of QList object local qlist_iterator = function(list, n) @@ -150,13 +176,13 @@ function table.simpleClone(self) return ret end --- similar to table.clone but convert all class/instances to string +-- similar to table.clone but not clone class/instances function table.cloneWithoutClass(self) local ret = {} for k, v in pairs(self) do if type(v) == "table" then if v.class or v.super then - ret[k] = tostring(v) + ret[k] = v else ret[k] = table.cloneWithoutClass(v) end diff --git a/lua/lib/middleclass.lua b/lua/lib/middleclass.lua index b479f1ef..626a13d7 100644 --- a/lua/lib/middleclass.lua +++ b/lua/lib/middleclass.lua @@ -77,6 +77,9 @@ local function _call(self, ...) return self:new(...) end local function _createClass(name, super) local dict = {} dict.__index = dict + --[[ debug + dict.__gc = function(t) printf("%s destructed", tostring(t)) end + --]] local aClass = { name = name, super = super, static = {}, __instanceDict = dict, __declaredMethods = {}, diff --git a/lua/server/event.lua b/lua/server/event.lua index 2c31c4a2..a88deace 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -111,4 +111,7 @@ fk.CardShown = 77 -- 79 = BeforeTurnOver -- 80 = BeforeChainStateChange -fk.NumOfEvents = 81 +fk.SkillEffect = 81 +fk.AfterSkillEffect = 82 + +fk.NumOfEvents = 83 diff --git a/lua/server/events/init.lua b/lua/server/events/init.lua index 8706c6eb..139727e6 100644 --- a/lua/server/events/init.lua +++ b/lua/server/events/init.lua @@ -2,6 +2,9 @@ -- Definitions of game events +-- 某类事件对应的结束事件,其id刚好就是那个事件的相反数 +-- GameEvent.EventFinish = -1 + GameEvent.ChangeHp = 1 GameEvent.Damage = 2 GameEvent.LoseHp = 3 diff --git a/lua/server/events/skill.lua b/lua/server/events/skill.lua index dd1a8d81..ff391338 100644 --- a/lua/server/events/skill.lua +++ b/lua/server/events/skill.lua @@ -1,6 +1,16 @@ -- SPDX-License-Identifier: GPL-3.0-or-later GameEvent.functions[GameEvent.SkillEffect] = function(self) - local effect_cb = table.unpack(self.data) - return effect_cb() + local effect_cb, player, skill = table.unpack(self.data) + local room = self.room + local logic = room.logic + + local cost_data_bak = skill.cost_data + logic:trigger(fk.SkillEffect, player, skill) + skill.cost_data = cost_data_bak + + local ret = effect_cb() + + logic:trigger(fk.AfterSkillEffect, player, skill) + return ret end diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index 4e135c9f..2ea21a0d 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -1,40 +1,70 @@ -- SPDX-License-Identifier: GPL-3.0-or-later ---@class GameEvent: Object ----@field public room Room ----@field public event integer ----@field public data any ----@field public parent GameEvent ----@field public main_func fun(self: GameEvent) ----@field public clear_func fun(self: GameEvent) ----@field public extra_clear_funcs any[] ----@field public interrupted boolean +---@field public id integer @ 事件的id,随着时间推移自动增加并分配给新事件 +---@field public end_id integer @ 事件的对应结束id,如果整个事件中未插入事件,那么end_id就是自己的id +---@field public room Room @ room实例 +---@field public event integer @ 该事件对应的EventType +---@field public data any @ 事件的附加数据,视类型而定 +---@field public parent GameEvent @ 事件的父事件(栈中的上一层事件) +---@field public main_func fun(self: GameEvent) @ 事件的主函数 +---@field public clear_func fun(self: GameEvent) @ 事件结束时执行的函数 +---@field public extra_clear_funcs fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表 +---@field public exit_func fun(self: GameEvent) @ 事件结束后执行的函数 +---@field public extra_exit_funcs fun(self:GameEvent)[] @ 事件结束后执行的自定义函数 +---@field public interrupted boolean @ 事件是否是因为被强行中断而结束的 local GameEvent = class("GameEvent") +---@type fun(self: GameEvent)[] GameEvent.functions = {} + +---@type fun(self: GameEvent)[] GameEvent.cleaners = {} + +---@type fun(self: GameEvent)[] +GameEvent.exit_funcs = {} + local function wrapCoFunc(f, ...) if not f then return nil end local args = {...} return function() return f(table.unpack(args)) end end -local function dummyFunc() end +local dummyFunc = Util.DummyFunc function GameEvent:initialize(event, ...) + self.id = -1 + self.end_id = -1 self.room = RoomInstance self.event = event self.data = { ... } self.main_func = wrapCoFunc(GameEvent.functions[event], self) or dummyFunc self.clear_func = GameEvent.cleaners[event] or dummyFunc - self.extra_clear_funcs = {} + self.extra_clear_funcs = Util.DummyTable + self.exit_func = GameEvent.exit_funcs[event] or dummyFunc + self.extra_exit_funcs = Util.DummyTable self.interrupted = false end function GameEvent:__tostring() - return GameEvent:translate(self.event) + return string.format("<%s #%d>", GameEvent:translate(self.event), self.id) end -function GameEvent:findParent(eventType) +function GameEvent:addCleaner(f) + if self.extra_clear_funcs == Util.DummyTable then + self.extra_clear_funcs = {} + end + table.insert(self.extra_clear_funcs, f) +end + +function GameEvent:addExitFunc(f) + if self.extra_exit_funcs == Util.DummyTable then + self.extra_exit_funcs = {} + end + table.insert(self.extra_exit_funcs, f) +end + +function GameEvent:findParent(eventType, includeSelf) + if includeSelf and self.event == eventType then return self end local e = self.parent repeat if e.event == eventType then return e end @@ -43,12 +73,83 @@ function GameEvent:findParent(eventType) return nil end +-- 找n个id介于from和to之间的事件。 +local function bin_search(events, from, to, n, func) + local left = 1 + local right = #events + local mid + local ret = {} + + if from < events[1].id then + mid = 1 + elseif from > events[right].id then + return ret + else + while true do + if left > right then return ret end + mid = (left + right) // 2 + local id = events[mid].id + local id_left = mid == 1 and -math.huge or events[mid - 1].id + + if from < id then + if from >= id_left then + break + end + right = mid - 1 + else + left = mid + 1 + end + end + end + + for i = mid, #events do + local v = events[i] + if v.id < to and func(v) then + table.insert(ret, v) + end + if #ret >= n then break end + end + + return ret +end + +-- 从某个区间中,找出类型符合且符合func函数检测的至多n个事件。 +---@param eventType integer @ 要查找的事件类型 +---@param n integer @ 最多找多少个 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param endEvent GameEvent|nil @ 区间终止点,默认为本事件结束 +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameEvent:searchEvents(eventType, n, func, endEvent) + local logic = self.room.logic + local events = logic.event_recorder[eventType] + local from = self.id + local to = endEvent and endEvent.id or self.end_id + if to == -1 then to = #logic.all_game_events end + n = n or 1 + func = func or function() return true end + + local ret + if #events < 6 then + ret = {} + for _, v in ipairs(events) do + if v.id > from and v.id < to and func(v) then + table.insert(ret, v) + end + if #ret >= n then break end + end + else + ret = bin_search(events, from, to, n, func) + end + + return ret +end + function GameEvent:clear() local clear_co = coroutine.create(function() + self:clear_func() for _, f in ipairs(self.extra_clear_funcs) do if type(f) == "function" then f(self) end end - self:clear_func() end) while true do @@ -72,6 +173,28 @@ function GameEvent:clear() break end end + + local logic = RoomInstance.logic + local end_id = logic.current_event_id + 1 + if self.id ~= end_id - 1 then + logic.all_game_events[end_id] = -self.event + logic.current_event_id = end_id + self.end_id = end_id + else + self.end_id = self.id + end + + 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 + 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 + end + end end local function breakEvent(self, extra_yield_result) @@ -95,6 +218,12 @@ function GameEvent:exec() self.parent = logic:getCurrentEvent() logic.game_event_stack:push(self) + logic.current_event_id = logic.current_event_id + 1 + self.id = logic.current_event_id + logic.all_game_events[self.id] = self + logic.event_recorder[self.event] = logic.event_recorder[self.event] or {} + table.insert(logic.event_recorder[self.event], self) + local co = coroutine.create(self.main_func) while true do local err, yield_result, extra_yield_result = coroutine.resume(co) @@ -123,7 +252,7 @@ function GameEvent:exec() -- yield to corresponding GameEvent, first pop self from stack self.interrupted = true self:clear() - logic.game_event_stack:pop(self) + -- logic.game_event_stack:pop(self) coroutine.close(co) -- then, call yield @@ -151,7 +280,6 @@ function GameEvent:exec() end end - logic.game_event_stack:pop(self) return ret, extra_ret end diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 3d0e4893..61c42eaf 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -3,12 +3,14 @@ ---@class GameLogic: Object ---@field public room Room ---@field public skill_table table ----@field public skill_priority_table +---@field public skill_priority_table table ---@field public refresh_skill_table table ---@field public skills string[] ----@field public event_stack Stack ---@field public game_event_stack Stack ---@field public role_table string[][] +---@field public all_game_events GameEvent[] +---@field public event_recorder table +---@field public current_event_id integer local GameLogic = class("GameLogic") function GameLogic:initialize(room) @@ -17,8 +19,10 @@ function GameLogic:initialize(room) self.skill_priority_table = {} self.refresh_skill_table = {} self.skills = {} -- skillName[] - self.event_stack = Stack:new() self.game_event_stack = Stack:new() + self.all_game_events = {} + self.event_recorder = {} + self.current_event_id = 0 self.role_table = { { "lord" }, @@ -381,8 +385,6 @@ function GameLogic:trigger(event, target, data, refresh_only) local _target = room.current -- for iteration local player = _target - self.event_stack:push({event, target, data}) - if #skills_to_refresh > 0 then repeat do -- refresh skills. This should not be broken for _, skill in ipairs(skills_to_refresh) do @@ -450,7 +452,6 @@ function GameLogic:trigger(event, target, data, refresh_only) ::trigger_loop_continue:: end - self.event_stack:pop() return broken end @@ -459,6 +460,29 @@ function GameLogic:getCurrentEvent() return self.game_event_stack.t[self.game_event_stack.p] end +-- 在指定历史范围中找至多n个符合条件的事件 +---@param eventType integer @ 要查找的事件类型 +---@param n integer @ 最多找多少个 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param scope integer @ 查询历史范围,只能是当前阶段/回合/轮次 +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameLogic:getEventsOfScope(eventType, n, func, scope) + scope = scope or Player.HistoryTurn + local event = self:getCurrentEvent() + local start_event ---@type GameEvent + if scope == Player.HistoryGame then + start_event = self.all_game_events[1] + elseif scope == Player.HistoryRound then + start_event = event:findParent(GameEvent.Round) + elseif scope == Player.HistoryTurn then + start_event = event:findParent(GameEvent.Turn) + elseif scope == Player.HistoryPhase then + start_event = event:findParent(GameEvent.Phase) + end + + return start_event:searchEvents(eventType, n, func) +end + function GameLogic:dumpEventStack(detailed) local top = self:getCurrentEvent() local i = self.game_event_stack.p @@ -493,6 +517,28 @@ function GameLogic:dumpEventStack(detailed) print("\n===== End of event stack dump =====") end +function GameLogic:dumpAllEvents(from, to) + from = from or 1 + to = to or #self.all_game_events + assert(from <= to) + + local indent = 0 + local tab = " " + for i = from, to, 1 do + local v = self.all_game_events[i] + if type(v) == "number" then + indent = math.max(indent - 1, 0) + -- v = "End" + -- print(tab:rep(indent) .. string.format("#%d: %s", i, v)) + else + print(tab:rep(indent) .. string.format("%s", tostring(v))) + if v.id ~= v.end_id then + indent = indent + 1 + end + end + end +end + function GameLogic:breakEvent(ret) coroutine.yield("__breakEvent", ret) end diff --git a/lua/server/room.lua b/lua/server/room.lua index ed6eba0c..71bab027 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -62,8 +62,7 @@ dofile "lua/server/ai/init.lua" ---@param _room fk.Room function Room:initialize(_room) self.room = _room - - self.room.startGame = function(_self) + _room.startGame = function(_self) Room.initialize(self, _room) -- clear old data self.settings = json.decode(_room:settings()) Fk.disabled_packs = self.settings.disabledPack @@ -1387,7 +1386,7 @@ function Room:handleUseCardReply(player, data) Self = player local c = skill:viewAs(selected_cards) if c then - self:useSkill(player, skill) + self:useSkill(player, skill, Util.DummyFunc) local use = {} ---@type CardUseStruct use.from = player.id @@ -2177,7 +2176,7 @@ function Room:handleCardEffect(event, cardEffectEvent) if cardEffectEvent.card.skill then execGameEvent(GameEvent.SkillEffect, function () cardEffectEvent.card.skill:onEffect(self, cardEffectEvent) - end) + end, self:getPlayerById(cardEffectEvent.from), cardEffectEvent.card.skill) end end end @@ -2691,7 +2690,7 @@ function Room:useSkill(player, skill, effect_cb) player:addSkillUseHistory(skill.name) if effect_cb then - return execGameEvent(GameEvent.SkillEffect, effect_cb) + return execGameEvent(GameEvent.SkillEffect, effect_cb, player, skill) end end diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index b78f3be3..35667d60 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -322,18 +322,6 @@ function ServerPlayer:isAlive() return self.dead == false end -function ServerPlayer:getNextAlive() - if #self.room.alive_players == 0 then - return self - end - - local ret = self.next - while ret.dead do - ret = ret.next - end - return ret -end - function ServerPlayer:turnOver() if self.room.logic:trigger(fk.BeforeTurnOver, self) then return @@ -367,6 +355,13 @@ function ServerPlayer:showCards(cards) room.logic:trigger(fk.CardShown, self, { cardIds = cards }) end +local phase_name_table = { + [Player.Judge] = "phase_judge", + [Player.Draw] = "phase_draw", + [Player.Play] = "phase_play", + [Player.Discard] = "phase_discard", +} + ---@param from_phase Phase ---@param to_phase Phase function ServerPlayer:changePhase(from_phase, to_phase) @@ -397,25 +392,35 @@ function ServerPlayer:changePhase(from_phase, to_phase) return false end -function ServerPlayer:gainAnExtraPhase(phase) +function ServerPlayer:gainAnExtraPhase(phase, delay) local room = self.room + delay = (delay == nil) and true or delay + if delay then + local logic = room.logic + local turn = logic:getCurrentEvent():findParent(GameEvent.Phase, true) + if turn then + turn:addExitFunc(function() self:gainAnExtraPhase(phase, false) end) + return + end + end + local current = self.phase self.phase = phase room:notifyProperty(self, self, "phase") + room:sendLog{ + type = "#GainAnExtraPhase", + from = self.id, + arg = phase_name_table[phase], + } + + GameEvent(GameEvent.Phase, self):exec() self.phase = current room:notifyProperty(self, self, "phase") end -local phase_name_table = { - [Player.Judge] = "phase_judge", - [Player.Draw] = "phase_draw", - [Player.Play] = "phase_play", - [Player.Discard] = "phase_discard", -} - ---@param phase_table Phase[] function ServerPlayer:play(phase_table) phase_table = phase_table or {} @@ -505,8 +510,18 @@ function ServerPlayer:skip(phase) end end -function ServerPlayer:gainAnExtraTurn() +function ServerPlayer:gainAnExtraTurn(delay) local room = self.room + delay = (delay == nil) and true or delay + if delay then + local logic = room.logic + local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true) + if turn then + turn:addExitFunc(function() self:gainAnExtraTurn(false) end) + return + end + end + room:sendLog{ type = "#GainAnExtraTurn", from = self.id