Update hooks and server to get past asset and master db download, initial user data download

This commit is contained in:
BillyCool
2026-02-24 20:22:08 +11:00
parent 05c22e32d0
commit 7f273cb17c
4 changed files with 262 additions and 20 deletions

View File

@@ -5,6 +5,12 @@ namespace MariesWonderland;
public static class Program
{
// https://archive.org/download/nier_reincarnation_assets/resource_dump_android.7z
private const string AssetDatabaseBasePath = @"path\to\resource_dump_android\revisions";
// https://archive.org/download/nierreincarnation/Global/master_data/bin/
private const string MasterDatabaseBasePath = @"path\to\master_data\bin";
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
@@ -31,12 +37,136 @@ public static class Program
// Add HTTP middleware
app.MapGet("/", () => "Marie's Wonderland is open for business :marie:");
app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>"); // Expects the version wrapped in delimiters like "###123###".
app.MapGet("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}");
app.MapPost("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}");
app.MapPut("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}");
app.MapDelete("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}");
app.MapPatch("/{**catchAll}", (string catchAll) => $"You requested: {catchAll}");
// ToS. Expects the version wrapped in delimiters like "###123###".
app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>");
// Asset Database
app.MapGet("/v2/pub/a/301/v/300116832/list/{revision}", async (string revision) =>
await File.ReadAllBytesAsync(Path.Combine(AssetDatabaseBasePath, revision, "list.bin")));
// Master Database
app.MapMethods("/assets/release/{masterVersion}/database.bin.e", ["GET", "HEAD"], async (HttpContext ctx, string masterVersion) =>
{
var filePath = Path.Combine(MasterDatabaseBasePath, $"{masterVersion}.bin.e");
var fileInfo = new FileInfo(filePath);
long totalLength = fileInfo.Length;
// Advertise range support
ctx.Response.Headers.AcceptRanges = "bytes";
// Simple ETag using last write ticks & length (optional but useful for clients)
ctx.Response.Headers.ETag = $"\"{fileInfo.LastWriteTimeUtc.Ticks:x}-{totalLength:x}\"";
// Handle HEAD quickly (send headers only)
bool isHead = string.Equals(ctx.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
// Parse Range header if present
if (ctx.Request.Headers.TryGetValue("Range", out var rangeHeader))
{
// Expect single range of form: bytes=start-end
var raw = rangeHeader.ToString();
if (!raw.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
{
// Malformed range; respond with 416
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
var rangePart = raw["bytes=".Length..].Trim();
var parts = rangePart.Split('-', 2);
if (parts.Length != 2)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
bool startParsed = long.TryParse(parts[0], out long start);
bool endParsed = long.TryParse(parts[1], out long end);
if (!startParsed && endParsed)
{
// suffix range: last 'end' bytes
long suffixLength = end;
if (suffixLength <= 0)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
start = Math.Max(0, totalLength - suffixLength);
end = totalLength - 1;
}
else if (startParsed && !endParsed)
{
// range from start to end of file
end = totalLength - 1;
}
else if (!startParsed && !endParsed)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
// Validate
if (start < 0 || end < start || start >= totalLength)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
long length = end - start + 1;
ctx.Response.StatusCode = StatusCodes.Status206PartialContent;
ctx.Response.Headers.ContentRange = $"bytes {start}-{end}/{totalLength}";
ctx.Response.ContentType = "application/octet-stream";
ctx.Response.ContentLength = length;
if (isHead)
{
return;
}
// Stream the requested range
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
fs.Seek(start, SeekOrigin.Begin);
var buffer = new byte[64 * 1024];
long remaining = length;
while (remaining > 0)
{
int toRead = (int)Math.Min(buffer.Length, remaining);
int read = await fs.ReadAsync(buffer, 0, toRead);
if (read == 0) break;
await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, read));
remaining -= read;
}
return;
}
// No Range header: return full file
ctx.Response.StatusCode = StatusCodes.Status200OK;
ctx.Response.ContentType = "application/octet-stream";
ctx.Response.ContentLength = totalLength;
if (isHead)
{
return;
}
// Stream the whole file
await using var fullFs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await fullFs.CopyToAsync(ctx.Response.Body);
});
// Catch all
app.MapMethods("/{**catchAll}", ["GET", "POST", "PUT", "DELETE", "PATCH",], (string catchAll) => $"You requested: {catchAll}");
app.Run();
}

View File

@@ -6,13 +6,38 @@ namespace MariesWonderland.Services;
public class DataService : Art.Framework.ApiNetwork.Grpc.Api.Data.DataService.DataServiceBase
{
private const string LatestMasterDataVersion = "20240404193219";
private const string UserDataBasePath = @"path\to\user\data";
public override Task<MasterDataGetLatestVersionResponse> GetLatestMasterDataVersion(Empty request, ServerCallContext context)
{
return Task.FromResult(new MasterDataGetLatestVersionResponse());
return Task.FromResult(new MasterDataGetLatestVersionResponse
{
LatestMasterDataVersion = LatestMasterDataVersion
});
}
public override Task<UserDataGetNameResponseV2> GetUserDataNameV2(Empty request, ServerCallContext context)
{
UserDataGetNameResponseV2 response = new();
TableNameList tableNameList = new();
tableNameList.TableName.AddRange(Directory.EnumerateFiles(UserDataBasePath, "*.json").Select(x => Path.GetFileNameWithoutExtension(x)));
response.TableNameList.Add(tableNameList);
return Task.FromResult(response);
}
public override Task<UserDataGetResponse> GetUserData(UserDataGetRequest request, ServerCallContext context)
{
return Task.FromResult(new UserDataGetResponse());
UserDataGetResponse response = new();
foreach (var tableName in request.TableName)
{
var filePath = Path.Combine(UserDataBasePath, tableName + ".json");
var jsonContent = File.ReadAllText(filePath);
response.UserDataJson.Add(tableName, jsonContent);
}
return Task.FromResult(response);
}
}

View File

@@ -22,7 +22,7 @@ public class UserService : Art.Framework.ApiNetwork.Grpc.Api.User.UserService.Us
ExpireDatetime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(30)),
UserId = 1234567890123450000,
SessionKey = "1234567890",
Signature = "V2UnbGxQbGF5QWdhaW5Tb21lZGF5TXJNb25zdGVyIQ=="
Signature = request.Signature
});
}