using UdonSharp; using UnityEngine; using VRC.SDK3.Components.Video; using VRC.SDK3.Image; using VRC.SDK3.UdonNetworkCalling; using VRC.SDK3.Video.Components.Base; using VRC.SDKBase; using VRC.Udon.Common.Interfaces; public enum ClueScreenType { Blank, Video, Map } [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class CaseVideoSyncPlayer : UdonSharpBehaviour { [SerializeField] private CaseManager _CaseManager; [UdonSynced, FieldChangeCallback(nameof(SubMapIndex))] private int _SubMapIndex = 0; [UdonSynced, FieldChangeCallback(nameof(ShowScreen))] private ClueScreenType _ShowScreen = ClueScreenType.Blank; [UdonSynced, FieldChangeCallback(nameof(FlashCorrectAnswer))] private bool _FlashCorrectAnswer = false; [UdonSynced, FieldChangeCallback(nameof(VideoIndex))] private int _VideoIndex = -1; [UdonSynced, FieldChangeCallback(nameof(TimeAndOffset))] private Vector2 _TimeAndOffset; [UdonSynced, FieldChangeCallback(nameof(PlayVideo))] private bool _VideoIsPlaying; [SerializeField] private BaseVRCVideoPlayer _VideoPlayer; [SerializeField] private float _SyncFrequency = 5.0f; [SerializeField] private MeshRenderer _BlankScreenMesh; [SerializeField] private MeshRenderer _VideoScreenMesh; [SerializeField] private MeshRenderer _MapScreenMesh; [SerializeField] private Material _MapScreenMaterial; [SerializeField] private Texture2D _PlaceholderMapTexture; private VRCImageDownloader _MapDownloader; private Texture2D[] _MapImages = new Texture2D[0]; private IUdonEventReceiver _UdonEventReceiverThis; private int[] _CachedMapIndices = new int[0]; private int _MapDownloadIndex = 0; private bool _MapDownloadsInProgress = false; private bool _UseFallback = false; private const int IMAGES_PER_MAP_ATLAS = 6; void Start() { _MapDownloader = new VRCImageDownloader(); _UdonEventReceiverThis = (IUdonEventReceiver)this; _MapScreenMesh.sharedMaterial = _MapScreenMaterial; UpdateMap(false); } void OnDestroy() { _MapDownloader.Dispose(); } [NetworkCallable] public void QueueMapDownloads(int[] MapIndices) { if (_MapDownloadsInProgress) { // Concatenate both caches into a new bigger cache and await new downloads as usual int[] NewMapIndicesCache = new int[_CachedMapIndices.Length + MapIndices.Length]; Texture2D[] NewMapImages = new Texture2D[_CachedMapIndices.Length + MapIndices.Length]; for (int i = 0; i < _CachedMapIndices.Length; i++) { NewMapIndicesCache[i] = _CachedMapIndices[i]; NewMapImages[i] = _MapImages[i]; } for (int i = 0; i < MapIndices.Length; i++) { NewMapIndicesCache[i + _CachedMapIndices.Length] = MapIndices[i]; NewMapImages[i + _CachedMapIndices.Length] = _PlaceholderMapTexture; } _CachedMapIndices = NewMapIndicesCache; _MapImages = NewMapImages; } else { _MapDownloadsInProgress = true; _CachedMapIndices = MapIndices; _MapImages = new Texture2D[_CachedMapIndices.Length]; for (int i = 0; i < _MapImages.Length; i++) { _MapImages[i] = _PlaceholderMapTexture; } _SubMapIndex = 0; _MapDownloadIndex = 0; LoadMapFromIndex(_CachedMapIndices[_MapDownloadIndex]); } } private void LoadMapFromIndex(int MapIndex) { VRCUrl MapURL = _CaseManager.GetMap(MapIndex); TextureInfo AdditionalTextureInfo = new TextureInfo(); AdditionalTextureInfo.WrapModeU = TextureWrapMode.Clamp; AdditionalTextureInfo.WrapModeV = TextureWrapMode.Clamp; AdditionalTextureInfo.GenerateMipMaps = true; _MapDownloader.DownloadImage(MapURL, null, _UdonEventReceiverThis, AdditionalTextureInfo); } public override void OnImageLoadSuccess(IVRCImageDownload Result) { _MapImages[_MapDownloadIndex] = Result.Result; int MapPage = SubMapIndex / IMAGES_PER_MAP_ATLAS; if (MapPage == _MapDownloadIndex) { _MapScreenMaterial.SetTexture("_EmissionMap", _MapImages[MapPage]); } _MapDownloadIndex++; if (_MapDownloadIndex >= _CachedMapIndices.Length) { _MapDownloadsInProgress = false; } else { LoadMapFromIndex(_CachedMapIndices[_MapDownloadIndex]); } base.OnImageLoadSuccess(Result); } public override void OnImageLoadError(IVRCImageDownload Result) { _MapDownloadsInProgress = false; base.OnImageLoadError(Result); } [NetworkCallable] public void NextCorrectAnswerFrame() { if (FlashCorrectAnswer) { SubMapIndex = (SubMapIndex == 4) ? 3 : 4; SendCustomEventDelayedSeconds(nameof(NextCorrectAnswerFrame), 0.2f); } else { _VideoPlayer.Stop(); SubMapIndex = 0; ShowScreen = ClueScreenType.Blank; RequestSerialization(); } } private void UpdateMap(bool SyncResult = true) { int MapPage = SubMapIndex / IMAGES_PER_MAP_ATLAS; int SubmapIndexWrapped = SubMapIndex % IMAGES_PER_MAP_ATLAS; if (MapPage < _MapImages.Length) { switch (SubmapIndexWrapped) { case 0: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.66666666f)); break; case 1: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.66666666f)); break; case 2: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.33333333f)); break; case 3: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.33333333f)); break; case 4: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.0f, 0.0f)); break; case 5: _MapScreenMaterial.SetVector("_MainTex_ST", new Vector4(0.5f, 0.33333333f, 0.5f, 0.0f)); break; } _MapScreenMaterial.SetTexture("_EmissionMap", _MapImages[MapPage]); ShowScreen = ClueScreenType.Map; if (SyncResult) { RequestSerialization(); } } } private void _LoadVideo_Private() { TryLoadURL(); } public void TryLoadURL() { if (VideoIndex >= 0 && VideoIndex < _CaseManager.GetVideoCount()) { _VideoPlayer.LoadURL(_CaseManager.GetVideo(VideoIndex, _UseFallback)); } else { Debug.LogWarning("[CaseVideoSyncPlayer] Index is out of range."); } } private void _PlayVideo_Private() { _VideoPlayer.Play(); } private void _StopVideo_Private() { _VideoPlayer.Stop(); _UseFallback = false; VideoIndex = -1; } public override void OnVideoReady() { Debug.Log("[CaseVideoSyncPlayer] Video is ready."); if (_VideoIsPlaying) { _PlayVideo_Private(); } base.OnVideoReady(); } public override void OnVideoError(VideoError VideoError) { switch(VideoError) { case VideoError.Unknown: Debug.LogError("[CaseVideoSyncPlayer] Unknown playback error."); break; case VideoError.InvalidURL: Debug.LogError("[CaseVideoSyncPlayer] Invalid URL."); break; case VideoError.AccessDenied: Debug.LogError("[CaseVideoSyncPlayer] Access denied."); break; case VideoError.PlayerError: Debug.LogError("[CaseVideoSyncPlayer] Error with video player."); break; case VideoError.RateLimited: Debug.LogError("[CaseVideoSyncPlayer] Rate limited."); break; } if (_UseFallback) { _StopVideo_Private(); } else { Debug.Log("[CaseVideoSyncPlayer] Attempting fallback in 5 seconds..."); SendCustomEventDelayedSeconds(nameof(TryLoadFallbackURL), 5.5f); } base.OnVideoError(VideoError); } public void TryLoadFallbackURL() { _UseFallback = true; TryLoadURL(); } public override void OnVideoStart() { if (_VideoIsPlaying) { ShowScreen = ClueScreenType.Video; //UpdateTimeAndOffset(); } else { _VideoPlayer.Stop(); } base.OnVideoStart(); } public override void OnVideoEnd() { PlayVideo = false; ShowScreen = ClueScreenType.Blank; RequestSerialization(); base.OnVideoEnd(); } private void UpdateTimeAndOffset() { if (Networking.IsOwner(gameObject)) { TimeAndOffset = new Vector2(_VideoPlayer.GetTime(), (float)Networking.GetServerTimeInSeconds()); if (_SyncFrequency > 0.0f) { SendCustomEventDelayedSeconds(nameof(UpdateTimeAndOffset), _SyncFrequency); } } else { Resync(); } RequestSerialization(); } public void Resync() { _VideoPlayer.SetTime(TimeAndOffset.x + ((float)Networking.GetServerTimeInSeconds() - TimeAndOffset.y)); } [NetworkCallable] public void ClearScreen() { FlashCorrectAnswer = false; ShowScreen = ClueScreenType.Blank; } private void SwapToScreen(ClueScreenType Screen) { if (ShowScreen == Screen) return; switch (Screen) { case ClueScreenType.Blank: { _BlankScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); _VideoScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); _MapScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); PlayVideo = false; break; } case ClueScreenType.Video: { _BlankScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); _VideoScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); _MapScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); break; } case ClueScreenType.Map: { _BlankScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); _VideoScreenMesh.transform.localPosition = new Vector3(0.0f, -1000.0f, 0.0f); _MapScreenMesh.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); PlayVideo = false; break; } } RequestSerialization(); } public int SubMapIndex { set { _SubMapIndex = value; UpdateMap(!FlashCorrectAnswer); } get => _SubMapIndex; } public ClueScreenType ShowScreen { set { SwapToScreen(value); _ShowScreen = value; } get => _ShowScreen; } public bool FlashCorrectAnswer { set { _FlashCorrectAnswer = value; SendCustomNetworkEvent(NetworkEventTarget.Self, nameof(NextCorrectAnswerFrame)); RequestSerialization(); } get => _FlashCorrectAnswer; } public int VideoIndex { set { _VideoIndex = value; if (_VideoIndex >= 0) { _UseFallback = false; _LoadVideo_Private(); } RequestSerialization(); } get => _VideoIndex; } public bool PlayVideo { set { _VideoIsPlaying = value; if (_VideoIsPlaying) _PlayVideo_Private(); else _StopVideo_Private(); RequestSerialization(); } get => _VideoIsPlaying; } public Vector2 TimeAndOffset { set { _TimeAndOffset = value; if (!Networking.IsOwner(gameObject)) { Resync(); } } get => _TimeAndOffset; } }