Elo-Aware Auto Assignment & Player Lifecycle Logger
This plugin overrides Squad's native team assignment mechanics to provide smart, fair, and fast team placements. When a player connects, the plugin evaluates the current Elo distribution and population difference between both teams and assigns the player to whichever team produces the most balanced match. All team changes are executed via RCON using a One-Hit & Verify approach, with a hard 3-second timeout ensuring predictable behavior.
The core timing challenge — Squad's RCON player list only refreshes every ~30 seconds — is solved by triggering the RCON move command directly from the Log Parser event (which fires within ~100ms of join), and then force-polling the player list after the command lands to verify the result. This approach typically achieves verified join-swaps in 1–2 seconds, with a hard 3-second completion guarantee.
Disconnect detection works via delta-diff: every time any player joins and triggers a forced RCON refresh, the player list is compared against the known state, which catches departures from other players as a side-effect — effectively solving disconnect lag without relying on the unreliable PLAYER_DISCONNECTED log event.
- Sub-2s Verified Join Swaps: Uses Log-Driven triggering + One-Hit & Verify to move players within ~1s of joining, verified against a fresh RCON poll.
- Strict Population Balance: Dynamically adjusts the allowed team population difference based on total player count, enforcing a strict 1-player max difference at high population.
- Reconnect Memory: Player disconnect states are stored in a fast in-memory Map for instant lookups on rejoin. The database serves as a crash-recovery backing store, written asynchronously on disconnect and re-hydrated into memory when the plugin restarts within the same round. If a player crashes or disconnects, they are automatically placed back on their previous team upon reconnecting (with a +1 imbalance grace allowance).
- Clan Grouping: Detects clan tags in player names (supports 5 detection strategies: brackets, separators, spaces, ALL-CAPS, and bare prefixes) and keeps clan members together on the same team when joining. If all clan mates are on one team and population caps allow, the joining player is routed to that team. Uses case-insensitive matching with Unicode normalization and Levenshtein edit-distance merging.
- Elo-Aware Routing: Integrates with the
EloTrackerplugin to route new players to the team that will most closely equalize the average skill of both sides. - Passive Mode: Set
enableSmartAssign: falseto observe real server events only (JOIN,LEAVE,TEAM_CHANGE). The assignment algorithm does not run, and noASSIGNMENTevents are logged—this mode is useful for monitoring server activity without any intervention. - Lifecycle Event Logging: Dumps precise
JOIN,LEAVE,TEAM_CHANGE,ASSIGNMENT,MOVE_SUCCESS, andMOVE_FAILEDevents into an easily ingestible JSONL file, with global team populations (t1,t2) embedded on every event. - High-Performance Logging: Events are batched in-memory and flushed periodically to minimize disk I/O overhead during large player waves.
- Round Snapshots: Automatically takes a full snapshot of connected players at the start of each round, logged as a
ROUND_SNAPSHOTevent for historical tracking and log replay. - Crash Recovery: On restart, the plugin detects whether the current round matches a persisted round start time and resumes from the temp log rather than starting fresh. In-memory reconnect memory is re-hydrated from the persisted database, allowing players who disconnect/crash to be re-assigned to their previous teams even after a plugin crash.
- Mode Ignorance: Automatically bypasses auto-assignment during "Seed" or "Jensen" layers (configurable).
Tracks per-player TrueSkill ratings (μ/σ) across rounds. SmartAssign automatically detects if EloTracker is active and uses its live ratings to make skill-based routing decisions. Without it, the plugin falls back to pure population balancing.
Add this to your config.json inside the plugins array.
"connectors": {
"sqlite": {
"dialect": "sqlite",
"storage": "squad-server.sqlite"
}
//"mysql": {
// "dialect": "mysql",
// "host": "localhost",
// "port": 3306,
// "username": "squad",
// "password": "password",
// "database": "squad_db"
//}
//"postgres": {
// "dialect": "postgres",
// "host": "localhost",
// "port": 5432,
// "username": "squad",
// "password": "password",
// "database": "squad_db"
//}
},
{
"plugin": "SmartAssign",
"enabled": true,
"database": "sqlite",
"logPath": "./smart-assign-log.jsonl",
"enableSmartAssign": true,
"enableEventLogging": true,
"enableClanGrouping": true,
"clanGroupMinSize": 2,
"clanGroupCaseSensitive": false,
"enableDatabaseLogging": false
}Database Options: The "database" option should match a connector name from above. Use "sqlite" for file-based storage (default), "mysql" for MySQL, or "postgres" for PostgreSQL. Any Sequelize-compatible backend is supported.
File Placement: Move the project files into your SquadJS directory's squad-server folder.
squad-server/
├── plugins/
│ └── smart-assign.js
├── utils/
│ ├── sa-database.js
│ ├── sa-swap-executor.js
│ ├── sa-event-logger.js
│ ├── sa-team-evaluator.js
│ └── sa-clan-grouper.js
├── testing/ ← Optional: diagnostic tools only
│ ├── clan-tag-timing-tester.js
│ ├── join-swap-tester.js
│ ├── unified-test-runner.js
│ └── optimize-params.js
testing/ folder to production servers. The testing/ directory contains diagnostic and simulation tools intended only for development and validation. Production deployments should include only the plugins/ and utils/ directories.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
database |
string | ❌ No | "sqlite" |
A valid Sequelize connector name (e.g. "sqlite", "mysql", "postgres") for reconnect memory storage. |
enableSmartAssign |
boolean | ❌ No | true |
If true, runs the assignment algorithm and moves players. If false, only logs real server events (passive mode). |
enableEventLogging |
boolean | ❌ No | true |
Toggle the JSONL lifecycle event logging output entirely. |
logPath |
string | ❌ No | "./auto-assign-log.jsonl" |
File path for JSONL player lifecycle events. |
ignoredGameModes |
array | ❌ No | ["Seed", "Jensen"] |
Array of layer/gamemode substrings where SmartAssign should not alter teams. |
enableClanGrouping |
boolean | ❌ No | true |
If true, players in clans will be kept together on the same team if all clan mates are on one team. |
clanGroupMinSize |
number | ❌ No | 2 |
Minimum number of players to consider a group as a clan for grouping purposes. |
clanGroupCaseSensitive |
boolean | ❌ No | false |
If false, clan tags are case-insensitive and diacritics/gamer-character lookalikes are normalized. |
enableDatabaseLogging |
boolean | ❌ No | false |
If true, mirrors JSONL event data into database tables for querying. |
SmartAssign uses a hierarchical decision process optimised for competitive parity and real-world stability:
The hard cap is a safety net that prevents extreme imbalance regardless of the Elo scoring outcome. The following values are optimized winners from parameter tuning against historical match data:
| Server Population | Max Allowed Difference |
|---|---|
| < 82 players | 4 players |
| 82–89 players | 3 players |
| 90–95 players | 2 players |
| 96+ players | 1 player (strict parity) |
A hard cap preventing any single team from exceeding 50 players. If both teams reach 50, the server is considered full and the plugin returns no assignment (Squad's native join handling takes over). This prevents the server state from becoming unrecoverable.
If the joining player has a record in the reconnect database from the current round, they are routed directly back to their previous team — before Clan Grouping or Elo scoring is evaluated. Reconnecting players are granted an additional +1 imbalance grace allowance on top of the base to allow them back to their squad.
If the reconnect target would violate the hard cap even with the grace allowance, the player falls through to Clan Grouping and Elo scoring with a small bias toward their previous team.
If the joining player is part of a clan and all their clan mates are currently on the same team, the player is routed to that team — provided the population cap allows it. Clan members are granted the same +1 imbalance grace allowance as reconnecting players to keep squads together.
If the clan target would violate the hard cap even with the grace allowance, the player falls through to Elo scoring. If clan mates are split across both teams, this step is skipped.
If neither reconnect memory nor clan grouping routes the player, the algorithm evaluates both teams with a 3-Metric Composite Scoring System aligned with TeamBalancer:
- Mean ELO Difference (0.6× weight): Calculates the average skill (Mu) of each team and measures the absolute difference. This ensures overall team skill parity.
- Top-15 ELO Difference (0.4× weight): Calculates the average skill of the 15 highest-rated players on each team and measures the absolute difference. This ensures high-skill player parity and prevents stacking.
- Veteran Parity Penalty (300× multiplier): Calculates the ratio of veteran players (10+ rounds) on each team and penalizes ratio imbalance to ensure experience distribution.
These metrics are combined as: compositeELO = 0.6 × meanDiff + 0.4 × top15Diff, then passed through a non-linear penalty curve that scales penalties more aggressively for larger differences. The veteran penalty is added independently.
The player is assigned to whichever team produces the lower combined penalty score — i.e., the placement that brings the match closest to a balanced skill and experience split.
If the reconnect target would violate the hard cap, reconnecting players receive a 0.25-point score reduction (reconnect bias) toward their previous team to tip near-ties in their favor, allowing them a chance to rejoin their squad even when the cap blocks direct placement.
- If
EloTrackeris unavailable, the algorithm falls back to pure population balancing (smaller team wins). - If both teams are at the 50-player physical cap, the plugin returns no assignment (server is full — joins are handled natively by the game engine).
join-swap-tester.js is a development diagnostic plugin intended for testing and validation only. Do not deploy to production servers.
It targets a specific player by EOSID and runs a full lifecycle profile:
- On join: immediately swaps them to the opposite team and reports the total verified swap time.
- On disconnect: reports the RCON detection delay and whether the engine-level
UNetConnection::Closelog was captured.
It was used to prove the 1s verified swap and to validate disconnect detection behaviour.
{
"plugin": "JoinSwapTester",
"enabled": true,
"targetEOSID": "your-eos-id-here"
}Slacker
Discord: `real_slacker`
GitHub: https://github.com/mikebjoyce
Built for SquadJS