Skip to content

Commit 0da664f

Browse files
Expand CampaignSelector with support for game options and forced spawn options (CnCNet#818)
* Refactor campaign launcher to use INI class instead of just StreamWriter Co-authored-by: SadPencil <[email protected]> * Unify campaign launch routine with game lobby one a bit more * Improvements to game options and campaign support - Added basic support for game options in campaign - Introduced interfaces for the common functionality - Refactored the code to encapsulate better - Added `CampaignForcedSpawnIniOptions` * Add saving of game settings for campaign selector * Add docs * Ban disallowed side indices settings from campaign selector * Redo in a cleaner way * File-scoped namespace; fix consistency of list parsing * Post-merge fixes --------- Co-authored-by: SadPencil <[email protected]>
1 parent e7d4712 commit 0da664f

17 files changed

+727
-378
lines changed

ClientCore/ClientConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ public string GetGameExecutableName()
382382
public string GameLauncherExecutableName => clientDefinitionsIni.GetStringValue(SETTINGS, "GameLauncherExecutableName", string.Empty);
383383

384384
public bool SaveSkirmishGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, "SaveSkirmishGameOptions", false);
385+
386+
public bool SaveCampaignGameOptions => clientDefinitionsIni.GetBooleanValue(SETTINGS, "SaveCampaignGameOptions", false);
385387

386388
public bool CreateSavedGamesDirectory => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CreateSavedGamesDirectory", false);
387389

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
3+
using ClientCore;
4+
5+
using DTAClient.DXGUI.Generic;
6+
7+
using Rampastring.Tools;
8+
using Rampastring.XNAUI;
9+
using Rampastring.XNAUI.XNAControls;
10+
11+
namespace DTAClient.DXGUI.Campaign;
12+
13+
public class CampaignCheckBox : GameSessionCheckBox
14+
{
15+
public CampaignCheckBox(WindowManager windowManager) : base (windowManager) { }
16+
17+
public override void Initialize()
18+
{
19+
// Find the campaign selector that this control belongs to and register ourselves as a game option.
20+
21+
XNAControl parent = Parent;
22+
while (true)
23+
{
24+
if (parent == null)
25+
break;
26+
27+
// oh no, we have a circular class reference here!
28+
if (parent is CampaignSelector configView)
29+
{
30+
configView.CheckBoxes.Add(this);
31+
break;
32+
}
33+
34+
parent = parent.Parent;
35+
}
36+
37+
base.Initialize();
38+
}
39+
40+
protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)
41+
{
42+
switch (key)
43+
{
44+
case "CustomIniPath" when !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI:
45+
throw new Exception($"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\n\n"
46+
+ $"Offending setting control: {Name}");
47+
}
48+
49+
base.ParseControlINIAttribute(iniFile, key, value);
50+
}
51+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
3+
using ClientCore;
4+
5+
using DTAClient.DXGUI.Generic;
6+
7+
using Rampastring.Tools;
8+
using Rampastring.XNAUI;
9+
using Rampastring.XNAUI.XNAControls;
10+
11+
namespace DTAClient.DXGUI.Campaign;
12+
13+
public class CampaignDropDown : GameSessionDropDown
14+
{
15+
public CampaignDropDown(WindowManager windowManager) : base (windowManager) { }
16+
17+
public override void Initialize()
18+
{
19+
// Find the campaign selector that this control belongs to and register ourselves as a game option.
20+
21+
XNAControl parent = Parent;
22+
while (true)
23+
{
24+
if (parent == null)
25+
break;
26+
27+
// oh no, we have a circular class reference here!
28+
if (parent is CampaignSelector configView)
29+
{
30+
configView.DropDowns.Add(this);
31+
break;
32+
}
33+
34+
parent = parent.Parent;
35+
}
36+
37+
base.Initialize();
38+
}
39+
40+
protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)
41+
{
42+
if (key == "DataWriteMode" && value.ToUpper() == "MAPCODE" && !ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)
43+
{
44+
throw new Exception($"Campaign settings can't affect map code if {nameof(ClientConfiguration.Instance.CopyMissionsToSpawnmapINI)} is disabled!\n\n"
45+
+ $"Offending setting control: {Name}");
46+
}
47+
48+
base.ParseControlINIAttribute(iniFile, key, value);
49+
}
50+
}

DXMainClient/DXGUI/Generic/CampaignSelector.cs renamed to DXMainClient/DXGUI/Campaign/CampaignSelector.cs

Lines changed: 158 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1-
using ClientCore;
2-
using Microsoft.Xna.Framework;
31
using System;
42
using System.Collections.Generic;
5-
using DTAClient.Domain;
3+
using System.Globalization;
64
using System.IO;
5+
6+
using ClientCore;
7+
using ClientCore.Enums;
8+
using ClientCore.Extensions;
9+
710
using ClientGUI;
8-
using Rampastring.XNAUI.XNAControls;
9-
using Rampastring.XNAUI;
10-
using Rampastring.Tools;
11+
1112
using ClientUpdater;
12-
using ClientCore.Extensions;
13-
using ClientCore.Enums;
1413

15-
namespace DTAClient.DXGUI.Generic
14+
using DTAClient.Domain;
15+
16+
using Microsoft.Xna.Framework;
17+
18+
using Rampastring.Tools;
19+
using Rampastring.XNAUI;
20+
using Rampastring.XNAUI.XNAControls;
21+
22+
namespace DTAClient.DXGUI.Campaign
1623
{
1724
public class CampaignSelector : XNAWindow
1825
{
1926
private const int DEFAULT_WIDTH = 650;
2027
private const int DEFAULT_HEIGHT = 600;
2128

29+
private const string SETTINGS_PATH = "Client/CampaignSettings.ini";
30+
2231
private static string[] DifficultyNames = new string[] { "Easy", "Medium", "Hard" };
2332

2433
private static string[] DifficultyIniPaths = new string[]
@@ -42,6 +51,11 @@ public CampaignSelector(WindowManager windowManager, DiscordHandler discordHandl
4251
private XNATrackbar trbDifficultySelector;
4352

4453
private CheaterWindow cheaterWindow;
54+
55+
public List<CampaignCheckBox> CheckBoxes { get; } = new();
56+
public List<CampaignDropDown> DropDowns { get; } = new();
57+
58+
private IniFile gameOptionsIni;
4559

4660
private string[] filesToCheck = new string[]
4761
{
@@ -171,6 +185,9 @@ public override void Initialize()
171185
AddChild(lblNormal);
172186
AddChild(lblHard);
173187

188+
gameOptionsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GetBaseResourcePath(),
189+
ClientConfiguration.GAME_OPTIONS));
190+
174191
// Set control attributes from INI file
175192
base.Initialize();
176193

@@ -189,6 +206,8 @@ public override void Initialize()
189206
cheaterWindow.CenterOnParent();
190207
cheaterWindow.YesClicked += CheaterWindow_YesClicked;
191208
cheaterWindow.Disable();
209+
210+
LoadSettings();
192211
}
193212

194213
private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)
@@ -222,11 +241,14 @@ private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)
222241

223242
private void BtnCancel_LeftClick(object sender, EventArgs e)
224243
{
244+
SaveSettings();
225245
Disable();
226246
}
227247

228248
private void BtnLaunch_LeftClick(object sender, EventArgs e)
229249
{
250+
SaveSettings();
251+
230252
int selectedMissionId = lbCampaignList.SelectedIndex;
231253

232254
Mission mission = Missions[selectedMissionId];
@@ -268,54 +290,94 @@ private void CheaterWindow_YesClicked(object sender, EventArgs e)
268290
/// </summary>
269291
private void LaunchMission(Mission mission)
270292
{
293+
string scenario = mission.Scenario;
294+
295+
FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS);
296+
297+
spawnerSettingsFile.Delete();
298+
271299
bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI;
272300

273301
Logger.Log("About to write spawn.ini.");
274-
using (var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini")))
302+
IniFile spawnIni = new(spawnerSettingsFile.FullName)
275303
{
276-
spawnStreamWriter.WriteLine("; Generated by DTA Client");
277-
spawnStreamWriter.WriteLine("[Settings]");
278-
if (copyMapsToSpawnmapINI)
279-
spawnStreamWriter.WriteLine("Scenario=spawnmap.ini");
280-
else
281-
spawnStreamWriter.WriteLine("Scenario=" + mission.Scenario);
282-
283-
// No one wants to play missions on Fastest, so we'll change it to Faster
284-
if (UserINISettings.Instance.GameSpeed == 0)
285-
UserINISettings.Instance.GameSpeed.Value = 1;
286-
287-
spawnStreamWriter.WriteLine("CampaignID=" + mission.CampaignID);
288-
spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed);
289-
290-
if (ClientConfiguration.Instance.ClientGameType == ClientType.YR ||
291-
ClientConfiguration.Instance.ClientGameType == ClientType.Ares)
292-
spawnStreamWriter.WriteLine("Ra2Mode=" + !mission.RequiredAddon);
293-
else if (ClientConfiguration.Instance.ClientGameType == ClientType.TS)
294-
spawnStreamWriter.WriteLine("Firestorm=" + mission.RequiredAddon);
295-
296-
spawnStreamWriter.WriteLine("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));
297-
spawnStreamWriter.WriteLine("IsSinglePlayer=Yes");
298-
spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack);
299-
spawnStreamWriter.WriteLine("Side=" + mission.Side);
300-
spawnStreamWriter.WriteLine("BuildOffAlly=" + mission.BuildOffAlly);
301-
302-
UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;
303-
304-
spawnStreamWriter.WriteLine("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString()));
305-
spawnStreamWriter.WriteLine("DifficultyModeComputer=" + GetComputerDifficulty());
306-
307-
spawnStreamWriter.WriteLine();
308-
spawnStreamWriter.WriteLine();
309-
spawnStreamWriter.WriteLine();
304+
Comment = "Generated by CnCNet Client"
305+
};
306+
IniSection spawnIniSettings = new("Settings");
307+
308+
if (copyMapsToSpawnmapINI)
309+
spawnIniSettings.AddKey("Scenario", "spawnmap.ini");
310+
else
311+
spawnIniSettings.AddKey("Scenario", scenario.ToUpperInvariant());
312+
313+
// No one wants to play missions on Fastest, so we'll change it to Faster
314+
if (UserINISettings.Instance.GameSpeed == 0)
315+
UserINISettings.Instance.GameSpeed.Value = 1;
316+
317+
spawnIniSettings.AddKey("CampaignID", mission.CampaignID.ToString(CultureInfo.InvariantCulture));
318+
spawnIniSettings.AddKey("GameSpeed", UserINISettings.Instance.GameSpeed.ToString());
319+
320+
switch (ClientConfiguration.Instance.ClientGameType)
321+
{
322+
case ClientType.YR or ClientType.Ares:
323+
spawnIniSettings.AddKey("Ra2Mode", (!mission.RequiredAddon).ToString(CultureInfo.InvariantCulture));
324+
break;
325+
case ClientType.TS:
326+
spawnIniSettings.AddKey("Firestorm", mission.RequiredAddon.ToString(CultureInfo.InvariantCulture));
327+
break;
328+
// TODO figure out the RA one
329+
}
330+
331+
spawnIniSettings.AddKey("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));
332+
333+
spawnIniSettings.AddKey("IsSinglePlayer", "Yes");
334+
spawnIniSettings.AddKey("SidebarHack", ClientConfiguration.Instance.SidebarHack.ToString(CultureInfo.InvariantCulture));
335+
spawnIniSettings.AddKey("Side", mission.Side.ToString(CultureInfo.InvariantCulture));
336+
spawnIniSettings.AddKey("BuildOffAlly", mission.BuildOffAlly.ToString(CultureInfo.InvariantCulture));
337+
338+
UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;
339+
340+
spawnIniSettings.AddKey("DifficultyModeHuman", mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString(CultureInfo.InvariantCulture));
341+
spawnIniSettings.AddKey("DifficultyModeComputer", GetComputerDifficulty().ToString(CultureInfo.InvariantCulture));
342+
343+
spawnIni.AddSection(spawnIniSettings);
344+
345+
foreach (CampaignCheckBox chkBox in CheckBoxes)
346+
chkBox.ApplySpawnIniCode(spawnIni);
347+
348+
foreach (CampaignDropDown dd in DropDowns)
349+
dd.ApplySpawnIniCode(spawnIni);
350+
351+
// Apply forced options from GameOptions.ini
352+
353+
List<string> forcedKeys = gameOptionsIni.GetSectionKeys("CampaignForcedSpawnIniOptions");
354+
355+
if (forcedKeys != null)
356+
{
357+
foreach (string key in forcedKeys)
358+
{
359+
spawnIni.SetStringValue("Settings", key,
360+
gameOptionsIni.GetStringValue("CampaignForcedSpawnIniOptions", key, String.Empty));
361+
}
310362
}
311363

364+
spawnIni.WriteIniFile();
365+
312366
var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value]));
313367
string difficultyName = DifficultyNames[trbDifficultySelector.Value];
314368

315369
if (copyMapsToSpawnmapINI)
316370
{
317371
var mapIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario));
372+
318373
IniFile.ConsolidateIniFiles(mapIni, difficultyIni);
374+
375+
foreach (CampaignCheckBox chkBox in CheckBoxes)
376+
chkBox.ApplyMapCode(mapIni, gameMode: null);
377+
378+
foreach (CampaignDropDown dd in DropDowns)
379+
dd.ApplyMapCode(mapIni, gameMode: null);
380+
319381
mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawnmap.ini"));
320382
}
321383

@@ -420,6 +482,58 @@ private bool ParseBattleIni(string path)
420482
Logger.Log("Finished parsing " + path + ".");
421483
return true;
422484
}
485+
486+
/// <summary>
487+
/// Saves settings to an INI file on the file system.
488+
/// </summary>
489+
private void SaveSettings()
490+
{
491+
if (!ClientConfiguration.Instance.SaveCampaignGameOptions)
492+
return;
493+
494+
try
495+
{
496+
FileInfo settingsFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SETTINGS_PATH);
497+
498+
settingsFileInfo.Delete();
499+
500+
var settingsIni = new IniFile(settingsFileInfo.FullName);
501+
502+
foreach (CampaignDropDown dd in DropDowns)
503+
settingsIni.SetStringValue("GameOptions", dd.Name, dd.SelectedIndex.ToString());
504+
505+
foreach (CampaignCheckBox cb in CheckBoxes)
506+
settingsIni.SetStringValue("GameOptions", cb.Name, cb.Checked.ToString());
507+
508+
settingsIni.WriteIniFile();
509+
}
510+
catch (Exception ex)
511+
{
512+
Logger.Log($"Saving campaign settings failed! Reason: {ex}");
513+
}
514+
}
515+
516+
/// <summary>
517+
/// Loads settings from an INI file on the file system.
518+
/// </summary>
519+
private void LoadSettings()
520+
{
521+
if (!ClientConfiguration.Instance.SaveCampaignGameOptions)
522+
return;
523+
524+
var settingsIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, SETTINGS_PATH));
525+
526+
foreach (CampaignDropDown dd in DropDowns)
527+
{
528+
dd.SelectedIndex = settingsIni.GetIntValue("GameOptions", dd.Name, dd.SelectedIndex);
529+
530+
if (dd.SelectedIndex > -1 && dd.SelectedIndex < dd.Items.Count)
531+
dd.SelectedIndex = dd.SelectedIndex;
532+
}
533+
534+
foreach (CampaignCheckBox cb in CheckBoxes)
535+
cb.Checked = settingsIni.GetBooleanValue("GameOptions", cb.Name, cb.Checked);
536+
}
423537

424538
public override void Draw(GameTime gameTime)
425539
{

0 commit comments

Comments
 (0)