@@ -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}
0 commit comments