Trading System
The trading system enables secure item exchanges between players with support for batch operations, configurable limits, and automatic expiration.
Overview
- TradeGrain manages each trade session
- Orleans Streams push real-time events to SignalR clients
- Configurable limits prevent abuse
- Auto-expiration cleans up abandoned trades
Trade Flow
sequenceDiagram
participant A as User A (Initiator)
participant T as TradeGrain
participant B as User B (Target)
A->>T: InitiateAsync(userA, userB)
A->>T: AddItemsAsync(items)
B->>T: AddItemAsync(item)
A->>T: AcceptAsync()
B->>T: AcceptAsync()
T->>T: ExecuteTradeAsync()
T-->>A: Items transferred
T-->>B: Items transferred
Configuration
// appsettings.json
{
"Trading": {
"TradeTimeout": "00:15:00",
"ExpirationCheckInterval": "00:01:00",
"MaxItemsPerUser": 50
}
}
| Option | Default | Description |
|---|---|---|
TradeTimeout | 15 min | Auto-expire pending trades after this duration |
ExpirationCheckInterval | 1 min | How often to check for expired trades |
MaxItemsPerUser | 50 | Max items per user per trade (0 = unlimited) |
API
Initiating a Trade
// Client-side via TradeHub
var session = await connection.InvokeAsync<TradeSession>("InitiateTradeAsync", targetUserId);
Adding Items
// Client-side via TradeHub
await connection.InvokeAsync("AddItemAsync", itemId);
Removing Items
// Client-side via TradeHub
await connection.InvokeAsync("RemoveItemAsync", itemId);
Accepting and Completing
// Client-side via TradeHub
await connection.InvokeAsync("AcceptTradeAsync");
// Wait for "TradeCompleted" event
Cancelling
// Client-side via TradeHub
await connection.InvokeAsync("CancelTradeAsync");
Trade Status
| Status | Description |
|---|---|
Pending | Trade in progress, items can be added/removed |
Accepted | One party accepted (waiting for other) |
Completed | Both accepted, items transferred |
Cancelled | Trade cancelled by a user |
Expired | Trade timed out |
Failed | Item transfer failed |
Validation
The trade system validates:
- Ownership - User must own items they're adding
- Tradeability - Item type must have
IsTradeable = truein registry - Limits - Cannot exceed
MaxItemsPerUser - Status - Can only modify pending trades
- Rules - Custom game rules (e.g.
SameSeason,SoloSelfFound) are validated via theIRule<T>engine.
Rule Engine
The trade system uses an extensible rule engine to enforce game-specific restrictions without modifying core trade logic.
Interface
Rules implement the IRule<T> interface:
public interface IRule<in TContext>
{
Task ValidateAsync(TContext context);
}
Built-in Rules
- SameSeasonRule: Ensures both players are in the same season/league.
- SoloSelfFoundRule: Blocks trading for characters marked as "Solo Self-Found" (SSF).
Registration
Rules are registered in DI and automatically injected into the TradeGrain:
// Program.cs
builder.Services.AddSingleton<IRule<TradeRequestContext>, SameSeasonRule>();
builder.Services.AddSingleton<IRule<TradeRequestContext>, SoloSelfFoundRule>();
Real-Time Events
Trade events are published via Orleans Streams:
| Event | Trigger |
|---|---|
TradeStarted | Trade initiated |
ItemAdded | Item added to trade |
ItemRemoved | Item removed from trade |
TradeAccepted | One party accepted |
TradeCompleted | Both accepted, items transferred |
TradeCancelled | Trade cancelled |
TradeExpired | Trade timed out |
SignalR Integration
Clients can subscribe to trade updates:
// Join trade session
await connection.invoke("JoinTradeSession", tradeId);
// Receive updates
connection.on("TradeUpdate", (event) => {
console.log(event.EventType, event.Data);
});
// Leave session
await connection.invoke("LeaveTradeSession", tradeId);