How to control entity visibility and network transmission using CheckTransmit system.
The CheckTransmit system in Source 2 allows you to control which entities are networked to specific players. This powerful feature enables creating advanced gameplay mechanics like hiding/showing entities, creating player-specific visibility rules, and optimizing network traffic.
What is CheckTransmit?: CheckTransmit is called every tick to determine which entities should be transmitted (networked) to each player. By hooking into this system, you can dynamically control what each player sees.
Register the CheckTransmit hook in your plugin's start method and unregister it on plugin end:
c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;
public unsafe class Sample : Plugin
{
public void OnPluginStart()
{
OnServerCheckTransmit_Register(OnCheckTransmit);
}
public void OnPluginEnd()
{
OnServerCheckTransmit_Unregister(OnCheckTransmit);
}
private void OnCheckTransmit(nint[] infos)
{
// infos array index represents player slot (0-63)
for (int playerSlot = 0; playerSlot < infos.Length; playerSlot++)
{
var info = infos[playerSlot];
// Modify transmit states for this player slot
}
}
}
#include <plg/plugin.hpp>
#include "s2sdk.hpp"
using namespace s2sdk;
class Sample : public plg::IPluginEntry {
public:
void OnPluginStart() override {
OnServerCheckTransmit_Register(
[](const plg::vector<void*>& infos) {
// infos array index represents player slot (0-63)
for (size_t playerSlot = 0; playerSlot < infos.size(); playerSlot++) {
auto* info = infos[playerSlot];
// Modify transmit states for this player slot
}
});
}
void OnPluginEnd() override {
OnServerCheckTransmit_Unregister();
}
};
from plugify.plugin import Plugin
from plugify.pps import s2sdk as s2
class Sample(Plugin):
def plugin_start(self):
s2.OnServerCheckTransmit_Register(self.on_check_transmit)
def plugin_end(self):
s2.OnServerCheckTransmit_Unregister(self.on_check_transmit)
def on_check_transmit(self, infos):
# infos array index represents player slot (0-63)
for player_slot, info in enumerate(infos):
# Modify transmit states for this player slot
pass
package main
import (
"s2sdk"
"github.com/untrustedmodders/go-plugify"
)
func init() {
plugify.OnPluginStart(func() {
s2sdk.OnServerCheckTransmit_Register(func(infos []s2sdk.CCheckTransmitInfo) {
// infos array index represents player slot (0-63)
for playerSlot, info := range infos {
// Modify transmit states for this player slot
_ = playerSlot
_ = info
}
})
})
plugify.OnPluginEnd(func() {
s2sdk.OnServerCheckTransmit_Unregister()
})
}
import { Plugin } from 'plugify';
import * as s2 from ':s2sdk';
export class Sample extends Plugin {
pluginStart() {
s2.OnServerCheckTransmit_Register((infos) => {
// infos array index represents player slot (0-63)
infos.forEach((info, playerSlot) => {
// Modify transmit states for this player slot
});
});
}
pluginEnd() {
s2.OnServerCheckTransmit_Unregister();
}
}
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local Sample = {}
setmetatable(Sample, { __index = Plugin })
function Sample:plugin_start()
s2:OnServerCheckTransmit_Register(function(infos)
-- infos array index represents player slot (0-63)
for playerSlot, info in ipairs(infos) do
-- Modify transmit states for this player slot
end
end)
end
function Sample:plugin_end()
s2:OnServerCheckTransmit_Unregister()
end
local M = {}
M.Sample = Sample
return M
Each CheckTransmitInfo object contains information about transmit states for a specific player. The player slot is determined by the array index (0-63), not by a field in the structure.
m_pTransmitEntity: BitVec controlling which entities are transmitted
m_pTransmitNonPlayers: BitVec for non-player entities
m_pTransmitAlways: BitVec for entities that should always transmit
m_vecTargetSlots: List of target player slots
m_bFullUpdate: Whether to send a full update
Player Slot: The position of CheckTransmitInfo in the infos array represents the player slot. infos[0] is for player slot 0, infos[1] is for player slot 1, etc.
BitVec Explained: A BitVec is an efficient data structure that uses individual bits to track entity states. Each bit position corresponds to an entity index.
SetTransmitInfoEntity(info, entityHandle); // Mark entity as transmittable
ClearTransmitInfoEntity(info, entityHandle); // Mark entity as not transmittable
IsTransmitInfoEntitySet(info, entityHandle); // Check if entity is transmittable
SetTransmitInfoEntityAll(info); // Mark all entities as transmittable
ClearTransmitInfoEntityAll(info); // Mark all entities as not transmittable
SetTransmitInfoNonPlayer(info, entityHandle); // Mark non-player entity as transmittable
ClearTransmitInfoNonPlayer(info, entityHandle); // Mark non-player entity as not transmittable
IsTransmitInfoNonPlayerSet(info, entityHandle); // Check if non-player is transmittable
SetTransmitInfoNonPlayerAll(info); // Mark all non-players as transmittable
ClearTransmitInfoNonPlayerAll(info); // Mark all non-players as not transmittable
SetTransmitInfoAlways(info, entityHandle); // Mark entity to always transmit
ClearTransmitInfoAlways(info, entityHandle); // Unmark entity from always transmit
IsTransmitInfoAlwaysSet(info, entityHandle); // Check if entity always transmits
SetTransmitInfoAlwaysAll(info); // Mark all entities to always transmit
ClearTransmitInfoAlwaysAll(info); // Unmark all entities from always transmit
GetTransmitInfoTargetSlotsCount(info); // Get count of target slots
GetTransmitInfoTargetSlot(info, index); // Get specific target slot
AddTransmitInfoTargetSlot(info, playerSlot); // Add a target slot
RemoveTransmitInfoTargetSlot(info, index); // Remove target slot by index
GetTransmitInfoTargetSlotsAll(info); // Get all target slots
RemoveTransmitInfoTargetSlotsAll(info); // Clear all target slots
In languages that support it, you can construct a CheckTransmitInfo class from the pointer for more intuitive OOP-style access:
c#
c++
private void OnCheckTransmit(nint[] infos)
{
for (int playerSlot = 0; playerSlot < infos.Length; playerSlot++)
{
// Construct OOP wrapper from pointer
var info = new CheckTransmitInfo(infos[playerSlot]);
// Use object methods instead of static functions
info.SetEntity(entityHandle);
info.ClearEntity(entityHandle);
bool isSet = info.IsEntitySet(entityHandle);
// Note: playerSlot is from the array index, not a method
info.SetFullUpdate(true);
}
}
void OnCheckTransmit(const plg::vector<void*>& infos) {
for (size_t playerSlot = 0; playerSlot < infos.size(); playerSlot++) {
// Use the pointer directly with methods
CheckTransmitInfo info(infos[playerSlot]);
info.SetEntity(entityHandle);
info.ClearEntity(entityHandle);
bool isSet = info.IsEntitySet(entityHandle);
// Note: playerSlot is from the array index, not a method
info.SetFullUpdate(true);
}
}
Hide entities from specific players based on conditions:
c#
using Plugify;
using static s2sdk.s2sdk;
using System.Collections.Generic;
public unsafe class PlayerVisibility : Plugin
{
// Map of player slot -> list of hidden entity handles
private Dictionary<int, List<int>> playerHiddenEntities = new();
public void OnPluginStart()
{
OnServerCheckTransmit_Register(OnCheckTransmit);
}
public void OnPluginEnd()
{
OnServerCheckTransmit_Unregister(OnCheckTransmit);
}
// Hide a specific entity from a specific player
public void HideEntityFromPlayer(int playerSlot, int entityHandle)
{
if (!playerHiddenEntities.ContainsKey(playerSlot))
{
playerHiddenEntities[playerSlot] = new List<int>();
}
if (!playerHiddenEntities[playerSlot].Contains(entityHandle))
{
playerHiddenEntities[playerSlot].Add(entityHandle);
}
}
// Show a previously hidden entity to a player
public void ShowEntityToPlayer(int playerSlot, int entityHandle)
{
if (playerHiddenEntities.ContainsKey(playerSlot))
{
playerHiddenEntities[playerSlot].Remove(entityHandle);
}
}
private void OnCheckTransmit(nint[] infos)
{
for (int playerSlot = 0; playerSlot < infos.Length; playerSlot++)
{
// Using OOP style
var info = new CheckTransmitInfo(infos[playerSlot]);
// Check if this player has any hidden entities
if (playerHiddenEntities.ContainsKey(playerSlot))
{
foreach (int entityHandle in playerHiddenEntities[playerSlot])
{
// Only hide if entity is still valid
if (IsValidEntHandle(entityHandle))
{
info.ClearEntity(entityHandle);
info.ClearAlways(entityHandle);
}
}
}
}
}
}
Display entities only to players on the same team:
c#
using Plugify;
using static s2sdk.s2sdk;
using System.Collections.Generic;
public unsafe class TeamVisibility : Plugin
{
// Map of entity handles to team numbers
private Dictionary<int, int> teamEntities = new();
public void OnPluginStart()
{
OnServerCheckTransmit_Register(OnCheckTransmit);
}
public void OnPluginEnd()
{
OnServerCheckTransmit_Unregister(OnCheckTransmit);
}
// Register an entity to be visible only to a specific team
public void SetEntityTeamVisibility(int entityHandle, int team)
{
teamEntities[entityHandle] = team;
}
private void OnCheckTransmit(nint[] infos)
{
for (int playerSlot = 0; playerSlot < infos.Length; playerSlot++)
{
var info = new CheckTransmitInfo(infos[playerSlot]);
// Get player's team
int playerHandle = PlayerSlotToEntHandle(playerSlot);
if (!IsValidEntHandle(playerHandle))
continue;
int playerTeam = GetEntityTeam(playerHandle);
// Check each team-restricted entity
foreach (var kvp in teamEntities)
{
int entityHandle = kvp.Key;
int requiredTeam = kvp.Value;
if (!IsValidEntHandle(entityHandle))
continue;
// Hide entity if player is not on the correct team
if (playerTeam != requiredTeam)
{
info.ClearEntity(entityHandle);
info.ClearAlways(entityHandle);
}
else
{
// Ensure it's visible to team members
info.SetEntity(entityHandle);
}
}
}
}
private int GetEntityTeam(int entityHandle)
{
// Assuming you have a way to get team number from entity
// This is just an example - implement based on your schema access
return GetEntSchemaInt32(entityHandle, "CBaseEntity", "m_iTeamNum", false, 0);
}
}
using Plugify;
using static s2sdk.s2sdk;
using System.Collections.Generic;
public unsafe class FullUpdateControl : Plugin
{
private HashSet<int> playersNeedingFullUpdate = new();
public void OnPluginStart()
{
OnServerCheckTransmit_Register(OnCheckTransmit);
// Hook player spawn to force full update
HookEvent("player_spawn", Event_PlayerSpawn, HookMode.Post);
}
public void OnPluginEnd()
{
OnServerCheckTransmit_Unregister(OnCheckTransmit);
}
private void Event_PlayerSpawn(string name, nint @event, bool dontBroadcast)
{
int playerSlot = GetEventPlayerSlot(@event, "userid", 0);
// Mark player for full update on next transmit
playersNeedingFullUpdate.Add(playerSlot);
}
private void OnCheckTransmit(nint[] infos)
{
for (int playerSlot = 0; playerSlot < infos.Length; playerSlot++)
{
var info = new CheckTransmitInfo(infos[playerSlot]);
// Check if this player needs a full update
if (playersNeedingFullUpdate.Contains(playerSlot))
{
info.SetFullUpdate(true);
// Remove after applying once
playersNeedingFullUpdate.Remove(playerSlot);
PrintToServer($"Sending full update to player {playerSlot}\n");
}
}
}
}
Problem: Server lag after implementing CheckTransmit
Solution: Profile your callback - likely doing too much work per tick. Cache data and minimize operations.
Problem: Transmit modifications don't seem to work
Solution: Ensure you're modifying the correct CheckTransmitInfo for the target player. Verify entity handles are valid.
The CheckTransmit system is a powerful tool for controlling entity visibility in Source 2. By hooking into the server's transmit decisions, you can create sophisticated gameplay mechanics, optimize network traffic, and build unique player experiences.
Key takeaways:
Register hooks in OnPluginStart, unregister in OnPluginEnd
Use either functional or OOP API based on preference
Keep callbacks efficient - they run every tick
Always validate entity handles before use
Clear both Entity and Always flags when hiding entities
Further Reading: Check out the Handle System guide for more information on working with entity handles and player slots.