2021-11-28 13:00:24 +00:00
#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 ;
2022-01-03 13:33:34 +00:00
using System.Threading ;
2021-11-28 13:00:24 +00:00
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 ;
2022-01-03 13:33:34 +00:00
int oldprocessingcount ;
2021-11-28 13:00:24 +00:00
#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 )
{
2022-01-03 13:33:34 +00:00
BuilderPlug . Me . ScriptRunnerForm . InvokePaused ( new Action ( ( ) = > {
if ( message = = null )
message = string . Empty ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
stopwatch . Stop ( ) ;
MessageForm mf = new MessageForm ( "OK" , null , message . ToString ( ) ) ;
DialogResult result = mf . ShowDialog ( ) ;
stopwatch . Start ( ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
if ( result = = DialogResult . Abort )
throw new UserScriptAbortException ( ) ;
} ) ) ;
2021-11-28 13:00:24 +00:00
}
/// <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 )
{
2022-01-03 13:33:34 +00:00
return ( bool ) BuilderPlug . Me . ScriptRunnerForm . InvokePaused ( new Func < bool > ( ( ) = >
{
if ( message = = null )
message = string . Empty ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
stopwatch . Stop ( ) ;
MessageForm mf = new MessageForm ( "Yes" , "No" , message . ToString ( ) ) ;
DialogResult result = mf . ShowDialog ( ) ;
stopwatch . Start ( ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
if ( result = = DialogResult . Abort )
throw new UserScriptAbortException ( ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
return result = = DialogResult . OK ? true : false ;
} ) ) ;
2021-11-28 13:00:24 +00:00
}
/// <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 ) ;
}
2022-01-03 13:33:34 +00:00
catch ( ParserException e )
2021-11-28 13:00:24 +00:00
{
MessageBox . Show ( "There was an error while loading the library " + file + ":\n\n" + e . Message , "Script error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return false ;
}
2022-01-03 13:33:34 +00:00
catch ( JavaScriptException e )
2021-11-28 13:00:24 +00:00
{
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>
2022-01-03 13:33:34 +00:00
/// Handles the different exceptions we're expecting, and withdraws the undo snapshot if necessary.
2021-11-28 13:00:24 +00:00
/// </summary>
2022-01-03 13:33:34 +00:00
/// <param name="e">The exception to handle</param>
public void HandleExceptions ( Exception e )
2021-11-28 13:00:24 +00:00
{
bool abort = false ;
2022-01-03 13:33:34 +00:00
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 ;
2021-11-28 13:00:24 +00:00
// 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.
2022-01-03 13:33:34 +00:00
if ( scriptinfo . Version > BuilderPlug . UDB_SCRIPT_VERSION & & ! scriptinfo . IgnoreVersion )
2021-11-28 13:00:24 +00:00
{
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 ( ) ;
2022-01-03 13:33:34 +00:00
options . CancellationToken ( cancellationtoken ) ;
2021-11-28 13:00:24 +00:00
options . AllowOperatorOverloading ( ) ;
2022-01-03 13:33:34 +00:00
options . SetTypeResolver ( new TypeResolver
{
2021-11-28 13:00:24 +00:00
MemberFilter = member = > member . Name ! = nameof ( GetType )
} ) ;
2022-01-03 13:33:34 +00:00
2021-11-28 13:00:24 +00:00
// 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 ) ) ) ;
UDBScript: Exported the classes Linedef, Sector, Sidedef, Thing, and Vertex, so that they can be used with instanceof
UDBScript: Map class: the getSidedefsFromSelectedLinedefs() method now correctly only returns the Sidedefs of selected Linedefs in visual mode (and not also the highlighted one)
UDBScript: Map class: added a new getSidedefsFromSelectedOrHighlightedLinedefs() method as the equivalent to the other getSelectedOrHighlighted*() methods
UDBScript: Sector class: added new floorSelected, ceilingSelected, floorHighlighted, and ceilingHighlighted properties. Those are mostly useful in visual mode, since they always return true when the Sector is selected or highlighted in the classic modes. The properties are read-only
UDBScript: Sidedef class: added new upperSelected, middleSelected, lowerSelected, upperHighlighted, middleHighlighted, and lowerHighlighted properties. Those are mostly useful in visual mode, since they always return true when the parent Linedef is selected or highlighted in the classic modes. The properties are read-only
UDBScript: added new example to apply textures for floor/ceiling and upper/middle/lower texture for selected map elements
UDBScript: updated documentation
2021-12-25 13:43:56 +00:00
// 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 ) ) ) ;
2021-11-28 13:00:24 +00:00
#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 ( ) ;
2022-01-03 13:33:34 +00:00
General . Map . UndoRedo . CreateUndo ( "Run script " + scriptinfo . Name ) ;
General . Map . Map . ClearAllMarks ( false ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
General . Map . Map . IsSafeToAccess = false ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
// 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 ( ) ;
}
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
/// <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 ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
// Run the script file
ParserOptions po = new ParserOptions ( scriptinfo . ScriptFile . Remove ( 0 , General . AppPath . Length ) ) ;
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
stopwatch . Start ( ) ;
engine . Execute ( script , po ) ;
stopwatch . Stop ( ) ;
}
2021-11-28 13:00:24 +00:00
2022-01-03 13:33:34 +00:00
/// <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 ;
2021-11-28 13:00:24 +00:00
// 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 ( ) ;
2022-01-03 13:33:34 +00:00
// Enable processing again, if required
for ( int i = 0 ; i < oldprocessingcount ; i + + )
General . Interface . EnableProcessing ( ) ;
2021-11-28 13:00:24 +00:00
}
#endregion
}
}