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();