feat(inventory): port retail item giving (#216)

Preserve SmartBox drag-release coordinates, route the picked 3-D target through the retail AttemptPlaceIn3D policy, and send authoritative GiveObject requests with the selected stack quantity. Honor PlayerDescription's player secure-trade option and document the remaining trade-system boundary.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-13 20:28:38 +02:00
parent a944e49b9c
commit 30d294506c
13 changed files with 421 additions and 10 deletions

View file

@ -848,6 +848,11 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Rendering.RetailChaseCamera? _retailChaseCamera;
private bool _playerMode;
private uint _playerServerGuid;
// Retail Default_CharacterOption (acclient.h:3434). PlayerDescription
// replaces this before any in-world drag can normally occur.
private AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
_characterOptions1 =
AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1.Default;
private uint? _playerMotionTableId; // server-sent MotionTable override for the player's character
private MovementTruthOutbound? _lastMovementTruthOutbound;
private (System.Numerics.Vector3 Position,
@ -2002,6 +2007,12 @@ public sealed class GameWindow : IDisposable
sendUseWithTarget: (source, target) => _liveSession?.SendUseWithTarget(source, target),
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
sendDrop: item => _liveSession?.SendDropItem(item),
sendGive: (target, item, amount) =>
_liveSession?.SendGiveObject(target, item, amount),
dragOnPlayerOpensSecureTrade: () =>
(_characterOptions1
& AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1
.DragItemOnPlayerOpensSecureTrade) != 0,
toast: text => _debugVm?.AddToast(text),
readyForInventoryRequest: () => _liveSession is not null
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld,
@ -2643,7 +2654,11 @@ public sealed class GameWindow : IDisposable
_retailUiRuntime?.HandleConfirmationDone(done),
friends: Friends,
squelch: Squelch,
onDesiredComponents: components => DesiredComponents = components);
onDesiredComponents: components => DesiredComponents = components,
onCharacterOptions: (options1, _) =>
_characterOptions1 =
(AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1)
options1);
// Phase I.7: subscribe to CombatState events and emit
// retail-faithful "You hit X for Y damage" chat lines into
@ -11332,7 +11347,10 @@ public sealed class GameWindow : IDisposable
private void OnUiDragReleasedOutside(object payload, int x, int y)
{
if (payload is AcDream.App.UI.ItemDragPayload itemPayload)
_itemInteractionController?.DropToWorld(itemPayload);
{
uint target = PickWorldGuidAt(x, y, includeSelf: true) ?? 0u;
_itemInteractionController?.PlaceIn3D(itemPayload, target);
}
}
// L.0 follow-up: persisted-settings cache populated by
@ -11997,6 +12015,9 @@ public sealed class GameWindow : IDisposable
/// yourself by clicking your own toon); plain selection never does.
/// </summary>
private uint? PickWorldGuidAtCursor(bool includeSelf)
=> PickWorldGuidAt(_lastMouseX, _lastMouseY, includeSelf);
private uint? PickWorldGuidAt(float mouseX, float mouseY, bool includeSelf)
{
if (_cameraController is null || _window is null) return null;
@ -12015,7 +12036,7 @@ public sealed class GameWindow : IDisposable
}
return AcDream.Core.Selection.WorldPicker.Pick(
mouseX: _lastMouseX, mouseY: _lastMouseY,
mouseX: mouseX, mouseY: mouseY,
view: camera.View, projection: camera.Projection,
viewport: viewport,
candidates: _entitiesByServerGuid.Values,