修了不少东西
This commit is contained in:
@@ -1140,17 +1140,37 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private static AllyHero_SO.AllyLevelInfo GetEffectiveLevelForExp(AllyHero_SO so, int exp)
|
||||
{
|
||||
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return null;
|
||||
AllyHero_SO.AllyLevelInfo best = null;
|
||||
foreach (var lvl in so.levelStats)
|
||||
|
||||
List<AllyHero_SO.AllyLevelInfo> sorted = new List<AllyHero_SO.AllyLevelInfo>();
|
||||
foreach (var levelInfo in so.levelStats)
|
||||
{
|
||||
if (lvl == null) continue;
|
||||
if (exp >= lvl.requiredEXP)
|
||||
if (levelInfo != null)
|
||||
{
|
||||
if (best == null || lvl.requiredEXP >= best.requiredEXP)
|
||||
best = lvl;
|
||||
sorted.Add(levelInfo);
|
||||
}
|
||||
}
|
||||
return best ?? so.levelStats[0];
|
||||
|
||||
if (sorted.Count == 0) return null;
|
||||
sorted.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP));
|
||||
|
||||
int expQualifiedIndex = 0;
|
||||
int safeExp = Mathf.Max(0, exp);
|
||||
for (int i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
if (safeExp >= Mathf.Max(0, sorted[i].requiredEXP))
|
||||
{
|
||||
expQualifiedIndex = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int unlockedIndex = Mathf.Clamp(so.ally_growthUnlockedTierIndex, 0, sorted.Count - 1);
|
||||
int effectiveIndex = Mathf.Min(expQualifiedIndex, unlockedIndex);
|
||||
|
||||
return sorted[Mathf.Clamp(effectiveIndex, 0, sorted.Count - 1)];
|
||||
}
|
||||
|
||||
// Scoring API
|
||||
|
||||
@@ -9,6 +9,9 @@ using UnityEditor;
|
||||
[CreateAssetMenu(fileName = "NewAllyHero", menuName = "SO_Data/AllyHero")]
|
||||
public class AllyHero_SO : ScriptableObject
|
||||
{
|
||||
private const string EquippedEquipmentSaveCategory = "ally_equipped_equipment";
|
||||
private const string EquippedEquipmentRecoverySlotPrefix = "ally_equipped_equipment_";
|
||||
|
||||
public static readonly string[] BehaviourAxisNames =
|
||||
{
|
||||
"题海战术",
|
||||
@@ -154,6 +157,13 @@ public class AllyHero_SO : ScriptableObject
|
||||
public equipmentSO equippedEquipment;
|
||||
public string equippedEquipmentId;
|
||||
|
||||
// 【性能】装备存档已从磁盘水合过一次的标记。水合后内存即权威来源:
|
||||
// SetEquippedEquipment / ApplyEquippedEquipmentPayload / ClearEquippedEquipment 都会同步更新内存与磁盘,
|
||||
// 因此无需反复解密读盘。idols 界面每次切换角色时 RebuildBag 会对"每件装备 × 每个英雄"调用
|
||||
// LoadEquippedEquipmentFromLocal 做占用检查,若每次都真正解密 SecureSaveVault 会造成严重卡顿。
|
||||
// 该标记确保每个英雄整个会话只读盘一次,之后走内存,行为完全不变。
|
||||
[System.NonSerialized] private bool equippedEquipmentHydrated;
|
||||
|
||||
public SkillDefinition GetPrimarySkill()
|
||||
{
|
||||
if (availableSkills == null || primarySkillIndex < 0 || primarySkillIndex >= availableSkills.Length) return null;
|
||||
@@ -506,12 +516,14 @@ public class AllyHero_SO : ScriptableObject
|
||||
{
|
||||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||||
int unlockedIndex = ResolveUnlockedLevelIndex(sorted);
|
||||
if (unlockedIndex < 0 || unlockedIndex >= sorted.Count)
|
||||
int expQualifiedIndex = ResolveExpQualifiedLevelIndex(sorted);
|
||||
if (unlockedIndex < 0 || expQualifiedIndex < 0 || sorted == null || sorted.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return sorted[unlockedIndex];
|
||||
int effectiveIndex = Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
|
||||
return sorted[effectiveIndex];
|
||||
}
|
||||
|
||||
private List<AllyLevelInfo> BuildSortedLevelStats()
|
||||
@@ -702,6 +714,16 @@ public class AllyHero_SO : ScriptableObject
|
||||
return "ally_equippedEquipment_" + ally_heroID;
|
||||
}
|
||||
|
||||
private string GetEquippedEquipmentSaveKey()
|
||||
{
|
||||
return ally_heroID.ToString();
|
||||
}
|
||||
|
||||
private string GetEquippedEquipmentRecoverySlotKey()
|
||||
{
|
||||
return EquippedEquipmentRecoverySlotPrefix + ally_heroID;
|
||||
}
|
||||
|
||||
private string GetSelectedSkinPrefsKey()
|
||||
{
|
||||
return "ally_selectedSkin_" + ally_heroID;
|
||||
@@ -731,13 +753,39 @@ public class AllyHero_SO : ScriptableObject
|
||||
equippedEquipmentId = equippedEquipment != null ? equippedEquipment.name : string.Empty;
|
||||
var payload = new EquippedEquipmentPayload { equippedEquipmentId = equippedEquipmentId ?? string.Empty };
|
||||
string json = JsonUtility.ToJson(payload);
|
||||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||||
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
|
||||
PlayerPrefs.SetString(GetEquippedEquipmentPrefsKey(), json);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public void LoadEquippedEquipmentFromLocal()
|
||||
public void LoadEquippedEquipmentFromLocal(bool forceReload = false)
|
||||
{
|
||||
// 已水合且非强制刷新时直接返回,避免重复解密读盘(见 equippedEquipmentHydrated 说明)。
|
||||
if (equippedEquipmentHydrated && !forceReload)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 无论后续走哪条分支,本次调用都视为已完成水合:
|
||||
// 命中存档→内存已填充;无存档→内存保持当前值,也不必反复重试读盘。
|
||||
equippedEquipmentHydrated = true;
|
||||
|
||||
string key = GetEquippedEquipmentPrefsKey();
|
||||
EquippedEquipmentPayload payload;
|
||||
if (SecureSaveVault.TryLoadJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), out payload) && payload != null)
|
||||
{
|
||||
ApplyEquippedEquipmentPayload(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(GetEquippedEquipmentRecoverySlotKey(), out payload) && payload != null)
|
||||
{
|
||||
ApplyEquippedEquipmentPayload(payload);
|
||||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PlayerPrefs.HasKey(key))
|
||||
{
|
||||
return;
|
||||
@@ -749,7 +797,19 @@ public class AllyHero_SO : ScriptableObject
|
||||
return;
|
||||
}
|
||||
|
||||
EquippedEquipmentPayload payload = JsonUtility.FromJson<EquippedEquipmentPayload>(json);
|
||||
payload = JsonUtility.FromJson<EquippedEquipmentPayload>(json);
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyEquippedEquipmentPayload(payload);
|
||||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||||
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
|
||||
}
|
||||
|
||||
private void ApplyEquippedEquipmentPayload(EquippedEquipmentPayload payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
return;
|
||||
@@ -763,6 +823,7 @@ public class AllyHero_SO : ScriptableObject
|
||||
{
|
||||
equippedEquipment = equipment;
|
||||
equippedEquipmentId = equipment != null ? equipment.name : string.Empty;
|
||||
equippedEquipmentHydrated = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
@@ -801,6 +862,9 @@ public class AllyHero_SO : ScriptableObject
|
||||
{
|
||||
equippedEquipment = null;
|
||||
equippedEquipmentId = string.Empty;
|
||||
equippedEquipmentHydrated = true;
|
||||
SecureSaveVault.Delete(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey());
|
||||
LocalRecoveryMirror.DeleteSlot(GetEquippedEquipmentRecoverySlotKey());
|
||||
PlayerPrefs.DeleteKey(GetEquippedEquipmentPrefsKey());
|
||||
PlayerPrefs.Save();
|
||||
#if UNITY_EDITOR
|
||||
|
||||
@@ -50,6 +50,10 @@ public class pressStart : MonoBehaviour
|
||||
public Image fadeOutImage;
|
||||
public float startButtonDelay = 2f; // UI starts showing after 2 seconds; allow clicking then.
|
||||
|
||||
[Header("UI_UI Jump Blocker")]
|
||||
public bool blockUIUISceneJump = false;
|
||||
public string blockUIUISceneJumpMessage = "暂不可进入。";
|
||||
|
||||
// polling
|
||||
private Coroutine pollCoroutine;
|
||||
private float pollInterval = 3f;
|
||||
@@ -252,6 +256,16 @@ public class pressStart : MonoBehaviour
|
||||
// 修改为点击一次即可开始进入游戏序列
|
||||
Debug.Log("[pressStart] Start requested, initiating transition sequence.");
|
||||
GameConfig.testMode = false;
|
||||
|
||||
if (blockUIUISceneJump)
|
||||
{
|
||||
string message = string.IsNullOrWhiteSpace(blockUIUISceneJumpMessage)
|
||||
? "暂不可进入。"
|
||||
: blockUIUISceneJumpMessage;
|
||||
gNotice.error.display(message);
|
||||
Debug.LogWarning("[pressStart] UI_UI scene jump blocked: " + message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (warningText != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
public static class TeamSelectionDefaults
|
||||
{
|
||||
public const int SlotCount = 5;
|
||||
|
||||
private static readonly int[] DefaultHeroIds = { 30201, 30202, 30203, 30204, 30205 };
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
EnsureDefaultTeamIfMissing();
|
||||
}
|
||||
|
||||
public static void EnsureDefaultTeamIfMissing()
|
||||
{
|
||||
for (int slot = 1; slot <= SlotCount; slot++)
|
||||
{
|
||||
if (PlayerPrefs.HasKey(GetHeroSlotKey(slot)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (int slot = 1; slot <= SlotCount; slot++)
|
||||
{
|
||||
PlayerPrefs.SetInt(GetHeroSlotKey(slot), GetDefaultHeroId(slot));
|
||||
}
|
||||
|
||||
PlayerPrefs.SetInt("SelectedMainHeroID", DefaultHeroIds[0]);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static int ReadHeroId(int slot)
|
||||
{
|
||||
EnsureDefaultTeamIfMissing();
|
||||
return Mathf.Max(0, PlayerPrefs.GetInt(GetHeroSlotKey(slot), GetDefaultHeroId(slot)));
|
||||
}
|
||||
|
||||
public static int GetDefaultHeroId(int slot)
|
||||
{
|
||||
int index = Mathf.Clamp(slot - 1, 0, DefaultHeroIds.Length - 1);
|
||||
return DefaultHeroIds[index];
|
||||
}
|
||||
|
||||
private static string GetHeroSlotKey(int slot)
|
||||
{
|
||||
int safeSlot = Mathf.Clamp(slot, 1, SlotCount);
|
||||
return $"selected_heroSlot0{safeSlot}_heroID";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dfd23d5af82d6045b465356990ca8b9
|
||||
@@ -147,21 +147,21 @@ public class TeamSelectorAnimController : MonoBehaviour
|
||||
|
||||
void CacheTeamSnapshot()
|
||||
{
|
||||
cachedTeam[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
|
||||
cachedTeam[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
|
||||
cachedTeam[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
|
||||
cachedTeam[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
|
||||
cachedTeam[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
|
||||
cachedTeam[0] = TeamSelectionDefaults.ReadHeroId(1);
|
||||
cachedTeam[1] = TeamSelectionDefaults.ReadHeroId(2);
|
||||
cachedTeam[2] = TeamSelectionDefaults.ReadHeroId(3);
|
||||
cachedTeam[3] = TeamSelectionDefaults.ReadHeroId(4);
|
||||
cachedTeam[4] = TeamSelectionDefaults.ReadHeroId(5);
|
||||
}
|
||||
|
||||
bool HasTeamChanged()
|
||||
{
|
||||
if (teamChanged) return true;
|
||||
if (cachedTeam[0] != PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0)) return true;
|
||||
if (cachedTeam[1] != PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0)) return true;
|
||||
if (cachedTeam[2] != PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0)) return true;
|
||||
if (cachedTeam[3] != PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0)) return true;
|
||||
if (cachedTeam[4] != PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0)) return true;
|
||||
if (cachedTeam[0] != TeamSelectionDefaults.ReadHeroId(1)) return true;
|
||||
if (cachedTeam[1] != TeamSelectionDefaults.ReadHeroId(2)) return true;
|
||||
if (cachedTeam[2] != TeamSelectionDefaults.ReadHeroId(3)) return true;
|
||||
if (cachedTeam[3] != TeamSelectionDefaults.ReadHeroId(4)) return true;
|
||||
if (cachedTeam[4] != TeamSelectionDefaults.ReadHeroId(5)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -289,12 +289,13 @@ public class newTeamSelector : MonoBehaviour
|
||||
|
||||
private int[] ReadSelectedHeroIdsFromPlayerPrefs()
|
||||
{
|
||||
TeamSelectionDefaults.EnsureDefaultTeamIfMissing();
|
||||
int[] ids = new int[Mathf.Max(5, slotCount)];
|
||||
if (ids.Length > 0) ids[0] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0));
|
||||
if (ids.Length > 1) ids[1] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0));
|
||||
if (ids.Length > 2) ids[2] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0));
|
||||
if (ids.Length > 3) ids[3] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0));
|
||||
if (ids.Length > 4) ids[4] = Mathf.Max(0, PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0));
|
||||
if (ids.Length > 0) ids[0] = TeamSelectionDefaults.ReadHeroId(1);
|
||||
if (ids.Length > 1) ids[1] = TeamSelectionDefaults.ReadHeroId(2);
|
||||
if (ids.Length > 2) ids[2] = TeamSelectionDefaults.ReadHeroId(3);
|
||||
if (ids.Length > 3) ids[3] = TeamSelectionDefaults.ReadHeroId(4);
|
||||
if (ids.Length > 4) ids[4] = TeamSelectionDefaults.ReadHeroId(5);
|
||||
return ids;
|
||||
}
|
||||
|
||||
|
||||
@@ -1679,7 +1679,7 @@ public static class MailRewardGrantService
|
||||
resolved = CreateBasicResolved(ResolvedKind.PlayerExp, amount, rewardName, reward.reward_image != null ? reward.reward_image : _playerExpRewardIcon, reward.reward_description, reward, "\u73a9\u5bb6\u7ecf\u9a8c");
|
||||
return true;
|
||||
case mail_so.reward_type.money:
|
||||
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image != null ? reward.reward_image : _coinRewardIcon, reward.reward_description, reward, "\u91d1\u5e01");
|
||||
resolved = CreateBasicResolved(ResolvedKind.Coins, amount, rewardName, reward.reward_image != null ? reward.reward_image : _coinRewardIcon, reward.reward_description, reward, "算力");
|
||||
return true;
|
||||
case mail_so.reward_type.metarial:
|
||||
resolved = CreateBasicResolved(ResolvedKind.Material, amount, rewardName, reward.reward_image != null ? reward.reward_image : _materialRewardIcon, reward.reward_description, reward, "\u8bb0\u5fc6\u788e\u7247");
|
||||
|
||||
@@ -7268,6 +7268,7 @@ GameObject:
|
||||
- component: {fileID: 1348388220439716234}
|
||||
- component: {fileID: 6137481741208887361}
|
||||
- component: {fileID: 1863233853179500241}
|
||||
- component: {fileID: 1338150670383926109}
|
||||
m_Layer: 5
|
||||
m_Name: Text_PLAYER
|
||||
m_TagString: Untagged
|
||||
@@ -7291,9 +7292,9 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: -80.7, y: -31.391098}
|
||||
m_SizeDelta: {x: 273.4951, y: 26.8889}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 56.047485, y: -31.391068}
|
||||
m_SizeDelta: {x: 0, y: 26.8889}
|
||||
m_Pivot: {x: 1, y: 0.5}
|
||||
--- !u!222 &6137481741208887361
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -7315,19 +7316,19 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.23921569, g: 0.21960784, b: 0.40392157, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 0
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: 2dfc162c344875b4da01e6a15073dce5, type: 3}
|
||||
m_FontSize: 26
|
||||
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
|
||||
m_FontSize: 20
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 1
|
||||
m_MinSize: 2
|
||||
m_BestFit: 0
|
||||
m_MinSize: 0
|
||||
m_MaxSize: 26
|
||||
m_Alignment: 5
|
||||
m_AlignByGeometry: 0
|
||||
@@ -7335,7 +7336,21 @@ MonoBehaviour:
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: USERNAME
|
||||
m_Text: "USERNAME\u8FD9\u662F\u7528\u6237\u7684\u795E\u79D8\u7528\u6237\u540D\u662F12123"
|
||||
--- !u!114 &1338150670383926109
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5745207992015071035}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
--- !u!1 &5751219486557543197
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -313,6 +313,12 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[AllyHeroDeployLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older idol growth data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncAllMirrorFlags();
|
||||
SyncAllLegacySelectedSlotExpKeys();
|
||||
AllyHeroDeployLedgerStorage.TrySave(BuildPayload());
|
||||
|
||||
@@ -128,20 +128,24 @@ public static class AllyHeroDeployLedgerStorage
|
||||
byte[] encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
AllyHeroDeployLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
string payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,25 +212,19 @@ public static class AllyHeroDeployLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
byte[] hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -234,21 +232,25 @@ public static class AllyHeroDeployLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (SHA256 sha = SHA256.Create())
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -197,6 +197,12 @@ public sealed class DushMaterialLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[DushMaterialLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
DushMaterialLedgerStorage.TrySave(BuildPayload());
|
||||
SyncToPlayerData();
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class DushMaterialLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
DushMaterialLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class DushMaterialLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class DushMaterialLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -10,6 +10,8 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
|
||||
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
@@ -65,11 +67,18 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
}
|
||||
|
||||
EquipmentConsumableLedgerPayload payload;
|
||||
EquipmentConsumableLedgerStorage.TryLoad(out payload);
|
||||
loadedFromSave = EquipmentConsumableLedgerStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || EquipmentConsumableLedgerStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
initialized = true;
|
||||
SyncMirrorCounts();
|
||||
SaveNow();
|
||||
|
||||
// If an older save exists but cannot be read right now, do not overwrite it
|
||||
// with an empty default payload during startup.
|
||||
if (loadedFromSave || !hasAnyRecoverableLocalState)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
}
|
||||
|
||||
public int GetCount(EquipmentConsumableKind kind)
|
||||
@@ -126,6 +135,12 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[EquipmentConsumableLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
EquipmentConsumableLedgerStorage.TrySave(BuildPayload());
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasEquipmentConsumableBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
@@ -136,20 +137,24 @@ public static class EquipmentConsumableLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
EquipmentConsumableLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,25 +207,19 @@ public static class EquipmentConsumableLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -228,21 +227,25 @@ public static class EquipmentConsumableLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -295,6 +295,13 @@ public sealed class ExpBottleLedger : MonoBehaviour
|
||||
public void SaveNow()
|
||||
{
|
||||
InitializeIfNeededForSave();
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[ExpBottleLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
|
||||
return;
|
||||
}
|
||||
|
||||
SyncMirrorCounts();
|
||||
ExpBottleLedgerStorage.TrySave(BuildPayload());
|
||||
SyncToPlayerData();
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class ExpBottleLedgerStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
ExpBottleLedgerPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class ExpBottleLedgerStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class ExpBottleLedgerStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -48,11 +48,99 @@ public static class FirstRunFactoryResetService
|
||||
return;
|
||||
}
|
||||
|
||||
// 物理文件兜底探测(最后一道防线):
|
||||
// HasAnyRecoverableLocalState() 依赖各 Storage 的解密/结构判断,若因根目录别名未覆盖、
|
||||
// 或存档结构异常而误判为"无存档",仍可能走到出厂重置。这里直接扫描磁盘上是否存在
|
||||
// 任何存档物理文件(不解密、只看存在),只要有就绝不重置,宁可等待其它恢复路径。
|
||||
if (HasAnyPhysicalSaveFileOnDisk())
|
||||
{
|
||||
Debug.LogWarning("[FactoryReset] 磁盘上存在存档物理文件但未能读出," +
|
||||
"为避免误删玩家数据,跳过出厂重置并补写初始化标记。");
|
||||
EnsureInitializedMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。");
|
||||
ApplyFactoryReset();
|
||||
EnsureInitializedMarker();
|
||||
}
|
||||
|
||||
// 扫描所有可能的存档根目录(含新旧包名/公司名别名),只要磁盘上存在任意一个存档物理文件
|
||||
// 就返回 true。只判存在、不解密——因此不受 deviceUniqueIdentifier 变化影响。
|
||||
private static bool HasAnyPhysicalSaveFileOnDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) 加密存档目录 .cache_bridge:递归找任意 .dat / .bak
|
||||
string vaultDir = Path.Combine(root, ".cache_bridge");
|
||||
if (DirectoryHasFileWithAnyExtension(vaultDir, new[] { ".dat", ".bak" }))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2) 设备无关明文备份 player_progress.bbackup
|
||||
if (File.Exists(Path.Combine(root, "player_progress.bbackup")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3) 恢复镜像目录 .save_recovery:存在任意文件即视为有存档
|
||||
string recoveryDir = Path.Combine(root, ".save_recovery");
|
||||
if (DirectoryHasAnyFile(recoveryDir))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 探测失败时采取保守策略:报告"存在存档",宁可跳过重置也不误删。
|
||||
Debug.LogWarning("[FactoryReset] 物理存档探测异常,保守跳过出厂重置: " + ex.Message);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DirectoryHasFileWithAnyExtension(string directory, string[] extensions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < extensions.Length; i++)
|
||||
{
|
||||
string[] matches = Directory.GetFiles(directory, "*" + extensions[i], SearchOption.AllDirectories);
|
||||
if (matches != null && matches.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DirectoryHasAnyFile(string directory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] matches = Directory.GetFiles(directory, "*", SearchOption.AllDirectories);
|
||||
return matches != null && matches.Length > 0;
|
||||
}
|
||||
|
||||
private static bool HasInitializedMarker()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -238,6 +238,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
public void SaveNow()
|
||||
{
|
||||
InitializeIfNeededForSave();
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[PlayerEconomyLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older economy data.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerEconomyStorage.TrySave(payload);
|
||||
SyncToPlayerData();
|
||||
GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins);
|
||||
|
||||
@@ -138,21 +138,25 @@ public static class PlayerEconomyStorage
|
||||
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
PlayerEconomyPayload loadedPayload = null;
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,25 +223,19 @@ public static class PlayerEconomyStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -245,21 +243,25 @@ public static class PlayerEconomyStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -191,6 +191,12 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
payload = CreateDefaultPayload();
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[PlayerExperienceLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older player experience data.");
|
||||
return;
|
||||
}
|
||||
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
|
||||
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
|
||||
|
||||
@@ -78,6 +78,12 @@ public static class PlayerProgressBackupService
|
||||
return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null;
|
||||
}
|
||||
|
||||
public static bool HasEquipmentConsumableBackup()
|
||||
{
|
||||
PlayerProgressBackupBundle bundle;
|
||||
return TryLoadBundle(out bundle) && bundle != null && bundle.equipmentConsumable != null;
|
||||
}
|
||||
|
||||
public static bool HasStoreOwnershipBackup()
|
||||
{
|
||||
PlayerProgressBackupBundle bundle;
|
||||
|
||||
@@ -301,7 +301,7 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
if (grantedCoins > 0)
|
||||
{
|
||||
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
|
||||
grantedEntries.Add(gItemGet.Create("\u91d1\u5e01", null, grantedCoins, new Color32(240, 196, 74, 255)));
|
||||
grantedEntries.Add(gItemGet.Create("算力", null, grantedCoins, new Color32(240, 196, 74, 255)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
|
||||
int grantedCoins = purchaseAmount * 4;
|
||||
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
|
||||
rewardEntry = gItemGet.Create("\u91d1\u5e01", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
|
||||
rewardEntry = gItemGet.Create("算力", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,69 @@ public static class SaveIdentityUtility
|
||||
"com.DefaultCompany.EaseOut_Main_FreshNew"
|
||||
};
|
||||
|
||||
// 设备无关绑定令牌:新版存档的加密/签名统一改用它,取代 SystemInfo.deviceUniqueIdentifier。
|
||||
// 根因修复——deviceUniqueIdentifier 在安卓更新/换签名/换渠道后会变化,导致旧密钥永远解不出存档。
|
||||
// 用固定令牌后,更新或换机都不再影响解密。读取时仍会尝试真实设备 ID 变体以兼容本机旧存档。
|
||||
public const string DeviceIndependentBindingToken = "device_independent_binding_v1";
|
||||
|
||||
// 【性能】这些值在进程生命周期内恒定:SystemInfo.deviceUniqueIdentifier、Application.identifier/
|
||||
// companyName/productName 在安卓上开销较大(尤其 deviceUniqueIdentifier 涉及系统调用与哈希)。
|
||||
// 存档每次读取/验签都会取用它们,若每次重算并分配 List,会在 UI 大量刷新(如 idols 角色切换、
|
||||
// 场景切换批量重建)时造成明显卡顿。这里缓存一次,之后直接复用,行为完全不变。
|
||||
private static IReadOnlyList<string> s_cachedApplicationIdentifierVariants;
|
||||
private static IReadOnlyList<string> s_cachedDeviceBindingVariants;
|
||||
private static string s_cachedDeviceUniqueIdentifier;
|
||||
private static bool s_deviceUniqueIdentifierResolved;
|
||||
|
||||
public static string GetPrimaryApplicationIdentifier()
|
||||
{
|
||||
return StableApplicationIdentifier;
|
||||
}
|
||||
|
||||
// 写入存档时使用的设备绑定:固定为设备无关令牌。
|
||||
public static string GetPrimaryDeviceBinding()
|
||||
{
|
||||
return DeviceIndependentBindingToken;
|
||||
}
|
||||
|
||||
// 读取/验签时尝试的设备绑定变体,按命中概率排序:
|
||||
// 1) 设备无关令牌(新版写入所用,最常命中)
|
||||
// 2) 当前设备真实 deviceUniqueIdentifier(兼容本机旧版写入的设备绑定存档,首次读到后会被重写为设备无关,实现无缝迁移)
|
||||
// 结果缓存,避免每次读取都触发一次昂贵的 deviceUniqueIdentifier 查询与 List 分配。
|
||||
public static IReadOnlyList<string> GetDeviceBindingVariants()
|
||||
{
|
||||
if (s_cachedDeviceBindingVariants != null)
|
||||
{
|
||||
return s_cachedDeviceBindingVariants;
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
AddDistinct(result, DeviceIndependentBindingToken);
|
||||
AddDistinct(result, SafeDeviceUniqueIdentifier());
|
||||
s_cachedDeviceBindingVariants = result;
|
||||
return s_cachedDeviceBindingVariants;
|
||||
}
|
||||
|
||||
private static string SafeDeviceUniqueIdentifier()
|
||||
{
|
||||
if (s_deviceUniqueIdentifierResolved)
|
||||
{
|
||||
return s_cachedDeviceUniqueIdentifier;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
s_cachedDeviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
s_cachedDeviceUniqueIdentifier = null;
|
||||
}
|
||||
|
||||
s_deviceUniqueIdentifierResolved = true;
|
||||
return s_cachedDeviceUniqueIdentifier;
|
||||
}
|
||||
|
||||
public static string GetCanonicalPersistentRoot()
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer)
|
||||
@@ -50,6 +108,11 @@ public static class SaveIdentityUtility
|
||||
|
||||
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
|
||||
{
|
||||
if (s_cachedApplicationIdentifierVariants != null)
|
||||
{
|
||||
return s_cachedApplicationIdentifierVariants;
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
AddDistinct(result, StableApplicationIdentifier);
|
||||
AddDistinct(result, Application.identifier);
|
||||
@@ -74,7 +137,8 @@ public static class SaveIdentityUtility
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
s_cachedApplicationIdentifierVariants = result;
|
||||
return s_cachedApplicationIdentifierVariants;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
|
||||
|
||||
@@ -373,12 +373,7 @@ public static class SecureSaveVault
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(category, payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] ProtectBytes(string category, string key, byte[] plainBytes)
|
||||
@@ -394,7 +389,7 @@ public static class SecureSaveVault
|
||||
{
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
aes.GenerateIV();
|
||||
using (var encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
@@ -419,37 +414,41 @@ public static class SecureSaveVault
|
||||
}
|
||||
#endif
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
using (var aes = Aes.Create())
|
||||
for (int dv = 0; dv < deviceBindingVariants.Count; dv++)
|
||||
{
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, identifierVariants[i]);
|
||||
int ivLength = aes.BlockSize / 8;
|
||||
if (protectedBytes == null || protectedBytes.Length <= ivLength)
|
||||
using (var aes = Aes.Create())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] iv = new byte[ivLength];
|
||||
byte[] cipher = new byte[protectedBytes.Length - ivLength];
|
||||
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
|
||||
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
|
||||
aes.IV = iv;
|
||||
try
|
||||
{
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = BuildAesKey(category, key, identifierVariants[i], deviceBindingVariants[dv]);
|
||||
int ivLength = aes.BlockSize / 8;
|
||||
if (protectedBytes == null || protectedBytes.Length <= ivLength)
|
||||
{
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
if (plainBytes != null)
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] iv = new byte[ivLength];
|
||||
byte[] cipher = new byte[protectedBytes.Length - ivLength];
|
||||
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
|
||||
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
|
||||
aes.IV = iv;
|
||||
try
|
||||
{
|
||||
using (var decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
return true;
|
||||
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
|
||||
if (plainBytes != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,7 +475,7 @@ public static class SecureSaveVault
|
||||
|
||||
try
|
||||
{
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
|
||||
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()), s_dpapiCurrentUserScope }) as byte[];
|
||||
return protectedBytes != null && protectedBytes.Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -496,18 +495,22 @@ public static class SecureSaveVault
|
||||
}
|
||||
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
|
||||
if (plainBytes != null && plainBytes.Length > 0)
|
||||
try
|
||||
{
|
||||
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i], deviceBindingVariants[d]), s_dpapiCurrentUserScope }) as byte[];
|
||||
if (plainBytes != null && plainBytes.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,20 +563,20 @@ public static class SecureSaveVault
|
||||
}
|
||||
#endif
|
||||
|
||||
private static byte[] BuildEntropy(string category, string applicationIdentifier)
|
||||
private static byte[] BuildEntropy(string category, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
|
||||
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
|
||||
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category + "|" + key;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -635,21 +638,25 @@ public static class SecureSaveVault
|
||||
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
|
||||
{
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
|
||||
|
||||
@@ -97,13 +97,13 @@ public static class StoreExpBottlePurchaseService
|
||||
|
||||
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
|
||||
{
|
||||
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
|
||||
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
|
||||
{
|
||||
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
|
||||
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public static class StoreExpBottlePurchaseService
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("[StorePurchase] 已成功购买:");
|
||||
builder.Append(itemSO != null ? itemSO.itemName : LocalizationService.Get("item.unknown.store_item", "Unknown Item"));
|
||||
builder.Append(" | 花费Coins=");
|
||||
builder.Append(" | 花费算力=");
|
||||
builder.Append(totalCost);
|
||||
builder.Append(" | 发放数量=");
|
||||
builder.Append(grantedCount);
|
||||
@@ -341,6 +341,11 @@ public static class StoreExpBottlePurchaseService
|
||||
|| currencyType == storeItemSO.CurrencyType.material;
|
||||
}
|
||||
|
||||
private static string GetInsufficientCurrencyMessage(storeItemSO.CurrencyType currencyType)
|
||||
{
|
||||
return currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
|
||||
}
|
||||
|
||||
private static bool HasEnoughCurrency(storeItemSO.CurrencyType currencyType, int amount)
|
||||
{
|
||||
switch (currencyType)
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
|
||||
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private int lastSaveFrame = -1;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
@@ -73,8 +75,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
|
||||
initialized = true;
|
||||
StoreOwnershipPayload payload;
|
||||
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
|
||||
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
|
||||
loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
LoadStoreItems();
|
||||
|
||||
@@ -208,6 +210,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loadedFromSave && hasAnyRecoverableLocalState)
|
||||
{
|
||||
Debug.LogWarning("[StoreOwnershipLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older store ownership data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -138,20 +138,24 @@ public static class StoreOwnershipStorage
|
||||
var encryptedBytes = Convert.FromBase64String(envelope.payload);
|
||||
StoreOwnershipPayload loadedPayload = null;
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
|
||||
{
|
||||
try
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
try
|
||||
{
|
||||
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
|
||||
var payloadJson = Encoding.UTF8.GetString(plainBytes);
|
||||
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
|
||||
if (loadedPayload != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,25 +222,19 @@ public static class StoreOwnershipStorage
|
||||
|
||||
private static string ComputeSignature(string payloadBase64)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes()
|
||||
{
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
|
||||
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
|
||||
}
|
||||
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier)
|
||||
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
|
||||
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
|
||||
}
|
||||
}
|
||||
@@ -244,21 +242,25 @@ public static class StoreOwnershipStorage
|
||||
private static bool TryValidateSignature(string payloadBase64, string signature)
|
||||
{
|
||||
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
|
||||
for (int i = 0; i < identifierVariants.Count; i++)
|
||||
{
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
for (int d = 0; d < deviceBindingVariants.Count; d++)
|
||||
{
|
||||
return true;
|
||||
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
|
||||
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
|
||||
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
|
||||
{
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
|
||||
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(signText);
|
||||
|
||||
@@ -398,7 +398,7 @@ public class dailyTaskManager : MonoBehaviour
|
||||
switch (definition.rewardType)
|
||||
{
|
||||
case userTasksPool.RewardType.coins:
|
||||
return gItemGet.Create("Coins", coinRewardSprite, Mathf.Max(1, definition.rewardAmount), ItemRarity.Common);
|
||||
return gItemGet.Create("算力", coinRewardSprite, Mathf.Max(1, definition.rewardAmount), ItemRarity.Common);
|
||||
|
||||
case userTasksPool.RewardType.expBottle:
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 1
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 50
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -30,7 +30,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 3
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 150
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -41,7 +41,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 99
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 120
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -52,7 +52,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 1000000
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 120
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -63,7 +63,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 10000000
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 300
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -74,7 +74,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 1
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 50
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -86,7 +86,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 1
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 100
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -97,18 +97,18 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 900
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 100
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
selectionWeight: 1
|
||||
- taskID: 10109
|
||||
description: "\u6D88\u8D39 2000 coins"
|
||||
description: "\u6D88\u8D39 2000 \u7B97\u529B"
|
||||
taskType: 7
|
||||
refreshFrequency: 1
|
||||
targetValue: 2000
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 200
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
@@ -119,7 +119,7 @@ MonoBehaviour:
|
||||
refreshFrequency: 1
|
||||
targetValue: 3
|
||||
rewardType: 0
|
||||
rewardAmount: 1
|
||||
rewardAmount: 180
|
||||
rewardExpBottleKind: 0
|
||||
rewardDushMaterialKind: 0
|
||||
addToTaskPool: 1
|
||||
|
||||
@@ -1289,7 +1289,7 @@ public class GameManager : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
|
||||
// Resume using PauseManager
|
||||
PauseManager.Instance?.Pause(false);
|
||||
@@ -1349,7 +1349,7 @@ public class GameManager : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
@@ -1441,7 +1441,7 @@ public class GameManager : MonoBehaviour
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
// gNotice.recommendation.display(LocalizationService.Get("gameplay.notice.game_start", "游戏开始!"));
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.EnhancedTouch;
|
||||
using ETouch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
|
||||
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
|
||||
///
|
||||
/// 为什么用这种方式而不是 UI 的 IPointerDownHandler:
|
||||
/// 【多点触控根因】项目 activeInputHandler = Both(同时启用新旧输入系统)。
|
||||
/// 在这种配置下,旧版 UnityEngine.Input.touches 走的是兼容 shim,同时按多指时
|
||||
/// 常常只上报一个触点 → 只能触发一个轨道。所以这里优先用新输入系统的
|
||||
/// EnhancedTouch(Touch.activeTouches),它能可靠上报所有并发手指;
|
||||
/// 仅在没有新输入系统时回退到旧版 Input.touches。判定逻辑完全不变。
|
||||
///
|
||||
/// 为什么用物理命中而不是 UI 的 IPointerDownHandler:
|
||||
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 Collider(OverlapPoint 支持旋转过的碰撞体)。
|
||||
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 fingerId 配对按下/抬起)。
|
||||
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 touchId 配对按下/抬起)。
|
||||
///
|
||||
/// 判定逻辑完全不变:这里只调用现成的 PressTrack/ReleaseTrack,
|
||||
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTime(dspTime)。
|
||||
///
|
||||
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 gameplayCamera(留空则自动取 Camera.main)。
|
||||
@@ -35,10 +45,8 @@ public class TrackTouchInput : MonoBehaviour
|
||||
"留空自动按相机 orthographic 判断;一般不用手动改。")]
|
||||
[SerializeField] private bool forceOrthographicMode = false;
|
||||
|
||||
// fingerId -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
|
||||
// 触点 id -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
|
||||
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
|
||||
// 鼠标模拟用的"手指 id",取一个不会和真实 fingerId 冲突的值。
|
||||
private const int MouseFingerId = -100;
|
||||
private int _mouseHeldTrack = -1;
|
||||
|
||||
private Camera Cam
|
||||
@@ -50,14 +58,13 @@ public class TrackTouchInput : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
private void OnEnable()
|
||||
{
|
||||
// 【多点触控根因修复】若 multiTouchEnabled 为 false,Input.touchCount 永远 ≤1,
|
||||
// 同时按多条轨道时只会报告一个触点 → 只能触发一个轨道。强制开启多点触控。
|
||||
// simulateMouseWithTouches 关掉:避免触摸被合成成鼠标事件,与真实多指冲突。
|
||||
Input.multiTouchEnabled = true;
|
||||
Input.simulateMouseWithTouches = false;
|
||||
// 开启增强触摸,Touch.activeTouches 才会被填充。
|
||||
EnhancedTouchSupport.Enable();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Update()
|
||||
{
|
||||
@@ -67,50 +74,101 @@ public class TrackTouchInput : MonoBehaviour
|
||||
Camera cam = Cam;
|
||||
if (cam == null) return;
|
||||
|
||||
ProcessTouches(im, cam);
|
||||
int touchCount = ProcessTouches(im, cam);
|
||||
|
||||
if (enableMouseFallback && Input.touchCount == 0)
|
||||
if (enableMouseFallback && touchCount == 0)
|
||||
{
|
||||
ProcessMouse(im, cam);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTouches(InputManager im, Camera cam)
|
||||
/// <summary>处理所有并发触点,返回本帧触点数量。</summary>
|
||||
private int ProcessTouches(InputManager im, Camera cam)
|
||||
{
|
||||
for (int i = 0; i < Input.touchCount; i++)
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
// 优先走新输入系统的增强触摸:可靠上报所有并发手指。
|
||||
var touches = ETouch.activeTouches;
|
||||
int count = touches.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ETouch t = touches[i];
|
||||
switch (t.phase)
|
||||
{
|
||||
case UnityEngine.InputSystem.TouchPhase.Began:
|
||||
HandleTouchBegan(im, cam, t.touchId, t.screenPosition);
|
||||
break;
|
||||
case UnityEngine.InputSystem.TouchPhase.Ended:
|
||||
case UnityEngine.InputSystem.TouchPhase.Canceled:
|
||||
HandleTouchEnded(im, t.touchId);
|
||||
break;
|
||||
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属。
|
||||
}
|
||||
}
|
||||
return count;
|
||||
#else
|
||||
// 回退:旧版 Input.touches(仅在未启用新输入系统时使用)。
|
||||
int count = Input.touchCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Touch t = Input.GetTouch(i);
|
||||
|
||||
switch (t.phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
{
|
||||
int track = ResolveTrack(cam, t.position);
|
||||
if (track >= 0)
|
||||
{
|
||||
_activeTouches[t.fingerId] = track;
|
||||
im.PressTrack(track);
|
||||
}
|
||||
HandleTouchBegan(im, cam, t.fingerId, t.position);
|
||||
break;
|
||||
}
|
||||
case TouchPhase.Ended:
|
||||
case TouchPhase.Canceled:
|
||||
{
|
||||
if (_activeTouches.TryGetValue(t.fingerId, out int track))
|
||||
{
|
||||
_activeTouches.Remove(t.fingerId);
|
||||
im.ReleaseTrack(track);
|
||||
}
|
||||
HandleTouchEnded(im, t.fingerId);
|
||||
break;
|
||||
}
|
||||
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属,
|
||||
// 避免手指轻微滑动跨到相邻轨道时反复 Press/Release 造成误判。
|
||||
}
|
||||
}
|
||||
return count;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void HandleTouchBegan(InputManager im, Camera cam, int touchId, Vector2 screenPos)
|
||||
{
|
||||
int track = ResolveTrack(cam, screenPos);
|
||||
if (track >= 0)
|
||||
{
|
||||
_activeTouches[touchId] = track;
|
||||
im.PressTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTouchEnded(InputManager im, int touchId)
|
||||
{
|
||||
if (_activeTouches.TryGetValue(touchId, out int track))
|
||||
{
|
||||
_activeTouches.Remove(touchId);
|
||||
im.ReleaseTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessMouse(InputManager im, Camera cam)
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
var mouse = Mouse.current;
|
||||
if (mouse == null) return;
|
||||
|
||||
if (mouse.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
int track = ResolveTrack(cam, mouse.position.ReadValue());
|
||||
if (track >= 0)
|
||||
{
|
||||
_mouseHeldTrack = track;
|
||||
im.PressTrack(track);
|
||||
}
|
||||
}
|
||||
else if (mouse.leftButton.wasReleasedThisFrame)
|
||||
{
|
||||
if (_mouseHeldTrack >= 0)
|
||||
{
|
||||
im.ReleaseTrack(_mouseHeldTrack);
|
||||
_mouseHeldTrack = -1;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
int track = ResolveTrack(cam, Input.mousePosition);
|
||||
@@ -128,6 +186,7 @@ public class TrackTouchInput : MonoBehaviour
|
||||
_mouseHeldTrack = -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,8 +194,7 @@ public class TrackTouchInput : MonoBehaviour
|
||||
///
|
||||
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
|
||||
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
|
||||
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点,
|
||||
/// 避免因相机 Z 变化(如 cameraDash)导致命中点偏移。
|
||||
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点。
|
||||
/// </summary>
|
||||
private int ResolveTrack(Camera cam, Vector2 screenPos)
|
||||
{
|
||||
@@ -149,7 +207,6 @@ public class TrackTouchInput : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// 透视:射线与轨道平面(法线 +Z,过 z = trackPlaneWorldZ)求交。
|
||||
Ray ray = cam.ScreenPointToRay(screenPos);
|
||||
float denom = ray.direction.z;
|
||||
if (Mathf.Abs(denom) < 1e-6f)
|
||||
@@ -187,5 +244,9 @@ public class TrackTouchInput : MonoBehaviour
|
||||
}
|
||||
_activeTouches.Clear();
|
||||
_mouseHeldTrack = -1;
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
EnhancedTouchSupport.Disable();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,30 +201,51 @@ namespace GameServer.Client
|
||||
|
||||
public async Task<ArenaRoomSnapshot> CreateRoom(string songId, string songName, string difficulty, string password = null)
|
||||
{
|
||||
await EnsureConnected();
|
||||
JObject resp = await SendAction("create_room", new
|
||||
try
|
||||
{
|
||||
song_id = songId,
|
||||
song_name = songName,
|
||||
difficulty = difficulty,
|
||||
password = string.IsNullOrWhiteSpace(password) ? null : password
|
||||
});
|
||||
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("create_room", resp);
|
||||
SetCurrentRoom(snapshot);
|
||||
return snapshot;
|
||||
await EnsureConnected();
|
||||
JObject resp = await SendAction("create_room", new
|
||||
{
|
||||
song_id = songId,
|
||||
song_name = songName,
|
||||
difficulty = difficulty,
|
||||
password = string.IsNullOrWhiteSpace(password) ? null : password
|
||||
});
|
||||
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("create_room", resp);
|
||||
SetCurrentRoom(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NetworkManager.ShowUserFacingNetworkError(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ArenaRoomSnapshot> JoinRoom(string roomCode, string password = null)
|
||||
{
|
||||
await EnsureConnected();
|
||||
JObject resp = await SendAction("join_room", new
|
||||
try
|
||||
{
|
||||
room_code = roomCode,
|
||||
password = string.IsNullOrWhiteSpace(password) ? null : password
|
||||
});
|
||||
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("join_room", resp);
|
||||
SetCurrentRoom(snapshot);
|
||||
return snapshot;
|
||||
if (string.IsNullOrWhiteSpace(roomCode))
|
||||
{
|
||||
throw new Exception("MISSING_ROOM_CODE: room_code is empty");
|
||||
}
|
||||
|
||||
await EnsureConnected();
|
||||
JObject resp = await SendAction("join_room", new
|
||||
{
|
||||
room_code = roomCode.Trim(),
|
||||
password = string.IsNullOrWhiteSpace(password) ? null : password
|
||||
});
|
||||
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("join_room", resp);
|
||||
SetCurrentRoom(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NetworkManager.ShowUserFacingNetworkError(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> LeaveRoom()
|
||||
@@ -385,6 +406,31 @@ namespace GameServer.Client
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public async Task<ArenaRoomSnapshot> ChangeRoomSong(string songId, string songName, string difficulty)
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureConnected();
|
||||
JObject resp = await SendAction("change_room_song", new
|
||||
{
|
||||
song_id = songId,
|
||||
song_name = songName,
|
||||
difficulty = difficulty
|
||||
});
|
||||
ArenaRoomSnapshot snapshot = NormalizeSnapshotFromActionData("change_room_song", resp);
|
||||
if (snapshot != null)
|
||||
{
|
||||
SetCurrentRoom(snapshot);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NetworkManager.ShowUserFacingNetworkError(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ArenaRoomSnapshot> StartGame()
|
||||
{
|
||||
await EnsureConnected();
|
||||
@@ -804,11 +850,11 @@ namespace GameServer.Client
|
||||
NetworkManager network = NetworkManager.Instance;
|
||||
if (network == null)
|
||||
{
|
||||
throw new Exception("NetworkManager is not initialized");
|
||||
throw new Exception("NETWORK_NOT_READY: NetworkManager is not initialized");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(network.SteamId))
|
||||
{
|
||||
throw new Exception("steam_id is empty");
|
||||
throw new Exception("MISSING_STEAM_ID: steam_id is empty");
|
||||
}
|
||||
|
||||
_isConnecting = true;
|
||||
@@ -864,11 +910,11 @@ namespace GameServer.Client
|
||||
NetworkManager network = NetworkManager.Instance;
|
||||
if (network == null)
|
||||
{
|
||||
throw new Exception("NetworkManager is not initialized");
|
||||
throw new Exception("NETWORK_NOT_READY: NetworkManager is not initialized");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(network.SteamId))
|
||||
{
|
||||
throw new Exception("steam_id is empty");
|
||||
throw new Exception("MISSING_STEAM_ID: steam_id is empty");
|
||||
}
|
||||
|
||||
_isSocialConnecting = true;
|
||||
@@ -1213,6 +1259,15 @@ namespace GameServer.Client
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "room_song_changed":
|
||||
{
|
||||
ArenaRoomSnapshot snapshot = ReadRoomSnapshot(message);
|
||||
if (snapshot != null)
|
||||
{
|
||||
Enqueue(() => SetCurrentRoom(snapshot));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "room_dismissed":
|
||||
{
|
||||
string roomCode = ReadString(message, "room_code");
|
||||
@@ -1734,13 +1789,19 @@ namespace GameServer.Client
|
||||
{
|
||||
_shutdownLeaveAttempted = false;
|
||||
string previousRoomCode = CurrentRoom != null ? CurrentRoom.room_code : null;
|
||||
string previousSongId = CurrentRoom != null ? CurrentRoom.song_id : null;
|
||||
string previousDifficulty = CurrentRoom != null ? CurrentRoom.difficulty : null;
|
||||
if (snapshot == null || !IsRoomStartedState(snapshot.status))
|
||||
{
|
||||
_isLaunchingArenaGameplay = false;
|
||||
}
|
||||
|
||||
if (snapshot == null || (!string.IsNullOrWhiteSpace(previousRoomCode)
|
||||
&& !string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal)))
|
||||
&& !string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal))
|
||||
|| (snapshot != null
|
||||
&& string.Equals(previousRoomCode, snapshot.room_code, StringComparison.Ordinal)
|
||||
&& (!string.Equals(previousSongId, snapshot.song_id, StringComparison.Ordinal)
|
||||
|| !string.Equals(previousDifficulty, snapshot.difficulty, StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
ClearRoomRankingScores();
|
||||
}
|
||||
@@ -2353,7 +2414,7 @@ namespace GameServer.Client
|
||||
{
|
||||
string finalCode = code ?? "ERROR";
|
||||
string finalMessage = message ?? "Unknown arena error";
|
||||
gNotice.error.display($"{finalCode}: {finalMessage}");
|
||||
gNotice.error.display(NetworkManager.DescribeUserFacingNetworkError($"{finalCode}: {finalMessage}"));
|
||||
OnErrorReceived?.Invoke(finalCode, finalMessage);
|
||||
});
|
||||
}
|
||||
@@ -3890,4 +3951,3 @@ namespace GameServer.Client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,7 @@ namespace GameServer.Client
|
||||
[JsonProperty("receiver_id")] public string receiver_id;
|
||||
[JsonProperty("uid")] public int uid;
|
||||
[JsonProperty("display_name")] public string display_name;
|
||||
[JsonProperty("player_title")] public string player_title;
|
||||
[JsonProperty("avatar_url")] public string avatar_url;
|
||||
[JsonProperty("content")] public string content;
|
||||
[JsonProperty("created_at")] public string created_at;
|
||||
|
||||
@@ -21,6 +21,8 @@ public class NetworkManager : MonoBehaviour
|
||||
public static NetworkManager Instance { get; private set; }
|
||||
private static string _startupSteamId = string.Empty;
|
||||
private static string _startupSteamDisplayName = string.Empty;
|
||||
private static string _lastUserFacingNetworkError = string.Empty;
|
||||
private static float _lastUserFacingNetworkErrorAt;
|
||||
private static readonly Dictionary<string, CachedRemoteIdentity> _remoteIdentityCache =
|
||||
new Dictionary<string, CachedRemoteIdentity>(StringComparer.Ordinal);
|
||||
private static readonly Dictionary<string, Task<ProfileData>> _remoteProfileTasks =
|
||||
@@ -99,6 +101,8 @@ public class NetworkManager : MonoBehaviour
|
||||
Instance = null;
|
||||
_startupSteamId = string.Empty;
|
||||
_startupSteamDisplayName = string.Empty;
|
||||
_lastUserFacingNetworkError = string.Empty;
|
||||
_lastUserFacingNetworkErrorAt = 0f;
|
||||
_remoteIdentityCache.Clear();
|
||||
_remoteProfileTasks.Clear();
|
||||
}
|
||||
@@ -627,6 +631,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
NotifyNetworkError("LOCAL_ONLY_MODE");
|
||||
return new ArenaCreateResponse
|
||||
{
|
||||
success = false,
|
||||
@@ -635,6 +640,7 @@ public class NetworkManager : MonoBehaviour
|
||||
};
|
||||
}
|
||||
|
||||
EnsureSteamIdAvailableForOnlineFeature();
|
||||
var req = new ArenaCreateRequest
|
||||
{
|
||||
steam_id = SteamId,
|
||||
@@ -643,6 +649,13 @@ public class NetworkManager : MonoBehaviour
|
||||
password = password
|
||||
};
|
||||
ArenaCreateResponse resp = await PostJson<ArenaCreateResponse>(BuildApiUrl("/api/arena/create"), req, CancellationToken.None);
|
||||
if (resp == null || !resp.success)
|
||||
{
|
||||
string error = BuildErrorText(resp?.error_code, resp?.message, "CREATE_ROOM_FAILED");
|
||||
NotifyNetworkError(error);
|
||||
throw new Exception(error);
|
||||
}
|
||||
|
||||
OnArenaCreated?.Invoke(resp);
|
||||
return resp;
|
||||
}
|
||||
@@ -651,6 +664,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
NotifyNetworkError("LOCAL_ONLY_MODE");
|
||||
return new ArenaJoinResponse
|
||||
{
|
||||
success = false,
|
||||
@@ -660,6 +674,7 @@ public class NetworkManager : MonoBehaviour
|
||||
};
|
||||
}
|
||||
|
||||
EnsureSteamIdAvailableForOnlineFeature();
|
||||
var req = new ArenaJoinRequest
|
||||
{
|
||||
steam_id = SteamId,
|
||||
@@ -667,6 +682,13 @@ public class NetworkManager : MonoBehaviour
|
||||
password = password
|
||||
};
|
||||
ArenaJoinResponse resp = await PostJson<ArenaJoinResponse>(BuildApiUrl("/api/arena/join"), req, CancellationToken.None);
|
||||
if (resp == null || !resp.success)
|
||||
{
|
||||
string error = BuildErrorText(resp?.error_code, resp?.message, "JOIN_ROOM_FAILED");
|
||||
NotifyNetworkError(error);
|
||||
throw new Exception(error);
|
||||
}
|
||||
|
||||
OnArenaJoined?.Invoke(resp);
|
||||
return resp;
|
||||
}
|
||||
@@ -1682,6 +1704,15 @@ public class NetworkManager : MonoBehaviour
|
||||
return new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
private void EnsureSteamIdAvailableForOnlineFeature()
|
||||
{
|
||||
RefreshSteamIdentity(true);
|
||||
if (string.IsNullOrWhiteSpace(SteamId))
|
||||
{
|
||||
throw new InvalidOperationException("MISSING_STEAM_ID: steam_id is empty");
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileData BuildLocalOnlyProfile(string targetSteamId)
|
||||
{
|
||||
string normalizedSteamId = string.IsNullOrWhiteSpace(targetSteamId) ? string.Empty : targetSteamId.Trim();
|
||||
@@ -1850,11 +1881,135 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
private static void NotifyNetworkError(string message)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(message))
|
||||
ShowUserFacingNetworkError(message);
|
||||
}
|
||||
|
||||
public static void ShowUserFacingNetworkError(string message)
|
||||
{
|
||||
string userMessage = DescribeUserFacingNetworkError(message);
|
||||
if (!string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
gNotice.error.display(message);
|
||||
float now = Time.realtimeSinceStartup;
|
||||
if (string.Equals(_lastUserFacingNetworkError, userMessage, StringComparison.Ordinal)
|
||||
&& now - _lastUserFacingNetworkErrorAt < 0.75f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastUserFacingNetworkError = userMessage;
|
||||
_lastUserFacingNetworkErrorAt = now;
|
||||
gNotice.error.display(userMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static string DescribeUserFacingNetworkError(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return "网络错误,请稍后重试";
|
||||
}
|
||||
|
||||
string raw = message.Trim();
|
||||
string upper = raw.ToUpperInvariant();
|
||||
|
||||
if (upper.Contains("LOCAL_ONLY_MODE") || raw.IndexOf("local-only mode", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "当前为离线模式,无法使用联网功能";
|
||||
}
|
||||
|
||||
if (upper.Contains("MISSING_STEAM_ID")
|
||||
|| upper.Contains("STEAM_ID IS EMPTY")
|
||||
|| upper.Contains("TARGETSTEAMID IS EMPTY")
|
||||
|| upper.Contains("STEAMID IS EMPTY")
|
||||
|| upper.Contains("STEAM ID IS EMPTY"))
|
||||
{
|
||||
return "无法得到 SteamID,请确认已启动 Steam、登录账号并保持在线";
|
||||
}
|
||||
|
||||
if (upper.Contains("NETWORKMANAGER IS NOT INITIALIZED") || upper.Contains("NETWORK_NOT_READY"))
|
||||
{
|
||||
return "网络模块尚未初始化,请稍后重试";
|
||||
}
|
||||
|
||||
if (upper.Contains("ROOM_NOT_FOUND")
|
||||
|| raw.IndexOf("room not found", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("房间不存在", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "房间不存在或已解散";
|
||||
}
|
||||
|
||||
if (upper.Contains("ALREADY_IN_ROOM"))
|
||||
{
|
||||
return "你已经在房间中";
|
||||
}
|
||||
|
||||
if (upper.Contains("MISSING_ROOM_CODE") || upper.Contains("ROOM_CODE IS EMPTY"))
|
||||
{
|
||||
return "房间号不能为空";
|
||||
}
|
||||
|
||||
if (upper.Contains("ROOM_FULL"))
|
||||
{
|
||||
return "房间已满";
|
||||
}
|
||||
|
||||
if (upper.Contains("ROOM_STARTED") || upper.Contains("GAME_STARTED") || upper.Contains("ALREADY_STARTED"))
|
||||
{
|
||||
return "房间已开始游戏,无法加入";
|
||||
}
|
||||
|
||||
if (upper.Contains("WRONG_PASSWORD") || upper.Contains("INVALID_PASSWORD") || upper.Contains("PASSWORD"))
|
||||
{
|
||||
return "房间密码错误";
|
||||
}
|
||||
|
||||
if (upper.Contains("NOT_IN_ROOM"))
|
||||
{
|
||||
return "当前不在房间中";
|
||||
}
|
||||
|
||||
if (upper.Contains("TIMED OUT")
|
||||
|| upper.Contains("TIMEOUT")
|
||||
|| upper.Contains("CONNECTFAILURE")
|
||||
|| upper.Contains("CONNECTIONERROR")
|
||||
|| upper.Contains("CONNECTION FAILED")
|
||||
|| upper.Contains("CANNOT CONNECT")
|
||||
|| upper.Contains("COULD NOT RESOLVE")
|
||||
|| upper.Contains("RESOLVE DESTINATION HOST")
|
||||
|| upper.Contains("NAMERESOLUTION")
|
||||
|| upper.Contains("HOST UNREACHABLE")
|
||||
|| upper.Contains("CONNECTION REFUSED")
|
||||
|| upper.Contains("CONNECTION RESET")
|
||||
|| upper.Contains("SOCKET")
|
||||
|| raw.IndexOf("unable to connect", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| raw.IndexOf("failed to connect", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return "未联网或服务器连接失败,请检查网络后重试";
|
||||
}
|
||||
|
||||
if (upper.Contains("HTTP") && (upper.Contains(" 404") || upper.Contains("404:")))
|
||||
{
|
||||
return "请求的服务不存在,请稍后重试";
|
||||
}
|
||||
|
||||
if (upper.Contains("HTTP") && (upper.Contains(" 500") || upper.Contains("500:")))
|
||||
{
|
||||
return "服务器暂时无法处理请求,请稍后重试";
|
||||
}
|
||||
|
||||
if (raw.Length > 80)
|
||||
{
|
||||
return "网络错误,请稍后重试";
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static string BuildErrorText(string code, string message, string fallbackCode)
|
||||
{
|
||||
string safeCode = string.IsNullOrWhiteSpace(code) ? fallbackCode : code.Trim();
|
||||
string safeMessage = string.IsNullOrWhiteSpace(message) ? safeCode : message.Trim();
|
||||
return $"{safeCode}: {safeMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -403,10 +403,10 @@ MonoBehaviour:
|
||||
rkPrefab: {fileID: 4439332187331332072, guid: 5d56483f68516d94e9277bbac63f8525, type: 3}
|
||||
rkParent: {fileID: 4242620377161803188}
|
||||
btmSprites:
|
||||
- {fileID: 21300000, guid: 040be0e474e09894ca320b08d9462cc8, type: 3}
|
||||
- {fileID: 21300000, guid: 60cfd84ae231a4943b3f0f6cf0dcf409, type: 3}
|
||||
- {fileID: 21300000, guid: 1258ac417dd158a48b05eeedb739c469, type: 3}
|
||||
- {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
|
||||
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
|
||||
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
|
||||
- {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
|
||||
- {fileID: 21300000, guid: 7f3e2cfd199d3a844b15751f1eddf15b, type: 3}
|
||||
--- !u!1 &2889696334540093026
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@@ -140,7 +140,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -208,7 +208,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -287,7 +287,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -422,7 +422,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -501,7 +501,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -589,7 +589,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 28c51f93b6260224c867f45f411a0e85, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: 9e6a75c290497c4469b9b86d5eab72a1, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -657,7 +657,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
|
||||
@@ -58,7 +58,7 @@ public class rewardPrefab : MonoBehaviour
|
||||
switch (rewardType)
|
||||
{
|
||||
case RewardVisualType.Coins:
|
||||
return "金币";
|
||||
return "算力";
|
||||
case RewardVisualType.Material:
|
||||
return "记忆碎片";
|
||||
case RewardVisualType.PlayerExp:
|
||||
|
||||
@@ -1153,11 +1153,11 @@ public class teamUIController : MonoBehaviour
|
||||
*/
|
||||
|
||||
// Load ally slot IDs from PlayerPrefs
|
||||
allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
|
||||
allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
|
||||
allySlotIds[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
|
||||
allySlotIds[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
|
||||
allySlotIds[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
|
||||
allySlotIds[0] = TeamSelectionDefaults.ReadHeroId(1);
|
||||
allySlotIds[1] = TeamSelectionDefaults.ReadHeroId(2);
|
||||
allySlotIds[2] = TeamSelectionDefaults.ReadHeroId(3);
|
||||
allySlotIds[3] = TeamSelectionDefaults.ReadHeroId(4);
|
||||
allySlotIds[4] = TeamSelectionDefaults.ReadHeroId(5);
|
||||
|
||||
PopulateAllySOsFromIds();
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -51.2}
|
||||
m_SizeDelta: {x: 200, y: 32.156}
|
||||
m_AnchoredPosition: {x: 0, y: -49.6}
|
||||
m_SizeDelta: {x: 146, y: 56}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5597810916468765611
|
||||
CanvasRenderer:
|
||||
@@ -67,8 +67,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 7621278097234006379, guid: 59df2390a598c904bab275d1ea743916, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: -2689630615245801869, guid: 2a46b5f36d286d447bd74d1db9a72cd7, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -157,8 +157,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -9.196}
|
||||
m_SizeDelta: {x: 200, y: 110.402}
|
||||
m_AnchoredPosition: {x: 0, y: -49.6}
|
||||
m_SizeDelta: {x: 146, y: 56}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5591616715459838773
|
||||
CanvasRenderer:
|
||||
@@ -188,8 +188,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 7919194353434859991, guid: b35fcaa46fa9f3e49b71c103f69df698, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: -566881566719626471, guid: 56ba1aaa875c6b7489f77555fd212d3e, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -355,8 +355,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 1.4682007}
|
||||
m_SizeDelta: {x: 0, y: -2.9365}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3720672462226780915
|
||||
CanvasRenderer:
|
||||
@@ -379,7 +379,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
|
||||
m_Color: {r: 0.63529414, g: 0.3529412, b: 0.16862746, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -387,7 +387,7 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
@@ -456,7 +456,7 @@ MonoBehaviour:
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 4
|
||||
m_Spacing: 0
|
||||
m_Spacing: -100
|
||||
m_ChildForceExpandWidth: 1
|
||||
m_ChildForceExpandHeight: 1
|
||||
m_ChildControlWidth: 0
|
||||
@@ -500,8 +500,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -171.99}
|
||||
m_SizeDelta: {x: 300, y: 40}
|
||||
m_AnchoredPosition: {x: 0, y: -171.1}
|
||||
m_SizeDelta: {x: 194.5543, y: 52.3284}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6903183453437413057
|
||||
CanvasRenderer:
|
||||
@@ -531,8 +531,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 2561534620446350566, guid: 23a48ca1e983a4c47971fcf7eab26b89, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: -6405685121925378258, guid: 92151af6550eb214aa4daecf4ea48edb, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -622,7 +622,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 555, y: 264.2}
|
||||
m_SizeDelta: {x: 450, y: 230}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7601750316680178786
|
||||
CanvasRenderer:
|
||||
@@ -652,7 +652,7 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
|
||||
m_Sprite: {fileID: 21300000, guid: a31ac6012ab10b146878694e7d720b11, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
@@ -698,8 +698,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 16}
|
||||
m_SizeDelta: {x: 200, y: 60}
|
||||
m_AnchoredPosition: {x: -100.424805, y: 16}
|
||||
m_SizeDelta: {x: 346.8495, y: 60}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &67614290717671043
|
||||
CanvasRenderer:
|
||||
@@ -729,8 +729,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 4717104774431673990, guid: 2204c5120d209e24b8d8473101c0670f, type: 3}
|
||||
m_Type: 1
|
||||
m_Sprite: {fileID: 21300000, guid: 7edca03691f782d46ac361a681ce5685, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@@ -874,8 +874,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: -2.8545, y: 2.3508}
|
||||
m_SizeDelta: {x: -5.7091, y: -4.7016}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &316840073610798188
|
||||
CanvasRenderer:
|
||||
@@ -898,7 +898,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0.63529414, g: 0.3529412, b: 0.16862746, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -906,8 +906,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 18
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -1104,8 +1104,8 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: -2.947403, y: 2.0960999}
|
||||
m_SizeDelta: {x: -5.8948, y: -4.1923}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2489646231650356367
|
||||
CanvasRenderer:
|
||||
@@ -1128,7 +1128,7 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_Color: {r: 0.09803922, g: 0.32941177, b: 0.6, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
@@ -1136,8 +1136,8 @@ MonoBehaviour:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_FontData:
|
||||
m_Font: {fileID: 12800000, guid: bd4be44636ad6c24a9b117a7108a75e9, type: 3}
|
||||
m_FontSize: 20
|
||||
m_Font: {fileID: 12800000, guid: d254d20d93651ae448ff13d10dff30ab, type: 3}
|
||||
m_FontSize: 24
|
||||
m_FontStyle: 0
|
||||
m_BestFit: 0
|
||||
m_MinSize: 1
|
||||
@@ -1148,7 +1148,7 @@ MonoBehaviour:
|
||||
m_HorizontalOverflow: 0
|
||||
m_VerticalOverflow: 0
|
||||
m_LineSpacing: 1
|
||||
m_Text: "\u521B\u5EFA\u623F\u95F4\u5E76\u4F5C\u4E3A\u623F\u4E3B"
|
||||
m_Text: "\u521B\u5EFA\u623F\u95F4"
|
||||
--- !u!1 &4714805719805124595
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -1373,8 +1373,8 @@ MonoBehaviour:
|
||||
m_Calls: []
|
||||
m_text: "\u200B"
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 452fa97492cae71479cf143f29930751, type: 2}
|
||||
m_sharedMaterial: {fileID: -346136068272202111, guid: 452fa97492cae71479cf143f29930751, type: 2}
|
||||
m_fontAsset: {fileID: 11400000, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
|
||||
m_sharedMaterial: {fileID: -346136068272202111, guid: 7cad3f54ac7b53d4885f00c16f1a6929, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
|
||||
@@ -942,7 +942,8 @@ public class globalChatSystem : MonoBehaviour
|
||||
playerMessagePrefab controller = instance.GetComponentInChildren<playerMessagePrefab>();
|
||||
if (controller != null)
|
||||
{
|
||||
controller.Bind(senderSteamId, avatarUrl, displayName, string.Empty, message.content, bubbleColor);
|
||||
string title = !string.IsNullOrWhiteSpace(message.player_title) ? message.player_title : string.Empty;
|
||||
controller.Bind(senderSteamId, avatarUrl, displayName, title, message.content, bubbleColor);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
||||
@@ -38,7 +38,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 323.75, y: 30}
|
||||
m_SizeDelta: {x: 380.56, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2228420748506963945
|
||||
MonoBehaviour:
|
||||
@@ -83,7 +83,7 @@ MonoBehaviour:
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: 30
|
||||
m_PreferredHeight: 0
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 2
|
||||
|
||||
@@ -100,6 +100,7 @@ public class roomAsk : MonoBehaviour
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[roomAsk] Create room failed: {ex.Message}");
|
||||
GameServer.Client.NetworkManager.ShowUserFacingNetworkError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +126,7 @@ public class roomAsk : MonoBehaviour
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[roomAsk] Join room failed: {ex.Message}");
|
||||
GameServer.Client.NetworkManager.ShowUserFacingNetworkError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ public class roomDetails : MonoBehaviour
|
||||
public Text thisSongName;
|
||||
public Text thisSongDifficulty;
|
||||
|
||||
[Header("change songs")]
|
||||
public Dropdown songDropdown;
|
||||
public Dropdown difficultyDropdown;
|
||||
|
||||
[Header("objs")]
|
||||
public GameObject roommatePrefab;
|
||||
public Transform roommateParent;
|
||||
@@ -82,6 +86,11 @@ public class roomDetails : MonoBehaviour
|
||||
private readonly List<RectTransform> _pendingMessageLayoutDirtyRoots = new List<RectTransform>();
|
||||
private Coroutine _refocusChatInputCoroutine;
|
||||
private bool _refocusChatInputRequested;
|
||||
private readonly List<SongData> _selectableSongs = new List<SongData>();
|
||||
private readonly List<int> _selectableDifficulties = new List<int>();
|
||||
private Coroutine _songLibraryWaitRoutine;
|
||||
private bool _suppressSongDropdownEvents;
|
||||
private bool _isChangingRoomSong;
|
||||
|
||||
private const float ChatSendCooldownSeconds = 3f;
|
||||
private const int InitialHistoryLoadLimit = 20;
|
||||
@@ -162,6 +171,18 @@ public class roomDetails : MonoBehaviour
|
||||
sendMessageButton.onClick.AddListener(OnSendMessageClicked);
|
||||
}
|
||||
|
||||
if (songDropdown != null)
|
||||
{
|
||||
songDropdown.onValueChanged.RemoveListener(OnSongDropdownChanged);
|
||||
songDropdown.onValueChanged.AddListener(OnSongDropdownChanged);
|
||||
}
|
||||
|
||||
if (difficultyDropdown != null)
|
||||
{
|
||||
difficultyDropdown.onValueChanged.RemoveListener(OnDifficultyDropdownChanged);
|
||||
difficultyDropdown.onValueChanged.AddListener(OnDifficultyDropdownChanged);
|
||||
}
|
||||
|
||||
if (playerMessageInputField != null)
|
||||
{
|
||||
playerMessageInputField.onEndEdit.RemoveListener(OnChatInputEndEdit);
|
||||
@@ -185,6 +206,7 @@ public class roomDetails : MonoBehaviour
|
||||
_ = InitializeRoomChatAsync();
|
||||
|
||||
Refresh(_service.CurrentRoom);
|
||||
EnsureSongDropdownsReady();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -225,6 +247,16 @@ public class roomDetails : MonoBehaviour
|
||||
sendMessageButton.onClick.RemoveListener(OnSendMessageClicked);
|
||||
}
|
||||
|
||||
if (songDropdown != null)
|
||||
{
|
||||
songDropdown.onValueChanged.RemoveListener(OnSongDropdownChanged);
|
||||
}
|
||||
|
||||
if (difficultyDropdown != null)
|
||||
{
|
||||
difficultyDropdown.onValueChanged.RemoveListener(OnDifficultyDropdownChanged);
|
||||
}
|
||||
|
||||
if (playerMessageInputField != null)
|
||||
{
|
||||
playerMessageInputField.onEndEdit.RemoveListener(OnChatInputEndEdit);
|
||||
@@ -259,6 +291,12 @@ public class roomDetails : MonoBehaviour
|
||||
_refocusChatInputCoroutine = null;
|
||||
}
|
||||
|
||||
if (_songLibraryWaitRoutine != null)
|
||||
{
|
||||
StopCoroutine(_songLibraryWaitRoutine);
|
||||
_songLibraryWaitRoutine = null;
|
||||
}
|
||||
|
||||
RestoreDefaultChatPlaceholder();
|
||||
|
||||
if (_spawnedRoomRankingInstance != null)
|
||||
@@ -396,6 +434,7 @@ public class roomDetails : MonoBehaviour
|
||||
UpdateQuitButtonTexts(snapshot);
|
||||
UpdateReadyButton(snapshot);
|
||||
RenderParticipants(snapshot);
|
||||
RefreshSongDropdowns(snapshot);
|
||||
}
|
||||
|
||||
private IEnumerator UpdateRemainTime(ArenaRoomSnapshot snapshot)
|
||||
@@ -760,7 +799,7 @@ public class roomDetails : MonoBehaviour
|
||||
&& string.Equals(senderSteamId, localSteamId, StringComparison.Ordinal);
|
||||
Color bubbleColor = ResolveMessageBubbleColor(isSelf);
|
||||
string displayName = ResolveMessageDisplayName(message, isSelf);
|
||||
string title = ResolveMessageTitle();
|
||||
string title = ResolveMessageTitle(message);
|
||||
|
||||
GameObject instance = Instantiate(playerMessagePrefab, messageParent, false);
|
||||
playerMessagePrefab controller = instance.GetComponent<playerMessagePrefab>();
|
||||
@@ -1026,9 +1065,11 @@ public class roomDetails : MonoBehaviour
|
||||
return GameServer.Client.NetworkManager.ResolveDisplayName(message != null ? message.display_name : string.Empty, messageSteamId, false);
|
||||
}
|
||||
|
||||
private static string ResolveMessageTitle()
|
||||
private static string ResolveMessageTitle(ArenaRoomChatMessage message)
|
||||
{
|
||||
return string.Empty;
|
||||
return message != null && !string.IsNullOrWhiteSpace(message.player_title)
|
||||
? message.player_title
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static string GetMessageSenderSteamId(ArenaRoomChatMessage message)
|
||||
@@ -1387,6 +1428,405 @@ public class roomDetails : MonoBehaviour
|
||||
|| string.Equals(snapshot.status, "PLAYING", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void EnsureSongDropdownsReady()
|
||||
{
|
||||
if (songDropdown == null && difficultyDropdown == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SongDataLibrary library = SongDataLibrary.Instance;
|
||||
if (library != null && library.IsLoaded)
|
||||
{
|
||||
RefreshSongDropdowns(_service != null ? _service.CurrentRoom : null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_songLibraryWaitRoutine == null)
|
||||
{
|
||||
_songLibraryWaitRoutine = StartCoroutine(WaitForSongLibraryAndRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator WaitForSongLibraryAndRefresh()
|
||||
{
|
||||
float timeoutAt = Time.realtimeSinceStartup + 8f;
|
||||
while (Time.realtimeSinceStartup < timeoutAt)
|
||||
{
|
||||
SongDataLibrary library = SongDataLibrary.Instance;
|
||||
if (library != null && library.IsLoaded)
|
||||
{
|
||||
_songLibraryWaitRoutine = null;
|
||||
RefreshSongDropdowns(_service != null ? _service.CurrentRoom : null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_songLibraryWaitRoutine = null;
|
||||
Debug.LogWarning("[roomDetails] SongDataLibrary was not ready; song dropdowns were left empty.");
|
||||
}
|
||||
|
||||
private void RefreshSongDropdowns(ArenaRoomSnapshot snapshot)
|
||||
{
|
||||
if (songDropdown == null && difficultyDropdown == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SongDataLibrary library = SongDataLibrary.Instance;
|
||||
if (library == null || !library.IsLoaded)
|
||||
{
|
||||
EnsureSongDropdownsReady();
|
||||
UpdateSongDropdownInteractable(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
RebuildSelectableSongs(library);
|
||||
SongData selectedSong = ResolveSnapshotSong(snapshot);
|
||||
if (selectedSong == null && _selectableSongs.Count > 0)
|
||||
{
|
||||
selectedSong = _selectableSongs[0];
|
||||
}
|
||||
|
||||
int selectedDifficulty = ConvertDifficultyKeyToId(snapshot != null ? snapshot.difficulty : null);
|
||||
if (selectedSong != null)
|
||||
{
|
||||
RebuildSelectableDifficulties(selectedSong);
|
||||
if (!_selectableDifficulties.Contains(selectedDifficulty))
|
||||
{
|
||||
selectedDifficulty = _selectableDifficulties.Count > 0 ? _selectableDifficulties[0] : selectedDifficulty;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectableDifficulties.Clear();
|
||||
}
|
||||
|
||||
_suppressSongDropdownEvents = true;
|
||||
try
|
||||
{
|
||||
PopulateSongDropdown(selectedSong);
|
||||
PopulateDifficultyDropdown(selectedSong, selectedDifficulty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSongDropdownEvents = false;
|
||||
}
|
||||
|
||||
UpdateSongDropdownInteractable(snapshot);
|
||||
}
|
||||
|
||||
private void RebuildSelectableSongs(SongDataLibrary library)
|
||||
{
|
||||
_selectableSongs.Clear();
|
||||
|
||||
List<SongData> songs = library != null ? library.GetAllSongs() : null;
|
||||
if (songs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
songs.Sort((left, right) =>
|
||||
{
|
||||
int idCompare = (left != null ? left.songID : int.MaxValue).CompareTo(right != null ? right.songID : int.MaxValue);
|
||||
if (idCompare != 0)
|
||||
{
|
||||
return idCompare;
|
||||
}
|
||||
|
||||
return string.Compare(left != null ? left.songName : string.Empty,
|
||||
right != null ? right.songName : string.Empty,
|
||||
StringComparison.CurrentCulture);
|
||||
});
|
||||
|
||||
foreach (SongData song in songs)
|
||||
{
|
||||
if (song != null && HasPlayableDifficulty(song))
|
||||
{
|
||||
_selectableSongs.Add(song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SongData ResolveSnapshotSong(ArenaRoomSnapshot snapshot)
|
||||
{
|
||||
if (snapshot != null && int.TryParse(snapshot.song_id, out int songId))
|
||||
{
|
||||
for (int i = 0; i < _selectableSongs.Count; i++)
|
||||
{
|
||||
SongData song = _selectableSongs[i];
|
||||
if (song != null && song.songID == songId)
|
||||
{
|
||||
return song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RebuildSelectableDifficulties(SongData song)
|
||||
{
|
||||
_selectableDifficulties.Clear();
|
||||
if (song == null || song.chartFiles == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ChartFileEntry entry in song.chartFiles)
|
||||
{
|
||||
if (entry == null || entry.chartFile == null || _selectableDifficulties.Contains(entry.difficulty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_selectableDifficulties.Add(entry.difficulty);
|
||||
}
|
||||
|
||||
_selectableDifficulties.Sort();
|
||||
}
|
||||
|
||||
private void PopulateSongDropdown(SongData selectedSong)
|
||||
{
|
||||
if (songDropdown == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
songDropdown.ClearOptions();
|
||||
List<string> options = new List<string>();
|
||||
for (int i = 0; i < _selectableSongs.Count; i++)
|
||||
{
|
||||
SongData song = _selectableSongs[i];
|
||||
options.Add(song != null && !string.IsNullOrWhiteSpace(song.songName) ? song.songName : $"Song {song?.songID ?? 0}");
|
||||
}
|
||||
|
||||
songDropdown.AddOptions(options);
|
||||
int selectedIndex = selectedSong != null ? _selectableSongs.IndexOf(selectedSong) : -1;
|
||||
songDropdown.value = Mathf.Clamp(selectedIndex, 0, Mathf.Max(0, _selectableSongs.Count - 1));
|
||||
songDropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
private void PopulateDifficultyDropdown(SongData selectedSong, int selectedDifficulty)
|
||||
{
|
||||
if (difficultyDropdown == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
difficultyDropdown.ClearOptions();
|
||||
List<string> options = new List<string>();
|
||||
for (int i = 0; i < _selectableDifficulties.Count; i++)
|
||||
{
|
||||
options.Add(FormatDifficultyOption(selectedSong, _selectableDifficulties[i]));
|
||||
}
|
||||
|
||||
difficultyDropdown.AddOptions(options);
|
||||
int selectedIndex = _selectableDifficulties.IndexOf(selectedDifficulty);
|
||||
difficultyDropdown.value = Mathf.Clamp(selectedIndex, 0, Mathf.Max(0, _selectableDifficulties.Count - 1));
|
||||
difficultyDropdown.RefreshShownValue();
|
||||
}
|
||||
|
||||
private void UpdateSongDropdownInteractable(ArenaRoomSnapshot snapshot)
|
||||
{
|
||||
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
|
||||
bool canChange = !_isChangingRoomSong
|
||||
&& snapshot != null
|
||||
&& IsLocalHost(snapshot, localSteamId)
|
||||
&& !IsRoomStarted(snapshot)
|
||||
&& _selectableSongs.Count > 0;
|
||||
|
||||
if (songDropdown != null)
|
||||
{
|
||||
songDropdown.interactable = canChange;
|
||||
}
|
||||
|
||||
if (difficultyDropdown != null)
|
||||
{
|
||||
difficultyDropdown.interactable = canChange && _selectableDifficulties.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSongDropdownChanged(int index)
|
||||
{
|
||||
if (_suppressSongDropdownEvents || index < 0 || index >= _selectableSongs.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SongData song = _selectableSongs[index];
|
||||
int currentDifficulty = GetSelectedDifficultyId();
|
||||
RebuildSelectableDifficulties(song);
|
||||
if (!_selectableDifficulties.Contains(currentDifficulty))
|
||||
{
|
||||
currentDifficulty = _selectableDifficulties.Count > 0 ? _selectableDifficulties[0] : 2;
|
||||
}
|
||||
|
||||
_suppressSongDropdownEvents = true;
|
||||
try
|
||||
{
|
||||
PopulateDifficultyDropdown(song, currentDifficulty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressSongDropdownEvents = false;
|
||||
}
|
||||
|
||||
RequestChangeRoomSong(song, currentDifficulty);
|
||||
}
|
||||
|
||||
private void OnDifficultyDropdownChanged(int index)
|
||||
{
|
||||
if (_suppressSongDropdownEvents || index < 0 || index >= _selectableDifficulties.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SongData song = GetSelectedSong();
|
||||
RequestChangeRoomSong(song, _selectableDifficulties[index]);
|
||||
}
|
||||
|
||||
private async void RequestChangeRoomSong(SongData song, int difficultyId)
|
||||
{
|
||||
if (_service == null || song == null || _isChangingRoomSong)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArenaRoomSnapshot snapshot = _service.CurrentRoom;
|
||||
string localSteamId = NetworkManager.Instance != null ? NetworkManager.Instance.SteamId : string.Empty;
|
||||
if (!IsLocalHost(snapshot, localSteamId) || IsRoomStarted(snapshot))
|
||||
{
|
||||
RefreshSongDropdowns(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
string difficultyKey = ConvertDifficultyIdToKey(difficultyId);
|
||||
if (snapshot != null
|
||||
&& string.Equals(snapshot.song_id, song.songID.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal)
|
||||
&& string.Equals((snapshot.difficulty ?? string.Empty).Trim(), difficultyKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isChangingRoomSong = true;
|
||||
UpdateSongDropdownInteractable(snapshot);
|
||||
try
|
||||
{
|
||||
await _service.ChangeRoomSong(
|
||||
song.songID.ToString(CultureInfo.InvariantCulture),
|
||||
song.songName,
|
||||
difficultyKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[roomDetails] Change room song failed: {ex.Message}");
|
||||
RefreshSongDropdowns(_service.CurrentRoom);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isChangingRoomSong = false;
|
||||
UpdateSongDropdownInteractable(_service.CurrentRoom);
|
||||
}
|
||||
}
|
||||
|
||||
private SongData GetSelectedSong()
|
||||
{
|
||||
if (songDropdown == null)
|
||||
{
|
||||
return ResolveSnapshotSong(_service != null ? _service.CurrentRoom : null);
|
||||
}
|
||||
|
||||
int index = songDropdown.value;
|
||||
return index >= 0 && index < _selectableSongs.Count ? _selectableSongs[index] : null;
|
||||
}
|
||||
|
||||
private int GetSelectedDifficultyId()
|
||||
{
|
||||
if (difficultyDropdown != null)
|
||||
{
|
||||
int index = difficultyDropdown.value;
|
||||
if (index >= 0 && index < _selectableDifficulties.Count)
|
||||
{
|
||||
return _selectableDifficulties[index];
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertDifficultyKeyToId(_service != null && _service.CurrentRoom != null ? _service.CurrentRoom.difficulty : null);
|
||||
}
|
||||
|
||||
private static bool HasPlayableDifficulty(SongData song)
|
||||
{
|
||||
if (song == null || song.chartFiles == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (ChartFileEntry entry in song.chartFiles)
|
||||
{
|
||||
if (entry != null && entry.chartFile != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string FormatDifficultyOption(SongData song, int difficultyId)
|
||||
{
|
||||
string shortName = FormatDifficulty(ConvertDifficultyIdToKey(difficultyId));
|
||||
ChartFileEntry entry = song != null && song.chartFiles != null
|
||||
? song.chartFiles.Find(item => item != null && item.difficulty == difficultyId)
|
||||
: null;
|
||||
|
||||
if (entry != null && entry.difficultyLEVEL > 0f)
|
||||
{
|
||||
return $"{shortName} Lv.{entry.difficultyLEVEL:0.#}";
|
||||
}
|
||||
|
||||
return shortName;
|
||||
}
|
||||
|
||||
private static int ConvertDifficultyKeyToId(string difficultyKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(difficultyKey))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
switch (difficultyKey.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "ez":
|
||||
return 0;
|
||||
case "hd":
|
||||
return 1;
|
||||
case "im":
|
||||
return 3;
|
||||
case "in":
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertDifficultyIdToKey(int difficultyId)
|
||||
{
|
||||
switch (difficultyId)
|
||||
{
|
||||
case 0:
|
||||
return "ez";
|
||||
case 1:
|
||||
return "hd";
|
||||
case 3:
|
||||
return "im";
|
||||
case 2:
|
||||
default:
|
||||
return "in";
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetButtonText(Button button, string text)
|
||||
{
|
||||
if (button == null)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -129,11 +129,11 @@ public class load_teammatesProfile : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
// Load selected hero IDs from PlayerPrefs
|
||||
int heroId1 = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
|
||||
int heroId2 = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
|
||||
int heroId3 = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
|
||||
int heroId4 = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
|
||||
int heroId5 = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
|
||||
int heroId1 = TeamSelectionDefaults.ReadHeroId(1);
|
||||
int heroId2 = TeamSelectionDefaults.ReadHeroId(2);
|
||||
int heroId3 = TeamSelectionDefaults.ReadHeroId(3);
|
||||
int heroId4 = TeamSelectionDefaults.ReadHeroId(4);
|
||||
int heroId5 = TeamSelectionDefaults.ReadHeroId(5);
|
||||
|
||||
// Set sprites for each profile image
|
||||
teammate_profile_01.sprite = GetHeroSquareProfile(heroId1);
|
||||
|
||||
Reference in New Issue
Block a user