From 45c4461515fe3146a25f82b837e33f2ea08ae63a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 2 Jun 2026 09:41:38 -0400 Subject: [PATCH] fix(rank-battle): use real rm_ai_setting.csv ai_id values in BotRoster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 shipped placeholder ai_id values 4001..4008, which the client's RankMatchAISettingList.GetSettingData() couldn't resolve — the lookup is .First() against the rm_ai_setting.csv master table and throws InvalidOperationException ("Sequence contains no matching element") when the id isn't present. Surfaced on live smoke as a Unity error during battle load: Wizard.RankMatchAISettingDataSet.GetSettingData (System.Int32 enemyAiId) BattleUI+d__9.MoveNext () Replaced with the series-1 enemy_ai_id per class from data_dumps/client-assets/rm_ai_setting.csv: 1111=Forest, 1121=Sword, 1131=Rune, 1141=Dragon, 1151=Shadow, 1161=Blood, 1171=Haven, 1181=Portal Practice mode's AI catalog (practice_ai_setting.csv) uses a different schema keyed by (class_id, difficulty) with no enemy_ai_id field, so practice ids aren't reusable here. Co-Authored-By: Claude Opus 4.7 --- .../Matching/BotRoster.cs | 27 ++++++++++--------- SVSim.UnitTests/Matching/BotRosterTests.cs | 7 +++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Matching/BotRoster.cs b/SVSim.EmulatedEntrypoint/Matching/BotRoster.cs index 486f08e..9fa7ed0 100644 --- a/SVSim.EmulatedEntrypoint/Matching/BotRoster.cs +++ b/SVSim.EmulatedEntrypoint/Matching/BotRoster.cs @@ -3,10 +3,13 @@ using SVSim.BattleNode.Bridge; namespace SVSim.EmulatedEntrypoint.Matching; /// -/// Phase 3 hardcoded fixture: one stub bot per class (1..8). ai_id -/// values 4001..4008 are placeholders per the existing -/// docs/api-spec/endpoints/post-login/rank-battle/ai-start.md TODO — -/// no live capture exists to validate the real prod catalog. +/// Phase 3 hardcoded fixture: one stub bot per class (1..8). ai_id values are +/// the "series 1" tier from the client's rm_ai_setting.csv master table +/// (1111=Forest, 1121=Sword, … 1181=Portal). The client's +/// RankMatchAISettingList.GetSettingData(aiId) calls .First(...) against +/// this table and throws InvalidOperationException if the id is absent — so the +/// ids MUST match. See data_dumps/client-assets/rm_ai_setting.csv for the full +/// catalog (32 rows: classes 1-8 × tiers 1-2 × deck variants 1-2). /// public sealed class BotRoster : IBotRoster { @@ -16,28 +19,28 @@ public sealed class BotRoster : IBotRoster // sleeveId/emblemId/etc returned here. private static readonly IReadOnlyList Roster = new[] { - new AIBotProfile(AiId: 4001, CountryCode: "NONE", UserName: "Forestcraft AI", + new AIBotProfile(AiId: 1111, CountryCode: "NONE", UserName: "Forestcraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 1, CharaId: 1, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4002, CountryCode: "NONE", UserName: "Swordcraft AI", + new AIBotProfile(AiId: 1121, CountryCode: "NONE", UserName: "Swordcraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 2, CharaId: 2, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4003, CountryCode: "NONE", UserName: "Runecraft AI", + new AIBotProfile(AiId: 1131, CountryCode: "NONE", UserName: "Runecraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 3, CharaId: 3, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4004, CountryCode: "NONE", UserName: "Dragoncraft AI", + new AIBotProfile(AiId: 1141, CountryCode: "NONE", UserName: "Dragoncraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 4, CharaId: 4, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4005, CountryCode: "NONE", UserName: "Shadowcraft AI", + new AIBotProfile(AiId: 1151, CountryCode: "NONE", UserName: "Shadowcraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 5, CharaId: 5, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4006, CountryCode: "NONE", UserName: "Bloodcraft AI", + new AIBotProfile(AiId: 1161, CountryCode: "NONE", UserName: "Bloodcraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 6, CharaId: 6, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4007, CountryCode: "NONE", UserName: "Havencraft AI", + new AIBotProfile(AiId: 1171, CountryCode: "NONE", UserName: "Havencraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 7, CharaId: 7, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), - new AIBotProfile(AiId: 4008, CountryCode: "NONE", UserName: "Portalcraft AI", + new AIBotProfile(AiId: 1181, CountryCode: "NONE", UserName: "Portalcraft AI", SleeveId: 1001, EmblemId: 1, DegreeId: 1, FieldId: 1, IsOfficial: 0, ClassId: 8, CharaId: 8, Rank: 10, BattlePoint: 0, IsMasterRank: 0, MasterPoint: 0), }; diff --git a/SVSim.UnitTests/Matching/BotRosterTests.cs b/SVSim.UnitTests/Matching/BotRosterTests.cs index 7efee8d..9043595 100644 --- a/SVSim.UnitTests/Matching/BotRosterTests.cs +++ b/SVSim.UnitTests/Matching/BotRosterTests.cs @@ -14,12 +14,15 @@ public class BotRosterTests EmblemId: "0", DegreeId: "0", FieldId: 0, IsOfficial: 0, BattleType: 11); [Test] - public void Pick_returns_a_bot_with_valid_ai_id() + public void Pick_returns_a_bot_with_valid_ai_id_from_rm_ai_setting() { var roster = new BotRoster(); var bot = roster.Pick(Ctx("PlayerA", "1")); - Assert.That(bot.AiId, Is.InRange(4001, 4008)); + // Series-1 enemy_ai_id values from data_dumps/client-assets/rm_ai_setting.csv — + // one per class (1=Forest, 2=Sword, 3=Rune, 4=Dragon, 5=Shadow, 6=Blood, 7=Haven, 8=Portal). + // Must match a real row or the client's RankMatchAISettingList.GetSettingData() throws. + Assert.That(bot.AiId, Is.AnyOf(1111, 1121, 1131, 1141, 1151, 1161, 1171, 1181)); } [Test]