In Source 1, entities had two types of properties: send props (synchronized with clients) and data props (server-only). Source 2 simplified this by merging them into a unified schema system. Schemas define the structure and properties of all game objects, and many schema fields are networked automatically.
Schemas allow you to access and modify properties of any entity or object in Source 2, including health, position, velocity, model, team, and countless other attributes. When you modify networked schema fields with changeState set to true, the changes are automatically synchronized to clients.
Here's a simple example of reading and modifying an entity's health:
c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;
public unsafe class Sample : Plugin
{
public void OnPluginStart()
{
AddConsoleCommand("set_health", "Sets player health to 200",
ConVarFlag.LinkedConcommand | ConVarFlag.Release,
(caller, context, arguments) =>
{
if (caller == -1) return ResultType.Handled;
// Convert player's slot to entity's handle
int player = PlayerSlotToEntHandle(caller);
// Set the caller's health to 200
SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, true, 0);
// Read back the health
int health = (int)GetEntSchema(player, "CBaseEntity", "m_iHealth", 0);
PrintToServer($"Player health set to {health}\n");
return ResultType.Handled;
}, HookMode.Post);
}
}
#include <plugify/cpp_plugin.hpp>
#include "s2sdk.hpp"
using namespace s2sdk;
class Sample : public plg::IPluginEntry {
public: void OnPluginStart() override {
ConVarFlag flags = ConVarFlag::LinkedConcommand | ConVarFlag::Release;
AddConsoleCommand("set_health", "Sets player health to 200", flags,
[](int caller, int context, const plg::vector<plg::string>& arguments) -> ResultType {
if (caller == -1) return ResultType::Handled;
// Convert player's slot to entity's handle
int player = PlayerSlotToEntHandle(caller);
// Set the caller's health to 200
SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, true, 0);
// Read back the health
int64_t health = GetEntSchema(player, "CBaseEntity", "m_iHealth", 0);
PrintToServer(std::format("Player health set to {}\n", health).c_str());
return ResultType::Handled;
}, HookMode::Post);
}
};
from plugify.plugin import Plugin
from plugify.pps import s2sdk as s2
class Sample(Plugin):
def plugin_start(self):
flags = s2.ConVarFlag.LinkedConcommand | s2.ConVarFlag.Release
def set_health_cmd(caller, context, arguments):
if caller == -1:
return s2.ResultType.Handled
# Convert player's slot to entity's handle
player = s2.PlayerSlotToEntHandle(caller);
# Set the caller's health to 200
s2.SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, True, 0)
# Read back the health
health = s2.GetEntSchema(player, "CBaseEntity", "m_iHealth", 0)
s2.PrintToServer(f"Player health set to {health}\n")
return s2.ResultType.Handled
s2.AddConsoleCommand("set_health", "Sets player health to 200", flags,
set_health_cmd, s2.HookMode.Post)
package main
import (
"fmt"
"s2sdk"
"github.com/untrustedmodders/go-plugify"
)
func init() {
plugify.OnPluginStart(func() {
flags := s2sdk.ConVarFlag.LinkedConcommand | s2sdk.ConVarFlag.Release
s2sdk.AddConsoleCommand("set_health", "Sets player health to 200", flags,
func(caller int, context int, arguments []string) s2sdk.ResultType {
if caller == -1 {
return s2sdk.ResultType.Handled
}
// Convert player's slot to entity's handle
int player = s2sdk.PlayerSlotToEntHandle(caller);
// Set the caller's health to 200
s2sdk.SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, true, 0)
// Read back the health
health := s2sdk.GetEntSchema(player, "CBaseEntity", "m_iHealth", 0)
s2sdk.PrintToServer(fmt.Sprintf("Player health set to %d\n", health))
return s2sdk.ResultType.Handled
}, s2sdk.HookMode.Post)
})
}
import { Plugin } from 'plugify';
import { s2 } from ':s2sdk';
export class Sample extends Plugin {
pluginStart() {
const flags = s2.ConVarFlag.LinkedConcommand | s2.ConVarFlag.Release;
s2.AddConsoleCommand("set_health", "Sets player health to 200", flags,
(caller, context, arguments) => {
if (caller === -1) return s2.ResultType.Handled;
// Convert player's slot to entity's handle
const player = s2.PlayerSlotToEntHandle(caller);
// Set the caller's health to 200
s2.SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, true, 0);
// Read back the health
const health = s2.GetEntSchema(player, "CBaseEntity", "m_iHealth", 0);
s2.PrintToServer(`Player health set to ${health}\n`);
return s2.ResultType.Handled;
}, s2.HookMode.Post);
}
}
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local Sample = {}
setmetatable(Sample, { __index = Plugin })
function Sample:plugin_start()
local flags = bit.bor(s2.ConVarFlag.LinkedConcommand, s2.ConVarFlag.Release)
s2:AddConsoleCommand("set_health", "Sets player health to 200", flags,
function(caller, context, arguments)
if caller == -1 then return s2.ResultType.Handled end
-- Convert player's slot to entity's handle
local player = s2:PlayerSlotToEntHandle(caller);
-- Set the caller's health to 200
s2:SetEntSchema(player, "CBaseEntity", "m_iHealth", 200, true, 0)
-- Read back the health
local health = s2:GetEntSchema(player, "CBaseEntity", "m_iHealth", 0)
s2:PrintToServer(string.format("Player health set to %d\n", health))
return s2.ResultType.Handled
end, s2.HookMode.Post)
end
local M = {}
M.Sample = Sample
return M
Some schema fields are arrays. Use the element parameter to access specific array indices:
// Get array size
int arraySize = GetEntSchemaArraySize(entityHandle, "SomeClass", "m_arrayField");
// Access array elements (element parameter is the last one before type-specific defaults)
for (int i = 0; i < arraySize; i++)
{
int value = (int)GetEntSchema(entityHandle, "SomeClass", "m_arrayField", i);
PrintToServer($"Array[{i}] = {value}\n");
}
// Set array element
SetEntSchema(entityHandle, "SomeClass", "m_arrayField", 42, true, 2); // Set index 2 to 42
When true, marks the field as changed and triggers network synchronization to clients. Set to true when modifying networked fields, false for server-only changes.
Advanced parameter for chain offsets in inheritance hierarchies. Use 0 for most cases, or use GetSchemaChainOffset() for specific inheritance chains. Use -1 for non-entity classes.
using Plugify;
using static s2sdk.s2sdk;
public unsafe class SpeedMod : Plugin
{
public void OnPluginStart()
{
AddConsoleCommand("speed", "Change player speed",
ConVarFlag.LinkedConcommand | ConVarFlag.Release,
Command_Speed, HookMode.Post);
}
public ResultType Command_Speed(int caller, int context, string[] arguments)
{
if (caller == -1) return ResultType.Handled;
if (arguments.Length < 2)
{
PrintToServer("Usage: speed <multiplier> (1.0 = normal)\n");
return ResultType.Handled;
}
int player = PlayerSlotToEntHandle(caller);
// Get pawn entity from player
int entityHandle = GetEntSchemaEnt(player, "CCSPlayerController", "m_hPlayerPawn");
float speedMultiplier = float.Parse(arguments[1]);
// Set speed on player pawn
SetEntSchemaFloat(entityHandle, "CCSPlayerPawn", "m_flVelocityModifier", speedMultiplier, true, 0);
PrintToServer($"Speed set to {speedMultiplier}x\n");
return ResultType.Handled;
}
}
For performance-critical code, you can use raw offsets:
// Get offset once and cache it
int healthOffset = GetSchemaOffset("CBaseEntity", "m_iHealth");
int chainOffset = GetSchemaChainOffset("CBaseEntity");
// Use offset directly for better performance
SetEntData(entityHandle, healthOffset, 100, 4, true, chainOffset);
int health = (int)GetEntData(entityHandle, healthOffset, 4);
For non-entity schema objects (when handles aren't available):
// Example: Working with a non-entity schema object
nint objectPointer = GetSomeNonEntityPointer();
// Use the '2' version of functions
SetEntSchema2(objectPointer, "SomeClass", "m_someField", 42, true, 0);
int value = (int)GetEntSchema2(objectPointer, "SomeClass", "m_someField", 0);
// Update specific field
NetworkStateChanged(entityHandle, "CBaseEntity", "m_iHealth");
// Or using offset
int offset = GetSchemaOffset("CBaseEntity", "m_iHealth");
int chainOffset = GetSchemaChainOffset("CBaseEntity");
ChangeEntityState(entityHandle, offset, chainOffset);