Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions RIN.Core/DB/SDB/SDB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ public SDB(IOptions<DbConnectionSettings> config, ILogger<SDB> 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.
Comment on lines +42 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation doesn't quite match what the 1962 CharacterCreation.lua does. Going back from Customize to Select keeps g_currentFrame, OnCancel never touches it.

The field goes missing when g_currentFrame is nil, because BuildCreateHTTPRequest then just drops the key. Only OnCloseCC() resets it, which runs after a successful create, on Back from the Select screen and when the random name request fails. OnCloseCC() however doesn't call ClearCharacterSelection(), so the Next button stays visible. When you open the creator a second time in the same session, you can click Next without picking a frame and end up in Customize with no frame at all.

Can you adjust the comment, commit message and PR description to that? I'd also only keep the comment to the why, the paperdoll and "Enter World" symptoms are better placed in the commit message. Claude loves to write books in code comments. 😅

public async Task<int> 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<int>(SELECT_SQL, new { requestedFrameId }),
exception => Logger.LogError($"Error looking up starting battleframe for requested frame {requestedFrameId} due to: {exception}"));

var frameId = result.FirstOrDefault();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the lookup comes back empty (e.g. SDB not imported), this returns 0, logs "using 0" and we store battleframe_sdb_id 0 again, so the exact bug this PR fixes, just quieter. And if the query throws, DBCall returns default, result is null and FirstOrDefault() throws an ArgumentNullException.

I'd rather fail loudly here and throw when no starting loadout is found, than create a broken character.

if (frameId != requestedFrameId)
Logger.LogWarning("Requested start class {Requested} is not a starting loadout, using {Fallback}", requestedFrameId, frameId);
Comment on lines +57 to +58

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the case this PR is about, this logs "Requested start class 0 is not a starting loadout", which is a bit misleading. Maybe log requestedFrameId == 0 separately as "client didn't send start_class_id" and keep this message for actual invalid ids?


return frameId;
}

public async Task<IEnumerable<CosmeticInfo>> GetOrnamentsInfoList()
{
const string SELECT_SQL = @"SELECT ornaments.id, lang.english AS lang_name, display_flags, ""Usage"" AS ""usage""
Expand Down
3 changes: 2 additions & 1 deletion RIN.WebAPI/Controllers/ClientApiV1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public async Task<object> 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);
Expand All @@ -168,7 +169,7 @@ public async Task<object> 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();
Expand Down
Loading