using Cryville.Audio; using Cryville.Audio.Source; using Cryville.Common.Font; using Cryville.Common.Logging; using Cryville.Common.Unity; using Cryville.Common.Unity.UI; using Cryville.Crtr.UI; using Cryville.Culture; using Cryville.Input; using Cryville.Input.Unity; #if UNITY_ANDROID && !UNITY_EDITOR using Cryville.Input.Unity.Android; using Cryville.Interop.Java; using Cryville.Interop.Java.Unity; #endif using FFmpeg.AutoGen; using Ionic.Zip; using Newtonsoft.Json; using System; using System.Globalization; using System.IO; using System.Text; using System.Xml; using System.Xml.Linq; using UnityEngine; using Logger = Cryville.Common.Logging.Logger; using unity = UnityEngine; namespace Cryville.Crtr { public static class Game { public static string GameDataPath { get; private set; } public static string UnityDataPath { get; private set; } public static IAudioDeviceManager AudioManager; public static IAudioDevice AudioDevice; public static AudioClient AudioClient; public static SimpleSequencerSource AudioSequencer; public static SimpleSequencerSession AudioSession; public static InputManager InputManager; public static readonly NetworkTaskWorker NetworkTaskWorker = new(); public static readonly JsonSerializerSettings GlobalJsonSerializerSettings = new() { DefaultValueHandling = DefaultValueHandling.Ignore, }; public static Logger MainLogger { get; private set; } static FileStream _logFileStream; static StreamLoggerListener _logWriter; static bool _init; public static void Init() { if (_init) return; _init = true; bool _bcflag = new Version(Settings.Default.LastRunVersion) < new Version("0.4"); if (_bcflag) Settings.Default.Reset(); GameDataPath = Settings.Default.GameDataPath; UnityDataPath = Application.dataPath; var logPath = Directory.CreateDirectory(Path.Combine(GameDataPath, "logs")); _logFileStream = new FileStream( Path.Combine( logPath.FullName, string.Format( CultureInfo.InvariantCulture, "{0}.log", (int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds ) ), FileMode.Create, FileAccess.Write, FileShare.Read ); _logWriter = new StreamLoggerListener(_logFileStream) { AutoFlush = true }; MainLogger = new Logger(); var listener = new InstantLoggerListener(); listener.Log += MainLogger.Log; Common.Shared.Logger.AddListener(listener); Input.Shared.Logger.AddListener(listener); MainLogger.AddListener(_logWriter); Application.logMessageReceivedThreaded += OnInternalLog; MainLogger.Log(1, "Game", "Game Version: {0}", Application.version); MainLogger.Log(1, "Game", "Unity Version: {0}", Application.unityVersion); MainLogger.Log(1, "Game", "Operating System: {0}, Unity = {1}, Family = {2}", Environment.OSVersion, SystemInfo.operatingSystem, SystemInfo.operatingSystemFamily); MainLogger.Log(1, "Game", "Platform: Build = {0}, Unity = {1}", PlatformConfig.Name, Application.platform); MainLogger.Log(1, "Game", "Culture: {0}, UI = {1}, System = {2}, Unity = {3}", CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture, CultureInfo.InstalledUICulture, Application.systemLanguage); MainLogger.Log(1, "Game", "Device: Model = {0}, Type = {1}", SystemInfo.deviceModel, SystemInfo.deviceType); MainLogger.Log(1, "Game", "Graphics: Name = {0}, Type = {1}, Vendor = {2}, Version = {3}", SystemInfo.graphicsDeviceName, SystemInfo.graphicsDeviceType, SystemInfo.graphicsDeviceVendor, SystemInfo.graphicsDeviceVersion); MainLogger.Log(1, "Game", "Processor: Count = {0}, Frequency = {1}MHz, Type = {2}", SystemInfo.processorCount, SystemInfo.processorFrequency, SystemInfo.processorType); if (_bcflag) MainLogger.Log(2, "Game", "Reset all settings"); #if UNITY_ANDROID && !UNITY_EDITOR JavaVMManager.Register(JniInvoke.Instance); #endif unity::Input.simulateMouseWithTouches = false; var emptyObjectArray = new object[0]; #if UNITY_ANDROID && !UNITY_EDITOR InputManager.HandlerRegistries.Add(typeof(AndroidAccelerometerHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidAccelerometerUncalibratedHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidGameRotationVectorHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidGravityHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidGyroscopeHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidLinearAccelerationHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidMagneticFieldHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidMagneticFieldUncalibratedHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidRotationVectorHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(AndroidTouchHandler), emptyObjectArray); #endif InputManager.HandlerRegistries.Add(typeof(UnityGuiInputHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(UnityGuiInputHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(UnityMouseHandler), emptyObjectArray); InputManager.HandlerRegistries.Add(typeof(UnityTouchHandler), emptyObjectArray); InputManager = new InputManager(); #if UNITY_EDITOR_WIN ffmpeg.RootPath = Path.Combine(Application.dataPath, "Plugins", "Windows"); #elif UNITY_STANDALONE_WIN ffmpeg.RootPath = Path.Combine(Application.dataPath, "Plugins", "x86_64"); #elif UNITY_ANDROID ffmpeg.RootPath = ""; #else #error No FFmpeg search path. #endif #if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN EngineBuilder.Engines.Add(typeof(Audio.Wasapi.MMDeviceEnumeratorWrapper)); EngineBuilder.Engines.Add(typeof(Audio.WaveformAudio.WaveDeviceManager)); #elif UNITY_ANDROID EngineBuilder.Engines.Add(typeof(Audio.AAudio.AAudioManager)); EngineBuilder.Engines.Add(typeof(Audio.OpenSLES.Engine)); #else #error No audio engine defined. #endif while (true) { try { AudioManager = EngineBuilder.Create(); if (AudioManager == null) { Dialog.Show(null, "Fatal error: Cannot initialize audio engine"); MainLogger.Log(5, "Audio", "Cannot initialize audio engine"); } else { MainLogger.Log(1, "Audio", "Using audio API: {0}", AudioManager.GetType().Namespace); AudioDevice = AudioManager.GetDefaultDevice(DataFlow.Out); AudioClient = AudioDevice.Connect(AudioDevice.DefaultFormat, AudioDevice.MinimumBufferSize + AudioDevice.BurstSize); MainLogger.Log( 1, "Audio", "Audio Output = {{ Name = \"{0}\", BurstSize = {1}, Format = {2}, BufferSize = {3} }}", AudioDevice.Name, AudioDevice.BurstSize, AudioClient.Format, AudioClient.BufferSize ); AudioClient.Source = AudioSequencer = new SimpleSequencerSource(); AudioSession = AudioSequencer.NewSession(); AudioSequencer.Playing = true; AudioClient.Start(); } break; } catch (Exception ex) { Dialog.Show(null, "An error occurred while trying to initialize the recommended audio engine\nTrying to use fallback audio engines"); MainLogger.Log(4, "Audio", "An error occurred when initializing the audio engine: {0}", ex); MainLogger.Log(2, "Audio", "Trying to use fallback audio engines"); EngineBuilder.Engines.Remove(AudioManager.GetType()); } } var dir = new DirectoryInfo(Path.Combine(Settings.Default.GameDataPath, "charts")); if (!dir.Exists || Settings.Default.LastRunVersion != Application.version) { Directory.CreateDirectory(dir.FullName); var defaultData = Resources.Load("default"); using var zip = ZipFile.Read(defaultData.bytes); zip.ExtractExistingFile = ExtractExistingFileAction.OverwriteSilently; zip.ExtractAll(Settings.Default.GameDataPath); } Settings.Default.LastRunVersion = Application.version; Settings.Default.Save(); MainLogger.Log(1, "UI", "Initializing font manager"); foreach (var res in Resources.LoadAll("cldr/common/validity")) { IdValidity.Load(LoadXmlDocument(res)); } var metadata = new SupplementalMetadata(LoadXmlDocument("cldr/common/supplemental/supplementalMetadata")); var subtags = new LikelySubtags(LoadXmlDocument("cldr/common/supplemental/likelySubtags"), metadata); var matcher = new LanguageMatching(LoadXmlDocument("cldr/common/supplemental/languageInfo"), subtags); TMPAutoFont.FontMatcher = new FallbackListFontMatcher(matcher, PlatformConfig.FontManager) { MapScriptToTypefaces = PlatformConfig.ScriptFontMap }; TMPAutoFont.DefaultShader = Resources.Load(PlatformConfig.TextShader); MainLogger.Log(1, "Game", "Initialized"); } static readonly Encoding _encoding = new UTF8Encoding(false, true); static readonly XmlReaderSettings _xmlSettings = new() { DtdProcessing = DtdProcessing.Ignore, }; static XDocument LoadXmlDocument(string path) { return LoadXmlDocument(Resources.Load(path)); } static XDocument LoadXmlDocument(TextAsset asset) { using var stream = new MemoryStream(_encoding.GetBytes(asset.text)); using var reader = XmlReader.Create(stream, _xmlSettings); return XDocument.Load(reader); } static bool _shutdown; public static void Shutdown() { if (_shutdown) return; _shutdown = true; MainLogger.Log(1, "Game", "Shutting down"); try { AudioClient.Dispose(); AudioSequencer.Dispose(); AudioDevice.Dispose(); AudioManager.Dispose(); } catch (Exception ex) { LogException("Game", "An error occurred while shutting down", ex); } finally { _logWriter.Dispose(); _logFileStream.Dispose(); } } public static void LogException(string module, string prefix, Exception ex) { MainLogger.Log(4, module, "{0}: {1}", prefix, ex); } static void OnInternalLog(string condition, string stackTrace, LogType type) { var l = type switch { LogType.Log => 1, LogType.Assert => 2, LogType.Warning => 3, LogType.Error or LogType.Exception => 4, _ => 1, }; MainLogger.Log(l, "Internal", "{0}\n{1}", condition, stackTrace); } public static void SuspendBackgroundTasks() { NetworkTaskWorker.SuspendBackgroundTasks(); Dialog.Suppress(); } public static void ResumeBackgroundTasks() { Dialog.Release(); NetworkTaskWorker.ResumeBackgroundTasks(); } } }