Files
crtr/Assets/Cryville/Crtr/ChartPlayer.cs
2022-11-01 11:28:48 +08:00

564 lines
17 KiB
C#

//#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<string, Texture2D> texs;
public static Dictionary<string, Cocos2dFrames.Frame> frames;
List<Cocos2dFrames> plists;
readonly Queue<string> texLoadQueue = new Queue<string>();
#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<string, MotionRegistry> motionRegistry = new Dictionary<string, MotionRegistry>();
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<Text>();
if (!initialized) {
Game.Init();
GenericResources.LoadDefault();
initialized = true;
}
OnSettingsUpdate();
status = GameObject.Find("Status").GetComponent<Text>();
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<color={1}bb><{2}> {3}</color>",
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<Master>().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
);
#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
Logger.Log("main", 0, "Load/MainThread", "Loading textures...");
texloadtimer = new diag::Stopwatch();
texloadtimer.Start();
texs = new Dictionary<string, Texture2D>();
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
}
}
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<Chart>(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<Ruleset>(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<Skin>(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<Cocos2dFrames>();
frames = new Dictionary<string, Cocos2dFrames.Frame>();
foreach (FileInfo f in file.Directory.GetFiles("*.plist")) {
var pobj = PlistConvert.Deserialize<Cocos2dFrames>(f.FullName);
plists.Add(pobj);
foreach (var i in pobj.frames)
frames.Add(StringUtils.TrimExt(i.Key), i.Value);
}
}
#endregion
}
}