-
Notifications
You must be signed in to change notification settings - Fork 125
Debugging Your Scripts
If there's one thing that is certain about being a programmer, it is that you will make mistakes. Bugs are just part of the job. Usually in IDEs like Visual Studio you can enable a "Debug Mode" though, which will give you file names and line numbers, and even breakpoints which enables you to stop at selected points and step-by-step the code to see what went wrong.
None of which is available for Space Engineers ingame scripts.
The reasoning for this is simple: Debuggable scripts run a lot slower than optimized scripts. Because of this the compiled scripts in SE contains no debug information at all.
So... We're still going to have bugs, how can we trace them?
Let's start simple, with the Echo
. This is a method belonging to the scripts base class, MyGridProgram
(it's a delegate property actually, but I'll get back to that). This method can be used to output text to the programmable block's detail area. For instance, this:
public void Main()
{
Echo("Hello World");
}
results in this:
By placing outputs like this at strategic locations in your code, you can figure out how far it goes before your error occurs - and in that way narrow down your search range.
However to provide full coverage you'd need Echo all over the place. It'd be nice to be able to narrow down a little more first.
A stack trace is a description showing you the route a call has made at the error point. There is actually a way to get at least a limited stack trace out of Space Engineers, this should help you narrow down at least what code member was running when the error occurred. You can do this by adding a try/catch
to the main callback points of your script - those being Program()
, Save()
, and most importantly Main()
.
public void ATestMethod()
{
// Simulate an exception happening by throwing one ourselves
throw new Exception("Boom!");
}
public void Main()
{
try
{
// Call the test method.
ATestMethod();
}
catch (Exception e)
{
// Dump the exception content to the
Echo("An error occurred during script execution.");
Echo($"Exception: {e}");
// Rethrow the exception to make the programmable block halt execution properly
throw;
}
}
As mentioned in the comment, the throw
statement rethrows the same exception to force the PB to shut down as it normally would. I recommend doing this because it's highly likely your script is in an undefined state at this point, even if you've tried to take errors into account.
When running this script, this is what will be shown in the programmable block's Details view:
The last two lines, the Caught exception...
line, that's how the programmable block itself displays errors. Not really helpful. The rest of the output comes from our changes in the script, and this provides more information. It tells us what exception occurred (System.Exception
), the specific error message (Boom!
) and then the stack trace I've been talking about. The first line at Program.ATestMethod()
tells you which method the error happened in. And it tells you that this particular ATestMethod
call was run from Program.Main()
. Now we know which method(s) we need to decorate with Echo's, we don't need to fill our application up completely.
Unfortunately even this method is inexact. The compiler optimizer is rather clever. Sometimes it takes some of your smaller methods and bakes them into larger methods, because it deems that to be faster. This means that you'll not see that particular method in the stack trace, because as far as the compiled code goes, it doesn't exist. This will give you a relatively decent idea where to start looking though.
Unfortunately there's a slight caveat to using Echo
a lot. In solo play there's little to no issue, but in multiplayer the text needs to be synchronized from the server where the script is running and to your client. This takes time. If a lot of scripts are echoing a lot of text every frame, it's going to have an impact. For this reason you shouldn't Echo everything always. This is where the thing I mentioned earlier comes into play: Echo
is actually a property containing a delegate, specifically one of the type Action<string>
. The method called can be replaced with whatever you want. For instance, if you have lots of diagnostic echoes in your script and want to keep it around for later, you can do this:
public Program()
{
Echo = () => {};
}
The part () => {} is called a lambda. This one is essentially representing a method without an argument which does notihg. This will effectively make Echo do nothing. Voila, any performance hit by your echoes are now gone - but you can switch it back on simply by removing the line above!
Another neat trick you can do because of this, is to reroute your echo.
IMyTextPanel _logOutput;
public Program()
{
// Replace the Echo
Echo = EchoToLCD;
// Fetch a log text panel
_logOutput = GridTerminalSystem.GetBlockWithName("Log LCD") as IMyTextBlock;
}
public void EchoToLCD(string text)
{
// Append the text and a newline to the logging LCD
// A nice little C# trick here:
// - The ?. after _logOutput means "call only if _logOutput is not null".
_logOutput?.WritePublicText($"{text}\n", true);
}
Putting a $ in front of the string literal makes it a string interpolation.
Do you have questions, comments, suggestions for improvements? Is there something I can do better? Did I make a mistake? Please add an issue here, and prefix your issue title with Wiki. Thank you, your help will be very appreciated!