-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
Synergy uses Harmony to patch vanilla server methods at startup. It doesn't change any game files; patches are applied in memory and disappear when the mod is removed.
Vintage Story servers spend most of their tick budget on entity ticking, collision detection, pathfinding, and network I/O. Many of these operations do unnecessary work: ticking entities nobody can see, resolving collisions for stationary objects, computing paths for distant mobs, sending position packets at full rate for faraway entities.
Synergy identifies these inefficiencies and eliminates the wasted work while preserving identical gameplay behavior.
The biggest single optimization. Vanilla ticks every loaded entity every server tick (50Hz), regardless of distance to players. Synergy skips OnGameTick for entities beyond 48 blocks of any player.
Load-adaptive range (v1.1.20): When the previous tick exceeds 200ms, Synergy proportionally shrinks the activation range down to a floor of 16 blocks. This creates automatic back-pressure: overloaded servers tick fewer entities, recover within 2-3 ticks instead of staying overloaded for minutes. After autosaves, the range is halved for 3 ticks to smooth the post-save entity burst. Configurable via AdaptiveRangeEnabled, AdaptiveRangeMinBlocks, and AdaptiveRangeThresholdMs.
Sleeping entities still run whitelisted behaviors: breathe, health, despawn, decay, grow, multiply, harvestable, so animals grow, breed, and despawn correctly. Calendar-based behaviors catch up automatically via Calendar.TotalHours. Entities in lava, on fire, or in water always get full ticks (prevents drowning in ponds while sleeping).
For entities with AI (taskai behavior), sleeping entities continue running their currently active AI tasks via ProcessRunningTasks; only new task evaluation (StartNewTasks) is skipped. This means a villager mid-walk to their workstation will keep walking, and their internal timers (stuck detection, task timeouts) stay coherent. No new tasks can start while sleeping, so entities won't pick up wander or other low-priority tasks during sleep. This matches Paper MC's inactive-goal-selector-disable pattern and provides automatic compatibility with all mods using custom AiTaskBase subclasses (VSVillage, PrimitiveSurvival, etc.).
Entity types listed in ActivationRangeExcludedEntities (default: ["smoke"]) always get full ticks regardless of distance. This prevents lifecycle-dependent mod entities (like Real Smoke) from accumulating when their OnGameTick dissipation logic is skipped.
This is the same approach used by Paper/Spigot's Entity Activation Range for Minecraft, which is the single most impactful server optimization in that ecosystem.
Saves: ~2-9ms per tick depending on entity count and player spread.
Vanilla runs the full ApplyTerrainCollision method (~160 lines) for every entity every tick, even when the entity has zero motion. With zero motion, the collision resolution produces no changes; it generates the collision box list, iterates all boxes, and finds nothing to push.
Synergy checks four conditions (motion == 0, not in liquid, not swimming, not on fire) and skips the entire method when all are met. Sets CollidedVertically = false and CollidedHorizontally = false to match vanilla's output.
This is the same concept as "sleeping" physics bodies in Unity, Unreal, and Godot.
Saves: ~0.6-2ms per tick. Dense animal pens benefit most.
Vanilla calls TcpSocket.SendAsync for every individual packet: one kernel syscall per packet. Synergy buffers small uncompressed packets and flushes them together when the buffer exceeds MTU (1400 bytes) or at the end of each tick via a postfix on ServerMain.Process(). Large or compressed packets flush immediately.
The end-of-tick flush is critical: the VS tick loop runs Systems.OnServerTick → EventManager.TriggerGameTick → ProcessMain. Inventory interactions (move, activate, flip) are processed in ProcessMain, which generates response packets. Without end-of-tick flush, these packets would be delayed to the next tick (~33ms). The postfix on Process() ensures they're flushed in the same tick.
This follows the industry-standard pattern used by Netty's FlushConsolidationHandler, Minecraft Paper, and Source Engine snapshots.
Saves: ~3-5% server CPU with 10+ players.
Vanilla's tryTickBlock calls atPos.Copy() for every block that needs ticking, allocating a new BlockPos on the heap. Synergy pools 512 BlockPos objects per thread via [ThreadStatic] arrays, reusing them each tick cycle. Falls back to Copy() when pool is exhausted.
Ticks that carry extra data (e.g. soil grass growth) use atPos.Copy() instead of the pool, since the position must remain stable until the main thread consumes it. The BlockPosWithExtraObject wrapper is created via a cached ConstructorInfo resolved at startup.
Saves: ~16 MB/s Gen0 GC pressure eliminated from block ticking.
Vanilla's SendDirtySlots runs every 30ms, iterating all connected clients and all their inventories to check if anything changed. On most ticks, nothing has changed; the iteration is wasted.
Synergy adds a Volatile dirty flag that's set when any inventory is modified. If the flag is clear, the entire iteration is skipped. Cost: one Volatile.Read (~2ns) vs iterating all clients × all inventories (~5-60µs).
Saves: ~0.4-2.3ms per tick when inventories are idle (90%+ of ticks).
Vanilla's A* algorithm creates a new PathNode for every neighbor expansion. up to 8 allocations per node, ~8000 per search. Synergy uses a Harmony transpiler to replace all new PathNode() calls with pooled equivalents from a [ThreadStatic] array of 4096 pre-allocated nodes.
The pool resets at the start of each search. When exhausted, it falls back to vanilla allocation.
Saves: ~87% reduction in pathfinding heap allocations (~7 MB/s with 200 entities).
Vanilla processes every pathfinding request regardless of how far the entity is from players. Synergy throttles requests based on distance:
- <32 blocks: every request accepted
- 32-64 blocks: ~1 in 2 accepted
- 64-96 blocks: ~1 in 4 accepted
-
96 blocks: ~1 in 8 accepted
Skipped requests are marked as "no path found"; the entity retries on the next tick. At 50+ blocks, the ~100-200ms delay is imperceptible.
Saves: ~30-60% of pathfinding CPU for distant entities.
Vanilla runs EntityBehaviorRepulseAgents.OnGameTick every tick per entity with zero distance throttling. In dense animal pens with 50 entities, this means 2500 pair checks per tick (O(N²)).
Synergy throttles based on distance to nearest player:
- <32 blocks: every tick (vanilla)
- 32-64 blocks: every 4th tick
-
64 blocks: skip entirely
Repulsion is purely cosmetic at distance; no gameplay impact from skipping.
Saves: ~3-8% TPS in dense animal pens.
Vanilla evaluates StartNewTasks every tick for every entity's AI task manager. Synergy uses a continuous DEAR formula to throttle evaluation frequency based on distance:
- ≤22 blocks: every tick (vanilla)
- 32 blocks: every 2nd tick
- 45 blocks: every 4th tick
- 100+ blocks: every 20th tick (cap)
ProcessRunningTasks always runs every tick; entities never freeze mid-action. Entities in combat or fleeing always get full-rate AI. Entity ID offset distributes load across tick slots to prevent thundering herd spikes.
Under server overload (tick > 200ms), the DEAR divisor scales down proportionally, throttling AI evaluation more aggressively for all entities within activation range.
Based on Airplane DEAR, Pufferfish DAB, and Game AI Pro Ch.14 "LOD Trader".
Saves: ~2-5% TPS with activation range active. ~8-12% standalone.
Vanilla has zero hysteresis on entity tracking. an entity oscillating at exactly the tracking range boundary generates spawn/despawn packets every few ticks, causing visible flickering.
Synergy adds a speed-proportional buffer zone: max(8, motion.Length × 20) blocks beyond the tracking range.
Effect: Eliminates entity pop-in/pop-out at view distance boundary.
Vanilla sends position packets at 30Hz for all tracked entities. Synergy reduces this to 15Hz for entities 50-128 blocks away. Fast-moving entities always send at full rate. Stationary entities with no dirty state skip force-update packets entirely.
Saves: ~30-40% bandwidth for distant entities.
Vanilla's SyncedTreeAttribute.RemoveAttribute calls MarkAllDirty() even when the key doesn't exist (no-op). This triggers a full attribute tree resync (500-2000 bytes) for nothing. Synergy skips the call entirely when the attribute is absent. When the attribute does exist, vanilla runs normally; full resync is required for client listener notification (e.g., dismounting).
Saves: 500-2000 bytes per no-op RemoveAttribute event.
Vanilla sends newly-tracked entities in arbitrary order. Synergy sorts by distance (farthest-first). Since the client uses a Stack (LIFO), this inverts to nearest-first processing.
Effect: Nearest entities appear ~100-500ms sooner after teleport/login.
Vanilla processes ALL entities in one tick on login/teleport (100-500ms freeze). Synergy limits to 5 entities per 20ms tick; entities appear gradually over ~1s. Combined with spawn priority ordering, nearest entities appear first.
Effect: Eliminates entity load frame spikes on login/teleport.
Requires both client and server to have Synergy installed. Vanilla clients receive unchanged packets.
Server-client handshake via mod TCP channel identifies delta-capable clients. For Synergy clients: sends baseline-relative delta-encoded positions via mod UDP channel using ZigZag + varint encoding. Walking entity delta ≈ 5 bytes vs 11 bytes absolute position.
Baseline resets are staggered per-entity across a ~1s window, matching vanilla's forceUpdate cadence without synchronized spikes. A per-client send budget caps absolute packets per tick to prevent UDP overload at high entity density. Packet loss behavior identical to vanilla.
Saves: ~50-60% bandwidth reduction on entity position packets for Synergy clients.
Logs GC configuration at startup: Server/Workstation GC mode, current latency mode, heap size, committed memory, and fragmentation.
Effect: Informational only. Zero runtime overhead.
Before applying each Harmony patch, Synergy checks Harmony.GetPatchInfo() for existing transpilers or prefixes from other mods. If found, Synergy skips its own patch with a logged warning.
Each optimization tracks consecutive errors via atomic counters. After 5 errors, the optimization auto-disables and all future calls fall through to vanilla.
All prefixes return true (let vanilla run) on any unexpected exception. The server never crashes due to a Synergy bug.
Synergy writes nothing to disk except its config file. Removing the mod restores vanilla behavior completely.
| Scenario | TPS Gain | Bandwidth | Fluidity |
|---|---|---|---|
| 1 player, 50 entities | ~2-5% | ~10% | Slight |
| 5 players, 150 entities | ~8-15% | ~35-45%* | Noticeable |
| 10 players, 300 entities | ~15-25% | ~50-60%* | Significant |
| 30+ players, 500+ entities | ~25-40% | ~55-65%* | Significant |
*Bandwidth reduction with delta encoding requires Synergy on both client and server. Without client mod, bandwidth reduction is ~30-40%.
The biggest contributors are Entity Activation Range (~40% of TPS savings), AI Brain Throttle (~15%), and Pathfinding Throttle (~15%).
Each optimization has a matching diagnostic module that tracks counters in real-time:
-
Zero overhead when disabled: each module guards with a
volatile boolcheck; noInterlockedoperations run unless explicitly enabled -
Thread-safe: all counters use
Interlocked.Increment/Add(safe from physics threads) - Auto-dump timer: 5-minute periodic dump to server log when diagnostics are enabled
-
Circuit breaker visibility:
/synergyshows which optimizations have tripped their error threshold
Architecture follows the same pattern as OptiTime and Tungsten: IDiagModule interface → DiagRegistry (static registry) → DiagLog (dual output to chat + log). 16 modules registered at startup.