diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4cfc74df..06c0d8d6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,7 +23,7 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
# The version string
- name: Set Version Environment Variable
@@ -40,7 +40,7 @@ jobs:
- name: Download Assemblies
uses: carlosperate/download-file-action@v2
with:
- file-url: https://gridentertainment.blob.core.windows.net/general-storage/csm/Assemblies.zip
+ file-url: https://storage.citiesskylinesmultiplayer.com/Assemblies.zip
# New filename to rename the downloaded file
file-name: Assemblies.zip
@@ -52,20 +52,17 @@ jobs:
- name: Build Mod
run: powershell.exe -NoP -NonI -Command "../scripts/build.ps1 -Build"
working-directory: src
+
+ # Copy output to common folder
+ - name: Copy DLLs
+ run: powershell.exe -NoP -NonI -Command "New-Item -ItemType directory -Path 'output'; Copy-Item './src/csm/bin/Release/*.dll','scripts/install.ps1' 'output'"
# Publish artifacts
- name: Upload mod DLLs
- uses: actions/upload-artifact@v3
- with:
- name: CSM ${{ steps.csm_version.outputs.version }}
- path: src/csm/bin/Release/*.dll
-
- # Publish install script
- - name: Upload install script
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: CSM ${{ steps.csm_version.outputs.version }}
- path: scripts/install.ps1
+ path: 'output/*'
# Pack API package (Nuget)
- name: Build nuget API package
@@ -84,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Build Docker Image
working-directory: src/gs
@@ -102,7 +99,7 @@ jobs:
if: github.ref == 'refs/heads/master'
- name: Upload Docker Image
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: API Server Docker Image
path: apiserver.tar
diff --git a/src/basegame/CSM.BaseGame.csproj b/src/basegame/CSM.BaseGame.csproj
index bc4f165a..e12646e3 100644
--- a/src/basegame/CSM.BaseGame.csproj
+++ b/src/basegame/CSM.BaseGame.csproj
@@ -74,10 +74,12 @@
+
+
@@ -105,6 +107,7 @@
+
@@ -156,10 +159,12 @@
+
+
@@ -187,6 +192,7 @@
+
diff --git a/src/basegame/Commands/Data/Buildings/BuildingSetIndustrialVariationCommand.cs b/src/basegame/Commands/Data/Buildings/BuildingSetIndustrialVariationCommand.cs
new file mode 100644
index 00000000..b5eb3e44
--- /dev/null
+++ b/src/basegame/Commands/Data/Buildings/BuildingSetIndustrialVariationCommand.cs
@@ -0,0 +1,32 @@
+using CSM.API.Commands;
+using ProtoBuf;
+
+namespace CSM.BaseGame.Commands.Data.Buildings
+{
+ ///
+ /// Called when the industry building variation was changed.
+ ///
+ /// Sent by:
+ /// - BuildingHandler
+ [ProtoContract]
+ public class BuildingSetIndustrialVariationCommand : CommandBase
+ {
+ ///
+ /// The id of the modified building.
+ ///
+ [ProtoMember(1)]
+ public ushort Building { get; set; }
+
+ ///
+ /// The new building info.
+ ///
+ [ProtoMember(2)]
+ public ushort VariationInfoIndex { get; set; }
+
+ ///
+ /// The selected variation index.
+ ///
+ [ProtoMember(3)]
+ public int VariationIndex { get; set; }
+ }
+}
diff --git a/src/basegame/Commands/Data/Buildings/BuildingUpdateIndustryLastIndexCommand.cs b/src/basegame/Commands/Data/Buildings/BuildingUpdateIndustryLastIndexCommand.cs
new file mode 100644
index 00000000..52d6c127
--- /dev/null
+++ b/src/basegame/Commands/Data/Buildings/BuildingUpdateIndustryLastIndexCommand.cs
@@ -0,0 +1,26 @@
+using CSM.API.Commands;
+using ProtoBuf;
+
+namespace CSM.BaseGame.Commands.Data.Buildings
+{
+ ///
+ /// Called when the last index cache for randomizing the industry variant is updated.
+ ///
+ /// Sent by:
+ /// - BuildingHandler
+ [ProtoContract]
+ public class BuildingUpdateIndustryLastIndexCommand : CommandBase
+ {
+ ///
+ /// The key of the changed index.
+ ///
+ [ProtoMember(1)]
+ public uint Key { get; set; }
+
+ ///
+ /// The new index value.
+ ///
+ [ProtoMember(2)]
+ public int Value { get; set; }
+ }
+}
diff --git a/src/basegame/Commands/Data/Events/EventSetConcertTicketPriceCommand.cs b/src/basegame/Commands/Data/Events/EventSetConcertTicketPriceCommand.cs
new file mode 100644
index 00000000..7d8abd45
--- /dev/null
+++ b/src/basegame/Commands/Data/Events/EventSetConcertTicketPriceCommand.cs
@@ -0,0 +1,31 @@
+using CSM.API.Commands;
+using ProtoBuf;
+
+namespace CSM.BaseGame.Commands.Data.Events
+{
+ ///
+ /// Called when the ticket price of a concert was changed.
+ ///
+ /// Sent by:
+ /// - EventHandler
+ [ProtoContract]
+ public class EventSetConcertTicketPriceCommand : CommandBase
+ {
+ ///
+ /// The building id of the related panel.
+ ///
+ [ProtoMember(1)]
+ public ushort Building { get; set; }
+ ///
+ /// The event info id of the concert that was modified.
+ ///
+ [ProtoMember(2)]
+ public uint Event { get; set; }
+
+ ///
+ /// The new ticket price.
+ ///
+ [ProtoMember(3)]
+ public int Price { get; set; }
+ }
+}
diff --git a/src/basegame/Commands/Handler/Buildings/BuildingSetIndustrialVariationHandler.cs b/src/basegame/Commands/Handler/Buildings/BuildingSetIndustrialVariationHandler.cs
new file mode 100644
index 00000000..485d7500
--- /dev/null
+++ b/src/basegame/Commands/Handler/Buildings/BuildingSetIndustrialVariationHandler.cs
@@ -0,0 +1,33 @@
+using ColossalFramework;
+using CSM.API.Commands;
+using CSM.API.Helpers;
+using CSM.BaseGame.Commands.Data.Buildings;
+using CSM.BaseGame.Helpers;
+
+namespace CSM.BaseGame.Commands.Handler.Buildings
+{
+ public class BuildingSetIndustrialVariationHandler : CommandHandler
+ {
+ protected override void Handle(BuildingSetIndustrialVariationCommand command)
+ {
+ IgnoreHelper.Instance.StartIgnore();
+
+ BuildingInfo newPrefab = PrefabCollection.GetPrefab(command.VariationInfoIndex);
+ Singleton.instance.UpdateBuildingInfo(command.Building, newPrefab);
+
+ ((IndustryBuildingAI) Singleton.instance.m_buildings.m_buffer[command.Building].Info.m_buildingAI).SetLastVariationIndex(command.VariationIndex);
+
+ // Refresh panel
+ if (InfoPanelHelper.IsBuilding(typeof(CityServiceWorldInfoPanel), command.Building,
+ out WorldInfoPanel panel))
+ {
+ SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() =>
+ {
+ ReflectionHelper.Call((CityServiceWorldInfoPanel)panel, "OnSetTarget");
+ });
+ }
+
+ IgnoreHelper.Instance.EndIgnore();
+ }
+ }
+}
diff --git a/src/basegame/Commands/Handler/Buildings/BuildingUpdateIndustryLastIndexHandler.cs b/src/basegame/Commands/Handler/Buildings/BuildingUpdateIndustryLastIndexHandler.cs
new file mode 100644
index 00000000..b3f5a61b
--- /dev/null
+++ b/src/basegame/Commands/Handler/Buildings/BuildingUpdateIndustryLastIndexHandler.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using CSM.API.Commands;
+using CSM.API.Helpers;
+using CSM.BaseGame.Commands.Data.Buildings;
+
+namespace CSM.BaseGame.Commands.Handler.Buildings
+{
+ public class BuildingUpdateIndustryLastIndexHandler : CommandHandler
+ {
+ protected override void Handle(BuildingUpdateIndustryLastIndexCommand command)
+ {
+ Dictionary lastTableIndex = ReflectionHelper.GetAttr>(typeof(IndustryBuildingAI), "m_lastTableIndex");
+ lastTableIndex[command.Key] = command.Value;
+ }
+ }
+}
diff --git a/src/basegame/Commands/Handler/Districts/DistrictChangeStyleHandler.cs b/src/basegame/Commands/Handler/Districts/DistrictChangeStyleHandler.cs
index 501999dd..8a617676 100644
--- a/src/basegame/Commands/Handler/Districts/DistrictChangeStyleHandler.cs
+++ b/src/basegame/Commands/Handler/Districts/DistrictChangeStyleHandler.cs
@@ -1,4 +1,5 @@
-using ColossalFramework;
+using System;
+using ColossalFramework;
using ColossalFramework.UI;
using CSM.API.Commands;
using CSM.API.Helpers;
@@ -13,11 +14,14 @@ protected override void Handle(DistrictChangeStyleCommand command)
{
Singleton.instance.m_districts.m_buffer[command.DistrictId].m_Style = command.Style;
- if (InfoPanelHelper.IsDistrict(typeof(DistrictWorldInfoPanel), command.DistrictId, out WorldInfoPanel panel))
+ if (InfoPanelHelper.IsDistrict(typeof(DistrictWorldInfoPanel), command.DistrictId, out WorldInfoPanel pan))
{
SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() =>
{
- ReflectionHelper.GetAttr((DistrictWorldInfoPanel)panel, "m_Style").selectedIndex = command.Style;
+ DistrictWorldInfoPanel panel = (DistrictWorldInfoPanel) pan;
+ int[] styleMap = ReflectionHelper.GetAttr(panel, "m_StyleMap");
+ int num = Array.IndexOf(styleMap, command.Style);
+ ReflectionHelper.GetAttr(panel, "m_Style").selectedIndex = num;
});
}
}
diff --git a/src/basegame/Commands/Handler/Events/EventColorChangedHandler.cs b/src/basegame/Commands/Handler/Events/EventColorChangedHandler.cs
index f3eb40c9..f7410b97 100644
--- a/src/basegame/Commands/Handler/Events/EventColorChangedHandler.cs
+++ b/src/basegame/Commands/Handler/Events/EventColorChangedHandler.cs
@@ -40,7 +40,7 @@ protected override void Handle(EventColorChangedCommand command)
});
}
}
- else if (aiType == typeof(SportMatchAI))
+ else if (aiType == typeof(SportMatchAI) || aiType == typeof(VarsitySportsMatchAI))
{
if (InfoPanelHelper.IsEventBuilding(typeof(FootballPanel), command.Event, out WorldInfoPanel panel))
{
diff --git a/src/basegame/Commands/Handler/Events/EventSetConcertTicketPriceHandler.cs b/src/basegame/Commands/Handler/Events/EventSetConcertTicketPriceHandler.cs
new file mode 100644
index 00000000..56a2a8d0
--- /dev/null
+++ b/src/basegame/Commands/Handler/Events/EventSetConcertTicketPriceHandler.cs
@@ -0,0 +1,30 @@
+using CSM.API;
+using CSM.API.Commands;
+using CSM.API.Helpers;
+using CSM.BaseGame.Commands.Data.Events;
+using CSM.BaseGame.Helpers;
+
+namespace CSM.BaseGame.Commands.Handler.Events
+{
+ public class EventSetConcertTicketPriceHandler : CommandHandler
+ {
+ protected override void Handle(EventSetConcertTicketPriceCommand command)
+ {
+ IgnoreHelper.Instance.StartIgnore();
+
+ ConcertAI concertAI = PrefabCollection.GetLoaded(command.Event).GetAI() as ConcertAI;
+ EventData data = new EventData();
+ if (concertAI != null) concertAI.SetTicketPrice(0, ref data, command.Price);
+
+ IgnoreHelper.Instance.EndIgnore();
+
+ if (InfoPanelHelper.IsBuilding(typeof(FestivalPanel), command.Building, out WorldInfoPanel panel))
+ {
+ SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() =>
+ {
+ ReflectionHelper.Call((FestivalPanel)panel, "RefreshTicketPrices");
+ });
+ }
+ }
+ }
+}
diff --git a/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs b/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs
index 2490e4ad..d969907b 100644
--- a/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs
+++ b/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs
@@ -17,10 +17,9 @@ protected override void Handle(EventSetSecurityBudgetCommand command)
if (InfoPanelHelper.IsEventBuilding(typeof(FestivalPanel), command.Event, out WorldInfoPanel panel))
{
- UISlider slider = ReflectionHelper.GetAttr(panel, "m_securityBudgetSlider");
SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() =>
{
- slider.value = command.Budget;
+ ReflectionHelper.Call(panel, "OnSetTarget");
});
}
diff --git a/src/basegame/Injections/BuildingHandler.cs b/src/basegame/Injections/BuildingHandler.cs
index e8637275..e3fa3ddb 100644
--- a/src/basegame/Injections/BuildingHandler.cs
+++ b/src/basegame/Injections/BuildingHandler.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using CSM.API;
using CSM.API.Commands;
using CSM.API.Helpers;
using CSM.BaseGame.Commands.Data.Buildings;
@@ -32,6 +33,13 @@ public static void Prefix(out CallState __state, object __instance)
return;
}
+ // Extracting facility AI generates random building, we don't sync it here
+ if (tool.m_prefab.m_buildingAI is ExtractingFacilityAI)
+ {
+ __state.run = false;
+ return;
+ }
+
__state.run = true;
__state.relocate = tool.m_relocate; // Save relocate state as it will be cleared at the end of the method
@@ -447,4 +455,97 @@ public static void Prefix(ushort buildingID, Building.Flags2 variation)
});
}
}
+
+ [HarmonyPatch(typeof(CityServiceWorldInfoPanel))]
+ [HarmonyPatch("OnVariationDropdownChanged")]
+ public class OnVariationDropdownChanged
+ {
+ public static void Prefix(int value, InstanceID ___m_InstanceID)
+ {
+ UpdateBuildingInfo.TrackNextBuilding = ___m_InstanceID.Building;
+ UpdateBuildingInfo.VariationIndex = value;
+ }
+ }
+
+ [HarmonyPatch(typeof(BuildingManager))]
+ [HarmonyPatch("UpdateBuildingInfo")]
+ [HarmonyPatch(new[] { typeof(ushort), typeof(BuildingInfo) })]
+ public class UpdateBuildingInfo
+ {
+ public static ushort TrackNextBuilding = 0;
+ public static int VariationIndex = 0;
+
+ public static void Prefix(ushort building, BuildingInfo newInfo, out bool __state)
+ {
+ if (IgnoreHelper.Instance.IsIgnored())
+ {
+ __state = false;
+ }
+ else if (building == TrackNextBuilding)
+ {
+ ushort prefabId = (ushort)Mathf.Clamp(newInfo.m_prefabDataIndex, 0, 65535);
+ Command.SendToAll(new BuildingSetIndustrialVariationCommand
+ {
+ Building = building,
+ VariationInfoIndex = prefabId,
+ VariationIndex = VariationIndex,
+ });
+
+ TrackNextBuilding = 0;
+
+ IgnoreHelper.Instance.StartIgnore();
+ __state = true;
+ }
+ else
+ {
+ __state = false;
+ }
+ }
+
+ public static void Postfix(ref bool __state)
+ {
+ if (__state)
+ {
+ IgnoreHelper.Instance.EndIgnore();
+ }
+ }
+ }
+
+ [HarmonyPatch(typeof(IndustryBuildingAI))]
+ [HarmonyPatch("CreateBuilding")]
+ public class IndustryCreateBuilding
+ {
+ public static void Prefix(IndustryBuildingAI __instance, out int __state)
+ {
+ if (__instance.m_info.m_placementStyle != ItemClass.Placement.Manual)
+ {
+ __state = -2;
+ return;
+ }
+
+ uint searchKey = ReflectionHelper.GetProp(__instance, "SearchKey");
+ Dictionary lastTableIndex = ReflectionHelper.GetAttr>(typeof(IndustryBuildingAI), "m_lastTableIndex");
+ __state = lastTableIndex[searchKey];
+ }
+
+ public static void Postfix(IndustryBuildingAI __instance, ref int __state)
+ {
+ if (__state == -2)
+ {
+ return;
+ }
+
+ uint searchKey = ReflectionHelper.GetProp(__instance, "SearchKey");
+ Dictionary lastTableIndex = ReflectionHelper.GetAttr>(typeof(IndustryBuildingAI), "m_lastTableIndex");
+ int newIndex = lastTableIndex[searchKey];
+ if (newIndex != __state)
+ {
+ Command.SendToAll(new BuildingUpdateIndustryLastIndexCommand()
+ {
+ Key = searchKey,
+ Value = newIndex
+ });
+ }
+ }
+ }
}
diff --git a/src/basegame/Injections/DistrictHandler.cs b/src/basegame/Injections/DistrictHandler.cs
index 9ec52d2e..47d48676 100644
--- a/src/basegame/Injections/DistrictHandler.cs
+++ b/src/basegame/Injections/DistrictHandler.cs
@@ -216,15 +216,17 @@ public static void Prefix(ref byte park)
[HarmonyPatch("OnStyleChanged")]
public class StyleChanged
{
- public static void Prefix(DistrictWorldInfoPanel __instance, int value)
+ public static void Prefix(DistrictWorldInfoPanel __instance, int[] ___m_StyleMap, int value)
{
byte district = ReflectionHelper.GetAttr(__instance, "m_InstanceID").District;
+
+ ushort newStyle = (ushort) ___m_StyleMap[value];
ushort oldStyle = Singleton.instance.m_districts.m_buffer[district].m_Style;
- if (oldStyle != value)
+ if (oldStyle != newStyle)
{
Command.SendToAll(new DistrictChangeStyleCommand
{
- Style = (ushort) value,
+ Style = newStyle,
DistrictId = district
});
}
diff --git a/src/basegame/Injections/EventHandler.cs b/src/basegame/Injections/EventHandler.cs
index 485a1563..4ffd0586 100644
--- a/src/basegame/Injections/EventHandler.cs
+++ b/src/basegame/Injections/EventHandler.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Reflection;
+using ColossalFramework.UI;
using CSM.API;
using CSM.API.Commands;
using CSM.API.Helpers;
@@ -34,7 +37,7 @@ public static void Prefix(ushort eventID, ref EventData data, Color32 newColor,
return;
Type type = __instance.GetType();
- if (type != typeof(RocketLaunchAI) && type != typeof(ConcertAI) && type != typeof(SportMatchAI))
+ if (type != typeof(RocketLaunchAI) && type != typeof(ConcertAI) && type != typeof(SportMatchAI) && type != typeof(VarsitySportsMatchAI))
return;
if (newColor.r == data.m_color.r && newColor.g == data.m_color.g && newColor.b == data.m_color.b)
@@ -78,7 +81,6 @@ public static void Prefix(ushort eventID, ref EventData data, int newPrice, Even
return;
// Event is 0, when it is set through the FestivalPanel for a certain band
- // TODO: Sync price changes in FestivalPanel and VarsitySportsArenaPanel
if (eventID == 0 || __instance.GetTicketPrice(eventID, ref data) == newPrice)
return;
@@ -90,8 +92,7 @@ public static void Prefix(ushort eventID, ref EventData data, int newPrice, Even
}
}
- [HarmonyPatch(typeof(SportMatchAI))]
- [HarmonyPatch("BeginEvent")]
+ [HarmonyPatch]
public class BeginEvent
{
private static bool _isWinMatchSet;
@@ -142,5 +143,32 @@ public static void Postfix(ushort eventID, ref EventData data)
});
}
}
+
+ public static IEnumerable TargetMethods()
+ {
+ yield return typeof(SportMatchAI).GetMethod("BeginEvent", ReflectionHelper.AllAccessFlags);
+ yield return typeof(ConcertAI).GetMethod("BeginEvent", ReflectionHelper.AllAccessFlags);
+ }
+ }
+
+ [HarmonyPatch(typeof(FestivalPanel))]
+ [HarmonyPatch("OnTicketPriceChanged")]
+ public class SetConcertTicketPrice
+ {
+ public static void Prefix(UIComponent comp, int value, InstanceID ___m_InstanceID)
+ {
+ ConcertAI concertAI = PrefabCollection.GetLoaded((uint) comp.objectUserData).GetAI() as ConcertAI;
+ int newPrice = value * 100;
+ int num = newPrice - concertAI.m_ticketPrice;
+ if (num == concertAI.m_info.m_ticketPriceOffset)
+ return;
+
+ Command.SendToAll(new EventSetConcertTicketPriceCommand()
+ {
+ Building = ___m_InstanceID.Building,
+ Event = (uint) comp.objectUserData,
+ Price = newPrice
+ });
+ }
}
}
diff --git a/src/basegame/Injections/Tools/ToolSimulatorCursorManager.cs b/src/basegame/Injections/Tools/ToolSimulatorCursorManager.cs
index 56e3c91a..b32341d0 100644
--- a/src/basegame/Injections/Tools/ToolSimulatorCursorManager.cs
+++ b/src/basegame/Injections/Tools/ToolSimulatorCursorManager.cs
@@ -19,7 +19,11 @@ public PlayerCursorManager GetCursorView(int sender) {
return newView;
}
- public void RemoveCursorView(int sender) {
+ public void RemoveCursorView(int sender)
+ {
+ if (!_playerCursorViews.ContainsKey(sender))
+ return;
+
Destroy(_playerCursorViews[sender]);
_playerCursorViews.Remove(sender);
}
@@ -39,7 +43,7 @@ public Vector3 DoRaycast(Ray mouseRay, float mouseRayLenght)
{
if (_rayCast == null)
{
- _rayCast = typeof(ToolBase).GetMethod("RayCast", BindingFlags.Static | BindingFlags.NonPublic);
+ _rayCast = typeof(ToolBase).GetMethod("RayCast", BindingFlags.Static | BindingFlags.NonPublic);
}
ToolBase.RaycastInput input = new ToolBase.RaycastInput(mouseRay, mouseRayLenght);
object[] parameters = { input, null };
diff --git a/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs b/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs
index 3e8f1e3d..98fae33f 100644
--- a/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs
+++ b/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs
@@ -126,7 +126,9 @@ public void HandleOnServer(ConnectionRequestCommand command, NetPeer peer)
List serverMods = ModSupport.Instance.RequiredModsForSync.OrderBy(x => x).ToList();
List clientMods = (command.Mods ?? new List()).OrderBy(x => x).ToList();
- if (!clientMods.SequenceEqual(serverMods) && !CSM.Settings.SkipModCompatibilityChecks.value == false)
+ Log.Info($"Client mods [{string.Join(", ", clientMods.ToArray())}]");
+ Log.Info($"Server mods [{string.Join(", ", serverMods.ToArray())}]");
+ if (!clientMods.SequenceEqual(serverMods) && !CSM.Settings.SkipModCompatibilityChecks.value)
{
Log.Info($"Connection rejected: List of mods [{string.Join(", ", clientMods.ToArray())}] (client) and [{string.Join(", ", serverMods.ToArray())}] (server) differ.");
CommandInternal.Instance.SendToClient(peer, new ConnectionResultCommand
diff --git a/src/csm/Helpers/DLCHelper.cs b/src/csm/Helpers/DLCHelper.cs
index f0179c81..9c1d404e 100644
--- a/src/csm/Helpers/DLCHelper.cs
+++ b/src/csm/Helpers/DLCHelper.cs
@@ -56,17 +56,17 @@ public static ModSupportType GetSupport(SteamHelper.DLC dlc)
case SteamHelper.DLC.CampusDLC:
return ModSupportType.Supported;
case SteamHelper.DLC.MusicFestival: // Concerts
- return ModSupportType.Unknown;
+ return ModSupportType.Supported;
case SteamHelper.DLC.FinancialDistrictsDLC:
return ModSupportType.Unknown;
case SteamHelper.DLC.GreenCitiesDLC:
- return ModSupportType.Unknown;
+ return ModSupportType.Supported;
case SteamHelper.DLC.HotelDLC:
return ModSupportType.Unknown;
case SteamHelper.DLC.IndustryDLC:
- return ModSupportType.Unknown;
+ return ModSupportType.Supported;
case SteamHelper.DLC.InMotionDLC: // Mass transit
- return ModSupportType.Unknown;
+ return ModSupportType.Supported;
case SteamHelper.DLC.Football: // Match day
return ModSupportType.Supported;
case SteamHelper.DLC.NaturalDisastersDLC:
@@ -104,6 +104,10 @@ public static ModSupportType GetSupport(SteamHelper.DLC dlc)
case SteamHelper.DLC.ModderPack19:
case SteamHelper.DLC.ModderPack20:
case SteamHelper.DLC.ModderPack21:
+ case SteamHelper.DLC.ModderPack22:
+ case SteamHelper.DLC.ModderPack23:
+ case SteamHelper.DLC.ModderPack24:
+ case SteamHelper.DLC.ModderPack25:
case SteamHelper.DLC.OrientalBuildings:
return ModSupportType.Supported;
default:
diff --git a/src/csm/Mods/ModCompat.cs b/src/csm/Mods/ModCompat.cs
index 856db859..e1a3dd58 100644
--- a/src/csm/Mods/ModCompat.cs
+++ b/src/csm/Mods/ModCompat.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using ColossalFramework;
@@ -36,10 +37,14 @@ public ModSupportStatus(string name, string typeName, ModSupportType type, bool
public static class ModCompat
{
- private static readonly string[] _clientSideMods = { "LoadingScreenMod.Mod", "MyFirstMod.DestroyChirperMod", "RemoveChirper.RemoveChirper", "ChirpRemover.ChirpRemover", "MoreAspectRatios.MoreAspectRatios" };
+ private static readonly string[] _clientSideMods =
+ {
+ "LoadingScreenMod.Mod", "MyFirstMod.DestroyChirperMod", "RemoveChirper.RemoveChirper",
+ "ChirpRemover.ChirpRemover", "MoreAspectRatios.MoreAspectRatios", "FPSCamera.Mod", "AchieveIt.ModInfo"
+ };
private static readonly string[] _ignoredMods = { "CitiesHarmony.Mod" };
- private static readonly string[] _knownToWork = { "LoadingScreenMod.Mod", "MoreAspectRatios.MoreAspectRatios" };
+ private static readonly string[] _knownToWork = { "NetworkExtensions.Mod", "BigCity.BigCityUserMod" };
private static readonly string[] _unsupportedMods = { "TrafficManager.Lifecycle.TrafficManagerMod" };
private static readonly string[] _disableChirperNames = { "MyFirstMod.DestroyChirperMod", "RemoveChirper.RemoveChirper", "ChirpRemover.ChirpRemover" };
@@ -64,6 +69,37 @@ public static bool HasDisableChirperMod {
private static bool? _hasDisableChirperMod;
+ public static bool NeedsToBePresent(PluginManager.PluginInfo info)
+ {
+ if (!info.isEnabled)
+ return false;
+
+ IUserMod modInstance = info.userModInstance as IUserMod;
+
+ // Skip CSM itself
+ if (modInstance?.GetType() == typeof(CSM))
+ return false;
+
+ if (info.isBuiltin)
+ return true;
+
+ string modInstanceName = modInstance?.GetType().ToString();
+
+ if (_ignoredMods.Contains(modInstanceName))
+ return false;
+
+ if (_clientSideMods.Contains(modInstanceName))
+ return false;
+
+ if (_knownToWork.Contains(modInstanceName))
+ return true;
+
+ if (ModSupport.Instance.ConnectedMods.Select(mod => mod.ModClass).Contains(modInstance?.GetType()))
+ return true;
+
+ return false;
+ }
+
private static IEnumerable GetModSupport()
{
foreach (SteamHelper.DLC dlc in DLCHelper.GetOwnedExpansions().DLCs())
@@ -89,11 +125,14 @@ private static IEnumerable GetModSupport()
if (modInstance?.GetType() == typeof(CSM))
continue;
- // Skip built-in mods (TODO: Check if actually everything works with them)
+ string modInstanceName = modInstance?.GetType().ToString();
+
+ // Built-in mods are supported (TODO: Check if actually everything works with them)
if (info.isBuiltin)
+ {
+ yield return new ModSupportStatus(modInstance?.Name, modInstanceName, ModSupportType.Supported, false);
continue;
-
- string modInstanceName = modInstance?.GetType().ToString();
+ }
// Skip ignored mods
if (_ignoredMods.Contains(modInstanceName))
@@ -130,7 +169,7 @@ private static IEnumerable GetModSupport()
continue;
}
- yield return new ModSupportStatus(modInstance?.Name, modInstanceName, ModSupportType.Unknown, isClientSide);
+ yield return new ModSupportStatus(modInstance?.Name, modInstanceName, isClientSide ? ModSupportType.KnownWorking : ModSupportType.Unknown, isClientSide);
}
}
@@ -142,7 +181,7 @@ public static void BuildModInfo(UIPanel panel)
modInfoPanel.Remove();
}
- IEnumerable modSupport = GetModSupport().ToList();
+ List modSupport = GetModSupport().ToList();
if (!modSupport.Any())
{
panel.width = 360;
@@ -162,6 +201,19 @@ public static void BuildModInfo(UIPanel panel)
modInfoPanel.CreateLabel("Mod/DLC Support", new Vector2(0, 0), 340, 20);
Log.Debug($"Mod support: {string.Join(", ", modSupport.Select(m => $"{m.TypeName} ({m.Type})").ToArray())}");
+ modSupport.Sort((status1, status2) =>
+ {
+ if (status1.Name.StartsWith("DLC") != status2.Name.StartsWith("DLC"))
+ {
+ return status1.Name.StartsWith("DLC") ? -1 : 1;
+ }
+ if (status1.ClientSide != status2.ClientSide)
+ {
+ return status1.ClientSide ? 1 : -1;
+ }
+
+ return string.Compare(status1.Name, status2.Name, StringComparison.Ordinal);
+ });
int y = -50;
foreach (ModSupportStatus mod in modSupport)
{
diff --git a/src/csm/Mods/ModSupport.cs b/src/csm/Mods/ModSupport.cs
index dbf14c6a..e127803b 100644
--- a/src/csm/Mods/ModSupport.cs
+++ b/src/csm/Mods/ModSupport.cs
@@ -22,20 +22,12 @@ public List RequiredModsForSync
{
get
{
- return ConnectedNonClientModNames
- .Concat(Singleton.instance.GetPluginsInfo()
- .Where(plugin => plugin.isEnabled && plugin.isBuiltin).Select(plugin => plugin.name))
- .Concat(AssetNames).ToList();
- }
- }
-
- private IEnumerable ConnectedNonClientModNames
- {
- get
- {
- return ConnectedMods.Where(connection =>
- connection.ModClass != null
- ).Select(connection => connection.Name).ToList();
+ return Singleton.instance.GetPluginsInfo()
+ .Where(ModCompat.NeedsToBePresent).Where(plugin => plugin != null)
+ .Select(plugin => plugin.userModInstance as IUserMod)
+ .Where(mod => mod != null)
+ .Select(mod => mod.Name)
+ .Concat(AssetNames).ToList();
}
}
@@ -114,7 +106,7 @@ public void DestroyConnections()
{
ConnectedMods.Clear();
ConnectedMods.TrimExcess();
-
+
Singleton.instance.eventPluginsChanged -= LoadModConnections;
Singleton.instance.eventPluginsStateChanged -= LoadModConnections;
}
diff --git a/src/csm/Networking/Client.cs b/src/csm/Networking/Client.cs
index 4bfbeb4e..0451d127 100644
--- a/src/csm/Networking/Client.cs
+++ b/src/csm/Networking/Client.cs
@@ -29,7 +29,7 @@ public class Client
{
// The client
private readonly LiteNetLib.NetManager _netClient;
-
+
///
/// Configuration for the client
///
@@ -111,7 +111,7 @@ public bool Connect(ClientConfig clientConfig)
// Set the configuration
Config = clientConfig;
ClientPlayer.Username = Config.Username;
-
+
// Stop the client, if client failed stopping
if (_netClient.IsRunning)
{
@@ -484,7 +484,7 @@ private void ListenerOnNetworkErrorEvent(IPEndPoint endpoint, SocketError socket
{
Log.Error($"Network error: {socketError}");
}
-
+
private void ListenerOnNetworkLatencyUpdateEvent(NetPeer peer, int latency)
{
ClientPlayer.Latency = latency;
diff --git a/src/csm/Networking/Server.cs b/src/csm/Networking/Server.cs
index c14791a5..d7450b36 100644
--- a/src/csm/Networking/Server.cs
+++ b/src/csm/Networking/Server.cs
@@ -33,7 +33,7 @@ public class Server
// Keep alive tick tracker
private int _keepAlive = 1;
-
+
// Connected clients
public Dictionary ConnectedPlayers { get; } = new Dictionary();
@@ -187,6 +187,7 @@ public void StopServer()
_netServer.Stop();
MultiplayerManager.Instance.PlayerList.Clear();
+ ConnectedPlayers.Clear();
TransactionHandler.ClearTransactions();
Singleton.instance.Clear();