diff --git a/packages/avatar/src/AvatarAssetDownloadManager.ts b/packages/avatar/src/AvatarAssetDownloadManager.ts index 51a3389a..8ea0687b 100644 --- a/packages/avatar/src/AvatarAssetDownloadManager.ts +++ b/packages/avatar/src/AvatarAssetDownloadManager.ts @@ -302,7 +302,7 @@ export class AvatarAssetDownloadManager this._figureListeners.set(figure, listeners); } - listeners.push(listener); + if(listeners.indexOf(listener) === -1) listeners.push(listener); } this._incompleteFigures.set(figure, pendingLibraries); diff --git a/packages/avatar/src/AvatarStructure.ts b/packages/avatar/src/AvatarStructure.ts index d70d46c9..2fa81317 100644 --- a/packages/avatar/src/AvatarStructure.ts +++ b/packages/avatar/src/AvatarStructure.ts @@ -333,7 +333,8 @@ export class AvatarStructure if(!action) return []; - const activePartTypes = this._partSetsData.getActiveParts(action.definition); + let activePartTypes = this._partSetsData.getActiveParts(action.definition); + let activePartTypesCopied = false; const partContainers: AvatarImagePartContainer[] = []; let defaultFrames: any[] = [0]; const animationAction = this._animationData.getAction(action.definition); @@ -357,6 +358,12 @@ export class AvatarStructure { for(const dynamicPart of geometryBodyPart.getDynamicParts(avatar)) { + if(!activePartTypesCopied) + { + activePartTypes = activePartTypes.slice(); + activePartTypesCopied = true; + } + activePartTypes.push(dynamicPart.id); } } @@ -370,8 +377,6 @@ export class AvatarStructure const mainAction = avatar?.getMainAction?.(); const isSittingPosture = (mainAction?.definition?.assetPartDefinition === 'sit') || (action.definition.assetPartDefinition === 'sit'); - // Effect 77 = "Riding". While in the saddle the companion/buddy ('pt') part - // is hidden the same way it is while sitting. const isRidingPosture = (avatar?.getEffectId?.() === 77); const hidePetPart = isSittingPosture || isRidingPosture; diff --git a/packages/avatar/src/EffectAssetDownloadManager.ts b/packages/avatar/src/EffectAssetDownloadManager.ts index 27dc7dca..ea436d3e 100644 --- a/packages/avatar/src/EffectAssetDownloadManager.ts +++ b/packages/avatar/src/EffectAssetDownloadManager.ts @@ -210,7 +210,7 @@ export class EffectAssetDownloadManager if(!listeners) listeners = []; - listeners.push(listener); + if(listeners.indexOf(listener) === -1) listeners.push(listener); this._effectListeners.set(id.toString(), listeners); } diff --git a/packages/avatar/src/alias/AssetAliasCollection.ts b/packages/avatar/src/alias/AssetAliasCollection.ts index 3602b1f9..07c649d4 100644 --- a/packages/avatar/src/alias/AssetAliasCollection.ts +++ b/packages/avatar/src/alias/AssetAliasCollection.ts @@ -8,6 +8,7 @@ export class AssetAliasCollection private _aliases: Map; private _avatarRenderManager: AvatarRenderManager; private _missingAssetNames: string[]; + private _processedCollections: WeakSet = new WeakSet(); constructor(avatarRenderManager: AvatarRenderManager, assets: IAssetManager) { @@ -32,7 +33,9 @@ export class AssetAliasCollection { for(const collection of this._assets.collections.values()) { - if(!collection) continue; + if(!collection || this._processedCollections.has(collection)) continue; + + this._processedCollections.add(collection); const aliases = collection.data && collection.data.aliases; diff --git a/packages/avatar/src/cache/AvatarImageCache.ts b/packages/avatar/src/cache/AvatarImageCache.ts index 22669da9..f93d7c23 100644 --- a/packages/avatar/src/cache/AvatarImageCache.ts +++ b/packages/avatar/src/cache/AvatarImageCache.ts @@ -16,6 +16,9 @@ import { ImageData } from './ImageData'; export class AvatarImageCache { private static DEFAULT_MAX_CACHE_STORAGE_TIME_MS: number = 60000; + // Shared read-only defaults for the per-frame hot path — never mutate. + private static EMPTY_REMOVE_DATA: string[] = []; + private static EMPTY_ITEMS: Map = new Map(); private _structure: AvatarStructure; private _avatar: IAvatarImage; @@ -176,9 +179,7 @@ export class AvatarImageCache public getImageContainer(key: string, frameNumber: number, forceRefresh: boolean = false): AvatarImageBodyPartContainer { - const bodyPartCache = this.getBodyPartCache(key) || new AvatarImageBodyPartCache(); - - this._cache.set(key, bodyPartCache); + const bodyPartCache = this.getBodyPartCache(key); let direction = bodyPartCache.getDirection(); let action = bodyPartCache.getAction(); @@ -187,9 +188,11 @@ export class AvatarImageCache if(action.definition.startFromFrameZero) adjustedFrameCount -= action.startFrame; let adjustedAction = action; - let removeData: string[] = []; - let items: Map = new Map(); + let removeData: string[] = AvatarImageCache.EMPTY_REMOVE_DATA; + let items: Map = AvatarImageCache.EMPTY_ITEMS; + // NOT a shared scratch: the (cached) image container stores this + // Point by reference via its offset setter. const positionOffset = new Point(); if(action.definition.isAnimation) diff --git a/packages/room/src/RoomInstance.ts b/packages/room/src/RoomInstance.ts index 53408a60..8aaa4a5a 100644 --- a/packages/room/src/RoomInstance.ts +++ b/packages/room/src/RoomInstance.ts @@ -230,8 +230,12 @@ export class RoomInstance implements IRoomInstance if(!objects.length) continue; - for(const object of objects.getValues()) + const total = objects.length; + + for(let index = 0; index < total; index++) { + const object = objects.getWithIndex(index); + if(!object) continue; const logic = object.logic; diff --git a/packages/room/src/object/visualization/RoomObjectSprite.ts b/packages/room/src/object/visualization/RoomObjectSprite.ts index 5577aa3c..e20d1e25 100644 --- a/packages/room/src/object/visualization/RoomObjectSprite.ts +++ b/packages/room/src/object/visualization/RoomObjectSprite.ts @@ -122,7 +122,6 @@ export class RoomObjectSprite implements IRoomObjectSprite return this._height; } - // Per-sprite zoom multiplier (1 = native). Applied on top of the room zoom. public get scale(): number { return this._scale; @@ -360,6 +359,8 @@ export class RoomObjectSprite implements IRoomObjectSprite public set filters(filters: Filter[]) { + if(this._filters === filters) return; + this._filters = filters; this._updateCounter++; diff --git a/packages/room/src/object/visualization/avatar/AvatarVisualization.ts b/packages/room/src/object/visualization/avatar/AvatarVisualization.ts index d4696d4c..d55fce6c 100644 --- a/packages/room/src/object/visualization/avatar/AvatarVisualization.ts +++ b/packages/room/src/object/visualization/avatar/AvatarVisualization.ts @@ -1,6 +1,6 @@ import { AlphaTolerance, AvatarAction, AvatarGuideStatus, AvatarSetType, IAdvancedMap, IAvatarEffectListener, IAvatarImage, IAvatarImageListener, IGraphicAsset, IObjectVisualizationData, IRoomGeometry, IRoomObject, IRoomObjectModel, RoomObjectSpriteType, RoomObjectVariable } from '@octane/api'; import { GetAssetManager } from '@octane/assets'; -import { AdvancedMap, GetRenderer } from '@octane/utils'; +import { AdvancedMap, GetRenderer, Vector3d } from '@octane/utils'; import { Container, RenderTexture, Sprite, Texture } from 'pixi.js'; import { RoomObjectSpriteVisualization } from '../RoomObjectSpriteVisualization'; import { RoomWindowReflectionState } from '../RoomWindowReflectionState'; @@ -87,6 +87,9 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement private _reflectionOppositeDirection: number; private _reflectionOppositeBaseTexture: Texture; private _windowReflectionPushed: boolean; + private _lastReflectionPushedTexture: Texture; + private _lastReflectionPushedDirection: number; + private _lastReflectionPushedLocation: Vector3d; private _additions: Map; @@ -146,6 +149,9 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement this._reflectionOppositeDirection = -1; this._reflectionOppositeBaseTexture = null; this._windowReflectionPushed = false; + this._lastReflectionPushedTexture = null; + this._lastReflectionPushedDirection = -1; + this._lastReflectionPushedLocation = new Vector3d(); this._additions = new Map(); } @@ -191,6 +197,13 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement if(this.object) RoomWindowReflectionState.removeAvatar(this.object.id, this.object.model?.getValue(RoomObjectVariable.OBJECT_ROOM_ID)); + if(this._additions) + { + for(const addition of this._additions.values()) addition?.dispose(); + + this._additions.clear(); + } + this._shadow = null; this._disposed = true; } @@ -305,7 +318,7 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement } else { - this.updateWindowReflectionSource(); + this.updateWindowReflectionSource(true); return; } @@ -1140,7 +1153,7 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement return target; } - private updateWindowReflectionSource(): void + private updateWindowReflectionSource(skipIfUnchanged: boolean = false): void { if(!this.object) return; @@ -1148,6 +1161,17 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement if(sprite?.texture) { + if(skipIfUnchanged && this._windowReflectionPushed) + { + const location = this.object.getLocation(); + + if((sprite.texture === this._lastReflectionPushedTexture) && + (this.object.getDirection().x === this._lastReflectionPushedDirection) && + (location.x === this._lastReflectionPushedLocation.x) && + (location.y === this._lastReflectionPushedLocation.y) && + (location.z === this._lastReflectionPushedLocation.z)) return; + } + const roomId = this.object.model?.getValue(RoomObjectVariable.OBJECT_ROOM_ID); if(!RoomWindowReflectionState.hasZones || !RoomWindowReflectionState.isNearAnyZone(this.object.getLocation(), roomId)) @@ -1157,6 +1181,7 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement RoomWindowReflectionState.removeAvatar(this.object.id, roomId); this._windowReflectionPushed = false; + this._lastReflectionPushedTexture = null; } return; @@ -1206,6 +1231,9 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement RoomWindowReflectionState.setAvatar(this.object.id, sprite.texture, this.object.getLocation(), this._reflectionVerticalOffset, this.object.getDirection().x, oppositeTexture, roomId); this._windowReflectionPushed = true; + this._lastReflectionPushedTexture = sprite.texture; + this._lastReflectionPushedDirection = this.object.getDirection().x; + this._lastReflectionPushedLocation.assign(this.object.getLocation()); return; } @@ -1213,6 +1241,7 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement RoomWindowReflectionState.removeAvatar(this.object.id, this.object.model?.getValue(RoomObjectVariable.OBJECT_ROOM_ID)); this._windowReflectionPushed = false; + this._lastReflectionPushedTexture = null; } private clearAvatar(): void @@ -1247,6 +1276,8 @@ export class AvatarVisualization extends RoomObjectSpriteVisualization implement if(this.object) RoomWindowReflectionState.removeAvatar(this.object.id, this.object.model?.getValue(RoomObjectVariable.OBJECT_ROOM_ID)); this._windowReflectionPushed = false; + this._lastReflectionPushedTexture = null; + this._lastReflectionPushedDirection = -1; } private getAddition(id: number): IAvatarAddition diff --git a/packages/room/src/object/visualization/avatar/additions/HabbiconAssetManager.ts b/packages/room/src/object/visualization/avatar/additions/HabbiconAssetManager.ts index 8b7615bd..3ecf54aa 100644 --- a/packages/room/src/object/visualization/avatar/additions/HabbiconAssetManager.ts +++ b/packages/room/src/object/visualization/avatar/additions/HabbiconAssetManager.ts @@ -38,6 +38,7 @@ export type HabbiconRuntimeAsset = { export class HabbiconAssetManager { private static _instance: HabbiconAssetManager = null; + private static MAX_COMPOSED_TEXTURES: number = 256; private static FRAME_SIZE: number = 40; private static OUTLINE_SIZE: number = 2; private static SHADOW_PADDING: number = 5; @@ -153,7 +154,13 @@ export class HabbiconAssetManager const existing = this._composedTextures.get(cacheKey); - if(existing) return existing; + if(existing) + { + this._composedTextures.delete(cacheKey); + this._composedTextures.set(cacheKey, existing); + + return existing; + } const composed = this.composeBubbleCanvas(source, sourceAlpha, backgroundAlpha, mirrored); @@ -163,6 +170,16 @@ export class HabbiconAssetManager this._composedTextures.set(cacheKey, texture); + while(this._composedTextures.size > HabbiconAssetManager.MAX_COMPOSED_TEXTURES) + { + const oldestKey = this._composedTextures.keys().next().value; + const oldest = this._composedTextures.get(oldestKey); + + this._composedTextures.delete(oldestKey); + + if(oldest && !oldest.destroyed) oldest.destroy(true); + } + return texture; } diff --git a/packages/room/src/object/visualization/avatar/additions/HabbiconBubbleAddition.ts b/packages/room/src/object/visualization/avatar/additions/HabbiconBubbleAddition.ts index 6b457d08..ea07aa2c 100644 --- a/packages/room/src/object/visualization/avatar/additions/HabbiconBubbleAddition.ts +++ b/packages/room/src/object/visualization/avatar/additions/HabbiconBubbleAddition.ts @@ -298,6 +298,11 @@ export class HabbiconBubbleAddition implements IAvatarAddition return Math.round((1 - progress) * HabbiconBubbleAddition.INTRO_START_OFFSET_Y); } + private quantizeAlpha(alpha: number): number + { + return Math.min(255, Math.round(alpha / 8) * 8); + } + private resolveAlpha(now: number): number { if(!this._startedAt || this._sourceFadeOutAt <= this._startedAt) return 255; @@ -307,7 +312,7 @@ export class HabbiconBubbleAddition implements IAvatarAddition if(this._sourceHideAt > this._startedAt && now >= this._sourceHideAt) return 0; - return Math.round(255 * Math.min(fadeIn, fadeOut)); + return this.quantizeAlpha(Math.round(255 * Math.min(fadeIn, fadeOut))); } private resolveBackgroundAlpha(now: number): number @@ -317,7 +322,7 @@ export class HabbiconBubbleAddition implements IAvatarAddition const fadeIn = Math.min(1, Math.max(0, (now - this._startedAt) / HabbiconBubbleAddition.FADE_IN_DURATION_MS)); const fadeOut = now < this._backgroundFadeOutAt ? 1 : 1 - Math.min(1, Math.max(0, (now - this._backgroundFadeOutAt) / HabbiconBubbleAddition.BACKGROUND_FADE_OUT_DURATION_MS)); - return Math.round(255 * Math.min(fadeIn, fadeOut)); + return this.quantizeAlpha(Math.round(255 * Math.min(fadeIn, fadeOut))); } private resolveFrameAnchorCompensationX(): number diff --git a/packages/room/src/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts b/packages/room/src/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts index d61b0c20..305bc0fc 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureDynamicThumbnailVisualization.ts @@ -37,7 +37,7 @@ export class FurnitureDynamicThumbnailVisualization extends IsometricImageFurniV const texture = Texture.from(image); texture.source.scaleMode = 'linear'; - this.setThumbnailImages(texture, thumbnailUrl); + this.setThumbnailImages(texture, thumbnailUrl, true); } else { diff --git a/packages/room/src/object/visualization/furniture/FurnitureParticleSystem.ts b/packages/room/src/object/visualization/furniture/FurnitureParticleSystem.ts index 9b7b0f03..cff10e6e 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureParticleSystem.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureParticleSystem.ts @@ -49,7 +49,7 @@ export class FurnitureParticleSystem if(this._canvasTexture) { - this._canvasTexture.destroy(); + this._canvasTexture.destroy(true); this._canvasTexture = null; } @@ -71,8 +71,17 @@ export class FurnitureParticleSystem this._particleSprite = null; } - this._blackOverlayAlphaTransform = null; - this._particleColorTransform = null; + if(this._blackOverlayAlphaTransform) + { + this._blackOverlayAlphaTransform.destroy(); + this._blackOverlayAlphaTransform = null; + } + + if(this._particleColorTransform) + { + this._particleColorTransform.destroy(); + this._particleColorTransform = null; + } this._identityMatrix = null; this._translationMatrix = null; } @@ -111,7 +120,7 @@ export class FurnitureParticleSystem if(this._canvasTexture && ((this._canvasTexture.width !== this._roomSprite.width) || (this._canvasTexture.height !== this._roomSprite.height))) { - this._canvasTexture.destroy(); + this._canvasTexture.destroy(true); this._canvasTexture = null; } @@ -310,7 +319,7 @@ export class FurnitureParticleSystem if(this._canvasTexture) { - this._canvasTexture.destroy(); + this._canvasTexture.destroy(true); this._canvasTexture = null; } } diff --git a/packages/room/src/object/visualization/furniture/FurnitureVisualization.ts b/packages/room/src/object/visualization/furniture/FurnitureVisualization.ts index c68c9bcc..9bc9f9ac 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureVisualization.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureVisualization.ts @@ -543,9 +543,14 @@ export class FurnitureVisualization extends RoomObjectSpriteVisualization sprite.posture = this.getPostureForAsset(scale, assetData.source); sprite.clickHandling = this._clickHandling; - const chooserFilters = (sprite.filters || []).filter(f => f instanceof ChooserSelectionFilter); + const currentFilters = sprite.filters; - sprite.filters = chooserFilters.length > 0 ? [...this._filters, ...chooserFilters] : this._filters; + if((currentFilters && currentFilters.length) || this._filters.length) + { + const chooserFilters = (currentFilters || []).filter(f => f instanceof ChooserSelectionFilter); + + sprite.filters = chooserFilters.length > 0 ? [...this._filters, ...chooserFilters] : this._filters; + } } else { diff --git a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts index 74d8a581..51effe4b 100644 --- a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts +++ b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts @@ -8,6 +8,7 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza protected static THUMBNAIL: string = 'THUMBNAIL'; private _thumbnailImageNormal: Texture; + private _thumbnailImageOwned: boolean = false; private _thumbnailDirection: number; private _thumbnailSize: number; private _thumbnailChanged: boolean; @@ -39,7 +40,14 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza } this._thumbnailTexture = null; + + if(this._thumbnailImageOwned && this._thumbnailImageNormal && !this._thumbnailImageNormal.destroyed) + { + this._thumbnailImageNormal.destroy(true); + } + this._thumbnailImageNormal = null; + this._thumbnailImageOwned = false; super.dispose(); } @@ -49,9 +57,15 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza return !(this._thumbnailImageNormal == null); } - public setThumbnailImages(texture: Texture, url?: string): void + public setThumbnailImages(texture: Texture, url?: string, owned: boolean = false): void { + if(this._thumbnailImageOwned && this._thumbnailImageNormal && (this._thumbnailImageNormal !== texture) && !this._thumbnailImageNormal.destroyed) + { + this._thumbnailImageNormal.destroy(true); + } + this._thumbnailImageNormal = texture; + this._thumbnailImageOwned = (owned && !!texture); this._photoUrl = url || null; this._thumbnailChanged = true; } diff --git a/packages/room/src/object/visualization/pet/ExperienceData.ts b/packages/room/src/object/visualization/pet/ExperienceData.ts index de31dbd5..328ddca0 100644 --- a/packages/room/src/object/visualization/pet/ExperienceData.ts +++ b/packages/room/src/object/visualization/pet/ExperienceData.ts @@ -18,7 +18,11 @@ export class ExperienceData public renderBubble(amount: number): Texture { - if(!this._sprite || (this._amount === amount)) return null; + if(!this._sprite) return null; + + if((this._amount === amount) && this._texture) return this._texture; + + this._amount = amount; const container = new Container(); @@ -50,9 +54,27 @@ export class ExperienceData TextureUtils.writeToTexture(container, this._texture, true); } + container.removeChild(this._sprite); + container.destroy({ children: true, style: true }); + return this._texture; } + public dispose(): void + { + if(this._sprite) + { + this._sprite.destroy(); + this._sprite = null; + } + + if(this._texture) + { + this._texture.destroy(true); + this._texture = null; + } + } + public get amount(): number { return this._amount; diff --git a/packages/room/src/object/visualization/pet/PetVisualization.ts b/packages/room/src/object/visualization/pet/PetVisualization.ts index 3dcf1550..a62bf0ab 100644 --- a/packages/room/src/object/visualization/pet/PetVisualization.ts +++ b/packages/room/src/object/visualization/pet/PetVisualization.ts @@ -104,6 +104,13 @@ export class PetVisualization extends FurnitureAnimatedVisualization this._animationStates = null; } + + if(this._experienceData) + { + this._experienceData.dispose(); + + this._experienceData = null; + } } protected getAnimationId(animationData: AnimationStateData): number diff --git a/packages/room/src/object/visualization/room/RoomPlane.ts b/packages/room/src/object/visualization/room/RoomPlane.ts index 65a70a1a..f7852d7f 100644 --- a/packages/room/src/object/visualization/room/RoomPlane.ts +++ b/packages/room/src/object/visualization/room/RoomPlane.ts @@ -74,6 +74,7 @@ export class RoomPlane implements IRoomPlane private _animationLayers: PlaneVisualizationAnimationLayer[] = []; private _isAnimated: boolean = false; private _lastAnimationUpdate: number = 0; + private _animationRenderTime: number = -1; private _animationCanvasWidth: number = 0; private _animationCanvasHeight: number = 0; private _landscapeRenderWidth: number = 0; @@ -680,10 +681,15 @@ export class RoomPlane implements IRoomPlane if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE) { const timeSinceLastUpdate = timeSinceStartMs - this._lastAnimationUpdate; - if(timeSinceLastUpdate >= RoomPlane.ANIMATION_UPDATE_INTERVAL || needsUpdate || reflectionUpdate) + if((timeSinceLastUpdate >= RoomPlane.ANIMATION_UPDATE_INTERVAL) || (this._animationRenderTime < 0)) { animationUpdate = true; this._lastAnimationUpdate = timeSinceStartMs; + this._animationRenderTime = timeSinceStartMs; + } + else if(needsUpdate || reflectionUpdate) + { + animationUpdate = true; } } @@ -720,7 +726,7 @@ export class RoomPlane implements IRoomPlane if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0) { - this.renderAnimationLayers(timeSinceStartMs, geometry); + this.renderAnimationLayers(((this._animationRenderTime >= 0) ? this._animationRenderTime : timeSinceStartMs), geometry); } if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeForegroundTexture) @@ -1158,6 +1164,19 @@ export class RoomPlane implements IRoomPlane } const screenSpot = projection.apply(new Point(x, y)); + + const avatarPxPerTile = (canvasWidth / this._leftSide.length); + + if(normal2DLength > 0.0001) + { + const depthX = (-normalX * planeDistance); + const depthY = (-normalY * planeDistance); + + screenSpot.x += ((depthX - depthY) * avatarPxPerTile); + } + + screenSpot.y += (avatarPxPerTile * 0.35); + const uprightMatrix = projectionInverse.clone().append(new Matrix(1, 0, 0, 1, Math.trunc(screenSpot.x), Math.trunc(screenSpot.y))); let avatarParent: Container = container; @@ -1240,6 +1259,7 @@ export class RoomPlane implements IRoomPlane let boundsMinX = Number.POSITIVE_INFINITY; let boundsMaxX = Number.NEGATIVE_INFINITY; + let boundsMaxY = Number.NEGATIVE_INFINITY; for(const layer of layers) { @@ -1247,11 +1267,13 @@ export class RoomPlane implements IRoomPlane if(layer.offsetX < boundsMinX) boundsMinX = layer.offsetX; if((layer.offsetX + layer.texture.width) > boundsMaxX) boundsMaxX = (layer.offsetX + layer.texture.width); + if((layer.offsetY + layer.texture.height) > boundsMaxY) boundsMaxY = (layer.offsetY + layer.texture.height); } if(boundsMinX > boundsMaxX) return false; const centerShift = ((boundsMinX + boundsMaxX) / 2); + const bottomShift = boundsMaxY; const screenSpot = projection.apply(new Point(x, y)); @@ -1264,9 +1286,10 @@ export class RoomPlane implements IRoomPlane const depthY = (-(this._normal.y / normal2DLength) * planeDistance); screenSpot.x += ((depthX - depthY) * pxPerTile); - screenSpot.y += ((depthX + depthY) * (pxPerTile / 2)); } + screenSpot.y += (pxPerTile * 0.35); + const maskX = (canvasWidth - ((canvasWidth * closestMask.mask.leftSideLoc) / this._leftSide.length)); const clipHalfWidth = (pxPerTile * 1.25); const clipLeft = Math.max(0, (maskX - clipHalfWidth)); @@ -1293,7 +1316,7 @@ export class RoomPlane implements IRoomPlane const width = layer.texture.width; const screenX = (layer.flipH ? (screenSpot.x + relLeft + width) : (screenSpot.x + relLeft)); - const screenY = (screenSpot.y + layer.offsetY); + const screenY = (screenSpot.y + (layer.offsetY - bottomShift)); const screenMatrix = new Matrix((layer.flipH ? -1 : 1), 0, 0, 1, Math.trunc(screenX), Math.trunc(screenY)); diff --git a/packages/room/src/object/visualization/room/RoomVisualization.ts b/packages/room/src/object/visualization/room/RoomVisualization.ts index 6d26a8f5..5a764884 100644 --- a/packages/room/src/object/visualization/room/RoomVisualization.ts +++ b/packages/room/src/object/visualization/room/RoomVisualization.ts @@ -186,9 +186,6 @@ export class RoomVisualization extends RoomObjectSpriteVisualization implements } } - // RoomSpriteCanvas uses this counter as its actual visual-dirty - // signal. Do not advance it for a no-op animation tick: static room - // planes otherwise force preview texture readbacks every frame. if(needsUpdate) this.updateSpriteCounter++; this.updateModelCounter = objectModel.updateCounter; @@ -494,6 +491,13 @@ export class RoomVisualization extends RoomObjectSpriteVisualization implements index = (index + 1); }; + for(let planeIndex = (this._planes.length - removedCount); planeIndex < this._planes.length; planeIndex++) + { + const plane = this._planes[planeIndex]; + + if(plane) plane.dispose(); + } + this._planes = this._planes.slice(0, (this._planes.length - removedCount)); this.createSprites(this._planes.length); diff --git a/packages/room/src/renderer/RoomSpriteCanvas.ts b/packages/room/src/renderer/RoomSpriteCanvas.ts index 0ebf33ad..404bcdbd 100644 --- a/packages/room/src/renderer/RoomSpriteCanvas.ts +++ b/packages/room/src/renderer/RoomSpriteCanvas.ts @@ -473,14 +473,6 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this.updateBoundaryMask(); - - // `updateVisuals` only means that visualizations were given an - // animation tick. It does not mean that any visualization actually - // changed a sprite. `renderObject` marks the canvas dirty when its - // visualization counter, location, or forced update changes; treating - // every animation tick as dirty makes DOM preview consumers perform a - // full GPU readback and repaint at the configured animation FPS even - // for a completely static room. if(update) this._canvasUpdated = true; this._renderTimestamp = this._totalTimeRunning; @@ -703,10 +695,6 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas if(extendedSprite.texture !== objectTexture) extendedSprite.setTexture(objectTexture); - - // Per-sprite zoom (objectSprite.scale, default 1) combined with flip. - // Setting the magnitude directly (instead of reading the previous - // scale) avoids compounding across frames. const magnitude = (objectSprite.scale && (objectSprite.scale > 0)) ? objectSprite.scale : 1; extendedSprite.scale.x = objectSprite.flipH ? -magnitude : magnitude; @@ -787,9 +775,6 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas if(spriteCount < 0) spriteCount = 0; - // Removing the last (or any trailing) sprite is a real visual change, - // even though there may be no remaining object whose renderObject call - // can set the dirty flag. if(spriteCount !== this._activeSpriteCount) this._canvasUpdated = true; if((spriteCount < this._activeSpriteCount) || !this._activeSpriteCount) @@ -900,6 +885,8 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas return this._mouseSpriteWasHit; } + private static SCRATCH_MOUSE_POINT: Point = new Point(); + private checkMouseHits(x: number, y: number, type: string, altKey: boolean = false, ctrlKey: boolean = false, shiftKey: boolean = false, buttonDown: boolean = false): boolean { const checkedSprites: string[] = []; @@ -908,17 +895,18 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas let mouseEvent: IRoomSpriteMouseEvent = null; let spriteId = (this._activeSpriteCount - 1); + const hitPoint = RoomSpriteCanvas.SCRATCH_MOUSE_POINT; + while(spriteId >= 0) { const extendedSprite = this.getExtendedSprite(spriteId); - if(extendedSprite && extendedSprite.containsPoint(new Point((x - extendedSprite.x), (y - extendedSprite.y)))) + if(extendedSprite && (hitPoint.set((x - extendedSprite.x), (y - extendedSprite.y)), extendedSprite.containsPoint(hitPoint))) { if(!extendedSprite.skipMouseHandling) { if(extendedSprite.clickHandling && ((type === MouseEventType.MOUSE_CLICK) || (type === MouseEventType.DOUBLE_CLICK))) { - // } else { diff --git a/packages/room/src/utils/RoomGeometry.ts b/packages/room/src/utils/RoomGeometry.ts index 5954cdab..bffaf195 100644 --- a/packages/room/src/utils/RoomGeometry.ts +++ b/packages/room/src/utils/RoomGeometry.ts @@ -148,7 +148,8 @@ export class RoomGeometry implements IRoomGeometry { return; } - if(this._dir == null) + const isFirstAssignment = (this._dir == null); + if(isFirstAssignment) { this._dir = new Vector3d(); } @@ -157,10 +158,13 @@ export class RoomGeometry implements IRoomGeometry const previousZ: number = this._dir.z; this._dir.assign(direction); this._direction.assign(direction); - if((((!(this._dir.x == previousX)) || (!(this._dir.y == previousY))) || (!(this._dir.z == previousZ)))) + const changed = (((!(this._dir.x == previousX)) || (!(this._dir.y == previousY))) || (!(this._dir.z == previousZ))); + if(changed) { this._updateId++; } + + if(!isFirstAssignment && !changed) return; const unitY: IVector3D = new Vector3d(0, 1, 0); const unitZ: IVector3D = new Vector3d(0, 0, 1); const unitX: IVector3D = new Vector3d(1, 0, 0); @@ -262,13 +266,11 @@ export class RoomGeometry implements IRoomGeometry private getDisplacenent(location: IVector3D): IVector3D { - let key: string; - if(this._displacements != null) - { - key = Math.trunc(Math.round(location.x)) + '_' + Math.trunc(Math.round(location.y)) + '_' + Math.trunc(Math.round(location.z)); - return this._displacements.get(key); - } - return null; + if((this._displacements == null) || !this._displacements.size) return null; + + const key = Math.trunc(Math.round(location.x)) + '_' + Math.trunc(Math.round(location.y)) + '_' + Math.trunc(Math.round(location.z)); + + return this._displacements.get(key); } public setDepthVector(direction: IVector3D): void diff --git a/packages/session/src/badge/BadgeImageManager.ts b/packages/session/src/badge/BadgeImageManager.ts index 446015f7..7e70d896 100644 --- a/packages/session/src/badge/BadgeImageManager.ts +++ b/packages/session/src/badge/BadgeImageManager.ts @@ -16,8 +16,10 @@ export class BadgeImageManager private _groupBases: Map = new Map(); private _groupSymbols: Map = new Map(); private _groupPartColors: Map = new Map(); + private static MAX_GROUP_BADGE_ATTEMPTS: number = 20; + private _requestedBadges: Map = new Map(); - private _groupBadgesQueue: Map = new Map(); + private _groupBadgesQueue: Map = new Map(); private _readyToGenerateGroupBadges: boolean = false; private _groupBadgeAssetsLoaded: boolean = false; private _groupBadgeAssetsLoading: Promise | null = null; @@ -83,7 +85,7 @@ export class BadgeImageManager else if(type === BadgeImageManager.GROUP_BADGE) { - this._groupBadgesQueue.set(badgeName, true); + if(!this._groupBadgesQueue.has(badgeName)) this._groupBadgesQueue.set(badgeName, 0); void this.processGroupBadgeQueue(); } @@ -162,7 +164,23 @@ export class BadgeImageManager for(const badgeCode of Array.from(this._groupBadgesQueue.keys())) { - if(!this.loadGroupBadge(badgeCode)) hasPending = true; + if(this.loadGroupBadge(badgeCode)) continue; + + const attempts = ((this._groupBadgesQueue.get(badgeCode) ?? 0) + 1); + + if(attempts >= BadgeImageManager.MAX_GROUP_BADGE_ATTEMPTS) + { + this._groupBadgesQueue.delete(badgeCode); + this._requestedBadges.delete(badgeCode); + + OctaneLogger.warn(`Group badge could not be rendered, giving up: ${badgeCode}`); + } + else + { + this._groupBadgesQueue.set(badgeCode, attempts); + + hasPending = true; + } } if(hasPending) this.scheduleQueueRetry(); @@ -201,6 +219,13 @@ export class BadgeImageManager const tempSprite = new Sprite(Texture.EMPTY); let renderedLayers = 0; + const abort = (): boolean => + { + container.destroy({ children: true }); + + return false; + }; + tempSprite.width = GroupBadgePart.IMAGE_WIDTH; tempSprite.height = GroupBadgePart.IMAGE_HEIGHT; @@ -213,7 +238,7 @@ export class BadgeImageManager const partNames = ((part.type === 'b') ? this._groupBases.get(part.key) : this._groupSymbols.get(part.key)); - if(!partNames || !partNames.length) return false; + if(!partNames || !partNames.length) return abort(); for(const partName of partNames) { @@ -242,10 +267,10 @@ export class BadgeImageManager container.addChild(sprite); } - if(!renderedPartLayers) return false; + if(!renderedPartLayers) return abort(); } - if(!renderedLayers) return false; + if(!renderedLayers) return abort(); const texture = TextureUtils.generateTexture(container); container.destroy({ children: true }); diff --git a/packages/sound/src/music/MusicPlayer.ts b/packages/sound/src/music/MusicPlayer.ts index c4777b58..d93f6f4e 100644 --- a/packages/sound/src/music/MusicPlayer.ts +++ b/packages/sound/src/music/MusicPlayer.ts @@ -43,6 +43,8 @@ export class MusicPlayer this._currentSongId = currentSongId; await this.preload(); await this.unlockAudio(); + + if(this._tickerInterval !== undefined) window.clearInterval(this._tickerInterval); this._isPlaying = true; this.tick(); this._tickerInterval = window.setInterval(() => this.tick(), 1000); diff --git a/packages/utils/src/AdvancedMap.ts b/packages/utils/src/AdvancedMap.ts index 12fa57f9..b349214b 100644 --- a/packages/utils/src/AdvancedMap.ts +++ b/packages/utils/src/AdvancedMap.ts @@ -29,9 +29,9 @@ export class AdvancedMap implements IAdvancedMap public dispose(): void { - if(!this._dictionary) + if(this._dictionary) { - for(const key of this._dictionary.keys()) this._dictionary.delete(key); + this._dictionary.clear(); this._dictionary = null; }