Files
ft/Client/Assets/Scripts/DataCenter/OfferChainsChestManager.cs
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

574 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using asap.core;
using cfg;
using game;
using GameCore;
using Newtonsoft.Json;
using UnityEngine;
namespace DataCenter
{
// 锁链礼包
public class TOfferChainsData
{
public class TItem
{
public int Id;
public int Item;
public int Number;
// 0 可选,1 高亮 2 糊了 ,锁链隐藏 应该转换成Enum
public int Status;
// public int SKUId;
}
public bool FirstOpen; // 是否首次打开
public int EventId; // 默认存一下ID
public DateTime StartTime; // 开始时间
public DateTime EndTime;// 结束时间
public int ItemId = -1;
// 当前抽奖次数,必须抽了才会更新
public int LotteryNum;
public List<TItem> Items;
public bool Finished;
public int ChestReward;
public List<int> RewardList; // 当前获得的奖励Item
}
public class OfferChainsChestManager
{
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
public const string SaveKey = "OfferChainsChestData";
[Inject] public Tables _tables { get; set; }
// 计入当前Event
private FishingEvent _fishingEvent;
//
public EventOfferChainsChestMain OfferChainsChestMain { get; set; }
public List<EventOfferChainsChestRewards> OfferChainsChestRewards { get; set; }
// 当前可保存数据
public TOfferChainsData OfferChainsData { get; private set; }
internal static game.PlayerDataStorage.PlayerKVOfferState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeOffer(raw,false,snapshot,errors);
internal void ResetForKVLoad()
{
OfferChainsData=null; ValidIds=new List<int>(); _fishingEvent=null; _isInit=false;
OfferChainsChestMain=null; OfferChainsChestRewards=null;
}
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVOfferState state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
OfferChainsData=!state.IsPresent ? null : new TOfferChainsData
{
FirstOpen=state.FirstOpen,EventId=state.EventId,StartTime=state.StartTime,EndTime=state.EndTime,
ItemId=state.ItemId,LotteryNum=state.LotteryNum,
Items=state.Items.Select(item=>new TOfferChainsData.TItem {
Id=item.Id,Item=item.Item,Number=item.Number,Status=item.Status }).ToList(),
RewardList=new List<int>(state.RewardList),Finished=state.Finished,ChestReward=state.ChestReward
};
}
internal bool IsInactiveForKVValidation => !_isInit && _fishingEvent == null;
public List<int> ValidIds;
public bool IsFirstOpen
{
get => OfferChainsData.FirstOpen;
set
{
if (OfferChainsData.FirstOpen)
{
OfferChainsData.FirstOpen = false;
SaveData();
}
}
}
public void UpdateEventData(FishingEvent fishingEvent)
{
ELog($"UpdateEventData - > {fishingEvent}");
_fishingEvent = fishingEvent;
UpdateLocalData();
}
private void UpdateLocalData()
{
//选择当前的 Main
var redirectId = _fishingEvent.RedirectID;
var configList = _tables.TbEventOfferChainsChestMain.DataList;
foreach (var eventChallenge in configList.Where(eventChallenge => eventChallenge.ID == redirectId))
{
OfferChainsChestMain = eventChallenge;
break;
}
OfferChainsChestRewards = _tables.TbEventOfferChainsChestRewards.DataList;
// SkuIds = OfferChainsChestMain.IapList;
}
private bool _isInit = false;
public void Init()
{
ELog("Init-> ");
if (_fishingEvent == null)
return;
//-
if (_fishingEvent.ID != OfferChainsData?.EventId)
{
ClearOfferChainsData();
}
if (_isInit)
return;
if (OfferChainsData == null)
{
if (_fishingEvent == null) // 不存在Event
{
return;
}
InitOfferChainsData();
// CreateNewData();
}
UpdateOfferChainsData();
//UpdateData()
_isInit = true;
ELog("初始化完成-> ");
}
//
private void ProcessLastOne()
{
ELog("ProcessLastOne-> ");
if (OfferChainsData != null)
{
Settle();
// if (!IsTimeAllow())
// {
// Settle();
// // ClearOfferChainsData();
// }
// // else if (IsCanGetFinalReward())
// else
// {
// Settle(); // 补充一下最终大奖
// }
}
}
private void UpdateOfferChainsData()
{
ELog("UpdateOfferChainsData");
// 更新可用数据
UpdateTime();
UpdateOfferChainsItems();
UpdateValidIds();
GetLotteryItemId();
//-
if (_fishingEvent != null && OfferChainsData != null)
{
GContext.container.Resolve<IFaceUIService>().AddGiftFaceUI(_fishingEvent.ID, new UIType(OfferChainsChestMain.UIPanel), 0);
}
}
private void UpdateValidIds()
{
var lotteryNum = OfferChainsData.LotteryNum;
ValidIds = (from item in OfferChainsData?.Items where item.Status == 0 select item.Id).ToList();
}
private void InitOfferChainsData()
{
var limitedTime = (LimitedTime)_fishingEvent.TimeDefinition;
var startTime = GlobalUtils.TryParseDateTime(limitedTime.StartTime, ZZTimeHelper.UtcNow());
var endTime = GlobalUtils.TryParseDateTime(limitedTime.EndTime, ZZTimeHelper.UtcNow());
OfferChainsData = new TOfferChainsData
{
FirstOpen = true,
EventId = _fishingEvent.ID,
StartTime = startTime,
EndTime = endTime,
Items = new List<TOfferChainsData.TItem>(),
RewardList = new List<int>(),
ChestReward = OfferChainsChestMain.ChestReward, // 最终大奖存储下,方便额外的处理
};
for (var i = 0; i < OfferChainsChestRewards.Count; ++i)
{
var item = new TOfferChainsData.TItem
{
Id = OfferChainsChestRewards[i].ID,
Item = OfferChainsChestRewards[i].Item,
Number = OfferChainsChestRewards[i].Number,
Status = 0,
// SKUId = OfferChainsChestMain.IapList[i],
};
OfferChainsData.Items.Add(item);
}
}
public bool IsTimeAllow()
{
var now = ZZTimeHelper.UtcNow();
var startTime = OfferChainsData.StartTime;
var endTime = OfferChainsData.EndTime;
#if UNITY_EDITOR
var timeOpened = (now - startTime).TotalSeconds;
var timeEnded = (endTime - now).TotalSeconds;
ELog($"StartTime: {startTime} Now: {now} EndTime:{endTime} TimeOpened:{timeOpened} TimeToEnd:{timeEnded} Other: {1800000 - timeOpened} IsTimeAllow => {now <= endTime}");
#endif
return now >= startTime && now <= endTime;
}
public void CheckInit()
{
ELog("CheckInit -> ");
ProcessLastOne();
Init();
}
// ReSharper disable Unity.PerformanceAnalysis
private static void Log(object t)
{
Debug.Log($"<color=yellow>OfferChainsChestManager -> {t} </color>");
}
private static void ELog(object t)
{
#if UNITY_EDITOR
Debug.Log($"<color=yellow>OfferChainsChestManager -> {t} </color>");
#endif
}
public bool CheckOpen()
{
//
if (OfferChainsData == null) return false;
//
if (!IsTimeAllow()) return false;
if (OfferChainsChestMain == null)
{
return false;
}
if (IsFinished())
{
return false;
}
return true;
}
public bool IsFinished()
{
return OfferChainsData?.Finished ?? false;
}
private void SaveData()
{
ELog($"SaveData-> {JsonConvert.SerializeObject(OfferChainsData)}");
playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferChainsData)).RequireAccepted();
}
public void LoadData(string dataValue)
{
ELog($"LoadData -> {dataValue}");
OfferChainsData = JsonConvert.DeserializeObject<TOfferChainsData>(dataValue);
}
// 进入游戏舞台
public async void OnEnterGameAct()
{
if (!playerSaveStore.IsCurrent) return;
Log("OnEnterGameAct()");
IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferChainsChestMain.UIPanel));
}
public static T WeightedRandom<T>(Dictionary<T, int> weightTable)
{
// 1. 计算总权重
var totalWeight = weightTable.Values.Sum();
// 2. 生成随机数
var randomValue = UnityEngine.Random.Range(0, totalWeight);
// 3. 线性遍历选择
var currentWeight = 0;
foreach (var (key, value) in weightTable)
{
currentWeight += value;
if (randomValue < currentWeight)
return key;
}
return default; // 理论上不会执行到这里
}
// 活动当前的抽奖索引
private int CalcLotteryItemId()
{
var lotteryNum = OfferChainsData.LotteryNum;
if (ValidIds.Count <= 0)
{
return -1;
}
if (OfferChainsChestRewards == null)
{
return -1;
}
//
// while (true)
// {
var dict = OfferChainsChestRewards.Where(
item => ValidIds.Contains(item.ID) && item.LimitConfig <= lotteryNum)
.ToDictionary(elem => (elem.ID, elem.LimitConfig), elem => elem.Weight);
var (elemId, limitConfig) = WeightedRandom(dict);
Log($"CalcLotteryItemId -> {lotteryNum} {elemId} {limitConfig}");
// if (limitConfig >0 && lotteryNum > limitConfig )
return elemId;
// }
}
public int GetLotteryItemId()
{
ELog($"GetLotteryItemId -> {OfferChainsData.ItemId}");
if (OfferChainsData.ItemId == -1 || !ValidIds.Contains(OfferChainsData.ItemId))
{
UpdateLotteryItemId();
}
return OfferChainsData.ItemId;
}
private void UpdateOfferChainsItems()
{
if (OfferChainsData != null && _fishingEvent != null && OfferChainsData.EventId == _fishingEvent.ID)
{
var items = OfferChainsData.Items;
if (OfferChainsChestRewards is { Count: > 0 })
{
for (int i = 0; i < items.Count; ++i)
{
EventOfferChainsChestRewards reward = null;
if (i < OfferChainsChestRewards.Count)
{
reward = OfferChainsChestRewards[i];
}
if (reward != null)
{
items[i].Id = reward.ID;
items[i].Item = reward.Item;
items[i].Number = reward.Number;
}
}
}
if (OfferChainsChestMain != null)
{
OfferChainsData.ChestReward = OfferChainsChestMain.ChestReward;
}
}
// for (var i = 0; i < OfferChainsChestRewards.Count; ++i)
// {
// var item = new TOfferChainsData.TItem
// {
// Id = OfferChainsChestRewards[i].ID,
// Item = OfferChainsChestRewards[i].Item,
// Number = OfferChainsChestRewards[i].Number,
// Status = 0,
// };
// OfferChainsData.Items.Add(item);
// }
}
private void UpdateTime()
{
if (OfferChainsData != null && _fishingEvent != null && OfferChainsData.EventId == _fishingEvent.ID)
{
var limitedTime = (LimitedTime)_fishingEvent.TimeDefinition;
var startTime = GlobalUtils.TryParseDateTime(limitedTime.StartTime, ZZTimeHelper.UtcNow());
var endTime = GlobalUtils.TryParseDateTime(limitedTime.EndTime, ZZTimeHelper.UtcNow().AddDays(-1));
OfferChainsData.StartTime = startTime;
OfferChainsData.EndTime = endTime;
}
}
private void UpdateLotteryItemId()
{
var targetId = CalcLotteryItemId();
ELog($"UpdateLotteryItemId -> {OfferChainsData.ItemId} => {targetId}");
OfferChainsData.ItemId = targetId;
SaveData();
}
//- 结束当前
public void FinishLottery(int targetId)
{
Log($"FinishLottery -> {targetId}");
var lastItem = OfferChainsData.Items.SingleOrDefault(it => it.Id == targetId);
if (lastItem is { Status: not 2 })
{
lastItem.Status = 2;
}
}
//- 开始下一轮数据
public void UpdateForNextLottery()
{
UpdateValidIds();
UpdateLotteryItemId();
}
public bool IsCanGetFinalReward()
{
var ret = OfferChainsData.LotteryNum >= OfferChainsData.Items.Count
&& !OfferChainsData.Finished;
ELog($"IsCanGetFinalReward {ret} : {OfferChainsData.LotteryNum} {OfferChainsData.Items.Count} {OfferChainsData.Finished}");
return ret;
}
public void ClearOfferChainsData()
{
OfferChainsData = null;
_isInit = false;
SaveData();
}
public TOfferChainsData.TItem GetTItemById(int targetId)
{
var lastItem = OfferChainsData.Items.FirstOrDefault(it => it.Id == targetId);
return lastItem;
}
public int GetDropIdById(int id)
{
var item = GetTItemById(id);
var itemData = _tables.TbItem.GetOrDefault(item.Item);
return itemData.RedirectID;
}
public void OnGetFinalReward()
{
ELog("OnGetFinalReward()-> ");
// var item = _tables.TbItem.GetOrDefault(OfferChainsChestMain.ChestReward);
var item = _tables.TbItem.GetOrDefault(OfferChainsData.ChestReward);
var itemFinal = GContext.container.Resolve<PlayerItemData>()
.GetItemDropItemList(item.RedirectID, 1);
GContext.container.Resolve<PlayerItemData>().LureInflation(itemFinal, null);
GContext.container.Resolve<PlayerItemData>().AddItem(itemFinal, null);
GContext.Publish(new ShowData(itemFinal));
GContext.Publish(new ShowData());
OfferChainsData.Finished = true;
SaveData();
GContext.Publish(new OfferChainsRefreshEvent());
}
public float GetProbability(int id)
{
var theIdWeight = OfferChainsChestRewards.FirstOrDefault(item => item.ID == id)?.Weight ?? 0;
var weightTable = OfferChainsChestRewards
.Where(item => !IsItemFinished(item.ID))
.Select(item => item.Weight).ToList();
var totalWeight = weightTable.Sum();
return (float)Math.Round((float)theIdWeight / totalWeight * 100, 2);
}
private bool IsItemFinished(int itemID)
{
var lastItem = OfferChainsData.Items.SingleOrDefault(it => it.Id == itemID);
return lastItem?.Status == 2;
}
public TimeSpan GetTimeRemain()
{
if (OfferChainsData != null)
{
return OfferChainsData.EndTime - ZZTimeHelper.UtcNow();
}
// 活动未开始
return ZZTimeHelper.UtcNow().AddDays(-1) - ZZTimeHelper.UtcNow();
}
public void Settle()
{
ELog("Settle");
if (IsCanGetFinalReward())
{
OnGetFinalReward();
}
}
public bool UIRunning { get; set; } = false;
public void OnBuySuccess()
{
Log("OnBuySuccess -> ");
OfferChainsData.RewardList.Add(OfferChainsData.ItemId);
OfferChainsData.LotteryNum += 1;
// 为了处理打点
if (IsCanGetFinalReward())
{
OfferChainsData.RewardList.Add(OfferChainsData.ChestReward);
}
FinishLottery(OfferChainsData.ItemId);
SaveData(); // -存
EventTracking();
//NEXT TURN
if (UIRunning)
{ // 播放动画逻辑,交给UI
return;
}
UpdateForNextLottery();
Settle();
}
public void EventTracking()
{
//var price = GetPayAmountById(OfferChainsData.ItemId);
var price = GetIapItemListThisTime(OfferChainsData.LotteryNum - 1)?.PaymentAmount ?? 0F;
var itemEnt = GetTItemById(OfferChainsData.ItemId);
var rewardStr = string.Join(',', OfferChainsData.RewardList);
var number = OfferChainsData.LotteryNum;
#if AGG
using (var e = GEvent.TackEvent("offer_chainchest"))
{
e.AddContent("event_id", OfferChainsData.EventId);
e.AddContent("number", number);
e.AddContent("price", price);
e.AddContent("reward", OfferChainsData.ItemId);
e.AddContent("reward_claimed_list", rewardStr);
}
#endif
Log($"EventTracking: event_id: {OfferChainsData.EventId} number: {number} price: {price} " +
$"reward :{OfferChainsData.ItemId} reward_claimed_list :{rewardStr}");
}
public IAPItemList GetIapItemListThisTime()
{
return GetIapItemListThisTime(OfferChainsData.LotteryNum);
}
private IAPItemList GetIapItemListThisTime(int index)
{
// var index = OfferChainsData.LotteryNum;
index = Mathf.Clamp(index, 0, OfferChainsChestMain.IapList.Count - 1);
var iapId = OfferChainsChestMain.IapList[index];
var iAPItemList = _tables.TbIAPItemList.GetOrDefault(iapId);
return iAPItemList;
}
}
}