Skip to content

Commit 5e97c4d

Browse files
committed
Implement scripting API version 2, making it possible for scripts to show custom INItializableWindow UIs
1 parent 3285d2a commit 5e97c4d

7 files changed

Lines changed: 245 additions & 47 deletions

File tree

src/TSMapEditor/Constants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace TSMapEditor
44
{
55
public static class Constants
66
{
7-
public const string ReleaseVersion = "1.4.12";
7+
public const string ReleaseVersion = "1.5.0";
88

99
public static int CellSizeX = 48;
1010
public static int CellSizeY = 24;

src/TSMapEditor/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@
3434
// You can specify all the values or you can default the Build and Revision Numbers
3535
// by using the '*' as shown below:
3636
// [assembly: AssemblyVersion("1.0.*")]
37-
[assembly: AssemblyVersion("1.4.*")]
37+
[assembly: AssemblyVersion("1.5.*")]

src/TSMapEditor/Scripts/ScriptRunner.cs

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,64 @@
11
using Rampastring.Tools;
2+
using Rampastring.XNAUI;
23
using System;
34
using System.IO;
45
using System.Reflection;
56
using TSMapEditor.Models;
7+
using TSMapEditor.Rendering;
8+
using TSMapEditor.UI;
9+
using TSMapEditor.UI.Windows;
610
using Westwind.Scripting;
711

812
namespace TSMapEditor.Scripts
913
{
14+
// Has to be a class, Westwind.Scripting does not appear to recognize this if it is a struct.
15+
public class ScriptDependencies
16+
{
17+
public Map Map;
18+
public ICursorActionTarget CursorActionTarget;
19+
public EditorState EditorState;
20+
public WindowManager WindowManager;
21+
public WindowController WindowController;
22+
23+
public ScriptDependencies(Map map, ICursorActionTarget cursorActionTarget, EditorState editorState, WindowManager windowManager, WindowController windowController)
24+
{
25+
Map = map;
26+
CursorActionTarget = cursorActionTarget;
27+
EditorState = editorState;
28+
WindowManager = windowManager;
29+
WindowController = windowController;
30+
}
31+
}
32+
1033
public static class ScriptRunner
1134
{
1235
private static object scriptClassInstance;
13-
private static MethodInfo getDescriptionMethod;
36+
37+
private static MethodInfo getDescriptionMethod; // V1 only
1438
private static MethodInfo performMethod;
15-
private static MethodInfo getSuccessMessageMethod;
39+
private static MethodInfo getSuccessMessageMethod; // V1 only
40+
41+
public static int ActiveScriptAPIVersion;
42+
43+
public static string CompileScript(ScriptDependencies scriptDependencies, string scriptPath)
44+
{
45+
if (!File.Exists(scriptPath))
46+
return "The script file does not exist!";
47+
48+
var sourceCode = File.ReadAllText(scriptPath);
49+
string error = CompileSource(scriptDependencies, sourceCode);
50+
if (error != null)
51+
return error;
52+
53+
return null;
54+
}
55+
56+
public static string GetDescriptionFromScriptV1()
57+
{
58+
return (string)getDescriptionMethod.Invoke(scriptClassInstance, null);
59+
}
1660

17-
public static string RunScript(Map map, string scriptPath)
61+
public static string RunScriptV1(Map map, string scriptPath)
1862
{
1963
if (scriptClassInstance == null || performMethod == null || getSuccessMessageMethod == null)
2064
throw new InvalidOperationException("Script not properly compiled!");
@@ -26,7 +70,7 @@ public static string RunScript(Map map, string scriptPath)
2670
performMethod.Invoke(scriptClassInstance, new object[] { map });
2771
return (string)getSuccessMessageMethod.Invoke(scriptClassInstance, null);
2872
}
29-
catch (Exception ex) // rare case where catching Exception is OK, we cannot know what the script can throw
73+
catch (Exception ex) // catching Exception is OK, we cannot know what the script can throw
3074
{
3175
string errorMessage = ex.Message;
3276

@@ -44,27 +88,43 @@ public static string RunScript(Map map, string scriptPath)
4488
}
4589
}
4690

47-
public static (string error, string description) GetDescriptionFromScript(Map map, string scriptPath)
91+
public static string RunScriptV2()
4892
{
49-
if (!File.Exists(scriptPath))
50-
return ("The script file does not exist!", null);
93+
try
94+
{
95+
performMethod.Invoke(scriptClassInstance, null);
96+
}
97+
catch (Exception ex) // catching Exception is OK, we cannot know what the script can throw
98+
{
99+
string errorMessage = ex.Message;
51100

52-
var sourceCode = File.ReadAllText(scriptPath);
53-
string error = CompileSource(map, sourceCode);
54-
if (error != null)
55-
return (error, null);
101+
while (ex.InnerException != null)
102+
{
103+
ex = ex.InnerException;
104+
errorMessage += Environment.NewLine + Environment.NewLine +
105+
"Inner exception message: " + ex.Message + Environment.NewLine +
106+
"Stack trace: " + ex.StackTrace;
107+
}
108+
109+
Logger.Log("Exception while running script. Returned exception message: " + errorMessage);
56110

57-
return (null, (string)getDescriptionMethod.Invoke(scriptClassInstance, null));
111+
return "An error occurred while running the script. Returned error message: " + Environment.NewLine + Environment.NewLine + errorMessage;
112+
}
113+
114+
return null;
58115
}
59116

60-
private static string CompileSource(Map map, string source)
117+
private static string CompileSource(ScriptDependencies scriptDependencies, string source)
61118
{
62119
var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
63120
script.AddLoadedReferences();
64121
script.AddNamespace("TSMapEditor");
65122
script.AddNamespace("TSMapEditor.Models");
66123
script.AddNamespace("TSMapEditor.Rendering");
67124
script.AddNamespace("TSMapEditor.GameMath");
125+
script.AddNamespace("TSMapEditor.Scripts");
126+
script.AddNamespace("TSMapEditor.UI");
127+
script.AddNamespace("TSMapEditor.UI.Controls");
68128

69129
getDescriptionMethod = null;
70130
performMethod = null;
@@ -77,8 +137,80 @@ private static string CompileSource(Map map, string source)
77137
return script.ErrorMessage;
78138
}
79139

140+
int version = 1;
141+
Type classType = instance.GetType();
142+
var properties = classType.GetProperties();
143+
var apiVersionProperty = Array.Find(properties, prop => prop.Name == "ApiVersion");
144+
if (apiVersionProperty != null)
145+
{
146+
if (apiVersionProperty.PropertyType != typeof(int))
147+
{
148+
return "ApiVersion property is not an integer!";
149+
}
150+
151+
version = (int)apiVersionProperty.GetValue(instance);
152+
}
153+
154+
ActiveScriptAPIVersion = version;
155+
156+
if (version == 1)
157+
{
158+
return ExtractScriptV1(instance);
159+
}
160+
else if (version == 2)
161+
{
162+
return ExtractScriptV2(instance, scriptDependencies);
163+
}
164+
165+
return $"Unsupported scripting API version: {version}. Contact the script's author for troubleshooting.";
166+
}
167+
168+
private static string ExtractScriptV2(object instance, ScriptDependencies scriptDependencies)
169+
{
170+
scriptClassInstance = instance;
171+
Type classType = instance.GetType();
172+
173+
var methods = classType.GetMethods();
174+
foreach (MethodInfo method in methods)
175+
{
176+
if (method.Name == "Perform")
177+
{
178+
performMethod = method;
179+
if (performMethod.GetParameters().Length > 0)
180+
{
181+
return "The Perform method has one or more parameters." + Environment.NewLine +
182+
"It should have no parameters in a V2 script." + Environment.NewLine +
183+
"To access map data, access ScriptDependencies.Map.";
184+
}
185+
}
186+
}
187+
188+
var properties = classType.GetProperties();
189+
foreach (PropertyInfo property in properties)
190+
{
191+
var setter = property.GetSetMethod();
192+
if (setter == null)
193+
continue;
194+
195+
if (property.Name == "ScriptDependencies")
196+
{
197+
setter.Invoke(instance, [scriptDependencies]);
198+
}
199+
}
200+
201+
if (performMethod == null)
202+
{
203+
return "The script does not declare the Perform method.";
204+
}
205+
206+
return null;
207+
}
208+
209+
private static string ExtractScriptV1(object instance)
210+
{
80211
scriptClassInstance = instance;
81212
Type classType = instance.GetType();
213+
82214
var methods = classType.GetMethods();
83215
foreach (MethodInfo method in methods)
84216
{

src/TSMapEditor/UI/Controls/EditorWindow.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Xna.Framework;
2+
using Microsoft.Xna.Framework.Graphics;
23
using Rampastring.Tools;
34
using Rampastring.XNAUI;
45
using Rampastring.XNAUI.XNAControls;
@@ -57,12 +58,6 @@ public override void Kill()
5758
base.Kill();
5859
}
5960

60-
private void CloseButton_LeftClick(object sender, EventArgs e)
61-
{
62-
Hide();
63-
Closed?.Invoke(this, EventArgs.Empty);
64-
}
65-
6661
protected override void ParseControlINIAttribute(IniFile iniFile, string key, string value)
6762
{
6863
if (key == nameof(CanBeMoved))
@@ -118,7 +113,10 @@ public override void Update(GameTime gameTime)
118113
base.Update(gameTime);
119114

120115
if (Alpha <= 0f && AlphaRate < 0.0f)
116+
{
117+
Closed?.Invoke(this, EventArgs.Empty);
121118
Disable();
119+
}
122120

123121
if (IsDragged)
124122
{

src/TSMapEditor/UI/Controls/INItializableWindow.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public INItializableWindow(WindowManager windowManager) : base(windowManager)
1818
{
1919
}
2020

21-
protected IniFile ConfigIni { get; private set; }
21+
protected IniFile ConfigIni { get; set; }
2222

2323
private bool _initialized = false;
2424

@@ -58,15 +58,19 @@ public override void Initialize()
5858
throw new InvalidOperationException("INItializableWindow cannot be initialized twice.");
5959

6060
var dsc = Path.DirectorySeparatorChar;
61-
string defaultConfigIniPath = Path.Combine(Environment.CurrentDirectory, "Config", "Default", "UI", SubDirectory, Name + ".ini");
62-
string configIniPath = Path.Combine(Environment.CurrentDirectory, "Config", "UI", SubDirectory, Name + ".ini");
63-
64-
if (File.Exists(configIniPath))
65-
ConfigIni = new IniFile(configIniPath);
66-
else if (File.Exists(defaultConfigIniPath))
67-
ConfigIni = new IniFile(defaultConfigIniPath);
68-
else
69-
throw new FileNotFoundException("Config INI not found: " + configIniPath);
61+
62+
if (ConfigIni == null)
63+
{
64+
string defaultConfigIniPath = Path.Combine(Environment.CurrentDirectory, "Config", "Default", "UI", SubDirectory, Name + ".ini");
65+
string configIniPath = Path.Combine(Environment.CurrentDirectory, "Config", "UI", SubDirectory, Name + ".ini");
66+
67+
if (File.Exists(configIniPath))
68+
ConfigIni = new IniFile(configIniPath);
69+
else if (File.Exists(defaultConfigIniPath))
70+
ConfigIni = new IniFile(defaultConfigIniPath);
71+
else
72+
throw new FileNotFoundException("Config INI not found: " + configIniPath);
73+
}
7074

7175
Parser.Instance.SetPrimaryControl(this);
7276
ReadINIForControl(this);

src/TSMapEditor/UI/Windows/RunScriptWindow.cs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,21 @@
33
using Rampastring.XNAUI.XNAControls;
44
using System;
55
using System.IO;
6-
using TSMapEditor.Models;
76
using TSMapEditor.Scripts;
87
using TSMapEditor.UI.Controls;
98

109
namespace TSMapEditor.UI.Windows
1110
{
1211
public class RunScriptWindow : INItializableWindow
1312
{
14-
public RunScriptWindow(WindowManager windowManager, Map map) : base(windowManager)
13+
public RunScriptWindow(WindowManager windowManager, ScriptDependencies scriptDependencies) : base(windowManager)
1514
{
16-
this.map = map;
15+
this.scriptDependencies = scriptDependencies;
1716
}
1817

1918
public event EventHandler ScriptRun;
2019

21-
private readonly Map map;
20+
private readonly ScriptDependencies scriptDependencies;
2221

2322
private EditorListBox lbScriptFiles;
2423

@@ -34,6 +33,14 @@ public override void Initialize()
3433
}
3534

3635
private void BtnRunScript_LeftClick(object sender, EventArgs e)
36+
{
37+
// Run script on next game loop frame so that in case the script displays
38+
// UI, the UI will be shown on top of our window despite that the user
39+
// clicked on our window this frame
40+
AddCallback(RunScript_Callback);
41+
}
42+
43+
private void RunScript_Callback()
3744
{
3845
if (lbScriptFiles.SelectedItem == null)
3946
return;
@@ -49,36 +56,48 @@ private void BtnRunScript_LeftClick(object sender, EventArgs e)
4956

5057
scriptPath = filePath;
5158

52-
(string error, string confirmation) = ScriptRunner.GetDescriptionFromScript(map, filePath);
59+
string error = ScriptRunner.CompileScript(scriptDependencies, filePath);
5360

5461
if (error != null)
5562
{
56-
Logger.Log("Compilation error when attempting to run fetch script description: " + error);
63+
Logger.Log("Compilation error when attempting to run script: " + error);
5764
EditorMessageBox.Show(WindowManager, "Error",
5865
"Compiling the script failed! Check its syntax, or contact its author for support." + Environment.NewLine + Environment.NewLine +
5966
"Returned error was: " + error, MessageBoxButtons.OK);
6067
return;
6168
}
6269

63-
if (confirmation == null)
70+
if (ScriptRunner.ActiveScriptAPIVersion == 1)
6471
{
65-
EditorMessageBox.Show(WindowManager, "Error", "The script provides no description!", MessageBoxButtons.OK);
66-
return;
67-
}
72+
string confirmation = ScriptRunner.GetDescriptionFromScriptV1();
73+
74+
confirmation = Renderer.FixText(confirmation, Constants.UIDefaultFont, Width).Text;
6875

69-
confirmation = Renderer.FixText(confirmation, Constants.UIDefaultFont, Width).Text;
76+
var messageBox = EditorMessageBox.Show(WindowManager, "Are you sure?",
77+
confirmation, MessageBoxButtons.YesNo);
78+
messageBox.YesClickedAction = (_) => ApplyCode();
7079

71-
var messageBox = EditorMessageBox.Show(WindowManager, "Are you sure?",
72-
confirmation, MessageBoxButtons.YesNo);
73-
messageBox.YesClickedAction = (_) => ApplyCode();
80+
}
81+
else if (ScriptRunner.ActiveScriptAPIVersion == 2)
82+
{
83+
error = ScriptRunner.RunScriptV2();
84+
85+
if (error != null)
86+
EditorMessageBox.Show(WindowManager, "Error running script", error, MessageBoxButtons.OK);
87+
}
88+
else
89+
{
90+
EditorMessageBox.Show(WindowManager, "Unsupported Scripting API Version",
91+
"Script uses an unsupported scripting API version: " + ScriptRunner.ActiveScriptAPIVersion, MessageBoxButtons.OK);
92+
}
7493
}
7594

7695
private void ApplyCode()
7796
{
7897
if (scriptPath == null)
7998
throw new InvalidOperationException("Pending script path is null!");
8099

81-
string result = ScriptRunner.RunScript(map, scriptPath);
100+
string result = ScriptRunner.RunScriptV1(scriptDependencies.Map, scriptPath);
82101
result = Renderer.FixText(result, Constants.UIDefaultFont, Width).Text;
83102

84103
EditorMessageBox.Show(WindowManager, "Result", result, MessageBoxButtons.OK);

0 commit comments

Comments
 (0)