feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

View File

@@ -0,0 +1,588 @@
using System;
using System.Collections.Generic;
using System.Linq;
using asap.core;
using cfg;
using GameCore;
using Newtonsoft.Json;
using UnityEngine;
namespace DataCenter
{
// 数据保存
public enum TItemStatues
{
Valid = 0,
HighLight = 1,
Done = 2,
}
public class TOfferDiscoPartyData
{
public class TItem
{
public int Id;
public int Item;
public int Number;
// 0 可选,1 高亮 2 糊了 ,锁链隐藏 应该转换成Enum
public TItemStatues Status;
}
public bool FirstOpen; // 是否首次打开
public int EventId; // 默认存一下ID
public DateTime StartTime; // 开始时间
public DateTime EndTime; // 结束时间
public int ItemId = -1;
public int LotteryNum;
// public bool Finished;
// 这个看看能否删掉,只保留状态
public List<TItem> Items;
public List<int> RewardList; // 当前获得的奖励Item
}
// Disco Manager
public class OfferDiscoPartyManager
{
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
public const string SaveKey = "OfferDiscoPartyManager";
// 免费抽奖次数
public int FreeTime = 1;
[Inject] public Tables _tables { get; set; }
#region 数据
private FishingEvent _fishingEvent;
public EventOfferDiscoParty OfferDiscoParty { get; private set; }
public List<EventOfferDiscoPartyReward> OfferDiscoPartyRewards { get; private set; }
public TOfferDiscoPartyData OfferDiscoPartyData { 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,true,snapshot,errors);
internal void ResetForKVLoad()
{
OfferDiscoPartyData=null; ValidIds=new List<int>(); _fishingEvent=null; _isInit=false;
OfferDiscoParty=null; OfferDiscoPartyRewards=null; FreeTime=1;
}
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVOfferState state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
OfferDiscoPartyData=!state.IsPresent ? null : new TOfferDiscoPartyData
{
FirstOpen=state.FirstOpen,EventId=state.EventId,StartTime=state.StartTime,EndTime=state.EndTime,
ItemId=state.ItemId,LotteryNum=state.LotteryNum,
Items=state.Items.Select(item=>new TOfferDiscoPartyData.TItem {
Id=item.Id,Item=item.Item,Number=item.Number,Status=(TItemStatues) item.Status }).ToList(),
RewardList=new List<int>(state.RewardList)
};
}
internal bool IsInactiveForKVValidation => !_isInit && _fishingEvent == null;
public List<int> ValidIds; //
#endregion
public bool IsFirstOpen
{
get => OfferDiscoPartyData?.FirstOpen?? false;
set
{
if (OfferDiscoPartyData?.FirstOpen??false)
{
OfferDiscoPartyData.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.TbEventOfferDiscoParty.DataList;
foreach (var eventChallenge in configList.Where(eventChallenge => eventChallenge.ID == redirectId))
{
OfferDiscoParty = eventChallenge;
break;
}
FreeTime = OfferDiscoParty.Free;
var rewardList = OfferDiscoParty.RewardList;
// OfferDiscoPartyRewards = _tables.TbEventOfferDiscoPartyReward.DataList;
var list = _tables.TbEventOfferDiscoPartyReward.DataList;
//TODO:LF 这个筛选未应用到程序中,看策划的应用
//
OfferDiscoPartyRewards = list.Where(item => rewardList.Contains(item.ID)).ToList();
OfferDiscoPartyRewards.Sort((a,b) => a.ID.CompareTo(b.ID));
//添加拍脸
// _tables.TbEventOfferDiscoParty
}
private bool _isInit = false;
public void Init()
{
ELog("Init-> ");
if (_fishingEvent == null)
return;
if (_fishingEvent.ID != OfferDiscoPartyData?.EventId)
{
ClearOfferDiscoPartyData();
}
if (_isInit)
return;
if (OfferDiscoPartyData == null)
{
if (_fishingEvent == null) // 不存在Event
{
return;
}
InitOfferDiscoPartyData();
}
UpdateOfferDiscoPartyData();
_isInit = true;
ELog("初始化完成-> ");
}
private void ProcessLastOne()
{
if (OfferDiscoPartyData != null)
{
Settle();
// if (!IsTimeAllow())
// {
// Settle();
// ClearOfferDiscoPartyData();
// }
// else if (IsFinished())
// {
// Settle(); // 补充一下最终大奖
// }
}
}
private void InitOfferDiscoPartyData()
{
var limitedTime = (LimitedTime)_fishingEvent.TimeDefinition;
var startTime = GlobalUtils.TryParseDateTime(limitedTime.StartTime, ZZTimeHelper.UtcNow());
var endTime = GlobalUtils.TryParseDateTime(limitedTime.EndTime, ZZTimeHelper.UtcNow());
OfferDiscoPartyData = new TOfferDiscoPartyData
{
FirstOpen = true,
EventId = _fishingEvent.ID,
StartTime = startTime,
EndTime = endTime,
Items = new List<TOfferDiscoPartyData.TItem>(),
RewardList = new List<int>()
};
for (var i = 0; i < OfferDiscoPartyRewards.Count; ++i)
{
var item = new TOfferDiscoPartyData.TItem
{
Id = OfferDiscoPartyRewards[i].ID,
Item = OfferDiscoPartyRewards[i].Item,
Number = OfferDiscoPartyRewards[i].Number,
Status = 0,
};
OfferDiscoPartyData.Items.Add(item);
}
}
private void UpdateOfferDiscoPartyData()
{
ELog("UpdateOfferDiscoPartyData");
// 更新可用数据
OfferDiscoPartyData.EventId = _fishingEvent?.ID ?? OfferDiscoPartyData.EventId;
UpdateTime();
UpdateOfferDiscoPartyItems();
UpdateValidIds();
// UpdateLotteryItemId();
GetLotteryItemId();
if (_fishingEvent != null)
{
GContext.container.Resolve<IFaceUIService>().AddGiftFaceUI(_fishingEvent.ID, new UIType(OfferDiscoParty.UIPanel), 0);
}
}
public bool IsTimeAllow()
{
var now = ZZTimeHelper.UtcNow();
var startTime = OfferDiscoPartyData.StartTime;
var endTime = OfferDiscoPartyData.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();
}
private static void Log(object t)
{
Debug.Log($"<color=orange>OfferDiscoPartyManager -> {t} </color>");
}
private static void ELog(object t)
{
#if UNITY_EDITOR
Debug.Log($"<color=yellow>OfferDiscoPartyManager -> {t} </color>");
#endif
}
public bool CheckOpen()
{
//
if (OfferDiscoPartyData == null) return false;
//
if (!IsTimeAllow()) return false;
if (OfferDiscoParty == null)
{
return false;
}
if (IsFinished())
{
return false;
}
return true;
}
public bool IsFinished()
{
// return OfferDiscoPartyData?.Finished ?? false;
return OfferDiscoPartyData.LotteryNum >= OfferDiscoPartyData.Items.Count;
}
private void SaveData()
{
playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferDiscoPartyData)).RequireAccepted();
}
public void LoadData(string dataValue)
{
ELog($"LoadData- {dataValue}");
OfferDiscoPartyData = JsonConvert.DeserializeObject<TOfferDiscoPartyData>(dataValue);
//
// if (true)
// {
// OfferDiscoPartyData.EventId = 1001;
// SaveData();
// }
}
public async void OnEnterGameAct()
{
if (!playerSaveStore.IsCurrent) return;
ELog("OnEnterGameAct()");
IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferDiscoParty.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()
{
// UpdateValidIds();
var lotteryNum = OfferDiscoPartyData.LotteryNum;
if (ValidIds.Count <= 0)
{
return -1;
}
if (OfferDiscoPartyRewards == null)
{
return -1;
}
//
// while (true)
// {
var dict = OfferDiscoPartyRewards
.Where(item => ValidIds.Contains(item.ID) && lotteryNum >= (item.LimitConfig - 1))
.ToDictionary(elem => (elem.ID, elem.LimitConfig), elem => elem.Weight);
if (dict?.Count <= 0)
return -1;
var (elemId, limitConfig) = WeightedRandom(dict);
ELog($"CalcLotteryItemId -> {lotteryNum} {elemId} {limitConfig}");
// if (limitConfig >0 && lotteryNum > limitConfig )
return elemId;
// }
}
private void UpdateValidIds()
{
ValidIds = (from item in OfferDiscoPartyData?.Items where item.Status == 0 select item.Id).ToList();
// return lotteryNum;
}
public int GetLotteryItemId()
{
ELog($"GetLotteryItemId -> {OfferDiscoPartyData.ItemId}");
if (OfferDiscoPartyData.ItemId == -1 || !ValidIds.Contains(OfferDiscoPartyData.ItemId))
{
UpdateLotteryItemId();
}
return OfferDiscoPartyData.ItemId;
}
private void UpdateOfferDiscoPartyItems()
{
ELog("UpdateOfferDiscoPartyItems-> ");
if (OfferDiscoPartyData != null && _fishingEvent != null && OfferDiscoPartyData.EventId == _fishingEvent.ID)
{
var items = OfferDiscoPartyData.Items;
if (OfferDiscoPartyRewards is { Count: > 0 })
{
for (int i = 0; i < items.Count; ++i)
{
EventOfferDiscoPartyReward reward = null;
if (i < OfferDiscoPartyRewards.Count)
{
reward = OfferDiscoPartyRewards[i];
}
if (reward != null)
{
items[i].Id = reward.ID;
items[i].Item = reward.Item;
items[i].Number = reward.Number;
}
}
}
}
}
private void UpdateTime()
{
if (OfferDiscoPartyData != null && _fishingEvent != null && OfferDiscoPartyData.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));
OfferDiscoPartyData.StartTime = startTime;
OfferDiscoPartyData.EndTime = endTime;
}
}
private void UpdateLotteryItemId()
{
var targetId = CalcLotteryItemId();
OfferDiscoPartyData.ItemId = targetId;
SaveData();
ELog($"UpdateLotteryItemId -> {targetId}");
}
public void FinishLottery(int targetId)
{
ELog($"FinishLottery -> {targetId}");
var lastItem = OfferDiscoPartyData.Items.SingleOrDefault(it => it.Id == targetId);
if (lastItem is { Status: not TItemStatues.Done })
{
lastItem.Status = TItemStatues.Done;
UpdateForNextLottery();
}
}
//- 开始下一轮数据
public void UpdateForNextLottery()
{
UpdateValidIds();
UpdateLotteryItemId();
}
public void ClearOfferDiscoPartyData()
{
OfferDiscoPartyData = null;
_isInit = false;
SaveData();
}
public TOfferDiscoPartyData.TItem GetTItemById(int targetId)
{
var lastItem = OfferDiscoPartyData.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 OnSettleFinalEvent()
{
// var item = _tables.TbItem.GetOrDefault(OfferDiscoParty.ChestReward);
// var item = _tables.TbItem.GetOrDefault(OfferDiscoPartyData.ChestReward);
// var itemFinal = GContext.container.Resolve<PlayerItemData>()
// .GetItemDropPackageItemList(item.RedirectID, 1);
// GContext.container.Resolve<PlayerItemData>().LureInflation(itemFinal, null);
// GContext.container.Resolve<PlayerItemData>().AddItem(itemFinal);
// GContext.Publish(new ShowData(itemFinal));
// GContext.Publish(new ShowData());
// OfferDiscoPartyData.Finished = true;
SaveData();
GContext.Publish(new OfferDiscoPartyRefreshEvent());
}
public float GetProbability(int id)
{
var theIdWeight = OfferDiscoPartyRewards.FirstOrDefault(item => item.ID == id)?.Weight ?? 0;
var weightTable = OfferDiscoPartyRewards
.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 = OfferDiscoPartyData.Items.SingleOrDefault(it => it.Id == itemID);
return lastItem?.Status == TItemStatues.Done;
}
public TimeSpan GetTimeRemain()
{
if (OfferDiscoPartyData != null)
{
return OfferDiscoPartyData.EndTime - ZZTimeHelper.UtcNow();
}
// 活动未开始
return ZZTimeHelper.UtcNow().AddDays(-1) - ZZTimeHelper.UtcNow();
}
public void Settle()
{
if (IsFinished())
{
OnSettleFinalEvent();
}
}
public bool UIRunning { get; set; } = false;
// private bool IsCanGetFinalReward()
// {
// // return OfferDiscoPartyData.LotteryNum >= OfferDiscoPartyData.Items.Count;
// // && !OfferDiscoPartyData.Finished;
// }
public void OnBuySuccess()
{
ELog("OnBuySuccess -> ");
OfferDiscoPartyData.RewardList.Add(OfferDiscoPartyData.ItemId);
OfferDiscoPartyData.LotteryNum += 1;
FinishLottery(OfferDiscoPartyData.ItemId);
SaveData();
EventTracking();
//NEXT TURN
if (UIRunning)
{
// 播放动画逻辑,交给UI
return;
}
// FinishLottery(OfferDiscoPartyData.ItemId);
UpdateForNextLottery();
Settle();
}
public void EventTracking()
{
var iapItemIdx = OfferDiscoPartyData.LotteryNum - 1 - FreeTime;
var price = GetIapItemListThisTime(iapItemIdx)?.PaymentAmount ?? 0F;
// var itemEnt = GetTItemById(OfferDiscoPartyData.ItemId);
// var rewardStr = string.Join(',', OfferDiscoPartyData.RewardList);
var number = OfferDiscoPartyData.LotteryNum;
#if AGG
using (var e = GEvent.TackEvent("offer_discoparty"))
{
e.AddContent("event_id", OfferDiscoPartyData.EventId);
e.AddContent("number",number);
e.AddContent("price", price);
e.AddContent("reward",OfferDiscoPartyData.ItemId);
}
#endif
ELog($"EventTracking: event_id: {OfferDiscoPartyData.EventId} number: {number} price: {price} " +
$"reward :{OfferDiscoPartyData.ItemId} ");
}
public IAPItemList GetIapItemListThisTime()
{
return GetIapItemListThisTime(OfferDiscoPartyData.LotteryNum - FreeTime);
}
private IAPItemList GetIapItemListThisTime(int index)
{
// index -= FreeTime;
// index = Mathf.Clamp(index, 0, OfferDiscoParty.IapList.Count - 1);
if (index < 0 || index > OfferDiscoParty.IapList.Count - 1)
return null;
var iapId = OfferDiscoParty.IapList[index];
var iAPItemList = _tables.TbIAPItemList.GetOrDefault(iapId);
return iAPItemList;
}
}
}