Files
test/Assets/PerfectWorld/Scripts/Config/Editor/SkillStateActionConfigImporter.cs
T
vuong dinh hoang f8512de72e deal damage overtime
2026-05-12 16:56:22 +07:00

238 lines
9.6 KiB
C#

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<string>();
List<SkillStateActionRow> 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<SkillStateActionConfig>();
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<string>();
List<SkillStateActionRow> 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)}");
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// Writes list via SerializedProperty so Unity persists nested structs reliably (Inspector shows Size &gt; 0).
/// </summary>
private static void WriteSerializedEntries(SkillStateActionConfig cfg, List<SkillStateActionRow> 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();
}
/// <summary>
/// Ensure the user picked gameplay config text (skill_state_action.txt), not a Unity YAML .asset/.meta.
/// </summary>
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<string> warnings)
{
if (warnings == null || warnings.Count == 0)
return;
foreach (string w in warnings)
Debug.LogWarning($"SkillStateActionConfigImporter: {w}");
}
}
}