diff --git a/src-overlay-ui/src/routes/+layout.ts b/src-overlay-ui/src/routes/+layout.ts index 92f90f2e..83bd8017 100644 --- a/src-overlay-ui/src/routes/+layout.ts +++ b/src-overlay-ui/src/routes/+layout.ts @@ -1,11 +1,11 @@ -import { browser } from "$app/environment"; -import ipcService from "$lib/services/ipc.service"; -import type { Load } from "@sveltejs/kit"; -import { loadTranslations } from "$lib/translations"; -import { get } from "svelte/store"; -import { fontLoader } from "src-shared-ts/src/font-loader"; +import { browser } from '$app/environment'; +import ipcService from '$lib/services/ipc.service'; +import type { Load } from '@sveltejs/kit'; +import { loadTranslations } from '$lib/translations'; +import { get } from 'svelte/store'; +import { fontLoader } from 'src-shared-ts/src/font-loader'; -export const trailingSlash = "always"; +export const trailingSlash = 'always'; export const prerender = true; export const ssr = false; @@ -15,14 +15,23 @@ if (browser) { export const load: Load = async ({ url }) => { // Obtain query params const urlParams = new URLSearchParams(window.location.search); - const corePort = parseInt(urlParams.get("corePort") ?? "5177", 10); + const corePort = parseInt(urlParams.get('corePort') ?? '5177', 10); // If the core port was provided, initialize the font loader - if (corePort > 0 && corePort < 65536) fontLoader.init(corePort); + // if (corePort > 0 && corePort < 65536) fontLoader.init(corePort); + // Load fonts from Google if running outside of the overlay (development mode) + if (!window.CefSharp) loadDevFonts(); // Initialize IPC await ipcService.init(); // Load translations const { pathname } = url; - await loadTranslations(get(ipcService.state).locale ?? "en", pathname); + await loadTranslations(get(ipcService.state).locale ?? 'en', pathname); return {}; }; + +function loadDevFonts() { + const link = document.createElement('link'); + link.href = 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); +} diff --git a/src-ui/app/app.module.ts b/src-ui/app/app.module.ts index 0f46b340..ca713a77 100644 --- a/src-ui/app/app.module.ts +++ b/src-ui/app/app.module.ts @@ -226,11 +226,10 @@ import { BrightnessAutomationDetailsComponent } from './views/dashboard-view/vie import { DurationInputSettingComponent } from './components/duration-input-setting/duration-input-setting.component'; import { CCTControlService } from './services/cct-control/cct-control.service'; import { CCTControlModalComponent } from './components/cct-control-modal/cct-control-modal.component'; -import { - SettingsBrightnessCctViewComponent, -} from './views/dashboard-view/views/settings-brightness-cct-view/settings-brightness-cct-view.component'; +import { SettingsBrightnessCctViewComponent } from './views/dashboard-view/views/settings-brightness-cct-view/settings-brightness-cct-view.component'; import { CCTInputSettingComponent } from './components/cct-input-setting/cct-input-setting.component'; import { BrightnessAdvancedModeToggleComponent } from './components/brightness-advanced-mode-toggle/brightness-advanced-mode-toggle.component'; +import { FBTAvatarReloadHotfixService } from './services/hotfixes/f-b-t-avatar-reload-hotfix.service'; [ localeEN, @@ -490,7 +489,9 @@ export class AppModule { private nightmareDetectionAutomationService: NightmareDetectionAutomationService, private bigscreenBeyondLedAutomationService: BigscreenBeyondLedAutomationService, private bigscreenBeyondFanAutomationService: BigscreenBeyondFanAutomationService, - private vrchatAvatarAutomationsService: VRChatAvatarAutomationsService + private vrchatAvatarAutomationsService: VRChatAvatarAutomationsService, + // Hotfixes + private fbtAvatarReloadHotfixService: FBTAvatarReloadHotfixService ) { this.init(); } @@ -779,6 +780,10 @@ export class AppModule { 'VRChatAvatarAutomationsService initialization', this.vrchatAvatarAutomationsService.init() ), + this.logInit( + 'FBTAvatarReloadHotfixService initialization', + this.fbtAvatarReloadHotfixService.init() + ), ]); await info(`[Init] Initialization complete! (took ${Date.now() - initStartTime}ms)`); })(), diff --git a/src-ui/app/models/automations.ts b/src-ui/app/models/automations.ts index d0687ded..a7c4247d 100644 --- a/src-ui/app/models/automations.ts +++ b/src-ui/app/models/automations.ts @@ -288,6 +288,7 @@ export interface SleepingAnimationsAutomationConfig extends AutomationConfig { unlockFeetOnAutomationDisable: boolean; releaseFootLockOnPoseChange: boolean; footLockReleaseWindow: number; + enableAvatarReloadOnFBTDisableHotfix: boolean; } export type VRChatVoiceMode = 'TOGGLE' | 'PUSH_TO_TALK'; @@ -683,6 +684,7 @@ export const AUTOMATION_CONFIGS_DEFAULT: AutomationConfigs = { unlockFeetOnAutomationDisable: true, releaseFootLockOnPoseChange: true, footLockReleaseWindow: 600, + enableAvatarReloadOnFBTDisableHotfix: false, oscScripts: {}, }, JOIN_NOTIFICATIONS: { diff --git a/src-ui/app/services/hotfixes/f-b-t-avatar-reload-hotfix.service.ts b/src-ui/app/services/hotfixes/f-b-t-avatar-reload-hotfix.service.ts new file mode 100644 index 00000000..086ed1a4 --- /dev/null +++ b/src-ui/app/services/hotfixes/f-b-t-avatar-reload-hotfix.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { OpenVRService } from '../openvr.service'; +import { + asyncScheduler, + distinctUntilChanged, + filter, + map, + pairwise, + switchMap, + take, + throttleTime, +} from 'rxjs'; +import { isEqual } from 'lodash'; +import { OscService } from '../osc.service'; +import { sleep } from '../../utils/promise-utils'; +import { info } from 'tauri-plugin-log-api'; +import { SleepingAnimationsAutomationService } from '../osc-automations/sleeping-animations-automation.service'; +import { VRChatService } from '../vrchat.service'; +import { AutomationConfigService } from '../automation-config.service'; + +@Injectable({ + providedIn: 'root', +}) +export class FBTAvatarReloadHotfixService { + private enabled = false; + + constructor( + private openvr: OpenVRService, + private osc: OscService, + private vrchat: VRChatService, + private sleepAnimations: SleepingAnimationsAutomationService, + private automationConfig: AutomationConfigService + ) {} + + async init() { + this.automationConfig.configs + .pipe(map((configs) => configs.SLEEPING_ANIMATIONS.enableAvatarReloadOnFBTDisableHotfix)) + .subscribe((enabled) => (this.enabled = enabled)); + this.openvr.devices + .pipe( + // Only run if this hotfix is enabled + filter(() => this.enabled), + // Detect all trackers turning off + map((devices) => + devices + .filter((d) => d.class === 'GenericTracker' && (d.canPowerOff || d.isTurningOff)) + .map((d) => d.serialNumber) + ), + pairwise(), + distinctUntilChanged((a, b) => isEqual(a, b)), + filter(([prev, curr]) => prev.length > 0 && curr.length === 0), + // Only run while VRChat is active + switchMap(() => this.vrchat.vrchatProcessActive.pipe(take(1))), + filter(Boolean), + // Only trigger once every 5s max + throttleTime(5000, asyncScheduler, { leading: true, trailing: false }) + ) + .subscribe(() => this.triggerHotfix()); + } + + private async triggerHotfix() { + info( + '[FBTAvatarReloadHotfix] All trackers have been turned off. Running hotfix to reload avatar' + ); + // Wait for VRC to process the trackers fully turning off + await sleep(3000); + + // Open the quick menu + await this.osc.send_int('/input/QuickMenuToggleLeft', 0); + await sleep(150); + await this.osc.send_int('/input/QuickMenuToggleLeft', 1); + await sleep(150); + await this.osc.send_int('/input/QuickMenuToggleLeft', 0); + + // Wait a bit + await sleep(500); + + // Close the quick menu + await this.osc.send_int('/input/QuickMenuToggleLeft', 0); + await sleep(150); + await this.osc.send_int('/input/QuickMenuToggleLeft', 1); + await sleep(150); + await this.osc.send_int('/input/QuickMenuToggleLeft', 0); + + // Inform sleeping automations to reapply + await this.sleepAnimations.retrigger(); + + info('[FBTAvatarReloadHotfix] Hotfix applied'); + } +} diff --git a/src-ui/app/services/osc-automations/sleeping-animations-automation.service.ts b/src-ui/app/services/osc-automations/sleeping-animations-automation.service.ts index 708ea964..0a711725 100644 --- a/src-ui/app/services/osc-automations/sleeping-animations-automation.service.ts +++ b/src-ui/app/services/osc-automations/sleeping-animations-automation.service.ts @@ -14,6 +14,7 @@ import { map, pairwise, startWith, + Subject, } from 'rxjs'; import { SleepService } from '../sleep.service'; import { SleepingPose } from '../../models/sleeping-pose'; @@ -28,6 +29,7 @@ export class SleepingAnimationsAutomationService { private config: SleepingAnimationsAutomationConfig = structuredClone( AUTOMATION_CONFIGS_DEFAULT.SLEEPING_ANIMATIONS ); + private retrigger$ = new Subject(); constructor( private automationConfig: AutomationConfigService, @@ -50,6 +52,8 @@ export class SleepingAnimationsAutomationService { combineLatest([ // Pose changes this.sleep.pose, + // External retriggers + this.retrigger$.pipe(startWith(void 0)), // Retrigger when automation is enabled this.automationConfig.configs.pipe( map((configs) => configs.SLEEPING_ANIMATIONS.enabled), @@ -147,4 +151,8 @@ export class SleepingAnimationsAutomationService { async forcePose(pose: SleepingPose) { this.sleep.forcePose(pose); } + + async retrigger() { + this.retrigger$.next(); + } } diff --git a/src-ui/app/utils/promise-utils.ts b/src-ui/app/utils/promise-utils.ts index 7c1379cd..e1168af0 100644 --- a/src-ui/app/utils/promise-utils.ts +++ b/src-ui/app/utils/promise-utils.ts @@ -10,3 +10,7 @@ export function pTimeout( }); return Promise.race([promise, timeout]) as Promise; } + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src-ui/app/views/dashboard-view/views/sleep-animations-view/sleep-animations-view.component.html b/src-ui/app/views/dashboard-view/views/sleep-animations-view/sleep-animations-view.component.html index 03416093..50ef7804 100644 --- a/src-ui/app/views/dashboard-view/views/sleep-animations-view/sleep-animations-view.component.html +++ b/src-ui/app/views/dashboard-view/views/sleep-animations-view/sleep-animations-view.component.html @@ -284,5 +284,38 @@

oscAutomations.sleepingAnimations.options.general.footLock.title +
+

oscAutomations.sleepingAnimations.options.general.advanced.title

+
+
+
+ oscAutomations.sleepingAnimations.options.general.advanced.avatarReloadHotfix.title + +
+
+ +
+
+
+
diff --git a/src-ui/assets/i18n/en.json b/src-ui/assets/i18n/en.json index 51f1bc22..53c8d50b 100644 --- a/src-ui/assets/i18n/en.json +++ b/src-ui/assets/i18n/en.json @@ -20,6 +20,13 @@ "SET_VOLUME": "Change Volume", "UNMUTE": "Unmute" }, + "applyOnStart": { + "description": { + "onSleepDisable": "Run this automation when OyasumiVR starts with the sleep mode disabled", + "onSleepEnable": "Run this automation when OyasumiVR starts with the sleep mode enabled" + }, + "title": "Apply on start" + }, "automations": { "label": "{count, plural, one {1 Automation} other {# Automations}}" }, @@ -58,13 +65,6 @@ "volume": { "description": "What volume level to set for the audio device when this automation runs", "title": "Volume" - }, - "applyOnStart": { - "title": "Apply on start", - "description": { - "onSleepEnable": "Run this automation when OyasumiVR starts with the sleep mode enabled", - "onSleepDisable": "Run this automation when OyasumiVR starts with the sleep mode disabled" - } } }, "auto-invite-request-accept": { @@ -1249,6 +1249,13 @@ "title": "Foot Lock" }, "general": { + "advanced": { + "avatarReloadHotfix": { + "title": "Hotfix for using avatar animations with FBT", + "description": "Enable this if you want to use avatar animations after OyasumiVR has turned off your full body trackers. What is this and why is it needed?" + }, + "title": "Advanced Options" + }, "footLock": { "description": "A foot lock commonly prevents the player from moving or rotating when it is enabled.", "title": "Foot Lock"