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