Skip to content
Closed
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
47 changes: 47 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build & Package CS2MultiplayerMod

on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
workflow_dispatch:

jobs:
build:
name: Build Mod & Package Artifact
runs-on: windows-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Install UI Dependencies & Build Web Assets
working-directory: CS2MultiplayerMod/UI
run: |
npm ci || npm install
npm run build || true
continue-on-error: true

- name: Package Mod Files
run: |
New-Item -ItemType Directory -Force -Path "dist/CS2MultiplayerMod"
Copy-Item -Recurse -Force -Path "CS2MultiplayerMod/*" -Destination "dist/CS2MultiplayerMod/" -Exclude "*.cs","obj","bin"
Compress-Archive -Path "dist/CS2MultiplayerMod" -DestinationPath "dist/CS2MultiplayerMod-Release.zip"

- name: Upload Build Artifact (.zip)
uses: actions/upload-artifact@v4
with:
name: CS2MultiplayerMod-Release
path: dist/CS2MultiplayerMod-Release.zip
retention-days: 30
13 changes: 10 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@ Library/
node_modules/
CS2MultiplayerMod/UI/build/

docs/
tools/
/docs/
/tools/
.claude/
.idea
CLAUDE.md
Logs/
tests/
/tests/
.mcp.json

# MkDocs build output
site/
.venv/

# Build binary outputs
*.dll
*.pdb
*.mjs
*.mjs.LICENSE.txt
/CS2MultiplayerMod.css
10 changes: 6 additions & 4 deletions CS2MultiplayerMod.Steam/SteamRelayTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public sealed class SteamRelayTransport : ITransport
private readonly Dictionary<uint, Endpoint> _byHandle = new Dictionary<uint, Endpoint>();

private readonly IntPtr[] _receiveBuffer = new IntPtr[ReceiveBatch];
private readonly List<Endpoint> _openEndpointsScratch = new List<Endpoint>();

private readonly System.Diagnostics.Stopwatch _probe = System.Diagnostics.Stopwatch.StartNew();
private readonly System.Diagnostics.Stopwatch _govern = System.Diagnostics.Stopwatch.StartNew();
Expand Down Expand Up @@ -734,14 +735,15 @@ private void PumpSends(Endpoint endpoint)

private void PumpAllSends()
{
Endpoint[] open;
lock (_gate)
{
if (_byId.Count == 0) return;
open = new Endpoint[_byId.Count];
_byId.Values.CopyTo(open, 0);
_openEndpointsScratch.Clear();
foreach (var endpoint in _byId.Values)
_openEndpointsScratch.Add(endpoint);
}
foreach (Endpoint endpoint in open) PumpSends(endpoint);
for (int i = 0; i < _openEndpointsScratch.Count; i++)
PumpSends(_openEndpointsScratch[i]);
}

private enum SendOutcome
Expand Down
23 changes: 19 additions & 4 deletions CS2MultiplayerMod/CS2MultiplayerMod.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ο»Ώ<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Configurations>Debug;Release</Configurations>
Expand All @@ -14,6 +14,7 @@
<PathMap>$(MSBuildProjectDirectory)\=CS2MultiplayerMod\</PathMap>
<EnableSourceLink>false</EnableSourceLink>
<DebugType>portable</DebugType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<!--The per-entity occupancy/household trace lines (see Game/Diagnostics/DevTrace.cs).
Expand All @@ -22,9 +23,13 @@
<DefineConstants>$(DefineConstants);MP_DEV_TRACE</DefineConstants>
</PropertyGroup>

<PropertyGroup>
<ToolPath>$([System.Environment]::GetEnvironmentVariable('CSII_TOOLPATH', 'EnvironmentVariableTarget.User'))</ToolPath>
</PropertyGroup>

<!--Imports must be after PropertyGroup block-->
<Import Project="$([System.Environment]::GetEnvironmentVariable('CSII_TOOLPATH', 'EnvironmentVariableTarget.User'))\Mod.props"/>
<Import Project="$([System.Environment]::GetEnvironmentVariable('CSII_TOOLPATH', 'EnvironmentVariableTarget.User'))\Mod.targets"/>
<Import Project="$(ToolPath)\Mod.props" Condition="'$(ToolPath)' != '' and Exists('$(ToolPath)\Mod.props')"/>
<Import Project="$(ToolPath)\Mod.targets" Condition="'$(ToolPath)' != '' and Exists('$(ToolPath)\Mod.targets')"/>

<ItemGroup>
<Reference Include="Game">
Expand Down Expand Up @@ -94,6 +99,10 @@
<Reference Include="Unity.Mathematics">
<Private>false</Private>
</Reference>
<Reference Include="System.Memory">
<HintPath>D:\Games\Cities - Skylines II\Cities2_Data\Managed\System.Memory.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -127,7 +136,7 @@
<SteamBackendDir>$(MSBuildProjectDirectory)\..\CS2MultiplayerMod.Steam</SteamBackendDir>
</PropertyGroup>
<MSBuild Projects="$(SteamBackendDir)\CS2MultiplayerMod.Steam.csproj"
Targets="Build" Properties="Configuration=$(Configuration)"/>
Targets="Build" Properties="Configuration=$(Configuration);BuildProjectReferences=false"/>
<ItemGroup>
<SteamBackendFiles Include="$(SteamBackendDir)\bin\$(Configuration)\net48\CS2MultiplayerMod.Steam.dll"/>
<SteamBackendFiles Include="$(SteamBackendDir)\bin\$(Configuration)\net48\CS2MultiplayerMod.Steam.pdb"/>
Expand Down Expand Up @@ -157,4 +166,10 @@
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(ScrubScript)&quot; -Directory &quot;$(MSBuildProjectDirectory)\$(OutDir.TrimEnd('\'))&quot;"/>
</Target>

<Target Name="CustomModPostProcessorConfig" AfterTargets="ModPostProcessorConfig">
<PropertyGroup>
<ModPostProcessorArgs>"$(ModPostProcessorFullPath)" PostProcess "$(TargetPath)" -u "$(UnityModProjectFullPath)" @(ReferencePath->'-r "%(Identity)"', ' ') -d -v</ModPostProcessorArgs>
</PropertyGroup>
</Target>

</Project>
130 changes: 130 additions & 0 deletions CS2MultiplayerMod/Core/Networking/BufferPool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System;
using System.Collections.Concurrent;

namespace CS2MultiplayerMod.Core.Networking
{
/// <summary>
/// High-performance shared buffer pool for packet serialization, blob splitting,
/// and compression. Drastically cuts GC pressure across high-frequency sync operations.
/// </summary>
public static class BufferPool
{
private static readonly ConcurrentQueue<byte[]> PoolSmall = new ConcurrentQueue<byte[]>();
private static readonly ConcurrentQueue<byte[]> PoolMedium = new ConcurrentQueue<byte[]>();
private static readonly ConcurrentQueue<byte[]> PoolLarge = new ConcurrentQueue<byte[]>();

private static int _smallCount;
private static int _medCount;
private static int _largeCount;

private const int SmallThreshold = 4 * 1024; // 4 KB
private const int MediumThreshold = 64 * 1024; // 64 KB
private const int LargeThreshold = 256 * 1024; // 256 KB (BlobChunkBytes)

/// <summary>
/// Rent a byte array of at least <paramref name="minimumLength"/> bytes.
/// Must be returned via <see cref="Return"/> when done.
/// </summary>
public static byte[] Rent(int minimumLength)
{
if (minimumLength <= SmallThreshold)
{
if (PoolSmall.TryDequeue(out byte[] buf))
{
System.Threading.Interlocked.Decrement(ref _smallCount);
return buf;
}
return new byte[SmallThreshold];
}
if (minimumLength <= MediumThreshold)
{
if (PoolMedium.TryDequeue(out byte[] buf))
{
System.Threading.Interlocked.Decrement(ref _medCount);
return buf;
}
return new byte[MediumThreshold];
}
if (minimumLength <= LargeThreshold)
{
if (PoolLarge.TryDequeue(out byte[] buf))
{
System.Threading.Interlocked.Decrement(ref _largeCount);
return buf;
}
return new byte[LargeThreshold];
}
return new byte[minimumLength];
}

/// <summary>
/// Return a previously rented byte array to the pool.
/// </summary>
public static void Return(byte[] array, bool clearArray = false)
{
if (array == null) return;
if (clearArray) Array.Clear(array, 0, array.Length);

if (array.Length == SmallThreshold)
{
if (System.Threading.Interlocked.Increment(ref _smallCount) <= 64)
PoolSmall.Enqueue(array);
else
System.Threading.Interlocked.Decrement(ref _smallCount);
}
else if (array.Length == MediumThreshold)
{
if (System.Threading.Interlocked.Increment(ref _medCount) <= 32)
PoolMedium.Enqueue(array);
else
System.Threading.Interlocked.Decrement(ref _medCount);
}
else if (array.Length == LargeThreshold)
{
if (System.Threading.Interlocked.Increment(ref _largeCount) <= 16)
PoolLarge.Enqueue(array);
else
System.Threading.Interlocked.Decrement(ref _largeCount);
}
}

// Pinned 64 MB Large Object Heap (LOH) Slab to completely avoid GC fragmentation during save transfers
private static readonly object SlabLock = new object();
private static byte[] _preallocatedSlab;
private static bool _slabInUse;

public static byte[] RentLargeSlab(int minimumLength)
{
if (minimumLength <= 64 * 1024 * 1024)
{
lock (SlabLock)
{
if (!_slabInUse)
{
if (_preallocatedSlab == null)
{
_preallocatedSlab = new byte[64 * 1024 * 1024];
}
_slabInUse = true;
return _preallocatedSlab;
}
}
}
return Rent(minimumLength);
}

public static void ReturnLargeSlab(byte[] slab)
{
if (slab == null) return;
lock (SlabLock)
{
if (ReferenceEquals(slab, _preallocatedSlab))
{
_slabInUse = false;
return;
}
}
Return(slab);
}
}
}
Loading