using System.Drawing; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; namespace Outnumbered.Engine; // The one place a point_worldtext entity is created + configured (HUD panel, the 2 shop panels, the draft cards all // share it). CreateEntityByName returns a NON-null wrapper even on failure (e.g. the entity limit) with a Zero Handle, // so the guard checks the raw handle (no memory deref) before touching schema members. Callers supply the per-use // font/justify/colour/border and own the last-text caching; the volatile "SetMessage" input + the Teleport placement // go through SetText/Place so every engine touchpoint for this entity lives here. internal static class WorldText { public static CPointWorldText? Create(float fontSize, float worldUnitsPerPx, string fontName, Color color, bool drawBackground, float border, PointWorldTextJustifyHorizontal_t justify) { var e = Utilities.CreateEntityByName(EngineNames.PointWorldText); if (e is null || e.Handle == nint.Zero) return null; e.MessageText = " "; e.Enabled = true; e.Fullbright = true; e.FontSize = fontSize; e.WorldUnitsPerPx = worldUnitsPerPx; if (!string.IsNullOrEmpty(fontName)) e.FontName = fontName; e.Color = color; e.JustifyHorizontal = justify; e.JustifyVertical = PointWorldTextJustifyVertical_t.POINT_WORLD_TEXT_JUSTIFY_VERTICAL_CENTER; e.ReorientMode = PointWorldTextReorientMode_t.POINT_WORLD_TEXT_REORIENT_NONE; e.DrawBackground = drawBackground; e.BackgroundBorderHeight = border; e.BackgroundBorderWidth = border; e.DispatchSpawn(); return e; } // Push new text via the point_worldtext "SetMessage" input (the volatile input-name literal lives here, not at the 4 // call sites). Callers keep their own last-text cache and only call this on a change. public static void SetText(CPointWorldText ent, string text) => ent.AcceptInput("SetMessage", ent, ent, text); // Position + orient a panel. point_worldtext is moved via Teleport (no velocity); callers compute the eye-relative frame. public static void Place(CPointWorldText ent, Vector pos, QAngle ang) => ent.Teleport(pos, ang, null); // Tear a panel down: Remove the entity if it's still live, then null the caller's reference. Symmetric with Create — // every point_worldtext create + destroy goes through this file. public static void Destroy(ref CPointWorldText? ent) { if (ent is { IsValid: true }) ent.Remove(); ent = null; } public const float EyeZFallback = 64f; // ViewOffset.Z fallback when the pawn doesn't report one // The eye-relative frame for placing a panel in front of a pawn's view: eye position + forward/right/up basis + the // panel orientation (yaw+270 / 90-pitch = worldtext faces the player, upright across pitch/yaw). false if AbsOrigin is // null. Callers add only their own per-panel offsets. Shared by the HUD + the shop/draft panels. public static bool TryEyeFrame(CCSPlayerPawn pawn, out Vector eye, out Vector fwd, out Vector right, out Vector up, out QAngle ang) { eye = null!; fwd = null!; right = null!; up = null!; ang = null!; var origin = pawn.AbsOrigin; if (origin is null) return false; var ea = pawn.EyeAngles; float eyeZ = pawn.ViewOffset?.Z ?? EyeZFallback; (fwd, right, up) = AngleVectors(ea); eye = new Vector(origin.X, origin.Y, origin.Z + eyeZ); ang = new QAngle(0f, ea.Y + 270f, 90f - ea.X); return true; } // Source-engine AngleVectors with roll assumed 0 (HUD/shop panels never roll). private static (Vector forward, Vector right, Vector up) AngleVectors(QAngle a) { const double d2r = Math.PI / 180.0; double p = a.X * d2r, y = a.Y * d2r; double sp = Math.Sin(p), cp = Math.Cos(p), sy = Math.Sin(y), cy = Math.Cos(y); return ( new Vector((float)(cp * cy), (float)(cp * sy), (float)(-sp)), new Vector((float)sy, (float)(-cy), 0f), new Vector((float)(sp * cy), (float)(sp * sy), (float)cp)); } }