ZoneBuilder/Source/Core/General/Launcher.cs

462 lines
14 KiB
C#

#region ================== Copyright (c) 2007 Pascal vd Heiden
/*
* Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
* This program is released under GNU General Public License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/
#endregion
#region ================== Namespaces
using System;
using System.IO;
using CodeImp.DoomBuilder.Data;
using System.Diagnostics;
using CodeImp.DoomBuilder.Actions;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Windows;
using CodeImp.DoomBuilder.IO;
using CodeImp.DoomBuilder.Editing;
using System.Security.Cryptography;
#endregion
namespace CodeImp.DoomBuilder
{
internal class Launcher : IDisposable
{
#region ================== Constants
#endregion
#region ================== Variables
private string tempwad;
private Process process; //mxd
private bool isdisposed;
delegate void EngineExitedCallback(); //mxd
#endregion
#region ================== Properties
public string TempWAD { get { return tempwad; } }
#endregion
#region ================== Constructor / Destructor
// Constructor
public Launcher(MapManager manager)
{
// Initialize
CleanTempFile(manager);
// Bind actions
General.Actions.BindMethods(this);
}
// Disposer
public void Dispose()
{
// Not yet disposed?
if(!isdisposed)
{
// Unbind actions
General.Actions.UnbindMethods(this);
//mxd. Terminate process?
if(process != null)
{
process.CloseMainWindow();
process.Close();
}
// Remove temporary file
try { File.Delete(tempwad); }
catch(Exception) { }
// Done
isdisposed = true;
}
}
#endregion
#region ================== Parameters
// This takes the unconverted parameters (with placeholders) and converts it
// to parameters with full paths, names and numbers where placeholders were put.
// The tempfile must be the full path and filename to the PWAD file to test.
public string ConvertParameters(string parameters, int skill, string skin, int gametype, bool shortpaths)
{
string outp = parameters;
DataLocation iwadloc;
string p_wp = "", p_wf = "";
string p_ap = "", p_apq = "";
string p_aa = "", p_aaq = "";
string p_af = "", p_afq = "";
string p_l1 = "", p_l2 = "";
string p_nm = "";
string f = tempwad;
// Make short path if needed
if(shortpaths) f = General.GetShortFilePath(f);
// Find the first IWAD file
if(General.Map.Data.FindFirstIWAD(out iwadloc))
{
// %WP and %WF result in IWAD file
p_wp = iwadloc.location;
p_wf = Path.GetFileName(p_wp);
if(shortpaths)
{
p_wp = General.GetShortFilePath(p_wp);
p_wf = General.GetShortFilePath(p_wf);
}
}
// Make a list of all data locations, including map location
DataLocation maplocation = new DataLocation(DataLocation.RESOURCE_WAD, General.Map.FilePathName, false, false, false);
DataLocationList locations = new DataLocationList();
locations.AddRange(General.Map.ConfigSettings.Resources);
locations.AddRange(General.Map.Options.Resources);
if(!string.IsNullOrEmpty(maplocation.location)) locations.Add(maplocation); //mxd. maplocation.location will be empty when a newly created map was not saved yet.
FileInfo fi = new FileInfo(f);
// Go for all data locations
foreach(DataLocation dl in locations)
{
// Location not the IWAD file?
if((dl.type != DataLocation.RESOURCE_WAD) || (dl.location != iwadloc.location))
{
// Location not included?
if(!dl.notfortesting)
{
// hack to prevent another hack from crashing: don't check directories
if (dl.type != DataLocation.RESOURCE_DIRECTORY && FilesAreEqual(fi, new FileInfo(dl.location)))
{
outp = outp.Replace("%f", "%F");
outp = outp.Replace("\"%F\"", "");
}
// Add to string of files
if (shortpaths)
{
p_ap += General.GetShortFilePath(dl.location) + " ";
p_apq += "\"" + General.GetShortFilePath(dl.location) + "\" ";
if (dl.type == DataLocation.RESOURCE_WAD || dl.type == DataLocation.RESOURCE_PK3)
{
p_aa += General.GetShortFilePath(dl.location) + " ";
p_aaq += "\"" + General.GetShortFilePath(dl.location) + "\" ";
}
else
{
p_af += General.GetShortFilePath(dl.location) + " ";
p_afq += "\"" + General.GetShortFilePath(dl.location) + "\" ";
}
}
else
{
p_ap += dl.location + " ";
p_apq += "\"" + dl.location + "\" ";
if (dl.type == DataLocation.RESOURCE_WAD || dl.type == DataLocation.RESOURCE_PK3)
{
p_aa += dl.location + " ";
p_aaq += "\"" + dl.location + "\" ";
}
else
{
p_af += dl.location + " ";
p_afq += "\"" + dl.location + "\" ";
}
}
}
}
}
// Trim last space from resource file locations
p_ap = p_ap.TrimEnd(' ');
p_apq = p_apq.TrimEnd(' ');
p_aa = p_aa.TrimEnd(' ');
p_aaq = p_aaq.TrimEnd(' ');
p_af = p_af.TrimEnd(' ');
p_afq = p_afq.TrimEnd(' ');
// Try finding the L1 and L2 numbers from the map name
string numstr = "";
bool first = true;
foreach(char c in General.Map.Options.CurrentName)
{
// Character is a number?
if(Configuration.NUMBERS.IndexOf(c) > -1)
{
// Include it
numstr += c;
}
else
{
// Store the number if we found one
if(numstr.Length > 0)
{
int num;
int.TryParse(numstr, out num);
if(first) p_l1 = num.ToString(); else p_l2 = num.ToString();
numstr = "";
first = false;
}
}
}
// Store the number if we found one
if(numstr.Length > 0)
{
int num;
int.TryParse(numstr, out num);
if(first) p_l1 = num.ToString(); else p_l2 = num.ToString();
}
// No monsters?
if(!General.Settings.TestMonsters) p_nm = "-nomonsters";
// Make sure all our placeholders are in uppercase
outp = outp.Replace("%f", "%F");
outp = outp.Replace("%wp", "%WP");
outp = outp.Replace("%wf", "%WF");
outp = outp.Replace("%wP", "%WP");
outp = outp.Replace("%wF", "%WF");
outp = outp.Replace("%Wp", "%WP");
outp = outp.Replace("%Wf", "%WF");
outp = outp.Replace("%l1", "%L1");
outp = outp.Replace("%l2", "%L2");
outp = outp.Replace("%l", "%L");
outp = outp.Replace("%ap", "%AP");
outp = outp.Replace("%aP", "%AP");
outp = outp.Replace("%Ap", "%AP");
outp = outp.Replace("%s", "%S");
outp = outp.Replace("%nM", "%NM");
outp = outp.Replace("%Nm", "%NM");
outp = outp.Replace("%nm", "%NM");
// Replace placeholders with actual values
outp = outp.Replace("%F", f);
outp = outp.Replace("%WP", p_wp);
outp = outp.Replace("%WF", p_wf);
outp = outp.Replace("%L1", p_l1);
outp = outp.Replace("%L2", p_l2);
outp = outp.Replace("%L", General.Map.Options.CurrentName);
outp = outp.Replace("\"%AP\"", p_apq);
outp = outp.Replace("\"%AA\"", p_aaq);
outp = outp.Replace("\"%AF\"", p_afq);
outp = outp.Replace("%AP", p_ap);
outp = outp.Replace("%AA", p_aa);
outp = outp.Replace("%AF", p_af);
outp = outp.Replace("%S", skill.ToString());
outp = outp.Replace("%NM", p_nm);
outp = outp + " +skin " + skin.ToLowerInvariant();
if (gametype != -1)
outp = outp + " -server -gametype " + gametype.ToString();
// Return result
return outp;
}
//mxd
private bool AlreadyTesting()
{
if(process != null)
{
General.ShowWarningMessage("Game engine is already running." + Environment.NewLine + "Please close \"" + process.MainModule.FileName + "\" first.", MessageBoxButtons.OK);
return true;
}
return false;
}
#endregion
#region ================== Test
// This saves the map to a temporary file and launches a test
[BeginAction("testmap")]
public void Test()
{
if(AlreadyTesting() || !General.Editing.Mode.OnMapTestBegin(false)) return; //mxd
TestAtSkill(General.Map.ConfigSettings.TestSkill);
General.Editing.Mode.OnMapTestEnd(false); //mxd
}
//mxd
[BeginAction("testmapfromview")]
public void TestFromView()
{
if(AlreadyTesting() || !General.Editing.Mode.OnMapTestBegin(true)) return;
TestAtSkill(General.Map.ConfigSettings.TestSkill);
General.Editing.Mode.OnMapTestEnd(true);
}
// This saves the map to a temporary file and launches a test with the given skill
public void TestAtSkill(int skill)
{
Cursor oldcursor = Cursor.Current;
// Check if configuration is OK
if (string.IsNullOrEmpty(General.Map.ConfigSettings.TestProgram) || !File.Exists(General.Map.ConfigSettings.TestProgram))
{
//mxd. Let's be more precise
string message;
if(String.IsNullOrEmpty(General.Map.ConfigSettings.TestProgram))
message = "Your test program is not set for the current game configuration";
else
message = "Current test program has invalid path";
// Show message
Cursor.Current = Cursors.Default;
DialogResult result = General.ShowWarningMessage(message + ". Would you like to set up your test program now?", MessageBoxButtons.YesNo);
if(result == DialogResult.Yes)
{
// Show game configuration on the right page
General.MainWindow.ShowConfigurationPage(2);
}
return;
}
// No custom parameters?
if(!General.Map.ConfigSettings.CustomParameters)
{
// Set parameters to the default ones
General.Map.ConfigSettings.TestParameters = General.Map.Config.TestParameters;
General.Map.ConfigSettings.TestShortPaths = General.Map.Config.TestShortPaths;
}
// Remove temporary file
try { File.Delete(tempwad); }
catch(Exception) { }
// Save map to temporary file
Cursor.Current = Cursors.WaitCursor;
tempwad = General.MakeTempFilename(General.Map.TempPath, "wad");
General.Plugins.OnMapSaveBegin(SavePurpose.Testing);
if(General.Map.SaveMap(tempwad, SavePurpose.Testing))
{
// No compiler errors?
if(General.Map.Errors.Count == 0)
{
// Make arguments
string args = ConvertParameters(General.Map.ConfigSettings.TestParameters, skill, General.Map.ConfigSettings.TestSkin, General.Map.ConfigSettings.TestGametype, General.Map.ConfigSettings.TestShortPaths);
// Setup process info
ProcessStartInfo processinfo = new ProcessStartInfo();
processinfo.Arguments = args;
processinfo.FileName = General.Map.ConfigSettings.TestProgram;
processinfo.CreateNoWindow = false;
processinfo.ErrorDialog = false;
processinfo.UseShellExecute = true;
processinfo.WindowStyle = ProcessWindowStyle.Normal;
processinfo.WorkingDirectory = Path.GetDirectoryName(processinfo.FileName);
// Output info
General.WriteLogLine("Running test program: " + processinfo.FileName);
General.WriteLogLine("Program parameters: " + processinfo.Arguments);
General.MainWindow.DisplayStatus(StatusType.Info, "Launching " + processinfo.FileName + "...");
try
{
// Start the program
process = Process.Start(processinfo);
process.EnableRaisingEvents = true; //mxd
process.Exited += ProcessOnExited; //mxd
Cursor.Current = oldcursor; //mxd
}
catch(Exception e)
{
// Unable to start the program
General.ShowErrorMessage("Unable to start the test program, " + e.GetType().Name + ": " + e.Message, MessageBoxButtons.OK);
}
}
else
{
General.MainWindow.DisplayStatus(StatusType.Warning, "Unable to test the map due to script errors.");
}
}
}
//Check if two files are equal using MD5 hash. I can't believe I need this...
static bool FilesAreEqual(FileInfo first, FileInfo second)
{
byte[] firstHash = MD5.Create().ComputeHash(first.OpenRead());
byte[] secondHash = MD5.Create().ComputeHash(second.OpenRead());
for (int i = 0; i < firstHash.Length; i++)
{
if (firstHash[i] != secondHash[i])
return false;
}
return true;
}
//mxd
private void TestingFinished()
{
//Done
TimeSpan deltatime = TimeSpan.FromTicks(process.ExitTime.Ticks - process.StartTime.Ticks);
process = null;
General.WriteLogLine("Test program has finished.");
General.WriteLogLine("Run time: " + deltatime.TotalSeconds.ToString("###########0.00") + " seconds");
General.MainWindow.DisplayReady();
// Clean up temp file
CleanTempFile(General.Map);
if(General.Map != null)
{
// Device reset may be needed...
if(General.Editing.Mode is ClassicMode)
{
General.Map.Graphics.Reset();
General.MainWindow.RedrawDisplay();
}
/*else if(General.Editing.Mode is VisualMode)
{
General.MainWindow.StopExclusiveMouseInput();
General.MainWindow.StartExclusiveMouseInput();
}*/
}
General.Plugins.OnMapSaveEnd(SavePurpose.Testing);
General.MainWindow.FocusDisplay();
if(General.Editing.Mode is ClassicMode) General.MainWindow.RedrawDisplay();
}
//mxd
private void ProcessOnExited(object sender, EventArgs eventArgs)
{
General.MainWindow.Invoke(new EngineExitedCallback(TestingFinished));
}
// This deletes the previous temp file and creates a new, empty temp file
private void CleanTempFile(MapManager manager)
{
// Remove temporary file
try { File.Delete(tempwad); }
catch(Exception) { }
// Make new empty temp file
tempwad = General.MakeTempFilename(manager.TempPath, "wad");
File.WriteAllText(tempwad, "");
}
#endregion
}
}