Skip to content

Commit e1fe330

Browse files
committed
feat: allowe free mode spectator to use menu.
1 parent 620747e commit e1fe330

File tree

1 file changed

+159
-1
lines changed

1 file changed

+159
-1
lines changed

src/MenuRenderer/MenuRenderer.cs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using CounterStrikeSharp.API;
55
using CounterStrikeSharp.API.Core;
66
using CounterStrikeSharp.API.Modules.Utils;
7+
using CounterStrikeSharp.API.Modules.Memory;
78
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
89

910
namespace 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

Comments
 (0)