任务系统一半多,商城筛选功能,一些细节和免费包

This commit is contained in:
FloatGaming
2026-03-15 04:27:21 +08:00
parent 416190b23e
commit ec42f2e34b
344 changed files with 32225 additions and 6481 deletions
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
public static class DushMaterialCatalog
{
private static readonly DushMaterialDescriptor[] Descriptors =
{
new DushMaterialDescriptor(DushMaterialKind.Material78021, "dush_78021", "突破材料78021", "dushMaterial78021"),
new DushMaterialDescriptor(DushMaterialKind.Material78022, "dush_78022", "突破材料78022", "dushMaterial78022"),
new DushMaterialDescriptor(DushMaterialKind.Material78023, "dush_78023", "突破材料78023", "dushMaterial78023"),
new DushMaterialDescriptor(DushMaterialKind.Material78024, "dush_78024", "突破材料78024", "dushMaterial78024")
};
private static readonly Dictionary<DushMaterialKind, DushMaterialDescriptor> ByKind = BuildByKind();
private static readonly Dictionary<string, DushMaterialDescriptor> ByKey = BuildByKey();
public static IReadOnlyList<DushMaterialDescriptor> All
{
get { return Descriptors; }
}
public static string GetKey(DushMaterialKind kind)
{
return ByKind[kind].Key;
}
public static string GetDisplayName(DushMaterialKind kind)
{
return ByKind[kind].DisplayName;
}
public static bool TryGetKind(string key, out DushMaterialKind kind)
{
kind = default(DushMaterialKind);
if (string.IsNullOrEmpty(key))
{
return false;
}
DushMaterialDescriptor descriptor;
if (!ByKey.TryGetValue(key, out descriptor))
{
return false;
}
kind = descriptor.Kind;
return true;
}
public static bool TryGetLegacyFieldName(DushMaterialKind kind, out string fieldName)
{
fieldName = null;
DushMaterialDescriptor descriptor;
if (!ByKind.TryGetValue(kind, out descriptor) || string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
return false;
}
fieldName = descriptor.LegacyPlayerFieldName;
return true;
}
private static Dictionary<DushMaterialKind, DushMaterialDescriptor> BuildByKind()
{
var map = new Dictionary<DushMaterialKind, DushMaterialDescriptor>();
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Kind] = Descriptors[i];
}
return map;
}
private static Dictionary<string, DushMaterialDescriptor> BuildByKey()
{
var map = new Dictionary<string, DushMaterialDescriptor>(StringComparer.Ordinal);
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Key] = Descriptors[i];
}
return map;
}
}
public sealed class DushMaterialDescriptor
{
public DushMaterialDescriptor(DushMaterialKind kind, string key, string displayName, string legacyPlayerFieldName)
{
Kind = kind;
Key = key;
DisplayName = displayName;
LegacyPlayerFieldName = legacyPlayerFieldName;
}
public DushMaterialKind Kind { get; private set; }
public string Key { get; private set; }
public string DisplayName { get; private set; }
public string LegacyPlayerFieldName { get; private set; }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ddef3f5e3ba2bb459b87b89cbc204c1
@@ -0,0 +1,7 @@
public enum DushMaterialKind
{
Material78021,
Material78022,
Material78023,
Material78024
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5e9ca0167d7b814d8fe2d7c462606df
@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class DushMaterialLedger : MonoBehaviour
{
public static DushMaterialLedger Instance { get; private set; }
public event Action<DushMaterialKind, int> OnMaterialCountChanged;
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static DushMaterialLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_dush_material_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<DushMaterialLedger>();
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;
}
DushMaterialLedgerPayload payload;
loadedFromSave = DushMaterialLedgerStorage.TryLoad(out payload);
RebuildFromPayload(payload);
initialized = true;
SaveNow();
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
SeedFromPlayerSo(playerData, false);
loadedFromSave = true;
}
else
{
SyncToPlayerData();
}
}
public int GetCount(DushMaterialKind kind)
{
return GetCountByKey(DushMaterialCatalog.GetKey(kind));
}
public void Add(DushMaterialKind kind, int amount)
{
if (amount <= 0)
{
return;
}
ChangeCount(DushMaterialCatalog.GetKey(kind), amount);
}
public bool SeedFromPlayerSo(Player_SO playerData, bool overwriteExistingCounts)
{
if (playerData == null)
{
return false;
}
InitializeIfNeeded();
bool changed = false;
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
int legacyValue = Mathf.Max(0, playerData.GetLegacyDushMaterialCount(descriptor.LegacyPlayerFieldName));
int currentValue = GetCountByKey(descriptor.Key);
if (!overwriteExistingCounts && currentValue > 0)
{
continue;
}
if (currentValue == legacyValue)
{
continue;
}
countsByKey[descriptor.Key] = legacyValue;
changed = true;
}
if (!changed)
{
return false;
}
SaveNow();
NotifyAllCountsChanged();
return true;
}
public void SaveNow()
{
if (!initialized)
{
InitializeIfNeeded();
}
DushMaterialLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
}
private int GetCountByKey(string key)
{
InitializeIfNeeded();
int count;
if (!countsByKey.TryGetValue(key, out 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();
DushMaterialKind kind;
if (DushMaterialCatalog.TryGetKind(key, out kind) && OnMaterialCountChanged != null)
{
OnMaterialCountChanged(kind, safeValue);
}
}
private void RebuildFromPayload(DushMaterialLedgerPayload payload)
{
countsByKey.Clear();
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
countsByKey[DushMaterialCatalog.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 DushMaterialLedgerPayload BuildPayload()
{
var payload = DushMaterialLedgerStorage.CreateDefaultPayload();
payload.entries.Clear();
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
payload.entries.Add(new DushMaterialEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
}
return payload;
}
private void SyncToPlayerData()
{
if (boundPlayerData == null)
{
return;
}
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
boundPlayerData.SetLegacyDushMaterialCount(descriptor.LegacyPlayerFieldName, GetCountByKey(descriptor.Key));
}
}
private void NotifyAllCountsChanged()
{
if (OnMaterialCountChanged == null)
{
return;
}
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
OnMaterialCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ada06cb892a78874e9bf37d4a8944f3a
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
[Serializable]
public class DushMaterialEntry
{
public string key;
public int count;
}
[Serializable]
public class DushMaterialLedgerPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public List<DushMaterialEntry> entries = new List<DushMaterialEntry>();
}
[Serializable]
public class DushMaterialLedgerEnvelope
{
public string payload;
public string signature;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5723f5a049e0ef244b877816827a2678
@@ -0,0 +1,202 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class DushMaterialLedgerStorage
{
private const string SecretSeed = "ban_total.dush_material_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".dsm.dat";
private const string BackupFileName = ".dsm.bak";
private const string TempFileName = ".dsm.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out DushMaterialLedgerPayload 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(DushMaterialLedgerPayload 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($"[DushMaterialLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static DushMaterialLedgerPayload CreateDefaultPayload()
{
return new DushMaterialLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<DushMaterialEntry>()
};
}
private static bool TryReadPayload(string path, out DushMaterialLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<DushMaterialLedgerEnvelope>(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("[DushMaterialLedgerStorage] 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<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[DushMaterialLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(DushMaterialLedgerPayload 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 DushMaterialLedgerEnvelope
{
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: 86b3e9466d0c05649a03301d13dd870c
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
public static class ExpBottleCatalog
{
private static readonly ExpBottleDescriptor[] Descriptors =
{
new ExpBottleDescriptor(ExpBottleKind.Common, "exp_common", "凡品经验瓶", "commonExpBottle78001"),
new ExpBottleDescriptor(ExpBottleKind.Medium, "exp_medium", "中品经验瓶", "mediumExpBottle78002"),
new ExpBottleDescriptor(ExpBottleKind.Superior, "exp_superior", "上品经验瓶", "superiorExpBottle78003"),
new ExpBottleDescriptor(ExpBottleKind.Supreme, "exp_supreme", "极品经验瓶", "supremeExpBottle78004"),
new ExpBottleDescriptor(ExpBottleKind.Extraordinary, "exp_extraordinary", "绝品经验瓶", "extraordinaryExpBottle78005"),
new ExpBottleDescriptor(ExpBottleKind.Celestial, "exp_celestial", "仙品经验瓶", "celestialExpBottle78006"),
new ExpBottleDescriptor(ExpBottleKind.RainAll, "exp_rain_all", "雨露均沾", null),
new ExpBottleDescriptor(ExpBottleKind.AdvancedRainAll, "exp_rain_all_advanced", "高级雨露均沾", null),
new ExpBottleDescriptor(ExpBottleKind.SuperRainAll, "exp_rain_all_super", "超级雨露均沾", null)
};
private static readonly Dictionary<ExpBottleKind, ExpBottleDescriptor> ByKind = BuildByKind();
private static readonly Dictionary<string, ExpBottleDescriptor> ByKey = BuildByKey();
public static IReadOnlyList<ExpBottleDescriptor> All
{
get { return Descriptors; }
}
public static string GetKey(ExpBottleKind kind)
{
return ByKind[kind].Key;
}
public static string GetDisplayName(ExpBottleKind kind)
{
return ByKind[kind].DisplayName;
}
public static bool TryGetKind(string key, out ExpBottleKind kind)
{
kind = default(ExpBottleKind);
if (string.IsNullOrEmpty(key))
{
return false;
}
ExpBottleDescriptor descriptor;
if (!ByKey.TryGetValue(key, out descriptor))
{
return false;
}
kind = descriptor.Kind;
return true;
}
public static bool TryGetLegacyFieldName(ExpBottleKind kind, out string fieldName)
{
fieldName = null;
ExpBottleDescriptor descriptor;
if (!ByKind.TryGetValue(kind, out descriptor) || string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
return false;
}
fieldName = descriptor.LegacyPlayerFieldName;
return true;
}
private static Dictionary<ExpBottleKind, ExpBottleDescriptor> BuildByKind()
{
var map = new Dictionary<ExpBottleKind, ExpBottleDescriptor>();
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Kind] = Descriptors[i];
}
return map;
}
private static Dictionary<string, ExpBottleDescriptor> BuildByKey()
{
var map = new Dictionary<string, ExpBottleDescriptor>(StringComparer.Ordinal);
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Key] = Descriptors[i];
}
return map;
}
}
public sealed class ExpBottleDescriptor
{
public ExpBottleDescriptor(ExpBottleKind kind, string key, string displayName, string legacyPlayerFieldName)
{
Kind = kind;
Key = key;
DisplayName = displayName;
LegacyPlayerFieldName = legacyPlayerFieldName;
}
public ExpBottleKind Kind { get; private set; }
public string Key { get; private set; }
public string DisplayName { get; private set; }
public string LegacyPlayerFieldName { get; private set; }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f77f44577d9ae334bba50305822e2a54
@@ -0,0 +1,12 @@
public enum ExpBottleKind
{
Common,
Medium,
Superior,
Supreme,
Extraordinary,
Celestial,
RainAll,
AdvancedRainAll,
SuperRainAll
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c5a8cc0e8ff06254f891cfd7aebd7c81
@@ -0,0 +1,444 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
public sealed class ExpBottleLedger : MonoBehaviour
{
public static ExpBottleLedger Instance { get; private set; }
public event Action<ExpBottleKind, int> OnBottleCountChanged;
public event Action OnLedgerReloaded;
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
public bool IsReady
{
get { return initialized; }
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static ExpBottleLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_cache_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<ExpBottleLedger>();
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;
}
ExpBottleLedgerPayload payload;
loadedFromSave = ExpBottleLedgerStorage.TryLoad(out payload);
RebuildFromPayload(payload);
initialized = true;
SaveNow();
if (OnLedgerReloaded != null)
{
OnLedgerReloaded();
}
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
SeedFromPlayerSo(playerData, false);
loadedFromSave = true;
}
else
{
SyncToPlayerData();
}
}
public int GetCount(ExpBottleKind kind)
{
return GetCountByKey(ExpBottleCatalog.GetKey(kind));
}
public int GetCountByKey(string key)
{
InitializeIfNeeded();
int count;
if (!countsByKey.TryGetValue(key, out count))
{
return 0;
}
return count;
}
public IReadOnlyList<ExpBottleSnapshotEntry> GetSnapshot()
{
InitializeIfNeeded();
var result = new List<ExpBottleSnapshotEntry>(ExpBottleCatalog.All.Count);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
result.Add(new ExpBottleSnapshotEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
}
return result;
}
public void Add(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return;
}
ChangeCount(ExpBottleCatalog.GetKey(kind), amount);
}
public void AddByKey(string key, int amount)
{
if (string.IsNullOrWhiteSpace(key) || amount <= 0)
{
return;
}
ChangeCount(key.Trim(), amount);
}
public bool TryConsume(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return false;
}
var key = ExpBottleCatalog.GetKey(kind);
var current = GetCountByKey(key);
if (current < amount)
{
return false;
}
ChangeCount(key, -amount);
DailyTaskEventHub.ReportUseItem(amount);
return true;
}
public bool TryConsumeByKey(string key, int amount)
{
if (string.IsNullOrWhiteSpace(key) || amount <= 0)
{
return false;
}
var safeKey = key.Trim();
var current = GetCountByKey(safeKey);
if (current < amount)
{
return false;
}
ChangeCount(safeKey, -amount);
DailyTaskEventHub.ReportUseItem(amount);
return true;
}
public void SetCount(ExpBottleKind kind, int value)
{
var safeValue = Mathf.Max(0, value);
SetCountByKey(ExpBottleCatalog.GetKey(kind), safeValue);
}
public void ResetAllToZero()
{
InitializeIfNeeded();
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
countsByKey[ExpBottleCatalog.All[i].Key] = 0;
}
SaveNow();
NotifyAllCountsChanged();
}
public bool SeedFromPlayerSo(Player_SO playerData, bool overwriteExistingCounts)
{
if (playerData == null)
{
return false;
}
InitializeIfNeeded();
bool changed = false;
var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
var type = typeof(Player_SO);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
var field = type.GetField(descriptor.LegacyPlayerFieldName, flags);
if (field == null || field.FieldType != typeof(int))
{
continue;
}
var legacyValue = Mathf.Max(0, (int)field.GetValue(playerData));
var currentValue = GetCountByKey(descriptor.Key);
if (!overwriteExistingCounts && currentValue > 0)
{
continue;
}
if (currentValue == legacyValue)
{
continue;
}
countsByKey[descriptor.Key] = legacyValue;
changed = true;
}
if (!changed)
{
return false;
}
SaveNow();
NotifyAllCountsChanged();
return true;
}
public bool HasEnough(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return true;
}
return GetCount(kind) >= amount;
}
public bool HasEnoughByKey(string key, int amount)
{
if (amount <= 0)
{
return true;
}
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
return GetCountByKey(key.Trim()) >= amount;
}
public void SaveNow()
{
InitializeIfNeededForSave();
ExpBottleLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
}
private void InitializeIfNeededForSave()
{
if (!initialized)
{
InitializeIfNeeded();
}
}
private void ChangeCount(string key, int delta)
{
InitializeIfNeeded();
var 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)
{
InitializeIfNeeded();
int safeValue = Mathf.Max(0, value);
int current = GetCountByKey(key);
if (current == safeValue)
{
return;
}
countsByKey[key] = safeValue;
SaveNow();
ExpBottleKind kind;
if (ExpBottleCatalog.TryGetKind(key, out kind) && OnBottleCountChanged != null)
{
OnBottleCountChanged(kind, safeValue);
}
}
private void RebuildFromPayload(ExpBottleLedgerPayload payload)
{
countsByKey.Clear();
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
countsByKey[ExpBottleCatalog.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 ExpBottleLedgerPayload BuildPayload()
{
var payload = ExpBottleLedgerStorage.CreateDefaultPayload();
payload.entries.Clear();
var serializedKeys = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
payload.entries.Add(new ExpBottleEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
serializedKeys.Add(descriptor.Key);
}
foreach (var pair in countsByKey)
{
if (serializedKeys.Contains(pair.Key))
{
continue;
}
payload.entries.Add(new ExpBottleEntry
{
key = pair.Key,
count = Mathf.Max(0, pair.Value)
});
}
return payload;
}
private void NotifyAllCountsChanged()
{
if (OnBottleCountChanged == null)
{
return;
}
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
OnBottleCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key));
}
}
private void SyncToPlayerData()
{
if (boundPlayerData == null)
{
return;
}
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
boundPlayerData.SetLegacyExpBottleCount(descriptor.LegacyPlayerFieldName, GetCountByKey(descriptor.Key));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: afddc8319b116d444af5bd7bb307bb53
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
[Serializable]
public class ExpBottleEntry
{
public string key;
public int count;
}
[Serializable]
public class ExpBottleLedgerPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public List<ExpBottleEntry> entries = new List<ExpBottleEntry>();
}
[Serializable]
public class ExpBottleLedgerEnvelope
{
public string payload;
public string signature;
}
[Serializable]
public class ExpBottleSnapshotEntry
{
public string key;
public int count;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 657e3733be92d094ab912ce247c6e9e6
@@ -0,0 +1,202 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class ExpBottleLedgerStorage
{
private const string SecretSeed = "ban_total.exp_bottle_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".xpv.dat";
private const string BackupFileName = ".xpv.bak";
private const string TempFileName = ".xpv.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out ExpBottleLedgerPayload 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(ExpBottleLedgerPayload 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($"[ExpBottleLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static ExpBottleLedgerPayload CreateDefaultPayload()
{
return new ExpBottleLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<ExpBottleEntry>()
};
}
private static bool TryReadPayload(string path, out ExpBottleLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<ExpBottleLedgerEnvelope>(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("[ExpBottleLedgerStorage] 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<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[ExpBottleLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(ExpBottleLedgerPayload 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 ExpBottleLedgerEnvelope
{
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: 33984b0353b28904d8e022ea7f2355e5
@@ -0,0 +1,222 @@
using System;
using UnityEngine;
public sealed class PlayerEconomyLedger : MonoBehaviour
{
public static PlayerEconomyLedger Instance { get; private set; }
public event Action<int> OnCoinsChanged;
public event Action<int> OnMaterialChanged;
private PlayerEconomyPayload payload;
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static PlayerEconomyLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_wallet_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<PlayerEconomyLedger>();
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;
}
PlayerEconomyPayload loadedPayload;
loadedFromSave = PlayerEconomyStorage.TryLoad(out loadedPayload);
payload = loadedPayload;
initialized = true;
SaveNow();
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
payload.coins = Mathf.Max(0, playerData.Coins);
payload.material = Mathf.Max(0, playerData.Material);
SaveNow();
loadedFromSave = true;
}
SyncToPlayerData();
NotifyEconomyChanged();
}
public int GetCoins()
{
InitializeIfNeeded();
return payload.coins;
}
public int GetMaterial()
{
InitializeIfNeeded();
return payload.material;
}
public bool HasEnoughCoins(long amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
return payload.coins >= amount;
}
public void AddCoins(int amount)
{
if (amount <= 0)
{
return;
}
InitializeIfNeeded();
long next = (long)payload.coins + amount;
payload.coins = next > int.MaxValue ? int.MaxValue : (int)next;
SaveNow();
}
public void AddMaterial(int amount)
{
if (amount <= 0)
{
return;
}
InitializeIfNeeded();
long next = (long)payload.material + amount;
payload.material = next > int.MaxValue ? int.MaxValue : (int)next;
SaveNow();
}
public bool TrySpendCoins(int amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
if (payload.coins < amount)
{
return false;
}
payload.coins -= amount;
SaveNow();
DailyTaskEventHub.ReportSpendCoins(amount);
return true;
}
public void SetCoins(int value)
{
InitializeIfNeeded();
payload.coins = Mathf.Max(0, value);
SaveNow();
}
public void SaveNow()
{
InitializeIfNeededForSave();
PlayerEconomyStorage.TrySave(payload);
SyncToPlayerData();
NotifyEconomyChanged();
}
private void InitializeIfNeededForSave()
{
if (!initialized)
{
InitializeIfNeeded();
}
if (payload == null)
{
payload = PlayerEconomyStorage.CreateDefaultPayload();
}
}
private void SyncToPlayerData()
{
if (boundPlayerData == null || payload == null)
{
return;
}
boundPlayerData.SetCoins(payload.coins);
boundPlayerData.SetMaterial(payload.material);
}
private void NotifyEconomyChanged()
{
if (payload == null)
{
return;
}
if (OnCoinsChanged != null)
{
OnCoinsChanged(payload.coins);
}
if (OnMaterialChanged != null)
{
OnMaterialChanged(payload.material);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f7dcb3a6dc02924a9c4ac11aada44c5
@@ -0,0 +1,17 @@
using System;
[Serializable]
public class PlayerEconomyPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public int coins;
public int material;
}
[Serializable]
public class PlayerEconomyEnvelope
{
public string payload;
public string signature;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c23bb09c2ddc89b44b2dc1e4fdea6b31
@@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class PlayerEconomyStorage
{
private const string SecretSeed = "ban_total.player_economy.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".eco.dat";
private const string BackupFileName = ".eco.bak";
private const string TempFileName = ".eco.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out PlayerEconomyPayload 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(PlayerEconomyPayload 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($"[PlayerEconomyStorage] Save failed: {ex.Message}");
return false;
}
}
public static PlayerEconomyPayload CreateDefaultPayload()
{
return new PlayerEconomyPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
coins = 0,
material = 0
};
}
private static bool TryReadPayload(string path, out PlayerEconomyPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<PlayerEconomyEnvelope>(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("[PlayerEconomyStorage] 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<PlayerEconomyPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[PlayerEconomyStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(PlayerEconomyPayload 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 PlayerEconomyEnvelope
{
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: 10031f858527cb54e9670306e650d834
@@ -0,0 +1,101 @@
using System;
public static class StoreExpBottleGrantResolver
{
public static bool TryResolve(storeItemSO itemSO, out ExpBottleKind kind)
{
kind = default(ExpBottleKind);
if (itemSO == null)
{
return false;
}
switch (itemSO.itemID)
{
case 78001:
kind = ExpBottleKind.Common;
return true;
case 78002:
kind = ExpBottleKind.Medium;
return true;
case 78003:
kind = ExpBottleKind.Superior;
return true;
case 78004:
kind = ExpBottleKind.Supreme;
return true;
case 78005:
kind = ExpBottleKind.Extraordinary;
return true;
case 78006:
kind = ExpBottleKind.Celestial;
return true;
case 78011:
kind = ExpBottleKind.RainAll;
return true;
case 78012:
kind = ExpBottleKind.AdvancedRainAll;
return true;
case 78013:
kind = ExpBottleKind.SuperRainAll;
return true;
}
var name = (itemSO.itemName ?? string.Empty).Trim();
if (name.IndexOf("超级雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.SuperRainAll;
return true;
}
if (name.IndexOf("高级雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.AdvancedRainAll;
return true;
}
if (name.IndexOf("雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.RainAll;
return true;
}
if (name.IndexOf("凡品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Common;
return true;
}
if (name.IndexOf("中品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Medium;
return true;
}
if (name.IndexOf("上品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Superior;
return true;
}
if (name.IndexOf("极品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Supreme;
return true;
}
if (name.IndexOf("绝品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Extraordinary;
return true;
}
if (name.IndexOf("仙品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Celestial;
return true;
}
return false;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d3976e57689138418c34c6437f16798
@@ -0,0 +1,133 @@
using System.Text;
using UnityEngine;
public static class StoreExpBottlePurchaseService
{
public static bool TryPurchase(Player_SO playerData, storeItemSO itemSO, int packageCount, out string failureMessage, out int grantedCount)
{
failureMessage = string.Empty;
grantedCount = 0;
if (playerData == null)
{
failureMessage = "玩家数据丢失";
return false;
}
if (itemSO == null)
{
failureMessage = "商品数据丢失";
return false;
}
if (!itemSO.isOnShelf || !itemSO.canbepurchased)
{
failureMessage = "物品未上架";
return false;
}
if (packageCount <= 0)
{
failureMessage = "本店禁止无实物交易";
return false;
}
if (itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
{
failureMessage = "不可购买";
return false;
}
var primaryCost = itemSO.costRequirements[0];
if (primaryCost.currencyType != storeItemSO.CurrencyType.coins)
{
failureMessage = "不可购买";
return false;
}
ExpBottleKind kind;
if (!StoreExpBottleGrantResolver.TryResolve(itemSO, out kind))
{
failureMessage = "当前仅支持经验瓶发放";
return false;
}
long longGrantedCount = (long)Mathf.Max(1, itemSO.itemSinglePurchaseQty) * packageCount;
if (longGrantedCount > int.MaxValue)
{
failureMessage = "达到单次购买限额";
return false;
}
grantedCount = (int)longGrantedCount;
if (itemSO.itemPurchaseQuota >= 0 && itemSO.purchasedCount + grantedCount > itemSO.itemPurchaseQuota)
{
failureMessage = "超过限购额度";
return false;
}
long totalCostLong = (long)primaryCost.amount * packageCount;
if (totalCostLong > int.MaxValue)
{
failureMessage = "达到单次购买限额";
return false;
}
int totalCost = (int)totalCostLong;
PlayerEconomyLedger.EnsureInstance().AttachPlayerData(playerData);
ExpBottleLedger.EnsureInstance().AttachPlayerData(playerData);
if (!PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(totalCost))
{
failureMessage = "货币不足";
return false;
}
if (!PlayerEconomyLedger.EnsureInstance().TrySpendCoins(totalCost))
{
failureMessage = "货币不足";
return false;
}
ExpBottleLedger.EnsureInstance().Add(kind, grantedCount);
DebugPurchaseSuccess(itemSO, totalCost, grantedCount);
return true;
}
private static void DebugPurchaseSuccess(storeItemSO itemSO, int totalCost, int grantedCount)
{
var snapshot = ExpBottleLedger.EnsureInstance().GetSnapshot();
var builder = new StringBuilder();
builder.Append("[StorePurchase] 已成功购买:");
builder.Append(itemSO != null ? itemSO.itemName : "未知物品");
builder.Append(" | 花费Coins=");
builder.Append(totalCost);
builder.Append(" | 发放数量=");
builder.Append(grantedCount);
builder.Append(" | 经验瓶库存:");
bool appendedAny = false;
for (int i = 0; i < snapshot.Count; i++)
{
var entry = snapshot[i];
ExpBottleKind kind;
if (!ExpBottleCatalog.TryGetKind(entry.key, out kind))
{
continue;
}
if (appendedAny)
{
builder.Append(" ; ");
}
builder.Append(ExpBottleCatalog.GetDisplayName(kind));
builder.Append("=");
builder.Append(entry.count);
appendedAny = true;
}
Debug.Log(builder.ToString());
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dabd40a1c2ebdaa4b87f6ce4d3a18a3f