XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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;
    }
}