//---------------------------------------------------------------------------------------------------- // Leader3 markers: unloze_ping prop_dynamic markers with point_worldtext titles. // The ArrayList is the authoritative store; entities are respawned from it every round. //---------------------------------------------------------------------------------------------------- enum struct Marker { int ownerUserId; char ownerName[MAX_NAME_LENGTH]; int skin; float pos[3]; char title[128]; float placedAt; int propRef; int textRef; } ArrayList g_alMarkers; void Markers_OnPluginStart() { g_alMarkers = new ArrayList(sizeof(Marker)); } void SkinName(int skin, char[] buf, int maxlen) { switch (skin) { case SKIN_SKULL: strcopy(buf, maxlen, "Skull"); case SKIN_MOON: strcopy(buf, maxlen, "Moon"); case SKIN_CROSS: strcopy(buf, maxlen, "Cross"); default: strcopy(buf, maxlen, "Star"); } } int SkinFromName(const char[] name) { if (StrEqual(name, "star", false)) return SKIN_STAR; if (StrEqual(name, "skull", false)) return SKIN_SKULL; if (StrEqual(name, "moon", false)) return SKIN_MOON; if (StrEqual(name, "cross", false)) return SKIN_CROSS; return -1; } //---------------------------------------------------------------------------------------------------- // Placement //---------------------------------------------------------------------------------------------------- void Marker_Place(int client, int skin) { if (!IsLeader(client)) return; // Dead and spectating leaders may keep placing markers (leading from spec); // only living zombies are blocked. if (IsLivingZombie(client)) { PrintToChat(client, "%sYou cannot place markers as a zombie.", PREFIX); return; } float pos[3]; if (!TracePlayerAngles(client, pos)) { PrintToChat(client, "%sCould not find a spot to place the marker on.", PREFIX); return; } char skinName[16]; SkinName(skin, skinName, sizeof(skinName)); int userid = GetClientUserId(client); int existing = FindMarkerByOwnerSkin(userid, skin); Marker marker; if (existing != -1) { // Moving keeps the title and stays out of chat. The prop and text entities are // teleported rather than respawned so repeated moves don't churn edicts. g_alMarkers.GetArray(existing, marker); marker.pos = pos; marker.placedAt = GetGameTime(); GetClientName(client, marker.ownerName, sizeof(marker.ownerName)); g_alMarkers.SetArray(existing, marker); Marker_MoveProp(existing); Marker_UpdateText(existing); } else { if (CountMarkersByOwner(userid) >= g_cvMarkerLimit.IntValue) { PrintToChat(client, "%sMarker limit reached (%d). Replace one of your markers or use \x04!clearmarkers\x01.", PREFIX, g_cvMarkerLimit.IntValue); return; } marker.ownerUserId = userid; GetClientName(client, marker.ownerName, sizeof(marker.ownerName)); marker.skin = skin; marker.pos = pos; marker.title[0] = '\0'; marker.placedAt = GetGameTime(); marker.propRef = INVALID_ENT_REFERENCE; marker.textRef = INVALID_ENT_REFERENCE; int idx = g_alMarkers.PushArray(marker); SpawnMarkerProp(idx); PrintToChatAll("%s\x04%N\x01 placed a \x04%s\x01 marker.", PREFIX, client, skinName); PrintToChat(client, "%sType \x04`your text\x01 in chat to put a title on it.", PREFIX); } } void Marker_PlaceBySpray(int client) { Marker_Place(client, SKIN_STAR); } int FindMarkerByOwnerSkin(int userid, int skin) { Marker marker; for (int i = 0; i < g_alMarkers.Length; i++) { g_alMarkers.GetArray(i, marker); if (marker.ownerUserId == userid && marker.skin == skin) return i; } return -1; } bool Marker_GetTitle(int client, int skin, char[] buf, int maxlen) { int idx = FindMarkerByOwnerSkin(GetClientUserId(client), skin); if (idx == -1) return false; Marker marker; g_alMarkers.GetArray(idx, marker); if (marker.title[0] == '\0') return false; strcopy(buf, maxlen, marker.title); return true; } int CountMarkersByOwner(int userid) { int count = 0; Marker marker; for (int i = 0; i < g_alMarkers.Length; i++) { g_alMarkers.GetArray(i, marker); if (marker.ownerUserId == userid) count++; } return count; } //---------------------------------------------------------------------------------------------------- // Titles //---------------------------------------------------------------------------------------------------- // Titles typed entirely in lowercase are uppercased; mixed-case titles are kept as typed. void UppercaseAllLowerTitle(char[] text) { bool hasLower = false; for (int i = 0; text[i] != '\0'; i++) { int bytes = IsCharMB(text[i]); if (bytes > 1) { i += bytes - 1; continue; } if (IsCharUpper(text[i])) return; if (IsCharLower(text[i])) hasLower = true; } if (!hasLower) return; for (int i = 0; text[i] != '\0'; i++) { int bytes = IsCharMB(text[i]); if (bytes > 1) { i += bytes - 1; continue; } text[i] = CharToUpper(text[i]); } } void Marker_SetTitle(int client, const char[] text) { int userid = GetClientUserId(client); // Most recently placed marker owned by this leader. int lastIdx = -1; float lastTime = -1.0; Marker marker; for (int i = 0; i < g_alMarkers.Length; i++) { g_alMarkers.GetArray(i, marker); if (marker.ownerUserId == userid && marker.placedAt > lastTime) { lastTime = marker.placedAt; lastIdx = i; } } if (lastIdx == -1) { PrintToChat(client, "%sYou have no marker to title. Place one first.", PREFIX); return; } g_alMarkers.GetArray(lastIdx, marker); strcopy(marker.title, sizeof(marker.title), text); UppercaseAllLowerTitle(marker.title); g_alMarkers.SetArray(lastIdx, marker); Marker_UpdateText(lastIdx); char skinName[16]; SkinName(marker.skin, skinName, sizeof(skinName)); if (text[0] == '\0') { PrintToChat(client, "%sTitle removed from your \x04%s\x01 marker.", PREFIX, skinName); return; } PrintToChatAll("%s\x04%N\x01 marked \x04%s\x01: \x04%s", PREFIX, client, skinName, marker.title); } //---------------------------------------------------------------------------------------------------- // Entity lifecycle //---------------------------------------------------------------------------------------------------- void SpawnMarkerProp(int idx) { Marker marker; g_alMarkers.GetArray(idx, marker); int ent = CreateEntityByName("prop_dynamic"); if (ent == -1) return; SetEntityModel(ent, MARKER_MODEL); DispatchKeyValueFloat(ent, "modelscale", MARKER_MODEL_SCALE); char skin[4]; IntToString(marker.skin, skin, sizeof(skin)); SetVariantString(skin); AcceptEntityInput(ent, "Skin"); DispatchSpawn(ent); TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR); marker.propRef = EntIndexToEntRef(ent); g_alMarkers.SetArray(idx, marker); } void SpawnMarkerText(int idx) { Marker marker; g_alMarkers.GetArray(idx, marker); if (marker.title[0] == '\0' || g_bWorldTextBroken) return; int ent = CreateEntityByName("point_worldtext"); if (ent == -1) { // Engine build without point_worldtext: titles stay chat-only for this map. g_bWorldTextBroken = true; LogMessage("point_worldtext could not be created; marker titles will not render in the world."); return; } char text[192]; Format(text, sizeof(text), "%s", marker.title); DispatchKeyValue(ent, "message", text); DispatchKeyValue(ent, "font", "10"); DispatchKeyValue(ent, "textsize", "12"); DispatchKeyValue(ent, "color", "255 255 255"); DispatchKeyValue(ent, "orientation", "1"); DispatchKeyValue(ent, "rainbow", "1"); DispatchSpawn(ent); float pos[3]; ComputeMarkerTextPos(marker, pos); TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR); marker.textRef = EntIndexToEntRef(ent); g_alMarkers.SetArray(idx, marker); } void ComputeMarkerTextPos(const Marker marker, float pos[3]) { pos = marker.pos; // unloze_ping.mdl is 80 units tall and spawned at modelscale 1.5 (120 units). pos[2] += MARKER_MODEL_HEIGHT * MARKER_MODEL_SCALE + 8.0; // Markers placed in low rooms would put the text inside the ceiling: trace up // from the marker base and keep the text MARKER_TEXT_CLEARANCE below any hit. float up[3]; up = marker.pos; up[2] = pos[2] + MARKER_TEXT_CLEARANCE; Handle trace = TR_TraceRayFilterEx(marker.pos, up, MASK_SOLID_BRUSHONLY, RayType_EndPoint, TraceFilter_NoPlayers); if (TR_DidHit(trace)) { float hit[3]; TR_GetEndPosition(hit, trace); pos[2] = hit[2] - MARKER_TEXT_CLEARANCE; } delete trace; } // Teleports the existing marker prop to the stored position, spawning one only if the // reference went stale. void Marker_MoveProp(int idx) { Marker marker; g_alMarkers.GetArray(idx, marker); int ent = LiveEntRef(marker.propRef, "prop_dynamic"); if (ent == -1) { SpawnMarkerProp(idx); return; } TeleportEntity(ent, marker.pos, NULL_VECTOR, NULL_VECTOR); } // Retitles/moves the existing point_worldtext in place (SetText is networked via // SendPropString) instead of respawning it, so retitles and moves don't churn edicts. void Marker_UpdateText(int idx) { Marker marker; g_alMarkers.GetArray(idx, marker); if (marker.title[0] == '\0') { KillEntRef(marker.textRef, "point_worldtext"); marker.textRef = INVALID_ENT_REFERENCE; g_alMarkers.SetArray(idx, marker); return; } int ent = LiveEntRef(marker.textRef, "point_worldtext"); if (ent == -1) { SpawnMarkerText(idx); return; } SetVariantString(marker.title); AcceptEntityInput(ent, "SetText"); float pos[3]; ComputeMarkerTextPos(marker, pos); TeleportEntity(ent, pos, NULL_VECTOR, NULL_VECTOR); } void Marker_KillEnts(Marker marker) { KillEntRef(marker.propRef, "prop_dynamic"); KillEntRef(marker.textRef, "point_worldtext"); } void KillEntRef(int ref, const char[] expectedClass) { int ent = LiveEntRef(ref, expectedClass); if (ent != -1) AcceptEntityInput(ent, "Kill"); } // Resolves an entity reference, returning -1 unless it is still a live edict of the // expected class. int LiveEntRef(int ref, const char[] expectedClass) { if (ref == INVALID_ENT_REFERENCE) return -1; int ent = EntRefToEntIndex(ref); if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent)) return -1; char classname[64]; GetEdictClassname(ent, classname, sizeof(classname)); return strcmp(classname, expectedClass) == 0 ? ent : -1; } // Round transitions delete runtime entities: rebuild every marker from the stored data. void Markers_OnRoundStart() { Marker marker; for (int i = 0; i < g_alMarkers.Length; i++) { g_alMarkers.GetArray(i, marker); marker.propRef = INVALID_ENT_REFERENCE; marker.textRef = INVALID_ENT_REFERENCE; g_alMarkers.SetArray(i, marker); SpawnMarkerProp(i); SpawnMarkerText(i); } } void Markers_ClearAll(bool killEnts = true) { if (killEnts) { Marker marker; for (int i = 0; i < g_alMarkers.Length; i++) { g_alMarkers.GetArray(i, marker); Marker_KillEnts(marker); } } g_alMarkers.Clear(); } void Marker_ClearByOwner(int client, bool announce = true) { int userid = GetClientUserId(client); int removed = 0; Marker marker; for (int i = g_alMarkers.Length - 1; i >= 0; i--) { g_alMarkers.GetArray(i, marker); if (marker.ownerUserId != userid) continue; Marker_KillEnts(marker); g_alMarkers.Erase(i); removed++; } if (!announce) return; if (removed > 0) PrintToChat(client, "%sRemoved %d of your markers.", PREFIX, removed); else PrintToChat(client, "%sYou have no markers.", PREFIX); } //---------------------------------------------------------------------------------------------------- // Commands //---------------------------------------------------------------------------------------------------- public Action Cmd_Marker(int client, int args) { if (client == 0) { ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN); return Plugin_Handled; } if (!IsLeader(client)) { ReplyToCommand(client, "%s Only leaders can place markers.", PREFIX_PLAIN); return Plugin_Handled; } if (args < 1) { ReplyToCommand(client, "%s Usage: sm_marker ", PREFIX_PLAIN); return Plugin_Handled; } char arg[16]; GetCmdArg(1, arg, sizeof(arg)); int skin = SkinFromName(arg); if (skin == -1) { ReplyToCommand(client, "%s Unknown marker type '%s'. Use star, skull, moon or cross.", PREFIX_PLAIN, arg); return Plugin_Handled; } Marker_Place(client, skin); return Plugin_Handled; } public Action Cmd_MarkerTitle(int client, int args) { if (client == 0) { ReplyToCommand(client, "%s Cannot use this from the console.", PREFIX_PLAIN); return Plugin_Handled; } if (!IsLeader(client)) { ReplyToCommand(client, "%s Only leaders can title markers.", PREFIX_PLAIN); return Plugin_Handled; } char text[128]; GetCmdArgString(text, sizeof(text)); StripQuotes(text); TrimString(text); Marker_SetTitle(client, text); return Plugin_Handled; } public Action Cmd_ClearMarkers(int client, int args) { if (client == 0) { Markers_ClearAll(); ReplyToCommand(client, "%s All markers cleared.", PREFIX_PLAIN); return Plugin_Handled; } if (args >= 1 && CheckCommandAccess(client, "sm_admin", ADMFLAG_GENERIC, false)) { char arg[65]; GetCmdArg(1, arg, sizeof(arg)); if (StrEqual(arg, "all", false)) { Markers_ClearAll(); PrintToChatAll("%sAll markers have been cleared by \x04%N\x01.", PREFIX, client); return Plugin_Handled; } char targetName[MAX_TARGET_LENGTH]; int targets[MAXPLAYERS]; bool tnIsMl; int count = ProcessTargetString(arg, client, targets, sizeof(targets), COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, targetName, sizeof(targetName), tnIsMl); if (count <= 0) { ReplyToTargetError(client, count); return Plugin_Handled; } for (int i = 0; i < count; i++) Marker_ClearByOwner(targets[i]); ReplyToCommand(client, "%s Cleared markers of %s.", PREFIX_PLAIN, targetName); return Plugin_Handled; } if (!IsLeader(client)) { ReplyToCommand(client, "%s Only leaders can clear markers.", PREFIX_PLAIN); return Plugin_Handled; } Marker_ClearByOwner(client); return Plugin_Handled; } //---------------------------------------------------------------------------------------------------- // Aim trace (port of Leader2's TracePlayerAngles) //---------------------------------------------------------------------------------------------------- bool TracePlayerAngles(int client, float vecResult[3]) { if (!IsClientInGame(client)) return false; float vecEyeAngles[3], vecEyeOrigin[3]; GetClientEyeAngles(client, vecEyeAngles); GetClientEyePosition(client, vecEyeOrigin); Handle trace = TR_TraceRayFilterEx(vecEyeOrigin, vecEyeAngles, MASK_SHOT_HULL, RayType_Infinite, TraceFilter_NoPlayers); // AllSolid: eyes clipped inside a solid (crowd pushes); endpos degenerates to the eye. bool hit = TR_DidHit(trace) && !TR_AllSolid(trace); int hitEnt = hit ? TR_GetEntityIndex(trace) : -1; if (hit) TR_GetEndPosition(vecResult, trace); delete trace; if (!hit) return false; // NaN or out-of-grid components reach the client as 0 (WriteBitCoord encodes them // as zero), which draws the beam to the world origin. Catch them before the TE. if (vecResult[0] != vecResult[0] || vecResult[1] != vecResult[1] || vecResult[2] != vecResult[2] || FloatAbs(vecResult[0]) > 16384.0 || FloatAbs(vecResult[1]) > 16384.0 || FloatAbs(vecResult[2]) > 16384.0) { LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "insane endpos"); return false; } // ZE maps park "removed" but still solid props at the world origin; their hulls // catch the ray there. Hits on the world itself near origin stay legitimate. if (hitEnt > 0 && GetVectorLength(vecResult) < 64.0) { LogSuspectTrace(client, hitEnt, vecResult, vecEyeOrigin, vecEyeAngles, "entity hull at world origin"); return false; } return true; } // One line per 5s so a leader holding the tracer on a culprit doesn't flood the log. static float g_fNextSuspectTraceLog; static void LogSuspectTrace(int client, int ent, const float end[3], const float eye[3], const float ang[3], const char[] why) { float now = GetGameTime(); if (now < g_fNextSuspectTraceLog) return; g_fNextSuspectTraceLog = now + 5.0; char classname[64] = "worldspawn"; char model[PLATFORM_MAX_PATH]; if (ent > 0 && IsValidEdict(ent)) { GetEdictClassname(ent, classname, sizeof(classname)); GetEntPropString(ent, Prop_Data, "m_ModelName", model, sizeof(model)); } LogMessage("Aim trace rejected (%s) for %L: end (%.1f %.1f %.1f), hit ent %d (%s, model '%s'), eye (%.1f %.1f %.1f), ang (%.1f %.1f %.1f)", why, client, end[0], end[1], end[2], ent, classname, model, eye[0], eye[1], eye[2], ang[0], ang[1], ang[2]); } public bool TraceFilter_NoPlayers(int entity, int contentsMask) { return entity > MaxClients; }