diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/RGOverlay/RGSegmentEntry.cs b/src/gg.regression.unity.bots/Runtime/Scripts/RGOverlay/RGSegmentEntry.cs
index df88c915..cdf60adf 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/RGOverlay/RGSegmentEntry.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/RGOverlay/RGSegmentEntry.cs
@@ -5,6 +5,7 @@
using RegressionGames.StateRecorder;
using RegressionGames.StateRecorder.BotSegments;
using RegressionGames.StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
@@ -30,7 +31,7 @@ public class RGSegmentEntry : MonoBehaviour
* Indicates whether the Segment or Segment List is overridden by a local file.
*/
public bool isOverride;
-
+
/**
* UI component fields
*/
@@ -48,7 +49,7 @@ public class RGSegmentEntry : MonoBehaviour
[SerializeField]
public GameObject segmentListIndicatorComponent;
-
+
[SerializeField]
public GameObject overrideIndicator;
@@ -83,8 +84,8 @@ public void Start()
resourcePathComponent.gameObject.SetActive(false);
}
}
-
- // set indicator that this Segment is being overriden by a local file, within a build
+
+ // set indicator that this Segment is being overriden by a local file, within a build
overrideIndicator.gameObject.SetActive(isOverride);
// assign values to the UI components
@@ -140,7 +141,8 @@ private void OnPlay()
}
// play the segment
- playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(segmentList.segments, sessionId));
+ playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(new List() {segmentList}, new List(), sessionId));
+
playbackController.Play();
}
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/AndKeyFrameCriteriaEvaluator.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/AndKeyFrameCriteriaEvaluator.cs
index fe5ca4c0..0873f5cb 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/AndKeyFrameCriteriaEvaluator.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/AndKeyFrameCriteriaEvaluator.cs
@@ -6,13 +6,13 @@ namespace RegressionGames.StateRecorder.BotSegments
{
public static class AndKeyFrameCriteriaEvaluator
{
- public static bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, KeyFrameCriteria criteria)
+ public static bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, bool validationsCompleted, KeyFrameCriteria criteria)
{
if (criteria.data is AndKeyFrameCriteriaData { criteriaList: not null } andCriteria)
{
try
{
- return KeyFrameEvaluator.Evaluator.MatchedHelper(firstSegment, segmentNumber, botActionCompleted, BooleanCriteria.And, andCriteria.criteriaList);
+ return KeyFrameEvaluator.Evaluator.MatchedHelper(firstSegment, segmentNumber, botActionCompleted, validationsCompleted, BooleanCriteria.And, andCriteria.criteriaList);
}
catch (Exception)
{
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentZipParser.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentZipParser.cs
index b8551942..827ca1fa 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentZipParser.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentZipParser.cs
@@ -4,9 +4,7 @@
using System.IO.Compression;
using System.Linq;
using Newtonsoft.Json;
-using RegressionGames.StateRecorder.BotSegments.JsonConverters;
using RegressionGames.StateRecorder.BotSegments.Models;
-using StateRecorder.BotSegments;
namespace RegressionGames.StateRecorder.BotSegments
{
@@ -66,9 +64,9 @@ public static IOrderedEnumerable OrderZipJsonEntries(IEnumerabl
return entries;
}
- public static List ParseBotSegmentZipFromSystemPath(string zipFilePath, out string sessionId)
+ public static List ParseBotSegmentZipFromSystemPath(string zipFilePath, out string sessionId)
{
- List results = new();
+ List results = new();
sessionId = null;
@@ -100,17 +98,7 @@ public static List ParseBotSegmentZipFromSystemPath(string zipFilePa
break;
}
- foreach (var botSegment in botSegmentList.segments)
- {
- botSegment.Replay_SegmentNumber = replayNumber++;
-
- if (sessionId == null)
- {
- sessionId = botSegment.sessionId;
- }
-
- results.Add(botSegment);
- }
+ results.Add(botSegmentList);
}
catch (Exception)
{
@@ -132,7 +120,16 @@ public static List ParseBotSegmentZipFromSystemPath(string zipFilePa
sessionId = frameData.sessionId;
}
- results.Add(frameData);
+ results.Add(new BotSegmentList()
+ {
+ segments = new List()
+ {
+ frameData
+ },
+ name = "BotSegmentList for BotSegment - " + frameData.name,
+ description = "BotSegmentList for BotSegment - " + frameData.description,
+ validations = new()
+ });
}
catch (Exception ex)
{
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackContainer.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackContainer.cs
index 8a857ec6..cd852995 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackContainer.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackContainer.cs
@@ -1,56 +1,130 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using RegressionGames.StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models.SegmentValidations;
namespace RegressionGames.StateRecorder.BotSegments
{
public class BotSegmentsPlaybackContainer
{
- private readonly List _botSegments;
+
+ private readonly List _botSegmentLists;
+ private int _botSegmentListIndex = 0;
private int _botSegmentIndex = 0;
+ /**
+ * A top-level set of validations to run for an entire sequence of segments
+ */
+ public readonly List Validations;
+
public readonly string SessionId;
- public BotSegmentsPlaybackContainer(IEnumerable segments, string sessionId = null)
+ public BotSegmentsPlaybackContainer(IEnumerable segmentLists, IEnumerable validations, string sessionId = null)
{
var replayNumber = 1; // 1 to align with the actual numbers in the recording
- _botSegments = new(segments);
- _botSegments.ForEach(a => a.Replay_SegmentNumber = replayNumber++);
+ _botSegmentLists = new(segmentLists);
+ _botSegmentLists.ForEach(a => a.segments.ForEach(b => b.Replay_SegmentNumber = replayNumber++));
+ Validations = new(validations);
this.SessionId = sessionId ?? Guid.NewGuid().ToString("n");
}
public void Reset()
{
// sets indexes back to 0
- _botSegmentIndex = 0;
+ _botSegmentListIndex = 0;
- // reset all the tracking flags
- foreach (var botSegment in _botSegments)
+ // reset all the tracking flags in the segmentlists / segments
+ _botSegmentLists.ForEach(a =>
{
- botSegment.ReplayReset();
+ a.segments.ForEach(b => b.ReplayReset()); // This also handles resetting that segments validations
+ a.validations.ForEach(b => b.ReplayReset());
+ });
+
+ // reset all the top-level sequence validations
+ foreach (var validation in Validations)
+ {
+ validation.ReplayReset();
}
+
}
- public BotSegment DequeueBotSegment()
+ /**
+ * Returns the next bot segment and validations to evaluate and also provides the current segmentList level validations
+ */
+ public (BotSegment, List)? DequeueBotSegment()
{
- if (_botSegmentIndex < _botSegments.Count)
+ while (_botSegmentListIndex < _botSegmentLists.Count)
{
- return _botSegments[_botSegmentIndex++];
- }
+ var segmentList = _botSegmentLists[_botSegmentListIndex];
+ if (_botSegmentIndex < segmentList.segments.Count)
+ {
+ var segment = segmentList.segments[_botSegmentIndex++];
+ var segmentListValidations = segmentList.validations;
+ return (segment, segmentListValidations);
+ }
+ else
+ {
+ // move to the next segmentlist starting on the 0th segment in that list
+ _botSegmentIndex = 0;
+ ++_botSegmentListIndex;
+ }
+ }
+
return null;
}
-
- public BotSegment PeekBotSegment()
+
+
+ /**
+ * Collects all of the results from the top-level validations and individual bot segments
+ */
+ public List GetAllValidationResults()
{
- if (_botSegmentIndex < _botSegments.Count)
+
+ // First add all the top level results
+ var results = Validations.Select(validation => validation.data.GetResults()).ToList();
+
+ // Then add the validations from bot segment lists and individual bot segment results
+ foreach (var botSegmentList in _botSegmentLists)
{
- // do not update index
- return _botSegments[_botSegmentIndex];
+ results.AddRange(botSegmentList.validations.Select(v => v.data.GetResults()));
+
+ foreach (var botSegment in botSegmentList.segments)
+ {
+ results.AddRange(botSegment.validations.Select(v => v.data.GetResults()));
+ }
}
- return null;
+ return results;
+ }
+
+ /**
+ *
+ * This will request to stop all validations in the container, including sequence validations, bot
+ * segment list validations, and individual bot segment validations.
+ *
+ */
+ public void StopAllValidations(int segmentNumber)
+ {
+ // First stop the sequence validations
+ foreach (var validation in Validations)
+ {
+ validation.StopValidation(segmentNumber);
+ }
+
+ // Then stop the segment list validations and bot segment validations
+ foreach (var botSegmentList in _botSegmentLists)
+ {
+ botSegmentList.validations.ForEach(v => v.StopValidation(segmentNumber));
+ foreach (var botSegment in botSegmentList.segments)
+ {
+ botSegment.validations.ForEach(v => v.StopValidation(segmentNumber));
+ }
+ }
}
+
}
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackController.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackController.cs
index 356faa12..0030723e 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackController.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/BotSegmentsPlaybackController.cs
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using Newtonsoft.Json;
using RegressionGames.StateRecorder.BotSegments.Models;
using RegressionGames.StateRecorder.BotSegments.Models.BotActions.KeyMoments;
using RegressionGames.StateRecorder.Models;
using StateRecorder.BotSegments;
using StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models.SegmentValidations;
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -45,7 +47,12 @@ public class BotSegmentsPlaybackController : MonoBehaviour
// We track this as a list instead of a single entry to allow the UI and game object conditions to evaluate separately
// We still only unlock the input sequences for a key frame once both UI and game object conditions are met
// This is done this way to allow situations like when loading screens (UI) are changing while game objects are loading in the background and the process is not consistent/deterministic between the 2
- private readonly List _nextBotSegments = new();
+ private readonly List<(BotSegment, List)> _nextSegmentsAndValidations = new();
+
+ // We can only stop bot segment list validations once we are sure that we have moved on to the next
+ // bot segment list. This variable holds the last bot segment list validations so we can compare it to the
+ // next if needed and end them if the validations change.
+ private List _previousBotSegmentListValidations = null;
// helps indicate if we made it through the full replay successfully
private bool? _replaySuccessful;
@@ -96,21 +103,56 @@ private void ProcessBotSegments()
// track if we have a new segment to evaluate... so long as we do, keep looping here before releasing from this Update call
// thus we process each new segment as soon as possible and don't have any artificial one frame delays before processing
var nextBotSegmentIndex = 0;
- while (nextBotSegmentIndex < _nextBotSegments.Count)
+ while (nextBotSegmentIndex < _nextSegmentsAndValidations.Count)
{
- var nextBotSegment = _nextBotSegments[nextBotSegmentIndex];
+ var (nextBotSegment, validations) = _nextSegmentsAndValidations[nextBotSegmentIndex];
_botSegmentPlaybackStatusManager.UpdateActiveSegment(nextBotSegment);
-
+
+ // Before moving on, check to see if these bot segment list validations are new. If so, we need to stop
+ // any previous validations. Note that the check by reference here is intentional because we want
+ // to see if is actually the same list.
+ if (_previousBotSegmentListValidations != null && validations != _previousBotSegmentListValidations)
+ {
+ _previousBotSegmentListValidations.ForEach(v => v.StopValidation(nextBotSegment.Replay_SegmentNumber - 1));
+ _previousBotSegmentListValidations = validations;
+ }
+
+ // Check that the individual bot segment validations, segment list validations, and sequence validations
+ // are ready to go. Only continue after that.
+ if (!EnsureValidationsAreReady(nextBotSegment.validations, nextBotSegment.Replay_SegmentNumber)
+ || !EnsureValidationsAreReady(validations, nextBotSegment.Replay_SegmentNumber)
+ || !EnsureValidationsAreReady(_dataPlaybackContainer.Validations, nextBotSegment.Replay_SegmentNumber))
+ {
+ return;
+ }
+
// if we're working on the first entry in the list is the only time we do actions
if (nextBotSegmentIndex == 0)
{
+ // At this point, we are ready to run the actions!
ProcessBotSegmentAction(nextBotSegment, transformStatuses, entityStatuses);
}
+
+ // Run (and potentially end) the validations for this segment
+ nextBotSegment.ProcessValidation();
+
+ // Also run the validations for the botsegmentList if they exist
+ foreach (var validation in validations)
+ {
+ validation.ProcessValidation(nextBotSegment.Replay_SegmentNumber);
+ }
+
+ // Finally, run the validations for the sequence if they exist
+ foreach (var validation in _dataPlaybackContainer.Validations)
+ {
+ validation.ProcessValidation(nextBotSegment.Replay_SegmentNumber);
+ }
var matched = nextBotSegment.Replay_Matched || nextBotSegment.endCriteria == null || nextBotSegment.endCriteria.Count == 0 || KeyFrameEvaluator.Evaluator.Matched(
nextBotSegmentIndex == 0,
nextBotSegment.Replay_SegmentNumber,
nextBotSegment.Replay_ActionCompleted,
+ nextBotSegment.Replay_ValidationsCompleted,
nextBotSegment.endCriteria
);
@@ -149,7 +191,13 @@ private void ProcessBotSegments()
_botSegmentPlaybackStatusManager.UpdateActiveSegmentAndErrorStatus(nextBotSegment, null);
RGDebug.LogInfo($"({nextBotSegment.Replay_SegmentNumber}) - Bot Segment - DONE - Criteria Matched && Action Completed - {nextBotSegment.name ?? nextBotSegment.resourcePath} - {nextBotSegment.description}");
//Process the inputs from that bot segment if necessary
- _nextBotSegments.RemoveAt(nextBotSegmentIndex);
+ if (!nextBotSegment.Replay_ValidationsCompleted && !nextBotSegment.HasValidationEndCriteria)
+ {
+ // tell the validation that our segment completed and it should stop, but only if they don't have an endCriteria that explicitly asks to wait for validations
+ RGDebug.LogInfo($"({nextBotSegment.Replay_SegmentNumber}) - Bot Segment - DONE - Actions and Criteria have been met, but validations have not completed. Forcing them to complete. - {nextBotSegment.name ?? nextBotSegment.resourcePath} - {nextBotSegment.description}");
+ nextBotSegment.StopValidations();
+ }
+ _nextSegmentsAndValidations.RemoveAt(nextBotSegmentIndex);
// don't update the index since we shortened the list
}
else
@@ -172,22 +220,22 @@ private void ProcessBotSegments()
}
// we possibly removed from the list above.. need this check
- if (_nextBotSegments.Count > 0)
+ if (_nextSegmentsAndValidations.Count > 0)
{
// see if the last entry has transient matches.. if so.. dequeue another up to a limit of 2 total segments being evaluated... we may need to come back to this.. but without this look ahead, loading screens like bossroom fail due to background loading
// but if you go too far.. you can match segments in the replay that you won't see for another 50 segments when you go back to the menu again.. which is obviously wrong
- var lastSegment = _nextBotSegments[^1];
+ var (lastSegment, lastValidations) = _nextSegmentsAndValidations[^1];
if (lastSegment.Replay_TransientMatched)
{
- if (_nextBotSegments.Count < 2)
+ if (_nextSegmentsAndValidations.Count < 2)
{
- var next = _dataPlaybackContainer.DequeueBotSegment();
+ var (next, nextValidations) = _dataPlaybackContainer.DequeueBotSegment() ?? (null, null);
if (next != null)
{
_explorationStartTimer = now;
_botSegmentPlaybackStatusManager.UpdateActiveSegmentAndErrorStatus(nextBotSegment, null);
RGDebug.LogInfo($"({next.Replay_SegmentNumber}) - Bot Segment - Added {(next.HasTransientCriteria ? "" : "Non-")}Transient BotSegment for Evaluation after Transient BotSegment - {next.name ?? next.resourcePath} - {next.description}");
- _nextBotSegments.Add(next);
+ _nextSegmentsAndValidations.Add((next, nextValidations));
//next while loop iteration will get this guy
}
}
@@ -196,13 +244,13 @@ private void ProcessBotSegments()
else
{
// segment list empty.. dequeue another
- var next = _dataPlaybackContainer.DequeueBotSegment();
+ var (next, nextValidations) = _dataPlaybackContainer.DequeueBotSegment() ?? (null, null);
if (next != null)
{
_explorationStartTimer = now;
_botSegmentPlaybackStatusManager.UpdateActiveSegmentAndErrorStatus(nextBotSegment, null);
RGDebug.LogInfo($"({next.Replay_SegmentNumber}) - Bot Segment - Added {(next.HasTransientCriteria ? "" : "Non-")}Transient BotSegment for Evaluation - {next.name ?? next.resourcePath} - {next.description}");
- _nextBotSegments.Add(next);
+ _nextSegmentsAndValidations.Add((next, nextValidations));
//next while loop iteration will get this guy
}
}
@@ -350,7 +398,7 @@ public void LateUpdate()
ProcessBotSegments();
}
- if (_nextBotSegments.Count == 0)
+ if (_nextSegmentsAndValidations.Count == 0)
{
MouseEventSender.MoveMouseOffScreen();
@@ -517,9 +565,24 @@ public void Pause()
{
_playState = PlayState.Paused;
- foreach (var nextBotSegment in _nextBotSegments)
+ var currentSegmentNumber = _nextSegmentsAndValidations.Count > 0 ? _nextSegmentsAndValidations[0].Item1.Replay_SegmentNumber : -1;
+
+ // First, pause all the validations that are part of a segment list or bot segments themselves
+ foreach (var (nextBotSegment, validations) in _nextSegmentsAndValidations)
{
nextBotSegment.PauseAction();
+ nextBotSegment.PauseValidations();
+
+ foreach (var v in validations)
+ {
+ v.PauseValidation(currentSegmentNumber);
+ }
+ }
+
+ // Also pause the top-level sequence validations
+ foreach (var validation in _dataPlaybackContainer.Validations)
+ {
+ validation.PauseValidation(currentSegmentNumber);
}
}
}
@@ -541,9 +604,23 @@ public void Play()
// resume
_playState = PlayState.Playing;
- foreach (var nextBotSegment in _nextBotSegments)
+ var currentSegmentNumber = _nextSegmentsAndValidations.Count > 0 ? _nextSegmentsAndValidations[0].Item1.Replay_SegmentNumber : -1;
+
+ // Unpause all the validations that are part of a segment list or bot segments themselves
+ foreach (var (nextBotSegment, validations) in _nextSegmentsAndValidations)
{
nextBotSegment.UnPauseAction();
+ nextBotSegment.UnPauseValidations();
+ foreach (var v in validations)
+ {
+ v.UnPauseValidation(currentSegmentNumber);
+ }
+ }
+
+ // Also unpause the top-level sequence validations
+ foreach (var validation in _dataPlaybackContainer.Validations)
+ {
+ validation.UnPauseValidation(currentSegmentNumber);
}
}
}
@@ -566,22 +643,35 @@ public void Loop(Action loopCountCallback)
public void UnloadSegmentsAndReset()
{
- if (_nextBotSegments.Count > 0)
+ if (_nextSegmentsAndValidations.Count > 0)
{
- foreach (var nextBotSegment in _nextBotSegments)
+ // Reset and stop both the validations in the bot segment lists and the bot segments themselves
+ foreach (var (nextBotSegment, validations) in _nextSegmentsAndValidations)
{
// stop any action
nextBotSegment.AbortAction();
+ nextBotSegment.StopValidations();
+
+ foreach (var v in validations)
+ {
+ v.StopValidation(nextBotSegment.Replay_SegmentNumber);
+ }
}
}
- _nextBotSegments.Clear();
+ var currentSegmentNumber = _nextSegmentsAndValidations.Count > 0 ? _nextSegmentsAndValidations[0].Item1.Replay_SegmentNumber : -1;
+
+ // Wrap up all other validations. This does repeat some of the stopping from above, but is safest
+ _dataPlaybackContainer?.StopAllValidations(currentSegmentNumber);
+
+ _nextSegmentsAndValidations.Clear();
_playState = PlayState.NotLoaded;
BotSequence.ActiveBotSequence = null;
_loopCount = -1;
_replaySuccessful = null;
WaitingForKeyFrameConditions = null;
+ _screenRecorder.validationResults = _dataPlaybackContainer?.GetAllValidationResults() ?? new List();
_screenRecorder.StopRecording();
#if ENABLE_LEGACY_INPUT_MANAGER
RGLegacyInputWrapper.StopSimulation();
@@ -605,13 +695,22 @@ public void UnloadSegmentsAndReset()
public void Stop()
{
- _nextBotSegments.Clear();
+
+ var currentSegmentNumber = _nextSegmentsAndValidations.Count > 0 ? _nextSegmentsAndValidations[0].Item1.Replay_SegmentNumber : -1;
+
+ // Make sure to stop any running validations
+ _dataPlaybackContainer?.StopAllValidations(currentSegmentNumber);
+
+ _nextSegmentsAndValidations.Clear();
_playState = PlayState.Stopped;
_loopCount = -1;
_replaySuccessful = null;
WaitingForKeyFrameConditions = null;
+ _previousBotSegmentListValidations = null;
_botSegmentPlaybackStatusManager.Reset();
+ // Note that this retrieves all the validation results from the bot segments, bot segment lists, and sequences
+ _screenRecorder.validationResults = _dataPlaybackContainer?.GetAllValidationResults() ?? new List();
_screenRecorder.StopRecording();
#if ENABLE_LEGACY_INPUT_MANAGER
RGLegacyInputWrapper.StopSimulation();
@@ -636,11 +735,12 @@ public void Stop()
public void PrepareForNextLoop()
{
- _nextBotSegments.Clear();
+ _nextSegmentsAndValidations.Clear();
_playState = PlayState.Starting;
// don't change _loopCount
_replaySuccessful = null;
WaitingForKeyFrameConditions = null;
+ _previousBotSegmentListValidations = null;
_botSegmentPlaybackStatusManager.Reset();
#if ENABLE_LEGACY_INPUT_MANAGER
@@ -687,7 +787,11 @@ public void Update()
#endif
RGUtils.ConfigureInputSettings();
_playState = PlayState.Playing;
- _nextBotSegments.Add(_dataPlaybackContainer.DequeueBotSegment());
+ var nextSegmentAndValidations = _dataPlaybackContainer.DequeueBotSegment();
+ if (nextSegmentAndValidations != null)
+ {
+ _nextSegmentsAndValidations.Add(nextSegmentAndValidations.Value);
+ }
// if starting to play, or on loop 1.. start recording
if (_loopCount < 2)
{
@@ -711,12 +815,20 @@ public void Update()
// ReSharper disable once InconsistentNaming
private const int EXPLORATION_START_INTERVAL = 3; // seconds before we log or start exploring other bot actions
+ /**
+ * Returns true if the validations are ready to be executed, and false otherwise.
+ */
+ private bool EnsureValidationsAreReady(List validations, int segmentNumber)
+ {
+ return validations.All(v => v.data.AttemptPrepareValidation(segmentNumber));
+ }
+
public void OnGUI()
{
if (_playState == PlayState.Playing || _playState == PlayState.Paused)
{
// render any GUI things for the first segment action
- if (_nextBotSegments.Count > 0)
+ if (_nextSegmentsAndValidations.Count > 0)
{
var transformStatuses = new Dictionary();
var entityStatuses = new Dictionary();
@@ -733,7 +845,7 @@ public void OnGUI()
entityStatuses = objectFinder.GetObjectStatusForCurrentFrame().Item2;
}
}
- _nextBotSegments[0].OnGUI(transformStatuses, entityStatuses);
+ _nextSegmentsAndValidations[0].Item1.OnGUI(transformStatuses, entityStatuses);
}
}
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentJsonConverter.cs
index 359da776..0493e954 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentJsonConverter.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentJsonConverter.cs
@@ -4,6 +4,7 @@
using Newtonsoft.Json.Linq;
using RegressionGames.StateRecorder.BotSegments.Models;
using RegressionGames.StateRecorder.BotSegments.Models.BotCriteria;
+using StateRecorder.BotSegments.Models;
namespace RegressionGames.StateRecorder.BotSegments.JsonConverters
{
@@ -48,6 +49,18 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
botSegment.endCriteria = new List();
}
+ if (jObject.ContainsKey("validations"))
+ {
+ botSegment.validations =
+ new List(jObject.GetValue("validations")
+ .ToObject(serializer));
+ }
+ else
+ {
+ // default value of validation is an empty list if not present
+ botSegment.validations = new List();
+ }
+
return botSegment;
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentListJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentListJsonConverter.cs
index 1a337eb4..5c056ef9 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentListJsonConverter.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSegmentListJsonConverter.cs
@@ -3,6 +3,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RegressionGames.StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models;
namespace RegressionGames.StateRecorder.BotSegments.JsonConverters
{
@@ -23,6 +24,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
// allow description to be null/undefined in the file
list.description = jObject.GetValue("description")?.ToObject(serializer) ?? "";
list.segments = jObject.GetValue("segments").ToObject>(serializer);
+ list.validations = jObject.GetValue("validations")?.ToObject>(serializer) ?? new List();
return list;
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSequenceJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSequenceJsonConverter.cs
index af1ebd5d..0f552461 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSequenceJsonConverter.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/BotSequenceJsonConverter.cs
@@ -3,6 +3,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RegressionGames.StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models;
namespace RegressionGames.StateRecorder.BotSegments.JsonConverters
{
@@ -21,6 +22,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
sequence.name = jObject.GetValue("name").ToObject(serializer);
sequence.description = jObject.GetValue("description")?.ToObject(serializer) ?? "";
sequence.segments = jObject.GetValue("segments").ToObject>(serializer);
+ sequence.validations = jObject.GetValue("validations")?.ToObject>(serializer) ?? new List();
return sequence;
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/KeyFrameCriteriaJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/KeyFrameCriteriaJsonConverter.cs
index b0a152e0..e1ffb132 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/KeyFrameCriteriaJsonConverter.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/KeyFrameCriteriaJsonConverter.cs
@@ -43,6 +43,9 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
case KeyFrameCriteriaType.ActionComplete:
data = jObject["data"].ToObject(serializer);
break;
+ case KeyFrameCriteriaType.ValidationsComplete:
+ data = jObject["data"].ToObject(serializer);
+ break;
case KeyFrameCriteriaType.CVObjectDetection:
data = jObject["data"].ToObject(serializer);
break;
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs
new file mode 100644
index 00000000..6763143d
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs
@@ -0,0 +1,39 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+
+namespace RegressionGames.StateRecorder.BotSegments.JsonConverters
+{
+ public class ScriptSegmentValidationDataJsonConverter: JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return typeof(ScriptSegmentValidationData).IsAssignableFrom(objectType);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JObject jObject = JObject.Load(reader);
+ // ReSharper disable once UseObjectOrCollectionInitializer - easier to debug lines without this
+ ScriptSegmentValidationData data = new();
+ data.classFullName = jObject.GetValue("classFullName").ToObject(serializer);
+ if (jObject.ContainsKey("apiVersion"))
+ {
+ data.apiVersion = jObject.GetValue("apiVersion").ToObject(serializer);
+ }
+ if (jObject.ContainsKey("timeout"))
+ {
+ data.timeout = jObject.GetValue("timeout").ToObject(serializer);
+ }
+ return data;
+ }
+
+ public override bool CanWrite => false;
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs.meta
new file mode 100644
index 00000000..9deeea17
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/ScriptSegmentValidationDataJsonConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 6b9d9da857fd44a08e2fa2648ba09e5d
+timeCreated: 1734234105
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs
new file mode 100644
index 00000000..bdb61a25
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs
@@ -0,0 +1,48 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RegressionGames.StateRecorder.BotSegments.Models;
+using RegressionGames.StateRecorder.BotSegments.Models.BotCriteria;
+using StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+
+namespace RegressionGames.StateRecorder.BotSegments.JsonConverters
+{
+ public class SegmentValidationJsonConverter: JsonConverter
+ {
+ public override bool CanConvert(Type objectType)
+ {
+ return typeof(SegmentValidation).IsAssignableFrom(objectType);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ JObject jObject = JObject.Load(reader);
+ SegmentValidation validation = new();
+ validation.type = jObject["type"].ToObject(serializer);
+ if (jObject.ContainsKey("apiVersion"))
+ {
+ validation.apiVersion = jObject["apiVersion"].ToObject(serializer);
+ }
+ IRGSegmentValidationData data = null;
+ switch (validation.type)
+ {
+ case SegmentValidationType.Script:
+ data = jObject["data"].ToObject(serializer);
+ break;
+ default:
+ throw new JsonSerializationException($"Unsupported SegmentValidation type: '{validation.type}'");
+ }
+
+ validation.data = data;
+ return validation;
+ }
+
+ public override bool CanWrite => false;
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs.meta
new file mode 100644
index 00000000..9fe1c79f
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/JsonConverters/SegmentValidationJsonConverter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 3e2858e00eaa41869cf5650812b5b89a
+timeCreated: 1734234536
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/KeyFrameEvaluator.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/KeyFrameEvaluator.cs
index aebaddc8..3a28b25c 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/KeyFrameEvaluator.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/KeyFrameEvaluator.cs
@@ -81,10 +81,10 @@ public void PersistPriorFrameStatus()
/**
* Publicly callable.. caches the statuses of the last passed key frame for computing delta counts from
*/
- public bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, List criteriaList)
+ public bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, bool validationsCompleted, List criteriaList)
{
_newUnmatchedCriteria.Clear();
- bool matched = MatchedHelper(firstSegment, segmentNumber, botActionCompleted, BooleanCriteria.And, criteriaList);
+ bool matched = MatchedHelper(firstSegment, segmentNumber, botActionCompleted, validationsCompleted, BooleanCriteria.And, criteriaList);
if (matched)
{
CVTextCriteriaEvaluator.Cleanup(segmentNumber);
@@ -106,7 +106,7 @@ public bool Matched(bool firstSegment, int segmentNumber, bool botActionComplete
/**
* Only to be called internally by KeyFrameEvaluator. firstSegment represents if this is the first segment in the current pass's list of segments to evaluate
*/
- internal bool MatchedHelper(bool firstSegment, int segmentNumber, bool botActionCompleted, BooleanCriteria andOr, List criteriaList)
+ internal bool MatchedHelper(bool firstSegment, int segmentNumber, bool botActionCompleted, bool validationsCompleted, BooleanCriteria andOr, List criteriaList)
{
var objectFinders = Object.FindObjectsByType(FindObjectsSortMode.None);
var currentFrameCount = Time.frameCount;
@@ -194,6 +194,13 @@ internal bool MatchedHelper(bool firstSegment, int segmentNumber, bool botAction
return false;
}
break;
+ case KeyFrameCriteriaType.ValidationsComplete:
+ if (!validationsCompleted)
+ {
+ _newUnmatchedCriteria.Add("Waiting for validations to complete...");
+ return false;
+ }
+ break;
}
}
@@ -368,7 +375,7 @@ internal bool MatchedHelper(bool firstSegment, int segmentNumber, bool botAction
for (var j = 0; j < orCount; j++)
{
var orEntry = orsToMatch[j];
- var m = OrKeyFrameCriteriaEvaluator.Matched(firstSegment, segmentNumber, botActionCompleted, orEntry);
+ var m = OrKeyFrameCriteriaEvaluator.Matched(firstSegment, segmentNumber, botActionCompleted, validationsCompleted, orEntry);
if (m)
{
if (andOr == BooleanCriteria.Or)
@@ -394,7 +401,7 @@ internal bool MatchedHelper(bool firstSegment, int segmentNumber, bool botAction
for (var j = 0; j < andCount; j++)
{
var andEntry = andsToMatch[j];
- var m = AndKeyFrameCriteriaEvaluator.Matched(firstSegment, segmentNumber, botActionCompleted, andEntry);
+ var m = AndKeyFrameCriteriaEvaluator.Matched(firstSegment, segmentNumber, botActionCompleted, validationsCompleted, andEntry);
if (m)
{
if (andOr == BooleanCriteria.Or)
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs
new file mode 100644
index 00000000..166f7bb6
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text;
+using RegressionGames.StateRecorder.JsonConverters;
+
+namespace RegressionGames.StateRecorder.BotSegments.Models.BotCriteria
+{
+ [Serializable]
+ public class ValidationsCompleteKeyFrameCriteriaData : IKeyFrameCriteriaData
+ {
+ public int apiVersion = SdkApiVersion.VERSION_11;
+
+ public void WriteToStringBuilder(StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("{\"apiVersion\":");
+ IntJsonConverter.WriteToStringBuilder(stringBuilder, apiVersion);
+ stringBuilder.Append("}");
+ }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder(100);
+ WriteToStringBuilder(sb);
+ return sb.ToString();
+ }
+
+ public int EffectiveApiVersion()
+ {
+ return apiVersion;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs.meta
new file mode 100644
index 00000000..ad095e1c
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotCriteria/ValidationsCompleteKeyFrameCriteriaData.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ebf7600390894ba9bacd2a5d5c11f4a8
+timeCreated: 1734378457
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs
index 1a3949fc..18bc8c36 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegment.cs
@@ -7,6 +7,7 @@
using RegressionGames.StateRecorder.BotSegments.Models.BotCriteria;
using RegressionGames.StateRecorder.JsonConverters;
using RegressionGames.StateRecorder.Models;
+using StateRecorder.BotSegments.Models;
using UnityEngine.Serialization;
@@ -25,7 +26,7 @@ public class BotSegment : IStringBuilderWriteable, IKeyMomentStringBuilderWritea
// versioning support for bot segments in the SDK, the is for this top level schema only
// update this if this top level schema changes
- public int apiVersion = SdkApiVersion.VERSION_24;
+ public int apiVersion = SdkApiVersion.VERSION_29;
// the highest apiVersion component included in this json.. used for compatibility checks on replay load
public int EffectiveApiVersion => Math.Max(Math.Max(apiVersion, botAction?.EffectiveApiVersion ?? SdkApiVersion.CURRENT_VERSION), endCriteria.DefaultIfEmpty().Max(a=>a?.EffectiveApiVersion ?? SdkApiVersion.CURRENT_VERSION));
@@ -53,6 +54,8 @@ public class BotSegment : IStringBuilderWriteable, IKeyMomentStringBuilderWritea
public BotAction botAction;
+ public List validations = new();
+
// Replay only - if this was fully matched (still not done until actions also completed)
[NonSerialized]
public bool Replay_Matched;
@@ -67,13 +70,13 @@ public class BotSegment : IStringBuilderWriteable, IKeyMomentStringBuilderWritea
// Replay only - tracks if we have completed the action for this bot segment
// returns true if botAction.IsCompleted || botAction.IsCompleted==null && Replay_Matched
public bool Replay_ActionCompleted => botAction == null || (botAction.IsCompleted ?? Replay_Matched);
+
+ // Replay only - true if we have completed the validations for this bot segment
+ public bool Replay_ValidationsCompleted => validations.Count == 0 || validations.All(v => v.HasSetAllResults());
public void OnGUI(Dictionary currentTransforms, Dictionary currentEntities)
{
- if (botAction != null)
- {
- botAction.OnGUI(currentTransforms, currentEntities);
- }
+ botAction?.OnGUI(currentTransforms, currentEntities);
}
// Replay only - called at least once per frame
@@ -97,10 +100,7 @@ public bool ProcessAction(Dictionary currentTransforms, Dict
*/
public void AbortAction()
{
- if (botAction != null)
- {
- botAction.AbortAction(Replay_SegmentNumber);
- }
+ botAction?.AbortAction(Replay_SegmentNumber);
}
/**
@@ -137,6 +137,40 @@ public void PauseAction()
}
}
+ public void ProcessValidation()
+ {
+ // Go through each validation and process them. If they have not been started yet, this will also
+ // start them.
+ foreach (var validation in validations)
+ {
+ validation.ProcessValidation(Replay_SegmentNumber);
+ }
+ }
+
+ public void PauseValidations()
+ {
+ foreach (var validation in validations)
+ {
+ validation.PauseValidation(Replay_SegmentNumber);
+ }
+ }
+
+ public void UnPauseValidations()
+ {
+ foreach (var validation in validations)
+ {
+ validation.UnPauseValidation(Replay_SegmentNumber);
+ }
+ }
+
+ public void StopValidations()
+ {
+ foreach (var validation in validations)
+ {
+ validation.StopValidation(Replay_SegmentNumber);
+ }
+ }
+
// Replay only
public void ReplayReset()
{
@@ -150,6 +184,12 @@ public void ReplayReset()
{
botAction.ReplayReset();
}
+
+ var validationsLength = validations.Count;
+ for (var i = 0; i < validationsLength; i++)
+ {
+ validations[i].ReplayReset();
+ }
Replay_Matched = false;
}
@@ -224,7 +264,10 @@ private bool HasTransientCriteriaHelper(List criteriaList)
}
return false;
}
-
+
+ public bool HasValidationEndCriteria =>
+ endCriteria.Exists(ec => ec.type == KeyFrameCriteriaType.ValidationsComplete);
+
public string ToKeyMomentJsonString()
{
_stringBuilder.Value.Clear();
@@ -262,7 +305,18 @@ public void WriteKeyMomentToStringBuilder(StringBuilder stringBuilder)
{
stringBuilder.Append("null");
}
- stringBuilder.Append("}");
+ stringBuilder.Append(",\n\"validations\":[\n");
+ var validationsLength = validations.Count;
+ for (var i = 0; i < validationsLength; i++)
+ {
+ var validation = validations[i];
+ validation.WriteToStringBuilder(stringBuilder);
+ if (i + 1 < validationsLength)
+ {
+ stringBuilder.Append(",\n");
+ }
+ }
+ stringBuilder.Append("\n]\n}");
}
public string ToJsonString()
@@ -302,7 +356,18 @@ public void WriteToStringBuilder(StringBuilder stringBuilder)
{
stringBuilder.Append("null");
}
- stringBuilder.Append("}");
+ stringBuilder.Append(",\n\"validations\":[\n");
+ var validationsLength = validations.Count;
+ for (var i = 0; i < validationsLength; i++)
+ {
+ var validation = validations[i];
+ validation.WriteToStringBuilder(stringBuilder);
+ if (i + 1 < validationsLength)
+ {
+ stringBuilder.Append(",\n");
+ }
+ }
+ stringBuilder.Append("\n]\n}");
}
/**
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegmentList.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegmentList.cs
index 9a4d070a..410e69f4 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegmentList.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSegmentList.cs
@@ -4,6 +4,7 @@
using System.Text;
using System.Threading;
using RegressionGames.StateRecorder.JsonConverters;
+using StateRecorder.BotSegments.Models;
namespace RegressionGames.StateRecorder.BotSegments.Models
{
@@ -37,6 +38,11 @@ public class BotSegmentList : IStringBuilderWriteable, IKeyMomentStringBuilderWr
public string description;
public List segments = new();
+ /**
+ * A set of top-level validations to run on this list of segments
+ */
+ public List validations = new();
+
internal BotSegmentList()
{
// used by json converter
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSequence.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSequence.cs
index 5fd8c0fb..824f7bda 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSequence.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/BotSequence.cs
@@ -6,6 +6,7 @@
using System.Threading;
using Newtonsoft.Json;
using RegressionGames.StateRecorder.JsonConverters;
+using StateRecorder.BotSegments.Models;
// ReSharper disable once RedundantUsingDirective - used in #if block
using UnityEngine;
@@ -25,6 +26,11 @@ public class BotSequence
public List segments = new();
+ /**
+ * A set of top-level validations to run on a sequence of segments
+ */
+ public List validations = new();
+
/**
* Define the name of this sequence that will be seen in user interfaces and runtime summaries. This SHOULD NOT be null.
*/
@@ -491,7 +497,7 @@ public void Play()
}
sessionId ??= Guid.NewGuid().ToString();
- playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(_segmentsToProcess.SelectMany(a => a.segments), sessionId));
+ playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(_segmentsToProcess, validations, sessionId));
ActiveBotSequence = this; // SetDataContainer clears this, so set it here before starting
playbackController.Play();
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta
new file mode 100644
index 00000000..07751df3
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/CVService.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5c74a766426ce23499df244c81e55ee7
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs
new file mode 100644
index 00000000..2a6a832c
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs
@@ -0,0 +1,60 @@
+using System.Text;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+
+namespace StateRecorder.BotSegments.Models
+{
+ public interface IRGSegmentValidationData
+ {
+
+ /**
+ * Attempts to conduct any preparation needed for validation. Returns true if the validations
+ * are ready to be run, and false otherwise. Implementors should make sure that this method
+ * can handle being called ever update once, and ignore those requests if the validation is preparing itself
+ * or is already prepared.
+ */
+ public bool AttemptPrepareValidation(int segmentNumber);
+
+ /**
+ * Called at least once per frame
+ * The validation may choose to evaluate this turn or skip validation
+ */
+ public void ProcessValidation(int segmentNumber);
+
+ /**
+ * Handles pausing the validation from the UI
+ */
+ public void PauseValidation(int segmentNumber);
+
+ /**
+ * Handles unpausing the validation from the UI
+ */
+ public void UnPauseValidation(int segmentNumber);
+
+ /**
+ * Indicates that the segment has ended the validation phase
+ */
+ public void StopValidation(int segmentNumber);
+
+ /**
+ * Resets any results contained within this validation
+ */
+ public void ResetResults();
+
+ /**
+ * Returns true if there are no "UNKNOWN" validations.
+ */
+ public bool HasSetAllResults();
+
+ /**
+ * Returns all results for this particular validation. In some
+ * cases, this can be a set of results rather than just a single
+ * result.
+ */
+ public SegmentValidationResultSetContainer GetResults();
+
+ public void WriteToStringBuilder(StringBuilder stringBuilder);
+
+ public int EffectiveApiVersion();
+
+ }
+}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs.meta
new file mode 100644
index 00000000..a559624c
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/IRGSegmentValidationData.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 08eb1ad84a1a4464b4037ac0c46b2120
+timeCreated: 1734114973
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/KeyFrameCriteriaType.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/KeyFrameCriteriaType.cs
index 872af53d..4d9bc1d9 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/KeyFrameCriteriaType.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/KeyFrameCriteriaType.cs
@@ -11,6 +11,7 @@ public enum KeyFrameCriteriaType
CVImage,
ActionComplete,
CVObjectDetection,
+ ValidationsComplete
//FUTURE
//Path,
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs
new file mode 100644
index 00000000..401c87a3
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Text;
+using RegressionGames.StateRecorder;
+using RegressionGames.StateRecorder.JsonConverters;
+
+namespace StateRecorder.BotSegments.Models
+{
+ [Serializable]
+ public class SegmentValidation
+ {
+
+ // api version for this top level schema, update if we add/remove/change fields here
+ public int apiVersion = SdkApiVersion.VERSION_29;
+
+ public SegmentValidationType type;
+ public IRGSegmentValidationData data;
+
+ public int EffectiveApiVersion => Math.Max(apiVersion, data?.EffectiveApiVersion() ?? SdkApiVersion.CURRENT_VERSION);
+
+ private bool _validationIsReady;
+
+ // called once per frame
+ // returns true if the validation is running, or false if it is still loading up. This is used to
+ // be able to wait for the validation to be ready before running the segments.
+ public bool ProcessValidation(int segmentNumber)
+ {
+ if (!_validationIsReady)
+ {
+ _validationIsReady = data.AttemptPrepareValidation(segmentNumber);
+ }
+
+ // If the validation is now ready, we can start running it
+ if (_validationIsReady)
+ {
+ data.ProcessValidation(segmentNumber);
+ }
+
+ return _validationIsReady;
+ }
+
+ // called when a segment ends to stop any validation processing
+ // Validations should update their final results at this point
+ public void StopValidation(int segmentNumber)
+ {
+ data.StopValidation(segmentNumber);
+ }
+
+ /**
+ * Handles resuming the paused validation if un-paused from the UI
+ */
+ public void UnPauseValidation(int segmentNumber)
+ {
+ data.UnPauseValidation(segmentNumber);
+ }
+
+ /**
+ * Handle pausing the validation if paused from the UI
+ */
+ public void PauseValidation(int segmentNumber)
+ {
+ data.PauseValidation(segmentNumber);
+ }
+
+ public void ReplayReset()
+ {
+ data.ResetResults();
+ }
+
+ public bool HasSetAllResults()
+ {
+ return data.HasSetAllResults();
+ }
+
+ public void WriteToStringBuilder(StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("{\"type\":");
+ StringJsonConverter.WriteToStringBuilder(stringBuilder, type.ToString());
+ stringBuilder.Append(",\"apiVersion\":");
+ IntJsonConverter.WriteToStringBuilder(stringBuilder, apiVersion);
+ stringBuilder.Append(",\"data\":");
+ data.WriteToStringBuilder(stringBuilder);
+ stringBuilder.Append("}");
+ }
+
+ public override string ToString()
+ {
+ return ((IStringBuilderWriteable) this).ToJsonString();
+ }
+
+ }
+}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs.meta
new file mode 100644
index 00000000..7027eb8e
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidation.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ad4688e75a49446e8714acc9def0dcb0
+timeCreated: 1734114824
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs
new file mode 100644
index 00000000..8b692e76
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs
@@ -0,0 +1,7 @@
+namespace StateRecorder.BotSegments.Models
+{
+ public enum SegmentValidationType
+ {
+ Script
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs.meta
new file mode 100644
index 00000000..09432469
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidationType.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: abea2f581f354b0e8ce81eef8789ea65
+timeCreated: 1734114914
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations.meta
new file mode 100644
index 00000000..fbfe57a2
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5adb4eafa5044798b7b4c001ffad41e5
+timeCreated: 1734115741
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs
new file mode 100644
index 00000000..9722c56a
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs
@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using JetBrains.Annotations;
+using RegressionGames;
+using RegressionGames.StateRecorder;
+using RegressionGames.StateRecorder.BotSegments;
+using RegressionGames.StateRecorder.JsonConverters;
+using RegressionGames.Validation;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace StateRecorder.BotSegments.Models.SegmentValidations
+{
+
+ /**
+ * Data and functionality for a RGValidator script
+ */
+ [Serializable]
+ public class ScriptSegmentValidationData: IRGSegmentValidationData
+ {
+
+ public int apiVersion = SdkApiVersion.VERSION_29;
+
+ private static readonly Dictionary CachedTypes = new();
+
+ public string classFullName;
+ public float timeout = float.NegativeInfinity;
+
+ private bool _isStopped;
+ private float _startTime;
+
+ private RGValidationScript _myValidationScript;
+ private SegmentValidationResultSetContainer _storedResults;
+
+ private volatile Type _typeToCreate = null;
+ private volatile bool _readyToCreate;
+ private volatile string _error = null;
+ private volatile bool _isAttemptingToStart;
+
+ private string GetSegmentId(int segmentNumber)
+ {
+ return segmentNumber >= 0 ? segmentNumber.ToString() : "SEQUENCE";
+ }
+
+ public bool AttemptPrepareValidation(int segmentNumber)
+ {
+
+ // This means we've already ran ProcessValidations and are definitely good to go
+ // or that we are ready to create the script
+ if (_myValidationScript != null || _readyToCreate)
+ {
+ return true;
+ }
+
+ // This means that the process to construct the script has started but has not completed
+ if (!_readyToCreate && _isAttemptingToStart)
+ {
+ return false;
+ }
+
+ if (!_isStopped)
+ {
+ _isAttemptingToStart = true;
+ // load the type on another thread to avoid 'hitching' the game
+ new Thread(() =>
+ {
+ if (!CachedTypes.TryGetValue(classFullName, out var t))
+ {
+ // load our script type without knowing the assembly name, just the full type
+ foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ foreach (var type in a.GetTypes())
+ {
+ if (type.FullName != null && type.FullName.Equals(classFullName))
+ {
+ t = type;
+ CachedTypes[classFullName] = t;
+ break;
+ }
+ }
+ }
+ }
+
+ // Make sure this type is not null and is inheriting from RGValidationScript
+ if (t == null)
+ {
+ _error = $"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Regression Games could not load Bot Segment Validation Script for Type - {classFullName}. Type was not found in any assembly in the current runtime.";
+ RGDebug.LogError(_error);
+ }
+ else if (!typeof(RGValidationScript).IsAssignableFrom(t))
+ {
+ _error = $"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Regression Games could not load Bot Segment Validation Script for Type - {classFullName}. This Type does not inherit from RGValidationScript.";
+ RGDebug.LogError(_error);
+ }
+ else
+ {
+ _typeToCreate = t;
+ RGDebug.LogInfo($"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Validation ready - {classFullName}.");
+ }
+
+ _readyToCreate = true;
+ _isAttemptingToStart = false;
+ }).Start();
+ }
+
+ return false;
+ }
+
+ public void ProcessValidation(int segmentNumber)
+ {
+
+ if (!_isStopped)
+ {
+ if (_readyToCreate)
+ {
+ _readyToCreate = false;
+ if (_typeToCreate != null)
+ {
+ // Create the type
+ _myValidationScript = Activator.CreateInstance(_typeToCreate) as RGValidationScript;
+ _myValidationScript?.Initialize();
+ _startTime = Time.time;
+ }
+ else
+ {
+ RGDebug.LogError($"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Could not load type for validation script");
+ _isStopped = true;
+ }
+ }
+
+ if (_myValidationScript != null)
+ {
+
+ _myValidationScript.ProcessValidations();
+
+ if (timeout > 0 && _startTime > 0 && Time.time - _startTime > timeout)
+ {
+ // Validation is still not stopped at the time limit
+ RGDebug.LogInfo($"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Time limit has been reached for validations in {classFullName}");
+ StopValidation(segmentNumber);
+ }
+
+ }
+ }
+
+ }
+
+ public void PauseValidation(int segmentNumber)
+ {
+ _myValidationScript?.PauseValidation();
+ }
+
+ public void UnPauseValidation(int segmentNumber)
+ {
+ _myValidationScript?.UnPauseValidation();
+ }
+
+ public void StopValidation(int segmentNumber)
+ {
+
+ if (_isStopped) return; // Don't try to write the results twice
+
+ RGDebug.LogInfo($"({GetSegmentId(segmentNumber)}) - Bot Segment Validations - Stopping validation for {classFullName}");
+
+ // First, make sure RGValidateBehaviour marks the final results as pass or fail based on the desired
+ // conditions.
+ _myValidationScript?.StopValidations();
+
+ // Then backup the results since we are going to be destroying the behaviour
+ _storedResults = _myValidationScript?.GetResults();
+
+ // Finally, make this null
+ _myValidationScript = null;
+ _isStopped = true;
+ _startTime = float.NegativeInfinity;
+ }
+
+ public void ResetResults()
+ {
+ _storedResults = null; // Technically not needed, but here for safety
+ _myValidationScript?.ResetResults();
+ _isStopped = false;
+ }
+
+ public bool HasSetAllResults()
+ {
+ var results = _storedResults ?? _myValidationScript?.GetResults();
+ return results?.validationResults.All(v => v.result != SegmentValidationStatus.UNKNOWN) ?? false;
+ }
+
+ [CanBeNull]
+ public SegmentValidationResultSetContainer GetResults()
+ {
+ return _storedResults ?? _myValidationScript?.GetResults();
+ }
+
+ public void WriteToStringBuilder(StringBuilder stringBuilder)
+ {
+ stringBuilder.Append("{\"apiVersion\":");
+ IntJsonConverter.WriteToStringBuilder(stringBuilder, apiVersion);
+ stringBuilder.Append(",\"classFullName\":");
+ StringJsonConverter.WriteToStringBuilder(stringBuilder, classFullName);
+ stringBuilder.Append(",\"timeout\":");
+ StringJsonConverter.WriteToStringBuilder(stringBuilder, timeout.ToString());
+ stringBuilder.Append("}");
+ }
+
+ public int EffectiveApiVersion()
+ {
+ return apiVersion;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs.meta
new file mode 100644
index 00000000..9bd89dae
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ScriptSegmentValidationData.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 543db51442cd4fb683f1b712025fd33a
+timeCreated: 1734115787
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs
new file mode 100644
index 00000000..ffd4b2c0
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs
@@ -0,0 +1,43 @@
+using System;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace StateRecorder.BotSegments.Models.SegmentValidations
+{
+
+ [Serializable]
+ public class SegmentValidationResultContainer
+ {
+
+ /**
+ *
+ * The name of the validation. Sometimes this is just the name
+ * of the test function.
+ *
+ */
+ public string name;
+
+ /**
+ *
+ * An optional description of the validation.
+ *
+ */
+ [CanBeNull] public string description;
+
+ /**
+ *
+ * The actual state of this validation result
+ *
+ */
+ [JsonConverter(typeof(StringEnumConverter))]
+ public SegmentValidationStatus result;
+
+ public SegmentValidationResultContainer(string name, [CanBeNull] string description, SegmentValidationStatus result)
+ {
+ this.name = name;
+ this.description = description;
+ this.result = result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs.meta
new file mode 100644
index 00000000..0fee31c8
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultContainer.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e935cce85ca840d288a83940fa3bc982
+timeCreated: 1734196514
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs
new file mode 100644
index 00000000..a6484b5b
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace StateRecorder.BotSegments.Models.SegmentValidations
+{
+
+ [Serializable]
+ public class SegmentValidationResultSetContainer
+ {
+
+ /**
+ *
+ * The optional name of this set of results. In some cases such
+ * as that of the Script type, there is a name for the "suite" of
+ * validations in the script. In other cases, like EntityExists,
+ * it is just a standalone result, and therefore does not have a
+ * name or any organizational structure.
+ *
+ */
+ [CanBeNull] public string name;
+
+ /**
+ *
+ * The results of the validations. Note that these containers are
+ * instantiated right away for a segment, and so they may start off
+ * in an "UNKNOWN" status state since they have not been evaluated
+ * yet.
+ *
+ */
+ public List validationResults;
+
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs.meta
new file mode 100644
index 00000000..9dc00bc1
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationResultSetContainer.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b28fc25c73b444b8aa9f6c59bab53fa1
+timeCreated: 1734196342
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs
new file mode 100644
index 00000000..b386a9a1
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs
@@ -0,0 +1,20 @@
+namespace StateRecorder.BotSegments.Models.SegmentValidations
+{
+ public enum SegmentValidationStatus
+ {
+ /**
+ * This validation is currently passing.
+ */
+ PASSED,
+
+ /**
+ * This validation is current failing.
+ */
+ FAILED,
+
+ /**
+ * This validation has not yet been set, it is unknown if it will pass or fail yet.
+ */
+ UNKNOWN
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs.meta
new file mode 100644
index 00000000..a0a77481
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/SegmentValidationStatus.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1347f76651be4290ba73ced24e0f229d
+timeCreated: 1734196726
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs
new file mode 100644
index 00000000..2229a852
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs
@@ -0,0 +1,34 @@
+namespace StateRecorder.BotSegments.Models.SegmentValidations
+{
+ /**
+ *
+ * The mode in which to apply the validaton
+ *
+ */
+ public enum ValidationMode
+ {
+ /**
+ * This condition should be true on every frame that it is
+ * evaluated.
+ */
+ ALWAYS_TRUE,
+
+ /**
+ * This condition should never be true for any frame it is
+ * evaluated.
+ */
+ NEVER_TRUE,
+
+ /**
+ * This condition should be true at least once during the
+ * test.
+ */
+ EVENTUALLY_TRUE,
+
+ /**
+ * Once there is a frame that the given condition turns true, it must always
+ * be marked as true.
+ */
+ ONCE_TRUE_ALWAYS_TRUE
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs.meta
new file mode 100644
index 00000000..15c69c53
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/Models/SegmentValidations/ValidationMode.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 1ec29bde4dca4007853187e02b3999de
+timeCreated: 1734199067
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/OrKeyFrameCriteriaEvaluator.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/OrKeyFrameCriteriaEvaluator.cs
index ed893290..528fbde9 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/OrKeyFrameCriteriaEvaluator.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/BotSegments/OrKeyFrameCriteriaEvaluator.cs
@@ -6,13 +6,13 @@ namespace RegressionGames.StateRecorder.BotSegments
{
public static class OrKeyFrameCriteriaEvaluator
{
- public static bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, KeyFrameCriteria criteria)
+ public static bool Matched(bool firstSegment, int segmentNumber, bool botActionCompleted, bool validationsCompleted, KeyFrameCriteria criteria)
{
if (criteria.data is OrKeyFrameCriteriaData { criteriaList: not null } orCriteria)
{
try
{
- return KeyFrameEvaluator.Evaluator.MatchedHelper(firstSegment, segmentNumber, botActionCompleted, BooleanCriteria.Or, orCriteria.criteriaList);
+ return KeyFrameEvaluator.Evaluator.MatchedHelper(firstSegment, segmentNumber, botActionCompleted, validationsCompleted, BooleanCriteria.Or, orCriteria.criteriaList);
}
catch (Exception)
{
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/JsonConverterContractResolver.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/JsonConverterContractResolver.cs
index f9898a1a..0b37871d 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/JsonConverterContractResolver.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/JsonConverterContractResolver.cs
@@ -13,6 +13,7 @@
using RegressionGames.StateRecorder.JsonConverters;
using RegressionGames.StateRecorder.Models;
using StateRecorder.BotSegments.Models;
+using StateRecorder.BotSegments.Models.SegmentValidations;
using TMPro;
using UnityEngine;
using UnityEngine.AI;
@@ -91,7 +92,10 @@ public class JsonConverterContractResolver : DefaultContractResolver
{ typeof(BotSequenceEntry), new BotSequenceEntryJsonConverter() },
{ typeof(SequenceRestartCheckpoint), new SequenceRestartCheckpointJsonConverter() },
- { typeof(WorkAssignment), new WorkAssignmentJsonConverter() }
+ { typeof(WorkAssignment), new WorkAssignmentJsonConverter() },
+
+ { typeof(SegmentValidation), new SegmentValidationJsonConverter() },
+ { typeof(ScriptSegmentValidationData), new ScriptSegmentValidationDataJsonConverter() }
};
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ReplayToolbarManager.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ReplayToolbarManager.cs
index d7b1fd78..e33b1a54 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ReplayToolbarManager.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ReplayToolbarManager.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections;
+using System.Collections.Generic;
using System.Threading.Tasks;
using RegressionGames.StateRecorder.BotSegments;
using SimpleFileBrowser;
+using StateRecorder.BotSegments.Models;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -235,8 +237,9 @@ private void ProcessDataContainerZipAndSetup(String filePath)
{
try
{
+ var botSegmentLists = BotSegmentZipParser.ParseBotSegmentZipFromSystemPath(filePath, out var sessionId);
// do this on background thread
- var dataContainer = new BotSegmentsPlaybackContainer(BotSegmentZipParser.ParseBotSegmentZipFromSystemPath(filePath, out var sessionId), sessionId);
+ var dataContainer = new BotSegmentsPlaybackContainer(botSegmentLists, new List(), sessionId);
_playbackContainer = dataContainer;
}
catch (Exception e)
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ScreenRecorder.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ScreenRecorder.cs
index 24e6e843..30038c63 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ScreenRecorder.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/ScreenRecorder.cs
@@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
+using Newtonsoft.Json;
using RegressionGames.ActionManager;
using RegressionGames.CodeCoverage;
using RegressionGames.RemoteOrchestration;
@@ -16,7 +17,9 @@
using RegressionGames.StateRecorder.BotSegments.Models.BotActions;
using RegressionGames.StateRecorder.BotSegments.Models.BotCriteria;
using RegressionGames.StateRecorder.Models;
+using RegressionGames.Validation;
using StateRecorder.BotSegments;
+using StateRecorder.BotSegments.Models.SegmentValidations;
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -85,6 +88,7 @@ public class ScreenRecorder : MonoBehaviour
private string _currentGameplaySessionGameMetadataPath;
private string _currentGameplaySessionThumbnailPath;
private string _currentGameplaySessionLogsDirectoryPrefix;
+ private string _currentGameplaySessionValidationsPrefix;
private CancellationTokenSource _tokenSource;
@@ -112,6 +116,8 @@ public class ScreenRecorder : MonoBehaviour
private KeyMomentEvaluator _keyMomentEvaluator = new();
+ public List validationResults = new();
+
#if UNITY_EDITOR
private bool _needToRefreshAssets;
#endif
@@ -182,6 +188,7 @@ private async Task HandleEndRecording(long tickCount,
string thumbnailPath,
string logsDirectoryPrefix,
string gameMetadataPath,
+ string validationsPath,
bool onDestroy = false)
{
if (!onDestroy)
@@ -265,6 +272,9 @@ private async Task HandleEndRecording(long tickCount,
ZipFile.CreateFromDirectory(keyMomentsDirectoryPrefix, keyMomentsDirectoryPrefix + ".zip");
RGDebug.LogInfo($"Finished zipping replay to file: {keyMomentsDirectoryPrefix}.zip");
});
+
+ // Save the validation results to the validations JSON file, if there are any
+ await File.WriteAllBytesAsync(_currentGameplaySessionValidationsPrefix, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(validationResults ?? new List())));
// Finally, we also save a thumbnail, by choosing the middle file in the screenshots
var screenshotFiles = Directory.GetFiles(screenshotsDirectoryPrefix);
@@ -279,6 +289,9 @@ private async Task HandleEndRecording(long tickCount,
// wait for the zip tasks to finish
Task.WaitAll(zipTask1, zipTask2, zipTask3, zipTask4, zipTask5);
+
+ // print validation results
+ RGValidateLoggerUtility.LogValidationResults(validationResults);
if (!wasReplay)
{
@@ -575,6 +588,7 @@ private IEnumerator StartRecordingCoroutine(string referenceSessionId)
_startTime = DateTime.Now;
_tickQueue = new BlockingCollection<(TickDataToWriteToDisk, Action)>(new ConcurrentQueue<(TickDataToWriteToDisk, Action)>());
_tokenSource = new CancellationTokenSource();
+ validationResults = new();
Directory.CreateDirectory(stateRecordingsDirectory);
@@ -616,6 +630,7 @@ private IEnumerator StartRecordingCoroutine(string referenceSessionId)
Directory.CreateDirectory(_currentGameplaySessionMetadataDirectoryPrefix);
_currentGameplaySessionThumbnailPath = _currentGameplaySessionDirectoryPrefix + "/thumbnail.jpg";
+ _currentGameplaySessionValidationsPrefix = _currentGameplaySessionDirectoryPrefix + "/validations.json";
// run the tick processor in the background, but don't hook it to the token source.. we'll manage cancelling this on our own so we don't miss processing ticks
Task.Run(ProcessTicks);
@@ -739,6 +754,7 @@ private void StopRecordingCleanupHelper(bool wasRecording, bool wasReplay)
_currentGameplaySessionThumbnailPath,
_currentGameplaySessionLogsDirectoryPrefix,
_currentGameplaySessionGameMetadataPath,
+ _currentGameplaySessionValidationsPrefix,
true);
}
else
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/SdkApiVersion.cs b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/SdkApiVersion.cs
index 2d2adef5..f1b9d185 100644
--- a/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/SdkApiVersion.cs
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/StateRecorder/SdkApiVersion.cs
@@ -121,9 +121,13 @@ public static class SdkApiVersion
* Add initial framework for key moment bot segments
*/
public const int VERSION_28 = 28;
+ /**
+ * Add validations field and functionality
+ */
+ public const int VERSION_29 = 29;
// Update this when new features are used in the SDK
- public const int CURRENT_VERSION = VERSION_28;
+ public const int CURRENT_VERSION = VERSION_29;
}
}
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta
new file mode 100644
index 00000000..c0d7d704
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: eac368faa0a64e8a844004066126aa33
+timeCreated: 1732638994
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs
new file mode 100644
index 00000000..b6bc518d
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs
@@ -0,0 +1,31 @@
+using System;
+using JetBrains.Annotations;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+using UnityEngine;
+
+namespace RegressionGames.Validation
+{
+
+ /**
+ * An attribute that marks a method as a validation method
+ * to be run by Regression Games during a bot sequence or bot segment.
+ * You can set a frequency attribute to indicate how often the validation
+ * is running (e.g. every 10th frame).
+ * You can also indicate whether this is a validation that should
+ * always be true, never be true, or be true at least once.
+ */
+ [AttributeUsage(AttributeTargets.Method)]
+ [MeansImplicitUse]
+ public class RGValidate: Attribute {
+
+ public int Frequency { get; private set; }
+ public ValidationMode Mode { get; private set; }
+
+ public RGValidate(ValidationMode mode, int frequency = 1)
+ {
+ Mode = mode;
+ Frequency = Mathf.Max(1, frequency); // Ensure frequency is at least 1
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta
new file mode 100644
index 00000000..ee87a2f4
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidate.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 15d18d146c8f453d9154e544fa22353e
+timeCreated: 1732639009
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs
new file mode 100644
index 00000000..79032fbb
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Text;
+using JetBrains.Annotations;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+
+namespace RegressionGames.Validation
+{
+
+ /**
+ * A set of utilities for logging results related to validations
+ */
+ public class RGValidateLoggerUtility
+ {
+
+ /**
+ * Prints out validation results in a clean way in the logs
+ */
+ public static void LogValidationResults([CanBeNull] List results)
+ {
+
+ if (results == null)
+ {
+ // For now, if there are no validations, we just print a small message
+ RGDebug.LogInfo("No validations were provided as part of this run");
+ return;
+ }
+
+ // Print out results while also collecting total results
+ var passed = 0;
+ var failed = 0;
+ var unknown = 0;
+
+ var logBuilder = new StringBuilder(100_000);
+
+ logBuilder.Append("--------------- VALIDATION RESULTS --------------- (If in the editor, click this to view more)\n\n");
+ foreach (var resultSet in results)
+ {
+ logBuilder.Append("" + resultSet.name + "\n");
+ foreach (var validation in resultSet.validationResults)
+ {
+ switch (validation.result)
+ {
+ case SegmentValidationStatus.PASSED:
+ logBuilder.Append(" [PASS] ");
+ passed++;
+ break;
+ case SegmentValidationStatus.FAILED:
+ logBuilder.Append(" [FAIL] ");
+ failed++;
+ break;
+ case SegmentValidationStatus.UNKNOWN:
+ logBuilder.Append(" [UNKNOWN] ");
+ unknown++;
+ break;
+ }
+ logBuilder.Append(validation.name + "\n");
+ }
+
+ logBuilder.Append("\n");
+ }
+
+ if (failed > 0)
+ {
+ logBuilder.Append("VALIDATIONS FAILED - ");
+ }
+ else
+ {
+ logBuilder.Append("VALIDATIONS PASSED - ");
+ }
+
+ logBuilder.Append($"{failed} FAILED, {passed} PASSED ({unknown} UNKNOWN)\n\n");
+
+ // Finally log the results
+ if (failed > 0)
+ {
+ RGDebug.LogError(logBuilder.ToString());
+ }
+ else
+ {
+ RGDebug.LogInfo(logBuilder.ToString());
+ }
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs.meta
new file mode 100644
index 00000000..31a25e82
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidateLoggerUtility.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2c2c3e9500ae4570b6f70b0bbf1935c0
+timeCreated: 1734494119
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs
new file mode 100644
index 00000000..0060f24f
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs
@@ -0,0 +1,255 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using StateRecorder.BotSegments.Models.SegmentValidations;
+using UnityEngine;
+
+namespace RegressionGames.Validation
+{
+ public class RGValidationScript
+ {
+
+ private bool _isPaused;
+
+ public class ValidatorData
+ {
+ public MethodInfo Method { get; set; }
+ public int Frequency { get; set; }
+
+ public ValidationMode Mode { get; set; }
+
+ public SegmentValidationStatus Status { get; set; } = SegmentValidationStatus.UNKNOWN;
+
+ public System.Exception ThrownException { get; set; }
+ }
+
+ public List Validators = new ();
+
+ /**
+ * Called when any validation here changes
+ */
+ public delegate void ValidationsUpdated();
+ public event ValidationsUpdated OnValidationsUpdated;
+
+ private string _className;
+ private int frame = 0;
+ private ValidatorData currentValidator;
+ private SegmentValidationStatus currentStatus = SegmentValidationStatus.UNKNOWN;
+
+ public void Initialize()
+ {
+
+ _className = GetType().FullName;
+
+ // Cache all methods with UpdateMethod attribute
+ Validators = new List();
+ var methods = GetType().GetMethods(BindingFlags.Instance |
+ BindingFlags.NonPublic |
+ BindingFlags.Public);
+
+ foreach (var method in methods)
+ {
+ var attribute = method.GetCustomAttribute();
+ if (attribute != null)
+ {
+ Validators.Add(new ValidatorData
+ {
+ Method = method,
+ Frequency = attribute.Frequency,
+ Mode = attribute.Mode
+ });
+ RGDebug.Log("[RGValidator] Activated " + method.Name);
+ }
+ }
+
+ }
+
+ /**
+ *
+ * Marking this validation as evaluating to true. This is one of the main "assert"-type methods to use
+ * when writing your validations.
+ *
+ */
+ public void AssertAsTrue(string message = null)
+ {
+ currentStatus = SegmentValidationStatus.PASSED;
+ }
+
+ /**
+ *
+ * Marking this validation as evaluating to false. This is one of the main "assert"-type methods to use
+ * when writing your validations.
+ *
+ */
+ public void AssertAsFalse(string message = null)
+ {
+ currentStatus = SegmentValidationStatus.FAILED;
+ }
+
+ /**
+ *
+ * Returns a collection of all the results for the validations so far.
+ * This does not necessarily mean the final result - it is the result
+ * for this moment in time.
+ *
+ */
+ public SegmentValidationResultSetContainer GetResults()
+ {
+ var resultSet = new SegmentValidationResultSetContainer
+ {
+ name = _className,
+ validationResults = Validators.Select(v => new SegmentValidationResultContainer(v.Method.Name, null, v.Status)).ToList()
+ };
+ return resultSet;
+ }
+
+ public virtual void ResetValidationStates()
+ {
+ // The base implementation of this doesn't do anything - it's up to the validation script to implement this
+ }
+
+ /**
+ *
+ * Resets all results for this script.
+ *
+ */
+ public void ResetResults()
+ {
+ // First, reset all the stored results
+ foreach (var validator in Validators)
+ {
+ validator.Status = SegmentValidationStatus.UNKNOWN;
+ validator.ThrownException = null;
+ }
+
+ // Then call the reset function on the script, in case they need to reset some intermediate state
+ ResetValidationStates();
+ }
+
+ public void ProcessValidations()
+ {
+
+ // If paused, don't do anything now
+ if (_isPaused) return;
+
+ // Call tagged methods based on their frequency
+ foreach (var validator in Validators)
+ {
+
+ // Check that we should run on this frame
+ if (frame % validator.Frequency != 0)
+ {
+ continue;
+ }
+
+ // Skip over validators that are already passed for failed
+ if (validator.Status == SegmentValidationStatus.FAILED)
+ {
+ continue;
+ }
+ if (validator.Status == SegmentValidationStatus.PASSED && validator.Mode != ValidationMode.ONCE_TRUE_ALWAYS_TRUE)
+ {
+ continue;
+ }
+
+ // Run the validator
+ try
+ {
+ currentStatus = SegmentValidationStatus.UNKNOWN;
+ currentValidator = validator;
+ validator.Method.Invoke(this, null);
+ }
+ catch (Exception e)
+ {
+ Debug.LogError($"Error running validation method {validator.Method.Name}: {e.Message}");
+ validator.ThrownException = e;
+ }
+
+ // After the method is called, we can figure out what to do with the validators that should immediately
+ // fail or pass
+ if (currentValidator.Mode == ValidationMode.NEVER_TRUE && currentStatus == SegmentValidationStatus.PASSED)
+ {
+ // Debug.LogError($"Validation method {validator.Method.Name} should never pass, but it did.");
+ currentValidator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (currentValidator.Mode == ValidationMode.ALWAYS_TRUE && currentStatus is SegmentValidationStatus.FAILED or SegmentValidationStatus.UNKNOWN)
+ {
+ // Debug.LogError($"Validation method {validator.Method.Name} should always pass, but it did not.");
+ currentValidator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (currentValidator.Mode == ValidationMode.EVENTUALLY_TRUE && currentStatus == SegmentValidationStatus.PASSED)
+ {
+ // Debug.LogError($"Validation method {validator.Method.Name} finally passed.");
+ currentValidator.Status = SegmentValidationStatus.PASSED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (currentValidator.Mode == ValidationMode.ONCE_TRUE_ALWAYS_TRUE &&
+ currentValidator.Status == SegmentValidationStatus.PASSED &&
+ currentStatus != SegmentValidationStatus.PASSED)
+ {
+ // Debug.LogError($"Validation method {validator.Method.Name} passed before but is no longer passing.");
+ currentValidator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (currentValidator.Mode == ValidationMode.ONCE_TRUE_ALWAYS_TRUE &&
+ currentValidator.Status == SegmentValidationStatus.UNKNOWN &&
+ currentStatus == SegmentValidationStatus.PASSED)
+ {
+ // Debug.LogError($"Validation method {validator.Method.Name} is now passing.");
+ currentValidator.Status = SegmentValidationStatus.PASSED;
+ OnValidationsUpdated?.Invoke();
+ }
+
+ }
+
+ frame++;
+ }
+
+ public void PauseValidation()
+ {
+ _isPaused = true;
+ }
+
+ public void UnPauseValidation()
+ {
+ _isPaused = false;
+ }
+
+ public void StopValidations()
+ {
+ // First, make sure RGValidateBehaviour marks the final results as pass or fail based on the desired
+ // conditions.
+ foreach (var validator in Validators)
+ {
+ if (validator.Mode == ValidationMode.ALWAYS_TRUE && validator.Status == SegmentValidationStatus.UNKNOWN)
+ {
+ //Debug.LogError($"Validation method {validator.Method.Name} should always pass, but it never did.");
+ validator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (validator.Mode == ValidationMode.NEVER_TRUE && validator.Status == SegmentValidationStatus.UNKNOWN)
+ {
+ //Debug.LogError($"Validation method {validator.Method.Name} should never pass, and it never did!");
+ validator.Status = SegmentValidationStatus.PASSED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (validator.Mode == ValidationMode.EVENTUALLY_TRUE && validator.Status is SegmentValidationStatus.UNKNOWN or SegmentValidationStatus.FAILED)
+ {
+ //Debug.LogError($"Validation method {validator.Method.Name} should eventually pass, but it never did.");
+ validator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ else if (validator.Mode == ValidationMode.ONCE_TRUE_ALWAYS_TRUE && validator.Status == SegmentValidationStatus.UNKNOWN)
+ {
+ //Debug.LogError($"Validation method {validator.Method.Name} was never passed or failed, so it is technically failed.");
+ validator.Status = SegmentValidationStatus.FAILED;
+ OnValidationsUpdated?.Invoke();
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs.meta b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs.meta
new file mode 100644
index 00000000..fd83d904
--- /dev/null
+++ b/src/gg.regression.unity.bots/Runtime/Scripts/Validation/RGValidationScript.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 933a62accbff41e28af49b5e7cc982a4
+timeCreated: 1732641487
\ No newline at end of file
diff --git a/src/gg.regression.unity.bots/Tests/TestFramework/Scripts/RGTestUtils.cs b/src/gg.regression.unity.bots/Tests/TestFramework/Scripts/RGTestUtils.cs
index 3e967b43..b0e1d749 100644
--- a/src/gg.regression.unity.bots/Tests/TestFramework/Scripts/RGTestUtils.cs
+++ b/src/gg.regression.unity.bots/Tests/TestFramework/Scripts/RGTestUtils.cs
@@ -8,6 +8,7 @@
using RegressionGames.StateRecorder.Models;
using RegressionGames.Types;
using StateRecorder.BotSegments;
+using StateRecorder.BotSegments.Models;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.SceneManagement;
@@ -87,7 +88,7 @@ public static IEnumerator StartPlaybackFromZipFile(string recordingPath, Action<
var playbackController = Object.FindObjectOfType();
var statusManager = Object.FindObjectOfType();
var botSegments = BotSegmentZipParser.ParseBotSegmentZipFromSystemPath(recordingPath, out var sessionId);
- var replayData = new BotSegmentsPlaybackContainer(botSegments, sessionId);
+ var replayData = new BotSegmentsPlaybackContainer(botSegments, new List(), sessionId);
playbackController.SetDataContainer(replayData);
playbackController.Play();
@@ -131,8 +132,12 @@ public static IEnumerator StartPlaybackFromDirectory(string recordingPath, Actio
RGDebug.LogInfo("Loading and starting playback recording from " + recordingPath);
var playbackController = Object.FindObjectOfType();
var statusManager = Object.FindObjectOfType();
- var botSegments = BotSegmentDirectoryParser.ParseBotSegmentSystemDirectory(recordingPath, out var sessionId);
- var replayData = new BotSegmentsPlaybackContainer(botSegments, sessionId);
+ var botSegments = BotSegmentDirectoryParser.ParseBotSegmentSystemDirectory(recordingPath, out var sessionId).Select(a=>new BotSegmentList("BotSegmentList for BotSegment - " + a.name, new List() {a})
+ {
+ description = "BotSegmentList for BotSegment - " + a.description,
+ validations = new()
+ });
+ var replayData = new BotSegmentsPlaybackContainer(botSegments, new List(), sessionId);
playbackController.SetDataContainer(replayData);
playbackController.Play();
@@ -199,6 +204,7 @@ public static IEnumerator WaitForBotCriteria(List botCriteria,
true,
0,
true,
+ true,
botCriteria
);
@@ -309,15 +315,20 @@ public static IEnumerator PerformBotAction(BotAction botAction, ActionThe bot segment
* A callback that will be called with the results of this playback
* How long in seconds to wait for this segment to complete, <= 0 means wait forever (default)
+ * An optional set of validations to run against these segments
*/
- public static IEnumerator StartBotSegment(BotSegment botSegment, Action setPlaybackResult, int timeout = 0)
+ public static IEnumerator StartBotSegment(BotSegment botSegment, Action setPlaybackResult, int timeout = 0, List validations = null)
{
RGDebug.LogInfo("Starting bot segment from path: " + botSegment.resourcePath);
var playbackController = Object.FindObjectOfType();
var statusManager = Object.FindObjectOfType();
- playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(new[] { botSegment }));
+ playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(new List() {new BotSegmentList("BotSegmentList for BotSegment - " + botSegment.name, new List() {botSegment})
+ {
+ description = "BotSegmentList for BotSegment - " + botSegment.description,
+ validations = new()
+ }}, validations ?? new List()));
playbackController.Play();
@@ -355,15 +366,16 @@ public static IEnumerator StartBotSegment(BotSegment botSegment, ActionThe bot segment list
* A callback that will be called with the results of this playback
* How long in seconds to wait for this segment to complete, <= 0 means wait forever (default)
+ * An optional set of validations to run against these segments
*/
- public static IEnumerator StartBotSegmentList(BotSegmentList botSegmentList, Action setPlaybackResult, int timeout = 0)
+ public static IEnumerator StartBotSegmentList(BotSegmentList botSegmentList, Action setPlaybackResult, int timeout = 0, List validations = null)
{
RGDebug.LogInfo("Starting bot segment list from path: " + botSegmentList.resourcePath);
var playbackController = Object.FindObjectOfType();
var statusManager = Object.FindObjectOfType();
- playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(botSegmentList.segments));
+ playbackController.SetDataContainer(new BotSegmentsPlaybackContainer(new List() {botSegmentList}, validations ?? new List()));
playbackController.Play();