Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 46 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,71 @@

</div>

**sat·set** /sat-sèt/ *adjective (slang)*: Indonesian colloquialism for being rapid, efficient, and quick to act.
**sat·set** /sat-sèt/ *adjective (slang)*: Indonesian slang for doing something quickly.

> *"Sat set, sampai."* means "Swiftly done."

Satset is a buffer-backed networking library for [Roblox](https://roblox.com). It handles packet serialization, batching, rate limiting, and state synchronization. The public API covers stateless events (Packets) and delta-compressed state (Channels).
Satset is a buffer-backed networking library for [Roblox](https://roblox.com). It handles packet serialization, batching, rate limiting, and state sync. The public API covers stateless events (Packets) and bitmask-tracked state (Channels).

Satset keeps the send path in Luau `buffer` objects where possible. Packet listeners receive decoded tables, and channel subscribers receive reconstructed state tables.
Satset keeps packet data in Luau `buffer` objects until the API boundary. Packet listeners receive decoded tables. Channel subscribers receive reconstructed state tables.

# Performance Benchmarks

The [benchmark suite](benchmark/Benchmarks.md) compares Satset with native Roblox remotes and community networking libraries. It sends 200 events per frame for 10 seconds per payload. The tables report Roblox `Stats.DataSendKbps` p50 values normalized to a 60 FPS baseline.
The benchmark suite in `benchmark/` compares Satset with native Roblox remotes and community networking libraries. It sends 200 events per frame for 10 seconds per payload. The tables report Roblox `Stats.DataSendKbps` p50 values normalized to a 60 FPS baseline.

## Batching Profiles
## Latest Full Run

The latest benchmark uses two Satset profiles:
Lower normalized bandwidth is better. The latest full Studio run uses one Satset runtime path.

| Payload | Default normalized | Latency normalized | Default commits/frame | Latency commits/frame |
| :--- | ---: | ---: | ---: | ---: |
| Vectors | 118.13 | 38.84 | 3 | 1 |
| Booleans | 10.43 | 10.38 | 1 | 1 |
| Mixed | 4.29 | 4.31 | 1 | 1 |
| Entities | 117.53 | 39.15 | 3 | 1 |
| Strings | 899.63 | 96.37 | 8 | 1 |
| SingleValue | 2.39 | 2.38 | 1 | 1 |
| Payload | **Satset** rank | Winner | **Satset** Kbps (p50) | Notes |
| :--- | ---: | :--- | ---: | :--- |
| Vectors | 1 / 10 | Satset | 2.51 | 120.2K / 120.2K received |
| Booleans | 1 / 10 | Satset | 2.36 | 120.2K / 120.2K received |
| Mixed | 2 / 10 | Warp | 2.32 | Warp leads by about 3.6% |
| Entities | 1 / 10 | Satset | 2.64 | 120.2K / 120.2K received |
| SingleValue | 2 / 10 | Warp | 2.28 | Warp leads by about 3.8% |
| Strings | 1 / 10 valid rows | Satset | 3.28 | ByteNet stopped after 8 frames |

> [!NOTE]
> `Normalized` is `Stats.DataSendKbps` adjusted to 60 FPS. The default benchmark profile uses `reliableThreshold = 60000` and `maxPacketsPerFrame = 20`. The latency profile uses `reliableThreshold = 0` and `maxPacketsPerFrame = 0`.
Satset completed all payload rows at 601 frames, 60 FPS minimum, and zero packet loss. Reliable delta counters matched on both sides: `deltaEncoded == deltaDecoded == 600`.

Detailed methodology and raw data are in the [Benchmarks Report](benchmark/Benchmarks.md).
Raw data is in [benchmark-result.json](benchmark/benchmark-result.json).

# Documentation

Technical documentation is available in the `docs/` directory:
Technical documentation lives in the `docs/` directory:

- **[Architecture & Getting Started](docs/guide/getting-started.md)**: High-level overview and initialization.
- **[API Reference](docs/api/satset.md)**: Detailed breakdown of the `Satset` namespace.
- **[API Reference](docs/api/satset.md)**: `Satset` namespace, packets, channels, guards, and types.
- **[Development Patterns](docs/guide/development-patterns.md)**: Design rules and performance constraints.
- **[Security & Guard](docs/guide/security.md)**: Documentation on the token bucket rate limiting implementation.
- **[Serialization Types](docs/api/types.md)**: Available data types for buffer-backed schemas.

# Contributing

Contributions are welcome! Please review our **[Contribution Guide](CONTRIBUTING.md)** and **[Development Patterns](docs/guide/development-patterns.md)** before submitting a pull request.
Before opening a pull request, read the **[Contribution Guide](CONTRIBUTING.md)** and **[Development Patterns](docs/guide/development-patterns.md)**.

# Features

## Hybrid Networking Engine

Satset provides two distinct communication modes:
Satset has two public networking surfaces:

- **Packets (Stateless)**: For one-off events like character actions or effects. These are batched automatically every frame to minimize RemoteEvent overhead.
- **Channels (Stateful)**: The core state synchronization engine. It tracks changes to a defined schema and transmits only the dirty fields (deltas) using bitmask-based compression.
- **Packets (Stateless)**: One-off events like character actions or effects. Satset batches them every frame.
- **Channels (Stateful)**: State sync for fixed-size schemas. Channels write state into a buffer, mark dirty fields with a bitmask, and send changed bytes between keyframes.

## Implementation Details

- **Buffer-backed batching**: Outgoing payloads are encoded into Luau buffers and committed as exact-size buffers before transport.
- **Nested Structs**: Use `Satset.struct(schema)` to create reusable, composable type objects for complex nested schemas.
- **Readable Type Names**: Provides human-readable aliases (`string`, `uint8`, `float64`, etc.) alongside shorthand forms for cleaner, more self-documenting schemas.
- **Reliable run grouping**: Same-packet reliable runs share one packet id and one run count.
- **Reliable delta encoding**: Direct reliable traffic tracks the previous batch and XORs same-size batches. Broadcast reliable traffic stays raw.
- **Nested structs**: Use `Satset.struct(schema)` to build reusable type objects.
- **ByteNet-style aliases**: `string`, `uint8`, `float64`, and related names map to Satset's shorthand types.
- **Packet dispatch**: Incoming batches are decoded in place. Each packet listener receives a decoded table.
- **Built-in Security**: Relies on Luau's native buffer bounds checks. Payload errors are caught via `pcall` before they reach game code.
- **Buffer Safety**: Dynamic data (strings/arrays) is capped relative to the physical buffer size to prevent memory-related issues.
- **Sanitized Floats**: Floating-point types (`f32`, `f64`, `Vector3`, etc.) are clamped to 0 if they are `NaN` or `±Infinity` to prevent state corruption.
- **Header Stripping**: Automatically identifies fixed-size schemas and omits size headers when possible to reduce protocol overhead.
- **Batch Segmentation**: Reliable batches split at `reliableThreshold`. Unreliable batches split around the configured unreliable threshold, which defaults to 900 bytes.
- **Guard**: Built-in server-side rate limiting using a token bucket algorithm to prevent spam.
- **Bounds checks**: Payload errors are caught through protected calls before game code receives data.
- **Buffer safety**: Dynamic data, such as strings and arrays, is capped against the physical buffer size.
- **Float sanitization**: Floating-point types (`f32`, `f64`, `Vector3`, etc.) clamp `NaN` and `±Infinity` to `0`.
- **Header stripping**: Fixed-size schemas omit per-payload size headers.
- **Guard**: Built-in server-side rate limiting through a token bucket.

# Architecture

Expand Down Expand Up @@ -120,12 +120,13 @@ flowchart TB
SR --> SC
SR --> SN
SC --> TP
TP --> SN

PK -->|"allocate stream slot"| BT
PK -->|"encodeInto(buffer, offset)"| SR
CH -->|"encodeDelta(bitmask)"| BT

BT -->|"flush & segment"| BR
BT -->|"flush, group, delta"| BR

BR --> RE
BR --> URE
Expand All @@ -136,7 +137,8 @@ flowchart TB
GD -->|"consume(player)"| PK
GD -->|"consume(player)"| CH

SN -.->|"clamp NaN/Inf"| SR
SN -.->|"bounds + float checks"| SR
SN -.->|"float checks"| TP
```

For a detailed step-by-step walkthrough of a packet's lifecycle, see the [Architecture Guide](docs/guide/architecture.md).
Expand Down Expand Up @@ -164,12 +166,14 @@ local Satset = require(ReplicatedStorage.Packages.Satset)

Satset.start({
guard = {
maxTokens = 60,
refillRate = 30,
maxTokens = 1000,
refillRate = 500,
studioBypass = true,
},
batching = {
reliableThreshold = 60000, -- Split reliable batches above 60 KB
maxPacketsPerFrame = 0, -- Send all ready batches each frame
reliableThreshold = 0, -- Commit reliable traffic at the frame flush
unreliableThreshold = 900, -- Keep UnreliableRemoteEvent payloads small
maxPacketsPerFrame = 0, -- No per-frame send cap
}
})
```
Expand Down Expand Up @@ -227,9 +231,9 @@ Packets.Damage:listen(function(data)
end)
```

## Channels (Stateful Synchronization)
## Channels (Stateful Sync)

Channels are for data that has "state" (like health or positions). They use **delta-compression** and are much more efficient than packets for frequent updates.
Channels are for data that has state, such as health or positions. The first flush sends a full keyframe. Later flushes send only fields marked by the dirty bitmask.

**Shared Definition:**

Expand All @@ -247,7 +251,7 @@ return {
position = Types.Vector3Quantized(2048)
},
unreliable = true,
resyncInterval = 5 -- Periodic keyframe to prevent drift
resyncInterval = 5 -- Periodic keyframe after dropped unreliable updates
})
}
```
Expand All @@ -257,13 +261,13 @@ return {
```luau
local Channels = require(path.to.Shared.Channels)

-- Create an entity instance for a player
-- Create state for a player
local entity = Channels.PlayerState:create(player.UserId, {
health = 100,
position = Vector3.new(0, 5, 0)
})

-- Update state (only changed fields are transmitted)
-- Only changed fields are sent on the next flush
entity:set("health", 85)
```

Expand All @@ -272,7 +276,6 @@ entity:set("health", 85)
```luau
local Channels = require(path.to.Shared.Channels)

-- Subscribe to state changes
Channels.PlayerState:subscribe(function(entityId, state)
print("Entity", entityId, "updated. Health:", state.health)
end)
Expand Down
Loading
Loading