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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Xml;
using System.Xml.Linq;

namespace XFEEDToolBox.Core.Galaxy;

public sealed record GalaxySettingsImport(GalaxyMapFilter Filter,IReadOnlyDictionary<string,string> OriginalOptions,IReadOnlyList<string> Limitations,int? ColourVisIndex)
{
    public int? SizeVisIndex { get; init; }
    public string Summary=>$"已只读导入 {OriginalOptions.Count} 项配置,保留 {Filter.SavedValues.Count} 组筛选。"+(ColourVisIndex is not null?$"配色:{GalaxyFilterDefinitions.All.First(d=>d.Category==Filter.Category).Label}。":"")+(Limitations.Count>0?"其余原始值与覆盖说明可展开查看。":"");
}

/// <summary>Reads the game's saved player options. Never writes, activates, or edits game configuration.</summary>
public static class GalaxyOriginalSettings
{
    // Verified against original 3B45ED0 settings slots 11/12 and 3B16B00 dispatch.
    private static readonly GalaxyFilterCategory[] visibilityCategories=[GalaxyFilterCategory.None,GalaxyFilterCategory.Population,GalaxyFilterCategory.Allegiance,GalaxyFilterCategory.Government,GalaxyFilterCategory.Economy,GalaxyFilterCategory.StarClass,GalaxyFilterCategory.State,GalaxyFilterCategory.Security,GalaxyFilterCategory.Visited,GalaxyFilterCategory.Commodity,GalaxyFilterCategory.Civilization,GalaxyFilterCategory.Services,GalaxyFilterCategory.FleetCarrier,GalaxyFilterCategory.Colonisation,GalaxyFilterCategory.Exploration,GalaxyFilterCategory.TradeData];
    public static GalaxyFilterCategory? CategoryForIndex(int index)=>index>=0&&index<visibilityCategories.Length?visibilityCategories[index]:null;
    public const int MaximumBytes=2*1024*1024;
    public static GalaxySettingsImport Read(string path,GalaxyMapFilter current)
    {
        var info=new FileInfo(path);if(info.Length>MaximumBytes)throw new InvalidDataException("游戏星图配置超过大小限制。");
        using var file=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
        using var reader=XmlReader.Create(file,new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null,MaxCharactersInDocument=MaximumBytes});
        return Parse(XDocument.Load(reader),current);
    }
    public static GalaxySettingsImport Parse(string xml,GalaxyMapFilter current)
    {
        if(xml.Length>MaximumBytes)throw new InvalidDataException("游戏星图配置超过大小限制。");
        using var text=new StringReader(xml);using var reader=XmlReader.Create(text,new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null,MaxCharactersInDocument=MaximumBytes});
        return Parse(XDocument.Load(reader),current);
    }
    private static GalaxySettingsImport Parse(XDocument document,GalaxyMapFilter current)
    {
        var root=document.Root;if(root?.Name.LocalName!="Root")throw new InvalidDataException("游戏星图配置根节点无效。");
        var options=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);var limitations=new List<string>();
        var categories=new Dictionary<string,GalaxyFilterCategory>(StringComparer.OrdinalIgnoreCase)
        {{"government",GalaxyFilterCategory.Government},{"minorFactionStatus",GalaxyFilterCategory.State},{"faction",GalaxyFilterCategory.Allegiance},{"economy",GalaxyFilterCategory.Economy},
         {"starclass",GalaxyFilterCategory.StarClass},{"security",GalaxyFilterCategory.Security},{"population",GalaxyFilterCategory.Population},{"visited",GalaxyFilterCategory.Visited},
         {"exploration",GalaxyFilterCategory.Exploration},{"services",GalaxyFilterCategory.Services},{"site",GalaxyFilterCategory.Civilization},{"trade",GalaxyFilterCategory.TradeData},
         {"commodity",GalaxyFilterCategory.Commodity},{"fleetcarrier",GalaxyFilterCategory.FleetCarrier}};
        var saved=new Dictionary<GalaxyFilterCategory,IReadOnlySet<string>?>(current.SavedValues);var originalKeys=new Dictionary<GalaxyFilterCategory,IReadOnlySet<string>>(current.OriginalOptionKeys);
        var routes=new Dictionary<GalaxyFilterCategory,bool>(current.OriginalRouteFilters);
        var routeKeys=new Dictionary<GalaxyFilterCategory,IReadOnlySet<string>>(current.OriginalRouteOptionKeys);
        var routeSelected=new Dictionary<GalaxyFilterCategory,IReadOnlySet<string>>(current.OriginalRouteSelectedKeys);
        IReadOnlyDictionary<string,bool>? tradeCategories=null,tradeCommodities=null;
        var scalarNames=new HashSet<string>(("StarNames RegionNames TradeRoutes Constellations ShowGrid NavigationMarkers routePlottingMode routePlottingVirtualCargoMass routePlottingRealCargoMass NebulaNames RealisticView ColourVisIndex SizeVisIndex FriendMarkers WingMarkers MissionMarkers CommunityGoalMarkers ShipMarkers EngineerMarkers PoliticsMode PoliticsTerritoryType PoliticsTerritoryModePowersFilter PoliticsTerritoryModeStatesFilter PoliticsTerritoryActivityFilterPower PoliticsTerritoryActivityFilterAction PoliticsTerritoryActivityFilterFilterMinActivity PoliticsTerritoryActivityFilterMinActivity BookmarkMarkers TypeDMarkers TypeRMarkers NavJetConeBoost NavigationPreviousRoute CommodityIndex CommodityPriceMode TradeCategory ServicesOption SquadronBookmarkMarkers GalacticZonesPlane GalacticZoneNames StarterZoneMarkers FleetCarrierMarkers ConflictZoneMarkers DryDockMarkers TypeUMarkers TypeAMarkers MaelstromMarkers RescueMegashipMarkers ThargoidFrontlineMarkers PowerActivityMarkers ShowControls ShowSystemConnectors").Split(' '),StringComparer.OrdinalIgnoreCase);
        foreach(var element in root.Elements().Where(x=>scalarNames.Contains(x.Name.LocalName)))
        {if(element.Attribute("Value") is {} value)options[element.Name.LocalName]=value.Value;}
        foreach(var group in root.Element("VisibilityFlags")?.Elements()??[])
        {
            if(group.Name.LocalName is "categories" or "commodities")
            {
                // Original Settings VT52A0960+1B0/26069A0 reads the same
                // VisibilityFlags map loaded by2631820. Its absent-key
                // fallback is the caller's1, independently for each namespace.
                var flags=new Dictionary<string,bool>(StringComparer.Ordinal);
                foreach(var element in group.Elements())
                {
                    var raw=element.Attribute("Value")?.Value;if(raw is null)continue;
                    options["VisibilityFlags/"+group.Name.LocalName+"/"+element.Name.LocalName]=raw;
                    if(raw is not("0" or "1"))throw new InvalidDataException("游戏贸易筛选布尔值无效:"+element.Name.LocalName);
                    flags[GalaxyMapFilter.Normalize(element.Name.LocalName)]=raw=="1";
                }
                if(group.Name.LocalName=="categories")tradeCategories=flags;else tradeCommodities=flags;
                continue;
            }
            if(!categories.TryGetValue(group.Name.LocalName,out var category))continue;
            var selected=new HashSet<string>(StringComparer.Ordinal);var keys=new HashSet<string>(StringComparer.Ordinal);
            var exactKeys=new HashSet<string>(StringComparer.Ordinal);var exactSelected=new HashSet<string>(StringComparer.Ordinal);
            foreach(var element in group.Elements())
            {
                var raw=element.Attribute("Value")?.Value;if(raw is null)continue;options["VisibilityFlags/"+group.Name.LocalName+"/"+element.Name.LocalName]=raw;
                if(raw is not("0" or "1"))throw new InvalidDataException("游戏筛选布尔值无效:"+element.Name.LocalName);
                if(element.Name.LocalName.Equals("affectsRoutePlotting",StringComparison.OrdinalIgnoreCase)){routes[category]=raw=="1";continue;}
                var exact=GalaxyMapFilter.OriginalRouteKey(element.Name.LocalName);
                if(!exactKeys.Add(exact))throw new InvalidDataException("重复的原路线分类选项:"+element.Name.LocalName);
                if(raw=="1")exactSelected.Add(exact);
                var key=category==GalaxyFilterCategory.StarClass?GalaxyMapFilter.StarKey(element.Name.LocalName):GalaxyMapFilter.Normalize(element.Name.LocalName);
                if(key is null){limitations.Add("未识别恒星选项:"+element.Name.LocalName+"。");continue;}
                keys.Add(key);if(raw=="1")selected.Add(key);
            }
            saved[category]=selected;originalKeys[category]=keys;
            routeKeys[category]=exactKeys;routeSelected[category]=exactSelected;
        }
        foreach(var element in root.Element("VisibilityValues")?.Descendants()??[])
        {
            if(element.Attribute("Value") is not {} value)continue;
            var path=string.Join('/',element.AncestorsAndSelf().TakeWhile(x=>x!=root).Reverse().Select(x=>x.Name.LocalName));options[path]=value.Value;
        }
        double? PopulationBound(string suffix)=>options.TryGetValue("VisibilityValues/population/"+suffix,out var raw)&&double.TryParse(raw,System.Globalization.NumberStyles.Float,System.Globalization.CultureInfo.InvariantCulture,out var value)&&double.IsFinite(value)&&value>=0?value:null;
        var populationMinimum=PopulationBound("min")??current.PopulationMinimum;var populationMaximum=PopulationBound("max")??current.PopulationMaximum;
        if(populationMaximum<populationMinimum){limitations.Add("原人口范围无效,保留当前范围。");populationMinimum=current.PopulationMinimum;populationMaximum=current.PopulationMaximum;}
        if(options.Keys.Any(k=>k.StartsWith("VisibilityValues/",StringComparison.Ordinal)&&!k.StartsWith("VisibilityValues/population/",StringComparison.Ordinal)))limitations.Add("其他原数值范围保留原始值,尚未映射。");
        bool? Flag(string key)
        {if(!options.TryGetValue(key,out var value))return null;return value switch{"0"=>false,"1"=>true,_=>throw new InvalidDataException("游戏显示布尔值无效:"+key)};}
        int? Integer(string key)=>options.TryGetValue(key,out var value)&&int.TryParse(value,out var number)?number:null;
        var powerMask=current.TerritoryPowerMask;var stateMask=current.TerritoryStateMask;var powerStateValues=current.PowerStateValues;
        if(options.ContainsKey("PoliticsTerritoryModePowersFilter"))
        {if(Integer("PoliticsTerritoryModePowersFilter") is {} rawPowerMask)powerMask=GalaxyOriginalPowerFilters.FromSavedMask(rawPowerMask);else limitations.Add("原势力位掩码格式无效,保留当前选择。");}
        if(options.ContainsKey("PoliticsTerritoryModeStatesFilter"))
        {if(Integer("PoliticsTerritoryModeStatesFilter") is {} rawStateMask){stateMask=GalaxyOriginalPowerFilters.FromSavedMask(rawStateMask);powerStateValues=null;}else limitations.Add("原领土状态位掩码格式无效,保留当前选择。");}
        if(options.ContainsKey("ServicesOption"))
        {
            // Original 3AD17F0 reads slot34 as an index into exactly ten rows.
            // It is independent of the current ColourVisIndex and is retained
            // for switching back to the service query later.
            if(Integer("ServicesOption") is {} serviceIndex&&GalaxyOriginalServiceOptions.KeyForIndex(serviceIndex) is {} serviceKey)
            {saved[GalaxyFilterCategory.Services]=new HashSet<string>([serviceKey],StringComparer.Ordinal);originalKeys[GalaxyFilterCategory.Services]=GalaxyOriginalServiceOptions.Keys.ToHashSet(StringComparer.Ordinal);}
            else limitations.Add("未识别原服务选项编号,保留当前服务选择;原始值完整保留。");
        }
        var colour=Integer("ColourVisIndex");var size=Integer("SizeVisIndex");
        var mappedCategory=colour is {} colourIndex?CategoryForIndex(colourIndex)??current.Category:current.Category;
        var sizeCategory=size is {} sizeIndex?CategoryForIndex(sizeIndex)??current.SizeCategory:current.SizeCategory;
        if(colour is {} unknownColour&&CategoryForIndex(unknownColour) is null)limitations.Add($"未识别原配色编号 {unknownColour},保留当前类别。");
        if(size is {} unknownSize&&CategoryForIndex(unknownSize) is null)limitations.Add($"未识别原大小编号 {unknownSize},保留当前大小方式。");
        if(GalaxyOriginalSizeOptions.ForCategory(sizeCategory) is {CurrentPanelOption:false} savedSize)
            limitations.Add(savedSize.Formula==GalaxyOriginalSizeFormula.Uniform?"已恢复原保存的大小类别;其原实现返回统一尺寸0.7,当前游戏大小面板只提供人口开关。":"已恢复原保存的大小类别;使用该类别独立原公式,当前游戏大小面板只提供人口开关。");
        if(options.Keys.Any(k=>!k.StartsWith("VisibilityFlags/",StringComparison.OrdinalIgnoreCase)&&k is not("StarNames" or "RegionNames" or "NebulaNames" or "Constellations" or "GalacticZonesPlane" or "GalacticZoneNames" or "ShowGrid" or "NavigationMarkers" or "ShowSystemConnectors" or "RealisticView")))limitations.Add("其他原图层/路线选项保留为只读值,当前不支持的图层不会模拟。");
        if(routes.Values.Any(x=>x))limitations.Add("已记录游戏原有路线筛选开关;未向游戏写入任何路线设置。");
        var mode=Integer("RealisticView") switch{0=>GalaxyMapMode.Realistic,1=>GalaxyMapMode.Map,2=>GalaxyMapMode.Powerplay,4=>GalaxyMapMode.Colonisation,_=>current.Mode};
        if(Integer("RealisticView") is {} view&&view is not(0 or 1 or 2 or 4))limitations.Add($"原视图编号 {view} 的语义尚未核验,保留当前模式。");
        var powerplayView=current.PowerplayView;var territoryActivityEnabled=current.TerritoryActivityFilterEnabled;var useMinimumActivity=current.UseMinimumActivity;var minimumActivity=current.MinimumActivity;var activity=current.Activity;
        var activitySelection=current;
        if(mode==GalaxyMapMode.Powerplay)
        {
            mappedCategory=GalaxyFilterCategory.Power;powerplayView=Integer("PoliticsMode")==1?GalaxyPowerplayView.Activities:GalaxyPowerplayView.Territory;
            limitations.Add("原领土掩码按实际势力表索引及原状态分组恢复;筛选须由当前游戏元数据确认,缺少资料保持未知。");
            if(options.ContainsKey("PoliticsTerritoryActivityFilterPower")||options.ContainsKey("PoliticsTerritoryActivityFilterAction"))
            {
                if(Integer("PoliticsTerritoryActivityFilterPower") is {} savedScope&&savedScope>=-2)
                {
                    activitySelection=GalaxyOriginalPowerSelection.FromSavedActivityPower(current,savedScope);
                    territoryActivityEnabled=activitySelection.TerritoryActivityFilterEnabled;
                    limitations.Add(activitySelection.OriginalActivityPowerIndex is not null?"已保留保存的活动势力;等待当前游戏定义表后按原索引自动恢复。":
                        activitySelection.ActivityAllPowers?"已恢复原领土附加条件的“全部势力”范围;与外层活动视图独立。":
                        savedScope==-1?"原领土附加活动条件已停用。":"已按原定义索引恢复活动势力:"+activitySelection.ActivityPowerName+"。");
                }
                else if(options.ContainsKey("PoliticsTerritoryActivityFilterPower"))limitations.Add("原活动势力选择值无效,保留当前选择及原始值。");
                useMinimumActivity=Flag("PoliticsTerritoryActivityFilterFilterMinActivity")??useMinimumActivity;
                if(Integer("PoliticsTerritoryActivityFilterAction") is {} action)
                {if(GalaxyPowerActivities.FromOriginalUid(action) is {} originalAction)activity=originalAction;else limitations.Add($"原活动编号 {action} 尚未识别,保留当前活动。");}
                if(options.TryGetValue("PoliticsTerritoryActivityFilterMinActivity",out var rawActivity)&&double.TryParse(rawActivity,System.Globalization.NumberStyles.Float,System.Globalization.CultureInfo.InvariantCulture,out var originalActivity)&&double.IsFinite(originalActivity)&&originalActivity is >=0 and <=1)minimumActivity=originalActivity;
            }
        }
        if(mode==GalaxyMapMode.Colonisation)mappedCategory=GalaxyFilterCategory.Colonisation;
        var filter=current with{Mode=mode,PowerplayView=powerplayView,TerritoryPowerMask=powerMask,TerritoryStateMask=stateMask,PowerStateValues=powerStateValues,TerritoryActivityFilterEnabled=territoryActivityEnabled,Category=mappedCategory,SizeCategory=sizeCategory,PopulationMinimum=populationMinimum,PopulationMaximum=populationMaximum,ActivityPowerName=activitySelection.ActivityPowerName,ActivityInternalPowerId=activitySelection.ActivityInternalPowerId,ActivityAllPowers=activitySelection.ActivityAllPowers,OriginalActivityPowerIndex=activitySelection.OriginalActivityPowerIndex,Activity=activity,UseMinimumActivity=useMinimumActivity,MinimumActivity=minimumActivity,ShowGrid=Flag("ShowGrid")??current.ShowGrid,ShowLabels=Flag("StarNames")??current.ShowLabels,ShowLinks=Flag("ShowSystemConnectors")??current.ShowLinks,
            ShowRegionNames=Flag("RegionNames")??current.ShowRegionNames,ShowNebulaNames=Flag("NebulaNames")??current.ShowNebulaNames,ShowConstellations=Flag("Constellations")??current.ShowConstellations,ShowGalacticZonesPlane=Flag("GalacticZonesPlane")??current.ShowGalacticZonesPlane,ShowGalacticZoneNames=Flag("GalacticZoneNames")??current.ShowGalacticZoneNames,ShowCurrentTradeFlows=Flag("TradeRoutes")??current.ShowCurrentTradeFlows,
            ShowRoute=Flag("NavigationMarkers")??current.ShowRoute,SavedValues=saved,OriginalOptionKeys=originalKeys,OriginalRouteFilters=routes,OriginalRouteOptionKeys=routeKeys,OriginalRouteSelectedKeys=routeSelected,
            Values=saved.TryGetValue(mappedCategory,out var values)?values:mappedCategory==current.Category?current.Values:null};
        if(options.TryGetValue("CommodityIndex",out var originalCommodity))
        {
            if(GalaxyOriginalCommodities.TrySavedUid(originalCommodity,out var uid))
            {
                filter=filter with{OriginalCommodityUid=uid};filter=GalaxyOriginalCommodities.WithOptions(filter,current.OriginalCommodityOptions);
                if(GalaxyOriginalCommodities.Resolve(current.OriginalCommodityOptions,uid) is null)limitations.Add("原商品UID尚未在当前游戏定义源中确认;保留原UID,暂不按市场编号或枚举行号猜选项。");
                else if(GalaxyOriginalCommodities.Resolve(current.OriginalCommodityOptions,uid)?.PublisherEligible==false)limitations.Add("原商品UID已确认,但当前原库存条件不满足;保留UID,不将其恢复为原可选商品。");
            }
            else limitations.Add("原商品UID尚未识别,保留当前选择和原始值。");
        }
        if(Integer("CommodityPriceMode") is {} priceMode)
        {
            filter=filter with{OriginalCommodityPriceMode=priceMode};
            if(GalaxyOriginalCommodities.DirectionFromSavedPriceMode(priceMode) is {} direction)filter=filter with{CommodityDirection=direction};
            else limitations.Add("原商品价格模式尚未识别,保留当前工具流向与原始值。");
        }
        if(options.ContainsKey("TradeCategory"))
        {
            if(Integer("TradeCategory") is {} traderCategory&&GalaxyOriginalTradeCategories.FromSavedTraderCategory(traderCategory) is {} source)filter=filter with{CurrentTradeSource=source,HistoricalTradeSource=source};
            else limitations.Add("未识别原交易者来源编号,保留当前贸易来源;原始值完整保留。");
            limitations.Add("已恢复原交易者来源;当前流仅使用原时钟有效期内的现有缓存。历史层独立,不自动开启。");
        }
        if(tradeCategories is not null)
        {
            var definitions=GalaxyOriginalTradeCategories.Categories;
            filter=filter with{CurrentTradeCategories=definitions.Where(c=>!tradeCategories.TryGetValue(GalaxyMapFilter.Normalize(c.Name),out var enabled)||enabled).Select(c=>c.Id).ToHashSet()};
            var known=definitions.Select(c=>GalaxyMapFilter.Normalize(c.Name)).ToHashSet(StringComparer.Ordinal);
            if(tradeCategories.Keys.Any(k=>!known.Contains(k)))limitations.Add("未识别的原贸易父类保留原始值,未猜测对应分类。");
        }
        if(tradeCommodities is not null)
        {
            var definitions=GalaxyOriginalTradeCategories.Commodities;
            filter=filter with{CurrentTradeCommodities=definitions.Select(c=>GalaxyMapFilter.Normalize(c.Name)).Where(k=>!tradeCommodities.TryGetValue(k,out var enabled)||enabled).ToHashSet(StringComparer.Ordinal)};
            var known=definitions.Select(c=>GalaxyMapFilter.Normalize(c.Name)).ToHashSet(StringComparer.Ordinal);
            if(tradeCommodities.Keys.Any(k=>!known.Contains(k)))limitations.Add("未识别的原贸易商品保留原始值,未按市场编号或原商品UID猜映射。");
        }
        filter=filter with{VisibleMarkerCategories=GalaxyMarkerDisplayOptions.Import(options,current.VisibleMarkerCategories)};
        if(mode==GalaxyMapMode.Powerplay&&mappedCategory==GalaxyFilterCategory.Power&&options.ContainsKey("PoliticsTerritoryModePowersFilter"))filter=filter with{Values=null};
        if(options.Count==0&&tradeCategories is null&&tradeCommodities is null)throw new InvalidDataException("文件不包含可识别的游戏星图设置。");
        return new(filter,options,limitations,colour){SizeVisIndex=size};
    }
}