UDBScript asynchronous execution (#684)

Script run by UDBScript are now executed asynchronously
This commit is contained in:
biwa 2022-01-03 14:33:34 +01:00 committed by GitHub
parent 12f32e2bc6
commit e2374102ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 801 additions and 115 deletions

View file

@ -144,6 +144,7 @@ namespace CodeImp.DoomBuilder
internal const int WM_UIACTION = WM_USER + 1;
internal const int WM_SYSCOMMAND = 0x112;
internal const int WM_MOUSEHWHEEL = 0x020E; // [ZZ]
internal const int WM_MOUSEWHEEL = 0x20A;
internal const int SC_KEYMENU = 0xF100;
internal const int CB_SETITEMHEIGHT = 0x153;
//internal const int CB_SHOWDROPDOWN = 0x14F;

View file

@ -101,6 +101,9 @@ namespace CodeImp.DoomBuilder.Map
// Statics
private static long emptylongname;
private static UniValue virtualsectorvalue;
// Concurrency
private bool issafetoaccess;
// Disposing
private bool isdisposed;
@ -169,6 +172,13 @@ namespace CodeImp.DoomBuilder.Map
internal List<UniversalEntry> UnknownUDMFData { get { return unknownudmfdata; } set { unknownudmfdata = value; } }
/// <summary>
/// If it's safe to access (either reading or modifying) the map data. May only be read or set from the UI thread to
/// avoid racing conditions. Code that wants to access the map data on on a timer or in another thread must honor
/// this setting to avoid exceptions
/// </summary>
public bool IsSafeToAccess { get { return issafetoaccess; } set { issafetoaccess = value; } }
#endregion
#region ================== Constructor / Disposer
@ -190,6 +200,8 @@ namespace CodeImp.DoomBuilder.Map
lastsectorindex = 0;
autoremove = true;
unknownudmfdata = new List<UniversalEntry>();
issafetoaccess = true;
// We have no destructor
GC.SuppressFinalize(this);
@ -213,6 +225,8 @@ namespace CodeImp.DoomBuilder.Map
autoremove = true;
unknownudmfdata = new List<UniversalEntry>();
issafetoaccess = true;
// Deserialize
Deserialize(stream);
@ -1077,9 +1091,14 @@ namespace CodeImp.DoomBuilder.Map
// Update all sectors
if(dosectors)
{
foreach(Sector s in sectors) s.Triangulate();
foreach (Sector s in sectors)
{
s.Triangulate();
s.UpdateBBox();
}
General.Map.CRenderer2D.Surfaces.AllocateBuffers();
foreach(Sector s in sectors) s.CreateSurfaces();
foreach (Sector s in sectors) s.CreateSurfaces();
General.Map.CRenderer2D.Surfaces.UnlockBuffers();
}
}

View file

@ -42,6 +42,7 @@ namespace CodeImp.DoomBuilder.Windows
bool IsActiveWindow { get; }
string ActiveDockerTabName { get; } //mxd
RenderTargetControl Display { get; }
int ProcessingCount { get; }
//mxd. Events
event EventHandler OnEditFormValuesChanged;

View file

@ -180,6 +180,7 @@ namespace CodeImp.DoomBuilder.Windows
public StatusInfo Status { get { return status; } }
public static Size ScaledIconSize = new Size(16, 16); //mxd
public static SizeF DPIScaler = new SizeF(1.0f, 1.0f); //mxd
public int ProcessingCount { get { return processingcount; } }
#endregion
@ -1352,22 +1353,26 @@ namespace CodeImp.DoomBuilder.Windows
if(alt) mod |= (int)Keys.Alt;
if(shift) mod |= (int)Keys.Shift;
if(ctrl) mod |= (int)Keys.Control;
// Scrollwheel up?
if(e.Delta > 0)
// Only send key events when the main window can be focused (i.e. no modal dialogs are open)
if (CanFocus)
{
// Invoke actions for scrollwheel
//for(int i = 0; i < e.Delta; i += 120)
General.Actions.KeyPressed((int)SpecialKeys.MScrollUp | mod);
General.Actions.KeyReleased((int)SpecialKeys.MScrollUp | mod);
}
// Scrollwheel down?
else if(e.Delta < 0)
{
// Invoke actions for scrollwheel
//for(int i = 0; i > e.Delta; i -= 120)
General.Actions.KeyPressed((int)SpecialKeys.MScrollDown | mod);
General.Actions.KeyReleased((int)SpecialKeys.MScrollDown | mod);
// Scrollwheel up?
if (e.Delta > 0)
{
// Invoke actions for scrollwheel
//for(int i = 0; i < e.Delta; i += 120)
General.Actions.KeyPressed((int)SpecialKeys.MScrollUp | mod);
General.Actions.KeyReleased((int)SpecialKeys.MScrollUp | mod);
}
// Scrollwheel down?
else if (e.Delta < 0)
{
// Invoke actions for scrollwheel
//for(int i = 0; i > e.Delta; i -= 120)
General.Actions.KeyPressed((int)SpecialKeys.MScrollDown | mod);
General.Actions.KeyReleased((int)SpecialKeys.MScrollDown | mod);
}
}
// Let the base know
@ -4394,7 +4399,7 @@ namespace CodeImp.DoomBuilder.Windows
OnMouseHWheel(delta);
m.Result = new IntPtr(delta);
break;
default:
// Let the base handle the message
base.WndProc(ref m);

View file

@ -159,7 +159,7 @@ namespace CodeImp.DoomBuilder.CommentsPanel
// This finds all comments and updates the list
public void UpdateList()
{
if(!preventupdate)
if(!preventupdate && General.Map.Map.IsSafeToAccess)
{
// Update vertices
Dictionary<string, CommentInfo> newcomments = new Dictionary<string, CommentInfo>(StringComparer.Ordinal);

View file

@ -23,6 +23,7 @@
#region ================== Namespaces
using System;
using System.Collections.Generic;
using System.Linq;
using CodeImp.DoomBuilder.BuilderModes;
@ -41,6 +42,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper
private MapSet map;
private VisualCameraWrapper visualcamera;
private Vector2D mousemappos;
#endregion
@ -86,10 +88,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper
{
get
{
if (General.Editing.Mode is ClassicMode)
return ((ClassicMode)General.Editing.Mode).MouseMapPos;
else
return ((VisualMode)General.Editing.Mode).GetHitPosition();
return mousemappos;
}
}
@ -112,6 +111,11 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper
{
map = General.Map.Map;
visualcamera = new VisualCameraWrapper();
if (General.Editing.Mode is ClassicMode)
mousemappos = ((ClassicMode)General.Editing.Mode).MouseMapPos;
else
mousemappos = ((VisualMode)General.Editing.Mode).GetHitPosition();
}
#endregion
@ -416,8 +420,8 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper
// Snap to map format accuracy
General.Map.Map.SnapAllToAccuracy();
// Update map
General.Map.Map.Update();
// Update map. This has to run on the UI thread
BuilderPlug.Me.ScriptRunnerForm.RunAction(() => General.Map.Map.Update());
// Update textures
General.Map.Data.UpdateUsedTextures();
@ -1178,7 +1182,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper
sectors[i].Join(first);
// Update
General.Map.Map.Update();
BuilderPlug.Me.ScriptRunnerForm.RunAction(() => General.Map.Map.Update());
}
#endregion

View file

@ -97,6 +97,7 @@ namespace CodeImp.DoomBuilder.UDBScript
private Dictionary<int, ScriptInfo> scriptslots;
private string editorexepath;
private PreferencesForm preferencesform;
private ScriptRunnerForm scriptrunnerform;
#endregion
@ -108,6 +109,7 @@ namespace CodeImp.DoomBuilder.UDBScript
internal ScriptRunner ScriptRunner { get { return scriptrunner; } }
internal ScriptDirectoryStructure ScriptDirectoryStructure { get { return scriptdirectorystructure; } }
internal string EditorExePath { get { return editorexepath; } }
public ScriptRunnerForm ScriptRunnerForm { get { return scriptrunnerform; } }
#endregion
@ -138,6 +140,8 @@ namespace CodeImp.DoomBuilder.UDBScript
editorexepath = General.Settings.ReadPluginSetting("externaleditor", string.Empty);
scriptrunnerform = new ScriptRunnerForm();
FindEditor();
}
@ -617,7 +621,7 @@ namespace CodeImp.DoomBuilder.UDBScript
return;
scriptrunner = new ScriptRunner(currentscript);
scriptrunner.Run();
scriptrunnerform.ShowDialog();
}
[BeginAction("udbscriptexecuteslot1")]
@ -665,7 +669,7 @@ namespace CodeImp.DoomBuilder.UDBScript
if (scriptslots.ContainsKey(slot) && scriptslots[slot] != null)
{
scriptrunner = new ScriptRunner(scriptslots[slot]);
scriptrunner.Run();
scriptrunnerform.ShowDialog();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CodeImp.DoomBuilder.UDBScript
{
class ProgressInfo
{
IProgress<int> progress;
IProgress<string> status;
IProgress<string> _log;
public ProgressInfo(IProgress<int> progress, IProgress<string> status, IProgress<string> log)
{
this.progress = progress;
this.status = status;
_log = log;
}
public void setProgress(int p)
{
progress.Report(p);
}
public void setStatus(string s)
{
status.Report(s);
}
public void log(string s)
{
_log.Report(s);
}
}
}

View file

@ -26,6 +26,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Map;
using CodeImp.DoomBuilder.Windows;
@ -47,6 +48,7 @@ namespace CodeImp.DoomBuilder.UDBScript
private ScriptInfo scriptinfo;
Engine engine;
Stopwatch stopwatch;
int oldprocessingcount;
#endregion
@ -84,16 +86,18 @@ namespace CodeImp.DoomBuilder.UDBScript
/// <param name="message">Message to show</param>
public void ShowMessage(object message)
{
if (message == null)
message = string.Empty;
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();
stopwatch.Stop();
MessageForm mf = new MessageForm("OK", null, message.ToString());
DialogResult result = mf.ShowDialog();
stopwatch.Start();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
}));
}
/// <summary>
@ -103,18 +107,21 @@ namespace CodeImp.DoomBuilder.UDBScript
/// <returns>true if "Yes" was clicked, false if "No" was clicked</returns>
public bool ShowMessageYesNo(object message)
{
if (message == null)
message = string.Empty;
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();
stopwatch.Stop();
MessageForm mf = new MessageForm("Yes", "No", message.ToString());
DialogResult result = mf.ShowDialog();
stopwatch.Start();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
if (result == DialogResult.Abort)
throw new UserScriptAbortException();
return result == DialogResult.OK ? true : false;
return result == DialogResult.OK ? true : false;
}));
}
/// <summary>
@ -166,17 +173,16 @@ namespace CodeImp.DoomBuilder.UDBScript
ParserOptions po = new ParserOptions(file.Remove(0, General.AppPath.Length));
engine.Execute(File.ReadAllText(file), po);
}
catch (Esprima.ParserException e)
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 (Jint.Runtime.JavaScriptException e)
catch (JavaScriptException e)
{
if (e.Error.Type != Jint.Runtime.Types.String)
{
//MessageBox.Show("There is an error in the script in line " + e.LineNumber + ":\n\n" + e.Message + "\n\n" + e.StackTrace, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace);
sef.ShowDialog();
}
@ -191,16 +197,74 @@ namespace CodeImp.DoomBuilder.UDBScript
}
/// <summary>
/// Runs the script
/// Handles the different exceptions we're expecting, and withdraws the undo snapshot if necessary.
/// </summary>
public void Run()
/// <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;
bool abort = false;
// 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 (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;
@ -208,22 +272,19 @@ namespace CodeImp.DoomBuilder.UDBScript
scriptinfo.IgnoreVersion = true;
}
// Read the current script file
string script = File.ReadAllText(scriptinfo.ScriptFile);
// 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.Constraint(new RuntimeConstraint(stopwatch));
options.CancellationToken(cancellationtoken);
options.AllowOperatorOverloading();
options.SetTypeResolver(new TypeResolver {
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));
@ -260,65 +321,41 @@ namespace CodeImp.DoomBuilder.UDBScript
// 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
try
{
General.Map.UndoRedo.CreateUndo("Run script " + scriptinfo.Name);
General.Map.Map.ClearAllMarks(false);
ParserOptions po = new ParserOptions(scriptinfo.ScriptFile.Remove(0, General.AppPath.Length));
ParserOptions po = new ParserOptions(scriptinfo.ScriptFile.Remove(0, General.AppPath.Length));
stopwatch.Start();
engine.Execute(script, po);
stopwatch.Stop();
}
stopwatch.Start();
engine.Execute(script, po);
stopwatch.Stop();
}
catch (UserScriptAbortException)
{
General.Interface.DisplayStatus(StatusType.Warning, "Script aborted");
abort = true;
}
catch (ParserException e)
{
MessageBox.Show("There is an error while parsing the script:\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
abort = true;
}
catch (Jint.Runtime.JavaScriptException e)
{
if (e.Error.Type != Jint.Runtime.Types.String)
{
//MessageBox.Show("There is an error in the script in line " + e.LineNumber + ":\n\n" + e.Message + "\n\n" + e.StackTrace, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error);
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;
}
catch(ExitScriptException e)
{
if (!string.IsNullOrEmpty(e.Message))
General.Interface.DisplayStatus(StatusType.Ready, e.Message);
}
catch(DieScriptException e)
{
if (!string.IsNullOrEmpty(e.Message))
General.Interface.DisplayStatus(StatusType.Warning, e.Message);
abort = true;
}
catch (Exception e) // 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>
/// 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();
@ -327,6 +364,10 @@ namespace CodeImp.DoomBuilder.UDBScript
// 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

View file

@ -93,6 +93,7 @@
<Compile Include="Controls\ScriptOptionsControl.Designer.cs">
<DependentUpon>ScriptOptionsControl.cs</DependentUpon>
</Compile>
<Compile Include="ProgressInfo.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
@ -123,6 +124,12 @@
<Compile Include="Windows\QueryOptionsForm.Designer.cs">
<DependentUpon>QueryOptionsForm.cs</DependentUpon>
</Compile>
<Compile Include="Windows\ScriptRunnerForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Windows\ScriptRunnerForm.Designer.cs">
<DependentUpon>ScriptRunnerForm.cs</DependentUpon>
</Compile>
<Compile Include="Windows\UDBScriptErrorForm.cs">
<SubType>Form</SubType>
</Compile>
@ -162,6 +169,9 @@
<EmbeddedResource Include="Windows\QueryOptionsForm.resx">
<DependentUpon>QueryOptionsForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Windows\ScriptRunnerForm.resx">
<DependentUpon>ScriptRunnerForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Windows\UDBScriptErrorForm.resx">
<DependentUpon>UDBScriptErrorForm.cs</DependentUpon>
</EmbeddedResource>

View file

@ -0,0 +1,110 @@
namespace CodeImp.DoomBuilder.UDBScript
{
partial class ScriptRunnerForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.progressbar = new System.Windows.Forms.ProgressBar();
this.lbStatus = new System.Windows.Forms.Label();
this.btnAction = new System.Windows.Forms.Button();
this.tbLog = new System.Windows.Forms.TextBox();
this.SuspendLayout();
//
// progressbar
//
this.progressbar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressbar.Location = new System.Drawing.Point(12, 25);
this.progressbar.Name = "progressbar";
this.progressbar.Size = new System.Drawing.Size(419, 23);
this.progressbar.Step = 1;
this.progressbar.Style = System.Windows.Forms.ProgressBarStyle.Continuous;
this.progressbar.TabIndex = 0;
//
// lbStatus
//
this.lbStatus.AutoSize = true;
this.lbStatus.Location = new System.Drawing.Point(12, 9);
this.lbStatus.Name = "lbStatus";
this.lbStatus.Size = new System.Drawing.Size(84, 13);
this.lbStatus.TabIndex = 1;
this.lbStatus.Text = "Running script...";
//
// btnAction
//
this.btnAction.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnAction.Location = new System.Drawing.Point(437, 25);
this.btnAction.Name = "btnAction";
this.btnAction.Size = new System.Drawing.Size(75, 23);
this.btnAction.TabIndex = 2;
this.btnAction.Text = "Cancel";
this.btnAction.UseVisualStyleBackColor = true;
this.btnAction.Click += new System.EventHandler(this.btnAction_Click);
//
// tbLog
//
this.tbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tbLog.Location = new System.Drawing.Point(12, 54);
this.tbLog.Multiline = true;
this.tbLog.Name = "tbLog";
this.tbLog.ReadOnly = true;
this.tbLog.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.tbLog.Size = new System.Drawing.Size(500, 118);
this.tbLog.TabIndex = 3;
//
// ScriptRunnerForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(524, 184);
this.ControlBox = false;
this.Controls.Add(this.tbLog);
this.Controls.Add(this.btnAction);
this.Controls.Add(this.lbStatus);
this.Controls.Add(this.progressbar);
this.MinimumSize = new System.Drawing.Size(540, 200);
this.Name = "ScriptRunnerForm";
this.ShowIcon = false;
this.Text = "Running script";
this.WindowState = System.Windows.Forms.FormWindowState.Minimized;
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.ScriptRunnerForm_FormClosed);
this.Shown += new System.EventHandler(this.ScriptRunnerForm_Shown);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ProgressBar progressbar;
private System.Windows.Forms.Label lbStatus;
private System.Windows.Forms.Button btnAction;
private System.Windows.Forms.TextBox tbLog;
}
}

View file

@ -0,0 +1,334 @@
#region ================== Copyright (c) 2022 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.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Windows;
#endregion
namespace CodeImp.DoomBuilder.UDBScript
{
public partial class ScriptRunnerForm : DelayedForm
{
#region ================== Constants
/// <summary>
/// How long a script is allowed to run until the form is made visible.
/// </summary>
const int RUNTIME_THRESHOLD = 1000;
#endregion
#region ================== Variables
/// <summary>
/// Cancellation token for stopping the script.
/// </summary>
CancellationTokenSource cancellationtokensource;
/// <summary>
/// If script is currently executed or not.
/// </summary>
bool running;
/// <summary>
/// How many milliseconds the script has been running
/// </summary>
double runningseconds;
/// <summary>
/// Determines if the form should be automatically closed when the script is finished.
/// </summary>
bool autoclose;
/// <summary>
/// Stopwatch used to determine how long the script is already running.
/// </summary>
Stopwatch stopwatch;
/// <summary>
/// Timer for making the form visible when the script is running for too long.
/// </summary>
System.Windows.Forms.Timer timer;
#endregion
#region ================== Methods
public ScriptRunnerForm()
{
InitializeComponent();
}
/// <summary>
/// Invokes a method, but stops the timer before running the code, and starting the timer after the code ran.
/// </summary>
/// <param name="method">Method to invoke</param>
/// <returns>Return value of the method</returns>
public object InvokePaused(Delegate method)
{
if (InvokeRequired)
{
return Invoke(new Action(() => InvokePaused(method)));
}
else
{
stopwatch.Stop();
object result = Invoke(method);
stopwatch.Start();
return result;
}
}
/// <summary>
/// Invokes a method.
/// </summary>
/// <param name="action">Method to invoke</param>
public void RunAction(Action action)
{
if (InvokeRequired)
Invoke(action);
else
action();
}
/// <summary>
/// Sets the value of the progress bar (in the range from 0 to 100).
/// </summary>
/// <param name="value">Value of the progress bar (in the range from 0 to 100)</param>
private void SetProgress(int value)
{
if (progressbar.Style != ProgressBarStyle.Continuous)
progressbar.Style = ProgressBarStyle.Continuous;
// Do some trickery to remove the movement of the progress bar, since it can
// otherwise screw with how much the progress bar is filled
if (progressbar.Value != value)
{
if (value > progressbar.Maximum)
value = progressbar.Maximum;
else if (value < progressbar.Minimum)
value = progressbar.Minimum;
if (progressbar.Maximum == value)
{
progressbar.Value = value;
progressbar.Value = value - 1;
}
else
{
progressbar.Value = value + 1;
}
progressbar.Value = value;
}
// Make the form visible so that the user can actually see the progress bar
MakeVisible();
}
private void SetProgressStatus(string status)
{
lbStatus.Text = status;
}
private void Log(string text)
{
// If there's something in the log we don't want to automatically
// close the form, otherwise the user could not read the contents
autoclose = false;
// Since we don't want to have a useless line at the end of the textbox
// we add a new line before adding the new text (unless there's no text
// at all yet, then we don't add a new line
if (!string.IsNullOrEmpty(tbLog.Text))
tbLog.AppendText(Environment.NewLine);
// Add the new text
tbLog.AppendText(text);
// Make the form visible so that the user can actually see the status bar
MakeVisible();
}
private async void RunScript(CancellationToken cancellationtoken)
{
// Callbacks for setting the progress bar, status text, and adding log lines from the script
Progress<int> progress = new Progress<int>(SetProgress);
Progress<string> status = new Progress<string>(SetProgressStatus);
Progress<string> log = new Progress<string>(Log);
running = true;
// Prepare running the script
BuilderPlug.Me.ScriptRunner.PreRun(cancellationtoken);
try
{
await Task.Run(() => BuilderPlug.Me.ScriptRunner.Run(progress, status, log));
stopwatch.Stop();
}
catch (Exception ex)
{
stopwatch.Stop();
BuilderPlug.Me.ScriptRunner.HandleExceptions(ex);
}
// Clean up and update after running the script
BuilderPlug.Me.ScriptRunner.PostRun();
running = false;
btnAction.Text = "Close";
btnAction.Enabled = true;
if (autoclose)
{
MakeInvisible();
//Hide();
Close();
}
}
/// <summary>
/// Makes the form visible.
/// </summary>
private void MakeVisible()
{
Opacity = 1.0;
btnAction.Enabled = true;
}
/// <summary>
/// Makes the form invisible.
/// </summary>
private void MakeInvisible()
{
Opacity = 0.0;
}
#endregion
#region ================== Events
/// <summary>
/// Cancels the currently running script, or closes the form if no script is running.
/// </summary>
/// <param name="sender">The sender</param>
/// <param name="e">Event arguments</param>
private void btnAction_Click(object sender, EventArgs e)
{
if (running)
{
btnAction.Enabled = false;
cancellationtokensource.Cancel();
}
else
{
MakeInvisible();
//Hide();
Close();
}
}
/// <summary>
/// Sets everything up for running the script, and then immediately runs the script.
/// </summary>
/// <param name="sender">The sender</param>
/// <param name="e">Event arguments</param>
private void ScriptRunnerForm_Shown(object sender, EventArgs e)
{
cancellationtokensource = new CancellationTokenSource();
autoclose = true;
runningseconds = 0;
progressbar.Value = 0;
progressbar.Style = ProgressBarStyle.Marquee;
Text = "Running script";
lbStatus.Text = "Running script...";
btnAction.Text = "Cancel";
// Disable the button because it could otherwise be pressed while the form is invisible.
// It'll be enabled as soon as the form is made visible
btnAction.Enabled = false;
tbLog.Clear();
// The timer ticks ever 100ms. The method it runs checks how long the script is running
// and makes the form visible if the runtime threshold has been reached
timer = new System.Windows.Forms.Timer();
timer.Interval = 100;
timer.Tick += timerShow_Tick;
timer.Start();
// This stopwatch is used to measure how long the script has been running
stopwatch = new Stopwatch();
stopwatch.Start();
// Start running the script
RunScript(cancellationtokensource.Token);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
MakeInvisible();
}
/// <summary>
/// Makes the form visible if the runtime threshold has been reached. Shows the elapsed time the script is running.
/// </summary>
/// <param name="sender">The sender</param>
/// <param name="e">Event arguments</param>
private void timerShow_Tick(object sender, EventArgs e)
{
if (Opacity == 0.0 && stopwatch.ElapsedMilliseconds > 1000)
{
MakeVisible();
}
double newrunningsecods = Math.Floor(stopwatch.Elapsed.TotalSeconds);
if(newrunningsecods > runningseconds)
{
runningseconds = newrunningsecods;
Text = "Running script (" + string.Format("{0:D2}:{1:D2}:{2:D2}", stopwatch.Elapsed.Hours, stopwatch.Elapsed.Minutes, stopwatch.Elapsed.Seconds) + ")";
}
}
private void ScriptRunnerForm_FormClosed(object sender, FormClosedEventArgs e)
{
timer.Stop();
}
#endregion
}
}

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>