The one non-drop-in piece. Player-vendor purchases raise no EventSink, so the sale is invisible to subscription. Two git-format core patches add a PlayerVendorSale event and raise it at the committed sale in PlayerVendorBuyGump.OnResponse (right after HoldGold +=), where buyer, vendor owner, item, price, and commission are all in scope. The subscriber BridgeVendorSale emits vendor.sale. All three are a coupled unit. The subscriber references PlayerVendorSaleEventArgs, which does not exist until the EventSink patch is applied, so it lives in patches/ not overlay/ -- shipping it in overlay would break the build on any unpatched install. patches/README.md documents applying the unit; both patches verified with git apply --check against stock ServUO 57.4. This is the first phase that rebuilds the core (ServUO.exe), not just Scripts.dll. vendor.sale carries buyer and vendor-owner accounts, both present and distinct, which is the pair that flags gold-laundering when they match -- richer than the ownerless NPC ValidVendor* events, and on a committed sale rather than a validation stage. Verified with a probe firing the event on real seeded-vendor data: vendor.sale emitted with buyerAcct=seed_001, ownerAcct=seed_000, Longsword, price 69819. The probe proves the event, args, subscriber, and payload; the literal gump call site firing on a real purchase needs a live buyer with a NetState and is confirmed by an in-game buy. Evidence in docs/PLAN.md §17. This completes every phase on the ServUO side. Phases 0-6 are drop-in (overlay/); 7 is patches/. Cheat signals are folded into existing streams (fastwalk, audit, vendor.sale), not a separate phase. Remaining work is the Rust sidecar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.1 KiB
C#
84 lines
3.1 KiB
C#
using System;
|
|
|
|
using Server;
|
|
using Server.Accounting;
|
|
using Server.Custom.Bridge;
|
|
|
|
namespace Server.Custom.Bridge
|
|
{
|
|
/// <summary>
|
|
/// Subscribes to the PlayerVendorSale event added by the Phase 7 core patches. This file is
|
|
/// part of that coupled unit and is NOT in overlay/, because it references
|
|
/// PlayerVendorSaleEventArgs, which does not exist until the EventSink patch is applied —
|
|
/// shipping it in overlay/ would break the build on any install without the patch.
|
|
///
|
|
/// Deploy: apply patches/playervendor-sale-*.patch, then copy this file to
|
|
/// Scripts/Custom/Bridge/BridgeVendorSale.cs.
|
|
///
|
|
/// The event fires at the committed sale (PlayerVendorBuyGump.OnResponse), on the Core
|
|
/// thread, with buyer, vendor owner, item, price, and commission all in scope — richer than
|
|
/// the NPC ValidVendor* events (which lack owner and commission) and, unlike them, on a
|
|
/// committed sale rather than a validation stage. It is the backbone of the cheat-detection
|
|
/// feed: same-account buyer≈owner is gold laundering, off-market prices and burst patterns
|
|
/// are visible to the sidecar.
|
|
/// </summary>
|
|
public static class BridgeVendorSale
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
if (!BridgeConfig.Enabled)
|
|
return;
|
|
|
|
EventSink.PlayerVendorSale += OnPlayerVendorSale;
|
|
Console.WriteLine("[Bridge] player-vendor sale stream attached");
|
|
}
|
|
|
|
private static void OnPlayerVendorSale(PlayerVendorSaleEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var sb = BridgeJson.Begin("vendor.sale")
|
|
.Bool("committed", true);
|
|
|
|
// Buyer
|
|
if (e.Buyer != null)
|
|
{
|
|
sb.Ser("buyerSerial", e.Buyer.Serial);
|
|
var ba = e.Buyer.Account as Account;
|
|
if (ba != null)
|
|
sb.Str("buyerAcct", ba.Username);
|
|
}
|
|
|
|
// Vendor owner — the player who actually profits.
|
|
if (e.Owner != null)
|
|
{
|
|
sb.Ser("ownerSerial", e.Owner.Serial);
|
|
var oa = e.Owner.Account as Account;
|
|
if (oa != null)
|
|
sb.Str("ownerAcct", oa.Username);
|
|
}
|
|
|
|
if (e.Vendor != null)
|
|
sb.Ser("vendorSerial", e.Vendor.Serial);
|
|
|
|
if (e.Item != null)
|
|
{
|
|
sb.Ser("itemSerial", e.Item.Serial);
|
|
sb.Str("itemType", e.Item.GetType().Name);
|
|
sb.Num("itemId", e.Item.ItemID);
|
|
sb.Num("amount", e.Item.Amount);
|
|
}
|
|
|
|
sb.Num("price", e.Price);
|
|
sb.Num("commission", e.Commission);
|
|
|
|
BridgeLink.Emit(sb.End());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("[Bridge] vendor.sale handler threw: {0}", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|