using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEliteDangerous.Client;
namespace XFEEliteDangerous.Core;
/// Detached native save business and the fixed host's bounded commit boundary.
public static class NativeSaveModuleAdapter
{
public static NativeSaveModuleReply Evaluate(NativeSaveModuleRequest request)
{
try
{
if(request.Protocol!=1||request.NativeUserId==0||request.CommanderId.Length is 0 or >128
||request.Revision<0||request.SaveJson.Length is 0 or >4*1024*1024||request.Body.Length>1024*1024)
throw new InvalidDataException("Invalid detached native save request.");
var save=JsonSerializer.Deserialize(request.SaveJson,Wire.Json)
??throw new InvalidDataException("Missing detached native save.");
var account=new AccountState{NativeUserId=request.NativeUserId,NativeSave=save,
Commander=new(){Id=request.CommanderId,Revision=request.Revision}};
account.Commander=save.CreateInventory(account,request.Revision);
var result=NativeSavegameUpload.Handle(account,request.Selected,request.Body);
return new(1,result.Status,result.Payload,JsonSerializer.Serialize(account.NativeSave!.ExportCurrent(account),Wire.Json));
}
catch(NativeDataException error)
{return new(1,error.Status,JsonSerializer.Serialize(new{error=error.Message},Wire.Json),null,
error.GetType().FullName,error.Message,error.ToString());}
}
internal static NativeSaveModuleRequest Request(AccountState account,bool selected,byte[] body)
{
if(!selected)throw new NativeDataException(409,"Select a private commander before saving.");
var save=account.NativeSave??throw new NativeDataException(409,"Native save is not initialized.");
return new(1,account.NativeUserId,account.Commander.Id,account.Commander.Revision,selected,
JsonSerializer.Serialize(save.ExportCurrent(account),Wire.Json),body);
}
internal static NativeDataReply Commit(AccountState account,NativeSaveModuleReply reply)
{
if(reply.Protocol!=1||reply.Status is <200 or >599||reply.Payload is null||reply.Payload.Length>65536)
throw new InvalidDataException("Invalid native save module reply contract.");
if(reply.Status>=400)
{
if(reply.SaveJson is not null)throw new InvalidDataException("A rejected save returned a candidate.");
return new(reply.Status,reply.Payload);
}
if(reply.Status!=200||reply.ErrorType is not null||reply.SaveJson is null||reply.SaveJson.Length is 0 or >4*1024*1024)
throw new InvalidDataException("Missing successful native save candidate.");
var before=account.NativeSave!.ExportCurrent(account);
var candidate=JsonSerializer.Deserialize(reply.SaveJson,Wire.Json)
??throw new InvalidDataException("Missing native save candidate.");
if(!JsonNode.DeepEquals(Protected(before),Protected(candidate)))
throw new InvalidDataException("The native save module changed protected inventory or world fields.");
var oldJson=JsonSerializer.Serialize(before,Wire.Json);
var newJson=JsonSerializer.Serialize(candidate,Wire.Json);
if(oldJson==newJson)return new(reply.Status,reply.Payload);
if(account.Commander.Revision==long.MaxValue)throw new NativeDataException(409,"Native inventory revision exhausted.");
var inventory=candidate.CreateInventory(account,account.Commander.Revision+1);
var temporary=new AccountState{NativeUserId=account.NativeUserId,NativeSave=candidate,Commander=inventory};
candidate.ValidateCurrent(temporary);
// StateStore's enclosing transaction persists this pair atomically.
account.NativeSave=candidate;account.Commander=inventory;
return new(reply.Status,reply.Payload);
}
private static JsonObject Protected(NativeCommanderSave save)
{
var root=JsonSerializer.SerializeToNode(save,Wire.Json)!.AsObject();
root.Remove("uploadState");
var profile=root["profile"]!.AsObject();
foreach(var name in new[]{"vehicleDistanceTravelled","startupState","atrLevel","markedWanted","gender"})profile.Remove(name);
var snapshot=root["snapshot"]!.AsObject();snapshot.Remove("location");
foreach(var item in snapshot["vessels"]!.AsArray())
{
var ship=item!.AsObject();
foreach(var name in new[]{"cockpitBreached","oxygenRemainingMilliseconds","radarRangeSelection","weaponMode",
"health","location","lastLocation","firegroups","capacitors"})ship.Remove(name);
var fuel=ship["fuel"]!.AsObject();fuel.Remove("level");fuel.Remove("reserveLevel");
foreach(var list in new[]{"modules","fixedModules"})
foreach(var slot in ship[list]!.AsArray())
if(slot!["module"] is JsonObject module)
foreach(var name in new[]{"uid","healthMillion","on","priority","boost","damageOverride","ammo"})module.Remove(name);
}
return root;
}
}