using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using MU3.CustomUI; using MU3.Game; using MU3.User; using MU3.Util; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; namespace Inohara; public class Exporter: SingletonMonoBehaviour { private struct Config { public bool Enable; public int Timeout; public string BaseUrl; public string StatusPoint; public string ImportPoint; public bool EnableText; public bool ExportPBs; } static readonly float REQ2_DELAY = 4f; static readonly float DRAW_DURATION = 4f; protected new bool _dontDestroyOnLoad = true; private Config _cfg; private readonly Dictionary _tokens = new(); private string _currToken = ""; private readonly Font _arial = Resources.GetBuiltinResource("Arial.ttf"); private List _scores = new(); private void Log(object o, bool drawText) { Debug.Log("[Inohara] " + o.ToString()); if(_cfg.EnableText && drawText) { StartCoroutine(DrawMessage(o.ToString(), new Color(1f, 1f, 1f, 1.0f))); } } private void LogSuccess(object o) { Debug.Log("[Inohara] " + o.ToString()); if(_cfg.EnableText) { StartCoroutine(DrawMessage(o.ToString(), new Color(0f, 0.83f, 0.14f, 1.0f))); } } private void LogError(object o) { Debug.LogError("[Inohara] " + o.ToString()); if(_cfg.EnableText) { StartCoroutine(DrawMessage(o.ToString(), new Color(1f, 0.42f, 0.42f, 1.0f))); } } public void LoadCfg() { _cfg.Enable = false; try { using StreamReader reader = new(Path.Combine(Application.dataPath, "../inohara.cfg")); Dictionary options = new(); Dictionary dict = null; string line; while ((line = reader.ReadLine()) != null) { line = line.Split(new char[] { '#' }, 2)[0]; if(line.ToLower() == "[keys]") { dict = _tokens; } else if(line.ToLower() == "[options]") { dict = options; } else if(line.StartsWith("[")) { dict = null; } else if(dict != null) { string[] tokens = line.Split(new char[] { '=' }, 2); if(tokens.Length == 2) { dict.Add(tokens[0].Trim().ToLower(), tokens[1].Trim()); } } } if(!options.ContainsKey("enable") || !bool.TryParse(options["enable"], out _cfg.Enable)) { _cfg.Enable = false; } if(!options.ContainsKey("enableosd") || !bool.TryParse(options["enableosd"], out _cfg.EnableText)) { _cfg.EnableText = false; } if(!options.ContainsKey("exportpbs") || !bool.TryParse(options["exportpbs"], out _cfg.ExportPBs)) { _cfg.ExportPBs = false; } if(!options.ContainsKey("timeout") || !int.TryParse(options["timeout"], out _cfg.Timeout)) { _cfg.Timeout = 3; } if(!options.TryGetValue("baseurl", out _cfg.BaseUrl)) { LogError("Config error: missing option BaseUrl"); _cfg.Enable = false; } if(!options.TryGetValue("status", out _cfg.StatusPoint)) { _cfg.StatusPoint = "/api/v1/status"; } if(!options.TryGetValue("import", out _cfg.ImportPoint)) { _cfg.ImportPoint = "/ir/direct-manual/import"; } } catch (IOException e) { LogError("Config loading failed: " + e); _cfg.Enable = false; } if(_cfg.Enable) { Log("Score submissions enabled", true); } else { Log("Score submissions disabled", true); } } public void Authorize() { if(!_cfg.Enable) { return; } var username = Singleton.instance.UserName .Normalize(NormalizationForm.FormKC) .ToLower(); _currToken = ""; _scores.Clear(); string tmpBearer; if(!_tokens.ContainsKey(username) && !_tokens.ContainsKey("*")) { LogError("User " + username + " not found"); return; } else { if(!_tokens.TryGetValue(username, out tmpBearer)) { tmpBearer = _tokens["*"]; } } StartCoroutine(AuthorizeReq(tmpBearer)); } private IEnumerator AuthorizeReq(string tmpBearer) { using var req = new UnityWebRequest(_cfg.BaseUrl + _cfg.StatusPoint, "GET"); req.SetRequestHeader("Authorization", "Bearer " + tmpBearer); req.downloadHandler = new DownloadHandlerBuffer(); req.timeout = _cfg.Timeout; yield return req.Send(); try { if(req.responseCode == 200) { var res = JsonUtility.FromJson(req.downloadHandler.text); if(res.success == true) { if(res.body.permissions.Contains("submit_score")) { LogSuccess("Logged in"); Log(string.Format("Tachi {0} (user id={1})", res.body.version, res.body.whoami), false); _currToken = tmpBearer; } else { LogError("Missing the submit_score permission"); } } else { LogError("Unable to log into Tachi"); } } } catch(Exception e) { LogError("Unable to log into Tachi: " + e); } } public void Export(BattleResult result, SessionInfo info) { if(_currToken == "" || !_cfg.Enable) { return; } StartCoroutine(ExportReq(result, info)); } private IEnumerator ExportReq(BattleResult result, SessionInfo info) { using var req = new UnityWebRequest(_cfg.BaseUrl + _cfg.ImportPoint, "POST"); req.timeout = _cfg.Timeout; req.SetRequestHeader("Authorization", "Bearer " + _currToken); req.SetRequestHeader("Content-Type", "application/json"); _scores.Add(Util.CreateScore(result, info)); var batch = Util.CreateBatch(_scores, "inohara"); byte[] jsonToSend = new UTF8Encoding().GetBytes(batch); req.uploadHandler = new UploadHandlerRaw(jsonToSend); req.downloadHandler = new DownloadHandlerBuffer(); Log("Uploading " + _scores.Count + " score(s)", false); yield return req.Send(); if(req.responseCode == 200) { LogSuccess("Upload successful"); Log("Warning: Non-deferred DIRECT-MANUAL is untested", false); _scores.Clear(); } else if (req.responseCode == 202) { var res = JsonUtility.FromJson(req.downloadHandler.text); if(res.success == true) { StartCoroutine(ExportReq2(res.body.url)); } else { LogError("Upload failed: " + res.body); } } else { LogError(string.Format("Upload failed ({0}): {1}", req.responseCode, req.error)); } } private IEnumerator ExportReq2(string url) { yield return new WaitForSeconds(REQ2_DELAY); using var req = new UnityWebRequest(url, "GET"); req.downloadHandler = new DownloadHandlerBuffer(); yield return req.Send(); if(req.responseCode == 200) { var res = JsonUtility.FromJson(req.downloadHandler.text); if(res.success == true) { LogSuccess("Upload successful"); _scores.Clear(); } else { LogError("Upload failed"); } } else { LogError(string.Format("Upload failed ({0}): {1}", req.responseCode, req.error)); } } public void ExportPBs() { if(!_cfg.ExportPBs) { return; } var userMusic = Singleton.instance.userMusic; var scores = new List(); foreach(var x in userMusic.Values) { foreach(var y in x.UserFumen) { if(y != null) { scores.Add(Util.CreatePB(y)); } } } var batch = Util.CreateBatch(scores, "inohara-pb"); using StreamWriter writer = new(Path.Combine(Application.dataPath, "../batch-manual.json")); writer.Write(batch); Log("Exported PBs to batch-manual.json", false); } // This is just for fun private IEnumerator DrawMessage(string message, Color color) { GameObject canvasGO = new() { name = "Canvas" }; DontDestroyOnLoad(canvasGO); canvasGO.AddComponent(); canvasGO.AddComponent(); canvasGO.AddComponent(); Canvas canvas = canvasGO.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; GameObject textGO = new(); textGO.transform.parent = canvasGO.transform; textGO.AddComponent(); var text = textGO.GetComponent(); text.font = _arial; text.text = message; text.fontSize = 20; text.color = color; text.alignment = TextAnchor.UpperCenter; RectTransform rectTransform = text.GetComponent(); rectTransform.localPosition = new Vector3(0, 0, 0); rectTransform.sizeDelta = new Vector2(1080, 1400); yield return new WaitForSeconds(DRAW_DURATION); Destroy(canvasGO); Destroy(text); } }