From 1a9862358afb4742c43a6d6bc834514bc3eb1529 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 12 Jul 2026 07:46:21 -0400 Subject: [PATCH 01/30] Ported "Correction of the calculation that was incorrect." from base TTT --- RELEASE.md | 6 ++++++ gamemodes/terrortown/gamemode/cl_radar.lua | 10 ++++++---- gamemodes/terrortown/gamemode/shared.lua | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 3a15fee14..047fc97a9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,11 @@ # Release Notes +## 2.5.1 +**Released:** + +### Fixes +- Ported "Correction of the calculation that was incorrect." from base TTT + ## 2.5.0 **Released: June 30th, 2026**\ Includes beta updates [2.4.2](#242-beta) to [2.4.7](#247-beta). diff --git a/gamemodes/terrortown/gamemode/cl_radar.lua b/gamemodes/terrortown/gamemode/cl_radar.lua index 44c87dc34..cd283512d 100644 --- a/gamemodes/terrortown/gamemode/cl_radar.lua +++ b/gamemodes/terrortown/gamemode/cl_radar.lua @@ -147,7 +147,8 @@ local tele_mark = surface.GetTextureID("vgui/ttt/tele_mark") local GetPTranslation = LANG.GetParamTranslation local FormatTime = util.SimpleTime -local near_cursor_dist = 32400 +local near_cursor_dist = 180 +local near_cursor_distsqrt = near_cursor_dist * near_cursor_dist function RADAR:Draw(client) if not client then return end @@ -221,14 +222,15 @@ function RADAR:Draw(client) local glitchMode = GetConVar("ttt_glitch_mode"):GetInt() local beggarMode = GetConVar("ttt_beggar_reveal_traitor"):GetInt() local bodysnatcherMode = GetConVar("ttt_bodysnatcher_reveal_traitor"):GetInt() - local role, alpha, scrpos, md + local role, alpha, scrpos for _, tgt in pairs(RADAR.targets) do alpha = alpha_base scrpos = tgt.pos:ToScreen() if scrpos.visible then - md = mpos:DistToSqr(Vector(scrpos.x, scrpos.y, 0)) - if md < near_cursor_dist then + local md_sqrt = mpos:DistToSqr(Vector(scrpos.x, scrpos.y, 0)) + if md_sqrt < near_cursor_distsqrt then + local md = math.sqrt(md_sqrt) alpha = math.Clamp(alpha * (md / near_cursor_dist), 40, 230) end diff --git a/gamemodes/terrortown/gamemode/shared.lua b/gamemodes/terrortown/gamemode/shared.lua index 55c44897d..738908d52 100644 --- a/gamemodes/terrortown/gamemode/shared.lua +++ b/gamemodes/terrortown/gamemode/shared.lua @@ -30,7 +30,7 @@ local TableHasValue = table.HasValue include("player_class/player_ttt.lua") -- Version string for display and function for version checks -CR_VERSION = "2.5.0" +CR_VERSION = "2.5.1" CR_BETA = true CR_WORKSHOP_ID = CR_BETA and "2404251054" or "2421039084" From 970c07343afbda45afaf44a5e6a5be301e6de76a Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 12 Jul 2026 07:48:50 -0400 Subject: [PATCH 02/30] Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT --- RELEASE.md | 1 + gamemodes/terrortown/gamemode/cl_search.lua | 2 +- gamemodes/terrortown/gamemode/cl_voice.lua | 2 +- gamemodes/terrortown/gamemode/gamemsg.lua | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 047fc97a9..f3be7b9b7 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,6 +5,7 @@ ### Fixes - Ported "Correction of the calculation that was incorrect." from base TTT +- Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT ## 2.5.0 **Released: June 30th, 2026**\ diff --git a/gamemodes/terrortown/gamemode/cl_search.lua b/gamemodes/terrortown/gamemode/cl_search.lua index 8f3568e39..105410287 100644 --- a/gamemodes/terrortown/gamemode/cl_search.lua +++ b/gamemodes/terrortown/gamemode/cl_search.lua @@ -188,7 +188,7 @@ function PreprocSearch(raw) elseif t == "words" then if #d > 0 then -- only append "--" if there's no ending interpunction - local final = string.match(d, "[\\.\\!\\?]$") ~= nil + local final = string.match(d, "[.!?]$") ~= nil search[t].text = PT("search_words", { lastwords = d .. (final and "" or "--.") }) search[t].p = 16 end diff --git a/gamemodes/terrortown/gamemode/cl_voice.lua b/gamemodes/terrortown/gamemode/cl_voice.lua index ab84cd6c2..3c4eb48df 100644 --- a/gamemodes/terrortown/gamemode/cl_voice.lua +++ b/gamemodes/terrortown/gamemode/cl_voice.lua @@ -46,7 +46,7 @@ local function LastWordsRecv() local death_type = net.ReadUInt(2) -- only append "--" if there's no ending interpunction - local final = string.match(words, "[\\.\\!\\?]$") ~= nil + local final = string.match(words, "[.!?]$") ~= nil local lastWordsStr = words .. (final and " " or "-- ") -- add optional context relating to death type diff --git a/gamemodes/terrortown/gamemode/gamemsg.lua b/gamemodes/terrortown/gamemode/gamemsg.lua index 3e94a4351..abe17768e 100644 --- a/gamemodes/terrortown/gamemode/gamemsg.lua +++ b/gamemodes/terrortown/gamemode/gamemsg.lua @@ -428,7 +428,7 @@ local LastWordContext = { local function LastWordsMsg(ply, words) -- only append "--" if there's no ending interpunction - local final = string.match(words, "[\\.\\!\\?]$") ~= nil + local final = string.match(words, "[.!?]$") ~= nil -- add optional context relating to death type local death_type = ply.death_type From 7149920c1a43122c13fd9e2bcbcf9da386a6af26 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 12 Jul 2026 07:55:31 -0400 Subject: [PATCH 03/30] Ported "Some TTT cleanups" from base TTT --- RELEASE.md | 3 +++ gamemodes/terrortown/gamemode/init.lua | 2 +- gamemodes/terrortown/gamemode/vgui/sb_info.lua | 3 ++- gamemodes/terrortown/gamemode/weaponry.lua | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index f3be7b9b7..a7c8e3e3e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,6 +3,9 @@ ## 2.5.1 **Released:** +### Changes +- Ported "Some TTT cleanups" from base TTT + ### Fixes - Ported "Correction of the calculation that was incorrect." from base TTT - Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT diff --git a/gamemodes/terrortown/gamemode/init.lua b/gamemodes/terrortown/gamemode/init.lua index d0108489e..b99323d76 100644 --- a/gamemodes/terrortown/gamemode/init.lua +++ b/gamemodes/terrortown/gamemode/init.lua @@ -902,7 +902,7 @@ function BeginRound() ROLE_STARTING_TEAM[role] = player.GetRoleTeam(role, false) end - ents.TTT.TriggerRoundStateOutputs(ROUND_BEGIN) + ents.TTT.TriggerRoundStateOutputs(ROUND_ACTIVE) end function PrintResultMessage(type) diff --git a/gamemodes/terrortown/gamemode/vgui/sb_info.lua b/gamemodes/terrortown/gamemode/vgui/sb_info.lua index 3789b0fb5..a6e5b65cb 100644 --- a/gamemodes/terrortown/gamemode/vgui/sb_info.lua +++ b/gamemodes/terrortown/gamemode/vgui/sb_info.lua @@ -256,9 +256,10 @@ function PANEL:DoClick() end end +local select_color = Color(255, 200, 0, 255) function PANEL:PaintOver() if self.Player and self.Player.sb_tag == self.Tag then - surface.SetDrawColor(255,200,0,255) + surface.SetDrawColor(select_color) surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall()) end end diff --git a/gamemodes/terrortown/gamemode/weaponry.lua b/gamemodes/terrortown/gamemode/weaponry.lua index abe5e5452..47b47b90f 100644 --- a/gamemodes/terrortown/gamemode/weaponry.lua +++ b/gamemodes/terrortown/gamemode/weaponry.lua @@ -166,7 +166,7 @@ end local Hattables = { "phoenix.mdl", "arctic.mdl", "Group01", "monk.mdl" } local function CanWearHat(ply) local path = string.Explode("/", ply:GetModel()) - if #path == 1 then path = string.Explode("\\", path) end + if #path == 1 then path = string.Explode("\\", path[1]) end return table.HasValue(Hattables, path[3]) end From 8b0349ffb530b8f1096336194876c67ad95887ae Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 12 Jul 2026 07:56:26 -0400 Subject: [PATCH 04/30] Ported "Fix TTT state and report chunking" from base TTT --- RELEASE.md | 1 + .../entities/entities/ttt_health_station.lua | 7 ++++--- .../entities/weapons/weapon_ttt_wtester.lua | 1 + gamemodes/terrortown/gamemode/scoring.lua | 12 ++++++------ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index a7c8e3e3e..19a60941e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -9,6 +9,7 @@ ### Fixes - Ported "Correction of the calculation that was incorrect." from base TTT - Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT +- Ported "Fix TTT state and report chunking" from base TTT ## 2.5.0 **Released: June 30th, 2026**\ diff --git a/gamemodes/terrortown/entities/entities/ttt_health_station.lua b/gamemodes/terrortown/entities/entities/ttt_health_station.lua index a5471940c..8e7050867 100644 --- a/gamemodes/terrortown/entities/entities/ttt_health_station.lua +++ b/gamemodes/terrortown/entities/entities/ttt_health_station.lua @@ -19,6 +19,7 @@ ENT.RechargeRate = 1 ENT.RechargeFreq = 2 -- in seconds ENT.NextHeal = 0 +ENT.NextCharge = 0 ENT.HealRate = 1 ENT.HealFreq = 0.2 @@ -71,6 +72,7 @@ function ENT:Initialize() self:SetPlacer(nil) self.NextHeal = 0 + self.NextCharge = 0 self.fingerprints = {} @@ -166,12 +168,11 @@ end if SERVER then -- recharge - local nextcharge = 0 function ENT:Think() - if nextcharge < CurTime() then + if self.NextCharge < CurTime() then self:AddToStorage(self.RechargeRate) - nextcharge = CurTime() + self.RechargeFreq + self.NextCharge = CurTime() + self.RechargeFreq end end diff --git a/gamemodes/terrortown/entities/weapons/weapon_ttt_wtester.lua b/gamemodes/terrortown/entities/weapons/weapon_ttt_wtester.lua index ca1324de5..8e1b61933 100644 --- a/gamemodes/terrortown/entities/weapons/weapon_ttt_wtester.lua +++ b/gamemodes/terrortown/entities/weapons/weapon_ttt_wtester.lua @@ -107,6 +107,7 @@ function SWEP:SetupDataTables() end function SWEP:Initialize() + self.ItemSamples = {} self:SetCharge(MAX_CHARGE) self:SetLastScanned(-1) diff --git a/gamemodes/terrortown/gamemode/scoring.lua b/gamemodes/terrortown/gamemode/scoring.lua index f667ab706..79b1b7f82 100644 --- a/gamemodes/terrortown/gamemode/scoring.lua +++ b/gamemodes/terrortown/gamemode/scoring.lua @@ -244,23 +244,23 @@ function SCORE:StreamToClients() if len <= MaxStreamLength then net.Start("TTT_ReportStream") - net.WriteUInt(len, 16) - net.WriteData(events, len) + net.WriteUInt(len, 16) + net.WriteData(events, len) net.Broadcast() else local curpos = 0 repeat net.Start("TTT_ReportStream_Part") - net.WriteData(StringSub(events, curpos + 1, curpos + MaxStreamLength + 1), MaxStreamLength) + net.WriteData(StringSub(events, curpos + 1, curpos + MaxStreamLength), MaxStreamLength) net.Broadcast() - curpos = curpos + MaxStreamLength + 1 + curpos = curpos + MaxStreamLength until (len - curpos <= MaxStreamLength) net.Start("TTT_ReportStream") - net.WriteUInt(len, 16) - net.WriteData(StringSub(events, curpos + 1, len), len - curpos) + net.WriteUInt(len - curpos, 16) + net.WriteData(StringSub(events, curpos + 1, len), len - curpos) net.Broadcast() end end From e90440ffcaf871caee44fbbb686f0ca32de291b8 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 12 Jul 2026 07:57:13 -0400 Subject: [PATCH 05/30] Ported "Minor cleanups" from base TTT --- RELEASE.md | 1 + gamemodes/terrortown/gamemode/scoring_shd.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 19a60941e..de5c139fe 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,6 +5,7 @@ ### Changes - Ported "Some TTT cleanups" from base TTT +- Ported "Minor cleanups" from base TTT ### Fixes - Ported "Correction of the calculation that was incorrect." from base TTT diff --git a/gamemodes/terrortown/gamemode/scoring_shd.lua b/gamemodes/terrortown/gamemode/scoring_shd.lua index 5d313af28..b84ec0b71 100644 --- a/gamemodes/terrortown/gamemode/scoring_shd.lua +++ b/gamemodes/terrortown/gamemode/scoring_shd.lua @@ -1,7 +1,7 @@ -- Server and client both need this for scoring event logs -- 2^16 bytes - 4 (header) - 2 (UInt length in TTT_ReportStream) - 1 (terminanting byte) -(SERVER and SCORE or CLSCORE).MaxStreamLength = 65529 +(SERVER and SCORE or CLSCORE).MaxStreamLength = 65000 local IsValid = IsValid local math = math From c54bfe85d5f8b24dd0ff13aa651632033777d72b Mon Sep 17 00:00:00 2001 From: Malivil Date: Mon, 13 Jul 2026 22:12:53 -0400 Subject: [PATCH 06/30] Removed invalid global --- .luacheckrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 2eb93112b..8e74de6f9 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -612,7 +612,6 @@ globals = { "ROLE_INNOCENT", "ROLE_TRAITOR", "ROUND_ACTIVE", - "ROUND_BEGIN", "ROUND_POST", "ROUND_PREP", "ROUND_WAIT", From cab9ea445faf9bf15f78eb0dbb8ba9d2cf7345fe Mon Sep 17 00:00:00 2001 From: Malivil Date: Tue, 14 Jul 2026 11:57:56 -0400 Subject: [PATCH 07/30] Changed .md relative links to be lowercase so they work in VSCode too --- API/COMMANDS.md | 2 +- CONVARS.md | 42 +++++++-------- CREATE_TASKMASTER_TASKS.md | 42 +++++++-------- CREATE_YOUR_OWN_ROLE.md | 104 ++++++++++++++++++------------------- CUSTOM_WEAPONS.md | 6 +-- README.md | 4 +- 6 files changed, 100 insertions(+), 100 deletions(-) diff --git a/API/COMMANDS.md b/API/COMMANDS.md index 7a4e48d09..5e8692261 100644 --- a/API/COMMANDS.md +++ b/API/COMMANDS.md @@ -15,7 +15,7 @@ Allows for viewing and editing which weapons are available in each role's shop.\ - *copy FROM TO [REPLACE]* - Duplicates a role configuration. If `true` is provided for the REPLACE parameter, any existing configuration will be removed *(Added in 2.1.10)* - *duplicate* - Alias for *copy* *(Added in 2.1.10)* - *list* - Prints the current configuration in the server console, highlighting anything invalid - - *open (aka show)* - Opens the configuration UI. See [this tutorial](../CONVARS.md#Configuration-by-UI) for how to use the UI. (This command is the default if no parameter is provided) + - *open (aka show)* - Opens the configuration UI. See [this tutorial](../CONVARS.md#configuration-by-ui) for how to use the UI. (This command is the default if no parameter is provided) - *print* - Alias for *list* - *reload* - Reloads the configurations from the server's filesystem - *show* - Alias for *open* diff --git a/CONVARS.md b/CONVARS.md index 39d80a6b6..f6cb8f00d 100644 --- a/CONVARS.md +++ b/CONVARS.md @@ -1,26 +1,26 @@ # Configurations ## Table of Contents -1. [Server Configurations](#Server-Configurations) -1. [Client Configurations](#Client-Configurations) -1. [Role Weapon Shop](#Role-Weapon-Shop) - 1. [Configuration by UI](#Configuration-by-UI) - 1. [Explanation](#Explanation) - 1. [Example](#Example) - 1. [Configuration by Files](#Configuration-by-Files) +1. [Server Configurations](#server-configurations) +1. [Client Configurations](#client-configurations) +1. [Role Weapon Shop](#role-weapon-shop) + 1. [Configuration by UI](#configuration-by-ui) + 1. [Explanation](#explanation) + 1. [Example](#example) + 1. [Configuration by Files](#configuration-by-files) 1. [Preparing a Role for Configuration](#preparing-a-role-for-configuration) - 1. [Weapons](#Weapons) - 1. [Adding Weapons](#Adding-Weapons) - 1. [Removing Weapons](#Removing-Weapons) - 1. [Bypassing Weapon Randomization](#Bypassing-Weapon-Randomization) - 1. [Adding Weapons to a Role's Loadout](#Adding-Weapons-to-a-Roles-Loadout) - 1. [Finding a Weapon's Class](#Finding-a-Weapons-Class) - 1. [Equipment](#Equipment) - 1. [Adding Equipment](#Adding-Equipment) - 1. [Removing Equipment](#Removing-Equipment) - 1. [Adding Equipment to a Role's Loadout](#Adding-Equipment-to-a-Roles-Loadout) - 1. [Finding an Equipment Item's Name](#Finding-an-Equipment-Items-Name) -1. [Role Packs](#Role-Packs) + 1. [Weapons](#weapons) + 1. [Adding Weapons](#adding-weapons) + 1. [Removing Weapons](#removing-weapons) + 1. [Bypassing Weapon Randomization](#bypassing-weapon-randomization) + 1. [Adding Weapons to a Role's Loadout](#adding-weapons-to-a-roles-loadout) + 1. [Finding a Weapon's Class](#finding-a-weapons-class) + 1. [Equipment](#equipment) + 1. [Adding Equipment](#adding-equipment) + 1. [Removing Equipment](#removing-equipment) + 1. [Adding Equipment to a Role's Loadout](#adding-equipment-to-a-roles-loadout) + 1. [Finding an Equipment Item's Name](#finding-an-equipment-items-name) +1. [Role Packs](#role-packs) 1. [Overall](#role-pack-overall) 1. [Roles](#role-pack-roles) 1. [Adding a new Role Slot](#adding-a-new-role-slot) @@ -28,10 +28,10 @@ 1. [Role Pack Role Blocks](#role-pack-role-blocks) 1. [Weapons](#role-pack-weapons) 1. [ConVars](#role-pack-convars) -1. [Role Blocks](#Role-Blocks) +1. [Role Blocks](#role-blocks) 1. [Adding a new Blocking Group](#adding-a-new-blocking-group) 1. [Configuring a Blocking Group Role](#configuring-a-blocking-group-role) -1. [Renaming Roles](#Renaming-Roles) +1. [Renaming Roles](#renaming-roles) ## Server Configurations diff --git a/CREATE_TASKMASTER_TASKS.md b/CREATE_TASKMASTER_TASKS.md index 300312d49..121f26c40 100644 --- a/CREATE_TASKMASTER_TASKS.md +++ b/CREATE_TASKMASTER_TASKS.md @@ -1,28 +1,28 @@ # Creating Your Own Custom Task for the Taskmaster ## Table of Contents -1. [Before You Start](#Before-You-Start) -1. [Code](#Code) - 1. [Task Table](#Task-Table) - 1. [Task Properties](#Task-Properties) - 1. [Name](#Name) - 1. [Description](#Description) - 1. [Initialize](#Initialize) - 1. [Server-Side](#Server-Side) - 1. [CanAssignTask](#CanAssignTask) - 1. [RequiredFeatures](#RequiredFeatures) - 1. [OnTaskAssigned](#OnTaskAssigned) - 1. [OnTaskRemoved](#OnTaskRemoved) - 1. [OnTaskComplete](#OnTaskComplete) - 1. [Task Registration](#Task-Registration) - 1. [Example File](#Example-File) -1. [Uploading Your Addon](#Uploading-Your-Addon) +1. [Before You Start](#before-you-start) +1. [Code](#code) + 1. [Task Table](#task-table) + 1. [Task Properties](#task-properties) + 1. [Name](#name) + 1. [Description](#description) + 1. [Initialize](#initialize) + 1. [Server-Side](#server-side) + 1. [CanAssignTask](#canassigntask) + 1. [RequiredFeatures](#requiredfeatures) + 1. [OnTaskAssigned](#ontaskassigned) + 1. [OnTaskRemoved](#ontaskremoved) + 1. [OnTaskComplete](#ontaskcomplete) + 1. [Task Registration](#task-registration) + 1. [Example File](#example-file) +1. [Uploading Your Addon](#uploading-your-addon) 1. [addon.json](#addonjson) - 1. [Workshop Icon](#Workshop-Icon) - 1. [Folder Name](#Folder-Name) - 1. [Final Checks](#Final-Checks) - 1. [Uploading](#Uploading) -1. [Wrapping Up](#Wrapping-Up) + 1. [Workshop Icon](#workshop-icon) + 1. [Folder Name](#folder-name) + 1. [Final Checks](#final-checks) + 1. [Uploading](#uploading) +1. [Wrapping Up](#wrapping-up) ## Before You Start In order to create your own tasks you will need to make sure you have downloaded tools to edit the following file types: diff --git a/CREATE_YOUR_OWN_ROLE.md b/CREATE_YOUR_OWN_ROLE.md index e7266cf4c..bce30b998 100644 --- a/CREATE_YOUR_OWN_ROLE.md +++ b/CREATE_YOUR_OWN_ROLE.md @@ -1,53 +1,53 @@ # Creating Your Own Custom Roles for TTT Role ## Table of Contents -1. [Before You Start](#Before-You-Start) -1. [Code](#Code) - 1. [Role Table](#Role-Table) - 1. [Role Strings](#Role-Strings) - 1. [Description](#Description) - 1. [Short Description](#Short-Description) - 1. [Team](#Team) - 1. [Shop and Loadout Items](#Shop-and-Loadout-Items) - 1. [Weapon](#Weapon) - 1. [Equipment](#Equipment) - 1. [Credits](#Credits) - 1. [Health](#Health) - 1. [Role Activation](#Role-Activation) - 1. [Role Selection](#Role-Selection) - 1. [Acting Like a Jester](#Acting-Like-a-Jester) - 1. [Translations](#Translations) - 1. [Custom Spectator HUD](#Custom-Spectator-HUD) - 1. [Optional Rules](#Optional-Rules) - 1. [ConVars](#ConVars) - 1. [Custom Win Conditions](#Custom-Win-Conditions) - 1. [Win Identifier](#Win-Identifier) - 1. [Win Condition](#Win-Condition) - 1. [Round Summary Title](#Round-Summary-Title) - 1. [Round Summary Events](#Round-Summary-Events) - 1. [Round Result Message](#Round-Result-Message) - 1. [Full Win Condition Example](#Full-Win-Condition-Example) - 1. [Tutorial Page](#Tutorial-Page) - 1. [Role Registration](#Role-Registration) - 1. [Final Block](#Final-Block) - 1. [Example File](#Example-File) - 1. [File Separation](#File-Separation) - 1. [Role Modifications](#Role-Modifications) -1. [Sprites](#Sprites) - 1. [Updating the Sprite Folder Name](#Updating-the-Sprite-Folder-Name) - 1. [Finding a Role Icon](#Finding-a-Role-Icon) - 1. [Tab File](#Tab-File) - 1. [Score File](#Score-File) - 1. [Sprite File](#Sprite-File) - 1. [Icon File](#Icon-File) - 1. [.vmt Files](#vmt-Files) -1. [Uploading Your Addon](#Uploading-Your-Addon) +1. [Before You Start](#before-you-start) +1. [Code](#code) + 1. [Role Table](#role-table) + 1. [Role Strings](#role-strings) + 1. [Description](#description) + 1. [Short Description](#short-description) + 1. [Team](#team) + 1. [Shop and Loadout Items](#shop-and-loadout-items) + 1. [Weapon](#weapon) + 1. [Equipment](#equipment) + 1. [Credits](#credits) + 1. [Health](#health) + 1. [Role Activation](#role-activation) + 1. [Role Selection](#role-selection) + 1. [Acting Like a Jester](#acting-like-a-jester) + 1. [Translations](#translations) + 1. [Custom Spectator HUD](#custom-spectator-hud) + 1. [Optional Rules](#optional-rules) + 1. [ConVars](#convars) + 1. [Custom Win Conditions](#custom-win-conditions) + 1. [Win Identifier](#win-identifier) + 1. [Win Condition](#win-condition) + 1. [Round Summary Title](#round-summary-title) + 1. [Round Summary Events](#round-summary-events) + 1. [Round Result Message](#round-result-message) + 1. [Full Win Condition Example](#full-win-condition-example) + 1. [Tutorial Page](#tutorial-page) + 1. [Role Registration](#role-registration) + 1. [Final Block](#final-block) + 1. [Example File](#example-file) + 1. [File Separation](#file-separation) + 1. [Role Modifications](#role-modifications) +1. [Sprites](#sprites) + 1. [Updating the Sprite Folder Name](#updating-the-sprite-folder-name) + 1. [Finding a Role Icon](#finding-a-role-icon) + 1. [Tab File](#tab-file) + 1. [Score File](#score-file) + 1. [Sprite File](#sprite-file) + 1. [Icon File](#icon-file) + 1. [.vmt Files](#vmt-files) +1. [Uploading Your Addon](#uploading-your-addon) 1. [addon.json](#addonjson) - 1. [Workshop Icon](#Workshop-Icon) - 1. [Folder Name](#Folder-Name) - 1. [Final Checks](#Final-Checks) - 1. [Uploading](#Uploading) -1. [Wrapping Up](#Wrapping-Up) + 1. [Workshop Icon](#workshop-icon) + 1. [Folder Name](#folder-name) + 1. [Final Checks](#final-checks) + 1. [Uploading](#uploading) +1. [Wrapping Up](#wrapping-up) ## Before You Start In order to create your own role you will need to make sure you have downloaded tools to edit the following file types: @@ -280,7 +280,7 @@ ROLE.maxhealth = 150 ### Role Activation -Some roles may have special logic that changes how they behave after some activation event. For example, the Clown is activated when only one team remains and the effect of their activation is they can now do damage. If you want to be able to do something like that we recommend using the entity networked properties system (such as [SetNWBool](https://wiki.facepunch.com/gmod/Entity:SetNWBool)). When the role is activated you could `ply:SetNWBool("SummonerActive", true)` and then check that they are active in other places to change their behavior using `ply:GetNWBool("SummonerActive", false)`. To make this slightly nicer, we introduced the `ply:IsRoleActive()` method in v1.2.2 which is also used in delayed shop activation (see [Optional Rules](#Optional-Rules)). +Some roles may have special logic that changes how they behave after some activation event. For example, the Clown is activated when only one team remains and the effect of their activation is they can now do damage. If you want to be able to do something like that we recommend using the entity networked properties system (such as [SetNWBool](https://wiki.facepunch.com/gmod/Entity:SetNWBool)). When the role is activated you could `ply:SetNWBool("SummonerActive", true)` and then check that they are active in other places to change their behavior using `ply:GetNWBool("SummonerActive", false)`. To make this slightly nicer, we introduced the `ply:IsRoleActive()` method in v1.2.2 which is also used in delayed shop activation (see [Optional Rules](#optional-rules)). The next line in our role definition has to do with tying into the `ply:IsRoleActive()` method, allowing you to define if your role is "active" on your own terms: @@ -382,7 +382,7 @@ Some roles have features which are activated once the player dies, such as the p This section of the guide will explain the pieces of the system which should make these kind of spectator HUDs easier and provide a proof-of-concept example with the summoner role. -To implement a spectator HUD like the phantom has, you will need to create two hooks, one player method, and a series of translations and convars to control which powers are enabled and their costs. The translations should be added using the role translations system that you can read about [above](#Translations). The `TTTSpectatorShowHUD` hook (which is what is used to render the spectator HUD itself) must be defined on the client. The convars and the `TTTSpectatorHUDKeyPress` hook (which is what is used to handle when a key is pressed by someone who has a spectator HUD shown) must be defined on the server. The `ROLE.shouldshowspectatorhud` method (which determines whether a player should currently be seeing a spectator HUD) should be defined on both client and server. +To implement a spectator HUD like the phantom has, you will need to create two hooks, one player method, and a series of translations and convars to control which powers are enabled and their costs. The translations should be added using the role translations system that you can read about [above](#translations). The `TTTSpectatorShowHUD` hook (which is what is used to render the spectator HUD itself) must be defined on the client. The convars and the `TTTSpectatorHUDKeyPress` hook (which is what is used to handle when a key is pressed by someone who has a spectator HUD shown) must be defined on the server. The `ROLE.shouldshowspectatorhud` method (which determines whether a player should currently be seeing a spectator HUD) should be defined on both client and server. Due to how inter-connected the pieces of this system are, we're not going to break them down into individual blocks in this guide like other sections do. Instead, we'll go over them in concept and then leave the implemented example below for you to peruse. @@ -458,7 +458,7 @@ There are a few options for roles that aren't covered in the template because th |-----------------------------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `ROLE.canlootcredits` | boolean | Whether this role can loot credits from dead bodies. Automatically enabled if the role has a shop, but setting to `false` can make it so the role has a shop but cannot loot credits. Setting this to `true` will allow this role to loot credits regardless of whether they have a shop and will automatically create the `ttt_%NAMERAW%_credits_starting` convar. | 1.1.8 | | `ROLE.canusetraitorbuttons` | boolean | Whether this role can see and use traitor traps. Automatically enabled if the role is part of `ROLE_TEAM_TRAITOR`, but setting to `false` can make it so the role is a traitor that cannot use traitor traps. Setting to `true` will allow this role to use traitor traps regardless of their team association. | 1.1.8 | -| `ROLE.shoulddelayshop` | boolean | Whether this role's shop purchases are delayed. Purchases will only be given to the player when `plymeta:GiveDelayedShopItems` is called by your own role logic. Enabling this feature will automatically create `ttt_%NAMERAW%_shop_active_only` and `ttt_%NAMERAW%_shop_delay` convars. Requires that the role has a shop and has role activation defined (see [Role Activation](#Role-Activation)). | 1.2.2 | +| `ROLE.shoulddelayshop` | boolean | Whether this role's shop purchases are delayed. Purchases will only be given to the player when `plymeta:GiveDelayedShopItems` is called by your own role logic. Enabling this feature will automatically create `ttt_%NAMERAW%_shop_active_only` and `ttt_%NAMERAW%_shop_delay` convars. Requires that the role has a shop and has role activation defined (see [Role Activation](#role-activation)). | 1.2.2 | | `ROLE.haspassivewin` | boolean | Whether this role should not block another role from winning (like the old man). | 1.3.1 | | `ROLE.shouldnotdrown` | boolean | Whether the player should not show the drown effect or take drowning damage. | 1.5.7 | | `ROLE.canseec4` | boolean | Whether the player should be able to see the C4 icons like traitors can. | 1.5.14 | @@ -705,7 +705,7 @@ ROLE.translations = { } ``` -The first hook (`TTTEventFinishText`) is used to control the text to show in the row on the Events tab itself. We recommend using a translatable string (as we do in the example) but that is not strictly necessary. Don't forget to use the role translations system ([detailed above](#Translations)) to set up the translation string to use. +The first hook (`TTTEventFinishText`) is used to control the text to show in the row on the Events tab itself. We recommend using a translatable string (as we do in the example) but that is not strictly necessary. Don't forget to use the role translations system ([detailed above](#translations)) to set up the translation string to use. The second hook (`TTTEventFinishIconText`) is used to control the text that shows when you hover over the icon in the row on the Events tab. The second hook's first return value is the name of a translation string and in most cases doesn't need to be changed at all. In the most common case the only thing you need to do is return the plural string for the winning role (or team) as the second return value. @@ -818,7 +818,7 @@ if CLIENT then end ``` -*(Note: If you would like to make this information translatable, see the [Translations](#Translations) section of this document. )* +*(Note: If you would like to make this information translatable, see the [Translations](#translations) section of this document. )* For a more complex example, lets take the same string from before but change the phrase "traitor team" to be the color of the traitor team in TTT. To do that, we're going to use some fairly basic HTML instead of just raw text: @@ -970,7 +970,7 @@ For example in the case of the summoner, if I wanted to use this method my file If instead of creating your own role from scratch you would like to modify a pre-existing role, you can do this by placing your code inside of 'lua/rolemodifications' instead of 'lua/customroles'. You *SHOULD NOT* create the `ROLE` table or call `RegisterRole(ROLE)` when creating a role modification as the role you are modifying already exists. -You can either place your code inside a single file, or you can split it between three separate client, server, and shared files as is described in the [File Separation](#File-Separation) section. +You can either place your code inside a single file, or you can split it between three separate client, server, and shared files as is described in the [File Separation](#file-separation) section. Modifying pre-existing roles can end up being more confusing than creating one from scratch if you don't know what you are doing, and unfortunately as the scope of modifying a role is almost endless an in depth walkthrough would be impossible. It is strongly recommended that you familiarise yourself with Lua, Garry's Mod and Custom Roles for TTT before getting started with a role modification. If you would like to see an example of a role modification you can look at the code behind the enhanced detectives pack [here](https://github.com/NoxxFlame/TTT-Enhanced-Detectives). diff --git a/CUSTOM_WEAPONS.md b/CUSTOM_WEAPONS.md index 00b1669ef..3b234bcab 100644 --- a/CUSTOM_WEAPONS.md +++ b/CUSTOM_WEAPONS.md @@ -1,9 +1,9 @@ # Creating a Custom Weapon ## Table of Contents -1. [Creating a Custom Weapon](#Creating-a-Custom-Weapon) -1. [Custom SWEP Properties](#Custom-SWEP-Properties) - 1. [Required Items](#Required-Items) +1. [Creating a Custom Weapon](#creating-a-custom-weapon) +1. [Custom SWEP Properties](#custom-swep-properties) + 1. [Required Items](#required-items) ## Creating a Custom Weapon This tutorial will not go in-depth on how to create custom weapons in general, for that please see the official TTT SWEP tutorial [here](https://www.troubleinterroristtown.com/development/sweps/). diff --git a/README.md b/README.md index 291f9d02a..1068eb0f3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ If you would like to test the available configurations, we recommend using ULX/U ## FAQs **How do I use Custom Roles for TTT?**\ -To use CR for TTT, subscribe to the addon in the Steam workshop and refer to the [Configuration](#Configuration) section above for how to change settings (including enabling the new roles). +To use CR for TTT, subscribe to the addon in the Steam workshop and refer to the [Configuration](#configuration) section above for how to change settings (including enabling the new roles). **How do I get this on my server?**\ The easiest way to get CR for TTT onto a dedicated server is to create use an addon collection. See [this guide](https://wiki.facepunch.com/gmod/Workshop_for_Dedicated_Servers) on how to create and use a collection for your dedicated server. @@ -84,7 +84,7 @@ Everyone needs to subscribe to this workshop item, not just the host. We're not We would suggest making a workshop collection of the addons you have and then having your friends subscribe to them all. **How do I enable the new roles? How do I change X, Y, or Z?**\ -Check out the [Configuration](#Configuration) section above and add the setting value you want in your server.cfg (for dedicated servers) or listenserver.cfg (for peer-to-peer, listen, and local servers). If you don't see a setting for what you want to change, leave a comment on the workshop or join the Discord server (see below) and we'll either help you find it or try to add one. +Check out the [Configuration](#configuration) section above and add the setting value you want in your server.cfg (for dedicated servers) or listenserver.cfg (for peer-to-peer, listen, and local servers). If you don't see a setting for what you want to change, leave a comment on the workshop or join the Discord server (see below) and we'll either help you find it or try to add one. **How do I make a Detective spawn every round?**\ Set the following settings:\ From 00c38f7be7d219ce34bd9ecce7bb97fced6d1d1f Mon Sep 17 00:00:00 2001 From: Malivil Date: Wed, 15 Jul 2026 13:52:08 -0400 Subject: [PATCH 08/30] Updated role registration update script to handle more file formats --- scripts/role_register_update.py | 156 +++++++++++++++++++++----------- 1 file changed, 104 insertions(+), 52 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 28243c9ea..bf06e50ac 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -5,10 +5,72 @@ rootdir = input("Path to roles folder: ") -pattern = re.compile(r"^(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", function\((.*)\)", flags=re.MULTILINE) -named_pattern = re.compile(r"^(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", (?!function)(.*)\)", flags=re.MULTILINE) +pattern = re.compile(r"(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", function\((.*)\)", flags=re.MULTILINE) +named_pattern = re.compile(r"(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", (?!function)(.*)\)", flags=re.MULTILINE) +space_pattern = re.compile(r"\s*") substitution = "local function \\2(\\3)" +def file_output(fileHandle, line, newline = False): + if fileHandle != None: + fileHandle.write(line) + if newline: + fileHandle.write("\n") + elif newline: + print(line) + else: + print(line, end='') + +def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): + if len(hooks) == 0: + return lastLine + + if len(lastLine) > 0: + if not lastLine.isspace(): + file_output(fileHandle, "\n") + if not lastLine.endswith("\n"): + file_output(fileHandle, "\n") + + role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sh_").upper() + + file_output(fileHandle, lineSpaces + "------------------", True) + file_output(fileHandle, lineSpaces + "-- REGISTRATION --", True) + file_output(fileHandle, lineSpaces + "------------------\n", True) + + if isRole: + file_output(fileHandle, lineSpaces + "ROLE.registeredhooks = {", True) + else: + file_output(fileHandle, lineSpaces + "ROLE_REGISTERED_HOOKS[ROLE_" + role + "] = {", True) + keys = list(hooks.keys()) + keys.sort() + lastKey = keys[len(keys) - 1] + for key in keys: + handlers = list(hooks[key].keys()) + handlers.sort() + if len(handlers) > 1: + lastHandler = handlers[len(handlers) - 1] + file_output(fileHandle, lineSpaces + " [\"" + key + "\"] = {") + for handlerName in handlers: + fnName = handlerName + if hooks[key][handlerName] != None: + fnName = hooks[key][handlerName] + file_output(fileHandle, lineSpaces + " [\"" + handlerName + "\"] = " + fnName + "") + if handlerName != lastHandler: + file_output(fileHandle, ",") + file_output(fileHandle, "\n") + file_output(fileHandle, lineSpaces + " }") + else: + handlerName = handlers[0] + fnName = handlerName + if hooks[key][handlerName] != None: + fnName = hooks[key][handlerName] + file_output(fileHandle, lineSpaces + " [\"" + key + "\"] = " + fnName) + + if key != lastKey: + file_output(fileHandle, ",") + file_output(fileHandle, "\n") + file_output(fileHandle, lineSpaces + "}") + return "!PLACEHOLDER!" + for subdir, dirs, files in os.walk(rootdir): for file in files: path = os.path.join(subdir, file) @@ -24,14 +86,22 @@ lastLine = None isRole = False skipped = 0 + lineSpaces = "" + inScope = False + scopeSpaces = None + + # Check if this file registers a role because we handle the hooks differently + with open(os.path.join(subdir, file)) as f: + if "RegisterRole(ROLE)" in f.read(): + isRole = True print("Processing " + path) for line in fileinput.input(path, inplace=True): namedMatch = False matches = [] - if pattern.match(line) != None: + if pattern.search(line) != None: matches = pattern.finditer(line) - elif named_pattern.match(line) != None: + elif named_pattern.search(line) != None: namedMatch = True matches = named_pattern.finditer(line) @@ -54,16 +124,40 @@ else: hooks[hookName][hookId] = None + # If this is the registration line we want to skip it and the next (assumingly blank) line if line.startswith("RegisterRole(ROLE)"): - isRole = True skipNext = True else: + # If we're in a SERVER or CLIENT scope + if inScope: + # Save the first line within it's spaces so we know how + # many spaces to add to the hook mapping in this scope + if scopeSpaces == None: + space_match = space_pattern.match(line) + if space_match: + scopeSpaces = space_match.group() + # And we're ending the scope, print out the hooks that belong to it + if line == "end\n" or line == "end": + lastLine = write_hooks(file, isRole, hooks, lastLine, None, scopeSpaces) + print("") + hooks = {} + scopeSpaces = None + inScope = False + if replace: didReplace = True print(pattern.sub(substitution, line), end='') - elif didReplace and (line == "end)\n" or line == "end)"): + + # If this hook definition starts with spaces, save the amount so we know + # how much to indent the end line + space_match = space_pattern.match(line) + if space_match: + lineSpaces = space_match.group() + # If we're ending a hook definition, remove the ending paren + elif didReplace and (line == lineSpaces + "end)\n" or line == lineSpaces + "end)"): didReplace = False - print("end") + print(lineSpaces + "end") + lineSpaces = "" elif skipNext or namedMatch: skipNext = False if line.isspace(): @@ -71,55 +165,13 @@ else: line = "!PLACEHOLDER!\n" else: + if line.startswith("if SERVER then") or line.startswith("if CLIENT then"): + inScope = True print(line, end='') lastLine = line with open(os.path.join(subdir, file), "a") as f: - if len(hooks) > 0: - if len(lastLine) > 0: - if not lastLine.isspace(): - f.write("\n") - if not lastLine.endswith("\n"): - f.write("\n") - - role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sh_") - title_role = role.title() - - f.write("------------------\n") - f.write("-- REGISTRATION --\n") - f.write("------------------\n\n") - - f.write("ROLE.registeredhooks = {\n") - keys = list(hooks.keys()) - keys.sort() - lastKey = keys[len(keys) - 1] - for key in keys: - handlers = list(hooks[key].keys()) - handlers.sort() - if len(handlers) > 1: - lastHandler = handlers[len(handlers) - 1] - f.write(" [\"" + key + "\"] = {") - for handlerName in handlers: - fnName = handlerName - if hooks[key][handlerName] != None: - fnName = hooks[key][handlerName] - f.write(" [\"" + handlerName + "\"] = " + fnName + "") - if handlerName != lastHandler: - f.write(",") - f.write("\n") - f.write(" }") - else: - handlerName = handlers[0] - fnName = handlerName - if hooks[key][handlerName] != None: - fnName = hooks[key][handlerName] - f.write(" [\"" + key + "\"] = " + fnName) - - if key != lastKey: - f.write(",") - f.write("\n") - f.write("}") - lastLine = "!PLACEHOLDER!" + lastLine = write_hooks(file, isRole, hooks, lastLine, f) if isRole: if not lastLine.endswith("\n"): f.write("\n") From d7b4183f4195e53e46af5175124bb57daf8321b4 Mon Sep 17 00:00:00 2001 From: Malivil Date: Wed, 15 Jul 2026 19:39:59 -0400 Subject: [PATCH 09/30] Fixed spacing and count for hook tables in registration conversion script --- scripts/role_register_update.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index bf06e50ac..5c83a6f30 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -86,6 +86,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): lastLine = None isRole = False skipped = 0 + updated = 0 lineSpaces = "" inScope = False scopeSpaces = None @@ -115,7 +116,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): if hookName in ["Initialize", "TTTBeginRound", "TTTPlayerRoleChanged", "TTTPrepareRound", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState"]: replace = False namedMatch = False - skipped = skipped + 1 + skipped += 1 else: if hookName not in hooks: hooks[hookName] = {} @@ -138,11 +139,14 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): scopeSpaces = space_match.group() # And we're ending the scope, print out the hooks that belong to it if line == "end\n" or line == "end": - lastLine = write_hooks(file, isRole, hooks, lastLine, None, scopeSpaces) - print("") - hooks = {} - scopeSpaces = None - inScope = False + hooksToAdd = len(hooks) + if hooksToAdd > 0: + updated += hooksToAdd + lastLine = write_hooks(file, isRole, hooks, lastLine, None, scopeSpaces) + print("") + hooks = {} + scopeSpaces = None + inScope = False if replace: didReplace = True @@ -171,6 +175,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): lastLine = line with open(os.path.join(subdir, file), "a") as f: + updated += len(hooks) lastLine = write_hooks(file, isRole, hooks, lastLine, f) if isRole: if not lastLine.endswith("\n"): @@ -179,4 +184,4 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): f.write("\n") f.write("RegisterRole(ROLE)") - print("\tCleaned up " + str(len(hooks)) + " hook(s) and skipped " + str(skipped)) \ No newline at end of file + print("\tCleaned up " + str(updated) + " hook(s) and skipped " + str(skipped)) \ No newline at end of file From c3c8cb879d5f3a1164a9ad578ffae5c913960bd3 Mon Sep 17 00:00:00 2001 From: Malivil Date: Wed, 15 Jul 2026 20:58:56 -0400 Subject: [PATCH 10/30] Changed automatic role registration hooks to remove spaces from role names for consistency --- RELEASE.md | 3 +++ gamemodes/terrortown/gamemode/shared.lua | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index de5c139fe..8590cd56b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,6 +12,9 @@ - Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT - Ported "Fix TTT state and report chunking" from base TTT +### Developer +- Changed automatic role registration hooks to remove spaces from role names for consistency + ## 2.5.0 **Released: June 30th, 2026**\ Includes beta updates [2.4.2](#242-beta) to [2.4.7](#247-beta). diff --git a/gamemodes/terrortown/gamemode/shared.lua b/gamemodes/terrortown/gamemode/shared.lua index 738908d52..4173ca984 100644 --- a/gamemodes/terrortown/gamemode/shared.lua +++ b/gamemodes/terrortown/gamemode/shared.lua @@ -23,6 +23,7 @@ local StringFind = string.find local StringFormat = string.format local StringSplit = string.Split local StringSub = string.sub +local StringGsub = string.gsub local Utf8Lower = utf8.lower local Utf8Sub = utf8.sub local TableHasValue = table.HasValue @@ -1217,7 +1218,7 @@ local function RegisterHooks(role) AddHook(hookName, hookKey, hookFn) end else - local hookKey = (ROLE_HOOK_REGISTRATION_KEY[role] or ROLE_STRINGS[role]) .. "_" .. hookName + local hookKey = (ROLE_HOOK_REGISTRATION_KEY[role] or StringGsub(ROLE_STRINGS[role], "%s+", "")) .. "_" .. hookName AddHook(hookName, hookKey, hookData) end end @@ -1235,7 +1236,7 @@ local function UnregisterHooks(role) RemoveHook(hookName, hookKey) end else - local hookKey = (ROLE_HOOK_REGISTRATION_KEY[role] or ROLE_STRINGS[role]) .. "_" .. hookName + local hookKey = (ROLE_HOOK_REGISTRATION_KEY[role] or StringGsub(ROLE_STRINGS[role], "%s+", "")) .. "_" .. hookName RemoveHook(hookName, hookKey) end end From 04e52b256016ed8643e46dc4c03b3335b897aa1e Mon Sep 17 00:00:00 2001 From: Malivil Date: Wed, 15 Jul 2026 22:43:04 -0400 Subject: [PATCH 11/30] Added more hooks to the registration system block list --- CREATE_YOUR_OWN_ROLE.md | 3 +++ docs/tutorials/create_your_own_role.html | 3 +++ scripts/role_register_update.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CREATE_YOUR_OWN_ROLE.md b/CREATE_YOUR_OWN_ROLE.md index bce30b998..032db41c7 100644 --- a/CREATE_YOUR_OWN_ROLE.md +++ b/CREATE_YOUR_OWN_ROLE.md @@ -869,6 +869,9 @@ There are hooks that don't work with this system for one reason or another. If y - TTTPlayerRoleChanged - TTTSelectRoles - TTTTutorialRoleText +- TTTUpdateRoleState +- TTTSyncEventIDs +- TTTSyncWinIDs In the rare case that multiple roles share a hook implementation (like the Good Twin and Evil Twin), then you need to also specify a shared `ROLE.hookregistrationkey`. diff --git a/docs/tutorials/create_your_own_role.html b/docs/tutorials/create_your_own_role.html index 4e17c6087..2ed453871 100644 --- a/docs/tutorials/create_your_own_role.html +++ b/docs/tutorials/create_your_own_role.html @@ -983,6 +983,9 @@

Hook Management

  • TTTPlayerRoleChanged
  • TTTSelectRoles
  • TTTTutorialRoleText
  • +
  • TTTUpdateRoleState
  • +
  • TTTSyncEventIDs
  • +
  • TTTSyncWinIDs
  • In the rare case that multiple roles share a hook implementation (like the Good Twin and Evil Twin), then you need to also specify a shared ROLE.hookregistrationkey.

    diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 5c83a6f30..7e09a6e4c 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -113,7 +113,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): hookName = groups[0] hookId = groups[1] # These hooks need to run before or after registration and un-registration happen so don't move them to the new system - if hookName in ["Initialize", "TTTBeginRound", "TTTPlayerRoleChanged", "TTTPrepareRound", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState"]: + if hookName in ["Initialize", "TTTBeginRound", "TTTPrepareRound", "TTTPlayerRoleChanged", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState", "TTTSyncEventIDs", "TTTSyncWinIDs"]: replace = False namedMatch = False skipped += 1 From 72291d15fc500b53a40bf5e2d8bcdec017e7adc6 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 10:17:18 -0400 Subject: [PATCH 12/30] Added support for single-file roles with shared context hooks Fixed extra spaces around role registration line Fixed trying to add hooks added by functions into the registration system --- scripts/role_register_update.py | 110 +++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 7e09a6e4c..1ee411519 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -20,7 +20,7 @@ def file_output(fileHandle, line, newline = False): else: print(line, end='') -def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): +def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces = ""): if len(hooks) == 0: return lastLine @@ -36,40 +36,50 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): file_output(fileHandle, lineSpaces + "-- REGISTRATION --", True) file_output(fileHandle, lineSpaces + "------------------\n", True) - if isRole: - file_output(fileHandle, lineSpaces + "ROLE.registeredhooks = {", True) - else: - file_output(fileHandle, lineSpaces + "ROLE_REGISTERED_HOOKS[ROLE_" + role + "] = {", True) keys = list(hooks.keys()) keys.sort() lastKey = keys[len(keys) - 1] + prefix = " " + + if hasScope: + if isRole: + prefix = "ROLE.registeredhooks" + else: + prefix = "ROLE_REGISTERED_HOOKS[ROLE_" + role + "]" + else: + if isRole: + file_output(fileHandle, lineSpaces + "ROLE.registeredhooks = {", True) + else: + file_output(fileHandle, lineSpaces + "ROLE_REGISTERED_HOOKS[ROLE_" + role + "] = {", True) for key in keys: handlers = list(hooks[key].keys()) handlers.sort() if len(handlers) > 1: lastHandler = handlers[len(handlers) - 1] - file_output(fileHandle, lineSpaces + " [\"" + key + "\"] = {") + file_output(fileHandle, lineSpaces + prefix + "[\"" + key + "\"] = {", True) for handlerName in handlers: fnName = handlerName if hooks[key][handlerName] != None: fnName = hooks[key][handlerName] - file_output(fileHandle, lineSpaces + " [\"" + handlerName + "\"] = " + fnName + "") + file_output(fileHandle, lineSpaces + " " + prefix + "[\"" + handlerName + "\"] = " + fnName + "") if handlerName != lastHandler: file_output(fileHandle, ",") - file_output(fileHandle, "\n") + file_output(fileHandle, "", True) file_output(fileHandle, lineSpaces + " }") else: handlerName = handlers[0] fnName = handlerName if hooks[key][handlerName] != None: fnName = hooks[key][handlerName] - file_output(fileHandle, lineSpaces + " [\"" + key + "\"] = " + fnName) + file_output(fileHandle, lineSpaces + prefix + "[\"" + key + "\"] = " + fnName) - if key != lastKey: + if not hasScope and key != lastKey: file_output(fileHandle, ",") file_output(fileHandle, "\n") - file_output(fileHandle, lineSpaces + "}") - return "!PLACEHOLDER!" + if not hasScope: + file_output(fileHandle, lineSpaces + "}") + return "!PLACEHOLDER!" + return " " for subdir, dirs, files in os.walk(rootdir): for file in files: @@ -81,15 +91,27 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): continue hooks = {} + + # File state + isRole = False + hasScope = False didReplace = False + + # Scope state + inScope = False + scopeSpaces = None + + # Function state + inFunction = False + + # Line state skipNext = False lastLine = None - isRole = False + lineSpaces = "" + + # Stats skipped = 0 updated = 0 - lineSpaces = "" - inScope = False - scopeSpaces = None # Check if this file registers a role because we handle the hooks differently with open(os.path.join(subdir, file)) as f: @@ -106,24 +128,30 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): namedMatch = True matches = named_pattern.finditer(line) + # local function Something() + # local something = function() + if not inFunction: + inFunction = ("function " in line) or ("= function(" in line) or ("=function(" in line) + replace = False - for match_num, match in enumerate(matches, start=1): - replace = not namedMatch - groups = match.groups() - hookName = groups[0] - hookId = groups[1] - # These hooks need to run before or after registration and un-registration happen so don't move them to the new system - if hookName in ["Initialize", "TTTBeginRound", "TTTPrepareRound", "TTTPlayerRoleChanged", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState", "TTTSyncEventIDs", "TTTSyncWinIDs"]: - replace = False - namedMatch = False - skipped += 1 - else: - if hookName not in hooks: - hooks[hookName] = {} - if namedMatch: - hooks[hookName][hookId] = groups[2] + if not inFunction: + for match_num, match in enumerate(matches, start=1): + replace = not namedMatch + groups = match.groups() + hookName = groups[0] + hookId = groups[1] + # These hooks need to run before or after registration and un-registration happen so don't move them to the new system + if hookName in ["Initialize", "TTTBeginRound", "TTTPrepareRound", "TTTPlayerRoleChanged", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState", "TTTSyncEventIDs", "TTTSyncWinIDs"]: + replace = False + namedMatch = False + skipped += 1 else: - hooks[hookName][hookId] = None + if hookName not in hooks: + hooks[hookName] = {} + if namedMatch: + hooks[hookName][hookId] = groups[2] + else: + hooks[hookName][hookId] = None # If this is the registration line we want to skip it and the next (assumingly blank) line if line.startswith("RegisterRole(ROLE)"): @@ -142,11 +170,16 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): hooksToAdd = len(hooks) if hooksToAdd > 0: updated += hooksToAdd - lastLine = write_hooks(file, isRole, hooks, lastLine, None, scopeSpaces) + lastLine = write_hooks(file, isRole, hooks, lastLine, None, False, scopeSpaces) print("") hooks = {} scopeSpaces = None inScope = False + # If we're inside another function and it's ending, reset the state + if inFunction and line.endswith("end\n") or line.endswith("end"): + inFunction = False + + lastLine = line if replace: didReplace = True @@ -162,21 +195,20 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, lineSpaces = ""): didReplace = False print(lineSpaces + "end") lineSpaces = "" + # Setting this to a space fixes extra newlines when an ending block is at the end of the file + lastLine = " " elif skipNext or namedMatch: skipNext = False - if line.isspace(): - print(line, end='') - else: - line = "!PLACEHOLDER!\n" + line = "!PLACEHOLDER!\n" else: if line.startswith("if SERVER then") or line.startswith("if CLIENT then"): inScope = True + hasScope = True print(line, end='') - lastLine = line with open(os.path.join(subdir, file), "a") as f: updated += len(hooks) - lastLine = write_hooks(file, isRole, hooks, lastLine, f) + lastLine = write_hooks(file, isRole, hooks, lastLine, f, hasScope) if isRole: if not lastLine.endswith("\n"): f.write("\n") From 98b827b27ea48688a50c73d9aaa8d2d95a0d2248 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 10:26:37 -0400 Subject: [PATCH 13/30] Cleanup --- scripts/role_register_update.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 1ee411519..ba1d5280e 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -24,6 +24,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if len(hooks) == 0: return lastLine + # If we didn't just write a blank line, we might want to add a spacer if len(lastLine) > 0: if not lastLine.isspace(): file_output(fileHandle, "\n") @@ -31,16 +32,16 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces file_output(fileHandle, "\n") role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sh_").upper() - - file_output(fileHandle, lineSpaces + "------------------", True) - file_output(fileHandle, lineSpaces + "-- REGISTRATION --", True) - file_output(fileHandle, lineSpaces + "------------------\n", True) - keys = list(hooks.keys()) keys.sort() lastKey = keys[len(keys) - 1] prefix = " " + file_output(fileHandle, lineSpaces + "------------------", True) + file_output(fileHandle, lineSpaces + "-- REGISTRATION --", True) + file_output(fileHandle, lineSpaces + "------------------\n", True) + + # Set up the table accessor depending on the current state if hasScope: if isRole: prefix = "ROLE.registeredhooks" @@ -51,9 +52,11 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces file_output(fileHandle, lineSpaces + "ROLE.registeredhooks = {", True) else: file_output(fileHandle, lineSpaces + "ROLE_REGISTERED_HOOKS[ROLE_" + role + "] = {", True) + for key in keys: handlers = list(hooks[key].keys()) handlers.sort() + # If there are multiple handlers for this hook, structure it as a nested table if len(handlers) > 1: lastHandler = handlers[len(handlers) - 1] file_output(fileHandle, lineSpaces + prefix + "[\"" + key + "\"] = {", True) @@ -66,6 +69,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces file_output(fileHandle, ",") file_output(fileHandle, "", True) file_output(fileHandle, lineSpaces + " }") + # Otherwise just output the mapping directly else: handlerName = handlers[0] fnName = handlerName @@ -76,10 +80,15 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if not hasScope and key != lastKey: file_output(fileHandle, ",") file_output(fileHandle, "\n") - if not hasScope: - file_output(fileHandle, lineSpaces + "}") - return "!PLACEHOLDER!" - return " " + + # If this file has SERVER or CLIENT context scopes we don't need the ending bracket + # and we want to modify the last line (the return value) so it doesn't output + # an unnecessary empty line after this + if hasScope: + return " " + + file_output(fileHandle, lineSpaces + "}") + return "!PLACEHOLDER!" for subdir, dirs, files in os.walk(rootdir): for file in files: @@ -201,6 +210,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces skipNext = False line = "!PLACEHOLDER!\n" else: + # Keep track of the context scope if line.startswith("if SERVER then") or line.startswith("if CLIENT then"): inScope = True hasScope = True @@ -209,6 +219,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces with open(os.path.join(subdir, file), "a") as f: updated += len(hooks) lastLine = write_hooks(file, isRole, hooks, lastLine, f, hasScope) + # Output the registration line at the end if isRole: if not lastLine.endswith("\n"): f.write("\n") From 3666364be1e73fcf5feed73d5ed2d60e133a3c97 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 10:35:55 -0400 Subject: [PATCH 14/30] Fixed extra newlines in some scope-contained registration tables --- scripts/role_register_update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index ba1d5280e..20a4a1830 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -7,7 +7,7 @@ pattern = re.compile(r"(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", function\((.*)\)", flags=re.MULTILINE) named_pattern = re.compile(r"(?:hook\.Add|AddHook)\(\"(.+)\", \"(.+)\", (?!function)(.*)\)", flags=re.MULTILINE) -space_pattern = re.compile(r"\s*") +space_pattern = re.compile(r" *") substitution = "local function \\2(\\3)" def file_output(fileHandle, line, newline = False): @@ -67,7 +67,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces file_output(fileHandle, lineSpaces + " " + prefix + "[\"" + handlerName + "\"] = " + fnName + "") if handlerName != lastHandler: file_output(fileHandle, ",") - file_output(fileHandle, "", True) + file_output(fileHandle, "\n") file_output(fileHandle, lineSpaces + " }") # Otherwise just output the mapping directly else: From 934ecc4bd95ba15a133e4ff2f73657b48becee07 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 10:38:34 -0400 Subject: [PATCH 15/30] Fixed hook registration within scope context not having correct indentation sometimes --- scripts/role_register_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 20a4a1830..e4963138e 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -170,7 +170,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if inScope: # Save the first line within it's spaces so we know how # many spaces to add to the hook mapping in this scope - if scopeSpaces == None: + if scopeSpaces == None or scopeSpaces == "": space_match = space_pattern.match(line) if space_match: scopeSpaces = space_match.group() From 93dc5a58160f2c4565198fcc9cc8a553d99607a2 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 11:27:39 -0400 Subject: [PATCH 16/30] Added support for initializing empty table in contexts without hooks (so shared contexts can use it later) --- scripts/role_register_update.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index e4963138e..30583493d 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -21,9 +21,6 @@ def file_output(fileHandle, line, newline = False): print(line, end='') def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces = ""): - if len(hooks) == 0: - return lastLine - # If we didn't just write a blank line, we might want to add a spacer if len(lastLine) > 0: if not lastLine.isspace(): @@ -34,7 +31,8 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sh_").upper() keys = list(hooks.keys()) keys.sort() - lastKey = keys[len(keys) - 1] + keyLen = len(keys) + lastKey = None prefix = " " file_output(fileHandle, lineSpaces + "------------------", True) @@ -53,6 +51,12 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces else: file_output(fileHandle, lineSpaces + "ROLE_REGISTERED_HOOKS[ROLE_" + role + "] = {", True) + if keyLen > 0: + lastKey = keys[keyLen - 1] + else: + file_output(fileHandle, lineSpaces + " -- Create an empty table here so any hooks in a shared context can be added to it below", True) + file_output(fileHandle, lineSpaces + " -- If no hooks are registered in a shared context, this block can be removed", True) + for key in keys: handlers = list(hooks[key].keys()) handlers.sort() @@ -87,7 +91,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if hasScope: return " " - file_output(fileHandle, lineSpaces + "}") + file_output(fileHandle, lineSpaces + "}", keyLen == 0) return "!PLACEHOLDER!" for subdir, dirs, files in os.walk(rootdir): @@ -177,13 +181,13 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces # And we're ending the scope, print out the hooks that belong to it if line == "end\n" or line == "end": hooksToAdd = len(hooks) + lastLine = write_hooks(file, isRole, hooks, lastLine, None, False, scopeSpaces) if hooksToAdd > 0: - updated += hooksToAdd - lastLine = write_hooks(file, isRole, hooks, lastLine, None, False, scopeSpaces) + updated += len(hooks) print("") - hooks = {} - scopeSpaces = None - inScope = False + hooks = {} + scopeSpaces = None + inScope = False # If we're inside another function and it's ending, reset the state if inFunction and line.endswith("end\n") or line.endswith("end"): inFunction = False @@ -217,8 +221,10 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces print(line, end='') with open(os.path.join(subdir, file), "a") as f: - updated += len(hooks) - lastLine = write_hooks(file, isRole, hooks, lastLine, f, hasScope) + hooksToAdd = len(hooks) + if hooksToAdd > 0: + updated += hooksToAdd + lastLine = write_hooks(file, isRole, hooks, lastLine, f, hasScope) # Output the registration line at the end if isRole: if not lastLine.endswith("\n"): From 7ad009278b303123ee7dc5deb6926de9c9b271e8 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 12:30:57 -0400 Subject: [PATCH 17/30] Added new global --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index 8e74de6f9..8b83d6506 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -899,6 +899,7 @@ globals = { "vgui", "weapons", "DButton", + "DImage", "AWARDS", "CORPSE", "DISGUISE", From 7bd87a8da62fab8722746780ef5c65fc86b3e24d Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 12:31:51 -0400 Subject: [PATCH 18/30] Another new global --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index 8b83d6506..2954ef405 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -737,6 +737,7 @@ globals = { "Derma_Hook", "DetectiveMode", "Dev", + "DisableClipping", "DrawColorModify", "DynamicLight", "EffectData", From 33ae752415f466437919167eb350942396af16d0 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 13:19:43 -0400 Subject: [PATCH 19/30] Fixed handling of hooks added in net messages Fixed shared context hooks added at the start of a single file role not being added to the registration system --- scripts/role_register_update.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 30583493d..0f055697e 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -103,6 +103,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces print("Skipping " + path + ", shared files are not supported") continue + topLevelHooks = {} hooks = {} # File state @@ -116,6 +117,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces # Function state inFunction = False + functionSpaces = "" # Line state skipNext = False @@ -142,9 +144,14 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces matches = named_pattern.finditer(line) # local function Something() - # local something = function() + # local Something = function() + # net.Receive("Something" if not inFunction: - inFunction = ("function " in line) or ("= function(" in line) or ("=function(" in line) + inFunction = ("function " in line) or ("= function(" in line) or ("=function(" in line) or ("net.Receive(" in line) + if inFunction: + space_match = space_pattern.match(line) + if space_match: + functionSpaces = space_match.group() replace = False if not inFunction: @@ -166,7 +173,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces else: hooks[hookName][hookId] = None - # If this is the registration line we want to skip it and the next (assumingly blank) line + # If this is the registration line we want to skip it and the next (assumedly blank) line if line.startswith("RegisterRole(ROLE)"): skipNext = True else: @@ -189,7 +196,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces scopeSpaces = None inScope = False # If we're inside another function and it's ending, reset the state - if inFunction and line.endswith("end\n") or line.endswith("end"): + if inFunction and ((line == functionSpaces + "end\n") or (line == functionSpaces + "end")): inFunction = False lastLine = line @@ -218,9 +225,14 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if line.startswith("if SERVER then") or line.startswith("if CLIENT then"): inScope = True hasScope = True + if len(hooks) > 0: + topLevelHooks = hooks + hooks = {} print(line, end='') with open(os.path.join(subdir, file), "a") as f: + if len(topLevelHooks) > 0: + hooks = topLevelHooks hooksToAdd = len(hooks) if hooksToAdd > 0: updated += hooksToAdd From b948fb241b4bcdfc5d170097cc29530a5e1c8131 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 16:36:50 -0400 Subject: [PATCH 20/30] Added support for "sv_" named files --- scripts/role_register_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 0f055697e..b1943024b 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -28,7 +28,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces if not lastLine.endswith("\n"): file_output(fileHandle, "\n") - role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sh_").upper() + role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sv_").removeprefix("sh_").upper() keys = list(hooks.keys()) keys.sort() keyLen = len(keys) From 0e5ceba832ea4bd1fd72b83548eae29cc8719ad6 Mon Sep 17 00:00:00 2001 From: Malivil Date: Thu, 16 Jul 2026 16:55:51 -0400 Subject: [PATCH 21/30] Fixed hook processing after net method --- scripts/role_register_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index b1943024b..5e500f8fc 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -196,7 +196,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces scopeSpaces = None inScope = False # If we're inside another function and it's ending, reset the state - if inFunction and ((line == functionSpaces + "end\n") or (line == functionSpaces + "end")): + if inFunction and ((line == functionSpaces + "end\n") or (line == functionSpaces + "end") or (line == functionSpaces + "end)\n") or (line == functionSpaces + "end)")): inFunction = False lastLine = line From b3a0891e86d50ea1dfb555e0a51403b5a9c204b9 Mon Sep 17 00:00:00 2001 From: Malivil Date: Fri, 17 Jul 2026 10:46:19 -0400 Subject: [PATCH 22/30] Added PreRegisterSWEP to the blocked registration list Fixed handling of named net receive lines --- CREATE_YOUR_OWN_ROLE.md | 1 + docs/tutorials/create_your_own_role.html | 1 + scripts/role_register_update.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CREATE_YOUR_OWN_ROLE.md b/CREATE_YOUR_OWN_ROLE.md index 032db41c7..bf9e7350b 100644 --- a/CREATE_YOUR_OWN_ROLE.md +++ b/CREATE_YOUR_OWN_ROLE.md @@ -864,6 +864,7 @@ ROLE.registeredhooks = { There are hooks that don't work with this system for one reason or another. If you try to use the system for any of the following unsupported hooks, you will receive an error: - Initialize +- PreRegisterSWEP - TTTBeginRound - TTTPrepareRound - TTTPlayerRoleChanged diff --git a/docs/tutorials/create_your_own_role.html b/docs/tutorials/create_your_own_role.html index 2ed453871..d120a96bf 100644 --- a/docs/tutorials/create_your_own_role.html +++ b/docs/tutorials/create_your_own_role.html @@ -978,6 +978,7 @@

    Hook Management

    There are hooks that don't work with this system for one reason or another. If you try to use the system for any of the following unsupported hooks, you will receive an error:

    • Initialize
    • +
    • PreRegisterSWEP
    • TTTBeginRound
    • TTTPrepareRound
    • TTTPlayerRoleChanged
    • diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 5e500f8fc..3f2188c17 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -147,7 +147,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces # local Something = function() # net.Receive("Something" if not inFunction: - inFunction = ("function " in line) or ("= function(" in line) or ("=function(" in line) or ("net.Receive(" in line) + inFunction = ("function " in line) or ("= function(" in line) or ("=function(" in line) or (("net.Receive(" in line) and (" function(" in line)) if inFunction: space_match = space_pattern.match(line) if space_match: @@ -161,7 +161,7 @@ def write_hooks(file, isRole, hooks, lastLine, fileHandle, hasScope, lineSpaces hookName = groups[0] hookId = groups[1] # These hooks need to run before or after registration and un-registration happen so don't move them to the new system - if hookName in ["Initialize", "TTTBeginRound", "TTTPrepareRound", "TTTPlayerRoleChanged", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState", "TTTSyncEventIDs", "TTTSyncWinIDs"]: + if hookName in ["Initialize", "PreRegisterSWEP", "TTTBeginRound", "TTTPrepareRound", "TTTPlayerRoleChanged", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState", "TTTSyncEventIDs", "TTTSyncWinIDs"]: replace = False namedMatch = False skipped += 1 From 7b9f1f4891396d56bf970aac8e9361360f9c4243 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sat, 18 Jul 2026 10:29:05 -0400 Subject: [PATCH 23/30] Fixed some ragdoll logic not resetting when a player is unragdolled --- RELEASE.md | 1 + gamemodes/terrortown/gamemode/player_ext_shd.lua | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 8590cd56b..9003b7dab 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,7 @@ - Ported "Correction of the calculation that was incorrect." from base TTT - Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT - Ported "Fix TTT state and report chunking" from base TTT +- Fixed some ragdoll logic not resetting when a player is unragdolled ### Developer - Changed automatic role registration hooks to remove spaces from role names for consistency diff --git a/gamemodes/terrortown/gamemode/player_ext_shd.lua b/gamemodes/terrortown/gamemode/player_ext_shd.lua index 412959dc2..8d9086368 100644 --- a/gamemodes/terrortown/gamemode/player_ext_shd.lua +++ b/gamemodes/terrortown/gamemode/player_ext_shd.lua @@ -922,7 +922,6 @@ if SERVER then -- Turn a ragdoll back into a player if they have essentially stopped moving and have been a ragdoll "long enough" if physObj:GetVelocity():Length() <= 10 and (CurTime() - ply.last_ragdoll) > len then - RemoveHook("Think", hookId) ply:UnRagdoll() end end) @@ -1037,6 +1036,10 @@ if SERVER then end function plymeta:UnRagdoll() + local sid64 = self:SteamID64() + RemoveHook("Think", "PlayerRagdollTimer_" .. sid64) + RemoveHook("PostEntityTakeDamage", "PlayerRagdollDamageTransfer_" .. sid64) + if not self:IsRagdolled() then return end -- Save a local reference to use in the timer below From dc6a1688e21a897ffacf020bf9ae789acaef696d Mon Sep 17 00:00:00 2001 From: Malivil Date: Sat, 18 Jul 2026 11:46:37 -0400 Subject: [PATCH 24/30] Added `TTTBlockHUDAmmoPickedUp`, `TTTBlockHUDDrawPickupHistory`, `TTTBlockHUDItemPickedUp`, and `TTTBlockHUDWeaponPickedUp` to block different parts of the weapon pickup HUD --- API/HOOKS.md | 35 ++++++++++++ RELEASE.md | 1 + docs/api/hooks.html | 53 +++++++++++++++++++ .../terrortown/gamemode/cl_hudpickup.lua | 7 ++- 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/API/HOOKS.md b/API/HOOKS.md index 40a9d93bb..704990c6e 100644 --- a/API/HOOKS.md +++ b/API/HOOKS.md @@ -51,6 +51,41 @@ Changed `eq` parameter to be a table of the body's owned equipment IDs instead o - *search* - The body's search info - *eq* - A table of the body's owned equipment IDs +### TTTBlockHUDAmmoPickedUp(itemname, amount) +Called when ammunition is being picked up by the local player, allowing addons to block the display.\ +*Realm:* Client\ +*Added in:* 2.5.1\ +*Parameters:* +- *itemname* - The name of the ammo being picked up +- *amount* - The amount of ammo being picked up + +*Return:* `true` to block the display of this ammo's pickup HUD. Otherwise do not return anything. + +### TTTBlockHUDDrawPickupHistory() +Called when weapon pickup history is being displayed by the local player, allowing addons to block the display.\ +*Realm:* Client\ +*Added in:* 2.5.1 + +*Return:* `true` to block the display of the weapon pickup history HUD. Otherwise do not return anything. + +### TTTBlockHUDItemPickedUp(itemname) +Called when an equipment item is being picked up by the local player, allowing addons to block the display.\ +*Realm:* Client\ +*Added in:* 2.5.1\ +*Parameters:* +- *itemname* - The name of the equipment item being picked up + +*Return:* `true` to block the display of this equipment item's pickup HUD. Otherwise do not return anything. + +### TTTBlockHUDWeaponPickedUp(wep) +Called when a weapon is being picked up by the local player, allowing addons to block the display.\ +*Realm:* Client\ +*Added in:* 2.5.1\ +*Parameters:* +- *wep* - The weapon being picked up + +*Return:* `true` to block the display of this weapon's pickup HUD. Otherwise do not return anything. + ### TTTBlockPlayerFootstepSound(ply) Called when a player is making a footstep. Used to determine if the player's footstep sound should be stopped.\ *Realm:* Client and Server\ diff --git a/RELEASE.md b/RELEASE.md index 9003b7dab..27d4856f4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,7 @@ - Fixed some ragdoll logic not resetting when a player is unragdolled ### Developer +- Added `TTTBlockHUDAmmoPickedUp`, `TTTBlockHUDDrawPickupHistory`, `TTTBlockHUDItemPickedUp`, and `TTTBlockHUDWeaponPickedUp` to block different parts of the weapon pickup HUD - Changed automatic role registration hooks to remove spaces from role names for consistency ## 2.5.0 diff --git a/docs/api/hooks.html b/docs/api/hooks.html index ed8b369c7..7416b5749 100644 --- a/docs/api/hooks.html +++ b/docs/api/hooks.html @@ -84,6 +84,59 @@

      TTTBlockHUDAmmoPickedUp(itemname, amount)

      +

      + Called when ammunition is being picked up by the local player, allowing addons to block the display.
      + Realm: Client
      + Added in: 2.5.1
      + Parameters: +

      +
        +
      • itemname - The name of the ammo being picked up
      • +
      • amount - The amount of ammo being picked up
      • +
      +

      + Return: true to block the display of this ammo's pickup HUD. Otherwise do not return anything. +

      + +

      TTTBlockHUDDrawPickupHistory()

      +

      + Called when weapon pickup history is being displayed by the local player, allowing addons to block the display.
      + Realm: Client
      + Added in: 2.5.1
      +

      +

      + Return: true to block the display of the weapon pickup history HUD. Otherwise do not return anything. +

      + +

      TTTBlockHUDItemPickedUp(itemname)

      +

      + Called when an equipment item is being picked up by the local player, allowing addons to block the display.
      + Realm: Client
      + Added in: 2.5.1
      + Parameters: +

      +
        +
      • itemname - The name of the equipment item being picked up
      • +
      +

      + Return: true to block the display of this equipment item's pickup HUD. Otherwise do not return anything. +

      + +

      TTTBlockHUDWeaponPickedUp(wep)

      +

      + Called when a weapon is being picked up by the local player, allowing addons to block the display.
      + Realm: Client
      + Added in: 2.5.1
      + Parameters: +

      +
        +
      • wep - The weapon being picked up
      • +
      +

      + Return: true to block the display of this weapon's pickup HUD. Otherwise do not return anything. +

      +

      TTTBlockPlayerFootstepSound(ply)

      Called when a player is making a footstep. Used to determine if the player's footstep sound should be stopped.
      diff --git a/gamemodes/terrortown/gamemode/cl_hudpickup.lua b/gamemodes/terrortown/gamemode/cl_hudpickup.lua index bb81cfb9a..ffc6aa053 100644 --- a/gamemodes/terrortown/gamemode/cl_hudpickup.lua +++ b/gamemodes/terrortown/gamemode/cl_hudpickup.lua @@ -21,6 +21,7 @@ GM.PickupHistoryCorner = surface.GetTextureID("gui/corner8") local custom_ammo = CreateClientConVar("ttt_custom_ammo", 0, true, false, "Use custom ammo names.") local hide_role = GetConVar("ttt_hide_role") +local default_pickup = Color(100, 100, 100, 255) local function GetPickupColor() local role = LocalPlayer().GetDisplayedRole and LocalPlayer():GetDisplayedRole() or ROLE_INNOCENT @@ -32,11 +33,12 @@ local function GetPickupColor() return ROLE_COLORS[role] end - return Color(100, 100, 100, 255) + return default_pickup end function GM:HUDWeaponPickedUp(wep) if not (IsValid(wep) and IsValid(LocalPlayer())) or (not LocalPlayer():Alive()) then return end + if CallHook("TTTBlockHUDWeaponPickedUp", nil, wep) == true then return end local name = TryTranslation(wep.GetPrintName and wep:GetPrintName() or wep.PrintName or wep:GetClass() or "Unknown Weapon Name") @@ -68,6 +70,7 @@ end function GM:HUDItemPickedUp(itemname) if not (IsValid(LocalPlayer()) and LocalPlayer():Alive()) then return end + if CallHook("TTTBlockHUDItemPickedUp", nil, itemname) == true then return end local pickup = {} pickup.time = CurTime() @@ -96,6 +99,7 @@ end function GM:HUDAmmoPickedUp(itemname, amount) if not (IsValid(LocalPlayer()) and LocalPlayer():Alive()) then return end + if CallHook("TTTBlockHUDAmmoPickedUp", nil, itemname, amount) == true then return end local itemname_trans = TryTranslation(StringLower("ammo_" .. itemname)) @@ -151,6 +155,7 @@ end function GM:HUDDrawPickupHistory() if (not self.PickupHistory) then return end + if CallHook("TTTBlockHUDDrawPickupHistory", nil) == true then return end local x, y = ScrW() - self.PickupHistoryWide - 20, self.PickupHistoryTop local tall = 0 From 40984197f232ccbd45934e85ce726f2d073898ad Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 19 Jul 2026 09:51:48 -0400 Subject: [PATCH 25/30] Ported "TTT: Fix italic fonts on Linux" from base TTT --- RELEASE.md | 1 + docs/css/style.css | 8 ++++---- .../entities/entities/ttt_c4/cl_init.lua | 2 +- .../terrortown/entities/entities/ttt_c4/shared.lua | 2 +- gamemodes/terrortown/gamemode/cl_help.lua | 2 +- gamemodes/terrortown/gamemode/cl_hud.lua | 12 ++++++------ gamemodes/terrortown/gamemode/cl_init.lua | 6 +++--- gamemodes/terrortown/gamemode/cl_scoring.lua | 14 +++++++------- gamemodes/terrortown/gamemode/cl_targetid.lua | 2 +- gamemodes/terrortown/gamemode/shared.lua | 7 +++++++ gamemodes/terrortown/gamemode/vgui/sb_main.lua | 2 +- 11 files changed, 33 insertions(+), 25 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 27d4856f4..674fe153a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,7 @@ - Ported "Correction of the calculation that was incorrect." from base TTT - Ported "[TTT] Fix undefined variables and malformed patterns" from base TTT - Ported "Fix TTT state and report chunking" from base TTT +- Ported "TTT: Fix italic fonts on Linux" from base TTT - Fixed some ragdoll logic not resetting when a player is unragdolled ### Developer diff --git a/docs/css/style.css b/docs/css/style.css index c7bba47d1..3ed94b1fa 100644 --- a/docs/css/style.css +++ b/docs/css/style.css @@ -33,7 +33,7 @@ body { border-radius: 15px 0 15px 0; color: white; font-size: 1.3em; - font-family: "Trebuchet MS", Tahoma, sans-serif; + font-family: "Trebuchet MS", Tahoma, "DejaVu Sans", sans-serif; font-weight: bold; cursor: pointer; padding: 6px 8px; @@ -48,7 +48,7 @@ body { border-radius: 0 0 15px 0; background-color: #787878; color: white; - font-family: "Trebuchet MS", Tahoma, sans-serif; + font-family: "Trebuchet MS", Tahoma, "DejaVu Sans", sans-serif; padding: 0 10px; position: relative; } @@ -112,7 +112,7 @@ p { html { background-color: #282828; color: white; - font-family: "Trebuchet MS", Tahoma, sans-serif; + font-family: "Trebuchet MS", Tahoma, "DejaVu Sans", sans-serif; overflow-y: scroll; } @@ -236,7 +236,7 @@ code { gap: 6px; border-radius: 15px 0 15px 0; font-size: 1.3em; - font-family: "Trebuchet MS", Tahoma, sans-serif; + font-family: "Trebuchet MS", Tahoma, "DejaVu Sans", sans-serif; font-weight: bold; padding: 10px 12px; text-align: left; diff --git a/gamemodes/terrortown/entities/entities/ttt_c4/cl_init.lua b/gamemodes/terrortown/entities/entities/ttt_c4/cl_init.lua index e2fa8901b..d661532ce 100644 --- a/gamemodes/terrortown/entities/entities/ttt_c4/cl_init.lua +++ b/gamemodes/terrortown/entities/entities/ttt_c4/cl_init.lua @@ -279,7 +279,7 @@ end vgui.Register("DisarmPanel", PANEL, "DPanel") surface.CreateFont("C4Timer", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 30, weight = 750 }) diff --git a/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua b/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua index d2aeeba74..b7aec7f65 100644 --- a/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua +++ b/gamemodes/terrortown/entities/entities/ttt_c4/shared.lua @@ -605,7 +605,7 @@ end if CLIENT then surface.CreateFont("C4ModelTimer", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 13, weight = 0, antialias = false diff --git a/gamemodes/terrortown/gamemode/cl_help.lua b/gamemodes/terrortown/gamemode/cl_help.lua index 9122e7d3b..16d2cde87 100644 --- a/gamemodes/terrortown/gamemode/cl_help.lua +++ b/gamemodes/terrortown/gamemode/cl_help.lua @@ -17,7 +17,7 @@ local GetPTranslation = LANG.GetParamTranslation local HookCall = hook.Call surface.CreateFont("TutorialTitle", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 30, weight = 900 }) diff --git a/gamemodes/terrortown/gamemode/cl_hud.lua b/gamemodes/terrortown/gamemode/cl_hud.lua index 6f61854bb..dee35373b 100644 --- a/gamemodes/terrortown/gamemode/cl_hud.lua +++ b/gamemodes/terrortown/gamemode/cl_hud.lua @@ -26,32 +26,32 @@ local hide_role = GetConVar("ttt_hide_role") -- Fonts surface.CreateFont("TraitorState", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 28, weight = 1000 }) surface.CreateFont("TraitorStateSmall", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 1000 }) surface.CreateFont("TimeLeft", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 800 }) surface.CreateFont("HealthAmmo", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 750 }) surface.CreateFont("UseHintCaption", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 750 }) surface.CreateFont("UseHint", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 18, weight = 750 }) diff --git a/gamemodes/terrortown/gamemode/cl_init.lua b/gamemodes/terrortown/gamemode/cl_init.lua index 450e6d6d5..dd44ebd3f 100644 --- a/gamemodes/terrortown/gamemode/cl_init.lua +++ b/gamemodes/terrortown/gamemode/cl_init.lua @@ -32,16 +32,16 @@ local Utf8Upper = utf8.upper -- Define GM12 fonts for compatibility surface.CreateFont("DefaultBold", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 13, weight = 1000 }) surface.CreateFont("TabLarge", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 13, weight = 700, shadow = true, antialias = false }) surface.CreateFont("Trebuchet22", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 22, weight = 900 }) diff --git a/gamemodes/terrortown/gamemode/cl_scoring.lua b/gamemodes/terrortown/gamemode/cl_scoring.lua index 7ae50f2e6..0d0b56011 100644 --- a/gamemodes/terrortown/gamemode/cl_scoring.lua +++ b/gamemodes/terrortown/gamemode/cl_scoring.lua @@ -39,7 +39,7 @@ include("scoring_shd.lua") local skull_icon = Material("HUD/killicons/default") surface.CreateFont("WinHuge", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 72, weight = 1000, shadow = true, @@ -47,7 +47,7 @@ surface.CreateFont("WinHuge", { }) surface.CreateFont("WinLarge", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 48, weight = 1000, shadow = true, @@ -55,7 +55,7 @@ surface.CreateFont("WinLarge", { }) surface.CreateFont("WinMedium", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 40, weight = 1000, shadow = true, @@ -63,7 +63,7 @@ surface.CreateFont("WinMedium", { }) surface.CreateFont("WinSmall", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 32, weight = 1000, shadow = true, @@ -71,7 +71,7 @@ surface.CreateFont("WinSmall", { }) surface.CreateFont("WinTiny", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 1000, shadow = true, @@ -79,13 +79,13 @@ surface.CreateFont("WinTiny", { }) surface.CreateFont("ScoreNicks", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 32, weight = 100 }) surface.CreateFont("IconText", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 24, weight = 100 }) diff --git a/gamemodes/terrortown/gamemode/cl_targetid.lua b/gamemodes/terrortown/gamemode/cl_targetid.lua index c68e711f3..7865465f0 100644 --- a/gamemodes/terrortown/gamemode/cl_targetid.lua +++ b/gamemodes/terrortown/gamemode/cl_targetid.lua @@ -389,7 +389,7 @@ end ---- Crosshair affairs -surface.CreateFont("TargetIDSmall2", { font = "Tahoma", +surface.CreateFont("TargetIDSmall2", { font = GAMEMODE_DEFAULT_UI_FONT, size = 16, weight = 1000 }) diff --git a/gamemodes/terrortown/gamemode/shared.lua b/gamemodes/terrortown/gamemode/shared.lua index 4173ca984..63f094349 100644 --- a/gamemodes/terrortown/gamemode/shared.lua +++ b/gamemodes/terrortown/gamemode/shared.lua @@ -72,6 +72,13 @@ GM.Version = "Custom Roles for TTT v" .. CR_VERSION GM.Customized = false +-- Font definition +GAMEMODE_DEFAULT_UI_FONT = "Tahoma" + +if system.IsLinux() then + GAMEMODE_DEFAULT_UI_FONT = "DejaVu Sans" +end + local function IsCustomRolesMounted() for _, a in ipairs(engine.GetAddons()) do if tostring(a.wsid) == CR_WORKSHOP_ID and a.mounted then diff --git a/gamemodes/terrortown/gamemode/vgui/sb_main.lua b/gamemodes/terrortown/gamemode/vgui/sb_main.lua index cfea17df2..4962960aa 100644 --- a/gamemodes/terrortown/gamemode/vgui/sb_main.lua +++ b/gamemodes/terrortown/gamemode/vgui/sb_main.lua @@ -39,7 +39,7 @@ surface.CreateFont("cool_large", { weight = 400 }) surface.CreateFont("treb_small", { - font = "Tahoma", + font = GAMEMODE_DEFAULT_UI_FONT, size = 14, weight = 700 }) From d4d67d7dac915cbcf7afefad45c08cf1447010a3 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 19 Jul 2026 09:52:58 -0400 Subject: [PATCH 26/30] Added new global --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index 2954ef405..ab9870daa 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -463,6 +463,7 @@ globals = { "FVPHYSICS_PLAYER_HELD", "FVPHYSICS_WAS_THROWN", "GAMEMODE", + "GAMEMODE_DEFAULT_UI_FONT", "GESTURE_SLOT_CUSTOM", "GM", "GROUP_COUNT", From 262f7b8b755d8b72ccd94cc8fdee207e6f85c034 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sun, 19 Jul 2026 09:54:38 -0400 Subject: [PATCH 27/30] Added another new global --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index ab9870daa..56b71483d 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -696,6 +696,7 @@ globals = { "sboard_panel", "setmetatable", "sql", + "system", "tobool", "tonumber", "tostring", From 6e8488a84850a647c6f939334c029ef58cbb12aa Mon Sep 17 00:00:00 2001 From: Malivil Date: Tue, 21 Jul 2026 07:51:31 -0400 Subject: [PATCH 28/30] Fixed Mad Scientist not blocking the round from ending --- RELEASE.md | 1 + .../roles/madscientist/madscientist.lua | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 674fe153a..dc57e0189 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,6 +13,7 @@ - Ported "Fix TTT state and report chunking" from base TTT - Ported "TTT: Fix italic fonts on Linux" from base TTT - Fixed some ragdoll logic not resetting when a player is unragdolled +- Fixed Mad Scientist not blocking the round from ending ### Developer - Added `TTTBlockHUDAmmoPickedUp`, `TTTBlockHUDDrawPickupHistory`, `TTTBlockHUDItemPickedUp`, and `TTTBlockHUDWeaponPickedUp` to block different parts of the weapon pickup HUD diff --git a/gamemodes/terrortown/gamemode/roles/madscientist/madscientist.lua b/gamemodes/terrortown/gamemode/roles/madscientist/madscientist.lua index 9be84094a..87a5b46db 100644 --- a/gamemodes/terrortown/gamemode/roles/madscientist/madscientist.lua +++ b/gamemodes/terrortown/gamemode/roles/madscientist/madscientist.lua @@ -1,5 +1,9 @@ AddCSLuaFile() +local player = player + +local PlayerIterator = player.Iterator + ------------- -- CONVARS -- ------------- @@ -19,10 +23,38 @@ local function MadScientist_PlayerDeath(victim, infl, attacker) victim:RespawnAsZombie() end +---------------- +-- WIN CHECKS -- +---------------- + +local function MadScientist_TTTCheckForWin() + -- Only run the win check if the mad scientist win by themselves (or with the Zombies) + if not INDEPENDENT_ROLES[ROLE_MADSCIENTIST] then return end + + local zombie_alive = false + local other_alive = false + for _, v in PlayerIterator() do + if v:IsActive() then + if v:IsZombie() or v:IsMadScientist() then + zombie_alive = true + elseif not v:ShouldActLikeJester() and not ROLE_HAS_PASSIVE_WIN[v:GetRole()] then + other_alive = true + end + end + end + + if zombie_alive and not other_alive then + return WIN_ZOMBIE + elseif zombie_alive then + return WIN_NONE + end +end + ------------------ -- REGISTRATION -- ------------------ ROLE_REGISTERED_HOOKS[ROLE_MADSCIENTIST] = { + ["TTTCheckForWin"] = MadScientist_TTTCheckForWin, ["PlayerDeath"] = MadScientist_PlayerDeath } \ No newline at end of file From 9cb4b9c5086f3d89300574b6ab547533c74306d1 Mon Sep 17 00:00:00 2001 From: Malivil Date: Fri, 24 Jul 2026 18:01:54 -0400 Subject: [PATCH 29/30] Added incoming translation from base TTT --- gamemodes/terrortown/gamemode/lang/english.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gamemodes/terrortown/gamemode/lang/english.lua b/gamemodes/terrortown/gamemode/lang/english.lua index 4ef534ffd..362160b2d 100644 --- a/gamemodes/terrortown/gamemode/lang/english.lua +++ b/gamemodes/terrortown/gamemode/lang/english.lua @@ -1237,6 +1237,9 @@ L.equip_sort_direction_tip = "Sort direction" L.set_hide_unbuyable = "Move unbuyable equipment items to the bottom of the list" +-- 2026-07-24 +L.flame_burn = "FIRE! IT BURNS!" + -- Custom Events L.ev_defi = "{victim} was respawned" L.ev_defi_icon = "Defibrillated" From adb65e8afa55954e3271a6582826bb4cd93af242 Mon Sep 17 00:00:00 2001 From: Malivil Date: Sat, 25 Jul 2026 11:53:11 -0400 Subject: [PATCH 30/30] Updated release date --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index dc57e0189..0e27618cd 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ # Release Notes ## 2.5.1 -**Released:** +**Released: July 25th, 2026** ### Changes - Ported "Some TTT cleanups" from base TTT