acdream never parsed retail's Sound event, so every server-driven cue was silent: melee hits and wounds, wield/unwield, pickup/drop, lockpicking, lifestone bind, spell resist, trap triggers, item mana depletion. SoundEvent parses the 16-byte message (guid, SoundType, f32 volume) whose layout three oracles agree on: retail CM_Physics::DispatchSB_SoundEvent @0x006AC760 reading buf+4/+8/+0xC, ACE's GameMessageSound at declared length 16, and holtburger's PlaySoundData. Playback reuses EntityEffectController's existing per-guid queue rather than adding a second one, because retail routes sounds through the SAME CObjectMaint blob queue as F754/F755: an event for a guid the client does not know yet is parked and drained by HandleCreateObject, so a creature that spawns and immediately grunts still grunts. Dropping it — the obvious alternative — would silently lose the cue. Sound joins Direct and Typed as a third PendingEffect kind so one readiness edge releases the whole mixed stream in order. AudioHookSink.PlayServerSound reproduces two decoded asymmetries with the animation-hook path: the sound plays at the WIRE volume and the SoundTable entry's volume is ignored (the hook path does the opposite), while the entry's probability still gates it and its priority still drives eviction. An object with no SoundTable plays nothing, matching CPhysicsObj::play_sound @0x0050F460's early return. The no-window host parses and discards, exactly as it does for F754/F755 — sound is presentation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.9 KiB
C#
103 lines
3.9 KiB
C#
using AcDream.App.Physics;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.App.Net;
|
|
|
|
/// <summary>
|
|
/// Routes one live session's entity packets through the non-reentrant retail
|
|
/// inbound envelope without retaining the window host.
|
|
/// </summary>
|
|
internal sealed class LiveEntitySessionController
|
|
{
|
|
private readonly RetailInboundEventDispatcher _inbound;
|
|
private readonly LiveEntityHydrationController _hydration;
|
|
private readonly LiveEntityNetworkUpdateController _updates;
|
|
private readonly ILocalPlayerTeleportNetworkSink _teleport;
|
|
private readonly EntityEffectController _effects;
|
|
|
|
public LiveEntitySessionController(
|
|
RetailInboundEventDispatcher inbound,
|
|
LiveEntityHydrationController hydration,
|
|
LiveEntityNetworkUpdateController updates,
|
|
ILocalPlayerTeleportNetworkSink teleport,
|
|
EntityEffectController effects)
|
|
{
|
|
_inbound = inbound ?? throw new ArgumentNullException(nameof(inbound));
|
|
_hydration = hydration ?? throw new ArgumentNullException(nameof(hydration));
|
|
_updates = updates ?? throw new ArgumentNullException(nameof(updates));
|
|
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
|
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
|
}
|
|
|
|
public LiveEntitySessionSink CreateSink() => new(
|
|
OnSpawned,
|
|
OnDeleted,
|
|
OnPickedUp,
|
|
OnMotionUpdated,
|
|
OnPositionUpdated,
|
|
OnVectorUpdated,
|
|
OnStateUpdated,
|
|
OnParentUpdated,
|
|
OnTeleportStarted,
|
|
OnAppearanceUpdated,
|
|
OnPlayPhysicsScript,
|
|
OnPlayPhysicsScriptType,
|
|
OnSoundEvent);
|
|
|
|
private void OnSpawned(WorldSession.EntitySpawn value) =>
|
|
_inbound.Run(_hydration, value,
|
|
static (owner, message) => owner.OnCreate(message));
|
|
|
|
private void OnDeleted(DeleteObject.Parsed value) =>
|
|
_inbound.Run(_hydration, value,
|
|
static (owner, message) => owner.OnDelete(message));
|
|
|
|
private void OnPickedUp(PickupEvent.Parsed value) =>
|
|
_inbound.Run(_hydration, value,
|
|
static (owner, message) => owner.OnPickup(message));
|
|
|
|
private void OnMotionUpdated(WorldSession.EntityMotionUpdate value) =>
|
|
_inbound.Run(_updates, value,
|
|
static (owner, message) => owner.OnMotion(message));
|
|
|
|
private void OnPositionUpdated(WorldSession.EntityPositionUpdate value) =>
|
|
_inbound.Run(_updates, value,
|
|
static (owner, message) => owner.OnPosition(message));
|
|
|
|
private void OnVectorUpdated(VectorUpdate.Parsed value) =>
|
|
_inbound.Run(_updates, value,
|
|
static (owner, message) => owner.OnVector(message));
|
|
|
|
private void OnStateUpdated(SetState.Parsed value) =>
|
|
_inbound.Run(_updates, value,
|
|
static (owner, message) => owner.OnState(message));
|
|
|
|
private void OnParentUpdated(ParentEvent.Parsed value) =>
|
|
_inbound.Run(_hydration, value,
|
|
static (owner, message) => owner.OnParent(message));
|
|
|
|
private void OnTeleportStarted(uint value) =>
|
|
_inbound.Run(_teleport, value,
|
|
static (owner, message) => owner.OnTeleportStarted(message));
|
|
|
|
private void OnAppearanceUpdated(ObjDescEvent.Parsed value) =>
|
|
_inbound.Run(_hydration, value,
|
|
static (owner, message) => owner.OnAppearance(message));
|
|
|
|
private void OnPlayPhysicsScript(PlayPhysicsScript value) =>
|
|
_inbound.Run(_effects, value,
|
|
static (owner, message) => owner.HandleDirect(message));
|
|
|
|
private void OnPlayPhysicsScriptType(PlayPhysicsScriptType value) =>
|
|
_inbound.Run(_effects, value,
|
|
static (owner, message) => owner.HandleTyped(message));
|
|
|
|
private void OnSoundEvent(SoundEvent value) =>
|
|
_inbound.Run(_effects, value,
|
|
static (owner, message) => owner.HandleSound(message));
|
|
}
|