mirror of
https://github.com/AssetRipper/AssetRipper.git
synced 2025-12-11 20:15:29 +01:00
parent
2c32c98013
commit
297106d0ea
@ -1,4 +1,6 @@
|
||||
namespace AssetRipper.Assets.Generics
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AssetRipper.Assets.Generics
|
||||
{
|
||||
public sealed class AssetList<T> : AccessListBase<T>
|
||||
where T : notnull, new()
|
||||
@ -9,12 +11,12 @@
|
||||
|
||||
public AssetList()
|
||||
{
|
||||
items = Array.Empty<T>();
|
||||
items = [];
|
||||
}
|
||||
|
||||
public AssetList(int capacity)
|
||||
{
|
||||
items = capacity == 0 ? Array.Empty<T>() : new T[capacity];
|
||||
items = capacity == 0 ? [] : new T[capacity];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -26,10 +28,7 @@
|
||||
get => items.Length;
|
||||
set
|
||||
{
|
||||
if (value < count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, count);
|
||||
|
||||
if (value != items.Length)
|
||||
{
|
||||
@ -44,7 +43,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
items = Array.Empty<T>();
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,10 +153,7 @@
|
||||
/// <inheritdoc/>
|
||||
public override void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
if (array == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(array));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
|
||||
if (arrayIndex < 0 || arrayIndex > array.Length - count)
|
||||
{
|
||||
@ -172,6 +168,25 @@
|
||||
new ReadOnlySpan<T>(items, 0, count).CopyTo(destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a span for this list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <typeparamref name="T"/> must be blittable.
|
||||
/// </remarks>
|
||||
/// <returns>A span for the underlying array, with length equal to <see cref="Count"/>.</returns>
|
||||
public Span<T> GetSpan()
|
||||
{
|
||||
if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
{
|
||||
return new(items, 0, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Type must be blittable.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int IndexOf(T item) => Array.IndexOf(items, item, 0, count);
|
||||
|
||||
|
||||
@ -49,5 +49,22 @@
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(_this)),
|
||||
};
|
||||
}
|
||||
|
||||
//When extension types come in C# 13, this will be more convenient to use.
|
||||
public static bool TryGetFromExtension(string extension, out ImageExportFormat format)
|
||||
{
|
||||
format = extension switch
|
||||
{
|
||||
"bmp" => ImageExportFormat.Bmp,
|
||||
"exr" => ImageExportFormat.Exr,
|
||||
"hdr" => ImageExportFormat.Hdr,
|
||||
"jpeg" => ImageExportFormat.Jpeg,
|
||||
"jpg" => ImageExportFormat.Jpeg,
|
||||
"png" => ImageExportFormat.Png,
|
||||
"tga" => ImageExportFormat.Tga,
|
||||
_ => (ImageExportFormat)(-1),
|
||||
};
|
||||
return format >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
214
Source/AssetRipper.GUI.Web/Pages/Assets/AssetAPI.cs
Normal file
214
Source/AssetRipper.GUI.Web/Pages/Assets/AssetAPI.cs
Normal file
@ -0,0 +1,214 @@
|
||||
using AssetRipper.Assets;
|
||||
using AssetRipper.Export.UnityProjects.Configuration;
|
||||
using AssetRipper.Export.UnityProjects.Terrains;
|
||||
using AssetRipper.Export.UnityProjects.Textures;
|
||||
using AssetRipper.GUI.Web.Paths;
|
||||
using AssetRipper.Import.AssetCreation;
|
||||
using AssetRipper.Processing.Textures;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_156;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_213;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_28;
|
||||
using AssetRipper.SourceGenerated.Extensions;
|
||||
using AssetRipper.Web.Extensions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using DirectBitmap = AssetRipper.Export.UnityProjects.Utils.DirectBitmap<AssetRipper.TextureDecoder.Rgb.Formats.ColorBGRA32, byte>;
|
||||
|
||||
namespace AssetRipper.GUI.Web.Pages.Assets;
|
||||
|
||||
internal static class AssetAPI
|
||||
{
|
||||
public const string Extension = "Extension";
|
||||
public const string Path = "Path";
|
||||
|
||||
#region Image
|
||||
public static Task GetImageData(HttpContext context)
|
||||
{
|
||||
context.Response.DisableCaching();
|
||||
if (!TryGetAssetFromQuery(context, out IUnityObjectBase? asset, out Task? failureTask))
|
||||
{
|
||||
return failureTask;
|
||||
}
|
||||
|
||||
if (TryGetImageExtensionFromQuery(context, out string? extension, out ImageExportFormat format))
|
||||
{
|
||||
MemoryStream stream = new();
|
||||
GetImageBitmap(asset).Save(stream, format);
|
||||
return Results.Bytes(stream.ToArray(), $"image/{extension}").ExecuteAsync(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.Bytes(GetRawImageData(asset), "application/octet-stream").ExecuteAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasImageData(IUnityObjectBase asset) => asset switch
|
||||
{
|
||||
ITexture2D texture => texture.CheckAssetIntegrity(),
|
||||
SpriteInformationObject spriteInformationObject => spriteInformationObject.Texture.CheckAssetIntegrity(),
|
||||
ISprite sprite => sprite.TryGetTexture()?.CheckAssetIntegrity() ?? false,
|
||||
ITerrainData terrainData => terrainData.Heightmap.Heights.Count > 0,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static DirectBitmap GetImageBitmap(IUnityObjectBase asset)
|
||||
{
|
||||
return asset switch
|
||||
{
|
||||
ITexture2D texture => TextureToBitmap(texture),
|
||||
SpriteInformationObject spriteInformationObject => TextureToBitmap(spriteInformationObject.Texture),
|
||||
ISprite sprite => SpriteToBitmap(sprite),
|
||||
ITerrainData terrainData => TerrainHeatmapExporter.GetBitmap(terrainData),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
static DirectBitmap TextureToBitmap(ITexture2D texture)
|
||||
{
|
||||
return TextureConverter.TryConvertToBitmap(texture, out DirectBitmap bitmap) ? bitmap : default;
|
||||
}
|
||||
|
||||
static DirectBitmap SpriteToBitmap(ISprite sprite)
|
||||
{
|
||||
return sprite.TryGetTexture() is { } spriteTexture ? TextureToBitmap(spriteTexture) : default;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] GetRawImageData(IUnityObjectBase asset)
|
||||
{
|
||||
return asset switch
|
||||
{
|
||||
ITexture2D texture => texture.GetImageData(),
|
||||
SpriteInformationObject spriteInformationObject => spriteInformationObject.Texture.GetImageData(),
|
||||
ISprite sprite => sprite.TryGetTexture()?.GetImageData() ?? [],
|
||||
ITerrainData terrainData => MemoryMarshal.AsBytes(terrainData.Heightmap.Heights.GetSpan()).ToArray(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetImageExtensionFromQuery(HttpContext context, [NotNullWhen(true)] out string? extension, out ImageExportFormat format)
|
||||
{
|
||||
if (context.Request.Query.TryGetValue(Extension, out extension))
|
||||
{
|
||||
return ImageExportFormatExtensions.TryGetFromExtension(extension, out format);
|
||||
}
|
||||
else
|
||||
{
|
||||
format = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Audio
|
||||
public static Task GetAudioData(HttpContext context)
|
||||
{
|
||||
//Accept Path and Extension in the query.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public static bool HasAudioData(IUnityObjectBase asset)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Model
|
||||
public static Task GetModelData(HttpContext context)
|
||||
{
|
||||
//Only accept Path in the query.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public static bool HasModelData(IUnityObjectBase asset)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Font
|
||||
public static Task GetFontData(HttpContext context)
|
||||
{
|
||||
//Only accept Path in the query.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public static bool HasFontData(IUnityObjectBase asset)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Json
|
||||
public static Task GetJson(HttpContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Yaml
|
||||
public static Task GetYaml(HttpContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Text
|
||||
public static Task GetText(HttpContext context)
|
||||
{
|
||||
//Only accept Path in the query. It sensibly determines the file extension.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public static bool HasText(IUnityObjectBase asset)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Binary Data
|
||||
public static Task GetBinaryData(HttpContext context)
|
||||
{
|
||||
//Only for RawDataObject. This should not call any of the IUnityAssetBase Write methods.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public static bool HasBinaryData(IUnityObjectBase asset)
|
||||
{
|
||||
return asset is RawDataObject { RawData.Length: > 0 };
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static bool TryGetAssetFromQuery(HttpContext context, [NotNullWhen(true)] out IUnityObjectBase? asset, [NotNullWhen(false)] out Task? failureTask)
|
||||
{
|
||||
if (!context.Request.Query.TryGetValue(Path, out string? json) || string.IsNullOrEmpty(json))
|
||||
{
|
||||
asset = null;
|
||||
failureTask = context.Response.NotFound("The path must be included in the request.");
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetPath path;
|
||||
try
|
||||
{
|
||||
path = AssetPath.FromJson(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
asset = null;
|
||||
failureTask = context.Response.NotFound(ex.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GameFileLoader.IsLoaded)
|
||||
{
|
||||
asset = null;
|
||||
failureTask = context.Response.NotFound("No files loaded.");
|
||||
return false;
|
||||
}
|
||||
else if (!GameFileLoader.GameBundle.TryGetAsset(path, out asset))
|
||||
{
|
||||
failureTask = context.Response.NotFound($"Asset could not be resolved: {path}");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
failureTask = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,64 +1,34 @@
|
||||
using AssetRipper.Assets;
|
||||
using AssetRipper.Export.UnityProjects.Terrains;
|
||||
using AssetRipper.Export.UnityProjects.Textures;
|
||||
using AssetRipper.Export.UnityProjects.Utils;
|
||||
using AssetRipper.Processing.Textures;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_156;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_213;
|
||||
using AssetRipper.SourceGenerated.Classes.ClassID_28;
|
||||
using AssetRipper.SourceGenerated.Extensions;
|
||||
using AssetRipper.TextureDecoder.Rgb.Formats;
|
||||
using DirectBitmap = AssetRipper.Export.UnityProjects.Utils.DirectBitmap<AssetRipper.TextureDecoder.Rgb.Formats.ColorBGRA32, byte>;
|
||||
using AssetRipper.GUI.Web.Paths;
|
||||
|
||||
namespace AssetRipper.GUI.Web.Pages.Assets;
|
||||
|
||||
internal sealed class ImageTab : HtmlTab
|
||||
{
|
||||
private readonly DirectBitmap<ColorBGRA32, byte> bitmap;
|
||||
|
||||
public override string DisplayName => Localization.AssetTabImage;
|
||||
|
||||
public override string HtmlName => "image";
|
||||
|
||||
public override bool Enabled => bitmap != default;
|
||||
public override bool Enabled => AssetAPI.HasImageData(Asset);
|
||||
|
||||
public IUnityObjectBase Asset { get; }
|
||||
|
||||
public ImageTab(IUnityObjectBase asset)
|
||||
{
|
||||
bitmap = GetImageBitmap(asset);
|
||||
Asset = asset;
|
||||
}
|
||||
|
||||
public override void Write(TextWriter writer)
|
||||
{
|
||||
MemoryStream stream = new();
|
||||
bitmap.SaveAsPng(stream);
|
||||
string sourcePath = $"data:image/png;base64,{stream.ToArray().ToBase64String()}";
|
||||
string sourcePath = $"/Assets/Image?{AssetAPI.Path}={Asset.GetPath().ToJson().ToUrl()}&{AssetAPI.Extension}=png";
|
||||
|
||||
// Click on image to save
|
||||
using (new A(writer).WithHref(sourcePath).WithDownload("extracted_image").End())
|
||||
{
|
||||
new Img(writer).WithSrc(sourcePath).WithStyle("object-fit:contain; width:100%; height:100%").Close();
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectBitmap GetImageBitmap(IUnityObjectBase asset)
|
||||
{
|
||||
return asset switch
|
||||
{
|
||||
ITexture2D texture => TextureToBitmap(texture),
|
||||
SpriteInformationObject spriteInformationObject => TextureToBitmap(spriteInformationObject.Texture),
|
||||
ISprite sprite => SpriteToBitmap(sprite),
|
||||
ITerrainData terrainData => TerrainHeatmapExporter.GetBitmap(terrainData),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
static DirectBitmap TextureToBitmap(ITexture2D texture)
|
||||
{
|
||||
return TextureConverter.TryConvertToBitmap(texture, out DirectBitmap bitmap) ? bitmap : default;
|
||||
}
|
||||
|
||||
static DirectBitmap SpriteToBitmap(ISprite sprite)
|
||||
{
|
||||
return sprite.TryGetTexture() is { } spriteTexture ? TextureToBitmap(spriteTexture) : default;
|
||||
}
|
||||
// Todo: add a button beneath the image to download its raw data
|
||||
// https://github.com/AssetRipper/AssetRipper/issues/1298
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,4 +6,6 @@ internal static class StringExtensions
|
||||
{
|
||||
[return: NotNullIfNotNull(nameof(value))]
|
||||
public static string? ToHtml(this string? value) => WebUtility.HtmlEncode(value);
|
||||
[return: NotNullIfNotNull(nameof(value))]
|
||||
public static string? ToUrl(this string? value) => WebUtility.UrlEncode(value);
|
||||
}
|
||||
|
||||
@ -145,6 +145,16 @@ public static class WebApplicationLauncher
|
||||
app.MapPost("/Resources/View", Pages.Resources.ViewPage.HandlePostRequest);
|
||||
app.MapPost("/Scenes/View", Pages.Scenes.ViewPage.HandlePostRequest);
|
||||
|
||||
//Asset GET API
|
||||
app.MapGet("/Assets/Image", Pages.Assets.AssetAPI.GetImageData);
|
||||
app.MapGet("/Assets/Audio", Pages.Assets.AssetAPI.GetAudioData);
|
||||
app.MapGet("/Assets/Model", Pages.Assets.AssetAPI.GetModelData);
|
||||
app.MapGet("/Assets/Font", Pages.Assets.AssetAPI.GetFontData);
|
||||
app.MapGet("/Assets/Json", Pages.Assets.AssetAPI.GetJson);
|
||||
app.MapGet("/Assets/Yaml", Pages.Assets.AssetAPI.GetYaml);
|
||||
app.MapGet("/Assets/Text", Pages.Assets.AssetAPI.GetText);
|
||||
app.MapGet("/Assets/Binary", Pages.Assets.AssetAPI.GetBinaryData);
|
||||
|
||||
app.MapPost("/Localization", (context) =>
|
||||
{
|
||||
context.Response.DisableCaching();
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace AssetRipper.Web.Extensions;
|
||||
|
||||
public static class QueryCollectionExtensions
|
||||
{
|
||||
public static bool TryGetValue(this IQueryCollection query, string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (query.TryGetValue(key, out StringValues values))
|
||||
{
|
||||
value = values.ToString();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user