OpenMU

This project aims to create an easy to use, extendable and customizable server for a MMORPG called "MU Online".

View on GitHub

Cash shop (in-game shop)

Status: in progress on the branch feature/cash-shop; see Roadmap for what’s missing. Packet group: 0xD2 (PacketType.CashShopGroup) Operator documentation: Cash shop, granting coins in the admin panel and through the API

Purpose

The cash shop of the Season 6 client (key X) sells items for three coins:

Coin Packet field Typically
WCoin (C) CashCredit bought with real money
WCoin (P) CashPrepaid bought with prepaid cards
Goblin Points TotalMileage earned by playing, or with “[+GP]” purchases

Every account has a balance of each coin and a storage with two tabs (normal and gift), which holds bought and gifted items until the player uses them. Using a storage item puts its items into the inventory.

Client constraints

These facts come from the open source client MuMain (src/source/GameShop, src/source/Network/Server/WSclient.cpp), as protocol facts — see rule 9 of the coding rules.

The catalog is local

The client reads the catalog from script files in Data\InGameShopScript\[SaleZone].[Year].[YearId]\ (IBSCategory.txt, IBSPackage.txt, IBSProduct.txt) and the banner from Data\InGameShopBanner\.... The server only sends the version to load; the download from Webzen’s server doesn’t work. The client ships with the script 512.2012.084 and the banner 583.2011.001.

Script format

One record per line, fields separated by @, CRLF, CP949 (only the Korean property names are non-ASCII). Sub-lists are separated by |.

IBSPackage.txt (27 fields), relevant fields:

Index Field Meaning
0 ProductDisplaySeq category; a package can be in several categories
2 PackageProductSeq package sequence number
3 PackageProductName name
5 Price price of the package (used by bundles)
8 SalesFlag 182 for sale, 183 not
9 GiftFlag 184 giftable, 185 not
19 ProductSeqList products of the package
20 InGamePackageID item code group * 512 + number
23 PriceSeqList price options; empty for bundles
25 CashType 508 WCoin (C), 509 WCoin (P), 0 Goblin Points

IBSProduct.txt (17 fields) has one line per property of a price option: 0 ProductSeq, 1 name, 2 property name, 3 value, 4 unit, 5 price, 6 PriceSeq, 13 item code, 14 PropertySeq, 16 UnitType. A price sequence belongs to exactly one product.

Protocol

All 0xD2 packets use a C1 header with sub code. The client structures are packed (#pragma pack(1)), so e.g. the doubles of the point info are at unaligned offsets.

Sub Client → server Server → client State
01 CashShopPointInfoRequest CashShopPointInfo (5 doubles) done
02 CashShopOpenState (IsClosed) CashShopOpenStateResponse (IsAllowed) done
03 CashShopItemBuyRequest CashShopItemBuyResult done
04 CashShopItemGiftRequest CashShopItemGiftResult done
05 CashShopStorageListRequest (page, 'S'/'G') 06 list header, then one 0D (storage) or 0E (gift) per item done
0A CashShopDeleteStorageItemRequest — never sent by the client
0B CashShopStorageItemConsumeRequest CashShopStorageItemConsumeResult done
0C — CashShopScriptVersion done
11/12 — period item count / expire time not implemented
13/14 CashShopEventItemListRequest event item count / list not implemented
15 — CashShopBannerVersion done

Client behaviour the server depends on:

Design

Components

Layer Files
Packets src/Network/Packets/ServerToClient/ServerToClientPackets.xml (D2 packets)
Data model src/DataModel/Configuration/CashShop*.cs, src/DataModel/Entities/CashShopStorageItem.cs, CashShopCoinGrant.cs, balances on Account.cs
Persistence src/Persistence/EntityFramework/Extensions/ModelBuilder/CashShopExtensions.cs, queries on IPlayerContext (EF and in-memory), migration AddCashShop
Initialization src/Persistence/Initialization/VersionSeasonSix/CashShopInitializer.cs, VersionSeasonSix/Items/CashShopItems.cs, Updates/AddCashShopUpdatePlugIn.cs
Game logic src/GameLogic/PlayerActions/CashShop/CashShopActions.cs; src/GameLogic/CashShop/ (CashShopFeaturePlugIn with CashShopSettings, CashShopVersionPlugIn, CashShopCoinExtensions, CashShopProductExtensions, CashShopProductDelivery with InventoryProductDelivery and the delivery plugins); src/GameLogic/PlugIns/ICashShop*PlugIn.cs; Player.IsCashShopOpen
Views src/GameLogic/Views/CashShop/ (ICashShopViewPlugIn, result enums), implemented by src/GameServer/RemoteView/CashShop/CashShopViewPlugIn.cs ([MinimumClient(6, 0)])
Handlers src/GameServer/MessageHandler/CashShop/ — a group handler and one sub handler per request
Admin panel / API src/Web/AdminPanel/: Services/CashShopCoinService.cs, API/CashShopController.cs, Pages/CashShopCoins.razor, Pages/CashShopCatalog.razor, Services/CashShopCatalogCheck.cs

Data model

Feature plugin and settings

CashShopFeaturePlugIn (IFeaturePlugIn) is the master switch: while it’s deactivated, CashShopFeaturePlugIn.GetSettings returns null, the versions aren’t sent and the shop can’t be opened. Its custom configuration, CashShopSettings, holds the behaviour settings: gifting on/off, gifts to the own account, and the maximum number of storage items per account (0 = unlimited). The catalog stays in the game configuration, because it refers to item definitions.

Plugin points

Custom plugins can hook into purchases (and gifts):

ICashShopProductDeliveryPlugIn delivers products when they’re used, see Product delivery.

GoblinPointsForPlayTimePlugIn (IPeriodicTaskPlugIn, disabled by default) gives Goblin Points for play time. As it runs on the game server, which owns the balance, it changes the balance directly instead of adding a grant.

Product delivery

A product is delivered when the player uses it from the storage, not when it’s bought. The storage is the delivery queue: gifts need nothing extra, and a refused delivery loses nothing.

CashShopProductDelivery.GetDelivery selects the delivery by the product’s item:

  1. the active ICashShopProductDeliveryPlugIn (IStrategyPlugIn<ItemIdentifier>) of the item, then the one of its item group (key without number);
  2. none, if a delivery plugin of the item or group is known but inactive: the product can’t be bought or used until it’s active again, instead of putting a useless item into the inventory. The plugin manager only knows the types of inactive plugins, so their keys are read from new instances;
  3. otherwise InventoryProductDelivery, which puts the items into the inventory. It isn’t a plugin and can’t be deactivated.

The selected delivery’s CanDeliver(product) must agree as well; all delivery plugins refuse time-limited products. A plugin’s DeliverAsync runs under the persistence lock before the storage item is removed, and changes nothing unless it succeeds. DeliveredAsync runs after the lock, for messages and view updates.

Built-in delivery plugins, active by default:

Plugin Item Use does Refused when
VaultExtensionDeliveryPlugIn Vault Expansion Certificate (14/163) Account.IsVaultExtended the vault is extended
InventoryExtensionDeliveryPlugIn Magic Backpack (14/162) Character.InventoryExtensions + 1 the configured maximum (default and limit 4, like the client) is reached
SummonerCharacterCardDeliveryPlugIn Summoner Character Card (14/91) unlocks the Summoner class it’s unlocked or not configured
RageFighterCharacterCardDeliveryPlugIn Rage Fighter Character Card (14/169) unlocks the Rage Fighter class it’s unlocked or not configured

The client learns the inventory extensions, and the server builds the inventory, when the character enters the world, so a new extension can be used after selecting the character again; the player is told so. The plugin UnlockSummonerAtLevel1 unlocks the Summoner for every account, so the Summoner card only makes sense when that plugin is deactivated.

Flows

Administration

Catalog initialization

New Season 6 databases get the catalog of the script 512.2012.084; existing ones get it with the optional update Add cash shop. The initializer does nothing when a configuration exists. The data follows the script with these rules:

CashShopItems defines the items of the delivery plugins (1x1), before the catalog is initialized. They have no item rules (ItemRules), because they never reach the inventory: a delivery plugin applies them, or they stay in the storage.

Packages with a product that can’t be delivered are initialized as not for sale. That leaves 37 packages for sale: jewel bundles, Kundun boxes and Cherry Blossom items for Goblin Points, and the character cards, the Magic Backpack, the Vault Expansion Certificate, the free tickets of Devil Square, Blood Castle and Illusion Temple and the reset fruits for WCoin (C) and (P). Most cash shop items, like seals, scrolls and talismans, aren’t defined in the item configuration yet.

The free tickets (13/46 Devil Square, 13/47 Blood Castle, 13/61 Illusion Temple and 13/121 Chaos Castle, which isn’t in the catalog) are created by EventTicketItems and set as Free Ticket Item of their mini game definitions, which can be changed in the admin panel. They allow to enter the event of any level, instead of its regular ticket, which is preferred when the player has both. Each entry costs one durability, and a ticket holds up to ten entries, like in the client. The package with ten tickets delivers one ticket with ten entries. The tickets can’t be traded, dropped or sold. The update AddFreeTicketItemsUpdatePlugIn adds them to an existing configuration, sets them as free tickets of the mini games which have none, and puts their packages on sale. It’s optional, so a server owner who wants to keep these packages off sale can deselect it.

The update also adds the items of the delivery plugins to an existing configuration, assigns them to its products which have no item, and puts those packages on sale.

Rules

Decisions

Decision Reason
Pre-fill the catalog from the client’s 512.2012.084 the stock client works out of the box
Coins as whole numbers the values are integral; doubles only exist in the protocol
The Add cash shop update is optional not every server offers a cash shop
The catalog editor only edits the server side the client script stays the source of truth for the stock client
No item templates (options) for products the client can’t show options of its script items; revisit with a server-side catalog
Deliver products on use, select the delivery by the product’s item the storage is the queue, gifts work; the client identifies products by item, like the consume handlers
A known but inactive delivery plugin blocks its products they’d be useless items in the inventory
A delivery is refused only on use, not on purchase a gift recipient’s account isn’t loaded; the product stays in the storage

Roadmap

  1. Items which expire: expiry on Item, the period packets D2 11/12, then selling time-limited products. This unlocks most of the catalog.
  2. Missing cash shop items: item definitions for seals, scrolls, talismans, tickets etc., with their effects.
  3. Event item list: D2 13/14.
  4. More ways to earn coins: e.g. Goblin Points for “[+GP]” purchases (an ICashShopPackageBoughtPlugIn), kills or events.
  5. More delivery plugins: e.g. Premium Service, once there is a premium model.
  6. Server-side catalog: the client queries or receives the catalog from the server instead of reading its script, with new …Extended packets for the open source client (rule 2); the original packets stay for the stock client. This needs display data in the catalog (categories, names, descriptions) and is where item templates become useful.

Before the pull request(s):

Testing

Tests Covers
tests/MUnique.OpenMU.Tests/CashShopActionsTest.cs opening, points, storage, buy, use (incl. rollback on a full inventory), gift, coin grants
tests/MUnique.OpenMU.Tests/CashShopProductDeliveryTest.cs delivery selection, inactive plugins, the built-in delivery plugins
tests/MUnique.OpenMU.Tests/GoblinPointsForPlayTimePlugInTest.cs Goblin Points for play time
tests/MUnique.OpenMU.Tests/CashShopRemoteViewTests.cs packet bytes, paging, storage indexes
tests/MUnique.OpenMU.Persistence.Initialization.Tests/CashShopUpdateTest.cs fresh database, update plugin incl. a catalog without the delivery items, idempotence
tests/MUnique.OpenMU.Web.Tests/Api/CashShopControllerTests.cs API status codes, references
tests/MUnique.OpenMU.Web.Tests/Pages/CashShopCoinsPageTests.cs coins page
tests/MUnique.OpenMU.Web.Tests/Pages/CashShopCatalogPageTests.cs catalog editor, CashShopCatalogCheck
tests/MUnique.OpenMU.Network.Packets.Tests generated packet structure tests

The tests use the in-memory persistence. Verified with the game client on PostgreSQL: the shop opens and lists the items; buying with WCoin (C), WCoin (P) and Goblin Points; using an own purchase, which moves it into the inventory; sending a gift and using it from the gift storage of the recipient; granting coins in the admin panel. Not verified yet: the delivery plugins, and changes of the catalog or the plugin settings in the admin panel. Loading the catalog from PostgreSQL needs IntervalAsTimeSpanJsonConverter, because PostgreSQL writes a product duration of a day or more as e.g. "7 days" into the JSON.

What can be bought: 21 of 205 packages (see Catalog initialization). The others have items OpenMU doesn’t define, or are time-limited.

Working on it