SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
3ce1a40 verified
Raw History Blame Contribute Delete
4.04 kB
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Repoverse.Core.Bridge;
using Repoverse.Core.Generation;
using Repoverse.Core.Persistence;
// Local-only service. One Unreal client owns a session; reconnect starts a new stream.
var builder = WebApplication.CreateBuilder(args);
string Required(string name) => builder.Configuration[name] ?? throw new ArgumentException($"--{name} is required");
var manifest = JsonSerializer.Deserialize<WorldManifest>(File.ReadAllText(Required("manifest")), Repoverse.Core.Generation.Json.Options)
?? throw new InvalidDataException("Empty manifest");
var token = Required("token");
if (token.Length < 32) throw new ArgumentException("Use a random token of at least 32 characters.");
var tokenHash = SHA256.HashData(Encoding.UTF8.GetBytes(token));
int port = int.Parse(builder.Configuration["port"] ?? "38473");
if (port is < 1024 or > 65535) throw new ArgumentException("Invalid port");
builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
builder.WebHost.ConfigureKestrel(k => k.Limits.MaxRequestBodySize = 64 * 1024);
builder.Services.ConfigureHttpJsonOptions(o => {
o.SerializerOptions.PropertyNamingPolicy = Repoverse.Core.Generation.Json.Options.PropertyNamingPolicy;
foreach (var c in Repoverse.Core.Generation.Json.Options.Converters) o.SerializerOptions.Converters.Add(c);
});
var app = builder.Build();
var gate = new SemaphoreSlim(1,1);
var session = new WorldSession(manifest);
var savePath = Path.GetFullPath(builder.Configuration["save"] ?? "repoverse-save.json");
var sourceRoot = builder.Configuration["source-root"];
var sourceRepo = builder.Configuration["source-repo"];
var sources = sourceRoot == null || sourceRepo == null ? null : new LocalSourceReader(manifest, sourceRepo, sourceRoot);
app.Use(async (context,next) => {
// No CORS: web pages cannot use this service. Authenticate even read-only requests.
var supplied = SHA256.HashData(Encoding.UTF8.GetBytes(context.Request.Headers["X-Repoverse-Token"].ToString()));
if (context.Request.Headers.ContainsKey("Origin") || !CryptographicOperations.FixedTimeEquals(tokenHash,supplied)) {
context.Response.StatusCode=403; return;
}
await gate.WaitAsync(context.RequestAborted);
try { await next(context); }
catch (Exception e) when (e is ArgumentException or InvalidDataException or JsonException or FormatException or IOException) {
context.Response.StatusCode=400;
await context.Response.WriteAsJsonAsync(new { error=e.Message });
}
finally { gate.Release(); }
});
app.MapPost("/v1/session", () => {
var player = session.Player;
var saved = session.Capture();
session = new WorldSession(manifest); session.Restore(saved);
return new { schemaVersion=1, worldSeed=manifest.WorldSeed, player, destinations=session.Destinations, objects=session.Objects(), builders=session.Builders };
});
app.MapPost("/v1/focus", (FocusRequest r) => {var p=session.Focus(r.X,r.Z);return new {p.Unloaded,p.Chunks,p.Remaining,p.Resident,builders=session.Builders};});
app.MapPost("/v1/build", (BuilderPlan plan) => session.Build(plan));
app.MapPost("/v1/save", (PlayerState player) => {
session.SetPlayer(player);
Directory.CreateDirectory(Path.GetDirectoryName(savePath)!);
SaveGame.Write(savePath,session.Capture());
return new { saved=true };
});
app.MapPost("/v1/load", () => { session.Restore(SaveGame.Read(savePath)); return session.Player; });
app.MapPost("/v1/edit", (EditRequest r) => {session.Edit(r.X,r.Y,r.Z,r.Packed);return new { edited=true };});
app.MapGet("/v1/source", (string repo, string path, int offset = 0) => sources is null
? Results.BadRequest(new { error="Local source inspection is not configured. Use --source-root and --source-repo." })
: Results.Ok(sources.Read(repo,path,offset)));
app.MapGet("/v1/health", () => new { schemaVersion=1, resident=session.Resident });
await app.RunAsync();
record FocusRequest(double X,double Z);
record EditRequest(int X,int Y,int Z,ushort Packed);