@@ -257,15 +257,44 @@ public async Task<bool> ImportPluginAsync(string kxpFilePath)
257257 try
258258 {
259259 var decoder = new FileFormats . CSharp . ExtensionsPackage . Decoder ( kxpFilePath ) ;
260+
261+ // Read structs from KXP binary header (preferred path)
262+ var ( loaderStructHeader , pluginStructHeader ) = decoder . GetLoaderAndPluginInfo ( ) ;
263+
260264 decoder . Decode ( pluginDir ) ;
261265 Log . Information ( $ "Decoded KXP file to: { pluginDir } ") ;
266+
267+ // Parse LoaderStruct from header, fall back to extracted file
268+ LoaderInfo ? loaderInfo = ParseLoaderStructFromJson ( loaderStructHeader ) ;
269+ if ( loaderInfo is null )
270+ {
271+ var loaderStructPath = Path . Combine ( pluginDir , "LoaderStruct.json" ) ;
272+ if ( File . Exists ( loaderStructPath ) )
273+ {
274+ var loaderStructJson = await File . ReadAllTextAsync ( loaderStructPath ) ;
275+ loaderInfo = ParseLoaderStructFromJson ( loaderStructJson ) ;
276+ }
277+ }
278+
279+ // Parse PluginStruct from header, fall back to extracted file
280+ PluginInfo ? pluginInfo = ParsePluginStructFromJson ( pluginStructHeader ) ;
281+ if ( pluginInfo is null )
282+ {
283+ var pluginStructPath = Path . Combine ( pluginDir , "PluginStruct.json" ) ;
284+ if ( File . Exists ( pluginStructPath ) )
285+ {
286+ var pluginStructJson = await File . ReadAllTextAsync ( pluginStructPath ) ;
287+ pluginInfo = ParsePluginStructFromJson ( pluginStructJson ) ;
288+ }
289+ }
290+
291+ return await FinalizeImport ( pluginDir , kxpFilePath , loaderInfo , pluginInfo ) ;
262292 }
263293 catch ( Exception ex )
264294 {
265295 Log . Warning ( ex , "Failed to decode KXP file, trying as direct files..." ) ;
266296
267297 // If KXP decode fails, assume it's a directory with files already extracted
268- // Just copy the source directory contents
269298 var sourceDir = Path . GetDirectoryName ( kxpFilePath ) ;
270299 if ( sourceDir != null && Directory . Exists ( sourceDir ) )
271300 {
@@ -278,140 +307,100 @@ public async Task<bool> ImportPluginAsync(string kxpFilePath)
278307 }
279308 }
280309 }
310+
311+ return await FinalizeImport ( pluginDir , kxpFilePath , null , null ) ;
281312 }
313+ }
314+ catch ( Exception ex )
315+ {
316+ Log . Error ( ex , $ "In { location } : Error importing plugin { kxpFilePath } : { ex . Message } ") ;
317+ return false ;
318+ }
319+ }
282320
283- // Look for LoaderStruct.json and PluginStruct.json in the extracted files
284- var loaderStructPath = Path . Combine ( pluginDir , "LoaderStruct.json" ) ;
285- var pluginStructPath = Path . Combine ( pluginDir , "PluginStruct.json" ) ;
321+ private static LoaderInfo ? ParseLoaderStructFromJson ( string loaderStructJson )
322+ {
323+ try
324+ {
325+ return System . Text . Json . JsonSerializer . Deserialize < LoaderInfo > ( loaderStructJson ,
326+ new System . Text . Json . JsonSerializerOptions { PropertyNameCaseInsensitive = true } ) ;
327+ }
328+ catch ( Exception ex )
329+ {
330+ Log . Warning ( ex , "Failed to parse LoaderStruct JSON" ) ;
331+ return null ;
332+ }
333+ }
286334
287- PluginInfo ? pluginInfo = null ;
288- LoaderInfo ? loaderInfo = null ;
335+ private static PluginInfo ? ParsePluginStructFromJson ( string pluginStructJson )
336+ {
337+ try
338+ {
339+ return System . Text . Json . JsonSerializer . Deserialize < PluginInfo > ( pluginStructJson ,
340+ new System . Text . Json . JsonSerializerOptions { PropertyNameCaseInsensitive = true , IncludeFields = true } ) ;
341+ }
342+ catch ( Exception ex )
343+ {
344+ Log . Warning ( ex , "Failed to parse PluginStruct JSON" ) ;
345+ return null ;
346+ }
347+ }
289348
290- // Parse LoaderStruct.json if exists
291- if ( File . Exists ( loaderStructPath ) )
292- {
293- try
294- {
295- var loaderStructJson = await File . ReadAllTextAsync ( loaderStructPath ) ;
296- var loaderStruct = System . Text . Json . JsonSerializer . Deserialize < JsonElement > ( loaderStructJson ) ;
349+ private async Task < bool > FinalizeImport (
350+ string pluginDir , string kxpFilePath ,
351+ LoaderInfo ? loaderInfo , PluginInfo ? pluginInfo )
352+ {
353+ const string location = $ "{ nameof ( PluginsManager ) } .{ nameof ( FinalizeImport ) } ";
297354
298- loaderInfo = new LoaderInfo
299- {
300- LoaderName = loaderStruct . TryGetProperty ( "LoaderName" , out var name ) ? name . GetString ( ) ?? "Unknown" : "Unknown" ,
301- LoaderVersion = loaderStruct . TryGetProperty ( "LoaderVersion" , out var version ) ? version . GetString ( ) ?? "1.0.0" : "1.0.0" ,
302- LoaderLanguage = loaderStruct . TryGetProperty ( "LoaderLanguage" , out var lang ) ? lang . GetString ( ) ?? "Unknown" : "Unknown" ,
303- LoaderFramework = loaderStruct . TryGetProperty ( "LoaderFramework" , out var fw ) ? fw . GetString ( ) ?? "Unknown" : "Unknown" ,
304- SelfLoad = loaderStruct . TryGetProperty ( "SelfLoad" , out var selfLoad ) && selfLoad . GetBoolean ( ) ,
305- Tags = new Dictionary < string , string > ( )
306- } ;
355+ try
356+ {
357+ var pluginFileName = Path . GetFileNameWithoutExtension ( kxpFilePath ) ;
307358
308- // Copy loader struct to LoaderInfo.json (different format)
309- var loaderInfoJson = System . Text . Json . JsonSerializer . Serialize ( loaderInfo , new JsonSerializerOptions { WriteIndented = true } ) ;
310- await File . WriteAllTextAsync ( Path . Combine ( pluginDir , "LoaderInfo.json" ) , loaderInfoJson ) ;
359+ if ( pluginInfo == null )
360+ {
361+ // If no plugin info from KXP, look for LoaderStruct/PluginStruct files
362+ var loaderStructPath = Path . Combine ( pluginDir , "LoaderStruct.json" ) ;
363+ var pluginStructPath = Path . Combine ( pluginDir , "PluginStruct.json" ) ;
311364
312- Log . Information ( $ "Parsed LoaderStruct: { loaderInfo . LoaderName } v{ loaderInfo . LoaderVersion } ") ;
313- }
314- catch ( Exception ex )
365+ if ( File . Exists ( loaderStructPath ) )
315366 {
316- Log . Warning ( ex , "Failed to parse LoaderStruct.json" ) ;
367+ var json = await File . ReadAllTextAsync ( loaderStructPath ) ;
368+ loaderInfo ??= ParseLoaderStructFromJson ( json ) ;
317369 }
318- }
319370
320- // Parse PluginStruct.json if exists (or look for embedded plugin info)
321- if ( File . Exists ( pluginStructPath ) )
322- {
323- try
371+ if ( File . Exists ( pluginStructPath ) )
324372 {
325- var pluginStructJson = await File . ReadAllTextAsync ( pluginStructPath ) ;
326- var pluginStruct = System . Text . Json . JsonSerializer . Deserialize < JsonElement > ( pluginStructJson ) ;
373+ var json = await File . ReadAllTextAsync ( pluginStructPath ) ;
374+ pluginInfo = ParsePluginStructFromJson ( json ) ;
375+ }
327376
377+ if ( pluginInfo == null )
378+ {
328379 pluginInfo = new PluginInfo
329380 {
330- Name = pluginStruct . TryGetProperty ( "Name" , out var name ) ? name . GetString ( ) ?? "Unknown" : "Unknown" ,
331- Version = pluginStruct . TryGetProperty ( "Version" , out var ver ) ? ver . GetString ( ) ?? "1.0.0" : "1.0.0" ,
332- DisplayName = new Dictionary < string , string > ( ) ,
333- SimpleDescription = new Dictionary < string , string > ( ) ,
334- ComplexDescription = new Dictionary < string , string > ( ) ,
381+ Name = pluginFileName ,
382+ Version = "1.0.0" ,
383+ PublisherName = "Unknown" ,
384+ AuthorName = "Unknown" ,
385+ DisplayName = new Dictionary < string , string > { { "en-us" , pluginFileName } } ,
386+ SimpleDescription = new Dictionary < string , string > { { "en-us" , "Imported plugin" } } ,
387+ ComplexDescription = new Dictionary < string , string > { { "en-us" , "Imported plugin" } } ,
335388 TotalDescriptionInMarkdown = new Dictionary < string , string > ( ) ,
336389 Tags = new Dictionary < string , string > ( ) ,
337390 Functions = new List < Function > ( )
338391 } ;
339-
340- // Parse DisplayName
341- if ( pluginStruct . TryGetProperty ( "DisplayName" , out var displayName ) )
342- {
343- foreach ( var prop in displayName . EnumerateObject ( ) )
344- {
345- pluginInfo . DisplayName [ prop . Name ] = prop . Value . GetString ( ) ?? "" ;
346- }
347- }
348-
349- // Parse SimpleDescription
350- if ( pluginStruct . TryGetProperty ( "SimpleDescription" , out var simpleDesc ) )
351- {
352- foreach ( var prop in simpleDesc . EnumerateObject ( ) )
353- {
354- pluginInfo . SimpleDescription [ prop . Name ] = prop . Value . GetString ( ) ?? "" ;
355- }
356- }
357-
358- // Parse ComplexDescription
359- if ( pluginStruct . TryGetProperty ( "ComplexDescription" , out var complexDesc ) )
360- {
361- foreach ( var prop in complexDesc . EnumerateObject ( ) )
362- {
363- pluginInfo . ComplexDescription [ prop . Name ] = prop . Value . GetString ( ) ?? "" ;
364- }
365- }
366-
367- // Parse other fields
368- pluginInfo . AuthorName = pluginStruct . TryGetProperty ( "AuthorName" , out var author ) ? author . GetString ( ) ?? "Unknown" : "Unknown" ;
369- pluginInfo . AuthorLink = pluginStruct . TryGetProperty ( "AuthorLink" , out var authorLink ) ? authorLink . GetString ( ) ?? "" : "" ;
370- pluginInfo . PublisherName = pluginStruct . TryGetProperty ( "PublisherName" , out var publisher ) ? publisher . GetString ( ) ?? "Unknown" : "Unknown" ;
371- pluginInfo . PublisherLink = pluginStruct . TryGetProperty ( "PublisherLink" , out var publisherLink ) ? publisherLink . GetString ( ) ?? "" : "" ;
372- pluginInfo . IconInBase64 = pluginStruct . TryGetProperty ( "IconInBase64" , out var icon ) ? icon . GetString ( ) ?? "" : "" ;
373- pluginInfo . IsMarketVersion = pluginStruct . TryGetProperty ( "IsMarketVersion" , out var marketVer ) && marketVer . GetBoolean ( ) ;
374- pluginInfo . RootStartupFileName = pluginStruct . TryGetProperty ( "RootStartupFileName" , out var rootFile ) ? rootFile . GetString ( ) ?? "" : "" ;
375-
376- if ( pluginStruct . TryGetProperty ( "PublishDate" , out var publishDate ) )
377- {
378- if ( DateTime . TryParse ( publishDate . GetString ( ) , out var pd ) )
379- pluginInfo . PublishDate = pd ;
380- }
381-
382- if ( pluginStruct . TryGetProperty ( "LastUpdateDate" , out var lastUpdate ) )
383- {
384- if ( DateTime . TryParse ( lastUpdate . GetString ( ) , out var lud ) )
385- pluginInfo . LastUpdateDate = lud ;
386- }
387-
388- Log . Information ( $ "Parsed PluginStruct: { pluginInfo . Name } v{ pluginInfo . Version } ") ;
389- }
390- catch ( Exception ex )
391- {
392- Log . Warning ( ex , "Failed to parse PluginStruct.json" ) ;
393392 }
394393 }
395394
396- // If no plugin info from KXP, create basic info from filename
397- if ( pluginInfo = = null )
395+ // Write LoaderInfo.json
396+ if ( loaderInfo ! = null )
398397 {
399- pluginInfo = new PluginInfo
400- {
401- Name = pluginFileName ,
402- Version = "1.0.0" ,
403- PublisherName = "Unknown" ,
404- AuthorName = "Unknown" ,
405- DisplayName = new Dictionary < string , string > { { "en-us" , pluginFileName } } ,
406- SimpleDescription = new Dictionary < string , string > { { "en-us" , "Imported plugin" } } ,
407- ComplexDescription = new Dictionary < string , string > { { "en-us" , "Imported plugin" } } ,
408- TotalDescriptionInMarkdown = new Dictionary < string , string > ( ) ,
409- Tags = new Dictionary < string , string > ( ) ,
410- Functions = new List < Function > ( )
411- } ;
398+ var loaderInfoJson = System . Text . Json . JsonSerializer . Serialize ( loaderInfo ,
399+ new System . Text . Json . JsonSerializerOptions { WriteIndented = true } ) ;
400+ await File . WriteAllTextAsync ( Path . Combine ( pluginDir , "LoaderInfo.json" ) , loaderInfoJson ) ;
412401 }
413402
414- // Validate RootStartupFileName is specified and exists
403+ // Validate RootStartupFileName
415404 if ( string . IsNullOrEmpty ( pluginInfo . RootStartupFileName ) )
416405 {
417406 Log . Error ( $ "Plugin import failed: RootStartupFileName is not specified in plugin { pluginInfo . Name } . Please ensure the plugin package includes this field.") ;
@@ -422,13 +411,13 @@ public async Task<bool> ImportPluginAsync(string kxpFilePath)
422411 if ( ! File . Exists ( pluginFilePath ) )
423412 {
424413 Log . Error ( $ "Plugin import failed: RootStartupFileName '{ pluginInfo . RootStartupFileName } ' points to a non-existent file in plugin { pluginInfo . Name } . File not found at: { pluginFilePath } ") ;
425- // Clean up the created directory
426414 try { Directory . Delete ( pluginDir , true ) ; } catch { }
427415 return false ;
428416 }
429417
430- // Create PluginInfo.json
431- var pluginInfoJson = System . Text . Json . JsonSerializer . Serialize ( pluginInfo , new JsonSerializerOptions { WriteIndented = true } ) ;
418+ // Write PluginInfo.json
419+ var pluginInfoJson = System . Text . Json . JsonSerializer . Serialize ( pluginInfo ,
420+ new System . Text . Json . JsonSerializerOptions { WriteIndented = true } ) ;
432421 await File . WriteAllTextAsync ( Path . Combine ( pluginDir , "PluginInfo.json" ) , pluginInfoJson ) ;
433422
434423 // Create installation record
@@ -445,7 +434,6 @@ public async Task<bool> ImportPluginAsync(string kxpFilePath)
445434
446435 Log . Information ( $ "Imported plugin: { pluginInfo . Name } (v{ pluginInfo . Version } ) to { pluginDir } ") ;
447436
448- // Raise plugin status changed event
449437 PluginStatusChanged ? . Invoke ( this , new PluginStatusChangedEventArgs
450438 {
451439 PluginId = installation . Id ,
@@ -458,7 +446,7 @@ public async Task<bool> ImportPluginAsync(string kxpFilePath)
458446 }
459447 catch ( Exception ex )
460448 {
461- Log . Error ( ex , $ "In { location } : Error importing plugin { kxpFilePath } : { ex . Message } ") ;
449+ Log . Error ( ex , $ "In { location } : { ex . Message } ") ;
462450 return false ;
463451 }
464452 }
0 commit comments