feat(rank-battle): RankBattleController shell + DTOs + routing smoke tests

Stands up the controller with all 13 rank-battle URL routes wired via
explicit absolute [HttpPost] attributes (multi-prefix family — can't ride
[Route(\"[controller]\")]). Real DoMatching / AiStart logic arrives in
later tasks; finish + telemetry + force-finish are returnable stubs as
of this task.

DTOs cover the request + response shapes per the spec. Note the
camelCase wire keys on AiBattlePlayerInfo (sleeveId, emblemId, ...) —
the AI battle subsystem uses camelCase, not the project-default
snake_case, per AIBattleStartTask.Parse's literal Keys.Contains lookups.

DoMatchingResponseDto.NodeServerUrl is non-nullable + always-emit (with
[JsonIgnore(Never)]) — matches Phase 2's TK2 fix because the client's
DoMatchingBase parser calls .ToString() without a Keys.Contains guard.

13 routing smoke tests confirm each URL resolves to the controller.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-02 01:19:02 -04:00
parent a55187e10e
commit 7c4aa89d45
7 changed files with 410 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
using System.Text.Json.Serialization;
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
[MessagePackObject(keyAsPropertyName: true)]
public sealed class AiBattleStartResponseDto
{
[JsonPropertyName("ai_id")]
[Key("ai_id")]
public int AiId { get; set; }
[JsonPropertyName("turnState")]
[Key("turnState")]
public int TurnState { get; set; }
[JsonPropertyName("self_info")]
[Key("self_info")]
public AiBattlePlayerInfo SelfInfo { get; set; } = new();
[JsonPropertyName("oppo_info")]
[Key("oppo_info")]
public AiBattlePlayerInfo OppoInfo { get; set; } = new();
}
/// <summary>
/// Per docs/api-spec/endpoints/post-login/rank-battle/ai-start.md — the AI battle
/// subsystem uses camelCase keys (sleeveId, emblemId, ...), not the project-default
/// snake_case. The [JsonPropertyName] overrides bypass the global SnakeCaseLower
/// policy. country_code / self_info / oppo_info are the two outliers staying snake_case.
/// </summary>
[MessagePackObject(keyAsPropertyName: true)]
public sealed class AiBattlePlayerInfo
{
[JsonPropertyName("country_code")]
[Key("country_code")]
public string CountryCode { get; set; } = "NONE";
[JsonPropertyName("userName")]
[Key("userName")]
public string UserName { get; set; } = "NONE";
[JsonPropertyName("sleeveId")]
[Key("sleeveId")]
public int SleeveId { get; set; } = -1;
[JsonPropertyName("emblemId")]
[Key("emblemId")]
public int EmblemId { get; set; } = -1;
[JsonPropertyName("degreeId")]
[Key("degreeId")]
public int DegreeId { get; set; } = -1;
[JsonPropertyName("fieldId")]
[Key("fieldId")]
public int FieldId { get; set; } = -1;
[JsonPropertyName("isOfficial")]
[Key("isOfficial")]
public int IsOfficial { get; set; } = -1;
[JsonPropertyName("oppoId")]
[Key("oppoId")]
public int OppoId { get; set; } = -1;
[JsonPropertyName("seed")]
[Key("seed")]
public int Seed { get; set; } = -1;
[JsonPropertyName("rank")]
[Key("rank")]
public int Rank { get; set; } = -1;
[JsonPropertyName("battlePoint")]
[Key("battlePoint")]
public int BattlePoint { get; set; } = -1;
[JsonPropertyName("classId")]
[Key("classId")]
public int ClassId { get; set; } = -1;
[JsonPropertyName("charaId")]
[Key("charaId")]
public int CharaId { get; set; } = -1;
[JsonPropertyName("isMasterRank")]
[Key("isMasterRank")]
public int IsMasterRank { get; set; } = -1;
[JsonPropertyName("masterPoint")]
[Key("masterPoint")]
public int MasterPoint { get; set; } = -1;
}

View File

@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
[MessagePackObject(keyAsPropertyName: true)]
public sealed class DoMatchingRequestDto
{
[JsonPropertyName("deck_no")]
[Key("deck_no")]
public int DeckNo { get; set; }
[JsonPropertyName("need_init")]
[Key("need_init")]
public int NeedInit { get; set; }
[JsonPropertyName("card_master_hash")]
[Key("card_master_hash")]
public string? CardMasterHash { get; set; }
[JsonPropertyName("log")]
[Key("log")]
public string? Log { get; set; }
[JsonPropertyName("use_stage_select")]
[Key("use_stage_select")]
public int UseStageSelect { get; set; }
[JsonPropertyName("excluded_field_id_list")]
[Key("excluded_field_id_list")]
public int[]? ExcludedFieldIdList { get; set; }
[JsonPropertyName("is_default_skin")]
[Key("is_default_skin")]
public int IsDefaultSkin { get; set; }
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
[MessagePackObject(keyAsPropertyName: true)]
public sealed class DoMatchingResponseDto
{
[JsonPropertyName("matching_state")]
[Key("matching_state")]
public int MatchingState { get; set; }
[JsonPropertyName("timeout_period")]
[Key("timeout_period")]
public int TimeoutPeriod { get; set; } = 60;
[JsonPropertyName("retry_period")]
[Key("retry_period")]
public int RetryPeriod { get; set; } = 3;
[JsonPropertyName("battle_id")]
[Key("battle_id")]
public string? BattleId { get; set; }
// Always emitted, even on RETRY. Client's DoMatchingBase.SettingDoMatchingData()
// calls .ToString() on this without a Keys.Contains guard, so absence throws
// KeyNotFoundException before the matching_state switch runs. Same Phase 2 fix
// pattern as TK2.
[JsonPropertyName("node_server_url")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[Key("node_server_url")]
public string NodeServerUrl { get; set; } = "";
[JsonPropertyName("card_master_id")]
[Key("card_master_id")]
public int? CardMasterId { get; set; }
}

View File

@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
/// <summary>
/// Standard BattleFinishParam shape — see docs/api-spec/common/types.ts.md and
/// docs/api-spec/endpoints/post-login/rank-battle/finish.md. Future: promote to
/// a shared common DTO when a second finish endpoint reuses this.
/// </summary>
[MessagePackObject(keyAsPropertyName: true)]
public sealed class RankBattleFinishRequestDto
{
[JsonPropertyName("battle_result")]
[Key("battle_result")]
public int BattleResult { get; set; }
[JsonPropertyName("is_retire")]
[Key("is_retire")]
public int IsRetire { get; set; }
[JsonPropertyName("recovery_data")]
[Key("recovery_data")]
public string? RecoveryData { get; set; }
[JsonPropertyName("class_id")]
[Key("class_id")]
public int ClassId { get; set; }
[JsonPropertyName("total_turn")]
[Key("total_turn")]
public int TotalTurn { get; set; }
[JsonPropertyName("evolve_count")]
[Key("evolve_count")]
public int EvolveCount { get; set; }
[JsonPropertyName("enemy_evolve_count")]
[Key("enemy_evolve_count")]
public int EnemyEvolveCount { get; set; }
// RankBattleFinishTask extends BattleFinishParam with SDTRB.
[JsonPropertyName("sdtrb")]
[Key("sdtrb")]
public int Sdtrb { get; set; }
}

View File

@@ -0,0 +1,59 @@
using System.Text.Json.Serialization;
using MessagePack;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
/// <summary>
/// Stubbed Phase-3 rank-finish payload. Per RankBattleFinishTask.cs:57-63, the client
/// uses GetValueOrDefault(key, 0) for the seven primary scalars and Keys.Contains
/// for everything else — emitting zeros is safe. All-optional fields beyond these
/// eleven are deliberately omitted (no mission/treasure-box/battle-pass evaluation
/// in Phase 3 scope).
/// </summary>
[MessagePackObject(keyAsPropertyName: true)]
public sealed class RankBattleFinishResponseDto
{
[JsonPropertyName("rank")]
[Key("rank")]
public int Rank { get; set; }
[JsonPropertyName("after_battle_point")]
[Key("after_battle_point")]
public int AfterBattlePoint { get; set; }
[JsonPropertyName("after_master_point")]
[Key("after_master_point")]
public int AfterMasterPoint { get; set; }
[JsonPropertyName("battle_point")]
[Key("battle_point")]
public int BattlePoint { get; set; }
[JsonPropertyName("master_point")]
[Key("master_point")]
public int MasterPoint { get; set; }
[JsonPropertyName("successive_win_number")]
[Key("successive_win_number")]
public int SuccessiveWinNumber { get; set; }
[JsonPropertyName("successive_win_bonus")]
[Key("successive_win_bonus")]
public int SuccessiveWinBonus { get; set; }
[JsonPropertyName("battle_result")]
[Key("battle_result")]
public int BattleResult { get; set; }
[JsonPropertyName("get_class_experience")]
[Key("get_class_experience")]
public int GetClassExperience { get; set; }
[JsonPropertyName("class_experience")]
[Key("class_experience")]
public int ClassExperience { get; set; }
[JsonPropertyName("class_level")]
[Key("class_level")]
public int ClassLevel { get; set; } = 1;
}