Files
node-metaverse/lib/classes/Agent.ts
2024-09-09 01:46:44 +01:00

437 lines
15 KiB
TypeScript

import { UUID } from './UUID';
import { Vector3 } from './Vector3';
import { Inventory } from './Inventory';
import { Wearable } from './Wearable';
import { Region } from './Region';
import { Message } from '../enums/Message';
import { Packet } from './Packet';
import { AvatarAnimationMessage } from './messages/AvatarAnimation';
import { AgentUpdateMessage } from './messages/AgentUpdate';
import { Quaternion } from './Quaternion';
import { AgentState } from '../enums/AgentState';
import { BuiltInAnimations } from '../enums/BuiltInAnimations';
import { AgentWearablesRequestMessage } from './messages/AgentWearablesRequest';
import { AgentWearablesUpdateMessage } from './messages/AgentWearablesUpdate';
import { RezSingleAttachmentFromInvMessage } from './messages/RezSingleAttachmentFromInv';
import { AttachmentPoint } from '../enums/AttachmentPoint';
import { Utils } from './Utils';
import { ClientEvents } from './ClientEvents';
import * as Long from 'long';
import { GroupChatSessionAgentListEvent } from '../events/GroupChatSessionAgentListEvent';
import { AgentFlags } from '../enums/AgentFlags';
import { ControlFlags } from '../enums/ControlFlags';
import { PacketFlags } from '../enums/PacketFlags';
import { FolderType } from '../enums/FolderType';
import { Subject, Subscription } from 'rxjs';
import { InventoryFolder } from './InventoryFolder';
import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent';
import { BulkUpdateInventoryMessage } from './messages/BulkUpdateInventory';
import { InventoryItem } from './InventoryItem';
import { AgentDataUpdateMessage } from './messages/AgentDataUpdate';
import { InventoryLibrary } from '../enums/InventoryLibrary';
export class Agent
{
firstName: string;
lastName: string;
localID = 0;
agentID: UUID;
activeGroupID: UUID = UUID.zero();
accessMax: string;
regionAccess: string;
agentAccess: string;
currentRegion: Region;
chatSessions = new Map<string, {
agents: Map<string, {
hasVoice: boolean;
isModerator: boolean
}>,
timeout?: NodeJS.Timeout
}>();
controlFlags: ControlFlags = 0;
openID: {
'token'?: string,
'url'?: string
} = {};
AOTransition: boolean;
buddyList: {
'buddyRightsGiven': boolean,
'buddyID': UUID,
'buddyRightsHas': boolean
}[] = [];
uiFlags: {
'allowFirstLife'?: boolean
} = {};
headRotation = Quaternion.getIdentity();
bodyRotation = Quaternion.getIdentity();
cameraLookAt: Vector3 = new Vector3([0.979546, 0.105575, -0.171303]);
cameraCenter: Vector3 = new Vector3([199.58, 203.95, 24.304]);
cameraLeftAxis: Vector3 = new Vector3([-1.0, 0.0, 0]);
cameraUpAxis: Vector3 = new Vector3([0.0, 0.0, 1.0]);
cameraFar = 1;
maxGroups: number;
agentFlags: number;
startLocation: string;
cofVersion: number;
home: {
'regionHandle'?: Long,
'position'?: Vector3,
'lookAt'?: Vector3
} = {};
snapshotConfigURL: string;
inventory: Inventory;
gestures: {
assetID: UUID,
itemID: UUID
}[] = [];
agentAppearanceService: string;
wearables?: {
attachments: Wearable[];
serialNumber: number
};
agentUpdateTimer: NodeJS.Timeout | null = null;
estateManager = false;
appearanceComplete = false;
appearanceCompleteEvent: Subject<void> = new Subject<void>();
private clientEvents: ClientEvents;
private animSubscription?: Subscription;
public onGroupChatExpired = new Subject<UUID>();
constructor(clientEvents: ClientEvents)
{
this.inventory = new Inventory(clientEvents, this);
this.clientEvents = clientEvents;
this.clientEvents.onGroupChatAgentListUpdate.subscribe((event: GroupChatSessionAgentListEvent) =>
{
const str = event.groupID.toString();
const agent = event.agentID.toString();
const session = this.chatSessions.get(str);
if (session === undefined)
{
return;
}
if (event.entered)
{
if (session.agents === undefined)
{
session.agents = new Map<string, {
hasVoice: boolean;
isModerator: boolean
}>();
}
session.agents.set(agent, {
hasVoice: event.canVoiceChat,
isModerator: event.isModerator
});
}
else
{
session.agents.delete(agent);
}
});
}
public updateLastMessage(groupID: UUID): void
{
const str = groupID.toString();
const entry = this.chatSessions.get(str);
if (entry === undefined)
{
return;
}
if (entry.timeout !== undefined)
{
clearInterval(entry.timeout);
entry.timeout = setTimeout(this.groupChatExpired.bind(this, groupID), 900000);
}
}
setIsEstateManager(is: boolean): void
{
this.estateManager = is;
}
getSessionAgentCount(uuid: UUID): number
{
const str = uuid.toString();
const session = this.chatSessions.get(str);
if (session === undefined)
{
return 0;
}
else
{
return Object.keys(session.agents).length;
}
}
addChatSession(uuid: UUID, timeout: boolean): boolean
{
const str = uuid.toString();
if (this.chatSessions.has(str))
{
return false;
}
this.chatSessions.set(str, {
agents: new Map<string, {
hasVoice: boolean,
isModerator: boolean
}>(),
timeout: timeout ? setTimeout(this.groupChatExpired.bind(this, uuid), 900000) : undefined
});
return true;
}
private groupChatExpired(groupID: UUID): void
{
this.onGroupChatExpired.next(groupID);
}
hasChatSession(uuid: UUID): boolean
{
const str = uuid.toString();
return this.chatSessions.has(str);
}
deleteChatSession(uuid: UUID): boolean
{
const str = uuid.toString();
if (!this.chatSessions.has(str))
{
return false;
}
this.chatSessions.delete(str);
return true;
}
setCurrentRegion(region: Region): void
{
if (this.animSubscription !== undefined)
{
this.animSubscription.unsubscribe();
}
this.currentRegion = region;
this.animSubscription = this.currentRegion.circuit.subscribeToMessages([
Message.AvatarAnimation,
Message.AgentDataUpdate,
Message.BulkUpdateInventory
], this.onMessage.bind(this));
}
circuitActive(): void
{
this.agentUpdateTimer = setInterval(this.sendAgentUpdate.bind(this), 1000);
}
sendAgentUpdate(): void
{
if (!this.currentRegion)
{
return;
}
const circuit = this.currentRegion.circuit;
const agentUpdate: AgentUpdateMessage = new AgentUpdateMessage();
agentUpdate.AgentData = {
AgentID: this.agentID,
SessionID: circuit.sessionID,
HeadRotation: this.headRotation,
BodyRotation: this.bodyRotation,
State: AgentState.None,
CameraCenter: this.cameraCenter,
CameraAtAxis: this.cameraLookAt,
CameraLeftAxis: this.cameraLeftAxis,
CameraUpAxis: this.cameraUpAxis,
Far: this.cameraFar,
ControlFlags: this.controlFlags,
Flags: AgentFlags.None
};
circuit.sendMessage(agentUpdate, 0 as PacketFlags);
}
shutdown(): void
{
if (this.agentUpdateTimer !== null)
{
clearInterval(this.agentUpdateTimer);
this.agentUpdateTimer = null;
}
}
onMessage(packet: Packet): void
{
if (packet.message.id === Message.AgentDataUpdate)
{
const msg = packet.message as AgentDataUpdateMessage;
this.activeGroupID = msg.AgentData.ActiveGroupID;
}
else if (packet.message.id === Message.BulkUpdateInventory)
{
const msg = packet.message as BulkUpdateInventoryMessage;
const evt = new BulkUpdateInventoryEvent();
for (const newItem of msg.ItemData)
{
const folder = this.inventory.findFolder(newItem.FolderID);
const item = new InventoryItem(folder || undefined, this);
item.assetID = newItem.AssetID;
item.inventoryType = newItem.InvType;
item.name = Utils.BufferToStringSimple(newItem.Name);
item.salePrice = newItem.SalePrice;
item.saleType = newItem.SaleType;
item.created = new Date(newItem.CreationDate * 1000);
item.parentID = newItem.FolderID;
item.flags = newItem.Flags;
item.itemID = newItem.ItemID;
item.description = Utils.BufferToStringSimple(newItem.Description);
item.type = newItem.Type;
item.callbackID = newItem.CallbackID;
item.permissions.baseMask = newItem.BaseMask;
item.permissions.groupMask = newItem.GroupMask;
item.permissions.nextOwnerMask = newItem.NextOwnerMask;
item.permissions.ownerMask = newItem.OwnerMask;
item.permissions.everyoneMask = newItem.EveryoneMask;
item.permissions.owner = newItem.OwnerID;
item.permissions.creator = newItem.CreatorID;
item.permissions.group = newItem.GroupID;
item.permissions.groupOwned = newItem.GroupOwned;
evt.itemData.push(item);
}
for (const newFolder of msg.FolderData)
{
const fld = new InventoryFolder(InventoryLibrary.Main, this.inventory.main, this);
fld.typeDefault = newFolder.Type;
fld.name = Utils.BufferToStringSimple(newFolder.Name);
fld.folderID = newFolder.FolderID;
fld.parentID = newFolder.ParentID;
evt.folderData.push(fld);
}
this.clientEvents.onBulkUpdateInventoryEvent.next(evt);
}
else if (packet.message.id === Message.AvatarAnimation)
{
const animMsg = packet.message as AvatarAnimationMessage;
if (animMsg.Sender.ID.toString() === this.agentID.toString())
{
for (const anim of animMsg.AnimationList)
{
const a = anim.AnimID.toString();
if (a === BuiltInAnimations.STANDUP ||
a === BuiltInAnimations.PRE_JUMP ||
a === BuiltInAnimations.LAND ||
a === BuiltInAnimations.MEDIUM_LAND ||
a === BuiltInAnimations.WALK ||
a === BuiltInAnimations.RUN)
{
// TODO: Pretty sure this isn't the best way to do this
this.controlFlags = ControlFlags.AGENT_CONTROL_FINISH_ANIM;
this.sendAgentUpdate();
this.controlFlags = 0;
}
}
}
}
}
async getWearables(): Promise<InventoryFolder>
{
for (const uuid of Object.keys(this.inventory.main.skeleton))
{
const folder = this.inventory.main.skeleton[uuid];
if (folder.typeDefault === FolderType.CurrentOutfit)
{
await folder.populate(false);
return folder;
}
}
throw new Error('Unable to get wearables from inventory')
}
async setInitialAppearance(): Promise<void>
{
const circuit = this.currentRegion.circuit;
const wearablesRequest: AgentWearablesRequestMessage = new AgentWearablesRequestMessage();
wearablesRequest.AgentData = {
AgentID: this.agentID,
SessionID: circuit.sessionID
};
circuit.sendMessage(wearablesRequest, PacketFlags.Reliable);
const wearables: AgentWearablesUpdateMessage = await circuit.waitForMessage<AgentWearablesUpdateMessage>(Message.AgentWearablesUpdate, 10000);
if (!this.wearables || wearables.AgentData.SerialNum > this.wearables.serialNumber)
{
this.wearables = {
serialNumber: wearables.AgentData.SerialNum,
attachments: []
};
for (const wearable of wearables.WearableData)
{
if (this.wearables && this.wearables.attachments)
{
this.wearables.attachments.push({
itemID: wearable.ItemID,
assetID: wearable.AssetID,
wearableType: wearable.WearableType
});
}
}
}
const currentOutfitFolder = await this.getWearables();
const wornObjects = this.currentRegion.objects.getObjectsByParent(this.localID);
for (const item of currentOutfitFolder.items)
{
if (item.type === 6)
{
let found = false;
for (const obj of wornObjects)
{
if (obj.hasNameValueEntry('AttachItemID'))
{
if (item.itemID.toString() === obj.getNameValueEntry('AttachItemID'))
{
found = true;
}
}
}
if (!found)
{
const rsafi = new RezSingleAttachmentFromInvMessage();
rsafi.AgentData = {
AgentID: this.agentID,
SessionID: circuit.sessionID
};
rsafi.ObjectData = {
ItemID: new UUID(item.itemID.toString()),
OwnerID: this.agentID,
AttachmentPt: 0x80 | AttachmentPoint.Default,
ItemFlags: item.flags,
GroupMask: item.permissions.groupMask,
EveryoneMask: item.permissions.everyoneMask,
NextOwnerMask: item.permissions.nextOwnerMask,
Name: Utils.StringToBuffer(item.name),
Description: Utils.StringToBuffer(item.description)
};
circuit.sendMessage(rsafi, PacketFlags.Reliable);
}
}
}
this.appearanceComplete = true;
this.appearanceCompleteEvent.next();
}
setControlFlag(flag: ControlFlags): void
{
this.controlFlags = this.controlFlags | flag;
}
clearControlFlag(flag: ControlFlags): void
{
this.controlFlags = this.controlFlags & ~flag;
}
}