implements PLN-0004 journey commit validation
This commit is contained in:
parent
da4f1caaa7
commit
27b751fd40
150
Assets/Scripts/Editor/GON/WorldEditor/JourneyCommitter.cs
Normal file
150
Assets/Scripts/Editor/GON/WorldEditor/JourneyCommitter.cs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Intrepid.Gameplay.Journey;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public static class JourneyCommitter
|
||||||
|
{
|
||||||
|
public static List<WorldValidationMessage> Commit(WorldLayoutSO layout, JGraphSO graph)
|
||||||
|
{
|
||||||
|
var messages = WorldLayoutValidator.Validate(layout);
|
||||||
|
if (WorldLayoutValidator.HasErrors(messages) || graph is null)
|
||||||
|
{
|
||||||
|
if (graph is null)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error, "No target JGraphSO selected."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.EnsureDefaults();
|
||||||
|
graph.sourceWorldLayoutId = layout.id;
|
||||||
|
graph.publishedAt = DateTime.UtcNow.ToString("O");
|
||||||
|
graph.schemaVersion = 1;
|
||||||
|
graph.stages = BuildStages(layout);
|
||||||
|
graph.endpoints = BuildEndpoints(layout);
|
||||||
|
graph.connections = BuildConnections(layout);
|
||||||
|
graph.contentHash = ComputeContentHash(graph);
|
||||||
|
graph.EnsureDefaults();
|
||||||
|
|
||||||
|
EditorUtility.SetDirty(graph);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<JStage> BuildStages(WorldLayoutSO layout)
|
||||||
|
{
|
||||||
|
var stages = new List<JStage>();
|
||||||
|
foreach (var source in layout.stages)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
source.EnsureDefaults();
|
||||||
|
var stage = new JStage
|
||||||
|
{
|
||||||
|
id = source.id,
|
||||||
|
displayName = source.displayName,
|
||||||
|
worldLayer = source.worldLayer,
|
||||||
|
occupiedTiles = new List<JWorldTile>(source.occupiedTiles)
|
||||||
|
};
|
||||||
|
stages.Add(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<JEndpoint> BuildEndpoints(WorldLayoutSO layout)
|
||||||
|
{
|
||||||
|
var endpoints = new List<JEndpoint>();
|
||||||
|
foreach (var source in layout.endpoints)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints.Add(new JEndpoint
|
||||||
|
{
|
||||||
|
id = source.id,
|
||||||
|
stageId = source.stageId,
|
||||||
|
worldTile = source.worldTile,
|
||||||
|
kind = source.kind,
|
||||||
|
required = source.required,
|
||||||
|
socketFace = source.socketFace
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<JConnection> BuildConnections(WorldLayoutSO layout)
|
||||||
|
{
|
||||||
|
var connections = new List<JConnection>();
|
||||||
|
foreach (var source in layout.connections)
|
||||||
|
{
|
||||||
|
if (source is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
connections.Add(new JConnection
|
||||||
|
{
|
||||||
|
id = source.id,
|
||||||
|
endpointAId = source.endpointAId,
|
||||||
|
endpointBId = source.endpointBId,
|
||||||
|
directionality = source.directionality
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeContentHash(JGraphSO graph)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append(graph.sourceWorldLayoutId).Append('|').Append(graph.schemaVersion).AppendLine();
|
||||||
|
|
||||||
|
foreach (var stage in graph.stages)
|
||||||
|
{
|
||||||
|
builder.Append("S|").Append(stage.id).Append('|').Append(stage.displayName).Append('|')
|
||||||
|
.Append(stage.worldLayer).AppendLine();
|
||||||
|
foreach (var tile in stage.occupiedTiles)
|
||||||
|
{
|
||||||
|
builder.Append("T|").Append(tile.x).Append('|').Append(tile.y).AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var endpoint in graph.endpoints)
|
||||||
|
{
|
||||||
|
builder.Append("E|").Append(endpoint.id).Append('|').Append(endpoint.stageId).Append('|')
|
||||||
|
.Append(endpoint.worldTile.x).Append('|').Append(endpoint.worldTile.y).Append('|')
|
||||||
|
.Append(endpoint.kind).Append('|').Append(endpoint.required).Append('|')
|
||||||
|
.Append(endpoint.socketFace).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var connection in graph.connections)
|
||||||
|
{
|
||||||
|
builder.Append("C|").Append(connection.id).Append('|').Append(connection.endpointAId).Append('|')
|
||||||
|
.Append(connection.endpointBId).Append('|').Append(connection.directionality).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||||
|
var result = new StringBuilder(bytes.Length * 2);
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
result.Append(value.ToString("x2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5b08cddf1c414e7b8d1f6ee4d7e67fb7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -11,10 +11,12 @@ namespace Editor.GON.WorldEditor
|
|||||||
public sealed class WorldEditorWindow : EditorWindow
|
public sealed class WorldEditorWindow : EditorWindow
|
||||||
{
|
{
|
||||||
private WorldLayoutSO layout;
|
private WorldLayoutSO layout;
|
||||||
|
private JGraphSO graph;
|
||||||
private WorldEditorState state;
|
private WorldEditorState state;
|
||||||
private VisualElement topBar;
|
private VisualElement topBar;
|
||||||
private WorldMapCanvas canvas;
|
private WorldMapCanvas canvas;
|
||||||
private Label infoLabel;
|
private Label infoLabel;
|
||||||
|
private ScrollView validationView;
|
||||||
|
|
||||||
[MenuItem("Tools/Gardens Of Nil/World Editor")]
|
[MenuItem("Tools/Gardens Of Nil/World Editor")]
|
||||||
public static void Open()
|
public static void Open()
|
||||||
@ -63,6 +65,17 @@ namespace Editor.GON.WorldEditor
|
|||||||
};
|
};
|
||||||
rootVisualElement.Add(infoLabel);
|
rootVisualElement.Add(infoLabel);
|
||||||
|
|
||||||
|
validationView = new ScrollView
|
||||||
|
{
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
maxHeight = 130,
|
||||||
|
borderTopWidth = 1,
|
||||||
|
borderTopColor = new Color(1f, 1f, 1f, 0.12f)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rootVisualElement.Add(validationView);
|
||||||
|
|
||||||
RebuildTopBar();
|
RebuildTopBar();
|
||||||
RebuildInfo();
|
RebuildInfo();
|
||||||
}
|
}
|
||||||
@ -159,18 +172,41 @@ namespace Editor.GON.WorldEditor
|
|||||||
style = { marginLeft = 8 }
|
style = { marginLeft = 8 }
|
||||||
});
|
});
|
||||||
|
|
||||||
topBar.Add(new Button(ValidatePlaceholder)
|
var graphField = new ObjectField("J Graph")
|
||||||
|
{
|
||||||
|
objectType = typeof(JGraphSO),
|
||||||
|
value = graph,
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
marginLeft = 8,
|
||||||
|
minWidth = 200
|
||||||
|
}
|
||||||
|
};
|
||||||
|
graphField.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
graph = evt.newValue as JGraphSO;
|
||||||
|
RebuildTopBar();
|
||||||
|
});
|
||||||
|
topBar.Add(graphField);
|
||||||
|
|
||||||
|
topBar.Add(new Button(CreateNewGraphAsset)
|
||||||
|
{
|
||||||
|
text = "+ JGraphSO",
|
||||||
|
style = { marginLeft = 4 }
|
||||||
|
});
|
||||||
|
|
||||||
|
topBar.Add(new Button(ValidateLayout)
|
||||||
{
|
{
|
||||||
text = "Validate",
|
text = "Validate",
|
||||||
style = { marginLeft = 8 }
|
style = { marginLeft = 8 }
|
||||||
});
|
});
|
||||||
|
|
||||||
var commit = new Button(CommitPlaceholder)
|
var commit = new Button(CommitJourney)
|
||||||
{
|
{
|
||||||
text = "Commit Journey",
|
text = "Commit Journey",
|
||||||
style = { marginLeft = 4 }
|
style = { marginLeft = 4 }
|
||||||
};
|
};
|
||||||
commit.SetEnabled(false);
|
commit.SetEnabled(graph is not null);
|
||||||
topBar.Add(commit);
|
topBar.Add(commit);
|
||||||
|
|
||||||
topBar.Add(new Button(SaveLayout)
|
topBar.Add(new Button(SaveLayout)
|
||||||
@ -233,6 +269,26 @@ namespace Editor.GON.WorldEditor
|
|||||||
SetLayout(asset);
|
SetLayout(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateNewGraphAsset()
|
||||||
|
{
|
||||||
|
var path = EditorUtility.SaveFilePanelInProject(
|
||||||
|
"Create JGraphSO",
|
||||||
|
"JGraph",
|
||||||
|
"asset",
|
||||||
|
"Choose where to create the published Journey graph asset.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var asset = CreateInstance<JGraphSO>();
|
||||||
|
AssetDatabase.CreateAsset(asset, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
graph = asset;
|
||||||
|
RebuildTopBar();
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateStage()
|
private void CreateStage()
|
||||||
{
|
{
|
||||||
MutateLayout("Create world stage", delegate
|
MutateLayout("Create world stage", delegate
|
||||||
@ -262,14 +318,57 @@ namespace Editor.GON.WorldEditor
|
|||||||
Debug.Log("Saved WorldLayoutSO: " + AssetDatabase.GetAssetPath(layout));
|
Debug.Log("Saved WorldLayoutSO: " + AssetDatabase.GetAssetPath(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidatePlaceholder()
|
private void ValidateLayout()
|
||||||
{
|
{
|
||||||
EditorUtility.DisplayDialog("World Editor", "Validation is implemented by PLN-0004.", "OK");
|
ShowValidationMessages(WorldLayoutValidator.Validate(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CommitPlaceholder()
|
private void CommitJourney()
|
||||||
{
|
{
|
||||||
EditorUtility.DisplayDialog("World Editor", "Journey commit is implemented by PLN-0004.", "OK");
|
var messages = JourneyCommitter.Commit(layout, graph);
|
||||||
|
ShowValidationMessages(messages);
|
||||||
|
if (!WorldLayoutValidator.HasErrors(messages))
|
||||||
|
{
|
||||||
|
Debug.Log("Committed Journey graph: " + AssetDatabase.GetAssetPath(graph));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowValidationMessages(System.Collections.Generic.List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
validationView?.Clear();
|
||||||
|
if (validationView is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages is null || messages.Count == 0)
|
||||||
|
{
|
||||||
|
validationView.Add(new Label("Validation passed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
if (message is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var label = new Label(message.severity + ": " + message.message +
|
||||||
|
(string.IsNullOrEmpty(message.objectId) ? string.Empty : " [" + message.objectId + "]"))
|
||||||
|
{
|
||||||
|
style =
|
||||||
|
{
|
||||||
|
color = message.severity == WorldValidationSeverity.Error
|
||||||
|
? new Color(1f, 0.45f, 0.4f)
|
||||||
|
: new Color(1f, 0.82f, 0.35f),
|
||||||
|
paddingLeft = 8,
|
||||||
|
paddingTop = 2,
|
||||||
|
paddingBottom = 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
validationView.Add(label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,20 @@ namespace Editor.GON.WorldEditor
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WorldEndpointAuthoring FindEndpoint(string endpointId)
|
||||||
|
{
|
||||||
|
EnsureDefaults();
|
||||||
|
foreach (var endpoint in endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint != null && endpoint.id == endpointId)
|
||||||
|
{
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsTileOccupiedByOther(string stageId, int worldLayer, JWorldTile tile)
|
public bool IsTileOccupiedByOther(string stageId, int worldLayer, JWorldTile tile)
|
||||||
{
|
{
|
||||||
var occupyingStage = FindStageAt(worldLayer, tile);
|
var occupyingStage = FindStageAt(worldLayer, tile);
|
||||||
|
|||||||
291
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutValidator.cs
Normal file
291
Assets/Scripts/Editor/GON/WorldEditor/WorldLayoutValidator.cs
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Intrepid.Gameplay.Journey;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public static class WorldLayoutValidator
|
||||||
|
{
|
||||||
|
public static List<WorldValidationMessage> Validate(WorldLayoutSO layout)
|
||||||
|
{
|
||||||
|
var messages = new List<WorldValidationMessage>();
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error, "No WorldLayoutSO selected."));
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.EnsureDefaults();
|
||||||
|
ValidateStages(layout, messages);
|
||||||
|
ValidateEndpoints(layout, messages);
|
||||||
|
ValidateConnections(layout, messages);
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasErrors(List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
if (messages is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var message in messages)
|
||||||
|
{
|
||||||
|
if (message != null && message.severity == WorldValidationSeverity.Error)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateStages(WorldLayoutSO layout, List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
var occupied = new Dictionary<string, string>();
|
||||||
|
foreach (var stage in layout.stages)
|
||||||
|
{
|
||||||
|
if (stage is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.EnsureDefaults();
|
||||||
|
var endpointCount = 0;
|
||||||
|
foreach (var endpoint in layout.endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint != null && endpoint.stageId == stage.id)
|
||||||
|
{
|
||||||
|
endpointCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointCount == 0)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Warning,
|
||||||
|
"Stage has no Journey endpoints.", stage.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tile in stage.occupiedTiles)
|
||||||
|
{
|
||||||
|
var key = TileKey(stage.worldLayer, tile);
|
||||||
|
if (occupied.TryGetValue(key, out var otherStageId) && otherStageId != stage.id)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"WorldTile is occupied by more than one Stage in the same WorldLayer.", stage.id));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
occupied[key] = stage.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateEndpoints(WorldLayoutSO layout, List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
var nonSocketByTile = new Dictionary<string, JEndpointKind>();
|
||||||
|
var socketByFace = new HashSet<string>();
|
||||||
|
|
||||||
|
foreach (var endpoint in layout.endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stage = layout.FindStage(endpoint.stageId);
|
||||||
|
if (stage is null)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpoint references a missing Stage.", endpoint.id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stage.ContainsTile(endpoint.worldTile))
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpoint is placed on a WorldTile not occupied by its Stage.", endpoint.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoint.kind == JEndpointKind.Socket)
|
||||||
|
{
|
||||||
|
var socketKey = endpoint.stageId + ":" + endpoint.worldTile.x + ":" + endpoint.worldTile.y + ":" +
|
||||||
|
endpoint.socketFace;
|
||||||
|
if (!socketByFace.Add(socketKey))
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"More than one JEndpointSocket exists on the same tile face.", endpoint.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var adjacent = Adjacent(endpoint.worldTile, endpoint.socketFace);
|
||||||
|
if (stage.ContainsTile(adjacent))
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointSocket is placed on an internal Stage face.", endpoint.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var key = TileKey(stage.worldLayer, endpoint.worldTile);
|
||||||
|
if (nonSocketByTile.TryGetValue(key, out var existingKind) && existingKind != endpoint.kind)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointAnchor and JEndpointLift cannot coexist on the same WorldTile.", endpoint.id));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nonSocketByTile[key] = endpoint.kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateConnections(WorldLayoutSO layout, List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
var endpointConnectionCounts = new Dictionary<string, int>();
|
||||||
|
foreach (var connection in layout.connections)
|
||||||
|
{
|
||||||
|
if (connection is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpointA = layout.FindEndpoint(connection.endpointAId);
|
||||||
|
var endpointB = layout.FindEndpoint(connection.endpointBId);
|
||||||
|
|
||||||
|
if (endpointA is null || endpointB is null)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JConnection references a missing endpoint.", connection.id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointA.id == endpointB.id)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JConnection links an endpoint to itself.", connection.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointA.kind != endpointB.kind)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JConnection links incompatible endpoint types.", connection.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Increment(endpointConnectionCounts, endpointA.id);
|
||||||
|
Increment(endpointConnectionCounts, endpointB.id);
|
||||||
|
|
||||||
|
ValidateConnectionGeometry(layout, connection, endpointA, endpointB, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var endpoint in layout.endpoints)
|
||||||
|
{
|
||||||
|
if (endpoint is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointConnectionCounts.TryGetValue(endpoint.id, out var count);
|
||||||
|
switch (endpoint.kind)
|
||||||
|
{
|
||||||
|
case JEndpointKind.Socket when count > 1:
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointSocket has more than one JConnection.", endpoint.id));
|
||||||
|
break;
|
||||||
|
case JEndpointKind.Lift when count > 1:
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointLift has more than one JConnection.", endpoint.id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(endpoint.required
|
||||||
|
? WorldValidationSeverity.Error
|
||||||
|
: WorldValidationSeverity.Warning,
|
||||||
|
endpoint.required
|
||||||
|
? "Required JEndpoint has no JConnection."
|
||||||
|
: "Optional JEndpoint has no JConnection.",
|
||||||
|
endpoint.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateConnectionGeometry(
|
||||||
|
WorldLayoutSO layout,
|
||||||
|
WorldConnectionAuthoring connection,
|
||||||
|
WorldEndpointAuthoring endpointA,
|
||||||
|
WorldEndpointAuthoring endpointB,
|
||||||
|
List<WorldValidationMessage> messages)
|
||||||
|
{
|
||||||
|
if (endpointA.kind == JEndpointKind.Socket && endpointB.kind == JEndpointKind.Socket)
|
||||||
|
{
|
||||||
|
if (!AreOpposite(endpointA.socketFace, endpointB.socketFace) ||
|
||||||
|
!IsAdjacentForFaces(endpointA.worldTile, endpointA.socketFace, endpointB.worldTile))
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointSocket connections must use opposite faces on adjacent WorldTiles.", connection.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpointA.kind == JEndpointKind.Lift && endpointB.kind == JEndpointKind.Lift)
|
||||||
|
{
|
||||||
|
var stageA = layout.FindStage(endpointA.stageId);
|
||||||
|
var stageB = layout.FindStage(endpointB.stageId);
|
||||||
|
if (endpointA.worldTile.x != endpointB.worldTile.x || endpointA.worldTile.y != endpointB.worldTile.y)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointLift connections must be aligned on the same 2D WorldTile coordinate.",
|
||||||
|
connection.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stageA != null && stageB != null && stageA.worldLayer == stageB.worldLayer)
|
||||||
|
{
|
||||||
|
messages.Add(new WorldValidationMessage(WorldValidationSeverity.Error,
|
||||||
|
"JEndpointLift connections must connect different WorldLayers.", connection.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Increment(Dictionary<string, int> counts, string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
counts.TryGetValue(key, out var count);
|
||||||
|
counts[key] = count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TileKey(int layer, JWorldTile tile)
|
||||||
|
{
|
||||||
|
return layer + ":" + tile.x + ":" + tile.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JWorldTile Adjacent(JWorldTile tile, JSocketFace face)
|
||||||
|
{
|
||||||
|
return face switch
|
||||||
|
{
|
||||||
|
JSocketFace.North => new JWorldTile(tile.x, tile.y - 1),
|
||||||
|
JSocketFace.South => new JWorldTile(tile.x, tile.y + 1),
|
||||||
|
JSocketFace.East => new JWorldTile(tile.x + 1, tile.y),
|
||||||
|
_ => new JWorldTile(tile.x - 1, tile.y)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AreOpposite(JSocketFace a, JSocketFace b)
|
||||||
|
{
|
||||||
|
return (a == JSocketFace.North && b == JSocketFace.South) ||
|
||||||
|
(a == JSocketFace.South && b == JSocketFace.North) ||
|
||||||
|
(a == JSocketFace.East && b == JSocketFace.West) ||
|
||||||
|
(a == JSocketFace.West && b == JSocketFace.East);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdjacentForFaces(JWorldTile a, JSocketFace faceA, JWorldTile b)
|
||||||
|
{
|
||||||
|
var expected = Adjacent(a, faceA);
|
||||||
|
return expected.x == b.x && expected.y == b.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 86f52d9a97c44cb1b8593c99d411f5b1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
25
Assets/Scripts/Editor/GON/WorldEditor/WorldValidation.cs
Normal file
25
Assets/Scripts/Editor/GON/WorldEditor/WorldValidation.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Editor.GON.WorldEditor
|
||||||
|
{
|
||||||
|
public enum WorldValidationSeverity
|
||||||
|
{
|
||||||
|
Warning = 0,
|
||||||
|
Error = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class WorldValidationMessage
|
||||||
|
{
|
||||||
|
public WorldValidationSeverity severity;
|
||||||
|
public string message;
|
||||||
|
public string objectId;
|
||||||
|
|
||||||
|
public WorldValidationMessage(WorldValidationSeverity severity, string message, string objectId = null)
|
||||||
|
{
|
||||||
|
this.severity = severity;
|
||||||
|
this.message = message;
|
||||||
|
this.objectId = objectId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e037255368a041d491478b61b7e0e75e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -1,2 +1,2 @@
|
|||||||
{"type":"meta","next_id":{"DSC":2,"AGD":2,"DEC":2,"PLN":5,"LSN":1,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":2,"AGD":2,"DEC":2,"PLN":5,"LSN":1,"CLSN":1}}
|
||||||
{"type":"discussion","id":"DSC-0001","status":"accepted","ticket":"world-editor-journey-spec","title":"World Editor / Journey Spec","created_at":"2026-06-19","updated_at":"2026-06-19","tags":["world-editor","journey","stage-contract","architecture"],"agendas":[{"id":"AGD-0001","file":"AGD-0001-world-editor-journey-spec.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19"}],"decisions":[{"id":"DEC-0001","file":"DEC-0001-world-editor-journey-v1.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0001","file":"PLN-0001-world-journey-spec.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0002","file":"PLN-0002-journey-runtime-model.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0003","file":"PLN-0003-world-editor-authoring.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0004","file":"PLN-0004-journey-commit-validation.md","status":"review","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]}],"lessons":[]}
|
{"type":"discussion","id":"DSC-0001","status":"accepted","ticket":"world-editor-journey-spec","title":"World Editor / Journey Spec","created_at":"2026-06-19","updated_at":"2026-06-19","tags":["world-editor","journey","stage-contract","architecture"],"agendas":[{"id":"AGD-0001","file":"AGD-0001-world-editor-journey-spec.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19"}],"decisions":[{"id":"DEC-0001","file":"DEC-0001-world-editor-journey-v1.md","status":"accepted","created_at":"2026-06-19","updated_at":"2026-06-19","ref_agenda":"AGD-0001"}],"plans":[{"id":"PLN-0001","file":"PLN-0001-world-journey-spec.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0002","file":"PLN-0002-journey-runtime-model.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0003","file":"PLN-0003-world-editor-authoring.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]},{"id":"PLN-0004","file":"PLN-0004-journey-commit-validation.md","status":"done","created_at":"2026-06-19","updated_at":"2026-06-19","ref_decisions":["DEC-0001"]}],"lessons":[]}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: PLN-0004
|
id: PLN-0004
|
||||||
ticket: world-editor-journey-spec
|
ticket: world-editor-journey-spec
|
||||||
title: Journey Commit and Validation
|
title: Journey Commit and Validation
|
||||||
status: review
|
status: done
|
||||||
created: 2026-06-19
|
created: 2026-06-19
|
||||||
decisions:
|
decisions:
|
||||||
- DEC-0001
|
- DEC-0001
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user