acdream/docs/research/2026-07-13-retail-skill-training-confirmation-pseudocode.md
Erik 85980fe13f fix(ui): restore retail skill training confirmation
Route untrained-skill promotion through DialogFactory with retail's exact prompt and stored skill/credit properties. Defer the character-sheet refresh until accept or reject completes while leaving trained-skill XP raises immediate.

Co-Authored-By: Codex <codex@openai.com>
2026-07-13 17:57:29 +02:00

2.6 KiB

Retail skill-training confirmation

Date: 2026-07-13

Scope

This note covers the confirmation shown when an untrained skill is promoted to trained by spending skill credits. It does not apply when experience is added to an already trained or specialized skill; those raises are sent immediately.

Named retail sources

  • gmSkillUI::TrainSkillDialogCallback @ 0x0049C210
  • gmSkillUI::TrainSkill @ 0x0049C5F0
  • gmSkillUI::RaiseSelection @ 0x0049C8C0
  • DialogFactory::MakeCallbackDialogInCurrentUI @ 0x00478430
  • literal data_7A7864: Are you sure you want to spend %d credits to train %s?

The dialog property keys are shared with the existing DialogFactory port:

0x8E       dialog type (1 = ConfirmationDialog)
0x92       confirmation result written by ConfirmationDialog
0xC5       message StringInfo
0x10000040 selected skill id
0x10000041 skill-credit cost

Pseudocode

gmSkillUI.TrainSkill(playerQualities):
    if selectedSkill == 0:
        return false

    skillName = SkillSystem.InqSkillName(selectedSkill)
    creditCost = GetCostToRaise(playerQualities)
    message = format(
        "Are you sure you want to spend %d credits to train %s?",
        creditCost,
        skillName)

    data = PropertyCollection()
    data[0x8E] = 1                    // ConfirmationDialog
    data[0xC5] = message
    data[0x10000040] = selectedSkill
    data[0x10000041] = creditCost
    DialogFactory.MakeCallbackDialogInCurrentUI(
        data,
        gmSkillUI.TrainSkillDialogCallback)
    return true

gmSkillUI.TrainSkillDialogCallback(data):
    if data.bool(0x92) is not true:
        return

    skillId = data.uint(0x10000040)
    creditCost = data.uint(0x10000041)
    if skillId != 0 and creditCost != 0:
        CM_Train.Event_TrainSkillAdvancementClass(skillId, creditCost)

gmSkillUI::RaiseSelection branches on advancement class. Untrained skills enter TrainSkill; trained/specialized skills call CM_Train::Event_TrainSkill directly with the XP cost. Therefore a confirmation on every skill XP raise would be a retail divergence.

Cross-reference

  • ACE GameActionTrainSkill.Handle reads exactly two fields, skill id and credits spent, then calls HandleActionTrainSkill.
  • Chorizite's generated native client bindings expose CM_Train::Event_TrainSkillAdvancementClass(UInt32 skill, UInt32 credits) and separately expose CM_Train::Event_TrainSkill(UInt32 skill, UInt32 xp).

These corroborate the callback's two stored properties and the distinction between training a skill and adding XP to a trained skill. The named retail client remains the behavioral oracle for when the dialog appears.