Web Page for Failed Files

* Resolves #1588
This commit is contained in:
ds5678 2024-12-09 21:05:38 -08:00
parent e7b6db3545
commit 03646ed0c7
11 changed files with 187 additions and 3 deletions

View File

@ -204,6 +204,7 @@
"sprite_format_texture_description": "Export as an image of the sprite sheet. Can be viewed outside of Unity, but slower to export.",
"sprite_format_yaml": "Yaml",
"sprite_format_yaml_description": "Export as yaml assets which can be viewed in the editor. This is the only mode that ensures a precise recovery of all metadata of sprites.",
"stack_trace": "Stack Trace",
"submesh_count": "Submesh Count",
"success": "Success!",
"swagger_documentation": "Swagger Documentation",

View File

@ -1035,6 +1035,11 @@ partial class Localization
/// </summary>
public static string SpriteFormatYamlDescription => Get("sprite_format_yaml_description");
/// <summary>
/// Stack Trace
/// </summary>
public static string StackTrace => Get("stack_trace");
/// <summary>
/// Submesh Count
/// </summary>

View File

@ -76,10 +76,12 @@ public sealed class ViewPage : DefaultPage
new H2(writer).Close(Localization.FailedFiles);
using (new Ul(writer).End())
{
foreach (FailedFile failedFile in Bundle.FailedFiles)
for (int i = 0; i < Bundle.FailedFiles.Count; i++)
{
// Todo: page for failed files
new Li(writer).Close(failedFile.Name);
using (new Li(writer).End())
{
PathLinking.WriteLink(writer, Path.GetFailedFile(i), Bundle.FailedFiles[i].NameFixed);
}
}
}
}

View File

@ -0,0 +1,88 @@
using AssetRipper.GUI.Web.Paths;
using AssetRipper.IO.Files;
using AssetRipper.Web.Extensions;
using Microsoft.AspNetCore.Http;
namespace AssetRipper.GUI.Web.Pages.FailedFiles;
internal static class FailedFileAPI
{
public static class Urls
{
public const string Base = "/FailedFiles";
public const string View = Base + "/View";
public const string StackTrace = Base + "/StackTrace";
}
private const string Path = "Path";
public static string GetViewUrl(FailedFilePath path) => $"{Urls.View}?{GetPathQuery(path)}";
public static Task GetView(HttpContext context)
{
context.Response.DisableCaching();
if (TryGetFileFromQuery(context, out FailedFile? file, out FailedFilePath path, out Task? failureTask))
{
return new ViewPage() { File = file, Path = path }.WriteToResponse(context.Response);
}
else
{
return failureTask;
}
}
public static string GetStackTraceUrl(FailedFilePath path) => $"{Urls.StackTrace}?{GetPathQuery(path)}";
public static Task GetStackTrace(HttpContext context)
{
context.Response.DisableCaching();
if (TryGetFileFromQuery(context, out FailedFile? file, out _, out Task? failureTask))
{
return Results.Text(file.StackTrace).ExecuteAsync(context);
}
else
{
return failureTask;
}
}
private static string GetPathQuery(FailedFilePath path) => $"{Path}={path.ToJson().ToUrl()}";
private static bool TryGetFileFromQuery(HttpContext context, [NotNullWhen(true)] out FailedFile? file, out FailedFilePath path, [NotNullWhen(false)] out Task? failureTask)
{
if (!context.Request.Query.TryGetValue(Path, out string? json) || string.IsNullOrEmpty(json))
{
file = null;
path = default;
failureTask = context.Response.NotFound("The path must be included in the request.");
return false;
}
try
{
path = FailedFilePath.FromJson(json);
}
catch (Exception ex)
{
file = null;
path = default;
failureTask = context.Response.NotFound(ex.ToString());
return false;
}
if (!GameFileLoader.IsLoaded)
{
file = null;
failureTask = context.Response.NotFound("No files loaded.");
return false;
}
else if (!GameFileLoader.GameBundle.TryGetFailedFile(path, out file))
{
failureTask = context.Response.NotFound($"Resource file could not be resolved: {path}");
return false;
}
else
{
failureTask = null;
return true;
}
}
}

View File

@ -0,0 +1,37 @@
using AssetRipper.Assets.Bundles;
using AssetRipper.GUI.Web.Paths;
using AssetRipper.IO.Files;
namespace AssetRipper.GUI.Web.Pages.FailedFiles;
public sealed class ViewPage : DefaultPage
{
public required FailedFile File { get; init; }
public required FailedFilePath Path { get; init; }
public Bundle? Bundle => GameFileLoader.GameBundle.TryGetBundle(Path.BundlePath);
public override string GetTitle() => File.NameFixed;
public override void WriteInnerContent(TextWriter writer)
{
new H1(writer).Close(GetTitle());
if (Bundle is { } bundle)
{
new H2(writer).Close(Localization.Bundle);
PathLinking.WriteLink(writer, Path.BundlePath, bundle.Name);
}
new H2(writer).Close(Localization.StackTrace);
string url = FailedFileAPI.GetStackTraceUrl(Path);
new Pre(writer).WithClass("bg-dark-subtle rounded-3 p-2").WithDynamicTextContent(url).Close();
using (new Div(writer).WithClass("text-center").End())
{
SaveButton.Write(writer, url, "error.txt");
}
}
}

View File

@ -62,6 +62,11 @@ public readonly record struct BundlePath : IPath<BundlePath>
return new CollectionPath(this, index);
}
public FailedFilePath GetFailedFile(int index)
{
return new FailedFilePath(this, index);
}
public ResourcePath GetResource(int index)
{
return new ResourcePath(this, index);

View File

@ -0,0 +1,17 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AssetRipper.GUI.Web.Paths;
public readonly record struct FailedFilePath([property: JsonPropertyName("B")] BundlePath BundlePath, [property: JsonPropertyName("I")] int Index) : IPath<FailedFilePath>
{
public string ToJson()
{
return JsonSerializer.Serialize(this, PathSerializerContext.Default.FailedFilePath);
}
public static FailedFilePath FromJson(string json)
{
return JsonSerializer.Deserialize(json, PathSerializerContext.Default.FailedFilePath);
}
}

View File

@ -1,6 +1,7 @@
using AssetRipper.Assets;
using AssetRipper.Assets.Bundles;
using AssetRipper.Assets.Collections;
using AssetRipper.IO.Files;
using AssetRipper.IO.Files.ResourceFiles;
namespace AssetRipper.GUI.Web.Paths;
@ -117,6 +118,22 @@ public static class PathExtensions
return resource is not null;
}
public static FailedFile? TryGetFailedFile(this GameBundle gameBundle, FailedFilePath path)
{
Bundle? bundle = gameBundle.TryGetBundle(path.BundlePath);
if (bundle is null || path.Index < 0 || path.Index >= bundle.FailedFiles.Count)
{
return null;
}
return bundle.FailedFiles[path.Index];
}
public static bool TryGetFailedFile(this GameBundle gameBundle, FailedFilePath path, [NotNullWhen(true)] out FailedFile? failedFile)
{
failedFile = gameBundle.TryGetFailedFile(path);
return failedFile is not null;
}
public static IUnityObjectBase? TryGetAsset(this GameBundle gameBundle, AssetPath path)
{
AssetCollection? collection = gameBundle.TryGetCollection(path.CollectionPath);

View File

@ -4,6 +4,7 @@ using AssetRipper.Assets.Collections;
using AssetRipper.GUI.Web.Pages.Assets;
using AssetRipper.GUI.Web.Pages.Bundles;
using AssetRipper.GUI.Web.Pages.Collections;
using AssetRipper.GUI.Web.Pages.FailedFiles;
using AssetRipper.GUI.Web.Pages.Resources;
using AssetRipper.GUI.Web.Pages.Scenes;
using System.Runtime.CompilerServices;
@ -56,6 +57,10 @@ internal static class PathLinking
{
return ResourceAPI.GetViewUrl(Unsafe.As<T, ResourcePath>(ref path));
}
else if (typeof(T) == typeof(FailedFilePath))
{
return FailedFileAPI.GetViewUrl(Unsafe.As<T, FailedFilePath>(ref path));
}
else
{
return "";//Exceptions prevent inlining

View File

@ -7,6 +7,7 @@ namespace AssetRipper.GUI.Web.Paths;
[JsonSerializable(typeof(CollectionPath))]
[JsonSerializable(typeof(ScenePath))]
[JsonSerializable(typeof(ResourcePath))]
[JsonSerializable(typeof(FailedFilePath))]
internal sealed partial class PathSerializerContext : JsonSerializerContext
{
}

View File

@ -3,6 +3,7 @@ using AssetRipper.GUI.Web.Pages;
using AssetRipper.GUI.Web.Pages.Assets;
using AssetRipper.GUI.Web.Pages.Bundles;
using AssetRipper.GUI.Web.Pages.Collections;
using AssetRipper.GUI.Web.Pages.FailedFiles;
using AssetRipper.GUI.Web.Pages.Resources;
using AssetRipper.GUI.Web.Pages.Scenes;
using AssetRipper.GUI.Web.Pages.Settings;
@ -255,6 +256,11 @@ public static class WebApplicationLauncher
.WithSummary("Get the number of elements in the collection.")
.Produces<int>();
//Failed Files
app.MapGet(FailedFileAPI.Urls.View, FailedFileAPI.GetView).ProducesHtmlPage();
app.MapGet(FailedFileAPI.Urls.StackTrace, FailedFileAPI.GetStackTrace)
.Produces<string>();
//Resources
app.MapGet(ResourceAPI.Urls.View, ResourceAPI.GetView).ProducesHtmlPage();
app.MapGet(ResourceAPI.Urls.Data, ResourceAPI.GetData)