diff --git a/Source/Client/AsyncTime/AsyncTimeComp.cs b/Source/Client/AsyncTime/AsyncTimeComp.cs index 837d28f1..46dc0fd5 100644 --- a/Source/Client/AsyncTime/AsyncTimeComp.cs +++ b/Source/Client/AsyncTime/AsyncTimeComp.cs @@ -65,8 +65,22 @@ public void SetDesiredTimeSpeed(TimeSpeed speed) public int TickableId => map.uniqueID; + public int GameStartAbsTick + { + get + { + if (gameStartAbsTickMap == 0) + { + gameStartAbsTickMap = Find.TickManager?.gameStartAbsTick ?? 0; + } + + return gameStartAbsTickMap; + } + } + public Map map; public int mapTicks; + private int gameStartAbsTickMap; private TimeSpeed timeSpeedInt; public bool forcedNormalSpeed; public int eventCount; @@ -84,9 +98,10 @@ public void SetDesiredTimeSpeed(TimeSpeed speed) public Queue cmds = new(); - public AsyncTimeComp(Map map) + public AsyncTimeComp(Map map, int gameStartAbsTick = 0) { this.map = map; + this.gameStartAbsTickMap = gameStartAbsTick; } public void Tick() @@ -198,6 +213,8 @@ public void ExposeData() Scribe_Values.Look(ref mapTicks, "mapTicks"); Scribe_Values.Look(ref timeSpeedInt, "timeSpeed"); + Scribe_Values.Look(ref gameStartAbsTickMap, "gameStartAbsTickMap"); + Scribe_Deep.Look(ref storyteller, "storyteller"); Scribe_Deep.Look(ref storyWatcher, "storyWatcher"); @@ -439,5 +456,4 @@ public enum DesignatorMode : byte MultiCell, Thing } - } diff --git a/Source/Client/AsyncTime/SetMapTime.cs b/Source/Client/AsyncTime/SetMapTime.cs index 91c706b7..86506c71 100644 --- a/Source/Client/AsyncTime/SetMapTime.cs +++ b/Source/Client/AsyncTime/SetMapTime.cs @@ -168,12 +168,14 @@ public struct TimeSnapshot public int ticks; public TimeSpeed speed; public TimeSlower slower; + public int gameStartAbsTick; public void Set() { Find.TickManager.ticksGameInt = ticks; Find.TickManager.slower = slower; Find.TickManager.curTimeSpeed = speed; + Find.TickManager.gameStartAbsTick = gameStartAbsTick; } public static TimeSnapshot Current() @@ -182,7 +184,8 @@ public static TimeSnapshot Current() { ticks = Find.TickManager.ticksGameInt, speed = Find.TickManager.curTimeSpeed, - slower = Find.TickManager.slower + slower = Find.TickManager.slower, + gameStartAbsTick = Find.TickManager.gameStartAbsTick }; } @@ -198,6 +201,7 @@ public static TimeSnapshot Current() tickManager.ticksGameInt = mapComp.mapTicks; tickManager.slower = mapComp.slower; tickManager.CurTimeSpeed = mapComp.DesiredTimeSpeed; + tickManager.gameStartAbsTick = mapComp.GameStartAbsTick; return prev; } diff --git a/Source/Client/Factions/FactionCreator.cs b/Source/Client/Factions/FactionCreator.cs index 12e5d62b..50ed1c7e 100644 --- a/Source/Client/Factions/FactionCreator.cs +++ b/Source/Client/Factions/FactionCreator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Multiplayer.API; @@ -14,6 +15,7 @@ public static class FactionCreator { private static Dictionary> pawnStore = new(); public static bool generatingMap; + public const string preventSpecialCalculationPathInGenTicksTicksAbs = "dummy"; public static void ClearData() { @@ -27,34 +29,30 @@ public static void SendPawn(int playerId, Pawn p) } [SyncMethod] - public static void CreateFaction( - int playerId, string factionName, int tile, - [CanBeNull] ScenarioDef scenarioDef, ChooseIdeoInfo chooseIdeoInfo, - bool generateMap - ) + public static void CreateFaction(int playerId, FactionCreationData creationData) { var self = TickPatch.currentExecutingCmdIssuedBySelf; LongEventHandler.QueueLongEvent(() => { - PrepareGameInitData(playerId); + var scenario = creationData.scenarioDef?.scenario ?? Current.Game.Scenario; + Map newMap = null; + + PrepareGameInitData(playerId, scenario, self, creationData.startingPossessions); - var scenario = scenarioDef?.scenario ?? Current.Game.Scenario; var newFaction = NewFactionWithIdeo( - factionName, + creationData.factionName, scenario.playerFaction.factionDef, - chooseIdeoInfo + creationData.chooseIdeoInfo ); - Map newMap = null; - - if (generateMap) + if (creationData.generateMap) using (MpScope.PushFaction(newFaction)) { foreach (var pawn in StartingPawnUtility.StartingAndOptionalPawns) pawn.ideo.SetIdeo(newFaction.ideos.PrimaryIdeo); - newMap = GenerateNewMap(tile, scenario); + newMap = GenerateNewMap(creationData.startingTile, scenario, creationData.setupNextMapFromTickZero); } foreach (Map map in Find.Maps) @@ -70,6 +68,10 @@ bool generateMap Multiplayer.game.ChangeRealPlayerFaction(newFaction); + CameraJumper.TryJump(MapGenerator.playerStartSpotInt, newMap); + + PostGameStart(scenario); + // todo setting faction of self Multiplayer.Client.Send( Packets.Client_SetFaction, @@ -80,62 +82,137 @@ bool generateMap }, "GeneratingMap", doAsynchronously: true, GameAndMapInitExceptionHandlers.ErrorWhileGeneratingMap); } - private static Map GenerateNewMap(int tile, Scenario scenario) + private static void PostGameStart(Scenario scenario) + { + /** + ScenPart_StartingResearch.cs + ScenPart_AutoActivateMonolith.cs + ScenPart_CreateIncident.cs + ScenPart_GameStartDialog.cs + ScenPart_PlayerFaction.cs + ScenPart_Rule.cs + + Would like to call PostGameStart on all implementations (scenario.PostGameStart) - + but dont know if it breaks with dlcs other than biotech - especially while only called + on self + **/ + + HashSet types = new HashSet + { + typeof(ScenPart_PlayerFaction), + typeof(ScenPart_GameStartDialog), + typeof(ScenPart_StartingResearch), + }; + + foreach (ScenPart part in scenario.AllParts) + { + if (types.Contains(part.GetType())) + { + part.PostGameStart(); + } + } + } + + private static Map GenerateNewMap(int tile, Scenario scenario, bool setupNextMapFromTickZero) { // This has to be null, otherwise, during map generation, Faction.OfPlayer returns it which breaks FactionContext Find.GameInitData.playerFaction = null; Find.GameInitData.PrepForMapGen(); - var mapParent = (Settlement)WorldObjectMaker.MakeWorldObject(WorldObjectDefOf.Settlement); - mapParent.Tile = tile; - mapParent.SetFaction(Faction.OfPlayer); - Find.WorldObjects.Add(mapParent); + // ScenPart_PlayerFaction --> PreMapGenerate + + var settlement = (Settlement)WorldObjectMaker.MakeWorldObject(WorldObjectDefOf.Settlement); + settlement.Tile = tile; + settlement.SetFaction(Faction.OfPlayer); + Find.WorldObjects.Add(settlement); + + // ^^^^ Duplicate Code here ^^^^ var prevScenario = Find.Scenario; + var prevStartingTile = Find.GameInfo.startingTile; + Current.Game.Scenario = scenario; + Find.GameInfo.startingTile = tile; + Find.GameInitData.startingTile = tile; + + MapSetup.SetupNextMapFromTickZero = setupNextMapFromTickZero; + generatingMap = true; try { - return GetOrGenerateMapUtility.GetOrGenerateMap( + Map map = GetOrGenerateMapUtility.GetOrGenerateMap( tile, new IntVec3(250, 1, 250), null ); + + SetAllThingWithCompsOnMapForbidden(map); + + return map; } finally { generatingMap = false; Current.Game.Scenario = prevScenario; + Find.GameInfo.startingTile = prevStartingTile; + Find.GameInitData.startingTile = prevStartingTile; + } + } + + // TODO: Remove this Workaround (see Issue #535) + private static void SetAllThingWithCompsOnMapForbidden(Map map) + { + foreach (Thing thing in map.listerThings.AllThings) + { + if (thing is ThingWithComps) + { + thing.SetForbidden(true, false); + } } } private static void InitNewGame() { PawnUtility.GiveAllStartingPlayerPawnsThought(ThoughtDefOf.NewColonyOptimism); + ResearchUtility.ApplyPlayerStartingResearch(); } - private static void PrepareGameInitData(int sessionId) + private static void PrepareGameInitData(int sessionId, Scenario scenario, bool self, List startingPossessions) { - Current.Game.InitData = new GameInitData + if (!self) { - startingPawnCount = 3, - gameToLoad = "dummy" // Prevent special calculation path in GenTicks.TicksAbs - }; + Current.Game.InitData = new GameInitData() + { + startingPawnCount = GetStartingPawnsCountForScenario(scenario), + gameToLoad = preventSpecialCalculationPathInGenTicksTicksAbs + }; + } if (pawnStore.TryGetValue(sessionId, out var pawns)) { - Current.Game.InitData.startingAndOptionalPawns = pawns; - Current.Game.InitData.startingPossessions = new Dictionary>(); - foreach (var p in pawns) - Current.Game.InitData.startingPossessions[p] = new List(); + Pawn firstPawn = pawns.First(); + + GameInitData gameInitData = Current.Game.InitData; + gameInitData.startingAndOptionalPawns = pawns; + gameInitData.startingPossessions = new Dictionary>(); + + foreach (var pawn in pawns) + { + gameInitData.startingPossessions[pawn] = new List(); + } + + foreach (var possesion in startingPossessions) + { + gameInitData.startingPossessions[firstPawn].Add(possesion); + } pawnStore.Remove(sessionId); } } - private static Faction NewFactionWithIdeo(string name, FactionDef def, ChooseIdeoInfo chooseIdeoInfo) + private static Faction NewFactionWithIdeo(string name, FactionDef def, IdeologyData chooseIdeoInfo) { var faction = new Faction { @@ -144,6 +221,7 @@ private static Faction NewFactionWithIdeo(string name, FactionDef def, ChooseIde Name = name, hidden = true }; + faction.ideos = new FactionIdeosTracker(faction); if (!ModsConfig.IdeologyActive || Find.IdeoManager.classicMode || chooseIdeoInfo.SelectedIdeo == null) @@ -177,7 +255,7 @@ private static Faction NewFactionWithIdeo(string name, FactionDef def, ChooseIde return faction; } - private static Ideo GenerateIdeo(ChooseIdeoInfo chooseIdeoInfo) + private static Ideo GenerateIdeo(IdeologyData chooseIdeoInfo) { List list = chooseIdeoInfo.SelectedIdeo.memes.ToList(); @@ -191,4 +269,30 @@ private static Ideo GenerateIdeo(ChooseIdeoInfo chooseIdeoInfo) return ideo; } + + public static int GetStartingPawnsCountForScenario(Scenario scenario) + { + foreach (ScenPart part in scenario.AllParts) + { + if (part is ScenPart_ConfigPage_ConfigureStartingPawnsBase startingPawnsConfig) + { + return startingPawnsConfig.TotalPawnCount; + } + } + + MpLog.Error("No starting pawns config found to access startingPawnCount"); + + return 0; + } } +public record FactionCreationData : ISyncSimple +{ + public string factionName; + public int startingTile; + [CanBeNull] public ScenarioDef scenarioDef; + public IdeologyData chooseIdeoInfo; + public bool generateMap; + public List startingPossessions; + public bool setupNextMapFromTickZero; +} + diff --git a/Source/Client/Factions/FactionSidebar.cs b/Source/Client/Factions/FactionSidebar.cs index 4b7f0630..285dd523 100644 --- a/Source/Client/Factions/FactionSidebar.cs +++ b/Source/Client/Factions/FactionSidebar.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; -using Multiplayer.API; using Multiplayer.Client.Factions; using Multiplayer.Client.Util; using Multiplayer.Common; @@ -14,172 +13,154 @@ namespace Multiplayer.Client; public static class FactionSidebar { - private static ScenarioDef chosenScenario = ScenarioDefOf.Crashlanded; // null means current game's scenario - private static string newFactionName; + private static ScenarioDef chosenScenario = ScenarioDefOf.Crashlanded; + private static string factionNameTextField; + private static bool setupNextMapFromTickZero; + private static Vector2 scroll; + private static Rect factionBarRect; + private static Vector2 factionChooserScrollBarPos; + private static float buttonHeight = 25f; public static void DrawFactionSidebar(Rect rect) { using var _ = MpStyle.Set(GameFont.Small); - if (!Layouter.BeginArea(rect)) + if (!Layouter.BeginArea(rect, 0)) return; - Layouter.BeginScroll(ref scroll, spacing: 0f); - - using (MpStyle.Set(TextAnchor.MiddleLeft)) - using (MpStyle.Set(GameFont.Medium)) - Label("Create faction"); + factionBarRect = rect; DrawFactionCreator(); - Layouter.Rect(0, 20); - - using (MpStyle.Set(Color.gray)) - Widgets.DrawLineHorizontal(Layouter.LastRect().x, Layouter.LastRect().center.y, rect.width); - - using (MpStyle.Set(TextAnchor.MiddleLeft)) - using (MpStyle.Set(GameFont.Medium)) - Label("Join faction"); - DrawFactionChooser(); - Layouter.EndScroll(); Layouter.EndArea(); } private static void DrawFactionCreator() { - Layouter.BeginHorizontal(); - // Label($"Scenario: {chosenScenario?.label ?? Find.Scenario.name}"); - // - // if (Mouse.IsOver(Layouter.LastRect())) - // Widgets.DrawAltRect(Layouter.LastRect()); - // - // if (Widgets.ButtonInvisible(Layouter.LastRect())) - // OpenScenarioChooser(); + Layouter.BeginVertical(6, false); - Layouter.EndHorizontal(); + DrawHeadline("Create Faction"); - newFactionName = Widgets.TextField(Layouter.Rect(150, 24), newFactionName); + DrawFactionNameTextfield(); - if (Button("Settle new faction", 130)) - { - var tileError = new StringBuilder(); + DrawScenarioChooser(); - // todo check faction name not exists - if (newFactionName.NullOrEmpty()) - Messages.Message("The faction name can't be empty.", MessageTypeDefOf.RejectInput, historical: false); - else if (Event.current.button == 1) - { - Find.WindowStack.Add(new FloatMenu(new List() - { - new( - "Dev: create faction (no base)", () => DoCreateFaction(new ChooseIdeoInfo(null, null, null), false) - ) - })); - } - else if (Find.WorldInterface.SelectedTile < 0) - Messages.Message("MustSelectStartingSite".TranslateWithBackup("MustSelectLandingSite"), MessageTypeDefOf.RejectInput, historical: false); - else if (!TileFinder.IsValidTileForNewSettlement(Find.WorldInterface.SelectedTile, tileError)) - Messages.Message(tileError.ToString(), MessageTypeDefOf.RejectInput, historical: false); - else - { - PreparePawns(); + DrawDefaultMapTimeCheckbox(); + + DrawSettleButton(); + } - var pages = new List(); - Page_ChooseIdeo_Multifaction chooseIdeoPage = null; + private static void DrawSettleButton() + { + Layouter.BeginHorizontal(); - if (ModsConfig.IdeologyActive && !Find.IdeoManager.classicMode) - pages.Add(chooseIdeoPage = new Page_ChooseIdeo_Multifaction()); + Layouter.FlexibleWidth(); - pages.Add(new Page_ConfigureStartingPawns - { - nextAct = () => - { - DoCreateFaction( - new ChooseIdeoInfo( - chooseIdeoPage?.pageChooseIdeo.selectedIdeo, - chooseIdeoPage?.pageChooseIdeo.selectedStructure, - chooseIdeoPage?.pageChooseIdeo.selectedStyles - ), - true - ); - } - }); - - var page = PageUtility.StitchedPages(pages); - Find.WindowStack.Add(page); - } + if (Button("Settle faction", 130, buttonHeight) && FactionCreationCanBeStarted()) + { + OpenConfigurationPages(); } + + Layouter.EndHorizontal(); + + Layouter.EndVertical(); } - private static void OpenScenarioChooser() + private static void DrawDefaultMapTimeCheckbox() { - Find.WindowStack.Add(new FloatMenu( - DefDatabase.AllDefs. - Where(def => def.scenario.GetHashCode() != Find.Scenario.GetHashCode()). - Except(ScenarioDefOf.Tutorial). - Prepend(null). - Select(s => - { - return new FloatMenuOption(s?.label ?? Find.Scenario.name, () => - { - chosenScenario = s; - }); - }). - ToList())); + float spacingLabelCheckbox = 10; + bool asyncTimeDisabled = !Multiplayer.GameComp.asyncTime; + StringBuilder sb = new StringBuilder(); + Rect defaultMapTimeRect; + + Layouter.BeginHorizontal(spacingLabelCheckbox); + + Label("Default map time: "); + + defaultMapTimeRect = Layouter.LastRect(); + + MpUI.Checkbox( + (Layouter.LastRect().xMax / 2.0f) + (spacingLabelCheckbox / 2.0f), + Layouter.LastRect().y, + ref setupNextMapFromTickZero, + asyncTimeDisabled, + defaultMapTimeRect.height - 2); + + Layouter.EndHorizontal(); + + if (asyncTimeDisabled) + { + sb.Append("Only changeable with async setting active in host menu\n\n"); + } + + sb.AppendLine("This checkbox controls the starting time on the generated map\n"); + sb.AppendLine("Checked: Uses the default start time, similar to singleplayer maps\n"); + sb.AppendLine("Unchecked: Uses the current world time"); + + TooltipHandler.TipRegion(defaultMapTimeRect, sb.ToString()); } - private static void PreparePawns() + private static void OpenConfigurationPages() { - var scenario = chosenScenario?.scenario ?? Current.Game.Scenario; - var prevState = Current.programStateInt; + var gameConfigurationPages = new List(); + Page_ChooseIdeo_Multifaction ideologyConfigPage = null; + Page_ConfigureStartingPawns pawnConfigPage = null; - Current.programStateInt = ProgramState.Entry; // Set ProgramState.Entry so that InInterface is false + InitializeDataForGameConfigurationPages(); - Current.Game.InitData = new GameInitData + ideologyConfigPage = TryGetIdeologyConfigurationPage(); + + pawnConfigPage = new Page_ConfigureStartingPawns_Multifaction() { - startingPawnCount = 3, - startingPawnKind = scenario.playerFaction.factionDef.basicMemberKind, - gameToLoad = "dummy" // Prevent special calculation path in GenTicks.TicksAbs + nextAct = () => + { + IdeologyData ideologyData = ideologyConfigPage?.GetIdeologyData() ?? new IdeologyData(); + DoCreateFaction(ideologyData, true); + } }; - try - { - // Create starting pawns - new ScenPart_ConfigPage_ConfigureStartingPawns { pawnCount = Current.Game.InitData.startingPawnCount } - .GenerateStartingPawns(); - } - finally - { - Current.programStateInt = prevState; - } + if (ideologyConfigPage != null) + gameConfigurationPages.Add(ideologyConfigPage); + + gameConfigurationPages.Add(pawnConfigPage); + + Find.WindowStack.Add(PageUtility.StitchedPages(gameConfigurationPages)); } - private static void DoCreateFaction(ChooseIdeoInfo chooseIdeoInfo, bool generateMap) + private static void DoCreateFaction(IdeologyData chooseIdeoInfo, bool generateMap) { int playerId = Multiplayer.session.playerId; var prevState = Current.programStateInt; - Current.programStateInt = ProgramState.Playing; // This is to force a sync + List startingPawns = new List(); + FactionCreationData factionCreationDto = new FactionCreationData(); + + // OldComment: This is to force a sync + // TODO: Make this clearer without a needed comment + // LookUp Multiplayer.InInterface && Multiplayer.ShouldSync + + Current.programStateInt = ProgramState.Playing; try { if (Current.Game.InitData?.startingAndOptionalPawns is { } pawns) - foreach (var p in pawns) - FactionCreator.SendPawn( - playerId, - p - ); - - FactionCreator.CreateFaction( - playerId, - newFactionName, - Find.WorldInterface.SelectedTile, - chosenScenario, - chooseIdeoInfo, - generateMap - ); + for (int i = 0; i < Find.GameInitData.startingPawnCount; i++) + { + FactionCreator.SendPawn(playerId, pawns[i]); + startingPawns.Add(pawns[i]); + } + + factionCreationDto.factionName = factionNameTextField; + factionCreationDto.startingTile = Find.WorldInterface.SelectedTile; + factionCreationDto.scenarioDef = chosenScenario; + factionCreationDto.chooseIdeoInfo = chooseIdeoInfo; + factionCreationDto.generateMap = generateMap; + factionCreationDto.startingPossessions = GetStartingPossessions(startingPawns); + factionCreationDto.setupNextMapFromTickZero = setupNextMapFromTickZero; + + FactionCreator.CreateFaction(playerId, factionCreationDto); } finally { @@ -187,23 +168,43 @@ private static void DoCreateFaction(ChooseIdeoInfo chooseIdeoInfo, bool generate } } + private static void DrawHeadline(string headline) + { + using (MpStyle.Set(TextAnchor.MiddleLeft)) + using (MpStyle.Set(GameFont.Medium)) + Label(headline); + } + private static void DrawFactionChooser() { int i = 0; + float scrollSpacing = 4; + var allPlayerFactions = Find.FactionManager.AllFactions. + Where(f => f.def == FactionDefOf.PlayerColony || f.def == FactionDefOf.PlayerTribe). + Where(f => f.name != "Spectator"). + Where(f => !f.temporary); - foreach (var playerFaction in Find.FactionManager.AllFactions.Where(f => f.def == FactionDefOf.PlayerColony)) - { - if (playerFaction.Name == "Spectator") continue; + Layouter.BeginVertical(2); + + DrawHeadline("Join Faction"); + + Layouter.BeginScroll(ref factionChooserScrollBarPos, scrollSpacing); + Layouter.Rect(5, scrollSpacing / 2.0f); + foreach (var playerFaction in allPlayerFactions) + { Layouter.BeginHorizontal(); + if (i % 2 == 0) - Widgets.DrawAltRect(Layouter.GroupRect()); + { + Rect rect = Layouter.GroupRect(); + Widgets.DrawAltRect(new Rect(rect.x, rect.y - scrollSpacing / 2.0f, rect.width, rect.height + scrollSpacing)); + } - using (MpStyle.Set(TextAnchor.MiddleCenter)) - Label(playerFaction.Name, true); + using (MpStyle.Set(TextAnchor.MiddleLeft)) + Label($" {playerFaction.Name}", true); - Layouter.FlexibleWidth(); - if (Button("Join", 70)) + if (Button("Join", 70, buttonHeight)) { var factionHome = Find.Maps.FirstOrDefault(m => m.ParentFaction == playerFaction); if (factionHome != null) @@ -216,24 +217,160 @@ private static void DrawFactionChooser() playerFaction.loadID ); } + Layouter.EndHorizontal(); i++; } + + Layouter.Rect(5, scrollSpacing / 2.0f); + Layouter.EndScroll(); + Layouter.EndVertical(); } - public static void Label(string text, bool inheritHeight = false) + private static void DrawFactionNameTextfield() + { + float spacing = 10; + + Layouter.BeginHorizontal(spacing); + + using (MpStyle.Set(TextAnchor.MiddleLeft)) + Label("Faction name: ", true); + + factionNameTextField = Widgets.TextField(Layouter.Rect((factionBarRect.width / 2.0f) - (spacing / 2.0f), 20), factionNameTextField); + + Layouter.EndHorizontal(); + } + + private static bool FactionCreationCanBeStarted() + { + var tileError = new StringBuilder(); + + if (factionNameTextField.NullOrEmpty()) + { + ShowRejectInputMessage("The faction name can't be empty."); + return false; + } + else if (Find.FactionManager.AllFactions.Any(f => f.Name == factionNameTextField)) + { + ShowRejectInputMessage("The faction name is already taken"); + return false; + } + else if (MpVersion.IsDebug && Event.current.button == 1) + { + DrawDebugFactionCreationFloatMenu(); + return false; + } + else if (Find.WorldInterface.SelectedTile < 0) + { + ShowRejectInputMessage("MustSelectStartingSite".TranslateWithBackup("MustSelectLandingSite")); + return false; + } + else if (!TileFinder.IsValidTileForNewSettlement(Find.WorldInterface.SelectedTile, tileError)) + { + ShowRejectInputMessage(tileError.ToString()); + return false; + } + + return true; + } + + private static void ShowRejectInputMessage(string message) + { + Messages.Message(message, MessageTypeDefOf.RejectInput, historical: false); + } + + private static void DrawDebugFactionCreationFloatMenu() + { + Find.WindowStack.Add(new FloatMenu(new List() + { + new( + "Dev: create faction (no base)", () => DoCreateFaction(new IdeologyData(null, null, null), false) + ) + })); + } + + private static Page_ChooseIdeo_Multifaction TryGetIdeologyConfigurationPage() + { + Page_ChooseIdeo_Multifaction chooseIdeoPage = null; + + if (ModsConfig.IdeologyActive && !Find.IdeoManager.classicMode) + chooseIdeoPage = new Page_ChooseIdeo_Multifaction(); + + return chooseIdeoPage; + } + + private static void InitializeDataForGameConfigurationPages() + { + Current.Game.Scenario = chosenScenario.scenario; + + Current.Game.InitData = new GameInitData + { + startedFromEntry = true, + gameToLoad = FactionCreator.preventSpecialCalculationPathInGenTicksTicksAbs + }; + } + + private static void DrawScenarioChooser() + { + Layouter.BeginHorizontal(); + + using (MpStyle.Set(TextAnchor.MiddleLeft)) + { + Label($"Scenario: "); + Label(chosenScenario?.label ?? Find.Scenario.name); + } + + Layouter.EndHorizontal(); + + if (ModsConfig.AnomalyActive) + { + chosenScenario = ScenarioDefOf.Crashlanded; + TooltipHandler.TipRegion(Layouter.LastRect(),"Choosing a scenario is not available for Anomaly"); + return; + } + + if (Mouse.IsOver(Layouter.LastRect())) + Widgets.DrawAltRect(Layouter.LastRect()); + + if (Widgets.ButtonInvisible(Layouter.LastRect())) + OpenScenarioChooser(); + } + + private static void OpenScenarioChooser() + { + Find.WindowStack.Add(new FloatMenu( + DefDatabase.AllDefs. + Except(ScenarioDefOf.Tutorial). + Select(s => + { + return new FloatMenuOption(s.label, () => + { + chosenScenario = s; + }); + }). + ToList())); + } + + private static List GetStartingPossessions(List startingPawns) + { + Dictionary> allPossessions = Find.GameInitData.startingPossessions; + List startingPossessions = new List(); + + foreach(Pawn pawn in startingPawns) + { + startingPossessions.AddRange(allPossessions[pawn]); + } + + return startingPossessions; + } + + private static void Label(string text, bool inheritHeight = false) { GUI.Label(inheritHeight ? Layouter.FlexibleWidth() : Layouter.ContentRect(text), text, Text.CurFontStyle); } - public static bool Button(string text, float width, float height = 35f) + private static bool Button(string text, float width, float height = 35f) { return Widgets.ButtonText(Layouter.Rect(width, height), text); } } - -public record ChooseIdeoInfo( - IdeoPresetDef SelectedIdeo, - MemeDef SelectedStructure, - List SelectedStyles -) : ISyncSimple; diff --git a/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs index 0a312135..5a5b940b 100644 --- a/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs +++ b/Source/Client/Factions/Page_ChooseIdeo_Multifaction.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using Multiplayer.API; using RimWorld; using UnityEngine; using Verse; @@ -14,37 +16,37 @@ public class Page_ChooseIdeo_Multifaction : Page public override void DoWindowContents(Rect inRect) { DrawPageTitle(inRect); - float totalHeight = 0f; - Rect mainRect = GetMainRect(inRect); - TaggedString descText = "ChooseYourIdeoligionDesc".Translate(); - float descHeight = Text.CalcHeight(descText, mainRect.width); - Rect descRect = mainRect; - descRect.yMin += totalHeight; - Widgets.Label(descRect, descText); - totalHeight += descHeight + 10f; - - pageChooseIdeo.DrawStructureAndStyleSelection(inRect); - - Rect outRect = mainRect; - outRect.width = 954f; - outRect.yMin += totalHeight; - float num3 = (InitialSize.x - 937f) / 2f; - - Widgets.BeginScrollView( + float totalHeight = 0f; + Rect mainRect = GetMainRect(inRect); + TaggedString descText = "ChooseYourIdeoligionDesc".Translate(); + float descHeight = Text.CalcHeight(descText, mainRect.width); + Rect descRect = mainRect; + descRect.yMin += totalHeight; + Widgets.Label(descRect, descText); + totalHeight += descHeight + 10f; + + pageChooseIdeo.DrawStructureAndStyleSelection(inRect); + + Rect outRect = mainRect; + outRect.width = 954f; + outRect.yMin += totalHeight; + float num3 = (InitialSize.x - 937f) / 2f; + + Widgets.BeginScrollView( viewRect: new Rect(0f - num3, 0f, 921f, pageChooseIdeo.totalCategoryListHeight + 100f), outRect: outRect, scrollPosition: ref pageChooseIdeo.leftScrollPosition); - totalHeight = 0f; + totalHeight = 0f; pageChooseIdeo.lastCategoryGroupLabel = ""; - foreach (IdeoPresetCategoryDef item in DefDatabase.AllDefsListForReading.Where(c => c != IdeoPresetCategoryDefOf.Classic && c != IdeoPresetCategoryDefOf.Custom && c != IdeoPresetCategoryDefOf.Fluid)) - { - pageChooseIdeo.DrawCategory(item, ref totalHeight); - } + foreach (IdeoPresetCategoryDef item in DefDatabase.AllDefsListForReading.Where(c => c != IdeoPresetCategoryDefOf.Classic && c != IdeoPresetCategoryDefOf.Custom && c != IdeoPresetCategoryDefOf.Fluid)) + { + pageChooseIdeo.DrawCategory(item, ref totalHeight); + } pageChooseIdeo.totalCategoryListHeight = totalHeight; - Widgets.EndScrollView(); + Widgets.EndScrollView(); - DoBottomButtons(inRect); + DoBottomButtons(inRect); } public override bool CanDoNext() @@ -56,5 +58,16 @@ public override bool CanDoNext() } return base.CanDoNext(); + } + + public IdeologyData GetIdeologyData() + { + return new IdeologyData(pageChooseIdeo.selectedIdeo, pageChooseIdeo.selectedStructure, pageChooseIdeo.selectedStyles); } } + +public record IdeologyData( + IdeoPresetDef SelectedIdeo = null, + MemeDef SelectedStructure = null, + List SelectedStyles = null +) : ISyncSimple; diff --git a/Source/Client/Factions/Page_ConfigureStartingPawns_Multifaction.cs b/Source/Client/Factions/Page_ConfigureStartingPawns_Multifaction.cs new file mode 100644 index 00000000..38b0d968 --- /dev/null +++ b/Source/Client/Factions/Page_ConfigureStartingPawns_Multifaction.cs @@ -0,0 +1,86 @@ +using RimWorld; +using Verse; + +namespace Multiplayer.Client.Factions +{ + internal class Page_ConfigureStartingPawns_Multifaction : Page_ConfigureStartingPawns + { + Faction _temporaryFactionForPawnCreation; + Scenario _scenario; + + public override void PreOpen() + { + base.PreOpen(); + + _scenario = Find.Scenario; + + GenerateTemporaryFaction(); + GeneratePawnsForConfigurePage(); + } + + public override void PostClose() + { + base.PostClose(); + + CleanUpTemporaryFaction(); + } + + public override void DoNext() + { + CleanUpTemporaryFaction(); + + base.DoNext(); + } + + private void CleanUpTemporaryFaction() + { + if (_temporaryFactionForPawnCreation == null) return; + + Find.FactionManager.Remove(_temporaryFactionForPawnCreation); + Find.FactionManager.toRemove.Remove(_temporaryFactionForPawnCreation); + + Current.Game.InitData.playerFaction = null; + _temporaryFactionForPawnCreation = null; + } + + private void GenerateTemporaryFaction() + { + _temporaryFactionForPawnCreation = FactionGenerator.NewGeneratedFaction(new FactionGeneratorParms(_scenario.playerFaction.factionDef)); + _temporaryFactionForPawnCreation.temporary = true; + + Find.FactionManager.Add(_temporaryFactionForPawnCreation); + } + + private void GeneratePawnsForConfigurePage() + { + var prevProgramState = Current.programStateInt; + SetEntryProgramStateForCorrectPawnCreation(); + + Current.Game.InitData.playerFaction = _temporaryFactionForPawnCreation; + + try + { + GeneratingPawns(); + } + finally + { + Current.programStateInt = prevProgramState; + } + } + + private static void SetEntryProgramStateForCorrectPawnCreation() + { + // Set ProgramState.Entry to ensure InInterface is false. + // (#508 Tick-git) InInterface seems unrelated to PawnCreation to me, + // but it's necessary because it makes Faction.OfPlayer return + // the temporary generated faction — which is required for proper apparel generation. + + Current.programStateInt = ProgramState.Entry; + } + + private void GeneratingPawns() + { + _scenario.PostIdeoChosen(); + } + } +} diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 225e717a..e2dbe26e 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -157,11 +157,7 @@ private static void SetupGameFromSingleplayer() foreach (Map map in Find.Maps) { - MapSetup.SetupMap(map); - - AsyncTimeComp async = map.AsyncTime(); - async.mapTicks = Find.TickManager.TicksGame; - async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); + MapSetup.SetupMap(map, true); } } diff --git a/Source/Client/Patches/MapSetup.cs b/Source/Client/Patches/MapSetup.cs index 86cf666b..a3a1d066 100644 --- a/Source/Client/Patches/MapSetup.cs +++ b/Source/Client/Patches/MapSetup.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using HarmonyLib; using RimWorld; @@ -9,6 +9,8 @@ namespace Multiplayer.Client; [HarmonyPatch(typeof(MapGenerator), nameof(MapGenerator.GenerateMap))] public static class MapSetup { + public static bool SetupNextMapFromTickZero = false; + static void Prefix(ref Action extraInitBeforeContentGen) { if (Multiplayer.Client == null) return; @@ -16,30 +18,75 @@ static void Prefix(ref Action extraInitBeforeContentGen) } public static void SetupMap(Map map) + { + SetupMap(map, false); + } + + public static void SetupMap(Map map, bool usingMapTimeFromSingleplayer = false) { Log.Message("MP: Setting up map " + map.uniqueID); - // Initialize and store Multiplayer components - var async = new AsyncTimeComp(map); - Multiplayer.game.asyncTimeComps.Add(async); + // Initialize and store Multiplayer var mapComp = new MultiplayerMapComp(map); Multiplayer.game.mapComps.Add(mapComp); + var async = CreateAsyncTimeCompForMap(map, usingMapTimeFromSingleplayer); + Multiplayer.game.asyncTimeComps.Add(async); + // Store all current managers for Faction.OfPlayer InitFactionDataFromMap(map, Faction.OfPlayer); // Add all other (non Faction.OfPlayer) factions to the map foreach (var faction in Find.FactionManager.AllFactions.Where(f => f.IsPlayer)) if (faction != Faction.OfPlayer) - InitNewFactionData(map, faction); + InitNewFactionData(map, faction); + } - async.mapTicks = Find.Maps.Where(m => m != map).Select(m => m.AsyncTime()?.mapTicks).Max() ?? Find.TickManager.TicksGame; - async.storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty); - async.storyWatcher = new StoryWatcher(); + private static AsyncTimeComp CreateAsyncTimeCompForMap(Map map, bool usingMapTimeFromSingleplayer) + { + int startingMapTicks; + int gameStartAbsTick; + TimeSpeed startingTimeSpeed; + AsyncTimeComp asyncTimeCompForMap; + + bool startingMapTimeFromBeginning = + Multiplayer.GameComp.multifaction && + Multiplayer.GameComp.asyncTime && + SetupNextMapFromTickZero; + + if (usingMapTimeFromSingleplayer) + { + startingMapTicks = Find.TickManager.TicksGame; + gameStartAbsTick = Find.TickManager.gameStartAbsTick; + startingTimeSpeed = Find.TickManager.CurTimeSpeed; + } + else if (startingMapTimeFromBeginning) + { + startingMapTicks = 0; + gameStartAbsTick = GenTicks.ConfiguredTicksAbsAtGameStart; + startingTimeSpeed = TimeSpeed.Paused; + } + else + { + startingMapTicks = Find.Maps.Where(m => m != map).Select(m => m.AsyncTime()?.mapTicks).Max() ?? Find.TickManager.TicksGame; + gameStartAbsTick = Find.TickManager.gameStartAbsTick; + startingTimeSpeed = TimeSpeed.Paused; + } if (!Multiplayer.GameComp.asyncTime) - async.SetDesiredTimeSpeed(Find.TickManager.CurTimeSpeed); + startingTimeSpeed = Find.TickManager.CurTimeSpeed; + + asyncTimeCompForMap = new AsyncTimeComp(map, gameStartAbsTick); + asyncTimeCompForMap.mapTicks = startingMapTicks; + asyncTimeCompForMap.SetDesiredTimeSpeed(startingTimeSpeed); + + asyncTimeCompForMap.storyteller = new Storyteller(Find.Storyteller.def, Find.Storyteller.difficultyDef, Find.Storyteller.difficulty); + asyncTimeCompForMap.storyWatcher = new StoryWatcher(); + + SetupNextMapFromTickZero = false; + + return asyncTimeCompForMap; } private static void InitFactionDataFromMap(Map map, Faction f) diff --git a/Source/Client/Saving/SavingPatches.cs b/Source/Client/Saving/SavingPatches.cs index 9ab6cc37..95832ff1 100644 --- a/Source/Client/Saving/SavingPatches.cs +++ b/Source/Client/Saving/SavingPatches.cs @@ -104,7 +104,7 @@ static void Postfix(Map __instance) if (Scribe.mode == LoadSaveMode.LoadingVars || Scribe.mode == LoadSaveMode.Saving) { - Scribe_Deep.Look(ref asyncTime, "mpAsyncTime", __instance); + Scribe_Deep.Look(ref asyncTime, "mpAsyncTime", __instance, 0); Scribe_Deep.Look(ref comp, "mpMapComp", __instance); } @@ -114,7 +114,7 @@ static void Postfix(Map __instance) { Log.Error($"{typeof(AsyncTimeComp)} missing during loading"); // This is just so the game doesn't completely freeze - asyncTime = new AsyncTimeComp(__instance); + asyncTime = new AsyncTimeComp(__instance, 0); } Multiplayer.game.asyncTimeComps.Add(asyncTime); diff --git a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs index 0ceefbe2..33f4dd02 100644 --- a/Source/Client/Syncing/Dict/SyncDictRimWorld.cs +++ b/Source/Client/Syncing/Dict/SyncDictRimWorld.cs @@ -810,6 +810,23 @@ public static class SyncDictRimWorld } }, true // implicit }, + { + (SyncWorker sync, ref ThingDefCount thingDefCount) => + { + if (sync.isWriting) + { + sync.Write(thingDefCount.ThingDef); + sync.Write(thingDefCount.Count); + } + else + { + var def = sync.Read(); + var count = sync.Read(); + + thingDefCount = new ThingDefCount(def, count); + } + } + }, #endregion #region Databases diff --git a/Source/Client/Util/MpUI.cs b/Source/Client/Util/MpUI.cs index 23244053..b80ea128 100644 --- a/Source/Client/Util/MpUI.cs +++ b/Source/Client/Util/MpUI.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using HarmonyLib; using RimWorld; using UnityEngine; @@ -57,6 +57,20 @@ public static void Label(Rect rect, string label, GameFont? font = null, TextAnc Text.Font = prevFont; } + public static void Checkbox(float x, float y, ref bool checkOn, bool disabled, float size) + { + if (!disabled && Widgets.ButtonInvisible(new Rect(x, y, size, size), false)) + { + checkOn = !checkOn; + if (checkOn) + SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); + else + SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); + } + + Widgets.CheckboxDraw(x, y, checkOn, disabled, size); + } + 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; @@ -76,19 +90,10 @@ public static Rect CheckboxLabeled(Rect rect, string label, ref bool checkOn, bo Widgets.Label(rect, label); - if (!disabled && Widgets.ButtonInvisible(rect, false)) - { - checkOn = !checkOn; - if (checkOn) - SoundDefOf.Checkbox_TurnedOn.PlayOneShotOnCamera(null); - else - SoundDefOf.Checkbox_TurnedOff.PlayOneShotOnCamera(null); - } - - Widgets.CheckboxDraw( + Checkbox( rect.xMax - size, rect.y + (rect.height - size) / 2, - checkOn, + ref checkOn, disabled, size); @@ -248,7 +253,7 @@ public static void DoPasswordField(Rect rect, string controlName, ref string pas Text.textFieldStyles[1] ); else - password = GUI.TextField( + password = GUI.TextField( rect, password, Text.textFieldStyles[1]