#define BUILD using Cryville.Common; using Cryville.Common.Buffers; using Cryville.Crtr.Config; using Cryville.Crtr.Event; 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 diag = System.Diagnostics; using Logger = Cryville.Common.Logger; namespace Cryville.Crtr { public class ChartPlayer : MonoBehaviour { #region Fields Chart chart; Skin skin; public static PdtSkin pskin; Ruleset ruleset; PdtRuleset pruleset; Dictionary texs; public static Dictionary frames; readonly Queue texLoadQueue = new Queue(); #if UNITY_5_4_OR_NEWER DownloadHandlerTexture texHandler; UnityWebRequest texLoader = null; #else WWW texLoader = null; #endif 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; 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; public static float graphicalOffset = 0; public static float soundOffset = 0; static float startOffset = 0; public static float sv = 16f; public static Dictionary motionRegistry = new Dictionary(); public static PdtEvaluator etor; #endregion #region MonoBehaviour void Start() { d_addLogEntry = new Action(AddLogEntry); var logobj = GameObject.Find("Logs"); if (logobj != null) logs = logobj.GetComponent(); if (!initialized) { Game.Init(); GenericResources.LoadDefault(); initialized = true; } OnSettingsUpdate(); status = GameObject.Find("Status").GetComponent(); texHandler = new DownloadHandlerTexture(); #if BUILD try { Play(); } catch (Exception ex) { Game.LogException("Load/WorkerThread", "An error occured while loading the data", ex); Popup.CreateException(ex); ReturnToMenu(); } #endif // Camera.main.RenderToCubemap(); } void OnDestroy() { if (cbus != null) cbus.Dispose(); if (bbus != null) bbus.Dispose(); if (tbus != null) tbus.Dispose(); if (nbus != null) nbus.Dispose(); if (loadThread != null) loadThread.Abort(); if (texLoader != null) texLoader.Dispose(); if (inputProxy != null) inputProxy.Dispose(); if (texs != null) foreach (var t in texs) Texture.Destroy(t.Value); GC.Collect(); } bool texloaddone; diag::Stopwatch texloadtimer = new diag::Stopwatch(); int forceSyncFrames; double atime0; void Update() { if (started) GameUpdate(); else if (loadThread != null) LoadUpdate(); if (logEnabled) LogUpdate(); else Game.MainLogger.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(2, tbus); bbus.CopyTo(3, nbus); UnityEngine.Profiling.Profiler.EndSample(); actualRenderStep = step; 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 occured while playing", ex); Popup.CreateException(ex); Stop(); } } void LoadUpdate() { if (texLoader != null) { string url = texLoader.url; string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1)); #if UNITY_5_4_OR_NEWER if (texLoader.isDone) { if (texHandler.isDone) { var tex = texHandler.texture; tex.wrapMode = TextureWrapMode.Clamp; if (frames.ContainsKey(name)) { Logger.Log("main", 3, "Load/Prehandle", "Duplicated texture name: {0}", name); } else { frames.Add(name, new SpriteFrame(tex)); } texs.Add(name, tex); } else { Logger.Log("main", 4, "Load/Prehandle", "Unable to load texture: {0}", name); } texLoader.Dispose(); texHandler.Dispose(); texLoader = null; } #else if (texLoader.isDone) { var tex = texLoader.texture; tex.wrapMode = TextureWrapMode.Clamp; texs.Add(name, tex); texLoader.Dispose(); texLoader = null; } #endif } if (texLoader == null) { if (texLoadQueue.Count > 0) { #if UNITY_5_4_OR_NEWER texHandler = new DownloadHandlerTexture(); texLoader = new UnityWebRequest(Game.FileProtocolPrefix + texLoadQueue.Dequeue(), "GET", texHandler, null); texLoader.SendWebRequest(); #else texLoader = new WWW(Game.FileProtocolPrefix + texLoadQueue.Dequeue()); #endif } else if (!texloaddone) { texloaddone = true; texloadtimer.Stop(); Logger.Log("main", 1, "Load/MainThread", "Main thread done ({0}ms)", texloadtimer.Elapsed.TotalMilliseconds); } } if (!loadThread.IsAlive) { if (threadException != null) { Logger.Log("main", 4, "Load/MainThread", "Load failed"); loadThread = null; Popup.CreateException(threadException); #if BUILD ReturnToMenu(); #endif } else if (texLoader == null) { Prehandle(); loadThread = null; } } } readonly TargetString statusstr = new TargetString(); readonly StringBuffer statusbuf = new StringBuffer(); readonly TargetString logsstr = new TargetString(); readonly StringBuffer logsbuf = new StringBuffer(); readonly List logEntries = new List(); int logsLength = 0; Action d_addLogEntry; void AddLogEntry(int level, string module, string msg) { string color; switch (level) { case 0: color = "#888888"; break; case 1: color = "#bbbbbb"; break; case 2: color = "#0088ff"; break; case 3: color = "#ffff00"; break; case 4: color = "#ff0000"; break; case 5: color = "#bb0000"; break; default: color = "#ff00ff"; break; } var l = string.Format( "\n<{2}> {3}", DateTime.UtcNow.ToString("s"), color, module, msg ); logEntries.Add(l); logsLength += l.Length; } void LogUpdate() { logsbuf.Clear(); Game.MainLogger.Enumerate(d_addLogEntry); while (logsLength >= 4096) { logsLength -= logEntries[0].Length; logEntries.RemoveAt(0); } foreach (var l in logEntries) { logsbuf.Append(l); } logsstr.Length = logsbuf.Count; var larr = logsstr.TrustedAsArray(); logsbuf.CopyTo(0, larr, 0, logsbuf.Count); logs.SetText(larr, 0, logsbuf.Count); 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 (started) { statusbuf.AppendFormat( "\nStates: c{0} / b{1}\nPools: RMV {2}, MC {3}", cbus.ActiveStateCount, bbus.ActiveStateCount, ContainerState.RMVPool.RentedCount, ContainerState.MCPool.RentedCount ); statusbuf.AppendFormat( "\nSTime: {0:G17}s {3} {4}\ndATime: {1:+0.0ms;-0.0ms;0} {3} {4}\ndITime: {2:+0.0ms;-0.0ms;0} {3} {5}", cbus.Time, (Game.AudioClient.Position - atime0 - cbus.Time) * 1e3, (inputProxy.GetTimestampAverage() - cbus.Time) * 1e3, forceSyncFrames != 0 ? "(force sync)" : "", paused ? "(paused)" : "", paused ? "(semi-locked)" : "" ); if (judge != null) { statusbuf.Append("\n== Scores ==\n"); var fullScoreStr = judge.GetFullFormattedScoreString(); statusbuf.Append(fullScoreStr.TrustedAsArray(), 0, fullScoreStr.Length); } } statusstr.Length = statusbuf.Count; var sarr = statusstr.TrustedAsArray(); statusbuf.CopyTo(0, sarr, 0, statusbuf.Count); status.SetText(sarr, 0, statusbuf.Count); } #endregion #region Triggers private void ReturnToMenu() { #if UNITY_EDITOR Invoke(nameof(_returnToMenu), 4); #else _returnToMenu(); #endif } #pragma warning disable IDE1006 private void _returnToMenu() { GameObject.Find("Master").GetComponent().ShowMenu(); GameObject.Destroy(gameObject); #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 } #pragma warning restore IDE1006 bool logEnabled = true; public void ToggleLogs() { logEntries.Clear(); logsLength = 0; logs.text = ""; status.SetText(""); logEnabled = !logEnabled; } public void TogglePlay() { if (started) Stop(); else { if (loadThread == null) Play(); else Logger.Log("main", 2, "Load/MainThread", "The chart is currently loading"); } } bool paused = false; public void TogglePause() { paused = !paused; if (!paused) { forceSyncFrames = Settings.Default.ForceSyncFrames; Game.AudioClient.Start(); inputProxy.UnlockTime(); } else { Game.AudioClient.Pause(); inputProxy.LockTime(); } } #endregion #region Load void Play() { disableGC = Settings.Default.DisableGC; clippingDist = Settings.Default.BackwardClippingDistance; renderDist = Settings.Default.RenderDistance; renderStep = Settings.Default.RenderStep; actualRenderStep = renderStep; autoRenderStep = renderStep == 0; graphicalOffset = Settings.Default.GraphicalOffset; soundOffset = Settings.Default.SoundOffset; startOffset = Settings.Default.StartOffset; forceSyncFrames = Settings.Default.ForceSyncFrames; texloaddone = false; Game.NetworkTaskWorker.SuspendBackgroundTasks(); Game.AudioSession = Game.AudioSequencer.NewSession(); var hitPlane = new Plane(Vector3.forward, Vector3.zero); var r0 = Camera.main.ViewportPointToRay(new Vector3(0, 0, 1)); float dist; hitPlane.Raycast(r0, out 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 FileInfo( Game.GameDataPath + "/charts/" + Settings.Default.LoadChart ); FileInfo rulesetFile = new FileInfo( 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 FileInfo( 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 StreamReader(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 FileInfo( string.Format("{0}/skins/{1}/{2}/.umgs", Game.GameDataPath, rulesetFile.Directory.Name, _rscfg.generic.Skin) ); if (!skinFile.Exists) throw new FileNotFoundException("Skin not found\nPlease specify an available skin in the config"); using (StreamReader reader = new StreamReader(skinFile.FullName, Encoding.UTF8)) { skin = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (skin.format != Skin.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, }); Logger.Log("main", 0, "Load/MainThread", "Loading textures..."); texloadtimer = new diag::Stopwatch(); texloadtimer.Start(); frames = new Dictionary(); texs = new Dictionary(); var skinDir = skinFile.Directory.FullName; foreach (var f in skin.frames) { texLoadQueue.Enqueue(Path.Combine(skinDir, f)); } } void Prehandle() { try { diag::Stopwatch timer = new diag::Stopwatch(); timer.Reset(); timer.Start(); Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 2)"); cbus.BroadcastPreInit(); Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 3)"); using (var pbus = cbus.Clone(17)) { pbus.Forward(); } Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 4)"); cbus.BroadcastPostInit(); inputProxy.Activate(); if (logEnabled && Settings.Default.HideLogOnPlay) ToggleLogs(); Logger.Log("main", 0, "Load/Prehandle", "Cleaning up"); GC.Collect(); if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Disabled; cbus.ForwardByTime(startOffset); bbus.ForwardByTime(startOffset); timer.Stop(); Logger.Log("main", 1, "Load/Prehandle", "Prehandling done ({0}ms)", timer.Elapsed.TotalMilliseconds); if (Settings.Default.ClearLogOnPlay) { logEntries.Clear(); logsLength = 0; Game.MainLogger.Enumerate((level, module, msg) => { }); logs.text = ""; } Game.AudioSequencer.Playing = true; atime0 = Game.AudioClient.BufferPosition; Thread.Sleep((int)((atime0 - Game.AudioClient.Position) * 1000)); inputProxy.SyncTime(cbus.Time); started = true; } catch (Exception ex) { Game.LogException("Load/Prehandle", "An error occured while prehandling the data", ex); Popup.CreateException(ex); Stop(); } } public void Stop() { try { Logger.Log("main", 1, "Game", "Stopping"); Game.AudioClient.Start(); Game.AudioSession = Game.AudioSequencer.NewSession(); inputProxy.Deactivate(); 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; } effectManager.Dispose(); effectManager = null; etor = null; Logger.Log("main", 1, "Game", "Stopped"); } catch (Exception ex) { if (!logEnabled) ToggleLogs(); Game.LogException("Game", "An error occured while stopping", ex); Popup.CreateException(ex); } finally { if (started) { if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Enabled; GC.Collect(); started = false; } } Game.NetworkTaskWorker.ResumeBackgroundTasks(); #if BUILD ReturnToMenu(); #endif } 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; #if !NO_THREAD Thread loadThread = null; diag::Stopwatch workerTimer; #endif void Load(object _info) { var info = (LoadInfo)_info; try { workerTimer = new diag::Stopwatch(); workerTimer.Start(); LoadChart(info); workerTimer.Stop(); Logger.Log("main", 1, "Load/WorkerThread", "Worker thread done ({0}ms)", workerTimer.Elapsed.TotalMilliseconds); } catch (Exception ex) { Game.LogException("Load/WorkerThread", "An error occured while loading the data", ex); threadException = ex; } } void LoadChart(LoadInfo info) { DirectoryInfo dir = info.chartFile.Directory; Logger.Log("main", 0, "Load/WorkerThread", "Loading chart: {0}", info.chartFile); using (StreamReader reader = new StreamReader(info.chartFile.FullName, Encoding.UTF8)) { chart = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (chart.format != 2) throw new FormatException("Invalid chart file format version"); etor = new PdtEvaluator(); LoadRuleset(info.rulesetFile); Logger.Log("main", 0, "Load/WorkerThread", "Applying ruleset (iteration 1)"); pruleset.PrePatch(chart); Logger.Log("main", 0, "Load/WorkerThread", "Batching events"); var batcher = new EventBatcher(chart); batcher.Forward(); cbus = batcher.Batch(); LoadSkin(info.skinFile); Logger.Log("main", 0, "Load/WorkerThread", "Initializing judge and input"); judge = new Judge(this, pruleset); etor.ContextJudge = judge; inputProxy = new InputProxy(pruleset, judge); inputProxy.LoadFrom(_rscfg.inputs); if (!inputProxy.IsCompleted()) { throw new ArgumentException("Input config not completed\nPlease complete the input settings"); } Logger.Log("main", 0, "Load/WorkerThread", "Attaching handlers"); var ch = new ChartHandler(chart, dir); 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); Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 1)"); using (var pbus = cbus.Clone(16)) { pbus.Forward(); } Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 1)"); bbus = cbus.Clone(1, -clippingDist); Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 2)"); tbus = bbus.Clone(2); Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 3)"); nbus = bbus.Clone(3); } } void LoadRuleset(FileInfo file) { DirectoryInfo dir = file.Directory; Logger.Log("main", 0, "Load/WorkerThread", "Loading ruleset: {0}", file); using (StreamReader reader = new StreamReader(file.FullName, Encoding.UTF8)) { ruleset = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (ruleset.format != Ruleset.CURRENT_FORMAT) throw new FormatException("Invalid ruleset file version"); ruleset.LoadPdt(dir); pruleset = ruleset.Root; pruleset.Optimize(etor); } ContainerState.RMVPool = new RMVPool(); ContainerState.MCPool = new MotionCachePool(); } void LoadSkin(FileInfo file) { DirectoryInfo dir = file.Directory; Logger.Log("main", 0, "Load/WorkerThread", "Loading skin: {0}", file); skin.LoadPdt(dir); pskin = skin.Root; pskin.Optimize(etor); effectManager = new EffectManager(pskin); } #endregion } }