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

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json.Linq;

namespace XFEWarframe.Desktop.Editing;

public sealed record EquipmentTarget(string AccountId, string AccountName, string Category, string Item);
public sealed record EquipmentRevision(string Version, string Permissions, bool Enabled);
public sealed record EquipmentTab(string Key, string Title, IReadOnlyList<EquipmentAction> Actions);

public sealed class EquipmentAction(EditorAction source)
{
    public string Key => source.Key;
    public string Label => source.Label;
    public string Description => source.Description;
    public IReadOnlyList<EditorField> Fields { get; } = source.Fields.OfType<JObject>().Select(x => new EditorField(x)).ToArray();
    public bool Changed => Fields.Any(x => x.Changed);
}

/// <summary>One detail editor for both desktop roles. Transports supply only actions authorized by the server.</summary>
public sealed class EquipmentEditorModel : ObservableObject
{
    private readonly Func<EquipmentTarget, Task<JObject>> read;
    private readonly Func<EquipmentTarget, JObject, Task<JObject>> save;
    private readonly Func<EquipmentTarget, Task<EquipmentRevision>> revision;
    private readonly Func<string, string, Task<bool>> confirm;
    private readonly Action<string> notify;
    private readonly EditorChangeMonitor changes;
    private EquipmentTarget? target;
    private string? version, permissionVersion;
    private int generation;
    private bool busy, conflict;
    private string title = UiText.Translate("装备详情"), subtitle = "", summary = "", message = "";
    private IReadOnlyList<EquipmentTab> tabs = [];

    public string Title { get => title; private set => SetProperty(ref title, value); }
    public string Subtitle { get => subtitle; private set => SetProperty(ref subtitle, value); }
    public string Summary { get => summary; private set => SetProperty(ref summary, value); }
    public string Message { get => message; private set { SetProperty(ref message, value); OnPropertyChanged(nameof(HasMessage)); } }
    public bool HasMessage => Message.Length > 0;
    public string SelectedTabKey { get; set; } = "basic";
    public IReadOnlyList<EquipmentTab> Tabs { get => tabs; private set => SetProperty(ref tabs, value); }
    public IEnumerable<EquipmentAction> Actions => Tabs.SelectMany(x => x.Actions);
    public bool HasChanges => Actions.Any(x => x.Changed);
    public bool HasConflict { get => conflict; private set { SetProperty(ref conflict, value); SaveCommand.NotifyCanExecuteChanged(); } }
    public bool IsBusy { get => busy; private set { SetProperty(ref busy, value); OnPropertyChanged(nameof(IsIdle)); SaveCommand.NotifyCanExecuteChanged(); ResetCommand.NotifyCanExecuteChanged(); RefreshCommand.NotifyCanExecuteChanged(); } }
    public bool IsIdle => !IsBusy;
    public IAsyncRelayCommand<EquipmentAction> SaveCommand { get; }
    public IRelayCommand<EquipmentAction> ResetCommand { get; }
    public IAsyncRelayCommand RefreshCommand { get; }

    public EquipmentEditorModel(Func<EquipmentTarget, Task<JObject>> read,
        Func<EquipmentTarget, JObject, Task<JObject>> save,
        Func<EquipmentTarget, Task<EquipmentRevision>> revision,
        Func<string, string, Task<bool>> confirm, Action<string> notify)
    {
        this.read = read; this.save = save; this.revision = revision; this.confirm = confirm; this.notify = notify;
        changes = new EditorChangeMonitor(CheckChangesAsync);
        SaveCommand = new AsyncRelayCommand<EquipmentAction>(SaveAsync, action => !IsBusy && !HasConflict && version is not null && action is not null && Actions.Contains(action));
        ResetCommand = new RelayCommand<EquipmentAction>(Reset, _ => !IsBusy);
        RefreshCommand = new AsyncRelayCommand(() => RunAsync(async () =>
        {
            if (target is null || !await CanDiscardAsync()) return;
            await ReadAsync(false);
        }), () => !IsBusy);
    }

    public void Attach() => changes.Start();
    public void Detach() { changes.Stop(); generation++; }
    public Task LoadAsync(EquipmentTarget? next) => RunAsync(async () =>
    {
        generation++; target = next; version = permissionVersion = null; Tabs = []; HasConflict = false; Message = "";
        Title = UiText.Translate("装备详情"); Summary = "";
        Subtitle = next is null ? "" : next.AccountName + " · " + InventoryNavigation.Label(next.Category);
        if (next is null) { Message = UiText.Translate("请从库存列表选择一件装备。"); return; }
        await ReadAsync(false);
    });

    private async Task ReadAsync(bool preserveDrafts, string? savedKey = null)
    {
        var selected = target!; var observed = generation; var pageGeneration = changes.Generation;
        var data = await read(selected);
        var prepared = await Task.Run(() => ((JArray?)data["actions"] ?? []).OfType<JObject>().Select(x =>
            new EquipmentAction(new EditorAction(x.Value<string>("key")!, x.Value<string>("label")!, x.Value<string>("description") ?? "", (JArray)x["fields"]!, ""))).ToArray());
        if (selected != target || observed != generation || pageGeneration != changes.Generation) throw new OperationCanceledException();
        if (data.Value<string>("selectedKey") != selected.Item)
        {
            Tabs = []; version = null; HasConflict = true; Message = UiText.Translate("此物品已不存在,请返回库存列表。"); return;
        }
        if (preserveDrafts)
            foreach (var action in prepared.Where(x => x.Key != savedKey))
                if (Actions.FirstOrDefault(x => x.Key == action.Key) is { } previous)
                    foreach (var field in action.Fields)
                        if (previous.Fields.FirstOrDefault(x => x.Key == field.Key) is { Changed: true } draft)
                        { field.Value = draft.Value.DeepClone(); field.HasUnresolvedChoice = draft.HasUnresolvedChoice; field.ChoiceQuery = draft.ChoiceQuery; }
        version = data.Value<string>("version"); permissionVersion = data.Value<string>("permissionsRevision") ?? permissionVersion;
        Title = data.Value<string>("selectedName") ?? Title;
        Summary = data.Value<string>("summary") ?? ""; HasConflict = false;
        Tabs = prepared.GroupBy(x => Group(x.Key)).OrderBy(x => x.Key.Order)
            .Select(x => new EquipmentTab(x.Key.Key, UiText.Translate(x.Key.Title), x.ToArray())).ToArray();
        Message = prepared.Length == 0 ? UiText.Translate("管理员未允许编辑此物品。") : "";
        SaveCommand.NotifyCanExecuteChanged();
    }

    private static (int Order, string Key, string Title) Group(string action) => action switch
    {
        "rename" or "experience" or "max" or "forma" or "mod.rank" or "mature" => (0, "basic", "基础属性"),
        "features" => (1, "features", "装备特性"),
        var key when key.StartsWith("shards.", StringComparison.Ordinal) => (2, "shards", "源力石"),
        "umbra" or "umbra.remove" => (3, "umbra", "Umbra 回响"),
        "invigoration" or "invigoration.remove" => (4, "invigoration", "活力注入"),
        var key when key.StartsWith("ability.multiple.", StringComparison.Ordinal) => (5, "experimental-abilities", "多技能实验"),
        var key when key.StartsWith("ability.", StringComparison.Ordinal) => (5, "abilities", "移植技能"),
        "parts" or "valence" => (6, "parts", "部件与元素"),
        "remove" => (99, "manage", "物品管理"),
        _ => (7, "other", "其他编辑")
    };

    private Task SaveAsync(EquipmentAction? action) => RunAsync(async () =>
    {
        if (action is null || target is null || version is null || HasConflict || !Actions.Contains(action)) return;
        var values = new JObject(action.Fields.Select(x => new JProperty(x.Key, x.Serialize())));
        if ((action.Key is "remove" or "shards.remove" or "umbra.remove" or "invigoration.remove") &&
            !await confirm(action.Label, target.AccountName + " · " + Title + "\n" + action.Description)) return;
        await save(target, new JObject { ["version"] = version, ["category"] = target.Category, ["item"] = target.Item, ["action"] = action.Key, ["values"] = values });
        await ReadAsync(true, action.Key);
        notify(UiText.Translate("存档已保存"));
    });

    private void Reset(EquipmentAction? action)
    {
        if (action is null || !Actions.Contains(action)) return;
        foreach (var field in action.Fields) field.Reset();
        OnPropertyChanged(nameof(Tabs));
    }
    public Task<bool> CanDiscardAsync() => HasChanges
        ? confirm(UiText.Translate("放弃未保存的修改?"), UiText.Translate("当前装备有未保存的修改。继续后将丢弃这些输入。"))
        : Task.FromResult(true);

    public async Task CheckChangesAsync()
    {
        if (!changes.Active || IsBusy || target is null) return;
        var observed = version; var selected = target; var pageGeneration = changes.Generation;
        try
        {
            var current = await revision(selected);
            if (!changes.Active || IsBusy || target != selected || observed != version || pageGeneration != changes.Generation) return;
            if (!current.Enabled) { Revoke(); return; }
            if (version is null) { await RunAsync(() => ReadAsync(false)); return; }
            if (permissionVersion is not null && permissionVersion != current.Permissions)
            {
                var previousVersion = version;
                var existingConflict = HasConflict;
                await RunAsync(() => ReadAsync(true)); // Rebuild from the server's newly filtered action set.
                // The save can change after the revision request but before the actual read completes.
                // ReadAsync advances the permission version only on success so a failed refresh is retried.
                if (HasChanges && (existingConflict || current.Version != previousVersion || version != previousVersion))
                {
                    HasConflict = true;
                    Message = UiText.Translate("存档已在其他位置更新,输入已保留。请刷新后再保存。");
                }
                return;
            }
            permissionVersion = current.Permissions;
            if (current.Version == version) return;
            if (HasChanges)
            {
                HasConflict = true;
                Message = UiText.Translate("存档已在其他位置更新,输入已保留。请刷新后再保存。");
            }
            else await RunAsync(() => ReadAsync(false));
        }
        catch (OperationCanceledException) { }
        catch { if (changes.Active && target == selected) Message = UiText.Translate("连接中断,当前输入已保留。"); }
    }

    public void Revoke(bool forgetTarget = false)
    {
        generation++; Tabs = []; version = null; HasConflict = true;
        if (forgetTarget) target = null;
        Message = UiText.Translate("编辑权限已关闭或账号已退出,请返回库存列表。");
    }

    private async Task RunAsync(Func<Task> operation)
    {
        if (IsBusy) return;
        IsBusy = true; Message = "";
        try { await operation(); }
        catch (OperationCanceledException) { }
        catch (Exception ex) { Message = ex.Message; }
        finally { IsBusy = false; }
    }
}