Skip to content

Commit ed5a8e7

Browse files
committed
potentially horrible first pass of generating gdscript based ysls
1 parent 766b3f2 commit ed5a8e7

1 file changed

Lines changed: 192 additions & 2 deletions

File tree

src/YarnSpinner.Console/Commands/GenerateDefinitionsCommand.cs

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ public static void GenerateDefinitions(DirectoryInfo inputDirectory, DirectoryIn
2222
GenerateYSLSFilesForUnity(inputDirectory, outputDirectory);
2323
break;
2424
case "Godot-gd":
25-
Log.Error("At this stage ysc only supports generating YSLS files for csharp projects");
26-
System.Environment.Exit(1);
25+
GenerateYSLSFilesForGodotGDScript(inputDirectory, outputDirectory);
2726
break;
2827
case "Godot-csharp":
2928
GenerateYSLSFilesForGodot(inputDirectory, outputDirectory);
@@ -204,6 +203,197 @@ private static void GenerateYSLSFilesForGodot(DirectoryInfo inputDirectory, Dire
204203
};
205204
GenerateYSLSFilesForCSharp(inputDirectory, outputDirectory, required, emptyList, emptyList);
206205
}
206+
207+
private static void GenerateYSLSFilesForGodotGDScript(DirectoryInfo inputDirectory, DirectoryInfo outputDirectory)
208+
{
209+
Log.Info($"Scanning GDScript files in {inputDirectory.FullName}");
210+
211+
var gdFiles = inputDirectory.GetFiles("*.gd", SearchOption.AllDirectories);
212+
Log.Info($"Found {gdFiles.Length} .gd files");
213+
214+
var commands = new List<string>();
215+
var functions = new List<string>();
216+
217+
foreach (var file in gdFiles)
218+
{
219+
var lines = File.ReadAllLines(file.FullName);
220+
var relativePath = Path.GetRelativePath(inputDirectory.FullName, file.FullName);
221+
222+
for (int i = 0; i < lines.Length; i++)
223+
{
224+
var line = lines[i].Trim();
225+
226+
string? prefix = null;
227+
bool isFunction = false;
228+
229+
if (line.StartsWith("func _yarn_command_"))
230+
{
231+
prefix = "_yarn_command_";
232+
}
233+
else if (line.StartsWith("func _yarn_function_"))
234+
{
235+
prefix = "_yarn_function_";
236+
isFunction = true;
237+
}
238+
239+
if (prefix == null) continue;
240+
241+
// Handle multi-line signatures by joining lines until we find ')'
242+
var fullLine = line;
243+
while (!fullLine.Contains(')') && i + 1 < lines.Length)
244+
{
245+
i++;
246+
fullLine += " " + lines[i].Trim();
247+
}
248+
249+
var parsed = ParseGDScriptAction(fullLine, prefix, isFunction, relativePath, i);
250+
if (parsed != null)
251+
{
252+
(isFunction ? functions : commands).Add(parsed);
253+
}
254+
}
255+
}
256+
257+
if (commands.Count == 0 && functions.Count == 0)
258+
{
259+
Log.Info(" 😴 No Yarn commands or functions found in GDScript files");
260+
return;
261+
}
262+
263+
var ysls = "{" +
264+
@"""version"":2," +
265+
$@"""commands"":[{string.Join(",", commands)}]," +
266+
$@"""functions"":[{string.Join(",", functions)}]" +
267+
"}";
268+
269+
if (!outputDirectory.Exists)
270+
{
271+
outputDirectory.Create();
272+
}
273+
274+
var outputPath = Path.Combine(outputDirectory.FullName, "GDScript.ysls.json");
275+
File.WriteAllText(outputPath, ysls);
276+
Log.Info($" 😎 Found {commands.Count} commands and {functions.Count} functions");
277+
Log.Info($" 😎 Wrote {outputPath}");
278+
}
279+
280+
private static string? ParseGDScriptAction(string line, string prefix, bool isFunction, string fileName, int lineNumber)
281+
{
282+
var afterFunc = line.Substring("func ".Length);
283+
var parenStart = afterFunc.IndexOf('(');
284+
if (parenStart < 0) return null;
285+
286+
var methodName = afterFunc.Substring(0, parenStart);
287+
var yarnName = methodName.Substring(prefix.Length);
288+
289+
var parenEnd = afterFunc.IndexOf(')');
290+
if (parenEnd < 0) return null;
291+
292+
var paramString = afterFunc.Substring(parenStart + 1, parenEnd - parenStart - 1).Trim();
293+
var parameters = ParseGDScriptParams(paramString);
294+
295+
// Extract return type from -> annotation
296+
string? returnTypeStr = null;
297+
var arrowIndex = afterFunc.IndexOf("->", parenEnd);
298+
if (arrowIndex >= 0)
299+
{
300+
returnTypeStr = afterFunc.Substring(arrowIndex + 2).Trim().TrimEnd(':').Trim();
301+
}
302+
303+
var result = new Dictionary<string, object?>
304+
{
305+
["yarnName"] = yarnName,
306+
["definitionName"] = methodName,
307+
["fileName"] = fileName,
308+
["language"] = "gdscript",
309+
["containsErrors"] = false,
310+
["parameters"] = parameters,
311+
["location"] = new Dictionary<string, object>
312+
{
313+
["start"] = new Dictionary<string, int> { ["line"] = lineNumber, ["character"] = 0 },
314+
["end"] = new Dictionary<string, int> { ["line"] = lineNumber, ["character"] = line.Length }
315+
}
316+
};
317+
318+
if (isFunction)
319+
{
320+
var yarnType = returnTypeStr != null ? MapGDScriptTypeToYarn(returnTypeStr) : "any";
321+
result["return"] = new Dictionary<string, string> { ["type"] = yarnType };
322+
}
323+
else
324+
{
325+
result["async"] = returnTypeStr?.Equals("Signal", StringComparison.OrdinalIgnoreCase) == true;
326+
}
327+
328+
return System.Text.Json.JsonSerializer.Serialize(result);
329+
}
330+
331+
private static List<Dictionary<string, object?>> ParseGDScriptParams(string paramString)
332+
{
333+
var result = new List<Dictionary<string, object?>>();
334+
if (string.IsNullOrWhiteSpace(paramString)) return result;
335+
336+
var parts = paramString.Split(',');
337+
foreach (var part in parts)
338+
{
339+
var trimmed = part.Trim();
340+
if (string.IsNullOrEmpty(trimmed)) continue;
341+
342+
string name;
343+
string type = "any";
344+
string? defaultValue = null;
345+
346+
// Check for default value: param := value or param = value
347+
var eqIndex = trimmed.IndexOf('=');
348+
string beforeDefault = trimmed;
349+
if (eqIndex >= 0)
350+
{
351+
// Handle := (typed default) and = (untyped default)
352+
if (eqIndex > 0 && trimmed[eqIndex - 1] == ':')
353+
{
354+
beforeDefault = trimmed.Substring(0, eqIndex - 1).Trim();
355+
}
356+
else
357+
{
358+
beforeDefault = trimmed.Substring(0, eqIndex).Trim();
359+
}
360+
defaultValue = trimmed.Substring(eqIndex + 1).Trim();
361+
}
362+
363+
// Check for type annotation: param: Type
364+
var colonIndex = beforeDefault.IndexOf(':');
365+
if (colonIndex >= 0)
366+
{
367+
name = beforeDefault.Substring(0, colonIndex).Trim();
368+
var gdType = beforeDefault.Substring(colonIndex + 1).Trim();
369+
type = MapGDScriptTypeToYarn(gdType);
370+
}
371+
else
372+
{
373+
name = beforeDefault;
374+
}
375+
376+
var param = new Dictionary<string, object?> { ["name"] = name, ["type"] = type };
377+
if (defaultValue != null)
378+
{
379+
param["defaultValue"] = defaultValue;
380+
}
381+
result.Add(param);
382+
}
383+
384+
return result;
385+
}
386+
387+
private static string MapGDScriptTypeToYarn(string gdType)
388+
{
389+
return gdType.ToLowerInvariant() switch
390+
{
391+
"string" or "stringname" => "string",
392+
"int" or "float" => "number",
393+
"bool" => "bool",
394+
_ => "any",
395+
};
396+
}
207397
}
208398

209399
public interface ILogger

0 commit comments

Comments
 (0)