274 lines
18 KiB
C#
274 lines
18 KiB
C#
#if UNITY_EDITOR
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using asap.core;
|
|
using cfg;
|
|
using GameCore;
|
|
using Newtonsoft.Json;
|
|
using UnityEngine;
|
|
|
|
namespace game.PlayerDataStorage
|
|
{
|
|
internal static partial class PlayerKVGameplayLoop
|
|
{
|
|
internal sealed class FishingRewardPlan
|
|
{
|
|
internal FishingData Data;
|
|
internal int Fish, Item, Area, Rod, Map, Mag;
|
|
internal string Activities;
|
|
internal string MapKey => "Map_5_"+Area;
|
|
}
|
|
internal sealed class FishingRewardAction
|
|
{
|
|
internal readonly FishingAction Consumption;
|
|
internal readonly object Controller;
|
|
internal readonly FishingRewardPlan Plan;
|
|
internal readonly List<WriteRecord> Records=new List<WriteRecord>();
|
|
internal readonly HashSet<string> Segments=new HashSet<string>(StringComparer.Ordinal);
|
|
internal object Panel;
|
|
internal WriteBatch Segment;
|
|
internal string SegmentName;
|
|
internal bool Ending,Applying,Sealed,Confirmed,Presented,Cancelled;
|
|
internal int GuideNotifications,BeforeFishCount;
|
|
internal FishingRewardAction(FishingAction consumption,object controller,FishingRewardPlan plan)
|
|
{ Consumption=consumption; Controller=controller; Plan=plan; }
|
|
}
|
|
static FishingRewardPlan rewardPlan;
|
|
static FishingRewardAction rewardAction;
|
|
static readonly ConditionType[] rewardConditions={ConditionType.ConsumeEnergy,ConditionType.PerfectStrikeCount,
|
|
ConditionType.GetFishCount,ConditionType.CashFromFishing,ConditionType.MaxCashFromFishing,
|
|
ConditionType.GetFishWeight,ConditionType.GetFishID,ConditionType.GetBoosFish,ConditionType.GetBiggerThanHugeFishCount};
|
|
static readonly HashSet<string> rewardTaskKeys=new HashSet<string>(StringComparer.Ordinal)
|
|
{"BattlePassRecordTask","MiniBPKeyNew","EventLuckGameRecord","EventLuckMagicPlayfabData"};
|
|
|
|
static string ActivityStamp(Tables tables)
|
|
{
|
|
var manager=GContext.container.Resolve<Activity.ActivityManager>();
|
|
var scopes=GContext.container.Resolve<Activity.ActivityScopeManager>();
|
|
return string.Join(";",tables.TbFishingEvent.DataList.GroupBy(e=>(e.Type,e.SubType)).Select(g=>g.First())
|
|
.Select(e=>e.Type+":"+e.SubType+":"+manager.GetSentry(e)?.CurrentEventConfig?.ID+":"+scopes.HasActiveScope((e.Type,e.SubType))));
|
|
}
|
|
static readonly HashSet<string> specialRewardGuides=new HashSet<string>(StringComparer.Ordinal)
|
|
{"FishSkill01","Pollution01","FishSkill02","FishSkill03","FishSkill04","Egyptian01","EasterFish01"};
|
|
internal static void CheckRewardGuide(FishingData data=null)
|
|
{
|
|
var guide=GContext.container.Resolve<GuideDataCenter>();
|
|
if(guide.curDefine!=null || guide.curGroupDefine!=null || guide.guidanceGroupDefines==null ||
|
|
guide.guidanceGroupDefines.Any(g=>g==null || g.guidanceDefines==null || g.guidanceDefines.Any(n=>n==null)))
|
|
throw Stop("Reward requires inactive and complete guide definitions.");
|
|
var groups=guide.guidanceGroupDefines.Where(g=>g.guidanceDefines.Any(n=>n.panelName=="FishingPanel" ||
|
|
n.panelName=="FishingPanel_Advanced" || n.panelName=="FishingRewardPanel"))
|
|
.OrderBy(g=>g.groupName.ToString(),StringComparer.Ordinal).ToArray();
|
|
var asset=Path.Combine(Application.dataPath,"ABPackage/pkgPrepare/Export/GuidanceConfig.asset");
|
|
var baseline=File.ReadAllBytes(Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVFishingRewardTests/guide-baseline-projection.jsonl")));
|
|
if(PlayerKVLoopPermit.Hash(File.ReadAllBytes(asset))!="5926124E5633166CC1B91569B8A71AB2C388AA3446A736B3E7E42B3EBD9F3D3A" ||
|
|
PlayerKVLoopPermit.Hash(baseline)!="A3D056313FE0F40D395FE92F97BEDE31BA47D8D79A3C4C28781395A43118E89C" || groups.Length!=11 ||
|
|
string.Join("\n",groups.Select(g=>JsonUtility.ToJson(g)))!=System.Text.Encoding.UTF8.GetString(baseline))
|
|
throw Stop("Reward guide configuration differs from the reviewed full-node projection.");
|
|
foreach(var g in groups)
|
|
if(!guide.GetIsFinish(g.groupName.ToString()) && (!specialRewardGuides.Contains(g.groupName.ToString()) ||
|
|
g.TriggerConditionsType!=GuideTriggerConditions.None || g.guidanceDefines.Length==0 || g.guidanceDefines[0].panelName!="FishingPanel_Advanced"))
|
|
throw Stop("Reward requires completed ordinary fishing guides.");
|
|
data=data??rewardPlan?.Data;
|
|
if(data?.curFishData?.FishSKillList==null || data.curFishData.FishEscape==null || data.curFishData.FishSKillList.Any(x=>x==null) ||
|
|
data.fishSpSkillData!=null || data.superDashSpSkillData!=null || data.dirtyStuckSpSkillData!=null)
|
|
throw Stop("Reward requires an ordinary fish without active special skills.");
|
|
var tables=GContext.container.Resolve<Tables>();
|
|
var queried=data.curFishData.FishSKillList.Concat(new[]{data.curFishData.FishEscape}).Select(x=>x.SpSkillID).Distinct().OrderBy(x=>x).ToArray();
|
|
bool invalid=false;
|
|
foreach(var id in queried) { var resolved=tables.TbFishSpSkill.GetOrDefault(id); if(id>0 || resolved!=null)invalid=true; }
|
|
Audit("fishing-reward-special-skill-check",new{queriedIds=queried,invalid});
|
|
if(invalid) throw Stop("Reward selected fish has a special skill; no consumption is allowed.");
|
|
}
|
|
static string EventSignature(FishingEvent e) => JsonConvert.SerializeObject(new{e.ID,e.Type,e.SubType,e.RedirectID,
|
|
e.IsABTest,e.Channel,e.ConditionRelation,e.PopupPriority,e.PopupType,
|
|
conditions=e.ConditionList.Select(c=>new{c.Condition,c.Param}),time=e.TimeDefinition.ToString()});
|
|
static void CheckRewardConfig(Tables tables)
|
|
{
|
|
var path=Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData/tbfishingevent.json"));
|
|
var bytes=File.ReadAllBytes(path);
|
|
if(PlayerKVLoopPermit.Hash(bytes)!="C8A4A9794E9390C94B9C8F1927B3E718B3EB9F784F0889508E1E576DD4B77A2D")
|
|
throw Stop("Reward event configuration differs from the reviewed baseline.");
|
|
var baseline=new TbFishingEvent(SimpleJSON.JSON.Parse(System.Text.Encoding.UTF8.GetString(bytes)));
|
|
var relevant=tables.TbFishingEvent.DataList.Where(e=>e.ConditionList.Any(c=>rewardConditions.Contains(c.Condition))).ToArray();
|
|
if(relevant.Length!=19 || tables.TbTriggerPackManager.DataList.Any(t=>rewardConditions.Contains(t.Condition)))
|
|
throw Stop("Reward activates an unreviewed event or trigger pack.");
|
|
foreach(var e in relevant)
|
|
{
|
|
var old=baseline.GetOrDefault(e.ID);
|
|
if(old==null || EventSignature(old)!=EventSignature(e)) throw Stop("Reward event fields changed.");
|
|
if(e.ID==10100000 || e.ID==10200000 || e.ID==11300000) continue;
|
|
if(e.ID<30200350 || e.ID>30200365 || !(e.TimeDefinition is LimitedTime time) ||
|
|
!DateTime.TryParse(time.EndTime,CultureInfo.InvariantCulture,DateTimeStyles.None,out var end) ||
|
|
ZZTimeHelper.UtcNow() < end.AddSeconds(120)) throw Stop("Reward event must be outside its reviewed activity window.");
|
|
}
|
|
}
|
|
static void CheckRewardEnvironment(FishingData data,bool checkConfig)
|
|
{
|
|
var player=GContext.container.Resolve<PlayerData>();
|
|
var fish=GContext.container.Resolve<PlayerFishData>();
|
|
var events=GContext.container.Resolve<FishingEventData>();
|
|
var challenge=GContext.container.Resolve<FishingChallengeManager>();
|
|
if(data==null || data.curFishData==null || data.fishItem==null || !data.IsGeneralFish() || data.IsFree ||
|
|
data.IsRetry || data.IsDuel || data.fixedFishList!=null || data.IsPackItemFish() || data.IsEventGameFish())
|
|
throw Stop("Reward supports one ordinary paid fish only.");
|
|
var tables=GContext.container.Resolve<Tables>();
|
|
if(tables.TbEventSlapSpinach.TryGetByItemID(data.fishItem.ID,out _)) throw Stop("Slap reward is outside this run.");
|
|
var cruise=fish.GetFishSwimToMap(data.MapId);
|
|
if((cruise!=-1 && cruise!=0) || events.collectingTargetInit!=null ||
|
|
GContext.container.Resolve<CollectingData>().GetAddCollecting()!=0 ||
|
|
GContext.container.Resolve<PiggyBankPackData>().IsPackActivated || GContext.container.Resolve<BargainPackData>().IsPackActivated)
|
|
throw Stop("Additional collection, cruise or pack rewards require separate review.");
|
|
if(challenge.fishingChallengeData!=null && challenge.ChallengeState()!=0 && challenge.ChallengeState()!=3 && challenge.IsTimeAllow())
|
|
throw Stop("Active challenge rewards require separate review.");
|
|
if(player._fishingFrequency<3 || !player.IsOpenMagnification || !fish.IsOpenPiercing)
|
|
throw Stop("Reward requires already unlocked basic fishing.");
|
|
CheckRewardGuide(data);
|
|
if(checkConfig) CheckRewardConfig(tables);
|
|
}
|
|
internal static void PrepareFishingReward(FishingAction action,FishingData data)
|
|
{
|
|
if(Permit?.AllowsReward!=true) return;
|
|
if(rewardPlan!=null || !FishingCurrent(action)) throw Stop("Reward preflight already used or stale.");
|
|
CheckRewardEnvironment(data,true);
|
|
var p=GContext.container.Resolve<PlayerData>();
|
|
var plan=new FishingRewardPlan{Data=data,Fish=data.curFishData.ID,Item=data.fishItem.ID,Area=data.curFishData.AreaID,
|
|
Rod=data.RodId,Map=p.lastMapId,Mag=p.magnification,Activities=ActivityStamp(GContext.container.Resolve<Tables>())};
|
|
if(!Permit.AllowedKeys.Contains(plan.MapKey) || Read(plan.MapKey)==null) throw Stop("Reward requires an existing backed-up fish map key.");
|
|
rewardPlan=plan;
|
|
Audit("fishing-reward-preflight",new{plan.Fish,plan.Item,plan.Area,plan.Rod,plan.Map,plan.Mag});
|
|
}
|
|
static void CheckRewardContext()
|
|
{
|
|
var p=rewardPlan; var player=GContext.container.Resolve<PlayerData>();
|
|
if(p==null || p.Data.curFishData==null || p.Data.fishItem==null || p.Data.curFishData.ID!=p.Fish || p.Data.fishItem.ID!=p.Item ||
|
|
p.Data.curFishData.AreaID!=p.Area || p.Data.RodId!=p.Rod || player.lastMapId!=p.Map || player.magnification!=p.Mag)
|
|
throw Stop("Reward fishing context changed.");
|
|
CheckRewardEnvironment(p.Data,false);
|
|
if(p.Activities!=ActivityStamp(GContext.container.Resolve<Tables>())) throw Stop("Activity changed during fishing reward.");
|
|
}
|
|
internal static bool BeginRewardDrawing(object controller)
|
|
{
|
|
if(Permit?.AllowsReward!=true || !FishingCurrent(fishingAction) || !fishingAction.Confirmed || !fishingAction.Continued)
|
|
throw Stop("A confirmed v3 fishing action is required before drawing.");
|
|
if(rewardAction!=null) { Audit("duplicate-suppressed",new{point="draw"}); return false; }
|
|
CheckRewardContext();
|
|
rewardAction=new FishingRewardAction(fishingAction,controller,rewardPlan);
|
|
return true;
|
|
}
|
|
internal static bool BeginRewardEnding(object controller,bool success)
|
|
{
|
|
var a=rewardAction;
|
|
if(a==null || !ReferenceEquals(controller,a.Controller) || !RewardCurrent(a)) throw Stop("Reward ending is stale.");
|
|
if(a.Ending) { Audit("duplicate-suppressed",new{point="fish-end"}); return false; }
|
|
a.Ending=true;
|
|
if(!success) throw Stop("Fishing failed; reward run stops without retry or refund.");
|
|
CheckRewardContext(); return true;
|
|
}
|
|
internal static FishingRewardAction BindRewardPanel(object panel)
|
|
{
|
|
var a=rewardAction;
|
|
if(!RewardCurrent(a) || !a.Ending) throw Stop("Reward panel has no completed catch.");
|
|
if(a.Applying) { Audit("duplicate-suppressed",new{point="reward-panel"}); return null; }
|
|
CheckRewardContext();
|
|
a.Applying=true; a.Panel=panel;
|
|
var fish=GContext.container.Resolve<PlayerFishData>();
|
|
var map=JsonConvert.DeserializeObject<Dictionary<int,string>>(Read(a.Plan.MapKey));
|
|
a.BeforeFishCount=map.TryGetValue(a.Plan.Fish,out var old)?FishWeightData.Deserialize(old).TotalNum:0;
|
|
return a;
|
|
}
|
|
internal static bool RewardCurrent(FishingRewardAction a) => a!=null && ReferenceEquals(a,rewardAction) &&
|
|
!a.Cancelled && FishingCurrent(a.Consumption);
|
|
sealed class RewardSegment : IDisposable
|
|
{
|
|
readonly FishingRewardAction action;
|
|
internal RewardSegment(FishingRewardAction a) { action=a; }
|
|
public void Dispose()
|
|
{
|
|
if(action.Segment==null) return;
|
|
action.Segment.Dispose(); action.Records.AddRange(action.Segment.Records);
|
|
Audit("fishing-reward-segment",new{segment=action.SegmentName,writes=action.Segment.Records.Count});
|
|
action.Segment=null;action.SegmentName=null;
|
|
}
|
|
}
|
|
internal static IDisposable BeginRewardSegment(string name)
|
|
{
|
|
var a=rewardAction;
|
|
if(!RewardCurrent(a) || a.Sealed || collecting!=null || a.Segment!=null ||
|
|
(name!="strike" && name!="ending" && name!="reward") || !a.Segments.Add(name)) throw Stop("Reward segment is not admissible.");
|
|
if((name=="ending" && !a.Ending) || (name=="reward" && !a.Applying)) throw Stop("Reward segment order is invalid.");
|
|
a.SegmentName=name; a.Segment=collecting=new WriteBatch(a.Consumption.Owner);
|
|
return new RewardSegment(a);
|
|
}
|
|
internal static void CheckRewardWrite(string key,string value)
|
|
{
|
|
var a=rewardAction;
|
|
if(Permit?.AllowsReward!=true || a==null || a.Cancelled || a.Confirmed) return;
|
|
if(!RewardCurrent(a)) throw Stop("Reward write belongs to a stale action.");
|
|
if(a.Segment==null)
|
|
{
|
|
Audit("fishing-reward-outside-segment",new{key});
|
|
if(!string.Equals(Read(key),value,StringComparison.Ordinal)) throw Stop("Value changed outside a fishing reward segment: "+key);
|
|
}
|
|
else if(key!="NewPlayerDataDic" && key!=a.Plan.MapKey && !rewardTaskKeys.Contains(key))
|
|
throw Stop("Unexpected fishing reward key: "+key);
|
|
}
|
|
internal static void RecordRewardGuideNotification()
|
|
{
|
|
var a=rewardAction;
|
|
if(!RewardCurrent(a) || a.SegmentName!="reward" || ++a.GuideNotifications!=1) throw Stop("Duplicate reward guide notification.");
|
|
CheckRewardGuide(); Audit("fishing-reward-guide",new{notifications=a.GuideNotifications,activated=0});
|
|
}
|
|
internal static async Task ConfirmRewardAsync(FishingRewardAction a,PlayerKVEditResult primary)
|
|
{
|
|
try
|
|
{
|
|
if(!RewardCurrent(a) || a.Segment!=null || collecting!=null || a.Sealed || a.Segments.Count!=3 || a.GuideNotifications!=1)
|
|
throw Stop("Reward records must be complete and sealed once.");
|
|
a.Sealed=true;
|
|
var last=a.Records.GroupBy(r=>r.Key,StringComparer.Ordinal).ToDictionary(g=>g.Key,g=>g.Last(),StringComparer.Ordinal);
|
|
if(!last.TryGetValue("NewPlayerDataDic",out var main) || !ReferenceEquals(main.Edit,primary) ||
|
|
!last.ContainsKey(a.Plan.MapKey) || last.Values.Any(r=>!r.Remote || r.Edit.Ticket==null))
|
|
throw Stop("Reward final writes require the original map/main and remote tickets.");
|
|
using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
|
{
|
|
var owner=a.Consumption.Owner;
|
|
foreach(var r in a.Records)
|
|
{
|
|
var t=r.Edit.Ticket;if(t==null)continue;
|
|
var final=last[r.Key].Edit.Ticket;
|
|
if(t.OwnerInstanceId!=owner.OwnerInstanceId || t.Epoch!=owner.Epoch || t.Key!=r.Key ||
|
|
final.OwnerInstanceId!=t.OwnerInstanceId || final.Epoch!=t.Epoch) throw Stop("Reward ticket identity mismatch.");
|
|
var result=await KVMgr.WaitForKVTicketAsync(owner,t,timeout.Token);
|
|
if(!RewardCurrent(a)) throw Stop("Reward action changed while waiting.");
|
|
bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(t,final) && final.Sequence>t.Sequence;
|
|
if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Reward ticket: "+result.Status);
|
|
}
|
|
}
|
|
a.Confirmed=true;
|
|
Audit("fishing-reward-confirmed",new{tickets=last.Values.Select(r=>new{key=r.Key,sequence=r.Edit.Ticket.Sequence,valueHash=r.ValueHash})});
|
|
}
|
|
catch(Exception e) { Fault(e.Message);throw; }
|
|
}
|
|
internal static void PresentReward(FishingRewardAction a,Action show)
|
|
{
|
|
if(!RewardCurrent(a) || !a.Confirmed) throw Stop("Reward cannot be presented before confirmation.");
|
|
if(a.Presented)return;a.Presented=true;show();Audit("fishing-reward-presented",new{a.Plan.Fish});
|
|
}
|
|
internal static void CancelReward(object actor,string reason)
|
|
{
|
|
var a=rewardAction;
|
|
if(a==null || a.Cancelled || (!ReferenceEquals(actor,a.Controller) && !ReferenceEquals(actor,a.Panel) && !ReferenceEquals(actor,a.Consumption.Actor)))return;
|
|
if(a.Ending && !a.Confirmed && ReferenceEquals(actor,a.Controller))return;
|
|
a.Cancelled=true;
|
|
if(Ready && !a.Confirmed) Fault("Fishing reward cancelled: "+reason);
|
|
}
|
|
}
|
|
}
|
|
#endif
|