using System; using System.Collections.Generic; using System.IO; using System.Text; using BrewMonster.Config; using UnityEditor; using UnityEngine; namespace BrewMonster.Config.Editor { public static class SkillStateActionConfigImporter { private const string MenuRoot = "PerfectWorld/Skill State Action/"; [MenuItem(MenuRoot + "Import TXT Into New Asset…")] public static void ImportTxtIntoNewAsset() { string txtPath = EditorUtility.OpenFilePanel("Import skill_state_action.txt", "", "txt"); if (string.IsNullOrEmpty(txtPath)) return; string text; try { text = ReadAllTextLegacyPwConfig(txtPath); } catch (IOException ex) { EditorUtility.DisplayDialog("Import failed", $"Could not read file:\n{ex.Message}", "OK"); return; } if (!ValidateImportedTxtSource(txtPath, text)) return; var warnings = new List(); List rows = SkillStateActionTextParser.Parse(text, warnings); LogWarnings(warnings); NotifyIfZeroRows(rows.Count); string assetPath = EditorUtility.SaveFilePanelInProject( "Save Skill State Action Config", "skill_state_action", "asset", "Choose asset path under Assets/"); if (string.IsNullOrEmpty(assetPath)) return; var asset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(asset, assetPath); WriteSerializedEntries(asset, rows); EditorUtility.SetDirty(asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); EditorUtility.FocusProjectWindow(); Selection.activeObject = asset; Debug.Log($"SkillStateActionConfigImporter: wrote {rows.Count} rows to {assetPath}"); } [MenuItem(MenuRoot + "Import TXT Into Selected Asset…", true)] public static bool ImportTxtIntoSelectedValidate() { return Selection.activeObject is SkillStateActionConfig; } [MenuItem(MenuRoot + "Import TXT Into Selected Asset…")] public static void ImportTxtIntoSelected() { if (!(Selection.activeObject is SkillStateActionConfig cfg)) return; string txtPath = EditorUtility.OpenFilePanel("Import skill_state_action.txt", "", "txt"); if (string.IsNullOrEmpty(txtPath)) return; string text; try { text = ReadAllTextLegacyPwConfig(txtPath); } catch (IOException ex) { EditorUtility.DisplayDialog("Import failed", $"Could not read file:\n{ex.Message}", "OK"); return; } if (!ValidateImportedTxtSource(txtPath, text)) return; var warnings = new List(); List rows = SkillStateActionTextParser.Parse(text, warnings); LogWarnings(warnings); NotifyIfZeroRows(rows.Count); Undo.RecordObject(cfg, "Import Skill State Action"); WriteSerializedEntries(cfg, rows); EditorUtility.SetDirty(cfg); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"SkillStateActionConfigImporter: replaced {rows.Count} rows on {AssetDatabase.GetAssetPath(cfg)}"); } /// /// PW client configs are often GBK (Windows code page 936). Reading those bytes as UTF-8 produces mojibake (� / gibberish). /// After optional UTF-8 BOM: decode as strict UTF-8; if invalid, use GBK — consistent with OctetsStream / globaldataman. /// private static string ReadAllTextLegacyPwConfig(string path) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); byte[] bytes = File.ReadAllBytes(path); int start = 0; if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) start = 3; int len = bytes.Length - start; if (len <= 0) return string.Empty; try { var utf8Strict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); return utf8Strict.GetString(bytes, start, len); } catch (DecoderFallbackException) { return Encoding.GetEncoding(936).GetString(bytes, start, len); } } /// /// Writes list via SerializedProperty so Unity persists nested structs reliably (Inspector shows Size > 0). /// private static void WriteSerializedEntries(SkillStateActionConfig cfg, List rows) { SerializedObject so = new SerializedObject(cfg); SerializedProperty entriesProp = so.FindProperty("entries"); if (entriesProp == null) { Debug.LogError("SkillStateActionConfigImporter: serialized field 'entries' not found — check SkillStateActionConfig."); return; } entriesProp.ClearArray(); int n = rows?.Count ?? 0; entriesProp.arraySize = n; for (int i = 0; i < n; i++) { SerializedProperty elem = entriesProp.GetArrayElementAtIndex(i); SkillStateActionRow r = rows[i]; elem.FindPropertyRelative("skill").intValue = r.skill; elem.FindPropertyRelative("state").intValue = r.state; elem.FindPropertyRelative("beHitAction").stringValue = r.beHitAction ?? string.Empty; elem.FindPropertyRelative("stayDownAction").stringValue = r.stayDownAction ?? string.Empty; } so.ApplyModifiedProperties(); } /// /// Ensure the user picked gameplay config text (skill_state_action.txt), not a Unity YAML .asset/.meta. /// private static bool ValidateImportedTxtSource(string path, string text) { string ext = Path.GetExtension(path); if (ext.Equals(".asset", StringComparison.OrdinalIgnoreCase) || ext.Equals(".meta", StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog( "Wrong file — pick skill_state_action.txt", "You selected a Unity project file (" + ext + "), not the classic PW config.\n\n" + "Use the plain text table exported from the game/client:\n" + " skill_state_action.txt\n\n" + "Each line looks like:\n" + " skillId,stateId,beHitAction[,stayDownAction]\n\n" + "Do not choose skill_state_action.asset (that is the ScriptableObject we fill).", "OK"); return false; } if (string.IsNullOrEmpty(text)) { EditorUtility.DisplayDialog("Import failed", "The chosen file is empty.", "OK"); return false; } string head = text.Length <= 2048 ? text : text.Substring(0, 2048); string trimmedHead = head.TrimStart('\uFEFF', ' ', '\t', '\r', '\n'); if (trimmedHead.StartsWith("%YAML", StringComparison.Ordinal)) { EditorUtility.DisplayDialog( "Wrong file — this is Unity YAML", "The file begins with %YAML (Unity serialized asset).\n\n" + "Select skill_state_action.txt instead — the comma-separated gameplay table — not .asset or scene YAML.", "OK"); return false; } if (head.IndexOf("m_Script:", StringComparison.Ordinal) >= 0 && head.IndexOf("fileID:", StringComparison.Ordinal) >= 0) { EditorUtility.DisplayDialog( "Wrong file — Unity ScriptableObject YAML", "This content looks like a Unity .asset (m_Script / fileID).\n\n" + "You must pick skill_state_action.txt (plain rows: skillId,stateId,...).\n\n" + "Tip: copy skill_state_action.txt from client configs or Assets/Resources if you still keep it there.", "OK"); return false; } return true; } private static void NotifyIfZeroRows(int count) { if (count != 0) return; EditorUtility.DisplayDialog( "Skill State Action import", "Parsed 0 rows.\n\n" + "If you meant to import rows, check that you chose skill_state_action.txt, not .asset.\n\n" + "Open the Console for line warnings.\n\n" + "Expected per line:\n" + " skillId,stateId,beHitAction[,stayDownAction]\n" + "or the same columns separated by TAB.\n\n" + "First two columns must be integers.", "OK"); } private static void LogWarnings(List warnings) { if (warnings == null || warnings.Count == 0) return; foreach (string w in warnings) Debug.LogWarning($"SkillStateActionConfigImporter: {w}"); } } }