From dd995c4b43ac5b61158ae973a162700b04a0abff Mon Sep 17 00:00:00 2001 From: michaelinfaith Date: Tue, 13 Oct 2026 15:07:11 -0400 Subject: [PATCH] Fall back to a starting loadout when the client omits start_class_id The client only sends start_class_id on the first pass through the character creator. Re-entering it from the customize screen omits the field, and RIN stored battleframe_sdb_id 0, which makes the zone selection paperdoll throw "Invalid item ID" and leaves Enter World unclickable. Co-Authored-By: Claude Fable 5.1 Co-Authored-By: Claude Opus 5.5 --- RIN.Core/DB/SDB/SDB.cs | 21 +++++++++++++++++++++ RIN.WebAPI/Controllers/ClientApiV1.cs | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/RIN.Core/DB/SDB/SDB.cs b/RIN.Core/DB/SDB/SDB.cs index cc64947..dab0e37 100644 --- a/RIN.Core/DB/SDB/SDB.cs +++ b/RIN.Core/DB/SDB/SDB.cs @@ -39,6 +39,27 @@ public SDB(IOptions config, ILogger logger) : base(co return result.FirstOrDefault(); } + // The client omits start_class_id when the player re-enters the creator from the customize screen. + // Returns the requested frame when it is a starting loadout, otherwise the first starting loadout + // (Accord Dreadnaught), so a character never ends up with battleframe_sdb_id 0, which breaks the + // zone selection paperdoll ("Invalid item ID") and leaves "Enter World" dead. + public async Task GetStartingBattleframeId(int requestedFrameId) + { + const string SELECT_SQL = @"SELECT frame_id FROM sdb.""dbcharacter::CharCreateLoadout"" + WHERE is_starting_loadout = 1 AND is_dev = 0 + ORDER BY CASE WHEN frame_id = @requestedFrameId THEN 0 ELSE 1 END, id + LIMIT 1;"; + + var result = await DBCall(conn => conn.QueryAsync(SELECT_SQL, new { requestedFrameId }), + exception => Logger.LogError($"Error looking up starting battleframe for requested frame {requestedFrameId} due to: {exception}")); + + var frameId = result.FirstOrDefault(); + if (frameId != requestedFrameId) + Logger.LogWarning("Requested start class {Requested} is not a starting loadout, using {Fallback}", requestedFrameId, frameId); + + return frameId; + } + public async Task> GetOrnamentsInfoList() { const string SELECT_SQL = @"SELECT ornaments.id, lang.english AS lang_name, display_flags, ""Usage"" AS ""usage"" diff --git a/RIN.WebAPI/Controllers/ClientApiV1.cs b/RIN.WebAPI/Controllers/ClientApiV1.cs index 459887f..e345458 100644 --- a/RIN.WebAPI/Controllers/ClientApiV1.cs +++ b/RIN.WebAPI/Controllers/ClientApiV1.cs @@ -158,6 +158,7 @@ public async Task Characters(CreateCharacterReq reqData) const byte DEFAULT_RACE = 0; var colors = await Sdb.GetNewCharactersColors(reqData.eye_color_id, reqData.skin_color_id, reqData.hair_color_id); + var startClass = await Sdb.GetStartingBattleframeId(reqData.start_class_id); var loginResult = await Db.GetLoginData(GetUid()); // temp var genderInt = CharacterUtil.GenderStrToNum(reqData.gender); @@ -168,7 +169,7 @@ public async Task Characters(CreateCharacterReq reqData) { var charId = await Db.CreateNewCharacter(loginResult.account_id, reqData.name, reqData.is_dev, reqData.voice_set, genderInt, visualBlob); var bfVisuals = PlayerBattleframeVisuals.CreateDefault(); - var bfId = await Db.CreateBattleframeLoadout(charId, reqData.start_class_id, bfVisuals); + var bfId = await Db.CreateBattleframeLoadout(charId, startClass, bfVisuals); await Db.SetCharacterCurrentBattleframe(charId, bfId.Value); tx.Complete();