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>
This commit is contained in:
parent
66bdae7a83
commit
85980fe13f
7 changed files with 341 additions and 14 deletions
|
|
@ -195,6 +195,14 @@ public static class CharacterStatController
|
|||
long Cost,
|
||||
int Amount);
|
||||
|
||||
/// <summary>
|
||||
/// Handles a retail stat raise and invokes <paramref name="completed"/> when
|
||||
/// the request has either been applied or cancelled. The completion seam is
|
||||
/// required by asynchronous retail confirmation dialogs such as
|
||||
/// <c>gmSkillUI::TrainSkill @ 0x0049C5F0</c>.
|
||||
/// </summary>
|
||||
public delegate void RaiseRequestHandler(RaiseRequest request, Action completed);
|
||||
|
||||
private sealed class TabVisual
|
||||
{
|
||||
public required CharacterStatTab Tab { get; init; }
|
||||
|
|
@ -248,7 +256,7 @@ public static class CharacterStatController
|
|||
UiDatFont? datFont = null,
|
||||
UiDatFont? rowDatFont = null,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve = null,
|
||||
Action<RaiseRequest>? onRaiseRequest = null,
|
||||
RaiseRequestHandler? onRaiseRequest = null,
|
||||
Action? onClose = null)
|
||||
{
|
||||
// rowDatFont: larger font for attribute row name/value text (18px vs 16px default).
|
||||
|
|
@ -1129,7 +1137,7 @@ public static class CharacterStatController
|
|||
int[] attrSel,
|
||||
int[] skillSel,
|
||||
Func<IReadOnlyList<SkillRowBinding>> skillRows,
|
||||
Action<RaiseRequest>? onRaiseRequest,
|
||||
RaiseRequestHandler? onRaiseRequest,
|
||||
Action<uint?>? afterRaiseRequest)
|
||||
{
|
||||
foreach (var button in allRaise1)
|
||||
|
|
@ -1154,7 +1162,7 @@ public static class CharacterStatController
|
|||
int[] attrSel,
|
||||
int[] skillSel,
|
||||
Func<IReadOnlyList<SkillRowBinding>> skillRows,
|
||||
Action<RaiseRequest>? onRaiseRequest,
|
||||
RaiseRequestHandler? onRaiseRequest,
|
||||
Action<uint?>? afterRaiseRequest)
|
||||
{
|
||||
if (onRaiseRequest is null) return;
|
||||
|
|
@ -1180,8 +1188,7 @@ public static class CharacterStatController
|
|||
|
||||
if (request is not { } value) return;
|
||||
|
||||
onRaiseRequest(value);
|
||||
afterRaiseRequest?.Invoke(selectedSkillId);
|
||||
onRaiseRequest(value, () => afterRaiseRequest?.Invoke(selectedSkillId));
|
||||
}
|
||||
|
||||
private static RaiseRequest? TryBuildAttributeRaiseRequest(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public static class RetailDialogProperty
|
|||
public const uint QueueKey = 0xC3u;
|
||||
public const uint Message = 0xC5u;
|
||||
public const uint UsageObjectId = 0x1000003Du;
|
||||
public const uint TrainSkillId = 0x10000040u;
|
||||
public const uint TrainSkillCredits = 0x10000041u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
using System.Globalization;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Ports the untrained-skill confirmation owned by
|
||||
/// <c>gmSkillUI::TrainSkill @ 0x0049C5F0</c> and
|
||||
/// <c>gmSkillUI::TrainSkillDialogCallback @ 0x0049C210</c>.
|
||||
/// Already-trained skill XP raises deliberately bypass this controller, matching
|
||||
/// retail's separate <c>gmSkillUI::RaiseSelection @ 0x0049C8C0</c> path.
|
||||
/// </summary>
|
||||
public sealed class RetailSkillTrainingConfirmationController
|
||||
{
|
||||
public const string MessageFormat =
|
||||
"Are you sure you want to spend {0} credits to train {1}?";
|
||||
|
||||
private readonly RetailDialogFactory _dialogs;
|
||||
|
||||
public RetailSkillTrainingConfirmationController(RetailDialogFactory dialogs)
|
||||
=> _dialogs = dialogs ?? throw new ArgumentNullException(nameof(dialogs));
|
||||
|
||||
public uint Request(
|
||||
CharacterStatController.RaiseRequest request,
|
||||
CharacterSheet sheet,
|
||||
Action<CharacterStatController.RaiseRequest> accepted,
|
||||
Action completed)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sheet);
|
||||
ArgumentNullException.ThrowIfNull(accepted);
|
||||
ArgumentNullException.ThrowIfNull(completed);
|
||||
|
||||
if (request.Kind != CharacterStatController.RaiseTargetKind.TrainSkill)
|
||||
throw new ArgumentException("Only TrainSkill requests require this confirmation.", nameof(request));
|
||||
if (request.Cost <= 0 || request.Cost > uint.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(nameof(request), "Training cost must fit retail's uint32 field.");
|
||||
|
||||
CharacterSkill skill = sheet.Skills.FirstOrDefault(candidate => candidate.Id == request.StatId)
|
||||
?? throw new InvalidOperationException($"Skill {request.StatId} is absent from the character sheet.");
|
||||
|
||||
string message = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
MessageFormat,
|
||||
request.Cost,
|
||||
skill.Name);
|
||||
|
||||
RetailDialogData data = RetailDialogData.Confirmation(message)
|
||||
.Set(RetailDialogProperty.TrainSkillId, request.StatId)
|
||||
.Set(RetailDialogProperty.TrainSkillCredits, checked((uint)request.Cost));
|
||||
|
||||
return _dialogs.MakeDialog(data, result =>
|
||||
{
|
||||
if (result.GetBoolean(RetailDialogProperty.ConfirmationResult))
|
||||
{
|
||||
uint skillId = result.GetUInt32(RetailDialogProperty.TrainSkillId);
|
||||
uint credits = result.GetUInt32(RetailDialogProperty.TrainSkillCredits);
|
||||
if (skillId != 0u && credits != 0u)
|
||||
{
|
||||
accepted(new CharacterStatController.RaiseRequest(
|
||||
CharacterStatController.RaiseTargetKind.TrainSkill,
|
||||
skillId,
|
||||
credits,
|
||||
Amount: 1));
|
||||
}
|
||||
}
|
||||
|
||||
completed();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +142,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private readonly RetailUiAutomationScriptRunner? _automation;
|
||||
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||
private bool _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
|
|
@ -707,6 +708,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_itemConfirmationController = new RetailItemConfirmationController(
|
||||
DialogFactory,
|
||||
ItemInteraction);
|
||||
_skillTrainingConfirmationController =
|
||||
new RetailSkillTrainingConfirmationController(DialogFactory);
|
||||
Console.WriteLine(
|
||||
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
||||
}
|
||||
|
|
@ -806,7 +809,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont(0x40000001u) ?? _bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
provider.HandleRaiseRequest,
|
||||
(request, completed) => HandleCharacterRaise(provider, request, completed),
|
||||
() => CloseWindow(WindowNames.Character));
|
||||
RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
|
|
@ -826,6 +829,30 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E).");
|
||||
}
|
||||
|
||||
private void HandleCharacterRaise(
|
||||
CharacterSheetProvider provider,
|
||||
CharacterStatController.RaiseRequest request,
|
||||
Action completed)
|
||||
{
|
||||
if (request.Kind != CharacterStatController.RaiseTargetKind.TrainSkill)
|
||||
{
|
||||
provider.HandleRaiseRequest(request);
|
||||
completed();
|
||||
return;
|
||||
}
|
||||
|
||||
RetailSkillTrainingConfirmationController confirmations =
|
||||
_skillTrainingConfirmationController
|
||||
?? throw new InvalidOperationException(
|
||||
"The retail dialog factory must be mounted before the character panel.");
|
||||
|
||||
confirmations.Request(
|
||||
request,
|
||||
provider.BuildSheet(),
|
||||
provider.HandleRaiseRequest,
|
||||
completed);
|
||||
}
|
||||
|
||||
private void MountPlugins()
|
||||
{
|
||||
if (_bindings.Plugins is null) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue