using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using static XFEEDToolBox.Plugin.JsonWire;
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private struct RearmTarget { public fixed byte slot[48]; public int clip, hopper; }
[InlineArray(64)] private struct Rearm64 { private RearmTarget item; }
private struct CommerceReadRequest
{
public ulong generation, entity, epoch, expires;
public nuint life, request;
public uint market, state, error, kind, commodity, price;
public int delta, before, after, totalBefore;
public ulong queued, observed;
public bool mutationSent, clientApplied;
public bool refreshClient,profileApplied,profileValidated;
public bool formal;
public int capacityBound;
public fixed byte commodityName[128];
public ulong serverCredits;
public int serverCargo;
public nuint maintenance;
public bool borrowed;
public ulong creditsBefore;
public float fuelTarget, reserveTarget;
public bool serviceReplyVerified;
public ulong creditsResult;
public float fuelResult, reserveResult, fuelCapacityResult, reserveCapacityResult;
public uint nextRefuelCost, nextRepairCost, integrityWear;
public fixed byte servicePayload[4096];
public uint servicePayloadLength, rearmCount;
public Rearm64 rearm;
public CommerceReadRequest()
{
this = default;
// The original response structure initializes these output-only fields to zero.
nextRepairCost = 0; integrityWear = 0;
}
}
private static readonly object commerceReadLock = new();
private static CommerceReadRequest commerceRead;
private static ulong commerceReadGeneration;
private static string commerceReadBody = "";
private static readonly byte[] commerceReadBuffer = new byte[1024 * 1024];
private static bool commerceReconciliationRequired;
private static readonly Dictionary<string, ulong> commerceOperationKeys = new(StringComparer.Ordinal);
private struct CommerceShipInventory { public uint count, capacity; public ulong credits; public bool verified; }
private static CommerceShipInventory ReadCommerceShipInventory(in Snapshot frame)
{
CommerceShipInventory result = default;
try
{
if (!frame.valid || !frame.healthValid || GetTickCount64() - frame.tick > 200) return result;
if (GameRoute(frame, out var pilot) == 0) return result;
var cargo = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read<uint>(image + 0x5F2F25C));
var wallet = RegisteredComponent(Pointer(pilot + 0xC8) - 0x30, Read<uint>(image + 0x5F59D90));
if (cargo == 0 || Pointer(cargo) != image + 0x530B858 || wallet == 0 || Pointer(wallet) != image + 0x5295868) return result;
result.count = Read<uint>(cargo + 0x28C); result.capacity = Read<uint>(cargo + 0x288);
result.credits = Read<ulong>(wallet + 0x98) ^ Read<ulong>(wallet + 0x90);
result.verified = result.capacity <= int.MaxValue && result.count <= result.capacity && result.credits < 100000000000000UL;
}
catch { result.verified = false; }
return result;
}
private static bool CommerceDocked(in Snapshot frame, uint market)
{
return ReadCommerceDocking(LocalDocking(frame), market, out _, out _);
}
private static bool ReadCommerceDocking(nuint dock,uint market,out uint state,out ulong actualMarket)
{
state=0;actualMarket=0;if(dock==0)return false;
state=Read<uint>(dock+0x4C0);var station=Pointer(dock+0x4E0);
if(station==0||Read<uint>(station+0xC0)!=8)return false;
actualMarket=Read<ulong>(station+0xB0);
return market is not(0 or uint.MaxValue)&&(state is 5 or 7)&&actualMarket==market;
}
private static JsonObject CommerceDockedStatus(JsonElement request,in Snapshot frame)
{
var market=request.Required("marketId").GetUInt32();
if(market is 0 or uint.MaxValue)throw new InvalidOperationException("Identified docked market required");
var now=GetTickCount64();var fresh=frame.valid&&now>=frame.tick&&now-frame.tick<=200;
var healthy=fresh&&frame.healthValid&&!frame.healthEmergency&&double.IsFinite(frame.hullFraction)&&frame.hullFraction>0;
uint state=0;ulong actualMarket=0;var docked=false;var service=false;string? issue=null;
if(fresh)try{docked=ReadCommerceDocking(LocalDocking(frame),market,out state,out actualMarket);if(docked&&healthy)service=OriginalServiceBinding(frame,0,market);}catch(Exception error){issue=error.GetType().Name;}
var ready=inGame&&Enabled()&&healthy&&docked&&issue is null;
var stage=!inGame?"notInGame":!Enabled()?"disabled":!fresh?"telemetry":!healthy?"health":issue is not null?"observation":!docked?"docking":!service?"services":"ready";
return Obj(("ready",ready),("serviceReady",ready&&service),("stage",stage),("fresh",fresh),("healthy",healthy),
("entityId",frame.entity.ToString(System.Globalization.CultureInfo.InvariantCulture)),("marketId",market),("actualMarketId",actualMarket),("rawDockingState",state),("issue",issue),("mutationsSent",0));
}
private static uint CommerceExceptionCode(Exception error) => unchecked((uint)error.HResult);
private static bool PollCommerceReadImpl(in Snapshot frame, ref CommerceReadRequest work, byte* body, nuint capacity, out nuint size)
{
size = 0;
try
{
if (!CommerceWorkCurrent(frame, ref work)) return false;
if (work.request == 0)
{
var route = GameRoute(frame, out _); var context = route != 0 ? route - 0x50 : 0;
work.error = 3;
if (context == 0 || Pointer(Pointer(context)) != image + 0x541C70 || Pointer(context + 8) == 0) { work.state = 4; return false; }
if (work.kind != 0)
{
if (GetTickCount64() - work.queued > 250 || !(work.kind == 3 ? StartOriginalServiceRead(frame, ref work) : work.kind >= 4 ? StartOriginalServiceMutation(frame, ref work, context) : StartCommerceMutation(frame, ref work, context))) { work.state = 4; return false; }
}
else
{
var storage = (nuint)Call(image + 0x53A690, 0xF8); work.error = 4;
if (storage == 0) { work.state = 4; return false; }
work.request = (nuint)Call(image + 0x1E84F00, storage, context); work.error = 5;
if (work.request == 0 || Pointer(work.request) != image + 0x511F9D0) { work.state = 4; return false; }
Call(image + 0x3425CF0, work.request, work.market, 1, 0, 0);
}
work.state = 2;
}
work.error = 0;
var request = work.request;
if (Read<uint>(request + 0x3C) is 1 or 2)
if ((byte)Call(image + 0x9A3250, request) == 0) return true;
if ((byte)Call(image + 0x9B5480, request) == 0)
{
work.error = (uint)Call(image + 0x9A8F10, request); work.state = 4; return false;
}
nuint text = 0;
Call(image + 0x9A8DC0, request, (ulong)&text);
var copied = false;
try
{
var length = text != 0 ? Read<ulong>(text) : 0;
if (length != 0 && length < capacity)
{
CopyCommerceMemory(text + 0x14, body, checked((int)length)); size = (nuint)length; copied = true;
}
}
finally { if (text != 0) Call(image + 0x204010, (ulong)&text); }
work.state = copied ? 3u : 4u; if (!copied) work.error = 6; return copied;
}
catch (Exception error) { work.error = CommerceExceptionCode(error); work.state = 4; return false; }
}
private static void CopyCommerceMemory(nuint source, byte* destination, int length)
{
Platform.ReadBytes(source, length).CopyTo(new Span<byte>(destination, length));
}
private static void DeleteCommerceRead(ref CommerceReadRequest work)
{
try
{
var instance = work.request;
if (!work.borrowed && instance != 0 && (Pointer(instance) == image + 0x511F9D0 || Pointer(instance) == image + 0x51A4640))
{
var destroy = Pointer(Pointer(instance)); if (InImage(destroy)) Call(destroy, instance, 1);
}
}
catch { }
work.request = 0;
}
private static void PollCommerceRead(in Snapshot frame)
{
// A busy IPC reader must never make the game thread wait.
if (!Monitor.TryEnter(commerceReadLock)) return;
try { PollCommerceReadOwned(frame); }
finally { Monitor.Exit(commerceReadLock); }
}
private static void PollCommerceReadOwned(in Snapshot frame)
{
CommerceReadRequest work = commerceRead;
if (work.generation == 0 || work.state >= 3) return;
if (commerceCompletion is {} completion && completion.generation == work.generation)
{ PollCommerceCompletion(frame, completion); return; }
nuint length;
fixed (byte* body = commerceReadBuffer)
{
PollCommerceReadImpl(frame, ref work, body, (nuint)commerceReadBuffer.Length, out length);
if (work.state == 3)
{
// Copy/release the original request here; decode and validate
// its immutable response on a worker, then apply on a later tick.
var capacity = work.refreshClient ? ReadCommerceShipInventory(frame).capacity : 0;
var prepared = new CommerceCompletion(work, new ReadOnlySpan<byte>(body, checked((int)length)).ToArray(), capacity);
DeleteCommerceRead(ref work); prepared.work.request = 0;
commerceCompletion = prepared; work.state = 2; commerceRead = work;
try
{
if (!ThreadPool.QueueUserWorkItem(static item => PrepareCommerceCompletion(item), prepared, preferLocal: false))
throw new InvalidOperationException("Response worker unavailable");
}
catch { prepared.work.state = 4; prepared.work.error = 8; Volatile.Write(ref prepared.ready, true); }
return;
}
}
if (work.state == 3) work.observed = GetTickCount64();
var uncertain = (work.kind == 1 || work.kind >= 4) && work.mutationSent && !work.clientApplied && work.state >= 3;
if (work.state >= 3) DeleteCommerceRead(ref work);
commerceRead = work; if (uncertain) commerceReconciliationRequired = true;
}
private static bool CommerceOperationKey(string value) => value.Length == 32 && value.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static CommerceReadRequest NewCommerceRead(in Snapshot frame, uint market, ulong generation) => new()
{
generation = generation, entity = frame.entity, epoch = controllerEpoch, expires = GetTickCount64() + 30000,
life = frame.life, market = market, state = 1
};
private static IEnumerable<JsonElement> CommerceRows(JsonElement value) => value.ValueKind == JsonValueKind.Array ? value.EnumerateArray().ToArray() : value.ValueKind == JsonValueKind.Object ? value.EnumerateObject().Select(x => x.Value).ToArray() : [];
private static JsonNode? CommerceNode(JsonElement value) => JsonNode.Parse(value.GetRawText());
private static JsonArray CommerceFields(JsonElement value)
{
var result = new JsonArray();
foreach (var field in value.EnumerateObject())
{
var type = field.Value.ValueKind switch { JsonValueKind.Object => "object", JsonValueKind.Array => "array", JsonValueKind.String => "string", JsonValueKind.Number => "number", JsonValueKind.True or JsonValueKind.False => "boolean", _ => "null" };
var size = field.Value.ValueKind switch { JsonValueKind.Object => field.Value.EnumerateObject().Count(), JsonValueKind.Array => field.Value.GetArrayLength(), JsonValueKind.Null => 0, _ => 1 };
result.Add((JsonNode)Obj(("name", field.Name), ("type", type), ("count", size)));
}
return result;
}
private static JsonObject CommerceReadCall(string op, JsonElement request, in Snapshot frame)
{
if(op=="commerce.docked.status")return CommerceDockedStatus(request,frame);
if (op == "commerce.operation.status")
{
var key = request.Required("operationKey").GetString()!;
if (!CommerceOperationKey(key)) throw new InvalidOperationException("Identified original operation required");
ulong generation, current;
lock (commerceReadLock) { commerceOperationKeys.TryGetValue(key, out generation); current = commerceRead.generation; }
if (generation == 0) return Obj(("known", false), ("mutationsSent", 0));
if (generation != current) return Obj(("known", true), ("receiptAvailable", false), ("generation", generation), ("reconciliationRequired", true));
using var query = JsonDocument.Parse(Obj(("generation", generation)).ToJsonString());
var receipt = CommerceReadCall("commerce.market.status", query.RootElement, frame); receipt["known"] = true; receipt["receiptAvailable"] = true; return receipt;
}
if (op == "commerce.inventory")
{
var value = ReadCommerceShipInventory(frame); if (!value.verified) throw new InvalidOperationException("Original cargo and wallet observation unavailable");
return Obj(("count", value.count), ("capacity", value.capacity), ("credits", value.credits), ("verified", value.verified), ("entityId", frame.entity.ToString(System.Globalization.CultureInfo.InvariantCulture)));
}
if (op == "commerce.market.read")
{
var market = request.Required("marketId").GetUInt32();
if (market == 0 || market == uint.MaxValue || !inGame || !Enabled() || !frame.valid || !frame.healthValid || frame.healthEmergency || GetTickCount64() - frame.tick > 200 || !CommerceDocked(frame, market)) throw new InvalidOperationException("Actual healthy docked ship and matching market required");
lock (commerceReadLock)
{
if (commerceRead.generation != 0 && commerceRead.state < 3) throw new InvalidOperationException("Original market read already pending");
var refresh=request.Bool("refreshClient");
if(refresh&&commerceReconciliationRequired)throw new InvalidOperationException("An uncertain original order must be reconciled before refreshing client inventory");
commerceRead = NewCommerceRead(frame, market, ++commerceReadGeneration);commerceRead.refreshClient=refresh; commerceReadBody = "";
return Obj(("generation", commerceRead.generation), ("queued", true), ("mutationsSent", 0));
}
}
CommerceReadRequest work; string body;
lock (commerceReadLock) { work = commerceRead; body = commerceReadBody; }
if (request.Required("generation").GetUInt64() != work.generation) throw new InvalidOperationException("Market read generation changed");
var result = Obj(("generation", work.generation), ("marketId", work.market), ("state", work.state), ("error", work.error), ("mutationsSent", work.mutationSent ? 1 : 0),
("clientApplied", work.clientApplied), ("reconciliationRequired", commerceReconciliationRequired), ("commodityId", work.commodity), ("quantityDelta", work.delta), ("before", work.before), ("after", work.after), ("unitPrice", work.price), ("serviceKind", work.kind), ("creditsBefore", work.creditsBefore));
result["serverConfirmed"]=work.serviceReplyVerified;
result["profileRefreshRequested"]=work.refreshClient;result["profileApplied"]=work.profileApplied;
if(work.profileApplied){result["serverCredits"]=work.serverCredits;result["serverCargoCount"]=work.serverCargo;}
if(work.state==4&&work.kind==1)
{
result["failureStage"]=work.serviceReplyVerified?"client-application":"server-response-validation";
if(body.Length!=0)try
{
using var failed=JsonDocument.Parse(body);var failedResponse=failed.RootElement;
if(failedResponse.TryGetProperty("credits",out var balance)&&balance.TryGetUInt64(out var actual))result["serverCredits"]=actual;
result["serverCommodityCount"]=CommerceInventoryCount(failedResponse,work.commodity,CommerceExpectedCommodity(work));
if(failedResponse.TryGetProperty("error",out var error))result["serverError"]=CommerceNode(error);
}catch{result["serverResponseReadable"]=false;}
}
if (work.state != 3) return result;
result["serverConfirmed"] = work.serviceReplyVerified; result["credits"] = work.creditsResult; result["cargoBefore"] = work.totalBefore; result["cargoAfter"] = work.totalBefore + work.delta;
if (work.kind >= 4)
{
result["costToRepair"] = work.nextRepairCost; result["integrityWear"] = work.integrityWear;
result["fuelPrices"] = Obj(("fuelLevel", work.fuelResult), ("fuelCapacity", work.fuelCapacityResult), ("reserveLevel", work.reserveResult), ("reserveCapacity", work.reserveCapacityResult), ("costToRefuel", work.nextRefuelCost));
return result;
}
using var document = JsonDocument.Parse(body); var response = document.RootElement;
result["responseFields"] = response.ValueKind == JsonValueKind.Object ? CommerceFields(response) : new JsonArray();
var commodities = new JsonArray(); result["commodities"] = commodities;
if (response.TryGetProperty("credits", out var credits) && credits.ValueKind == JsonValueKind.Number && (credits.TryGetInt64(out _) || credits.TryGetUInt64(out _))) result["serverCredits"] = CommerceNode(credits);
if (response.TryGetProperty("prices", out var prices) && prices.ValueKind == JsonValueKind.Object)
{
result["priceFields"] = CommerceFields(prices); var fuelPrices = new JsonObject(); result["fuelPrices"] = fuelPrices;
var fuel = prices.TryGetProperty("fuel", out var nestedFuel) ? nestedFuel : prices;
foreach (var key in new[] { "costPerPartialUnit", "costToRefuel", "fuelLevel", "fuelCapacity", "reserveLevel", "reserveCapacity" }) if (fuel.TryGetProperty(key, out var field)) fuelPrices[key] = CommerceNode(field);
var servicePrices = new JsonObject(); result["servicePrices"] = servicePrices;
foreach (var key in new[] { "fuel", "repair", "ammunition" }) if (prices.TryGetProperty(key, out var field)) servicePrices[key] = CommerceNode(field);
}
uint index = 0; var offset = request.TryGetProperty("offset", out var at) ? at.GetUInt32() : 0; var maximum = Math.Min(request.TryGetProperty("maximum", out var max) ? max.GetUInt32() : 20, 50u);
if (response.TryGetProperty("commodities", out var rows) && rows.ValueKind is JsonValueKind.Array or JsonValueKind.Object)
{
foreach (var row in CommerceRows(rows))
{
if (index++ < offset || commodities.Count >= maximum) continue;
var quote = new JsonObject(); foreach (var key in new[] { "id", "name", "buyPrice", "sellPrice", "meanPrice", "stock", "demand", "statusFlags", "legality" }) if (row.TryGetProperty(key, out var field)) quote[key] = CommerceNode(field);
commodities.Add((JsonNode)quote);
}
}
result["commodityCount"] = index; var cargo = new JsonObject(); result["cargo"] = cargo;
if (response.TryGetProperty("inventory", out var inventory) && inventory.ValueKind == JsonValueKind.Array)
foreach (var row in inventory.EnumerateArray())
if (row.TryGetProperty("commodity", out var commodity) && commodity.ValueKind == JsonValueKind.String && row.TryGetProperty("qty", out var quantity) && quantity.TryGetInt32(out var count) && !row.Bool("stolen") && row.Int("marked") == 0)
{ var name = commodity.GetString()!; cargo[name] = (cargo[name]?.GetValue<int>() ?? 0) + count; }
return result;
}
}
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using static XFEEDToolBox.Plugin.JsonWire;
namespace XFEEDToolBox.Plugin;
internal static unsafe partial class Navigation
{
private struct RearmTarget { public fixed byte slot[48]; public int clip, hopper; }
[InlineArray(64)] private struct Rearm64 { private RearmTarget item; }
private struct CommerceReadRequest
{
public ulong generation, entity, epoch, expires;
public nuint life, request;
public uint market, state, error, kind, commodity, price;
public int delta, before, after, totalBefore;
public ulong queued, observed;
public bool mutationSent, clientApplied;
public bool refreshClient,profileApplied,profileValidated;
public bool formal;
public int capacityBound;
public fixed byte commodityName[128];
public ulong serverCredits;
public int serverCargo;
public nuint maintenance;
public bool borrowed;
public ulong creditsBefore;
public float fuelTarget, reserveTarget;
public bool serviceReplyVerified;
public ulong creditsResult;
public float fuelResult, reserveResult, fuelCapacityResult, reserveCapacityResult;
public uint nextRefuelCost, nextRepairCost, integrityWear;
public fixed byte servicePayload[4096];
public uint servicePayloadLength, rearmCount;
public Rearm64 rearm;
public CommerceReadRequest()
{
this = default;
// The original response structure initializes these output-only fields to zero.
nextRepairCost = 0; integrityWear = 0;
}
}
private static readonly object commerceReadLock = new();
private static CommerceReadRequest commerceRead;
private static ulong commerceReadGeneration;
private static string commerceReadBody = "";
private static readonly byte[] commerceReadBuffer = new byte[1024 * 1024];
private static bool commerceReconciliationRequired;
private static readonly Dictionary<string, ulong> commerceOperationKeys = new(StringComparer.Ordinal);
private struct CommerceShipInventory { public uint count, capacity; public ulong credits; public bool verified; }
private static CommerceShipInventory ReadCommerceShipInventory(in Snapshot frame)
{
CommerceShipInventory result = default;
try
{
if (!frame.valid || !frame.healthValid || GetTickCount64() - frame.tick > 200) return result;
if (GameRoute(frame, out var pilot) == 0) return result;
var cargo = RegisteredComponent(Pointer(frame.life + 0xC8) - 0x30, Read<uint>(image + 0x5F2F25C));
var wallet = RegisteredComponent(Pointer(pilot + 0xC8) - 0x30, Read<uint>(image + 0x5F59D90));
if (cargo == 0 || Pointer(cargo) != image + 0x530B858 || wallet == 0 || Pointer(wallet) != image + 0x5295868) return result;
result.count = Read<uint>(cargo + 0x28C); result.capacity = Read<uint>(cargo + 0x288);
result.credits = Read<ulong>(wallet + 0x98) ^ Read<ulong>(wallet + 0x90);
result.verified = result.capacity <= int.MaxValue && result.count <= result.capacity && result.credits < 100000000000000UL;
}
catch { result.verified = false; }
return result;
}
private static bool CommerceDocked(in Snapshot frame, uint market)
{
return ReadCommerceDocking(LocalDocking(frame), market, out _, out _);
}
private static bool ReadCommerceDocking(nuint dock,uint market,out uint state,out ulong actualMarket)
{
state=0;actualMarket=0;if(dock==0)return false;
state=Read<uint>(dock+0x4C0);var station=Pointer(dock+0x4E0);
if(station==0||Read<uint>(station+0xC0)!=8)return false;
actualMarket=Read<ulong>(station+0xB0);
return market is not(0 or uint.MaxValue)&&(state is 5 or 7)&&actualMarket==market;
}
private static JsonObject CommerceDockedStatus(JsonElement request,in Snapshot frame)
{
var market=request.Required("marketId").GetUInt32();
if(market is 0 or uint.MaxValue)throw new InvalidOperationException("Identified docked market required");
var now=GetTickCount64();var fresh=frame.valid&&now>=frame.tick&&now-frame.tick<=200;
var healthy=fresh&&frame.healthValid&&!frame.healthEmergency&&double.IsFinite(frame.hullFraction)&&frame.hullFraction>0;
uint state=0;ulong actualMarket=0;var docked=false;var service=false;string? issue=null;
if(fresh)try{docked=ReadCommerceDocking(LocalDocking(frame),market,out state,out actualMarket);if(docked&&healthy)service=OriginalServiceBinding(frame,0,market);}catch(Exception error){issue=error.GetType().Name;}
var ready=inGame&&Enabled()&&healthy&&docked&&issue is null;
var stage=!inGame?"notInGame":!Enabled()?"disabled":!fresh?"telemetry":!healthy?"health":issue is not null?"observation":!docked?"docking":!service?"services":"ready";
return Obj(("ready",ready),("serviceReady",ready&&service),("stage",stage),("fresh",fresh),("healthy",healthy),
("entityId",frame.entity.ToString(System.Globalization.CultureInfo.InvariantCulture)),("marketId",market),("actualMarketId",actualMarket),("rawDockingState",state),("issue",issue),("mutationsSent",0));
}
private static uint CommerceExceptionCode(Exception error) => unchecked((uint)error.HResult);
private static bool PollCommerceReadImpl(in Snapshot frame, ref CommerceReadRequest work, byte* body, nuint capacity, out nuint size)
{
size = 0;
try
{
if (!CommerceWorkCurrent(frame, ref work)) return false;
if (work.request == 0)
{
var route = GameRoute(frame, out _); var context = route != 0 ? route - 0x50 : 0;
work.error = 3;
if (context == 0 || Pointer(Pointer(context)) != image + 0x541C70 || Pointer(context + 8) == 0) { work.state = 4; return false; }
if (work.kind != 0)
{
if (GetTickCount64() - work.queued > 250 || !(work.kind == 3 ? StartOriginalServiceRead(frame, ref work) : work.kind >= 4 ? StartOriginalServiceMutation(frame, ref work, context) : StartCommerceMutation(frame, ref work, context))) { work.state = 4; return false; }
}
else
{
var storage = (nuint)Call(image + 0x53A690, 0xF8); work.error = 4;
if (storage == 0) { work.state = 4; return false; }
work.request = (nuint)Call(image + 0x1E84F00, storage, context); work.error = 5;
if (work.request == 0 || Pointer(work.request) != image + 0x511F9D0) { work.state = 4; return false; }
Call(image + 0x3425CF0, work.request, work.market, 1, 0, 0);
}
work.state = 2;
}
work.error = 0;
var request = work.request;
if (Read<uint>(request + 0x3C) is 1 or 2)
if ((byte)Call(image + 0x9A3250, request) == 0) return true;
if ((byte)Call(image + 0x9B5480, request) == 0)
{
work.error = (uint)Call(image + 0x9A8F10, request); work.state = 4; return false;
}
nuint text = 0;
Call(image + 0x9A8DC0, request, (ulong)&text);
var copied = false;
try
{
var length = text != 0 ? Read<ulong>(text) : 0;
if (length != 0 && length < capacity)
{
CopyCommerceMemory(text + 0x14, body, checked((int)length)); size = (nuint)length; copied = true;
}
}
finally { if (text != 0) Call(image + 0x204010, (ulong)&text); }
work.state = copied ? 3u : 4u; if (!copied) work.error = 6; return copied;
}
catch (Exception error) { work.error = CommerceExceptionCode(error); work.state = 4; return false; }
}
private static void CopyCommerceMemory(nuint source, byte* destination, int length)
{
Platform.ReadBytes(source, length).CopyTo(new Span<byte>(destination, length));
}
private static void DeleteCommerceRead(ref CommerceReadRequest work)
{
try
{
var instance = work.request;
if (!work.borrowed && instance != 0 && (Pointer(instance) == image + 0x511F9D0 || Pointer(instance) == image + 0x51A4640))
{
var destroy = Pointer(Pointer(instance)); if (InImage(destroy)) Call(destroy, instance, 1);
}
}
catch { }
work.request = 0;
}
private static void PollCommerceRead(in Snapshot frame)
{
// A busy IPC reader must never make the game thread wait.
if (!Monitor.TryEnter(commerceReadLock)) return;
try { PollCommerceReadOwned(frame); }
finally { Monitor.Exit(commerceReadLock); }
}
private static void PollCommerceReadOwned(in Snapshot frame)
{
CommerceReadRequest work = commerceRead;
if (work.generation == 0 || work.state >= 3) return;
if (commerceCompletion is {} completion && completion.generation == work.generation)
{ PollCommerceCompletion(frame, completion); return; }
nuint length;
fixed (byte* body = commerceReadBuffer)
{
PollCommerceReadImpl(frame, ref work, body, (nuint)commerceReadBuffer.Length, out length);
if (work.state == 3)
{
// Copy/release the original request here; decode and validate
// its immutable response on a worker, then apply on a later tick.
var capacity = work.refreshClient ? ReadCommerceShipInventory(frame).capacity : 0;
var prepared = new CommerceCompletion(work, new ReadOnlySpan<byte>(body, checked((int)length)).ToArray(), capacity);
DeleteCommerceRead(ref work); prepared.work.request = 0;
commerceCompletion = prepared; work.state = 2; commerceRead = work;
try
{
if (!ThreadPool.QueueUserWorkItem(static item => PrepareCommerceCompletion(item), prepared, preferLocal: false))
throw new InvalidOperationException("Response worker unavailable");
}
catch { prepared.work.state = 4; prepared.work.error = 8; Volatile.Write(ref prepared.ready, true); }
return;
}
}
if (work.state == 3) work.observed = GetTickCount64();
var uncertain = (work.kind == 1 || work.kind >= 4) && work.mutationSent && !work.clientApplied && work.state >= 3;
if (work.state >= 3) DeleteCommerceRead(ref work);
commerceRead = work; if (uncertain) commerceReconciliationRequired = true;
}
private static bool CommerceOperationKey(string value) => value.Length == 32 && value.All(c => c is >= '0' and <= '9' or >= 'a' and <= 'f');
private static CommerceReadRequest NewCommerceRead(in Snapshot frame, uint market, ulong generation) => new()
{
generation = generation, entity = frame.entity, epoch = controllerEpoch, expires = GetTickCount64() + 30000,
life = frame.life, market = market, state = 1
};
private static IEnumerable<JsonElement> CommerceRows(JsonElement value) => value.ValueKind == JsonValueKind.Array ? value.EnumerateArray().ToArray() : value.ValueKind == JsonValueKind.Object ? value.EnumerateObject().Select(x => x.Value).ToArray() : [];
private static JsonNode? CommerceNode(JsonElement value) => JsonNode.Parse(value.GetRawText());
private static JsonArray CommerceFields(JsonElement value)
{
var result = new JsonArray();
foreach (var field in value.EnumerateObject())
{
var type = field.Value.ValueKind switch { JsonValueKind.Object => "object", JsonValueKind.Array => "array", JsonValueKind.String => "string", JsonValueKind.Number => "number", JsonValueKind.True or JsonValueKind.False => "boolean", _ => "null" };
var size = field.Value.ValueKind switch { JsonValueKind.Object => field.Value.EnumerateObject().Count(), JsonValueKind.Array => field.Value.GetArrayLength(), JsonValueKind.Null => 0, _ => 1 };
result.Add((JsonNode)Obj(("name", field.Name), ("type", type), ("count", size)));
}
return result;
}
private static JsonObject CommerceReadCall(string op, JsonElement request, in Snapshot frame)
{
if(op=="commerce.docked.status")return CommerceDockedStatus(request,frame);
if (op == "commerce.operation.status")
{
var key = request.Required("operationKey").GetString()!;
if (!CommerceOperationKey(key)) throw new InvalidOperationException("Identified original operation required");
ulong generation, current;
lock (commerceReadLock) { commerceOperationKeys.TryGetValue(key, out generation); current = commerceRead.generation; }
if (generation == 0) return Obj(("known", false), ("mutationsSent", 0));
if (generation != current) return Obj(("known", true), ("receiptAvailable", false), ("generation", generation), ("reconciliationRequired", true));
using var query = JsonDocument.Parse(Obj(("generation", generation)).ToJsonString());
var receipt = CommerceReadCall("commerce.market.status", query.RootElement, frame); receipt["known"] = true; receipt["receiptAvailable"] = true; return receipt;
}
if (op == "commerce.inventory")
{
var value = ReadCommerceShipInventory(frame); if (!value.verified) throw new InvalidOperationException("Original cargo and wallet observation unavailable");
return Obj(("count", value.count), ("capacity", value.capacity), ("credits", value.credits), ("verified", value.verified), ("entityId", frame.entity.ToString(System.Globalization.CultureInfo.InvariantCulture)));
}
if (op == "commerce.market.read")
{
var market = request.Required("marketId").GetUInt32();
if (market == 0 || market == uint.MaxValue || !inGame || !Enabled() || !frame.valid || !frame.healthValid || frame.healthEmergency || GetTickCount64() - frame.tick > 200 || !CommerceDocked(frame, market)) throw new InvalidOperationException("Actual healthy docked ship and matching market required");
lock (commerceReadLock)
{
if (commerceRead.generation != 0 && commerceRead.state < 3) throw new InvalidOperationException("Original market read already pending");
var refresh=request.Bool("refreshClient");
if(refresh&&commerceReconciliationRequired)throw new InvalidOperationException("An uncertain original order must be reconciled before refreshing client inventory");
commerceRead = NewCommerceRead(frame, market, ++commerceReadGeneration);commerceRead.refreshClient=refresh; commerceReadBody = "";
return Obj(("generation", commerceRead.generation), ("queued", true), ("mutationsSent", 0));
}
}
CommerceReadRequest work; string body;
lock (commerceReadLock) { work = commerceRead; body = commerceReadBody; }
if (request.Required("generation").GetUInt64() != work.generation) throw new InvalidOperationException("Market read generation changed");
var result = Obj(("generation", work.generation), ("marketId", work.market), ("state", work.state), ("error", work.error), ("mutationsSent", work.mutationSent ? 1 : 0),
("clientApplied", work.clientApplied), ("reconciliationRequired", commerceReconciliationRequired), ("commodityId", work.commodity), ("quantityDelta", work.delta), ("before", work.before), ("after", work.after), ("unitPrice", work.price), ("serviceKind", work.kind), ("creditsBefore", work.creditsBefore));
result["serverConfirmed"]=work.serviceReplyVerified;
result["profileRefreshRequested"]=work.refreshClient;result["profileApplied"]=work.profileApplied;
if(work.profileApplied){result["serverCredits"]=work.serverCredits;result["serverCargoCount"]=work.serverCargo;}
if(work.state==4&&work.kind==1)
{
result["failureStage"]=work.serviceReplyVerified?"client-application":"server-response-validation";
if(body.Length!=0)try
{
using var failed=JsonDocument.Parse(body);var failedResponse=failed.RootElement;
if(failedResponse.TryGetProperty("credits",out var balance)&&balance.TryGetUInt64(out var actual))result["serverCredits"]=actual;
result["serverCommodityCount"]=CommerceInventoryCount(failedResponse,work.commodity,CommerceExpectedCommodity(work));
if(failedResponse.TryGetProperty("error",out var error))result["serverError"]=CommerceNode(error);
}catch{result["serverResponseReadable"]=false;}
}
if (work.state != 3) return result;
result["serverConfirmed"] = work.serviceReplyVerified; result["credits"] = work.creditsResult; result["cargoBefore"] = work.totalBefore; result["cargoAfter"] = work.totalBefore + work.delta;
if (work.kind >= 4)
{
result["costToRepair"] = work.nextRepairCost; result["integrityWear"] = work.integrityWear;
result["fuelPrices"] = Obj(("fuelLevel", work.fuelResult), ("fuelCapacity", work.fuelCapacityResult), ("reserveLevel", work.reserveResult), ("reserveCapacity", work.reserveCapacityResult), ("costToRefuel", work.nextRefuelCost));
return result;
}
using var document = JsonDocument.Parse(body); var response = document.RootElement;
result["responseFields"] = response.ValueKind == JsonValueKind.Object ? CommerceFields(response) : new JsonArray();
var commodities = new JsonArray(); result["commodities"] = commodities;
if (response.TryGetProperty("credits", out var credits) && credits.ValueKind == JsonValueKind.Number && (credits.TryGetInt64(out _) || credits.TryGetUInt64(out _))) result["serverCredits"] = CommerceNode(credits);
if (response.TryGetProperty("prices", out var prices) && prices.ValueKind == JsonValueKind.Object)
{
result["priceFields"] = CommerceFields(prices); var fuelPrices = new JsonObject(); result["fuelPrices"] = fuelPrices;
var fuel = prices.TryGetProperty("fuel", out var nestedFuel) ? nestedFuel : prices;
foreach (var key in new[] { "costPerPartialUnit", "costToRefuel", "fuelLevel", "fuelCapacity", "reserveLevel", "reserveCapacity" }) if (fuel.TryGetProperty(key, out var field)) fuelPrices[key] = CommerceNode(field);
var servicePrices = new JsonObject(); result["servicePrices"] = servicePrices;
foreach (var key in new[] { "fuel", "repair", "ammunition" }) if (prices.TryGetProperty(key, out var field)) servicePrices[key] = CommerceNode(field);
}
uint index = 0; var offset = request.TryGetProperty("offset", out var at) ? at.GetUInt32() : 0; var maximum = Math.Min(request.TryGetProperty("maximum", out var max) ? max.GetUInt32() : 20, 50u);
if (response.TryGetProperty("commodities", out var rows) && rows.ValueKind is JsonValueKind.Array or JsonValueKind.Object)
{
foreach (var row in CommerceRows(rows))
{
if (index++ < offset || commodities.Count >= maximum) continue;
var quote = new JsonObject(); foreach (var key in new[] { "id", "name", "buyPrice", "sellPrice", "meanPrice", "stock", "demand", "statusFlags", "legality" }) if (row.TryGetProperty(key, out var field)) quote[key] = CommerceNode(field);
commodities.Add((JsonNode)quote);
}
}
result["commodityCount"] = index; var cargo = new JsonObject(); result["cargo"] = cargo;
if (response.TryGetProperty("inventory", out var inventory) && inventory.ValueKind == JsonValueKind.Array)
foreach (var row in inventory.EnumerateArray())
if (row.TryGetProperty("commodity", out var commodity) && commodity.ValueKind == JsonValueKind.String && row.TryGetProperty("qty", out var quantity) && quantity.TryGetInt32(out var count) && !row.Bool("stolen") && row.Int("marked") == 0)
{ var name = commodity.GetString()!; cargo[name] = (cargo[name]?.GetValue<int>() ?? 0) + count; }
return result;
}
}