From 712954f1c4ab6e54d3d04b166546e1234dd48fd2 Mon Sep 17 00:00:00 2001
From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Date: Thu, 24 Apr 2025 07:39:40 -0400
Subject: [PATCH] Turnstiles (#36313)
* construction rotation fix
* Turnstiles
* renaming
* review-slarticodefast-1
* mild attempts to fix (sorry sloth)
* move some more shit
* Remove engine dependency
* grid agnostic
* remove debug string
* fix json
* Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
* Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
* remove pass delay for mispredict reasons.
* most minor of changes
* Give directional indicator on examine
---------
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
---
Content.Client/Doors/TurnstileSystem.cs | 75 ++++++++++
.../Doors/Systems/TurnstileSystem.cs | 6 +
.../Doors/Components/TurnstileComponent.cs | 71 ++++++++++
.../Doors/Systems/SharedTurnstileSystem.cs | 131 ++++++++++++++++++
.../Movement/Pulling/Systems/PullingSystem.cs | 10 ++
.../en-US/doors/components/turnstile.ftl | 1 +
.../Entities/Structures/Doors/turnstile.yml | 77 ++++++++++
.../Graphs/structures/turnstile.yml | 33 +++++
.../Recipes/Construction/structures.yml | 17 +++
.../Structures/Doors/turnstile.rsi/arrow.png | Bin 0 -> 543 bytes
.../Structures/Doors/turnstile.rsi/deny.png | Bin 0 -> 1960 bytes
.../Structures/Doors/turnstile.rsi/meta.json | 65 +++++++++
.../Doors/turnstile.rsi/operate.png | Bin 0 -> 3481 bytes
.../Doors/turnstile.rsi/turnstile.png | Bin 0 -> 1442 bytes
.../Doors/turnstile.rsi/turnstile_map.png | Bin 0 -> 1832 bytes
15 files changed, 486 insertions(+)
create mode 100644 Content.Client/Doors/TurnstileSystem.cs
create mode 100644 Content.Server/Doors/Systems/TurnstileSystem.cs
create mode 100644 Content.Shared/Doors/Components/TurnstileComponent.cs
create mode 100644 Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
create mode 100644 Resources/Locale/en-US/doors/components/turnstile.ftl
create mode 100644 Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/deny.png
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/meta.json
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/operate.png
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png
create mode 100644 Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png
diff --git a/Content.Client/Doors/TurnstileSystem.cs b/Content.Client/Doors/TurnstileSystem.cs
new file mode 100644
index 0000000000..6e76ce6aa0
--- /dev/null
+++ b/Content.Client/Doors/TurnstileSystem.cs
@@ -0,0 +1,75 @@
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Examine;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Doors;
+
+///
+public sealed class TurnstileSystem : SharedTurnstileSystem
+{
+ [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
+
+ private static EntProtoId _examineArrow = "TurnstileArrow";
+
+ private const string AnimationKey = "Turnstile";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAnimationCompleted);
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ private void OnAnimationCompleted(Entity ent, ref AnimationCompletedEvent args)
+ {
+ if (args.Key != AnimationKey)
+ return;
+
+ if (!TryComp(ent, out var sprite))
+ return;
+ sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState));
+ }
+
+ private void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0));
+ }
+
+ protected override void PlayAnimation(EntityUid uid, string stateId)
+ {
+ if (!TryComp(uid, out var animation) || !TryComp(uid, out var sprite))
+ return;
+ var ent = (uid, animation);
+
+ if (_animationPlayer.HasRunningAnimation(animation, AnimationKey))
+ _animationPlayer.Stop(ent, AnimationKey);
+
+ if (sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(stateId, out var state))
+ return;
+ var animLength = state.AnimationLength;
+
+ var anim = new Animation
+ {
+ AnimationTracks =
+ {
+ new AnimationTrackSpriteFlick
+ {
+ LayerKey = TurnstileVisualLayers.Base,
+ KeyFrames =
+ {
+ new AnimationTrackSpriteFlick.KeyFrame(state.StateId, 0f),
+ },
+ },
+ },
+ Length = TimeSpan.FromSeconds(animLength),
+ };
+
+ _animationPlayer.Play(ent, anim, AnimationKey);
+ }
+}
diff --git a/Content.Server/Doors/Systems/TurnstileSystem.cs b/Content.Server/Doors/Systems/TurnstileSystem.cs
new file mode 100644
index 0000000000..687a97095f
--- /dev/null
+++ b/Content.Server/Doors/Systems/TurnstileSystem.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Doors.Systems;
+
+namespace Content.Server.Doors.Systems;
+
+///
+public sealed class TurnstileSystem : SharedTurnstileSystem;
diff --git a/Content.Shared/Doors/Components/TurnstileComponent.cs b/Content.Shared/Doors/Components/TurnstileComponent.cs
new file mode 100644
index 0000000000..5f19dd3e0b
--- /dev/null
+++ b/Content.Shared/Doors/Components/TurnstileComponent.cs
@@ -0,0 +1,71 @@
+using Content.Shared.Doors.Systems;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Doors.Components;
+
+///
+/// This is used for a condition door that allows entry only through a single side.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedTurnstileSystem))]
+public sealed partial class TurnstileComponent : Component
+{
+ ///
+ /// A whitelist of the things this turnstile can choose to block or let through.
+ /// Things not in this whitelist will be ignored by default.
+ ///
+ [DataField]
+ public EntityWhitelist? ProcessWhitelist;
+
+ ///
+ /// The next time at which the resist message can show.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextResistTime;
+
+ ///
+ /// Maintained hashset of entities currently passing through the turnstile.
+ ///
+ [DataField, AutoNetworkedField]
+ public HashSet CollideExceptions = new();
+
+ ///
+ /// default state of the turnstile sprite.
+ ///
+ [DataField]
+ public string DefaultState = "turnstile";
+
+ ///
+ /// animation state of the turnstile spinning.
+ ///
+ [DataField]
+ public string SpinState = "operate";
+
+ ///
+ /// animation state of the turnstile denying entry.
+ ///
+ [DataField]
+ public string DenyState = "deny";
+
+ ///
+ /// Sound to play when the turnstile admits a mob through.
+ ///
+ [DataField]
+ public SoundSpecifier? TurnSound = new SoundPathSpecifier("/Audio/Items/ratchet.ogg");
+
+ ///
+ /// Sound to play when the turnstile denies entry
+ ///
+ [DataField]
+ public SoundSpecifier? DenySound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
+}
+
+[Serializable, NetSerializable]
+public enum TurnstileVisualLayers : byte
+{
+ Base
+}
diff --git a/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs b/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
new file mode 100644
index 0000000000..0af6b91378
--- /dev/null
+++ b/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
@@ -0,0 +1,131 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.Doors.Components;
+using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Doors.Systems;
+
+///
+/// This handles logic and interactions related to
+///
+public abstract partial class SharedTurnstileSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
+ [Dependency] private readonly PullingSystem _pulling = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnPreventCollide);
+ SubscribeLocalEvent(OnStartCollide);
+ SubscribeLocalEvent(OnEndCollide);
+ }
+
+ private void OnPreventCollide(Entity ent, ref PreventCollideEvent args)
+ {
+ if (args.Cancelled || !args.OurFixture.Hard || !args.OtherFixture.Hard)
+ return;
+
+ if (ent.Comp.CollideExceptions.Contains(args.OtherEntity))
+ {
+ args.Cancelled = true;
+ return;
+ }
+
+ // We need to add this in here too for chain pulls
+ if (_pulling.GetPuller(args.OtherEntity) is { } puller && ent.Comp.CollideExceptions.Contains(puller))
+ {
+ ent.Comp.CollideExceptions.Add(args.OtherEntity);
+ Dirty(ent);
+ args.Cancelled = true;
+ return;
+ }
+
+ // unblockables go through for free.
+ if (_entityWhitelist.IsWhitelistFail(ent.Comp.ProcessWhitelist, args.OtherEntity))
+ {
+ args.Cancelled = true;
+ return;
+ }
+
+ if (CanPassDirection(ent, args.OtherEntity))
+ {
+ if (!_accessReader.IsAllowed(args.OtherEntity, ent))
+ return;
+
+ ent.Comp.CollideExceptions.Add(args.OtherEntity);
+ if (_pulling.GetPulling(args.OtherEntity) is { } uid)
+ ent.Comp.CollideExceptions.Add(uid);
+
+ args.Cancelled = true;
+ Dirty(ent);
+ }
+ else
+ {
+ if (_timing.CurTime >= ent.Comp.NextResistTime)
+ {
+ _popup.PopupClient(Loc.GetString("turnstile-component-popup-resist", ("turnstile", ent.Owner)), ent, args.OtherEntity);
+ ent.Comp.NextResistTime = _timing.CurTime + TimeSpan.FromSeconds(0.1);
+ Dirty(ent);
+ }
+ }
+ }
+
+ private void OnStartCollide(Entity ent, ref StartCollideEvent args)
+ {
+ if (!ent.Comp.CollideExceptions.Contains(args.OtherEntity))
+ {
+ if (CanPassDirection(ent, args.OtherEntity))
+ {
+ if (!_accessReader.IsAllowed(args.OtherEntity, ent))
+ {
+ _audio.PlayPredicted(ent.Comp.DenySound, ent, args.OtherEntity);
+ PlayAnimation(ent, ent.Comp.DenyState);
+ }
+ }
+
+ return;
+ }
+ // if they passed through:
+ PlayAnimation(ent, ent.Comp.SpinState);
+ _audio.PlayPredicted(ent.Comp.TurnSound, ent, args.OtherEntity);
+ }
+
+ private void OnEndCollide(Entity ent, ref EndCollideEvent args)
+ {
+ if (!args.OurFixture.Hard)
+ {
+ ent.Comp.CollideExceptions.Remove(args.OtherEntity);
+ Dirty(ent);
+ }
+ }
+
+ protected bool CanPassDirection(Entity ent, EntityUid other)
+ {
+ var xform = Transform(ent);
+ var otherXform = Transform(other);
+
+ var (pos, rot) = _transform.GetWorldPositionRotation(xform);
+ var otherPos = _transform.GetWorldPosition(otherXform);
+
+ var approachAngle = (pos - otherPos).ToAngle();
+ var rotateAngle = rot.ToWorldVec().ToAngle();
+
+ var dif = Math.Min(Math.Abs(approachAngle.Theta - rotateAngle.Theta), Math.Abs(rotateAngle.Theta - approachAngle.Theta));
+ return dif < Math.PI / 4;
+ }
+
+ protected virtual void PlayAnimation(EntityUid uid, string stateId)
+ {
+
+ }
+}
diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
index 6ac0460f8e..369225df2d 100644
--- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
+++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
@@ -341,6 +341,16 @@ public sealed class PullingSystem : EntitySystem
return Resolve(puller, ref component, false) && component.Pulling != null;
}
+ public EntityUid? GetPuller(EntityUid puller, PullableComponent? component = null)
+ {
+ return !Resolve(puller, ref component, false) ? null : component.Puller;
+ }
+
+ public EntityUid? GetPulling(EntityUid puller, PullerComponent? component = null)
+ {
+ return !Resolve(puller, ref component, false) ? null : component.Pulling;
+ }
+
private void OnReleasePulledObject(ICommonSession? session)
{
if (session?.AttachedEntity is not { Valid: true } player)
diff --git a/Resources/Locale/en-US/doors/components/turnstile.ftl b/Resources/Locale/en-US/doors/components/turnstile.ftl
new file mode 100644
index 0000000000..16dca94244
--- /dev/null
+++ b/Resources/Locale/en-US/doors/components/turnstile.ftl
@@ -0,0 +1 @@
+turnstile-component-popup-resist = {CAPITALIZE(THE($turnstile))} resists your efforts!
diff --git a/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml b/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
new file mode 100644
index 0000000000..6d675c5928
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
@@ -0,0 +1,77 @@
+- type: entity
+ id: Turnstile
+ parent: BaseStructure
+ name: turnstile
+ description: A mechanical door that permits one-way access and prevents tailgating.
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/turnstile.rsi
+ snapCardinals: true
+ drawdepth: Doors
+ layers:
+ - state: turnstile
+ map: [ "enum.TurnstileVisualLayers.Base" ]
+ - type: AnimationPlayer
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.49,-0.49,0.49,0.49" # same dimensions as a door for tall turnstile, prevents objects being thrown through
+ density: 100
+ mask:
+ - FullTileMask
+ layer:
+ - AirlockLayer
+ fix2:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.50,-0.50,0.50,0.50" # same dimensions as a door for tall turnstile, prevents objects being thrown through
+ hard: false
+ mask:
+ - FullTileMask
+ layer:
+ - AirlockLayer
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/smash.ogg"
+ - type: InteractionOutline
+ - type: Turnstile
+ processWhitelist:
+ components:
+ - MobState # no mobs
+ - Pullable # no dragging things in
+ - type: Appearance
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: StrongMetallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 500
+ behaviors:
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - type: Construction
+ graph: Turnstile
+ node: turnstile
+
+- # Spawned by the client-side turnstile examine code to indicate the direction to pass through.
+ type: entity
+ id: TurnstileArrow
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: Sprite
+ sprite: Structures/Doors/turnstile.rsi
+ color: "#FFFFFFBB"
+ layers:
+ - state: arrow
+ offset: 0, 0.78125
+ - type: TimedDespawn
+ lifetime: 2
+ - type: Tag
+ tags:
+ - HideContextMenu
diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml
new file mode 100644
index 0000000000..cc1b8efb6a
--- /dev/null
+++ b/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml
@@ -0,0 +1,33 @@
+- type: constructionGraph
+ id: Turnstile
+ start: start
+ graph:
+ - node: start
+ actions:
+ - !type:DeleteEntity { }
+ edges:
+ - to: turnstile
+ completed:
+ - !type:SnapToGrid
+ steps:
+ - material: MetalRod
+ amount: 4
+ doAfter: 6
+ - material: Steel
+ amount: 1
+ doAfter: 2
+
+ - node: turnstile
+ entity: Turnstile
+ edges:
+ - to: start
+ completed:
+ - !type:SpawnPrototype
+ prototype: PartRodMetal1
+ amount: 4
+ - !type:DeleteEntity
+ steps:
+ - tool: Welding
+ doAfter: 4.0
+ - tool: Cutting
+ doAfter: 2.0
diff --git a/Resources/Prototypes/Recipes/Construction/structures.yml b/Resources/Prototypes/Recipes/Construction/structures.yml
index 7602a6cb11..11579ac9ba 100644
--- a/Resources/Prototypes/Recipes/Construction/structures.yml
+++ b/Resources/Prototypes/Recipes/Construction/structures.yml
@@ -796,6 +796,23 @@
conditions:
- !type:TileNotBlocked
+- type: construction
+ name: turnstile
+ id: Turnstile
+ graph: Turnstile
+ startNode: start
+ targetNode: turnstile
+ category: construction-category-structures
+ description: A mechanical door that permits one-way access and prevents tailgating.
+ icon:
+ sprite: Structures/Doors/turnstile.rsi
+ state: turnstile
+ objectType: Structure
+ placementMode: SnapgridCenter
+ canBuildInImpassable: false
+ conditions:
+ - !type:TileNotBlocked
+
- type: construction
name: shutter
id: Shutters
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png b/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..6332f6b32619791298624df56d16518b06bcbf3e
GIT binary patch
literal 543
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
R#rf9)m>XX_UvB6T`F}ZY2L1p5
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png
new file mode 100644
index 0000000000000000000000000000000000000000..48c85577e71f296bf745e0b0ad8e7df1ffebb0e8
GIT binary patch
literal 1442
zcmV;T1zq}yP)G=3qZqU-wqALoA!?MT!iAe9oht%?A#BiHZ?si4?)r6qX
z(UI)KtlVL@OAt1Tg)H2z_w)$wrAwONF*FN+M8TPrU@%CYXYSM23(*k7N=8NoebUik
z%AN4g{{B7*Arb-rkLB-0*609opJbgs6Mb|0wmIdziOu04i_J!Mr!ye{PM&{w7&rj{
z_n&pgPnwvR5QwoQ>Uw+wIl{5EwMDD`Ra#qHlg*LrpuxdGfh5MDeyuo}VYf@5@jI&yg
zwd3RCqPn_Tf~c&lq~YP=6A%1ds;a6C!*JvC8zL<&jS311=-&N%)Y#Z)Ds+$xHeKb7
z1h5qlw6hgi|JM2Q!b~2(Vpoz&ZUl=Hk*w@&{aXUa+qLL903K*-Ym+=+kCq@5z_s}H
zdc6`HDgesW_`elB1b0W{$wVo@<_8A{dII+K^@--@W(|3OF@VJPe*&-+u+r165Z&F5
z@IZ$N9#EE}CCUSTtgpudc`rX8~kn
zWJF*QT#JSc(L0#=P~X@%*6mP=F@y!*XrO49%Oyc$9NePH`~6oI=-KC;vnnC5}w<8V9=#EMxSfQR52R6?T1F$5vi0U!rJMiB5o
zYip~>&(D|fgWJ)=jZlgb#5`pIdUY=_E3S3iGr}hT{J+JQk&3@kplThhzrSBJH8p7}
zIaS<^q`bUbZn(U>OkMOG(N}Z=AXdNw?d|QRs2B;U4k;t$AM$*4z(D}Kn=48O@Y0JG
zzLJ^e0hA+Luju%nq`0`4-u3ykTcxF9Z*NbAH7*I0{TkhQ$cYrAdUSMDz&a>Vvx2LMc|(ArIhkg&vBO04O}7G8_NLaz#o?3QbNJ_l-m)
zry?!s0S1054}5&(ieU&;k|7Lcj$?JAD(Y6rZ9|Rr{>V?Rm*?exSsuV!z{=L|a$R#~
z8O{e-EjmwyKpx+CAmWV%5N^f(@6!sb{hjNVn1x01m4@rB4+5i9m07*qoM6N<$f>u7Wng9R*
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb3fcc5c31e20199b87146eb55e4a6cd56d33535
GIT binary patch
literal 1832
zcmV+@2iN$CP)MMVX5
zc6RFS*~RbGk_Q3-DlILg%a<>^Vl{HI@g&P!ok;}+1$4jrJ{|nzV9a-bojZ3jaDh{H
z!cKm#B>!Mz7qtz(Nu#5qbnMtMmYtnVE|-fI7Z=In@zBD;f@q+wu1+Nig+k(r=T^|T
zx0mX^RR#{&$?uiq0l(6&i!oZ9pBL8Hub}aLV|dWb>;6$a{`+
z{z3G^l`H1Rc_Ut*kKA4_rKF}>>Od;5KQ}k2yJr`_5CGI~O+a7i`t|D!Hs)SZJ^T;J
zK^@1%O4Gh(<<6#Lu_NN0%;LVsK)pJ2y8+DO_~{%nsh!zJ0qou)U2%G%+#3ot{#*
zlm7`50B+vA$%Nf;>Ea529F0MGAi;HZYF0c$oq2Azs1IkAmX;>$OumnI+uGXLwr$%)
zC$?@Zj;F`
z1TZ$qXnah0|NULNm>D~S#QkZ0(GVnV1=%?{y0V@8LI4F5Zq)`6hmRaNBI0n$QV0S;
zeSN*Ik>g(;*Ol}0^VKPR`0%0W*|3{mHGw-V`o#o7jC3F~Gc)2|M@NUg7f_=i*)Ca5{DZM;;>f200u9w;X;Uoe}X|>IUfE*pyd|=
zclgV&a=2w{|mF)gg}CkidU5WqMu4`uLm^`pm?T3cHg!VOdr;iRgn
zN_{2=8)@JOIWm4jg<;D)^r!>7b?cU{zE%EMu);3{00$)3;=r|Q*BD-c4dMrnYX#^q
zmZ*NeUo2n{n-SY(c-#L*554qCD%GCPqD1&3O@I}C!*#%p9Xm|fU?jDV6~`AALQ&TN
zX89w*3jY%VFdK&x1(vHuFvG9)LGpjAj#%=mJ^+gm0CD)xp+l^&uu#~Kk8e0|<(`?A
z3;rPX`60Dzl^=uE3c>2&wfsT=!+-0wOWGfRtK3*5g7Wfm(cr+q0G*(hiN1UMEFOL-
z1ha#W#BX>WaP;U=LmP~DkpuDr+>yx*4gkk=(qIW|i9esP@=1ta6~Jq+x~Xk2avTm{
zUN)U?Vs7T9qM{<|?&+oj`wv94+YK8w&_~V9t_vGCvZ<*lq1NF}&@O%C+BH#b*=EW}>`@8SQ+$w_K%
zZl>~gaCGIPPcQsQUwv3g^pnD6C%+KDbRMHBu1UY%R>=86fk|R{7zsPX9$QLJ8=KKl
zY*?gc#d8qLPN%1*(~TQ9*n{R;Wj1+}4<4aiWe#3QucGVOKCr+JU{&YW;l$22C
z@13-J_wI=AR903pJ_D{(Wn~P5+LNA7JwJ0VyxO>aGWk^j46RX4Ud*loAaO`OQalgG
z*X!3ad2*AHk)fXS*v+pBV0>wRAkXF3ty`y5;9e6ife_c4ni|nLsHV^36E+I9j0GHF
zwH6oC+x`9GUFnVO=2v^6jm7e;9d@+9C5*HAY3@iscCM+3eat(7vwJr;;Z)crUw6lX
zbWZYPhL{0O$L-=bTnBu1_;5@Wluia}D7|hpK;lqW)YpFLv76tE7}@w6QQhm^;)dCM
z0M6n)T>29{u5!;3vpzcOTB;{iFf%iga&mHP<@e$PA-<8J6M+Dk2DY1Amcstb%)lzH
za#e$aG|N>G+thQM41>3uUzmXGM0`rTo){d#Y2udWQU3(pU*i{Hr)aPkA3`~W?i`~W9EK!KAV;N%BH
zhm#-RdUK%bg#PNPdFl{DAP}2qV~$AD|V&isc8G{VJQx
z{D3FQ`74+op#3TvP34l6AE3=tQOn#3u4sNh_^0W3kw|_(q<+Q2A6I@rA`TEpG(R95
z&t!hUa`FGa^8=QF-^mZaA16P+$q&%O$q#Vy1DyN-CqE!ogHC>clON#Z2Rs$|0sjHw
WmMV`eM+$}j0000