I got "Unknown Hard Error" when I tried to run an editor script

Godot Version

4.7.2 Mono

Question

First time trying to run an EditorScript. I got the error box in the screen shot. As soon as I click the “Ok” button, Godot terminates immediately. What could be the reason for this?

And the script file simply transforms some PNG files in a directory. Gemini wrote it for me. I am sharing it below.

using Embodiment.Types;
using Godot;
using System.Collections.Generic;

namespace Embodiment.EditorScripts
{
	[Tool]
	public partial class PatternFileConverter : EditorScript
	{
		public override void _Run()
		{
			string rootDirectoryPath = "res://Game Data/Patterns";
			
			// 1. Build the Reverse Lookup Dictionary (Natural Color -> Material ID)
			Dictionary<Color, byte> colorToMaterialId = new Dictionary<Color, byte>();
			
			for (int i = 0; i < MaterialPresets.MAX_MATERIALS; i++)
			{
				var mat = MaterialPresets.materialsUnmanaged[i];
				if (mat.id != 0 || i == 0) // Skip empty slots, but keep Air (ID 0)
				{
					byte r = (byte) ( mat.color			& 0xFF);
					byte g = (byte) ((mat.color >> 8)	& 0xFF);
					byte b = (byte) ((mat.color >> 16)	& 0xFF);
					byte a = (byte) ((mat.color >> 24)	& 0xFF);
					
					Color godotColor = new Color(r / 255f, g / 255f, b / 255f, a / 255f);
					colorToMaterialId[godotColor] = mat.id;
				}
			}
			
			// 2. Scan recursively to find all _Colored.png files
			List<string> filesToProcess = new List<string>();
			CollectColoredFiles(rootDirectoryPath, filesToProcess);
			
			// 3. Process every collected file
			foreach (string file in filesToProcess)
			{
				ProcessImage(file, colorToMaterialId);
			}
			
			GD.Print($"Batch conversion complete! Processed {filesToProcess.Count} files.");
		}
		
		// Recursive method to dig through Cxxx and Byyy directories
		private void CollectColoredFiles(string currentPath, List<string> foundFiles)
		{
			using var dir = DirAccess.Open(currentPath);
			if (dir == null) return;
			
			// Find files in the current folder
			foreach (string file in dir.GetFiles())
			{
				if (file.EndsWith("_Colored.png"))
				{
					foundFiles.Add($"{currentPath}/{file}");
				}
			}
			
			// Find sub-folders and crawl them
			foreach (string subDir in dir.GetDirectories())
			{
				CollectColoredFiles($"{currentPath}/{subDir}", foundFiles);
			}
		}
		
		private void ProcessImage(string inputPath, Dictionary<Color, byte> lookup)
		{
			Image img = Image.LoadFromFile(inputPath);
			if (img.GetFormat() != Image.Format.Rgba8)
			{
				img.Convert(Image.Format.Rgba8);
			}
			
			int width = img.GetWidth();
			int height = img.GetHeight();
			
			Image dataImage = Image.CreateEmpty(width, height, false, Image.Format.Rgba8);

			for (int y = 0; y < height; y++)
			{
				for (int x = 0; x < width; x++)
				{
					Color pixelColor = img.GetPixel(x, y);
					
					if (lookup.TryGetValue(pixelColor, out byte materialId))
					{
						dataImage.SetPixel(x, y, new Color(0, materialId / 255f, 0, 1f));
					}
					else
					{
						dataImage.SetPixel(x, y, new Color(0, 0, 0, 1f));
					}
				}
			}
			
			string outputPath = inputPath.Replace("_Colored.png", ".png");
			dataImage.SavePng(outputPath);
			GD.Print($"Converted: {outputPath}");
		}
	}
}

As a workaround, I disabled inheriting EditorScript, and run the same code at the game start to do the file conversion. The code itself is not crashing and doing whatever it is supposed to.

What is MaterialPresets? Could you show us the code for that and also give us the images you are trying to do operations on? It’s impossible to tell because as far as I can tell, that error message is not even included in the Godot source code, or I was unable to find it via a full text search.

The MaterialPresets class:

using System;
using System.Runtime.CompilerServices;
using static Embodiment.Types.Material;
using Category = Embodiment.Types.Enums.MaterialCategory;

namespace Embodiment.Types
{
	public static class MaterialPresets
	{
		private const int VS = 0;	// Viscosity value for static materials. It is ignored since they won't move.
		private const int SL = 0;
		public const int MAX_MATERIALS = 256;
		private static ByteTypeButCantTakeSameValueTwice id = new ByteTypeButCantTakeSameValueTwice(0);
		public static readonly Material[] materials;//	= new Material[MAX_MATERIALS];
		public static readonly Material_Unmanaged[] materialsUnmanaged;	// An copy of `materials` without managed types like `string`.
		
		public static readonly Material AIR;
		public static readonly Material SAND;
		public static readonly Material WOOD_WEAK;
		public static readonly Material SOIL_WEAK;
		public static readonly Material ROCK_WEAK;
		public static readonly Material ICE_SOLID;
		public static readonly Material IRON_RUSTY;
		
		public static readonly Material SOIL;
		
		
		static MaterialPresets()
		{
			materials			= new Material[MAX_MATERIALS];
			materialsUnmanaged	= new Material_Unmanaged[MAX_MATERIALS];
			
			//			PRESET									   ID	NAME			TYPE				IS STATIC?	DENSITY		VISCOSITY		R		G		B		A
						AIR			= materials[id]	= new Material(id,	"Air",			Category.Gas,		false,		0,			0.0f,			0,		0,		0,		0,		StrengthPresets.NA);
			id = 0x40;	SAND		= materials[id]	= new Material(id,	"Sand",			Category.Powdered,	false,		100,		0.2f,			216,	216,	0,		255,	StrengthPresets.NA);
			id = 0x60;	WOOD_WEAK	= materials[id]	= new Material(id,	"Wood",			Category.Solid,		true,		SL,			VS,				128,	90,		30,		255,	StrengthPresets.WOOD_WEAK);
			id = 0x70;	SOIL_WEAK	= materials[id]	= new Material(id,	"Soil",			Category.Solid,		true,		SL,			VS,				60,		30,		30,		255,	StrengthPresets.SOIL_WEAK);
			id = 0x71;	SOIL		= materials[id]	= new Material(id,	"Soil",			Category.Solid,		true,		SL,			VS,				40,		10,		40,		255,	StrengthPresets.NA);
			id = 0x80;	ROCK_WEAK	= materials[id]	= new Material(id,	"Rock",			Category.Solid,		true,		SL,			VS,				128,	128,	128,	255,	StrengthPresets.ROCK_WEAK);
			id = 0x90;	ICE_SOLID	= materials[id]	= new Material(id,	"Ice",			Category.Solid,		true,		SL,			VS,				190,	226,	222,	200,	StrengthPresets.ICE_SOLID);
			id = 0xA0;	IRON_RUSTY	= materials[id]	= new Material(id,	"Rusty Iron",	Category.Solid,		true,		SL,			VS,				64,		30,		40,		255,	StrengthPresets.IRON_RUSTY);
			
			InitializeUnmanagedCopy();
		}
		
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static UInt32 GetColor(in byte materialId)
		{
			return materials[materialId].color;
		}
		
		private static void InitializeUnmanagedCopy()
		{
			for (int i=0; i<MAX_MATERIALS; i++)
			{
				materialsUnmanaged[i]	= new Material_Unmanaged(	materials[i].id,			//byte				id,
																								//in string			name,
																	materials[i].category,		//MaterialCategory	category,
																	materials[i].isStatic,		//bool				isStatic),
																	materials[i].density,		//int				density,
																	materials[i].viscosity,		//float				viscosity,
																	materials[i].color);		//UInt32			color,
																								//in Durability		durability);
			}
		}
	}
}

A colored image (The RGBA values in the PNG are the material colors in the MaterialPresets):

A resulting image (the RGBA values in the colored version transform into material IDs in the green channel):

By the way, I updated the script a little. This is the final one which runs perfectly fine:

using Embodiment.Types;
using Godot;
using System.Collections.Generic;
using System.IO;

namespace Embodiment.EditorScripts
{
	public class PatternFileConverter
	{
		public void RunConversion_ColoredToMaterialId()
		{
			string rootDirectoryPath = ProjectSettings.GlobalizePath("res://Game Data/Patterns");

			if (!Directory.Exists(rootDirectoryPath))
			{
				GD.PrintErr($"PatternFileConverter: Directory not found at OS path: {rootDirectoryPath}");
				return;
			}
			
			Dictionary<uint, byte> colorToMaterialId = new Dictionary<uint, byte>();
			for (int i = 0; i < MaterialPresets.MAX_MATERIALS; i++)
			{
				var mat = MaterialPresets.materialsUnmanaged[i];
				if (mat.id != 0 || i == 0) // Skip empty slots, but keep Air (ID 0)
				{
					colorToMaterialId[mat.color] = mat.id;
				}
			}
			
			string[] filesToProcess = Directory.GetFiles(rootDirectoryPath, "*_Colored.png", SearchOption.AllDirectories);
			
			foreach (string file in filesToProcess)
			{
				ProcessImage_ToMaterialId(file, colorToMaterialId);
			}
			
			GD.Print($"Colored -> Material ID complete! Processed {filesToProcess.Length} files.");
		}
		
		private void ProcessImage_ToMaterialId(string inputPath, Dictionary<uint, byte> lookup)
		{
			using Image img = Image.LoadFromFile(inputPath);
			if (img.GetFormat() != Image.Format.Rgba8) img.Convert(Image.Format.Rgba8);
			
			int width = img.GetWidth();
			int height = img.GetHeight();
			
			byte[] sourceData = img.GetData();
			byte[] targetData;
			
			string outputPath = inputPath.Replace("_Colored.png", ".png");
			bool fileExists = File.Exists(outputPath);
			
			if (fileExists)
			{
				// Load the existing raw data file to preserve the WIMB entities in the R and B channels
				using Image existingImg = Image.LoadFromFile(outputPath);
				if (existingImg.GetFormat() != Image.Format.Rgba8) existingImg.Convert(Image.Format.Rgba8);
				targetData = existingImg.GetData();
			}
			else
			{
				// Create a fresh array if the file doesn't exist yet
				targetData = new byte[sourceData.Length];
			}
			
			for (int i = 0; i < sourceData.Length; i += 4)
			{
				byte r = sourceData[i];
				byte g = sourceData[i + 1];
				byte b = sourceData[i + 2];
				byte a = sourceData[i + 3];
				
				uint pixelColorPacked = (uint)((a << 24) | (b << 16) | (g << 8) | (r << 0));
				byte materialId = lookup.TryGetValue(pixelColorPacked, out byte id) ? id : (byte)0;
				
				if (fileExists)
				{
					// ONLY overwrite the Green channel. Leave R, B, and A completely untouched.
					targetData[i + 1] = materialId;
				}
				else
				{
					targetData[i]     = 0;          // R (Empty WIMB slot)
					targetData[i + 1] = materialId; // G (Material ID)
					targetData[i + 2] = 0;          // B (Empty WIMB metadata)
					targetData[i + 3] = 255;        // A (Fully Opaque)
				}
			}
			
			using Image dataImage = Image.CreateFromData(width, height, false, Image.Format.Rgba8, targetData);
			dataImage.SavePng(outputPath);
		}

		public void RunConversion_MaterialIdToColored()
		{
			string rootDirectoryPath = ProjectSettings.GlobalizePath("res://Game Data/Patterns");

			if (!Directory.Exists(rootDirectoryPath))
			{
				GD.PrintErr($"PatternFileConverter: Directory not found at OS path: {rootDirectoryPath}");
				return;
			}
			
			// 1. Build Reverse Dictionary: Material ID (Green Channel) -> Packed Natural Color (UInt32)
			Dictionary<byte, uint> materialIdToColor = new Dictionary<byte, uint>();
			for (int i = 0; i < MaterialPresets.MAX_MATERIALS; i++)
			{
				var mat = MaterialPresets.materialsUnmanaged[i];
				if (mat.id != 0 || i == 0)
				{
					materialIdToColor[mat.id] = mat.color;
				}
			}
			
			// 2. Fetch all PNGs, but filter out the ones already marked "_Colored.png"
			string[] allPngs = Directory.GetFiles(rootDirectoryPath, "*.png", SearchOption.AllDirectories);
			List<string> filesToProcess = new List<string>();
			foreach(string file in allPngs)
			{
				if (!file.EndsWith("_Colored.png"))
				{
					filesToProcess.Add(file);
				}
			}
			
			// 3. Process every collected data file
			foreach (string file in filesToProcess)
			{
				ProcessImage_ToColored(file, materialIdToColor);
			}
			
			GD.Print($"Material ID -> Colored complete! Processed {filesToProcess.Count} files.");
		}
		
		private void ProcessImage_ToColored(string inputPath, Dictionary<byte, uint> lookup)
		{
			using Image img = Image.LoadFromFile(inputPath);
			if (img.GetFormat() != Image.Format.Rgba8) img.Convert(Image.Format.Rgba8);
			
			int width = img.GetWidth();
			int height = img.GetHeight();
			
			byte[] sourceData = img.GetData();
			byte[] targetData = new byte[sourceData.Length];
			
			for (int i = 0; i < sourceData.Length; i += 4)
			{
				// The Green channel holds the material ID
				byte materialId = sourceData[i + 1];
				
				if (!lookup.TryGetValue(materialId, out uint naturalColor))
				{
					naturalColor = lookup[0]; // Fallback to Air (0) if the ID is missing/corrupted
				}
				
				// Unpack the natural color exactly how MakeColor packed it
				targetData[i]     = (byte)( naturalColor        & 0xFF); // R
				targetData[i + 1] = (byte)((naturalColor >> 8)  & 0xFF); // G
				targetData[i + 2] = (byte)((naturalColor >> 16) & 0xFF); // B
				targetData[i + 3] = (byte)((naturalColor >> 24) & 0xFF); // A
			}
			
			using Image dataImage = Image.CreateFromData(width, height, false, Image.Format.Rgba8, targetData);
			
			string outputPath = inputPath.Replace(".png", "_Colored.png");
			dataImage.SavePng(outputPath);
		}
	}
}