This is an old revision of the document!


Copying Vectors

Copying vectors can be done by right-clicking an object, selecting the Object tab and then clicking the round (C) button next to either position, size or rotation input boxes. Then, by pressing Ctrl+V or using the menu EditPaste.

Printing Texture Information (Including Face Number)

The shortcut to print a texture info is: Ctrl+Alt+Shift+T

This can be accessed from the menu as well, in Viewer 2+, Develop→Rendering→Selected Texture Info. On Viewer 1, it is under Advanced→Rendering→Selected Texture Info.

Decreasing Viewer Loading Times

After some testing, we consider this light-speed rezzing… It really blasts all other methods away.

Some *nix flavors, as well as Windows, have options for creating (mounting) a drive that represents a slice of your RAM. Consider, if you have memory to spare, offshoring the entire cache to RAM. There may be solutions around that would offer safely mounting and unmounting the drive by writing the cache files to harddrive after shutting down the viewer. This would have the effect that all your transactions are made directly from RAM, which would offer an incredible speed compared to hard storage.

If you are on Mac, Snow Leopard onwards, try the following in a Terminal:

imp:~noob$ diskutil erasevolume HFS+ "ramdisk" `hdiutil attach -nomount ram://2097152`

which will create a new ram-disk of 1GB size.

You can check by issuing the command in a terminal 'df -h':

imp:~noob$ df -h
Filesystem      Size   Used  Avail Capacity  Mounted on
/dev/disk2     1.0Gi   12Mi  1.0Gi     2%    /Volumes/ramdisk

Next, configure your cache to write to that folder:

Ramdisk cache for Mac.

  • Based on the former, how about running the entire viewer in RAM. The viewer itself is only a couple 100MB in file-size. The cache, Imprudence's cache is limited to 1GB so you are looking at up to 1.5GB of RAM usage. This may not be too much for newer machines. Just drag and drop the viewer into the newly created ramdisk.

Keep in mind that by using a RAM-disk, whenever you restart your machine, the cache will be purged. If you want to take this to the next level, consider a commercial/third-party solution that swaps out your ramdisk to your hard storage before resetting and then loads it back. You probably could script that as well and spare your money.

Hide the User Interface

Ctrl+Alt+F1

Using the Viewer AO

Third party viewers, in particular, those that have forked off Snowglobe have a built-in AO that can be enabled via the options and allow an avatar to substitute in-world AOs for viewer-based AOs.

The main difference between the two is that in-world AOs rely on scripts that run server-side on the region that the avatar is currently occupying such that they are less prone to lag that would affect the viewer whilst viewer-based AOs run on the game client (the viewer) such that lag affecting the viewer will make the avatar animations lag.

On the other hand, viewer-based AOs reduce the overall lag to the server since the animations are triggered by the viewer directly without having any active scripts running.

Perhaps the easiest way to activate the viewer-based AO is to go to the debug settings by following the menu items Advanced→Debug settings… and then searching for:

  • EnableAORemote,
  • ToolbarVisibleAO

and enabling them by toggling the True radio button.

The toolbar should now have a button named AO Settings that will load up an interface allowing the avatar to configure the viewer-based AO. Perhaps the easiest is to unpack a commercial AO and adjust the notecard following the viewer-AO pattern: the Animation Overrider UI for configuring the viewer-based AO has a New Notecard Template button that will generate a blank template for the viewer-AO.

Once the notecard is configured, it can be dropped on the interface and then by pressing the Reload button, the animations should be loaded on the right.

Solving the Local Typing Issue with Viewer AOs

One of the problems with the built-in viewer AO is that the typing animation will override the "Play typing animation when chatting" setting such that even if the user disables the "Play typing animation when chatting" from the viewer settings and additionally the viewer AO is enabled and has a typing animation selected then the avatar will still animate in local chat when the user types text.

Unfortunately, the viewer AO has been a little neglected over the development cycle such that it is very difficult to get rid of the typing animation: even if the user deletes the typing animation or even the entire [Typing] section from the viewer AO notecard, the viewer will not remove the typing animation.

A workaround is to load the debug settings by following the menu items Advanced→Debug settings… and search for:

  • AODefaultTyping

and then click on the Reset to default button.

If everything was done correctly, then the UI to configure the viewer AO should reflect the change and the Default Typing drop down box should be blank. Now, when typing on local chat, the avatar should not animate anymore.

Reverse-Engineering the Script Message API

Amongst many other debug settings (following the menu items Advanced→Debug settings…) there are two settings, namely:

  • ScriptMessageAPI, and
  • ScriptMessageAPIKey

for which there is no documentation to be found.

In brief, these settings can be traced down to some code within the viewer meant to emit messages on a chosen channel number whenever certain events occur.

The ScriptMessageAPI setting allows the user to define a channel number where the messages will be received and ScriptMessageAPIKey is an encryption key used to encrypt the message being sent. The "encryption" is really a character-by-character XOR between the message generated by the viewer and the key provided by the ScriptMessageAPIKey that is linearly shifted to the length of the message. In order words, the final message that will be sent to the channel defined by the ScriptMessageAPI could be expressed as follows.

Given a key $k$ of any arbitrary length $\gt 0$, and a message $M$ containing characters $m_{i..n}$ then the encrypted message $E$ will consists of corresponding characters $e_{i..n}$ where each character $e_{i}$ can be computed as:

\begin{eqnarray*}
e_{i} &=& m_{i} \oplus k[i_{c} \% length(k)]
\end{eqnarray*}

The encrypted message $E$ is then converted to Base64 and sent on the channel specified by the ScriptMessageAPI debug setting.

The code responsible for encrypting the message can be found in the source inside the file llimprocessing.cpp in Snowglobe-based viewers defined within the script_msg_api function:

void script_msg_api(const std::string& msg)
{
        static const LLCachedControl<S32> channel("ScriptMessageAPI");
        if (!channel) return;
        static const LLCachedControl<std::string> key("ScriptMessageAPIKey");
        std::string str;
        for (size_t i = 0, keysize = key().size(); i != msg.size(); ++i)
                str += msg[i] ^ key()[i%keysize];
        send_chat_from_viewer(LLBase64::encode(reinterpret_cast<const U8*>(str.c_str()), str.size()), CHAT_TYPE_WHISPER, channel);
}

The actual messages being sent by the viewer have the following format:

UUID + ", " + n

where:

  • UUID is the key of the avatar generating the message,
  • n is a single digit number corresponding to the event type.

The single digit number n corresponds to certain events that are spread out through llimprocessing.cpp and some in llviewermessage.cpp.

n Event Example Message
0 instant message bcb678be-cddc-a470-8fd7-845fefb4a853, 0
1 inventory item offered f0e9f375-e2de-9853-4a0b-ebb8a0e9da54, 1
2 teleport lure received 67a469f5-0692-4524-92d9-f3c340af6ba9, 2
3 teleport lure sent 91b45476-d316-4755-b667-9819441e13e6, 3
4 start typing ea7f05b7-aaf1-4e7c-ae08-d96f9af131d5, 4
5 stop typing 62ef45a0-4642-4b9a-ac2b-46a2fb5e79ca, 5
6 message from object 9da01e7f-c82b-4ca1-b7dd-345afc473ff7, 6
7 group message 52bd6c1e-9b04-419c-be5a-f39ce898e9ee, 7

Obtaining the plaintext message from the viewer can be done in LSL. Unfortunately, a well-chosen key is an easy choice to work around the limitations of llBase64ToString; namely, llBase64ToString will display a placeholder (?) for all non-printable characters meaning that performing a XOR between two values that would yield an unprintable character cannot be converted back using llBase64ToString. As a workaround, and knowing that all the messages sent by the "Script Message API" have a character set restricted to [0-9a-f], then the key to be chosen could consist of only upper-case characters ([A-Z]) in order to avoid that the XOR operation produces unprintable characters.

The following script is more or less a verbatim translation of the algorithm implemented in llimprocessing.cpp using Ord and Chr by Pedro Oval for elegance:

///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2022 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
 
// Chr() function, written by Pedro Oval, 2010-05-28
// Auxiliary function UrlCode, which returns the hex code of the given integer
// (which must be in range 0-255) with a "%" prepended.
string UrlCode(integer b)
{
    string hexd = "0123456789ABCDEF";
    return "%" + llGetSubString(hexd, b>>4, b>>4)
               + llGetSubString(hexd, b&15, b&15);
}
 
// Chr function itself, which implements Unicode to UTF-8 conversion and uses
// llUnescapeURL to do its job.
string Chr(integer ord)
{
    if (ord <= 0)
        return "";
    if (ord < 0x80)
        return llUnescapeURL(UrlCode(ord));
    if (ord < 0x800)
        return llUnescapeURL(UrlCode((ord >> 6) | 0xC0)
                            +UrlCode(ord & 0x3F | 0x80));
    if (ord < 0x10000)
        return llUnescapeURL(UrlCode((ord >> 12) | 0xE0)
                            +UrlCode((ord >> 6) & 0x3F | 0x80)
                            +UrlCode(ord & 0x3F | 0x80));
    return llUnescapeURL(UrlCode((ord >> 18) | 0xF0)
                        +UrlCode((ord >> 12) & 0x3F | 0x80)
                        +UrlCode((ord >> 6) & 0x3F | 0x80)
                        +UrlCode(ord & 0x3F | 0x80));
}
 
// Ord() function, written by Pedro Oval, 2010-05-28
// This function works by using llEscapeURL to find the corresponding UTF-8
// string then converts it to the Unicode code. In cases where llEscapeURL
// doesn't help, a combination of llStringToBase64 and llBase64ToInteger
// does the job instead.
integer Ord(string chr)
{
    if (chr == "")
        return 0;
    chr = llGetSubString(chr, 0, 0);
    string hex = llEscapeURL(chr);
    if (llGetSubString(hex, 0, 0) != "%")
    {
        // Regular character - we can't take advantage of llEscapeURL in this case,
        // so we use llStringToBase64/llBase64ToInteger instead.
        return llBase64ToInteger("AAAA" + llStringToBase64(chr));
    }
    integer b = (integer)("0x" + llGetSubString(hex, 1, 2));
    if (b < 194 || b > 244)
        return b;
    if (b < 224)
        return ((b & 0x1F) << 6) | (integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F;
    integer cp;
    if (b < 240)
    {
        cp = (b & 0x0F) << 12;
        cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 6;
        cp += (integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F;
        return cp;
    }
    cp = (b & 0x07) << 18;
    cp += ((integer)("0x" + llGetSubString(hex, 4, 5)) & 0x3F) << 12;
    cp += ((integer)("0x" + llGetSubString(hex, 7, 8)) & 0x3F) << 6;
    cp += (integer)("0x" + llGetSubString(hex, 10, 11)) & 0x3F;
    return cp;
}
 
default
{
    state_entry() {
        // Listen on the channel number specified by the ScriptMessageAPI debug setting.
        llListen(1, "", "", "");
    }
 
    listen(integer channel, string name, key id, string message) {
        // Define the encryption key specified by the ScriptMessageAPIKey debug setting.
        string enc = "ABRACADABRA";
 
        // Dump all listen() parameters.
        llOwnerSay("CHANNEL: " + (string)channel);
        llOwnerSay("NAME: " + name);
        llOwnerSay("KEY: " + (string)id);
 
        // Convert the message from Base64 to its string equivalent.
        message = llBase64ToString(message);
 
        // Reproduce the algorith verbatim as defined by the viewer in the script_msg_api() 
        // function within the llimprocessing.cpp file.
        string output = "";
        integer i;
        integer size;
        for(i = 0, size = llStringLength(enc); i != llStringLength(message); ++i) {
            output += Chr(Ord(llGetSubString(message, i, i)) ^ Ord(llGetSubString(enc, i % size, i % size)));
        }
 
        // Print out the converted message.
        llOwnerSay("MESSAGE: " + output);
    }
}

fuss/metaverse_viewer.1663870630.txt.gz · Last modified: 2022/09/22 18:17 by office

Wizardry and Steamworks

© 2025 Wizardry and Steamworks

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.