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.
Find links by name
Section titled “Find links by name”// 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;}A scan loop of your own
Section titled “A scan loop of your own”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; }}Another object
Section titled “Another object”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);}Notes and limits
Section titled “Notes and limits”LinkByNamereturnsFALSE(0) when nothing matches. Link 0 is never a valid result in a linkset, so test the result withif (link)before using it.LinksetListleaves unmatched names in the list as strings, andllList2Integerreads 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.
llGetNumberOfPrimscounts seated avatars too, so the functions for your own linkset also compare avatar names. To exclude avatars, count withllGetObjectPrimCount(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,llGetObjectLinkKeyandllKey2Nameonly see objects there.
Linkset resources
Section titled “Linkset resources”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.
The functions
Section titled “The functions”// 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);}Two resources: free and in use
Section titled “Two resources: free and in use”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, "");}Using a resource
Section titled “Using a resource”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(); }}Resource notes and limits
Section titled “Resource notes and limits”- 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
LinksetResourceSetupagain onCHANGED_LINK, or reset the script as the example does. LinksetResourceReserve,LinksetResourceUseandLinksetResourceLookupreturnFALSE(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_datain all scripts in the linkset. Give the keys a prefix such asmarkers: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.
Source and licence
Section titled “Source and licence”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
LinksetScanandObjectLinksetScanpreprocessor macros are written out as a plain loop. The macro’sdo … while (--link > 1)also visited link 1 in a single-prim object; the plainforloop does not. - The scan loop is shown inside a
FindLinksfunction with global variables. - The remote variants call
llKey2Nameinline 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), andllCharonly accepts values from 1, so link 1 (the root) never made it into a resource. Links are now stored asllChar(link). - Lookup past the end returned 1.
llOrdreturns 0 for an index outside the string, so the original1 + llOrd(links, index)returned link 1 instead ofFALSE. With links stored directly,LinksetResourceLookupreturns 0. - Reset batches by free memory, as in the original. The list is sent early when
llGetFreeMemoryfalls 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 passesLINK_THISinstead of link 0; each batch starts withPRIM_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.
LinksetResourceUsecopies the first character into the used resource instead of decoding and re-encoding it.