This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
fuss:metaverse_viewer [2022/04/19 08:28] – external edit 127.0.0.1 | fuss:metaverse_viewer [2022/09/22 18:17] – [Solving the Local Typing Issue with Viewer AOs] office | ||
---|---|---|---|
Line 43: | Line 43: | ||
< | < | ||
+ | |||
+ | ====== 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 '' | ||
+ | |||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | and enabling them by toggling the '' | ||
+ | |||
+ | The toolbar should now have a button named '' | ||
+ | |||
+ | Once the notecard is configured, it can be dropped on the interface and then by pressing the '' | ||
+ | |||
+ | ===== 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" | ||
+ | |||
+ | Unfortunately, | ||
+ | |||
+ | A workaround is to load the debug settings by following the menu items '' | ||
+ | |||
+ | * '' | ||
+ | |||
+ | and then click on the '' | ||
+ | |||
+ | {{fuss: | ||
+ | |||
+ | If everything was done correctly, then the UI to configure the viewer AO should reflect the change and the '' | ||
+ | |||
+ | ====== Reverse-Engineering the Script Message API ====== | ||
+ | |||
+ | Amongst many other debug settings (following the menu items '' | ||
+ | |||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | {{fuss: | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | The code responsible for encrypting the message can be found in the source inside the file '' | ||
+ | |||
+ | <code cpp> | ||
+ | void script_msg_api(const std:: | ||
+ | { | ||
+ | static const LLCachedControl< | ||
+ | if (!channel) return; | ||
+ | static const LLCachedControl< | ||
+ | std::string str; | ||
+ | for (size_t i = 0, keysize = key().size(); | ||
+ | str += msg[i] ^ key()[i%keysize]; | ||
+ | send_chat_from_viewer(LLBase64:: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The actual messages being sent by the viewer have the following format: | ||
+ | <code bash> | ||
+ | UUID + ", " + n | ||
+ | </ | ||
+ | where: | ||
+ | * UUID is the key of the avatar generating the message, | ||
+ | * '' | ||
+ | |||
+ | The single digit number '' | ||
+ | |||
+ | ^ n ^ Event ^ Example Message ^ | ||
+ | | 0 | instant message | '' | ||
+ | | 1 | inventory item offered | '' | ||
+ | | 2 | teleport lure received | '' | ||
+ | | 3 | teleport lure sent | '' | ||
+ | | 4 | start typing | '' | ||
+ | | 5 | stop typing | '' | ||
+ | | 6 | message from object | '' | ||
+ | | 7 | group message | '' | ||
+ | |||
+ | Obtaining the plaintext message from the viewer can be done in LSL. Unfortunately, | ||
+ | |||
+ | {{fuss: | ||
+ | |||
+ | The following script is more or less a verbatim translation of the algorithm implemented in '' | ||
+ | <code lsl2> | ||
+ | /////////////////////////////////////////////////////////////////////////// | ||
+ | // 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 " | ||
+ | string UrlCode(integer b) | ||
+ | { | ||
+ | string hexd = " | ||
+ | return " | ||
+ | + llGetSubString(hexd, | ||
+ | } | ||
+ | |||
+ | // 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' | ||
+ | // does the job instead. | ||
+ | integer Ord(string chr) | ||
+ | { | ||
+ | if (chr == "" | ||
+ | return 0; | ||
+ | chr = llGetSubString(chr, | ||
+ | string hex = llEscapeURL(chr); | ||
+ | if (llGetSubString(hex, | ||
+ | { | ||
+ | // Regular character - we can't take advantage of llEscapeURL in this case, | ||
+ | // so we use llStringToBase64/ | ||
+ | return llBase64ToInteger(" | ||
+ | } | ||
+ | integer b = (integer)(" | ||
+ | if (b < 194 || b > 244) | ||
+ | return b; | ||
+ | if (b < 224) | ||
+ | return ((b & 0x1F) << 6) | (integer)(" | ||
+ | integer cp; | ||
+ | if (b < 240) | ||
+ | { | ||
+ | cp = (b & 0x0F) << 12; | ||
+ | cp += ((integer)(" | ||
+ | cp += (integer)(" | ||
+ | return cp; | ||
+ | } | ||
+ | cp = (b & 0x07) << 18; | ||
+ | cp += ((integer)(" | ||
+ | cp += ((integer)(" | ||
+ | cp += (integer)(" | ||
+ | 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 = " | ||
+ | | ||
+ | // Dump all listen() parameters. | ||
+ | llOwnerSay(" | ||
+ | llOwnerSay(" | ||
+ | llOwnerSay(" | ||
+ | | ||
+ | // 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); | ||
+ | output += Chr(Ord(llGetSubString(message, | ||
+ | } | ||
+ | | ||
+ | // Print out the converted message. | ||
+ | llOwnerSay(" | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ |