Bridge is in a state of development and is not stable.
RAGE:MP to FiveM Bridge
Architecture

Entity IDs, Pools, and netId

How the bridge assigns RAGE:MP-style entity IDs on top of FiveM's global netId, and the cross-side rules that come with it.

Entity IDs, Pools, and netId

The bridge exposes RAGE:MP-style entity IDs on top of FiveM's networking. Because the two engines identify entities very differently, every entity carries three distinct identifiers. Knowing which one to use prevents the most common cross-side bugs.

The three identifiers

IdentifierScopeSame on all sides?Use it for
entity.idLocal to the current side, per poolNoLocal lookups: mp.vehicles.at(id)
entity.remoteIdShared across server + every clientYesRAGE:MP cross-side references: mp.vehicles.atRemoteId(remoteId)
entity.netIdFiveM network ID (raw)YesFiveM interop and native calls

Never send entity.id to the other side

id is a local pool index. The same car is almost always a different id on the server than on each client. To reference an entity across the network, send remoteId (RAGE:MP style) or netId (FiveM style) — never id.

How FiveM identifies entities

FiveM gives every networked entity a netId (network ID), returned by NetworkGetNetworkIdFromEntity. Two facts drive everything below:

  • It is a 16-bit integer, so it ranges from 0 to 65535 (2¹⁶ − 1).
  • It is one global pool shared by every networked entity: vehicles, peds, and objects are all assigned IDs from the same space, so no two networked entities ever hold the same netId at the same time. It is not a separate counter per entity type.

A netId stays fixed for the lifetime of its entity, but the moment that entity is destroyed the number is released back into that one global pool and reused by a future entity of any type. On a long-running server that spawns and removes thousands of entities, these values churn constantly and cycle around the 65535 ceiling.

A player's own server ID (its source) is handed out by a separate registry, but the ped that represents that player is an ordinary entity drawn from this same global pool.

That works for FiveM, but it is nothing like RAGE:MP, where each pool hands out small, ordered IDs starting at 0.

Straight from the FiveM source

On the server, FiveM keeps a single, global pool of entity IDs. The object-ID space is MaxObjectId = (1 << 16) - 1 (so 65535), tracked by one shared bitset, and GetFreeObjectIds hands out the lowest free ID to whichever entity asks — it never branches on the entity type, so vehicles, peds, and objects all compete for the same numbers:

citizen-server-impl/src/state/ServerGameState.cpp (condensed)
// MaxObjectId = (1 << 16) - 1  →  65535, one shared space for every entity
uint16_t id = 1;
for (; id < static_cast<uint16_t>(MaxObjectId); id++)
{
    // m_objectIdsSent / m_objectIdsUsed are single, global bitsets
    if (!m_objectIdsSent.test(id) && !m_objectIdsUsed.test(id))
    {
        data->objectIds.insert(id);
        freeIds.push_back(id);
        m_objectIdsSent.set(id);
        break; // first free ID in the global pool
    }
}

When an entity is destroyed, its ID is returned to that same pool — the source literally comments "we want to make this object ID return to the global pool" — so the numbers churn and get reused across all entity types.

Reference

Sources: ServerGameState::GetFreeObjectIds and MaxObjectId — citizenfx/fivem.

The same wrap-and-reuse pattern shows up in the player (client) registry, which is the clearest place to see the 0xFFFF sentinel and the scan for a free slot. When a client connects, FiveM walks a 16-bit counter, treats 0xFFFF (65535) as the "invalid" value, wraps back to 1 at the top, and scans forward until it finds a free ID:

citizen-server-impl/src/ClientRegistry.cpp
auto incrementId = [this]()
{
    m_curNetId++;

    // 0xFFFF is a sentinel value for 'invalid' ID
    // 0 is not a valid ID
    if (m_curNetId == 0xFFFF)
    {
        m_curNetId = 1;
    }
};

// in case of overflow, ensure no client is currently using said ID
while (m_clientsByNetId[m_curNetId].lock())
{
    incrementId();
}

client->SetNetId(m_curNetId);

This second snippet is the client (player) NetID path, separate from entity IDs, but it shows the same model: a 16-bit space, 0xFFFF reserved as invalid, and a linear search for the next free slot.

Reference

Source: ClientRegistry::HandleConnectingClient — citizenfx/fivem.

How the bridge re-thinks IDs

To stay faithful to RAGE:MP, the bridge decouples its own IDs from netId and keeps netId as an internal/interop detail only.

Per-pool, 0-based, recyclable

Every pool (mp.vehicles, mp.peds, mp.objects, mp.players, …) owns its own ID space, handed out by a shared lowest-free allocator:

  • IDs start at 0 and only count up when no freed ID is available.
  • When an entity is destroyed, its ID is freed and reused by the next entity that needs one — the lowest free value first.
  • IDs stay small and dense instead of growing forever or wrapping at a hard ceiling.
how allocation behaves
// Each pool has its own allocator.
ids.allocate(); // 0
ids.allocate(); // 1
ids.allocate(); // 2
ids.free(1);    // 1 is now reusable
ids.allocate(); // 1  ← lowest free value, reused

Pools are independent, so a vehicle and a ped can both have id === 0 at the same time — exactly like RAGE:MP.

id vs remoteId

  • id — the local pool index above. On the server, id === remoteId. On a client, id is generated locally and will differ from the server's value.
  • remoteId — the shared identity, identical on the server and on every client. Resolve it with atRemoteId() to land on the same entity on the other side.
server.ts
mp.events.add('giveCar', (player) => {
  const veh = mp.vehicles.new(mp.joaat('sultan'), player.position);
  player.call('carReady', [veh.remoteId]); // send remoteId, not id
});
client.ts
mp.events.add('carReady', (remoteId) => {
  const veh = mp.vehicles.atRemoteId(remoteId); // same car, resolved locally
  if (veh) mp.gui.chat.push(`local id ${veh.id}, remoteId ${veh.remoteId}`);
});

netId is still there for FiveM interop

When you call a raw FiveM native, or talk to a non-bridge resource, use entity.netId — the value FiveM understands. For RAGE:MP-style code inside the bridge, prefer remoteId + atRemoteId().

Players

Players follow the same model. On join, the server allocates a 0-based recyclable remoteId for the player and maps it to FiveM's volatile source. That mapping is synced to every client, so player.id and player.remoteId are clean, small, stable numbers rather than the raw source.

Recycling caveats

A freed ID can belong to a different entity later

Because IDs are reused, do not cache an id (or remoteId) and assume it still points to the same entity after that entity was destroyed — a freed value can be reassigned to a brand-new entity. Resolve entities fresh each time, or keep the entity reference itself and guard with mp.<pool>.exists(entity).

65535 is the 'no remote ID' sentinel

A purely local, client-only entity (for example a non-networked mp.objects.new) has no shared identity. Internally its remoteId is set to 65535 (INVALID_REMOTE_ID) to mark "no network identity", so atRemoteId() will not resolve it on another side. This deliberately mirrors FiveM, which reserves the same 0xFFFF (65535) value as its "invalid" sentinel in the server client registry.

Quick reference

  • Local lookup → mp.<pool>.at(entity.id)
  • Cross-side, RAGE:MP style → send remoteId, resolve with mp.<pool>.atRemoteId(remoteId)
  • Cross-side, FiveM interop / natives → use entity.netId
  • Never send entity.id across the network.

On this page