From 3000e6dd509de8175ea5acde6ece4677a5275b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Tue, 12 Sep 2023 20:03:45 +0200 Subject: [PATCH 01/11] Update mod and dlc support list, improve mod equality checking --- src/csm/Helpers/DLCHelper.cs | 2 +- src/csm/Mods/ModCompat.cs | 62 ++++++++++++++++++++++++++++++++---- src/csm/Mods/ModSupport.cs | 19 +++-------- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/csm/Helpers/DLCHelper.cs b/src/csm/Helpers/DLCHelper.cs index f0179c81..ac3283e3 100644 --- a/src/csm/Helpers/DLCHelper.cs +++ b/src/csm/Helpers/DLCHelper.cs @@ -66,7 +66,7 @@ public static ModSupportType GetSupport(SteamHelper.DLC dlc) case SteamHelper.DLC.IndustryDLC: return ModSupportType.Unknown; 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: diff --git a/src/csm/Mods/ModCompat.cs b/src/csm/Mods/ModCompat.cs index 856db859..ad949851 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,15 @@ 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.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..f785c689 100644 --- a/src/csm/Mods/ModSupport.cs +++ b/src/csm/Mods/ModSupport.cs @@ -22,20 +22,9 @@ 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).Select(plugin => plugin.name) + .Concat(AssetNames).ToList(); } } @@ -114,7 +103,7 @@ public void DestroyConnections() { ConnectedMods.Clear(); ConnectedMods.TrimExcess(); - + Singleton.instance.eventPluginsChanged -= LoadModConnections; Singleton.instance.eventPluginsStateChanged -= LoadModConnections; } From 471a36e2b256ae937442e78162c1f89a7cfacbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Tue, 12 Sep 2023 20:06:41 +0200 Subject: [PATCH 02/11] Fix connected player list not being cleared when stopping the server --- src/csm/Networking/Server.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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(); From a6b6e8e8e288844e73eec0db955bd2c95f8419a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Sat, 7 Jun 2025 00:15:52 +0200 Subject: [PATCH 03/11] Some more old fixes and new features? --- src/basegame/CSM.BaseGame.csproj | 7 ++ .../BuildingSetIndustrialVariationCommand.cs | 32 ++++++++ .../BuildingUpdateIndustryLastIndexCommand.cs | 26 ++++++ .../EventSetConcertTicketPriceCommand.cs | 31 +++++++ .../BuildingSetIndustrialVariationHandler.cs | 33 ++++++++ .../BuildingUpdateIndustryLastIndexHandler.cs | 16 ++++ .../Events/EventColorChangedHandler.cs | 2 +- .../EventSetConcertTicketPriceHandler.cs | 30 +++++++ .../Events/EventSetSecurityBudgetHandler.cs | 6 +- src/basegame/Injections/BuildingHandler.cs | 82 +++++++++++++++++++ src/basegame/Injections/EventHandler.cs | 36 +++++++- .../Tools/ToolSimulatorCursorManager.cs | 8 +- .../Internal/ConnectionRequestHandler.cs | 4 +- src/csm/Helpers/DLCHelper.cs | 8 +- src/csm/Mods/ModCompat.cs | 4 + src/csm/Mods/ModSupport.cs | 5 +- src/csm/Networking/Client.cs | 6 +- 17 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 src/basegame/Commands/Data/Buildings/BuildingSetIndustrialVariationCommand.cs create mode 100644 src/basegame/Commands/Data/Buildings/BuildingUpdateIndustryLastIndexCommand.cs create mode 100644 src/basegame/Commands/Data/Events/EventSetConcertTicketPriceCommand.cs create mode 100644 src/basegame/Commands/Handler/Buildings/BuildingSetIndustrialVariationHandler.cs create mode 100644 src/basegame/Commands/Handler/Buildings/BuildingUpdateIndustryLastIndexHandler.cs create mode 100644 src/basegame/Commands/Handler/Events/EventSetConcertTicketPriceHandler.cs diff --git a/src/basegame/CSM.BaseGame.csproj b/src/basegame/CSM.BaseGame.csproj index bc4f165a..5013a39c 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 @@ + @@ -239,6 +245,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/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..64c6502e 100644 --- a/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs +++ b/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs @@ -17,11 +17,7 @@ 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"); } IgnoreHelper.Instance.EndIgnore(); diff --git a/src/basegame/Injections/BuildingHandler.cs b/src/basegame/Injections/BuildingHandler.cs index e8637275..2f105724 100644 --- a/src/basegame/Injections/BuildingHandler.cs +++ b/src/basegame/Injections/BuildingHandler.cs @@ -447,4 +447,86 @@ 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) + { + 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) + { + 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/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 ac3283e3..ff0381bb 100644 --- a/src/csm/Helpers/DLCHelper.cs +++ b/src/csm/Helpers/DLCHelper.cs @@ -56,11 +56,11 @@ 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: @@ -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 ad949851..e1a3dd58 100644 --- a/src/csm/Mods/ModCompat.cs +++ b/src/csm/Mods/ModCompat.cs @@ -203,6 +203,10 @@ public static void BuildModInfo(UIPanel panel) 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; diff --git a/src/csm/Mods/ModSupport.cs b/src/csm/Mods/ModSupport.cs index f785c689..e127803b 100644 --- a/src/csm/Mods/ModSupport.cs +++ b/src/csm/Mods/ModSupport.cs @@ -23,7 +23,10 @@ public List RequiredModsForSync get { return Singleton.instance.GetPluginsInfo() - .Where(ModCompat.NeedsToBePresent).Select(plugin => plugin.name) + .Where(ModCompat.NeedsToBePresent).Where(plugin => plugin != null) + .Select(plugin => plugin.userModInstance as IUserMod) + .Where(mod => mod != null) + .Select(mod => mod.Name) .Concat(AssetNames).ToList(); } } 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; From 4273e3b051e0c5d1c9fe668d2aa0b328fdea7223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Mon, 9 Jun 2025 19:00:49 +0200 Subject: [PATCH 04/11] Try to fix changing district styles --- .../Handler/Districts/DistrictChangeStyleHandler.cs | 10 +++++++--- src/basegame/Injections/DistrictHandler.cs | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) 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/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 }); } From 32ca2a82a2168280ceb0833510608efaf2ada672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Mon, 9 Jun 2025 19:01:52 +0200 Subject: [PATCH 05/11] Fix initial industry building variation and finalize industries DLC support --- src/basegame/Injections/BuildingHandler.cs | 19 +++++++++++++++++++ src/csm/Helpers/DLCHelper.cs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/basegame/Injections/BuildingHandler.cs b/src/basegame/Injections/BuildingHandler.cs index 2f105724..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 @@ -509,6 +517,12 @@ 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]; @@ -516,6 +530,11 @@ public static void Prefix(IndustryBuildingAI __instance, out int __state) 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]; diff --git a/src/csm/Helpers/DLCHelper.cs b/src/csm/Helpers/DLCHelper.cs index ff0381bb..9c1d404e 100644 --- a/src/csm/Helpers/DLCHelper.cs +++ b/src/csm/Helpers/DLCHelper.cs @@ -64,7 +64,7 @@ public static ModSupportType GetSupport(SteamHelper.DLC dlc) 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.Supported; case SteamHelper.DLC.Football: // Match day From 4406591b891734e5f1e09a1ec82bd5c4ec744e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Mon, 9 Jun 2025 19:11:04 +0200 Subject: [PATCH 06/11] Remove unused disaster handler --- src/basegame/CSM.BaseGame.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/basegame/CSM.BaseGame.csproj b/src/basegame/CSM.BaseGame.csproj index 5013a39c..e12646e3 100644 --- a/src/basegame/CSM.BaseGame.csproj +++ b/src/basegame/CSM.BaseGame.csproj @@ -245,7 +245,6 @@ - From 19ab91d0a7d9a7a0c905577b4726efefa790e406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Mon, 9 Jun 2025 19:14:49 +0200 Subject: [PATCH 07/11] Update github actions versions --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4cfc74df..4be72752 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 @@ -55,14 +55,14 @@ jobs: # Publish artifacts - name: Upload mod DLLs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 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 @@ -84,7 +84,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 +102,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 From f5a3a220cbd01f88ec538c8677feb645b89c1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Thu, 12 Jun 2025 23:57:37 +0200 Subject: [PATCH 08/11] Update assemblies in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4be72752..6f50f5fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 From 35aac306000a84646a2816465f5a0798e5e1fb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Fri, 13 Jun 2025 00:29:18 +0200 Subject: [PATCH 09/11] Use single step to upload artifacts --- .github/workflows/main.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6f50f5fd..13e0db1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,14 +58,9 @@ jobs: uses: actions/upload-artifact@v4 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@v4 - with: - name: CSM ${{ steps.csm_version.outputs.version }} - path: scripts/install.ps1 + path: | + src/csm/bin/Release/*.dll + scripts/install.ps1 # Pack API package (Nuget) - name: Build nuget API package From ef05610e8882445ead830ed5dd017d6e41a38dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Fri, 13 Jun 2025 00:46:36 +0200 Subject: [PATCH 10/11] Work around upload-artifacts preserving directory structure --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 13e0db1d..06c0d8d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,15 +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@v4 with: name: CSM ${{ steps.csm_version.outputs.version }} - path: | - src/csm/bin/Release/*.dll - scripts/install.ps1 + path: 'output/*' # Pack API package (Nuget) - name: Build nuget API package From 195368e7b0d03f850798abf692de5acbbd6434a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Borgsm=C3=BCller?= Date: Tue, 17 Jun 2025 22:49:52 +0200 Subject: [PATCH 11/11] Move festival panel call to main thread --- .../Commands/Handler/Events/EventSetSecurityBudgetHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs b/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs index 64c6502e..d969907b 100644 --- a/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs +++ b/src/basegame/Commands/Handler/Events/EventSetSecurityBudgetHandler.cs @@ -17,7 +17,10 @@ protected override void Handle(EventSetSecurityBudgetCommand command) if (InfoPanelHelper.IsEventBuilding(typeof(FestivalPanel), command.Event, out WorldInfoPanel panel)) { - ReflectionHelper.Call(panel, "OnSetTarget"); + SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() => + { + ReflectionHelper.Call(panel, "OnSetTarget"); + }); } IgnoreHelper.Instance.EndIgnore();