using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Data;
namespace XFEEDToolBox.Core.Execution;
public sealed record CommunityGoalInfo(ulong Id,string Title,string Activity,string GoalType,long MarketId,
string SystemName,string StationName,DateTimeOffset Expires,bool Active,bool Subscribed,long? Contribution)
{public IReadOnlyList<string> AcceptedCommodities{get;init;}=[];}
/// <summary>Schema observed from the original initiative SDK, including hex UTF-8 bulletin text.</summary>
public static class CommunityGoalCatalogue
{
public static IReadOnlyList<CommunityGoalInfo> MatchRoute(IReadOnlyList<CommunityGoalInfo> goals,RoutePlan plan,DateTimeOffset now)
=>goals.Where(goal=>goal.Active&&goal.Expires>now&&goal.GoalType=="trade"&&goal.Activity.StartsWith("trade",StringComparison.Ordinal)
&&goal.AcceptedCommodities.Count>0&&plan.Legs.Any(leg=>leg.To.MarketId==goal.MarketId
&&leg.To.SystemName.Equals(goal.SystemName,StringComparison.OrdinalIgnoreCase)&&leg.To.Name.Equals(goal.StationName,StringComparison.OrdinalIgnoreCase)
&&leg.Orders.Any(order=>order.Quantity>0&&goal.AcceptedCommodities.Contains(CommodityCatalog.Key(order.Commodity),StringComparer.Ordinal))))
.DistinctBy(goal=>goal.Id).ToArray();
private static IReadOnlyList<string> AcceptedGoods(JsonElement row,string station,string system)
{
var bulletin=row.GetProperty("bulletin");if(!bulletin.TryGetProperty("body",out var encoded)||encoded.ValueKind!=JsonValueKind.String)return [];
var body=Decode(encoded);
// This is the observed original bulletin sentence, bounded by its exact
// destination. Unknown prose or localisation is not commodity evidence.
var pattern="by delivering (?<goods>[A-Za-z][A-Za-z ,'-]{0,1000}) to "+Regex.Escape(station)+" in the "+Regex.Escape(system)+" system\\.";
var matches=Regex.Matches(body,pattern,RegexOptions.CultureInvariant,TimeSpan.FromMilliseconds(100));
if(matches.Count!=1)return [];
var names=matches[0].Groups["goods"].Value.Replace(", and ",", ",StringComparison.Ordinal).Replace(" and ",", ",StringComparison.Ordinal).Split(", ",StringSplitOptions.RemoveEmptyEntries|StringSplitOptions.TrimEntries);
if(names.Length is <1 or >32||names.Any(name=>!Regex.IsMatch(name,"^[A-Za-z][A-Za-z '-]{0,80}$",RegexOptions.CultureInvariant)))return [];
return names.Select(name=>CommodityCatalog.Key(name)).Distinct(StringComparer.Ordinal).ToArray();
}
public static CommunityGoalInfo RequireDestinationTrade(IReadOnlyList<CommunityGoalInfo> goals,long marketId,ulong? goalId,DateTimeOffset now,bool requireSubscribed=false)
{
var matches=goals.Where(g=>g.MarketId==marketId&&(!goalId.HasValue||g.Id==goalId.Value)&&g.GoalType=="trade"
&&g.Activity.StartsWith("trade",StringComparison.Ordinal)&&g.Active&&g.Expires>now).ToArray();
if(matches.Length!=1||requireSubscribed&&!matches[0].Subscribed)throw new InvalidDataException("目的站的有效贸易CG不唯一、已结束或报名尚未确认。");
return matches[0];
}
public const string TradeTitle="Faulcon DeLacy Trade Initiative";
public const long HomerMarket=4210924803;
private static readonly UTF8Encoding Utf8=new(false,true);
private static string Decode(JsonElement value)
{
var text=value.GetString()??throw new InvalidDataException("CG text is null.");
if(text.Length>262144||text.Length%2!=0)throw new InvalidDataException("Invalid CG text length.");
try{return Utf8.GetString(Convert.FromHexString(text));}
catch(Exception error)when(error is FormatException or DecoderFallbackException){throw new InvalidDataException("Invalid original CG text encoding.",error);}
}
public static IReadOnlyList<CommunityGoalInfo> Parse(string body)
{
if(body.Length>1048576)throw new InvalidDataException("CG response is oversized.");
using var document=JsonDocument.Parse(body);var rows=document.RootElement.GetProperty("initiatives");
if(rows.ValueKind==JsonValueKind.Array&&rows.GetArrayLength()==0)return [];
if(rows.ValueKind!=JsonValueKind.Object)throw new InvalidDataException("Unknown original CG list schema.");
var result=new List<CommunityGoalInfo>();var ids=new HashSet<ulong>();
foreach(var item in rows.EnumerateObject())
{
var row=item.Value;var id=row.GetProperty("id").GetUInt64();
if(!ulong.TryParse(item.Name,NumberStyles.None,CultureInfo.InvariantCulture,out var key)||id==0||id>uint.MaxValue||id!=key||!ids.Add(id))
throw new InvalidDataException("CG identity is duplicated or inconsistent.");
var dynamic=row.GetProperty("dynamic");var quantity=dynamic.GetProperty("commanderContributionQty");
// The original SDK returns null before delivery, and can return
// either an integer or decimal-digit string after contribution.
long? contribution=null;
if(quantity.ValueKind!=JsonValueKind.Null)
{
long parsed;
var valid=quantity.ValueKind==JsonValueKind.Number?quantity.TryGetInt64(out parsed)
:long.TryParse(quantity.ValueKind==JsonValueKind.String?quantity.GetString():null,
NumberStyles.None,CultureInfo.InvariantCulture,out parsed);
if(!valid||parsed<0)throw new InvalidDataException("Invalid original CG contribution.");
contribution=parsed;
}
var system=Decode(row.GetProperty("systemName"));var station=Decode(row.GetProperty("marketName"));
result.Add(new(id,Decode(row.GetProperty("bulletin").GetProperty("title")),row.GetProperty("activityType").GetString()!,row.GetProperty("goalType").GetString()!,
row.GetProperty("market_id").GetInt64(),system,station,
DateTimeOffset.FromUnixTimeSeconds(row.GetProperty("expiry").GetInt64()),
row.GetProperty("final_state").GetString()=="active"&&!row.GetProperty("complete").GetBoolean()&&!dynamic.GetProperty("complete").GetBoolean()
&&row.GetProperty("hidden").GetString()=="0",
dynamic.GetProperty("commanderSubscribed").GetBoolean(),contribution){AcceptedCommodities=AcceptedGoods(row,station,system)});
}
return result;
}
public static CommunityGoalInfo RequireTrade(IReadOnlyList<CommunityGoalInfo> goals,DateTimeOffset now,bool requireSubscribed=false)
{
var matches=goals.Where(goal=>goal.Title==TradeTitle).ToArray();
if(matches.Length!=1)throw new InvalidDataException("The exact trade CG is missing or ambiguous.");
var goal=matches[0];
if(goal.MarketId!=HomerMarket||goal.SystemName!="AD Leonis"||goal.StationName!="Homer Platform"
||goal.Activity!="tradelist"||goal.GoalType!="trade"||!goal.Active||goal.Expires<=now||requireSubscribed&&!goal.Subscribed)
throw new InvalidDataException("Original CG destination, activity, expiry or membership is not confirmed.");
return goal;
}
}
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Data;
namespace XFEEDToolBox.Core.Execution;
public sealed record CommunityGoalInfo(ulong Id,string Title,string Activity,string GoalType,long MarketId,
string SystemName,string StationName,DateTimeOffset Expires,bool Active,bool Subscribed,long? Contribution)
{public IReadOnlyList<string> AcceptedCommodities{get;init;}=[];}
/// <summary>Schema observed from the original initiative SDK, including hex UTF-8 bulletin text.</summary>
public static class CommunityGoalCatalogue
{
public static IReadOnlyList<CommunityGoalInfo> MatchRoute(IReadOnlyList<CommunityGoalInfo> goals,RoutePlan plan,DateTimeOffset now)
=>goals.Where(goal=>goal.Active&&goal.Expires>now&&goal.GoalType=="trade"&&goal.Activity.StartsWith("trade",StringComparison.Ordinal)
&&goal.AcceptedCommodities.Count>0&&plan.Legs.Any(leg=>leg.To.MarketId==goal.MarketId
&&leg.To.SystemName.Equals(goal.SystemName,StringComparison.OrdinalIgnoreCase)&&leg.To.Name.Equals(goal.StationName,StringComparison.OrdinalIgnoreCase)
&&leg.Orders.Any(order=>order.Quantity>0&&goal.AcceptedCommodities.Contains(CommodityCatalog.Key(order.Commodity),StringComparer.Ordinal))))
.DistinctBy(goal=>goal.Id).ToArray();
private static IReadOnlyList<string> AcceptedGoods(JsonElement row,string station,string system)
{
var bulletin=row.GetProperty("bulletin");if(!bulletin.TryGetProperty("body",out var encoded)||encoded.ValueKind!=JsonValueKind.String)return [];
var body=Decode(encoded);
// This is the observed original bulletin sentence, bounded by its exact
// destination. Unknown prose or localisation is not commodity evidence.
var pattern="by delivering (?<goods>[A-Za-z][A-Za-z ,'-]{0,1000}) to "+Regex.Escape(station)+" in the "+Regex.Escape(system)+" system\\.";
var matches=Regex.Matches(body,pattern,RegexOptions.CultureInvariant,TimeSpan.FromMilliseconds(100));
if(matches.Count!=1)return [];
var names=matches[0].Groups["goods"].Value.Replace(", and ",", ",StringComparison.Ordinal).Replace(" and ",", ",StringComparison.Ordinal).Split(", ",StringSplitOptions.RemoveEmptyEntries|StringSplitOptions.TrimEntries);
if(names.Length is <1 or >32||names.Any(name=>!Regex.IsMatch(name,"^[A-Za-z][A-Za-z '-]{0,80}$",RegexOptions.CultureInvariant)))return [];
return names.Select(name=>CommodityCatalog.Key(name)).Distinct(StringComparer.Ordinal).ToArray();
}
public static CommunityGoalInfo RequireDestinationTrade(IReadOnlyList<CommunityGoalInfo> goals,long marketId,ulong? goalId,DateTimeOffset now,bool requireSubscribed=false)
{
var matches=goals.Where(g=>g.MarketId==marketId&&(!goalId.HasValue||g.Id==goalId.Value)&&g.GoalType=="trade"
&&g.Activity.StartsWith("trade",StringComparison.Ordinal)&&g.Active&&g.Expires>now).ToArray();
if(matches.Length!=1||requireSubscribed&&!matches[0].Subscribed)throw new InvalidDataException("目的站的有效贸易CG不唯一、已结束或报名尚未确认。");
return matches[0];
}
public const string TradeTitle="Faulcon DeLacy Trade Initiative";
public const long HomerMarket=4210924803;
private static readonly UTF8Encoding Utf8=new(false,true);
private static string Decode(JsonElement value)
{
var text=value.GetString()??throw new InvalidDataException("CG text is null.");
if(text.Length>262144||text.Length%2!=0)throw new InvalidDataException("Invalid CG text length.");
try{return Utf8.GetString(Convert.FromHexString(text));}
catch(Exception error)when(error is FormatException or DecoderFallbackException){throw new InvalidDataException("Invalid original CG text encoding.",error);}
}
public static IReadOnlyList<CommunityGoalInfo> Parse(string body)
{
if(body.Length>1048576)throw new InvalidDataException("CG response is oversized.");
using var document=JsonDocument.Parse(body);var rows=document.RootElement.GetProperty("initiatives");
if(rows.ValueKind==JsonValueKind.Array&&rows.GetArrayLength()==0)return [];
if(rows.ValueKind!=JsonValueKind.Object)throw new InvalidDataException("Unknown original CG list schema.");
var result=new List<CommunityGoalInfo>();var ids=new HashSet<ulong>();
foreach(var item in rows.EnumerateObject())
{
var row=item.Value;var id=row.GetProperty("id").GetUInt64();
if(!ulong.TryParse(item.Name,NumberStyles.None,CultureInfo.InvariantCulture,out var key)||id==0||id>uint.MaxValue||id!=key||!ids.Add(id))
throw new InvalidDataException("CG identity is duplicated or inconsistent.");
var dynamic=row.GetProperty("dynamic");var quantity=dynamic.GetProperty("commanderContributionQty");
// The original SDK returns null before delivery, and can return
// either an integer or decimal-digit string after contribution.
long? contribution=null;
if(quantity.ValueKind!=JsonValueKind.Null)
{
long parsed;
var valid=quantity.ValueKind==JsonValueKind.Number?quantity.TryGetInt64(out parsed)
:long.TryParse(quantity.ValueKind==JsonValueKind.String?quantity.GetString():null,
NumberStyles.None,CultureInfo.InvariantCulture,out parsed);
if(!valid||parsed<0)throw new InvalidDataException("Invalid original CG contribution.");
contribution=parsed;
}
var system=Decode(row.GetProperty("systemName"));var station=Decode(row.GetProperty("marketName"));
result.Add(new(id,Decode(row.GetProperty("bulletin").GetProperty("title")),row.GetProperty("activityType").GetString()!,row.GetProperty("goalType").GetString()!,
row.GetProperty("market_id").GetInt64(),system,station,
DateTimeOffset.FromUnixTimeSeconds(row.GetProperty("expiry").GetInt64()),
row.GetProperty("final_state").GetString()=="active"&&!row.GetProperty("complete").GetBoolean()&&!dynamic.GetProperty("complete").GetBoolean()
&&row.GetProperty("hidden").GetString()=="0",
dynamic.GetProperty("commanderSubscribed").GetBoolean(),contribution){AcceptedCommodities=AcceptedGoods(row,station,system)});
}
return result;
}
public static CommunityGoalInfo RequireTrade(IReadOnlyList<CommunityGoalInfo> goals,DateTimeOffset now,bool requireSubscribed=false)
{
var matches=goals.Where(goal=>goal.Title==TradeTitle).ToArray();
if(matches.Length!=1)throw new InvalidDataException("The exact trade CG is missing or ambiguous.");
var goal=matches[0];
if(goal.MarketId!=HomerMarket||goal.SystemName!="AD Leonis"||goal.StationName!="Homer Platform"
||goal.Activity!="tradelist"||goal.GoalType!="trade"||!goal.Active||goal.Expires<=now||requireSubscribed&&!goal.Subscribed)
throw new InvalidDataException("Original CG destination, activity, expiry or membership is not confirmed.");
return goal;
}
}