215 lines
14 KiB
C#
215 lines
14 KiB
C#
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Campus_live_rewrite.api.server
|
|
{
|
|
/// <summary>
|
|
/// The daily challenges (the watch's Challenges > Daily tab): which three objectives are served
|
|
/// in GET /api/config/v2 "DailyObjectives", when the day rolls over, and what finishing all of
|
|
/// them pays - a game request (the three-card reward picker), once per day.
|
|
///
|
|
/// The set used to be types 20/21/22, which are the ONBOARDING objectives
|
|
/// (OOBE_GoToLockerRoom / OOBE_GoToActivity / OOBE_FinishActivity - the 2025 client's enum has
|
|
/// the same order as the reference table, read from its metadata). The client only fires those
|
|
/// during the tutorial, so no one's challenges ever progressed: the live log has 3,000+
|
|
/// myprogress reads and not one updateobjective. The defaults below are the everyday versions.
|
|
///
|
|
/// Edited from the admin panel (Challenges tab), stored in Data/APIS/daily-objectives.json:
|
|
/// { "rewardEnabled": true, "xpPerObjective": 25, "sets": [[{"type":30,"score":1}, ...], ...] }
|
|
/// A bare array (one set, or one set per day) is still read. Type ids are the ObjectiveType enum
|
|
/// (reference .agents/skills/daily-objectives-config/SKILL.md). Players pick a change up on
|
|
/// their next login (config/v2 is read then).
|
|
///
|
|
/// Weekly challenges: NOT in this client build. It has no /api/challenge route at all (its API
|
|
/// path table has objectives/v1/* and nothing for challenges; the live log has never seen one),
|
|
/// only leftover enum names. api/server/WeeklyChallenge.cs serves a route nothing calls.
|
|
/// </summary>
|
|
public static class DailyChallenges
|
|
{
|
|
// 30 GoToRecCenter, 32 VisitACustomRoom, 31 FinishActivity.
|
|
private static readonly int[][] DefaultSet = { new[] { 30, 1 }, new[] { 32, 1 }, new[] { 31, 1 } };
|
|
|
|
private static readonly string File1 =
|
|
System.IO.Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "daily-objectives.json");
|
|
|
|
private static readonly object _lock = new();
|
|
private static List<List<Classes.DailyObjective>>? _sets;
|
|
|
|
/// <summary>The config/v2 "DailyObjectives" value: seven day-sets (the client indexes by day).</summary>
|
|
public static List<List<Classes.DailyObjective>> Sets { get { lock (_lock) return _sets ??= Load(); } }
|
|
|
|
/// <summary>"file" when Data/APIS/daily-objectives.json supplied the sets, else "default".</summary>
|
|
public static string Source { get; private set; } = "default";
|
|
|
|
/// <summary>Finishing every challenge sends a game request. Off = XP per objective only.</summary>
|
|
public static bool RewardEnabled { get; private set; } = true;
|
|
|
|
/// <summary>How many objectives a group has to finish before its reward is paid.</summary>
|
|
public static int RequiredFor(long group)
|
|
{
|
|
var s = Sets;
|
|
if (s.Count == 0) return int.MaxValue;
|
|
var set = group >= 0 && group < s.Count ? s[(int)group] : s[0];
|
|
return set.Count == 0 ? int.MaxValue : set.Count;
|
|
}
|
|
|
|
/// <summary>The start of the current challenge day (UTC midnight). Progress from before it is stale.</summary>
|
|
public static DateTime PeriodStartUtc => DateTime.UtcNow.Date;
|
|
|
|
/// <summary>True when a group last reset (ClearedAt) before today - its progress belongs to an earlier day.</summary>
|
|
public static bool IsStale(string? clearedAt) =>
|
|
!DateTime.TryParse(clearedAt, null, System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal, out var t)
|
|
|| t < PeriodStartUtc;
|
|
|
|
/// <summary>
|
|
/// Pay the daily reward: a game request (three token cards in the tokens box) pushed to the
|
|
/// player's watch. The caller has already latched the group complete for today, so this runs
|
|
/// once per player per day.
|
|
/// </summary>
|
|
public static void GrantReward(ulong accountId, long group)
|
|
{
|
|
if (!RewardEnabled)
|
|
{
|
|
Console.WriteLine($"[objectives] {accountId} finished every daily challenge (group {group}) - game request is switched off (admin panel)");
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
var sel = QuestRewards.Loaded
|
|
? RewardSelections.Offer(accountId, QuestRewards.GiftContextGameRewardTokens, null, "Daily Challenges complete!")
|
|
: null;
|
|
Console.WriteLine(sel != null
|
|
? $"[objectives] {accountId} finished every daily challenge (group {group}) -> game request {sel.RewardSelectionId}"
|
|
: $"[objectives] {accountId} finished every daily challenge (group {group}) but no game request could be made (reward table not loaded)");
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"[objectives] {accountId} daily reward failed: {ex.Message}"); }
|
|
}
|
|
|
|
/// <summary>Re-read the file (after the admin panel saved it or reset it).</summary>
|
|
public static void Reload() { lock (_lock) _sets = Load(); }
|
|
|
|
/// <summary>Store new settings and apply them. Sets are validated by the caller.</summary>
|
|
public static void Save(List<List<Classes.DailyObjective>> sets, bool rewardEnabled, int xpPerObjective)
|
|
{
|
|
var o = new JObject
|
|
{
|
|
["rewardEnabled"] = rewardEnabled,
|
|
["xpPerObjective"] = xpPerObjective,
|
|
["sets"] = new JArray(sets.Select(set => new JArray(set.Select(d => new JObject { ["type"] = d.Type, ["score"] = d.Score }))))
|
|
};
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(File1)!);
|
|
System.IO.File.WriteAllText(File1, o.ToString());
|
|
Reload();
|
|
}
|
|
|
|
/// <summary>Back to the built-in defaults (deletes the file).</summary>
|
|
public static void ResetToDefaults()
|
|
{
|
|
try { if (System.IO.File.Exists(File1)) System.IO.File.Delete(File1); } catch { }
|
|
ConfigCampus.ObjectiveCompletionXp = DefaultXp;
|
|
Reload();
|
|
}
|
|
|
|
private static readonly int DefaultXp = ConfigCampus.ObjectiveCompletionXp;
|
|
|
|
private static List<List<Classes.DailyObjective>> Load()
|
|
{
|
|
List<List<Classes.DailyObjective>>? days = null;
|
|
Source = "default";
|
|
RewardEnabled = true;
|
|
try
|
|
{
|
|
if (System.IO.File.Exists(File1))
|
|
{
|
|
var tok = JToken.Parse(System.IO.File.ReadAllText(File1));
|
|
JArray? arr = tok as JArray;
|
|
if (tok is JObject obj)
|
|
{
|
|
if (obj.TryGetValue("rewardEnabled", StringComparison.OrdinalIgnoreCase, out var re) && re.Type == JTokenType.Boolean)
|
|
RewardEnabled = re.Value<bool>();
|
|
if (obj.TryGetValue("xpPerObjective", StringComparison.OrdinalIgnoreCase, out var xp) && xp.Type == JTokenType.Integer)
|
|
ConfigCampus.ObjectiveCompletionXp = Math.Clamp(xp.Value<int>(), 0, 100000);
|
|
arr = obj.GetValue("sets", StringComparison.OrdinalIgnoreCase) as JArray;
|
|
}
|
|
if (arr != null && arr.Count > 0)
|
|
{
|
|
days = arr[0] is JArray
|
|
? arr.OfType<JArray>().Select(ParseSet).Where(x => x.Count > 0).ToList()
|
|
: new List<List<Classes.DailyObjective>> { ParseSet(arr) };
|
|
if (days.Count == 0) days = null;
|
|
}
|
|
if (days != null) Source = "file";
|
|
Console.WriteLine(days != null
|
|
? $"[objectives] daily challenges from Data/APIS/daily-objectives.json ({days.Count} set(s), reward {(RewardEnabled ? "on" : "off")}, {ConfigCampus.ObjectiveCompletionXp} XP each)"
|
|
: "[objectives] Data/APIS/daily-objectives.json has no usable sets - using the defaults");
|
|
}
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"[objectives] could not read daily-objectives.json ({ex.Message}) - using the defaults"); days = null; }
|
|
|
|
days ??= new List<List<Classes.DailyObjective>>
|
|
{
|
|
DefaultSet.Select(d => new Classes.DailyObjective { Type = d[0], Score = d[1] }).ToList()
|
|
};
|
|
foreach (var set in days) foreach (var d in set) d.Xp = ConfigCampus.ObjectiveCompletionXp;
|
|
|
|
// Seven entries, one per weekday, like the reference config; a shorter list repeats.
|
|
var week = new List<List<Classes.DailyObjective>>();
|
|
for (int i = 0; i < 7; i++) week.Add(days[i % days.Count]);
|
|
return week;
|
|
}
|
|
|
|
private static List<Classes.DailyObjective> ParseSet(JArray set) => set.OfType<JObject>()
|
|
.Select(o => new Classes.DailyObjective
|
|
{
|
|
Type = (int?)(o.GetValue("type", StringComparison.OrdinalIgnoreCase)) ?? 0,
|
|
Score = Math.Clamp((int?)(o.GetValue("score", StringComparison.OrdinalIgnoreCase)) ?? 1, 1, 1000)
|
|
})
|
|
.Where(o => Types.ContainsKey(o.Type))
|
|
.ToList();
|
|
|
|
// ---- the ObjectiveType enum (2025 client: same members and order as the reference table) ----
|
|
|
|
/// <summary>Tutorial-only objectives: the client fires them during the new-player tutorial, never after.</summary>
|
|
public static readonly HashSet<int> TutorialOnly = new() { 20, 21, 22, 25, 26 };
|
|
|
|
/// <summary>Every usable objective type, id -> name. The rollups (10-15) and Default (-1) are left out.</summary>
|
|
public static readonly SortedDictionary<int, string> Types = new()
|
|
{
|
|
[1] = "FirstSessionOfDay", [2] = "AddAFriend", [3] = "PartyUp", [5] = "LevelUp", [6] = "CheerAPlayer",
|
|
[7] = "PointedAtPlayer", [8] = "CheerARoom", [9] = "SubscribeToPlayer",
|
|
[20] = "OOBE_GoToLockerRoom", [21] = "OOBE_GoToActivity", [22] = "OOBE_FinishActivity",
|
|
[25] = "NUX_PunchcardObjective", [26] = "NUX_AllPunchcardObjectives",
|
|
[30] = "GoToRecCenter", [31] = "FinishActivity", [32] = "VisitACustomRoom", [33] = "CreateACustomRoom",
|
|
[35] = "ScoreBasketInRecCenter", [36] = "UploadPhotoToRecNet", [37] = "UpdatePlayerBio", [38] = "SaveOutfitSlot",
|
|
[39] = "PurchaseClothingItem", [40] = "PurchaseNonClothingItem", [41] = "DrinkWater", [42] = "ColorOnWhiteboard",
|
|
[43] = "SetBasketballSkin", [44] = "ThrowBasketball", [45] = "PlaceInventionInDorm", [46] = "ChangeDormRoomSkin",
|
|
[47] = "ToggleOwnedClothes", [48] = "EquipHat", [49] = "LoadOutfit", [50] = "SaveNewOutfitSlot", [51] = "SpawnCamera",
|
|
[52] = "TakeSelfie", [53] = "PrintSelfie", [54] = "TakePictureOfPlayer", [55] = "PrintPictureOfPlayer",
|
|
[56] = "PublishSelfieWithPlayer", [57] = "SpawnFoodWithOtherPlayers", [58] = "EmoteInRecCenter",
|
|
[59] = "SendRoomChatInRecCenter", [60] = "UseFrendotron", [61] = "GoToDormRoom", [62] = "VisitSpecificRoom",
|
|
[63] = "VisitPublicRRO", [64] = "VisitPublicRoomBySource", [65] = "FavoriteARoom", [66] = "TakePhotoWithFilter",
|
|
[67] = "OpenYourPlayerProfile", [68] = "OpenOnlineStatusModal", [69] = "ChangeProfilePicture",
|
|
[70] = "ChangePlayerDisplayName", [71] = "ChangePlayerDescriptionText", [72] = "OpenPlayerPronounsModal",
|
|
[73] = "OpenOtherPlayersProfile", [74] = "VisitPlayersPortfolio", [75] = "FavoriteAFriend",
|
|
[100] = "CharadesGames", [101] = "CharadesWinsPerformer", [102] = "CharadesWinsGuesser",
|
|
[200] = "DiscGolfWins", [201] = "DiscGolfGames", [202] = "DiscGolfHolesUnderPar",
|
|
[300] = "DodgeballWins", [301] = "DodgeballGames", [302] = "DodgeballHits",
|
|
[400] = "PaddleballGames", [401] = "PaddleballWins", [402] = "PaddleballScores",
|
|
[500] = "PaintballAnyModeGames", [501] = "PaintballAnyModeWins", [502] = "PaintballAnyModeHits",
|
|
[600] = "PaintballCTFWins", [601] = "PaintballCTFGames", [602] = "PaintballCTFHits", [603] = "PaintballFlagCaptures",
|
|
[700] = "PaintballTeamBattleWins", [701] = "PaintballTeamBattleGames", [702] = "PaintballTeamBattleHits",
|
|
[710] = "PaintballFreeForAllWins", [711] = "PaintballFreeForAllGames", [712] = "PaintballFreeForAllHits",
|
|
[800] = "SoccerWins", [801] = "SoccerGames", [802] = "SoccerGoals",
|
|
[900] = "BowlingGames", [901] = "BowlingWins", [902] = "BowlingStrike",
|
|
[1000] = "QuestGames", [1001] = "QuestWins", [1002] = "QuestPlayerRevives", [1003] = "QuestEnemyKills",
|
|
[1010] = "QuestGames_Goblin1", [1011] = "QuestWins_Goblin1", [1012] = "QuestPlayerRevives_Goblin1", [1013] = "QuestEnemyKills_Goblin1",
|
|
[1020] = "QuestGames_Goblin2", [1021] = "QuestWins_Goblin2", [1022] = "QuestPlayerRevives_Goblin2", [1023] = "QuestEnemyKills_Goblin2",
|
|
[1030] = "QuestGames_Scifi1", [1031] = "QuestWins_Scifi1", [1032] = "QuestPlayerRevives_Scifi1", [1033] = "QuestEnemyKills_Scifi1",
|
|
[1040] = "QuestGames_Pirate1", [1041] = "QuestWins_Pirate1", [1042] = "QuestPlayerRevives_Pirate1", [1043] = "QuestEnemyKills_Pirate1",
|
|
[1050] = "QuestGames_Dracula1", [1051] = "QuestWins_Dracula1", [1052] = "QuestPlayerRevives_Dracula1", [1053] = "QuestEnemyKills_Dracula1",
|
|
[2000] = "ArenaGames", [2001] = "ArenaWins", [2002] = "ArenaPlayerRevives", [2003] = "ArenaHeroTags", [2004] = "ArenaBotTags",
|
|
[3000] = "RecRoyaleGames", [3001] = "RecRoyaleWins", [3002] = "RecRoyaleTags",
|
|
[4000] = "StuntRunnerGames", [4001] = "StuntRunnerWins", [5000] = "RecRallyGames", [5001] = "RecRallyWins",
|
|
};
|
|
}
|
|
}
|