From eb0e5901092694d3069d0bf9248f31608e20f069 Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 16 Jan 2026 16:06:46 -0600 Subject: [PATCH 1/7] Add skeleton of new configuration work --- .../terrortown/gamemode/client/cl_main.lua | 1 + .../terrortown/gamemode/server/sv_main.lua | 1 + .../gamemode/server/sv_player_custom.lua | 28 +++++++++++++++++++ .../terrortown/gamemode/shared/sh_include.lua | 2 ++ .../gamemode/shared/sh_player_custom.lua | 3 ++ 5 files changed, 35 insertions(+) create mode 100644 gamemodes/terrortown/gamemode/server/sv_player_custom.lua create mode 100644 gamemodes/terrortown/gamemode/shared/sh_player_custom.lua diff --git a/gamemodes/terrortown/gamemode/client/cl_main.lua b/gamemodes/terrortown/gamemode/client/cl_main.lua index 15bb81e73f..ba33079e13 100644 --- a/gamemodes/terrortown/gamemode/client/cl_main.lua +++ b/gamemodes/terrortown/gamemode/client/cl_main.lua @@ -31,6 +31,7 @@ ttt_include("sh_rolelayering") ttt_include("sh_scoring") ttt_include("sh_corpse") ttt_include("sh_player_ext") +ttt_include("sh_player_custom") ttt_include("sh_weaponry") ttt_include("sh_inventory") ttt_include("sh_door") diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index f7ebc86c7b..0abcdd07c1 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -43,6 +43,7 @@ ttt_include("sv_armor") ttt_include("sh_armor") ttt_include("sh_player_ext") +ttt_include("sh_player_custom") ttt_include("sv_player_ext") ttt_include("sv_player") diff --git a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua new file mode 100644 index 0000000000..c9d8cf0eba --- /dev/null +++ b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua @@ -0,0 +1,28 @@ +--- +-- logic to control player customization on the server side +-- @realm server + +customization = customization or {} + +customization.cv = { + + enforcePlayermodel = CreateConVar( + "ttt_enforce_playermodel", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE } + ), + + useMapPlayermodel = CreateConVar( + "ttt2_prefer_map_models", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE } + ), + + -- use_custom_models? + + allowPlayerCustom = CreateConVar( + "ttt2_allow_player_customization", + "0", + { FCVAR_NOTIFY, FCVAR_ARCHIVE } + ), +} diff --git a/gamemodes/terrortown/gamemode/shared/sh_include.lua b/gamemodes/terrortown/gamemode/shared/sh_include.lua index 7725bc8d4f..3a3db07789 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_include.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_include.lua @@ -56,6 +56,7 @@ TTTFiles = { sh_network_sync = { file = "sh_network_sync.lua", on = "shared" }, sh_player_ext = { file = "sh_player_ext.lua", on = "shared" }, sh_playerclass = { file = "sh_playerclass.lua", on = "shared" }, + sh_player_custom = { file = "sh_player_custom.lua", on = "shared" }, sh_printmessage_override = { file = "sh_printmessage_override.lua", on = "shared" }, sh_cvar_handler = { file = "sh_cvar_handler.lua", on = "shared" }, sh_role_module = { file = "sh_role_module.lua", on = "shared" }, @@ -169,6 +170,7 @@ if SERVER then sv_networking = { file = "sv_networking.lua", on = "server" }, sv_network_sync = { file = "sv_network_sync.lua", on = "server" }, sv_player_ext = { file = "sv_player_ext.lua", on = "server" }, + sv_player_custom = { file = "sv_player_custom.lua", on = "server" }, sv_player = { file = "sv_player.lua", on = "server" }, sv_propspec = { file = "sv_propspec.lua", on = "server" }, sv_roleselection = { file = "sv_roleselection.lua", on = "server" }, diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua b/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua new file mode 100644 index 0000000000..33e409a9e6 --- /dev/null +++ b/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua @@ -0,0 +1,3 @@ +--- +-- logic for managing player customization +-- @realm shared From 014a7d4fc92565191edbeafc30a0cbe0a7b7aafb Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 16 Jan 2026 19:46:00 -0600 Subject: [PATCH 2/7] Transition existing playermodel logic to using centralized convar definitions --- .../terrortown/gamemode/server/sv_main.lua | 33 +++------- .../gamemode/server/sv_player_custom.lua | 64 ++++++++++++++----- .../gamemode/server/sv_player_ext.lua | 2 +- .../gamemode/shared/sh_player_ext.lua | 2 +- lua/ttt2/libraries/playermodels.lua | 6 +- 5 files changed, 60 insertions(+), 47 deletions(-) diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index 0abcdd07c1..88de7e82e5 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -46,6 +46,7 @@ ttt_include("sh_player_ext") ttt_include("sh_player_custom") ttt_include("sv_player_ext") +ttt_include("sv_player_custom") ttt_include("sv_player") ttt_include("sv_addonchecker") @@ -69,18 +70,7 @@ local playerGetAll = player.GetAll --- -- @realm server -local cvPreferMapModels = - CreateConVar("ttt2_prefer_map_models", "1", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) - ---- --- @realm server -local cvSelectModelPerRound = - CreateConVar("ttt2_select_model_per_round", "1", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) - ---- --- @realm server -local cvSelectUniqueModelPerPlayer = - CreateConVar("ttt2_select_unique_model_per_player", "0", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) +local cvSelectModelPerRound --- -- @realm server @@ -131,15 +121,6 @@ local map_switch_delay = CreateConVar( 0 ) ---- --- @realm server -CreateConVar( - "ttt_enforce_playermodel", - "1", - { FCVAR_NOTIFY, FCVAR_ARCHIVE }, - "Whether or not to enforce terrorist playermodels. Set to 0 for compatibility with Enhanced Playermodel Selector" -) - --- -- @realm server CreateConVar("ttt_newroles_enabled", "1", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) @@ -925,10 +906,14 @@ function GM:TTT2PrePrepareRound(duration) -- sets the player model -- supports map models or random player models - if cvPreferMapModels:GetBool() and self.force_plymodel and self.force_plymodel ~= "" then + if + customization.cv.playermodels.useMap:GetBool() + and self.force_plymodel + and self.force_plymodel ~= "" + then self.playermodel = self.force_plymodel - elseif cvSelectModelPerRound:GetBool() then - if cvSelectUniqueModelPerPlayer:GetBool() then + elseif customization.cv.playermodels.perRound:GetBool() then + if customization.cv.playermodels.uniquePerPlayer:GetBool() then local plys = player.GetAll() for i = 1, #plys do plys[i].defaultModel = playermodels.GetRandomPlayerModel() diff --git a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua index c9d8cf0eba..25b783d361 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua @@ -6,23 +6,55 @@ customization = customization or {} customization.cv = { - enforcePlayermodel = CreateConVar( - "ttt_enforce_playermodel", - "1", - { FCVAR_NOTIFY, FCVAR_ARCHIVE } - ), + playermodels = { + --- + -- + -- @realm server + enforce = CreateConVar( + "ttt_enforce_playermodel", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Whether or not to enforce terrorist playermodels. Set to 0 for compatibility with Enhanced Playermodel Selector" + ), - useMapPlayermodel = CreateConVar( - "ttt2_prefer_map_models", - "1", - { FCVAR_NOTIFY, FCVAR_ARCHIVE } - ), + --- + -- + -- @realm server + useMap = CreateConVar( + "ttt2_prefer_map_models", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Whether to use a map's preferred model when available" + ), - -- use_custom_models? + --- + -- + -- @realm server + customModels = CreateConVar( + "ttt2_use_custom_models", + "0", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Whether to use custom playermodels instead of just the default" + ), - allowPlayerCustom = CreateConVar( - "ttt2_allow_player_customization", - "0", - { FCVAR_NOTIFY, FCVAR_ARCHIVE } - ), + --- + -- + -- @realm server + perRound = CreateConVar( + "ttt2_select_model_per_round", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Whether to select a player's model each round or once per map" + ), + + --- + -- + -- @realm server + uniquePerPlayer = CreateConVar( + "ttt2_select_unique_model_per_player", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Whether playermodels should be unique among players" + ), + }, } diff --git a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua index dfe409f16f..37f4492dce 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua @@ -1705,7 +1705,7 @@ local function SetPlayerReady(_, ply) -- if random models for all players are enabled, they should be set as soon -- as the player connects - if GetConVar("ttt2_select_unique_model_per_player"):GetBool() then + if customization.cv.playermodels.uniquePerPlayer:GetBool() then ply.defaultModel = playermodels.GetRandomPlayerModel() end diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua index 18619ab603..3b3043bdde 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua @@ -194,7 +194,7 @@ function plymeta:SetRole(subrole, team, forceHooks, suppressEvent) hook.Run("PlayerLoadout", self, false) -- Don't update the model if oldSubrole is nil (player isn't already spawned, leading to an initialization error) - if oldSubrole and GetConVar("ttt_enforce_playermodel"):GetBool() then + if oldSubrole and customization.cv.playermodels.enforce:GetBool() then -- update subroleModel self:SetModel(self:GetSubRoleModel()) end diff --git a/lua/ttt2/libraries/playermodels.lua b/lua/ttt2/libraries/playermodels.lua index a29bb97c09..f2b0ed270c 100644 --- a/lua/ttt2/libraries/playermodels.lua +++ b/lua/ttt2/libraries/playermodels.lua @@ -18,10 +18,6 @@ local function GetPlayerSize(ply) return top - bottom end ---- --- @realm server -local cvCustomModels = CreateConVar("ttt2_use_custom_models", "0", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) - local initialDefaultStates = { selected = { ["css_phoenix"] = true, @@ -271,7 +267,7 @@ function playermodels.GetRandomPlayerModel() local availableModels = playermodels.GetSelectedModels() local sizeAvailableModels = #availableModels - if cvCustomModels:GetBool() and sizeAvailableModels > 0 then + if customization.cv.playermodels.customModels:GetBool() and sizeAvailableModels > 0 then local modelPaths = playerManagerAllValidModels() local randomModel = availableModels[mathRandom(sizeAvailableModels)] From 1e6a1233495103b628e920e9defe4c547c5c6c8c Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 16 Jan 2026 21:34:33 -0600 Subject: [PATCH 3/7] Tear out all existing playermodel handling logic --- .../terrortown/gamemode/server/sv_main.lua | 36 ---------- .../terrortown/gamemode/server/sv_player.lua | 19 +----- .../gamemode/server/sv_player_ext.lua | 6 -- .../terrortown/gamemode/shared/sh_main.lua | 11 --- .../gamemode/shared/sh_player_ext.lua | 67 +------------------ 5 files changed, 2 insertions(+), 137 deletions(-) diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index 88de7e82e5..71de6c1866 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -409,10 +409,6 @@ function GM:InitPostEntity() -- initialize playermodel database playermodels.Initialize() - -- set the default random playermodel - self.playermodel = playermodels.GetRandomPlayerModel() - self.playercolor = COLOR_WHITE - timer.Simple(0, function() addonChecker.Check() end) @@ -682,10 +678,6 @@ function GM:OnReloaded() button.SetUp() - -- set the default random playermodel - self.playermodel = playermodels.GetRandomPlayerModel() - self.playercolor = COLOR_WHITE - -- register synced player variables player.RegisterSettingOnServer("enable_dynamic_fov", "bool") @@ -903,34 +895,6 @@ function GM:TTT2PrePrepareRound(duration) timer.Create("restartmute", 1, 1, function() MuteForRestart(false) end) - - -- sets the player model - -- supports map models or random player models - if - customization.cv.playermodels.useMap:GetBool() - and self.force_plymodel - and self.force_plymodel ~= "" - then - self.playermodel = self.force_plymodel - elseif customization.cv.playermodels.perRound:GetBool() then - if customization.cv.playermodels.uniquePerPlayer:GetBool() then - local plys = player.GetAll() - for i = 1, #plys do - plys[i].defaultModel = playermodels.GetRandomPlayerModel() - end - else - local plys = player.GetAll() - for i = 1, #plys do - plys[i].defaultModel = nil - end - - self.playermodel = playermodels.GetRandomPlayerModel() - end - end - - --- - -- @realm server - self.playercolor = hook.Run("TTTPlayerColor", self.playermodel) end --- diff --git a/gamemodes/terrortown/gamemode/server/sv_player.lua b/gamemodes/terrortown/gamemode/server/sv_player.lua index 36103f1192..3f113872d7 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player.lua @@ -266,12 +266,6 @@ function GM:PlayerSetModel(ply) if not IsValid(ply) then return end - - -- this will call the overwritten internal function to modify the model - ply:SetModel(ply.defaultModel or GAMEMODE.playermodel) - - -- Always clear color state, may later be changed in TTTPlayerSetColor - ply:SetColor(COLOR_WHITE) end --- @@ -279,18 +273,7 @@ end -- @param Player ply -- @hook -- @realm server -function GM:TTTPlayerSetColor(ply) - local c = COLOR_WHITE - - if GAMEMODE.playercolor then - -- If this player has a colorable model, always use the same color as all - -- other colorable players, so color will never be the factor that lets - -- you tell players apart. - c = GAMEMODE.playercolor - end - - ply:SetPlayerColor(Vector(c.r / 255.0, c.g / 255.0, c.b / 255.0)) -end +function GM:TTTPlayerSetColor(ply) end --- -- Determines if the @{Player} can kill themselves using the concommands "kill" or "explode". diff --git a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua index 37f4492dce..d53113987b 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua @@ -1703,12 +1703,6 @@ local function SetPlayerReady(_, ply) gameloop.PlayerReady(ply) - -- if random models for all players are enabled, they should be set as soon - -- as the player connects - if customization.cv.playermodels.uniquePerPlayer:GetBool() then - ply.defaultModel = playermodels.GetRandomPlayerModel() - end - --- -- @realm server hook.Run("TTT2PlayerReady", ply) diff --git a/gamemodes/terrortown/gamemode/shared/sh_main.lua b/gamemodes/terrortown/gamemode/shared/sh_main.lua index 7d8378ddf1..9f134cb214 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_main.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_main.lua @@ -365,17 +365,6 @@ local colormode = -- @hook -- @realm shared function GM:TTTPlayerColor(model) - local mode = colormode:GetInt() - - if mode == 1 then - return ttt_playercolors.serious[math.random(ttt_playercolors_serious_count)] - elseif mode == 2 then - return ttt_playercolors.all[math.random(ttt_playercolors_all_count)] - elseif mode == 3 then - -- Full randomness - return Color(math.random(0, 255), math.random(0, 255), math.random(0, 255)) - end - -- No coloring return COLOR_WHITE end diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua index 3b3043bdde..9d0c5c32eb 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua @@ -192,19 +192,6 @@ function plymeta:SetRole(subrole, team, forceHooks, suppressEvent) --- -- @realm server hook.Run("PlayerLoadout", self, false) - - -- Don't update the model if oldSubrole is nil (player isn't already spawned, leading to an initialization error) - if oldSubrole and customization.cv.playermodels.enforce:GetBool() then - -- update subroleModel - self:SetModel(self:GetSubRoleModel()) - end - - -- Always clear color state, may later be changed in TTTPlayerSetColor - self:SetColor(COLOR_WHITE) - - --- - -- @realm server - hook.Run("TTTPlayerSetColor", self) end end @@ -1084,59 +1071,7 @@ local oldSetModel = plymeta.SetModel or plymeta.MetaBaseClass.SetModel -- @param string mdlName -- @note override to fix PS/ModelSelector/... issues -- @realm shared -function plymeta:SetModel(mdlName) - local mdl - - local curMdl = mdlName or self:GetModel() - - if not checkModel(curMdl) then - curMdl = self.defaultModel - - if not checkModel(curMdl) then - if not checkModel(GAMEMODE.playermodel) then - GAMEMODE.playermodel = GAMEMODE.force_plymodel - - if not checkModel(GAMEMODE.playermodel) then - GAMEMODE.playermodel = "models/player/phoenix.mdl" - end - end - - curMdl = GAMEMODE.playermodel - end - end - - local srMdl = self:GetSubRoleModel() - if srMdl then - mdl = srMdl - - if curMdl ~= srMdl then - self.oldModel = curMdl - end - else - if self.oldModel then - mdl = self.oldModel - self.oldModel = nil - else - mdl = curMdl - end - end - - -- last but not least, we fix this grey model "bug" - if not checkModel(mdl) then - mdl = "models/player/phoenix.mdl" - end - - oldSetModel(self, Model(mdl)) - - if SERVER then - net.Start("TTT2SyncModel") - net.WriteString(mdl) - net.WriteEntity(self) - net.Broadcast() - - self:SetupHands() - end -end +function plymeta:SetModel(mdlName) end hook.Add("TTTEndRound", "TTTEndRound4TTT2TargetPlayer", function() local plys = player.GetAll() From 99651e138487909d4478d5f841c70a3222cc74ec Mon Sep 17 00:00:00 2001 From: DaNike Date: Fri, 16 Jan 2026 21:56:52 -0600 Subject: [PATCH 4/7] Reimplement existing playermodel logic via new hooks --- .../terrortown/gamemode/server/sv_main.lua | 7 + .../terrortown/gamemode/server/sv_player.lua | 47 ---- .../gamemode/server/sv_player_custom.lua | 206 ++++++++++++++++++ .../gamemode/server/sv_player_ext.lua | 8 - .../terrortown/gamemode/shared/sh_main.lua | 41 ---- .../gamemode/shared/sh_player_ext.lua | 47 +++- 6 files changed, 259 insertions(+), 97 deletions(-) diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index 71de6c1866..3e3212d423 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -409,6 +409,8 @@ function GM:InitPostEntity() -- initialize playermodel database playermodels.Initialize() + self.playermodel, self.playercolor = hook.Run("TTT2GetDefaultPlayerDisplay") + timer.Simple(0, function() addonChecker.Check() end) @@ -678,6 +680,8 @@ function GM:OnReloaded() button.SetUp() + self.playermodel, self.playercolor = hook.Run("TTT2GetDefaultPlayerDisplay") + -- register synced player variables player.RegisterSettingOnServer("enable_dynamic_fov", "bool") @@ -895,6 +899,9 @@ function GM:TTT2PrePrepareRound(duration) timer.Create("restartmute", 1, 1, function() MuteForRestart(false) end) + + -- select new default playermodels for the round + self.playermodel, self.playercolor = hook.Run("TTT2GetDefaultPlayerDisplayForRound") end --- diff --git a/gamemodes/terrortown/gamemode/server/sv_player.lua b/gamemodes/terrortown/gamemode/server/sv_player.lua index 3f113872d7..be8d439d82 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player.lua @@ -176,27 +176,6 @@ function GM:PlayerSpawn(ply) end end ---- --- Called whenever view model hands needs setting a model. --- By default this calls @{Player:GetHandsModel} and if that fails, --- sets the hands model according to his @{Player} model. --- @param Player ply The @{Player} whose hands needs a model set --- @param Entity ent The hands to set model of --- @hook --- @realm server --- @ref https://wiki.facepunch.com/gmod/GM:PlayerSetHandsModel --- @local -function GM:PlayerSetHandsModel(ply, ent) - local simplemodel = player_manager.TranslateToPlayerModelName(ply:GetModel()) - local info = player_manager.TranslatePlayerHands(simplemodel) - - if info then - ent:SetModel(info.model) - ent:SetSkin(info.skin) - ent:SetBodyGroups(info.body) - end -end - --- -- Check if a @{Player} can spawn at a certain spawnpoint. -- @param Player ply The @{Player} who is spawned @@ -249,32 +228,6 @@ function GM:PlayerSelectSpawn(ply, transition) -- "[PlayerSelectSpawn] Error! No spawn points!" end ---- --- Called whenever a @{Player} spawns and must choose a model. --- A good place to assign a model to a @{Player}. --- @note This function may not work in your custom gamemode if you have overridden --- your @{GM:PlayerSpawn} and you do not use self.BaseClass.PlayerSpawn or @{hook.Run}. --- @param Player ply The @{Player} being chosen --- @hook --- @realm server --- @ref https://wiki.facepunch.com/gmod/GM:PlayerSetModel --- @local -function GM:PlayerSetModel(ply) - -- The player modes has to be applied here since some player model selectors overwrite - -- this hook to suppress the TTT2 player models. If the model is assigned elsewhere, it - -- breaks with external model selectors. - if not IsValid(ply) then - return - end -end - ---- --- Called when a @{Player} spawns and updates the @{Color} --- @param Player ply --- @hook --- @realm server -function GM:TTTPlayerSetColor(ply) end - --- -- Determines if the @{Player} can kill themselves using the concommands "kill" or "explode". -- Only active players can use kill cmd diff --git a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua index 25b783d361..8eb6b595fb 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua @@ -57,4 +57,210 @@ customization.cv = { "Whether playermodels should be unique among players" ), }, + + colors = { + --- + -- + -- @realm server + mode = CreateConVar( + "ttt_playercolor_mode", + "1", + { FCVAR_NOTIFY, FCVAR_ARCHIVE }, + "Controls the player color selection mode." + ), + }, +} + +local ttt_playercolors = { + all = { + COLOR_WHITE, + COLOR_BLACK, + COLOR_GREEN, + COLOR_DGREEN, + COLOR_RED, + COLOR_YELLOW, + COLOR_LGRAY, + COLOR_BLUE, + COLOR_NAVY, + COLOR_PINK, + COLOR_OLIVE, + COLOR_ORANGE, + }, + serious = { + COLOR_WHITE, + COLOR_BLACK, + COLOR_NAVY, + COLOR_LGRAY, + COLOR_DGREEN, + COLOR_OLIVE, + }, } + +--- +-- @note In TTT (and before the playermodel rework) this is marked "shared" realm, and the +-- associated playercolor_mode cvar is likewise. The rework moved both to be server-only, since all +-- meaningful interaction with it occurs on the server exclusively. +-- @param string model The selected (default) playermodel +-- @hook +-- @realm server +function GM:TTTPlayerColor(model) + local mode = customization.cv.colors.mode:GetInt() + + if mode == 1 then + return ttt_playercolors.serious[math.random(#ttt_playercolors.serious)] + elseif mode == 2 then + return ttt_playercolors.all[math.random(#ttt_playercolors.all)] + elseif mode == 3 then + return Color(math.random(0, 255), math.random(0, 255), math.random(0, 255)) + end + + -- No coloring + return COLOR_WHITE +end + +local plySelectedModels + +--- +-- Called to get the default player display information (model/color) for this map. +-- The returned settings will be overridden when the round starts by +-- @{TTT2GetDefaultPlayerDisplayForRound}. +-- @return string,Color the model,color pair for the default display +-- @hook +-- @realm server +function GM:TTT2GetDefaultPlayerDisplay() + local pm = playermodels.GetRandomPlayerModel() + return pm, hook.Run("TTTPlayerColor", pm) +end + +function GM:TTT2GetDefaultPlayerDisplayForRound() + local pm = self.playermodel + + -- clear the per-player selected models table + plySelectedModels = nil + + if + customization.cv.playermodels.useMap:GetBool() + and self.force_plymodel + and self.force_plymodel ~= "" + then + -- server wants map-configured playermodel and the map specified a playermodel, + -- respect that by default + pm = self.force_plymodel + elseif customization.cv.playermodels.perRound:GetBool() then + -- server wants new playermodels each round, choose a new default + pm = playermodels.GetRandomPlayerModel() + + if customization.cv.playermodels.uniquePerPlayer:GetBool() then + -- we want to set a unique playermodel for each player as well + plySelectedModels = {} + local plys = player.GetAll() + for i = 1, #plys do + if not plys[i]:IsTerror() then + continue + end + + plySelectedModels[plys[i]] = playermodels.GetRandomPlayerModel() + end + end + end + + local color = self.playercolor + + if pm ~= self.playermodel then + -- the playermodel got changed, so we also want to recompute color + color = hook.Run("TTTPlayerColor", pm) + end + + return pm, color +end + +--- +-- Called whenever a @{Player} spawns and must choose a model. +-- A good place to assign a model to a @{Player}. +-- @note This function may not work in your custom gamemode if you have overridden +-- your @{GM:PlayerSpawn} and you do not use self.BaseClass.PlayerSpawn or @{hook.Run}. +-- @param Player ply The @{Player} being chosen +-- @hook +-- @realm server +-- @ref https://wiki.facepunch.com/gmod/GM:PlayerSetModel +-- @local +function GM:PlayerSetModel(ply) + -- The player modes has to be applied here since some player model selectors overwrite + -- this hook to suppress the TTT2 player models. If the model is assigned elsewhere, it + -- breaks with external model selectors. + if not IsValid(ply) then + return + end + + local pm = plySelectedModels and plySelectedModels[ply] + + if + not pm + and customization.cv.playermodels.perRound:GetBool() + and customization.cv.playermodels.uniquePerPlayer:GetBool() + then + -- plySelectedModels doesn't have this player's model, but a unique one was requested for + -- each round. Compute it now. + pm = playermodels.GetRandomPlayerModel() + plySelectedModels = plySelectedModels or {} + plySelectedModels[ply] = pm + end + + ply:SetModel(pm) + -- Always reset the color, which may (will) be later changed by TTTPlayerSetColor + ply:SetColor(COLOR_WHITE) +end + +--- +-- Called whenever view model hands needs setting a model. +-- By default this calls @{Player:GetHandsModel} and if that fails, +-- sets the hands model according to his @{Player} model. +-- @param Player ply The @{Player} whose hands needs a model set +-- @param Entity ent The hands to set model of +-- @hook +-- @realm server +-- @ref https://wiki.facepunch.com/gmod/GM:PlayerSetHandsModel +-- @local +function GM:PlayerSetHandsModel(ply, ent) + local simplemodel = player_manager.TranslateToPlayerModelName(ply:GetModel()) + local info = player_manager.TranslatePlayerHands(simplemodel) + + if info then + ent:SetModel(info.model) + ent:SetSkin(info.skin) + ent:SetBodyGroups(info.body) + end +end + +--- +-- Called when a @{Player} spawns and updates the @{Color} +-- @param Player ply +-- @hook +-- @realm server +function GM:TTTPlayerSetColor(ply) + local c = COLOR_WHITE + + if GAMEMODE.playercolor then + -- If we have a playercolor, by default we should set it for ALL players. + c = GAMEMODE.playercolor + end + + ply:SetPlayerColor(Vector(c.r / 255.0, c.g / 255.0, c.b / 255.0)) +end + +--- +-- @param ply Player the player to update +-- @param doSetModel boolean true if the player's model should be explicitly set +function GM:TTT2UpdateSubrolePlayermodel(ply, doSetModel) + -- doSetModel is false when the 'oldSubrole' local of the caller is unset. The original coments + -- there indicated that this meant that the player isn't already spawned, which causes problems. + -- I am unsure what problems, exactly, though it likely has to do with some part of the work + -- done in plymeta:SetModel. + if doSetModel and customization.cv.playermodels.enforce:GetBool() then + ply:SetModel(ply:GetSubRoleModel()) + end + + -- Always clear color state before invoking TTTPlayerSetColor + ply:SetColor(COLOR_WHITE) + hook.Run("TTTPlayerSetColor", ply) +end diff --git a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua index d53113987b..70cc976fb4 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_ext.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_ext.lua @@ -621,14 +621,6 @@ end -- @return boolean Returns true if player is spawned -- @realm server function plymeta:SpawnForRound(deadOnly) - --- - -- @realm server - hook.Run("PlayerSetModel", self) - - --- - -- @realm server - hook.Run("TTTPlayerSetColor", self) - -- wrong alive status and not a willing spec who unforced after prep started -- (and will therefore be "alive") if deadOnly and self:Alive() and not self:IsSpec() then diff --git a/gamemodes/terrortown/gamemode/shared/sh_main.lua b/gamemodes/terrortown/gamemode/shared/sh_main.lua index 9f134cb214..8e9f05a796 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_main.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_main.lua @@ -328,47 +328,6 @@ function GM:ClientSignOnStateChanged(userID, oldState, newState) GAMEMODE.PlayerSignOnStates[userID] = newState end -local ttt_playercolors = { - all = { - COLOR_WHITE, - COLOR_BLACK, - COLOR_GREEN, - COLOR_DGREEN, - COLOR_RED, - COLOR_YELLOW, - COLOR_LGRAY, - COLOR_BLUE, - COLOR_NAVY, - COLOR_PINK, - COLOR_OLIVE, - COLOR_ORANGE, - }, - serious = { - COLOR_WHITE, - COLOR_BLACK, - COLOR_NAVY, - COLOR_LGRAY, - COLOR_DGREEN, - COLOR_OLIVE, - }, -} -local ttt_playercolors_all_count = #ttt_playercolors.all -local ttt_playercolors_serious_count = #ttt_playercolors.serious - ---- --- @realm shared -local colormode = - CreateConVar("ttt_playercolor_mode", "1", { FCVAR_NOTIFY, FCVAR_ARCHIVE, FCVAR_REPLICATED }) - ---- --- @param string model The selected (default) playermodel --- @hook --- @realm shared -function GM:TTTPlayerColor(model) - -- No coloring - return COLOR_WHITE -end - --- -- Called every frame on client and server. -- This will be the same as @{GM:Tick} on the server when there is no lag, diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua index 9d0c5c32eb..549e42f6c8 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua @@ -192,6 +192,10 @@ function plymeta:SetRole(subrole, team, forceHooks, suppressEvent) --- -- @realm server hook.Run("PlayerLoadout", self, false) + + --- + -- @realm server + hook.Run("TTT2UpdateSubrolePlayermodel", self, oldSubrole ~= nil) end end @@ -1071,7 +1075,48 @@ local oldSetModel = plymeta.SetModel or plymeta.MetaBaseClass.SetModel -- @param string mdlName -- @note override to fix PS/ModelSelector/... issues -- @realm shared -function plymeta:SetModel(mdlName) end +function plymeta:SetModel(mdlName) + --Dev(1, "Player", self, ":SetModel", mdlName or "(nil)") + --ErrorNoHaltWithStack("^^ ply:SetModel") + + local mdl = mdlName or self:GetModel() + + if not checkModel(mdl) then + -- TODO: should this be a fallback model hook? + mdl = self:GetModel() + + if not checkModel(mdl) then + -- TODO: this is in the original code; is it still actually needed? + if not checkModel(GAMEMODE.playermodel) then + GAMEMODE.playermodel = GAMEMODE.force_plymodel + if not checkModel(GAMEMODE.playermodel) then + GAMEMODE.playermodel = "models/player/phoenix.mdl" + end + end + + mdl = GAMEMODE.playermodel + end + end + + -- TODO: original checked and enforced subrole model here. I don't think this is a good idea, so + -- that other addons can override the model selection here, so I'm ommitting it, but there's + -- probably a reason for it to exist. + + if not checkModel(mdl) then + mdl = "models/player/phoenix.mdl" + end + + oldSetModel(self, Model(mdl)) + + if SERVER then + net.Start("TTT2SyncModel") + net.WriteString(mdl) + net.WriteEntity(self) + net.Broadcast() + + self:SetupHands() + end +end hook.Add("TTTEndRound", "TTTEndRound4TTT2TargetPlayer", function() local plys = player.GetAll() From 730f47ac743240b9281bdeba3adf404d304141a0 Mon Sep 17 00:00:00 2001 From: DaNike Date: Sat, 17 Jan 2026 01:24:30 -0600 Subject: [PATCH 5/7] Try for more exclusivity with playermodel selection in unique mode Also fix subrole model enforcement --- .../gamemode/server/sv_player_custom.lua | 28 +++++++++++++++++-- .../gamemode/shared/sh_player_ext.lua | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua index 8eb6b595fb..4eb5663c7a 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua @@ -119,6 +119,27 @@ function GM:TTTPlayerColor(model) end local plySelectedModels +local plyUsedModels + +local function RandomUniqueModel() + plyUsedModels = plyUsedModels or {} + local pm = nil + + -- select a playermodel which hasn't been used before + local i = #playermodels.GetSelectedModels() -- i means we only attempt at #playermodels times, before figuring there are no remaining options + while i > 0 and (not pm or plyUsedModels[pm]) do + pm = playermodels.GetRandomPlayerModel() + i = i - 1 + end + + if not pm or i == 0 then + -- we reached the iteration limit; likely all models are used, so start reusing + plyUsedModels = {} + return RandomUniqueModel() + end + + return pm +end --- -- Called to get the default player display information (model/color) for this map. @@ -159,7 +180,7 @@ function GM:TTT2GetDefaultPlayerDisplayForRound() continue end - plySelectedModels[plys[i]] = playermodels.GetRandomPlayerModel() + plySelectedModels[plys[i]] = RandomUniqueModel() end end end @@ -192,6 +213,9 @@ function GM:PlayerSetModel(ply) return end + -- We need to clear subrole models at some point; we'll do it here + ply:SetSubRoleModel(nil) + local pm = plySelectedModels and plySelectedModels[ply] if @@ -201,7 +225,7 @@ function GM:PlayerSetModel(ply) then -- plySelectedModels doesn't have this player's model, but a unique one was requested for -- each round. Compute it now. - pm = playermodels.GetRandomPlayerModel() + pm = RandomUniqueModel() plySelectedModels = plySelectedModels or {} plySelectedModels[ply] = pm end diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua index 549e42f6c8..214f820d8b 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_player_ext.lua @@ -1082,7 +1082,7 @@ function plymeta:SetModel(mdlName) local mdl = mdlName or self:GetModel() if not checkModel(mdl) then - -- TODO: should this be a fallback model hook? + hook.Run("PlayerSetModel", self) mdl = self:GetModel() if not checkModel(mdl) then From 3d927419603a8f389a23920980ddb159cb5d4054 Mon Sep 17 00:00:00 2001 From: DaNike Date: Sat, 17 Jan 2026 13:12:32 -0600 Subject: [PATCH 6/7] Remove empty sh_player_custom --- gamemodes/terrortown/gamemode/client/cl_main.lua | 1 - gamemodes/terrortown/gamemode/server/sv_main.lua | 5 ----- gamemodes/terrortown/gamemode/shared/sh_include.lua | 1 - gamemodes/terrortown/gamemode/shared/sh_player_custom.lua | 3 --- 4 files changed, 10 deletions(-) delete mode 100644 gamemodes/terrortown/gamemode/shared/sh_player_custom.lua diff --git a/gamemodes/terrortown/gamemode/client/cl_main.lua b/gamemodes/terrortown/gamemode/client/cl_main.lua index ba33079e13..15bb81e73f 100644 --- a/gamemodes/terrortown/gamemode/client/cl_main.lua +++ b/gamemodes/terrortown/gamemode/client/cl_main.lua @@ -31,7 +31,6 @@ ttt_include("sh_rolelayering") ttt_include("sh_scoring") ttt_include("sh_corpse") ttt_include("sh_player_ext") -ttt_include("sh_player_custom") ttt_include("sh_weaponry") ttt_include("sh_inventory") ttt_include("sh_door") diff --git a/gamemodes/terrortown/gamemode/server/sv_main.lua b/gamemodes/terrortown/gamemode/server/sv_main.lua index 3e3212d423..60f31d39db 100644 --- a/gamemodes/terrortown/gamemode/server/sv_main.lua +++ b/gamemodes/terrortown/gamemode/server/sv_main.lua @@ -43,7 +43,6 @@ ttt_include("sv_armor") ttt_include("sh_armor") ttt_include("sh_player_ext") -ttt_include("sh_player_custom") ttt_include("sv_player_ext") ttt_include("sv_player_custom") @@ -68,10 +67,6 @@ local util = util local hook = hook local playerGetAll = player.GetAll ---- --- @realm server -local cvSelectModelPerRound - --- -- @realm server CreateConVar("ttt_haste_minutes_per_death", "0.5", { FCVAR_NOTIFY, FCVAR_ARCHIVE }) diff --git a/gamemodes/terrortown/gamemode/shared/sh_include.lua b/gamemodes/terrortown/gamemode/shared/sh_include.lua index 3a3db07789..26a7c27f38 100644 --- a/gamemodes/terrortown/gamemode/shared/sh_include.lua +++ b/gamemodes/terrortown/gamemode/shared/sh_include.lua @@ -56,7 +56,6 @@ TTTFiles = { sh_network_sync = { file = "sh_network_sync.lua", on = "shared" }, sh_player_ext = { file = "sh_player_ext.lua", on = "shared" }, sh_playerclass = { file = "sh_playerclass.lua", on = "shared" }, - sh_player_custom = { file = "sh_player_custom.lua", on = "shared" }, sh_printmessage_override = { file = "sh_printmessage_override.lua", on = "shared" }, sh_cvar_handler = { file = "sh_cvar_handler.lua", on = "shared" }, sh_role_module = { file = "sh_role_module.lua", on = "shared" }, diff --git a/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua b/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua deleted file mode 100644 index 33e409a9e6..0000000000 --- a/gamemodes/terrortown/gamemode/shared/sh_player_custom.lua +++ /dev/null @@ -1,3 +0,0 @@ ---- --- logic for managing player customization --- @realm shared From 221c6fea965e184a4d79b7b3fb4dc0e08c5a520d Mon Sep 17 00:00:00 2001 From: DaNike Date: Sat, 18 Apr 2026 06:32:53 -0500 Subject: [PATCH 7/7] spelling --- gamemodes/terrortown/gamemode/server/sv_player_custom.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua index 4eb5663c7a..3ea1acc0e3 100644 --- a/gamemodes/terrortown/gamemode/server/sv_player_custom.lua +++ b/gamemodes/terrortown/gamemode/server/sv_player_custom.lua @@ -206,7 +206,7 @@ end -- @ref https://wiki.facepunch.com/gmod/GM:PlayerSetModel -- @local function GM:PlayerSetModel(ply) - -- The player modes has to be applied here since some player model selectors overwrite + -- The player model has to be applied here since some player model selectors overwrite -- this hook to suppress the TTT2 player models. If the model is assigned elsewhere, it -- breaks with external model selectors. if not IsValid(ply) then