Trade System - Player-to-Player Trading

How the player-to-player trade UI is driven by the server state and rebuilt from scratch on every trade state change.

Setting up trade in the admin panel

Players trade the items they carry in their inventories: one player sends a trade request to another one, both place items in the trade window and the exchange happens when both confirm. The player-to-player trade is part of the inventory feature and works out of the box; the admin panel controls the items that exist in the game, how long a trade request waits for an answer and where the trade window is shown.

  1. Log in to the admin panel at /reldens-admin with an admin user (the path can be changed with the RELDENS_ADMIN_ROUTE_PATH environment variable).
  2. Open Features and check that the inventory feature is enabled (the default installation enables it).
  3. Open Items & Inventory > Items (/reldens-admin/items-item) to review the items the players can own and trade, and use Create New to add more (the item fields are explained in Items System Implementation).
  4. Open Settings > Config (/reldens-admin/config), type trade in the search box and click Filter.
  5. Edit trade/players/timeOut to change how many milliseconds a trade request waits for the other player to accept it, and trade/players/awaitTimeOut to turn that time limit on (1) or off (0). Click Save after each change.
  6. Optionally move the trade window with the ui/trade/* rows (see Configuration reference below).
  7. Restart the server so the new values are loaded.
Admin Panel - Items list Admin Panel - Config list filtered by trade

To test it, log in with two players in the same room: one of them selects the other player and clicks the trade button in the target box, the second player accepts the request, both add items to the trade and both confirm.

Configuration reference

The rows listed by the trade search, with the values installed by the basic configuration (all of them with scope client):

  • trade/players/awaitTimeOut (boolean, 1) - when enabled, a trade request that was not accepted within trade/players/timeOut is cancelled. With 0 the request is never cancelled automatically.
  • trade/players/timeOut (8000) - milliseconds a trade request waits for the answer (5000 when the row is missing).
  • ui/trade/x and ui/trade/y (5) - position of the trade window in pixels.
  • ui/trade/responsiveX and ui/trade/responsiveY (5) - position of the trade window as a percentage of the game screen, used instead of x and y when the responsive UI (ui/screen/responsive) is enabled.

The items offered in a trade are the ones the players hold in their inventories, see Item Entity for the item fields and Items & Inventory > Players Inventory to check or change what each player owns.

Code integration (advanced)

The sections below describe how the trade window is built from the server state and kept in sync for both players, for developers customizing it.

Server to Client Data Flow

The server sends to each player, via the TRADE_SHOW message:

  • playerToExchangeKey - the OTHER player's exchange key ('A' or 'B').
  • playerConfirmed - the OTHER player's confirmation status (for the display message).
  • myConfirmed - THIS player's confirmation status (for the button state logic).
  • items - THIS player's available inventory items (for column 1).
  • traderItemsData - the OTHER player's item data (for the column 3 display).
  • exchangeData - complete exchange object with the structure { 'A': {itemUid: qty}, 'B': {itemUid: qty} }.
  • isTradeEnd - boolean indicating if both players confirmed (triggers the trade completion).

The server determines playerToExchangeKey in InventoryMessageActions.sendExchangeUpdate() (lib/inventory/server/message-actions.js):

let playerToExchangeKey = ownerSessionId === playerTo.sessionId ? 'A' : 'B';

This identifies which exchange key belongs to the OTHER player (the one being sent data about in the message).

Three Column Structure

The trade UI displays three columns:

  • Column 1 (.my-items): my available inventory items - items I can add to the trade.
  • Column 2 (.pushed-to-trade): items I'M SENDING to the other player.
  • Column 3 (.got-from-trade): items I'M RECEIVING from the other player.

HTML structure:

  • .trade-container
    • .trade-row.trade-items-boxes-headers (column titles)
    • .trade-row.trade-items-boxes
      • .trade-player-col.trade-col-1.my-items (My Items)
      • .trade-player-col.trade-col-2.pushed-to-trade (Sending)
      • .trade-player-col.trade-col-3.got-from-trade (Receiving)
    • .trade-row.trade-confirm-actions
      • .cancel-action button
      • .disconfirm-action button
      • .confirm-action button
      • .player-confirmed span

Client Processing Flow

When the client receives the TRADE_SHOW message, TradeMessageHandler.showTradeBox() (lib/inventory/client/trade-message-handler.js) runs:

let traderExchangeKey = sc.get(this.message, 'playerToExchangeKey', 'A');
// my exchange key is the opposite to the received exchange key:
let myExchangeKey = 'A' === traderExchangeKey ? 'B' : 'A';
this.updateItemsList(items, container, exchangeData[myExchangeKey]);
this.updateMyExchangeData((exchangeData[myExchangeKey] || {}), items, myExchangeKey);
this.updateTraderExchangeData((exchangeData[traderExchangeKey] || {}), traderItemsData, traderExchangeKey);

Processing steps:

  1. Extract the exchange keys:
    • traderExchangeKey = value from playerToExchangeKey (OTHER player's key).
    • myExchangeKey = opposite of traderExchangeKey (THIS player's key).
  2. Update column 1 (my available items):
    • Call updateItemsList(items, container, exchangeData[myExchangeKey]).
    • Passes MY exchange data to check which items to hide (items with the full quantity in the trade).
  3. Update column 2 (items I'm sending):
    • Call updateMyExchangeData(exchangeData[myExchangeKey], items, myExchangeKey).
    • Shows the items from MY exchange key.
  4. Update column 3 (items I'm receiving):
    • Call updateTraderExchangeData(exchangeData[traderExchangeKey], traderItemsData, traderExchangeKey).
    • Shows the items from the TRADER's exchange key.

HTML Recreation Pattern

Every TRADE_SHOW message triggers a full HTML recreation, in TradeMessageHandler.updateItemsList():

container.innerHTML = this.createTradeContainer(tradeItems);
this.activateItemsBoxActions(tempItemsList);
this.activateConfirmButtonAction(sc.get(this.message, 'exchangeData', {}));

The server sends TRADE_SHOW to BOTH players simultaneously when:

  • An item is added / removed.
  • A player confirms / disconfirms.
  • ANY trade state changes.

Implications:

  • All buttons and DOM elements are DESTROYED and RECREATED each time.
  • Event listeners must be re-attached after every update (activateItemsBoxActions() and activateConfirmButtonAction() run right after the HTML is set).
  • The server state is the ONLY source of truth.
  • No client-side state should be maintained between updates.

Button State Logic

The server sends the confirmation statuses:

  • playerConfirmed - OTHER player's confirmation status (for the display message "Player X CONFIRMED").
  • myConfirmed - THIS player's confirmation status (for the button state logic).

TradeMessageHandler.updateItemsList() calls:

this.activateConfirmButtonAction(sc.get(this.message, 'exchangeData', {}));

And the button states are calculated in TradeMessageHandler.activateConfirmButtonAction():

let myExchangeKey = sc.get(this.message, 'playerToExchangeKey', 'A');
let traderExchangeKey = 'A' === myExchangeKey ? 'B' : 'A';
let myExchangeData = exchangeData[myExchangeKey] || {};
let traderExchangeData = exchangeData[traderExchangeKey] || {};
let myHasItems = 0 < Object.keys(myExchangeData).length;
let traderHasItems = 0 < Object.keys(traderExchangeData).length;
let hasAnyItems = myHasItems || traderHasItems;
let iConfirmed = sc.get(this.message, 'myConfirmed', false);
  • Confirm button: disabled = iConfirmed || !hasAnyItems.
    • Disabled when the player already confirmed OR there are no items in the trade.
    • Enabled when the player did not confirm AND there are items in the trade.
  • Disconfirm button: disabled = !iConfirmed.
    • Disabled when the player did not confirm.
    • Enabled when the player already confirmed.

Each player sees their own button states based on their own confirmation status from the myConfirmed field.

Example Data Flow

Scenario: Player A (key='A') adds itemX to the trade, then confirms.

After adding the item

Server state:

exchangeData = {
  'A': {itemX: 1},
  'B': {}
}
confirmations = {
  'A': false,
  'B': false
}

Message sent to Player A:

{
  playerToExchangeKey: 'B',
  playerConfirmed: false,
  myConfirmed: false,
  exchangeData: { 'A': {itemX: 1}, 'B': {} },
  items: {...},
  traderItemsData: {}
}

Player A UI state:

  • Column 1: shows Player A's available items (itemX hidden if the full quantity was placed).
  • Column 2: shows exchangeData['A'] = {itemX: 1} (sending to Player B).
  • Column 3: shows exchangeData['B'] = {} (receiving from Player B - empty).
  • Confirm button: ENABLED (myConfirmed=false, hasAnyItems=true).
  • Disconfirm button: DISABLED (myConfirmed=false).

Message sent to Player B:

{
  playerToExchangeKey: 'A',
  playerConfirmed: false,
  myConfirmed: false,
  exchangeData: { 'A': {itemX: 1}, 'B': {} },
  items: {...},
  traderItemsData: {itemX: {...}}
}

Player B UI state:

  • Column 1: shows Player B's available items.
  • Column 2: shows exchangeData['B'] = {} (sending to Player A - empty).
  • Column 3: shows exchangeData['A'] = {itemX: 1} (receiving from Player A).
  • Display message: no confirmation message (playerConfirmed=false).
  • Confirm button: ENABLED (myConfirmed=false, hasAnyItems=true).
  • Disconfirm button: DISABLED (myConfirmed=false).

After Player A clicks confirm

The server updates the confirmations:

confirmations = {
  'A': true,
  'B': false
}

Message sent to Player A:

{
  playerToExchangeKey: 'B',
  playerConfirmed: false,
  myConfirmed: true,
  // ... rest same
}

Player A UI state:

  • Confirm button: DISABLED (myConfirmed=true).
  • Disconfirm button: ENABLED (myConfirmed=true).

Message sent to Player B:

{
  playerToExchangeKey: 'A',
  playerConfirmed: true,
  myConfirmed: false,
  // ... rest same
}

Player B UI state:

  • Display message: "Player A CONFIRMED" (playerConfirmed=true).
  • Confirm button: ENABLED (myConfirmed=false).
  • Disconfirm button: DISABLED (myConfirmed=false).

Item Actions Display

CSS behavior in theme/default/css/items-system.scss, inside .trade-container .trade-row.trade-items-boxes:

.my-items,
.pushed-to-trade,
.got-from-trade {

    .trade-item .actions-container.trade-actions {
        display: block;
    }

}

Important: the trade actions are always visible in the three columns. The only toggle is the item info box, handled on the client by ItemDisplayEnricher.activateItemInfoToggle(), which adds / removes the item-info-visible class on the item box.

CSS Styling

All the rules below are in theme/default/css/items-system.scss.

  • Player confirmed message (.trade-container .player-confirmed):
    • Styled block with border and background.
    • Empty state handling with transparent background.
  • Button layout (.trade-container .trade-confirm-actions):
    • Flexbox with center justification.
    • No float positioning.
  • Remove button (.trade-item .trade-action-remove):
    • Absolute positioning at right: -10px.
    • Icon size 20px.
  • Item actions (.trade-container .trade-row.trade-items-boxes, .trade-item .actions-container.trade-actions):
    • Displayed as block in the three columns.
    • Inside the NPC trader dialog box (.ui-dialog-box.type-trader.trade-in-progress .item-box.trade-item) they are laid out inline as a flex row.

Files Involved

Client:

  • lib/inventory/client/trade-message-handler.js - main trade UI handler.
  • lib/inventory/client/trade-items-helper.js - item instance creation.
  • lib/inventory/client/item-display-enricher.js - item info toggle and trade action buttons.
  • theme/default/css/items-system.scss - trade UI styles.
  • theme/default/assets/features/inventory/templates/trade-player-container.html - trade UI template.

Server:

  • lib/inventory/server/message-actions.js - trade message handling.
  • lib/inventory/server/exchange/processor.js - exchange operations (init, add, remove, confirm).
  • lib/inventory/server/exchange/player-processor.js - player-to-player confirm / disconfirm operations.

Constants:

  • lib/objects/constants.js - trade action constants (ADD, REMOVE, CONFIRM, DISCONFIRM).
  • lib/inventory/constants.js - inventory action constants (TRADE_START, TRADE_SHOW, etc.).

Translations:

  • lib/inventory/client/snippets/en_US.js - UI labels (trade.actions.disconfirm).

Related Documentation

Go Up