636 lines
18 KiB
C#
636 lines
18 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading.Tasks;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityEngine.Networking;
|
||
|
||
/// <summary>
|
||
/// 库存管理器 - 单例模式
|
||
/// </summary>
|
||
public class InventoryManager : MonoSingleton<InventoryManager>
|
||
{
|
||
|
||
[Header("配置文件")]
|
||
[SerializeField] private string fileName = "inventory.json";
|
||
|
||
[Header("事件")]
|
||
public UnityEvent onInventoryLoaded; // 库存加载完成事件
|
||
public UnityEvent onInventoryUpdated; // 库存更新事件
|
||
|
||
// 库存数据
|
||
private InventoryData _inventoryData = new InventoryData();
|
||
private string _persistentFilePath; // 可读写路径
|
||
private string _streamingAssetsPath; // 只读路径
|
||
private bool _isInitialized = false;
|
||
private readonly object _lockObject = new object(); // 线程安全锁
|
||
|
||
#region 属性
|
||
|
||
/// <summary>
|
||
/// 获取所有库存项(只读)
|
||
/// </summary>
|
||
public IReadOnlyList<InventoryItem> AllItems
|
||
{
|
||
get
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _inventoryData.inventory.AsReadOnly();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 库存是否已加载
|
||
/// </summary>
|
||
public bool IsInitialized => _isInitialized;
|
||
|
||
#endregion
|
||
|
||
#region Unity 生命周期
|
||
|
||
protected override void Initialize()
|
||
{
|
||
|
||
// 初始化路径
|
||
_streamingAssetsPath = Path.Combine(Application.streamingAssetsPath, fileName);
|
||
_persistentFilePath = Path.Combine(Application.persistentDataPath, fileName);
|
||
}
|
||
|
||
private IEnumerator Start()
|
||
{
|
||
yield return StartCoroutine(InitializeInventoryAsync());
|
||
}
|
||
|
||
private void OnApplicationQuit()
|
||
{
|
||
// 退出时自动保存
|
||
SaveInventoryData();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 初始化与加载
|
||
|
||
/// <summary>
|
||
/// 异步初始化库存系统
|
||
/// </summary>
|
||
public IEnumerator InitializeInventoryAsync(bool forceReload = false)
|
||
{
|
||
if (_isInitialized && !forceReload)
|
||
yield break;
|
||
|
||
Debug.Log($"初始化库存系统...");
|
||
//Debug.Log($"StreamingAssets路径: {_streamingAssetsPath}");
|
||
//Debug.Log($"可读写路径: {_persistentFilePath}");
|
||
|
||
bool loadSuccess = false;
|
||
|
||
// 策略2: 如果可读写路径失败,从StreamingAssets加载
|
||
if (!loadSuccess)
|
||
{
|
||
yield return StartCoroutine(LoadFromStreamingAssets());
|
||
}
|
||
|
||
_isInitialized = true;
|
||
onInventoryLoaded?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从StreamingAssets加载(跨平台兼容)
|
||
/// </summary>
|
||
private IEnumerator LoadFromStreamingAssets()
|
||
{
|
||
// 使用UnityWebRequest加载
|
||
UnityWebRequest request = UnityWebRequest.Get(_streamingAssetsPath);
|
||
|
||
yield return request.SendWebRequest();
|
||
|
||
if (request.result != UnityWebRequest.Result.Success)
|
||
{
|
||
Debug.LogError($"从StreamingAssets加载失败: {request.error}");
|
||
CreateDefaultInventory();
|
||
}
|
||
else
|
||
{
|
||
// 获取文本并处理编码
|
||
string json = request.downloadHandler.text;
|
||
|
||
// 尝试检测并修复编码问题
|
||
json = FixEncodingIfNeeded(json);
|
||
//Debug.Log($"原始JSON内容: {json}");
|
||
try
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
_inventoryData = JsonUtility.FromJson<InventoryData>(json);
|
||
}
|
||
Debug.Log($"从StreamingAssets加载成功,共 {_inventoryData.inventory.Count} 个种类");
|
||
SaveInventoryData(); // 保存到可读写路径
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
Debug.LogError($"JSON解析失败: {e.Message}");
|
||
Debug.Log($"原始JSON内容: {json}");
|
||
CreateDefaultInventory();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测并修复编码问题
|
||
/// </summary>
|
||
private string FixEncodingIfNeeded(string text)
|
||
{
|
||
// 检测是否是乱码(中文变问号等)
|
||
if (string.IsNullOrEmpty(text))
|
||
return text;
|
||
|
||
// 尝试检测UTF-8 BOM
|
||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text);
|
||
|
||
// 检查是否有UTF-8 BOM (EF BB BF)
|
||
if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
|
||
{
|
||
// 去掉BOM
|
||
return System.Text.Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3);
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建默认库存(当配置文件不存在时)
|
||
/// </summary>
|
||
private void CreateDefaultInventory()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
_inventoryData.inventory.Clear();
|
||
_inventoryData.inventory.Add(new InventoryItem("红外测温仪", 99));
|
||
_inventoryData.inventory.Add(new InventoryItem("检测试纸-A", 23));
|
||
_inventoryData.inventory.Add(new InventoryItem("扳手", 3));
|
||
_inventoryData.lastModified = DateTime.Now;
|
||
}
|
||
|
||
Debug.Log("创建默认库存数据");
|
||
SaveInventoryData();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公开接口
|
||
|
||
|
||
/// <summary>
|
||
/// 获取库存中物品种类数量
|
||
/// </summary>
|
||
/// <returns>库存中不同类型的物品数量</returns>
|
||
public int GetItemTypeCount()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _inventoryData.inventory.Count;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有物品类型的名称列表
|
||
/// </summary>
|
||
/// <returns>所有物品类型的名称数组</returns>
|
||
public string[] GetAllItemTypeNames()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _inventoryData.inventory.Select(item => item.type).ToArray();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有物品类型的详细信息
|
||
/// 返回格式:类型名称 -> 数量
|
||
/// </summary>
|
||
/// <returns>字典,键为类型名称,值为数量</returns>
|
||
public Dictionary<string, int> GetAllItemDetails()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _inventoryData.inventory.ToDictionary(
|
||
item => item.type,
|
||
item => item.remaining
|
||
);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取库存概要信息
|
||
/// 返回格式:数组,每个元素是元组(类型, 数量)
|
||
/// </summary>
|
||
public (string type, int quantity)[] GetInventorySummary()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _inventoryData.inventory
|
||
.Select(item => (item.type, item.remaining))
|
||
.ToArray();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取库存统计信息
|
||
/// 包含:种类数量、总数量、平均数量等
|
||
/// </summary>
|
||
public InventoryStats GetInventoryStatistics()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
if (_inventoryData.inventory.Count == 0)
|
||
{
|
||
return new InventoryStats
|
||
{
|
||
itemTypeCount = 0,
|
||
totalQuantity = 0,
|
||
averageQuantity = 0,
|
||
maxQuantity = 0,
|
||
minQuantity = 0
|
||
};
|
||
}
|
||
|
||
var quantities = _inventoryData.inventory.Select(item => item.remaining).ToList();
|
||
|
||
return new InventoryStats
|
||
{
|
||
itemTypeCount = _inventoryData.inventory.Count,
|
||
totalQuantity = quantities.Sum(),
|
||
averageQuantity = (int)quantities.Average(),
|
||
maxQuantity = quantities.Max(),
|
||
minQuantity = quantities.Min(),
|
||
lastModified = _inventoryData.lastModified
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取按数量排序的库存列表
|
||
/// </summary>
|
||
/// <param name="descending">是否降序排列(默认true,数量多的在前)</param>
|
||
/// <returns>排序后的库存列表</returns>
|
||
public IReadOnlyList<InventoryItem> GetSortedItems(bool descending = true)
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
if (descending)
|
||
{
|
||
return _inventoryData.inventory
|
||
.OrderByDescending(item => item.remaining)
|
||
.ToList()
|
||
.AsReadOnly();
|
||
}
|
||
else
|
||
{
|
||
return _inventoryData.inventory
|
||
.OrderBy(item => item.remaining)
|
||
.ToList()
|
||
.AsReadOnly();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取库存数量范围统计
|
||
/// </summary>
|
||
/// <param name="threshold">数量阈值</param>
|
||
/// <returns>低于阈值、等于阈值、高于阈值的物品数量</returns>
|
||
public (int belowThreshold, int atThreshold, int aboveThreshold) GetQuantityStats(int threshold = 10)
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
int below = 0, at = 0, above = 0;
|
||
|
||
foreach (var item in _inventoryData.inventory)
|
||
{
|
||
if (item.remaining < threshold) below++;
|
||
else if (item.remaining == threshold) at++;
|
||
else above++;
|
||
}
|
||
|
||
return (below, at, above);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定关键字匹配的物品列表
|
||
/// </summary>
|
||
/// <param name="keyword">搜索关键字</param>
|
||
/// <param name="caseSensitive">是否区分大小写(默认false)</param>
|
||
/// <returns>匹配的物品列表</returns>
|
||
public IReadOnlyList<InventoryItem> SearchItems(string keyword, bool caseSensitive = false)
|
||
{
|
||
if (string.IsNullOrEmpty(keyword))
|
||
return _inventoryData.inventory.AsReadOnly();
|
||
|
||
lock (_lockObject)
|
||
{
|
||
StringComparison comparison = caseSensitive ?
|
||
StringComparison.Ordinal :
|
||
StringComparison.OrdinalIgnoreCase;
|
||
|
||
return _inventoryData.inventory
|
||
.Where(item => item.type.IndexOf(keyword, comparison) >= 0)
|
||
.ToList()
|
||
.AsReadOnly();
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 获取指定类型的物品数量
|
||
/// </summary>
|
||
/// <param name="type">物品类型</param>
|
||
/// <param name="quantity">输出:数量,如果不存在则返回0</param>
|
||
/// <returns>是否找到该物品</returns>
|
||
public bool TryGetItemQuantity(string type, out int quantity)
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
var item = _inventoryData.inventory.Find(x => x.type == type);
|
||
if (item != null)
|
||
{
|
||
quantity = item.remaining;
|
||
return true;
|
||
}
|
||
|
||
quantity = 0;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新物品数量(接口1:直接设置)
|
||
/// </summary>
|
||
/// <param name="type">物品类型</param>
|
||
/// <param name="newQuantity">新数量</param>
|
||
/// <returns>是否更新成功</returns>
|
||
public bool UpdateItemQuantity(string type, int newQuantity)
|
||
{
|
||
if (newQuantity < 0)
|
||
{
|
||
Debug.LogWarning($"数量不能为负数: {type}");
|
||
return false;
|
||
}
|
||
|
||
lock (_lockObject)
|
||
{
|
||
var item = _inventoryData.inventory.Find(x => x.type == type);
|
||
if (item == null)
|
||
{
|
||
// 如果没有找到,创建一个新项
|
||
_inventoryData.inventory.Add(new InventoryItem(type, newQuantity));
|
||
Debug.Log($"添加新物品: {type}, 数量: {newQuantity}");
|
||
}
|
||
else
|
||
{
|
||
item.remaining = newQuantity;
|
||
}
|
||
|
||
_inventoryData.lastModified = DateTime.Now;
|
||
}
|
||
|
||
// 保存到文件
|
||
SaveInventoryData();
|
||
onInventoryUpdated?.Invoke();
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 增加物品数量(接口2:增量修改)
|
||
/// </summary>
|
||
/// <param name="type">物品类型</param>
|
||
/// <param name="delta">变化量(可正可负)</param>
|
||
/// <returns>修改后的数量,如果失败返回-1</returns>
|
||
public int AddItemQuantity(string type, int delta)
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
var item = _inventoryData.inventory.Find(x => x.type == type);
|
||
if (item == null && delta < 0)
|
||
{
|
||
Debug.LogWarning($"尝试减少不存在的物品: {type}");
|
||
return -1;
|
||
}
|
||
|
||
if (item == null)
|
||
{
|
||
// 创建新项
|
||
item = new InventoryItem(type, delta);
|
||
_inventoryData.inventory.Add(item);
|
||
}
|
||
else
|
||
{
|
||
int newQuantity = item.remaining + delta;
|
||
if (newQuantity < 0)
|
||
{
|
||
Debug.LogWarning($"数量不能为负数: {type}, 当前: {item.remaining}, 尝试减少: {delta}");
|
||
return -1;
|
||
}
|
||
item.remaining = newQuantity;
|
||
}
|
||
|
||
_inventoryData.lastModified = DateTime.Now;
|
||
int result = item.remaining;
|
||
|
||
// 异步保存
|
||
Task.Run(() => SaveInventoryDataAsync());
|
||
onInventoryUpdated?.Invoke();
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加新物品类型
|
||
/// </summary>
|
||
/// <param name="type">物品类型</param>
|
||
/// <param name="initialQuantity">初始数量</param>
|
||
/// <returns>是否添加成功</returns>
|
||
public bool AddNewItemType(string type, int initialQuantity = 0)
|
||
{
|
||
if (initialQuantity < 0)
|
||
{
|
||
Debug.LogWarning($"初始数量不能为负数: {type}");
|
||
return false;
|
||
}
|
||
|
||
lock (_lockObject)
|
||
{
|
||
if (_inventoryData.inventory.Exists(x => x.type == type))
|
||
{
|
||
Debug.LogWarning($"物品类型已存在: {type}");
|
||
return false;
|
||
}
|
||
|
||
_inventoryData.inventory.Add(new InventoryItem(type, initialQuantity));
|
||
_inventoryData.lastModified = DateTime.Now;
|
||
}
|
||
|
||
SaveInventoryData();
|
||
onInventoryUpdated?.Invoke();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除物品类型
|
||
/// </summary>
|
||
public bool RemoveItemType(string type)
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
int removed = _inventoryData.inventory.RemoveAll(x => x.type == type);
|
||
if (removed > 0)
|
||
{
|
||
_inventoryData.lastModified = DateTime.Now;
|
||
SaveInventoryData();
|
||
onInventoryUpdated?.Invoke();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出当前库存为JSON字符串
|
||
/// </summary>
|
||
public string ExportToJson()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return JsonUtility.ToJson(_inventoryData, true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置为初始配置(从StreamingAssets重新加载)
|
||
/// </summary>
|
||
public IEnumerator ResetToDefault()
|
||
{
|
||
Debug.Log("重置库存为初始配置...");
|
||
|
||
// 删除可读写路径的文件
|
||
if (File.Exists(_persistentFilePath))
|
||
{
|
||
File.Delete(_persistentFilePath);
|
||
}
|
||
|
||
// 重新初始化
|
||
_isInitialized = false;
|
||
yield return StartCoroutine(InitializeInventoryAsync(true));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 保存与持久化
|
||
|
||
/// <summary>
|
||
/// 保存库存数据到可读写路径
|
||
/// </summary>
|
||
public void SaveInventoryData()
|
||
{
|
||
try
|
||
{
|
||
string json;
|
||
lock (_lockObject)
|
||
{
|
||
json = JsonUtility.ToJson(_inventoryData, true);
|
||
}
|
||
|
||
// 确保目录存在
|
||
string directory = Path.GetDirectoryName(_persistentFilePath);
|
||
if (!Directory.Exists(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
File.WriteAllText(_persistentFilePath, json);
|
||
Debug.Log($"库存数据已保存: {_persistentFilePath}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"保存库存数据失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步保存库存数据
|
||
/// </summary>
|
||
private async Task SaveInventoryDataAsync()
|
||
{
|
||
try
|
||
{
|
||
string json;
|
||
lock (_lockObject)
|
||
{
|
||
json = JsonUtility.ToJson(_inventoryData, true);
|
||
}
|
||
|
||
string directory = Path.GetDirectoryName(_persistentFilePath);
|
||
if (!Directory.Exists(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
await File.WriteAllTextAsync(_persistentFilePath, json);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"异步保存库存数据失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
/// <summary>
|
||
/// 库存统计信息
|
||
/// </summary>
|
||
public struct InventoryStats
|
||
{
|
||
public int itemTypeCount; // 物品种类数量
|
||
public int totalQuantity; // 总数量
|
||
public int averageQuantity; // 平均数量
|
||
public int maxQuantity; // 最大数量
|
||
public int minQuantity; // 最小数量
|
||
public DateTime lastModified; // 最后修改时间
|
||
|
||
public override string ToString()
|
||
{
|
||
return $"种类: {itemTypeCount}, 总数: {totalQuantity}, 平均: {averageQuantity}, 最大: {maxQuantity}, 最小: {minQuantity}, 最后更新: {lastModified:yyyy-MM-dd HH:mm:ss}";
|
||
}
|
||
}
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class InventoryItem
|
||
{
|
||
public string type; // 物品类型
|
||
public int remaining; // 剩余数量
|
||
|
||
public InventoryItem(string type, int remaining)
|
||
{
|
||
this.type = type;
|
||
this.remaining = remaining;
|
||
}
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class InventoryData
|
||
{
|
||
public List<InventoryItem> inventory = new List<InventoryItem>();
|
||
public DateTime lastModified; // 最后修改时间
|
||
} |