diff --git a/RebuildClient/Assets/Scripts/Editor/ActHandling.meta b/RebuildClient/Assets/Scripts/Editor/ActHandling.meta new file mode 100644 index 00000000..cfdf4481 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/ActHandling.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 47dd2ce6a05942dd9b160373aa6cc1dd +timeCreated: 1761678548 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs new file mode 100644 index 00000000..cc205102 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Assets.Scripts.Sprites; +using UnityEditor; +using UnityEditor.AssetImporters; +using UnityEngine; + +namespace Assets.Scripts.Editor.ActHandling +{ + [ScriptedImporter(1, ext:"act", AllowCaching = true)] + public class RoActAssetImporter : ScriptedImporter + { + public string actVersion; + public string filePath; + public string actFileName; + public List animationClips; + + private static void TryIncludeActExtension() + { + if (EditorSettings.projectGenerationUserExtensions.Contains(ActExtension)) return; + var list = EditorSettings.projectGenerationUserExtensions.ToList(); + list.Add(ActExtension); + EditorSettings.projectGenerationUserExtensions = list.ToArray(); + } + + private RoActAsset asset; + private const string ActExtension = "act"; + + public override void OnImportAsset(AssetImportContext ctx) + { + asset = ScriptableObject.CreateInstance(); + Debug.Log($"Loading asset on {ctx.assetPath}"); + asset.Load(ctx.assetPath); + + PopulateImporterFields(); + + ctx.AddObjectToAsset(GUID.Generate().ToString(), asset); + ctx.SetMainObject(asset); + + TryIncludeActExtension(); + } + + private void PopulateImporterFields() + { + actVersion = asset.actVersion; + filePath = asset.filePath; + actFileName = asset.actFileName; + animationClips = asset.animationClips; + } + + // [CustomEditor(typeof(RoActAssetImporter))] + // public sealed class RoActAssetImporterEditor : ScriptedImporterEditor + // { + // public override bool showImportedObject + // { + // get { return false; } + // } + // } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs.meta b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs.meta new file mode 100644 index 00000000..ab568131 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActAssetImporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dee733df1e104d27a6744cdc99e2a7ab +timeCreated: 1761678569 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs new file mode 100644 index 00000000..34c37911 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs @@ -0,0 +1,35 @@ +using Assets.Scripts.Sprites; +using UnityEditor; +using UnityEngine; + +namespace Assets.Scripts.Editor.ActHandling +{ + [CustomEditor(typeof(RoActAsset))] + public class RoActInspector : UnityEditor.Editor + { + private SerializedProperty actName; + private SerializedProperty actVersion; + private SerializedProperty animationClips; + + private void OnEnable() + { + actName = serializedObject.FindProperty("actName"); + actVersion = serializedObject.FindProperty("actVersion"); + animationClips = serializedObject.FindProperty("animationClips"); + } + + public override void OnInspectorGUI() + { + DrawImporterGUI(); + serializedObject.ApplyModifiedProperties(); + } + + private void DrawImporterGUI() + { + EditorGUILayout.PropertyField(actName, new GUIContent("Act Name")); + EditorGUILayout.PropertyField(actVersion, new GUIContent("Act Version")); + EditorGUILayout.PropertyField(animationClips, new GUIContent("Animation Clips")); + + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs.meta b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs.meta new file mode 100644 index 00000000..3696b124 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/ActHandling/RoActInspector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d82d896da7294d2e89a78e2ab0f66199 +timeCreated: 1761678582 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActImporter.cs b/RebuildClient/Assets/Scripts/Editor/ActImporter.cs deleted file mode 100644 index c3772c18..00000000 --- a/RebuildClient/Assets/Scripts/Editor/ActImporter.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Assets.Editor; -using Assets.Scripts; -using Assets.Scripts.MapEditor.Editor; -using UnityEditor; -using UnityEditor.AssetImporters; -using UnityEngine; -using Utility.Editor; - -[ScriptedImporter(1, "act", AllowCaching = true)] -public class ActImporter : ScriptedImporter -{ - public int PaletteCount; - - public override void OnImportAsset(AssetImportContext ctx) - { - // var asset = ScriptableObject.CreateInstance(); - // ctx.AddObjectToAsset(name + " data", asset); - // ctx.SetMainObject(asset); - } - - public static void ImportActFile(string actPath) - { - var dir = Path.GetDirectoryName(actPath); - var baseName = Path.GetFileNameWithoutExtension(actPath); - var rel = dir.Substring(dir.Replace("\\", "/").IndexOfOccurence("/", 2) + 1); - var targetFolder = Path.Combine("Assets/Sprites/Imported", rel).Replace("\\", "/"); - var atlasPath = Path.Combine(targetFolder, "Atlas/", $"{baseName}_atlas.png").Replace("\\", "/"); - //var palettePath = Path.Combine("G:\\Games\\RagnarokJP\\data\\palette\\몸\\costume_1", $"{basename}_0_1.pal"); - var palettePath = Path.Combine(dir, "Palette/"); - var palettes = new List(); - - for (var i = 0; i < 10; i++) - { - var pName = Path.Combine(palettePath, $"{baseName}_{i}_1.pal"); - if (File.Exists(pName)) - palettes.Add(pName); - pName = Path.Combine(palettePath, $"{baseName}_{i}.pal"); - if (File.Exists(pName)) - palettes.Add(pName); - } - - // Debug.Log($"{targetFolder}"); - - if (!Directory.Exists(targetFolder)) - Directory.CreateDirectory(targetFolder); - - var asset = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(asset, Path.Combine(targetFolder, $"{baseName}.asset")); - - var loader = new RagnarokSpriteLoader(); - loader.Load(actPath.Replace(".act", ".spr"), atlasPath, asset, null); - SetUpSpriteData(loader, asset, dir, baseName, baseName); - - for (var i = 0; i < palettes.Count; i++) - { - asset = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(asset, Path.Combine(targetFolder, $"{baseName}_{i}.asset")); - - atlasPath = Path.Combine(targetFolder, "Atlas/", $"{baseName}_{i}_atlas.png").Replace("\\", "/"); - loader = new RagnarokSpriteLoader(); - loader.Load(actPath.Replace(".act", ".spr"), atlasPath, asset, palettes[i]); - SetUpSpriteData(loader, asset, dir, baseName, $"{baseName}_{i}"); - } - - AssetDatabase.SaveAssets(); - } - - private static void SetUpSpriteData(RagnarokSpriteLoader spr, RoSpriteData asset, string basePath, string baseName, string outName) - { - var actName = Path.Combine(basePath, baseName + ".act"); - var imfFile = Path.Combine(RagnarokDirectory.GetRagnarokDataDirectorySafe, "imf/", baseName + ".imf"); - if (!File.Exists(imfFile)) - imfFile = null; - - var actLoader = new RagnarokActLoader(); - var actions = actLoader.Load(spr, actName, imfFile); - - asset.Actions = actions.ToArray(); - asset.Sprites = spr.Sprites.ToArray(); - asset.SpriteSizes = spr.SpriteSizes.ToArray(); - asset.Name = outName; - asset.Atlas = spr.Atlas; - asset.SpritesPerPalette = spr.SpriteFrameCount; - - if (actLoader.Sounds != null) - { - asset.Sounds = new AudioClip[actLoader.Sounds.Length]; - - for (var i = 0; i < asset.Sounds.Length; i++) - { - var s = actLoader.Sounds[i]; - if (s == "atk") - continue; - var sPath = $"Assets/Sounds/{s}"; - if (!File.Exists(sPath)) - sPath = $"Assets/Sounds/{s.Replace(".wav", "")}.ogg"; - - var sound = AssetDatabase.LoadAssetAtPath(sPath); - if (sound == null) - Debug.Log("Could not find sound " + sPath + " for sprite " + baseName); - asset.Sounds[i] = sound; - } - } - - switch (asset.Actions.Length) - { - case 8: - asset.Type = SpriteType.Npc; - break; - case 13: - asset.Type = SpriteType.Npc; - break; - case 32: - asset.Type = SpriteType.ActionNpc; - break; - case 39: //mi gao/increase soil for some reason - case 40: - case 41: //zerom for some reason - asset.Type = SpriteType.Monster; - break; - case 47: //dullahan for some reason - case 48: - asset.Type = SpriteType.Monster2; - break; - case 56: - asset.Type = SpriteType.Monster; //??? - break; - case 64: - asset.Type = SpriteType.Monster; - break; - case 72: - asset.Type = SpriteType.Pet; - break; - } - - var maxExtent = 0f; - var totalWidth = 0f; - var widthCount = 0; - - foreach (var a in asset.Actions) - { - var frameId = 0; - foreach (var f in a.Frames) - { - if (f.IsAttackFrame) - asset.AttackFrameTime = a.Delay * frameId; - - frameId++; - - foreach (var l in f.Layers) - { - if (l.Index == -1) - continue; - var sprite = asset.SpriteSizes[l.Index]; - var y = l.Position.y + sprite.y / 2f; - if (l.Position.x < 0) - y = Mathf.Abs(l.Position.y - sprite.y / 2f); - if (y > maxExtent) - maxExtent = y; - totalWidth += Mathf.Abs(l.Position.x) + sprite.x / 2f; - widthCount++; - } - } - } - - //far better way to get sprite attack timing - if (asset.Type == SpriteType.Monster || asset.Type == SpriteType.Monster2 || asset.Type == SpriteType.Pet) - { - var actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack1); - if (actionId == -1) - actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack2); - if (actionId >= 0) - { - var frames = asset.Actions[actionId].Frames; - var found = false; - - for (var j = 0; j < frames.Length; j++) - { - if (frames[j].IsAttackFrame) - { - asset.AttackFrameTime = j * asset.Actions[actionId].Delay; - found = true; - break; - } - } - - if (!found) - { - var pos = frames.Length - 2; - if (pos < 0) - pos = 0; - asset.AttackFrameTime = pos * asset.Actions[actionId].Delay; - } - } - } - - asset.Size = Mathf.CeilToInt(maxExtent); - asset.AverageWidth = totalWidth / widthCount; - - //ok so this is a giant shitshow. We want to know where to display emotes, cast bars, and npcs above this character - //to do so, we save the highest y value of the first frame of the first action. - //a lot of monsters use a high overhead attack which we don't want to count, so using the idle pose is probably safer - asset.StandingHeight = 20; - try - { - var maxHeight = 0f; - var firstFrame = asset.Actions[0].Frames[0]; - for (var i = 0; i < firstFrame.Layers.Length; i++) - { - var l = firstFrame.Layers[i]; - if (l.Index < 0) - continue; - var curSpr = asset.Sprites[l.Index]; - var height = curSpr.rect.height / 2 - l.Position.y; //y is negative - if (height > maxHeight) - maxHeight = height; - } - - if (maxHeight > asset.StandingHeight) - asset.StandingHeight = maxHeight; - } - catch (Exception) - { - Debug.Log($"Couldn't process standing height for sprite {asset.Name}"); - } - - - } -} - -public class ActPostProcessor : AssetPostprocessor -{ - static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, - bool didDomainReload) - { - foreach (var importedAsset in importedAssets) - { - if (!importedAsset.EndsWith(".act")) - continue; - - var sprName = Path.Combine(Path.GetDirectoryName(importedAsset), Path.GetFileNameWithoutExtension(importedAsset) + ".spr"); - if (!File.Exists(sprName)) - { - Debug.LogError($"Could not load sprite {importedAsset} as it did not have an associated .spr sprite data file."); - continue; - } - - ActImporter.ImportActFile(importedAsset); - } - } -} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/EffectStrImporter.cs b/RebuildClient/Assets/Scripts/Editor/EffectStrImporter.cs index d6215d19..9ade4350 100644 --- a/RebuildClient/Assets/Scripts/Editor/EffectStrImporter.cs +++ b/RebuildClient/Assets/Scripts/Editor/EffectStrImporter.cs @@ -4,6 +4,7 @@ using Assets.Scripts.MapEditor.Editor; using Assets.Scripts.Objects; using RebuildSharedData.ClientTypes; +using Scripts.Editor; using UnityEditor; using UnityEditor.U2D; using UnityEngine; diff --git a/RebuildClient/Assets/Scripts/Editor/ItemIconImporter.cs b/RebuildClient/Assets/Scripts/Editor/ItemIconImporter.cs index 311e3deb..66ea8f6a 100644 --- a/RebuildClient/Assets/Scripts/Editor/ItemIconImporter.cs +++ b/RebuildClient/Assets/Scripts/Editor/ItemIconImporter.cs @@ -4,6 +4,7 @@ using Assets.Scripts.MapEditor.Editor; using Assets.Scripts.Sprites; using RebuildSharedData.ClientTypes; +using Scripts.Editor; using UnityEditor; using UnityEditor.U2D; using UnityEngine; diff --git a/RebuildClient/Assets/Scripts/Editor/RagnarokCopyFromRealClient.cs b/RebuildClient/Assets/Scripts/Editor/RagnarokCopyFromRealClient.cs index 61c6cbfc..1b6b4ef9 100644 --- a/RebuildClient/Assets/Scripts/Editor/RagnarokCopyFromRealClient.cs +++ b/RebuildClient/Assets/Scripts/Editor/RagnarokCopyFromRealClient.cs @@ -1,45 +1,23 @@ -using System; +using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Linq; +using System.Windows.Forms; using Assets.Scripts; using Assets.Scripts.Editor; using Assets.Scripts.MapEditor.Editor; +using Scripts.Editor; using UnityEditor; +using UnityEditor.VersionControl; using UnityEngine; +using Application = UnityEngine.Application; +using Debug = UnityEngine.Debug; namespace Assets.Editor { public class RagnarokCopyFromRealClient : EditorWindow { - public static void TestCopy() - { - var dataDir = RagnarokDirectory.GetRagnarokDataDirectorySafe; - - Func updateHeadName = (str) => str.Replace("머리", ""); - Func updateBodyName = (str) => str.Replace("몸", ""); - - var headPath = Path.Combine(dataDir, "palette/머리"); - var bodyPath = Path.Combine(dataDir, "palette/몸"); - - if (Directory.Exists(headPath)) - { - CopyFolder(headPath, "Assets/Sprites/Characters/HeadFemale/Palette/", false, false, "*_여_*.pal", - updateHeadName); - CopyFolder(headPath, "Assets/Sprites/Characters/HeadMale/Palette/", false, false, "*_남_*.pal", - updateHeadName); - } - - headPath = Path.Combine(dataDir, "palette/머리/costume_1"); - bodyPath = Path.Combine(dataDir, "palette/몸/costume_1"); - - if (Directory.Exists(bodyPath)) - { - CopyFolder(bodyPath, "Assets/Sprites/Characters/BodyFemale/Palette/", false, false, "*_여_*.pal", - updateHeadName); - CopyFolder(bodyPath, "Assets/Sprites/Characters/BodyMale/Palette/", false, false, "*_남_*.pal"); - } - } private static string UpdateSpriteName(string name) { @@ -55,58 +33,16 @@ private static string UpdateSpriteName(string name) return name; } - - - [MenuItem("Ragnarok/TestCopy")] - public static void TestCopy2() - { - var dataDir = RagnarokDirectory.GetRagnarokDataDirectorySafe; - CopyFolder(Path.Combine(dataDir, "sprite/인간족/성직자"), "Assets/Sprites/Weapons/Acolyte/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/궁수"), "Assets/Sprites/Weapons/Archer/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/마법사"), "Assets/Sprites/Weapons/Mage/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/상인"), "Assets/Sprites/Weapons/Merchant/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/초보자"), "Assets/Sprites/Weapons/Novice/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/검사"), "Assets/Sprites/Weapons/Swordsman/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/도둑"), "Assets/Sprites/Weapons/Thief/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/인간족/슈퍼노비스"), "Assets/Sprites/Weapons/SuperNovice/", false, true, - "*", UpdateSpriteName); - - - CopyFolder(Path.Combine(dataDir, "sprite/방패/성직자"), "Assets/Sprites/Shields/Acolyte/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/궁수"), "Assets/Sprites/Shields/Archer/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/마법사"), "Assets/Sprites/Shields/Mage/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/상인"), "Assets/Sprites/Shields/Merchant/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/초보자"), "Assets/Sprites/Shields/Novice/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/검사"), "Assets/Sprites/Shields/Swordsman/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/도둑"), "Assets/Sprites/Shields/Thief/", false, true, "*", - UpdateSpriteName); - CopyFolder(Path.Combine(dataDir, "sprite/방패/슈퍼노비스"), "Assets/Sprites/Shields/SuperNovice/", false, true, - "*", UpdateSpriteName); - } - - [MenuItem("Ragnarok/Copy data from client data folder", priority = 1)] - public static void CopyClientData() + [UnityEditor.MenuItem("Ragnarok/Test New Process", priority = -100)] + public static void TestNewProcess() { + Debug.Log("Testing new process"); var dataDir = RagnarokDirectory.GetRagnarokDataDirectorySafe; if (dataDir == null) { - var prompt = - @"Before you continue, you will need to specify a directory containing the contents of an extracted data.grf. " - + "For this import process to work correctly, the files will need to have been extracted with the right locale and working korean file names."; + const string prompt = @"Before you continue, you will need to specify a directory containing the contents of an extracted data.grf. " + + "For this import process to work correctly, the files will need to have been extracted with the right locale and working korean file names."; if (!EditorUtility.DisplayDialog("Copy from RO Client", prompt, "Continue", "Cancel")) return; @@ -117,27 +53,137 @@ public static void CopyClientData() if (dataDir == null) return; } + Debug.Log("Instantiate new boss"); + Debug.Log("Start process"); + RoDataImportManager.ProcessGrfData(dataDir); + } - bool TestPath(string fileName) + [UnityEditor.MenuItem("Ragnarok/Clear Imported Assets", priority = -99)] + public static void ClearImportedAssets() + { + Debug.Log("Start Clearing Imported Assets"); + try { - if (!File.Exists(Path.Combine(dataDir, fileName))) + AssetDatabase.StartAssetEditing(); + var filesToDelete = AssetDatabase.FindAssets("*", new[] {"Assets/Sprites", "Assets/Sounds"}); + Debug.Log($"Found {filesToDelete.Length} assets to remove"); + foreach (var guid in filesToDelete) { - Debug.LogError( - $"Could not verify client data directory \"{dataDir}\" is valid. File checked: {fileName} "); - return false; + AssetDatabase.DeleteAsset(AssetDatabase.GUIDToAssetPath(guid)); } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + finally + { + AssetDatabase.StopAssetEditing(); + } + } + + [UnityEditor.MenuItem("Ragnarok/Select GRF data to import", priority = -98)] + public static void OpenSelectGrfDataToImportWindow() + { + var window = GetWindow("Select GRF data to import"); + window.minSize = new Vector2(450, 600); + window.Focus(); + } - return true; + public class RagnarokSelectGrfDataToImportWindow : EditorWindow + { + private Vector2 scrollPosition; + private bool[] selections; + private GrfDataFilesCategorized grfData; + + private void OnEnable() + { + grfData = RoDataImportManager.ReadGrfDataFolder(); + selections = new bool[grfData.Categories.Length]; + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Select data to import:", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Select All", GUILayout.Height(20))) + for (int i = 0; i < selections.Length; i++) + selections[i] = true; + if (GUILayout.Button("Unselect All", GUILayout.Height(20))) + for (int i = 0; i < selections.Length; i++) + selections[i] = false; + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + for (var i = 0; i < grfData.Categories.Length; i++) + { + EditorGUILayout.BeginHorizontal(); + selections[i] = EditorGUILayout.ToggleLeft(grfData[i], selections[i]); + EditorGUILayout.LabelField($"imported {grfData.Categories[i].Imported} of {grfData.Categories[i].Count} files", EditorStyles.miniLabel); + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + EditorGUILayout.Space(); + + EditorGUILayout.HelpBox( + "Before you continue, you will need to specify a directory containing the contents of an extracted data.grf. " + + "For this import process to work correctly, the files will need to have been extracted with the right locale and working Korean file names.", + MessageType.Warning + ); + + if (GUILayout.Button("Copy Selected Data", GUILayout.Height(30))) + { + if (selections.Any(x => !x)) + { + var actualGrfDataToProcess = new GrfDataFilesCategorized(); + for (var i = 0; i < selections.Length; i++) + { + if (selections[i]) + actualGrfDataToProcess.Categories[i].AddRange(grfData.Categories[i]); + } + RoDataImportManager.ProcessGrfData(actualGrfDataToProcess); + } + else + { + RoDataImportManager.ProcessGrfData(grfData); + } + } + + } + } + + [UnityEditor.MenuItem("Ragnarok/Copy data from client data folder", priority = 1)] + public static void CopyClientData() + { + string dataDir = RagnarokDirectory.GetRagnarokDataDirectorySafe; + + if (dataDir == null) + { + const string prompt = @"Before you continue, you will need to specify a directory containing the contents of an extracted data.grf. " + + "For this import process to work correctly, the files will need to have been extracted with the right locale and working korean file names."; + + if (!EditorUtility.DisplayDialog("Copy from RO Client", prompt, "Continue", "Cancel")) + return; + + RagnarokDirectory.SetDataDirectory(); + + dataDir = RagnarokDirectory.GetRagnarokDataDirectorySafe; + if (dataDir == null) + return; } if (!TestPath("prontera.gat") || !TestPath(@"texture\워터\water000.jpg")) return; - var prompt2 = @"This import process will copy files from your data folder into this project. " - + "Because this includes converting all maps and objects, expect this process to take more than an hour." - + "\n\nWhen complete, the lighting window will load where you can bake the lighting for all the scenes (accessible via 'Ragnarok->Lighting Manager'). " - + "You will also need to manually copy over your BGM into the music folder if you want music." - + "\n\nLastly, before you run you will need to use 'Ragnarok->Update Addressables' to make sure everything can load."; + const string prompt2 = @"This import process will copy files from your data folder into this project. " + + "Because this includes converting all maps and objects, expect this process to take more than an hour." + + "\n\nWhen complete, the lighting window will load where you can bake the lighting for all the scenes (accessible via 'Ragnarok->Lighting Manager'). " + + "You will also need to manually copy over your BGM into the music folder if you want music." + + "\n\nLastly, before you run you will need to use 'Ragnarok->Update Addressables' to make sure everything can load."; if (!EditorUtility.DisplayDialog("Copy from RO Client", prompt2, "Continue", "Cancel")) return; @@ -291,17 +337,26 @@ bool TestPath(string fileName) ItemIconImporter.ImportItems(); RoLightingManagerWindow.CreateOrOpen(); + return; + + bool TestPath(string fileName) + { + if (File.Exists(Path.Combine(dataDir, fileName))) return true; + Debug.LogError( + $"Could not verify client data directory \"{dataDir}\" is valid. File checked: {fileName} "); + return false; + } } - [MenuItem("Ragnarok/Select data to copy from client data folder", priority = 2)] + [UnityEditor.MenuItem("Ragnarok/Select data to copy from client data folder", priority = 2)] public static void ShowCopyClientDataWindow() { - var window = GetWindow("Copy Client Data"); + var window = GetWindow("Prepare Client Data"); window.minSize = new Vector2(450, 600); window.Focus(); } - public class RagnarokCopyFromRealClientWindow : EditorWindow + public class RagnarokPrepareClientDataFromGrfDataWindow : EditorWindow { private struct CopyCategory { @@ -333,19 +388,7 @@ private void OnEnable() } // sanity checks - bool TestPath(string fileName) - { - var full = Path.Combine(dataDir, fileName); - if (!File.Exists(full)) - { - Debug.LogError($"Invalid client data directory: missing {fileName}"); - return false; - } - - return true; - } - - if (!TestPath("prontera.gat") || !TestPath("texture\\워터\\water000.jpg")) + if (!TestPath("prontera.gat") || !TestPath(@"texture\워터\water000.jpg")) return; // Define each copy category @@ -356,109 +399,118 @@ bool TestPath(string fileName) Label = "Sounds (WAV)", Execute = () => CopyFolder(Path.Combine(dataDir, "wav/"), "Assets/Sounds/", recursive: true), IsAlreadyImported = () => - Directory.Exists("Assets/Sounds") && Directory - .GetFiles("Assets/Sounds", "*.wav", SearchOption.AllDirectories).Any() + Directory.Exists("Assets/Sounds") && + Directory.GetFiles("Assets/Sounds", + "*.wav", SearchOption.AllDirectories).Any() }, new CopyCategory { Label = "Monster Sprites", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/몬스터"), "Assets/Sprites/Monsters/"), + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/몬스터"), "Assets/Sprites/Monsters/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Monsters") && Directory.GetFiles("Assets/Sprites/Monsters", + Directory.Exists("Assets/Sprites/Monsters") && + Directory.GetFiles("Assets/Sprites/Monsters", "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "Headgear Sprites (Male)", Execute = () => - CopyFolder(Path.Combine(dataDir, "sprite/악세사리/남"), "Assets/Sprites/Headgear/Male/"), + ImportFromGrfFolder(Path.Combine(dataDir, "sprite/악세사리/남"), "Assets/Sprites/Headgear/Male/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Headgear/Male") && Directory - .GetFiles("Assets/Sprites/Headgear/Male", "*.spr", SearchOption.TopDirectoryOnly).Any() + Directory.Exists("Assets/Sprites/Headgear/Male") && + Directory.GetFiles("Assets/Sprites/Headgear/Male", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "Headgear Sprites (Female)", Execute = () => - CopyFolder(Path.Combine(dataDir, "sprite/악세사리/여"), "Assets/Sprites/Headgear/Female/"), + ImportFromGrfFolder(Path.Combine(dataDir, "sprite/악세사리/여"), "Assets/Sprites/Headgear/Female/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Headgear/Female") && Directory - .GetFiles("Assets/Sprites/Headgear/Female", "*.spr", SearchOption.TopDirectoryOnly) - .Any() + Directory.Exists("Assets/Sprites/Headgear/Female") && + Directory.GetFiles("Assets/Sprites/Headgear/Female", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "NPC Sprites", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/npc"), "Assets/Sprites/Npcs/"), + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/npc"), "Assets/Sprites/Npcs/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Npcs") && Directory - .GetFiles("Assets/Sprites/Npcs", "*.spr", SearchOption.TopDirectoryOnly).Any() + Directory.Exists("Assets/Sprites/Npcs") && + Directory.GetFiles("Assets/Sprites/Npcs", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "Effect Sprites", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/이팩트"), "Assets/Sprites/Effects/"), + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/이팩트"), "Assets/Sprites/Effects/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Effects") && Directory.GetFiles("Assets/Sprites/Effects", + Directory.Exists("Assets/Sprites/Effects") && + Directory.GetFiles("Assets/Sprites/Effects", "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { - Label = "Head Palettes (Female)", - Execute = () => CopyFolder(Path.Combine(dataDir, "palette/몸"), - "Assets/Sprites/Characters/HeadFemale/", recursive: false, maleFemaleSplit: false, - filter: "*_여_*.pal"), - IsAlreadyImported = () => Directory.GetFiles("Assets/Sprites/Characters/HeadFemale", - "*_여_*.pal", SearchOption.TopDirectoryOnly).Any() - }, - new CopyCategory - { - Label = "Head Palettes (Male)", - Execute = () => CopyFolder(Path.Combine(dataDir, "palette/몸"), - "Assets/Sprites/Characters/HeadMale/", recursive: false, maleFemaleSplit: false, - filter: "*_남_*.pal"), - IsAlreadyImported = () => Directory.GetFiles("Assets/Sprites/Characters/HeadMale", "*_남_*.pal", - SearchOption.TopDirectoryOnly).Any() - }, - new CopyCategory - { - Label = "Character Heads (Male)", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/인간족/머리통/남"), - "Assets/Sprites/Characters/HeadMale/"), + Label = "Character Heads (Male) - Import", + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/인간족/머리통/남"), + "Assets/Sprites/Characters/HeadMale/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Characters/HeadMale") && Directory - .GetFiles("Assets/Sprites/Characters/HeadMale", "*.spr", SearchOption.TopDirectoryOnly) - .Any() + Directory.Exists("Assets/Sprites/Characters/HeadMale") && + Directory.GetFiles("Assets/Sprites/Characters/HeadMale", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { - Label = "Character Heads (Female)", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/인간족/머리통/여"), + Label = "Character Heads (Female) - Import", + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/인간족/머리통/여"), "Assets/Sprites/Characters/HeadFemale/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Characters/HeadFemale") && Directory - .GetFiles("Assets/Sprites/Characters/HeadFemale", "*.spr", - SearchOption.TopDirectoryOnly).Any() + Directory.Exists("Assets/Sprites/Characters/HeadFemale") && + Directory.GetFiles("Assets/Sprites/Characters/HeadFemale", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "Character Bodies (Male)", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/인간족/몸통/남"), + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/인간족/몸통/남"), "Assets/Sprites/Characters/BodyMale/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Characters/BodyMale") && Directory - .GetFiles("Assets/Sprites/Characters/BodyMale", "*.spr", SearchOption.TopDirectoryOnly) - .Any() + Directory.Exists("Assets/Sprites/Characters/BodyMale") && + Directory.GetFiles("Assets/Sprites/Characters/BodyMale", + "*.spr", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { Label = "Character Bodies (Female)", - Execute = () => CopyFolder(Path.Combine(dataDir, "sprite/인간족/몸통/여"), + Execute = () => ImportFromGrfFolder(Path.Combine(dataDir, "sprite/인간족/몸통/여"), "Assets/Sprites/Characters/BodyFemale/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Characters/BodyFemale") && Directory - .GetFiles("Assets/Sprites/Characters/BodyFemale", "*.spr", - SearchOption.TopDirectoryOnly).Any() + Directory.Exists("Assets/Sprites/Characters/BodyFemale") && + Directory.GetFiles("Assets/Sprites/Characters/BodyFemale", + "*.spr", SearchOption.TopDirectoryOnly).Any() + }, + new CopyCategory + { + Label = "Head Palettes (Female)", + Execute = () => CopyFolder(Path.Combine(dataDir, "palette/몸"), + "Assets/Sprites/Characters/HeadFemale/", recursive: false, maleFemaleSplit: false, + filter: "*_여_*.pal"), + IsAlreadyImported = () => + Directory.Exists(@"Assets/Sprites/Characters/HeadFemale") && + Directory.GetFiles("Assets/Sprites/Characters/HeadFemale", + "*_여_*.pal",SearchOption.TopDirectoryOnly).Any() + }, + new CopyCategory + { + Label = "Head Palettes (Male)", + Execute = () => CopyFolder(Path.Combine(dataDir, "palette/몸"), + "Assets/Sprites/Characters/HeadMale/", recursive: false, maleFemaleSplit: false, + filter: "*_남_*.pal"), + IsAlreadyImported = () => + Directory.Exists(@"Assets/Sprites/Characters/HeadMale") && + Directory.GetFiles("Assets/Sprites/Characters/HeadMale", + "*_남_*.pal", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory { @@ -466,7 +518,8 @@ bool TestPath(string fileName) Execute = () => CopyFolder(Path.Combine(dataDir, "texture/유저인터페이스/illust"), "Assets/Sprites/Cutins/"), IsAlreadyImported = () => - Directory.Exists("Assets/Sprites/Cutins") && Directory.GetFiles("Assets/Sprites/Cutins", + Directory.Exists("Assets/Sprites/Cutins") && + Directory.GetFiles("Assets/Sprites/Cutins", "*.bmp", SearchOption.TopDirectoryOnly).Any() }, new CopyCategory @@ -474,27 +527,19 @@ bool TestPath(string fileName) Label = "Weapon Sprites (All Classes)", Execute = () => { - var jobs = new[] + // TODO: Extract these into an importable asset so that these strings are centralized + var jobs = new (string kr, string en)[] { - "성직자", "궁수", "마법사", "상인", "초보자", "검사", "도둑", "슈퍼노비스", - "기사", "위저드", "프리스트", "헌터", "어세신", "제철공", "크루세이더", "세이지", - "바드", "무희", "몽크", "로그", "연금술사", "운영자", "신페코크루세이더", "페코페코_기사" + ("성직자", "Acolyte"), ("궁수", "Archer"), ("마법사", "Mage"), ("상인", "Merchant"), ("초보자", "Novice"), ("검사", "Swordsman"), + ("도둑", "Thief"), ("슈퍼노비스", "SuperNovice"), ("기사", "Knight"), ("위저드", "Wizard"), ("프리스트", "Priest"), ("헌터", "Hunter"), + ("어세신", "Assassin"), ("제철공", "Blacksmith"), ("크루세이더", "Crusader"), ("세이지", "Sage"), ("바드", "Bard"), ("무희바지", "Dancer"), + ("몽크", "Monk"), ("로그", "Rogue"), ("연금술사", "Alchemist"), ("운영자", "GameMaster"), ("신페코크루세이더", "PecoCrusader"), ("페코페코_기사_남", "PecoKnight") }; - var outputs = new[] - { - "Acolyte", "Archer", "Mage", "Merchant", "Novice", "Swordsman", "Thief", "SuperNovice", - "Knight", "Wizard", "Priest", "Hunter", "Assassin", "Blacksmith", "Crusader", "Sage", - "Bard", "Dancer", "Monk", "Rogue", "Alchemist", "GameMaster", "PecoCrusader", "PecoKnight" - }; - for (int i = 0; i < jobs.Length; i++) - CopyFolder(Path.Combine(dataDir, $"sprite/인간족/{jobs[i]}/"), - $"Assets/Sprites/Weapons/{outputs[i]}/", false, true, "*", - (p) => - { - for(var j = 0; j < jobs.Length; j++) - p = p.Replace(jobs[j], outputs[j]); - return p.Replace("여_", "F_").Replace("남_", "M_"); - }); + foreach ((string kr, string en) job in jobs) + ImportFromGrfFolder(Path.Combine(dataDir, $"sprite/인간족/{job.kr}/"), + $"Assets/Sprites/Weapons/{job.en}/", false, true, "*", + (p) => p.Replace($"{job.kr}_", $"{job.en}_").Replace("여_", "F_") + .Replace("남_", "M_")); }, IsAlreadyImported = () => Directory.Exists("Assets/Sprites/Weapons") && @@ -505,26 +550,16 @@ bool TestPath(string fileName) Label = "Shield Sprites (All Classes)", Execute = () => { - var jobs = new[] - { - "성직자", "궁수", "마법사", "상인", "초보자", "검사", "도둑", "슈퍼노비스", - "기사", "위저드", "프리스트", "헌터", "어세신", "제철공", "크루세이더", "세이지", - "바드", "무희", "몽크", "로그", "연금술사", "운영자", "신페코크루세이더", "페코페코_기사" - }; - var outputs = new[] + var jobs = new (string kr, string en)[] { - "Acolyte", "Archer", "Mage", "Merchant", "Novice", "Swordsman", "Thief", "SuperNovice", - "Knight", "Wizard", "Priest", "Hunter", "Assassin", "Blacksmith", "Crusader", "Sage", - "Bard", "Dancer", "Monk", "Rogue", "Alchemist", "GameMaster", "PecoCrusader", "PecoKnight" + ("성직자", "Acolyte"), ("궁수", "Archer"), ("마법사", "Mage"), ("상인", "Merchant"), ("초보자", "Novice"), ("검사", "Swordsman"), + ("도둑", "Thief"), ("슈퍼노비스", "SuperNovice"), ("기사", "Knight"), ("위저드", "Wizard"), ("프리스트", "Priest"), ("헌터", "Hunter"), + ("어세신", "Assassin"), ("제철공", "Blacksmith"), ("크루세이더", "Crusader"), ("세이지", "Sage"), ("바드", "Bard"), ("무희바지", "Dancer"), + ("몽크", "Monk"), ("로그", "Rogue"), ("연금술사", "Alchemist"), ("운영자", "GameMaster"), ("신페코크루세이더", "PecoCrusader"), ("페코페코_기사_남", "PecoKnight") }; - for (int i = 0; i < jobs.Length; i++) - CopyFolder(Path.Combine(dataDir, $"sprite/방패/{jobs[i]}/"), - $"Assets/Sprites/Shields/{outputs[i]}/", false, true, "*", (p) => - { - for(var j = 0; j < jobs.Length; j++) - p = p.Replace(jobs[j], outputs[j]); - return p.Replace("여_", "F_").Replace("남_", "M_"); - }); + foreach ((string kr, string en) job in jobs) + ImportFromGrfFolder(Path.Combine(dataDir, $"sprite/방패/{job.kr}/"), + $"Assets/Sprites/Shields/{job.kr}/", false, true); }, IsAlreadyImported = () => Directory.Exists("Assets/Sprites/Shields") && @@ -552,6 +587,16 @@ bool TestPath(string fileName) // Initialize selections based on whether already imported selections = categories.Select(cat => !cat.IsAlreadyImported()).ToArray(); + return; + + + bool TestPath(string fileName) + { + string full = Path.Combine(dataDir, fileName); + if (File.Exists(full)) return true; + Debug.LogError($"Invalid client data directory: missing {fileName}"); + return false; + } } private void OnGUI() @@ -586,53 +631,79 @@ private void OnGUI() if (GUILayout.Button("Copy Selected Data", GUILayout.Height(30))) { - int count = 0; for (int i = 0; i < categories.Count; i++) { if (!selections[i]) continue; try { categories[i].Execute(); - count++; } catch (Exception ex) { - Debug.LogError( - $"[Copy Failure] Category = '{categories[i].Label}'\n" + + throw new FileLoadException( + $"[Copy Failure] Category = '{categories[i].Label}" + $"Exception message: {ex.Message}\n" + - $"Full stack trace:\n{ex}" - ); + $"Full stack trace:\n{ex}"); } } AssetDatabase.Refresh(); - Debug.Log($"Copied {count} categories."); + //Debug.Log($"Copied {count} categories."); } } // Reuse copy helpers from original - private static bool CopyFolder(string src, string dest, bool recursive = false, + private static void ImportFromGrfFolder(string srcFolderPath, string destFolderPath, bool recursive = false, + bool maleFemaleSplit = false, string searchPattern = "*", Func updateFileName = null) + { + if (!Directory.Exists(srcFolderPath)) + throw new DirectoryNotFoundException($"ImportFromGrfFolder: source directory not found: {srcFolderPath}"); + + SearchOption searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + string[] srcFilePathArray = Directory.GetFiles(srcFolderPath, searchPattern, searchOption); + if (srcFilePathArray.Length == 0) + throw new FileNotFoundException($"CopyFolder: no files in '{srcFolderPath}' matching '{searchPattern}'"); + + foreach (string srcFilePath in srcFilePathArray) + { + string relativePath = Path.GetRelativePath(srcFolderPath, srcFilePath); + string assetFilePath = Path.Combine(destFolderPath, relativePath); + if (maleFemaleSplit) + { + if (relativePath.Contains("_남_")) + assetFilePath = Path.Combine(destFolderPath, "Male", relativePath); + if (relativePath.Contains("_여_")) + assetFilePath = Path.Combine(destFolderPath, "Female", relativePath); + } + + if (updateFileName != null) assetFilePath = updateFileName(assetFilePath); + string assetDirectoryPath = Path.GetDirectoryName(assetFilePath) ?? throw new NoNullAllowedException("Invalid directory on {destPath}"); + Directory.CreateDirectory(assetDirectoryPath); + string fileExtension = Path.GetExtension(srcFilePath); + if (!fileExtension.Equals(".spr", StringComparison.OrdinalIgnoreCase)) continue; + //Debug.Log($"Importing file {srcFilePath} to {assetDirectoryPath}"); + RoDataImportManager.ImportActFile(srcFilePath.Replace(".spr",".act"), assetDirectoryPath); + } + } + private static void CopyFolder(string src, string dest, bool recursive = false, bool maleFemaleSplit = false, string filter = "*", Func updateFileName = null) { if (!Directory.Exists(src)) { - Debug.LogError($"CopyFolder: source directory not found: {src}"); - return false; + throw new DirectoryNotFoundException($"CopyFolder: source directory not found: {src}"); } var opt = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var files = Directory.GetFiles(src, filter, opt); if (files.Length == 0) { - Debug.LogWarning($"CopyFolder: no files in '{src}' matching '{filter}'"); - return false; + throw new FileNotFoundException($"CopyFolder: no files in '{src}' matching '{filter}'"); } foreach (var path in Directory.GetFiles(src, filter, opt)) { var rel = Path.GetRelativePath(src, path); var destPath = Path.Combine(dest, rel); - Debug.Log($"CopyFolder: {path} → {destPath}"); if (maleFemaleSplit) { if (rel.Contains("_남_")) @@ -661,64 +732,56 @@ private static bool CopyFolder(string src, string dest, bool recursive = false, File.Copy(path, destPath, true); } } - - return true; } private static void CopySingleFile(string src, string dest) { - Debug.Log($"[CopySingleFile] Attempting to copy:\n src = {src}\n dest = {dest}"); + //Debug.Log($"[CopySingleFile] Attempting to copy:\n src = {src}\n dest = {dest}"); string destPath; bool looksLikeFolder = dest.EndsWith("/") || dest.EndsWith("\\") || Directory.Exists(dest); - if (looksLikeFolder && (dest.StartsWith("Assets/") || dest.StartsWith("Assets\\"))) + switch (looksLikeFolder) { - string folder = dest.TrimEnd('\\', '/'); + case true when (dest.StartsWith("Assets/") || dest.StartsWith("Assets\\")): + { + string folder = dest.TrimEnd('\\', '/'); - string assetsSubpath = folder.Substring("Assets/".Length); - destPath = Path.Combine(Application.dataPath, assetsSubpath); + string assetsSubpath = folder.Substring("Assets/".Length); + destPath = Path.Combine(Application.dataPath, assetsSubpath); - var fileName = Path.GetFileName(src); - destPath = Path.Combine(destPath, fileName); - } - else if (looksLikeFolder) - { - string folder = dest.TrimEnd('\\', '/'); - var fileName = Path.GetFileName(src); - destPath = Path.Combine(folder, fileName); - } - else - { - destPath = dest; + string fileName = Path.GetFileName(src); + destPath = Path.Combine(destPath, fileName); + break; + } + case true: + { + string folder = dest.TrimEnd('\\', '/'); + string fileName = Path.GetFileName(src); + destPath = Path.Combine(folder, fileName); + break; + } + default: + destPath = dest; + break; } var parentDir = Path.GetDirectoryName(destPath); if (string.IsNullOrEmpty(parentDir)) - { - Debug.LogError($"[CopySingleFile] Invalid destination: {destPath}"); - return; - } + throw new DirectoryNotFoundException($"CopySingleFile: parent directory not found: {destPath}"); - if (!Directory.Exists(parentDir)) - { - Debug.Log($"[CopySingleFile] Creating directory: {parentDir}"); - Directory.CreateDirectory(parentDir); - } + Directory.CreateDirectory(parentDir); if (!File.Exists(src)) - { - Debug.LogError($"[CopySingleFile] Source not found: {src}"); - return; - } + throw new FileNotFoundException($"CopySingleFile: source file not found: {src}"); try { File.Copy(src, destPath, overwrite: true); - Debug.Log($"[CopySingleFile] Successfully copied {src} → {destPath}"); + //Debug.Log($"[CopySingleFile] Successfully copied {src} → {destPath}"); } catch (Exception ex) { - Debug.LogError($"[CopySingleFile] Failed to copy {src} → {destPath}\nException: {ex}"); + throw new FileLoadException($"[CopySingleFile] Failed to copy {src} → {destPath}\nException: {ex}"); } } } @@ -777,24 +840,20 @@ private static void CopySingleFile(string src, string dest) } } - private static bool CopyFolder(string src, string dest, bool recursive = false, bool maleFemaleSplit = false, + private static void CopyFolder(string sourcePath, string dest, bool recursive = false, + bool maleFemaleSplit = false, string filter = "*", Func updateFileName = null) { var opt = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - var hasFiles = false; + Directory.CreateDirectory(dest); //CreateDirectory already checks for directory existence - if (!Directory.Exists(dest)) - Directory.CreateDirectory(dest); - - foreach (var path in Directory.GetFiles(src, filter, opt)) + foreach (var path in Directory.GetFiles(sourcePath, filter, opt)) { - var rel = Path.GetRelativePath(src, path); + var rel = Path.GetRelativePath(sourcePath, path); var destPath = Path.Combine(dest, rel); - hasFiles = true; - if (maleFemaleSplit) { if (rel.Contains("_남_")) @@ -810,11 +869,9 @@ private static bool CopyFolder(string src, string dest, bool recursive = false, destPath = updateFileName(destPath); var outDir = Path.GetDirectoryName(destPath); - if (!Directory.Exists(outDir)) - Directory.CreateDirectory(outDir); + Directory.CreateDirectory(outDir ?? throw new InvalidOperationException("output directory must exist")); var ext = Path.GetExtension(path); - var fName = Path.GetFileName(path); if (ext == ".bmp") { @@ -826,7 +883,6 @@ private static bool CopyFolder(string src, string dest, bool recursive = false, ti.crunchedCompression = false; ti.textureCompression = TextureImporterCompression.CompressedHQ; }); - //TextureImportHelper.GetOrImportTextureToProject(fName, path, destPath); } else { @@ -836,8 +892,6 @@ private static bool CopyFolder(string src, string dest, bool recursive = false, } AssetDatabase.Refresh(); - - return hasFiles; } } } \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs b/RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs new file mode 100644 index 00000000..0cd5b5fe --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace Scripts.Editor +{ + public static class RagnarokDirectory + { + // These definitions could possibly come from an external file later on + private static readonly Dictionary RelativeDirectoryConversionSounds = new() + { + {Path.Combine("wav"), Path.Combine("Sounds")}, + {Path.Combine("wav", "effect"), Path.Combine("Sounds", "Effects")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionMonsters = new() + { + {Path.Combine("sprite", "몬스터"), Path.Combine("Sprites", "Monsters")} + }; + + private static readonly Dictionary RelativeDirectoryConversionPlayerHead = new() + { + {Path.Combine("sprite", "인간족", "머리통", "남"), Path.Combine("Sprites", "Characters", "HeadMale")}, + {Path.Combine("sprite", "인간족", "머리통", "여"), Path.Combine("Sprites", "Characters", "HeadFemale")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionPlayerBody = new() + { + {Path.Combine("sprite", "인간족", "몸통", "남"), Path.Combine("Sprites", "Characters", "BodyMale")}, + {Path.Combine("sprite", "인간족", "몸통", "여"), Path.Combine("Sprites", "Characters", "BodyFemale")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionPlayerHeadgear = new() + { + {Path.Combine("sprite", "악세사리", "남"), Path.Combine("Sprites", "Headgear", "Male")}, + {Path.Combine("sprite", "악세사리", "여"), Path.Combine("Sprites", "Headgear", "Female")}, + + }; + + private static readonly Dictionary RelativeDirectoryConversionWeapons = new() + { + {Path.Combine("sprite", "인간족", "성직자"), Path.Combine("Sprites", "Weapons", "Acolyte")}, + {Path.Combine("sprite", "인간족", "궁수"), Path.Combine("Sprites", "Weapons", "Archer")}, + {Path.Combine("sprite", "인간족", "마법사"), Path.Combine("Sprites", "Weapons", "Mage")}, + {Path.Combine("sprite", "인간족", "상인"), Path.Combine("Sprites", "Weapons", "Merchant")}, + {Path.Combine("sprite", "인간족", "초보자"), Path.Combine("Sprites", "Weapons", "Novice")}, + {Path.Combine("sprite", "인간족", "검사"), Path.Combine("Sprites", "Weapons", "Swordsman")}, + {Path.Combine("sprite", "인간족", "도둑"), Path.Combine("Sprites", "Weapons", "Thief")}, + {Path.Combine("sprite", "인간족", "슈퍼노비스"), Path.Combine("Sprites", "Weapons", "SuperNovice")}, + {Path.Combine("sprite", "인간족", "기사"), Path.Combine("Sprites", "Weapons", "Knight")}, + {Path.Combine("sprite", "인간족", "위저드"), Path.Combine("Sprites", "Weapons", "Wizard")}, + {Path.Combine("sprite", "인간족", "프리스트"), Path.Combine("Sprites", "Weapons", "Priest")}, + {Path.Combine("sprite", "인간족", "헌터"), Path.Combine("Sprites", "Weapons", "Hunter")}, + {Path.Combine("sprite", "인간족", "어세신"), Path.Combine("Sprites", "Weapons", "Assassin")}, + {Path.Combine("sprite", "인간족", "제철공"), Path.Combine("Sprites", "Weapons", "Blacksmith")}, + {Path.Combine("sprite", "인간족", "크루세이더"), Path.Combine("Sprites", "Weapons", "Crusader")}, + {Path.Combine("sprite", "인간족", "세이지"), Path.Combine("Sprites", "Weapons", "Sage")}, + {Path.Combine("sprite", "인간족", "바드"), Path.Combine("Sprites", "Weapons", "Bard")}, + {Path.Combine("sprite", "인간족", "무희바지"), Path.Combine("Sprites", "Weapons", "Dancer")}, + {Path.Combine("sprite", "인간족", "몽크"), Path.Combine("Sprites", "Weapons", "Monk")}, + {Path.Combine("sprite", "인간족", "로그"), Path.Combine("Sprites", "Weapons", "Rogue")}, + {Path.Combine("sprite", "인간족", "연금술사"), Path.Combine("Sprites", "Weapons", "Alchemist")}, + {Path.Combine("sprite", "인간족", "운영자"), Path.Combine("Sprites", "Weapons", "GameMaster")}, + {Path.Combine("sprite", "인간족", "신페코크루세이더"), Path.Combine("Sprites", "Weapons", "PecoCrusader")}, + {Path.Combine("sprite", "인간족", "페코페코_기사_남"), Path.Combine("Sprites", "Weapons", "PecoKnight")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionShields = new() + { + {Path.Combine("sprite", "방패", "성직자"), Path.Combine("Sprites", "Shields", "Acolyte")}, + {Path.Combine("sprite", "방패", "궁수"), Path.Combine("Sprites", "Shields", "Archer")}, + {Path.Combine("sprite", "방패", "마법사"), Path.Combine("Sprites", "Shields", "Mage")}, + {Path.Combine("sprite", "방패", "상인"), Path.Combine("Sprites", "Shields", "Merchant")}, + {Path.Combine("sprite", "방패", "초보자"), Path.Combine("Sprites", "Shields", "Novice")}, + {Path.Combine("sprite", "방패", "검사"), Path.Combine("Sprites", "Shields", "Swordsman")}, + {Path.Combine("sprite", "방패", "도둑"), Path.Combine("Sprites", "Shields", "Thief")}, + {Path.Combine("sprite", "방패", "슈퍼노비스"), Path.Combine("Sprites", "Shields", "SuperNovice")}, + {Path.Combine("sprite", "방패", "기사"), Path.Combine("Sprites", "Shields", "Knight")}, + {Path.Combine("sprite", "방패", "위저드"), Path.Combine("Sprites", "Shields", "Wizard")}, + {Path.Combine("sprite", "방패", "프리스트"), Path.Combine("Sprites", "Shields", "Priest")}, + {Path.Combine("sprite", "방패", "헌터"), Path.Combine("Sprites", "Shields", "Hunter")}, + {Path.Combine("sprite", "방패", "어세신"), Path.Combine("Sprites", "Shields", "Assassin")}, + {Path.Combine("sprite", "방패", "제철공"), Path.Combine("Sprites", "Shields", "Blacksmith")}, + {Path.Combine("sprite", "방패", "크루세이더"), Path.Combine("Sprites", "Shields", "Crusader")}, + {Path.Combine("sprite", "방패", "세이지"), Path.Combine("Sprites", "Shields", "Sage")}, + {Path.Combine("sprite", "방패", "바드"), Path.Combine("Sprites", "Shields", "Bard")}, + {Path.Combine("sprite", "방패", "무희바지"), Path.Combine("Sprites", "Shields", "Dancer")}, + {Path.Combine("sprite", "방패", "몽크"), Path.Combine("Sprites", "Shields", "Monk")}, + {Path.Combine("sprite", "방패", "로그"), Path.Combine("Sprites", "Shields", "Rogue")}, + {Path.Combine("sprite", "방패", "연금술사"), Path.Combine("Sprites", "Shields", "Alchemist")}, + {Path.Combine("sprite", "방패", "운영자"), Path.Combine("Sprites", "Shields", "GameMaster")}, + {Path.Combine("sprite", "방패", "신페코크루세이더"), Path.Combine("Shields", "PecoCrusader")}, + {Path.Combine("sprite", "방패", "페코페코_기사_남"), Path.Combine("Sprites", "Shields", "PecoKnight")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionNpc = new() + { + {Path.Combine("sprite", "npc"), Path.Combine("Sprites", "Npcs")}, + }; + + private static readonly Dictionary RelativeDirectoryConversionPlayerPalette = new() + { + {Path.Combine("palette", "몸"), Path.Combine("Sprites", "Characters")}, //Need way to separate male/female since the split is on files and not directory + }; + + private static readonly Dictionary RelativeDirectoryConversionCutins = new() + { + {Path.Combine("texture", "유저인터페이스", "illust"), Path.Combine("Sprites", "Cutins")} + }; + + public static readonly string[] ExpectedMiscFiles = + { + Path.Combine("sprite", "cursors.act"), Path.Combine("sprite", "cursors.spr"), + Path.Combine("sprite", "이팩트", "emotion.act"), Path.Combine("sprite", "이팩트", "emotion.spr"), + Path.Combine("sprite", "이팩트", "숫자.act"), Path.Combine("sprite", "이팩트", "숫자.spr") + }; + + public static readonly string[] PalPlayerDirectories = RelativeDirectoryConversionPlayerPalette.Keys.ToArray(); + public static readonly string[] ImgCutinDirectories = RelativeDirectoryConversionCutins.Keys.ToArray(); + + public static readonly string[][] ActDirectories = + { + RelativeDirectoryConversionMonsters.Keys.ToArray(), + RelativeDirectoryConversionPlayerHead.Keys.ToArray(), + RelativeDirectoryConversionPlayerBody.Keys.ToArray(), + RelativeDirectoryConversionPlayerHeadgear.Keys.ToArray(), + RelativeDirectoryConversionWeapons.Keys.ToArray(), + RelativeDirectoryConversionShields.Keys.ToArray(), + RelativeDirectoryConversionNpc.Keys.ToArray(), + }; + + public enum ActDirectoriesID + { + ActMonstersDirectories = 0, + ActPlayerHeadDirectories, + ActPlayerBodyDirectories, + ActPlayerHeadgearDirectories, + ActWeaponsDirectories, + ActShieldsDirectories, + ActNpcDirectories + } + + public static readonly Dictionary RelativeDirectoryConversion = new Dictionary() + .Concat(RelativeDirectoryConversionSounds) + .Concat(RelativeDirectoryConversionMonsters) + .Concat(RelativeDirectoryConversionPlayerHeadgear) + .Concat(RelativeDirectoryConversionPlayerHead) + .Concat(RelativeDirectoryConversionPlayerBody) + .Concat(RelativeDirectoryConversionPlayerPalette) + .Concat(RelativeDirectoryConversionWeapons) + .Concat(RelativeDirectoryConversionShields) + .Concat(RelativeDirectoryConversionNpc) + .Concat(RelativeDirectoryConversionCutins) + .ToDictionary(entry => entry.Key, entry => entry.Value); + + public static string GetRagnarokDataDirectory + { + get + { + var path = EditorPrefs.GetString("RagnarokDataPath", null); + return path == null ? throw new DirectoryNotFoundException("You must set a ragnarok data directory first!") : Path.Combine(path); + } + } + + //alternative that does not throw an exception + public static string GetRagnarokDataDirectorySafe + { + get + { + var path = EditorPrefs.GetString("RagnarokDataPath", null); + return Path.Combine(path); + } + } + + + [MenuItem("Ragnarok/Set Ragnarok Data Directory", priority = 0)] + public static void SetDataDirectory() + { + var defaultName = "Data"; + var oldPath = EditorPrefs.GetString("RagnarokDataPath", null); + if (!string.IsNullOrWhiteSpace(oldPath) && Directory.Exists(oldPath)) + { + var di = new DirectoryInfo(oldPath); + oldPath = di.Parent?.FullName; + defaultName = di.Name; + } + var path = EditorUtility.SaveFolderPanel("Locate Ragnarok Data Folder", oldPath, defaultName); + if (Directory.Exists(path)) + { + EditorPrefs.SetString("RagnarokDataPath", path); + Debug.Log("Ragnarok data directory set to: " + path); + } + else + Debug.LogWarning("Failed to set data directory. Using old directory: " + EditorPrefs.GetString("RagnarokDataPath", null)); + } + + [MenuItem("Ragnarok/Open Ragnarok Data Directory", priority = 1)] + public static void OpenDataDirectory() + { + var oldPath = EditorPrefs.GetString("RagnarokDataPath", null); + EditorUtility.RevealInFinder(oldPath); + } + } +} diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs.meta b/RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs.meta similarity index 83% rename from RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs.meta rename to RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs.meta index 3ff50a6d..a83c0422 100644 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs.meta +++ b/RebuildClient/Assets/Scripts/Editor/RagnarokDirectory.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1dc871c188364c242a87f7c51a178122 +guid: 0cdaea6247408964597056fcb014b4dd MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/RebuildClient/Assets/Scripts/Editor/RagnarokSpriteLoader.cs b/RebuildClient/Assets/Scripts/Editor/RagnarokSpriteLoader.cs index f9f1fb4a..72347c00 100644 --- a/RebuildClient/Assets/Scripts/Editor/RagnarokSpriteLoader.cs +++ b/RebuildClient/Assets/Scripts/Editor/RagnarokSpriteLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Assets.Scripts; @@ -22,7 +22,6 @@ class RagnarokSpriteLoader private BinaryReader br; private int version; - private int indexCount; private int rgbaCount; private List spriteFrames; @@ -31,15 +30,21 @@ class RagnarokSpriteLoader public List Textures = new(); public List Sprites = new(); public List SpriteSizes = new(); - public int SpriteFrameCount => spriteFrames.Count; + public int SpriteFrameCount + { + get + { + return spriteFrames.Count; + } + } public Texture2D Atlas; - public int IndexCount => indexCount; + public int IndexCount { get; private set; } private void ReadIndexedImage() { - for (var i = 0; i < indexCount; i++) + for (var i = 0; i < IndexCount; i++) { var width = br.ReadUInt16(); var height = br.ReadUInt16(); @@ -60,7 +65,7 @@ private void ReadIndexedImage() private void ReadRleIndexedImage() { - for (var i = 0; i < indexCount; i++) + for (var i = 0; i < IndexCount; i++) { var width = br.ReadUInt16(); var height = br.ReadUInt16(); @@ -122,7 +127,7 @@ private void ReadRgbaImage() } } - private void ExtendSpriteTextureData(Color[] colors, SpriteFrameData frame) + private static void ExtendSpriteTextureData(Color[] colors, SpriteFrameData frame) { //we're going to extend the sprite color into the transparent area around the sprite //this is to make bilinear filtering work good with the sprite @@ -169,16 +174,16 @@ private void ExtendSpriteTextureData(Color[] colors, SpriteFrameData frame) } } - private Texture2D RgbaToTexture(SpriteFrameData frame) + private static Texture2D RgbaToTexture(SpriteFrameData frame) { - var image = new Texture2D(frame.Width, frame.Height, TextureFormat.ARGB32, false); - image.wrapMode = TextureWrapMode.Clamp; - image.alphaIsTransparency = true; + var image = new Texture2D(frame.Width, frame.Height, TextureFormat.ARGB32, false) + { + wrapMode = TextureWrapMode.Clamp, + alphaIsTransparency = true + }; var colors = new Color[frame.Width * frame.Height]; - //Debug.Log(frame.Width + " " + frame.Height); - for (var y = 0; y < frame.Height; y++) { for (var x = 0; x < frame.Width; x++) @@ -205,9 +210,11 @@ private Texture2D RgbaToTexture(SpriteFrameData frame) private Texture2D IndexedToTexture(SpriteFrameData frame) { - var image = new Texture2D(frame.Width, frame.Height, TextureFormat.ARGB32, false); - image.wrapMode = TextureWrapMode.Clamp; - image.alphaIsTransparency = true; + var image = new Texture2D(frame.Width, frame.Height, TextureFormat.ARGB32, false) + { + wrapMode = TextureWrapMode.Clamp, + alphaIsTransparency = true + }; var colors = new Color[frame.Width * frame.Height]; @@ -226,7 +233,6 @@ private Texture2D IndexedToTexture(SpriteFrameData frame) colors[x + (frame.Height - y - 1) * frame.Width] = color; - //image.SetPixel(x, frame.Height - y - 1, color); } } @@ -244,10 +250,9 @@ private void ReadPalette() public Texture2D LoadFirstSpriteTextureOnly(string sprPath) { - var filename = sprPath; - var basename = Path.GetFileNameWithoutExtension(filename); + var basename = Path.GetFileNameWithoutExtension(sprPath); - var bytes = File.ReadAllBytes(filename); + var bytes = File.ReadAllBytes(sprPath); ms = new MemoryStream(bytes); br = new BinaryReader(ms); @@ -260,16 +265,13 @@ public Texture2D LoadFirstSpriteTextureOnly(string sprPath) var majorVersion = br.ReadByte(); version = majorVersion * 10 + minorVersion; - indexCount = br.ReadUInt16(); + IndexCount = br.ReadUInt16(); rgbaCount = 0; if (version > 11) rgbaCount = br.ReadUInt16(); - //Debug.Log($"RGBA count: {rgbaCount}"); - - var frameCount = indexCount + rgbaCount; - var rgbaIndex = indexCount; + int frameCount = IndexCount + rgbaCount; spriteFrames = new List(frameCount); @@ -283,33 +285,12 @@ public Texture2D LoadFirstSpriteTextureOnly(string sprPath) if (version > 10) ReadPalette(); - if (spriteFrames.Count > 0) - { - var i = 0; - - Texture2D image; - if (spriteFrames[i].IsIndexed) - image = IndexedToTexture(spriteFrames[i]); - else - image = RgbaToTexture(spriteFrames[i]); - image.name = basename; - // image.name = $"{basename}_{i:D4}"; - // image.hideFlags = HideFlags.HideInHierarchy; - - //ctx.AddObjectToAsset(image.name, image); - - //var sprite = Sprite.Create(image, new Rect(0, 0, indexedFrames[i].Width, indexedFrames[i].Height), - // new Vector2(0.5f, 0.5f), 100); - - //sprite.name = $"sprite_{basename}_{i:D4}"; - - //ctx.AddObjectToAsset(sprite.name, sprite); - - return image; - //Sprites.Add(sprite); - } + if (spriteFrames.Count <= 0) + return null; + Texture2D image = spriteFrames[0].IsIndexed ? IndexedToTexture(spriteFrames[0]) : RgbaToTexture(spriteFrames[0]); + image.name = basename; + return image; - return null; } private void LoadTextures(string baseName, int frame, int paletteId = -1) @@ -330,23 +311,17 @@ private void LoadTextures(string baseName, int frame, int paletteId = -1) public void Load(string filename, string atlasPath, RoSpriteData dataObject, string paletteFile, string imfPath = null) { - //var filename = ctx.assetPath; var basename = Path.GetFileNameWithoutExtension(filename); - var dirName = Path.GetDirectoryName(filename); if (!File.Exists(filename)) { - Debug.LogError($"Could not import asset {filename}, the related .spr file could not be found."); - return; + throw new FileNotFoundException($"File {filename} not found"); } var bytes = File.ReadAllBytes(filename); ms = new MemoryStream(bytes); br = new BinaryReader(ms); - - //fs = new FileStream(filename, FileMode.Open); - //br = new BinaryReader(fs); - + var header = new string(br.ReadChars(2)); if (header != "SP") throw new Exception("Not sprite"); @@ -355,16 +330,13 @@ public void Load(string filename, string atlasPath, RoSpriteData dataObject, str var majorVersion = br.ReadByte(); version = majorVersion * 10 + minorVersion; - indexCount = br.ReadUInt16(); + IndexCount = br.ReadUInt16(); rgbaCount = 0; if (version > 11) rgbaCount = br.ReadUInt16(); - //Debug.Log($"RGBA count: {rgbaCount}"); - - var frameCount = indexCount + rgbaCount; - var rgbaIndex = indexCount; + int frameCount = IndexCount + rgbaCount; spriteFrames = new List(frameCount); @@ -378,13 +350,6 @@ public void Load(string filename, string atlasPath, RoSpriteData dataObject, str if (version > 10) ReadPalette(); - // Debug.Log($"Palette check " + Path.Combine(dirName, "Palette/", basename + "_0.pal")); - - - //var palPath = Path.Combine("G:\\Games\\RagnarokJP\\data\\palette\\몸\\costume_1", $"{basename}_0_1.pal"); - - //Debug.Log(palPath); - if (File.Exists(paletteFile)) { var origPalette = paletteData; @@ -415,48 +380,33 @@ public void Load(string filename, string atlasPath, RoSpriteData dataObject, str } - var supertexture = new Texture2D(2, 2); - supertexture.name = Path.GetFileNameWithoutExtension(atlasPath); - var rects = supertexture.PackTextures(Textures.ToArray(), 2, 2048, false); - supertexture.filterMode = FilterMode.Bilinear; - - //var atlasDir = Path.Combine(dirName, "atlas/"); - //var atlasPath = Path.Combine(atlasDir, supertexture.name + "_.png"); + var superTexture = new Texture2D(2, 2) + { + name = Path.GetFileNameWithoutExtension(atlasPath) + }; + Rect[] rects = superTexture.PackTextures(Textures.ToArray(), 2, 2048, false); + superTexture.filterMode = FilterMode.Bilinear; + var compression = TextureImporterCompression.CompressedHQ; if (atlasPath.Replace("\\", "/").Contains("/Icons/")) compression = TextureImporterCompression.Uncompressed; - supertexture = TextureImportHelper.SaveAndUpdateTexture(supertexture, atlasPath, ti => + superTexture = TextureImportHelper.SaveAndUpdateTexture(superTexture, atlasPath, ti => { ti.textureType = TextureImporterType.Sprite; ti.spriteImportMode = SpriteImportMode.Single; ti.textureCompression = compression; ti.crunchedCompression = false; }); - // - //var bytes2 = supertexture.EncodeToPNG(); - //File.WriteAllBytes(atlasPath, bytes2); - //supertexture.Compress(true); - - - //ctx.AddObjectToAsset(supertexture.name, supertexture); - - - Atlas = supertexture; - //var byteData = supertexture.EncodeToPNG(); - //if (!Directory.Exists(atlasDir)) - // Directory.CreateDirectory(atlasDir); - //File.WriteAllBytes(atlasPath, byteData); //we will reattach this in a bit - //AssetDatabase.CreateAsset(supertexture, Path.Combine(basePath, $"{supertexture.name}.texture")); - //supertexture = AssetDatabase.LoadAssetAtPath(Path.Combine(subdir, $"{supertexture.name}.anim"), typeof(Texture2D)) as Texture2D; + Atlas = superTexture; for (var i = 0; i < rects.Length; i++) { - var texrect = new Rect(rects[i].x * supertexture.width, rects[i].y * supertexture.height, rects[i].width * supertexture.width, - rects[i].height * supertexture.height); + var texRect = new Rect(rects[i].x * superTexture.width, rects[i].y * superTexture.height, rects[i].width * superTexture.width, + rects[i].height * superTexture.height); SpriteSizes.Add(new Vector2Int(Textures[i].width, Textures[i].height)); - var sprite = Sprite.Create(supertexture, texrect, new Vector2(0.5f, 0.5f), 50, 0, SpriteMeshType.FullRect); + var sprite = Sprite.Create(superTexture, texRect, new Vector2(0.5f, 0.5f), 50, 0, SpriteMeshType.FullRect); sprite.name = $"sprite_{basename}_{i:D4}"; diff --git a/RebuildClient/Assets/Scripts/Editor/RoDataImportManager.cs b/RebuildClient/Assets/Scripts/Editor/RoDataImportManager.cs new file mode 100644 index 00000000..af551737 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/RoDataImportManager.cs @@ -0,0 +1,611 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Assets.Editor; +using Assets.Scripts; +using UnityEditor; +using UnityEngine; + +namespace Scripts.Editor +{ + public class GrfDataFilesCategorized + { + public sealed class RoFilesList : List + { + public uint Imported + { + get; + set; + } + } + + public readonly RoFilesList SoundFiles = new(); + public readonly RoFilesList MonsterFiles = new(); + public readonly RoFilesList HeadFiles = new(); + public readonly RoFilesList BodyFiles = new(); + public readonly RoFilesList HeadgearFiles = new(); + public readonly RoFilesList WeaponFiles = new(); + public readonly RoFilesList ShieldFiles = new(); + public readonly RoFilesList NpcFiles = new(); + public readonly RoFilesList PaletteFiles = new(); + public readonly RoFilesList CutinFiles = new(); + public readonly RoFilesList MiscFiles = new(); + + public string this[int index] + { + get + { + return categoriesDict.Keys.ToArray()[index]; + } + } + public readonly RoFilesList[] Categories; + public int Count + { + get + { + return Categories.Aggregate(0, (acc, cur) => acc + cur.Count); + } + } + private readonly Dictionary categoriesDict; + + public GrfDataFilesCategorized() + { + Categories = new[] + { + SoundFiles, MonsterFiles, HeadFiles, + BodyFiles, HeadgearFiles, WeaponFiles, + ShieldFiles, NpcFiles, PaletteFiles, + CutinFiles, MiscFiles + }; + + categoriesDict = new Dictionary + { + ["SoundFiles"] = SoundFiles, + ["MonsterFiles"] = MonsterFiles, + ["HeadFiles"] = HeadFiles, + ["BodyFiles"] = BodyFiles, + ["HeadgearFiles"] = HeadgearFiles, + ["WeaponFiles"] = WeaponFiles, + ["ShieldFiles"] = ShieldFiles, + ["NpcFiles"] = NpcFiles, + ["PaletteFiles"] = PaletteFiles, + ["CutinFiles"] = CutinFiles, + ["MiscFiles"] = MiscFiles + }; + } + } + + /// + /// Prepares all relevant data from an extracted GRF to be used by the Rebuild client + /// + public static class RoDataImportManager // Open to suggestions on this name + { + private static string TranslateFilename(string name) + { + return name + .Replace("성직자_", "Acolyte_") + .Replace("궁수_", "Archer_") + .Replace("마법사_", "Mage_") + .Replace("상인_", "Merchant_") + .Replace("초보자_", "Novice_") + .Replace("검사_", "Swordsman_") + .Replace("도둑_", "Thief_") + .Replace("여_", "F_") + .Replace("남_", "M_"); + } + + /// + /// Goes through all RagnarokDirectory content and categorize file paths + /// + /// + /// + public static GrfDataFilesCategorized ReadGrfDataFolder(string inputDataPath = null) + { + inputDataPath = ValidateGrfDataFolder(inputDataPath); + Debug.Log("Reading and categorizing Grf Data folder"); + + var grfData = new GrfDataFilesCategorized(); + + var directoryToCategoryMap = new Dictionary() + { + {(int)RagnarokDirectory.ActDirectoriesID.ActMonstersDirectories, grfData.MonsterFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActPlayerHeadDirectories, grfData.HeadFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActPlayerBodyDirectories, grfData.BodyFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActPlayerHeadgearDirectories, grfData.HeadgearFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActWeaponsDirectories, grfData.WeaponFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActShieldsDirectories, grfData.ShieldFiles}, + {(int)RagnarokDirectory.ActDirectoriesID.ActNpcDirectories, grfData.NpcFiles} + }; + + foreach (var filePath in Directory.EnumerateFiles(inputDataPath, "*", SearchOption.AllDirectories)) + { + var extension = Path.GetExtension(filePath); + var fileRelativePath = Path.GetRelativePath(inputDataPath, filePath); + var directoryRelativePath = Path.GetDirectoryName(fileRelativePath); + switch (extension) + { + case ".wav" or ".ogg": + grfData.SoundFiles.Add(filePath); + if (WasThisFileAlreadyImported(filePath)) + grfData.SoundFiles.Imported++; + break; + case ".act": + foreach (var (directoriesCategory, id) in RagnarokDirectory.ActDirectories.Select((directories, id) => (directory: directories, id))) + { + if (directoriesCategory.Contains(directoryRelativePath)) + { + directoryToCategoryMap[id] + .Add(filePath); + if (WasThisFileAlreadyImported(filePath)) + directoryToCategoryMap[id].Imported++; + break; + } + if (RagnarokDirectory.ExpectedMiscFiles.Contains(fileRelativePath)) + { + grfData.MiscFiles.Add(filePath); + // if (WasThisFileAlreadyImported(filePath)) + // grfData.IncrementImportedCount(nameof(grfData.MiscFiles)); + break; + } + } + break; + case ".pal": + grfData.PaletteFiles.Add(filePath); + if (WasThisFileAlreadyImported(filePath)) + grfData.PaletteFiles.Imported++; + break; + case ".bmp": + if (RagnarokDirectory.ImgCutinDirectories.Contains(directoryRelativePath)) + { + grfData.CutinFiles.Add(filePath); + if (WasThisFileAlreadyImported(filePath)) + grfData.CutinFiles.Imported++; + } + break; + } + } + return grfData; + } + public static void ProcessGrfData(string inputDataPath = null) + { + var grfData = ReadGrfDataFolder(inputDataPath); + + + Debug.Log("Sorted files by type"); + Debug.Log($"Processing grf data with {grfData.Count} Files"); + ProcessWavFiles(grfData.SoundFiles); + ProcessActFiles(grfData.MonsterFiles); + //ProcessPalFiles(palFilePaths); // TODO: Rework spr/act so that sprites are kept in a indexed format and use the color lookup table to render the correct colors + //ProcessRswFiles(rswFilePaths); + Debug.Log("Done processing"); + } + + public static void ProcessGrfData(GrfDataFilesCategorized grfData) + { + Debug.Log($"Processing grf data with {grfData.Count} Files"); + Debug.Log($"{grfData.SoundFiles.Count} in Sounds"); + Debug.Log($"{grfData.MonsterFiles.Count} in Monsters"); + Debug.Log($"{grfData.HeadFiles.Count} in Heads"); + Debug.Log($"{grfData.BodyFiles.Count} in Body"); + Debug.Log($"{grfData.HeadgearFiles.Count} in Headgear"); + Debug.Log($"{grfData.WeaponFiles.Count} in Weapons"); + Debug.Log($"{grfData.ShieldFiles.Count} in Shields"); + Debug.Log($"{grfData.NpcFiles.Count} in Npcs"); + Debug.Log($"{grfData.PaletteFiles.Count} in Palettes"); + Debug.Log($"{grfData.CutinFiles.Count} in Cutins"); + Debug.Log($"{grfData.MiscFiles.Count} in Misc"); + + ProcessWavFiles(grfData.SoundFiles); + ProcessActFiles(grfData.MonsterFiles); + } + + private static string ValidateGrfDataFolder(string inputDataPath = null) + { + Debug.Log($"Our input path is :{inputDataPath}"); + if (inputDataPath == null) + { + try + { + inputDataPath = RagnarokDirectory.GetRagnarokDataDirectory; + } + catch (DirectoryNotFoundException) + { + throw new DirectoryNotFoundException($"ProcessGrfData: directory not found: {inputDataPath}\nYou must pass a directory to ProcessGrfData or set a ragnarok data directory first!"); + } + } else if (!Directory.Exists(inputDataPath)) + throw new DirectoryNotFoundException($"ProcessGrfData: directory not found: {inputDataPath}"); + + return inputDataPath; + } + + private static void ProcessWavFiles(List wavFilesPath) + { + Debug.Log("Starting wav processing"); + if (wavFilesPath.Count == 0) + { + Debug.Log("No wav files to process"); + return; + } + try + { + AssetDatabase.StartAssetEditing(); + foreach (var wavFilePath in wavFilesPath) + { + var wavFileName = Path.GetFileName(wavFilePath); + var grfRelativeFolderPath = Path.GetRelativePath(RagnarokDirectory.GetRagnarokDataDirectory, Path.GetDirectoryName(wavFilePath)); + string assetRelativeFolderPath; + try + { + assetRelativeFolderPath = RagnarokDirectory.RelativeDirectoryConversion[grfRelativeFolderPath]; + } + catch (KeyNotFoundException) + { + Debug.Log($"Converted path not found for {grfRelativeFolderPath}. Skipping it."); + continue; + } + Directory.CreateDirectory(Path.Combine(Application.dataPath, assetRelativeFolderPath)); + File.Copy(wavFilePath, Path.Combine(Application.dataPath, assetRelativeFolderPath, wavFileName)); + } + } + catch (Exception) + { + Debug.LogError($"Issue during sound import stopping further imports."); + throw; + } + finally + { + AssetDatabase.StopAssetEditing(); + AssetDatabase.Refresh(); + } + } + private static void ProcessRswFiles() + { + throw new NotImplementedException(); + } + private static void ProcessPalFiles() + { + throw new NotImplementedException(); + } + private static void ProcessActFiles(List actFilePaths, bool overwrite = false) + { + Debug.Log("Starting act processing"); + if (actFilePaths.Count == 0) + { + Debug.Log("No act files to process"); + return; + } + Debug.Log($"We have the following list: {actFilePaths}"); + + foreach (var actFilePath in actFilePaths) + { + //Debug.Log($"Got an act file:{actFilePath}"); + var sprFilePath = DoesSprExist(actFilePath, true); + if (sprFilePath == null) // Skipping this .act + continue; + var imfFilePath = DoesImfExist(actFilePath); + + var fileName = Path.GetFileNameWithoutExtension(actFilePath); + var grfRelativeFolderPath = Path.GetRelativePath(RagnarokDirectory.GetRagnarokDataDirectory, Path.GetDirectoryName(actFilePath)); + string assetRelativeFolderPath; + try + { + assetRelativeFolderPath = RagnarokDirectory.RelativeDirectoryConversion[grfRelativeFolderPath]; + } + catch (KeyNotFoundException) + { + Debug.Log($"Converted path not found for {grfRelativeFolderPath}. Skipping it."); + continue; + } + + if (File.Exists(Path.Combine("Assets", assetRelativeFolderPath, fileName, ".asset")) && !overwrite) + continue; + + var roSpriteData = ScriptableObject.CreateInstance(); + Directory.CreateDirectory(Path.Combine("Assets", assetRelativeFolderPath)); + + // TODO: Rethink texture loading so that we don't need to do both a CreateAsset and a SaveAssets after the texture import + var assetAtlasFilePath = Path.Combine("Assets", assetRelativeFolderPath, "Atlas", $"{fileName}_atlas.png"); + AssetDatabase.CreateAsset(roSpriteData, Path.Combine("Assets", assetRelativeFolderPath, $"{fileName}.asset")); + + var loader = new RagnarokSpriteLoader(); + loader.Load(sprFilePath, assetAtlasFilePath, roSpriteData, null, imfFilePath); + SetUpSpriteData(loader, roSpriteData, Path.Combine(RagnarokDirectory.GetRagnarokDataDirectory, grfRelativeFolderPath), fileName); + } + AssetDatabase.SaveAssets(); + + // TODO: investigate the possibility of a batch import, but seems unlikely to be feasible + // try + // { + // AssetDatabase.StartAssetEditing(); + // + // } + // catch (Exception) + // { + // Debug.LogError("Issue during asset creation"); + // throw; + // } + // finally + // { + // + // AssetDatabase.StopAssetEditing(); + // } + } + + private static bool WasThisFileAlreadyImported(string filePath) + { + var relativeFilePath = Path.GetRelativePath(RagnarokDirectory.GetRagnarokDataDirectory, filePath); + var relativeFolderPath = Path.GetDirectoryName(relativeFilePath); + string relativeAssetFolderPath; + try + { + relativeAssetFolderPath = RagnarokDirectory.RelativeDirectoryConversion[relativeFolderPath!]; + } + catch (KeyNotFoundException e) + { + Debug.LogException(e); + return false; + } + var fileName = Path.GetFileNameWithoutExtension(filePath); + var fileExtension = Path.GetExtension(filePath); + var relativeAssetFilePath = Path.Combine(relativeAssetFolderPath, fileName) + fileExtension; + //Debug.Log($"Checking if file {filePath} was already imported as {relativeAssetFilePath} "); + return fileExtension switch + { + ".act" => File.Exists(Path.Combine(Application.dataPath, relativeAssetFolderPath, fileName, ".asset")), + _ => File.Exists(Path.Combine(Application.dataPath, relativeAssetFilePath)) + }; + } + + /// + /// Check if a .spr file pair exists for the .act file on .
+ /// Will ask for the .spr file path if is false + ///
+ /// Full .act file path + /// Skip import if no matching .spr file is found + /// Path to .spr file if or null if it was skipped + private static string DoesSprExist(string actFilePath, bool autoSkip = false) + { + string sprFilePath = Path.ChangeExtension(actFilePath, ".spr"); + if (File.Exists(sprFilePath)) + { + return sprFilePath; + } + if (!autoSkip) + { + if (EditorUtility.DisplayDialog("Sprite Data Missing", $"Matching .spr file not found for { Path.GetFileName(actFilePath) }.\n" + + $"Please select the corresponding .spr file.", "Select", "Skip")) + { + sprFilePath = EditorUtility.OpenFilePanel("Select matching .spr", "", "spr"); + return sprFilePath; + } + } + Debug.LogWarning($"Skipping {actFilePath} conversion."); + return null; + } + + private static string DoesImfExist(string actFilePath) + { + var fileName = Path.GetFileNameWithoutExtension(actFilePath); + var imfFilePath = Path.Combine(RagnarokDirectory.GetRagnarokDataDirectorySafe, "imf", fileName + ".imf"); + if (File.Exists(imfFilePath)) + return imfFilePath; + Debug.LogWarning($"No IMF file for {actFilePath}"); + return null; + } + + public static void ConvertToAsset(string actFilePath, string assetDirectoryPath = null) + { + //Check for .spr pair immediately, otherwise quit the import + var sprFilePath = DoesSprExist(actFilePath); + if (sprFilePath == null) return; + Debug.Log($"Ready for conversion: {Path.GetFileName(actFilePath)} + {Path.GetFileName(sprFilePath)}"); + var roSpriteData = ScriptableObject.CreateInstance(); + } + + public static void ImportActFile(string actFilePath, string assetDirectoryPath = null) + { + //Check for .spr pair immediately, otherwise quit the import + var sprFilePath = actFilePath.Replace(".act", ".spr"); + if (!File.Exists(sprFilePath)) + { + throw new NotSupportedException($"Couldn't find a matching {Path.GetFileName(sprFilePath)} file on path {sprFilePath}, aborting import"); + }; + + var actFileName = Path.GetFileNameWithoutExtension(actFilePath); + var actDirectoryPath = Path.GetDirectoryName(actFilePath) ?? throw new ArgumentNullException($"Resulting diretory for {actFilePath} cannot be null"); + var atlasFilePath = Path.Combine(assetDirectoryPath ?? actDirectoryPath, "Atlas", $"{actFileName}_atlas.png"); + var palettePath = Path.Combine(actDirectoryPath, "Palette"); + var palettes = new List(); + + for (var i = 0; i < 10; i++) + { + var pName = Path.Combine(palettePath, $"{actFileName}_{i}_1.pal"); + if (File.Exists(pName)) + palettes.Add(pName); + pName = Path.Combine(palettePath, $"{actFileName}_{i}.pal"); + if (File.Exists(pName)) + palettes.Add(pName); + } + + var spriteAsset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(spriteAsset, Path.Combine(assetDirectoryPath ?? actDirectoryPath, $"{actFileName}.asset")); + + var loader = new RagnarokSpriteLoader(); + loader.Load(actFilePath.Replace(".act", ".spr"), atlasFilePath, spriteAsset, null); + SetUpSpriteData(loader, spriteAsset, actDirectoryPath, actFileName, actFileName); + + for (var i = 0; i < palettes.Count; i++) + { + spriteAsset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(spriteAsset, Path.Combine(assetDirectoryPath ?? actDirectoryPath, $"{actFileName}_{i}.asset")); + + atlasFilePath = Path.Combine(assetDirectoryPath ?? actDirectoryPath, "Atlas", $"{actFileName}_{i}_atlas.png").Replace("\\", "/"); + loader = new RagnarokSpriteLoader(); + loader.Load(actFilePath.Replace(".act", ".spr"), atlasFilePath, spriteAsset, palettes[i]); + SetUpSpriteData(loader, spriteAsset, actDirectoryPath, actFileName, $"{actFileName}_{i}"); + } + AssetDatabase.SaveAssets(); + } + + private static void SetUpSpriteData(RagnarokSpriteLoader spr, RoSpriteData asset, string basePath, string baseName, string outName = null) + { + outName ??= baseName; + var actName = Path.Combine(basePath, baseName + ".act"); + var imfFile = Path.Combine(RagnarokDirectory.GetRagnarokDataDirectorySafe, "imf", baseName + ".imf"); + if (!File.Exists(imfFile)) + imfFile = null; + + var actLoader = new RagnarokActLoader(); + var actions = actLoader.Load(spr, actName, imfFile); + + asset.Actions = actions.ToArray(); + asset.Sprites = spr.Sprites.ToArray(); + asset.SpriteSizes = spr.SpriteSizes.ToArray(); + asset.Name = outName; + asset.Atlas = spr.Atlas; + asset.SpritesPerPalette = spr.SpriteFrameCount; + + if (actLoader.Sounds != null) + { + asset.Sounds = new AudioClip[actLoader.Sounds.Length]; + + for (var i = 0; i < asset.Sounds.Length; i++) + { + var soundAction = actLoader.Sounds[i]; + if (!Path.GetExtension(soundAction).Equals(".wav", StringComparison.InvariantCultureIgnoreCase)) + continue; + var sPath = $"Assets/Sounds/{soundAction}"; + + var sound = AssetDatabase.LoadAssetAtPath(sPath); + if (!sound) + { + Debug.LogError($"Couldn't find sound asset at {sPath} for sprite {baseName}. Skipping."); + continue; + //throw new FileNotFoundException($"Sound {sPath} for sprite {baseName} not found."); + } + asset.Sounds[i] = sound; + } + } + + asset.Type = asset.Actions.Length switch + { + 8 or 13 => SpriteType.Npc, + 32 => SpriteType.ActionNpc, + 39 or 40 or 41 => SpriteType.Monster, //zerom/mi gao/increase soil for some reason + 47 or 48 => SpriteType.Monster2, //dullahan for some reason + 56 or 64 => SpriteType.Monster, //56 ??? + 72 => SpriteType.Pet, + _ => asset.Type + }; + + var maxExtent = 0f; + var totalWidth = 0f; + var widthCount = 0; + + foreach (var action in asset.Actions) + { + var frameId = 0; + foreach (var frame in action.Frames) + { + if (frame.IsAttackFrame) + asset.AttackFrameTime = action.Delay * frameId; + + frameId++; + + foreach (var layer in frame.Layers) + { + if (layer.Index == -1) + continue; + var sprite = asset.SpriteSizes[layer.Index]; + var y = layer.Position.y + sprite.y / 2f; + if (layer.Position.x < 0) + y = Mathf.Abs(layer.Position.y - sprite.y / 2f); + if (y > maxExtent) + maxExtent = y; + totalWidth += Mathf.Abs(layer.Position.x) + sprite.x / 2f; + widthCount++; + } + } + } + + //far better way to get sprite attack timing + if (asset.Type is (SpriteType.Monster or SpriteType.Monster2 or SpriteType.Pet)) + { + var actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack1); + if (actionId == -1) + actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack2); + if (actionId >= 0) + { + var frames = asset.Actions[actionId].Frames; + var found = false; + + for (var j = 0; j < frames.Length; j++) + { + if (frames[j].IsAttackFrame) + { + asset.AttackFrameTime = j * asset.Actions[actionId].Delay; + found = true; + break; + } + } + + if (!found) + { + var pos = frames.Length - 2; + if (pos < 0) + pos = 0; + asset.AttackFrameTime = pos * asset.Actions[actionId].Delay; + } + } + } + + asset.Size = Mathf.CeilToInt(maxExtent); + asset.AverageWidth = totalWidth / widthCount; + + //ok so this is a giant shitshow. We want to know where to display emotes, cast bars, and npcs above this character + //to do so, we save the highest y value of the first frame of the first action. + //a lot of monsters use a high overhead attack which we don't want to count, so using the idle pose is probably safer + asset.StandingHeight = 20; + try + { + var firstFrame = asset.Actions[0].Frames[0]; + var maxHeight = + ( + from layer in firstFrame.Layers where layer.Index >= 0 + let curSpr = asset.Sprites[layer.Index] + select curSpr.rect.height / 2 - layer.Position.y + ).Prepend(0f).Max(); + + if (maxHeight > asset.StandingHeight) + asset.StandingHeight = maxHeight; + } + catch (Exception) + { + throw new Exception($"Couldn't process standing height for sprite {asset.Name}"); + } + } + } + +// public class ActPostProcessor : AssetPostprocessor +// { +// private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, +// bool didDomainReload) +// { +// foreach (var importedAsset in importedAssets) +// { +// if (!importedAsset.EndsWith(".act")) +// continue; +// +// var sprName = Path.Combine(Path.GetDirectoryName(importedAsset), Path.GetFileNameWithoutExtension(importedAsset) + ".spr"); +// if (!File.Exists(sprName)) +// { +// Debug.LogError($"Could not load sprite {importedAsset} as it did not have an associated .spr sprite data file."); +// continue; +// } +// +// RoDataImportManager.ImportActFile(importedAsset); +// } +// } +// } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/ActImporter.cs.meta b/RebuildClient/Assets/Scripts/Editor/RoDataImportManager.cs.meta similarity index 100% rename from RebuildClient/Assets/Scripts/Editor/ActImporter.cs.meta rename to RebuildClient/Assets/Scripts/Editor/RoDataImportManager.cs.meta diff --git a/RebuildClient/Assets/Scripts/Editor/SprHandling.meta b/RebuildClient/Assets/Scripts/Editor/SprHandling.meta new file mode 100644 index 00000000..f5a4e84f --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/SprHandling.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 605cb2a461114a7ea5e2455354095527 +timeCreated: 1761677686 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs new file mode 100644 index 00000000..f1ea1860 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Assets.Scripts.Sprites; +using UnityEditor; +using UnityEditor.AssetImporters; +using UnityEngine; + +namespace Assets.Scripts.Editor.SprHandling +{ + [ScriptedImporter(1, "spr", AllowCaching = true)] + public sealed class RoSprAssetImporter : ScriptedImporter + { + // This allows us to avoid fiddling with project settings + // to add support for the file extension + private static void TryIncludeSprExtension() + { + if (EditorSettings.projectGenerationUserExtensions.Contains(SprExtension)) return; + var list = EditorSettings.projectGenerationUserExtensions.ToList(); + list.Add(SprExtension); + EditorSettings.projectGenerationUserExtensions = list.ToArray(); + } + + public string sprVersion; + public string filepath; + public string sprFileName; + public Texture2D palette; + public Texture2D atlas; + public Rect[] atlasRects; + + private RoSprAsset asset; + private const string SprExtension = "spr"; + + public override void OnImportAsset(AssetImportContext ctx) + { + asset = ScriptableObject.CreateInstance(); + asset.Load(ctx.assetPath); + + ctx.AddObjectToAsset(GUID.Generate().ToString(), asset.atlas); + ctx.AddObjectToAsset(GUID.Generate().ToString(), asset.palette); + + PopulateImporterFields(); + + ctx.AddObjectToAsset(GUID.Generate().ToString(), asset); + ctx.SetMainObject(asset); + + TryIncludeSprExtension(); + } + + private void PopulateImporterFields() + { + sprVersion = asset.sprVersion; + filepath = asset.filepath; + sprFileName = asset.sprFileName; + palette = asset.palette; + atlas = asset.atlas; + atlasRects = asset.atlasRects; + } + } + + [CustomEditor(typeof(RoSprAssetImporter))] + public sealed class RoSprAssetImporterEditor : ScriptedImporterEditor + { + public override bool showImportedObject + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/SprImporter.cs.meta b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs.meta similarity index 83% rename from RebuildClient/Assets/Scripts/Editor/SprImporter.cs.meta rename to RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs.meta index 83736b44..f1d04989 100644 --- a/RebuildClient/Assets/Scripts/Editor/SprImporter.cs.meta +++ b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprAssetImporter.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 49fe68463c56b9049a2275c7ed72363b +guid: b2da6e2764ba4adb84760a7423406358 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs new file mode 100644 index 00000000..5bb801df --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs @@ -0,0 +1,38 @@ +using Assets.Scripts.Sprites; +using UnityEditor; +using UnityEngine; + +namespace Assets.Scripts.Editor.SprHandling +{ + [CustomEditor(typeof(RoSprAsset))] + public class RoSprInspector : UnityEditor.Editor + { + private SerializedProperty sprName; + private SerializedProperty sprVersion; + private SerializedProperty palette; + private SerializedProperty atlas; + private SerializedProperty atlasRects; + + private void OnEnable() + { + sprName = serializedObject.FindProperty("sprFileName"); + sprVersion = serializedObject.FindProperty("spriteVersion"); + palette = serializedObject.FindProperty("palette"); + atlas = serializedObject.FindProperty("atlas"); + } + + public override void OnInspectorGUI() + { + DrawImporterGUI(); + serializedObject.ApplyModifiedProperties(); + } + + private void DrawImporterGUI() + { + EditorGUILayout.PropertyField(sprName, new GUIContent("Sprite Name")); + EditorGUILayout.PropertyField(sprVersion, new GUIContent("Sprite Version")); + EditorGUILayout.PropertyField(palette, new GUIContent("Palette")); + EditorGUILayout.PropertyField(atlas, new GUIContent("Atlas")); + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs.meta b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs.meta new file mode 100644 index 00000000..3d292b03 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/SprHandling/RoSprInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41b4f90271854152aca8465389e346a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RebuildClient/Assets/Scripts/Editor/SprImporter.cs b/RebuildClient/Assets/Scripts/Editor/SprImporter.cs deleted file mode 100644 index d829da21..00000000 --- a/RebuildClient/Assets/Scripts/Editor/SprImporter.cs +++ /dev/null @@ -1,215 +0,0 @@ - -// -// public class LayerCurveSet -// { -// public string Path; -// public EditorCurveBinding SpriteBinding; -// public EditorCurveBinding PositionXBinding; -// public EditorCurveBinding PositionYBinding; -// public EditorCurveBinding PositionZBinding; -// } -// -// public static class ImporterExtensions -// { -// public static Keyframe SetConstant(this Keyframe frame) -// { -// frame.inTangent = Mathf.Infinity; -// frame.outTangent = Mathf.Infinity; -// return frame; -// } -// } -// -// -// -// [UnityEditor.AssetImporters.ScriptedImporter(1, "spr")] -// public class SprImporter : ScriptedImporter -// { -// public override void OnImportAsset(UnityEditor.AssetImporters.AssetImportContext ctx) -// { -// var path = ctx.assetPath; -// var name = Path.GetFileNameWithoutExtension(ctx.assetPath); -// -// var spr = new RagnarokSpriteLoader(); -// spr.Load(ctx); -// -// // if(ctx.selectedBuildTarget == BuildTarget.WebGL) -// // spr.Atlas.Compress(true); -// -// var basePath = Path.GetDirectoryName(path); -// var baseName = Path.GetFileNameWithoutExtension(path); -// var actName = Path.Combine(basePath, baseName + ".act"); -// -// if (File.Exists(actName)) -// { -// var actLoader = new RagnarokActLoader(); -// var actions = actLoader.Load(spr, actName); -// -// var asset = ScriptableObject.CreateInstance(typeof(RoSpriteData)) as RoSpriteData; -// asset.Actions = actions.ToArray(); -// asset.Sprites = spr.Sprites.ToArray(); -// asset.SpriteSizes = spr.SpriteSizes.ToArray(); -// asset.Name = baseName; -// asset.Atlas = spr.Atlas; -// asset.SpritesPerPalette = spr.SpriteFrameCount; -// if (actLoader.Sounds != null) -// { -// asset.Sounds = new AudioClip[actLoader.Sounds.Length]; -// -// for (var i = 0; i < asset.Sounds.Length; i++) -// { -// var s = actLoader.Sounds[i]; -// if (s == "atk") -// continue; -// var sPath = $"Assets/Sounds/{s}"; -// if(!File.Exists(sPath)) -// sPath = $"Assets/Sounds/{s.Replace(".wav", "")}.ogg"; -// -// var sound = AssetDatabase.LoadAssetAtPath(sPath); -// if (sound == null) -// Debug.Log("Could not find sound " + sPath + " for sprite " + name); -// asset.Sounds[i] = sound; -// } -// } -// //asset.Sounds = asset.Sounds.ToArray(); -// -// //Debug.Log(asset.Sprites.Length); -// -// switch (asset.Actions.Length) -// { -// -// case 8: -// asset.Type = SpriteType.Npc; -// break; -// case 13: -// asset.Type = SpriteType.Npc; -// break; -// case 32: -// asset.Type = SpriteType.ActionNpc; -// break; -// case 39: //mi gao/increase soil for some reason -// case 40: -// case 41: //zerom for some reason -// asset.Type = SpriteType.Monster; -// break; -// case 47: //dullahan for some reason -// case 48: -// asset.Type = SpriteType.Monster2; -// break; -// case 56: -// asset.Type = SpriteType.Monster; //??? -// break; -// case 64: -// asset.Type = SpriteType.Monster; -// break; -// case 72: -// asset.Type = SpriteType.Pet; -// break; -// } -// -// var maxExtent = 0f; -// var totalWidth = 0f; -// var widthCount = 0; -// -// foreach (var a in asset.Actions) -// { -// var frameId = 0; -// foreach (var f in a.Frames) -// { -// if (f.IsAttackFrame) -// asset.AttackFrameTime = a.Delay * frameId; -// -// frameId++; -// -// foreach (var l in f.Layers) -// { -// if (l.Index == -1) -// continue; -// var sprite = asset.SpriteSizes[l.Index]; -// var y = l.Position.y + sprite.y / 2f; -// if (l.Position.x < 0) -// y = Mathf.Abs(l.Position.y - sprite.y / 2f); -// if (y > maxExtent) -// maxExtent = y; -// totalWidth += Mathf.Abs(l.Position.x) + sprite.x / 2f; -// widthCount++; -// } -// } -// } -// -// //far better way to get sprite attack timing -// if (asset.Type == SpriteType.Monster || asset.Type == SpriteType.Monster2 || asset.Type == SpriteType.Pet) -// { -// var actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack1); -// if (actionId == -1) -// actionId = RoAnimationHelper.GetMotionIdForSprite(asset.Type, SpriteMotion.Attack2); -// if (actionId >= 0) -// { -// -// var frames = asset.Actions[actionId].Frames; -// var found = false; -// -// for (var j = 0; j < frames.Length; j++) -// { -// if (frames[j].IsAttackFrame) -// { -// asset.AttackFrameTime = j * asset.Actions[actionId].Delay; -// found = true; -// break; -// } -// } -// -// if (!found) -// { -// var pos = frames.Length - 2; -// if (pos < 0) -// pos = 0; -// asset.AttackFrameTime = pos * asset.Actions[actionId].Delay; -// } -// } -// } -// -// asset.Size = Mathf.CeilToInt(maxExtent); -// asset.AverageWidth = totalWidth / widthCount; -// -// //Debug.Log(asset.Actions.Length); -// -// ctx.AddObjectToAsset(name + " data", asset); -// ctx.SetMainObject(asset); -// // -// // EditorApplication.delayCall += () => -// // { -// // AssetDatabase.Refresh(); -// // -// // var atlasPath = Path.Combine(Path.Combine(Path.GetDirectoryName(assetPath), "atlas/", $"{name}_atlas.png")); -// // TextureImporter importer = (TextureImporter)TextureImporter.GetAtPath(atlasPath); -// // importer.textureType = TextureImporterType.Default; -// // importer.npotScale = TextureImporterNPOTScale.None; -// // importer.textureCompression = TextureImporterCompression.Compressed; -// // importer.crunchedCompression = true; -// // importer.compressionQuality = 50; -// // importer.wrapMode = TextureWrapMode.Clamp; -// // importer.isReadable = false; -// // importer.mipmapEnabled = false; -// // importer.alphaIsTransparency = true; -// // importer.maxTextureSize = 4096; -// // -// // EditorUtility.SetDirty(importer); -// // importer.SaveAndReimport(); -// // AssetDatabase.Refresh(); -// // -// // var newTex = AssetDatabase.LoadAssetAtPath(atlasPath); -// // -// // //var atlasPath = Path.Combine(Path.GetDirectoryName(path), "atlas/", asset.Atlas[0].name + ".png"); -// // var spr = AssetDatabase.LoadAssetAtPath(assetPath); -// // spr.Atlas = newTex; -// // EditorUtility.SetDirty(spr); -// // EditorUtility.SetDirty(spr.Atlas); -// // EditorUtility.SetDirty(newTex); -// // AssetDatabase.SaveAssets(); -// // AssetDatabase.Refresh(); -// // }; -// -// //CreateObjectWithAnimations(obj, ctx, spr.Sprites, actions); -// } -// } -// } diff --git a/RebuildClient/Assets/AddressableAssetsData/Windows/addressables_content_state.bin.meta b/RebuildClient/Assets/Scripts/Editor/Tests.meta similarity index 67% rename from RebuildClient/Assets/AddressableAssetsData/Windows/addressables_content_state.bin.meta rename to RebuildClient/Assets/Scripts/Editor/Tests.meta index dc5b08a2..b17c615c 100644 --- a/RebuildClient/Assets/AddressableAssetsData/Windows/addressables_content_state.bin.meta +++ b/RebuildClient/Assets/Scripts/Editor/Tests.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: 483a8460915db194080f70c54e5efc35 +guid: e51094673ffcbd64880666d8ed819024 +folderAsset: yes DefaultImporter: externalObjects: {} userData: diff --git a/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs b/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs new file mode 100644 index 00000000..da8064b8 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs @@ -0,0 +1,54 @@ +using Assets.Scripts.Sprites; +using NUnit.Framework; + +namespace Assets.Scripts.Editor.Tests +{ + class RoSpriteTests + { + private static readonly RoCompressedBitmapImage RoCompressedImage = new RoCompressedBitmapImage + { + ImageHeight = 5, + ImageWidth = 5, + CompressedSize = 15, + CompressedPaletteIndexes = new byte[] + { + 0x00, 0x05, 0x02, 0x03, 0x00, + 0x04, 0x03, 0x09, 0x08, 0x00, + 0x02, 0x04, 0x00, 0x07, 0x07, + } + }; + + private static readonly RoBitmapImage DecompressedImage = new RoBitmapImage + { + ImageHeight = 5, + ImageWidth = 5, + PaletteIndexes = new byte[] + { + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x09, 0x08, 0x00, + 0x00, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, + } + }; + + [Test] + public void RoSpriteV21DecompressSuccessfully() + { + var result = RoSpr.DecompressBitmapImage(RoCompressedImage); + Assert.AreEqual(DecompressedImage.ImageHeight, result.ImageHeight); + Assert.AreEqual(DecompressedImage.ImageWidth, result.ImageWidth); + Assert.AreEqual(DecompressedImage.PaletteIndexes, result.PaletteIndexes); + } + + [Test] + public void RoSpriteV21CompressSuccessfully() + { + var result = RoSpr.CompressBitmapImage(DecompressedImage); + Assert.AreEqual(RoCompressedImage.ImageHeight, result.ImageHeight); + Assert.AreEqual(RoCompressedImage.ImageWidth, result.ImageWidth); + Assert.AreEqual(RoCompressedImage.CompressedSize, result.CompressedSize); + Assert.AreEqual(RoCompressedImage.CompressedPaletteIndexes, result.CompressedPaletteIndexes); + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs.meta b/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs.meta new file mode 100644 index 00000000..cbceb1f2 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Editor/Tests/RoSpriteTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 756a35b9adfc4784b85d524f03c18090 +timeCreated: 1761272590 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs deleted file mode 100644 index 4d74963c..00000000 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokDirectory.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.IO; -using UnityEditor; -using UnityEngine; - -namespace Assets.Scripts.MapEditor.Editor -{ - class RagnarokDirectory : EditorWindow - { - public static string GetRagnarokDataDirectory - { - get - { - var path = EditorPrefs.GetString("RagnarokDataPath", null); - if(path == null) - throw new Exception("You must set a ragnarok data directory first!"); - return path; - } - } - - //alternative that does not throw an exception - public static string GetRagnarokDataDirectorySafe - { - get - { - var path = EditorPrefs.GetString("RagnarokDataPath", null); - return path; - } - } - - - [MenuItem("Ragnarok/Set Ragnarok Data Directory", priority = 0)] - public static void SetDataDirectory() - { - var defaultName = "Data"; - var oldPath = EditorPrefs.GetString("RagnarokDataPath", null); - if (!string.IsNullOrWhiteSpace(oldPath) && Directory.Exists(oldPath)) - { - var di = new DirectoryInfo(oldPath); - oldPath = di.Parent.FullName; - defaultName = di.Name; - } - var path = EditorUtility.SaveFolderPanel("Locate Ragnarok Data Folder", oldPath, defaultName); - if (Directory.Exists(path)) - { - EditorPrefs.SetString("RagnarokDataPath", path); - Debug.Log("Ragnarok data directory set to: " + path); - } - else - Debug.LogWarning("Failed to set data directory. Using old directory: " + EditorPrefs.GetString("RagnarokDataPath", null)); - } - - [MenuItem("Ragnarok/Open Ragnarok Data Directory", priority = 1)] - public static void OpenDataDirectory() - { - var oldPath = EditorPrefs.GetString("RagnarokDataPath", null); - EditorUtility.RevealInFinder(oldPath); - } - } -} diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokMapImporterWindow.cs b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokMapImporterWindow.cs index 7f16e215..6dffc425 100644 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokMapImporterWindow.cs +++ b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokMapImporterWindow.cs @@ -6,6 +6,7 @@ using Assets.Scripts.Sprites; using B83.Image.BMP; using RebuildSharedData.ClientTypes; +using Scripts.Editor; using SFB; using UnityEditor; using UnityEditor.AddressableAssets; diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokModelLoader.cs b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokModelLoader.cs index 25e7bb21..af631425 100644 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokModelLoader.cs +++ b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokModelLoader.cs @@ -5,6 +5,7 @@ using Assets.Scripts.Objects; using Assets.Scripts.Sprites; using Assets.Scripts.Utility; +using Scripts.Editor; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokWorldSceneBuilder.cs b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokWorldSceneBuilder.cs index b6301ed1..72f51a2e 100644 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokWorldSceneBuilder.cs +++ b/RebuildClient/Assets/Scripts/MapEditor/Editor/RagnarokWorldSceneBuilder.cs @@ -5,6 +5,7 @@ using Assets.Scripts.Objects; using Assets.Scripts.Sprites; using Objects; +using Scripts.Editor; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; diff --git a/RebuildClient/Assets/Scripts/MapEditor/Editor/WaterFix.cs b/RebuildClient/Assets/Scripts/MapEditor/Editor/WaterFix.cs index d4b75724..1c4ab3f5 100644 --- a/RebuildClient/Assets/Scripts/MapEditor/Editor/WaterFix.cs +++ b/RebuildClient/Assets/Scripts/MapEditor/Editor/WaterFix.cs @@ -1,4 +1,5 @@ using System.IO; +using Scripts.Editor; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; diff --git a/RebuildClient/Assets/Scripts/Sprites/RagnarokActFile.cs b/RebuildClient/Assets/Scripts/Sprites/RagnarokActFile.cs deleted file mode 100644 index 2ef80ec7..00000000 --- a/RebuildClient/Assets/Scripts/Sprites/RagnarokActFile.cs +++ /dev/null @@ -1,6 +0,0 @@ -using UnityEngine; - -public class RagnarokActFile : ScriptableObject -{ - //I hate that this needs to be in its own file -} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoAct.cs b/RebuildClient/Assets/Scripts/Sprites/RoAct.cs new file mode 100644 index 00000000..b060aaab --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoAct.cs @@ -0,0 +1,329 @@ +using System; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace Assets.Scripts.Sprites +{ + public class RoAct + { + private readonly struct Versions + { + public const string V20 = "2.0"; + public const string V21 = "2.1"; + public const string V23 = "2.3"; + public const string V24 = "2.4"; + public const string V25 = "2.5"; + } + + public string Version + { + get { return $"{VersionMajor}.{VersionMinor}"; } + } + + public RoAnimationClip[] AnimationClips + { + get { return roActData.AnimationClips; } + set + { + roActData.AnimationClips = value; + roActData.AnimationClipCount = (ushort)value.Length; + } + } + public RoAnimationEvent[] AnimationEvents + { + get + { + var animationEvents = Version switch + { + Versions.V21 or Versions.V23 or Versions.V24 or Versions.V25 => roActData.AnimationEvents, + Versions.V20 => throw new InvalidDataException("Animation events are available on version 2.1+"), + _ => throw new InvalidDataException("Invalid act version?") + }; + return animationEvents; + } + set + { + switch (Version) + { + case Versions.V21: + case Versions.V23: + case Versions.V24: + case Versions.V25: + roActData.AnimationEvents = value; + roActData.AnimationEventCount = (uint)value.Length; + break; + case Versions.V20: + throw new InvalidDataException("Animation events are available on version 2.1+"); + default: + throw new InvalidDataException("Invalid act version?"); + } + } + } + public float[] FrameTimes + { + get + { + var frameTimes = Version switch + { + Versions.V23 or Versions.V24 or Versions.V25 => roActData.FrameTimes, + Versions.V20 or Versions.V21 => throw new InvalidDataException("Frame times are available on version 2.3+"), + _ => throw new InvalidDataException("Invalid act version?") + }; + return frameTimes; + } + set + { + switch (Version) + { + case Versions.V23: + case Versions.V24: + case Versions.V25: + if (value.Length != roActData.AnimationClipCount) + throw new InvalidDataException("Invalid frameTime array size"); + roActData.FrameTimes = value; + break; + case Versions.V20: + case Versions.V21: + throw new InvalidDataException("Frame times are available on version 2.3+"); + default: + throw new InvalidDataException("Invalid act version?"); + } + } + } + + private char[] Signature + { + get { return roActData.Signature; } + set { roActData.Signature = value; } + } + private byte VersionMajor + { + get { return roActData.VersionMajor; } + set { roActData.VersionMajor = value; } + } + private byte VersionMinor + { + get { return roActData.VersionMinor; } + set { roActData.VersionMinor = value; } + } + private ushort AnimationClipCount + { + get { return roActData.AnimationClipCount; } + set { roActData.AnimationClipCount = value; } + } + private uint AnimationEventCount + { + get { return roActData.AnimationEventCount; } + set { roActData.AnimationEventCount = value; } + } + + private RoActData roActData; + + public RoAct() {} + public RoAct(string filepath) + { + Debug.Log("LETS FUCKING GOOOO"); + ReadBytes(filepath); + } + public RoAct(FileStream fileStream) + { + ReadBytes(fileStream); + } + public RoAct(BinaryReader binaryReader) + { + ReadBytes(binaryReader); + } + + public void WriteBytes(string filePath) + { + + } + + public void ReadBytes(string filePath) + { + Debug.Log("ReadBytes string"); + var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + try + { + ReadBytes(fileStream); + } + catch (NotSupportedException) + { + fileStream.Close(); + throw; + } + fileStream.Close(); + } + + public void ReadBytes(FileStream fileStream) + { + Debug.Log("ReadBytes filestream"); + var binaryReader = new BinaryReader(fileStream); + try + { + ReadBytes(binaryReader); + } + catch (NotSupportedException) + { + binaryReader.Close(); + throw; + } + binaryReader.Close(); + } + + public void ReadBytes(BinaryReader binaryReader) + { + Debug.Log("ReadBytes binaryreader"); + roActData = new RoActData(); + + Debug.Log("Reading signature"); + Signature = binaryReader.ReadChars(2); + if (new string(Signature) != "AC") + { + throw new NotSupportedException("Not a act file"); + } + + Debug.Log("Reading version"); + VersionMinor = binaryReader.ReadByte(); + VersionMajor = binaryReader.ReadByte(); + if (!new[] { "2.0", "2.1", "2.3", "2.4", "2.5" }.Contains(Version)) + { + throw new NotSupportedException("Unsupported act version"); + } + Debug.Log($"Version is {Version}"); + + AnimationClipCount = binaryReader.ReadUInt16(); + Debug.Log($"We have {AnimationClipCount} animations"); + + var skip = binaryReader.ReadBytes(10); + Debug.Log($"Skipping irrelevant bytes: {BitConverter.ToString(skip)}"); + + AnimationClips = new RoAnimationClip[AnimationClipCount]; + for (var cIndex = 0; cIndex < AnimationClipCount; cIndex++) + { + Debug.Log($"Reading animation clip {cIndex}"); + var animationClip = new RoAnimationClip(); + animationClip.AnimationFrameCount = binaryReader.ReadUInt32(); + Debug.Log($"Animation clip {cIndex} has {animationClip.AnimationFrameCount} frames"); + + animationClip.AnimationFrames = new RoAnimationFrame[animationClip.AnimationFrameCount]; + for (var fIndex = 0; fIndex < animationClip.AnimationFrameCount; fIndex++) + { + var animationFrame = new RoAnimationFrame(); + _ = binaryReader.ReadBytes(32); + animationFrame.SpriteLayerCount = binaryReader.ReadUInt32(); + animationFrame.SpriteLayers = new RoSpriteLayer[animationFrame.SpriteLayerCount]; + for (var sIndex = 0; sIndex < animationFrame.SpriteLayerCount; sIndex++) + { + var spriteLayer = new RoSpriteLayer(); + spriteLayer.PositionU = binaryReader.ReadUInt32(); + spriteLayer.PositionV = binaryReader.ReadUInt32(); + spriteLayer.SpritesheetCellIndex = binaryReader.ReadUInt32(); + spriteLayer.IsFlippedV = binaryReader.ReadUInt32(); + spriteLayer.ColorTintRed = binaryReader.ReadByte(); + spriteLayer.ColorTintGreen = binaryReader.ReadByte(); + spriteLayer.ColorTintBlue = binaryReader.ReadByte(); + spriteLayer.ColorTintAlpha = binaryReader.ReadByte(); + switch (Version) + { + case Versions.V20: + case Versions.V21: + case Versions.V23: + spriteLayer.Scale = binaryReader.ReadSingle(); + break; + case Versions.V24: + case Versions.V25: + spriteLayer.ScaleU = binaryReader.ReadSingle(); + spriteLayer.ScaleV = binaryReader.ReadSingle(); + break; + default: + throw new InvalidDataException("Invalid act version?"); + } + spriteLayer.RotationDegrees = binaryReader.ReadUInt32(); + spriteLayer.ImageTypeID = binaryReader.ReadUInt32(); + switch (Version) + { + case Versions.V25: + spriteLayer.ImageWidth = binaryReader.ReadUInt32(); + spriteLayer.ImageHeight = binaryReader.ReadUInt32(); + break; + case Versions.V20: + case Versions.V21: + case Versions.V23: + case Versions.V24: + break; + default: + throw new InvalidDataException("Invalid act version?"); + } + animationFrame.SpriteLayers[sIndex] = spriteLayer; + } + animationFrame.AnimationEventId = binaryReader.ReadUInt32(); + switch (Version) + { + case Versions.V23: + case Versions.V24: + case Versions.V25: + animationFrame.SpriteAnchorCount = binaryReader.ReadUInt32(); + animationFrame.AnchorPoints = new RoSpriteAnchor[animationFrame.SpriteAnchorCount]; + for (var saIndex = 0; saIndex < animationFrame.SpriteAnchorCount; saIndex++) + { + var anchorPoints = new RoSpriteAnchor(); + _ = binaryReader.ReadBytes(4); + anchorPoints.PositionU = binaryReader.ReadUInt32(); + anchorPoints.PositionV = binaryReader.ReadUInt32(); + _ = binaryReader.ReadUInt32(); + animationFrame.AnchorPoints[saIndex] = anchorPoints; + } + break; + case Versions.V20: + case Versions.V21: + break; + default: + throw new InvalidDataException("Invalid act version?"); + } + animationClip.AnimationFrames[fIndex] = animationFrame; + } + AnimationClips[cIndex] = animationClip; + switch (Version) + { + case Versions.V21: + case Versions.V23: + case Versions.V24: + case Versions.V25: + AnimationEventCount = binaryReader.ReadUInt32(); + AnimationEvents = new RoAnimationEvent[AnimationEventCount]; + for (var eIndex = 0; eIndex < AnimationEventCount; eIndex++) + { + var animationEvent = new RoAnimationEvent(); + animationEvent.Name = binaryReader.ReadChars(40); + AnimationEvents[eIndex] = animationEvent; + } + break; + case Versions.V20: + break; + default: + throw new InvalidDataException("Invalid act version?"); + } + switch (Version) + { + case Versions.V23: + case Versions.V24: + case Versions.V25: + FrameTimes = new float[AnimationClipCount]; + for (var tIndex = 0; tIndex < AnimationClipCount; tIndex++) + { + var frameTime = binaryReader.ReadSingle(); + FrameTimes[tIndex] = frameTime; + } + break; + case Versions.V20: + case Versions.V21: + break; + default: + throw new InvalidDataException("Invalid act version?"); + } + } + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoAct.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoAct.cs.meta new file mode 100644 index 00000000..5bda1d4c --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoAct.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 362577d110d34e888f95c7fdd458c334 +timeCreated: 1761679491 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoActAsset.cs b/RebuildClient/Assets/Scripts/Sprites/RoActAsset.cs new file mode 100644 index 00000000..537bef9c --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoActAsset.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.VersionControl; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Assets.Scripts.Sprites +{ + // RoActData || Animation + // RoActData.RoAnimationClip || AnimationClip + // RoActData.RoAnimationFrame.RoSpriteLayer || AnimationCurve + // RoActData.RoAnimationEvents || AnimationClip.events + + public class RoActAsset : ScriptableObject + { + public int InstanceID + { + get + { + if (instanceID == 0) + instanceID = GetInstanceID(); + return instanceID; + } + } + public int HashCode + { + get { return InstanceID.GetHashCode(); } + } + + [HideInInspector] public string actVersion; + [HideInInspector] public string filePath; + [HideInInspector] public string actFileName; + [HideInInspector] public List animationClips; + [HideInInspector] public RoSprAsset spr; + + private int instanceID; + + public void Load(string assetFilePath) + { + Debug.Log($"Getting spr asset"); + spr = AssetDatabase.LoadAssetAtPath(Path.ChangeExtension(filePath, "spr")); + filePath = assetFilePath; + actFileName = Path.GetFileNameWithoutExtension(filePath); + animationClips = new List(); + + Debug.Log($"Reading act at {filePath}"); + var rawActData = new RoAct(filePath); + actVersion = rawActData.Version; + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RagnarokActFile.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoActAsset.cs.meta similarity index 100% rename from RebuildClient/Assets/Scripts/Sprites/RagnarokActFile.cs.meta rename to RebuildClient/Assets/Scripts/Sprites/RoActAsset.cs.meta diff --git a/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs b/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs new file mode 100644 index 00000000..3846967e --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs @@ -0,0 +1,59 @@ +namespace Assets.Scripts.Sprites +{ + public class RoActData + { + public char[] Signature { get; set; } //2.0 + public byte VersionMinor { get; set; } //2.0 + public byte VersionMajor { get; set; } //2.0 + public ushort AnimationClipCount { get; set; } //2.0 + public RoAnimationClip[] AnimationClips { get; set; } //2.0 + public uint AnimationEventCount { get; set; } //2.1 + public RoAnimationEvent[] AnimationEvents { get; set; } //2.1 + public float[] FrameTimes { get; set; } //2.3 + } + + public class RoAnimationClip + { + public uint AnimationFrameCount; //2.0 + public RoAnimationFrame[] AnimationFrames; //2.0 + } + + public class RoAnimationFrame + { + public uint SpriteLayerCount {get; set;} //2.0 + public RoSpriteLayer[] SpriteLayers {get; set;} //2.0 + public uint AnimationEventId {get; set;} //2.0 + public uint SpriteAnchorCount {get; set;} //2.3 + public RoSpriteAnchor[] AnchorPoints {get; set;} //2.3 + } + + public class RoAnimationEvent + { + public char[] Name; //2.1 + } + + + public class RoSpriteLayer + { + public uint PositionU {get; set;} //2.0 + public uint PositionV {get; set;} //2.0 + public uint SpritesheetCellIndex {get; set;} //2.0 + public uint IsFlippedV {get; set;} //2.0 + public byte ColorTintRed {get; set;} //2.0 + public byte ColorTintGreen {get; set;} //2.0 + public byte ColorTintBlue {get; set;} //2.0 + public byte ColorTintAlpha {get; set;} //2.0 + public float Scale {get; set;} //2.0 - //2.3 + public float ScaleU {get; set;} //2.4 + public float ScaleV {get; set;} //2.4 + public uint RotationDegrees {get; set;} //2.0 + public uint ImageTypeID {get; set;} //2.0 + public uint ImageWidth {get; set;} //2.5 + public uint ImageHeight {get; set;} //2.5 + } + + public class RoSpriteAnchor { + public uint PositionU; //2.3 + public uint PositionV; //2.3 + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs.meta new file mode 100644 index 00000000..05ebffe9 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoActDataStructures.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7cb4c28c83e0421ab542a84d12d9c365 +timeCreated: 1761688354 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs b/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs new file mode 100644 index 00000000..ea732816 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace Assets.Scripts.Sprites +{ + public class RoSpr + { + private readonly struct Versions + { + public const string V20 = "2.0"; + public const string V21 = "2.1"; + } + + public string Version + { + get { return $"{VersionMajor}.{VersionMinor}"; } + } + public RoBitmapImage[] BitmapImages + { + get + { + var bitmapImages = Version switch + { + Versions.V20 => roSprData.BitmapImages, + Versions.V21 => DecompressBitmapImage(roSprData.CompressedBitmapImages), + _ => throw new InvalidDataException("Invalid spr version?") + }; + return bitmapImages; + } + set + { + switch (Version) + { + case Versions.V20: + { + if (value.Length > ushort.MaxValue) + { + throw new FormatException($"BitMapImages cant have more than {ushort.MaxValue} entries"); + } + roSprData.BitmapImages = value; + roSprData.BitmapImageCount = (ushort)value.Length; + break; + } + case Versions.V21: + { + if (value.Length > ushort.MaxValue) + { + throw new FormatException($"BitMapImages cant have more than {ushort.MaxValue} entries"); + } + roSprData.CompressedBitmapImages = CompressBitmapImage(value); + roSprData.BitmapImageCount = (ushort)value.Length; + break; + } + + } + } + } + public RoTrueColorImage[] TrueColorImages + { + get { return roSprData.TrueColorImages; } + + set + { + if (value.Length > ushort.MaxValue) + { + throw new FormatException($"TrueColorImages cant have more than {ushort.MaxValue} entries"); + } + roSprData.TrueColorImages = value; + roSprData.TrueColorImageCount = (ushort)value.Length; + } + } + public Color32[] PaletteColors + { + get { return roSprData.PaletteColors; } + set + { + if (value.Length > 256) + { + throw new FormatException("PaletteColors cant have more than 256 entries"); + } + roSprData.PaletteColors = value; + } + } + + private char[] Signature + { + get { return roSprData.Signature; } + set { roSprData.Signature = value; } + } + private byte VersionMajor + { + get { return roSprData.VersionMajor; } + set { roSprData.VersionMajor = value; } + } + private byte VersionMinor + { + get { return roSprData.VersionMinor; } + set { roSprData.VersionMinor = value; } + } + private ushort BitmapImageCount + { + get { return roSprData.BitmapImageCount; } + set { roSprData.BitmapImageCount = value; } + } + private RoCompressedBitmapImage[] CompressedBitmapImages + { + get + { + var compressedBitmapImages = Version switch + { + Versions.V21 => roSprData.CompressedBitmapImages, + Versions.V20 => throw new InvalidDataException("Compressed images are available on version 2.1"), + _ => throw new InvalidDataException("Invalid spr version?") + }; + return compressedBitmapImages; + } + set + { + switch (Version) + { + case Versions.V21: + if (value.Length > ushort.MaxValue) + { + throw new FormatException($"BitmapImages cant have more than {ushort.MaxValue} entries"); + } + roSprData.CompressedBitmapImages = value; + roSprData.BitmapImageCount = (ushort)value.Length; + break; + case Versions.V20: + throw new InvalidDataException("Compressed images are available on version 2.1"); + default: + throw new InvalidDataException("Invalid spr version?"); + } + } + } + private ushort TrueColorImageCount + { + get { return roSprData.TrueColorImageCount; } + set { roSprData.TrueColorImageCount = value; } + } + + private RoSprData roSprData; + + /// + /// Compress the given RoBitmapImage and return a RoCompressedBitmapImage + /// + /// + /// + public static RoCompressedBitmapImage CompressBitmapImage(RoBitmapImage image) + { + var compressedPaletteIndexesList = new List(); + var zeroRun = false; + byte zeroCount = 0; + foreach (var colorIndex in image.PaletteIndexes) + { + switch (colorIndex) + { + case 0 when !zeroRun: + zeroRun = true; + zeroCount++; + compressedPaletteIndexesList.Add(colorIndex); + break; + case 0: + zeroCount++; + break; + case > 0 when !zeroRun: + compressedPaletteIndexesList.Add(colorIndex); + break; + case > 0: + zeroRun = false; + compressedPaletteIndexesList.Add(zeroCount); + zeroCount = 0; + compressedPaletteIndexesList.Add(colorIndex); + break; + } + } + var compressedImage = new RoCompressedBitmapImage + { + ImageHeight = image.ImageHeight, + ImageWidth = image.ImageWidth, + CompressedSize = (ushort)compressedPaletteIndexesList.Count, + CompressedPaletteIndexes = compressedPaletteIndexesList.ToArray() + }; + return compressedImage; + } + /// + /// Compress the given RoBitmapImage array and return a RoCompressedBitmapImage array + /// + /// + /// + public static RoCompressedBitmapImage[] CompressBitmapImage(RoBitmapImage[] images) + { + var compressedBitmapImages = new RoCompressedBitmapImage[images.Length]; + foreach (var ii in images.Select((image, index) => new { image, index })) + { + var compressedBitmapImage = CompressBitmapImage(ii.image); + compressedBitmapImages[ii.index] = compressedBitmapImage; + } + return compressedBitmapImages; + } + public static RoBitmapImage DecompressBitmapImage(RoCompressedBitmapImage roCompressedImage) + { + var decompressedPaletteIndexes = new byte[roCompressedImage.ImageWidth * roCompressedImage.ImageHeight]; + var decompressedIndex = 0; + var zeroRun = false; + for (var compressedIndex = 0; compressedIndex < roCompressedImage.CompressedSize; compressedIndex++) + { + var currentByte = roCompressedImage.CompressedPaletteIndexes[compressedIndex]; + switch (currentByte) + { + case 0 when !zeroRun: + zeroRun = true; + break; + case 0: + throw new InvalidDataException("Found 00 00 while decompressing sprite. Not RLE?"); + case > 0 when zeroRun: + { + for (var count = 1; count <= currentByte; count++) + { + decompressedPaletteIndexes[decompressedIndex++] = 0x00; + } + zeroRun = false; + break; + } + case > 0: + decompressedPaletteIndexes[decompressedIndex++] = currentByte; + break; + } + } + var bitmapImage = new RoBitmapImage() + { + ImageHeight = roCompressedImage.ImageHeight, + ImageWidth = roCompressedImage.ImageWidth, + PaletteIndexes = decompressedPaletteIndexes + }; + return bitmapImage; + } + public static RoBitmapImage[] DecompressBitmapImage(RoCompressedBitmapImage[] compressedImages) + { + var bitmapImages = new RoBitmapImage[compressedImages.Length]; + foreach (var ii in compressedImages.Select((image, index) => new { image, index })) + { + var bitmapImage = DecompressBitmapImage(ii.image); + bitmapImages[ii.index] = bitmapImage; + } + return bitmapImages; + } + + public RoSpr(string filename) + { + ReadBytes(filename); + } + + public RoSpr(FileStream filestream) + { + ReadBytes(filestream); + } + + public RoSpr(BinaryReader binaryReader) + { + ReadBytes(binaryReader); + } + + /// + /// Write spr data to file + /// + /// + public void WriteBytes(string filePath) + { + var fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + var binaryWriter = new BinaryWriter(fileStream); + + binaryWriter.Write(Signature); + binaryWriter.Write(VersionMinor); + binaryWriter.Write(VersionMajor); + binaryWriter.Write(BitmapImageCount); + binaryWriter.Write(TrueColorImageCount); + switch (Version) + { + case Versions.V20: + { + foreach (var bitmapImage in BitmapImages) + { + binaryWriter.Write(bitmapImage.ImageWidth); + binaryWriter.Write(bitmapImage.ImageWidth); + binaryWriter.Write(bitmapImage.PaletteIndexes); + } + break; + } + case Versions.V21: + { + foreach (var compressedBitmapImage in CompressedBitmapImages) + { + binaryWriter.Write(compressedBitmapImage.ImageWidth); + binaryWriter.Write(compressedBitmapImage.ImageHeight); + binaryWriter.Write(compressedBitmapImage.CompressedSize); + binaryWriter.Write(compressedBitmapImage.CompressedPaletteIndexes); + } + break; + } + } + foreach (var trueColorImage in TrueColorImages) + { + binaryWriter.Write(trueColorImage.ImageWidth); + binaryWriter.Write(trueColorImage.ImageHeight); + foreach (var color in trueColorImage.ImageData) + { + binaryWriter.Write(color); + } + } + + foreach (var color in PaletteColors) + { + binaryWriter.Write(color.r); + binaryWriter.Write(color.g); + binaryWriter.Write(color.b); + binaryWriter.Write(color.a); + } + + binaryWriter.Close(); + fileStream.Close(); + } + + /// + /// Read spr data from file + /// + /// + public void ReadBytes(string filePath) + { + var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + try + { + ReadBytes(fileStream); + } + catch (NotSupportedException) + { + fileStream.Close(); + throw; + } + fileStream.Close(); + } + + public void ReadBytes(FileStream fileStream) + { + var binaryReader = new BinaryReader(fileStream); + try + { + ReadBytes(binaryReader); + } + catch (NotSupportedException) + { + binaryReader.Close(); + throw; + } + binaryReader.Close(); + } + + public void ReadBytes(BinaryReader binaryReader) + { + roSprData = new RoSprData(); + + Signature = binaryReader.ReadChars(2); + if (new string(Signature) != "SP") + { + throw new NotSupportedException("Not a spr file"); + } + VersionMinor = binaryReader.ReadByte(); + VersionMajor = binaryReader.ReadByte(); + if (!new[] { "2.0", "2.1" }.Contains(Version)) + { + throw new NotSupportedException("Unsupported spr version"); + } + BitmapImageCount = binaryReader.ReadUInt16(); + TrueColorImageCount = binaryReader.ReadUInt16(); + + switch (Version) + { + case Versions.V20: + { + BitmapImages = new RoBitmapImage[BitmapImageCount]; + for (var index = 0; index < BitmapImageCount; index++) + { + var bitmapSprite = new RoBitmapImage + { + ImageWidth = binaryReader.ReadUInt16(), + ImageHeight = binaryReader.ReadUInt16() + }; + bitmapSprite.PaletteIndexes = new byte[bitmapSprite.ImageWidth * bitmapSprite.ImageHeight]; + bitmapSprite.PaletteIndexes = binaryReader.ReadBytes(bitmapSprite.PaletteIndexes.Length); + BitmapImages[index] = bitmapSprite; + } + break; + } + case Versions.V21: + { + CompressedBitmapImages = new RoCompressedBitmapImage[BitmapImageCount]; + for (var index = 0; index < BitmapImageCount; index++) + { + var bitmapSprite = new RoCompressedBitmapImage + { + ImageWidth = binaryReader.ReadUInt16(), + ImageHeight = binaryReader.ReadUInt16(), + CompressedSize = binaryReader.ReadUInt16() + }; + bitmapSprite.CompressedPaletteIndexes = new byte[bitmapSprite.CompressedSize]; + bitmapSprite.CompressedPaletteIndexes = binaryReader.ReadBytes(bitmapSprite.CompressedPaletteIndexes.Length); + CompressedBitmapImages[index] = bitmapSprite; + } + break; + } + + } + + + TrueColorImages = new RoTrueColorImage[TrueColorImageCount]; + for (var index = 0; index < TrueColorImageCount; index++) + { + var trueColorSprite = new RoTrueColorImage + { + ImageWidth = binaryReader.ReadUInt16(), + ImageHeight = binaryReader.ReadUInt16() + }; + trueColorSprite.ImageData = new Color32[trueColorSprite.ImageWidth * trueColorSprite.ImageHeight]; + for (var pixelIndex = 0; pixelIndex < trueColorSprite.ImageData.Length; pixelIndex++) + { + var trueColorPixel = new Color32() + { + a = binaryReader.ReadByte(), + b = binaryReader.ReadByte(), + g = binaryReader.ReadByte(), + r = binaryReader.ReadByte() + }; + trueColorPixel.a = byte.MaxValue; + trueColorSprite.ImageData[pixelIndex] = trueColorPixel; + } + TrueColorImages[index] = trueColorSprite; + } + + PaletteColors = new Color32[256]; + for (var index = 0; index < 256; index++) + { + var bitmapColor = new Color32() + { + r = binaryReader.ReadByte(), + g = binaryReader.ReadByte(), + b = binaryReader.ReadByte(), + a = binaryReader.ReadByte() + }; + bitmapColor.a = byte.MaxValue; + PaletteColors[index] = bitmapColor; + } + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs.meta new file mode 100644 index 00000000..99058505 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSpr.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 25f5607870ca40708343a4eecef8253a +timeCreated: 1756156446 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs b/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs new file mode 100644 index 00000000..b614de2e --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace Assets.Scripts.Sprites +{ + public class RoSprAsset : ScriptableObject + { + public int InstanceID + { + get + { + if (instanceID == 0) + instanceID = GetInstanceID(); + return instanceID; + } + } + public int HashCode + { + get { return InstanceID.GetHashCode(); } + } + + [HideInInspector] public string sprVersion; + [HideInInspector] public string filepath; + [HideInInspector] public string sprFileName; + [HideInInspector] public Texture2D palette; + [HideInInspector] public Texture2D atlas; + [HideInInspector] public Rect[] atlasRects; + + private int instanceID; + + public void Load(string assetFilePath) + { + filepath = assetFilePath; + sprFileName = Path.GetFileNameWithoutExtension(filepath); + atlas = new Texture2D(2, 2, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Point, + alphaIsTransparency = true, + name = $"{sprFileName}_atlas" + }; + + var rawSprData = new RoSpr(filepath); + + sprVersion = rawSprData.Version; + var palTex = new Texture2D(16, 16, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Point, + alphaIsTransparency = false, + name = $"{sprFileName}_palette" + }; + foreach (var ci in rawSprData.PaletteColors.Select((color, index) => new { color, index })) + { + var pColor = rawSprData.PaletteColors[ci.index]; + palTex.SetPixel( + ci.index % 16, + 15 - ci.index / 16, + pColor + ); + } + palTex.Apply(); + palette = palTex; + + var sprites = new List(); + foreach (var bitMapSprite in rawSprData.BitmapImages) + { + var texture = new Texture2D(bitMapSprite.ImageWidth, bitMapSprite.ImageHeight, TextureFormat.RGBA32, false) + { + filterMode = FilterMode.Point, + alphaIsTransparency = false + }; + foreach (var cii in bitMapSprite.PaletteIndexes.Select((colorIndex, index) => new { colorIndex, index })) + { + var pColor = cii.colorIndex == 0 ? new Color32(0, 0, 0, 0) : rawSprData.PaletteColors[cii.colorIndex]; + texture.SetPixel( + cii.index % bitMapSprite.ImageWidth, + bitMapSprite.ImageHeight - cii.index / bitMapSprite.ImageWidth, + pColor + ); + } + texture.Apply(); + sprites.Add(texture); + } + atlasRects = atlas.PackTextures(sprites.ToArray(), 0); + sprites.Clear(); + } + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs.meta new file mode 100644 index 00000000..2e4938b3 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSprAsset.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e987896378db4aecbb9a342842a81948 +timeCreated: 1756151952 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs b/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs new file mode 100644 index 00000000..63750e17 --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs @@ -0,0 +1,39 @@ +using UnityEngine; + +namespace Assets.Scripts.Sprites +{ + public class RoSprData + { + public char[] Signature { get; set; } + public byte VersionMinor { get; set; } + public byte VersionMajor { get; set; } + public ushort BitmapImageCount { get; set; } + public ushort TrueColorImageCount { get; set; } + public RoBitmapImage[] BitmapImages { get; set; } + public RoCompressedBitmapImage[] CompressedBitmapImages { get; set; } + public RoTrueColorImage[] TrueColorImages { get; set; } + public Color32[] PaletteColors { get; set; } + } + + public class RoBitmapImage + { + public ushort ImageWidth; + public ushort ImageHeight; + public byte[] PaletteIndexes; + } + + public class RoCompressedBitmapImage + { + public ushort ImageWidth; + public ushort ImageHeight; + public ushort CompressedSize; + public byte[] CompressedPaletteIndexes; + } + + public class RoTrueColorImage + { + public ushort ImageWidth; + public ushort ImageHeight; + public Color32[] ImageData; + } +} \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs.meta b/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs.meta new file mode 100644 index 00000000..272489de --- /dev/null +++ b/RebuildClient/Assets/Scripts/Sprites/RoSprDataStructures.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 30341c399e164626a82f9fd5d879cbc7 +timeCreated: 1761683838 \ No newline at end of file diff --git a/RebuildClient/Assets/Scripts/Utility/bmploader.cs b/RebuildClient/Assets/Scripts/Utility/bmploader.cs index ee8e0035..3d9872b0 100644 --- a/RebuildClient/Assets/Scripts/Utility/bmploader.cs +++ b/RebuildClient/Assets/Scripts/Utility/bmploader.cs @@ -141,13 +141,11 @@ public BMPImage LoadBMP(BinaryReader aReader) BMPImage bmp = new BMPImage(); if (!ReadFileHeader(aReader, ref bmp.header)) { - Debug.LogError("Not a BMP file"); - return null; + throw new FileLoadException("Not a valid BMP file."); } if (!ReadInfoHeader(aReader, ref bmp.info)) { - Debug.LogError("Unsupported header format"); - return null; + throw new FileLoadException("Unsupported header format"); } if ( bmp.info.compressionMethod != BMPComressionMode.BI_RGB && bmp.info.compressionMethod != BMPComressionMode.BI_BITFIELDS @@ -156,8 +154,7 @@ public BMPImage LoadBMP(BinaryReader aReader) && bmp.info.compressionMethod != BMPComressionMode.BI_RLE8 ) { - Debug.LogError("Unsupported image format: "+ bmp.info.compressionMethod); - return null; + throw new FileLoadException($"Unsupported image format: {bmp.info.compressionMethod}"); } long offset = 14 + bmp.info.size; aReader.BaseStream.Seek(offset, SeekOrigin.Begin); @@ -188,22 +185,50 @@ public BMPImage LoadBMP(BinaryReader aReader) bool uncompressed = bmp.info.compressionMethod == BMPComressionMode.BI_RGB || bmp.info.compressionMethod == BMPComressionMode.BI_BITFIELDS || bmp.info.compressionMethod == BMPComressionMode.BI_ALPHABITFIELDS; - if (bmp.info.nBitsPerPixel == 32 && uncompressed) - Read32BitImage(aReader, bmp); - else if (bmp.info.nBitsPerPixel == 24 && uncompressed) - Read24BitImage(aReader, bmp); - else if (bmp.info.nBitsPerPixel == 16 && uncompressed) - Read16BitImage(aReader, bmp); - else if (bmp.info.compressionMethod == BMPComressionMode.BI_RLE4 && bmp.info.nBitsPerPixel == 4 && bmp.palette != null) - ReadIndexedImageRLE4(aReader, bmp); - else if (bmp.info.compressionMethod == BMPComressionMode.BI_RLE8 && bmp.info.nBitsPerPixel == 8 && bmp.palette != null) - ReadIndexedImageRLE8(aReader, bmp); - else if (uncompressed && bmp.info.nBitsPerPixel <= 8 && bmp.palette != null) - ReadIndexedImage(aReader, bmp); - else + switch (bmp.info.nBitsPerPixel) { - Debug.LogError("Unsupported file format: " + bmp.info.compressionMethod + " BPP: " + bmp.info.nBitsPerPixel); - return null; + case 32 when uncompressed: + Read32BitImage(aReader, bmp); + break; + case 24 when uncompressed: + Read24BitImage(aReader, bmp); + break; + case 16 when uncompressed: + Read16BitImage(aReader, bmp); + break; + default: + { + switch (bmp.info.compressionMethod) + { + case BMPComressionMode.BI_RLE4 when bmp.info.nBitsPerPixel == 4 && bmp.palette != null: + ReadIndexedImageRLE4(aReader, bmp); + break; + case BMPComressionMode.BI_RLE8 when bmp.info.nBitsPerPixel == 8 && bmp.palette != null: + ReadIndexedImageRLE8(aReader, bmp); + break; + case BMPComressionMode.BI_RGB: + case BMPComressionMode.BI_BITFIELDS: + case BMPComressionMode.BI_JPEG: + case BMPComressionMode.BI_PNG: + case BMPComressionMode.BI_ALPHABITFIELDS: + case BMPComressionMode.BI_CMYK: + case BMPComressionMode.BI_CMYKRLE8: + case BMPComressionMode.BI_CMYKRLE4: + default: + { + if (uncompressed && bmp.info.nBitsPerPixel <= 8 && bmp.palette != null) + ReadIndexedImage(aReader, bmp); + else + { + throw new FileLoadException($"Unsupported file format: {bmp.info.compressionMethod} BPP: {bmp.info.nBitsPerPixel}"); + } + + break; + } + } + + break; + } } return bmp; } @@ -216,8 +241,7 @@ private static void Read32BitImage(BinaryReader aReader, BMPImage bmp) Color32[] data = bmp.imageData = new Color32[w * h]; if (aReader.BaseStream.Position + w*h*4 > aReader.BaseStream.Length) { - Debug.LogError("Unexpected end of file."); - return; + throw new FileLoadException("Unexpected end of file on BMP stream"); } int shiftR = GetShiftCount(bmp.rMask); int shiftG = GetShiftCount(bmp.gMask); @@ -247,8 +271,7 @@ private static void Read24BitImage(BinaryReader aReader, BMPImage bmp) Color32[] data = bmp.imageData = new Color32[w * h]; if (aReader.BaseStream.Position + count > aReader.BaseStream.Length) { - Debug.LogError("Unexpected end of file. (Have "+ (aReader.BaseStream.Position + count)+" bytes, expected " + aReader.BaseStream.Length+" bytes)"); - return; + throw new FileLoadException($"Unexpected end of file. (Have {(aReader.BaseStream.Position + count)} bytes, expected {aReader.BaseStream.Length} bytes)"); } int shiftR = GetShiftCount(bmp.rMask); int shiftG = GetShiftCount(bmp.gMask); @@ -278,8 +301,7 @@ private static void Read16BitImage(BinaryReader aReader, BMPImage bmp) Color32[] data = bmp.imageData = new Color32[w * h]; if (aReader.BaseStream.Position + count > aReader.BaseStream.Length) { - Debug.LogError("Unexpected end of file. (Have " + (aReader.BaseStream.Position + count) + " bytes, expected " + aReader.BaseStream.Length + " bytes)"); - return; + throw new FileLoadException($"Unexpected end of file. (Have {aReader.BaseStream.Position + count} bytes, expected {aReader.BaseStream.Length} bytes)"); } int shiftR = GetShiftCount(bmp.rMask); int shiftG = GetShiftCount(bmp.gMask); @@ -314,8 +336,7 @@ private static void ReadIndexedImage(BinaryReader aReader, BMPImage bmp) Color32[] data = bmp.imageData = new Color32[w * h]; if (aReader.BaseStream.Position + count > aReader.BaseStream.Length) { - Debug.LogError("Unexpected end of file. (Have " + (aReader.BaseStream.Position + count) + " bytes, expected " + aReader.BaseStream.Length + " bytes)"); - return; + throw new FileLoadException($"Unexpected end of file. (Have {aReader.BaseStream.Position + count} bytes, expected {aReader.BaseStream.Length} bytes)"); } BitStreamReader bitReader = new BitStreamReader(aReader); for (int y = 0; y < h; y++) @@ -325,8 +346,7 @@ private static void ReadIndexedImage(BinaryReader aReader, BMPImage bmp) int v = (int)bitReader.ReadBits(bitCount); if (v >= bmp.palette.Count) { - Debug.LogError("Indexed bitmap has indices greater than it's color palette"); - return; + throw new FileLoadException("Indexed bitmap has indices greater than it's color palette"); } data[x + y * w] = bmp.palette[v]; } @@ -345,7 +365,7 @@ private static void ReadIndexedImageRLE4(BinaryReader aReader, BMPImage bmp) int yOffset = 0; while (aReader.BaseStream.Position < aReader.BaseStream.Length-1) { - int count = (int)aReader.ReadByte(); + int count = aReader.ReadByte(); byte d = aReader.ReadByte(); if (count > 0) { @@ -407,7 +427,7 @@ private static void ReadIndexedImageRLE8(BinaryReader aReader, BMPImage bmp) int yOffset = 0; while (aReader.BaseStream.Position < aReader.BaseStream.Length - 1) { - int count = (int)aReader.ReadByte(); + int count = aReader.ReadByte(); byte d = aReader.ReadByte(); if (count > 0) { @@ -498,6 +518,7 @@ private static bool ReadInfoHeader(BinaryReader aReader, ref BitmapInfoHeader aH aReader.ReadBytes(pad); return true; } + public static List ReadPalette(BinaryReader aReader, BMPImage aBmp, bool aReadAlpha) { uint count = aBmp.info.nPaletteColors; @@ -520,9 +541,9 @@ public static List ReadPalette(BinaryReader aReader, BMPImage aBmp, boo } public class BitStreamReader { - BinaryReader m_Reader; - byte m_Data = 0; - int m_Bits = 0; + private readonly BinaryReader m_Reader; + private byte m_Data; + private int m_Bits; public BitStreamReader(BinaryReader aReader) { @@ -530,23 +551,21 @@ public BitStreamReader(BinaryReader aReader) } public BitStreamReader(Stream aStream) : this(new BinaryReader(aStream)) { } - public byte ReadBit() + private byte ReadBit() { - if (m_Bits <= 0) - { - m_Data = m_Reader.ReadByte(); - m_Bits = 8; - } + if (m_Bits > 0) return (byte)((m_Data >> --m_Bits) & 1); + m_Data = m_Reader.ReadByte(); + m_Bits = 8; return (byte)((m_Data >> --m_Bits) & 1); } public ulong ReadBits(int aCount) { ulong val = 0UL; - if (aCount <= 0 || aCount > 32) - throw new System.ArgumentOutOfRangeException("aCount", "aCount must be between 1 and 32 inclusive"); + if (aCount is <= 0 or > 32) + throw new System.ArgumentOutOfRangeException(nameof(aCount), "aCount must be between 1 and 32 inclusive"); for (int i = aCount-1; i>=0; i--) - val |= ((ulong)ReadBit() << i); + val |= (ulong)ReadBit() << i; return val; } public void Flush() diff --git a/RebuildClient/ProjectSettings/EditorSettings.asset b/RebuildClient/ProjectSettings/EditorSettings.asset index 4c8b6f7f..3e06f97b 100644 --- a/RebuildClient/ProjectSettings/EditorSettings.asset +++ b/RebuildClient/ProjectSettings/EditorSettings.asset @@ -17,7 +17,7 @@ EditorSettings: m_EtcTextureFastCompressor: 1 m_EtcTextureNormalCompressor: 2 m_EtcTextureBestCompressor: 4 - m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref;spr m_ProjectGenerationRootNamespace: m_EnableTextureStreamingInEditMode: 1 m_EnableTextureStreamingInPlayMode: 1