diff --git a/.luacheckrc b/.luacheckrc index 2eb93112b..56b71483d 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", @@ -612,7 +613,6 @@ globals = { "ROLE_INNOCENT", "ROLE_TRAITOR", "ROUND_ACTIVE", - "ROUND_BEGIN", "ROUND_POST", "ROUND_PREP", "ROUND_WAIT", @@ -696,6 +696,7 @@ globals = { "sboard_panel", "setmetatable", "sql", + "system", "tobool", "tonumber", "tostring", @@ -738,6 +739,7 @@ globals = { "Derma_Hook", "DetectiveMode", "Dev", + "DisableClipping", "DrawColorModify", "DynamicLight", "EffectData", @@ -900,6 +902,7 @@ globals = { "vgui", "weapons", "DButton", + "DImage", "AWARDS", "CORPSE", "DISGUISE", 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/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/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..bf9e7350b 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: @@ -864,11 +864,15 @@ 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 - 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`. @@ -970,7 +974,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:\ diff --git a/RELEASE.md b/RELEASE.md index 3a15fee14..0e27618cd 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,24 @@ # Release Notes +## 2.5.1 +**Released: July 25th, 2026** + +### 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 +- 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 +- 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 +- 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/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: +

+ +

+ 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: +

+ +

+ 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: +

+ +

+ 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/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/docs/tutorials/create_your_own_role.html b/docs/tutorials/create_your_own_role.html index 4e17c6087..d120a96bf 100644 --- a/docs/tutorials/create_your_own_role.html +++ b/docs/tutorials/create_your_own_role.html @@ -978,11 +978,15 @@

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:

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/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/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/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_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 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_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/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_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_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/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 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/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" 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 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 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 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 diff --git a/gamemodes/terrortown/gamemode/shared.lua b/gamemodes/terrortown/gamemode/shared.lua index dab9b36b4..63f094349 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 @@ -30,8 +31,8 @@ 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_BETA = false +CR_VERSION = "2.5.1" +CR_BETA = true CR_WORKSHOP_ID = CR_BETA and "2404251054" or "2421039084" function CRVersion(version) @@ -71,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 @@ -1217,7 +1225,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 +1243,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 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/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 }) 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 diff --git a/scripts/role_register_update.py b/scripts/role_register_update.py index 28243c9ea..3f2188c17 100644 --- a/scripts/role_register_update.py +++ b/scripts/role_register_update.py @@ -5,10 +5,95 @@ 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" *") 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, hasScope, lineSpaces = ""): + # 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") + if not lastLine.endswith("\n"): + file_output(fileHandle, "\n") + + role = os.path.splitext(file)[0].removeprefix("cl_").removeprefix("sv_").removeprefix("sh_").upper() + keys = list(hooks.keys()) + keys.sort() + keyLen = len(keys) + lastKey = None + 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" + 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) + + 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() + # 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) + for handlerName in handlers: + fnName = handlerName + if hooks[key][handlerName] != None: + fnName = hooks[key][handlerName] + file_output(fileHandle, lineSpaces + " " + prefix + "[\"" + handlerName + "\"] = " + fnName + "") + if handlerName != lastHandler: + file_output(fileHandle, ",") + file_output(fileHandle, "\n") + file_output(fileHandle, lineSpaces + " }") + # Otherwise just output the mapping directly + else: + handlerName = handlers[0] + fnName = handlerName + if hooks[key][handlerName] != None: + fnName = hooks[key][handlerName] + file_output(fileHandle, lineSpaces + prefix + "[\"" + key + "\"] = " + fnName) + + if not hasScope and key != lastKey: + file_output(fileHandle, ",") + file_output(fileHandle, "\n") + + # 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 + "}", keyLen == 0) + return "!PLACEHOLDER!" + for subdir, dirs, files in os.walk(rootdir): for file in files: path = os.path.join(subdir, file) @@ -18,108 +103,141 @@ print("Skipping " + path + ", shared files are not supported") continue + topLevelHooks = {} hooks = {} + + # File state + isRole = False + hasScope = False didReplace = False + + # Scope state + inScope = False + scopeSpaces = None + + # Function state + inFunction = False + functionSpaces = "" + + # Line state skipNext = False lastLine = None - isRole = False + lineSpaces = "" + + # Stats skipped = 0 + updated = 0 + + # 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) + # local function Something() + # 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) and (" function(" in line)) + if inFunction: + space_match = space_pattern.match(line) + if space_match: + functionSpaces = space_match.group() + 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", "TTTPlayerRoleChanged", "TTTPrepareRound", "TTTSelectRoles", "TTTTutorialRoleText", "TTTUpdateRoleState"]: - replace = False - namedMatch = False - skipped = 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", "PreRegisterSWEP", "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 (assumedly 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 or scopeSpaces == "": + 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": + hooksToAdd = len(hooks) + lastLine = write_hooks(file, isRole, hooks, lastLine, None, False, scopeSpaces) + if hooksToAdd > 0: + updated += len(hooks) + print("") + hooks = {} + 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") or (line == functionSpaces + "end)\n") or (line == functionSpaces + "end)")): + inFunction = False + + lastLine = line + 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 = "" + # 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: + # Keep track of the context scope + 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='') - 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!" + if len(topLevelHooks) > 0: + hooks = topLevelHooks + 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"): f.write("\n") @@ -127,4 +245,4 @@ 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