Skip to content

Commit 6414310

Browse files
committed
Implement custom mission support with CampaignTagSelector
1 parent 928f04c commit 6414310

File tree

12 files changed

+467
-114
lines changed

12 files changed

+467
-114
lines changed

ClientCore/ClientConfiguration.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ private List<TranslationGameFile> ParseTranslationGameFiles()
322322

323323
public string AllowedCustomGameModes => clientDefinitionsIni.GetStringValue(SETTINGS, "AllowedCustomGameModes", "Standard,Custom Map");
324324

325+
public bool CampaignTagSelectorEnabled => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CampaignTagSelectorEnabled", false);
326+
325327
public string GetGameExecutableName()
326328
{
327329
string[] exeNames = clientDefinitionsIni.GetStringValue(SETTINGS, "GameExecutableNames", "Game.exe").Split(',');
@@ -383,6 +385,11 @@ public IEnumerable<string> SupplementalMapFileExtensions
383385

384386
#endregion
385387

388+
public string CustomMissionPath => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionPath", "Maps/CustomMissions");
389+
public string CustomMissionCsfName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionCsfName", "stringtable99.csf");
390+
public string CustomMissionPalName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionPalName", "custommission.pal");
391+
public string CustomMissionShpName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionShpName", "custommission.shp");
392+
386393
public OSVersion GetOperatingSystemVersion()
387394
{
388395
#if NETFRAMEWORK

ClientGUI/INItializableWindow.cs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,43 @@ public INItializableWindow(WindowManager windowManager) : base(windowManager)
2929
/// instead of the window's name.
3030
/// </summary>
3131
protected string IniNameOverride { get; set; }
32+
private bool VisitChild(IEnumerable<XNAControl> list, Func<XNAControl, bool> judge)
33+
{
34+
foreach (XNAControl child in list)
35+
{
36+
bool stop = judge(child);
37+
if (stop) return true;
38+
stop = VisitChild(child.Children, judge);
39+
if (stop) return true;
40+
}
41+
return false;
42+
}
3243

3344
public T FindChild<T>(string childName, bool optional = false) where T : XNAControl
3445
{
35-
T child = FindChild<T>(Children, childName);
36-
if (child == null && !optional)
46+
XNAControl result = null;
47+
VisitChild(new List<XNAControl>() { this }, control =>
48+
{
49+
if (control.Name != childName) return false;
50+
result = control;
51+
return true;
52+
});
53+
if (result == null && !optional)
3754
throw new KeyNotFoundException("Could not find required child control: " + childName);
38-
39-
return child;
55+
return (T)result;
4056
}
4157

42-
private T FindChild<T>(IEnumerable<XNAControl> list, string controlName) where T : XNAControl
58+
public List<T> FindChildrenStartWith<T>(string prefix) where T : XNAControl
4359
{
44-
foreach (XNAControl child in list)
60+
List<T> result = new List<T>();
61+
VisitChild(new List<XNAControl>() { this }, (control) =>
4562
{
46-
if (child.Name == controlName)
47-
return (T)child;
48-
49-
T childOfChild = FindChild<T>(child.Children, controlName);
50-
if (childOfChild != null)
51-
return childOfChild;
52-
}
53-
54-
return null;
63+
if (string.IsNullOrEmpty(prefix) ||
64+
!string.IsNullOrEmpty(control.Name) && control.Name.StartsWith(prefix))
65+
result.Add((T)control);
66+
return false;
67+
});
68+
return result;
5569
}
5670

5771
/// <summary>

DXMainClient/DXGUI/GameClass.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager)
226226
.AddSingletonXnaControl<CnCNetGameLoadingLobby>()
227227
.AddSingletonXnaControl<CnCNetLobby>()
228228
.AddSingletonXnaControl<GameInProgressWindow>()
229+
.AddSingletonXnaControl<CampaignTagSelector>()
230+
.AddSingletonXnaControl<GameLoadingWindow>()
229231
.AddSingletonXnaControl<SkirmishLobby>()
230232
.AddSingletonXnaControl<MainMenu>()
231233
.AddSingletonXnaControl<MapPreviewBox>()

DXMainClient/DXGUI/Generic/CampaignSelector.cs

Lines changed: 153 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
using ClientCore;
2-
using Microsoft.Xna.Framework;
3-
using System;
1+
using System;
42
using System.Collections.Generic;
5-
using DTAClient.Domain;
63
using System.IO;
4+
using System.Linq;
5+
using ClientCore;
76
using ClientGUI;
8-
using Rampastring.XNAUI.XNAControls;
9-
using Rampastring.XNAUI;
10-
using Rampastring.Tools;
117
using ClientUpdater;
128
using ClientCore.Extensions;
9+
using DTAClient.Domain;
10+
using Microsoft.Xna.Framework;
11+
using Rampastring.Tools;
12+
using Rampastring.XNAUI;
13+
using Rampastring.XNAUI.XNAControls;
14+
using System.Diagnostics;
15+
using System.Globalization;
1316

1417
namespace DTAClient.DXGUI.Generic
1518
{
@@ -34,7 +37,7 @@ public CampaignSelector(WindowManager windowManager, DiscordHandler discordHandl
3437

3538
private DiscordHandler discordHandler;
3639

37-
private List<Mission> Missions = new List<Mission>();
40+
private List<Mission> lbCampaignListMissions = new List<Mission>();
3841
private XNAListBox lbCampaignList;
3942
private XNAClientButton btnLaunch;
4043
private XNATextBlock tbMissionDescription;
@@ -57,6 +60,30 @@ public CampaignSelector(WindowManager windowManager, DiscordHandler discordHandl
5760

5861
private Mission missionToLaunch;
5962

63+
private List<Mission> _allMissions = [];
64+
public IReadOnlyCollection<Mission> AllMissions { get => _allMissions; }
65+
66+
private Dictionary<int, Mission> _uniqueIDToMissions = new();
67+
public IReadOnlyDictionary<int, Mission> UniqueIDToMissions => _uniqueIDToMissions;
68+
69+
private void AddMission(Mission mission)
70+
{
71+
// no matter whether the key is duplicated, the mission is always added to AllMissions
72+
_allMissions.Add(mission);
73+
74+
// but only the first mission is recorded in UniqueIDToMissions
75+
if (_uniqueIDToMissions.ContainsKey(mission.MissionID))
76+
{
77+
Logger.Log($"CampaignSelector: duplicated mission. CodeName: {mission.CodeName}. ID: {mission.MissionID}. Description: {mission.UntranslatedGUIName}.");
78+
if (!string.IsNullOrEmpty(mission.Scenario))
79+
mission.Enabled = false;
80+
}
81+
else
82+
{
83+
_uniqueIDToMissions.Add(mission.MissionID, mission);
84+
}
85+
}
86+
6087
public override void Initialize()
6188
{
6289
BackgroundTexture = AssetLoader.LoadTexture("missionselectorbg.png");
@@ -199,7 +226,7 @@ private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)
199226
return;
200227
}
201228

202-
Mission mission = Missions[lbCampaignList.SelectedIndex];
229+
Mission mission = lbCampaignListMissions[lbCampaignList.SelectedIndex];
203230

204231
if (string.IsNullOrEmpty(mission.Scenario))
205232
{
@@ -221,14 +248,14 @@ private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)
221248

222249
private void BtnCancel_LeftClick(object sender, EventArgs e)
223250
{
224-
Enabled = false;
251+
Disable();
225252
}
226253

227254
private void BtnLaunch_LeftClick(object sender, EventArgs e)
228255
{
229256
int selectedMissionId = lbCampaignList.SelectedIndex;
230257

231-
Mission mission = Missions[selectedMissionId];
258+
Mission mission = lbCampaignListMissions[selectedMissionId];
232259

233260
if (!ClientConfiguration.Instance.ModMode &&
234261
(!Updater.IsFileNonexistantOrOriginal(mission.Scenario) || AreFilesModified()))
@@ -267,45 +294,70 @@ private void CheaterWindow_YesClicked(object sender, EventArgs e)
267294
/// </summary>
268295
private void LaunchMission(Mission mission)
269296
{
297+
CustomMissionHelper.DeleteSupplementalMissionFiles();
298+
CustomMissionHelper.CopySupplementalMissionFiles(mission);
299+
300+
string scenario = mission.Scenario;
301+
270302
bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI;
271303

272304
Logger.Log("About to write spawn.ini.");
273-
using (var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini")))
305+
IniFile spawnIni = new()
274306
{
275-
spawnStreamWriter.WriteLine("; Generated by DTA Client");
276-
spawnStreamWriter.WriteLine("[Settings]");
277-
if (copyMapsToSpawnmapINI)
278-
spawnStreamWriter.WriteLine("Scenario=spawnmap.ini");
279-
else
280-
spawnStreamWriter.WriteLine("Scenario=" + mission.Scenario);
307+
Comment = "Generated by CnCNet Client"
308+
};
309+
IniSection spawnIniSettings = new("Settings");
310+
311+
if (copyMapsToSpawnmapINI)
312+
spawnIniSettings.AddKey("Scenario", "spawnmap.ini");
313+
else
314+
spawnIniSettings.AddKey("Scenario", scenario);
281315

282316
// No one wants to play missions on Fastest, so we'll change it to Faster
283317
if (UserINISettings.Instance.GameSpeed == 0)
284318
UserINISettings.Instance.GameSpeed.Value = 1;
285319

286-
spawnStreamWriter.WriteLine("CampaignID=" + mission.Index);
287-
spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed);
320+
spawnIniSettings.AddKey("GameSpeed", UserINISettings.Instance.GameSpeed.ToString());
288321
#if YR || ARES
289-
spawnStreamWriter.WriteLine("Ra2Mode=" + !mission.RequiredAddon);
322+
spawnIniSettings.AddKey("Ra2Mode", (!mission.RequiredAddon).ToString(CultureInfo.InvariantCulture));
290323
#else
291-
spawnStreamWriter.WriteLine("Firestorm=" + mission.RequiredAddon);
324+
spawnIniSettings.AddKey("Firestorm", mission.RequiredAddon.ToString(CultureInfo.InvariantCulture));
292325
#endif
293-
spawnStreamWriter.WriteLine("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));
294-
spawnStreamWriter.WriteLine("IsSinglePlayer=Yes");
295-
spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack);
296-
spawnStreamWriter.WriteLine("Side=" + mission.Side);
297-
spawnStreamWriter.WriteLine("BuildOffAlly=" + mission.BuildOffAlly);
326+
327+
spawnIniSettings.AddKey("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));
328+
329+
spawnIniSettings.AddKey("IsSinglePlayer", "Yes");
330+
spawnIniSettings.AddKey("SidebarHack", ClientConfiguration.Instance.SidebarHack.ToString(CultureInfo.InvariantCulture));
331+
spawnIniSettings.AddKey("Side", mission.Side.ToString(CultureInfo.InvariantCulture));
332+
spawnIniSettings.AddKey("BuildOffAlly", mission.BuildOffAlly.ToString(CultureInfo.InvariantCulture));
298333

299334
UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;
300335

301-
spawnStreamWriter.WriteLine("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString()));
302-
spawnStreamWriter.WriteLine("DifficultyModeComputer=" + GetComputerDifficulty());
336+
spawnIniSettings.AddKey("DifficultyModeHuman", mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString(CultureInfo.InvariantCulture));
337+
spawnIniSettings.AddKey("DifficultyModeComputer", GetComputerDifficulty().ToString(CultureInfo.InvariantCulture));
338+
339+
if (mission.IsCustomMission)
340+
{
341+
spawnIniSettings.AddKey("CustomMissionID", mission.MissionID.ToString(CultureInfo.InvariantCulture));
342+
}
343+
344+
spawnIni.AddSection(spawnIniSettings);
345+
346+
if (mission.IsCustomMission && mission.CustomMission_MissionMdIniSection is not null)
347+
{
348+
// copy an IniSection
349+
IniSection spawnIniMissionIniSection = new(scenario);
350+
foreach (var kvp in mission.CustomMission_MissionMdIniSection.Keys)
351+
{
352+
spawnIniMissionIniSection.AddKey(kvp.Key, kvp.Value);
353+
}
303354

304-
spawnStreamWriter.WriteLine();
305-
spawnStreamWriter.WriteLine();
306-
spawnStreamWriter.WriteLine();
355+
// append the new IniSection
356+
spawnIni.AddSection(spawnIniMissionIniSection);
307357
}
308358

359+
spawnIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini"));
360+
309361
var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value]));
310362
string difficultyName = DifficultyNames[trbDifficultySelector.Value];
311363

@@ -319,7 +371,7 @@ private void LaunchMission(Mission mission)
319371
UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value;
320372
UserINISettings.Instance.SaveSettings();
321373

322-
((MainMenuDarkeningPanel)Parent).Hide();
374+
Disable();
323375

324376
discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true);
325377
GameProcessLogic.GameProcessExited += GameProcessExited_Callback;
@@ -338,6 +390,9 @@ private void GameProcessExited_Callback()
338390
protected virtual void GameProcessExited()
339391
{
340392
GameProcessLogic.GameProcessExited -= GameProcessExited_Callback;
393+
394+
CustomMissionHelper.DeleteSupplementalMissionFiles();
395+
341396
// Logger.Log("GameProcessExited: Updating Discord Presence.");
342397
discordHandler.UpdatePresence();
343398
}
@@ -346,8 +401,36 @@ private void ReadMissionList()
346401
{
347402
ParseBattleIni("INI/Battle.ini");
348403

349-
if (Missions.Count == 0)
404+
if (AllMissions.Count == 0)
350405
ParseBattleIni("INI/" + ClientConfiguration.Instance.BattleFSFileName);
406+
407+
LoadCustomMissions();
408+
409+
LoadMissionsWithFilter(null);
410+
}
411+
412+
private void LoadCustomMissions()
413+
{
414+
string customMissionsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ClientConfiguration.Instance.CustomMissionPath);
415+
if (!Directory.Exists(customMissionsDirectory))
416+
return;
417+
418+
string[] mapFiles = Directory.GetFiles(customMissionsDirectory, "*.map");
419+
foreach (string mapFilePath in mapFiles)
420+
{
421+
var mapFile = new IniFile(mapFilePath);
422+
423+
IniSection missionSection = mapFile.GetSection("CNCNET:MISSION:BATTLE.INI");
424+
if (missionSection is null)
425+
continue;
426+
427+
IniSection? missionMdIniSection = mapFile.GetSection("CNCNET:MISSION:MISSION.INI");
428+
429+
string filename = new FileInfo(mapFilePath).Name;
430+
string scenario = SafePath.CombineFilePath(ClientConfiguration.Instance.CustomMissionPath, filename);
431+
Mission mission = Mission.NewCustomMission(missionSection, missionCodeName: filename.ToUpperInvariant(), scenario, missionMdIniSection);
432+
AddMission(mission);
433+
}
351434
}
352435

353436
/// <summary>
@@ -366,7 +449,7 @@ private bool ParseBattleIni(string path)
366449
return false;
367450
}
368451

369-
if (Missions.Count > 0)
452+
if (lbCampaignListMissions.Count > 0)
370453
{
371454
throw new InvalidOperationException("Loading multiple Battle*.ini files is not supported anymore.");
372455
}
@@ -386,10 +469,40 @@ private bool ParseBattleIni(string path)
386469
if (!battleIni.SectionExists(battleSection))
387470
continue;
388471

389-
var mission = new Mission(battleIni, battleSection, i);
472+
var mission = new Mission(battleIni.GetSection(battleSection), missionCodeName: battleEntry);
473+
AddMission(mission);
474+
}
475+
476+
Logger.Log("Finished parsing " + path + ".");
477+
return true;
478+
}
479+
480+
/// <summary>
481+
/// Load or re-load missons with selected tags.
482+
/// </summary>
483+
/// <param name="selectedTags">Missions with at lease one of which tags to be shown. As an exception, null means show all missions.</param>
484+
public void LoadMissionsWithFilter(ISet<string> selectedTags = null)
485+
{
486+
lbCampaignListMissions.Clear();
390487

391-
Missions.Add(mission);
488+
lbCampaignList.IsChangingSize = true;
392489

490+
lbCampaignList.Clear();
491+
lbCampaignList.SelectedIndex = -1;
492+
493+
// The following two lines are handled by LbCampaignList_SelectedIndexChanged
494+
// tbMissionDescription.Text = string.Empty;
495+
// btnLaunch.AllowClick = false;
496+
497+
// Select missions with the filter
498+
IEnumerable<Mission> missions = AllMissions;
499+
if (selectedTags != null)
500+
missions = missions.Where(mission => mission.Tags.Intersect(selectedTags).Any()).ToList();
501+
lbCampaignListMissions = missions.ToList();
502+
503+
// Update lbCampaignList with selected missions
504+
foreach (Mission mission in lbCampaignListMissions)
505+
{
393506
var item = new XNAListBoxItem();
394507
item.Text = mission.GUIName;
395508
if (!mission.Enabled)
@@ -414,10 +527,10 @@ private bool ParseBattleIni(string path)
414527
lbCampaignList.AddItem(item);
415528
}
416529

417-
Logger.Log("Finished parsing " + path + ".");
418-
return true;
419-
}
530+
lbCampaignList.IsChangingSize = false;
420531

532+
lbCampaignList.TopIndex = 0;
533+
}
421534
public override void Draw(GameTime gameTime)
422535
{
423536
base.Draw(gameTime);

0 commit comments

Comments
 (0)