> For the complete documentation index, see [llms.txt](https://axiomemu.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://axiomemu.gitbook.io/docs/data/plugins/market.md).

# Market

Market (personal store v2) - 50-row player stores, price in one of 7 currencies, server-wide search, commission, sale log and offline store

The Market replaces the vanilla personal store. Every character owns a store of up to 50 rows, one item per row. The player puts items from the inventory into the store, gives each row a price in **one** currency, then opens the store (title over the head). Other players buy in person (click the seller) or from the Market search window. Zen and jewel sales are paid into the store **wallet**; the seller takes them out with **Collect**. WCoinC, WCoinP and GoblinPoint sales go straight to the seller's account.

{% hint style="info" %}
Free and always on. No license module needed. Set `Enabled="0"` to turn it off.
{% endhint %}

## Player Side

| Action                      | How                                                                                      |
| --------------------------- | ---------------------------------------------------------------------------------------- |
| Open the search window      | **S** key, or type `/market`. The S key does nothing in Illusion Temple and Castle Siege |
| Open My Store               | Store button in the inventory window, or `/personalshop`                                 |
| Open another player's store | Click the seller, or `/purchase <name>`                                                  |
| Offline store               | **Go Offline** button in My Store (see [Offline Store](#offline-store))                  |

## File Location

```
Data/Plugins/Market.xml
```

A missing file is not an error: the Market runs **enabled** with built-in defaults (see [Currency](#currency) - the built-in price caps differ from the shipped file).

Changes need a GameServer restart. The plugin reload does not re-read this file.

## Dependencies

| Depends On                                    | Purpose                                                                                                                                                          |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [ItemMove.xml](/docs/data/item/itemmove.md)   | `AllowStore="0"` items cannot go into a store                                                                                                                    |
| [Common.dat](/docs/gameserver/common-dat.md)  | `TradeItemBlock*` rules also block listing. `WriteMarketLog` turns the Market log on. `OnlineRewardOfflineSystems` decides if offline stores earn online rewards |
| [MapManager.xml](/docs/data/mapmanager.md)    | Map numbers for `<BlockedMaps>`. Safe zone tiles for the safe-zone checks                                                                                        |
| [ItemStack.xml](/docs/data/item/itemstack.md) | Single jewels paid out by Collect stack up to the jewel's stack size                                                                                             |
| [CashShop](/docs/data/cashshop.md)            | WCoinC / WCoinP / GoblinPoint balances (`CashShopData` table)                                                                                                    |
| DataServer                                    | Column `Character.MarketStore` and table `dbo.MarketSaleLog`, created by `DataServer/SQL/Migrations/0002_market.sql`                                             |

## Database Setup

The Market needs database schema **2**. Apply `DataServer/SQL/Migrations/0002_market.sql` (the [Axiom Server Manager](/docs/guides/updating-with-server-manager.md) applies it for you). The script:

* adds `Character.MarketStore` (store rows + wallet, saved with the inventory in the same transaction)
* creates `dbo.MarketSaleLog` (one row per sale)
* **drops** the vanilla `dbo.PShopItemValue` table (no longer used)

{% hint style="danger" %}
The DataServer refuses to start on a database older than schema 2. The log shows `[Schema] Database schema is 1, this DataServer needs 2` and `NOT STARTING`.
{% endhint %}

## Configuration Sections

```xml
<Market Enabled="1" MaxRows="50" OpenMinLevel="6" SearchSafeZoneOnly="1" StoreViewRange="10"
        SearchPageSize="20" SearchCooldownMs="500" SaleLogKeep="50">
    <BlockedMaps></BlockedMaps>
    <Offline Enabled="1" SafeZoneOnly="1" MaxMinutes="1440" CostPerHour="0"/>
    <Currency Id="1" Enabled="1" Commission="0" Min="1" Max="0"/>
    ...
</Market>
```

### Market

| Attribute            | Type | Range | Default | Effect                                                                                            |
| -------------------- | ---- | ----- | ------- | ------------------------------------------------------------------------------------------------- |
| `Enabled`            | 0/1  | -     | 1       | 0 = Market off. Store, search, buy, withdraw and collect are all refused                          |
| `MaxRows`            | int  | 1-50  | 50      | Store rows per character                                                                          |
| `OpenMinLevel`       | int  | -     | 6       | Character level needed to open a store                                                            |
| `SearchSafeZoneOnly` | 0/1  | -     | 1       | 1 = the search window (search and buying from it) only works inside a safe zone                   |
| `StoreViewRange`     | int  | 0+    | 10      | Tiles a buyer may stand from the seller to open the store in person. 0 = anywhere on the same map |
| `SearchPageSize`     | int  | 5-25  | 20      | Maximum rows per search page. The window asks for as many as it shows                             |
| `SearchCooldownMs`   | int  | 100+  | 500     | Minimum milliseconds between two searches of one player                                           |
| `SaleLogKeep`        | int  | 1-50  | 50      | Sales kept per seller for the **Sold** tab                                                        |

Out-of-range values are clamped to the nearest limit.

### BlockedMaps

Comma-separated map numbers where a store cannot be opened. Spaces are ignored.

```xml
<BlockedMaps>0, 2, 3</BlockedMaps>
```

These maps are **always** blocked, even with an empty list:

| Map                       | Numbers   |
| ------------------------- | --------- |
| Chaos Castle 1-7          | 18-23, 53 |
| Illusion Temple 1-6       | 45-50     |
| Silent (Custom Arena map) | 40        |

{% hint style="warning" %}
Castle Siege (map 30) is **not** blocked by default, even though the comment in the shipped file says so. Add `30` to `<BlockedMaps>` if you want it blocked.
{% endhint %}

### Offline

| Attribute      | Type | Range   | Default | Effect                                                          |
| -------------- | ---- | ------- | ------- | --------------------------------------------------------------- |
| `Enabled`      | 0/1  | -       | 1       | 0 = no offline store                                            |
| `SafeZoneOnly` | 0/1  | -       | 1       | 1 = going offline only works inside a safe zone                 |
| `MaxMinutes`   | int  | 0-43200 | 1440    | Minutes a store may stay offline. 0 = no limit. 43200 = 30 days |
| `CostPerHour`  | zen  | 0+      | 0       | Zen per hour. 0 = free                                          |

### Currency

One element per currency. A currency with no element keeps its built-in rule.

| Id | Currency       | Paid to          |
| -- | -------------- | ---------------- |
| 1  | Zen            | Store wallet     |
| 2  | Jewel of Bless | Store wallet     |
| 3  | Jewel of Soul  | Store wallet     |
| 4  | Jewel of Chaos | Store wallet     |
| 5  | WCoinC         | Seller's account |
| 6  | WCoinP         | Seller's account |
| 7  | GoblinPoint    | Seller's account |

| Attribute    | Type | Range | Default | Effect                                                            |
| ------------ | ---- | ----- | ------- | ----------------------------------------------------------------- |
| `Id`         | int  | 1-7   | -       | Currency. Other values are skipped                                |
| `Enabled`    | 0/1  | -     | 1       | 0 = players cannot pick this currency for a listing               |
| `Commission` | int  | 0-100 | 0       | Percent the server keeps from each sale. The seller gets the rest |
| `Min`        | int  | 1+    | 1       | Lowest allowed price                                              |
| `Max`        | int  | 0+    | 0       | Highest allowed price. 0 = hard maximum                           |

Jewel prices count **single jewels**. A bundle counts as 10, 20 or 30 by its level.

Hard maximum (used when `Max` is 0 or too high):

| Currency         | Hard maximum              |
| ---------------- | ------------------------- |
| Zen              | 9,000,000,000,000,000,000 |
| Jewels and coins | 2,147,483,647             |

Built-in rule when the element (or the whole file) is missing: `Enabled 1`, `Commission 0`, `Min 1`, `Max` = zen hard maximum / **10,000** for jewels / **1,000,000** for coins. Note that an element with `Max="0"` gives the hard maximum, not these values.

## Important Behavior

### Store rows

* Items go into the store and come out only while the store is **closed**. Prices also change only while it is closed.
* A row may sit in the store with no price (unlisted). Only priced rows are shown to buyers.
* Opening needs: `OpenMinLevel`, a map that is not blocked, at least one priced row, no trade / duel / other window open, and no item lock (`/lock`).
* Logging out closes the store (unless it went offline). Rows and wallet stay saved. The player reopens it next time.
* The store closes by itself when the last priced row is sold.
* Lowering `MaxRows` does not delete anything. Rows above the new limit are not sold or shown to buyers, but the owner still sees them and can take them out.

### What cannot be listed

Refused when putting the item in, when pricing it and again at the moment of sale:

* `AllowStore="0"` in [ItemMove.xml](/docs/data/item/itemmove.md)
* event items and rental (time-limited) items
* lucky items when `TradeItemBlockLucky = 1` in [Common.dat](/docs/gameserver/common-dat.md)
* excellent / set / harmony items blocked by the `TradeItemBlock*` rules in Common.dat (GMs bypass these)
* pentagrams with errtels in their sockets
* items bound to the character (item extension binding, such as a bound inscription)
* any item while the player has an item lock (`/lock`) on

When a rule changes, rows that are no longer allowed become unlisted the next time the owner opens the store. The item stays in the store.

### Buying

* **In person**: same map, within `StoreViewRange` tiles of the seller.
* **From search**: inside a safe zone when `SearchSafeZoneOnly = 1`. Distance does not matter.
* Search only lists stores on **the same GameServer**. Own rows never show in search.
* Search filters: name text, currency, minimum item level, excellent, ancient, socket, luck, skill, option. Sort: newest, price low-high, price high-low. When a store changes, every open search page that shows it refreshes.
* The buyer must have room in the inventory. If the row changed after the buyer saw it (sold, repriced, withdrawn) the buy is refused.
* One store action or buy per 150 ms per player.

### Jewel payment

The buyer must pay the **exact** price from single jewels and bundles. Bundles are used first (30, 20, 10), then singles. If the exact amount cannot be made from what the buyer holds, the buy is refused, even if the total is higher. Example: price 15, buyer holds one bundle of 20 and nothing else = refused.

### Wallet and Collect

* Zen and jewel sales go into the store wallet (price minus commission).
* **Collect** moves zen to the character (up to the zen cap) and creates jewels in the inventory, bundles first, then singles. If the inventory fills up, the rest stays in the wallet.
* A sale is refused when it would push the seller's wallet past its cap (zen cap, or 2,147,483,647 jewels per type).

### Coin sales (escrow)

WCoinC, WCoinP and GoblinPoint sales run through the DataServer:

1. The DataServer takes the coins from the buyer and writes a `MarketSaleLog` row with `Status = 0` (pending).
2. The GameServer moves the item and tells the DataServer to pay the seller (`Status = 1`).
3. If the buyer or seller left, the row changed, or the buyer's inventory is full, the buyer is refunded (`Status = 2`) and the item stays in the store.
4. If the DataServer does not answer within 15 seconds, the row is released. A late answer is refunded.

The DataServer checks every 60 seconds for rows still at `Status = 0` after 10 minutes (GameServer crash, link drop):

| Item is found...         | Result                                                      |
| ------------------------ | ----------------------------------------------------------- |
| in the seller's store    | Buyer refunded, `Status = 2`                                |
| in the buyer's inventory | Seller paid, `Status = 1`                                   |
| anywhere else            | `Status = 3`, red log line. **A GM must settle it by hand** |

### Sale log

Each seller sees the last `SaleLogKeep` sales in the **Sold** tab. Older sold rows are deleted. Pending (`Status 0`) and stuck (`Status 3`) rows are never deleted: they are the proof of payment.

To show listings and sales on a website, see [Website: Market](/docs/guides/website-market.md).

### Offline Store

**Go Offline** in My Store logs the player out. The character stays in the world with the store open and keeps selling, like `/offattack`.

Needs: `Offline Enabled = 1`, the store already open with priced rows, a safe zone when `SafeZoneOnly = 1`, MuHelper and `/offattack` not running, no coin purchase in progress, and enough zen for the first hour.

* The first hour is paid when going offline, then every hour. The cost is taken from the wallet zen first, then the character's zen.
* Online-time rewards are paused while offline unless `OnlineRewardOfflineSystems = 1` in Common.dat.

The offline store ends when:

* the player logs in again
* the store sells out or is closed
* `MaxMinutes` passes
* the hourly cost cannot be paid
* a GM disconnects the character, or the server shuts down

Rows and wallet are saved in every case.

### Old personal store items

On the first login after the update, items left in the vanilla 8x4 personal store grid move into free store rows, unlisted. If the store is full, the rest stay in the old grid and a log line is written.

### Market log

`WriteMarketLog` in `GameServerInfo - Common.dat` (default `1`) writes every deposit, withdraw, price change, open/close, sale, collect and coin escrow step to the `Market` log folder. Refused requests are logged as `[Market][Reject][<action>]` with the reason (max 10 lines per second per player).

## Examples

{% tabs %}
{% tab title="Zen + jewels only, 5% fee" %}

```xml
<Market Enabled="1" MaxRows="50" OpenMinLevel="50" SearchSafeZoneOnly="1" StoreViewRange="10"
        SearchPageSize="20" SearchCooldownMs="500" SaleLogKeep="50">
    <BlockedMaps>30</BlockedMaps>
    <Offline Enabled="1" SafeZoneOnly="1" MaxMinutes="1440" CostPerHour="0"/>
    <Currency Id="1" Enabled="1" Commission="5" Min="1000" Max="0"/>
    <Currency Id="2" Enabled="1" Commission="5" Min="1" Max="10000"/>
    <Currency Id="3" Enabled="1" Commission="5" Min="1" Max="10000"/>
    <Currency Id="4" Enabled="1" Commission="5" Min="1" Max="10000"/>
    <Currency Id="5" Enabled="0"/>
    <Currency Id="6" Enabled="0"/>
    <Currency Id="7" Enabled="0"/>
</Market>
```

{% endtab %}

{% tab title="Paid offline store" %}
Offline stores cost 1,000,000 zen per hour, 12 hours max. Search works anywhere.

```xml
<Market Enabled="1" SearchSafeZoneOnly="0">
    <Offline Enabled="1" SafeZoneOnly="1" MaxMinutes="720" CostPerHour="1000000"/>
</Market>
```

{% endtab %}

{% tab title="Small stores, no offline" %}

```xml
<Market Enabled="1" MaxRows="20" OpenMinLevel="100" StoreViewRange="0" SaleLogKeep="20">
    <BlockedMaps>0, 2, 3</BlockedMaps>
    <Offline Enabled="0"/>
</Market>
```

{% endtab %}
{% endtabs %}

## Common Issues

{% hint style="warning" %}
**DataServer will not start, log says schema 1 / needs 2**: apply `DataServer/SQL/Migrations/0002_market.sql`, or run the update from the Server Manager.
{% endhint %}

{% hint style="warning" %}
**"Character.MarketStore is missing" in the DataServer log**: the column was removed by hand. The Market stays off for every character until it is back. The log names `add_market.sql`; the real script is `0002_market.sql`.
{% endhint %}

{% hint style="warning" %}
**Players cannot take items out after `Enabled="0"`**: with the Market off, withdraw and collect are refused too. Items and wallet are safe in the database. Turn it back on so players can empty their stores.
{% endhint %}

{% hint style="warning" %}
**Buyer has enough jewels but the buy fails**: jewel payment must be exact. The buyer needs the right mix of singles and bundles.
{% endhint %}

{% hint style="warning" %}
**Store opens in Castle Siege**: map 30 is not blocked by default. Add it to `<BlockedMaps>`.
{% endhint %}

{% hint style="warning" %}
**Coin sale stuck, buyer lost coins**: check the DataServer log and `dbo.MarketSaleLog`. Rows at `Status = 0` settle by themselves after 10 minutes. Rows at `Status = 3` need a GM: find the item, then refund the buyer or pay the seller by hand.
{% endhint %}

{% hint style="warning" %}
**Config change has no effect**: Market.xml is read only at GameServer start. Restart the GameServer.
{% endhint %}
