Improving ATM Convert invoke

I have a function that converts a certain number, it works fine, but any improvements I should implement?

__CLIENTEVENTSFOLDER.Convert.OnServerInvoke = function(Player:Player?, Number:number?, ATM:Model?)
	if not __CS:HasTag(Player, "JunkCD") then
		task.delay(2,function()
			__CS:RemoveTag(Player, "JunkCD")
		end)
		__CS:AddTag(Player, "JunkCD")
	else
		return nil
	end
	local P = ATM:FindFirstChild("SA", true)
	local err:Sound? = P.error
	if P.Busy.Value == true then
		return nil, "BUSY"
	end
	local KS = tonumber(Number)
	if not KS then
		return false, "NOT A NUMBER"
	end
	if math.floor(KS) ~= KS then
		return false, "NO DECIMALS"
	end
	if KS == 0 then
		return false, "0...?"
	end
	if Player.Character and Player.Character:FindFirstChildOfClass("Humanoid").Health > 0 and Player.Character.ValuesFolder.Ragdolled.Value == false then
		if Player.Stats.KillStreak.Value >= KS and RisytalEngine:Distance(Player.Character, 8, ATM.PrimaryPart) then
			P.Busy.Value = true
			P.process:Play()
			task.wait(2.44)
			if not RisytalEngine:Distance(Player.Character, 8, ATM.PrimaryPart) then
				err:Play()
				return false,"AN ERROR HAS OCCURED"
			end
			if Player.Character:FindFirstChildOfClass("Humanoid").Health <= 0 or Player.Character.ValuesFolder.Ragdolled.Value == true then
				err:Play()
				return false ,"AN ERROR HAS OCCURED"
			end
			P.Withdraw:Play()
			Player.Stats.KillStreak.Value -= KS
			Player.leaderstats.TIX.Value += KS * GetTIXKS(Player)
			P.Busy.Value = false
			return true, "SUCCESS"
		elseif Player.Stats.KillStreak.Value <= KS then
			err:Play()
			return false, "NOT ENOUGH KS"
		end
	end
end

NO!!! NO NO NO…

If the second check for if a player is dead or too far away (the one’s that return “AN ERROR HAS OCCURED”) fails, you never set P.Busy back to false!

Also, if you’re looking for general, concise improvements; there’s a lot that can be done here.

You are not following any general code-writing guidelines, which is fine in practice: only if you never plan on sourcing collaboration. Also, I would heavily recommend using Promises in this sense instead of LuaTuple types.

Here is a completely altered code! Dissect it all you want, this is just the way I would’ve made it if I had the opportunity to.

Server-side
-- atmConvert.server.lua
--!strict
---@diagnostic disable: undefined-global

-- Never prefix your services with "__", this is a convention for private variables and functions inside of a class
-- Also, keep your services at the top of your file so you're not constantly redeclaring them every time you need them
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CollectionService = game:GetService("CollectionService")
-- Use promises instead of LuaTuples, they are much easier to use and read
-- I am using VSCode and Wally, so that is what ReplicatedStorage.Packages refers to
local Promise = require(ReplicatedStorage.Packages.Promise)

-- Keep these at the top too, so you don't have to unnecessarily declare them every time you need them
local clientEvents: Folder = ReplicatedStorage:WaitForChild("ClientEvents")
local convert: RemoteFunction = clientEvents:WaitForChild("Convert")

-- create a type for the ATM model so that you can easily navigate the children with intellisense
type ATMModel = Model & {
    SA: Instance & {
        ErrorSound: Sound,
        ProcessSound: Sound,
        WithdrawSound: Sound
    }
}

type Character = Model & {
    Humanoid: Humanoid,
    Values: Folder & {
        Ragdolled: BoolValue
    }
}

type ExtendedPlayer = Player & {
    Character: Character?,
    Stats: Folder & {
        -- killstreak does not have a capital S
        Killstreak: IntValue
    }
}

-- store the convert.OnServerInvoke function in a local function so that you can easily debug it
local function convertInvoked(player: ExtendedPlayer, amount: number | any, atm: ATMModel)
    -- Use promises so that you don't have to use an ugly LuaTuple type [boolean, string?]
    return Promise.new(function(resolve, reject, onCancel)
        -- do character checking first so that you don't have to unnecessarily do everything else, just to drop it later
        local character = player.Character

        -- also don't use FindFirstChildOfClass, it is slower and Humanoid should never be renamed
        if not character or not character:FindFirstChild("Humanoid") then
            return reject("Your character is not valid!")
        end

        -- never name a folder with the word "Folder" in it, it is redundant
        -- also, for BoolValues or any type of boolean, you do not need to do "Value == true", just "Value" is enough
        -- this is because is already a boolean, it's like doing "if true == true then"
        if not character.Humanoid.Health > 0 or character.Values.Ragdolled.Value then
            return reject("Your character is dead!")
        end

        -- instead of doing an "if not" statement, just do an "if" statement so it takes less space
        -- also, do not shorten service names, the only real exception to this for me is "UserInputService" to "UserInput"
        if CollectionService:HasTag(player, "junkCooldown") then
            return reject("You are on cooldown!")
        end

        CollectionService:AddTag(player, "junkCooldown")
        task.delay(2, function()
            CollectionService:RemoveTag(player, "junkCooldown")
        end)

        -- please give your variables proper names, not "P" or "KS", abbreviations are very horrible to debug
        local unknownChild = atm.SA
        -- never name a variable "err" unless it is an error object
        -- also, give your Instances concise names, do not randomly switch from camelCase to PascalCase

        -- use attributes here instead of Value objects as they clog up the hierarchy
        if unknownChild:GetAttribute("busy") then
            return reject("ATM is busy!")
        end

        -- do not abbreviate "killstreak" as "KS", it is hard to read
        local killstreak: number? = tonumber(amount)
        if not killstreak then
            return reject("Parameter 'amount' is not a number!")
        end

        -- do not reject if not a whole number, just round it down
        killstreak = math.floor(killstreak)

        if killstreak == 0 then
            return reject("Parameter 'amount' expected a non-zero number!")
        end

        -- i don't know if this module belongs to you, but :Distance should be renamed to :GetDistance or :CompareDistance
        if player.Stats.Killstreak.Value < killstreak then
            return reject("You do not have enough killstreak!")
        end

        -- do not run both comparisons for killstreak and distance to the atm, you will get an error which you can't tell which one it is
        if not RisytalEngine:Distance(character, 8, atm.PrimaryPart) then
            return reject("You are too far away from the ATM!")
        end

        unknownChild:SetAttribute("busy", true)
        unknownChild.ProcessSound:Play()

        -- use a connection for this instead of rawdogging a task.wait(sound length)
        -- however, you should make this a variable so you can disconnect it later to avoid a memory leak
        local connection
        connection = unknownChild.Process.Ended:Connect(function()
            -- disconnect it at the top so you don't have to disconnect it in multiple places
            -- declaring something in spot so you don't have to redefine it multiple times is a good practice
            -- it also makes your code way more readable and concise
            connection:Disconnect()

            -- MAKE SURE TO SET BUSY TO FALSE BEFORE YOU REJECT OR RESOLVE!
            -- THIS ERROR IS PRESENT IN THE ORIGINAL SCRIPT
            unknownChild:SetAttribute("busy", false)

            if not RisytalEngine:Distance(character, 8, atm.PrimaryPart) then
                unknownChild.ErrorSound:Play()
                return reject("You went too far away from the ATM!")
            elseif character.Humanoid.Health <= 0 or character.Values.Ragdolled.Value then
                unknownChild.ErrorSound:Play()
                return reject("Your character died!")
            end

            unknownChild.WithdrawSound:Play()
            player.Stats.Killstreak.Value -= killstreak
            -- i do not know what GetTIXKS does, but it should be renamed to something more descriptive
            -- also should be in camelCase intead, PascalCase is for classes
            player.leaderstats.TIX.Value += killstreak * getTIXfromKillstreak(player)

            -- you should resolve with true as there's no extra data to resolve with
            return resolve(true)
        end)

        -- you should disconnect the connection if the promise is cancelled,
        -- there is multiple reasons why a promise may be cancelled, such as the player leaving the game
        onCancel(function()
            connection:Disconnect()
        end)
    end)
end

convert.OnServerInvoke = convertInvoked
Client-side example
-- templateAtmConvertCall.client.lua
--!strict

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local clientEvents: Folder = ReplicatedStorage:WaitForChild("ClientEvents")
local convert: RemoteFunction = clientEvents:WaitForChild("Convert")

-- just a template, i doubt this is what your code actually looks like
local testAmount = 100
local testModel = game.Workspace:WaitForChild("ATM")
-- You should wrap this in a pcall too, just in case something completely unexepected was to occur
local success, response = pcall(convert.InvokeServer, convert, testAmount, testModel)
if not success then
    warn("Failed to invoke convert: " .. response)
    -- use debug.traceback() to get the full stack trace of the error
    -- you use this as pcall will swallow the error and only return the error message
    warn(debug.traceback())
    return
end

response
    :andThen(function(result)
        print("Success: " .. result) -- will print true, this is where you can handle it however
    end)
    :catch(function(err)
        warn("Failed to convert: " .. err)
        -- also use debug.traceback() here to be able to easily debug what all went down
        warn(debug.traceback())
    end)
1 Like

This is atcually module! The function distance compares distance then returns true if close!

function GetClass(Child):string?
	return Child.ClassName
end
function RisytalModules:Distance(Character:Model?, Distance: number?, InitialPart:BasePart?)
	if GetClass(Character) ~= "Model" then warn("Not a Character model!") return nil end
	if type(Distance) ~= "number" then warn(Distance.. " is not a number!") return nil end
	if typeof(InitialPart) ~= "Instance" then warn(InitialPart.. " is not an instance.. somehow..?") return nil end
	assert(Character.HumanoidRootPart, "HRP doesn't exist yet.")
	assert(InitialPart.Position, tostring(InitialPart).. " doesn't have position.")
	if (Character.HumanoidRootPart.Position - InitialPart.Position).Magnitude <= Distance then return true end
end 

This just returns a number depending if player owns a gamepass or not!

How does this work exactly? And how can I use it as a string parameter for text purposes. Like what my old script did.

local txt = script.Parent.Parent.letsgo
local parency = game:GetService("TweenService"):Create(txt, TweenInfo.new(2), {TextTransparency = 1})
script.Parent.MouseButton1Down:Connect(function()
	local lol, text = game:GetService("ReplicatedStorage").Events.Convert:InvokeServer(script.Parent.Parent.TextBox.Text, script.Parent.Parent.ATM.Value)
	if lol == true then
		txt.Text = text
		txt.TextTransparency = 0
		task.delay(1,function()
			parency:Play()
		end)
	else
		txt.TextColor3 = Color3.fromRGB(255, 60, 63)
		txt.Text = text
		txt.TextTransparency = 0
		task.delay(1,function()
			parency:Play()
			game:GetService("TweenService"):Create(txt, TweenInfo.new(2), {TextColor3 = Color3.fromRGB(255, 255, 255)}):Play()
		end)
	end
end)

I would really recommend using assert for this! Also, GetClass is highly unnecessary, use ?:IsA()

-- P.S. unless you're using a class-based metatable, there's no reason to use : instead of .
-- Using : silently passes the `self` parameter, which refers to the calling table
-- However, that is useless information in this case
function RisytalModules.compareDistance(character: Model, distance: number, initialPart: BasePart)
    assert(typeof(character) == "Instance" and character:IsA("Model"), "No character model was given! Instead got " .. typeof(character))
    assert(type(number) == "number", "Expected a number for 'distance', instead got " .. typeof(number))
    assert(typeof(initialPart) == "Instance", "Parameter 'initialPart' was not an Instance! Instead got " .. typeof(initialPart))
    assert(character:FindFirstChild("HumanoidRootPart"), "Either the Character has not finished loading, or the given character was invalid.")
    assert(initialPart.Position, "'initialPart' was missing a Position field, this is likely due to it not being a BasePart. Class received: " .. initialPart.ClassName)
    -- by default this is a truthy statement, so by just returning this will return true or false
    return (character.HumanoidRootPart.Position - initialPart.Position).Magnitude <= distance
end
1 Like

If you’re asking how to use Promises, again it is only a recommendation! Obviously, at the end of the day it is completely your choice.

If you would like to read about Promises and what exactly they do you can refer to resource page made by evaera, the creator of it and a highly skilled individual who used to work for Roblox, and also worked with Uplift Games (creators of Adopt Me) and creator of the RoVer Discord bot!


Promises are a great resource, and honestly I probably didn’t them complete justice in this post, sorry about that evaera!

1 Like

I tried to put informations that would cause the script to return false but it returns an error on the localscript but a warn message on the Promise modulescript.

script.Parent.MouseButton1Down:Connect(function()
	local success, response = pcall(convert.InvokeServer, convert, script.Parent.Parent.TextBox.Text, script.Parent.Parent.ATM.Value)
	if not success then
		warn("Failed to invoke convert: " .. response)
		warn(debug.traceback())
		return
	end

	response
	:andThen(function(result)
		print("Success: " .. result) 
	end)
	:catch(function(err)
		warn("Failed to convert: " .. err)
		warn(debug.traceback())
	end)
end)

Yeah this looks like an error I caused, I did not actually fully test the code I wrote as I just wanted to provide an example of better code-writing; however, try changing it to this to see what exactly the server is responding with!

local success, response = pcall(function()
    local result = convert:InvokeServer(testAmount, testModel)
    print(result) -- put this here to debug what the server is responding with!
    return result
end)```
1 Like

It returns a table!
image
Of which I used to make the text! But I don’t know how would I detect if the operation was a success. (Edit: I could just use if result._status = "Started" then)!

local result
	local success, response = pcall(function()
		result = convert:InvokeServer(script.Parent.Parent.TextBox.Text, script.Parent.Parent.ATM.Value)
		print(result) -- put this here to debug what the server is responding with!
		return result
	end)
	txt.Text = result._values[1]

Yeah, I mean you could, but that in no means is the way it was intended to be used haha!
It is strange how it returns the Promise’s table, but not the methods with it. Try this? :confused:

local success: string, response: string = convert:InvokeServer(testAmount, testModel)
    :tap(print)
    :catch(function(err: string)
        warn("Failed to convert: " .. err)
        warn(debug.traceback())
    end)
    :await()

if success then
    print("Success: " .. response)
end
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.