From 0ccc2c3e75761714b983ec6c92d8623b94210745 Mon Sep 17 00:00:00 2001 From: Tim Goll Date: Mon, 9 Oct 2023 14:44:07 +0200 Subject: [PATCH] refactor phase 2; dynamic button handling; removed console commands --- .../terrortown/gamemode/client/cl_main.lua | 4 +- .../terrortown/gamemode/client/cl_search.lua | 221 ++++---------- .../gamemode/client/vgui/cl_sb_info.lua | 6 +- .../gamemode/client/vgui/cl_sb_row.lua | 4 +- .../terrortown/gamemode/server/sv_corpse.lua | 287 ++---------------- .../terrortown/gamemode/server/sv_main.lua | 1 - lua/ttt2/libraries/bodysearch.lua | 263 +++++++++++++++- lua/ttt2/libraries/targetid.lua | 6 +- 8 files changed, 336 insertions(+), 456 deletions(-) diff --git a/gamemodes/terrortown/gamemode/client/cl_main.lua b/gamemodes/terrortown/gamemode/client/cl_main.lua index 53468f440a..1e71c0c222 100644 --- a/gamemodes/terrortown/gamemode/client/cl_main.lua +++ b/gamemodes/terrortown/gamemode/client/cl_main.lua @@ -411,7 +411,7 @@ local function RoundStateChange(o, n) -- people may have died and been searched during prep for i = 1, #plys do - plys[i].search_result = nil + bodysearch.ResetSearchResult(plys[i]) end -- clear blood decals produced during prep @@ -570,7 +570,7 @@ function GM:ClearClientState() pl:SetRole(ROLE_NONE) - pl.search_result = nil + bodysearch.ResetSearchResult(pl) end VOICE.CycleMuteState(MUTE_NONE) diff --git a/gamemodes/terrortown/gamemode/client/cl_search.lua b/gamemodes/terrortown/gamemode/client/cl_search.lua index 7fa71c4c28..2ccb334bc6 100644 --- a/gamemodes/terrortown/gamemode/client/cl_search.lua +++ b/gamemodes/terrortown/gamemode/client/cl_search.lua @@ -8,134 +8,6 @@ local util = util local IsValid = IsValid local hook = hook -local function StoreSearchResult(search) - if not search.owner then return end - - -- if existing result was not ours, it was detective's, and should not - -- be overwritten - local ply = search.owner - - if ply.search_result and not ply.search_result.show then return end - - ply.search_result = search - - -- this is useful for targetid - local rag = Entity(search.eidx) - if not IsValid(rag) then return end - - rag.search_result = search -end - -local function bitsRequired(num) - local bits, max = 0, 1 - - while max <= num do - bits = bits + 1 - max = max + max - end - - return bits -end - -net.Receive("TTT_RagdollSearch", function() - local search = {} - - -- Basic info - search.eidx = net.ReadUInt(16) - - local owner = Entity(net.ReadUInt(8)) - - search.owner = owner - - if not IsValid(search.owner) or not search.owner:IsPlayer() or search.owner:IsTerror() then - search.owner = nil - end - - search.nick = net.ReadString() - search.role_color = net.ReadColor() - - -- Equipment - local eq = {} - - local eqAmount = net.ReadUInt(16) - - for i = 1, eqAmount do - local eqStr = net.ReadString() - - eq[i] = eqStr - search["eq_" .. eqStr] = true - end - - -- Traitor things - search.role = net.ReadUInt(ROLE_BITS) - search.team = net.ReadString() - search.c4 = net.ReadInt(bitsRequired(C4_WIRE_COUNT) + 1) - - -- Kill info - search.dmg = net.ReadUInt(30) - search.wep = net.ReadString() - search.head = net.ReadBit() == 1 - search.dtime = net.ReadInt(16) - search.stime = net.ReadInt(16) - - -- Players killed - local num_kills = net.ReadUInt(8) - if num_kills > 0 then - local t_kills = {} - - for i = 1, num_kills do - t_kills[i] = net.ReadUInt(8) - end - - search.kills = t_kills - else - search.kills = nil - end - - search.lastid = {idx = net.ReadUInt(8)} - - -- should we show a menu for this result? - search.finder = net.ReadUInt(8) - search.show = LocalPlayer():EntIndex() == search.finder - - -- last words - local words = net.ReadString() - search.words = (words ~= "") and words or nil - - -- long range - search.lrng = net.ReadBit() - - search.killDistance = net.ReadUInt(2) - search.hitGroup = net.ReadUInt(8) - search.floorSurface = net.ReadUInt(8) - search.waterLevel = net.ReadUInt(2) - search.killOrientation = net.ReadUInt(2) - search.plyModel = net.ReadString() - search.plyModelColor = net.ReadVector() - search.plySID64 = net.ReadString() - search.credits = net.ReadUInt(8) - search.lastDamage = net.ReadUInt(16) - - -- searched by detective? - search.detective_search = net.ReadBool() - - -- set search.show_sb based on detective_search or self search - search.show_sb = search.show or search.detective_search - - --- - -- @realm shared - hook.Run("TTTBodySearchEquipment", search, eq) - - if search.show then - SEARCHSCRN:Show(search) - end - - -- cache search result in rag.search_result, e.g. useful for scoreboard - StoreSearchResult(search) - - search = nil -end) - net.Receive("TTT2SendConfirmMsg", function() local msgName = net.ReadString() local sid64 = net.ReadString() @@ -151,7 +23,14 @@ net.Receive("TTT2SendConfirmMsg", function() tbl.team = LANG.GetTranslation(net.ReadString()) end - eidx = net.ReadUInt(8) + local searchUID = net.ReadUInt(16) + local credits = net.ReadUInt(8) + + -- update credits on victrim table + local victimEnt = player.GetBySteamID64(tbl.victim) + if IsValid(victimEnt) then + victimEnt.searchResultData.credits = credits + end -- checking for bots if sid64 == "" then @@ -166,10 +45,8 @@ net.Receive("TTT2SendConfirmMsg", function() MSTACK:AddColoredImagedMessage(LANG.GetParamTranslation(msgName, tbl), clr, img) - if (IsValid(SEARCHSCRN.menuFrame) and SEARCHSCRN.menuFrame.data.idx == edix) then - SEARCHSCRN.buttonConfirm:SetEnabled(false) - SEARCHSCRN.buttonConfirm:SetText("search_confirmed") - SEARCHSCRN.buttonConfirm:SetIcon(nil) + if (IsValid(SEARCHSCRN.menuFrame) and SEARCHSCRN.data.searchUID == searchUID) then + SEARCHSCRN:PlayerWasConfirmed(credits == 0) end end) @@ -203,6 +80,28 @@ function SEARCHSCRN:CalculateSizes() self.sizes.heightInfoItem = 78 end +function SEARCHSCRN:PlayerWasConfirmed(tookCredits) + self.buttonConfirm:SetEnabled(false) + self.buttonConfirm:SetText("search_confirmed") + self.buttonConfirm:SetIcon(nil) + + if not LocalPlayer():IsSpec() then + self.buttonReport:SetEnabled(true) + end + + -- remove hint about player needing to confirm corpse + local confirmBox = self.infoBoxes["detective_call_confirm"] + if IsValid(confirmBox) then + confirmBox:Remove() + end + + -- if credits were taken, remove credit box + local creditBox = self.infoBoxes["credits"] + if IsValid(creditBox) and tookCredits then + creditBox:Remove() + end +end + function SEARCHSCRN:MakeInfoItem(parent, name, data, height) local box = vgui.Create("DInfoItemTTT2", parent) box:SetSize(self.sizes.widthContentBox, height or self.sizes.heightInfoItem) @@ -236,11 +135,10 @@ function SEARCHSCRN:Show(data) frame:SetKeyboardInputEnabled(true) frame.OnKeyCodePressed = util.BasicKeyHandler + self.data = data self.menuFrame = frame - frame.data = data - - local rd = roles.GetByIndex(data.role) + local rd = roles.GetByIndex(data.subrole) local contentBox = vgui.Create("DPanelTTT2", frame) contentBox:SetSize(self.sizes.widthMainArea, self.sizes.heightMainArea) @@ -249,9 +147,9 @@ function SEARCHSCRN:Show(data) local profileBox = vgui.Create("DProfilePanelTTT2", contentBox) profileBox:SetSize(self.sizes.widthProfileArea, self.sizes.heightMainArea) profileBox:Dock(LEFT) - profileBox:SetModel(data.plyModel, data.plyModelColor) - profileBox:SetPlayerIconBySteamID64(data.plySID64) - profileBox:SetPlayerRoleColor(data.role_color) + profileBox:SetModel(data.playerModel) + profileBox:SetPlayerIconBySteamID64(data.sid64) + profileBox:SetPlayerRoleColor(data.roleColor) profileBox:SetPlayerRoleIcon(rd.iconMaterial) profileBox:SetPlayerRoleString(rd.name) profileBox:SetPlayerTeamString(data.team) @@ -263,7 +161,7 @@ function SEARCHSCRN:Show(data) contentAreaScroll:Dock(RIGHT) -- POPULATE WITH SPECIAL INFORMATION - if data.owner and IsValid(data.owner) and not data.owner:TTT2NETGetBool("body_found", false) then + if data.ragOwner and IsValid(data.ragOwner) and not data.ragOwner:TTT2NETGetBool("body_found", false) then -- a detective can only be called AFTER a body was confirmed self:MakeInfoItem(contentAreaScroll, "detective_call_confirm", { @@ -298,7 +196,7 @@ function SEARCHSCRN:Show(data) hook.Run("TTTBodySearchPopulate", search_add, search) for _, v in pairs(search_add) do if (istable(v.text)) then - self:MakeInfoItem(contentAreaScroll, Material(v.img), {title = v.title, text = text}) + self:MakeInfoItem(contentAreaScroll, Material(v.img), {title = v.title, text = v.text}) else self:MakeInfoItem(contentAreaScroll, Material(v.img), {title = v.title, text = {{body = v.text}}}) end @@ -309,17 +207,17 @@ function SEARCHSCRN:Show(data) buttonArea:SetSize(self.sizes.width, self.sizes.heightBottomButtonPanel) buttonArea:Dock(BOTTOM) - local buttonCall = vgui.Create("DButtonTTT2", buttonArea) - buttonCall:SetText("search_call") - buttonCall:SetSize(self.sizes.widthButton, self.sizes.heightButton) - buttonCall:SetPos(0, self.sizes.padding + 1) - buttonCall.DoClick = function(btn) - RunConsoleCommand("ttt_call_detective", data.eidx) + local buttonReport = vgui.Create("DButtonTTT2", buttonArea) + buttonReport:SetText("search_call") + buttonReport:SetSize(self.sizes.widthButton, self.sizes.heightButton) + buttonReport:SetPos(0, self.sizes.padding + 1) + buttonReport.DoClick = function(btn) + bodysearch.ClientReportsCorpse(data.rag) end - buttonCall:SetIcon(roles.DETECTIVE.iconMaterial, true, 16) + buttonReport:SetIcon(roles.DETECTIVE.iconMaterial, true, 16) - if client:IsSpec() or data.owner and IsValid(data.owner) and not data.owner:TTT2NETGetBool("body_found", false) then - buttonCall:SetEnabled(false) + if client:IsSpec() or data.ragOwner and IsValid(data.ragOwner) and not data.ragOwner:TTT2NETGetBool("body_found", false) then + buttonReport:SetEnabled(false) end local buttonConfirm = vgui.Create("DButtonTTT2", buttonArea) @@ -334,7 +232,7 @@ function SEARCHSCRN:Show(data) buttonConfirm:SetText(text) buttonConfirm:SetSize(self.sizes.widthButton, self.sizes.heightButton) buttonConfirm:SetPos(self.sizes.widthMainArea - self.sizes.widthButton, self.sizes.padding + 1) - elseif data.owner and IsValid(data.owner) and data.owner:TTT2NETGetBool("body_found", false) then + elseif data.ragOwner and IsValid(data.ragOwner) and data.ragOwner:TTT2NETGetBool("body_found", false) then buttonConfirm:SetEnabled(false) buttonConfirm:SetText("search_confirmed") buttonConfirm:SetSize(self.sizes.widthButton, self.sizes.heightButton) @@ -357,23 +255,16 @@ function SEARCHSCRN:Show(data) buttonConfirm:SetPos(self.sizes.widthMainArea - self.sizes.widthButton, self.sizes.padding + 1) end buttonConfirm.DoClick = function(btn) - RunConsoleCommand("ttt_confirm_death", data.eidx, data.eidx + data.dtime, data.lrng) - - local creditBox = self.infoBoxes["credits"] - if IsValid(creditBox) and btn.player_can_take_credits then - creditBox:Remove() - end + bodysearch.ClientConfirmsCorpse(data.rag, data.searchUID, data.lrng) - local confirmBox = self.infoBoxes["detective_call_confirm"] - if IsValid(confirmBox) then - confirmBox:Remove() - end - - if not client:IsSpec() then - buttonCall:SetEnabled(true) - end + -- call this function locally to give the user an instant feedback + -- the same function will be called again after the server processed + -- the confirmation and reported back to the clients + self:PlayerWasConfirmed(btn.player_can_take_credits) end + -- cache button references for external interaction + self.buttonReport = buttonReport self.buttonConfirm = buttonConfirm end diff --git a/gamemodes/terrortown/gamemode/client/vgui/cl_sb_info.lua b/gamemodes/terrortown/gamemode/client/vgui/cl_sb_info.lua index f26b80602e..b33e4d2568 100644 --- a/gamemodes/terrortown/gamemode/client/vgui/cl_sb_info.lua +++ b/gamemodes/terrortown/gamemode/client/vgui/cl_sb_info.lua @@ -94,7 +94,7 @@ end function PANEL:UpdatePlayerData() if not IsValid(self.Player) then return end - if not self.Player.search_result or not self.Player.search_result.show_sb then + if not self.Player.bodySearchResult or not self.Player.bodySearchResult.show_sb then self.Help:SetVisible(true) return @@ -102,13 +102,13 @@ function PANEL:UpdatePlayerData() self.Help:SetVisible(false) - if self.Search == self.Player.search_result then return end + if self.Search == self.Player.bodySearchResult then return end self.List:Clear(true) self.Scroll.Panels = {} - local search_raw = self.Player.search_result + local search_raw = self.Player.bodySearchResult -- standard search result preproc local search = bodysearch.PreprocSearch(search_raw) diff --git a/gamemodes/terrortown/gamemode/client/vgui/cl_sb_row.lua b/gamemodes/terrortown/gamemode/client/vgui/cl_sb_row.lua index 56922eca17..76f7bc3117 100644 --- a/gamemodes/terrortown/gamemode/client/vgui/cl_sb_row.lua +++ b/gamemodes/terrortown/gamemode/client/vgui/cl_sb_row.lua @@ -430,10 +430,10 @@ function PANEL:UpdatePlayerData() self.tag:SetText(ptag and GetTranslation(ptag.txt) or "") self.tag:SetTextColor(ptag and ptag.color or COLOR_WHITE) - self.sresult:SetVisible(ply.search_result and ply.search_result.detective_search) + self.sresult:SetVisible(ply.bodySearchResult and ply.bodySearchResult.isPublicPolicingSearch) -- more blue if a detective searched them - if ply.search_result and (LocalPlayer():GetSubRoleData().isPolicingRole or not ply.search_result.show) then + if ply.bodySearchResult and (LocalPlayer():GetSubRoleData().isPolicingRole or not ply.bodySearchResult.show) then self.sresult:SetImageColor(Color(200, 200, 255)) end diff --git a/gamemodes/terrortown/gamemode/server/sv_corpse.lua b/gamemodes/terrortown/gamemode/server/sv_corpse.lua index aea8679c90..9de958b404 100644 --- a/gamemodes/terrortown/gamemode/server/sv_corpse.lua +++ b/gamemodes/terrortown/gamemode/server/sv_corpse.lua @@ -72,7 +72,7 @@ function CORPSE.SetCredits(rag, credits) rag:SetDTInt(dti.INT_CREDITS, credits) end -local function IdentifyBody(ply, rag) +function CORPSE.IdentifyBody(ply, rag, searchUID) if not ply:IsTerror() or not ply:Alive() then return end -- simplified case for those who die and get found during prep @@ -188,114 +188,19 @@ local function IdentifyBody(ply, rag) net.WriteString(team) end - net.WriteUInt(ply.search_id and ply.search_id.eidx or 0, 8) + -- send searchUID to update UI buttons on client + net.WriteUInt(searchUID or 0, 16) - net.Broadcast() - end -end - -local function GiveFoundCredits(ply, rag, isLongRange) - local corpseNick = CORPSE.GetPlayerNick(rag) - local credits = CORPSE.GetCredits(rag, 0) - - if not ply:IsActiveShopper() or ply:GetSubRoleData().preventFindCredits - or credits == 0 or isLongRange - then return end - - LANG.Msg(ply, "body_credits", {num = credits}) - - ply:AddCredits(credits) - - CORPSE.SetCredits(rag, 0) - - ServerLog(ply:Nick() .. " took " .. credits .. " credits from the body of " .. corpseNick .. "\n") - - events.Trigger(EVENT_CREDITFOUND, ply, rag, credits) -end - -local function ttt_confirm_death(ply, cmd, args) - if not IsValid(ply) then return end - - if #args < 2 then return end + -- send new credit value as it might have changed + net.WriteUInt(CORPSE.GetCredits(rag, 0), 8) - local eidx = tonumber(args[1]) - local id = tonumber(args[2]) - local isLongRange = (args[3] and tonumber(args[3]) == 1) and true or false - - if not eidx or not id then return end - - if not ply.search_id or ply.search_id.id ~= id or ply.search_id.eidx ~= eidx then - ply.search_id = nil - - return - end - - local rag = Entity(eidx) - - if IsValid(rag) and (rag:GetPos():Distance(ply:GetPos()) < 128 or isLongRange) and not CORPSE.GetFound(rag, false) then - IdentifyBody(ply, rag) - - GiveFoundCredits(ply, rag, false) - end - - ply.search_id = nil -end -concommand.Add("ttt_confirm_death", ttt_confirm_death) - --- Call detectives to a corpse -local function ttt_call_detective(ply, cmd, args) - if not IsValid(ply) then return end - - if #args ~= 1 then return end - - if not ply:IsActive() then return end - - local eidx = tonumber(args[1]) - if not eidx then return end - - local rag = Entity(eidx) - - if IsValid(rag) and rag:GetPos():Distance(ply:GetPos()) < 128 then - if CORPSE.GetFound(rag, false) then - local plyTable = util.GetFilteredPlayers(function(p) - return p:GetSubRoleData().isPolicingRole and p:IsTerror() - end) - - --- - -- @realm server - hook.Run("TTT2ModifyCorpseCallRadarRecipients", plyTable, rag, ply) - - -- show indicator in radar to detectives - net.Start("TTT_CorpseCall") - net.WriteVector(rag:GetPos()) - net.Send(plyTable) - - LANG.MsgAll("body_call", {player = ply:Nick(), victim = CORPSE.GetPlayerNick(rag, "someone")}, MSG_MSTACK_PLAIN) - - --- - -- @realm server - hook.Run("TTT2CalledPolicingRole", plyTable, ply, rag, CORPSE.GetPlayer(rag)) - else - LANG.Msg(ply, "body_call_error", nil, MSG_MSTACK_WARN) - end - end -end -concommand.Add("ttt_call_detective", ttt_call_detective) - -local function bitsRequired(num) - local bits, max = 0, 1 - - while max <= num do - bits = bits + 1 -- increase - max = max + max -- double + net.Broadcast() end - - return bits end --- -- Send a usermessage to client containing search results. --- @param Player ply The player that is inspection the ragdoll +-- @param Player ply The player that is inspecting the ragdoll -- @param Entity rag The ragdoll that is inspected -- @param boolean isCovert Is the search hidden -- @param boolean isLongRange Is the search performed from a long range @@ -303,16 +208,7 @@ end function CORPSE.ShowSearch(ply, rag, isCovert, isLongRange) if not IsValid(ply) or not IsValid(rag) then return end - local roleData = ply:GetSubRoleData() - - if cvDeteOnlyInspect:GetBool() and not roleData.isPolicingRole then - LANG.Msg(ply, "inspect_detective_only", nil, MSG_MSTACK_WARN) - - GiveFoundCredits(ply, rag, isLongRange) - - return - end - + -- prevent search for anyone if the body is burning if rag:IsOnFire() then LANG.Msg(ply, "body_burning", nil, MSG_CHAT_WARN) @@ -323,173 +219,30 @@ function CORPSE.ShowSearch(ply, rag, isCovert, isLongRange) -- @realm server if not hook.Run("TTTCanSearchCorpse", ply, rag, isCovert, isLongRange) then return end - -- init a heap of data we'll be sending - local nick = CORPSE.GetPlayerNick(rag) - local subrole = rag.was_role - local role_color = rag.role_color - local team = rag.was_team - local eq = rag.equipment or {} - local c4 = rag.bomb_wire or - 1 - local dmg = rag.dmgtype or DMG_GENERIC - local wep = rag.dmgwep or "" - local words = rag.last_words or "" - local hshot = rag.was_headshot or false - local dtime = rag.time or 0 - - -- prepare additional corpse information - local killDistance = CORPSE_KILL_NONE - if rag.scene.hit_trace then - local rawKillDistance = rag.scene.hit_trace.StartPos:Distance(rag.scene.hit_trace.HitPos) - if rawKillDistance < 200 then - killDistance = CORPSE_KILL_POINT_BLANK - elseif rawKillDistance >= 700 then - killDistance = CORPSE_KILL_FAR - elseif rawKillDistance >= 200 then - killDistance = CORPSE_KILL_CLOSE - end - end - - local killHitGroup = HITGROUP_GENERIC - if rag.scene.hit_group and rag.scene.hit_group > 0 then - killHitGroup = rag.scene.hit_group - end + local sData = bodysearch.AssimilateSceneData(ply, rag, isCovert, isLongRange) - local killFloorSurface = rag.scene.floorSurface or 0 - local killWaterLevel = rag.scene.waterLevel or 0 + if not sData then return end - local killAngle = CORPSE_KILL_NONE - if rag.scene.hit_trace then - local rawKillAngle = math.abs(math.AngleDifference(rag.scene.hit_trace.StartAng.yaw, rag.scene.victim.aim_yaw)) - - if rawKillAngle < 45 then - killAngle = CORPSE_KILL_BACK - elseif rawKillAngle < 135 then - killAngle = CORPSE_KILL_SIDE - else - killAngle = CORPSE_KILL_FRONT - end + -- only give credits if body is also confirmed + if not isCovert then + bodysearch.GiveFoundCredits(ply, rag, isLongRange) end - local plyModel = rag.scene.plyModel or "" - local plyModelColor = rag.scene.plyModelColor or COLOR_WHITE - local plySID64 = rag.scene.plySID64 or "" - local lastDamage = math.max(0, rag.scene.lastDamage) - - local owner = player.GetBySteamID64(rag.sid64) - owner = IsValid(owner) and owner:EntIndex() or -1 - - -- basic sanity check - if not nick or not eq or not subrole or not team then return end - if GetConVar("ttt_identify_body_woconfirm"):GetBool() and DetectiveMode() and not isCovert then - IdentifyBody(ply, rag) - end - - -- only give credits if body is also confirmed - if not isCovert then - GiveFoundCredits(ply, rag, isLongRange) + CORPSE.IdentifyBody(ply, rag, sData.searchUID) end -- cache credits of corpse here, AFTER one might has taken them - local credits = CORPSE.GetCredits(rag, 0) - - -- time of death relative to current time (saves bits) - if dtime ~= 0 then - dtime = dtime - end + sData.credits = CORPSE.GetCredits(rag, 0) -- identifier so we know whether a ttt_confirm_death was legit - ply.search_id = {eidx = rag:EntIndex(), id = rag:EntIndex() + math.floor(dtime)} - - -- time of dna sample decay relative to current time - local stime = 0 - - if rag.killer_sample then - stime = rag.killer_sample.t - end - - -- build list of people this player killed, but only if convar is enabled - local kill_entids = {} - if GetConVar("ttt2_confirm_killlist"):GetBool() then - local ragKills = rag.kills - - for i = 1, #ragKills do - local vicsid = ragKills[i] - - -- also send disconnected players as a marker - local vic = player.GetBySteamID64(vicsid) - - kill_entids[#kill_entids + 1] = IsValid(vic) and vic:EntIndex() or -1 - end - end - - local lastid = -1 - - if rag.lastid and ply:IsActive() and roleData.isPolicingRole then - -- if the person this victim last id'd has since disconnected, send -1 to - -- indicate this - lastid = IsValid(rag.lastid.ent) and rag.lastid.ent:EntIndex() or - 1 - end - - -- Send a message with basic info - net.Start("TTT_RagdollSearch") - net.WriteUInt(rag:EntIndex(), 16) -- 16 bits - net.WriteUInt(owner, 8) -- 128 max players. (8 bits) - net.WriteString(nick) - net.WriteColor(role_color) - - net.WriteUInt(#eq, 16) -- Equipment (16 = max.) - - for i = 1, #eq do - net.WriteString(eq[i]) - end + ply.searchID = sData.searchUID - net.WriteUInt(subrole, ROLE_BITS) -- (... bits) - net.WriteString(team) - net.WriteInt(c4, bitsRequired(C4_WIRE_COUNT) + 1) -- -1 -> 2^bits (default c4: 4 bits) - net.WriteUInt(dmg, 30) -- DMG_BUCKSHOT is the highest. (30 bits) - net.WriteString(wep) - net.WriteBit(hshot) -- (1 bit) - net.WriteInt(dtime, 16) - net.WriteInt(stime, 16) - - net.WriteUInt(#kill_entids, 8) - - for i = 1, #kill_entids do - net.WriteUInt(kill_entids[i], 8) -- first game.MaxPlayers() of entities are for players. - end - - net.WriteUInt(lastid, 8) - - -- Who found this, so if we get this from a detective we can decide not to - -- show a window - net.WriteUInt(ply:EntIndex(), 8) - net.WriteString(words) - - net.WriteBit(isLongRange) - - net.WriteUInt(killDistance, 2) - net.WriteUInt(killHitGroup, 8) - net.WriteUInt(killFloorSurface, 8) - net.WriteUInt(killWaterLevel, 2) - net.WriteUInt(killAngle, 2) - net.WriteString(plyModel) - net.WriteVector(plyModelColor) - net.WriteString(plySID64) - net.WriteUInt(credits, 8) - net.WriteUInt(lastDamage, 16) - - -- 133 + string data + #kill_entids * 8 + team + 1 - -- 200 + ? - - -- workaround to make sure only detective searches are added to the scoreboard - net.WriteBool(ply:IsActive() and roleData.isPolicingRole and not isCovert) - - -- If searched publicly, send to all, else just the finder - if not isCovert then - net.Broadcast() + local roleData = ply:GetSubRoleData() + if ply:IsActive() and roleData.isPolicingRole and roleData.isPublicRole and not isCovert then + bodysearch.StreamSceneData(sData) else - net.Send(ply) + bodysearch.StreamSceneData(sData, ply) end end diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index 79cd473f32..ee3a5f410e 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -206,7 +206,6 @@ local ttt_newroles_enabled = CreateConVar("ttt_newroles_enabled", "1", {FCVAR_NO -- Pool some network names. util.AddNetworkString("TTT_RoundState") -util.AddNetworkString("TTT_RagdollSearch") util.AddNetworkString("TTT_GameMsg") util.AddNetworkString("TTT_GameMsgColor") util.AddNetworkString("TTT_RoleChat") diff --git a/lua/ttt2/libraries/bodysearch.lua b/lua/ttt2/libraries/bodysearch.lua index 9f9ed6d513..7900ba8a02 100644 --- a/lua/ttt2/libraries/bodysearch.lua +++ b/lua/ttt2/libraries/bodysearch.lua @@ -16,10 +16,194 @@ CORPSE_KILL_FRONT = 1 CORPSE_KILL_BACK = 2 CORPSE_KILL_SIDE = 3 +--- +-- @realm shared +-- mode 0: normal behavior, everyone can search/confirm bodies +-- mode 1: only public policing roles can confirm bodies, but everyone can still see all data in the menu +-- mode 2: only public policing roles can confirm and search bodies +local cvInspectConfirmMode = CreateConVar("ttt2_inspect_confirm_mode", "0", {FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED}) + bodysearch = bodysearch or {} if SERVER then + local mathMax = math.max + + util.AddNetworkString("ttt2_client_reports_corpse") + util.AddNetworkString("ttt2_client_confirm_corpse") + + net.Receive("ttt2_client_confirm_corpse", function(_, ply) + if not IsValid(ply) then return end + + local rag = net.ReadEntity() + local searchUID = net.ReadUInt(16) + local isLongRange = net.ReadBool() + + if ply.searchID ~= searchUID then + ply.searchID = nil + + return + end + + if IsValid(rag) and (rag:GetPos():Distance(ply:GetPos()) < 128 or isLongRange) and not CORPSE.GetFound(rag, false) then + CORPSE.IdentifyBody(ply, rag, searchUID) + + bodysearch.GiveFoundCredits(ply, rag, false) + end + + ply.searchID = nil + end) + + net.Receive("ttt2_client_reports_corpse", function(_, ply) + if not IsValid(ply) then return end + + if not ply:IsActive() then return end + + local rag = net.ReadEntity() + + if not IsValid(rag) or rag:GetPos():Distance(ply:GetPos()) > 128 then return end + + if CORPSE.GetFound(rag, false) then + local plyTable = util.GetFilteredPlayers(function(p) + local roleData = p:GetSubRoleData() + + return roleData.isPolicingRole and p.isPublicRole and p:IsTerror() + end) + + --- + -- @realm server + hook.Run("TTT2ModifyCorpseCallRadarRecipients", plyTable, rag, ply) + + -- show indicator in radar to detectives + net.Start("TTT_CorpseCall") + net.WriteVector(rag:GetPos()) + net.Send(plyTable) + LANG.MsgAll("body_call", {player = ply:Nick(), victim = CORPSE.GetPlayerNick(rag, "someone")}, MSG_MSTACK_PLAIN) + + --- + -- @realm server + hook.Run("TTT2CalledPolicingRole", plyTable, ply, rag, CORPSE.GetPlayer(rag)) + else + LANG.Msg(ply, "body_call_error", nil, MSG_MSTACK_WARN) + end + end) + + function bodysearch.GiveFoundCredits(ply, rag, isLongRange) + local corpseNick = CORPSE.GetPlayerNick(rag) + local credits = CORPSE.GetCredits(rag, 0) + + if not ply:IsActiveShopper() or ply:GetSubRoleData().preventFindCredits + or credits == 0 or isLongRange + then return end + + LANG.Msg(ply, "body_credits", {num = credits}) + + ply:AddCredits(credits) + + CORPSE.SetCredits(rag, 0) + + ServerLog(ply:Nick() .. " took " .. credits .. " credits from the body of " .. corpseNick .. "\n") + + events.Trigger(EVENT_CREDITFOUND, ply, rag, credits) + end + + function bodysearch.AssimilateSceneData(inspector, rag, isCovert, isLongRange) + local sData = {} + local inspectorRoleData = inspector:GetSubRoleData() + + -- if a non-public or non-policing role tries to search a body in mode 2, nothing happens + if cvInspectConfirmMode:GetInt() == 2 and not inspectorRoleData.isPolicingRole and not inspectorRoleData.isPublicRole then + LANG.Msg(inspector, "inspect_detective_only", nil, MSG_MSTACK_WARN) + + return + end + + sData.nick = CORPSE.GetPlayerNick(rag) + sData.subrole = rag.was_role + sData.roleColor = rag.role_color + sData.team = rag.was_team + + if not sData.nick or not sData.subrole or not sData.team then + return + end + + sData.inspector = inspector + sData.rag = rag + sData.eq = rag.equipment or {} + sData.c4 = rag.bomb_wire or - 1 + sData.dmg = rag.dmgtype or DMG_GENERIC + sData.wep = rag.dmgwep or "" + sData.words = rag.last_words + sData.hshot = rag.was_headshot or false + sData.dtime = rag.time or 0 + sData.playerModel = rag.scene.plyModel or "" + sData.sid64 = rag.scene.plySID64 or "" + sData.lastDamage = mathMax(0, rag.scene.lastDamage) + sData.killFloorSurface = rag.scene.floorSurface or 0 + sData.killWaterLevel = rag.scene.waterLevel or 0 + sData.credits = CORPSE.GetCredits(rag, 0) + sData.lastSeenEnt = rag.lastid and rag.lastid.ent or nil + sData.isPublicPolicingSearch = inspector:IsActive() and inspectorRoleData.isPolicingRole and inspectorRoleData.isPublicRole and not isCovert + sData.searchUID = math.floor(rag:EntIndex() + sData.dtime) + + sData.ragOwner = player.GetBySteamID64(rag.sid64) + + sData.killDistance = CORPSE_KILL_NONE + if rag.scene.hit_trace then + local rawKillDistance = rag.scene.hit_trace.StartPos:Distance(rag.scene.hit_trace.HitPos) + if rawKillDistance < 200 then + sData.killDistance = CORPSE_KILL_POINT_BLANK + elseif rawKillDistance >= 700 then + sData.killDistance = CORPSE_KILL_FAR + elseif rawKillDistance >= 200 then + sData.killDistance = CORPSE_KILL_CLOSE + end + end + + sData.killHitGroup = HITGROUP_GENERIC + if rag.scene.hit_group and rag.scene.hit_group > 0 then + sData.killHitGroup = rag.scene.hit_group + end + + sData.killOrientation = CORPSE_KILL_NONE + if rag.scene.hit_trace then + local rawKillAngle = math.abs(math.AngleDifference(rag.scene.hit_trace.StartAng.yaw, rag.scene.victim.aim_yaw)) + + if rawKillAngle < 45 then + sData.killOrientation = CORPSE_KILL_BACK + elseif rawKillAngle < 135 then + sData.killOrientation = CORPSE_KILL_SIDE + else + sData.killOrientation = CORPSE_KILL_FRONT + end + end + + sData.stime = 0 + if rag.killer_sample then + sData.stime = rag.killer_sample.t + end + + -- build list of people this player killed, but only if convar is enabled + sData.kill_entids = {} + if GetConVar("ttt2_confirm_killlist"):GetBool() then + local ragKills = rag.kills + + for i = 1, #ragKills do + local vicsid = ragKills[i] + + -- also send disconnected players as a marker + local vic = player.GetBySteamID64(vicsid) + + sData.kill_entids[#kill_entids + 1] = IsValid(vic) and vic:EntIndex() or -1 + end + end + + return sData + end + + function bodysearch.StreamSceneData(sData, client) + net.SendStream("TTT2_BodySearchData", sData, client) + end end if CLIENT then @@ -32,6 +216,27 @@ if CLIENT then local IsValid = IsValid local pairs = pairs + net.ReceiveStream("TTT2_BodySearchData", function(searchStreamData) + PrintTable(searchStreamData) + + local eq = {} -- placeholder for the hook, not used right now + --- + -- @realm shared + hook.Run("TTTBodySearchEquipment", searchStreamData, eq) + + searchStreamData.show = LocalPlayer() == searchStreamData.inspector + + if searchStreamData.show then + SEARCHSCRN:Show(searchStreamData) + end + + -- add this hack here to keep compatibility to the old scoreboard + searchStreamData.show_sb = searchStreamData.show or searchStreamData.isPublicPolicingSearch + + -- cache search result in rag.bodySearchResult, e.g. useful for scoreboard + bodysearch.StoreSearchResult(searchStreamData) + end) + local damageToText = { ["crush"] = DMG_CRUSH, ["bullet"] = DMG_BULLET, @@ -201,9 +406,9 @@ if CLIENT then } end - if data.hitGroup > 0 then + if data.killHitGroup > 0 then rawText.text[#rawText.text + 1] = { - body = hitgroup_to_text[data.hitGroup], + body = hitgroup_to_text[data.killHitGroup], params = nil } end @@ -282,11 +487,7 @@ if CLIENT then end end, last_id = function(data) - if not data.idx or data.idx == -1 then return end - - local ent = Entity(data.idx) - - if not IsValid(ent) or not ent:IsPlayer() then return end + if not IsValid(data.lastSeenEnt) or not data.lastSeenEnt:IsPlayer() then return end return { title = { @@ -295,12 +496,12 @@ if CLIENT then }, text = {{ body = "search_eyes", - params = {player = ent:Nick()} + params = {player = data.lastSeenEnt:Nick()} }} } end, floor_surface = function(data) - if data.floorSurface == 0 or not floorIDToText[data.floorSurface] then return end + if data.killFloorSurface == 0 or not floorIDToText[data.killFloorSurface] then return end return { title = { @@ -308,7 +509,7 @@ if CLIENT then params = nil }, text = {{ - body = floorIDToText[data.floorSurface], + body = floorIDToText[data.killFloorSurface], params = nil }} } @@ -328,15 +529,15 @@ if CLIENT then } end, water_level = function(data) - if not data.waterLevel or data.waterLevel == 0 then return end + if not data.killWaterLevel or data.killWaterLevel == 0 then return end return { title = { body = "search_title_water", - params = {level = data.waterLevel} + params = {level = data.killWaterLevel} }, text = {{ - body = "search_water_" .. data.waterLevel, + body = "search_water_" .. data.killWaterLevel, params = nil }} } @@ -520,6 +721,42 @@ if CLIENT then return search end + function bodysearch.StoreSearchResult(sData) + if not sData.ragOwner then return end + + -- if existing result was not ours, it was detective's, and should not + -- be overwritten + local ply = sData.ragOwner + + -- if the currently stored search result is by a public policing role, it should be kept + -- it can be overwritten by another public policing role though + if ply.bodySearchResult and ply.bodySearchResult.isPublicPolicingSearch + and not sData.isPublicPolicingSearch + then return end + + ply.bodySearchResult = sData + end + + function bodysearch.ResetSearchResult(ply) + if not IsValid(ply) then return end + + ply.bodySearchResult = nil + end + + function bodysearch.ClientConfirmsCorpse(rag, searchUID, isLongRange) + net.Start("ttt2_client_confirm_corpse") + net.WriteEntity(rag) + net.WriteUInt(searchUID, 16) + net.WriteBool(isLongRange) + net.SendToServer() + end + + function bodysearch.ClientReportsCorpse(rag) + net.Start("ttt2_client_reports_corpse") + net.WriteEntity(rag) + net.SendToServer() + end + -- HOOKS -- --- diff --git a/lua/ttt2/libraries/targetid.lua b/lua/ttt2/libraries/targetid.lua index 7e4061b0ed..478a5bda6a 100644 --- a/lua/ttt2/libraries/targetid.lua +++ b/lua/ttt2/libraries/targetid.lua @@ -479,9 +479,9 @@ function targetid.HUDDrawTargetIDRagdolls(tData) if not CORPSE.GetPlayerNick(ent, false) then return end local corpse_found = CORPSE.GetFound(ent, false) or not DetectiveMode() - local role_found = corpse_found and ent.search_result and ent.search_result.role + local role_found = corpse_found and ent.bodySearchResult and ent.bodySearchResult.role local binoculars_useable = IsValid(c_wep) and c_wep:GetClass() == "weapon_ttt_binoculars" or false - local roleData = roles.GetByIndex(role_found and ent.search_result.role or ROLE_INNOCENT) + local roleData = roles.GetByIndex(role_found and ent.bodySearchResult.role or ROLE_INNOCENT) local roleDataClient = client:GetSubRoleData() -- enable targetID rendering @@ -530,7 +530,7 @@ function targetid.HUDDrawTargetIDRagdolls(tData) end -- add info if searched by detectives - if ent.search_result and ent.search_result.detective_search and roleDataClient.isPolicingRole then + if ent.bodySearchResult and ent.bodySearchResult.isPublicPolicingSearch and roleDataClient.isPolicingRole then tData:AddDescriptionLine( TryT("corpse_searched_by_detective"), roles.DETECTIVE.ltcolor,