Vastly simplify the object resolution stuff.
This commit is contained in:
227
lib/classes/BatchQueue.spec.ts
Normal file
227
lib/classes/BatchQueue.spec.ts
Normal 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
126
lib/classes/BatchQueue.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
lib/classes/ConcurrentQueue.ts
Normal file
47
lib/classes/ConcurrentQueue.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@caspertech/node-metaverse",
|
||||
"version": "0.6.22",
|
||||
"version": "0.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@caspertech/node-metaverse",
|
||||
"version": "0.6.22",
|
||||
"version": "0.7.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@caspertech/llsd": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@caspertech/node-metaverse",
|
||||
"version": "0.6.22",
|
||||
"version": "0.7.2",
|
||||
"description": "A node.js interface for Second Life.",
|
||||
"main": "dist/lib/index.js",
|
||||
"types": "dist/lib/index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user