diff --git a/BSIPA-ModList/BSIPA-ModList.csproj b/BSIPA-ModList/BSIPA-ModList.csproj index e8e42f88..ec6091f0 100644 --- a/BSIPA-ModList/BSIPA-ModList.csproj +++ b/BSIPA-ModList/BSIPA-ModList.csproj @@ -54,6 +54,9 @@ ..\Refs\UnityEngine.dll + + ..\Refs\UnityEngine.AssetBundleModule.dll + ..\Refs\UnityEngine.CoreModule.dll False @@ -116,6 +119,7 @@ + diff --git a/BSIPA-ModList/Bundles/consolas b/BSIPA-ModList/Bundles/consolas new file mode 100644 index 00000000..13341231 Binary files /dev/null and b/BSIPA-ModList/Bundles/consolas differ diff --git a/BSIPA-ModList/UI/ViewControllers/MarkdownView.cs b/BSIPA-ModList/UI/ViewControllers/MarkdownView.cs index 90cece20..29ee25c7 100644 --- a/BSIPA-ModList/UI/ViewControllers/MarkdownView.cs +++ b/BSIPA-ModList/UI/ViewControllers/MarkdownView.cs @@ -10,6 +10,10 @@ using UnityEngine.UI; using TMPro; using CustomUI.BeatSaber; using IPA.Utilities; +using System.Reflection; +using UnityEngine.EventSystems; +using System.Diagnostics; +using System.Collections; namespace BSIPA_ModList.UI.ViewControllers { @@ -70,8 +74,50 @@ namespace BSIPA_ModList.UI.ViewControllers return arg; } + private static string GetLinkUri(string uri) + { + if (uri[0] == '!') + { + Logger.md.Error($"Cannot link to embedded resource in mod description"); + return null; + } + else + return uri.Substring(3); + } + + private static AssetBundle _bundle; + private static AssetBundle Bundle + { + get + { + if (_bundle == null) + _bundle = AssetBundle.LoadFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas")); + return _bundle; + } + } + private static TMP_FontAsset _consolas; + private static TMP_FontAsset Consolas + { + get + { + if (_consolas == null) + { + _consolas = Bundle?.LoadAsset("CONSOLAS"); + if (_consolas != null) + { + _consolas.material.color = new Color(1f, 1f, 1f, 0f); + _consolas.material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None; + } + } + return _consolas; + } + } + protected void Awake() { + if (Consolas == null) + Logger.md.Error($"Loading of Consolas font failed"); + gameObject.SetActive(false); var vpgo = new GameObject("Viewport"); @@ -173,21 +219,20 @@ namespace BSIPA_ModList.UI.ViewControllers } #endif if (mdDirty) - UpdateMd(); - else if (resetContentPosition) - { - resetContentPosition = false; - scrView.Setup(); + StartCoroutine(UpdateMd()); + } - // this is the bullshit I have to use to make it work properly - content.gameObject.GetComponent().enabled = false; - var childRt = content.GetChild(0) as RectTransform; - childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y); - } + [Flags] + private enum CurrentTextFlags + { + None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8, } + private const string LinkDefaultColor = "#0061ff"; + private const string LinkHoverColor = "#009dff"; + private bool resetContentPosition = false; - private void UpdateMd() + private IEnumerator UpdateMd() { mdDirty = false; Clear(); @@ -200,6 +245,8 @@ namespace BSIPA_ModList.UI.ViewControllers Stack layout = new Stack(); layout.Push(content); TextMeshProUGUI currentText = null; + List texts = new List(); + CurrentTextFlags textFlags = 0; foreach (var node in doc.AsEnumerable()) { Logger.md.Debug($"node {node}"); @@ -365,49 +412,192 @@ namespace BSIPA_ModList.UI.ViewControllers { // inline element var inl = node.Inline; + void Flag(CurrentTextFlags flag) + { + if (node.IsOpening) + textFlags |= flag; + else if (node.IsClosing) + textFlags &= ~flag; + } + const float PSize = 3.5f; const float H1Size = 4.8f; const float HLevelDecrease = 0.5f; - switch (inl.Tag) + void EnsureText() { - case InlineTag.String: - if (currentText == null) + if (currentText == null) + { + Logger.md.Debug($"Adding new text element"); + + var tt = layout.Peek().gameObject.GetComponent(); + currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero); + currentText.gameObject.AddComponent(); + + /*if (Consolas != null) { - Logger.md.Debug($"Adding new text element"); - - var tt = layout.Peek().gameObject.GetComponent(); - currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero); - //var le = currentText.gameObject.AddComponent(); - - switch (tt.Tag) - { - case BlockTag.List: - case BlockTag.ListItem: - case BlockTag.Paragraph: - currentText.fontSize = PSize; - currentText.enableWordWrapping = true; - break; - case BlockTag.AtxHeading: - var size = H1Size; - size -= HLevelDecrease * (tt.hData.Level - 1); - currentText.fontSize = size; - currentText.enableWordWrapping = true; - break; - case BlockTag.SetextHeading: - currentText.fontSize = H1Size; - currentText.enableWordWrapping = true; - break; + // Set the font to Consolas so code blocks work + currentText.font = Instantiate(Consolas); + currentText.text = $""; + }*/ + + switch (tt.Tag) + { + case BlockTag.List: + case BlockTag.ListItem: + case BlockTag.Paragraph: + currentText.fontSize = PSize; + currentText.enableWordWrapping = true; + break; + case BlockTag.AtxHeading: + var size = H1Size; + size -= HLevelDecrease * (tt.hData.Level - 1); + currentText.fontSize = size; + currentText.enableWordWrapping = true; + break; + case BlockTag.SetextHeading: + currentText.fontSize = H1Size; + currentText.enableWordWrapping = true; + break; // TODO: add other relevant types - } } - Logger.md.Debug($"Appending '{inl.LiteralContent}' to current element"); - currentText.text += inl.LiteralContent; + + texts.Add(currentText); + } + } + switch (inl.Tag) + { + case InlineTag.String: + EnsureText(); + + string head = "", tail = ""; + if (textFlags.HasFlag(CurrentTextFlags.Bold)) + { head = "" + head; tail += ""; } + if (textFlags.HasFlag(CurrentTextFlags.Italic)) + { head = "" + head; tail += ""; } + if (textFlags.HasFlag(CurrentTextFlags.Strikethrough)) + { head = "" + head; tail += ""; } + if (textFlags.HasFlag(CurrentTextFlags.Underline)) + { head = "" + head; tail += ""; } + + currentText.text += head + inl.LiteralContent + tail; + break; + case InlineTag.Strong: + Flag(CurrentTextFlags.Bold); + break; + case InlineTag.Strikethrough: + Flag(CurrentTextFlags.Strikethrough); + break; + case InlineTag.Emphasis: + Flag(CurrentTextFlags.Italic); + break; + case InlineTag.Code: + EnsureText(); + currentText.text += $"{inl.LiteralContent}"; + break; + case InlineTag.Link: + EnsureText(); + Flag(CurrentTextFlags.Underline); + if (node.IsOpening) + currentText.text += $""; + else if (node.IsClosing) + currentText.text += ""; break; } } } - resetContentPosition = true; + yield return null; // delay one frame + + scrView.Setup(); + + // this is the bullshit I have to use to make it work properly + content.gameObject.GetComponent().enabled = false; + var childRt = content.GetChild(0) as RectTransform; + childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y); + + if (Consolas != null) + { + foreach (var link in texts.Select(t => t.textInfo.linkInfo).Aggregate>(Enumerable.Concat).Where(l => l.GetLinkID() == "$$codeBlock")) + { + //link.textComponent.font = Consolas; + var texinfo = link.textComponent.textInfo; + texinfo.characterInfo[link.linkTextfirstCharacterIndex].DebugPrintTo(Logger.md.Debug, 2); + for (int i = link.linkTextfirstCharacterIndex; i < link.linkTextfirstCharacterIndex + link.linkTextLength; i++) + { + + texinfo.characterInfo[i].fontAsset = Consolas; + texinfo.characterInfo[i].material = Consolas.material; + texinfo.characterInfo[i].isUsingAlternateTypeface = true; + } + } + foreach (var text in texts) + { + text.SetLayoutDirty(); + text.SetVerticesDirty(); + } + } + } + + private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler + { + private TextMeshProUGUI tmp; + + public void Awake() + { + tmp = GetComponent(); + } + + public void OnPointerClick(PointerEventData eventData) + { + // this may not actually get me what i want + int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null); + if (linkIndex != -1) + { // was a link clicked? + TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex]; + + // open the link id as a url, which is the metadata we added in the text field + var qualifiedUrl = linkInfo.GetLinkID(); + if (qualifiedUrl.StartsWith("$$")) + return; // this means its used for something else + + Logger.md.Debug($"Link pressed {qualifiedUrl}"); + + var uri = GetLinkUri(qualifiedUrl); + if (uri != null) + Process.Start(uri); + } + } + + private List SetLinkToColor(int linkIndex, Color32 color) + { + TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex]; + + var oldVertColors = new List(); // store the old character colors + + for (int i = 0; i < linkInfo.linkTextLength; i++) + { // for each character in the link string + int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text + var charInfo = tmp.textInfo.characterInfo[characterIndex]; + int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character. + int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character. + + Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character + oldVertColors.Add(vertexColors.ToArray()); + + if (charInfo.isVisible) + { + vertexColors[vertexIndex + 0] = color; + vertexColors[vertexIndex + 1] = color; + vertexColors[vertexIndex + 2] = color; + vertexColors[vertexIndex + 3] = color; + } + } + + // Update Geometry + tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All); + + return oldVertColors; + } } private void Clear() diff --git a/BSIPA-ModList/Utilities.cs b/BSIPA-ModList/Utilities.cs index e576a787..b3186202 100644 --- a/BSIPA-ModList/Utilities.cs +++ b/BSIPA-ModList/Utilities.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; +using System.Reflection; using UnityEngine; +using System.Runtime.CompilerServices; +using IPA.Utilities; namespace BSIPA_ModList { @@ -75,5 +77,91 @@ namespace BSIPA_ModList return null; } } + + public static void DebugPrintTo(this T obj, Action log, int maxDepth = -1) => + DebugPrintTo(obj?.GetType() ?? typeof(T), obj, log, "", new ConditionalWeakTable>(), maxDepth); + + private static void DebugPrintTo(Type type, object obj, Action log, string indent, ConditionalWeakTable> table, int maxDepth) + { + if (maxDepth == 0) + { + log(indent + ""); + return; + } + + if (obj == null) + { + log(indent + "null"); + return; + } + + table.Add(obj, true); + + if (type.IsPrimitive) + { + log(indent + obj.ToString()); + return; + } + if (type.IsEnum) + { + log(indent + obj.ToString()); + return; + } + if (type == typeof(string)) + { + log(indent + $"\"{obj.ToString()}\""); + return; + } + if (type.IsArray) + { + log(indent + $"{type.GetElementType()} ["); + foreach (var o in obj as Array) + { + if (type.GetElementType().IsPrimitive) + log(indent + "- " + o?.ToString() ?? "null"); + else if (type.GetElementType().IsEnum) + log(indent + "- " + o?.ToString() ?? "null"); + else if (type.GetElementType() == typeof(string)) + log(indent + "- " + $"\"{o?.ToString()}\""); + else + { + log(indent + $"- {o?.GetType()?.ToString() ?? "null"}"); + if (o != null) + { + if (!table.TryGetValue(o, out _)) + DebugPrintTo(o.GetType(), o, log, indent + " ", table, maxDepth - 1); + else + log(indent + " "); + } + } + } + log(indent + "]"); + return; + } + + var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (var field in fields) + { + var value = field.GetValue(obj); + + if (field.FieldType.IsPrimitive) + log(indent + field.Name + ": " + value?.ToString() ?? "null"); + else if (field.FieldType.IsEnum) + log(indent + field.Name + ": " + value?.ToString() ?? "null"); + else if (field.FieldType == typeof(string)) + log(indent + field.Name + ": " + $"\"{value?.ToString()}\""); + else + { + log(indent + field.Name + ": " + value?.GetType()?.ToString() ?? "null"); + if (value != null) + { + if (!table.TryGetValue(value, out _)) + DebugPrintTo(value?.GetType() ?? field.FieldType, value, log, indent + " ", table, maxDepth - 1); + else + log(indent + " "); + } + } + } + } } } diff --git a/BSIPA-ModList/manifest.json b/BSIPA-ModList/manifest.json index a1ce5b51..c020806e 100644 --- a/BSIPA-ModList/manifest.json +++ b/BSIPA-ModList/manifest.json @@ -9,19 +9,19 @@ "", "***", "", - "Look, ma! Markdown! Its **[CommonMark](w::https://commonmark.org/)**!", + "Look, `ma`! Markdown! Its **[CommonMark](w::https://commonmark.org/)**!", "", "# H1 type 2 test", "", "***", "", - "and many lines to come!", - "", - "and many lines to come!", - "", - "and many lines to come!", - "", - "and many lines to come!", + ">and many lines to come!", + ">", + ">and many lines to come!", + ">", + ">and many lines to come!", + ">", + ">and many lines to come!", ], "gameVersion": "0.13.2", "id": "BSIPA Mod List", diff --git a/Refs/UnityEngine.AssetBundleModule.dll b/Refs/UnityEngine.AssetBundleModule.dll new file mode 100644 index 00000000..433c7c35 Binary files /dev/null and b/Refs/UnityEngine.AssetBundleModule.dll differ diff --git a/Refs/UnityEngine.AssetBundleModule.xml b/Refs/UnityEngine.AssetBundleModule.xml new file mode 100644 index 00000000..e566954d --- /dev/null +++ b/Refs/UnityEngine.AssetBundleModule.xml @@ -0,0 +1,492 @@ + + + + + UnityEngine.AssetBundleModule + + + + AssetBundles let you stream additional assets via the UnityWebRequest class and instantiate them at runtime. AssetBundles are created via BuildPipeline.BuildAssetBundle. + + + + + Return true if the AssetBundle is a streamed Scene AssetBundle. + + + + + Check if an AssetBundle contains a specific object. + + + + + + Return all asset names in the AssetBundle. + + + + + To use when you need to get a list of all the currently loaded Asset Bundles. + + + Returns IEnumerable<AssetBundle> of all currently loaded Asset Bundles. + + + + + Return all the Scene asset paths (paths to *.unity assets) in the AssetBundle. + + + + + Loads all assets contained in the asset bundle that inherit from type. + + + + + + Loads all assets contained in the asset bundle. + + + + + Loads all assets contained in the asset bundle that inherit from type T. + + + + + Loads all assets contained in the asset bundle asynchronously. + + + + + Loads all assets contained in the asset bundle that inherit from T asynchronously. + + + + + Loads all assets contained in the asset bundle that inherit from type asynchronously. + + + + + + Loads asset with name from the bundle. + + + + + + Loads asset with name of a given type from the bundle. + + + + + + + Loads asset with name of type T from the bundle. + + + + + + Asynchronously loads asset with name from the bundle. + + + + + + Asynchronously loads asset with name of a given T from the bundle. + + + + + + Asynchronously loads asset with name of a given type from the bundle. + + + + + + + Loads asset and sub assets with name from the bundle. + + + + + + Loads asset and sub assets with name of a given type from the bundle. + + + + + + + Loads asset and sub assets with name of type T from the bundle. + + + + + + Loads asset with sub assets with name from the bundle asynchronously. + + + + + + Loads asset with sub assets with name of type T from the bundle asynchronously. + + + + + + Loads asset with sub assets with name of a given type from the bundle asynchronously. + + + + + + + Synchronously loads an AssetBundle from a file on disk. + + Path of the file on disk. + An optional CRC-32 checksum of the uncompressed content. If this is non-zero, then the content will be compared against the checksum before loading it, and give an error if it does not match. + An optional byte offset. This value specifies where to start reading the AssetBundle from. + + Loaded AssetBundle object or null if failed. + + + + + Synchronously loads an AssetBundle from a file on disk. + + Path of the file on disk. + An optional CRC-32 checksum of the uncompressed content. If this is non-zero, then the content will be compared against the checksum before loading it, and give an error if it does not match. + An optional byte offset. This value specifies where to start reading the AssetBundle from. + + Loaded AssetBundle object or null if failed. + + + + + Asynchronously loads an AssetBundle from a file on disk. + + Path of the file on disk. + An optional CRC-32 checksum of the uncompressed content. If this is non-zero, then the content will be compared against the checksum before loading it, and give an error if it does not match. + An optional byte offset. This value specifies where to start reading the AssetBundle from. + + Asynchronous create request for an AssetBundle. Use AssetBundleCreateRequest.assetBundle property to get an AssetBundle once it is loaded. + + + + + Synchronously create an AssetBundle from a memory region. + + Array of bytes with the AssetBundle data. + An optional CRC-32 checksum of the uncompressed content. If this is non-zero, then the content will be compared against the checksum before loading it, and give an error if it does not match. + + Loaded AssetBundle object or null if failed. + + + + + Asynchronously create an AssetBundle from a memory region. + + Array of bytes with the AssetBundle data. + An optional CRC-32 checksum of the uncompressed content. If this is non-zero, then the content will be compared against the checksum before loading it, and give an error if it does not match. + + Asynchronous create request for an AssetBundle. Use AssetBundleCreateRequest.assetBundle property to get an AssetBundle once it is loaded. + + + + + Synchronously loads an AssetBundle from a managed Stream. + + The managed Stream object. Unity calls Read(), Seek() and the Length property on this object to load the AssetBundle data. + An optional CRC-32 checksum of the uncompressed content. + You can use this to override the size of the read buffer Unity uses while loading data. The default size is 32KB. + + The loaded AssetBundle object or null when the object fails to load. + + + + + Asynchronously loads an AssetBundle from a managed Stream. + + The managed Stream object. Unity calls Read(), Seek() and the Length property on this object to load the AssetBundle data. + An optional CRC-32 checksum of the uncompressed content. + You can use this to override the size of the read buffer Unity uses while loading data. The default size is 32KB. + + Asynchronous create request for an AssetBundle. Use AssetBundleCreateRequest.assetBundle property to get an AssetBundle once it is loaded. + + + + + Asynchronously recompress a downloaded/stored AssetBundle from one BuildCompression to another. + + Path to the AssetBundle to recompress. + Path to the recompressed AssetBundle to be generated. Can be the same as inputPath. + The compression method, level and blocksize to use during recompression. Only some BuildCompression types are supported (see note). + CRC of the AssetBundle to test against. Testing this requires additional file reading and computation. Pass in 0 to skip this check. + The priority at which the recompression operation should run. This sets thread priority during the operation and does not effect the order in which operations are performed. Recompression operations run on a background worker thread. + + + + Unloads all assets in the bundle. + + + + + + Unloads all currently loaded Asset Bundles. + + Determines whether the current instances of objects loaded from Asset Bundles will also be unloaded. + + + + Asynchronous create request for an AssetBundle. + + + + + Asset object being loaded (Read Only). + + + + + The result of an Asset Bundle Load or Recompress Operation. + + + + + The Asset Bundle is already loaded. + + + + + The operation was cancelled. + + + + + The Asset Bundle was not successfully cached. + + + + + Failed to decompress the Asset Bundle. + + + + + The target path given for the Recompression operation could not be deleted for swap with recompressed bundle file. + + + + + Failed to read the Asset Bundle file. + + + + + Failed to write to the file system. + + + + + The Asset Bundle does not contain any serialized data. It may be empty, or corrupt. + + + + + The AssetBundle is incompatible with this version of Unity. + + + + + The decompressed Asset data did not match the precomputed CRC. This may suggest that the AssetBundle did not download correctly. + + + + + This does not appear to be a valid Asset Bundle. + + + + + The target path given for the Recompression operation exists but is not an Archive container. + + + + + The target path given for the Recompression operation is an Archive that is currently loaded. + + + + + The operation completed successfully. + + + + + Manifest for all the AssetBundles in the build. + + + + + Get all the AssetBundles in the manifest. + + + An array of asset bundle names. + + + + + Get all the AssetBundles with variant in the manifest. + + + An array of asset bundle names. + + + + + Get all the dependent AssetBundles for the given AssetBundle. + + Name of the asset bundle. + + + + Get the hash for the given AssetBundle. + + Name of the asset bundle. + + The 128-bit hash for the asset bundle. + + + + + Get the direct dependent AssetBundles for the given AssetBundle. + + Name of the asset bundle. + + Array of asset bundle names this asset bundle depends on. + + + + + Asynchronous AssetBundle recompression from one compression method and level to another. + + + + + A string describing the recompression operation result (Read Only). + + + + + Path of the AssetBundle being recompressed (Read Only). + + + + + Path of the resulting recompressed AssetBundle (Read Only). + + + + + Result of the recompression operation. + + + + + True if the recompress operation is complete and was successful, otherwise false (Read Only). + + + + + Asynchronous load request from an AssetBundle. + + + + + Asset objects with sub assets being loaded. (Read Only) + + + + + Asset object being loaded (Read Only). + + + + + Contains information about compression methods, compression levels and block sizes that are supported by Asset Bundle compression at build time and recompression at runtime. + + + + + LZ4HC "Chunk Based" Compression. + + + + + LZ4 Compression for runtime recompression. + + + + + LZMA Compression. + + + + + Uncompressed Asset Bundle. + + + + + Uncompressed Asset Bundle. + + + + + Compression Levels relate to how much time should be spent compressing Assets into an Asset Bundle. + + + + + No compression. + + + + + Compression Method for Asset Bundles. + + + + + LZ4 compression results in larger compressed files than LZMA, but does not require the entire bundle to be decompressed before use. + + + + + LZ4HC is a high compression variant of LZ4. LZ4HC compression results in larger compressed files than LZMA, but does not require the entire bundle to be decompressed before use. + + + + + LZMA compression results in smaller compressed Asset Bundles but they must be entirely decompressed before use. + + + + + Uncompressed Asset Bundles are larger than compressed Asset Bundles, but they are the fastest to access once downloaded. + + + + + The AssetBundle module implements the AssetBundle class and related APIs to load data from AssetBundles. + + + + diff --git a/Refs/refs.txt b/Refs/refs.txt index 685b5628..3c9fd3aa 100644 --- a/Refs/refs.txt +++ b/Refs/refs.txt @@ -15,6 +15,9 @@ """"TextRenderingModule. """""dll """""xml +""""AssetBundleModule. +"""""dll +"""""xml """"UI """"".dll """""Module.