refactor(net): own live session composition
Extract reset, selection, entered-world, and route construction behind LiveSessionHost while preserving the sole LiveSessionController authority. Retain partial route and subscription cleanup for retry, and replace the embedded ACE-only shortcut with the exact named-retail unsigned skill formula. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
18d4b999de
commit
557eb7ef6b
22 changed files with 1430 additions and 236 deletions
|
|
@ -380,10 +380,6 @@ public sealed class GameWindow : IDisposable
|
|||
// DevToolsEnabled reads through typed RuntimeOptions.
|
||||
private bool DevToolsEnabled => _options.DevTools;
|
||||
|
||||
// Slice 3: the reset transaction remains cached by the host, while the
|
||||
// lifecycle controller owns each exact event/command/session generation.
|
||||
private AcDream.App.Net.LiveSessionResetPlan? _liveSessionResetPlan;
|
||||
|
||||
// Phase G.1-G.2 world lighting/time state.
|
||||
public readonly AcDream.Core.World.WorldTimeService WorldTime =
|
||||
new AcDream.Core.World.WorldTimeService(
|
||||
|
|
@ -485,7 +481,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
// Phase K.2 — auto-enter player mode after a successful login. Armed
|
||||
// by ApplyLiveSessionEnteredWorld; ticked from
|
||||
// by LiveSessionHost's entered-world transition; ticked from
|
||||
// OnUpdate; disarmed if the user manually enters fly mode (or any
|
||||
// other path that pre-empts the chase camera). Skipped entirely
|
||||
// offline (orbit camera stays the foreground). The class internally
|
||||
|
|
@ -529,8 +525,9 @@ public sealed class GameWindow : IDisposable
|
|||
// Slice 3: the controller is the sole App owner. Outbound feature
|
||||
// callbacks resolve this borrowed handle at call time and never cache it.
|
||||
private AcDream.App.Net.LiveSessionController? _liveSessionController;
|
||||
private AcDream.App.Net.LiveSessionHost? _liveSessionHost;
|
||||
private AcDream.Core.Net.WorldSession? LiveSession =>
|
||||
_liveSessionController?.CurrentSession;
|
||||
_liveSessionHost?.CurrentSession;
|
||||
private readonly AcDream.App.World.LiveWorldOriginState _liveWorldOrigin = new();
|
||||
private int _liveCenterX => _liveWorldOrigin.CenterX;
|
||||
private int _liveCenterY => _liveWorldOrigin.CenterY;
|
||||
|
|
@ -1194,7 +1191,7 @@ public sealed class GameWindow : IDisposable
|
|||
try
|
||||
{
|
||||
// _activeToonKey is updated by
|
||||
// ApplyLiveSessionEnteredWorld
|
||||
// LiveSessionHost entered-world transition
|
||||
// so saving character settings always
|
||||
// writes under the chosen character's
|
||||
// name (or "default" pre-login).
|
||||
|
|
@ -1483,7 +1480,7 @@ public sealed class GameWindow : IDisposable
|
|||
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
|
||||
.DragItemOnPlayerOpensSecureTrade) != 0,
|
||||
toast: text => _debugVm?.AddToast(text),
|
||||
readyForInventoryRequest: () => _liveSessionController?.IsInWorld == true,
|
||||
readyForInventoryRequest: () => _liveSessionHost?.IsInWorld == true,
|
||||
playerOnGround: GetDebugPlayerOnGround,
|
||||
inNonCombatMode: () => Combat.CurrentMode
|
||||
== AcDream.Core.Combat.CombatMode.NonCombat,
|
||||
|
|
@ -1534,7 +1531,7 @@ public sealed class GameWindow : IDisposable
|
|||
playerGuid: () => _playerServerGuid,
|
||||
activeToonName: () => _activeToonKey,
|
||||
fallbackSheet: AcDream.App.Studio.SampleData.SampleCharacter,
|
||||
canSendRaise: () => _liveSessionController?.IsInWorld == true,
|
||||
canSendRaise: () => _liveSessionHost?.IsInWorld == true,
|
||||
sendRaiseAttribute: (statId, cost) => LiveSession?.SendRaiseAttribute(statId, cost),
|
||||
sendRaiseVital: (statId, cost) => LiveSession?.SendRaiseVital(statId, cost),
|
||||
sendRaiseSkill: (statId, cost) => LiveSession?.SendRaiseSkill(statId, cost),
|
||||
|
|
@ -1555,7 +1552,7 @@ public sealed class GameWindow : IDisposable
|
|||
sendTargeted: (target, spellId) => LiveSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage: text => Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: () => _itemInteractionController.IncrementBusyCount(),
|
||||
canSend: () => _liveSessionController?.IsInWorld == true);
|
||||
canSend: () => _liveSessionHost?.IsInWorld == true);
|
||||
_uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside;
|
||||
|
||||
// Feed Silk input to the UiRoot tree so windows drag / close / select.
|
||||
|
|
@ -1653,7 +1650,7 @@ public sealed class GameWindow : IDisposable
|
|||
Vitals: new AcDream.App.UI.VitalsRuntimeBindings(_vitalsVm),
|
||||
Chat: new AcDream.App.UI.ChatRuntimeBindings(
|
||||
retailChatVm,
|
||||
() => _liveSessionController?.Commands
|
||||
() => _liveSessionHost?.Commands
|
||||
?? AcDream.UI.Abstractions.NullCommandBus.Instance),
|
||||
Radar: new AcDream.App.UI.RadarRuntimeBindings(
|
||||
radarSnapshotProvider.BuildSnapshot,
|
||||
|
|
@ -2960,10 +2957,9 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||
_playerModeAutoEntry),
|
||||
cameraFrame);
|
||||
_liveSessionHost = CreateLiveSessionHost();
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionController.Start(
|
||||
_options,
|
||||
CreateLiveSessionLifecycleHost());
|
||||
_liveSessionHost.Start(_options);
|
||||
switch (liveStart.Status)
|
||||
{
|
||||
case AcDream.App.Net.LiveSessionStartStatus.MissingCredentials:
|
||||
|
|
@ -2976,11 +2972,27 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSessionLifecycleHost CreateLiveSessionLifecycleHost() =>
|
||||
new(new AcDream.App.Net.LiveSessionLifecycleBindings(
|
||||
Bind: CreateLiveSessionBinding,
|
||||
Reset: () =>
|
||||
(_liveSessionResetPlan ??= CreateLiveSessionResetPlan()).Execute(),
|
||||
private AcDream.App.Net.LiveSessionHost CreateLiveSessionHost() =>
|
||||
new(_liveSessionController!, new AcDream.App.Net.LiveSessionHostBindings(
|
||||
Routing: new(
|
||||
CreateLiveSessionEventRouter,
|
||||
session => new AcDream.App.Net.LiveSessionCommandRouter(
|
||||
CreateLiveSessionCommandBindings(session))),
|
||||
Reset: CreateLiveSessionResetBindings(),
|
||||
Selection: new(
|
||||
SetPlayerIdentity: id => _playerServerGuid = id,
|
||||
SetVitalsIdentity: id => _vitalsVm?.SetLocalPlayerGuid(id),
|
||||
SetChatIdentity: Chat.SetLocalPlayerGuid,
|
||||
MarkPersistent: _worldState.MarkPersistent,
|
||||
SetVanishProbeIdentity: id =>
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = id,
|
||||
ClearCombat: Combat.Clear),
|
||||
EnteredWorld: new(
|
||||
SetActiveCharacter: name => _activeToonKey = name,
|
||||
RestoreLayout: () => _retailUiRuntime?.RestoreLayout(),
|
||||
SyncToolbar: SyncToolbarWindowButtons,
|
||||
LoadCharacterSettings: LoadLiveSessionCharacterSettings,
|
||||
ArmPlayerModeAutoEntry: () => _playerModeAutoEntry?.Arm()),
|
||||
Connecting: (host, port, user) =>
|
||||
Chat.OnSystemMessage(
|
||||
$"connecting to {host}:{port} as {user}",
|
||||
|
|
@ -2988,38 +3000,21 @@ public sealed class GameWindow : IDisposable
|
|||
Connected: () =>
|
||||
Chat.OnSystemMessage(
|
||||
"connected — character list received",
|
||||
chatType: 1),
|
||||
Selected: ApplyLiveSessionSelection,
|
||||
Entered: ApplyLiveSessionEnteredWorld));
|
||||
chatType: 1)));
|
||||
|
||||
private void ApplyLiveSessionSelection(
|
||||
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||
private void LoadLiveSessionCharacterSettings(string characterName)
|
||||
{
|
||||
_playerServerGuid = selection.CharacterId;
|
||||
_vitalsVm?.SetLocalPlayerGuid(selection.CharacterId);
|
||||
Chat.SetLocalPlayerGuid(selection.CharacterId);
|
||||
_worldState.MarkPersistent(selection.CharacterId);
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = selection.CharacterId;
|
||||
Combat.Clear();
|
||||
}
|
||||
|
||||
private void ApplyLiveSessionEnteredWorld(
|
||||
AcDream.App.Net.LiveSessionCharacterSelection selection)
|
||||
{
|
||||
_activeToonKey = selection.CharacterName;
|
||||
_retailUiRuntime?.RestoreLayout();
|
||||
SyncToolbarWindowButtons();
|
||||
if (_settingsStore is not null && _settingsVm is not null)
|
||||
{
|
||||
var toonBag = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
var toonBag = _settingsStore.LoadCharacter(characterName);
|
||||
_settingsVm.LoadCharacterContext(toonBag);
|
||||
Console.WriteLine($"settings: loaded character[{_activeToonKey}] preferences");
|
||||
Console.WriteLine(
|
||||
$"settings: loaded character[{characterName}] preferences");
|
||||
}
|
||||
_playerModeAutoEntry?.Arm();
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSessionResetPlan CreateLiveSessionResetPlan() =>
|
||||
AcDream.App.Net.LiveSessionResetManifest.Create(new()
|
||||
private AcDream.App.Net.LiveSessionResetBindings
|
||||
CreateLiveSessionResetBindings() => new()
|
||||
{
|
||||
MouseCapture = ResetSessionMouseCapture,
|
||||
PlayerPresentation = ResetSessionPlayerPresentation,
|
||||
|
|
@ -3052,7 +3047,7 @@ public sealed class GameWindow : IDisposable
|
|||
AnimationHookFrames = () => _animationHookFrames?.Clear(),
|
||||
LivePresentation = () => _liveEntityPresentation?.Clear(),
|
||||
RemoteMovementDiagnostics = _remoteMovementObservations.Clear,
|
||||
});
|
||||
};
|
||||
|
||||
private void ResetSessionMouseCapture()
|
||||
=> _gameplayInputFrame?.ResetSession();
|
||||
|
|
@ -3108,12 +3103,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes the exact inbound and outbound owners for one live session.
|
||||
/// Registration completes before Connect; outbound commands remain inert
|
||||
/// until EnterWorld succeeds.
|
||||
/// </summary>
|
||||
private AcDream.App.Net.LiveSessionBinding CreateLiveSessionBinding(
|
||||
private AcDream.App.Net.LiveSessionEventRouter CreateLiveSessionEventRouter(
|
||||
AcDream.Core.Net.WorldSession session)
|
||||
{
|
||||
var skillTable = _dats?.Get<DatReaderWriter.DBObjs.SkillTable>(0x0E000004u);
|
||||
|
|
@ -3126,39 +3116,19 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine);
|
||||
}
|
||||
|
||||
AcDream.App.Net.LiveSessionEventRouter? events = null;
|
||||
AcDream.App.Net.LiveSessionCommandRouter? commands = null;
|
||||
try
|
||||
{
|
||||
events = new AcDream.App.Net.LiveSessionEventRouter(
|
||||
session,
|
||||
_liveEntitySessionEvents.CreateSink(),
|
||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||
OnEnvironChanged,
|
||||
ticks =>
|
||||
{
|
||||
WorldTime.SyncFromServer(ticks);
|
||||
RefreshSkyForCurrentDay();
|
||||
}),
|
||||
CreateLiveInventorySessionBindings(),
|
||||
CreateLiveCharacterSessionBindings(skillTable),
|
||||
CreateLiveSocialSessionBindings());
|
||||
|
||||
commands = new AcDream.App.Net.LiveSessionCommandRouter(
|
||||
CreateLiveSessionCommandBindings(session));
|
||||
return new AcDream.App.Net.LiveSessionBinding(
|
||||
session,
|
||||
commands,
|
||||
activateCommands: commands.Activate,
|
||||
deactivateCommands: commands.Dispose,
|
||||
detachEvents: events.Dispose);
|
||||
}
|
||||
catch
|
||||
{
|
||||
commands?.Dispose();
|
||||
events?.Dispose();
|
||||
throw;
|
||||
}
|
||||
return new AcDream.App.Net.LiveSessionEventRouter(
|
||||
session,
|
||||
_liveEntitySessionEvents.CreateSink(),
|
||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||
OnEnvironChanged,
|
||||
ticks =>
|
||||
{
|
||||
WorldTime.SyncFromServer(ticks);
|
||||
RefreshSkyForCurrentDay();
|
||||
}),
|
||||
CreateLiveInventorySessionBindings(),
|
||||
CreateLiveCharacterSessionBindings(skillTable),
|
||||
CreateLiveSocialSessionBindings());
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveInventorySessionBindings
|
||||
|
|
@ -3178,37 +3148,14 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private AcDream.App.Net.LiveCharacterSessionBindings
|
||||
CreateLiveCharacterSessionBindings(
|
||||
DatReaderWriter.DBObjs.SkillTable? skillTable) => new(
|
||||
DatReaderWriter.DBObjs.SkillTable? skillTable)
|
||||
{
|
||||
var skillCreditResolver =
|
||||
new AcDream.App.Net.LiveSkillCreditResolver(skillTable);
|
||||
return new(
|
||||
Combat,
|
||||
SpellBook,
|
||||
ResolveSkillFormulaBonus: (skillId, attributeCurrents) =>
|
||||
{
|
||||
// ACE AttributeFormula.GetFormula
|
||||
// (references/ACE/Source/ACE.Entity/Models/AttributeFormula.cs:55-):
|
||||
// Attribute1Multiplier == 0 means no attribute contribution.
|
||||
// Otherwise the retail data formula is
|
||||
// (attr1 * mult1 + attr2 * mult2) / divisor + additive.
|
||||
// Guard a zero divisor because malformed/custom DAT input must not
|
||||
// take down the live-session dispatcher.
|
||||
if (skillTable?.Skills is null)
|
||||
return 0u;
|
||||
if (!skillTable.Skills.TryGetValue(
|
||||
(DatReaderWriter.Enums.SkillId)skillId,
|
||||
out var skillBase))
|
||||
return 0u;
|
||||
|
||||
var formula = skillBase.Formula;
|
||||
if (formula.Attribute1Multiplier == 0 || formula.Divisor == 0)
|
||||
return 0u;
|
||||
|
||||
attributeCurrents.TryGetValue((uint)formula.Attribute1, out uint attribute1);
|
||||
attributeCurrents.TryGetValue((uint)formula.Attribute2, out uint attribute2);
|
||||
long numerator =
|
||||
(long)attribute1 * formula.Attribute1Multiplier +
|
||||
(long)attribute2 * formula.Attribute2Multiplier;
|
||||
long bonus = numerator / formula.Divisor + formula.AdditiveBonus;
|
||||
return bonus < 0 ? 0u : (uint)bonus;
|
||||
},
|
||||
ResolveSkillFormulaBonus: skillCreditResolver.Resolve,
|
||||
OnSkillsUpdated: (runSkill, jumpSkill) =>
|
||||
{
|
||||
_localPlayerSkills.Update(runSkill, jumpSkill, _playerController);
|
||||
|
|
@ -3228,6 +3175,7 @@ public sealed class GameWindow : IDisposable
|
|||
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
|
||||
options1,
|
||||
ClientTime: ClientTimerNow);
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveSocialSessionBindings
|
||||
CreateLiveSocialSessionBindings() => new(
|
||||
|
|
@ -3676,7 +3624,7 @@ public sealed class GameWindow : IDisposable
|
|||
// optional SettingsPanel and its visibility/input operations.
|
||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsVM? _settingsVm;
|
||||
// L.0: settings.json store + active toon key. The store is held as
|
||||
// a field so ApplyLiveSessionEnteredWorld can re-load the chosen toon's
|
||||
// a field so LiveSessionHost can re-load the chosen toon's
|
||||
// bag once we know its name (post-EnterWorld). Toon key starts as
|
||||
// "default" and gets swapped to the actual character name on the
|
||||
// first EnterWorld.
|
||||
|
|
@ -3816,7 +3764,7 @@ public sealed class GameWindow : IDisposable
|
|||
_persistedGameplay = _settingsStore.LoadGameplay();
|
||||
_persistedChat = _settingsStore.LoadChat();
|
||||
// _activeToonKey is "default" pre-EnterWorld; the post-login
|
||||
// ApplyLiveSessionEnteredWorld swaps to the chosen toon's
|
||||
// LiveSessionHost swaps to the chosen toon's
|
||||
// name and re-loads via SettingsVM.LoadCharacterContext.
|
||||
_persistedCharacter = _settingsStore.LoadCharacter(_activeToonKey);
|
||||
|
||||
|
|
@ -4159,7 +4107,7 @@ public sealed class GameWindow : IDisposable
|
|||
private void ToggleLiveCombatMode()
|
||||
{
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||
if (_liveSessionHost?.IsInWorld != true || session is null)
|
||||
return;
|
||||
|
||||
IReadOnlyList<AcDream.Core.Items.ClientObject> orderedEquipment =
|
||||
|
|
@ -4193,7 +4141,7 @@ public sealed class GameWindow : IDisposable
|
|||
private void UseItemByGuid(uint guid)
|
||||
{
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (_liveSessionController?.IsInWorld != true || session is null)
|
||||
if (_liveSessionHost?.IsInWorld != true || session is null)
|
||||
return;
|
||||
uint sequence = session.NextGameActionSequence();
|
||||
session.SendGameAction(
|
||||
|
|
@ -4418,7 +4366,10 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
if (ReferenceEquals(_liveSessionController, controller))
|
||||
{
|
||||
_liveSessionHost = null;
|
||||
_liveSessionController = null;
|
||||
}
|
||||
}),
|
||||
]),
|
||||
// Frame composition borrows equipped, effect, audio, and render
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue