Recently, while working on my Shoutcast script, I discovered a limitation in the osDraw commands that made them not usable in some applications. Basically, I needed an old-school text displayer instead. On the Inworldz grid I found a modified version of Furware Text, which works also in Kitely.
Other than this main script that needed tweaks, all the other scripts (like the rezzer) work out of the box. Extensive documentation for Furware Text can be found here and the meshes and scripts can be found on their Github here.
//////////////////////////////////////////////////////////// // // // _cg####ggi_ // // _g00000000000o_ // // ,0000^.omggc.000¡ // // 000 ,F ¯°0#¡000L // // FURWARE text #00 ¡ ¡0 000#00 ^ // // #0 ]O 00 #0 00 #L // // v2.1-RC0 0 #0 0O J0 #0 0O // // Open Source v #00#0¡ #0 0 ]0O // // J000000c_ J0 c00^ // // 0000c^00@NN ,#000 // // `0000@ggg#0000^ // // ^°0000000^^ // // // //////////////////////////////////////////////////////////// ////////// DOCUMENTATION /////////////////////////////////// /* A user's manual as well as documentation for developers is available on the Second Life (R) Wiki at http://wiki.secondlife.com/wiki/FURWARE_text */ ////////// LICENSE ///////////////////////////////////////// /* MIT License Copyright (c) 2010-2014 Ochi Wolfe, FURWARE, the contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ ////////// CONTRIBUTORS //////////////////////////////////// /* Riendra Taselian - Help with InWorldz compatibility. Christine Nyn - Help with InWorldz compatibility. ElectaFox Spark - Fixes for compatibility with OpenSim. Ochi Wolfe - Initial development and general maintenance. */ ////////// CONSTANTS /////////////////////////////////////// // Index offsets and stride size of data in "boxDataList". integer BOX_DATA = 0; // The referenced data has type "string". integer BOX_CONF = 1; // The referenced data has type "string". integer BOX_STATUS = 2; // The referenced data has type "integer". integer BOX_GEOM = 3; // The referenced data has type "rotation". integer BOX_STRIDE = 4; // Tags used to remember the kind of the last performed action. integer ACTION_CONTENT = 1; // Text or style may have been modified. integer ACTION_ADD_BOX = 2; // A virtual text box was added. integer ACTION_DEL_BOX = 3; // A virtual text box was deleted. // String of displayable characters, in the order of the font texture. string CHARS = ""; // Populated in state_entry. ////////// VARIABLES /////////////////////////////////////// // Global status integer primCount; // Used to check for prim count changes. integer lastAction; // The kind of the last performed action. // Per-box data (box = virtual text box) integer boxDataLength; // Length of the "boxDataList". list boxNameList; // List of virtual text boxes' names. list boxDataList; // Strided list of virtual text box data. // Per-set data (set = physical display) integer setCount; // Number of display sets. list setDataList; // Strided list of display set data. // Per-prim data list primLinkList; // List of the prims' link indices list primFillList; // Tells us which faces are showing non-blanks. list primLayerList; // Contains layer assignments for each face. // Memory for templates list tmplNameList; // Names of stored templates. list tmplDataList; // Contents of stored templates. // Global configuration string gConfAll; // Base configuration for all boxes everywhere. string gConfRoot; // Base configuration for root boxes. string gConfNonRoot; // Base configuration for non-root boxes. // Default configuration string dAlign; // Text alignment. string dTrim; // Whitespace trimming at begin/end. string dWrap; // Text wrapping mode. rotation dColor; // Text color. string dFont; // Text font. string dBorder; // Border type around virtual text box. string dTags; // Use of inline style tags (<!...>). integer dForce; // Force refresh, overriding cached face-filled state. // Current configuration string cAlign; // See above. string cTrim; string cWrap; rotation cColor; string cFont; string cBorder; string cTags; integer cForce; // Prim data cache integer cacheIndex = -1; // Index of currently cached prim data. integer cacheDirty; // Bit 0 set = layer dirty, bit 1 set = stat dirty. integer cacheLink; // Link number cache. integer cacheFill; // Face-filled state cache. integer cacheLayer; // Layer cache. // Shared data between refresh() and draw(), // just to avoid lots of parameter passing integer setAuxIndex; integer setColCount; integer setFaceCount; integer boxStatus; ////////// FUNCTIONS /////////////////////////////////////// refresh() { llSetTimerEvent(0.0); integer boxDataIndex; integer step = BOX_STRIDE; integer end = boxDataLength; if (lastAction != ACTION_ADD_BOX) { // Last action is NOT adding a box -> iterate backwards boxDataIndex = boxDataLength - BOX_STRIDE; step = end = -BOX_STRIDE; } while (boxDataIndex != end) { boxStatus = llList2Integer(boxDataList, boxDataIndex + BOX_STATUS); if (boxStatus & 0x1000) { // Dirty // Set the factory default settings. cAlign = dAlign = "left"; cTrim = dTrim = "on"; cWrap = dWrap = "word"; cColor = dColor = <1.0, 1.0, 1.0, 1.0>; cFont = dFont = "b332c61e-d141-4606-b531-ffec40fb4053"; cBorder = dBorder = ""; cTags = dTags = "on"; cForce = dForce = FALSE; integer setIndex = (boxStatus >> 4) & 0xFF; rotation boxGeometry = llList2Rot(boxDataList, boxDataIndex + BOX_GEOM); // Load set data. vector setGeometry = llList2Vector(setDataList, 2*setIndex); setAuxIndex = llList2Integer(setDataList, 2*setIndex+1); // Load box config. set(gConfAll, TRUE, TRUE); if (boxDataIndex / BOX_STRIDE < setCount) { set(gConfRoot, TRUE, TRUE); } else { set(gConfNonRoot, TRUE, TRUE); } set(llList2String(boxDataList, boxDataIndex + BOX_CONF), TRUE, TRUE); integer boxX = (integer)boxGeometry.x; integer boxY = (integer)boxGeometry.y; integer boxW = (integer)boxGeometry.z; integer boxH = (integer)boxGeometry.s; integer boxR = boxX+boxW-1; integer boxB = boxY+boxH-1; setColCount = (integer)setGeometry.x; setFaceCount = (integer)setGeometry.z; integer borderTT = !!(~llSubStringIndex(cBorder, "T")); integer borderRR = !!(~llSubStringIndex(cBorder, "R")); integer borderBB = !!(~llSubStringIndex(cBorder, "B")); integer borderLL = !!(~llSubStringIndex(cBorder, "L")); integer borderT = borderTT || (~llSubStringIndex(cBorder, "t")); integer borderR = borderRR || (~llSubStringIndex(cBorder, "r")); integer borderB = borderBB || (~llSubStringIndex(cBorder, "b")); integer borderL = borderLL || (~llSubStringIndex(cBorder, "l")); integer borderSt; if (~llSubStringIndex(cBorder, "1")) borderSt = 1; else if (~llSubStringIndex(cBorder, "2")) borderSt = 2; if (boxStatus & 0x2000) { // Potentially need to refresh borders? integer i; if (borderT) { for (i = boxX+borderL; i <= boxR-borderR; ++i) draw(235 + 25*borderSt, i, boxY); } if (borderR) { if (borderT) draw(227 - borderRR + 25*borderTT + 3*borderSt, boxR, boxY); for (i = boxY+borderT; i <= boxB-borderB; ++i) draw(262 + borderSt, boxR, i); if (borderB) draw(277 - borderRR - 25*borderBB + 3*borderSt, boxR, boxB); } if (borderB) { for (i = boxR-borderR; i >= boxX+borderL; --i) draw(235 + 25*borderSt, i, boxB); } if (borderL) { if (borderB) draw(275 + borderLL - 25*borderBB + 3*borderSt, boxX, boxB); for (i = boxB-borderB; i >= boxY+borderT; --i) draw(262 + borderSt, boxX, i); if (borderT) draw(225 + borderLL + 25*borderTT + 3*borderSt, boxX, boxY); } } boxX += borderL; boxY += borderT; boxR -= borderR; boxB -= borderB; boxW -= borderL + borderR; boxH -= borderT + borderB; if (boxW > 0 && boxH > 0) { // Prepare data. string text = llList2String(boxDataList, boxDataIndex + BOX_DATA); integer textLength = llStringLength(text); integer textIndex; list part; integer partLength; integer partIndex; integer dataNewLine = TRUE; string token; integer tokenLength; integer boxRow; while (boxRow < boxH) { list line; integer lineLength; list tagPosList; list tagCmdList; integer tagCmdListLength; string tagCommand; integer spacesTail; integer textMode = TRUE; integer rowDone; integer skipRestOfLine; integer lastTokenWasSpace; // Parsing loop. while ((partIndex < partLength || textIndex < textLength) && !rowDone) { if (partIndex >= (partLength-1) && textIndex < textLength) { part = llParseString2List( llGetSubString(text, textIndex, textIndex+boxW), [""], [" ", "\n", "<!", ">"] ); partLength = llGetListLength(part); partIndex = 0; tokenLength = 0; } if (!tokenLength) { token = llList2String(part, partIndex); tokenLength = llStringLength(token); } integer dataAdvance = TRUE; integer tokenLengthPrev = tokenLength; // Text mode takes care of all tokens that are not enclosed by <! ... >. if (textMode) { if ((cTags == "on") && (token == "<!")) { textMode = FALSE; } else if (token == "\n") { dataNewLine = TRUE; rowDone = TRUE; } else if (!skipRestOfLine) { dataNewLine = FALSE; integer tokenIsSpace = (token == " "); if ((cTrim != "off") && tokenIsSpace) { if (lineLength) { ++spacesTail; line += [0]; } } else { integer spaceLeft = boxW - lineLength - spacesTail; integer toAppend; if (tokenLength <= spaceLeft) { toAppend = tokenLength; lineLength += spacesTail; spacesTail = 0; } else { if ((cWrap != "word") || !lastTokenWasSpace) { if (spaceLeft > 0) { toAppend = spaceLeft; lineLength += spacesTail; spacesTail = 0; } } if (cWrap != "none") { rowDone = TRUE; dataAdvance = FALSE; } else { skipRestOfLine = TRUE; } } integer i; for (i = 0; i < toAppend; ++i) { integer charPos = llSubStringIndex(CHARS, llGetSubString(token, i, i)); if (~charPos) line += [charPos]; else line += [68]; // 68 = "?" --tokenLength; ++lineLength; } if ((cWrap != "none") && tokenLength) { token = llGetSubString(token, -tokenLength, -1); } } lastTokenWasSpace = tokenIsSpace; } // Tag mode takes care of all tokens within <! ... >. } else { if (token == ">") { if (dataNewLine) { set(tagCommand, TRUE, FALSE); } else { tagPosList += [lineLength+spacesTail]; tagCmdList += [tagCommand]; ++tagCmdListLength; } tagCommand = ""; textMode = TRUE; } else { tagCommand += token; } } if (dataAdvance) { ++partIndex; tokenLength = 0; } textIndex += (tokenLengthPrev - tokenLength); } integer nextTagListIndex; integer nextTagCharIndex = -1; if (tagCmdListLength) { nextTagCharIndex = llList2Integer(tagPosList, 0); } integer lineIndex; if (cAlign != "left") { integer delta = boxW - lineLength; if (cAlign == "center") delta /= 2; lineIndex -= delta; } integer x; for (x = boxX; x <= boxR; ++x) { integer pos; if (lineIndex >= 0 && lineIndex < lineLength) { while (nextTagListIndex < tagCmdListLength && lineIndex >= nextTagCharIndex) { set(llList2String(tagCmdList, nextTagListIndex++), FALSE, FALSE); nextTagCharIndex = llList2Integer(tagPosList, nextTagListIndex); } pos = llList2Integer(line, lineIndex); } draw(pos, x, boxY + boxRow); ++lineIndex; } while (nextTagListIndex < tagCmdListLength) { set(llList2String(tagCmdList, nextTagListIndex++), FALSE, FALSE); } ++boxRow; } } // Mark box as clean. boxDataList = llListReplaceList( boxDataList, [boxStatus & 0xFFF], boxDataIndex + BOX_STATUS, boxDataIndex + BOX_STATUS ); } boxDataIndex += step; } draw(-1, 0, 0); // Flush cache. lastAction = 0; } draw(integer char, integer x, integer y) { integer newCacheIndex = -1; if (~char) newCacheIndex = setAuxIndex + y*setColCount + x/setFaceCount; if (newCacheIndex != cacheIndex) { if (~cacheIndex) { // Stat cache dirty if (cacheDirty & 2) primFillList = llListReplaceList( primFillList, [cacheFill], cacheIndex, cacheIndex ); // Layer cache dirty if (cacheDirty & 1) primLayerList = llListReplaceList( primLayerList, [cacheLayer], cacheIndex, cacheIndex ); cacheDirty = 0; } cacheIndex = newCacheIndex; if (~cacheIndex) { cacheLink = llList2Integer(primLinkList, cacheIndex); cacheFill = llList2Integer(primFillList, cacheIndex); cacheLayer = llList2Integer(primLayerList, cacheIndex); } } if (!~char) return; integer face = x % setFaceCount; integer layer = (cacheLayer >> 4*face) & 0xF; integer boxLayer = (boxStatus & 0xF); if ((0x10000 << layer) & boxStatus) { // Layer override cacheLayer = (cacheLayer & ~(0xF << 4*face)) | (boxLayer << 4*face); cacheDirty = cacheDirty | 1; } else if (layer != boxLayer) { return; } integer filled = cForce || char; integer statDiffers = (filled ^ ((cacheFill >> face) & 1)); if (filled || statDiffers) { if (statDiffers) { cacheFill = (cacheFill & ~(1 << face)) | (filled << face); cacheDirty = cacheDirty | 2; } llSetLinkPrimitiveParamsFast(cacheLink, [ PRIM_TEXTURE, face, cFont, <0.03125, 0.0625, 0.0>, <0.0390625 * (char % 25), -0.0703125 * (char / 25), 0.0>, 0.0, PRIM_COLOR, face, <cColor.x, cColor.y, cColor.z>, filled * cColor.s ]); } } set(string data, integer startOfLine, integer setDefaults) { if (data == "") return; list parts = llParseString2List(data, [";"], []); integer partCount = llGetListLength(parts); integer part; for (part = 0; part < partCount; ++part) { list tokens = llParseString2List(llList2String(parts, part), ["="], []); if (llGetListLength(tokens) > 1) { string tag = llStringTrim(llList2String(tokens, 0), STRING_TRIM); string valUpper = llStringTrim(llList2String(tokens, 1), STRING_TRIM); string valLower = llToLower(valUpper); integer valIsDef = (valLower == "def"); if (tag == "c") { if (valIsDef) cColor = dColor; else if (valLower == "rand") cColor = <llFrand(1.0), llFrand(1.0), llFrand(1.0), 1.0>; else if (valLower == "white") cColor = <1.0, 1.0, 1.0, 1.0>; else if (valLower == "black") cColor = <0.0, 0.0, 0.0, 1.0>; else if (valLower == "darkred") cColor = <0.5, 0.0, 0.0, 1.0>; else if (valLower == "darkgreen") cColor = <0.0, 0.5, 0.0, 1.0>; else if (valLower == "darkblue") cColor = <0.0, 0.0, 0.5, 1.0>; else if (valLower == "darkcyan") cColor = <0.0, 0.5, 0.5, 1.0>; else if (valLower == "darkmagenta") cColor = <0.5, 0.0, 0.5, 1.0>; else if (valLower == "darkyellow") cColor = <0.5, 0.5, 0.0, 1.0>; else if (valLower == "gray") cColor = <0.5, 0.5, 0.5, 1.0>; else if (valLower == "red") cColor = <1.0, 0.0, 0.0, 1.0>; else if (valLower == "green") cColor = <0.0, 1.0, 0.0, 1.0>; else if (valLower == "blue") cColor = <0.0, 0.0, 1.0, 1.0>; else if (valLower == "cyan") cColor = <0.0, 1.0, 1.0, 1.0>; else if (valLower == "magenta") cColor = <1.0, 0.0, 1.0, 1.0>; else if (valLower == "yellow") cColor = <1.0, 1.0, 0.0, 1.0>; else if (valLower == "silver") cColor = <0.75, 0.75, 0.75, 1.0>; else { valLower = "<" + valLower + ">"; cColor = (rotation)valLower; if (cColor == ZERO_ROTATION) { vector tmp = (vector)valLower; cColor = <tmp.x, tmp.y, tmp.z, 1.0>; } } } else if (tag == "f") { if (valIsDef) cFont = dFont; else cFont = valUpper; } else if (tag == "style") { integer tmplIndex = llListFindList(tmplNameList, [valUpper]); if (~tmplIndex) { set(llList2String(tmplDataList, tmplIndex), startOfLine, setDefaults); } else { llOwnerSay("FW text: Style var \"" + valUpper + "\" not found."); } } else if (startOfLine) { if (tag == "w") { if (valIsDef) cWrap = dWrap; else cWrap = valLower; } else if (tag == "t") { if (valIsDef) cTrim = dTrim; else cTrim = valLower; } else if (tag == "a") { if (valIsDef) cAlign = dAlign; else cAlign = valLower; } else if (tag == "border") { if (valIsDef) cBorder = dBorder; else cBorder = valUpper; } else if (tag == "tags") { if (valIsDef) cTags = dTags; else cTags = valLower; } else if (tag == "force") { if (valIsDef) cForce = dForce; else cForce = (valLower == "on"); } } } } if (setDefaults) { dAlign = cAlign; dTrim = cTrim; dWrap = cWrap; dColor = cColor; dFont = cFont; dBorder = cBorder; dTags = cTags; dForce = cForce; } } setDirty(integer action, integer first, integer last, integer isConf, integer newLayerOverrideBits, integer setIndex, integer withData, string data) { integer i; for (i = first; i <= last; i += BOX_STRIDE) { integer setMatches = TRUE; if (~setIndex) { setMatches = (((llList2Integer(boxDataList, i + BOX_STATUS) >> 4) & 0xFF) == setIndex); } if (setMatches) { integer j; if (withData) { j = i + isConf; boxDataList = llListReplaceList(boxDataList, [data], j, j); } j = i + BOX_STATUS; boxDataList = llListReplaceList( boxDataList, [ llList2Integer(boxDataList, j) | 0x1000 | (isConf * 0x2000) | (newLayerOverrideBits << 16) ], j, j ); } } if (!lastAction) { llSetTimerEvent(0.05); lastAction = action; } } ////////// STATES ////////////////////////////////////////// default { state_entry() { llOwnerSay("FURWARE text is starting..."); CHARS = llBase64ToString( "IGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFS" + "U1RVVldYWVowMTIzNDU2Nzg5Liw6OyE/IifCtGBefistKi9cfCgpW117fTw+" + "PUAkJSYjX8Ogw6HDosOjw6TDpcOmwqrDp8Oww6jDqcOqw6vDrMOtw67Dr8Ox" + "w7LDs8O0w7XDtsO4xZPCusOexaHDn8O5w7rDu8O8w73Dv8W+w4DDgcOCw4PD" + "hMOFw4bDh8OQw4jDicOKw4vDjMONw47Dj8ORw5LDk8OUw5XDlsOYxZLDvsWg" + "w5nDmsObw5zDncW4xb3CosKj4oKswqXCp8K1wqHCv8Kpwq7CscOXw7fCt8Kw" + "wrnCssKzwqvCu8Ks4oCm4oC54oC64oCTwrzCvcK+4oSi4oCiICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgIOKUjOKUrOKUkOKUj+KUs+KUk+KVlOKVpuKV" + "l+KVtuKUgOKVtOKVt+KVuyDilK/ilrLil4Dilrzilrbil4vil5Til5Hil5Xi" + "l4/ilJzilLzilKTilKPilYvilKvilaDilazilaPilbrilIHilbjilILilIPi" + "lZHilL/ihpHihpDihpPihpLihrrihrvimJDimJHimJLilJTilLTilJjilJfi" + "lLvilJvilZrilanilZ0g4pWQIOKVteKVuSDilLfilKDilYLilKjihpXihpTi" + "mYDimYLimqDihLninYzijJbiiKHijJvijJrimarimavimaDimaPimaXimabi" + "moDimoHimoLimoPimoTimoXinJTinJjimLrimLnilqDil77ilqzilq7ilog=" ); // Remember prim count to detect changes later on. primCount = llGetObjectPrimCount(llGetKey())+llGetNumberOfPrims()*!!llGetAttached(); // Determine which link IDs to iterate. integer linkMin; integer linkMax; if (primCount > 1) { linkMin = 1; linkMax = primCount; } // Fetch data from prims. list sets; integer dataLength; while (linkMin <= linkMax) { list tokens = llParseStringKeepNulls(llGetLinkName(linkMin), [":"], []); if (llList2String(tokens, 0) == "FURWARE text mesh") { string setStr = llList2String(tokens, 1); if (setStr != "") { integer set1 = llListFindList(sets, [setStr]); if (!~set1) { set1 = llGetListLength(sets); sets += [setStr]; } primLinkList += [ (set1 << 20) | ((llList2Integer(tokens, 2) & 0x3FF) << 10) | (llList2Integer(tokens, 3) & 0x3FF), llList2Integer(tokens, 4), linkMin ]; ++dataLength; } } ++linkMin; } // Abort if not a single element was found. if (!dataLength) { llOwnerSay("FW text: No text 2.x prims found."); return; } // Sort the data according to their set1-row-col values. primLinkList = llListSort(primLinkList, 3, TRUE); // Parse the gathered data. integer dataIndex; integer setrowcol = llList2Integer(primLinkList, dataIndex); integer set2 = (setrowcol >> 20) & 0x3FF; integer row = (setrowcol >> 10) & 0x3FF; integer nextSet; integer nextRow; do { // set2 integer rowCount; integer colCount; integer faceCount; integer setAuxPtr = dataIndex; do { // Row ++rowCount; integer newColCount; do { // Col ++newColCount; integer link = llList2Integer(primLinkList, 3*dataIndex+2); integer newFaceCount = llList2Integer(primLinkList, 3*dataIndex+1); if (!newFaceCount) newFaceCount = llGetLinkNumberOfSides(link); if (faceCount) { if (newFaceCount != faceCount) { llOwnerSay("FW text: All prims within a set need to have the same number of faces."); setCount = 0; return; } } else { faceCount = newFaceCount; } llSetLinkPrimitiveParamsFast(link, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_TRANSPARENT, <1.0, 1.0, 0.0>, ZERO_VECTOR, 0.0 ]); setrowcol = llList2Integer(primLinkList, 3*(++dataIndex)); nextSet = (setrowcol >> 20) & 0x3FF; nextRow = (setrowcol >> 10) & 0x3FF; } while (dataIndex < dataLength && set2 == nextSet && row == nextRow); if (colCount) { if (newColCount != colCount) { llOwnerSay("FW text: All rows within a set need to have the same number of prims."); setCount = 0; return; } } else { colCount = newColCount; } row = nextRow; } while (dataIndex < dataLength && set2 == nextSet); setDataList += [<colCount, rowCount, faceCount>, setAuxPtr]; boxNameList += [llList2String(sets, set2)]; boxDataList += ["", "", setCount << 4, <0, 0, colCount*faceCount, rowCount>]; boxDataLength += BOX_STRIDE; ++setCount; set2 = nextSet; } while (dataIndex < dataLength); primLinkList = llDeleteSubList(primLinkList, 0, 1); primLinkList = llList2ListStrided(primLinkList, 0, -1, 3); while (dataIndex--) primFillList += [0]; primLayerList = primFillList; llOwnerSay("FURWARE text started with " + (string)setCount + " set(s)."); llMessageLinked(LINK_SET, 0, "", "fw_ready"); } link_message(integer sender, integer num, string str, key id) { if (!setCount) return; if (llGetSubString(id, 0, 2) != "fw_") return; list tokens = llParseStringKeepNulls(id, [":"], []); string token0 = llStringTrim(llList2String(tokens, 0), STRING_TRIM); integer isConf = (token0 == "fw_conf"); if (token0 == "fw_data" || isConf) { if (lastAction && lastAction != ACTION_CONTENT) refresh(); integer tokenCount = llGetListLength(tokens); if (tokenCount < 2) tokenCount = 2; integer t; for (t = 1; t < tokenCount; ++t) { list boxTokens = llParseStringKeepNulls(llList2String(tokens, t), [";"], []); integer boxTokenCount = llGetListLength(boxTokens); string boxToken0 = llStringTrim(llList2String(boxTokens, 0), STRING_TRIM); string boxToken1 = llStringTrim(llList2String(boxTokens, 1), STRING_TRIM); integer first = 0; integer last = boxDataLength - BOX_STRIDE; integer setIndex = -1; if (boxToken0 != "") { first = llListFindList(boxNameList, [boxToken0]); if (!~first) { llOwnerSay(token0 + ": Box \"" + boxToken0 + "\" not found."); jump SkipBox; } first *= BOX_STRIDE; setIndex = (llList2Integer(boxDataList, first + BOX_STATUS) >> 4) & 0xFF; } if (boxToken1 != "") { last = llListFindList(boxNameList, [boxToken1]); if (!~last) { llOwnerSay(token0 + ": Box \"" + boxToken1 + "\" not found."); jump SkipBox; } last *= BOX_STRIDE; integer secondSetIndex = (llList2Integer(boxDataList, last + BOX_STATUS) >> 4) & 0xFF; if ((~setIndex) && setIndex != secondSetIndex) { llOwnerSay(token0 + ": Box sets must match when specifying a range."); jump SkipBox; } setIndex = secondSetIndex; } else if (boxTokenCount == 1 && ~setIndex) { last = first; } setDirty(ACTION_CONTENT, first, last, isConf, 0, setIndex, TRUE, str); @SkipBox; } return; } string token1 = llStringTrim(llList2String(tokens, 1), STRING_TRIM); if (token0 == "fw_var") { if (lastAction && lastAction != ACTION_CONTENT) refresh(); if (token1 == "") { llOwnerSay("fw_var: No variable name given."); return; } integer tmplIndex = llListFindList(tmplNameList, [token1]); if (~tmplIndex) { tmplNameList = llDeleteSubList(tmplNameList, tmplIndex, tmplIndex); tmplDataList = llDeleteSubList(tmplDataList, tmplIndex, tmplIndex); } if (str != "") { tmplNameList += [token1]; tmplDataList += [str]; } setDirty(ACTION_CONTENT, 0, boxDataLength - BOX_STRIDE, TRUE, 0, -1, FALSE, ""); return; } if (token0 == "fw_defaultconf") { if (lastAction && lastAction != ACTION_CONTENT) refresh(); integer first = -1; integer last = boxDataLength - BOX_STRIDE; if (token1 == "") { gConfAll = str; first = 0; } else if (token1 == "root") { gConfRoot = str; first = 0; last = setCount * BOX_STRIDE - BOX_STRIDE; } else if (token1 == "nonroot") { gConfNonRoot = str; first = setCount * BOX_STRIDE; } if (~first) { setDirty(ACTION_CONTENT, first, last, TRUE, 0, -1, FALSE, ""); } return; } string token2 = llStringTrim(llList2String(tokens, 2), STRING_TRIM); string token3 = llStringTrim(llList2String(tokens, 3), STRING_TRIM); string token4 = llStringTrim(llList2String(tokens, 4), STRING_TRIM); if (token0 == "fw_addbox") { if (token1 == "") { llOwnerSay("fw_addbox: Box name cannot be empty."); return; } if (lastAction && lastAction != ACTION_ADD_BOX) refresh(); integer boxNameIndex = llListFindList(boxNameList, [token1]); if (~boxNameIndex) { llOwnerSay("fw_addbox: Box \"" + token1 + "\" already exists."); return; } integer parNameIndex = llListFindList(boxNameList, [token2]); if (!~parNameIndex) { llOwnerSay("fw_addbox: No parent box \"" + token2 + "\"."); return; } integer boxDataIndex = BOX_STRIDE * boxNameIndex; integer parDataIndex = BOX_STRIDE * parNameIndex; integer setIndex = (llList2Integer(boxDataList, parDataIndex + BOX_STATUS) >> 4) & 0xFF; integer layersUsed; integer b; for (b = 0; b < boxDataLength; b += BOX_STRIDE) { if (setIndex == ((llList2Integer(boxDataList, b + BOX_STATUS) >> 4) & 0xFF)) { layersUsed = layersUsed | (1 << (llList2Integer(boxDataList, b + BOX_STATUS) & 0xF)); } } if (layersUsed == 0xFFFF) { llOwnerSay("fw_addbox: No layer available."); return; } integer boxLayer; while (layersUsed & (1 << boxLayer)) ++boxLayer; rotation boxGeom = (rotation)("<" + token3 + ">"); rotation parGeom = llList2Rot(boxDataList, parDataIndex + BOX_GEOM); vector setGeom = llList2Vector(setDataList, 2*setIndex); boxGeom.x += parGeom.x; boxGeom.y += parGeom.y; if (boxGeom.x < 0 || boxGeom.y < 0 || boxGeom.z < 1 || boxGeom.s < 1 || (boxGeom.x + boxGeom.z) > (setGeom.x * setGeom.z) || (boxGeom.y + boxGeom.s) > setGeom.y) { llOwnerSay("fw_addbox: Invalid box geometry."); return; } boxNameList += [token1]; boxDataList += [str, token4, (setIndex << 4) | boxLayer, boxGeom]; setDirty(ACTION_ADD_BOX, boxDataLength, boxDataLength, TRUE, 0xFFFF, -1, FALSE, ""); boxDataLength += BOX_STRIDE; return; } if (token0 == "fw_delbox") { if (lastAction && lastAction != ACTION_DEL_BOX) refresh(); integer tokenCount = llGetListLength(tokens); integer t; for (t = 1; t < tokenCount; ++t) { string boxName = llStringTrim(llList2String(tokens, t), STRING_TRIM); integer boxNameIndex = llListFindList(boxNameList, [boxName]); if (!(~boxNameIndex) || (boxNameIndex < setCount)) { llOwnerSay("fw_delbox: Box \"" + boxName + "\" doesn't exist."); jump SkipDelBox; } integer boxDataIndex = BOX_STRIDE * boxNameIndex; integer boxStatus = llList2Integer(boxDataList, boxDataIndex + BOX_STATUS); setDirty(ACTION_DEL_BOX, 0, boxDataIndex - BOX_STRIDE, TRUE, 1 << (boxStatus & 0xF), (boxStatus >> 4) & 0xFF, FALSE, ""); boxNameList = llDeleteSubList(boxNameList, boxNameIndex, boxNameIndex); boxDataList = llDeleteSubList(boxDataList, boxDataIndex, boxDataIndex + BOX_STRIDE - 1); boxDataLength -= BOX_STRIDE; @SkipDelBox; } return; } if (token0 == "fw_touchquery") { // Need to flush first so that BOX_STATUS only contains the set index and box layer. if (lastAction) refresh(); string reply = "::::::" + str; integer link = (integer)token1; integer face = (integer)token2; list primNameTokens = llParseStringKeepNulls(llGetLinkName(link), [":"], []); if (llList2String(primNameTokens, 0) == "FURWARE text mesh") { integer rootIndex = llListFindList(boxNameList, [llList2String(primNameTokens, 1)]); if (~rootIndex) { integer auxIndex = llListFindList(primLinkList, [link]); if (~auxIndex) { integer layer = (llList2Integer(primLayerList, auxIndex) >> (4*face)) & 0xF; integer boxIndex = llListFindList(boxDataList, [(rootIndex << 4) | layer]); if (~boxIndex) { boxIndex -= BOX_STATUS; vector setGeom = llList2Vector(setDataList, 2*rootIndex); rotation boxGeom = llList2Rot(boxDataList, boxIndex + BOX_GEOM); integer x = llList2Integer(primNameTokens, 3) * (integer)setGeom.z + face; integer y = llList2Integer(primNameTokens, 2); reply = llList2String(boxNameList, boxIndex/BOX_STRIDE) + ":" + (string)(x - (integer)boxGeom.x) + ":" + (string)(y - (integer)boxGeom.y) + ":" + llList2String(boxNameList, rootIndex) + ":" + (string)x + ":" + (string)y + ":" + str; } } } } llMessageLinked(sender, 0, reply, "fw_touchreply"); return; } if (token0 == "fw_flush") { if (lastAction) refresh(); if (str != "") llMessageLinked(sender, 0, str, "fw_done"); return; } if (token0 == "fw_memory") { llOwnerSay((string)llGetFreeMemory() + " bytes free"); return; } if (token0 == "fw_reset") { llResetScript(); } } timer() { refresh(); } changed(integer change) { if (change & CHANGED_LINK) { if (llGetObjectPrimCount(llGetKey())+llGetNumberOfPrims()*!!llGetAttached() != primCount) { llResetScript(); } } } }