diff --git a/quasar.config.js b/quasar.config.js index 7d589f1a..ca1712e6 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -180,6 +180,219 @@ module.exports = configure(function (ctx) { process.env.VRCA_WIZARD_BUTTON_TEXT ?? "Get Started", // Desktop App VRCA_DESKTOP_MODE: process.env.VRCA_DESKTOP_MODE, + // Profile + // Profile + VRCA_DEFAULT_AVATAR_DISPLAY_NAME: + process.env.VRCA_DEFAULT_AVATAR_DISPLAY_NAME ?? "anonymous", + VRCA_DEFAULT_AVATARS: + process.env.VRCA_DEFAULT_AVATARS ?? + JSON.stringify({ + HTP45FSQ: { + name: "Sara", + image: "https://staging.vircadia.com/O12OR634/UA92/sara-cropped-small.webp", + file: "https://staging.vircadia.com/O12OR634/UA92/sara.glb", + scale: 1, + starred: true, + }, + KLM23NOP: { + name: "Mark", + image: "/assets/models/avatars/Mark-small.webp", + file: "/assets/models/avatars/Mark.glb", + scale: 1, + starred: false, + }, + QRS78TUV: { + name: "Megan", + image: "/assets/models/avatars/Megan-small.webp", + file: "/assets/models/avatars/Megan.glb", + scale: 1, + starred: false, + }, + WXY12ZAB: { + name: "Jack", + image: "/assets/models/avatars/Jack-small.webp", + file: "/assets/models/avatars/Jack.glb", + scale: 1, + starred: false, + }, + CDE56FGH: { + name: "Martha", + image: "/assets/models/avatars/Martha-small.webp", + file: "/assets/models/avatars/Martha.glb", + scale: 1, + starred: false, + }, + IJK90LMN: { + name: "Miles", + image: "/assets/models/avatars/Miles-small.webp", + file: "/assets/models/avatars/Miles.glb", + scale: 1, + starred: false, + }, + OPQ34RST: { + name: "Taylor", + image: "/assets/models/avatars/Taylor-small.webp", + file: "/assets/models/avatars/Taylor.glb", + scale: 1, + starred: false, + }, + UVW78XYZ: { + name: "Tiffany", + image: "/assets/models/avatars/Tiffany-small.webp", + file: "/assets/models/avatars/Tiffany.glb", + scale: 1, + starred: false, + }, + ABC12DEF: { + name: "Victor", + image: "/assets/models/avatars/Victor-small.webp", + file: "/assets/models/avatars/Victor.glb", + scale: 1, + starred: false, + }, + GHI56JKL: { + name: "Audrey", + image: "/assets/models/avatars/Audrey-small.webp", + file: "/assets/models/avatars/Audrey.glb", + scale: 1, + starred: false, + }, + MNO90PQR: { + name: "Kristine", + image: "/assets/models/avatars/Kristine-small.webp", + file: "/assets/models/avatars/Kristine.glb", + scale: 1, + starred: false, + }, + STU34VWX: { + name: "William", + image: "/assets/models/avatars/William-small.webp", + file: "/assets/models/avatars/William.glb", + scale: 1, + starred: false, + }, + YZA78BCD: { + name: "Erica", + image: "/assets/models/avatars/Erica-small.webp", + file: "/assets/models/avatars/Erica.glb", + scale: 1, + starred: false, + }, + EFG12HIJ: { + name: "Samantha", + image: "/assets/models/avatars/Samantha-small.webp", + file: "/assets/models/avatars/Samantha.glb", + scale: 1, + starred: false, + }, + KLM56NOP: { + name: "Roman", + image: "/assets/models/avatars/Roman-small.webp", + file: "/assets/models/avatars/Roman.glb", + scale: 1, + starred: false, + }, + QRS90TUV: { + name: "Cathy", + image: "/assets/models/avatars/Cathy-small.webp", + file: "/assets/models/avatars/Cathy.glb", + scale: 1, + starred: false, + }, + WXY34ZAB: { + name: "Lucas", + image: "/assets/models/avatars/Lucas-small.webp", + file: "/assets/models/avatars/Lucas.glb", + scale: 1, + starred: false, + }, + CDE78FGH: { + name: "Michaella", + image: "/assets/models/avatars/Michaella-small.webp", + file: "/assets/models/avatars/Michaella.glb", + scale: 1, + starred: false, + }, + IJK12LMN: { + name: "David", + image: "/assets/models/avatars/David-small.webp", + file: "/assets/models/avatars/David.glb", + scale: 1, + starred: false, + }, + OPQ56RST: { + name: "Rochella", + image: "/assets/models/avatars/Rochella-small.webp", + file: "/assets/models/avatars/Rochella.glb", + scale: 1, + starred: false, + }, + UVW90XYZ: { + name: "Susan", + image: "/assets/models/avatars/Susan-small.webp", + file: "/assets/models/avatars/Susan.glb", + scale: 1, + starred: false, + }, + ABC34DEF: { + name: "Diego", + image: "/assets/models/avatars/Diego-small.webp", + file: "/assets/models/avatars/Diego.glb", + scale: 1, + starred: false, + }, + GHI78JKL: { + name: "Jameson", + image: "/assets/models/avatars/Jameson-small.webp", + file: "/assets/models/avatars/Jameson.glb", + scale: 1, + starred: false, + }, + MNO12PQR: { + name: "Kevin", + image: "/assets/models/avatars/Kevin-small.webp", + file: "/assets/models/avatars/Kevin.glb", + scale: 1, + starred: false, + }, + STU56VWX: { + name: "Lila", + image: "/assets/models/avatars/Lila-small.webp", + file: "/assets/models/avatars/Lila.glb", + scale: 1, + starred: false, + }, + YZA90BCD: { + name: "Vikki", + image: "/assets/models/avatars/Vikki-small.webp", + file: "/assets/models/avatars/Vikki.glb", + scale: 1, + starred: false, + }, + EFG34HIJ: { + name: "Jonas", + image: "/assets/models/avatars/Jonas-small.webp", + file: "/assets/models/avatars/Jonas.glb", + scale: 1, + starred: false, + }, + KLM78NOP: { + name: "Kelly", + image: "/assets/models/avatars/Kelly-small.webp", + file: "/assets/models/avatars/Kelly.glb", + scale: 1, + starred: false, + }, + }), + VRCA_FALLBACK_AVATAR: + process.env.VRCA_FALLBACK_AVATAR ?? + JSON.stringify({ + name: "Maria", + image: "/assets/models/avatars/Maria-small.webp", + file: "/assets/models/avatars/Maria.glb", + scale: 1, + starred: false, + }), }, }, diff --git a/src/modules/avatar/DefaultModels.ts b/src/modules/avatar/DefaultModels.ts index 978e8f5d..232122ee 100644 --- a/src/modules/avatar/DefaultModels.ts +++ b/src/modules/avatar/DefaultModels.ts @@ -1,271 +1,70 @@ -// -// DefaultModels.ts -// -// Created by Giga on 1 Sep 2022. -// Copyright 2022 Vircadia contributors. -// Copyright 2022 DigiSomni LLC. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -// TODO: Get most of these variables from the config instead of this file (so it can be overridden with environment variables correctly). - -const modelRoot = "/assets/models/avatars/"; - -export interface AvatarModel { - name: string, - image: string, - file: string, - scale: number, - starred: boolean -} - -export interface AvatarModelMap { - [key: string]: AvatarModel -} - -/** - * @returns The URL of the default avatar model. - */ -export function defaultActiveAvatarUrl(): string { - return `${modelRoot}sara.glb`; -} - -/** - * @returns The ID of the default avatar model. - */ -export function defaultActiveAvatarId(): string { - return "HTP45FSQ"; -} - -/** - * @returns The fallback avatar model. - */ -export function fallbackAvatar(): AvatarModel { - return { - name: "Maria", - image: `${modelRoot}Maria-small.webp`, - file: `${modelRoot}default_avatar.glb`, - scale: 1, - starred: false - }; -} - -/** - * @returns The URL of the fallback avatar model. - */ -export function fallbackAvatarUrl(): string { - return fallbackAvatar().file; -} - -/** - * @returns The ID of the fallback avatar model. - */ -export function fallbackAvatarId(): string { - return "FALLBACK"; -} - -/** - * @returns The default collection of avatar models. - */ -export function defaultAvatars(): AvatarModelMap { - return { - HTP45FSQ: { - name: "Sara", - image: "https://staging.vircadia.com/O12OR634/UA92/sara-cropped-small.webp", - file: "https://staging.vircadia.com/O12OR634/UA92/sara.glb", - scale: 1, - starred: true - } as AvatarModel, - ZPNSHHIJ: { - name: "Mark", - image: `${modelRoot}Mark-small.webp`, - file: `${modelRoot}Mark.glb`, - scale: 1, - starred: false - } as AvatarModel, - C5E0NT3P: { - name: "Megan", - image: `${modelRoot}Megan-small.webp`, - file: `${modelRoot}Megan.glb`, - scale: 1, - starred: false - } as AvatarModel, - HYGME2O8: { - name: "Jack", - image: `${modelRoot}Jack-small.webp`, - file: `${modelRoot}Jack.glb`, - scale: 1, - starred: false - } as AvatarModel, - AIOUPXVY: { - name: "Martha", - image: `${modelRoot}Martha-small.webp`, - file: `${modelRoot}Martha.glb`, - scale: 1, - starred: false - } as AvatarModel, - LRX76LNL: { - name: "Miles", - image: `${modelRoot}Miles-small.webp`, - file: `${modelRoot}Miles.glb`, - scale: 1, - starred: false - } as AvatarModel, - HTLZ3SVU: { - name: "Taylor", - image: `${modelRoot}Taylor-small.webp`, - file: `${modelRoot}Taylor.glb`, - scale: 1, - starred: false - } as AvatarModel, - EPS62RC9: { - name: "Tiffany", - image: `${modelRoot}Tiffany-small.webp`, - file: `${modelRoot}Tiffany.glb`, - scale: 1, - starred: false - } as AvatarModel, - QIA9XG4G: { - name: "Victor", - image: `${modelRoot}Victor-small.webp`, - file: `${modelRoot}Victor.glb`, - scale: 1, - starred: false - } as AvatarModel, - N5PBHE7C: { - name: "Audrey", - image: `${modelRoot}Audrey-small.webp`, - file: `${modelRoot}Audrey.glb`, - scale: 1, - starred: false - } as AvatarModel, - E7RCM559: { - name: "Kristine", - image: `${modelRoot}Kristine-small.webp`, - file: `${modelRoot}Kristine.glb`, - scale: 1, - starred: false - } as AvatarModel, - SG35OH2Y: { - name: "William", - image: `${modelRoot}William-small.webp`, - file: `${modelRoot}William.glb`, - scale: 1, - starred: false - } as AvatarModel, - JKV34GST: { - name: "Erica", - image: `${modelRoot}Erica-small.webp`, - file: `${modelRoot}Erica.glb`, - scale: 1, - starred: false - } as AvatarModel, - X5AII7GT: { - name: "Samantha", - image: `${modelRoot}Samantha-small.webp`, - file: `${modelRoot}Samantha.glb`, - scale: 1, - starred: false - } as AvatarModel, - ZGK9IGRB: { - name: "Roman", - image: `${modelRoot}Roman-small.webp`, - file: `${modelRoot}Roman.glb`, - scale: 1, - starred: false - } as AvatarModel, - DBYRNKR8: { - name: "Cathy", - image: `${modelRoot}Cathy-small.webp`, - file: `${modelRoot}Cathy.glb`, - scale: 1, - starred: false - } as AvatarModel, - EG1XOUR4: { - name: "Lucas", - image: `${modelRoot}Lucas-small.webp`, - file: `${modelRoot}Lucas.glb`, - scale: 1, - starred: false - } as AvatarModel, - OPX471R4: { - name: "Michaella", - image: `${modelRoot}Michaella-small.webp`, - file: `${modelRoot}Michaella.glb`, - scale: 1, - starred: false - } as AvatarModel, - V5DYP68J: { - name: "David", - image: `${modelRoot}David-small.webp`, - file: `${modelRoot}David.glb`, - scale: 1, - starred: false - } as AvatarModel, - M9G7AFFC: { - name: "Rochella", - image: `${modelRoot}Rochella-small.webp`, - file: `${modelRoot}Rochella.glb`, - scale: 1, - starred: false - } as AvatarModel, - LHUVJ7RA: { - name: "Susan", - image: `${modelRoot}Susan-small.webp`, - file: `${modelRoot}Susan.glb`, - scale: 1, - starred: false - } as AvatarModel, - EQQC5125: { - name: "Diego", - image: `${modelRoot}Diego-small.webp`, - file: `${modelRoot}Diego.glb`, - scale: 1, - starred: false - } as AvatarModel, - GYC8OLSF: { - name: "Jameson", - image: `${modelRoot}Jameson-small.webp`, - file: `${modelRoot}Jameson.glb`, - scale: 1, - starred: false - } as AvatarModel, - OFTR0UR0: { - name: "Kevin", - image: `${modelRoot}Kevin-small.webp`, - file: `${modelRoot}Kevin.glb`, - scale: 1, - starred: false - } as AvatarModel, - VO3YR5QC: { - name: "Lila", - image: `${modelRoot}Lila-small.webp`, - file: `${modelRoot}Lila.glb`, - scale: 1, - starred: false - } as AvatarModel, - S4Q8O9CE: { - name: "Vikki", - image: `${modelRoot}Vikki-small.webp`, - file: `${modelRoot}Vikki.glb`, - scale: 1, - starred: false - } as AvatarModel, - ETQZ8G3W: { - name: "Jonas", - image: `${modelRoot}Jonas-small.webp`, - file: `${modelRoot}Jonas.glb`, - scale: 1, - starred: false - } as AvatarModel, - D8WRU1KS: { - name: "Kelly", - image: `${modelRoot}Kelly-small.webp`, - file: `${modelRoot}Kelly.glb`, - scale: 1, - starred: false - } as AvatarModel, - FALLBACK: fallbackAvatar() - } as AvatarModelMap; -} +// +// DefaultModels.ts +// +// Created by Giga on 1 Sep 2022. +// Copyright 2022 Vircadia contributors. +// Copyright 2022 DigiSomni LLC. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +const modelRoot = "/assets/models/avatars/" + +export interface AvatarModel { + name: string, + image: string, + file: string, + scale: number, + starred: boolean +} + +export interface AvatarModelMap { + [key: string]: AvatarModel +} + +/** + * @returns The URL of the default avatar model. + */ +export function defaultActiveAvatarUrl(): string { + return `${modelRoot}sara.glb`; +} + +/** + * @returns The ID of the default avatar model. + */ +export function defaultActiveAvatarId(): string { + return "HTP45FSQ"; +} + +/** + * @returns The fallback avatar model. + */ +export function fallbackAvatar(): AvatarModel { + const configFallbackAvatar = JSON.parse(process.env.VRCA_FALLBACK_AVATAR as string); + + return configFallbackAvatar as AvatarModel; +} + +/** + * @returns The URL of the fallback avatar model. + */ +export function fallbackAvatarUrl(): string { + return fallbackAvatar().file; +} + +/** + * @returns The ID of the fallback avatar model. + */ +export function fallbackAvatarId(): string { + return "FALLBACK"; +} + +/** + * @returns The default collection of avatar models. + */ +export function defaultAvatars(): AvatarModelMap { + const configDefaultAvatars = JSON.parse(process.env.VRCA_DEFAULT_AVATARS ?? JSON.stringify([fallbackAvatar()])); + + return configDefaultAvatars as AvatarModelMap; +} diff --git a/src/modules/avatar/StoreInterface.ts b/src/modules/avatar/StoreInterface.ts index 818085bb..76b71d94 100644 --- a/src/modules/avatar/StoreInterface.ts +++ b/src/modules/avatar/StoreInterface.ts @@ -1,159 +1,158 @@ -// -// StoreInterface.ts -// -// Created by Giga on 1 Sep 2022. -// Copyright 2022 Vircadia contributors. -// Copyright 2022 DigiSomni LLC. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import { fallbackAvatar, fallbackAvatarId, type AvatarModel } from "./DefaultModels"; -import { userStore } from "@Stores/index"; -import { Renderer } from "@Modules/scene"; - -/** - * Static methods for interacting with the list of avatar models in the Store. - */ -export class AvatarStoreInterface { - /** - * Generate a random, alpha-numeric, 8 character long ID string that is unique within the avatar Store. - * @returns An ID string. - */ - private static _generateID(): string { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - const idLength = 8; - function generate(): string { - let id = ""; - for (let i = 0; i < idLength; i += 1) { - id += chars[Math.floor(Math.random() * chars.length)]; - } - return id; - } - let uniqueId = generate(); - // Ensure that the ID doesn't already exist in the Store. - while (uniqueId in userStore.avatar.models) { - uniqueId = generate(); - } - return uniqueId; - } - - /** - * Retrieve the data (name, thumbnail, scale, etc) for a given avatar model. - * @param modelId The ID of the model to retrieve. - * @param key `(Optional)` A specific property of the model to retrieve. - * @returns The data for the requested avatar model, or the fallback model if the requested one doesn't exist. - * If a key was specified, only the value of that property is returned. - */ - public static getModelData(modelId: string | number): AvatarModel; - public static getModelData(modelId: string | number, key: T): AvatarModel[T]; - public static getModelData(modelId: string | number, key?: T): AvatarModel | AvatarModel[T] { - const models = userStore.avatar.models; - if (key && key in (models[modelId] || fallbackAvatar())) { - return models[modelId][key]; - } - return models[modelId] || fallbackAvatar(); - } - - /** - * Retrieve the data (name, thumbnail, scale, etc) for the avatar model currently equipped by the user. - * @param key `(Optional)` A specific property of the model to retrieve. - * @returns The data for the currently equipped avatar model, or the fallback model if the currently equipped one doesn't exist. - * If a key was specified, only the value of that property is returned. - */ - public static getActiveModelData(): AvatarModel; - public static getActiveModelData(key: T): AvatarModel[T]; - public static getActiveModelData(key?: T): AvatarModel | AvatarModel[T] { - const activeModel = userStore.avatar.activeModel; - if (key) { - return this.getModelData(activeModel, key); - } - return this.getModelData(activeModel); - } - - /** - * @returns A stringified map of all the stored avatar models. - */ - public static getAllModelsJSON(): string { - return JSON.stringify(userStore.avatar.models); - } - - /** - * Set the value of a specific property of an avatar model. - * @param modelId The ID of the model to update. - * @param key The property to update. - * @param value The new value for that property. - */ - public static setModelData(modelId: string | number, key: T, value: AvatarModel[T]): void { - if (modelId in userStore.avatar.models) { - userStore.avatar.models[modelId][key] = value; - // If the model's file was updated, make it the active model so there is immediate feedback on that change. - if (key === "file") { - this.setActiveModel(modelId); - } - } - } - - /** - * Set the value of a specific property of the avatar model currently equipped by the user. - * @param key The property to update. - * @param value The new value for that property. - */ - public static setActiveModelData(key: keyof AvatarModel, value: string | number | boolean): void { - const activeModel = userStore.avatar.activeModel; - this.setModelData(activeModel, key, value); - } - - /** - * Create an entry for a new avatar model in the Store. - * @param modelData The data (name, thumbnail, scale, etc) for the new avatar model. - * @param setToActive `(Optional)` Set equip this model. - * @returns The ID of the new model. - */ - public static createNewModel(modelData: AvatarModel, setToActive = true): string { - const ID = this._generateID(); - userStore.avatar.models[ID] = modelData; - if (setToActive) { - this.setActiveModel(ID); - } - return ID; - } - - /** - * Remove a model from the Store. - * @param modelId The ID of the model to remove. - */ - public static removeModel(modelId: string | number): void { - // Prevent the fallback model from being deleted. - if (modelId === fallbackAvatarId()) { - return; - } - - // Switch to the fallback model if the removed model is currently equipped. - if (modelId === userStore.avatar.activeModel) { - this.setActiveModel(fallbackAvatarId()); - } - - // Remove the requested model from the Store. - if (modelId in userStore.avatar.models) { - delete userStore.avatar.models[modelId]; - } - } - - /** - * Equip a particular model. - * @param modelId The ID of the model to equip. - */ - public static setActiveModel(modelId: string | number): void { - if (modelId in userStore.avatar.models) { - userStore.avatar.activeModel = typeof modelId === "number" ? modelId.toString() : modelId; - } - try { - const scene = Renderer.getScene(); - scene.loadMyAvatar(AvatarStoreInterface.getModelData(modelId, "file")).catch((err) => console.warn("Failed to load avatar:", err)); - } catch (error) { - console.warn("Cannot render active avatar model before the scene has been loaded.", error); - } - } -} +// +// StoreInterface.ts +// +// Created by Giga on 1 Sep 2022. +// Copyright 2022 Vircadia contributors. +// Copyright 2022 DigiSomni LLC. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import { fallbackAvatar, fallbackAvatarId, type AvatarModel } from "./DefaultModels"; +import { userStore } from "@Stores/index"; +import { Renderer } from "@Modules/scene"; + +/** + * Static methods for interacting with the list of avatar models in the Store. + */ +export class AvatarStoreInterface { + /** + * Generate a random, alpha-numeric, 8 character long ID string that is unique within the avatar Store. + * @returns An ID string. + */ + private static _generateID(): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const idLength = 8; + function generate(): string { + let id = ""; + for (let i = 0; i < idLength; i += 1) { + id += chars[Math.floor(Math.random() * chars.length)]; + } + return id; + } + let uniqueId = generate(); + // Ensure that the ID doesn't already exist in the Store. + while (uniqueId in userStore.avatar.models) { + uniqueId = generate(); + } + return uniqueId; + } + + /** + * Retrieve the data (name, thumbnail, scale, etc) for a given avatar model. + * @param modelId The ID of the model to retrieve. + * @param key `(Optional)` A specific property of the model to retrieve. + * @returns The data for the requested avatar model, or the fallback model if the requested one doesn't exist. + * If a key was specified, only the value of that property is returned. + */ + public static getModelData(modelId: string | number): AvatarModel; + public static getModelData(modelId: string | number, key: T): AvatarModel[T]; + public static getModelData(modelId: string | number, key?: T): AvatarModel | AvatarModel[T] { + const models = userStore.avatar.models; + const model = models[modelId] || fallbackAvatar(); + + if (key && key in model) { + return model[key]; + } + return model; + } + + /** + * Retrieve the data (name, thumbnail, scale, etc) for the avatar model currently equipped by the user. + * @param key `(Optional)` A specific property of the model to retrieve. + * @returns The data for the currently equipped avatar model, or the fallback model if the currently equipped one doesn't exist. + * If a key was specified, only the value of that property is returned. + */ + public static getActiveModelData(): AvatarModel; + public static getActiveModelData(key: T): AvatarModel[T]; + public static getActiveModelData(key?: T): AvatarModel | AvatarModel[T] { + const activeModel = userStore.avatar.activeModel; + return this.getModelData(activeModel, key as T); + } + + /** + * @returns A stringified map of all the stored avatar models. + */ + public static getAllModelsJSON(): string { + return JSON.stringify(userStore.avatar.models); + } + + /** + * Set the value of a specific property of an avatar model. + * @param modelId The ID of the model to update. + * @param key The property to update. + * @param value The new value for that property. + */ + public static setModelData(modelId: string | number, key: T, value: AvatarModel[T]): void { + if (modelId in userStore.avatar.models) { + userStore.avatar.models[modelId][key] = value; + // If the model's file was updated, make it the active model so there is immediate feedback on that change. + if (key === "file") { + this.setActiveModel(modelId); + } + } + } + + /** + * Set the value of a specific property of the avatar model currently equipped by the user. + * @param key The property to update. + * @param value The new value for that property. + */ + public static setActiveModelData(key: keyof AvatarModel, value: string | number | boolean): void { + const activeModel = userStore.avatar.activeModel; + this.setModelData(activeModel, key, value); + } + + /** + * Create an entry for a new avatar model in the Store. + * @param modelData The data (name, thumbnail, scale, etc) for the new avatar model. + * @param setToActive `(Optional)` Set equip this model. + * @returns The ID of the new model. + */ + public static createNewModel(modelData: AvatarModel, setToActive = true): string { + const ID = this._generateID(); + userStore.avatar.models[ID] = modelData; + if (setToActive) { + this.setActiveModel(ID); + } + return ID; + } + + /** + * Remove a model from the Store. + * @param modelId The ID of the model to remove. + */ + public static removeModel(modelId: string | number): void { + // Prevent the fallback model from being deleted. + if (modelId === fallbackAvatarId()) { + return; + } + + // Switch to the fallback model if the removed model is currently equipped. + if (modelId === userStore.avatar.activeModel) { + this.setActiveModel(fallbackAvatarId()); + } + + // Remove the requested model from the Store. + if (modelId in userStore.avatar.models) { + delete userStore.avatar.models[modelId]; + } + } + + /** + * Equip a particular model. + * @param modelId The ID of the model to equip. + */ + public static setActiveModel(modelId: string | number): void { + if (modelId in userStore.avatar.models) { + userStore.avatar.activeModel = typeof modelId === "number" ? modelId.toString() : modelId; + } + try { + const scene = Renderer.getScene(); + scene.loadMyAvatar(AvatarStoreInterface.getModelData(modelId, "file")).catch((err) => console.warn("Failed to load avatar:", err)); + } catch (error) { + console.warn("Cannot render active avatar model before the scene has been loaded.", error); + } + } +} diff --git a/src/stores/user-store.ts b/src/stores/user-store.ts index dadf1f4a..3fe2aba7 100644 --- a/src/stores/user-store.ts +++ b/src/stores/user-store.ts @@ -1,190 +1,225 @@ -// -// user-store.ts -// -// Created by Giga on 30 May 2023. -// Copyright 2023 Vircadia contributors. -// Copyright 2023 DigiSomni LLC. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import { defineStore } from "pinia"; -import { useStorage } from "@vueuse/core"; -import { Vec3 } from "@vircadia/web-sdk"; -import { onAttributeChangePayload } from "@Modules/account"; -import { defaultActiveAvatarId, defaultAvatars } from "@Modules/avatar/DefaultModels"; -import type { Domain } from "@Base/modules/domain/domain"; -import type { DomainAvatarClient } from "@Base/modules/domain/avatar"; -import { DataMapper } from "@Modules/domain/dataMapper"; -import type { vec3 } from "@vircadia/web-sdk"; - -const persistentStorageMedium = localStorage; - -const defaultControls = { - keyboard: { - movement: { - walkForwards: { name: "Walk Forwards", keycode: "KeyW" } as Keybind, - walkBackwards: { name: "Walk Backwards", keycode: "KeyS" } as Keybind, - walkLeft: { name: "Walk Left", keycode: "KeyA" } as Keybind, - walkRight: { name: "Walk Right", keycode: "KeyD" } as Keybind, - run: { name: "Run", keycode: "ShiftLeft" } as Keybind, - jump: { name: "Jump", keycode: "Space" } as Keybind, - crouch: { name: "Crouch", keycode: "KeyC" } as Keybind, - fly: { name: "Fly", keycode: "KeyF" } as Keybind, - sit: { name: "Sit", keycode: "KeyG" } as Keybind, - clap: { name: "Clap", keycode: "KeyH" } as Keybind, - salute: { name: "Salute", keycode: "KeyJ" } as Keybind - }, - camera: { - pitchUp: { name: "Pitch Up", keycode: "ArrowUp" } as Keybind, - pitchDown: { name: "Pitch Down", keycode: "ArrowDown" } as Keybind, - yawLeft: { name: "Yaw Left", keycode: "ArrowLeft" } as Keybind, - yawRight: { name: "Yaw Right", keycode: "ArrowRight" } as Keybind, - firstPerson: { name: "First-Person", keycode: "Digit1" } as Keybind, - thirdPerson: { name: "Third-Person", keycode: "Digit3" } as Keybind, - collisions: { name: "Toggle Collisions", keycode: "Digit4" } as Keybind - }, - audio: { - mute: { name: "Toggle Mic Mute", keycode: "KeyV" } as Keybind, - pushToTalk: { name: "Push-To-Talk", keycode: "KeyB" } as Keybind - }, - other: { - resetPosition: { name: "Reset Position", keycode: "KeyK" } as Keybind, - toggleMenu: { name: "Toggle Menu", keycode: "KeyM" } as Keybind, - openChat: { name: "Open Chat", keycode: "KeyT" } as Keybind - } - }, - mouse: { - acceleration: true, - invert: false, - sensitivity: 50 - } -}; - -export type KeyboardControlCategory = keyof typeof defaultControls.keyboard; -export type KeyboardControl = keyof typeof defaultControls.keyboard[T]; -export interface Keybind { - name: string, - keycode: string -} - -export interface LocationBookmark { - name: string, - color: string, - url: string -} - -export const useUserStore = defineStore("user", { - state: () => ({ - avatar: useStorage( - "userAvatarSettings", - { - displayName: "anonymous", - showLabels: true, - position: Vec3.ZERO, - location: "0,0,0", - models: defaultAvatars(), - activeModel: defaultActiveAvatarId() - }, - persistentStorageMedium, - { mergeDefaults: true, listenToStorageChanges: false } - ), - // Graphics configuration. - graphics: useStorage( - "userGraphicsSettings", - { - fieldOfView: 85, - bloom: true, - fxaaEnabled: true, - msaa: 2, - sharpen: false, - fpsCounter: false, - cameraBobbing: true - }, - persistentStorageMedium, - { mergeDefaults: true, listenToStorageChanges: false } - ), - // Information about the logged in account. Refer to Account module. - account: useStorage( - "userAccountSettings", - { - id: "UNKNOWN", - username: "Guest", - isLoggedIn: false, - accessToken: "UNKNOWN", - tokenType: "Bearer", - scope: "UNKNOWN", - isAdmin: false, - useAsAdmin: false, - images: { - hero: undefined as string | undefined, - tiny: undefined as string | undefined, - thumbnail: undefined as string | undefined - } - }, - persistentStorageMedium, - { mergeDefaults: true, listenToStorageChanges: false } - ), - // Saved bookmarks. - bookmarks: useStorage( - "userBookmarks", - { - locations: [] as Array - }, - persistentStorageMedium, - { mergeDefaults: true, listenToStorageChanges: true } - ), - // Controls. - controls: useStorage("userControlSettings", - defaultControls, - persistentStorageMedium, - { mergeDefaults: true, listenToStorageChanges: true } - ) - }), - - actions: { - /** - * Reset the state of the store to default. - */ - reset(): void { - this.$reset(); - }, - /** - * Update the stored account information for the current user. - * @param data - */ - updateAccountInfo(data: onAttributeChangePayload): void { - this.account.accessToken = data.accessToken; - this.account.isAdmin = data.isAdmin; - this.account.isLoggedIn = data.isLoggedIn; - this.account.scope = data.scope; - this.account.tokenType = data.accessTokenType; - this.account.username = data.accountInfo.username; - Object.assign(this.account.images, data.accountInfo.images ?? {}); - }, - /** - * Update the stored information for the local avatar. - * @param domain A reference to the domain server connection instance. - * @param domainAvatar `(Optional)` A reference to the local avatar instance. - * @param position `(Optional)` The new position of the local avatar in the world. - */ - updateLocalAvatarInfo(domain: Domain, domainAvatar?: DomainAvatarClient, position?: vec3): void { - const domainLocation = domain.DomainClient - ? domain.Location.protocol + "//" + domain.Location.host - : "Disconnected"; - if (domainAvatar) { - const myAvaInfo = domainAvatar.MyAvatar; - this.avatar.displayName = myAvaInfo?.displayName ?? myAvaInfo?.sessionDisplayName ?? "anonymous"; - this.avatar.location - = `${domainLocation}/${DataMapper.vec3ToString(myAvaInfo?.position)}/${DataMapper.quaternionToString(myAvaInfo?.orientation)}`; - this.avatar.position = myAvaInfo?.position ?? Vec3.ZERO; - } - // An optional update to just the avatar's position. - if (position) { - this.avatar.position = position; - this.avatar.location = `${domainLocation}/${DataMapper.vec3ToString(position)}/${DataMapper.quaternionToString(null)}`; - } - } - } -}); +// +// user-store.ts +// +// Created by Giga on 30 May 2023. +// Copyright 2023 Vircadia contributors. +// Copyright 2023 DigiSomni LLC. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import { defineStore } from "pinia"; +import { useStorage } from "@vueuse/core"; +import { Vec3 } from "@vircadia/web-sdk"; +import { onAttributeChangePayload } from "@Modules/account"; +import { defaultActiveAvatarId, defaultAvatars, type AvatarModelMap } from "@Modules/avatar/DefaultModels"; +import type { Domain } from "@Base/modules/domain/domain"; +import type { DomainAvatarClient } from "@Base/modules/domain/avatar"; +import { DataMapper } from "@Modules/domain/dataMapper"; +import type { vec3 } from "@vircadia/web-sdk"; + +const persistentStorageMedium = localStorage; + +// Function to generate a hash of an object +function generateHash(obj: object): string { + const str = JSON.stringify(obj); + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(16); +} + +// Function to get the current avatar settings with version hash +function getCurrentAvatarSettings() { + const defaultModels = defaultAvatars(); + const modelVersion = generateHash(defaultModels); + + return { + displayName: process.env.VRCA_DEFAULT_AVATAR_DISPLAY_NAME ?? "anonymous", + showLabels: true, + position: Vec3.ZERO, + location: "0,0,0", + models: defaultModels, + activeModel: defaultActiveAvatarId(), + modelVersion + }; +} + +const defaultControls = { + keyboard: { + movement: { + walkForwards: { name: "Walk Forwards", keycode: "KeyW" } as Keybind, + walkBackwards: { name: "Walk Backwards", keycode: "KeyS" } as Keybind, + walkLeft: { name: "Walk Left", keycode: "KeyA" } as Keybind, + walkRight: { name: "Walk Right", keycode: "KeyD" } as Keybind, + run: { name: "Run", keycode: "ShiftLeft" } as Keybind, + jump: { name: "Jump", keycode: "Space" } as Keybind, + crouch: { name: "Crouch", keycode: "KeyC" } as Keybind, + fly: { name: "Fly", keycode: "KeyF" } as Keybind, + sit: { name: "Sit", keycode: "KeyG" } as Keybind, + clap: { name: "Clap", keycode: "KeyH" } as Keybind, + salute: { name: "Salute", keycode: "KeyJ" } as Keybind + }, + camera: { + pitchUp: { name: "Pitch Up", keycode: "ArrowUp" } as Keybind, + pitchDown: { name: "Pitch Down", keycode: "ArrowDown" } as Keybind, + yawLeft: { name: "Yaw Left", keycode: "ArrowLeft" } as Keybind, + yawRight: { name: "Yaw Right", keycode: "ArrowRight" } as Keybind, + firstPerson: { name: "First-Person", keycode: "Digit1" } as Keybind, + thirdPerson: { name: "Third-Person", keycode: "Digit3" } as Keybind, + collisions: { name: "Toggle Collisions", keycode: "Digit4" } as Keybind + }, + audio: { + mute: { name: "Toggle Mic Mute", keycode: "KeyV" } as Keybind, + pushToTalk: { name: "Push-To-Talk", keycode: "KeyB" } as Keybind + }, + other: { + resetPosition: { name: "Reset Position", keycode: "KeyK" } as Keybind, + toggleMenu: { name: "Toggle Menu", keycode: "KeyM" } as Keybind, + openChat: { name: "Open Chat", keycode: "KeyT" } as Keybind + } + }, + mouse: { + acceleration: true, + invert: false, + sensitivity: 50 + } +}; + +export type KeyboardControlCategory = keyof typeof defaultControls.keyboard; +export type KeyboardControl = keyof typeof defaultControls.keyboard[T]; +export interface Keybind { + name: string, + keycode: string +} + +export interface LocationBookmark { + name: string, + color: string, + url: string +} + +export const useUserStore = defineStore("user", { + state: () => ({ + avatar: useStorage( + "userAvatarSettings", + getCurrentAvatarSettings(), + persistentStorageMedium, + { + mergeDefaults: (storageValue, defaults) => { + if (!storageValue || storageValue.modelVersion !== defaults.modelVersion) { + // If the model version has changed, update models and reset active model + return { + ...storageValue, + models: defaults.models, + activeModel: defaults.activeModel, + modelVersion: defaults.modelVersion + }; + } + return { ...defaults, ...storageValue }; + }, + listenToStorageChanges: false + } + ), + // Graphics configuration. + graphics: useStorage( + "userGraphicsSettings", + { + fieldOfView: 85, + bloom: true, + fxaaEnabled: true, + msaa: 2, + sharpen: false, + fpsCounter: false, + cameraBobbing: true + }, + persistentStorageMedium, + { mergeDefaults: true, listenToStorageChanges: false } + ), + // Information about the logged in account. Refer to Account module. + account: useStorage( + "userAccountSettings", + { + id: "UNKNOWN", + username: "Guest", + isLoggedIn: false, + accessToken: "UNKNOWN", + tokenType: "Bearer", + scope: "UNKNOWN", + isAdmin: false, + useAsAdmin: false, + images: { + hero: undefined as string | undefined, + tiny: undefined as string | undefined, + thumbnail: undefined as string | undefined + } + }, + persistentStorageMedium, + { mergeDefaults: true, listenToStorageChanges: false } + ), + // Saved bookmarks. + bookmarks: useStorage( + "userBookmarks", + { + locations: [] as Array + }, + persistentStorageMedium, + { mergeDefaults: true, listenToStorageChanges: true } + ), + // Controls. + controls: useStorage("userControlSettings", + defaultControls, + persistentStorageMedium, + { mergeDefaults: true, listenToStorageChanges: true } + ) + }), + + actions: { + /** + * Reset the state of the store to default. + */ + reset(): void { + this.$reset(); + }, + /** + * Update the stored account information for the current user. + * @param data + */ + updateAccountInfo(data: onAttributeChangePayload): void { + this.account.accessToken = data.accessToken; + this.account.isAdmin = data.isAdmin; + this.account.isLoggedIn = data.isLoggedIn; + this.account.scope = data.scope; + this.account.tokenType = data.accessTokenType; + this.account.username = data.accountInfo.username; + Object.assign(this.account.images, data.accountInfo.images ?? {}); + }, + /** + * Update the stored information for the local avatar. + * @param domain A reference to the domain server connection instance. + * @param domainAvatar `(Optional)` A reference to the local avatar instance. + * @param position `(Optional)` The new position of the local avatar in the world. + */ + updateLocalAvatarInfo(domain: Domain, domainAvatar?: DomainAvatarClient, position?: vec3): void { + const domainLocation = domain.DomainClient + ? domain.Location.protocol + "//" + domain.Location.host + : "Disconnected"; + if (domainAvatar) { + const myAvaInfo = domainAvatar.MyAvatar; + this.avatar.displayName = myAvaInfo?.displayName ?? myAvaInfo?.sessionDisplayName ?? "anonymous"; + this.avatar.location + = `${domainLocation}/${DataMapper.vec3ToString(myAvaInfo?.position)}/${DataMapper.quaternionToString(myAvaInfo?.orientation)}`; + this.avatar.position = myAvaInfo?.position ?? Vec3.ZERO; + } + // An optional update to just the avatar's position. + if (position) { + this.avatar.position = position; + this.avatar.location = `${domainLocation}/${DataMapper.vec3ToString(position)}/${DataMapper.quaternionToString(null)}`; + } + } + } +}); diff --git a/src/vircadia-world b/src/vircadia-world index b434d685..d3e21d33 160000 --- a/src/vircadia-world +++ b/src/vircadia-world @@ -1 +1 @@ -Subproject commit b434d685d94b72dcb42ee3ab0a9942223b514075 +Subproject commit d3e21d334eb56c416a6a1f6e406b1b7e9b169fdd