Files
the-cycle/src/Prospect.Server.Api/Services/UserData/UserDataService.cs
T

347 lines
14 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.Database;
namespace Prospect.Server.Api.Services.UserData;
public class UserTechTreeNodeData {
[JsonPropertyName("nodeInProgress")]
public string NodeInProgress { get; set; }
[JsonPropertyName("totalUpgrades")]
public int TotalUpgrades { get; set; }
[JsonPropertyName("nodes")]
public Dictionary<string, FYTechTreeNodeStatus> Nodes { get; set; }
}
public class CharacterTechTreeBonuses {
[JsonPropertyName("aurumCap")]
public int AurumCap { get; set; }
[JsonPropertyName("aurumRate")]
public float AurumRate { get; set; }
[JsonPropertyName("kmarksCap")]
public int KmarksCap { get; set; }
[JsonPropertyName("kmarksRate")]
public int KmarksRate { get; set; }
[JsonPropertyName("crateTier")]
public int CrateTier { get; set; }
[JsonPropertyName("addStashSize")]
public int AddStashSize { get; set; }
[JsonPropertyName("addSafePocketSize")]
public int AddSafePocketSize { get; set; }
[JsonPropertyName("upgradeSpeedFactor")]
public float UpgradeSpeedFactor { get; set; }
}
public class UserDataService
{
private readonly ILogger<UserDataService> _logger;
private readonly DbUserDataService _dbUserDataService;
private readonly TitleDataService _titleDataService;
public UserDataService(ILogger<UserDataService> logger, DbUserDataService dbUserDataService, TitleDataService titleDataService)
{
_logger = logger;
_dbUserDataService = dbUserDataService;
_titleDataService = titleDataService;
}
/// <summary>
/// Initialize data for the given PlayFabId.
/// </summary>
public async Task InitAsync(string playFabId)
{
// TODO: Do not call this on each InitAsync
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes", "Blueprints"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
UserTechTreeNodeData userTechTreeNodesData = new UserTechTreeNodeData {
NodeInProgress = "",
TotalUpgrades = 0,
Nodes = [],
};
foreach (var nodeData in techTreeNodes) {
userTechTreeNodesData.Nodes[nodeData.Key] = new FYTechTreeNodeStatus {
NodeID = nodeData.Key,
Level = 0,
UpgradeStartedTime = new CloudScript.Models.Data.FYTimestamp {
Seconds = 0,
}
};
}
var defaultVanity = new FYCharacterVanity {
UserID = playFabId,
ArchetypeID = "E01_G02",
SlotIndex = 0,
BaseSuitItem = new FYVanityMaterialItem {
ID = "StarterOutfit01M_BaseSuit",
MaterialIndex = 0,
},
HeadItem = new FYVanityMaterialItem {
ID = "E01_G02_Head01",
MaterialIndex = 0,
},
MeleeWeaponItem = new FYVanityMaterialItem {
ID = "Melee_Omega",
MaterialIndex = 0,
},
BodyType = 1,
};
#if SEASON_2_RELEASE || SEASON_2_DEBUG
defaultVanity.BootsItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Boots_M",
MaterialIndex = 0,
};
defaultVanity.ChestItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Chest_M",
MaterialIndex = 0,
};
defaultVanity.GloveItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Gloves_M",
MaterialIndex = 0,
};
#elif SEASON_3_RELEASE || SEASON_3_DEBUG
defaultVanity.BootsItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Boots",
MaterialIndex = 0,
};
defaultVanity.ChestItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Chest",
MaterialIndex = 0,
};
defaultVanity.GloveItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Gloves",
MaterialIndex = 0,
};
#else
#error Unsupported build type
#endif
// TODO: Proper objects.
var defaultData = new Dictionary<string, (bool isPublic, string value)>
{
// GET /Client/GetUserData & GET /Client/GetUserReadOnlyData
// TODO: Contracts and factions progression
["FactionProgressionKorolev"] = (false, "0"),
["FactionProgressionICA"] = (false, "0"),
["FactionProgressionOsiris"] = (false, "0"),
["ContractsActive"] = (false, $"{{\"userId\":\"{playFabId}\",\"error\":\"\",\"contracts\":[]}}"),
["ContractsOneTimeCompleted"] = (false, "{\"contractsIds\":[]}"),
["OnboardingProgression"] = (false, $"{{\"userId\":\"{playFabId}\",\"currentMissionID\":\"TalkToBadum\",\"progress\":0,\"showPopup\":true}}"),
// TODO: Notifications
["Notifications"] = (false, "{\"notifications\":[]}"), // FYNotificationDescription?
["Notifications_Account"] = (false, "{\"notifications\":[]}"), // FYNotificationDescription?
// TODO: Fortuna Pass Season 2
["FortunaPass2_DailyCompletedActions"] = (false, "{\"actions\":{},\"lastDailyCapResetTimeUtc\":{\"seconds\":0}}"),
["FortunaPass2_ClaimedRewards"] = (false, "{\"rewardsIds\":[]}"),
["FortunaPass2_PremiumUnlock"] = (false, "false"),
["FortunaPass2_SeasonXp"] = (false, "0"),
// TODO: Fortuna Pass Season 3
["FortunaPass3_DailyCompletedActions"] = (false, "{\"actions\":{},\"lastDailyCapResetTimeUtc\":{\"seconds\":0}}"),
["FortunaPass3_ClaimedRewards"] = (false, "{\"rewardsIds\":[]}"),
["FortunaPass3_PremiumUnlock"] = (false, "false"),
["FortunaPass3_SeasonXp"] = (false, "0"),
// TODO: Quarters
["TechTreeNodeData"] = (false, JsonSerializer.Serialize(userTechTreeNodesData)),
["CraftingTimer__2022_05_12"] = (false, "{\"itemId\":\"\",\"utcTimestampWhenCraftingStarted\":{\"seconds\":0}}"),
["Generators__2021_09_09"] = (false, "[{\"generatorId\":\"playerquarters_gen_aurum\",\"lastClaimTime\":{\"seconds\":0}},{\"generatorId\":\"playerquarters_gen_kmarks\",\"lastClaimTime\":{\"seconds\":0}},{\"generatorId\":\"playerquarters_gen_crate\",\"lastClaimTime\":{\"seconds\":0}}]"),
["ClaimableStarterPacks"] = (false, "{\"packages\":[]}"), // FYClaimableStarterPackPackageResult?
["ClaimableVictimCompensations"] = (false, "{\"packages\":[]}"), // FYBackendClaimableVictimCompensationWrapper?
["PlayerQuartersLevel"] = (false, "{\"level\":1,\"upgradeStartedTime\":{\"seconds\":0}}"),
["TwitchDropRewards"] = (false, "{\"grantedItems\":[]}"), // FYInventoryItem
["InsuranceInvoice"] = (false, "{}"),
["InsuranceClaims"] = (false, "{\"packages\":[]}"), // FYBackendInsurancePayoutPackage. Limited to 20.
// TODO: Player data
["LOADOUT"] = (false, $"{{\"shield\":\"\",\"helmet\":\"\",\"weaponOne\":\"\",\"weaponTwo\":\"\",\"bag\":\"\",\"bagItemsAsJsonStr\":\"\",\"safeItemsAsJsonStr\":\"\"}}"),
["SessionData"] = (false, "{}"),
["SeasonWipe"] = (false, "{}"),
["VeteranPoints"] = (false, "{}"),
["FreeLoadout"] = (false, "{}"), // Season 3
// Function-specific data
// Used to optimize bonuses calculation and further validations without getting the user data and computing bonuses in runtime.
["CharacterTechTreeBonuses"] = (false, "{\"aurumCap\":0,\"aurumRate\":0,\"kmarksCap\":0,\"kmarksRate\":0,\"crateTier\":0,\"stashSize\":0,\"safePocketSize\":0,\"upgradeSpeed\":1.0}"),
["GlobalVanity"] = (false, $"{{\"activeGlobalVanityIds\":[\"Season03_Spray_13\",\"Banner_MIne\",\"Emote_Chill_01\",\"\",\"\",\"\",\"\",\"\"], \"droppodId\": \"VDP_Omega02\"}}"),
["CharacterVanity"] = (false, JsonSerializer.Serialize(defaultVanity)),
["Inventory"] = (false, "[]"),
["JobBoardsData"] = (false, JsonSerializer.Serialize(
new JobBoardsData {
LastBoardRefreshTimeUtc = new CloudScript.Models.Data.FYTimestamp { Seconds = 0 },
Boards = [
new FYFactionContractsData {
FactionId = "Korolev",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
},
new FYFactionContractsData {
FactionId = "Osiris",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
},
new FYFactionContractsData {
FactionId = "ICA",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
}
]
}
)),
["VanityItems"] = (false, "[]"),
["RetentionBonus"] = (false, "{\"claimedAll\":false,\"daysClaimed\":0,\"lastClaimTime\":{\"seconds\":0}}"),
["Balance"] = (false, "{\"IN\": 0,\"AU\": 800,\"SC\": 30000}"),
};
foreach (var (key, (isPublic, value)) in defaultData)
{
if (!await _dbUserDataService.HasAsync(playFabId, key))
{
await _dbUserDataService.InsertAsync(playFabId, key, value, isPublic);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="currentUserId">
/// The authenticated user id.
/// </param>
/// <param name="requestUserId">
/// The requested user id.
/// </param>
/// <param name="keys"></param>
/// <returns></returns>
public async Task<Dictionary<string, FUserDataRecord>> FindAsync(
string currentUserId,
string requestUserId,
List<string> keys)
{
if (string.IsNullOrEmpty(currentUserId))
{
throw new ArgumentNullException(nameof(currentUserId));
}
if (requestUserId == null)
{
requestUserId = currentUserId;
}
var other = currentUserId != requestUserId;
var result = new Dictionary<string, FUserDataRecord>();
if (keys != null && keys.Count > 0)
{
foreach (var key in keys)
{
var data = await _dbUserDataService.FindAsync(requestUserId, key);
if (data == null)
{
// TODO: Error?
continue;
}
if (!data.Public && other)
{
// TODO: Error?
continue;
}
result.Add(data.Key, new FUserDataRecord
{
LastUpdated = data.LastUpdated,
Permission = data.Public ? UserDataPermission.Public : UserDataPermission.Private,
Value = data.Value
});
}
}
else
{
// TODO: Getting ALL user data seems to be required only for fetching the player's passive generators data.
// This is not OK and normally should not be allowed.
var cursor = await _dbUserDataService.AllAsync(requestUserId, other);
while (await cursor.MoveNextAsync())
{
foreach (var data in cursor.Current)
{
result.Add(data.Key, new FUserDataRecord
{
LastUpdated = data.LastUpdated,
Permission = data.Public ? UserDataPermission.Public : UserDataPermission.Private,
Value = data.Value
});
}
}
}
return result;
}
/// <summary>
///
/// </summary>
/// <param name="currentUserId">
/// The authenticated user id.
/// </param>
/// <param name="targetUserId">
/// The target user id.
/// </param>
/// <param name="changes"></param>
/// <returns></returns>
public async Task UpdateAsync(
string currentUserId,
string? targetUserId,
Dictionary<string, string> changes)
{
if (changes.Count == 0)
{
return;
}
if (string.IsNullOrEmpty(currentUserId))
{
throw new ArgumentNullException(nameof(currentUserId));
}
if (targetUserId == null)
{
targetUserId = currentUserId;
}
// Whether we are updating someone else.
var other = currentUserId != targetUserId;
foreach (var (key, value) in changes)
{
var data = await _dbUserDataService.FindAsync(currentUserId, key);
if (data == null)
{
if (other)
{
_logger.LogWarning("User {PlayFabId} attempted to update non-existing key {Key} of another user {PlayFabIdOther}", currentUserId, key, targetUserId);
}
else
{
_logger.LogDebug("User {PlayFabId} created key {Key}", currentUserId, key);
await _dbUserDataService.InsertAsync(currentUserId, key, value, false);
}
continue;
}
if (!data.Public && other)
{
_logger.LogWarning("User {PlayFabId} attempted to update non-public key {Key} of another user {PlayFabIdOther}", currentUserId, key, data.PlayFabId);
continue;
}
await _dbUserDataService.UpdateValueAsync(data.Id, value);
}
}
}