一些修复和优化
This commit is contained in:
@@ -59,7 +59,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
[Tooltip("(Fallback) Damage multiplier applied to ally base attack for Great hits if SO level value not present")] public float damageMultiplierGreat = 0.75f;
|
||||
[Tooltip("(Fallback) Damage multiplier applied to ally base attack for Perfect hits if SO level value not present")] public float damageMultiplierPerfect = 1f;
|
||||
|
||||
[Tooltip("(Fallback) Base HP loss on Miss will be (missHpLossBase * (1 - damageResistance)) if SO level value not present")] public float missHpLossBase = 10f;
|
||||
[Tooltip("(Fallback) 若 SO 等级数据未提供,则 Miss 基本生命损失 = missHpLossBase * (1 - damageResistance)")] public float missHpLossBase = 10f;
|
||||
|
||||
// --------- Caches to avoid first-hit hitch (Resources.LoadAll) ---------
|
||||
private AllyHero_SO[] _allAllyHeroSOs;
|
||||
|
||||
@@ -9,6 +9,15 @@ using UnityEditor;
|
||||
[CreateAssetMenu(fileName = "NewAllyHero", menuName = "SO_Data/AllyHero")]
|
||||
public class AllyHero_SO : ScriptableObject
|
||||
{
|
||||
public enum AllyRolePosition
|
||||
{
|
||||
攻击,
|
||||
回血,
|
||||
回蓝,
|
||||
加分,
|
||||
综合
|
||||
}
|
||||
|
||||
private const string EquippedEquipmentSaveCategory = "ally_equipped_equipment";
|
||||
private const string EquippedEquipmentRecoverySlotPrefix = "ally_equipped_equipment_";
|
||||
|
||||
@@ -31,7 +40,13 @@ public class AllyHero_SO : ScriptableObject
|
||||
public string ally_heroDesignation;
|
||||
public int ally_heroID;
|
||||
public bool isUnlocked;
|
||||
[Header("Role Setup")]
|
||||
[InspectorName("所属阵营")]
|
||||
[Tooltip("Uses the same category list as equipment skill types.")]
|
||||
public equipmentSO.EquipmentSkillType allyType;
|
||||
[InspectorName("角色定位")]
|
||||
[Tooltip("Primary combat role used for configuration and display.")]
|
||||
public AllyRolePosition allyRolePosition = AllyRolePosition.综合;
|
||||
[Tooltip("Optional obsession tag used by memory skills such as 30011012. Leave empty to ignore mismatch checks.")]
|
||||
public string obsessionTag;
|
||||
|
||||
@@ -506,12 +521,57 @@ public class AllyHero_SO : ScriptableObject
|
||||
public string GetDisplayLevelRatingKey()
|
||||
{
|
||||
int displayIndex = GetDisplayLevelIndex();
|
||||
return DisplayLevelIndexToRatingKey(displayIndex);
|
||||
}
|
||||
|
||||
// 用外部传入的成长值(来自持久化账本 AllyHeroDeployLedger)计算等级评级,
|
||||
// 而不是读取 SO 上被打包烘焙的镜像字段。用于 teamSelector 等直接依赖账本真相的界面,
|
||||
// 避免"账本已加载但尚未回写 SO 镜像"窗口内读到过期的烘焙值。判级规则与 GetDisplayLevelRatingKey 完全一致。
|
||||
public string GetDisplayLevelRatingKeyFromGrowth(int currentExp, int unlockedTierIndex, bool levelLocked)
|
||||
{
|
||||
int displayIndex = ResolveDisplayLevelIndexFromGrowth(currentExp, unlockedTierIndex, levelLocked);
|
||||
return DisplayLevelIndexToRatingKey(displayIndex);
|
||||
}
|
||||
|
||||
private static string DisplayLevelIndexToRatingKey(int displayIndex)
|
||||
{
|
||||
if (displayIndex <= 0) return "C";
|
||||
if (displayIndex == 1) return "B";
|
||||
if (displayIndex == 2) return "A";
|
||||
return "S";
|
||||
}
|
||||
|
||||
private int ResolveDisplayLevelIndexFromGrowth(int currentExp, int unlockedTierIndex, bool levelLocked)
|
||||
{
|
||||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||||
if (sorted == null || sorted.Count == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
int safeExp = Mathf.Max(0, currentExp);
|
||||
int expQualifiedIndex = 0;
|
||||
for (int i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
if (safeExp >= sorted[i].requiredEXP)
|
||||
{
|
||||
expQualifiedIndex = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!levelLocked)
|
||||
{
|
||||
return expQualifiedIndex;
|
||||
}
|
||||
|
||||
int unlockedIndex = Mathf.Clamp(unlockedTierIndex, 0, sorted.Count - 1);
|
||||
return Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
|
||||
}
|
||||
|
||||
public AllyLevelInfo GetEffectiveLevelForCurrentEXP()
|
||||
{
|
||||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||||
@@ -656,7 +716,11 @@ public class AllyHero_SO : ScriptableObject
|
||||
total += SumEffectValues(equipment.typeSameEffects, effectType);
|
||||
}
|
||||
|
||||
total += SumEffectValues(equipment.maxLevelEffects, effectType);
|
||||
if (equipment.IsMaxLevelEffectActive())
|
||||
{
|
||||
total += SumEffectValues(equipment.maxLevelEffects, effectType);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ public class newTeamSelector : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建卡片前先确保成长账本已完成加载与镜像同步,消除"账本未初始化就读等级"的时序竞争。
|
||||
AllyHeroDeployLedger.EnsureInstance().InitializeIfNeeded();
|
||||
|
||||
// Clear existing
|
||||
for (int i = container.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -449,7 +452,7 @@ public class newTeamSelector : MonoBehaviour
|
||||
return null;
|
||||
|
||||
AllyHero_SO hero = FindHeroById(heroId);
|
||||
return hero != null ? GetRatingFromSO(hero) : null;
|
||||
return hero != null ? GetRatingFromLedger(hero) : null;
|
||||
}
|
||||
|
||||
private IEnumerator RefreshSkillsGradually()
|
||||
@@ -519,8 +522,26 @@ public class newTeamSelector : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRatingFromSO(AllyHero_SO so)
|
||||
// 等级评级改从持久化账本(AllyHeroDeployLedger)取真值,而非 SO 上被打包烘焙的镜像字段。
|
||||
// 修复:客户端更新后 SO 镜像为出厂 0 值,若账本尚未回写 SO 就构建卡片,会显示过期等级,
|
||||
// 且需玩家重新给一次经验才同步。这里直接读账本(getter 内部会保证 InitializeIfNeeded),从根源消除不一致。
|
||||
private string GetRatingFromLedger(AllyHero_SO so)
|
||||
{
|
||||
return so != null ? so.GetDisplayLevelRatingKey() : "C";
|
||||
if (so == null)
|
||||
{
|
||||
return "C";
|
||||
}
|
||||
|
||||
if (so.ally_heroID <= 0)
|
||||
{
|
||||
// 无有效 heroID 无法查账本,退回 SO 自身判级,保持原行为。
|
||||
return so.GetDisplayLevelRatingKey();
|
||||
}
|
||||
|
||||
AllyHeroDeployLedger ledger = AllyHeroDeployLedger.EnsureInstance();
|
||||
int currentExp = ledger.GetCurrentExp(so.ally_heroID);
|
||||
int unlockedTier = ledger.GetUnlockedTierIndex(so.ally_heroID);
|
||||
bool levelLocked = ledger.IsLevelLockEnabled(so.ally_heroID);
|
||||
return so.GetDisplayLevelRatingKeyFromGrowth(currentExp, unlockedTier, levelLocked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6516,6 +6516,7 @@ MonoBehaviour:
|
||||
notice_display: {fileID: 920723299105391293}
|
||||
market_launch: {fileID: 5379967691629323644}
|
||||
button_userBag: {fileID: 7063352251955770202}
|
||||
enableGuideDisplay: 0
|
||||
userGuideButton: {fileID: 6945238783669072533}
|
||||
guideDisplayImage: {fileID: 2438452334951697137}
|
||||
preIMGb: {fileID: 7091982211545804283}
|
||||
|
||||
@@ -14,6 +14,8 @@ using GameServer.Client;
|
||||
|
||||
public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
{
|
||||
private const string MusicLockedMessage = "暂无可选择的音乐";
|
||||
|
||||
public static event System.Action<bool> GlobalSettingsVisibilityChanged;
|
||||
public static event System.Action<bool> GlobalOverlayPanelVisibilityChanged;
|
||||
public static bool CurrentSettingsVisible { get; private set; }
|
||||
@@ -47,6 +49,8 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
public Button button_userBag;
|
||||
|
||||
[Header("User Guide")]
|
||||
[Tooltip("是否启用指引 sprite 展示。true:点击指引按钮时按场景展示 guideMappings 的 sprite;false:点击无反应,不展示任何指引。")]
|
||||
[SerializeField] private bool enableGuideDisplay = true;
|
||||
[SerializeField] private Button userGuideButton;
|
||||
[SerializeField] private Image guideDisplayImage;
|
||||
[SerializeField] private Button preIMGb;
|
||||
@@ -227,7 +231,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
EnsureMusicPicRoot();
|
||||
SetupMusicPicDefault();
|
||||
if (button_Music != null)
|
||||
button_Music.onClick.AddListener(ToggleMusicPicLocal);
|
||||
button_Music.onClick.AddListener(HandleMusicButtonLockedClick);
|
||||
|
||||
UpdateSteamUserInfo();
|
||||
InitializeMailRedPot();
|
||||
@@ -473,6 +477,11 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
SetMusicPicVisible(!musicPicVisible, false);
|
||||
}
|
||||
|
||||
private void HandleMusicButtonLockedClick()
|
||||
{
|
||||
gNotice.error.display(MusicLockedMessage);
|
||||
}
|
||||
|
||||
private void SetMusicPicVisible(bool visible, bool instant)
|
||||
{
|
||||
if (musicPicRoot == null)
|
||||
@@ -578,7 +587,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
closeGuideButton.onClick.RemoveListener(CloseGuideDisplay);
|
||||
|
||||
if (button_Music != null)
|
||||
button_Music.onClick.RemoveListener(ToggleMusicPicLocal);
|
||||
button_Music.onClick.RemoveListener(HandleMusicButtonLockedClick);
|
||||
|
||||
if (back_navButton != null && navBackAction != null)
|
||||
back_navButton.onClick.RemoveListener(navBackAction);
|
||||
@@ -842,6 +851,16 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
|
||||
|
||||
private void ToggleGuideDisplay()
|
||||
{
|
||||
// 关闭指引展示时点击无反应;若指引已在显示中则允许关闭,避免残留面板卡住。
|
||||
if (!enableGuideDisplay)
|
||||
{
|
||||
if (guideDisplayImage != null && guideDisplayImage.gameObject.activeSelf)
|
||||
{
|
||||
CloseGuideDisplay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (guideDisplayImage == null)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -469,11 +469,11 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
{
|
||||
bool revealLockedSkills = IsSkillEnabledInternal(RevealLockedIdolSkillsSkillIndex);
|
||||
|
||||
idolSkillsHub[] hubs = Resources.FindObjectsOfTypeAll<idolSkillsHub>();
|
||||
idolSkillsHub[] hubs = FindObjectsByType<idolSkillsHub>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < hubs.Length; i++)
|
||||
{
|
||||
idolSkillsHub hub = hubs[i];
|
||||
if (hub == null || !IsSceneInstance(hub))
|
||||
if (hub == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -488,19 +488,19 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
storeSystem[] stores = Resources.FindObjectsOfTypeAll<storeSystem>();
|
||||
storeSystem[] stores = FindObjectsByType<storeSystem>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
if (stores[i] != null && IsSceneInstance(stores[i]))
|
||||
if (stores[i] != null)
|
||||
{
|
||||
stores[i].RefreshPlayerSkillItems();
|
||||
}
|
||||
}
|
||||
|
||||
equipSmelt[] smelters = Resources.FindObjectsOfTypeAll<equipSmelt>();
|
||||
equipSmelt[] smelters = FindObjectsByType<equipSmelt>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < smelters.Length; i++)
|
||||
{
|
||||
if (smelters[i] != null && IsSceneInstance(smelters[i]))
|
||||
if (smelters[i] != null)
|
||||
{
|
||||
smelters[i].RefreshPlayerSkillAdjustedUi();
|
||||
}
|
||||
@@ -516,7 +516,7 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
uLevel_skills[] skillPanels = Resources.FindObjectsOfTypeAll<uLevel_skills>();
|
||||
uLevel_skills[] skillPanels = FindObjectsByType<uLevel_skills>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < skillPanels.Length; i++)
|
||||
{
|
||||
if (skillPanels[i] != null && skillPanels[i].ulsSO != null)
|
||||
|
||||
@@ -167,6 +167,9 @@ public static class SecureSaveVault
|
||||
DeleteIfExists(GetFilePath(category, key, ".bak"));
|
||||
DeleteIfExists(GetFilePath(category, key, ".tmp"));
|
||||
DeleteLegacyPlainFile(legacyPlainPath);
|
||||
// 同时删除恢复镜像(.save_recovery/*),否则下次 TryLoadRawJson 会从镜像
|
||||
// 复活刚删掉的数据并回写主存档——删除必须彻底,覆盖所有根目录变体与 .bak。
|
||||
DeleteRecoveryCopies(category, key);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -176,6 +179,15 @@ public static class SecureSaveVault
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteRecoveryCopies(string category, string key)
|
||||
{
|
||||
IReadOnlyList<string> candidates = GetRecoveryFilePathVariants(category, key);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
DeleteIfExists(candidates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<string> LoadAllRawJson(string category, string legacyDirectory = null, string legacySearchPattern = "*.json")
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
@@ -86,6 +86,12 @@ public class NoteSpawner : MonoBehaviour
|
||||
// optional: constants for runtime clamping (kept for internal use)
|
||||
private const float SpeedMultiplierMin = 0.5f;
|
||||
private const float SpeedMultiplierMax = 2f;
|
||||
private const float FlowSpeedGlobalMultiplier = 1.5f;
|
||||
|
||||
public float EffectiveSpeedMultiplier
|
||||
{
|
||||
get { return Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax) * FlowSpeedGlobalMultiplier; }
|
||||
}
|
||||
|
||||
public NoteJudgeConfig judgeConfig; // Documentation text normalized.
|
||||
private Beatmap beatmap;
|
||||
@@ -264,7 +270,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
|
||||
// Cache parameters that don't change within the loop
|
||||
float sm = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
|
||||
float sm = EffectiveSpeedMultiplier;
|
||||
float baseTravelTime = (60f / bpm) * 4f;
|
||||
float baseNoteTravelTime = baseTravelTime / Mathf.Max(0.0001f, sm);
|
||||
float noteSpeed = CalculateSpeed(baseNoteTravelTime);
|
||||
|
||||
@@ -60,6 +60,13 @@ public class groundParticularController : MonoBehaviour
|
||||
[SerializeField] private float speedMultiplier = 0.1f;
|
||||
[Tooltip("发射速度倍增器")]
|
||||
[SerializeField] private float emissionSpeedMultiplier = 1f;
|
||||
[SerializeField] private float perFrameMoveMultiplier = 0.01f;
|
||||
|
||||
[Header("Rotation Randomness")]
|
||||
[Tooltip("If enabled, each spawned particle gets a random initial rotation offset.")]
|
||||
[SerializeField] private bool enableRandomRotation = false;
|
||||
[Tooltip("Random initial rotation range in degrees. Each axis is offset from -range to +range.")]
|
||||
[SerializeField] private float randomRotationRangeDegrees = 0f;
|
||||
|
||||
[Tooltip("起点缩放抖动强度 (基于音频电平)")]
|
||||
[SerializeField] private float shakeIntensity = 0.1f;
|
||||
@@ -214,7 +221,7 @@ public class groundParticularController : MonoBehaviour
|
||||
Random.Range(-spawnOffsetY, spawnOffsetY)
|
||||
);
|
||||
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
|
||||
particle.transform.rotation = startPoint.rotation;
|
||||
particle.transform.rotation = GetSpawnRotation();
|
||||
|
||||
// 5. 材质均衡选取逻辑
|
||||
MaterialConfig? selectedConfig = GetBalancedMaterialConfig();
|
||||
@@ -251,6 +258,24 @@ public class groundParticularController : MonoBehaviour
|
||||
/// <summary>
|
||||
/// 轮询式均衡选取材质,确保每种材质数量大致相同
|
||||
/// </summary>
|
||||
|
||||
private Quaternion GetSpawnRotation()
|
||||
{
|
||||
if (!enableRandomRotation || randomRotationRangeDegrees <= 0f || startPoint == null)
|
||||
{
|
||||
return startPoint != null ? startPoint.rotation : Quaternion.identity;
|
||||
}
|
||||
|
||||
float range = Mathf.Abs(randomRotationRangeDegrees);
|
||||
Vector3 randomEuler = new Vector3(
|
||||
Random.Range(-range, range),
|
||||
Random.Range(-range, range),
|
||||
Random.Range(-range, range)
|
||||
);
|
||||
|
||||
return startPoint.rotation * Quaternion.Euler(randomEuler);
|
||||
}
|
||||
|
||||
private MaterialConfig? GetBalancedMaterialConfig()
|
||||
{
|
||||
if (particleMaterials == null || particleMaterials.Count == 0) return null;
|
||||
@@ -301,13 +326,13 @@ public class groundParticularController : MonoBehaviour
|
||||
float noteSpawnerSpeedMultiplier = 1.0f;
|
||||
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
||||
{
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.speedMultiplier;
|
||||
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier;
|
||||
}
|
||||
|
||||
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
||||
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
||||
|
||||
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * Time.deltaTime;
|
||||
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * perFrameMoveMultiplier * Time.deltaTime;
|
||||
if (step <= 0) step = 0.01f;
|
||||
|
||||
traveledDist += step;
|
||||
|
||||
@@ -16,10 +16,11 @@ public class GameServerBridge : MonoBehaviour
|
||||
[Header("配置")]
|
||||
[SerializeField] private bool autoSubmitOnSettlement = true;
|
||||
|
||||
private const string HMAC_SECRET = "your_secret_key_change_this_in_production";
|
||||
private const string HMAC_SECRET = "1d95faf15dca25edf0342014428f7dbbd5785ba993113c687b1b86c207b000e0";
|
||||
|
||||
private bool _hasSubmitted = false;
|
||||
private float _gameStartTime;
|
||||
private int _currentRetryCount = 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public class GameServerBridge : MonoBehaviour
|
||||
{
|
||||
_gameStartTime = Time.realtimeSinceStartup;
|
||||
_hasSubmitted = false;
|
||||
_currentRetryCount = 0;
|
||||
if (autoSubmitOnSettlement)
|
||||
settlementController.OnSettlementCompleted += OnSettlementTriggered;
|
||||
}
|
||||
@@ -141,24 +143,31 @@ public class GameServerBridge : MonoBehaviour
|
||||
Debug.Log("╔══════════════════════════════════════════════════════╗");
|
||||
Debug.Log("║ ✅✅✅ 上传成功!数据已写入服务器数据库 ✅✅✅ ║");
|
||||
Debug.Log("╚══════════════════════════════════════════════════════╝");
|
||||
_currentRetryCount = 0;
|
||||
}
|
||||
else if (result == "QUEUED")
|
||||
{
|
||||
Debug.LogWarning("[Bridge] ⏳ 服务器繁忙,3秒后重试...");
|
||||
Debug.LogWarning("[Bridge] ⏳ 服务器繁忙,稍后重试...");
|
||||
_hasSubmitted = false;
|
||||
await Task.Delay(3000);
|
||||
SubmitCurrentSettlement();
|
||||
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
}
|
||||
else if (result == "OPT_OUT")
|
||||
{
|
||||
Debug.Log("[Bridge] 当前已关闭排行榜加入选项,未提交世界排行榜。");
|
||||
_currentRetryCount = 0;
|
||||
}
|
||||
else
|
||||
else if (result == "ERROR" || result == "REJECTED")
|
||||
{
|
||||
Debug.LogWarning($"╔══════════════════════════════════════════════════════╗");
|
||||
Debug.LogWarning($"║ ❌ 上传失败!服务器返回: {result}");
|
||||
Debug.LogWarning($"╚══════════════════════════════════════════════════════╝");
|
||||
_hasSubmitted = false;
|
||||
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[Bridge] 未知返回状态: {result}");
|
||||
_hasSubmitted = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -168,6 +177,58 @@ public class GameServerBridge : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重试上传逻辑:3次重试,每次间隔20秒,失败后缓存到本地
|
||||
/// </summary>
|
||||
private async Task RetrySubmission(string songId, string difficulty, int chartScore, int idolScore,
|
||||
long totalScore, string grade, double runtime, string playedAt, string hmac)
|
||||
{
|
||||
_currentRetryCount++;
|
||||
|
||||
if (_currentRetryCount >= 3)
|
||||
{
|
||||
Debug.LogWarning($"[Bridge] 已重试 {_currentRetryCount} 次仍失败,缓存到本地待下次启动重试");
|
||||
PendingScoreCache.CacheScore(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
_currentRetryCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[Bridge] 等待 20 秒后进行第 {_currentRetryCount} 次重试...");
|
||||
await Task.Delay(20000);
|
||||
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm == null)
|
||||
{
|
||||
Debug.LogWarning("[Bridge] NetworkManager 不存在,缓存到本地");
|
||||
PendingScoreCache.CacheScore(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
_currentRetryCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[Bridge] 开始第 {_currentRetryCount} 次重试上传...");
|
||||
try
|
||||
{
|
||||
string result = await nm.PushSettlement(songId, difficulty, chartScore, idolScore,
|
||||
totalScore, grade, runtime, playedAt, hmac);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
Debug.Log($"[Bridge] ✅ 第 {_currentRetryCount} 次重试成功!");
|
||||
_currentRetryCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[Bridge] 第 {_currentRetryCount} 次重试失败: {result}");
|
||||
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Bridge] 第 {_currentRetryCount} 次重试异常: {ex.Message}");
|
||||
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertDifficulty(int i) => i switch { 0 => "ez", 1 => "hd", 2 => "in", 3 => "im", _ => "unknown" };
|
||||
private static string GetGrade(long s) => s >= 960000 ? "SSS" : s >= 920000 ? "SS" : s >= 880000 ? "S" : s >= 820000 ? "A" : s >= 720000 ? "B" : s >= 600000 ? "C" : s >= 400000 ? "D" : "F";
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace GameServer.Client
|
||||
public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
private static readonly bool VerboseLogs = false;
|
||||
private const int PendingScoreRetryAttemptsPerLaunch = 3;
|
||||
private const float PendingScoreRetryDelaySeconds = 20f;
|
||||
private const string LegacyScoreHmacSecret = "1d95faf15dca25edf0342014428f7dbbd5785ba993113c687b1b86c207b000e0";
|
||||
public static NetworkManager Instance { get; private set; }
|
||||
private static string _startupSteamId = string.Empty;
|
||||
private static string _startupSteamDisplayName = string.Empty;
|
||||
@@ -55,6 +58,8 @@ public class NetworkManager : MonoBehaviour
|
||||
private bool _startupHandshakeCompleted;
|
||||
private Task<string> _avatarUploadTask;
|
||||
private bool _isRefreshingSteamIdentity;
|
||||
private Coroutine _pendingScoreRetryCoroutine;
|
||||
private bool _isRetryingPendingScores;
|
||||
|
||||
private struct CachedRemoteIdentity
|
||||
{
|
||||
@@ -151,6 +156,7 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
|
||||
StartCoroutine(StartupHandshakeRoutine());
|
||||
_pendingScoreRetryCoroutine = StartCoroutine(RetryPendingScoresRoutine());
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] Startup handshake enabled. Settlement submit will use {BuildApiUrl("/api/submit")}");
|
||||
}
|
||||
|
||||
@@ -163,12 +169,137 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
_requestCts?.Cancel();
|
||||
OnlineModeSettings.ModeChanged -= HandleOnlineModeChanged;
|
||||
if (_pendingScoreRetryCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_pendingScoreRetryCoroutine);
|
||||
_pendingScoreRetryCoroutine = null;
|
||||
}
|
||||
if (!_isApplicationQuitting && Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator RetryPendingScoresRoutine()
|
||||
{
|
||||
if (_isRetryingPendingScores)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
_isRetryingPendingScores = true;
|
||||
yield return null;
|
||||
|
||||
List<PendingScoreData> pendingScores = PendingScoreCache.GetPendingScores();
|
||||
if (pendingScores == null || pendingScores.Count == 0)
|
||||
{
|
||||
_isRetryingPendingScores = false;
|
||||
_pendingScoreRetryCoroutine = null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (VerboseLogs)
|
||||
{
|
||||
Debug.Log($"[NetworkManager] Found {pendingScores.Count} cached score(s) to retry on startup.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < pendingScores.Count; i++)
|
||||
{
|
||||
if (_isApplicationQuitting || OnlineModeSettings.IsLocalOnlyMode)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
PendingScoreData score = pendingScores[i];
|
||||
if (score == null || string.IsNullOrWhiteSpace(score.songId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool uploaded = false;
|
||||
for (int attempt = 1; attempt <= PendingScoreRetryAttemptsPerLaunch; attempt++)
|
||||
{
|
||||
string hmac = BuildScoreHmac(score);
|
||||
Task<string> task = PushSettlement(
|
||||
score.songId,
|
||||
score.difficulty,
|
||||
score.chartScore,
|
||||
score.idolScore,
|
||||
score.totalScore,
|
||||
score.grade,
|
||||
score.runtimeSeconds,
|
||||
score.playedAt,
|
||||
hmac);
|
||||
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
if (_isApplicationQuitting)
|
||||
{
|
||||
_isRetryingPendingScores = false;
|
||||
_pendingScoreRetryCoroutine = null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
string status = "ERROR";
|
||||
if (task.Status == TaskStatus.RanToCompletion && !string.IsNullOrWhiteSpace(task.Result))
|
||||
{
|
||||
status = task.Result;
|
||||
}
|
||||
else if (task.IsFaulted)
|
||||
{
|
||||
Debug.LogWarning($"[NetworkManager] Pending score retry faulted for songId={score.songId}: {task.Exception?.GetBaseException().Message}");
|
||||
}
|
||||
|
||||
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PendingScoreCache.RemoveScore(score.songId, score.totalScore, score.playedAt);
|
||||
uploaded = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.Equals(status, "OPT_OUT", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "LOCAL_ONLY", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (VerboseLogs)
|
||||
{
|
||||
Debug.Log($"[NetworkManager] Skip retrying cached score songId={score.songId} because status={status}.");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
PendingScoreCache.IncrementRetryCount(score.songId, score.totalScore, score.playedAt);
|
||||
|
||||
if (attempt < PendingScoreRetryAttemptsPerLaunch)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(PendingScoreRetryDelaySeconds);
|
||||
}
|
||||
}
|
||||
|
||||
if (!uploaded && VerboseLogs)
|
||||
{
|
||||
Debug.Log($"[NetworkManager] Cached score kept for next launch. songId={score.songId} totalScore={score.totalScore}");
|
||||
}
|
||||
}
|
||||
|
||||
_isRetryingPendingScores = false;
|
||||
_pendingScoreRetryCoroutine = null;
|
||||
}
|
||||
|
||||
private static string BuildScoreHmac(PendingScoreData score)
|
||||
{
|
||||
if (score == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string payload = $"{score.songId}|{score.difficulty}|{score.totalScore}|{score.chartScore}|{score.idolScore}";
|
||||
return GameServerSession.ComputeScoreHmac(payload, LegacyScoreHmacSecret);
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameServer.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores score submissions that could not be uploaded because of network issues,
|
||||
/// then retries them on a later startup.
|
||||
/// </summary>
|
||||
public static class PendingScoreCache
|
||||
{
|
||||
[Serializable]
|
||||
private class CachedScore
|
||||
{
|
||||
public string songId;
|
||||
public string difficulty;
|
||||
public int chartScore;
|
||||
public int idolScore;
|
||||
public long totalScore;
|
||||
public string grade;
|
||||
public double runtimeSeconds;
|
||||
public string playedAt;
|
||||
public string hmac;
|
||||
public int retryCount;
|
||||
public string cachedAt;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class CacheContainer
|
||||
{
|
||||
public List<CachedScore> scores = new List<CachedScore>();
|
||||
}
|
||||
|
||||
private static readonly string CachePath = Path.Combine(Application.persistentDataPath, "pending_scores.json");
|
||||
private static readonly object FileLock = new object();
|
||||
|
||||
public static void CacheScore(
|
||||
string songId,
|
||||
string difficulty,
|
||||
int chartScore,
|
||||
int idolScore,
|
||||
long totalScore,
|
||||
string grade,
|
||||
double runtimeSeconds,
|
||||
string playedAt,
|
||||
string hmac)
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
CacheContainer container = LoadContainer();
|
||||
CachedScore existing = FindScore(container, songId, totalScore, playedAt);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.difficulty = difficulty;
|
||||
existing.chartScore = chartScore;
|
||||
existing.idolScore = idolScore;
|
||||
existing.grade = grade;
|
||||
existing.runtimeSeconds = runtimeSeconds;
|
||||
existing.hmac = hmac;
|
||||
if (string.IsNullOrWhiteSpace(existing.cachedAt))
|
||||
{
|
||||
existing.cachedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
container.scores.Add(new CachedScore
|
||||
{
|
||||
songId = songId,
|
||||
difficulty = difficulty,
|
||||
chartScore = chartScore,
|
||||
idolScore = idolScore,
|
||||
totalScore = totalScore,
|
||||
grade = grade,
|
||||
runtimeSeconds = runtimeSeconds,
|
||||
playedAt = playedAt,
|
||||
hmac = hmac,
|
||||
retryCount = 0,
|
||||
cachedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
});
|
||||
}
|
||||
|
||||
SaveContainer(container);
|
||||
Debug.Log($"[PendingScoreCache] Cached score locally. songId={songId} totalScore={totalScore}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[PendingScoreCache] Failed to cache score: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<PendingScoreData> GetPendingScores()
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
CacheContainer container = LoadContainer();
|
||||
var result = new List<PendingScoreData>(container.scores.Count);
|
||||
for (int i = 0; i < container.scores.Count; i++)
|
||||
{
|
||||
CachedScore cached = container.scores[i];
|
||||
if (cached == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new PendingScoreData
|
||||
{
|
||||
songId = cached.songId,
|
||||
difficulty = cached.difficulty,
|
||||
chartScore = cached.chartScore,
|
||||
idolScore = cached.idolScore,
|
||||
totalScore = cached.totalScore,
|
||||
grade = cached.grade,
|
||||
runtimeSeconds = cached.runtimeSeconds,
|
||||
playedAt = cached.playedAt,
|
||||
hmac = cached.hmac,
|
||||
retryCount = cached.retryCount
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[PendingScoreCache] Failed to read cache: {ex.Message}");
|
||||
return new List<PendingScoreData>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveScore(string songId, long totalScore, string playedAt = null)
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
CacheContainer container = LoadContainer();
|
||||
int removed = container.scores.RemoveAll(score =>
|
||||
score != null
|
||||
&& string.Equals(score.songId, songId, StringComparison.Ordinal)
|
||||
&& score.totalScore == totalScore
|
||||
&& (string.IsNullOrWhiteSpace(playedAt)
|
||||
|| string.Equals(score.playedAt, playedAt, StringComparison.Ordinal)));
|
||||
|
||||
if (removed <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveContainer(container);
|
||||
Debug.Log($"[PendingScoreCache] Removed uploaded cached score. songId={songId} totalScore={totalScore}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[PendingScoreCache] Failed to remove cached score: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void IncrementRetryCount(string songId, long totalScore, string playedAt = null)
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
CacheContainer container = LoadContainer();
|
||||
CachedScore score = FindScore(container, songId, totalScore, playedAt);
|
||||
if (score == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
score.retryCount = Mathf.Max(0, score.retryCount) + 1;
|
||||
SaveContainer(container);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[PendingScoreCache] Failed to update retry count: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(CachePath))
|
||||
{
|
||||
File.Delete(CachePath);
|
||||
Debug.Log("[PendingScoreCache] Cleared all cached scores.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[PendingScoreCache] Failed to clear cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetCachedCount()
|
||||
{
|
||||
lock (FileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
return LoadContainer().scores.Count;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static CacheContainer LoadContainer()
|
||||
{
|
||||
if (!File.Exists(CachePath))
|
||||
{
|
||||
return new CacheContainer();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(CachePath);
|
||||
return JsonConvert.DeserializeObject<CacheContainer>(json) ?? new CacheContainer();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[PendingScoreCache] Failed to read cache file, creating a fresh container: {ex.Message}");
|
||||
return new CacheContainer();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveContainer(CacheContainer container)
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(container ?? new CacheContainer(), Formatting.Indented);
|
||||
File.WriteAllText(CachePath, json);
|
||||
}
|
||||
|
||||
private static CachedScore FindScore(CacheContainer container, string songId, long totalScore, string playedAt)
|
||||
{
|
||||
if (container == null || container.scores == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < container.scores.Count; i++)
|
||||
{
|
||||
CachedScore score = container.scores[i];
|
||||
if (score == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool playedAtMatches = string.IsNullOrWhiteSpace(playedAt)
|
||||
|| string.Equals(score.playedAt, playedAt, StringComparison.Ordinal);
|
||||
if (string.Equals(score.songId, songId, StringComparison.Ordinal)
|
||||
&& score.totalScore == totalScore
|
||||
&& playedAtMatches)
|
||||
{
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class PendingScoreData
|
||||
{
|
||||
public string songId;
|
||||
public string difficulty;
|
||||
public int chartScore;
|
||||
public int idolScore;
|
||||
public long totalScore;
|
||||
public string grade;
|
||||
public double runtimeSeconds;
|
||||
public string playedAt;
|
||||
public string hmac;
|
||||
public int retryCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 838755ae207aa0547ae9471fcf9d052f
|
||||
@@ -174,7 +174,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 121, y: -2}
|
||||
m_AnchoredPosition: {x: 0, y: -2}
|
||||
m_SizeDelta: {x: 113, y: 20}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &6919442420098058668
|
||||
@@ -363,7 +363,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 53}
|
||||
m_AnchoredPosition: {x: 0, y: 63.484863}
|
||||
m_SizeDelta: {x: 40, y: 40}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!222 &225269693782171328
|
||||
@@ -439,7 +439,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -2}
|
||||
m_AnchoredPosition: {x: 118, y: -2}
|
||||
m_SizeDelta: {x: 116, y: 20}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &2431985745475878602
|
||||
@@ -533,7 +533,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 128, y: -53}
|
||||
m_AnchoredPosition: {x: 128, y: -63.484863}
|
||||
m_SizeDelta: {x: 256, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &5490276556566493605
|
||||
@@ -616,7 +616,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -143.99913}
|
||||
m_SizeDelta: {x: 0, y: 53}
|
||||
m_SizeDelta: {x: 0, y: 63.484863}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!114 &6728628998878729785
|
||||
MonoBehaviour:
|
||||
@@ -691,7 +691,7 @@ MonoBehaviour:
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: 53
|
||||
m_PreferredHeight: 63.484863
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 2
|
||||
@@ -742,8 +742,8 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 8469056815856914825}
|
||||
- {fileID: 423933864671952839}
|
||||
- {fileID: 8469056815856914825}
|
||||
m_Father: {fileID: 4696069065802349809}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
|
||||
@@ -174,7 +174,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -2}
|
||||
m_AnchoredPosition: {x: 121, y: -2}
|
||||
m_SizeDelta: {x: 113, y: 20}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &6919442420098058668
|
||||
@@ -363,7 +363,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 62.501953}
|
||||
m_AnchoredPosition: {x: 0, y: 125.23433}
|
||||
m_SizeDelta: {x: 40, y: 40}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!222 &225269693782171328
|
||||
@@ -439,7 +439,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 118, y: -2}
|
||||
m_AnchoredPosition: {x: 0, y: -2}
|
||||
m_SizeDelta: {x: 116, y: 20}
|
||||
m_Pivot: {x: 0, y: 0.5}
|
||||
--- !u!222 &2431985745475878602
|
||||
@@ -533,7 +533,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 3840, y: -62.501953}
|
||||
m_AnchoredPosition: {x: 3840, y: -125.234314}
|
||||
m_SizeDelta: {x: 256, y: 0}
|
||||
m_Pivot: {x: 1, y: 0.5}
|
||||
--- !u!114 &5490276556566493605
|
||||
@@ -616,7 +616,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: -143.99913}
|
||||
m_SizeDelta: {x: 0, y: 62.501953}
|
||||
m_SizeDelta: {x: 0, y: 125.23433}
|
||||
m_Pivot: {x: 0.5, y: 1}
|
||||
--- !u!114 &6728628998878729785
|
||||
MonoBehaviour:
|
||||
@@ -691,7 +691,7 @@ MonoBehaviour:
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: 62.501953
|
||||
m_PreferredHeight: 125.23433
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 2
|
||||
@@ -742,8 +742,8 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 423933864671952839}
|
||||
- {fileID: 8469056815856914825}
|
||||
- {fileID: 423933864671952839}
|
||||
m_Father: {fileID: 4696069065802349809}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
|
||||
@@ -170,10 +170,22 @@ public class load_teammatesProfile : MonoBehaviour
|
||||
return GetRatingKeyFromSO(hero);
|
||||
}
|
||||
|
||||
// 等级评级改从持久化账本(AllyHeroDeployLedger)取真值,而非 SO 上被打包烘焙的镜像字段,
|
||||
// 修复客户端更新后卡片显示过期等级、需重新给经验才同步的问题。
|
||||
private string GetRatingKeyFromSO(AllyHero_SO so)
|
||||
{
|
||||
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return "Fallback";
|
||||
return so.GetDisplayLevelRatingKey();
|
||||
|
||||
if (so.ally_heroID <= 0)
|
||||
{
|
||||
return so.GetDisplayLevelRatingKey();
|
||||
}
|
||||
|
||||
AllyHeroDeployLedger ledger = AllyHeroDeployLedger.EnsureInstance();
|
||||
int currentExp = ledger.GetCurrentExp(so.ally_heroID);
|
||||
int unlockedTier = ledger.GetUnlockedTierIndex(so.ally_heroID);
|
||||
bool levelLocked = ledger.IsLevelLockEnabled(so.ally_heroID);
|
||||
return so.GetDisplayLevelRatingKeyFromGrowth(currentExp, unlockedTier, levelLocked);
|
||||
}
|
||||
|
||||
private void ApplyBorderSprite(Image targetImage, string level)
|
||||
|
||||
@@ -91,9 +91,9 @@ public class userSettings : MonoBehaviour
|
||||
if (clearup_saveData_button != null)
|
||||
clearup_saveData_button.onClick.AddListener(OnClearupSaveDataClicked);
|
||||
|
||||
if (export_saveData_button != null) export_saveData_button.onClick.AddListener(ResetClearupCounter);
|
||||
if (import_saveData_button != null) import_saveData_button.onClick.AddListener(ResetClearupCounter);
|
||||
if (view_detailedHighlight_button != null) view_detailedHighlight_button.onClick.AddListener(ResetClearupCounter);
|
||||
if (export_saveData_button != null) export_saveData_button.onClick.AddListener(OnUnimplementedFeatureClicked);
|
||||
if (import_saveData_button != null) import_saveData_button.onClick.AddListener(OnUnimplementedFeatureClicked);
|
||||
if (view_detailedHighlight_button != null) view_detailedHighlight_button.onClick.AddListener(OnUnimplementedFeatureClicked);
|
||||
if (language_dropdown != null) language_dropdown.onValueChanged.AddListener(OnLanguageDropdownValueChanged);
|
||||
|
||||
ResetClearupCounter();
|
||||
@@ -108,9 +108,9 @@ public class userSettings : MonoBehaviour
|
||||
if (clearup_saveData_button != null)
|
||||
clearup_saveData_button.onClick.RemoveListener(OnClearupSaveDataClicked);
|
||||
|
||||
if (export_saveData_button != null) export_saveData_button.onClick.RemoveListener(ResetClearupCounter);
|
||||
if (import_saveData_button != null) import_saveData_button.onClick.RemoveListener(ResetClearupCounter);
|
||||
if (view_detailedHighlight_button != null) view_detailedHighlight_button.onClick.RemoveListener(ResetClearupCounter);
|
||||
if (export_saveData_button != null) export_saveData_button.onClick.RemoveListener(OnUnimplementedFeatureClicked);
|
||||
if (import_saveData_button != null) import_saveData_button.onClick.RemoveListener(OnUnimplementedFeatureClicked);
|
||||
if (view_detailedHighlight_button != null) view_detailedHighlight_button.onClick.RemoveListener(OnUnimplementedFeatureClicked);
|
||||
if (language_dropdown != null) language_dropdown.onValueChanged.RemoveListener(OnLanguageDropdownValueChanged);
|
||||
|
||||
ResetClearupCounter();
|
||||
@@ -129,6 +129,14 @@ public class userSettings : MonoBehaviour
|
||||
clearupClickCount = 0;
|
||||
}
|
||||
|
||||
// 导出/导入存档、查看详细高光等按钮已定义 UI 但尚未接入实际逻辑,
|
||||
// 点击时提示功能未开放。仍复位危险操作确认计数,保持原有副作用。
|
||||
private void OnUnimplementedFeatureClicked()
|
||||
{
|
||||
ResetClearupCounter();
|
||||
gNotice.error.display(LocalizationService.LocalizeLiteral("功能未开放"));
|
||||
}
|
||||
|
||||
private void InitializeLanguageDropdown()
|
||||
{
|
||||
if (language_dropdown == null)
|
||||
|
||||
Reference in New Issue
Block a user