Files
ft/CodexOutput/upserver/B3BatchMigration/M2/C2d修改前快照/084-OfferTreasureAscentManager.cs.txt
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

448 lines
15 KiB
Plaintext

using System.Collections.Generic;
using GameCore;
using System.Linq;
using asap.core;
using UnityEngine;
using cfg;
using System.Text;
using game;
namespace OfferTreasureAscent
{
public class Manager
{
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
private int _status = 0, _eventId, _redirectId;
private List<int> _failedPurchaseRewardIndices;
private PlayerItemData _playerItemData => GContext.container.Resolve<PlayerItemData>();
private cfg.Tables _tables => GContext.container.Resolve<cfg.Tables>();
public int EventId => _eventId;
public int RoundCount
{
get
{
return FtUtils.PopCount(_status);
}
}
public int Status => _status;
#region RedPoint
public bool DoNeedRedPoint()
{
return GetIapId() <= 0;
}
public readonly string RedPointKey = "offer_treasure_ascent";
#endregion
#region DataStorage
public const string PfKey = "OfferTreasureAscent";
public static void InitEvent(FishingEvent e)
{
var manager = GContext.container.Resolve<Manager>();
if (manager == null)
{
GContext.container.Register<Manager>().AsSingleton();
manager = GContext.container.Resolve<Manager>();
}
var loadRes = manager.Load();
if (!loadRes || e.ID != manager.EventId)
{
manager.Init(e.ID);
manager.Save();
}
UITypes.OfferTreasureAscentMainPanel.SetType(manager.GetUiData()[0]);
UITypes.OfferTreasureAscentBoxPanel.SetType(manager.GetUiData()[2]);
}
public void InitTest()
{
_eventId = 90400139;
_redirectId = 1001;
_status = 0;
_failedPurchaseRewardIndices = new List<int>();
}
public void Reset()
{
_status = 0;
_failedPurchaseRewardIndices = new List<int>();
Save();
}
public void Save()
{
var sb = new StringBuilder();
sb.AppendFormat("{0},{1},", _eventId, _status);
if (_failedPurchaseRewardIndices != null && _failedPurchaseRewardIndices.Count > 0)
foreach (var i in _failedPurchaseRewardIndices)
sb.AppendFormat("{0},", i);
saveStore.Write(PfKey, sb.ToString()).RequireAccepted();
}
public bool Load()
{
try
{
var s = saveStore.Read(PfKey).ValueOrThrow();
if (s == null || s == "")
return false;
var tokens = s.Trim(',').Split(',');
if (tokens.Length < 2)
return false;
_eventId = int.Parse(tokens[0]);
_status = int.Parse(tokens[1]);
_failedPurchaseRewardIndices = new List<int>();
for (int i = 2; i < tokens.Length; i++)
_failedPurchaseRewardIndices.Add(int.Parse(tokens[i]));
_redirectId = _tables.TbFishingEvent[_eventId].RedirectID;
return true;
}
catch (System.Exception e)
{
Debug.LogWarning(e);
return false;
}
}
public void Init(int eventId)
{
_eventId = eventId;
_redirectId = _tables.TbFishingEvent[_eventId].RedirectID;
_status = 0;
_failedPurchaseRewardIndices = new List<int>();
Save();
}
#endregion
#region TableContext
private EventOfferTreasureAscent TbMain
{
get
{
return _tables.TbEventOfferTreasureAscent[_redirectId];
}
}
public List<EventOfferTreasureAscentRewards> GetRandomizedRewardList()
{
// warp the ordered list to randomize the order, with player playfab id as seed so that player got consistent result.
var id = GContext.container.Resolve<IUserService>().UserId;
// treat playfab id as an ordinary string instead of a hexadicimal number, and roll it into a hash. Consistant for each player.
int seed = id.Aggregate(17, (hash,c)=> hash * 31 + c);
var rng = new System.Random(seed);
// var rng = new System.Random((int)System.Convert.ToUInt64(id, 16));
var res = _tables.TbEventOfferTreasureAscentRewards.DataList.ToList();
FtUtils.ShuffleList(res, rng);
return res;
}
/// <summary>
/// Get item data from reward index. Will return null if no table data found. Note that the ID of the item, which is not used here, is not the same as the index.
/// </summary>
/// <param name="idx">Index of rewards.</param>
/// <returns>The ItemData, after transition and stuff. Will return null if not found.</returns>
public ItemData GetItemFromIndex(int idx)
{
// var tbRewards = _tables.TbEventOfferTreasureAscentRewards.DataList;
var tbRewards = GetRandomizedRewardList();
idx = Mathf.Clamp(idx, 0, tbRewards.Count - 1);
var reward = tbRewards[idx];
var res = new ItemData(reward.Item, reward.Number);
_playerItemData.ItemTransition(res);
_playerItemData.LureInflation(res, null);
return res;
}
/// <summary>
/// Pick random reward index.
/// </summary>
/// <param name="currentRound">Current round count that limits the option.</param>
/// <returns>Reward idx. -1 if not found.</returns>
public int PickRandomAvailableRewardIdx(int currentRound)
{
// var tbRewards = _tables.TbEventOfferTreasureAscentRewards.DataList;
var tbRewards = GetRandomizedRewardList();
var availableIdxList = tbRewards
.Select((r, i) => (r, i))
.Where(ri => ri.r.LimitConfig - 1 <= currentRound && !IsClaimed(ri.i))
.Select(ri => ri.i)
.ToList();
if (availableIdxList.Count <= 0)
{
Debug.LogError($"[OfferTreasureAscent] No available reward found in round {currentRound}. This is not supposed to happen.");
return -1;
}
var weightList = availableIdxList.Select(i => tbRewards[i].Weight).ToList();
return FtUtils.PickRandomItemWithWeight(availableIdxList, i => weightList[i]);
}
public int PickRandomAvailableRewardIdx()
{
return PickRandomAvailableRewardIdx(RoundCount);
}
public int GetRewardCount()
{
return _tables.TbEventOfferTreasureAscentRewards.DataList.Count;
}
public async System.Threading.Tasks.Task<(int normalRewardIdx, List<ItemData> finalRewards, int iap_id)> GrantRandomReward()
{
var iapId = GetIapId();
var pickedRewardIdx = PickRandomAvailableRewardIdx();
List<ItemData> finalRewards = null;
if (iapId > 0)
{
GContext.container.Resolve<PlayerItemData>().ResolveIapId(iapId, out var iapItemList, out _);
var buyTypeData = new ShopBuyTypeData
{
type = ShopBuyType.EventPack,
ID = EventId,
NoShow = true
};
var rewardItem = GetItemFromIndex(pickedRewardIdx);
if (pickedRewardIdx < 0 || rewardItem == null)
{
Debug.LogError("[OfferTreasureAscent] No reward to claim.");
return (-1, null, iapId);
}
// Drop id required here is fill with 0, because it is a legacy feature and is no longer used.
var res = await GContext.container.Resolve<PlayerShopData>()
.OnBuy(0, buyTypeData, iapItemList, new List<ItemData>() { rewardItem });
if (res)
{
Claim(pickedRewardIdx);
// Debug.Log($"[OfferTreasureAscent] Grant paid {pickedRewardIdx}, ID {pickedRewardIdx + 1}");
finalRewards = TryGrantFinalReward();
}
else
{
RecordFailedRewardIdx(pickedRewardIdx);
return (-1, null, iapId);
}
}
else
{
if (pickedRewardIdx >= 0)
{
Claim(pickedRewardIdx);
TryGrantReward(pickedRewardIdx);
finalRewards = TryGrantFinalReward();
}
}
return (pickedRewardIdx, finalRewards, iapId);
}
public List<ItemData> GetRewards()
{
var res = new List<ItemData>();
for (int i = 0; i < GetRewardCount(); i++)
res.Add(GetItemFromIndex(i));
return res;
}
/// <summary>
/// Get IAP ID list. Fill in 0s for free chances.
/// </summary>
/// <returns>IAP ID</returns>
public List<int> GetIapIdList()
{
var res = new List<int>();
for (int i = 0; i < TbMain.FreeChance; i++)
{
res.Add(0);
}
res.AddRange(TbMain.IapList);
return res;
}
/// <summary>
/// Get IAP ID. Get 0 for free chance.
/// </summary>
/// <param name="currentRound">Round count. Starts with 0.</param>
/// <returns>IAP ID, get 0 for free chance.</returns>
public int GetIapId(int currentRound)
{
var iapIdList = GetIapIdList();
currentRound = Mathf.Clamp(currentRound, 0, iapIdList.Count - 1);
return iapIdList[currentRound];
}
/// <summary>
/// Get IAP ID. Get 0 for free chance. For current round.
/// </summary>
/// <returns>IAP ID of this round. 0 for free chance.</returns>
public int GetIapId()
{
return GetIapId(RoundCount);
}
#endregion
#region UI data
/// <summary>
/// Get UI Data.
/// </summary>
/// <returns>First is main panel, second is the grand reward showing animation panel.</returns>
public string[] GetUiData()
{
return new string[] { TbMain.UIPanel, TbMain.Icon, TbMain.BoxOpenPrefab};
}
/// <summary>
/// Get Entrance button data.
/// </summary>
/// <param name="idx"></param>
public OfferTreasureAscentEntranceData GetEntranceData()
{
var res = new OfferTreasureAscentEntranceData();
res.IconUrl = TbMain.Icon;
(var startTime, var endTime) = FtUtils.GetEventTimeLimit(EventId);
res.TimerData = new TimerContext
{
StartTime = startTime,
ExpiryTime = endTime
};
return res;
}
public GeneralProbabilityData[] GetProbabilityData()
{
var res = new GeneralProbabilityData[GetRewardCount()];
var tbRewards = GetRandomizedRewardList();
var totalWeight = tbRewards.Where((r, i) => !IsClaimed(i)).Sum(r => r.Weight);
for (int i = 0; i < GetRewardCount(); i++)
{
res[i] = new GeneralProbabilityData
{
Item = GetItemFromIndex(i),
Probability = tbRewards[i].Weight / (float)totalWeight,
IsClaimed = IsClaimed(i)
};
}
return res;
}
#endregion
#region System
public void Claim(int idx)
{
_status |= 1 << idx;
Save();
}
public bool IsClaimed(int idx)
{
return (_status & (1 << idx)) != 0;
}
public ItemData GetGrandReward()
{
return new ItemData(TbMain.ChestReward, 1);
}
public int GetGrandRewardItemId()
{
return TbMain.ChestReward;
}
/// <summary>
/// Check if all normal rewards are claimed
/// </summary>
/// <returns>True is all is claimed.</returns>
public bool IsNormalRewardsDepleted
{
get
{
int rewardCount = GetRewardCount();
if (rewardCount <= 0)
return true;
if (rewardCount > 31)
{
UnityEngine.Debug.LogError("[OfferTreasureAscent] Reward count is too large for int: " + rewardCount);
return true;
}
int expectedMask = (1 << rewardCount) - 1;
return (_status & expectedMask) == expectedMask;
}
}
public bool IsFinalRewardClaimed
{
get
{
return IsNormalRewardsDepleted && IsClaimed(GetRewardCount());
}
}
/// <summary>
/// Grant reward according to reward index, and try grant final reward if all rewards are claimed.
/// </summary>
/// <param name="rewardIdx">The index of the target reward.</param>
public void TryGrantReward(int rewardIdx)
{
Debug.Log($"[OfferTreasureAscent] Grant free {rewardIdx}, ID {rewardIdx + 1}");
var rewardItem = GetItemFromIndex(rewardIdx);
if (rewardItem == null)
{
Debug.LogError("[OfferTreasureAscent] Reward item is null. This is not supposed to happen.");
return;
}
_playerItemData.AddItem(rewardItem, null);
GContext.Publish(new ShowData(rewardItem));
}
public List<ItemData> TryGrantFinalReward()
{
// Debug.Log("[OfferTreasureAscent]Try Grant Final...");
if (!IsNormalRewardsDepleted || IsFinalRewardClaimed)
return null;
// var grandReward = GetGrandReward();
// _playerItemData.AddItem(grandReward, null);
var grandRewardId = GetGrandRewardItemId();
var items = FtUtils.ResolveChestContent(grandRewardId);
if (items.Count <= 0)
{
Debug.LogError("[OfferTreasureAscent] Grand reward is empty.");
return null;
}
_playerItemData.AddItem(items, null);
Claim(GetRewardCount());
// Debug.Log("[OfferTreasureAscent] Grant Final.");
return items;
}
public void OnBuySuccess()
{
if (_failedPurchaseRewardIndices is { Count: > 0 })
{
foreach (var idx in _failedPurchaseRewardIndices)
Claim(idx);
_failedPurchaseRewardIndices.Clear();
Save();
}
TryGrantFinalReward();
}
public void RecordFailedRewardIdx(int idx)
{
_failedPurchaseRewardIndices.Add(idx);
Save();
}
#endregion
}
}