feat: 重构仓库识别结构,支持根据关卡掉落情况自动更新 (#15358)

* feat: 重构仓库识别结构,支持根据关卡掉落情况自动更新

* feat: 上次同步时间

* chore: 使用 TryParseExact
This commit is contained in:
uye
2026-01-10 21:01:01 +08:00
committed by GitHub
parent 4a42d1142b
commit 20b317b07a
19 changed files with 398 additions and 204 deletions

View File

@@ -524,39 +524,8 @@ Todo
```json
// Corresponding details field example
"done": bool, // Whether recognition is complete, false means still in progress (data during process)
"arkplanner": { // https://penguin-stats.cn/planner
"object": {
"items": [
{
"id": "2004",
"have": 4,
"name": "高级作战记录" // "Advanced Battle Record"
},
{
"id": "mod_unlock_token",
"have": 25,
"name": "模组数据块" // "Module Data Block"
},
{
"id": "2003",
"have": 20,
"name": "中级作战记录" // "Tactical Battle Record"
}
],
"@type": "@penguin-statistics/depot"
},
"data": "{\"@type\":\"@penguin-statistics/depot\",\"items\":[{\"id\":\"2004\",\"have\":4,\"name\":\"高级作战记录\"},{\"id\":\"mod_unlock_token\",\"have\":25,\"name\":\"模组数据块\"},{\"id\":\"2003\",\"have\":20,\"name\":\"中级作战记录\"}]}"
},
"lolicon": { // https://arkntools.app/#/material
"object": {
"2004" : 4,
"mod_unlock_token": 25,
"2003": 20
},
"data": "{\"2003\":20,\"2004\": 4,\"mod_unlock_token\": 25}"
}
// Currently only supports ArkPlanner and Lolicon (Arkntools) formats, may support more websites in the future
"done": bool, // Whether recognition is complete, false means still in progress (data during process)
"data": "{\"2001\":18000,\"31043\":317}" // JSON string, format: {"itemId": quantity, ...}
```
- `OperBox`

View File

@@ -535,44 +535,13 @@ Todo
}
```
- `DepotInfo`
- `Depot`
倉庫のアイテムの認識結果
```json
// 対応する詳細フィールドの例
"done": bool, // 認識が完了したかどうかfalse はまだ進行中かどうか(処理中のデータ)
"arkplanner": { // https://penguin-stats.cn/planner
"object": {
"items": [
{
"id": "2004",
"have": 4,
"name": "上級作戦記録"
},
{
"id": "mod_unlock_token",
"have": 25,
"name": "モジュールデータ"
},
{
"id": "2003",
"have": 20,
"name": "中級作戦記録"
}
],
"@type": "@penguin-statistics/depot"
},
"data": "{\"@type\":\"@penguin-statistics/depot\",\"items\":[{\"id\":\"2004\",\"have\":4,\"name\":\"上級作戦記録\"},{\"id\":\"mod_unlock_token\",\"have\":25,\"name\":\"モジュールデータ\"},{\"id\":\"2003\",\"have\":20,\"name\":\"中級作戦記録\"}]}"
},
"lolicon": { // https://arkntools.app/#/material
"object": {
"2004" : 4,
"mod_unlock_token": 25,
"2003": 20
},
"data": "{\"2003\":20,\"2004\": 4,\"mod_unlock_token\": 25}"
}
// 現在は ArkPlanner と Lolicon (Arkntools) 形式のみ対応、今後対応するサイトが増える可能性あり
"done": bool, // 認識が完了したかどうかfalse はまだ進行中かどうか(処理中のデータ)
"data": "{\"2001\":18000,\"31043\":317}" // JSON文字列、形式: {"アイテムID": 数量, ...}
```
- `OperBoxInfo`

View File

@@ -474,8 +474,7 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom
- `Depot`
창고 인식 결과. `details` 필드 구조는 다음과 같습니다:
- `done` (boolean, required): 인식 완료 여부, `false`는 아직 인식 중임(진행 중 데이터)을 의미
- `arkplanner` (object, required): [ArkPlanner](https://penguin-stats.cn/planner) 포맷 데이터
- `lolicon` (object, required): [lolicon](https://arkntools.app/#/material) (Arkntools) 포맷 데이터
- `data` (string, required): JSON 문자열, 형식은 `{"아이템ID": 수량, ...}`, 예: `{"2001":18000,"31043":317}`
- `OperBox`
오퍼레이터 보관함 인식 결과. `details` 필드 구조는 다음과 같습니다:

View File

@@ -474,8 +474,7 @@ typedef void(ASST_CALL* AsstCallback)(int msg, const char* details, void* custom
- `Depot`
仓库识别结果。`details` 字段结构如下:
- `done` (boolean, required): 是否已经识别完了,为 `false` 表示仍在识别中(过程中的数据)。
- `arkplanner` (object, required): [ArkPlanner](https://penguin-stats.cn/planner) 格式的数据
- `lolicon` (object, required): [lolicon](https://arkntools.app/#/material) (Arkntools) 格式的数据。
- `data` (string, required): JSON 字符串,格式为 `{"物品ID": 数量, ...}`,例如 `{"2001":18000,"31043":317}`
- `OperBox`
干员识别结果。`details` 字段结构如下:

View File

@@ -536,39 +536,8 @@ Todo
```json
// 對應的 details 欄位舉例
"done": bool, // 是否已經辨識完了,為 false 表示仍在辨識中(過程中的數據)
"arkplanner": { // https://penguin-stats.io/planner
"object": {
"items": [
{
"id": "2004",
"have": 4,
"name": "高級作戰記錄"
},
{
"id": "mod_unlock_token",
"have": 25,
"name": "模組數據塊"
},
{
"id": "2003",
"have": 20,
"name": "中級作戰記錄"
}
],
"@type": "@penguin-statistics/depot"
},
"data": "{\"@type\":\"@penguin-statistics/depot\",\"items\":[{\"id\":\"2004\",\"have\":4,\"name\":\"高級作戰記錄\"},{\"id\":\"mod_unlock_token\",\"have\":25,\"name\":\"模組數據塊\"},{\"id\":\"2003\",\"have\":20,\"name\":\"中級作戰記錄\"}]}"
},
"lolicon": { // https://arkntools.app/#/material
"object": {
"2004" : 4,
"mod_unlock_token": 25,
"2003": 20
},
"data": "{\"2003\":20,\"2004\": 4,\"mod_unlock_token\": 25}"
}
// 目前只支援 ArkPlanner 和 Lolicon (Arkntools) 的格式,以後可能會兼容更多網站
"done": bool, // 是否已經辨識完了,為 false 表示仍在辨識中(過程中的數據)
"data": "{\"2001\":18000,\"31043\":317}" // JSON 字串,格式為 {"物品ID": 數量, ...}
```
- `OperBox`

View File

@@ -36,11 +36,6 @@
"recruitUrl": "https://backend.yituliu.cn/maa/upload/recruit",
"dropUrl": "https://backend.yituliu.cn/maa/upload/stageDrop"
},
"depotExportTemplate": {
"Doc": "仓库识别导出结果模板",
"arkPlanner": "{\"@type\": \"@penguin-statistics/depot\",\"items\": []}",
"arkPlanner_Doc": "https://penguin-stats.cn/planner"
},
"debug": {
"cleanFilesFreq": 5,
"maxDebugFileNum": 10

View File

@@ -48,8 +48,6 @@ bool asst::GeneralConfig::parse(const json::value& json)
m_options.yituliu_report.drop_url = yituliu_opt->get("dropUrl", std::string());
m_options.yituliu_report.recruit_url = yituliu_opt->get("recruitUrl", std::string());
}
m_options.depot_export_template.ark_planner =
options_json.get("depotExportTemplate", "arkPlanner", std::string());
m_options.debug.clean_files_freq = options_json.get("debug", "cleanFilesFreq", 50);
m_options.debug.max_debug_file_num = options_json.get("debug", "maxDebugFileNum", 100);
}

View File

@@ -54,7 +54,6 @@ struct Options
int swipe_with_pause_required_distance = 0;
std::vector<std::string> minitouch_programs_order;
RequestInfo penguin_report; // 企鹅物流汇报:每次到结算界面,汇报掉落数据至企鹅物流 https://penguin-stats.io
DepotExportTemplate depot_export_template; // 仓库识别结果导出模板
RequestInfo yituliu_report; // 一图流大数据汇报目前只有公招功能https://ark.yituliu.cn/survey/maarecruitdata
DebugConf debug;
};

View File

@@ -58,38 +58,18 @@ void asst::DepotRecognitionTask::callback_analyze_result(bool done)
{
LogTraceFunction;
auto& templ = Config.get_options().depot_export_template;
json::value info = basic_info_with_what("DepotInfo");
auto& details = info["details"];
// https://penguin-stats.io/planner
if (auto arkplanner_template_opt = json::parse(templ.ark_planner)) {
auto& arkplanner = details["arkplanner"];
auto& arkplanner_obj = arkplanner["object"];
arkplanner_obj = arkplanner_template_opt.value();
auto& arkplanner_data_items = arkplanner_obj["items"];
for (const auto& [item_id, item_info] : m_all_items) {
arkplanner_data_items.emplace(
json::object {
{ "id", item_id },
{ "have", item_info.quantity },
{ "name", item_info.item_name },
});
}
arkplanner["data"] = arkplanner_obj.to_string();
// 简化格式:只保留 done 和 data
// data 为 {"itemId": count} 格式
json::object data_obj;
for (const auto& [item_id, item_info] : m_all_items) {
data_obj.emplace(item_id, item_info.quantity);
}
// https://arkntools.app/#/material
{
auto& lolicon = details["lolicon"];
auto& lolicon_obj = lolicon["object"];
for (const auto& [item_id, item_info] : m_all_items) {
lolicon_obj.emplace(item_id, item_info.quantity);
}
lolicon["data"] = lolicon_obj.to_string();
}
details["done"] = done;
details["data"] = data_obj.to_string();
callback(AsstMsg::SubTaskExtraInfo, info);
}

View File

@@ -1721,7 +1721,7 @@ public class AsstProxy
switch (taskChain)
{
case "Depot":
Instances.ToolboxViewModel.DepotParse((JObject?)subTaskDetails);
Instances.ToolboxViewModel.DepotParse((JObject?)subTaskDetails, updateSyncTime: true);
break;
case "OperBox":
@@ -1785,6 +1785,9 @@ public class AsstProxy
AchievementTrackerHelper.Instance.AddProgressToGroup(AchievementIds.SanitySpenderGroup, curTimes > 0 ? curTimes : 1);
// 联动更新 Depot 数据
Instances.ToolboxViewModel.UpdateDepotFromDrops(drops);
break;
}

View File

@@ -726,7 +726,7 @@ Do not adjust the proxy multiplier setting in the game</system:String>
<system:String x:Key="MinimumRequirements" xml:space="preserve">Minimum Requirements:&#160;</system:String>
<system:String x:Key="DaysLeftOpen" xml:space="preserve">Days left open:&#160;</system:String>
<system:String x:Key="Inventory">Inventory</system:String>
<system:String x:Key="InventoryUpdateTip">Inventory data can be updated via 「{key=Toolbox}-{key=DepotRecognition}」 </system:String>
<system:String x:Key="InventoryUpdateTip">Inventory data can be updated via 「{key=Toolbox}-{key=DepotRecognition}」</system:String>
<system:String x:Key="LessThanOneDay">Less than 1 day</system:String>
<system:String x:Key="1-7">1-7</system:String>
<system:String x:Key="CE-6">CE-6</system:String>
@@ -809,6 +809,7 @@ Right Click: Select Target Process</system:String>
<!-- Depot Recognition -->
<system:String x:Key="DepotRecognition">Depot</system:String>
<system:String x:Key="Depot">{key=DepotRecognition}</system:String>
<system:String x:Key="LastDepotSyncTime">Last Sync Time</system:String>
<system:String x:Key="DepotRecognitionTip">This function is still in testing. If you encounter any unexpected results, please zip the 「debug/depot」 folder in the installation path and submit an issue on GitHub</system:String>
<system:String x:Key="StartToDepotRecognition">Start to Identify</system:String>
<system:String x:Key="ExportToArkplanner">Export to Arkplanner</system:String>

View File

@@ -810,6 +810,7 @@ C:\\leidian\\LDPlayer9
<!-- 倉庫アイテム認識 -->
<system:String x:Key="DepotRecognition">倉庫アイテム認識</system:String>
<system:String x:Key="Depot">{key=DepotRecognition}</system:String>
<system:String x:Key="LastDepotSyncTime">前回同期時間</system:String>
<system:String x:Key="DepotRecognitionTip">この機能はまだテスト版です。エラーが発生/統計情報に問題があれば、debug/depotフォルダを圧縮し、issueを提出してください~</system:String>
<system:String x:Key="StartToDepotRecognition">認識開始</system:String>
<system:String x:Key="ExportToArkplanner">Arkplannerへ出力</system:String>

View File

@@ -811,6 +811,7 @@ C:\\leidian\\LDPlayer9
<!-- 창고 인식 -->
<system:String x:Key="DepotRecognition">창고 인식</system:String>
<system:String x:Key="Depot">{key=DepotRecognition}</system:String>
<system:String x:Key="LastDepotSyncTime">마지막 동기화 시간</system:String>
<system:String x:Key="DepotRecognitionTip">이 기능은 현재 테스트 중입니다. 내보내기 전 인식 결과가 맞는지 확인 후 사용하세요\n만약 오류가 있다면 MAA 경로 내 debug 폴더 안의 depot 폴더 전체를 압축해서 Github의 issue로 올려주세요</system:String>
<system:String x:Key="StartToDepotRecognition">인식 시작</system:String>
<system:String x:Key="ExportToArkplanner">Arkplanner로 내보내기</system:String>

View File

@@ -810,6 +810,7 @@ C:\\leidian\\LDPlayer9。\n
<!-- 仓库识别 -->
<system:String x:Key="DepotRecognition">仓库识别</system:String>
<system:String x:Key="Depot">{key=DepotRecognition}</system:String>
<system:String x:Key="LastDepotSyncTime">上次同步时间</system:String>
<system:String x:Key="DepotRecognitionTip">该功能尚处于测试阶段,请检查结果是否准确再行使用。若有误,欢迎打包 debug/depot 文件夹后向我们提交 issue ~</system:String>
<system:String x:Key="StartToDepotRecognition">开始识别</system:String>
<system:String x:Key="ExportToArkplanner">导出至企鹅物流刷图规划</system:String>

View File

@@ -810,6 +810,7 @@ C:\\leidian\\LDPlayer9。\n
<!-- 倉庫辨識 -->
<system:String x:Key="DepotRecognition">倉庫辨識</system:String>
<system:String x:Key="Depot">{key=DepotRecognition}</system:String>
<system:String x:Key="LastDepotSyncTime">上次同步時間</system:String>
<system:String x:Key="DepotRecognitionTip">該功能尚處於測試階段,請檢查結果是否準確再行使用。若有誤,歡迎打包 debug/depot 資料夾後向我們提交 issue ~</system:String>
<system:String x:Key="StartToDepotRecognition">開始辨識</system:String>
<system:String x:Key="ExportToArkplanner">匯出到企鵝物流刷圖規劃</system:String>

View File

@@ -689,7 +689,7 @@ public class StageManager
if (!string.IsNullOrEmpty(stage.Drop))
{
var str = $"{stage.Value}: {ItemListHelper.GetItemName(stage.Drop) ?? stage.Drop}";
var count = Instances.ToolboxViewModel?.DepotResult?.FirstOrDefault(d => d.Id == stage.Drop)?.Count;
var count = Instances.ToolboxViewModel?.DepotResult?.FirstOrDefault(d => d.Id == stage.Drop)?.DisplayCount;
if (!string.IsNullOrEmpty(count) && count != "-1")
{
str += $" ({LocalizationHelper.GetString("Inventory")} {count})";

View File

@@ -16,6 +16,7 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -32,6 +33,7 @@ using MaaWpfGui.Main;
using MaaWpfGui.Models;
using MaaWpfGui.Models.AsstTasks;
using MaaWpfGui.States;
using MaaWpfGui.Utilities;
using MaaWpfGui.Utilities.ValueType;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -54,6 +56,7 @@ public class ToolboxViewModel : Screen
/// </summary>
public ToolboxViewModel()
{
PropertyDependsOnUtility.InitializePropertyDependencies(this);
DisplayName = LocalizationHelper.GetString("Toolbox");
_runningState = RunningState.Instance;
_runningState.StateChanged += (__, e) => {
@@ -406,6 +409,35 @@ public class ToolboxViewModel : Screen
set => SetAndNotify(ref _depotInfo, value);
}
private DateTime? _lastDepotSyncTime;
/// <summary>
/// Gets or sets 上次仓库同步时间UTC 时间)
/// </summary>
public DateTime? LastDepotSyncTime
{
get => _lastDepotSyncTime;
set => SetAndNotify(ref _lastDepotSyncTime, value);
}
/// <summary>
/// Gets 上次仓库同步时间的显示文本(本地时间)
/// </summary>
[PropertyDependsOn(nameof(LastDepotSyncTime))]
public string LastDepotSyncTimeText
{
get {
if (LastDepotSyncTime == null)
{
return string.Empty;
}
// 将 UTC 时间转换为本地时间显示
var localTime = LastDepotSyncTime.Value.ToLocalTime();
return localTime.ToString();
}
}
private ObservableCollection<DepotResultDate> _depotResult = [];
/// <summary>
@@ -414,7 +446,25 @@ public class ToolboxViewModel : Screen
public ObservableCollection<DepotResultDate> DepotResult
{
get => _depotResult;
set => SetAndNotify(ref _depotResult, value);
set {
SetAndNotify(ref _depotResult, value);
InvalidateDepotCache();
}
}
// 缓存相关字段
private bool _depotCacheInvalid = true;
private string? _cachedArkPlannerResult;
private string? _cachedLoliconResult;
/// <summary>
/// 标记仓库缓存失效
/// </summary>
private void InvalidateDepotCache()
{
_depotCacheInvalid = true;
_cachedArkPlannerResult = null;
_cachedLoliconResult = null;
}
public class DepotResultDate
@@ -425,54 +475,135 @@ public class ToolboxViewModel : Screen
public BitmapSource? Image { get; set; }
public string? Count { get; set; }
/// <summary>
/// Gets or sets 物品数量(原始数值)
/// </summary>
public int Count { get; set; }
/// <summary>
/// Gets 格式化后的显示数量(用于 UI 绑定)
/// </summary>
public string DisplayCount => Count >= 0 ? Count.FormatNumber(false) : "-1";
}
private void SaveDepotDetails(JObject details)
/// <summary>
/// 保存仓库详情数据
/// </summary>
private void SaveDepotDetails()
{
// var json = details.ToString(Formatting.None);
// ConfigurationHelper.SetValue(ConfigurationKeys.DepotResult, json);
// 构建简化格式:{"itemId": count}
var depotData = new JObject();
foreach (var item in DepotResult)
{
if (item.Count >= 0)
{
depotData[item.Id] = item.Count;
}
}
var details = new JObject {
["done"] = true,
["data"] = depotData.ToString(Formatting.None),
};
// 保存同步时间为 UTC如果有
if (LastDepotSyncTime.HasValue)
{
details["syncTime"] = LastDepotSyncTime.Value.ToString("o"); // ISO 8601 格式
}
JsonDataHelper.Set(JsonDataKey.DepotData, details);
}
private void LoadDepotDetails()
{
// TODO: 删除老数据节省 gui.json 的大小,后续版本可以删除
// var json = ConfigurationHelper.GetValue(ConfigurationKeys.DepotResult, string.Empty);
ConfigurationHelper.DeleteValue(ConfigurationKeys.DepotResult);
var json = JsonDataHelper.Get(JsonDataKey.DepotData, string.Empty);
if (string.IsNullOrWhiteSpace(json))
{
return;
}
try
{
var details = JObject.Parse(json);
DepotParse(details);
}
catch
{
// 兼容老数据或异常时忽略
var details = JObject.Parse(json);
DepotParse(details);
}
/// <summary>
/// Gets 获取 ArkPlanner 导出格式(带缓存)
/// </summary>
public string ArkPlannerResult
{
get {
if (DepotResult.Count == 0)
{
return string.Empty;
}
// 使用缓存
if (!_depotCacheInvalid && _cachedArkPlannerResult != null)
{
return _cachedArkPlannerResult;
}
// 重新计算
var items = DepotResult
.Where(item => item.Count >= 0)
.Select(item => new JObject {
["id"] = item.Id,
["have"] = item.Count,
["name"] = item.Name ?? string.Empty,
});
var result = new JObject {
["@type"] = "@penguin-statistics/depot",
["items"] = new JArray(items),
};
_cachedArkPlannerResult = result.ToString(Formatting.None);
_depotCacheInvalid = false; // 标记缓存已更新
return _cachedArkPlannerResult;
}
}
/// <summary>
/// Gets or sets the ArkPlanner result.
/// Gets 获取 工具箱 导出格式(带缓存)
/// </summary>
public string ArkPlannerResult { get; set; } = string.Empty;
public string LoliconResult
{
get {
if (DepotResult.Count == 0)
{
return string.Empty;
}
// 使用缓存
if (!_depotCacheInvalid && _cachedLoliconResult != null)
{
return _cachedLoliconResult;
}
// 重新计算
var depotData = new JObject();
foreach (var item in DepotResult)
{
if (item.Count >= 0)
{
depotData[item.Id] = item.Count;
}
}
_cachedLoliconResult = depotData.ToString(Formatting.None);
_depotCacheInvalid = false; // 标记缓存已更新
return _cachedLoliconResult;
}
}
/// <summary>
/// Gets or sets the Lolicon result.
/// 解析仓库识别结果(兼容新旧格式)
/// </summary>
public string LoliconResult { get; set; } = string.Empty;
/// <summary>
/// parse of depot recognition result
/// </summary>
/// <param name="details">detailed json-style parameters</param>
/// <returns>Success or not</returns>
public bool DepotParse(JObject? details)
/// <param name="details">详细的 JSON 参数</param>
/// <param name="updateSyncTime">是否更新同步时间为当前时间(从 Core 获取新数据时为 true从本地加载时为 false</param>
/// <returns>是否成功</returns>
public bool DepotParse(JObject? details, bool updateSyncTime = false)
{
if (details == null)
{
@@ -481,38 +612,68 @@ public class ToolboxViewModel : Screen
DepotResult.Clear();
var sortedItems = details["arkplanner"]?["object"]?["items"]
?.Cast<JObject>()
.OrderBy(item => (string?)item["id"])
?? Enumerable.Empty<JObject>();
Dictionary<string, int> depotItems = [];
foreach (var item in sortedItems)
// 尝试解析新格式
var dataStr = details["data"]?.ToString();
if (!string.IsNullOrEmpty(dataStr))
{
var id = (string?)item["id"];
if (string.IsNullOrEmpty(id))
try
{
continue;
var dataObj = JObject.Parse(dataStr);
foreach (var prop in dataObj.Properties())
{
if (int.TryParse(prop.Value.ToString(), out var count))
{
depotItems[prop.Name] = count;
}
}
}
int count = -1;
bool hasValidCount = false;
if (item["have"] != null)
catch (Exception ex)
{
var haveValue = item["have"]?.ToString();
hasValidCount = int.TryParse(haveValue, out count);
_logger.Warning(ex, "Failed to parse depot data format");
}
}
string countStr = hasValidCount ? count.FormatNumber(false) : "-1";
// 如果新格式解析失败,尝试旧格式
if (depotItems.Count == 0)
{
if (depotItems.Count == 0)
{
var arkplannerItems = details["arkplanner"]?["object"]?["items"]
?.Cast<JObject>()
?? [];
foreach (var item in arkplannerItems)
{
var id = (string?)item["id"];
if (string.IsNullOrEmpty(id))
{
continue;
}
if (item["have"] != null && int.TryParse(item["have"]?.ToString(), out var count))
{
depotItems[id] = count;
}
}
}
}
// 转换为 DepotResult按 ID 排序
foreach (var kvp in depotItems.OrderBy(x => x.Key))
{
var id = kvp.Key;
var count = kvp.Value;
DepotResultDate result = new() {
Id = id,
Name = ItemListHelper.GetItemName(id),
Image = ItemListHelper.GetItemImage(id),
Count = countStr,
Count = count,
};
if (hasValidCount && count > 0 &&
if (count > 0 &&
count > AchievementTrackerHelper.Instance.GetProgress(AchievementIds.WarehouseMiser))
{
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.WarehouseMiser, count);
@@ -521,8 +682,8 @@ public class ToolboxViewModel : Screen
DepotResult.Add(result);
}
ArkPlannerResult = details["arkplanner"]?["data"]?.ToString() ?? string.Empty;
LoliconResult = details["lolicon"]?["data"]?.ToString() ?? string.Empty;
// 标记缓存失效
InvalidateDepotCache();
bool done = (bool)(details["done"] ?? false);
if (!done)
@@ -530,8 +691,25 @@ public class ToolboxViewModel : Screen
return true;
}
if (updateSyncTime)
{
// 从 Core 获取新数据,更新为当前 UTC 时间
LastDepotSyncTime = DateTime.UtcNow;
}
else
{
// 从本地加载,读取保存的时间
var syncTimeStr = details["syncTime"]?.ToString(Formatting.None)?.Trim('"');
if (!string.IsNullOrEmpty(syncTimeStr) &&
DateTime.TryParseExact(syncTimeStr, "O", null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var lastDepotSyncTime))
{
LastDepotSyncTime = lastDepotSyncTime;
}
}
DepotInfo = LocalizationHelper.GetString("IdentificationCompleted");
SaveDepotDetails(details);
SaveDepotDetails();
Instances.TaskQueueViewModel.UpdateDatePrompt();
return true;
@@ -564,8 +742,120 @@ public class ToolboxViewModel : Screen
private void DepotClear()
{
DepotResult.Clear();
ArkPlannerResult = string.Empty;
LoliconResult = string.Empty;
InvalidateDepotCache();
}
// 需要排除的物品 ID不统计到仓库
private static readonly HashSet<string> ExcludedItemIds =
[
"3401", // 家具
"3112", "3113", "3114", // 碳
"4001", // 龙门币
"4003", // 合成玉
"4006", // 红票
"5001", // 经验
];
/// <summary>
/// 检查物品 ID 是否应该被排除(不统计到仓库)
/// </summary>
/// <param name="itemId">物品 ID</param>
/// <returns>true 表示应该排除</returns>
private static bool ShouldExcludeItem(string itemId)
{
// 排除特定 ID
if (ExcludedItemIds.Contains(itemId))
{
return true;
}
// 排除非纯数字的 ID
if (!int.TryParse(itemId, out _))
{
return true;
}
return false;
}
/// <summary>
/// 根据 StageDrops 数据更新仓库
/// </summary>
/// <param name="drops">关卡掉落数据列表 (ItemId, ItemName, Total, Add)</param>
public void UpdateDepotFromDrops(List<(string ItemId, string ItemName, int Total, int Add)> drops)
{
if (drops == null || drops.Count == 0)
{
return;
}
bool hasUpdates = false;
foreach (var (itemId, _, total, add) in drops)
{
if (string.IsNullOrEmpty(itemId) || add <= 0)
{
continue;
}
// 过滤不需要统计的物品
if (ShouldExcludeItem(itemId))
{
continue;
}
// 查找现有仓库项
var existingItem = DepotResult.FirstOrDefault(x => x.Id == itemId);
if (existingItem != null)
{
// 更新现有物品数量
if (existingItem.Count >= 0)
{
var newCount = existingItem.Count + add;
existingItem.Count = newCount;
hasUpdates = true;
// 更新成就进度
if (newCount > AchievementTrackerHelper.Instance.GetProgress(AchievementIds.WarehouseMiser))
{
AchievementTrackerHelper.Instance.SetProgress(AchievementIds.WarehouseMiser, newCount);
}
}
}
else
{
// 添加新物品
var newItem = new DepotResultDate {
Id = itemId,
Name = ItemListHelper.GetItemName(itemId),
Image = ItemListHelper.GetItemImage(itemId),
Count = add,
};
DepotResult.Add(newItem);
hasUpdates = true;
}
}
// 如果有更新,重新排序并保存
if (hasUpdates)
{
// 按 ID 排序(与 DepotParse 保持一致)
var sortedItems = DepotResult.OrderBy(x => x.Id).ToList();
DepotResult.Clear();
foreach (var item in sortedItems)
{
DepotResult.Add(item);
}
// 标记缓存失效
InvalidateDepotCache();
// 保存更新后的数据
SaveDepotDetails();
Instances.TaskQueueViewModel.UpdateDatePrompt();
_logger.Information("Depot updated from stage drops, {Count} items processed", drops.Count);
}
}
/// <summary>

View File

@@ -387,8 +387,15 @@
<controls:TooltipBlock
HorizontalAlignment="Right"
VerticalContentAlignment="Top"
NormalOpacity="0.5"
TooltipText="{DynamicResource InventoryUpdateTip}" />
NormalOpacity="0.5">
<controls:TooltipBlock.TooltipText>
<MultiBinding StringFormat="{}{0}&#x0a;{1}: {2}">
<Binding Source="{StaticResource InventoryUpdateTip}" />
<Binding Source="{StaticResource LastDepotSyncTime}" />
<Binding Path="LastDepotSyncTimeText" Source="{x:Static helper:Instances.ToolboxViewModel}" />
</MultiBinding>
</controls:TooltipBlock.TooltipText>
</controls:TooltipBlock>
</Border>
</Grid>
</Grid>

View File

@@ -304,23 +304,34 @@
</Grid>
</TabItem>
<TabItem Header="{DynamicResource DepotRecognition}">
<Grid Margin="20,0,20,20" IsEnabled="{Binding Idle}">
<Grid Margin="20,0,20,20">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="150" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel>
<controls:TextBlock
Height="36"
Margin="10"
HorizontalAlignment="Center"
d:Text="上次同步时间: 2026/1/1 上午00:00:00"
Visibility="{c:Binding 'LastDepotSyncTimeText.Length > 0'}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Source="{StaticResource LastDepotSyncTime}" />
<Binding Path="LastDepotSyncTimeText" Source="{x:Static helper:Instances.ToolboxViewModel}" />
</MultiBinding>
</TextBlock.Text>
</controls:TextBlock>
<controls:TextBlock
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Focusable="True"
FontSize="14"
Style="{StaticResource TextBlockDefaultBold}"
Text="{Binding DepotInfo}"
TextWrapping="Wrap" />
TextWrapping="Wrap"
Visibility="{c:Binding 'DepotInfo.Length > 0'}" />
</StackPanel>
<controls:NoAutomationDataGrid
Grid.Row="1"
@@ -350,19 +361,20 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding Name}" Header="Name" />
<DataGridTextColumn Binding="{Binding Count}" Header="Count" />
<DataGridTextColumn Binding="{Binding DisplayCount}" Header="Count" />
</DataGrid.Columns>
</controls:NoAutomationDataGrid>
<StackPanel
Grid.Row="2"
Margin="27,0"
Margin="0,10,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsEnabled="{Binding Idle}"
Orientation="Horizontal">
<Button
Width="180"
Height="70"
Margin="30"
Margin="30,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{s:Action ExportToArkplanner}"
@@ -371,7 +383,7 @@
<Button
Width="180"
Height="70"
Margin="30"
Margin="30,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{s:Action ExportToLolicon}"
@@ -380,7 +392,7 @@
<Button
Width="180"
Height="70"
Margin="30"
Margin="30,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{s:Action StartDepot}"