Repository navigation
Expand file tree
/
Copy pathAbstractStatusTracker.cs
More file actions
213 lines (182 loc) · 7.9 KB
/
Copy pathAbstractStatusTracker.cs
File metadata and controls
213 lines (182 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// Timberborn Mod: Automation
// Author: igor.zavoychinskiy@gmail.com
// License: Public Domain
using System;
using System.Collections.Generic;
using System.Linq;
using IgorZ.Automation.AutomationSystem;
using IgorZ.Automation.ScriptingEngine.Core;
using IgorZ.Automation.ScriptingEngine.Expressions;
using IgorZ.TimberDev.Utils;
using Timberborn.Persistence;
using Timberborn.WorldPersistence;
namespace IgorZ.Automation.ScriptingEngine.ScriptableComponents.Components;
/// <summary>A base component to track the signals and actions on an automation behavior.</summary>
abstract class AbstractStatusTracker : AbstractDynamicComponent, IPersistentEntity {
sealed record SignalSink {
public ScriptValue LastValue;
public readonly List<ISignalListener> Listeners = [];
readonly List<(SignalOperator signalOperator, ISignalListener listener)> _registrants = [];
public bool UpdateLastValue() {
if (_registrants.Count == 0) {
return false;
}
return UpdateLastValue(_registrants[0].signalOperator.ValueFn());
}
public bool UpdateLastValue(ScriptValue newValue) {
if (_registrants.Count == 0) {
return false;
}
var isChanged = LastValue != newValue;
LastValue = newValue;
return isChanged;
}
public void Register(SignalOperator signalOperator, ISignalListener listener) {
var item = (signalOperator, listener);
if (_registrants.Contains(item)) {
throw new InvalidOperationException($"Signal listener already registered: {item}");
}
_registrants.Add(item);
if (!Listeners.Contains(listener)) {
Listeners.Add(listener);
}
}
public void Unregister(SignalOperator signalOperator, ISignalListener listener) {
var item = (signalOperator, listener);
if (!_registrants.Remove(item)) {
throw new InvalidOperationException($"Signal listener not registered: {item}");
}
Listeners.Clear();
Listeners.AddRange(_registrants.Select(x => x.listener).Distinct());
}
}
#region IPersistentEntity implementation
static readonly PropertyKey<string> SavedSignalsKey = new("SavedSignals");
ComponentKey EntityComponentKey => new(GetType().FullName);
/// <inheritdoc/>
public virtual void Save(IEntitySaver entitySaver) {
var values = _signals
.Where(x => x.Value.Listeners.Count > 0 && x.Value.LastValue.ValueType != ScriptValue.TypeEnum.Unset)
.Select(kvp => SavedSignalValues.SignalValue.FromScriptValue(kvp.Key, kvp.Value.LastValue))
.ToArray();
if (values.Length == 0) {
return;
}
var component = entitySaver.GetComponent(EntityComponentKey);
var state = new SavedSignalValues { SignalValues = values };
component.Set(SavedSignalsKey, StringProtoSerializer.Serialize(state));
}
/// <inheritdoc/>
public virtual void Load(IEntityLoader entityLoader) {
if (!entityLoader.TryGetComponent(EntityComponentKey, out var component)) {
return;
}
var savedSignals = component.GetValueOrDefault(SavedSignalsKey, null);
if (savedSignals == null) {
return;
}
var state = StringProtoSerializer.Deserialize<SavedSignalValues>(savedSignals);
foreach (var signalValue in state.SignalValues) {
_signals.Add(signalValue.SignalName, new SignalSink { LastValue = signalValue.ToScriptValue() });
}
}
#endregion
#region API
/// <summary>True if the component has any actions.</summary>
public bool HasActions => _actions.Count > 0;
/// <summary>Returns the last observed or restored value without evaluating the signal's source.</summary>
public bool TryGetSignalValue(string signalName, out ScriptValue value) {
if (_signals.TryGetValue(signalName, out var signalSink)) {
value = signalSink.LastValue;
return true;
}
value = ScriptValue.InvalidValue;
return false;
}
/// <summary>Seeds a precomputed value before subscription, preserving any restored baseline.</summary>
public void InitializeSignalValue(string signalName, ScriptValue value) {
if (!_signals.ContainsKey(signalName)) {
_signals.Add(signalName, new SignalSink { LastValue = value });
}
}
/// <summary>Adds a signal to the given host. The host will be notified when the signal is scheduled.</summary>
/// <seealso cref="TriggerSignalUpdate(string)"/>
/// <returns><c>true</c> if the first listener for the signal was added.</returns>
public virtual bool AddSignal(SignalOperator signalOperator, ISignalListener host) {
if (!_signals.TryGetValue(signalOperator.SignalName, out var signalSink)) {
signalSink = new SignalSink { LastValue = signalOperator.ValueFn() };
_signals.Add(signalOperator.SignalName, signalSink);
}
var isFirstItem = signalSink.Listeners.Count == 0;
signalSink.Register(signalOperator, host);
return isFirstItem;
}
/// <summary>Removes a signal notification from the given host.</summary>
/// <returns><c>true</c> if there are more listeners for the signal left.</returns>
public virtual bool RemoveSignal(SignalOperator signalOperator, ISignalListener host) {
if (!_signals.TryGetValue(signalOperator.SignalName, out var signalSink)) {
throw new InvalidOperationException($"Signal listener not registered: {(signalOperator, host)}");
}
signalSink.Unregister(signalOperator, host);
if (signalSink.Listeners.Count > 0) {
return true;
}
_signals.Remove(signalOperator.SignalName);
return false;
}
/// <summary>Registers a new action. The action must be unique.</summary>
/// <returns><c>true</c> if the first action was added.</returns>
public virtual bool AddAction(ActionOperator actionOperator) {
if (!_actions.Add(actionOperator)) {
throw new InvalidOperationException($"Action already added: {actionOperator}");
}
return _actions.Count == 1;
}
/// <summary>Removes an action.</summary>
/// <returns><c>true</c> if there are more actions left.</returns>
public virtual bool RemoveAction(ActionOperator actionOperator) {
if (!_actions.Remove(actionOperator)) {
throw new InvalidOperationException($"Action not found: {actionOperator}");
}
return _actions.Count > 0;
}
/// <summary>Notifies all signal listeners if the value has changed.</summary>
/// <remarks>
/// If the same host has registered to the same signal multiple times, it will be notified only once. If the value has
/// not changed, no update will be triggered.
/// </remarks>
public void TriggerSignalUpdate(string signalName) {
if (!_signals.TryGetValue(signalName, out var signalSink) || !signalSink.UpdateLastValue()) {
return;
}
NotifySignalListeners(signalName, signalSink);
}
/// <summary>Notifies all signal listeners if the supplied value has changed.</summary>
/// <remarks>
/// Use this overload for an already computed value. Force notification when recovering from a read error,
/// even if the valid value equals the last one; listeners must clear their error state.
/// </remarks>
public void TriggerSignalUpdate(string signalName, ScriptValue currentValue, bool forceNotify = false) {
if (!_signals.TryGetValue(signalName, out var signalSink)
|| !signalSink.UpdateLastValue(currentValue) && !forceNotify) {
return;
}
NotifySignalListeners(signalName, signalSink);
}
/// <summary>Notifies listeners of a read error without replacing the last valid value.</summary>
public void TriggerSignalError(string signalName) {
if (_signals.TryGetValue(signalName, out var signalSink)) {
NotifySignalListeners(signalName, signalSink);
}
}
static void NotifySignalListeners(string signalName, SignalSink signalSink) {
foreach (var listener in signalSink.Listeners.ToArray()) {
ScriptingService.Instance.NotifySignalListener(signalName, listener);
}
}
#endregion
#region Implementation
readonly Dictionary<string, SignalSink> _signals = new();
readonly HashSet<ActionOperator> _actions = [];
#endregion
}