Add Cyborg crew indicator (#37038)

* Initial commit

* Include uncertain crew and make it work for AI

* Add new definition to Silicon Rules 8

* Update based on review

* Remove Cluwne from job list

* ProtoIdify

* Update and also make monkey/corgi show IDs

* Remove unnecessary property

* Remove redundant code

* Carrrrd

* cleanup

* Nicer code

* Update to fix the spawn bug + agent ID

* Fix new icons

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
SlamBamActionman 2026-01-29 07:59:06 +01:00 committed by Coryler
parent 4ddd649595
commit 53fcd12147
25 changed files with 333 additions and 62 deletions

View File

@ -0,0 +1,39 @@
using Content.Client.Overlays;
using Content.Shared.Access.Systems;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Access.Systems;
public sealed class JobStatusSystem : SharedJobStatusSystem
{
[Dependency] private readonly ShowJobIconsSystem _showJobIcons = default!;
[Dependency] private readonly ShowCrewIconsSystem _showCrewIcons = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private static readonly ProtoId<SecurityIconPrototype> CrewBorderIcon = "CrewBorderIcon";
private static readonly ProtoId<SecurityIconPrototype> CrewUncertainBorderIcon = "CrewUncertainBorderIcon";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JobStatusComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
// show the status icons if the player has the correponding HUDs
private void OnGetStatusIconsEvent(Entity<JobStatusComponent> ent, ref GetStatusIconsEvent ev)
{
if (_showJobIcons.IsActive && ent.Comp.JobStatusIcon != null)
ev.StatusIcons.Add(_prototype.Index(ent.Comp.JobStatusIcon));
if (_showCrewIcons.IsActive)
{
if (_showCrewIcons.UncertainCrewBorder)
ev.StatusIcons.Add(_prototype.Index(CrewUncertainBorderIcon));
else if (ent.Comp.IsCrew)
ev.StatusIcons.Add(_prototype.Index(CrewBorderIcon));
}
}
}

View File

@ -15,7 +15,7 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
[Dependency] private readonly IPlayerManager _player = default!;
[ViewVariables]
protected bool IsActive;
public bool IsActive { get; private set; }
protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET;
public override void Initialize()

View File

@ -0,0 +1,34 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
namespace Content.Client.Overlays;
// The GetStatusIconsEvent subscription is handled in JobStatusSystem
public sealed class ShowCrewIconsSystem : EquipmentHudSystem<ShowCrewIconsComponent>
{
public bool UncertainCrewBorder = false;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ShowCrewIconsComponent, AfterAutoHandleStateEvent>(OnHandleState);
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowCrewIconsComponent> component)
{
base.UpdateInternal(component);
UncertainCrewBorder = false;
foreach (var comp in component.Components)
{
if (comp.UncertainCrewBorder)
UncertainCrewBorder = true;
}
}
private void OnHandleState(Entity<ShowCrewIconsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay();
}
}

View File

@ -1,59 +1,6 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Overlays;
using Content.Shared.PDA;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
private static readonly ProtoId<JobIconPrototype> JobIconForNoId = "JobIconNoId";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusIconComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent ev)
{
if (!IsActive)
return;
var iconId = JobIconForNoId;
if (_accessReader.FindAccessItemsInventory(uid, out var items))
{
foreach (var item in items)
{
// ID Card
if (TryComp<IdCardComponent>(item, out var id))
{
iconId = id.JobIcon;
break;
}
// PDA
if (TryComp<PdaComponent>(item, out var pda)
&& pda.ContainedId != null
&& TryComp(pda.ContainedId, out id))
{
iconId = id.JobIcon;
break;
}
}
}
if (_prototype.Resolve(iconId, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
else
Log.Error($"Invalid job icon prototype: {iconPrototype}");
}
}
// The GetStatusIconsEvent subscription is handled in JobStatusSystem
public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponent>;

View File

@ -96,7 +96,7 @@ public sealed class StatusIconOverlay : Overlay
countL++;
}
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetL - proto.Offset) / EyeManager.PixelsPerMeter;
xOffset = -(bounds.Width + sprite.Offset.X) / 2f;
xOffset = -(bounds.Width + sprite.Offset.X) / 2f + (float)proto.OffsetHorizontal / EyeManager.PixelsPerMeter;
}
else
@ -109,7 +109,7 @@ public sealed class StatusIconOverlay : Overlay
countR++;
}
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float)(accOffsetR - proto.Offset) / EyeManager.PixelsPerMeter;
xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)texture.Width / EyeManager.PixelsPerMeter;
xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float)(texture.Width - proto.OffsetHorizontal) / EyeManager.PixelsPerMeter;
}

View File

@ -30,6 +30,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedJobStatusSystem _jobStatus = default!;
public override void Initialize()
{
@ -197,6 +198,8 @@ namespace Content.Server.Access.Systems
if (TryFindJobProtoFromIcon(jobIcon, out var job))
_cardSystem.TryChangeJobDepartment(uid, job, idCard);
_jobStatus.UpdateStatus(Transform(uid).ParentUid);
}
private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)

View File

@ -0,0 +1,5 @@
using Content.Shared.Access.Systems;
namespace Content.Server.Access.Systems;
public sealed class JobStatusSystem : SharedJobStatusSystem;

View File

@ -9,6 +9,7 @@ using Content.Shared.Emag.Systems;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Overlays;
using Content.Shared.Radio.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
@ -34,6 +35,8 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private static readonly ProtoId<SiliconLawsetPrototype> DefaultCrewLawset = "Crewsimov";
/// <inheritdoc/>
public override void Initialize()
{
@ -305,6 +308,11 @@ public sealed class SiliconLawSystem : SharedSiliconLawSystem
while (query.MoveNext(out var update))
{
if (TryComp<ShowCrewIconsComponent>(update, out var crewIconComp))
{
crewIconComp.UncertainCrewBorder = DefaultCrewLawset != provider.Laws;
Dirty(update, crewIconComp);
}
SetLaws(lawset.Laws, update, provider.LawUploadSound);
RaiseLocalEvent(new AILawUpdatedEvent(update, provider.Laws)); // DeltaV - Subvert AI objective
}

View File

@ -8,7 +8,7 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Access.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
[AutoGenerateComponentState(true)]
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)]
public sealed partial class IdCardComponent : Component
{

View File

@ -26,6 +26,7 @@ public abstract class SharedIdCardSystem : EntitySystem
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedJobStatusSystem _jobStatus = default!;
[Dependency] private readonly SharedSubdermalIdCardSystem _subdermalId = default!; // DeltaV - Subdermal ID Cards
// CCVar.
@ -37,6 +38,7 @@ public abstract class SharedIdCardSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<IdCardComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
SubscribeLocalEvent<EntityRenamedEvent>(OnRename);
@ -79,6 +81,15 @@ public abstract class SharedIdCardSystem : EntitySystem
ev.Handled = true;
}
private void OnHandleState(Entity<IdCardComponent> ent, ref AfterAutoHandleStateEvent args)
{
// Try to update the job status icon of the player owning the ID, if any.
if (HasComp<PdaComponent>(Transform(ent).ParentUid))
_jobStatus.UpdateStatus(Transform(Transform(ent).ParentUid).ParentUid); //ID is inside a PDA
else
_jobStatus.UpdateStatus(Transform(ent).ParentUid); //ID is held/directly in the ID slot
}
/// <summary>
/// Attempt to find an ID card on an entity. This will look in the entity itself, in the entity's hands, and
/// in the entity's inventory.

View File

@ -0,0 +1,65 @@
using Content.Shared.Access.Components;
using Content.Shared.Hands;
using Content.Shared.Inventory.Events;
using Content.Shared.PDA;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.Access.Systems;
public abstract class SharedJobStatusSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private static readonly ProtoId<JobIconPrototype> JobIconForNoId = "JobIconNoId";
public override void Initialize()
{
base.Initialize();
// if the mob picks up, drops or (un)equips a pda or Id card then update their crew status
SubscribeLocalEvent<JobStatusComponent, DidEquipEvent>((uid, comp, _) => UpdateStatus((uid, comp)));
SubscribeLocalEvent<JobStatusComponent, DidEquipHandEvent>((uid, comp, _) => UpdateStatus((uid, comp)));
SubscribeLocalEvent<JobStatusComponent, DidUnequipEvent>((uid, comp, _) => UpdateStatus((uid, comp)));
SubscribeLocalEvent<JobStatusComponent, DidUnequipHandEvent>((uid, comp, _) => UpdateStatus((uid, comp)));
}
/// <summary>
/// Updates this mob's job and crew status depending on their currently equipped or held pda or Id card.
/// </summary>
public void UpdateStatus(Entity<JobStatusComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
var iconId = JobIconForNoId;
if (_accessReader.FindAccessItemsInventory(ent.Owner, out var items))
{
foreach (var item in items)
{
// ID Card
if (TryComp<IdCardComponent>(item, out var id))
{
iconId = id.JobIcon;
break;
}
// PDA
if (TryComp<PdaComponent>(item, out var pda)
&& pda.ContainedId != null
&& TryComp(pda.ContainedId, out id))
{
iconId = id.JobIcon;
break;
}
}
}
ent.Comp.JobStatusIcon = iconId;
ent.Comp.IsCrew = _prototype.Index(iconId).IsCrewJob;
Dirty(ent);
}
}

View File

@ -0,0 +1,17 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
namespace Content.Shared.Overlays;
/// <summary>
/// This component allows you to see a crew border icon above mobs. The HUD will include a green border around jobs that are considered crew according to <see cref="JobIconPrototype.IsCrewJob"/>.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class ShowCrewIconsComponent : Component
{
/// <summary>
/// If true, the HUD will include a yellow border around all icons, to indicate crew uncertainty.
/// </summary>
[DataField, AutoNetworkedField]
public bool UncertainCrewBorder = false;
}

View File

@ -1,4 +1,5 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Containers;
@ -8,6 +9,7 @@ namespace Content.Shared.PDA
{
[Dependency] protected readonly ItemSlotsSystem ItemSlotsSystem = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] private readonly SharedJobStatusSystem _jobStatus = default!;
public override void Initialize()
{
@ -46,6 +48,7 @@ namespace Content.Shared.PDA
pda.ContainedId = args.Entity;
UpdatePdaAppearance(uid, pda);
UpdateJobStatus(uid);
}
protected virtual void OnItemRemoved(EntityUid uid, PdaComponent pda, EntRemovedFromContainerMessage args)
@ -54,6 +57,7 @@ namespace Content.Shared.PDA
pda.ContainedId = null;
UpdatePdaAppearance(uid, pda);
UpdateJobStatus(uid);
}
private void OnGetAdditionalAccess(EntityUid uid, PdaComponent component, ref GetAdditionalAccessEvent args)
@ -67,6 +71,14 @@ namespace Content.Shared.PDA
Appearance.SetData(uid, PdaVisuals.IdCardInserted, pda.ContainedId != null);
}
// update the status icon of the player that has the pda currently equipped
private void UpdateJobStatus(EntityUid uid)
{
// Only the player who has the pda currently equipped can insert or remove Ids
var parent = Transform(uid).ParentUid;
_jobStatus.UpdateStatus(parent);
}
public virtual void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
{
// This does nothing yet while I finish up PDA prediction

View File

@ -1,5 +1,6 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Mind;
using Content.Shared.Overlays;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Stunnable;
@ -69,12 +70,28 @@ public abstract partial class SharedSiliconLawSystem : EntitySystem
protected virtual void EnsureSubvertedSiliconRole(EntityUid mindId)
{
if (TryComp<MindComponent>(mindId, out var mind))
{
var owner = mind.OwnedEntity;
if (TryComp<ShowCrewIconsComponent>(owner, out var crewIconComp))
{
crewIconComp.UncertainCrewBorder = true;
Dirty(owner.Value, crewIconComp);
}
}
}
protected virtual void RemoveSubvertedSiliconRole(EntityUid mindId)
{
if (TryComp<MindComponent>(mindId, out var mind))
{
var owner = mind.OwnedEntity;
if (TryComp<ShowCrewIconsComponent>(owner, out var crewIconComp))
{
crewIconComp.UncertainCrewBorder = false;
Dirty(owner.Value, crewIconComp);
}
}
}
}

View File

@ -0,0 +1,27 @@
using Content.Shared.Overlays;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.StatusIcon.Components;
/// <summary>
/// Used to indicate a mob can have their job status read by HUDs.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class JobStatusComponent : Component
{
/// <summary>
/// The currently displayed status icon for the mobs's job.
/// Visible with <see cref="ShowJobIconsComponent"/>
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<JobIconPrototype>? JobStatusIcon = "JobIconNoId";
/// <summary>
/// If the mob is currently considered crew.
/// This is true depending on their current job icon.
/// Visible with <see cref="ShowCrewIconsComponent"/>
/// </summary>
[DataField, AutoNetworkedField]
public bool IsCrew;
}

View File

@ -68,6 +68,12 @@ public partial class StatusIconData : IComparable<StatusIconData>
[DataField]
public int Offset = 0;
/// <summary>
/// Offset of the status icon, left and right only.
/// </summary>
[DataField]
public int OffsetHorizontal = 0;
/// <summary>
/// Sets if the icon should be rendered with or without the effect of lighting.
/// </summary>
@ -118,6 +124,12 @@ public sealed partial class JobIconPrototype : StatusIconPrototype, IInheritingP
/// </summary>
[DataField]
public bool AllowSelection = true;
/// <summary>
/// Should this job icon be considered a crew job for silicons?
/// </summary>
[DataField]
public bool IsCrewJob = true;
}
/// <summary>

View File

@ -33,6 +33,7 @@
False: { visible: false }
- type: StatusIcon
bounds: -0.5,-0.5,0.5,0.5
- type: JobStatus
- type: RotationVisuals
defaultRotation: 90
horizontalRotation: 90

View File

@ -372,6 +372,7 @@
- type: Wires # DeltaV: NT borgs have a circuit panel
layoutId: Cyborg
- type: ShowJobIcons
- type: ShowCrewIcons
- type: InteractionPopup
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
@ -439,6 +440,7 @@
- type: ShowJobIcons
- type: Fiber # DeltaV - derelict borgs have degraded plasteel fibers
fiberMaterial: fibers-degraded-plasteel
- type: ShowCrewIcons
- type: entity
id: BaseBorgChassisSyndicateDerelict #For assault borg and maybe others in time
@ -452,6 +454,7 @@
- type: IonStormTarget
chance: 1
- type: ShowJobIcons
- type: ShowCrewIcons
- type: NpcFactionMember # They're still syndicate even if they can't listen to the radio or see icons
factions:
- Syndicate
@ -598,6 +601,7 @@
- type: AccessReader
access: [["Xenoborg"]]
- type: ShowJobIcons # not sure if it is needed
- type: ShowCrewIcons
- type: InteractionPopup
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg

View File

@ -1572,6 +1572,8 @@
- !type:WashCreamPie
- type: Crawler
- type: StatusIcon
bounds: -0.5,-0.5,0.5,0.5
- type: JobStatus # marks them as crew
- type: entity
name: monkey
@ -4106,8 +4108,9 @@
- VimPilot
- CanPilot
- DoorBumpOpener
- type: StatusIcon # marks them as crew
- type: StatusIcon
bounds: -0.5,-0.5,0.5,0.5
- type: JobStatus # marks them as crew
- type: NpcFactionMember
factions:
- NanoTrasen

View File

@ -99,6 +99,7 @@
title: comms-console-announcement-title-station-ai
color: "#5ed7aa"
- type: ShowJobIcons
- type: ShowCrewIcons
- type: DamagedSiliconAccent
startPowerCorruptionAtCharIdx: 4
maxPowerCorruptionAtCharIdx: 20
@ -146,6 +147,7 @@
enum.SiliconLawsUiKey.Key:
type: SiliconLawBoundUserInterface
- type: ShowJobIcons
- type: ShowCrewIcons
# Ai
- type: entity

View File

@ -376,6 +376,7 @@
sprite: *icon-rsi
state: Borg
jobName: job-name-borg
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -384,6 +385,7 @@
sprite: *icon-rsi
state: StationAi
jobName: job-name-station-ai
isCrewJob: false
# Bad guys
@ -394,6 +396,7 @@
sprite: *icon-rsi
state: Cluwne
jobName: job-name-cluwne
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -402,6 +405,7 @@
sprite: *icon-rsi
state: Ninja
jobName: job-name-ninja
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -410,6 +414,7 @@
sprite: *icon-rsi
state: Pirate
jobName: job-name-pirate
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -418,6 +423,7 @@
sprite: *icon-rsi-dv # DeltaV - resprite prisoner jobicon
state: Prisoner
jobName: job-name-prisoner
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -426,6 +432,7 @@
sprite: *icon-rsi
state: Syndicate
jobName: job-name-syndicate
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -434,6 +441,7 @@
sprite: *icon-rsi
state: SyndicateCommander
jobName: job-name-syndicate-commander
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -442,6 +450,7 @@
sprite: *icon-rsi
state: SyndicateCorpsman
jobName: job-name-syndicate-corpsman
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -450,6 +459,7 @@
sprite: *icon-rsi
state: SyndicateOperative
jobName: job-name-syndicate-operative
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -458,6 +468,7 @@
sprite: *icon-rsi
state: Wizard
jobName: job-name-wizard
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -466,6 +477,7 @@
sprite: *icon-rsi
state: Zombie
jobName: job-name-zombie
isCrewJob: false
# Admin
@ -594,6 +606,7 @@
sprite: *icon-rsi
state: NoId
jobName: job-name-no-id
isCrewJob: false
- type: jobIcon
parent: JobIcon
@ -602,3 +615,4 @@
sprite: *icon-rsi
state: Unknown
jobName: job-name-unknown
isCrewJob: false

View File

@ -64,3 +64,27 @@
icon:
sprite: /Textures/Interface/Misc/job_icons.rsi
state: MindShield
- type: securityIcon
id: CrewBorderIcon
priority: 2
offset: -4
offsetHorizontal: 4
locationPreference: Right
layer: Mod
isShaded: true
icon:
sprite: /Textures/Interface/Misc/job_icons_borders.rsi
state: CrewBorder
- type: securityIcon
id: CrewUncertainBorderIcon
priority: 2
offset: -4
offsetHorizontal: 4
locationPreference: Right
layer: Mod
isShaded: true
icon:
sprite: /Textures/Interface/Misc/job_icons_borders.rsi
state: CrewUncertainBorder

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,26 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "CrewBorder and CrewUncertainBorder by SlamBamActionman (Github)",
"size": {
"x": 16,
"y": 16
},
"states": [
{
"name": "CrewBorder",
"delays":
[
[1.0,1.0]
]
},
{
"name": "CrewUncertainBorder",
"delays":
[
[1.0,1.0]
]
}
]
}