Vastly simplify the object resolution stuff.

This commit is contained in:
Casper Warden
2023-11-21 13:57:06 +00:00
parent 9cbc80e1ef
commit e7e790efff
12 changed files with 655 additions and 449 deletions

View File

@@ -0,0 +1,227 @@
import { BatchQueue } from './BatchQueue';
import * as assert from 'assert';
import { Utils } from './Utils';
export interface TestType
{
baseNumber: number;
numberToAdd: number;
result?: number;
}
describe('BatchQueue', () =>
{
it('batches correctly', async() =>
{
let maxBatchSize = -1;
let minBatchSize = -1;
const b = new BatchQueue<TestType>(5, async(items: Set<TestType>): Promise<Set<TestType>> =>
{
const failures = new Set<TestType>();
if (items.size > maxBatchSize || maxBatchSize === -1)
{
maxBatchSize = items.size;
}
if (items.size < minBatchSize || minBatchSize === -1)
{
minBatchSize = items.size;
}
for (const e of items.values())
{
e.result = e.baseNumber + e.numberToAdd;
}
return failures;
});
const batch1: TestType[] = [
{
baseNumber: 1,
numberToAdd: 5
},
{
baseNumber: 2,
numberToAdd: 6
},
{
baseNumber: 3,
numberToAdd: 7
},
{
baseNumber: 4,
numberToAdd: 8
},
{
baseNumber: 5,
numberToAdd: 9
},
{
baseNumber: 6,
numberToAdd: 10
},
{
baseNumber: 7,
numberToAdd: 11
},
{
baseNumber: 8,
numberToAdd: 12
},
{
baseNumber: 9,
numberToAdd: 13
},
{
baseNumber: 10,
numberToAdd: 14
}
];
await b.add(batch1);
assert.equal(maxBatchSize, 5);
assert.equal(minBatchSize, 5);
for (const item of batch1)
{
assert.equal(item.result, item.baseNumber + item.numberToAdd);
}
});
it('fails correctly on exception', async() =>
{
const batch1: TestType[] = [
{
baseNumber: 50,
numberToAdd: 5
}
];
const batch2: TestType[] = [
{
baseNumber: 60,
numberToAdd: 5
}
];
const batch3: TestType[] = [
{
baseNumber: 70,
numberToAdd: 5
}
];
const b = new BatchQueue<TestType>(5, async(items: Set<TestType>): Promise<Set<TestType>> =>
{
await Utils.sleep(100);
const failures = new Set<TestType>();
for (const e of items.values())
{
if (e.baseNumber === 60)
{
throw new Error('60 is prohibited!');
}
e.result = e.baseNumber + e.numberToAdd;
}
return failures;
});
const promises: Promise<TestType[]>[] = [
b.add(batch1),
b.add(batch2),
b.add(batch3)
];
const result = await Promise.allSettled(promises);
assert.equal(result[0].status, 'fulfilled'); // because the first job will start executing immediately, it won't get failed by the rejection
assert.equal(result[1].status, 'rejected');
assert.equal(result[2].status, 'rejected');
});
it('returns failed jobs correctly', async() =>
{
const batch1: TestType[] = [
{
baseNumber: 50,
numberToAdd: 5
},
{
baseNumber: 70,
numberToAdd: 5
}
];
const batch2: TestType[] = [
{
baseNumber: 60,
numberToAdd: 5
},
{
baseNumber: 50,
numberToAdd: 5
}
];
const batch3: TestType[] = [
{
baseNumber: 40,
numberToAdd: 5
},
{
baseNumber: 50,
numberToAdd: 5
},
{
baseNumber: 60,
numberToAdd: 5
}
];
const b = new BatchQueue<TestType>(5, async(items: Set<TestType>): Promise<Set<TestType>> =>
{
await Utils.sleep(100);
const failures = new Set<TestType>();
for (const e of items.values())
{
if (e.baseNumber === 60)
{
failures.add(e)
}
else
{
e.result = e.baseNumber + e.numberToAdd;
}
}
return failures;
});
const promises: Promise<TestType[]>[] = [
b.add(batch1),
b.add(batch2),
b.add(batch3)
];
const result = await Promise.allSettled(promises);
function isFulfilled<T>(prResult: PromiseSettledResult<T>): prResult is PromiseFulfilledResult<T>
{
return prResult.status === 'fulfilled';
}
assert.equal(result[0].status, 'fulfilled');
assert.equal(result[1].status, 'fulfilled');
assert.equal(result[2].status, 'fulfilled');
if (isFulfilled(result[0]))
{
assert.equal(result[0].value.length, 0);
}
if (isFulfilled(result[1]))
{
assert.equal(result[1].value.length, 1);
assert.equal(result[1].value[0].baseNumber, 60);
}
if (isFulfilled(result[2]))
{
assert.equal(result[2].value.length, 1);
assert.equal(result[2].value[0].baseNumber, 60);
}
});
});

126
lib/classes/BatchQueue.ts Normal file
View File

@@ -0,0 +1,126 @@
import { Subject } from 'rxjs';
export class BatchQueue<T>
{
private running = false;
private pending: Set<T> = new Set<T>();
private onResult = new Subject<{
batch: Set<T>,
failed?: Set<T>,
exception?: unknown
}>();
public constructor(private readonly batchSize: number, private readonly func: (items: Set<T>) => Promise<Set<T>>)
{
}
public async add(ids: T[]): Promise<T[]>
{
const waiting = new Set<T>();
for (const id of ids)
{
waiting.add(id);
this.pending.add(id);
}
if (!this.running)
{
this.processBatch().catch((_e) =>
{
// ignore
});
}
return new Promise<T[]>((resolve, reject) =>
{
const failed: T[] = [];
const subs = this.onResult.subscribe((results: {
batch: Set<T>,
failed?: Set<T>,
exception?: unknown
}) =>
{
let included = false;
for (const v of results.batch.values())
{
if (waiting.has(v))
{
included = true;
if (results.failed?.has(v))
{
failed.push(v);
}
waiting.delete(v);
}
}
if (!included)
{
return;
}
if (results.exception !== undefined)
{
subs.unsubscribe();
reject(results.exception);
return;
}
if (waiting.size === 0)
{
subs.unsubscribe();
resolve(failed);
return;
}
});
});
}
private async processBatch(): Promise<void>
{
if (this.running)
{
return;
}
try
{
this.running = true;
const thisBatch = new Set<T>();
const values = this.pending.values();
for (const v of values)
{
thisBatch.add(v);
this.pending.delete(v);
if (thisBatch.size >= this.batchSize)
{
break;
}
}
try
{
const failedItems = await this.func(thisBatch)
this.onResult.next({
batch: thisBatch,
failed: failedItems
});
}
catch (e)
{
this.onResult.next({
batch: thisBatch,
exception: e
});
}
}
finally
{
this.running = false;
if (this.pending.size > 0)
{
this.processBatch().catch((_e) =>
{
// ignore
});
}
}
}
}

View File

@@ -0,0 +1,47 @@
export class ConcurrentQueue
{
private concurrency: number;
private runningCount;
private jobQueue: { job: () => Promise<void>, resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: unknown) => void }[];
constructor(concurrency: number)
{
this.concurrency = concurrency;
this.runningCount = 0;
this.jobQueue = [];
}
private executeJob(job: () => Promise<void>, resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: unknown) => void): void
{
this.runningCount++;
job().then(resolve).catch(reject).finally(() =>
{
this.runningCount--;
this.tryExecuteNext();
});
}
private tryExecuteNext(): void
{
if (this.runningCount < this.concurrency && this.jobQueue.length > 0)
{
const { job, resolve, reject } = this.jobQueue.shift()!;
this.executeJob(job, resolve, reject);
}
}
public addJob(job: () => Promise<void>): Promise<void>
{
return new Promise<void>((resolve, reject) =>
{
if (this.runningCount < this.concurrency)
{
this.executeJob(job, resolve, reject);
}
else
{
this.jobQueue.push({ job, resolve, reject });
}
});
}
}

View File

@@ -917,7 +917,7 @@ export class InventoryItem
if (evt.createSelected && !evt.object.resolvedAt)
{
// We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory
await agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true);
await agent.currentRegion.clientCommands.region.resolveObject(evt.object, {});
}
if (evt.createSelected && !evt.object.claimedForBuild)
{
@@ -1031,7 +1031,7 @@ export class InventoryItem
if (evt.createSelected && !evt.object.resolvedAt)
{
// We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory
await agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true);
await agent.currentRegion.clientCommands.region.resolveObject(evt.object, {});
}
if (evt.createSelected && !evt.object.claimedForBuild && !claimedPrim)
{

View File

@@ -1,222 +1,86 @@
import { GameObject } from './public/GameObject';
import { PrimFlags, UUID } from '..';
import { PCode, PrimFlags, UUID } from '..';
import { Region } from './Region';
import { IResolveJob } from './interfaces/IResolveJob';
import { Subject, Subscription } from 'rxjs';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import * as LLSD from '@caspertech/llsd';
import { Subscription } from 'rxjs';
import { GetObjectsOptions } from './commands/RegionCommands';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import { clearTimeout } from 'timers';
import { BatchQueue } from './BatchQueue';
export class ObjectResolver
{
private objectsInQueue: { [key: number]: IResolveJob } = {};
private queue: number[] = [];
private maxConcurrency = 128;
private currentlyRunning = false;
private onObjectResolveRan: Subject<GameObject> = new Subject<GameObject>();
private resolveQueue = new BatchQueue<GameObject>(256, this.resolveInternal.bind(this));
private getCostsQueue = new BatchQueue<GameObject>(256, this.getCostsInternal.bind(this));
constructor(private region?: Region)
{
}
resolveObjects(objects: GameObject[], options: GetObjectsOptions): Promise<GameObject[]>
public async resolveObjects(objects: GameObject[], options: GetObjectsOptions): Promise<GameObject[]>
{
return new Promise<GameObject[]>((resolve, reject) =>
if (!this.region)
{
if (!this.region)
throw new Error('Region is going away');
}
// First, create a map of all object IDs
const objs = new Map<number, GameObject>();
const failed: GameObject[] = [];
for (const obj of objects)
{
if (!options.includeTempObjects && ((obj.Flags ?? 0) & PrimFlags.TemporaryOnRez))
{
reject(new Error('Region is going away'));
return;
continue;
}
if (options.outputLog)
if (!options.includeAvatars && obj.PCode === PCode.Avatar)
{
// console.log('[RESOLVER] Scanning ' + objects.length + ' objects, skipInventory: ' + skipInventory);
continue;
}
this.region.objects.populateChildren(obj);
this.scanObject(obj, objs);
}
// First, create a map of all object IDs
const objs: { [key: number]: GameObject } = {};
const failed: GameObject[] = [];
for (const obj of objects)
{
this.region.objects.populateChildren(obj);
this.scanObject(obj, objs);
}
if (objs.size === 0)
{
return failed;
}
const queueObject = (id: number) =>
{
if (this.objectsInQueue[id] === undefined)
{
this.objectsInQueue[id] = {
object: objs[id],
options
};
this.queue.push(id);
}
else if (this.objectsInQueue[id].options.skipInventory && !options.skipInventory)
{
this.objectsInQueue[id].options.skipInventory = false;
}
};
const skipped: number[] = [];
for (const obj of Object.keys(objs))
{
const id = parseInt(obj, 10);
const gameObject = objs[id];
if (options.outputLog === true)
{
// console.log('ResolvedInventory: ' + gameObject.resolvedInventory + ', skip: ' + skipInventory);
}
if (!options.onlyUnresolved || gameObject.resolvedAt === undefined || gameObject.resolvedAt === 0 || (!options.skipInventory && !gameObject.resolvedInventory))
{
if (!options.onlyUnresolved)
{
gameObject.resolvedAt = 0;
gameObject.resolveAttempts = 0;
}
queueObject(id);
}
else
{
skipped.push(id);
}
}
for (const id of skipped)
{
delete objs[id];
if (options.outputLog === true)
{
// console.log('[RESOLVER] Skipping already resolved object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
}
if (Object.keys(objs).length === 0)
{
resolve(failed);
return;
}
let objResolve: Subscription | undefined = undefined;
let objProps: Subscription | undefined = undefined;
const checkObject = (obj: GameObject): boolean =>
{
let done = false;
if (obj.resolvedAt !== undefined && obj.resolvedAt > 0)
{
if (options.skipInventory === true || obj.resolvedInventory)
{
if (options.outputLog === true)
{
// console.log('[RESOLVER] Resolved an object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
done = true;
}
}
if (obj.resolveAttempts > 2)
{
failed.push(obj);
done = true;
}
if (done)
{
delete objs[obj.ID];
if (Object.keys(objs).length === 0)
{
if (objResolve !== undefined)
{
objResolve.unsubscribe();
objResolve = undefined;
}
if (objProps !== undefined)
{
objProps.unsubscribe();
objProps = undefined;
}
resolve(failed);
}
}
return done;
};
objResolve = this.onObjectResolveRan.subscribe((obj: GameObject) =>
{
if (objs[obj.ID] !== undefined)
{
if (options.outputLog === true)
{
// console.log('Got onObjectResolveRan for 1 object ...');
}
if (!checkObject(obj))
{
if (options.outputLog === true)
{
// console.log(' .. Not resolved yet');
}
setTimeout(() =>
{
if (!checkObject(obj))
{
// Requeue
if (options.outputLog)
{
// console.log(' .. ' + obj.ID + ' still not resolved yet, requeuing');
}
queueObject(obj.ID);
this.run().then(() =>
{
}).catch((err) =>
{
console.error(err);
});
}
}, 10000);
}
}
});
if (!this.region)
{
return;
}
objProps = this.region.clientEvents.onObjectResolvedEvent.subscribe((obj: ObjectResolvedEvent) =>
{
if (objs[obj.object.ID] !== undefined)
{
if (options.outputLog)
{
// console.log('Got object resolved event for ' + obj.object.ID);
}
if (!checkObject(obj.object))
{
// console.log(' ... Still not resolved yet');
}
}
});
this.run().then(() =>
{
}).catch((err) =>
{
console.error(err);
});
});
return this.resolveQueue.add(Array.from(objs.values()));
}
private scanObject(obj: GameObject, map: { [key: number]: GameObject }): void
public async getInventory(object: GameObject): Promise<void>
{
await this.getInventories([object]);
}
public async getInventories(objects: GameObject[]): Promise<void>
{
for (const obj of objects)
{
if (!obj.resolvedInventory)
{
await obj.updateInventory();
}
}
}
public async getCosts(objects: GameObject[]): Promise<void>
{
await this.getCostsQueue.add(objects);
}
public shutdown(): void
{
delete this.region;
}
private scanObject(obj: GameObject, map: Map<number, GameObject>): void
{
const localID = obj.ID;
if (!map[localID])
if (!map.has(localID))
{
map[localID] = obj;
map.set(localID, obj);
if (obj.children)
{
for (const child of obj.children)
@@ -227,250 +91,173 @@ export class ObjectResolver
}
}
private async run(): Promise<void>
{
if (this.currentlyRunning)
{
// console.log('Prodded but already running');
return;
}
try
{
// console.log('Running. Queue length: ' + this.queue.length);
while (this.queue.length > 0)
{
const jobs = [];
for (let x = 0; x < this.maxConcurrency && this.queue.length > 0; x++)
{
const objectID = this.queue.shift();
if (objectID !== undefined)
{
jobs.push(this.objectsInQueue[objectID]);
delete this.objectsInQueue[objectID];
}
}
await this.doResolve(jobs);
}
}
catch (error)
{
console.error(error);
}
finally
{
this.currentlyRunning = false;
}
if (this.queue.length > 0)
{
this.run().then(() =>
{
}, (err) =>
{
console.error(err);
});
}
}
private async doResolve(jobs: IResolveJob[]): Promise<void>
private async resolveInternal(objs: Set<GameObject>): Promise<Set<GameObject>>
{
if (!this.region)
{
return;
throw new Error('Region went away');
}
const resolveTime = new Date().getTime() / 1000;
const objectList = [];
let totalRemaining = 0;
const objArray = Array.from(objs.values());
try
{
for (const job of jobs)
{
if (job.object.resolvedAt === undefined || job.object.resolvedAt < resolveTime)
{
objectList.push(job.object);
totalRemaining++;
}
}
if (objectList.length > 0)
{
// console.log('Selecting ' + objectList.length + ' objects');
if (!this.region)
{
return;
}
await this.region.clientCommands.region.selectObjects(objectList);
// console.log('Deselecting ' + objectList.length + ' objects');
if (!this.region)
{
return;
}
await this.region.clientCommands.region.deselectObjects(objectList);
for (const chk of objectList)
{
if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime)
{
totalRemaining --;
}
}
}
for (const job of jobs)
{
if (!this.region)
{
return;
}
if (!job.options.skipInventory && (job.options.includeTempObjects || ((job.object.Flags ?? 0) & PrimFlags.TemporaryOnRez) === 0))
{
const o = job.object;
if ((o.resolveAttempts === undefined || o.resolveAttempts < 3) && o.FullID !== undefined && o.name !== undefined && o.Flags !== undefined && !(o.Flags & PrimFlags.InventoryEmpty) && (!o.inventory || o.inventory.length === 0))
{
if (job.options.outputLog)
{
// console.log('Processing inventory for ' + job.object.ID);
}
try
{
await o.updateInventory();
}
catch (error)
{
if (o.resolveAttempts === undefined)
{
o.resolveAttempts = 0;
}
o.resolveAttempts++;
if (o.FullID !== undefined)
{
console.error('Error downloading task inventory of ' + o.FullID.toString() + ':');
console.error(error);
}
else
{
console.error('Error downloading task inventory of ' + o.ID + ':');
console.error(error);
}
}
}
else
{
if (job.options.outputLog)
{
// console.log('Skipping inventory for ' + job.object.ID);
}
}
o.resolvedInventory = true;
}
}
}
catch (ignore)
{
console.error(ignore);
await this.region.clientCommands.region.selectObjects(objArray);
}
finally
{
if (totalRemaining < 1)
await this.region.clientCommands.region.deselectObjects(objArray);
}
if (!this.region)
{
throw new Error('Region went away');
}
const objects = new Map<number, GameObject>();
for (const obj of objs.values())
{
objects.set(obj.ID, obj);
}
for (let x = 0; x < 3; x++)
{
try
{
totalRemaining = 0;
for (const obj of objectList)
{
if (obj.resolvedAt === undefined || obj.resolvedAt < resolveTime)
{
totalRemaining++;
}
}
if (totalRemaining > 0)
{
console.error(totalRemaining + ' objects could not be resolved');
}
await this.waitForResolve(objects, 10000);
}
const that = this;
const getCosts = async function(objIDs: UUID[]): Promise<void>
catch (_e)
{
// Ignore
}
}
const failed = new Set<GameObject>();
for (const o of objects.values())
{
failed.add(o);
}
return failed;
}
private async getCostsInternal(objs: Set<GameObject>): Promise<Set<GameObject>>
{
const failed = new Set<GameObject>();
const submitted: Map<string, GameObject> = new Map<string, GameObject>();
for (const obj of objs.values())
{
submitted.set(obj.FullID.toString(), obj);
}
try
{
if (!this.region)
{
return objs;
}
const result = await this.region.caps.capsPostXML('GetObjectCost', {
'object_ids': Array.from(submitted.keys())
});
const uuids = Object.keys(result);
for (const key of uuids)
{
const costs = result[key];
try
{
if (!that.region)
if (!this.region)
{
return;
continue;
}
const result = await that.region.caps.capsPostXML('GetObjectCost', {
'object_ids': objIDs
});
const uuids = Object.keys(result);
for (const key of uuids)
{
const costs = result[key];
try
{
if (!that.region)
{
return;
}
const obj: GameObject = that.region.objects.getObjectByUUID(new UUID(key));
obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']);
obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']);
obj.physicaImpact = parseFloat(costs['physics_cost']);
obj.resourceImpact = parseFloat(costs['resource_cost']);
obj.limitingType = costs['resource_limiting_type'];
const obj: GameObject = this.region.objects.getObjectByUUID(new UUID(key));
obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']);
obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']);
obj.physicaImpact = parseFloat(costs['physics_cost']);
obj.resourceImpact = parseFloat(costs['resource_cost']);
obj.limitingType = costs['resource_limiting_type'];
obj.landImpact = Math.round(obj.linkPhysicsImpact);
if (obj.linkResourceImpact > obj.linkPhysicsImpact)
{
obj.landImpact = Math.round(obj.linkResourceImpact);
}
obj.calculatedLandImpact = obj.landImpact;
if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy')
{
obj.calculatedLandImpact = 0;
}
}
catch (error)
{
}
obj.landImpact = Math.round(obj.linkPhysicsImpact);
if (obj.linkResourceImpact > obj.linkPhysicsImpact)
{
obj.landImpact = Math.round(obj.linkResourceImpact);
}
obj.calculatedLandImpact = obj.landImpact;
if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy')
{
obj.calculatedLandImpact = 0;
}
submitted.delete(key);
}
catch (error)
{
}
};
let ids: UUID[] = [];
const promises: Promise<void>[] = [];
for (const job of jobs)
{
if (job.object.landImpact === undefined)
{
ids.push(new LLSD.UUID(job.object.FullID));
}
if (ids.length > 255)
{
promises.push(getCosts(ids));
ids = [];
}
}
if (ids.length > 0)
{
promises.push(getCosts(ids));
}
// console.log('Waiting for all');
await Promise.all(promises);
for (const job of jobs)
{
if (job.options.outputLog)
{
// console.log('Signalling resolve OK for ' + job.object.ID);
}
this.onObjectResolveRan.next(job.object);
}
}
catch (error)
{
}
for (const go of submitted.values())
{
failed.add(go);
}
return failed;
}
public shutdown(): void
private async waitForResolve(objs: Map<number, GameObject>, timeout: number = 10000): Promise<void>
{
delete this.region;
const entries = objs.entries();
for (const [localID, entry] of entries)
{
if (entry.resolvedAt !== undefined)
{
objs.delete(localID);
}
}
if (objs.size === 0)
{
return;
}
return new Promise<void>((resolve, reject) =>
{
if (!this.region)
{
reject(new Error('Region went away'));
return;
}
let subs: Subscription | undefined = undefined;
let timer: number | undefined = undefined;
subs = this.region.clientEvents.onObjectResolvedEvent.subscribe((obj: ObjectResolvedEvent) =>
{
objs.delete(obj.object.ID);
if (objs.size === 0)
{
if (timer !== undefined)
{
clearTimeout(timer);
timer = undefined;
}
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
resolve();
}
});
timer = setTimeout(() =>
{
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
reject(new Error('Timeout'));
}, timeout) as unknown as number;
});
}
}

View File

@@ -36,12 +36,12 @@ import { Vector3 } from './Vector3';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import { Avatar } from './public/Avatar';
import Timer = NodeJS.Timer;
import { GenericStreamingMessageMessage } from './messages/GenericStreamingMessage';
import { LLSDNotationParser } from './llsd/LLSDNotationParser';
import { LLSDMap } from './llsd/LLSDMap';
import { LLGLTFMaterialOverride, LLGLTFTextureTransformOverride } from './LLGLTFMaterialOverride';
import * as Long from 'long';
import Timer = NodeJS.Timer;
export class ObjectStoreLite implements IObjectStore
{
@@ -90,6 +90,7 @@ export class ObjectStoreLite implements IObjectStore
private selectedPrimsWithoutUpdate = new Map<number, boolean>();
private selectedChecker?: Timer;
private blacklist: Map<number, Date> = new Map<number, Date>();
private pendingResolves: Set<number> = new Set<number>();
rtree?: RBush3D;
@@ -739,7 +740,7 @@ export class ObjectStoreLite implements IObjectStore
invItemID = new UUID(obj.NameValue['AttachItemID'].value);
}
this.agent.currentRegion.clientCommands.region.resolveObject(obj, true, false).then(() =>
this.agent.currentRegion.clientCommands.region.resolveObject(obj, {}).then(() =>
{
try
{
@@ -796,6 +797,11 @@ export class ObjectStoreLite implements IObjectStore
}
}
public pendingResolve(id: number): void
{
this.pendingResolves.add(id);
}
protected objectUpdateCached(objectUpdateCached: ObjectUpdateCachedMessage): void
{
if (this.circuit === undefined)

View File

@@ -989,4 +989,15 @@ export class Utils
lineObj.pos += Buffer.byteLength(line) + 1;
return line.replace(/\r/, '').trim().replace(/[\t ]+/g, ' ');
}
public static sleep(ms: number): Promise<void>
{
return new Promise((resolve) =>
{
setTimeout(() =>
{
resolve();
}, ms)
});
}
}

View File

@@ -23,7 +23,6 @@ import { ObjectPropertiesMessage } from '../messages/ObjectProperties';
import { ObjectSelectMessage } from '../messages/ObjectSelect';
import { RegionHandleRequestMessage } from '../messages/RegionHandleRequest';
import { RegionIDAndHandleReplyMessage } from '../messages/RegionIDAndHandleReply';
import { ObjectResolver } from '../ObjectResolver';
import { Avatar } from '../public/Avatar';
import { GameObject } from '../public/GameObject';
import { Parcel } from '../public/Parcel';
@@ -39,9 +38,6 @@ import Timer = NodeJS.Timer;
export interface GetObjectsOptions
{
resolve?: boolean;
onlyUnresolved?: boolean;
skipInventory?: boolean;
outputLog?: boolean;
includeTempObjects?: boolean;
includeAvatars?: boolean;
}
@@ -382,14 +378,24 @@ export class RegionCommands extends CommandsBase
return this.currentRegion.regionName;
}
async resolveObject(object: GameObject, forceResolve = false, skipInventory = false): Promise<GameObject[]>
async resolveObject(object: GameObject, options: GetObjectsOptions): Promise<GameObject[]>
{
return this.currentRegion.resolver.resolveObjects([object], { onlyUnresolved: !forceResolve, skipInventory });
return this.currentRegion.resolver.resolveObjects([object], options);
}
async resolveObjects(objects: GameObject[], forceResolve = false, skipInventory = false, outputLog = false): Promise<GameObject[]>
async resolveObjects(objects: GameObject[], options: GetObjectsOptions): Promise<GameObject[]>
{
return this.currentRegion.resolver.resolveObjects(objects, { onlyUnresolved: !forceResolve, skipInventory, outputLog });
return this.currentRegion.resolver.resolveObjects(objects, options);
}
public async fetchObjectInventory(object: GameObject): Promise<void>
{
return this.currentRegion.resolver.getInventory(object);
}
public async fetchObjectInventories(objects: GameObject[]): Promise<void>
{
return this.currentRegion.resolver.getInventories(objects);
}
private waitForObjectByLocalID(localID: number, timeout: number): Promise<GameObject>
@@ -1410,7 +1416,7 @@ export class RegionCommands extends CommandsBase
if (!evt.object.resolvedAt)
{
// We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory
await this.resolveObject(evt.object, false, true);
await this.resolveObject(evt.object, {});
}
if (evt.createSelected && !evt.object.claimedForBuild)
{
@@ -1623,23 +1629,7 @@ export class RegionCommands extends CommandsBase
const objs = await this.currentRegion.objects.getAllObjects();
if (options.resolve)
{
const resolver = new ObjectResolver(this.currentRegion);
const incl: GameObject[] = [];
for (const obj of objs)
{
if (!options.includeAvatars && obj.PCode === PCode.Avatar)
{
continue;
}
if (!options.includeTempObjects && (((obj.Flags ?? 0) & (PrimFlags.Temporary | PrimFlags.TemporaryOnRez)) !== 0))
{
continue;
}
incl.push(obj);
}
await resolver.resolveObjects(incl, options);
await this.currentRegion.resolver.resolveObjects(objs, options);
}
return objs;
}

View File

@@ -66,7 +66,7 @@ export class Avatar extends AvatarQueryResult
const objs: GameObject[] = this._gameObject.region.objects.getObjectsByParent(this._gameObject.ID);
for (const attachment of objs)
{
this._gameObject.region.clientCommands.region.resolveObject(attachment, true, false).then(() =>
this._gameObject.region.clientCommands.region.resolveObject(attachment, {}).then(() =>
{
this.addAttachment(attachment);
}).catch(() =>
@@ -238,8 +238,11 @@ export class Avatar extends AvatarQueryResult
{
if (obj.itemID !== undefined)
{
this.attachments[obj.itemID.toString()] = obj;
this.onAttachmentAdded.next(obj);
if (this.attachments[obj.itemID.toString()] === undefined)
{
this.attachments[obj.itemID.toString()] = obj;
this.onAttachmentAdded.next(obj);
}
}
}

View File

@@ -946,8 +946,13 @@ export class GameObject implements IGameObjectData
throw new Error('Failed to add script to object');
}
updateInventory(): Promise<void>
public async updateInventory(): Promise<void>
{
if (this.PCode === PCode.Avatar)
{
return;
}
const req = new RequestTaskInventoryMessage();
req.AgentData = {
AgentID: this.region.agent.agentID,
@@ -1501,10 +1506,14 @@ export class GameObject implements IGameObjectData
private async getXML(xml: XMLNode, rootPrim: GameObject, linkNum: number, rootNode?: string): Promise<void>
{
if ((this.resolvedAt === undefined || this.resolvedAt === 0 || !this.resolvedInventory) && this.region?.resolver)
const resolver = this.region?.resolver;
if (resolver)
{
await this.region.resolver.resolveObjects([this], { onlyUnresolved: false });
await resolver.resolveObjects([this], { includeTempObjects: true });
await resolver.getInventory(this);
await resolver.getCosts([this]);
}
let root = xml;
if (rootNode)
{