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>
79 lines
2.6 KiB
Markdown
79 lines
2.6 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
0x8E dialog type (1 = ConfirmationDialog)
|
|
0x92 confirmation result written by ConfirmationDialog
|
|
0xC5 message StringInfo
|
|
0x10000040 selected skill id
|
|
0x10000041 skill-credit cost
|
|
```
|
|
|
|
## Pseudocode
|
|
|
|
```text
|
|
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.
|