Changelog: v0.4.22
Some checks failed
Check Whitespace and New Line / check (push) Has been cancelled
Deploy Doxygen to Pages / build (push) Has been cancelled
Deploy Doxygen to Pages / deploy (push) Has been cancelled

This commit is contained in:
notify 2024-10-26 00:08:20 +08:00
parent b92e61db3f
commit fd35f4f034
22 changed files with 242 additions and 162 deletions

View File

@ -1,6 +1,6 @@
# ChangeLog
## v0.4.21
## v0.4.21 & v0.4.22
- 修复了确认键亮起时取消键不可用的bug
- lua端的ob属性根本没同步同步一下
@ -12,6 +12,8 @@
- 删除了已经不用的autoPending和respond_play
- 修复异常烧条(或许吧)
- 修复负数烧条时间,若为负数则无事发生
- 修改了定期刷新状态技UI的时机
- 录像可以看别人牌了
___

View File

@ -6,7 +6,7 @@
cmake_minimum_required(VERSION 3.22)
project(FreeKill VERSION 0.4.21)
project(FreeKill VERSION 0.4.22)
add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\")
find_package(Qt6 REQUIRED COMPONENTS

View File

@ -1095,6 +1095,19 @@ Item {
onActivated: menuContainer.open();
}
Timer {
id: statusSkillTimer
interval: 200
running: isStarted
repeat: true
onTriggered: {
lcall("RefreshStatusSkills");
//
for (let i = 0; i < photos.count; i++)
photos.itemAt(i).handcardsChanged();
}
}
function addToChat(pid, raw, msg) {
if (raw.type === 1) return;
const photo = Logic.getPhoto(pid);
@ -1313,6 +1326,39 @@ Item {
}
function applyChange(uiUpdate) {
const sskilldata = uiUpdate["SpecialSkills"]?.[0]
if (sskilldata) {
specialCardSkills.model = sskilldata?.skills ?? [];
}
dashboard.applyChange(uiUpdate);
const pdatas = uiUpdate["Photo"];
pdatas?.forEach(pdata => {
const photo = Logic.getPhoto(pdata.id);
photo.state = pdata.state;
photo.selectable = pdata.enabled;
photo.selected = pdata.selected;
})
const buttons = uiUpdate["Button"];
if (buttons) {
okCancel.visible = true;
}
buttons?.forEach(bdata => {
switch (bdata.id) {
case "OK":
okButton.enabled = bdata.enabled;
break;
case "Cancel":
cancelButton.enabled = bdata.enabled;
break;
case "End":
endPhaseButton.enabled = bdata.enabled;
endPhaseButton.visible = bdata.enabled;
break;
}
})
// Interaction
uiUpdate["_delete"]?.forEach(data => {
if (data.type == "Interaction") {
skillInteraction.sourceComponent = undefined;
@ -1356,38 +1402,6 @@ Item {
}
}
});
const sskilldata = uiUpdate["SpecialSkills"]?.[0]
if (sskilldata) {
specialCardSkills.model = sskilldata?.skills ?? [];
}
dashboard.applyChange(uiUpdate);
const pdatas = uiUpdate["Photo"];
pdatas?.forEach(pdata => {
const photo = Logic.getPhoto(pdata.id);
photo.state = pdata.state;
photo.selectable = pdata.enabled;
photo.selected = pdata.selected;
})
const buttons = uiUpdate["Button"];
if (buttons) {
okCancel.visible = true;
}
buttons?.forEach(bdata => {
switch (bdata.id) {
case "OK":
okButton.enabled = bdata.enabled;
break;
case "Cancel":
cancelButton.enabled = bdata.enabled;
break;
case "End":
endPhaseButton.enabled = bdata.enabled;
endPhaseButton.visible = bdata.enabled;
break;
}
})
}
Component.onCompleted: {

View File

@ -73,17 +73,17 @@ Item {
gradient: Gradient {
GradientStop {
position: 0
color: "#FEF7C2"
color: root.locked ? "#CCC8C4" : "#FEF7C2"
}
GradientStop {
position: 0.5
color: "#D2AD4A"
position: 0.8
color: root.locked ? "#A09691" : "#D2AD4A"
}
GradientStop {
position: 1
color: "#BE9878"
color: root.locked ? "#787173" : "#BE9878"
}
}
}
@ -142,17 +142,17 @@ Item {
gradient: Gradient {
GradientStop {
position: 0
color: "#FEF7C2"
color: root.locked ? "#CCC8C4" : "#FEF7C2"
}
GradientStop {
position: 0.8
color: "#D2AD4A"
color: root.locked ? "#A09691" : "#D2AD4A"
}
GradientStop {
position: 1
color: "#BE9878"
color: root.locked ? "#787173" : "#BE9878"
}
}
}

View File

@ -3,8 +3,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.notify.FreeKill"
android:installLocation="preferExternal"
android:versionCode="421"
android:versionName="0.4.21">
android:versionCode="422"
android:versionName="0.4.22">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

@ -6,7 +6,9 @@
---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组
---@field public observers ClientPlayer[] @ 观察者的数组
---@field public current ClientPlayer @ 当前回合玩家
---@field public observing boolean
---@field public observing boolean 客户端是否在旁观
---@field public replaying boolean 客户端是否在重放
---@field public replaying_show boolean 重放时是否要看到全部牌
---@field public record any
---@field public last_update_ui integer @ 上次刷新状态技UI的时间
Client = AbstractRoom:subclass('Client')
@ -62,29 +64,13 @@ function Client:initialize()
end
if (type(cb) == "function") then
if command:startsWith("AskFor") or command == "PlayCard" then
self:notifyUI("CancelRequest") -- 确保变成notactive 防止卡双active 权宜之计
end
cb(data)
else
self:notifyUI(command, data)
end
if self.recording and command == "GameLog" then
--and os.getms() - self.last_update_ui > 60000 then
-- self.last_update_ui = os.getms()
-- TODO: create a function
-- 刷所有人手牌上限
for _, p in ipairs(self.alive_players) do
self:notifyUI("MaxCard", {
pcardMax = p:getMaxCards(),
id = p.id,
})
end
-- 刷自己的手牌
for _, cid in ipairs(Self:getCardIds("h")) do
self:notifyUI("UpdateCard", cid)
end
-- 刷技能状态
self:notifyUI("UpdateSkill", nil)
end
end
self.disabled_packs = {}
@ -245,8 +231,12 @@ fk.client_callback["EnterRoom"] = function(_data)
Self = ClientPlayer:new(fk.Self)
-- FIXME: 需要改Qml
local ob = ClientInstance.observing
local replaying = ClientInstance.replaying
local showcards = ClientInstance.replaying_show
ClientInstance = Client:new() -- clear old client data
ClientInstance.observing = ob
ClientInstance.replaying = replaying
ClientInstance.replaying_show = showcards
ClientInstance.players = {Self}
ClientInstance.alive_players = {Self}
ClientInstance.discard_pile = {}
@ -380,11 +370,19 @@ fk.client_callback["AskForCardChosen"] = function(data)
if not string.find(flag, "j") then
judge = {}
end
local visible_data = {}
for _, cid in ipairs(hand) do
if not Self:cardVisible(cid) then
visible_data[tostring(cid)] = false
end
end
if next(visible_data) == nil then visible_data = nil end
ui_data = {
_id = id,
_reason = reason,
card_data = {},
_prompt = prompt,
visible_data = visible_data,
}
if #hand ~= 0 then table.insert(ui_data.card_data, { "$Hand", hand }) end
if #equip ~= 0 then table.insert(ui_data.card_data, { "$Equip", equip }) end
@ -417,6 +415,13 @@ fk.client_callback["AskForCardsChosen"] = function(data)
if not string.find(flag, "j") then
judge = {}
end
local visible_data = {}
for _, cid in ipairs(hand) do
if not Self:cardVisible(cid) then
visible_data[tostring(cid)] = false
end
end
if next(visible_data) == nil then visible_data = nil end
ui_data = {
_id = id,
_min = min,
@ -424,6 +429,7 @@ fk.client_callback["AskForCardsChosen"] = function(data)
_reason = reason,
card_data = {},
_prompt = prompt,
visible_data = visible_data,
}
if #hand ~= 0 then table.insert(ui_data.card_data, { "$Hand", hand }) end
if #equip ~= 0 then table.insert(ui_data.card_data, { "$Equip", equip }) end
@ -706,13 +712,19 @@ end
fk.client_callback["ShowCard"] = function(data)
local from = data.from
local cards = data.cards
ClientInstance:notifyUI("MoveCards", {
local merged = {
{
ids = cards,
fromArea = Card.DrawPile,
toArea = Card.Processing,
}
})
}
local vdata = {}
for _, id in ipairs(cards) do
vdata[tostring(id)] = true
end
vdata.merged = merged
ClientInstance:notifyUI("MoveCards", vdata)
end
-- 说是限定技,其实也适用于觉醒技、转换技、使命技

View File

@ -97,7 +97,7 @@ function GetCardData(id, virtualCardForm)
mark = mark,
type = card.type,
subtype = cardSubtypeStrings[card.sub_type],
known = Self:cardVisible(id)
-- known = Self:cardVisible(id)
}
if card.skillName ~= "" then
local orig = Fk:getCardById(id, true)
@ -799,6 +799,19 @@ function SetObserving(o)
ClientInstance.observing = o
end
function SetReplaying(o)
ClientInstance.replaying = o
end
function SetReplayingShowCards(o)
ClientInstance.replaying_show = o
if o then
for _, p in ipairs(ClientInstance.players) do
ClientInstance:notifyUI("PropertyUpdate", { p.id, "role_shown", true })
end
end
end
function CheckSurrenderAvailable(playedTime)
local curMode = ClientInstance.room_settings.gameMode
return Fk.game_modes[curMode]:surrenderFunc(playedTime)
@ -982,4 +995,22 @@ function HasVisibleCard(me, other, special_name)
return false
end
function RefreshStatusSkills()
local self = ClientInstance
if not self.recording then return end -- 在回放录像就别刷了
-- 刷所有人手牌上限
for _, p in ipairs(self.alive_players) do
self:notifyUI("MaxCard", {
pcardMax = p:getMaxCards(),
id = p.id,
})
end
-- 刷自己的手牌
for _, cid in ipairs(Self:getCardIds("h")) do
self:notifyUI("UpdateCard", cid)
end
-- 刷技能状态
self:notifyUI("UpdateSkill", nil)
end
dofile "lua/client/i18n/init.lua"

View File

@ -13,7 +13,7 @@ end
local function fillMoveData(card_moves, visible_data, self, area, specialName)
local cards = self.player_cards
local ids = cards[area]
if specialName then ids = ids[specialName] end
if specialName then ids = self.special_cards[specialName] end
if #ids ~= 0 then
for _, id in ipairs(ids) do
visible_data[tostring(id)] = Self:cardVisible(id)

View File

@ -353,6 +353,7 @@ FreeKill使用的是libgit2的C API与此同时使用Git完成拓展包的下
["$AddObserver"] = '玩家 <b>%s</b> 开始旁观',
["$RemoveObserver"] = '旁观者 <b>%s</b> 离开了房间',
["Show All Cards"] = "显示未知信息",
["Speed Resume"] = "匀速",
["Speed Up"] = "加速",
["Speed Down"] = "减速",

View File

@ -1199,27 +1199,31 @@ end
---@param move? CardsMoveStruct
---@return boolean
function Player:cardVisible(cardId, move)
local room = Fk:currentRoom()
if room.replaying and room.replaying_show then return true end
local falsy = false -- 当难以决定时是否要选择暗置?
if move then
if table.find(move.moveInfo, function(info) return info.cardId == cardId end) then
if move.moveVisible then return true end
if move.moveVisible == false then falsy = true end
-- specialVisible还要控制这个pile对他人是否应该可见但是不在这里生效
if move.specialVisible then return true end
if (type(move.visiblePlayers) == "number" and move.visiblePlayers == self.id) or
(type(move.visiblePlayers) == "table" and table.find(move.visiblePlayers, self.id)) then
(type(move.visiblePlayers) == "table" and table.contains(move.visiblePlayers, self.id)) then
return true
end
end
end
local room = Fk:currentRoom()
local area = room:getCardArea(cardId)
local card = Fk:getCardById(cardId)
local public_areas = {Card.DiscardPile, Card.Processing, Card.Void, Card.PlayerEquip, Card.PlayerJudge}
local player_areas = {Card.PlayerHand, Card.PlayerSpecial}
if room.observing == true then return table.contains(public_areas, area) end
if room.observing and not room.replaying then return table.contains(public_areas, area) end
local status_skills = Fk:currentRoom().status_skills[VisibilitySkill] or Util.DummyTable
for _, skill in ipairs(status_skills) do
@ -1230,9 +1234,9 @@ function Player:cardVisible(cardId, move)
end
if area == Card.DrawPile then return false
elseif table.contains(public_areas, area) then return true
elseif table.contains(public_areas, area) then return not falsy
elseif move and area == Card.PlayerSpecial and not move.specialName:startsWith("$") then
return true
return not falsy
elseif table.contains(player_areas, area) then
local to = room:getCardOwner(cardId)
return to == self or self:isBuddy(to)
@ -1254,7 +1258,8 @@ function Player:roleVisible(target)
end
end
if not room.observing and target == self then return true end
if (room.replaying or not room.observing) and target == self then return true end
if room.replaying and room.replaying_show then return true end
return target.role_shown
end

View File

@ -76,8 +76,11 @@ end
function ReqActiveSkill:setupInteraction()
local skill = Fk.skills[self.skill_name]
if skill and skill.interaction then
skill.interaction.data = nil -- FIXME
local interaction = skill:interaction()
if not interaction then
return
end
skill.interaction.data = interaction.default_choice or nil -- FIXME
-- 假设只有1个interaction (其实目前就是这样)
local i = Interaction:new(self.scene, "1", interaction)
i.skill_name = self.skill_name

View File

@ -37,7 +37,17 @@ function ReqPlayCard:cardValidity(cid)
return true
end
end
return false
ret = false
end
end
if not ret then
local skills = card.special_skills
if not skills then return false end
for _, skill in ipairs(skills) do
if Fk.skills[skill]:canUse(player) then
return true
end
end
end
return ret
@ -47,7 +57,27 @@ function ReqPlayCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
if skill:isInstanceOf(ViewAsSkill) then
return skill:enabledAtPlay(player, true)
local ret = skill:enabledAtPlay(player, true)
if ret then -- 没有pattern或者至少有一个满足
local exp = Exppattern:Parse(skill.pattern)
local cnames = {}
for _, m in ipairs(exp.matchers) do
if m.name then
table.insertTable(cnames, m.name)
end
if m.trueName then
table.insertTable(cnames, m.trueName)
end
end
local extra_data = self.extra_data
for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n)
c.skillName = name
ret = c.skill:canUse(Self, c, extra_data)
if ret then break end
end
end
return ret
elseif skill:isInstanceOf(ActiveSkill) then
return skill:canUse(player, nil)
end
@ -67,6 +97,11 @@ function ReqPlayCard:feasible()
return ret
end
function ReqPlayCard:isCancelable()
if self.skill_name and self.selected_card then return false end
return ReqUseCard.isCancelable(self)
end
function ReqPlayCard:selectSpecialUse(data)
-- 相当于使用一个以已选牌为pendings的主动技
if not data or data == "_normal_use" then
@ -115,14 +150,17 @@ function ReqPlayCard:selectCard(cid, data)
local sp_skills = {}
if self.selected_card.special_skills then
sp_skills = table.simpleClone(self.selected_card.special_skills)
if self:cardValidity(self.selected_card) then
if self.player:canUse(self.selected_card) then
table.insert(sp_skills, 1, "_normal_use")
else
self:selectSpecialUse(sp_skills[1])
end
end
self.scene:update("SpecialSkills", "1", { skills = sp_skills })
else
self.selected_card = nil
self:setPrompt(self.original_prompt)
self.skill_name = nil
self.scene:update("SpecialSkills", "1", { skills = {} })
end
end

View File

@ -42,6 +42,7 @@ function ReqResponseCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, true)
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
end
function ReqResponseCard:cardValidity(cid)
@ -74,22 +75,7 @@ end
function ReqResponseCard:updateSkillButtons()
local scene = self.scene
for name, item in pairs(scene:getAllItems("SkillButton")) do
local skill = Fk.skills[name]
local ret = self:skillButtonValidity(name)
if ret and skill:isInstanceOf(ViewAsSkill) then
local exp = Exppattern:Parse(skill.pattern)
local cnames = {}
for _, m in ipairs(exp.matchers) do
if m.name then table.insertTable(cnames, m.name) end
if m.trueName then table.insertTable(cnames, m.trueName) end
end
for _, n in ipairs(cnames) do
local c = Fk:cloneCard(n)
c.skillName = name
ret = self:cardValidity(c)
if ret then break end
end
end
local ret = self:skillButtonValidity(name) -- 分散判断
scene:update("SkillButton", name, { enabled = not not ret })
end
end

View File

@ -8,6 +8,7 @@ function ReqUseCard:skillButtonValidity(name)
local player = self.player
local skill = Fk.skills[name]
return skill:isInstanceOf(ViewAsSkill) and skill:enabledAtResponse(player, false)
and skill.pattern and Exppattern:Parse(self.pattern):matchExp(skill.pattern)
end
function ReqUseCard:cardValidity(cid)
@ -25,6 +26,30 @@ function ReqUseCard:targetValidity(pid)
local card = self.selected_card
local ret = card and not player:isProhibited(p, card) and
card.skill:targetFilter(pid, self.selected_targets, { card.id }, card, self.extra_data)
if ret and self.extra_data then
local data = self.extra_data
if data.exclusive_targets then
-- target不在exclusive中则不可选择
ret = table.contains(data.exclusive_targets, pid)
end
if ret and data.must_targets then
-- 若must中有还没被选的且这个target不在must中则不可选择
if table.find(data.must_targets, function(id)
return not table.contains(self.selected_targets, id)
end) and not table.contains(data.must_targets, pid) then
ret = false
end
end
if ret and data.include_targets then
-- 若include中全都没选且target不在include中则不可选择
if table.every(data.include_targets, function(id)
return not table.contains(self.selected_targets, id)
end) and not table.contains(data.must_targets, pid) then
ret = false
end
end
end
return ret
end

View File

@ -103,7 +103,8 @@ function Death:main()
room:sendLogEvent("Death", {to = victim.id})
if victim.rest == 0 then
room:broadcastProperty(victim, "role")
room:setPlayerProperty(victim, "role_shown", true)
-- room:broadcastProperty(victim, "role")
end
room:broadcastProperty(victim, "dead")

View File

@ -329,7 +329,6 @@ function Phase:main()
[Player.Play] = function()
player._play_phase_end = false
room:doBroadcastNotify("UpdateSkill", "", {player})
while not player.dead do
if player._phase_end then break end
logic:trigger(fk.StartPlayCard, player, nil, true)

View File

@ -112,6 +112,7 @@ function MoveCards:main()
---@param info MoveInfo
for _, info in ipairs(data.moveInfo) do
local realFromArea = room:getCardArea(info.cardId)
room:applyMoveInfo(data, info)
if data.toArea == Card.DrawPile or realFromArea == Card.DrawPile then
room:doBroadcastNotify("UpdateDrawPile", #room.draw_pile)
@ -140,8 +141,6 @@ function MoveCards:main()
if name:find("-inarea", 1, true) and
type(value) == "table" and table.contains(value, realFromArea) and not table.contains(value, data.toArea)
then
p(realFromArea)
p(value)
room:setCardMark(currentCard, name, 0)
end
end

View File

@ -59,9 +59,9 @@ function GameLogic:run()
-- default logic
local room = self.room
table.shuffle(self.room.players)
self:assignRoles()
self.room.game_started = true
room:doBroadcastNotify("StartGame", "")
self:assignRoles()
room:adjustSeats()
--[[ 因为未完工在release版暂时不启用。
for _, p in ipairs(room.players) do

View File

@ -178,6 +178,11 @@ function Request:ask()
local currentTime = os.time()
local resume_reason = "unknown"
-- 设置所有人为未思考
for _, p in ipairs(players) do
p.serverplayer:setThinking(false)
end
-- 1. 向所有人发送询问请求
for _, p in ipairs(players) do
self:_sendPacket(p)

View File

@ -150,64 +150,6 @@ function Room:makeGeneralPile()
return true
end
-- 因为现在已经不是轮询了,加上有点难分析
-- 选择开摆
function Room:isReady()
-- 因为delay函数而延时判断延时是否已经结束。
-- 注意整个delay函数的实现都搬到这来了delay本身只负责挂起协程了。
--[[
if self.in_delay then
local rest = self.delay_duration - (os.getms() - self.delay_start) / 1000
if rest <= 50 then
self.in_delay = false
return true
end
return false, rest
end
--]]
return true
end
--[[
-- 供调度器使用的函数,用来指示房间是否就绪。
-- 如果没有就绪的话,可能会返回第二个值来告诉调度器自己还有多久就绪。
function Room:isReady()
-- 没有活人了?那就告诉调度器我就绪了,恢复时候就会自己杀掉
if self:checkNoHuman(true) then
return true
end
-- 剩下的就是因为等待应答而未就绪了
-- 检查所有正在等回答的玩家,如果已经过了烧条时间
-- 那么就不认为他还需要时间就绪了
-- 然后在调度器第二轮刷新的时候就应该能返回自己已就绪
local ret = true
local rest
for _, p in ipairs(self.players) do
-- 这里判断的话需要用_splayer了不然一控多的情况下会导致重复判断
if p._splayer:thinking() then
-- 烧条烧光了的话就把thinking设为false
rest = p.request_timeout * 1000 - (os.getms() -
p.request_start) / 1000
if rest <= 0 or p.serverplayer:getState() ~= fk.Player_Online then
p._splayer:setThinking(false)
else
ret = false
end
end
if self.race_request_list and table.contains(self.race_request_list, p) then
local result = p.serverplayer:waitForReply(0)
if result ~= "__notready" and result ~= "__cancel" and result ~= "" then
return true
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
@ -1614,17 +1556,21 @@ function Room:askForCardsChosen(chooser, target, min, max, flag, reason, prompt)
skillName = reason,
prompt = prompt,
}
local visible_data = {}
local cards_data = {}
if type(flag) == "string" then
local handcards = target:getCardIds(Player.Hand)
local equips = target:getCardIds(Player.Equip)
local judges = target:getCardIds(Player.Judge)
if string.find(flag, "h") and #handcards > 0 then
-- TODO: 关于明置的牌
if target ~= chooser then
handcards = table.map(handcards, function() return -1 end)
end
table.insert(cards_data, {"$Hand", handcards})
for _, id in ipairs(handcards) do
if not chooser:cardVisible(id) then
visible_data[tostring(id)] = false
end
end
if next(visible_data) == nil then visible_data = nil end
data.visible_data = visible_data
end
if string.find(flag, "e") and #equips > 0 then
table.insert(cards_data, {"$Equip", equips})
@ -2710,7 +2656,8 @@ function Room:gameOver(winner)
self.game_finished = true
for _, p in ipairs(self.players) do
self:broadcastProperty(p, "role")
-- self:broadcastProperty(p, "role")
self:setPlayerProperty(p, "role_shown", true)
end
self:doBroadcastNotify("GameOver", winner)
fk.qInfo(string.format("[GameOver] %d, %s, %s, in %ds", self.id, self.settings.gameMode, winner, os.time() - self.start_time))

View File

@ -49,7 +49,6 @@ end
function ResumeRoom(roomId, reason)
local room = requestRoom:getRoom(roomId)
if not room then return false end
if not room:isReady() then return false end
RoomInstance = (room ~= requestRoom and room or nil)
local over = room:resume(reason)
RoomInstance = nil

View File

@ -85,6 +85,19 @@ function ServerPlayer:__index(k)
end
end
-- FIXME: 理由同上垃圾request体系赶紧狠狠重构
function ServerPlayer:__newindex(k, v)
if k == "client_reply" then
local request = self.room.last_request
if not request then return end
request.result[self.id] = v
return
elseif k == "reply_ready" then
return
end
rawset(self, k, v)
end
--- 发送一句聊天
---@param msg string
function ServerPlayer:chat(msg)