Skip to content

Commit 74de692

Browse files
authored
Add TM:PE compatibility logic and notification support for missing addon (#335)
1 parent bf58ebd commit 74de692

6 files changed

Lines changed: 208 additions & 15 deletions

File tree

src/api/Log.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public static void Warn(string message)
6464
Instance.Write(message, "Warn");
6565
}
6666

67+
public static void Warn(string message, Exception ex)
68+
{
69+
Instance.Write($"{message}: {ex}", "Warn");
70+
}
71+
6772
public static void Info(string message)
6873
{
6974
Instance.Write(message, "Info");

src/csm/CSM.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
<Compile Include="Models\Vector3Surrogate.cs" />
146146
<Compile Include="Mods\ModCompat.cs" />
147147
<Compile Include="Mods\ModSupport.cs" />
148+
<Compile Include="Mods\TmpeSupportHelper.cs" />
148149
<Compile Include="Networking\Client.cs" />
149150
<Compile Include="Networking\Config\ClientConfig.cs" />
150151
<Compile Include="Networking\Config\ConfigData.cs" />
@@ -201,4 +202,4 @@
201202
<PostBuildEvent Condition="$([MSBuild]::IsOsPlatform('linux'))">pwsh "$(SolutionDir)scripts/build.ps1" -OutputDirectory "$(TargetDir)" -Install</PostBuildEvent>
202203
<PostBuildEvent Condition="$([MSBuild]::IsOsPlatform('osx'))">pwsh "$(SolutionDir)scripts/build.ps1" -OutputDirectory "$(TargetDir)" -Install</PostBuildEvent>
203204
</PropertyGroup>
204-
</Project>
205+
</Project>

src/csm/Helpers/SteamHelpers.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Runtime.InteropServices;
45
using System.Threading;
56
using ColossalFramework;
@@ -93,6 +94,36 @@ public void CheckCommandLine(bool checkLoadingManager)
9394
}
9495
}
9596

97+
public static void OpenOverlayToUrl(string url)
98+
{
99+
if (string.IsNullOrEmpty(url))
100+
{
101+
return;
102+
}
103+
104+
if (CSM.IsSteamPresent && Instance != null && Instance._friendsPtr != IntPtr.Zero)
105+
{
106+
try
107+
{
108+
SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage(Instance._friendsPtr, url, EActivateGameOverlayToWebPageMode.k_EActivateGameOverlayToWebPageMode_Default);
109+
return;
110+
}
111+
catch (Exception ex)
112+
{
113+
Log.Warn($"Failed to open Steam overlay for {url}", ex);
114+
}
115+
}
116+
117+
try
118+
{
119+
Process.Start(url);
120+
}
121+
catch (Exception ex)
122+
{
123+
Log.Warn($"Failed to open url {url}", ex);
124+
}
125+
}
126+
96127
private void JoinFromSteam(string token, bool checkLoadingManager)
97128
{
98129
if (checkLoadingManager && Singleton<LoadingManager>.exists && (Singleton<LoadingManager>.instance.m_currentlyLoading ||
@@ -284,6 +315,14 @@ private static void SteamAPI_ISteamFriends_ActivateGameOverlay(IntPtr instancePt
284315
else SteamAPI_ISteamFriends_ActivateGameOverlay_32(instancePtr, pchDialog);
285316
}
286317

318+
private static void SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage(IntPtr instancePtr, string url, EActivateGameOverlayToWebPageMode mode)
319+
{
320+
if (_use64Bit)
321+
SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage_64(instancePtr, url, mode);
322+
else
323+
SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage_32(instancePtr, url, mode);
324+
}
325+
287326
[DllImport("steam_api", EntryPoint = "SteamAPI_Init", CallingConvention = CallingConvention.Cdecl)]
288327
private static extern bool SteamAPI_Init_32();
289328

@@ -319,6 +358,8 @@ private static void SteamAPI_ISteamFriends_ActivateGameOverlay(IntPtr instancePt
319358

320359
[DllImport("steam_api", EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlay", CallingConvention = CallingConvention.Cdecl)]
321360
private static extern void SteamAPI_ISteamFriends_ActivateGameOverlay_32(IntPtr instancePtr, [MarshalAs(UnmanagedType.LPStr)] string pchDialog);
361+
[DllImport("steam_api", EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage", CallingConvention = CallingConvention.Cdecl)]
362+
private static extern void SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage_32(IntPtr instancePtr, [MarshalAs(UnmanagedType.LPStr)] string url, EActivateGameOverlayToWebPageMode mode);
322363

323364
[DllImport("steam_api64", EntryPoint = "SteamAPI_Init", CallingConvention = CallingConvention.Cdecl)]
324365
private static extern bool SteamAPI_Init_64();
@@ -355,5 +396,7 @@ private static void SteamAPI_ISteamFriends_ActivateGameOverlay(IntPtr instancePt
355396

356397
[DllImport("steam_api64", EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlay", CallingConvention = CallingConvention.Cdecl)]
357398
private static extern void SteamAPI_ISteamFriends_ActivateGameOverlay_64(IntPtr instancePtr, [MarshalAs(UnmanagedType.LPStr)] string pchDialog);
399+
[DllImport("steam_api64", EntryPoint = "SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage", CallingConvention = CallingConvention.Cdecl)]
400+
private static extern void SteamAPI_ISteamFriends_ActivateGameOverlayToWebPage_64(IntPtr instancePtr, [MarshalAs(UnmanagedType.LPStr)] string url, EActivateGameOverlayToWebPageMode mode);
358401
}
359402
}

src/csm/Mods/ModCompat.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using ColossalFramework.UI;
77
using CSM.API;
88
using CSM.Helpers;
9+
using CSM.Panels;
910
using ICities;
1011
using UnityEngine;
1112

@@ -68,6 +69,7 @@ public static bool HasDisableChirperMod {
6869
}
6970

7071
private static bool? _hasDisableChirperMod;
72+
private static bool _tmpeWarningShown;
7173

7274
public static bool NeedsToBePresent(PluginManager.PluginInfo info)
7375
{
@@ -113,6 +115,8 @@ private static IEnumerable<ModSupportStatus> GetModSupport()
113115
yield return new ModSupportStatus("DLC: " + name, name, DLCHelper.GetSupport(dlc), false);
114116
}
115117

118+
bool hasTmpeSyncMod = TmpeSupportHelper.HasTmpeSyncMod();
119+
116120
foreach (PluginManager.PluginInfo info in Singleton<PluginManager>.instance.GetPluginsInfo())
117121
{
118122
// Skip disabled mods
@@ -140,6 +144,13 @@ private static IEnumerable<ModSupportStatus> GetModSupport()
140144

141145
bool isClientSide = _clientSideMods.Contains(modInstanceName);
142146

147+
if (TmpeSupportHelper.IsTmpeMod(modInstanceName))
148+
{
149+
ModSupportType tmpeState = hasTmpeSyncMod ? ModSupportType.Supported : ModSupportType.Unsupported;
150+
yield return new ModSupportStatus(modInstance?.Name, modInstanceName, tmpeState, isClientSide);
151+
continue;
152+
}
153+
143154
// Explicitly supported mods
144155
if (_disableChirperNames.Contains(modInstanceName))
145156
{
@@ -173,6 +184,12 @@ private static IEnumerable<ModSupportStatus> GetModSupport()
173184
}
174185
}
175186

187+
private static bool NeedsTmpeSyncWarning(IEnumerable<ModSupportStatus> modSupport)
188+
{
189+
return modSupport.Any(status =>
190+
status.Type == ModSupportType.Unsupported && TmpeSupportHelper.IsTmpeMod(status.TypeName));
191+
}
192+
176193
public static void BuildModInfo(UIPanel panel)
177194
{
178195
UIScrollablePanel modInfoPanel = panel.Find<UIScrollablePanel>("modInfoPanel");
@@ -182,6 +199,13 @@ public static void BuildModInfo(UIPanel panel)
182199
}
183200

184201
List<ModSupportStatus> modSupport = GetModSupport().ToList();
202+
if (!_tmpeWarningShown && NeedsTmpeSyncWarning(modSupport))
203+
{
204+
MessagePanel warningPanel = PanelManager.ShowPanel<MessagePanel>();
205+
warningPanel?.DisplayTmpeSyncRequirement();
206+
_tmpeWarningShown = true;
207+
}
208+
185209
if (!modSupport.Any())
186210
{
187211
panel.width = 360;
@@ -265,10 +289,14 @@ public static void Init()
265289
Singleton<PluginManager>.instance.eventPluginsChanged += () =>
266290
{
267291
_hasDisableChirperMod = null;
292+
_tmpeWarningShown = false;
293+
TmpeSupportHelper.InvalidateCache();
268294
};
269295
Singleton<PluginManager>.instance.eventPluginsStateChanged += () =>
270296
{
271297
_hasDisableChirperMod = null;
298+
_tmpeWarningShown = false;
299+
TmpeSupportHelper.InvalidateCache();
272300
};
273301
}
274302
}

src/csm/Mods/TmpeSupportHelper.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Linq;
3+
using ColossalFramework;
4+
using ColossalFramework.Plugins;
5+
using ICities;
6+
7+
namespace CSM.Mods
8+
{
9+
internal static class TmpeSupportHelper
10+
{
11+
public const string TmpeSyncWorkshopLink = "https://steamcommunity.com/sharedfiles/filedetails/?id=3600743038";
12+
13+
private static readonly string[] TmpeTypeNames =
14+
{
15+
"TrafficManager.Lifecycle.TrafficManagerMod"
16+
};
17+
18+
private static readonly string[] TmpeSyncTypeNames =
19+
{
20+
"CSM.TmpeSync.Mod",
21+
"CSM.TmpeSync.TmpeSyncMod"
22+
};
23+
24+
private static bool? _hasTmpeSync;
25+
26+
public static bool IsTmpeMod(string typeName)
27+
{
28+
return !string.IsNullOrEmpty(typeName) && TmpeTypeNames.Contains(typeName);
29+
}
30+
31+
public static bool HasTmpeSyncMod()
32+
{
33+
if (_hasTmpeSync.HasValue)
34+
{
35+
return _hasTmpeSync.Value;
36+
}
37+
38+
foreach (PluginManager.PluginInfo info in Singleton<PluginManager>.instance.GetPluginsInfo())
39+
{
40+
if (!info.isEnabled)
41+
{
42+
continue;
43+
}
44+
45+
IUserMod modInstance = info.userModInstance as IUserMod;
46+
string typeName = modInstance?.GetType().ToString();
47+
if (IsTmpeSyncMod(typeName))
48+
{
49+
_hasTmpeSync = true;
50+
return true;
51+
}
52+
}
53+
54+
_hasTmpeSync = false;
55+
return false;
56+
}
57+
58+
public static void InvalidateCache()
59+
{
60+
_hasTmpeSync = null;
61+
}
62+
63+
private static bool IsTmpeSyncMod(string typeName)
64+
{
65+
if (string.IsNullOrEmpty(typeName))
66+
{
67+
return false;
68+
}
69+
70+
if (TmpeSyncTypeNames.Contains(typeName))
71+
{
72+
return true;
73+
}
74+
75+
return typeName.StartsWith("CSM.TmpeSync", StringComparison.Ordinal);
76+
}
77+
}
78+
}

src/csm/Panels/MessagePanel.cs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using ColossalFramework.UI;
77
using CSM.API;
88
using CSM.Helpers;
9+
using CSM.Mods;
910
using UnityEngine;
1011

1112
namespace CSM.Panels
@@ -17,8 +18,10 @@ public class MessagePanel : UIPanel
1718
private string _title;
1819
private string _message;
1920

20-
private UIButton _closeButton, _githubButton;
21-
private bool _githubShown = false;
21+
private UIButton _closeButton, _actionButton;
22+
private bool _actionButtonShown;
23+
private string _actionButtonLabel = "Open Link";
24+
private Action _actionButtonHandler;
2225

2326
public override void Start()
2427
{
@@ -49,13 +52,10 @@ public override void Start()
4952

5053
this.AddScrollbar(messagePanel);
5154

52-
// Github button
53-
_githubButton = this.CreateButton("Open GitHub", new Vector2(60, -340));
54-
_githubButton.eventClicked += (c, p) =>
55-
{
56-
Process.Start("https://github.com/CitiesSkylinesMultiplayer/CSM/releases");
57-
};
58-
_githubButton.isVisible = _githubShown;
55+
// Generic action button (e.g. open links)
56+
_actionButton = this.CreateButton(_actionButtonLabel, new Vector2(60, -340));
57+
_actionButton.eventClicked += (c, p) => _actionButtonHandler?.Invoke();
58+
_actionButton.isVisible = _actionButtonShown;
5959

6060
// Close button
6161
_closeButton = this.CreateButton("Close", new Vector2(60, -410));
@@ -83,8 +83,29 @@ private void SetMessage(string message)
8383
if (_messageLabel)
8484
_messageLabel.text = message;
8585

86-
if (_githubButton)
87-
_githubButton.Hide();
86+
HideActionButton();
87+
}
88+
89+
private void HideActionButton()
90+
{
91+
_actionButtonShown = false;
92+
_actionButtonHandler = null;
93+
94+
if (_actionButton)
95+
_actionButton.Hide();
96+
}
97+
98+
private void ShowActionButton(string label, Action handler)
99+
{
100+
_actionButtonLabel = label;
101+
_actionButtonHandler = handler;
102+
_actionButtonShown = true;
103+
104+
if (_actionButton)
105+
{
106+
_actionButton.text = _actionButtonLabel;
107+
_actionButton.Show();
108+
}
88109
}
89110

90111
public void DisplayInvalidApiServer()
@@ -203,9 +224,7 @@ public void DisplayUpdateAvailable(Version current, Version latest)
203224
"CSM/releases";
204225
SetMessage(message);
205226

206-
_githubShown = true;
207-
if (_githubButton)
208-
_githubButton.Show();
227+
ShowActionButton("Open GitHub", () => Process.Start("https://github.com/CitiesSkylinesMultiplayer/CSM/releases"));
209228

210229
Show(true);
211230
}
@@ -221,6 +240,25 @@ public void DisplayNoUpdateAvailable()
221240
Show(true);
222241
}
223242

243+
public void DisplayTmpeSyncRequirement()
244+
{
245+
SetTitle("TM:PE support requires an addon");
246+
247+
string message = "Traffic Manager: President Edition is currently\n" +
248+
"marked as unsupported in multiplayer because the\n" +
249+
"required CSM.TmpeSync addon is not enabled.\n\n" +
250+
"If you want to play with supported TM:PE, please\n" +
251+
"subscribe to the \"CSM.TmpeSync\" addon from the\n" +
252+
"Steam Workshop and enable it ingame.\n\n" +
253+
"Use the button below to open the workshop page\n" +
254+
"in the Steam overlay.";
255+
SetMessage(message);
256+
257+
ShowActionButton("Subscribe", () => SteamHelpers.OpenOverlayToUrl(TmpeSupportHelper.TmpeSyncWorkshopLink));
258+
259+
Show(true);
260+
}
261+
224262
public void DisplayTroubleshooting(bool isHost, int port=4230, bool hasVpn=false)
225263
{
226264
SetTitle("Troubleshooting");

0 commit comments

Comments
 (0)