2023-06-09 11:44:39 +02:00

533 lines
26 KiB
C#

//
// Copyright 2020 Electronic Arts Inc.
//
// The Command & Conquer Map Editor and corresponding source code is free
// software: you can redistribute it and/or modify it under the terms of
// the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
// The Command & Conquer Map Editor and corresponding source code is distributed
// in the hope that it will be useful, but with permitted additional restrictions
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
// distributed with this program. You should have received a copy of the
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
//#define CLASSICIMPLEMENTED
using MobiusEditor.Dialogs;
using MobiusEditor.Interface;
using MobiusEditor.Utility;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
namespace MobiusEditor
{
static class Program
{
const string gameId = "1213210";
static readonly String ApplicationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
TryEnableDPIAware();
// Change current culture to en-US
if (Thread.CurrentThread.CurrentCulture.Name != "en-US")
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
}
Version version = Assembly.GetExecutingAssembly().GetName().Version;
if (!Version.TryParse(Properties.Settings.Default.ApplicationVersion, out Version oldVersion) || oldVersion < version)
{
Properties.Settings.Default.Upgrade();
if (String.IsNullOrEmpty(Properties.Settings.Default.ApplicationVersion))
{
CopyLastUserConfig(Properties.Settings.Default.ApplicationVersion, v => Properties.Settings.Default.ApplicationVersion = v);
}
Properties.Settings.Default.ApplicationVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
Properties.Settings.Default.Save();
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Dictionary<GameType, string[]> modPaths = new Dictionary<GameType, string[]>();
// Check if any mods are allowed to override the default stuff to load.
const string tdModFolder = "Tiberian_Dawn";
const string raModFolder = "Red_Alert";
modPaths.Add(GameType.TiberianDawn, GetModPaths(gameId, Properties.Settings.Default.ModsToLoadTD, tdModFolder, "TD"));
modPaths.Add(GameType.RedAlert, GetModPaths(gameId, Properties.Settings.Default.ModsToLoadRA, raModFolder, "RA"));
modPaths.Add(GameType.SoleSurvivor, GetModPaths(gameId, Properties.Settings.Default.ModsToLoadSS, tdModFolder, "TD"));
String runPath = Globals.UseClassicGraphics ? null : GetRemasterRunPath();
if (runPath != null)
{
LoadEditorRemastered(runPath);
}
else
{
#if CLASSICIMPLEMENTED
LoadEditorClassic();
#else
return;
#endif
}
if (SteamworksUGC.IsSteamBuild && runPath != null)
{
// Ignore result from this.
SteamworksUGC.Init();
}
if (Properties.Settings.Default.ShowInviteWarning)
{
using (var inviteMessageBox = new InviteMessageBox())
{
// Ensures the dialog does not get lost by showing its icon in the taskbar.
inviteMessageBox.ShowInTaskbar = true;
inviteMessageBox.ShowDialog();
Properties.Settings.Default.ShowInviteWarning = !inviteMessageBox.DontShowAgain;
Properties.Settings.Default.Save();
}
}
String arg = null;
try
{
if (args.Length > 0 && File.Exists(args[0]))
arg = args[0];
}
catch
{
arg = null;
}
using (MainForm mainForm = new MainForm(arg))
{
mainForm.ModPaths = modPaths;
Application.Run(mainForm);
}
if (SteamworksUGC.IsSteamBuild)
{
SteamworksUGC.Shutdown();
}
Globals.TheArchiveManager.Dispose();
}
private static void LoadEditorClassic()
{
#if CLASSICIMPLEMENTED
// The system should scan all mix archives for known filenames of other mix archives so it can do recursive searches.
// Mix files should be given in order or depth, so first give ones that are in the folder, then ones that may occur inside others.
// The order of load determines the file priority; only the first found occurrence of a file is used.
Dictionary<GameType, String> gameFolders = new Dictionary<GameType, string>();
gameFolders.Add(GameType.TiberianDawn, "Classic\\TD\\");
gameFolders.Add(GameType.RedAlert, "Classic\\RA\\");
gameFolders.Add(GameType.SoleSurvivor, "Classic\\TD\\");
//Globals.TheArchiveManager = new MixfileManager(ApplicationPath, gameFolders);
var mixfilesLoaded = true;
// This will map the mix files to the respective games, and look for them in the respective folders.
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.TiberianDawn, "cclocal.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.TiberianDawn, "conquer.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.TiberianDawn, "desert.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.TiberianDawn, "temperat.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.TiberianDawn, "winter.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.SoleSurvivor, "cclocal.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.SoleSurvivor, "conquer.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.SoleSurvivor, "desert.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.SoleSurvivor, "temperat.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.SoleSurvivor, "winter.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "expand2.mix");
// All graphics from expand are also in expand2
//mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "expand.mix");
Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "redalert.mix");
Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "main.mix");
// Only needed for conquer.eng, and expand* override those anyway.
//mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "local.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "conquer.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "lores.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "lores1.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "temperat.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "snow.mix");
mixfilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.RedAlert, "interior.mix");
#if !DEVELOPER
if (!mixfilesLoaded)
{
MessageBox.Show("Required data is missing or corrupt, please validate your installation.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
#endif
#endif
}
private static void LoadEditorRemastered(String runPath)
{
// Initialize megafiles
Globals.TheArchiveManager = new MegafileManager(Path.Combine(runPath, Globals.MegafilePath), runPath);
var megafilesLoaded = true;
megafilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.None, "CONFIG.MEG");
megafilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.None, "TEXTURES_COMMON_SRGB.MEG");
megafilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.None, "TEXTURES_RA_SRGB.MEG");
megafilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.None, "TEXTURES_SRGB.MEG");
megafilesLoaded &= Globals.TheArchiveManager.LoadArchive(GameType.None, "TEXTURES_TD_SRGB.MEG");
#if !DEVELOPER
if (!megafilesLoaded)
{
MessageBox.Show("Required data is missing or corrupt, please validate your installation.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
#endif
// Initialize texture, tileset, team color, and game text managers
Globals.TheTextureManager = new TextureManager(Globals.TheArchiveManager);
Globals.TheTilesetManager = new TilesetManager(Globals.TheArchiveManager, Globals.TheTextureManager, Globals.TilesetsXMLPath, Globals.TexturesPath);
Globals.TheTeamColorManager = new TeamColorManager(Globals.TheArchiveManager);
// Not adapted to mods for now...
var cultureName = CultureInfo.CurrentUICulture.Name;
var gameTextFilename = string.Format(Globals.GameTextFilenameFormat, cultureName.ToUpper());
if (!Globals.TheArchiveManager.FileExists(gameTextFilename))
{
gameTextFilename = string.Format(Globals.GameTextFilenameFormat, "EN-US");
}
GameTextManager gtm = new GameTextManager(Globals.TheArchiveManager, gameTextFilename);
//gtm.Dump("alltext.txt");
Globals.TheGameTextManager = gtm;
AddMissingRemasterText(gtm);
}
private static String GetRemasterRunPath()
{
// Do a test for CONFIG.MEG
string runPath = Environment.CurrentDirectory;
if (FileTest(runPath))
{
return runPath;
}
// If it does not exist, try to use the directory from the settings.
bool validSavedDirectory = false;
if (!String.IsNullOrWhiteSpace(Properties.Settings.Default.GameDirectoryPath) &&
Directory.Exists(Properties.Settings.Default.GameDirectoryPath))
{
if (FileTest(Properties.Settings.Default.GameDirectoryPath))
{
runPath = Properties.Settings.Default.GameDirectoryPath;
validSavedDirectory = true;
}
}
// Before showing a dialog to ask, try to autodetect the Steam path.
if (!validSavedDirectory)
{
string gameFolder = SteamAssist.TryGetSteamGameFolder(gameId, "TiberianDawn.dll", "RedAlert.dll");
if (gameFolder != null)
{
if (FileTest(gameFolder))
{
runPath = gameFolder;
validSavedDirectory = true;
Properties.Settings.Default.GameDirectoryPath = gameFolder;
Properties.Settings.Default.Save();
}
}
}
// If the directory in the settings is wrong, and it can not be autodetected, we need to ask the user for the installation dir.
if (!validSavedDirectory)
{
var gameInstallationPathForm = new GameInstallationPathForm();
if (gameInstallationPathForm.ShowDialog() == DialogResult.No)
return null;
runPath = Path.GetDirectoryName(gameInstallationPathForm.SelectedPath);
Properties.Settings.Default.GameDirectoryPath = runPath;
Properties.Settings.Default.Save();
}
return runPath;
}
private static void AddMissingRemasterText(GameTextManager gtm)
{
// Buildings
gtm["TEXT_STRUCTURE_TITLE_OIL_PUMP"] = "Oil Pump";
gtm["TEXT_STRUCTURE_TITLE_OIL_TANKER"] = "Oil Tanker";
String fake = " (" + gtm["TEXT_UI_FAKE"] + ")";
if (!gtm["TEXT_STRUCTURE_RA_WEAF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_WEAF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_WEAF"] + fake;
if (!gtm["TEXT_STRUCTURE_RA_FACF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_FACF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_FACF"] + fake;
if (!gtm["TEXT_STRUCTURE_RA_SYRF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_SYRF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_SYRF"] + fake;
if (!gtm["TEXT_STRUCTURE_RA_SPEF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_SPEF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_SPEF"] + fake;
if (!gtm["TEXT_STRUCTURE_RA_DOMF"].EndsWith(fake)) gtm["TEXT_STRUCTURE_RA_DOMF"] = Globals.TheGameTextManager["TEXT_STRUCTURE_RA_DOMF"] + fake;
// Overlay
gtm["TEXT_OVERLAY_CONCRETE_PAVEMENT"] = "Concrete";
gtm["TEXT_OVERLAY_CONCRETE_ROAD"] = "Concrete Road";
gtm["TEXT_OVERLAY_CONCRETE_ROAD_FULL"] = "Concrete Road (full)";
gtm["TEXT_OVERLAY_TIBERIUM"] = "Tiberium";
// "Gold" exists as "TEXT_CURRENCY_TACTICAL"
gtm["TEXT_OVERLAY_GEMS"] = "Gems";
gtm["TEXT_OVERLAY_WCRATE"] = "Wooden Crate";
gtm["TEXT_OVERLAY_SCRATE"] = "Steel Crate";
gtm["TEXT_OVERLAY_WATER_CRATE"] = "Water Crate";
// Smudge
gtm["TEXT_SMUDGE_CRATER"] = "Crater";
gtm["TEXT_SMUDGE_SCORCH"] = "Scorch Mark";
gtm["TEXT_SMUDGE_BIB"] = "Road Bib";
}
private static void AddMissingClassicText(GameTextManager gtm)
{
// Overlay
gtm["TEXT_OVERLAY_CONCRETE_ROAD"] = "Concrete Road";
gtm["TEXT_OVERLAY_CONCRETE_ROAD_FULL"] = "Concrete Road (full)";
gtm["TEXT_UI_FAKE"] = "FAKE";
}
[DllImport("SHCore.dll")]
private static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness);
private enum PROCESS_DPI_AWARENESS
{
Process_DPI_Unaware = 0,
Process_System_DPI_Aware = 1,
Process_Per_Monitor_DPI_Aware = 2
}
[DllImport("user32.dll", SetLastError = true)]
static extern bool SetProcessDPIAware();
internal static void TryEnableDPIAware()
{
try
{
SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware);
}
catch
{
try
{ // fallback, use (simpler) internal function
SetProcessDPIAware();
}
catch { }
}
}
private static string[] GetModPaths(string gameId, string modstoLoad, string modFolder, string modIdentifier)
{
Regex numbersOnly = new Regex("^\\d+$");
Regex modregex = new Regex("\"game_type\"\\s*:\\s*\""+ modIdentifier + "\"");
const string contentFile = "ccmod.json";
string modsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "CnCRemastered", "Mods");
string[] steamLibraryFolders = SteamAssist.GetLibraryFoldersForAppId(gameId);
string[] mods = (modstoLoad ?? String.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
List<string> modPaths = new List<string>();
for (int i = 0; i < mods.Length; ++i)
{
string modDef = mods[i].Trim();
if (String.IsNullOrEmpty(modDef))
{
continue;
}
string addonModPath;
// Lookup by Steam ID
if (numbersOnly.IsMatch(modDef))
{
addonModPath = SteamAssist.TryGetSteamWorkshopFolder(gameId, modDef, contentFile, null);
if (addonModPath != null)
{
if (CheckAddonPathModType(addonModPath, contentFile, modregex))
{
modPaths.Add(addonModPath);
}
}
// don't bother checking more on a numbers-only entry.
continue;
}
// Lookup by folder name
addonModPath = Path.Combine(modsFolder, modFolder, modDef);
if (CheckAddonPathModType(addonModPath, contentFile, modregex))
{
modPaths.Add(addonModPath);
// Found in local mods; don't check Steam ones.
continue;
}
// try to find mod in steam library.
foreach (string libFolder in steamLibraryFolders)
{
string modPath = Path.Combine(libFolder, "steamapps", "workshop", "content", gameId);
if (!Directory.Exists(modPath))
{
continue;
}
foreach (string modPth in Directory.GetDirectories(modPath))
{
addonModPath = Path.Combine(modPth, modDef);
if (CheckAddonPathModType(addonModPath, contentFile, modregex))
{
modPaths.Add(addonModPath);
break;
}
}
}
}
return modPaths.Distinct(StringComparer.CurrentCultureIgnoreCase).ToArray();
}
private static bool CheckAddonPathModType(string addonModPath, string contentFile, Regex modregex)
{
try
{
string checkPath = Path.Combine(addonModPath, contentFile);
if (!File.Exists(checkPath))
{
return false;
}
string ccModDefContents = File.ReadAllText(checkPath);
return modregex.IsMatch(ccModDefContents);
}
catch
{
return false;
}
}
/// <summary>
/// Ports settings over from older versions, and ensures only one settings folder remains.
/// Taken from https://stackoverflow.com/a/14845448 and adapted to scan deeper.
/// </summary>
/// <param name="currentSettingsVer">Curent version fetched from the settings.</param>
/// <param name="versionSetter">Delegate to set the version into the settings after the process is complete.</param>
private static void CopyLastUserConfig(String currentSettingsVer, Action<String> versionSetter)
{
AssemblyName assn = Assembly.GetExecutingAssembly().GetName();
Version currentVersion = assn.Version;
if (currentVersion.ToString() == currentSettingsVer)
{
return;
}
string userConfigFileName = "user.config";
// Expected location of the current user config
DirectoryInfo currentVersionConfigFileDir = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory;
if (currentVersionConfigFileDir == null)
{
return;
}
DirectoryInfo currentParent = currentVersionConfigFileDir.Parent;
DirectoryInfo previousSettingsDir = null;
if (currentParent != null && currentParent.Exists)
{
try
{
// Location of the previous user config
// grab the most recent folder from the list of user's settings folders, prior to the current version
previousSettingsDir = (from dir in currentParent.GetDirectories()
let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
where dirVer.Ver < currentVersion
orderby dirVer.Ver descending
select dir).FirstOrDefault();
}
catch
{
// Ignore.
}
}
// Find any other folders for this application.
DirectoryInfo[] otherSettingsFolders = null;
string dirNameProgPart = currentParent?.Name;
const string dirnameCutoff = "_Url_";
int dirNameProgPartLen = dirNameProgPart == null ? -1 : dirNameProgPart.LastIndexOf(dirnameCutoff, StringComparison.OrdinalIgnoreCase);
if (dirNameProgPartLen == -1)
{
// Fallback: reproduce what's done to the exe string to get a folder. Just to be sure. From observation, actual exe cutoff length is 25.
dirNameProgPart = Path.GetFileName(assn.CodeBase).Replace(' ', '_');
dirNameProgPart = dirNameProgPart.Substring(0, Math.Min(20, dirNameProgPart.Length));
}
else
{
dirNameProgPart = dirNameProgPart.Substring(0, dirNameProgPartLen + dirnameCutoff.Length);
}
if (currentParent != null && currentParent.Parent != null && currentParent.Parent.Exists)
{
otherSettingsFolders = currentParent.Parent.GetDirectories(dirNameProgPart + "*").OrderBy(p => p.CreationTime).Reverse().ToArray();
}
if (otherSettingsFolders != null && otherSettingsFolders.Length > 0 && previousSettingsDir == null)
{
foreach (DirectoryInfo parDir in otherSettingsFolders)
{
if (parDir.Name == currentParent.Name)
continue;
try
{
// see if there's a same-version folder in other parent folder
previousSettingsDir = (from dir in parDir.GetDirectories()
let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
where dirVer.Ver == currentVersion
orderby dirVer.Ver descending
select dir).FirstOrDefault();
}
catch
{
// Ignore.
}
if (previousSettingsDir != null)
break;
try
{
// see if there's an older version folder in other parent folder
previousSettingsDir = (from dir in parDir.GetDirectories()
let dirVer = new { Dir = dir, Ver = new Version(dir.Name) }
where dirVer.Ver < currentVersion
orderby dirVer.Ver descending
select dir).FirstOrDefault();
}
catch
{
// Ignore.
}
if (previousSettingsDir != null)
break;
}
}
string previousVersionConfigFile = previousSettingsDir == null ? null : string.Concat(previousSettingsDir.FullName, @"\", userConfigFileName);
string currentVersionConfigFile = string.Concat(currentVersionConfigFileDir.FullName, @"\", userConfigFileName);
if (!currentVersionConfigFileDir.Exists)
{
Directory.CreateDirectory(currentVersionConfigFileDir.FullName);
}
if (previousVersionConfigFile != null)
{
File.Copy(previousVersionConfigFile, currentVersionConfigFile, true);
}
if (File.Exists(currentVersionConfigFile))
{
Properties.Settings.Default.Reload();
}
versionSetter(currentVersion.ToString());
Properties.Settings.Default.Save();
if (otherSettingsFolders != null && otherSettingsFolders.Length > 0)
{
foreach (DirectoryInfo parDir in otherSettingsFolders)
{
if (parDir.Name == currentParent.Name)
continue;
// Wipe them out. All of them.
try
{
parDir.Delete(true);
}
catch
{
// Ignore
}
}
}
}
static bool FileTest(String basePath)
{
return File.Exists(Path.Combine(basePath, "DATA", "CONFIG.MEG"));
}
}
}