Vastly simplify the object resolution stuff.
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user