mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
211 lines
10 KiB
C#
211 lines
10 KiB
C#
using System.Data;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Storefront;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.RecNet.Progression.Challenges;
|
|
|
|
namespace RadicadeServer.Utils;
|
|
|
|
public static class WeeklyChallenges
|
|
{
|
|
public const int Required = 3;
|
|
private static readonly DateTime Epoch = new(2026, 10, 7, 0, 0, 0, DateTimeKind.Utc);
|
|
private static readonly SemaphoreSlim[] Gates = Enumerable.Range(0, 128).Select(_ => new SemaphoreSlim(1)).ToArray();
|
|
|
|
|
|
|
|
public static WeeklyChallengeMap CreateMap(DateTime now)
|
|
{
|
|
var week = (int)Math.Floor((now.ToUniversalTime() - Epoch).TotalDays / 7);
|
|
var start = Epoch.AddDays(week * 7);
|
|
return new WeeklyChallengeMap
|
|
{
|
|
ChallengeMapId = 100000 + week,
|
|
StartAt = start, EndAt = start.AddDays(7), ServerTime = now,
|
|
Enabled = false
|
|
};
|
|
}
|
|
|
|
public static WeeklyChallengeMap MergeProgress(WeeklyChallengeMap definition, WeeklyChallengeState? state)
|
|
{
|
|
var map = JsonSerializer.Deserialize<WeeklyChallengeMap>(JsonSerializer.Serialize(definition))!;
|
|
if (state == null) return map;
|
|
var previous = JsonSerializer.Deserialize<WeeklyChallengeMap>(state.MapJson)!;
|
|
foreach (var goal in map.Challenges)
|
|
{
|
|
var saved = previous.Challenges.FirstOrDefault(x => x.ChallengeId == goal.ChallengeId);
|
|
if (saved != null && TryMergeConfig(goal.Config, saved.Config, saved.Complete, out var config))
|
|
{
|
|
goal.Config = config;
|
|
goal.Complete = saved.Complete;
|
|
}
|
|
}
|
|
map.CompletedRequired = state.RewardGiftDropId.HasValue || map.Challenges.Count(x => x.Complete) >= Required;
|
|
return map;
|
|
}
|
|
|
|
|
|
|
|
public static bool TryMergeConfig(string original, string submitted, bool complete, out string merged)
|
|
{
|
|
merged = original;
|
|
if (submitted.Length > 16384) return false;
|
|
try
|
|
{
|
|
var expected = JsonNode.Parse(original)!.AsObject();
|
|
var actual = JsonNode.Parse(submitted, documentOptions: new JsonDocumentOptions { MaxDepth = 16 })!.AsObject();
|
|
if (!ValidateNode(expected, actual) || (actual["c"]?.GetValue<bool>() ?? false) != complete) return false;
|
|
merged = actual.ToJsonString();
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException or NullReferenceException)
|
|
{ return false; }
|
|
}
|
|
|
|
private static bool ValidateNode(JsonObject expected, JsonObject actual)
|
|
{
|
|
foreach (var p in actual)
|
|
{
|
|
if (p.Key == "c") { _ = p.Value!.GetValue<bool>(); continue; }
|
|
if (p.Key == "ipc") { _ = p.Value!.GetValue<bool>(); continue; }
|
|
if (p.Key is "in" or "ex")
|
|
{
|
|
if (p.Value?.GetValue<bool>() != true) return false;
|
|
continue;
|
|
}
|
|
if (p.Key == "wc" && p.Value is JsonArray { Count: 0 }) continue;
|
|
if (!expected.ContainsKey(p.Key)) return false;
|
|
}
|
|
foreach (var p in expected)
|
|
{
|
|
if (p.Key is "c" or "ipc" or "in" or "ex" or "wc") continue;
|
|
if (p.Key == "rc")
|
|
{
|
|
if (p.Value is not JsonArray left || actual[p.Key] is not JsonArray right || left.Count != right.Count) return false;
|
|
for (var i = 0; i < left.Count; i++)
|
|
if (!ValidateNode(left[i]!.AsObject(), right[i]!.AsObject())) return false;
|
|
}
|
|
else if (!JsonNode.DeepEquals(p.Value, actual[p.Key])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static async Task<WeeklyChallengeMap> GetAsync(RecDBContext db, long playerId, DateTime now)
|
|
{
|
|
var current = await GetDefinitionAsync(db, now);
|
|
var state = await db.WeeklyChallenges.AsNoTracking().SingleOrDefaultAsync(x => x.PlayerId == playerId && x.ChallengeMapId == current.ChallengeMapId);
|
|
var map = MergeProgress(current, state);
|
|
if (!map.Enabled) { map.Challenges.Clear(); map.Gift = new(); }
|
|
map.ServerTime = now;
|
|
return map;
|
|
}
|
|
|
|
public static string ScheduleKey(int mapId) => $"WeeklyChallengeMap:{mapId}";
|
|
|
|
public static async Task<WeeklyChallengeMap> GetDefinitionAsync(RecDBContext db, DateTime now)
|
|
{
|
|
var defaults = CreateMap(now);
|
|
var key = ScheduleKey(defaults.ChallengeMapId);
|
|
var scheduled = await db.ServerConfig.AsNoTracking().SingleOrDefaultAsync(x => x.Key == key);
|
|
var map = scheduled == null ? defaults : JsonSerializer.Deserialize<WeeklyChallengeMap>(scheduled.Value)!;
|
|
map.ServerTime = now;
|
|
return map;
|
|
}
|
|
|
|
public static bool IsSupportedDefinition(string config)
|
|
{
|
|
try
|
|
{
|
|
var node = JsonNode.Parse(config)!.AsObject();
|
|
if (node["ct"]?.GetValue<int>() != 0 || node["rc"] is not JsonArray { Count: 1 } rc) return false;
|
|
var predicate = rc[0]!.AsObject();
|
|
if (predicate["ct"]?.GetValue<int>() != 6 || predicate["vs"] is not JsonArray { Count: 1 } values) return false;
|
|
var eventId = values[0]!.GetValue<int>();
|
|
if (eventId is not (2 or 4 or 5 or 8 or 10)) return false;
|
|
var canonical = JsonSerializer.Serialize(new { ct = 0, rc = new[] { new { ct = 6, vs = new[] { eventId } } } });
|
|
return TryMergeConfig(canonical, config, false, out _)
|
|
&& !(predicate["c"]?.GetValue<bool>() ?? false);
|
|
}
|
|
catch (Exception ex) when (ex is JsonException or InvalidOperationException or FormatException or NullReferenceException)
|
|
{ return false; }
|
|
}
|
|
|
|
public static async Task<string?> UpdateAsync(RecDBContext db, long playerId, int mapId, int challengeId,
|
|
string config, bool complete, DateTime now)
|
|
{
|
|
var current = await GetDefinitionAsync(db, now);
|
|
if (mapId != current.ChallengeMapId) return "stale_challenge_map";
|
|
var gate = Gates[(int)((ulong)playerId % (uint)Gates.Length)];
|
|
await gate.WaitAsync();
|
|
Gift? notification = null;
|
|
try
|
|
{
|
|
var error = await db.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
|
|
{
|
|
db.ChangeTracker.Clear();
|
|
notification = null;
|
|
await using var tx = db.Database.IsSqlServer()
|
|
? await db.Database.BeginTransactionAsync(IsolationLevel.Serializable) : null;
|
|
|
|
var scheduleKey = ScheduleKey(mapId);
|
|
var scheduleQuery = db.Database.IsSqlServer()
|
|
? db.ServerConfig.FromSqlInterpolated($"SELECT * FROM [ServerConfig] WITH (UPDLOCK, HOLDLOCK) WHERE [Key] = {scheduleKey}")
|
|
: db.ServerConfig.Where(x => x.Key == scheduleKey);
|
|
var schedule = await scheduleQuery.AsNoTracking().SingleOrDefaultAsync();
|
|
if (schedule == null) return "weekly_not_published";
|
|
current = JsonSerializer.Deserialize<WeeklyChallengeMap>(schedule.Value)!;
|
|
if (!current.Enabled) return "weekly_not_published";
|
|
var query = db.Database.IsSqlServer()
|
|
? db.WeeklyChallenges.FromSqlInterpolated($"SELECT * FROM [WeeklyChallenges] WITH (UPDLOCK, HOLDLOCK) WHERE [PlayerId] = {playerId} AND [ChallengeMapId] = {mapId}")
|
|
: db.WeeklyChallenges.Where(x => x.PlayerId == playerId && x.ChallengeMapId == mapId);
|
|
var state = await query.SingleOrDefaultAsync();
|
|
var map = MergeProgress(current, state);
|
|
var challenge = map.Challenges.SingleOrDefault(x => x.ChallengeId == challengeId);
|
|
if (challenge == null) return "unknown_challenge";
|
|
if (!TryMergeConfig(challenge.Config, config, complete, out var merged)) return "invalid_challenge_config";
|
|
if (!challenge.Complete)
|
|
{
|
|
challenge.Config = merged;
|
|
challenge.Complete = complete;
|
|
}
|
|
if (state == null)
|
|
{
|
|
state = new WeeklyChallengeState { PlayerId = playerId, ChallengeMapId = mapId };
|
|
db.WeeklyChallenges.Add(state);
|
|
}
|
|
map.CompletedRequired = state.RewardGiftDropId.HasValue || map.Challenges.Count(x => x.Complete) >= Required;
|
|
state.MapJson = JsonSerializer.Serialize(map);
|
|
if (map.CompletedRequired && !state.RewardGiftDropId.HasValue)
|
|
{
|
|
var gift = map.Gift.Clone();
|
|
var unlocks = await db.Unlocks.SingleOrDefaultAsync(x => x.AccountId == playerId);
|
|
var pending = await db.Gifts.SingleOrDefaultAsync(x => x.AccountId == playerId);
|
|
var pendingGifts = StorefrontShared.ParseGiftList(pending?.Data);
|
|
var owned = !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)
|
|
? unlocks?.GetAvatarItems().Any(x => LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc) == LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc)) == true
|
|
|| pendingGifts.Any(x => !string.IsNullOrWhiteSpace(x.AvatarItemDesc) && LevelUpRewardPool.OwnershipKey(x.AvatarItemDesc) == LevelUpRewardPool.OwnershipKey(gift.AvatarItemDesc))
|
|
: unlocks?.GetEquipment()?.Any(x => x.PrefabName == gift.EquipmentPrefabName && x.ModificationGuid == gift.EquipmentModificationGuid) == true
|
|
|| pendingGifts.Any(x => x.EquipmentPrefabName == gift.EquipmentPrefabName && x.EquipmentModificationGuid == gift.EquipmentModificationGuid);
|
|
if (owned) gift = RroRewardPools.TokenGift(GiftContext.All_Weekly_Challenge_Complete, map.FallbackTokens, "Weekly Challenge Complete!");
|
|
gift.GiftDropId = StorefrontShared.NewGiftDropId();
|
|
state.RewardGiftDropId = gift.GiftDropId;
|
|
await StorefrontShared.PersistGiftBoxAsync(db, playerId, gift, notify: false);
|
|
notification = gift;
|
|
}
|
|
else await db.SaveChangesAsync();
|
|
if (tx != null) await tx.CommitAsync();
|
|
return (string?)null;
|
|
});
|
|
if (error == null && notification != null)
|
|
await StorefrontShared.SendGiftPackageNotificationAsync(playerId, notification);
|
|
return error;
|
|
}
|
|
finally { gate.Release(); }
|
|
}
|
|
}
|