Skip to content

Port a script

Most scripts need no changes. This tutorial takes a modern script that uses several newer APIs and rewrites it for Nova.

The original

-- ServerScriptService/Interact.server.lua
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local REACH = 12

local function getCharacter(player: Player): Model?
    return player.Character
end

local function findTarget(player: Player)
    local character = getCharacter(player)
    if not character then
        return nil
    end

    local head = character:FindFirstChild("Head") :: BasePart?
    if not head then
        return nil
    end

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = { character }

    local result = workspace:Raycast(head.Position, head.CFrame.LookVector * REACH, params)
    if result and result.Instance:HasTag("Interactable") then
        return result.Instance
    end

    return nil
end

Players.PlayerAdded:Connect(function(player)
    player.Chatted:Connect(function(message)
        if message == "/use" then
            local target = findTarget(player)
            if target then
                print(player.Name, "used", target:GetFullName())
            end
        end
    end)
end)

What is non-Nova here?

  • RaycastParams and Workspace:Raycast are missing.
  • RaycastResult is missing.
  • player.Chatted exists in Nova? Check the Player page. It does, as a legacy chat event, but TextChatService places may not use it. For the tutorial, keep it.

Everything else (Players, PlayerAdded, Character, BasePart, HasTag, Luau types, :: casts, Enum) works.

The port

-- ServerScriptService/Interact.server.lua
local Players = game:GetService("Players")

local REACH = 12

local function findTarget(player: Player)
    local character = player.Character
    if not character then
        return nil
    end

    local head = character:FindFirstChild("Head")
    if not head or not head:IsA("BasePart") then
        return nil
    end

    local ray = Ray.new(head.Position, head.CFrame.LookVector * REACH)
    local part = workspace:FindPartOnRayWithIgnoreList(ray, { character }, false, true)

    if part and part:HasTag("Interactable") then
        return part
    end

    return nil
end

Players.PlayerAdded:Connect(function(player)
    player.Chatted:Connect(function(message)
        if message == "/use" then
            local target = findTarget(player)
            if target then
                print(player.Name, "used", target:GetFullName())
            end
        end
    end)
end)

Changes:

  • RaycastParams → an explicit ignore list passed to FindPartOnRayWithIgnoreList.
  • Workspace:Raycast → FindPartOnRayWithIgnoreList.
  • result.Instance → the returned part (the first return value).
  • result.Position/Normal → the second and third return values if needed.
  • Type annotations are optional; they are supported, so you can keep them.

A pattern for keeping both paths

If you maintain one codebase for Roblox and Nova, isolate the platform difference:

local function cast(origin: Vector3, direction: Vector3, ignore: { Instance })
    if workspace.Raycast then
        local params = RaycastParams.new()
        params.FilterType = Enum.RaycastFilterType.Exclude
        params.FilterDescendantsInstances = ignore

        local result = workspace:Raycast(origin, direction, params)
        if result then
            return {
                Instance = result.Instance,
                Position = result.Position,
                Normal = result.Normal,
                Material = result.Material,
            }
        end
        return nil
    end

    local ray = Ray.new(origin, direction)
    local part, position, normal, material =
        workspace:FindPartOnRayWithIgnoreList(ray, ignore, false, true)

    if part then
        return {
            Instance = part,
            Position = position,
            Normal = normal,
            Material = material,
        }
    end
    return nil
end

workspace.Raycast is nil on Nova, so the branch is safe.

Other common rewrites

task and legacy waits

-- Before
wait(1)
spawn(function() ... end)

-- After (preferred)
task.wait(1)
task.spawn(function() ... end)

Legacy wait/spawn still work, but task is the modern scheduler API.

Deprecated property names

-- Before
sound.Pitch = 1.2
sound.MinDistance = 10
sound.MaxDistance = 100

-- After
sound.PlaybackSpeed = 1.2
sound.RollOffMinDistance = 10
sound.RollOffMaxDistance = 100

Lowercase legacy methods

-- Before
local part = workspace:findFirstChild("Part")
part:remove()

-- After
local part = workspace:FindFirstChild("Part")
if part then
    part:Destroy()
end

GetChildren and filtering

local parts = {}
for _, child in ipairs(workspace:GetChildren()) do
    if child:IsA("BasePart") then
        table.insert(parts, child)
    end
end

Attributes and tags

part:SetAttribute("Owner", player.UserId)
part:AddTag("Interactable")

print(part:GetAttribute("Owner"))
print(part:HasTag("Interactable"))

Test the port

  1. Put the script in ServerScriptService.
  2. Add a tagged part in front of the character.
  3. Run Play Solo and type /use in chat.
  4. Confirm the Output window shows the part name.
  5. Repeat in a local server with two clients.

If nothing happens, check:

  • The part has the Interactable tag.
  • The ray direction is not zero.
  • The ignore list contains the character.
  • FindPartOnRayWithIgnoreList is called on workspace, not on the part.

Next