Skip to content

Commit

Permalink
Implement custom mission support with CampaignTagSelector
Browse files Browse the repository at this point in the history
  • Loading branch information
SadPencil committed Mar 10, 2024
1 parent 928f04c commit 787614a
Show file tree
Hide file tree
Showing 12 changed files with 466 additions and 114 deletions.
7 changes: 7 additions & 0 deletions ClientCore/ClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,8 @@ private List<TranslationGameFile> ParseTranslationGameFiles()

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

public bool CampaignTagSelectorEnabled => clientDefinitionsIni.GetBooleanValue(SETTINGS, "CampaignTagSelectorEnabled", false);

public string GetGameExecutableName()
{
string[] exeNames = clientDefinitionsIni.GetStringValue(SETTINGS, "GameExecutableNames", "Game.exe").Split(',');
Expand Down Expand Up @@ -383,6 +385,11 @@ public IEnumerable<string> SupplementalMapFileExtensions

#endregion

public string CustomMissionPath => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionPath", "Maps/CustomMissions");
public string CustomMissionCsfName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionCsfName", "stringtable99.csf");
public string CustomMissionPalName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionPalName", "custommission.pal");
public string CustomMissionShpName => clientDefinitionsIni.GetStringValue(SETTINGS, "CustomMissionShpName", "custommission.shp");

public OSVersion GetOperatingSystemVersion()
{
#if NETFRAMEWORK
Expand Down
44 changes: 29 additions & 15 deletions ClientGUI/INItializableWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,43 @@ public INItializableWindow(WindowManager windowManager) : base(windowManager)
/// instead of the window's name.
/// </summary>
protected string IniNameOverride { get; set; }
private bool VisitChild(IEnumerable<XNAControl> list, Func<XNAControl, bool> judge)
{
foreach (XNAControl child in list)
{
bool stop = judge(child);
if (stop) return true;
stop = VisitChild(child.Children, judge);
if (stop) return true;
}
return false;
}

public T FindChild<T>(string childName, bool optional = false) where T : XNAControl
{
T child = FindChild<T>(Children, childName);
if (child == null && !optional)
XNAControl result = null;
VisitChild(new List<XNAControl>() { this }, control =>
{
if (control.Name != childName) return false;
result = control;
return true;
});
if (result == null && !optional)
throw new KeyNotFoundException("Could not find required child control: " + childName);

return child;
return (T)result;
}

private T FindChild<T>(IEnumerable<XNAControl> list, string controlName) where T : XNAControl
public List<T> FindChildrenStartWith<T>(string prefix) where T : XNAControl
{
foreach (XNAControl child in list)
List<T> result = new List<T>();
VisitChild(new List<XNAControl>() { this }, (control) =>
{
if (child.Name == controlName)
return (T)child;

T childOfChild = FindChild<T>(child.Children, controlName);
if (childOfChild != null)
return childOfChild;
}

return null;
if (string.IsNullOrEmpty(prefix) ||
!string.IsNullOrEmpty(control.Name) && control.Name.StartsWith(prefix))
result.Add((T)control);
return false;
});
return result;
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions DXMainClient/DXGUI/GameClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager)
.AddSingletonXnaControl<CnCNetGameLoadingLobby>()
.AddSingletonXnaControl<CnCNetLobby>()
.AddSingletonXnaControl<GameInProgressWindow>()
.AddSingletonXnaControl<CampaignTagSelector>()
.AddSingletonXnaControl<GameLoadingWindow>()
.AddSingletonXnaControl<SkirmishLobby>()
.AddSingletonXnaControl<MainMenu>()
.AddSingletonXnaControl<MapPreviewBox>()
Expand Down
193 changes: 153 additions & 40 deletions DXMainClient/DXGUI/Generic/CampaignSelector.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using ClientCore;
using Microsoft.Xna.Framework;
using System;
using System;
using System.Collections.Generic;
using DTAClient.Domain;
using System.IO;
using System.Linq;
using ClientCore;
using ClientGUI;
using Rampastring.XNAUI.XNAControls;
using Rampastring.XNAUI;
using Rampastring.Tools;
using ClientUpdater;
using ClientCore.Extensions;
using DTAClient.Domain;
using Microsoft.Xna.Framework;
using Rampastring.Tools;
using Rampastring.XNAUI;
using Rampastring.XNAUI.XNAControls;
using System.Diagnostics;
using System.Globalization;

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

private DiscordHandler discordHandler;

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

private Mission missionToLaunch;

private List<Mission> _allMissions = [];
public IReadOnlyCollection<Mission> AllMissions { get => _allMissions; }

private Dictionary<int, Mission> _uniqueIDToMissions = new();
public IReadOnlyDictionary<int, Mission> UniqueIDToMissions => _uniqueIDToMissions;

private void AddMission(Mission mission)
{
// no matter whether the key is duplicated, the mission is always added to AllMissions
_allMissions.Add(mission);

// but only the first mission is recorded in UniqueIDToMissions
if (_uniqueIDToMissions.ContainsKey(mission.MissionID))
{
Logger.Log($"CampaignSelector: duplicated mission. CodeName: {mission.CodeName}. ID: {mission.MissionID}. Description: {mission.UntranslatedGUIName}.");
if (!string.IsNullOrEmpty(mission.Scenario))
mission.Enabled = false;
}
else
{
_uniqueIDToMissions.Add(mission.MissionID, mission);
}
}

public override void Initialize()
{
BackgroundTexture = AssetLoader.LoadTexture("missionselectorbg.png");
Expand Down Expand Up @@ -199,7 +226,7 @@ private void LbCampaignList_SelectedIndexChanged(object sender, EventArgs e)
return;
}

Mission mission = Missions[lbCampaignList.SelectedIndex];
Mission mission = lbCampaignListMissions[lbCampaignList.SelectedIndex];

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

private void BtnCancel_LeftClick(object sender, EventArgs e)
{
Enabled = false;
Disable();
}

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

Mission mission = Missions[selectedMissionId];
Mission mission = lbCampaignListMissions[selectedMissionId];

if (!ClientConfiguration.Instance.ModMode &&
(!Updater.IsFileNonexistantOrOriginal(mission.Scenario) || AreFilesModified()))
Expand Down Expand Up @@ -267,45 +294,70 @@ private void CheaterWindow_YesClicked(object sender, EventArgs e)
/// </summary>
private void LaunchMission(Mission mission)
{
CustomMissionHelper.DeleteSupplementalMissionFiles();
CustomMissionHelper.CopySupplementalMissionFiles(mission);

string scenario = mission.Scenario;

bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI;

Logger.Log("About to write spawn.ini.");
using (var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini")))
IniFile spawnIni = new()
{
spawnStreamWriter.WriteLine("; Generated by DTA Client");
spawnStreamWriter.WriteLine("[Settings]");
if (copyMapsToSpawnmapINI)
spawnStreamWriter.WriteLine("Scenario=spawnmap.ini");
else
spawnStreamWriter.WriteLine("Scenario=" + mission.Scenario);
Comment = "Generated by CnCNet Client"
};
IniSection spawnIniSettings = new("Settings");

if (copyMapsToSpawnmapINI)
spawnIniSettings.AddKey("Scenario", "spawnmap.ini");
else
spawnIniSettings.AddKey("Scenario", scenario);

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

spawnStreamWriter.WriteLine("CampaignID=" + mission.Index);
spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed);
spawnIniSettings.AddKey("GameSpeed", UserINISettings.Instance.GameSpeed.ToString());
#if YR || ARES
spawnStreamWriter.WriteLine("Ra2Mode=" + !mission.RequiredAddon);
spawnIniSettings.AddKey("Ra2Mode", (!mission.RequiredAddon).ToString(CultureInfo.InvariantCulture));
#else
spawnStreamWriter.WriteLine("Firestorm=" + mission.RequiredAddon);
spawnIniSettings.AddKey("Firestorm", mission.RequiredAddon.ToString(CultureInfo.InvariantCulture));
#endif
spawnStreamWriter.WriteLine("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));
spawnStreamWriter.WriteLine("IsSinglePlayer=Yes");
spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack);
spawnStreamWriter.WriteLine("Side=" + mission.Side);
spawnStreamWriter.WriteLine("BuildOffAlly=" + mission.BuildOffAlly);

spawnIniSettings.AddKey("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(mission.Side.ToString()));

spawnIniSettings.AddKey("IsSinglePlayer", "Yes");
spawnIniSettings.AddKey("SidebarHack", ClientConfiguration.Instance.SidebarHack.ToString(CultureInfo.InvariantCulture));
spawnIniSettings.AddKey("Side", mission.Side.ToString(CultureInfo.InvariantCulture));
spawnIniSettings.AddKey("BuildOffAlly", mission.BuildOffAlly.ToString(CultureInfo.InvariantCulture));

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

spawnStreamWriter.WriteLine("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString()));
spawnStreamWriter.WriteLine("DifficultyModeComputer=" + GetComputerDifficulty());
spawnIniSettings.AddKey("DifficultyModeHuman", mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString(CultureInfo.InvariantCulture));
spawnIniSettings.AddKey("DifficultyModeComputer", GetComputerDifficulty().ToString(CultureInfo.InvariantCulture));

if (mission.IsCustomMission)
{
spawnIniSettings.AddKey("CustomMissionID", mission.MissionID.ToString(CultureInfo.InvariantCulture));
}

spawnIni.AddSection(spawnIniSettings);

if (mission.IsCustomMission && mission.CustomMission_MissionMdIniSection is not null)
{
// copy an IniSection
IniSection spawnIniMissionIniSection = new(scenario);
foreach (var kvp in mission.CustomMission_MissionMdIniSection.Keys)
{
spawnIniMissionIniSection.AddKey(kvp.Key, kvp.Value);
}

spawnStreamWriter.WriteLine();
spawnStreamWriter.WriteLine();
spawnStreamWriter.WriteLine();
// append the new IniSection
spawnIni.AddSection(spawnIniMissionIniSection);
}

spawnIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini"));

var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value]));
string difficultyName = DifficultyNames[trbDifficultySelector.Value];

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

((MainMenuDarkeningPanel)Parent).Hide();
Disable();

discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true);
GameProcessLogic.GameProcessExited += GameProcessExited_Callback;
Expand All @@ -338,6 +390,9 @@ private void GameProcessExited_Callback()
protected virtual void GameProcessExited()
{
GameProcessLogic.GameProcessExited -= GameProcessExited_Callback;

CustomMissionHelper.DeleteSupplementalMissionFiles();

// Logger.Log("GameProcessExited: Updating Discord Presence.");
discordHandler.UpdatePresence();
}
Expand All @@ -346,8 +401,36 @@ private void ReadMissionList()
{
ParseBattleIni("INI/Battle.ini");

if (Missions.Count == 0)
if (AllMissions.Count == 0)
ParseBattleIni("INI/" + ClientConfiguration.Instance.BattleFSFileName);

LoadCustomMissions();

LoadMissionsWithFilter(null);
}

private void LoadCustomMissions()
{
string customMissionsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ClientConfiguration.Instance.CustomMissionPath);
if (!Directory.Exists(customMissionsDirectory))
return;

string[] mapFiles = Directory.GetFiles(customMissionsDirectory, "*.map");
foreach (string mapFilePath in mapFiles)
{
var mapFile = new IniFile(mapFilePath);

IniSection missionSection = mapFile.GetSection("CNCNET:MISSION:BATTLE.INI");
if (missionSection is null)
continue;

IniSection? missionMdIniSection = mapFile.GetSection("CNCNET:MISSION:MISSION.INI");

Check warning on line 427 in DXMainClient/DXGUI/Generic/CampaignSelector.cs

View workflow job for this annotation

GitHub Actions / build-clients (Ares)

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

string filename = new FileInfo(mapFilePath).Name;
string scenario = SafePath.CombineFilePath(ClientConfiguration.Instance.CustomMissionPath, filename);
Mission mission = Mission.NewCustomMission(missionSection, missionCodeName: filename.ToUpperInvariant(), scenario, missionMdIniSection);
AddMission(mission);
}
}

/// <summary>
Expand All @@ -366,7 +449,7 @@ private bool ParseBattleIni(string path)
return false;
}

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

var mission = new Mission(battleIni, battleSection, i);
var mission = new Mission(battleIni.GetSection(battleSection), missionCodeName: battleEntry);
AddMission(mission);
}

Logger.Log("Finished parsing " + path + ".");
return true;
}

/// <summary>
/// Load or re-load missons with selected tags.
/// </summary>
/// <param name="selectedTags">Missions with at lease one of which tags to be shown. As an exception, null means show all missions.</param>
public void LoadMissionsWithFilter(ISet<string> selectedTags = null)
{
lbCampaignListMissions.Clear();

Missions.Add(mission);
lbCampaignList.IsChangingSize = true;

lbCampaignList.Clear();
lbCampaignList.SelectedIndex = -1;

// The following two lines are handled by LbCampaignList_SelectedIndexChanged
// tbMissionDescription.Text = string.Empty;
// btnLaunch.AllowClick = false;

// Select missions with the filter
IEnumerable<Mission> missions = AllMissions;
if (selectedTags != null)
missions = missions.Where(mission => mission.Tags.Intersect(selectedTags).Any()).ToList();
lbCampaignListMissions = missions.ToList();

// Update lbCampaignList with selected missions
foreach (Mission mission in lbCampaignListMissions)
{
var item = new XNAListBoxItem();
item.Text = mission.GUIName;
if (!mission.Enabled)
Expand All @@ -414,10 +527,10 @@ private bool ParseBattleIni(string path)
lbCampaignList.AddItem(item);
}

Logger.Log("Finished parsing " + path + ".");
return true;
}
lbCampaignList.IsChangingSize = false;

lbCampaignList.TopIndex = 0;
}
public override void Draw(GameTime gameTime)
{
base.Draw(gameTime);
Expand Down
Loading

0 comments on commit 787614a

Please sign in to comment.