Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Copy Key")]
public class CopyKeyAction : IBlackboardAction
{
[BlackboardProperty("Source", BlackboardKeyType.Object)]
public BlackboardProperty Source { get; set; }
[BlackboardProperty("Target", BlackboardKeyType.Object)]
public BlackboardProperty Target { get; set; }
public Task Execute(Blackboard blackboard)
{
if (Source != Target && Source.IsKey && Target.IsKey)
{
var value = blackboard[Source];
blackboard[Target] = value;
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Set Value")]
public class SetKeyValueAction : IBlackboardAction
{
[BlackboardProperty(BlackboardKeyType.Object)]
public BlackboardProperty Key { get; set; }
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Value { get; set; }
public Task Execute(Blackboard blackboard)
{
var value = blackboard.GetValue<int>(Value);
blackboard[Key] = value;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Set State Delay")]
public class SetStateDelayAction : IBlackboardAction
{
[BlackboardProperty("Delay", BlackboardKeyType.Integer)]
public BlackboardProperty Delay { get; set; }
[BlackboardProperty("Success", BlackboardKeyType.Boolean, Usage = BlackboardKeyUsage.Output)]
public BlackboardProperty Success { get; set; }
public Task Execute(Blackboard blackboard)
{
var delay = blackboard.GetValue<int>(Delay);
if (delay.HasValue)
{
blackboard[DebugBlackboardDecorator.StateDelayKey] = delay;
}
blackboard[Success] = delay.HasValue;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class Blackboard
{
private readonly Dictionary<BlackboardKey, object?> _objects = new Dictionary<BlackboardKey, object?>();
public virtual IReadOnlyCollection<BlackboardKey> Keys
=> _objects.Keys;
public virtual T? GetValue<T>(BlackboardKey key)
where T : struct
{
if (_objects.TryGetValue(key, out var value) && value is T result)
{
return result;
}
return default;
}
public virtual T? GetObject<T>(BlackboardKey key)
where T : class
{
if (_objects.TryGetValue(key, out var value))
{
return value as T;
}
return default;
}
public virtual object? GetObject(BlackboardKey key)
{
if (_objects.TryGetValue(key, out var value))
{
return value;
}
return default;
}
public virtual void Set(BlackboardKey key, object? value)
=> _objects[key] = value;
public virtual bool HasKey(BlackboardKey key)
=> _objects.ContainsKey(key);
public virtual void Remove(BlackboardKey key)
=> _objects.Remove(key);
public virtual void Clear()
=> _objects.Clear();
public void CopyTo(Blackboard newBlackboard)
{
foreach (var kvp in _objects)
{
newBlackboard.Set(kvp.Key, kvp.Value);
}
}
public object? this[BlackboardKey key]
{
get => GetObject(key);
set => Set(key, value);
}
public T? GetValue<T>(BlackboardProperty value) where T : struct
=> value.IsValue ? value.GetValue<T>() : GetValue<T>(value.Key);
public T? GetObject<T>(BlackboardProperty value) where T : class
=> value.IsValue ? value.GetObject<T>() : GetObject<T>(value.Key);
public object? GetObject(BlackboardProperty value)
=> value.IsValue ? value.Value : GetObject(value.Key);
}
public static class BlackboardExtensions
{
public static bool IsValid(this BlackboardKey key)
=> key != BlackboardKey.Invalid;
public static bool IsValid(this BlackboardProperty action)
=> action != BlackboardProperty.Invalid;
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public enum BooleanOperator
{
And,
Or
}
public class BlackboardConditionSet : IBlackboardCondition
{
public BlackboardConditionSet(IEnumerable<IBlackboardCondition> conditions, BooleanOperator op)
{
Conditions = new List<IBlackboardCondition>(conditions);
Operator = op;
}
public IReadOnlyList<IBlackboardCondition> Conditions { get; }
public BooleanOperator Operator { get; set; }
public async Task<bool> Evaluate(Blackboard blackboard)
{
bool result = true;
if (Operator == BooleanOperator.And)
{
for (int i = 0; i < Conditions.Count; i++)
{
result &= await Conditions[i].Evaluate(blackboard);
}
}
else if (Operator == BooleanOperator.Or)
{
for (int i = 0; i < Conditions.Count; i++)
{
result |= await Conditions[i].Evaluate(blackboard);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace Nodify.StateMachine
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class BlackboardItemAttribute : Attribute
{
public BlackboardItemAttribute(string displayName)
{
DisplayName = displayName;
}
public string DisplayName { get; }
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
namespace Nodify.StateMachine
{
public enum BlackboardKeyType
{
Boolean,
Integer,
Double,
String,
Object
}
[DebuggerDisplay("{Name}: {Type}")]
public readonly struct BlackboardKey : IEquatable<BlackboardKey>
{
public static BlackboardKey Invalid { get; } = new BlackboardKey();
public BlackboardKey(string name, BlackboardKeyType type)
{
Name = name ?? throw new ArgumentException(nameof(name));
Type = type;
}
public BlackboardKey(string name) : this(name, BlackboardKeyType.Object)
{
}
public readonly string Name;
public readonly BlackboardKeyType Type;
public static implicit operator BlackboardKey(string name)
=> new BlackboardKey(name);
public static implicit operator string(BlackboardKey key)
=> key.Name;
public override bool Equals(object? obj)
=> obj is BlackboardKey bk && bk.Equals(this);
public override int GetHashCode()
=> Name?.GetHashCode() ?? -1;
public bool Equals(BlackboardKey other)
=> other.Name == Name;
public static bool operator ==(BlackboardKey left, BlackboardKey right)
=> left.Equals(right);
public static bool operator !=(BlackboardKey left, BlackboardKey right)
=> !(left == right);
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Diagnostics;
namespace Nodify.StateMachine
{
[DebuggerDisplay("{IsKey ? Key : Value}")]
public struct BlackboardProperty : IEquatable<BlackboardProperty>
{
public static BlackboardProperty Invalid { get; } = new BlackboardProperty();
public BlackboardProperty(BlackboardKey key)
{
Key = key;
Value = default;
}
public BlackboardProperty(object? value)
{
Key = BlackboardKey.Invalid;
Value = value;
}
public BlackboardKey Key { get; }
public object? Value { get; }
public bool IsKey => Key.IsValid();
public bool IsValue => !IsKey;
public static implicit operator BlackboardKey(BlackboardProperty action)
=> action.Key;
public override bool Equals(object? obj)
=> obj is BlackboardProperty action && action.Equals(this);
public override int GetHashCode()
=> IsKey ? Key.GetHashCode() : Value?.GetHashCode() ?? -1;
public bool Equals(BlackboardProperty other)
=> IsKey == other.IsKey && IsValue == other.IsValue && Key == other.Key && Value == other.Value;
public static bool operator ==(BlackboardProperty left, BlackboardProperty right)
=> left.Equals(right);
public static bool operator !=(BlackboardProperty left, BlackboardProperty right)
=> !(left == right);
public T? GetValue<T>() where T : struct
=> Value is T result ? result : default;
public T? GetObject<T>() where T : class
=> Value as T;
}
}

View File

@@ -0,0 +1,42 @@
using System;
namespace Nodify.StateMachine
{
public enum BlackboardKeyUsage
{
Input,
Output
}
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class BlackboardPropertyAttribute : Attribute
{
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
/// <param name="name">The display name of the key.</param>
/// <param name="type">The data type of the value that the key refers to.</param>
public BlackboardPropertyAttribute(string? name, BlackboardKeyType type = BlackboardKeyType.Object)
{
Name = name;
Type = type;
}
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
/// <param name="type">The data type of the value that the key refers to.</param>
public BlackboardPropertyAttribute(BlackboardKeyType type = BlackboardKeyType.Object) : this(null, type)
{
}
public string? Name { get; }
public BlackboardKeyType Type { get; }
public BlackboardKeyUsage Usage { get; set; }
public bool CanChangeType { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public interface IBlackboardAction
{
Task Execute(Blackboard blackboard);
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public interface IBlackboardCondition
{
Task<bool> Evaluate(Blackboard blackboard);
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Are Equal")]
public class AreEqualCondition : IBlackboardCondition
{
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Left { get; set; }
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Right { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
{
var left = blackboard.GetObject(Left);
var right = blackboard.GetObject(Right);
// TODO: Equality
return Task.FromResult(Equals(left, right));
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Has Key")]
public class HasKeyCondition : IBlackboardCondition
{
[BlackboardProperty("Key Name", BlackboardKeyType.String)]
public BlackboardProperty Key { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
{
var keyName = blackboard.GetObject<string>(Key);
if (keyName != null)
{
return Task.FromResult(blackboard.HasKey(keyName));
}
return Task.FromResult(false);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Has Value")]
public class HasValueCondition : IBlackboardCondition
{
[BlackboardProperty(BlackboardKeyType.Object)]
public BlackboardKey Key { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
=> Task.FromResult(blackboard.GetObject(Key) != null);
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class DebugBlackboardDecorator : Blackboard
{
public static BlackboardKey StateDelayKey { get; } = "__state.delay";
public static BlackboardKey TransitionDelayKey { get; } = "__transition.delay";
private Blackboard? _blackboard;
public event Action<BlackboardKey, object?>? ValueChanged;
public DebugBlackboardDecorator(Blackboard? blackboard = default)
=> Attach(blackboard);
public override IReadOnlyCollection<BlackboardKey> Keys => _blackboard?.Keys ?? Array.Empty<BlackboardKey>();
public override void Remove(BlackboardKey key)
=> _blackboard?.Remove(key);
public override void Clear()
=> _blackboard?.Clear();
public override T? GetObject<T>(BlackboardKey key) where T : class
=> _blackboard?.GetObject<T>(key);
public override T? GetValue<T>(BlackboardKey key)
=> _blackboard?.GetValue<T>(key);
public override void Set(BlackboardKey key, object? value)
{
_blackboard?.Set(key, value);
ValueChanged?.Invoke(key, value);
}
public override bool HasKey(BlackboardKey key)
=> _blackboard?.HasKey(key) ?? false;
public override object? GetObject(BlackboardKey key)
=> _blackboard?.GetObject(key);
public virtual void Attach(Blackboard? blackboard)
{
_blackboard = blackboard;
Set(StateDelayKey, 100);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class DebugStateDecorator : State
{
private readonly State _state;
public DebugStateDecorator(State state) : base(state.Id, state.Transitions)
{
_state = state;
}
public override async Task Activate(Blackboard blackboard)
{
int? delay = blackboard.GetValue<int>(DebugBlackboardDecorator.StateDelayKey);
await Task.Delay(Math.Max(10, delay ?? 10));
await _state.Activate(blackboard);
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class DebugTransitionDecorator : Transition
{
private readonly Transition _transition;
public DebugTransitionDecorator(Transition transition) : base(transition.From, transition.To)
{
_transition = transition;
}
public override async Task<bool> CanActivate(Blackboard blackboard)
{
int? delay = blackboard.GetValue<int>(DebugBlackboardDecorator.TransitionDelayKey);
if (delay > 0)
{
await Task.Delay(delay.Value);
}
return await _transition.CanActivate(blackboard);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class State
{
public Guid Id { get; }
public IBlackboardAction? Action { get; }
public State(Guid id, IEnumerable<Transition> transitions, IBlackboardAction? action = default)
{
Id = id;
Action = action;
Transitions = new List<Transition>(transitions);
}
public IReadOnlyList<Transition> Transitions { get; }
public virtual Task Activate(Blackboard blackboard)
=> Action?.Execute(blackboard) ?? Task.CompletedTask;
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public enum MachineState
{
Stopped,
Running,
Paused,
}
public delegate void StateTransitionEventHandler(Guid from, Guid to);
public delegate void StateChangedEventHandler(MachineState newStatus);
public class StateMachine
{
private readonly Dictionary<Guid, State> _states;
public State Root { get; }
public MachineState? State { get; private set; }
public Blackboard Blackboard { get; } = new Blackboard();
// param = aborted
public event StateChangedEventHandler? StateChanged;
public event StateTransitionEventHandler? StateTransition;
public StateMachine(Guid root, IEnumerable<State> states, Blackboard? blackboard = default)
{
_states = states.ToDictionary(x => x.Id, x => x);
if (!_states.ContainsKey(root))
{
throw new ArgumentException(nameof(root));
}
Root = _states[root];
if (blackboard != null)
{
Blackboard = blackboard;
}
}
public async Task Start()
{
if (ChangeState(MachineState.Running))
{
// Skip root state
State? previous = Root;
State? current = await GetNext(Root);
while (State != MachineState.Stopped && current != null)
{
if (State == MachineState.Paused)
{
await Task.Delay(10);
}
else
{
StateTransition?.Invoke(previous.Id, current.Id);
previous = current;
await current.Activate(Blackboard);
current = await GetNext(current);
}
}
ChangeState(MachineState.Stopped);
}
}
private async Task<State?> GetNext(State current)
{
var transitions = current.Transitions;
for (int i = 0; i < transitions.Count; i++)
{
var transition = transitions[i];
if (_states.TryGetValue(transition.To, out var result) && await transition.CanActivate(Blackboard))
{
return result;
}
}
return default;
}
public void Stop()
=> ChangeState(MachineState.Stopped);
public void Pause()
=> ChangeState(MachineState.Paused);
public void Unpause()
=> ChangeState(MachineState.Running);
private bool ChangeState(MachineState newState)
{
if (newState == MachineState.Running || (State != null && State != newState))
{
State = newState;
StateChanged?.Invoke(newState);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class Transition
{
public Transition(Guid from, Guid to, IBlackboardCondition? condition = default)
{
From = from;
To = to;
Condition = condition;
}
public Guid From { get; }
public Guid To { get; }
public IBlackboardCondition? Condition { get; }
public virtual Task<bool> CanActivate(Blackboard blackboard)
=> Condition?.Evaluate(blackboard) ?? Task.FromResult(true);
}
}