diff --git a/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs b/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs index 7bd08a8..3510c5c 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs @@ -5,6 +5,7 @@ using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ItemPurchase; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ItemPurchase; using SVSim.EmulatedEntrypoint.Services; @@ -31,7 +32,7 @@ public class ItemPurchaseController : SVSimController } [HttpPost("info")] - public async Task> Info() + public async Task> Info(BaseRequest _) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); diff --git a/SVSim.EmulatedEntrypoint/Controllers/LeaderSkinController.cs b/SVSim.EmulatedEntrypoint/Controllers/LeaderSkinController.cs index 927db7f..a85850a 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LeaderSkinController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LeaderSkinController.cs @@ -5,6 +5,7 @@ using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.LeaderSkin; @@ -76,7 +77,7 @@ public class LeaderSkinController : SVSimController } [HttpPost("ids")] - public async Task> Ids() + public async Task> Ids(BaseRequest _) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); @@ -90,7 +91,7 @@ public class LeaderSkinController : SVSimController } [HttpPost("products")] - public async Task>> Products() + public async Task>> Products(BaseRequest _) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); @@ -262,15 +263,24 @@ public class LeaderSkinController : SVSimController /// /// Computes the per-viewer rewards.status for a series: - /// 0=none — set_sales_status==0 (no set sale active) - /// 1=not_got — series completed by viewer but bonus unclaimed + /// 0=none — set_sales_status==0 OR no bonus items configured (matches prod, which ships + /// status=0 for series where items[] is empty even when set_sales_status==1) + /// 1=not_got — bonus exists, series completed by viewer, bonus unclaimed /// 2=got — viewer claimed the bonus - /// 1 (effectively "available later") when set sale active but viewer hasn't completed it. + /// 1 (effectively "available later") when set sale active with bonus and viewer hasn't + /// completed the series. /// The 1/2 distinction matches the client enum (RewardStatus.not_got vs .got). + /// + /// Important: emitting status=1 when items[] is empty triggers the client's + /// is_completed && not_got branch in SkinPurchaseInfoTask.CreateSetSaleInfo, + /// which marks the set sale as FREE and renders a useless "claim" button for a + /// nonexistent bonus. Always return 0 when there's nothing to claim. + /// /// private static int ComputeRewardStatus(LeaderSkinShopSeriesEntry series, bool seriesCompleted, bool claimed) { if (series.SetSalesStatus == 0) return 0; + if (series.SetCompletionRewards.Count == 0) return 0; if (claimed) return 2; if (seriesCompleted) return 1; return 1; diff --git a/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs b/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs index 0da5b50..c9d623e 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs @@ -5,6 +5,7 @@ using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Sleeve; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Sleeve; @@ -27,7 +28,7 @@ public class SleeveController : SVSimController } [HttpPost("info")] - public async Task> Info() + public async Task> Info(BaseRequest _) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); diff --git a/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs b/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs index ff85f78..584cbc7 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/SpotCardExchangeController.cs @@ -5,6 +5,7 @@ using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.SpotCardExchange; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.SpotCardExchange; @@ -38,7 +39,7 @@ public class SpotCardExchangeController : SVSimController } [HttpPost("top")] - public async Task> Top() + public async Task> Top(BaseRequest _) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); diff --git a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs index 2ffc0e6..b92cd4e 100644 --- a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs +++ b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs @@ -100,7 +100,19 @@ public class ShadowverseTranslationMiddleware : IMiddleware throw; } - Type requestType = endpointDescriptor.Parameters.FirstOrDefault().ParameterType; + var firstParam = endpointDescriptor.Parameters.FirstOrDefault(); + if (firstParam is null) + { + // Action method has no parameters — middleware can't bind the (encrypted+msgpacked) + // body to anything. The codebase convention is to take a BaseRequest even for body- + // less endpoints (see e.g. PuzzleController.Info(BaseRequest _)). Fail loud with a + // specific message rather than NREing below on .ParameterType. + throw new InvalidOperationException( + $"Action {endpointDescriptor.DisplayName} has no parameters; the SV translation " + + "middleware needs at least one to bind the decrypted body. Add a BaseRequest parameter " + + "(or a derived DTO) — see other *Info/*Top actions for the convention."); + } + Type requestType = firstParam.ParameterType; object? data; try {