//#define NO_THREAD #define BUILD using Cryville.Common; using Cryville.Common.Plist; using Cryville.Crtr.Event; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.Scripting; using UnityEngine.UI; using diag = System.Diagnostics; using Logger = Cryville.Common.Logger; namespace Cryville.Crtr { public class ChartPlayer : MonoBehaviour { Chart chart; Skin skin; PdtSkin pskin; Ruleset ruleset; PdtRuleset pruleset; Dictionary texs; public static Dictionary frames; List plists; 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; Judge judge; bool started = false; static bool initialized; static Text logs; Text status; static Vector2 screenSize; public static Rect hitRect; public static Plane[] frustumPlanes; static bool disableGC = true; static float clippingDist = 1f; static float renderDist = 6f; static float renderStep = 0.05f; public static float actualRenderStep = 0f; static bool autoRenderStep = false; public static float soundOffset = 0; static float startOffset = 0; public static float sv = 16f; public static Dictionary motionRegistry = new Dictionary(); public static PdtEvaluator etor; ~ChartPlayer() { Dispose(); } public void Dispose() { #if !NO_THREAD if (loadThread != null) loadThread.Abort(); #endif if (texLoader != null) texLoader.Dispose(); } #region MonoBehaviour void Start() { 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 Play(); #endif // Camera.main.RenderToCubemap(); } bool texloaddone; diag::Stopwatch texloadtimer = new diag::Stopwatch(); bool firstFrame; void Update() { // if (Input.GetKeyDown(KeyCode.Return)) TogglePlay(); if (started) { try { if (Screen.width != screenSize.x || Screen.height != screenSize.y) throw new InvalidOperationException("Window resized while playing"); float dt = firstFrame ? 1f / Application.targetFrameRate : Time.deltaTime; firstFrame = false; cbus.ForwardByTime(dt); bbus.ForwardByTime(dt); UnityEngine.Profiling.Profiler.BeginSample("ChartPlayer.FeedJudge"); judge.StartFrame(); Game.InputManager.EnumerateEvents(ev => { // Logger.Log("main", 0, "Input", ev.ToString()); judge.Feed(ev); }); judge.EndFrame(); UnityEngine.Profiling.Profiler.EndSample(); UnityEngine.Profiling.Profiler.BeginSample("ChartPlayer.Forward"); UnityEngine.Profiling.Profiler.BeginSample("EventBus.Copy"); bbus.CopyTo(2, tbus); bbus.CopyTo(3, nbus); UnityEngine.Profiling.Profiler.EndSample(); float step = autoRenderStep ? ( firstFrame ? 1f / Application.targetFrameRate : Time.smoothDeltaTime ) : renderStep; actualRenderStep = step; nbus.ForwardStepByTime(clippingDist, step); nbus.BroadcastEndUpdate(); nbus.Anchor(); tbus.ForwardStepByTime(clippingDist, step); tbus.ForwardStepByTime(renderDist, step); tbus.BroadcastEndUpdate(); UnityEngine.Profiling.Profiler.EndSample(); } catch (Exception ex) { Game.LogException("Game", "An error occured while playing", ex); Stop(); } } #if !NO_THREAD else if (loadThread != null) { 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 (texHandler.isDone) { var tex = texHandler.texture; texs.Add(name, tex); Logger.Log("main", 0, "Load/MainThread", "Loaded texture {0} ({1} bytes)", name, texLoader.downloadedBytes); texLoader.Dispose(); texHandler.Dispose(); texLoader = null; } else if (texLoader.downloadProgress != 0) { Logger.Log("main", 0, "Load/MainThread", "Loading texture {0} {1:P0}", name, texLoader.downloadProgress); } #else if (texLoader.isDone) { var tex = texLoader.texture; texs.Add(name, tex); Logger.Log("main", 0, "Load/MainThread", "Loaded texture {0} ({1} bytes)", name, texLoader.bytesDownloaded); texLoader.Dispose(); texLoader = null; } else if (texLoader.progress != 0) { Logger.Log("main", 0, "Load/MainThread", "Loading texture {0} {1:P0}", name, texLoader.progress); } #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 (cbus == null) { Logger.Log("main", 4, "Load/MainThread", "Load failed"); loadThread = null; #if BUILD ReturnToMenu(); #endif } else if (texLoader == null) { Prehandle(); loadThread = null; } } } #endif if (logEnabled) { string _logs = logs.text; Game.MainLogger.Enumerate((level, module, 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; } _logs += string.Format( "\r\n<{2}> {3}", DateTime.UtcNow.ToString("s"), color, module, msg ); }); logs.text = _logs.Substring(Mathf.Max(0, _logs.IndexOf('\n', Mathf.Max(0, _logs.Length - 4096)))); var sttext = string.Format( "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 (judge != null) sttext += "\n== Scores ==\n" + judge.GetFullFormattedScoreString(); status.text = sttext; } else { Game.MainLogger.Enumerate((level, module, msg) => { }); } } #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 } #pragma warning restore IDE1006 bool logEnabled = true; public void ToggleLogs() { logs.text = ""; status.text = ""; 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"); } } #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; soundOffset = Settings.Default.SoundOffset; startOffset = Settings.Default.StartOffset; sv = Settings.Default.ScrollVelocity; firstFrame = true; #if !NO_THREAD texloaddone = false; #endif 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 ); FileInfo skinFile = new FileInfo( Game.GameDataPath + "/skins/" + Settings.Default.LoadSkin ); Logger.Log("main", 0, "Load/MainThread", "Loading textures..."); texloadtimer = new diag::Stopwatch(); texloadtimer.Start(); texs = new Dictionary(); var flist = skinFile.Directory.GetFiles("*.png"); foreach (FileInfo f in flist) { #if NO_THREAD using (WWW w = new WWW("file:///" + f.FullName)) { string name = StringUtils.TrimExt(f.Name); while (!w.isDone); texs.Add(name, w.texture); } #else texLoadQueue.Enqueue(f.FullName); #endif } #if NO_THREAD texloadtimer.Stop(); Logger.LogFormat("main", 0, "Load/MainThread", "Textures loaded successfully ({0}ms)", texloadtimer.Elapsed.TotalMilliseconds); Load(new LoadInfo(){ chartFile = chartFile, rulesetFile = rulesetFile, skinFile = skinFile, }); Prehandle(); #else loadThread = new Thread(new ParameterizedThreadStart(Load)); loadThread.Start(new LoadInfo() { chartFile = chartFile, rulesetFile = rulesetFile, skinFile = skinFile, }); #endif } void Prehandle() { try { diag::Stopwatch timer = new diag::Stopwatch(); timer.Reset(); timer.Start(); Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 3)"); foreach (var i in plists) i.Init(texs); foreach (var t in texs) { if (frames.ContainsKey(t.Key)) { Logger.Log("main", 3, "Load/Prehandle", "Duplicated texture name: {0}", t.Key); continue; } var f = new Cocos2dFrames.Frame(t.Value); f.Init(); frames.Add(t.Key, f); } Logger.Log("main", 0, "Load/Prehandle", "Initializing states"); cbus.BroadcastInit(); Game.InputManager.Activate(); if (logEnabled) ToggleLogs(); Logger.Log("main", 0, "Load/Prehandle", "Cleaning up"); GC.Collect(); if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Disabled; timer.Stop(); Logger.Log("main", 1, "Load/Prehandle", "Prehandling done ({0}ms)", timer.Elapsed.TotalMilliseconds); Game.AudioSequencer.Playing = true; Thread.Sleep((int)(Game.AudioClient.BufferPosition - Game.AudioClient.Position)); Game.InputManager.SyncTime(cbus.Time); started = true; } catch (Exception ex) { Game.LogException("Load/Prehandle", "An error occured while prehandling the data", ex); Stop(); } } public void Stop() { try { Logger.Log("main", 1, "Game", "Stopping"); chart = null; Game.AudioSession = Game.AudioSequencer.NewSession(); if (cbus != null) cbus.Dispose(); if (bbus != null) bbus.Dispose(); if (tbus != null) tbus.Dispose(); if (nbus != null) nbus.Dispose(); // Game.InputManager.Deactivate(); foreach (var t in texs) Texture.Destroy(t.Value); Logger.Log("main", 1, "Game", "Stopped"); } catch (Exception ex) { if (!logEnabled) ToggleLogs(); Game.LogException("Game", "An error occured while stopping", 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; } #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(); RMVPool.Prepare(); 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); } } 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(); Logger.Log("main", 0, "Load/WorkerThread", "Batched {0} event batches", cbus.events.Count); judge = new Judge(pruleset); etor.ContextJudge = judge; LoadSkin(info.skinFile); cbus.AttachSystems(pskin, judge); 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(gh, (Chart.Note)ts.Key, judge); } else { th = new TrackHandler(gh, (Chart.Track)ts.Key); } ts.Value.AttachHandler(th); } } Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 1)"); cbus.Clone(16).Forward(); Logger.Log("main", 0, "Load/WorkerThread", "Patching events"); cbus.DoPatch(); Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 2)"); cbus.Clone(17).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 != 1) throw new FormatException("Invalid ruleset file version"); ruleset.LoadPdt(dir); pruleset = ruleset.Root; pruleset.Optimize(etor); } } void LoadSkin(FileInfo file) { DirectoryInfo dir = file.Directory; Logger.Log("main", 0, "Load/WorkerThread", "Loading skin: {0}", file); using (StreamReader reader = new StreamReader(file.FullName, Encoding.UTF8)) { skin = JsonConvert.DeserializeObject(reader.ReadToEnd(), new JsonSerializerSettings() { MissingMemberHandling = MissingMemberHandling.Error }); if (skin.format != 1) throw new FormatException("Invalid skin file version"); skin.LoadPdt(dir); pskin = skin.Root; pskin.Optimize(etor); } plists = new List(); frames = new Dictionary(); foreach (FileInfo f in file.Directory.GetFiles("*.plist")) { var pobj = PlistConvert.Deserialize(f.FullName); plists.Add(pobj); foreach (var i in pobj.frames) frames.Add(StringUtils.TrimExt(i.Key), i.Value); } } #endregion } }