diff --git a/Assets/Scripts/Editor/GON/StageEditor/StageEditorWindow.cs b/Assets/Scripts/Editor/GON/StageEditor/StageEditorWindow.cs index 60ff36f..16ae1b5 100644 --- a/Assets/Scripts/Editor/GON/StageEditor/StageEditorWindow.cs +++ b/Assets/Scripts/Editor/GON/StageEditor/StageEditorWindow.cs @@ -31,6 +31,15 @@ namespace Editor.GON.StageEditor window.Show(); } + public static void Open(StageDefinition stageDefinition) + { + var window = GetWindow(); + window.titleContent = new GUIContent("Gardens Stage Editor"); + window.minSize = new Vector2(900, 600); + window.Show(); + window.SetStage(stageDefinition); + } + public void CreateGUI() { state ??= new StageEditorState(); diff --git a/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs b/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs new file mode 100644 index 0000000..88febce --- /dev/null +++ b/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Editor.GON.StageEditor; +using Intrepid.Gameplay.Journey; +using UnityEditor; +using UnityEngine; + +namespace Editor.GON.WorldEditor +{ + public static class StageDefinitionSyncer + { + public const int ContractVersion = 1; + + public static StageDefinition CreateOrSyncStageDefinition(WorldLayoutSO layout, WorldStageAuthoring stage) + { + if (layout is null || stage is null) + { + return null; + } + + layout.EnsureDefaults(); + stage.EnsureDefaults(); + + var definition = stage.stageDefinition; + if (definition is null) + { + definition = CreateStageDefinitionAsset(layout, stage); + if (definition is null) + { + return null; + } + + stage.stageDefinition = definition; + EditorUtility.SetDirty(layout); + } + + SyncStageDefinition(layout, stage, definition); + return definition; + } + + public static void SyncStageDefinition(WorldLayoutSO layout, WorldStageAuthoring stage, StageDefinition definition) + { + if (layout is null || stage is null || definition is null) + { + return; + } + + Undo.RecordObject(definition, "Sync stage definition world contract"); + definition.EnsureDefaults(); + definition.worldSync = BuildSyncData(layout, stage); + EditorUtility.SetDirty(definition); + AssetDatabase.SaveAssets(); + } + + public static StageWorldSyncData BuildSyncData(WorldLayoutSO layout, WorldStageAuthoring stage) + { + layout.EnsureDefaults(); + stage.EnsureDefaults(); + + var data = new StageWorldSyncData + { + worldLayoutId = layout.id, + worldStageId = stage.id, + worldStageDisplayName = stage.displayName, + worldStageKind = stage.stageKind, + worldLayer = stage.worldLayer, + worldTileSize = layout.worldTileSize, + worldLayerHeight = layout.worldLayerHeight, + syncedAt = DateTime.UtcNow.ToString("O"), + contractVersion = ContractVersion + }; + + foreach (var tile in stage.occupiedTiles) + { + data.worldTiles.Add(tile); + } + + foreach (var endpoint in GetStageEndpoints(layout, stage.id)) + { + data.expectedEndpoints.Add(BuildEndpoint(layout, endpoint)); + } + + data.contractHash = ComputeContractHash(data); + return data; + } + + public static string ComputeExpectedContractHash(WorldLayoutSO layout, WorldStageAuthoring stage) + { + var data = BuildSyncData(layout, stage); + return data.contractHash; + } + + public static string ComputeContractHash(StageWorldSyncData data) + { + if (data is null) + { + return string.Empty; + } + + data.EnsureDefaults(); + var builder = new StringBuilder(); + builder.Append("v=").Append(ContractVersion).Append('\n'); + builder.Append("layout=").Append(data.worldLayoutId).Append('\n'); + builder.Append("stage=").Append(data.worldStageId).Append('\n'); + builder.Append("name=").Append(data.worldStageDisplayName).Append('\n'); + builder.Append("kind=").Append((int)data.worldStageKind).Append('\n'); + builder.Append("layer=").Append(data.worldLayer).Append('\n'); + builder.Append("tileSize=").Append(data.worldTileSize.ToString("R", CultureInfo.InvariantCulture)).Append('\n'); + builder.Append("layerHeight=").Append(data.worldLayerHeight.ToString("R", CultureInfo.InvariantCulture)).Append('\n'); + + var tiles = new List(data.worldTiles); + tiles.Sort(CompareTiles); + foreach (var tile in tiles) + { + builder.Append("tile=").Append(tile.x).Append(',').Append(tile.y).Append('\n'); + } + + var endpoints = new List(data.expectedEndpoints); + endpoints.Sort(CompareEndpoints); + foreach (var endpoint in endpoints) + { + builder.Append("endpoint=") + .Append(endpoint.endpointId).Append('|') + .Append((int)endpoint.kind).Append('|') + .Append(endpoint.worldTile.x).Append(',').Append(endpoint.worldTile.y).Append('|') + .Append((int)endpoint.socketFace).Append('|') + .Append(endpoint.required ? "1" : "0").Append('|') + .Append(endpoint.connectionId).Append('|') + .Append(endpoint.connectedEndpointId).Append('|') + .Append(endpoint.debugLabel).Append('\n'); + } + + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())); + var hash = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + { + hash.Append(b.ToString("x2")); + } + + return hash.ToString(); + } + + private static StageDefinition CreateStageDefinitionAsset(WorldLayoutSO layout, WorldStageAuthoring stage) + { + var layoutPath = AssetDatabase.GetAssetPath(layout); + if (string.IsNullOrEmpty(layoutPath)) + { + Debug.LogError("Cannot create StageDefinition because the WorldLayoutSO is not saved as an asset."); + return null; + } + + var layoutDirectory = Path.GetDirectoryName(layoutPath)?.Replace('\\', '/'); + if (string.IsNullOrEmpty(layoutDirectory)) + { + return null; + } + + var stagesFolder = layoutDirectory + "/Stages"; + if (!AssetDatabase.IsValidFolder(stagesFolder)) + { + AssetDatabase.CreateFolder(layoutDirectory, "Stages"); + } + + var fileName = MakeSafeFileName(string.IsNullOrWhiteSpace(stage.displayName) ? stage.id : stage.displayName); + var path = AssetDatabase.GenerateUniqueAssetPath(stagesFolder + "/" + fileName + ".stage-definition.asset"); + + var definition = ScriptableObject.CreateInstance(); + definition.Id = Path.GetFileNameWithoutExtension(path).ToLowerInvariant(); + definition.EnsureDefaults(); + AssetDatabase.CreateAsset(definition, path); + return definition; + } + + private static StageWorldSyncEndpoint BuildEndpoint(WorldLayoutSO layout, WorldEndpointAuthoring endpoint) + { + var connection = FindFirstConnection(layout, endpoint.id); + var connectedEndpointId = connection is null + ? string.Empty + : connection.endpointAId == endpoint.id + ? connection.endpointBId + : connection.endpointAId; + + return new StageWorldSyncEndpoint + { + endpointId = endpoint.id, + kind = endpoint.kind, + worldTile = endpoint.worldTile, + socketFace = endpoint.socketFace, + required = endpoint.required, + connectionId = connection?.id ?? string.Empty, + connectedEndpointId = connectedEndpointId, + debugLabel = endpoint.kind + " " + endpoint.id + }; + } + + private static List GetStageEndpoints(WorldLayoutSO layout, string stageId) + { + var endpoints = new List(); + foreach (var endpoint in layout.endpoints) + { + if (endpoint != null && endpoint.stageId == stageId) + { + endpoints.Add(endpoint); + } + } + + endpoints.Sort((a, b) => string.CompareOrdinal(a.id, b.id)); + return endpoints; + } + + private static WorldConnectionAuthoring FindFirstConnection(WorldLayoutSO layout, string endpointId) + { + foreach (var connection in layout.connections) + { + if (connection != null && + (connection.endpointAId == endpointId || connection.endpointBId == endpointId)) + { + return connection; + } + } + + return null; + } + + private static int CompareTiles(JWorldTile a, JWorldTile b) + { + var x = a.x.CompareTo(b.x); + return x != 0 ? x : a.y.CompareTo(b.y); + } + + private static int CompareEndpoints(StageWorldSyncEndpoint a, StageWorldSyncEndpoint b) + { + return string.CompareOrdinal(a?.endpointId, b?.endpointId); + } + + private static string MakeSafeFileName(string value) + { + var safe = new StringBuilder(); + foreach (var c in value) + { + safe.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' ? c : '-'); + } + + var result = safe.ToString().Trim('-'); + return string.IsNullOrEmpty(result) ? "StageDefinition" : result; + } + } +} diff --git a/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs.meta b/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs.meta new file mode 100644 index 0000000..2ee02b6 --- /dev/null +++ b/Assets/Scripts/Editor/GON/WorldEditor/StageDefinitionSyncer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26d7437e85a64a85a49c28877b7e145b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor/GON/WorldEditor/WorldInspectorPanel.cs b/Assets/Scripts/Editor/GON/WorldEditor/WorldInspectorPanel.cs index 181f612..c8360b2 100644 --- a/Assets/Scripts/Editor/GON/WorldEditor/WorldInspectorPanel.cs +++ b/Assets/Scripts/Editor/GON/WorldEditor/WorldInspectorPanel.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Editor.GON.StageEditor; using Intrepid.Gameplay.Journey; +using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; @@ -128,6 +129,39 @@ namespace Editor.GON.WorldEditor stage.stageDefinition = evt.newValue as StageDefinition; })); Add(definition); + + var syncButton = new Button(delegate + { + owner.MutateLayout( + stage.stageDefinition is null ? "Create stage definition" : "Sync stage definition", + delegate + { + StageDefinitionSyncer.CreateOrSyncStageDefinition(worldLayout, stage); + }); + }) + { + text = stage.stageDefinition is null ? "Create StageDefinition" : "Sync StageDefinition", + style = + { + marginTop = 8 + } + }; + Add(syncButton); + + var openButton = new Button(delegate + { + Selection.activeObject = stage.stageDefinition; + StageEditorWindow.Open(stage.stageDefinition); + }) + { + text = "Open Stage Editor", + style = + { + marginTop = 4 + } + }; + openButton.SetEnabled(stage.stageDefinition is not null); + Add(openButton); } private void BuildEndpointInspector(WorldEndpointAuthoring endpoint)