Build a teleport command from scratch
In the last lesson you built a command from an empty folder and watched it fire. This time the command does something you can see in the world: type /tp spawn and your character jumps to a saved spot. Along the way you will meet the one rule every real server lives by, the server decides and the client moves, and you will write a per-player cooldown so nobody can spam it. By the end you type /tp garage, your character is at the garage, and a second /tp too soon is refused.
Watch a similar script
Why it exists
A teleport command is the first thing almost every server builder writes, because it is the fastest way to get somewhere without driving. Admins use it to reach a problem player. Builders use it to test a custom interior without spawning a car. It is also the cleanest possible example of a script that has to reach across the network: the typed command lands on one machine, but the thing it changes (your character's position) lives on another. Learning where each half runs here is the whole point, because every command that grants something real (a heal, an item, a repair) has exactly this shape.
Vocabulary
- location
- A named spot in the world, stored as three numbers: x, y, and z coordinates. We keep a small list of them in a config file so a player types a name, not raw numbers.
- cooldown
- A minimum wait before an action is allowed again. Here it stops a player from teleporting over and over. We store the moment of the last teleport and compare it to now.
- server authority
- The rule that the server, not the player's PC, makes every decision that matters. The player can edit their own machine, so the server must own the rulebook.
The plan
You will build a resource named qu_teleport. The qu_ prefix is the practice naming habit from earlier lessons, so your own scripts never clash with a downloaded one. It has four files: a manifest that lists them, a config file holding your named locations and the cooldown length, a server file that owns the decision, and a client file that moves the character.
Here is the notional machine, the picture of what the running code actually does. The server reads all the files top to bottom when the resource starts, and it builds one private list: a cooldown table that starts empty. When a player types /tp garage, that request lands on the server. The server looks up garage in the config, checks the player is not on cooldown, stamps the time, and sends a single message to that one player saying "move to these coordinates." The client receives that message and does the one thing only it can do: it sets the character's position. The server never moves the character itself, because the character only exists on the player's PC. The client never owns the cooldown, because a player can edit anything on their own machine. Success looks like your character standing at the saved spot, with the move logged in two consoles.
What happens behind the scenes
Here is the folder you are about to create and the path one /tp garage takes through it.
resources/qu_teleport/
fxmanifest.lua lists the files (loads first)
config.lua named locations + cooldown length (shared)
server.lua looks up the name, checks cooldown, decides (server side)
client.lua moves the character (client side)player types /tp garage (in chat)
|
v
SERVER is "garage" a known location?
|
+-- no --> print "unknown location", stop
|
+-- yes --> on cooldown?
|
+-- yes --> print "wait Ns", stop
|
+-- no --> stamp os.time() in the table
send coords to that one player
|
v
CLIENT receives coords --> SetEntityCoords(ped, x, y, z, ...) --> movedThe shape to memorize: the server holds the rulebook (the location list and the cooldown) and makes the decision, the client only applies the move it was told to apply.
Build it
Follow the folder names, file names, and code exactly. Each step has you predict the result before you see it. Once it all works, the next section takes it apart line by line.
Make the qu_teleport folder
Inside your server's resources folder, create this folder:
resources/qu_teleportThen create four empty files inside it so the layout looks exactly like this:
resources/qu_teleport/
fxmanifest.lua
config.lua
server.lua
client.luaWrite fxmanifest.lua
The manifest is the file FiveM reads first to learn which other files belong to this resource and which side each one runs on. Open fxmanifest.lua and paste this:
fx_version 'cerulean'
game 'gta5'
shared_script 'config.lua'
server_script 'server.lua'
client_script 'client.lua'server_script runs on the server machine. client_script runs on every player's PC. shared_script is loaded on both sides, which is exactly what config.lua needs to be, because the server reads the location list to decide and (in your own later versions) the client may read it too. Do not add a lua54 'yes' line: as of June 2025 it is deprecated and ignored, because Lua 5.4 is now the only runtime.
Write config.lua
Predict before you write it: this file does not teleport anyone or print anything. So what do you expect to see in the console when the resource restarts after you add it? (Answer: nothing. It only defines values for the other files to read.)
Open config.lua and paste this:
Config = {}
Config.Cooldown = 10
Config.Locations = {
spawn = { x = -1037.7, y = -2738.0, z = 20.2 },
garage = { x = -337.2, y = -136.8, z = 39.0 },
pier = { x = -1850.1, y = -1231.7, z = 13.0 },
}Write server.lua
Predict before you write it: when the resource restarts, does this file teleport you or print anything yet? (Answer: no. It only registers the command and creates an empty cooldown table. Nothing fires until you type /tp.)
Open server.lua and paste this:
local cooldown = {}
RegisterCommand('tp', function(src, args)
local name = args[1]
local place = Config.Locations[name]
if not place then
print('[qu_teleport] unknown location: ' .. tostring(name))
return
end
local now = os.time()
if cooldown[src] and now - cooldown[src] < Config.Cooldown then
local wait = Config.Cooldown - (now - cooldown[src])
print('[qu_teleport] denied, ' .. src .. ' must wait ' .. wait .. 's')
return
end
cooldown[src] = now
TriggerClientEvent('qu_teleport:go', src, place.x, place.y, place.z)
print('[qu_teleport] sent ' .. GetPlayerName(src) .. ' to ' .. name)
end, true)The cooldown table sits at the top of the file, outside the command, on purpose. We will explain why in the next section. The true at the end is the restricted flag you met last lesson.
Write client.lua
Predict before you write it: this file does not run on restart either. What single thing do you expect it to do, and only when the server tells it? (Answer: set your character's position to the coordinates the server sent.)
Open client.lua and paste this:
RegisterNetEvent('qu_teleport:go', function(x, y, z)
SetEntityCoords(PlayerPedId(), x, y, z, false, false, false, true)
print('[qu_teleport] moved to ' .. x .. ', ' .. y .. ', ' .. z)
end)RegisterNetEvent is required here because this event arrives from across the network. Without it the message is rejected and nothing moves.
Ensure it and test it
Open server.cfg and add this line:
ensure qu_teleportSave it. Now open the txAdmin Live Console (the box at the bottom of the txAdmin web panel where you type server commands) and run this to start the resource right now:
ensure qu_teleportUse ensure, not restart, the first time. The server.cfg line is only read when the whole server boots, so on an already-running server the resource has not started yet. ensure starts it if it is stopped and restarts it if it is already running, so it always works.
Now join your own server, wait until your character has fully spawned in, open the in-game chat box (press the chat key, by default T), and run the command with a location name:
/tp garageThe server line lands in the txAdmin Live Console, and the client line lands in your F8 console (press F8 in game). Your character is now at the garage.
Now run /tp pier again immediately, before the cooldown passes. The server refuses and no move is sent:
Finally, type a name that is not in the config to see the rejection path:
/tp moonHow it works
You ran it and your character moved. Now take the code apart so next time you are deciding, not copying. There are four ideas here, one per piece, and each one lands on a specific side of the network.
The config table: names instead of numbers
Config = {}
Config.Cooldown = 10
Config.Locations = {
spawn = { x = -1037.7, y = -2738.0, z = 20.2 },
garage = { x = -337.2, y = -136.8, z = 39.0 },
pier = { x = -1850.1, y = -1231.7, z = 13.0 },
}This file runs on both sides (it is a shared_script), but for now only the server reads it. Config is a Lua table, the same kind of lookup list you used for the cooldown in the heal capstone. The difference is what it stores. Config.Locations is a table whose keys are names (spawn, garage, pier) and whose values are themselves tiny tables of three numbers: x, y, and z. Those three numbers are a point in the GTA world, the same coordinates the map editor shows you. Pulling these out into a config file is a habit, not a requirement: it means a player types a friendly word and you change a spot, or add a new one, without ever touching the logic in server.lua. Config.Cooldown = 10 is the wait in seconds, named once so you can tune it in one place.
Reading the typed name and rejecting unknowns
RegisterCommand('tp', function(src, args)
local name = args[1]
local place = Config.Locations[name]
if not place then
print('[qu_teleport] unknown location: ' .. tostring(name))
return
endThis runs on the server. RegisterCommand is the FiveM native you met last lesson: it wires the typed word tp to this block of code and runs the block later, when someone types /tp. FiveM hands the block two values you use here. src is the source, the server id of the player who ran it (the console is source 0). args is a table of the words typed after the command, so for /tp garage the table holds garage at position 1. args[1] reads that first word, and we name it name.
Config.Locations[name] is a table lookup: it asks the config "do you have an entry under this key?" If the player typed garage, it hands back that location's { x, y, z } table. If they typed something not in the list, like moon, there is no entry, so the lookup hands back nil, Lua's word for "nothing here." The if not place then line reads in plain English as "if we found nothing," and on a miss it prints a message and returns, which stops the function right there so none of the teleport code below ever runs. tostring(name) guards the print: if the player typed /tp with no name at all, name is nil, and tostring turns that nil into the readable text nil instead of crashing the join. Rejecting bad input first, then returning, is the cleanest way to handle a decision: deal with the wrong case, leave, and let the good case be the rest of the function.
The per-player cooldown
local now = os.time()
if cooldown[src] and now - cooldown[src] < Config.Cooldown then
local wait = Config.Cooldown - (now - cooldown[src])
print('[qu_teleport] denied, ' .. src .. ' must wait ' .. wait .. 's')
return
end
cooldown[src] = nowThis runs on the server too. os.time() is a built-in Lua function that returns the current time as a whole number of seconds, and subtracting two readings tells you how many seconds passed between them. cooldown is the table at the top of the file: its key is a player's src, its value is the moment that player last teleported. cooldown[src] is that last reading, or nil if this player has never teleported. The if reads as "if this player has teleported before, and fewer than Config.Cooldown seconds have passed since then, refuse." The cooldown[src] and part guards the first-ever teleport: if the value is nil, the whole condition is false and the move is allowed. On a refusal we print how many seconds are left and return. If the code gets past the if, the request passed, so cooldown[src] = now stamps this moment, and the next attempt is measured from here.
Why does the table live at the top of server.lua, outside the command? Because it has to survive between calls. A local declared inside the handler would be rebuilt empty every single time the command fired, so it would forget the last teleport and never block anything. Declared once at the top, it persists for the life of the resource and remembers every player.
Sending the move and applying it on the client
TriggerClientEvent('qu_teleport:go', src, place.x, place.y, place.z)
print('[qu_teleport] sent ' .. GetPlayerName(src) .. ' to ' .. name)
end, true)The first line runs on the server. TriggerClientEvent is a FiveM native the server uses to send a message to one specific player. The first argument is the event name, qu_teleport:go. The second is the target, src, the id of the one player who asked, so the message travels only to them and not to everyone on the server. After the target you can pass extra values along, and here we pass the three coordinates the server looked up. GetPlayerName(src) returns that player's name for the log line. The trailing true is the restricted flag: only the server console and identities you grant an ace permission may run this command. Set it to false and any connected player could fire it from chat, which is exactly how servers get exploited, so true is the safe default for anything that changes game state.
RegisterNetEvent('qu_teleport:go', function(x, y, z)
SetEntityCoords(PlayerPedId(), x, y, z, false, false, false, true)
print('[qu_teleport] moved to ' .. x .. ', ' .. y .. ', ' .. z)
end)This runs on the client, on the player's own PC. RegisterNetEvent listens for the qu_teleport:go message; it is required for any event that crosses the network, or the message is silently rejected. The three coordinates the server sent arrive as the handler's arguments x, y, z. PlayerPedId() is a client native that returns the handle of your character, the body the camera follows. SetEntityCoords is the client native that actually moves an entity, and its arguments are, in order: the entity (your ped), the three coordinates, and then three booleans you leave false (they control extra movement options you do not need), and a final true that clears the area around the landing spot so you do not appear stuck inside another object. Both PlayerPedId and SetEntityCoords are client natives: they only exist on the client, because the character lives on the player's machine. This is server-decides, client-acts made real. The server said where; the client did the moving.
Why does the server send coordinates to the client instead of just moving the character itself?
Because the character (the ped) is a client-side entity: it only exists on the player's own PC, so the natives that move it, PlayerPedId and SetEntityCoords, only exist on the client. The server has no ped to move. So the server does the part it can do, deciding whether the teleport is allowed and which coordinates to use, then sends those coordinates to the one player with TriggerClientEvent. The client receives them and runs SetEntityCoords. The general rule: every decision lives on the server, and only the visible effect lives on the client.
Common mistakes
| Symptom | Fix |
|---|---|
attempt to call a nil value (global 'SetEntityCoords') | You put SetEntityCoords (or PlayerPedId) in server.lua. They are client natives and only exist on the client. Move them into client.lua, inside the RegisterNetEvent handler, and let the server only send the coordinates. |
/tp garage prints 'unknown location: garage' even though garage is in the config | The name does not match the config key exactly. Lua keys are case sensitive, so Garage and garage are different, and a typo or trailing space breaks the lookup. Compare the typed word to the key in Config.Locations character for character. |
The cooldown never blocks: you can /tp again instantly every time | The cooldown table was declared inside the command handler (local cooldown = {} below RegisterCommand), so it is rebuilt empty on every call and forgets the last teleport. Move local cooldown = {} to the top of server.lua, outside the handler, so it persists between calls. |
'end' expected (near <eof>) | The RegisterCommand function has no closing end, or an if block is missing its end. Every function and every if must finish with its own end. Pair each one by eye: the command needs end, true) at the very bottom. |
The server prints 'sent ... to garage' but the character never moves and F8 shows nothing | The client is missing RegisterNetEvent for qu_teleport:go, or the event name does not match on both sides. A networked event is rejected unless its exact name was registered with RegisterNetEvent on the receiving side. Compare the two strings character for character. |
Recap
- Build a four-file resource: an fxmanifest.lua that lists the files, a shared config, a server file, and a client file.
- Keep named locations and a cooldown length in config.lua so you change a spot or a number in one place, not in the logic.
- Read the typed location name from args[1] on the server, look it up in a table, and reject an unknown name with an early return.
- Enforce a per-player cooldown with a server-side table and os.time, declared at the top of the file so it persists between calls.
- Send coordinates to one player with TriggerClientEvent and apply the move on the client with PlayerPedId and SetEntityCoords, because the ped is client side.
Try it yourself
You just built a real teleport that respects server authority and a cooldown, the same spine behind heal, give, and repair commands. Next you take this further into something with state that has to be stored and trusted: the next lesson, "Build a banking script," has the server hold a balance per player, so a deposit on the client can never lie about how much money you have.