--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] -- Body search popup local T = LANG.GetTranslation local PT = LANG.GetParamTranslation local is_dmg = util.BitSet local dtt = { search_dmg_crush = DMG_CRUSH, search_dmg_bullet = DMG_BULLET, search_dmg_fall = DMG_FALL, search_dmg_boom = DMG_BLAST, search_dmg_club = DMG_CLUB, search_dmg_drown = DMG_DROWN, search_dmg_stab = DMG_SLASH, search_dmg_burn = DMG_BURN, search_dmg_tele = DMG_SONIC, search_dmg_car = DMG_VEHICLE } -- "From his body you can tell XXX" local function DmgToText(d) for k, v in pairs(dtt) do if is_dmg(d, v) then return T(k) end end if is_dmg(d, DMG_DIRECT) then return T("search_dmg_burn") end return T("search_dmg_other") end -- Info type to icon mapping -- Some icons have different appearances based on the data value. These have a -- separate table inside the TypeToMat table. -- Those that have a lot of possible data values are defined separately, either -- as a function or a table. local dtm = { bullet = DMG_BULLET, rock = DMG_CRUSH, splode = DMG_BLAST, fall = DMG_FALL, fire = DMG_BURN } local function DmgToMat(d) for k, v in pairs(dtm) do if is_dmg(d, v) then return k end end if is_dmg(d, DMG_DIRECT) then return "fire" else return "skull" end end local function WeaponToIcon(d) local wep = util.WeaponForClass(d) return wep and wep.Icon or "vgui/ttt/icon_nades" end local TypeToMat = { nick="id", words="halp", eq_armor="armor", eq_radar="radar", eq_disg="disguise", role={[ROLE_TRAITOR]="traitor", [ROLE_DETECTIVE]="det", [ROLE_INNOCENT]="inno"}, c4="code", dmg=DmgToMat, wep=WeaponToIcon, head="head", dtime="time", stime="wtester", lastid="lastid", kills="list" } -- Accessor for better fail handling local function IconForInfoType(t, data) local base = "vgui/ttt/icon_" local mat = TypeToMat[t] if istable(mat) then mat = mat[data] elseif isfunction(mat) then mat = mat(data) end if not mat then mat = TypeToMat["nick"] end -- ugly special casing for weapons, because they are more likely to be -- customized and hence need more freedom in their icon filename if t != "wep" then return base .. mat else return mat end end function PreprocSearch(raw) local search = {} for t, d in pairs(raw) do search[t] = {img=nil, text="", p=10} if t == "nick" then search[t].text = PT("search_nick", {player = d}) search[t].p = 1 search[t].nick = d elseif t == "role" then if d == ROLE_TRAITOR then search[t].text = T("search_role_t") elseif d == ROLE_DETECTIVE then search[t].text = T("search_role_d") else search[t].text = T("search_role_i") end search[t].p = 2 elseif t == "words" then if d != "" then -- only append "--" if there's no ending interpunction local final = string.match(d, "[\\.\\!\\?]$") != nil search[t].text = PT("search_words", {lastwords = d .. (final and "" or "--.")}) end elseif t == "eq_armor" then if d then search[t].text = T("search_armor") search[t].p = 17 end elseif t == "eq_disg" then if d then search[t].text = T("search_disg") search[t].p = 18 end elseif t == "eq_radar" then if d then search[t].text = T("search_radar") search[t].p = 19 end elseif t == "c4" then if d > 0 then search[t].text= PT("search_c4", {num = d}) end elseif t == "dmg" then search[t].text = DmgToText(d) search[t].p = 12 elseif t == "wep" then local wep = util.WeaponForClass(d) local wname = wep and LANG.TryTranslation(wep.PrintName) if wname then search[t].text = PT("search_weapon", {weapon = wname}) end elseif t == "head" then if d then search[t].text = T("search_head") end search[t].p = 15 elseif t == "dtime" then if d != 0 then local ftime = util.SimpleTime(d, "%02i:%02i") search[t].text = PT("search_time", {time = ftime}) search[t].text_icon = ftime search[t].p = 8 end elseif t == "stime" then if d > 0 then local ftime = util.SimpleTime(d, "%02i:%02i") search[t].text = PT("search_dna", {time = ftime}) search[t].text_icon = ftime end elseif t == "kills" then local num = table.Count(d) if num == 1 then local vic = Entity(d[1]) local dc = d[1] == 0 -- disconnected if dc or (IsValid(vic) and vic:IsPlayer()) then search[t].text = PT("search_kills1", {player = (dc and "" or vic:Nick())}) end elseif num > 1 then local txt = T("search_kills2") .. "\n" local nicks = {} for k, idx in ipairs(d) do local vic = Entity(idx) local dc = idx == 0 if dc or (IsValid(vic) and vic:IsPlayer()) then table.insert(nicks, (dc and "" or vic:Nick())) end end local last = #nicks txt = txt .. table.concat(nicks, "\n", 1, last) search[t].text = txt end search[t].p = 30 elseif t == "lastid" then if d and d.idx != 0 then local ent = Entity(d.idx) if IsValid(ent) and ent:IsPlayer() then search[t].text = PT("search_eyes", {player = ent:Nick()}) search[t].ply = ent end end else -- Not matching a type, so don't display search[t] = nil end -- anything matching a type but not given a text should be removed if search[t] and search[t].text == "" then search[t] = nil end -- if there's still something here, we'll be showing it, so find an icon if search[t] then search[t].img = IconForInfoType(t, d) end end hook.Call("TTTBodySearchPopulate", nil, search, raw) return search end -- Returns a function meant to override OnActivePanelChanged, which modifies -- dactive and dtext based on the search information that is associated with the -- newly selected panel local function SearchInfoController(search, dactive, dtext) return function(s, pold, pnew) local t = pnew.info_type local data = search[t] if not data then ErrorNoHalt("Search: data not found", t, data,"\n") return end -- If wrapping is on, the Label's SizeToContentsY misbehaves for -- text that does not need wrapping. I long ago stopped wondering -- "why" when it comes to VGUI. Apply hack, move on. dtext:GetLabel():SetWrap(#data.text > 50) dtext:SetText(data.text) dactive:SetImage(data.img) end end local function ShowSearchScreen(search_raw) local client = LocalPlayer() if not IsValid(client) then return end local m = 8 local bw, bh = 100, 25 local bw_large = 125 local w, h = 425, 260 local rw, rh = (w - m*2), (h - 25 - m*2) local rx, ry = 0, 0 local rows = 1 local listw, listh = rw, (64 * rows + 6) local listx, listy = rx, ry ry = ry + listh + m*2 rx = m local descw, desch = rw - m*2, 80 local descx, descy = rx, ry ry = ry + desch + m local butx, buty = rx, ry local dframe = vgui.Create("DFrame") dframe:SetSize(w, h) dframe:Center() dframe:SetTitle(T("search_title") .. " - " .. search_raw.nick or "???") dframe:SetVisible(true) dframe:ShowCloseButton(true) dframe:SetMouseInputEnabled(true) dframe:SetKeyboardInputEnabled(true) dframe:SetDeleteOnClose(true) dframe.OnKeyCodePressed = util.BasicKeyHandler -- contents wrapper local dcont = vgui.Create("DPanel", dframe) dcont:SetPaintBackground(false) dcont:SetSize(rw, rh) dcont:SetPos(m, 25 + m) -- icon list local dlist = vgui.Create("DPanelSelect", dcont) dlist:SetPos(listx, listy) dlist:SetSize(listw, listh) dlist:EnableHorizontal(true) dlist:SetSpacing(1) dlist:SetPadding(2) if dlist.VBar then dlist.VBar:Remove() dlist.VBar = nil end -- description area local dscroll = vgui.Create("DHorizontalScroller", dlist) dscroll:StretchToParent(3,3,3,3) local ddesc = vgui.Create("ColoredBox", dcont) ddesc:SetColor(Color(50, 50, 50)) ddesc:SetName(T("search_info")) ddesc:SetPos(descx, descy) ddesc:SetSize(descw, desch) local dactive = vgui.Create("DImage", ddesc) dactive:SetImage("vgui/ttt/icon_id") dactive:SetPos(m, m) dactive:SetSize(64, 64) local dtext = vgui.Create("ScrollLabel", ddesc) dtext:SetSize(descw - 120, desch - m*2) dtext:MoveRightOf(dactive, m*2) dtext:AlignTop(m) dtext:SetText("...") -- buttons local by = rh - bh - (m/2) local dident = vgui.Create("DButton", dcont) dident:SetPos(m, by) dident:SetSize(bw_large, bh) dident:SetText(T("search_confirm")) local id = search_raw.eidx + search_raw.dtime dident.DoClick = function() RunConsoleCommand("ttt_confirm_death", search_raw.eidx, id) end dident:SetDisabled(client:IsSpec() or (not client:KeyDownLast(IN_WALK))) local dcall = vgui.Create("DButton", dcont) dcall:SetPos(m*2 + bw_large, by) dcall:SetSize(bw_large, bh) dcall:SetText(T("search_call")) dcall.DoClick = function(s) client.called_corpses = client.called_corpses or {} table.insert(client.called_corpses, search_raw.eidx) s:SetDisabled(true) RunConsoleCommand("ttt_call_detective", search_raw.eidx) end dcall:SetDisabled(client:IsSpec() or table.HasValue(client.called_corpses or {}, search_raw.eidx)) local dconfirm = vgui.Create("DButton", dcont) dconfirm:SetPos(rw - m - bw, by) dconfirm:SetSize(bw, bh) dconfirm:SetText(T("close")) dconfirm.DoClick = function() dframe:Close() end -- Finalize search data, prune stuff that won't be shown etc -- search is a table of tables that have an img and text key local search = PreprocSearch(search_raw) -- Install info controller that will link up the icons to the text etc dlist.OnActivePanelChanged = SearchInfoController(search, dactive, dtext) -- Create table of SimpleIcons, each standing for a piece of search -- information. local start_icon = nil for t, info in SortedPairsByMemberValue(search, "p") do local ic = nil -- Certain items need a special icon conveying additional information if t == "nick" then local avply = IsValid(search_raw.owner) and search_raw.owner or nil ic = vgui.Create("SimpleIconAvatar", dlist) ic:SetPlayer(avply) start_icon = ic elseif t == "lastid" then ic = vgui.Create("SimpleIconAvatar", dlist) ic:SetPlayer(info.ply) ic:SetAvatarSize(24) elseif info.text_icon then ic = vgui.Create("SimpleIconLabelled", dlist) ic:SetIconText(info.text_icon) else ic = vgui.Create("SimpleIcon", dlist) end ic:SetIconSize(64) ic:SetIcon(info.img) ic.info_type = t dlist:AddPanel(ic) dscroll:AddPanel(ic) end dlist:SelectPanel(start_icon) dframe:MakePopup() end local function StoreSearchResult(search) if search.owner then -- if existing result was not ours, it was detective's, and should not -- be overwritten local ply = search.owner if (not ply.search_result) or ply.search_result.show then ply.search_result = search -- this is useful for targetid local rag = Entity(search.eidx) if IsValid(rag) then rag.search_result = search end end end end local function bitsRequired(num) local bits, max = 0, 1 while max <= num do bits = bits + 1 max = max + max end return bits end local plyBits = bitsRequired(game.MaxPlayers()) local search = {} local function ReceiveRagdollSearch() search = {} -- Basic info search.eidx = net.ReadUInt(16) search.owner = Entity(net.ReadUInt(plyBits)) if not (IsValid(search.owner) and search.owner:IsPlayer() and (not search.owner:IsTerror())) then search.owner = nil end search.nick = net.ReadString() -- Equipment local eq = net.ReadUInt(bitsRequired(EQUIP_MAX)) -- All equipment pieces get their own icon search.eq_armor = util.BitSet(eq, EQUIP_ARMOR) search.eq_radar = util.BitSet(eq, EQUIP_RADAR) search.eq_disg = util.BitSet(eq, EQUIP_DISGUISE) -- Traitor things search.role = net.ReadUInt(2) search.c4 = net.ReadUInt(bitsRequired(C4_WIRE_COUNT)) -- Kill info search.dmg = net.ReadUInt(30) search.wep = net.ReadString() search.head = net.ReadBool() search.dtime = net.ReadUInt(15) search.stime = net.ReadUInt(15) -- Players killed local num_kills = net.ReadUInt(8) if num_kills > 0 then search.kills = {} for i=1,num_kills do table.insert(search.kills, net.ReadUInt(plyBits)) end else search.kills = nil end search.lastid = {idx=net.ReadUInt(plyBits)} -- should we show a menu for this result? search.finder = net.ReadUInt(plyBits) search.show = (LocalPlayer():EntIndex() == search.finder) -- -- last words -- local words = net.ReadString() search.words = (words ~= "") and words or nil hook.Call("TTTBodySearchEquipment", nil, search, eq) if search.show and hook.Run("TTTShowSearchScreen", search) ~= false then ShowSearchScreen(search) end StoreSearchResult(search) search = nil end net.Receive("TTT_RagdollSearch", ReceiveRagdollSearch)