ChangeLog

17 July 2015 - thanks Talia Draconia

  • Added check for "&" and "=" characters used in mark names.
  • Added "@free" command.

15 July 2015

  • Removed linking, doubled drive capacity and code cleanups.

9th June 2014

  • Code maintenance.
  • Fixed linking.

6th January 2014

  • Added linking feature, extended the default storage to any amount of storage primitives.

2nd March 2014

  • Code cleanup and added search function.

About

Jump! is a chat-based grid-wide teleport system that is designed to make teleports convenient.

Video

Features

  • save locations permanently in SL
  • teleport to locations
  • remove locations
  • automatically updates history
  • works grid-wide and within parcels
  • no obtrusive HUD, attaches to HUD but remains transparent
  • your data is saved on your device
  • lock and unlock to prevent detaching

Quickstart

Unpack the item and wear it. If you have RLV enabled, the Jump! object will attach to you and lock. You can unlock the item at any time by writing:

@unlock

in local chat. Or, you can lock it back by typing @lock.

Locations that you set with "@mark" will be persistently stored and can be retrieved with:

@list

which will display a list of locations for every stored bookmark.

Now you are ready to set some landmarks - simply teleport to some simulator you like and type:

@mark name 

where "name" is some name you want to give to that location. When you feel like going back, you just type the name with "@" prefixed:

@name

that will teleport you to the location you have previously set.

To remove a previous landmark, type:

@unmark name

where "name" is the name you have given to your stored bookmark.

Locking

The following commands will lock and lock [WaS] Jump! from the attachment point.

@lock
@unlock

Bookmarks

Command Description
@mark <name> will mark the current location with name
@unmark <name> will unmark a location marked with name
@<name> will teleport you to the location marked with name
@list will list all previous bookmarks

Example

First list all previously stored locations:

@list

Mark the current location, let's call it "saloon":

@mark saloon

At some later time, teleport back to the location marked "saloon":

@saloon

Now, let's remove the "saloon" location:

@unmark saloon

Limitations

  • Since data is stored in-world, inside the primitive, there is only a limited amount of locations available.

Code

jump.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                            CONFIGURATION                              //
///////////////////////////////////////////////////////////////////////////
 
// The channel on which Jump! listens - you can change this to some number
// greater or equal to zero and then send commands by prefixing the channel
// number. For example: /5 @mark new in case you set the channel to 5.
integer LISTEN_CHANNEL = 0;
 
///////////////////////////////////////////////////////////////////////////
//                              INTERNALS                                //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                      Second Life Constants                            //
//                   PrimFS™ Physical Parameters.                        //
///////////////////////////////////////////////////////////////////////////
 
// The maximum number of bytes that we can store in a description is set 
// here to 127 bytes which is the current number of bytes that you can 
// store in a primitive's description in Second Life.
integer BYTES_PER_SECTOR = 127;
// Primitives can have a zero-length description but that can only be 
// accomplished by setting the description to the empty string using a 
// script. Otherwise, any primitive created with actually have this 
// string in the description indicating that no description is set.
string UNFORMATTED_MARKER = "(No Description)";
 
///////////////////////////////////////////////////////////////////////////
//                      Wizardry and Steamworks                          //
//                         API @ grimore.org                             //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasCompoundToList(integer T, list compound) {
    integer S = llGetListEntryType(compound, 0);
    if(S != TYPE_VECTOR && S != TYPE_ROTATION) return [];
    list a = llParseString2List((string)compound, ["<", ",", ">"], []);
    compound = [];
    do {
        if(T == TYPE_FLOAT) 
            compound += llList2Float(a, 0);
        if(T == TYPE_INTEGER) 
            compound += llList2Integer(a, 0);
        if(T == TYPE_STRING) 
            compound += llList2String(
                llParseString2List((string)a, [" "], [])
            , 0);
        a = llDeleteSubList(a, 0, 0);
    } while(llGetListLength(a) != 0);
    return compound;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(data) == 0) return k + "=" + v;
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llListReplaceList(
            llParseString2List(data, ["&"], []), 
            [ k + "=" + v ], 
        i, i), 
    "&");
    return data + "&" + k + "=" + v;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueDelete(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []), 
            0, -1, 2
        ), 
    [ k ]);
    if(i != -1) return llDumpList2String(
        llDeleteSubList(
            llParseString2List(data, ["&"], []),
        i, i), 
    "&");
    return data;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasKeyValueGetKeys(string data) {
    return llList2ListStrided(
        llParseString2List(
            data, 
            ["&", "="], 
            []
        ), 
        0, 
        -1, 
        2
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueEncode(list data) {
    integer i = llGetListLength(data);
    if (i % 2 != 0 || i == 0) return "";
    --i;
    do {
        data = llListInsertList(
            llDeleteSubList(
                data, 
                i-1, 
                i
            ),
            [ llList2String(data, i-1) + "=" + llList2String(data, i) ], 
            i-1
        );
        i -= 2;
    } while(i > 0);
    return llDumpList2String(data, "&");
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// returns the size of a linked primitive description
string wasGetLinkDescription(integer link) {
    string d = (string)llGetLinkPrimitiveParams(link, [PRIM_DESC]);
    if(d == UNFORMATTED_MARKER || d == "") return "";  
    return d;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// sets the description of a linked primitive
wasSetLinkDescription(integer link, string description) {
    llSetLinkPrimitiveParamsFast(link, [PRIM_DESC, description]);
}
 
///////////////////////////////////////////////////////////////////////////
//                           PrimFS™ API                                 //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Remove all descriptors k and all data associated with descriptor k 
// from head to tail and return the number of bytes deleted (excluding
// the length of the descriptor k).
integer wasPrimFSDelete(string k, integer head, integer tail) {
    integer b;
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        b += llStringLength(wasKeyValueGet(k, d));
        wasSetLinkDescription(tail, 
            wasKeyValueDelete(
                k,
                d
            )
        );
        // GC
        d = "";
@continue;
    } while(--tail>=head);
    return b;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns a list containing all unique file descriptors.
list wasPrimFSGetDescriptors(integer head, integer tail) {
    list descriptors = [];
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        list c = llList2ListStrided(
            llParseString2List(d, ["&", "="], []), 
            0, -1, 2
        );
        // GC
        d = "";
        do {
            string k = llList2String(c, 0);
            if(llListFindList(
                descriptors, (list)k
                    ) != -1) jump skip;
            descriptors += k;
            // GC
            k = "";
@skip;
            c = llDeleteSubList(c, 0, 0);
        } while(llGetListLength(c) != 0);
        // GC
        c = [];
@continue;
    } while(--tail>=head);
    return descriptors;
}
 
///////////////////////////////////////////////////////////////////////////
//                        PrimFS™ Read-Write                             //
//                         Descriptor Level.                             //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Writes data to the filesystem in the partition between ead and tail 
// and returns the number of written bytes. 
//
// data cannot contain the = (equal, ASCII 061) sign, a restriction of 
// key-value data structure (http://grimore.org/secondlife:key_value_data).
integer wasPrimFSWrite(string k, string data, integer head, integer tail) {
    integer b;
    do {
        string d = wasGetLinkDescription(tail);
        string v = "";
        if(llSubStringIndex(d, k) != -1)
            v = wasKeyValueGet(k, d);
        integer s = llStringLength(v);
        // if the key was found
        if(s != 0) {
            s = BYTES_PER_SECTOR - llStringLength(d) + s;
            v = llGetSubString(data, 0, s-1);
            if(llStringLength(v) != 0) jump write;
            wasSetLinkDescription(tail,
                wasKeyValueDelete(
                    k,
                    d
                )
            );
            jump continue;
        }
        // if the key was not found
        if(llStringLength(d) >= BYTES_PER_SECTOR) jump continue;
        s = BYTES_PER_SECTOR - llStringLength(d) - llStringLength(k) - 1;
        // &
        if(llStringLength(d) != 0) --s;
        if(s < 0) jump continue;
        v = llGetSubString(data, 0, s-1);
        if(llStringLength(v) == 0) jump continue;
        // write
@write;
        b += llStringLength(v) + llStringLength(k) + 1;
        v = wasKeyValueSet(k, v, d);
        wasSetLinkDescription(tail,v);
        // GC
        v = "";
        data = llDeleteSubString(data, 0, s-1);
@continue;
        // GC
        d = "";
    } while(--tail>=head);
    // return written bytes
    return b;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Reads the filesystem on the partition between head and tail and returns
// the data associated with the file descriptor k.
string wasPrimFSRead(string k, integer head, integer tail) {
    string output = "";
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        if(llListFindList(wasKeyValueGetKeys(d), [ k ]) == -1)
            jump continue;
        output += wasKeyValueGet(k, d);
@continue;
        // GC
        d = "";
    } while(--tail>=head);
    return output;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Formats all blocks for PrimFS™ between head and tail and returns the 
// number of sectors for the new partition.
// The resulting formatted partions can be found between head and tail.
integer wasPrimFSFormat(integer head, integer tail) {
    integer seek = tail;
    do {
        wasSetLinkDescription(seek, "");
    } while(--seek>=head);
    return tail-head;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns the number of free space (in bytes) on a partition bounded
// by head and tail
integer wasPrimFSGetFreeSpace(integer head, integer tail) {
    integer occupied = 0;
    integer seek = tail;
    do {
        occupied += llStringLength(wasGetLinkDescription(seek));
    } while(--seek>=head);
    return BYTES_PER_SECTOR*(tail-head)-occupied;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns the number of free space (in bytes) on a partition bounded 
// by head and tail
integer wasPrimFSGetUsedSpace(integer head, integer tail) {
    integer occupied = 0;
    do {
        occupied += llStringLength(wasGetLinkDescription(tail));
    } while(--tail>=head);
    return occupied;
}
 
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3      //
///////////////////////////////////////////////////////////////////////////
vector magVector(vector i, float mag) {
    i.x *= 2;
    i.y *= 2;
    i.z *= 2;
    if(llFabs(i.x) < mag && llFabs(i.y) < mag && llFabs(i.z) < mag) 
        return magVector(i, mag);
    return i/2;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
wasTellOwner(string type, string message) {
    llOwnerSay("⎣" + type + "⎤: " + message + ".");
}
 
// The following wrapper is purely aesthentic wrapper that will make sure 
// that no_sensor will //always// be triggered after one second. 
wasSensorAttach(integer t) {
    llSensorRepeat("", NULL_KEY, AGENT, .1, .1, t);
}
 
vector rp = ZERO_VECTOR;
vector gridPos = ZERO_VECTOR;
string jumpTarget = "";
integer target = 0;
 
default {
    state_entry() {
        llSetTimerEvent(30);
        integer c = 1 + (integer)llFrand(9);
        llListen(c, "", llGetOwner(), "");
        llOwnerSay("@version=" + (string)c);
    }
    listen(integer channel, string name, key id, string message) {
        llSetTimerEvent(0);
        // Don't detach.
        llOwnerSay("@detach=n");
        state main;
    }
    timer() {
        llSetTimerEvent(0);
        wasTellOwner("jump!", "your viewer is not RLV-enabled. This gizmo requires a RLV-enabled viewer");
    }
    on_rez(integer num) {
        llResetScript();
    }
    attach(key id) {
        llResetScript();
    }
}
 
state main {
    state_entry() {
        // owner channel
        llListen(LISTEN_CHANNEL, "", llGetOwner(), "");
    }
    listen(integer i, string top, key id, string message) {
        if(id != llGetOwner()) return;
 
        // only accept "@" jump prefix
        list command = llParseString2List(message, [" "], ["@"]);
        if(llList2String(command, 0) != "@") return;
        command = llDeleteSubList(command, 0, 0);
 
        // second, check if we have a bookmark request
        list descriptors = wasPrimFSGetDescriptors(2, llGetNumberOfPrims());
        do {
            string o = llList2String(descriptors, 0);
            if(llSubStringIndex(o, llList2String(command, 0)) == -1) jump continue_mark;
            list tokens = llParseString2List(wasPrimFSRead(o, 2, llGetNumberOfPrims()), ["/"], []);
            if (llGetListLength(tokens) != 4) jump continue_mark;
            jumpTarget = llList2String(tokens, 0);
            tokens = llDeleteSubList(tokens, 0, 0);
            if (jumpTarget == llGetRegionName() && llVecDist((vector)("<" + llList2CSV(tokens) + ">"), llGetPos()) < 1) {
                wasTellOwner("jump", "destination too close");
                return;
            }
            // we found the mark, so proceed to teleport
            rp = (vector)("<" + llList2CSV(tokens) + ">");
            llOwnerSay("@unsit=force");
            llRequestSimulatorData(jumpTarget, DATA_SIM_STATUS);
            wasTellOwner("jump", llGetRegionName() + " ↦ " + jumpTarget);
            return;
@continue_mark;
            descriptors = llDeleteSubList(descriptors, 0, 0);
        } while(llGetListLength(descriptors) != 0);
 
        if(llList2String(command, 0) == "help") {
            llGiveInventory(llGetOwner(), "Jump! Cheatsheet");
            return;
        }
 
        if(llList2String(command, 0) == "wipe") state wipe;
 
        if(llList2String(command, 0) == "free") {
            integer free = wasPrimFSGetFreeSpace(2, llGetNumberOfPrims());
            integer used = wasPrimFSGetUsedSpace(2, llGetNumberOfPrims());
            wasTellOwner("free", "Free: " + (string)free + "b / Used: " + (string)used + "b / Occupation: " + (string)((integer)(100.0 * (float)used/(float)(free + used))) + "%");
            return;
        }
 
        if(llList2String(command, 0) == "unlock") {
            llOwnerSay("@detach=y");
            wasTellOwner("unlock", "jump! is now unlocked, you can detach the object from inventory");
            return;
        }
 
        if(llList2String(command, 0) == "lock") {
            llOwnerSay("@detach=n");
            wasTellOwner("lock", "jump! is now locked");
            return;
        }
 
        if(llList2String(command, 0) == "list") {
            list d = wasPrimFSGetDescriptors(2, llGetNumberOfPrims());
            do {
                string k = llList2String(d, 0);
                string v = wasPrimFSRead(k, 2, llGetNumberOfPrims());
                list t = llParseString2List(v, ["/"], []);
                if(llGetListLength(t) != 4) jump continue_list;
                if(llList2String(t, 0) == llGetRegionName()) v = "⚉ " + v;
                wasTellOwner(k, v);
@continue_list;
                d = llDeleteSubList(d, 0, 0);
            } while(llGetListLength(d) != 0);
            return;
        }
 
        // command with one parameter
        top = llList2String(command, 0);
        command = llDeleteSubList(command, 0, 0);
        if(llGetListLength(command) == 0) return;
        message = llList2String(command, 0);
        command = llDeleteSubList(command, 0, 0);
        if(llStringLength(message) == 0) {
            wasTellOwner(llList2String(command, 0), "requires a parameter");
            return;
        }
 
        if(top == "search") {
            // check for syntax violation.
            if(llSubStringIndex(message, "&") != -1 || llSubStringIndex(message, "=") != -1) {
                wasTellOwner("mark", "mark names may not contain the ampersand (&) or equal (=) characters");
                return;
            }
            list d = wasPrimFSGetDescriptors(2, llGetNumberOfPrims());
            do {
                string k = llList2String(d, 0);
                string v = wasPrimFSRead(k, 2, llGetNumberOfPrims());
                if(llGetListLength(
                    llParseString2List(v, ["/"], [])) != 4
                ) jump continue_search;
                if(llSubStringIndex(
                    llList2String(d, 0), message) != -1 ||
                    llSubStringIndex(v, message) != -1
                ) wasTellOwner(k, v);
@continue_search;
                d = llDeleteSubList(d, 0, 0);
            } while(llGetListLength(d) != 0);
            return;
        }
 
        if(top == "mark") {
            // check for overwrite.
            if(llListFindList(wasPrimFSGetDescriptors(2, llGetNumberOfPrims()), (list)message) != -1) {
                wasTellOwner("mark", "marking would overwrite previously stored mark");
                return;
            }
            // check for syntax violation.
            if(llSubStringIndex(message, "&") != -1 || llSubStringIndex(message, "=") != -1) {
                wasTellOwner("mark", "mark names may not contain the ampersand (&) or equal (=) characters");
                return;
            }
            if(wasPrimFSWrite(message, llGetRegionName() + "/" + 
                llDumpList2String(
                    wasCompoundToList(TYPE_INTEGER, [llGetPos()]), 
                "/"), 2, llGetNumberOfPrims()) == 0) {
                wasTellOwner("mark", "could not write mark");
                return;
            }
            wasTellOwner("mark", "mark set");
            return;
        }
 
        if(top == "unmark") {
            // check for syntax violation.
            if(llSubStringIndex(message, "&") != -1 || llSubStringIndex(message, "=") != -1) {
                wasTellOwner("mark", "mark names may not contain the ampersand (&) or equal (=) characters");
                return;
            }
            i = llListFindList(wasPrimFSGetDescriptors(2, llGetNumberOfPrims()), (list)message);
            if(i == -1) {
                wasTellOwner("unmark", "mark not found");
                return;
            }
            if(wasPrimFSDelete(message, 2, llGetNumberOfPrims())) {
                wasTellOwner("unmark", "mark removed");
                return;
            }
            wasTellOwner("unmark", "no mark removed");
            return;
        }
 
        // did not recognize command
    }
 
    dataserver(key queryid, string data) {
        if(data == "up") {
            llRequestSimulatorData(jumpTarget, DATA_SIM_POS);
            return;
        }
        if(data == "down" || data == "starting" || data == "crashed" || data == "unknown") {
            wasTellOwner("jump", "simulator state is not ready, refusing to jump");
            return;
        }
        gridPos = rp + (vector)data;
        state teleport;
    }
    on_rez(integer num) {
        llResetScript();
    }
    attach(key id) {
        llResetScript();
    }
}
 
state teleport {
    state_entry() {
        // schedule the jump
        llSetTimerEvent(1);
    }
    timer() {
        // wait RLV unsit window
        if(llGetAgentInfo(llGetOwner()) & (AGENT_ON_OBJECT|AGENT_SITTING)) return;
        // jump!
        llSetTimerEvent(0);
        llOwnerSay("@tpto:" + llDumpList2String(wasCompoundToList(TYPE_INTEGER, [gridPos]), "/") + "=force");
        wasSensorAttach(5);
    }
    changed(integer change) {
        if(change & CHANGED_REGION == 0) return;
        state default;
    }
    no_sensor() {
        state default;
    }
    on_rez(integer num) {
        llResetScript();
    }
    attach(key id) {
        llResetScript();
    }
}
 
state wipe {
    state_entry() {
        integer c = (integer)("0x8" + llGetSubString(llGetKey(), 0, 6));
        llListen(c, "", llGetOwner(), "");
        llSetTimerEvent(30);
        llDialog(llGetOwner(), "Are you sure you want to wipe your entire drive? By clicking confirm, you will delete all stored landmarks. This process is irreversible and you would need to set all your landmarks again. Consider making a copy of Jump! before proceeding.", ["✔ CONFIRM", "✖ CANCEL"], c);
    }
    listen(integer channel, string name, key id, string message) {
        if(message == "✔ CONFIRM") {
            wasPrimFSFormat(2, llGetNumberOfPrims());
            wasTellOwner("wipe", "drive formatted");
            state default;
        }
        wasTellOwner("wipe", "aborted");
        state default;
    }
    timer() {
        wasTellOwner("wipe", "aborted, dialog timed out, please reissue the command");
        state default;
    }
    on_rez(integer num) {
        llResetScript();
    }
    attach(key id) {
        llResetScript();
    }
}

secondlife/jump.txt · Last modified: 2022/11/24 07:46 by 127.0.0.1

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.