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?
RaycastParamsandWorkspace:Raycastare missing.RaycastResultis missing.player.Chattedexists in Nova? Check thePlayerpage. 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 toFindPartOnRayWithIgnoreList.Workspace:Raycast→FindPartOnRayWithIgnoreList.result.Instance→ the returnedpart(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¶
- Put the script in
ServerScriptService. - Add a tagged part in front of the character.
- Run Play Solo and type
/usein chat. - Confirm the Output window shows the part name.
- Repeat in a local server with two clients.
If nothing happens, check:
- The part has the
Interactabletag. - The ray direction is not zero.
- The ignore list contains the character.
FindPartOnRayWithIgnoreListis called onworkspace, not on the part.