using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using Ichni; using Ichni.Editor; using Ichni.RhythmGame; using Ichni.StartMenu; using I2.Loc.SimpleJSON; using TMPro; using UnityEngine; using UnityEngine.InputSystem; public class consoleOnMono : MonoBehaviour { private const string LegacyImportFileName = "old_tbm_trams.json"; private const int ElementsPerFrame = 120; private const float LegacyHoldNoteVisualPositionScale = 0.1f; private const string LegacyEnvironmentThemeBundleName = "basic"; private const string LegacyEnvironmentFallbackObjectName = "EmptyObject"; private bool isImportingLegacyTbm; private readonly Dictionary legacyIdToElement = new(); private readonly Dictionary legacyIdToNode = new(); private readonly Dictionary pendingTrackPathNodes = new(); private readonly Dictionary pendingTrackTimeNodes = new(); private readonly Dictionary pendingTrackRendererNodes = new(); private readonly Dictionary legacyTrackColorMultipliers = new(); void Update() { if (Keyboard.current.f7Key.wasPressedThisFrame) { if (!isImportingLegacyTbm) { StartCoroutine(Read()); } } } public IEnumerator Read() { isImportingLegacyTbm = true; legacyIdToElement.Clear(); legacyIdToNode.Clear(); pendingTrackPathNodes.Clear(); pendingTrackTimeNodes.Clear(); pendingTrackRendererNodes.Clear(); legacyTrackColorMultipliers.Clear(); string path = SelectLegacyImportFilePath(); if (string.IsNullOrEmpty(path)) { LogWindow.Log("Legacy tbm import canceled.", Color.yellow); isImportingLegacyTbm = false; yield break; } if (!File.Exists(path)) { LogWindow.Log($"Legacy tbm import file not found: {path}", Color.red); isImportingLegacyTbm = false; yield break; } LogWindow.Log($"Start legacy tbm import: {path}"); JSONNode root; try { root = JSONNode.Parse(File.ReadAllText(path)); } catch (Exception e) { Debug.LogException(e); LogWindow.Log($"Legacy tbm parse failed: {e.Message}", Color.red); isImportingLegacyTbm = false; yield break; } JSONNode elements = root["BeatMap"]["value"]["beatMapElementList_BM"]; if (elements == null || elements.Count == 0) { LogWindow.Log("Legacy tbm has no BeatMap.value.beatMapElementList_BM.", Color.red); isImportingLegacyTbm = false; yield break; } for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string id = GetString(node, "id", string.Empty); if (!string.IsNullOrEmpty(id) && !legacyIdToNode.ContainsKey(id)) { legacyIdToNode.Add(id, node); } } LegacyImportStats stats = new(); yield return GenerateLegacyMainElements(elements, stats); yield return GenerateLegacyTrackSubmodules(stats); yield return GenerateLegacyPathNodes(elements, stats); yield return GenerateLegacyTrackPoints(elements, stats); yield return GenerateLegacyNotes(elements, stats); yield return GenerateLegacyNoteVisualOverrides(elements, stats); yield return GenerateLegacyAnimations(elements, stats); yield return RefreshLegacyImport(stats); yield return ApplyLegacyPositionSyncInitialPass(); LogWindow.Log( $"Legacy tbm import finished. Created {stats.created}, skipped {stats.skipped}, failed {stats.failed}.", stats.failed > 0 ? Color.yellow : Color.green); isImportingLegacyTbm = false; } private static string SelectLegacyImportFilePath() { OpenFileName file = new(); file.structSize = Marshal.SizeOf(file); file.filter = "Legacy TBM or JSON (*.tbm;*.json)\0*.tbm;*.json\0All Files (*.*)\0*.*\0\0"; file.file = new string(new char[4096]); file.maxFile = file.file.Length; file.fileTitle = new string(new char[512]); file.maxFileTitle = file.fileTitle.Length; file.initialDir = Application.streamingAssetsPath.Replace('/', '\\'); file.title = "Select Legacy TBM Import File"; file.defExt = "json"; file.flags = 0x00080000 | 0x00001000 | 0x00000800 | 0x00000008; return LocalDialog.GetOpenFileName(file) && !string.IsNullOrEmpty(file.file) ? Path.GetFullPath(file.file) : string.Empty; } private IEnumerator GenerateLegacyMainElements(JSONNode elements, LegacyImportStats stats) { for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string typeName = GetLegacyTypeName(node); switch (typeName) { case "ElementFolder_BM": Debug.Log($"[LegacyImport] Processing ElementFolder_BM: {GetElementName(node, "unnamed")}"); TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is ElementFolder); return ElementFolder.GenerateElement(GetElementName(node, "Legacy Folder"), CreateGuid(), new List(), true, parent); }, stats); break; case "GameCamera_BM": Debug.Log($"[LegacyImport] Processing GameCamera_BM: {GetElementName(node, "unnamed")}"); TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node) ?? CreateFallbackFolder(); if (EditorManager.instance.cameraManager.haveGameCamera) { GameCamera existingCamera = EditorManager.instance.cameraManager.gameCamera; ApplyLegacyGameCamera(existingCamera, node, parent); Debug.Log($"[LegacyImport] Map GameCamera_BM to existing camera under parent='{parent.elementName}'"); return existingCamera; } GameCamera camera = GameCamera.GenerateElement(GetElementName(node, "Legacy Camera"), CreateGuid(), new List(), true, parent, GetCameraViewType(node), GetFloat(node, "perspectiveAngle", 60f), GetFloat(node, "orthographicSize", 5f)); if (camera != null) { ApplyLegacyGameCamera(camera, node, parent); } return camera; }, stats); break; case "Track_BM": Debug.Log($"[LegacyImport] Processing Track_BM: {GetElementName(node, "unnamed")}"); TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is ElementFolder); if (parent == null) { parent = CreateFallbackFolder(); Debug.Log("[LegacyImport] Track_BM no parent, assigned to fallback folder"); } Track track = Track.GenerateElement(GetElementName(node, "Legacy Track"), CreateGuid(), new List(), true, parent); ApplyLegacyDuration(track, node); Debug.Log($"[LegacyImport] Track_BM created: {track.elementName}, duration overridden={track is IHaveTimeDurationSubmodule timed && timed.timeDurationSubmodule.isOverridingDuration}"); return track; }, stats); break; case "EnvironmentObject_BM": Debug.Log($"[LegacyImport] Processing EnvironmentObject_BM: {GetElementName(node, "unnamed")}"); TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node) ?? CreateFallbackFolder(); if (!TryFindLegacyEnvironmentObject(node, out string themeBundleName, out string objectName)) { return null; } EnvironmentObject environmentObject = EnvironmentObject.GenerateElement( GetElementName(node, "Legacy Environment Object"), CreateGuid(), new List(), true, themeBundleName, objectName, parent, GetBool(node, "isStatic", false)); ApplyLegacySubstantialObject(environmentObject, node); Debug.Log($"[LegacyImport] EnvironmentObject_BM created: oldObject='{GetString(node, "objectName", "?")}', newObject='{themeBundleName}/{objectName}', parent='{parent.elementName}'"); return environmentObject; }, stats); break; case "TimeTrackPoint_BM": Debug.Log($"[LegacyImport] Processing TimeTrackPoint_BM as CrossTrackPoint: {GetElementName(node, "unnamed")}"); TryCreateLegacyElement(node, () => { ElementFolder parent = GetLegacyParentOrAncestor(node, e => e is ElementFolder) as ElementFolder; if (parent == null) { parent = CreateFallbackFolder(); Debug.Log("[LegacyImport] TimeTrackPoint_BM no folder parent, assigned to fallback folder"); } return CrossTrackPoint.GenerateElement(GetElementName(node, "New Cross Track Point"), CreateGuid(), new List(), true, parent, ConvertLegacyTrackSwitch(node["trackSwitch"]), ConvertLegacyTrackPercent(node["trackPercent"])); }, stats); break; case "TrackPath_BM": Debug.Log($"[LegacyImport] Queue TrackPath_BM for track: {GetString(node, "id", "?")}"); QueueTrackNode(node, pendingTrackPathNodes, stats); break; case "TimeTrackMovable_BM": case "TimeTrackStatic_BM": Debug.Log($"[LegacyImport] Queue {typeName} for track: {GetString(node, "id", "?")}"); QueueTrackNode(node, pendingTrackTimeNodes, stats); break; case "TrackRendererAutoOrient_BM": case "TrackRendererPathGenerator_BM": Debug.Log($"[LegacyImport] Queue {typeName} for track: {GetString(node, "id", "?")}"); QueueTrackNode(node, pendingTrackRendererNodes, stats); break; } if (i % ElementsPerFrame == 0) { yield return null; } } } private IEnumerator GenerateLegacyTrackSubmodules(LegacyImportStats stats) { int index = 0; Debug.Log($"[LegacyImport] Processing {pendingTrackPathNodes.Count} pending TrackPath nodes..."); foreach (KeyValuePair pair in pendingTrackPathNodes) { Track track = pair.Key; JSONNode node = pair.Value; Debug.Log($"[LegacyImport] Replace TrackPath for Track '{track.elementName}'"); ReplaceTrackPathSubmodule(track, node); stats.created++; if (++index % ElementsPerFrame == 0) yield return null; } Debug.Log($"[LegacyImport] Processing {pendingTrackTimeNodes.Count} pending TimeTrack nodes..."); foreach (KeyValuePair pair in pendingTrackTimeNodes) { Track track = pair.Key; JSONNode node = pair.Value; Debug.Log($"[LegacyImport] Replace TimeTrack for Track '{track.elementName}'"); ReplaceTrackTimeSubmodule(track, node); stats.created++; if (++index % ElementsPerFrame == 0) yield return null; } Debug.Log($"[LegacyImport] Processing {pendingTrackRendererNodes.Count} pending TrackRenderer nodes..."); foreach (KeyValuePair pair in pendingTrackRendererNodes) { Track track = pair.Key; JSONNode node = pair.Value; Debug.Log($"[LegacyImport] Replace TrackRenderer for Track '{track.elementName}'"); ReplaceTrackRendererSubmodule(track, node); stats.created++; if (++index % ElementsPerFrame == 0) yield return null; } } private IEnumerator GenerateLegacyPathNodes(JSONNode elements, LegacyImportStats stats) { int pathNodeCount = 0; for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; if (GetLegacyTypeName(node) != "PathNode_BM") { continue; } pathNodeCount++; TryCreateLegacyElement(node, () => { Track track = GetLegacyParentOrAncestor(node, e => e is Track) as Track; if (track == null) { Debug.Log($"[LegacyImport] SKIP PathNode_BM: parent Track is null"); return null; } if (track.trackPathSubmodule == null) { Debug.Log($"[LegacyImport] SKIP PathNode_BM: Track '{track.elementName}' has no TrackPathSubmodule"); return null; } int index = Mathf.Clamp(GetInt(node, "index", track.trackPathSubmodule.pathNodeList.Count), 0, track.trackPathSubmodule.pathNodeList.Count); PathNode pathNode = PathNode.GenerateElement(GetElementName(node, "Legacy PathNode"), CreateGuid(), new List(), true, track, true, index); Vector3 position = GetVector3(node["position"], Vector3.zero); Vector3 normal = GetVector3(node["normal"], Vector3.up); Color color = GetColor(node["color"], Color.white); if (legacyTrackColorMultipliers.TryGetValue(track, out Color multiplier)) { color = MultiplyPathNodeColor(color, multiplier); } float size = Mathf.Max(0.001f, GetFloat(node, "size", 1f)); pathNode.transformSubmodule.originalPosition = position; pathNode.transformSubmodule.originalEulerAngles = NormalToEuler(normal); pathNode.transformSubmodule.originalScale = Vector3.one * size; pathNode.transformSubmodule.Refresh(); pathNode.colorSubmodule.originalBaseColor = color; pathNode.colorSubmodule.Refresh(); pathNode.Refresh(); Debug.Log($"[LegacyImport] PathNode_BM created: '{pathNode.elementName}', index={index}, pos={position}, size={size}"); return pathNode; }, stats); if (i % ElementsPerFrame == 0) { yield return null; } } Debug.Log($"[LegacyImport] Total PathNode_BM entries processed: {pathNodeCount}"); } private IEnumerator GenerateLegacyTrackPoints(JSONNode elements, LegacyImportStats stats) { int trackPercentPointCount = 0; for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string typeName = GetLegacyTypeName(node); switch (typeName) { case "TrackPercentPoint_BM": trackPercentPointCount++; TryCreateLegacyElement(node, () => { Track parent = GetLegacyParentOrAncestor(node, e => e is Track) as Track; if (parent == null) { Debug.LogWarning($"[LegacyImport] SKIP TrackPercentPoint_BM: Track parent is null, id={GetString(node, "id", "?")}"); return null; } TrackPercentPoint point = TrackPercentPoint.GenerateElement(GetElementName(node, "Legacy Track Percent Point"), CreateGuid(), new List(), true, parent, ConvertLegacyTrackPercentPoint(node)); point.MotionAngles = GetBool(node, "MotionAngles", point.MotionAngles); point.Refresh(); return point; }, stats); break; } if (i % ElementsPerFrame == 0) { yield return null; } } Debug.Log($"[LegacyImport] TrackPoints summary: TrackPercentPoint={trackPercentPointCount}"); } private IEnumerator GenerateLegacyNotes(JSONNode elements, LegacyImportStats stats) { int[] noteCounts = new int[4]; // 0=Tap, 1=Stay, 2=Hold, 3=unknown skipped for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string typeName = GetLegacyTypeName(node); switch (typeName) { case "Tap_BM": noteCounts[0]++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is Track); if (parent == null) { Debug.Log($"[LegacyImport] SKIP Tap_BM: parent is null, id={GetString(node, "id", "?")}"); return null; } float judgeTime = GetFloat(node, "exactJudgeTime", 0f); Debug.Log($"[LegacyImport] Creating Tap_BM at t={judgeTime}, parent='{parent.elementName}'"); return Tap.GenerateElement(GetElementName(node, "Legacy Tap"), CreateGuid(), new List(), true, parent, judgeTime); }, stats); break; case "Stay_BM": noteCounts[1]++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is Track); if (parent == null) { Debug.Log($"[LegacyImport] SKIP Stay_BM: parent is null, id={GetString(node, "id", "?")}"); return null; } float judgeTime = GetFloat(node, "exactJudgeTime", 0f); Debug.Log($"[LegacyImport] Creating Stay_BM at t={judgeTime}, parent='{parent.elementName}'"); return Stay.GenerateElement(GetElementName(node, "Legacy Stay"), CreateGuid(), new List(), true, parent, judgeTime); }, stats); break; case "Hold_BM": noteCounts[2]++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is Track); if (parent == null) { Debug.Log($"[LegacyImport] SKIP Hold_BM: parent is null, id={GetString(node, "id", "?")}"); return null; } float start = GetFloat(node, "exactJudgeTime", 0f); float end = ConvertLegacyHoldEndTime(node, start); Debug.Log($"[LegacyImport] Creating Hold_BM: start={start}, end={end}, duration={end - start}, parent='{parent.elementName}'"); return Hold.GenerateElement(GetElementName(node, "Legacy Hold"), CreateGuid(), new List(), true, parent, start, end); }, stats); break; default: if (typeName.EndsWith("_BM") && !IsKnownSkippedType(typeName)) { noteCounts[3]++; stats.skipped++; } break; } if (i % ElementsPerFrame == 0) { yield return null; } } Debug.Log($"[LegacyImport] Notes summary: Tap={noteCounts[0]}, Stay={noteCounts[1]}, Hold={noteCounts[2]}, unknownSkipped={noteCounts[3]}"); } private IEnumerator GenerateLegacyNoteVisualOverrides(JSONNode elements, LegacyImportStats stats) { int noteVisualCount = 0; int appliedCount = 0; for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string typeName = GetLegacyTypeName(node); if (!IsLegacyNoteVisualType(typeName)) { continue; } noteVisualCount++; string legacyId = GetString(node, "id", string.Empty); if (string.IsNullOrEmpty(legacyId) || legacyIdToElement.ContainsKey(legacyId)) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP NoteVisual override: duplicate or no ID, type={typeName}, id={legacyId}"); continue; } NoteBase note = GetLegacyParentOrAncestor(node, e => e is NoteBase) as NoteBase; if (note == null || note.noteVisual == null) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP NoteVisual override: Note parent or generated NoteVisual is null, type={typeName}, id={legacyId}"); continue; } try { IHaveTransformSubmodule transformHost = note.noteVisual; TransformSubmodule transformSubmodule = transformHost.transformSubmodule; Vector3 legacyPosition = GetVector3(node["position"], transformSubmodule.originalPosition); if (note is Hold) { legacyPosition *= LegacyHoldNoteVisualPositionScale; } transformSubmodule.originalPosition = legacyPosition; transformSubmodule.originalEulerAngles = GetVector3(node["eulerAngles"], transformSubmodule.originalEulerAngles); transformSubmodule.originalScale = GetVector3(node["scale"], transformSubmodule.originalScale); transformSubmodule.Refresh(); transformHost.UpdateTransform(false); note.noteVisual.Refresh(); legacyIdToElement.Add(legacyId, note.noteVisual); stats.created++; appliedCount++; Debug.Log($"[LegacyImport] APPLY NoteVisual override {typeName} -> Note '{note.elementName}', pos={transformSubmodule.originalPosition}, euler={transformSubmodule.originalEulerAngles}, scale={transformSubmodule.originalScale}, holdPositionScale={(note is Hold ? LegacyHoldNoteVisualPositionScale : 1f)}"); } catch (Exception e) { stats.failed++; Debug.LogError($"[LegacyImport] FAILED NoteVisual override type={typeName}, id={legacyId}: {e.Message}"); } if (i % ElementsPerFrame == 0) { yield return null; } } Debug.Log($"[LegacyImport] NoteVisual override summary: found={noteVisualCount}, applied={appliedCount}"); } private IEnumerator GenerateLegacyAnimations(JSONNode elements, LegacyImportStats stats) { int displacementCount = 0; int swirlCount = 0; int trackColorCount = 0; int baseColorCount = 0; int emissionColorCount = 0; int environmentBaseColorCount = 0; int environmentEmissionColorCount = 0; int positionSyncCount = 0; for (int i = 0; i < elements.Count; i++) { JSONNode node = elements[i]; string typeName = GetLegacyTypeName(node); switch (typeName) { case "Displacement_BM": displacementCount++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveTransformSubmodule); if (parent == null) { Debug.Log($"[LegacyImport] SKIP Displacement_BM: transform parent is null, id={GetString(node, "id", "?")}"); return null; } return Displacement.GenerateElement(GetElementName(node, "Legacy Displacement"), CreateGuid(), new List(), true, parent, ConvertLegacyFlexibleFloat(node["positionX"]), ConvertLegacyFlexibleFloat(node["positionY"]), ConvertLegacyFlexibleFloat(node["positionZ"])); }, stats); break; case "Swirl_BM": swirlCount++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveTransformSubmodule); if (parent == null) { Debug.Log($"[LegacyImport] SKIP Swirl_BM: transform parent is null, id={GetString(node, "id", "?")}"); return null; } return Swirl.GenerateElement(GetElementName(node, "Legacy Swirl"), CreateGuid(), new List(), true, parent, ConvertLegacyFlexibleFloat(node["swirlX"]), ConvertLegacyFlexibleFloat(node["swirlY"]), ConvertLegacyFlexibleFloat(node["swirlZ"])); }, stats); break; case "PositionSync_BM": positionSyncCount++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveTransformSubmodule); if (parent == null) { Debug.LogWarning($"[LegacyImport] SKIP PositionSync_BM: transform parent is null, id={GetString(node, "id", "?")}"); return null; } GameElement target = GetLegacyPositionSyncTarget(node); if (target == null) { Debug.LogWarning($"[LegacyImport] SKIP PositionSync_BM: target '{GetString(node, "targetElementName", "?")}' not found, id={GetString(node, "id", "?")}"); return null; } return PositionSync.GenerateElement(GetElementName(node, "Legacy Position Sync"), CreateGuid(), new List(), true, parent, target, ConvertLegacyFlexibleBool(node["enabling"], true)); }, stats); break; case "BaseColorChange_BM": baseColorCount++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveColorSubmodule); if (parent == null) { Debug.LogWarning($"[LegacyImport] SKIP BaseColorChange_BM: color parent is null, id={GetString(node, "id", "?")}"); return null; } return BaseColorChange.GenerateElement(GetElementName(node, "Legacy Base Color Change"), CreateGuid(), new List(), true, parent, ConvertLegacyFlexibleFloat(node["colorR"]), ConvertLegacyFlexibleFloat(node["colorG"]), ConvertLegacyFlexibleFloat(node["colorB"]), ConvertLegacyFlexibleFloat(node["colorA"])); }, stats); break; case "EmissionColorChange_BM": emissionColorCount++; TryCreateLegacyElement(node, () => { GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveColorSubmodule); if (parent == null) { Debug.LogWarning($"[LegacyImport] SKIP EmissionColorChange_BM: color parent is null, id={GetString(node, "id", "?")}"); return null; } return EmissionColorChange.GenerateElement(GetElementName(node, "Legacy Emission Color Change"), CreateGuid(), new List(), true, parent, ConvertLegacyFlexibleFloat(node["colorR"]), ConvertLegacyFlexibleFloat(node["colorG"]), ConvertLegacyFlexibleFloat(node["colorB"]), ConvertLegacyFlexibleFloat(node["colorI"])); }, stats); break; case "EnvironmentObjectBaseColor_BM": environmentBaseColorCount++; ApplyLegacyEnvironmentBaseColor(node, stats); break; case "EnvironmentObjectEmissionColor_BM": environmentEmissionColorCount++; ApplyLegacyEnvironmentEmissionColor(node, stats); break; case "TrackGlobalColor_BM": trackColorCount++; TryCreateLegacyElement(node, () => { Track parent = GetLegacyParentOrAncestor(node, e => e is Track) as Track; if (parent == null) { Debug.Log($"[LegacyImport] SKIP TrackGlobalColor_BM: Track parent is null, id={GetString(node, "id", "?")}"); return null; } return TrackGlobalColorChange.GenerateElement(GetElementName(node, "Legacy Track Global Color"), CreateGuid(), new List(), true, parent, ConvertLegacyFlexibleFloat(node["colorR"]), ConvertLegacyFlexibleFloat(node["colorG"]), ConvertLegacyFlexibleFloat(node["colorB"]), ConvertLegacyFlexibleFloat(node["colorA"])); }, stats); break; } if (i % ElementsPerFrame == 0) { yield return null; } } Debug.Log($"[LegacyImport] Animations summary: Displacement={displacementCount}, Swirl={swirlCount}, PositionSync={positionSyncCount}, TrackGlobalColor={trackColorCount}, BaseColor={baseColorCount}, EmissionColor={emissionColorCount}, EnvBaseColor={environmentBaseColorCount}, EnvEmissionColor={environmentEmissionColorCount}"); } private IEnumerator RefreshLegacyImport(LegacyImportStats stats) { int refreshed = 0; foreach (GameElement element in legacyIdToElement.Values) { try { if (element is Track track) { track.trackPathSubmodule?.Refresh(); track.trackTimeSubmodule?.Refresh(); track.trackRendererSubmodule?.Refresh(); Debug.Log($"[LegacyImport] Refreshed Track: '{track.elementName}' (submodules: path={track.trackPathSubmodule != null}, time={track.trackTimeSubmodule != null}, renderer={track.trackRendererSubmodule != null})"); } else { Debug.Log($"[LegacyImport] Refreshing element: '{element.elementName}' ({element.GetType().Name})"); } element.Refresh(); refreshed++; } catch (Exception e) { stats.failed++; Debug.LogWarning($"[LegacyImport] Refresh failed on '{element.elementName}': {e.Message}"); } yield return null; } Debug.Log($"[LegacyImport] Refresh complete: {refreshed} elements refreshed, {stats.failed} failures"); } private IEnumerator ApplyLegacyPositionSyncInitialPass() { float songTime = CoreServices.TimeProvider?.SongTime ?? 0f; int syncCount = 0; foreach (GameElement element in legacyIdToElement.Values) { if (element is IHaveTransformSubmodule transformHost) { transformHost.UpdateTransform(false); } } foreach (GameElement element in legacyIdToElement.Values) { if (element is not PositionSync sync) { continue; } sync.ScheduledUpdate(UpdatePhase.Animation, songTime); sync.ScheduledUpdate(UpdatePhase.Effect, songTime); syncCount++; yield return null; } Debug.Log($"[LegacyImport] PositionSync initial pass complete: {syncCount} syncs applied at songTime={songTime}"); } private void TryCreateLegacyElement(JSONNode node, Func create, LegacyImportStats stats) { string legacyId = GetString(node, "id", string.Empty); string typeName = GetLegacyTypeName(node); string elementName = GetElementName(node, "unnamed"); if (string.IsNullOrEmpty(legacyId) || legacyIdToElement.ContainsKey(legacyId)) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP (duplicate or no ID) type={typeName}, id={legacyId}, name={elementName}"); return; } try { GameElement element = create.Invoke(); if (element == null) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP (create returned null) type={typeName}, id={legacyId}, name={elementName}"); return; } legacyIdToElement.Add(legacyId, element); stats.created++; Debug.Log($"[LegacyImport] CREATE {typeName} -> {element.elementName}, id={legacyId}"); } catch (Exception e) { stats.failed++; Debug.LogError($"[LegacyImport] FAILED type={typeName}, id={legacyId}, name={elementName}: {e.Message}"); } } private void QueueTrackNode(JSONNode node, Dictionary queue, LegacyImportStats stats) { string legacyId = GetString(node, "id", "?"); Track track = GetLegacyParentOrAncestor(node, e => e is Track) as Track; if (track == null) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP QueueTrackNode id={legacyId}: parent Track not found"); return; } queue[track] = node; Debug.Log($"[LegacyImport] Queued {GetLegacyTypeName(node)} id={legacyId} -> Track '{track.elementName}'"); } private void ReplaceTrackPathSubmodule(Track track, JSONNode node) { track.trackPathSubmodule?.Delete(); track.trackPathSubmodule = new TrackPathSubmodule(track, (Track.TrackSpaceType)GetInt(node, "trackSpaceType", (int)Track.TrackSpaceType.CatmullRom), (Track.TrackSamplingType)GetInt(node, "trackSamplingType", (int)Track.TrackSamplingType.TimeDistributed), GetBool(node, "isClosed", false), GetBool(node, "isShowingPath", false)); Debug.Log($"[LegacyImport] ReplaceTrackPathSubmodule done for '{track.elementName}': spaceType={track.trackPathSubmodule.trackSpaceType}"); } private void ReplaceTrackTimeSubmodule(Track track, JSONNode node) { track.trackTimeSubmodule?.Delete(); if (GetLegacyTypeName(node) == "TimeTrackStatic_BM") { float totalTime = Mathf.Max(0.1f, GetFloat(node, "trackTotalTime", GetFloat(node, "visibleTrackTimeLength", 1f))); track.trackTimeSubmodule = new TrackTimeSubmoduleStatic(track, totalTime, GetCurveType(node)); Debug.Log($"[LegacyImport] ReplaceTrackTimeSubmodule(Static) for '{track.elementName}': totalTime={totalTime}"); return; } float start = GetFloat(node, "trackStartTime", 0f); float end = Mathf.Max(start + 0.1f, GetFloat(node, "trackEndTime", start + 1f)); float visibleLength = Mathf.Max(0.1f, GetFloat(node, "visibleTrackTimeLength", 1f)); track.trackTimeSubmodule = new TrackTimeSubmoduleMovable(track, start, end, visibleLength, GetCurveType(node)); Debug.Log($"[LegacyImport] ReplaceTrackTimeSubmodule(Movable) for '{track.elementName}': start={start}, end={end}, visibleLength={visibleLength}"); } private void ReplaceTrackRendererSubmodule(Track track, JSONNode node) { track.trackRendererSubmodule?.Delete(); bool enableEmission = GetBool(node, "emissionEnabled", false); float emissionIntensity = GetFloat(node, "emissionIntensity", 0f); Color emissionColor = GetColor(node["emissionColor"], Color.white); legacyTrackColorMultipliers[track] = enableEmission ? new Color( emissionColor.r * Mathf.Pow(2f, emissionIntensity), emissionColor.g * Mathf.Pow(2f, emissionIntensity), emissionColor.b * Mathf.Pow(2f, emissionIntensity), 1f) : Color.white; Vector2 uvScale = Vector2.one; Vector2 uvOffset = Vector2.zero; if (GetLegacyTypeName(node) == "TrackRendererPathGenerator_BM") { track.trackRendererSubmodule = new TrackRendererSubmodulePathGenerator(track, enableEmission, emissionIntensity, true, uvScale, uvOffset); Debug.Log($"[LegacyImport] ReplaceTrackRendererSubmodule(PathGenerator) for '{track.elementName}': emission={enableEmission}, intensity={emissionIntensity}"); } else { track.trackRendererSubmodule = new TrackRendererSubmoduleAutoOrient(track, enableEmission, emissionIntensity, true, uvScale, uvOffset); Debug.Log($"[LegacyImport] ReplaceTrackRendererSubmodule(AutoOrient) for '{track.elementName}': emission={enableEmission}, intensity={emissionIntensity}"); } track.trackRendererSubmodule.Refresh(); } private bool TryFindLegacyEnvironmentObject(JSONNode node, out string themeBundleName, out string objectName) { themeBundleName = string.Empty; objectName = string.Empty; string legacyObjectName = GetString(node, "objectName", string.Empty); ThemeBundle themeBundle = ThemeBundleManager.instance.loadedThemeBundleList .FirstOrDefault(bundle => string.Equals(bundle.themeBundleName, LegacyEnvironmentThemeBundleName, StringComparison.OrdinalIgnoreCase)); if (themeBundle == null) { Debug.LogWarning($"[LegacyImport] SKIP EnvironmentObject_BM: theme bundle '{LegacyEnvironmentThemeBundleName}' is not loaded, id={GetString(node, "id", "?")}, oldObject='{legacyObjectName}'"); return false; } GameObject asset = themeBundle.assetList_GameObject .FirstOrDefault(obj => obj != null && string.Equals(obj.name, legacyObjectName, StringComparison.OrdinalIgnoreCase)); if (asset == null) { asset = themeBundle.assetList_GameObject .FirstOrDefault(obj => obj != null && string.Equals(obj.name, LegacyEnvironmentFallbackObjectName, StringComparison.OrdinalIgnoreCase)); if (asset == null) { Debug.LogWarning($"[LegacyImport] SKIP EnvironmentObject_BM: object '{legacyObjectName}' and fallback '{LegacyEnvironmentFallbackObjectName}' not found in theme bundle '{themeBundle.themeBundleName}', id={GetString(node, "id", "?")}"); return false; } Debug.LogWarning($"[LegacyImport] EnvironmentObject_BM object '{legacyObjectName}' not found in '{themeBundle.themeBundleName}', fallback to '{asset.name}', id={GetString(node, "id", "?")}"); } themeBundleName = themeBundle.themeBundleName; objectName = asset.name; return true; } private void ApplyLegacySubstantialObject(SubstantialObject substantialObject, JSONNode node) { if (substantialObject == null) return; if (substantialObject.transformSubmodule != null) { substantialObject.transformSubmodule.originalPosition = GetVector3(node["position"], substantialObject.transformSubmodule.originalPosition); substantialObject.transformSubmodule.originalEulerAngles = GetVector3(node["eulerAngles"], substantialObject.transformSubmodule.originalEulerAngles); substantialObject.transformSubmodule.originalScale = GetVector3(node["scale"], substantialObject.transformSubmodule.originalScale); substantialObject.transformSubmodule.Refresh(); ((IHaveTransformSubmodule)substantialObject).UpdateTransform(false); } ApplyLegacyDuration(substantialObject, node); substantialObject.Refresh(); } private void ApplyLegacyEnvironmentBaseColor(JSONNode node, LegacyImportStats stats) { string legacyId = GetString(node, "id", string.Empty); if (string.IsNullOrEmpty(legacyId) || legacyIdToElement.ContainsKey(legacyId)) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP EnvironmentObjectBaseColor_BM: duplicate or no ID, id={legacyId}"); return; } GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveColorSubmodule); if (parent is not IHaveColorSubmodule colorHost || colorHost.colorSubmodule == null) { stats.skipped++; Debug.LogWarning($"[LegacyImport] SKIP EnvironmentObjectBaseColor_BM: color parent is null, id={legacyId}"); return; } ColorSubmodule colorSubmodule = colorHost.colorSubmodule; colorSubmodule.originalBaseColor = GetColor(node["baseColor"], colorSubmodule.originalBaseColor); colorSubmodule.Refresh(); colorHost.UpdateColor(false); parent.Refresh(); legacyIdToElement.Add(legacyId, parent); stats.created++; Debug.Log($"[LegacyImport] APPLY EnvironmentObjectBaseColor_BM -> '{parent.elementName}', color={colorSubmodule.originalBaseColor}"); } private void ApplyLegacyEnvironmentEmissionColor(JSONNode node, LegacyImportStats stats) { string legacyId = GetString(node, "id", string.Empty); if (string.IsNullOrEmpty(legacyId) || legacyIdToElement.ContainsKey(legacyId)) { stats.skipped++; Debug.Log($"[LegacyImport] SKIP EnvironmentObjectEmissionColor_BM: duplicate or no ID, id={legacyId}"); return; } GameElement parent = GetLegacyParentOrAncestor(node, e => e is IHaveColorSubmodule); if (parent is not IHaveColorSubmodule colorHost || colorHost.colorSubmodule == null) { stats.skipped++; Debug.LogWarning($"[LegacyImport] SKIP EnvironmentObjectEmissionColor_BM: color parent is null, id={legacyId}"); return; } ColorSubmodule colorSubmodule = colorHost.colorSubmodule; colorSubmodule.emissionEnabled = true; colorSubmodule.originalEmissionColor = GetColor(node["emissionColor"], colorSubmodule.originalEmissionColor); colorSubmodule.originalEmissionIntensity = GetFloat(node, "intensity", colorSubmodule.originalEmissionIntensity); colorSubmodule.Refresh(); colorHost.UpdateColor(false); parent.Refresh(); legacyIdToElement.Add(legacyId, parent); stats.created++; Debug.Log($"[LegacyImport] APPLY EnvironmentObjectEmissionColor_BM -> '{parent.elementName}', color={colorSubmodule.originalEmissionColor}, intensity={colorSubmodule.originalEmissionIntensity}"); } private void ApplyLegacyDuration(GameElement element, JSONNode node) { if (element is not IHaveTimeDurationSubmodule timed) return; float start = GetFloat(node, "enableStartTime", -32767f); float end = GetFloat(node, "enableEndTime", 32767f); if (Mathf.Approximately(start, 0f) && Mathf.Approximately(end, 0f)) { return; } timed.timeDurationSubmodule.isOverridingDuration = true; timed.timeDurationSubmodule.startTime = start; timed.timeDurationSubmodule.endTime = end; } private GameElement GetLegacyPositionSyncTarget(JSONNode node) { string targetElementName = GetString(node, "targetElementName", string.Empty); if (string.IsNullOrEmpty(targetElementName)) { return null; } GameElement result = null; int matchCount = 0; foreach (KeyValuePair pair in legacyIdToElement) { if (pair.Value is IHaveTransformSubmodule && string.Equals(pair.Value.elementName, targetElementName, StringComparison.Ordinal)) { result ??= pair.Value; matchCount++; } } if (matchCount > 1) { Debug.LogWarning($"[LegacyImport] PositionSync target name '{targetElementName}' matched {matchCount} elements, using '{result.elementName}'"); } return result; } private void ApplyLegacyGameCamera(GameCamera camera, JSONNode node, GameElement parent) { if (camera == null) return; ReparentLegacyElement(camera, parent); camera.elementName = GetElementName(node, camera.elementName); camera.cameraViewType = GetCameraViewType(node); camera.perspectiveAngle = GetFloat(node, "perspectiveAngle", camera.perspectiveAngle); camera.orthographicSize = GetFloat(node, "orthographicSize", camera.orthographicSize); if (camera.cam != null) { camera.cam.orthographic = camera.cameraViewType == GameCamera.CameraViewType.Orthographic; camera.cam.fieldOfView = camera.perspectiveAngle; camera.cam.orthographicSize = camera.orthographicSize; } if (camera.transformSubmodule != null) { camera.transformSubmodule.originalPosition = GetVector3(node["position"], camera.transformSubmodule.originalPosition); camera.transformSubmodule.originalEulerAngles = GetVector3(node["eulerAngles"], camera.transformSubmodule.originalEulerAngles); camera.transformSubmodule.Refresh(); camera.UpdateTransform(false); } ApplyLegacyDuration(camera, node); camera.Refresh(); Debug.Log($"[LegacyImport] ApplyLegacyGameCamera: parent='{parent?.elementName ?? "null"}', pos={camera.transformSubmodule?.originalPosition}, euler={camera.transformSubmodule?.originalEulerAngles}, view={camera.cameraViewType}"); } private void ReparentLegacyElement(GameElement element, GameElement parent) { if (element == null) return; element.parentElement?.childElementList.Remove(element); if (parent != null) { parent.childElementList.Remove(element); parent.childElementList.Add(element); element.parentElement = parent; element.transform.SetParent(parent.transform); } else { element.parentElement = null; element.transform.SetParent(null); } RefreshLegacyHierarchyParent(element, parent); } private void RefreshLegacyHierarchyParent(GameElement element, GameElement parent) { if (EditorManager.instance?.uiManager?.hierarchy == null) return; if (parent != null) { EnsureLegacyHierarchyTab(parent); } if (element.connectedTab != null) { element.connectedTab.parentTab?.childTabList.Remove(element.connectedTab); element.connectedTab.SetTab(element, parent, false); } else if (parent == null || parent.connectedTab != null) { EditorManager.instance.uiManager.hierarchy.GenerateTab(element, parent, false); } parent?.connectedTab?.SetStatus(); } private void EnsureLegacyHierarchyTab(GameElement element) { if (element == null || element.connectedTab != null) return; EnsureLegacyHierarchyTab(element.parentElement); if (element.parentElement == null || element.parentElement.connectedTab != null) { EditorManager.instance.uiManager.hierarchy.GenerateTab(element, element.parentElement, false); } } private GameElement GetLegacyParentOrAncestor(JSONNode node, Func predicate = null) { string parentId = GetString(node, "attachedElementId", string.Empty); HashSet visited = new(); while (!string.IsNullOrEmpty(parentId) && parentId != "BeatMapContainer" && visited.Add(parentId)) { if (legacyIdToElement.TryGetValue(parentId, out GameElement parentElement) && (predicate == null || predicate(parentElement))) { return parentElement; } if (!legacyIdToNode.TryGetValue(parentId, out JSONNode parentNode)) { break; } if (GetLegacyTypeName(parentNode) == "GameCamera_BM") { GameCamera camera = EditorManager.instance?.cameraManager?.gameCamera; if (camera != null && (predicate == null || predicate(camera))) { if (!legacyIdToElement.ContainsKey(parentId)) { legacyIdToElement.Add(parentId, camera); Debug.Log($"[LegacyImport] Restored missing GameCamera_BM parent mapping id={parentId} -> '{camera.elementName}'"); } return camera; } } parentId = GetString(parentNode, "attachedElementId", string.Empty); } return null; } private ElementFolder CreateFallbackFolder() { const string fallbackId = "__legacy_import_fallback_folder"; if (legacyIdToElement.TryGetValue(fallbackId, out GameElement existing)) { Debug.Log("[LegacyImport] Reusing existing fallback folder"); return existing as ElementFolder; } ElementFolder folder = ElementFolder.GenerateElement("Legacy Import", CreateGuid(), new List(), true, null); legacyIdToElement.Add(fallbackId, folder); Debug.Log("[LegacyImport] Created fallback folder: 'Legacy Import'"); return folder; } private static string GetLegacyTypeName(JSONNode node) { string type = GetString(node, "__type", string.Empty); int commaIndex = type.IndexOf(",", StringComparison.Ordinal); if (commaIndex >= 0) type = type.Substring(0, commaIndex); int dotIndex = type.LastIndexOf(".", StringComparison.Ordinal); return dotIndex >= 0 ? type.Substring(dotIndex + 1) : type; } private static bool IsKnownSkippedType(string typeName) { return IsLegacyNoteVisualType(typeName) || typeName.Contains("NoteEffect") || typeName is "Displacement_BM" or "Swirl_BM" or "TrackGlobalColor_BM" or "TimeTrackPoint_BM" or "EnvironmentObject_BM" or "EnvironmentObjectBaseColor_BM" or "EnvironmentObjectEmissionColor_BM" or "BaseColorChange_BM" or "EmissionColorChange_BM" or "TrackPercentPoint_BM" or "PositionSync_BM"; } private static bool IsLegacyNoteVisualType(string typeName) { return typeName.Contains("NoteVisual") || typeName.Contains("NoteHoldVisual"); } private static FlexibleInt ConvertLegacyTrackSwitch(JSONNode node) { FlexibleInt result = new(); JSONNode list = node["animatedIntList_BM"]; for (int i = 0; i < list.Count; i++) { JSONNode item = list[i]; result.Add(new AnimatedInt(GetFloat(item, "startTime", 0f), GetInt(item, "value", 0))); } return result; } private static FlexibleFloat ConvertLegacyTrackPercent(JSONNode node) { return ConvertLegacyFlexibleFloat(node); } private static float ConvertLegacyHoldEndTime(JSONNode node, float start) { if (HasJsonKey(node, "holdEndTime")) { return Mathf.Max(start + 0.1f, GetFloat(node, "holdEndTime", start + 0.1f)); } float duration = GetFloat(node, "holdTime", 0.1f); return start + Mathf.Max(0.1f, duration); } private static FlexibleFloat ConvertLegacyTrackPercentPoint(JSONNode node) { JSONNode trackPercent = node["trackPercent"]; if (trackPercent != null && trackPercent.Count > 0) { return ConvertLegacyFlexibleFloat(trackPercent); } float percent = GetFloat(node, "percent", 0f); return new FlexibleFloat(new List { new(0f, 0.1f, percent, percent, AnimationCurveType.Linear) }); } private static FlexibleFloat ConvertLegacyFlexibleFloat(JSONNode node) { FlexibleFloat result = new(); JSONNode list = node["animatedFloatList_BM"]; for (int i = 0; i < list.Count; i++) { JSONNode item = list[i]; float startTime = GetFloat(item, "startTime", 0f); float endTime = Mathf.Max(startTime + 0.001f, GetFloat(item, "endTime", startTime + 1f)); result.Add(new AnimatedFloat(startTime, endTime, GetFloat(item, "startValue", 0f), GetFloat(item, "endValue", 1f), GetCurveType(item, "presetAnimationCurveType"))); } result.Sort(); return result; } private static FlexibleBool ConvertLegacyFlexibleBool(JSONNode node, bool fallback) { FlexibleBool result = new(); if (node == null || node.Count == 0) { result.Add(new AnimatedBool(0f, fallback)); return result; } JSONNode list = node["animatedBoolList_BM"]; if (list == null || list.Count == 0) { list = node["animatedBoolList"]; } for (int i = 0; i < list.Count; i++) { JSONNode item = list[i]; result.Add(new AnimatedBool(GetFloat(item, "time", 0f), GetBool(item, "value", fallback))); } if (result.animations.Count == 0) { result.Add(new AnimatedBool(0f, fallback)); } return result; } private static AnimationCurveType GetCurveType(JSONNode node) { return GetCurveType(node["presetAnimationCurve"], "animationCurveType"); } private static AnimationCurveType GetCurveType(JSONNode node, string key) { int value = GetInt(node, key, 0); return Enum.IsDefined(typeof(AnimationCurveType), value) ? (AnimationCurveType)value : AnimationCurveType.Linear; } private static GameCamera.CameraViewType GetCameraViewType(JSONNode node) { int value = GetInt(node, "cameraViewType", (int)GameCamera.CameraViewType.Perspective); return Enum.IsDefined(typeof(GameCamera.CameraViewType), value) ? (GameCamera.CameraViewType)value : GameCamera.CameraViewType.Perspective; } private static Color MultiplyPathNodeColor(Color color, Color multiplier) { return new Color(color.r * multiplier.r, color.g * multiplier.g, color.b * multiplier.b, color.a); } private static Vector3 GetVector3(JSONNode node, Vector3 fallback) { if (node == null) return fallback; return new Vector3(GetFloat(node, "x", fallback.x), GetFloat(node, "y", fallback.y), GetFloat(node, "z", fallback.z)); } private static Color GetColor(JSONNode node, Color fallback) { if (node == null) return fallback; return new Color(GetFloat(node, "r", fallback.r), GetFloat(node, "g", fallback.g), GetFloat(node, "b", fallback.b), GetFloat(node, "a", fallback.a)); } private static Vector3 NormalToEuler(Vector3 normal) { if (normal.sqrMagnitude < 0.0001f) return Vector3.zero; return Quaternion.FromToRotation(Vector3.up, normal.normalized).eulerAngles; } private static string GetElementName(JSONNode node, string fallback) { return GetString(node, "elementName", fallback); } private static string GetString(JSONNode node, string key, string fallback) { if (node == null || node[key] is JSONLazyCreator) return fallback; string value = node[key]; return string.IsNullOrEmpty(value) ? fallback : value; } private static int GetInt(JSONNode node, string key, int fallback) { if (node == null || node[key] is JSONLazyCreator) return fallback; return node[key].AsInt; } private static float GetFloat(JSONNode node, string key, float fallback) { if (node == null || node[key] is JSONLazyCreator) return fallback; return node[key].AsFloat; } private static bool HasJsonKey(JSONNode node, string key) { return node != null && node[key] is not JSONLazyCreator; } private static bool GetBool(JSONNode node, string key, bool fallback) { if (node == null || node[key] is JSONLazyCreator) return fallback; return node[key].AsBool; } private static Guid CreateGuid() { return Guid.NewGuid(); } private sealed class LegacyImportStats { public int created; public int skipped; public int failed; } }