diff --git a/build-linux.sh b/build-linux.sh new file mode 100755 index 00000000..d7117090 --- /dev/null +++ b/build-linux.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Configuration +REAL_GAME_DIR="/var/mnt/schijven/1TB SSD/Games/Cities - Skylines/drive_c/Program Files (x86)/Cities.Skylines.v1.21.1.F5" +MANAGED_DIR="$REAL_GAME_DIR/Cities_Data/Managed" +MODS_DIR="$REAL_GAME_DIR/Files/Mods" +CSM_ASSEMBLIES="/home/king/CS1 Mods/CSM/assemblies" + +# Harmony Assembly Paths +HARMONY_CORE="$MODS_DIR/Harmony 2.2.2-0/CitiesHarmony.Harmony.dll" +HARMONY_API="$MODS_DIR/81 Tiles 2 1.0.5/CitiesHarmony.API.dll" +PROTOBUF="$CSM_ASSEMBLIES/protobuf-net.dll" + +echo "=== Building CSM (Linux) ===" + +# Build command using dotnet msbuild properties to pass paths +dotnet build src/csm/CSM.csproj -c Release \ + -p:ManagedDir="$MANAGED_DIR" \ + -p:CsmDir="$CSM_ASSEMBLIES" \ + -p:HarmonyDll="$HARMONY_CORE" \ + -p:HarmonyApiDll="$HARMONY_API" + +if [ $? -eq 0 ]; then + echo "=== Build Successful! ===" + + # Optional: Install to game directory + TARGET_MOD_DIR="$MODS_DIR/CSM_Built" + echo "Installing to: $TARGET_MOD_DIR" + mkdir -p "$TARGET_MOD_DIR" + cp src/csm/bin/Release/net35/CSM.dll "$TARGET_MOD_DIR/" + cp src/api/bin/Release/net35/CSM.API.dll "$TARGET_MOD_DIR/" + cp src/basegame/bin/Release/net35/CSM.BaseGame.dll "$TARGET_MOD_DIR/" + # Copy dependencies + cp "$CSM_ASSEMBLIES"/*.dll "$TARGET_MOD_DIR/" + + echo "Done." +else + echo "=== Build Failed! ===" + exit 1 +fi diff --git a/src/api/CSM.API.csproj b/src/api/CSM.API.csproj index d0005a66..8699126f 100644 --- a/src/api/CSM.API.csproj +++ b/src/api/CSM.API.csproj @@ -1,64 +1,45 @@ - - - + - Debug - AnyCPU - {AB27EACD-B9A9-42BC-BF8A-3B25AABFF6CA} + net35 Library - Properties CSM.API CSM.API - v3.5 - 512 - - + true + false + 7.3 + true + false - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - + - - - - - - - - - - - - + $(ManagedDir)\Assembly-CSharp.dllfalse + $(ManagedDir)\ColossalManaged.dllfalse + $(ManagedDir)\ICities.dllfalse + $(ManagedDir)\UnityEngine.dllfalse + $(ManagedDir)\UnityEngine.UI.dllfalse + $(CsmDir)\protobuf-net.dllfalse + - - 1.0.2856.0 - + + + + $(PkgMicrosoft_NETFramework_ReferenceAssemblies_net35)build\.NETFramework\v3.5 + + - - False - ..\..\assemblies\protobuf-net.dll + + $(Net35RefDir)\mscorlib.dll + false + + + $(Net35RefDir)\System.dll + false + + + $(Net35RefDir)\System.Core.dll + false - diff --git a/src/api/Helpers/IgnoreHelper.cs b/src/api/Helpers/IgnoreHelper.cs index 47caece2..97eb6260 100644 --- a/src/api/Helpers/IgnoreHelper.cs +++ b/src/api/Helpers/IgnoreHelper.cs @@ -11,14 +11,22 @@ namespace CSM.API.Helpers /// public class IgnoreHelper { + [ThreadStatic] + private static IgnoreHelper _instance; + public static IgnoreHelper Instance { - get => _instance.Value; - set => _instance.Value = value; + get + { + if (_instance == null) + { + _instance = new IgnoreHelper(); + } + return _instance; + } + set => _instance = value; } - private static readonly ThreadLocal _instance = new ThreadLocal(() => new IgnoreHelper()); - private int _ignoreAll = 0; private readonly HashSet _exceptions = new HashSet(); diff --git a/src/basegame/CSM.BaseGame.csproj b/src/basegame/CSM.BaseGame.csproj index 72b732a9..4d475cc7 100644 --- a/src/basegame/CSM.BaseGame.csproj +++ b/src/basegame/CSM.BaseGame.csproj @@ -1,293 +1,51 @@ - - - + - Debug - AnyCPU - {bfc8de00-f495-4388-9e36-f1471f8ed578} + net35 Library - Properties CSM.BaseGame CSM.BaseGame - v3.5 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + true + false + 7.3 + true + false + - - False - ..\..\assemblies\Assembly-CSharp.dll - False - - - False - ..\..\assemblies\ColossalManaged.dll - False - - - False - ..\..\assemblies\ICities.dll - False - - - False - ..\..\assemblies\UnityEngine.dll - False - - - False - ..\..\assemblies\UnityEngine.UI.dll - False - - - False - ..\..\assemblies\protobuf-net.dll - - - + $(ManagedDir)\Assembly-CSharp.dllfalse + $(ManagedDir)\ColossalManaged.dllfalse + $(ManagedDir)\ICities.dllfalse + $(ManagedDir)\UnityEngine.dllfalse + $(ManagedDir)\UnityEngine.UI.dllfalse + $(CsmDir)\protobuf-net.dllfalse + $(HarmonyApiDll)false + $(HarmonyDll)false + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - 0.9.4 - - - 2.2.0 - + + + + $(PkgMicrosoft_NETFramework_ReferenceAssemblies_net35)build\.NETFramework\v3.5 + + - - {ab27eacd-b9a9-42bc-bf8a-3b25aabff6ca} - CSM.API - + + $(Net35RefDir)\mscorlib.dll + false + + + $(Net35RefDir)\System.dll + false + + + $(Net35RefDir)\System.Core.dll + false + - diff --git a/src/basegame/Commands/Data/Disasters/DisasterCommand.cs b/src/basegame/Commands/Data/Disasters/DisasterCommand.cs new file mode 100644 index 00000000..841c436b --- /dev/null +++ b/src/basegame/Commands/Data/Disasters/DisasterCommand.cs @@ -0,0 +1,25 @@ +using CSM.API.Commands; +using ProtoBuf; +using UnityEngine; + +namespace CSM.BaseGame.Commands.Data.Disasters +{ + [ProtoContract] + public class DisasterCommand : CommandBase + { + [ProtoMember(1)] + public uint InfoIndex { get; set; } + + [ProtoMember(2)] + public Vector3 Position { get; set; } + + [ProtoMember(3)] + public int Intensity { get; set; } + + [ProtoMember(4)] + public float Angle { get; set; } + + [ProtoMember(5)] + public ulong RandomSeed { get; set; } + } +} diff --git a/src/basegame/Commands/Data/Disasters/EvacuationCommand.cs b/src/basegame/Commands/Data/Disasters/EvacuationCommand.cs new file mode 100644 index 00000000..36be10c7 --- /dev/null +++ b/src/basegame/Commands/Data/Disasters/EvacuationCommand.cs @@ -0,0 +1,15 @@ +using CSM.API.Commands; +using ProtoBuf; + +namespace CSM.BaseGame.Commands.Data.Disasters +{ + [ProtoContract] + public class EvacuationCommand : CommandBase + { + [ProtoMember(1)] + public ushort BuildingID { get; set; } + + [ProtoMember(2)] + public bool Evacuating { get; set; } + } +} diff --git a/src/basegame/Commands/Data/Events/RaceEventCancelCommand.cs b/src/basegame/Commands/Data/Events/RaceEventCancelCommand.cs new file mode 100644 index 00000000..fd51cbcf --- /dev/null +++ b/src/basegame/Commands/Data/Events/RaceEventCancelCommand.cs @@ -0,0 +1,20 @@ +using CSM.API.Commands; +using ProtoBuf; + +namespace CSM.BaseGame.Commands.Data.Events +{ + /// + /// Called when a race event is cancelled by a player. + /// + /// Sent by: + /// - RaceEventHandler + [ProtoContract] + public class RaceEventCancelCommand : CommandBase + { + /// + /// The id of the race event to cancel. + /// + [ProtoMember(1)] + public ushort Event { get; set; } + } +} diff --git a/src/basegame/Commands/Data/Events/RaceEventEndCommand.cs b/src/basegame/Commands/Data/Events/RaceEventEndCommand.cs new file mode 100644 index 00000000..53773432 --- /dev/null +++ b/src/basegame/Commands/Data/Events/RaceEventEndCommand.cs @@ -0,0 +1,20 @@ +using CSM.API.Commands; +using ProtoBuf; + +namespace CSM.BaseGame.Commands.Data.Events +{ + /// + /// Called when a race event is manually ended by a player. + /// + /// Sent by: + /// - RaceEventHandler + [ProtoContract] + public class RaceEventEndCommand : CommandBase + { + /// + /// The id of the race event to end. + /// + [ProtoMember(1)] + public ushort Event { get; set; } + } +} diff --git a/src/basegame/Commands/Data/Events/RaceEventStartCommand.cs b/src/basegame/Commands/Data/Events/RaceEventStartCommand.cs new file mode 100644 index 00000000..896566a0 --- /dev/null +++ b/src/basegame/Commands/Data/Events/RaceEventStartCommand.cs @@ -0,0 +1,20 @@ +using CSM.API.Commands; +using ProtoBuf; + +namespace CSM.BaseGame.Commands.Data.Events +{ + /// + /// Called when a race event is manually started by a player. + /// + /// Sent by: + /// - RaceEventHandler + [ProtoContract] + public class RaceEventStartCommand : CommandBase + { + /// + /// The id of the race event to start. + /// + [ProtoMember(1)] + public ushort Event { get; set; } + } +} diff --git a/src/basegame/Commands/Handler/Disasters/DisasterHandler.cs b/src/basegame/Commands/Handler/Disasters/DisasterHandler.cs new file mode 100644 index 00000000..4b68be19 --- /dev/null +++ b/src/basegame/Commands/Handler/Disasters/DisasterHandler.cs @@ -0,0 +1,43 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Disasters; +using ColossalFramework; +using UnityEngine; + +namespace CSM.BaseGame.Commands.Handler.Disasters +{ + public class DisasterHandler : CommandHandler + { + protected override void Handle(DisasterCommand command) + { + DisasterInfo info = PrefabCollection.GetPrefab(command.InfoIndex); + + if (info == null) + { + Log.Warn($"[CSM] Received DisasterCommand with invalid InfoIndex: {command.InfoIndex}"); + return; + } + + Log.Info($"[CSM] Triggering disaster {info.name} at {command.Position}"); + + IgnoreHelper.Instance.StartIgnore(); + + DisasterManager instance = Singleton.instance; + if (instance.CreateDisaster(out ushort disasterID, info)) + { + instance.m_disasters.m_buffer[disasterID].m_targetPosition = command.Position; + instance.m_disasters.m_buffer[disasterID].m_angle = command.Angle; + instance.m_disasters.m_buffer[disasterID].m_intensity = (byte)command.Intensity; + instance.m_disasters.m_buffer[disasterID].m_randomSeed = command.RandomSeed; + instance.m_disasters.m_buffer[disasterID].m_flags |= DisasterData.Flags.SelfTrigger | DisasterData.Flags.Detected; + + Log.Info($"[CSM] Starting disaster ID {disasterID} ({info.name}). Flags: {instance.m_disasters.m_buffer[disasterID].m_flags}"); + + info.m_disasterAI.StartNow(disasterID, ref instance.m_disasters.m_buffer[disasterID]); + } + + IgnoreHelper.Instance.EndIgnore(); + } + } +} diff --git a/src/basegame/Commands/Handler/Disasters/EvacuationHandler.cs b/src/basegame/Commands/Handler/Disasters/EvacuationHandler.cs new file mode 100644 index 00000000..ce03c7e2 --- /dev/null +++ b/src/basegame/Commands/Handler/Disasters/EvacuationHandler.cs @@ -0,0 +1,34 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Disasters; +using ColossalFramework; +using HarmonyLib; + +namespace CSM.BaseGame.Commands.Handler.Disasters +{ + public class EvacuationHandler : CommandHandler + { + protected override void Handle(EvacuationCommand command) + { + if (command.BuildingID == 0) + return; + + BuildingManager instance = Singleton.instance; + BuildingInfo info = instance.m_buildings.m_buffer[command.BuildingID].Info; + + if (info == null || info.m_buildingAI == null) + return; + + Log.Info($"[CSM] Setting evacuation for building {command.BuildingID} ({info.name}) to {command.Evacuating}"); + + IgnoreHelper.Instance.StartIgnore(); + + // Use Harmony's AccessTools to call the protected SetEvacuating method + var method = AccessTools.Method(typeof(CommonBuildingAI), "SetEvacuating", new[] { typeof(ushort), typeof(Building).MakeByRefType(), typeof(bool) }); + method.Invoke(info.m_buildingAI, new object[] { command.BuildingID, instance.m_buildings.m_buffer[command.BuildingID], command.Evacuating }); + + IgnoreHelper.Instance.EndIgnore(); + } + } +} diff --git a/src/basegame/Commands/Handler/Events/RaceEventCancelHandler.cs b/src/basegame/Commands/Handler/Events/RaceEventCancelHandler.cs new file mode 100644 index 00000000..76b96bd8 --- /dev/null +++ b/src/basegame/Commands/Handler/Events/RaceEventCancelHandler.cs @@ -0,0 +1,34 @@ +using System; +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Events; +using ColossalFramework; + +namespace CSM.BaseGame.Commands.Handler.Events +{ + public class RaceEventCancelHandler : CommandHandler + { + protected override void Handle(RaceEventCancelCommand command) + { + IgnoreHelper.Instance.StartIgnore(); + + ref EventData eventData = ref Singleton.instance.m_events.m_buffer[command.Event]; + RaceEventAI raceAI = eventData.Info.m_eventAI as RaceEventAI; + if (raceAI != null) + { + Log.Info($"[CSM Race] Cancelling race event {command.Event}"); + Type[] types = new Type[] { typeof(ushort), typeof(EventData).MakeByRefType() }; + object[] param = new object[] { command.Event, eventData }; + ReflectionHelper.Call(raceAI, "Cancel", types, param); + eventData = (EventData)param[1]; + } + else + { + Log.Warn($"[CSM Race] Received RaceEventCancelCommand for non-race event {command.Event}"); + } + + IgnoreHelper.Instance.EndIgnore(); + } + } +} diff --git a/src/basegame/Commands/Handler/Events/RaceEventEndHandler.cs b/src/basegame/Commands/Handler/Events/RaceEventEndHandler.cs new file mode 100644 index 00000000..b9f1672c --- /dev/null +++ b/src/basegame/Commands/Handler/Events/RaceEventEndHandler.cs @@ -0,0 +1,30 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Events; +using ColossalFramework; + +namespace CSM.BaseGame.Commands.Handler.Events +{ + public class RaceEventEndHandler : CommandHandler + { + protected override void Handle(RaceEventEndCommand command) + { + IgnoreHelper.Instance.StartIgnore(); + + ref EventData eventData = ref Singleton.instance.m_events.m_buffer[command.Event]; + RaceEventAI raceAI = eventData.Info.m_eventAI as RaceEventAI; + if (raceAI != null) + { + Log.Info($"[CSM Race] Ending race event {command.Event}"); + ReflectionHelper.Call(raceAI, "EndRace", command.Event); + } + else + { + Log.Warn($"[CSM Race] Received RaceEventEndCommand for non-race event {command.Event}"); + } + + IgnoreHelper.Instance.EndIgnore(); + } + } +} diff --git a/src/basegame/Commands/Handler/Events/RaceEventStartHandler.cs b/src/basegame/Commands/Handler/Events/RaceEventStartHandler.cs new file mode 100644 index 00000000..6aa17370 --- /dev/null +++ b/src/basegame/Commands/Handler/Events/RaceEventStartHandler.cs @@ -0,0 +1,30 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Events; +using ColossalFramework; + +namespace CSM.BaseGame.Commands.Handler.Events +{ + public class RaceEventStartHandler : CommandHandler + { + protected override void Handle(RaceEventStartCommand command) + { + IgnoreHelper.Instance.StartIgnore(); + + ref EventData eventData = ref Singleton.instance.m_events.m_buffer[command.Event]; + RaceEventAI raceAI = eventData.Info.m_eventAI as RaceEventAI; + if (raceAI != null) + { + Log.Info($"[CSM Race] Starting race event {command.Event}"); + ReflectionHelper.Call(raceAI, "StartRace", command.Event); + } + else + { + Log.Warn($"[CSM Race] Received RaceEventStartCommand for non-race event {command.Event}"); + } + + IgnoreHelper.Instance.EndIgnore(); + } + } +} diff --git a/src/basegame/Helpers/ToolSimulator.cs b/src/basegame/Helpers/ToolSimulator.cs index fdea987b..d03d9572 100644 --- a/src/basegame/Helpers/ToolSimulator.cs +++ b/src/basegame/Helpers/ToolSimulator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using CSM.API.Helpers; using UnityEngine; @@ -26,11 +26,21 @@ public IEnumerable GetTools() { public void GetToolAndController(int sender, out Tool tool, out ToolController toolController) where Tool: ToolBase { tool = this.GetTool(sender); + if (object.ReferenceEquals(tool, null)) + { + toolController = null; + return; + } toolController = ReflectionHelper.GetAttr(tool, "m_toolController"); } public T GetTool(int sender) where T : ToolBase { + // During download/loading, tool instances can't be safely created + // because Unity managers aren't initialized yet. + if (!Singleton.exists || !Singleton.instance.m_loadingComplete) + return null; + if (_currentTools.TryGetValue(sender, out ToolBase tool)) { if (tool.GetType() == typeof(T)) @@ -52,22 +62,34 @@ public T GetTool(int sender) where T : ToolBase ReflectionHelper.SetAttr(controller, "m_collidingBuildings1", new ulong[768]); ReflectionHelper.SetAttr(controller, "m_collidingBuildings2", new ulong[768]); ReflectionHelper.SetAttr(controller, "m_collidingDepth", 0); - controller.m_brushMaterial = ToolsModifierControl.toolController.m_brushMaterial; - ReflectionHelper.SetAttr(controller, "m_brushMaterial2", new Material(controller.m_brushMaterial)); + if (ToolsModifierControl.toolController != null) + { + controller.m_brushMaterial = ToolsModifierControl.toolController.m_brushMaterial; + ReflectionHelper.SetAttr(controller, "m_brushMaterial2", new Material(controller.m_brushMaterial)); + } switch (tool) { case BulldozeTool bulldozeTool: - bulldozeTool.m_cursor = ToolsModifierControl.toolController.GetComponent().m_cursor; - bulldozeTool.m_undergroundCursor = ToolsModifierControl.toolController.GetComponent().m_undergroundCursor; + if (ToolsModifierControl.toolController != null) + { + bulldozeTool.m_cursor = ToolsModifierControl.toolController.GetComponent().m_cursor; + bulldozeTool.m_undergroundCursor = ToolsModifierControl.toolController.GetComponent().m_undergroundCursor; + } break; case DefaultTool defaultTool: - defaultTool.m_cursor = ToolsModifierControl.toolController.GetComponent().m_cursor; - defaultTool.m_undergroundCursor = ToolsModifierControl.toolController.GetComponent().m_undergroundCursor; + if (ToolsModifierControl.toolController != null) + { + defaultTool.m_cursor = ToolsModifierControl.toolController.GetComponent().m_cursor; + defaultTool.m_undergroundCursor = ToolsModifierControl.toolController.GetComponent().m_undergroundCursor; + } break; case BuildingTool buildingTool: - buildingTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; + if (ToolsModifierControl.toolController != null) + { + buildingTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; + } break; case NetTool netTool: // See NetTool::Awake @@ -80,8 +102,11 @@ public T GetTool(int sender) where T : ToolBase ReflectionHelper.SetAttr(netTool, "m_tempUpgraded", new FastList()); ReflectionHelper.SetAttr(netTool, "m_helperLineTimer", new Dictionary()); ReflectionHelper.SetAttr(netTool, "m_overlayBuildings", new HashSet()); - netTool.m_upgradeCursor = ToolsModifierControl.toolController.GetComponent().m_upgradeCursor; - netTool.m_placementCursor = ToolsModifierControl.toolController.GetComponent().m_placementCursor; + if (ToolsModifierControl.toolController != null) + { + netTool.m_upgradeCursor = ToolsModifierControl.toolController.GetComponent().m_upgradeCursor; + netTool.m_placementCursor = ToolsModifierControl.toolController.GetComponent().m_placementCursor; + } break; case ZoneTool zoneTool: { @@ -93,18 +118,24 @@ public T GetTool(int sender) where T : ToolBase Type fillPos = typeof(ZoneTool).GetNestedType("FillPos", ReflectionHelper.AllAccessFlags); ReflectionHelper.SetAttr(zoneTool, "m_fillPositions", Activator.CreateInstance(typeof(FastList<>).MakeGenericType(fillPos))); ReflectionHelper.SetAttr(zoneTool, "m_dataLock", new object()); - zoneTool.m_zoneCursors = ToolsModifierControl.toolController.GetComponent().m_zoneCursors; + if (ToolsModifierControl.toolController != null) + { + zoneTool.m_zoneCursors = ToolsModifierControl.toolController.GetComponent().m_zoneCursors; + } break; } case TerrainTool terrainTool: { // copy terrain tools across - TerrainTool realTerrainTool = ToolsModifierControl.toolController.GetComponent(); - terrainTool.m_brush = realTerrainTool.m_brush; - terrainTool.m_shiftCursor = realTerrainTool.m_shiftCursor; - terrainTool.m_levelCursor = realTerrainTool.m_levelCursor; - terrainTool.m_slopeCursor = realTerrainTool.m_slopeCursor; - terrainTool.m_softenCursor = realTerrainTool.m_softenCursor; + if (ToolsModifierControl.toolController != null) + { + TerrainTool realTerrainTool = ToolsModifierControl.toolController.GetComponent(); + terrainTool.m_brush = realTerrainTool.m_brush; + terrainTool.m_shiftCursor = realTerrainTool.m_shiftCursor; + terrainTool.m_levelCursor = realTerrainTool.m_levelCursor; + terrainTool.m_slopeCursor = realTerrainTool.m_slopeCursor; + terrainTool.m_softenCursor = realTerrainTool.m_softenCursor; + } ReflectionHelper.SetAttr(terrainTool, "m_undoList", new List()); break; } @@ -115,8 +146,11 @@ public T GetTool(int sender) where T : ToolBase break; case PropTool propTool: // copy prop tool cursor across - propTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; - propTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + if (ToolsModifierControl.toolController != null) + { + propTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; + propTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + } break; case TreeTool treeTool: // see TreeTool::Awake() @@ -125,13 +159,19 @@ public T GetTool(int sender) where T : ToolBase ReflectionHelper.SetAttr(treeTool, "m_upgradedSegments", new HashSet()); ReflectionHelper.SetAttr(treeTool, "m_tempUpgraded", new FastList()); - treeTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; - treeTool.m_upgradeCursor = ToolsModifierControl.toolController.GetComponent().m_upgradeCursor; - treeTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + if (ToolsModifierControl.toolController != null) + { + treeTool.m_buildCursor = ToolsModifierControl.toolController.GetComponent().m_buildCursor; + treeTool.m_upgradeCursor = ToolsModifierControl.toolController.GetComponent().m_upgradeCursor; + treeTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + } break; case DistrictTool districtTool: - districtTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + if (ToolsModifierControl.toolController != null) + { + districtTool.m_brush = ToolsModifierControl.toolController.GetComponent().m_brush; + } break; } diff --git a/src/basegame/Injections/Buildings/BuildingAIHandler.cs b/src/basegame/Injections/Buildings/BuildingAIHandler.cs new file mode 100644 index 00000000..44ea119a --- /dev/null +++ b/src/basegame/Injections/Buildings/BuildingAIHandler.cs @@ -0,0 +1,32 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Disasters; +using HarmonyLib; + +namespace CSM.BaseGame.Injections.Buildings +{ + [HarmonyPatch(typeof(CommonBuildingAI))] + [HarmonyPatch("SetEvacuating")] + public class BuildingAIHandler + { + public static void Prefix(ushort buildingID, ref Building data, bool evacuating) + { + if (IgnoreHelper.Instance.IsIgnored()) + return; + + if (Command.CurrentRole == MultiplayerRole.None) + return; + + bool isEvacuating = (data.m_flags & Building.Flags.Evacuating) != Building.Flags.None; + if (isEvacuating == evacuating) + return; + + Command.SendToAll(new EvacuationCommand + { + BuildingID = buildingID, + Evacuating = evacuating + }); + } + } +} diff --git a/src/basegame/Injections/Disasters/DisasterAIHandler.cs b/src/basegame/Injections/Disasters/DisasterAIHandler.cs new file mode 100644 index 00000000..50d04603 --- /dev/null +++ b/src/basegame/Injections/Disasters/DisasterAIHandler.cs @@ -0,0 +1,39 @@ +using CSM.API; +using CSM.API.Commands; +using CSM.API.Helpers; +using CSM.BaseGame.Commands.Data.Disasters; +using HarmonyLib; + +namespace CSM.BaseGame.Injections.Disasters +{ + [HarmonyPatch(typeof(DisasterAI))] + [HarmonyPatch("StartNow")] + public class DisasterAIHandler + { + public static void Prefix(ushort disasterID, ref DisasterData data, DisasterAI __instance) + { + if (IgnoreHelper.Instance.IsIgnored()) + return; + + if (Command.CurrentRole == MultiplayerRole.None) + return; + + // Sync if: + // 1. It was manually triggered by a player (SelfTrigger flag set). + // 2. We are the Server (coordinates all disasters, including random ones). + bool isManual = (data.m_flags & DisasterData.Flags.SelfTrigger) != DisasterData.Flags.None; + + if (isManual || Command.CurrentRole == MultiplayerRole.Server) + { + Command.SendToAll(new DisasterCommand + { + InfoIndex = (uint)__instance.m_info.m_prefabDataIndex, + Position = data.m_targetPosition, + Angle = data.m_angle, + Intensity = (int)data.m_intensity, + RandomSeed = data.m_randomSeed + }); + } + } + } +} diff --git a/src/basegame/Injections/EventHandler.cs b/src/basegame/Injections/EventHandler.cs index 4ffd0586..c3a5ef80 100644 --- a/src/basegame/Injections/EventHandler.cs +++ b/src/basegame/Injections/EventHandler.cs @@ -37,7 +37,8 @@ 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) && type != typeof(VarsitySportsMatchAI)) + if (type != typeof(RocketLaunchAI) && type != typeof(ConcertAI) && type != typeof(SportMatchAI) && type != typeof(VarsitySportsMatchAI) + && !type.IsSubclassOf(typeof(RaceEventAI))) return; if (newColor.r == data.m_color.r && newColor.g == data.m_color.g && newColor.b == data.m_color.b) diff --git a/src/basegame/Injections/RaceEventHandler.cs b/src/basegame/Injections/RaceEventHandler.cs new file mode 100644 index 00000000..2abe21c1 --- /dev/null +++ b/src/basegame/Injections/RaceEventHandler.cs @@ -0,0 +1,76 @@ +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.Events; +using HarmonyLib; + +namespace CSM.BaseGame.Injections +{ + /// + /// Harmony patches for Race Day DLC synchronization. + /// Handles: StartRace, EndRace, Cancel for all RaceEventAI subclasses. + /// + + [HarmonyPatch(typeof(RaceEventAI))] + [HarmonyPatch("StartRace")] + public class RaceEventStart + { + public static void Prefix(ushort eventID) + { + if (IgnoreHelper.Instance.IsIgnored()) + return; + + Log.Info($"[CSM Race] StartRace intercepted for event {eventID}"); + + Command.SendToAll(new RaceEventStartCommand() + { + Event = eventID + }); + } + } + + [HarmonyPatch(typeof(RaceEventAI))] + [HarmonyPatch("EndRace")] + public class RaceEventEnd + { + public static void Prefix(ushort eventID) + { + if (IgnoreHelper.Instance.IsIgnored()) + return; + + Log.Info($"[CSM Race] EndRace intercepted for event {eventID}"); + + Command.SendToAll(new RaceEventEndCommand() + { + Event = eventID + }); + } + } + + [HarmonyPatch(typeof(RaceEventAI))] + [HarmonyPatch("Cancel")] + public class RaceEventCancel + { + public static void Prefix(ushort eventID, ref EventData data, RaceEventAI __instance) + { + if (IgnoreHelper.Instance.IsIgnored()) + return; + + // Only sync if the event is not already completed or cancelled + if ((data.m_flags & (EventData.Flags.Completed | EventData.Flags.Cancelled)) != 0) + return; + + Log.Info($"[CSM Race] Cancel intercepted for event {eventID}"); + + Command.SendToAll(new RaceEventCancelCommand() + { + Event = eventID + }); + } + } + + +} diff --git a/src/basegame/Injections/Tools/BaseToolHandler.cs b/src/basegame/Injections/Tools/BaseToolHandler.cs index e1f8b4c6..86f921eb 100644 --- a/src/basegame/Injections/Tools/BaseToolHandler.cs +++ b/src/basegame/Injections/Tools/BaseToolHandler.cs @@ -13,21 +13,33 @@ protected BaseToolCommandHandler() protected override void Handle(Cmd command) { + // Skip tool sync commands while the game is still loading/downloading. + // The host sends cursor position updates while the client is connecting, + // but the client's Unity managers aren't ready to create tool instances yet. + if (!Singleton.exists || !Singleton.instance.m_loadingComplete) + return; + Singleton.instance.GetToolAndController(command.SenderId, out Tool tool, out ToolController controller); + if (object.ReferenceEquals(tool, null)) + return; + Configure(tool, controller, command); - SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() => + if (SimulationManager.instance != null && SimulationManager.instance.m_ThreadingWrapper != null) { - PlayerCursorManager cursorView = - Singleton.instance.GetCursorView(command.SenderId); - if (cursorView) + SimulationManager.instance.m_ThreadingWrapper.QueueMainThread(() => { - cursorView.SetLabelContent(command); - cursorView.SetCursor(this.GetCursorInfo(tool)); - } - }); + PlayerCursorManager cursorView = + Singleton.instance.GetCursorView(command.SenderId); + if (cursorView) + { + cursorView.SetLabelContent(command); + cursorView.SetCursor(this.GetCursorInfo(tool)); + } + }); + } } protected abstract void Configure(Tool tool, ToolController toolController, Cmd command); diff --git a/src/basegame/Injections/Tools/NetToolHandler.cs b/src/basegame/Injections/Tools/NetToolHandler.cs index a028ce14..75be2a6e 100644 --- a/src/basegame/Injections/Tools/NetToolHandler.cs +++ b/src/basegame/Injections/Tools/NetToolHandler.cs @@ -110,6 +110,8 @@ protected override void Configure(NetTool tool, ToolController toolController, P protected override CursorInfo GetCursorInfo(NetTool tool) { + if (tool.Prefab == null) return tool.m_placementCursor; + if (tool.m_mode == NetTool.Mode.Upgrade) { return tool.Prefab.m_upgradeCursor ? tool.Prefab.m_upgradeCursor : tool.m_upgradeCursor; } else { diff --git a/src/basegame/Injections/Tools/PlayerCursorManager.cs b/src/basegame/Injections/Tools/PlayerCursorManager.cs index 63b35a28..dccb3137 100644 --- a/src/basegame/Injections/Tools/PlayerCursorManager.cs +++ b/src/basegame/Injections/Tools/PlayerCursorManager.cs @@ -14,8 +14,17 @@ public class PlayerCursorManager: MonoBehaviour { private const float ScreenEdgeInset = 12; public void Start() { + UIView uiView = null; + if (ToolBase.cursorInfoLabel != null) + uiView = ToolBase.cursorInfoLabel.GetUIView(); + else + uiView = UIView.GetAView(); - UIView uiView = ToolBase.cursorInfoLabel.GetUIView(); + if (uiView == null) + { + Destroy(this); + return; + } _playerNameLabel = (UILabel)uiView.AddUIComponent(typeof(UILabel)); _playerNameLabel.textAlignment = UIHorizontalAlignment.Center; @@ -100,7 +109,8 @@ public void SetLabelContent(string playerName, Vector3 worldPosition) { public void SetCursor(CursorInfo newCursorInfo) { if (newCursorInfo == null) { - newCursorInfo = ToolsModifierControl.toolController.GetComponent().m_cursor; + if (ToolsModifierControl.toolController != null) + newCursorInfo = ToolsModifierControl.toolController.GetComponent().m_cursor; } if (newCursorInfo != null && (this._cursorInfo == null || this._cursorInfo.name != newCursorInfo.name)) { diff --git a/src/basegame/Injections/Tools/TerrainToolHandler.cs b/src/basegame/Injections/Tools/TerrainToolHandler.cs index a8cb314e..d914f820 100644 --- a/src/basegame/Injections/Tools/TerrainToolHandler.cs +++ b/src/basegame/Injections/Tools/TerrainToolHandler.cs @@ -66,9 +66,16 @@ public bool Equals(PlayerTerrainToolCommand other) public class PlayerTerrainToolCommandHandler : BaseToolCommandHandler { protected override void Configure(TerrainTool tool, ToolController toolController, PlayerTerrainToolCommand command) { - // The terrain tool uses to the tool controller to hold onto brush state and to render it - tool.m_mode = (TerrainTool.Mode) command.Mode; - toolController.SetBrush(tool.m_brush, command.MousePosition, command.BrushSize); + try + { + // The terrain tool uses to the tool controller to hold onto brush state and to render it + tool.m_mode = (TerrainTool.Mode) command.Mode; + toolController?.SetBrush(tool.m_brush, command.MousePosition, command.BrushSize); + } + catch (Exception ex) + { + Log.Warn($"Error configuring terrain tool: {ex.Message}"); + } } protected override CursorInfo GetCursorInfo(TerrainTool tool) diff --git a/src/csm/CSM.csproj b/src/csm/CSM.csproj index 6cdcf716..a0e44f03 100644 --- a/src/csm/CSM.csproj +++ b/src/csm/CSM.csproj @@ -1,205 +1,65 @@ - - - - - Debug - AnyCPU - {63933537-DA87-4026-A44C-382298FBB399} - Library - Properties - CSM - CSM - v3.5 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - ..\..\assemblies\Assembly-CSharp.dll - False - - - False - ..\..\assemblies\ColossalManaged.dll - False - - - False - ..\..\assemblies\ICities.dll - False - - - - - False - ..\..\assemblies\UnityEngine.dll - False - - - False - ..\..\assemblies\UnityEngine.UI.dll - False - - - False - ..\..\assemblies\protobuf-net.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0.9.4 - - - 2.1.0 - - - 2.2.0 - - - - - {ab27eacd-b9a9-42bc-bf8a-3b25aabff6ca} - CSM.API - - - {bfc8de00-f495-4388-9e36-f1471f8ed578} - CSM.BaseGame - - - - - - - - powershell -ExecutionPolicy Unrestricted -Command "$(SolutionDir)\scripts\build.ps1" -OutputDirectory $(TargetDir) -Install - pwsh "$(SolutionDir)scripts/build.ps1" -OutputDirectory "$(TargetDir)" -Install - pwsh "$(SolutionDir)scripts/build.ps1" -OutputDirectory "$(TargetDir)" -Install - + + + net35 + Library + CSM + CSM + true + false + 7.3 + true + false + + + + $(ManagedDir)\Assembly-CSharp.dllfalse + $(ManagedDir)\ColossalManaged.dllfalse + $(ManagedDir)\ICities.dllfalse + $(ManagedDir)\UnityEngine.dllfalse + $(ManagedDir)\UnityEngine.UI.dllfalse + $(CsmDir)\LiteNetLib.dllfalse + $(CsmDir)\protobuf-net.dllfalse + $(HarmonyApiDll)false + $(HarmonyDll)false + + + + + + + + + + + + + + + + + + + $(PkgMicrosoft_NETFramework_ReferenceAssemblies_net35)build\.NETFramework\v3.5 + + + + + $(Net35RefDir)\mscorlib.dll + false + + + $(Net35RefDir)\System.dll + false + + + $(Net35RefDir)\System.Core.dll + false + + + $(Net35RefDir)\System.Xml.dll + false + + + + diff --git a/src/csm/Commands/CommandInternal.cs b/src/csm/Commands/CommandInternal.cs index ae85405a..4c2fadbf 100644 --- a/src/csm/Commands/CommandInternal.cs +++ b/src/csm/Commands/CommandInternal.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -49,7 +49,7 @@ public void SendToClient(NetPeer peer, CommandBase command) /// /// The Player to send the command to. /// The command to send. - public void SendToClient(Player player, CommandBase command) + public void SendToClient(global::CSM.API.Networking.Player player, CommandBase command) { if (player is CSMPlayer csmPlayer) { @@ -83,7 +83,7 @@ public void SendToClients(CommandBase command) /// /// The command to send. /// The player to not send the packet to. - public void SendToOtherClients(CommandBase command, Player exclude) + public void SendToOtherClients(CommandBase command, global::CSM.API.Networking.Player exclude) { foreach (CSMPlayer player in MultiplayerManager.Instance.CurrentServer.ConnectedPlayers.Values) { @@ -150,7 +150,7 @@ private void SetSenderId(CommandBase command) /// It calls the OnClientConnect methods of all handlers. /// /// The connected player. - public void HandleClientConnect(Player player) + public void HandleClientConnect(global::CSM.API.Networking.Player player) { foreach (CommandHandler handler in _cmdMapping.Values) { @@ -163,7 +163,7 @@ public void HandleClientConnect(Player player) /// It calls the OnClientDisconnect methods of all handlers. /// /// The disconnected player. - public void HandleClientDisconnect(Player player) + public void HandleClientDisconnect(global::CSM.API.Networking.Player player) { foreach (CommandHandler handler in _cmdMapping.Values) { diff --git a/src/csm/Commands/Handler/Internal/ClientConnectHandler.cs b/src/csm/Commands/Handler/Internal/ClientConnectHandler.cs index 665151f3..17f68df9 100644 --- a/src/csm/Commands/Handler/Internal/ClientConnectHandler.cs +++ b/src/csm/Commands/Handler/Internal/ClientConnectHandler.cs @@ -1,4 +1,4 @@ -using CSM.API; +using CSM.API; using CSM.API.Commands; using CSM.API.Networking; using CSM.Commands.Data.Internal; @@ -30,9 +30,9 @@ protected override void Handle(ClientConnectCommand command) } } - public override void OnClientConnect(Player player) + public override void OnClientConnect(global::CSM.API.Networking.Player player) { - CommandInternal.Instance.SendToOtherClients(new ClientConnectCommand { Username = player.Username }, player); + CommandInternal.Instance.SendToOtherClients(new ClientConnectCommand { Username = player.Username }, player as global::CSM.Networking.Player); } } } diff --git a/src/csm/Commands/Handler/Internal/ClientDisonnectHandler.cs b/src/csm/Commands/Handler/Internal/ClientDisonnectHandler.cs index fcc44db8..50e881db 100644 --- a/src/csm/Commands/Handler/Internal/ClientDisonnectHandler.cs +++ b/src/csm/Commands/Handler/Internal/ClientDisonnectHandler.cs @@ -1,4 +1,4 @@ -using CSM.API; +using CSM.API; using CSM.API.Commands; using CSM.API.Networking; using CSM.BaseGame.Helpers; @@ -34,9 +34,10 @@ protected override void Handle(ClientDisconnectCommand command) } } - public override void OnClientDisconnect(Player player) + public override void OnClientDisconnect(global::CSM.API.Networking.Player player) { - int clientId = player is CSMPlayer csmPlayer ? csmPlayer.NetPeer.Id : -2; + var internalPlayer = player as global::CSM.Networking.Player; + int clientId = (internalPlayer is CSMPlayer csmPlayer) ? csmPlayer.NetPeer.Id : -2; Command.SendToClients(new ClientDisconnectCommand { Username = player.Username, diff --git a/src/csm/Commands/Handler/Internal/ClientJoiningHandler.cs b/src/csm/Commands/Handler/Internal/ClientJoiningHandler.cs index 5144570f..65979c2d 100644 --- a/src/csm/Commands/Handler/Internal/ClientJoiningHandler.cs +++ b/src/csm/Commands/Handler/Internal/ClientJoiningHandler.cs @@ -1,4 +1,4 @@ -using CSM.API.Commands; +using CSM.API.Commands; using CSM.API.Networking; using CSM.API.Networking.Status; using CSM.Commands.Data.Internal; @@ -25,16 +25,17 @@ protected override void Handle(ClientJoiningCommand command) } } - public override void OnClientDisconnect(Player player) + public override void OnClientConnect(global::CSM.API.Networking.Player player) { - if (player.Status != ClientStatus.Connected) + var internalPlayer = player as global::CSM.Networking.Player; + if (internalPlayer != null && internalPlayer.Status != global::CSM.API.Networking.Status.ClientStatus.Connected) { Command.SendToClients(new ClientJoiningCommand { - JoiningFinished = true, + JoiningFinished = false, JoiningUsername = player.Username }); - MultiplayerManager.Instance.UnblockGame(); + MultiplayerManager.Instance.BlockGame(player.Username); } } } diff --git a/src/csm/Commands/Handler/Internal/ClientLevelLoadedHandler.cs b/src/csm/Commands/Handler/Internal/ClientLevelLoadedHandler.cs index 12c0e7d1..d3b09f9b 100644 --- a/src/csm/Commands/Handler/Internal/ClientLevelLoadedHandler.cs +++ b/src/csm/Commands/Handler/Internal/ClientLevelLoadedHandler.cs @@ -1,4 +1,4 @@ -using CSM.API.Commands; +using CSM.API.Commands; using CSM.API.Networking; using CSM.API.Networking.Status; using CSM.Commands.Data.Internal; @@ -16,8 +16,8 @@ public ClientLevelLoadedHandler() protected override void Handle(ClientLevelLoadedCommand command) { - Player P = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers[command.SenderId]; - P.Status = ClientStatus.Connected; + global::CSM.Networking.Player P = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers[command.SenderId]; + P.Status = global::CSM.API.Networking.Status.ClientStatus.Connected; CommandInternal.Instance.SendToOtherClients(new ClientJoiningCommand { JoiningFinished = true, diff --git a/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs b/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs index 98fae33f..a99be875 100644 --- a/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs +++ b/src/csm/Commands/Handler/Internal/ConnectionRequestHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -18,7 +18,7 @@ namespace CSM.Commands.Handler.Internal { public class ConnectionRequestHandler : CommandHandler { - public static Player WorldLoadingPlayer = null; + public static global::CSM.Networking.Player WorldLoadingPlayer = null; public ConnectionRequestHandler() { @@ -141,7 +141,7 @@ public void HandleOnServer(ConnectionRequestCommand command, NetPeer peer) } // Check that no other player is currently connecting - bool clientJoining = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers.Values.Any(p => p.Status != ClientStatus.Connected); + bool clientJoining = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers.Values.Any(p => p.Status != global::CSM.API.Networking.Status.ClientStatus.Connected); if (clientJoining) { Log.Info("Connection rejected: A client is already joining."); @@ -170,9 +170,9 @@ public void HandleOnServer(ConnectionRequestCommand command, NetPeer peer) MultiplayerManager.Instance.CurrentServer.HandlePlayerConnect(newPlayer); } - public static void PrepareWorldLoad(Player newPlayer) + public static void PrepareWorldLoad(global::CSM.Networking.Player newPlayer) { - newPlayer.Status = ClientStatus.Downloading; + newPlayer.Status = global::CSM.API.Networking.Status.ClientStatus.Downloading; MultiplayerManager.Instance.BlockGame(newPlayer.Username); @@ -197,7 +197,7 @@ public static void PrepareWorldLoad(Player newPlayer) public static void AllGamesBlocked() { - Player newPlayer = WorldLoadingPlayer; + global::CSM.Networking.Player newPlayer = WorldLoadingPlayer; WorldLoadingPlayer = null; new Thread(() => diff --git a/src/csm/Commands/Handler/Internal/ConnectionResultHandler.cs b/src/csm/Commands/Handler/Internal/ConnectionResultHandler.cs index 1b44fa16..ef4a9b21 100644 --- a/src/csm/Commands/Handler/Internal/ConnectionResultHandler.cs +++ b/src/csm/Commands/Handler/Internal/ConnectionResultHandler.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using ColossalFramework.Threading; using CSM.API; @@ -23,7 +23,7 @@ public ConnectionResultHandler() protected override void Handle(ConnectionResultCommand command) { // We only want this message while connecting - if (MultiplayerManager.Instance.CurrentClient.Status != ClientStatus.Connecting) + if (MultiplayerManager.Instance.CurrentClient.Status != global::CSM.API.Networking.Status.ClientStatus.Connecting) return; // If we are allowed to connect @@ -31,8 +31,8 @@ protected override void Handle(ConnectionResultCommand command) { // Log and set that we are connected. Log.Info("Successfully connected to server. Downloading world..."); - MultiplayerManager.Instance.CurrentClient.ClientPlayer = new Player(); - MultiplayerManager.Instance.CurrentClient.Status = ClientStatus.Downloading; + MultiplayerManager.Instance.CurrentClient.ClientPlayer = new global::CSM.Networking.Player(); + MultiplayerManager.Instance.CurrentClient.Status = global::CSM.API.Networking.Status.ClientStatus.Downloading; MultiplayerManager.Instance.CurrentClient.ClientId = command.ClientId; if (CSM.IsSteamPresent) { diff --git a/src/csm/Commands/Handler/Internal/PlayerListHandler.cs b/src/csm/Commands/Handler/Internal/PlayerListHandler.cs index 4d7811bc..ed75066c 100644 --- a/src/csm/Commands/Handler/Internal/PlayerListHandler.cs +++ b/src/csm/Commands/Handler/Internal/PlayerListHandler.cs @@ -1,4 +1,4 @@ -using CSM.API.Commands; +using CSM.API.Commands; using CSM.API.Networking; using CSM.Commands.Data.Internal; using CSM.Helpers; @@ -19,10 +19,10 @@ protected override void Handle(PlayerListCommand command) } } - public override void OnClientConnect(Player player) + public override void OnClientConnect(global::CSM.API.Networking.Player player) { // Send current player list - CommandInternal.Instance.SendToClient(player, new PlayerListCommand { PlayerList = MultiplayerManager.Instance.PlayerList }); + CommandInternal.Instance.SendToClient(player as global::CSM.Networking.Player, new PlayerListCommand { PlayerList = MultiplayerManager.Instance.PlayerList }); } } } diff --git a/src/csm/Commands/Handler/Internal/RequestWorldTransferHandler.cs b/src/csm/Commands/Handler/Internal/RequestWorldTransferHandler.cs index 62dd521b..a9971957 100644 --- a/src/csm/Commands/Handler/Internal/RequestWorldTransferHandler.cs +++ b/src/csm/Commands/Handler/Internal/RequestWorldTransferHandler.cs @@ -1,4 +1,4 @@ -using CSM.API.Commands; +using CSM.API.Commands; using CSM.API.Networking; using CSM.Commands.Data.Internal; using CSM.Networking; @@ -15,7 +15,7 @@ public RequestWorldTransferHandler() protected override void Handle(RequestWorldTransferCommand command) { - Player newPlayer = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers[command.SenderId]; + global::CSM.Networking.Player newPlayer = MultiplayerManager.Instance.CurrentServer.ConnectedPlayers[command.SenderId]; ConnectionRequestHandler.PrepareWorldLoad(newPlayer); } } diff --git a/src/csm/Helpers/SpeedPauseHelper.cs b/src/csm/Helpers/SpeedPauseHelper.cs index efd0974f..2c6ec7e7 100644 --- a/src/csm/Helpers/SpeedPauseHelper.cs +++ b/src/csm/Helpers/SpeedPauseHelper.cs @@ -70,7 +70,7 @@ public static void SimulationStep() // If game is blocked or client is connecting if (MultiplayerManager.Instance.GameBlocked || (MultiplayerManager.Instance.CurrentRole == MultiplayerRole.Client && - MultiplayerManager.Instance.CurrentClient.Status != ClientStatus.Connected)) + MultiplayerManager.Instance.CurrentClient.Status != global::CSM.API.Networking.Status.ClientStatus.Connected)) { // Pause the game if it is not yet paused (on the server) and thus trigger the pause negotiation (PauseRequest) if (!SimulationManager.instance.SimulationPaused && @@ -85,7 +85,7 @@ public static void SimulationStep() } // Pause the game if it is not yet paused (on the joining/syncing client) but don't trigger a negotiation else if (!SimulationManager.instance.SimulationPaused && - MultiplayerManager.Instance.CurrentClient.Status != ClientStatus.Connected) + MultiplayerManager.Instance.CurrentClient.Status != global::CSM.API.Networking.Status.ClientStatus.Connected) { SetSpeedStateInternal(1); SetPauseStateInternal(true); @@ -380,7 +380,7 @@ private static void SendSpeedPauseResponse(int requestId) break; case MultiplayerRole.Server: numClients = 1 + MultiplayerManager.Instance.CurrentServer.ConnectedPlayers - .Count(p => p.Value.Status == ClientStatus.Connected); + .Count(p => p.Value.Status == global::CSM.API.Networking.Status.ClientStatus.Connected); break; default: numClients = 1; diff --git a/src/csm/Networking/CSMPlayer.cs b/src/csm/Networking/CSMPlayer.cs index 94a57b3c..86aceb19 100644 --- a/src/csm/Networking/CSMPlayer.cs +++ b/src/csm/Networking/CSMPlayer.cs @@ -5,7 +5,7 @@ namespace CSM.Networking { public class CSMPlayer : Player { - public NetPeer NetPeer { get; } + public new NetPeer NetPeer { get; } public CSMPlayer(NetPeer peer, string username) : base(username) { diff --git a/src/csm/Networking/Client.cs b/src/csm/Networking/Client.cs index 0451d127..f2054a15 100644 --- a/src/csm/Networking/Client.cs +++ b/src/csm/Networking/Client.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Net; using System.Net.Sockets; @@ -38,7 +38,7 @@ public class Client /// /// The current status of the client /// - public ClientStatus Status { + public global::CSM.API.Networking.Status.ClientStatus Status { get => ClientPlayer.Status; set => ClientPlayer.Status = value; } @@ -48,7 +48,7 @@ public ClientStatus Status { /// public int ClientId { get; set; } - public Player ClientPlayer { get; set; } = new Player(); + public global::CSM.Networking.Player ClientPlayer { get; set; } = new global::CSM.Networking.Player(); /// /// If the status is disconnected, this will contain diff --git a/src/csm/Networking/MultiplayerManager.cs b/src/csm/Networking/MultiplayerManager.cs index 6592fe14..3421d2dc 100644 --- a/src/csm/Networking/MultiplayerManager.cs +++ b/src/csm/Networking/MultiplayerManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using ColossalFramework.Threading; @@ -57,7 +57,7 @@ public void ProcessEvents() break; case MultiplayerRole.None: - if (CurrentClient.Status == ClientStatus.Connecting) + if (CurrentClient.Status == global::CSM.API.Networking.Status.ClientStatus.Connecting) { CurrentClient.ProcessEvents(); } @@ -78,7 +78,7 @@ public void ConnectToServer(ClientConfig config, Action callback) return; } - if (CurrentClient.Status != ClientStatus.Disconnected) + if (CurrentClient.Status != global::CSM.API.Networking.Status.ClientStatus.Disconnected) { callback.Invoke(false); return; @@ -157,7 +157,7 @@ public void StopClientOnDisconnect() public bool IsConnected() { return CurrentRole == MultiplayerRole.Server || (CurrentRole == MultiplayerRole.Client && - CurrentClient.Status == ClientStatus.Connected); + CurrentClient.Status == global::CSM.API.Networking.Status.ClientStatus.Connected); } public void BlockGameReSync() diff --git a/src/csm/Networking/Player.cs b/src/csm/Networking/Player.cs index c2102ea2..8b899d5e 100644 --- a/src/csm/Networking/Player.cs +++ b/src/csm/Networking/Player.cs @@ -1,17 +1,15 @@ -using CSM.Networking.Status; +using CSM.API.Networking.Status; using LiteNetLib; namespace CSM.Networking { - public class Player + public class Player : global::CSM.API.Networking.Player { - public string Username { get; set; } + // Username is inherited public NetPeer NetPeer { get; set; } - public long Latency { get; set; } - - public ClientStatus Status { get; set; } + // Status and Latency are inherited public Player(NetPeer peer, string username) { diff --git a/src/csm/Networking/Server.cs b/src/csm/Networking/Server.cs index f7130ceb..f3a64f22 100644 --- a/src/csm/Networking/Server.cs +++ b/src/csm/Networking/Server.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -148,7 +148,7 @@ public bool StartServer(ServerConfig serverConfig) // Initialize host player _hostPlayer = new CSMPlayer(Config.Username); - _hostPlayer.Status = ClientStatus.Connected; + _hostPlayer.Status = global::CSM.API.Networking.Status.ClientStatus.Connected; MultiplayerManager.Instance.PlayerList.Add(_hostPlayer.Username); // Set the steam presence 'connect' key. This allows users to click "Join Game" within the steam overlay. @@ -380,7 +380,7 @@ private void ListenerOnConnectionRequestEvent(ConnectionRequest request) request.AcceptIfKey("CSM"); } - public void HandlePlayerConnect(Player player) + public void HandlePlayerConnect(global::CSM.Networking.Player player) { Log.Info($"Player {player.Username} has connected!"); Chat.Instance.PrintGameMessage($"Player {player.Username} has connected!"); diff --git a/src/csm/Networking/Status/ClientStatus.cs b/src/csm/Networking/Status/ClientStatus.cs deleted file mode 100644 index 3eda74c0..00000000 --- a/src/csm/Networking/Status/ClientStatus.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace CSM.Networking.Status -{ - /// - /// Different client connection states - /// - public enum ClientStatus - { - /// - /// The client is not connected to a server - /// and will not process any network events. - /// - Disconnected, - - /// - /// The client is trying to connect to the server, this phase - /// can take up to 30 seconds. - /// - Connecting, - - /// - /// The client is connected and downloading the save game from - /// the server - /// - Downloading, - - /// - /// The client is connected, has downloaded the save game from - /// the server and now loads the level - /// - Loading, - - /// - /// The client is connected to the server and is - /// transmitting information. - /// - Connected - } -}