fix(journal): the location readout is a FIELD, refresh at the click, and Abandon works
Three fixes from the first connected round. The location readout is authored EDITABLE (0x16), so it builds as a UiField — not the UiText its "00.0S, 00.0W" placeholder suggests. The controller resolved it as text, got null, and threw every write away in silence: Record reached the model and reached the FILE, and never reached the screen. That is exactly what was reported, and it is a whole class of bug, so the sweep that found it is now a test over every element all three controllers bind. The handlers mutated the model and left redrawing to the next frame's Tick. Retail's ListenToElementMessage @0x004968D0 ends every one of them in Update() instead — at the moment of the click. The deferred version happened to work in the client and made the behaviour untestable and a frame late; the notes-page tests I had not written until now fail against it. Abandon is wired. "Retail's abandon path is a contract-registry command we have not ported" was wrong — it is game action 0x0316 with a single contract id, and ACE replies with the 0x0315 delete QT3 already handles. Nothing is removed locally, so a refusal leaves the quest visibly intact rather than vanishing it optimistically and having it reappear on the next full table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0b27c5d0fe
commit
e35d9386e4
12 changed files with 556 additions and 16 deletions
|
|
@ -1029,6 +1029,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
Journal: d.Runtime.JournalOwner.View,
|
||||
JournalCommands: d.Runtime.JournalOwner,
|
||||
PlayerCell: () => d.PlayerController.Controller?.CellId ?? 0u,
|
||||
AbandonContract: contractId =>
|
||||
late.Session.CurrentSession?.SendAbandonContract(contractId),
|
||||
JournalDirectory: System.IO.Path.Combine(
|
||||
AcDream.Platform.ApplicationPathSet.Resolve().DataDirectory,
|
||||
"journal"),
|
||||
|
|
|
|||
|
|
@ -57,11 +57,17 @@ public sealed class JournalContractsPageController
|
|||
/// testable without waiting for it.
|
||||
/// </param>
|
||||
/// <param name="TemplateResolver">Builds one row from the authored template.</param>
|
||||
/// <param name="Abandon">
|
||||
/// Sends the abandon action for one contract. The row disappears only when
|
||||
/// the SERVER answers with its own delete, so a refused abandon leaves the
|
||||
/// quest exactly where it was.
|
||||
/// </param>
|
||||
public sealed record Bindings(
|
||||
IRuntimeContractView Contracts,
|
||||
Func<ContractCatalog> Catalog,
|
||||
Func<DateTime> Now,
|
||||
Func<uint, uint, UiElement?> TemplateResolver);
|
||||
Func<uint, uint, UiElement?> TemplateResolver,
|
||||
Action<uint>? Abandon = null);
|
||||
|
||||
private readonly Bindings _bindings;
|
||||
private readonly UiTemplateListBox? _list;
|
||||
|
|
@ -104,11 +110,8 @@ public sealed class JournalContractsPageController
|
|||
_description = UiElement.FindDescendant(page, DescriptionId) as UiText;
|
||||
_timedValue = UiElement.FindDescendant(page, TimedValueId) as UiText;
|
||||
|
||||
// The Abandon button has no wire message in this campaign's scope —
|
||||
// retail's own abandon path is a contract-registry command we have not
|
||||
// ported. Left unwired rather than given a no-op handler that would
|
||||
// look responsive and do nothing.
|
||||
_ = AbandonButtonId;
|
||||
if (UiElement.FindDescendant(page, AbandonButtonId) is UiButton abandon)
|
||||
abandon.OnClick = AbandonSelected;
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
|
@ -192,6 +195,23 @@ public sealed class JournalContractsPageController
|
|||
RefreshDetail();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abandons the selected contract — game action <c>0x0316</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Nothing is removed locally. The server replies with a <c>0x0315</c>
|
||||
/// carrying <c>DeleteContract</c> and the tracker drops the row then, so a
|
||||
/// refusal leaves the quest visibly intact rather than vanishing it
|
||||
/// optimistically and having it reappear.
|
||||
/// </remarks>
|
||||
public void AbandonSelected()
|
||||
{
|
||||
if (_selectedContractId == 0u)
|
||||
return;
|
||||
|
||||
_bindings.Abandon?.Invoke(_selectedContractId);
|
||||
}
|
||||
|
||||
/// <summary>Points the detail pane at one contract.</summary>
|
||||
public void Select(uint contractId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -69,7 +69,15 @@ public sealed class JournalNotesPageController
|
|||
private readonly UiField? _timerHours;
|
||||
private readonly UiField? _timerMinutes;
|
||||
private readonly UiText? _pageNumber;
|
||||
private readonly UiText? _location;
|
||||
|
||||
/// <summary>
|
||||
/// The location readout is authored EDITABLE (<c>0x16</c>), so it builds as
|
||||
/// a <see cref="UiField"/> — not the <see cref="UiText"/> its placeholder
|
||||
/// text suggests. Resolving it as text yielded null and threw every write
|
||||
/// away silently: Record reached the model and the file, and never the
|
||||
/// screen.
|
||||
/// </summary>
|
||||
private readonly UiField? _location;
|
||||
private readonly UiText? _runningTimer;
|
||||
private readonly UiButton? _start;
|
||||
|
||||
|
|
@ -87,7 +95,7 @@ public sealed class JournalNotesPageController
|
|||
_timerHours = UiElement.FindDescendant(page, TimerHoursFieldId) as UiField;
|
||||
_timerMinutes = UiElement.FindDescendant(page, TimerMinutesFieldId) as UiField;
|
||||
_pageNumber = UiElement.FindDescendant(page, PageNumberId) as UiText;
|
||||
_location = UiElement.FindDescendant(page, LocationTextId) as UiText;
|
||||
_location = UiElement.FindDescendant(page, LocationTextId) as UiField;
|
||||
_runningTimer = UiElement.FindDescendant(page, RunningTimerTextId) as UiText;
|
||||
_start = UiElement.FindDescendant(page, StartButtonId) as UiButton;
|
||||
|
||||
|
|
@ -108,7 +116,16 @@ public sealed class JournalNotesPageController
|
|||
field.OnFocusLost = _ => CommitText();
|
||||
}
|
||||
|
||||
Bind(page, NewButtonId, () => { CommitText(); _bindings.Commands.NewPage(); });
|
||||
// Retail's ListenToElementMessage @0x004968D0 ends every one of these
|
||||
// in Update() — the panel redraws at the moment of the click, not on
|
||||
// the next frame. Deferring to Tick would work in the client and makes
|
||||
// the behaviour untestable and a frame late.
|
||||
Bind(page, NewButtonId, () =>
|
||||
{
|
||||
CommitText();
|
||||
_bindings.Commands.NewPage();
|
||||
Refresh();
|
||||
});
|
||||
Bind(page, FirstButtonId, () => Navigate(1));
|
||||
Bind(page, LastButtonId, () => Navigate(_bindings.Journal.Snapshot.PageCount));
|
||||
Bind(page, PreviousButtonId,
|
||||
|
|
@ -172,7 +189,7 @@ public sealed class JournalNotesPageController
|
|||
? string.Empty
|
||||
: $"~ {snapshot.CurrentPage.ToString(CultureInfo.InvariantCulture)} ~");
|
||||
|
||||
SetText(_location, page.HasLocation
|
||||
_location?.SetText(page.HasLocation
|
||||
? FormatLocation(page.LocationX, page.LocationY)
|
||||
: string.Empty);
|
||||
|
||||
|
|
@ -210,6 +227,7 @@ public sealed class JournalNotesPageController
|
|||
{
|
||||
CommitText();
|
||||
_bindings.Commands.GotoPage(pageNumber);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void RecordLocation()
|
||||
|
|
@ -222,6 +240,7 @@ public sealed class JournalNotesPageController
|
|||
return; // indoors: retail's own gid_to_lcoord failure
|
||||
|
||||
_bindings.Commands.RecordLocation((float)coordinates.X, (float)coordinates.Y);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void ToggleTimer()
|
||||
|
|
@ -229,11 +248,13 @@ public sealed class JournalNotesPageController
|
|||
if (_bindings.Journal.RemainingTimerSeconds(_bindings.Now()) > 0d)
|
||||
{
|
||||
_bindings.Commands.ResetTimer();
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
CommitText(); // the fields the countdown reads
|
||||
_bindings.Commands.StartTimer(_bindings.Now());
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -324,6 +324,8 @@ public sealed record QuestRuntimeBindings(
|
|||
AcDream.Runtime.Gameplay.IRuntimeJournalView Journal,
|
||||
AcDream.Runtime.Gameplay.RuntimeJournalState JournalCommands,
|
||||
Func<uint> PlayerCell,
|
||||
/// <summary>Sends the abandon-contract action (0x0316).</summary>
|
||||
Action<uint> AbandonContract,
|
||||
/// <summary>Where the per-character journal file lives.</summary>
|
||||
string JournalDirectory,
|
||||
/// <summary>How a load or save failure reaches the player.</summary>
|
||||
|
|
@ -3553,7 +3555,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
return rowTemplates.Resolve(templateLayoutId, templateElementId);
|
||||
}),
|
||||
},
|
||||
Abandon: _bindings.Quests.AbandonContract),
|
||||
Notes: new Layout.JournalNotesPageController.Bindings(
|
||||
Journal: _bindings.Quests.Journal,
|
||||
Commands: _bindings.Quests.JournalCommands,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue