diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index b00f0f21..85e275ed 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -35,7 +35,7 @@ jobs: - run: mkdir -p output/Multiplayer - name: Move files - run: mv About/ Assemblies/ Defs/ Languages/ Textures/ output/Multiplayer + run: mv About/ Assemblies/ AssembliesCustom/ Defs/ Languages/ Textures/ output/Multiplayer - name: Upload Mod Artifacts uses: actions/upload-artifact@v2 diff --git a/.gitignore b/.gitignore index 7f1f6ede..97724ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -261,6 +261,7 @@ __pycache__/ *.pyc /Assemblies +/AssembliesCustom /Source/Publicise-1.0.0.zip /Source/publicise_hash.txt /Source/Publicise.exe diff --git a/Assemblies/1I18N.dll b/Assemblies/1I18N.dll deleted file mode 100644 index ab732539..00000000 Binary files a/Assemblies/1I18N.dll and /dev/null differ diff --git a/Assemblies/2I18N.West.dll b/Assemblies/2I18N.West.dll deleted file mode 100644 index 28dac5ad..00000000 Binary files a/Assemblies/2I18N.West.dll and /dev/null differ diff --git a/Languages b/Languages index fdad1f33..a79f017d 160000 --- a/Languages +++ b/Languages @@ -1 +1 @@ -Subproject commit fdad1f33992e7cf74d4dc3c9d0c14857806e5004 +Subproject commit a79f017dfbd26e7648ff8e5c7b0d67a8246deb89 diff --git a/Source/.dockerignore b/Source/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/Source/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Source/API/AdhocAPI.cs b/Source/API/AdhocAPI.cs deleted file mode 100644 index 954d1d53..00000000 --- a/Source/API/AdhocAPI.cs +++ /dev/null @@ -1,8 +0,0 @@ -using RimWorld; - -namespace Multiplayer.API; - -public static class AdhocAPI -{ - public static Faction RealPlayerFaction => Client.Multiplayer.RealPlayerFaction; -} diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 0262fb75..a135ae04 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -1,5 +1,3 @@ -extern alias zip; - using HarmonyLib; using Multiplayer.Common; using RimWorld; @@ -15,7 +13,6 @@ namespace Multiplayer.Client { - public class AsyncTimeComp : IExposable, ITickable { public static Map tickingMap; @@ -59,15 +56,16 @@ public float TickRateMultiplier(TimeSpeed speed) } } - public TimeSpeed TimeSpeed + public TimeSpeed DesiredTimeSpeed => timeSpeedInt; + + public void SetDesiredTimeSpeed(TimeSpeed speed) { - get => timeSpeedInt; - set => timeSpeedInt = value; + timeSpeedInt = speed; } - public bool Paused => this.ActualRateMultiplier(TimeSpeed) == 0f; + public bool Paused => this.ActualRateMultiplier(DesiredTimeSpeed) == 0f; - public float RealTimeToTickThrough { get; set; } + public float TimeToTickThrough { get; set; } public Queue Cmds { get => cmds; } @@ -159,7 +157,7 @@ public void TickMapTrading() // These are normally called in Map.MapUpdate() and react to changes in the game state even when the game is paused (not ticking) // Update() methods are not deterministic, but in multiplayer all game state changes (which don't happen during ticking) happen in commands - // Thus these methods can be moved to Tick() and ExecuteCmd() + // Thus these methods can be moved to Tick() and ExecuteCmd() by way of this method public void UpdateManagers() { map.regionGrid.UpdateClean(); @@ -226,7 +224,10 @@ public void ExposeData() public void FinalizeInit() { - cmds = new Queue(Multiplayer.session.dataSnapshot.mapCmds.GetValueSafe(map.uniqueID) ?? new List()); + cmds = new Queue( + Multiplayer.session.dataSnapshot?.MapCmds.GetValueSafe(map.uniqueID) ?? new List() + ); + Log.Message($"Init map with cmds {cmds.Count}"); } @@ -241,7 +242,6 @@ public void ExecuteCmd(ScheduledCommand cmd) MpContext context = data.MpContext(); - var updateWorldTime = false; keepTheMap = false; var prevMap = Current.Game.CurrentMap; Current.Game.currentMapIndex = (sbyte)map.Index; @@ -284,8 +284,7 @@ public void ExecuteCmd(ScheduledCommand cmd) if (cmdType == CommandType.MapTimeSpeed && Multiplayer.GameComp.asyncTime) { TimeSpeed speed = (TimeSpeed)data.ReadByte(); - TimeSpeed = speed; - updateWorldTime = true; + SetDesiredTimeSpeed(speed); MpLog.Debug("Set map time speed " + speed); } @@ -334,8 +333,6 @@ public void ExecuteCmd(ScheduledCommand cmd) if (!keepTheMap) TrySetCurrentMap(prevMap); - Multiplayer.WorldComp.UpdateTimeSpeed(); // In case a letter pauses the map - keepTheMap = false; Multiplayer.game.sync.TryAddCommandRandomState(randState); @@ -479,12 +476,6 @@ public void QuestManagerTickAsyncTime() MultiplayerAsyncQuest.TickMapQuests(this); } - - public void TrySetPrevTimeSpeed(TimeSpeed speed) - { - if (prevTime != null) - prevTime = prevTime.Value with { speed = speed }; - } } public enum DesignatorMode : byte diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index 2330d755..526e7c5d 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -21,7 +21,6 @@ static IEnumerable TargetMethods() } [HarmonyPatch(typeof(Autosaver), nameof(Autosaver.AutosaverTick))] - static class DisableAutosaver { static bool Prefix() => Multiplayer.Client == null; @@ -135,7 +134,7 @@ static void Postfix(ref float __result) var map = PreDrawCalcMarker.calculating.Map ?? Find.CurrentMap; var asyncTime = map.AsyncTime(); - var timeSpeed = Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : asyncTime.TimeSpeed; + var timeSpeed = Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : asyncTime.DesiredTimeSpeed; __result = TickPatch.Simulating ? 6 : asyncTime.ActualRateMultiplier(timeSpeed); } @@ -150,78 +149,12 @@ static void Postfix(ref bool __result) if (WorldRendererUtility.WorldRenderedNow) return; var asyncTime = Find.CurrentMap.AsyncTime(); - var timeSpeed = Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : asyncTime.TimeSpeed; + var timeSpeed = Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : asyncTime.DesiredTimeSpeed; __result = asyncTime.ActualRateMultiplier(timeSpeed) == 0; } } - [HarmonyPatch] - public class StorytellerTickPatch - { - public static bool updating; - - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(Storyteller), nameof(Storyteller.StorytellerTick)); - yield return AccessTools.Method(typeof(StoryWatcher), nameof(StoryWatcher.StoryWatcherTick)); - } - - static bool Prefix() - { - updating = true; - return Multiplayer.Client == null || Multiplayer.Ticking; - } - - static void Postfix() => updating = false; - } - - [HarmonyPatch(typeof(Storyteller))] - [HarmonyPatch(nameof(Storyteller.AllIncidentTargets), MethodType.Getter)] - public class StorytellerTargetsPatch - { - static void Postfix(List __result) - { - if (Multiplayer.Client == null) return; - - if (Multiplayer.MapContext != null) - { - __result.Clear(); - __result.Add(Multiplayer.MapContext); - } - else if (MultiplayerWorldComp.tickingWorld) - { - __result.Clear(); - - foreach (var caravan in Find.WorldObjects.Caravans) - if (caravan.IsPlayerControlled) - __result.Add(caravan); - - __result.Add(Find.World); - } - } - } - - // The MP Mod's ticker calls Storyteller.StorytellerTick() on both the World and each Map, each tick - // This patch aims to ensure each "spawn raid" Quest is only triggered once, to prevent 2x or 3x sized raids - [HarmonyPatch(typeof(Quest), nameof(Quest.PartsListForReading), MethodType.Getter)] - public class QuestPartsListForReadingPatch - { - static void Postfix(ref List __result) - { - if (StorytellerTickPatch.updating) - { - __result = __result.Where(questPart => { - if (questPart is QuestPart_ThreatsGenerator questPartThreatsGenerator) - { - return questPartThreatsGenerator.mapParent?.Map == Multiplayer.MapContext; - } - return true; - }).ToList(); - } - } - } - [HarmonyPatch(typeof(TickManager), nameof(TickManager.Notify_GeneratedPotentiallyHostileMap))] static class GeneratedHostileMapPatch { @@ -236,56 +169,6 @@ static void Postfix() } } - [HarmonyPatch(typeof(StorytellerUtility), nameof(StorytellerUtility.DefaultParmsNow))] - static class MapContextIncidentParms - { - static void Prefix(IIncidentTarget target, ref Map __state) - { - // This may be running inside a context already - if (AsyncTimeComp.tickingMap != null) - return; - - if (MultiplayerWorldComp.tickingWorld && target is Map map) - { - AsyncTimeComp.tickingMap = map; - map.AsyncTime().PreContext(); - __state = map; - } - } - - static void Postfix(Map __state) - { - if (__state != null) - { - __state.AsyncTime().PostContext(); - AsyncTimeComp.tickingMap = null; - } - } - } - - [HarmonyPatch(typeof(IncidentWorker), nameof(IncidentWorker.TryExecute))] - static class MapContextIncidentExecute - { - static void Prefix(IncidentParms parms, ref Map __state) - { - if (MultiplayerWorldComp.tickingWorld && parms.target is Map map) - { - AsyncTimeComp.tickingMap = map; - map.AsyncTime().PreContext(); - __state = map; - } - } - - static void Postfix(Map __state) - { - if (__state != null) - { - __state.AsyncTime().PostContext(); - AsyncTimeComp.tickingMap = null; - } - } - } - [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string))] static class ReceiveLetterPause { @@ -302,14 +185,14 @@ static IEnumerable Transpiler(IEnumerable inst } } - static AutomaticPauseMode AutomaticPauseMode() + private static AutomaticPauseMode AutomaticPauseMode() { return Multiplayer.Client != null ? (AutomaticPauseMode) Multiplayer.GameComp.pauseOnLetter : Prefs.AutomaticPauseMode; } - static void PauseOnLetter(TickManager manager) + private static void PauseOnLetter(TickManager manager) { if (Multiplayer.Client == null) { @@ -319,21 +202,15 @@ static void PauseOnLetter(TickManager manager) if (Multiplayer.GameComp.asyncTime) { - var tickable = (ITickable)Multiplayer.MapContext.AsyncTime() ?? Multiplayer.WorldComp; - tickable.TimeSpeed = TimeSpeed.Paused; + var tickable = (ITickable)Multiplayer.MapContext.AsyncTime() ?? Multiplayer.AsyncWorldTime; + tickable.SetDesiredTimeSpeed(TimeSpeed.Paused); Multiplayer.GameComp.ResetAllTimeVotes(tickable.TickableId); - if (tickable is AsyncTimeComp comp) - comp.TrySetPrevTimeSpeed(TimeSpeed.Paused); } else { - Multiplayer.WorldComp.SetTimeEverywhere(TimeSpeed.Paused); + Multiplayer.AsyncWorldTime.SetTimeEverywhere(TimeSpeed.Paused); foreach (var tickable in TickPatch.AllTickables) - { Multiplayer.GameComp.ResetAllTimeVotes(tickable.TickableId); - if (tickable is AsyncTimeComp comp) - comp.TrySetPrevTimeSpeed(TimeSpeed.Paused); - } } } } diff --git a/Source/Client/AsyncTime/AsyncWorldTimeComp.cs b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs new file mode 100644 index 00000000..c9a6a421 --- /dev/null +++ b/Source/Client/AsyncTime/AsyncWorldTimeComp.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Client.Comp; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Patches; +using Multiplayer.Client.Saving; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.AsyncTime; + +public class AsyncWorldTimeComp : IExposable, ITickable +{ + public static bool tickingWorld; + public static bool executingCmdWorld; + private TimeSpeed timeSpeedInt; + + public float TimeToTickThrough { get; set; } + + public float TickRateMultiplier(TimeSpeed speed) + { + if (Multiplayer.GameComp.asyncTime) + { + var enforcePause = Multiplayer.WorldComp.splitSession != null || + AsyncTimeComp.pauseLocks.Any(x => x(null)); + + if (enforcePause) + return 0f; + } + + return speed switch + { + TimeSpeed.Paused => 0f, + TimeSpeed.Normal => 1f, + TimeSpeed.Fast => 3f, + TimeSpeed.Superfast => 6f, + TimeSpeed.Ultrafast => 15f, + _ => -1f + }; + } + + // Run at the speed of the fastest map + public TimeSpeed DesiredTimeSpeed => Find.Maps.Select(m => m.AsyncTime()) + .Where(a => a.ActualRateMultiplier(a.DesiredTimeSpeed) != 0f) + .Max(a => a?.DesiredTimeSpeed) ?? timeSpeedInt; + + public void SetDesiredTimeSpeed(TimeSpeed speed) + { + timeSpeedInt = speed; + } + + public Queue Cmds => cmds; + public Queue cmds = new(); + + public int TickableId => -1; + + public World world; + public ulong randState = 2; + + public int worldTicks; + + public AsyncWorldTimeComp(World world) + { + this.world = world; + } + + public void ExposeData() + { + var timer = TickPatch.Timer; + Scribe_Values.Look(ref timer, "timer"); + TickPatch.SetTimer(timer); + + Scribe_Values.Look(ref timeSpeedInt, "timeSpeed"); + Scribe_Custom.LookULong(ref randState, "randState", 2); + + TimeSpeed timeSpeed = Find.TickManager.CurTimeSpeed; + Scribe_Values.Look(ref timeSpeed, "timeSpeed"); + if (Scribe.mode == LoadSaveMode.LoadingVars) + Find.TickManager.CurTimeSpeed = timeSpeed; + + if (Scribe.mode == LoadSaveMode.LoadingVars) + Multiplayer.game.worldComp = new MultiplayerWorldComp(world); + + Multiplayer.game.worldComp.ExposeData(); + + if (Scribe.mode == LoadSaveMode.LoadingVars) + worldTicks = Find.TickManager.TicksGame; + } + + public void Tick() + { + tickingWorld = true; + PreContext(); + + try + { + Find.TickManager.DoSingleTick(); + worldTicks++; + Multiplayer.WorldComp.TickWorldTrading(); + + if (ModsConfig.BiotechActive) + { + // Vanilla puts those into a separate try/catch blocks + try + { + CompDissolutionEffect_Goodwill.WorldUpdate(); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + try + { + CompDissolutionEffect_Pollution.WorldUpdate(); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + } + finally + { + PostContext(); + tickingWorld = false; + + Multiplayer.game.sync.TryAddWorldRandomState(randState); + } + } + + public void PreContext() + { + Find.TickManager.CurTimeSpeed = DesiredTimeSpeed; + UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; + Rand.PushState(); + Rand.StateCompressed = randState; + } + + public void PostContext() + { + randState = Rand.StateCompressed; + Rand.PopState(); + UniqueIdsPatch.CurrentBlock = null; + } + + public void ExecuteCmd(ScheduledCommand cmd) + { + CommandType cmdType = cmd.type; + LoggingByteReader data = new LoggingByteReader(cmd.data); + data.Log.Node($"{cmdType} Global"); + + executingCmdWorld = true; + TickPatch.currentExecutingCmdIssuedBySelf = cmd.issuedBySelf && !TickPatch.Simulating; + + PreContext(); + Extensions.PushFaction(null, cmd.GetFaction()); + + bool prevDevMode = Prefs.data.devMode; + var prevGodMode = DebugSettings.godMode; + Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId)?.SetContext(); + + var randCalls1 = DeferredStackTracing.randCalls; + + try + { + if (cmdType == CommandType.Sync) + { + var handler = SyncUtil.HandleCmd(data); + data.Log.current.text = handler.ToString(); + } + + if (cmdType == CommandType.DebugTools) + { + MpDebugTools.HandleCmd(data); + } + + if (cmdType == CommandType.GlobalTimeSpeed) + { + HandleTimeSpeed(cmd, data); + } + + if (cmdType == CommandType.TimeSpeedVote) + { + HandleTimeVote(cmd, data); + } + + if (cmdType == CommandType.PauseAll) + { + SetTimeEverywhere(TimeSpeed.Paused); + } + + if (cmdType == CommandType.SetupFaction) + { + HandleSetupFaction(cmd, data); + } + + if (cmdType == CommandType.CreateJoinPoint) + { + LongEventHandler.QueueLongEvent(CreateJoinPoint, "MpCreatingJoinPoint", false, null); + } + + if (cmdType == CommandType.InitPlayerData) + { + var playerId = data.ReadInt32(); + var canUseDevMode = data.ReadBool(); + Multiplayer.GameComp.playerData[playerId] = new PlayerData { canUseDevMode = canUseDevMode }; + } + } + catch (Exception e) + { + Log.Error($"World cmd exception ({cmdType}): {e}"); + } + finally + { + DebugSettings.godMode = prevGodMode; + Prefs.data.devMode = prevDevMode; + + MpLog.Debug($"rand calls {DeferredStackTracing.randCalls - randCalls1}"); + MpLog.Debug("rand state " + Rand.StateCompressed); + + Extensions.PopFaction(); + PostContext(); + TickPatch.currentExecutingCmdIssuedBySelf = false; + executingCmdWorld = false; + + Multiplayer.game.sync.TryAddCommandRandomState(randState); + + if (cmdType != CommandType.GlobalTimeSpeed) + Multiplayer.ReaderLog.AddCurrentNode(data); + } + } + + private static void CreateJoinPoint() + { + Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); + + if (!TickPatch.Simulating && !Multiplayer.IsReplay && + (Multiplayer.LocalServer != null || Multiplayer.arbiterInstance)) + SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true); + } + + public void SetTimeEverywhere(TimeSpeed speed) + { + foreach (var map in Find.Maps) + map.AsyncTime().SetDesiredTimeSpeed(speed); + SetDesiredTimeSpeed(speed); + } + + public static float lastSpeedChange; + + private void HandleTimeSpeed(ScheduledCommand cmd, ByteReader data) + { + TimeSpeed speed = (TimeSpeed)data.ReadByte(); + SetDesiredTimeSpeed(speed); + + if (!Multiplayer.GameComp.asyncTime) + { + SetTimeEverywhere(speed); + + if (!cmd.issuedBySelf) + lastSpeedChange = Time.realtimeSinceStartup; + } + + MpLog.Debug($"Set world speed {speed} {TickPatch.Timer} {Find.TickManager.TicksGame}"); + } + + private void HandleTimeVote(ScheduledCommand cmd, ByteReader data) + { + TimeVote vote = (TimeVote)data.ReadByte(); + int tickableId = data.ReadInt32(); + + // Update the vote + if (vote >= TimeVote.ResetTickable) + Multiplayer.GameComp.playerData.Do(p => p.Value.SetTimeVote(tickableId, vote)); + else if (Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId) is { } playerData) + playerData.SetTimeVote(tickableId, vote); + + // Update the time speed + if (!Multiplayer.GameComp.asyncTime || vote == TimeVote.ResetGlobal) + SetTimeEverywhere(Multiplayer.GameComp.GetLowestTimeVote(TickableId)); + else if (TickPatch.TickableById(tickableId) is { } tickable) + tickable.SetDesiredTimeSpeed(Multiplayer.GameComp.GetLowestTimeVote(tickableId)); + } + + private void HandleSetupFaction(ScheduledCommand command, ByteReader data) + { + int factionId = data.ReadInt32(); + Faction faction = Find.FactionManager.GetById(factionId); + + if (faction == null) + { + faction = new Faction + { + loadID = factionId, + def = FactionDefOf.PlayerColony, + Name = "Multiplayer faction", + }; + + Find.FactionManager.Add(faction); + + foreach (Faction current in Find.FactionManager.AllFactionsListForReading) + { + if (current == faction) continue; + current.TryMakeInitialRelationsWith(faction); + } + + Multiplayer.WorldComp.factionData[factionId] = FactionWorldData.New(factionId); + + MpLog.Log($"New faction {faction.GetUniqueLoadID()}"); + } + } + + public void FinalizeInit() + { + Multiplayer.game.SetThingMakerSeed((int)(randState >> 32)); + } +} diff --git a/Source/Client/AsyncTime/ITickable.cs b/Source/Client/AsyncTime/ITickable.cs index 7ecb78d3..3b9dd6f4 100644 --- a/Source/Client/AsyncTime/ITickable.cs +++ b/Source/Client/AsyncTime/ITickable.cs @@ -1,4 +1,3 @@ -extern alias zip; using Multiplayer.Common; using System.Collections.Generic; using Verse; @@ -9,11 +8,13 @@ public interface ITickable { int TickableId { get; } - float RealTimeToTickThrough { get; set; } + Queue Cmds { get; } - TimeSpeed TimeSpeed { get; set; } + float TimeToTickThrough { get; set; } - Queue Cmds { get; } + TimeSpeed DesiredTimeSpeed { get; } + + void SetDesiredTimeSpeed(TimeSpeed speed); float TickRateMultiplier(TimeSpeed speed); diff --git a/Source/Client/AsyncTime/SetMapTime.cs b/Source/Client/AsyncTime/SetMapTime.cs index c6afd26c..9884d9db 100644 --- a/Source/Client/AsyncTime/SetMapTime.cs +++ b/Source/Client/AsyncTime/SetMapTime.cs @@ -192,12 +192,12 @@ public static TimeSnapshot Current() TimeSnapshot prev = Current(); - var man = Find.TickManager; - var comp = map.AsyncTime(); + var tickManager = Find.TickManager; + var mapComp = map.AsyncTime(); - man.ticksGameInt = comp.mapTicks; - man.slower = comp.slower; - man.CurTimeSpeed = comp.TimeSpeed; + tickManager.ticksGameInt = mapComp.mapTicks; + tickManager.slower = mapComp.slower; + tickManager.CurTimeSpeed = mapComp.DesiredTimeSpeed; return prev; } diff --git a/Source/Client/AsyncTime/StorytellerPatches.cs b/Source/Client/AsyncTime/StorytellerPatches.cs new file mode 100644 index 00000000..ce445145 --- /dev/null +++ b/Source/Client/AsyncTime/StorytellerPatches.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using RimWorld; +using Verse; + +namespace Multiplayer.Client.AsyncTime; + +public class StorytellerPatches +{ + [HarmonyPatch] + public class StorytellerTickPatch + { + public static bool updating; + + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(Storyteller), nameof(Storyteller.StorytellerTick)); + yield return AccessTools.Method(typeof(StoryWatcher), nameof(StoryWatcher.StoryWatcherTick)); + } + + static bool Prefix() + { + updating = true; + return Multiplayer.Client == null || Multiplayer.Ticking; + } + + static void Postfix() => updating = false; + } + + [HarmonyPatch(typeof(Storyteller))] + [HarmonyPatch(nameof(Storyteller.AllIncidentTargets), MethodType.Getter)] + public class StorytellerTargetsPatch + { + static void Postfix(List __result) + { + if (Multiplayer.Client == null) return; + + if (Multiplayer.MapContext != null) + { + __result.Clear(); + __result.Add(Multiplayer.MapContext); + } + else if (AsyncWorldTimeComp.tickingWorld) + { + __result.Clear(); + + foreach (var caravan in Find.WorldObjects.Caravans) + if (caravan.IsPlayerControlled) + __result.Add(caravan); + + __result.Add(Find.World); + } + } + } + + // The MP Mod's ticker calls Storyteller.StorytellerTick() on both the World and each Map, each tick + // This patch aims to ensure each "spawn raid" Quest is only triggered once, to prevent 2x or 3x sized raids + [HarmonyPatch(typeof(Quest), nameof(Quest.PartsListForReading), MethodType.Getter)] + public class QuestPartsListForReadingPatch + { + static void Postfix(ref List __result) + { + if (StorytellerTickPatch.updating) + { + __result = __result.Where(questPart => { + if (questPart is QuestPart_ThreatsGenerator questPartThreatsGenerator) + { + return questPartThreatsGenerator.mapParent?.Map == Multiplayer.MapContext; + } + return true; + }).ToList(); + } + } + } + + [HarmonyPatch(typeof(StorytellerUtility), nameof(StorytellerUtility.DefaultParmsNow))] + static class MapContextIncidentParms + { + static void Prefix(IIncidentTarget target, ref Map __state) + { + // This may be running inside a context already + if (AsyncTimeComp.tickingMap != null) + return; + + if (AsyncWorldTimeComp.tickingWorld && target is Map map) + { + AsyncTimeComp.tickingMap = map; + map.AsyncTime().PreContext(); + __state = map; + } + } + + static void Postfix(Map __state) + { + if (__state != null) + { + __state.AsyncTime().PostContext(); + AsyncTimeComp.tickingMap = null; + } + } + } + + [HarmonyPatch(typeof(IncidentWorker), nameof(IncidentWorker.TryExecute))] + static class MapContextIncidentExecute + { + static void Prefix(IncidentParms parms, ref Map __state) + { + if (AsyncWorldTimeComp.tickingWorld && parms.target is Map map) + { + AsyncTimeComp.tickingMap = map; + map.AsyncTime().PreContext(); + __state = map; + } + } + + static void Postfix(Map __state) + { + if (__state != null) + { + __state.AsyncTime().PostContext(); + AsyncTimeComp.tickingMap = null; + } + } + } +} diff --git a/Source/Client/AsyncTime/TimeControlUI.cs b/Source/Client/AsyncTime/TimeControlUI.cs index 69fad91c..4a88713c 100644 --- a/Source/Client/AsyncTime/TimeControlUI.cs +++ b/Source/Client/AsyncTime/TimeControlUI.cs @@ -23,14 +23,14 @@ public static class TimeControlPatch private static ITickable Tickable => !WorldRendererUtility.WorldRenderedNow && Multiplayer.GameComp.asyncTime ? Find.CurrentMap.AsyncTime() - : Multiplayer.WorldComp; + : Multiplayer.AsyncWorldTime; private static TimeVote CurTimeSpeedGame => - (TimeVote)(Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : Tickable.TimeSpeed); + (TimeVote)(Multiplayer.IsReplay ? TickPatch.replayTimeSpeed : Tickable.DesiredTimeSpeed); private static TimeVote? CurTimeSpeedUI => Multiplayer.IsReplay ? (TimeVote?)TickPatch.replayTimeSpeed : - Multiplayer.GameComp.IsLowestWins ? Multiplayer.GameComp.LocalPlayerDataOrNull?.GetTimeVoteOrNull(Tickable.TickableId) : (TimeVote?)Tickable.TimeSpeed; + Multiplayer.GameComp.IsLowestWins ? Multiplayer.GameComp.LocalPlayerDataOrNull?.GetTimeVoteOrNull(Tickable.TickableId) : (TimeVote?)Tickable.DesiredTimeSpeed; static IEnumerable Transpiler(IEnumerable insts) { @@ -80,7 +80,7 @@ private static void DoTimeControlsGUI(Rect timerRect) )); if (Widgets.ButtonInvisible(descRect, false) && ShouldReset && Multiplayer.LocalServer != null) - SendTimeVote(TimeVote.Reset); + SendTimeVote(TimeVote.ResetTickable); } foreach (var speed in GameSpeeds) @@ -89,7 +89,7 @@ private static void DoTimeControlsGUI(Rect timerRect) { // todo Move the host check to the server? if (ShouldReset && Multiplayer.LocalServer != null) - SendTimeVote(TimeVote.Reset); + SendTimeVote(TimeVote.ResetTickable); else if (speed == TimeVote.Paused) SendTimeVote(TogglePaused(CurTimeSpeedUI)); else @@ -127,12 +127,12 @@ private static void DoTimeControlsHotkeys() return; // Prevent multiple players changing the speed too quickly - if (Time.realtimeSinceStartup - MultiplayerWorldComp.lastSpeedChange < 0.4f) + if (Time.realtimeSinceStartup - AsyncWorldTimeComp.lastSpeedChange < 0.4f) return; if (KeyBindingDefOf.TogglePause.KeyDownEvent) { - SendTimeVote(ShouldReset ? TimeVote.PlayerReset : TogglePaused(CurTimeSpeedUI)); + SendTimeVote(ShouldReset ? TimeVote.PlayerResetTickable : TogglePaused(CurTimeSpeedUI)); if (ShouldReset) prePauseTimeSpeed = null; } @@ -245,7 +245,7 @@ private static void SendTimeVote(TimeVote vote) if (Event.current.type == EventType.KeyDown) Event.current.Use(); - TimeControls.PlaySoundOf(vote >= TimeVote.PlayerReset ? TimeSpeed.Paused : (TimeSpeed)vote); + TimeControls.PlaySoundOf(vote >= TimeVote.PlayerResetTickable ? TimeSpeed.Paused : (TimeSpeed)vote); } private static void DoSingleTick() @@ -254,11 +254,11 @@ private static void DoSingleTick() { var replaySpeed = TickPatch.replayTimeSpeed; TickPatch.replayTimeSpeed = TimeSpeed.Normal; - TickPatch.accumulator = 1; + TickPatch.ticksToRun = 1; - TickPatch.Tick(out _); + TickPatch.DoUpdate(out _); - TickPatch.accumulator = 0; + TickPatch.ticksToRun = 0; TickPatch.replayTimeSpeed = replaySpeed; } } @@ -311,7 +311,7 @@ static void DrawButtons() if (curGroup == entry.group) continue; ITickable entryTickable = entry.map?.AsyncTime(); - if (entryTickable == null) entryTickable = Multiplayer.WorldComp; + if (entryTickable == null) entryTickable = Multiplayer.AsyncWorldTime; Rect groupBar = bar.drawer.GroupFrameRect(entry.group); float drawXPos = groupBar.x; @@ -443,7 +443,7 @@ static void Prefix(MainButtonWorker __instance, Rect rect, ref Rect? __state) __state = button; if (Event.current.type is EventType.MouseDown or EventType.MouseUp) - MpTimeControls.TimeControlButton(__state.Value, ColonistBarTimeControl.normalBgColor, Multiplayer.WorldComp); + MpTimeControls.TimeControlButton(__state.Value, ColonistBarTimeControl.normalBgColor, Multiplayer.AsyncWorldTime); } static void Postfix(MainButtonWorker __instance, Rect? __state) @@ -451,7 +451,7 @@ static void Postfix(MainButtonWorker __instance, Rect? __state) if (__state == null) return; if (Event.current.type == EventType.Repaint) - MpTimeControls.TimeControlButton(__state.Value, ColonistBarTimeControl.normalBgColor, Multiplayer.WorldComp); + MpTimeControls.TimeControlButton(__state.Value, ColonistBarTimeControl.normalBgColor, Multiplayer.AsyncWorldTime); } } @@ -465,7 +465,7 @@ public static void TimeIndicateBlockingPause(Rect button, Color bgColor) public static void TimeControlButton(Rect button, Color bgColor, ITickable tickable) { - int speed = (int)tickable.TimeSpeed; + int speed = (int)tickable.DesiredTimeSpeed; if (tickable.ActualRateMultiplier(TimeSpeed.Normal) == 0f) speed = 0; @@ -480,7 +480,7 @@ public static void TimeControlButton(Rect button, Color bgColor, ITickable ticka public static void SendTimeChange(ITickable tickable, TimeSpeed newSpeed) { - if (tickable is MultiplayerWorldComp) + if (tickable is AsyncWorldTimeComp) Multiplayer.Client.SendCommand(CommandType.GlobalTimeSpeed, ScheduledCommand.Global, (byte)newSpeed); else if (tickable is AsyncTimeComp comp) Multiplayer.Client.SendCommand(CommandType.MapTimeSpeed, comp.map.uniqueID, (byte)newSpeed); diff --git a/Source/Client/Comp/MultiplayerGameComp.cs b/Source/Client/Comp/Game/MultiplayerGameComp.cs similarity index 70% rename from Source/Client/Comp/MultiplayerGameComp.cs rename to Source/Client/Comp/Game/MultiplayerGameComp.cs index 68848167..60abf1e5 100644 --- a/Source/Client/Comp/MultiplayerGameComp.cs +++ b/Source/Client/Comp/Game/MultiplayerGameComp.cs @@ -73,46 +73,7 @@ public TimeSpeed GetLowestTimeVote(int tickableId, bool excludePaused = false) public void ResetAllTimeVotes(int tickableId) { - playerData.Values.Do(p => p.SetTimeVote(tickableId, TimeVote.PlayerReset)); - } - } - - public class PlayerData : ISynchronizable - { - public bool canUseDevMode; - public bool godMode; - private Dictionary timeVotes = new(); - - public Dictionary AllTimeVotes => timeVotes; - - public void Sync(SyncWorker sync) - { - sync.Bind(ref canUseDevMode); - sync.Bind(ref godMode); - sync.Bind(ref timeVotes); - } - - public void SetContext() - { - Prefs.data.devMode = canUseDevMode; - DebugSettings.godMode = godMode; - } - - public void SetTimeVote(int tickableId, TimeVote vote) - { - if (vote is TimeVote.Reset or TimeVote.PlayerReset) - timeVotes.Remove(tickableId); - else if (vote is TimeVote.ResetAll or TimeVote.PlayerResetAll) - timeVotes.Clear(); - else - timeVotes[tickableId] = vote; - } - - public TimeVote? GetTimeVoteOrNull(int tickableId) - { - if (timeVotes.TryGetValue(tickableId, out var vote)) - return vote; - return null; + playerData.Values.Do(p => p.SetTimeVote(tickableId, TimeVote.PlayerResetTickable)); } } } diff --git a/Source/Client/Comp/Game/PlayerData.cs b/Source/Client/Comp/Game/PlayerData.cs new file mode 100644 index 00000000..9b9d6d50 --- /dev/null +++ b/Source/Client/Comp/Game/PlayerData.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Multiplayer.API; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client.Comp; + +public class PlayerData : ISynchronizable +{ + public bool canUseDevMode; + public bool godMode; + private Dictionary timeVotes = new(); + + public Dictionary AllTimeVotes => timeVotes; + + public void Sync(SyncWorker sync) + { + sync.Bind(ref canUseDevMode); + sync.Bind(ref godMode); + sync.Bind(ref timeVotes); + } + + public void SetContext() + { + Prefs.data.devMode = canUseDevMode; + DebugSettings.godMode = godMode; + } + + public void SetTimeVote(int tickableId, TimeVote vote) + { + if (vote is TimeVote.ResetTickable or TimeVote.PlayerResetTickable) + timeVotes.Remove(tickableId); + else if (vote is TimeVote.ResetGlobal or TimeVote.PlayerResetGlobal) + timeVotes.Clear(); + else + timeVotes[tickableId] = vote; + } + + public TimeVote? GetTimeVoteOrNull(int tickableId) + { + if (timeVotes.TryGetValue(tickableId, out var vote)) + return vote; + return null; + } +} diff --git a/Source/Client/Comp/Map/CustomFactionMapData.cs b/Source/Client/Comp/Map/CustomFactionMapData.cs new file mode 100644 index 00000000..694858ec --- /dev/null +++ b/Source/Client/Comp/Map/CustomFactionMapData.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.Client; + +public class CustomFactionMapData : IExposable +{ + public Map map; + public int factionId; + + public HashSet claimed = new(); + public HashSet unforbidden = new(); + + // Loading ctor + public CustomFactionMapData(Map map) + { + this.map = map; + } + + public void ExposeData() + { + Scribe_Values.Look(ref factionId, "factionId"); + Scribe_Collections.Look(ref unforbidden, "unforbidden", LookMode.Reference); + } + + public static CustomFactionMapData New(int factionId, Map map) + { + return new CustomFactionMapData(map) { factionId = factionId }; + } +} diff --git a/Source/Client/Comp/Map/ExposeActor.cs b/Source/Client/Comp/Map/ExposeActor.cs new file mode 100644 index 00000000..db59388a --- /dev/null +++ b/Source/Client/Comp/Map/ExposeActor.cs @@ -0,0 +1,29 @@ +using System; +using Verse; + +namespace Multiplayer.Client; + +public class ExposeActor : IExposable +{ + private Action action; + + private ExposeActor(Action action) + { + this.action = action; + } + + public void ExposeData() + { + if (Scribe.mode == LoadSaveMode.PostLoadInit) + action(); + } + + // This depends on the fact that the implementation of HashSet RimWorld currently uses + // "preserves" insertion order (as long as elements are only added and not removed + // [which is the case for Scribe managers]) + public static void Register(Action action) + { + if (Scribe.mode == LoadSaveMode.LoadingVars) + Scribe.loader.initer.RegisterForPostLoadInit(new ExposeActor(action)); + } +} diff --git a/Source/Client/Comp/Map/FactionMapData.cs b/Source/Client/Comp/Map/FactionMapData.cs new file mode 100644 index 00000000..dd862593 --- /dev/null +++ b/Source/Client/Comp/Map/FactionMapData.cs @@ -0,0 +1,81 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +// Per-faction storage for RimWorld managers +public class FactionMapData : IExposable +{ + public Map map; + public int factionId; + + // Saved + public DesignationManager designationManager; + public AreaManager areaManager; + public ZoneManager zoneManager; + + // Not saved + public HaulDestinationManager haulDestinationManager; + public ListerHaulables listerHaulables; + public ResourceCounter resourceCounter; + public ListerFilthInHomeArea listerFilthInHomeArea; + public ListerMergeables listerMergeables; + + private FactionMapData() { } + + // Loading ctor + public FactionMapData(Map map) + { + this.map = map; + + haulDestinationManager = new HaulDestinationManager(map); + listerHaulables = new ListerHaulables(map); + resourceCounter = new ResourceCounter(map); + listerFilthInHomeArea = new ListerFilthInHomeArea(map); + listerMergeables = new ListerMergeables(map); + } + + private FactionMapData(int factionId, Map map) : this(map) + { + this.factionId = factionId; + + designationManager = new DesignationManager(map); + areaManager = new AreaManager(map); + zoneManager = new ZoneManager(map); + } + + public void ExposeData() + { + ExposeActor.Register(() => map.PushFaction(factionId)); + + Scribe_Values.Look(ref factionId, "factionId"); + Scribe_Deep.Look(ref designationManager, "designationManager", map); + Scribe_Deep.Look(ref areaManager, "areaManager", map); + Scribe_Deep.Look(ref zoneManager, "zoneManager", map); + + ExposeActor.Register(() => map.PopFaction()); + } + + public static FactionMapData New(int factionId, Map map) + { + return new FactionMapData(factionId, map); + } + + public static FactionMapData NewFromMap(Map map, int factionId) + { + return new FactionMapData(map) + { + factionId = factionId, + + designationManager = map.designationManager, + areaManager = map.areaManager, + zoneManager = map.zoneManager, + + haulDestinationManager = map.haulDestinationManager, + listerHaulables = map.listerHaulables, + resourceCounter = map.resourceCounter, + listerFilthInHomeArea = map.listerFilthInHomeArea, + listerMergeables = map.listerMergeables, + }; + } +} diff --git a/Source/Client/Comp/MultiplayerMapComp.cs b/Source/Client/Comp/Map/MultiplayerMapComp.cs similarity index 67% rename from Source/Client/Comp/MultiplayerMapComp.cs rename to Source/Client/Comp/Map/MultiplayerMapComp.cs index 4c4193df..8c650718 100644 --- a/Source/Client/Comp/MultiplayerMapComp.cs +++ b/Source/Client/Comp/Map/MultiplayerMapComp.cs @@ -17,7 +17,7 @@ public class MultiplayerMapComp : IExposable, IHasSemiPersistentData public Map map; - //public IdBlock mapIdBlock; + public IdBlock mapIdBlock; public Dictionary factionData = new Dictionary(); public Dictionary customFactionData = new Dictionary(); @@ -121,7 +121,10 @@ public void ExposeData() if (Scribe.mode == LoadSaveMode.LoadingVars && mapDialogs == null) mapDialogs = new List(); - //Multiplayer.ExposeIdBlock(ref mapIdBlock, "mapIdBlock"); + // todo for split sim + // Scribe_Custom.LookIdBlock(ref mapIdBlock, "mapIdBlock"); + // const int mapBlockSize = int.MaxValue / 2 / 1024; + // mapIdBlock ??= new IdBlock(int.MaxValue / 2 + mapBlockSize * map.uniqueID, mapBlockSize); ExposeFactionData(); ExposeCustomFactionData(); @@ -184,134 +187,6 @@ public void ReadSemiPersistent(ByteReader reader) } } - // Per-faction storage for RimWorld managers - public class FactionMapData : IExposable - { - public Map map; - public int factionId; - - // Saved - public DesignationManager designationManager; - public AreaManager areaManager; - public ZoneManager zoneManager; - - // Not saved - public HaulDestinationManager haulDestinationManager; - public ListerHaulables listerHaulables; - public ResourceCounter resourceCounter; - public ListerFilthInHomeArea listerFilthInHomeArea; - public ListerMergeables listerMergeables; - - private FactionMapData() { } - - // Loading ctor - public FactionMapData(Map map) - { - this.map = map; - - haulDestinationManager = new HaulDestinationManager(map); - listerHaulables = new ListerHaulables(map); - resourceCounter = new ResourceCounter(map); - listerFilthInHomeArea = new ListerFilthInHomeArea(map); - listerMergeables = new ListerMergeables(map); - } - - private FactionMapData(int factionId, Map map) : this(map) - { - this.factionId = factionId; - - designationManager = new DesignationManager(map); - areaManager = new AreaManager(map); - zoneManager = new ZoneManager(map); - } - - public void ExposeData() - { - ExposeActor.Register(() => map.PushFaction(factionId)); - - Scribe_Values.Look(ref factionId, "factionId"); - Scribe_Deep.Look(ref designationManager, "designationManager", map); - Scribe_Deep.Look(ref areaManager, "areaManager", map); - Scribe_Deep.Look(ref zoneManager, "zoneManager", map); - - ExposeActor.Register(() => map.PopFaction()); - } - - public static FactionMapData New(int factionId, Map map) - { - return new FactionMapData(factionId, map); - } - - public static FactionMapData NewFromMap(Map map, int factionId) - { - return new FactionMapData(map) - { - factionId = factionId, - - designationManager = map.designationManager, - areaManager = map.areaManager, - zoneManager = map.zoneManager, - - haulDestinationManager = map.haulDestinationManager, - listerHaulables = map.listerHaulables, - resourceCounter = map.resourceCounter, - listerFilthInHomeArea = map.listerFilthInHomeArea, - listerMergeables = map.listerMergeables, - }; - } - } - - public class CustomFactionMapData : IExposable - { - public Map map; - public int factionId; - - public HashSet claimed = new HashSet(); - public HashSet unforbidden = new HashSet(); - - // Loading ctor - public CustomFactionMapData(Map map) - { - this.map = map; - } - - public void ExposeData() - { - Scribe_Values.Look(ref factionId, "factionId"); - Scribe_Collections.Look(ref unforbidden, "unforbidden", LookMode.Reference); - } - - public static CustomFactionMapData New(int factionId, Map map) - { - return new CustomFactionMapData(map) { factionId = factionId }; - } - } - - public class ExposeActor : IExposable - { - private Action action; - - private ExposeActor(Action action) - { - this.action = action; - } - - public void ExposeData() - { - if (Scribe.mode == LoadSaveMode.PostLoadInit) - action(); - } - - // This depends on the fact that the implementation of HashSet RimWorld currently uses - // "preserves" insertion order (as long as elements are only added and not removed - // [which is the case for Scribe managers]) - public static void Register(Action action) - { - if (Scribe.mode == LoadSaveMode.LoadingVars) - Scribe.loader.initer.RegisterForPostLoadInit(new ExposeActor(action)); - } - } - [HarmonyPatch(typeof(MapDrawer), nameof(MapDrawer.DrawMapMesh))] static class ForceShowDialogs { diff --git a/Source/Client/Comp/MultiplayerWorldComp.cs b/Source/Client/Comp/MultiplayerWorldComp.cs deleted file mode 100644 index 4410e9c5..00000000 --- a/Source/Client/Comp/MultiplayerWorldComp.cs +++ /dev/null @@ -1,526 +0,0 @@ -using HarmonyLib; -using Multiplayer.Common; -using RimWorld; -using RimWorld.Planet; -using System; -using System.Collections.Generic; -using System.Linq; -using Multiplayer.Client.Comp; -using UnityEngine; -using Verse; -using Multiplayer.Client.Persistent; -using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Patches; -using Multiplayer.Client.Saving; -using Multiplayer.Client.Util; - -namespace Multiplayer.Client -{ - - public class MultiplayerWorldComp : IExposable, ITickable - { - public static bool tickingWorld; - public static bool executingCmdWorld; - private TimeSpeed desiredTimeSpeed = TimeSpeed.Paused; - - public float RealTimeToTickThrough { get; set; } - - public float TickRateMultiplier(TimeSpeed speed) - { - if (Multiplayer.GameComp.asyncTime) - { - var enforcePause = Multiplayer.WorldComp.splitSession != null || - AsyncTimeComp.pauseLocks.Any(x => x(null)); - - if (enforcePause) - return 0f; - } - - return speed switch - { - TimeSpeed.Paused => 0f, - TimeSpeed.Normal => 1f, - TimeSpeed.Fast => 3f, - TimeSpeed.Superfast => 6f, - TimeSpeed.Ultrafast => 15f, - _ => -1f - }; - } - - public TimeSpeed TimeSpeed - { - get => Find.TickManager.CurTimeSpeed; - set { - desiredTimeSpeed = value; - UpdateTimeSpeed(); - } - } - - /** - * Clamps the World's TimeSpeed to be between (slowest map) and (fastest map) - * Caution: doesn't work if called inside a MapAsyncTime.PreContext() - */ - public void UpdateTimeSpeed() - { - if (!Multiplayer.GameComp.asyncTime) { - Find.TickManager.CurTimeSpeed = desiredTimeSpeed; - return; - } - - var mapSpeeds = Find.Maps.Select(m => m.AsyncTime()) - .Where(a => a.ActualRateMultiplier(a.TimeSpeed) != 0f) - .Select(a => a.TimeSpeed) - .ToList(); - - if (mapSpeeds.NullOrEmpty()) { - // all maps are paused = pause the world - Find.TickManager.CurTimeSpeed = TimeSpeed.Paused; - } - else { - Find.TickManager.CurTimeSpeed = (TimeSpeed)Math.Min(Math.Max((byte)desiredTimeSpeed, (byte)mapSpeeds.Min()), (byte)mapSpeeds.Max()); - } - } - - public Queue Cmds => cmds; - - public int TickableId => -1; - - public Dictionary factionData = new(); - - public World world; - public ulong randState = 2; - public TileTemperaturesComp uiTemperatures; - - public List trading = new(); - public CaravanSplittingSession splitSession; - - public Queue cmds = new(); - - public MultiplayerWorldComp(World world) - { - this.world = world; - this.uiTemperatures = new TileTemperaturesComp(world); - } - - public void ExposeData() - { - var timer = TickPatch.Timer; - Scribe_Values.Look(ref timer, "timer"); - TickPatch.SetTimer(timer); - - Scribe_Custom.LookULong(ref randState, "randState", 2); - - TimeSpeed timeSpeed = Find.TickManager.CurTimeSpeed; - Scribe_Values.Look(ref timeSpeed, "timeSpeed"); - if (Scribe.mode == LoadSaveMode.LoadingVars) - Find.TickManager.CurTimeSpeed = timeSpeed; - - ExposeFactionData(); - - Scribe_Collections.Look(ref trading, "tradingSessions", LookMode.Deep); - if (Scribe.mode == LoadSaveMode.PostLoadInit) - { - if (trading.RemoveAll(t => t.trader == null || t.playerNegotiator == null) > 0) - Log.Message("Some trading sessions had null entries"); - } - } - - private int currentFactionId; - - private void ExposeFactionData() - { - if (Scribe.mode == LoadSaveMode.Saving) - { - int currentFactionId = Faction.OfPlayer.loadID; - Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); - - var factionData = new Dictionary(this.factionData); - factionData.Remove(currentFactionId); - - Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); - } - else - { - // The faction whose data is currently set - Scribe_Values.Look(ref currentFactionId, "currentFactionId"); - - Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); - factionData ??= new Dictionary(); - } - - if (Scribe.mode == LoadSaveMode.LoadingVars && Multiplayer.session != null && Multiplayer.game != null) - { - Multiplayer.game.myFactionLoading = Find.FactionManager.GetById(Multiplayer.session.myFactionId); - } - - if (Scribe.mode == LoadSaveMode.LoadingVars) - { - // Game manager order? - factionData[currentFactionId] = FactionWorldData.FromCurrent(currentFactionId); - } - } - - public void Tick() - { - tickingWorld = true; - PreContext(); - - try - { - Find.TickManager.DoSingleTick(); - TickWorldTrading(); - - if (ModsConfig.BiotechActive) - { - // Vanilla puts those into a separate try/catch blocks - try - { - CompDissolutionEffect_Goodwill.WorldUpdate(); - } - catch (Exception e) - { - Log.Error(e.ToString()); - } - try - { - CompDissolutionEffect_Pollution.WorldUpdate(); - } - catch (Exception e) - { - Log.Error(e.ToString()); - } - } - } - finally - { - PostContext(); - tickingWorld = false; - - Multiplayer.game.sync.TryAddWorldRandomState(randState); - } - } - - public void TickWorldTrading() - { - for (int i = trading.Count - 1; i >= 0; i--) - { - var session = trading[i]; - if (session.playerNegotiator.Spawned) continue; - - if (session.ShouldCancel()) - RemoveTradeSession(session); - } - } - - public void RemoveTradeSession(MpTradeSession session) - { - int index = trading.IndexOf(session); - trading.Remove(session); - Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); - } - - public void PreContext() - { - UniqueIdsPatch.CurrentBlock = Multiplayer.GlobalIdBlock; - Rand.PushState(); - Rand.StateCompressed = randState; - } - - public void PostContext() - { - randState = Rand.StateCompressed; - Rand.PopState(); - UniqueIdsPatch.CurrentBlock = null; - } - - public void SetFaction(Faction faction) - { - if (!factionData.TryGetValue(faction.loadID, out FactionWorldData data)) - return; - - Game game = Current.Game; - game.researchManager = data.researchManager; - game.drugPolicyDatabase = data.drugPolicyDatabase; - game.outfitDatabase = data.outfitDatabase; - game.foodRestrictionDatabase = data.foodRestrictionDatabase; - game.playSettings = data.playSettings; - - SyncResearch.researchSpeed = data.researchSpeed; - } - - public static float lastSpeedChange; - - public void ExecuteCmd(ScheduledCommand cmd) - { - CommandType cmdType = cmd.type; - LoggingByteReader data = new LoggingByteReader(cmd.data); - data.Log.Node($"{cmdType} Global"); - - executingCmdWorld = true; - TickPatch.currentExecutingCmdIssuedBySelf = cmd.issuedBySelf && !TickPatch.Simulating; - - PreContext(); - Extensions.PushFaction(null, cmd.GetFaction()); - - bool prevDevMode = Prefs.data.devMode; - var prevGodMode = DebugSettings.godMode; - Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId)?.SetContext(); - - var randCalls1 = DeferredStackTracing.randCalls; - - try - { - if (cmdType == CommandType.Sync) - { - var handler = SyncUtil.HandleCmd(data); - data.Log.current.text = handler.ToString(); - } - - if (cmdType == CommandType.DebugTools) - { - MpDebugTools.HandleCmd(data); - } - - if (cmdType == CommandType.GlobalTimeSpeed) - { - HandleTimeSpeed(cmd, data); - } - - if (cmdType == CommandType.TimeSpeedVote) - { - HandleTimeVote(cmd, data); - } - - if (cmdType == CommandType.PauseAll) - { - SetTimeEverywhere(TimeSpeed.Paused); - } - - if (cmdType == CommandType.SetupFaction) - { - HandleSetupFaction(cmd, data); - } - - if (cmdType == CommandType.CreateJoinPoint) - { - LongEventHandler.QueueLongEvent(CreateJoinPoint, "MpCreatingJoinPoint", false, null); - } - - if (cmdType == CommandType.InitPlayerData) - { - var playerId = data.ReadInt32(); - var canUseDevMode = data.ReadBool(); - Multiplayer.GameComp.playerData[playerId] = new PlayerData { canUseDevMode = canUseDevMode }; - } - } - catch (Exception e) - { - Log.Error($"World cmd exception ({cmdType}): {e}"); - } - finally - { - DebugSettings.godMode = prevGodMode; - Prefs.data.devMode = prevDevMode; - - MpLog.Debug($"rand calls {DeferredStackTracing.randCalls - randCalls1}"); - MpLog.Debug("rand state " + Rand.StateCompressed); - - Extensions.PopFaction(); - PostContext(); - TickPatch.currentExecutingCmdIssuedBySelf = false; - executingCmdWorld = false; - - Multiplayer.game.sync.TryAddCommandRandomState(randState); - - if (cmdType != CommandType.GlobalTimeSpeed) - Multiplayer.ReaderLog.AddCurrentNode(data); - } - } - - private static void CreateJoinPoint() - { - Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); - - if (!TickPatch.Simulating && !Multiplayer.IsReplay && (Multiplayer.LocalServer != null || Multiplayer.arbiterInstance)) - SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true); - } - - public void SetTimeEverywhere(TimeSpeed speed) - { - foreach (var map in Find.Maps) - map.AsyncTime().TimeSpeed = speed; - TimeSpeed = speed; - } - - private void HandleTimeSpeed(ScheduledCommand cmd, ByteReader data) - { - TimeSpeed speed = (TimeSpeed)data.ReadByte(); - - Multiplayer.WorldComp.TimeSpeed = speed; - - if (!Multiplayer.GameComp.asyncTime) - { - SetTimeEverywhere(speed); - - if (!cmd.issuedBySelf) - lastSpeedChange = Time.realtimeSinceStartup; - } - - MpLog.Debug($"Set world speed {speed} {TickPatch.Timer} {Find.TickManager.TicksGame}"); - } - - private void HandleTimeVote(ScheduledCommand cmd, ByteReader data) - { - TimeVote vote = (TimeVote)data.ReadByte(); - int tickableId = data.ReadInt32(); - - if (vote >= TimeVote.Reset) - Multiplayer.GameComp.playerData.Do(p => p.Value.SetTimeVote(tickableId, vote)); - else if (Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId) is { } playerData) - playerData.SetTimeVote(tickableId, vote); - - if (!Multiplayer.GameComp.asyncTime || vote == TimeVote.ResetAll) - SetTimeEverywhere(Multiplayer.GameComp.GetLowestTimeVote(Multiplayer.WorldComp.TickableId)); - else if (TickPatch.TickableById(tickableId) is { } tickable) - tickable.TimeSpeed = Multiplayer.GameComp.GetLowestTimeVote(tickableId); - - UpdateTimeSpeed(); - } - - private void HandleSetupFaction(ScheduledCommand command, ByteReader data) - { - int factionId = data.ReadInt32(); - Faction faction = Find.FactionManager.GetById(factionId); - - if (faction == null) - { - faction = new Faction - { - loadID = factionId, - def = FactionDefOf.PlayerColony, - Name = "Multiplayer faction", - }; - - Find.FactionManager.Add(faction); - - foreach (Faction current in Find.FactionManager.AllFactionsListForReading) - { - if (current == faction) continue; - current.TryMakeInitialRelationsWith(faction); - } - - Multiplayer.WorldComp.factionData[factionId] = FactionWorldData.New(factionId); - - MpLog.Log($"New faction {faction.GetUniqueLoadID()}"); - } - } - - public void DirtyColonyTradeForMap(Map map) - { - if (map == null) return; - foreach (MpTradeSession session in trading) - if (session.playerNegotiator.Map == map) - session.deal.recacheColony = true; - } - - public void DirtyTraderTradeForTrader(ITrader trader) - { - if (trader == null) return; - foreach (MpTradeSession session in trading) - if (session.trader == trader) - session.deal.recacheTrader = true; - } - - public void DirtyTradeForSpawnedThing(Thing t) - { - if (t is not { Spawned: true }) return; - foreach (MpTradeSession session in trading) - if (session.playerNegotiator.Map == t.Map) - session.deal.recacheThings.Add(t); - } - - public bool AnyTradeSessionsOnMap(Map map) - { - foreach (MpTradeSession session in trading) - if (session.playerNegotiator.Map == map) - return true; - return false; - } - - public void FinalizeInit() - { - Multiplayer.game.SetThingMakerSeed((int)(randState >> 32)); - } - - public override string ToString() - { - return $"{nameof(MultiplayerWorldComp)}_{world}"; - } - } - - public class FactionWorldData : IExposable - { - public int factionId; - public bool online; - - public ResearchManager researchManager; - public OutfitDatabase outfitDatabase; - public DrugPolicyDatabase drugPolicyDatabase; - public FoodRestrictionDatabase foodRestrictionDatabase; - public PlaySettings playSettings; - - public ResearchSpeed researchSpeed; - - public FactionWorldData() { } - - public void ExposeData() - { - Scribe_Values.Look(ref factionId, "factionId"); - Scribe_Values.Look(ref online, "online"); - - Scribe_Deep.Look(ref researchManager, "researchManager"); - Scribe_Deep.Look(ref drugPolicyDatabase, "drugPolicyDatabase"); - Scribe_Deep.Look(ref outfitDatabase, "outfitDatabase"); - Scribe_Deep.Look(ref foodRestrictionDatabase, "foodRestrictionDatabase"); - Scribe_Deep.Look(ref playSettings, "playSettings"); - - Scribe_Deep.Look(ref researchSpeed, "researchSpeed"); - } - - public void Tick() - { - } - - public static FactionWorldData New(int factionId) - { - return new FactionWorldData() - { - factionId = factionId, - - researchManager = new ResearchManager(), - drugPolicyDatabase = new DrugPolicyDatabase(), - outfitDatabase = new OutfitDatabase(), - foodRestrictionDatabase = new FoodRestrictionDatabase(), - playSettings = new PlaySettings(), - researchSpeed = new ResearchSpeed(), - }; - } - - public static FactionWorldData FromCurrent(int factionId) - { - return new FactionWorldData() - { - factionId = factionId == int.MinValue ? Faction.OfPlayer.loadID : factionId, - online = true, - - researchManager = Find.ResearchManager, - drugPolicyDatabase = Current.Game.drugPolicyDatabase, - outfitDatabase = Current.Game.outfitDatabase, - foodRestrictionDatabase = Current.Game.foodRestrictionDatabase, - playSettings = Current.Game.playSettings, - - researchSpeed = new ResearchSpeed(), - }; - } - } -} diff --git a/Source/Client/Comp/World/FactionWorldData.cs b/Source/Client/Comp/World/FactionWorldData.cs new file mode 100644 index 00000000..ac715ac7 --- /dev/null +++ b/Source/Client/Comp/World/FactionWorldData.cs @@ -0,0 +1,70 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +public class FactionWorldData : IExposable +{ + public int factionId; + public bool online; + + public ResearchManager researchManager; + public OutfitDatabase outfitDatabase; + public DrugPolicyDatabase drugPolicyDatabase; + public FoodRestrictionDatabase foodRestrictionDatabase; + public PlaySettings playSettings; + + public ResearchSpeed researchSpeed; + + public FactionWorldData() { } + + public void ExposeData() + { + Scribe_Values.Look(ref factionId, "factionId"); + Scribe_Values.Look(ref online, "online"); + + Scribe_Deep.Look(ref researchManager, "researchManager"); + Scribe_Deep.Look(ref drugPolicyDatabase, "drugPolicyDatabase"); + Scribe_Deep.Look(ref outfitDatabase, "outfitDatabase"); + Scribe_Deep.Look(ref foodRestrictionDatabase, "foodRestrictionDatabase"); + Scribe_Deep.Look(ref playSettings, "playSettings"); + + Scribe_Deep.Look(ref researchSpeed, "researchSpeed"); + } + + public void Tick() + { + } + + public static FactionWorldData New(int factionId) + { + return new FactionWorldData() + { + factionId = factionId, + + researchManager = new ResearchManager(), + drugPolicyDatabase = new DrugPolicyDatabase(), + outfitDatabase = new OutfitDatabase(), + foodRestrictionDatabase = new FoodRestrictionDatabase(), + playSettings = new PlaySettings(), + researchSpeed = new ResearchSpeed(), + }; + } + + public static FactionWorldData FromCurrent(int factionId) + { + return new FactionWorldData() + { + factionId = factionId == int.MinValue ? Faction.OfPlayer.loadID : factionId, + online = true, + + researchManager = Find.ResearchManager, + drugPolicyDatabase = Current.Game.drugPolicyDatabase, + outfitDatabase = Current.Game.outfitDatabase, + foodRestrictionDatabase = Current.Game.foodRestrictionDatabase, + playSettings = Current.Game.playSettings, + + researchSpeed = new ResearchSpeed(), + }; + } +} diff --git a/Source/Client/Comp/World/MultiplayerWorldComp.cs b/Source/Client/Comp/World/MultiplayerWorldComp.cs new file mode 100644 index 00000000..e7ef795e --- /dev/null +++ b/Source/Client/Comp/World/MultiplayerWorldComp.cs @@ -0,0 +1,144 @@ +using RimWorld; +using RimWorld.Planet; +using System.Collections.Generic; +using Verse; +using Multiplayer.Client.Persistent; +using Multiplayer.Client.Saving; + +namespace Multiplayer.Client; + +public class MultiplayerWorldComp +{ + public Dictionary factionData = new(); + + public World world; + + public TileTemperaturesComp uiTemperatures; + public List trading = new(); + public CaravanSplittingSession splitSession; + + private int currentFactionId; + + public MultiplayerWorldComp(World world) + { + this.world = world; + uiTemperatures = new TileTemperaturesComp(world); + } + + // Called from AsyncWorldTimeComp.ExposeData + public void ExposeData() + { + ExposeFactionData(); + + Scribe_Collections.Look(ref trading, "tradingSessions", LookMode.Deep); + if (Scribe.mode == LoadSaveMode.PostLoadInit) + { + if (trading.RemoveAll(t => t.trader == null || t.playerNegotiator == null) > 0) + Log.Message("Some trading sessions had null entries"); + } + } + + private void ExposeFactionData() + { + if (Scribe.mode == LoadSaveMode.Saving) + { + int currentFactionId = Faction.OfPlayer.loadID; + Scribe_Custom.LookValue(currentFactionId, "currentFactionId"); + + var factionData = new Dictionary(this.factionData); + factionData.Remove(currentFactionId); + + Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); + } + else + { + // The faction whose data is currently set + Scribe_Values.Look(ref currentFactionId, "currentFactionId"); + + Scribe_Collections.Look(ref factionData, "factionData", LookMode.Value, LookMode.Deep); + factionData ??= new Dictionary(); + } + + if (Scribe.mode == LoadSaveMode.LoadingVars && Multiplayer.session != null && Multiplayer.game != null) + { + Multiplayer.game.myFactionLoading = Find.FactionManager.GetById(Multiplayer.session.myFactionId); + } + + if (Scribe.mode == LoadSaveMode.LoadingVars) + { + // Game manager order? + factionData[currentFactionId] = FactionWorldData.FromCurrent(currentFactionId); + } + } + + public void TickWorldTrading() + { + for (int i = trading.Count - 1; i >= 0; i--) + { + var session = trading[i]; + if (session.playerNegotiator.Spawned) continue; + + if (session.ShouldCancel()) + RemoveTradeSession(session); + } + } + + public void RemoveTradeSession(MpTradeSession session) + { + int index = trading.IndexOf(session); + trading.Remove(session); + Find.WindowStack?.WindowOfType()?.Notify_RemovedSession(index); + } + + public void SetFaction(Faction faction) + { + if (!factionData.TryGetValue(faction.loadID, out FactionWorldData data)) + return; + + Game game = Current.Game; + game.researchManager = data.researchManager; + game.drugPolicyDatabase = data.drugPolicyDatabase; + game.outfitDatabase = data.outfitDatabase; + game.foodRestrictionDatabase = data.foodRestrictionDatabase; + game.playSettings = data.playSettings; + + SyncResearch.researchSpeed = data.researchSpeed; + } + + public void DirtyColonyTradeForMap(Map map) + { + if (map == null) return; + foreach (MpTradeSession session in trading) + if (session.playerNegotiator.Map == map) + session.deal.recacheColony = true; + } + + public void DirtyTraderTradeForTrader(ITrader trader) + { + if (trader == null) return; + foreach (MpTradeSession session in trading) + if (session.trader == trader) + session.deal.recacheTrader = true; + } + + public void DirtyTradeForSpawnedThing(Thing t) + { + if (t is not { Spawned: true }) return; + foreach (MpTradeSession session in trading) + if (session.playerNegotiator.Map == t.Map) + session.deal.recacheThings.Add(t); + } + + public bool AnyTradeSessionsOnMap(Map map) + { + foreach (MpTradeSession session in trading) + if (session.playerNegotiator.Map == map) + return true; + return false; + } + + public override string ToString() + { + return $"{nameof(MultiplayerWorldComp)}_{world}"; + } +} diff --git a/Source/Client/ConstantTicker.cs b/Source/Client/ConstantTicker.cs index d4d7b377..1928a138 100644 --- a/Source/Client/ConstantTicker.cs +++ b/Source/Client/ConstantTicker.cs @@ -1,4 +1,3 @@ -extern alias zip; using System; using HarmonyLib; using Multiplayer.Common; @@ -35,6 +34,9 @@ public static void Tick() } } + private const float TicksPerMinute = 60 * 60; + private const float TicksPerIngameDay = 2500 * 24; + static void TickAutosave() { if (Multiplayer.LocalServer is not { } server) return; @@ -45,7 +47,7 @@ static void TickAutosave() session.autosaveCounter++; if (server.settings.autosaveInterval > 0 && - session.autosaveCounter > server.settings.autosaveInterval * 60 * 60) + session.autosaveCounter > server.settings.autosaveInterval * TicksPerMinute) { session.autosaveCounter = 0; MultiplayerSession.DoAutosave(); @@ -54,7 +56,7 @@ static void TickAutosave() { var anyMapCounterUp = Multiplayer.game.mapComps - .Any(m => m.autosaveCounter > server.settings.autosaveInterval * 2500 * 24); + .Any(m => m.autosaveCounter > server.settings.autosaveInterval * TicksPerIngameDay); if (anyMapCounterUp) { @@ -100,7 +102,7 @@ private static void TickSync() } } - private static Pawn dummyPawn = new Pawn() + private static Pawn dummyPawn = new() { relations = new Pawn_RelationsTracker(dummyPawn), }; diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index 21cbaecb..21673987 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -8,6 +8,7 @@ using HarmonyLib; using RimWorld; +using RimWorld.Planet; using UnityEngine; using Verse; using Debug = UnityEngine.Debug; @@ -18,7 +19,102 @@ static class MpDebugActions { const string MultiplayerCategory = "Multiplayer"; - [DebugAction("General", actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] + [DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolWorld, allowedGameStates = AllowedGameStates.PlayingOnWorld)] + public static void SpawnCaravans() + { + for (int a = 0; a < 10; a++) + { + int num = GenWorld.MouseTile(); + if (Find.WorldGrid[num].biome.impassable) + { + return; + } + + List list = new List(); + int num2 = Rand.RangeInclusive(1, 10); + for (int i = 0; i < num2; i++) + { + Pawn pawn = PawnGenerator.GeneratePawn(Faction.OfPlayer.def.basicMemberKind, Faction.OfPlayer); + list.Add(pawn); + if (!pawn.WorkTagIsDisabled(WorkTags.Violent)) + { + ThingDef thingDef = DefDatabase.AllDefs.Where((ThingDef def) => + def.IsWeapon && !def.weaponTags.NullOrEmpty() && + (def.weaponTags.Contains("SimpleGun") || + def.weaponTags.Contains( + "IndustrialGunAdvanced") || + def.weaponTags.Contains("SpacerGun") || + def.weaponTags.Contains( + "MedievalMeleeAdvanced") || + def.weaponTags.Contains( + "NeolithicRangedBasic") || + def.weaponTags.Contains( + "NeolithicRangedDecent") || + def.weaponTags.Contains( + "NeolithicRangedHeavy"))) + .RandomElementWithFallback(); + pawn.equipment.AddEquipment( + (ThingWithComps)ThingMaker.MakeThing(thingDef, GenStuff.RandomStuffFor(thingDef))); + } + } + + int num3 = Rand.RangeInclusive(-4, 10); + for (int j = 0; j < num3; j++) + { + Pawn item = PawnGenerator.GeneratePawn( + DefDatabase.AllDefs + .Where((PawnKindDef d) => d.RaceProps.Animal && d.RaceProps.wildness < 1f).RandomElement(), + Faction.OfPlayer); + list.Add(item); + } + + Caravan caravan = + CaravanMaker.MakeCaravan(list, Faction.OfPlayer, num, addToWorldPawnsIfNotAlready: true); + + List list2 = ThingSetMakerDefOf.DebugCaravanInventory.root.Generate(); + for (int k = 0; k < list2.Count; k++) + { + Thing thing = list2[k]; + if (!(thing.GetStatValue(StatDefOf.Mass) * (float)thing.stackCount > + caravan.MassCapacity - caravan.MassUsage)) + { + CaravanInventoryUtility.GiveThing(caravan, thing); + continue; + } + + break; + } + } + } + + [DebugAction(MultiplayerCategory, "Set faction", actionType = DebugActionType.Action, allowedGameStates = AllowedGameStates.PlayingOnMap, displayPriority = 100)] + private static void SetFaction() + { + DebugToolsGeneral.GenericRectTool("Set faction", rect => + { + List factionOptions = new List(); + foreach (Faction faction in Find.FactionManager.AllFactionsInViewOrder) + { + FloatMenuOption item = new FloatMenuOption(faction.Name, () => + { + foreach (Thing thing in rect.SelectMany(c => Find.CurrentMap.thingGrid.ThingsAt(c))) + { + if (thing.def.CanHaveFaction) + thing.SetFaction(faction); + + if (thing is IThingHolder holder) + foreach (var heldThing in holder.GetDirectlyHeldThings()) + if (heldThing.def.CanHaveFaction) + heldThing.SetFaction(faction); + } + }); + factionOptions.Add(item); + } + Find.WindowStack.Add(new FloatMenu(factionOptions)); + }); + } + + [DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolMap, allowedGameStates = AllowedGameStates.PlayingOnMap)] public static void SpawnShuttleAcceptColonists() { var shuttle = ThingMaker.MakeThing(ThingDefOf.Shuttle, null); @@ -62,17 +158,17 @@ public static void SaveGameLocal() public static void DumpSyncTypes() { var dict = new Dictionary() { - {"ThingComp", ImplSerialization.thingCompTypes}, - {"AbilityComp", ImplSerialization.abilityCompTypes}, - {"Designator", ImplSerialization.designatorTypes}, - {"WorldObjectComp", ImplSerialization.worldObjectCompTypes}, - {"HediffComp", ImplSerialization.hediffCompTypes}, - {"IStoreSettingsParent", ImplSerialization.storageParents}, - {"IPlantToGrowSettable", ImplSerialization.plantToGrowSettables}, - - {"GameComponent", ImplSerialization.gameCompTypes}, - {"WorldComponent", ImplSerialization.worldCompTypes}, - {"MapComponent", ImplSerialization.mapCompTypes}, + {"ThingComp", RwImplSerialization.thingCompTypes}, + {"AbilityComp", RwImplSerialization.abilityCompTypes}, + {"Designator", RwImplSerialization.designatorTypes}, + {"WorldObjectComp", RwImplSerialization.worldObjectCompTypes}, + {"HediffComp", RwImplSerialization.hediffCompTypes}, + {"IStoreSettingsParent", RwImplSerialization.storageParents}, + {"IPlantToGrowSettable", RwImplSerialization.plantToGrowSettables}, + + {"GameComponent", RwImplSerialization.gameCompTypes}, + {"WorldComponent", RwImplSerialization.worldCompTypes}, + {"MapComponent", RwImplSerialization.mapCompTypes}, }; foreach(var kv in dict) { diff --git a/Source/Client/Debug/DebugTools.cs b/Source/Client/Debug/DebugTools.cs index b4470aef..88e88168 100644 --- a/Source/Client/Debug/DebugTools.cs +++ b/Source/Client/Debug/DebugTools.cs @@ -31,7 +31,7 @@ public static void HandleCmd(ByteReader data) MouseTilePatch.result = cursorX; currentHash = data.ReadInt32(); - var path = data.ReadString(); + var path = data.ReadStringNullable(); var state = Multiplayer.game.playerDebugState.GetOrAddNew(currentPlayer); diff --git a/Source/Client/Desyncs/DeferredStackTracing.cs b/Source/Client/Desyncs/DeferredStackTracing.cs index 3db35b56..a5437c70 100644 --- a/Source/Client/Desyncs/DeferredStackTracing.cs +++ b/Source/Client/Desyncs/DeferredStackTracing.cs @@ -11,7 +11,6 @@ namespace Multiplayer.Client.Desyncs { [EarlyPatch] [HarmonyPatch] - static class DeferredStackTracing { public static int ignoreTraces; @@ -55,7 +54,6 @@ public static bool ShouldAddStackTraceForDesyncLog() if (Multiplayer.IsReplay) return false; if (!Multiplayer.Ticking && !Multiplayer.ExecutingCmds) return false; - if (AsyncTimeComp.tickingMap is not { uniqueID: 0 }) return false; return ignoreTraces == 0; } diff --git a/Source/Client/Desyncs/SaveableDesyncInfo.cs b/Source/Client/Desyncs/SaveableDesyncInfo.cs index 89135074..d6aa61fd 100644 --- a/Source/Client/Desyncs/SaveableDesyncInfo.cs +++ b/Source/Client/Desyncs/SaveableDesyncInfo.cs @@ -1,15 +1,15 @@ -extern alias zip; -using System; +using System; using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text; using HarmonyLib; using Multiplayer.Common; +using Multiplayer.Common.Util; using RimWorld; using UnityEngine; using Verse; -using zip::Ionic.Zip; namespace Multiplayer.Client.Desyncs; @@ -35,7 +35,7 @@ public void Save() try { var desyncFilePath = FindFileNameForNextDesyncFile(); - using var zip = new ZipFile(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip")); + using var zip = MpZipFile.Open(Path.Combine(Multiplayer.DesyncsDir, desyncFilePath + ".zip"), ZipArchiveMode.Create); zip.AddEntry("desync_info", GetDesyncDetails()); zip.AddEntry("local_traces.txt", GetLocalTraces()); @@ -43,8 +43,6 @@ public void Save() var extraLogs = LogGenerator.PrepareLogData(); if (extraLogs != null) zip.AddEntry("local_logs.txt", extraLogs); - - zip.Save(); } catch (Exception e) { diff --git a/Source/Client/Desyncs/StackTraceLogItem.cs b/Source/Client/Desyncs/StackTraceLogItem.cs index 61520287..d210bd26 100644 --- a/Source/Client/Desyncs/StackTraceLogItem.cs +++ b/Source/Client/Desyncs/StackTraceLogItem.cs @@ -25,12 +25,12 @@ public virtual void ReturnToPool() { } public class StackTraceLogItemObj : StackTraceLogItem { - public StackTrace stackTrace; - public string additionalInfo; + public string info1; + public string info2; - public override string AdditionalInfo => additionalInfo; + public override string AdditionalInfo => $"{info1} {info2}"; - public override string StackTraceString { get => stackTrace.ToString(); } + public override string StackTraceString => ""; } public class StackTraceLogItemRaw : StackTraceLogItem @@ -46,7 +46,7 @@ public class StackTraceLogItemRaw : StackTraceLogItem public override string AdditionalInfo => $"{thingDef}{thingId} {factionName} {depth} {iters} {moreInfo}"; - static Dictionary methodNames = new Dictionary(); + static Dictionary methodNames = new(); public override string StackTraceString { diff --git a/Source/Client/Desyncs/SyncCoordinator.cs b/Source/Client/Desyncs/SyncCoordinator.cs index d337c7b4..683f7396 100644 --- a/Source/Client/Desyncs/SyncCoordinator.cs +++ b/Source/Client/Desyncs/SyncCoordinator.cs @@ -1,4 +1,3 @@ -extern alias zip; using Multiplayer.Common; using System; using System.Collections.Generic; @@ -39,6 +38,8 @@ private ClientSyncOpinion OpinionInBuilding public int lastValidTick = -1; public bool arbiterWasPlayingOnLastValidTick; + private const int MaxBacklog = 30; + /// /// Adds a client opinion to the list and checks that it matches the most recent currently in there. If not, a desync event is fired. /// @@ -58,7 +59,7 @@ public void AddClientOpinionAndCheckDesync(ClientSyncOpinion newOpinion) if (knownClientOpinions[0].isLocalClientsOpinion == newOpinion.isLocalClientsOpinion) { knownClientOpinions.Add(newOpinion); - if (knownClientOpinions.Count > 30) + if (knownClientOpinions.Count > MaxBacklog) RemoveAndClearFirst(); } else @@ -189,27 +190,24 @@ public void TryAddMapRandomState(int map, ulong state) } /// - /// Logs the current stack so that in the event of a desync we have some stack traces. + /// Logs an item to aid in desync debugging. /// - /// Any additional message to be logged with the stack - public void TryAddStackTraceForDesyncLog(string info = null) + /// Information to be logged + public void TryAddInfoForDesyncLog(string info1, string info2) { if (!ShouldCollect) return; OpinionInBuilding.TryMarkSimulating(); - //Get the current stack trace - var trace = new StackTrace(2, true); - var hash = trace.Hash() /*^ (info?.GetHashCode() ?? 0)*/; + int hash = Gen.HashCombineInt(info1.GetHashCode(), info2.GetHashCode()); OpinionInBuilding.desyncStackTraces.Add(new StackTraceLogItemObj { - stackTrace = trace, tick = TickPatch.Timer, hash = hash, - additionalInfo = info, + info1 = info1, + info2 = info2, }); - // Track & network trace hash, for comparison with other opinions. OpinionInBuilding.desyncStackTraceHashes.Add(hash); } diff --git a/Source/Client/Desyncs/UserReadableDesyncInfo.cs b/Source/Client/Desyncs/UserReadableDesyncInfo.cs index 76fb395c..7d269984 100644 --- a/Source/Client/Desyncs/UserReadableDesyncInfo.cs +++ b/Source/Client/Desyncs/UserReadableDesyncInfo.cs @@ -1,19 +1,18 @@ -extern alias zip; using System; using System.IO; using System.Linq; using System.Text; using System.Xml; using Multiplayer.Common; +using Multiplayer.Common.Util; using Verse; -using zip::Ionic.Zip; namespace Multiplayer.Client { public static class UserReadableDesyncInfo { /// - /// Attempts to generate user-readable desync info from the given replay + /// Attempts to generate user-readable desync info from the given replay /// /// The replay to generate the info from /// The desync info as a human-readable string @@ -21,14 +20,14 @@ public static string GenerateFromReplay(Replay replay) { var text = new StringBuilder(); - //Open the replay zip - using (var zip = replay.ZipFile) + // Open the replay zip + using (var zip = replay.OpenZipRead()) { try { text.AppendLine("[header]"); - using (var reader = new XmlTextReader(new MemoryStream(zip["game_snapshot"].GetBytes()))) + using (var reader = new XmlTextReader(new MemoryStream(zip.GetBytes("game_snapshot")))) { //Read to the element reader.ReadToNextElement(); @@ -50,7 +49,7 @@ public static string GenerateFromReplay(Replay replay) { //The info is the replay save data, including game name, protocol version, and assembly hashes text.AppendLine("[info]"); - text.AppendLine(zip["info"].GetString()); + text.AppendLine(zip.GetString("info")); } catch { @@ -62,7 +61,7 @@ public static string GenerateFromReplay(Replay replay) try { //Local Client Opinion data - local = DeserializeAndPrintSyncInfo(text, zip, "sync_local"); + local = DeserializeAndPrintSyncInfo(text, "sync_local", zip.GetBytes("sync_local")); } catch { @@ -74,7 +73,7 @@ public static string GenerateFromReplay(Replay replay) try { //Remote Client Opinion data - remote = DeserializeAndPrintSyncInfo(text, zip, "sync_remote"); + remote = DeserializeAndPrintSyncInfo(text, "sync_remote", zip.GetBytes("sync_remote")); } catch { @@ -88,13 +87,13 @@ public static string GenerateFromReplay(Replay replay) text.AppendLine("[desync_info]"); //Backwards compatibility! (AKA v1 support) - if (zip["desync_info"].GetString().StartsWith("###")) + if (zip.GetString("desync_info").StartsWith("###")) //This is a V2 file, dump as-is - text.AppendLine(zip["desync_info"].GetString()); + text.AppendLine(zip.GetString("desync_info")); else { //V1 file, parse it. - var desyncInfo = new ByteReader(zip["desync_info"].GetBytes()); + var desyncInfo = new ByteReader(zip.GetBytes("desync_info")); text.AppendLine($"Arbiter online: {desyncInfo.ReadBool()}"); text.AppendLine($"Last valid tick: {desyncInfo.ReadInt32()}"); text.AppendLine($"Last valid arbiter online: {desyncInfo.ReadBool()}"); @@ -138,7 +137,7 @@ public static string GenerateFromReplay(Replay replay) { //Add commands random states saved with the replay text.AppendLine("[map_cmds]"); - foreach (var cmd in Replay.DeserializeCmds(zip["maps/000_0_cmds"].GetBytes())) + foreach (var cmd in ScheduledCommand.DeserializeCmds(zip.GetBytes("maps/000_0_cmds"))) PrintCmdInfo(text, cmd); } catch @@ -151,7 +150,7 @@ public static string GenerateFromReplay(Replay replay) { //Add world random states saved with the replay text.AppendLine("[world_cmds]"); - foreach (var cmd in Replay.DeserializeCmds(zip["world/000_cmds"].GetBytes())) + foreach (var cmd in ScheduledCommand.DeserializeCmds(zip.GetBytes("world/000_cmds"))) PrintCmdInfo(text, cmd); } catch @@ -184,14 +183,14 @@ private static void PrintCmdInfo(StringBuilder builder, ScheduledCommand cmd) /// and dumps its data in a human-readable format to the provided string builder /// /// The builder to append the data to - /// The zip file that contains the file with the provided name /// The name of the sync file to dump + /// The contents of the sync file to dump /// The deserialized client opinion that was dumped - private static ClientSyncOpinion DeserializeAndPrintSyncInfo(StringBuilder builder, ZipFile zip, string filename) + private static ClientSyncOpinion DeserializeAndPrintSyncInfo(StringBuilder builder, string filename, byte[] bytes) { builder.AppendLine($"[{filename}]"); - var sync = ClientSyncOpinion.Deserialize(new ByteReader(zip[filename].GetBytes())); + var sync = ClientSyncOpinion.Deserialize(new ByteReader(bytes)); builder.AppendLine($"Start: {sync.startTick}"); builder.AppendLine($"Was simulating: {sync.simulating}"); builder.AppendLine($"Map count: {sync.mapStates.Count}"); @@ -203,4 +202,4 @@ private static ClientSyncOpinion DeserializeAndPrintSyncInfo(StringBuilder build return sync; } } -} \ No newline at end of file +} diff --git a/Source/Client/EarlyInit.cs b/Source/Client/EarlyInit.cs new file mode 100644 index 00000000..71d7950b --- /dev/null +++ b/Source/Client/EarlyInit.cs @@ -0,0 +1,87 @@ +using System; +using System.Reflection; +using HarmonyLib; +using Multiplayer.Client.Patches; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client; + +public static class EarlyInit +{ + public const string RestartConnectVariable = "MultiplayerRestartConnect"; + public const string RestartConfigsVariable = "MultiplayerRestartConfigs"; + + internal static void ProcessEnvironment() + { + if (!Environment.GetEnvironmentVariable(RestartConnectVariable).NullOrEmpty()) + { + Multiplayer.restartConnect = Environment.GetEnvironmentVariable(RestartConnectVariable); + Environment.SetEnvironmentVariable(RestartConnectVariable, ""); // Effectively unsets it + } + + if (!Environment.GetEnvironmentVariable(RestartConfigsVariable).NullOrEmpty()) + { + Multiplayer.restartConfigs = Environment.GetEnvironmentVariable(RestartConfigsVariable) == "true"; + Environment.SetEnvironmentVariable(RestartConfigsVariable, ""); + } + } + + internal static void EarlyPatches(Harmony harmony) + { + // Might fix some mod desyncs + harmony.PatchMeasure( + AccessTools.Constructor(typeof(Def), new Type[0]), + new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Prefix)), + new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Postfix)) + ); + + Assembly.GetCallingAssembly().GetTypes().Do(type => + { + if (type.IsDefined(typeof(EarlyPatchAttribute))) + harmony.CreateClassProcessor(type).Patch(); + }); + +#if DEBUG + DebugPatches.Init(); +#endif + } + + internal static void InitSync() + { + using (DeepProfilerWrapper.Section("Multiplayer SyncSerialization.Init")) + SyncSerialization.Init(); + + using (DeepProfilerWrapper.Section("Multiplayer SyncGame")) + SyncGame.Init(); + + using (DeepProfilerWrapper.Section("Multiplayer Sync register attributes")) + Sync.RegisterAllAttributes(typeof(Multiplayer).Assembly); + + using (DeepProfilerWrapper.Section("Multiplayer Sync validation")) + Sync.ValidateAll(); + } + + internal static void LatePatches(Harmony harmony) + { + // optimization, cache DescendantThingDefs + harmony.PatchMeasure( + AccessTools.Method(typeof(ThingCategoryDef), "get_DescendantThingDefs"), + new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Prefix"), + new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Postfix") + ); + + // optimization, cache ThisAndChildCategoryDefs + harmony.PatchMeasure( + AccessTools.Method(typeof(ThingCategoryDef), "get_ThisAndChildCategoryDefs"), + new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Prefix"), + new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Postfix") + ); + + if (MpVersion.IsDebug) + { + Log.Message("== Structure == \n" + SyncDict.syncWorkers.PrintStructure()); + } + } +} diff --git a/Source/Client/EarlyPatches/EarlyPatches.cs b/Source/Client/EarlyPatches/EarlyPatches.cs index 8b137cb9..5f506002 100644 --- a/Source/Client/EarlyPatches/EarlyPatches.cs +++ b/Source/Client/EarlyPatches/EarlyPatches.cs @@ -7,6 +7,7 @@ using System.Reflection; using Verse; using Multiplayer.Client.Patches; +using Multiplayer.Client.Util; namespace Multiplayer.Client { @@ -81,7 +82,7 @@ static class GenTypesSubclassesOptimization { static bool Prefix(Type baseType, ref List __result) { - __result = Multiplayer.subClasses.GetOrAddNew(baseType); + __result = TypeCache.subClasses.GetOrAddNew(baseType); return false; } } @@ -92,7 +93,7 @@ static class GenTypesSubclassesNonAbstractOptimization { static bool Prefix(Type baseType, ref List __result) { - __result = Multiplayer.subClassesNonAbstract.GetOrAddNew(baseType); + __result = TypeCache.subClassesNonAbstract.GetOrAddNew(baseType); return false; } } @@ -103,7 +104,7 @@ static class AccessToolsTypeByNamePatch { static bool Prefix(string name, ref Type __result) { - return !Multiplayer.typeByFullName.TryGetValue(name, out __result) && !Multiplayer.typeByName.TryGetValue(name, out __result); + return !TypeCache.typeByFullName.TryGetValue(name, out __result) && !TypeCache.typeByName.TryGetValue(name, out __result); } } } diff --git a/Source/Client/EarlyPatches/SettingsPatches.cs b/Source/Client/EarlyPatches/SettingsPatches.cs index 16328954..12d9a6e4 100644 --- a/Source/Client/EarlyPatches/SettingsPatches.cs +++ b/Source/Client/EarlyPatches/SettingsPatches.cs @@ -112,16 +112,16 @@ static void Postfix(string modIdentifier, string modHandleName, ref string __res if (mod == null) return; + if (JoinData.ignoredConfigsModIds.Contains(mod.ModMetaData.PackageIdNonUnique)) + return; + // Example: MultiplayerTempConfigs/rwmt.multiplayer-Multiplayer var newPath = Path.Combine( GenFilePaths.FolderUnderSaveData(JoinData.TempConfigsDir), GenText.SanitizeFilename(mod.PackageIdPlayerFacing.ToLowerInvariant() + "-" + modHandleName) ); - if (File.Exists(newPath)) - { - __result = newPath; - } + __result = newPath; } } diff --git a/Source/Client/Experimental/Experimental.cs b/Source/Client/Experimental/Experimental.cs new file mode 100644 index 00000000..4fa3f108 --- /dev/null +++ b/Source/Client/Experimental/Experimental.cs @@ -0,0 +1,8 @@ +using RimWorld; + +namespace Multiplayer.Client.Experimental; + +public static class Experimental +{ + public static Faction RealPlayerFaction => Multiplayer.RealPlayerFaction; +} diff --git a/Source/Client/Experimental/ISyncSimple.cs b/Source/Client/Experimental/ISyncSimple.cs new file mode 100644 index 00000000..e9458684 --- /dev/null +++ b/Source/Client/Experimental/ISyncSimple.cs @@ -0,0 +1,5 @@ +namespace Multiplayer.Client.Experimental; + +// Objects implementing this marker interface sync their exact type and all declared fields. +// This is useful when subtypes need to be synced by value. +public interface ISyncSimple { } diff --git a/Source/Client/Experimental/ThingFilterContext.cs b/Source/Client/Experimental/ThingFilterContext.cs new file mode 100644 index 00000000..9f5836b0 --- /dev/null +++ b/Source/Client/Experimental/ThingFilterContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.Client.Experimental; + +public abstract record ThingFilterContext : ISyncSimple +{ + public abstract ThingFilter Filter { get; } + public abstract ThingFilter ParentFilter { get; } + public virtual IEnumerable HiddenFilters => null; +} diff --git a/Source/ExternalAnnotations/0Harmony/Annotations.xml b/Source/Client/ExternalAnnotations/0Harmony/Annotations.xml similarity index 100% rename from Source/ExternalAnnotations/0Harmony/Annotations.xml rename to Source/Client/ExternalAnnotations/0Harmony/Annotations.xml diff --git a/Source/ExternalAnnotations/Assembly-CSharp/Annotations.xml b/Source/Client/ExternalAnnotations/Assembly-CSharp/Annotations.xml similarity index 100% rename from Source/ExternalAnnotations/Assembly-CSharp/Annotations.xml rename to Source/Client/ExternalAnnotations/Assembly-CSharp/Annotations.xml diff --git a/Source/Client/Patches/Blueprints.cs b/Source/Client/Factions/Blueprints.cs similarity index 98% rename from Source/Client/Patches/Blueprints.cs rename to Source/Client/Factions/Blueprints.cs index 4a1e137c..f65962f9 100644 --- a/Source/Client/Patches/Blueprints.cs +++ b/Source/Client/Factions/Blueprints.cs @@ -107,7 +107,7 @@ static class PlaceWorkerTrapPatch { static IEnumerable Transpiler(ILGenerator gen, IEnumerable e, MethodBase original) { - List insts = (List)e; + List insts = e.ToList(); Label label = gen.DefineLabel(); var finder = new CodeFinder(original, insts); @@ -206,7 +206,7 @@ static class HaulPlaceBlockerInPatch { static IEnumerable Transpiler(ILGenerator gen, IEnumerable e, MethodBase original) { - List insts = (List)e; + List insts = e.ToList(); Label label = gen.DefineLabel(); CodeFinder finder = new CodeFinder(original, insts); @@ -270,7 +270,7 @@ static class DisableInstaBuild static IEnumerable Transpiler(IEnumerable e, MethodBase original) { - List insts = (List)e; + List insts = e.ToList(); int pos = new CodeFinder(original, insts).Forward(OpCodes.Call, GetStatValueAbstract); insts[pos + 1] = new CodeInstruction(OpCodes.Call, WorkToBuildMethod); diff --git a/Source/Client/FactionContext.cs b/Source/Client/Factions/FactionContext.cs similarity index 97% rename from Source/Client/FactionContext.cs rename to Source/Client/Factions/FactionContext.cs index a19e9260..0c525313 100644 --- a/Source/Client/FactionContext.cs +++ b/Source/Client/Factions/FactionContext.cs @@ -1,5 +1,3 @@ -//extern alias zip; - using System.Collections.Generic; using RimWorld; using Verse; diff --git a/Source/Client/Patches/FactionRepeater.cs b/Source/Client/Factions/FactionRepeater.cs similarity index 100% rename from Source/Client/Patches/FactionRepeater.cs rename to Source/Client/Factions/FactionRepeater.cs diff --git a/Source/Client/Factions/Multifaction.cs b/Source/Client/Factions/Multifaction.cs new file mode 100644 index 00000000..42543a77 --- /dev/null +++ b/Source/Client/Factions/Multifaction.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.API; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client.Patches; + +[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] +static class DisableDraftGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) + { + return __instance.pawn.Faction == Faction.OfPlayer ? gizmos : Enumerable.Empty(); + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.GetGizmos))] +static class PawnChangeRelationGizmo +{ + static IEnumerable Postfix(IEnumerable gizmos, Pawn __instance) + { + foreach (var gizmo in gizmos) + yield return gizmo; + + if (__instance.Faction is { IsPlayer: true } && __instance.Faction != Faction.OfPlayer) + { + var otherFaction = __instance.Faction; + + yield return new Command_Action() + { + defaultLabel = "Change faction relation", + action = () => + { + List list = new List(); + for (int i = 0; i <= 2; i++) + { + var kind = (FactionRelationKind)i; + list.Add(new FloatMenuOption(kind.ToString(), () => { SetFactionRelation(otherFaction, kind); })); + } + + Find.WindowStack.Add(new FloatMenu(list)); + } + }; + } + } + + [SyncMethod] + static void SetFactionRelation(Faction other, FactionRelationKind kind) + { + Faction.OfPlayer.SetRelation(new FactionRelation(other, kind)); + } +} + +[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] +static class CheckDefeatedPatch +{ + static bool Prefix(Settlement factionBase) + { + return factionBase.Faction is not { IsPlayer: true }; + } +} + +[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] +static class CheckRemoveMapNowPatch +{ + static bool Prefix(MapParent __instance) + { + return __instance.Faction is not { IsPlayer: true }; + } +} + +// todo this is temporary +// [HarmonyPatch(typeof(GoodwillSituationManager), nameof(GoodwillSituationManager.GoodwillManagerTick))] +// static class GoodwillManagerTickCancel +// { +// static bool Prefix() => false; +// } diff --git a/Source/Client/MpSettings.cs b/Source/Client/MpSettings.cs deleted file mode 100644 index 97497a52..00000000 --- a/Source/Client/MpSettings.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Multiplayer.Client.Saving; -using Multiplayer.Client.Util; -using Multiplayer.Common; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client -{ - - public class MpSettings : ModSettings - { - public string username; - public bool showCursors = true; - public bool autoAcceptSteam; - public bool transparentChat = true; - public int autosaveSlots = 5; - public bool showDevInfo; - public int desyncTracesRadius = 40; - public string serverAddress = "127.0.0.1"; - public bool appendNameToAutosave; - public bool showModCompatibility = true; - public bool hideTranslationMods = true; - public bool enablePings = true; - public KeyCode? sendPingButton = KeyCode.Mouse4; - public KeyCode? jumpToPingButton = KeyCode.Mouse3; - public Rect chatRect; - public Vector2 resolutionForChat; - public bool showMainMenuAnim = true; - public DesyncTracingMode desyncTracingMode = DesyncTracingMode.Fast; - public bool transparentPlayerCursors = true; - public List playerColors = new(DefaultPlayerColors); - private (string r, string g, string b)[] colorsBuffer = { }; - - private Vector2 scrollPosition = Vector2.zero; - private SettingsPage currentPage = SettingsPage.General; - - private static readonly ColorRGB[] DefaultPlayerColors = - { - new(0,125,255), - new(255,0,0), - new(0,255,45), - new(255,0,150), - new(80,250,250), - new(200,255,75), - new(100,0,75) - }; - - public ServerSettings serverSettings = new(); - - public override void ExposeData() - { - // Remember to mirror the default values - - Scribe_Values.Look(ref username, "username"); - Scribe_Values.Look(ref showCursors, "showCursors", true); - Scribe_Values.Look(ref autoAcceptSteam, "autoAcceptSteam"); - Scribe_Values.Look(ref transparentChat, "transparentChat", true); - Scribe_Values.Look(ref autosaveSlots, "autosaveSlots", 5); - Scribe_Values.Look(ref showDevInfo, "showDevInfo"); - Scribe_Values.Look(ref desyncTracesRadius, "desyncTracesRadius", 40); - Scribe_Values.Look(ref serverAddress, "serverAddress", "127.0.0.1"); - Scribe_Values.Look(ref showModCompatibility, "showModCompatibility", true); - Scribe_Values.Look(ref hideTranslationMods, "hideTranslationMods", true); - Scribe_Values.Look(ref enablePings, "enablePings", true); - Scribe_Values.Look(ref sendPingButton, "sendPingButton", KeyCode.Mouse4); - Scribe_Values.Look(ref jumpToPingButton, "jumpToPingButton", KeyCode.Mouse3); - Scribe_Custom.LookRect(ref chatRect, "chatRect"); - Scribe_Values.Look(ref resolutionForChat, "resolutionForChat"); - Scribe_Values.Look(ref showMainMenuAnim, "showMainMenuAnim", true); - Scribe_Values.Look(ref appendNameToAutosave, "appendNameToAutosave"); - Scribe_Values.Look(ref transparentPlayerCursors, "transparentPlayerCursors", true); - - Scribe_Collections.Look(ref playerColors, "playerColors", LookMode.Deep); - if (playerColors.NullOrEmpty()) - playerColors = new List(DefaultPlayerColors); - if (Scribe.mode == LoadSaveMode.PostLoadInit) - PlayerManager.PlayerColors = playerColors.ToArray(); - - Scribe_Deep.Look(ref serverSettings, "serverSettings"); - serverSettings ??= new ServerSettings(); - } - - private string slotsBuffer; - private string desyncRadiusBuffer; - - public void DoSettingsWindowContents(Rect inRect) - { - var buttonPos = new Rect(inRect.xMax - 150, inRect.yMin + 10, 125, 32); - - Widgets.Dropdown(buttonPos, currentPage, x => x, GeneratePageMenu, $"MpSettingsPage{currentPage}".Translate()); - - switch (currentPage) - { - default: - case SettingsPage.General: - DoGeneralSettings(inRect, buttonPos); - break; - case SettingsPage.Color: - DoColorContents(inRect, buttonPos); - break; - } - } - - private IEnumerable> GeneratePageMenu(SettingsPage p) - { - return from SettingsPage page in Enum.GetValues(typeof(SettingsPage)) - where page != p - select new Widgets.DropdownMenuElement - { - option = new FloatMenuOption($"MpSettingsPage{page}".Translate(), () => - { - currentPage = page; - scrollPosition = Vector2.zero; - }) - { - tooltip = page == SettingsPage.Color ? "MpSettingsPageColorDesc".Translate() : null - }, - payload = page, - }; - } - - public void DoGeneralSettings(Rect inRect, Rect pageButtonPos) - { - var listing = new Listing_Standard(); - listing.Begin(inRect); - listing.ColumnWidth = 270f; - - DoUsernameField(listing); - listing.TextFieldNumericLabeled("MpAutosaveSlots".Translate() + ": ", ref autosaveSlots, ref slotsBuffer, 1f, 99f); - - listing.CheckboxLabeled("MpShowPlayerCursors".Translate(), ref showCursors); - listing.CheckboxLabeled("MpPlayerCursorTransparency".Translate(), ref transparentPlayerCursors); - listing.CheckboxLabeled("MpAutoAcceptSteam".Translate(), ref autoAcceptSteam, "MpAutoAcceptSteamDesc".Translate()); - listing.CheckboxLabeled("MpTransparentChat".Translate(), ref transparentChat); - listing.CheckboxLabeled("MpAppendNameToAutosave".Translate(), ref appendNameToAutosave); - listing.CheckboxLabeled("MpShowModCompat".Translate(), ref showModCompatibility, "MpShowModCompatDesc".Translate()); - listing.CheckboxLabeled("MpEnablePingsSetting".Translate(), ref enablePings); - listing.CheckboxLabeled("MpShowMainMenuAnimation".Translate(), ref showMainMenuAnim); - - const string buttonOff = "Off"; - - using (MpStyle.Set(TextAnchor.MiddleCenter)) - if (listing.ButtonTextLabeled("MpPingLocButtonSetting".Translate(), sendPingButton != null ? $"Mouse {sendPingButton - (int)KeyCode.Mouse0 + 1}" : buttonOff)) - Find.WindowStack.Add(new FloatMenu(new List(ButtonChooser(b => sendPingButton = b)))); - - using (MpStyle.Set(TextAnchor.MiddleCenter)) - if (listing.ButtonTextLabeled("MpJumpToPingButtonSetting".Translate(), jumpToPingButton != null ? $"Mouse {jumpToPingButton - (int)KeyCode.Mouse0 + 1}" : buttonOff)) - Find.WindowStack.Add(new FloatMenu(new List(ButtonChooser(b => jumpToPingButton = b)))); - - if (Prefs.DevMode) - { - listing.CheckboxLabeled("Show debug info", ref showDevInfo); - listing.TextFieldNumericLabeled("Desync radius: ", ref desyncTracesRadius, ref desyncRadiusBuffer, 1f, 200f); - -#if DEBUG - using (MpStyle.Set(TextAnchor.MiddleCenter)) - if (listing.ButtonTextLabeled("Desync tracing mode", desyncTracingMode.ToString())) - desyncTracingMode = desyncTracingMode.Cycle(); -#endif - } - - listing.End(); - - IEnumerable ButtonChooser(Action setter) - { - yield return new FloatMenuOption(buttonOff, () => { setter(null); }); - - for (var btn = 0; btn < 5; btn++) - { - var b = btn; - yield return new FloatMenuOption($"Mouse {b + 3}", () => { setter(KeyCode.Mouse2 + b); }); - } - } - } - - private void DoColorContents(Rect inRect, Rect pageButtonPos) - { - var viewRect = new Rect(inRect) - { - height = (playerColors.Count + 1) * 32f, - width = inRect.width - 20f, - }; - - var rect = new Rect(pageButtonPos.xMin - 150, pageButtonPos.yMin, 125, 32); - if (Widgets.ButtonText(rect, "MpResetColors".Translate())) - { - playerColors = new List(DefaultPlayerColors); - PlayerManager.PlayerColors = playerColors.ToArray(); - } - - if (playerColors.Count != colorsBuffer.Length) - { - colorsBuffer = new (string r, string g, string b)[playerColors.Count]; - } - - Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect); - - var toRemove = -1; - for (var i = 0; i < playerColors.Count; i++) - { - var colors = playerColors[i]; - if (DrawColorRow(i * 32 + 120, ref colors, ref colorsBuffer[i], out var edited)) - toRemove = i; - if (edited) - { - playerColors[i] = colors; - PlayerManager.PlayerColors = playerColors.ToArray(); - } - } - - rect = new Rect(402, playerColors.Count * 32 + 118, 32, 32); - if (Widgets.ButtonText(rect, "+")) - { - var rand = new System.Random(); - playerColors.Add(new ColorRGB((byte)rand.Next(256), (byte)rand.Next(256), (byte)rand.Next(256))); - PlayerManager.PlayerColors = playerColors.ToArray(); - } - - Widgets.EndScrollView(); - - if (toRemove >= 0) - { - playerColors.RemoveAt(toRemove); - PlayerManager.PlayerColors = playerColors.ToArray(); - } - } - - private bool DrawColorRow(int pos, ref ColorRGB color, ref (string r, string g, string b) buffer, out bool edited) - { - var (r, g, b) = ((int)color.r, (int)color.g, (int)color.b); - var rect = new Rect(10, pos, 100, 28); - Widgets.TextFieldNumericLabeled(rect, "R", ref r, ref buffer.r, 0, 255); - rect = new Rect(120, pos, 100, 28); - Widgets.TextFieldNumericLabeled(rect, "G", ref g, ref buffer.g, 0, 255); - rect = new Rect(230, pos, 100, 28); - Widgets.TextFieldNumericLabeled(rect, "B", ref b, ref buffer.b, 0, 255); - - rect = new Rect(350, pos - 2, 32, 32); - Widgets.DrawBoxSolid(rect, color); - - if (color.r != r || color.g != g || color.b != b) - { - color = new ColorRGB((byte)r, (byte)g, (byte)b); - edited = true; - } - else edited = false; - - if (playerColors.Count > 1) - { - rect = new Rect(402, pos - 2, 32, 32); - return Widgets.ButtonText(rect, "-"); - } - return false; - } - - const string UsernameField = "UsernameField"; - - private void DoUsernameField(Listing_Standard listing) - { - GUI.SetNextControlName(UsernameField); - - var prevField = username; - var fieldStr = listing.TextEntryLabeled("MpUsernameSetting".Translate() + ": ", username); - - if (prevField != fieldStr && fieldStr.Length <= 15 && ServerJoiningState.UsernamePattern.IsMatch(fieldStr)) - { - username = fieldStr; - Multiplayer.username = fieldStr; - } - - // Don't allow changing the username while playing - if (Multiplayer.Client != null && GUI.GetNameOfFocusedControl() == UsernameField) - UI.UnfocusCurrentControl(); - } - - private enum SettingsPage - { - General, Color, - } - } - - public enum DesyncTracingMode - { - None, Fast, Slow - } -} diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index 92ede428..4c13f35d 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -11,13 +10,14 @@ using Multiplayer.Common; using System.Runtime.CompilerServices; +using System.Threading; +using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; -using Multiplayer.Client.Patches; using Multiplayer.Client.Util; namespace Multiplayer.Client { - public class Multiplayer : Mod + public static class Multiplayer { public static Harmony harmony = new("multiplayer"); public static MpSettings settings; @@ -25,8 +25,10 @@ public class Multiplayer : Mod public static MultiplayerGame game; public static MultiplayerSession session; - public static ConnectionBase Client => session?.client; public static MultiplayerServer LocalServer { get; set; } + public static Thread localServerThread; + + public static ConnectionBase Client => session?.client; public static PacketLogWindow WriterLog => session?.writerLog; public static PacketLogWindow ReaderLog => session?.readerLog; public static bool IsReplay => session?.replay ?? false; @@ -38,14 +40,15 @@ public class Multiplayer : Mod public static IdBlock GlobalIdBlock => game.gameComp.globalIdBlock; public static MultiplayerGameComp GameComp => game.gameComp; public static MultiplayerWorldComp WorldComp => game.worldComp; + public static AsyncWorldTimeComp AsyncWorldTime => game.asyncWorldTimeComp; public static bool ShowDevInfo => Prefs.DevMode && settings.showDevInfo; public static bool GhostMode => session is { ghostModeCheckbox: true }; public static Faction RealPlayerFaction => Client != null ? game.RealPlayerFaction : Faction.OfPlayer; - public static bool ExecutingCmds => MultiplayerWorldComp.executingCmdWorld || AsyncTimeComp.executingCmdMap != null; - public static bool Ticking => MultiplayerWorldComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; + public static bool ExecutingCmds => AsyncWorldTimeComp.executingCmdWorld || AsyncTimeComp.executingCmdMap != null; + public static bool Ticking => AsyncWorldTimeComp.tickingWorld || AsyncTimeComp.tickingMap != null || ConstantTicker.ticking; public static Map MapContext => AsyncTimeComp.tickingMap ?? AsyncTimeComp.executingCmdMap; public static bool dontSync; @@ -70,16 +73,19 @@ public class Multiplayer : Mod public static string restartConnect; public static bool restartConfigs; - public Multiplayer(ModContentPack pack) : base(pack) + public static void InitMultiplayer() { Native.EarlyInit(); DisableOmitFramePointer(); + MultiplayerLoader.Multiplayer.settingsWindowDrawer = + rect => MpSettingsUI.DoSettingsWindowContents(settings, rect); + using (DeepProfilerWrapper.Section("Multiplayer CacheTypeHierarchy")) - CacheTypeHierarchy(); + TypeCache.CacheTypeHierarchy(); using (DeepProfilerWrapper.Section("Multiplayer CacheTypeByName")) - CacheTypeByName(); + TypeCache.CacheTypeByName(); if (GenCommandLine.CommandLineArgPassed("profiler")) { @@ -94,19 +100,20 @@ public Multiplayer(ModContentPack pack) : base(pack) arbiterInstance = true; } - settings = GetSettings(); + ScribeLike.provider = new ScribeProvider(); + settings = MultiplayerLoader.Multiplayer.instance!.GetSettings(); - ProcessEnvironment(); + EarlyInit.ProcessEnvironment(); SyncDict.Init(); - EarlyPatches(); - InitSync(); + EarlyInit.EarlyPatches(harmony); + EarlyInit.InitSync(); CheckInterfaceVersions(); LongEventHandler.ExecuteWhenFinished(() => { // Double Execute ensures it'll run last. - LongEventHandler.ExecuteWhenFinished(LatePatches); + LongEventHandler.ExecuteWhenFinished(() => EarlyInit.LatePatches(harmony)); }); #if DEBUG @@ -120,121 +127,6 @@ private static void DisableOmitFramePointer() Native.mini_parse_debug_option("disable_omit_fp"); } - public const string RestartConnectVariable = "MultiplayerRestartConnect"; - public const string RestartConfigsVariable = "MultiplayerRestartConfigs"; - - private static void ProcessEnvironment() - { - if (!Environment.GetEnvironmentVariable(RestartConnectVariable).NullOrEmpty()) - { - restartConnect = Environment.GetEnvironmentVariable(RestartConnectVariable); - Environment.SetEnvironmentVariable(RestartConnectVariable, ""); // Effectively unsets it - } - - if (!Environment.GetEnvironmentVariable(RestartConfigsVariable).NullOrEmpty()) - { - restartConfigs = Environment.GetEnvironmentVariable(RestartConfigsVariable) == "true"; - Environment.SetEnvironmentVariable(RestartConfigsVariable, ""); - } - } - - internal static Dictionary> subClasses = new(); - internal static Dictionary> subClassesNonAbstract = new(); - internal static Dictionary> implementations = new(); - - private static void CacheTypeHierarchy() - { - foreach (var type in GenTypes.AllTypes) - { - for (var baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) - { - subClasses.GetOrAddNew(baseType).Add(type); - if (!type.IsAbstract) - subClassesNonAbstract.GetOrAddNew(baseType).Add(type); - } - - foreach (var i in type.GetInterfaces()) - implementations.GetOrAddNew(i).Add(type); - } - } - - internal static Dictionary typeByName = new(); - internal static Dictionary typeByFullName = new(); - - private static void CacheTypeByName() - { - foreach (var type in GenTypes.AllTypes) - { - if (!typeByName.ContainsKey(type.Name)) - typeByName[type.Name] = type; - - if (!typeByFullName.ContainsKey(type.Name)) - typeByFullName[type.FullName] = type; - } - } - - private static void EarlyPatches() - { - // Might fix some mod desyncs - harmony.PatchMeasure( - AccessTools.Constructor(typeof(Def), new Type[0]), - new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Prefix)), - new HarmonyMethod(typeof(RandPatches), nameof(RandPatches.Postfix)) - ); - - Assembly.GetCallingAssembly().GetTypes().Do(type => { - if (type.IsDefined(typeof(EarlyPatchAttribute))) - harmony.CreateClassProcessor(type).Patch(); - }); - -#if DEBUG - DebugPatches.Init(); -#endif - } - - private static void InitSync() - { - using (DeepProfilerWrapper.Section("Multiplayer CollectTypes")) - SyncSerialization.Init(); - - using (DeepProfilerWrapper.Section("Multiplayer SyncGame")) - SyncGame.Init(); - - using (DeepProfilerWrapper.Section("Multiplayer Sync register attributes")) - Sync.RegisterAllAttributes(typeof(Multiplayer).Assembly); - - using (DeepProfilerWrapper.Section("Multiplayer Sync validation")) - Sync.ValidateAll(); - } - - private static void LatePatches() - { - // optimization, cache DescendantThingDefs - harmony.PatchMeasure( - AccessTools.Method(typeof(ThingCategoryDef), "get_DescendantThingDefs"), - new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Prefix"), - new HarmonyMethod(typeof(ThingCategoryDef_DescendantThingDefsPatch), "Postfix") - ); - - // optimization, cache ThisAndChildCategoryDefs - harmony.PatchMeasure( - AccessTools.Method(typeof(ThingCategoryDef), "get_ThisAndChildCategoryDefs"), - new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Prefix"), - new HarmonyMethod(typeof(ThingCategoryDef_ThisAndChildCategoryDefsPatch), "Postfix") - ); - - if (MpVersion.IsDebug) { - Log.Message("== Structure == \n" + SyncDict.syncWorkers.PrintStructure()); - } - } - - public override void DoSettingsWindowContents(Rect inRect) - { - settings.DoSettingsWindowContents(inRect); - } - - public override string SettingsCategory() => "Multiplayer"; - private static void CheckInterfaceVersions() { var mpAssembly = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "Multiplayer"); @@ -301,7 +193,7 @@ public static void StopMultiplayer() if (LocalServer != null) { LocalServer.running = false; - LocalServer.serverThread?.Join(); + localServerThread?.Join(); LocalServer.TryStop(); LocalServer = null; } @@ -320,10 +212,5 @@ public static void StopMultiplayer() Application.Quit(); } } - - public static void WriteSettingsToDisk() - { - LoadedModManager.GetMod().WriteSettings(); - } } } diff --git a/Source/Multiplayer.csproj b/Source/Client/Multiplayer.csproj similarity index 55% rename from Source/Multiplayer.csproj rename to Source/Client/Multiplayer.csproj index cb05019b..694161fa 100644 --- a/Source/Multiplayer.csproj +++ b/Source/Client/Multiplayer.csproj @@ -5,12 +5,13 @@ true 10 false - ..\Assemblies\ + ..\..\AssembliesCustom\ false false - None 0.6.2 false + Multiplayer.Client + Multiplayer @@ -22,17 +23,14 @@ - + - - - - - + + @@ -41,15 +39,23 @@ - + - - - - zip - - + + + + + + + + + + + + + + diff --git a/Source/Common/MultiplayerAPIBridge.cs b/Source/Client/MultiplayerAPIBridge.cs similarity index 99% rename from Source/Common/MultiplayerAPIBridge.cs rename to Source/Client/MultiplayerAPIBridge.cs index ad64d3af..a6a05727 100644 --- a/Source/Common/MultiplayerAPIBridge.cs +++ b/Source/Client/MultiplayerAPIBridge.cs @@ -3,6 +3,7 @@ using Multiplayer.API; using Multiplayer.Client; +// ReSharper disable once CheckNamespace namespace Multiplayer.Common { // Note: the API expects this type to be Multiplayer.Common.MultiplayerAPIBridge diff --git a/Source/Client/MultiplayerData.cs b/Source/Client/MultiplayerData.cs index 2340eac2..ceec4357 100644 --- a/Source/Client/MultiplayerData.cs +++ b/Source/Client/MultiplayerData.cs @@ -38,13 +38,22 @@ public static bool IsTranslationMod(ModMetaData mod) if (translationMods.TryGetValue(mod.RootDir.FullName, out var xml)) return xml; - var dummyPack = DummyContentPack(mod); + // ModContentPack.InitLoadFolders can throw "Illegal characters in path" + try + { + var dummyPack = DummyContentPack(mod); - return translationMods[mod.RootDir.FullName] = - !ModHasAssemblies(mod) - && !ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Defs/", _ => true).Any() - && !ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Patches/", _ => true).Any() - && ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Languages/", _ => true).Any(); + return translationMods[mod.RootDir.FullName] = + !ModHasAssemblies(mod) + && !ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Defs/", _ => true).Any() + && !ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Patches/", _ => true).Any() + && ModContentPack.GetAllFilesForModPreserveOrder(dummyPack, "Languages/", _ => true).Any(); + } + catch (Exception e) + { + Log.Error($"Error getting information about mod {mod.RootDir.FullName}: {e}"); + return translationMods[mod.RootDir.FullName] = false; + } } public static bool ModHasAssemblies(ModMetaData mod) @@ -108,18 +117,19 @@ internal static void CollectDefInfos() int TypeHash(Type type) => GenText.StableStringHash(type.FullName); - dict["ThingComp"] = GetDefInfo(ImplSerialization.thingCompTypes, TypeHash); - dict["AbilityComp"] = GetDefInfo(ImplSerialization.abilityCompTypes, TypeHash); - dict["Designator"] = GetDefInfo(ImplSerialization.designatorTypes, TypeHash); - dict["WorldObjectComp"] = GetDefInfo(ImplSerialization.worldObjectCompTypes, TypeHash); - dict["HediffComp"] = GetDefInfo(ImplSerialization.hediffCompTypes, TypeHash); - dict["IStoreSettingsParent"] = GetDefInfo(ImplSerialization.storageParents, TypeHash); - dict["IPlantToGrowSettable"] = GetDefInfo(ImplSerialization.plantToGrowSettables, TypeHash); + dict["ThingComp"] = GetDefInfo(RwImplSerialization.thingCompTypes, TypeHash); + dict["AbilityComp"] = GetDefInfo(RwImplSerialization.abilityCompTypes, TypeHash); + dict["Designator"] = GetDefInfo(RwImplSerialization.designatorTypes, TypeHash); + dict["WorldObjectComp"] = GetDefInfo(RwImplSerialization.worldObjectCompTypes, TypeHash); + dict["HediffComp"] = GetDefInfo(RwImplSerialization.hediffCompTypes, TypeHash); + dict["IStoreSettingsParent"] = GetDefInfo(RwImplSerialization.storageParents, TypeHash); + dict["IPlantToGrowSettable"] = GetDefInfo(RwImplSerialization.plantToGrowSettables, TypeHash); dict["DefTypes"] = GetDefInfo(DefSerialization.DefTypes, TypeHash); - dict["GameComponent"] = GetDefInfo(ImplSerialization.gameCompTypes, TypeHash); - dict["WorldComponent"] = GetDefInfo(ImplSerialization.worldCompTypes, TypeHash); - dict["MapComponent"] = GetDefInfo(ImplSerialization.mapCompTypes, TypeHash); + dict["GameComponent"] = GetDefInfo(RwImplSerialization.gameCompTypes, TypeHash); + dict["WorldComponent"] = GetDefInfo(RwImplSerialization.worldCompTypes, TypeHash); + dict["MapComponent"] = GetDefInfo(RwImplSerialization.mapCompTypes, TypeHash); + dict["ISyncSimple"] = GetDefInfo(ImplSerialization.syncSimples, TypeHash); dict["PawnBio"] = GetDefInfo(SolidBioDatabase.allBios, b => b.name.GetHashCode()); diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index 38d4a690..a633647e 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -4,17 +4,21 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; using Multiplayer.Client.Persistent; +using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { + [HotSwappable] public class MultiplayerGame { public SyncCoordinator sync = new(); + public AsyncWorldTimeComp asyncWorldTimeComp; public MultiplayerWorldComp worldComp; public MultiplayerGameComp gameComp; public List mapComps = new(); @@ -141,8 +145,8 @@ public void ChangeRealPlayerFaction(Faction newFaction) foreach (Map m in Find.Maps) m.MpComp().SetFaction(newFaction); - Find.ColonistBar.MarkColonistsDirty(); - Find.CurrentMap.mapDrawer.RegenerateEverythingNow(); + Find.ColonistBar?.MarkColonistsDirty(); + Find.CurrentMap?.mapDrawer.RegenerateEverythingNow(); } } } diff --git a/Source/Client/MultiplayerSession.cs b/Source/Client/MultiplayerSession.cs index be52a23b..7219d0de 100644 --- a/Source/Client/MultiplayerSession.cs +++ b/Source/Client/MultiplayerSession.cs @@ -32,7 +32,7 @@ public class MultiplayerSession : IConnectionStatusListener public PacketLogWindow readerLog = new(); public int myFactionId; public List players = new(); - public GameDataSnapshot dataSnapshot = new(); + public GameDataSnapshot dataSnapshot; public CursorAndPing cursorAndPing = new(); public int autosaveCounter; public float? lastSaveAt; @@ -70,7 +70,7 @@ public void Stop() if (client != null) { client.Close(MpDisconnectReason.Internal); - client.State = ConnectionStateEnum.Disconnected; + client.ChangeState(ConnectionStateEnum.Disconnected); } netClient?.Stop(); @@ -113,7 +113,7 @@ public void AddMsg(ChatMsg msg, bool notify = true) public void NotifyChat() { hasUnread = true; - SoundDefOf.PageChange.PlayOneShotOnCamera(null); + SoundDefOf.PageChange.PlayOneShotOnCamera(); } public void ProcessDisconnectPacket(MpDisconnectReason reason, byte[] data) @@ -221,12 +221,12 @@ public void ProcessTimeControl() public void ScheduleCommand(ScheduledCommand cmd) { MpLog.Debug(cmd.ToString()); - dataSnapshot.mapCmds.GetOrAddNew(cmd.mapId).Add(cmd); + dataSnapshot.MapCmds.GetOrAddNew(cmd.mapId).Add(cmd); if (Current.ProgramState != ProgramState.Playing) return; if (cmd.mapId == ScheduledCommand.Global) - Multiplayer.WorldComp.cmds.Enqueue(cmd); + Multiplayer.AsyncWorldTime.cmds.Enqueue(cmd); else cmd.GetMap()?.AsyncTime().cmds.Enqueue(cmd); } @@ -240,7 +240,7 @@ public static void DoAutosave() { LongEventHandler.QueueLongEvent(() => { - SaveGameToFile(GetNextAutosaveFileName()); + SaveGameToFile(GetNextAutosaveFileName(), false); Multiplayer.Client.Send(Packets.Client_Autosaving); }, "MpSaving", false, null); } @@ -259,14 +259,16 @@ private static string GetNextAutosaveFileName() .First(); } - public static void SaveGameToFile(string fileNameNoExtension) + public static void SaveGameToFile(string fileNameNoExtension, bool currentReplay) { Log.Message($"Multiplayer: saving to file {fileNameNoExtension}"); try { new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.zip")).Delete(); - Replay.ForSaving(fileNameNoExtension).WriteData(SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData())); + Replay.ForSaving(fileNameNoExtension).WriteData( + currentReplay ? Multiplayer.session.dataSnapshot : SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData()) + ); Messages.Message("MpGameSaved".Translate(fileNameNoExtension), MessageTypeDefOf.SilentInput, false); Multiplayer.session.lastSaveAt = Time.realtimeSinceStartup; } @@ -279,9 +281,11 @@ public static void SaveGameToFile(string fileNameNoExtension) public static void DoRejoin() { - Multiplayer.Client.State = ConnectionStateEnum.ClientJoining; - Multiplayer.Client.Send(Packets.Client_WorldRequest); - ((ClientJoiningState)Multiplayer.Client.StateObj).subState = JoiningState.Waiting; + Multiplayer.Client.Send(Packets.Client_RequestRejoin); + + Multiplayer.Client.ChangeState(ConnectionStateEnum.ClientLoading); + Multiplayer.Client.GetState()!.subState = LoadingState.Waiting; + Multiplayer.Client.Lenient = true; Multiplayer.session.desynced = false; @@ -312,16 +316,13 @@ public struct SessionDisconnectInfo public bool wideWindow; } - public class GameDataSnapshot - { - public int cachedAtTime; - public byte[] gameData; - public byte[] semiPersistentData; - public Dictionary mapData = new(); - - // Global cmds are -1 - public Dictionary> mapCmds = new(); - } + public record GameDataSnapshot( + int CachedAtTime, + byte[] GameData, + byte[] SemiPersistentData, + Dictionary MapData, + Dictionary> MapCmds // Global cmds are -1, this is mutated by MultiplayerSession.ScheduleCommand + ); public class PlayerInfo { @@ -332,6 +333,7 @@ public class PlayerInfo public int latency; public int ticksBehind; public bool simulating; + public float frameTime; public PlayerType type; public PlayerStatus status; public Color color; diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 80614c72..b3eded21 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -1,10 +1,11 @@ -//extern alias zip; - using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using System.Text.RegularExpressions; using HarmonyLib; using LiteNetLib; @@ -17,6 +18,7 @@ using Verse; using Verse.Sound; using Verse.Steam; +using Debug = UnityEngine.Debug; namespace Multiplayer.Client { @@ -37,8 +39,8 @@ static MultiplayerStatic() Native.InitLmfPtr(); // UnityEngine.Debug.Log instead of Verse.Log.Message because the server runs on its own thread - ServerLog.info = str => UnityEngine.Debug.Log($"MpServerLog: {str}"); - ServerLog.error = str => UnityEngine.Debug.Log($"MpServerLog Error: {str}"); + ServerLog.info = str => Debug.Log($"MpServerLog: {str}"); + ServerLog.error = str => Debug.Log($"MpServerLog Error: {str}"); NetDebug.Logger = new ServerLog(); SetUsername(); @@ -55,6 +57,7 @@ static MultiplayerStatic() MpConnectionState.SetImplementation(ConnectionStateEnum.ClientSteam, typeof(ClientSteamState)); MpConnectionState.SetImplementation(ConnectionStateEnum.ClientJoining, typeof(ClientJoiningState)); + MpConnectionState.SetImplementation(ConnectionStateEnum.ClientLoading, typeof(ClientLoadingState)); MpConnectionState.SetImplementation(ConnectionStateEnum.ClientPlaying, typeof(ClientPlayingState)); MultiplayerData.CollectCursorIcons(); @@ -166,18 +169,67 @@ private static void HandleCommandLine() if (GenCommandLine.TryGetCommandLineArg("replay", out string replay)) { + GenCommandLine.TryGetCommandLineArg("replaydata", out string replayData); + var replays = replay.Split(';').ToList().GetEnumerator(); + DoubleLongEvent(() => { - Replay.LoadReplay(Replay.ReplayFile(replay), true, () => + void LoadNextReplay() { - var rand = Find.Maps.Select(m => m.AsyncTime().randState).Select(s => $"{s} {(uint)s} {s >> 32}"); + if (!replays.MoveNext()) + { + Application.Quit(); + return; + } + + var current = replays.Current!.Split(':'); + int totalTicks = int.Parse(current[2]); + int batchSize = int.Parse(current[3]); + int ticksDone = 0; + double timeSpent = 0; - Log.Message($"timer {TickPatch.Timer}"); - Log.Message($"world rand {Multiplayer.WorldComp.randState} {(uint)Multiplayer.WorldComp.randState} {Multiplayer.WorldComp.randState >> 32}"); - Log.Message($"map rand {rand.ToStringSafeEnumerable()} | {Find.Maps.Select(m => m.AsyncTime().mapTicks).ToStringSafeEnumerable()}"); + Replay.LoadReplay(Replay.ReplayFile(current[1]), true, () => + { + TickPatch.AllTickables.Do(t => t.SetDesiredTimeSpeed(TimeSpeed.Normal)); + + void TickBatch() + { + if (ticksDone >= totalTicks) + { + if (!replayData.NullOrEmpty()) + { + string output = ""; + void Log(string text) => output += text + "\n"; + + Log($"Ticks done: {ticksDone}"); + Log($"TPS: {1000.0/(timeSpent / ticksDone)}"); + Log($"Timer: {TickPatch.Timer}"); + Log($"World: {Multiplayer.AsyncWorldTime.worldTicks}/{Multiplayer.AsyncWorldTime.randState}"); + foreach (var map in Find.Maps) + Log($"Map {map.uniqueID} rand: {map.AsyncTime().mapTicks}/{map.AsyncTime().randState}"); + + File.WriteAllText(Path.Combine(replayData, $"{current[0]}"), output); + } + + LoadNextReplay(); + return; + } + + OnMainThread.Enqueue(() => + { + var watch = Stopwatch.StartNew(); + TickPatch.DoTicks(batchSize); + timeSpent += watch.Elapsed.TotalMilliseconds; + ticksDone += batchSize; + TickBatch(); + }); + } + + TickBatch(); + }); + } - Application.Quit(); - }); + LoadNextReplay(); }, "Replay"); } diff --git a/Source/Client/Networking/ClientUtil.cs b/Source/Client/Networking/ClientUtil.cs index 39539bb3..ef4ed6c6 100644 --- a/Source/Client/Networking/ClientUtil.cs +++ b/Source/Client/Networking/ClientUtil.cs @@ -46,14 +46,14 @@ public static void TrySteamConnectWithWindow(CSteamID user, bool returnToServerB Find.WindowStack.Add(new SteamConnectingWindow(user) { returnToServerBrowser = returnToServerBrowser }); Multiplayer.session.ReapplyPrefs(); - Multiplayer.Client.State = ConnectionStateEnum.ClientSteam; + Multiplayer.Client.ChangeState(ConnectionStateEnum.ClientSteam); } public static void HandleReceive(ByteReader data, bool reliable) { try { - Multiplayer.Client.HandleReceive(data, reliable); + Multiplayer.Client.HandleReceiveRaw(data, reliable); } catch (Exception e) { diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 3e5811bc..3ed92a3e 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -1,5 +1,4 @@ using HarmonyLib; -using Ionic.Zlib; using Multiplayer.Client.Networking; using Multiplayer.Common; using RimWorld; @@ -9,19 +8,51 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; +using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; +using Multiplayer.Client.Util; +using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client { - + [HotSwappable] public static class HostUtil { - public static void HostServer(ServerSettings settings, bool fromReplay, bool hadSimulation, bool asyncTime) + // Host entry points: + // - singleplayer save, server browser + // - singleplayer save, ingame + // - replay, server browser + // - replay, ingame + public static async ClientTask HostServer(ServerSettings settings, bool fromReplay, bool hadSimulation, bool asyncTime) { Log.Message($"Starting the server"); + CreateSession(settings); + + // Server already pre-inited in HostWindow + PrepareLocalServer(settings, fromReplay); + + if (!fromReplay) + SetupGameFromSingleplayer(); + + CreateLocalClient(); + PrepareGame(); + + Multiplayer.session.dataSnapshot = await CreateGameData(settings, asyncTime); + + MakeHostOnServer(); + + // todo handle sending cmds for hosting from loaded replay? + SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, false); + + StartLocalServer(); + } + + private static void CreateSession(ServerSettings settings) + { var session = new MultiplayerSession(); if (Multiplayer.session != null) // This is the case when hosting from a replay session.dataSnapshot = Multiplayer.session.dataSnapshot; @@ -31,28 +62,15 @@ public static void HostServer(ServerSettings settings, bool fromReplay, bool had session.myFactionId = Faction.OfPlayer.loadID; session.localServerSettings = settings; session.gameName = settings.gameName; + } - // Server already pre-inited in HostWindow + private static void PrepareLocalServer(ServerSettings settings, bool fromReplay) + { var localServer = Multiplayer.LocalServer; MultiplayerServer.instance = Multiplayer.LocalServer; - if (hadSimulation) - { - localServer.savedGame = GZipStream.CompressBuffer(session.dataSnapshot.gameData); - localServer.semiPersistent = GZipStream.CompressBuffer(session.dataSnapshot.semiPersistentData); - localServer.mapData = session.dataSnapshot.mapData.ToDictionary(kv => kv.Key, kv => GZipStream.CompressBuffer(kv.Value)); - localServer.mapCmds = session.dataSnapshot.mapCmds.ToDictionary(kv => kv.Key, kv => kv.Value.Select(ScheduledCommand.Serialize).ToList()); - } - - localServer.commands.debugOnlySyncCmds = Sync.handlers.Where(h => h.debugOnly).Select(h => h.syncId).ToHashSet(); - localServer.commands.hostOnlySyncCmds = Sync.handlers.Where(h => h.hostOnly).Select(h => h.syncId).ToHashSet(); localServer.hostUsername = Multiplayer.username; - localServer.defaultFactionId = Faction.OfPlayer.loadID; - - localServer.rwVersion = VersionControl.CurrentVersionString; - localServer.mpVersion = MpVersion.Version; - localServer.defInfos = MultiplayerData.localDefInfos; - localServer.serverData = JoinData.WriteServerData(settings.syncConfigs); + localServer.worldData.defaultFactionId = Faction.OfPlayer.loadID; if (settings.steam) localServer.TickEvent += SteamIntegration.ServerSteamNetTick; @@ -60,68 +78,48 @@ public static void HostServer(ServerSettings settings, bool fromReplay, bool had if (fromReplay) localServer.gameTimer = TickPatch.Timer; - if (!fromReplay) - SetupGameFromSingleplayer(); + localServer.initDataSource = new TaskCompletionSource(); + localServer.CompleteInitData( + ServerInitData.Deserialize(new ByteReader(ClientJoiningState.PackInitData(settings.syncConfigs))) + ); + } + private static void PrepareGame() + { foreach (var tickable in TickPatch.AllTickables) tickable.Cmds.Clear(); - Find.PlaySettings.usePlanetDayNightSystem = false; - Multiplayer.game.ChangeRealPlayerFaction(Faction.OfPlayer); - SetupLocalClient(); + Multiplayer.session.ReapplyPrefs(); Find.MainTabsRoot.EscapeCurrentTab(false); Multiplayer.session.AddMsg("If you are having any issues with the mod and would like some help resolving them, then please reach out to us on our Discord server:", false); Multiplayer.session.AddMsg(new ChatMsg_Url("https://discord.gg/S4bxXpv"), false); + } - if (hadSimulation) - { - StartServerThread(); - } - else - { - Multiplayer.WorldComp.TimeSpeed = TimeSpeed.Paused; - foreach (var map in Find.Maps) - map.AsyncTime().TimeSpeed = TimeSpeed.Paused; - - Multiplayer.WorldComp.UpdateTimeSpeed(); - - Multiplayer.GameComp.asyncTime = asyncTime; - Multiplayer.GameComp.debugMode = settings.debugMode; - Multiplayer.GameComp.logDesyncTraces = settings.desyncTraces; - Multiplayer.GameComp.pauseOnLetter = settings.pauseOnLetter; - Multiplayer.GameComp.timeControl = settings.timeControl; - - LongEventHandler.QueueLongEvent(() => - { - Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); - SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, false); + private static async Task CreateGameData(ServerSettings settings, bool asyncTime) + { + Multiplayer.AsyncWorldTime.SetDesiredTimeSpeed(TimeSpeed.Paused); + foreach (var map in Find.Maps) + map.AsyncTime().SetDesiredTimeSpeed(TimeSpeed.Paused); - StartServerThread(); - }, "MpSaving", false, null); - } + Find.TickManager.CurTimeSpeed = TimeSpeed.Paused; - void StartServerThread() - { - localServer.running = true; + Multiplayer.GameComp.asyncTime = asyncTime; + Multiplayer.GameComp.debugMode = settings.debugMode; + Multiplayer.GameComp.logDesyncTraces = settings.desyncTraces; + Multiplayer.GameComp.pauseOnLetter = settings.pauseOnLetter; + Multiplayer.GameComp.timeControl = settings.timeControl; - Multiplayer.LocalServer.serverThread = new Thread(localServer.Run) - { - Name = "Local server thread" - }; - Multiplayer.LocalServer.serverThread.Start(); + await LongEventTask.ContinueInLongEvent("MpSaving", false); - const string text = "Server started."; - Messages.Message(text, MessageTypeDefOf.SilentInput, false); - Log.Message(text); - } + return SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload()); } private static void SetupGameFromSingleplayer() { - MultiplayerWorldComp comp = new MultiplayerWorldComp(Find.World); + var worldComp = new MultiplayerWorldComp(Find.World); Faction NewFaction(int id, string name, FactionDef def) { @@ -139,7 +137,7 @@ Faction NewFaction(int id, string name, FactionDef def) Find.FactionManager.Add(faction); - comp.factionData[faction.loadID] = FactionWorldData.New(faction.loadID); + worldComp.factionData[faction.loadID] = FactionWorldData.New(faction.loadID); } faction.Name = name; @@ -157,14 +155,15 @@ Faction NewFaction(int id, string name, FactionDef def) { globalIdBlock = new IdBlock(GetMaxUniqueId(), 1_000_000_000) }, - worldComp = comp + asyncWorldTimeComp = new AsyncWorldTimeComp(Find.World) { worldTicks = Find.TickManager.TicksGame }, + worldComp = worldComp }; - var opponent = NewFaction(Multiplayer.GlobalIdBlock.NextId(), "Opponent", FactionDefOf.PlayerColony); - opponent.hidden = true; - opponent.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Hostile)); + // var opponent = NewFaction(Multiplayer.GlobalIdBlock.NextId(), "Opponent", FactionDefOf.PlayerColony); + // opponent.hidden = true; + // opponent.SetRelation(new FactionRelation(Faction.OfPlayer, FactionRelationKind.Neutral)); - foreach (FactionWorldData data in comp.factionData.Values) + foreach (FactionWorldData data in worldComp.factionData.Values) { foreach (DrugPolicy p in data.drugPolicyDatabase.policies) p.uniqueId = Multiplayer.GlobalIdBlock.NextId(); @@ -181,17 +180,15 @@ Faction NewFaction(int id, string name, FactionDef def) //mapComp.mapIdBlock = localServer.NextIdBlock(); BeforeMapGeneration.SetupMap(map); - BeforeMapGeneration.InitNewMapFactionData(map, opponent); + // BeforeMapGeneration.InitNewMapFactionData(map, opponent); AsyncTimeComp async = map.AsyncTime(); async.mapTicks = Find.TickManager.TicksGame; - async.TimeSpeed = Find.TickManager.CurTimeSpeed; + async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); } - - Multiplayer.WorldComp.UpdateTimeSpeed(); } - private static void SetupLocalClient() + private static void CreateLocalClient() { if (Multiplayer.session.localServerSettings.arbiter) StartArbiter(); @@ -199,22 +196,34 @@ private static void SetupLocalClient() LocalClientConnection localClient = new LocalClientConnection(Multiplayer.username); LocalServerConnection localServerConn = new LocalServerConnection(Multiplayer.username); - localServerConn.clientSide = localClient; localClient.serverSide = localServerConn; + localServerConn.clientSide = localClient; - localClient.State = ConnectionStateEnum.ClientPlaying; - localServerConn.State = ConnectionStateEnum.ServerPlaying; - - var serverPlayer = Multiplayer.LocalServer.playerManager.OnConnected(localServerConn); - serverPlayer.color = PlayerManager.PlayerColors[0]; - PlayerManager.givenColors[serverPlayer.Username] = serverPlayer.color; - serverPlayer.status = PlayerStatus.Playing; - serverPlayer.FactionId = Faction.OfPlayer.loadID; - serverPlayer.SendPlayerList(); - Multiplayer.LocalServer.playerManager.SendInitDataCommand(serverPlayer); + localClient.ChangeState(ConnectionStateEnum.ClientPlaying); Multiplayer.session.client = localClient; - Multiplayer.session.ReapplyPrefs(); + } + + private static void MakeHostOnServer() + { + var server = Multiplayer.LocalServer; + var player = server.playerManager.OnConnected(((LocalClientConnection)Multiplayer.Client).serverSide); + server.playerManager.MakeHost(player); + } + + private static void StartLocalServer() + { + Multiplayer.LocalServer.running = true; + + Multiplayer.localServerThread = new Thread(Multiplayer.LocalServer.Run) + { + Name = "Local server thread" + }; + Multiplayer.localServerThread.Start(); + + const string text = "Server started."; + Messages.Message(text, MessageTypeDefOf.SilentInput, false); + Log.Message(text); } private static void StartArbiter() @@ -231,11 +240,11 @@ private static void StartArbiter() string arbiterInstancePath; if (Application.platform == RuntimePlatform.OSXPlayer) { - arbiterInstancePath = Application.dataPath + "/MacOS/" + Process.GetCurrentProcess().MainModule.ModuleName; + arbiterInstancePath = Application.dataPath + "/MacOS/" + Process.GetCurrentProcess().MainModule!.ModuleName; } else { - arbiterInstancePath = Process.GetCurrentProcess().MainModule.FileName; + arbiterInstancePath = Process.GetCurrentProcess().MainModule!.FileName; } try @@ -252,7 +261,7 @@ private static void StartArbiter() Log.Error(ex.ToString()); if (ex.InnerException is Win32Exception) { - Log.Error("Win32 Error Code: " + ((Win32Exception)ex).NativeErrorCode.ToString()); + Log.Error("Win32 Error Code: " + ((Win32Exception)ex).NativeErrorCode); } } } diff --git a/Source/Client/Networking/JoinData.cs b/Source/Client/Networking/JoinData.cs index 3a57dc30..09a203a1 100644 --- a/Source/Client/Networking/JoinData.cs +++ b/Source/Client/Networking/JoinData.cs @@ -124,7 +124,7 @@ public static ModMetaData GetInstalledMod(string id) } [SuppressMessage("ReSharper", "StringLiteralTypo")] - private static string[] ignoredConfigsModIds = + public static string[] ignoredConfigsModIds = { // The old mod management code also included TacticalGroupsMod.xml and GraphicSetter.xml but I couldn't find their ids // todo unhardcode it @@ -138,7 +138,8 @@ public static ModMetaData GetInstalledMod(string id) "fluffy.modmanager", "jelly.modswitch", "betterscenes.rimconnect", // contains secret key for streamer - "jaxe.rimhud" + "jaxe.rimhud", + //"zetrith.prepatcher" }; public const string TempConfigsDir = "MultiplayerTempConfigs"; diff --git a/Source/Client/Networking/NetworkingInMemory.cs b/Source/Client/Networking/NetworkingInMemory.cs index 4b343b2c..12e1c7b2 100644 --- a/Source/Client/Networking/NetworkingInMemory.cs +++ b/Source/Client/Networking/NetworkingInMemory.cs @@ -21,7 +21,7 @@ protected override void SendRaw(byte[] raw, bool reliable) { try { - serverSide.HandleReceive(new ByteReader(raw), reliable); + serverSide.HandleReceiveRaw(new ByteReader(raw), reliable); } catch (Exception e) { @@ -57,7 +57,7 @@ protected override void SendRaw(byte[] raw, bool reliable) { try { - clientSide.HandleReceive(new ByteReader(raw), reliable); + clientSide.HandleReceiveRaw(new ByteReader(raw), reliable); } catch (Exception e) { diff --git a/Source/Client/Networking/NetworkingLiteNet.cs b/Source/Client/Networking/NetworkingLiteNet.cs index aa40eb33..1451c6db 100644 --- a/Source/Client/Networking/NetworkingLiteNet.cs +++ b/Source/Client/Networking/NetworkingLiteNet.cs @@ -13,8 +13,7 @@ public void OnPeerConnected(NetPeer peer) { ConnectionBase conn = new LiteNetConnection(peer); conn.username = Multiplayer.username; - conn.State = ConnectionStateEnum.ClientJoining; - conn.StateObj.StartState(); + conn.ChangeState(ConnectionStateEnum.ClientJoining); Multiplayer.session.client = conn; Multiplayer.session.ReapplyPrefs(); diff --git a/Source/Client/Networking/NetworkingSteam.cs b/Source/Client/Networking/NetworkingSteam.cs index adbf393c..3c662ecf 100644 --- a/Source/Client/Networking/NetworkingSteam.cs +++ b/Source/Client/Networking/NetworkingSteam.cs @@ -47,8 +47,6 @@ public override void Close(MpDisconnectReason reason, byte[] data) public abstract void OnError(EP2PSessionError error); - protected abstract void OnDisconnect(); - public override string ToString() { return $"SteamP2P ({remoteId}) ({username})"; @@ -63,7 +61,7 @@ public SteamClientConn(CSteamID remoteId) : base(remoteId, RandomChannelId(), 0) { } - protected override void HandleReceive(int msgId, int fragState, ByteReader reader, bool reliable) + protected override void HandleReceiveMsg(int msgId, int fragState, ByteReader reader, bool reliable) { if (msgId == (int)Packets.Special_Steam_Disconnect) { @@ -75,7 +73,7 @@ protected override void HandleReceive(int msgId, int fragState, ByteReader reade return; } - base.HandleReceive(msgId, fragState, reader, reliable); + base.HandleReceiveMsg(msgId, fragState, reader, reliable); } public override void OnError(EP2PSessionError error) @@ -86,7 +84,7 @@ public override void OnError(EP2PSessionError error) OnDisconnect(); } - protected override void OnDisconnect() + private void OnDisconnect() { ConnectionStatusListeners.TryNotifyAll_Disconnected(); Multiplayer.StopMultiplayer(); @@ -99,7 +97,7 @@ public SteamServerConn(CSteamID remoteId, ushort clientChannel) : base(remoteId, { } - protected override void HandleReceive(int msgId, int fragState, ByteReader reader, bool reliable) + protected override void HandleReceiveMsg(int msgId, int fragState, ByteReader reader, bool reliable) { if (msgId == (int)Packets.Special_Steam_Disconnect) { @@ -107,7 +105,7 @@ protected override void HandleReceive(int msgId, int fragState, ByteReader reade return; } - base.HandleReceive(msgId, fragState, reader, reliable); + base.HandleReceiveMsg(msgId, fragState, reader, reliable); } public override void OnError(EP2PSessionError error) @@ -115,9 +113,9 @@ public override void OnError(EP2PSessionError error) OnDisconnect(); } - protected override void OnDisconnect() + private void OnDisconnect() { - serverPlayer.Server.playerManager.OnDisconnected(this, MpDisconnectReason.ClientLeft); + serverPlayer.Server.playerManager.SetDisconnected(this, MpDisconnectReason.ClientLeft); } } } diff --git a/Source/Client/Networking/State/ClientBaseState.cs b/Source/Client/Networking/State/ClientBaseState.cs index e6aab6b1..05821211 100644 --- a/Source/Client/Networking/State/ClientBaseState.cs +++ b/Source/Client/Networking/State/ClientBaseState.cs @@ -1,13 +1,12 @@ using Multiplayer.Common; -namespace Multiplayer.Client +namespace Multiplayer.Client; + +public abstract class ClientBaseState : MpConnectionState { - public abstract class ClientBaseState : MpConnectionState - { - public MultiplayerSession Session => Multiplayer.session; + protected MultiplayerSession Session => Multiplayer.session; - public ClientBaseState(ConnectionBase connection) : base(connection) - { - } + public ClientBaseState(ConnectionBase connection) : base(connection) + { } } diff --git a/Source/Client/Networking/State/ClientJoiningState.cs b/Source/Client/Networking/State/ClientJoiningState.cs index 11b18296..c9d973a5 100644 --- a/Source/Client/Networking/State/ClientJoiningState.cs +++ b/Source/Client/Networking/State/ClientJoiningState.cs @@ -1,26 +1,16 @@ using HarmonyLib; -using Ionic.Zlib; using Multiplayer.Client.Networking; using Multiplayer.Common; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Xml; +using Multiplayer.Common.Util; +using RimWorld; using Verse; -using Verse.Profile; namespace Multiplayer.Client { - public enum JoiningState - { - Connected, Waiting, Downloading - } - - + [HotSwappable] public class ClientJoiningState : ClientBaseState { - public JoiningState subState = JoiningState.Connected; - public ClientJoiningState(ConnectionBase connection) : base(connection) { } @@ -50,6 +40,24 @@ public void HandleProtocolOk(ByteReader data) } } + [PacketHandler(Packets.Server_InitDataRequest)] + public void HandleInitDataRequest(ByteReader data) + { + var includeConfigs = data.ReadBool(); + connection.SendFragmented(Packets.Client_InitData, PackInitData(includeConfigs)); + } + + public static byte[] PackInitData(bool includeConfigs) + { + return ByteWriter.GetBytes( + JoinData.WriteServerData(includeConfigs), + VersionControl.CurrentVersionString, + Sync.handlers.Where(h => h.debugOnly).Select(h => h.syncId).ToList(), + Sync.handlers.Where(h => h.hostOnly).Select(h => h.syncId).ToList(), + MultiplayerData.localDefInfos.Select(p => (p.Key, p.Value.count, p.Value.hash)).ToList() + ); + } + [PacketHandler(Packets.Server_UsernameOk)] public void HandleUsernameOk(ByteReader data) { @@ -124,157 +132,10 @@ void Complete() void StartDownloading() { connection.Send(Packets.Client_WorldRequest); - subState = JoiningState.Waiting; + connection.ChangeState(ConnectionStateEnum.ClientLoading); } } } - - [PacketHandler(Packets.Server_WorldDataStart)] - public void HandleWorldDataStart(ByteReader data) - { - subState = JoiningState.Downloading; - } - - [PacketHandler(Packets.Server_WorldData)] - [IsFragmented] - public void HandleWorldData(ByteReader data) - { - connection.State = ConnectionStateEnum.ClientPlaying; - Log.Message("Game data size: " + data.Length); - - int factionId = data.ReadInt32(); - Multiplayer.session.myFactionId = factionId; - - int tickUntil = data.ReadInt32(); - - var dataSnapshot = new GameDataSnapshot(); - - byte[] worldData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); - dataSnapshot.gameData = worldData; - - byte[] semiPersistentData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); - dataSnapshot.semiPersistentData = semiPersistentData; - - List mapsToLoad = new List(); - - int mapCmdsCount = data.ReadInt32(); - for (int i = 0; i < mapCmdsCount; i++) - { - int mapId = data.ReadInt32(); - - int mapCmdsLen = data.ReadInt32(); - List mapCmds = new List(mapCmdsLen); - for (int j = 0; j < mapCmdsLen; j++) - mapCmds.Add(ScheduledCommand.Deserialize(new ByteReader(data.ReadPrefixedBytes()))); - - dataSnapshot.mapCmds[mapId] = mapCmds; - } - - int mapDataCount = data.ReadInt32(); - for (int i = 0; i < mapDataCount; i++) - { - int mapId = data.ReadInt32(); - byte[] rawMapData = data.ReadPrefixedBytes(); - - byte[] mapData = GZipStream.UncompressBuffer(rawMapData); - dataSnapshot.mapData[mapId] = mapData; - mapsToLoad.Add(mapId); - } - - Session.dataSnapshot = dataSnapshot; - Multiplayer.session.receivedCmds = data.ReadInt32(); - TickPatch.serverFrozen = data.ReadBool(); - TickPatch.tickUntil = tickUntil; - - int syncInfos = data.ReadInt32(); - for (int i = 0; i < syncInfos; i++) - Session.initialOpinions.Add(ClientSyncOpinion.Deserialize(new ByteReader(data.ReadPrefixedBytes()))); - - Log.Message(syncInfos > 0 - ? $"Initial sync opinions: {Session.initialOpinions.First().startTick}...{Session.initialOpinions.Last().startTick}" - : "No initial sync opinions"); - - TickPatch.SetSimulation( - toTickUntil: true, - onFinish: () => Multiplayer.Client.Send(Packets.Client_WorldReady), - cancelButtonKey: "Quit", - onCancel: GenScene.GoToMainMenu // Calls StopMultiplayer through a patch - ); - - ReloadGame(mapsToLoad, true, false); - } - - private static XmlDocument GetGameDocument(List mapsToLoad) - { - XmlDocument gameDoc = ScribeUtil.LoadDocument(Multiplayer.session.dataSnapshot.gameData); - XmlNode gameNode = gameDoc.DocumentElement["game"]; - - foreach (int map in mapsToLoad) - { - using XmlReader reader = XmlReader.Create(new MemoryStream(Multiplayer.session.dataSnapshot.mapData[map])); - XmlNode mapNode = gameDoc.ReadNode(reader); - gameNode["maps"].AppendChild(mapNode); - - if (gameNode["currentMapIndex"] == null) - gameNode.AddNode("currentMapIndex", map.ToString()); - } - - return gameDoc; - } - - public static void ReloadGame(List mapsToLoad, bool changeScene, bool forceAsyncTime) - { - var gameDoc = GetGameDocument(mapsToLoad); - - LoadPatch.gameToLoad = new(gameDoc, Multiplayer.session.dataSnapshot.semiPersistentData); - TickPatch.replayTimeSpeed = TimeSpeed.Paused; - - if (changeScene) - { - LongEventHandler.QueueLongEvent(() => - { - MemoryUtility.ClearAllMapsAndWorld(); - Current.Game = new Game - { - InitData = new GameInitData - { - gameToLoad = "server" - } - }; - - LongEventHandler.ExecuteWhenFinished(() => - { - LongEventHandler.QueueLongEvent(() => PostLoad(forceAsyncTime), "MpSimulating", false, null); - }); - }, "Play", "MpLoading", true, null); - } - else - { - LongEventHandler.QueueLongEvent(() => - { - SaveLoad.LoadInMainThread(LoadPatch.gameToLoad); - PostLoad(forceAsyncTime); - }, "MpLoading", false, null); - } - } - - private static void PostLoad(bool forceAsyncTime) - { - // If the client gets disconnected during loading - if (Multiplayer.Client == null) return; - - Multiplayer.session.dataSnapshot.cachedAtTime = TickPatch.Timer; - Multiplayer.session.replayTimerStart = TickPatch.Timer; - - Multiplayer.game.ChangeRealPlayerFaction(Find.FactionManager.GetById(Multiplayer.session.myFactionId)); - Multiplayer.game.myFactionLoading = null; - - if (forceAsyncTime) - Multiplayer.game.gameComp.asyncTime = true; - - Multiplayer.WorldComp.cmds = new Queue(Multiplayer.session.dataSnapshot.mapCmds.GetValueSafe(ScheduledCommand.Global) ?? new List()); - // Map cmds are added in MapAsyncTimeComp.FinalizeInit - } } } diff --git a/Source/Client/Networking/State/ClientLoadingState.cs b/Source/Client/Networking/State/ClientLoadingState.cs new file mode 100644 index 00000000..2b41812a --- /dev/null +++ b/Source/Client/Networking/State/ClientLoadingState.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Linq; +using Ionic.Zlib; +using Multiplayer.Client.Saving; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client; + +public enum LoadingState +{ + Waiting, + Downloading +} + +public class ClientLoadingState : ClientBaseState +{ + public LoadingState subState = LoadingState.Waiting; + + public ClientLoadingState(ConnectionBase connection) : base(connection) + { + } + + [PacketHandler(Packets.Server_WorldDataStart)] + public void HandleWorldDataStart(ByteReader data) + { + subState = LoadingState.Downloading; + connection.Lenient = false; + } + + [PacketHandler(Packets.Server_WorldData)] + [IsFragmented] + public void HandleWorldData(ByteReader data) + { + Log.Message("Game data size: " + data.Length); + + int factionId = data.ReadInt32(); + Multiplayer.session.myFactionId = factionId; + + int tickUntil = data.ReadInt32(); + int remoteSentCmds = data.ReadInt32(); + bool serverFrozen = data.ReadBool(); + + byte[] worldData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); + byte[] semiPersistentData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); + + var mapCmdsDict = new Dictionary>(); + var mapDataDict = new Dictionary(); + List mapsToLoad = new List(); + + int mapCmdsCount = data.ReadInt32(); + for (int i = 0; i < mapCmdsCount; i++) + { + int mapId = data.ReadInt32(); + + int mapCmdsLen = data.ReadInt32(); + List mapCmds = new List(mapCmdsLen); + for (int j = 0; j < mapCmdsLen; j++) + mapCmds.Add(ScheduledCommand.Deserialize(new ByteReader(data.ReadPrefixedBytes()))); + + mapCmdsDict[mapId] = mapCmds; + } + + int mapDataCount = data.ReadInt32(); + for (int i = 0; i < mapDataCount; i++) + { + int mapId = data.ReadInt32(); + byte[] rawMapData = data.ReadPrefixedBytes(); + + byte[] mapData = GZipStream.UncompressBuffer(rawMapData); + mapDataDict[mapId] = mapData; + mapsToLoad.Add(mapId); + } + + //mapsToLoad.RemoveAt(Multiplayer.LocalServer != null ? 1 : 0); // todo dbg + + Session.dataSnapshot = new GameDataSnapshot( + 0, + worldData, + semiPersistentData, + mapDataDict, + mapCmdsDict + ); + + TickPatch.tickUntil = tickUntil; + Multiplayer.session.receivedCmds = remoteSentCmds; + TickPatch.serverFrozen = serverFrozen; + + int syncInfos = data.ReadInt32(); + for (int i = 0; i < syncInfos; i++) + Session.initialOpinions.Add(ClientSyncOpinion.Deserialize(new ByteReader(data.ReadPrefixedBytes()))); + + Log.Message(syncInfos > 0 + ? $"Initial sync opinions: {Session.initialOpinions.First().startTick}...{Session.initialOpinions.Last().startTick}" + : "No initial sync opinions"); + + TickPatch.SetSimulation( + toTickUntil: true, + onFinish: () => Multiplayer.Client.Send(Packets.Client_WorldReady), + cancelButtonKey: "Quit", + onCancel: GenScene.GoToMainMenu // Calls StopMultiplayer through a patch + ); + + Loader.ReloadGame(mapsToLoad, true, false); + connection.ChangeState(ConnectionStateEnum.ClientPlaying); + } +} diff --git a/Source/Client/Networking/State/ClientPlayingState.cs b/Source/Client/Networking/State/ClientPlayingState.cs index ba063c90..4ab3aabf 100644 --- a/Source/Client/Networking/State/ClientPlayingState.cs +++ b/Source/Client/Networking/State/ClientPlayingState.cs @@ -52,12 +52,6 @@ public void HandleCommand(ByteReader data) Multiplayer.session.ProcessTimeControl(); } - [PacketHandler(Packets.Server_CanRejoin)] - public void HandleCanRejoin(ByteReader data) - { - MultiplayerSession.DoRejoin(); - } - [PacketHandler(Packets.Server_PlayerList)] public void HandlePlayerList(ByteReader data) { @@ -92,6 +86,7 @@ public void HandlePlayerList(ByteReader data) player.latency = data.ReadInt32(); player.ticksBehind = data.ReadInt32(); player.simulating = data.ReadBool(); + player.frameTime = data.ReadFloat(); } } else if (action == PlayerListAction.Status) @@ -194,10 +189,10 @@ public void HandleMapResponse(ByteReader data) for (int j = 0; j < mapCmdsLen; j++) mapCmds.Add(ScheduledCommand.Deserialize(new ByteReader(data.ReadPrefixedBytes()))); - Session.dataSnapshot.mapCmds[mapId] = mapCmds; + Session.dataSnapshot.MapCmds[mapId] = mapCmds; byte[] mapData = GZipStream.UncompressBuffer(data.ReadPrefixedBytes()); - Session.dataSnapshot.mapData[mapId] = mapData; + Session.dataSnapshot.MapData[mapId] = mapData; //ClientJoiningState.ReloadGame(TickPatch.tickUntil, Find.Maps.Select(m => m.uniqueID).Concat(mapId).ToList()); // todo Multiplayer.client.Send(Packets.CLIENT_MAP_LOADED); diff --git a/Source/Client/Networking/State/ClientSteamState.cs b/Source/Client/Networking/State/ClientSteamState.cs index 05acc6f8..f6896375 100644 --- a/Source/Client/Networking/State/ClientSteamState.cs +++ b/Source/Client/Networking/State/ClientSteamState.cs @@ -16,8 +16,7 @@ public ClientSteamState(ConnectionBase connection) : base(connection) [PacketHandler(Packets.Server_SteamAccept)] public void HandleSteamAccept(ByteReader data) { - connection.State = ConnectionStateEnum.ClientJoining; - connection.StateObj.StartState(); + connection.ChangeState(ConnectionStateEnum.ClientJoining); } } diff --git a/Source/Client/Networking/SteamIntegration.cs b/Source/Client/Networking/SteamIntegration.cs index 30e3ef8e..d9ae0cd0 100644 --- a/Source/Client/Networking/SteamIntegration.cs +++ b/Source/Client/Networking/SteamIntegration.cs @@ -118,7 +118,7 @@ public static void ServerSteamNetTick(MultiplayerServer server) continue; } - conn.State = ConnectionStateEnum.ServerJoining; + conn.ChangeState(ConnectionStateEnum.ServerJoining); player = playerManager.OnConnected(conn); player.type = PlayerType.Steam; diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 537c6caf..3abdfac5 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Util; using RimWorld.QuestGen; using UnityEngine; @@ -282,7 +283,7 @@ static IEnumerable TargetMethods() static bool Prefix() { // In MP only allow updates from MultiplayerWorldComp:Tick() - return Multiplayer.Client == null || MultiplayerWorldComp.tickingWorld; + return Multiplayer.Client == null || AsyncWorldTimeComp.tickingWorld; } } diff --git a/Source/Client/Patches/LongEvents.cs b/Source/Client/Patches/LongEvents.cs index 179802d7..40e99255 100644 --- a/Source/Client/Patches/LongEvents.cs +++ b/Source/Client/Patches/LongEvents.cs @@ -14,7 +14,7 @@ static class MarkLongEvents static void Prefix(ref Action action, string textKey) { - if (Multiplayer.Client != null && (Multiplayer.Ticking || Multiplayer.ExecutingCmds || textKey == "MpSaving")) + if (Multiplayer.Client is { State: ConnectionStateEnum.ClientPlaying } && (Multiplayer.Ticking || Multiplayer.ExecutingCmds || textKey == "MpSaving")) { action += Marker; } diff --git a/Source/Client/Patches/Multifaction.cs b/Source/Client/Patches/Multifaction.cs deleted file mode 100644 index c16fe95a..00000000 --- a/Source/Client/Patches/Multifaction.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using RimWorld; -using Verse; - -namespace Multiplayer.Client.Patches; - -[HarmonyPatch(typeof(Pawn_DraftController), nameof(Pawn_DraftController.GetGizmos))] -static class DisableDraftGizmo -{ - static IEnumerable Postfix(IEnumerable gizmos, Pawn_DraftController __instance) - { - return __instance.pawn.Faction == Faction.OfPlayer ? - gizmos : - Enumerable.Empty(); - } -} - -// todo: needed for multifaction -/*[HarmonyPatch(typeof(SettlementDefeatUtility), nameof(SettlementDefeatUtility.CheckDefeated))] -static class CheckDefeatedPatch -{ - static bool Prefix() - { - return false; - } -} - -[HarmonyPatch(typeof(MapParent), nameof(MapParent.CheckRemoveMapNow))] -static class CheckRemoveMapNowPatch -{ - static bool Prefix() - { - return false; - } -}*/ diff --git a/Source/Client/Patches/MultiplayerPawnComp.cs b/Source/Client/Patches/MultiplayerPawnComp.cs index 0d186f68..895bdce1 100644 --- a/Source/Client/Patches/MultiplayerPawnComp.cs +++ b/Source/Client/Patches/MultiplayerPawnComp.cs @@ -7,6 +7,8 @@ namespace Multiplayer.Client public class MultiplayerPawnComp : ThingComp { public SituationalThoughtHandler thoughtsForInterface; + public int lastMap = -1; + public int worldPawnRemoveTick = -1; } [HarmonyPatch(typeof(ThingWithComps), nameof(ThingWithComps.InitializeComps))] diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 9fc10df0..53127bf5 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -9,6 +9,7 @@ using System.Reflection.Emit; using System.Text.RegularExpressions; using System.Xml.Linq; +using Multiplayer.Common.Util; using UnityEngine; using Verse; using Verse.AI; @@ -346,7 +347,7 @@ public static void SetupMap(Map map) async.storyWatcher = new StoryWatcher(); if (!Multiplayer.GameComp.asyncTime) - async.TimeSpeed = Find.TickManager.CurTimeSpeed; + async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); } public static void InitFactionDataFromMap(Map map, Faction f) @@ -623,4 +624,34 @@ static void Postfix(bool __state) Multiplayer.GameComp.SetGodMode(Multiplayer.session.playerId, DebugSettings.godMode); } } + + [HarmonyPatch(typeof(Settlement), nameof(Settlement.Material), MethodType.Getter)] + static class SettlementNullFactionPatch1 + { + static bool Prefix(Settlement __instance, ref Material __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadMat; + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(Settlement), nameof(Settlement.ExpandingIcon), MethodType.Getter)] + static class SettlementNullFactionPatch2 + { + static bool Prefix(Settlement __instance, ref Texture2D __result) + { + if (__instance.factionInt == null) + { + __result = BaseContent.BadTex; + return false; + } + + return true; + } + } } diff --git a/Source/Client/Patches/StylingStation.cs b/Source/Client/Patches/StylingStation.cs index 7c5f0005..828f8244 100644 --- a/Source/Client/Patches/StylingStation.cs +++ b/Source/Client/Patches/StylingStation.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Util; using UnityEngine; using Verse; diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index f2994bdc..3b4da9df 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -1,5 +1,3 @@ -extern alias zip; - using HarmonyLib; using Multiplayer.Common; using RimWorld.Planet; @@ -8,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Multiplayer.Client.AsyncTime; +using Multiplayer.Common.Util; using UnityEngine; using Verse; @@ -19,17 +18,21 @@ public static class TickPatch { public static int Timer { get; private set; } - public static double accumulator; + public static int ticksToRun; public static int tickUntil; public static int workTicks; public static bool currentExecutingCmdIssuedBySelf; public static bool serverFrozen; public static int frozenAt; + public const float StandardTimePerFrame = 1000.0f / 60.0f; + + // Time is in milliseconds private static float realTime; - public static float avgFrameTime; + public static float avgFrameTime = StandardTimePerFrame; public static float serverTimePerTick; - private static int frames; + + private static float frameTimeSentAt; public static TimeSpeed replayTimeSpeed; @@ -43,8 +46,7 @@ public static IEnumerable AllTickables { get { - MultiplayerWorldComp comp = Multiplayer.WorldComp; - yield return comp; + yield return Multiplayer.AsyncWorldTime; var maps = Find.Maps; for (int i = maps.Count - 1; i >= 0; i--) @@ -55,8 +57,8 @@ public static IEnumerable AllTickables static Stopwatch updateTimer = Stopwatch.StartNew(); public static Stopwatch tickTimer = Stopwatch.StartNew(); - [TweakValue("Multiplayer", 0f, 100f)] - public static float maxBehind = 6f; + [TweakValue("Multiplayer")] + public static bool doSimulate = true; static bool Prefix() { @@ -67,26 +69,37 @@ static bool Prefix() int ticksBehind = tickUntil - Timer; realTime += Time.deltaTime * 1000f; + // Slow down when few ticksBehind to accumulate a buffer + // Try to speed up when many ticksBehind + // Else run at the speed from the server float stpt = ticksBehind <= 3 ? serverTimePerTick * 1.2f : ticksBehind >= 7 ? serverTimePerTick * 0.8f : serverTimePerTick; + if (Multiplayer.IsReplay) + stpt = StandardTimePerFrame * ReplayMultiplier(); + if (Timer >= tickUntil) { - accumulator = 0; + ticksToRun = 0; } - else if (!Multiplayer.IsReplay && realTime > 0 && stpt > 0) + else if (realTime > 0 && stpt > 0) { - realTime -= stpt; avgFrameTime = (avgFrameTime + Time.deltaTime * 1000f) / 2f; - accumulator = 1; + + ticksToRun = Multiplayer.IsReplay ? Mathf.CeilToInt(realTime / stpt) : 1; + realTime -= ticksToRun * stpt; } - if (frames % 3 == 0) - Multiplayer.Client.Send(Packets.Client_FrameTime, avgFrameTime); + if (realTime > 0) + realTime = 0; - frames++; + if (Time.time - frameTimeSentAt > 32f/1000f) + { + Multiplayer.Client.Send(Packets.Client_FrameTime, avgFrameTime); + frameTimeSentAt = Time.time; + } - if (Multiplayer.IsReplay && replayTimeSpeed == TimeSpeed.Paused) - accumulator = 0; + if (Multiplayer.IsReplay && replayTimeSpeed == TimeSpeed.Paused || !doSimulate) + ticksToRun = 0; if (simulating is { targetIsTickUntil: true }) simulating.target = tickUntil; @@ -96,7 +109,7 @@ static bool Prefix() if (MpVersion.IsDebug) SimpleProfiler.Start(); - Tick(out var worked); + DoUpdate(out var worked); if (worked) workTicks++; if (MpVersion.IsDebug) @@ -118,7 +131,7 @@ private static void CheckFinishSimulating() public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action onFinish = null, Action onCancel = null, string cancelButtonKey = null, bool canESC = false, string simTextKey = null) { - simulating = new SimulatingData() + simulating = new SimulatingData { target = ticks, targetIsTickUntil = toTickUntil, @@ -133,7 +146,7 @@ public static void SetSimulation(int ticks = 0, bool toTickUntil = false, Action static ITickable CurrentTickable() { if (WorldRendererUtility.WorldRenderedNow) - return Multiplayer.WorldComp; + return Multiplayer.AsyncWorldTime; if (Find.CurrentMap != null) return Find.CurrentMap.AsyncTime(); @@ -147,59 +160,77 @@ static void Postfix() Shader.SetGlobalFloat(ShaderPropertyIDs.GameSeconds, Find.CurrentMap.AsyncTime().mapTicks.TicksToSeconds()); } - public static void Tick(out bool worked) + public static void DoUpdate(out bool worked) { worked = false; updateTimer.Restart(); - while (Simulating ? (Timer < simulating.target && updateTimer.ElapsedMilliseconds < 25) : (accumulator > 0)) + while (Simulating ? (Timer < simulating.target && updateTimer.ElapsedMilliseconds < 25) : (ticksToRun > 0)) { - tickTimer.Restart(); - int curTimer = Timer; + if (DoTick(ref worked)) + return; + } + } - foreach (ITickable tickable in AllTickables) - { - while (tickable.Cmds.Count > 0 && tickable.Cmds.Peek().ticks == curTimer) - { - ScheduledCommand cmd = tickable.Cmds.Dequeue(); - tickable.ExecuteCmd(cmd); + public static void DoTicks(int ticks) + { + for (int i = 0; i < ticks; i++) + { + bool worked = false; + DoTick(ref worked); + } + } - if (LongEventHandler.eventQueue.Count > 0) return; // Yield to e.g. join-point creation - } - } + // Returns whether the tick loop should stop + private static bool DoTick(ref bool worked) + { + tickTimer.Restart(); + int curTimer = Timer; - foreach (ITickable tickable in AllTickables) + foreach (ITickable tickable in AllTickables) + { + while (tickable.Cmds.Count > 0 && tickable.Cmds.Peek().ticks == curTimer) { - if (tickable.TimePerTick(tickable.TimeSpeed) == 0) continue; - tickable.RealTimeToTickThrough += 1f; + ScheduledCommand cmd = tickable.Cmds.Dequeue(); + tickable.ExecuteCmd(cmd); - worked = true; - TickTickable(tickable); + if (LongEventHandler.eventQueue.Count > 0) return true; // Yield to e.g. join-point creation } + } - ConstantTicker.Tick(); + foreach (ITickable tickable in AllTickables) + { + if (tickable.TimePerTick(tickable.DesiredTimeSpeed) == 0) continue; + tickable.TimeToTickThrough += 1f; - accumulator -= 1 * ReplayMultiplier(); - Timer += 1; + worked = true; + TickTickable(tickable); + } - tickTimer.Stop(); + ConstantTicker.Tick(); - if (Multiplayer.session.desynced || Timer >= tickUntil || LongEventHandler.eventQueue.Count > 0) - { - accumulator = 0; - return; - } + ticksToRun -= 1; + Timer += 1; + + tickTimer.Stop(); + + if (Multiplayer.session.desynced || Timer >= tickUntil || LongEventHandler.eventQueue.Count > 0) + { + ticksToRun = 0; + return true; } + + return false; } private static void TickTickable(ITickable tickable) { - while (tickable.RealTimeToTickThrough >= 0) + while (tickable.TimeToTickThrough >= 0) { - float timePerTick = tickable.TimePerTick(tickable.TimeSpeed); + float timePerTick = tickable.TimePerTick(tickable.DesiredTimeSpeed); if (timePerTick == 0) break; - tickable.RealTimeToTickThrough -= timePerTick; + tickable.TimeToTickThrough -= timePerTick; try { @@ -220,10 +251,10 @@ private static float ReplayMultiplier() return 0f; ITickable tickable = CurrentTickable(); - if (tickable.TimePerTick(tickable.TimeSpeed) == 0f) + if (tickable.TimePerTick(tickable.DesiredTimeSpeed) == 0f) return 1 / 100f; // So paused sections of the timeline are skipped through - return tickable.ActualRateMultiplier(tickable.TimeSpeed) / tickable.ActualRateMultiplier(replayTimeSpeed); + return tickable.ActualRateMultiplier(tickable.DesiredTimeSpeed) / tickable.ActualRateMultiplier(replayTimeSpeed); } public static float TimePerTick(this ITickable tickable, TimeSpeed speed) @@ -238,7 +269,7 @@ public static float ActualRateMultiplier(this ITickable tickable, TimeSpeed spee if (Multiplayer.GameComp.asyncTime) return tickable.TickRateMultiplier(speed); - var rate = Multiplayer.WorldComp.TickRateMultiplier(speed); + var rate = Multiplayer.AsyncWorldTime.TickRateMultiplier(speed); foreach (var map in Find.Maps) rate = Math.Min(rate, map.AsyncTime().TickRateMultiplier(speed)); @@ -255,11 +286,11 @@ public static void Reset() ClearSimulating(); Timer = 0; tickUntil = 0; - accumulator = 0; + ticksToRun = 0; serverFrozen = false; workTicks = 0; serverTimePerTick = 0; - avgFrameTime = 0; + avgFrameTime = StandardTimePerFrame; realTime = 0; TimeControlPatch.prePauseTimeSpeed = null; } diff --git a/Source/Client/Patches/TimestampFixer.cs b/Source/Client/Patches/TimestampFixer.cs new file mode 100644 index 00000000..dabea825 --- /dev/null +++ b/Source/Client/Patches/TimestampFixer.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using HarmonyLib; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using Verse; +using Verse.AI; + +namespace Multiplayer.Client.Patches; + +public static class TimestampFixer +{ + delegate ref int FieldGetter(T obj) where T : IExposable; + + private static Dictionary>> timestampFields = new(); + + private static void Add(FieldGetter t) where T : IExposable + { + timestampFields.GetOrAddNew(typeof(T)).Add(obj => ref t((T)obj)); + } + + static TimestampFixer() + { + Add((Pawn_MindState mind) => ref mind.canSleepTick); + Add((Pawn_MindState mind) => ref mind.canLovinTick); + Add((Pawn_GuestTracker guest) => ref guest.ticksWhenAllowedToEscapeAgain); + Add((Pawn_GuestTracker guest) => ref guest.lastPrisonBreakTicks); + } + + public static int? currentOffset; + + public static void FixPawn(Pawn p, Map oldMap, Map newMap) + { + var oldTime = oldMap?.AsyncTime().mapTicks ?? Multiplayer.AsyncWorldTime.worldTicks; + var newTime = newMap?.AsyncTime().mapTicks ?? Multiplayer.AsyncWorldTime.worldTicks; + currentOffset = newTime - oldTime; + + MpLog.Debug($"Fixing pawn timestamps for {p} moving from {oldMap?.ToString() ?? "World"}:{oldTime} to {newMap?.ToString() ?? "World"}:{newTime}"); + + try + { + // Auxiliary save which is used to visit the pawn's data + Scribe.saver.DebugOutputFor(p); + } + finally + { + currentOffset = null; + } + } + + public static void ProcessExposable(IExposable exposable) + { + if (timestampFields.ContainsKey(exposable.GetType())) + foreach (var del in timestampFields[exposable.GetType()]) + del(exposable) += currentOffset!.Value; + } +} + +[HarmonyPatch(typeof(DebugLoadIDsSavingErrorsChecker), nameof(DebugLoadIDsSavingErrorsChecker.RegisterDeepSaved))] +static class RegisterDeepSaved_ProcessExposable +{ + static void Prefix(object obj) + { + if (TimestampFixer.currentOffset != null && obj is IExposable exposable) + TimestampFixer.ProcessExposable(exposable); + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.DeSpawn))] +static class PawnDespawn_RememberMap +{ + static void Prefix(Pawn __instance) + { + if (Multiplayer.Client == null) return; + __instance.GetComp().lastMap = __instance.Map.uniqueID; + } +} + +[HarmonyPatch(typeof(WorldPawns), nameof(WorldPawns.RemovePawn))] +static class WorldPawnsRemovePawn_RememberTick +{ + static void Prefix(Pawn p) + { + if (Multiplayer.Client == null) return; + p.GetComp().worldPawnRemoveTick = Multiplayer.AsyncWorldTime.worldTicks; + } +} + +[HarmonyPatch(typeof(Pawn), nameof(Pawn.SpawnSetup))] +static class PawnSpawn_FixTimestamp +{ + static void Postfix(Pawn __instance) + { + if (Multiplayer.Client == null) return; + if (__instance.Map == null) return; + + if (__instance.GetComp().worldPawnRemoveTick == Multiplayer.AsyncWorldTime.worldTicks) + TimestampFixer.FixPawn(__instance, null, __instance.Map); + } +} + +[HarmonyPatch(typeof(WorldPawns), nameof(WorldPawns.AddPawn))] +static class WorldPawnsAddPawn_FixTimestamp +{ + static void Prefix(Pawn p) + { + if (Multiplayer.Client == null) return; + + var lastMap = p.GetComp().lastMap; + if (lastMap != -1) + TimestampFixer.FixPawn(p, Find.Maps.FirstOrDefault(m => m.uniqueID == lastMap), null); + } +} diff --git a/Source/Client/Patches/UniqueIds.cs b/Source/Client/Patches/UniqueIds.cs index b6c4cc1f..69faeb5a 100644 --- a/Source/Client/Patches/UniqueIds.cs +++ b/Source/Client/Patches/UniqueIds.cs @@ -3,12 +3,14 @@ using RimWorld; using System.Collections.Generic; using System.Reflection; +using Multiplayer.Common.Util; using Verse; namespace Multiplayer.Client.Patches { [HarmonyPatch(typeof(UniqueIDsManager))] [HarmonyPatch(nameof(UniqueIDsManager.GetNextID))] + [HotSwappable] public static class UniqueIdsPatch { private static IdBlock currentBlock; @@ -37,16 +39,15 @@ static void Postfix(ref int __result) { if (Multiplayer.Client == null) return; - /*IdBlock currentBlock = CurrentBlock; - if (currentBlock == null) - { - __result = localIds--; - if (!Multiplayer.ShouldSync) - Log.Warning("Tried to get a unique id without an id block set!"); - return; - } - - __result = currentBlock.NextId();*/ + // if (currentBlockBlock == null) + // { + // __result = localIds--; + // if (!Multiplayer.ShouldSync) + // Log.Warning("Tried to get a unique id without an id block set!"); + // return; + // } + // + // __result = CurrentBlock.NextId(); if (Multiplayer.InInterface) { diff --git a/Source/Client/Patches/VanillaTweaks.cs b/Source/Client/Patches/VanillaTweaks.cs index fbdd10f3..43422113 100644 --- a/Source/Client/Patches/VanillaTweaks.cs +++ b/Source/Client/Patches/VanillaTweaks.cs @@ -60,7 +60,7 @@ static class LongEventWindowPreventCameraMotion static void Postfix(int ID) { - if (ID == -LongEventWindowId || ID == -IngameUIPatch.ModalWindowId) + if (ID == -LongEventWindowId || ID == -IngameModal.ModalWindowId) { var window = Find.WindowStack.windows.Find(w => w.ID == ID); @@ -78,7 +78,7 @@ static void Prefix(Window __instance) if (Current.ProgramState == ProgramState.Entry) return; if (__instance.ID == -LongEventWindowPreventCameraMotion.LongEventWindowId || - __instance.ID == -IngameUIPatch.ModalWindowId || + __instance.ID == -IngameModal.ModalWindowId || __instance is DisconnectedWindow || __instance is CaravanFormingProxy ) diff --git a/Source/Client/Patches/WorldPawns.cs b/Source/Client/Patches/WorldPawns.cs new file mode 100644 index 00000000..2517b1b6 --- /dev/null +++ b/Source/Client/Patches/WorldPawns.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using HarmonyLib; +using Multiplayer.API; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client.Patches; + +// todo wip +//[HarmonyPatch(typeof(WorldPawns), nameof(WorldPawns.AddPawn))] +static class WorldPawnsAdd +{ + public static List toProcess = new(); + + public static void PostProcess() + { + foreach (var action in toProcess) + action(); + + toProcess.Clear(); + } + + static bool Prefix(WorldPawns __instance, Pawn p) + { + Log.Message($"Add world pawn from {Multiplayer.MapContext}: {p} {new StackTrace()}"); + + if (Multiplayer.MapContext != null) + { + toProcess.Insert(0, () => Add(p)); + OnMainThread.Enqueue(PostProcess); + } + + return Multiplayer.MapContext == null; + } + + [SyncMethod(exposeParameters = new[]{0})] + static void Add(Pawn p) + { + Find.WorldPawns.AddPawn(p); + } +} + +//[HarmonyPatch(typeof(WorldObjectsHolder), nameof(WorldObjectsHolder.Add))] +static class WorldObjectAdd +{ + static bool Prefix(WorldObjectsHolder __instance, WorldObject o) + { + Log.Message($"Add world object from {Multiplayer.MapContext}: {o}"); + + if (Multiplayer.MapContext != null) + { + WorldPawnsAdd.toProcess.Add(() => Add(o)); + OnMainThread.Enqueue(WorldPawnsAdd.PostProcess); + } + + return Multiplayer.MapContext == null; + } + + [SyncMethod(exposeParameters = new[]{0})] + static void Add(WorldObject o) + { + Find.WorldObjects.Add(o); + } +} diff --git a/Source/Client/Persistent/CaravanFormingSession.cs b/Source/Client/Persistent/CaravanFormingSession.cs index 826d0601..8b381723 100644 --- a/Source/Client/Persistent/CaravanFormingSession.cs +++ b/Source/Client/Persistent/CaravanFormingSession.cs @@ -4,10 +4,12 @@ using RimWorld.Planet; using System; using System.Collections.Generic; +using Multiplayer.Common.Util; using Verse; namespace Multiplayer.Client { + [HotSwappable] public class CaravanFormingSession : IExposable, ISessionWithTransferables, IPausingWithDialog { public Map map; diff --git a/Source/Client/Persistent/RitualData.cs b/Source/Client/Persistent/RitualData.cs index 25de1b2e..d1f23d92 100644 --- a/Source/Client/Persistent/RitualData.cs +++ b/Source/Client/Persistent/RitualData.cs @@ -62,15 +62,14 @@ private static void WriteDelegate(ByteWriter writer, Delegate del) SyncSerialization.WriteSync(writer, targetType); var fieldPaths = GetFields(targetType).ToArray(); - var fieldTypes = fieldPaths.Select(path => MpReflection.PathType(path)).ToArray(); + var fieldTypes = fieldPaths.Select(MpReflection.PathType).ToArray(); void SyncObj(object obj, Type type, string debugInfo) { if (type.IsCompilerGenerated()) return; - if (writer is LoggingByteWriter log1) - log1.Log.Enter(debugInfo); + (writer as LoggingByteWriter)?.Log.Enter(debugInfo); try { @@ -81,8 +80,7 @@ void SyncObj(object obj, Type type, string debugInfo) } finally { - if (writer is LoggingByteWriter log2) - log2.Log.Exit(); + (writer as LoggingByteWriter)?.Log.Exit(); } } diff --git a/Source/Properties/AssemblyInfo.cs b/Source/Client/Properties/AssemblyInfo.cs similarity index 100% rename from Source/Properties/AssemblyInfo.cs rename to Source/Client/Properties/AssemblyInfo.cs diff --git a/Source/Client/Saving/CrossRefs.cs b/Source/Client/Saving/CrossRefs.cs index 9177e1cd..f59e1524 100644 --- a/Source/Client/Saving/CrossRefs.cs +++ b/Source/Client/Saving/CrossRefs.cs @@ -216,7 +216,7 @@ public static class LoadedObjectsRegisterPatch { static bool Prefix(LoadedObjectDirectory __instance, ILoadReferenceable reffable) { - if (!(__instance is SharedCrossRefs)) return true; + if (__instance is not SharedCrossRefs) return true; if (reffable == null) return false; string key = reffable.GetUniqueLoadID(); @@ -237,7 +237,7 @@ public static class LoadedObjectsClearPatch { static bool Prefix(LoadedObjectDirectory __instance) { - if (!(__instance is SharedCrossRefs)) return true; + if (__instance is not SharedCrossRefs) return true; Scribe.loader.crossRefs.loadedObjectDirectory = ScribeUtil.defaultCrossRefs; ScribeUtil.sharedCrossRefs.UnregisterAllTemp(); diff --git a/Source/Client/Saving/LoadPatch.cs b/Source/Client/Saving/LoadPatch.cs index 8e5f6285..7eb4b512 100644 --- a/Source/Client/Saving/LoadPatch.cs +++ b/Source/Client/Saving/LoadPatch.cs @@ -1,5 +1,4 @@ -using System.Linq; -using HarmonyLib; +using HarmonyLib; using Multiplayer.Client.Saving; using Verse; using Verse.Profile; @@ -46,7 +45,7 @@ static bool Prefix() LongEventHandler.ExecuteWhenFinished(() => { // Inits all caches - foreach (ITickable tickable in TickPatch.AllTickables.Where(t => !(t is ConstantTicker))) + foreach (ITickable tickable in TickPatch.AllTickables) tickable.Tick(); if (!Current.Game.Maps.Any()) diff --git a/Source/Client/Saving/Loader.cs b/Source/Client/Saving/Loader.cs new file mode 100644 index 00000000..83dc5f18 --- /dev/null +++ b/Source/Client/Saving/Loader.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml; +using HarmonyLib; +using Multiplayer.Common; +using Verse; +using Verse.Profile; + +namespace Multiplayer.Client.Saving; + +public static class Loader +{ + public static void ReloadGame(List mapsToLoad, bool changeScene, bool forceAsyncTime) + { + var gameDoc = DataSnapshotToXml(Multiplayer.session.dataSnapshot, mapsToLoad); + + LoadPatch.gameToLoad = new(gameDoc, Multiplayer.session.dataSnapshot.SemiPersistentData); + TickPatch.replayTimeSpeed = TimeSpeed.Paused; + + if (changeScene) + { + LongEventHandler.QueueLongEvent(() => + { + MemoryUtility.ClearAllMapsAndWorld(); + Current.Game = new Game + { + InitData = new GameInitData + { + gameToLoad = "server" + } + }; + + LongEventHandler.ExecuteWhenFinished(() => + { + LongEventHandler.QueueLongEvent(() => PostLoad(forceAsyncTime), "MpSimulating", false, null); + }); + }, "Play", "MpLoading", true, null); + } + else + { + LongEventHandler.QueueLongEvent(() => + { + SaveLoad.LoadInMainThread(LoadPatch.gameToLoad); + PostLoad(forceAsyncTime); + }, "MpLoading", false, null); + } + } + + private static void PostLoad(bool forceAsyncTime) + { + // If the client gets disconnected during loading + if (Multiplayer.Client == null) return; + + Multiplayer.session.dataSnapshot = Multiplayer.session.dataSnapshot with + { + CachedAtTime = TickPatch.Timer + }; + + Multiplayer.session.replayTimerStart = TickPatch.Timer; + + Multiplayer.game.ChangeRealPlayerFaction(Find.FactionManager.GetById(Multiplayer.session.myFactionId)); + Multiplayer.game.myFactionLoading = null; + + if (forceAsyncTime) + Multiplayer.game.gameComp.asyncTime = true; + + Multiplayer.AsyncWorldTime.cmds = new Queue( + Multiplayer.session.dataSnapshot.MapCmds.GetValueSafe(ScheduledCommand.Global) ?? + new List()); + // Map cmds are added in MapAsyncTimeComp.FinalizeInit + } + + private static XmlDocument DataSnapshotToXml(GameDataSnapshot dataSnapshot, List mapsToLoad) + { + XmlDocument gameDoc = ScribeUtil.LoadDocument(dataSnapshot.GameData); + XmlNode gameNode = gameDoc.DocumentElement["game"]; + + foreach (int map in mapsToLoad) + { + using XmlReader reader = XmlReader.Create(new MemoryStream(dataSnapshot.MapData[map])); + XmlNode mapNode = gameDoc.ReadNode(reader); + gameNode["maps"].AppendChild(mapNode); + + if (gameNode["currentMapIndex"] == null) + gameNode.AddNode("currentMapIndex", map.ToString()); + } + + return gameDoc; + } +} diff --git a/Source/Client/Saving/Replay.cs b/Source/Client/Saving/Replay.cs new file mode 100644 index 00000000..1a7c8052 --- /dev/null +++ b/Source/Client/Saving/Replay.cs @@ -0,0 +1,168 @@ +using Multiplayer.Common; +using RimWorld; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Multiplayer.Client.Saving; +using Multiplayer.Common.Util; +using Verse; + +namespace Multiplayer.Client +{ + public class Replay + { + public ReplayInfo info; + + private Replay(FileInfo file) + { + File = file; + } + + public FileInfo File { get; } + + public ZipArchive CreateZipWrite() + { + return MpZipFile.Open(File.FullName, ZipArchiveMode.Create); + } + + public ZipArchive OpenZipRead() + { + return MpZipFile.Open(File.FullName, ZipArchiveMode.Read); + } + + public void WriteCurrentData() + { + WriteData(Multiplayer.session.dataSnapshot); + } + + public void WriteData(GameDataSnapshot gameData) + { + string sectionId = info.sections.Count.ToString("D3"); + using var zip = CreateZipWrite(); + + foreach (var (mapId, mapData) in gameData.MapData) + zip.AddEntry($"maps/{sectionId}_{mapId}_save", mapData); + + foreach (var (mapId, mapCmdData) in gameData.MapCmds) + if (mapId >= 0) + zip.AddEntry($"maps/{sectionId}_{mapId}_cmds", ScheduledCommand.SerializeCmds(mapCmdData)); + + if (gameData.MapCmds.TryGetValue(ScheduledCommand.Global, out var worldCmds)) + zip.AddEntry($"world/{sectionId}_cmds", ScheduledCommand.SerializeCmds(worldCmds)); + + zip.AddEntry($"world/{sectionId}_save", gameData.GameData); + info.sections.Add(new ReplaySection(gameData.CachedAtTime, TickPatch.Timer)); + + zip.AddEntry("info", ReplayInfo.Write(info)); + } + + public bool LoadInfo() + { + using var zip = OpenZipRead(); + var infoFile = zip.GetEntry("info"); + if (infoFile == null) return false; + + info = ReplayInfo.Read(infoFile.GetBytes()); + + return true; + } + + public GameDataSnapshot LoadGameData(int sectionId) + { + var mapCmdsDict = new Dictionary>(); + var mapDataDict = new Dictionary(); + + string sectionIdStr = sectionId.ToString("D3"); + + using var zip = OpenZipRead(); + + foreach (var mapCmds in zip.GetEntries($"maps/{sectionIdStr}_*_cmds")) + { + int mapId = int.Parse(mapCmds.Name.Split('_')[1]); + mapCmdsDict[mapId] = ScheduledCommand.DeserializeCmds(mapCmds.GetBytes()); + } + + var worldCmds = zip.GetEntry($"world/{sectionIdStr}_cmds"); + if (worldCmds != null) + mapCmdsDict[ScheduledCommand.Global] = ScheduledCommand.DeserializeCmds(worldCmds.GetBytes()); + + foreach (var mapSave in zip.GetEntries($"maps/{sectionIdStr}_*_save")) + { + int mapId = int.Parse(mapSave.Name.Split('_')[1]); + mapDataDict[mapId] = mapSave.GetBytes(); + } + + return new GameDataSnapshot( + 0, + zip.GetBytes($"world/{sectionIdStr}_save"), + Array.Empty(), + mapDataDict, + mapCmdsDict + ); + } + + public static FileInfo ReplayFile(string fileName, string folder = null) + => new(Path.Combine(folder ?? Multiplayer.ReplaysDir, $"{fileName}.zip")); + + public static Replay ForLoading(string fileName) => ForLoading(ReplayFile(fileName)); + public static Replay ForLoading(FileInfo file) => new Replay(file); + + public static Replay ForSaving(string fileName) => ForSaving(ReplayFile(fileName)); + public static Replay ForSaving(FileInfo file) + { + var replay = new Replay(file) + { + info = new ReplayInfo() + { + name = Multiplayer.session.gameName, + playerFaction = Multiplayer.session.myFactionId, + protocol = MpVersion.Protocol, + rwVersion = VersionControl.CurrentVersionStringWithRev, + modIds = LoadedModManager.RunningModsListForReading.Select(m => m.PackageId).ToList(), + modNames = LoadedModManager.RunningModsListForReading.Select(m => m.Name).ToList(), + asyncTime = Multiplayer.GameComp.asyncTime, + } + }; + + return replay; + } + + public static void LoadReplay(FileInfo file, bool toEnd = false, Action after = null, Action cancel = null, string simTextKey = null) + { + var session = new MultiplayerSession + { + client = new ReplayConnection(), + replay = true + }; + + Multiplayer.session = session; + session.client.ChangeState(ConnectionStateEnum.ClientPlaying); + + var replay = ForLoading(file); + replay.LoadInfo(); + + var sectionIndex = toEnd ? (replay.info.sections.Count - 1) : 0; + session.dataSnapshot = replay.LoadGameData(sectionIndex); + + // todo ensure everything is read correctly + + session.myFactionId = replay.info.playerFaction; + session.replayTimerStart = replay.info.sections[sectionIndex].start; + + int tickUntil = replay.info.sections[sectionIndex].end; + session.replayTimerEnd = tickUntil; + TickPatch.tickUntil = tickUntil; + + TickPatch.SetSimulation( + toEnd ? tickUntil : session.replayTimerStart, + onFinish: after, + onCancel: cancel, + simTextKey: simTextKey + ); + + Loader.ReloadGame(session.dataSnapshot.MapData.Keys.ToList(), true, replay.info.asyncTime); + } + } +} diff --git a/Source/Client/Saving/ReplayConnection.cs b/Source/Client/Saving/ReplayConnection.cs new file mode 100644 index 00000000..6de3ead1 --- /dev/null +++ b/Source/Client/Saving/ReplayConnection.cs @@ -0,0 +1,18 @@ +using Multiplayer.Common; + +namespace Multiplayer.Client; + +public class ReplayConnection : ConnectionBase +{ + protected override void SendRaw(byte[] raw, bool reliable) + { + } + + public override void HandleReceiveRaw(ByteReader data, bool reliable) + { + } + + public override void Close(MpDisconnectReason reason, byte[] data) + { + } +} diff --git a/Source/Client/Saving/Replays.cs b/Source/Client/Saving/Replays.cs deleted file mode 100644 index b3ac3501..00000000 --- a/Source/Client/Saving/Replays.cs +++ /dev/null @@ -1,232 +0,0 @@ -extern alias zip; - -using Multiplayer.Common; -using RimWorld; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using UnityEngine; -using Verse; -using zip::Ionic.Zip; - -namespace Multiplayer.Client -{ - public class Replay - { - public ReplayInfo info; - - private Replay(FileInfo file) - { - File = file; - } - - public FileInfo File { get; } - public ZipFile ZipFile => new ZipFile(File.FullName); - - public void WriteCurrentData() - { - WriteData(Multiplayer.session.dataSnapshot); - } - - public void WriteData(GameDataSnapshot gameData) - { - string sectionId = info.sections.Count.ToString("D3"); - using var zip = ZipFile; - - foreach (var (mapId, mapData) in gameData.mapData) - zip.AddEntry($"maps/{sectionId}_{mapId}_save", mapData); - - foreach (var (mapId, mapCmdData) in gameData.mapCmds) - if (mapId >= 0) - zip.AddEntry($"maps/{sectionId}_{mapId}_cmds", SerializeCmds(mapCmdData)); - - if (gameData.mapCmds.TryGetValue(ScheduledCommand.Global, out var worldCmds)) - zip.AddEntry($"world/{sectionId}_cmds", SerializeCmds(worldCmds)); - - zip.AddEntry($"world/{sectionId}_save", gameData.gameData); - info.sections.Add(new ReplaySection(gameData.cachedAtTime, TickPatch.Timer)); - - zip.UpdateEntry("info", DirectXmlSaver.XElementFromObject(info, typeof(ReplayInfo)).ToString()); - zip.Save(); - } - - public static byte[] SerializeCmds(List cmds) - { - ByteWriter writer = new ByteWriter(); - - writer.WriteInt32(cmds.Count); - foreach (var cmd in cmds) - writer.WritePrefixedBytes(ScheduledCommand.Serialize(cmd)); - - return writer.ToArray(); - } - - public static List DeserializeCmds(byte[] data) - { - var reader = new ByteReader(data); - - int count = reader.ReadInt32(); - var result = new List(count); - for (int i = 0; i < count; i++) - result.Add(ScheduledCommand.Deserialize(new ByteReader(reader.ReadPrefixedBytes()))); - - return result; - } - - public bool LoadInfo() - { - using var zip = ZipFile; - var infoFile = zip["info"]; - if (infoFile == null) return false; - - var doc = ScribeUtil.LoadDocument(infoFile.GetBytes()); - info = DirectXmlToObject.ObjectFromXml(doc.DocumentElement, true); - - return true; - } - - public void LoadCurrentData(int sectionId) - { - var dataSnapshot = new GameDataSnapshot(); - string sectionIdStr = sectionId.ToString("D3"); - - using var zip = ZipFile; - - foreach (var mapCmds in zip.SelectEntries($"name = maps/{sectionIdStr}_*_cmds")) - { - int mapId = int.Parse(mapCmds.FileName.Split('_')[1]); - dataSnapshot.mapCmds[mapId] = DeserializeCmds(mapCmds.GetBytes()); - } - - foreach (var mapSave in zip.SelectEntries($"name = maps/{sectionIdStr}_*_save")) - { - int mapId = int.Parse(mapSave.FileName.Split('_')[1]); - dataSnapshot.mapData[mapId] = mapSave.GetBytes(); - } - - var worldCmds = zip[$"world/{sectionIdStr}_cmds"]; - if (worldCmds != null) - dataSnapshot.mapCmds[ScheduledCommand.Global] = DeserializeCmds(worldCmds.GetBytes()); - - dataSnapshot.gameData = zip[$"world/{sectionIdStr}_save"].GetBytes(); - dataSnapshot.semiPersistentData = new byte[0]; - - Multiplayer.session.dataSnapshot = dataSnapshot; - } - - public static FileInfo ReplayFile(string fileName, string folder = null) - => new(Path.Combine(folder ?? Multiplayer.ReplaysDir, $"{fileName}.zip")); - - public static Replay ForLoading(string fileName) => ForLoading(ReplayFile(fileName)); - public static Replay ForLoading(FileInfo file) => new Replay(file); - - public static Replay ForSaving(string fileName) => ForSaving(ReplayFile(fileName)); - public static Replay ForSaving(FileInfo file) - { - var replay = new Replay(file) - { - info = new ReplayInfo() - { - name = Multiplayer.session.gameName, - playerFaction = Multiplayer.session.myFactionId, - protocol = MpVersion.Protocol, - rwVersion = VersionControl.CurrentVersionStringWithRev, - modIds = LoadedModManager.RunningModsListForReading.Select(m => m.PackageId).ToList(), - modNames = LoadedModManager.RunningModsListForReading.Select(m => m.Name).ToList(), - asyncTime = Multiplayer.GameComp.asyncTime, - } - }; - - return replay; - } - - public static void LoadReplay(FileInfo file, bool toEnd = false, Action after = null, Action cancel = null, string simTextKey = null) - { - var session = Multiplayer.session = new MultiplayerSession(); - session.client = new ReplayConnection(); - session.client.State = ConnectionStateEnum.ClientPlaying; - session.replay = true; - - var replay = ForLoading(file); - replay.LoadInfo(); - - var sectionIndex = toEnd ? (replay.info.sections.Count - 1) : 0; - replay.LoadCurrentData(sectionIndex); - - // todo ensure everything is read correctly - - session.myFactionId = replay.info.playerFaction; - session.replayTimerStart = replay.info.sections[sectionIndex].start; - - int tickUntil = replay.info.sections[sectionIndex].end; - session.replayTimerEnd = tickUntil; - TickPatch.tickUntil = tickUntil; - - TickPatch.SetSimulation( - toEnd ? tickUntil : session.replayTimerStart, - onFinish: after, - onCancel: cancel, - simTextKey: simTextKey - ); - - ClientJoiningState.ReloadGame(session.dataSnapshot.mapData.Keys.ToList(), true, replay.info.asyncTime); - } - } - - public class ReplayInfo - { - public string name; - public int protocol; - public int playerFaction; - - public List sections = new List(); - public List events = new List(); - - public string rwVersion; - public List modIds; - public List modNames; - public List modAssemblyHashes; // Unused, here to satisfy DirectXmlToObject on old saves - - public bool asyncTime; - } - - public class ReplaySection - { - public int start; - public int end; - - public ReplaySection() - { - } - - public ReplaySection(int start, int end) - { - this.start = start; - this.end = end; - } - } - - public class ReplayEvent - { - public string name; - public int time; - public Color color; - } - - public class ReplayConnection : ConnectionBase - { - protected override void SendRaw(byte[] raw, bool reliable) - { - } - - public override void HandleReceive(ByteReader data, bool reliable) - { - } - - public override void Close(MpDisconnectReason reason, byte[] data) - { - } - } - -} diff --git a/Source/Client/Saving/SaveCompression.cs b/Source/Client/Saving/SaveCompression.cs index b130221e..a134070e 100644 --- a/Source/Client/Saving/SaveCompression.cs +++ b/Source/Client/Saving/SaveCompression.cs @@ -146,6 +146,11 @@ public static void Load(Map map) DecompressedThingsPatch.thingsToSpawn[map.uniqueID] = loadedThings; } + private static void WarnBadShortHash(int id, ushort defId) + { + Log.WarningOnce($"Multiplayer couldn't decompress thing id {id}: bad short hash {defId}", defId); + } + private static Thing LoadRock(Map map, BinaryReader reader, IntVec3 cell) { ushort defId = reader.ReadUInt16(); @@ -153,7 +158,11 @@ private static Thing LoadRock(Map map, BinaryReader reader, IntVec3 cell) return null; int id = reader.ReadInt32(); - ThingDef def = thingDefsByShortHash[defId]; + if (!thingDefsByShortHash.TryGetValue(defId, out var def)) + { + WarnBadShortHash(id, defId); + return null; + } Thing thing = (Thing)Activator.CreateInstance(def.thingClass); thing.def = def; @@ -174,7 +183,11 @@ private static Thing LoadRockRubble(Map map, BinaryReader reader, IntVec3 cell) int id = reader.ReadInt32(); byte thickness = reader.ReadByte(); int growTick = reader.ReadInt32(); - ThingDef def = thingDefsByShortHash[defId]; + if (!thingDefsByShortHash.TryGetValue(defId, out var def)) + { + WarnBadShortHash(id, defId); + return null; + } Filth thing = (Filth)Activator.CreateInstance(def.thingClass); thing.def = def; @@ -213,7 +226,11 @@ private static Thing LoadPlant(Map map, BinaryReader reader, IntVec3 cell) if (hasLeafless) plantMadeLeaflessTick = reader.ReadInt32(); - ThingDef def = thingDefsByShortHash[defId]; + if (!thingDefsByShortHash.TryGetValue(defId, out var def)) + { + WarnBadShortHash(id, defId); + return null; + } Plant thing = (Plant)Activator.CreateInstance(def.thingClass); thing.def = def; diff --git a/Source/Client/Saving/SaveLoad.cs b/Source/Client/Saving/SaveLoad.cs index 317043a7..7b6129b2 100644 --- a/Source/Client/Saving/SaveLoad.cs +++ b/Source/Client/Saving/SaveLoad.cs @@ -15,7 +15,6 @@ namespace Multiplayer.Client { public record TempGameData(XmlDocument SaveData, byte[] SemiPersistent); - public static class SaveLoad { public static TempGameData SaveAndReload() @@ -48,7 +47,7 @@ public static TempGameData SaveAndReload() mapCmds[map.uniqueID] = map.AsyncTime().cmds; } - mapCmds[ScheduledCommand.Global] = Multiplayer.WorldComp.cmds; + mapCmds[ScheduledCommand.Global] = Multiplayer.AsyncWorldTime.cmds; DeepProfiler.Start("Multiplayer SaveAndReload: Save"); //WriteElementPatch.cachedVals = new Dictionary(); @@ -103,7 +102,7 @@ public static TempGameData SaveAndReload() Find.Selector.selected = SyncSerialization.ReadSync>(selectedReader).AllNotNull().Cast().ToList(); Find.World.renderer.wantedMode = planetRenderMode; - Multiplayer.WorldComp.cmds = mapCmds[ScheduledCommand.Global]; + Multiplayer.AsyncWorldTime.cmds = mapCmds[ScheduledCommand.Global]; Multiplayer.reloading = false; //SimpleProfiler.Pause(); @@ -144,8 +143,8 @@ private static void ClearState() sustainer.Cleanup(); // todo destroy other game objects? - UnityEngine.Object.Destroy(pool.sourcePoolCamera.cameraSourcesContainer); - UnityEngine.Object.Destroy(pool.sourcePoolWorld.sourcesWorld[0].gameObject); + Object.Destroy(pool.sourcePoolCamera.cameraSourcesContainer); + Object.Destroy(pool.sourcePoolWorld.sourcesWorld[0].gameObject); } } @@ -190,34 +189,37 @@ public static GameDataSnapshot CreateGameDataSnapshot(TempGameData data) XmlNode gameNode = data.SaveData.DocumentElement["game"]; XmlNode mapsNode = gameNode["maps"]; - var dataSnapshot = new GameDataSnapshot(); + var mapCmdsDict = new Dictionary>(); + var mapDataDict = new Dictionary(); foreach (XmlNode mapNode in mapsNode) { int id = int.Parse(mapNode["uniqueID"].InnerText); byte[] mapData = ScribeUtil.XmlToByteArray(mapNode); - dataSnapshot.mapData[id] = mapData; - dataSnapshot.mapCmds[id] = new List(Find.Maps.First(m => m.uniqueID == id).AsyncTime().cmds); + mapDataDict[id] = mapData; + mapCmdsDict[id] = new List(Find.Maps.First(m => m.uniqueID == id).AsyncTime().cmds); } gameNode["currentMapIndex"].RemoveFromParent(); mapsNode.RemoveAll(); byte[] gameData = ScribeUtil.XmlToByteArray(data.SaveData); - dataSnapshot.cachedAtTime = TickPatch.Timer; - dataSnapshot.gameData = gameData; - dataSnapshot.semiPersistentData = data.SemiPersistent; - dataSnapshot.mapCmds[ScheduledCommand.Global] = new List(Multiplayer.WorldComp.cmds); - - return dataSnapshot; + mapCmdsDict[ScheduledCommand.Global] = new List(Multiplayer.AsyncWorldTime.cmds); + + return new GameDataSnapshot( + TickPatch.Timer, + gameData, + data.SemiPersistent, + mapDataDict, + mapCmdsDict + ); } - public static void SendGameData(GameDataSnapshot data, bool async) + public static void SendGameData(GameDataSnapshot snapshot, bool async) { - var cache = Multiplayer.session.dataSnapshot; - var mapsData = new Dictionary(cache.mapData); - var gameData = cache.gameData; - var semiPersistent = cache.semiPersistentData; + var mapsData = new Dictionary(snapshot.MapData); + var gameData = snapshot.GameData; + var semiPersistent = snapshot.SemiPersistentData; void Send() { diff --git a/Source/Client/Saving/SavingPatches.cs b/Source/Client/Saving/SavingPatches.cs index b482d25b..9379037c 100644 --- a/Source/Client/Saving/SavingPatches.cs +++ b/Source/Client/Saving/SavingPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using Multiplayer.Client.AsyncTime; using Multiplayer.Client.Comp; using RimWorld.Planet; using Verse; @@ -49,12 +50,13 @@ static void Postfix(World __instance) if (Scribe.mode is LoadSaveMode.LoadingVars or LoadSaveMode.Saving) { - Scribe_Deep.Look(ref Multiplayer.game.worldComp, "mpWorldComp", __instance); + // Node called mpWorldComp for backwards compatibility + Scribe_Deep.Look(ref Multiplayer.game.asyncWorldTimeComp, "mpWorldComp", __instance); - if (Multiplayer.game.worldComp == null) + if (Multiplayer.game.asyncWorldTimeComp == null) { - Log.Warning($"No {nameof(MultiplayerWorldComp)} during loading/saving"); - Multiplayer.game.worldComp = new MultiplayerWorldComp(__instance); + Log.Warning($"No {nameof(AsyncWorldTimeComp)} during loading/saving"); + Multiplayer.game.asyncWorldTimeComp = new AsyncWorldTimeComp(__instance); } } } @@ -124,7 +126,7 @@ static class WorldCompFinalizeInit static void Postfix() { if (Multiplayer.Client == null) return; - Multiplayer.WorldComp.FinalizeInit(); + Multiplayer.AsyncWorldTime.FinalizeInit(); } } diff --git a/Source/Client/Saving/ScribeUtil.cs b/Source/Client/Saving/ScribeUtil.cs index 5896f97d..4936ac20 100644 --- a/Source/Client/Saving/ScribeUtil.cs +++ b/Source/Client/Saving/ScribeUtil.cs @@ -9,57 +9,6 @@ namespace Multiplayer.Client { - public class SharedCrossRefs : LoadedObjectDirectory - { - // Used in CrossRefs patches - public HashSet tempKeys = new HashSet(); - - public void Unregister(ILoadReferenceable reffable) - { - allObjectsByLoadID.Remove(reffable.GetUniqueLoadID()); - } - - public void UnregisterAllTemp() - { - foreach (var key in tempKeys) - allObjectsByLoadID.Remove(key); - - tempKeys.Clear(); - } - - public void UnregisterAllFrom(Map map) - { - foreach (var val in allObjectsByLoadID.Values.ToArray()) - { - if (val is Thing thing && thing.Map == map || - val is PassingShip ship && ship.Map == map || - val is Bill bill && bill.Map == map - ) - Unregister(val); - } - } - } - - public static class ThingsById - { - public static Dictionary thingsById = new Dictionary(); - - public static void Register(Thing t) - { - thingsById[t.thingIDNumber] = t; - } - - public static void Unregister(Thing t) - { - thingsById.Remove(t.thingIDNumber); - } - - public static void UnregisterAllFrom(Map map) - { - thingsById.RemoveAll(kv => kv.Value.Map == map); - } - } - public static class ScribeUtil { private const string RootNode = "root"; @@ -71,6 +20,10 @@ public static class ScribeUtil public static bool loading; + //dbg + public static bool removeMapRefs; + private static List removedMapRefs; + public static void StartWriting(bool indent = false) { writingStream = new MemoryStream(); @@ -108,7 +61,7 @@ public static void StartWritingToDoc() public static XmlDocument FinishWritingToDoc() { - var doc = (Scribe.saver.writer as CustomXmlWriter).doc; + var doc = ((CustomXmlWriter)Scribe.saver.writer).doc; Scribe.saver.FinalizeSaving(); return doc; } @@ -207,6 +160,13 @@ public static void SupplyCrossRefs() defaultCrossRefs ??= Scribe.loader.crossRefs.loadedObjectDirectory; Scribe.loader.crossRefs.loadedObjectDirectory = sharedCrossRefs; + if (removeMapRefs) + { + removedMapRefs = new List(); + foreach (var map in Find.Maps) + removedMapRefs.AddRange(sharedCrossRefs.UnregisterAllFrom(map)); + } + MpLog.Debug($"Cross ref supply: {sharedCrossRefs.allObjectsByLoadID.Count} {sharedCrossRefs.allObjectsByLoadID.LastOrDefault()} {Faction.OfPlayer}"); } @@ -223,11 +183,18 @@ public static T ReadExposable(byte[] data, Action beforeFinish = null) whe { StartLoading(data); SupplyCrossRefs(); + T element = default; Scribe_Deep.Look(ref element, RootNode); beforeFinish?.Invoke(element); + if (removeMapRefs) + { + sharedCrossRefs.Reregister(removedMapRefs); + removedMapRefs = null; + } + FinalizeLoading(); // Default cross refs restored in LoadedObjectsClearPatch diff --git a/Source/Client/Saving/SharedCrossRefs.cs b/Source/Client/Saving/SharedCrossRefs.cs new file mode 100644 index 00000000..70fc93fc --- /dev/null +++ b/Source/Client/Saving/SharedCrossRefs.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +public class SharedCrossRefs : LoadedObjectDirectory +{ + // Used in CrossRefs patches + public HashSet tempKeys = new(); + + public void Unregister(ILoadReferenceable reffable) + { + allObjectsByLoadID.Remove(reffable.GetUniqueLoadID()); + } + + public void UnregisterAllTemp() + { + foreach (var key in tempKeys) + allObjectsByLoadID.Remove(key); + + tempKeys.Clear(); + } + + public List UnregisterAllFrom(Map map) + { + var unregistered = new List(); + + foreach (var val in allObjectsByLoadID.Values.ToArray()) + { + if (val is Thing thing && thing.Map == map || + val is PassingShip ship && ship.Map == map || + val is Bill bill && bill.Map == map + ) + { + Unregister(val); + unregistered.Add(val); + } + } + + return unregistered; + } + + public void Reregister(List items) + { + foreach (var item in items) + allObjectsByLoadID[item.GetUniqueLoadID()] = item; + } +} diff --git a/Source/Client/Saving/ThingsById.cs b/Source/Client/Saving/ThingsById.cs new file mode 100644 index 00000000..b9a366a6 --- /dev/null +++ b/Source/Client/Saving/ThingsById.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Verse; + +namespace Multiplayer.Client; + +public static class ThingsById +{ + public static Dictionary thingsById = new(); + + public static void Register(Thing t) + { + thingsById[t.thingIDNumber] = t; + } + + public static void Unregister(Thing t) + { + thingsById.Remove(t.thingIDNumber); + } + + public static void UnregisterAllFrom(Map map) + { + thingsById.RemoveAll(kv => kv.Value.Map == map); + } +} diff --git a/Source/Client/Settings/MpSettings.cs b/Source/Client/Settings/MpSettings.cs new file mode 100644 index 00000000..49d2cca9 --- /dev/null +++ b/Source/Client/Settings/MpSettings.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Linq; +using Multiplayer.Client.Saving; +using Multiplayer.Common; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client +{ + public class MpSettings : ModSettings + { + public string username; + public bool showCursors = true; + public bool autoAcceptSteam; + public bool transparentChat = true; + public int autosaveSlots = 5; + public bool showDevInfo; + public int desyncTracesRadius = 40; + public string serverAddress = "127.0.0.1"; + public bool appendNameToAutosave; + public bool showModCompatibility = true; + public bool hideTranslationMods = true; + public bool enablePings = true; + public KeyCode? sendPingButton = KeyCode.Mouse4; + public KeyCode? jumpToPingButton = KeyCode.Mouse3; + public Rect chatRect; + public Vector2 resolutionForChat; + public bool showMainMenuAnim = true; + public DesyncTracingMode desyncTracingMode = DesyncTracingMode.Fast; + public bool transparentPlayerCursors = true; + public List playerColors = new(DefaultPlayerColors); + + internal static readonly ColorRGBClient[] DefaultPlayerColors = + { + new(0,125,255), + new(255,0,0), + new(0,255,45), + new(255,0,150), + new(80,250,250), + new(200,255,75), + new(100,0,75) + }; + + private ServerSettingsClient serverSettingsClient = new(); + public ServerSettings ServerSettings => serverSettingsClient.settings; + + public override void ExposeData() + { + // Remember to mirror the default values + + Scribe_Values.Look(ref username, "username"); + Scribe_Values.Look(ref showCursors, "showCursors", true); + Scribe_Values.Look(ref autoAcceptSteam, "autoAcceptSteam"); + Scribe_Values.Look(ref transparentChat, "transparentChat", true); + Scribe_Values.Look(ref autosaveSlots, "autosaveSlots", 5); + Scribe_Values.Look(ref showDevInfo, "showDevInfo"); + Scribe_Values.Look(ref desyncTracesRadius, "desyncTracesRadius", 40); + Scribe_Values.Look(ref serverAddress, "serverAddress", "127.0.0.1"); + Scribe_Values.Look(ref showModCompatibility, "showModCompatibility", true); + Scribe_Values.Look(ref hideTranslationMods, "hideTranslationMods", true); + Scribe_Values.Look(ref enablePings, "enablePings", true); + Scribe_Values.Look(ref sendPingButton, "sendPingButton", KeyCode.Mouse4); + Scribe_Values.Look(ref jumpToPingButton, "jumpToPingButton", KeyCode.Mouse3); + Scribe_Custom.LookRect(ref chatRect, "chatRect"); + Scribe_Values.Look(ref resolutionForChat, "resolutionForChat"); + Scribe_Values.Look(ref showMainMenuAnim, "showMainMenuAnim", true); + Scribe_Values.Look(ref appendNameToAutosave, "appendNameToAutosave"); + Scribe_Values.Look(ref transparentPlayerCursors, "transparentPlayerCursors", true); + + Scribe_Collections.Look(ref playerColors, "playerColors", LookMode.Deep); + if (playerColors.NullOrEmpty()) + playerColors = new List(DefaultPlayerColors); + if (Scribe.mode == LoadSaveMode.PostLoadInit) + PlayerManager.PlayerColors = playerColors.Select(c => (ColorRGB)c).ToArray(); + + Scribe_Deep.Look(ref serverSettingsClient, "serverSettings"); + serverSettingsClient ??= new ServerSettingsClient(); + } + } + + public enum DesyncTracingMode + { + None, Fast, Slow + } + + public struct ColorRGBClient : IExposable + { + public byte r, g, b; + + public ColorRGBClient(byte r, byte g, byte b) + { + this.r = r; + this.g = g; + this.b = b; + } + + public void ExposeData() + { + ScribeAsInt(ref r, "r"); + ScribeAsInt(ref g, "g"); + ScribeAsInt(ref b, "b"); + } + + private void ScribeAsInt(ref byte value, string label) + { + int temp = value; + Scribe_Values.Look(ref temp, label); + value = (byte)temp; + } + + public static implicit operator Color(ColorRGBClient value) => new(value.r / 255f, value.g / 255f, value.b / 255f); + + public static implicit operator ColorRGB(ColorRGBClient value) => new(value.r, value.g, value.b); + } +} diff --git a/Source/Client/Settings/MpSettingsUI.cs b/Source/Client/Settings/MpSettingsUI.cs new file mode 100644 index 00000000..845a6b0f --- /dev/null +++ b/Source/Client/Settings/MpSettingsUI.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public static class MpSettingsUI +{ + private static string slotsBuffer; + private static string desyncRadiusBuffer; + + private static Vector2 scrollPosition = Vector2.zero; + private static SettingsPage currentPage = SettingsPage.General; + + internal static void DoSettingsWindowContents(MpSettings settings, Rect inRect) + { + var buttonPos = new Rect(inRect.xMax - 150, inRect.yMin + 10, 125, 32); + + Widgets.Dropdown(buttonPos, currentPage, x => x, GeneratePageMenu, $"MpSettingsPage{currentPage}".Translate()); + + switch (currentPage) + { + default: + case SettingsPage.General: + DoGeneralSettings(settings, inRect, buttonPos); + break; + case SettingsPage.Color: + DoColorContents(settings, inRect, buttonPos); + break; + } + } + + private static IEnumerable> GeneratePageMenu(SettingsPage p) + { + return from SettingsPage page in Enum.GetValues(typeof(SettingsPage)) + where page != p + select new Widgets.DropdownMenuElement + { + option = new FloatMenuOption($"MpSettingsPage{page}".Translate(), () => + { + currentPage = page; + scrollPosition = Vector2.zero; + }) + { + tooltip = page == SettingsPage.Color ? "MpSettingsPageColorDesc".Translate() : null + }, + payload = page, + }; + } + + public static void DoGeneralSettings(MpSettings settings, Rect inRect, Rect pageButtonPos) + { + var listing = new Listing_Standard(); + listing.Begin(inRect); + listing.ColumnWidth = 270f; + + DoUsernameField(settings, listing); + listing.TextFieldNumericLabeled("MpAutosaveSlots".Translate() + ": ", ref settings.autosaveSlots, ref slotsBuffer, 1f, + 99f); + + listing.CheckboxLabeled("MpShowPlayerCursors".Translate(), ref settings.showCursors); + listing.CheckboxLabeled("MpPlayerCursorTransparency".Translate(), ref settings.transparentPlayerCursors); + listing.CheckboxLabeled("MpAutoAcceptSteam".Translate(), ref settings.autoAcceptSteam, + "MpAutoAcceptSteamDesc".Translate()); + listing.CheckboxLabeled("MpTransparentChat".Translate(), ref settings.transparentChat); + listing.CheckboxLabeled("MpAppendNameToAutosave".Translate(), ref settings.appendNameToAutosave); + listing.CheckboxLabeled("MpShowModCompat".Translate(), ref settings.showModCompatibility, + "MpShowModCompatDesc".Translate()); + listing.CheckboxLabeled("MpEnablePingsSetting".Translate(), ref settings.enablePings); + listing.CheckboxLabeled("MpShowMainMenuAnimation".Translate(), ref settings.showMainMenuAnim); + + const string buttonOff = "Off"; + + using (MpStyle.Set(TextAnchor.MiddleCenter)) + if (listing.ButtonTextLabeled("MpPingLocButtonSetting".Translate(), + settings.sendPingButton != null ? $"Mouse {settings.sendPingButton - (int)KeyCode.Mouse0 + 1}" : buttonOff)) + Find.WindowStack.Add(new FloatMenu(new List(ButtonChooser(b => settings.sendPingButton = b)))); + + using (MpStyle.Set(TextAnchor.MiddleCenter)) + if (listing.ButtonTextLabeled("MpJumpToPingButtonSetting".Translate(), + settings.jumpToPingButton != null ? $"Mouse {settings.jumpToPingButton - (int)KeyCode.Mouse0 + 1}" : buttonOff)) + Find.WindowStack.Add( + new FloatMenu(new List(ButtonChooser(b => settings.jumpToPingButton = b)))); + + if (Prefs.DevMode) + { + listing.CheckboxLabeled("Show debug info", ref settings.showDevInfo); + listing.TextFieldNumericLabeled("Desync radius: ", ref settings.desyncTracesRadius, ref desyncRadiusBuffer, 1f, + 200f); + +#if DEBUG + using (MpStyle.Set(TextAnchor.MiddleCenter)) + if (listing.ButtonTextLabeled("Desync tracing mode", settings.desyncTracingMode.ToString())) + settings.desyncTracingMode = settings.desyncTracingMode.Cycle(); +#endif + } + + listing.End(); + + IEnumerable ButtonChooser(Action setter) + { + yield return new FloatMenuOption(buttonOff, () => { setter(null); }); + + for (var btn = 0; btn < 5; btn++) + { + var b = btn; + yield return new FloatMenuOption($"Mouse {b + 3}", () => { setter(KeyCode.Mouse2 + b); }); + } + } + } + + private static (string r, string g, string b)[] colorsBuffer = { }; + + private static void DoColorContents(MpSettings settings, Rect inRect, Rect pageButtonPos) + { + var viewRect = new Rect(inRect) + { + height = (settings.playerColors.Count + 1) * 32f, + width = inRect.width - 20f, + }; + + var rect = new Rect(pageButtonPos.xMin - 150, pageButtonPos.yMin, 125, 32); + if (Widgets.ButtonText(rect, "MpResetColors".Translate())) + { + settings.playerColors = new List(MpSettings.DefaultPlayerColors); + PlayerManager.PlayerColors = settings.playerColors.Select(c => (ColorRGB)c).ToArray(); + } + + if (settings.playerColors.Count != colorsBuffer.Length) + { + colorsBuffer = new (string r, string g, string b)[settings.playerColors.Count]; + } + + Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect); + + var toRemove = -1; + for (var i = 0; i < settings.playerColors.Count; i++) + { + var colors = settings.playerColors[i]; + if (DrawColorRow(settings, i * 32 + 120, ref colors, ref colorsBuffer[i], out var edited)) + toRemove = i; + if (edited) + { + settings.playerColors[i] = colors; + PlayerManager.PlayerColors = settings.playerColors.Select(c => (ColorRGB)c).ToArray(); + } + } + + rect = new Rect(402, settings.playerColors.Count * 32 + 118, 32, 32); + if (Widgets.ButtonText(rect, "+")) + { + var rand = new System.Random(); + settings.playerColors.Add(new ColorRGBClient((byte)rand.Next(256), (byte)rand.Next(256), (byte)rand.Next(256))); + PlayerManager.PlayerColors = settings.playerColors.Select(c => (ColorRGB)c).ToArray(); + } + + Widgets.EndScrollView(); + + if (toRemove >= 0) + { + settings.playerColors.RemoveAt(toRemove); + PlayerManager.PlayerColors = settings.playerColors.Select(c => (ColorRGB)c).ToArray(); + } + } + + private static bool DrawColorRow(MpSettings settings, int pos, ref ColorRGBClient color, ref (string r, string g, string b) buffer, out bool edited) + { + var (r, g, b) = ((int)color.r, (int)color.g, (int)color.b); + var rect = new Rect(10, pos, 100, 28); + Widgets.TextFieldNumericLabeled(rect, "R", ref r, ref buffer.r, 0, 255); + rect = new Rect(120, pos, 100, 28); + Widgets.TextFieldNumericLabeled(rect, "G", ref g, ref buffer.g, 0, 255); + rect = new Rect(230, pos, 100, 28); + Widgets.TextFieldNumericLabeled(rect, "B", ref b, ref buffer.b, 0, 255); + + rect = new Rect(350, pos - 2, 32, 32); + Widgets.DrawBoxSolid(rect, color); + + if (color.r != r || color.g != g || color.b != b) + { + color = new ColorRGBClient((byte)r, (byte)g, (byte)b); + edited = true; + } + else edited = false; + + if (settings.playerColors.Count > 1) + { + rect = new Rect(402, pos - 2, 32, 32); + return Widgets.ButtonText(rect, "-"); + } + + return false; + } + + const string UsernameField = "UsernameField"; + + private static void DoUsernameField(MpSettings settings, Listing_Standard listing) + { + GUI.SetNextControlName(UsernameField); + + var prevField = settings.username; + var fieldStr = listing.TextEntryLabeled("MpUsernameSetting".Translate() + ": ", settings.username); + + if (prevField != fieldStr && fieldStr.Length <= 15 && MultiplayerServer.UsernamePattern.IsMatch(fieldStr)) + { + settings.username = fieldStr; + Multiplayer.username = fieldStr; + } + + // Don't allow changing the username while playing + if (Multiplayer.Client != null && GUI.GetNameOfFocusedControl() == UsernameField) + UI.UnfocusCurrentControl(); + } + + private enum SettingsPage + { + General, + Color, + } +} diff --git a/Source/Client/Settings/ScribeProvider.cs b/Source/Client/Settings/ScribeProvider.cs new file mode 100644 index 00000000..6f3286ff --- /dev/null +++ b/Source/Client/Settings/ScribeProvider.cs @@ -0,0 +1,12 @@ +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client; + +public class ScribeProvider : ScribeLike.Provider +{ + public override void Look(ref T value, string label, T defaultValue, bool forceSave) + { + Scribe_Values.Look(ref value, label, defaultValue, forceSave); + } +} diff --git a/Source/Client/Settings/ServerSettingsClient.cs b/Source/Client/Settings/ServerSettingsClient.cs new file mode 100644 index 00000000..b0ed1a08 --- /dev/null +++ b/Source/Client/Settings/ServerSettingsClient.cs @@ -0,0 +1,14 @@ +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client; + +public class ServerSettingsClient : IExposable +{ + public ServerSettings settings = new(); + + public void ExposeData() + { + settings.ExposeData(); + } +} diff --git a/Source/Client/Syncing/Dict/SyncDictMisc.cs b/Source/Client/Syncing/Dict/SyncDictMisc.cs index 47b056d2..956c45df 100644 --- a/Source/Client/Syncing/Dict/SyncDictMisc.cs +++ b/Source/Client/Syncing/Dict/SyncDictMisc.cs @@ -22,7 +22,7 @@ public static class SyncDictMisc #region System { (ByteWriter data, Type type) => data.WriteString(type.FullName), - (ByteReader data) => AccessTools.TypeByName(data.ReadString()) + (ByteReader data) => AccessTools.TypeByName(data.ReadStringNullable()) }, #endregion @@ -56,7 +56,7 @@ public static class SyncDictMisc data.WriteString(name.nameInt); data.WriteBool(name.numerical); }, - (ByteReader data) => new NameSingle(data.ReadString(), data.ReadBool()) + (ByteReader data) => new NameSingle(data.ReadStringNullable(), data.ReadBool()) }, { (ByteWriter data, NameTriple name) => { @@ -64,7 +64,7 @@ public static class SyncDictMisc data.WriteString(name.nickInt); data.WriteString(name.lastInt); }, - (ByteReader data) => new NameTriple(data.ReadString(), data.ReadString(), data.ReadString()) + (ByteReader data) => new NameTriple(data.ReadStringNullable(), data.ReadStringNullable(), data.ReadStringNullable()) }, #endregion @@ -73,7 +73,7 @@ public static class SyncDictMisc (ByteWriter data, TaggedString str) => { data.WriteString(str.rawText); }, - (ByteReader data) => new TaggedString(data.ReadString()) + (ByteReader data) => new TaggedString(data.ReadStringNullable()) }, { (SyncWorker worker, ref ColorInt color) => diff --git a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs index c4d22e18..3be1743f 100644 --- a/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs +++ b/Source/Client/Syncing/Dict/SyncDictMultiplayer.cs @@ -99,6 +99,13 @@ public static class SyncDictMultiplayer (ByteReader _) => Multiplayer.GameComp }, #endregion + + #region Multiplayer ScheduledCommand + { + (ByteWriter _, ScheduledCommand _) => throw new NotImplementedException(), + (ByteReader data) => ScheduledCommand.Deserialize(new ByteReader(data.ReadPrefixedBytes())) + }, + #endregion }; } } diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index 3c3f835d..30045f78 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -10,7 +10,7 @@ using Verse.AI; using Verse.AI.Group; using static Multiplayer.Client.SyncSerialization; -using static Multiplayer.Client.ImplSerialization; +using static Multiplayer.Client.RwImplSerialization; // ReSharper disable RedundantLambdaParameterType namespace Multiplayer.Client @@ -684,11 +684,11 @@ public static class SyncDictRimWorld holder = thing.Map; else if (thing.ParentHolder is ThingComp thingComp) holder = thingComp; - else if (ThingOwnerUtility.GetFirstSpawnedParentThing(thing) is Thing parentThing) + else if (ThingOwnerUtility.GetFirstSpawnedParentThing(thing) is { } parentThing) holder = parentThing; - else if (GetAnyParent(thing) is WorldObject worldObj) + else if (GetAnyParent(thing) is { } worldObj) holder = worldObj; - else if (GetAnyParent(thing) is WorldObjectComp worldObjComp) + else if (GetAnyParent(thing) is { } worldObjComp) holder = worldObjComp; GetImpl(holder, supportedThingHolders, out Type implType, out int index); @@ -706,7 +706,6 @@ public static class SyncDictRimWorld context.syncingThingParent = true; WriteSyncObject(data, holder, implType); context.syncingThingParent = false; - return; } } }, diff --git a/Source/Client/Syncing/Game/RwImplSerialization.cs b/Source/Client/Syncing/Game/RwImplSerialization.cs new file mode 100644 index 00000000..7016ab11 --- /dev/null +++ b/Source/Client/Syncing/Game/RwImplSerialization.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client +{ + public static class RwImplSerialization + { + public static Type[] storageParents; + public static Type[] plantToGrowSettables; + + public static Type[] thingCompTypes; + public static Type[] hediffCompTypes; + public static Type[] abilityCompTypes; + public static Type[] designatorTypes; + public static Type[] worldObjectCompTypes; + + public static Type[] gameCompTypes; + public static Type[] worldCompTypes; + public static Type[] mapCompTypes; + + internal static Type[] supportedThingHolders = + { + typeof(Map), + typeof(Thing), + typeof(ThingComp), + typeof(WorldObject), + typeof(WorldObjectComp) + }; + + // ReSharper disable once InconsistentNaming + internal enum ISelectableImpl : byte + { + None, Thing, Zone, WorldObject + } + + internal enum VerbOwnerType : byte + { + None, Pawn, Ability, ThingComp + } + + public static void Init() + { + storageParents = TypeUtil.AllImplementationsOrdered(typeof(IStoreSettingsParent)); + plantToGrowSettables = TypeUtil.AllImplementationsOrdered(typeof(IPlantToGrowSettable)); + + thingCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(ThingComp)); + hediffCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(HediffComp)); + abilityCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(AbilityComp)); + designatorTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(Designator)); + worldObjectCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(WorldObjectComp)); + + gameCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(GameComponent)); + worldCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(WorldComponent)); + mapCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(MapComponent)); + } + + internal static T ReadWithImpl(ByteReader data, IList impls) where T : class + { + ushort impl = data.ReadUShort(); + if (impl == ushort.MaxValue) return null; + return (T)SyncSerialization.ReadSyncObject(data, impls[impl]); + } + + internal static void WriteWithImpl(ByteWriter data, object obj, IList impls) where T : class + { + if (obj == null) + { + data.WriteUShort(ushort.MaxValue); + return; + } + + GetImpl(obj, impls, out Type implType, out int impl); + + if (implType == null) + throw new SerializationException($"Unknown {typeof(T)} implementation type {obj.GetType()}"); + + data.WriteUShort((ushort)impl); + SyncSerialization.WriteSyncObject(data, obj, implType); + } + + internal static void GetImpl(object obj, IList impls, out Type type, out int index) + { + type = null; + index = -1; + + if (obj == null) return; + + for (int i = 0; i < impls.Count; i++) + { + if (impls[i].IsInstanceOfType(obj)) + { + type = impls[i]; + index = i; + break; + } + } + } + } +} diff --git a/Source/Client/Syncing/Game/SyncFields.cs b/Source/Client/Syncing/Game/SyncFields.cs index de32e6f0..3cb25562 100644 --- a/Source/Client/Syncing/Game/SyncFields.cs +++ b/Source/Client/Syncing/Game/SyncFields.cs @@ -4,6 +4,7 @@ using RimWorld.Planet; using System; using System.Collections.Generic; +using Multiplayer.Client.Experimental; using UnityEngine; using Verse; using static Verse.Widgets; @@ -35,8 +36,8 @@ public static class SyncFields public static ISyncField SyncFactionAcceptRoyalFavor; public static ISyncField SyncFactionAcceptGoodwill; - public static SyncField[] SyncThingFilterHitPoints; - public static SyncField[] SyncThingFilterQuality; + public static ISyncField SyncThingFilterHitPoints; + public static ISyncField SyncThingFilterQuality; public static ISyncField SyncBillSuspended; public static ISyncField SyncIngredientSearchRadius; @@ -130,9 +131,8 @@ public static void Init() SyncFactionAcceptRoyalFavor = Sync.Field(typeof(Faction), nameof(Faction.allowRoyalFavorRewards)); SyncFactionAcceptGoodwill = Sync.Field(typeof(Faction), nameof(Faction.allowGoodwillRewards)); - var thingFilterTarget = new MultiTarget() { { SyncThingFilters.ThingFilterTarget, "Filter" } }; - SyncThingFilterHitPoints = Sync.FieldMultiTarget(thingFilterTarget, "AllowedHitPointsPercents").SetBufferChanges(); - SyncThingFilterQuality = Sync.FieldMultiTarget(thingFilterTarget, "AllowedQualityLevels").SetBufferChanges(); + SyncThingFilterHitPoints = Sync.Field(typeof(ThingFilterContext), "Filter/AllowedHitPointsPercents").SetBufferChanges(); + SyncThingFilterQuality = Sync.Field(typeof(ThingFilterContext), "Filter/AllowedQualityLevels").SetBufferChanges(); SyncBillSuspended = Sync.Field(typeof(Bill), "suspended"); SyncIngredientSearchRadius = Sync.Field(typeof(Bill), "ingredientSearchRadius").SetBufferChanges(); @@ -368,13 +368,13 @@ static void PlaySettingsControls() [MpPrefix(typeof(ThingFilterUI), "DrawHitPointsFilterConfig")] static void ThingFilterHitPoints() { - SyncThingFilterHitPoints.Watch(SyncMarkers.DrawnThingFilter); + SyncThingFilterHitPoints.Watch(ThingFilterMarkers.DrawnThingFilter); } [MpPrefix(typeof(ThingFilterUI), "DrawQualityFilterConfig")] static void ThingFilterQuality() { - SyncThingFilterQuality.Watch(SyncMarkers.DrawnThingFilter); + SyncThingFilterQuality.Watch(ThingFilterMarkers.DrawnThingFilter); } [MpPrefix(typeof(Bill), "DoInterface")] @@ -495,13 +495,13 @@ static void WatchBillPaused(Bill __instance) static void WatchPolicyLabels() { if (SyncMarkers.dialogOutfit != null) - SyncOutfitLabel.Watch(SyncMarkers.dialogOutfit.Outfit); + SyncOutfitLabel.Watch(SyncMarkers.dialogOutfit); if (SyncMarkers.drugPolicy != null) SyncDrugPolicyLabel.Watch(SyncMarkers.drugPolicy); if (SyncMarkers.foodRestriction != null) - SyncFoodRestrictionLabel.Watch(SyncMarkers.foodRestriction.Food); + SyncFoodRestrictionLabel.Watch(SyncMarkers.foodRestriction); } static void UseWorkPriorities_PostApply(object target, object value) diff --git a/Source/Client/Syncing/Game/SyncGame.cs b/Source/Client/Syncing/Game/SyncGame.cs index 23d6f7ca..15e38cfe 100644 --- a/Source/Client/Syncing/Game/SyncGame.cs +++ b/Source/Client/Syncing/Game/SyncGame.cs @@ -1,9 +1,4 @@ -using HarmonyLib; -using Multiplayer.API; -using RimWorld; using System; -using System.Collections.Generic; -using System.Linq; using Verse; namespace Multiplayer.Client @@ -28,7 +23,6 @@ static void TryInit(string name, Action action) TryInit("SyncMethods", SyncMethods.Init); TryInit("SyncFields", SyncFields.Init); TryInit("SyncDelegates", SyncDelegates.Init); - TryInit("SyncThingFilters", SyncThingFilters.Init); TryInit("SyncActions", SyncActions.Init); //RuntimeHelpers.RunClassConstructor(typeof(SyncResearch).TypeHandle); @@ -36,176 +30,4 @@ static void TryInit(string name, Action action) SyncFieldUtil.ApplyWatchFieldPatches(typeof(SyncFields)); } } - - public static class SyncMarkers - { - public static bool manualPriorities; - public static bool researchToil; - public static DrugPolicy drugPolicy; - - public static bool drawingThingFilter; - public static TabStorageWrapper tabStorage; - public static BillConfigWrapper billConfig; - public static OutfitWrapper dialogOutfit; - public static FoodRestrictionWrapper foodRestriction; - public static PenAutocutWrapper penAutocut; - public static PenAnimalsWrapper penAnimals; - public static DefaultAutocutWrapper windTurbine; - - public static ThingFilterContext DrawnThingFilter => - !drawingThingFilter ? null : - tabStorage ?? billConfig ?? dialogOutfit ?? foodRestriction ?? penAutocut ?? penAnimals ?? (ThingFilterContext)windTurbine; - - #region Misc Markers - [MpPrefix(typeof(MainTabWindow_Work), "DoManualPrioritiesCheckbox")] - static void ManualPriorities_Prefix() => manualPriorities = true; - - [MpPostfix(typeof(MainTabWindow_Work), "DoManualPrioritiesCheckbox")] - static void ManualPriorities_Postfix() => manualPriorities = false; - - [MpPrefix(typeof(JobDriver_Research), nameof(JobDriver_Research.MakeNewToils), lambdaOrdinal: 0)] - static void ResearchToil_Prefix() => researchToil = true; - - [MpPostfix(typeof(JobDriver_Research), nameof(JobDriver_Research.MakeNewToils), lambdaOrdinal: 0)] - static void ResearchToil_Postfix() => researchToil = false; - - [MpPostfix(typeof(Dialog_ManageOutfits), "DoWindowContents")] - static void ManageOutfit_Postfix() => dialogOutfit = null; - - [MpPrefix(typeof(Dialog_ManageDrugPolicies), "DoWindowContents")] - static void ManageDrugPolicy_Prefix(Dialog_ManageDrugPolicies __instance) => drugPolicy = __instance.SelectedPolicy; - #endregion - - #region ThingFilter Markers - [MpPrefix(typeof(ITab_Storage), "FillTab")] - static void TabStorageFillTab_Prefix(ITab_Storage __instance) - { - var selThing = __instance.SelObject; - var selParent = __instance.SelStoreSettingsParent; - // If SelStoreSettingsParent is null, just return early. There'll be nothing to sync. - // The map could potentially be null - for example, if we're syncing mortar. The mortar hun itself - // holds the store settings, and turret guns don't have a map/location assigned - so we sync their parent. - // Because of that, we check if the parent is not null. - if (selParent == null || selThing is Thing { Map: null } or ThingComp { parent: { Map: null } }) - return; - tabStorage = new(selParent); - } - - [MpPostfix(typeof(ITab_Storage), "FillTab")] - static void TabStorageFillTab_Postfix() => tabStorage = null; - - [MpPrefix(typeof(Dialog_BillConfig), "DoWindowContents")] - static void BillConfig_Prefix(Dialog_BillConfig __instance) => - billConfig = new(__instance.bill); - - [MpPostfix(typeof(Dialog_BillConfig), "DoWindowContents")] - static void BillConfig_Postfix() => billConfig = null; - - [MpPrefix(typeof(Dialog_ManageOutfits), "DoWindowContents")] - static void ManageOutfit_Prefix(Dialog_ManageOutfits __instance) => - dialogOutfit = new(__instance.SelectedOutfit); - - [MpPostfix(typeof(Dialog_ManageOutfits), "DoWindowContents")] - static void ManageDrugPolicy_Postfix() => drugPolicy = null; - - [MpPrefix(typeof(Dialog_ManageFoodRestrictions), "DoWindowContents")] - static void ManageFoodRestriction_Prefix(Dialog_ManageFoodRestrictions __instance) => - foodRestriction = new(__instance.SelectedFoodRestriction); - - [MpPostfix(typeof(Dialog_ManageFoodRestrictions), "DoWindowContents")] - static void ManageFoodRestriction_Postfix() => foodRestriction = null; - - [MpPrefix(typeof(ITab_PenAutoCut), "FillTab")] - static void TabPenAutocutFillTab_Prefix(ITab_PenAutoCut __instance) => - penAutocut = new(__instance.SelectedCompAnimalPenMarker); - - [MpPostfix(typeof(ITab_PenAutoCut), "FillTab")] - static void TabPenAutocutFillTab_Postfix() => penAutocut = null; - - [MpPrefix(typeof(ITab_PenAnimals), "FillTab")] - static void TabPenAnimalsFillTab_Prefix(ITab_PenAnimals __instance) => - penAnimals = new(__instance.SelectedCompAnimalPenMarker); - - [MpPostfix(typeof(ITab_PenAnimals), "FillTab")] - static void TabPenAnimalsFillTab_Postfix() => penAnimals = null; - - [MpPrefix(typeof(ITab_WindTurbineAutoCut), nameof(ITab_WindTurbineAutoCut.FillTab))] - static void TabWindTurbineAutocutFillTab_Prefix(ITab_WindTurbineAutoCut __instance) => - windTurbine = new(__instance.AutoCut); - - [MpPostfix(typeof(ITab_WindTurbineAutoCut), nameof(ITab_WindTurbineAutoCut.FillTab))] - static void TabWindTurbineAutocutFillTab_Postfix(ITab_WindTurbineAutoCut __instance) => windTurbine = null; - - [MpPrefix(typeof(ThingFilterUI), "DoThingFilterConfigWindow")] - static void ThingFilterUI_Prefix() => drawingThingFilter = true; - - [MpPostfix(typeof(ThingFilterUI), "DoThingFilterConfigWindow")] - static void ThingFilterUI_Postfix() => drawingThingFilter = false; - #endregion - } - - // Currently unused - public static class SyncResearch - { - private static Dictionary localResearch = new Dictionary(); - - //[MpPrefix(typeof(ResearchManager), nameof(ResearchManager.ResearchPerformed))] - static bool ResearchPerformed_Prefix(float amount, Pawn researcher) - { - if (Multiplayer.Client == null || !SyncMarkers.researchToil) - return true; - - // todo only faction leader - if (Faction.OfPlayer == Multiplayer.RealPlayerFaction) - { - float current = localResearch.GetValueSafe(researcher.thingIDNumber); - localResearch[researcher.thingIDNumber] = current + amount; - } - - return false; - } - - // Set by faction context - public static ResearchSpeed researchSpeed; - public static ISyncField SyncResearchSpeed = - Sync.Field(null, "Multiplayer.Client.SyncResearch/researchSpeed/[]").SetBufferChanges().InGameLoop(); - - public static void ConstantTick() - { - if (localResearch.Count == 0) return; - - SyncFieldUtil.FieldWatchPrefix(); - - foreach (int pawn in localResearch.Keys.ToList()) - { - SyncResearchSpeed.Watch(null, pawn); - researchSpeed[pawn] = localResearch[pawn]; - localResearch[pawn] = 0; - } - - SyncFieldUtil.FieldWatchPostfix(); - } - } - - public class ResearchSpeed : IExposable - { - public Dictionary data = new Dictionary(); - - public float this[int pawnId] - { - get => data.TryGetValue(pawnId, out float speed) ? speed : 0f; - - set - { - if (value == 0) data.Remove(pawnId); - else data[pawnId] = value; - } - } - - public void ExposeData() - { - Scribe_Collections.Look(ref data, "data", LookMode.Value, LookMode.Value); - } - } - } diff --git a/Source/Client/Syncing/Game/SyncMarkers.cs b/Source/Client/Syncing/Game/SyncMarkers.cs new file mode 100644 index 00000000..7fde4106 --- /dev/null +++ b/Source/Client/Syncing/Game/SyncMarkers.cs @@ -0,0 +1,44 @@ +using RimWorld; + +namespace Multiplayer.Client; + +public static class SyncMarkers +{ + public static bool manualPriorities; + public static bool researchToil; + + public static DrugPolicy drugPolicy; + public static Outfit dialogOutfit; + public static FoodRestriction foodRestriction; + + [MpPrefix(typeof(MainTabWindow_Work), nameof(MainTabWindow_Work.DoManualPrioritiesCheckbox))] + static void ManualPriorities_Prefix() => manualPriorities = true; + + [MpPostfix(typeof(MainTabWindow_Work), nameof(MainTabWindow_Work.DoManualPrioritiesCheckbox))] + static void ManualPriorities_Postfix() => manualPriorities = false; + + [MpPrefix(typeof(JobDriver_Research), nameof(JobDriver_Research.MakeNewToils), lambdaOrdinal: 0)] + static void ResearchToil_Prefix() => researchToil = true; + + [MpPostfix(typeof(JobDriver_Research), nameof(JobDriver_Research.MakeNewToils), lambdaOrdinal: 0)] + static void ResearchToil_Postfix() => researchToil = false; + + [MpPrefix(typeof(Dialog_ManageOutfits), nameof(Dialog_ManageOutfits.DoWindowContents))] + static void ManageOutfit_Prefix(Dialog_ManageOutfits __instance) => dialogOutfit = __instance.SelectedOutfit; + + [MpPostfix(typeof(Dialog_ManageOutfits), nameof(Dialog_ManageOutfits.DoWindowContents))] + static void ManageOutfit_Postfix() => dialogOutfit = null; + + [MpPrefix(typeof(Dialog_ManageFoodRestrictions), nameof(Dialog_ManageFoodRestrictions.DoWindowContents))] + static void ManageFoodRestriction_Prefix(Dialog_ManageFoodRestrictions __instance) => + foodRestriction = __instance.SelectedFoodRestriction; + + [MpPostfix(typeof(Dialog_ManageFoodRestrictions), nameof(Dialog_ManageFoodRestrictions.DoWindowContents))] + static void ManageFoodRestriction_Postfix() => foodRestriction = null; + + [MpPrefix(typeof(Dialog_ManageDrugPolicies), nameof(Dialog_ManageDrugPolicies.DoWindowContents))] + static void ManageDrugPolicy_Prefix(Dialog_ManageDrugPolicies __instance) => drugPolicy = __instance.SelectedPolicy; + + [MpPostfix(typeof(Dialog_ManageDrugPolicies), nameof(Dialog_ManageDrugPolicies.DoWindowContents))] + static void ManageDrugPolicy_Postfix(Dialog_ManageDrugPolicies __instance) => drugPolicy = null; +} diff --git a/Source/Client/Syncing/Game/SyncMethods.cs b/Source/Client/Syncing/Game/SyncMethods.cs index 839a2378..a4658542 100644 --- a/Source/Client/Syncing/Game/SyncMethods.cs +++ b/Source/Client/Syncing/Game/SyncMethods.cs @@ -6,12 +6,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection.Emit; +using Multiplayer.Client.Util; using Verse; using Verse.AI; namespace Multiplayer.Client { - public static class SyncMethods { static SyncField SyncTimetable; diff --git a/Source/Client/Syncing/Game/SyncResearch.cs b/Source/Client/Syncing/Game/SyncResearch.cs new file mode 100644 index 00000000..64999cf7 --- /dev/null +++ b/Source/Client/Syncing/Game/SyncResearch.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.API; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +// Currently unused +public static class SyncResearch +{ + private static Dictionary localResearch = new Dictionary(); + + //[MpPrefix(typeof(ResearchManager), nameof(ResearchManager.ResearchPerformed))] + static bool ResearchPerformed_Prefix(float amount, Pawn researcher) + { + if (Multiplayer.Client == null || !SyncMarkers.researchToil) + return true; + + // todo only faction leader + if (Faction.OfPlayer == Multiplayer.RealPlayerFaction) + { + float current = localResearch.GetValueSafe(researcher.thingIDNumber); + localResearch[researcher.thingIDNumber] = current + amount; + } + + return false; + } + + // Set by faction context + public static ResearchSpeed researchSpeed; + public static ISyncField SyncResearchSpeed = + Sync.Field(null, "Multiplayer.Client.SyncResearch/researchSpeed/[]").SetBufferChanges().InGameLoop(); + + public static void ConstantTick() + { + if (localResearch.Count == 0) return; + + SyncFieldUtil.FieldWatchPrefix(); + + foreach (int pawn in localResearch.Keys.ToList()) + { + SyncResearchSpeed.Watch(null, pawn); + researchSpeed[pawn] = localResearch[pawn]; + localResearch[pawn] = 0; + } + + SyncFieldUtil.FieldWatchPostfix(); + } +} + +public class ResearchSpeed : IExposable +{ + public Dictionary data = new Dictionary(); + + public float this[int pawnId] + { + get => data.TryGetValue(pawnId, out float speed) ? speed : 0f; + + set + { + if (value == 0) data.Remove(pawnId); + else data[pawnId] = value; + } + } + + public void ExposeData() + { + Scribe_Collections.Look(ref data, "data", LookMode.Value, LookMode.Value); + } +} diff --git a/Source/Client/Syncing/Game/SyncThingFilters.cs b/Source/Client/Syncing/Game/SyncThingFilters.cs index 1403abb3..6cb650a0 100644 --- a/Source/Client/Syncing/Game/SyncThingFilters.cs +++ b/Source/Client/Syncing/Game/SyncThingFilters.cs @@ -1,173 +1,103 @@ -using Multiplayer.Common; using RimWorld; using System.Collections.Generic; +using Multiplayer.API; +using Multiplayer.Client.Experimental; using Verse; -namespace Multiplayer.Client +namespace Multiplayer.Client; + +public static class SyncThingFilters { - public static class SyncThingFilters + [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(StuffCategoryDef), typeof(bool) })] + static bool ThingFilter_SetAllow(StuffCategoryDef cat, bool allow) { - static SyncMethod[] AllowThing; - static SyncMethod[] AllowSpecial; - static SyncMethod[] AllowStuffCategory; - static SyncMethod[] AllowCategory; - static SyncMethod[] AllowAll; - static SyncMethod[] DisallowAll; - - public static MultiTarget ThingFilterTarget = new MultiTarget() - { - { typeof(TabStorageWrapper) }, - { typeof(BillConfigWrapper) }, - { typeof(OutfitWrapper) }, - { typeof(FoodRestrictionWrapper) }, - { typeof(PenAnimalsWrapper) }, - { typeof(PenAutocutWrapper) }, - { typeof(DefaultAutocutWrapper) }, - }; - - public static void Init() - { - AllowThing = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.AllowThing_Helper)); - AllowSpecial = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.AllowSpecial_Helper)); - AllowStuffCategory = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.AllowStuffCat_Helper)); - AllowCategory = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.AllowCategory_Helper)); - AllowAll = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.AllowAll_Helper)); - DisallowAll = Sync.MethodMultiTarget(ThingFilterTarget, nameof(ThingFilterContext.DisallowAll_Helper)); - } - - [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(StuffCategoryDef), typeof(bool) })] - static bool ThingFilter_SetAllow(StuffCategoryDef cat, bool allow) - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !AllowStuffCategory.DoSync(SyncMarkers.DrawnThingFilter, cat, allow); - } - - [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(SpecialThingFilterDef), typeof(bool) })] - static bool ThingFilter_SetAllow(SpecialThingFilterDef sfDef, bool allow) - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !AllowSpecial.DoSync(SyncMarkers.DrawnThingFilter, sfDef, allow); - } - - [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(ThingDef), typeof(bool) })] - static bool ThingFilter_SetAllow(ThingDef thingDef, bool allow) - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !AllowThing.DoSync(SyncMarkers.DrawnThingFilter, thingDef, allow); - } - - [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(ThingCategoryDef), typeof(bool), typeof(IEnumerable), typeof(IEnumerable) })] - static bool ThingFilter_SetAllow(ThingCategoryDef categoryDef, bool allow) - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !AllowCategory.DoSync(SyncMarkers.DrawnThingFilter, categoryDef, allow); - } - - [MpPrefix(typeof(ThingFilter), "SetAllowAll")] - static bool ThingFilter_SetAllowAll() - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !AllowAll.DoSync(SyncMarkers.DrawnThingFilter); - } - - [MpPrefix(typeof(ThingFilter), "SetDisallowAll")] - static bool ThingFilter_SetDisallowAll() - { - if (SyncMarkers.DrawnThingFilter == null) return true; - return !DisallowAll.DoSync(SyncMarkers.DrawnThingFilter); - } + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + AllowStuffCat_Helper(ThingFilterMarkers.DrawnThingFilter, cat, allow); + return false; } - public abstract record ThingFilterContext : ISyncSimple + [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(SpecialThingFilterDef), typeof(bool) })] + static bool ThingFilter_SetAllow(SpecialThingFilterDef sfDef, bool allow) { - public abstract ThingFilter Filter { get; } - public abstract ThingFilter ParentFilter { get; } - public virtual IEnumerable HiddenFilters { get => null; } - - internal void AllowStuffCat_Helper(StuffCategoryDef cat, bool allow) - { - Filter.SetAllow(cat, allow); - } - - internal void AllowSpecial_Helper(SpecialThingFilterDef sfDef, bool allow) - { - Filter.SetAllow(sfDef, allow); - } - - internal void AllowThing_Helper(ThingDef thingDef, bool allow) - { - Filter.SetAllow(thingDef, allow); - } - - internal void DisallowAll_Helper() - { - Filter.SetDisallowAll(null, HiddenFilters); - } - - internal void AllowAll_Helper() - { - Filter.SetAllowAll(ParentFilter); - } + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + AllowSpecial_Helper(ThingFilterMarkers.DrawnThingFilter, sfDef, allow); + return false; + } - internal void AllowCategory_Helper(ThingCategoryDef categoryDef, bool allow) - { - var node = new TreeNode_ThingCategory(categoryDef); + [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(ThingDef), typeof(bool) })] + static bool ThingFilter_SetAllow(ThingDef thingDef, bool allow) + { + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + AllowThing_Helper(ThingFilterMarkers.DrawnThingFilter, thingDef, allow); + return false; + } - Filter.SetAllow( - categoryDef, - allow, - null, - Listing_TreeThingFilter - .CalculateHiddenSpecialFilters(node, ParentFilter) - .ConcatIfNotNull(HiddenFilters) - ); - } + [MpPrefix(typeof(ThingFilter), "SetAllow", new[] { typeof(ThingCategoryDef), typeof(bool), typeof(IEnumerable), typeof(IEnumerable) })] + static bool ThingFilter_SetAllow(ThingCategoryDef categoryDef, bool allow) + { + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + AllowCategory_Helper(ThingFilterMarkers.DrawnThingFilter, categoryDef, allow); + return false; } - public record TabStorageWrapper(IStoreSettingsParent Storage) : ThingFilterContext + [MpPrefix(typeof(ThingFilter), "SetAllowAll")] + static bool ThingFilter_SetAllowAll() { - public override ThingFilter Filter => Storage.GetStoreSettings().filter; - public override ThingFilter ParentFilter => Storage.GetParentStoreSettings()?.filter; + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + AllowAll_Helper(ThingFilterMarkers.DrawnThingFilter); + return false; } - public record BillConfigWrapper(Bill Bill) : ThingFilterContext + [MpPrefix(typeof(ThingFilter), "SetDisallowAll")] + static bool ThingFilter_SetDisallowAll() { - public override ThingFilter Filter => Bill.ingredientFilter; - public override ThingFilter ParentFilter => Bill.recipe.fixedIngredientFilter; - public override IEnumerable HiddenFilters => Bill.recipe.forceHiddenSpecialFilters; + if (!Multiplayer.ShouldSync || ThingFilterMarkers.DrawnThingFilter == null) return true; + DisallowAll_Helper(ThingFilterMarkers.DrawnThingFilter); + return false; } - public record OutfitWrapper(Outfit Outfit) : ThingFilterContext + [SyncMethod] + static void AllowStuffCat_Helper(ThingFilterContext context, StuffCategoryDef cat, bool allow) { - public override ThingFilter Filter => Outfit.filter; - public override ThingFilter ParentFilter => Dialog_ManageOutfits.apparelGlobalFilter; - public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowNonDeadmansApparel.ToEnumerable(); + context.Filter.SetAllow(cat, allow); } - public record FoodRestrictionWrapper(FoodRestriction Food) : ThingFilterContext + [SyncMethod] + private static void AllowSpecial_Helper(ThingFilterContext context, SpecialThingFilterDef sfDef, bool allow) { - public override ThingFilter Filter => Food.filter; - public override ThingFilter ParentFilter => Dialog_ManageFoodRestrictions.foodGlobalFilter; - public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowFresh.ToEnumerable(); + context.Filter.SetAllow(sfDef, allow); } - public record PenAnimalsWrapper(CompAnimalPenMarker Pen) : ThingFilterContext + [SyncMethod] + private static void AllowThing_Helper(ThingFilterContext context, ThingDef thingDef, bool allow) { - public override ThingFilter Filter => Pen.AnimalFilter; - public override ThingFilter ParentFilter => AnimalPenUtility.GetFixedAnimalFilter(); + context.Filter.SetAllow(thingDef, allow); } - public record PenAutocutWrapper(CompAnimalPenMarker Pen) : ThingFilterContext + [SyncMethod] + private static void DisallowAll_Helper(ThingFilterContext context) { - public override ThingFilter Filter => Pen.AutoCutFilter; - public override ThingFilter ParentFilter => Pen.parent.Map.animalPenManager.GetFixedAutoCutFilter(); - public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowFresh.ToEnumerable(); + context.Filter.SetDisallowAll(null, context.HiddenFilters); } - public record DefaultAutocutWrapper(CompAutoCut AutoCut) : ThingFilterContext + [SyncMethod] + private static void AllowAll_Helper(ThingFilterContext context) { - public override ThingFilter Filter => AutoCut.AutoCutFilter; - public override ThingFilter ParentFilter => AutoCut.GetFixedAutoCutFilter(); + context.Filter.SetAllowAll(context.ParentFilter); } + [SyncMethod] + private static void AllowCategory_Helper(ThingFilterContext context, ThingCategoryDef categoryDef, bool allow) + { + var node = new TreeNode_ThingCategory(categoryDef); + + context.Filter.SetAllow( + categoryDef, + allow, + null, + Listing_TreeThingFilter + .CalculateHiddenSpecialFilters(node, context.ParentFilter) + .ConcatIfNotNull(context.HiddenFilters) + ); + } } diff --git a/Source/Client/Syncing/Game/ThingFilterContexts.cs b/Source/Client/Syncing/Game/ThingFilterContexts.cs new file mode 100644 index 00000000..e3ff5182 --- /dev/null +++ b/Source/Client/Syncing/Game/ThingFilterContexts.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Multiplayer.Client.Experimental; +using Multiplayer.Common; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +public record TabStorageWrapper(IStoreSettingsParent Storage) : ThingFilterContext +{ + public override ThingFilter Filter => Storage.GetStoreSettings().filter; + public override ThingFilter ParentFilter => Storage.GetParentStoreSettings()?.filter; +} + +public record BillConfigWrapper(Bill Bill) : ThingFilterContext +{ + public override ThingFilter Filter => Bill.ingredientFilter; + public override ThingFilter ParentFilter => Bill.recipe.fixedIngredientFilter; + public override IEnumerable HiddenFilters => Bill.recipe.forceHiddenSpecialFilters; +} + +public record OutfitWrapper(Outfit Outfit) : ThingFilterContext +{ + public override ThingFilter Filter => Outfit.filter; + public override ThingFilter ParentFilter => Dialog_ManageOutfits.apparelGlobalFilter; + public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowNonDeadmansApparel.ToEnumerable(); +} + +public record FoodRestrictionWrapper(FoodRestriction Food) : ThingFilterContext +{ + public override ThingFilter Filter => Food.filter; + public override ThingFilter ParentFilter => Dialog_ManageFoodRestrictions.foodGlobalFilter; + public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowFresh.ToEnumerable(); +} + +public record PenAnimalsWrapper(CompAnimalPenMarker Pen) : ThingFilterContext +{ + public override ThingFilter Filter => Pen.AnimalFilter; + public override ThingFilter ParentFilter => AnimalPenUtility.GetFixedAnimalFilter(); +} + +public record PenAutocutWrapper(CompAnimalPenMarker Pen) : ThingFilterContext +{ + public override ThingFilter Filter => Pen.AutoCutFilter; + public override ThingFilter ParentFilter => Pen.parent.Map.animalPenManager.GetFixedAutoCutFilter(); + public override IEnumerable HiddenFilters => SpecialThingFilterDefOf.AllowFresh.ToEnumerable(); +} + +public record DefaultAutocutWrapper(CompAutoCut AutoCut) : ThingFilterContext +{ + public override ThingFilter Filter => AutoCut.AutoCutFilter; + public override ThingFilter ParentFilter => AutoCut.GetFixedAutoCutFilter(); +} diff --git a/Source/Client/Syncing/Game/ThingFilterMarkers.cs b/Source/Client/Syncing/Game/ThingFilterMarkers.cs new file mode 100644 index 00000000..3b52d31f --- /dev/null +++ b/Source/Client/Syncing/Game/ThingFilterMarkers.cs @@ -0,0 +1,92 @@ +using System; +using Multiplayer.Client.Experimental; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +static class ThingFilterMarkers +{ + public static bool drawingThingFilter; + + private static ThingFilterContext thingFilterContext; + + public static ThingFilterContext DrawnThingFilter + { + get => drawingThingFilter ? thingFilterContext : null; + set + { + if (value != null && thingFilterContext != null) + throw new Exception("Thing filter context already set!"); + + thingFilterContext = value; + } + } + + #region ThingFilter Markers + [MpPrefix(typeof(ITab_Storage), "FillTab")] + static void TabStorageFillTab_Prefix(ITab_Storage __instance) + { + var selThing = __instance.SelObject; + var selParent = __instance.SelStoreSettingsParent; + // If SelStoreSettingsParent is null, just return early. There'll be nothing to sync. + // The map could potentially be null - for example, if we're syncing mortar. The mortar gun itself + // holds the store settings, and turret guns don't have a map/location assigned - so we sync their parent. + // Because of that, we check if the parent is not null. + if (selParent == null || selThing is Thing { Map: null } or ThingComp { parent: { Map: null } }) + return; + DrawnThingFilter = new TabStorageWrapper(selParent); + } + + [MpPostfix(typeof(ITab_Storage), "FillTab")] + static void TabStorageFillTab_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(Dialog_BillConfig), "DoWindowContents")] + static void BillConfig_Prefix(Dialog_BillConfig __instance) => + DrawnThingFilter = new BillConfigWrapper(__instance.bill); + + [MpPostfix(typeof(Dialog_BillConfig), "DoWindowContents")] + static void BillConfig_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(Dialog_ManageOutfits), "DoWindowContents")] + static void ManageOutfit_Prefix(Dialog_ManageOutfits __instance) => + DrawnThingFilter = new OutfitWrapper(__instance.SelectedOutfit); + + [MpPostfix(typeof(Dialog_ManageOutfits), "DoWindowContents")] + static void ManageOutfit_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(Dialog_ManageFoodRestrictions), "DoWindowContents")] + static void ManageFoodRestriction_Prefix(Dialog_ManageFoodRestrictions __instance) => + DrawnThingFilter = new FoodRestrictionWrapper(__instance.SelectedFoodRestriction); + + [MpPostfix(typeof(Dialog_ManageFoodRestrictions), "DoWindowContents")] + static void ManageFoodRestriction_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(ITab_PenAutoCut), "FillTab")] + static void TabPenAutocutFillTab_Prefix(ITab_PenAutoCut __instance) => + DrawnThingFilter = new PenAutocutWrapper(__instance.SelectedCompAnimalPenMarker); + + [MpPostfix(typeof(ITab_PenAutoCut), "FillTab")] + static void TabPenAutocutFillTab_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(ITab_PenAnimals), "FillTab")] + static void TabPenAnimalsFillTab_Prefix(ITab_PenAnimals __instance) => + DrawnThingFilter = new PenAnimalsWrapper(__instance.SelectedCompAnimalPenMarker); + + [MpPostfix(typeof(ITab_PenAnimals), "FillTab")] + static void TabPenAnimalsFillTab_Postfix() => DrawnThingFilter = null; + + [MpPrefix(typeof(ITab_WindTurbineAutoCut), nameof(ITab_WindTurbineAutoCut.FillTab))] + static void TabWindTurbineAutocutFillTab_Prefix(ITab_WindTurbineAutoCut __instance) => + DrawnThingFilter = new DefaultAutocutWrapper(__instance.AutoCut); + + [MpPostfix(typeof(ITab_WindTurbineAutoCut), nameof(ITab_WindTurbineAutoCut.FillTab))] + static void TabWindTurbineAutocutFillTab_Postfix(ITab_WindTurbineAutoCut __instance) => DrawnThingFilter = null; + + [MpPrefix(typeof(ThingFilterUI), "DoThingFilterConfigWindow")] + static void ThingFilterUI_Prefix() => drawingThingFilter = true; + + [MpPostfix(typeof(ThingFilterUI), "DoThingFilterConfigWindow")] + static void ThingFilterUI_Postfix() => drawingThingFilter = false; + #endregion +} diff --git a/Source/Client/Syncing/Handler/SyncAction.cs b/Source/Client/Syncing/Handler/SyncAction.cs index de35df00..21ad38bf 100644 --- a/Source/Client/Syncing/Handler/SyncAction.cs +++ b/Source/Client/Syncing/Handler/SyncAction.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Multiplayer.Client.Util; using Verse; namespace Multiplayer.Client diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index 6f624cdf..661888dc 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -1,103 +1,15 @@ using System; -using System.Collections.Generic; +using Multiplayer.Client.Experimental; using Multiplayer.Client.Util; -using Multiplayer.Common; -using RimWorld; -using RimWorld.Planet; -using Verse; -namespace Multiplayer.Client -{ - public static class ImplSerialization - { - public static Type[] storageParents; - public static Type[] plantToGrowSettables; - - public static Type[] thingCompTypes; - public static Type[] abilityCompTypes; - public static Type[] designatorTypes; - public static Type[] worldObjectCompTypes; - public static Type[] hediffCompTypes; - - public static Type[] gameCompTypes; - public static Type[] worldCompTypes; - public static Type[] mapCompTypes; - - internal static Type[] supportedThingHolders = - { - typeof(Map), - typeof(Thing), - typeof(ThingComp), - typeof(WorldObject), - typeof(WorldObjectComp) - }; - - internal enum ISelectableImpl : byte - { - None, Thing, Zone, WorldObject - } - - internal enum VerbOwnerType : byte - { - None, Pawn, Ability, ThingComp - } - - public static void Init() - { - storageParents = TypeUtil.AllImplementationsOrdered(typeof(IStoreSettingsParent)); - plantToGrowSettables = TypeUtil.AllImplementationsOrdered(typeof(IPlantToGrowSettable)); - - thingCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(ThingComp)); - abilityCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(AbilityComp)); - designatorTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(Designator)); - worldObjectCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(WorldObjectComp)); - hediffCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(HediffComp)); +namespace Multiplayer.Client; - gameCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(GameComponent)); - worldCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(WorldComponent)); - mapCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(MapComponent)); - } - - internal static T ReadWithImpl(ByteReader data, IList impls) where T : class - { - ushort impl = data.ReadUShort(); - if (impl == ushort.MaxValue) return null; - return (T)SyncSerialization.ReadSyncObject(data, impls[impl]); - } - - internal static void WriteWithImpl(ByteWriter data, object obj, IList impls) where T : class - { - if (obj == null) - { - data.WriteUShort(ushort.MaxValue); - return; - } - - GetImpl(obj, impls, out Type implType, out int impl); - - if (implType == null) - throw new SerializationException($"Unknown {typeof(T)} implementation type {obj.GetType()}"); - - data.WriteUShort((ushort)impl); - SyncSerialization.WriteSyncObject(data, obj, implType); - } - - internal static void GetImpl(object obj, IList impls, out Type type, out int index) - { - type = null; - index = -1; - - if (obj == null) return; +public static class ImplSerialization +{ + public static Type[] syncSimples; - for (int i = 0; i < impls.Count; i++) - { - if (impls[i].IsAssignableFrom(obj.GetType())) - { - type = impls[i]; - index = i; - break; - } - } - } + public static void Init() + { + syncSimples = TypeUtil.AllImplementationsOrdered(typeof(ISyncSimple)); } } diff --git a/Source/Client/Syncing/Logger/LoggingByteReader.cs b/Source/Client/Syncing/Logger/LoggingByteReader.cs index 9e8641ff..b0fd1c4b 100644 --- a/Source/Client/Syncing/Logger/LoggingByteReader.cs +++ b/Source/Client/Syncing/Logger/LoggingByteReader.cs @@ -51,6 +51,11 @@ public override short ReadShort() return Log.NodePassthrough("short: ", base.ReadShort()); } + public override string ReadStringNullable(int maxLen = 32767) + { + return Log.NodePassthrough("string?: ", base.ReadStringNullable(maxLen)); + } + public override string ReadString(int maxLen = 32767) { return Log.NodePassthrough("string: ", base.ReadString(maxLen)); diff --git a/Source/Client/Syncing/Sync.cs b/Source/Client/Syncing/Sync.cs index 9b881a11..b9517202 100644 --- a/Source/Client/Syncing/Sync.cs +++ b/Source/Client/Syncing/Sync.cs @@ -2,7 +2,6 @@ using Multiplayer.API; using Multiplayer.Common; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -45,11 +44,6 @@ public static SyncMethod Method(Type targetType, string instancePath, string met return handler; } - public static SyncMethod[] MethodMultiTarget(MultiTarget targetType, string methodName, SyncType[] argTypes = null) - { - return targetType.Select(type => Method(type.Item1, type.Item2, methodName, argTypes)).ToArray(); - } - public static SyncField Field(Type targetType, string fieldName) { return Field(targetType, null, fieldName); @@ -62,11 +56,6 @@ public static SyncField Field(Type targetType, string instancePath, string field return handler; } - public static SyncField[] FieldMultiTarget(MultiTarget targetType, string fieldName) - { - return targetType.Select(type => Field(type.Item1, type.Item2, fieldName)).ToArray(); - } - public static SyncField[] Fields(Type targetType, string instancePath, params string[] memberPaths) { return memberPaths.Select(path => Field(targetType, instancePath, path)).ToArray(); @@ -418,15 +407,6 @@ public static void Watch(this SyncField[] group, object target = null, int index field.Watch(target, index); } - public static bool DoSync(this SyncMethod[] group, object target, params object[] args) - { - foreach (SyncMethod method in group) - if (method.targetType == null || method.targetType.IsInstanceOfType(target)) - return method.DoSync(target, args); - - return false; - } - public static SyncField[] SetBufferChanges(this SyncField[] group) { foreach (SyncField field in group) @@ -441,85 +421,4 @@ public static SyncField[] PostApply(this SyncField[] group, Action - { - private List<(Type, string)> types = new(); - - public void Add(Type type, string path) - { - types.Add((type, path)); - } - - public void Add(MultiTarget type, string path) - { - foreach (var multiType in type) - Add(multiType.Item1, multiType.Item2 + "/" + path); - } - - public void Add(Type type) - { - types.Add((type, null)); - } - - public IEnumerator<(Type, string)> GetEnumerator() - { - return types.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return types.GetEnumerator(); - } - } - - public class MethodGroup : IEnumerable - { - private List methods = new(); - - public void Add(string methodName, params SyncType[] argTypes) - { - methods.Add(Sync.Method(null, methodName, argTypes)); - } - - public bool MatchSync(object target, params object[] args) - { - if (!Multiplayer.ShouldSync) - return false; - - foreach (SyncMethod method in methods) { - if (Enumerable.SequenceEqual(method.argTypes.Select(t => t.type), args.Select(o => o.GetType()), TypeComparer.instance)) { - method.DoSync(target, args); - return true; - } - } - - return false; - } - - private class TypeComparer : IEqualityComparer - { - public static TypeComparer instance = new(); - - public bool Equals(Type x, Type y) - { - return x.IsAssignableFrom(y); - } - - public int GetHashCode(Type obj) - { - throw new NotImplementedException(); - } - } - - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - throw new NotImplementedException(); - } - } } diff --git a/Source/Client/Syncing/SyncSerialization.cs b/Source/Client/Syncing/SyncSerialization.cs index 6c2ddf03..d1f13ebb 100644 --- a/Source/Client/Syncing/SyncSerialization.cs +++ b/Source/Client/Syncing/SyncSerialization.cs @@ -9,17 +9,16 @@ using System.Runtime.CompilerServices; using System.Text; using System.Xml; +using Multiplayer.Client.Experimental; using Verse; namespace Multiplayer.Client { - // Syncs a type with all its declared fields - public interface ISyncSimple { } - public static class SyncSerialization { public static void Init() { + RwImplSerialization.Init(); ImplSerialization.Init(); DefSerialization.Init(); } @@ -54,7 +53,10 @@ public static bool CanHandle(SyncType syncType) || typeof(ITuple).IsAssignableFrom(gtd)) && CanHandleGenericArgs(type); if (typeof(ISyncSimple).IsAssignableFrom(type)) - return AccessTools.GetDeclaredFields(type).All(f => CanHandle(f.FieldType)); + return ImplSerialization.syncSimples. + Where(t => type.IsAssignableFrom(t)). + SelectMany(AccessTools.GetDeclaredFields). + All(f => CanHandle(f.FieldType)); if (typeof(Def).IsAssignableFrom(type)) return true; if (typeof(Designator).IsAssignableFrom(type)) @@ -214,7 +216,7 @@ private static object ReadSyncObjectInternal(ByteReader data, SyncType syncType) return dictionary; } - if (genericTypeDefinition == typeof(Pair<,>)) + if (genericTypeDefinition == typeof(Pair<,>) || genericTypeDefinition == typeof(ValueTuple<,>)) { Type[] arguments = type.GetGenericArguments(); object[] parameters = @@ -249,8 +251,10 @@ private static object ReadSyncObjectInternal(ByteReader data, SyncType syncType) if (typeof(ISyncSimple).IsAssignableFrom(type)) { - var obj = MpUtil.NewObjectNoCtor(type); - foreach (var field in AccessTools.GetDeclaredFields(type)) + ushort typeIndex = data.ReadUShort(); + var objType = ImplSerialization.syncSimples[typeIndex]; + var obj = MpUtil.NewObjectNoCtor(objType); + foreach (var field in AccessTools.GetDeclaredFields(objType)) field.SetValue(obj, ReadSyncObject(data, field.FieldType)); return obj; } @@ -277,7 +281,7 @@ private static object ReadSyncObjectInternal(ByteReader data, SyncType syncType) if (typeof(Designator).IsAssignableFrom(type)) { ushort desId = ReadSync(data); - type = ImplSerialization.designatorTypes[desId]; // Replaces the type! + type = RwImplSerialization.designatorTypes[desId]; // Replaces the type! } // Where the magic happens @@ -309,13 +313,11 @@ public static void WriteSync(ByteWriter data, T obj) public static void WriteSyncObject(ByteWriter data, object obj, SyncType syncType) { - MpContext context = data.MpContext(); Type type = syncType.type; - var log = (data as LoggingByteWriter)?.Log; log?.Enter($"{type.FullName}: {obj ?? "null"}"); - if (obj != null && !type.IsAssignableFrom(obj.GetType())) + if (obj != null && !type.IsInstanceOfType(obj)) throw new SerializationException($"Serializing with type {type} but got object of type {obj.GetType()}"); try @@ -476,6 +478,16 @@ public static void WriteSyncObject(ByteWriter data, object obj, SyncType syncTyp return; } + if (genericTypeDefinition == typeof(ValueTuple<,>)) + { + Type[] arguments = type.GetGenericArguments(); + + WriteSyncObject(data, AccessTools.DeclaredField(type, "Item1").GetValue(obj), arguments[0]); + WriteSyncObject(data, AccessTools.DeclaredField(type, "Item2").GetValue(obj), arguments[1]); + + return; + } + if (typeof(ITuple).IsAssignableFrom(genericTypeDefinition)) // ValueTuple or Tuple { Type[] arguments = type.GetGenericArguments(); @@ -492,7 +504,8 @@ public static void WriteSyncObject(ByteWriter data, object obj, SyncType syncTyp if (typeof(ISyncSimple).IsAssignableFrom(type)) { - foreach (var field in AccessTools.GetDeclaredFields(type)) + data.WriteUShort((ushort)ImplSerialization.syncSimples.FindIndex(obj.GetType())); + foreach (var field in AccessTools.GetDeclaredFields(obj.GetType())) WriteSyncObject(data, field.GetValue(obj), field.FieldType); return; } @@ -519,7 +532,7 @@ public static void WriteSyncObject(ByteWriter data, object obj, SyncType syncTyp // Special case for Designators to change the type if (typeof(Designator).IsAssignableFrom(type)) { - data.WriteUShort((ushort) Array.IndexOf(ImplSerialization.designatorTypes, obj.GetType())); + data.WriteUShort((ushort) Array.IndexOf(RwImplSerialization.designatorTypes, obj.GetType())); } // Where the magic happens diff --git a/Source/Client/Syncing/Worker/ReadingSyncWorker.cs b/Source/Client/Syncing/Worker/ReadingSyncWorker.cs index 21bbed65..60bc3320 100644 --- a/Source/Client/Syncing/Worker/ReadingSyncWorker.cs +++ b/Source/Client/Syncing/Worker/ReadingSyncWorker.cs @@ -96,12 +96,12 @@ public override void Bind(ref bool obj) public override void Bind(ref string obj) { - obj = reader.ReadString(); + obj = reader.ReadStringNullable(); } public override void BindType(ref Type type) { - type = TypeRWHelper.GetType(reader.ReadUShort(), typeof(T)); + type = RwTypeHelper.GetType(reader.ReadUShort(), typeof(T)); } internal void Reset() diff --git a/Source/Client/Syncing/Worker/SyncWorkers.cs b/Source/Client/Syncing/Worker/SyncWorkers.cs index ae2a9207..64602c29 100644 --- a/Source/Client/Syncing/Worker/SyncWorkers.cs +++ b/Source/Client/Syncing/Worker/SyncWorkers.cs @@ -58,7 +58,7 @@ public void Add(SyncWorkerDelegate func) }), true); } - void Add(SyncWorkerDelegate sync, bool append = true) + private void Add(SyncWorkerDelegate sync, bool append = true) { if (append) syncWorkers.Add(sync); @@ -418,24 +418,24 @@ public static SyncWorkerDictionaryTree Merge(params SyncWorkerDictionaryTree[] t } } - internal static class TypeRWHelper + internal static class RwTypeHelper { private static Dictionary cache = new Dictionary(); - static TypeRWHelper() + static RwTypeHelper() { - cache[typeof(IStoreSettingsParent)] = ImplSerialization.storageParents; - cache[typeof(IPlantToGrowSettable)] = ImplSerialization.plantToGrowSettables; + cache[typeof(IStoreSettingsParent)] = RwImplSerialization.storageParents; + cache[typeof(IPlantToGrowSettable)] = RwImplSerialization.plantToGrowSettables; - cache[typeof(ThingComp)] = ImplSerialization.thingCompTypes; - cache[typeof(AbilityComp)] = ImplSerialization.abilityCompTypes; - cache[typeof(Designator)] = ImplSerialization.designatorTypes; - cache[typeof(WorldObjectComp)] = ImplSerialization.worldObjectCompTypes; - cache[typeof(HediffComp)] = ImplSerialization.hediffCompTypes; + cache[typeof(ThingComp)] = RwImplSerialization.thingCompTypes; + cache[typeof(AbilityComp)] = RwImplSerialization.abilityCompTypes; + cache[typeof(Designator)] = RwImplSerialization.designatorTypes; + cache[typeof(WorldObjectComp)] = RwImplSerialization.worldObjectCompTypes; + cache[typeof(HediffComp)] = RwImplSerialization.hediffCompTypes; - cache[typeof(GameComponent)] = ImplSerialization.gameCompTypes; - cache[typeof(WorldComponent)] = ImplSerialization.worldCompTypes; - cache[typeof(MapComponent)] = ImplSerialization.mapCompTypes; + cache[typeof(GameComponent)] = RwImplSerialization.gameCompTypes; + cache[typeof(WorldComponent)] = RwImplSerialization.worldCompTypes; + cache[typeof(MapComponent)] = RwImplSerialization.mapCompTypes; } internal static void FlushCache() diff --git a/Source/Client/Syncing/Worker/WritingSyncWorker.cs b/Source/Client/Syncing/Worker/WritingSyncWorker.cs index 78e92186..e801a3f0 100644 --- a/Source/Client/Syncing/Worker/WritingSyncWorker.cs +++ b/Source/Client/Syncing/Worker/WritingSyncWorker.cs @@ -98,7 +98,7 @@ public override void Bind(ref string obj) public override void BindType(ref Type type) { - writer.WriteUShort(TypeRWHelper.GetTypeIndex(type, typeof(T))); + writer.WriteUShort(RwTypeHelper.GetTypeIndex(type, typeof(T))); } internal void Reset() diff --git a/Source/Client/UI/CursorAndPing.cs b/Source/Client/UI/CursorAndPing.cs index 94316696..882fb701 100644 --- a/Source/Client/UI/CursorAndPing.cs +++ b/Source/Client/UI/CursorAndPing.cs @@ -11,7 +11,6 @@ namespace Multiplayer.Client { - public class CursorAndPing { public List pings = new(); @@ -171,7 +170,6 @@ private void SendSelected() } } - public class PingInfo { public int player; diff --git a/Source/Client/UI/DrawPingMap.cs b/Source/Client/UI/DrawPingMap.cs index 3eb91f1c..86911551 100644 --- a/Source/Client/UI/DrawPingMap.cs +++ b/Source/Client/UI/DrawPingMap.cs @@ -17,7 +17,7 @@ static void Postfix() foreach (var ping in Multiplayer.session.cursorAndPing.pings) { if (ping.mapId != Find.CurrentMap.uniqueID) continue; - if (Multiplayer.session.GetPlayerInfo(ping.player) is not { } player) continue; + if (ping.PlayerInfo is not { } player) continue; ping.DrawAt(ping.mapLoc.MapToUIPosition(), player.color, size); } diff --git a/Source/Client/UI/DrawPingPlanet.cs b/Source/Client/UI/DrawPingPlanet.cs index d071c26f..002e60bd 100644 --- a/Source/Client/UI/DrawPingPlanet.cs +++ b/Source/Client/UI/DrawPingPlanet.cs @@ -15,7 +15,7 @@ static void Postfix() foreach (var ping in Multiplayer.session.cursorAndPing.pings) { if (ping.mapId != -1) continue; - if (Multiplayer.session.GetPlayerInfo(ping.player) is not { } player) continue; + if (ping.PlayerInfo is not { } player) continue; var tileCenter = GenWorldUI.WorldToUIPosition(Find.WorldGrid.GetTileCenter(ping.planetTile)); const float size = 30f; diff --git a/Source/Client/UI/IngameDebug.cs b/Source/Client/UI/IngameDebug.cs new file mode 100644 index 00000000..86d67d8b --- /dev/null +++ b/Source/Client/UI/IngameDebug.cs @@ -0,0 +1,157 @@ +using System.Linq; +using System.Text; +using HarmonyLib; +using Multiplayer.Client.Desyncs; +using Multiplayer.Client.Util; +using RimWorld; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public static class IngameDebug +{ + private static double avgDelta; + private static double avgTickTime; + + public static float tps; + private static float lastTicksAt; + private static int lastTicks; + + private const float BtnMargin = 8f; + private const float BtnHeight = 27f; + private const float BtnWidth = 80f; + + internal static void DoDebugPrintout() + { + if (Multiplayer.ShowDevInfo) + { + int timerLag = (TickPatch.tickUntil - TickPatch.Timer); + StringBuilder text = new StringBuilder(); + text.Append( + $"{Faction.OfPlayer.loadID} {Multiplayer.RealPlayerFaction?.loadID} {Find.UniqueIDsManager.nextThingID} j:{Find.UniqueIDsManager.nextJobID} {Find.TickManager.TicksGame} {Find.TickManager.CurTimeSpeed} {TickPatch.Timer} {TickPatch.tickUntil} {timerLag}"); + text.Append($"\n{Time.deltaTime * 60f:0.0000} {TickPatch.tickTimer.ElapsedMilliseconds}"); + text.Append($"\n{avgDelta = (avgDelta * 59.0 + Time.deltaTime * 60.0) / 60.0:0.0000}"); + text.Append( + $"\n{avgTickTime = (avgTickTime * 59.0 + TickPatch.tickTimer.ElapsedMilliseconds) / 60.0:0.0000} {Find.World.worldObjects.settlements.Count}"); + text.Append( + $"\n{Multiplayer.session?.receivedCmds} {Multiplayer.session?.remoteSentCmds} {Multiplayer.session?.remoteTickUntil}"); + Rect rect = new Rect(80f, 60f, 330f, Text.CalcHeight(text.ToString(), 330f)); + Widgets.Label(rect, text.ToString()); + + if (Input.GetKey(KeyCode.End)) + { + avgDelta = 0; + avgTickTime = 0; + } + } + + if (Multiplayer.ShowDevInfo && Multiplayer.Client != null && Find.CurrentMap != null) + { + var async = Find.CurrentMap.AsyncTime(); + StringBuilder text = new StringBuilder(); + text.Append( + $"{Multiplayer.game.sync.knownClientOpinions.Count} {Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick} {async.mapTicks} {TickPatch.serverFrozen} {TickPatch.frozenAt} "); + + text.Append( + $"z: {Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count()} d: {Find.CurrentMap.designationManager.designationsByDef.Count} hc: {Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}"); + + if (Find.CurrentMap.ParentFaction != null) + { + int faction = Find.CurrentMap.ParentFaction.loadID; + MultiplayerMapComp comp = Find.CurrentMap.MpComp(); + FactionMapData data = comp.factionData.GetValueSafe(faction); + + if (data != null) + { + text.Append($" h: {data.listerHaulables.ThingsPotentiallyNeedingHauling().Count}"); + text.Append($" sg: {data.haulDestinationManager.AllGroupsListForReading.Count}"); + } + } + + text.Append( + $" {Find.CurrentMap.Parent.IncidentTargetTags().ToStringSafeEnumerable()} {Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}"); + + text.Append( + $"\n{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)} {Find.UniqueIDsManager.nextThingID}"); + text.Append( + $"\n{DeferredStackTracing.acc} {MpInput.Mouse2UpWithoutDrag} {Input.GetKeyUp(KeyCode.Mouse2)} {Input.GetKey(KeyCode.Mouse2)}"); + text.Append($"\n{(uint)async.randState} {(uint)(async.randState >> 32)}"); + text.Append($"\n{(uint)Multiplayer.AsyncWorldTime.randState} {(uint)(Multiplayer.AsyncWorldTime.randState >> 32)}"); + text.Append( + $"\n{async.cmds.Count} {Multiplayer.AsyncWorldTime.cmds.Count} {async.slower.forceNormalSpeedUntil} {Multiplayer.GameComp.asyncTime}"); + text.Append( + $"\nt{DeferredStackTracing.maxTraceDepth} p{SimplePool.FreeItemsCount} {DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize} {DeferredStackTracingImpl.collisions}"); + + text.Append(Find.WindowStack.focusedWindow is ImmediateWindow win + ? $"\nImmediateWindow: {MpUtil.DelegateMethodInfo(win.doWindowFunc?.Method)}" + : $"\n{Find.WindowStack.focusedWindow}"); + + text.Append($"\n{UI.CurUICellSize()} {Find.WindowStack.windows.ToStringSafeEnumerable()}"); + text.Append($"\n\nMap TPS: {tps:0.00}"); + text.Append($"\nDelta: {Time.deltaTime * 1000f}"); + text.Append($"\nAverage ft: {TickPatch.avgFrameTime}"); + text.Append($"\nServer tpt: {TickPatch.serverTimePerTick}"); + + var calcStpt = TickPatch.tickUntil - TickPatch.Timer <= 3 ? TickPatch.serverTimePerTick * 1.2f : + TickPatch.tickUntil - TickPatch.Timer >= 7 ? TickPatch.serverTimePerTick * 0.8f : + TickPatch.serverTimePerTick; + text.Append($"\nServer tpt: {calcStpt}"); + + Rect rect1 = new Rect(80f, 170f, 330f, Text.CalcHeight(text.ToString(), 330f)); + Widgets.Label(rect1, text.ToString()); + + if (Time.time - lastTicksAt > 0.5f) + { + tps = (tps + (async.mapTicks - lastTicks) * 2f) / 2f; + lastTicks = async.mapTicks; + lastTicksAt = Time.time; + } + } + + //if (Event.current.type == EventType.Repaint) + // RandGetValuePatch.tracesThistick = 0; + } + + internal static float DoDevInfo(float y) + { + float x = UI.screenWidth - BtnWidth - BtnMargin; + + if (Multiplayer.ShowDevInfo && Multiplayer.WriterLog != null) + { + if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Write ({Multiplayer.WriterLog.NodeCount})")) + Find.WindowStack.Add(Multiplayer.WriterLog); + + y += BtnHeight; + if (Widgets.ButtonText(new Rect(x, y, BtnWidth, BtnHeight), $"Read ({Multiplayer.ReaderLog.NodeCount})")) + Find.WindowStack.Add(Multiplayer.ReaderLog); + + y += BtnHeight; + var oldGhostMode = Multiplayer.session.ghostModeCheckbox; + Widgets.CheckboxLabeled(new Rect(x, y, BtnWidth, 30f), "Ghost", ref Multiplayer.session.ghostModeCheckbox); + if (oldGhostMode != Multiplayer.session.ghostModeCheckbox) + { + SyncFieldUtil.ClearAllBufferedChanges(); + } + + return BtnHeight * 3; + } + + return 0; + } + + internal static float DoDebugModeLabel(float y) + { + float x = UI.screenWidth - BtnWidth - BtnMargin; + + if (Multiplayer.Client != null && Multiplayer.GameComp.debugMode) + { + using (MpStyle.Set(GameFont.Tiny).Set(TextAnchor.MiddleCenter)) + Widgets.Label(new Rect(x, y, BtnWidth, 30f), $"Debug mode"); + + return BtnHeight; + } + + return 0; + } +} diff --git a/Source/Client/UI/IngameModal.cs b/Source/Client/UI/IngameModal.cs new file mode 100644 index 00000000..d5ee4728 --- /dev/null +++ b/Source/Client/UI/IngameModal.cs @@ -0,0 +1,42 @@ +using System; +using Multiplayer.Client.Util; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public static class IngameModal +{ + public const int ModalWindowId = 26461263; + + internal static void DrawModalWindow(string text, Func shouldShow, Action onCancel, string cancelButtonLabel) + { + string textWithEllipsis = $"{text}{MpUI.FixedEllipsis()}"; + float textWidth = Text.CalcSize(textWithEllipsis).x; + float windowWidth = Math.Max(240f, textWidth + 40f); + float windowHeight = onCancel != null ? 100f : 75f; + var rect = new Rect(0, 0, windowWidth, windowHeight).CenterOn(new Rect(0, 0, UI.screenWidth, UI.screenHeight)); + + Find.WindowStack.ImmediateWindow(ModalWindowId, rect, WindowLayer.Super, () => + { + if (!shouldShow()) return; + + var textRect = rect.AtZero(); + if (onCancel != null) + { + textRect.yMin += 5f; + textRect.height -= 50f; + } + + Text.Anchor = TextAnchor.MiddleCenter; + Text.Font = GameFont.Small; + Widgets.Label(textRect, text); + Text.Anchor = TextAnchor.UpperLeft; + + var cancelBtn = new Rect(0, textRect.yMax, 100f, 35f).CenteredOnXIn(textRect); + + if (onCancel != null && Widgets.ButtonText(cancelBtn, cancelButtonLabel)) + onCancel(); + }, absorbInputAroundWindow: true); + } +} diff --git a/Source/Client/UI/IngameUI.cs b/Source/Client/UI/IngameUI.cs index ac938a80..f45d53ff 100644 --- a/Source/Client/UI/IngameUI.cs +++ b/Source/Client/UI/IngameUI.cs @@ -1,16 +1,12 @@ using HarmonyLib; using Multiplayer.Common; using RimWorld; -using RimWorld.Planet; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Reflection; using UnityEngine; using Verse; -using Multiplayer.Client.Desyncs; -using Multiplayer.Client.Util; +using Multiplayer.Common.Util; +using RimWorld.Planet; namespace Multiplayer.Client { @@ -18,38 +14,43 @@ namespace Multiplayer.Client [HotSwappable] public static class IngameUIPatch { - const float btnMargin = 8f; - const float btnHeight = 27f; - const float btnWidth = 80f; + public static List> upperLeftDrawers = new() + { + DoChatAndTicksBehind, + IngameDebug.DoDevInfo, + IngameDebug.DoDebugModeLabel + }; - public static List> upperLeftDrawers = new() { DoChatAndTicksBehind, DoDevInfo, DoDebugModeLabel }; + private const float BtnMargin = 8f; + private const float BtnHeight = 27f; + private const float BtnWidth = 80f; static bool Prefix() { Text.Font = GameFont.Small; if (MpVersion.IsDebug) { - DoDebugPrintout(); + IngameDebug.DoDebugPrintout(); } if (Multiplayer.IsReplay || TickPatch.Simulating) - DrawTimeline(); + ReplayTimeline.DrawTimeline(); if (TickPatch.Simulating) { - HandleSimulatingEvents(); - - DrawModalWindow( + IngameModal.DrawModalWindow( TickPatch.simulating.simTextKey.Translate(), () => TickPatch.Simulating, TickPatch.simulating.onCancel, TickPatch.simulating.cancelButtonKey.Translate() ); + + HandleUiEventsWhenSimulating(); } if (TickPatch.Frozen) { - DrawModalWindow( + IngameModal.DrawModalWindow( "Waiting for other players", () => TickPatch.Frozen, MainMenuPatch.AskQuitToMainMenu, @@ -57,7 +58,7 @@ static bool Prefix() ); } - DoButtons(); + DoUpperLeftButtons(); if (Multiplayer.Client != null && !Multiplayer.IsReplay @@ -74,95 +75,26 @@ static bool Prefix() return Find.Maps.Count > 0; } - private static double avgDelta; - private static double avgTickTime; - - private static float tps; - private static float lastTicksAt; - private static int lastTicks; - - static void DoDebugPrintout() + private static void DoUpperLeftButtons() { - if (Multiplayer.ShowDevInfo) - { - int timerLag = (TickPatch.tickUntil - TickPatch.Timer); - StringBuilder text = new StringBuilder(); - text.Append($"{Faction.OfPlayer.loadID} {Multiplayer.RealPlayerFaction?.loadID} {Find.UniqueIDsManager.nextThingID} j:{Find.UniqueIDsManager.nextJobID} {Find.TickManager.TicksGame} {Find.TickManager.CurTimeSpeed} {TickPatch.Timer} {TickPatch.tickUntil} {timerLag} {TickPatch.maxBehind}"); - text.Append($"\n{Time.deltaTime * 60f:0.0000} {TickPatch.tickTimer.ElapsedMilliseconds}"); - text.Append($"\n{avgDelta = (avgDelta * 59.0 + Time.deltaTime * 60.0) / 60.0:0.0000}"); - text.Append($"\n{avgTickTime = (avgTickTime * 59.0 + TickPatch.tickTimer.ElapsedMilliseconds) / 60.0:0.0000} {Find.World.worldObjects.settlements.Count}"); - text.Append($"\n{Multiplayer.session?.receivedCmds} {Multiplayer.session?.remoteSentCmds} {Multiplayer.session?.remoteTickUntil}"); - Rect rect = new Rect(80f, 60f, 330f, Text.CalcHeight(text.ToString(), 330f)); - Widgets.Label(rect, text.ToString()); - - if (Input.GetKey(KeyCode.End)) - { - avgDelta = 0; - avgTickTime = 0; - } - } - - if (Multiplayer.ShowDevInfo && Multiplayer.Client != null && Find.CurrentMap != null) - { - var async = Find.CurrentMap.AsyncTime(); - StringBuilder text = new StringBuilder(); - text.Append($"{Multiplayer.game.sync.knownClientOpinions.Count} {Multiplayer.game.sync.knownClientOpinions.FirstOrDefault()?.startTick} {async.mapTicks} {TickPatch.serverFrozen} {TickPatch.frozenAt} "); - - text.Append($"z: {Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count()} d: {Find.CurrentMap.designationManager.designationsByDef.Count} hc: {Find.CurrentMap.listerHaulables.ThingsPotentiallyNeedingHauling().Count}"); - - if (Find.CurrentMap.ParentFaction != null) - { - int faction = Find.CurrentMap.ParentFaction.loadID; - MultiplayerMapComp comp = Find.CurrentMap.MpComp(); - FactionMapData data = comp.factionData.GetValueSafe(faction); - - if (data != null) - { - text.Append($" h: {data.listerHaulables.ThingsPotentiallyNeedingHauling().Count}"); - text.Append($" sg: {data.haulDestinationManager.AllGroupsListForReading.Count}"); - } - } - - text.Append($" {Multiplayer.GlobalIdBlock.Current} {Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id}"); - - text.Append($"\n{SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)} {Find.UniqueIDsManager.nextThingID}"); - text.Append($"\n{DeferredStackTracing.acc} {MpInput.Mouse2UpWithoutDrag} {Input.GetKeyUp(KeyCode.Mouse2)} {Input.GetKey(KeyCode.Mouse2)}"); - text.Append($"\n{(uint)async.randState} {(uint)(async.randState >> 32)}"); - text.Append($"\n{(uint)Multiplayer.WorldComp.randState} {(uint)(Multiplayer.WorldComp.randState >> 32)}"); - text.Append($"\n{async.cmds.Count} {Multiplayer.WorldComp.cmds.Count} {async.slower.forceNormalSpeedUntil} {Multiplayer.GameComp.asyncTime}"); - text.Append($"\nt{DeferredStackTracing.maxTraceDepth} p{SimplePool.FreeItemsCount} {DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize} {DeferredStackTracingImpl.collisions}"); - - text.Append(Find.WindowStack.focusedWindow is ImmediateWindow win - ? $"\nImmediateWindow: {MpUtil.DelegateMethodInfo(win.doWindowFunc?.Method)}" - : $"\n{Find.WindowStack.focusedWindow}"); - - text.Append($"\n{UI.CurUICellSize()} {Find.WindowStack.windows.ToStringSafeEnumerable()}\n\nMap TPS: {tps}"); - - Rect rect1 = new Rect(80f, 170f, 330f, Text.CalcHeight(text.ToString(), 330f)); - Widgets.Label(rect1, text.ToString()); - - if (Time.time - lastTicksAt > 0.5f) - { - tps = (tps + (async.mapTicks - lastTicks) * 2f) / 2f; - lastTicks = async.mapTicks; - lastTicksAt = Time.time; - } - } + if (Multiplayer.session == null) + return; + float y = BtnMargin; - //if (Event.current.type == EventType.Repaint) - // RandGetValuePatch.tracesThistick = 0; + foreach (var drawer in upperLeftDrawers) + y += drawer(y); } - static float DoChatAndTicksBehind(float y) + private static float DoChatAndTicksBehind(float y) { if (Multiplayer.IsReplay) return 0; - float x = UI.screenWidth - btnWidth - btnMargin; + float x = UI.screenWidth - BtnWidth - BtnMargin; var session = Multiplayer.session; - var btnRect = new Rect(x, y, btnWidth, btnHeight); + var btnRect = new Rect(x, y, BtnWidth, BtnHeight); var chatColor = session.players.Any(p => p.status == PlayerStatus.Desynced) ? "#ff5555" : "#dddddd"; var hasUnread = session.hasUnread ? "*" : ""; var chatLabel = $"{"MpChatButton".Translate()} ({session.players.Count}){hasUnread}"; @@ -189,61 +121,10 @@ static float DoChatAndTicksBehind(float y) TooltipHandler.TipRegion(indRect, new TipSignal(text, 31641624)); } - return btnHeight; + return BtnHeight; } - static float DoDevInfo(float y) - { - float x = UI.screenWidth - btnWidth - btnMargin; - - if (Multiplayer.ShowDevInfo && Multiplayer.WriterLog != null) - { - if (Widgets.ButtonText(new Rect(x, y, btnWidth, btnHeight), $"Write ({Multiplayer.WriterLog.NodeCount})")) - Find.WindowStack.Add(Multiplayer.WriterLog); - - y += btnHeight; - if (Widgets.ButtonText(new Rect(x, y, btnWidth, btnHeight), $"Read ({Multiplayer.ReaderLog.NodeCount})")) - Find.WindowStack.Add(Multiplayer.ReaderLog); - - y += btnHeight; - var oldGhostMode = Multiplayer.session.ghostModeCheckbox; - Widgets.CheckboxLabeled(new Rect(x, y, btnWidth, 30f), "Ghost", ref Multiplayer.session.ghostModeCheckbox); - if (oldGhostMode != Multiplayer.session.ghostModeCheckbox) - SyncFieldUtil.ClearAllBufferedChanges(); - - return btnHeight * 3; - } - - return 0; - } - - static float DoDebugModeLabel(float y) - { - float x = UI.screenWidth - btnWidth - btnMargin; - - if (Multiplayer.Client != null && Multiplayer.GameComp.debugMode) - { - using (MpStyle.Set(GameFont.Tiny).Set(TextAnchor.MiddleCenter)) - Widgets.Label(new Rect(x, y, btnWidth, 30f), $"Debug mode"); - - return btnHeight; - } - - return 0; - } - - static void DoButtons() - { - if (Multiplayer.session == null) - return; - - float y = btnMargin; - - foreach (var drawer in upperLeftDrawers) - y += drawer(y); - } - - static void IndicatorInfo(out Color color, out string text, out bool slow) + private static void IndicatorInfo(out Color color, out string text, out bool slow) { int behind = TickPatch.tickUntil - TickPatch.Timer; text = "MpTicksBehind".Translate(behind); @@ -263,133 +144,12 @@ static void IndicatorInfo(out Color color, out string text, out bool slow) { color = new Color(0.0f, 0.8f, 0.0f); } - } - - const float TimelineMargin = 50f; - const float TimelineHeight = 35f; - static void DrawTimeline() - { - Rect rect = new Rect(TimelineMargin, UI.screenHeight - 35f - TimelineHeight - 10f - 30f, UI.screenWidth - TimelineMargin * 2, TimelineHeight + 30f); - Find.WindowStack.ImmediateWindow(TimelineWindowId, rect, WindowLayer.SubSuper, DrawTimelineWindow, doBackground: false, shadowAlpha: 0); - } - - static void DrawTimelineWindow() - { - Rect rect = new Rect(0, 30f, UI.screenWidth - TimelineMargin * 2, TimelineHeight); - - Widgets.DrawBoxSolid(rect, new Color(0.6f, 0.6f, 0.6f, 0.8f)); - - int timerStart = Multiplayer.session.replayTimerStart >= 0 ? - Multiplayer.session.replayTimerStart : Multiplayer.session.dataSnapshot.cachedAtTime; - - int timerEnd = Multiplayer.session.replayTimerEnd >= 0 ? - Multiplayer.session.replayTimerEnd : TickPatch.tickUntil; - - int timeLen = timerEnd - timerStart; - - MpUI.DrawRotatedLine(new Vector2(rect.xMin + 2f, rect.center.y), TimelineHeight, 20f, 90f, Color.white); - MpUI.DrawRotatedLine(new Vector2(rect.xMax - 2f, rect.center.y), TimelineHeight, 20f, 90f, Color.white); - - float progress = (TickPatch.Timer - timerStart) / (float)timeLen; - float progressX = rect.xMin + progress * rect.width; - MpUI.DrawRotatedLine(new Vector2((int)progressX, rect.center.y), TimelineHeight, 20f, 90f, Color.green); - - float mouseX = Event.current.mousePosition.x; - ReplayEvent mouseEvent = null; - - foreach (var ev in Multiplayer.session.events) - { - if (ev.time < timerStart || ev.time > timerEnd) - continue; - - var pointX = rect.xMin + (ev.time - timerStart) / (float)timeLen * rect.width; - - //GUI.DrawTexture(new Rect(pointX - 12f, rect.yMin - 24f, 24f, 24f), texture); - MpUI.DrawRotatedLine(new Vector2(pointX, rect.center.y), TimelineHeight, 20f, 90f, ev.color); - - if (Mouse.IsOver(rect) && Math.Abs(mouseX - pointX) < 10) - { - mouseX = pointX; - mouseEvent = ev; - } - } - - if (Mouse.IsOver(rect)) - { - float mouseProgress = (mouseX - rect.xMin) / rect.width; - int mouseTimer = timerStart + (int)(timeLen * mouseProgress); - - MpUI.DrawRotatedLine(new Vector2(mouseX, rect.center.y), TimelineHeight, 15f, 90f, Color.blue); - - if (Event.current.type == EventType.MouseUp) - { - TickPatch.SetSimulation(mouseTimer, canESC: true); - - if (mouseTimer < TickPatch.Timer) - { - ClientJoiningState.ReloadGame(Multiplayer.session.dataSnapshot.mapData.Keys.ToList(), false, Multiplayer.GameComp.asyncTime); - } - } - - if (Event.current.isMouse) - Event.current.Use(); - - string tooltip = $"Tick {mouseTimer}"; - if (mouseEvent != null) - tooltip = $"{mouseEvent.name}\n{tooltip}"; - - const int TickTipId = 215462143; - - TooltipHandler.TipRegion(rect, new TipSignal(tooltip, TickTipId)); - // Remove delay between the mouseover and showing - if (TooltipHandler.activeTips.TryGetValue(TickTipId, out ActiveTip tip)) - tip.firstTriggerTime = 0; - } - - if (TickPatch.Simulating) - { - float pct = (TickPatch.simulating.target.Value - timerStart) / (float)timeLen; - float simulateToX = rect.xMin + rect.width * pct; - MpUI.DrawRotatedLine(new Vector2(simulateToX, rect.center.y), TimelineHeight, 15f, 90f, Color.yellow); - } - } - - public const int ModalWindowId = 26461263; - public const int TimelineWindowId = 5723681; - - static void DrawModalWindow(string text, Func shouldShow, Action onCancel, string cancelButtonLabel) - { - string textWithEllipsis = $"{text}{MpUI.FixedEllipsis()}"; - float textWidth = Text.CalcSize(textWithEllipsis).x; - float windowWidth = Math.Max(240f, textWidth + 40f); - float windowHeight = onCancel != null ? 100f : 75f; - var rect = new Rect(0, 0, windowWidth, windowHeight).CenterOn(new Rect(0, 0, UI.screenWidth, UI.screenHeight)); - - Find.WindowStack.ImmediateWindow(ModalWindowId, rect, WindowLayer.Super, () => - { - if (!shouldShow()) return; - - var textRect = rect.AtZero(); - if (onCancel != null) - { - textRect.yMin += 5f; - textRect.height -= 50f; - } - - Text.Anchor = TextAnchor.MiddleCenter; - Text.Font = GameFont.Small; - Widgets.Label(textRect, text); - Text.Anchor = TextAnchor.UpperLeft; - - var cancelBtn = new Rect(0, textRect.yMax, 100f, 35f).CenteredOnXIn(textRect); - - if (onCancel != null && Widgets.ButtonText(cancelBtn, cancelButtonLabel)) - onCancel(); - }, absorbInputAroundWindow: true); + if (!WorldRendererUtility.WorldRenderedNow) + text += $"\n\nCurrent map avg TPS: {IngameDebug.tps:0.00}"; } - static void HandleSimulatingEvents() + private static void HandleUiEventsWhenSimulating() { if (TickPatch.simulating.canEsc && Event.current.type == EventType.KeyUp && Event.current.keyCode == KeyCode.Escape) { @@ -398,27 +158,4 @@ static void HandleSimulatingEvents() } } } - - [HarmonyPatch] - static class MakeSpaceForReplayTimeline - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(MouseoverReadout), nameof(MouseoverReadout.MouseoverReadoutOnGUI)); - yield return AccessTools.Method(typeof(GlobalControls), nameof(GlobalControls.GlobalControlsOnGUI)); - yield return AccessTools.Method(typeof(WorldGlobalControls), nameof(WorldGlobalControls.WorldGlobalControlsOnGUI)); - } - - static void Prefix() - { - if (Multiplayer.IsReplay) - UI.screenHeight -= 60; - } - - static void Postfix() - { - if (Multiplayer.IsReplay) - UI.screenHeight += 60; - } - } } diff --git a/Source/Client/UI/MainMenuAnimation.cs b/Source/Client/UI/MainMenuAnimation.cs index 0658c1f7..a73b438a 100644 --- a/Source/Client/UI/MainMenuAnimation.cs +++ b/Source/Client/UI/MainMenuAnimation.cs @@ -3,6 +3,7 @@ using System.Linq; using HarmonyLib; using Multiplayer.Client.Util; +using Multiplayer.Common.Util; using RimWorld; using UnityEngine; using Verse; @@ -25,7 +26,7 @@ static class MainMenuAnimation // The background might be drawn at the same time as the loading of a map on a different thread calls Verse.Rand // Verse.Rand can't be used here because it isn't thread-safe - private static Random rand = new(); + private static Random rand = new(SystemInfo.deviceUniqueIdentifier.GetHashCode()); private static float newPulseTimer; diff --git a/Source/Client/UI/MainMenuPatches.cs b/Source/Client/UI/MainMenuPatches.cs index a3d08bb7..d2765d61 100644 --- a/Source/Client/UI/MainMenuPatches.cs +++ b/Source/Client/UI/MainMenuPatches.cs @@ -27,7 +27,6 @@ public static class MainMenu_AddHeight static void Prefix(ref Rect rect) => rect.height += 45f; } - [HarmonyPatch(typeof(OptionListingUtility), nameof(OptionListingUtility.DrawOptionListing))] public static class MainMenuPatch { @@ -61,7 +60,7 @@ static void Prefix(Rect rect, List optList) if (MpVersion.IsDebug && Multiplayer.IsReplay) optList.Insert(0, new ListableOption( "MpHostServer".Translate(), - () => Find.WindowStack.Add(new HostWindow(withSimulation: true) { layer = WindowLayer.Super }) + () => Find.WindowStack.Add(new HostWindow(hadSimulation: true) { layer = WindowLayer.Super }) )); if (Multiplayer.Client != null) diff --git a/Source/Client/UI/PlayerCursors.cs b/Source/Client/UI/PlayerCursors.cs index 50ece44c..49fd2081 100644 --- a/Source/Client/UI/PlayerCursors.cs +++ b/Source/Client/UI/PlayerCursors.cs @@ -21,6 +21,7 @@ static void Postfix() { if (player.username == Multiplayer.username) continue; if (player.map != curMap) continue; + if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; if (Multiplayer.settings.transparentPlayerCursors) GUI.color = player.color * new Color(1, 1, 1, 0.5f); @@ -77,6 +78,8 @@ static void Postfix() foreach (var player in Multiplayer.session.players) { + if (player.factionId != Multiplayer.RealPlayerFaction.loadID) continue; + foreach (var sel in player.selectedThings) { if (!drawnThisUpdate.Add(sel.Key)) continue; diff --git a/Source/Client/UI/ReplayTimeline.cs b/Source/Client/UI/ReplayTimeline.cs new file mode 100644 index 00000000..1b9ad42d --- /dev/null +++ b/Source/Client/UI/ReplayTimeline.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using Multiplayer.Client.Saving; +using Multiplayer.Client.Util; +using Multiplayer.Common; +using RimWorld; +using RimWorld.Planet; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public static class ReplayTimeline +{ + const float TimelineMargin = 50f; + const float TimelineHeight = 35f; + const int TimelineWindowId = 5723681; + + internal static void DrawTimeline() + { + Rect rect = new Rect(TimelineMargin, UI.screenHeight - 35f - TimelineHeight - 10f - 30f, + UI.screenWidth - TimelineMargin * 2, TimelineHeight + 30f); + + Find.WindowStack.ImmediateWindow(TimelineWindowId, rect, WindowLayer.SubSuper, DrawTimelineWindow, + doBackground: false, shadowAlpha: 0); + } + + private static void DrawTimelineWindow() + { + Rect rect = new Rect(0, 30f, UI.screenWidth - TimelineMargin * 2, TimelineHeight); + + Widgets.DrawBoxSolid(rect, new Color(0.6f, 0.6f, 0.6f, 0.8f)); + + int timerStart = Multiplayer.session.replayTimerStart >= 0 + ? Multiplayer.session.replayTimerStart + : Multiplayer.session.dataSnapshot.CachedAtTime; + + int timerEnd = Multiplayer.session.replayTimerEnd >= 0 + ? Multiplayer.session.replayTimerEnd + : TickPatch.tickUntil; + + int timeLen = timerEnd - timerStart; + + MpUI.DrawRotatedLine(new Vector2(rect.xMin + 2f, rect.center.y), TimelineHeight, 20f, 90f, Color.white); + MpUI.DrawRotatedLine(new Vector2(rect.xMax - 2f, rect.center.y), TimelineHeight, 20f, 90f, Color.white); + + float progress = (TickPatch.Timer - timerStart) / (float)timeLen; + float progressX = rect.xMin + progress * rect.width; + MpUI.DrawRotatedLine(new Vector2((int)progressX, rect.center.y), TimelineHeight, 20f, 90f, Color.green); + + float mouseX = Event.current.mousePosition.x; + ReplayEvent mouseEvent = null; + + foreach (var ev in Multiplayer.session.events) + { + if (ev.time < timerStart || ev.time > timerEnd) + continue; + + var pointX = rect.xMin + (ev.time - timerStart) / (float)timeLen * rect.width; + + //GUI.DrawTexture(new Rect(pointX - 12f, rect.yMin - 24f, 24f, 24f), texture); + MpUI.DrawRotatedLine(new Vector2(pointX, rect.center.y), TimelineHeight, 20f, 90f, /*ev.color*/ Color.red); + + if (Mouse.IsOver(rect) && Math.Abs(mouseX - pointX) < 10) + { + mouseX = pointX; + mouseEvent = ev; + } + } + + // Draw mouse pointer and tooltip + if (Mouse.IsOver(rect)) + { + float mouseProgress = (mouseX - rect.xMin) / rect.width; + int mouseTimer = timerStart + (int)(timeLen * mouseProgress); + + MpUI.DrawRotatedLine(new Vector2(mouseX, rect.center.y), TimelineHeight, 15f, 90f, Color.blue); + + if (Event.current.type == EventType.MouseUp) + SimulateToTick(mouseTimer); + + if (Event.current.isMouse) + Event.current.Use(); + + string tooltip = $"Tick {mouseTimer}"; + if (mouseEvent != null) + tooltip = $"{mouseEvent.name}\n{tooltip}"; + + const int tickTipId = 215462143; + + TooltipHandler.TipRegion(rect, new TipSignal(tooltip, tickTipId)); + + // Remove delay between the mouseover and showing + if (TooltipHandler.activeTips.TryGetValue(tickTipId, out ActiveTip tip)) + tip.firstTriggerTime = 0; + } + + // Draw simulation target when simulating + if (TickPatch.Simulating) + { + float pct = (TickPatch.simulating.target.Value - timerStart) / (float)timeLen; + float simulateToX = rect.xMin + rect.width * pct; + MpUI.DrawRotatedLine(new Vector2(simulateToX, rect.center.y), TimelineHeight, 15f, 90f, Color.yellow); + } + } + + private static void SimulateToTick(int targetTick) + { + TickPatch.SetSimulation(targetTick, canESC: true); + + if (targetTick < TickPatch.Timer) + { + Loader.ReloadGame( + Multiplayer.session.dataSnapshot.MapData.Keys.ToList(), + false, + Multiplayer.GameComp.asyncTime + ); + } + } +} + +[HarmonyPatch] +static class MakeSpaceForReplayTimeline +{ + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(MouseoverReadout), nameof(MouseoverReadout.MouseoverReadoutOnGUI)); + yield return AccessTools.Method(typeof(GlobalControls), nameof(GlobalControls.GlobalControlsOnGUI)); + yield return AccessTools.Method(typeof(WorldGlobalControls), nameof(WorldGlobalControls.WorldGlobalControlsOnGUI)); + } + + static void Prefix() + { + if (Multiplayer.IsReplay) + UI.screenHeight -= 60; + } + + static void Postfix() + { + if (Multiplayer.IsReplay) + UI.screenHeight += 60; + } +} diff --git a/Source/Client/Util/ClientTask.cs b/Source/Client/Util/ClientTask.cs new file mode 100644 index 00000000..16ee4b37 --- /dev/null +++ b/Source/Client/Util/ClientTask.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.CompilerServices; +using Verse; + +namespace Multiplayer.Client.Util; + +[AsyncMethodBuilder(typeof(ClientTaskMethodBuilder))] +public struct ClientTask : INotifyCompletion +{ + public bool IsCompleted => false; + public void GetResult(){} + public void OnCompleted(Action continuation){} + public ClientTask GetAwaiter() => this; +} + +public struct ClientTaskMethodBuilder +{ + public static ClientTaskMethodBuilder Create() => new(); + + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine => + stateMachine.MoveNext(); + + public void SetStateMachine(IAsyncStateMachine stateMachine) { } + + public void SetException(Exception exception) + { + Log.Error($"Multiplayer ClientTask exception: {exception}"); + } + + public void SetResult(){} + + public void AwaitOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(stateMachine.MoveNext); + } + + public void AwaitUnsafeOnCompleted( + ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.UnsafeOnCompleted(stateMachine.MoveNext); + } + + public ClientTask Task { get; } +} diff --git a/Source/Client/Util/CompilerTypes.cs b/Source/Client/Util/CompilerTypes.cs index 181be853..3ff82c52 100644 --- a/Source/Client/Util/CompilerTypes.cs +++ b/Source/Client/Util/CompilerTypes.cs @@ -1,3 +1,4 @@ +// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices { // Added to support compiling with C# 9 diff --git a/Source/Client/Util/Extensions.cs b/Source/Client/Util/Extensions.cs index 45658473..e563a73a 100644 --- a/Source/Client/Util/Extensions.cs +++ b/Source/Client/Util/Extensions.cs @@ -1,5 +1,3 @@ -extern alias zip; - using HarmonyLib; using Ionic.Crc; using Multiplayer.Common; @@ -17,7 +15,6 @@ using System.Xml; using UnityEngine; using Verse; -using zip::Ionic.Zip; using Random = System.Random; namespace Multiplayer.Client @@ -26,16 +23,6 @@ public static class Extensions { private static Regex methodNameCleaner = new Regex(@"(\?[0-9\-]+)"); - public static IEnumerable AllSubtypesAndSelf(this Type t) - { - return t.AllSubclasses().Concat(t); - } - - public static IEnumerable AllImplementing(this Type type) - { - return Multiplayer.implementations.GetValueSafe(type) is { } list ? list : Array.Empty(); - } - // Sets the current Faction.OfPlayer // Applies faction's world components // Applies faction's map components if map not null @@ -174,18 +161,6 @@ public static int Hash(this StackTrace trace) return traceToHash.ToString().GetHashCode(); } - public static byte[] GetBytes(this ZipEntry entry) - { - MemoryStream stream = new MemoryStream(); - entry.Extract(stream); - return stream.ToArray(); - } - - public static string GetString(this ZipEntry entry) - { - return Encoding.UTF8.GetString(entry.GetBytes()); - } - public static bool IsCompilerGenerated(this Type type) { while (type != null) diff --git a/Source/Client/Util/LongEventTask.cs b/Source/Client/Util/LongEventTask.cs new file mode 100644 index 00000000..08aef704 --- /dev/null +++ b/Source/Client/Util/LongEventTask.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Verse; + +namespace Multiplayer.Client.Util; + +public static class LongEventTask +{ + public static Task ContinueInLongEvent(string textKey, bool async) + { + TaskCompletionSource source = new(); + LongEventHandler.QueueLongEvent( + () => source.SetResult(null), + textKey, + async, + null + ); + return source.Task; + } +} diff --git a/Source/Client/Util/MpLayout.cs b/Source/Client/Util/MpLayout.cs new file mode 100644 index 00000000..03891cd7 --- /dev/null +++ b/Source/Client/Util/MpLayout.cs @@ -0,0 +1,36 @@ +using UnityEngine; +using Verse; + +namespace Multiplayer.Client.Util; + +public static class MpLayout +{ + public static void Label(string text) + { + GUILayout.Label(text, Text.CurFontStyle); + } + + public static bool Button(string text, float width, float height = 35f) + { + return Widgets.ButtonText( + GUILayoutUtility.GetRect( + new GUIContent(text), + Text.CurFontStyle, + GUILayout.Height(height), + GUILayout.Width(width)), + text + ); + } + + public static void BeginHorizCenter() + { + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + } + + public static void EndHorizCenter() + { + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + } +} diff --git a/Source/Client/Util/MpUtil.cs b/Source/Client/Util/MpUtil.cs index c260b9b3..a65d2085 100644 --- a/Source/Client/Util/MpUtil.cs +++ b/Source/Client/Util/MpUtil.cs @@ -240,14 +240,4 @@ IEnumerator IEnumerable.GetEnumerator() return dict.Keys.GetEnumerator(); } } - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] - public class HotSwappableAttribute : Attribute - { - public HotSwappableAttribute() - { - - } - } - } diff --git a/Source/Client/Util/TypeCache.cs b/Source/Client/Util/TypeCache.cs new file mode 100644 index 00000000..158442a8 --- /dev/null +++ b/Source/Client/Util/TypeCache.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client.Util; + +public static class TypeCache +{ + internal static Dictionary> subClasses = new(); + internal static Dictionary> subClassesNonAbstract = new(); + internal static Dictionary> implementations = new(); + + internal static void CacheTypeHierarchy() + { + foreach (var type in GenTypes.AllTypes) + { + for (var baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) + { + subClasses.GetOrAddNew(baseType).Add(type); + if (!type.IsAbstract) + subClassesNonAbstract.GetOrAddNew(baseType).Add(type); + } + + foreach (var i in type.GetInterfaces()) + implementations.GetOrAddNew(i).Add(type); + } + } + + internal static Dictionary typeByName = new(); + internal static Dictionary typeByFullName = new(); + + internal static void CacheTypeByName() + { + foreach (var type in GenTypes.AllTypes) + { + if (!typeByName.ContainsKey(type.Name)) + typeByName[type.Name] = type; + + if (!typeByFullName.ContainsKey(type.Name)) + typeByFullName[type.FullName] = type; + } + } + + public static IEnumerable AllSubtypesAndSelf(this Type t) + { + return t.AllSubclasses().Concat(t); + } + + public static IEnumerable AllImplementing(this Type type) + { + return implementations.GetValueSafe(type) ?? Enumerable.Empty(); + } + +} diff --git a/Source/Client/Windows/ChatWindow.cs b/Source/Client/Windows/ChatWindow.cs index 92105ebb..c8ed0802 100644 --- a/Source/Client/Windows/ChatWindow.cs +++ b/Source/Client/Windows/ChatWindow.cs @@ -126,6 +126,7 @@ private void DrawInfo(Rect inRect) string toolTip = $"{p.username}\n\nPing: {p.latency}ms\n{p.ticksBehind} ticks behind"; if (p.simulating) toolTip += "\n(Simulating)"; + toolTip += $"\nAvg frame time: {p.frameTime:0.00}ms"; TooltipHandler.TipRegion(rect, new TipSignal(toolTip, p.id)); }, diff --git a/Source/Client/Windows/ConnectingWindow.cs b/Source/Client/Windows/ConnectingWindow.cs index a2890b07..2fd7d404 100644 --- a/Source/Client/Windows/ConnectingWindow.cs +++ b/Source/Client/Windows/ConnectingWindow.cs @@ -42,9 +42,9 @@ public override void DoWindowContents(Rect inRect) { string label; - if (Multiplayer.Client?.StateObj is ClientJoiningState { subState: JoiningState.Waiting }) + if (Multiplayer.Client?.StateObj is ClientLoadingState { subState: LoadingState.Waiting }) label = "MpWaitingForGameData".Translate() + MpUI.FixedEllipsis(); - else if (Multiplayer.Client?.StateObj is ClientJoiningState { subState: JoiningState.Downloading }) + else if (Multiplayer.Client?.StateObj is ClientLoadingState { subState: LoadingState.Downloading }) label = "MpDownloading".Translate(Multiplayer.Client.FragmentProgress); else label = IsConnecting ? (ConnectingString + MpUI.FixedEllipsis()) : result; diff --git a/Source/Client/Windows/DesyncedWindow.cs b/Source/Client/Windows/DesyncedWindow.cs index 6443d968..e5b77428 100644 --- a/Source/Client/Windows/DesyncedWindow.cs +++ b/Source/Client/Windows/DesyncedWindow.cs @@ -1,6 +1,7 @@ using Multiplayer.Common; using Multiplayer.Client.Desyncs; using Multiplayer.Client.Util; +using Multiplayer.Common.Util; using UnityEngine; using Verse; @@ -54,8 +55,7 @@ public override void DoWindowContents(Rect inRect) if (Widgets.ButtonText(new Rect(x, 0, 120, 35), "MpTryResync".Translate()) && !rejoining) { Log.Message("Multiplayer: requesting rejoin"); - Multiplayer.Client.Send(Packets.Client_RequestRejoin); - rejoining = true; + MultiplayerSession.DoRejoin(); } x += 120 + 10; diff --git a/Source/Client/Windows/HostWindow.cs b/Source/Client/Windows/HostWindow.cs index e57338e2..f0c67168 100644 --- a/Source/Client/Windows/HostWindow.cs +++ b/Source/Client/Windows/HostWindow.cs @@ -13,7 +13,6 @@ namespace Multiplayer.Client { - [StaticConstructorOnStartup] public class HostWindow : Window { @@ -26,7 +25,7 @@ enum Tab private SaveFile file; public bool returnToServerBrowser; - private bool withSimulation; + private bool hadSimulation; private bool asyncTime; private bool asyncTimeLocked; private Tab tab; @@ -35,14 +34,14 @@ enum Tab private ServerSettings serverSettings; - public HostWindow(SaveFile file = null, bool withSimulation = false) + public HostWindow(SaveFile file = null, bool hadSimulation = false) { closeOnAccept = false; doCloseX = true; - serverSettings = Multiplayer.settings.serverSettings; + serverSettings = Multiplayer.settings.ServerSettings; - this.withSimulation = withSimulation; + this.hadSimulation = hadSimulation; this.file = file; serverSettings.gameName = file?.gameName ?? Multiplayer.session?.gameName ?? $"{Multiplayer.username}'s game"; @@ -416,11 +415,11 @@ private void TryHost() return; if (file?.replay ?? Multiplayer.IsReplay) - HostFromMultiplayerSave(settings); + HostFromReplay(settings); else if (file == null) - HostUtil.HostServer(settings, false, false, asyncTime); + HostFromSpIngame(settings); else - HostFromSingleplayer(settings); + HostFromSpSaveFile(settings); Close(); } @@ -459,7 +458,7 @@ static bool TryStartLocalServer(ServerSettings settings) failed = true; } - if (settings.lan && !localServer.liteNet.lanManager.IsRunning) + if (settings.lan && !localServer.liteNet.lanManager!.IsRunning) { Messages.Message("Failed to bind LAN on " + settings.lanAddress, MessageTypeDefOf.RejectInput, false); failed = true; @@ -475,13 +474,13 @@ static bool TryStartLocalServer(ServerSettings settings) public override void PostClose() { - Multiplayer.WriteSettingsToDisk(); + Multiplayer.settings.Write(); if (returnToServerBrowser) Find.WindowStack.Add(new ServerBrowser()); } - private void HostFromSingleplayer(ServerSettings settings) + private void HostFromSpSaveFile(ServerSettings settings) { LongEventHandler.QueueLongEvent(() => { @@ -501,9 +500,14 @@ private void HostFromSingleplayer(ServerSettings settings) }, "Play", "LoadingLongEvent", true, null); } - private void HostFromMultiplayerSave(ServerSettings settings) + private void HostFromSpIngame(ServerSettings settings) + { + HostUtil.HostServer(settings, false, false, asyncTime); + } + + private void HostFromReplay(ServerSettings settings) { - void ReplayLoaded() => HostUtil.HostServer(settings, true, withSimulation, asyncTime); + void ReplayLoaded() => HostUtil.HostServer(settings, true, hadSimulation, asyncTime); if (file != null) { diff --git a/Source/Client/Windows/JoinDataWindow.cs b/Source/Client/Windows/JoinDataWindow.cs index 6b9196b4..70b1f40f 100644 --- a/Source/Client/Windows/JoinDataWindow.cs +++ b/Source/Client/Windows/JoinDataWindow.cs @@ -763,8 +763,8 @@ private void DoRestart() : $"{data.remoteAddress}:{data.remotePort}"; // The env variables will get inherited by the child process started in GenCommandLine.Restart - Environment.SetEnvironmentVariable(Multiplayer.RestartConnectVariable, connectTo); - Environment.SetEnvironmentVariable(Multiplayer.RestartConfigsVariable, applyConfigs ? "true" : "false"); + Environment.SetEnvironmentVariable(EarlyInit.RestartConnectVariable, connectTo); + Environment.SetEnvironmentVariable(EarlyInit.RestartConfigsVariable, applyConfigs ? "true" : "false"); GenCommandLine.Restart(); } diff --git a/Source/Client/Windows/ModCompatWindow.cs b/Source/Client/Windows/ModCompatWindow.cs index 79e7ff9b..4dc0e628 100644 --- a/Source/Client/Windows/ModCompatWindow.cs +++ b/Source/Client/Windows/ModCompatWindow.cs @@ -323,7 +323,7 @@ public override void WindowUpdate() public override void PostClose() { base.PostClose(); - Multiplayer.WriteSettingsToDisk(); + Multiplayer.settings.Write(); } private void RecacheMods() diff --git a/Source/Client/Windows/SaveFileReader.cs b/Source/Client/Windows/SaveFileReader.cs index c00c7a27..e9f066ae 100644 --- a/Source/Client/Windows/SaveFileReader.cs +++ b/Source/Client/Windows/SaveFileReader.cs @@ -43,27 +43,26 @@ public void WaitTasks() private void ReadSpSave(FileInfo file) { - var saveFile = new SaveFile(Path.GetFileNameWithoutExtension(file.Name), false, file); - try { + var saveFile = new SaveFile(Path.GetFileNameWithoutExtension(file.Name), false, file); using var stream = file.OpenRead(); ReadSaveInfo(stream, saveFile); data[file] = saveFile; } catch (Exception ex) { - Log.Warning("Exception loading save info of " + file.Name + ": " + ex.ToString()); + Log.Warning($"Exception loading save info of {file.Name}: {ex}"); } } private void ReadMpSave(FileInfo file) { - var displayName = Path.ChangeExtension(file.FullName.Substring(Multiplayer.ReplaysDir.Length + 1), null); - var saveFile = new SaveFile(displayName, true, file); - try { + var displayName = Path.ChangeExtension(file.FullName.Substring(Multiplayer.ReplaysDir.Length + 1), null); + var saveFile = new SaveFile(displayName, true, file); + var replay = Replay.ForLoading(file); if (!replay.LoadInfo()) return; @@ -80,8 +79,8 @@ private void ReadMpSave(FileInfo file) } else { - using var zip = replay.ZipFile; - var stream = zip["world/000_save"].OpenReader(); + using var zip = replay.OpenZipRead(); + using var stream = zip.GetEntry("world/000_save")!.Open(); ReadSaveInfo(stream, saveFile); } @@ -89,7 +88,7 @@ private void ReadMpSave(FileInfo file) } catch (Exception ex) { - Log.Warning("Exception loading replay info of " + file.Name + ": " + ex.ToString()); + Log.Warning($"Exception loading replay info of {file.Name}: {ex}"); } } diff --git a/Source/Client/Windows/SaveGameWindow.cs b/Source/Client/Windows/SaveGameWindow.cs index 99de20c6..38458c1a 100644 --- a/Source/Client/Windows/SaveGameWindow.cs +++ b/Source/Client/Windows/SaveGameWindow.cs @@ -1,60 +1,80 @@ -using System; -using System.IO; +using System.IO; +using Multiplayer.Client.Util; +using Multiplayer.Common.Util; using UnityEngine; using Verse; namespace Multiplayer.Client; - -public class SaveGameWindow : AbstractTextInputWindow +[HotSwappable] +public class SaveGameWindow : Window { + public override Vector2 InitialSize => new(350f, 175f); + + private string curText; private bool fileExists; public SaveGameWindow(string gameName) { - title = "MpSaveGameAs".Translate(); + closeOnClickedOutside = true; + doCloseX = true; + absorbInputAroundWindow = true; + closeOnAccept = true; curText = GenFile.SanitizedFileName(gameName); } - public override bool Accept() + public override void DoWindowContents(Rect inRect) { - if (curText.Length == 0) return false; + GUILayout.BeginArea(inRect.AtZero()); + GUILayout.BeginVertical(); - try - { - LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile(curText), "MpSaving", false, null); - Close(); - } - catch (Exception e) + MpLayout.Label("MpSaveGameAs".Translate()); + + UpdateText(ref curText, GUILayout.TextField(curText)); + + using (MpStyle.Set(GameFont.Tiny)) + MpLayout.Label(fileExists ? "MpWillOverwrite".Translate() : ""); + + MpLayout.BeginHorizCenter(); { - Log.Error($"Exception saving replay {e}"); + if (MpLayout.Button("OK".Translate(), 120f)) + Accept(false); + + if (Prefs.DevMode && MpLayout.Button("Dev: save replay", 120f)) + Accept(true); } + MpLayout.EndHorizCenter(); + + GUILayout.EndVertical(); + GUILayout.EndArea(); + } - return true; + public override void OnAcceptKeyPressed() + { + Accept(false); } - public override bool Validate(string str) + private void UpdateText(ref string value, string newValue) { - if (str.Length == 0) - return true; + if (newValue == value) + return; - if (str.Length > 30) - return false; + if (newValue.Length > 30) + return; - if (GenFile.SanitizedFileName(str) != str) - return false; + if (!newValue.NullOrEmpty() && GenFile.SanitizedFileName(newValue) != newValue) + return; - fileExists = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{str}.zip")).Exists; - return true; + fileExists = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{newValue}.zip")).Exists; + value = newValue; } - public override void DrawExtra(Rect inRect) + private void Accept(bool currentReplay) { - if (fileExists) + if (curText.Length != 0) { - Text.Font = GameFont.Tiny; - Widgets.Label(new Rect(0, 25 + 15 + 35, inRect.width, 35f), "MpWillOverwrite".Translate()); - Text.Font = GameFont.Small; + LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile(curText, currentReplay), "MpSaving", false, null); + Close(); } } } diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index e04fb998..f67a36f8 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -1,5 +1,3 @@ -extern alias zip; - using LiteNetLib; using Multiplayer.Common; using RimWorld; @@ -63,8 +61,6 @@ enum Tabs Lan, Direct, Steam, Host } - private WidgetRow widgetRow = new WidgetRow(); - public override void DoWindowContents(Rect inRect) { DrawInfoButtons(); @@ -102,8 +98,8 @@ private void DrawInfoButtons() { float x = 0; - const string WebsiteLink = "https://rimworldmultiplayer.com"; - const string DiscordLink = "https://discord.gg/n5E2cb2Y4Z"; + const string websiteLink = "https://rimworldmultiplayer.com"; + const string discordLink = "https://discord.gg/n5E2cb2Y4Z"; bool Button(Texture2D icon, string labelKey, string tip, Color baseIconColor, float iconSize = 24f) { @@ -139,11 +135,11 @@ bool Button(Texture2D icon, string labelKey, string tip, Color baseIconColor, fl if (Button(TexButton.ToggleLog, compatLabel, MpUtil.TranslateWithDoubleNewLines(compatLabelDesc, 2), Color.grey, 20)) Find.WindowStack.Add(new ModCompatWindow(null, false, false, null)); - if (Button(MultiplayerStatic.WebsiteIcon, "MpWebsiteButton", "MpLinkButtonDesc".Translate() + " " + WebsiteLink, Color.grey, 20)) - Application.OpenURL(WebsiteLink); + if (Button(MultiplayerStatic.WebsiteIcon, "MpWebsiteButton", "MpLinkButtonDesc".Translate() + " " + websiteLink, Color.grey, 20)) + Application.OpenURL(websiteLink); - if (Button(MultiplayerStatic.DiscordIcon, "MpDiscordButton", "MpLinkButtonDesc".Translate() + " " + DiscordLink, Color.white)) - Application.OpenURL(DiscordLink); + if (Button(MultiplayerStatic.DiscordIcon, "MpDiscordButton", "MpLinkButtonDesc".Translate() + " " + discordLink, Color.white)) + Application.OpenURL(discordLink); if (false) // todo Button( @@ -161,7 +157,7 @@ bool Button(Texture2D icon, string labelKey, string tip, Color baseIconColor, fl private void ReloadFiles() { selectedFile = null; - reader?.WaitTasks(); + reader?.WaitTasks(); // Wait for the existing reader to finish reader = new SaveFileReader(); reader.StartReading(); @@ -462,7 +458,7 @@ private void DrawSteam(Rect inRect) float height = friends.Count * 40; Rect viewRect = new Rect(0, 0, outRect.width - 16f, height); - Widgets.BeginScrollView(outRect, ref steamScroll, viewRect, true); + Widgets.BeginScrollView(outRect, ref steamScroll, viewRect); float y = 0; int i = 0; @@ -617,7 +613,7 @@ private void UpdateLan() } } - private long lastFriendUpdate = 0; + private long lastFriendUpdate; private void UpdateSteam() { @@ -652,7 +648,7 @@ private void UpdateSteam() id = friend, avatar = avatar, username = username, - playingRimworld = playingRimworld, + playingRimworld = true, serverHost = serverHost, }); } @@ -669,12 +665,12 @@ public override void PostClose() public void Cleanup(bool sync) { - WaitCallback stop = s => lanListener.Stop(); + void Stop(object s) => lanListener.Stop(); if (sync) - stop(null); + Stop(null); else - ThreadPool.QueueUserWorkItem(stop); + ThreadPool.QueueUserWorkItem(Stop); } private void AddOrUpdate(IPEndPoint endpoint) diff --git a/Source/Common/ByteReader.cs b/Source/Common/ByteReader.cs index a7b8e58f..cf66d535 100644 --- a/Source/Common/ByteReader.cs +++ b/Source/Common/ByteReader.cs @@ -9,7 +9,7 @@ public class ByteReader private readonly byte[] array; private int index; - public object context; + public object? context; public int Length => array.Length; public int Position => index; @@ -44,7 +44,7 @@ public ByteReader(byte[] array) public virtual bool ReadBool() => BitConverter.ToBoolean(array, IncrementIndex(1)); - public virtual string ReadString(int maxLen = DefaultMaxStringLen) + public virtual string? ReadStringNullable(int maxLen = DefaultMaxStringLen) { int bytes = ReadInt32(); if (bytes == -1) return null; @@ -60,12 +60,27 @@ public virtual string ReadString(int maxLen = DefaultMaxStringLen) return result; } + public virtual string ReadString(int maxLen = DefaultMaxStringLen) + { + int bytes = ReadInt32(); + + if (bytes < 0) + throw new ReaderException($"String byte length ({bytes}<0)"); + if (bytes > maxLen) + throw new ReaderException($"String too long ({bytes}>{maxLen})"); + + string result = Encoding.UTF8.GetString(array, index, bytes); + index += bytes; + + return result; + } + public virtual byte[] ReadRaw(int len) { return array.SubArray(IncrementIndex(len), len); } - public virtual byte[] ReadPrefixedBytes(int maxLen = int.MaxValue) + public virtual byte[]? ReadPrefixedBytes(int maxLen = int.MaxValue) { int len = ReadInt32(); if (len == -1) return null; @@ -113,10 +128,10 @@ public virtual ulong[] ReadPrefixedULongs() return result; } - public virtual string[] ReadPrefixedStrings() + public virtual string?[] ReadPrefixedStrings() { int len = ReadInt32(); - string[] result = new string[len]; + string?[] result = new string[len]; for (int i = 0; i < len; i++) result[i] = ReadString(); return result; diff --git a/Source/Common/ByteWriter.cs b/Source/Common/ByteWriter.cs index 1e929fb4..ad4af93b 100644 --- a/Source/Common/ByteWriter.cs +++ b/Source/Common/ByteWriter.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; using System.Text; namespace Multiplayer.Common @@ -9,7 +10,7 @@ namespace Multiplayer.Common public class ByteWriter { private MemoryStream stream; - public object context; + public object? context; public int Position => (int)stream.Position; @@ -40,7 +41,7 @@ public ByteWriter(int capacity = 0) public virtual void WriteBool(bool val) => stream.WriteByte(val ? (byte)1 : (byte)0); - public virtual void WritePrefixedBytes(byte[] bytes) + public virtual void WritePrefixedBytes(byte[]? bytes) { if (bytes == null) { @@ -76,7 +77,7 @@ public virtual void WriteFrom(byte[] buffer, int offset, int length) stream.Write(buffer, offset, length); } - public virtual ByteWriter WriteString(string s) + public virtual ByteWriter WriteString(string? s) { WritePrefixedBytes(s == null ? null : Encoding.UTF8.GetBytes(s)); return this; @@ -144,6 +145,11 @@ private void Write(object obj) foreach (object o in list) Write(o); } + else if (obj is ITuple tuple) + { + for (int i = 0; i < tuple.Length; i++) + Write(tuple[i]); + } else { ServerLog.Error($"MP ByteWriter.Write: Unknown type {obj.GetType()}"); @@ -166,7 +172,7 @@ public static byte[] GetBytes(params object[] data) return writer.ToArray(); } - internal void SetLength(long value) + public void SetLength(long value) { stream.SetLength(value); } diff --git a/Source/Common/ChatCommands.cs b/Source/Common/ChatCommands.cs index acc7fd72..bb14dae4 100644 --- a/Source/Common/ChatCommands.cs +++ b/Source/Common/ChatCommands.cs @@ -1,4 +1,6 @@ -namespace Multiplayer.Common +using System.Diagnostics; + +namespace Multiplayer.Common { public abstract class ChatCmdHandler { @@ -6,7 +8,7 @@ public abstract class ChatCmdHandler public MultiplayerServer Server => MultiplayerServer.instance; - public abstract void Handle(ServerPlayer player, string[] args); + public abstract void Handle(IChatSource source, string[] args); public void SendNoPermission(ServerPlayer player) { @@ -26,10 +28,10 @@ public ChatCmdJoinPoint() requiresHost = true; } - public override void Handle(ServerPlayer player, string[] args) + public override void Handle(IChatSource source, string[] args) { - if (!Server.TryStartJoinPointCreation(true)) - player.SendChat("Join point creation already in progress."); + if (!Server.worldData.TryStartJoinPointCreation(true)) + source.SendMsg("Join point creation already in progress."); } } @@ -40,28 +42,41 @@ public ChatCmdKick() requiresHost = true; } - public override void Handle(ServerPlayer player, string[] args) + public override void Handle(IChatSource source, string[] args) { if (args.Length < 1) { - player.SendChat("No username provided."); + source.SendMsg("No username provided."); return; } var toKick = FindPlayer(args[0]); if (toKick == null) { - player.SendChat("Couldn't find the player."); + source.SendMsg("Couldn't find the player."); return; } if (toKick.IsHost) { - player.SendChat("You can't kick the host."); + source.SendMsg("You can't kick the host."); return; } toKick.Disconnect(MpDisconnectReason.Kick); } } + + public class ChatCmdStop : ChatCmdHandler + { + public ChatCmdStop() + { + requiresHost = true; + } + + public override void Handle(IChatSource source, string[] args) + { + Server.running = false; + } + } } diff --git a/Source/Common/CommandHandler.cs b/Source/Common/CommandHandler.cs index ef545a40..022419d1 100644 --- a/Source/Common/CommandHandler.cs +++ b/Source/Common/CommandHandler.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; namespace Multiplayer.Common { public class CommandHandler { private MultiplayerServer server; - public HashSet debugOnlySyncCmds = new(); - public HashSet hostOnlySyncCmds = new(); public int SentCmds { get; private set; } @@ -16,7 +13,7 @@ public CommandHandler(MultiplayerServer server) this.server = server; } - public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerPlayer sourcePlayer = null, ServerPlayer fauxSource = null) + public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerPlayer? sourcePlayer = null, ServerPlayer? fauxSource = null) { if (server.freezeManager.Frozen) return; @@ -25,11 +22,11 @@ public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerP { bool debugCmd = cmd == CommandType.DebugTools || - cmd == CommandType.Sync && debugOnlySyncCmds.Contains(BitConverter.ToInt32(data, 0)); + cmd == CommandType.Sync && server.initData!.DebugOnlySyncCmds.Contains(BitConverter.ToInt32(data, 0)); if (debugCmd && !CanUseDevMode(sourcePlayer)) return; - bool hostOnly = cmd == CommandType.Sync && hostOnlySyncCmds.Contains(BitConverter.ToInt32(data, 0)); + bool hostOnly = cmd == CommandType.Sync && server.initData!.HostOnlySyncCmds.Contains(BitConverter.ToInt32(data, 0)); if (hostOnly && !sourcePlayer.IsHost) return; @@ -41,15 +38,15 @@ public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerP byte[] toSave = ScheduledCommand.Serialize( new ScheduledCommand( cmd, - server.gameTimer, + server.gameTimer + 1, factionId, mapId, sourcePlayer?.id ?? fauxSource?.id ?? ScheduledCommand.NoPlayer, data)); // todo cull target players if not global - server.mapCmds.GetOrAddNew(mapId).Add(toSave); - server.tmpMapCmds?.GetOrAddNew(mapId).Add(toSave); + server.worldData.mapCmds.GetOrAddNew(mapId).Add(toSave); + server.worldData.tmpMapCmds?.GetOrAddNew(mapId).Add(toSave); byte[] toSend = toSave.Append(new byte[] { 0 }); byte[] toSendSource = toSave.Append(new byte[] { 1 }); @@ -72,14 +69,14 @@ public void PauseAll() CommandType.TimeSpeedVote, ScheduledCommand.NoFaction, ScheduledCommand.Global, - ByteWriter.GetBytes(TimeVote.ResetAll, -1) + ByteWriter.GetBytes(TimeVote.ResetGlobal, -1) ); else Send( CommandType.PauseAll, ScheduledCommand.NoFaction, ScheduledCommand.Global, - null + Array.Empty() ); } diff --git a/Source/Common/Common.csproj b/Source/Common/Common.csproj new file mode 100644 index 00000000..f035ffb7 --- /dev/null +++ b/Source/Common/Common.csproj @@ -0,0 +1,28 @@ + + + + net472 + true + enable + 10 + false + false + Multiplayer.Common + MultiplayerCommon + + + + + + + + + + + + + + + + + diff --git a/Source/Common/FreezeManager.cs b/Source/Common/FreezeManager.cs index c983f776..58dc2e41 100644 --- a/Source/Common/FreezeManager.cs +++ b/Source/Common/FreezeManager.cs @@ -12,7 +12,7 @@ public bool Frozen private set { frozen = value; - Server.SendToAll(Packets.Server_Freeze, new object[] { frozen, Server.gameTimer }); + Server.SendToPlaying(Packets.Server_Freeze, new object[] { frozen, Server.gameTimer }); } } @@ -27,6 +27,9 @@ public FreezeManager(MultiplayerServer server) public void Tick() { + if (!Server.PlayingPlayers.Any(p => p.IsHost)) + return; + if (!Frozen && Server.HostPlayer.frozen) Frozen = true; diff --git a/Source/Common/IChatSource.cs b/Source/Common/IChatSource.cs new file mode 100644 index 00000000..83fb0b46 --- /dev/null +++ b/Source/Common/IChatSource.cs @@ -0,0 +1,6 @@ +namespace Multiplayer.Common; + +public interface IChatSource +{ + void SendMsg(string msg); +} diff --git a/Source/Common/LiteNetManager.cs b/Source/Common/LiteNetManager.cs index 441e6dcd..f0bfbb79 100644 --- a/Source/Common/LiteNetManager.cs +++ b/Source/Common/LiteNetManager.cs @@ -13,10 +13,10 @@ public class LiteNetManager private MultiplayerServer server; public List<(LiteNetEndpoint, NetManager)> netManagers = new(); - public NetManager lanManager; - private NetManager arbiter; + public NetManager? lanManager; + private NetManager? arbiter; - public int ArbiterPort => arbiter.LocalPort; + public int ArbiterPort => arbiter!.LocalPort; private int broadcastTimer; @@ -65,6 +65,7 @@ public void StartNet() foreach (var (endpoint, man) in netManagers) { + ServerLog.Detail($"Starting NetManager at {endpoint}"); man.Start(endpoint.ipv4 ?? IPAddress.Any, endpoint.ipv6 ?? IPAddress.IPv6Any, endpoint.port); } } @@ -120,8 +121,8 @@ public void OnServerStop() public class LiteNetEndpoint { - public IPAddress ipv4; - public IPAddress ipv6; + public IPAddress? ipv4; + public IPAddress? ipv6; public int port; public override string ToString() diff --git a/Source/Common/MpZipFile.cs b/Source/Common/MpZipFile.cs new file mode 100644 index 00000000..6777e37d --- /dev/null +++ b/Source/Common/MpZipFile.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace Multiplayer.Common; + +public static class MpZipFile +{ + public static ZipArchive Open(string path, ZipArchiveMode mode) + { + FileMode fileMode; + FileAccess access; + FileShare fileShare; + + switch (mode) + { + case ZipArchiveMode.Read: + fileMode = FileMode.Open; + access = FileAccess.Read; + fileShare = FileShare.Read; + break; + + case ZipArchiveMode.Create: + fileMode = FileMode.CreateNew; + access = FileAccess.Write; + fileShare = FileShare.None; + break; + + case ZipArchiveMode.Update: + fileMode = FileMode.OpenOrCreate; + access = FileAccess.ReadWrite; + fileShare = FileShare.None; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + var fs = new FileStream(path, fileMode, access, fileShare, bufferSize: 0x1000, useAsync: false); + + try + { + return new ZipArchive(fs, mode, leaveOpen: false); + } + catch + { + fs.Dispose(); + throw; + } + } +} diff --git a/Source/Common/MultiplayerServer.cs b/Source/Common/MultiplayerServer.cs index 5b3c6914..0c2af38e 100644 --- a/Source/Common/MultiplayerServer.cs +++ b/Source/Common/MultiplayerServer.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; namespace Multiplayer.Common { @@ -12,60 +14,54 @@ static MultiplayerServer() { MpConnectionState.SetImplementation(ConnectionStateEnum.ServerSteam, typeof(ServerSteamState)); MpConnectionState.SetImplementation(ConnectionStateEnum.ServerJoining, typeof(ServerJoiningState)); + MpConnectionState.SetImplementation(ConnectionStateEnum.ServerLoading, typeof(ServerLoadingState)); MpConnectionState.SetImplementation(ConnectionStateEnum.ServerPlaying, typeof(ServerPlayingState)); } - public static MultiplayerServer instance; + public static MultiplayerServer? instance; public const int DefaultPort = 30502; public const int MaxUsernameLength = 15; public const int MinUsernameLength = 3; public const char EndpointSeparator = '&'; - public int defaultFactionId; - public byte[] savedGame; // Compressed game save - public byte[] semiPersistent; // Compressed semi persistent data - public Dictionary mapData = new(); // Map id to compressed map data - - public Dictionary> mapCmds = new(); // Map id to serialized cmds list - public Dictionary> tmpMapCmds; - public int lastJoinPointAtWorkTicks = -1; - public List syncInfos = new(); - - public bool CreatingJoinPoint => tmpMapCmds != null; + public static readonly Regex UsernamePattern = new(@"^[a-zA-Z0-9_]+$"); + public WorldData worldData; public FreezeManager freezeManager; public CommandHandler commands; public PlayerManager playerManager; public LiteNetManager liteNet; + public IEnumerable JoinedPlayers => playerManager.JoinedPlayers; public IEnumerable PlayingPlayers => playerManager.PlayingPlayers; + public IEnumerable PlayingIngamePlayers => playerManager.PlayingPlayers.Where(p => p.status == PlayerStatus.Playing); - public string hostUsername; + public string? hostUsername; public int gameTimer; public int workTicks; public ActionQueue queue = new(); public ServerSettings settings; - public volatile bool running; + public ServerInitData? initData; + public TaskCompletionSource initDataSource = new(); + public InitDataState initDataState = InitDataState.Waiting; - private Dictionary chatCmds = new(); + private Dictionary chatCmdHandlers = new(); public int nextUniqueId; // currently unused - public string rwVersion; - public string mpVersion; - public Dictionary defInfos; - public byte[] serverData; - - public Thread serverThread; - public event Action TickEvent; + public volatile bool running; + public event Action? TickEvent; public bool ArbiterPlaying => PlayingPlayers.Any(p => p.IsArbiter && p.status == PlayerStatus.Playing); public ServerPlayer HostPlayer => PlayingPlayers.First(p => p.IsHost); - public bool FullyStarted => running && savedGame != null; + public bool FullyStarted => running && worldData.savedGame != null; + + public const float StandardTimePerTick = 1000.0f / 60.0f; - public float serverTimePerTick = 1000.0f / 60.0f; + public float serverTimePerTick = StandardTimePerTick; + public int sentCmdsSnapshot; public int NetTimer { get; private set; } @@ -73,6 +69,7 @@ public MultiplayerServer(ServerSettings settings) { this.settings = settings; + worldData = new WorldData(this); freezeManager = new FreezeManager(this); commands = new CommandHandler(this); playerManager = new PlayerManager(this); @@ -80,11 +77,17 @@ public MultiplayerServer(ServerSettings settings) RegisterChatCmd("joinpoint", new ChatCmdJoinPoint()); RegisterChatCmd("kick", new ChatCmdKick()); + RegisterChatCmd("stop", new ChatCmdStop()); + + initDataSource.SetResult(null); } public void Run() { + ServerLog.Detail("Server started"); + Stopwatch time = Stopwatch.StartNew(); + Stopwatch tickTime = Stopwatch.StartNew(); double realTime = 0; while (running) @@ -93,24 +96,48 @@ public void Run() { double elapsed = time.ElapsedMillisDouble(); time.Restart(); - realTime += elapsed; - if (realTime > 0) - { - queue.RunQueue(ServerLog.Error); - TickEvent?.Invoke(this); - liteNet.Tick(); - TickNet(); - freezeManager.Tick(); + tickTime.Restart(); - if (!freezeManager.Frozen) + freezeManager.Tick(); + queue.RunQueue(ServerLog.Error); + TickEvent?.Invoke(this); + liteNet.Tick(); + TickNet(); + + int ticked = 0; + while (realTime > 0 && ticked < 2) + { + if (!freezeManager.Frozen && + PlayingPlayers.Any(p => p.ExtrapolatedTicksBehind < 40) && + !PlayingIngamePlayers.Any(p => p.ExtrapolatedTicksBehind > 90)) + { gameTimer++; + sentCmdsSnapshot = commands.SentCmds; + } + + // Run up to three times slower depending on max ticksBehind + var slowdown = Math.Min( + PlayingIngamePlayers.MaxOrZero(p => p.ticksBehind) / 60f, + 2f + ); + realTime -= serverTimePerTick * (1f + slowdown); - realTime -= serverTimePerTick * 1.05f; + ticked++; } - Thread.Sleep(16); + if (realTime > 0) + realTime = 0f; + + if (MpVersion.IsDebug && tickTime.ElapsedMillisDouble() > 15f) + ServerLog.Log($"Server tick took {tickTime.ElapsedMillisDouble()}ms"); + + // On Windows, the clock ticks 64 times a second and sleep durations too close to a multiple of 15.625ms + // tend to be rounded up so we sleep for a bit less + int sleepFor = (int)Math.Floor((1000 / 30f - tickTime.ElapsedMillisDouble()) * 0.9f); + if (sleepFor > 0) + Thread.Sleep(sleepFor); } catch (Exception e) { @@ -132,84 +159,57 @@ private void TickNet() { NetTimer++; - if (NetTimer % 60 == 0) + if (NetTimer % 30 == 0) playerManager.SendLatencies(); - if (NetTimer % 30 == 0) - foreach (var player in PlayingPlayers) + if (NetTimer % 6 == 0) + foreach (var player in JoinedPlayers) player.SendPacket(Packets.Server_KeepAlive, ByteWriter.GetBytes(player.keepAliveId), false); - if (NetTimer % 2 == 0) - SendToAll(Packets.Server_TimeControl, ByteWriter.GetBytes(gameTimer, commands.SentCmds, serverTimePerTick), false); + SendToPlaying(Packets.Server_TimeControl, ByteWriter.GetBytes(gameTimer, sentCmdsSnapshot, serverTimePerTick), false); + + serverTimePerTick = PlayingIngamePlayers.MaxOrZero(p => p.frameTime); + + if (serverTimePerTick < StandardTimePerTick) + serverTimePerTick = StandardTimePerTick; - serverTimePerTick = Math.Max(1000.0f / 60.0f, PlayingPlayers.Max(p => p.frameTime)); + if (serverTimePerTick > StandardTimePerTick * 4f) + serverTimePerTick = StandardTimePerTick * 4f; } public void TryStop() { + ServerLog.Detail("Server shutting down..."); + playerManager.OnServerStop(); liteNet.OnServerStop(); instance = null; } - public bool TryStartJoinPointCreation(bool force = false) - { - if (!force && workTicks - lastJoinPointAtWorkTicks < 30) - return false; - - if (CreatingJoinPoint) - return false; - - SendChat("Creating a join point..."); - - commands.Send(CommandType.CreateJoinPoint, ScheduledCommand.NoFaction, ScheduledCommand.Global, new byte[0]); - tmpMapCmds = new Dictionary>(); - - return true; - } - - public void EndJoinPointCreation() - { - mapCmds = tmpMapCmds; - tmpMapCmds = null; - lastJoinPointAtWorkTicks = workTicks; - - foreach (var playerId in playerManager.playersWaitingForWorldData) - if (GetPlayer(playerId)?.conn.StateObj is ServerJoiningState state) - state.SendWorldData(); - - playerManager.playersWaitingForWorldData.Clear(); - } - public void Enqueue(Action action) { queue.Enqueue(action); } - public void SendToAll(Packets id) + public void SendToPlaying(Packets id, object[] data) { - SendToAll(id, new byte[0]); + SendToPlaying(id, ByteWriter.GetBytes(data)); } - public void SendToAll(Packets id, object[] data) - { - SendToAll(id, ByteWriter.GetBytes(data)); - } - - public void SendToAll(Packets id, byte[] data, bool reliable = true, ServerPlayer excluding = null) + public void SendToPlaying(Packets id, byte[] data, bool reliable = true, ServerPlayer? excluding = null) { foreach (ServerPlayer player in PlayingPlayers) if (player != excluding) player.conn.Send(id, data, reliable); } - public ServerPlayer GetPlayer(string username) + public ServerPlayer? GetPlayer(string username) { return playerManager.GetPlayer(username); } - public ServerPlayer GetPlayer(int id) + public ServerPlayer? GetPlayer(int id) { return playerManager.GetPlayer(id); } @@ -225,23 +225,61 @@ public IdBlock NextIdBlock(int blockSize = 30000) public void SendChat(string msg) { - SendToAll(Packets.Server_Chat, new[] { msg }); + ServerLog.Detail($"[Chat] {msg}"); + SendToPlaying(Packets.Server_Chat, new object[] { msg }); } public void SendNotification(string key, params string[] args) { - SendToAll(Packets.Server_Notification, new object[] { key, args }); + SendToPlaying(Packets.Server_Notification, new object[] { key, args }); } public void RegisterChatCmd(string cmdName, ChatCmdHandler handler) { - chatCmds[cmdName] = handler; + chatCmdHandlers[cmdName] = handler; } - public ChatCmdHandler GetChatCmdHandler(string cmdName) + public ChatCmdHandler? GetChatCmdHandler(string cmdName) { - chatCmds.TryGetValue(cmdName, out ChatCmdHandler handler); + chatCmdHandlers.TryGetValue(cmdName, out ChatCmdHandler handler); return handler; } + + public void HandleChatCmd(IChatSource source, string cmd) + { + var parts = cmd.Split(' '); + var handler = GetChatCmdHandler(parts[0]); + + if (handler != null) + { + if (handler.requiresHost && source is ServerPlayer { IsHost: false }) + source.SendMsg("No permission"); + else + handler.Handle(source, parts.SubArray(1)); + } + else + { + source.SendMsg("Invalid command"); + } + } + + public Task InitData() + { + return initDataSource.Task; + } + + public void CompleteInitData(ServerInitData data) + { + initData = data; + initDataState = InitDataState.Complete; + initDataSource.SetResult(data); + } + } + + public enum InitDataState + { + Waiting, + Requested, + Complete } } diff --git a/Source/Common/Networking/AsyncConnectionState.cs b/Source/Common/Networking/AsyncConnectionState.cs new file mode 100644 index 00000000..7a2d5278 --- /dev/null +++ b/Source/Common/Networking/AsyncConnectionState.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Multiplayer.Common.Util; + +namespace Multiplayer.Common; + +public abstract class AsyncConnectionState : MpConnectionState +{ + private PacketAwaitable? packetAwaitable; + + public Task? CurrentTask { get; private set; } + + public AsyncConnectionState(ConnectionBase connection) : base(connection) + { + } + + public override void StartState() + { + async Task RunStateCatch() + { + try + { + await RunState(); + } + catch (Exception e) + { + ServerLog.Error($"Exception in state {GetType().Name}: {e}"); + Player.Disconnect(MpDisconnectReason.StateException); + } + } + + CurrentTask = RunStateCatch(); + } + + protected abstract Task RunState(); + + public override void OnDisconnect() + { + if (packetAwaitable is { AnnouncePacketFailure: true }) + { + var source = packetAwaitable; + packetAwaitable = null; + source.SetResult(null); + } + } + + /// + /// Wait for a packet of the given type. The packet must arrive after the call to this method. + /// An exception is thrown if this is called again before the packet arrives. The player is disconnected if a + /// different packet type arrives. + /// + protected PacketAwaitable Packet(Packets packet) + { + if (packetAwaitable != null) + throw new Exception($"Already waiting for another packet: {packetAwaitable}"); + + ServerLog.Verbose($"{connection} waiting for {packet}"); + + packetAwaitable = new PacketAwaitable(packet, false); + return packetAwaitable!; + } + + /// + /// Wait for a packet of the given type. The packet must arrive after the call to this method. + /// An exception is thrown if this is called again before the packet arrives. The player is disconnected if a + /// different packet type arrives. + /// The result is null if the player is disconnected while waiting. + /// + protected PacketAwaitable PacketOrNull(Packets packet) + { + if (packetAwaitable != null) + throw new Exception($"Already waiting for another packet: {packetAwaitable}"); + + ServerLog.Verbose($"{connection} waiting for {packet}"); + + packetAwaitable = new PacketAwaitable(packet, true); + return packetAwaitable; + } + + public override PacketHandlerInfo? GetPacketHandler(Packets packet) + { + return packetAwaitable != null && packetAwaitable.PacketType == packet ? + new PacketHandlerInfo((_, args) => + { + var source = packetAwaitable; + packetAwaitable = null; + source.SetResult((ByteReader)args[0]); + return null; + }, packetAwaitable.Fragment) : + null; + } + + protected async Task EndIfDead() + { + if (!alive) + await new Blackhole(); + return true; + } +} + +public class PacketAwaitable : INotifyCompletion +{ + private List continuations = new(); + public Packets PacketType { get; } + public bool AnnouncePacketFailure { get; } + private T? result; + + public bool Fragment { get; private set; } + + public PacketAwaitable(Packets packetType, bool announcePacketFailure) + { + PacketType = packetType; + AnnouncePacketFailure = announcePacketFailure; + } + + public void OnCompleted(Action continuation) + { + continuations.Add(continuation); + } + + public bool IsCompleted => result != null; + + public T GetResult() + { + return result!; + } + + public PacketAwaitable GetAwaiter() => this; + + public void SetResult(T r) + { + result = r; + foreach (var continuation in continuations) + continuation(); + } + + public override string ToString() + { + return PacketType.ToString(); + } + + public PacketAwaitable Fragmented() + { + Fragment = true; + return this; + } +} diff --git a/Source/Common/Networking/Connection.cs b/Source/Common/Networking/ConnectionBase.cs similarity index 63% rename from Source/Common/Networking/Connection.cs rename to Source/Common/Networking/ConnectionBase.cs index 1c20a98f..4bf32832 100644 --- a/Source/Common/Networking/Connection.cs +++ b/Source/Common/Networking/ConnectionBase.cs @@ -4,34 +4,35 @@ namespace Multiplayer.Common { public abstract class ConnectionBase { - public string username; - public ServerPlayer serverPlayer; + public string? username; + public ServerPlayer? serverPlayer; public virtual int Latency { get; set; } - public ConnectionStateEnum State - { - get => state; + public ConnectionStateEnum State { get; private set; } + public MpConnectionState? StateObj { get; private set; } + public bool Lenient { get; set; } - set - { - state = value; + public T? GetState() where T : MpConnectionState => (T?)StateObj; - if (state == ConnectionStateEnum.Disconnected) - stateObj = null; - else - stateObj = (MpConnectionState)Activator.CreateInstance(MpConnectionState.connectionImpls[(int)value], this); - } - } + public void ChangeState(ConnectionStateEnum state) + { + if (StateObj != null) + StateObj.alive = false; + + State = state; - public MpConnectionState StateObj => stateObj; + if (State == ConnectionStateEnum.Disconnected) + StateObj = null; + else + StateObj = (MpConnectionState)Activator.CreateInstance(MpConnectionState.stateImpls[(int)state], this); - private ConnectionStateEnum state; - private MpConnectionState stateObj; + StateObj?.StartState(); + } public virtual void Send(Packets id) { - Send(id, new byte[0]); + Send(id, Array.Empty()); } public virtual void Send(Packets id, params object[] msg) @@ -41,7 +42,7 @@ public virtual void Send(Packets id, params object[] msg) public virtual void Send(Packets id, byte[] message, bool reliable = true) { - if (state == ConnectionStateEnum.Disconnected) + if (State == ConnectionStateEnum.Disconnected) return; if (message.Length > FragmentSize) @@ -58,27 +59,27 @@ public virtual void Send(Packets id, byte[] message, bool reliable = true) public const int FragmentSize = 65_536; public const int MaxPacketSize = 33_554_432; - private const int FRAG_NONE = 0x0; - private const int FRAG_MORE = 0x40; - private const int FRAG_END = 0x80; + private const int FragNone = 0x0; + private const int FragMore = 0x40; + private const int FragEnd = 0x80; // All fragmented packets need to be sent from the same thread public virtual void SendFragmented(Packets id, byte[] message) { - if (state == ConnectionStateEnum.Disconnected) + if (State == ConnectionStateEnum.Disconnected) return; int read = 0; while (read < message.Length) { int len = Math.Min(FragmentSize, message.Length - read); - int fragState = (read + len >= message.Length) ? FRAG_END : FRAG_MORE; - byte state = (byte)((Convert.ToByte(id) & 0x3F) | fragState); + int fragState = (read + len >= message.Length) ? FragEnd : FragMore; + byte headerByte = (byte)((Convert.ToByte(id) & 0x3F) | fragState); var writer = new ByteWriter(1 + 4 + len); // Write the packet id and fragment state: MORE or END - writer.WriteByte(state); + writer.WriteByte(headerByte); // Send the message length with the first packet if (read == 0) writer.WriteInt32(message.Length); @@ -91,6 +92,7 @@ public virtual void SendFragmented(Packets id, byte[] message) read += len; } } + public virtual void SendFragmented(Packets id, params object[] msg) { SendFragmented(id, ByteWriter.GetBytes(msg)); @@ -98,9 +100,9 @@ public virtual void SendFragmented(Packets id, params object[] msg) protected abstract void SendRaw(byte[] raw, bool reliable = true); - public virtual void HandleReceive(ByteReader data, bool reliable) + public virtual void HandleReceiveRaw(ByteReader data, bool reliable) { - if (state == ConnectionStateEnum.Disconnected) + if (State == ConnectionStateEnum.Disconnected) return; if (data.Left == 0) @@ -110,39 +112,40 @@ public virtual void HandleReceive(ByteReader data, bool reliable) byte msgId = (byte)(info & 0x3F); byte fragState = (byte)(info & 0xC0); - HandleReceive(msgId, fragState, data, reliable); + HandleReceiveMsg(msgId, fragState, data, reliable); } - private ByteWriter fragmented; + private ByteWriter? fragmented; private int fullSize; // For information, doesn't affect anything public int FragmentProgress => (fragmented?.Position * 100 / fullSize) ?? 0; - protected virtual void HandleReceive(int msgId, int fragState, ByteReader reader, bool reliable) + protected virtual void HandleReceiveMsg(int msgId, int fragState, ByteReader reader, bool reliable) { if (msgId is < 0 or >= (int)Packets.Count) throw new PacketReadException($"Bad packet id {msgId}"); Packets packetType = (Packets)msgId; + ServerLog.Verbose($"Received packet {this}: {packetType}"); - var handler = MpConnectionState.packetHandlers[(int)state, (int)packetType]; + var handler = StateObj?.GetPacketHandler(packetType) ?? MpConnectionState.packetHandlers[(int)State, (int)packetType]; if (handler == null) { - if (reliable) - throw new PacketReadException($"No handler for packet {packetType} in state {state}"); - else - return; + if (reliable && !Lenient) + throw new PacketReadException($"No handler for packet {packetType} in state {State}"); + + return; } - if (fragState != FRAG_NONE && fragmented == null) + if (fragState != FragNone && fragmented == null) fullSize = reader.ReadInt32(); if (reader.Left > FragmentSize) throw new PacketReadException($"Packet {packetType} too big {reader.Left}>{FragmentSize}"); - if (fragState == FRAG_NONE) + if (fragState == FragNone) { - handler.Method.Invoke(stateObj, reader); + handler.Method.Invoke(StateObj, reader); } else if (!handler.Fragment) { @@ -150,29 +153,27 @@ protected virtual void HandleReceive(int msgId, int fragState, ByteReader reader } else { - if (fragmented == null) - fragmented = new ByteWriter(reader.Left); - + fragmented ??= new ByteWriter(reader.Left); fragmented.WriteRaw(reader.ReadRaw(reader.Left)); if (fragmented.Position > MaxPacketSize) throw new PacketReadException($"Full packet {packetType} too big {fragmented.Position}>{MaxPacketSize}"); - if (fragState == FRAG_END) + if (fragState == FragEnd) { - handler.Method.Invoke(stateObj, new ByteReader(fragmented.ToArray())); + handler.Method.Invoke(StateObj, new ByteReader(fragmented.ToArray())); fragmented = null; } } } - public abstract void Close(MpDisconnectReason reason, byte[] data = null); + public abstract void Close(MpDisconnectReason reason, byte[]? data = null); - public static byte[] GetDisconnectBytes(MpDisconnectReason reason, byte[] data = null) + public static byte[] GetDisconnectBytes(MpDisconnectReason reason, byte[]? data = null) { var writer = new ByteWriter(); writer.WriteByte((byte)reason); - writer.WritePrefixedBytes(data ?? new byte[0]); + writer.WritePrefixedBytes(data ?? Array.Empty()); return writer.ToArray(); } } diff --git a/Source/Common/Networking/ConnectionStateEnum.cs b/Source/Common/Networking/ConnectionStateEnum.cs new file mode 100644 index 00000000..56492331 --- /dev/null +++ b/Source/Common/Networking/ConnectionStateEnum.cs @@ -0,0 +1,17 @@ +namespace Multiplayer.Common; + +public enum ConnectionStateEnum : byte +{ + ClientJoining, + ClientLoading, + ClientPlaying, + ClientSteam, + + ServerJoining, + ServerLoading, + ServerPlaying, + ServerSteam, // unused + + Count, + Disconnected +} diff --git a/Source/Common/Networking/LiteNetConnection.cs b/Source/Common/Networking/LiteNetConnection.cs index 1ea54354..0b58dc4b 100644 --- a/Source/Common/Networking/LiteNetConnection.cs +++ b/Source/Common/Networking/LiteNetConnection.cs @@ -16,7 +16,7 @@ protected override void SendRaw(byte[] raw, bool reliable) peer.Send(raw, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable); } - public override void Close(MpDisconnectReason reason, byte[] data) + public override void Close(MpDisconnectReason reason, byte[]? data) { peer.NetManager.TriggerUpdate(); // todo: is this needed? peer.NetManager.DisconnectPeer(peer, GetDisconnectBytes(reason, data)); diff --git a/Source/Common/Networking/MpConnectionState.cs b/Source/Common/Networking/MpConnectionState.cs index 9b2e55e6..51b69b0f 100644 --- a/Source/Common/Networking/MpConnectionState.cs +++ b/Source/Common/Networking/MpConnectionState.cs @@ -7,9 +7,10 @@ namespace Multiplayer.Common public abstract class MpConnectionState { public readonly ConnectionBase connection; + public bool alive = true; protected ServerPlayer Player => connection.serverPlayer; - protected MultiplayerServer Server => MultiplayerServer.instance; + protected MultiplayerServer Server => MultiplayerServer.instance!; public MpConnectionState(ConnectionBase connection) { @@ -20,14 +21,23 @@ public virtual void StartState() { } - public static Type[] connectionImpls = new Type[(int)ConnectionStateEnum.Count]; - public static PacketHandlerInfo[,] packetHandlers = new PacketHandlerInfo[(int)ConnectionStateEnum.Count, (int)Packets.Count]; + public virtual void OnDisconnect() + { + } + + public virtual PacketHandlerInfo? GetPacketHandler(Packets packet) + { + return null; + } + + public static Type[] stateImpls = new Type[(int)ConnectionStateEnum.Count]; + public static PacketHandlerInfo?[,] packetHandlers = new PacketHandlerInfo?[(int)ConnectionStateEnum.Count, (int)Packets.Count]; public static void SetImplementation(ConnectionStateEnum state, Type type) { if (!type.IsSubclassOf(typeof(MpConnectionState))) return; - connectionImpls[(int)state] = type; + stateImpls[(int)state] = type; foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)) { @@ -36,9 +46,13 @@ public static void SetImplementation(ConnectionStateEnum state, Type type) continue; if (method.GetParameters().Length != 1 || method.GetParameters()[0].ParameterType != typeof(ByteReader)) - continue; + throw new Exception($"Bad packet handler signature for {method}"); bool fragment = method.GetAttribute() != null; + + if (packetHandlers[(int)state, (int)attr.packet] != null) + throw new Exception($"Packet {state}:{type} already has a handler"); + packetHandlers[(int)state, (int)attr.packet] = new PacketHandlerInfo(MethodInvoker.GetHandler(method), fragment); } } diff --git a/Source/Common/Networking/MpDisconnectReason.cs b/Source/Common/Networking/MpDisconnectReason.cs index 05e08d59..7a162053 100644 --- a/Source/Common/Networking/MpDisconnectReason.cs +++ b/Source/Common/Networking/MpDisconnectReason.cs @@ -18,7 +18,8 @@ public enum MpDisconnectReason : byte ServerPacketRead, Internal, ServerStarting, - BadGamePassword + BadGamePassword, + StateException } } diff --git a/Source/Common/Networking/NetworkingLiteNet.cs b/Source/Common/Networking/NetworkingLiteNet.cs index 5ccc697f..530caf2c 100644 --- a/Source/Common/Networking/NetworkingLiteNet.cs +++ b/Source/Common/Networking/NetworkingLiteNet.cs @@ -30,7 +30,7 @@ public void OnConnectionRequest(ConnectionRequest req) public void OnPeerConnected(NetPeer peer) { ConnectionBase conn = new LiteNetConnection(peer); - conn.State = ConnectionStateEnum.ServerJoining; + conn.ChangeState(ConnectionStateEnum.ServerJoining); peer.Tag = conn; var player = server.playerManager.OnConnected(conn); @@ -44,7 +44,7 @@ public void OnPeerConnected(NetPeer peer) public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { ConnectionBase conn = peer.GetConnection(); - server.playerManager.OnDisconnected(conn, MpDisconnectReason.ClientLeft); + server.playerManager.SetDisconnected(conn, MpDisconnectReason.ClientLeft); } public void OnNetworkLatencyUpdate(NetPeer peer, int latency) diff --git a/Source/Common/Networking/Packets.cs b/Source/Common/Networking/Packets.cs index e7a10861..1e1f0431 100644 --- a/Source/Common/Networking/Packets.cs +++ b/Source/Common/Networking/Packets.cs @@ -1,80 +1,68 @@ -namespace Multiplayer.Common -{ - public enum Packets : byte - { - // Client_ means the origin is the client - // Server_ means the origin is the server - - // Special - Client_Protocol, // Must be zeroth for future proofing - Server_SteamAccept, // Packet for the special Steam state, must be first +namespace Multiplayer.Common; - // Joining - Client_Username, - Client_JoinData, - Client_WorldRequest, +public enum Packets : byte +{ + // Client_ means the origin is the client + // Server_ means the origin is the server - // Playing - Client_WorldReady, - Client_Command, - Client_WorldDataUpload, - Client_IdBlockRequest, - Client_Chat, - Client_KeepAlive, - Client_SteamRequest, - Client_SyncInfo, - Client_Cursor, - Client_Desynced, - Client_Freeze, - Client_Debug, - Client_Selected, - Client_PingLocation, - Client_Traces, - Client_Autosaving, - Client_RequestRejoin, - Client_SetFaction, - Client_FrameTime, + // Special + Client_Protocol, // Must be zeroth for future proofing + Server_SteamAccept, // Packet for the special Steam state, must be first - // Joining - Server_ProtocolOk, - Server_UsernameOk, - Server_JoinData, - Server_WorldDataStart, - Server_WorldData, + // Joining + Client_Username, + Client_InitData, + Client_JoinData, + Client_WorldRequest, - // Playing - Server_Command, - Server_MapResponse, - Server_Notification, - Server_TimeControl, - Server_Chat, - Server_PlayerList, - Server_KeepAlive, - Server_SyncInfo, - Server_Cursor, - Server_Freeze, - Server_Debug, - Server_Selected, - Server_PingLocation, - Server_Traces, - Server_CanRejoin, - Server_SetFaction, + // Playing + Client_WorldReady, + Client_Command, + Client_WorldDataUpload, + Client_IdBlockRequest, + Client_Chat, + Client_KeepAlive, + Client_SteamRequest, + Client_SyncInfo, + Client_Cursor, + Client_Desynced, + Client_Freeze, + Client_Debug, + Client_Selected, + Client_PingLocation, + Client_Traces, + Client_Autosaving, + Client_RequestRejoin, + Client_SetFaction, + Client_FrameTime, - Count, - Special_Steam_Disconnect = 63 // Also the max packet id - } + // Joining + Server_ProtocolOk, + Server_InitDataRequest, + Server_UsernameOk, + Server_JoinData, - public enum ConnectionStateEnum : byte - { - ClientJoining, - ClientPlaying, - ClientSteam, + // Loading + Server_WorldDataStart, + Server_WorldData, - ServerJoining, - ServerPlaying, - ServerSteam, // unused + // Playing + Server_Command, + Server_MapResponse, + Server_Notification, + Server_TimeControl, + Server_Chat, + Server_PlayerList, + Server_KeepAlive, + Server_SyncInfo, + Server_Cursor, + Server_Freeze, + Server_Debug, + Server_Selected, + Server_PingLocation, + Server_Traces, + Server_SetFaction, - Count, - Disconnected - } + Count, + Special_Steam_Disconnect = 63 // Also the max packet id } diff --git a/Source/Common/Networking/State/ServerJoiningState.cs b/Source/Common/Networking/State/ServerJoiningState.cs index 7b73bc5f..982670ac 100644 --- a/Source/Common/Networking/State/ServerJoiningState.cs +++ b/Source/Common/Networking/State/ServerJoiningState.cs @@ -1,199 +1,159 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Threading.Tasks; -namespace Multiplayer.Common +namespace Multiplayer.Common; + +public class ServerJoiningState : AsyncConnectionState { - public class ServerJoiningState : MpConnectionState + public ServerJoiningState(ConnectionBase connection) : base(connection) { - public static Regex UsernamePattern = new(@"^[a-zA-Z0-9_]+$"); + } - private bool defsMismatched; + protected override async Task RunState() + { + HandleProtocol(await Packet(Packets.Client_Protocol)); + HandleUsername(await Packet(Packets.Client_Username)); - public ServerJoiningState(ConnectionBase conn) : base(conn) - { - } + while (await Server.InitData() is not { } && await EndIfDead()) + if (Server.initDataState == InitDataState.Waiting) + await RequestInitData(); - [PacketHandler(Packets.Client_Protocol)] - public void HandleProtocol(ByteReader data) - { - int clientProtocol = data.ReadInt32(); + connection.Send(Packets.Server_UsernameOk); - if (clientProtocol != MpVersion.Protocol) - Player.Disconnect(MpDisconnectReason.Protocol, ByteWriter.GetBytes(MpVersion.Version, MpVersion.Protocol)); - else - Player.SendPacket(Packets.Server_ProtocolOk, new object[] { Server.settings.hasPassword }); - } + if (HandleClientJoinData(await Packet(Packets.Client_JoinData).Fragmented()) is false) + return; - [PacketHandler(Packets.Client_Username)] - public void HandleUsername(ByteReader data) - { - if (!string.IsNullOrEmpty(connection.username)) // Username already set - return; + if (Server.settings.pauseOnJoin) + Server.commands.PauseAll(); - if (Server.settings.hasPassword) - { - string password = data.ReadString(); - if (password != Server.settings.password) - { - Player.Disconnect(MpDisconnectReason.BadGamePassword); - return; - } - } + if (Server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Join)) + Server.worldData.TryStartJoinPointCreation(); - string username = data.ReadString(); + Server.playerManager.OnJoin(Player); + Server.playerManager.SendInitDataCommand(Player); - if (username.Length < MultiplayerServer.MinUsernameLength || username.Length > MultiplayerServer.MaxUsernameLength) - { - Player.Disconnect(MpDisconnectReason.UsernameLength); - return; - } + await Packet(Packets.Client_WorldRequest); - if (!Player.IsArbiter && !UsernamePattern.IsMatch(username)) - { - Player.Disconnect(MpDisconnectReason.UsernameChars); - return; - } + connection.ChangeState(ConnectionStateEnum.ServerLoading); + } - if (Server.GetPlayer(username) != null) - { - Player.Disconnect(MpDisconnectReason.UsernameAlreadyOnline); - return; - } + private void HandleProtocol(ByteReader data) + { + int clientProtocol = data.ReadInt32(); - connection.username = username; - connection.Send(Packets.Server_UsernameOk); - } + if (clientProtocol != MpVersion.Protocol) + Player.Disconnect(MpDisconnectReason.Protocol, ByteWriter.GetBytes(MpVersion.Version, MpVersion.Protocol)); + else + Player.SendPacket(Packets.Server_ProtocolOk, new object[] { Server.settings.hasPassword }); + } - [PacketHandler(Packets.Client_JoinData)] - [IsFragmented] - public void HandleJoinData(ByteReader data) + private void HandleUsername(ByteReader data) + { + if (Server.settings.hasPassword) { - var defTypeCount = data.ReadInt32(); - if (defTypeCount > 512) + string password = data.ReadString(); + if (password != Server.settings.password) { - Player.Disconnect("Too many defs"); + Player.Disconnect(MpDisconnectReason.BadGamePassword); return; } + } - var defsResponse = new ByteWriter(); - - for (int i = 0; i < defTypeCount; i++) - { - var defType = data.ReadString(128); - var defCount = data.ReadInt32(); - var defHash = data.ReadInt32(); - - var status = DefCheckStatus.Ok; - - if (!Server.defInfos.TryGetValue(defType, out DefInfo info)) - status = DefCheckStatus.Not_Found; - else if (info.count != defCount) - status = DefCheckStatus.Count_Diff; - else if (info.hash != defHash) - status = DefCheckStatus.Hash_Diff; - - if (status != DefCheckStatus.Ok) - defsMismatched = true; - - defsResponse.WriteByte((byte)status); - } - - connection.SendFragmented( - Packets.Server_JoinData, - Server.settings.gameName, - Player.id, - Server.rwVersion, - Server.mpVersion, - defsResponse.ToArray(), - Server.serverData - ); - - if (!defsMismatched) - { - if (Server.settings.pauseOnJoin) - Server.commands.PauseAll(); - - if (Server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Join)) - Server.TryStartJoinPointCreation(); + string username = data.ReadString(); - Server.playerManager.OnJoin(Player); - } + if (username.Length < MultiplayerServer.MinUsernameLength || username.Length > MultiplayerServer.MaxUsernameLength) + { + Player.Disconnect(MpDisconnectReason.UsernameLength); + return; } - [PacketHandler(Packets.Client_WorldRequest)] - public void HandleWorldRequest(ByteReader data) + if (!Player.IsArbiter && !MultiplayerServer.UsernamePattern.IsMatch(username)) { - if (Server.CreatingJoinPoint) - { - Server.playerManager.playersWaitingForWorldData.Add(Player.id); - return; - } - - SendWorldData(); + Player.Disconnect(MpDisconnectReason.UsernameChars); + return; } - public void SendWorldData() + if (Server.GetPlayer(username) != null) { - connection.Send(Packets.Server_WorldDataStart); - - ByteWriter writer = new ByteWriter(); - - writer.WriteInt32(Player.FactionId); - writer.WriteInt32(MultiplayerServer.instance.gameTimer); - writer.WritePrefixedBytes(MultiplayerServer.instance.savedGame); - writer.WritePrefixedBytes(MultiplayerServer.instance.semiPersistent); - - writer.WriteInt32(MultiplayerServer.instance.mapCmds.Count); - - foreach (var kv in MultiplayerServer.instance.mapCmds) - { - int mapId = kv.Key; + Player.Disconnect(MpDisconnectReason.UsernameAlreadyOnline); + return; + } - //MultiplayerServer.instance.SendCommand(CommandType.CreateMapFactionData, ScheduledCommand.NoFaction, mapId, ByteWriter.GetBytes(factionId)); + connection.username = username; + } - List mapCmds = kv.Value; + private async Task RequestInitData() + { + Server.initDataState = InitDataState.Requested; + Server.initDataSource = new TaskCompletionSource(); - writer.WriteInt32(mapId); + Player.SendPacket(Packets.Server_InitDataRequest, ByteWriter.GetBytes(Server.settings.syncConfigs)); - writer.WriteInt32(mapCmds.Count); - foreach (var arr in mapCmds) - writer.WritePrefixedBytes(arr); - } + ServerLog.Verbose("Sent initial data request"); - writer.WriteInt32(MultiplayerServer.instance.mapData.Count); + var initData = await PacketOrNull(Packets.Client_InitData).Fragmented(); - foreach (var kv in MultiplayerServer.instance.mapData) - { - int mapId = kv.Key; - byte[] mapData = kv.Value; + if (initData != null) + { + Server.CompleteInitData(ServerInitData.Deserialize(initData)); + } + else + { + Server.initDataState = InitDataState.Waiting; + Server.initDataSource.SetResult(null); + } + } - writer.WriteInt32(mapId); - writer.WritePrefixedBytes(mapData); - } + private bool HandleClientJoinData(ByteReader data) + { + var defTypeCount = data.ReadInt32(); + if (defTypeCount > 512) + { + Player.Disconnect("Too many defs"); + return false; + } - writer.WriteInt32(Server.commands.SentCmds); - writer.WriteBool(Server.freezeManager.Frozen); + var defsResponse = new ByteWriter(); + var defsMatch = true; - writer.WriteInt32(Server.syncInfos.Count); - foreach (var syncInfo in Server.syncInfos) - writer.WritePrefixedBytes(syncInfo); + for (int i = 0; i < defTypeCount; i++) + { + var defType = data.ReadString(128); + var defCount = data.ReadInt32(); + var defHash = data.ReadInt32(); - connection.State = ConnectionStateEnum.ServerPlaying; + var status = DefCheckStatus.Ok; - byte[] packetData = writer.ToArray(); - connection.SendFragmented(Packets.Server_WorldData, packetData); + if (!Server.initData!.DefInfos.TryGetValue(defType, out DefInfo info)) + status = DefCheckStatus.Not_Found; + else if (info.count != defCount) + status = DefCheckStatus.Count_Diff; + else if (info.hash != defHash) + status = DefCheckStatus.Hash_Diff; - Player.SendPlayerList(); + if (status != DefCheckStatus.Ok) + defsMatch = false; - ServerLog.Log("World response sent: " + packetData.Length); + defsResponse.WriteByte((byte)status); } - } - public enum DefCheckStatus : byte - { - Ok, - Not_Found, - Count_Diff, - Hash_Diff, + connection.SendFragmented( + Packets.Server_JoinData, + Server.settings.gameName, + Player.id, + Server.initData!.RwVersion, + MpVersion.Version, + defsResponse.ToArray(), + Server.initData.RawData + ); + + return defsMatch; } } + +public enum DefCheckStatus : byte +{ + Ok, + Not_Found, + Count_Diff, + Hash_Diff, +} diff --git a/Source/Common/Networking/State/ServerLoadingState.cs b/Source/Common/Networking/State/ServerLoadingState.cs new file mode 100644 index 00000000..7b9507b9 --- /dev/null +++ b/Source/Common/Networking/State/ServerLoadingState.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Multiplayer.Common; + +public class ServerLoadingState : AsyncConnectionState +{ + public ServerLoadingState(ConnectionBase connection) : base(connection) + { + } + + protected override async Task RunState() + { + await Server.worldData.WaitJoinPoint(); + await EndIfDead(); + + SendWorldData(); + + connection.ChangeState(ConnectionStateEnum.ServerPlaying); + Player.SendPlayerList(); + } + + public void SendWorldData() + { + connection.Send(Packets.Server_WorldDataStart); + + ByteWriter writer = new ByteWriter(); + + writer.WriteInt32(Player.FactionId); + writer.WriteInt32(Server.gameTimer); + writer.WriteInt32(Server.sentCmdsSnapshot); + writer.WriteBool(Server.freezeManager.Frozen); + writer.WritePrefixedBytes(Server.worldData.savedGame); + writer.WritePrefixedBytes(Server.worldData.semiPersistent); + + writer.WriteInt32(Server.worldData.mapCmds.Count); + + foreach (var kv in Server.worldData.mapCmds) + { + int mapId = kv.Key; + + //MultiplayerServer.instance.SendCommand(CommandType.CreateMapFactionData, ScheduledCommand.NoFaction, mapId, ByteWriter.GetBytes(factionId)); + + List mapCmds = kv.Value; + + writer.WriteInt32(mapId); + + writer.WriteInt32(mapCmds.Count); + foreach (var arr in mapCmds) + writer.WritePrefixedBytes(arr); + } + + writer.WriteInt32(Server.worldData.mapData.Count); + + foreach (var kv in Server.worldData.mapData) + { + int mapId = kv.Key; + byte[] mapData = kv.Value; + + writer.WriteInt32(mapId); + writer.WritePrefixedBytes(mapData); + } + + writer.WriteInt32(Server.worldData.syncInfos.Count); + foreach (var syncInfo in Server.worldData.syncInfos) + writer.WritePrefixedBytes(syncInfo); + + byte[] packetData = writer.ToArray(); + connection.SendFragmented(Packets.Server_WorldData, packetData); + + ServerLog.Log("World response sent: " + packetData.Length); + } +} diff --git a/Source/Common/Networking/State/ServerPlayingState.cs b/Source/Common/Networking/State/ServerPlayingState.cs index 324175f2..464b1af7 100644 --- a/Source/Common/Networking/State/ServerPlayingState.cs +++ b/Source/Common/Networking/State/ServerPlayingState.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Linq; +using Verse; namespace Multiplayer.Common { @@ -17,8 +19,7 @@ public void HandleWorldReady(ByteReader data) [PacketHandler(Packets.Client_RequestRejoin)] public void HandleRejoin(ByteReader data) { - connection.State = ConnectionStateEnum.ServerJoining; - connection.Send(Packets.Server_CanRejoin); + connection.ChangeState(ConnectionStateEnum.ServerLoading); Player.ResetTimeVotes(); } @@ -71,20 +72,7 @@ public void HandleChat(ByteReader data) if (msg[0] == '/') { var cmd = msg.Substring(1); - var parts = cmd.Split(' '); - var handler = Server.GetChatCmdHandler(parts[0]); - - if (handler != null) - { - if (handler.requiresHost && !Player.IsHost) - Player.SendChat("No permission"); - else - handler.Handle(Player, parts.SubArray(1)); - } - else - { - Player.SendChat("Invalid command"); - } + Server.HandleChatCmd(Player, cmd); } else { @@ -96,22 +84,25 @@ public void HandleChat(ByteReader data) [IsFragmented] public void HandleWorldDataUpload(ByteReader data) { - var arbiter = Server.ArbiterPlaying; - if (arbiter && !Player.IsArbiter) return; - if (!arbiter && !Player.IsHost) return; + if (Server.ArbiterPlaying ? !Player.IsArbiter : !Player.IsHost) + return; + + ServerLog.Log($"Got world upload {data.Left}"); + + Server.worldData.mapData = new Dictionary(); int maps = data.ReadInt32(); for (int i = 0; i < maps; i++) { int mapId = data.ReadInt32(); - Server.mapData[mapId] = data.ReadPrefixedBytes(); + Server.worldData.mapData[mapId] = data.ReadPrefixedBytes(); } - Server.savedGame = data.ReadPrefixedBytes(); - Server.semiPersistent = data.ReadPrefixedBytes(); + Server.worldData.savedGame = data.ReadPrefixedBytes(); + Server.worldData.semiPersistent = data.ReadPrefixedBytes(); - if (Server.CreatingJoinPoint) - Server.EndJoinPointCreation(); + if (Server.worldData.CreatingJoinPoint) + Server.worldData.EndJoinPointCreation(); } [PacketHandler(Packets.Client_Cursor)] @@ -150,7 +141,7 @@ public void HandleCursor(ByteReader data) Player.lastCursorTick = Server.NetTimer; - Server.SendToAll(Packets.Server_Cursor, writer.ToArray(), reliable: false, excluding: Player); + Server.SendToPlaying(Packets.Server_Cursor, writer.ToArray(), reliable: false, excluding: Player); } [PacketHandler(Packets.Client_Selected)] @@ -165,7 +156,7 @@ public void HandleSelected(ByteReader data) writer.WritePrefixedInts(data.ReadPrefixedInts(200)); writer.WritePrefixedInts(data.ReadPrefixedInts(200)); - Server.SendToAll(Packets.Server_Selected, writer.ToArray(), excluding: Player); + Server.SendToPlaying(Packets.Server_Selected, writer.ToArray(), excluding: Player); } [PacketHandler(Packets.Client_PingLocation)] @@ -181,7 +172,7 @@ public void HandlePing(ByteReader data) writer.WriteFloat(data.ReadFloat()); // Y writer.WriteFloat(data.ReadFloat()); // Z - Server.SendToAll(Packets.Server_PingLocation, writer.ToArray()); + Server.SendToPlaying(Packets.Server_PingLocation, writer.ToArray()); } [PacketHandler(Packets.Client_IdBlockRequest)] @@ -209,6 +200,7 @@ public void HandleClientKeepAlive(ByteReader data) var workTicks = data.ReadInt32(); Player.ticksBehind = ticksBehind; + Player.ticksBehindReceivedAt = Server.gameTimer; Player.simulating = simulating; Player.keepAliveAt = Server.NetTimer; @@ -237,9 +229,9 @@ public void HandleDesyncCheck(ByteReader data) var raw = data.ReadRaw(data.Left); // Keep at most 10 sync infos - Server.syncInfos.Add(raw); - if (Server.syncInfos.Count > 10) - Server.syncInfos.RemoveAt(0); + Server.worldData.syncInfos.Add(raw); + if (Server.worldData.syncInfos.Count > 10) + Server.worldData.syncInfos.RemoveAt(0); foreach (var p in Server.PlayingPlayers.Where(p => !p.IsArbiter && (arbiter || !p.IsHost))) p.conn.SendFragmented(Packets.Server_SyncInfo, raw); @@ -259,7 +251,7 @@ public void HandleFreeze(ByteReader data) public void HandleAutosaving(ByteReader data) { if (Player.IsHost && Server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Autosave)) - Server.TryStartJoinPointCreation(); + Server.worldData.TryStartJoinPointCreation(); } [PacketHandler(Packets.Client_Debug)] @@ -272,11 +264,14 @@ public void HandleSetFaction(ByteReader data) { if (!Player.IsHost) return; - int player = data.ReadInt32(); + int playerId = data.ReadInt32(); int factionId = data.ReadInt32(); - Server.GetPlayer(player).FactionId = factionId; - Server.SendToAll(Packets.Server_SetFaction, new object[] { player, factionId }); + var player = Server.GetPlayer(playerId); + if (player == null) return; + + player.FactionId = factionId; + Server.SendToPlaying(Packets.Server_SetFaction, new object[] { playerId, factionId }); } [PacketHandler(Packets.Client_FrameTime)] diff --git a/Source/Common/Networking/State/ServerSteamState.cs b/Source/Common/Networking/State/ServerSteamState.cs index 3058c52c..414094a8 100644 --- a/Source/Common/Networking/State/ServerSteamState.cs +++ b/Source/Common/Networking/State/ServerSteamState.cs @@ -10,7 +10,7 @@ public ServerSteamState(ConnectionBase conn) : base(conn) [PacketHandler(Packets.Client_SteamRequest)] public void HandleSteamRequest(ByteReader data) { - connection.State = ConnectionStateEnum.ServerJoining; + connection.ChangeState(ConnectionStateEnum.ServerJoining); connection.Send(Packets.Server_SteamAccept); } } diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index e3455d51..68dcf3b3 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -11,10 +11,10 @@ public class PlayerManager const long ThrottleMillis = 1000; private Dictionary lastConnection = new(); private Stopwatch clock = Stopwatch.StartNew(); - public HashSet playersWaitingForWorldData = new(); public List Players { get; } = new(); + public IEnumerable JoinedPlayers => Players.Where(p => p.HasJoined); public IEnumerable PlayingPlayers => Players.Where(p => p.IsPlaying); public PlayerManager(MultiplayerServer server) @@ -27,11 +27,11 @@ public void SendLatencies() var writer = new ByteWriter(); writer.WriteByte((byte)PlayerListAction.Latencies); - writer.WriteInt32(PlayingPlayers.Count()); - foreach (var player in PlayingPlayers) + writer.WriteInt32(JoinedPlayers.Count()); + foreach (var player in JoinedPlayers) player.WriteLatencyUpdate(writer); - server.SendToAll(Packets.Server_PlayerList, writer.ToArray()); + server.SendToPlaying(Packets.Server_PlayerList, writer.ToArray()); } // id can be an IPAddress or CSteamID @@ -69,10 +69,12 @@ public ServerPlayer OnConnected(ConnectionBase conn) return conn.serverPlayer; } - public void OnDisconnected(ConnectionBase conn, MpDisconnectReason reason) + public void SetDisconnected(ConnectionBase conn, MpDisconnectReason reason) { if (conn.State == ConnectionStateEnum.Disconnected) return; + conn.StateObj?.OnDisconnect(); + ServerPlayer player = conn.serverPlayer; Players.Remove(player); @@ -89,12 +91,12 @@ public void OnDisconnected(ConnectionBase conn, MpDisconnectReason reason) server.SendNotification("MpPlayerDisconnected", conn.username); server.SendChat($"{conn.username} has left."); - server.SendToAll(Packets.Server_PlayerList, new object[] { (byte)PlayerListAction.Remove, player.id }); + server.SendToPlaying(Packets.Server_PlayerList, new object[] { (byte)PlayerListAction.Remove, player.id }); player.ResetTimeVotes(); } - conn.State = ConnectionStateEnum.Disconnected; + conn.ChangeState(ConnectionStateEnum.Disconnected); ServerLog.Log($"Disconnected ({reason}): {conn}"); } @@ -110,7 +112,7 @@ public void OnDesync(ServerPlayer player, int tick, int diffAt) server.commands.PauseAll(); if (server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Desync)) - server.TryStartJoinPointCreation(true); + server.worldData.TryStartJoinPointCreation(true); } public static ColorRGB[] PlayerColors = @@ -129,9 +131,7 @@ public void OnDesync(ServerPlayer player, int tick, int diffAt) public void OnJoin(ServerPlayer player) { player.hasJoined = true; - player.FactionId = server.defaultFactionId; - - SendInitDataCommand(player); + player.FactionId = server.worldData.defaultFactionId; server.SendNotification("MpPlayerConnected", player.Username); server.SendChat($"{player.Username} has joined."); @@ -147,7 +147,7 @@ public void OnJoin(ServerPlayer player) writer.WriteByte((byte)PlayerListAction.Add); writer.WriteRaw(player.SerializePlayerInfo()); - server.SendToAll(Packets.Server_PlayerList, writer.ToArray()); + server.SendToPlaying(Packets.Server_PlayerList, writer.ToArray()); } public void SendInitDataCommand(ServerPlayer player) @@ -167,12 +167,22 @@ public void OnServerStop() Players.Clear(); } - public ServerPlayer GetPlayer(string username) + public void MakeHost(ServerPlayer host) + { + OnJoin(host); + + host.conn.ChangeState(ConnectionStateEnum.ServerPlaying); + host.SendPlayerList(); + SendInitDataCommand(host); + host.UpdateStatus(PlayerStatus.Playing); + } + + public ServerPlayer? GetPlayer(string username) { return Players.Find(player => player.Username == username); } - public ServerPlayer GetPlayer(int id) + public ServerPlayer? GetPlayer(int id) { return Players.Find(player => player.id == id); } diff --git a/Source/Common/ReplayInfo.cs b/Source/Common/ReplayInfo.cs new file mode 100644 index 00000000..7e67189a --- /dev/null +++ b/Source/Common/ReplayInfo.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace Multiplayer.Common; + +public class ReplayInfo +{ + public string name; + public int protocol; + public int playerFaction; + + public List sections = new(); + public List events = new(); + + public string rwVersion; + public List modIds; + public List modNames; + public List modAssemblyHashes; // Unused, here to satisfy DirectXmlToObject on old saves + + public XmlBool asyncTime; + + public static byte[] Write(ReplayInfo info) + { + var stream = new MemoryStream(); + var ns = new XmlSerializerNamespaces(); + ns.Add("", ""); + var settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + using var writer = XmlWriter.Create(stream, settings); + GetSerializer().Serialize(writer, info, ns); + return stream.ToArray(); + } + + public static ReplayInfo Read(byte[] xml) + { + return (ReplayInfo)GetSerializer().Deserialize(new MemoryStream(xml))!; + } + + private static XmlSerializer GetSerializer() + { + var overrides = new XmlAttributeOverrides(); + overrides.Add(typeof(ReplayInfo), nameof(events), new XmlAttributes + { + XmlArrayItems = { new XmlArrayItemAttribute("li") } + }); + overrides.Add(typeof(ReplayInfo), nameof(sections), new XmlAttributes + { + XmlArrayItems = { new XmlArrayItemAttribute("li") } + }); + overrides.Add(typeof(ReplayInfo), nameof(modIds), new XmlAttributes + { + XmlArrayItems = { new XmlArrayItemAttribute("li") } + }); + overrides.Add(typeof(ReplayInfo), nameof(modNames), new XmlAttributes + { + XmlArrayItems = { new XmlArrayItemAttribute("li") } + }); + + return new XmlSerializer(typeof(ReplayInfo), overrides); + } +} + +public class ReplaySection +{ + public int start; + public int end; + + // ReSharper disable once UnusedMember.Global + public ReplaySection() + { + } + + public ReplaySection(int start, int end) + { + this.start = start; + this.end = end; + } +} + +public class ReplayEvent +{ + public string name; + public int time; +} + +// Taken from StackOverflow, makes bool serialization case-insensitive +public struct XmlBool : IXmlSerializable +{ + private bool value; + + public static implicit operator bool(XmlBool yn) + { + return yn.value; + } + + public static implicit operator XmlBool(bool b) + { + return new XmlBool { value = b }; + } + + public XmlSchema? GetSchema() + { + return null; + } + + public void ReadXml(XmlReader reader) + { + var s = reader.ReadElementContentAsString().ToLowerInvariant(); + value = s is "true" or "yes" or "y"; + } + + public void WriteXml(XmlWriter writer) + { + writer.WriteString(value ? "true" : "false"); + } +} diff --git a/Source/Common/ScheduledCommand.cs b/Source/Common/ScheduledCommand.cs index ab769333..5f988eee 100644 --- a/Source/Common/ScheduledCommand.cs +++ b/Source/Common/ScheduledCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Multiplayer.Common { @@ -48,7 +49,7 @@ public static ScheduledCommand Deserialize(ByteReader data) int factionId = data.ReadInt32(); int mapId = data.ReadInt32(); int playerId = data.ReadInt32(); - byte[] extraBytes = data.ReadPrefixedBytes(); + byte[] extraBytes = data.ReadPrefixedBytes()!; return new ScheduledCommand(cmd, ticks, factionId, mapId, playerId, extraBytes); } @@ -57,5 +58,28 @@ public override string ToString() { return $"Cmd: {type}, faction: {factionId}, map: {mapId}, ticks: {ticks}, player: {playerId}"; } + + public static List DeserializeCmds(byte[] data) + { + var reader = new ByteReader(data); + + int count = reader.ReadInt32(); + var result = new List(count); + for (int i = 0; i < count; i++) + result.Add(Deserialize(new ByteReader(reader.ReadPrefixedBytes()!))); + + return result; + } + + public static byte[] SerializeCmds(List cmds) + { + ByteWriter writer = new ByteWriter(); + + writer.WriteInt32(cmds.Count); + foreach (var cmd in cmds) + writer.WritePrefixedBytes(Serialize(cmd)); + + return writer.ToArray(); + } } } diff --git a/Source/Common/ScribeLike.cs b/Source/Common/ScribeLike.cs new file mode 100644 index 00000000..ab714822 --- /dev/null +++ b/Source/Common/ScribeLike.cs @@ -0,0 +1,19 @@ +namespace Multiplayer.Common; + +public static class ScribeLike +{ + public static Provider provider; + + /// + /// Corresponds to Scribe_Values.Look + /// + public static void Look(ref T? value, string label, T? defaultValue = default, bool forceSave = false) + { + provider.Look(ref value, label, defaultValue, forceSave); + } + + public abstract class Provider + { + public abstract void Look(ref T value, string label, T defaultValue, bool forceSave); + } +} diff --git a/Source/Common/ServerInitData.cs b/Source/Common/ServerInitData.cs new file mode 100644 index 00000000..bafc6032 --- /dev/null +++ b/Source/Common/ServerInitData.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Common; + +public record ServerInitData( + byte[] RawData, + string RwVersion, + HashSet DebugOnlySyncCmds, + HashSet HostOnlySyncCmds, + Dictionary DefInfos +) +{ + public static ServerInitData Deserialize(ByteReader data) + { + var joinData = new ServerInitData + ( + data.ReadPrefixedBytes()!, + data.ReadString(), + data.ReadPrefixedInts().ToHashSet(), + data.ReadPrefixedInts().ToHashSet(), + new Dictionary() + ); + + var defInfoCount = data.ReadInt32(); + while (defInfoCount-- > 0) + joinData.DefInfos[data.ReadString()] = new DefInfo { count = data.ReadInt32(), hash = data.ReadInt32() }; + + return joinData; + } +} diff --git a/Source/Common/ServerPlayer.cs b/Source/Common/ServerPlayer.cs index d9d13cb6..eba51c9d 100644 --- a/Source/Common/ServerPlayer.cs +++ b/Source/Common/ServerPlayer.cs @@ -4,18 +4,21 @@ namespace Multiplayer.Common { - public class ServerPlayer + public class ServerPlayer : IChatSource { public int id; public ConnectionBase conn; public PlayerType type = PlayerType.Normal; - public PlayerStatus status; + public PlayerStatus status = PlayerStatus.Simulating; public ColorRGB color; public bool hasJoined; - public int ticksBehind; public bool simulating; public float frameTime; + public int ticksBehind; + public int ticksBehindReceivedAt; + public int ExtrapolatedTicksBehind => ticksBehind + (Server.gameTimer - ticksBehindReceivedAt); + public ulong steamId; public string steamPersonaName = ""; @@ -31,11 +34,12 @@ public class ServerPlayer public string Username => conn.username; public int Latency => conn.Latency; public int FactionId { get; set; } + public bool HasJoined => conn.State is ConnectionStateEnum.ServerLoading or ConnectionStateEnum.ServerPlaying; public bool IsPlaying => conn.State == ConnectionStateEnum.ServerPlaying; public bool IsHost => Server.hostUsername == Username; public bool IsArbiter => type == PlayerType.Arbiter; - public MultiplayerServer Server => MultiplayerServer.instance; + public MultiplayerServer Server => MultiplayerServer.instance!; public ServerPlayer(int id, ConnectionBase connection) { @@ -47,7 +51,7 @@ public void HandleReceive(ByteReader data, bool reliable) { try { - conn.HandleReceive(data, reliable); + conn.HandleReceiveRaw(data, reliable); } catch (Exception e) { @@ -61,15 +65,15 @@ public void Disconnect(string reasonKey) Disconnect(MpDisconnectReason.GenericKeyed, ByteWriter.GetBytes(reasonKey)); } - public void Disconnect(MpDisconnectReason reason, byte[] data = null) + public void Disconnect(MpDisconnectReason reason, byte[]? data = null) { conn.Close(reason, data); - Server.playerManager.OnDisconnected(conn, reason); + Server.playerManager.SetDisconnected(conn, reason); } public void SendChat(string msg) { - SendPacket(Packets.Server_Chat, new[] { msg }); + SendPacket(Packets.Server_Chat, new object[] { msg }); } public void SendPacket(Packets packet, byte[] data, bool reliable = true) @@ -87,9 +91,9 @@ public void SendPlayerList() var writer = new ByteWriter(); writer.WriteByte((byte)PlayerListAction.List); - writer.WriteInt32(Server.PlayingPlayers.Count()); + writer.WriteInt32(Server.JoinedPlayers.Count()); - foreach (var player in Server.PlayingPlayers) + foreach (var player in Server.JoinedPlayers) writer.WriteRaw(player.SerializePlayerInfo()); conn.Send(Packets.Server_PlayerList, writer.ToArray()); @@ -121,13 +125,14 @@ public void WriteLatencyUpdate(ByteWriter writer) writer.WriteInt32(Latency); writer.WriteInt32(ticksBehind); writer.WriteBool(simulating); + writer.WriteFloat(frameTime); } - public void UpdateStatus(PlayerStatus status) + public void UpdateStatus(PlayerStatus newStatus) { - if (this.status == status) return; - this.status = status; - Server.SendToAll(Packets.Server_PlayerList, new object[] { (byte)PlayerListAction.Status, id, (byte)status }); + if (status == newStatus) return; + status = newStatus; + Server.SendToPlaying(Packets.Server_PlayerList, new object[] { (byte)PlayerListAction.Status, id, (byte)newStatus }); } public void ResetTimeVotes() @@ -136,10 +141,15 @@ public void ResetTimeVotes() CommandType.TimeSpeedVote, ScheduledCommand.NoFaction, ScheduledCommand.Global, - ByteWriter.GetBytes(TimeVote.PlayerResetAll, -1), + ByteWriter.GetBytes(TimeVote.PlayerResetGlobal, -1), fauxSource: this ); } + + public void SendMsg(string msg) + { + SendChat(msg); + } } public enum PlayerStatus : byte diff --git a/Source/Common/ServerSettings.cs b/Source/Common/ServerSettings.cs index 032f19f1..31414c7f 100644 --- a/Source/Common/ServerSettings.cs +++ b/Source/Common/ServerSettings.cs @@ -1,9 +1,8 @@ using System; -using Verse; namespace Multiplayer.Common { - public class ServerSettings : IExposable + public class ServerSettings { public string gameName; public string lanAddress; @@ -32,24 +31,24 @@ public void ExposeData() { // Remember to mirror the default values - Scribe_Values.Look(ref directAddress, "directAddress", $"0.0.0.0:{MultiplayerServer.DefaultPort}"); - Scribe_Values.Look(ref maxPlayers, "maxPlayers", 8); - Scribe_Values.Look(ref autosaveInterval, "autosaveInterval", 1f); - Scribe_Values.Look(ref autosaveUnit, "autosaveUnit"); - Scribe_Values.Look(ref steam, "steam"); - Scribe_Values.Look(ref direct, "direct"); - Scribe_Values.Look(ref lan, "lan", true); - Scribe_Values.Look(ref debugMode, "debugMode"); - Scribe_Values.Look(ref desyncTraces, "desyncTraces", true); - Scribe_Values.Look(ref syncConfigs, "syncConfigs", true); - Scribe_Values.Look(ref autoJoinPoint, "autoJoinPoint", AutoJoinPointFlags.Join | AutoJoinPointFlags.Desync); - Scribe_Values.Look(ref devModeScope, "devModeScope"); - Scribe_Values.Look(ref hasPassword, "hasPassword"); - Scribe_Values.Look(ref password, "password", ""); - Scribe_Values.Look(ref pauseOnLetter, "pauseOnLetter", PauseOnLetter.AnyThreat); - Scribe_Values.Look(ref pauseOnJoin, "pauseOnJoin", true); - Scribe_Values.Look(ref pauseOnDesync, "pauseOnDesync", true); - Scribe_Values.Look(ref timeControl, "timeControl"); + ScribeLike.Look(ref directAddress!, "directAddress", $"0.0.0.0:{MultiplayerServer.DefaultPort}"); + ScribeLike.Look(ref maxPlayers, "maxPlayers", 8); + ScribeLike.Look(ref autosaveInterval, "autosaveInterval", 1f); + ScribeLike.Look(ref autosaveUnit, "autosaveUnit"); + ScribeLike.Look(ref steam, "steam"); + ScribeLike.Look(ref direct, "direct"); + ScribeLike.Look(ref lan, "lan", true); + ScribeLike.Look(ref debugMode, "debugMode"); + ScribeLike.Look(ref desyncTraces, "desyncTraces", true); + ScribeLike.Look(ref syncConfigs, "syncConfigs", true); + ScribeLike.Look(ref autoJoinPoint, "autoJoinPoint", AutoJoinPointFlags.Join | AutoJoinPointFlags.Desync); + ScribeLike.Look(ref devModeScope, "devModeScope"); + ScribeLike.Look(ref hasPassword, "hasPassword"); + ScribeLike.Look(ref password!, "password", ""); + ScribeLike.Look(ref pauseOnLetter, "pauseOnLetter", PauseOnLetter.AnyThreat); + ScribeLike.Look(ref pauseOnJoin, "pauseOnJoin", true); + ScribeLike.Look(ref pauseOnDesync, "pauseOnDesync", true); + ScribeLike.Look(ref timeControl, "timeControl"); } } diff --git a/Source/Common/TimeVote.cs b/Source/Common/TimeVote.cs index 21a7eeba..cb2da1a7 100644 --- a/Source/Common/TimeVote.cs +++ b/Source/Common/TimeVote.cs @@ -7,8 +7,9 @@ public enum TimeVote : byte Fast, Superfast, Ultrafast, - PlayerReset, - PlayerResetAll, - Reset, - ResetAll + + PlayerResetTickable, + PlayerResetGlobal, + ResetTickable, + ResetGlobal } diff --git a/Source/Common/Util/Blackhole.cs b/Source/Common/Util/Blackhole.cs new file mode 100644 index 00000000..6d50f07d --- /dev/null +++ b/Source/Common/Util/Blackhole.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Multiplayer.Common.Util; + +public struct Blackhole : INotifyCompletion +{ + public bool IsCompleted => false; + public void GetResult() { } + public void OnCompleted(Action continuation) { } + public Blackhole GetAwaiter() => this; +} diff --git a/Source/Common/Util/ColorRGB.cs b/Source/Common/Util/ColorRGB.cs index 04cc0f9a..3cea397e 100644 --- a/Source/Common/Util/ColorRGB.cs +++ b/Source/Common/Util/ColorRGB.cs @@ -1,9 +1,6 @@ -using UnityEngine; -using Verse; - namespace Multiplayer.Common { - public struct ColorRGB : IExposable + public struct ColorRGB { public byte r, g, b; @@ -13,21 +10,5 @@ public ColorRGB(byte r, byte g, byte b) this.g = g; this.b = b; } - - public void ExposeData() - { - ScribeAsInt(ref r, "r"); - ScribeAsInt(ref g, "g"); - ScribeAsInt(ref b, "b"); - } - - private void ScribeAsInt(ref byte value, string label) - { - int temp = value; - Scribe_Values.Look(ref temp, label); - value = (byte)temp; - } - - public static implicit operator Color(ColorRGB value) => new(value.r / 255f, value.g / 255f, value.b / 255f); } } diff --git a/Source/Common/Util/CompilerTypes.cs b/Source/Common/Util/CompilerTypes.cs new file mode 100644 index 00000000..3ff82c52 --- /dev/null +++ b/Source/Common/Util/CompilerTypes.cs @@ -0,0 +1,19 @@ +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + // Added to support compiling with C# 9 + public sealed class IsExternalInit + { + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] + public sealed class AsyncMethodBuilderAttribute : Attribute + { + public Type BuilderType { get; } + + public AsyncMethodBuilderAttribute(Type builderType) + { + BuilderType = builderType; + } + } +} diff --git a/Source/Common/Util/Extensions.cs b/Source/Common/Util/Extensions.cs index 8d31a7df..05ed0541 100644 --- a/Source/Common/Util/Extensions.cs +++ b/Source/Common/Util/Extensions.cs @@ -7,13 +7,12 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; -using System.Xml; namespace Multiplayer.Common { public static class Extensions { - public static object GetDefaultValue(this Type t) + public static object? GetDefaultValue(this Type t) { return t.IsValueType ? Activator.CreateInstance(t) : null; } @@ -79,7 +78,7 @@ public static IEnumerable AllAttributes(this MemberInfo member) where T : return Attribute.GetCustomAttributes(member).OfType(); } - public static T GetAttribute(this MemberInfo member) where T : Attribute + public static T? GetAttribute(this MemberInfo member) where T : Attribute { return AllAttributes(member).FirstOrDefault(); } @@ -113,35 +112,16 @@ public static string ToHexString(this byte[] ba) hex.AppendFormat("{0:x2}", b); return hex.ToString(); } - } - - public static class XmlExtensions - { - public static void SelectAndRemove(this XmlNode node, string xpath) - { - XmlNodeList nodes = node.SelectNodes(xpath); - foreach (XmlNode selected in nodes) - selected.RemoveFromParent(); - } - - public static void RemoveChildIfPresent(this XmlNode node, string child) - { - XmlNode childNode = node[child]; - if (childNode != null) - node.RemoveChild(childNode); - } - public static void RemoveFromParent(this XmlNode node) + public static void Deconstruct(this KeyValuePair tuple, out Key key, out Value value) { - if (node == null) return; - node.ParentNode.RemoveChild(node); + key = tuple.Key; + value = tuple.Value; } - public static void AddNode(this XmlNode parent, string name, string value) + public static float MaxOrZero(this IEnumerable items, Func map) { - XmlNode node = parent.OwnerDocument.CreateElement(name); - node.InnerText = value; - parent.AppendChild(node); + return items.Max(i => (float?)map(i)) ?? 0f; } } diff --git a/Source/Common/Util/HotSwappableAttribute.cs b/Source/Common/Util/HotSwappableAttribute.cs new file mode 100644 index 00000000..5b6d300f --- /dev/null +++ b/Source/Common/Util/HotSwappableAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Multiplayer.Common.Util; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class HotSwappableAttribute : Attribute +{ +} diff --git a/Source/Common/Util/ServerLog.cs b/Source/Common/Util/ServerLog.cs index 19730e5a..a5ea0d56 100644 --- a/Source/Common/Util/ServerLog.cs +++ b/Source/Common/Util/ServerLog.cs @@ -7,6 +7,8 @@ public class ServerLog : INetLogger { public static Action info; public static Action error; + public static bool detailEnabled; + public static bool verboseEnabled; public static void Log(string s) { @@ -16,6 +18,12 @@ public static void Log(string s) Console.WriteLine(s); } + public static void Detail(string s) + { + if (detailEnabled) + Console.WriteLine(s); + } + public static void Error(string s) { if (error != null) @@ -24,6 +32,12 @@ public static void Error(string s) Console.Error.WriteLine(s); } + public static void Verbose(string s) + { + if (verboseEnabled) + Console.WriteLine($"(Verbose) {s}"); + } + public void WriteNet(NetLogLevel level, string str, params object[] args) { if (level == NetLogLevel.Error) diff --git a/Source/Common/Util/XmlExtensions.cs b/Source/Common/Util/XmlExtensions.cs new file mode 100644 index 00000000..2ed76c5f --- /dev/null +++ b/Source/Common/Util/XmlExtensions.cs @@ -0,0 +1,33 @@ +using System.Xml; + +namespace Multiplayer.Common; + +public static class XmlExtensions +{ + public static void SelectAndRemove(this XmlNode node, string xpath) + { + XmlNodeList nodes = node.SelectNodes(xpath); + foreach (XmlNode selected in nodes) + selected.RemoveFromParent(); + } + + public static void RemoveChildIfPresent(this XmlNode node, string child) + { + XmlNode childNode = node[child]; + if (childNode != null) + node.RemoveChild(childNode); + } + + public static void RemoveFromParent(this XmlNode node) + { + if (node == null) return; + node.ParentNode.RemoveChild(node); + } + + public static void AddNode(this XmlNode parent, string name, string value) + { + XmlNode node = parent.OwnerDocument.CreateElement(name); + node.InnerText = value; + parent.AppendChild(node); + } +} diff --git a/Source/Common/Util/ZipExtensions.cs b/Source/Common/Util/ZipExtensions.cs new file mode 100644 index 00000000..2ce4c1e2 --- /dev/null +++ b/Source/Common/Util/ZipExtensions.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; + +namespace Multiplayer.Common.Util; + +[HotSwappable] +public static class ZipExtensions +{ + public static byte[] GetBytes(this ZipArchive zip, string path) + { + return zip.GetEntry(path)!.GetBytes(); + } + + public static string GetString(this ZipArchive zip, string path) + { + return Encoding.UTF8.GetString(zip.GetBytes(path)); + } + + public static byte[] GetBytes(this ZipArchiveEntry entry) + { + using var stream = entry.Open(); + using MemoryStream ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + public static void AddEntry(this ZipArchive zip, string path, byte[] bytes) + { + using var stream = zip.CreateEntry(path).Open(); + stream.Write(bytes, 0, bytes.Length); + } + + public static void AddEntry(this ZipArchive zip, string path, string text) + { + zip.AddEntry(path, Encoding.UTF8.GetBytes(text)); + } + + public static IEnumerable GetEntries(this ZipArchive zip, string pathPattern) + { + pathPattern = Regex.Escape(pathPattern).Replace("\\*", ".*"); + var regex = new Regex(pathPattern); + foreach (var entry in zip.Entries) + if (regex.IsMatch(entry.FullName)) + yield return entry; + } +} diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 6ebc2f78..4ac51a3e 100644 --- a/Source/Common/Version.cs +++ b/Source/Common/Version.cs @@ -2,8 +2,8 @@ namespace Multiplayer.Common { public static class MpVersion { - public const string Version = "0.7.3"; - public const int Protocol = 29; + public const string Version = "0.8"; + public const int Protocol = 32; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/Common/WorldData.cs b/Source/Common/WorldData.cs new file mode 100644 index 00000000..c868155b --- /dev/null +++ b/Source/Common/WorldData.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Multiplayer.Common; + +public class WorldData +{ + public int defaultFactionId; + public byte[]? savedGame; // Compressed game save + public byte[]? semiPersistent; // Compressed semi persistent data + public Dictionary mapData = new(); // Map id to compressed map data + + public Dictionary> mapCmds = new(); // Map id to serialized cmds list + public Dictionary>? tmpMapCmds; + public int lastJoinPointAtWorkTicks = -1; + + public List syncInfos = new(); + + private TaskCompletionSource? dataSource; + + public bool CreatingJoinPoint => tmpMapCmds != null; + + public MultiplayerServer Server { get; } + + public WorldData(MultiplayerServer server) + { + Server = server; + } + + public bool TryStartJoinPointCreation(bool force = false) + { + if (!force && Server.workTicks - lastJoinPointAtWorkTicks < 30) + return false; + + if (CreatingJoinPoint) + return false; + + Server.SendChat("Creating a join point..."); + + Server.commands.Send(CommandType.CreateJoinPoint, ScheduledCommand.NoFaction, ScheduledCommand.Global, Array.Empty()); + tmpMapCmds = new Dictionary>(); + dataSource = new TaskCompletionSource(); + + return true; + } + + public void EndJoinPointCreation() + { + mapCmds = tmpMapCmds!; + tmpMapCmds = null; + lastJoinPointAtWorkTicks = Server.workTicks; + dataSource!.SetResult(this); + } + + public Task WaitJoinPoint() + { + return dataSource?.Task ?? Task.FromResult(this); + } +} diff --git a/Source/Multiplayer.sln b/Source/Multiplayer.sln index 8b1f7441..69ea40c4 100644 --- a/Source/Multiplayer.sln +++ b/Source/Multiplayer.sln @@ -3,7 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27428.2015 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer", "Multiplayer.csproj", "{562CC73B-D52D-4615-B8AA-BCE875838D24}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Multiplayer.csproj", "{562CC73B-D52D-4615-B8AA-BCE875838D24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{1F2E809B-8891-495C-964B-C5A1B3FF5D69}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{C1C3634E-4BCB-4761-A6ED-01A040759DD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{66D1E6FF-3134-4F70-A278-0939CF3D48D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerLoader", "MultiplayerLoader\MultiplayerLoader.csproj", "{D9F53E9B-A72C-4183-B549-63B649320B61}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,6 +27,22 @@ Global {1DF67DDA-04CB-4DDF-B867-4C0E152BC959}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DF67DDA-04CB-4DDF-B867-4C0E152BC959}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DF67DDA-04CB-4DDF-B867-4C0E152BC959}.Release|Any CPU.Build.0 = Release|Any CPU + {1F2E809B-8891-495C-964B-C5A1B3FF5D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F2E809B-8891-495C-964B-C5A1B3FF5D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F2E809B-8891-495C-964B-C5A1B3FF5D69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F2E809B-8891-495C-964B-C5A1B3FF5D69}.Release|Any CPU.Build.0 = Release|Any CPU + {C1C3634E-4BCB-4761-A6ED-01A040759DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1C3634E-4BCB-4761-A6ED-01A040759DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1C3634E-4BCB-4761-A6ED-01A040759DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1C3634E-4BCB-4761-A6ED-01A040759DD7}.Release|Any CPU.Build.0 = Release|Any CPU + {66D1E6FF-3134-4F70-A278-0939CF3D48D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66D1E6FF-3134-4F70-A278-0939CF3D48D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66D1E6FF-3134-4F70-A278-0939CF3D48D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66D1E6FF-3134-4F70-A278-0939CF3D48D6}.Release|Any CPU.Build.0 = Release|Any CPU + {D9F53E9B-A72C-4183-B549-63B649320B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9F53E9B-A72C-4183-B549-63B649320B61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9F53E9B-A72C-4183-B549-63B649320B61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9F53E9B-A72C-4183-B549-63B649320B61}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/MultiplayerLoader/MultiplayerLoader.cs b/Source/MultiplayerLoader/MultiplayerLoader.cs new file mode 100644 index 00000000..68b06ccc --- /dev/null +++ b/Source/MultiplayerLoader/MultiplayerLoader.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Verse; + +namespace MultiplayerLoader +{ + // This class has to be named Multiplayer for backwards compatibility of the settings file location + public class Multiplayer : Mod + { + public static Multiplayer? instance; + public static Action? settingsWindowDrawer; + + public Multiplayer(ModContentPack content) : base(content) + { + instance = this; + LoadAssembliesCustom(); + + GenTypes.GetTypeInAnyAssembly("Multiplayer.Client.Multiplayer")!.GetMethod("InitMultiplayer")!.Invoke(null, null); + } + + private void LoadAssembliesCustom() + { + var assemblies = new List(); + + foreach (FileInfo item in + from f + in ModContentPack.GetAllFilesForModPreserveOrder(Content, "AssembliesCustom/", + e => e.ToLower() == ".dll") + select f.Item2) + { + Assembly assembly; + + try + { + byte[] rawAssembly = File.ReadAllBytes(item.FullName); + FileInfo fileInfo = + new FileInfo(Path.Combine(item.DirectoryName!, Path.GetFileNameWithoutExtension(item.FullName)) + + ".pdb"); + + if (fileInfo.Exists) + { + byte[] rawSymbolStore = File.ReadAllBytes(fileInfo.FullName); + assembly = AppDomain.CurrentDomain.Load(rawAssembly, rawSymbolStore); + Log.Message(""+AssemblyName.GetAssemblyName(item.FullName)); + } + else + { + assembly = AppDomain.CurrentDomain.Load(rawAssembly); + } + } + catch (Exception ex) + { + Log.Error($"Exception loading {item.Name}: {ex}"); + break; + } + + assemblies.Add(assembly); + } + + var asmResolve = (Delegate)typeof(AppDomain).GetField("AssemblyResolve", BindingFlags.Instance | BindingFlags.NonPublic) + !.GetValue(AppDomain.CurrentDomain)!; + + Assembly Resolver(object _, ResolveEventArgs args) + { + return assemblies.Concat(Content.assemblies.loadedAssemblies).FirstOrDefault(a => a.FullName == args.Name); + } + + typeof(AppDomain).GetField("AssemblyResolve", BindingFlags.Instance | BindingFlags.NonPublic) + !.SetValue(AppDomain.CurrentDomain, Delegate.Combine( + (ResolveEventHandler)Resolver, + asmResolve)); + + foreach (var asm in assemblies) + if (Content.assemblies.AssemblyIsUsable(asm)) + { + GenTypes.ClearCache(); + Content.assemblies.loadedAssemblies.Add(asm); + } + } + + public override string SettingsCategory() => "Multiplayer"; + + public override void DoSettingsWindowContents(Rect inRect) + { + settingsWindowDrawer?.Invoke(inRect); + } + } +} diff --git a/Source/MultiplayerLoader/MultiplayerLoader.csproj b/Source/MultiplayerLoader/MultiplayerLoader.csproj new file mode 100644 index 00000000..30b8b4ad --- /dev/null +++ b/Source/MultiplayerLoader/MultiplayerLoader.csproj @@ -0,0 +1,35 @@ + + + + net472 + true + 10 + false + false + MultiplayerLoader + ..\..\Assemblies\ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/MultiplayerLoader/Prepatches.cs b/Source/MultiplayerLoader/Prepatches.cs new file mode 100644 index 00000000..d9179f02 --- /dev/null +++ b/Source/MultiplayerLoader/Prepatches.cs @@ -0,0 +1,21 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; +using Multiplayer.Common; +using Prepatcher; +using RimWorld; + +namespace Multiplayer.Client; + +public static class Prepatches +{ + [FreePatch] + static void DontSetResolution(ModuleDefinition module) + { + if (MpVersion.IsDebug) + { + var utilType = module.ImportReference(typeof(ResolutionUtility)).Resolve(); + utilType.FindMethod("SetResolutionRaw").Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ret)); + } + } +} diff --git a/Source/Server/Dockerfile b/Source/Server/Dockerfile new file mode 100644 index 00000000..6410f803 --- /dev/null +++ b/Source/Server/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["Server/Server.csproj", "Server/"] +RUN dotnet restore "Server/Server.csproj" +COPY . . +WORKDIR "/src/Server" +RUN dotnet build "Server.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Server.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Server.dll"] diff --git a/Source/Server/Server.cs b/Source/Server/Server.cs new file mode 100644 index 00000000..2f545e1e --- /dev/null +++ b/Source/Server/Server.cs @@ -0,0 +1,85 @@ +using System.IO.Compression; +using Multiplayer.Common; +using Multiplayer.Common.Util; +using Server; + +ServerLog.detailEnabled = true; + +const string settingsFile = "settings.toml"; +const string stopCmd = "stop"; +const string saveFile = "save.zip"; + +var settings = new ServerSettings +{ + direct = true, + lan = false +}; + +if (File.Exists(settingsFile)) + settings = TomlSettings.Load(settingsFile); +else + TomlSettings.Save(settings, settingsFile); // Save default settings + +var server = MultiplayerServer.instance = new MultiplayerServer(settings) +{ + running = true, + initDataState = InitDataState.Waiting +}; + +var consoleSource = new ConsoleSource(); + +LoadSave(server, saveFile); +server.liteNet.StartNet(); + +new Thread(server.Run) { Name = "Server thread" }.Start(); + +while (true) +{ + var cmd = Console.ReadLine(); + if (cmd != null) + server.Enqueue(() => server.HandleChatCmd(consoleSource, cmd)); + + if (cmd == stopCmd) + break; +} + +static void LoadSave(MultiplayerServer server, string path) +{ + using var zip = ZipFile.OpenRead(path); + + var replayInfo = ReplayInfo.Read(zip.GetBytes("info")); + ServerLog.Detail($"Loading {path} saved in RW {replayInfo.rwVersion} with {replayInfo.modNames.Count} mods"); + + server.settings.gameName = replayInfo.name; + server.worldData.defaultFactionId = replayInfo.playerFaction; + + // todo this assumes only one zero-tick section and map id 0 + server.gameTimer = replayInfo.sections[0].start; + + server.worldData.savedGame = Compress(zip.GetBytes("world/000_save")); + server.worldData.mapData[0] = Compress(zip.GetBytes("maps/000_0_save")); + server.worldData.mapCmds[0] = ScheduledCommand.DeserializeCmds(zip.GetBytes("maps/000_0_cmds")).Select(ScheduledCommand.Serialize).ToList(); + server.worldData.mapCmds[-1] = ScheduledCommand.DeserializeCmds(zip.GetBytes("world/000_cmds")).Select(ScheduledCommand.Serialize).ToList(); + server.worldData.semiPersistent = Array.Empty(); +} + +static byte[] Compress(byte[] input) +{ + using var result = new MemoryStream(); + + using (var compressionStream = new GZipStream(result, CompressionMode.Compress)) + { + compressionStream.Write(input, 0, input.Length); + compressionStream.Flush(); + + } + return result.ToArray(); +} + +class ConsoleSource : IChatSource +{ + public void SendMsg(string msg) + { + ServerLog.Log(msg); + } +} diff --git a/Source/Server/Server.csproj b/Source/Server/Server.csproj new file mode 100644 index 00000000..0f262938 --- /dev/null +++ b/Source/Server/Server.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + enable + enable + Linux + + + + + .dockerignore + + + + + + + + + + diff --git a/Source/Server/TomlSettings.cs b/Source/Server/TomlSettings.cs new file mode 100644 index 00000000..cc495a2d --- /dev/null +++ b/Source/Server/TomlSettings.cs @@ -0,0 +1,72 @@ +using Multiplayer.Common; +using Tomlyn; +using Tomlyn.Model; + +namespace Server; + +public static class TomlSettings +{ + public static ServerSettings Load(string filename) + { + var toml = new TomlScribe + { + mode = TomlScribeMode.Loading, + root = Toml.ToModel(File.ReadAllText(filename)) + }; + + ScribeLike.provider = toml; + + var settings = new ServerSettings(); + settings.ExposeData(); + + return settings; + } + + public static void Save(ServerSettings settings, string filename) + { + var toml = new TomlScribe { mode = TomlScribeMode.Saving }; + ScribeLike.provider = toml; + + settings.ExposeData(); + + File.WriteAllText(filename, Toml.FromModel(toml.root)); + } +} + +class TomlScribe : ScribeLike.Provider +{ + public TomlTable root = new(); + public TomlScribeMode mode; + + public override void Look(ref T value, string label, T defaultValue, bool forceSave) + { + if (mode == TomlScribeMode.Loading) + { + if (root.ContainsKey(label)) + { + if (typeof(T).IsEnum) + value = (T)Enum.Parse(typeof(T), (string)root[label]); + else if (root[label] is IConvertible convertible) + value = (T)convertible.ToType(typeof(T), null); + else + value = (T)root[label]; + } + else + { + value = defaultValue; + } + } + else if (mode == TomlScribeMode.Saving) + { + if (typeof(T).IsEnum) + root[label] = value.ToString()!; + else + root[label] = value; + } + } +} + +enum TomlScribeMode +{ + Saving, Loading +} diff --git a/Source/Tests/Helper/TestJoiningState.cs b/Source/Tests/Helper/TestJoiningState.cs new file mode 100644 index 00000000..f9dcea3f --- /dev/null +++ b/Source/Tests/Helper/TestJoiningState.cs @@ -0,0 +1,46 @@ +using Multiplayer.Common; + +namespace Tests; + +public class TestJoiningState : AsyncConnectionState +{ + public TestJoiningState(ConnectionBase connection) : base(connection) + { + } + + private const string RwVersion = "1.0.0"; + + protected override async Task RunState() + { + connection.Send(Packets.Client_Protocol, MpVersion.Protocol); + await Packet(Packets.Server_ProtocolOk); + + connection.Send(Packets.Client_Username, connection.username!); + await Packet(Packets.Server_InitDataRequest); + + connection.Send( + Packets.Client_InitData, + Array.Empty(), + RwVersion, + Array.Empty(), + Array.Empty(), + Array.Empty() + ); + + await Packet(Packets.Server_UsernameOk); + + connection.Send( + Packets.Client_JoinData, + 0 + ); + + await Packet(Packets.Server_JoinData).Fragmented(); + + connection.Send(Packets.Client_WorldRequest); + await Packet(Packets.Server_WorldDataStart); + await Packet(Packets.Server_WorldData).Fragmented(); + + connection.Close(MpDisconnectReason.Generic); + connection.ChangeState(ConnectionStateEnum.Disconnected); + } +} diff --git a/Source/Tests/Helper/TestNetListener.cs b/Source/Tests/Helper/TestNetListener.cs new file mode 100644 index 00000000..c241e5a1 --- /dev/null +++ b/Source/Tests/Helper/TestNetListener.cs @@ -0,0 +1,54 @@ +using System.Net; +using System.Net.Sockets; +using LiteNetLib; +using Multiplayer.Common; + +namespace Tests; + +public class TestNetListener : INetEventListener +{ + public ConnectionBase? conn; + + public void OnPeerConnected(NetPeer peer) + { + conn = new LiteNetConnection(peer); + conn.username = "test1"; + conn.ChangeState(ConnectionStateEnum.ClientJoining); + + Console.WriteLine("TestNetListener: OnPeerConnected"); + } + + public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + { + Console.WriteLine($"Peer disconnected {disconnectInfo.Reason}"); + } + + public void OnNetworkError(IPEndPoint endPoint, SocketError socketError) + { + Assert.Fail($"Network error: {endPoint} {socketError}"); + } + + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod method) + { + try + { + byte[] data = reader.GetRemainingBytes(); + conn!.HandleReceiveRaw(new ByteReader(data), method == DeliveryMethod.ReliableOrdered); + } catch (Exception e) + { + ServerLog.Error($"Exception in OnNetworkReceive: {e}"); + } + } + + public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + } + + public void OnNetworkLatencyUpdate(NetPeer peer, int latency) + { + } + + public void OnConnectionRequest(ConnectionRequest request) + { + } +} diff --git a/Source/Tests/ReplayInfoTest.cs b/Source/Tests/ReplayInfoTest.cs new file mode 100644 index 00000000..3271ae5a --- /dev/null +++ b/Source/Tests/ReplayInfoTest.cs @@ -0,0 +1,22 @@ +using System.Text; +using Multiplayer.Common; + +namespace Tests; + +public class ReplayInfoTest +{ + [Test] + public void Test() + { + var replayInfo = new ReplayInfo + { + name = "test" + }; + + var xml = ReplayInfo.Write(replayInfo); + Console.WriteLine(Encoding.UTF8.GetString(xml)); + + var readInfo = ReplayInfo.Read(xml); + Assert.That(readInfo.name, Is.EqualTo(replayInfo.name)); + } +} diff --git a/Source/Tests/ServerTest.cs b/Source/Tests/ServerTest.cs new file mode 100644 index 00000000..a7356443 --- /dev/null +++ b/Source/Tests/ServerTest.cs @@ -0,0 +1,107 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using LiteNetLib; +using Multiplayer.Common; + +namespace Tests; + +public class ServerTest +{ + private List teardownActions = new(); + + [OneTimeSetUp] + public void OneTimeSetUp() + { + ServerLog.detailEnabled = true; + ServerLog.verboseEnabled = true; + ServerLog.error = Assert.Fail; + } + + [TearDown] + public void TearDown() + { + foreach (var action in teardownActions) + action(); + teardownActions.Clear(); + } + + [Test] + public void Test() + { + MpConnectionState.SetImplementation(ConnectionStateEnum.ClientJoining, typeof(TestJoiningState)); + + int port = GetFreePort(); + var server = MakeServer(port); + ConnectClient(port); + + var timeoutWatch = Stopwatch.StartNew(); + while (true) + { + if (server.initDataState == InitDataState.Complete && server.playerManager.Players.Count == 0) + break; // Success + + if (timeoutWatch.ElapsedMilliseconds > 2000) + Assert.Fail("Timeout"); + + Thread.Sleep(50); + } + } + + private void ConnectClient(int port) + { + var clientListener = new TestNetListener(); + var client = new NetManager(clientListener); + client.Start(); + var peer = client.Connect($"127.0.0.1", port, ""); + + teardownActions.Add(() => + { + client.Stop(); + }); + + Console.WriteLine($"Connected to {peer.EndPoint}"); + + new Thread(() => + { + while (true) + { + client.PollEvents(); + Thread.Sleep(50); + } + }) { IsBackground = true }.Start(); + } + + private MultiplayerServer MakeServer(int port) + { + var server = MultiplayerServer.instance = new MultiplayerServer(new ServerSettings() + { + gameName = "Test", + direct = true, + directAddress = $"127.0.0.1:{port}", + lan = false + }) + { + running = true, + initDataState = InitDataState.Waiting + }; + + server.worldData.savedGame = Array.Empty(); + + server.liteNet.StartNet(); + new Thread(server.Run) { IsBackground = true }.Start(); + + teardownActions.Add(() => { server.running = false; }); + + return server; + } + + private static int GetFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } +} diff --git a/Source/Tests/Tests.csproj b/Source/Tests/Tests.csproj new file mode 100644 index 00000000..0ec55c7f --- /dev/null +++ b/Source/Tests/Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/Source/Tests/Usings.cs b/Source/Tests/Usings.cs new file mode 100644 index 00000000..32445676 --- /dev/null +++ b/Source/Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/workshop_bundler.sh b/workshop_bundler.sh index 0e0fe2b8..2194fc54 100755 --- a/workshop_bundler.sh +++ b/workshop_bundler.sh @@ -44,7 +44,7 @@ sed -i "s/.*<\/version>\$/${VERSION}<\/version>/" About/Manife # The current version mkdir -p 1.4 -cp -r ../Assemblies ../Defs ../Languages 1.4/ +cp -r ../Assemblies ../AssembliesCustom ../Defs ../Languages 1.4/ rm -f 1.4/Languages/.git 1.4/Languages/LICENSE 1.4/Languages/README.md # Past versions