implements PLN-0006

This commit is contained in:
Nilton Constantino 2026-06-21 08:29:14 +01:00
parent da79d61e9f
commit 21cece1977
No known key found for this signature in database
4 changed files with 307 additions and 0 deletions

View File

@ -31,6 +31,15 @@ namespace Editor.GON.StageEditor
window.Show();
}
public static void Open(StageDefinition stageDefinition)
{
var window = GetWindow<StageEditorWindow>();
window.titleContent = new GUIContent("Gardens Stage Editor");
window.minSize = new Vector2(900, 600);
window.Show();
window.SetStage(stageDefinition);
}
public void CreateGUI()
{
state ??= new StageEditorState();

View File

@ -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<JWorldTile>(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<StageWorldSyncEndpoint>(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<StageDefinition>();
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<WorldEndpointAuthoring> GetStageEndpoints(WorldLayoutSO layout, string stageId)
{
var endpoints = new List<WorldEndpointAuthoring>();
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26d7437e85a64a85a49c28877b7e145b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)