diff --git a/Assets/Arcweave/Plugin/Editor/ArcweaveProjectAssetInspector.cs b/Assets/Arcweave/Plugin/Editor/ArcweaveProjectAssetInspector.cs index a43394a..5aef0a5 100644 --- a/Assets/Arcweave/Plugin/Editor/ArcweaveProjectAssetInspector.cs +++ b/Assets/Arcweave/Plugin/Editor/ArcweaveProjectAssetInspector.cs @@ -2,7 +2,6 @@ using UnityEngine; using UnityEditor; -using System.Collections.Generic; namespace Arcweave { @@ -72,16 +71,13 @@ public override void OnInspectorGUI() { GUILayout.Label("Global Variables:", EditorStyles.boldLabel); EditorGUI.indentLevel++; bool hasGlobalVariables = false; - foreach ( var variable in aw.Project.Variables) - { - if(variable.Parent == null) - { + foreach ( var variable in aw.Project.Variables ) { + if ( variable.Parent == null ) { hasGlobalVariables = true; EditorGUILayout.LabelField(variable.Name, variable.Value?.ToString()); } } - if (!hasGlobalVariables) - { + if ( !hasGlobalVariables ) { EditorGUILayout.LabelField("(None)"); } EditorGUI.indentLevel--; @@ -91,25 +87,44 @@ public override void OnInspectorGUI() { // Board Variables Section - Grouped by Board GUILayout.Label("Board Variables:", EditorStyles.boldLabel); bool hasBoardVariables = false; - foreach (var board in aw.Project.Boards) - { - if (board.Variables != null && board.Variables.Count > 0) - { - hasBoardVariables = true; - EditorGUI.indentLevel++; - GUILayout.Label(string.IsNullOrEmpty(board.Name) ? $"Board: {board.Id}" : board.Name, EditorStyles.miniLabel); - EditorGUI.indentLevel++; - foreach (var variable in board.Variables) - { - string variableName = variable.Name; - EditorGUILayout.LabelField(variableName, variable.Value?.ToString()); - } - EditorGUI.indentLevel -= 2; - GUILayout.Space(5); + foreach ( var board in aw.Project.Boards ) { + if ( board.Variables == null || board.Variables.Count == 0 ) { continue; } + + hasBoardVariables = true; + EditorGUI.indentLevel++; + GUILayout.Label(string.IsNullOrEmpty(board.Name) ? $"Board: {board.Id}" : board.Name, EditorStyles.miniLabel); + EditorGUI.indentLevel++; + foreach ( var variable in board.Variables ) { + EditorGUILayout.LabelField(variable.Name, variable.Value?.ToString()); } + EditorGUI.indentLevel -= 2; + GUILayout.Space(5); } - if (!hasBoardVariables) - { + if ( !hasBoardVariables ) { + EditorGUI.indentLevel++; + EditorGUILayout.LabelField("(None)"); + EditorGUI.indentLevel--; + } + + GUILayout.Space(10); + + // Component Variables Section - Grouped by Component + GUILayout.Label("Component Variables:", EditorStyles.boldLabel); + bool hasComponentVariables = false; + foreach ( var component in aw.Project.Components ) { + if ( component.Variables == null || component.Variables.Count == 0 ) { continue; } + + hasComponentVariables = true; + EditorGUI.indentLevel++; + GUILayout.Label(string.IsNullOrEmpty(component.Name) ? $"Component: {component.Id}" : component.Name, EditorStyles.miniLabel); + EditorGUI.indentLevel++; + foreach ( var variable in component.Variables ) { + EditorGUILayout.LabelField(variable.Name, variable.Value?.ToString()); + } + EditorGUI.indentLevel -= 2; + GUILayout.Space(5); + } + if ( !hasComponentVariables ) { EditorGUI.indentLevel++; EditorGUILayout.LabelField("(None)"); EditorGUI.indentLevel--; @@ -128,4 +143,4 @@ public override void OnInspectorGUI() { } } -#endif \ No newline at end of file +#endif diff --git a/Assets/Arcweave/Plugin/Editor/ProjectViewerWindow.cs b/Assets/Arcweave/Plugin/Editor/ProjectViewerWindow.cs index f739c05..69ca87b 100644 --- a/Assets/Arcweave/Plugin/Editor/ProjectViewerWindow.cs +++ b/Assets/Arcweave/Plugin/Editor/ProjectViewerWindow.cs @@ -39,7 +39,11 @@ public class ProjectViewerWindow : EditorWindow private readonly Color NODE_DEFAULT_COLOR = COLOR_THEMES["default"]; private ArcweaveProjectAsset _asset; +#if UNITY_6000_5_OR_NEWER + private EntityId _assetID; +#else private int _assetID; +#endif private Rect _canvasRect; private Vector2 _translation; private float _zoomFactor = 1f; @@ -55,14 +59,22 @@ private ArcweaveProjectAsset asset { get { if ( _asset == null ) { +#if UNITY_6000_5_OR_NEWER + _asset = EditorUtility.EntityIdToObject(_assetID) as ArcweaveProjectAsset; +#else _asset = EditorUtility.InstanceIDToObject(_assetID) as ArcweaveProjectAsset; +#endif } return _asset; } set { _asset = value; +#if UNITY_6000_5_OR_NEWER + _assetID = value != null ? value.GetEntityId() : default; +#else _assetID = value != null ? value.GetInstanceID() : 0; +#endif } } @@ -88,7 +100,11 @@ private float zoomFactor { public static void Open(ArcweaveProjectAsset asset) { var window = GetWindow(); window._asset = asset; +#if UNITY_6000_5_OR_NEWER + window._assetID = asset.GetEntityId(); +#else window._assetID = asset.GetInstanceID(); +#endif window._currentBoardIndex = 0; window.PanTo(asset.Project.Boards[0].Nodes[0].Pos - new Vector2(100, 100)); } @@ -551,4 +567,4 @@ Vector2 GetPosAlongCurve(Vector2 from, Vector2 to, Vector2 fromTangent, Vector2 } } -#endif \ No newline at end of file +#endif diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptFunctions.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptFunctions.cs index cfae923..ddf54bf 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptFunctions.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptFunctions.cs @@ -167,12 +167,12 @@ public object Reset(IList args) { } public object ResetAll(IList args) { - List variableNames = new List(); + HashSet variableIds = new HashSet(); foreach ( IVariable argument in args ) { - variableNames.Add(argument.Name); + variableIds.Add(argument.Id); } - foreach ( IVariable variable in this._project.Variables ) { - if ( !variableNames.Contains(variable.Name) ) { + foreach ( IVariable variable in this._project.GetAllVariables() ) { + if ( !variableIds.Contains(variable.Id) ) { state.SetVarValue(variable, variable.DefaultValue); } } @@ -212,4 +212,4 @@ public object ResetVisits(IList args) { return null; } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptParserBase.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptParserBase.cs index 5f06ba1..f8f9f91 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptParserBase.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptParserBase.cs @@ -26,17 +26,11 @@ public ArcscriptParserBase(ITokenStream input, TextWriter output, TextWriter err public void SetProject(IProject project) { this.Project = project; - foreach (var projectVariable in project.Variables) + foreach (var projectVariable in project.GetAllVariables()) { - VariableNames.Add(projectVariable.Name); - } - foreach (var projectBoard in project.Boards) - { - if (projectBoard.Variables == null) continue; - foreach (var projectBoardVariable in projectBoard.Variables) - { - VariableNames.Add(projectBoard.CustomId + "." + projectBoardVariable.Name); - } + VariableNames.Add(projectVariable.Parent == null + ? projectVariable.Name + : projectVariable.Parent.CustomId + "." + projectVariable.Name); } } @@ -126,4 +120,4 @@ internal void setLineStart(IToken token) } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptState.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptState.cs index 8ee3a4d..e091824 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptState.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptState.cs @@ -23,14 +23,9 @@ public ArcscriptState(string elementId, IProject project, System.Action? this.currentElement = elementId; this.project = project; - this.Variables = project.Variables.ToDictionary(variable => variable.Id, variable => variable); - foreach (var projectBoard in project.Boards) + foreach (var variable in project.GetAllVariables()) { - if (projectBoard.Variables == null) continue; - foreach (var projectBoardVariable in projectBoard.Variables) - { - Variables.TryAdd(projectBoardVariable.Id, projectBoardVariable); - } + Variables.TryAdd(variable.Id, variable); } if (emit != null) @@ -50,7 +45,9 @@ public ArcscriptState(string elementId, IProject project, System.Action? { if (scope != null) { - return variable.Name == name && scope == variable.Parent.CustomId; + return variable.Name == name && + variable.Parent != null && + scope == variable.Parent.CustomId; } return variable.Name == name && variable.Parent == null; @@ -109,4 +106,4 @@ public void ResetVisits() _emit("resetVisits"); } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptTranspiler.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptTranspiler.cs index 3f4c2dd..889fea2 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptTranspiler.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/ArcscriptTranspiler.cs @@ -83,7 +83,7 @@ public TranspilerOutput RunScript(string code) // List outputs = visitor.state.outputs; var outputResult = visitor.state.Outputs.GetText(); - var isCondition = tree.script() != null; + var isCondition = tree.condition() != null; return new TranspilerOutput(outputResult, visitor.state.VariableChanges, result, isCondition); } @@ -112,4 +112,4 @@ public TranspilerOutput(string output, Dictionary changes, objec } } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IAttribute.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IAttribute.cs index c4200d3..de46ca8 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IAttribute.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IAttribute.cs @@ -4,18 +4,22 @@ public interface IAttribute { public enum DataType { - Undefined, - StringPlainText, - StringRichText, - ComponentList, - AssetList, + Undefined = 0, + StringPlainText = 1, + StringRichText = 2, + ComponentList = 3, + AssetList = 4, + Boolean = 5, + Integer = 6, + Float = 7, } public enum ContainerType { - Undefined, - Component, - Element, + Undefined = 0, + Component = 1, + Element = 2, + Board = 3, } public string Name { get; } diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IBoard.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IBoard.cs index f60fead..63a796f 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IBoard.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IBoard.cs @@ -1,6 +1,6 @@ namespace Arcweave.Interpreter.INodes { - public interface IBoard : IHasVariables + public interface IBoard : IHasVariables, IHasAttributes { } } diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IComponent.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IComponent.cs index 8e4efe8..40b4702 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IComponent.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IComponent.cs @@ -1,6 +1,6 @@ namespace Arcweave.Interpreter.INodes { - public interface IComponent : IHasAttributes + public interface IComponent : IHasAttributes, IHasVariables { } } diff --git a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IProject.cs b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IProject.cs index ec66040..4760ea1 100644 --- a/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IProject.cs +++ b/Assets/Arcweave/Plugin/Runtime/Interpreter/INodes/IProject.cs @@ -12,10 +12,13 @@ public interface IProject #if GODOT public Array Variables { get; } public Dictionary Boards { get; } + public Dictionary Components { get; } #else public List Variables { get; } public List Boards { get; } + public List Components { get; } #endif + public System.Collections.Generic.IEnumerable GetAllVariables(); public Arcweave.Project.Element ElementWithId(string id); public Arcweave.Project.Variable GetVariable(string name, string scope = null); diff --git a/Assets/Arcweave/Plugin/Runtime/Project/Attribute.cs b/Assets/Arcweave/Plugin/Runtime/Project/Attribute.cs index 1d5ce48..9ec7669 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/Attribute.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/Attribute.cs @@ -10,6 +10,10 @@ namespace Arcweave.Project public partial class Attribute { + [field: SerializeField] + public string Id { get; private set; } + [field: SerializeField] + public string CustomId { get; private set; } [field: SerializeField] public string Name { get; private set; } [field: SerializeField] @@ -31,6 +35,15 @@ public partial class Attribute [SerializeReference] private List data_componentList; + [SerializeField] + private bool data_boolean; + + [SerializeField] + private int data_integer; + + [SerializeField] + private double data_float; + public object data { get { @@ -48,13 +61,18 @@ public object data { return data_stringRichText; } if ( Type == IAttribute.DataType.ComponentList ) { return data_componentList; } + if ( Type == IAttribute.DataType.Boolean ) { return data_boolean; } + if ( Type == IAttribute.DataType.Integer ) { return data_integer; } + if ( Type == IAttribute.DataType.Float ) { return data_float; } return null; } } public void InitializeInProject(Project project) { this.project = project; } - internal void Set(string name, IAttribute.DataType type, object _data, IAttribute.ContainerType containerType, string containerId) { + internal void Set(string id, string customId, string name, IAttribute.DataType type, object _data, IAttribute.ContainerType containerType, string containerId) { + this.Id = id; + this.CustomId = customId; this.Name = name; this.Type = type; if ( type == IAttribute.DataType.StringPlainText ) { data_string = (string)_data; } @@ -65,8 +83,11 @@ internal void Set(string name, IAttribute.DataType type, object _data, IAttribut data_stringRichText = null; } if ( type == IAttribute.DataType.ComponentList ) { data_componentList = (List)_data; } + if ( type == IAttribute.DataType.Boolean ) { data_boolean = (bool)_data; } + if ( type == IAttribute.DataType.Integer ) { data_integer = (int)_data; } + if ( type == IAttribute.DataType.Float ) { data_float = (double)_data; } this.containerType = containerType; this.containerId = containerId; } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Project/Board.cs b/Assets/Arcweave/Plugin/Runtime/Project/Board.cs index 5e6f052..89697e2 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/Board.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/Board.cs @@ -14,7 +14,7 @@ public partial class Board /* Hash key related to this board (e.g "1c11fa2a-f76c-4be7-aac4-6bf92a63240f") */ public string Id { get; private set; } [field: SerializeField] - + /* Name string id (e.g healer dialogue) */ public string CustomId { get; private set; } [field: SerializeField] @@ -25,6 +25,8 @@ public partial class Board public List Notes { get; private set; } [field: SerializeField] public List Variables { get; private set; } + [field: SerializeField] + public List Attributes { get; private set; } /// /// Initializes a board with an optional Arcweave custom id used for board-scoped variables. @@ -36,6 +38,7 @@ public Board(string id, string name, string customId, List nodes) Nodes = nodes; CustomId = customId; Variables = new List(); + Attributes = new List(); } /// @@ -48,6 +51,7 @@ public Board(string id, string name, string customId, List nodes, List(); + Attributes = new List(); } /// @@ -64,6 +68,7 @@ public Board(string id, string name, string customId, List nodes, List(); } ///Returns INode of type T with id. @@ -79,5 +84,18 @@ public void AddVariable(Variable variable) variable.Parent = this; Variables.Add(variable); } + + public void AddAttribute(Attribute attribute) + { + Attributes.Add(attribute); + } + + public void InitializeInProject(Project project) + { + foreach (var attribute in Attributes) + { + attribute.InitializeInProject(project); + } + } } } diff --git a/Assets/Arcweave/Plugin/Runtime/Project/ComponentAW.cs b/Assets/Arcweave/Plugin/Runtime/Project/ComponentAW.cs index e07ef6e..b4ffddd 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/ComponentAW.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/ComponentAW.cs @@ -10,15 +10,25 @@ public partial class Component [field: SerializeField] public string Id { get; private set; } [field: SerializeField] + public string CustomId { get; private set; } + [field: SerializeField] public string Name { get; private set; } [field: SerializeField] public List Attributes { get; private set; } + [field: SerializeField] + public List Variables { get; private set; } public void AddAttribute(Attribute attribute) { Attributes.Add(attribute); } + public void AddVariable(Variable variable) + { + variable.Parent = this; + Variables.Add(variable); + } + public void InitializeInProject(Project project) { foreach (var attribute in Attributes) @@ -30,13 +40,15 @@ public void InitializeInProject(Project project) [field: SerializeField] public Cover cover { get; private set; } - public void Set(string id, string name, List attributes, Cover cover) { + public void Set(string id, string customId, string name, List attributes, Cover cover) { Id = id; + CustomId = customId; Name = name; Attributes = attributes; + Variables = new List(); this.cover = cover; } public Texture2D GetCoverImage() => cover?.ResolveImage(); } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Project/Project.cs b/Assets/Arcweave/Plugin/Runtime/Project/Project.cs index 080f64d..bf6eb20 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/Project.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/Project.cs @@ -17,6 +17,7 @@ public partial class Project public List Boards { get; private set; } [field: UnityEngine.SerializeReference] public List components { get; private set; } + public List Components => components; [field: UnityEngine.SerializeReference] public List Variables { get; private set; } @@ -43,6 +44,7 @@ public void Initialize() { ResetVariablesToDefaultValues(); ResetVisits(); foreach ( var board in Boards ) { + board.InitializeInProject(this); foreach ( var node in board.Nodes ) { node.InitializeInProject(this); } @@ -89,19 +91,25 @@ public T GetNodeWithID(string id) where T : INode { ///---------------------------------------------------------------------------------------------- - ///Returns a variable by name. Provide a board CustomId in to search board-scoped variables; otherwise only global variables are searched. + ///Returns a variable by name. Provide a board or component CustomId in to search scoped variables; otherwise only global variables are searched. public Variable GetVariable(string name, string scope = null) { if (scope == null) { return Variables.FirstOrDefault(variable => variable.Name == name); } - var board = Boards.FirstOrDefault(board => board.CustomId == scope); - if (board != null) - { - return board.Variables.FirstOrDefault(variable => variable.Name == name); - } - return null; + var container = Boards.Cast() + .Concat(Components) + .FirstOrDefault(candidate => candidate.CustomId == scope); + return container?.Variables.FirstOrDefault(variable => variable.Name == name); + } + + ///Enumerates every global, board-scoped, and component-scoped variable. + public IEnumerable GetAllVariables() + { + return (Variables ?? new List()) + .Concat((Boards ?? new List()).SelectMany(board => board.Variables ?? new List())) + .Concat((Components ?? new List()).SelectMany(component => component.Variables ?? new List())); } /// @@ -112,7 +120,7 @@ public Variable GetVariable(string name, string scope = null) /// True if the variable was found and updated successfully; otherwise, false. public bool SetVariable(string name, object value) { - var variable = Variables.FirstOrDefault(x => x.Name == name); + var variable = GetVariable(name); if (variable == null) { Debug.LogError($"Global variable with name '{name}' not found."); @@ -123,10 +131,10 @@ public bool SetVariable(string name, object value) } /// - /// Sets the value of a board-scoped variable by its name and board CustomId. + /// Sets the value of a board- or component-scoped variable by its name and owner CustomId. /// - /// The variable name inside the board scope. - /// The board CustomId used as the variable scope. + /// The variable name inside the owner scope. + /// The board or component CustomId used as the variable scope. /// The new value to assign to the variable. /// True if the scoped variable was found and updated successfully; otherwise, false. public bool SetVariable(string name, string scope, object value) @@ -150,19 +158,7 @@ public bool SetVariable(string name, string scope, object value) /// public bool SetVariableById(string id, object value) { - var variable = Variables.FirstOrDefault(x => x.Id == id); - - if (variable == null) - { - foreach (var board in Boards) - { - variable = board.Variables?.FirstOrDefault(x => x.Id == id); - if (variable != null) - { - break; - } - } - } + var variable = GetAllVariables().FirstOrDefault(x => x.Id == id); if (variable == null) { @@ -175,20 +171,19 @@ public bool SetVariableById(string id, object value) ///---------------------------------------------------------------------------------------------- - ///Reset all global and board-scoped variables to their default values. + ///Reset all global, board-scoped, and component-scoped variables to their default values. public void ResetVariablesToDefaultValues() { - if (Variables != null) - foreach (var variable in Variables) - { - variable.ResetToDefaultValue(); - } + foreach (var variable in GetAllVariables()) + { + variable.ResetToDefaultValue(); + } } - ///Returns a string containing the saved state of all global and board-scoped variables. + ///Returns a string containing the saved state of all global and scoped variables. public string SaveVariables() { - var state = new State(Variables); + var state = new State(GetAllVariables()); return state.ToJson(); } @@ -201,12 +196,12 @@ public void LoadVariables(string save) var type = System.Type.GetType(variableState.type); object value = null; if (type == typeof(string)) { value = variableState.value; } - if ( type == typeof(int) ) { value = int.Parse(variableState.value); } - if ( type == typeof(double) ) { value = double.Parse(variableState.value); } + if ( type == typeof(int) ) { value = int.Parse(variableState.value, System.Globalization.CultureInfo.InvariantCulture); } + if ( type == typeof(double) ) { value = double.Parse(variableState.value, System.Globalization.CultureInfo.InvariantCulture); } if ( type == typeof(bool) ) { value = bool.Parse(variableState.value); } SetVariableById(variableState.id, value); } } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Project/ProjectMaker.cs b/Assets/Arcweave/Plugin/Runtime/Project/ProjectMaker.cs index 9802df6..956c00a 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/ProjectMaker.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/ProjectMaker.cs @@ -79,8 +79,9 @@ public Project MakeProject() { } foreach ( var key in jvariables.AsDictionary.Keys ) { var variable = TryMakeVariable(key); - if ( variable != null ) { projVariables.Add(variable); } //null = has children + if ( variable != null && variable.Parent == null ) { projVariables.Add(variable); } //null = has children or a scoped variable } + MakeAttributeVariables(); return new Project(name, startElement, projBoards, projComponents, projVariables); } @@ -106,11 +107,19 @@ Board TryMakeBoard(string id) { var boardNotes = new List(); foreach ( var key in GetProp(jboards, id, "notes").AsList ) { boardNotes.Add(TryMakeNote(key.AsString)); } - string customId = null; - if ( HasProperty(jboards, id, "customId", out var customIdProp) ) { - customId = customIdProp.AsString; + var boardAttributes = new List(); + if ( HasProperty(jboards, id, "attributes", out var attributeids) && attributeids.IsList ) { + foreach ( var attributeid in attributeids.AsList ) { + boardAttributes.Add(TryMakeAttribute(attributeid.AsString)); + } + } + + var customId = GetProp(jboards, id, "customId")?.AsString; + var board = boards[id] = new Board(id, name, customId, boardNodes, boardNotes); + foreach ( var attribute in boardAttributes ) { + board.AddAttribute(attribute); } - return boards[id] = new Board(id, name, customId, boardNodes, boardNotes); + return board; } //.. @@ -249,13 +258,14 @@ Component TryMakeComponent(string id) { components[id] = component = new Component(); var name = GetProp(jcomponents, id, "name")?.AsString; + var customId = GetProp(jcomponents, id, "customId")?.AsString; var attributes = new List(); var attributeids = GetProp(jcomponents, id, "attributes").AsList; foreach ( var attributeid in attributeids ) { attributes.Add(TryMakeAttribute(attributeid.AsString)); } var cover = MakeCover(jcomponents, id); - component.Set(id, name, attributes, cover); + component.Set(id, customId, name, attributes, cover); } return component; } @@ -265,11 +275,13 @@ Attribute TryMakeAttribute(string id) { if ( !attributes.TryGetValue(id, out var attribute) ) { attributes[id] = attribute = new Attribute(); var name = GetProp(jattributes, id, "name")?.AsString; + var customId = GetProp(jattributes, id, "customId")?.AsString; var jcontainerType = GetProp(jattributes, id, "cType")?.AsString; IAttribute.ContainerType containerType = IAttribute.ContainerType.Undefined; if ( jcontainerType == "elements" ) { containerType = IAttribute.ContainerType.Element; } if ( jcontainerType == "components" ) { containerType = IAttribute.ContainerType.Component; } + if ( jcontainerType == "boards" ) { containerType = IAttribute.ContainerType.Board; } var containerId = GetProp(jattributes, id, "cId")?.AsString; @@ -278,11 +290,9 @@ Attribute TryMakeAttribute(string id) { var jtype = GetProp(jattributes, id, "value.type")?.AsString; if ( jtype == "string" ) { - type = IAttribute.DataType.StringRichText; - data = GetProp(jattributes, id, "value.data")?.AsString; - if ( HasProperty(jattributes, id, "value.plain", out var isPlain) && isPlain.AsBool == true ) { - type = IAttribute.DataType.StringPlainText; - } + var isPlain = HasProperty(jattributes, id, "value.plain", out var plain) && plain.AsBool; + type = isPlain ? IAttribute.DataType.StringPlainText : IAttribute.DataType.StringRichText; + data = GetProp(jattributes, id, "value.data")?.AsString ?? (isPlain ? string.Empty : null); } if ( jtype == "component-list" ) { @@ -295,7 +305,21 @@ Attribute TryMakeAttribute(string id) { } data = componentList; } - attribute.Set(name, type, data, containerType, containerId); + + var jdata = GetProp(jattributes, id, "value.data"); + if ( jtype == "boolean" && jdata != null ) { + type = IAttribute.DataType.Boolean; + data = jdata.AsBool; + } + if ( jtype == "integer" && jdata != null ) { + type = IAttribute.DataType.Integer; + data = (int)jdata.AsInt64; + } + if ( jtype == "float" && jdata != null ) { + type = IAttribute.DataType.Float; + data = jdata.Type == fsDataType.Double ? jdata.AsDouble : (double)jdata.AsInt64; + } + attribute.Set(id, customId, name, type, data, containerType, containerId); } return attribute; } @@ -304,6 +328,7 @@ Attribute TryMakeAttribute(string id) { Variable TryMakeVariable(string id) { if ( HasChildren(jvariables, id) ) { return null; } + if ( variables.TryGetValue(id, out var existingVariable) ) { return existingVariable; } object value = null; var name = GetProp(jvariables, id, "name")?.AsString; @@ -333,14 +358,49 @@ Variable TryMakeVariable(string id) { { var variable = new Variable(id, name, value, board); board.AddVariable(variable); + variables[id] = variable; return variable; } else { Debug.LogWarning($"Variable '{name}' is supposed to be attached to board with id '{boardId}', but no such board was found."); + return null; } } - return new Variable(id, name, value); + return variables[id] = new Variable(id, name, value); + } + + void MakeAttributeVariables() { + foreach ( var board in boards.Values ) { + MakeAttributeVariables(board, board.Id, IAttribute.ContainerType.Board); + } + foreach ( var component in components.Values ) { + MakeAttributeVariables(component, component.Id, IAttribute.ContainerType.Component); + } + } + + void MakeAttributeVariables(IHasVariables container, string containerId, IAttribute.ContainerType containerType) { + if ( string.IsNullOrEmpty(container.CustomId) ) { return; } + + var containerAttributes = container is Board board ? board.Attributes : ((Component)container).Attributes; + foreach ( var attribute in containerAttributes ) { + if ( !IsVariableAttribute(attribute, containerId, containerType) ) { continue; } + if ( variables.ContainsKey(attribute.Id) ) { continue; } + + var variable = new Variable(attribute.Id, attribute.CustomId, attribute.data, container); + variables[attribute.Id] = variable; + container.AddVariable(variable); + } + } + + bool IsVariableAttribute(Attribute attribute, string containerId, IAttribute.ContainerType containerType) { + if ( attribute == null || string.IsNullOrEmpty(attribute.CustomId) ) { return false; } + if ( attribute.containerType != containerType || attribute.containerId != containerId ) { return false; } + + return attribute.Type == IAttribute.DataType.Boolean + || attribute.Type == IAttribute.DataType.Integer + || attribute.Type == IAttribute.DataType.Float + || attribute.Type == IAttribute.DataType.StringPlainText; } ///---------------------------------------------------------------------------------------------- @@ -415,7 +475,7 @@ private AudioAsset MakeAudioAsset(fsData audioEntry) //... fsData GetProp(fsData source, string id, string propertyPath) { var result = source[id + '.' + propertyPath]; - return result.IsNull ? null : result; + return result == null || result.IsNull ? null : result; } //... @@ -435,4 +495,4 @@ UnityEngine.Vector2Int GetPos(fsData x, fsData y) return new UnityEngine.Vector2Int(xValue, yValue); } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Project/State.cs b/Assets/Arcweave/Plugin/Runtime/Project/State.cs index cd29557..4ddd0b9 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/State.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/State.cs @@ -19,21 +19,24 @@ internal struct VariableState public State() {} - public State(List variables) + public State(IEnumerable variables) { SetState(variables); } - public void SetState(List vars) + public void SetState(IEnumerable vars) { - variables = new VariableState[vars.Count]; + var variableList = new List(vars); + variables = new VariableState[variableList.Count]; int i = 0; - foreach (var variable in vars) + foreach (var variable in variableList) { variables[i].id = variable.Id; try { - variables[i].value = variable.Value.ToString(); + variables[i].value = variable.Value is IFormattable formattable + ? formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture) + : variable.Value.ToString(); } catch (Exception e) { @@ -60,4 +63,4 @@ public VariableState[] GetVariables() return variables; } } -} \ No newline at end of file +} diff --git a/Assets/Arcweave/Plugin/Runtime/Project/Variable.cs b/Assets/Arcweave/Plugin/Runtime/Project/Variable.cs index 321209d..ee077d5 100644 --- a/Assets/Arcweave/Plugin/Runtime/Project/Variable.cs +++ b/Assets/Arcweave/Plugin/Runtime/Project/Variable.cs @@ -1,3 +1,5 @@ +using System; +using System.Globalization; using Arcweave.Interpreter.INodes; using UnityEngine; @@ -45,6 +47,10 @@ public object DefaultValue /// Initializes a variable with an explicit Arcweave variable id. /// public Variable(string id, string name, object value) { + if (value == null) + { + throw new ArgumentNullException(nameof(value), "Variable value cannot be null."); + } this.Id = id; this.Name = name; this.Value = value; @@ -56,12 +62,8 @@ public Variable(string id, string name, object value) { /// Initializes a variable with an explicit Arcweave variable id and owning scope. /// public Variable(string id, string name, object value, IHasVariables parent) + : this(id, name, value) { - Id = id; - Name = name; - Value = value; - DefaultValue = value; - _typeName = value.GetType().FullName; Parent = parent; } @@ -86,11 +88,11 @@ private string GetSerializedValue(object value) } if (type == typeof(int)) { - return "i" + value; + return "i" + ((int)value).ToString(CultureInfo.InvariantCulture); } if (type == typeof(double)) { - return "d" + value; + return "d" + ((double)value).ToString("R", CultureInfo.InvariantCulture); } if (type == typeof(bool)) { @@ -102,7 +104,7 @@ private string GetSerializedValue(object value) private object DeserializeValue(string stringValue) { - if (stringValue.Length == 0) + if (string.IsNullOrEmpty(stringValue)) { return default; } @@ -111,10 +113,10 @@ private object DeserializeValue(string stringValue) return type switch { 'n' => null, - 's' => valueSerialized[1..], - 'i' => int.Parse(valueSerialized[1..]), - 'd' => double.Parse(valueSerialized[1..]), - 'b' => bool.Parse(valueSerialized[1..]), + 's' => stringValue[1..], + 'i' => int.Parse(stringValue[1..], CultureInfo.InvariantCulture), + 'd' => double.Parse(stringValue[1..], CultureInfo.InvariantCulture), + 'b' => bool.Parse(stringValue[1..]), _ => default }; } diff --git a/Changelog.md b/Changelog.md index 2f8fb05..fb82e30 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,16 +6,18 @@ All notable changes to this project will be documented in this file. ### Added -- Added board-scoped variable support through `Board.Variables`, board `CustomId` scopes, and the shared `IHasVariables` interface. +- Added attribute-backed board and component variable support through container `CustomId` scopes and the shared `IHasVariables` interface. +- Added boolean, integer, float, and plain-string attribute value support. - Added variable identity metadata via `Variable.Id` and `Variable.Parent` so runtime updates and saved state can resolve variables unambiguously. -- Added `Project.SetVariable(string name, object value, string scope)` for setting board-scoped variables from code. +- Added `Project.SetVariable(string name, string scope, object value)` for setting board- or component-scoped variables from code. - Added `ArcweavePlugin.asmdef` to give the plugin its own Unity assembly definition. ### Changed -- Updated `Project.GetVariable` to accept an optional board scope and documented the difference between global/project variables and board variables. -- Updated variable reset and save/load flows to include both global and board-scoped variables. -- Updated Arcscript parsing and runtime resolution to support scoped variable references in the form `boardCustomId.variableName`. +- Updated `Project.GetVariable` to accept an optional board or component scope. +- Updated variable reset and save/load flows to include global, board-scoped, and component-scoped variables. +- Updated Arcscript parsing and runtime resolution to support scoped variable references in the form `containerCustomId.variableName`. +- Updated the JSON importer to create scoped variables from eligible board and component attributes while retaining legacy board-variable imports. - Updated internal Arcscript state mutation to resolve variables by Arcweave variable id instead of by name. - Updated imported float variables to use `double` consistently across parsing, evaluation, and state restoration. @@ -26,5 +28,9 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fixed interpreter results so Arcscript conditions are identified correctly. +- Fixed the project viewer to use Unity's `EntityId` APIs on Unity 6000.5 and newer while retaining compatibility with earlier editor versions. - Fixed runtime application of Arcscript variable changes for board-scoped variables. - Fixed variable save/load restoration to target variables by id rather than by name. +- Fixed `resetAll` to include scoped variables and exclude variables by stable ID rather than by name. +- Fixed Unity variable deserialization so current and default values remain distinct. diff --git a/README.md b/README.md index f49a581..1caa9f8 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ This plugin imports [Arcweave](https://arcweave.com/) projects into Unity. It su Choose one of the following methods: +### Prerequisite: Unity UI + +The bundled demo uses the Unity UI package. If your project does not already include it, install **Unity UI** (`com.unity.ugui`, version `2.0.0` or a compatible production release) from **Window > Package Manager** before importing the plugin. New Unity 6000.5 projects do not include this package by default, and the demo scripts cannot compile without it. + ### Method 1: Manual Installation from GitHub - Recommended 1. Download this repository as a ZIP file from [GitHub](https://github.com/arcweave/arcweave-unity-plugin/archive/refs/heads/main.zip) @@ -272,7 +276,7 @@ The root container for an imported Arcweave project. Access via `ArcweaveProject |----------|------|-------------| | `name` | string | Project name | | `boards` | List\ | All boards in the project | -| `components` | List\ | All components (characters/entities) | +| `components` / `Components` | List\ | All components (characters/entities) | | `Variables` | List\ | All global/project-scoped variables | | `StartingElement` | Element | Entry point for the narrative | @@ -285,35 +289,39 @@ The root container for an imported Arcweave project. Access via `ArcweaveProject | `Board BoardWithName(string name)` | Get board by name | | `Element ElementWithId(string id)` | Get element by ID | | `T GetNodeWithID(string id)` | Get any node by ID and type | -| `Variable GetVariable(string name, string scope = null)` | Get a variable by name. Pass a board `CustomId` as `scope` for board-scoped variables | +| `Variable GetVariable(string name, string scope = null)` | Get a variable by name. Pass a board or component `CustomId` for scoped variables | +| `IEnumerable GetAllVariables()` | Enumerate global, board-scoped, and component-scoped variables | | `bool SetVariable(string name, object value)` | Set a global/project-scoped variable by name | -| `bool SetVariable(string name, string scope, object value)` | Set a board-scoped variable by name and board `CustomId` | +| `bool SetVariable(string name, string scope, object value)` | Set a scoped variable by name and owner `CustomId` | | `void ResetVariablesToDefaultValues()` | Reset all variables to defaults | -| `string SaveVariables()` | Serialize current global and board-scoped variable state to JSON | -| `void LoadVariables(string json)` | Restore current global and board-scoped variable state from JSON | +| `string SaveVariables()` | Serialize current global and scoped variable state to JSON | +| `void LoadVariables(string json)` | Restore current global and scoped variable state from JSON | | `int Visits(string elementId)` | Get visit count for an element | | `void ResetVisits()` | Reset all visit counts to 0 | --- -### Board Variables +### Scoped Variables -Boards can now define their own scoped variables in addition to project/global variables. +Boards and components can define scoped variables in addition to project/global variables. In current exports, scoped variables are backed by boolean, integer, float, or plain-string attributes with a custom ID. - `Project.Variables` contains only project/global variables. -- `Board.Variables` contains variables scoped to that specific board. -- Board-scoped variables are addressed by the board's `CustomId`, not by the board name. -- In Arcscript, scoped variables use `boardCustomId.variableName`. +- `Board.Variables` and `Component.Variables` contain their scoped variables. +- Scoped variables use the attribute's `CustomId` as their name and the owning container's `CustomId` as their scope. +- The attribute ID remains the variable ID used by runtime changes and save data. +- In Arcscript, scoped variables use `containerCustomId.variableName`. ```csharp Variable globalHealth = project.GetVariable("health"); Variable boardHealth = project.GetVariable("health", "intro_board"); +Variable componentHealth = project.GetVariable("health", "hero"); project.SetVariable("health", 10); project.SetVariable("health", 2, "intro_board"); +project.SetVariable("health", 5, "hero"); ``` -If a board variable exists, ensure the source Arcweave board has a `CustomId`, otherwise it cannot be resolved by scope. +Both the source attribute and its board/component must have a `CustomId` for the attribute to become a scoped variable. Plain-string attributes with null data import as an empty string. --- @@ -330,6 +338,7 @@ Container for narrative nodes. | `Name` | string | Board name | | `Nodes` | List\ | All nodes (Elements, Branches, Jumpers) | | `Notes` | List\ | Annotation notes | +| `Attributes` | List\ | Board attributes | | `Variables` | List\ | Variables scoped to this board | #### Methods @@ -474,8 +483,10 @@ Character or entity definition. **Not a Unity MonoBehaviour.** | Property | Type | Description | |----------|------|-------------| | `Id` | string | Unique identifier | +| `CustomId` | string | Arcweave custom ID used as a variable scope | | `Name` | string | Component name | | `Attributes` | List\ | Custom attributes | +| `Variables` | List\ | Variables scoped to this component | | `cover` | Cover | Cover image reference | #### Methods @@ -488,23 +499,25 @@ Character or entity definition. **Not a Unity MonoBehaviour.** ### Attribute -Custom metadata on elements or components. +Custom metadata on elements, components, or boards. #### Properties | Property | Type | Description | |----------|------|-------------| +| `Id` | string | Arcweave attribute identifier | +| `CustomId` | string | Optional Arcscript variable name for eligible scoped attributes | | `Name` | string | Attribute name | -| `Type` | DataType | StringPlainText, StringRichText, or ComponentList | +| `Type` | DataType | Plain/rich string, component list, boolean, integer, or float | | `data` | object | Attribute data (lazy-evaluated for RichText) | -| `containerType` | ContainerType | Element or Component | +| `containerType` | ContainerType | Element, Component, or Board | | `containerId` | string | ID of the container | --- ### Variable -Project/global or board-scoped state variable. Supports: `int`, `double`, `bool`, `string`. +Project/global, board-scoped, or component-scoped state variable. Supports: `int`, `double`, `bool`, `string`. #### Properties @@ -514,7 +527,7 @@ Project/global or board-scoped state variable. Supports: `int`, `double`, `bool` | `Name` | string | Variable name | | `Value` | object | Current value | | `DefaultValue` | object | Initial value | -| `Parent` | IHasVariables | Owning scope (`null` for global variables, `Board` for board-scoped variables) | +| `Parent` | IHasVariables | Owning scope (`null` for globals, otherwise a `Board` or `Component`) | | `Type` | Type | System.Type of the value | #### Methods @@ -618,9 +631,9 @@ Event-driven narrative player (demo helper class). 3. **GetOptions() has side effects**: This method internally saves and restores variable state while evaluating branch conditions. -4. **Initialize() resets everything**: Calling `Project.Initialize()` resets all global and board-scoped variables to defaults and all visit counts to 0. +4. **Initialize() resets everything**: Calling `Project.Initialize()` resets all global and scoped variables to defaults and all visit counts to 0. -5. **Board variables require scope**: `Project.GetVariable("health")` only searches project/global variables. Use `Project.GetVariable("health", "boardCustomId")` and `Project.SetVariable("health", "boardCustomId", value)` for board-scoped variables. +5. **Scoped variables require scope**: `Project.GetVariable("health")` only searches project/global variables. Pass the owning board or component `CustomId` to access a scoped variable. 6. **Images and Audio clips must be in Resources folder**: Cover images and Audio clips are loaded via `Resources.Load()`. Place them in any `Resources` folder and ensure filenames match (without extension).