From 91b0aaf1f2d1a82e87efcb0f2fcecb75d28be814 Mon Sep 17 00:00:00 2001 From: Liriosha <57261793+Liriosha@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:27:26 -0400 Subject: [PATCH 1/6] Updated command system to allow multiple commands on one namespace being called at once --- modules/client/commands/init.luau | 110 ++++++++++++++++-------------- modules/server/commands/init.luau | 25 +------ 2 files changed, 61 insertions(+), 74 deletions(-) diff --git a/modules/client/commands/init.luau b/modules/client/commands/init.luau index 0e9693d..f1c1895 100644 --- a/modules/client/commands/init.luau +++ b/modules/client/commands/init.luau @@ -83,41 +83,15 @@ function Commands:Run(command, context, arguments) return true, result end -function Commands:ProcessServer(data, input, inputContext) - return Network:InvokeServer("processCommand", data, input, inputContext) +function Commands:ProcessServer(prefix, commandName, arguments, contextData, input, inputContext) + contextData.prefix=prefix + contextData.commandName=commandName + contextData.arguments=arguments + return Network:InvokeServer("processCommand", contextData, input, inputContext) end -function Commands:ProcessClient(input, inputContext) - -- Check if the input matches a command string, if not then we can ignore it. - if not string.match(input, "^.+/") then - return ProcessResult.Fail, "Not a valid command string." - end - - local arguments = string.split(input, "/") - local prefix = table.remove(arguments, 1) - if not prefix then - return ProcessResult.Fail, "No prefix was found." - end - - -- Remove last argument if empty. - if #arguments[#arguments] == 0 then - table.remove(arguments, #arguments) - end - - -- Find namespace, and get the command. - local commandName - local namespace = Commands.namespaceLookup[string.lower(prefix)] - if not namespace then - -- No namespace with prefix found, default namespace, command should be the 1st split string (command/arguments). - commandName = prefix - namespace = Commands.namespaces.default - elseif arguments[1] and #arguments[1] > 0 then -- Check if there is a 2nd split string, otherwise no command was given. - -- Not default namespace, command should be the 2nd split string (prefix/command/arguments). - commandName = table.remove(arguments, 1) - else - return ProcessResult.Fail, "No command was given." - end +function Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) local command = namespace.commandLookup[string.lower(commandName)] if not command then return ProcessResult.Fail, `Command "{commandName}" was not found.` @@ -138,30 +112,66 @@ function Commands:ProcessClient(input, inputContext) end function Commands:Process(input, inputContext) - local clientResult, clientMessage, contextData = Commands:ProcessClient(input, inputContext) - if clientResult ~= ProcessResult.Success then - -- Only output fail messages if the inputContext wasn't the chat (because you can also send chat messages in chat, duh), - -- Error messages should always be displayed. - if - clientResult == ProcessResult.Error - or (clientResult == ProcessResult.Fail and inputContext ~= InputContext.Chat) - then - Output:append(Output.MessageType.Error, clientMessage or "Unexpected error occured while running command.") - end + -- Check if the input matches a command string, if not then we can ignore it. + if not string.match(input, "^.+/") then + Output:append(Output.MessageType.Error, "Not a valid command string.") + return + end + local arguments = string.split(input, "/") + local prefix = table.remove(arguments, 1) + if not prefix then + Output:append(Output.MessageType.Error, "No prefix was found.") return - elseif clientMessage then - Output:append(Output.MessageType.Success, clientMessage) end - local serverResult, serverMessage = Commands:ProcessServer(contextData, input, inputContext) - if serverResult ~= ProcessResult.Success then - -- We don't check if the ProcessResult was a Fail or Error here, because it should always be an Error. - Output:append(Output.MessageType.Error, serverMessage or "Unexpected error occured while running command.") + -- Remove last argument if empty. + if #arguments[#arguments] == 0 then + table.remove(arguments, #arguments) + end + -- Find namespace, and get the command. + local commandName + local namespace = Commands.namespaceLookup[string.lower(prefix)] + if not namespace then + -- No namespace with prefix found, default namespace, command should be the 1st split string (command/arguments). + commandName = prefix + namespace = Commands.namespaces.default + elseif arguments[1] and #arguments[1] > 0 then -- Check if there is a 2nd split string, otherwise no command was given. + -- Not default namespace, command should be the 2nd split string (prefix/command/arguments). + commandName = table.remove(arguments, 1) + else + Output:append(Output.MessageType.Error, "No command was given.") return - elseif serverMessage then - Output:append(Output.MessageType.Success, serverMessage) + end + commandName = commandName:split(' ') + + for i,commandName in pairs(commandName) do + print(commandName) + local clientResult, clientMessage, contextData = Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) + if clientResult ~= ProcessResult.Success then + -- Only output fail messages if the inputContext wasn't the chat (because you can also send chat messages in chat, duh), + -- Error messages should always be displayed. + if + clientResult == ProcessResult.Error + or (clientResult == ProcessResult.Fail and inputContext ~= InputContext.Chat) + then + Output:append(Output.MessageType.Error, clientMessage or "Unexpected error occured while running command.") + end + + continue + elseif clientMessage then + Output:append(Output.MessageType.Success, clientMessage) + end + + local serverResult, serverMessage = Commands:ProcessServer(prefix, commandName, arguments, contextData, input, inputContext) + if serverResult ~= ProcessResult.Success then + -- We don't check if the ProcessResult was a Fail or Error here, because it should always be an Error. + Output:append(Output.MessageType.Error, serverMessage or "Unexpected error occured while running command.") + continue + elseif serverMessage then + Output:append(Output.MessageType.Success, serverMessage) + end end end diff --git a/modules/server/commands/init.luau b/modules/server/commands/init.luau index 5f03957..9d0f1ab 100644 --- a/modules/server/commands/init.luau +++ b/modules/server/commands/init.luau @@ -79,34 +79,11 @@ function Commands:Run(command, context, arguments) end function Commands:Process(player, data, input, inputContext) - -- Check if the input matches a command string, if not then we can ignore it. - if not string.match(input, "^.+/") then - return ProcessResult.Fail, "Not a valid command string." - end - - local arguments = string.split(input, "/") - local prefix = table.remove(arguments, 1) - if not prefix then - return ProcessResult.Fail, "No prefix was found." - end - - -- Remove last argument if empty. - if #arguments[#arguments] == 0 then - table.remove(arguments, #arguments) - end - - -- Find namespace, and get the command. - local commandName + local prefix,arguments,commandName = data.prefix,data.arguments,data.commandName local namespace = Commands.namespaceLookup[string.lower(prefix)] if not namespace then -- No namespace with prefix found, default namespace, command should be the 1st split string (command/arguments). - commandName = prefix namespace = Commands.namespaces.default - elseif arguments[1] and #arguments[1] > 0 then -- Check if there is a 2nd split string, otherwise no command was given. - -- Not default namespace, command should be the 2nd split string (prefix/command/arguments). - commandName = table.remove(arguments, 1) - else - return ProcessResult.Fail, "No command was given." end local command = namespace.commandLookup[string.lower(commandName)] From 17d176e1345ee24db658fba464265bea571c7bbf Mon Sep 17 00:00:00 2001 From: Liriosha <57261793+Liriosha@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:11:03 -0400 Subject: [PATCH 2/6] Removed a debug print, woops! --- modules/client/commands/init.luau | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/client/commands/init.luau b/modules/client/commands/init.luau index f1c1895..6d53115 100644 --- a/modules/client/commands/init.luau +++ b/modules/client/commands/init.luau @@ -145,9 +145,8 @@ function Commands:Process(input, inputContext) return end commandName = commandName:split(' ') - + for i,commandName in pairs(commandName) do - print(commandName) local clientResult, clientMessage, contextData = Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) if clientResult ~= ProcessResult.Success then -- Only output fail messages if the inputContext wasn't the chat (because you can also send chat messages in chat, duh), From ab53c3520559e37c30bf979f728dfd198b04eb0f Mon Sep 17 00:00:00 2001 From: Liriosha <57261793+Liriosha@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:38:28 -0400 Subject: [PATCH 3/6] Added tracebacks for WaitForChild, Connect, Once and ConnectParallel. --- modules/client/wm/sandbox/rules.luau | 6 +++--- modules/server/wm/sandbox/rules.luau | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/modules/client/wm/sandbox/rules.luau b/modules/client/wm/sandbox/rules.luau index 479d2a0..4557f1c 100644 --- a/modules/client/wm/sandbox/rules.luau +++ b/modules/client/wm/sandbox/rules.luau @@ -40,18 +40,18 @@ function Module.Init(_, Sandbox) local parentThread = coroutine.running() local warnThread + local fullName = GetFullName(self) + local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`,2) if not timeOut and typeof(self) == "Instance" then - local fullName = GetFullName(self) warnThread = task.delay(5, function() -- Incase the WaitForChild thread stopped yielding (could be because the function threw an error), then don't emit the warning if coroutine.status(parentThread) ~= "suspended" then return end - -- TODO: Add stacktrace ManagerCommunication:Send( "warn", - `Infinite yield possible on '{fullName}:WaitForChild("{childName}")'` + traceback ) end) sandbox.Threads[warnThread] = true diff --git a/modules/server/wm/sandbox/rules.luau b/modules/server/wm/sandbox/rules.luau index 9842582..a103971 100644 --- a/modules/server/wm/sandbox/rules.luau +++ b/modules/server/wm/sandbox/rules.luau @@ -95,19 +95,19 @@ function Module.Init(_, Sandbox) local parentThread = coroutine.running() local warnThread + local fullName = GetFullName(self) + local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`,2) if not timeOut and typeof(self) == "Instance" then - local fullName = GetFullName(self) warnThread = task.delay(5, function() -- Incase the WaitForChild thread stopped yielding (could be because the function threw an error), then don't emit the warning if coroutine.status(parentThread) ~= "suspended" then return end - -- TODO: Add stacktrace ManagerCommunication:Send( "warn", sandbox.Owner, - `Infinite yield possible on '{fullName}:WaitForChild("{childName}")'` + traceback ) end) sandbox.Threads[warnThread] = true @@ -150,8 +150,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - -- TODO: Add stacktrace - ManagerCommunication:Send("error", sandbox.Owner, Errors.connectNonFunction()) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) return Connect(unwrap(self)) end @@ -171,8 +170,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - -- TODO: Add stacktrace - ManagerCommunication:Send("error", sandbox.Owner, Errors.connectNonFunction()) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) return ConnectParallel(unwrap(self)) end @@ -192,8 +190,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - -- TODO: Add stacktrace - ManagerCommunication:Send("error", sandbox.Owner, Errors.connectNonFunction()) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) return Once(unwrap(self)) end From 975b8b6f011e313a2eb7918a3f35bbb32d3165cb Mon Sep 17 00:00:00 2001 From: techs-sus <92276908+techs-sus@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:30:19 -0400 Subject: [PATCH 4/6] format changes with stylua --- modules/client/commands/init.luau | 24 ++++++++++++++---------- modules/client/wm/sandbox/rules.luau | 7 ++----- modules/server/commands/init.luau | 2 +- modules/server/wm/sandbox/rules.luau | 14 +++++--------- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/modules/client/commands/init.luau b/modules/client/commands/init.luau index 6d53115..ec7b3b3 100644 --- a/modules/client/commands/init.luau +++ b/modules/client/commands/init.luau @@ -84,13 +84,12 @@ function Commands:Run(command, context, arguments) end function Commands:ProcessServer(prefix, commandName, arguments, contextData, input, inputContext) - contextData.prefix=prefix - contextData.commandName=commandName - contextData.arguments=arguments + contextData.prefix = prefix + contextData.commandName = commandName + contextData.arguments = arguments return Network:InvokeServer("processCommand", contextData, input, inputContext) end - function Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) local command = namespace.commandLookup[string.lower(commandName)] if not command then @@ -144,10 +143,11 @@ function Commands:Process(input, inputContext) Output:append(Output.MessageType.Error, "No command was given.") return end - commandName = commandName:split(' ') + commandName = commandName:split(" ") - for i,commandName in pairs(commandName) do - local clientResult, clientMessage, contextData = Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) + for i, commandName in pairs(commandName) do + local clientResult, clientMessage, contextData = + Commands:ProcessClient(namespace, commandName, arguments, input, inputContext) if clientResult ~= ProcessResult.Success then -- Only output fail messages if the inputContext wasn't the chat (because you can also send chat messages in chat, duh), -- Error messages should always be displayed. @@ -155,15 +155,19 @@ function Commands:Process(input, inputContext) clientResult == ProcessResult.Error or (clientResult == ProcessResult.Fail and inputContext ~= InputContext.Chat) then - Output:append(Output.MessageType.Error, clientMessage or "Unexpected error occured while running command.") + Output:append( + Output.MessageType.Error, + clientMessage or "Unexpected error occured while running command." + ) end continue elseif clientMessage then Output:append(Output.MessageType.Success, clientMessage) end - - local serverResult, serverMessage = Commands:ProcessServer(prefix, commandName, arguments, contextData, input, inputContext) + + local serverResult, serverMessage = + Commands:ProcessServer(prefix, commandName, arguments, contextData, input, inputContext) if serverResult ~= ProcessResult.Success then -- We don't check if the ProcessResult was a Fail or Error here, because it should always be an Error. Output:append(Output.MessageType.Error, serverMessage or "Unexpected error occured while running command.") diff --git a/modules/client/wm/sandbox/rules.luau b/modules/client/wm/sandbox/rules.luau index 4557f1c..5a55b8a 100644 --- a/modules/client/wm/sandbox/rules.luau +++ b/modules/client/wm/sandbox/rules.luau @@ -41,7 +41,7 @@ function Module.Init(_, Sandbox) local parentThread = coroutine.running() local warnThread local fullName = GetFullName(self) - local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`,2) + local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`, 2) if not timeOut and typeof(self) == "Instance" then warnThread = task.delay(5, function() -- Incase the WaitForChild thread stopped yielding (could be because the function threw an error), then don't emit the warning @@ -49,10 +49,7 @@ function Module.Init(_, Sandbox) return end - ManagerCommunication:Send( - "warn", - traceback - ) + ManagerCommunication:Send("warn", traceback) end) sandbox.Threads[warnThread] = true end diff --git a/modules/server/commands/init.luau b/modules/server/commands/init.luau index 9d0f1ab..e2b6863 100644 --- a/modules/server/commands/init.luau +++ b/modules/server/commands/init.luau @@ -79,7 +79,7 @@ function Commands:Run(command, context, arguments) end function Commands:Process(player, data, input, inputContext) - local prefix,arguments,commandName = data.prefix,data.arguments,data.commandName + local prefix, arguments, commandName = data.prefix, data.arguments, data.commandName local namespace = Commands.namespaceLookup[string.lower(prefix)] if not namespace then -- No namespace with prefix found, default namespace, command should be the 1st split string (command/arguments). diff --git a/modules/server/wm/sandbox/rules.luau b/modules/server/wm/sandbox/rules.luau index a103971..31946cf 100644 --- a/modules/server/wm/sandbox/rules.luau +++ b/modules/server/wm/sandbox/rules.luau @@ -96,7 +96,7 @@ function Module.Init(_, Sandbox) local parentThread = coroutine.running() local warnThread local fullName = GetFullName(self) - local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`,2) + local traceback = debug.traceback(`Infinite yield possible on '{fullName}:WaitForChild("{childName}")`, 2) if not timeOut and typeof(self) == "Instance" then warnThread = task.delay(5, function() -- Incase the WaitForChild thread stopped yielding (could be because the function threw an error), then don't emit the warning @@ -104,11 +104,7 @@ function Module.Init(_, Sandbox) return end - ManagerCommunication:Send( - "warn", - sandbox.Owner, - traceback - ) + ManagerCommunication:Send("warn", sandbox.Owner, traceback) end) sandbox.Threads[warnThread] = true end @@ -150,7 +146,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(), 2)) return Connect(unwrap(self)) end @@ -170,7 +166,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(), 2)) return ConnectParallel(unwrap(self)) end @@ -190,7 +186,7 @@ function Module.Init(_, Sandbox) assertTerminated(sandbox) if type(func) ~= "function" then - ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(),2)) + ManagerCommunication:Send("error", sandbox.Owner, debug.traceback(Errors.connectNonFunction(), 2)) return Once(unwrap(self)) end From 2a2e7645cfa5d3d413ef16a12523b9dda51a464c Mon Sep 17 00:00:00 2001 From: techs-sus <92276908+techs-sus@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:41:39 -0400 Subject: [PATCH 5/6] silently ignore messages that are not command like --- modules/client/commands/init.luau | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/client/commands/init.luau b/modules/client/commands/init.luau index ec7b3b3..3fac2b8 100644 --- a/modules/client/commands/init.luau +++ b/modules/client/commands/init.luau @@ -111,9 +111,8 @@ function Commands:ProcessClient(namespace, commandName, arguments, input, inputC end function Commands:Process(input, inputContext) - -- Check if the input matches a command string, if not then we can ignore it. + -- Check if the input matches a command string, if not then we can silently ignore it. if not string.match(input, "^.+/") then - Output:append(Output.MessageType.Error, "Not a valid command string.") return end From 59fc1deeedfdc96b8f7898f653ecb641b0300de2 Mon Sep 17 00:00:00 2001 From: techs-sus <92276908+techs-sus@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:46:32 -0400 Subject: [PATCH 6/6] add back error message for invalid command strings but only in non-chat contexts --- modules/client/commands/init.luau | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/client/commands/init.luau b/modules/client/commands/init.luau index 3fac2b8..0d064fa 100644 --- a/modules/client/commands/init.luau +++ b/modules/client/commands/init.luau @@ -113,6 +113,10 @@ end function Commands:Process(input, inputContext) -- Check if the input matches a command string, if not then we can silently ignore it. if not string.match(input, "^.+/") then + if inputContext ~= InputContext.Chat then + Output:append(Output.MessageType.Error, "Not a valid command string.") + end + return end