-- SPDX-License-Identifier: GPL-3.0-or-later ---@class ServerPlayer : Player ---@field public serverplayer fk.ServerPlayer ---@field public room Room ---@field public next ServerPlayer ---@field public request_data string ---@field public client_reply string ---@field public default_reply string ---@field public reply_ready boolean ---@field public reply_cancel boolean ---@field public phases Phase[] ---@field public skipped_phases Phase[] ---@field public phase_state table[] ---@field public phase_index integer ---@field public role_shown boolean ---@field private _fake_skills Skill[] ---@field private _manually_fake_skills Skill[] ---@field public prelighted_skills Skill[] ---@field private _timewaste_count integer ---@field public ai AI ---@field public ai_data any local ServerPlayer = Player:subclass("ServerPlayer") function ServerPlayer:initialize(_self) Player.initialize(self) self.serverplayer = _self -- 控制者 self._splayer = _self -- 真正在玩的玩家 self._observers = { _self } -- "旁观"中的玩家,然而不包括真正的旁观者 self.id = _self:getId() self.room = nil -- Below are for doBroadcastRequest self.request_data = "" self.client_reply = "" self.default_reply = "" self.reply_ready = false self.reply_cancel = false self.phases = {} self.skipped_phases = {} self._fake_skills = {} self._manually_fake_skills = {} self.prelighted_skills = {} self._prelighted_skills = {} self._timewaste_count = 0 self.ai = SmartAI:new(self) end ---@param command string ---@param jsonData string function ServerPlayer:doNotify(command, jsonData) for _, p in ipairs(self._observers) do p:doNotify(command, jsonData) end local room = self.room for _, t in ipairs(room.observers) do local id, p = table.unpack(t) if id == self.id and room.room:hasObserver(p) then p:doNotify(command, jsonData) end end end --- Send a request to client, and allow client to reply within *timeout* seconds. --- --- *timeout* must not be negative. If nil, room.timeout is used. ---@param command string ---@param jsonData string ---@param timeout integer|nil function ServerPlayer:doRequest(command, jsonData, timeout) self.client_reply = "" self.reply_ready = false self.reply_cancel = false if self.serverplayer:busy() then self.room.request_queue[self.serverplayer] = self.room.request_queue[self.serverplayer] or {} table.insert(self.room.request_queue[self.serverplayer], { self.id, command, jsonData, timeout }) return end self.room.request_self[self.serverplayer:getId()] = self.id if not table.contains(self._observers, self.serverplayer) then self.serverplayer:doNotify("StartChangeSelf", tostring(self.id)) end timeout = timeout or self.room.timeout self.serverplayer:setBusy(true) self.ai_data = { command = command, jsonData = jsonData, } self.serverplayer:doRequest(command, jsonData, timeout) end local function _waitForReply(player, timeout) local result local start = os.getms() local state = player.serverplayer:getState() player.request_timeout = timeout player.request_start = start if state ~= fk.Player_Online then if player.room.hasSurrendered then return "__cancel" end if state ~= fk.Player_Robot then player.room:checkNoHuman() player.room:delay(500) return "__cancel" end -- Let AI make reply. First handle request -- coroutine.yield("__handleRequest", 0) player.room:checkNoHuman() player.ai:readRequestData() local reply = player.ai:makeReply() if reply == "" then reply = "__cancel" end return reply end while true do player.serverplayer:setThinking(true) result = player.serverplayer:waitForReply(0) if result ~= "__notready" then player._timewaste_count = 0 player.serverplayer:setThinking(false) return result end local rest = timeout * 1000 - (os.getms() - start) / 1000 if timeout and rest <= 0 then if timeout >= 15 then player._timewaste_count = player._timewaste_count + 1 end player.serverplayer:setThinking(false) if player._timewaste_count >= 3 then player._timewaste_count = 0 player.serverplayer:emitKick() end return "" end if player.room.hasSurrendered then player.serverplayer:setThinking(false) return "" end coroutine.yield("__handleRequest", rest) 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 = _waitForReply(self, timeout) local sid = self.serverplayer:getId() local id = self.id if self.room.request_self[sid] ~= id then result = "" end self.request_data = "" self.client_reply = result if result == "__cancel" then result = "" self.reply_cancel = true self.serverplayer:setBusy(false) self.serverplayer:setThinking(false) end if result ~= "" then self.reply_ready = true self.serverplayer:setBusy(false) self.serverplayer:setThinking(false) end local queue = self.room.request_queue[self.serverplayer] if queue and #queue > 0 and not self.serverplayer:busy() then local i, c, j, t = table.unpack(table.remove(queue, 1)) self.room:getPlayerById(i):doRequest(c, j, t) end return result end ---@param player ServerPlayer ---@param observe bool function ServerPlayer:marshal(player, observe) local room = self.room if not room.game_started then -- If game does not starts, that mean we are entering room that -- all players are choosing their generals. -- Note that when we are in this function, the main thread must be -- calling delay() or waiting for reply. if self.role_shown then room:notifyProperty(player, self, "role") end return end room:notifyProperty(player, self, "maxHp") room:notifyProperty(player, self, "hp") room:notifyProperty(player, self, "shield") room:notifyProperty(player, self, "gender") room:notifyProperty(player, self, "kingdom") 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 for k, v in pairs(self.special_cards) do local info = {} for _, i in ipairs(v) do table.insert(info, { cardId = i, fromArea = Card.DrawPile }) end local move = { moveInfo = info, to = self.id, toArea = Card.PlayerSpecial, specialName = k, specialVisible = self == player, } table.insert(card_moves, move) end if #card_moves > 0 then room:notifyMoveCards({ player }, card_moves, observe and self.seat == 1) end for k, v in pairs(self.mark) do player:doNotify("SetPlayerMark", json.encode { self.id, k, v }) end 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 if v[1] > 0 then player:doNotify("AddCardUseHistory", json.encode { k, v[1] }) end end for k, v in pairs(self.skillUsedHistory) do if v[4] > 0 then player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[1], 1 }) player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[2], 2 }) player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[3], 3 }) player:doNotify("SetSkillUseHistory", json.encode { self.id, k, v[4], 4 }) end end if self.role_shown then room:notifyProperty(player, self, "role") end if #self.sealedSlots > 0 then room:notifyProperty(player, self, "sealedSlots") end end function ServerPlayer:reconnect() local room = self.room self.serverplayer:setState(fk.Player_Online) self:doNotify("Setup", json.encode { self.id, self._splayer:getScreenName(), self._splayer:getAvatar(), }) self:doNotify("EnterLobby", "") self:doNotify("EnterRoom", json.encode { #room.players, room.timeout, room.settings, }) self:doNotify("StartGame", "") room:notifyProperty(self, self, "role") -- send player data for _, p in ipairs(room:getOtherPlayers(self, false, true)) do self:doNotify("AddPlayer", json.encode { p.id, p._splayer:getScreenName(), p._splayer: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)) -- send printed_cards for i = -2, -math.huge, -1 do local c = Fk.printed_cards[i] if not c then break end self:doNotify("PrintCard", json.encode { c.name, c.suit, c.number }) end -- send card marks for id, marks in pairs(room.card_marks) do for k, v in pairs(marks) do self:doNotify("SetCardMark", json.encode { id, k, v }) end end for _, p in ipairs(room.players) do room:notifyProperty(self, p, "general") room:notifyProperty(self, p, "deputyGeneral") p:marshal(self) end self:doNotify("UpdateDrawPile", #room.draw_pile) self:doNotify("UpdateRoundNum", room:getTag("RoundCount") or 0) -- send fake skills for _, s in ipairs(self._manually_fake_skills) do self:doNotify("AddSkill", json.encode { self.id, s.name, true }) if table.contains(self.prelighted_skills, s) then self:doNotify("PrelightSkill", json.encode { s.name, true }) end end room:broadcastProperty(self, "state") end function ServerPlayer:isAlive() return self.dead == false end function ServerPlayer:turnOver() if self.room.logic:trigger(fk.BeforeTurnOver, self) then return end self.faceup = not self.faceup self.room:broadcastProperty(self, "faceup") self.room:sendLog { type = "#TurnOver", from = self.id, arg = self.faceup and "face_up" or "face_down", } self.room.logic:trigger(fk.TurnedOver, self) end function ServerPlayer:showCards(cards) cards = Card:getIdList(cards) for _, id in ipairs(cards) do Fk:filterCard(id, self) end local room = self.room room:sendLog { type = "#ShowCard", from = self.id, card = cards, } room:doBroadcastNotify("ShowCard", json.encode { from = self.id, cards = cards, }) room:sendFootnote(cards, { type = "##ShowCard", from = self.id, }) 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) local room = self.room local logic = room.logic self.phase = Player.PhaseNone local phase_change = { from = from_phase, to = to_phase } local skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) if skip and to_phase ~= Player.NotActive then self.phase = from_phase return true end self.phase = to_phase room:broadcastProperty(self, "phase") if #self.phases > 0 then table.remove(self.phases, 1) end GameEvent(GameEvent.Phase, self, self.phase):exec() return false end 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:broadcastProperty(self, "phase") room:sendLog { type = "#GainAnExtraPhase", from = self.id, arg = phase_name_table[phase], } GameEvent(GameEvent.Phase, self, self.phase):exec() self.phase = current room:broadcastProperty(self, "phase") end ---@param phase_table Phase[]|nil function ServerPlayer:play(phase_table) phase_table = phase_table or {} if #phase_table > 0 then if not table.contains(phase_table, Player.NotActive) then table.insert(phase_table, Player.NotActive) end else phase_table = { Player.RoundStart, Player.Start, Player.Judge, Player.Draw, Player.Play, Player.Discard, Player.Finish, Player.NotActive, } end self.phases = phase_table self.phase_state = {} local phases = self.phases local phase_state = self.phase_state local room = self.room for i = 1, #phases do phase_state[i] = { phase = phases[i], skipped = self.skipped_phases[phases[i]] or false } end for i = 1, #phases do if self.dead then self:changePhase(self.phase, Player.NotActive) break end self.phase_index = i local phase_change = { from = self.phase, to = phases[i] } local logic = self.room.logic self.phase = Player.PhaseNone local skip = phase_state[i].skipped if not skip then skip = logic:trigger(fk.EventPhaseChanging, self, phase_change) end phases[i] = phase_change.to phase_state[i].phase = phases[i] self.phase = phases[i] room:broadcastProperty(self, "phase") local cancel_skip = true if phases[i] ~= Player.NotActive and (skip) then cancel_skip = logic:trigger(fk.EventPhaseSkipping, self) end if (not skip) or (cancel_skip) then GameEvent(GameEvent.Phase, self, self.phase):exec() else room:sendLog { type = "#PhaseSkipped", from = self.id, arg = phase_name_table[self.phase], } end end end ---@param phase Phase function ServerPlayer:skip(phase) if not table.contains({ Player.Judge, Player.Draw, Player.Play, Player.Discard }, phase) then return end self.skipped_phases[phase] = true for _, t in ipairs(self.phase_state) do if t.phase == phase then t.skipped = true end end end function ServerPlayer:endPlayPhase() self._play_phase_end = true -- TODO: send log end 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 } local current = room.current room.current = self GameEvent(GameEvent.Turn, self):exec() room.current = current end function ServerPlayer:drawCards(num, skillName, fromPlace) return self.room:drawCards(self, num, skillName, fromPlace) end ---@param pile_name string ---@param card integer|Card ---@param visible boolean ---@param skillName string|nil function ServerPlayer:addToPile(pile_name, card, visible, skillName) local room = self.room room:moveCardTo(card, Card.PlayerSpecial, self, fk.ReasonJustMove, skillName, pile_name, visible) end function ServerPlayer:bury() self:setCardUseHistory("") self:setSkillUseHistory("") self:throwAllCards() self:throwAllMarks() self:clearPiles() self:reset() end function ServerPlayer:throwAllCards(flag) local cardIds = {} flag = flag or "hej" if string.find(flag, "h") then table.insertTable(cardIds, self.player_cards[Player.Hand]) end if string.find(flag, "e") then table.insertTable(cardIds, self.player_cards[Player.Equip]) end if string.find(flag, "j") then table.insertTable(cardIds, self.player_cards[Player.Judge]) end self.room:throwCard(cardIds, "", self) end function ServerPlayer:throwAllMarks() for name, _ in pairs(self.mark) do self.room:setPlayerMark(self, name, 0) end end function ServerPlayer:clearPiles() local cardIds = {} for _, ids in pairs(self.special_cards) do table.insertTable(cardIds, ids) end self.room:moveCardTo(cardIds, Card.DiscardPile, nil, fk.ReasonPutIntoDiscardPile, "", nil, true) end function ServerPlayer:addVirtualEquip(card) Player.addVirtualEquip(self, card) self.room:doBroadcastNotify("AddVirtualEquip", json.encode { player = self.id, name = card.name, subcards = card.subcards, }) end function ServerPlayer:removeVirtualEquip(cid) local ret = Player.removeVirtualEquip(self, cid) self.room:doBroadcastNotify("RemoveVirtualEquip", json.encode { player = self.id, id = cid, }) return ret end function ServerPlayer:addCardUseHistory(cardName, num) Player.addCardUseHistory(self, cardName, num) self:doNotify("AddCardUseHistory", json.encode { cardName, num }) end function ServerPlayer:setCardUseHistory(cardName, num, scope) Player.setCardUseHistory(self, cardName, num, scope) self:doNotify("SetCardUseHistory", json.encode { cardName, num, scope }) end function ServerPlayer:addSkillUseHistory(cardName, num) Player.addSkillUseHistory(self, cardName, num) self.room:doBroadcastNotify("AddSkillUseHistory", json.encode { self.id, cardName, num }) end function ServerPlayer:setSkillUseHistory(cardName, num, scope) Player.setSkillUseHistory(self, cardName, num, scope) self.room:doBroadcastNotify("SetSkillUseHistory", json.encode { self.id, cardName, num, scope }) end ---@param chained boolean function ServerPlayer:setChainState(chained) local room = self.room if room.logic:trigger(fk.BeforeChainStateChange, self) then return end self.chained = chained room:broadcastProperty(self, "chained") room:sendLog { type = "#ChainStateChange", from = self.id, arg = self.chained and "chained" or "un-chained" } room:delay(150) room:broadcastPlaySound("./audio/system/chain") room.logic:trigger(fk.ChainStateChanged, self) end function ServerPlayer:reset() self.room:sendLog { type = "#ChainStateChange", from = self.id, arg = "reset-general" } if self.chained then self:setChainState(false) end if not self.faceup then self:turnOver() end end --- 进行拼点。 ---@param from ServerPlayer ---@param tos ServerPlayer[] ---@param skillName string ---@param initialCard Card|nil ---@return PindianStruct function ServerPlayer:pindian(tos, skillName, initialCard) local pindianData = { from = self, tos = tos, reason = skillName, fromCard = initialCard, results = {} } self.room:pindian(pindianData) return pindianData end --- 播放技能的语音。 ---@param skill_name string @ 技能名 ---@param index integer | nil @ 语音编号,默认为-1(也就是随机播放) function ServerPlayer:broadcastSkillInvoke(skill_name, index) index = index or -1 self.room:sendLogEvent("PlaySkillSound", { name = skill_name, i = index, general = self.general, deputy = self.deputyGeneral, }) end -- Hegemony func ---@param skill Skill function ServerPlayer:addFakeSkill(skill) assert(type(skill) == "string" or skill:isInstanceOf(Skill)) if type(skill) == "string" then skill = Fk.skills[skill] end if table.contains(self._fake_skills, skill) then return end table.insertIfNeed(self._manually_fake_skills, skill) table.insert(self._fake_skills, skill) for _, s in ipairs(skill.related_skills) do -- if s.main_skill == skill then -- TODO: need more detailed table.insert(self._fake_skills, s) -- end end -- TODO self:doNotify("AddSkill", json.encode { self.id, skill.name, true }) end ---@param skill Skill function ServerPlayer:loseFakeSkill(skill) assert(type(skill) == "string" or skill:isInstanceOf(Skill)) if type(skill) == "string" then skill = Fk.skills[skill] end if not table.contains(self._fake_skills, skill) then return end table.removeOne(self._manually_fake_skills, skill) table.removeOne(self._fake_skills, skill) for _, s in ipairs(skill.related_skills) do table.removeOne(self._fake_skills, s) end -- TODO self:doNotify("LoseSkill", json.encode { self.id, skill.name, true }) end function ServerPlayer:isFakeSkill(skill) if type(skill) == "string" then skill = Fk.skills[skill] end assert(skill:isInstanceOf(Skill)) return table.contains(self._fake_skills, skill) end ---@param skill string | Skill ---@param isPrelight bool function ServerPlayer:prelightSkill(skill, isPrelight) if type(skill) == "string" then skill = Fk.skills[skill] end assert(skill:isInstanceOf(Skill)) if not self._prelighted_skills[skill] and not self:hasSkill(skill) then self._prelighted_skills[skill] = true -- to attach skill to room self:addSkill(skill) self:loseSkill(skill) end if isPrelight then -- self:addSkill(skill) table.insert(self.prelighted_skills, skill) for _, s in ipairs(skill.related_skills) do table.insert(self.prelighted_skills, s) end else -- self:loseSkill(skill) table.removeOne(self.prelighted_skills, skill) for _, s in ipairs(skill.related_skills) do table.removeOne(self.prelighted_skills, s) end end self:doNotify("PrelightSkill", json.encode { skill.name, isPrelight }) end ---@param isDeputy bool ---@param no_trigger bool function ServerPlayer:revealGeneral(isDeputy, no_trigger) local room = self.room local generalName if isDeputy then if self.deputyGeneral ~= "anjiang" then return end generalName = self:getMark("__heg_deputy") else if self.general ~= "anjiang" then return end generalName = self:getMark("__heg_general") end local general = Fk.generals[generalName] or Fk.generals["blank_shibing"] for _, s in ipairs(general:getSkillNameList()) do local skill = Fk.skills[s] self:loseFakeSkill(skill) end local ret = true if not ((isDeputy and self.general ~= "anjiang") or (not isDeputy and self.deputyGeneral ~= "anjiang")) then local other = Fk.generals[self:getMark(isDeputy and "__heg_general" or "__heg_deputy")] or Fk.generals["blank_shibing"] for _, sname in ipairs(other:getSkillNameList()) do local s = Fk.skills[sname] if s.frequency == Skill.Compulsory and s.relate_to_place ~= (isDeputy and "m" or "d") then ret = false break end end end if ret then self:loseFakeSkill("reveal_skill") end local oldKingdom = self.kingdom room:changeHero(self, generalName, false, isDeputy, false, false) if oldKingdom ~= "wild" then local kingdom = general.kingdom self.kingdom = kingdom if oldKingdom == "unknown" and #table.filter(room:getOtherPlayers(self, false, true), function(p) return p.kingdom == kingdom end) >= #room.players // 2 then self.kingdom = "wild" end room:broadcastProperty(self, "kingdom") else room:setPlayerProperty(self, "kingdom", "wild") end if self.gender == General.Agender then self.gender = general.gender end room:sendLog { type = "#RevealGeneral", from = self.id, arg = isDeputy and "deputyGeneral" or "mainGeneral", arg2 = generalName, } if not no_trigger then room.logic:trigger(fk.GeneralRevealed, self, generalName) end end function ServerPlayer:revealBySkillName(skill_name) local main = self.general == "anjiang" local deputy = self.deputyGeneral == "anjiang" if main then if table.contains(Fk.generals[self:getMark("__heg_general")] :getSkillNameList(), skill_name) then self:revealGeneral(false) return end end if deputy then if table.contains(Fk.generals[self:getMark("__heg_deputy")] :getSkillNameList(), skill_name) then self:revealGeneral(true) return end end end function ServerPlayer:hideGeneral(isDeputy) local room = self.room local generalName = isDeputy and self.deputyGeneral or self.general local mark = isDeputy and "__heg_deputy" or "__heg_general" self:setMark(mark, generalName) self:doNotify("SetPlayerMark", json.encode { self.id, mark, generalName }) if isDeputy then room:setDeputyGeneral(self, "anjiang") room:broadcastProperty(self, "deputyGeneral") else room:setPlayerGeneral(self, "anjiang", false) room:broadcastProperty(self, "general") end local general = Fk.generals[generalName] local skills = general.skills local place = isDeputy and "m" or "d" for _, s in ipairs(skills) do room:handleAddLoseSkills(self, "-" .. s.name, nil, false, true) if s.relate_to_place ~= place then if s.frequency == Skill.Compulsory then self:addFakeSkill("reveal_skill") end self:addFakeSkill(s) end end for _, sname in ipairs(general.other_skills) do room:handleAddLoseSkills(self, "-" .. sname, nil, false, true) local s = Fk.skills[sname] if s.relate_to_place ~= place then if s.frequency == Skill.Compulsory then self:addFakeSkill("reveal_skill") end self:addFakeSkill(s) end end room.logic:trigger(fk.GeneralHidden, room, generalName) end -- 神貂蝉 ---@param p ServerPlayer function ServerPlayer:control(p) if self == p then self.room:setPlayerMark(p, "@ControledBy", 0) else self.room:setPlayerMark(p, "@ControledBy", "seat#" .. self.seat) end p.serverplayer = self._splayer end -- 22 function ServerPlayer:addBuddy(other) if type(other) == "number" then other = self.room:getPlayerById(other) end Player.addBuddy(self, other) self:doNotify("AddBuddy", json.encode { other.id, other.player_cards[Player.Hand] }) end function ServerPlayer:removeBuddy(other) if type(other) == "number" then other = self.room:getPlayerById(other) end Player.removeBuddy(self, other) self:doNotify("RmBuddy", tostring(other.id)) end function ServerPlayer:getAI() return self.ai end return ServerPlayer