diff --git a/MiraAPI/Patches/RoleGuidePatches.cs b/MiraAPI/Patches/RoleGuidePatches.cs index 9d3fbc8..3bdcc4c 100644 --- a/MiraAPI/Patches/RoleGuidePatches.cs +++ b/MiraAPI/Patches/RoleGuidePatches.cs @@ -1726,22 +1726,7 @@ public static class RoleGuidePatches // ---- General (non-role) option pages: paginated General tab + cross-page search ---- private static Scroller? _generalScroller; private static GameObject? _generalNoResults; - private static readonly List GeneralRows = []; - private static GameObject? _generalRowTemplate; - private static Vector3 _generalRowOrigin; - private static Vector3 _generalRowColumnStep; - private static float _generalRowHeight = -0.65f; // negative: rows go down - private static int _generalColumns = 2; - private static float _generalVisibleHeight; - private static bool _generalScrollerFailed; - - /// One vanilla-looking settings row (label box + value box) that is reused between pages. - private sealed class GeneralRow(GameObject root, TextMeshPro label, TextMeshPro value) - { - public GameObject Root { get; } = root; - public TextMeshPro Label { get; } = label; - public TextMeshPro Value { get; } = value; - } + private static GameObject? _generalPageObject; private static List _generalPages = []; private static int _generalPage; // 0 = vanilla settings, 1..N = _generalPages[page - 1] private static bool _generalSearching; @@ -1760,17 +1745,27 @@ public static class RoleGuidePatches { _generalScroller = null; _generalNoResults = null; - GeneralRows.Clear(); - _generalRowTemplate = null; - _generalScrollerFailed = false; + _generalPageObject = null; _generalPages = []; _generalPage = 0; _generalSearching = false; _sortButtonsRepurposed = false; } + /// + /// Clones the roles tab to make the Settings tab. Used to display Options initialized by MiraAPI mods not related to roles or modifiers. + /// private static void InitializeGeneralPanel(MatchInfoGuide instance) { + var rolesTab = instance.settingsTabs[RolesTabIndex]; + var generalTab = Object.Instantiate(rolesTab, rolesTab.transform.parent); + generalTab.name = "GeneralOptionsPanel"; + generalTab.transform.FindChild("MaskArea")?.transform.localPosition = new Vector3(-0.0184f, 0.15f, -0.1f); + generalTab.transform.GetAllChildren() + .First(x => x.name.Contains("BG_Gradient")).transform.localPosition = new Vector3(0, -0.62f, -5); + _generalScroller = generalTab.GetComponent(); + generalTab.SetActive(false); + _generalNoResults = Object.Instantiate( instance.rolesEnabledMessage, instance.rolesEnabledMessage.transform.parent, @@ -1780,111 +1775,6 @@ public static class RoleGuidePatches SetNoResultsText(_generalNoResults); } - /// - /// Clones the vanilla settings tab the first time it is needed, so extra pages are drawn with the exact same - /// rows, fonts, masking and scroll area as the vanilla list. Done lazily because the vanilla rows are only - /// created when the guide first opens. The clone is NOT added to settingsTabs; its visibility is managed - /// here so the guide's tab count and tab buttons stay untouched. - /// - private static bool EnsureGeneralScroller(MatchInfoGuide guide) - { - if (_generalScroller != null && _generalRowTemplate) - { - return true; - } - - if (_generalScrollerFailed) - { - return false; - } - - var vanillaTab = guide.settingsTabs[GeneralTabIndex]; - var vanillaScroller = vanillaTab.GetComponent() ?? vanillaTab.GetComponentInChildren(true); - if (!vanillaScroller || vanillaScroller.Inner.childCount < 2) - { - return false; // vanilla rows not built yet; try again next refresh - } - - // Read the vanilla grid: first row, second entry (same row => 2 columns), third entry (next row). - var vInner = vanillaScroller.Inner; - var p0 = vInner.GetChild(0).localPosition; - var p1 = vInner.GetChild(1).localPosition; - var sameRow = Mathf.Abs(p1.y - p0.y) < 0.01f; - var vanillaCount = vInner.childCount; - _generalColumns = sameRow ? 2 : 1; - _generalRowOrigin = p0; - _generalRowColumnStep = sameRow ? new Vector3(p1.x - p0.x, 0f, 0f) : Vector3.zero; - if (sameRow) - { - _generalRowHeight = vanillaCount >= 3 ? vInner.GetChild(2).localPosition.y - p0.y : -0.65f; - } - else - { - _generalRowHeight = p1.y - p0.y; - } - - if (Mathf.Abs(_generalRowHeight) < 0.05f) - { - _generalRowHeight = -0.65f; - } - - // Self-calibrate the visible height from how the vanilla list was bounded. - var vanillaRows = Mathf.CeilToInt(vanillaCount / (float)_generalColumns); - var visible = (vanillaRows * Mathf.Abs(_generalRowHeight)) - vanillaScroller.ContentYBounds.max; - _generalVisibleHeight = visible > 0.1f && visible < vanillaRows * Mathf.Abs(_generalRowHeight) - ? visible - : 3.3f * Mathf.Abs(_generalRowHeight); - - var clone = Object.Instantiate(vanillaTab, vanillaTab.transform.parent); - clone.name = "GeneralOptionsPanel"; - var cloneScroller = clone.GetComponent() ?? clone.GetComponentInChildren(true); - var cInner = cloneScroller.Inner; - - // Keep the first row as an inactive prototype, delete the rest. - var template = cInner.GetChild(0).gameObject; - for (var i = cInner.childCount - 1; i >= 1; i--) - { - var child = cInner.GetChild(i).gameObject; - child.SetActive(false); - child.Destroy(); - } - - template.SetActive(false); - _generalRowTemplate = template; - _generalScroller = cloneScroller; - clone.SetActive(false); - return true; - } - - private static GeneralRow? CreateGeneralRow() - { - if (_generalScroller == null || !_generalRowTemplate) - { - return null; - } - - var go = Object.Instantiate(_generalRowTemplate!, _generalScroller.Inner); - go.name = "GeneralOptionRow"; - - // The game would re-translate these and overwrite our text. - foreach (var translator in go.GetComponentsInChildren(true)) - { - translator.Destroy(); - } - - // Label is the left-most text, value is the right-most (same as the vanilla entries). - var texts = go.GetComponentsInChildren(true).OrderBy(t => t.transform.position.x).ToArray(); - if (texts.Length < 2) - { - Warning("Vanilla settings row has fewer than two text fields; cannot clone it for the general pages."); - go.Destroy(); - _generalScrollerFailed = true; - return null; - } - - return new GeneralRow(go, texts[0], texts[^1]); - } - private static void EnterGeneralTab() { if (!_uiInitialized) @@ -1983,45 +1873,29 @@ public static class RoleGuidePatches private static void ShowGeneralEntries(IReadOnlyList entries, bool showGroup, bool scrollToTop) { var guide = MatchInfoGuide.Instance; - if (!guide || !EnsureGeneralScroller(guide)) + if (_generalScroller == null || !guide || !guide.MatchInfoRolePanelPrefab) { - // Could not clone the vanilla rows: fall back to the plain vanilla tab rather than a blank page. + // Never leave the tab blank: fall back to the plain vanilla list. guide?.settingsTabs[GeneralTabIndex].SetActive(true); return; } - while (GeneralRows.Count < entries.Count) + if (_generalPageObject) { - var row = CreateGeneralRow(); - if (row == null) - { - break; - } - - GeneralRows.Add(row); + _generalPageObject!.SetActive(false); + _generalPageObject.Destroy(); + _generalPageObject = null; } - for (var i = 0; i < GeneralRows.Count; i++) + var rows = new List>(entries.Count); + foreach (var entry in entries) { - var row = GeneralRows[i]; - if (i >= entries.Count) - { - row.Root.SetActive(false); - continue; - } - - var entry = entries[i]; - row.Label.text = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; - row.Value.text = entry.Value; - row.Root.transform.localPosition = _generalRowOrigin + - (_generalRowColumnStep * (i % _generalColumns)) + - new Vector3(0f, _generalRowHeight * (i / _generalColumns), 0f); - row.Root.SetActive(true); + var label = showGroup ? $"{entry.GroupName}\n{entry.Title}" : entry.Title; + rows.Add(new KeyValuePair(label, entry.Value)); } - var rows = Mathf.CeilToInt(Mathf.Min(entries.Count, GeneralRows.Count) / (float)_generalColumns); - _generalScroller!.gameObject.SetActive(true); - _generalScroller.SetYBoundsMax(Mathf.Clamp((rows * Mathf.Abs(_generalRowHeight)) - _generalVisibleHeight, 0f, 999f)); + _generalScroller.gameObject.SetActive(true); + _generalPageObject = Helpers.CreateAdvancedSettingsPage(guide, _generalScroller, rows); _generalNoResults?.SetActive(entries.Count == 0); if (scrollToTop) { diff --git a/MiraAPI/Utilities/Helpers.cs b/MiraAPI/Utilities/Helpers.cs index 53bade5..26d68df 100644 --- a/MiraAPI/Utilities/Helpers.cs +++ b/MiraAPI/Utilities/Helpers.cs @@ -844,6 +844,119 @@ public static class Helpers return obj; } + /// + /// Creates a page of vanilla-looking settings rows (label box on the left, value box on the right, two columns) + /// inside a roles-style scroller. Use this for lists of "Name: Value" settings that are not roles or modifiers. + /// + /// The guide object. + /// The the page is parented to. + /// The rows to show: key is the label, value is the displayed value. + /// The that holds all the rows. Destroy it to remove the page. + public static GameObject CreateAdvancedSettingsPage( + MatchInfoGuide guide, + Scroller parent, + IReadOnlyList> settings) + { + const float rowHeight = 0.4f; + const float rowPitch = 0.52f; + const float contentTop = 0.95f; + const float visibleHeight = 1.5f; + const int columns = 2; + + var inner = parent.Inner; + var layout = inner.GetComponent(); + if (layout) + { + layout.enabled = false; + } + + var columnWidth = layout ? Mathf.Clamp(layout.cellSize.x, 2f, 3.2f) : 2.7f; + var columnGap = layout ? Mathf.Clamp(layout.spacing.x, 0.1f, 0.6f) : 0.3f; + var labelWidth = columnWidth * 0.62f; + var valueWidth = columnWidth * 0.30f; + var boxGap = columnWidth - labelWidth - valueWidth; + var left = -((columns * columnWidth) + ((columns - 1) * columnGap)) / 2f; + + var prefab = guide.MatchInfoRolePanelPrefab; + var vanillaFont = HudManager.Instance.TaskPanel.taskText; + + var boxMaterial = new Material(prefab.roleIcon.sharedMaterial); + boxMaterial.SetInt(PlayerMaterial.MaskLayer, 50); + var boxSprite = MiraAssets.BlankSprite.LoadAsset(); + var layer = inner.gameObject.layer; + + var page = new GameObject("AdvancedSettingsPage") { layer = layer }; + page.transform.SetParent(inner, false); + page.transform.localPosition = Vector3.zero; + + void MakeBox(Vector3 pos, Vector2 size, Color color) + { + var box = new GameObject("Box") { layer = layer }; + box.transform.SetParent(page.transform, false); + box.transform.localPosition = pos; + var sr = box.AddComponent(); + sr.sprite = boxSprite; + sr.drawMode = SpriteDrawMode.Sliced; + sr.size = size; + sr.color = color; + sr.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + sr.sharedMaterial = boxMaterial; + } + + void MakeText(string text, Vector3 pos, Vector2 size) + { + var tmp = Object.Instantiate(prefab.roleName, page.transform); + tmp.richText = true; + tmp.font = vanillaFont.font; + tmp.fontMaterial = vanillaFont.fontMaterial; + tmp.text = text; + tmp.enableAutoSizing = true; + tmp.fontSizeMin = 0.9f; + tmp.fontSizeMax = 2.2f; + tmp.fontStyle = FontStyles.Normal; + tmp.enableWordWrapping = true; + tmp.alignment = TextAlignmentOptions.Center; + tmp.color = Color.white; + tmp.rectTransform.sizeDelta = size; + tmp.transform.localPosition = pos; + tmp.EnableStencilMasking(); + } + + for (var i = 0; i < settings.Count; i++) + { + var col = i % columns; + var row = i / columns; + var originX = left + (col * (columnWidth + columnGap)); + var centerY = contentTop - (rowHeight / 2f) - (row * rowPitch); + + var labelX = originX + (labelWidth / 2f); + var valueX = originX + labelWidth + boxGap + (valueWidth / 2f); + + MakeBox( + new Vector3(labelX, centerY, 0f), + new Vector2(labelWidth, rowHeight), + new Color(0.16f, 0.16f, 0.16f, 1f)); + MakeBox(new Vector3(valueX, centerY, -0.01f), new Vector2(valueWidth, rowHeight), Color.white); + MakeBox( + new Vector3(valueX, centerY, -0.02f), + new Vector2(valueWidth - 0.04f, rowHeight - 0.04f), + Color.black); + + MakeText( + $"{settings[i].Key}", + new Vector3(labelX, centerY, -0.1f), + new Vector2(labelWidth - 0.12f, rowHeight - 0.04f)); + MakeText( + settings[i].Value, + new Vector3(valueX, centerY, -0.1f), + new Vector2(valueWidth - 0.1f, rowHeight - 0.04f)); + } + + var rows = Mathf.CeilToInt(settings.Count / (float)columns); + parent.SetYBoundsMax(Mathf.Clamp((rows * rowPitch) - visibleHeight, 0f, 999f)); + return page; + } + private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false) { panel.roleCount.text = ability.AbilityType; @@ -868,4 +981,4 @@ public static class Helpers panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f); panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f); } -} +} \ No newline at end of file