From ea4e7da92836bd4968ef31761766eb0c7c80ccb0 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:36:57 -0600 Subject: [PATCH 01/11] Add fish curse --- lua/cfc_ulx_commands/curse/effects/fish.lua | 115 ++++++++++++++++++ sound/cfc_ulx_commands/curse/fish/fish.ogg | Bin 0 -> 6655 bytes .../curse/fish/you_know_what_that_means.ogg | Bin 0 -> 12256 bytes 3 files changed, 115 insertions(+) create mode 100644 lua/cfc_ulx_commands/curse/effects/fish.lua create mode 100644 sound/cfc_ulx_commands/curse/fish/fish.ogg create mode 100644 sound/cfc_ulx_commands/curse/fish/you_know_what_that_means.ogg diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua new file mode 100644 index 00000000..e150ee16 --- /dev/null +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -0,0 +1,115 @@ +local EFFECT_NAME = "Fish" +local MAIN_SOUND = "cfc_ulx_commands/curse/fish/fish.ogg" +local SPECIAL_SOUND = "cfc_ulx_commands/curse/fish/you_know_what_that_means.ogg" +local SPECIAL_SOUND_CHANCE = 0.01 +local SPECIAL_SOUND_COOLDOWN = 10 +local START_DELAY = 1.5 + + +-- Create global table +local EFFECT_NAME_LOWER = string.lower( EFFECT_NAME ) +CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] or {} +local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] + +local nextSpecialSoundTime = 0 +local entityMeta = FindMetaTable( "Entity" ) + + +local function getSound( _snd ) + local now = CurTime() + + if now > nextSpecialSoundTime and math.Rand( 0, 1 ) < SPECIAL_SOUND_CHANCE then + nextSpecialSoundTime = now + SPECIAL_SOUND_COOLDOWN + + return SPECIAL_SOUND + end + + return MAIN_SOUND +end + + +CFCUlxCurse.RegisterEffect( { + name = EFFECT_NAME, + + onStart = function( cursedPly ) + if SERVER then return end + + globals.CreateSound = globals.CreateSound or CreateSound + globals.EmitSound = globals.EmitSound or EmitSound + globals.SoundDuration = globals.SoundDuration or SoundDuration + globals.EntityEmitSound = globals.EntityEmitSound or entityMeta.EmitSound + globals.soundPlay = globals.soundPlay or sound.Play + globals.soundPlayFile = globals.soundPlayFile or sound.PlayFile + globals.surfacePlaySound = globals.surfacePlaySound or surface.PlaySound + + -- Stop everything and play the special sound first + RunConsoleCommand( "stopsound" ) + timer.Simple( 0.1, function() -- Account for concmd delay + globals.surfacePlaySound( SPECIAL_SOUND ) + end ) + + CFCUlxCurse.CreateEffectTimer( cursedPly, EFFECT_NAME, "DelayTheFishening", START_DELAY, 0, function() + CreateSound = function( ent, _snd, ... ) + return globals.CreateSound( ent, getSound(), ... ) + end + + EmitSound = function( _snd, ... ) + globals.EmitSound( getSound(), ... ) + end + + SoundDuration = function() + return globals.SoundDuration( getSound() ) + end + + entityMeta.EmitSound = function( self, _snd, ... ) + globals.EntityEmitSound( self, getSound(), ... ) + end + + sound.Play = function( _snd, ... ) + globals.soundPlay( getSound( _snd ), ... ) + end + + sound.PlayFile = function( _snd, ... ) + -- sound.PlayFile() expects the leading "sound/" + globals.soundPlayFile( "sound/" .. getSound(), ... ) + end + + surface.PlaySound = function() + globals.surfacePlaySound( getSound() ) + end + + -- Footsteps are played at the engine level, need to block them and call from lua for the wrap to apply. + CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "PlayerFootstep", "OverrideSound", function( ply, _, _, snd, volume ) + ply:EmitSound( snd, nil, nil, volume ) + + return true + end ) + end ) + end, + + onEnd = function() + if SERVER then return end + + CreateSound = globals.CreateSound + EmitSound = globals.EmitSound + SoundDuration = globals.SoundDuration + entityMeta.EmitSound = globals.EntityEmitSound + sound.Play = globals.soundPlay + sound.PlayFile = globals.soundPlayFile + surface.PlaySound = globals.surfacePlaySound + + RunConsoleCommand( "stopsound" ) + end, + + minDuration = nil, + maxDuration = nil, + onetimeDurationMult = nil, + excludeFromOnetime = true, + incompatibileEffects = {}, + groups = { + "Wrap:Sound", + }, + incompatibleGroups = { + "Wrap:Sound", + }, +} ) diff --git a/sound/cfc_ulx_commands/curse/fish/fish.ogg b/sound/cfc_ulx_commands/curse/fish/fish.ogg new file mode 100644 index 0000000000000000000000000000000000000000..561950a39245608931d105040201af213e05db6d GIT binary patch literal 6655 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 E0j_pK82|tP literal 0 HcmV?d00001 diff --git a/sound/cfc_ulx_commands/curse/fish/you_know_what_that_means.ogg b/sound/cfc_ulx_commands/curse/fish/you_know_what_that_means.ogg new file mode 100644 index 0000000000000000000000000000000000000000..c0bbb2c59ac8a1c5b694beb524c4fd052d7899a1 GIT binary patch literal 12256 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 literal 0 HcmV?d00001 From 8a7e40f8368b7a9072c65ddcb8d893433911ac47 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:52:13 -0600 Subject: [PATCH 02/11] Localize --- lua/cfc_ulx_commands/curse/effects/fish.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index e150ee16..e853f95f 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -14,11 +14,14 @@ local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] local nextSpecialSoundTime = 0 local entityMeta = FindMetaTable( "Entity" ) +local CurTime = CurTime +local mathRand = math.Rand + local function getSound( _snd ) local now = CurTime() - if now > nextSpecialSoundTime and math.Rand( 0, 1 ) < SPECIAL_SOUND_CHANCE then + if now > nextSpecialSoundTime and mathRand( 0, 1 ) < SPECIAL_SOUND_CHANCE then nextSpecialSoundTime = now + SPECIAL_SOUND_COOLDOWN return SPECIAL_SOUND From cff1164fe077ca38ed1a0707c12e2d72e226c13e Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:23:00 -0600 Subject: [PATCH 03/11] Randomly replace words with fish --- lua/cfc_ulx_commands/curse/effects/fish.lua | 47 ++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index e853f95f..89f5146a 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -1,9 +1,13 @@ local EFFECT_NAME = "Fish" + +local START_DELAY = 1.5 + local MAIN_SOUND = "cfc_ulx_commands/curse/fish/fish.ogg" local SPECIAL_SOUND = "cfc_ulx_commands/curse/fish/you_know_what_that_means.ogg" local SPECIAL_SOUND_CHANCE = 0.01 local SPECIAL_SOUND_COOLDOWN = 10 -local START_DELAY = 1.5 + +local WORD_REPLACE_CHANCE = 0.1 -- Create global table @@ -12,10 +16,13 @@ CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] = CFCUlxCurse.EffectGlobals[EFFECT_ local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] local nextSpecialSoundTime = 0 +local wordLookup = { [""] = false, ["\n"] = false, } local entityMeta = FindMetaTable( "Entity" ) local CurTime = CurTime local mathRand = math.Rand +local stringSplit = string.Split +local tableConcat = table.concat local function getSound( _snd ) @@ -30,6 +37,28 @@ local function getSound( _snd ) return MAIN_SOUND end +local function shouldReplaceWord( word ) + local shouldReplace = wordLookup[word] + if shouldReplace ~= nil then return shouldReplace end + + shouldReplace = mathRand( 0, 1 ) < WORD_REPLACE_CHANCE + wordLookup[word] = shouldReplace + + return shouldReplace +end + +local function fishifyText( str ) + local words = stringSplit( str, " " ) + + for i, word in ipairs( words ) do + if shouldReplaceWord( word ) then + words[i] = "fish" + end + end + + return tableConcat( words, " " ) +end + CFCUlxCurse.RegisterEffect( { name = EFFECT_NAME, @@ -37,6 +66,7 @@ CFCUlxCurse.RegisterEffect( { onStart = function( cursedPly ) if SERVER then return end + -- Sound Functions globals.CreateSound = globals.CreateSound or CreateSound globals.EmitSound = globals.EmitSound or EmitSound globals.SoundDuration = globals.SoundDuration or SoundDuration @@ -45,6 +75,9 @@ CFCUlxCurse.RegisterEffect( { globals.soundPlayFile = globals.soundPlayFile or sound.PlayFile globals.surfacePlaySound = globals.surfacePlaySound or surface.PlaySound + -- String Functions + globals.surfaceDrawText = globals.surfaceDrawText or surface.DrawText + -- Stop everything and play the special sound first RunConsoleCommand( "stopsound" ) timer.Simple( 0.1, function() -- Account for concmd delay @@ -52,6 +85,7 @@ CFCUlxCurse.RegisterEffect( { end ) CFCUlxCurse.CreateEffectTimer( cursedPly, EFFECT_NAME, "DelayTheFishening", START_DELAY, 0, function() + -- Sound Wraps CreateSound = function( ent, _snd, ... ) return globals.CreateSound( ent, getSound(), ... ) end @@ -81,6 +115,11 @@ CFCUlxCurse.RegisterEffect( { globals.surfacePlaySound( getSound() ) end + -- String Wraps + surface.DrawText = function( text, ... ) + globals.surfaceDrawText( fishifyText( text ), ... ) + end + -- Footsteps are played at the engine level, need to block them and call from lua for the wrap to apply. CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "PlayerFootstep", "OverrideSound", function( ply, _, _, snd, volume ) ply:EmitSound( snd, nil, nil, volume ) @@ -93,6 +132,7 @@ CFCUlxCurse.RegisterEffect( { onEnd = function() if SERVER then return end + -- Sound Unwrap CreateSound = globals.CreateSound EmitSound = globals.EmitSound SoundDuration = globals.SoundDuration @@ -101,7 +141,12 @@ CFCUlxCurse.RegisterEffect( { sound.PlayFile = globals.soundPlayFile surface.PlaySound = globals.surfacePlaySound + -- String Unwrap + surface.DrawText = globals.surfaceDrawText + RunConsoleCommand( "stopsound" ) + + wordLookup = { [""] = false, ["\n"] = false, } -- Discard the old table so gc can clean it. end, minDuration = nil, From d8d0b0b60928043e511235b8081df29d694ff2c0 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:27:35 -0600 Subject: [PATCH 04/11] Don't gobble up trailing/leading punctuation and newlines --- lua/cfc_ulx_commands/curse/effects/fish.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index 89f5146a..f25c5a5e 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -51,8 +51,11 @@ local function fishifyText( str ) local words = stringSplit( str, " " ) for i, word in ipairs( words ) do - if shouldReplaceWord( word ) then - words[i] = "fish" + local prepend, postpend -- Set aside any trailing or leading newlines or punctuation + prepend, word, postpend = stringMatch( word, "([%c%p]*)([^%c%p]*[%g%c]*[^%c%p]+)([%c%p]*)" ) + + if prepend and shouldReplaceWord( word ) then + words[i] = prepend .. "fish" .. postpend end end From 2e7b0235b99b56cd6cad17c0aa677e22fe8fad88 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:40:11 -0600 Subject: [PATCH 05/11] Respect all punctuation and newlines, not just at the ends --- lua/cfc_ulx_commands/curse/effects/fish.lua | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index f25c5a5e..9fd18494 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -16,12 +16,12 @@ CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] = CFCUlxCurse.EffectGlobals[EFFECT_ local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] local nextSpecialSoundTime = 0 -local wordLookup = { [""] = false, ["\n"] = false, } +local wordLookup = { [""] = false, ["s"] = false, ["S"] = false, } local entityMeta = FindMetaTable( "Entity" ) local CurTime = CurTime local mathRand = math.Rand -local stringSplit = string.Split +local stringGmatch = string.gmatch local tableConcat = table.concat @@ -48,18 +48,25 @@ local function shouldReplaceWord( word ) end local function fishifyText( str ) - local words = stringSplit( str, " " ) - - for i, word in ipairs( words ) do - local prepend, postpend -- Set aside any trailing or leading newlines or punctuation - prepend, word, postpend = stringMatch( word, "([%c%p]*)([^%c%p]*[%g%c]*[^%c%p]+)([%c%p]*)" ) + local fragments = {} + local count = 0 + + -- Replace words separated by punctuation, spaces, and/or control characters. + for nonword, word in stringGmatch( str, "([%c%p%s]*)([^%c%p%s]*)" ) do + if nonword ~= "" then + count = count + 1 + fragments[count] = nonword + end - if prepend and shouldReplaceWord( word ) then - words[i] = prepend .. "fish" .. postpend + if shouldReplaceWord( word ) then + word = "fish" end + + count = count + 1 + fragments[count] = word end - return tableConcat( words, " " ) + return tableConcat( fragments ) -- At scale, table.concat produces considerably less garbage than manual concatenation. end @@ -149,7 +156,7 @@ CFCUlxCurse.RegisterEffect( { RunConsoleCommand( "stopsound" ) - wordLookup = { [""] = false, ["\n"] = false, } -- Discard the old table so gc can clean it. + wordLookup = { [""] = false, ["s"] = false, ["S"] = false, } -- Discard the old table so gc can clean it. end, minDuration = nil, From 3edbbbee7a2769abc249ab35d2f5fe5fbe8c55c4 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:00:43 -0600 Subject: [PATCH 06/11] Preserve case --- lua/cfc_ulx_commands/curse/effects/fish.lua | 42 ++++++++++++++------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index 9fd18494..96a98683 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -16,12 +16,14 @@ CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] = CFCUlxCurse.EffectGlobals[EFFECT_ local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] local nextSpecialSoundTime = 0 -local wordLookup = { [""] = false, ["s"] = false, ["S"] = false, } +local wordLookup = { [""] = "", ["s"] = "s", ["S"] = "S", } local entityMeta = FindMetaTable( "Entity" ) local CurTime = CurTime local mathRand = math.Rand local stringGmatch = string.gmatch +local stringSub = string.sub +local stringUpper = string.upper local tableConcat = table.concat @@ -37,14 +39,32 @@ local function getSound( _snd ) return MAIN_SOUND end -local function shouldReplaceWord( word ) - local shouldReplace = wordLookup[word] - if shouldReplace ~= nil then return shouldReplace end +local function replaceWord( word ) + local replacement = wordLookup[word] + if replacement ~= nil then return replacement end - shouldReplace = mathRand( 0, 1 ) < WORD_REPLACE_CHANCE - wordLookup[word] = shouldReplace + if mathRand( 0, 1 ) < WORD_REPLACE_CHANCE then + local firstChar = stringSub( word, 1, 1 ) - return shouldReplace + -- Maintain capitalization of first letter, and use full caps if the first two are uppercase. + if firstChar == stringUpper( firstChar ) then + local lastChar = stringSub( word, #word, #word ) + + if lastChar == stringUpper( lastChar ) then + replacement = "FISH" + else + replacement = "Fish" + end + else + replacement = "fish" + end + else + replacement = word + end + + wordLookup[word] = replacement + + return replacement end local function fishifyText( str ) @@ -58,12 +78,8 @@ local function fishifyText( str ) fragments[count] = nonword end - if shouldReplaceWord( word ) then - word = "fish" - end - count = count + 1 - fragments[count] = word + fragments[count] = replaceWord( word ) end return tableConcat( fragments ) -- At scale, table.concat produces considerably less garbage than manual concatenation. @@ -156,7 +172,7 @@ CFCUlxCurse.RegisterEffect( { RunConsoleCommand( "stopsound" ) - wordLookup = { [""] = false, ["s"] = false, ["S"] = false, } -- Discard the old table so gc can clean it. + wordLookup = { [""] = "", ["s"] = "s", ["S"] = "S", } -- Discard the old table so gc can clean it. end, minDuration = nil, From 48fed1871591b7fdd9dbb015068811b7b94ef2f1 Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:54:17 -0600 Subject: [PATCH 07/11] Respect language phrases --- lua/cfc_ulx_commands/curse/effects/fish.lua | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index 96a98683..ff3e8c08 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -23,8 +23,10 @@ local CurTime = CurTime local mathRand = math.Rand local stringGmatch = string.gmatch local stringSub = string.sub +local stringFind = string.find local stringUpper = string.upper local tableConcat = table.concat +local languageGetPhrase = CLIENT and language.GetPhrase local function getSound( _snd ) @@ -67,10 +69,26 @@ local function replaceWord( word ) return replacement end -local function fishifyText( str ) +local function fishifyText( str, isRecursing ) local fragments = {} local count = 0 + -- Respect language phrases. + if not isRecursing then + local hashInd = stringFind( str, "#", 1, true ) + local endInd + + while hashInd do + hashInd, endInd = stringFind( str, "#[^%s%c]*", hashInd, false ) + if not hashInd then break end + + local internalPhrase = stringSub( str, hashInd + 1, endInd ) + local newPhrase = fishifyText( languageGetPhrase( internalPhrase ), true ) + + str = stringSub( str, 1, hashInd - 1 ) .. newPhrase .. stringSub( str, endInd + 1 ) + end + end + -- Replace words separated by punctuation, spaces, and/or control characters. for nonword, word in stringGmatch( str, "([%c%p%s]*)([^%c%p%s]*)" ) do if nonword ~= "" then From 8587e17a8d1ffd87a3a011d0084c781e07e0f55c Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:33:09 -0600 Subject: [PATCH 08/11] Also wrap chat/console functions --- lua/cfc_ulx_commands/curse/effects/fish.lua | 103 +++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index ff3e8c08..c8280d3d 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -18,6 +18,7 @@ local globals = CFCUlxCurse.EffectGlobals[EFFECT_NAME_LOWER] local nextSpecialSoundTime = 0 local wordLookup = { [""] = "", ["s"] = "s", ["S"] = "S", } local entityMeta = FindMetaTable( "Entity" ) +local playerMeta = FindMetaTable( "Player" ) local CurTime = CurTime local mathRand = math.Rand @@ -28,6 +29,41 @@ local stringUpper = string.upper local tableConcat = table.concat local languageGetPhrase = CLIENT and language.GetPhrase +local fishifyText +local fishifyTextAny = function( arg ) return fishifyText( tostring( arg ) ) end + +local consoleWrappers = { -- true to leave arg unmodified, otherwise uses a function. + Msg = { + default = fishifyTextAny, + string = fishifyText, + }, + MsgN = { + default = fishifyTextAny, + string = fishifyText, + }, + MsgC = { + default = fishifyTextAny, + string = fishifyText, + table = true, + }, + MsgAll = { + default = fishifyTextAny, + string = fishifyText, + }, + print = { + default = fishifyTextAny, + string = fishifyText, + }, + AddText = { + default = fishifyTextAny, + string = fishifyText, + table = true, + Player = true, + + _container = "chat", + }, +} + local function getSound( _snd ) local now = CurTime() @@ -69,7 +105,7 @@ local function replaceWord( word ) return replacement end -local function fishifyText( str, isRecursing ) +fishifyText = function( str, isRecursing ) local fragments = {} local count = 0 @@ -122,6 +158,19 @@ CFCUlxCurse.RegisterEffect( { -- String Functions globals.surfaceDrawText = globals.surfaceDrawText or surface.DrawText + -- Chat/Console Functions + globals.Msg = globals.Msg or Msg + globals.MsgN = globals.MsgN or MsgN + globals.MsgC = globals.MsgC or MsgC + globals.MsgAll = globals.MsgAll or MsgAll + globals.print = globals.print or print + globals.chatAddText = globals.chatAddText or chat.AddText + + globals.PrintMessage = globals.PrintMessage or PrintMessage + globals.playerChatPrint = globals.playerChatPrint or playerMeta.ChatPrint + globals.playerPrintMessage = globals.playerPrintMessage or playerMeta.PrintMessage + + -- Stop everything and play the special sound first RunConsoleCommand( "stopsound" ) timer.Simple( 0.1, function() -- Account for concmd delay @@ -159,11 +208,49 @@ CFCUlxCurse.RegisterEffect( { globals.surfacePlaySound( getSound() ) end + -- String Wraps surface.DrawText = function( text, ... ) globals.surfaceDrawText( fishifyText( text ), ... ) end + + -- Chat/Console Wraps + for funcName, wrapsByType in pairs( consoleWrappers ) do + local containerName = wrapsByType._container + local container = containerName and _G[containerName] or _G + local ogFunc = globals[( containerName or "" ) .. funcName] + local defaultFunc = wrapsByType.default + + container[funcName] = function( ... ) + local args = { ... } + + for i = 1, #args do + local arg = args[i] + local wrapFunc = wrapsByType[type( arg )] or defaultFunc + + if wrapFunc ~= true then + args[i] = wrapFunc( arg ) + end + end + + return ogFunc( unpack( args ) ) + end + end + + PrintMessage = function( msgType, msg, ... ) + return globals.PrintMessage( msgType, fishifyText( msg ), ... ) + end + + playerMeta.ChatPrint = function( self, msg, ... ) + return globals.playerChatPrint( self, fishifyText( msg ), ... ) + end + + playerMeta.PrintMessage = function( self, msgType, msg, ... ) + return globals.playerPrintMessage( self, msgType, fishifyText( msg, ... ) ) + end + + -- Footsteps are played at the engine level, need to block them and call from lua for the wrap to apply. CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "PlayerFootstep", "OverrideSound", function( ply, _, _, snd, volume ) ply:EmitSound( snd, nil, nil, volume ) @@ -188,8 +275,20 @@ CFCUlxCurse.RegisterEffect( { -- String Unwrap surface.DrawText = globals.surfaceDrawText - RunConsoleCommand( "stopsound" ) + -- Chat/Console Unwrap + Msg = globals.Msg + MsgN = globals.MsgN + MsgC = globals.MsgC + MsgAll = globals.MsgAll + print = globals.print + chat.AddText = globals.chatAddText + + PrintMessage = globals.PrintMessage + playerMeta.ChatPrint = globals.playerChatPrint + playerMeta.PrintMessage = globals.playerPrintMessage + -- Cleanup + RunConsoleCommand( "stopsound" ) wordLookup = { [""] = "", ["s"] = "s", ["S"] = "S", } -- Discard the old table so gc can clean it. end, From c88282876005d67e9efa1ee3b792343f6e98755d Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:49:46 -0600 Subject: [PATCH 09/11] Wrap surface.getTextSize() --- lua/cfc_ulx_commands/curse/effects/fish.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index c8280d3d..a78d4482 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -157,6 +157,7 @@ CFCUlxCurse.RegisterEffect( { -- String Functions globals.surfaceDrawText = globals.surfaceDrawText or surface.DrawText + globals.surfaceGetTextSize = globals.surfaceGetTextSize or surface.GetTextSize -- Chat/Console Functions globals.Msg = globals.Msg or Msg @@ -214,6 +215,10 @@ CFCUlxCurse.RegisterEffect( { globals.surfaceDrawText( fishifyText( text ), ... ) end + surface.GetTextSize = function( text, ... ) + return globals.surfaceGetTextSize( fishifyText( text ), ... ) + end + -- Chat/Console Wraps for funcName, wrapsByType in pairs( consoleWrappers ) do @@ -274,6 +279,7 @@ CFCUlxCurse.RegisterEffect( { -- String Unwrap surface.DrawText = globals.surfaceDrawText + surface.GetTextSize = globals.surfaceGetTextSize -- Chat/Console Unwrap Msg = globals.Msg From 4f15a74c5df92c8765ad31c5e04fd929508cb69e Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:06:57 -0600 Subject: [PATCH 10/11] Add more comments --- lua/cfc_ulx_commands/curse/effects/fish.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index a78d4482..3f4629a3 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -115,9 +115,12 @@ fishifyText = function( str, isRecursing ) local endInd while hashInd do + -- Find the next # and the index of the last non-space, non-control character following it. + -- (i.e. the start and end of the next untranslated language phrase) hashInd, endInd = stringFind( str, "#[^%s%c]*", hashInd, false ) if not hashInd then break end + -- Translate, fishify, and reinsert the phrase. local internalPhrase = stringSub( str, hashInd + 1, endInd ) local newPhrase = fishifyText( languageGetPhrase( internalPhrase ), true ) From 159fcbbd33e520a79d5d0f1b16db76d939617cfb Mon Sep 17 00:00:00 2001 From: legokidlogan <26103433+legokidlogan@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:16:52 -0600 Subject: [PATCH 11/11] Add EntityEmitSound hook --- lua/cfc_ulx_commands/curse/effects/fish.lua | 7 +++++++ lua/cfc_ulx_commands/curse/effects/random_sounds.lua | 7 +++++++ lua/cfc_ulx_commands/curse/effects/sound_shuffle.lua | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/lua/cfc_ulx_commands/curse/effects/fish.lua b/lua/cfc_ulx_commands/curse/effects/fish.lua index 3f4629a3..0ab21bf2 100644 --- a/lua/cfc_ulx_commands/curse/effects/fish.lua +++ b/lua/cfc_ulx_commands/curse/effects/fish.lua @@ -265,6 +265,13 @@ CFCUlxCurse.RegisterEffect( { return true end ) + + -- Covers a lot of cases that ENTITY:EmitSound() doesn't. + CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "EntityEmitSound", "OverrideSound", function( data ) + data.SoundName = getSound( data.SoundName ) + + return true + end ) end ) end, diff --git a/lua/cfc_ulx_commands/curse/effects/random_sounds.lua b/lua/cfc_ulx_commands/curse/effects/random_sounds.lua index 3b6a2731..d9af8425 100644 --- a/lua/cfc_ulx_commands/curse/effects/random_sounds.lua +++ b/lua/cfc_ulx_commands/curse/effects/random_sounds.lua @@ -93,6 +93,13 @@ CFCUlxCurse.RegisterEffect( { return true end ) + + -- Covers a lot of cases that ENTITY:EmitSound() doesn't. + CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "EntityEmitSound", "OverrideSound", function( data ) + data.SoundName = getSound( data.SoundName ) + + return true + end ) end, onEnd = function() diff --git a/lua/cfc_ulx_commands/curse/effects/sound_shuffle.lua b/lua/cfc_ulx_commands/curse/effects/sound_shuffle.lua index 2fbc4499..d2b8bef4 100644 --- a/lua/cfc_ulx_commands/curse/effects/sound_shuffle.lua +++ b/lua/cfc_ulx_commands/curse/effects/sound_shuffle.lua @@ -79,6 +79,13 @@ CFCUlxCurse.RegisterEffect( { return true end ) + + -- Covers a lot of cases that ENTITY:EmitSound() doesn't. + CFCUlxCurse.AddEffectHook( cursedPly, EFFECT_NAME, "EntityEmitSound", "OverrideSound", function( data ) + data.SoundName = getSound( data.SoundName ) + + return true + end ) end, onEnd = function()