提交GAS 打算做一个帧同步的GAS

This commit is contained in:
DESKTOP-5RP3AKU\Jisol
2024-10-18 03:16:09 +08:00
parent b0a2e4a900
commit d9b0c78827
726 changed files with 76601 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 35878043833f4ae3bb6976f748a85a8b
timeCreated: 1705029950

View File

@@ -0,0 +1,79 @@
using System.Linq;
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
namespace GAS.Runtime
{
public abstract class GameplayCue : ScriptableObject
{
protected const int WIDTH_LABEL = 70;
[TitleGroup("Base")]
[HorizontalGroup("Base/H1")]
[TabGroup("Base/H1/V1", "Summary", SdfIconType.InfoSquareFill, TextColor = "#0BFFC5", Order = 1)]
[HideLabel]
[MultiLineProperty(10)]
public string Description;
#if UNITY_EDITOR
[TabGroup("Base/H1/V2", "General", SdfIconType.AwardFill, TextColor = "#FF7F00", Order = 2)]
[TabGroup("Base/H1/V2", "Detail", SdfIconType.TicketDetailedFill, TextColor = "#BC2FDE")]
[LabelText("类型名称", SdfIconType.FileCodeFill)]
[LabelWidth(WIDTH_LABEL)]
[ShowInInspector]
[PropertyOrder(-1)]
public string TypeName => GetType().Name;
[TabGroup("Base/H1/V2", "Detail")]
[LabelText("类型全名", SdfIconType.FileCodeFill)]
[LabelWidth(WIDTH_LABEL)]
[ShowInInspector]
[PropertyOrder(-1)]
public string TypeFullName => GetType().FullName;
[TabGroup("Base/H1/V2", "Detail")]
[ListDrawerSettings(ShowFoldout = true, ShowItemCount = false, ShowPaging = false)]
[ShowInInspector]
[LabelText("继承关系")]
[LabelWidth(WIDTH_LABEL)]
[PropertyOrder(-1)]
public string[] InheritanceChain => GetType().GetInheritanceChain().Reverse().ToArray();
#endif
// Tags
[TabGroup("Base/H1/V3", "Tags", SdfIconType.TagsFill, TextColor = "#45B1FF", Order = 3)]
[ValueDropdown("@ValueDropdownHelper.GameplayTagChoices", IsUniqueList = true, HideChildProperties = true)]
[ListDrawerSettings(ShowFoldout = true, ShowItemCount = false, DraggableItems = false)]
[DisableContextMenu(disableForMember: false, disableCollectionElements: true)]
[CustomContextMenu("排序", "@RequiredTags = TagHelper.Sort($value)")]
[LabelText("RequiredTags - 持有所有标签才可触发")]
public GameplayTag[] RequiredTags;
[TabGroup("Base/H1/V3", "Tags")]
[ValueDropdown("@ValueDropdownHelper.GameplayTagChoices", IsUniqueList = true, HideChildProperties = true)]
[ListDrawerSettings(ShowFoldout = true, ShowItemCount = false, DraggableItems = false)]
[DisableContextMenu(disableForMember: false, disableCollectionElements: true)]
[CustomContextMenu("排序", "@ImmunityTags = TagHelper.Sort($value)")]
[LabelText("ImmunityTags - 持有任意标签不可触发")]
public GameplayTag[] ImmunityTags;
public virtual bool Triggerable(AbilitySystemComponent owner)
{
if (owner == null) return false;
// 持有【所有】RequiredTags才可触发
if (!owner.HasAllTags(new GameplayTagSet(RequiredTags)))
return false;
// 持有【任意】ImmunityTags不可触发
if (owner.HasAnyTags(new GameplayTagSet(ImmunityTags)))
return false;
return true;
}
}
public abstract class GameplayCue<T> : GameplayCue where T : GameplayCueSpec
{
public abstract T CreateSpec(GameplayCueParameters parameters);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 24a60d4a49b842c28e848c52adb587e4
timeCreated: 1702536571

View File

@@ -0,0 +1,53 @@
using UnityEngine;
namespace GAS.Runtime
{
public abstract class GameplayCueDurational : GameplayCue<GameplayCueDurationalSpec>
{
public GameplayCueDurationalSpec ApplyFrom(GameplayEffectSpec gameplayEffectSpec)
{
if (!Triggerable(gameplayEffectSpec.Owner)) return null;
var durationalCue = CreateSpec(new GameplayCueParameters
{ sourceGameplayEffectSpec = gameplayEffectSpec });
return durationalCue;
}
public GameplayCueDurationalSpec ApplyFrom(AbilitySpec abilitySpec,params object[] customArguments)
{
if (!Triggerable(abilitySpec.Owner)) return null;
var durationalCue = CreateSpec(new GameplayCueParameters
{ sourceAbilitySpec = abilitySpec, customArguments = customArguments});
return durationalCue;
}
#if UNITY_EDITOR
public virtual void OnEditorPreview(GameObject previewObject,int frameIndex,int startFrame,int endFrame)
{
}
#endif
}
public abstract class GameplayCueDurationalSpec : GameplayCueSpec
{
protected GameplayCueDurationalSpec(GameplayCueDurational cue, GameplayCueParameters parameters) :
base(cue, parameters)
{
}
public abstract void OnAdd();
public abstract void OnRemove();
public abstract void OnGameplayEffectActivate();
public abstract void OnGameplayEffectDeactivate();
public abstract void OnTick();
}
public abstract class GameplayCueDurationalSpec<T> : GameplayCueDurationalSpec where T : GameplayCueDurational
{
public readonly T cue;
protected GameplayCueDurationalSpec(T cue, GameplayCueParameters parameters) : base(cue, parameters)
{
this.cue = cue;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfc27df4bdc54282b3e7540eef7f4679
timeCreated: 1705027856

View File

@@ -0,0 +1,53 @@
using UnityEngine;
namespace GAS.Runtime
{
public abstract class GameplayCueInstant : GameplayCue<GameplayCueInstantSpec>
{
public virtual void ApplyFrom(GameplayEffectSpec gameplayEffectSpec)
{
if (Triggerable(gameplayEffectSpec.Owner))
{
var instantCue = CreateSpec(new GameplayCueParameters
{ sourceGameplayEffectSpec = gameplayEffectSpec });
instantCue?.Trigger();
}
}
public virtual void ApplyFrom(AbilitySpec abilitySpec, params object[] customArguments)
{
if (Triggerable(abilitySpec.Owner))
{
var instantCue = CreateSpec(new GameplayCueParameters
{ sourceAbilitySpec = abilitySpec, customArguments = customArguments });
instantCue?.Trigger();
}
}
#if UNITY_EDITOR
public virtual void OnEditorPreview(GameObject previewObject, int frame, int startFrame)
{
}
#endif
}
public abstract class GameplayCueInstantSpec : GameplayCueSpec
{
public GameplayCueInstantSpec(GameplayCueInstant cue, GameplayCueParameters parameters) : base(cue,
parameters)
{
}
public abstract void Trigger();
}
public abstract class GameplayCueInstantSpec<T>:GameplayCueInstantSpec where T:GameplayCueInstant
{
public readonly T cue;
public GameplayCueInstantSpec(T cue, GameplayCueParameters parameters) : base(cue, parameters)
{
this.cue = cue;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2483d9a3b6064679b6ec7cf5fc2831b6
timeCreated: 1705027797

View File

@@ -0,0 +1,15 @@
namespace GAS.Runtime
{
public struct GameplayCueParameters
{
public GameplayEffectSpec sourceGameplayEffectSpec;
public AbilitySpec sourceAbilitySpec;
public object[] customArguments;
// AggregatedSourceTags
// AggregatedTargetTags
// EffectContext
// Magnitude
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 71478534541e4560a993cd0ead34edc3
timeCreated: 1705915979

View File

@@ -0,0 +1,29 @@

namespace GAS.Runtime
{
public abstract class GameplayCueSpec
{
protected readonly GameplayCue _cue;
protected readonly GameplayCueParameters _parameters;
public AbilitySystemComponent Owner { get; protected set; }
public virtual bool Triggerable()
{
return _cue.Triggerable(Owner);
}
public GameplayCueSpec(GameplayCue cue, GameplayCueParameters cueParameters)
{
_cue = cue;
_parameters = cueParameters;
if (_parameters.sourceGameplayEffectSpec != null)
{
Owner = _parameters.sourceGameplayEffectSpec.Owner;
}
else if (_parameters.sourceAbilitySpec != null)
{
Owner = _parameters.sourceAbilitySpec.Owner;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d2c7719fe70c46e98344f1d9d8ebb9e6
timeCreated: 1705042515

View File

@@ -0,0 +1,105 @@
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
namespace GAS.Runtime
{
public class CueAnimation : GameplayCueDurational
{
[BoxGroup]
[InfoBox(GASTextDefine.CUE_ANIMATION_PATH_TIP)]
[LabelText(GASTextDefine.CUE_ANIMATION_PATH)]
[SerializeField]
private string _animatorRelativePath;
[BoxGroup]
[InfoBox(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN_ANIMATOR_TIP)]
[LabelText(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN)]
[SerializeField]
private bool _includeChildrenAnimator;
[BoxGroup]
[LabelText(GASTextDefine.CUE_ANIMATION_STATE)]
[SerializeField]
private string _stateName;
public string AnimatorRelativePath => _animatorRelativePath;
public bool IncludeChildrenAnimator => _includeChildrenAnimator;
public string StateName => _stateName;
public override GameplayCueDurationalSpec CreateSpec(GameplayCueParameters parameters)
{
return new CueAnimationSpec(this, parameters);
}
#if UNITY_EDITOR
public override void OnEditorPreview(GameObject previewObject, int frame, int startFrame, int endFrame)
{
if (startFrame <= frame && frame <= endFrame)
{
var transform = previewObject.transform.Find(AnimatorRelativePath);
var animator = IncludeChildrenAnimator ? transform.GetComponentInChildren<Animator>() : transform.GetComponent<Animator>();
if (animator != null)
{
var stateMap = animator.GetAllAnimationState();
if (stateMap.TryGetValue(StateName, out var clip))
{
float clipFrameCount = (int)(clip.frameRate * clip.length);
if (frame <= clipFrameCount + startFrame)
{
var progress = (frame - startFrame) / clipFrameCount;
if (progress > 1 && clip.isLooping) progress -= (int)progress;
clip.SampleAnimation(animator.gameObject, progress * clip.length);
}
}
}
else
{
Debug.LogError($"Animator is null. Please check the cue asset: {name}, AnimatorRelativePath: {AnimatorRelativePath}, IncludeChildrenAnimator: {IncludeChildrenAnimator}");
}
}
}
#endif
}
public class CueAnimationSpec : GameplayCueDurationalSpec<CueAnimation>
{
private readonly Animator _animator;
public CueAnimationSpec(CueAnimation cue, GameplayCueParameters parameters) : base(cue,
parameters)
{
var transform = Owner.transform.Find(cue.AnimatorRelativePath);
_animator = cue.IncludeChildrenAnimator ? transform.GetComponentInChildren<Animator>() : transform.GetComponent<Animator>();
if (_animator == null)
{
Debug.LogError($"Animator is null. Please check the cue asset: {cue.name}, AnimatorRelativePath: {cue.AnimatorRelativePath}, IncludeChildrenAnimator: {cue.IncludeChildrenAnimator}");
}
}
public override void OnAdd()
{
if (_animator != null)
{
_animator.Play(cue.StateName);
}
}
public override void OnRemove()
{
}
public override void OnGameplayEffectActivate()
{
}
public override void OnGameplayEffectDeactivate()
{
}
public override void OnTick()
{
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2b9fc094e7fa46ac85ce5efa6359b74f
timeCreated: 1711451948

View File

@@ -0,0 +1,111 @@
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
namespace GAS.Runtime
{
[CreateAssetMenu(fileName = "CuePlayAnimation", menuName = "GAS/Cue/CuePlayAnimation")]
public class CueAnimationOneShot : GameplayCueInstant
{
[BoxGroup]
[InfoBox(GASTextDefine.CUE_ANIMATION_PATH_TIP)]
[LabelText(GASTextDefine.CUE_ANIMATION_PATH)]
[SerializeField]
private string _animatorRelativePath;
[BoxGroup]
[InfoBox(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN_ANIMATOR_TIP)]
[LabelText(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN)]
[SerializeField]
private bool _includeChildrenAnimator;
[BoxGroup]
[LabelText(GASTextDefine.CUE_ANIMATION_STATE)]
[SerializeField]
private string _stateName;
public string AnimatorRelativePath => _animatorRelativePath;
public bool IncludeChildrenAnimator => _includeChildrenAnimator;
public string StateName => _stateName;
public override GameplayCueInstantSpec CreateSpec(GameplayCueParameters parameters)
{
return new CueAnimationOneShotSpec(this, parameters);
}
#if UNITY_EDITOR
public override void OnEditorPreview(GameObject previewObject, int frame, int startFrame)
{
if (startFrame <= frame)
{
var transform = previewObject.transform.Find(AnimatorRelativePath);
Animator animator = null;
if (transform != null)
{
animator = IncludeChildrenAnimator
? transform.GetComponentInChildren<Animator>()
: transform.GetComponent<Animator>();
}
if (animator == null)
{
Debug.LogError(
$"Animator is null. Please check the cue asset: {name}, AnimatorRelativePath: {AnimatorRelativePath}, IncludeChildrenAnimator: {IncludeChildrenAnimator}");
return;
}
var stateMap = animator.GetAllAnimationState();
if (stateMap.TryGetValue(StateName, out var clip))
{
if (clip != null)
{
float clipFrameCount = (int)(clip.frameRate * clip.length);
if (frame <= clipFrameCount + startFrame)
{
var progress = (frame - startFrame) / clipFrameCount;
if (progress > 1 && clip.isLooping) progress -= (int)progress;
clip.SampleAnimation(animator.gameObject, progress * clip.length);
}
}
else
{
Debug.LogWarning($"Clip is null. Please check the cue asset: {name}, StateName: {StateName}");
}
}
}
}
#endif
}
public class CueAnimationOneShotSpec : GameplayCueInstantSpec<CueAnimationOneShot>
{
private readonly Animator _animator;
public CueAnimationOneShotSpec(CueAnimationOneShot cue, GameplayCueParameters parameters)
: base(cue, parameters)
{
var transform = Owner.transform.Find(cue.AnimatorRelativePath);
if (transform != null)
{
_animator = cue.IncludeChildrenAnimator
? transform.GetComponentInChildren<Animator>()
: transform.GetComponent<Animator>();
}
if (_animator == null)
{
Debug.LogError(
$"Animator is null. Please check the cue asset: {cue.name}, AnimatorRelativePath: {cue.AnimatorRelativePath}, IncludeChildrenAnimator: {cue.IncludeChildrenAnimator}");
}
}
public override void Trigger()
{
if (_animator != null)
{
_animator.Play(cue.StateName);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c0831f70ea054910b7c7a907e5121590
timeCreated: 1703670598

View File

@@ -0,0 +1,89 @@
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
namespace GAS.Runtime
{
public sealed class CueAnimationSpeedModifier : GameplayCueDurational
{
const int LabelWidth = 120;
[TabGroup("Data", "Data", SdfIconType.Gear, TextColor = "#FF7F00")]
[InfoBox(GASTextDefine.CUE_ANIMATION_PATH_TIP, InfoMessageType.None)]
[LabelText(GASTextDefine.CUE_ANIMATION_PATH), LabelWidth(LabelWidth)]
public string animatorRelativePath;
[TabGroup("Data", "Data")]
[InfoBox(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN_ANIMATOR_TIP, InfoMessageType.None)]
[LabelText(GASTextDefine.CUE_ANIMATION_INCLUDE_CHILDREN), LabelWidth(LabelWidth)]
public bool includeChildrenAnimator;
[TabGroup("Data", "Data")]
[LabelText("播放速度"), LabelWidth(LabelWidth)]
[Range(0, 5f)]
public float speed = 1f;
[TabGroup("Data", "Data")]
[InfoBox("结束时会设置的值, 如果有其它需求, 需要另外实现. ^_^", InfoMessageType.None)]
[LabelText("默认播放速度"), LabelWidth(LabelWidth)]
[Range(0, 5f)]
public float defaultSpeed = 1f;
public override GameplayCueDurationalSpec CreateSpec(GameplayCueParameters parameters)
{
return new GCS_ChangeAnimationSpeed(this, parameters);
}
}
public sealed class GCS_ChangeAnimationSpeed : GameplayCueDurationalSpec<CueAnimationSpeedModifier>
{
private readonly Animator _animator;
public GCS_ChangeAnimationSpeed(CueAnimationSpeedModifier cue, GameplayCueParameters parameters)
: base(cue, parameters)
{
var transform = Owner.transform.Find(cue.animatorRelativePath);
if (transform != null)
{
_animator = cue.includeChildrenAnimator
? transform.GetComponentInChildren<Animator>()
: transform.GetComponent<Animator>();
}
if (_animator == null)
{
Debug.LogError(
$"Animator is null. Please check the cue asset: {cue.name}, AnimatorRelativePath: {cue.animatorRelativePath}, IncludeChildrenAnimator: {cue.includeChildrenAnimator}");
}
}
public override void OnAdd()
{
}
public override void OnRemove()
{
}
public override void OnGameplayEffectActivate()
{
if (_animator != null)
{
_animator.speed = cue.speed;
}
}
public override void OnGameplayEffectDeactivate()
{
if (_animator != null)
{
_animator.speed = cue.defaultSpeed;
}
}
public override void OnTick()
{
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 05165f697da94e96b844dd2d8c77aea9
timeCreated: 1718704588

View File

@@ -0,0 +1,78 @@
using System;
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
using Object = UnityEngine.Object;
namespace GAS.Runtime
{
public class CuePlaySound : GameplayCueDurational
{
[BoxGroup]
[LabelText(GASTextDefine.CUE_SOUND_EFFECT)]
public AudioClip soundEffect;
[BoxGroup]
[LabelText(GASTextDefine.CUE_ATTACH_TO_OWNER)]
public bool isAttachToOwner = true;
public override GameplayCueDurationalSpec CreateSpec(GameplayCueParameters parameters)
{
return new CuePlaySoundSpec(this, parameters);
}
}
public class CuePlaySoundSpec : GameplayCueDurationalSpec<CuePlaySound>
{
private AudioSource _audioSource;
public CuePlaySoundSpec(CuePlaySound cue, GameplayCueParameters parameters) : base(cue,
parameters)
{
if (cue.isAttachToOwner)
{
_audioSource = Owner.gameObject.GetComponent<AudioSource>();
if (_audioSource == null)
{
_audioSource = Owner.gameObject.AddComponent<AudioSource>();
}
}
else
{
var soundRoot = new GameObject("SoundRoot");
soundRoot.transform.position = Owner.transform.position;
_audioSource = soundRoot.AddComponent<AudioSource>();
}
}
public override void OnAdd()
{
_audioSource.clip = cue.soundEffect;
_audioSource.Play();
}
public override void OnRemove()
{
if (!cue.isAttachToOwner)
{
Object.Destroy(_audioSource.gameObject);
}else
{
_audioSource.Stop();
_audioSource.clip = null;
}
}
public override void OnGameplayEffectActivate()
{
}
public override void OnGameplayEffectDeactivate()
{
}
public override void OnTick()
{
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c3aca85c2044621903d913d5bd792b1
timeCreated: 1703670793

View File

@@ -0,0 +1,146 @@
using GAS.General;
using Sirenix.OdinInspector;
using UnityEngine;
namespace GAS.Runtime
{
public class CueVFX : GameplayCueDurational
{
[BoxGroup]
[LabelText(GASTextDefine.CUE_VFX_PREFAB)]
public GameObject VfxPrefab;
[BoxGroup]
[LabelText(GASTextDefine.CUE_ATTACH_TO_OWNER)]
public bool IsAttachToTarget = true;
[BoxGroup]
[LabelText(GASTextDefine.CUE_VFX_OFFSET)]
public Vector3 Offset;
[BoxGroup]
[LabelText(GASTextDefine.CUE_VFX_ROTATION)]
public Vector3 Rotation;
[BoxGroup]
[LabelText(GASTextDefine.CUE_VFX_SCALE)]
public Vector3 Scale = Vector3.one;
[BoxGroup]
[LabelText(GASTextDefine.CUE_VFX_ACTIVE_WHEN_ADDED)]
public bool ActiveWhenAdded = false;
public override GameplayCueDurationalSpec CreateSpec(GameplayCueParameters parameters)
{
return new CueVFXSpec(this, parameters);
}
#if UNITY_EDITOR
private GameObject _effectPreviewInstance;
public override void OnEditorPreview(GameObject preview, int frameIndex, int startFrame, int endFrame)
{
if (VfxPrefab == null) return;
if (frameIndex >= startFrame && frameIndex <= endFrame)
{
if (_effectPreviewInstance != null && _effectPreviewInstance.name != VfxPrefab.name)
{
DestroyImmediate(_effectPreviewInstance);
_effectPreviewInstance = null;
}
if (_effectPreviewInstance == null)
{
_effectPreviewInstance = Instantiate(VfxPrefab, preview.transform);
_effectPreviewInstance.name = VfxPrefab.name;
_effectPreviewInstance.transform.localPosition = Offset;
_effectPreviewInstance.transform.localEulerAngles = Rotation;
_effectPreviewInstance.transform.localScale = Scale;
}
// 模拟例子的播放
var particleSystems = _effectPreviewInstance.GetComponentsInChildren<ParticleSystem>();
foreach (var ps in particleSystems)
{
var t = (frameIndex - startFrame) / GASTimer.FrameRate;
ps.Simulate(t);
}
}
else
{
if (_effectPreviewInstance != null)
{
DestroyImmediate(_effectPreviewInstance);
_effectPreviewInstance = null;
}
}
}
#endif
}
public class CueVFXSpec : GameplayCueDurationalSpec<CueVFX>
{
private GameObject _vfxInstance;
public CueVFXSpec(CueVFX cue, GameplayCueParameters parameters) : base(cue,
parameters)
{
}
public override void OnAdd()
{
if (cue.VfxPrefab != null)
{
_vfxInstance = cue.IsAttachToTarget
? Object.Instantiate(cue.VfxPrefab, Owner.transform)
: Object.Instantiate(cue.VfxPrefab, Owner.transform.position, Quaternion.identity);
_vfxInstance.transform.localPosition = cue.Offset;
_vfxInstance.transform.localEulerAngles = cue.Rotation;
_vfxInstance.transform.localScale = cue.Scale;
_vfxInstance.SetActive(cue.ActiveWhenAdded);
}
else
{
#if UNITY_EDITOR
Debug.LogError("VFX prefab is null!");
#endif
}
}
public override void OnRemove()
{
if (_vfxInstance != null)
{
Object.Destroy(_vfxInstance);
}
}
public override void OnGameplayEffectActivate()
{
if (_vfxInstance != null)
{
_vfxInstance.SetActive(true);
}
}
public override void OnGameplayEffectDeactivate()
{
if (_vfxInstance != null)
{
_vfxInstance.SetActive(false);
}
}
public override void OnTick()
{
}
public void SetVisible(bool visible)
{
if (_vfxInstance != null)
{
_vfxInstance.SetActive(visible);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1261c2b342e03224ebc341132b01bf92
timeCreated: 1703670852