Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7241e9c
feat: 定制 MAP 首页与总控台布局
NotZoruak Oct 17, 2026
f9fded7
feat: 自适应设置页布局
NotZoruak Oct 17, 2026
4001a2a
feat: 重构任务全局设置与全选操作
NotZoruak Oct 17, 2026
0519d0e
feat: 改进教程重播与动态高亮
NotZoruak Oct 17, 2026
e55f3ed
修复任务运行稳定性
NotZoruak Oct 18, 2026
c9883a2
维护 MaaFramework 调试日志
NotZoruak Oct 18, 2026
1376bed
增强 MuMu 设备连接兼容性
NotZoruak Oct 18, 2026
020ea35
处理定时任务设备交接
NotZoruak Oct 18, 2026
b8f6281
添加 Focus 外部通知渠道
NotZoruak Oct 18, 2026
96bcd66
feat: 增加任务卡死自动恢复
NotZoruak Oct 18, 2026
06aaf3f
feat: 固定启用卡死恢复并恢复高级选项页
NotZoruak Oct 18, 2026
f87b376
feat: 支持按接口配置下拉搜索
NotZoruak Oct 18, 2026
ec8fffc
feat: 将轻量识别加入运行设置
NotZoruak Oct 18, 2026
84e16ac
调整首页任务卡片与任务项布局
NotZoruak Oct 18, 2026
3938965
修复轻量识别设置项布局
NotZoruak Oct 18, 2026
cb8693c
调整任务子选项下拉框布局
NotZoruak Oct 18, 2026
80e62eb
更新应用狐狸图标
NotZoruak Oct 18, 2026
8530baf
为狐狸图标添加单圈描边
NotZoruak Oct 18, 2026
6be7bff
修复任务说明卡片宽度
NotZoruak Oct 18, 2026
3f3ae43
优化狐狸图标小尺寸清晰度
NotZoruak Oct 18, 2026
4c6c72e
记录任务轮次进度并释放 Agent 客户端
NotZoruak Oct 18, 2026
bd7301f
feat: 添加作战策略编辑器
NotZoruak Oct 19, 2026
05e8a8a
修复实时画面首帧误报
NotZoruak Oct 19, 2026
c706410
feat: 支持自定义动作提前结束重复任务
NotZoruak Oct 19, 2026
2b5633e
feat: 将发布程序名称改为 MaaPazugiri
NotZoruak Oct 19, 2026
3d05f9d
fix: 简化应用窗口标题
NotZoruak Oct 19, 2026
286906f
feat: 完善策略编辑器界面与交互
NotZoruak Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ build_output.txt
/test_interface/

# Local-only verification project
/MFAAvalonia.Tests/
/MFAAvalonia.Tests/*
# 策略编辑器的回归测试随功能保留,其他本地核对文件继续忽略。
!/MFAAvalonia.Tests/MFAAvalonia.Tests.csproj
!/MFAAvalonia.Tests/CombatStrategy*Tests.cs
/MFAAvalonia/AssemblyInfo.cs
workflows/android/__pycache__/mfa_android_agent.cpython-312.pyc
# MAP 本地定制迁移设计文档
/docs/map定制迁移/
4 changes: 2 additions & 2 deletions MFAAvalonia.Desktop/MFAAvalonia.Desktop.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<AssemblyName>MFAAvalonia</AssemblyName>
<OutputName>MFAAvalonia</OutputName>
<AssemblyName>MaaPazugiri</AssemblyName>
<OutputName>MaaPazugiri</OutputName>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
Expand Down
2 changes: 1 addition & 1 deletion MFAAvalonia.Desktop/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static void Main(string[] args)

try
{
var executablePath = Environment.ProcessPath ?? Path.Combine(AppContext.BaseDirectory, "MFAAvalonia.exe");
var executablePath = Environment.ProcessPath ?? Path.Combine(AppContext.BaseDirectory, "MaaPazugiri.exe");
var instanceKey = AppRuntime.CreateInstanceKey(executablePath);
AppRuntime.Initialize(args, instanceKey);
TelemetryService.InitializeBootstrapFromInterface(AppContext.BaseDirectory);
Expand Down
93 changes: 93 additions & 0 deletions MFAAvalonia.Tests/CombatStrategyControlsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Input;
using MFAAvalonia.Features.CombatStrategies;
using MFAAvalonia.Features.CombatStrategies.Controls;
using System.Linq;
using System;
using System.IO;
using System.Text.Json;
using MFAAvalonia.Views.Pages;
using Xunit;

namespace MFAAvalonia.Tests;

[Collection("策略界面")]
public sealed class CombatStrategyControlsTests
{
[Theory]
[InlineData(0, 60, 0)]
[InlineData(0, 150, 0)]
[InlineData(0, 190, 1)]
[InlineData(0, 350, 2)]
[InlineData(2, 20, 0)]
[InlineData(2, 140, 1)]
public void DragDrop_AccountsForRemovedSourceRow(int from, double y, int expected)
{
var reorder = typeof(StrategyStageCard).Assembly.GetType("MFAAvalonia.Features.CombatStrategies.Controls.StrategyReorder")!;
var calculate = reorder.GetMethod("CalculateDropIndex", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;
Avalonia.Rect[] rows = [new(0, 0, 200, 100), new(0, 110, 200, 100), new(0, 220, 200, 100)];
Assert.Equal(expected, calculate.Invoke(null, [rows, from, y]));
}

[Fact]
public void PageControls_SaveStageAndUltimateEditsToRealJson()
{
var directory = Path.Combine(Path.GetTempPath(), "map-page-tests", Guid.NewGuid().ToString("N"));
try
{
var session = new StrategyEditorSession(new StrategyStore(directory));
session.Create("界面策略");
var view = new CombatStrategyView(session);
view.FindControl<Button>("AddStage")!.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
Assert.Equal(2, session.Current.Stages.Count);
var cards = view.FindControl<StackPanel>("StageCards")!;
var firstCard = cards.Children.OfType<StrategyStageCard>().First();
firstCard.DragHandle.RaiseEvent(new KeyEventArgs { RoutedEvent = Avalonia.Input.InputElement.KeyDownEvent, Key = Key.Down });
Assert.Equal(session.Current.InitialStageId, session.Current.Stages[1].Id);
var initialCard = cards.Children.OfType<StrategyStageCard>().Last();
var slot = initialCard.GetLogicalDescendants().OfType<CheckBox>().First(x => Equals(x.Content, "位置 2"));
slot.IsChecked = false;
view.FindControl<Button>("SaveStrategy")!.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
var restored = new StrategyStore(directory).Load(session.Current.Id);
Assert.Equal(2, restored.Stages.Count);
Assert.False(restored.Stages[1].Actions.Ultimates.Single(x => x.Slot == 2).Enabled);
Assert.False(session.HasUnsavedChanges);
}
finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); }
}

[Fact]
public void ConditionControls_WriteNumericValuesAndNestedGroupsToDraft()
{
var changed = 0;
var root = new ConditionGroup
{
Children = [new ConditionPredicate { Metric = ConditionMetric.StageNormalLinks, Comparison = ComparisonOperator.GreaterOrEqual, Value = 1 }]
};
var editor = new StrategyConditionEditor(root, () => changed++, ownerSlot: 3);
var number = editor.GetLogicalDescendants().OfType<NumericUpDown>().Single();
number.Value = 2;
Assert.Equal(2, Assert.IsType<ConditionPredicate>(root.Children[0]).Value);
var addGroup = editor.GetLogicalDescendants().OfType<Button>().First(x => x.Name == "AddConditionGroup");
addGroup.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
Assert.Equal(2, root.Children.Count);
Assert.IsType<ConditionGroup>(root.Children[1]);
Assert.True(changed >= 2);
}

[Fact]
public void RootCondition_ContainsExplicitTypeForPythonConsumer()
{
var document = StrategyDefaults.CreateEditable("条件契约");
document.Stages[0].Actions.Ultimates[0].Condition = new ConditionGroup
{
Children = [new ConditionPredicate { Metric = ConditionMetric.EnemyHp, Comparison = ComparisonOperator.LessOrEqual, Value = 10000 }]
};
using var json = JsonDocument.Parse(StrategyJson.Serialize(document));
var condition = json.RootElement.GetProperty("stages")[0].GetProperty("actions").GetProperty("ultimates")[0].GetProperty("condition");
Assert.Equal("group", condition.GetProperty("type").GetString());
}
}
52 changes: 52 additions & 0 deletions MFAAvalonia.Tests/CombatStrategyDialogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Avalonia.Controls;
using MFAAvalonia.Views.Pages;
using SukiUI.Controls;
using SukiUI.MessageBox;
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Xunit;

namespace MFAAvalonia.Tests;

[Collection("策略界面")]
public sealed class CombatStrategyDialogTests
{
[Theory]
[InlineData("删除", true)]
[InlineData("取消", false)]
[InlineData(null, false)]
public async Task DeleteConfirmation_UsesMessageBoxResult(string? action, bool expected)
{
var result = await Invoke<bool>("ConfirmDeleteAsync", "测试策略", Presenter(action));
Assert.Equal(expected, result);
}

[Theory]
[InlineData("确定", "新名称")]
[InlineData("取消", null)]
[InlineData(null, null)]
public async Task RenamePrompt_UsesMessageBoxResult(string? action, string? expected)
{
Assert.Equal(expected, await Invoke<string?>("RenameAsync", "重命名", "旧名称", Presenter(action, " 新名称 ")));
}

private static Func<SukiMessageBoxHost, SukiMessageBoxOptions, Task<object?>> Presenter(string? action, string? name = null) =>
(host, _) =>
{
if (name != null) Assert.IsType<TextBox>(host.Content).Text = name;
if (action == null) return Task.FromResult<object?>(null);
var button = host.ActionButtonsSource!.Single(x => Equals(x.Content, action));
// 弹窗库会原样返回标准结果,自定义标签则返回按钮本身。
var result = Assert.IsType<SukiMessageBoxResult>(button.Tag);
return Task.FromResult<object?>(result);
};

private static Task<T> Invoke<T>(string name, params object?[] arguments)
{
var dialogs = typeof(CombatStrategyView).Assembly.GetType("MFAAvalonia.Features.CombatStrategies.Controls.StrategyDialogs")!;
var method = dialogs.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static)!;
return (Task<T>)method.Invoke(null, arguments)!;
}
}
126 changes: 126 additions & 0 deletions MFAAvalonia.Tests/CombatStrategyEditorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MFAAvalonia.Features.CombatStrategies;
using Xunit;

namespace MFAAvalonia.Tests;

public sealed class CombatStrategyEditorTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "map-editor-tests", Guid.NewGuid().ToString("N"));

[Fact]
public async Task Cancel_KeepsCurrentDraftAndUnsavedChanges()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("未保存");
var id = session.Current.Id;
Assert.False(await session.TrySwitchAsync("default", () => Task.FromResult(UnsavedDecision.Cancel)));
Assert.Equal(id, session.Current.Id);
Assert.True(session.HasUnsavedChanges);
}

[Fact]
public async Task SaveBeforeSwitch_ReloadsEditedDataFromDisk()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("保存策略");
var id = session.Current.Id;
session.Current.Stages[0].Name = "准备";
session.MarkDirty();
Assert.True(await session.TrySwitchAsync("default", () => Task.FromResult(UnsavedDecision.Save)));
Assert.False(session.HasUnsavedChanges);
session.Select(id);
Assert.Equal("准备", session.Current.Stages[0].Name);
}

[Fact]
public async Task Discard_RestoresSavedDocumentAndRemovesNewDraft()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("策略");
session.Save();
session.Current.Name = "不应保留";
session.MarkDirty();
Assert.True(await session.TryLeaveAsync(() => Task.FromResult(UnsavedDecision.Discard)));
Assert.Equal("策略", session.Current.Name);
session.Create("临时策略");
var unsavedId = session.Current.Id;
Assert.True(await session.TryLeaveAsync(() => Task.FromResult(UnsavedDecision.Discard)));
Assert.DoesNotContain(session.Catalog.Strategies, x => x.Id == unsavedId);
}

[Fact]
public async Task SaveFailure_DoesNotSwitchOrClearDirtyState()
{
Directory.CreateDirectory(Path.GetDirectoryName(_directory)!);
File.WriteAllText(_directory, "阻止创建策略目录");
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("保存失败");
var id = session.Current.Id;
Assert.False(await session.TrySwitchAsync("default", () => Task.FromResult(UnsavedDecision.Save)));
Assert.Equal(id, session.Current.Id);
Assert.True(session.HasUnsavedChanges);
Assert.False(string.IsNullOrWhiteSpace(session.Error));
}

[Fact]
public void CopyStrategy_RemapTargetsAndKeepOriginalIndependent()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("来源");
var first = session.Current.Stages[0];
var second = session.AddStage();
first.Transitions.Add(new StageTransition
{
TargetStageId = second.Id,
Condition = new ConditionGroup { Children = [new ConditionPredicate { Metric = ConditionMetric.FrenzyActive, Expected = true }] }
});
session.Save();
var sourceId = session.Current.Id;
var oldStageIds = session.Current.Stages.Select(x => x.Id).ToArray();
session.Duplicate();
Assert.NotEqual(sourceId, session.Current.Id);
Assert.DoesNotContain(session.Current.Stages[0].Id, oldStageIds);
Assert.Equal(session.Current.Stages[1].Id, session.Current.Stages[0].Transitions[0].TargetStageId);
Assert.Equal(session.Current.Stages[0].Id, session.Current.InitialStageId);
session.Current.Stages[0].Name = "修改副本";
Assert.NotEqual("修改副本", new StrategyStore(_directory).Load(sourceId).Stages[0].Name);
}

[Fact]
public void StageDelete_ProtectsReferencesAndInitialStage()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
session.Create("引用");
var first = session.Current.Stages[0];
var second = session.AddStage();
first.Transitions.Add(new StageTransition { TargetStageId = second.Id });
Assert.Throws<InvalidOperationException>(() => session.DeleteStage(first));
Assert.Throws<InvalidOperationException>(() => session.DeleteStage(second));
first.Transitions.Clear();
session.DeleteStage(second);
Assert.Single(session.Current.Stages);
}

[Fact]
public void DefaultCannotBeEditedButCanBeCopied()
{
var session = new StrategyEditorSession(new StrategyStore(_directory));
Assert.True(session.IsReadOnly);
Assert.Throws<InvalidOperationException>(() => session.AddStage());
session.Duplicate();
Assert.False(session.IsReadOnly);
Assert.True(session.HasUnsavedChanges);
session.Current.Stages[0].Name = "自定义";
Assert.Equal("全程作战", StrategyDefaults.Create().Stages[0].Name);
}

public void Dispose()
{
if (Directory.Exists(_directory)) Directory.Delete(_directory, true);
if (File.Exists(_directory)) File.Delete(_directory);
}
}
80 changes: 80 additions & 0 deletions MFAAvalonia.Tests/CombatStrategyMenuTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using MFAAvalonia.Features.CombatStrategies;
using MFAAvalonia.Views.Pages;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace MFAAvalonia.Tests;

[CollectionDefinition("策略界面", DisableParallelization = true)]
public sealed class CombatStrategyUiCollection { }

[Collection("策略界面")]
public sealed class CombatStrategyMenuTests
{
[Theory]
[InlineData("删除", false, false)]
[InlineData("重命名", false, false)]
[InlineData("删除", true, false)]
[InlineData("重命名", true, false)]
[InlineData("删除", false, true)]
[InlineData("重命名", false, true)]
[InlineData("删除", true, true)]
[InlineData("重命名", true, true)]
public void ContextMenu_ClickPromptsAndChangesTheClickedStrategy(string action, bool requestAlreadyHandled, bool isSelected)
{
var directory = Path.Combine(Path.GetTempPath(), "map-menu-tests", Guid.NewGuid().ToString("N"));
try
{
var store = new StrategyStore(directory);
var document = StrategyDefaults.CreateEditable("右键目标");
store.Save(document);
var session = new StrategyEditorSession(store);
if (isSelected) session.Select(document.Id);
var prompted = false;
var view = new CombatStrategyView(session,
(_, name) => { Assert.Equal(document.Name, name); prompted = true; return Task.FromResult<string?>("修改后的名称"); },
name => { Assert.Equal(document.Name, name); prompted = true; return Task.FromResult(true); });
var target = session.Catalog.Strategies.Single(x => x.Id == document.Id);
var list = view.FindControl<ListBox>("StrategyList")!;
var row = list.ItemTemplate!.Build(target)!;
row.DataContext = target;
var menu = row.ContextMenu!;
list.Template = new FuncControlTemplate<ListBox>((_, _) => new StackPanel { Children = { row } });
list.ApplyTemplate();
Assert.Contains(list, row.GetVisualAncestors());
// 触发真实菜单打开事件,同时阻止创建依赖桌面平台的弹出窗口。
menu.Opening += (_, e) => e.Cancel = true;
// 模拟框架先自动打开菜单并将请求标记为已处理。
using var consumeRequest = Control.ContextRequestedEvent.AddClassHandler<Border>((sender, e) =>
{
if (requestAlreadyHandled && ReferenceEquals(sender, row)) e.Handled = true;
});
row.GetVisualDescendants().OfType<TextBlock>().Single()
.RaiseEvent(new ContextRequestedEventArgs { RoutedEvent = Control.ContextRequestedEvent });
var item = menu.Items.OfType<MenuItem>().Single(x => Equals(x.Header, action));
item.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
Assert.True(prompted);
Assert.Equal(isSelected && action != "删除" ? document.Id : "default", session.Current.Id);
if (action == "删除")
Assert.DoesNotContain(store.List().Strategies, x => x.Id == document.Id);
else
{
if (isSelected)
{
Assert.Equal("修改后的名称", session.Current.Name);
Assert.True(session.HasUnsavedChanges);
view.FindControl<Button>("SaveStrategy")!.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
Assert.Equal("修改后的名称", store.Load(document.Id).Name);
}
}
finally { if (Directory.Exists(directory)) Directory.Delete(directory, true); }
}
}
Loading