Skip to content
lsl.devlsl.devLSL Dev

Working with link numbers

Linking and unlinking renumber prims; avatars sitting and standing renumber the avatar links after the last prim (and turn a lone prim’s link 0 into link 1). So scripts find the prims they drive by name: when they start, and again on CHANGED_LINK. These functions do that lookup with llGetLinkName for your own linkset, and with llGetObjectLinkKey and llKey2Name for another object in the region. For a pool of reusable prims, see Linkset resources below.

// Returns the link numbers of every prim named needle, in ascending order.
list LinksByName(string needle)
{
list needles;
integer link = 1;
integer prims = llGetNumberOfPrims();
for (; link <= prims; ++link)
if (llGetLinkName(link) == needle) needles += link;
return needles;
}
// Returns the link number of a prim named needle, or FALSE (0) if there is none.
// With several matches, the highest link number wins.
integer LinkByName(string needle)
{
integer prims = llGetNumberOfPrims() + 1;
while (--prims)
if (llGetLinkName(prims) == needle) return prims;
return FALSE;
}
// Replaces each prim name in needles with its link number, in one pass over the linkset.
// Names that match no prim are left in the list as strings.
list LinksetList(list needles)
{
integer prims = llGetNumberOfPrims() + 1;
while (--prims)
{
integer pointer = llListFindList(needles, [llGetLinkName(prims)]);
if (~pointer) needles = llListReplaceList(needles, [prims], pointer, pointer);
}
return needles;
}

When you want several prims at once, one loop that checks each name is cheaper than calling LinkByName once per prim. This loop visits every child prim, from the last link down to link 2:

integer Foot;
integer Leg;
integer Torso;
integer Head;
FindLinks()
{
integer link = llGetNumberOfPrims();
for (; link > 1; --link)
{
string linkName = llGetLinkName(link);
if (linkName == "Foot") Foot = link;
else if (linkName == "Leg") Leg = link;
else if (linkName == "Torso") Torso = link;
else if (linkName == "Head") Head = link;
}
}

These variants take the object key of another object in the same region and return prim keys instead of link numbers.

// Returns the keys of every prim in object named needle, in link order.
list ObjectLinksByName(key object, string needle)
{
list needles;
integer link = 1;
integer prims = llGetObjectPrimCount(object);
for (; link <= prims; ++link)
{
key linkKey = llGetObjectLinkKey(object, link);
if (llKey2Name(linkKey) == needle) needles += linkKey;
}
return needles;
}
// Returns the key of a prim in object named needle, or NULL_KEY if there is none.
key ObjectLinkByName(key object, string needle)
{
integer prims = llGetObjectPrimCount(object) + 1;
while (--prims)
{
key linkKey = llGetObjectLinkKey(object, prims);
if (llKey2Name(linkKey) == needle) return linkKey;
}
return NULL_KEY;
}
// Replaces each prim name in needles with the key of the matching prim in object.
list ObjectLinksetList(key object, list needles)
{
integer prims = llGetObjectPrimCount(object) + 1;
while (--prims)
{
key linkKey = llGetObjectLinkKey(object, prims);
integer pointer = llListFindList(needles, [llKey2Name(linkKey)]);
if (~pointer) needles = llListReplaceList(needles, [linkKey], pointer, pointer);
}
return needles;
}
// Returns the keys of the avatars sitting on object.
// Seated avatars take the link numbers after the last prim.
list ObjectLinksetSittingAvatars(key object)
{
list details = llGetObjectDetails(object, [OBJECT_PRIM_COUNT, OBJECT_SIT_COUNT]);
integer prims = llList2Integer(details, 0);
integer sitters = llList2Integer(details, 1);
list agents;
integer index = prims + 1;
integer count = prims + sitters;
for (; index <= count; ++index) agents += llGetObjectLinkKey(object, index);
return agents;
}

The same loop works for another object: count down from llGetObjectPrimCount and read each name with llKey2Name(llGetObjectLinkKey(object, link)).

integer Door;
list Lights;
FindLinks()
{
Door = LinkByName("Door");
Lights = LinksByName("Light");
}
default
{
state_entry()
{
FindLinks();
}
changed(integer change)
{
// Linking, unlinking, and avatars sitting or standing all renumber links.
if (change & CHANGED_LINK) FindLinks();
}
touch_start(integer total_number)
{
if (Door) llSetLinkPrimitiveParamsFast(Door, [PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0]);
}
}

LinksetList fills several variables from one pass:

integer Turret;
integer Barrel;
integer Muzzle;
FindTurret()
{
list links = LinksetList(["Turret", "Barrel", "Muzzle"]);
Turret = llList2Integer(links, 0);
Barrel = llList2Integer(links, 1);
Muzzle = llList2Integer(links, 2);
}
  • LinkByName returns FALSE (0) when nothing matches. Link 0 is never a valid result in a linkset, so test the result with if (link) before using it.
  • LinksetList leaves unmatched names in the list as strings, and llList2Integer reads those as 0, so a missing prim also comes back as 0 there. If two prims share a name, list the name twice to get both links.
  • The scan loop stops before link 1, so it never looks at the root prim. In a single-prim object with no one seated it does nothing.
  • llGetNumberOfPrims counts seated avatars too, so the functions for your own linkset also compare avatar names. To exclude avatars, count with llGetObjectPrimCount(llGetKey()) instead (it does not count seated avatars, and returns 0 in an attachment).
  • Each call reads every name in the linkset. Look links up once, when the script starts and on CHANGED_LINK, and keep the numbers in global variables rather than calling these functions in a timer.
  • The remote variants only work for objects in the same region as the script, because llGetObjectPrimCount, llGetObjectLinkKey and llKey2Name only see objects there.

A linkset resource stores a list of link numbers in linkset data in a compact format, one character per link. The typical use is reusable prims or meshes in a HUD or user interface, for example to render markers, text or a data visualisation: the linkset holds a set of spare prims, and the script takes one out of the resource when it needs it and puts it back when done.

// Creates (or rebuilds) the resource kv from every prim whose name is exactly pattern.
LinksetResourceSetup(string kv, string pattern)
{
string links;
integer link = llGetNumberOfPrims();
for (; link > 0; --link)
if (llGetLinkName(link) == pattern) links = llChar(link) + links;
llLinksetDataWrite(kv, links);
}
// Takes a link number out of the resource, or returns FALSE (0) if it is empty.
integer LinksetResourceReserve(string kv)
{
string links = llLinksetDataRead(kv);
if (links == "") return FALSE;
llLinksetDataWrite(kv, llDeleteSubString(links, 0, 0));
return llOrd(links, 0);
}
// Puts a link number back into the resource.
LinksetResourceRelease(string kv, integer link)
{
llLinksetDataWrite(kv, llChar(link) + llLinksetDataRead(kv));
}
// Applies the same prim parameters to every prim in the resource,
// e.g. to park all the spare prims out of sight in their default state.
LinksetResourceReset(string kv, list reset)
{
list params;
string links = llLinksetDataRead(kv);
integer index = llStringLength(links);
while (index--)
{
params += [PRIM_LINK_TARGET, llOrd(links, index)] + reset;
// Send the batch early only when memory runs low, so each call carries as many prims as fit.
if (llGetFreeMemory() < 1500)
{
llSetLinkPrimitiveParamsFast(LINK_THIS, params);
params = [];
}
}
if (params) llSetLinkPrimitiveParamsFast(LINK_THIS, params);
}
// Returns the link number at position index in the resource, or FALSE (0) if there is none.
integer LinksetResourceLookup(string kv, integer index)
{
return llOrd(llLinksetDataRead(kv), index);
}

These build on the same storage to move link numbers between two resources, typically a pool of free prims and a list of the prims currently on display, so you can clear the display in one call.

// Moves a link number from kvPool to kvUsed and returns it, or FALSE (0) if kvPool is empty.
// Swap the arguments to move one back.
integer LinksetResourceUse(string kvPool, string kvUsed)
{
string links = llLinksetDataRead(kvPool);
if (links == "") return FALSE;
llLinksetDataWrite(kvPool, llDeleteSubString(links, 0, 0));
llLinksetDataWrite(kvUsed, llGetSubString(links, 0, 0) + llLinksetDataRead(kvUsed));
return llOrd(links, 0);
}
// Moves one specific link number from kvUsed back to kvPool.
LinksetResourceFree(string kvPool, string kvUsed, integer link)
{
string used = llLinksetDataRead(kvUsed);
integer at = llSubStringIndex(used, llChar(link));
if (~at)
{
llLinksetDataWrite(kvUsed, llDeleteSubString(used, at, at));
llLinksetDataWrite(kvPool, llChar(link) + llLinksetDataRead(kvPool));
}
}
// Moves every link number in kvUsed back to kvPool.
LinksetResourceFreeAll(string kvPool, string kvUsed)
{
llLinksetDataWrite(kvPool, llLinksetDataRead(kvUsed) + llLinksetDataRead(kvPool));
llLinksetDataWrite(kvUsed, "");
}

A HUD with spare child prims named Marker that shows one marker per nearby avatar. Put the functions above at the top of the same script.

list HIDDEN = [PRIM_POS_LOCAL, ZERO_VECTOR, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0];
ClearMarkers()
{
LinksetResourceReset("markers:used", HIDDEN);
LinksetResourceFreeAll("markers:free", "markers:used");
}
default
{
state_entry()
{
LinksetResourceSetup("markers:free", "Marker");
llLinksetDataWrite("markers:used", "");
LinksetResourceReset("markers:free", HIDDEN);
llSensorRepeat("", NULL_KEY, AGENT, 96.0, PI, 5.0);
}
changed(integer change)
{
if (change & CHANGED_LINK) llResetScript();
}
sensor(integer detected)
{
ClearMarkers();
integer i;
for (; i < detected; ++i)
{
integer link = LinksetResourceUse("markers:free", "markers:used");
if (!link) return; // out of spare prims
// Scale region offsets of up to 96 m down to 0.1 m on the HUD; adjust to your layout.
vector offset = ((llDetectedPos(i) - llGetPos()) / 96.0) * 0.1;
llSetLinkPrimitiveParamsFast(link, [
PRIM_POS_LOCAL, <0.0, offset.x, offset.y>,
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0
]);
}
}
no_sensor()
{
ClearMarkers();
}
}
  • Linking and unlinking renumber prims; avatars sitting and standing renumber the avatar links after the last prim (and turn a lone prim’s link 0 into link 1). Call LinksetResourceSetup again on CHANGED_LINK, or reset the script as the example does.
  • LinksetResourceReserve, LinksetResourceUse and LinksetResourceLookup return FALSE (0) when there is nothing to return, so test the result before using it.
  • Nothing stops you releasing the same link twice, which puts a duplicate in the pool. Release only links you reserved.
  • Each link is one character: one byte of linkset data for links below 128 and two bytes above, so even a pool of the whole linkset costs a few hundred bytes of the 128 KiB store.
  • Every write fires linkset_data in all scripts in the linkset. Give the keys a prefix such as markers: so scripts that handle that event can ignore them.
  • In a single-prim object with no one seated, the only prim is link 0, which a resource cannot hold. The pattern is meant for linksets with spare child prims.

From linkset.lsl in NexiiLSL by Martin Pitt, © 2026, under the MIT licence, at commit 996fbd7.

Changes from the original, for finding links by name:

  • The LinksetScan and ObjectLinksetScan preprocessor macros are written out as a plain loop. The macro’s do … while (--link > 1) also visited link 1 in a single-prim object; the plain for loop does not.
  • The scan loop is shown inside a FindLinks function with global variables.
  • The remote variants call llKey2Name inline instead of storing the name in a variable first.
  • Formatting and comments only otherwise; the functions behave as in the original.

Changes for linkset resources:

  • Link 1 could not be stored. The original stored each link as llChar(link - 1), and llChar only accepts values from 1, so link 1 (the root) never made it into a resource. Links are now stored as llChar(link).
  • Lookup past the end returned 1. llOrd returns 0 for an index outside the string, so the original 1 + llOrd(links, index) returned link 1 instead of FALSE. With links stored directly, LinksetResourceLookup returns 0.
  • Reset batches by free memory, as in the original. The list is sent early when llGetFreeMemory falls below 1500 bytes, then flushed at the end. Batching by memory rather than every N prims lets each call carry far more prims, which made large resets much faster in testing. (Under Mono the figure also counts memory still waiting for garbage collection, so it errs on the side of sending early.) It now passes LINK_THIS instead of link 0; each batch starts with PRIM_LINK_TARGET, which picks the prim the parameters apply to.
  • LinksetResourceFree, which moves one specific link back to the pool, is new; the original listed it as a to-do.
  • Setup counts down link numbers directly instead of an offset counter.
  • LinksetResourceUse copies the first character into the used resource instead of decoding and re-encoding it.