装备系统做了很多

This commit is contained in:
FloatGaming
2026-03-27 08:02:55 +08:00
parent dd205b6cb4
commit 538085b64f
119 changed files with 27552 additions and 1672 deletions
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
public static class EquipmentConsumableCatalog
{
private static readonly EquipmentConsumableDescriptor[] Descriptors =
{
new EquipmentConsumableDescriptor(EquipmentConsumableKind.Material78101, "eqc_78101", "装备升级材料78101"),
new EquipmentConsumableDescriptor(EquipmentConsumableKind.Material78111, "eqc_78111", "装备突破材料78111")
};
private static readonly Dictionary<EquipmentConsumableKind, EquipmentConsumableDescriptor> ByKind = BuildByKind();
private static readonly Dictionary<string, EquipmentConsumableDescriptor> ByKey = BuildByKey();
public static IReadOnlyList<EquipmentConsumableDescriptor> All => Descriptors;
public static string GetKey(EquipmentConsumableKind kind)
{
return ByKind[kind].Key;
}
public static string GetDisplayName(EquipmentConsumableKind kind)
{
return ByKind[kind].DisplayName;
}
public static bool TryGetKind(string key, out EquipmentConsumableKind kind)
{
kind = default;
if (string.IsNullOrEmpty(key))
{
return false;
}
if (!ByKey.TryGetValue(key, out EquipmentConsumableDescriptor descriptor))
{
return false;
}
kind = descriptor.Kind;
return true;
}
private static Dictionary<EquipmentConsumableKind, EquipmentConsumableDescriptor> BuildByKind()
{
var map = new Dictionary<EquipmentConsumableKind, EquipmentConsumableDescriptor>();
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Kind] = Descriptors[i];
}
return map;
}
private static Dictionary<string, EquipmentConsumableDescriptor> BuildByKey()
{
var map = new Dictionary<string, EquipmentConsumableDescriptor>(StringComparer.Ordinal);
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Key] = Descriptors[i];
}
return map;
}
}
public sealed class EquipmentConsumableDescriptor
{
public EquipmentConsumableDescriptor(EquipmentConsumableKind kind, string key, string displayName)
{
Kind = kind;
Key = key;
DisplayName = displayName;
}
public EquipmentConsumableKind Kind { get; }
public string Key { get; }
public string DisplayName { get; }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0fc8afc632ac0b94685b585bfdc387d9
@@ -0,0 +1,5 @@
public enum EquipmentConsumableKind
{
Material78101,
Material78111
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b2eb15fea2aeb9141873f6e710224887
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class EquipmentConsumableLedger : MonoBehaviour
{
public static EquipmentConsumableLedger Instance { get; private set; }
public event Action<EquipmentConsumableKind, int> OnCountChanged;
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static EquipmentConsumableLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_equipment_consumable_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<EquipmentConsumableLedger>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeIfNeeded();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SaveNow();
}
}
private void OnApplicationQuit()
{
SaveNow();
}
public void InitializeIfNeeded()
{
if (initialized)
{
return;
}
EquipmentConsumableLedgerPayload payload;
EquipmentConsumableLedgerStorage.TryLoad(out payload);
RebuildFromPayload(payload);
initialized = true;
SyncMirrorCounts();
SaveNow();
}
public int GetCount(EquipmentConsumableKind kind)
{
return GetCountByKey(EquipmentConsumableCatalog.GetKey(kind));
}
public void Add(EquipmentConsumableKind kind, int amount)
{
if (amount <= 0)
{
return;
}
ChangeCount(EquipmentConsumableCatalog.GetKey(kind), amount);
}
public bool TryConsume(EquipmentConsumableKind kind, int amount)
{
if (amount <= 0)
{
return false;
}
string key = EquipmentConsumableCatalog.GetKey(kind);
int current = GetCountByKey(key);
if (current < amount)
{
return false;
}
ChangeCount(key, -amount);
DailyTaskEventHub.ReportUseItem(amount);
return true;
}
public void ResetAllToZero()
{
InitializeIfNeeded();
for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++)
{
countsByKey[EquipmentConsumableCatalog.All[i].Key] = 0;
}
SaveNow();
NotifyAllCountsChanged();
}
public void SaveNow()
{
if (!initialized)
{
InitializeIfNeeded();
}
SyncMirrorCounts();
EquipmentConsumableLedgerStorage.TrySave(BuildPayload());
}
private void SyncMirrorCounts()
{
equipmentConsumableSO[] definitions = Resources.FindObjectsOfTypeAll<equipmentConsumableSO>();
for (int i = 0; i < definitions.Length; i++)
{
equipmentConsumableSO definition = definitions[i];
if (definition == null)
{
continue;
}
definition.runtimeOwnedCount = GetCount(definition.consumableKind);
}
}
private int GetCountByKey(string key)
{
InitializeIfNeeded();
if (!countsByKey.TryGetValue(key, out int count))
{
return 0;
}
return count;
}
private void ChangeCount(string key, int delta)
{
InitializeIfNeeded();
int current = GetCountByKey(key);
long next = (long)current + delta;
if (next < 0)
{
next = 0;
}
else if (next > int.MaxValue)
{
next = int.MaxValue;
}
SetCountByKey(key, (int)next);
}
private void SetCountByKey(string key, int value)
{
int safeValue = Mathf.Max(0, value);
int current = GetCountByKey(key);
if (current == safeValue)
{
return;
}
countsByKey[key] = safeValue;
SaveNow();
if (EquipmentConsumableCatalog.TryGetKind(key, out EquipmentConsumableKind kind) && OnCountChanged != null)
{
OnCountChanged(kind, safeValue);
}
}
private void RebuildFromPayload(EquipmentConsumableLedgerPayload payload)
{
countsByKey.Clear();
for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++)
{
countsByKey[EquipmentConsumableCatalog.All[i].Key] = 0;
}
if (payload == null || payload.entries == null)
{
return;
}
for (int i = 0; i < payload.entries.Count; i++)
{
var entry = payload.entries[i];
if (entry == null || string.IsNullOrEmpty(entry.key))
{
continue;
}
countsByKey[entry.key] = Mathf.Max(0, entry.count);
}
}
private EquipmentConsumableLedgerPayload BuildPayload()
{
var payload = EquipmentConsumableLedgerStorage.CreateDefaultPayload();
payload.entries.Clear();
for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++)
{
var descriptor = EquipmentConsumableCatalog.All[i];
payload.entries.Add(new EquipmentConsumableEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
}
return payload;
}
private void NotifyAllCountsChanged()
{
if (OnCountChanged == null)
{
return;
}
for (int i = 0; i < EquipmentConsumableCatalog.All.Count; i++)
{
var descriptor = EquipmentConsumableCatalog.All[i];
OnCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 164e32ce0a98c6840943dda5f15d02a8
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
[Serializable]
public class EquipmentConsumableEntry
{
public string key;
public int count;
}
[Serializable]
public class EquipmentConsumableLedgerPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public List<EquipmentConsumableEntry> entries = new List<EquipmentConsumableEntry>();
}
[Serializable]
public class EquipmentConsumableLedgerEnvelope
{
public string payload;
public string signature;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e5c6f1d972017041a0a3f4c7c44723f
@@ -0,0 +1,187 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class EquipmentConsumableLedgerStorage
{
private const string SecretSeed = "ban_total.equipment_consumable_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".eqc.dat";
private const string BackupFileName = ".eqc.bak";
private const string TempFileName = ".eqc.tmp";
private static string VaultDirectoryPath => Path.Combine(Application.persistentDataPath, VaultDirectoryName);
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
private static string BackupFilePath => Path.Combine(VaultDirectoryPath, BackupFileName);
private static string TempFilePath => Path.Combine(VaultDirectoryPath, TempFileName);
public static bool TryLoad(out EquipmentConsumableLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
{
TrySave(payload);
return true;
}
return false;
}
public static bool TrySave(EquipmentConsumableLedgerPayload payload)
{
try
{
Directory.CreateDirectory(VaultDirectoryPath);
TryHidePath(VaultDirectoryPath);
var envelopeJson = BuildEnvelopeJson(payload);
File.WriteAllText(TempFilePath, envelopeJson, Encoding.UTF8);
TryHidePath(TempFilePath);
if (File.Exists(MainFilePath))
{
File.Copy(MainFilePath, BackupFilePath, true);
TryHidePath(BackupFilePath);
}
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[EquipmentConsumableLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static EquipmentConsumableLedgerPayload CreateDefaultPayload()
{
return new EquipmentConsumableLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<EquipmentConsumableEntry>()
};
}
private static bool TryReadPayload(string path, out EquipmentConsumableLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<EquipmentConsumableLedgerEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
{
Debug.LogWarning("[EquipmentConsumableLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[EquipmentConsumableLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(EquipmentConsumableLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
var payloadJson = JsonUtility.ToJson(payload, false);
var plainBytes = Encoding.UTF8.GetBytes(payloadJson);
var encryptedBytes = XorTransform(plainBytes, BuildKeyBytes());
var payloadBase64 = Convert.ToBase64String(encryptedBytes);
var envelope = new EquipmentConsumableLedgerEnvelope
{
payload = payloadBase64,
signature = ComputeSignature(payloadBase64)
};
return JsonUtility.ToJson(envelope, false);
}
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
private static byte[] BuildKeyBytes()
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] XorTransform(byte[] source, byte[] key)
{
var result = new byte[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = (byte)(source[i] ^ key[i % key.Length]);
}
return result;
}
private static void TryHidePath(string path)
{
try
{
if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer)
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Hidden) == 0)
{
File.SetAttributes(path, attributes | FileAttributes.Hidden);
}
}
catch
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d400833b3a18409458680f66c6f6e18e
@@ -111,6 +111,17 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
return payload.coins >= amount;
}
public bool HasEnoughMaterial(long amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
return payload.material >= amount;
}
public void AddCoins(int amount)
{
if (amount <= 0)
@@ -157,6 +168,24 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
return true;
}
public bool TrySpendMaterial(int amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
if (payload.material < amount)
{
return false;
}
payload.material -= amount;
SaveNow();
return true;
}
public void SetCoins(int value)
{
InitializeIfNeeded();
@@ -7,7 +7,8 @@ public static class StoreExpBottlePurchaseService
{
None,
ExpBottle,
GrowthMaterial
GrowthMaterial,
EquipmentConsumable
}
public static bool TryPurchase(Player_SO playerData, storeItemSO itemSO, int packageCount, out string failureMessage, out int grantedCount)
@@ -137,7 +138,8 @@ public static class StoreExpBottlePurchaseService
ConsumableGrantType grantType;
ExpBottleKind bottleKind;
DushMaterialKind materialKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind))
EquipmentConsumableKind equipmentConsumableKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind, out equipmentConsumableKind))
{
failureMessage = "当前未配置发放逻辑";
return false;
@@ -170,7 +172,8 @@ public static class StoreExpBottlePurchaseService
ConsumableGrantType grantType;
ExpBottleKind bottleKind;
DushMaterialKind materialKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind))
EquipmentConsumableKind equipmentConsumableKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind, out equipmentConsumableKind))
{
failureMessage = "当前未配置发放逻辑";
return false;
@@ -190,6 +193,12 @@ public static class StoreExpBottlePurchaseService
return true;
}
if (grantType == ConsumableGrantType.EquipmentConsumable)
{
EquipmentConsumableLedger.EnsureInstance().Add(equipmentConsumableKind, grantedCount);
return true;
}
failureMessage = "当前未配置发放逻辑";
return false;
@@ -224,7 +233,8 @@ public static class StoreExpBottlePurchaseService
ConsumableGrantType grantType;
ExpBottleKind bottleKind;
DushMaterialKind materialKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind))
EquipmentConsumableKind equipmentConsumableKind;
if (!TryResolveConsumableGrant(itemSO, out grantType, out bottleKind, out materialKind, out equipmentConsumableKind))
{
Debug.Log(builder.ToString());
return;
@@ -240,6 +250,16 @@ public static class StoreExpBottlePurchaseService
return;
}
if (grantType == ConsumableGrantType.EquipmentConsumable)
{
builder.Append(" | 装备材料库存=");
builder.Append(EquipmentConsumableCatalog.GetDisplayName(equipmentConsumableKind));
builder.Append(":");
builder.Append(EquipmentConsumableLedger.EnsureInstance().GetCount(equipmentConsumableKind));
Debug.Log(builder.ToString());
return;
}
var snapshot = ExpBottleLedger.EnsureInstance().GetSnapshot();
builder.Append(" | 经验瓶库存:");
@@ -267,11 +287,12 @@ public static class StoreExpBottlePurchaseService
Debug.Log(builder.ToString());
}
private static bool TryResolveConsumableGrant(storeItemSO itemSO, out ConsumableGrantType grantType, out ExpBottleKind bottleKind, out DushMaterialKind materialKind)
private static bool TryResolveConsumableGrant(storeItemSO itemSO, out ConsumableGrantType grantType, out ExpBottleKind bottleKind, out DushMaterialKind materialKind, out EquipmentConsumableKind equipmentConsumableKind)
{
grantType = ConsumableGrantType.None;
bottleKind = default(ExpBottleKind);
materialKind = default(DushMaterialKind);
equipmentConsumableKind = default(EquipmentConsumableKind);
if (itemSO == null)
{
@@ -292,6 +313,13 @@ public static class StoreExpBottlePurchaseService
return true;
}
if (itemSO.associatedEquipmentConsumable != null)
{
grantType = ConsumableGrantType.EquipmentConsumable;
equipmentConsumableKind = itemSO.associatedEquipmentConsumable.consumableKind;
return true;
}
if (StoreExpBottleGrantResolver.TryResolve(itemSO, out bottleKind))
{
grantType = ConsumableGrantType.ExpBottle;