🏙️ Full-Spectrum Sync (45 Commands), SIMD Engine, Stability Improvements and more... - #18
Conversation
- 45 synchronized command channels (construction, transport, zoning, economy, environment, utilities, traffic lights, fares, building toggles, park fees, service districts, chirper) - Hardware-accelerated 64-bit unrolled XOR delta savegame engine - 64MB Large Object Heap (LOH) pre-allocated memory slab allocator - Rolling 32-bit checksum hash automated desync detector - Sliding-window 64-bit idempotency command deduplicator and 200-command replay ring buffer - 5-tier role permission matrix, democratic vote-kick, and 200-event municipal audit log - 3D compass radar HUD, Catmull-Rom spline camera smoothing, 3D laser ruler, and spatial audio cues - DualMode IPv6/IPv4, UDP multi-NIC LAN discovery, and RFC 5389 STUN NAT client - Full 8-language localization suite
|
Thanks for the PR! Before I dive into the review, I have a couple of questions:
Since this PR touches a very large number of files, I’d like to clarify this before spending time on a full code review. |
Hi @Rollocraft, thanks for checking in! Totally understand why you're asking given the size of the PR. To answer your questions:
Like I said i havent really tested it yet as a whole because Im having issues with my pc so i cant even boot up the game, but that will change soon and when it happens I will debug and test it thoroughly. |
…suite, and UI integrations
…ams and orientation sync
…ize player tracking
|
Ok so I've done my review first of all zhanks for the effort here, there's a lot in this branch, and a few pieces I want to keep. But I can't merge it as it stands. |
Salvage from #18: wire and transport fixes, cursor throttling, map pings and host lobby controls
Five small fixes, adapted from t1garbiznisbrate-ship-it's work in #18. The float codec allocated a four-byte array per value through BitConverter.GetBytes and relied on the runtime's endianness matching the manual little-endian integer writes beside it. Floats are the densest thing on this wire - a terrain brush or a road curve is little else - so both the allocation and the unstated assumption sat on the hottest path. Writer and reader now reinterpret through an overlaid struct and lay the bytes out explicitly. Output is identical on a little-endian host, so the wire format does not move. The send loop wrote the four-byte length prefix and the payload as two separate calls. Over TLS each call is its own record, so describing four bytes of length cost about twenty-nine bytes of record overhead, and with NoDelay set it was also its own TCP segment - close to a fixed tax on every command. Payloads up to 8 KiB are now copied in behind their prefix and written once. Larger ones keep the two-call path, where the copy would cost more than the write saves. Sockets ask for 1 MiB kernel buffers. A world transfer is tens of megabytes through this socket and the defaults are sized for request/response traffic. Best-effort: a platform that refuses keeps its default. The TLS handshake had a read deadline but no write deadline, so a peer that stalled mid-write wedged the thread the read timeout existed to protect. Both are now set for the handshake and cleared after it. ReadExactly dereferenced _stream without checking it. Close() clears that field from another thread, so the read loop could fault between the close and its own cancellation check and end the connection with an unhandled NullReferenceException instead of a reason. BlobReassembler sizes its buffer to the announced total. A savegame arrives in 256 KiB chunks, so growing from the default meant a doubling and a full large-object-heap copy roughly per chunk. The announced total is already rejected above the channel's registered ceiling before we get here, so the allocation is bounded by that ceiling rather than by the sender's claim. NetUpgradeSyncSystem and DisasterSyncSystem read components off entities taken from a query snapshot. A delete realized between the snapshot and the loop - or the simulation reaping a finished disaster on its own cadence - leaves a handle that no longer resolves, and GetComponentData on it throws out of the entire pass, taking every other live entry with it. Both scans now check Exists and the components they are about to read, and skip the one entry rather than losing the pass.
Adapted from t1garbiznisbrate-ship-it's work in #18. PlayerCursorSyncSystem transmitted at 10 Hz for the whole session regardless of whether the camera had moved, so a player reading a panel or sitting in a menu - the common case in a co-op session - sent ten packets a second to say nothing had changed. It now sends at that cadence only while the focus, eye or yaw actually move, and drops to a 1 Hz keepalive otherwise, which is well inside the five seconds after which a peer stops drawing the marker. The same system now owns every camera move the mod makes, because it holds the only CameraUpdateSystem reference. Chat commands record an intent on the service - a one-shot jump target, or a player to follow - and this system carries it out on its next update. Follow mode ends when the player takes their own camera back, detected by the pivot drifting from where the follow lerp last put it rather than by polling keys: nothing to maintain as keybindings change, and it cannot fire while someone is typing. The tree channel's periodic sweep copied every tree in the city into a NativeArray to pick at most MaxRecords of them - a six-figure allocation per sweep on a forested map, and the sweep, not the send, is what this channel costs the host. It now walks the archetype chunks the ECS already holds. The round-robin cursor is chunk-granular, so every tree is still eventually visited. Peer latency was the raw last sample, which swings with whatever the OS was doing when the echo landed. It is now the Jacobson/Karels smoothed estimate with the standard 1/8 and 1/4 gains, and the mean deviation it produces is kept as jitter - the "is it steady" half of the story that a single number cannot tell. A client was handed an empty player list and rendered nothing, so a two-player session looked like a single-player one from one side of it. Clients now build a roster from the host peer plus the players they are tracking positions for. Another client's name genuinely is not known there - no roster travels to clients - so it reads "Player 3" rather than inventing one, and an unmeasured latency is -1 so the panel can show nothing instead of a misleading zero. MultiplayerSession.HostPlayerId is public for the same reason: the game layer has to tell which entry is the host, and guessing 0 mislabels everyone.
The co-op features from #18 that the codebase genuinely does not already have, rebuilt to fit it. Original idea and first implementation by t1garbiznisbrate-ship-it. A map ping is a transient "look here" beacon with an optional note, drawn as a ring that grows and fades over six seconds in the sender's cursor colour. It travels as command id 29 rather than as chat text. #18 encoded it into a chat line ("/ping x y z id label") and parsed it back out on receipt, which means the author of the marker is whoever typed the line - anyone could drop a ping signed with another player's name - and every chat line in the session had to be tested for it. As a command the sender comes from the message envelope the session already authenticates, and the coordinates go through WireGuard like every other command's do. A ping mutates nothing, so it is deliberately outside the sync pipeline: no echo guard, no snapshot, no resync on loss. A ping that does not arrive is a ping nobody saw, and that is the whole failure mode. The sender records its own ring at send time rather than waiting for the echo, because a host is notified of its own commands and a client is not - relying on the echo would draw the host's pings and silently swallow every client's. Pings are not gated on ShowPartnerMarkers. That setting hides the ambient cursor rings; someone who turned those off still wants to see a partner deliberately pointing at something. /goto <player>, /goto ping, /follow and /unfollow move the camera. Chat commands hold no camera reference and have no business taking one, so they record an intent on the service and PlayerCursorSyncSystem - which already owns the CameraUpdateSystem reference - carries it out. Name resolution takes an exact id, then an exact name, then a unique prefix; an ambiguous prefix resolves to nothing, because following the wrong partner is worse than being asked to be more specific. /lock and /unlock refuse new joins without ending the session; everyone already connected stays. /banlist and /unban make the existing per-session address bans visible and reversible, which matters mostly for undoing a ban placed on the wrong player. All four are host-only and say so when they are not. The lock clears when hosting ends, alongside the ban list. /help lists what exists, and /clear clears the local log only and says so. An unrecognised slash word is not claimed and goes out as ordinary chat, so a typo is visible rather than silently swallowed.
Adapted from t1garbiznisbrate-ship-it's work in #18. Pressing sync as a host with nobody connected opened an epoch, found no participants and closed it again, in silence. From the button that was indistinguishable from a sync that had failed. It now says there is nobody to sync with, and the host log records the participant count either way. Completion was equally quiet on both sides. The host watched its own simulation stop and start again with no explanation, and a client watched its world reload and then had to infer from the clock moving that the handover was over. Each now gets a line when it finishes - the client as it installs the snapshot, the host once every participant has resumed. NotifyChat is internal rather than private so the world-sync flow, which lives in the game layer, reports through the same path the session's own notices use. SanitizeSpeed had no upper bound. The resume speed arrives over the wire, and the game's own selector tops out at 3, so a peer sending a large finite value would have had the simulation resume at it. Clamped to a generous 8.
Salvage from #18: wire and transport fixes, cursor throttling, map pings and host lobby controls
* Add diagnostics profiling to various sync systems for performance tracking aswell as fixing a bridge placement bug * Add NetWaterProfilePin and NetSyncSystem for water profile handling in networked environments * Sync commercial, industrial and office buildings (v49, channel 22) Makes the host the only author of workplace business: which company occupies each building, the money-facing figures behind its panel, and the goods it is holding. Figures and tenancy need opposite mechanisms, which is why an earlier attempt at this was withdrawn. It corrected companies on a 1024-frame rotation while CompanyEconomyStatisticSystem rewrites the same fields every 128 frames, over a partition that system picks from its own frame index, so every correction was overwritten several times before the next one arrived and the panels never settled. Figures are therefore corrected on that writer's own schedule: same interval, same UpdateFrame partition, same query shape, ordered directly after it. Each company is corrected in the frame its local value was recomputed. Tenancy cannot be corrected at all. A client running its own spawners opens businesses the host never had, and writing figures afterwards never removes one. So the client holds CommercialSpawnSystem, IndustrialSpawnSystem and both FindProperty systems, and CompanyLifecycleBoundarySystem strips local MovingAway/PropertySeeker proposals right before the native executor - those proposers keep running because they also produce figures and demand. Opening a business copies the game's own spawner (create from the prefab archetype, set PrefabRef, hand the move-in to the native rent-action queue); closing uses MovingAway, the game's emigration path. A page entry is about the building, not the business, so the host can say "nobody rents this one" - the one statement a client cannot derive for itself. Vacant entries skip the whole block. Business identity is positional (building, archetype): no global company id, because nothing a player sees distinguishes "same business" from "same archetype with the same books". Keeping the cost down, since structural work is also what makes a building redraw: a matching building costs a dictionary lookup and a string compare with no writes; per-update budgets on openings and closings; a settle window after any tenancy action so a page arriving mid-transaction cannot double-open or undo; only tenancy changes enqueue structural work, money changes never do; and local proposals are cancelled with the query-level EntityManager overloads. Employment stays local - an Employee buffer names real citizens, and fabricating those would invent people the receiving machine does not have. * Add a per-topic diagnostic log and per-zone performance reporting Two questions needed answering separately: is the mod costing frames or is the city just big, and which part of the city is doing it. SyncLog splits diagnostics into topics - Performance, Residential, Commercial, Industrial, Office - each with its own switch in a new Diagnostic Logging group in the options. A topic that is off costs nothing, because IsEnabled is a field read and callers ask before building the string; the company channel builds its shared health line once and only if at least one of its three zone topics is on. Every line carries its topic as a prefix so a log with several on stays readable. Faults are never routed through a topic - a warning is not something a player opts into - and the performance report uses Record, which reaches the flight log either way, since that is exactly the case where the log was already captured before anyone thought to turn a switch on. Mod.Verbose survives as the General topic on the existing switch, so untouched call sites are unaffected. SyncProfiler scopes can now declare a zone, and the 30 s report ends with a by-zone line. Zones have to be declared rather than inferred because the mapping is many-to-many in both directions: one channel serves commercial, industrial and office, while residential is served by four systems. To get real per-zone timings out of the shared company channel its correction pass sorts the partition into three buckets and times each separately - the classification replaces a property lookup the loop was doing anyway, so the split is close to free. Unzoned scopes are deliberately left out of the by-zone line rather than folded into a catch-all, because a zone total that quietly included everything else is the kind of number that ends an investigation in the wrong place. All nine locales carry the new keys: a settings key with no locale entry renders the raw key in-game, and the English fallback only covers runtime CS2MP.* keys, not @label/@desc. * Fix the resync causes in the logs, and make a resync justify itself The three attached session logs show four automatic world reloads across two sessions, plus a long run of manual /sync requests. Three of the four are one fault ("native net target did not resolve") and one is a drain timeout. Both turn out to be caused by the mod rather than observed in the city. Root causes fixed: * A native placement waiting for the road it anchors to was re-queued at the FRONT of the strictly ordered net queue, so it stopped every later operation from every player for the whole ten-second window. It now waits behind work from other senders, which is causally independent of it and may be what builds the target it is missing; its own sender's later work still stays behind it. * Bulldoze and road replacement live in their own feeders and ran ahead of that waiting placement every frame. The flight log shows two bulldozes applied during the ten seconds one placement waited, and the placement then asked for a full reload because its target was "missing". Those feeders now stand down while a placement is stalled, and the retry windows of anything they were holding are extended by the time they spent held, so a bulldoze is not dropped for failing to match while it was not allowed to look. * Road endpoints had no local-surface fallback, so an endpoint could not be matched once terrain drifted past the three-metre vertical tolerance. The same logs report the mod correcting endpoint elevations by up to 3.5 m, so an ordinary road drawn near drifted ground could only ever end in a reload. The existing utility-net projection now covers roads and rails on the last-resort pass, with identity still matched strictly. * The commit drain window was a fixed 3 s. A 311-entity replacement was quarantined on the same budget that comfortably drained the 55- and 86-entity batches around it. It now scales with the batch and restarts whenever the batch makes progress, and the log reports how many entities actually remained instead of how many the batch started with. Detection: every automatic reload now passes through ResyncArbiter. A claim about the city (a named thing absent, an identity contradiction, a lost command) settles on sight or on a second sighting; a claim about this machine's pipeline (a deadline, a budget, a drain window) is held first, with the mutating net feeders frozen so the retry sees a world that is standing still. A subsystem whose fault clears withdraws its report and no world is reloaded; one that never withdraws matures and reloads on schedule, so a real divergence is still repaired. Logging: SyncLog gains a prod level - always written, no prefix - and every outcome (held, withdrawn, settled) is written there as a full report: which edit, which endpoint, what stands there instead, how long the mod waited and what it tried first. A bulldoze that never found a target is reported there too; it was previously visible only with verbose logging on. ResyncRequest carries the reason (protocol v50) so the host's log distinguishes a player pressing the sync button from a client that gave up on an edit. * Terrain: land a stroke in one frame, and stop losing strokes in silence Following the surface disagreement in the logs (the realize pass reports road endpoint elevation corrections growing 2.1 -> 3.4 -> 3.5 m across one session). Apply in one frame. The per-frame budget of 64 brush samples existed to bound a frame spike, but a player's own terraforming produces about sixty samples a second, so at sixty frames a second it only ever bit on a backlog - and a backlog is exactly when spreading the work out is wrong, because HasBacklog holds back every road, building, zone, growable and route realize until terrain is level with the sender's. The game applies whatever brushes exist in a single ApplyBrushesSystem pass, so a bigger batch is one bigger frame rather than more frames. Raised to 512, which covers roughly eight seconds of continuous terraforming. It also makes the replay slightly more faithful: a height brush is a rate rescaled by this machine's frame time, so applying a stroke together under one frame time reproduces it more exactly than spreading it across frames of varying length. Stop losing strokes in silence. Terrain had six paths that dropped applied brush samples with no log at all - no tool name, no brush name, opacity outside the encodable range, an unresolvable tool or brush prefab, a prefab that cannot terraform, and a malformed command. A terraforming sample is metres of height, so a dropped one is not a rounding error: it is ground that is a different shape on the two machines, and the roads drawn near it afterwards resolve their endpoints against a surface the sender does not have. All six are now counted and reported at the production level, split by side, so a log says which machine lost what. Do not drop a whole frame's strokes over a bad frame time. Capture returned outright when the frame delta was implausible, discarding every applied sample in that frame - the ground moved locally and the other player was never told, a permanent divergence bought to avoid one mis-scaled sample. It now falls back to a normal frame time and reports having done so. Do not let one bad sample end the session. A sample that threw during creation rejected its whole batch and left it queued, so it was retried forever, so HasBacklog stayed true forever, so no road, building, zone, growable or route could be applied for the rest of the session - with no resync request and no log beyond one warning. The failing sample is now skipped, and an apply pass that stays unavailable for 300 frames gives up on its queue with a report and a resync request rather than wedging everything behind it. * Audit every resync trigger: fix two that fire for work never attempted Went through all sixty RequestResync/Settle sites. Two classes of real fault. A retry window that burns while its system is not allowed to run. Several realize systems give an unmet dependency a fixed wall-clock window and then treat its expiry as proof of divergence - but SyncRealizeSystem gates those same systems off entirely while terrain or the net pipeline catches up. Measured against the wall rather than against attempts, the window expires without the command ever having been tried, and the expiry asks for a full world reload. A stalled net placement holds that gate for its whole retry window, so this was not a rare race, and the placement-hold added in the previous commit widened it. - RouteSyncSystem: a 30 s window ending in "route <op> dependency did not resolve". Held windows are now extended by the time the system spent gated. - GrowableSyncSystem: a 15 s window ending in "growable state target did not resolve". Same treatment. - GrowableSyncSystem's road/service validation is worse in kind: it runs ungated, but what it waits for is a building joining its ROAD graph, and roads arrive through the pipeline being held. Its window is the same fifteen seconds as the hold, so it could expire entirely inside one. It now stops counting while the road pipeline cannot deliver. Audited and left alone: ZoneSync and NetUpgradeSync expire pending work too but drop it rather than requesting recovery, so no spurious reload - though the silent drop is a divergence of its own. DeleteSync and NetReplaceSync were fixed in the previous commit. PolicySync, VisualCustomizationSync and NameSync are not gated by SyncRealizeSystem. BuildSync, AreaSync, CityStateSync and the overflow paths are bounded by queue capacity rather than by a clock. A resync request the service was guaranteed to discard. When the host resumed a world handover before this client had installed the snapshot, the client reset, set its phase to WaitingForMap and asked for a new world. Both halves of that request were then swallowed: the session is still inside its epoch at that point and coalesces the request away, and WaitingForMap makes WorldRecoveryInFlight true, which reads as "a reload is already running". Nothing was running and nothing was coming, so the client sat in WaitingForMap for the rest of the session until somebody typed /sync. It now re-asks on a pumped flag once the session has actually left the epoch, deliberately outside the arbiter: this is a broken handover, not a claim that the two cities diverged. * Classify every resync trigger, and stop five more from firing untried Completes the audit. No call site asks for a world reload on a bare phrase any more: all 64 name what they saw, so the arbiter can tell a claim about the city from a claim about this machine's pipeline, and the log says which. 25 contradiction this city cannot represent the edit; reloads on sight 25 stream loss commands were shed or refused; reloads on sight 10 missing target something named is absent; reloads on a second sighting 4 timeout only a deadline passed; must survive a hold first Five more windows that expired against work never attempted. Same shape as the route and growable cases in the previous commit, but these systems are not themselves gated - what they WAIT for is. A prop's attachment retry says it in as many words ("the parent road never reached us"), and roads are exactly what the realize pipeline holds back. So the window ran down while the target could not possibly arrive, and expiring it asked for a full world reload. - BuildSync attachment retries and the blocked native-object window - GrowableSync road/service validation (previous commit) - PolicySync target retries, VisualCustomizationSync target retries - RouteSync created-line metadata finalization RealizeGate publishes the one fact none of them could see for themselves - whether roads, zoning and zone-grown buildings are currently held - and HeldTime turns it into the milliseconds to add to a pending deadline. The realize pipeline already computed that fact each frame; it just never told anyone. A resync requested because a resync was running. RouteSync treated a commit lost while gameplay was not ready as a reason to ask for recovery - but gameplay is not ready precisely because the world is already being replaced, and the replacement supersedes the commit. It now says so and drops it. * Classify the four triggers a literal-only search had missed The previous commit's "no bare-string trigger remains" check searched for RequestResync(" and so skipped every call whose reason is not a literal: two ternaries (BuildSync's compact-vs-native placement target, RouteSync's ambiguous -vs-absent created line) and two helpers taking the reason as a parameter (CityStateSyncSystem.PoisonOrderedStream, ZoneSyncSystem.RecoverFromQueueOverflow). All four fell through to the string overload and were weighed as unproven timeouts. They now carry evidence like the rest, including the one place where the class depends on the branch: an ambiguous created line is a contradiction, an absent one is a missing target. Also fixes a slip introduced while classifying: the BuildSync report read _lastUnresolvedObjectReason after the reset above it had already nulled the field, so the one fact worth having would always have read "unknown target". * Say a query's shape on one line instead of six Every entity query in the sync layer is read-only - a system observes the world and mirrors it, it never claims write access through a query - so each entry of an All/Any/None set was a full line of ComponentType.ReadOnly<X>(), and a four-component query took seventeen lines to say four words. SyncQuery.ReadOnly<...>() names the components as type arguments instead, so the shape of a query is readable at a glance: All = SyncQuery.ReadOnly<Updated, MovedLocation, PrefabRef, Transform>(), None = SyncQuery.ReadOnly<Temp, Owner, Deleted, Created>(), Applied to all 191 sets across 29 files; net 854 lines gone, with the seven sets whose components carry their own explanatory comments left in long form, because the comment is the reason that component is there and has nowhere to live in the short one. A fresh array per call, deliberately: EntityQueryDesc keeps the reference it is handed, and these run once per system in OnCreate. The rewrite was mechanical and checked by parsing every All/Any/None set before and after and comparing the component lists. * Give the three authority holds one implementation Growable buildings, company tenancy and residential occupancy each hold a set of native simulation systems off on a client, so the host's messages are the only thing deciding that part of the world. All three had written the hold out in full - the same dictionary of prior enabled-states, the same idempotent apply loop, the same restore - differing only in which systems they name and how they say so in the log. LocalAuthorityHold is that hold once, taking the four strings the log lines differ by. Each system keeps its ApplyLocalAuthority/RestoreLocalAuthority pair as a one-line delegation, so the twenty-odd call sites are untouched, and keeps the comment explaining which native systems it holds and why the others are deliberately left running - that reasoning is the valuable part. This also settles a disagreement between the copies. Growable's hold did not re-record a system's enabled state when the game turned it back on mid-session, so leaving a session restored that system to off, having been on before the hold. The other two fixed that and explained why; the shared hold takes the fixed behaviour, which is Growable's only change here. * Bind a sync system to the session in one place Twenty sync systems each wrote out both ends of their attachment to the session: an if (Mod.Service != null) block constructing an observer and adding it, a SyncInbox.RegisterDrain, and the mirrored pair in OnDestroy. The two ends have to stay symmetric - an observer added and never removed keeps feeding a destroyed system, and a drain left registered has SyncInbox calling into one - and twenty hand-written copies is not how that stays true. SyncObserverBinding.Bind/Unbind is the pair once. Bind takes a factory so the observer is still never constructed when there is no service to attach it to, which is what the guard did. Three systems run other work in OnDestroy between unregistering the drain and removing the observer; those keep their existing order and pass no drain to Unbind, rather than have the shared helper quietly reorder a teardown. Also adds the Infrastructure import to six files that the previous commit left referencing SyncQuery without it - it is a sibling namespace of Sync.Systems, so it does not resolve unqualified. * Split the occupancy realize pass by topic Realize.cs had grown to 2821 lines and 138 members, covering everything from resolving a page's properties to seeding a newly created citizen's age. Nothing about that is one subject, and at that size the file is only navigable by search. Split along the seams the code already had, into eleven partial files of 190 to 430 lines each - resolve, staging, property, bootstrap, household, vehicles, citizens, create, move-in, support - each carrying a note saying what belongs in it. Realize.cs keeps the shared state and the entry point, and points at the rest. Pure code movement: the members were compared byte for byte before and after, and come out identical and in the same order. * Split the net commit orchestration by topic Apply.cs was 2356 lines and 94 members: the per-frame commit cycle, temp-entity isolation, the drain-and-recover path, three separate transaction validators, owner resolution across two peers that share no entity ids, and the arming entry points. Each of those is a subject; together they were a file nobody reads top to bottom. Split into ten partial files of 180 to 360 lines - temps, commit, drain, the route/object/net validators, owner, topology, arm - each with a note saying what belongs in it, and Apply.cs left holding the frame cycle and the state the others read. Pure code movement, verified member by member: the only textual change is the blank line each file no longer needs before its first member. * Split the native object capture and realize passes by topic NativeCapture.cs (2184 lines) and NativeRealize.cs (1819) are the two halves of reproducing an object-tool operation on another machine, and each had grown to hold every part of that job at once - tool observation, commit matching, specialized areas, tool input, spawnable tracking, portable references, the specialized-industry rules, definition building. Capture becomes eight partial files and realize six, of 150 to 430 lines, each noting what belongs in it. The two entry files keep the shared state and the queue-and-retry loop. Pure code movement, verified member by member; the only textual change is the blank line each file no longer needs before its first member. * Split four more oversized sync files by topic NetSyncSystem/Realize.cs (1757), the occupancy Capture.cs (1302), VisualCustomizationSyncSystem.cs (1146) and RouteSyncSystem/Realize.cs (1110) each held several subjects at once. Split along the seams they already had, with each file noting what belongs in it: net realize -> operation assembly, unresolved holds, span geometry occupancy capture-> departures, the bucket scan, entity reads, hashing/trace visual custom. -> capture, apply, appearance state route realize -> connections, matching, commit VisualCustomizationSyncSystem gets its own folder, as every other multi-file system in the tree has. One thing deliberately left alone: NetSyncSystem's RealizeIncoming is still an 819-line method, and the file is still the largest here because of it. It holds about twenty-five interdependent locals across a try/finally that disposes native collections, so breaking it up is a real refactor with a real chance of changing behaviour - not something to do in the same pass as mechanical moves. Pure code movement, verified member by member against the previous revision. * Split five more sync files by topic PropertyRentSyncSystem (984) -> state/lifecycle, capture, realize; and into its own folder, as every other multi-file system here has BuildSyncSystem/Realize (972) -> realize, match, owned sub-elements, sub-nets NetSyncSystem/MixedOperation (934) -> operation, match, build, curves GrowableSyncSystem/Realize (867) -> realize, match, guard NetSyncSystem/Intent (832) -> intent, command encoding Pure code movement, verified member by member against the previous revision. * Split the Steam relay, occupancy state and net upgrades SteamRelayTransport (1180) -> transport, governor, connections, io, lifecycle ResidentialOccupancySyncSystem (797) -> state, cycle NetUpgradeSyncSystem (765) -> system, capture, apply; and into its own folder SteamRelayTransport becomes partial to allow the split - the only change to it beyond moving code. Pure code movement, verified member by member against the previous revision. * Split the net system state and the two largest command files NetSyncSystem.cs (741) -> the queues and geometry facts; ApplyState.cs, the state the commit machinery keeps, next to the Apply*.cs files that read it; Lifecycle.cs ObjectToolOperationCommand.cs (730) -> the ten portable intent types move to ObjectToolIntents.cs, leaving the command that carries them ResidentialOccupancySnapshot.cs (726) -> the five record structs to OccupancyRecords.cs, page validation and the name table to OccupancySnapshotValidation.cs Also refreshes NetSyncSystem's class comment, which still described a file layout from before this pass. Verified by comparing every type body and every class member against the previous revision, plus a repo-wide check that no type is now declared twice. * Constrain the observer binding to reference types Bind's T is always a class, and saying so makes the null it returns when there is no service a plain null rather than default(T). * Remove dead code and a stale committed UI bundle Nine unreferenced members and one checked-in build artifact, each confirmed dead by counting every code occurrence across the repo and separating real references from mentions in comments, string literals and other languages: CopyAllowedIds no caller; "for validation and tooling" never happened, and there is no test project or InternalsVisibleTo that could reach it CommittedRemoteTempsRemain a bool wrapper; only the count it wraps is used HelpLinks.Root Open() builds URLs from PageRoot, and Root is in no allow-list branch WorldSyncEpoch public getter never read (its field is used 18x) ActiveWorldSyncEpoch same AttemptsRemaining public getter never read TrackedAddresses "for tests/diagnostics" - there are no tests SendBlob (both overloads) only SendBlobTo is ever called; the comment claiming map sync used it was stale ChargeNet callers use CalculateNetCost + ChargeAmount UI/artifacts/ui-verify/ a 37 KB webpack bundle referenced by nothing and produced by no npm script; now gitignored beside the UI/build output it belongs with Checked and deliberately left alone: the 158 L10n.Key constants (every value is live in the .properties locale files, 74 of them used by the UI module), the 14 Setting.cs properties bound by name from the options UI, and PropertiesLocaleSource.ReadEntries/Unload, which implement IDictionarySource for the game. Two unreferenced things are kept on purpose and called out in the PR: ProtocolConstants.PasswordProofBytes documents the wire format, and NetReplaceSyncSystem.ExpectMixedLocalGeometryChange looks like a missing call rather than dead code. * Drop using directives the split files inherited but never use Splitting a file copies its whole using block into every fragment, and most fragments need only a part of it. These 119 are provably unnecessary: their namespace belongs to this mod, so every type in it is enumerable, none of those types is named anywhere in the file (comments and cref targets included), and the mod declares no extension methods that could be reached implicitly. The Unity, Game, Colossal and System imports are left alone - deciding those needs the compiler and the game assemblies, neither of which is available here. * Harden the wire codec, the socket framing and two entity scans Five small fixes, adapted from t1garbiznisbrate-ship-it's work in #18. The float codec allocated a four-byte array per value through BitConverter.GetBytes and relied on the runtime's endianness matching the manual little-endian integer writes beside it. Floats are the densest thing on this wire - a terrain brush or a road curve is little else - so both the allocation and the unstated assumption sat on the hottest path. Writer and reader now reinterpret through an overlaid struct and lay the bytes out explicitly. Output is identical on a little-endian host, so the wire format does not move. The send loop wrote the four-byte length prefix and the payload as two separate calls. Over TLS each call is its own record, so describing four bytes of length cost about twenty-nine bytes of record overhead, and with NoDelay set it was also its own TCP segment - close to a fixed tax on every command. Payloads up to 8 KiB are now copied in behind their prefix and written once. Larger ones keep the two-call path, where the copy would cost more than the write saves. Sockets ask for 1 MiB kernel buffers. A world transfer is tens of megabytes through this socket and the defaults are sized for request/response traffic. Best-effort: a platform that refuses keeps its default. The TLS handshake had a read deadline but no write deadline, so a peer that stalled mid-write wedged the thread the read timeout existed to protect. Both are now set for the handshake and cleared after it. ReadExactly dereferenced _stream without checking it. Close() clears that field from another thread, so the read loop could fault between the close and its own cancellation check and end the connection with an unhandled NullReferenceException instead of a reason. BlobReassembler sizes its buffer to the announced total. A savegame arrives in 256 KiB chunks, so growing from the default meant a doubling and a full large-object-heap copy roughly per chunk. The announced total is already rejected above the channel's registered ceiling before we get here, so the allocation is bounded by that ceiling rather than by the sender's claim. NetUpgradeSyncSystem and DisasterSyncSystem read components off entities taken from a query snapshot. A delete realized between the snapshot and the loop - or the simulation reaping a finished disaster on its own cadence - leaves a handle that no longer resolves, and GetComponentData on it throws out of the entire pass, taking every other live entry with it. Both scans now check Exists and the components they are about to read, and skip the one entry rather than losing the pass. * Stop sending a cursor that is not moving, and give clients a player list Adapted from t1garbiznisbrate-ship-it's work in #18. PlayerCursorSyncSystem transmitted at 10 Hz for the whole session regardless of whether the camera had moved, so a player reading a panel or sitting in a menu - the common case in a co-op session - sent ten packets a second to say nothing had changed. It now sends at that cadence only while the focus, eye or yaw actually move, and drops to a 1 Hz keepalive otherwise, which is well inside the five seconds after which a peer stops drawing the marker. The same system now owns every camera move the mod makes, because it holds the only CameraUpdateSystem reference. Chat commands record an intent on the service - a one-shot jump target, or a player to follow - and this system carries it out on its next update. Follow mode ends when the player takes their own camera back, detected by the pivot drifting from where the follow lerp last put it rather than by polling keys: nothing to maintain as keybindings change, and it cannot fire while someone is typing. The tree channel's periodic sweep copied every tree in the city into a NativeArray to pick at most MaxRecords of them - a six-figure allocation per sweep on a forested map, and the sweep, not the send, is what this channel costs the host. It now walks the archetype chunks the ECS already holds. The round-robin cursor is chunk-granular, so every tree is still eventually visited. Peer latency was the raw last sample, which swings with whatever the OS was doing when the echo landed. It is now the Jacobson/Karels smoothed estimate with the standard 1/8 and 1/4 gains, and the mean deviation it produces is kept as jitter - the "is it steady" half of the story that a single number cannot tell. A client was handed an empty player list and rendered nothing, so a two-player session looked like a single-player one from one side of it. Clients now build a roster from the host peer plus the players they are tracking positions for. Another client's name genuinely is not known there - no roster travels to clients - so it reads "Player 3" rather than inventing one, and an unmeasured latency is -1 so the panel can show nothing instead of a misleading zero. MultiplayerSession.HostPlayerId is public for the same reason: the game layer has to tell which entry is the host, and guessing 0 mislabels everyone. * Add map pings, camera navigation and host lobby controls (protocol v51) The co-op features from #18 that the codebase genuinely does not already have, rebuilt to fit it. Original idea and first implementation by t1garbiznisbrate-ship-it. A map ping is a transient "look here" beacon with an optional note, drawn as a ring that grows and fades over six seconds in the sender's cursor colour. It travels as command id 29 rather than as chat text. #18 encoded it into a chat line ("/ping x y z id label") and parsed it back out on receipt, which means the author of the marker is whoever typed the line - anyone could drop a ping signed with another player's name - and every chat line in the session had to be tested for it. As a command the sender comes from the message envelope the session already authenticates, and the coordinates go through WireGuard like every other command's do. A ping mutates nothing, so it is deliberately outside the sync pipeline: no echo guard, no snapshot, no resync on loss. A ping that does not arrive is a ping nobody saw, and that is the whole failure mode. The sender records its own ring at send time rather than waiting for the echo, because a host is notified of its own commands and a client is not - relying on the echo would draw the host's pings and silently swallow every client's. Pings are not gated on ShowPartnerMarkers. That setting hides the ambient cursor rings; someone who turned those off still wants to see a partner deliberately pointing at something. /goto <player>, /goto ping, /follow and /unfollow move the camera. Chat commands hold no camera reference and have no business taking one, so they record an intent on the service and PlayerCursorSyncSystem - which already owns the CameraUpdateSystem reference - carries it out. Name resolution takes an exact id, then an exact name, then a unique prefix; an ambiguous prefix resolves to nothing, because following the wrong partner is worse than being asked to be more specific. /lock and /unlock refuse new joins without ending the session; everyone already connected stays. /banlist and /unban make the existing per-session address bans visible and reversible, which matters mostly for undoing a ban placed on the wrong player. All four are host-only and say so when they are not. The lock clears when hosting ends, alongside the ban list. /help lists what exists, and /clear clears the local log only and says so. An unrecognised slash word is not claimed and goes out as ordinary chat, so a typo is visible rather than silently swallowed. * Say when a world sync starts, finishes, or had nobody to sync with Adapted from t1garbiznisbrate-ship-it's work in #18. Pressing sync as a host with nobody connected opened an epoch, found no participants and closed it again, in silence. From the button that was indistinguishable from a sync that had failed. It now says there is nobody to sync with, and the host log records the participant count either way. Completion was equally quiet on both sides. The host watched its own simulation stop and start again with no explanation, and a client watched its world reload and then had to infer from the clock moving that the handover was over. Each now gets a line when it finishes - the client as it installs the snapshot, the host once every participant has resumed. NotifyChat is internal rather than private so the world-sync flow, which lives in the game layer, reports through the same path the session's own notices use. SanitizeSpeed had no upper bound. The resume speed arrives over the wire, and the game's own selector tops out at 3, so a peer sending a large finite value would have had the simulation resume at it. Clamped to a generous 8. * Report the worst peer's jitter beside the latency spread RecordRttSample already computes the mean deviation; nothing showed it. The periodic session line now carries min/avg/max of the smoothed round-trip plus the worst peer's variation. A link reads as fine on the average right up until the variation is what is hurting it, and that is the number a session that feels like it is stuttering is actually about. * Replicate transport line ticket prices (protocol v52) A line's ticket price lives on its runtime TransportLine component, not in its Policy buffer, so PolicySyncSystem never saw it and no state channel carried it. Everything else about a line already replicates - geometry, stops, colour, name - which is what made the gap easy to miss: the two cities look identical and then disagree about fare revenue, and that disagreement compounds every transport tick, surfacing as budget drift rather than as anything visible on the map. Shape follows PolicySyncSystem. A 1 Hz scan diffs each line's price against what this machine last saw and sends only what changed; prices move when a player drags a slider, so watching every frame would buy nothing. The first ready tick seeds the baseline rather than sending it, because the prices in a freshly loaded save are already agreed and broadcasting them would have every peer re-announce the whole network on join. An echo guard keyed on route number and price stops an applied price being re-detected as a local edit. Lines are matched by route number - the identity RouteUpdateCommand already uses, agreed between peers because route creation replicates it - and the prefab name is checked before writing, so a number reused by a different kind of line cannot have a bus fare written onto a freight route. A line that has not arrived yet is not a reason to resync: it is still in the route pipeline, and the sender's next scan will carry the price again once it lands. The baseline is dropped on a world reload alongside the queue. Keeping it would have the first scan afterwards read every price as a change and rebroadcast the entire network. * Validate the transit fare route number through RouteCommandCodec * Route every log line through one topic-tagged logger The mod had two logging front doors writing two files that disagreed. 243 Mod.log calls and 97 Mod.Verbose calls went to the game log; 174 separate FlightRecorder.Note calls went to the flight log; SyncLog, added for exactly this purpose, had no callers at all. Roughly 65 places wrote the same fact twice, once as prose and once as key=value, so neither file was complete and the pair had to be read together. Everything now goes through SyncLog, and both files are written from that one path. Four tiers, chosen by what the line is rather than by who wrote it: Detail troubleshooting chatter - both logs, only while its topic is on Trace a compact breadcrumb - flight log always, game log when on Event a milestone - both logs, always Warn something went wrong - both logs, always, flushed Error the mod could not cope - both logs, always, flushed, with the stack Severity, not the settings, decides whether a switch is consulted. Connects, disconnects, world transfers, resyncs, dropped commands, quarantined batches and every fault are written with every switch off, because nobody turns a switch on before the crash they did not know was coming. Gating is per feature, not one "verbose" flag. LogTopic names eighteen of them - session, transport, world transfer, resync, pipeline, nets, buildings, land, city, routes, the four zone economies, players, UI, startup and performance - each with its own switch on a new Logging options tab, plus a Log Everything master for "I do not know which one". The enum lives in Core/Diagnostics so the portable networking and session code names the same topics; IModLogger mirrors the same shape and ColossalModLogger is the seam. Call sites no longer write prefixes. The "[MP] ", "[MP][OCC-DEV] ", "[security] ", "[upnp] ", "[relay] " and "[compatibility] " spellings are gone; the logger attaches one [topic] tag so every line is greppable and no two subsystems can drift. Why two files still, and what the difference is: the game log is the readable one. The flight log is the same content made durable - it is flushed per fault so a hard exit keeps its tail, it is not truncated when the game restarts, it also captures Unity and other mods' exceptions, and it is machine-readable. Nothing reaches the game log without also reaching it, so "send us CS2MP-flight.log" is now a complete answer. Detail and trace lines are buffered rather than flushed, and the next fault commits them, so turning a topic on no longer puts a disk write in every frame. Also: thirty "X ready." lines replaced by one startup line carrying the mod, protocol and game versions; the 30-second performance report demoted out of every player's log; expensive diagnostic probes now follow their own feature switch instead of the master one; resync reports log their full summary rather than just the reason. * Added full house sync for all Types + Recuded cpu load * Enhance synchronization systems with improved state handling and logging
🏙️ Full-Spectrum Sync (45 Commands), SIMD Engine, Stability Improvements and more...
Important
NEEDS TESTING!!!!! (I am having issues with my PC right now, so I can start testing in about a week or so)
This release establishes a 100% complete simulation synchronization engine covering all 45 discrete command pipelines, 64-bit unrolled SIMD delta diffing, Large Object Heap (LOH) slab memory management, rolling 32-bit checksum desync detection, 5-tier role authorization, 3D spatial presence, automatic micro-desync self-healing and more...
📑 Table of Contents
🌟 High-Level Architecture Overview
The mod uses a hybrid Authoritative Host + Lockstep Command Replication + Optimistic Prediction model that operates directly within the Unity ECS (Entities 1.0 / DOTS) simulation loop:
graph TD subgraph Client Layer [Remote Client Instances] UI[Multiplayer UI & HUD] Predict[Optimistic Predictor] Input[Tool & Mouse Input Capture] end subgraph Transport Layer [Network Pipeline] DualStack[DualMode IPv6/IPv4 Socket] Framing[VarInt Framed Streaming] STUN[RFC 5389 STUN NAT Client] LAN[Multi-Adapter UDP Broadcast] end subgraph Host Engine [Authoritative Server] Dedup[Sliding 64-Bit Bitmask Filter] Replay[200-Command Rolling Replay Buffer] Auth[5-Tier Role Security Guard] Slab[64MB Pinned LOH Slab Allocator] Delta[64-Bit Chunk XOR Delta Engine] Sweep[15s Micro-Desync Self-Healer] Checksum[32-Bit FNV-1a Checksum Verifier] end Input --> Predict Predict --> Framing Framing --> DualStack DualStack --> Dedup Dedup --> Auth Auth --> Host Host --> Delta Host --> Replay Host --> Checksum Host --> Sweep🎯 Complete 45-Command Deep-Dive Synchronization Matrix
Every physical entity, transit route, financial setting, environmental condition, and micro-management switch is mapped to an authoritative packet schema:
ObjectCreateNetPlacementObjectDeleteNetDeleteZonePaintTreeBrushTreeDeletePropBrushNetUpgradeAreaCreateAreaDeleteRouteCreateRouteDeleteDisasterControlPolicySetAreaModifyRouteUpdateObjectUpgradeNetReplaceVisualCustomizationColorPaletteTerrainBrushDistrictPolicyWaterSimulationSoilPollutionCityBudgetCustomNameSimulationSpeedCityLoanMilestoneUtilityGridPollutionWeatherControlGhostPlacementDistrictClaimBookmark/mark <name>,/goto <name>).MeasurementChecksumTrafficLightTransitLineDetailBuildingToggleParkFeeServiceDistrictTransitColorChirper/chirp <message>).⚡ Hardware Acceleration, SIMD & Memory Subsystems
1. Vectorized 64-Bit Chunk XOR Delta Snapshot Engine (
DeltaSnapshotCodec.cs)ulong), processing binary delta diffs at multi-gigabyte per second speeds./synccalculates and transmits a compact binary delta patch, reducing resync times to under 2 seconds.2. Large Object Heap (LOH) Slab Memory Allocator (
BufferPool.cs)3. Spatial Hash Grid Culling Engine (
SpatialGridCulling.cs)4. Adaptive Simulation Frame-Pacing (
PlayerCursorSyncSystem.cs)Time.unscaledDeltaTime.5. Adaptive Bezier Curve Point Compactor (
CurveCompactor.cs)🔒 Lockstep Determinism, Checksumming & Desync Prevention
ChecksumSyncSystem.cs):CommandDeduplicator.cs):Administration.cs&Messaging.cs):MicroDesyncHealerSystem.cs):GhostCleanupSystem.cs):🌐 Network Transport, Sockets & NAT Traversal
ThreadPriority.AboveNormal.StunClient.cs): Automatically discovers public IP addresses and NAT mapping behavior using standard STUN endpoints.LanDiscovery.cs): Broadcasts discovery beacons across all physical and virtual network adapters simultaneously.Peer.cs): Continuously calculates smoothed round-trip times (🎨 Visual Co-op Presence, Navigation & Audio Immersion
PlayerCompassSystem.cs):SplineInterpolator.cs):/follow <player>) using cubic Catmull-Rom spline curves.MeasurementSyncSystem.cs):MapPingSystem.cs):danger,build,traffic,plan) with spatial alert sounds.CoopAudio.cs):GhostPreviewSyncSystem.cs):🛡️ Host Governance, 5-Tier Roles & Municipal Audit
5-Tier Player Role Hierarchy (
PlayerRole.cs):Governance & Moderation Command Suite:
/votekick <player>&/vote yes|no: Democratic vote-kick engine requiring a >50% majority./audit: Displays a rolling 200-event municipal ledger of player actions./lock&/unlock: Mid-game lobby privacy locking./banlist&/unban <ip>: Host IP address ban management./chirp <message>: Broadcasts official announcements to the citizen social feed.🌍 Global Multi-Language Localization Matrix
All mod UI, bindings, chat messages, and status HUDs are translated across 8 embedded language locales (
.properties):en-US)de-DE)fr-FR)es-ES)ja-JP)pt-BR)ru-RU)zh-HANS) & Traditional (zh-HANT)📁 Comprehensive 96-File Master Inventory
Core Networking & Protocol:
CS2MultiplayerMod/Core/Diagnostics/NetworkProfiler.cs[NEW]CS2MultiplayerMod/Core/Networking/BufferPool.cs[NEW]CS2MultiplayerMod/Core/Networking/Discovery/LanDiscovery.cs[NEW]CS2MultiplayerMod/Core/Networking/Stun/StunClient.cs[NEW]CS2MultiplayerMod/Core/Networking/Tcp/FramedConnection.cs[MODIFIED]CS2MultiplayerMod/Core/Networking/Tcp/TcpClientTransport.cs[MODIFIED]CS2MultiplayerMod/Core/Networking/Tcp/TcpServerTransport.cs[MODIFIED]CS2MultiplayerMod/Core/Protocol/CommandDeduplicator.cs[NEW]CS2MultiplayerMod/Core/Protocol/CurveCompactor.cs[NEW]CS2MultiplayerMod/Core/Protocol/NativeBufferCodec.cs[NEW]CS2MultiplayerMod/Core/Protocol/SplineInterpolator.cs[NEW]CS2MultiplayerMod/Core/Protocol/VarInt.cs[NEW]CS2MultiplayerMod/Core/Protocol/VectorQuantizer.cs[NEW]CS2MultiplayerMod/Core/Protocol/ZoneRleCodec.cs[NEW]Session & Governance:
CS2MultiplayerMod/Core/Session/AuditLog.cs[NEW]CS2MultiplayerMod/Core/Session/BlobReassembler.cs[MODIFIED]CS2MultiplayerMod/Core/Session/DeltaSnapshotCodec.cs[NEW]CS2MultiplayerMod/Core/Session/MultiplayerSession/Administration.cs[MODIFIED]CS2MultiplayerMod/Core/Session/MultiplayerSession/Messaging.cs[MODIFIED]CS2MultiplayerMod/Core/Session/MultiplayerSession/MultiplayerSession.cs[MODIFIED]CS2MultiplayerMod/Core/Session/MultiplayerSession/Transport.cs[MODIFIED]CS2MultiplayerMod/Core/Session/Peer.cs[MODIFIED]CS2MultiplayerMod/Core/Session/PeerRateLimiter.cs[MODIFIED]CS2MultiplayerMod/Core/Session/PlayerRole.cs[NEW]CS2MultiplayerMod/Core/Session/SavegameCompression.cs[NEW]CS2MultiplayerMod/Core/Session/VoteSession.cs[NEW]Game Sync Commands (IDs 1–45):
CS2MultiplayerMod/Game/Sync/Commands/AreaCreateCommand.csCS2MultiplayerMod/Game/Sync/Commands/AreaDeleteCommand.csCS2MultiplayerMod/Game/Sync/Commands/AreaUpdateCommand.csCS2MultiplayerMod/Game/Sync/Commands/AssetStampCommand.csCS2MultiplayerMod/Game/Sync/Commands/BookmarkCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/BuildingToggleCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/ChecksumCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/ChirperCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/CityBudgetCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/CityLoanCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/ColorPaletteCommand.csCS2MultiplayerMod/Game/Sync/Commands/CustomNameCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/DeleteCommands.csCS2MultiplayerMod/Game/Sync/Commands/DevTreePurchaseCommand.csCS2MultiplayerMod/Game/Sync/Commands/DisasterEventCommand.csCS2MultiplayerMod/Game/Sync/Commands/DistrictClaimCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/EntityPolicyCommand.csCS2MultiplayerMod/Game/Sync/Commands/GhostPlacementCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/MeasurementCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/MilestoneCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/NetPlacementCommand.csCS2MultiplayerMod/Game/Sync/Commands/NetReplaceCommand.csCS2MultiplayerMod/Game/Sync/Commands/NetUpgradeCommand.csCS2MultiplayerMod/Game/Sync/Commands/ObjectMoveCommand.csCS2MultiplayerMod/Game/Sync/Commands/ObjectPlacementCommand.csCS2MultiplayerMod/Game/Sync/Commands/ObjectToolOperationCommand.csCS2MultiplayerMod/Game/Sync/Commands/OwnedAreaSnapshotCommand.csCS2MultiplayerMod/Game/Sync/Commands/ParkFeeCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/PollutionCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/RouteCreateCommand.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Commands/RouteDeleteCommand.csCS2MultiplayerMod/Game/Sync/Commands/RouteUpdateCommand.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Commands/RouteWaypointIntent.csCS2MultiplayerMod/Game/Sync/Commands/ServiceDistrictCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/SimulationSpeedCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/TerrainBrushCommand.csCS2MultiplayerMod/Game/Sync/Commands/TilePurchaseCommand.csCS2MultiplayerMod/Game/Sync/Commands/TrafficLightCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/TransitColorCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/TransitLineDetailCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/TreeStateBatch.csCS2MultiplayerMod/Game/Sync/Commands/UpgradePlacementCommand.csCS2MultiplayerMod/Game/Sync/Commands/UtilityGridCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/VisualCustomizationCommand.csCS2MultiplayerMod/Game/Sync/Commands/WeatherControlCommand.cs[NEW]CS2MultiplayerMod/Game/Sync/Commands/ZonePaintCommand.csGame Systems & Infrastructure:
CS2MultiplayerMod/Game/CoopAudio.cs[NEW]CS2MultiplayerMod/Game/JoinMapLoader.cs[MODIFIED]CS2MultiplayerMod/Game/MultiplayerService/Chat.cs[MODIFIED]CS2MultiplayerMod/Game/MultiplayerService/MultiplayerService.cs[MODIFIED]CS2MultiplayerMod/Game/MultiplayerService/WorldTransfer.cs[MODIFIED]CS2MultiplayerMod/Game/MultiplayerUISystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Infrastructure/EntityMapTable.cs[NEW]CS2MultiplayerMod/Game/Sync/Infrastructure/SpatialGridCulling.cs[NEW]CS2MultiplayerMod/Game/Sync/Players/MapPingSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Players/PlayerCompassSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Players/PlayerCursorRenderSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Players/PlayerCursorSyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/BuildingToggleSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/ChecksumSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/ChirperSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/CityBookmarkSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/CityBudgetSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/CityLoanSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/CustomNameSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/DisasterSyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/DistrictClaimSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/GhostCleanupSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/GhostPreviewSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/MeasurementSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/MicroDesyncHealerSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/MilestoneSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/NetUpgradeSyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/ParkFeeSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/PolicySyncSystem/PolicySyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/PollutionSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/RouteSyncSystem/Capture.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/RouteSyncSystem/Realize.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/RouteSyncSystem/RouteSyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/ServiceDistrictSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/SimulationSpeedSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/TrafficControlSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/TransitColorSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/TransitLineDetailSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/UpgradeSyncSystem.cs[MODIFIED]CS2MultiplayerMod/Game/Sync/Systems/UtilityGridSyncSystem.cs[NEW]CS2MultiplayerMod/Game/Sync/Systems/WeatherControlSyncSystem.cs[NEW]CS2MultiplayerMod/Mod.cs[MODIFIED]Localization Locales:
CS2MultiplayerMod/Localization/locales/es.properties[NEW]CS2MultiplayerMod/Localization/locales/fr.properties[NEW]CS2MultiplayerMod/Localization/locales/ja.properties[NEW]CS2MultiplayerMod/Localization/locales/pt-BR.properties[NEW]CS2MultiplayerMod/Localization/locales/ru.properties[NEW]CS2MultiplayerMod/Localization/locales/zh-HANS.properties[NEW]