diff --git a/.github/issue-checker.yml b/.github/issue-checker.yml index de52794af7..dd9515c835 100644 --- a/.github/issue-checker.yml +++ b/.github/issue-checker.yml @@ -252,7 +252,7 @@ labels: # `module: fight` - name: fight content: "module: fight" - regexes: "刷图|刷理智|剿灭|刷.{0,6}材料|关卡选择|战斗|作战|[戰戦][鬥闘]|[Ff]ight|[Bb]attle|[Aa]nihilation" + regexes: "刷图|理智|剿灭|刷.{0,6}材料|关卡选择|战斗|作战|[戰戦][鬥闘]|[Ff]ight|[Bb]attle|[Aa]nihilation" skip-if: - skip all - skip module diff --git a/README.md b/README.md index af7f54d42d..7dae315680 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ MAA 的意思是 MAA Assistant Arknights ## 亮点功能 -- 刷理智,掉落识别及上传 [企鹅物流](https://penguin-stats.cn/),[一图流](https://ark.yituliu.cn/) +- 理智作战,掉落识别及上传 [企鹅物流](https://penguin-stats.cn/),[一图流](https://ark.yituliu.cn/) - 智能基建换班,自动计算干员效率,单设施内最优解;同时也支持 [自定义排班](https://docs.maa.plus/zh-cn/protocol/base-scheduling-schema.html) - 自动公招,可选使用加急许可,一次全部刷完!公招数据自动上传 [企鹅物流](https://penguin-stats.cn/result/stage/recruit/recruit),[一图流](https://ark.yituliu.cn/survey/maarecruitdata) - 支持手动识别公招界面,方便对高星公招做出选择 ~~(你的这个高资回费出的是推王呢还是推王呢)~~ diff --git a/docs/zh-cn/manual/introduction/combat.md b/docs/zh-cn/manual/introduction/combat.md index 6be3719f43..009ca32bda 100644 --- a/docs/zh-cn/manual/introduction/combat.md +++ b/docs/zh-cn/manual/introduction/combat.md @@ -3,22 +3,22 @@ order: 3 icon: hugeicons:brain-02 --- -# 刷理智 +# 理智作战 ## 常规设置 -- `吃理智药` + `吃源石` 和 `指定次数`、`指定材料` 三个选项为短路开关(或门),即达成三个选项中的任一条件,均会视为任务完成,停止刷理智。 - - `吃理智药` 指定补充几次理智(可能一次吃多瓶药)。 - - `吃源石` 指定碎几颗石头(一次一颗),当仓库中有理智药时不会碎石。 +- `药剂恢复` + `源石恢复` 和 `指定次数`、`指定材料` 三个选项为短路开关(或门),即达成三个选项中的任一条件,均会视为任务完成,停止理智作战。 + - `药剂恢复` 指定补充几瓶理智(可能一次吃多瓶药)。 + - `源石恢复` 指定碎几颗石头(一次一颗),当仓库中有理智药时不会碎石。 - `指定次数` 指定刷多少次指定关卡(例如“刷 15 次后停止”)。 - `指定材料` 指定刷多少个指定材料(例如“获取 5 个固源岩后停止”)。 - `指定材料` 与 `关卡选择` 是两个互相独立的逻辑。`指定材料` 只是以材料个数作为任务完成依据,并不会自动导航到相应关卡。 -- `吃源石` 只会在 `吃理智药` 之后判断,因为 MAA 只有在没有理智药时才会使用源石补充理智。所以在勾选 `吃源石` 之后,MAA 会将 `吃理智药` 的次数锁定为 999,确保将理智药吃光,以避免跳过 `吃源石` 判断。 +- `源石恢复` 只会在 `药剂恢复` 之后判断,因为 MAA 只有在没有理智药时才会使用源石补充理智。所以在勾选 `源石恢复` 之后,MAA 会将 `药剂恢复` 的次数锁定为 999,确保将理智药吃光,以避免跳过 `源石恢复` 判断。 ::: details 例子 -| 吃理智药 | 吃源石 | 指定次数 | 指定材料 | 结果 | +| 药剂恢复 | 源石恢复 | 指定次数 | 指定材料 | 结果 | | :------: | :----: | :------: | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | | | | | 刷完现有理智即结束。 | | 2 | | | | 先刷完现有理智,然后吃一次理智药,一共吃 `2` 次,刷完理智后结束。 | @@ -39,7 +39,7 @@ icon: hugeicons:brain-02 - 若关卡选择中没有你需要的关卡,请在 MAA 中选择 `当前/上次`,然后在游戏中手动定位关卡 确保画面停留在 右上角有关卡名和剩余理智,右下角有代理指挥和开始行动 的关卡详情界面。 - 若当前不处于此界面,则 `当前/上次` 会自动进入终端首页右下角“上次作战”的关卡。 -- 也可以在 `任务设置` - `刷理智` - `高级设置` 中启用 `手动输入关卡名` 手动输入关卡编号。目前支持导航的关卡有: +- 也可以在 `任务设置` - `理智作战` - `高级设置` 中启用 `手动输入关卡名` 手动输入关卡编号。目前支持导航的关卡有: - 全部主线关卡。可在关卡末尾添加 `-NORMAL` 或 `-HARD` 来切换标准或磨难关卡。 - 龙门币、作战记录的 5 / 6 关,但必须输入 `CE-6` / `LS-6`。MAA 会在第六关无法代理的情况下自动切换至第五关。 - 技能书、采购凭证、碳本第 5 关,必须输入 `CA-5` / `AP-5` / `SK-5`。 @@ -71,21 +71,20 @@ icon: hugeicons:brain-02 这是一个类似于日程表的功能,并不能当做关卡选择任务失败时的备用关卡。 1. 例子:关卡选择 `龙门币-6/5`,备选选择 `1-7` 和 `经验-6/5`: - - 如果当天开放 `龙门币-6/5`,就会前往 `龙门币-6/5`,不会前往 `1-7` 和 `经验-6/5`。如果玩家此时未解锁 `龙门币-6/5` 代理,则刷理智任务出错。 - - 如果当天未开放 `龙门币-6/5`,则会前往 `1-7`,不会前往 `龙门币-6/5`。如果玩家此时未解锁 1-7 代理,则刷理智任务出错。 + - 如果当天开放 `龙门币-6/5`,就会前往 `龙门币-6/5`,不会前往 `1-7` 和 `经验-6/5`。如果玩家此时未解锁 `龙门币-6/5` 代理,则理智作战任务出错。 + - 如果当天未开放 `龙门币-6/5`,则会前往 `1-7`,不会前往 `龙门币-6/5`。如果玩家此时未解锁 1-7 代理,则理智作战任务出错。 - 由于 `经验-6/5` 前存在常驻关卡 `1-7`,在这种情况下,MAA 永远也不会前往 `经验-6/5` 战斗。 2. 若关卡选择为 `剿灭模式`,则: - - 不会因为剿灭的结果影响其余备选关卡的选择逻辑,即使剿灭出错,刷理智任务也不会出错。 - - 剩余的备选关卡只会继承 `吃理智药` 和 `代理倍率` 的设置,不受 `吃源石`、`指定次数`、`指定材料` 的控制。 + - 不会因为剿灭的结果影响其余备选关卡的选择逻辑,即使剿灭出错,理智作战任务也不会出错。 + - 剩余的备选关卡只会继承 `药剂恢复` 和 `代理倍率` 的设置,不受 `源石恢复`、`指定次数`、`指定材料` 的控制。 ### 剩余理智 -在 `刷理智` 任务结束后启动,不受 `吃理智药`、`吃源石`、`指定次数`、`指定材料`、`代理倍率` 的控制,刷完当前剩余理智即结束。 - +在 `理智作战` 任务结束后启动,不受 `药剂恢复`、`源石恢复`、`指定次数`、`指定材料`、`代理倍率` 的控制,刷完当前剩余理智即结束。 - 适用于在 `关卡选择` 关卡理智不足后,继续前往 `剩余理智` 关卡清理剩余的“边角”理智(如前往 1-7)。 - 亦适用于在代理倍率设置过高而理智不足时自动以单次连战刷光理智收尾(设置 1-7 连战 6 次,但只有 30 理智,于是自动转为刷 5 次不连战的 1-7)。 - 若剩余理智仍然不足则会结束任务(如少于 6 理智)。 -- 如果剩余理智选择关卡为未开放关卡,则刷理智任务出错。 +- 如果剩余理智选择关卡为未开放关卡,则理智作战任务出错。 ### 代理倍率 diff --git a/docs/zh-cn/manual/introduction/credit.md b/docs/zh-cn/manual/introduction/credit.md index 1bf8584fd5..5a392c634a 100644 --- a/docs/zh-cn/manual/introduction/credit.md +++ b/docs/zh-cn/manual/introduction/credit.md @@ -3,7 +3,7 @@ order: 5 icon: solar:shop-2-bold --- -# 获取信用及购物 +# 信用收支 - 会自动访问好友获取信用点,并前往信用交易所购物。 - 借助战赚信用: diff --git a/docs/zh-cn/protocol/callback-schema.md b/docs/zh-cn/protocol/callback-schema.md index 7f954ff57f..3eaebe0212 100644 --- a/docs/zh-cn/protocol/callback-schema.md +++ b/docs/zh-cn/protocol/callback-schema.md @@ -162,7 +162,7 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom - `CloseDown` 关闭游戏 - `Fight` - 刷理智 + 理智作战 - `Mall` 信用点及购物 - `Recruit` diff --git a/docs/zh-cn/protocol/integration.md b/docs/zh-cn/protocol/integration.md index c532d6cdca..e7ebc4b164 100644 --- a/docs/zh-cn/protocol/integration.md +++ b/docs/zh-cn/protocol/integration.md @@ -82,7 +82,7 @@ B服:`张三`,可输入 `张三`、`张`、`三` :::: - `Fight` - 刷理智 + 理智作战 :::: field-group ::: field name="enable" type="boolean" optional default="true" diff --git a/docs/zh-cn/protocol/task-schema.md b/docs/zh-cn/protocol/task-schema.md index 8938eaeadf..51310a9bda 100644 --- a/docs/zh-cn/protocol/task-schema.md +++ b/docs/zh-cn/protocol/task-schema.md @@ -84,7 +84,7 @@ JSON 文件是不支持注释的,文本中的注释仅用于演示,请勿直 "reduceOtherTimes": ["OtherTaskName1", "OtherTaskName2"], // 可选项,执行后减少其他任务的执行计数。 - // 例如执行了吃理智药,则说明上一次点击蓝色开始行动按钮没生效,所以蓝色开始行动要-1 + // 例如执行了药剂恢复,则说明上一次点击蓝色开始行动按钮没生效,所以蓝色开始行动要-1 "specificRect": [100, 100, 50, 50], // 当 action 为 ClickRect 时有效且必选,表示指定的点击位置(范围内随机一点)。 diff --git a/docs/zh-cn/readme.md b/docs/zh-cn/readme.md index 83a1faab53..d4a02d0a24 100644 --- a/docs/zh-cn/readme.md +++ b/docs/zh-cn/readme.md @@ -36,7 +36,7 @@ MAA 的意思是 MAA Assistant Arknights ## 亮点功能 -- 刷理智,掉落识别及上传 [企鹅物流](https://penguin-stats.cn/),[一图流](https://ark.yituliu.cn/) +- 理智作战,掉落识别及上传 [企鹅物流](https://penguin-stats.cn/),[一图流](https://ark.yituliu.cn/) - 智能基建换班,自动计算干员效率,单设施内最优解;同时也支持 [自定义排班](./protocol/base-scheduling-schema.md) - 自动公招,可选使用加急许可,一次全部刷完!公招数据自动上传 [企鹅物流](https://penguin-stats.cn/result/stage/recruit/recruit),[一图流](https://ark.yituliu.cn/survey/maarecruitdata) - 支持手动识别公招界面,方便对高星公招做出选择 ~~(你的这个高资回费出的是推王呢还是推王呢)~~ diff --git a/src/Golang/maa/task.go b/src/Golang/maa/task.go index 5c06cd3284..3528057f2e 100644 --- a/src/Golang/maa/task.go +++ b/src/Golang/maa/task.go @@ -5,7 +5,7 @@ type TaskID int const ( StartUp TaskType = iota //开始唤醒 - Fight //刷理智 + Fight //理智作战 Recruit //公开招募 Infrast //基建换班 Visit //访问好友 diff --git a/src/Java/Readme.md b/src/Java/Readme.md index 847f3209b8..3134f96050 100644 --- a/src/Java/Readme.md +++ b/src/Java/Readme.md @@ -171,7 +171,7 @@ | 字段名称 | 字段说明 | 类型 | 必填 | 备注 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---- | ---- | | id | 创建连接返回的实例ID | String | Y | - | -| type | 任务类型,此次示例为刷理智战斗,详情参考[集成文档](https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/master/docs/集成文档.md) | String | Y | - | +| type | 任务类型,此次示例为理智作战战斗,详情参考[集成文档](https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/master/docs/集成文档.md) | String | Y | - | | params | 任务参数 | Json | Y | - | ###### 4) 请求返回结果 @@ -223,7 +223,7 @@ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---- | ---- | | id | 创建连接返回的实例ID | String | Y | - | | taskId | 任务ID | Int | Y | - | -| type | 任务类型,此次示例为刷理智战斗,详情参考[集成文档](https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/master/docs/集成文档.md) | String | Y | - | +| type | 任务类型,此次示例为理智作战战斗,详情参考[集成文档](https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/master/docs/集成文档.md) | String | Y | - | | params | 任务参数 | Json | Y | - | ###### 4) 请求返回结果 diff --git a/src/MaaCore/Common/AsstTypes.h b/src/MaaCore/Common/AsstTypes.h index 1042e13aa2..50bf4e852c 100644 --- a/src/MaaCore/Common/AsstTypes.h +++ b/src/MaaCore/Common/AsstTypes.h @@ -593,7 +593,7 @@ struct TaskPipelineInfo TaskList sub; // 子任务(列表) TaskList on_error_next; // 任务出错之后要去执行什么 TaskList exceeded_next; // 达到最多次数了之后,下一个可能的任务(列表) - TaskList reduce_other_times; // 执行了该任务后,需要减少别的任务的执行次数。例如执行了吃理智药, + TaskList reduce_other_times; // 执行了该任务后,需要减少别的任务的执行次数。例如执行了药剂恢复, // 则说明上一次点击蓝色开始行动按钮没生效,所以蓝色开始行动要-1 }; diff --git a/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp b/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp index eba126f31a..8112da0a0d 100644 --- a/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp +++ b/src/MaaCore/Task/Fight/FightTimesTaskPlugin.cpp @@ -161,7 +161,7 @@ std::optional asst::FightTimesTaskPlugin::change_series(int sanity_current, } auto ret = select_series(true); - if (!ret && m_is_medicine_exhausted) { // 药品用完, 且没有次数可用, 刷理智结束 + if (!ret && m_is_medicine_exhausted) { // 药品用完, 且没有次数可用, 理智作战结束 m_task_ptr->set_enable(false); } else if (!ret) { diff --git a/src/MaaCore/Task/Interface/CopilotTask.cpp b/src/MaaCore/Task/Interface/CopilotTask.cpp index 9f892037ee..0dae24ecad 100644 --- a/src/MaaCore/Task/Interface/CopilotTask.cpp +++ b/src/MaaCore/Task/Interface/CopilotTask.cpp @@ -69,7 +69,7 @@ bool asst::CopilotTask::set_params(const json::value& params) return false; } - bool use_sanity_potion = params.get("use_sanity_potion", false); // 是否吃理智药 + bool use_sanity_potion = params.get("use_sanity_potion", false); // 是否药剂恢复 bool with_formation = params.get("formation", false); // 是否使用自动编队 int formation_index = params.get("formation_index", 0); // 选择第几个编队,0为不选择 if (!params.contains("formation_index") && params.contains("select_formation")) { diff --git a/src/MaaCore/Task/ProcessTask.cpp b/src/MaaCore/Task/ProcessTask.cpp index 0d254a7003..e952d37fda 100644 --- a/src/MaaCore/Task/ProcessTask.cpp +++ b/src/MaaCore/Task/ProcessTask.cpp @@ -283,7 +283,7 @@ ProcessTask::NodeStatus ProcessTask::run_task(const HitDetail& hits) status()->set_number(Status::ProcessTaskLastTimePrefix + task_name, time(nullptr)); // 减少其他任务的执行次数 - // 例如,进入吃理智药的界面了,相当于上一次点蓝色开始行动没生效 + // 例如,进入药剂恢复的界面了,相当于上一次点蓝色开始行动没生效 // 所以要给蓝色开始行动的次数减一 for (const std::string& other_task : task->reduce_other_times) { if (int& v = m_exec_times[other_task]; v > 0) { diff --git a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs index 6c84506728..ae159fdb18 100644 --- a/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs +++ b/src/MaaWpfGui/Configuration/Single/MaaTask/FightTask.cs @@ -17,7 +17,7 @@ using static MaaWpfGui.Main.AsstProxy; namespace MaaWpfGui.Configuration.Single.MaaTask; /// -/// 刷理智 +/// 理智作战 /// public class FightTask : BaseTask { diff --git a/src/MaaWpfGui/Constants/ConfigurationKeys.cs b/src/MaaWpfGui/Constants/ConfigurationKeys.cs index c210255425..e6bce2df12 100644 --- a/src/MaaWpfGui/Constants/ConfigurationKeys.cs +++ b/src/MaaWpfGui/Constants/ConfigurationKeys.cs @@ -50,6 +50,7 @@ public static class ConfigurationKeys public const string CustomStageCode = "GUI.CustomStageCode"; public const string InverseClearMode = "GUI.InverseClearMode"; public const string WindowTitlePrefix = "GUI.WindowTitlePrefix"; + public const string MaxNumberOfLogThumbnails = "GUI.MaxNumberOfLogThumbnails"; public const string WindowTitleSelectShowList = "GUI.WindowTitleSelectShowList"; public const string SoberLanguage = "GUI.SoberLanguage"; public const string Cheers = "GUI.Cheers"; @@ -95,7 +96,7 @@ public static class ConfigurationKeys public const string EmulatorAddCommand = "Start.EmulatorAddCommand"; public const string EmulatorWaitSeconds = "Start.EmulatorWaitSeconds"; - // 仅在刷理智的任务中会生效,前缀需要修改 + // 仅在理智作战的任务中会生效,前缀需要修改 public const string AutoRestartOnDrop = "Start.AutoRestartOnDrop"; public const string StartsWithScript = "Start.StartsWithScript"; public const string EndsWithScript = "Start.EndsWithScript"; diff --git a/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs b/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs index bc5ab5399b..d3942d5fb8 100644 --- a/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs +++ b/src/MaaWpfGui/Helper/AchievementTrackerHelper.cs @@ -399,7 +399,7 @@ public class AchievementTrackerHelper : PropertyChangedBase [ #region 基础使用类 - BasicUsage(id: AchievementIds.SanitySpender1, group: AchievementIds.SanitySpenderGroup, target: 10), // 刷理智次数 + BasicUsage(id: AchievementIds.SanitySpender1, group: AchievementIds.SanitySpenderGroup, target: 10), // 理智作战次数 BasicUsage(id: AchievementIds.SanitySpender2, group: AchievementIds.SanitySpenderGroup, target: 100), BasicUsage(id: AchievementIds.SanitySpender3, group: AchievementIds.SanitySpenderGroup, target: 1000), diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index 93eb3a97e1..96c58156a2 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -224,10 +224,22 @@ public class AsstProxy return AsstGetImage(_handle); } + public BitmapImage? AsstGetImage(bool forceScreencap) + { + // UI 端有两类取图场景: + // - 首页预览/缩略图:直接取 core 的缓存帧即可(避免频繁主动截图) + // - 监控/诊断:需要强制触发一次截图以拿到“此刻”的帧 + if (forceScreencap) + { + MaaService.AsstAsyncScreencap(_handle, true); + } + + return AsstGetImage(_handle); + } + public BitmapImage? AsstGetFreshImage() { - MaaService.AsstAsyncScreencap(_handle, true); - return AsstGetImage(_handle); + return AsstGetImage(forceScreencap: true); } public static async Task AsstGetImageAsync(AsstHandle handle) @@ -240,10 +252,19 @@ public class AsstProxy return await AsstGetImageAsync(_handle); } + public async Task AsstGetImageAsync(bool forceScreencap) + { + if (forceScreencap) + { + MaaService.AsstAsyncScreencap(_handle, true); + } + + return await AsstGetImageAsync(_handle); + } + public async Task AsstGetFreshImageAsync() { - MaaService.AsstAsyncScreencap(_handle, true); - return await AsstGetImageAsync(_handle); + return await AsstGetImageAsync(forceScreencap: true); } // 需要外部调用 ArrayPool.Shared.Return(buffer) @@ -277,11 +298,20 @@ public class AsstProxy return AsstGetImageBgrData(_handle); } + public byte[]? AsstGetImageBgrData(bool forceScreencap) + { + if (forceScreencap) + { + MaaService.AsstAsyncScreencap(_handle, true); + } + + return AsstGetImageBgrData(_handle); + } + // 需要外部调用 ArrayPool.Shared.Return(buffer) public byte[]? AsstGetFreshImageBgrData() { - MaaService.AsstAsyncScreencap(_handle, true); - return AsstGetImageBgrData(_handle); + return AsstGetImageBgrData(forceScreencap: true); } // 需要外部调用 ArrayPool.Shared.Return(buffer) @@ -296,11 +326,20 @@ public class AsstProxy return await AsstGetImageBgrDataAsync(_handle); } + public async Task AsstGetImageBgrDataAsync(bool forceScreencap) + { + if (forceScreencap) + { + MaaService.AsstAsyncScreencap(_handle, true); + } + + return await AsstGetImageBgrDataAsync(_handle); + } + // 需要外部调用 ArrayPool.Shared.Return(buffer) public async Task AsstGetFreshImageBgrDataAsync() { - MaaService.AsstAsyncScreencap(_handle, true); - return await AsstGetImageBgrDataAsync(_handle); + return await AsstGetImageBgrDataAsync(forceScreencap: true); } public static WriteableBitmap WriteBgrToBitmap(byte[] bgrData, WriteableBitmap? targetBitmap) @@ -319,6 +358,61 @@ public class AsstProxy return targetBitmap; } + public const int ScreencapWidth = 1280; + public const int ScreencapHeight = 720; + public const int ScreencapChannels = 3; + + public static BitmapSource? CreateBgrBitmapSourceScaled(byte[] bgrData, int targetWidth, int targetHeight) + { + if (targetWidth <= 0 || targetHeight <= 0) + { + _logger.Warning("Invalid target size: {Width}x{Height}", targetWidth, targetHeight); + return null; + } + + const int SrcWidth = ScreencapWidth; + const int SrcHeight = ScreencapHeight; + const int SrcStride = SrcWidth * ScreencapChannels; + + if (bgrData.Length < SrcHeight * SrcStride) + { + _logger.Warning("Invalid bgrData size: {Size}, expected at least {ExpectedSize}", bgrData.Length, SrcHeight * SrcStride); + return null; + } + + int dstStride = targetWidth * ScreencapChannels; + + // 直接生成小图,避免先构建大图再缩放导致不必要的内存峰值。 + var dst = new byte[targetHeight * dstStride]; + for (int y = 0; y < targetHeight; y++) + { + int srcY = y * SrcHeight / targetHeight; + int srcRow = srcY * SrcStride; + int dstRow = y * dstStride; + for (int x = 0; x < targetWidth; x++) + { + int srcX = x * SrcWidth / targetWidth; + int srcIndex = srcRow + (srcX * ScreencapChannels); + int dstIndex = dstRow + (x * ScreencapChannels); + dst[dstIndex] = bgrData[srcIndex]; + dst[dstIndex + 1] = bgrData[srcIndex + 1]; + dst[dstIndex + 2] = bgrData[srcIndex + 2]; + } + } + + var bitmap = BitmapSource.Create( + targetWidth, + targetHeight, + 96, + 96, + PixelFormats.Bgr24, + null, + dst, + dstStride); + bitmap.Freeze(); + return bitmap; + } + private readonly MaaService.CallbackDelegate _callback; /// @@ -936,7 +1030,7 @@ public class AsstProxy Task.Run(async () => { var screenshot = await AsstGetImageAsync(); Execute.OnUIThread(() => { - Instances.TaskQueueViewModel.AddLog(log, UiLogColor.Error, toolTip: screenshot?.CreateTooltip()); + Instances.TaskQueueViewModel.AddLog(log, UiLogColor.Error, toolTip: screenshot?.CreateTooltip(), updateCardImage: true, fetchLatestImage: true); }); }); @@ -952,13 +1046,14 @@ public class AsstProxy case AsstMsg.TaskChainStart: { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartTask") + LocalizationHelper.GetString(taskChain)); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StartTask") + LocalizationHelper.GetString(taskChain), splitMode: TaskQueueViewModel.LogCardSplitMode.Before); TaskStatusUpdate(taskId, TaskStatus.InProgress); // LinkStart 按钮也会修改,但小工具中的日志源需要在这里修改 Instances.OverlayViewModel.LogItemsSource = (taskChain is "Copilot" or "SSSCopilot" or "VideoRecognition") ? Instances.CopilotViewModel.LogItemViewModels : Instances.TaskQueueViewModel.LogItemViewModels; + break; } @@ -1074,7 +1169,7 @@ public class AsstProxy sanityReport = sanityReport.Replace("{DateTime}", recoveryTime.ToString("yyyy-MM-dd HH:mm")).Replace("{TimeDiff}", (recoveryTime - DateTimeOffset.Now).ToString(@"h\h\ m\m")); allTaskCompleteLog = allTaskCompleteLog + Environment.NewLine + sanityReport; - Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); + Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog, splitMode: TaskQueueViewModel.LogCardSplitMode.Both); if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) { @@ -1103,7 +1198,7 @@ public class AsstProxy } else { - Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog); + Instances.TaskQueueViewModel.AddLog(allTaskCompleteLog, splitMode: TaskQueueViewModel.LogCardSplitMode.Both); if (SettingsViewModel.ExternalNotificationSettings.ExternalNotificationSendWhenComplete) { @@ -1385,7 +1480,7 @@ public class AsstProxy missionStartLogBuilder.AppendFormat(LocalizationHelper.GetString("StoneUsedTimes"), StoneUsedTimes); } - Instances.TaskQueueViewModel.AddLog(missionStartLogBuilder.ToString().TrimEnd(), UiLogColor.Info); + Instances.TaskQueueViewModel.AddLog(missionStartLogBuilder.ToString().TrimEnd(), UiLogColor.Info, splitMode: TaskQueueViewModel.LogCardSplitMode.Before); break; case "StoneConfirm": @@ -1424,11 +1519,11 @@ public class AsstProxy // Instances.TaskQueueViewModel.AddLog("开始战斗"); // break; case "MissionCompletedFlag": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightCompleted"), UiLogColor.SuccessIS); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightCompleted"), UiLogColor.SuccessIS, updateCardImage: true); break; case "MissionFailedFlag": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightFailed"), UiLogColor.Error); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("FightFailed"), UiLogColor.Error, updateCardImage: true); break; case "StageTrader": @@ -1543,7 +1638,7 @@ public class AsstProxy if (taskName == "StartExplore") { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("BegunToExplore") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("BegunToExplore") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info, splitMode: TaskQueueViewModel.LogCardSplitMode.Before); } break; @@ -1651,7 +1746,8 @@ public class AsstProxy $"{allDrops}{(curTimes >= 0 ? $"\n{LocalizationHelper.GetString("CurTimes")} : {curTimes}" : string.Empty)}", - toolTip: dropsForTooltip.CreateMaterialDropTooltip()); + toolTip: dropsForTooltip.CreateMaterialDropTooltip(), + updateCardImage: true); AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySpenderGroup, curTimes > 0 ? curTimes : 1); @@ -1661,7 +1757,8 @@ public class AsstProxy case "EnterFacility": Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ThisFacility") + LocalizationHelper.GetString($"{subTaskDetails?["facility"]}") + " " + - ((int)(subTaskDetails?["index"] ?? -2) + 1).ToString("D2")); + ((int)(subTaskDetails?["index"] ?? -2) + 1).ToString("D2"), + splitMode: TaskQueueViewModel.LogCardSplitMode.Before); break; case "ProductIncorrect": @@ -1683,7 +1780,7 @@ public class AsstProxy .Aggregate(string.Empty, (current, tagStr) => current + (tagStr + "\n")); logContent = logContent.EndsWith('\n') ? logContent.TrimEnd('\n') : LocalizationHelper.GetString("Error"); - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitingResults") + "\n" + logContent); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("RecruitingResults") + "\n" + logContent, splitMode: TaskQueueViewModel.LogCardSplitMode.Before, updateCardImage: true); break; } @@ -2354,10 +2451,10 @@ public class AsstProxy /// 关闭游戏 CloseDown, - /// 刷理智 + /// 理智作战 Fight, - /// 关卡选择为剿灭时的备选刷理智 + /// 关卡选择为剿灭时的备选理智作战 FightAnnihilationAlternate, /// 剩余理智 diff --git a/src/MaaWpfGui/Res/Localizations/en-us.xaml b/src/MaaWpfGui/Res/Localizations/en-us.xaml index a5d3b36e9e..19eb4ec7a8 100644 --- a/src/MaaWpfGui/Res/Localizations/en-us.xaml +++ b/src/MaaWpfGui/Res/Localizations/en-us.xaml @@ -497,6 +497,7 @@ Then delete the entries corresponding to the paths. Window Title Scrolling Reverse right-click effect for main task Default: Apply once; Checked: Skip once + Maximum number of log thumbnails Title Bar Display Content Manually create a 「background」 folder and place 「background.png」 inside to customize the background Custom log date format diff --git a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml index 98404f92c2..0c778a69a4 100644 --- a/src/MaaWpfGui/Res/Localizations/ja-jp.xaml +++ b/src/MaaWpfGui/Res/Localizations/ja-jp.xaml @@ -497,6 +497,7 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settin タイトルのスクロール機能 メインタスクの右クリック効果を反転 デフォルト:1回のみ有効、チェック後:1回のみスキップ + ログサムネイルの最大数 タイトルバー表示コンテンツ 「background」フォルダを手動で作成し、background.png を配置すると背景画像をカスタマイズできます カスタムログ日付形式 diff --git a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml index 924b549bfb..b929ae27e8 100644 --- a/src/MaaWpfGui/Res/Localizations/ko-kr.xaml +++ b/src/MaaWpfGui/Res/Localizations/ko-kr.xaml @@ -497,6 +497,7 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settin 윈도우 제목 스크롤 메인 작업 오른쪽 클릭 효과 반전 기본: 한 번만 적용, 체크 시: 한 번만 건너뛰기 + 로그 썸네일 최대 개수 제목 표시줄 표시 내용 「background」 폴더를 직접 생성한 후 background.png를 넣으면 배경 이미지를 변경할 수 있습니다 커스텀 로그 시간 형식 diff --git a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml index 1e25f9ed41..017a56b0ac 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-cn.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-cn.xaml @@ -497,6 +497,7 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settin 窗口标题滚动 反转主任务右键单击效果 默认仅一次生效,勾选后仅一次跳过 + 日志缩略图最大数量 标题栏显示内容 手动创建 background 文件夹,将 background.png 放入即可自定义背景图片 自选日志日期格式 @@ -504,7 +505,7 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settin 是否立刻重启应用更改? 将模拟器分辨率更改为 1920x1080! 确保模拟器设置为 1920x1080 - 允许吃源石保存状态 + 允许源石恢复保存状态 该功能属于危险功能。按下 「{key=Confirm}」 键即视为你了解并愿意承担可能出现的风险。 使用备选关卡 使用剩余理智 @@ -595,17 +596,17 @@ C:\\leidian\\LDPlayer9。\n {key=Recruiting} 基建换班 {key=Base} - 刷理智 + 理智作战 {key=Combat} 访问好友 - 获取信用及购物 + 信用收支 领取奖励 {key=Mission} 自动肉鸽 {key=AutoRoguelike} 生息演算 小游戏 - 自定义任务 + 自定任务 单步任务 生息演算主题 沙中之火 @@ -646,8 +647,8 @@ C:\\leidian\\LDPlayer9。\n 停止 等待 & 停止 - 吃理智药 - 吃源石 + 药剂恢复 + 源石恢复 指定次数 指定材料 刷取数量 @@ -664,7 +665,7 @@ C:\\leidian\\LDPlayer9。\n • {key=NotSwitch}: 不调整游戏内代理倍率设定 关卡选择 - 备选 + 关卡备选 剩余理智 不使用 diff --git a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml index 41f053e897..606e0b7415 100644 --- a/src/MaaWpfGui/Res/Localizations/zh-tw.xaml +++ b/src/MaaWpfGui/Res/Localizations/zh-tw.xaml @@ -497,6 +497,7 @@ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settin 窗口標題滾動 反轉主任務右鍵單擊效果 預設僅一次生效,勾選後僅一次跳過 + 日誌縮略圖最大數量 標題欄顯示內容 手動建立 background 資料夾,將 background.png 放入即可自訂背景圖片 自訂日誌日期格式 diff --git a/src/MaaWpfGui/Res/Themes/Dark.xaml b/src/MaaWpfGui/Res/Themes/Dark.xaml index 408ccd6879..866166303f 100644 --- a/src/MaaWpfGui/Res/Themes/Dark.xaml +++ b/src/MaaWpfGui/Res/Themes/Dark.xaml @@ -14,11 +14,13 @@ + + diff --git a/src/MaaWpfGui/Res/Themes/Light.xaml b/src/MaaWpfGui/Res/Themes/Light.xaml index 435246541f..f75cbfb34b 100644 --- a/src/MaaWpfGui/Res/Themes/Light.xaml +++ b/src/MaaWpfGui/Res/Themes/Light.xaml @@ -8,11 +8,13 @@ + + diff --git a/src/MaaWpfGui/Services/MaaService.cs b/src/MaaWpfGui/Services/MaaService.cs index 7543836ba6..a050427eac 100644 --- a/src/MaaWpfGui/Services/MaaService.cs +++ b/src/MaaWpfGui/Services/MaaService.cs @@ -110,7 +110,7 @@ public enum AsstTaskType : byte CloseDown, /// - /// 刷理智 + /// 理智作战 /// Fight, diff --git a/src/MaaWpfGui/Styles/Controls/TextBlock.cs b/src/MaaWpfGui/Styles/Controls/TextBlock.cs index 0cfdbdc594..0763848762 100644 --- a/src/MaaWpfGui/Styles/Controls/TextBlock.cs +++ b/src/MaaWpfGui/Styles/Controls/TextBlock.cs @@ -55,7 +55,10 @@ public class TextBlock : System.Windows.Controls.TextBlock set { SetValue(ForegroundKeyProperty, value); - if (Application.Current.Resources.Contains(value)) + + // `Application.Current.Resources.Contains(key)` 不会递归检查 MergedDictionaries, + // 而主题 Brush(如 ErrorLogBrush)通常来自合并字典;用 TryFindResource 才能稳定命中。 + if (TryFindResource(value) is Brush) { SetResourceReference(ForegroundProperty, value); return; diff --git a/src/MaaWpfGui/ViewModels/LogCardViewModel.cs b/src/MaaWpfGui/ViewModels/LogCardViewModel.cs new file mode 100644 index 0000000000..688896e499 --- /dev/null +++ b/src/MaaWpfGui/ViewModels/LogCardViewModel.cs @@ -0,0 +1,96 @@ +// +// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team) +// Copyright (C) 2021-2025 MaaAssistantArknights Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License v3.0 only as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY +// + +#nullable enable + +using System.Collections.ObjectModel; +using System.Windows.Media; +using MaaWpfGui.Utilities; +using Stylet; + +namespace MaaWpfGui.ViewModels +{ + /// + /// Represents a grouped log card that contains several . + /// + public class LogCardViewModel : PropertyDependsOnViewModel + { + public ObservableCollection Items { get; } = new(); + + private ImageSource? _thumbnail; + + public ImageSource? Thumbnail + { + get => _thumbnail; + set => SetAndNotify(ref _thumbnail, value); + } + + [PropertyDependsOn(nameof(Thumbnail))] + public bool ShowThumbnail => Thumbnail is not null; + + public LogCardViewModel() + { + InitializePropertyDependencies(); + + // Keep StartTime/EndTime in sync when Items changes or an item's Time updates. + Items.CollectionChanged += Items_CollectionChanged; + + // Attach to existing items if any (defensive). + for (int i = 0; i < Items.Count; i++) + { + Items[i].PropertyChanged += LogItem_PropertyChanged; + } + } + + private void Items_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + if (e.OldItems != null) + { + foreach (var obj in e.OldItems) + { + if (obj is LogItemViewModel oldItem) + { + oldItem.PropertyChanged -= LogItem_PropertyChanged; + } + } + } + + if (e.NewItems != null) + { + foreach (var obj in e.NewItems) + { + if (obj is LogItemViewModel newItem) + { + newItem.PropertyChanged += LogItem_PropertyChanged; + } + } + } + + NotifyOfPropertyChange(nameof(StartTime)); + NotifyOfPropertyChange(nameof(EndTime)); + } + + private void LogItem_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LogItemViewModel.Time)) + { + NotifyOfPropertyChange(nameof(StartTime)); + NotifyOfPropertyChange(nameof(EndTime)); + } + } + + public string StartTime => Items.Count > 0 ? Items[0].Time : string.Empty; + + public string EndTime => Items.Count > 0 ? Items[^1].Time : string.Empty; + } +} diff --git a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs index 28ef23396a..1b5645d4f0 100644 --- a/src/MaaWpfGui/ViewModels/LogItemViewModel.cs +++ b/src/MaaWpfGui/ViewModels/LogItemViewModel.cs @@ -14,6 +14,7 @@ #nullable enable using System; using System.Windows.Controls; +using System.Windows.Media; using MaaWpfGui.Constants; using MaaWpfGui.Utilities; using MaaWpfGui.ViewModels.UI; @@ -24,7 +25,7 @@ namespace MaaWpfGui.ViewModels; /// /// The view model of log item. /// -public class LogItemViewModel : PropertyChangedBase +public class LogItemViewModel : PropertyDependsOnViewModel { /// /// Initializes a new instance of the class. @@ -37,6 +38,8 @@ public class LogItemViewModel : PropertyChangedBase /// The toolTip public LogItemViewModel(string content, string color = UiLogColor.Message, string weight = "Regular", string dateFormat = "", bool showTime = true, ToolTip? toolTip = null) { + InitializePropertyDependencies(); + if (string.IsNullOrEmpty(dateFormat)) { dateFormat = SettingsViewModel.GuiSettings.LogItemDateFormatString; @@ -80,6 +83,12 @@ public class LogItemViewModel : PropertyChangedBase set => SetAndNotify(ref _content, value); } + /// + /// Gets the display content for UI. + /// + [PropertyDependsOn(nameof(Content))] + public string DisplayContent => Content; + private string _color; /// diff --git a/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs b/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs new file mode 100644 index 0000000000..5cb81b90f9 --- /dev/null +++ b/src/MaaWpfGui/ViewModels/PropertyDependsOnViewModel.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using MaaWpfGui.Utilities; +using Stylet; + +namespace MaaWpfGui.ViewModels +{ + /// + /// Provides support for PropertyDependsOnAttribute: when a source property changes, + /// dependent properties are automatically notified. + /// + public abstract class PropertyDependsOnViewModel : PropertyChangedBase + { + private readonly Dictionary> _propertyDependencies = []; // key: source property name, value: dependent property names + + /// + /// Scan for PropertyDependsOnAttribute on this instance's properties and wire notifications. + /// Call this from the derived type's constructor. + /// + protected void InitializePropertyDependencies() + { + var type = GetType(); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + + foreach (var property in properties) + { + var dependsOnAttributes = property.GetCustomAttributes(true); + foreach (var attribute in dependsOnAttributes.Where(i => i.PropertyNames.Length > 0)) + { + foreach (var key in attribute.PropertyNames) + { + if (!_propertyDependencies.TryGetValue(key, out var value)) + { + value = []; + _propertyDependencies[key] = value; + } + + if (_propertyDependencies.TryGetValue(property.Name, out var values) && values.Contains(key)) + { + throw new ArgumentException($"属性 {key} 依赖于属性 {property.Name}, 但它们之间存在循环依赖关系"); + } + + value.Add(property.Name); + } + } + } + + PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.PropertyName) || !_propertyDependencies.TryGetValue(e.PropertyName, out var value)) + { + return; + } + + foreach (var dependentProperty in value) + { + NotifyOfPropertyChange(dependentProperty); + } + } + } +} diff --git a/src/MaaWpfGui/ViewModels/TaskViewModel.cs b/src/MaaWpfGui/ViewModels/TaskViewModel.cs index cb1a627a2a..55b4af0f4c 100644 --- a/src/MaaWpfGui/ViewModels/TaskViewModel.cs +++ b/src/MaaWpfGui/ViewModels/TaskViewModel.cs @@ -29,62 +29,13 @@ using Stylet; namespace MaaWpfGui.ViewModels; -public abstract class TaskViewModel : PropertyChangedBase +public abstract class TaskViewModel : PropertyDependsOnViewModel { - private readonly Dictionary> _propertyDependencies = []; // 属性依赖关系, key为被订阅的属性名, value为依赖于该属性的属性名列表 - protected TaskViewModel() { InitializePropertyDependencies(); } - /// - /// 初始化属性依赖关系 - /// - private void InitializePropertyDependencies() - { - var type = GetType(); - var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - - foreach (var property in properties) - { - var dependsOnAttributes = property.GetCustomAttributes(true); - foreach (var attribute in dependsOnAttributes.Where(i => i.PropertyNames.Length > 0)) - { - foreach (var key in attribute.PropertyNames) - { - if (!_propertyDependencies.TryGetValue(key, out var value)) - { - value = []; - _propertyDependencies[key] = value; - } - - if (_propertyDependencies.TryGetValue(property.Name, out var values) && values.Contains(key)) - { - throw new ArgumentException($"属性 {key} 依赖于属性 {property.Name}, 但它们之间存在循环依赖关系"); - } - - value.Add(property.Name); - } - } - } - - PropertyChanged += OnPropertyChanged; - } - - private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (string.IsNullOrEmpty(e.PropertyName) || !_propertyDependencies.TryGetValue(e.PropertyName, out var value)) - { - return; - } - - foreach (var dependentProperty in value) - { - NotifyOfPropertyChange(dependentProperty); - } - } - protected T? GetTaskConfig() where T : BaseTask { diff --git a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs index 6437270d43..5e5af7b02c 100644 --- a/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/SettingsViewModel.cs @@ -870,9 +870,8 @@ public class SettingsViewModel : Screen prefix += " - "; } - List windowTitleSelectShowList = GuiSettings.WindowTitleSelectShowList - .Cast>().Select(pair => pair.Key) - .ToList(); + List windowTitleSelectShowList = [.. GuiSettings.WindowTitleSelectShowList + .Cast>().Select(pair => pair.Key)]; string currentConfiguration = string.Empty; string connectConfigName = string.Empty; @@ -896,7 +895,7 @@ public class SettingsViewModel : Screen break; case "3": // 端口地址 - connectAddress = $" ({ConnectSettings.ConnectAddress})"; + connectAddress = $" ({ConnectSettings.ConnectAddress})".Replace("127.0.0.1:", string.Empty).Replace("localhost:", string.Empty); break; case "4": // 客户端类型 diff --git a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs index a057ad20cd..090a50286d 100644 --- a/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/TaskQueueViewModel.cs @@ -13,6 +13,7 @@ #nullable enable using System; +using System.Buffers; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -21,9 +22,11 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using System.Windows.Media.Imaging; using JetBrains.Annotations; using MaaWpfGui.Constants; using MaaWpfGui.Extensions; @@ -40,7 +43,6 @@ using MaaWpfGui.Views.UI; using Newtonsoft.Json.Linq; using Serilog; using Stylet; -using static MaaWpfGui.Main.AsstProxy; using Application = System.Windows.Application; using Screen = Stylet.Screen; using Task = System.Threading.Tasks.Task; @@ -219,6 +221,26 @@ public class TaskQueueViewModel : Screen /// public ObservableCollection LogItemViewModels { get; private set; } = []; + /// + /// Gets the grouped log cards. Each card contains multiple . + /// + public ObservableCollection LogCardViewModels { get; private set; } = []; + + private bool TryMergeIntoLastCard(string content, string color, string weight, ToolTip? toolTip) + { + // Merge into last existing card when it exists and is not sealed. + if (LogCardViewModels.Count == 0) + { + return false; + } + + var lastCard = LogCardViewModels[^1]; + var log = new LogItemViewModel(content, color, weight, toolTip: toolTip); + LogItemViewModels.Add(log); + lastCard.Items.Add(log); + return true; + } + /// /// Gets or private sets the single download-related log item. /// Use a single LogItemViewModel instead of a collection because only one entry is shown. @@ -231,6 +253,93 @@ public class TaskQueueViewModel : Screen private set => SetAndNotify(ref _downloadLogItemViewModel, value); } + #region LogThumbnails + + private readonly SemaphoreSlim _logThumbnailSemaphore = new(1, 1); + + private const int LogThumbnailWidth = 640; + private const int LogThumbnailHeight = 360; + + private static int MaxLogItemsWithThumbnails => SettingsViewModel.GuiSettings.MaxNumberOfLogThumbnails; + + private async Task AttachThumbnailToCardAsync(LogCardViewModel card, bool forceScreencap) + { + if (card is null) + { + _logger.Warning("Cannot attach thumbnail to null log card."); + return; + } + + try + { + var thumbnail = await GetOrCaptureLogThumbnailAsync(forceScreencap).ConfigureAwait(false); + if (thumbnail is null) + { + return; + } + + await Execute.OnUIThreadAsync(() => { + card.Thumbnail = thumbnail; + TrimOldThumbnails(); + }).ConfigureAwait(false); + } + catch + { + _logger.Warning("Failed to attach thumbnail to log card."); + } + } + + private async Task GetOrCaptureLogThumbnailAsync(bool forceScreencap = false) + { + if (!await _logThumbnailSemaphore.WaitAsync(0).ConfigureAwait(false)) + { + return null; + } + + try + { + var frameData = await Instances.AsstProxy.AsstGetImageBgrDataAsync(forceScreencap: forceScreencap).ConfigureAwait(false); + if (frameData is null || frameData.Length == 0) + { + return null; + } + + try + { + // 只保留小图,避免日志列表长期运行时占用过多内存。 + var thumbnail = AsstProxy.CreateBgrBitmapSourceScaled(frameData, LogThumbnailWidth, LogThumbnailHeight); + return thumbnail; + } + finally + { + ArrayPool.Shared.Return(frameData); + } + } + finally + { + _logThumbnailSemaphore.Release(); + } + } + + private void TrimOldThumbnails() + { + var thumbnailIndices = LogCardViewModels + .Select((vm, index) => new { vm, index }) + .Where(x => x.vm.Thumbnail != null) + .Select(x => x.index) + .ToList(); + + if (thumbnailIndices.Count > MaxLogItemsWithThumbnails) + { + for (int i = 0; i < thumbnailIndices.Count - MaxLogItemsWithThumbnails; i++) + { + LogCardViewModels[thumbnailIndices[i]].Thumbnail = null; + } + } + } + + #endregion + #region ActionAfterTasks private bool _enableAfterActionSetting; @@ -435,6 +544,12 @@ public class TaskQueueViewModel : Screen _ = UpdateDatePromptAndStagesWeb(); } + /// + protected override void OnClose() + { + base.OnClose(); + } + /* public void ShowButton() { @@ -951,6 +1066,29 @@ public class TaskQueueViewModel : Screen private set => SetAndNotify(ref _stagesOfToday, value); } + public enum LogCardSplitMode + { + /// + /// 不拆分日志卡片 + /// + None = 0, + + /// + /// 插入日志前拆分卡片 + /// + Before = 1, + + /// + /// 插入日志后拆分卡片 + /// + After = 2, + + /// + /// 插入日志前后都拆分卡片 + /// + Both = 3, + } + /// /// Adds log. /// @@ -958,15 +1096,46 @@ public class TaskQueueViewModel : Screen /// The font color. /// The font weight. /// The toolTip - public void AddLog(string? content, string color = UiLogColor.Trace, string weight = "Regular", ToolTip? toolTip = null) + /// Whether to update the containing card's image/thumbnail. + /// Whether to force fetching a fresh screenshot instead of using cache. + /// Whether to split cards before/after this log. + public void AddLog(string? content, string color = UiLogColor.Trace, string weight = "Regular", ToolTip? toolTip = null, bool updateCardImage = false, bool fetchLatestImage = false, LogCardSplitMode splitMode = LogCardSplitMode.None) { if (string.IsNullOrEmpty(content)) { + if (splitMode != LogCardSplitMode.None) + { + createNewCard(); + } return; } + Execute.OnUIThread(() => { - var log = new LogItemViewModel(content, color, weight, toolTip: toolTip); - LogItemViewModels.Add(log); + if (LogCardViewModels.Count <= 0) + { + createNewCard(); + } + + switch (splitMode) + { + case LogCardSplitMode.None: + addToCard(); + break; + case LogCardSplitMode.Before: + createNewCard(); + addToCard(); + break; + case LogCardSplitMode.After: + addToCard(); + createNewCard(); + break; + case LogCardSplitMode.Both: + createNewCard(); + addToCard(); + createNewCard(); + break; + } + switch (color) { case UiLogColor.Error: @@ -980,6 +1149,29 @@ public class TaskQueueViewModel : Screen break; } }); + + return; + void createNewCard() + { + if (LogCardViewModels.Count > 0 && LogCardViewModels[^1].Items.Count <= 0) + { + return; + } + + var card = new LogCardViewModel(); + LogCardViewModels.Add(card); + } + + void addToCard() + { + if (TryMergeIntoLastCard(content, color, weight, toolTip)) + { + if (updateCardImage) + { + _ = AttachThumbnailToCardAsync(LogCardViewModels[^1], fetchLatestImage); + } + } + } } /// @@ -989,6 +1181,7 @@ public class TaskQueueViewModel : Screen { Execute.OnUIThread(() => { LogItemViewModels.Clear(); + LogCardViewModels.Clear(); DownloadLogItemViewModel = new(string.Empty); _logger.Information("Main windows log clear."); _logger.Information("{Empty}", string.Empty); @@ -1385,7 +1578,7 @@ public class TaskQueueViewModel : Screen break; case "WakeUp": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.StartUp, StartUpTask.Serialize()); break; case "Combat": @@ -1393,23 +1586,23 @@ public class TaskQueueViewModel : Screen break; case "Recruiting": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Recruit, RecruitTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Recruit, RecruitTask.Serialize()); break; case "Mall": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Mall, MallTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Mall, MallTask.Serialize()); break; case "Mission": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Award, AwardTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Award, AwardTask.Serialize()); break; case "AutoRoguelike": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Roguelike, RoguelikeTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Roguelike, RoguelikeTask.Serialize()); break; case "Reclamation": - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Reclamation, ReclamationTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Reclamation, ReclamationTask.Serialize()); break; case "Custom": @@ -1417,7 +1610,7 @@ public class TaskQueueViewModel : Screen var tasks = CustomTask.SerializeMultiTasks(); foreach (var (type, param) in tasks) { - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Custom, type, param); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Custom, type, param); } break; @@ -1501,7 +1694,7 @@ public class TaskQueueViewModel : Screen public async Task Stop(int timeout = 60 * 1000) { _runningState.SetStopping(true); - AddLog(LocalizationHelper.GetString("Stopping")); + AddLog(LocalizationHelper.GetString("Stopping"), splitMode: LogCardSplitMode.Both); await Task.Run(() => { if (!Instances.AsstProxy.AsstStop()) { @@ -1567,12 +1760,14 @@ public class TaskQueueViewModel : Screen if (!_runningState.GetIdle() || _runningState.GetStopping()) { - AddLog(LocalizationHelper.GetString("Stopped")); + AddLog(LocalizationHelper.GetString("Stopped"), splitMode: LogCardSplitMode.Both); } Waiting = false; _runningState.SetStopping(false); _runningState.SetIdle(true); + + // 只抑制“本轮任务期间”的自动开启;任务结束后应允许下一轮自动开启 LiveView。 } // 该函数将于未来被废弃,改用 LinkStart 代替 @@ -1624,7 +1819,7 @@ public class TaskQueueViewModel : Screen } bool taskRet = true; - taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.StartUp, StartUpTask.Serialize()); + taskRet &= Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.StartUp, StartUpTask.Serialize()); taskRet &= Instances.AsstProxy.AsstStart(); if (taskRet) @@ -1644,7 +1839,7 @@ public class TaskQueueViewModel : Screen string curStage = FightTask.Stage; var (type, mainParam) = FightTask.Serialize(); - bool mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Fight, type, mainParam); + bool mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Fight, type, mainParam); if (!mainFightRet) { AddLog(LocalizationHelper.GetString("UnsupportedStages") + ": " + curStage, UiLogColor.Error); @@ -1668,7 +1863,7 @@ public class TaskQueueViewModel : Screen task.Stone = 0; task.MaxTimes = int.MaxValue; task.Drops = []; - mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightAnnihilationAlternate, type, task.Serialize().Params); + mainFightRet = Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.FightAnnihilationAlternate, type, task.Serialize().Params); } break; @@ -1689,7 +1884,7 @@ public class TaskQueueViewModel : Screen ServerType = Instances.SettingsViewModel.ServerType, ClientType = SettingsViewModel.GameSettings.ClientType, }; - return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.FightRemainingSanity, type, task.Serialize().Params); + return Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.FightRemainingSanity, type, task.Serialize().Params); } return mainFightRet; @@ -1702,7 +1897,7 @@ public class TaskQueueViewModel : Screen /// public void SetFightParams() { - var type = TaskType.Fight; + var type = AsstProxy.TaskType.Fight; var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; if (!EnableSetFightParams || id == 0) { @@ -1715,7 +1910,7 @@ public class TaskQueueViewModel : Screen public static void SetFightRemainingSanityParams() { - var type = TaskType.FightRemainingSanity; + var type = AsstProxy.TaskType.FightRemainingSanity; var id = Instances.AsstProxy.TasksStatus.FirstOrDefault(t => t.Value.Type == type).Key; if (id == 0) { @@ -1749,7 +1944,7 @@ public class TaskQueueViewModel : Screen try { - return Instances.AsstProxy.AsstAppendTaskWithEncoding(TaskType.Infrast, InfrastTask.Serialize()); + return Instances.AsstProxy.AsstAppendTaskWithEncoding(AsstProxy.TaskType.Infrast, InfrastTask.Serialize()); } catch (Exception ex) { diff --git a/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs b/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs index 77fe7807e9..fbc5ebebb2 100644 --- a/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs +++ b/src/MaaWpfGui/ViewModels/UI/ToolboxViewModel.cs @@ -1090,7 +1090,7 @@ public class ToolboxViewModel : Screen { var count = Interlocked.Increment(ref _peepImageCount); var index = count % _peepImageCache.Length; - var frameData = await Instances.AsstProxy.AsstGetFreshImageBgrDataAsync(); + var frameData = await Instances.AsstProxy.AsstGetImageBgrDataAsync(forceScreencap: true); if (frameData is null || frameData.Length == 0) { _logger.Warning("Peep image data is null or empty."); diff --git a/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs index f6b6333399..e183be14c1 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/Settings/ConnectSettingsUserControlModel.cs @@ -917,7 +917,7 @@ public class ConnectSettingsUserControlModel : PropertyChangedBase return; } - TestLinkImage = await Instances.AsstProxy.AsstGetFreshImageAsync(); + TestLinkImage = await Instances.AsstProxy.AsstGetImageAsync(forceScreencap: true); _runningState.SetIdle(true); if (TestLinkImage is null) diff --git a/src/MaaWpfGui/ViewModels/UserControl/Settings/GuiSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/Settings/GuiSettingsUserControlModel.cs index e42b37c13b..7a6f4ad7ee 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/Settings/GuiSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/Settings/GuiSettingsUserControlModel.cs @@ -274,6 +274,17 @@ public class GuiSettingsUserControlModel : PropertyChangedBase } } + private int _maxNumberOfLogThumbnails = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.MaxNumberOfLogThumbnails, 20); + + public int MaxNumberOfLogThumbnails + { + get => _maxNumberOfLogThumbnails; + set { + SetAndNotify(ref _maxNumberOfLogThumbnails, value); + ConfigurationHelper.SetGlobalValue(ConfigurationKeys.MaxNumberOfLogThumbnails, value.ToString()); + } + } + private static readonly Dictionary _windowTitleAllShowDict = new() { { "1", LocalizationHelper.GetString("ConfigurationName") }, @@ -284,11 +295,10 @@ public class GuiSettingsUserControlModel : PropertyChangedBase public static Dictionary WindowTitleAllShowDict { get => _windowTitleAllShowDict; } - private static object[] _windowTitleSelectShowList = ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowTitleSelectShowList, "1 2 3 4") + private static object[] _windowTitleSelectShowList = [.. ConfigurationHelper.GetGlobalValue(ConfigurationKeys.WindowTitleSelectShowList, "2 3 4") .Split(' ') .Where(s => _windowTitleAllShowDict.ContainsKey(s.ToString())) - .Select(s => (object)new KeyValuePair(s, _windowTitleAllShowDict[s])) - .ToArray(); + .Select(s => (object)new KeyValuePair(s, _windowTitleAllShowDict[s]))]; public object[] WindowTitleSelectShowList { diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs index 815ff349d4..71b474ce86 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/FightSettingsUserControlModel.cs @@ -32,7 +32,7 @@ using Stylet; namespace MaaWpfGui.ViewModels.UserControl.TaskQueue; /// -/// 刷理智 +/// 理智作战 /// public class FightSettingsUserControlModel : TaskViewModel { @@ -923,7 +923,7 @@ public class FightSettingsUserControlModel : TaskViewModel stage4 = Instances.TaskQueueViewModel.GetValidStage(stage4); } - // rss 如果结束后还选择了不开放的关卡,刷理智任务会报错 + // rss 如果结束后还选择了不开放的关卡,理智作战任务会报错 rss = Instances.TaskQueueViewModel.IsStageOpen(rss) ? rss : string.Empty; if (tempRemainingSanityStageList.Any(item => item.Value == string.Empty)) diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/InfrastSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/InfrastSettingsUserControlModel.cs index 97ff624d0b..d80350f464 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/InfrastSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/InfrastSettingsUserControlModel.cs @@ -497,7 +497,7 @@ public class InfrastSettingsUserControlModel : TaskViewModel if (output) { - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CustomInfrastTitle"), UiLogColor.Message); + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CustomInfrastTitle"), UiLogColor.Message, splitMode: UI.TaskQueueViewModel.LogCardSplitMode.Before); Instances.TaskQueueViewModel.AddLog($"title: {root.Title}", UiLogColor.Info); Instances.TaskQueueViewModel.AddLog($"description: {root.Description}", UiLogColor.Info); } @@ -532,6 +532,8 @@ public class InfrastSettingsUserControlModel : TaskViewModel Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("CustomInfrastFileHasPlanNoPeriod"), UiLogColor.Warning); } + Instances.TaskQueueViewModel.AddLog(string.Empty, splitMode: UI.TaskQueueViewModel.LogCardSplitMode.After); + CustomInfrastPlanList = [.. list]; } catch (Exception) diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/MallSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/MallSettingsUserControlModel.cs index 34460569d0..0b408354d9 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/MallSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/MallSettingsUserControlModel.cs @@ -314,7 +314,7 @@ public class MallSettingsUserControlModel : TaskViewModel && ConfigFactory.CurrentConfig.TaskQueue.Where(x => x is FightTask).Cast().FirstOrDefault()?.Stage1 == string.Empty; if (fightStage) { - Log.Warning("刷理智 当前/上次导致无法OF-1"); + Log.Warning("理智作战 当前/上次 导致无法 OF-1"); return false; } diff --git a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs index c2faa2c990..5437451f75 100644 --- a/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs +++ b/src/MaaWpfGui/ViewModels/UserControl/TaskQueue/RoguelikeSettingsUserControlModel.cs @@ -507,7 +507,7 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel } SetAndNotify(ref _roguelikeCoreChar, value); - Instances.TaskQueueViewModel.AddLog(value); + Instances.TaskQueueViewModel.AddLog(value, splitMode: TaskQueueViewModel.LogCardSplitMode.Both); ConfigurationHelper.SetValue(ConfigurationKeys.RoguelikeCoreChar, value); } } @@ -1044,7 +1044,7 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel } } - Instances.TaskQueueViewModel.AddLog(roguelikeInfo, UiLogColor.Message); + Instances.TaskQueueViewModel.AddLog(roguelikeInfo, UiLogColor.Message, updateCardImage: true); break; } @@ -1059,14 +1059,19 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel case "RoguelikeEncounterOptions": var options = (subTaskDetails!["options"]! as JArray) ?? []; - Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString("RoguelikeEncounterOptions"), options.Count, UiLogColor.EventIS)); + var logLines = new List + { + string.Format(LocalizationHelper.GetString("RoguelikeEncounterOptions"), options.Count, UiLogColor.EventIS), + }; + foreach (var option in options) { string messageKey = option["enabled"]!.Value() ? "RoguelikeEncounterEnabledOption" : "RoguelikeEncounterDisabledOption"; var text = option["text"]!.ToString(); - Instances.TaskQueueViewModel.AddLog(string.Format(LocalizationHelper.GetString(messageKey), text), UiLogColor.EventIS); + logLines.Add(string.Format(LocalizationHelper.GetString(messageKey), text)); } + Instances.TaskQueueViewModel.AddLog(string.Join("\n", logLines), UiLogColor.EventIS, updateCardImage: true); break; case "BoskyPassageNode": @@ -1089,12 +1094,12 @@ public class RoguelikeSettingsUserControlModel : TaskViewModel } case "RoguelikeCoppersRecognitionError": - { - var recognizedName = subTaskDetails!["recognized_name"]?.ToString() ?? "Unknown"; - var message = string.Format(LocalizationHelper.GetString("RoguelikeCoppersRecognitionError"), recognizedName); - Instances.TaskQueueViewModel.AddLog(message, UiLogColor.Error); - break; - } + { + var recognizedName = subTaskDetails!["recognized_name"]?.ToString() ?? "Unknown"; + var message = string.Format(LocalizationHelper.GetString("RoguelikeCoppersRecognitionError"), recognizedName); + Instances.TaskQueueViewModel.AddLog(message, UiLogColor.Error); + break; + } case "RoguelikeCoppersExchangeInfo": { diff --git a/src/MaaWpfGui/Views/UI/TaskQueueView.xaml b/src/MaaWpfGui/Views/UI/TaskQueueView.xaml index 6a4400caec..cff3eac7ed 100644 --- a/src/MaaWpfGui/Views/UI/TaskQueueView.xaml +++ b/src/MaaWpfGui/Views/UI/TaskQueueView.xaml @@ -245,7 +245,6 @@ -