MASK_ATTACK_TRACE <- Constants.FContents.CONTENTS_SOLID | Constants.FContents.CONTENTS_PLAYERCLIP | Constants.FContents.CONTENTS_WINDOW | Constants.FContents.CONTENTS_GRATE; ::Log <- function(msg) { local time = Time(); printl(format("[timer][%.2f] | %s", time, msg)); } ::PrintToPlayer <- function(ply, msg) { ClientPrint(ply, Constants.EHudNotify.HUD_PRINTTALK, msg); } ::GetPlayerName <- function(ply) { return NetProps.GetPropString(ply, "m_szNetname"); } ::GetZoneFileName <- function() { return "zones_" + GetMapName(); } ::FormatTime <- function(ticks) { local seconds = ticks * 3 / 200.0; local minutes = floor(seconds / 60); seconds %= 60; if (minutes >= 60) { local hours = floor(minutes / 60); minutes %= 60; return format("%02d:%02d:%06.3f", hours, minutes, seconds); } else { return format("%02d:%06.3f", minutes, seconds); } } class Zone { // aabb min = Vector(); max = Vector(); // bottom vertices b1 = Vector(); b2 = Vector(); b3 = Vector(); b4 = Vector(); // top vertices t1 = Vector(); t2 = Vector(); t3 = Vector(); t4 = Vector(); constructor(min, max) { this.min = min; this.max = max; this.b1 = Vector(min.x, min.y, min.z); this.b2 = Vector(min.x, max.y, min.z); this.b3 = Vector(max.x, max.y, min.z); this.b4 = Vector(max.x, min.y, min.z); this.t1 = Vector(min.x, min.y, max.z); this.t2 = Vector(min.x, max.y, max.z); this.t3 = Vector(max.x, max.y, max.z); this.t4 = Vector(max.x, min.y, max.z); } function Draw(duration) { // bottom DebugDrawLine(this.b1, this.b2, 0, 255, 0, true, duration); DebugDrawLine(this.b2, this.b3, 0, 255, 0, true, duration); DebugDrawLine(this.b3, this.b4, 0, 255, 0, true, duration); DebugDrawLine(this.b4, this.b1, 0, 255, 0, true, duration); // top DebugDrawLine(this.t1, this.t2, 0, 255, 0, true, duration); DebugDrawLine(this.t2, this.t3, 0, 255, 0, true, duration); DebugDrawLine(this.t3, this.t4, 0, 255, 0, true, duration); DebugDrawLine(this.t4, this.t1, 0, 255, 0, true, duration); // sides DebugDrawLine(this.b1, this.t1, 0, 255, 0, true, duration); DebugDrawLine(this.b2, this.t2, 0, 255, 0, true, duration); DebugDrawLine(this.b3, this.t3, 0, 255, 0, true, duration); DebugDrawLine(this.b4, this.t4, 0, 255, 0, true, duration); } // Returns true if any part of entity bounding box intersects with zone function Clips(entity) { local entOrigin = entity.GetOrigin(); local entMin = entOrigin + entity.GetBoundingMins(); local entMax = entOrigin + entity.GetBoundingMaxs(); local xClip = entMin.x < this.max.x && entMax.x > this.min.x; local yClip = entMin.y < this.max.y && entMax.y > this.min.y; local zClip = entMin.z < this.max.z && entMax.z > this.min.z; return xClip && yClip && zClip; } function Print() { Log(format("min: [%f, %f, %f]", this.min.x, this.min.y, this.min.z)); Log(format("max: [%f, %f, %f]", this.max.x, this.max.y, this.max.z)); } // Serialize zone into space separated floats. function ToString() { return format( "%f %f %f %f %f %f", this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z ); } // Deserialize zone from a string of 6 floats. function FromString(str) { local parts = split(str, " "); if (parts.len() != 6) { return null; } local min = Vector(); min.x = parts[0].tofloat(); min.y = parts[1].tofloat(); min.z = parts[2].tofloat(); local max = Vector(); max.x = parts[3].tofloat(); max.y = parts[4].tofloat(); max.z = parts[5].tofloat(); return Zone(min, max); } } class ZoneBuilder { points = []; constructor() { } // Pick a point for the new zone. // Should be called 3 times in total. function PickPoint(point) { local num_points = this.points.len(); if (num_points == 1) { // Only care about horizontal area point.z = this.points[0].z; } else if (num_points == 2) { // Only care about the height point.x = this.points[0].x; point.y = this.points[0].y; } Log(format("Adding point [%f, %f, %f]", point.x, point.y, point.z)); this.points.append(point); if (num_points == 0) { PrintToPlayer(null, "Pick second horizontal point"); return null; } else if (num_points == 1) { PrintToPlayer(null, "Pick the height"); return null; } // Create the zone local min = Vector(this.points[0].x, this.points[0].y, this.points[0].z); local max = Vector(this.points[0].x, this.points[0].y, this.points[0].z); for (local i = 1; i < 3; i++) { if (this.points[i].x < min.x) min.x = points[i].x; if (this.points[i].y < min.y) min.y = points[i].y; if (this.points[i].z < min.z) min.z = points[i].z; if (this.points[i].x > max.x) max.x = points[i].x; if (this.points[i].y > max.y) max.y = points[i].y; if (this.points[i].z > max.z) max.z = points[i].z; } this.points = []; return Zone(min, max); } // Draw the zone we are building, // including potential point we are going to pick. function Draw(point) { local num_points = this.points.len(); if (num_points <= 0) { return; } local min = Vector(this.points[0].x, this.points[0].y, this.points[0].z); local max = Vector(this.points[0].x, this.points[0].y, this.points[0].z); // Adding a horizontal point? if (num_points == 1) { if (point.x < min.x) min.x = point.x; if (point.y < min.y) min.y = point.y; if (point.x > max.x) max.x = point.x; if (point.y > max.y) max.y = point.y; } // Adding height? else if (num_points == 2) { if (this.points[1].x < min.x) min.x = this.points[1].x; if (this.points[1].y < min.y) min.y = this.points[1].y; if (this.points[1].x > max.x) max.x = this.points[1].x; if (this.points[1].y > max.y) max.y = this.points[1].y; if (point.z < min.z) min.z = point.z; if (point.z > max.z) max.z = point.z; } local temp_zone = Zone(min, max); temp_zone.Draw(0.1); } function IsBuilding() { return this.points.len() > 0; } function GetPlayerAimPoint(ply) { local vecStart = ply.EyePosition(); local vecEnd = vecStart + ply.EyeAngles().Forward() * 2048.0; local tr = { "start": vecStart, "end": vecEnd, "mask": MASK_ATTACK_TRACE, "ignore": ply }; if (TraceLineEx(tr)) { return tr["endpos"]; } return vecEnd; } } class PlayerState { prev_zone_index = -1; start_tick = -1; constructor() { } } class Timer { zones = []; players = {}; tick_count = 0; constructor() { } function AddZone(zone) { zone.Draw(5.0); zones.append(zone); if (zones.len() > 1) { PrintToPlayer(null, "Zone " + (this.zones.len() - 1) + " added"); } else { PrintToPlayer(null, "Starting zone added"); } } function ClearZones() { this.zones = []; PrintToPlayer(null, "Zones cleared"); } function RemoveZone() { local zone_num = this.zones.len(); if (zone_num <= 0) { PrintToPlayer(null, "No zones to remove"); return; } this.zones.pop(); if (zone_num == 1) { PrintToPlayer(null, "Removed the start zone"); } else { PrintToPlayer(null, "Removed zone " + zone_num); } } function SaveZones() { if (this.zones.len() <= 0) { PrintToPlayer(null, "No zones to save"); return; } local zone_str = ""; for (local i = 0; i < this.zones.len(); i++) { zone_str += this.zones[i].ToString(); if (i < this.zones.len() - 1) { zone_str += "\n"; } } local file_name = GetZoneFileName(); StringToFile(file_name, zone_str); PrintToPlayer(null, format("Saved %d zones to '%s'", this.zones.len(), file_name)); } function LoadZones() { local file_name = GetZoneFileName(); local zone_str = FileToString(file_name); if (zone_str == null) { PrintToPlayer(null, format("Could not load zones from '%s'", file_name)); Log("No file"); return; } local lines = split(zone_str, "\n"); Log(format("Found %d lines in %s", lines.len(), file_name)); this.zones = []; foreach (line in lines) { local zone = Zone.FromString(line); if (zone == null) { PrintToPlayer(null, format("Could not load zones from '%s'", file_name)); Log(format("Malformed line: %s", line)); this.zones = []; return; } this.zones.append(zone); } PrintToPlayer(null, format("Loaded %d zones from '%s'", this.zones.len(), file_name)); DrawZones(); } function Tick() { this.tick_count++; local ply = null; while (ply = Entities.FindByClassname(ply, "player")) { local entindex = ply.entindex(); // Dead or spectator? if (!ply.IsAlive() || (ply.GetTeam() != Constants.ETFTeam.TF_TEAM_RED && ply.GetTeam() != Constants.ETFTeam.TF_TEAM_BLUE) ) { if (entindex in this.players) { delete this.players[entindex]; } continue; } // New player to track? if (!(entindex in this.players)) { this.players[entindex] <- PlayerState(); } if (this.zones.len() <= 0) { continue; } local prev_zone_index = this.players[entindex].prev_zone_index; local start_tick = this.players[entindex].start_tick; if (prev_zone_index < 0) { // Not yet in start zone, only check if we are entering it. if (this.zones[0].Clips(ply)) { this.players[entindex].prev_zone_index = 0; Log("Entered the start zone"); PrintToPlayer(ply, "Entered the start zone"); } } else if (prev_zone_index == 0 && start_tick < 0) { // In start zone, check if leaving. if (!this.zones[0].Clips(ply)) { this.players[entindex].start_tick = this.tick_count; Log("Left the start zone"); PrintToPlayer(ply, "Run started"); } } else { // Run ongoing. // Check if re-entering the start zone. if (this.zones[0].Clips(ply)) { this.players[entindex].prev_zone_index = 0; this.players[entindex].start_tick = -1; Log("Entered the start zone"); PrintToPlayer(ply, "Entered the start zone"); continue; } // Check if entering any later zone. // We allow skipping zones on purpose here. for (local i = prev_zone_index + 1; i < this.zones.len(); i++) { if (this.zones[i].Clips(ply)) { local run_ticks = this.tick_count - start_tick; local run_time = FormatTime(run_ticks); if (i == this.zones.len() - 1) { // Entered the final zone. this.players[entindex].prev_zone_index = -1; this.players[entindex].start_tick = -1; Log(format("Finished run in %s (%d ticks)", run_time, run_ticks)); PrintToPlayer( null, format( "\x03%s\x01 finished a run in \x04%s\x01 (%d ticks)", GetPlayerName(ply), run_time, run_ticks ) ); } else { this.players[entindex].prev_zone_index = i; Log(format("Entered zone %d at %s (%d ticks)", i, run_time, run_ticks)); PrintToPlayer(ply, format("Entered zone \x03%d\x01 at \x04%s\x01 (%d ticks)", i, run_time, run_ticks)); } break; } } } } } } // -------------------------------- // Commands // -------------------------------- // Build a new zone! // You should call this 3 times to pick the bounds. ::CreateZone <- function() { if ("zone_builder" in getroottable()) { local ply = GetListenServerHost(); local zone = ::zone_builder.PickPoint(::zone_builder.GetPlayerAimPoint(ply)); if (zone != null) { ::cool_timer.AddZone(zone); } } } // Draw all created zones. ::DrawZones <- function() { if ("cool_timer" in getroottable()) { foreach (zone in ::cool_timer.zones) { zone.Draw(10.0); } } } // Remove the last zone. ::RemoveZone <- function() { if ("cool_timer" in getroottable()) { ::cool_timer.RemoveZone(); } } // Remove all the zones. ::ClearZones <- function() { if ("cool_timer" in getroottable()) { ::cool_timer.ClearZones(); } } // Debug print zone bounds to console. ::DumpZones <- function() { if ("cool_timer" in getroottable()) { local num_zones = ::cool_timer.zones.len(); for (local i = 0; i < num_zones; i++) { Log("Zone " + i); ::cool_timer.zones[i].Print(); Log(""); } } } // Save zones to a text file in scriptdata folder. ::SaveZones <- function() { if ("cool_timer" in getroottable()) { ::cool_timer.SaveZones(); } } // Load zones from a text file in scriptdata folder. ::LoadZones <- function() { if ("cool_timer" in getroottable()) { ::cool_timer.LoadZones(); } } // -------------------------------- // Hooks // -------------------------------- function TimerThink() { if ("cool_timer" in getroottable()) { ::cool_timer.Tick(); } if ("zone_builder" in getroottable()) { if (::zone_builder.IsBuilding()) { local ply = GetListenServerHost(); ::zone_builder.Draw(::zone_builder.GetPlayerAimPoint(ply)); } } return -1; // Think every tick } if (!("cool_timer" in getroottable())) { ::cool_timer <- Timer(); ::zone_builder <- ZoneBuilder(); if (!("TIMER_HOOKED_EVENTS" in getroottable())) { Log("Hooking game events"); __CollectGameEventCallbacks(this); ::TIMER_HOOKED_EVENTS <- true; } local thinker = SpawnEntityFromTable("info_target", { targetname = "cool_timer_thinker" } ); if (thinker.ValidateScriptScope()) { Log("Creating thinker"); thinker.GetScriptScope()["TimerThink"] <- TimerThink; AddThinkToEnt(thinker, "TimerThink"); } }