Audit fixes: Fortuna pass level, rename persist, loadout & craft-timer #6

Merged
neckfire merged 1 commits from preprod into main 2026-07-15 00:57:11 +00:00
9 changed files with 80 additions and 29 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -274,8 +274,12 @@ public class ClientController : Controller
[HttpPost("UpdateUserTitleDisplayName")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult UpdateUserTitleDisplayName(FUpdateUserTitleDisplayNameRequest request)
public async Task<IActionResult> UpdateUserTitleDisplayName(FUpdateUserTitleDisplayNameRequest request)
{
// Persist the rename, otherwise it is echoed back but lost on next login.
var userId = User.FindAuthUserId();
await _userService.UpdateDisplayNameAsync(userId, request.DisplayName);
return Ok(new ClientResponse<FUpdateUserTitleDisplayNameResult>
{
Code = 200,
@@ -81,9 +81,16 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
}
var factionKey = "FactionProgression" + contract.Faction;
// Fortuna pass season XP is stored per season; the client derives the pass level
// from this key. No challenge system is emulated, so contracts are the XP source.
#if SEASON_3_RELEASE || SEASON_3_DEBUG
const string seasonXpKey = "FortunaPass3_SeasonXp";
#else
const string seasonXpKey = "FortunaPass2_SeasonXp";
#endif
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey, "JobBoardsData" }
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey, "JobBoardsData", seasonXpKey }
);
var factionProgression = JsonSerializer.Deserialize<int>(userData[factionKey].Value);
@@ -270,6 +277,12 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
newContracts.Add(newContract);
}
// Grant Fortuna pass season XP for completing this contract and persist it, so the
// pass level (derived client-side from FortunaPass{season}_SeasonXp) actually moves.
// No exact rate ships in the data, so scale it to the contract's reputation value.
var seasonXp = int.TryParse(userData[seasonXpKey].Value, out var currentSeasonXp) ? currentSeasonXp : 0;
seasonXp += contract.ReputationIncrease * 10;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
@@ -279,6 +292,7 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
["Inventory"] = JsonSerializer.Serialize(newInventory),
[factionKey] = JsonSerializer.Serialize(factionProgression),
["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData),
[seasonXpKey] = JsonSerializer.Serialize(seasonXp),
}
);
@@ -297,7 +311,7 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
CurrentProgression = factionProgression,
},
Status = 18, // EYClaimContractRewardsStatus::OK
UpdatedSeasonXp = 0 // TODO: Probably a separate season XP rate configured by server?
UpdatedSeasonXp = seasonXp // new season XP total (client derives the pass level from it)
};
}
}
@@ -1,6 +1,8 @@
using Prospect.Server.Api.Services.Auth.Extensions;
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -8,13 +10,15 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInProgressDataRequest, FYGetCraftingInProgressDataResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetCraftingInProgressData(IHttpContextAccessor httpContextAccessor)
public GetCraftingInProgressData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public Task<FYGetCraftingInProgressDataResult> ExecuteAsync(FYGetCraftingInProgressDataRequest request)
public async Task<FYGetCraftingInProgressDataResult> ExecuteAsync(FYGetCraftingInProgressDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
@@ -22,11 +26,20 @@ public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInPro
throw new CloudScriptException("CloudScript was not called within a http request");
}
return Task.FromResult(new FYGetCraftingInProgressDataResult
var userId = context.User.FindAuthUserId();
// Return the persisted in-progress craft (written by StartItemCraftingClient)
// instead of an empty stub, so the crafting timer survives a menu reopen / relog.
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "CraftingTimer__2022_05_12" });
// Fully qualified: a different FYItemCurrentlyBeingCrafted also exists in this namespace.
var craft = JsonSerializer.Deserialize<Models.Data.FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value)
?? new Models.Data.FYItemCurrentlyBeingCrafted();
return new FYGetCraftingInProgressDataResult
{
UserId = context.User.FindAuthUserId(),
UserId = userId,
Error = string.Empty,
ItemCurrentlyBeingCrafted = {},
});
ItemCurrentlyBeingCrafted = craft,
};
}
}
@@ -1,5 +1,7 @@
using Prospect.Server.Api.Services.Auth.Extensions;
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -7,21 +9,31 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class GetPlayerSets : ICloudScriptFunction<FYGetPlayersSets, object?>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetPlayerSets(IHttpContextAccessor httpContextAccessor)
public GetPlayerSets(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public Task<object?> ExecuteAsync(FYGetPlayersSets request)
public async Task<object?> ExecuteAsync(FYGetPlayersSets request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return Task.FromResult<object?>(null);
return null;
}
return Task.FromResult<object?>(new
var userId = context.User.FindAuthUserId();
// Return the player's persisted loadout (written by CompleteInventoryUpdate /
// RequestUpdateStationInventory) instead of a blank set, so saved gear survives
// a menu reopen / relog. Same response shape as before, just populated.
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "LOADOUT" });
var loadout = JsonSerializer.Deserialize<LoadoutData>(userData["LOADOUT"].Value) ?? new LoadoutData();
return new
{
success = true,
entries = new[]
@@ -31,19 +43,19 @@ public class GetPlayerSets : ICloudScriptFunction<FYGetPlayersSets, object?>
setData = new
{
id = "",
userId = context.User.FindAuthUserId(),
userId,
kit = "",
shield = "",
helmet = "",
weaponOne = "",
weaponTwo = "",
bag = "",
bagItemsAsJsonStr = "",
safeItemsAsJsonStr = ""
shield = loadout.Shield ?? "",
helmet = loadout.Helmet ?? "",
weaponOne = loadout.WeaponOne ?? "",
weaponTwo = loadout.WeaponTwo ?? "",
bag = loadout.Bag ?? "",
bagItemsAsJsonStr = loadout.BagItemsAsJsonStr ?? "",
safeItemsAsJsonStr = loadout.SafeItemsAsJsonStr ?? ""
},
items = Array.Empty<object>()
}
}
});
};
}
}
@@ -24,6 +24,14 @@ public class DbUserService : BaseDbService<PlayFabUser>
return await Collection.Find(user => user.Id == id).FirstOrDefaultAsync();
}
// Persist a display-name change (UpdateUserTitleDisplayName). Without this the rename
// is echoed back to the client but lost on next login.
public async Task UpdateDisplayNameAsync(string id, string displayName)
{
var update = Builders<PlayFabUser>.Update.Set(user => user.DisplayName, displayName);
await Collection.UpdateOneAsync(user => user.Id == id, update);
}
private async Task<PlayFabUser> CreateAsync(PlayFabUserAuthType type, string key)
{
var user = new PlayFabUser