Ping / circuit latency, break out commands, add typing function for IM, add thinkingTime and charactersPerSecond parameters to typing functions

This commit is contained in:
Casper Warden
2017-12-13 19:55:08 +00:00
parent af71aa597e
commit 4e8feb181f
96 changed files with 2151 additions and 935 deletions

View File

@@ -2,7 +2,6 @@ import {LoginHandler} from './LoginHandler';
import {LoginResponse} from './classes/LoginResponse';
import {LoginParameters} from './classes/LoginParameters';
import {Agent} from './classes/Agent';
import {UUID} from './classes/UUID';
import {PacketFlags} from './enums/PacketFlags';
import {UseCircuitCodeMessage} from './classes/messages/UseCircuitCode';
import {CompleteAgentMovementMessage} from './classes/messages/CompleteAgentMovement';
@@ -13,38 +12,29 @@ import {LogoutRequestMessage} from './classes/messages/LogoutRequest';
import {Utils} from './classes/Utils';
import {RegionHandshakeReplyMessage} from './classes/messages/RegionHandshakeReply';
import {RegionProtocolFlags} from './enums/RegionProtocolFlags';
import {AgentThrottleMessage} from './classes/messages/AgentThrottle';
import {AgentDataUpdateRequestMessage} from './classes/messages/AgentDataUpdateRequest';
import {RegionHandleRequestMessage} from './classes/messages/RegionHandleRequest';
import {RegionIDAndHandleReplyMessage} from './classes/messages/RegionIDAndHandleReply';
import * as Long from 'long';
import {MapItemRequestMessage} from './classes/messages/MapItemRequest';
import {GridItemType} from './enums/GridItemType';
import {MapItemReplyMessage} from './classes/messages/MapItemReply';
import {MapBlockRequestMessage} from './classes/messages/MapBlockRequest';
import {MapBlockReplyMessage} from './classes/messages/MapBlockReply';
import {MapInfoReply} from './events/MapInfoReply';
import {TeleportLureRequestMessage} from './classes/messages/TeleportLureRequest';
import {LureEvent} from './events/LureEvent';
import {TeleportFlags} from './enums/TeleportFlags';
import {TeleportProgressMessage} from './classes/messages/TeleportProgress';
import {TeleportStartMessage} from './classes/messages/TeleportStart';
import {SoundTriggerMessage} from './classes/messages/SoundTrigger';
import {AttachedSoundMessage} from './classes/messages/AttachedSound';
import {AvatarAnimationMessage} from './classes/messages/AvatarAnimation';
import {HTTPAssets} from './enums/HTTPAssets';
import * as LLSD from 'llsd';
import {TeleportEvent} from './events/TeleportEvent';
import {ClientEvents} from './classes/ClientEvents';
import {TeleportEventType} from './enums/TeleportEventType';
import {ClientCommands} from './classes/ClientCommands';
import {DisconnectEvent} from './events/DisconnectEvent';
import {KickUserMessage} from './classes/messages/KickUser';
import {StartPingCheckMessage} from './classes/messages/StartPingCheck';
import {CompletePingCheckMessage} from './classes/messages/CompletePingCheck';
import Timer = NodeJS.Timer;
export class Bot
{
private loginParams: LoginParameters;
private currentRegion: Region;
private agent: Agent;
private throttleGenCounter = 0;
private clientEvents: ClientEvents | null = null;
private ping: Timer | null = null;
private pingNumber = 0;
private lastSuccessfulPing = 0;
public clientEvents: ClientEvents | null = null;
public clientCommands: ClientCommands;
constructor(login: LoginParameters)
{
@@ -61,6 +51,7 @@ export class Bot
this.clientEvents = response.clientEvents;
this.currentRegion = response.region;
this.agent = response.agent;
this.clientCommands = new ClientCommands(response.region, response.agent, this);
resolve(response);
}).catch((error: Error) =>
{
@@ -69,6 +60,22 @@ export class Bot
});
}
changeRegion(region: Region)
{
return new Promise((resolve, reject) =>
{
this.currentRegion = region;
this.clientCommands = new ClientCommands(this.currentRegion, this.agent, this);
this.connectToSim().then(() =>
{
resolve();
}).catch((error) =>
{
reject(error);
});
});
}
close()
{
return new Promise((resolve, reject) =>
@@ -92,238 +99,21 @@ export class Bot
this.currentRegion.shutdown();
delete this.currentRegion;
delete this.agent;
resolve();
});
});
}
setBandwidth(total: number)
{
const circuit = this.currentRegion.circuit;
const agentThrottle: AgentThrottleMessage = new AgentThrottleMessage();
agentThrottle.AgentData = {
AgentID: this.agent.agentID,
SessionID: circuit.sessionID,
CircuitCode: circuit.circuitCode
};
const throttleData = Buffer.allocUnsafe(28);
let pos = 0;
const resendThrottle = total * 0.1;
const landThrottle = total * 0.172;
const windThrottle = total * 0.05;
const cloudThrottle = total * 0.05;
const taskThrottle = total * 0.234;
const textureThrottle = total * 0.234;
const assetThrottle = total * 0.160;
throttleData.writeFloatLE(resendThrottle, pos); pos += 4;
throttleData.writeFloatLE(landThrottle, pos); pos += 4;
throttleData.writeFloatLE(windThrottle, pos); pos += 4;
throttleData.writeFloatLE(cloudThrottle, pos); pos += 4;
throttleData.writeFloatLE(taskThrottle, pos); pos += 4;
throttleData.writeFloatLE(textureThrottle, pos); pos += 4;
throttleData.writeFloatLE(assetThrottle, pos);
agentThrottle.Throttle = {
GenCounter: this.throttleGenCounter++,
Throttles: throttleData
};
circuit.sendMessage(agentThrottle, PacketFlags.Reliable);
}
acceptTeleport(lure: LureEvent): Promise<TeleportEvent>
{
return new Promise<TeleportEvent>((resolve, reject) =>
{
const circuit = this.currentRegion.circuit;
const tlr = new TeleportLureRequestMessage();
tlr.Info = {
AgentID: this.agent.agentID,
SessionID: circuit.sessionID,
LureID: lure.lureID,
TeleportFlags: TeleportFlags.ViaLure
};
circuit.sendMessage(tlr, PacketFlags.Reliable);
if (this.currentRegion.caps.eventQueueClient)
{
if (this.clientEvents === null)
delete this.clientCommands;
if (this.ping !== null)
{
reject(new Error('ClientEvents is null'));
return;
clearInterval(this.ping);
this.ping = null;
}
const subscription = this.clientEvents.onTeleportEvent.subscribe((e: TeleportEvent) =>
{
if (e.eventType === TeleportEventType.TeleportFailed || e.eventType === TeleportEventType.TeleportCompleted)
{
subscription.unsubscribe();
}
if (e.eventType === TeleportEventType.TeleportFailed)
{
reject(e);
}
else if (e.eventType === TeleportEventType.TeleportCompleted)
{
if (e.simIP === 'local')
{
// Local TP - no need for any other shindiggery
resolve(e);
return;
}
if (this.clientEvents === null)
{
reject(new Error('ClientEvents is null'));
return;
}
// Successful teleport! First, rip apart circuit
this.currentRegion.shutdown();
const region: Region = new Region(this.agent, this.clientEvents);
region.circuit.circuitCode = this.currentRegion.circuit.circuitCode;
region.circuit.secureSessionID = this.currentRegion.circuit.secureSessionID;
region.circuit.sessionID = this.currentRegion.circuit.sessionID;
region.circuit.udpBlacklist = this.currentRegion.circuit.udpBlacklist;
region.circuit.ipAddress = e.simIP;
region.circuit.port = e.simPort;
this.agent.setCurrentRegion(region);
this.currentRegion = region;
this.currentRegion.activateCaps(e.seedCapability);
this.connectToSim().then(() =>
{
resolve(e);
}).catch((error) =>
{
reject(e);
});
}
});
}
});
}
getRegionHandle(regionID: UUID): Promise<Long>
{
return new Promise<Long>((resolve, reject) =>
{
const circuit = this.currentRegion.circuit;
const msg: RegionHandleRequestMessage = new RegionHandleRequestMessage();
msg.RequestBlock = {
RegionID: regionID,
};
circuit.sendMessage(msg, PacketFlags.Reliable);
circuit.waitForMessage(Message.RegionIDAndHandleReply, 10000, (packet: Packet) =>
{
const filterMsg = packet.message as RegionIDAndHandleReplyMessage;
return (filterMsg.ReplyBlock.RegionID.toString() === regionID.toString());
}).then((packet: Packet) =>
{
const responseMsg = packet.message as RegionIDAndHandleReplyMessage;
resolve(responseMsg.ReplyBlock.RegionHandle);
});
});
}
getRegionMapInfo(gridX: number, gridY: number): Promise<MapInfoReply>
{
return new Promise<MapInfoReply>((resolve, reject) =>
{
const circuit = this.currentRegion.circuit;
const response = new MapInfoReply();
const msg: MapBlockRequestMessage = new MapBlockRequestMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: circuit.sessionID,
Flags: 65536,
EstateID: 0,
Godlike: true
};
msg.PositionData = {
MinX: (gridX / 256),
MaxX: (gridX / 256),
MinY: (gridY / 256),
MaxY: (gridY / 256)
};
circuit.sendMessage(msg, PacketFlags.Reliable);
circuit.waitForMessage(Message.MapBlockReply, 10000, (packet: Packet) =>
{
const filterMsg = packet.message as MapBlockReplyMessage;
let found = false;
filterMsg.Data.forEach((data) =>
const disconnectEvent = new DisconnectEvent();
disconnectEvent.requested = true;
disconnectEvent.message = 'Logout completed';
if (this.clientEvents)
{
if (data.X === (gridX / 256) && data.Y === (gridY / 256))
{
found = true;
}
});
return found;
}).then((packet: Packet) =>
{
const responseMsg = packet.message as MapBlockReplyMessage;
responseMsg.Data.forEach((data) =>
{
if (data.X === (gridX / 256) && data.Y === (gridY / 256))
{
response.name = Utils.BufferToStringSimple(data.Name);
response.accessFlags = data.Access;
response.mapImage = data.MapImageID;
}
});
// Now get the region handle
const regionHandle: Long = Utils.RegionCoordinatesToHandle(gridX, gridY);
const mi = new MapItemRequestMessage();
mi.AgentData = {
AgentID: this.agent.agentID,
SessionID: circuit.sessionID,
Flags: 2,
EstateID: 0,
Godlike: false
};
mi.RequestData = {
ItemType: GridItemType.AgentLocations,
RegionHandle: regionHandle
};
circuit.sendMessage(mi, PacketFlags.Reliable);
const minX = Math.floor(gridX / 256) * 256;
const maxX = minX + 256;
const minY = Math.floor(gridY / 256) * 256;
const maxY = minY + 256;
response.avatars = [];
circuit.waitForMessage(Message.MapItemReply, 10000, (packet: Packet) =>
{
const filterMsg = packet.message as MapItemReplyMessage;
let found = false;
filterMsg.Data.forEach((data) =>
{
// Check if avatar is within our bounds
if (data.X >= minX && data.X <= maxX && data.Y >= minY && data.Y <= maxY)
{
found = true;
}
});
return found;
}).then((packet2: Packet) =>
{
const responseMsg2 = packet2.message as MapItemReplyMessage;
responseMsg2.Data.forEach((data) =>
{
response.avatars.push({
X: data.X,
Y: data.Y
});
});
resolve(response);
}).catch((err) =>
{
reject(err);
});
}).catch((err) =>
{
reject(err);
this.clientEvents.onDisconnected.next(disconnectEvent);
}
resolve();
});
});
}
@@ -363,7 +153,10 @@ export class Bot
return circuit.waitForAck(circuit.sendMessage(handshakeReply, PacketFlags.Reliable), 10000)
}).then(() =>
{
this.setBandwidth(1536000);
if (this.clientCommands !== null)
{
this.clientCommands.network.setBandwidth(1536000);
}
const agentRequest = new AgentDataUpdateRequestMessage();
agentRequest.AgentData = {
@@ -374,6 +167,67 @@ export class Bot
this.agent.setInitialAppearance();
this.agent.circuitActive();
this.lastSuccessfulPing = new Date().getTime();
this.ping = setInterval(() =>
{
this.pingNumber++;
if (this.pingNumber > 255)
{
this.pingNumber = 0;
}
const ping = new StartPingCheckMessage();
ping.PingID = {
PingID: this.pingNumber,
OldestUnacked: this.currentRegion.circuit.getOldestUnacked()
};
circuit.sendMessage(ping, PacketFlags.Reliable);
circuit.waitForMessage(Message.CompletePingCheck, 10000, ((pingData: {
pingID: number,
timeSent: number
}, packet: Packet): boolean =>
{
const cpc = packet.message as CompletePingCheckMessage;
if (cpc.PingID.PingID === pingData.pingID)
{
this.lastSuccessfulPing = new Date().getTime();
const pingTime = this.lastSuccessfulPing - pingData.timeSent;
if (this.clientEvents !== null)
{
this.clientEvents.onCircuitLatency.next(pingTime);
}
return true;
}
return false;
}).bind(this, {
pingID: this.pingNumber,
timeSent: new Date().getTime()
}));
if ((new Date().getTime() - this.lastSuccessfulPing) > 60000)
{
// We're dead, jim
this.agent.shutdown();
this.currentRegion.shutdown();
delete this.currentRegion;
delete this.agent;
delete this.clientCommands;
if (this.ping !== null)
{
clearInterval(this.ping);
this.ping = null;
}
const disconnectEvent = new DisconnectEvent();
disconnectEvent.requested = false;
disconnectEvent.message = 'Circuit timeout';
if (this.clientEvents)
{
this.clientEvents.onDisconnected.next(disconnectEvent);
}
}
}, 5000);
circuit.subscribeToMessages(
[
Message.TeleportFailed,
@@ -382,9 +236,7 @@ export class Bot
Message.TeleportStart,
Message.TeleportProgress,
Message.TeleportCancel,
Message.SoundTrigger,
Message.AttachedSound,
Message.AvatarAnimation
Message.KickUser
], (packet: Packet) =>
{
switch (packet.message.id)
@@ -448,24 +300,27 @@ export class Bot
this.clientEvents.onTeleportEvent.next(tpEvent);
break;
}
case Message.SoundTrigger:
case Message.KickUser:
{
const soundTrigger = packet.message as SoundTriggerMessage;
const soundID = soundTrigger.SoundData.SoundID;
// TODO: SoundTrigger clientEvent
break;
}
case Message.AttachedSound:
{
const attachedSound = packet.message as AttachedSoundMessage;
const soundID = attachedSound.DataBlock.SoundID;
// TODO: AttachedSound clientEvent
break;
}
case Message.AvatarAnimation:
{
const avatarAnimation = packet.message as AvatarAnimationMessage;
// TODO: AvatarAnimation clientEvent
const kickUser = packet.message as KickUserMessage;
this.agent.shutdown();
this.currentRegion.shutdown();
delete this.currentRegion;
delete this.agent;
delete this.clientCommands;
if (this.ping !== null)
{
clearInterval(this.ping);
this.ping = null;
}
const disconnectEvent = new DisconnectEvent();
disconnectEvent.requested = false;
disconnectEvent.message = Utils.BufferToStringSimple(kickUser.UserInfo.Reason);
if (this.clientEvents)
{
this.clientEvents.onDisconnected.next(disconnectEvent);
}
break;
}
}
@@ -479,46 +334,4 @@ export class Bot
});
});
}
downloadAsset(type: HTTPAssets, uuid: UUID)
{
return this.currentRegion.caps.downloadAsset(uuid, type);
}
uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise<UUID>
{
return new Promise<UUID>((resolve, reject) =>
{
if (this.agent && this.agent.inventory && this.agent.inventory.main && this.agent.inventory.main.root)
{
this.currentRegion.caps.capsRequestXML('NewFileAgentInventory', {
'folder_id': new LLSD.UUID(this.agent.inventory.main.root.toString()),
'asset_type': type,
'inventory_type': Utils.HTTPAssetTypeToInventoryType(type),
'name': name,
'description': description,
'everyone_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
'group_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
'next_owner_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
'expected_upload_cost': 0
}).then((response: any) =>
{
if (response['state'] === 'upload')
{
const uploadURL = response['uploader'];
this.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) =>
{
resolve(new UUID(responseUpload['new_asset'].toString()));
}).catch((err) =>
{
reject(err);
});
}
}).catch((err) =>
{
console.log(err);
})
}
});
}
}