using Cryville.Common; using Cryville.Common.Buffers; using Cryville.Common.Logging; using Cryville.Crtr.Config; using Cryville.Crtr.Event; using Cryville.Crtr.Ruleset; using Cryville.Crtr.Skin; using Cryville.Crtr.UI; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Formatting; using System.Threading; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.Scripting; using Coroutine = Cryville.Common.Coroutine; using Stopwatch = System.Diagnostics.Stopwatch; namespace Cryville.Crtr { public class ChartPlayer : MonoBehaviour { #region Fields Chart chart; SkinDefinition skin; public static PdtSkin pskin; RulesetDefinition ruleset; PdtRuleset pruleset; Dictionary texs; public static Dictionary frames; EventBus cbus; EventBus bbus; EventBus tbus; EventBus nbus; InputProxy inputProxy; Judge judge; public static EffectManager effectManager; bool started = false; static bool initialized; TextMeshProUGUI logs; TextMeshProUGUI status; BufferedLoggerListener loggerListener; static Vector2 screenSize; public static Rect hitRect; public static Plane[] frustumPlanes; RulesetConfig _rscfg; static bool disableGC = true; static float clippingDist = 1f; static float renderDist = 6f; static double renderStep = 0.05; public static double actualRenderStep = 0; static bool autoRenderStep = false; static float graphicalOffset = 0; public static float soundOffset = 0; static float startOffset = 0; public static int areaJudgePrecision = 16; public static float sv = 16f; public static Dictionary motionRegistry; #endregion #region MonoBehaviour void Start() { d_addLogEntry = AddLogEntry; var logobj = GameObject.Find("Logs"); if (logobj != null) logs = logobj.GetComponent(); if (!initialized) { Game.Init(); BuiltinResources.LoadDefault(); initialized = true; } OnSettingsUpdate(); status = GameObject.Find("Status").GetComponent(); loggerListener = new BufferedLoggerListener(); Game.MainLogger.AddListener(loggerListener); Game.SuspendBackgroundTasks(); try { Play(); } catch (Exception ex) { Game.LogException("Load/WorkerThread", "An error occurred while loading the data", ex); Popup.CreateException(ex); ReturnToMenu(); } } void OnDestroy() { cbus?.Dispose(); bbus?.Dispose(); tbus?.Dispose(); nbus?.Dispose(); loadThread?.Abort(); inputProxy?.Dispose(); if (texs != null) foreach (var t in texs) Texture.Destroy(t.Value); Game.MainLogger.RemoveListener(loggerListener); loggerListener.Dispose(); GC.Collect(); } Coroutine texLoader; bool texloaddone; Coroutine prehandler; int forceSyncFrames; double atime0; void Update() { if (started) GameUpdate(); else if (prehandler != null) { try { if (!prehandler.Tick(1.0 / Application.targetFrameRate)) { prehandler = null; started = true; } } catch (Exception ex) { Game.LogException("Load/Prehandle", "An error occurred while prehandling the data", ex); Popup.CreateException(ex); prehandler = null; Stop(); } } else if (loadThread != null) LoadUpdate(); if (logEnabled) LogUpdate(); else loggerListener.Enumerate((level, module, msg) => { }); } void GameUpdate() { try { if (Screen.width != screenSize.x || Screen.height != screenSize.y) throw new InvalidOperationException("Window resized while playing"); double dt, step; if (forceSyncFrames != 0) { forceSyncFrames--; double target = Game.AudioClient.Position - atime0; dt = target - cbus.Time - graphicalOffset; step = autoRenderStep ? 1f / Application.targetFrameRate : renderStep; inputProxy.SyncTime(target); } else { dt = Time.deltaTime; step = autoRenderStep ? Time.smoothDeltaTime : renderStep; } inputProxy.ForceTick(); if (paused) return; cbus.ForwardByTime(dt); bbus.ForwardByTime(dt); UnityEngine.Profiling.Profiler.BeginSample("ChartPlayer.Forward"); UnityEngine.Profiling.Profiler.BeginSample("EventBus.Copy"); bbus.CopyTo(tbus); bbus.CopyTo(nbus); UnityEngine.Profiling.Profiler.EndSample(); actualRenderStep = step; nbus.PreAnchor(); nbus.StripTempEvents(); nbus.ForwardStepByTime(clippingDist, step); nbus.EndPreGraphicalUpdate(); nbus.Anchor(); tbus.StripTempEvents(); tbus.ForwardStepByTime(clippingDist, step); tbus.ForwardStepByTime(renderDist, step); tbus.EndGraphicalUpdate(); UnityEngine.Profiling.Profiler.EndSample(); effectManager.Tick(cbus.Time); } catch (Exception ex) { Game.LogException("Game", "An error occurred while playing", ex); Popup.CreateException(ex); Stop(); } } void LoadUpdate() { if (!texloaddone) texLoader.Tick(1.0 / Application.targetFrameRate); if (!loadThread.IsAlive) { if (threadException != null) { Game.MainLogger.Log(4, "Load/MainThread", "Load failed"); loadThread = null; Popup.CreateException(threadException); ReturnToMenu(); } else if (texloaddone) { if (texLoader == null) Stop(); else { prehandler = new Coroutine(Prehandle()); texLoader = null; } loadThread = null; } } } readonly StringBuffer statusbuf = new(); readonly StringBuffer logsbuf = new(); readonly List logEntries = new(); readonly ArrayPool logBufferPool = new(); int logsLength = 0; LogHandler d_addLogEntry; void AddLogEntry(int level, string module, string msg) { string color = level switch { 0 => "#888888", 1 => "#bbbbbb", 2 => "#0088ff", 3 => "#ffff00", 4 => "#ff0000", 5 => "#bb0000", _ => "#ff00ff", }; var l = string.Format( "\n<{2}> {3}", DateTime.UtcNow.ToString("s"), color, module, msg ); logEntries.Add(l); logsLength += l.Length; } void LogUpdate() { logsbuf.Clear(); loggerListener.Enumerate(d_addLogEntry); while (logsLength >= 4096) { logsLength -= logEntries[0].Length; logEntries.RemoveAt(0); } foreach (var l in logEntries) { logsbuf.Append(l); } var lbuf = logBufferPool.Rent(logsbuf.Count); logsbuf.CopyTo(0, lbuf, 0, logsbuf.Count); logs.SetText(lbuf, 0, logsbuf.Count); logBufferPool.Return(lbuf); statusbuf.Clear(); statusbuf.AppendFormat( "FPS: i{0:0} / s{1:0}\nSMem: {2:N0} / {3:N0}\nIMem: {4:N0} / {5:N0}", 1 / Time.deltaTime, 1 / Time.smoothDeltaTime, #if UNITY_5_6_OR_NEWER UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(), UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(), UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong(), UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong() #else UnityEngine.Profiling.Profiler.GetMonoUsedSize(), UnityEngine.Profiling.Profiler.GetMonoHeapSize(), UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(), UnityEngine.Profiling.Profiler.GetTotalReservedMemory() #endif ); if (MotionNodePool.Shared != null) { statusbuf.AppendFormat( "\nPools: RMV {0}, MC {1}, MN {2}", RMVPool.Shared.RentedCount, MotionCachePool.Shared.RentedCount, MotionNodePool.Shared.RentedCount ); } if (texLoader != null) statusbuf.AppendFormat("\n(Loading textures) Progress: {0:P}", texLoader.Progress); if (loadThread != null) statusbuf.AppendFormat("\n(Loading files) Progress: {0:P}", loadPregress); if (prehandler != null) statusbuf.AppendFormat("\n(Prehandling) Progress: {0:P}", prehandler.Progress); if (started) { statusbuf.AppendFormat( "\nStates: c{0} / b{1}", cbus.ActiveStateCount, bbus.ActiveStateCount ); var aTime = Game.AudioClient.Position - atime0; var iTime = inputProxy.GetTimestampAverage(); statusbuf.AppendFormat( "\nSTime: {0:G9}s {5} {6}\nATime: {1:G9}s ({3:+0.0ms;-0.0ms;0}) {5} {6}\nITime: {2:G9}s ({4:+0.0ms;-0.0ms;0}) {5} {7}", cbus.Time, aTime, iTime, (aTime - cbus.Time) * 1e3, (iTime - cbus.Time) * 1e3, forceSyncFrames != 0 ? "(force sync)" : "", paused ? "(paused)" : "", paused ? "(semi-locked)" : "" ); if (judge != null) { statusbuf.Append("\n== Scores ==\n"); var fullScoreStrLen = judge.GetFullFormattedScoreString(logBufferPool, out char[] fullScoreStr); statusbuf.Append(fullScoreStr, 0, fullScoreStrLen); logBufferPool.Return(fullScoreStr); } } var buf = logBufferPool.Rent(statusbuf.Count); statusbuf.CopyTo(0, buf, 0, statusbuf.Count); status.SetText(buf, 0, statusbuf.Count); logBufferPool.Return(buf); } #endregion #region Triggers private void ReturnToMenu() { #if UNITY_EDITOR Invoke(nameof(ReturnToMenuImpl), 4); #else ReturnToMenuImpl(); #endif } private void ReturnToMenuImpl() { Master.Instance.ShowMenu(); Destroy(gameObject); Game.ResumeBackgroundTasks(); #if UNITY_5_5_OR_NEWER SceneManager.UnloadSceneAsync("Play"); #elif UNITY_5_3_OR_NEWER SceneManager.UnloadScene("Play"); #endif #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN DiscordController.Instance.SetIdle(); #endif } bool logEnabled = true; public void ToggleLogs() { logEntries.Clear(); logsLength = 0; logs.text = ""; status.SetText(""); logEnabled = !logEnabled; } public void TogglePlay() { if (started) Stop(); else if (prehandler != null) { prehandler = null; Stop(); } else if (texLoader != null || loadThread != null) { texloaddone = true; texLoader = null; if (loadThread.IsAlive) { Game.MainLogger.Log(2, "Game", "Stop requested while the chart is loading. Waiting for the loading thread to exit..."); } } else { Play(); } } bool paused = false; public void TogglePause() { paused = !paused; if (!paused) { forceSyncFrames = Settings.Default.ForceSyncFrames; Game.AudioClient.Start(); inputProxy.UnlockTime(); #if !UNITY_ANDROID || UNITY_EDITOR DiscordController.Instance.SetResume(cbus.Time); #endif } else { Game.AudioClient.Pause(); inputProxy.LockTime(); #if !UNITY_ANDROID || UNITY_EDITOR DiscordController.Instance.SetPaused(); #endif } } #endregion #region Load void Play() { disableGC = Settings.Default.DisableGC; clippingDist = Settings.Default.BackwardRenderDistance; renderDist = Settings.Default.ForwardRenderDistance; renderStep = Settings.Default.RenderStep; actualRenderStep = renderStep; autoRenderStep = renderStep == 0; graphicalOffset = Settings.Default.GraphicalOffset; soundOffset = Settings.Default.SoundOffset; startOffset = Settings.Default.StartOffset; areaJudgePrecision = 1 << Settings.Default.AreaJudgePrecision; forceSyncFrames = Settings.Default.ForceSyncFrames; texloaddone = false; Game.AudioSession = Game.AudioSequencer.NewSession(); var hitPlane = new Plane(Vector3.forward, Vector3.zero); var r0 = Camera.main.ViewportPointToRay(new Vector3(0, 0, 1)); hitPlane.Raycast(r0, out float dist); var p0 = r0.GetPoint(dist); var r1 = Camera.main.ViewportPointToRay(new Vector3(1, 1, 1)); hitPlane.Raycast(r1, out dist); var p1 = r1.GetPoint(dist); hitRect = new Rect(p0, p1 - p0); screenSize = new Vector2(Screen.width, Screen.height); frustumPlanes = GeometryUtility.CalculateFrustumPlanes(Camera.main); FileInfo chartFile = new(Settings.Default.LoadChart); FileInfo rulesetFile = new(Path.Combine( Game.GameDataPath, "rulesets", Settings.Default.LoadRuleset )); if (!rulesetFile.Exists) throw new FileNotFoundException("Ruleset for the chart not found\nMake sure you have imported the ruleset"); FileInfo rulesetConfigFile = new(Path.Combine( Game.GameDataPath, "config", "rulesets", Settings.Default.LoadRulesetConfig )); if (!rulesetConfigFile.Exists) throw new FileNotFoundException("Ruleset config not found\nPlease open the config to generate"); using (StreamReader cfgreader = new(rulesetConfigFile.FullName, Encoding.UTF8)) { _rscfg = JsonConvert.DeserializeObject(cfgreader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); } sv = _rscfg.generic.ScrollVelocity; soundOffset += _rscfg.generic.SoundOffset; FileInfo skinFile = new(Path.Combine( Game.GameDataPath, "skins", rulesetFile.Directory.Name, _rscfg.generic.Skin, ".umgs" )); if (!skinFile.Exists) throw new FileNotFoundException("Skin not found\nPlease specify an available skin in the config"); using (StreamReader reader = new(skinFile.FullName, Encoding.UTF8)) { skin = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (skin.format != SkinDefinition.CURRENT_FORMAT) throw new FormatException("Invalid skin file version"); } loadThread = new Thread(new ParameterizedThreadStart(Load)); loadThread.Start(new LoadInfo() { chartFile = chartFile, rulesetFile = rulesetFile, skinFile = skinFile, }); Game.MainLogger.Log(0, "Load/MainThread", "Loading textures..."); frames = new Dictionary(); texs = new Dictionary(); var skinDir = skinFile.Directory.FullName; var texLoadQueue = new List(); foreach (var f in skin.frames) { texLoadQueue.Add(Path.Combine(skinDir, f)); } texLoader = new Coroutine(LoadTextures(texLoadQueue)); } IEnumerator LoadTextures(List queue) { Stopwatch stopwatch = new(); stopwatch.Start(); #if UNITY_5_4_OR_NEWER DownloadHandlerTexture texHandler = null; UnityWebRequest texLoader = null; #else WWW texLoader = null; #endif for (int i = 0; i < queue.Count; i++) { #if UNITY_5_4_OR_NEWER texHandler = new DownloadHandlerTexture(); texLoader = new UnityWebRequest(PlatformConfig.FileProtocolPrefix + queue[i], "GET", texHandler, null); texLoader.SendWebRequest(); #else texLoader = new WWW(Game.FileProtocolPrefix + queue[i]); #endif while (!texLoader.isDone) yield return (float)i / queue.Count; #if UNITY_5_4_OR_NEWER string url = texLoader.url; string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1)); if (texHandler.isDone) { var tex = texHandler.texture; tex.wrapMode = TextureWrapMode.Clamp; if (frames.ContainsKey(name)) { Game.MainLogger.Log(3, "Load/Prehandle", "Duplicated texture name: {0}", name); } else { frames.Add(name, new SpriteFrame(tex)); } texs.Add(name, tex); } else { Game.MainLogger.Log(4, "Load/Prehandle", "Unable to load texture: {0}", name); } texLoader.Dispose(); texHandler.Dispose(); #else string url = texLoader.url; string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1)); var tex = texLoader.texture; tex.wrapMode = TextureWrapMode.Clamp; texs.Add(name, tex); texLoader.Dispose(); texLoader = null; #endif } texloaddone = true; stopwatch.Stop(); Game.MainLogger.Log(1, "Load/MainThread", "Main thread done ({0}ms)", stopwatch.Elapsed.TotalMilliseconds); yield return 1; } IEnumerator Prehandle() { Stopwatch timer = new(); timer.Reset(); timer.Start(); Game.MainLogger.Log(0, "Load/Prehandle", "Prehandling (iteration 2)"); yield return 0; cbus.BroadcastPreInit(); Game.MainLogger.Log(0, "Load/Prehandle", "Prehandling (iteration 3)"); yield return 0; using (var pbus = cbus.Clone(17)) { while (pbus.Time != double.PositiveInfinity) { pbus.ForwardOnce(); yield return (float)pbus.EventId / pbus.EventCount; } } Game.MainLogger.Log(0, "Load/Prehandle", "Prehandling (iteration 4)"); yield return 1; cbus.BroadcastPostInit(); Game.MainLogger.Log(0, "Load/Prehandle", "Seeking to start offset"); yield return 1; cbus.ForwardByTime(startOffset); bbus.ForwardByTime(startOffset); Game.AudioSequencer.SeekTime(startOffset, SeekOrigin.Current); Game.MainLogger.Log(0, "Load/Prehandle", "Cleaning up"); yield return 1; if (logEnabled && Settings.Default.HideLogOnPlay) ToggleLogs(); Camera.main.cullingMask |= 1; GC.Collect(); if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Disabled; timer.Stop(); Game.MainLogger.Log(1, "Load/Prehandle", "Prehandling done ({0}ms)", timer.Elapsed.TotalMilliseconds); yield return 1; if (Settings.Default.ClearLogOnPlay) { logEntries.Clear(); logsLength = 0; loggerListener.Enumerate((level, module, msg) => { }); logs.text = ""; } Game.AudioSequencer.Playing = true; atime0 = Game.AudioClient.BufferPosition - startOffset; inputProxy.SyncTime(cbus.Time); inputProxy.Activate(); } public void Stop() { try { Game.MainLogger.Log(1, "Game", "Stopping"); Game.AudioClient.Start(); Game.AudioSession = Game.AudioSequencer.NewSession(); Camera.main.cullingMask &= ~1; if (inputProxy != null) { inputProxy.Deactivate(); inputProxy = null; } judge = null; if (nbus != null) { nbus.Dispose(); nbus = null; } if (tbus != null) { tbus.Dispose(); tbus = null; } if (bbus != null) { bbus.Dispose(); bbus = null; } if (cbus != null) { cbus.Dispose(); cbus.DisposeAll(); cbus = null; } if (effectManager != null) { effectManager.Dispose(); effectManager = null; } PdtEvaluator.Instance.Reset(); motionRegistry = null; Game.MainLogger.Log(1, "Game", "Stopped"); } catch (Exception ex) { if (!logEnabled) ToggleLogs(); Game.LogException("Game", "An error occurred while stopping", ex); Popup.CreateException(ex); } finally { if (started) { if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Enabled; GC.Collect(); started = false; } } ReturnToMenu(); } void OnSettingsUpdate() { Application.targetFrameRate = Settings.Default.TargetFrameRate; QualitySettings.vSyncCount = Settings.Default.VSync ? 1 : 0; } #endregion #region Threaded struct LoadInfo { public FileInfo chartFile; public FileInfo rulesetFile; public FileInfo skinFile; } Exception threadException; Thread loadThread = null; volatile float loadPregress; Stopwatch workerTimer; void Load(object _info) { var info = (LoadInfo)_info; try { workerTimer = new Stopwatch(); workerTimer.Start(); LoadChart(info); workerTimer.Stop(); Game.MainLogger.Log(1, "Load/WorkerThread", "Worker thread done ({0}ms)", workerTimer.Elapsed.TotalMilliseconds); } catch (Exception ex) { Game.LogException("Load/WorkerThread", "An error occurred while loading the data", ex); threadException = ex; } } void LoadChart(LoadInfo info) { Game.MainLogger.Log(0, "Load/WorkerThread", "Loading chart: {0}", info.chartFile); motionRegistry = new Dictionary { { new Identifier("pt") , new MotionRegistry(typeof(Vec2)) }, { new Identifier("dir") , new MotionRegistry(typeof(Vec3)) }, { new Identifier("normal") , new MotionRegistry(typeof(Vec3)) }, { new Identifier("sv") , new MotionRegistry(new Vec1(0f), new Vec1(hitRect.height)) }, { new Identifier("svm") , new MotionRegistry(new Vec1m(1f)) }, { new Identifier("dist") , new MotionRegistry(new Vec1(0f), new Vec1(float.PositiveInfinity)) }, { new Identifier("track") , new MotionRegistry(typeof(Vec1)) }, }; using StreamReader reader = new(info.chartFile.FullName, Encoding.UTF8); PdtEvaluator.Instance.Reset(); LoadRuleset(info.rulesetFile); loadPregress = .05f; chart = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); Game.MainLogger.Log(0, "Load/WorkerThread", "Applying ruleset (iteration 1)"); loadPregress = .10f; pruleset.PrePatch(chart); Game.MainLogger.Log(0, "Load/WorkerThread", "Batching events"); loadPregress = .20f; var batcher = new EventBatcher(chart); batcher.Forward(); cbus = batcher.Batch(); loadPregress = .30f; LoadSkin(info.skinFile); Game.MainLogger.Log(0, "Load/WorkerThread", "Initializing judge and input"); loadPregress = .35f; judge = new Judge(this, pruleset); PdtEvaluator.Instance.ContextJudge = judge; inputProxy = new InputProxy(pruleset, judge, screenSize); inputProxy.LoadFrom(_rscfg.inputs); if (!inputProxy.IsCompleted()) { Game.MainLogger.Log(2, "Game", "Input config not completed. Input disabled"); inputProxy.Clear(); } Game.MainLogger.Log(0, "Load/WorkerThread", "Attaching handlers"); loadPregress = .40f; var ch = new ChartHandler(chart); cbus.RootState.AttachHandler(ch); foreach (var gs in cbus.RootState.Children) { var gh = new GroupHandler((Chart.Group)gs.Key, ch); gs.Value.AttachHandler(gh); foreach (var ts in gs.Value.Children) { ContainerHandler th; if (ts.Key is Chart.Note) { th = new NoteHandler((Chart.Note)ts.Key, gh); } else { th = new TrackHandler((Chart.Track)ts.Key, gh); } ts.Value.AttachHandler(th); } } cbus.AttachSystems(pskin, judge); Game.MainLogger.Log(0, "Load/WorkerThread", "Prehandling (iteration 1)"); loadPregress = .60f; using (var pbus = cbus.Clone(16)) { pbus.Forward(); } Game.MainLogger.Log(0, "Load/WorkerThread", "Cloning states (type 1)"); loadPregress = .70f; bbus = cbus.Clone(1, -clippingDist); Game.MainLogger.Log(0, "Load/WorkerThread", "Cloning states (type 2)"); loadPregress = .80f; tbus = bbus.Clone(2); Game.MainLogger.Log(0, "Load/WorkerThread", "Cloning states (type 3)"); loadPregress = .90f; nbus = bbus.Clone(3); loadPregress = 1; } void LoadRuleset(FileInfo file) { DirectoryInfo dir = file.Directory; Game.MainLogger.Log(0, "Load/WorkerThread", "Loading ruleset: {0}", file); using (StreamReader reader = new(file.FullName, Encoding.UTF8)) { ruleset = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (ruleset.format != RulesetDefinition.CURRENT_FORMAT) throw new FormatException("Invalid ruleset file version"); ruleset.LoadPdt(dir); pruleset = ruleset.Root; pruleset.Optimize(PdtEvaluator.Instance); } PdtEvaluator.Instance.ContextRulesetConfig = new RulesetConfigStore(pruleset.configs, _rscfg.configs); RMVPool.Shared = new RMVPool(); MotionCachePool.Shared = new MotionCachePool(); MotionNodePool.Shared = new MotionNodePool(); } void LoadSkin(FileInfo file) { DirectoryInfo dir = file.Directory; Game.MainLogger.Log(0, "Load/WorkerThread", "Loading skin: {0}", file); skin.LoadPdt(dir); pskin = skin.Root; pskin.Optimize(PdtEvaluator.Instance); effectManager = new EffectManager(pskin); } #endregion } }