//---------------------------------------------------------------------------------------------------- // Leader3 effects: xray shell, overhead sprite, beacon rings, +use view tracer. //---------------------------------------------------------------------------------------------------- void Effects_Resume(int client) { if (!IsEffectsActive(client)) return; if (g_iEffectFlags[client] & EFFECT_XRAY) Xray_Create(client); if (g_iSpriteChoice[client] != SPRITE_NONE) Sprite_Create(client); if ((g_iEffectFlags[client] & EFFECT_BEACON) && g_iBeaconSerial[client] == 0) Beacon_Start(client); } void Effects_Suspend(int client) { Xray_Kill(client); Sprite_Kill(client); Beacon_Stop(client); g_iEffectFlags[client] &= ~EFFECT_TRACER_TOGGLED; } //---------------------------------------------------------------------------------------------------- // Xray shell: prop_dynamic_override with the shipped xray model ($additive + $ignorez material, // so it shows through walls), bonemerged onto the player and tinted with the tier colour. // SetTransmit hides it from zombies, from the leader themselves, and from first-person spectators // of the leader. // // $ignorez only helps once the client has the entity; CheckTransmit culls edicts against the // viewer's PVS first, so the shell (and the leader) vanished whenever the viewer was in another // vis area - thick walls, other side of the map. Fix: strip the shell's edict state down to // FL_EDICT_FULLCHECK so the engine calls ShouldTransmit() every frame, and DHook that to return // FL_EDICT_ALWAYS. The engine then calls SetTransmit(pInfo, true), which marks the shell AND its // move parent (the leader) for sending regardless of PVS, and still runs Hook_XrayShellTransmit, // so zombies keep seeing nothing. //---------------------------------------------------------------------------------------------------- void Xray_SetupForceTransmit() { // SDKHooks' gamedata keeps the SetTransmit vtable index current; ShouldTransmit sits two // slots before it (ShouldTransmit, UpdateTransmitState, SetTransmit in baseentity.h). Handle gc = LoadGameConfigFile("sdkhooks.games/engine.ep2v"); if (gc == null) { LogError("Could not load sdkhooks.games/engine.ep2v; leader xray stays PVS-limited."); return; } int offset = GameConfGetOffset(gc, "SetTransmit"); delete gc; if (offset == -1) { LogError("No SetTransmit offset in sdkhooks.games/engine.ep2v; leader xray stays PVS-limited."); return; } g_hXrayShouldTransmit = DHookCreate(offset - 2, HookType_Entity, ReturnType_Int, ThisPointer_Ignore, Hook_XrayShouldTransmit); DHookAddParam(g_hXrayShouldTransmit, HookParamType_ObjectPtr); // const CCheckTransmitInfo * } public MRESReturn Hook_XrayShouldTransmit(Handle hReturn, Handle hParams) { DHookSetReturn(hReturn, FL_EDICT_ALWAYS); return MRES_Supercede; } void Xray_Create(int client) { Xray_Kill(client); if (!IsEffectsActive(client)) return; int ent = CreateEntityByName("prop_dynamic_override"); if (ent == -1) return; DispatchKeyValue(ent, "model", XRAY_MODEL); DispatchKeyValue(ent, "solid", "0"); DispatchKeyValue(ent, "spawnflags", "256"); DispatchKeyValue(ent, "disableshadows", "1"); DispatchKeyValue(ent, "disablereceiveshadows", "1"); if (!DispatchSpawn(ent)) { AcceptEntityInput(ent, "Kill"); return; } SetEntProp(ent, Prop_Send, "m_CollisionGroup", 11); // COLLISION_GROUP_WEAPON: never collides with players int colour[4]; GetLeaderColour(client, colour); SetEntityRenderMode(ent, RENDER_TRANSADD); SetEntityRenderColor(ent, colour[0], colour[1], colour[2], g_cvXrayAlpha.IntValue); // CBaseEntity::FollowEntity() recipe. The client only bone-merges when the entity has // EF_BONEMERGE, a move parent AND MOVETYPE_NONE (C_BaseEntity::IsFollowingEntity) -- // prop_dynamic is MOVETYPE_PUSH and SetParent doesn't change that, hence t-posing shells. float origin[3], angles[3]; GetClientAbsOrigin(client, origin); GetClientAbsAngles(client, angles); TeleportEntity(ent, origin, angles, NULL_VECTOR); SetVariantString("!activator"); AcceptEntityInput(ent, "SetParent", client, ent); SetEntityMoveType(ent, MOVETYPE_NONE); SetEntProp(ent, Prop_Send, "m_fEffects", EF_BONEMERGE|EF_BONEMERGE_FASTCULL|EF_NOSHADOW|EF_NORECEIVESHADOW|EF_PARENT_ANIMATES); SetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity", client); SDKHook(ent, SDKHook_SetTransmit, Hook_XrayShellTransmit); if (g_hXrayShouldTransmit != null) { DHookEntity(g_hXrayShouldTransmit, false, ent); // FULLCHECK is the absence of the other transmit-state flags. SetEdictFlags(ent, GetEdictFlags(ent) & ~(FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK|FL_EDICT_DONTSEND)); } g_iXrayRef[client] = EntIndexToEntRef(ent); } public Action Hook_XrayShellTransmit(int ent, int client) { // Anything that recomputes transmit state (DispatchUpdateTransmitState) puts PVSCHECK // back on the edict; strip it so Hook_XrayShouldTransmit resumes overriding PVS culling. if (g_hXrayShouldTransmit != null) { int flags = GetEdictFlags(ent); if (flags & (FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK)) SetEdictFlags(ent, flags & ~(FL_EDICT_ALWAYS|FL_EDICT_PVSCHECK)); } int owner = GetEntPropEnt(ent, Prop_Send, "m_hOwnerEntity"); if (owner == client) return Plugin_Handled; // Humans only: never reveal the leader's through-wall xray to zombies. if (IsLivingZombie(client)) return Plugin_Handled; // First-person spectators of the owner would get the shell in their face too. if (!IsPlayerAlive(client) && owner != -1 && GetEntProp(client, Prop_Send, "m_iObserverMode") == 4 && GetEntPropEnt(client, Prop_Send, "m_hObserverTarget") == owner) return Plugin_Handled; return Plugin_Continue; } void Xray_Kill(int client) { int ent = EntRefToEntIndex(g_iXrayRef[client]); g_iXrayRef[client] = INVALID_ENT_REFERENCE; if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent)) return; char classname[64]; GetEdictClassname(ent, classname, sizeof(classname)); if (StrContains(classname, "prop_dynamic") == 0) AcceptEntityInput(ent, "Kill"); } // Re-create the xray with the right tier colour (used when primary changes). void Effects_RefreshXray(int client) { if ((g_iEffectFlags[client] & EFFECT_XRAY) && IsEffectsActive(client)) Xray_Create(client); } void Xray_Toggle(int client) { g_iEffectFlags[client] ^= EFFECT_XRAY; if (g_iEffectFlags[client] & EFFECT_XRAY) { Xray_Create(client); PrintToChat(client, "%sXray enabled.", PREFIX); } else { Xray_Kill(client); PrintToChat(client, "%sXray disabled.", PREFIX); } } //---------------------------------------------------------------------------------------------------- // Overhead sprite: 'Defend here' / 'Follow me' //---------------------------------------------------------------------------------------------------- void Sprite_Set(int client, int choice) { g_iSpriteChoice[client] = choice; Sprite_Kill(client); if (choice == SPRITE_NONE) { PrintToChat(client, "%sSprite removed.", PREFIX); return; } Sprite_Create(client); PrintToChat(client, "%sSprite changed to '%s'.", PREFIX, choice == SPRITE_CHOICE_DEFEND ? "Defend Here" : "Follow Me"); } void Sprite_Create(int client) { Sprite_Kill(client); if (!IsEffectsActive(client) || g_iSpriteChoice[client] == SPRITE_NONE) return; char mat[PLATFORM_MAX_PATH]; strcopy(mat, sizeof(mat), g_iSpriteChoice[client] == SPRITE_CHOICE_DEFEND ? SPRITE_DEFEND : SPRITE_FOLLOW); g_iSpriteRef[client] = AttachSpriteEnt(client, mat, "0.1", "1", "255 255 255", 45.0); } void Sprite_Kill(int client) { KillSpriteRef(g_iSpriteRef[client]); g_iSpriteRef[client] = INVALID_ENT_REFERENCE; } //---------------------------------------------------------------------------------------------------- // env_sprite attach for the overhead sprite (Leader2's targetname-rename parenting trick). //---------------------------------------------------------------------------------------------------- int AttachSpriteEnt(int client, const char[] material, const char[] scale, const char[] rendermode, const char[] colour, float zOffset) { if (!IsPlayerAlive(client)) return INVALID_ENT_REFERENCE; char targetname[16], oldTargetname[64]; GetEntPropString(client, Prop_Data, "m_iName", oldTargetname, sizeof(oldTargetname)); Format(targetname, sizeof(targetname), "Client%d", client); DispatchKeyValue(client, "targetname", targetname); float origin[3]; GetClientEyePosition(client, origin); origin[2] += zOffset; int ent = CreateEntityByName("env_sprite"); if (ent == -1) { DispatchKeyValue(client, "targetname", oldTargetname); return INVALID_ENT_REFERENCE; } DispatchKeyValue(ent, "model", material); DispatchKeyValue(ent, "classname", "env_sprite"); DispatchKeyValue(ent, "spawnflags", "1"); DispatchKeyValue(ent, "scale", scale); DispatchKeyValue(ent, "rendermode", rendermode); DispatchKeyValue(ent, "rendercolor", colour); DispatchSpawn(ent); TeleportEntity(ent, origin, NULL_VECTOR, NULL_VECTOR); SetVariantString(targetname); AcceptEntityInput(ent, "SetParent", ent, ent, 0); DispatchKeyValue(client, "targetname", oldTargetname); return EntIndexToEntRef(ent); } void KillSpriteRef(int ref) { if (ref == INVALID_ENT_REFERENCE) return; int ent = EntRefToEntIndex(ref); if (ent == INVALID_ENT_REFERENCE || !IsValidEdict(ent)) return; char classname[64]; GetEdictClassname(ent, classname, sizeof(classname)); if (strcmp(classname, "env_sprite") == 0) AcceptEntityInput(ent, "Kill"); } //---------------------------------------------------------------------------------------------------- // Beacon (serial-guarded repeating timer, Leader2/Couch ring pattern) //---------------------------------------------------------------------------------------------------- void Beacon_Toggle(int client) { g_iEffectFlags[client] ^= EFFECT_BEACON; if (g_iEffectFlags[client] & EFFECT_BEACON) { if (IsEffectsActive(client)) Beacon_Start(client); PrintToChat(client, "%sBeacon enabled.", PREFIX); LogAction(client, client, "\"%L\" set a leader beacon on himself", client); } else { Beacon_Stop(client); PrintToChat(client, "%sBeacon disabled.", PREFIX); } } void Beacon_Start(int client) { g_iBeaconSerial[client] = ++g_iSerialGen; CreateTimer(g_cvBeaconInterval.FloatValue, Timer_Beacon, client | (g_iSerialGen << 7), TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE); } void Beacon_Stop(int client) { g_iBeaconSerial[client] = 0; } public Action Timer_Beacon(Handle timer, any value) { int client = value & 0x7f; int serial = value >> 7; if (g_iBeaconSerial[client] != serial || !IsClientInGame(client) || !IsPlayerAlive(client)) { if (g_iBeaconSerial[client] == serial) g_iBeaconSerial[client] = 0; return Plugin_Stop; } float vec[3]; GetClientAbsOrigin(client, vec); vec[2] += 10.0; float radius = g_cvBeaconRadius.FloatValue; int tierColour[4]; GetLeaderColour(client, tierColour); TE_SetupBeamRingPoint(vec, 10.0, radius, g_iBeamSprite, g_iHaloSprite, 0, 15, 0.5, 5.0, 0.0, tierColour, 10, 0); TE_SendToAll(); int rainbowColour[4]; float gameTime = GetGameTime(); rainbowColour[0] = RoundFloat(Sine(2.5 * gameTime + 0.0) * 127.0 + 128.0); rainbowColour[1] = RoundFloat(Sine(2.5 * gameTime + 2.0943951) * 127.0 + 128.0); rainbowColour[2] = RoundFloat(Sine(2.5 * gameTime + 4.1887902) * 127.0 + 128.0); rainbowColour[3] = 255; TE_SetupBeamRingPoint(vec, 10.0, radius, g_iBeamSprite, g_iHaloSprite, 0, 10, 0.6, 10.0, 0.5, rainbowColour, 10, 0); TE_SendToAll(); return Plugin_Continue; } void GetLeaderColour(int client, int colour[4]) { if (g_bHasCustomColour[client]) { colour[0] = g_iCustomColour[client][0]; colour[1] = g_iCustomColour[client][1]; colour[2] = g_iCustomColour[client][2]; } else if (IsPrimaryLeader(client)) { colour[0] = g_iXrayColourPrimary[0]; colour[1] = g_iXrayColourPrimary[1]; colour[2] = g_iXrayColourPrimary[2]; } else { colour[0] = g_iXrayColourSub[0]; colour[1] = g_iXrayColourSub[1]; colour[2] = g_iXrayColourSub[2]; } colour[3] = 255; } //---------------------------------------------------------------------------------------------------- // Custom leader colour: !leadercolour . Applies to xray, beacon and tracer. //---------------------------------------------------------------------------------------------------- static const char g_sColourNames[][] = { "red", "orange", "yellow", "lime", "green", "cyan", "blue", "purple", "magenta", "pink", "white" }; static const int g_iColourValues[][3] = { {255, 0, 0}, {255, 128, 0}, {255, 255, 0}, {128, 255, 0}, {0, 255, 0}, {0, 255, 255}, {0, 100, 255}, {160, 0, 255}, {255, 0, 255}, {255, 105, 180}, {255, 255, 255} }; public Action Cmd_LeaderColour(int client, int args) { if (client == 0) { ReplyToCommand(client, "%s This command can only be used in-game.", PREFIX_PLAIN); return Plugin_Handled; } if (!IsLeader(client)) { PrintToChat(client, "%sOnly leaders can set a leader colour.", PREFIX); return Plugin_Handled; } if (args == 0) { LeaderColour_PrintUsage(client); return Plugin_Handled; } char arg[32]; GetCmdArg(1, arg, sizeof(arg)); TrimString(arg); if (StrEqual(arg, "default", false) || StrEqual(arg, "reset", false)) { g_bHasCustomColour[client] = false; LeaderColour_SaveCookie(client); Effects_RefreshXray(client); PrintToChat(client, "%sColour reset to the default %s colour.", PREFIX, IsPrimaryLeader(client) ? "primary leader" : "sub-leader"); return Plugin_Handled; } int rgb[3]; if (!ParseCustomColour(arg, rgb)) { LeaderColour_PrintUsage(client); return Plugin_Handled; } // Refuse colours too dark to see as a xray tint or beam. if (rgb[0] < 40 && rgb[1] < 40 && rgb[2] < 40) { PrintToChat(client, "%sThat colour is too dark to be visible. Pick a brighter one.", PREFIX); return Plugin_Handled; } g_iCustomColour[client][0] = rgb[0]; g_iCustomColour[client][1] = rgb[1]; g_iCustomColour[client][2] = rgb[2]; g_bHasCustomColour[client] = true; LeaderColour_SaveCookie(client); Effects_RefreshXray(client); PrintToChat(client, "%sColour set to \x07%02X%02X%02X#%02X%02X%02X\x01.", PREFIX, rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]); return Plugin_Handled; } public void OnClientCookiesCached(int client) { if (!IsFakeClient(client)) LeaderColour_LoadCookie(client); } void LeaderColour_LoadCookie(int client) { char buf[16]; g_hColourCookie.Get(client, buf, sizeof(buf)); int rgb[3]; if (buf[0] && ParseCustomColour(buf, rgb)) { g_iCustomColour[client][0] = rgb[0]; g_iCustomColour[client][1] = rgb[1]; g_iCustomColour[client][2] = rgb[2]; g_bHasCustomColour[client] = true; } } void LeaderColour_SaveCookie(int client) { if (g_bHasCustomColour[client]) { char buf[8]; Format(buf, sizeof(buf), "%02X%02X%02X", g_iCustomColour[client][0], g_iCustomColour[client][1], g_iCustomColour[client][2]); g_hColourCookie.Set(client, buf); } else { g_hColourCookie.Set(client, ""); } } void LeaderColour_PrintUsage(int client) { char names[256]; for (int i = 0; i < sizeof(g_sColourNames); i++) { if (names[0]) StrCat(names, sizeof(names), ", "); StrCat(names, sizeof(names), g_sColourNames[i]); } PrintToChat(client, "%sUsage: \x04!leadercolour \x01", PREFIX); PrintToChat(client, "%sColours: \x04%s\x01, or a hex code like \x04FF66CC\x01.", PREFIX, names); } bool ParseCustomColour(const char[] str, int rgb[3]) { // Colour name for (int i = 0; i < sizeof(g_sColourNames); i++) { if (StrEqual(str, g_sColourNames[i], false)) { rgb[0] = g_iColourValues[i][0]; rgb[1] = g_iColourValues[i][1]; rgb[2] = g_iColourValues[i][2]; return true; } } // Hex code: RRGGBB, optionally prefixed with # or 0x int offset = 0; if (str[0] == '#') offset = 1; else if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) offset = 2; if (strlen(str[offset]) != 6) return false; int value; if (StringToIntEx(str[offset], value, 16) != 6) return false; rgb[0] = (value >> 16) & 0xFF; rgb[1] = (value >> 8) & 0xFF; rgb[2] = value & 0xFF; return true; } //---------------------------------------------------------------------------------------------------- // View tracer: hold +use to show, tap +use to toggle. Visual only; buttons are never modified. //---------------------------------------------------------------------------------------------------- void Effects_OnPlayerRunCmd(int client, int buttons) { bool pressed = (buttons & IN_USE) && !(g_iLastButtons[client] & IN_USE); bool released = !(buttons & IN_USE) && (g_iLastButtons[client] & IN_USE); g_iLastButtons[client] = buttons; if (!IsEffectsActive(client) || g_iTracerMode[client] == TRACER_MODE_OFF) return; if (pressed) { g_fUsePressedAt[client] = GetGameTime(); } else if (released && g_iTracerMode[client] == TRACER_MODE_TAP && GetGameTime() - g_fUsePressedAt[client] < g_cvTracerTapTime.FloatValue) { g_iEffectFlags[client] ^= EFFECT_TRACER_TOGGLED; } bool active = (buttons & IN_USE) != 0 || (g_iEffectFlags[client] & EFFECT_TRACER_TOGGLED) != 0; if (!active) return; float now = GetGameTime(); if (now < g_fNextTracerSend[client]) return; g_fNextTracerSend[client] = now + g_cvTracerInterval.FloatValue; Tracer_Draw(client); } void Tracer_Draw(int client) { float endPos[3]; if (!TracePlayerAngles(client, endPos)) return; float startPos[3]; GetClientEyePosition(client, startPos); startPos[2] -= 5.0; // keep the beam out of the leader's own crosshair int colour[4]; GetLeaderColour(client, colour); TE_SetupBeamPoints(startPos, endPos, g_iBeamSprite, g_iHaloSprite, 0, 0, g_cvTracerLife.FloatValue, g_cvTracerWidth.FloatValue, g_cvTracerWidth.FloatValue, 0, 0.0, colour, 0); TE_SendToAll(); } void Tracer_CycleMode(int client) { // Rotate Hold + Tap-toggle -> Hold only -> Off -> Hold + Tap-toggle. switch (g_iTracerMode[client]) { case TRACER_MODE_TAP: g_iTracerMode[client] = TRACER_MODE_HOLD; case TRACER_MODE_HOLD: g_iTracerMode[client] = TRACER_MODE_OFF; default: g_iTracerMode[client] = TRACER_MODE_TAP; } g_iEffectFlags[client] &= ~EFFECT_TRACER_TOGGLED; char mode[40]; switch (g_iTracerMode[client]) { case TRACER_MODE_TAP: mode = "hold +USE OR tap to toggle"; case TRACER_MODE_HOLD: mode = "hold +USE only"; default: mode = "off"; } PrintToChat(client, "%sTracer mode: \x04%s\x01.", PREFIX, mode); }