fix(audit): Fortuna pass level, display-name persist, loadout & craft-timer queries
Build & Deploy / build (push) Successful in 39s
Build & Deploy / build (push) Successful in 39s
Audit of the emulator surface + 4 fixes:
- Fortuna pass level was frozen: nothing ever granted season XP. ClaimActiveContract
now grants + persists FortunaPass{2,3}_SeasonXp (scaled to the contract's reputation)
and returns the new total, so the pass level moves as you complete contracts.
- UpdateUserTitleDisplayName never persisted the rename (echoed back, lost on relog).
Added DbUserService.UpdateDisplayNameAsync and call it.
- GetPlayerSets returned a blank loadout; now reads the persisted LOADOUT key.
- GetCraftingInProgressData was a stub; now returns the persisted CraftingTimer so an
in-progress craft + remaining time survive a menu reopen/relog.
Remaining known gaps documented in Plane (need client data / multiplayer / out of scope).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
+20
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user