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+<WaitForSetUp>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 <noreply@anthropic.com>
59 lines
3.6 KiB
C#
59 lines
3.6 KiB
C#
using SVSim.BattleNode.Bridge;
|
||
|
||
namespace SVSim.EmulatedEntrypoint.Matching;
|
||
|
||
/// <summary>
|
||
/// Phase 3 hardcoded fixture: one stub bot per class (1..8). <c>ai_id</c> values are
|
||
/// the "series 1" tier from the client's <c>rm_ai_setting.csv</c> master table
|
||
/// (<c>1111</c>=Forest, <c>1121</c>=Sword, … <c>1181</c>=Portal). The client's
|
||
/// <c>RankMatchAISettingList.GetSettingData(aiId)</c> calls <c>.First(...)</c> against
|
||
/// this table and throws <c>InvalidOperationException</c> if the id is absent — so the
|
||
/// ids MUST match. See <c>data_dumps/client-assets/rm_ai_setting.csv</c> for the full
|
||
/// catalog (32 rows: classes 1-8 × tiers 1-2 × deck variants 1-2).
|
||
/// </summary>
|
||
public sealed class BotRoster : IBotRoster
|
||
{
|
||
// Cosmetic ids (sleeve / emblem / degree / field) intentionally use safe
|
||
// default values that match the master tables shipped in the project.
|
||
// The client-side AI catalog reads ai_id but renders cosmetics from the
|
||
// sleeveId/emblemId/etc returned here.
|
||
private static readonly IReadOnlyList<AIBotProfile> Roster = new[]
|
||
{
|
||
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: 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: 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: 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: 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: 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: 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: 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),
|
||
};
|
||
|
||
public AIBotProfile Pick(MatchContext selfCtx)
|
||
{
|
||
// Deterministic: hash the ctx and pick from the roster. Same ctx →
|
||
// same bot so a mid-flight retry of /ai_<fmt>/start returns the same
|
||
// opponent (no fresh roster pick on each call).
|
||
var hash = StringComparer.Ordinal.GetHashCode(selfCtx.UserName)
|
||
^ StringComparer.Ordinal.GetHashCode(selfCtx.ClassId);
|
||
var index = (int)((uint)hash % Roster.Count);
|
||
return Roster[index];
|
||
}
|
||
}
|