diff --git a/Defs/KeyBindings.xml b/Defs/KeyBindings.xml index 60fed326..8c98be50 100644 --- a/Defs/KeyBindings.xml +++ b/Defs/KeyBindings.xml @@ -2,13 +2,30 @@ - + + Multiplayer + + Controls used in multiplayer. + true + +
  • Development
  • +
  • Game
  • +
  • SelectionMisc
  • +
  • MainTabs
  • +
    +
    + + + Multiplayer + + + MpToggleChat Equals - + MpPingKey Keypad0 diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index ba470ab3..58fbde83 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -280,10 +280,10 @@ public void ExecuteCmd(ScheduledCommand cmd) data.Log.current.text = handler.ToString(); } - // if (cmdType == CommandType.DebugTools) - // { - // MpDebugTools.HandleCmd(data); - // } + if (cmdType == CommandType.DebugTools) + { + MpDebugTools.HandleCmd(data); + } if (cmdType == CommandType.CreateMapFactionData) { @@ -410,7 +410,7 @@ bool SetState(Designator designator, ByteReader data) Thing thing = SyncSerialization.ReadSync(data); if (thing == null) return false; - DesignatorInstallPatch.thingToInstall = thing; + DesignatorInstall_SetThingToInstall.thingToInstall = thing; } if (designator is Designator_Zone) @@ -428,7 +428,7 @@ void RestoreState() if (prevArea.HasValue) Designator_AreaAllowed.selectedArea = prevArea.Value.Inner; - DesignatorInstallPatch.thingToInstall = null; + DesignatorInstall_SetThingToInstall.thingToInstall = null; } try diff --git a/Source/Client/AsyncTime/AsyncTimePatches.cs b/Source/Client/AsyncTime/AsyncTimePatches.cs index c61ee758..11785814 100644 --- a/Source/Client/AsyncTime/AsyncTimePatches.cs +++ b/Source/Client/AsyncTime/AsyncTimePatches.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Reflection.Emit; using System.Text; using System.Threading.Tasks; using Multiplayer.Client.Util; @@ -602,4 +603,34 @@ static void Postfix(Map __state) } } } + + [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), typeof(Letter), typeof(string))] + static class ReceiveLetterPause + { + static IEnumerable Transpiler(IEnumerable insts) + { + foreach (var inst in insts) + { + if (inst.operand == AccessTools.PropertyGetter(typeof(Prefs), nameof(Prefs.AutomaticPauseMode))) + inst.operand = AccessTools.Method(typeof(ReceiveLetterPause), nameof(AutomaticPauseMode)); + else if (inst.operand == AccessTools.Method(typeof(TickManager), nameof(TickManager.Pause))) + inst.operand = AccessTools.Method(typeof(ReceiveLetterPause), nameof(PauseOnLetter)); + + yield return inst; + } + } + + static AutomaticPauseMode AutomaticPauseMode() + { + return (AutomaticPauseMode) Multiplayer.GameComp.pauseOnLetter; + } + + static void PauseOnLetter(TickManager manager) + { + if (Multiplayer.GameComp.asyncTime) + ((ITickable) Multiplayer.MapContext.AsyncTime() ?? Multiplayer.WorldComp).TimeSpeed = TimeSpeed.Paused; + else + Multiplayer.WorldComp.SetTimeEverywhere(TimeSpeed.Paused); + } + } } diff --git a/Source/Client/AsyncTime/ITickable.cs b/Source/Client/AsyncTime/ITickable.cs index 5600b328..7ecb78d3 100644 --- a/Source/Client/AsyncTime/ITickable.cs +++ b/Source/Client/AsyncTime/ITickable.cs @@ -11,7 +11,7 @@ public interface ITickable float RealTimeToTickThrough { get; set; } - TimeSpeed TimeSpeed { get; } + TimeSpeed TimeSpeed { get; set; } Queue Cmds { get; } diff --git a/Source/Client/Comp/MultiplayerGameComp.cs b/Source/Client/Comp/MultiplayerGameComp.cs index 937fae3e..7ed9ff88 100644 --- a/Source/Client/Comp/MultiplayerGameComp.cs +++ b/Source/Client/Comp/MultiplayerGameComp.cs @@ -11,6 +11,7 @@ public class MultiplayerGameComp : IExposable, IHasSemiPersistentData public bool asyncTime; public bool debugMode; public bool logDesyncTraces; + public PauseOnLetter pauseOnLetter; public Dictionary playerData = new(); // player id to player data public IdBlock globalIdBlock = new(int.MaxValue / 2, 1_000_000_000); @@ -24,6 +25,7 @@ public void ExposeData() Scribe_Values.Look(ref asyncTime, "asyncTime", true, true); Scribe_Values.Look(ref debugMode, "debugMode"); Scribe_Values.Look(ref logDesyncTraces, "logDesyncTraces"); + Scribe_Values.Look(ref pauseOnLetter, "pauseOnLetter"); Scribe_Custom.LookIdBlock(ref globalIdBlock, "globalIdBlock"); diff --git a/Source/Client/Comp/MultiplayerWorldComp.cs b/Source/Client/Comp/MultiplayerWorldComp.cs index a764abd3..08af2d3f 100644 --- a/Source/Client/Comp/MultiplayerWorldComp.cs +++ b/Source/Client/Comp/MultiplayerWorldComp.cs @@ -232,6 +232,13 @@ public void SetFaction(Faction faction) SyncResearch.researchSpeed = data.researchSpeed; } + public void SetTimeEverywhere(TimeSpeed speed) + { + TimeSpeed = speed; + foreach (var map in Find.Maps) + map.AsyncTime().TimeSpeed = speed; + } + public static float lastSpeedChange; public void ExecuteCmd(ScheduledCommand cmd) @@ -260,10 +267,10 @@ public void ExecuteCmd(ScheduledCommand cmd) data.Log.current.text = handler.ToString(); } - // if (cmdType == CommandType.DebugTools) - // { - // MpDebugTools.HandleCmd(data); - // } + if (cmdType == CommandType.DebugTools) + { + MpDebugTools.HandleCmd(data); + } if (cmdType == CommandType.WorldTimeSpeed) { @@ -273,14 +280,20 @@ public void ExecuteCmd(ScheduledCommand cmd) if (!Multiplayer.GameComp.asyncTime) { - foreach (var map in Find.Maps) - map.AsyncTime().TimeSpeed = speed; + SetTimeEverywhere(speed); if (!cmd.issuedBySelf) lastSpeedChange = Time.realtimeSinceStartup; } +#if DEBUG MpLog.Log($"Set world speed {speed} {TickPatch.Timer} {Find.TickManager.TicksGame}"); +#endif + } + + if (cmdType == CommandType.PauseAll) + { + SetTimeEverywhere(TimeSpeed.Paused); } if (cmdType == CommandType.SetupFaction) @@ -327,8 +340,10 @@ public void ExecuteCmd(ScheduledCommand cmd) DebugSettings.godMode = prevGodMode; Prefs.data.devMode = prevDevMode; +#if DEBUG Log.Message($"rand calls {DeferredStackTracing.randCalls - randCalls1}"); Log.Message("rand state " + Rand.StateCompressed); +#endif Extensions.PopFaction(); PostContext(); diff --git a/Source/Client/Debug/DebugActions.cs b/Source/Client/Debug/DebugActions.cs index ab472133..7fe43f2b 100644 --- a/Source/Client/Debug/DebugActions.cs +++ b/Source/Client/Debug/DebugActions.cs @@ -17,7 +17,7 @@ namespace Multiplayer.Client { [HotSwappable] - static class MPDebugActions + static class MpDebugActions { const string MultiplayerCategory = "Multiplayer"; diff --git a/Source/Client/Debug/DebugPatches.cs b/Source/Client/Debug/DebugPatches.cs index 8946b099..511ac778 100644 --- a/Source/Client/Debug/DebugPatches.cs +++ b/Source/Client/Debug/DebugPatches.cs @@ -74,7 +74,7 @@ static class GrammarRandomStringPatch static void Postfix(ref string __result) { if (SetupQuickTestPatch.marker) - __result = "multiplayer1"; + __result = "multiplayer"; } } diff --git a/Source/Client/Debug/DebugTools.cs b/Source/Client/Debug/DebugTools.cs index a390b886..4fed219c 100644 --- a/Source/Client/Debug/DebugTools.cs +++ b/Source/Client/Debug/DebugTools.cs @@ -1,409 +1,274 @@ -// using System; -// using System.Collections.Generic; -// using System.Linq; -// using System.Reflection; -// -// using HarmonyLib; -// using Multiplayer.Common; -// -// using RimWorld; -// using RimWorld.Planet; -// using Verse; -// -// namespace Multiplayer.Client -// { -// static class MpDebugTools -// { -// private static int currentPlayer; -// public static int currentHash; -// -// public static PlayerDebugState CurrentPlayerState => -// Multiplayer.game.playerDebugState.GetOrAddNew(currentPlayer == -1 ? Multiplayer.session.playerId : currentPlayer); -// -// public static void HandleCmd(ByteReader data) -// { -// currentPlayer = data.ReadInt32(); -// var source = (DebugSource)data.ReadInt32(); -// int cursorX = data.ReadInt32(); -// int cursorZ = data.ReadInt32(); -// -// if (Multiplayer.MapContext != null) -// MouseCellPatch.result = new IntVec3(cursorX, 0, cursorZ); -// else -// MouseTilePatch.result = cursorX; -// -// currentHash = data.ReadInt32(); -// var state = Multiplayer.game.playerDebugState.GetOrAddNew(currentPlayer); -// -// var prevTool = DebugTools.curTool; -// DebugTools.curTool = state.tool; -// -// List prevSelected = Find.Selector.selected; -// List prevWorldSelected = Find.WorldSelector.selected; -// -// Find.Selector.selected = new List(); -// Find.WorldSelector.selected = new List(); -// -// int selectedId = data.ReadInt32(); -// -// if (Multiplayer.MapContext != null) -// { -// var thing = ThingsById.thingsById.GetValueSafe(selectedId); -// if (thing != null) -// Find.Selector.selected.Add(thing); -// } -// else -// { -// var obj = Find.WorldObjects.AllWorldObjects.FirstOrDefault(w => w.ID == selectedId); -// if (obj != null) -// Find.WorldSelector.selected.Add(obj); -// } -// -// Log.Message($"Debug tool {source} ({cursorX}, {cursorZ}) {currentHash}"); -// -// try -// { -// if (source == DebugSource.ListingMap) -// { -// new Dialog_DebugActionsMenu().DoListingItems(); -// } -// else if (source == DebugSource.ListingWorld) -// { -// new Dialog_DebugActionsMenu().DoListingItems(); -// } -// else if (source == DebugSource.ListingPlay) -// { -// new Dialog_DebugActionsMenu().DoListingItems(); -// } -// else if (source == DebugSource.Lister) -// { -// var options = (state.window as List) ?? new List(); -// new Dialog_DebugOptionListLister(options).DoListingItems(); -// } -// else if (source == DebugSource.Tool) -// { -// DebugTools.curTool?.clickAction(); -// } -// else if (source == DebugSource.FloatMenu) -// { -// (state.window as List)?.FirstOrDefault(o => o.Hash() == currentHash)?.action(); -// } -// } -// finally -// { -// if (TickPatch.currentExecutingCmdIssuedBySelf && DebugTools.curTool != null && DebugTools.curTool != state.tool) -// { -// var map = Multiplayer.MapContext; -// prevTool = new DebugTool(DebugTools.curTool.label, () => -// { -// SendCmd(DebugSource.Tool, 0, map); -// }, DebugTools.curTool.onGUIAction); -// } -// -// state.tool = DebugTools.curTool; -// DebugTools.curTool = prevTool; -// -// MouseCellPatch.result = null; -// MouseTilePatch.result = null; -// Find.Selector.selected = prevSelected; -// Find.WorldSelector.selected = prevWorldSelected; -// -// currentHash = 0; -// currentPlayer = -1; -// } -// } -// -// public static void SendCmd(DebugSource source, int hash, Map map) -// { -// var writer = new LoggingByteWriter(); -// writer.Log.Node($"Debug tool {source}, map {map.ToStringSafe()}"); -// int cursorX = 0, cursorZ = 0; -// -// if (map != null) -// { -// cursorX = UI.MouseCell().x; -// cursorZ = UI.MouseCell().z; -// } -// else -// { -// cursorX = GenWorld.MouseTile(false); -// } -// -// writer.WriteInt32(Multiplayer.session.playerId); -// writer.WriteInt32((int)source); -// writer.WriteInt32(cursorX); -// writer.WriteInt32(cursorZ); -// writer.WriteInt32(hash); -// -// if (map != null) -// writer.WriteInt32(Find.Selector.SingleSelectedThing?.thingIDNumber ?? -1); -// else -// writer.WriteInt32(Find.WorldSelector.SingleSelectedObject?.ID ?? -1); -// -// Multiplayer.WriterLog.AddCurrentNode(writer); -// -// var mapId = map?.uniqueID ?? ScheduledCommand.Global; -// Multiplayer.Client.SendCommand(CommandType.DebugTools, mapId, writer.ToArray()); -// } -// -// public static DebugSource ListingSource() -// { -// if (ListingWorldMarker.drawing) -// return DebugSource.ListingWorld; -// else if (ListingMapMarker.drawing) -// return DebugSource.ListingMap; -// else if (ListingPlayMarker.drawing) -// return DebugSource.ListingPlay; -// -// return DebugSource.None; -// } -// } -// -// public class PlayerDebugState -// { -// public object window; -// public DebugTool tool; -// -// public Map Map => Find.Maps.Find(m => m.uniqueID == mapId); -// public int mapId; -// } -// -// public enum DebugSource -// { -// None, -// ListingWorld, -// ListingMap, -// ListingPlay, -// Lister, -// Tool, -// FloatMenu, -// } -// -// [HarmonyPatch(typeof(Dialog_DebugActionsMenu), nameof(Dialog_DebugActionsMenu.DoListingItems))] -// static class ListingPlayMarker -// { -// public static bool drawing; -// -// [HarmonyPriority(MpPriority.MpFirst)] -// static void Prefix() => drawing = true; -// -// [HarmonyPriority(MpPriority.MpLast)] -// static void Postfix() => drawing = false; -// } -// -// [HarmonyPatch(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugToolWorld))] -// static class ListingWorldMarker -// { -// public static bool drawing; -// -// [HarmonyPriority(MpPriority.MpFirst)] -// static void Prefix() => drawing = true; -// -// [HarmonyPriority(MpPriority.MpLast)] -// static void Postfix() => drawing = false; -// } -// -// [HarmonyPatch] -// static class ListingIncidentMarker -// { -// public static IIncidentTarget target; -// -// static IEnumerable TargetMethods() -// { -// yield return AccessTools.Method(typeof(DebugActionsIncidents), nameof(DebugActionsIncidents.DoIncidentDebugAction)); -// yield return AccessTools.Method(typeof(DebugActionsIncidents), nameof(DebugActionsIncidents.DoIncidentWithPointsAction)); -// } -// -// [HarmonyPriority(MpPriority.MpFirst)] -// static void Prefix(IIncidentTarget target) => ListingIncidentMarker.target = target; -// -// [HarmonyPriority(MpPriority.MpLast)] -// static void Postfix() => target = null; -// } -// -// [HarmonyPatch] -// static class ListingMapMarker -// { -// public static bool drawing; -// -// static IEnumerable TargetMethods() -// { -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugToolMap)); -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugToolMapForPawns)); -// } -// -// [HarmonyPriority(MpPriority.MpFirst)] -// static void Prefix() => drawing = true; -// -// [HarmonyPriority(MpPriority.MpLast)] -// static void Postfix() => drawing = false; -// } -// -// [HarmonyPatch] -// static class CancelDebugDrawing -// { -// static IEnumerable TargetMethods() -// { -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DoGap)); -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DoLabel)); -// } -// -// static bool Prefix() => !Multiplayer.ExecutingCmds; -// } -// -// [HarmonyPatch(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugAction))] -// static class DebugActionPatch -// { -// static bool Prefix(Dialog_DebugOptionLister __instance, string label, ref Action action) -// { -// if (Multiplayer.Client == null) return true; -// if (Current.ProgramState == ProgramState.Playing && !Multiplayer.GameComp.debugMode) return true; -// -// int hash = Gen.HashCombineInt( -// GenText.StableStringHash(action.Method.MethodDesc()), -// GenText.StableStringHash(label) -// ); -// -// if (Multiplayer.ExecutingCmds) -// { -// if (hash == MpDebugTools.currentHash) -// action(); -// -// return false; -// } -// -// if (__instance is Dialog_DebugActionsMenu) -// { -// var source = MpDebugTools.ListingSource(); -// if (source == DebugSource.None) return true; -// -// Map map = source == DebugSource.ListingMap ? Find.CurrentMap : null; -// -// if (ListingIncidentMarker.target != null) -// map = ListingIncidentMarker.target as Map; -// -// action = () => MpDebugTools.SendCmd(source, hash, map); -// } -// -// if (__instance is Dialog_DebugOptionListLister) -// { -// action = () => MpDebugTools.SendCmd(DebugSource.Lister, hash, MpDebugTools.CurrentPlayerState.Map); -// } -// -// return true; -// } -// } -// -// [HarmonyPatch] -// static class DebugToolPatch -// { -// static IEnumerable TargetMethods() -// { -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugToolMap)); -// yield return AccessTools.Method(typeof(Dialog_DebugOptionLister), nameof(Dialog_DebugOptionLister.DebugToolWorld)); -// } -// -// static bool Prefix(Dialog_DebugOptionLister __instance, string label, Action toolAction, ref Container? __state) -// { -// if (Multiplayer.Client == null) return true; -// if (Current.ProgramState == ProgramState.Playing && !Multiplayer.GameComp.debugMode) return true; -// -// if (Multiplayer.ExecutingCmds) -// { -// int hash = Gen.HashCombineInt(GenText.StableStringHash(toolAction.Method.MethodDesc()), GenText.StableStringHash(label)); -// if (hash == MpDebugTools.currentHash) -// DebugTools.curTool = new DebugTool(label, toolAction); -// -// return false; -// } -// -// __state = DebugTools.curTool; -// -// return true; -// } -// -// static void Postfix(Dialog_DebugOptionLister __instance, string label, Action toolAction, Container? __state) -// { -// // New tool chosen -// if (!__state.HasValue || DebugTools.curTool == __state?.Inner) -// { -// return; -// } -// -// int hash = Gen.HashCombineInt(GenText.StableStringHash(toolAction.Method.MethodDesc()), GenText.StableStringHash(label)); -// -// if (__instance is Dialog_DebugActionsMenu) -// { -// var source = MpDebugTools.ListingSource(); -// if (source == DebugSource.None) return; -// -// Map map = source == DebugSource.ListingMap ? Find.CurrentMap : null; -// -// MpDebugTools.SendCmd(source, hash, map); -// DebugTools.curTool = null; -// } -// -// else if (__instance is Dialog_DebugOptionListLister lister) -// { -// Map map = MpDebugTools.CurrentPlayerState.Map; -// if (ListingMapMarker.drawing) -// { -// map = Find.CurrentMap; -// } -// MpDebugTools.SendCmd(DebugSource.Lister, hash, map); -// DebugTools.curTool = null; -// } -// } -// } -// -// [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.Add))] -// static class DebugListerAddPatch -// { -// static bool Prefix(Window window) -// { -// if (Multiplayer.Client == null) return true; -// if (!Multiplayer.ExecutingCmds) return true; -// if (!Multiplayer.GameComp.debugMode) return true; -// -// bool keepOpen = TickPatch.currentExecutingCmdIssuedBySelf; -// var map = Multiplayer.MapContext; -// -// if (window is Dialog_DebugOptionListLister lister) -// { -// MpDebugTools.CurrentPlayerState.window = lister.options; -// MpDebugTools.CurrentPlayerState.mapId = map?.uniqueID ?? -1; -// -// return keepOpen; -// } -// -// if (window is FloatMenu menu) -// { -// var options = menu.options; -// -// if (keepOpen) -// { -// menu.options = new List(); -// -// foreach (var option in options) -// { -// var copy = new FloatMenuOption(option.labelInt, option.action); -// int hash = copy.Hash(); -// copy.action = () => MpDebugTools.SendCmd(DebugSource.FloatMenu, hash, map); -// menu.options.Add(copy); -// } -// } -// -// MpDebugTools.CurrentPlayerState.window = options; -// return keepOpen; -// } -// -// return true; -// } -// -// public static int Hash(this FloatMenuOption opt) -// { -// return Gen.HashCombineInt(GenText.StableStringHash(opt.action.Method.MethodDesc()), GenText.StableStringHash(opt.labelInt)); -// } -// } -// -// } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using HarmonyLib; +using Multiplayer.Common; + +using RimWorld; +using RimWorld.Planet; +using Verse; + +namespace Multiplayer.Client +{ + static class MpDebugTools + { + private static int currentPlayer; + public static int currentHash; + + public static PlayerDebugState CurrentPlayerState => + Multiplayer.game.playerDebugState.GetOrAddNew(currentPlayer == -1 ? Multiplayer.session.playerId : currentPlayer); + + public static void HandleCmd(ByteReader data) + { + currentPlayer = data.ReadInt32(); + var source = (DebugSource)data.ReadInt32(); + int cursorX = data.ReadInt32(); + int cursorZ = data.ReadInt32(); + + if (Multiplayer.MapContext != null) + MouseCellPatch.result = new IntVec3(cursorX, 0, cursorZ); + else + MouseTilePatch.result = cursorX; + + currentHash = data.ReadInt32(); + var path = data.ReadString(); + + var state = Multiplayer.game.playerDebugState.GetOrAddNew(currentPlayer); + + var prevTool = DebugTools.curTool; + DebugTools.curTool = state.tool; + + List prevSelected = Find.Selector.selected; + List prevWorldSelected = Find.WorldSelector.selected; + + Find.Selector.selected = new List(); + Find.WorldSelector.selected = new List(); + + int selectedId = data.ReadInt32(); + + if (Multiplayer.MapContext != null) + { + var thing = ThingsById.thingsById.GetValueSafe(selectedId); + if (thing != null) + Find.Selector.selected.Add(thing); + } + else + { + var obj = Find.WorldObjects.AllWorldObjects.FirstOrDefault(w => w.ID == selectedId); + if (obj != null) + Find.WorldSelector.selected.Add(obj); + } + + Log.Message($"Debug tool {source} ({cursorX}, {cursorZ}) {currentHash} {path}"); + + try + { + if (source == DebugSource.Tree) + { + var node = Dialog_Debug.GetNode(path); + if (node != null) + { + node.Enter(null); + if (node.actionType > DebugActionType.Action) + { + DebugTools.curTool.clickAction(); + DebugTools.curTool = null; + } + } + } + else if (source == DebugSource.Lister) + { + var options = state.currentData as List ?? new List(); + options.FirstOrDefault(o => o.Hash() == currentHash).method?.Invoke(); + } + else if (source == DebugSource.Tool) + { + DebugTools.curTool?.clickAction(); + } + else if (source == DebugSource.FloatMenu) + { + (state.currentData as List)?.FirstOrDefault(o => o.Hash() == currentHash)?.action(); + } + } + finally + { + if (TickPatch.currentExecutingCmdIssuedBySelf && DebugTools.curTool != null && DebugTools.curTool != state.tool) + { + var map = Multiplayer.MapContext; + prevTool = new DebugTool(DebugTools.curTool.label, () => + { + SendCmd(DebugSource.Tool, 0, null, map); + }, DebugTools.curTool.onGUIAction); + } + + state.tool = DebugTools.curTool; + DebugTools.curTool = prevTool; + + MouseCellPatch.result = null; + MouseTilePatch.result = null; + Find.Selector.selected = prevSelected; + Find.WorldSelector.selected = prevWorldSelected; + + currentHash = 0; + currentPlayer = -1; + } + } + + public static void SendCmd(DebugSource source, int hash, string path, Map map) + { + var writer = new LoggingByteWriter(); + writer.Log.Node($"Debug tool {source}, map {map.ToStringSafe()}"); + int cursorX = 0, cursorZ = 0; + + if (map != null) + { + cursorX = UI.MouseCell().x; + cursorZ = UI.MouseCell().z; + } + else + { + cursorX = GenWorld.MouseTile(false); + } + + writer.WriteInt32(Multiplayer.session.playerId); + writer.WriteInt32((int)source); + writer.WriteInt32(cursorX); + writer.WriteInt32(cursorZ); + writer.WriteInt32(hash); + writer.WriteString(path); + + if (map != null) + writer.WriteInt32(Find.Selector.SingleSelectedThing?.thingIDNumber ?? -1); + else + writer.WriteInt32(Find.WorldSelector.SingleSelectedObject?.ID ?? -1); + + Multiplayer.WriterLog.AddCurrentNode(writer); + + var mapId = map?.uniqueID ?? ScheduledCommand.Global; + Multiplayer.Client.SendCommand(CommandType.DebugTools, mapId, writer.ToArray()); + } + } + + public class PlayerDebugState + { + public object currentData; + public DebugTool tool; + } + + public enum DebugSource + { + Tree, + Lister, + Tool, + FloatMenu, + } + + [HarmonyPatch(typeof(DebugActionNode), nameof(DebugActionNode.Enter))] + static class DebugActionNodeEnter + { + static void Prefix(DebugActionNode __instance) + { + if (__instance.action is {Target: not MpDebugAction}) + __instance.action = new MpDebugAction { node = __instance, original = __instance.action }.Action; + } + + static void Postfix(DebugActionNode __instance) + { + // Other actionTypes get handled by the Prefix + if (__instance.actionType == DebugActionType.ToolMapForPawns) + DebugTools.curTool.clickAction = new MpDebugAction { node = __instance, original = DebugTools.curTool.clickAction }.Action; + } + + class MpDebugAction + { + public DebugActionNode node; + public Action original; + + public void Action() + { + if (Multiplayer.ExecutingCmds) + original(); + else + MpDebugTools.SendCmd( + DebugSource.Tree, + 0, + node.Path, + WorldRendererUtility.WorldRenderedNow ? null : Find.CurrentMap + ); + } + } + } + + [HarmonyPatch(typeof(WindowStack), nameof(WindowStack.Add))] + static class DebugListerAddPatch + { + static bool Prefix(Window window) + { + if (Multiplayer.Client == null) return true; + if (!Multiplayer.ExecutingCmds) return true; + if (!Multiplayer.GameComp.debugMode) return true; + + bool keepOpen = TickPatch.currentExecutingCmdIssuedBySelf; + var map = Multiplayer.MapContext; + + if (window is Dialog_DebugOptionListLister lister) + { + var origOptions = lister.options; + + if (keepOpen) + { + lister.options = new List(); + + foreach (var opt in origOptions) + { + int hash = opt.Hash(); + lister.options.Add(new DebugMenuOption( + opt.label, + opt.mode, + () => MpDebugTools.SendCmd(DebugSource.Lister, hash, null, map) + )); + } + } + + MpDebugTools.CurrentPlayerState.currentData = origOptions; + return keepOpen; + } + + if (window is FloatMenu menu) + { + var origOptions = menu.options; + + if (keepOpen) + { + menu.options = new List(); + + foreach (var option in origOptions) + { + var copy = new FloatMenuOption(option.labelInt, option.action); + int hash = copy.Hash(); + copy.action = () => MpDebugTools.SendCmd(DebugSource.FloatMenu, hash, null, map); + menu.options.Add(copy); + } + } + + MpDebugTools.CurrentPlayerState.currentData = origOptions; + return keepOpen; + } + + return true; + } + + public static int Hash(this FloatMenuOption opt) + { + return Gen.HashCombineInt(GenText.StableStringHash(opt.action.Method.MethodDesc()), GenText.StableStringHash(opt.labelInt)); + } + + public static int Hash(this DebugMenuOption opt) + { + return Gen.HashCombineInt(GenText.StableStringHash(opt.method.Method.MethodDesc()), GenText.StableStringHash(opt.label)); + } + } + +} diff --git a/Source/Client/Multiplayer.cs b/Source/Client/Multiplayer.cs index f0fd2d57..c67b66cf 100644 --- a/Source/Client/Multiplayer.cs +++ b/Source/Client/Multiplayer.cs @@ -292,6 +292,12 @@ private static void CheckInterfaceVersions() } } + public static void StopMultiplayerAndClearAllWindows() + { + StopMultiplayer(); + MpUI.ClearWindowStack(); + } + public static void StopMultiplayer() { Log.Message($"Stopping multiplayer session from {new StackTrace().GetFrame(1).GetMethod().FullDescription()}"); diff --git a/Source/Client/MultiplayerGame.cs b/Source/Client/MultiplayerGame.cs index 480a2f6a..b23b1c4d 100644 --- a/Source/Client/MultiplayerGame.cs +++ b/Source/Client/MultiplayerGame.cs @@ -23,7 +23,7 @@ public class MultiplayerGame private Faction myFaction; public Faction myFactionLoading; - // public Dictionary playerDebugState = new(); + public Dictionary playerDebugState = new(); public Faction RealPlayerFaction { diff --git a/Source/Client/MultiplayerSession.cs b/Source/Client/MultiplayerSession.cs index 1e4d17c6..99ae37f5 100644 --- a/Source/Client/MultiplayerSession.cs +++ b/Source/Client/MultiplayerSession.cs @@ -178,6 +178,7 @@ public void ProcessDisconnectPacket(MpDisconnectReason reason, byte[] data) if (reason == MpDisconnectReason.ServerStarting) titleKey = "MpDisconnectServerStarting"; if (reason == MpDisconnectReason.Kick) titleKey = "MpKicked"; if (reason == MpDisconnectReason.ServerPacketRead) descKey = "MpPacketErrorRemote"; + if (reason == MpDisconnectReason.BadGamePassword) descKey = "MpBadGamePassword"; disconnectInfo.titleTranslated ??= titleKey?.Translate(); disconnectInfo.descTranslated ??= descKey?.Translate(); diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 672a357c..b6ba7be7 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -269,7 +269,7 @@ void LogError(string str) MethodInfo prefix = AccessTools.Method(typeof(DesignatorPatches), m); try { - harmony.PatchMeasure(method, new HarmonyMethod(prefix), null, null, new HarmonyMethod(designatorFinalizer)); + harmony.PatchMeasure(method, new HarmonyMethod(prefix) { priority = MpPriority.MpFirst }, null, null, new HarmonyMethod(designatorFinalizer)); } catch (Exception e) { LogError($"FAIL: {t.FullName}:{method.Name} with {e}"); } diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 6f44993c..3124c4a6 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -96,6 +96,7 @@ public static void HostServer(ServerSettings settings, bool fromReplay, bool had Multiplayer.GameComp.asyncTime = asyncTime; Multiplayer.GameComp.debugMode = settings.debugMode; Multiplayer.GameComp.logDesyncTraces = settings.desyncTraces; + Multiplayer.GameComp.pauseOnLetter = settings.pauseOnLetter; LongEventHandler.QueueLongEvent(() => { diff --git a/Source/Client/Networking/JoinData.cs b/Source/Client/Networking/JoinData.cs index d7180cd6..996ab4ce 100644 --- a/Source/Client/Networking/JoinData.cs +++ b/Source/Client/Networking/JoinData.cs @@ -19,6 +19,9 @@ namespace Multiplayer.Client [HotSwappable] public static class JoinData { + public static List activeModsSnapshot; + public static ModFileDict modFilesSnapshot; + public static byte[] WriteServerData(bool writeConfigs) { var data = new ByteWriter(); @@ -88,7 +91,6 @@ public static void ReadServerData(byte[] compressedData, RemoteData remoteInfo) { var relPath = data.ReadString(); var hash = data.ReadInt32(); - //hash++;// todo for testing string absPath = null; if (mod != null) @@ -111,7 +113,7 @@ public static void ReadServerData(byte[] compressedData, RemoteData remoteInfo) var contents = data.ReadString(MaxConfigContentLen); remoteInfo.remoteModConfigs.Add(new ModConfig(modId, fileName, contents)); - //remoteInfo.remoteModConfigs[trimmedPath] = remoteInfo.remoteModConfigs[trimmedPath].Insert(0, "a"); // todo for testing + //remoteInfo.remoteModConfigs[trimmedPath] = remoteInfo.remoteModConfigs[trimmedPath].Insert(0, "a"); // for testing } } } @@ -209,9 +211,6 @@ public static bool CompareToLocal(RemoteData remote) (!remote.hasConfigs || remote.remoteModConfigs.EqualAsSets(GetSyncableConfigContents(remote.RemoteModIds))); } - public static List activeModsSnapshot; - public static ModFileDict modFilesSnapshot; - internal static void TakeModDataSnapshot() { activeModsSnapshot = ModsConfig.ActiveModsInLoadOrder.ToList(); @@ -313,6 +312,7 @@ public enum ModListDiff public record ModConfig(string ModId, string FileName, string Contents); + [HotSwappable] public class ModFileDict : IEnumerable>> { // Mod id => (path => file) @@ -331,7 +331,7 @@ public void Add(string mod, ModFile file){ public bool DictsEqual(ModFileDict other) { return files.Keys.EqualAsSets(other.files.Keys) && - files.All(kv => kv.Value.Values.EqualAsSets(kv.Value.Values)); + files.All(kv => kv.Value.Values.EqualAsSets(other.files[kv.Key].Values)); } public IEnumerator>> GetEnumerator() @@ -380,6 +380,21 @@ public ModFile(string absPath, string relPath, int hash) this.relPath = relPath.NormalizePath(); this.hash = hash; } + + public bool Equals(ModFile other) + { + return relPath == other.relPath && hash == other.hash; + } + + public override bool Equals(object obj) + { + return obj is ModFile other && Equals(other); + } + + public override int GetHashCode() + { + return Gen.HashCombineInt(relPath.GetHashCode(), hash); + } } [HarmonyPatch(typeof(ModLister), nameof(ModLister.RebuildModList))] diff --git a/Source/Client/Networking/State/ClientJoiningState.cs b/Source/Client/Networking/State/ClientJoiningState.cs index 0ec7d33e..4d9b1faf 100644 --- a/Source/Client/Networking/State/ClientJoiningState.cs +++ b/Source/Client/Networking/State/ClientJoiningState.cs @@ -25,7 +25,6 @@ public class ClientJoiningState : ClientBaseState public ClientJoiningState(ConnectionBase connection) : base(connection) { - } public override void StartState() @@ -37,7 +36,20 @@ public override void StartState() [PacketHandler(Packets.Server_ProtocolOk)] public void HandleProtocolOk(ByteReader data) { - connection.Send(Packets.Client_Username, Multiplayer.username); + bool hasPassword = data.ReadBool(); + + if (hasPassword) + { + // Delay showing the window for better UX + OnMainThread.Schedule(() => Find.WindowStack.Add(new GamePasswordWindow + { + returnToServerBrowser = Find.WindowStack.WindowOfType().returnToServerBrowser + }), 0.3f); + } + else + { + connection.Send(Packets.Client_Username, Multiplayer.username); + } } [PacketHandler(Packets.Server_UsernameOk)] @@ -87,6 +99,7 @@ public void HandleJoinData(ByteReader data) JoinData.ReadServerData(data.ReadPrefixedBytes(), remoteInfo); + // Delay showing the window for better UX OnMainThread.Schedule(Complete, 0.3f); void Complete() @@ -98,10 +111,7 @@ void Complete() } if (defDiff) - Multiplayer.StopMultiplayer(); - - var connectingWindow = Find.WindowStack.WindowOfType(); - MpUI.ClearWindowStack(); + Multiplayer.StopMultiplayerAndClearAllWindows(); var defDiffStr = "\n\n" + MultiplayerData.localDefInfos .Where(kv => kv.Value.status != DefCheckStatus.Ok) @@ -110,11 +120,7 @@ void Complete() Find.WindowStack.Add(new JoinDataWindow(remoteInfo){ connectAnywayDisabled = defDiff ? "MpMismatchDefsDiff".Translate() + defDiffStr : null, - connectAnywayCallback = () => - { - Find.WindowStack.Add(connectingWindow); - StartDownloading(); - } + connectAnywayCallback = StartDownloading }); void StartDownloading() diff --git a/Source/Client/Patches/Designators.cs b/Source/Client/Patches/Designators.cs index 95b75678..cddb2aa5 100644 --- a/Source/Client/Patches/Designators.cs +++ b/Source/Client/Patches/Designators.cs @@ -48,7 +48,7 @@ public static bool DesignateMultiCell(Designator __instance, IEnumerable !Cancel; + static bool Prefix() + { + return !Cancel; + } } [HarmonyPatch(typeof(Thing), nameof(Thing.DeSpawn))] @@ -145,4 +149,28 @@ static bool Prefix() => TickPatch.currentExecutingCmdIssuedBySelf; } + [HarmonyPatch] + static class NoCameraJumpingDuringSimulating + { + static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TrySelect)); + yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TryJumpAndSelect)); + yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TryJump), new[] {typeof(GlobalTargetInfo), typeof(CameraJumper.MovementMode)}); + } + static bool Prefix() => !TickPatch.Simulating; + } + + [HarmonyPatch(typeof(Selector), nameof(Selector.Deselect))] + static class SelectorDeselectPatch + { + public static List deselected; + + static void Prefix(object obj) + { + if (deselected != null) + deselected.Add(obj); + } + } + } diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index b90dda88..503f68cb 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -413,30 +413,6 @@ static class NoNamingInMultiplayer static bool Prefix() => Multiplayer.Client == null; } - [HarmonyPatch] - static class NoCameraJumpingDuringSimulating - { - static IEnumerable TargetMethods() - { - yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TrySelect)); - yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TryJumpAndSelect)); - yield return AccessTools.Method(typeof(CameraJumper), nameof(CameraJumper.TryJump), new[] {typeof(GlobalTargetInfo), typeof(CameraJumper.MovementMode)}); - } - static bool Prefix() => !TickPatch.Simulating; - } - - [HarmonyPatch(typeof(Selector), nameof(Selector.Deselect))] - static class SelectorDeselectPatch - { - public static List deselected; - - static void Prefix(object obj) - { - if (deselected != null) - deselected.Add(obj); - } - } - [HarmonyPatch(typeof(DirectXmlSaver), nameof(DirectXmlSaver.XElementFromObject), typeof(object), typeof(Type), typeof(string), typeof(FieldInfo), typeof(bool))] static class ExtendDirectXmlSaver { diff --git a/Source/Client/Patches/TickPatch.cs b/Source/Client/Patches/TickPatch.cs index fa020046..ed459502 100644 --- a/Source/Client/Patches/TickPatch.cs +++ b/Source/Client/Patches/TickPatch.cs @@ -125,7 +125,8 @@ static ITickable CurrentTickable() { if (WorldRendererUtility.WorldRenderedNow) return Multiplayer.WorldComp; - else if (Find.CurrentMap != null) + + if (Find.CurrentMap != null) return Find.CurrentMap.AsyncTime(); return null; diff --git a/Source/Client/Saving/LoadPatch.cs b/Source/Client/Saving/LoadPatch.cs index 5b5cb094..8e5f6285 100644 --- a/Source/Client/Saving/LoadPatch.cs +++ b/Source/Client/Saving/LoadPatch.cs @@ -26,7 +26,12 @@ static bool Prefix() Scribe.EnterNode("game"); Current.Game = new Game(); Current.Game.LoadGame(); // calls Scribe.loader.FinalizeLoading() - SemiPersistent.ReadSemiPersistent(gameToLoad.SemiPersistent); + + // Prevent errors when the client is disconnected during loading + // todo revisit disconnection during loading + // todo loading can be async, concurrency issues + if (Multiplayer.Client != null) + SemiPersistent.ReadSemiPersistent(gameToLoad.SemiPersistent); } finally { diff --git a/Source/Client/Syncing/DefSerialization.cs b/Source/Client/Syncing/DefSerialization.cs index 89586f42..d33fd26f 100644 --- a/Source/Client/Syncing/DefSerialization.cs +++ b/Source/Client/Syncing/DefSerialization.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using HarmonyLib; +using Multiplayer.Client.Util; using Multiplayer.Common; using Verse; @@ -14,7 +15,7 @@ public static class DefSerialization public static void Init() { - DefTypes = ImplSerialization.AllSubclassesNonAbstractOrdered(typeof(Def)); + DefTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(Def)); foreach (var defType in DefTypes) { diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index 141a3fba..f79935de 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -548,7 +548,7 @@ public static class SyncDictRimWorld }, { // Designator_Build is a Designator_Place but we aren't using Implicit - // We can't take part of the implicit tree because Designator_Build has an argument + // We can't take part of the implicit tree because Designator_Build ctor has an argument // So we need to implement placingRot here too, until we separate instancing from decorating. (SyncWorker sync, ref Designator_Build build) => { if (sync.isWriting) { @@ -569,6 +569,15 @@ public static class SyncDictRimWorld } } }, + { + (SyncWorker sync, ref Designator_Paint paint) => { + if (sync.isWriting) { + sync.Write(paint.colorDef); + } else { + paint.colorDef = sync.Read(); + } + }, true, true // <- Implicit ShouldConstruct + }, #endregion #region ThingComps diff --git a/Source/Client/Syncing/ImplSerialization.cs b/Source/Client/Syncing/ImplSerialization.cs index 88d751c0..34a2d715 100644 --- a/Source/Client/Syncing/ImplSerialization.cs +++ b/Source/Client/Syncing/ImplSerialization.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Multiplayer.Client.Util; using Multiplayer.Common; using RimWorld; using RimWorld.Planet; @@ -43,17 +44,17 @@ internal enum VerbOwnerType : byte public static void Init() { - storageParents = AllImplementationsOrdered(typeof(IStoreSettingsParent)); - plantToGrowSettables = AllImplementationsOrdered(typeof(IPlantToGrowSettable)); + storageParents = TypeUtil.AllImplementationsOrdered(typeof(IStoreSettingsParent)); + plantToGrowSettables = TypeUtil.AllImplementationsOrdered(typeof(IPlantToGrowSettable)); - thingCompTypes = AllSubclassesNonAbstractOrdered(typeof(ThingComp)); - abilityCompTypes = AllSubclassesNonAbstractOrdered(typeof(AbilityComp)); - designatorTypes = AllSubclassesNonAbstractOrdered(typeof(Designator)); - worldObjectCompTypes = AllSubclassesNonAbstractOrdered(typeof(WorldObjectComp)); + thingCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(ThingComp)); + abilityCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(AbilityComp)); + designatorTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(Designator)); + worldObjectCompTypes = TypeUtil.AllSubclassesNonAbstractOrdered(typeof(WorldObjectComp)); - gameCompTypes = AllSubclassesNonAbstractOrdered(typeof(GameComponent)); - worldCompTypes = AllSubclassesNonAbstractOrdered(typeof(WorldComponent)); - mapCompTypes = AllSubclassesNonAbstractOrdered(typeof(MapComponent)); + 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 @@ -97,20 +98,5 @@ internal static void GetImpl(object obj, IList impls, out Type type, out i } } } - - public static Type[] AllImplementationsOrdered(Type type) - { - return type.AllImplementing() - .OrderBy(t => t.IsInterface) - .ThenBy(t => t.Name) - .ToArray(); - } - - public static Type[] AllSubclassesNonAbstractOrdered(Type type) { - return type - .AllSubclassesNonAbstract() - .OrderBy(t => t.Name) - .ToArray(); - } } } diff --git a/Source/Client/UI/MainMenuPatches.cs b/Source/Client/UI/MainMenuPatches.cs index 6fb439be..5c509289 100644 --- a/Source/Client/UI/MainMenuPatches.cs +++ b/Source/Client/UI/MainMenuPatches.cs @@ -73,7 +73,7 @@ static void Prefix(Rect rect, List optList) optList.RemoveAll(opt => opt.label == "Save".Translate() || opt.label == "LoadGame".Translate()); if (!Multiplayer.IsReplay) { - optList.Insert(0, new ListableOption("Save".Translate(), () => Find.WindowStack.Add(new Dialog_SaveGame() { layer = WindowLayer.Super }))); + optList.Insert(0, new ListableOption("Save".Translate(), () => Find.WindowStack.Add(new SaveGameWindow(Multiplayer.session.gameName) { layer = WindowLayer.Super }))); } var quitMenuLabel = "QuitToMainMenu".Translate(); @@ -112,6 +112,9 @@ static void Prefix(Rect rect, List optList) static void ShowModDebugInfo() { + Find.WindowStack.Add(new SaveGameWindow("test")); + return; + var info = new RemoteData(); JoinData.ReadServerData(JoinData.WriteServerData(true), info); for (int i = 0; i < 200; i++) diff --git a/Source/Client/Util/MpUI.cs b/Source/Client/Util/MpUI.cs index 12d83731..73322075 100644 --- a/Source/Client/Util/MpUI.cs +++ b/Source/Client/Util/MpUI.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using HarmonyLib; using RimWorld; using UnityEngine; using Verse; @@ -57,16 +58,21 @@ public static void Label(Rect rect, string label, GameFont? font = null, TextAnc Text.Font = prevFont; } - public static void CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, Texture2D texChecked = null, Texture2D texUnchecked = null, bool placeTextNearCheckbox = false) + public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bool disabled = false, ElementOrder order = ElementOrder.Apart, float size = 24f) { TextAnchor anchor = Text.Anchor; Text.Anchor = TextAnchor.MiddleLeft; - if (placeTextNearCheckbox) + if (order == ElementOrder.Right) { float textWidth = Text.CalcSize(label).x; - rect.x = rect.xMax - textWidth - 24f - 5f; - rect.width = textWidth + 24f + 5f; + rect.x = rect.xMax - textWidth - size - 5f; + rect.width = textWidth + size + 5f; + } + else if (order == ElementOrder.Left) + { + float textWidth = Text.CalcSize(label).x; + rect.width = textWidth + size + 5f; } Widgets.Label(rect, label); @@ -80,8 +86,16 @@ public static void CheckboxLabeled(Rect rect, string label, ref bool checkOn, bo SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); } - Widgets.CheckboxDraw(rect.x + rect.width - 24f, rect.y + (rect.height - 24f) / 2, checkOn, disabled, 24f, null, null); + Widgets.CheckboxDraw( + rect.xMax - size, + rect.y + (rect.height - size) / 2, + checkOn, + disabled, + size); + Text.Anchor = anchor; + + return rect; } public static string TextEntryLabeled(Rect rect, string label, string text, float labelWidth) @@ -147,19 +161,19 @@ public static void LabelTruncatedWithTip(Rect rect, string str, Dictionary t.IsInterface) + .ThenBy(t => t.Name) + .ToArray(); + } + + public static Type[] AllSubclassesNonAbstractOrdered(Type type) { + return type + .AllSubclassesNonAbstract() + .OrderBy(t => t.Name) + .ToArray(); + } + } +} diff --git a/Source/Client/Windows/AbstractTextInputWindow.cs b/Source/Client/Windows/AbstractTextInputWindow.cs new file mode 100644 index 00000000..51e60418 --- /dev/null +++ b/Source/Client/Windows/AbstractTextInputWindow.cs @@ -0,0 +1,78 @@ +using Multiplayer.Client.Util; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public abstract class AbstractTextInputWindow : Window +{ + public override Vector2 InitialSize => new(350f, 175f); + + protected string curText; + protected string title; + protected string acceptBtnLabel; + protected string closeBtnLabel; + protected bool passwordField; + + private bool opened; + + public AbstractTextInputWindow() + { + closeOnClickedOutside = true; + doCloseX = true; + absorbInputAroundWindow = true; + closeOnAccept = true; + acceptBtnLabel = "OK".Translate(); + } + + public override void DoWindowContents(Rect inRect) + { + Widgets.Label(new Rect(0, 15f, inRect.width, 35f), title); + + if (passwordField) + { + // Doesn't call Validate currently + MpUI.DoPasswordField(new Rect(0, 25 + 15f, inRect.width, 35f), "RenameField", ref curText); + } + else + { + string text = Widgets.TextField(new Rect(0, 25 + 15f, inRect.width, 35f), curText); + if ((curText != text || !opened) && Validate(text)) + curText = text; + } + + DrawExtra(inRect); + + if (!opened) + { + UI.FocusControl("RenameField", this); + opened = true; + } + + var btnsRect = new Rect(0f, inRect.height - 35f - 5f, closeBtnLabel != null ? 210 : 120, 35f).CenteredOnXIn(inRect); + + if (Widgets.ButtonText(btnsRect.LeftPartPixels(closeBtnLabel != null ? 100 : 120), acceptBtnLabel, true, false)) + Accept(); + + if (closeBtnLabel != null) + if (Widgets.ButtonText(btnsRect.RightPartPixels(100), closeBtnLabel, true, false)) + OnCloseButton(); + } + + public virtual void OnCloseButton() + { + Close(); + } + + public override void OnAcceptKeyPressed() + { + if (Accept()) + base.OnAcceptKeyPressed(); + } + + public abstract bool Accept(); + + public virtual bool Validate(string str) => true; + + public virtual void DrawExtra(Rect inRect) { } +} diff --git a/Source/Client/Windows/ConnectingWindow.cs b/Source/Client/Windows/ConnectingWindow.cs index bcf3abda..8f1ed856 100644 --- a/Source/Client/Windows/ConnectingWindow.cs +++ b/Source/Client/Windows/ConnectingWindow.cs @@ -22,12 +22,27 @@ public abstract class BaseConnectingWindow : Window, IConnectionStatusListener public bool returnToServerBrowser; protected string result; + // Only show this window if there aren't any others during connecting + private bool ShouldShow => Find.WindowStack.Windows.Count(w => w.layer == WindowLayer.Dialog) == 1; + public BaseConnectingWindow() { closeOnAccept = false; closeOnCancel = false; } + // ExtraOnGUI is called before drawing the window shadow and WindowOnGUI + public override void ExtraOnGUI() + { + drawShadow = ShouldShow; + } + + public override void WindowOnGUI() + { + if (ShouldShow) + base.WindowOnGUI(); + } + public override void DoWindowContents(Rect inRect) { string label; diff --git a/Source/Client/Windows/DebugTextWindow.cs b/Source/Client/Windows/DebugTextWindow.cs new file mode 100644 index 00000000..34c9a265 --- /dev/null +++ b/Source/Client/Windows/DebugTextWindow.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public class DebugTextWindow : Window +{ + private Vector2 _initialSize; + public override Vector2 InitialSize => _initialSize; + + private Vector2 scroll; + private string text; + private List lines; + + private float fullHeight; + + public DebugTextWindow(string text, float width=800, float height=450) + { + this.text = text; + this._initialSize = new Vector2(width, height); + absorbInputAroundWindow = false; + doCloseX = true; + draggable = true; + + lines = text.Split('\n').ToList(); + } + + public override void DoWindowContents(Rect inRect) + { + const float offsetY = -5f; + + if (Event.current.type == EventType.Layout) + { + fullHeight = 0; + foreach (var str in lines) + fullHeight += Text.CalcHeight(str, inRect.width) + offsetY; + } + + Text.Font = GameFont.Tiny; + + if (Widgets.ButtonText(new Rect(0, 0, 55f, 20f), "Copy all")) + GUIUtility.systemCopyBuffer = text; + + Text.Font = GameFont.Small; + + var viewRect = new Rect(0f, 0f, inRect.width - 16f, Mathf.Max(fullHeight + 10f, inRect.height)); + inRect.y += 30f; + Widgets.BeginScrollView(inRect, ref scroll, viewRect, true); + + foreach (var str in lines) + { + float h = Text.CalcHeight(str, viewRect.width); + Widgets.TextArea(new Rect(viewRect.x, viewRect.y, viewRect.width, h), str, true); + viewRect.y += h + offsetY; + } + + Widgets.EndScrollView(); + } +} diff --git a/Source/Client/Windows/DesyncedWindow.cs b/Source/Client/Windows/DesyncedWindow.cs index 362f55ab..f5343235 100644 --- a/Source/Client/Windows/DesyncedWindow.cs +++ b/Source/Client/Windows/DesyncedWindow.cs @@ -58,7 +58,7 @@ public override void DoWindowContents(Rect inRect) x += 120 + 10; if (Widgets.ButtonText(new Rect(x, 0, 120, 35), "Save".Translate())) - Find.WindowStack.Add(new Dialog_SaveGame()); + Find.WindowStack.Add(new SaveGameWindow(Multiplayer.session.gameName)); x += 120 + 10; if (Widgets.ButtonText(new Rect(x, 0, 120, 35), "MpChatButton".Translate())) diff --git a/Source/Client/Windows/GamePasswordWindow.cs b/Source/Client/Windows/GamePasswordWindow.cs new file mode 100644 index 00000000..5af30419 --- /dev/null +++ b/Source/Client/Windows/GamePasswordWindow.cs @@ -0,0 +1,36 @@ +using Multiplayer.Client.Util; +using Multiplayer.Common; +using Verse; + +namespace Multiplayer.Client; + +public class GamePasswordWindow : AbstractTextInputWindow +{ + public bool returnToServerBrowser; + + public GamePasswordWindow() + { + title = "MpGamePassword".Translate(); + doCloseX = false; + closeOnCancel = false; + closeOnClickedOutside = false; + acceptBtnLabel = "MpConnectButton".Translate(); + closeBtnLabel = "CancelButton".Translate(); + passwordField = true; + } + + public override bool Accept() + { + Multiplayer.Client.Send(Packets.Client_Username, curText, Multiplayer.username); + Close(false); + return true; + } + + public override void OnCloseButton() + { + Multiplayer.StopMultiplayerAndClearAllWindows(); + + if (returnToServerBrowser) + Find.WindowStack.Add(new ServerBrowser()); + } +} diff --git a/Source/Client/Windows/HostWindow.cs b/Source/Client/Windows/HostWindow.cs index 6c8d7482..af88cdb8 100644 --- a/Source/Client/Windows/HostWindow.cs +++ b/Source/Client/Windows/HostWindow.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Text; using System.Threading; +using HarmonyLib; using UnityEngine; using Verse; using Verse.Profile; @@ -16,19 +17,27 @@ using Multiplayer.Client; using Multiplayer.Client.Util; using Multiplayer.Common.Util; +using TMPro; namespace Multiplayer.Client { [HotSwappable] + [StaticConstructorOnStartup] public class HostWindow : Window { - public override Vector2 InitialSize => new(450f, height + 45f); + enum Tab + { + Connecting, Gameplay + } + + public override Vector2 InitialSize => new(550f, 430f); private SaveFile file; public bool returnToServerBrowser; private bool withSimulation; private bool asyncTime; private bool asyncTimeLocked; + private Tab tab; private float height; @@ -64,6 +73,7 @@ public HostWindow(SaveFile file = null, bool withSimulation = false) private const int MaxGameNameLength = 70; private const float LabelWidth = 110f; + private const float CheckboxWidth = LabelWidth + 30f; public override void DoWindowContents(Rect inRect) { @@ -77,68 +87,82 @@ public override void DoWindowContents(Rect inRect) Text.Font = GameFont.Small; var entry = new Rect(0, 45, inRect.width, 30f); + entry.xMin += 4; // Game name serverSettings.gameName = MpUI.TextEntryLabeled(entry, $"{"MpGameName".Translate()}: ", serverSettings.gameName, LabelWidth); if (serverSettings.gameName.Length > MaxGameNameLength) serverSettings.gameName = serverSettings.gameName.Substring(0, MaxGameNameLength); - entry = entry.Down(40); + entry = entry.Down(50); - // Max players - MpUI.TextFieldNumericLabeled(entry.Width(LabelWidth + 30f), $"{"MpMaxPlayers".Translate()}: ", ref serverSettings.maxPlayers, ref maxPlayersBuffer, LabelWidth, 0, 999); + using (MpStyle.Set(TextAnchor.MiddleLeft)) + { + DoTabButton(entry.Width(140).Height(40f), Tab.Connecting); + DoTabButton(entry.Down(50f).Width(140).Height(40f), Tab.Gameplay); + } - // Autosave interval - var autosaveRect = entry.MinX(entry.x + LabelWidth + 30f + 10f); - var autosaveKey = serverSettings.autosaveUnit == AutosaveUnit.Days - ? "MpAutosaveIntervalDays" - : "MpAutosaveIntervalMinutes"; - - var changeAutosaveUnit = MpUI.TextFieldNumericLabeled( - autosaveRect, - $"{autosaveKey.Translate()}: ", - ref serverSettings.autosaveInterval, - ref autosaveBuffer, - 200f, - 0, - 999, - true, - MpUtil.TranslateWithDoubleNewLines("MpAutosaveIntervalDesc", 3) - ); + if (tab == Tab.Connecting) + DoConnecting(entry.MinX(entry.xMin + 150)); + else + DoGameplay(entry.MinX(entry.xMin + 150)); - if (changeAutosaveUnit) + if (Event.current.type == EventType.Layout && height != entry.yMax) { - serverSettings.autosaveUnit = serverSettings.autosaveUnit.Cycle(); - serverSettings.autosaveInterval *= - serverSettings.autosaveUnit == AutosaveUnit.Minutes ? - 8f : // Days to minutes - 0.125f; // Minutes to days - autosaveBuffer = serverSettings.autosaveInterval.ToString(); + height = entry.yMax; + SetInitialSizeAndPosition(); } - entry = entry.Down(40); + var buttonRect = new Rect((inRect.width - 100f) / 2f, inRect.height - 35f, 100f, 35f); - /*const char passChar = '\u2022'; - if (Event.current.type == EventType.Repaint || Event.current.isMouse) - TextEntryLabeled(entry.Width(200), "Password: ", new string(passChar, password.Length), labelWidth); - else - password = TextEntryLabeled(entry.Width(200), "Password: ", password, labelWidth); - entry = entry.Down(40);*/ + // Host button + if (Widgets.ButtonText(buttonRect, "MpHostButton".Translate())) + { + TryHost(); + } + } + + private void DoTabButton(Rect r, Tab tab) + { + Widgets.DrawOptionBackground(r, tab == this.tab); + if (Widgets.ButtonInvisible(r, true)) + { + this.tab = tab; + SoundDefOf.Click.PlayOneShotOnCamera(); + } - var checkboxWidth = LabelWidth + 30f; + float num = r.x + 10f; + Rect rect = new Rect(num, r.y + (r.height - 20f) / 2f, 20f, 20f); + Texture2D texture2D = ContentFinder.Get(tab == Tab.Connecting ? "UI/Icons/Options/OptionsGeneral" : "UI/Icons/Options/OptionsGameplay"); + GUI.DrawTexture(rect, texture2D); + num += 30f; + Widgets.Label(new Rect(num, r.y, r.width - num, r.height), tab == Tab.Connecting ? "MpHostTabConnecting".Translate() : "MpHostTabGameplay".Translate()); + } + + private void DoConnecting(Rect entry) + { + // Max players + MpUI.TextFieldNumericLabeled(entry.Width(LabelWidth + 35f), $"{"MpMaxPlayers".Translate()}: ", ref serverSettings.maxPlayers, ref maxPlayersBuffer, LabelWidth, 0, 999); + entry = entry.Down(30); + + // Password + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpHostGamePassword".Translate()}: ", ref serverSettings.hasPassword, order: ElementOrder.Right); + if (serverSettings.hasPassword) + MpUI.DoPasswordField(entry.Right(CheckboxWidth + 10).MaxX(entry.xMax), "PasswordField", ref serverSettings.password); + entry = entry.Down(30); // Direct hosting - var directLabel = $"{"MpDirect".Translate()}: "; - var directLabelWidth = Text.CalcSize(directLabel).x; - MpUI.CheckboxLabeled(entry.Width(checkboxWidth), directLabel, ref serverSettings.direct, placeTextNearCheckbox: true); + var directLabel = $"{"MpHostDirect".Translate()}: "; + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), directLabel, ref serverSettings.direct, order: ElementOrder.Right); + TooltipHandler.TipRegion(entry.Width(LabelWidth), MpUtil.TranslateWithDoubleNewLines("MpHostDirectDesc", 4)); if (serverSettings.direct) - serverSettings.directAddress = Widgets.TextField(entry.Right(checkboxWidth + 10).MaxX(inRect.xMax), serverSettings.directAddress); + serverSettings.directAddress = Widgets.TextField(entry.Right(CheckboxWidth + 10).MaxX(entry.xMax), serverSettings.directAddress); entry = entry.Down(30); // LAN hosting - var lanRect = entry.Width(checkboxWidth); - MpUI.CheckboxLabeled(lanRect, $"{"MpLan".Translate()}: ", ref serverSettings.lan, placeTextNearCheckbox: true); + var lanRect = entry.Width(CheckboxWidth); + MpUI.CheckboxLabeled(lanRect, $"{"MpLan".Translate()}: ", ref serverSettings.lan, order: ElementOrder.Right); TooltipHandler.TipRegion(lanRect, $"{"MpLanDesc1".Translate()}\n\n{"MpLanDesc2".Translate(serverSettings.lanAddress)}"); entry = entry.Down(30); @@ -146,20 +170,76 @@ public override void DoWindowContents(Rect inRect) // Steam hosting if (SteamManager.Initialized) { - MpUI.CheckboxLabeled(entry.Width(checkboxWidth), $"{"MpSteam".Translate()}: ", ref serverSettings.steam, placeTextNearCheckbox: true); + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpSteam".Translate()}: ", ref serverSettings.steam, order: ElementOrder.Right); entry = entry.Down(30); } - // Async time + // Sync configs + TooltipHandler.TipRegion(entry.Width(CheckboxWidth), MpUtil.TranslateWithDoubleNewLines("MpSyncConfigsDescNew", 3)); + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpSyncConfigs".Translate()}: ", ref serverSettings.syncConfigs, order: ElementOrder.Right); + entry = entry.Down(30); + } + + private void DoGameplay(Rect entry) + { + // Autosave interval + var autosaveUnitKey = serverSettings.autosaveUnit == AutosaveUnit.Days + ? "MpAutosavesDays" + : "MpAutosavesMinutes"; + + bool changeAutosaveUnit = false; + + LeftLabel(entry, $"{"MpAutosaves".Translate()}: "); + TooltipHandler.TipRegion(entry.Width(LabelWidth), MpUtil.TranslateWithDoubleNewLines("MpAutosavesDesc", 3)); + + using (MpStyle.Set(TextAnchor.MiddleRight)) + DoRow( + entry.Right(LabelWidth + 10), + rect => MpUI.LabelFlexibleWidth(rect, "MpAutosavesEvery".Translate()) + 6, + rect => + { + Widgets.TextFieldNumeric( + rect.Width(50f), + ref serverSettings.autosaveInterval, + ref autosaveBuffer, + 0, + 999 + ); + return 50f + 6; + }, + rect => MpUI.LabelFlexibleWidthClickable(rect, autosaveUnitKey.Translate(), ref changeAutosaveUnit) + ); + + if (changeAutosaveUnit) { - TooltipHandler.TipRegion(entry.Width(checkboxWidth), $"{"MpAsyncTimeDesc".Translate()}\n\n{"MpExperimentalFeature".Translate()}"); - MpUI.CheckboxLabeled(entry.Width(checkboxWidth), $"{"MpAsyncTime".Translate()}: ", ref asyncTime, placeTextNearCheckbox: true, disabled: asyncTimeLocked); - entry = entry.Down(30); + serverSettings.autosaveUnit = serverSettings.autosaveUnit.Cycle(); + serverSettings.autosaveInterval *= + serverSettings.autosaveUnit == AutosaveUnit.Minutes ? + 8f : // Days to minutes + 0.125f; // Minutes to days + autosaveBuffer = serverSettings.autosaveInterval.ToString(); } + entry = entry.Down(30); + + // Async time + TooltipHandler.TipRegion(entry.Width(CheckboxWidth), $"{"MpAsyncTimeDesc".Translate()}\n\n{"MpExperimentalFeature".Translate()}"); + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpAsyncTime".Translate()}: ", ref asyncTime, order: ElementOrder.Right, disabled: asyncTimeLocked); + entry = entry.Down(30); + + // Time control + // LeftLabel(entry, $"{"MpTimeControl".Translate()}: "); + // if (CustomButton(entry.Right(LabelWidth + 10), $"MpTimeControl{serverSettings.timeControl}".Translate())) + // { + // serverSettings.timeControl = serverSettings.timeControl.Cycle(); + // SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(); + // } + // + // entry = entry.Down(30); + // Log desync traces MpUI.CheckboxLabeledWithTipNoHighlight( - entry.Width(checkboxWidth), + entry.Width(CheckboxWidth), $"{"MpLogDesyncTraces".Translate()}: ", MpUtil.TranslateWithDoubleNewLines("MpLogDesyncTracesDesc", 2), ref serverSettings.desyncTraces, @@ -169,14 +249,14 @@ public override void DoWindowContents(Rect inRect) // Arbiter if (MpVersion.IsDebug) { - TooltipHandler.TipRegion(entry.Width(checkboxWidth), "MpArbiterDesc".Translate()); - MpUI.CheckboxLabeled(entry.Width(checkboxWidth), $"{"MpRunArbiter".Translate()}: ", ref serverSettings.arbiter, placeTextNearCheckbox: true); + TooltipHandler.TipRegion(entry.Width(CheckboxWidth), "MpArbiterDesc".Translate()); + MpUI.CheckboxLabeled(entry.Width(CheckboxWidth), $"{"MpRunArbiter".Translate()}: ", ref serverSettings.arbiter, order: ElementOrder.Right); entry = entry.Down(30); } // Dev mode MpUI.CheckboxLabeledWithTipNoHighlight( - entry.Width(checkboxWidth), + entry.Width(CheckboxWidth), $"{"MpHostingDevMode".Translate()}: ", MpUtil.TranslateWithDoubleNewLines("MpHostingDevModeDesc", 2), ref serverSettings.debugMode, @@ -184,35 +264,76 @@ public override void DoWindowContents(Rect inRect) ); // Dev mode scope - if (serverSettings.debugMode - && CustomButton(entry.Right(checkboxWidth + 10f), $"MpHostingDevMode{serverSettings.devModeScope}".Translate())) - { - serverSettings.devModeScope = serverSettings.devModeScope.Cycle(); - } - - entry = entry.Down(30); + if (serverSettings.debugMode) + if (CustomButton(entry.Right(CheckboxWidth + 10f), $"MpHostingDevMode{serverSettings.devModeScope}".Translate())) + { + serverSettings.devModeScope = serverSettings.devModeScope.Cycle(); + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(); + } - // Sync configs - TooltipHandler.TipRegion(entry.Width(checkboxWidth), MpUtil.TranslateWithDoubleNewLines("MpSyncConfigsDesc", 3)); - MpUI.CheckboxLabeled(entry.Width(checkboxWidth), $"{"MpSyncConfigs".Translate()}: ", ref serverSettings.syncConfigs, placeTextNearCheckbox: true); entry = entry.Down(30); // Auto join-points DrawJoinPointOptions(entry); entry = entry.Down(30); - if (Event.current.type == EventType.Layout && height != entry.yMax) + // Pause on letter + LeftLabel(entry, $"{"MpPauseOnLetter".Translate()}: "); + DoPauseOnLetter(entry.Right(LabelWidth + 10)); + entry = entry.Down(30); + + // Pause on (join, desync) + LeftLabel(entry, $"{"MpPauseOn".Translate()}: "); + DoRow( + entry.Right(LabelWidth + 10), + rect => MpUI.CheckboxLabeled( + rect.Width(CheckboxWidth), + "MpPauseOnJoin".Translate(), + ref serverSettings.pauseOnJoin, + size: 20f, + order: ElementOrder.Left).width + 15, + rect => MpUI.CheckboxLabeled( + rect.Width(CheckboxWidth), + "MpPauseOnDesync".Translate(), + ref serverSettings.pauseOnDesync, + size: 20f, + order: ElementOrder.Left).width + ); + + entry = entry.Down(30); + } + + private void DoPauseOnLetter(Rect entry) + { + if (CustomButton(entry, $"MpPauseOnLetter{serverSettings.pauseOnLetter}".Translate())) + Find.WindowStack.Add(new FloatMenu(Options().ToList())); + + IEnumerable Options() { - height = entry.yMax; - SetInitialSizeAndPosition(); + foreach (var opt in Enum.GetValues(typeof(PauseOnLetter)).OfType()) + yield return new FloatMenuOption($"MpPauseOnLetter{opt}".Translate(), () => + { + serverSettings.pauseOnLetter = opt; + }); } + } - var buttonRect = new Rect((inRect.width - 100f) / 2f, inRect.height - 35f, 100f, 35f); + static float LeftLabel(Rect entry, string text, string desc = null) + { + using (MpStyle.Set(TextAnchor.MiddleRight)) + MpUI.LabelWithTip( + entry.Width(LabelWidth + 1), + text, + desc + ); + return Text.CalcSize(text).x; + } - // Host button - if (Widgets.ButtonText(buttonRect, "MpHostButton".Translate())) + static void DoRow(Rect inRect, params Func[] drawers) + { + foreach (var drawer in drawers) { - TryHost(); + inRect.xMin += drawer(inRect); } } @@ -220,12 +341,7 @@ public override void DoWindowContents(Rect inRect) private void DrawJoinPointOptions(Rect entry) { - using (MpStyle.Set(TextAnchor.MiddleRight)) - MpUI.LabelWithTip( - entry.Width(LabelWidth + 1), - $"{"MpAutoJoinPoints".Translate()}: ", - MpUtil.TranslateWithDoubleNewLines("MpAutoJoinPointsDesc", 3) - ); + LeftLabel(entry, $"{"MpAutoJoinPoints".Translate()}: ", MpUtil.TranslateWithDoubleNewLines("MpAutoJoinPointsDesc", 3)); var flags = Enum.GetValues(typeof(AutoJoinPointFlags)) .OfType() @@ -278,6 +394,12 @@ private void TryHost() return; } + if (settings.hasPassword && settings.password.NullOrEmpty()) + { + Messages.Message("MpInvalidGamePassword".Translate(), MessageTypeDefOf.RejectInput, false); + return; + } + if (TryStartLocalServer(settings) is false) return; diff --git a/Source/Client/Windows/JoinDataWindow.cs b/Source/Client/Windows/JoinDataWindow.cs index cb7bf907..975248f3 100644 --- a/Source/Client/Windows/JoinDataWindow.cs +++ b/Source/Client/Windows/JoinDataWindow.cs @@ -262,8 +262,7 @@ void RefreshFiles() if (Widgets.ButtonText(btnCenter.Right(150f), "MpMismatchQuit".Translate())) { - Multiplayer.StopMultiplayer(); - Close(); + Multiplayer.StopMultiplayerAndClearAllWindows(); Find.WindowStack.Add(new ServerBrowser()); } } diff --git a/Source/Client/Windows/RenameFileWindow.cs b/Source/Client/Windows/RenameFileWindow.cs new file mode 100644 index 00000000..07eb0441 --- /dev/null +++ b/Source/Client/Windows/RenameFileWindow.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using RimWorld; +using Verse; + +namespace Multiplayer.Client; + +public class RenameFileWindow : AbstractTextInputWindow +{ + private FileInfo file; + private Action success; + + public RenameFileWindow(FileInfo file, Action success = null) + { + title = "MpFileRename".Translate(); + + this.file = file; + this.success = success; + + curText = Path.GetFileNameWithoutExtension(file.Name); + } + + public override bool Accept() + { + if (curText.Length == 0) + return false; + + string newPath = Path.Combine(file.Directory.FullName, curText + file.Extension); + + if (newPath == file.FullName) + return true; + + try + { + file.MoveTo(newPath); + Close(); + success?.Invoke(); + + return true; + } + catch (IOException e) + { + Messages.Message(e is DirectoryNotFoundException ? "Error renaming." : "File already exists.", MessageTypeDefOf.RejectInput, false); + return false; + } + } + + public override bool Validate(string str) + { + return str.Length < 30; + } +} diff --git a/Source/Client/Windows/SaveGameWindow.cs b/Source/Client/Windows/SaveGameWindow.cs new file mode 100644 index 00000000..e01fc28a --- /dev/null +++ b/Source/Client/Windows/SaveGameWindow.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +[HotSwappable] +public class SaveGameWindow : AbstractTextInputWindow +{ + private bool fileExists; + + public SaveGameWindow(string gameName) + { + title = "MpSaveGameAs".Translate(); + curText = GenFile.SanitizedFileName(gameName); + } + + public override bool Accept() + { + if (curText.Length == 0) return false; + + try + { + LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile(curText), "MpSaving", false, null); + Close(); + } + catch (Exception e) + { + Log.Error($"Exception saving replay {e}"); + } + + return true; + } + + public override bool Validate(string str) + { + if (str.Length == 0) + return true; + + if (str.Length > 30) + return false; + + if (GenFile.SanitizedFileName(str) != str) + return false; + + fileExists = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{str}.zip")).Exists; + return true; + } + + public override void DrawExtra(Rect inRect) + { + if (fileExists) + { + Text.Font = GameFont.Tiny; + Widgets.Label(new Rect(0, 25 + 15 + 35, inRect.width, 35f), "MpWillOverwrite".Translate()); + Text.Font = GameFont.Small; + } + } +} diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index 59c6c894..71bd3c15 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -411,7 +411,7 @@ private IEnumerable SaveFloatMenu(SaveFile save) yield return new FloatMenuOption("MpSeeModList".Translate(), () => { - Find.WindowStack.Add(new TwoTextAreas_Window($"RimWorld {save.rwVersion}\nSave mod list:\n\n{saveMods}", $"RimWorld {VersionControl.CurrentVersionString}\nActive mod list:\n\n{activeMods}")); + Find.WindowStack.Add(new TwoTextAreasWindow($"RimWorld {save.rwVersion}\nSave mod list:\n\n{saveMods}", $"RimWorld {VersionControl.CurrentVersionString}\nActive mod list:\n\n{activeMods}")); }); yield return new FloatMenuOption("MpOpenSaveFolder".Translate(), () => @@ -421,7 +421,7 @@ private IEnumerable SaveFloatMenu(SaveFile save) yield return new FloatMenuOption("MpFileRename".Translate(), () => { - Find.WindowStack.Add(new Dialog_RenameFile(save.file, ReloadFiles)); + Find.WindowStack.Add(new RenameFileWindow(save.file, ReloadFiles)); }); if (!MpVersion.IsDebug) yield break; diff --git a/Source/Client/Windows/TextAreaWindow.cs b/Source/Client/Windows/TextAreaWindow.cs new file mode 100644 index 00000000..79689d63 --- /dev/null +++ b/Source/Client/Windows/TextAreaWindow.cs @@ -0,0 +1,23 @@ +using UnityEngine; +using Verse; + +namespace Multiplayer.Client; + +public class TextAreaWindow : Window +{ + private string text; + private Vector2 scroll; + + public TextAreaWindow(string text) + { + this.text = text; + + absorbInputAroundWindow = true; + doCloseX = true; + } + + public override void DoWindowContents(Rect inRect) + { + Widgets.TextAreaScrollable(inRect, text, ref scroll); + } +} diff --git a/Source/Client/Windows/TwoTextAreasWindow.cs b/Source/Client/Windows/TwoTextAreasWindow.cs new file mode 100644 index 00000000..38bc07c4 --- /dev/null +++ b/Source/Client/Windows/TwoTextAreasWindow.cs @@ -0,0 +1,33 @@ +using Multiplayer.Client.Util; +using UnityEngine; +using Verse; + +namespace Multiplayer.Client +{ + public class TwoTextAreasWindow : Window + { + public override Vector2 InitialSize => new Vector2(600, 500); + + private Vector2 scroll1; + private Vector2 scroll2; + + private string left; + private string right; + + public TwoTextAreasWindow(string left, string right) + { + absorbInputAroundWindow = true; + doCloseX = true; + + this.left = left; + this.right = right; + } + + public override void DoWindowContents(Rect inRect) + { + Widgets.TextAreaScrollable(inRect.LeftHalf(), left, ref scroll1); + Widgets.TextAreaScrollable(inRect.RightHalf(), right, ref scroll2); + } + } + +} diff --git a/Source/Client/Windows/Windows.cs b/Source/Client/Windows/Windows.cs deleted file mode 100644 index eb3b4a9f..00000000 --- a/Source/Client/Windows/Windows.cs +++ /dev/null @@ -1,258 +0,0 @@ -using RimWorld; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Multiplayer.Client.Util; -using UnityEngine; -using Verse; - -namespace Multiplayer.Client -{ - public abstract class MpTextInput : Window - { - public override Vector2 InitialSize => new Vector2(350f, 175f); - - public string curName; - public string title; - private bool opened; - - public MpTextInput() - { - closeOnClickedOutside = true; - doCloseX = true; - absorbInputAroundWindow = true; - closeOnAccept = true; - } - - public override void DoWindowContents(Rect inRect) - { - Widgets.Label(new Rect(0, 15f, inRect.width, 35f), title); - - GUI.SetNextControlName("RenameField"); - string text = Widgets.TextField(new Rect(0, 25 + 15f, inRect.width, 35f), curName); - if ((curName != text || !opened) && Validate(text)) - curName = text; - - DrawExtra(inRect); - - if (!opened) - { - UI.FocusControl("RenameField", this); - opened = true; - } - - if (Widgets.ButtonText(new Rect(0f, inRect.height - 35f - 5f, 120f, 35f).CenteredOnXIn(inRect), "OK".Translate(), true, false, true)) - Accept(); - } - - public override void OnAcceptKeyPressed() - { - if (Accept()) - base.OnAcceptKeyPressed(); - } - - public abstract bool Accept(); - - public virtual bool Validate(string str) => true; - - public virtual void DrawExtra(Rect inRect) { } - } - - public class Dialog_SaveGame : MpTextInput - { - private bool fileExists; - - public Dialog_SaveGame() - { - title = "MpSaveGameAs".Translate(); - curName = GenFile.SanitizedFileName(Multiplayer.session.gameName); - } - - public override bool Accept() - { - if (curName.Length == 0) return false; - - try - { - LongEventHandler.QueueLongEvent(() => MultiplayerSession.SaveGameToFile(curName), "MpSaving", false, null); - Close(); - } - catch (Exception e) - { - Log.Error($"Exception saving replay {e}"); - } - - return true; - } - - public override bool Validate(string str) - { - if (str.Length > 30) - return false; - - if (GenFile.SanitizedFileName(str) != str) - return false; - - fileExists = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{str}.zip")).Exists; - return true; - } - - public override void DrawExtra(Rect inRect) - { - if (fileExists) - { - Text.Font = GameFont.Tiny; - Widgets.Label(new Rect(0, 25 + 15 + 35, inRect.width, 35f), "MpWillOverwrite".Translate()); - Text.Font = GameFont.Small; - } - } - } - - public class Dialog_RenameFile : MpTextInput - { - private FileInfo file; - private Action success; - - public Dialog_RenameFile(FileInfo file, Action success = null) - { - title = "MpFileRename".Translate(); - - this.file = file; - this.success = success; - - curName = Path.GetFileNameWithoutExtension(file.Name); - } - - public override bool Accept() - { - if (curName.Length == 0) - return false; - - string newPath = Path.Combine(file.Directory.FullName, curName + file.Extension); - - if (newPath == file.FullName) - return true; - - try - { - file.MoveTo(newPath); - Close(); - success?.Invoke(); - - return true; - } - catch (IOException e) - { - Messages.Message(e is DirectoryNotFoundException ? "Error renaming." : "File already exists.", MessageTypeDefOf.RejectInput, false); - return false; - } - } - - public override bool Validate(string str) - { - return str.Length < 30; - } - } - - public class TextAreaWindow : Window - { - private string text; - private Vector2 scroll; - - public TextAreaWindow(string text) - { - this.text = text; - - absorbInputAroundWindow = true; - doCloseX = true; - } - - public override void DoWindowContents(Rect inRect) - { - Widgets.TextAreaScrollable(inRect, text, ref scroll); - } - } - - public class DebugTextWindow : Window - { - private Vector2 _initialSize; - public override Vector2 InitialSize => _initialSize; - - private Vector2 scroll; - private string text; - private List lines; - - private float fullHeight; - - public DebugTextWindow(string text, float width=800, float height=450) - { - this.text = text; - this._initialSize = new Vector2(width, height); - absorbInputAroundWindow = false; - doCloseX = true; - draggable = true; - - lines = text.Split('\n').ToList(); - } - - public override void DoWindowContents(Rect inRect) - { - const float offsetY = -5f; - - if (Event.current.type == EventType.Layout) - { - fullHeight = 0; - foreach (var str in lines) - fullHeight += Text.CalcHeight(str, inRect.width) + offsetY; - } - - Text.Font = GameFont.Tiny; - - if (Widgets.ButtonText(new Rect(0, 0, 55f, 20f), "Copy all")) - GUIUtility.systemCopyBuffer = text; - - Text.Font = GameFont.Small; - - var viewRect = new Rect(0f, 0f, inRect.width - 16f, Mathf.Max(fullHeight + 10f, inRect.height)); - inRect.y += 30f; - Widgets.BeginScrollView(inRect, ref scroll, viewRect, true); - - foreach (var str in lines) - { - float h = Text.CalcHeight(str, viewRect.width); - Widgets.TextArea(new Rect(viewRect.x, viewRect.y, viewRect.width, h), str, true); - viewRect.y += h + offsetY; - } - - Widgets.EndScrollView(); - } - } - - public class TwoTextAreas_Window : Window - { - public override Vector2 InitialSize => new Vector2(600, 500); - - private Vector2 scroll1; - private Vector2 scroll2; - - private string left; - private string right; - - public TwoTextAreas_Window(string left, string right) - { - absorbInputAroundWindow = true; - doCloseX = true; - - this.left = left; - this.right = right; - } - - public override void DoWindowContents(Rect inRect) - { - Widgets.TextAreaScrollable(inRect.LeftHalf(), left, ref scroll1); - Widgets.TextAreaScrollable(inRect.RightHalf(), right, ref scroll2); - } - } - -} diff --git a/Source/Common/CommandHandler.cs b/Source/Common/CommandHandler.cs index 41a5965a..1fb80619 100644 --- a/Source/Common/CommandHandler.cs +++ b/Source/Common/CommandHandler.cs @@ -58,6 +58,16 @@ public void Send(CommandType cmd, int factionId, int mapId, byte[] data, ServerP NextCmdId++; } + public void PauseAll() + { + Send( + CommandType.PauseAll, + ScheduledCommand.NoFaction, + ScheduledCommand.Global, + null + ); + } + public bool CanUseDevMode(ServerPlayer player) => server.settings.debugMode && server.settings.devModeScope switch { diff --git a/Source/Common/Commands.cs b/Source/Common/Commands.cs index 270da269..d382a4c0 100644 --- a/Source/Common/Commands.cs +++ b/Source/Common/Commands.cs @@ -4,6 +4,7 @@ public enum CommandType : byte { // Global scope WorldTimeSpeed, + PauseAll, CreateJoinPoint, SetupFaction, GlobalIdBlock, diff --git a/Source/Common/Networking/MpDisconnectReason.cs b/Source/Common/Networking/MpDisconnectReason.cs index 3fc30dfc..05e08d59 100644 --- a/Source/Common/Networking/MpDisconnectReason.cs +++ b/Source/Common/Networking/MpDisconnectReason.cs @@ -17,7 +17,8 @@ public enum MpDisconnectReason : byte ConnectingFailed, ServerPacketRead, Internal, - ServerStarting + ServerStarting, + BadGamePassword } } diff --git a/Source/Common/Networking/State/ServerJoiningState.cs b/Source/Common/Networking/State/ServerJoiningState.cs index c40c5124..7ffc3ad1 100644 --- a/Source/Common/Networking/State/ServerJoiningState.cs +++ b/Source/Common/Networking/State/ServerJoiningState.cs @@ -23,7 +23,7 @@ public void HandleProtocol(ByteReader data) if (clientProtocol != MpVersion.Protocol) Player.Disconnect(MpDisconnectReason.Protocol, ByteWriter.GetBytes(MpVersion.Version, MpVersion.Protocol)); else - Player.SendPacket(Packets.Server_ProtocolOk, new byte[0]); + Player.SendPacket(Packets.Server_ProtocolOk, new object[] { Server.settings.hasPassword }); } [PacketHandler(Packets.Client_Username)] @@ -32,6 +32,16 @@ public void HandleUsername(ByteReader data) if (!string.IsNullOrEmpty(connection.username)) // Username already set return; + if (Server.settings.hasPassword) + { + string password = data.ReadString(); + if (password != Server.settings.password) + { + Player.Disconnect(MpDisconnectReason.BadGamePassword); + return; + } + } + string username = data.ReadString(); if (username.Length < MultiplayerServer.MinUsernameLength || username.Length > MultiplayerServer.MaxUsernameLength) @@ -102,6 +112,9 @@ public void HandleJoinData(ByteReader data) if (!defsMismatched) { + if (Server.settings.pauseOnJoin) + Server.commands.PauseAll(); + if (Server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Join)) Server.TryStartJoinPointCreation(); diff --git a/Source/Common/PlayerManager.cs b/Source/Common/PlayerManager.cs index b332e937..85f107a5 100644 --- a/Source/Common/PlayerManager.cs +++ b/Source/Common/PlayerManager.cs @@ -79,11 +79,12 @@ public void OnDisconnected(ConnectionBase conn, MpDisconnectReason reason) if (player.hasJoined) { // todo check player.IsPlaying? - if (Players.All(p => p.FactionId != player.FactionId)) - { - byte[] data = ByteWriter.GetBytes(player.FactionId); - server.commands.Send(CommandType.FactionOffline, ScheduledCommand.NoFaction, ScheduledCommand.Global, data); - } + // todo FactionId might throw when called for not fully initialized players + // if (Players.All(p => p.FactionId != player.FactionId)) + // { + // byte[] data = ByteWriter.GetBytes(player.FactionId); + // server.commands.Send(CommandType.FactionOffline, ScheduledCommand.NoFaction, ScheduledCommand.Global, data); + // } server.SendNotification("MpPlayerDisconnected", conn.username); server.SendChat($"{conn.username} has left."); @@ -101,6 +102,9 @@ public void OnDesync(ServerPlayer player, int tick, int diffAt) player.UpdateStatus(PlayerStatus.Desynced); server.HostPlayer.SendPacket(Packets.Server_Traces, new object[] { TracesPacket.Request, tick, diffAt, player.id }); + if (server.settings.pauseOnDesync) + server.commands.PauseAll(); + if (server.settings.autoJoinPoint.HasFlag(AutoJoinPointFlags.Desync)) server.TryStartJoinPointCreation(true); } diff --git a/Source/Common/ServerSettings.cs b/Source/Common/ServerSettings.cs index 3517e7c1..2a89a43a 100644 --- a/Source/Common/ServerSettings.cs +++ b/Source/Common/ServerSettings.cs @@ -21,6 +21,12 @@ public class ServerSettings : IExposable public bool syncConfigs = true; public AutoJoinPointFlags autoJoinPoint = AutoJoinPointFlags.Join | AutoJoinPointFlags.Desync; public DevModeScope devModeScope; + public bool hasPassword; + public string password = ""; + public PauseOnLetter pauseOnLetter = PauseOnLetter.AnyThreat; + public bool pauseOnJoin = true; + public bool pauseOnDesync = true; + public TimeControl timeControl; public void ExposeData() { @@ -38,12 +44,19 @@ public void ExposeData() 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"); } } public enum AutosaveUnit { - Days, Minutes + Days, + Minutes } [Flags] @@ -56,6 +69,21 @@ public enum AutoJoinPointFlags public enum DevModeScope { - HostOnly, Everyone + HostOnly, + Everyone + } + + public enum PauseOnLetter + { + Never, + MajorThreat, + AnyThreat, + AnyLetter + } + + public enum TimeControl + { + EveryoneControls, + LowestWins } } diff --git a/Source/Common/Version.cs b/Source/Common/Version.cs index 3515cc0d..647b267c 100644 --- a/Source/Common/Version.cs +++ b/Source/Common/Version.cs @@ -3,7 +3,7 @@ namespace Multiplayer.Common public static class MpVersion { public const string Version = "0.6.2.0"; - public const int Protocol = 26; + public const int Protocol = 27; public const string ApiAssemblyName = "0MultiplayerAPI"; diff --git a/Source/Multiplayer.csproj b/Source/Multiplayer.csproj index 2438214a..1ecf32fe 100644 --- a/Source/Multiplayer.csproj +++ b/Source/Multiplayer.csproj @@ -3,7 +3,7 @@ net472 true - 9.0 + 10 false ..\Assemblies\ false