Refactor examples into a better form factor

This commit is contained in:
Casper Warden
2020-11-23 15:43:27 +00:00
parent 1f3677905b
commit 8ba2cf231c
14 changed files with 672 additions and 460 deletions

18
examples/Camera/Camera.ts Normal file
View File

@@ -0,0 +1,18 @@
import { ExampleBot } from '../ExampleBot';
import { Vector3 } from '../../lib/classes/Vector3';
class Camera extends ExampleBot
{
async onConnected()
{
const height = 64;
this.bot.clientCommands.agent.setCamera(
new Vector3([128, 128, height]),
new Vector3([128, 128, 0]),
256,
new Vector3([-1.0, 0, 0]),
new Vector3([0.0, 1.0, 0]));
}
}
new Camera().run().then(() => {}).catch((err) => { console.error(err) });

171
examples/ExampleBot.ts Normal file
View File

@@ -0,0 +1,171 @@
import Signals = NodeJS.Signals;
import Timeout = NodeJS.Timeout;
import * as path from 'path';
import { LoginResponse } from '../lib/classes/LoginResponse';
import { Bot } from '../lib/Bot';
import { LoginParameters } from '../lib/classes/LoginParameters';
import { BotOptionFlags } from '../lib/enums/BotOptionFlags';
export class ExampleBot
{
protected masterAvatar = 'd1cd5b71-6209-4595-9bf0-771bf689ce00';
protected isConnected = false;
protected isConnecting = false;
protected loginResponse?: LoginResponse;
protected bot: Bot;
private reconnectTimer?: Timeout;
constructor()
{
const loginParameters = new LoginParameters();
const parameters = require(path.join(__dirname, '..', '..', 'examples', 'loginParameters.json'));
loginParameters.firstName = parameters.firstName;
loginParameters.lastName = parameters.lastName;
loginParameters.password = parameters.password;
loginParameters.start = parameters.start;
// If you don't intend to use the object store (i.e you have no interest in inworld objects, textures, etc,
// using nmv.BotOptionFlags.LiteObjectStore will drastically reduce the footprint and CPU usage.
//
// The full object store has a full searchable rtree index, the lite does not.
//
// For the minimum footprint, use :
//
// const options = nmv.BotOptionFlags.LiteObjectStore | nmv.BotOptionFlags.StoreMyAttachmentsOnly;
const options = BotOptionFlags.None;
this.bot = new Bot(loginParameters, options);
// This will tell the bot to keep trying to teleport back to the 'stay' location.
// You can specify a region and position, such as:
// bot.stayPut(true, 'Izanagi', new nmv.Vector3([128, 128, 21]));
// Note that the 'stay' location will be updated if you request or accept a lure (a teleport).
// If no region is specified, it will be set to the region you log in to.
this.bot.stayPut(true);
}
public async run()
{
const exitHandler = async(options: { exit?: boolean }, err: Error | number | Signals) =>
{
if (err && err instanceof Error)
{
console.log(err.stack);
}
if (this.isConnected)
{
console.log('Disconnecting');
try
{
await this.bot.close();
}
catch (error)
{
console.error('Error when closing client:');
console.error(error);
}
process.exit();
return;
}
if (options.exit)
{
process.exit();
}
}
// Do something when app is closing
process.on('exit', exitHandler.bind(this, {}));
// Catches ctrl+c event
process.on('SIGINT', exitHandler.bind(this, { exit: true }));
// Catches "kill pid"
process.on('SIGUSR1', exitHandler.bind(this, { exit: true }));
process.on('SIGUSR2', exitHandler.bind(this, { exit: true }));
// Catches uncaught exceptions
process.on('uncaughtException', exitHandler.bind(this, { exit: true }));
await this.login();
}
protected async onConnected()
{
}
private async login()
{
if (this.isConnecting)
{
return;
}
this.isConnecting = true;
try
{
if (this.reconnectTimer !== undefined)
{
clearInterval(this.reconnectTimer);
}
this.reconnectTimer = setInterval(this.reconnectCheck.bind(this), 60000);
console.log('Logging in..');
this.loginResponse = await this.bot.login();
console.log('Login complete');
// Establish circuit with region
await this.bot.connectToSim();
console.log('Waiting for event queue');
await this.bot.waitForEventQueue();
this.isConnected = true;
}
finally
{
this.isConnecting = false;
}
return this.connected();
}
private async reconnectCheck()
{
if (!this.isConnected)
{
await this.login();
}
}
private async connected()
{
this.bot.clientEvents.onDisconnected.subscribe((event) =>
{
if (event.requested)
{
if (this.reconnectTimer !== undefined)
{
clearInterval(this.reconnectTimer);
}
}
this.isConnected = false;
console.log('Disconnected from simulator: ' + event.message);
});
await this.onConnected();
}
private async close()
{
if (this.reconnectTimer !== undefined)
{
clearInterval(this.reconnectTimer);
this.reconnectTimer = undefined;
}
return this.bot.close();
}
}

View File

@@ -0,0 +1,58 @@
import { ExampleBot } from '../ExampleBot';
import { FriendRequestEvent } from '../../lib/events/FriendRequestEvent';
import { FriendResponseEvent } from '../../lib/events/FriendResponseEvent';
class Friends extends ExampleBot
{
async onConnected()
{
this.bot.clientEvents.onFriendRequest.subscribe(this.onFriendRequest.bind(this));
this.bot.clientEvents.onFriendResponse.subscribe(this.onFriendResponse.bind(this));
this.bot.clientCommands.friends.sendFriendRequest(this.masterAvatar, 'Be friends with me?').then(() => {});
try
{
// Get map location of the master avatar. Will fail if you don't have map rights
const regionLocation = await this.bot.clientCommands.friends.getFriendMapLocation(this.masterAvatar);
console.log('Master is in ' + regionLocation.regionName + ' at <' + regionLocation.localX + ', ' + regionLocation.localY + '> and there are ' + regionLocation.avatars.length + ' other avatars there too! You stalker!');
}
catch (error)
{
console.log('Map location request failed. The bot probably does not have map rights on the master avatar, or they are offline.');
}
}
async onFriendRequest(event: FriendRequestEvent)
{
if (event.from.toString() === this.masterAvatar)
{
console.log('Accepting friend request from ' + event.fromName);
this.bot.clientCommands.friends.acceptFriendRequest(event).then(() =>
{
});
}
else
{
console.log('Rejecting friend request from ' + event.fromName);
this.bot.clientCommands.friends.rejectFriendRequest(event).then(() =>
{
});
}
}
async onFriendResponse(response: FriendResponseEvent)
{
if (response.accepted)
{
console.log(response.fromName + ' accepted your friend request');
}
else
{
console.log(response.fromName + ' declined your friend request');
}
}
}
new Friends().run().then(() => {}).catch((err) => { console.error(err) });

96
examples/Groups/Group.ts Normal file
View File

@@ -0,0 +1,96 @@
import { ExampleBot } from '../ExampleBot';
import { UUID } from '../../lib/classes/UUID';
import { GroupNoticeEvent } from '../../lib/events/GroupNoticeEvent';
class Group extends ExampleBot
{
async onConnected()
{
this.bot.clientEvents.onGroupNotice.subscribe(this.onGroupNotice.bind(this));
// Group invite example
// Just omit the role parameter for "everyone" role
//
// bot.clientCommands.group.sendGroupInvite("c6424e05-6e2c-fb03-220b-ca7904d11e04", "d1cd5b71-6209-4595-9bf0-771bf689ce00");
// Advanced group invite example
//
const userToInvite = new UUID('d1cd5b71-6209-4595-9bf0-771bf689ce00');
const groupID = new UUID('4b35083d-b51a-a148-c400-6f1038a5589e');
// Retrieve group roles
const roles = await this.bot.clientCommands.group.getGroupRoles(groupID);
for (const role of roles)
{
if (role.Name === 'Officers')
{
// IMPORTANT: IN PRODUCTION, IT IS HIGHLY RECOMMENDED TO CACHE THIS LIST.
//
try
{
const members = await this.bot.clientCommands.group.getMemberList(groupID);
let found = true;
for (const member of members)
{
if (member.AgentID.toString() === userToInvite.toString())
{
found = true;
}
}
if (found)
{
console.log('User already in group, skipping invite');
}
else
{
this.bot.clientCommands.group.sendGroupInvite(groupID, userToInvite, role.RoleID).then(() =>
{
});
}
}
catch (error)
{
console.error('Error retrieving member list for group invite');
}
}
}
// Get group member list
try
{
const memberList = await this.bot.clientCommands.group.getMemberList(groupID);
console.log(memberList.length + ' members in member list');
}
catch (error)
{
// Probably access denied
console.error(error);
}
// Get group ban list
try
{
const banList = await this.bot.clientCommands.group.getBanList(groupID);
console.log(banList.length + ' members in ban list');
}
catch (error)
{
// Probably access denied
console.error(error);
}
}
async onGroupNotice(event: GroupNoticeEvent)
{
// Get group name
const groupProfile = await this.bot.clientCommands.group.getGroupProfile(event.groupID);
console.log('Group notice from ' + event.fromName + ' (' + event.from + '), from group ' + groupProfile.Name + ' (' + event.groupID + ')');
console.log('Subject: ' + event.subject);
console.log('Message: ' + event.message);
}
}
new Group().run().then(() => {}).catch((err: Error) => { console.error(err) });

View File

@@ -0,0 +1,80 @@
import { ExampleBot } from '../ExampleBot';
import { UUID } from '../../lib/classes/UUID';
import { GroupChatEvent } from '../../lib/events/GroupChatEvent';
class GroupChat extends ExampleBot
{
private pings: {[key: string]: number} = {};
async onConnected()
{
const groupID = new UUID('4b35083d-b51a-a148-c400-6f1038a5589e');
this.bot.clientEvents.onGroupChat.subscribe(this.onGroupChat.bind(this));
// Start a group chat session - equivalent to opening a group chat but not sending a message
await this.bot.clientCommands.comms.startGroupChatSession(groupID, '');
// Send a group message
await this.bot.clientCommands.comms.sendGroupMessage(groupID, 'Test');
const badGuyID = new UUID('1481561a-9113-46f8-9c02-9ac1bf005de7');
await this.bot.clientCommands.comms.moderateGroupChat(groupID, badGuyID, true, true);
// Now, the group mute stuff is often pretty useless because an avatar can just leave the session and re-join.
// Let's enforce it a little better.
const groupChatSubscriber = this.bot.clientEvents.onGroupChatAgentListUpdate.subscribe((event) =>
{
if (event.groupID.equals(groupID) && event.agentID.equals(badGuyID) && event.entered)
{
this.bot.clientCommands.comms.moderateGroupChat(groupID, badGuyID, true, true).then(() =>
{
console.log('Re-enforced mute on ' + badGuyID.toString());
}).catch((err) =>
{
console.error(err);
});
}
});
// Actually, maybe we want to ban the chump.
await this.bot.clientCommands.group.banMembers(groupID, [badGuyID]);
await this.bot.clientCommands.group.ejectFromGroup(groupID, badGuyID);
}
async onGroupChat(event: GroupChatEvent)
{
console.log('Group chat: ' + event.fromName + ': ' + event.message);
if (event.message === '!ping')
{
const ping = UUID.random().toString();
this.pings[ping] = Math.floor(new Date().getTime());
try
{
const memberCount = await this.bot.clientCommands.comms.sendGroupMessage(event.groupID, 'ping ' + ping);
console.log('Group message sent to ' + memberCount + ' members');
}
catch (error)
{
console.error('Failed to send group message:');
console.error(error);
}
}
else if (event.from.toString() === this.bot.agentID().toString())
{
if (event.message.substr(0, 5) === 'ping ')
{
const pingID = event.message.substr(5);
if (this.pings[pingID])
{
const time = (new Date().getTime()) - this.pings[pingID];
delete this.pings[pingID];
await this.bot.clientCommands.comms.sendGroupMessage(event.groupID, 'Chat lag: ' + time + 'ms');
}
}
}
}
}
new GroupChat().run().then(() => {}).catch((err: Error) => { console.error(err) });

View File

@@ -0,0 +1,35 @@
import { ExampleBot } from '../ExampleBot';
import { InstantMessageEvent } from '../../lib/events/InstantMessageEvent';
import { ChatSourceType } from '../../lib/enums/ChatSourceType';
import { InstantMessageEventFlags } from '../../lib/enums/InstantMessageEventFlags';
class InstantMessages extends ExampleBot
{
constructor()
{
super();
}
async onConnected()
{
this.bot.clientEvents.onInstantMessage.subscribe(this.onInstantMessage.bind(this));
}
async onInstantMessage(event: InstantMessageEvent)
{
if (event.source === ChatSourceType.Agent)
{
if (!(event.flags & InstantMessageEventFlags.startTyping || event.flags & InstantMessageEventFlags.finishTyping))
{
// typeInstantMessage will emulate a human-ish typing speed
await this.bot.clientCommands.comms.typeInstantMessage(event.from, 'Thanks for the message! This account is a scripted agent (bot), so cannot reply to your query. Sorry!');
// sendInstantMessage will send it instantly
await this.bot.clientCommands.comms.sendInstantMessage(event.from, 'Of course I still love you!');
}
}
}
}
new InstantMessages().run().then(() => {}).catch((err: Error) => { console.error(err) });

View File

@@ -0,0 +1,134 @@
import { ExampleBot } from '../ExampleBot';
import { InventoryFolder } from '../../lib/classes/InventoryFolder';
import { FolderType } from '../../lib/enums/FolderType';
import { InventoryItem } from '../../lib/classes/InventoryItem';
import { LLLindenText } from '../../lib/classes/LLLindenText';
import { AssetType } from '../../lib/enums/AssetType';
import { InventoryType } from '../../lib/enums/InventoryType';
import { PermissionMask } from '../../lib/enums/PermissionMask';
import { InventoryResponseEvent } from '../../lib/events/InventoryResponseEvent';
import { InventoryOfferedEvent } from '../../lib/events/InventoryOfferedEvent';
class Inventory extends ExampleBot
{
async onConnected()
{
this.bot.clientEvents.onInventoryOffered.subscribe(this.onInventoryOffered.bind(this));
this.bot.clientEvents.onInventoryResponse.subscribe(this.onInventoryResponse.bind(this));
// Get the root inventory folder
const rootFolder = this.bot.clientCommands.inventory.getInventoryRoot();
// Populate the root folder
await rootFolder.populate(false);
const exampleFolderName = 'node-metaverse example';
const exampleNotecardName = 'Example Notecard';
let exampleFolder: InventoryFolder | undefined = undefined;
for (const childFolder of rootFolder.folders)
{
if (childFolder.name === exampleFolderName)
{
exampleFolder = childFolder;
await exampleFolder.populate(false);
break;
}
}
// Our folder doesnt' seem to exist, so create it
if (exampleFolder === undefined)
{
exampleFolder = await rootFolder.createFolder(exampleFolderName, FolderType.None);
}
// See if we've already made our test notecard to avoid clutter..
let exampleNotecard: InventoryItem | undefined = undefined;
for (const childItem of exampleFolder.items)
{
if (childItem.name === exampleNotecardName)
{
exampleNotecard = childItem;
break;
}
}
// Create the notecard
if (exampleNotecard === undefined)
{
const notecard = new LLLindenText();
notecard.body = 'This is a notecard I made all by myself at ' + new Date().toString();
exampleNotecard = await exampleFolder.uploadAsset(AssetType.Notecard, InventoryType.Notecard, notecard.toAsset(), exampleNotecardName, 'This is an example notecard');
}
// Set notecard to transfer only
exampleNotecard.permissions.nextOwnerMask = PermissionMask.Transfer | PermissionMask.Modify;
await exampleNotecard.update();
// Give the notecard to our owner
await this.bot.clientCommands.comms.giveInventory(this.masterAvatar, exampleNotecard);
// Enumerate library
const folders = this.bot.clientCommands.inventory.getLibraryRoot().getChildFolders();
for (const folder of folders)
{
await this.iterateFolder(folder, '[ROOT]');
}
console.log('Done iterating through library');
}
async onInventoryResponse(response: InventoryResponseEvent)
{
if (response.accepted)
{
console.log(response.fromName + ' accepted your inventory offer');
}
else
{
console.log(response.fromName + ' declined your inventory offer');
}
}
async iterateFolder(folder: InventoryFolder, prefix: string)
{
console.log(prefix + ' [' + folder.name + ']');
await folder.populate(false);
for (const subFolder of folder.folders)
{
await this.iterateFolder(subFolder, prefix + ' [' + folder.name + ']');
}
for (const item of folder.items)
{
console.log(prefix + ' [' + folder.name + ']' + ': ' + item.name);
if (item.name === 'anim SMOOTH')
{
// Send this to our master av
}
}
}
async onInventoryOffered(event: InventoryOfferedEvent)
{
if (event.from.toString() === this.masterAvatar)
{
console.log('Accepting inventory offer from ' + event.fromName);
this.bot.clientCommands.inventory.acceptInventoryOffer(event).then(() =>
{
});
}
else
{
console.log('Rejecting inventory offer from ' + event.fromName);
this.bot.clientCommands.inventory.rejectInventoryOffer(event).then(() =>
{
});
}
}
}
new Inventory().run().then(() => {}).catch((err) => { console.error(err) });

View File

@@ -0,0 +1,21 @@
import { ExampleBot } from '../ExampleBot';
class Parcels extends ExampleBot
{
async onConnected()
{
const parcelInMiddle = await this.bot.clientCommands.region.getParcelAt(128, 128);
console.log('Parcel at 128x128 is ' + parcelInMiddle.Name);
const parcels = await this.bot.clientCommands.region.getParcels();
console.log('Parcels on region:');
console.log('========================');
for (const p of parcels)
{
console.log(p.Name);
}
console.log('========================');
}
}
new Parcels().run().then(() => {}).catch((err) => { console.error(err) });

View File

@@ -0,0 +1,48 @@
import { ExampleBot } from '../ExampleBot';
import { LureEvent } from '../../lib/events/LureEvent';
class Teleports extends ExampleBot
{
async onConnected()
{
// "OnLure" event fires when someone tries to teleport us
this.bot.clientEvents.onLure.subscribe(this.onLure.bind(this));
// Alternatively we can TP someone else to us
await this.bot.clientCommands.comms.sendTeleport(this.masterAvatar);
}
async onLure(lureEvent: LureEvent)
{
try
{
const regionInfo = await this.bot.clientCommands.grid.getRegionMapInfo(lureEvent.gridX / 256, lureEvent.gridY / 256);
if (lureEvent.from.toString() === this.masterAvatar)
{
console.log('Accepting teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1) ? '' : 's') + '' +
' present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage);
try
{
await this.bot.clientCommands.teleport.acceptTeleport(lureEvent);
}
catch (error)
{
console.error('Teleport error:');
console.error(error);
}
}
else
{
console.log('Ignoring teleport lure to ' + regionInfo.block.name + ' (' + regionInfo.avatars.length + ' avatar' + ((regionInfo.avatars.length === 1) ? '' : 's') + ' ' +
'present) from ' + lureEvent.fromName + ' with message: ' + lureEvent.lureMessage);
}
}
catch (error)
{
console.error('Failed to get region map info:');
console.error(error);
}
}
}
new Teleports().run().then(() => {}).catch((err) => { console.error(err) });

View File

@@ -0,0 +1,7 @@
{
"firstName": "Username",
"lastName": "Resident",
"password": "YourPassword",
"start": "last", //first, last, or login uri like uri:<existing region name>&<x>&<y>&<z>
"url": "https://login.agni.lindenlab.com/cgi-bin/login.cgi"
}