Skip to content

Commit 5aa5f80

Browse files
committed
Add support for project version 4
1 parent a5a25fc commit 5aa5f80

File tree

5 files changed

+205
-79
lines changed

5 files changed

+205
-79
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"version": 1,
3+
"commands": [
4+
{
5+
"yarnName": "test_command",
6+
"parameters": [
7+
{
8+
"name": "param_1",
9+
"type": "string"
10+
}
11+
]
12+
}
13+
],
14+
"functions": []
15+
}

Tests/Projects/Space/Space.yarnproject

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"projectFileVersion": 3,
2+
"projectFileVersion": 4,
33
"sourceFiles": [
44
"**/*.yarn"
55
],

YarnSpinner.Compiler/Project.cs

Lines changed: 150 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,28 @@ public class Project
2727
/// <summary>
2828
/// The current version of <c>.yarnproject</c> file format.
2929
/// </summary>
30-
public const int CurrentProjectFileVersion = YarnSpinnerProjectVersion3;
30+
public const int CurrentProjectFileVersion = YarnSpinnerProjectVersion4;
31+
32+
// There isn't a Yarn Spinner project version 1 representable in
33+
// JSON-based Yarn Spinner projects, because the first version of Yarn
34+
// Spinner stored its Yarn Projects as special .yarn files that had
35+
// extra Unity-specific metadata.
3136

3237
/// <summary>
33-
/// A version number representing Yarn Spinner 2.
38+
/// A version number representing project version 2 (released with Yarn Spinner 2.0)
3439
/// </summary>
3540
public const int YarnSpinnerProjectVersion2 = 2;
3641

3742
/// <summary>
38-
/// A version number representing Yarn Spinner 3.
43+
/// A version number representing project version 3 (released with Yarn Spinner 3.0).
3944
/// </summary>
4045
public const int YarnSpinnerProjectVersion3 = 3;
4146

47+
/// <summary>
48+
/// A version number representing project version 4 (released with Yarn Spinner 3.2.0).
49+
/// </summary>
50+
public const int YarnSpinnerProjectVersion4 = 4;
51+
4252
private static readonly JsonSerializerOptions SerializationOptions = new JsonSerializerOptions
4353
{
4454
AllowTrailingCommas = true,
@@ -131,15 +141,17 @@ public Project(string path, string? workspaceRootPath = null)
131141
public string BaseLanguage { get; set; } = CurrentNeutralCulture.Name;
132142

133143
/// <summary>
134-
/// Gets or sets the path to a JSON file containing command and function
135-
/// definitions that this project references.
144+
/// Gets or sets the patterns for matching paths to JSON files that
145+
/// contain command and function definitions that this project
146+
/// references.
136147
/// </summary>
137148
/// <remarks>
138149
/// Definitions files are used by editing tools to provide type
139150
/// information and other externally-defined data used by the Yarn
140151
/// scripts.
141152
/// </remarks>
142-
public string? Definitions { get; set; }
153+
[JsonConverter(typeof(StringOrListOfStringsConverter))]
154+
public List<string>? Definitions { get; set; }
143155

144156
/// <summary>
145157
/// Gets a value indicating whether this Project is an 'implicit'
@@ -235,7 +247,7 @@ public IEnumerable<string> SourceFiles
235247
/// </summary>
236248
/// <seealso cref="DefinitionsFiles"/>
237249
[JsonIgnore]
238-
[Obsolete("Use " + nameof(DefinitionsFilesPattern))]
250+
[Obsolete("Use " + nameof(Definitions))]
239251
public string? DefinitionsPath
240252
{
241253
get
@@ -253,117 +265,126 @@ public string? DefinitionsPath
253265
}
254266

255267
[JsonIgnore]
256-
private string? DefinitionsFilesPattern
268+
private List<string> DefinitionsFilesPatterns
257269
{
258270
get
259271
{
260272
if (this.Definitions == null)
261273
{
262-
return null;
274+
return new();
263275
}
264-
else if (this.Definitions.IndexOf(WorkspaceRootPlaceholder) != -1)
276+
277+
List<string> result = new();
278+
foreach (var pattern in Definitions)
265279
{
266-
if (this.WorkspaceRootPath != null
267-
&& System.IO.Directory.Exists(WorkspaceRootPath))
280+
281+
if (pattern.IndexOf(WorkspaceRootPlaceholder) != -1)
268282
{
269-
return this.Definitions.Replace(WorkspaceRootPlaceholder, WorkspaceRootPath);
283+
if (this.WorkspaceRootPath != null
284+
&& System.IO.Directory.Exists(WorkspaceRootPath))
285+
{
286+
result.Add(pattern.Replace(WorkspaceRootPlaceholder, WorkspaceRootPath));
287+
}
288+
else
289+
{
290+
// The path contains the placeholder, but we have no
291+
// value to insert it with. Early out here.
292+
continue;
293+
}
270294
}
271-
else
295+
else if (this.SearchDirectoryPath != null)
272296
{
273-
// The path contains the placeholder, but we have no
274-
// value to insert it with. Early out here.
275-
return null;
297+
result.Add(System.IO.Path.Combine(this.SearchDirectoryPath, pattern));
276298
}
299+
277300
}
278-
else if (this.SearchDirectoryPath != null)
279-
{
280-
return System.IO.Path.Combine(this.SearchDirectoryPath, this.Definitions);
281-
}
282-
else
283-
{
284-
return null;
285-
}
301+
return result;
286302
}
287303
}
288304

289305
/// <summary>
290306
/// Gets the absolute paths to the project's Definitions files.
291307
/// </summary>
308+
[JsonIgnore]
292309
public IEnumerable<string> DefinitionsFiles
293310
{
294311
get
295312
{
296-
if (DefinitionsFilesPattern == null)
313+
if (DefinitionsFilesPatterns.Count == 0)
297314
{
298315
return Array.Empty<string>();
299316
}
300317

301318
Matcher m = new Matcher(StringComparison.OrdinalIgnoreCase);
302319

320+
var result = new List<string>();
303321

304-
if (System.IO.Path.IsPathRooted(DefinitionsFilesPattern))
322+
foreach (var DefinitionsFilesPattern in this.DefinitionsFilesPatterns)
305323
{
306-
if (System.IO.File.Exists(DefinitionsFilesPattern))
307-
{
308-
// The path is to an absolute path that exists on disk; return it as-is
309-
return new[] { DefinitionsFilesPattern };
310-
}
311-
else
324+
if (System.IO.Path.IsPathRooted(DefinitionsFilesPattern))
312325
{
313-
// The path is absolute but doesn't exist; it may be a
314-
// pattern. Split the pattern into an absolute path and
315-
// the pattern, and attempt to match from there.
326+
if (System.IO.File.Exists(DefinitionsFilesPattern))
327+
{
328+
// The path is to an absolute path that exists on disk; add it as-is
329+
result.Add(DefinitionsFilesPattern);
330+
}
331+
else
332+
{
333+
// The path is absolute but doesn't exist; it may be a
334+
// pattern. Split the pattern into an absolute path and
335+
// the pattern, and attempt to match from there.
316336

317-
var fullPath = System.IO.Path.GetFullPath(DefinitionsFilesPattern);
318-
var pathRoot = System.IO.Path.GetPathRoot(fullPath);
337+
var fullPath = System.IO.Path.GetFullPath(DefinitionsFilesPattern);
338+
var pathRoot = System.IO.Path.GetPathRoot(fullPath);
319339

320-
var pathRelativeToRoot = fullPath.Substring(pathRoot.Length);
340+
var pathRelativeToRoot = fullPath.Substring(pathRoot.Length);
321341

322-
var allSegments = new Queue<string>(
323-
pathRelativeToRoot
324-
.Split(new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar })
325-
);
342+
var allSegments = new Queue<string>(
343+
pathRelativeToRoot
344+
.Split(new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar })
345+
);
326346

327-
var absoluteSegments = new List<string>();
328-
while (allSegments.Count > 0)
329-
{
330-
var segment = allSegments.Peek();
331-
if (segment.Contains("*"))
332-
{
333-
break;
334-
}
335-
else
347+
var absoluteSegments = new List<string>();
348+
while (allSegments.Count > 0)
336349
{
337-
allSegments.Dequeue();
338-
absoluteSegments.Add(segment);
350+
var segment = allSegments.Peek();
351+
if (segment.Contains("*"))
352+
{
353+
break;
354+
}
355+
else
356+
{
357+
allSegments.Dequeue();
358+
absoluteSegments.Add(segment);
359+
}
339360
}
340-
}
341361

342-
var searchBasePath = pathRoot + System.IO.Path.Combine(absoluteSegments.ToArray());
343-
var searchPattern = System.IO.Path.Combine(allSegments.ToArray());
362+
var searchBasePath = pathRoot + System.IO.Path.Combine(absoluteSegments.ToArray());
363+
var searchPattern = System.IO.Path.Combine(allSegments.ToArray());
344364

345-
m.AddInclude(searchPattern);
346-
return m.GetResultsInFullPath(searchBasePath);
365+
m.AddInclude(searchPattern);
366+
result.AddRange(m.GetResultsInFullPath(searchBasePath));
367+
}
347368
}
348-
}
349-
else
350-
{
351-
// The path is not absolute, so we can use the matcher
352-
// directly to find paths, starting from our
353-
354-
if (SearchDirectoryPath == null)
369+
else
355370
{
356-
// We don't know where to start searching from.
357-
return Array.Empty<string>();
358-
}
371+
// The path is not absolute, so we can use the matcher
372+
// directly to find paths, starting from our
359373

360-
m.AddInclude(DefinitionsFilesPattern);
374+
if (SearchDirectoryPath == null)
375+
{
376+
// We don't know where to start searching from.
377+
continue;
378+
}
379+
380+
m.AddInclude(DefinitionsFilesPattern);
361381

362-
IEnumerable<string> results;
363-
results = m.GetResultsInFullPath(SearchDirectoryPath);
382+
result.AddRange(m.GetResultsInFullPath(SearchDirectoryPath));
364383

365-
return results;
384+
}
366385
}
386+
387+
return result;
367388
}
368389
}
369390

@@ -558,4 +579,60 @@ public class LocalizationInfo
558579
public string? Strings { get; set; }
559580
}
560581
}
582+
583+
584+
// Starting in Yarn Spinner Project version 4, 'Definitions' is permitted to
585+
// be either a single string, or an array of strings. This converter can
586+
// read a string or list of strings (returning an array of strings), and
587+
// writes a list of strings as an array of strings.
588+
class StringOrListOfStringsConverter : JsonConverter<List<string>>
589+
{
590+
public override List<string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
591+
{
592+
switch (reader.TokenType)
593+
{
594+
case JsonTokenType.String:
595+
// Just a single string; return a new list with a single
596+
// element
597+
return new List<string> { reader.GetString() ?? "" };
598+
case JsonTokenType.StartArray:
599+
// An array; read strings out of it. If we encounter
600+
// something that's not a string, that's an error.
601+
{
602+
reader.Read();
603+
var result = new List<string>();
604+
605+
while (reader.TokenType == JsonTokenType.String)
606+
{
607+
result.Add(reader.GetString() ?? "");
608+
reader.Read();
609+
}
610+
if (reader.TokenType == JsonTokenType.EndArray)
611+
{
612+
return result;
613+
}
614+
else
615+
{
616+
throw new JsonException($"Unexpected {reader.TokenType} in list of strings");
617+
}
618+
}
619+
620+
default:
621+
// Neither a string nor a list; error.
622+
throw new JsonException("Expected a string, or a list of strings");
623+
}
624+
}
625+
626+
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options)
627+
{
628+
// Write the list of strings as an array of strings.
629+
writer.WriteStartArray();
630+
foreach (var str in value)
631+
{
632+
writer.WriteStringValue(str);
633+
}
634+
writer.WriteEndArray();
635+
}
636+
}
637+
561638
}

YarnSpinner.Compiler/YarnProject.schema.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
"$ref": "#/definitions/CompilerOptions"
2626
},
2727
"definitions": {
28-
"type": "string"
28+
"type": [
29+
"string",
30+
"array"
31+
],
32+
"items": {
33+
"type": "string"
34+
}
2935
}
3036
},
3137
"required": [
@@ -42,8 +48,8 @@
4248
},
4349
"Localisation": {
4450
"type": "object",
45-
"additionalProperties": true,
51+
"additionalProperties": true,
4652
"title": "Localisation"
4753
}
4854
}
49-
}
55+
}

0 commit comments

Comments
 (0)