mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[econ] fighting inflation
This commit is contained in:
Vendored
+8
@@ -10,3 +10,11 @@ declare namespace Cloudflare {
|
|||||||
mainModule: MainModule
|
mainModule: MainModule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vite serves a `?raw` import as the file's text. Used by the tests to read a data
|
||||||
|
// migration (0032, the custom-item price cap) as a STRING and run its UPDATE against the
|
||||||
|
// schema they build from SCHEMA_DDL, which the migration files never touch.
|
||||||
|
declare module '*.sql?raw' {
|
||||||
|
const content: string
|
||||||
|
export default content
|
||||||
|
}
|
||||||
|
|||||||
@@ -361,7 +361,11 @@ export const CheerPlayerResponse = z.object({
|
|||||||
export const CreateCustomAvatarItemMetadata = z.object({
|
export const CreateCustomAvatarItemMetadata = z.object({
|
||||||
Name: z.string(),
|
Name: z.string(),
|
||||||
Description: z.string().optional(),
|
Description: z.string().optional(),
|
||||||
Price: z.number().int().min(0).max(1000).optional().describe('Tokens, 0–1000; 0 when left out'),
|
Price: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.optional()
|
||||||
|
.describe('Tokens, 0–1000 (MAX_PLAYER_ITEM_PRICE); 0 when left out'),
|
||||||
BaseAvatarItemId: z.number().int(),
|
BaseAvatarItemId: z.number().int(),
|
||||||
BaseAvatarItemColor: z.string().describe('Hex colour, e.g. `#F55C1A`'),
|
BaseAvatarItemColor: z.string().describe('Hex colour, e.g. `#F55C1A`'),
|
||||||
Accessibility: z.number().int().optional(),
|
Accessibility: z.number().int().optional(),
|
||||||
@@ -434,7 +438,7 @@ export const CustomAvatarItemDto = z.object({
|
|||||||
export const UpdateCustomAvatarItemRequest = z.object({
|
export const UpdateCustomAvatarItemRequest = z.object({
|
||||||
Name: z.string().nullable().optional(),
|
Name: z.string().nullable().optional(),
|
||||||
Description: z.string().nullable().optional(),
|
Description: z.string().nullable().optional(),
|
||||||
Price: z.number().int().min(0).max(1000).nullable().optional().describe('Tokens, 0–1000'),
|
Price: z.number().int().nullable().optional().describe('Tokens, 0–1000 (MAX_PLAYER_ITEM_PRICE)'),
|
||||||
Accessibility: z.number().int().nullable().optional(),
|
Accessibility: z.number().int().nullable().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
SYSTEM_SENDER_ID,
|
SYSTEM_SENDER_ID,
|
||||||
THREAD_SCHEMA_DDL,
|
THREAD_SCHEMA_DDL,
|
||||||
} from '../../../../chat/src/thread-db'
|
} from '../../../../chat/src/thread-db'
|
||||||
|
import priceCapSql from '../../../migrations/0032_custom_avatar_item_price_cap.sql?raw'
|
||||||
import { banEvasionMatch, resolveBan } from '../../bans-db'
|
import { banEvasionMatch, resolveBan } from '../../bans-db'
|
||||||
import {
|
import {
|
||||||
createCustomAvatarItem,
|
createCustomAvatarItem,
|
||||||
@@ -47,7 +48,6 @@ import {
|
|||||||
MAX_PLAYER_ITEM_PRICE,
|
MAX_PLAYER_ITEM_PRICE,
|
||||||
} from '../../custom-avatar-items-db'
|
} from '../../custom-avatar-items-db'
|
||||||
import { customAvatarItemRowLiteral } from '../../custom-avatar-items-load'
|
import { customAvatarItemRowLiteral } from '../../custom-avatar-items-load'
|
||||||
import priceCapSql from '../../../migrations/0032_custom_avatar_item_price_cap.sql?raw'
|
|
||||||
import {
|
import {
|
||||||
countGoing,
|
countGoing,
|
||||||
SCHEMA_DDL as EVENTS_SCHEMA_DDL,
|
SCHEMA_DDL as EVENTS_SCHEMA_DDL,
|
||||||
@@ -4639,7 +4639,9 @@ describe('custom avatar items', () => {
|
|||||||
expect((await create(10.5)).status).toBe(400)
|
expect((await create(10.5)).status).toBe(400)
|
||||||
// Nothing was written for the refused ones.
|
// Nothing was written for the refused ones.
|
||||||
expect(
|
expect(
|
||||||
await env.DB.prepare(`SELECT count(*) AS n FROM custom_avatar_item WHERE name_lower = 'pricey'`)
|
await env.DB.prepare(
|
||||||
|
`SELECT count(*) AS n FROM custom_avatar_item WHERE name_lower = 'pricey'`
|
||||||
|
)
|
||||||
.first<{ n: number }>()
|
.first<{ n: number }>()
|
||||||
.then((r) => r!.n)
|
.then((r) => r!.n)
|
||||||
).toBe(0)
|
).toBe(0)
|
||||||
@@ -4647,9 +4649,13 @@ describe('custom avatar items', () => {
|
|||||||
// The cap itself is allowed, inclusive; a missing price is still 0.
|
// The cap itself is allowed, inclusive; a missing price is still 0.
|
||||||
const atCap = await create(1000)
|
const atCap = await create(1000)
|
||||||
expect(atCap.status).toBe(200)
|
expect(atCap.status).toBe(200)
|
||||||
const { Value } = (await atCap.json()) as { Value: { CustomAvatarItemId: string; Price: number } }
|
const { Value } = (await atCap.json()) as {
|
||||||
|
Value: { CustomAvatarItemId: string; Price: number }
|
||||||
|
}
|
||||||
expect(Value.Price).toBe(1000)
|
expect(Value.Price).toBe(1000)
|
||||||
expect(((await (await create(undefined)).json()) as { Value: { Price: number } }).Value.Price).toBe(0)
|
expect(
|
||||||
|
((await (await create(undefined)).json()) as { Value: { Price: number } }).Value.Price
|
||||||
|
).toBe(0)
|
||||||
|
|
||||||
// The edit is bound the same way, and a refused edit leaves the row as it was.
|
// The edit is bound the same way, and a refused edit leaves the row as it was.
|
||||||
const url = `${ORIGIN}/api/customAvatarItems/v1/${Value.CustomAvatarItemId}`
|
const url = `${ORIGIN}/api/customAvatarItems/v1/${Value.CustomAvatarItemId}`
|
||||||
@@ -4702,7 +4708,7 @@ describe('custom avatar items', () => {
|
|||||||
RankedEntityId: firstPartyId,
|
RankedEntityId: firstPartyId,
|
||||||
CreatorAccountId: 1,
|
CreatorAccountId: 1,
|
||||||
Name: 'Studio Wings',
|
Name: 'Studio Wings',
|
||||||
Description: null,
|
Description: '',
|
||||||
Price: 6000,
|
Price: 6000,
|
||||||
Accessibility: 1,
|
Accessibility: 1,
|
||||||
OutfitType: 100,
|
OutfitType: 100,
|
||||||
@@ -4736,8 +4742,14 @@ describe('custom avatar items', () => {
|
|||||||
expect((await price(dear.CustomAvatarItemId)).Price).toBe(1000)
|
expect((await price(dear.CustomAvatarItemId)).Price).toBe(1000)
|
||||||
// The lowered row is marked modified, as an edit would mark it; the untouched ones aren't.
|
// The lowered row is marked modified, as an edit would mark it; the untouched ones aren't.
|
||||||
expect((await price(dear.CustomAvatarItemId)).ModifiedAt).not.toBe(dear.ModifiedAt)
|
expect((await price(dear.CustomAvatarItemId)).ModifiedAt).not.toBe(dear.ModifiedAt)
|
||||||
expect(await price(fair.CustomAvatarItemId)).toMatchObject({ Price: 1000, ModifiedAt: fair.ModifiedAt })
|
expect(await price(fair.CustomAvatarItemId)).toMatchObject({
|
||||||
expect(await price(cheap.CustomAvatarItemId)).toMatchObject({ Price: 50, ModifiedAt: cheap.ModifiedAt })
|
Price: 1000,
|
||||||
|
ModifiedAt: fair.ModifiedAt,
|
||||||
|
})
|
||||||
|
expect(await price(cheap.CustomAvatarItemId)).toMatchObject({
|
||||||
|
Price: 50,
|
||||||
|
ModifiedAt: cheap.ModifiedAt,
|
||||||
|
})
|
||||||
expect((await price(firstPartyId)).Price).toBe(6000)
|
expect((await price(firstPartyId)).Price).toBe(6000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export const UNLISTED_SKIN_RARITY = 50
|
|||||||
* What a GOLD skin costs — any skin whose name carries `(Gold)`, thirteen of them, all from the
|
* What a GOLD skin costs — any skin whose name carries `(Gold)`, thirteen of them, all from the
|
||||||
* capture. A prestige price, and the same for a subscriber: no discount, no sale.
|
* capture. A prestige price, and the same for a subscriber: no discount, no sale.
|
||||||
*/
|
*/
|
||||||
export const GOLD_SKIN_PRICE = 1_000_000
|
export const GOLD_SKIN_PRICE = 5_000_000
|
||||||
|
|
||||||
/** Whether a listing is a gold skin — a skin whose name says `(Gold)`. */
|
/** Whether a listing is a gold skin — a skin whose name says `(Gold)`. */
|
||||||
export const isGoldSkin = (listing: StoreListing): boolean =>
|
export const isGoldSkin = (listing: StoreListing): boolean =>
|
||||||
|
|||||||
@@ -2816,12 +2816,17 @@ describe('econ endpoints', () => {
|
|||||||
listing.PurchasableItemId
|
listing.PurchasableItemId
|
||||||
)
|
)
|
||||||
// A skin's or a consumable's thumbnail is blanked — the dump names a PNG per item that
|
// A skin's or a consumable's thumbnail is blanked — the dump names a PNG per item that
|
||||||
// this server does not have — and nothing else about a drop changes.
|
// this server does not have — and a drop with Context -1 (the hair dyes and the dice
|
||||||
|
// skins) is served with Context 0. Nothing else about a drop changes.
|
||||||
const blanked =
|
const blanked =
|
||||||
listing.GiftDrop.EquipmentModificationGuid !== '' ||
|
listing.GiftDrop.EquipmentModificationGuid !== '' ||
|
||||||
listing.GiftDrop.ConsumableItemDesc !== ''
|
listing.GiftDrop.ConsumableItemDesc !== ''
|
||||||
|
const expectedDrop = {
|
||||||
|
...listing.GiftDrop,
|
||||||
|
Context: listing.GiftDrop.Context === -1 ? 0 : listing.GiftDrop.Context,
|
||||||
|
}
|
||||||
expect(served.GiftDrop).toEqual(
|
expect(served.GiftDrop).toEqual(
|
||||||
blanked ? { ...listing.GiftDrop, ThumbnailImageName: '' } : listing.GiftDrop
|
blanked ? { ...expectedDrop, ThumbnailImageName: '' } : expectedDrop
|
||||||
)
|
)
|
||||||
// The other edit to a listing: the dump was taken mid-sale (80% off nearly everything,
|
// The other edit to a listing: the dump was taken mid-sale (80% off nearly everything,
|
||||||
// no end date), and the client posts the sale price it computes — which this server,
|
// no end date), and the client posts the sale price it computes — which this server,
|
||||||
@@ -2865,7 +2870,7 @@ describe('econ endpoints', () => {
|
|||||||
expect(skinsJson.every((sk) => soldGuids.includes(sk.ModificationGuid))).toBe(true)
|
expect(skinsJson.every((sk) => soldGuids.includes(sk.ModificationGuid))).toBe(true)
|
||||||
expect(soldSkins.every((i) => i.GiftDrop.ThumbnailImageName === '')).toBe(true)
|
expect(soldSkins.every((i) => i.GiftDrop.ThumbnailImageName === '')).toBe(true)
|
||||||
expect(soldSkins.length).toBeGreaterThan(unlisted.length)
|
expect(soldSkins.length).toBeGreaterThan(unlisted.length)
|
||||||
// A "(Gold)" skin — thirteen, all from the capture — is a prestige item: 1,000,000 tokens,
|
// A "(Gold)" skin — thirteen, all from the capture — is a prestige item: 5,000,000 tokens,
|
||||||
// the same for a subscriber, no sale. A "(Gold)" AVATAR item is priced as the dump has it.
|
// the same for a subscriber, no sale. A "(Gold)" AVATAR item is priced as the dump has it.
|
||||||
const goldSkins = soldSkins.filter((i) => i.GiftDrop.FriendlyName.includes('(Gold)'))
|
const goldSkins = soldSkins.filter((i) => i.GiftDrop.FriendlyName.includes('(Gold)'))
|
||||||
expect(goldSkins.length).toBeGreaterThan(10)
|
expect(goldSkins.length).toBeGreaterThan(10)
|
||||||
|
|||||||
@@ -193541,14 +193541,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193594,14 +193594,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193647,14 +193647,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193912,14 +193912,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -195078,14 +195078,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -198205,14 +198205,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -198629,14 +198629,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -199583,14 +199583,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -200272,14 +200272,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -200855,14 +200855,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -201650,14 +201650,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -201756,14 +201756,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -202074,14 +202074,14 @@
|
|||||||
"Prices": [
|
"Prices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SubscriberPrices": [
|
"SubscriberPrices": [
|
||||||
{
|
{
|
||||||
"CurrencyType": 2,
|
"CurrencyType": 2,
|
||||||
"Price": 1000000,
|
"Price": 5000000,
|
||||||
"StorefrontSaleData": null
|
"StorefrontSaleData": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import type { StoreListing } from '../../../../apps/econ/src/catalog-load'
|
|||||||
* thumbnails every skin listing — the game's own 99 and the appended ones alike — and every
|
* thumbnails every skin listing — the game's own 99 and the appended ones alike — and every
|
||||||
* consumable listing has its `ThumbnailImageName` set to `""`. The dump names a
|
* consumable listing has its `ThumbnailImageName` set to `""`. The dump names a
|
||||||
* PNG per item that this server does not have.
|
* PNG per item that this server does not have.
|
||||||
* gold a skin whose name carries `(Gold)` sells at `GOLD_SKIN_PRICE`, 1,000,000 tokens,
|
* gold a skin whose name carries `(Gold)` sells at `GOLD_SKIN_PRICE`, 5,000,000 tokens,
|
||||||
* subscriber price the same: no discount, no sale.
|
* subscriber price the same: no discount, no sale.
|
||||||
* context a drop whose `Context` is `NO_CONTEXT` (-1) gets 0. The dump carries -1 on
|
* context a drop whose `Context` is `NO_CONTEXT` (-1) gets 0. The dump carries -1 on
|
||||||
* exactly 46 listings — the 22 permanent hair dyes and the 24 dice skins, the
|
* exactly 46 listings — the 22 permanent hair dyes and the 24 dice skins, the
|
||||||
|
|||||||
Reference in New Issue
Block a user