Skip to content

Commit 22853f6

Browse files
authored
Merge pull request #37 from gamesmiths-guild/feature/event-nodes
Added event nodes.
2 parents 15f5707 + aee400e commit 22853f6

47 files changed

Lines changed: 1931 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Forge.Tests/Events/EventTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,21 +439,21 @@ public void Single_handler_called_once_even_with_multiple_matching_tags()
439439

440440
[Fact]
441441
[Trait("Isolation", null)]
442-
public void Generic_raise_does_not_trigger_non_generic_handlers()
442+
public void Generic_raise_also_triggers_non_generic_handlers_with_boxed_payload()
443443
{
444444
var events = new EventManager();
445445
var eventTag = Tag.RequestTag(_tagsManager, "simple.tag");
446-
bool nonGenericCalled = false;
446+
object? capturedPayload = null;
447447
bool genericCalled = false;
448448

449-
events.Subscribe(eventTag, _ => nonGenericCalled = true);
449+
events.Subscribe(eventTag, data => capturedPayload = data.Payload);
450450
events.Subscribe<int>(eventTag, _ => genericCalled = true);
451451

452452
// Raise generic event
453453
events.Raise(new EventData<int> { EventTags = eventTag.GetSingleTagContainer()!, Payload = 42 });
454454

455-
nonGenericCalled.Should().BeFalse("non-generic handler should not be called by generic raise");
456455
genericCalled.Should().BeTrue();
456+
capturedPayload.Should().Be(42, "non-generic subscriptions are catch-all and receive the boxed payload");
457457
}
458458

459459
[Fact]

Forge.Tests/Helpers/StatescriptTestHelpers.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ public static CueNode CreateCueNode(
9595
node.BindInput(CueNode.TargetInput, targetPropertyName);
9696
return node;
9797
}
98+
99+
public static RaiseEventNode CreateRaiseEventNode(
100+
StringKey eventTagPropertyName,
101+
StringKey targetPropertyName)
102+
{
103+
var node = new RaiseEventNode();
104+
node.BindInput(RaiseEventNode.EventTagInput, eventTagPropertyName);
105+
node.BindInput(RaiseEventNode.TargetInput, targetPropertyName);
106+
return node;
107+
}
108+
109+
public static EventListenerNode CreateEventListenerNode(
110+
StringKey eventTagPropertyName,
111+
StringKey listenOnPropertyName)
112+
{
113+
var node = new EventListenerNode();
114+
node.BindInput(EventListenerNode.EventTagInput, eventTagPropertyName);
115+
node.BindInput(EventListenerNode.ListenOnInput, listenOnPropertyName);
116+
return node;
117+
}
98118
}
99119

100120
/// <summary>

Forge.Tests/Helpers/TestCueCustomParametersProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using Gamesmiths.Forge.Core;
44
using Gamesmiths.Forge.Statescript;
5+
using Gamesmiths.Forge.Statescript.Providers;
56

67
namespace Gamesmiths.Forge.Tests.Helpers;
78

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright © Gamesmiths Guild.
2+
3+
namespace Gamesmiths.Forge.Tests.Helpers;
4+
5+
/// <summary>
6+
/// A test payload carried by an event in the event-node tests.
7+
/// </summary>
8+
/// <param name="Amount">An arbitrary value the provider builds from an input and writes back to an output.</param>
9+
internal sealed record TestEventPayload(int Amount);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright © Gamesmiths Guild.
2+
3+
using Gamesmiths.Forge.Statescript;
4+
using Gamesmiths.Forge.Statescript.Providers;
5+
6+
namespace Gamesmiths.Forge.Tests.Helpers;
7+
8+
/// <summary>
9+
/// A bidirectional test event-payload provider: it builds a <see cref="TestEventPayload"/> from a declared
10+
/// <c>Amount</c> input and writes the payload's amount back to a declared <c>Amount</c> output. Used by both the
11+
/// raise-event and event-listener node tests.
12+
/// </summary>
13+
internal sealed class TestEventPayloadProvider : EventPayloadProvider<TestEventPayload>
14+
{
15+
/// <summary>
16+
/// The name of the declared input and output this provider uses.
17+
/// </summary>
18+
public const string AmountKey = "Amount";
19+
20+
/// <inheritdoc/>
21+
public override IReadOnlyList<EventPayloadInput> Inputs => [new EventPayloadInput(AmountKey, typeof(int))];
22+
23+
/// <inheritdoc/>
24+
public override IReadOnlyList<EventPayloadOutput> Outputs => [new EventPayloadOutput(AmountKey, typeof(int))];
25+
26+
/// <inheritdoc/>
27+
public override TestEventPayload CreatePayload(GraphContext graphContext, EventPayloadInputs inputs)
28+
{
29+
return new TestEventPayload(inputs.Get<int>(AmountKey));
30+
}
31+
32+
/// <inheritdoc/>
33+
public override void WriteOutputs(TestEventPayload payload, EventPayloadOutputs outputs)
34+
{
35+
outputs.Set(AmountKey, payload.Amount);
36+
}
37+
}

Forge.Tests/Statescript/Nodes/Action/ApplyEffectNodeTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Gamesmiths.Forge.Statescript.Nodes;
1313
using Gamesmiths.Forge.Statescript.Nodes.Action;
1414
using Gamesmiths.Forge.Statescript.Properties;
15+
using Gamesmiths.Forge.Statescript.Providers;
1516
using Gamesmiths.Forge.Tags;
1617
using Gamesmiths.Forge.Tests.Helpers;
1718

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright © Gamesmiths Guild.
2+
3+
using FluentAssertions;
4+
using Gamesmiths.Forge.Core;
5+
using Gamesmiths.Forge.Cues;
6+
using Gamesmiths.Forge.Events;
7+
using Gamesmiths.Forge.Statescript;
8+
using Gamesmiths.Forge.Statescript.Nodes;
9+
using Gamesmiths.Forge.Statescript.Nodes.Action;
10+
using Gamesmiths.Forge.Statescript.Properties;
11+
using Gamesmiths.Forge.Tags;
12+
using Gamesmiths.Forge.Tests.Helpers;
13+
14+
using static Gamesmiths.Forge.Tests.Helpers.NodeBindings;
15+
16+
namespace Gamesmiths.Forge.Tests.Statescript.Nodes.Action;
17+
18+
public class RaiseEventNodeTests(TagsAndCuesFixture tagsAndCuesFixture) : IClassFixture<TagsAndCuesFixture>
19+
{
20+
private readonly TagsManager _tagsManager = tagsAndCuesFixture.TagsManager;
21+
22+
[Fact]
23+
[Trait("Graph", "RaiseEvent")]
24+
public void Raise_event_node_raises_event_with_resolved_fields()
25+
{
26+
var cuesManager = new CuesManager();
27+
var target = new TestEntity(_tagsManager, cuesManager);
28+
var source = new TestEntity(_tagsManager, cuesManager);
29+
var eventTag = Tag.RequestTag(_tagsManager, "test.cue1");
30+
31+
EventData? captured = null;
32+
target.Events.Subscribe(eventTag, data => captured = data);
33+
34+
var graph = new Graph();
35+
graph.VariableDefinitions.DefineObjectVariable("eventTag", eventTag);
36+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("target", target);
37+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("source", source);
38+
graph.VariableDefinitions.DefineVariable("magnitude", 25f);
39+
40+
RaiseEventNode node = CreateRaiseEventNode("eventTag", "target");
41+
node.BindInput(RaiseEventNode.SourceInput, "source");
42+
node.BindInput(RaiseEventNode.MagnitudeInput, "magnitude");
43+
graph.AddNode(node);
44+
graph.AddConnection(new Connection(
45+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
46+
node.InputPorts[ActionNode.InputPort]));
47+
48+
var processor = new GraphProcessor(graph);
49+
processor.StartGraph();
50+
51+
captured.Should().NotBeNull();
52+
captured!.Value.EventTags.HasTag(eventTag).Should().BeTrue();
53+
captured.Value.Source.Should().BeSameAs(source);
54+
captured.Value.Target.Should().BeSameAs(target);
55+
captured.Value.EventMagnitude.Should().Be(25f);
56+
captured.Value.Payload.Should().BeNull();
57+
}
58+
59+
[Fact]
60+
[Trait("Graph", "RaiseEvent")]
61+
public void Raise_event_node_combines_multiple_tags_into_one_event()
62+
{
63+
var cuesManager = new CuesManager();
64+
var target = new TestEntity(_tagsManager, cuesManager);
65+
var firstTag = Tag.RequestTag(_tagsManager, "test.cue1");
66+
var secondTag = Tag.RequestTag(_tagsManager, "test.cue2");
67+
68+
EventData? captured = null;
69+
target.Events.Subscribe(firstTag, data => captured = data);
70+
71+
var graph = new Graph();
72+
graph.VariableDefinitions.DefineObjectArrayVariable("eventTag", firstTag, secondTag);
73+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("target", target);
74+
75+
RaiseEventNode node = CreateRaiseEventNode("eventTag", "target");
76+
graph.AddNode(node);
77+
graph.AddConnection(new Connection(
78+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
79+
node.InputPorts[ActionNode.InputPort]));
80+
81+
new GraphProcessor(graph).StartGraph();
82+
83+
captured.Should().NotBeNull();
84+
captured!.Value.EventTags.HasTag(firstTag).Should().BeTrue();
85+
captured.Value.EventTags.HasTag(secondTag).Should().BeTrue();
86+
}
87+
88+
[Fact]
89+
[Trait("Graph", "RaiseEvent")]
90+
public void Raise_event_node_raises_on_every_target()
91+
{
92+
var cuesManager = new CuesManager();
93+
var firstTarget = new TestEntity(_tagsManager, cuesManager);
94+
var secondTarget = new TestEntity(_tagsManager, cuesManager);
95+
var eventTag = Tag.RequestTag(_tagsManager, "test.cue1");
96+
97+
bool firstFired = false;
98+
bool secondFired = false;
99+
firstTarget.Events.Subscribe(eventTag, _ => firstFired = true);
100+
secondTarget.Events.Subscribe(eventTag, _ => secondFired = true);
101+
102+
var graph = new Graph();
103+
graph.VariableDefinitions.DefineObjectVariable("eventTag", eventTag);
104+
graph.VariableDefinitions.DefineObjectArrayVariable<IForgeEntity>("target", firstTarget, secondTarget);
105+
106+
RaiseEventNode node = CreateRaiseEventNode("eventTag", "target");
107+
graph.AddNode(node);
108+
graph.AddConnection(new Connection(
109+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
110+
node.InputPorts[ActionNode.InputPort]));
111+
112+
new GraphProcessor(graph).StartGraph();
113+
114+
firstFired.Should().BeTrue();
115+
secondFired.Should().BeTrue();
116+
}
117+
118+
[Fact]
119+
[Trait("Graph", "RaiseEvent")]
120+
public void Raise_event_node_raises_a_typed_event_with_the_provider_payload()
121+
{
122+
var cuesManager = new CuesManager();
123+
var target = new TestEntity(_tagsManager, cuesManager);
124+
var eventTag = Tag.RequestTag(_tagsManager, "test.cue1");
125+
126+
// A typed subscriber receives the event only if the node raises through the typed path (with no boxing).
127+
TestEventPayload? captured = null;
128+
target.Events.Subscribe<TestEventPayload>(eventTag, data => captured = data.Payload);
129+
130+
var graph = new Graph();
131+
graph.VariableDefinitions.DefineObjectVariable("eventTag", eventTag);
132+
graph.VariableDefinitions.DefineObjectVariable<IForgeEntity>("target", target);
133+
graph.VariableDefinitions.DefineObjectProperty(
134+
"payload",
135+
new EventPayloadResolver(
136+
new TestEventPayloadProvider(),
137+
new Dictionary<string, IPropertyResolver>
138+
{
139+
[TestEventPayloadProvider.AmountKey] = new VariantResolver(new Variant128(42), typeof(int)),
140+
}));
141+
142+
RaiseEventNode node = CreateRaiseEventNode("eventTag", "target");
143+
node.BindInput(RaiseEventNode.PayloadInput, "payload");
144+
graph.AddNode(node);
145+
graph.AddConnection(new Connection(
146+
graph.EntryNode.OutputPorts[EntryNode.OutputPort],
147+
node.InputPorts[ActionNode.InputPort]));
148+
149+
new GraphProcessor(graph).StartGraph();
150+
151+
captured.Should().NotBeNull();
152+
captured!.Amount.Should().Be(42);
153+
}
154+
}

Forge.Tests/Statescript/Nodes/State/EffectNodeTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Gamesmiths.Forge.Statescript.Nodes;
1313
using Gamesmiths.Forge.Statescript.Nodes.State;
1414
using Gamesmiths.Forge.Statescript.Properties;
15+
using Gamesmiths.Forge.Statescript.Providers;
1516
using Gamesmiths.Forge.Tags;
1617
using Gamesmiths.Forge.Tests.Helpers;
1718

0 commit comments

Comments
 (0)