Files
AIC-Project/Client/Assets/Script/Editor/JsonUIPrefabBuilder.cs
T
2026-07-10 10:24:29 +08:00

774 lines
30 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace XGame.EditorTools
{
public class ValidationResult
{
public bool Ok = true;
public readonly List<string> Errors = new List<string>();
public readonly List<string> Warnings = new List<string>();
}
public static class JsonUIPrefabBuilder
{
// Doc/UIPrefabCreater 位于仓库根(Unity 工程目录 Client/ 的上一级),
// 不能用相对路径(Unity 工作目录是工程目录),需基于 dataPath 解析为绝对路径。
private const string JsonRootRelative = "Doc/UIPrefabCreater";
private const string TextureRoot = "Assets/Game/Art/UI/Texture";
private static readonly int[] RequiredRefResolution = { 2048, 1024 };
// 解析 JSON 根目录绝对路径:优先 仓库根/Doc/UIPrefabCreater,回退 工程根/Doc/UIPrefabCreater
private static string ResolveJsonRoot()
{
// Application.dataPath = <repo>/Client/Assets
string projectDir = Directory.GetParent(Application.dataPath).FullName; // <repo>/Client
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir; // <repo>
string atRepo = Path.Combine(repoDir, JsonRootRelative);
if (Directory.Exists(atRepo))
{
return atRepo;
}
string atProject = Path.Combine(projectDir, JsonRootRelative);
return atProject;
}
public static string ResolveDefaultJsonRoot()
{
return ResolveJsonRoot();
}
// 已识别但本期未实现的组件:遇到只告警、不报错(让 LoginPanelControls.json 仍能通过)
private static readonly HashSet<string> KnownUnimplemented = new HashSet<string>
{
"Toggle", "TMP_InputField", "RotationAnimation"
};
[MenuItem("Tools/UI/Generate Prefab From JSON...")]
public static void GenerateOne()
{
string path = EditorUtility.OpenFilePanel("选择 UI JSON", ResolveJsonRoot(), "json");
if (string.IsNullOrEmpty(path))
{
return;
}
string output = BuildFromJsonFile(path);
if (!string.IsNullOrEmpty(output))
{
Debug.Log("[UIGen] 生成成功: " + output);
}
}
[MenuItem("Tools/UI/Generate All Prefabs From JSON")]
public static void GenerateAll()
{
IReadOnlyList<string> outputs = BuildAll(ResolveJsonRoot());
Debug.Log("[UIGen] 批量生成完成,成功 " + outputs.Count + " 个。");
}
public static void BuildGameLobbyFromCommandLine()
{
string jsonPath = Path.Combine(ResolveJsonRoot(), "GameLobbyControls.json");
string output = BuildFromJsonFile(jsonPath);
if (string.IsNullOrEmpty(output))
{
throw new Exception("[UIGen] GameLobbyControls.json build failed.");
}
}
public static void BuildMatchWaitingFromCommandLine()
{
string jsonPath = Path.Combine(ResolveJsonRoot(), "MatchWaitingControls.json");
string output = BuildFromJsonFile(jsonPath);
if (string.IsNullOrEmpty(output))
{
throw new Exception("[UIGen] MatchWaitingControls.json build failed.");
}
}
public static void BuildAllFromCommandLine()
{
string jsonRoot = GetCommandLineValue("-uiJsonRoot");
if (string.IsNullOrEmpty(jsonRoot))
{
jsonRoot = ResolveJsonRoot();
}
else if (!Path.IsPathRooted(jsonRoot))
{
string projectDir = Directory.GetParent(Application.dataPath).FullName;
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir;
jsonRoot = Path.Combine(repoDir, jsonRoot);
}
IReadOnlyList<string> outputs = BuildAll(jsonRoot);
if (outputs.Count == 0)
{
throw new Exception("[UIGen] no prefab was built.");
}
}
public static ValidationResult Validate(string jsonPath)
{
ValidationResult vr = new ValidationResult();
if (!File.Exists(jsonPath))
{
Fail(vr, "文件不存在: " + jsonPath);
return vr;
}
JObject root;
try
{
root = JObject.Parse(File.ReadAllText(jsonPath));
}
catch (Exception ex)
{
Fail(vr, "JSON 解析失败: " + ex.Message);
return vr;
}
foreach (string field in new[] { "schemaVersion", "prefabName", "prefabPath", "canvas", "assets", "nodes" })
{
if (root[field] == null)
{
Fail(vr, "缺少顶层字段: " + field);
}
}
if (!vr.Ok)
{
return vr;
}
string prefabName = (string)root["prefabName"];
string prefabPath = (string)root["prefabPath"];
if (!prefabPath.Replace("\\", "/").StartsWith("Assets/"))
{
Fail(vr, "prefabPath 必须位于 Assets/ 下: " + prefabPath);
}
JToken refRes = root["canvas"]?["canvasScaler"]?["referenceResolution"];
if (refRes == null || (int)refRes[0] != RequiredRefResolution[0] || (int)refRes[1] != RequiredRefResolution[1])
{
Fail(vr, "referenceResolution 必须为 [2048, 1024]");
}
JArray nodes = root["nodes"] as JArray;
if (nodes == null || nodes.Count == 0)
{
Fail(vr, "nodes 为空");
return vr;
}
HashSet<string> names = new HashSet<string>();
List<string> roots = new List<string>();
foreach (JToken node in nodes)
{
string name = (string)node["name"];
if (string.IsNullOrEmpty(name))
{
Fail(vr, "存在无 name 的节点");
continue;
}
if (!names.Add(name))
{
Fail(vr, "节点名重复: " + name);
}
if (node["rect"] == null)
{
Fail(vr, "节点缺少 rect: " + name);
}
if (node["parent"] == null || node["parent"].Type == JTokenType.Null)
{
roots.Add(name);
}
}
foreach (JToken node in nodes)
{
JToken parent = node["parent"];
if (parent != null && parent.Type != JTokenType.Null && !names.Contains((string)parent))
{
Fail(vr, "父节点不存在: " + (string)parent + " (来自 " + (string)node["name"] + ")");
}
}
if (roots.Count != 1)
{
Fail(vr, "根节点数量必须为 1,实际 " + roots.Count);
}
else if (roots[0] != prefabName)
{
Fail(vr, "prefabName 必须与根节点名一致: " + prefabName + " vs " + roots[0]);
}
foreach (string b in EnumStrings(root["bindings"]))
{
if (!names.Contains(b))
{
Fail(vr, "bindings 节点不存在: " + b);
}
}
foreach (JToken ev in (root["events"] as JArray) ?? new JArray())
{
string n = (string)ev["node"];
if (!names.Contains(n))
{
Fail(vr, "events 节点不存在: " + n);
}
}
return vr;
}
public static string BuildFromJsonFile(string jsonPath)
{
ValidationResult vr = Validate(jsonPath);
foreach (string w in vr.Warnings)
{
Debug.LogWarning("[UIGen] " + Path.GetFileName(jsonPath) + ": " + w);
}
if (!vr.Ok)
{
foreach (string e in vr.Errors)
{
Debug.LogError("[UIGen] " + Path.GetFileName(jsonPath) + ": " + e);
}
return null;
}
JObject root = JObject.Parse(File.ReadAllText(jsonPath));
string prefabName = (string)root["prefabName"];
string prefabPath = (string)root["prefabPath"];
JArray nodes = (JArray)root["nodes"];
GameObject rootGo = null;
try
{
Dictionary<string, GameObject> map = new Dictionary<string, GameObject>();
List<Action> deferred = new List<Action>();
foreach (JToken node in nodes)
{
string name = (string)node["name"];
string parentName = node["parent"]?.Type == JTokenType.Null ? null : (string)node["parent"];
GameObject go = new GameObject(name, typeof(RectTransform));
map[name] = go;
if (parentName == null)
{
rootGo = go;
}
else if (map.TryGetValue(parentName, out GameObject parentGo))
{
go.transform.SetParent(parentGo.transform, false);
}
ApplyRect(go.GetComponent<RectTransform>(), node["rect"] as JObject);
bool active = node["active"] == null || (bool)node["active"];
foreach (JToken comp in (node["components"] as JArray) ?? new JArray())
{
ApplyComponent(go, comp as JObject, map, deferred, Path.GetFileName(jsonPath));
}
go.SetActive(active);
}
foreach (Action a in deferred)
{
a();
}
string dir = Path.GetDirectoryName(prefabPath);
if (!AssetDatabase.IsValidFolder(dir))
{
Directory.CreateDirectory(dir);
AssetDatabase.Refresh();
}
GameObject saved = PrefabUtility.SaveAsPrefabAsset(rootGo, prefabPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[UIGen] " + prefabName + " 生成完成 -> " + prefabPath);
return saved == null ? null : prefabPath;
}
finally
{
if (rootGo != null)
{
UnityEngine.Object.DestroyImmediate(rootGo);
}
}
}
public static IReadOnlyList<string> BuildAll(string jsonRoot)
{
List<string> outputs = new List<string>();
int failed = 0;
if (!Directory.Exists(jsonRoot))
{
Debug.LogError("[UIGen] JSON 目录不存在: " + jsonRoot);
return outputs;
}
foreach (string file in Directory.GetFiles(jsonRoot, "*.json", SearchOption.AllDirectories))
{
// 只处理含 prefabName/nodes 的 UI 描述文件,跳过其它 json
string text;
try { text = File.ReadAllText(file); } catch { continue; }
if (!text.Contains("\"prefabName\"") || !text.Contains("\"nodes\""))
{
Debug.Log("[UIGen] 跳过非 UI JSON: " + file);
continue;
}
string output = BuildFromJsonFile(file);
if (string.IsNullOrEmpty(output))
{
failed++;
}
else
{
outputs.Add(output);
}
}
Debug.Log("[UIGen] BuildAll 完成 Success:" + outputs.Count + " Failed:" + failed);
return outputs;
}
public static IReadOnlyList<string> BuildChanged(string jsonRoot, string reason, bool force = false)
{
List<string> outputs = new List<string>();
int skipped = 0;
int failed = 0;
if (!Directory.Exists(jsonRoot))
{
Debug.LogError("[UIGen] JSON 目录不存在: " + jsonRoot);
return outputs;
}
foreach (string file in Directory.GetFiles(jsonRoot, "*.json", SearchOption.AllDirectories))
{
if (!TryReadUiJson(file, out JObject root))
{
skipped++;
continue;
}
string prefabPath = (string)root["prefabPath"];
if (!force && !IsJsonNewerThanPrefab(file, prefabPath))
{
skipped++;
continue;
}
string output = BuildFromJsonFile(file);
if (string.IsNullOrEmpty(output))
{
failed++;
}
else
{
outputs.Add(output);
Debug.Log("[UIGen:" + reason + "] " + Path.GetFileName(file) + " -> " + output);
}
}
if (outputs.Count > 0 || failed > 0)
{
Debug.Log("[UIGen:" + reason + "] BuildChanged 完成 Success:" + outputs.Count + " Failed:" + failed + " Skipped:" + skipped);
}
return outputs;
}
private static bool TryReadUiJson(string file, out JObject root)
{
root = null;
try
{
string text = File.ReadAllText(file);
if (!text.Contains("\"prefabName\"") || !text.Contains("\"nodes\""))
{
return false;
}
root = JObject.Parse(text);
return root["prefabName"] != null && root["prefabPath"] != null && root["nodes"] != null;
}
catch
{
return false;
}
}
private static bool IsJsonNewerThanPrefab(string jsonPath, string prefabPath)
{
if (string.IsNullOrEmpty(prefabPath))
{
return false;
}
string normalizedPrefabPath = prefabPath.Replace("\\", "/");
string fullPrefabPath = Path.IsPathRooted(normalizedPrefabPath)
? normalizedPrefabPath
: Path.Combine(Directory.GetParent(Application.dataPath).FullName, normalizedPrefabPath);
if (!File.Exists(fullPrefabPath))
{
return true;
}
return File.GetLastWriteTimeUtc(jsonPath) > File.GetLastWriteTimeUtc(fullPrefabPath).AddMilliseconds(100);
}
// ---------- RectTransform ----------
private static void ApplyRect(RectTransform rt, JObject rect)
{
if (rect == null)
{
return;
}
rt.anchorMin = ToVec2(rect["anchorMin"], new Vector2(0, 0));
rt.anchorMax = ToVec2(rect["anchorMax"], new Vector2(1, 1));
rt.pivot = ToVec2(rect["pivot"], new Vector2(0.5f, 0.5f));
if (rect["offsetMin"] != null && rect["offsetMax"] != null)
{
rt.offsetMin = ToVec2(rect["offsetMin"], Vector2.zero);
rt.offsetMax = ToVec2(rect["offsetMax"], Vector2.zero);
}
else
{
rt.sizeDelta = ToVec2(rect["sizeDelta"], Vector2.zero);
rt.anchoredPosition = ToVec2(rect["anchoredPosition"], Vector2.zero);
}
}
// 用 Unity 重载的 == 判空,避免 C# ?? 在 Unity 对象上的“假 null”陷阱
private static T GetOrAdd<T>(GameObject go) where T : Component
{
T c = go.GetComponent<T>();
if (c == null)
{
c = go.AddComponent<T>();
}
return c;
}
// ---------- 组件 ----------
private static void ApplyComponent(GameObject go, JObject comp, Dictionary<string, GameObject> map, List<Action> deferred, string fileName)
{
if (comp == null)
{
return;
}
string type = (string)comp["type"];
switch (type)
{
case "Canvas":
{
Canvas c = GetOrAdd<Canvas>(go);
c.renderMode = RenderMode.ScreenSpaceOverlay;
if (comp["sortingOrder"] != null) c.sortingOrder = (int)comp["sortingOrder"];
break;
}
case "CanvasScaler":
{
CanvasScaler cs = GetOrAdd<CanvasScaler>(go);
cs.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
cs.referenceResolution = ToVec2(comp["referenceResolution"], new Vector2(2048, 1024));
cs.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
cs.matchWidthOrHeight = comp["matchWidthOrHeight"] != null ? (float)comp["matchWidthOrHeight"] : 0.5f;
break;
}
case "GraphicRaycaster":
GetOrAdd<GraphicRaycaster>(go);
break;
case "CanvasGroup":
{
CanvasGroup cg = GetOrAdd<CanvasGroup>(go);
if (comp["alpha"] != null) cg.alpha = (float)comp["alpha"];
if (comp["interactable"] != null) cg.interactable = (bool)comp["interactable"];
if (comp["blocksRaycasts"] != null) cg.blocksRaycasts = (bool)comp["blocksRaycasts"];
break;
}
case "Image":
{
Image img = GetOrAdd<Image>(go);
string spriteName = (string)comp["sprite"];
Sprite sprite = ResolveSprite(spriteName, fileName);
img.sprite = sprite;
img.type = string.Equals((string)comp["imageType"], "Sliced", StringComparison.OrdinalIgnoreCase) ? Image.Type.Sliced : Image.Type.Simple;
if (comp["preserveAspect"] != null) img.preserveAspect = (bool)comp["preserveAspect"];
img.raycastTarget = comp["raycastTarget"] == null || (bool)comp["raycastTarget"];
img.color = ParseColor((string)comp["color"], Color.white);
break;
}
case "TextMeshProUGUI":
{
TextMeshProUGUI t = GetOrAdd<TextMeshProUGUI>(go);
t.text = (string)comp["text"] ?? string.Empty;
if (comp["fontSize"] != null) t.fontSize = (float)comp["fontSize"];
t.fontStyle = string.Equals((string)comp["fontStyle"], "Bold", StringComparison.OrdinalIgnoreCase) ? FontStyles.Bold : FontStyles.Normal;
t.alignment = ParseAlignment((string)comp["alignment"]);
t.color = ParseColor((string)comp["color"], Color.white);
t.raycastTarget = comp["raycastTarget"] != null && (bool)comp["raycastTarget"];
break;
}
case "Button":
{
Button btn = GetOrAdd<Button>(go);
Image target = go.GetComponent<Image>();
if (target != null) btn.targetGraphic = target;
if (string.Equals((string)comp["transition"], "SpriteSwap", StringComparison.OrdinalIgnoreCase))
{
btn.transition = Selectable.Transition.SpriteSwap;
SpriteState state = btn.spriteState;
state.highlightedSprite = ResolveSprite((string)comp["spriteHighlighted"], fileName);
state.pressedSprite = ResolveSprite((string)comp["spritePressed"], fileName);
state.disabledSprite = ResolveSprite((string)comp["spriteDisabled"], fileName);
btn.spriteState = state;
}
else
{
btn.transition = Selectable.Transition.ColorTint;
}
// onClick 仅作事件标记,不在 Prefab 写死回调
break;
}
case "Shadow":
{
Shadow shadow = GetOrAdd<Shadow>(go);
shadow.effectColor = ParseColor((string)comp["effectColor"], new Color(0.06f, 0.12f, 0.2f, 0.75f));
shadow.effectDistance = ToVec2(comp["effectDistance"], new Vector2(1f, -1f));
if (comp["useGraphicAlpha"] != null) shadow.useGraphicAlpha = (bool)comp["useGraphicAlpha"];
break;
}
case "Outline":
{
Outline outline = GetOrAdd<Outline>(go);
outline.effectColor = ParseColor((string)comp["effectColor"], new Color(0.06f, 0.12f, 0.2f, 0.75f));
outline.effectDistance = ToVec2(comp["effectDistance"], new Vector2(1f, -1f));
if (comp["useGraphicAlpha"] != null) outline.useGraphicAlpha = (bool)comp["useGraphicAlpha"];
break;
}
case "ScrollRect":
{
ScrollRect sr = GetOrAdd<ScrollRect>(go);
sr.horizontal = comp["horizontal"] != null && (bool)comp["horizontal"];
sr.vertical = comp["vertical"] == null || (bool)comp["vertical"];
string viewportName = (string)comp["viewport"];
string contentName = (string)comp["content"];
deferred.Add(() =>
{
if (!string.IsNullOrEmpty(viewportName) && map.TryGetValue(viewportName, out GameObject vp))
sr.viewport = vp.GetComponent<RectTransform>();
if (!string.IsNullOrEmpty(contentName) && map.TryGetValue(contentName, out GameObject ct))
sr.content = ct.GetComponent<RectTransform>();
});
break;
}
case "RectMask2D":
GetOrAdd<RectMask2D>(go);
break;
case "Mask":
{
GetOrAdd<Image>(go);
Mask m = GetOrAdd<Mask>(go);
if (comp["showMaskGraphic"] != null) m.showMaskGraphic = (bool)comp["showMaskGraphic"];
break;
}
case "VerticalLayoutGroup":
{
VerticalLayoutGroup v = GetOrAdd<VerticalLayoutGroup>(go);
if (comp["spacing"] != null) v.spacing = (float)comp["spacing"];
v.padding = ParsePadding(comp["padding"]);
v.childAlignment = ParseAnchor((string)comp["childAlignment"]);
if (comp["childControlWidth"] != null) v.childControlWidth = (bool)comp["childControlWidth"];
if (comp["childControlHeight"] != null) v.childControlHeight = (bool)comp["childControlHeight"];
if (comp["childForceExpandWidth"] != null) v.childForceExpandWidth = (bool)comp["childForceExpandWidth"];
if (comp["childForceExpandHeight"] != null) v.childForceExpandHeight = (bool)comp["childForceExpandHeight"];
break;
}
case "ContentSizeFitter":
{
ContentSizeFitter f = GetOrAdd<ContentSizeFitter>(go);
f.verticalFit = ParseFit((string)comp["verticalFit"]);
f.horizontalFit = ParseFit((string)comp["horizontalFit"]);
break;
}
case "LayoutElement":
{
LayoutElement le = GetOrAdd<LayoutElement>(go);
if (comp["minHeight"] != null) le.minHeight = (float)comp["minHeight"];
if (comp["minWidth"] != null) le.minWidth = (float)comp["minWidth"];
if (comp["preferredHeight"] != null) le.preferredHeight = (float)comp["preferredHeight"];
if (comp["preferredWidth"] != null) le.preferredWidth = (float)comp["preferredWidth"];
break;
}
case "MonoBehaviour":
{
string typeName = (string)comp["typeName"];
deferred.Add(() =>
{
Type componentType = FindComponentType(typeName);
if (componentType == null)
{
Debug.LogWarning("[UIGen] " + fileName + ": 找不到脚本组件: " + typeName + " (" + go.name + ")");
}
else if (go.GetComponent(componentType) == null)
{
go.AddComponent(componentType);
}
});
break;
}
default:
if (KnownUnimplemented.Contains(type))
Debug.LogWarning("[UIGen] " + fileName + ": 组件未实现,已跳过: " + type + " (" + go.name + ")");
else
Debug.LogWarning("[UIGen] " + fileName + ": 未知组件类型: " + type + " (" + go.name + ")");
break;
}
}
// ---------- 资源/解析辅助 ----------
private static Type FindComponentType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
return null;
}
Type type = Type.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
}
return null;
}
private static Sprite ResolveSprite(string spriteName, string fileName)
{
if (string.IsNullOrEmpty(spriteName))
{
return null;
}
string[] guids = AssetDatabase.FindAssets(spriteName, new[] { TextureRoot });
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.Equals(Path.GetFileNameWithoutExtension(path), spriteName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sprite != null)
{
return sprite;
}
Debug.LogWarning("[UIGen] " + fileName + ": 贴图存在但非 Sprite 类型,回退纯色: " + spriteName + " (" + path + ")");
return null;
}
Debug.LogWarning("[UIGen] " + fileName + ": 找不到 Sprite,回退纯色: " + spriteName);
return null;
}
private static Color ParseColor(string hex, Color fallback)
{
if (!string.IsNullOrEmpty(hex) && ColorUtility.TryParseHtmlString(hex, out Color c))
{
return c;
}
return fallback;
}
private static Vector2 ToVec2(JToken arr, Vector2 fallback)
{
if (arr is JArray a && a.Count >= 2)
{
return new Vector2((float)a[0], (float)a[1]);
}
return fallback;
}
private static RectOffset ParsePadding(JToken token)
{
if (token is JArray a && a.Count >= 4)
{
return new RectOffset((int)a[0], (int)a[1], (int)a[2], (int)a[3]);
}
return new RectOffset(0, 0, 0, 0);
}
private static TextAlignmentOptions ParseAlignment(string a)
{
switch (a)
{
case "Left": return TextAlignmentOptions.Left;
case "Right": return TextAlignmentOptions.Right;
default: return TextAlignmentOptions.Center;
}
}
private static TextAnchor ParseAnchor(string a)
{
switch (a)
{
case "UpperLeft": return TextAnchor.UpperLeft;
case "UpperCenter": return TextAnchor.UpperCenter;
case "MiddleCenter": return TextAnchor.MiddleCenter;
default: return TextAnchor.UpperCenter;
}
}
private static ContentSizeFitter.FitMode ParseFit(string f)
{
switch (f)
{
case "PreferredSize": return ContentSizeFitter.FitMode.PreferredSize;
case "MinSize": return ContentSizeFitter.FitMode.MinSize;
default: return ContentSizeFitter.FitMode.Unconstrained;
}
}
private static IEnumerable<string> EnumStrings(JToken token)
{
if (token is JArray a)
{
foreach (JToken t in a)
{
yield return (string)t;
}
}
}
private static void Fail(ValidationResult vr, string msg)
{
vr.Ok = false;
vr.Errors.Add(msg);
}
private static string GetCommandLineValue(string key)
{
string[] args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], key, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}
}
}
#endif