mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-25 21:31:14 +00:00
e2374102ee
Script run by UDBScript are now executed asynchronously
375 lines
12 KiB
C#
375 lines
12 KiB
C#
#region ================== Copyright (c) 2020 Boris Iwanski
|
|
|
|
/*
|
|
* This program 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.
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program.If not, see<http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#endregion
|
|
|
|
#region ================== Namespaces
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Windows.Forms;
|
|
using CodeImp.DoomBuilder.Map;
|
|
using CodeImp.DoomBuilder.Windows;
|
|
using CodeImp.DoomBuilder.UDBScript.Wrapper;
|
|
using CodeImp.DoomBuilder.UDBScript.API;
|
|
using Jint;
|
|
using Jint.Runtime;
|
|
using Jint.Runtime.Interop;
|
|
using Esprima;
|
|
|
|
#endregion
|
|
|
|
namespace CodeImp.DoomBuilder.UDBScript
|
|
{
|
|
class ScriptRunner
|
|
{
|
|
#region ================== Variables
|
|
|
|
private ScriptInfo scriptinfo;
|
|
Engine engine;
|
|
Stopwatch stopwatch;
|
|
int oldprocessingcount;
|
|
|
|
#endregion
|
|
|
|
#region ================== Constructor
|
|
|
|
public ScriptRunner(ScriptInfo scriptoption)
|
|
{
|
|
this.scriptinfo = scriptoption;
|
|
stopwatch = new Stopwatch();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Methods
|
|
|
|
/// <summary>
|
|
/// Stops the timer, pausing the script's runtime constraint
|
|
/// </summary>
|
|
public void StopTimer()
|
|
{
|
|
stopwatch.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes the timer, resuming the script's runtime constraint
|
|
/// </summary>
|
|
public void ResumeTimer()
|
|
{
|
|
stopwatch.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a message box with an "OK" button
|
|
/// </summary>
|
|
/// <param name="message">Message to show</param>
|
|
public void ShowMessage(object message)
|
|
{
|
|
BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Action(() => {
|
|
if (message == null)
|
|
message = string.Empty;
|
|
|
|
stopwatch.Stop();
|
|
MessageForm mf = new MessageForm("OK", null, message.ToString());
|
|
DialogResult result = mf.ShowDialog();
|
|
stopwatch.Start();
|
|
|
|
if (result == DialogResult.Abort)
|
|
throw new UserScriptAbortException();
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a message box with an "Yes" and "No" button
|
|
/// </summary>
|
|
/// <param name="message">Message to show</param>
|
|
/// <returns>true if "Yes" was clicked, false if "No" was clicked</returns>
|
|
public bool ShowMessageYesNo(object message)
|
|
{
|
|
return (bool)BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Func<bool>(() =>
|
|
{
|
|
if (message == null)
|
|
message = string.Empty;
|
|
|
|
stopwatch.Stop();
|
|
MessageForm mf = new MessageForm("Yes", "No", message.ToString());
|
|
DialogResult result = mf.ShowDialog();
|
|
stopwatch.Start();
|
|
|
|
if (result == DialogResult.Abort)
|
|
throw new UserScriptAbortException();
|
|
|
|
return result == DialogResult.OK ? true : false;
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exist the script prematurely without undoing its changes.
|
|
/// </summary>
|
|
/// <param name="s"></param>
|
|
private void ExitScript(string s = null)
|
|
{
|
|
if (string.IsNullOrEmpty(s))
|
|
throw new ExitScriptException();
|
|
|
|
throw new ExitScriptException(s);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exist the script prematurely with undoing its changes.
|
|
/// </summary>
|
|
/// <param name="s"></param>
|
|
private void DieScript(string s = null)
|
|
{
|
|
if (string.IsNullOrEmpty(s))
|
|
throw new DieScriptException();
|
|
|
|
throw new DieScriptException(s);
|
|
}
|
|
|
|
public JavaScriptException CreateRuntimeException(string message)
|
|
{
|
|
return new JavaScriptException(engine.Realm.Intrinsics.Error, message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Imports the code of all script library files in a single string
|
|
/// </summary>
|
|
/// <param name="engine">Scripting engine to load the code into</param>
|
|
/// <param name="errortext">Errors that occured while loading the library code</param>
|
|
/// <returns>true if there were no errors, false if there were errors</returns>
|
|
private bool ImportLibraryCode(Engine engine, out string errortext)
|
|
{
|
|
string path = Path.Combine(General.AppPath, "UDBScript", "Libraries");
|
|
string[] files = Directory.GetFiles(path, "*.js", SearchOption.AllDirectories);
|
|
|
|
errortext = string.Empty;
|
|
|
|
foreach (string file in files)
|
|
{
|
|
try
|
|
{
|
|
ParserOptions po = new ParserOptions(file.Remove(0, General.AppPath.Length));
|
|
engine.Execute(File.ReadAllText(file), po);
|
|
}
|
|
catch (ParserException e)
|
|
{
|
|
MessageBox.Show("There was an error while loading the library " + file + ":\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
return false;
|
|
}
|
|
catch (JavaScriptException e)
|
|
{
|
|
if (e.Error.Type != Jint.Runtime.Types.String)
|
|
{
|
|
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace);
|
|
sef.ShowDialog();
|
|
}
|
|
else
|
|
General.Interface.DisplayStatus(StatusType.Warning, e.Message); // We get here if "throw" is used in a script
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the different exceptions we're expecting, and withdraws the undo snapshot if necessary.
|
|
/// </summary>
|
|
/// <param name="e">The exception to handle</param>
|
|
public void HandleExceptions(Exception e)
|
|
{
|
|
bool abort = false;
|
|
|
|
if(e is UserScriptAbortException)
|
|
{
|
|
General.Interface.DisplayStatus(StatusType.Warning, "Script aborted");
|
|
abort = true;
|
|
}
|
|
else if(e is ParserException)
|
|
{
|
|
MessageBox.Show("There is an error while parsing the script:\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
abort = true;
|
|
}
|
|
else if(e is JavaScriptException)
|
|
{
|
|
if (((JavaScriptException)e).Error.Type != Jint.Runtime.Types.String)
|
|
{
|
|
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace);
|
|
sef.ShowDialog();
|
|
}
|
|
else
|
|
General.Interface.DisplayStatus(StatusType.Warning, e.Message); // We get here if "throw" is used in a script
|
|
|
|
abort = true;
|
|
}
|
|
else if(e is ExitScriptException)
|
|
{
|
|
if (!string.IsNullOrEmpty(e.Message))
|
|
General.Interface.DisplayStatus(StatusType.Ready, e.Message);
|
|
}
|
|
else if(e is DieScriptException)
|
|
{
|
|
if (!string.IsNullOrEmpty(e.Message))
|
|
General.Interface.DisplayStatus(StatusType.Warning, e.Message);
|
|
|
|
abort = true;
|
|
}
|
|
else if(e is ExecutionCanceledException)
|
|
{
|
|
abort = true;
|
|
}
|
|
else // Catch anything else we didn't think about
|
|
{
|
|
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace);
|
|
sef.ShowDialog();
|
|
|
|
abort = true;
|
|
}
|
|
|
|
if (abort)
|
|
General.Map.UndoRedo.WithdrawUndo();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets everything up for running the script. This has to be done on the UI thread.
|
|
/// </summary>
|
|
/// <param name="cancellationtoken">Cancellation token to cancel the running script</param>
|
|
public void PreRun(CancellationToken cancellationtoken)
|
|
{
|
|
string importlibraryerrors;
|
|
|
|
// If the script requires a higher version of UDBScript than this version ask the user if they want
|
|
// to execute it anyways. Remember the choice for this session if "yes" was selected.
|
|
if (scriptinfo.Version > BuilderPlug.UDB_SCRIPT_VERSION && !scriptinfo.IgnoreVersion)
|
|
{
|
|
if (MessageBox.Show("The script requires a higher version of the feature set than this version of UDBScript supports. Executing this script might fail\n\nRequired feature version: " + scriptinfo.Version + "\nUDBScript feature version: " + BuilderPlug.UDB_SCRIPT_VERSION + "\n\nExecute anyway?", "UDBScript feature version too low", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
|
|
return;
|
|
|
|
scriptinfo.IgnoreVersion = true;
|
|
}
|
|
|
|
// Make sure the option value gets saved if an option is currently being edited
|
|
BuilderPlug.Me.EndOptionEdit();
|
|
General.Interface.Focus();
|
|
|
|
// Set engine options
|
|
Options options = new Options();
|
|
options.CancellationToken(cancellationtoken);
|
|
options.AllowOperatorOverloading();
|
|
options.SetTypeResolver(new TypeResolver
|
|
{
|
|
MemberFilter = member => member.Name != nameof(GetType)
|
|
});
|
|
|
|
// Create the script engine
|
|
engine = new Engine(options);
|
|
engine.SetValue("showMessage", new Action<object>(ShowMessage));
|
|
engine.SetValue("showMessageYesNo", new Func<object, bool>(ShowMessageYesNo));
|
|
engine.SetValue("exit", new Action<string>(ExitScript));
|
|
engine.SetValue("die", new Action<string>(DieScript));
|
|
engine.SetValue("QueryOptions", TypeReference.CreateTypeReference(engine, typeof(QueryOptions)));
|
|
engine.SetValue("ScriptOptions", scriptinfo.GetScriptOptionsObject());
|
|
engine.SetValue("Map", new MapWrapper());
|
|
engine.SetValue("GameConfiguration", new GameConfigurationWrapper());
|
|
engine.SetValue("Angle2D", TypeReference.CreateTypeReference(engine, typeof(Angle2DWrapper)));
|
|
engine.SetValue("Vector3D", TypeReference.CreateTypeReference(engine, typeof(Vector3DWrapper)));
|
|
engine.SetValue("Vector2D", TypeReference.CreateTypeReference(engine, typeof(Vector2DWrapper)));
|
|
engine.SetValue("Line2D", TypeReference.CreateTypeReference(engine, typeof(Line2DWrapper)));
|
|
engine.SetValue("UniValue", TypeReference.CreateTypeReference(engine, typeof(UniValue)));
|
|
engine.SetValue("Data", TypeReference.CreateTypeReference(engine, typeof(DataWrapper)));
|
|
|
|
// These can not be directly instanciated and don't have static method, but it's required to
|
|
// for example use "instanceof" in scripts
|
|
engine.SetValue("Linedef", TypeReference.CreateTypeReference(engine, typeof(LinedefWrapper)));
|
|
engine.SetValue("Sector", TypeReference.CreateTypeReference(engine, typeof(SectorWrapper)));
|
|
engine.SetValue("Sidedef", TypeReference.CreateTypeReference(engine, typeof(SidedefWrapper)));
|
|
engine.SetValue("Thing", TypeReference.CreateTypeReference(engine, typeof(ThingWrapper)));
|
|
engine.SetValue("Vertex", TypeReference.CreateTypeReference(engine, typeof(VertexWrapper)));
|
|
|
|
#if DEBUG
|
|
engine.SetValue("log", new Action<object>(Console.WriteLine));
|
|
#endif
|
|
|
|
// Import all library files into the current engine
|
|
if (ImportLibraryCode(engine, out importlibraryerrors) == false)
|
|
return;
|
|
|
|
// Tell the mode that a script is about to be run
|
|
General.Editing.Mode.OnScriptRunBegin();
|
|
|
|
General.Map.UndoRedo.CreateUndo("Run script " + scriptinfo.Name);
|
|
General.Map.Map.ClearAllMarks(false);
|
|
|
|
General.Map.Map.IsSafeToAccess = false;
|
|
|
|
// Disable all processing. Has to be done as many times as it was enabled.
|
|
// Save old value since after running the script we need to enable it as many times
|
|
oldprocessingcount = General.Interface.ProcessingCount;
|
|
for (int i = 0; i < oldprocessingcount; i++)
|
|
General.Interface.DisableProcessing();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the script
|
|
/// </summary>
|
|
public void Run(IProgress<int> progress, IProgress<string> status, IProgress<string> log)
|
|
{
|
|
engine.SetValue("ProgressInfo", new ProgressInfo(progress, status, log));
|
|
// Read the current script file
|
|
string script = File.ReadAllText(scriptinfo.ScriptFile);
|
|
|
|
// Run the script file
|
|
ParserOptions po = new ParserOptions(scriptinfo.ScriptFile.Remove(0, General.AppPath.Length));
|
|
|
|
stopwatch.Start();
|
|
engine.Execute(script, po);
|
|
stopwatch.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanups and updates after the script stopped running. Has to be called from the UI thread.
|
|
/// </summary>
|
|
public void PostRun()
|
|
{
|
|
General.Map.Map.IsSafeToAccess = true;
|
|
|
|
// Do some updates
|
|
General.Map.Map.Update();
|
|
General.Map.ThingsFilter.Update();
|
|
//General.Interface.RedrawDisplay();
|
|
|
|
// Tell the mode that running the script ended
|
|
General.Editing.Mode.OnScriptRunEnd();
|
|
|
|
// Enable processing again, if required
|
|
for (int i = 0; i < oldprocessingcount; i++)
|
|
General.Interface.EnableProcessing();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|