-- Game report require("glon") include("cl_awards.lua") local table = table local string = string local vgui = vgui local pairs = pairs CLSCORE = {} CLSCORE.Events = {} CLSCORE.Scores = {} CLSCORE.TraitorIDs = {} CLSCORE.DetectiveIDs = {} CLSCORE.Players = {} CLSCORE.StartTime = 0 CLSCORE.Panel = nil CLSCORE.EventDisplay = {} local skull_icon = Material("HUD/killicons/default") surface.CreateFont("Trebuchet24", 72, 1000, true, false, "WinHuge", true) -- so much text here I'm using shorter names than usual local T = LANG.GetTranslation local PT = LANG.GetParamTranslation function CLSCORE:GetDisplay(key, event) local displayfns = self.EventDisplay[event.id] if not displayfns then return end local keyfn = displayfns[key] if not keyfn then return end return keyfn(event) end function CLSCORE:TextForEvent(e) return self:GetDisplay("text", e) end function CLSCORE:IconForEvent(e) return self:GetDisplay("icon", e) end function CLSCORE:TimeForEvent(e) local t = e.t - self.StartTime if t >= 0 then return string.FormattedTime(t, "%02i:%02i") else return " " end end -- Tell CLSCORE how to display an event. See cl_scoring_events for examples. -- Pass an empty table to keep an event from showing up. function CLSCORE.DeclareEventDisplay(event_id, event_fns) -- basic input vetting, can't check returned value types because the -- functions may be impure if not tonumber(event_id) then Error("Event ??? display: invalid event id\n") end if (not event_fns) or type(event_fns) != "table" then Error(Format("Event %d display: no display functions found.\n", event_id)) end if not event_fns.text then Error(Format("Event %d display: no text display function found.\n", event_id)) end if not event_fns.icon then Error(Format("Event %d display: no icon and tooltip display function found.\n", event_id)) end CLSCORE.EventDisplay[event_id] = event_fns end function CLSCORE:FillDList(dlst) for k, e in pairs(self.Events) do local etxt = self:TextForEvent(e) local eicon, ttip = self:IconForEvent(e) local etime = self:TimeForEvent(e) if etxt then if eicon then local mat = eicon eicon = vgui.Create("DImage") eicon:SetMaterial(mat) eicon:SetTooltip(ttip) eicon:SetKeepAspect(true) eicon:SizeToContents() end dlst:AddLine(etime, eicon, " " .. etxt) end end end function CLSCORE:BuildEventLogPanel(dpanel) local margin = 10 local w, h = dpanel:GetSize() local dlist = vgui.Create("DListView", dpanel) dlist:SetPos(0, 2) dlist:SetSize(w, h - 25) dlist:SetSortable(true) dlist:SetMultiSelect(false) local timecol = dlist:AddColumn(T("col_time")) local iconcol = dlist:AddColumn("") local eventcol = dlist:AddColumn(T("col_event")) iconcol:SetFixedWidth(16) timecol:SetFixedWidth(40) -- If sortable is off, no background is drawn for the headers which looks -- terrible. So enable it, but disable the actual use of sorting. iconcol.Header:SetDisabled(true) timecol.Header:SetDisabled(true) eventcol.Header:SetDisabled(true) self:FillDList(dlist) end function CLSCORE:BuildScorePanel(dpanel) local margin = 10 local w, h = dpanel:GetSize() local dlist = vgui.Create("DListView", dpanel) dlist:SetPos(0, 0) dlist:SetSize(w, h) dlist:SetSortable(true) dlist:SetMultiSelect(false) local colnames = {"", "col_player", "col_role", "col_kills1", "col_kills2", "col_points", "col_team", "col_total"} for k, name in pairs(colnames) do if name == "" then -- skull icon column local c = dlist:AddColumn("") c:SetFixedWidth(22) else dlist:AddColumn(T(name)) end end -- the type of win condition triggered is relevant for team bonus local wintype = WIN_NONE for i=#self.Events, 1, -1 do local e = self.Events[i] if e.id == EVENT_FINISH then wintype = e.win break end end local scores = self.Scores local nicks = self.Players local bonus = ScoreTeamBonus(scores, wintype) for id, s in pairs(scores) do if id != -1 then local was_traitor = s.was_traitor local role = was_traitor and T("traitor") or (s.was_detective and T("detective") or "") local surv = "" if s.deaths > 0 then surv = vgui.Create("DImage", dlist) surv:SetMaterial(skull_icon) surv:SetTooltip("Dead") surv:SetKeepAspect(true) surv:SetSize(16,16) end local points_own = KillsToPoints(s, was_traitor) local points_team = (was_traitor and bonus.traitors or bonus.innos) local points_total = points_own + points_team local l = dlist:AddLine(surv, nicks[id], role, s.innos, s.traitors, points_own, points_team, points_total) -- center align for k, col in pairs(l.Columns) do col:SetContentAlignment(5) end -- when sorting on the column showing survival, we would get an error -- because images can't be sorted, so instead hack in a dummy value local surv_col = l.Columns[1] if surv_col then surv_col.Value = type(surv_col.Value) == "Panel" and "1" or "0" end end end dlist:SortByColumn(6) end function CLSCORE:AddAward(y, pw, award, dpanel) local nick = award.nick local text = award.text local title = string.upper(award.title) local titlelbl = vgui.Create("Label", dpanel) titlelbl:SetText(title) titlelbl:SetFont("TabLarge") titlelbl:SizeToContents() local tiw, tih = titlelbl:GetSize() local nicklbl = vgui.Create("Label", dpanel) nicklbl:SetText(nick) nicklbl:SetFont("DefaultBold") nicklbl:SizeToContents() local nw, nh = nicklbl:GetSize() local txtlbl = vgui.Create("Label", dpanel) txtlbl:SetText(text) txtlbl:SetFont("Default") txtlbl:SizeToContents() local tw, th = txtlbl:GetSize() titlelbl:SetPos((pw - tiw) / 2, y) y = y + tih + 2 local fw = nw + tw + 5 local fx = ((pw - fw) / 2) nicklbl:SetPos(fx, y) txtlbl:SetPos(fx + nw + 5, y) y = y + nh return y end -- double check that we have no nils local function ValidAward(a) return a and a.nick and a.text and a.title and a.priority end local wintitle = { [WIN_TRAITOR] = {txt = "hilite_win_traitors", c = Color(190, 5, 5, 255)}, [WIN_INNOCENT] = {txt = "hilite_win_innocent", c = Color(5, 190, 5, 255)} } function CLSCORE:BuildHilitePanel(dpanel) local w, h = dpanel:GetSize() local title = wintitle[WIN_INNOCENT] local endtime = self.StartTime for i=#self.Events, 1, -1 do local e = self.Events[i] if e.id == EVENT_FINISH then endtime = e.t -- when win is due to timeout, innocents win local wintype = e.win if wintype == WIN_TIMELIMIT then wintype = WIN_INNOCENT end title = wintitle[wintype] break end end local roundtime = endtime - self.StartTime local numply = table.Count(self.Players) local numtr = table.Count(self.TraitorIDs) local bg = vgui.Create("DColouredBox", dpanel) bg:SetColor(Color(50, 50, 50, 255)) bg:SetSize(w,h) bg:SetPos(0,0) local winlbl = vgui.Create("DLabel", dpanel) winlbl:SetFont("WinHuge") winlbl:SetText( T(title.txt) ) winlbl:SetTextColor(COLOR_WHITE) winlbl:SizeToContents() local xwin = (w - winlbl:GetWide())/2 local ywin = 30 winlbl:SetPos(xwin, ywin) bg.PaintOver = function() draw.RoundedBox(8, xwin - 15, ywin - 5, winlbl:GetWide() + 30, winlbl:GetTall() + 10, title.c) end local ysubwin = ywin + winlbl:GetTall() local partlbl = vgui.Create("DLabel", dpanel) local plytxt = PT(numtr == 1 and "hilite_players2" or "hilite_players1", {numplayers = numply, numtraitors = numtr}) partlbl:SetText(plytxt) partlbl:SizeToContents() partlbl:SetPos(xwin, ysubwin + 8) local timelbl = vgui.Create("DLabel", dpanel) timelbl:SetText(PT("hilite_duration", {time= string.FormattedTime(roundtime, "%02i:%02i")})) timelbl:SizeToContents() timelbl:SetPos(xwin + winlbl:GetWide() - timelbl:GetWide(), ysubwin + 8) -- Awards local wa = math.Round(w * 0.9) local ha = h - ysubwin - 40 local xa = (w - wa) / 2 local ya = h - ha local awardp = vgui.Create("DPanel", dpanel) awardp:SetSize(wa, ha) awardp:SetPos(xa, ya) awardp:SetPaintBackground(false) -- Before we pick awards, seed the rng in a way that is the same on all -- clients. We can do this using the round start time. To make it a bit more -- random, involve the round's duration too. math.randomseed(self.StartTime + endtime) -- Attempt to generate every award, then sort the succeeded ones based on -- priority/interestingness local award_choices = {} for k, afn in pairs(AWARDS) do local a = afn(self.Events, self.Scores, self.Players, self.TraitorIDs, self.DetectiveIDs) if ValidAward(a) then table.insert(award_choices, a) end end local num_choices = table.Count(award_choices) local max_awards = 5 -- sort descending by priority table.SortByMember(award_choices, "priority") -- put the N most interesting awards in the menu for i=1,max_awards do local a = award_choices[i] if a then self:AddAward((i - 1) * 42, wa, a, awardp) end end end function CLSCORE:ShowPanel() local margin = 15 local dpanel = vgui.Create("DFrame") local w, h = 700, 500 dpanel:SetSize(700, 500) dpanel:Center() dpanel:SetTitle(T("report_title")) dpanel:SetVisible(true) dpanel:ShowCloseButton(true) dpanel:SetMouseInputEnabled(true) dpanel:SetKeyboardInputEnabled(true) dpanel.OnKeyCodePressed = util.BasicKeyHandler -- keep it around so we can reopen easily dpanel:SetDeleteOnClose(false) self.Panel = dpanel local dbut = vgui.Create("DButton", dpanel) local bw, bh = 100, 25 dbut:SetSize(bw, bh) dbut:SetPos(w - bw - margin, h - bh - margin/2) dbut:SetText(T("close")) dbut.DoClick = function() dpanel:Close() end local dsave = vgui.Create("DButton", dpanel) dsave:SetSize(bw,bh) dsave:SetPos(margin, h - bh - margin/2) dsave:SetText(T("report_save")) dsave:SetTooltip(T("report_save_tip")) dsave:SetConsoleCommand("ttt_save_events") local dtabsheet = vgui.Create("DPropertySheet", dpanel) dtabsheet:SetPos(margin, margin + 15) dtabsheet:SetSize(w - margin*2, h - margin*3 - bh) local padding = dtabsheet:GetPadding() -- Highlight tab local dtabhilite = vgui.Create("DPanel", dtabsheet) dtabhilite:SetPaintBackground(false) dtabhilite:StretchToParent(padding,padding,padding,padding) self:BuildHilitePanel(dtabhilite) dtabsheet:AddSheet(T("report_tab_hilite"), dtabhilite, "gui/silkicons/star", false, false, T("report_tab_hilite_tip")) -- Event log tab local dtabevents = vgui.Create("DPanel", dtabsheet) -- dtab1:SetSize(650, 450) dtabevents:StretchToParent(padding, padding, padding, padding) self:BuildEventLogPanel(dtabevents) dtabsheet:AddSheet(T("report_tab_events"), dtabevents, "gui/silkicons/application_view_detail", false, false, T("report_tab_events_tip")) -- Score tab local dtabscores = vgui.Create("DPanel", dtabsheet) dtabscores:SetPaintBackground(false) dtabscores:StretchToParent(padding, padding, padding, padding) self:BuildScorePanel(dtabscores) dtabsheet:AddSheet(T("report_tab_scores"), dtabscores, "gui/silkicons/user", false, false, T("report_tab_scores_tip")) dpanel:MakePopup() -- makepopup grabs keyboard, whereas we only need mouse dpanel:SetKeyboardInputEnabled(false) end function CLSCORE:ClearPanel() if self.Panel then -- move the mouse off any tooltips and then remove the panel next tick -- we need this hack as opposed to just calling Remove because gmod does -- not offer a means of killing the tooltip, and doesn't clean it up -- properly on Remove gui.SetMousePos( ScrW()/2, ScrH()/2 ) timer.Simple(0, self.Panel.Remove, self.Panel) end end function CLSCORE:SaveLog() if self.Events and #self.Events <= 0 then chat.AddText(COLOR_WHITE, T("report_save_error")) return end local logname = "ttt/logs/ttt_events_" .. os.time() .. ".txt" local log = "Trouble in Terrorist Town - Round Events Log\n".. string.rep("-", 50) .."\n" log = log .. string.format("%s | %-25s | %s\n", " TIME", "TYPE", "WHAT HAPPENED") .. string.rep("-", 50) .."\n" for _, e in pairs(self.Events) do local etxt = self:TextForEvent(e) local etime = self:TimeForEvent(e) local _, etype = self:IconForEvent(e) if etxt then log = log .. string.format("%s | %-25s | %s\n", etime, etype, etxt) end end file.Write(logname, log) chat.AddText(COLOR_WHITE, T("report_save_result"), COLOR_GREEN, " /garrysmod/data/" .. logname) end function CLSCORE:Reset() self.Events = {} --self.StoredEvents = nil self.TraitorIDs = {} self.DetectiveIDs = {} self.Scores = {} self.Players = {} self.RoundStarted = 0 self:ClearPanel() end function CLSCORE:Init(events) -- Get start time and traitors local starttime = nil local traitors = nil local detectives = nil for k, e in pairs(events) do if e.id == EVENT_GAME and e.state == ROUND_ACTIVE then starttime = e.t elseif e.id == EVENT_SELECTED then traitors = e.traitor_ids detectives = e.detective_ids end if starttime and traitors then break end end -- Get scores and players local scores = {} local nicks = {} for k, e in pairs(events) do if e.id == EVENT_SPAWN then scores[e.uid] = ScoreInit() nicks[e.uid] = e.ni end end scores = ScoreEventLog(events, scores, traitors, detectives) self.Players = nicks self.Scores = scores self.TraitorIDs = traitors self.DetectiveIDs = detectives self.StartTime = starttime self.Events = events end function CLSCORE:ReportEvents(events) self:Reset() -- if events != self.StoredEvents then -- self.StoredEvents = table.Copy(events) -- end self:Init(events) self:ShowPanel() end function CLSCORE:Reopen() if self.Panel and self.Panel:IsValid() and not self.Panel:IsVisible() then self.Panel:SetVisible(true) end end local buff = "" --evdebug = nil local function ReceiveReportStream(um) -- Msg("Receiving message... \n") local cont = um:ReadBool() buff = buff .. um:ReadString() if cont then return else -- do stuff with buffer contents local status, results = pcall(glon.decode, buff) if status then CLSCORE:ReportEvents(results) else ErrorNoHalt("Round report event encoding returned an error: " .. results .. "\n") end -- flush buff = "" end end usermessage.Hook("report_stream", ReceiveReportStream) local function SaveLog(ply, cmd, args) CLSCORE:SaveLog() end concommand.Add("ttt_save_events", SaveLog)