44using CounterStrikeSharp . API ;
55using CounterStrikeSharp . API . Core ;
66using CounterStrikeSharp . API . Modules . Utils ;
7+ using CounterStrikeSharp . API . Modules . Memory ;
78using Vector = CounterStrikeSharp . API . Modules . Utils . Vector ;
89
910namespace CS2ScreenMenuAPI
@@ -36,6 +37,8 @@ internal class MenuRenderer
3637 private readonly StringBuilder _backgroundTextSb = new ( ) ;
3738 private readonly StringBuilder _backgroundSb = new ( ) ;
3839 private readonly StringBuilder _htmlTextSb = new ( ) ;
40+ private readonly Dictionary < ulong , List < CPointWorldText > > _playerRoamingWorldTexts = new ( ) ;
41+ private readonly record struct VectorData ( Vector Position , QAngle Angle ) ;
3942
4043 public MenuRenderer ( Menu menu , CCSPlayerController player )
4144 {
@@ -53,6 +56,11 @@ public void Tick()
5356 var observerInfo = _player . GetObserverInfo ( ) ;
5457 bool needsRefresh = ForceRefresh || observerInfo . Mode != _menuCurrentObserverMode || observerInfo . Observing ? . Handle != _menuCurrentObserver ;
5558
59+ if ( observerInfo . Mode == ObserverMode . Roaming && ! needsRefresh )
60+ {
61+ UpdateRoamingWorldTexts ( ) ;
62+ }
63+
5664 if ( needsRefresh )
5765 {
5866 ForceRefresh = false ;
@@ -79,11 +87,25 @@ public void Draw()
7987 _presentingHtml = true ;
8088 }
8189 }
90+
8291 private bool DrawWorldText ( )
8392 {
8493 var observerInfo = _player . GetObserverInfo ( ) ;
85- if ( observerInfo . Mode != ObserverMode . FirstPerson ) return false ;
8694
95+ if ( observerInfo . Mode == ObserverMode . FirstPerson )
96+ {
97+ return DrawWorldTextFirstPerson ( observerInfo ) ;
98+ }
99+ else if ( observerInfo . Mode == ObserverMode . Roaming )
100+ {
101+ return DrawWorldTextRoaming ( observerInfo ) ;
102+ }
103+
104+ return false ;
105+ }
106+
107+ private bool DrawWorldTextFirstPerson ( ObserverInfo observerInfo )
108+ {
87109 var maybeEyeAngles = observerInfo . GetEyeAngles ( ) ;
88110 if ( ! maybeEyeAngles . HasValue ) return false ;
89111 var eyeAngles = maybeEyeAngles . Value ;
@@ -139,6 +161,135 @@ private bool DrawWorldText()
139161
140162 return true ;
141163 }
164+
165+ private bool DrawWorldTextRoaming ( ObserverInfo observerInfo )
166+ {
167+ var vectorData = FindVectorDataForFreeCamera ( _menuPosition . X , _menuPosition . Y ) ;
168+ if ( vectorData == null ) return false ;
169+
170+ _highlightTextSb . Clear ( ) ; _foregroundTextSb . Clear ( ) ; _backgroundTextSb . Clear ( ) ; _backgroundSb . Clear ( ) ;
171+
172+ BuildMenuStrings ( ( text , style , selectIndex ) =>
173+ {
174+ var line = $ "{ selectIndex } . { text } ";
175+ _highlightTextSb . AppendLine ( style . Highlight ? line : string . Empty ) ;
176+ _foregroundTextSb . AppendLine ( style . Foreground ? line : string . Empty ) ;
177+ _backgroundTextSb . AppendLine ( ! style . Foreground ? line : string . Empty ) ;
178+ _backgroundSb . AppendLine ( line ) ;
179+ } ,
180+ ( text , style ) =>
181+ {
182+ _highlightTextSb . AppendLine ( style . Highlight ? text : string . Empty ) ;
183+ _foregroundTextSb . AppendLine ( style . Foreground ? text : string . Empty ) ;
184+ _backgroundTextSb . AppendLine ( ! style . Foreground ? text : string . Empty ) ;
185+ _backgroundSb . AppendLine ( text ) ;
186+ } ) ;
187+
188+ _menuCurrentObserver = observerInfo . Observing ? . Handle ?? nint . Zero ;
189+ _menuCurrentObserverMode = observerInfo . Mode ;
190+
191+ bool allValid = _highlightText ? . IsValid == true && _foregroundText ? . IsValid == true && _backgroundText ? . IsValid == true && _background ? . IsValid == true ;
192+ if ( ! allValid )
193+ {
194+ DestroyEntities ( ) ;
195+ CreateEntities ( ) ;
196+ }
197+
198+ UpdateEntityWithoutParent ( _highlightText ! , _highlightTextSb . ToString ( ) , vectorData . Value . Position , vectorData . Value . Angle ) ;
199+ UpdateEntityWithoutParent ( _foregroundText ! , _foregroundTextSb . ToString ( ) , vectorData . Value . Position , vectorData . Value . Angle ) ;
200+ UpdateEntityWithoutParent ( _backgroundText ! , _backgroundTextSb . ToString ( ) , vectorData . Value . Position , vectorData . Value . Angle ) ;
201+ UpdateEntityWithoutParent ( _background ! , _backgroundSb . ToString ( ) , vectorData . Value . Position , vectorData . Value . Angle ) ;
202+
203+ RegisterRoamingWorldTexts ( ) ;
204+
205+ return true ;
206+ }
207+
208+ private VectorData ? FindVectorDataForFreeCamera ( float positionX , float positionY )
209+ {
210+ if ( _player . Pawn . Value == null || _player . Pawn . Value . ObserverServices == null )
211+ return null ;
212+
213+ CCSPlayerPawn ? pawn = _player . Pawn . Value . As < CCSPlayerPawn > ( ) ;
214+ if ( pawn == null ) return null ;
215+
216+ Vector observerPos = pawn . AbsOrigin ?? new Vector ( ) ;
217+ observerPos += new Vector ( 0 , 0 , pawn . ViewOffset . Z ) ;
218+
219+ QAngle eyeAngles = pawn . EyeAngles ;
220+
221+ Vector forward = new ( ) , right = new ( ) , up = new ( ) ;
222+ NativeAPI . AngleVectors ( eyeAngles . Handle , forward . Handle , right . Handle , up . Handle ) ;
223+
224+ float fov = _player . DesiredFOV == 0 ? 90 : _player . DesiredFOV ;
225+ if ( fov != 90 )
226+ {
227+ float scaleFactor = ( float ) Math . Tan ( ( fov / 2 ) * Math . PI / 180 ) / ( float ) Math . Tan ( 45 * Math . PI / 180 ) ;
228+ positionX *= scaleFactor ;
229+ positionY *= scaleFactor ;
230+ }
231+
232+ Vector offset = forward * 7 + right * positionX + up * positionY ;
233+
234+ QAngle angle = new ( )
235+ {
236+ Y = eyeAngles . Y + 270 ,
237+ Z = 90 - eyeAngles . X ,
238+ X = 0
239+ } ;
240+
241+ return new VectorData ( )
242+ {
243+ Position = observerPos + offset ,
244+ Angle = angle ,
245+ } ;
246+ }
247+
248+ private void UpdateEntityWithoutParent ( CPointWorldText ent , string newText , Vector position , QAngle angles , bool updateText = true )
249+ {
250+ if ( updateText ) ent . MessageText = newText ;
251+ ent . Teleport ( position , angles , null ) ;
252+ if ( updateText ) Utilities . SetStateChanged ( ent , "CPointWorldText" , "m_messageText" ) ;
253+ }
254+
255+ private void RegisterRoamingWorldTexts ( )
256+ {
257+ ulong steamId = _player . SteamID ;
258+ if ( ! _playerRoamingWorldTexts . ContainsKey ( steamId ) )
259+ {
260+ _playerRoamingWorldTexts [ steamId ] = new List < CPointWorldText > ( ) ;
261+ }
262+
263+ var entities = _playerRoamingWorldTexts [ steamId ] ;
264+ entities . Clear ( ) ;
265+
266+ if ( _highlightText ? . IsValid == true ) entities . Add ( _highlightText ) ;
267+ if ( _foregroundText ? . IsValid == true ) entities . Add ( _foregroundText ) ;
268+ if ( _backgroundText ? . IsValid == true ) entities . Add ( _backgroundText ) ;
269+ if ( _background ? . IsValid == true ) entities . Add ( _background ) ;
270+ }
271+
272+ private void UpdateRoamingWorldTexts ( )
273+ {
274+ ulong steamId = _player . SteamID ;
275+ if ( ! _playerRoamingWorldTexts . TryGetValue ( steamId , out var entities ) || entities . Count == 0 )
276+ return ;
277+
278+ var vectorData = FindVectorDataForFreeCamera ( _menuPosition . X , _menuPosition . Y ) ;
279+ if ( vectorData == null ) return ;
280+
281+ foreach ( var entity in entities . ToList ( ) )
282+ {
283+ if ( ! entity . IsValid )
284+ {
285+ entities . Remove ( entity ) ;
286+ continue ;
287+ }
288+
289+ entity . Teleport ( vectorData . Value . Position , vectorData . Value . Angle , null ) ;
290+ }
291+ }
292+
142293 private void DrawHtml ( )
143294 {
144295 _htmlTextSb . Clear ( ) ;
@@ -263,13 +414,20 @@ private void BuildMenuStrings(Action<string, TextStyling, int> writeLine, Action
263414 writeSimpleLine ( _player . Localizer ( "ExitKey" , _menu . ExitKey ) , default ) ;
264415 }
265416 }
417+
266418 public void DestroyEntities ( )
267419 {
268420 if ( _highlightText ? . IsValid == true ) _highlightText . Remove ( ) ;
269421 if ( _foregroundText ? . IsValid == true ) _foregroundText . Remove ( ) ;
270422 if ( _backgroundText ? . IsValid == true ) _backgroundText . Remove ( ) ;
271423 if ( _background ? . IsValid == true ) _background . Remove ( ) ;
272424 _highlightText = _foregroundText = _backgroundText = _background = null ;
425+
426+ ulong steamId = _player . SteamID ;
427+ if ( _playerRoamingWorldTexts . ContainsKey ( steamId ) )
428+ {
429+ _playerRoamingWorldTexts [ steamId ] . Clear ( ) ;
430+ }
273431 }
274432
275433 private void CreateEntities ( )
0 commit comments