Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a194bed
add paths for MS VS 18/2026
vrnhgd Jul 5, 2026
e8885f7
search for CS in normal directories and provide options for non-power…
vrnhgd Jul 5, 2026
13f97d9
add a powershell script for running the game server
vrnhgd Jul 5, 2026
791e357
API server might be hosted on a different port than 80 if it's not re…
vrnhgd Jul 5, 2026
2e25e18
add server name and public discoverability
vrnhgd Jul 5, 2026
2181fec
UI changes required for joining/finding public games
vrnhgd Jul 5, 2026
7dd005a
upgrade game server to include HTTP API and expose 4240 and 4241
vrnhgd Jul 5, 2026
fc44065
send the keep-alive every 5s as computed by wallclock time
vrnhgd Jul 5, 2026
ad961de
add a script that makes it more convenient to deploy built DLL files …
vrnhgd Jul 5, 2026
6f087bb
debug loggin for the parser
vrnhgd Jul 5, 2026
67bc06c
use custom Json parser for PublicServerListing because JsonUtility is…
vrnhgd Jul 6, 2026
1f4b71f
display message to show refresh list status in case things take a cou…
vrnhgd Jul 6, 2026
f8fe25c
remove vestigial macos
vrnhgd Jul 6, 2026
35d0b30
use 'Protected' instead of 'Locked'
vrnhgd Jul 6, 2026
ce3c5af
include ServerToken as a unique ID for the Server - include the Addre…
vrnhgd Jul 6, 2026
c3155ac
use protobufs and LiteNetLib instead of HTTP
vrnhgd Jul 7, 2026
4a8930d
prompt for password if the server has a password
vrnhgd Jul 7, 2026
9f8b3e8
prefill username from last username used with several fallbacks
vrnhgd Jul 7, 2026
ca5f342
only poll when the panel is open
vrnhgd Jul 7, 2026
fe0fab8
join public/Steam servers through a shared token-based flow
vrnhgd Jul 21, 2026
52bc2a6
revert changes that create HTTP server
vrnhgd Jul 22, 2026
ca64652
add a line break to the message
vrnhgd Jul 22, 2026
fc0508f
remove references to Address
vrnhgd Jul 22, 2026
b431ad1
Apply suggestions from code review
vrnhgd Jul 27, 2026
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
64 changes: 59 additions & 5 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ param (
)

# Functions
Function Find-MsBuild([int] $MaxVersion = 2022)
Function Find-MsBuild([int] $MaxVersion = 2026)
{
$agent2026Path = "$Env:programfiles\Microsoft Visual Studio\18\BuildTools\MSBuild\Current\Bin\msbuild.exe"
$ent2026Path = "$Env:programfiles\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin\msbuild.exe"
$pro2026Path = "$Env:programfiles\Microsoft Visual Studio\18\Professional\MSBuild\Current\Bin\msbuild.exe"
$community2026Path = "$Env:programfiles\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\msbuild.exe"

$agent2022Path = "$Env:programfiles\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\msbuild.exe"
$ent2022Path = "$Env:programfiles\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\msbuild.exe"
$pro2022Path = "$Env:programfiles\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\msbuild.exe"
Expand All @@ -39,7 +44,12 @@ Function Find-MsBuild([int] $MaxVersion = 2022)
$fallback2013Path = "${Env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe"
$fallbackPath = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe"

If ((2022 -le $MaxVersion) -And (Test-Path $agent2022Path)) { return $agent2022Path }
If ((2026 -le $MaxVersion) -And (Test-Path $agent2026Path)) { return $agent2026Path }
If ((2026 -le $MaxVersion) -And (Test-Path $ent2026Path)) { return $ent2026Path }
If ((2026 -le $MaxVersion) -And (Test-Path $pro2026Path)) { return $pro2026Path }
If ((2026 -le $MaxVersion) -And (Test-Path $community2026Path)) { return $community2026Path }

If ((2022 -le $MaxVersion) -And (Test-Path $agent2022Path)) { return $agent2022Path }
If ((2022 -le $MaxVersion) -And (Test-Path $ent2022Path)) { return $ent2022Path }
If ((2022 -le $MaxVersion) -And (Test-Path $pro2022Path)) { return $pro2022Path }
If ((2022 -le $MaxVersion) -And (Test-Path $community2022Path)) { return $community2022Path }
Expand Down Expand Up @@ -106,12 +116,56 @@ If ($Update)
Write-Host "[CSM Update Script] You have specified the -Update flag. The script will now update the local assemblies to the installed version under Cities: Skylines."
Write-Host "[CSM Update Script] Please Note: This may break the mod if any major game changes have occured."

# Get the steam directory
# Get the game directory
If ($GameDirectory -eq "")
{
$GameDirectory = Read-Host "[CSM Update Script] Please enter your game folder directory. For example, for Windows, 'C:\Program Files\Steam\steamapps\common\Cities_Skylines' for Steam or 'C:\Program Files\Epic Games\CitiesSkylines' for Epic Games."
# Common default install locations
$DefaultPaths = @()
If ($IsMacOS)
{
$DefaultPaths += @{ Label = "Steam"; Path = "~/Library/Application Support/Steam/steamapps/common/Cities_Skylines" }
}
ElseIf ($IsLinux)
{
$DefaultPaths += @{ Label = "Steam"; Path = "~/.local/share/Steam/steamapps/common/Cities_Skylines" }
}
Else
{
$DefaultPaths += @{ Label = "Steam"; Path = "C:\Program Files (x86)\Steam\steamapps\common\Cities_Skylines" }
$DefaultPaths += @{ Label = "Epic Games"; Path = "C:\Program Files\Epic Games\CitiesSkylines" }
}

# Only offer the ones that actually exist
$FoundPaths = @($DefaultPaths | Where-Object { Test-Path -Path $_.Path })

If ($FoundPaths.Count -gt 0)
{
Write-Host "[CSM Update Script] Found the following Cities: Skylines installation(s):"
For ($i = 0; $i -lt $FoundPaths.Count; $i++)
{
Write-Host "[CSM Update Script] $($i + 1)) $($FoundPaths[$i].Path) ($($FoundPaths[$i].Label))"
}
$OtherOption = $FoundPaths.Count + 1
Write-Host "[CSM Update Script] $($OtherOption)) Enter a different directory"

$Selection = Read-Host "[CSM Update Script] Please choose an option (1-$($OtherOption))"

$SelectionIndex = 0
If ([int]::TryParse($Selection, [ref]$SelectionIndex) -and $SelectionIndex -ge 1 -and $SelectionIndex -le $FoundPaths.Count)
{
$GameDirectory = $FoundPaths[$SelectionIndex - 1].Path
}
Else
{
$GameDirectory = Read-Host "[CSM Update Script] Please enter your game folder directory."
}
}
Else
{
$GameDirectory = Read-Host "[CSM Update Script] Please enter your game folder directory. For example, for Windows, 'C:\Program Files\Steam\steamapps\common\Cities_Skylines' for Steam or 'C:\Program Files\Epic Games\CitiesSkylines' for Epic Games."
}
}
Else
Else
{
Write-Host "[CSM Update Script] Game directory: $($GameDirectory)"
}
Expand Down
69 changes: 69 additions & 0 deletions scripts/deploy.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/pwsh
#########################################################################
# This script copies the locally built CSM mod .dll files to a remote #
# machine's Cities: Skylines mod folder (e.g. deploying from a Windows #
# dev box to a Mac for testing). #
#########################################################################

# Params that can be passed in
param (
[string]$RemoteUser,
[string]$RemoteHost = "varuns-macbook-pro",
[string]$RemoteOS = "mac",
[string]$SourceDirectory = "..\src\csm\bin\Release",
[string]$RemoteModDirectory = "Default"
)

if ([string]::IsNullOrEmpty($RemoteUser))
{
Write-Host "[CSM Deploy Script] You must specify -RemoteUser (the SSH username on $($RemoteHost))."
exit 1
}

If ($RemoteModDirectory -eq "Default")
{
Switch ($RemoteOS.ToLower())
{
"mac" { $RemoteModDirectory = "~/Library/Application Support/Colossal Order/Cities_Skylines/Addons/Mods/CSM" }
"linux" { $RemoteModDirectory = "~/.local/share/Colossal Order/Cities_Skylines/Addons/Mods/CSM" }
default
{
Write-Host "[CSM Deploy Script] Unknown -RemoteOS '$($RemoteOS)'. Expected 'mac' or 'linux', or pass -RemoteModDirectory explicitly."
exit 1
}
}
}

$RemoteTarget = "$($RemoteUser)@$($RemoteHost)"

Write-Host "[CSM Deploy Script] Source directory: $($SourceDirectory)"
Write-Host "[CSM Deploy Script] Remote target: $($RemoteTarget)"
Write-Host "[CSM Deploy Script] Remote mod directory: $($RemoteModDirectory)"

# Make sure there is something to deploy
$Dlls = Get-ChildItem -Path $SourceDirectory -Filter "*.dll" -ErrorAction SilentlyContinue
If (-not $Dlls)
{
Write-Host "[CSM Deploy Script] No .dll files found in $($SourceDirectory). Did you run build.ps1 -Build first?"
exit 1
}

# Make sure the remote mod directory exists
Write-Host "[CSM Deploy Script] Ensuring remote mod directory exists..."
ssh $RemoteTarget "mkdir -p '$($RemoteModDirectory)'"
If ($LASTEXITCODE -ne 0)
{
Write-Host "[CSM Deploy Script] Failed to create/verify the remote mod directory. Check SSH connectivity to $($RemoteTarget)."
exit $LASTEXITCODE
}

# Copy the dll files across
Write-Host "[CSM Deploy Script] Copying $($Dlls.Count) file(s)..."
scp "$($SourceDirectory)\*.dll" "$($RemoteTarget):$($RemoteModDirectory)/"
If ($LASTEXITCODE -ne 0)
{
Write-Host "[CSM Deploy Script] Deploy failed!"
exit $LASTEXITCODE
}

Write-Host "[CSM Deploy Script] Done! Files deployed to $($RemoteTarget):$($RemoteModDirectory)"
27 changes: 27 additions & 0 deletions scripts/run-gs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/pwsh
#########################################################################
# This script runs the CSM.GS server locally (the NAT relay / public #
# server list service). Useful for testing hosting a server against a #
# self-hosted API server instead of the official one. #
#########################################################################

# Params that can be passed in
param (
[int]$Port = 4240,
[int]$HttpPort = 4241,
[string]$Configuration = "Debug"
)

Write-Host "[CSM GS Script] Starting the CSM.GS server locally."
Write-Host "[CSM GS Script] UDP (NAT relay) port: $($Port)"
Write-Host "[CSM GS Script] HTTP (public server list) port: $($HttpPort)"
Write-Host "[CSM GS Script] Point your game's 'CSM API Server' setting at this machine, with matching ports, to use it."

$env:GS_PORT = $Port
$env:GS_HTTP_PORT = $HttpPort

# CSM.GS targets net7.0, which may not be installed if only a newer .NET is
# present. Roll forward to whatever major version is available.
$env:DOTNET_ROLL_FORWARD = "LatestMajor"

dotnet run --project "$PSScriptRoot/../src/gs/CSM.GS.csproj" --configuration $Configuration
7 changes: 7 additions & 0 deletions src/csm/CSM.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,15 @@
<Compile Include="..\gs\Commands\Data\ApiServer\ApiCommandBase.cs" Link="GS\Commands\Data\ApiServer\ApiCommandBase.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\PortCheckRequestCommand.cs" Link="GS\Commands\Data\ApiServer\PortCheckRequestCommand.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\PortCheckResultCommand.cs" Link="GS\Commands\Data\ApiServer\PortCheckResultCommand.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\PublicServerEntry.cs" Link="GS\Commands\Data\ApiServer\PublicServerEntry.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\ServerListRequestCommand.cs" Link="GS\Commands\Data\ApiServer\ServerListRequestCommand.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\ServerListResultCommand.cs" Link="GS\Commands\Data\ApiServer\ServerListResultCommand.cs" />
<Compile Include="..\gs\Commands\Data\ApiServer\ServerRegistrationCommand.cs" Link="GS\Commands\Data\ApiServer\ServerRegistrationCommand.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\ApiCommandHandler.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\PortCheckRequestHandler.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\PortCheckResultHandler.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\ServerListRequestHandler.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\ServerListResultHandler.cs" />
<Compile Include="GS\Commands\Handler\ApiServer\ServerRegistrationHandler.cs" />
<Compile Include="Helpers\AssemblyHelper.cs" />
<Compile Include="Helpers\SaveHelpers.cs" />
Expand Down Expand Up @@ -155,6 +160,7 @@
<Compile Include="Networking\MultiplayerManager.cs" />
<Compile Include="Networking\Server.cs" />
<Compile Include="Commands\TransactionHandler.cs" />
<Compile Include="Panels\BrowseServersPanel.cs" />
<Compile Include="Panels\ConnectedPlayersPanel.cs" />
<Compile Include="Panels\MessagePanel.cs" />
<Compile Include="Panels\PanelManager.cs" />
Expand All @@ -167,6 +173,7 @@
<Compile Include="Panels\ManageGamePanel.cs" />
<Compile Include="Panels\HostGamePanel.cs" />
<Compile Include="Panels\JoinGamePanel.cs" />
<Compile Include="Panels\PasswordPromptPanel.cs" />
<Compile Include="CSM.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Util\CSMWebClient.cs" />
Expand Down
12 changes: 12 additions & 0 deletions src/csm/GS/Commands/Handler/ApiServer/ServerListRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using CSM.GS.Commands.Data.ApiServer;

namespace CSM.GS.Commands.Handler.ApiServer
{
public class ServerListRequestHandler : ApiCommandHandler<ServerListRequestCommand>
{
protected override void Handle(ServerListRequestCommand command)
{
// Do nothing, this is a packet for the global server
}
}
}
13 changes: 13 additions & 0 deletions src/csm/GS/Commands/Handler/ApiServer/ServerListResultHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CSM.GS.Commands.Data.ApiServer;
using CSM.Panels;

namespace CSM.GS.Commands.Handler.ApiServer
{
public class ServerListResultHandler : ApiCommandHandler<ServerListResultCommand>
{
protected override void Handle(ServerListResultCommand command)
{
PanelManager.GetPanel<BrowseServersPanel>()?.OnServerListReceived(command.Servers ?? new PublicServerEntry[0]);
}
}
}
32 changes: 1 addition & 31 deletions src/csm/Helpers/SteamHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
using System.Threading;
using ColossalFramework;
using ColossalFramework.PlatformServices;
using ColossalFramework.Threading;
using CSM.API;
using CSM.Helpers.Steamworks;
using CSM.Networking;
using CSM.Networking.Config;
using CSM.Panels;

namespace CSM.Helpers
Expand Down Expand Up @@ -135,34 +132,7 @@ private void JoinFromSteam(string token, bool checkLoadingManager)
return;
}

Log.Info("Join request for " + token);

JoinGamePanel join = PanelManager.ShowPanel<JoinGamePanel>();
join.SetConnecting();

MultiplayerManager.Instance.CurrentClient.StartMainMenuEventProcessor();
MultiplayerManager.Instance.ConnectToServer(new ClientConfig(token, PlatformService.personaName),
(success) =>
{
if (success)
{
// See WorldTransferHandler for actual loading
ThreadHelper.dispatcher.Dispatch(() =>
{
MultiplayerManager.Instance.BlockGameFirstJoin();
PanelManager.HidePanel<JoinGamePanel>();
});
}
else
{
ThreadHelper.dispatcher.Dispatch(() =>
{
JoinGamePanel panel = PanelManager.ShowPanel<JoinGamePanel>();
panel.FillFieldsOnError(token, PlatformService.personaName,
MultiplayerManager.Instance.CurrentClient.ConnectionMessage);
});
}
});
JoinGamePanel.JoinByToken(token, PlatformService.personaName);
}

public void Shutdown()
Expand Down
2 changes: 1 addition & 1 deletion src/csm/Injections/MainMenuHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static void CheckForUpdate(bool alwaysShowInfo)
{
try
{
string latest = new CSMWebClient().DownloadString($"http://{CSM.Settings.ApiServer}/api/version");
string latest = new CSMWebClient().DownloadString($"http://{CSM.Settings.ApiServer}:{CSM.Settings.ApiServerHttpPort}/api/version");
latest = latest.Substring(1);
string[] versionParts = latest.Split('.');
Version latestVersion = new Version(int.Parse(versionParts[0]), int.Parse(versionParts[1]));
Expand Down
19 changes: 18 additions & 1 deletion src/csm/Networking/Config/ServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ public class ServerConfig
/// <param name="password">The optional password for this server.</param>
/// <param name="maxPlayers">The maximum amount of players that can connect to the server.</param>
/// <param name="enablePortForwarding">Whether automatic port forwarding (UPnP) should be enabled.</param>
public ServerConfig(int port, string username, string password, int maxPlayers, bool enablePortForwarding)
/// <param name="name">The display name of the server, shown in the public server list.</param>
/// <param name="listPublicly">Whether this server should be listed in the public server list.</param>
public ServerConfig(int port, string username, string password, int maxPlayers, bool enablePortForwarding,
string name = "", bool listPublicly = false)
{
Port = port;
Username = username;
Password = password;
MaxPlayers = maxPlayers;
EnablePortForwarding = enablePortForwarding;
Name = name;
ListPublicly = listPublicly;
}

public ServerConfig()
Expand All @@ -32,6 +37,8 @@ public ServerConfig()
Password = "";
MaxPlayers = 0;
EnablePortForwarding = true;
Name = "";
ListPublicly = false;
}

/// <summary>
Expand Down Expand Up @@ -59,5 +66,15 @@ public ServerConfig()
/// Default is false for security reasons.
/// </summary>
public bool EnablePortForwarding;

/// <summary>
/// Gets the display name of the server, shown in the public server list.
/// </summary>
public string Name;

/// <summary>
/// Gets whether this server should be listed in the public server list.
/// </summary>
public bool ListPublicly;
}
}
Loading