11using CitizenFX . FiveM . Client ;
22
3+ using vMenu . Enhanced . Data . Ticks ;
34using vMenu . Enhanced . MenuFramework . Localization ;
45using vMenu . Enhanced . Serialization ;
6+ using vMenu . Enhanced . Ticks ;
57
68namespace vMenu . Enhanced . MenuFramework ;
79
@@ -26,6 +28,27 @@ public static class Notifications
2628 /// <summary>Thirty seconds of waiting for a spawn, after which the message is shown regardless.</summary>
2729 private const int MaxVisibilityChecks = 60 ;
2830
31+ /// <summary>What the page hands a frozen message back when the pause menu closes, mirrored here.</summary>
32+ private const int PauseGraceMs = 2000 ;
33+
34+ /// <summary>Covers the fade the page plays after a message's time is up.</summary>
35+ private const int ExitGraceMs = 500 ;
36+
37+ private const long PauseCheckIntervalMs = 100 ;
38+
39+ private const string PausedMessage = """{"type":"notify_pause","paused":true}""" ;
40+
41+ private const string ResumedMessage = """{"type":"notify_pause","paused":false}""" ;
42+
43+ private static TickHandle ? _pauseTick ;
44+
45+ private static bool _paused ;
46+
47+ /// <summary>Game time by which every message sent so far has had its say.</summary>
48+ private static long _liveUntil ;
49+
50+ private static long _polledAt ;
51+
2952 public static void Info ( MenuText text , int durationMs = DefaultDurationMs ) =>
3053 Show ( NotificationStyle . Info , text , durationMs ) ;
3154
@@ -73,7 +96,75 @@ public static void Show(
7396 return ;
7497 }
7598
76- Native . SendNuiMessage ( BuildMessage ( Name ( style ) , message , durationMs , source ) ) ;
99+ // Read once and sent with the message, so a notification raised during a pause is drawn the
100+ // way the ones already on screen are instead of waiting for the next poll to catch up.
101+ var paused = Native . IsPauseMenuActive ( ) ;
102+
103+ Native . SendNuiMessage ( BuildMessage ( Name ( style ) , message , durationMs , source , paused ) ) ;
104+
105+ Watch ( durationMs , paused ) ;
106+ }
107+
108+ /// <summary>Keeps the pause menu watch alive for as long as this message can still be on screen.</summary>
109+ // An estimate, never the truth: the page owns the timers, and this side only needs to know when
110+ // watching is pointless. It rounds up on a repeat, which restarts the row and this window with
111+ // it, and falls short only when messages arrive faster than the page shows them, where the ones
112+ // still queued go without their blur and their extra time.
113+ private static void Watch ( int durationMs , bool paused )
114+ {
115+ var until = Native . GetGameTimer ( ) + durationMs + ExitGraceMs ;
116+
117+ if ( until > _liveUntil )
118+ {
119+ _liveUntil = until ;
120+ }
121+
122+ // Registered on the first message rather than from an Initialize, so a client that never
123+ // notifies never lists a loop, and nothing has to be ordered in front of this.
124+ _pauseTick ??= TickRegistry . Register (
125+ "Notifications.Pause" ,
126+ PollPause ,
127+ TickRate . Every ( PauseCheckIntervalMs ) ,
128+ ( ) => _liveUntil > Native . GetGameTimer ( ) ,
129+ ( ) => _polledAt = Native . GetGameTimer ( ) ) ;
130+
131+ // The page is told the state on every message, so the poll has nothing left to announce.
132+ _paused = paused ;
133+
134+ _pauseTick . Reevaluate ( ) ;
135+ }
136+
137+ private static void PollPause ( )
138+ {
139+ var now = Native . GetGameTimer ( ) ;
140+ var paused = Native . IsPauseMenuActive ( ) ;
141+
142+ // The page holds its messages for as long as the pause menu is up, so this window holds with them.
143+ if ( paused )
144+ {
145+ _liveUntil += now - _polledAt ;
146+ }
147+
148+ _polledAt = now ;
149+
150+ if ( paused != _paused )
151+ {
152+ _paused = paused ;
153+
154+ // Mirrors the grace the page hands back, so watching does not end while a message is still living on it.
155+ if ( ! paused )
156+ {
157+ _liveUntil += PauseGraceMs ;
158+ }
159+
160+ Native . SendNuiMessage ( paused ? PausedMessage : ResumedMessage ) ;
161+ }
162+
163+ // Conditions are only re-run on demand, so the loop has to be the one to notice it is done.
164+ if ( now >= _liveUntil )
165+ {
166+ _pauseTick ? . Reevaluate ( ) ;
167+ }
77168 }
78169
79170 /// <summary>The box the stack grows out of, as fractions of the screen, lined up with the minimap.</summary>
@@ -99,7 +190,7 @@ private static float MinimapWidth()
99190 return aspect > 0f ? 1f / ( 4f * aspect ) : 1f / 4f ;
100191 }
101192
102- private static string BuildMessage ( string style , string text , int durationMs , string ? source )
193+ private static string BuildMessage ( string style , string text , int durationMs , string ? source , bool paused )
103194 {
104195 var ( left , bottom , width ) = Anchor ( ) ;
105196
@@ -109,6 +200,7 @@ private static string BuildMessage(string style, string text, int durationMs, st
109200 Text = text ,
110201 Duration = durationMs ,
111202 Footer = string . IsNullOrWhiteSpace ( source ) ? null : source ,
203+ Paused = paused ,
112204 Anchor = new AnchorBox
113205 {
114206 Left = Fraction ( left ) ,
@@ -133,6 +225,9 @@ private sealed class NotifyMessage
133225
134226 public string ? Footer { get ; init ; }
135227
228+ /// <summary>Whether the pause menu is up, which is also the state the page as a whole goes into.</summary>
229+ public required bool Paused { get ; init ; }
230+
136231 public required AnchorBox Anchor { get ; init ; }
137232 }
138233
0 commit comments