1- using ClientCore ;
2- using Microsoft . Xna . Framework ;
3- using System ;
1+ using System ;
42using System . Collections . Generic ;
5- using DTAClient . Domain ;
63using System . IO ;
4+ using System . Linq ;
5+ using ClientCore ;
76using ClientGUI ;
8- using Rampastring . XNAUI . XNAControls ;
9- using Rampastring . XNAUI ;
10- using Rampastring . Tools ;
117using ClientUpdater ;
128using 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
1417namespace 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