任务系统一半多,商城筛选功能,一些细节和免费包
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user