Files
BeatSaber/Assets/Editor/VRBeatSaberSceneBuilder.cs
T
whdwo798 58c88dafff feat: SongSelect panel — NAS song list with download/play in Menu.unity
- Add DownloadManager, SongLibrary, SongSelectManager, SongDetailPanel scripts
- Rebuild SongSelect panel inside Menu.unity using VRBeatsKit style:
  left scroll list (ALL/OWNED tabs) + right detail panel (diff buttons, Download/Delete/Play)
- SceneBuilder replaced: only ③ Menu — Rebuild SongSelect Panel remains
- All UI text in English

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 01:22:16 +09:00

421 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public static class VRBeatSaberSceneBuilder
{
private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.unity";
// ─────────────────────────────────────────────
// ③ Menu — Rebuild SongSelect Panel
//
// Canvas(SongSelect) size: 105.885 × 68.223
// BG child covers full canvas (stretch anchors)
// BG local coord origin = center
// X: -52.94 ~ +52.94
// Y: -34.11 ~ +34.11
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/③ Menu — Rebuild SongSelect Panel")]
public static void RebuildSongSelectPanel()
{
var scene = EditorSceneManager.OpenScene(MenuScene, OpenSceneMode.Single);
var songSelectGO = GameObject.Find("SongSelect");
if (songSelectGO == null) { Debug.LogError("[SceneBuilder] 'SongSelect' not found."); return; }
var bgTransform = songSelectGO.transform.Find("BG");
if (bgTransform == null) { Debug.LogError("[SceneBuilder] 'SongSelect/BG' not found."); return; }
// Clear BG children
for (int i = bgTransform.childCount - 1; i >= 0; i--)
Object.DestroyImmediate(bgTransform.GetChild(i).gameObject);
// Create/reuse SongSystem root GO for SongLibrary (must be root for DontDestroyOnLoad)
var sysGO = GameObject.Find("SongSystem");
if (sysGO == null) sysGO = new GameObject("SongSystem");
var oldLib = sysGO.GetComponent<SongLibrary>();
if (oldLib != null) Object.DestroyImmediate(oldLib);
var songLibrary = sysGO.AddComponent<SongLibrary>();
// Add/replace SongSelectManager + DownloadManager on SongSelect GO
var oldSSM = songSelectGO.GetComponent<SongSelectManager>();
if (oldSSM != null) Object.DestroyImmediate(oldSSM);
var oldDM = songSelectGO.GetComponent<DownloadManager>();
if (oldDM != null) Object.DestroyImmediate(oldDM);
var downloadManager = songSelectGO.AddComponent<DownloadManager>();
var songSelectManager = songSelectGO.AddComponent<SongSelectManager>();
var bg = bgTransform;
// ── Header ──────────────────────────────────────────
CreateLabel(bg, "Title", "SONG SELECT",
new Vector2(0f, 28.5f), new Vector2(100f, 9f), 8.5f,
Color.white, TextAlignmentOptions.Center);
CreateDivider(bg, "DivHeader", new Vector2(0f, 23.5f), new Vector2(104f, 0.5f));
var tabAllBtn = CreateStyledButton(bg, "TabAll", "ALL", new Vector2(-18f, 19.5f), new Vector2(30f, 7f), 5f);
var tabOwnedBtn = CreateStyledButton(bg, "TabOwned", "OWNED", new Vector2( 14f, 19.5f), new Vector2(30f, 7f), 5f);
CreateDivider(bg, "DivTabs", new Vector2(0f, 15.5f), new Vector2(104f, 0.5f));
// ── Content area: Y from 15 to -34.11, height ~49 ───
// ListPanel (left half)
var listPanelGO = new GameObject("ListPanel");
listPanelGO.transform.SetParent(bg, false);
SetRect(listPanelGO, new Vector2(-26.6f, -9.4f), new Vector2(52.7f, 49f));
// Vertical divider
CreateDivider(bg, "DivVertical", new Vector2(0.1f, -9.4f), new Vector2(0.5f, 49f));
// DetailPanel (right half, hidden until card clicked)
var detailPanelGO = new GameObject("DetailPanel");
detailPanelGO.transform.SetParent(bg, false);
SetRect(detailPanelGO, new Vector2(26.6f, -9.4f), new Vector2(52.7f, 49f));
// ── ListPanel contents ───────────────────────────────
RectTransform scrollContent;
GameObject loadingOverlay;
GameObject errorOverlay;
TMP_Text errorText;
BuildScrollList(listPanelGO.transform,
out scrollContent, out loadingOverlay, out errorOverlay, out errorText);
// ── DetailPanel contents ─────────────────────────────
var detailPanelComp = detailPanelGO.AddComponent<SongDetailPanel>();
Button btnNormal, btnHard, btnExpert, btnExpertPlus;
Button downloadBtn, deleteBtn, playBtn, closeBtn;
GameObject progressGroup;
Slider progressSlider;
TMP_Text progressText;
TMP_Text titleTmp, artistTmp, infoTmp;
BuildDetailPanelUI(detailPanelGO.transform,
out titleTmp, out artistTmp, out infoTmp,
out btnNormal, out btnHard, out btnExpert, out btnExpertPlus,
out downloadBtn, out deleteBtn, out playBtn, out closeBtn,
out progressGroup, out progressSlider, out progressText);
detailPanelGO.SetActive(false);
// ── Wire SongDetailPanel refs ────────────────────────
var dpSO = new SerializedObject(detailPanelComp);
dpSO.FindProperty("titleText") .objectReferenceValue = titleTmp;
dpSO.FindProperty("artistText") .objectReferenceValue = artistTmp;
dpSO.FindProperty("infoText") .objectReferenceValue = infoTmp;
dpSO.FindProperty("btnNormal") .objectReferenceValue = btnNormal;
dpSO.FindProperty("btnHard") .objectReferenceValue = btnHard;
dpSO.FindProperty("btnExpert") .objectReferenceValue = btnExpert;
dpSO.FindProperty("btnExpertPlus") .objectReferenceValue = btnExpertPlus;
dpSO.FindProperty("downloadButton") .objectReferenceValue = downloadBtn;
dpSO.FindProperty("deleteButton") .objectReferenceValue = deleteBtn;
dpSO.FindProperty("playButton") .objectReferenceValue = playBtn;
dpSO.FindProperty("closeButton") .objectReferenceValue = closeBtn;
dpSO.FindProperty("progressGroup") .objectReferenceValue = progressGroup;
dpSO.FindProperty("progressSlider") .objectReferenceValue = progressSlider;
dpSO.FindProperty("progressText") .objectReferenceValue = progressText;
dpSO.FindProperty("gameSceneName") .stringValue = "Game";
dpSO.ApplyModifiedPropertiesWithoutUndo();
// ── Wire SongSelectManager refs ──────────────────────
var smSO = new SerializedObject(songSelectManager);
smSO.FindProperty("tabAllBtn") .objectReferenceValue = tabAllBtn.GetComponent<Button>();
smSO.FindProperty("tabOwnedBtn") .objectReferenceValue = tabOwnedBtn.GetComponent<Button>();
smSO.FindProperty("cardContainer") .objectReferenceValue = scrollContent;
smSO.FindProperty("detailPanel") .objectReferenceValue = detailPanelComp;
smSO.FindProperty("downloadManager").objectReferenceValue = downloadManager;
smSO.FindProperty("loadingOverlay") .objectReferenceValue = loadingOverlay;
smSO.FindProperty("errorOverlay") .objectReferenceValue = errorOverlay;
smSO.FindProperty("errorText") .objectReferenceValue = errorText;
smSO.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ SongSelect panel rebuilt in Menu.unity");
}
// ─────────────────────────────────────────────
// ListPanel: ScrollRect + overlays
// ─────────────────────────────────────────────
private static void BuildScrollList(Transform parent,
out RectTransform scrollContent,
out GameObject loadingOverlay,
out GameObject errorOverlay,
out TMP_Text errorText)
{
// ScrollRect (fills parent)
var scrollGO = new GameObject("Scroll");
scrollGO.transform.SetParent(parent, false);
StretchFull(scrollGO);
// Viewport with Mask
var vpGO = new GameObject("Viewport");
vpGO.transform.SetParent(scrollGO.transform, false);
StretchFull(vpGO);
var vpImg = vpGO.AddComponent<Image>();
vpImg.color = new Color(0f, 0f, 0f, 0.01f);
vpGO.AddComponent<Mask>().showMaskGraphic = false;
// Content with VerticalLayoutGroup + ContentSizeFitter
var contentGO = new GameObject("Content");
contentGO.transform.SetParent(vpGO.transform, false);
var contentRect = contentGO.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = new Vector2(1f, 1f);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.anchoredPosition = Vector2.zero;
contentRect.sizeDelta = Vector2.zero;
var vlg = contentGO.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 1.5f;
vlg.padding = new RectOffset(2, 2, 2, 2);
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.childControlWidth = true;
vlg.childControlHeight = true;
var csf = contentGO.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// ScrollRect component
var sr = scrollGO.AddComponent<ScrollRect>();
sr.content = contentRect;
sr.viewport = vpGO.GetComponent<RectTransform>();
sr.horizontal = false;
sr.vertical = true;
sr.movementType = ScrollRect.MovementType.Clamped;
sr.scrollSensitivity = 30f;
sr.inertia = true;
sr.decelerationRate = 0.135f;
// Loading overlay
loadingOverlay = new GameObject("LoadingOverlay");
loadingOverlay.transform.SetParent(parent, false);
StretchFull(loadingOverlay);
loadingOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
CreateLabel(loadingOverlay.transform, "Text", "Loading...",
Vector2.zero, new Vector2(40f, 10f), 5f, Color.white, TextAlignmentOptions.Center);
// Error overlay (hidden by default)
errorOverlay = new GameObject("ErrorOverlay");
errorOverlay.transform.SetParent(parent, false);
StretchFull(errorOverlay);
errorOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
var errLblGO = CreateLabel(errorOverlay.transform, "ErrorText", "",
Vector2.zero, new Vector2(48f, 20f), 4.5f, new Color(1f, 0.5f, 0.5f), TextAlignmentOptions.Center);
errorText = errLblGO.GetComponent<TMP_Text>();
errorOverlay.SetActive(false);
scrollContent = contentRect;
}
// ─────────────────────────────────────────────
// DetailPanel UI
// Local space: 52.7 × 49 → X: ±26.35, Y: ±24.5
// ─────────────────────────────────────────────
private static void BuildDetailPanelUI(Transform parent,
out TMP_Text titleTmp, out TMP_Text artistTmp, out TMP_Text infoTmp,
out Button btnNormal, out Button btnHard, out Button btnExpert, out Button btnExpertPlus,
out Button downloadBtn, out Button deleteBtn, out Button playBtn, out Button closeBtn,
out GameObject progressGroup, out Slider progressSlider, out TMP_Text progressText)
{
// Close button (top-right)
var closeBtnGO = CreateStyledButton(parent, "CloseBtn", "✕",
new Vector2(21f, 20.5f), new Vector2(8f, 7f), 5.5f);
closeBtn = closeBtnGO.GetComponent<Button>();
// Song info
var titleGO = CreateLabel(parent, "TitleText", "---",
new Vector2(-3f, 18.5f), new Vector2(38f, 8f), 6.5f,
Color.white, TextAlignmentOptions.MidlineLeft);
titleTmp = titleGO.GetComponent<TMP_Text>();
titleTmp.overflowMode = TextOverflowModes.Ellipsis;
var artistGO = CreateLabel(parent, "ArtistText", "",
new Vector2(0f, 12f), new Vector2(50f, 6f), 5f,
new Color(1f, 1f, 1f, 0.8f), TextAlignmentOptions.Center);
artistTmp = artistGO.GetComponent<TMP_Text>();
var infoGO = CreateLabel(parent, "InfoText", "",
new Vector2(0f, 7f), new Vector2(50f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.6f), TextAlignmentOptions.Center);
infoTmp = infoGO.GetComponent<TMP_Text>();
CreateDivider(parent, "Div1", new Vector2(0f, 4f), new Vector2(50f, 0.4f));
// Difficulty section
CreateLabel(parent, "LblDifficulty", "DIFFICULTY",
new Vector2(-16f, 1.5f), new Vector2(26f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.65f), TextAlignmentOptions.MidlineLeft);
var btnNormalGO = CreateStyledButton(parent, "BtnNormal", "Normal", new Vector2(-12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnHardGO = CreateStyledButton(parent, "BtnHard", "Hard", new Vector2( 12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnExpertGO = CreateStyledButton(parent, "BtnExpert", "Expert", new Vector2(-12f, -14f), new Vector2(22f, 7f), 4.5f);
var btnExpertPlusGO = CreateStyledButton(parent, "BtnExpertPlus", "Expert+", new Vector2( 12f, -14f), new Vector2(22f, 7f), 4.5f);
btnNormal = btnNormalGO .GetComponent<Button>();
btnHard = btnHardGO .GetComponent<Button>();
btnExpert = btnExpertGO .GetComponent<Button>();
btnExpertPlus = btnExpertPlusGO.GetComponent<Button>();
CreateDivider(parent, "Div2", new Vector2(0f, -18.5f), new Vector2(50f, 0.4f));
// Action buttons
var downloadBtnGO = CreateStyledButton(parent, "DownloadBtn", "Download",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var deleteBtnGO = CreateStyledButton(parent, "DeleteBtn", "Delete",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var playBtnGO = CreateStyledButton(parent, "PlayBtn", "Play",
new Vector2(19f, -21.5f), new Vector2(16f, 7f), 5f);
downloadBtn = downloadBtnGO.GetComponent<Button>();
deleteBtn = deleteBtnGO .GetComponent<Button>();
playBtn = playBtnGO .GetComponent<Button>();
// Make delete button red-tinted
var delImg = deleteBtnGO.GetComponent<Image>();
if (delImg != null) delImg.color = new Color(0.9f, 0.3f, 0.3f, 0.3f);
// Progress group (hidden by default)
progressGroup = new GameObject("ProgressGroup");
progressGroup.transform.SetParent(parent, false);
SetRect(progressGroup, new Vector2(0f, -21.5f), new Vector2(50f, 7f));
var pTextGO = CreateLabel(progressGroup.transform, "ProgressText", "--- 0%",
new Vector2(-13f, 0f), new Vector2(22f, 6f), 4f,
new Color(1f, 1f, 1f, 0.85f), TextAlignmentOptions.MidlineLeft);
progressText = pTextGO.GetComponent<TMP_Text>();
progressSlider = CreateSlider(progressGroup.transform, "ProgressSlider",
new Vector2(18f, 0f), new Vector2(18f, 4.5f));
progressGroup.SetActive(false);
}
// ─────────────────────────────────────────────
// Helpers — UI factory
// ─────────────────────────────────────────────
private static GameObject CreateStyledButton(Transform parent, string goName, string label,
Vector2 pos, Vector2 size, float fontSize)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.12f);
var btn = go.AddComponent<Button>();
btn.targetGraphic = img;
var c = btn.colors;
c.normalColor = Color.white;
c.highlightedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.pressedColor = new Color(0.78f, 0.78f, 0.78f, 1f);
c.selectedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.fadeDuration = 0.1f;
btn.colors = c;
var textGO = new GameObject("Text");
textGO.transform.SetParent(go.transform, false);
StretchFull(textGO);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.alignment = TextAlignmentOptions.Center;
tmp.fontSize = fontSize;
tmp.color = Color.white;
return go;
}
private static GameObject CreateLabel(Transform parent, string goName, string text,
Vector2 pos, Vector2 size, float fontSize,
Color? color = null, TextAlignmentOptions align = TextAlignmentOptions.MidlineLeft)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.color = color ?? Color.white;
tmp.alignment = align;
return go;
}
private static void CreateDivider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.18f);
img.raycastTarget = false;
}
private static Slider CreateSlider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bgGO = new GameObject("Background");
bgGO.transform.SetParent(go.transform, false);
StretchFull(bgGO);
bgGO.AddComponent<Image>().color = new Color(1f, 1f, 1f, 0.15f);
var fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(go.transform, false);
StretchFull(fillArea);
var fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
StretchFull(fill);
fill.AddComponent<Image>().color = new Color(0.3f, 0.8f, 0.3f, 0.9f);
var slider = go.AddComponent<Slider>();
slider.fillRect = fill.GetComponent<RectTransform>();
slider.minValue = 0f;
slider.maxValue = 1f;
slider.value = 0f;
slider.interactable = false;
return slider;
}
// ─────────────────────────────────────────────
// Utils
// ─────────────────────────────────────────────
private static void SetRect(GameObject go, Vector2 pos, Vector2 size)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = new Vector2(0.5f, 0.5f);
r.anchorMax = new Vector2(0.5f, 0.5f);
r.pivot = new Vector2(0.5f, 0.5f);
r.anchoredPosition = pos;
r.sizeDelta = size;
}
private static void StretchFull(GameObject go)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = Vector2.zero;
r.anchorMax = Vector2.one;
r.offsetMin = r.offsetMax = Vector2.zero;
}
}