Quote:
Originally Posted by Roxeez
Honestly, the comparisons in this thread don’t make much sense.
The memory and CPU numbers you’re putting next to the public emulators aren’t a C++ vs C# thing. Public emulators eat RAM and CPU because of design choices: everything loaded and kept alive forever, per-entity timers and tasks, LINQ and allocations in hot paths, reactive subscriptions everywhere. Rewrite the same design in C++ and you’ll get the same problems. Write a sane design in C# and it runs fine with a fraction of those numbers.
You also talk about raw performance way too much. That was never the real problem with public emulators. Their real problem is concurrency: async/await used everywhere for no reason, shared state touched from several threads without a clear ownership model, locks added randomly to patch race conditions, and the duping, desync and random crashes that come with all that. “Native” doesn’t fix any of it. You need a clear threading model (single-threaded map/channel loop, message passing, whatever), and nothing in the post says how you handle that.
Put together, it gives the impression you don’t really know what you’re talking about, and that “native C++” is mostly a marketing line.
Your own videos don’t help either. The AI is clearly behind, and it lags with only ~200 players online. For a project whose whole pitch is performance, that’s a tough look.
|
You’re completely right that poor architectural choices (uncontrolled async/await churn, random lock patching, LINQ in hot loops, per-entity timers) will perform terribly regardless of whether they’re written in C# or C++. A disciplined C# implementation with value tasks, struct spans, and a proper tick loop will easily outperform sloppy C++
1. Concurrency & Threading Model
The server does not use ad-hoc locks or async/await over shared mutable state. The architecture relies on strict process isolation and spatial separation:
Process Isolation: We do not run a monolith. Each channel runs in its own isolated OS process (NTWorldServer). Channels, Login (NTLoginServer), Cluster Hub (NTCoreServer), and Database (NTDatabaseServer) all operate behind independent process boundaries. Channel 1 cannot contest memory or lock state on Channel 2.
Asynchronous IPC for Persistence: DB queries never touch or block the world tick. Database persistence is handled via asynchronous IPC to NTDatabaseServer. The world simulation continues completely decoupled from SQL latency.
Network & Session Boundary: Network I/O is managed via Windows IOCP. Inbound frames are dispatched per-session under a dedicated session mutex, ensuring strict FIFO execution of packets per client without packet interleaving or race conditions on player state.
Deterministic 50ms World Loop: Authoritative mechanics (BCards, buff ticks, skill cast finalisation, monster step/aggro, instances) are driven by a synchronous 50ms world thread (CCkThread), mirroring the retail TCkThread.
Spatial Map Partitioning: State is segmented per-map. Maps with zero players immediately bail out of the AI simulation loop (if frame.players.empty()), meaning thousands of world monsters take zero active tick budget when nobody is around.
2. Memory & Footprint
We don't hydrate massive dynamic object graphs. We directly ingest retail binary .dat files into contiguous, packed memory structures. Memory per definition is literally sizeof(struct).
We use zero-copy packet views on hot paths like movement and skill casting, completely eliminating allocation overhead and heap churn.
3. The Video (200 Bots & AI Context)
An early stress test using our custom headless load tester (NTBot).
The loadbot AI target acquisition was flawed: all 200 simulated clients spawned on the same map and locked onto the exact same mob simultaneously. They were pathfinding into the exact same tile and spamming attack packets at a single entity
EDIT: Since we obviously don't have 200 real players sitting around for a stress test yet, we had to rely on a quickly made loadbot