Phase 7: PlayerVendorSale core event + subscriber
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>
This commit is contained in:
83
patches/BridgeVendorSale.cs
Normal file
83
patches/BridgeVendorSale.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,28 @@ git apply --check patches/<name>.patch # dry run
|
||||
git apply patches/<name>.patch
|
||||
```
|
||||
|
||||
## Current
|
||||
## Phase 7 — player-vendor sale (a coupled unit)
|
||||
|
||||
| Patch | Phase | File | Why |
|
||||
|-------|:-----:|------|-----|
|
||||
| _(none yet)_ | | | |
|
||||
Player-vendor purchases raise **no** EventSink. `ValidVendorPurchase` / `ValidVendorSell` cover NPC vendors only. The commit point is `PlayerVendorBuyGump.OnResponse`, the only place where buyer, vendor **owner**, price, and commission are all in scope — exactly what cheat detection needs. See `docs/PLAN.md` §6.
|
||||
|
||||
## Planned
|
||||
This is the one non-drop-in piece. Apply all three together:
|
||||
|
||||
| Patch | Phase | File | Why |
|
||||
|-------|:-----:|------|-----|
|
||||
| `playervendor-sale-event` | 7 | `Server/EventSink.cs` | Declare `PlayerVendorSale`, `InvokePlayerVendorSale`, `PlayerVendorSaleEventArgs { Buyer, Vendor, Owner, Item, Price, Commission }`. |
|
||||
| `playervendor-sale-event` | 7 | `Scripts/Gumps/PlayerVendorGumps.cs` | One `InvokePlayerVendorSale` call after the `HoldGold +=` at line 96, where the sale commits. |
|
||||
| Item | Target | What |
|
||||
|------|--------|------|
|
||||
| `playervendor-sale-eventsink.patch` | `Server/EventSink.cs` | Adds the `PlayerVendorSale` delegate, `PlayerVendorSaleEventArgs { Buyer, Vendor, Owner, Item, Price, Commission }`, the event field, and `InvokePlayerVendorSale`. |
|
||||
| `playervendor-sale-gump.patch` | `Scripts/Gumps/PlayerVendorGumps.cs` | One `InvokePlayerVendorSale(...)` call right after the committed `HoldGold +=`. |
|
||||
| `BridgeVendorSale.cs` | copy to `Scripts/Custom/Bridge/` | The subscriber that emits `vendor.sale`. **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 unpatched install. |
|
||||
|
||||
Player-vendor purchases raise **no** EventSink. `ValidVendorPurchase` / `ValidVendorSell` cover NPC vendors only. The commit point is `PlayerVendorBuyGump.OnResponse`, and it is the only place where buyer, vendor **owner**, price, and commission are all in scope — which is exactly what cheat detection needs. See `docs/PLAN.md` §6.
|
||||
```bash
|
||||
cd <servuo root>
|
||||
git apply --check patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch # dry run
|
||||
git apply patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch
|
||||
cp patches/BridgeVendorSale.cs Scripts/Custom/Bridge/BridgeVendorSale.cs
|
||||
```
|
||||
|
||||
Both patches are `git`-format and verified with `git apply --check` against stock ServUO 57.4. Modifying `EventSink.cs` means the **core** rebuilds, so `ScriptCompiler`'s dynamic script build is not enough — rebuild the solution (`dotnet build ServUO.sln`) or the server binary.
|
||||
|
||||
Not applicable to a non-git shard? `git apply` works in a plain directory too. If `patch` is used instead, note the core files are CRLF; use `patch --binary`.
|
||||
|
||||
## Note on `Scripts.csproj`
|
||||
|
||||
|
||||
66
patches/playervendor-sale-eventsink.patch
Normal file
66
patches/playervendor-sale-eventsink.patch
Normal file
@@ -0,0 +1,66 @@
|
||||
diff --git a/Server/EventSink.cs b/Server/EventSink.cs
|
||||
index d30788f..1da2667 100644
|
||||
--- a/Server/EventSink.cs
|
||||
+++ b/Server/EventSink.cs
|
||||
@@ -171,6 +171,8 @@ namespace Server
|
||||
|
||||
public delegate void ValidVendorSellEventHandler(ValidVendorSellEventArgs e);
|
||||
|
||||
+ public delegate void PlayerVendorSaleEventHandler(PlayerVendorSaleEventArgs e);
|
||||
+
|
||||
public delegate void CorpseLootEventHandler(CorpseLootEventArgs e);
|
||||
|
||||
public delegate void RepairItemEventHandler(RepairItemEventArgs e);
|
||||
@@ -1521,6 +1523,29 @@ namespace Server
|
||||
}
|
||||
}
|
||||
|
||||
+ // Player-vendor purchases raise no other EventSink. This fires at the committed sale in
|
||||
+ // PlayerVendorBuyGump.OnResponse, where buyer, vendor owner, item, price, and commission
|
||||
+ // are all in scope -- the data the bridge's cheat-detection feed needs.
|
||||
+ public class PlayerVendorSaleEventArgs : EventArgs
|
||||
+ {
|
||||
+ public Mobile Buyer { get; set; }
|
||||
+ public Mobile Vendor { get; set; }
|
||||
+ public Mobile Owner { get; set; }
|
||||
+ public Item Item { get; set; }
|
||||
+ public int Price { get; set; }
|
||||
+ public int Commission { get; set; }
|
||||
+
|
||||
+ public PlayerVendorSaleEventArgs(Mobile buyer, Mobile vendor, Mobile owner, Item item, int price, int commission)
|
||||
+ {
|
||||
+ Buyer = buyer;
|
||||
+ Vendor = vendor;
|
||||
+ Owner = owner;
|
||||
+ Item = item;
|
||||
+ Price = price;
|
||||
+ Commission = commission;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
public class CorpseLootEventArgs : EventArgs
|
||||
{
|
||||
public Mobile Mobile { get; set; }
|
||||
@@ -1771,6 +1796,7 @@ namespace Server
|
||||
public static event TameCreatureEventHandler TameCreature;
|
||||
public static event ValidVendorPurchaseEventHandler ValidVendorPurchase;
|
||||
public static event ValidVendorSellEventHandler ValidVendorSell;
|
||||
+ public static event PlayerVendorSaleEventHandler PlayerVendorSale;
|
||||
public static event CorpseLootEventHandler CorpseLoot;
|
||||
public static event RepairItemEventHandler RepairItem;
|
||||
public static event AlterItemEventHandler AlterItem;
|
||||
@@ -2416,6 +2442,14 @@ namespace Server
|
||||
}
|
||||
}
|
||||
|
||||
+ public static void InvokePlayerVendorSale(PlayerVendorSaleEventArgs e)
|
||||
+ {
|
||||
+ if (PlayerVendorSale != null)
|
||||
+ {
|
||||
+ PlayerVendorSale(e);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
public static void InvokeCorpseLoot(CorpseLootEventArgs e)
|
||||
{
|
||||
if (CorpseLoot != null)
|
||||
15
patches/playervendor-sale-gump.patch
Normal file
15
patches/playervendor-sale-gump.patch
Normal file
@@ -0,0 +1,15 @@
|
||||
diff --git a/Scripts/Gumps/PlayerVendorGumps.cs b/Scripts/Gumps/PlayerVendorGumps.cs
|
||||
index 049aae6..f1b30d2 100644
|
||||
--- a/Scripts/Gumps/PlayerVendorGumps.cs
|
||||
+++ b/Scripts/Gumps/PlayerVendorGumps.cs
|
||||
@@ -95,6 +95,10 @@ namespace Server.Gumps
|
||||
|
||||
m_Vendor.HoldGold += m_VI.Price - commission;
|
||||
|
||||
+ // uo-link: the only committed-sale hook for player vendors (no EventSink exists).
|
||||
+ EventSink.InvokePlayerVendorSale(
|
||||
+ new PlayerVendorSaleEventArgs(from, m_Vendor, m_Vendor.Owner, m_VI.Item, m_VI.Price, commission));
|
||||
+
|
||||
from.SendLocalizedMessage(503201); // You take the item.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user