more stubs

This commit is contained in:
Devin Zuczek
2026-08-15 13:54:19 -04:00
parent 8ea0caa1e5
commit 11b037a2f1
67 changed files with 38566 additions and 2652 deletions
+11 -1
View File
@@ -2,7 +2,17 @@
CDN Worker served on the `cdn` subdomain (`cdn.recflare.net`) — a Hono app that streams
the binary blobs the client downloads while playing out of the shared `recflare-cdn` R2
bucket, plus the one bundled config file the loading screen reads.
bucket, plus the JSON config files the client reads from `/config/`.
`/config/:name` serves `static/config/<name>.json` verbatim — `RRPlusConfig_v3` and
`SkuConfig_v1` today — with or without the `.json` in the path, since the game configs that
point at these files carry the extension and the client's older config calls don't. `{name}`
IS the filename: that directory is uploaded as Workers static assets and read through the
ASSETS binding, so publishing a config is dropping in a file, and `run_worker_first` keeps
the asset server from answering ahead of the Worker (nothing is reachable at its own asset
path). The files go out byte-for-byte, BOM included. `/config/LoadingScreenTipData` stays a
route of its own, bundled rather than an asset, because its file is named differently from
its path.
Objects are keyed by prefix — `sigs/` (anti-cheat signatures), `room/` (saved room
scenes, and room images by their bare `ImageName`), `invention/` (invention data) — and
+85 -3
View File
@@ -1,5 +1,5 @@
import { Hono } from 'hono'
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
import { describeRoute, openAPIRouteHandler, resolver } from 'hono-openapi'
import { useWorkersLogger } from 'workers-tagged-logger'
import {
@@ -15,6 +15,7 @@ import {
assetResponses,
CONDITIONAL_HEADERS,
json,
JsonValue,
keyParam,
LoadingScreenTip,
ServiceStatus,
@@ -88,6 +89,44 @@ async function serveAsset(c: Context<App>, key: string) {
return new Response(object.body, { headers })
}
/**
* What may reach the ASSETS binding as a config filename: one path segment, no slashes,
* and `..` rejected outright below. A traversal is then a 404 from this worker rather than
* a request the asset server has to be trusted to refuse.
*/
const CONFIG_NAME = /^[A-Za-z0-9._-]+$/
/**
* Serve a file from `static/config/` through the ASSETS binding, by its own filename —
* whatever is in that directory, not just the JSON (a config may be an opaque binary blob
* named by GUID). `null` when nothing is published under that name.
*
* A name carrying no extension also resolves against `<name>.json`, because the same file
* is asked for both ways: the game configs that point at these carry the extension
* (`Econ.MakerAI.DayPass.Config` is `"SkuConfig_v1.json"`) while the client's older config
* calls leave it off. The exact name is tried first, so an extension-less FILE always wins
* over the `.json` guess.
*
* The asset response is handed back whole rather than parsed and re-serialized: it already
* carries a content type and an etag (so `If-None-Match` gets its 304 for free), and these
* files go out BYTE-FOR-BYTE — `RRPlusConfig_v3.json` opens with a UTF-8 BOM, which is what
* the real CDN served and what the client's parser expects.
*/
async function serveConfig(c: Context<App>, name: string): Promise<Response | null> {
if (!CONFIG_NAME.test(name) || name.includes('..')) return null
const candidates = name.includes('.') ? [name] : [name, `${name}.json`]
for (const candidate of candidates) {
// Forwarding the original request keeps its conditional headers; only the URL is
// rewritten to the asset's path.
const res = await c.env.ASSETS.fetch(
new Request(new URL(`/config/${candidate}`, c.req.url), c.req.raw)
)
if (res.ok || res.status === 304) return res
}
return null
}
const app = new Hono<App>()
.use(
'*',
@@ -140,6 +179,49 @@ const app = new Hono<App>()
(c) => c.json(loadingScreenTipData)
)
// Everything else under `/config/`, served from `static/config/` by filename — JSON and
// opaque blobs alike. Declared AFTER the tip-data route above, which would otherwise be
// shadowed by this one: its file is named differently from its path, so it stays a
// route of its own.
.get(
'/config/:name',
describeRoute({
tags: ['Config'],
summary: 'Serve a config file',
description: [
'Serves a file out of `static/config/` verbatim — `RRPlusConfig_v3.json` (the Rec Room',
'Plus benefit lists), `SkuConfig_v1.json` (the Maker AI day-pass store copy) and a',
'GUID-named binary blob today. `{name}` IS the filename, so publishing a config is',
'dropping a file in that directory; nothing in the worker enumerates them, and not',
'everything there is JSON.',
'',
'A name with no extension also resolves against `<name>.json`, because the same file',
'is asked for both ways — the game configs that point at these carry the extension',
'(`Econ.MakerAI.DayPass.Config` is `"SkuConfig_v1.json"`), the clients older config',
'calls leave it off. An extension-less file wins over the `.json` guess.',
'',
'These are byte-for-byte copies of what the real CDN served, BOM included, and are',
'not rewritten or re-serialized on the way out.',
].join(' '),
parameters: [
keyParam('name', 'The configs filename. The `.json` may be left off.', false),
...CONDITIONAL_HEADERS.filter((h) => h.name === 'If-None-Match'),
],
responses: {
200: {
description: 'The config file, as stored',
content: {
'application/json': { schema: resolver(JsonValue) },
'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
},
},
304: { description: '`If-None-Match` matched the files etag (no body)' },
404: { description: 'No config is published under that name' },
},
}),
async (c) => (await serveConfig(c, c.req.param('name'))) ?? c.notFound()
)
// Signature blobs by name. Streamed from R2 under the `sigs/` key prefix;
// 404 when missing.
.get(
@@ -242,8 +324,8 @@ app.get(
'Binary asset delivery for recflare, a private-server reimplementation of the Rec',
'Room backend. Streams the blobs the client downloads while playing — anti-cheat',
'signatures, saved room scenes, invention data and generic client uploads — out of',
'the shared `recflare-cdn` R2 bucket, plus the one bundled config file the loading',
'screen reads.',
'the shared `recflare-cdn` R2 bucket, plus the JSON config files the client reads',
'from `/config/`.',
'',
'Everything is keyed by prefix (`sigs/`, `room/`, `invention/`, `data/`) and served as',
'`application/octet-stream`; the worker never interprets what it hands back. Reads',
+5
View File
@@ -9,6 +9,11 @@ export type Env = SharedHonoEnv & {
// R2 bucket holding CDN binaries: signature blobs under `sigs/<name>` and
// room build data under `room/<name>`.
CDN_ASSETS: R2Bucket
// Static-asset fetcher for the JSON configs in `static/config/` (see wrangler.jsonc
// `assets`). Fetched by filename so `/config/:name` serves whatever is published;
// the binding is the only way in, since `run_worker_first` keeps the runtime from
// serving the files directly.
ASSETS: Fetcher
}
/** Variables can be extended */
+7
View File
@@ -86,6 +86,13 @@ export function keyParam(
// ---- Response schemas ------------------------------------------------------
/**
* An opaque JSON document — the config files under `static/config/` are served verbatim
* and nothing here interprets them, so modelling their fields would be noise that goes
* stale the moment a file is replaced.
*/
export const JsonValue = z.record(z.string(), z.unknown())
/** `GET /` — the liveness probe body. */
export const ServiceStatus = z.object({
service: z.literal('cdn'),
+70
View File
@@ -28,6 +28,75 @@ describe('cdn endpoints', () => {
expect(body[0]).toHaveProperty('Title')
})
// The config directory is served by filename through the ASSETS binding, so a file
// dropped into `static/config/` is reachable without touching the worker.
test.each(['RRPlusConfig_v3', 'SkuConfig_v1'])(
'GET /config/%s serves the file from static/config',
async (name) => {
const res = await exports.default.fetch(`${ORIGIN}/config/${name}`)
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('application/json')
expect((await res.text()).length).toBeGreaterThan(0)
}
)
// Not everything in the directory is JSON: a config may be an opaque blob named by
// GUID, which is served as-is under its own name.
test('GET /config/:name serves an extension-less binary config', async () => {
const res = await exports.default.fetch(`${ORIGIN}/config/1b057e6e-979d-4f30-8856-a386f77c90da`)
expect(res.status).toBe(200)
expect((await res.arrayBuffer()).byteLength).toBeGreaterThan(0)
})
// The game configs name these files WITH the extension (`Econ.MakerAI.DayPass.Config`
// is `"SkuConfig_v1.json"`), so both spellings have to land on the same file.
test('GET /config/:name accepts the .json suffix', async () => {
const bare = await exports.default.fetch(`${ORIGIN}/config/SkuConfig_v1`)
const suffixed = await exports.default.fetch(`${ORIGIN}/config/SkuConfig_v1.json`)
expect(suffixed.status).toBe(200)
expect(await suffixed.text()).toBe(await bare.text())
})
// Byte-for-byte: RRPlusConfig_v3.json opens with a UTF-8 BOM, as the real CDN served
// it. Re-serializing the file (or parsing and re-emitting it) would strip those bytes.
test('GET /config/RRPlusConfig_v3 keeps the files bytes, BOM included', async () => {
const res = await exports.default.fetch(`${ORIGIN}/config/RRPlusConfig_v3`)
const bytes = new Uint8Array(await res.arrayBuffer())
expect(Array.from(bytes.slice(0, 3))).toEqual([0xef, 0xbb, 0xbf])
// `text()` decodes the BOM away, so the remainder still parses as the config.
expect(JSON.parse(new TextDecoder().decode(bytes))).toHaveProperty('BenefitLists')
})
test('GET /config/:name 404s a config that is not published', async () => {
const res = await exports.default.fetch(`${ORIGIN}/config/NoSuchConfig`)
expect(res.status).toBe(404)
})
// The name reaches the binding as a filename, so anything that could climb out of
// `static/config/` is refused before it gets there. (A bare `..` never arrives: the
// URL is normalized to `/` before routing, which is the liveness probe.)
test.each(['%2e%2e%2floading-screen-tip-data.json', 'sub%2Fdir', '.hidden', 'Sku.Config'])(
'GET /config/%s 404s rather than reaching the asset server',
async (name) => {
const res = await exports.default.fetch(`${ORIGIN}/config/${name}`)
expect(res.status).toBe(404)
}
)
// `run_worker_first` keeps the asset server from answering ahead of the Worker: the
// tip data is an asset too (`static/loading-screen-tip-data.json`), and it must stay
// unreachable at that path — its route is `/config/LoadingScreenTipData`.
test('assets are not served at their own paths', async () => {
const res = await exports.default.fetch(`${ORIGIN}/loading-screen-tip-data.json`)
expect(res.status).toBe(404)
})
test('GET /config/LoadingScreenTipData still wins over the wildcard', async () => {
const res = await exports.default.fetch(`${ORIGIN}/config/LoadingScreenTipData`)
expect(res.status).toBe(200)
expect(Array.isArray(await res.json())).toBe(true)
})
test('GET /sigs/:sigName 404s when the blob is absent', async () => {
const res = await exports.default.fetch(`${ORIGIN}/sigs/does-not-exist`)
expect(res.status).toBe(404)
@@ -191,6 +260,7 @@ describe('cdn endpoints', () => {
expect([...documented].sort()).toEqual([
'GET /',
'GET /config/LoadingScreenTipData',
'GET /config/{name}',
'GET /data/{id}',
'GET /invention/{dataBlob}',
'GET /room/{dataBlob}',
+219
View File
@@ -0,0 +1,219 @@
{
"BenefitLists": {
"0": [
0,
1,
2,
3,
4,
5,
6,
7
],
"1": [
0,
1,
2
],
"2": [
0,
1,
2
],
"3": [
8,
9
],
"4": [
10,
11,
12
],
"5": [
13,
14,
7
]
},
"BenefitLookup": {
"0": {
"CustomSpriteName": "Campus_Club_Card_Token_Icon",
"DetailedText": "You get a {{Tokens}} token box every month. Due to platform restrictions, you must log in each week in order to claim this reward. If you want to claim this reward on another platform you can, but you must login at least once a month on the platform you became a member on.",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
"3": "You get a {{Tokens}} token box every month. You must log in at least once during each 30 day period in order to claim this reward."
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "{{Tokens}} tokens per month! ($11 value)",
"TitleText": "{{Tokens}} bonus tokens per month ($11 USD in monthly value)"
},
"1": {
"CustomSpriteName": "icon_inventory_item",
"DetailedText": "You get a box containing a random 4-star item every week. If you have all 4-star items you will get a 800 token box instead. The week resets at 12 AM UTC on Sundays. Due to platform restrictions, you must login each week in order to claim this reward. If you want to claim this reward on another platform you can, but you must login at least once a month on the platform you became a member on.",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Weekly 4-star box!",
"TitleText": "Free weekly 4-star box"
},
"10": {
"CustomSpriteName": "UI_Menu_Token_Image_50",
"DetailedText": "/month. Cancel anytime",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Join RR+ for Endless Rewards!",
"TitleText": "Join RR+ for Endless Rewards!"
},
"11": {
"CustomSpriteName": "icon_currency_coins",
"DetailedText": null,
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "2X token rewards",
"TitleText": "2X token rewards"
},
"12": {
"CustomSpriteName": "icon_RecToken",
"DetailedText": null,
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "{{Tokens}} monthly tokens",
"TitleText": "Monthly Tokens"
},
"13": {
"CustomSpriteName": "icon_currency_dollars",
"DetailedText": "Unlock the ability to turn earned tokens into real-world money",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": true,
"RequiresPublishingEnabled": false,
"ShortText": "Unlock the ability to turn earned tokens into real-world money",
"TitleText": "Unlock the ability to turn earned tokens into real-world money"
},
"14": {
"CustomSpriteName": "Campus_Club_Card_Token_Icon",
"DetailedText": "Get {{Tokens}} every month and a {{RRODiscount}}% discount on eligible items",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Get {{Tokens}} every month and a {{RRODiscount}}% discount on eligible items",
"TitleText": "Monthly Benefits"
},
"2": {
"CustomSpriteName": "PriceTag",
"DetailedText": "You get {{RRODiscount}}% off all purchases in Rec Room Original token stores (e.g. the watch, merch booth, mirror, cafe, paintball, etc). This does not include Rec Room Original stores that dont use tokens (e.g. Laser Tag, Lost Skulls, and Crescendo)\r",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "{{RRODiscount}}% RRO store discount!",
"TitleText": "{{RRODiscount}}% discount in Rec Room stores that accept tokens "
},
"3": {
"CustomSpriteName": "icon_OwnedCostumePieces",
"DetailedText": "You get early access to items for a limited time and can claim them for free by logging in while they are in early access.",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Free, early access to items for a limited time!",
"TitleText": "Free, early access to items for a limited time"
},
"4": {
"CustomSpriteName": "Shirt",
"DetailedText": "You get {{MemberOutfitSlots}} outfit slots on top of the regular {{NonMemberOutfitSlots}}. Access them via the Saved Outfits button in the Profile section of your watch or on your Dorm room mirror.",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "{{MemberOutfitSlots}} saved outfit slots!",
"TitleText": "More saved outfit slots"
},
"5": {
"CustomSpriteName": null,
"DetailedText": "You can sell inventions and custom clothing in the watch store and keys, currencies, and consumables in your rooms. You will be paid 70% of all tokens earned through these sales. Those tokens will be held for a week in an escrow account to mitigate fraud, then theyll be delivered to you the same day as your other RR+ bonus tokens.\r",
"EnabledForPlatforms": 511,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": true,
"RequiresPublishingEnabled": false,
"ShortText": "Earn tokens for your creations!",
"TitleText": "Earn tokens for your creations"
},
"6": {
"CustomSpriteName": "icon_RecToken",
"DetailedText": "Whenever you are offered tokens from a post-activity or first activity of the day reward, you will be offered twice the normal amount.",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Double token rewards!",
"TitleText": "Double token rewards"
},
"7": {
"CustomSpriteName": "icon_Shirt",
"DetailedText": "You can create your own custom shirts with the clothing customizer! You can access the clothing customizer in your backpack, and publish shirts which can be purchased and worn.",
"EnabledForPlatforms": 511,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": true,
"ShortText": "Create your own custom shirts",
"TitleText": "Create your own custom shirts"
},
"8": {
"CustomSpriteName": "Campus_Club_Card_Token_Icon",
"DetailedText": "",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "Plus {{Tokens}} tokens NOW & monthly!",
"TitleText": "Plus {{Tokens}} tokens NOW & monthly!"
},
"9": {
"CustomSpriteName": "icon_Heart",
"DetailedText": "Keep saving {{RRODiscount}}% on every RRO order!",
"EnabledForPlatforms": -1,
"PlatformSpecificDetailedTexts": {
},
"RequiresMonetizationEnabled": false,
"RequiresPublishingEnabled": false,
"ShortText": "And much more!",
"TitleText": "And much more!"
}
},
"HighlightText": "The best deal in Rec Room!",
"MoreDetailsText": "And <u>much more!</u>",
"NumberReplacements": {
"{{MemberOutfitSlots}}": 200,
"{{NonMemberOutfitSlots}}": 16,
"{{RRODiscount}}": 10,
"{{Tokens}}": 6500
},
"StringConfigs": {
"RRPlusMessageBody": "Thanks for becoming a RR+ member. You now have access to all the member benefits!",
"RRPlusMessageTitle": "Welcome to the Club!"
},
"Version": 3
}
+245
View File
@@ -0,0 +1,245 @@
{
"Version": 0,
"SkuConfigs": [
{
"SkuId": 183,
"Name": "{{MakerAIDayPassName}}",
"Description": "{{MakerAIName}} springs to life for {{MakerAIHours}} hours* of creative collaboration!\n\nJust equip your {{MakerPenName}} in any Rooms 2.0 room you own (including Dorms with Maker AI), and your holographic helper can...\n• <b>Create images and patterns</b> on canvases\n• <b>Make objects come to life</b> through AI-generated circuits\n• <b>Generate objects</b> from the {{MakerPenName}} inventory \n• <u><link=\"ID\">Learn more about upcoming features</link> </u>",
"ThumbnailImageName": "img/makerAI_thumb_01.png",
"DetailsImageName": "img/makerAI_storeDetailsPromo_01.png",
"ShowSkuDetails": true,
"Footer": {
"Text": "* {{SubjectToDayPassUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
},
"DialogConfigs": {
"PurchaseSuccess": {
"Title": "Day Pass Purchased",
"Text": "Up to 24 hours of {{MakerAIName}} access has been added to your account. The clock starts now, so go equip your Maker Pen in one of your R2 rooms to get started!",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Explore my R2 Rooms",
"Type": "Primary",
"OnClick": "GoToCreate"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"UpsellAnnouncement": {
"Title": "Make Rooms Magical",
"Text": "Be among the first to try Maker AI in R2 rooms! Our new Day Pass extends beyond RR+ subscribers and dorms, empowering everyone to unleash their imagination.",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Unlock This Deal",
"Type": "Primary",
"OnClick": "GoToDayPass"
}
],
"Cooldown": 10080
},
"UsageLimitReached": {
"Title": "Energy Drained",
"Text": "{{MakerAIName}} hit our day pass usage limits and settled down for a power nap. Ready to wake it up with a fresh Day Pass?",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Wake Up Maker AI",
"Type": "Primary",
"OnClick": "GoToDayPass"
},
{
"Text": "Continue without Maker AI",
"Type": "Secondary",
"OnClick": "DismissDialog"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"TimeLimitReached": {
"Title": "Time's Up!",
"Text": "{{MakerAIName}} hit the day pass time limit and settled down for a power nap. Ready to wake it up with a fresh Day Pass?",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Wake Up Maker AI",
"Type": "Primary",
"OnClick": "GoToDayPass"
},
{
"Text": "Continue without Maker AI",
"Type": "Secondary",
"OnClick": "DismissDialog"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"UsageLimitWarning": {
"Title": "Energy Running Low",
"Text": "Heads up, Maker AI is nearing day pass usage limits and will need a power nap soon. Grab another day pass to keep the magic flowing uninterrupted!",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Add More Energy",
"Type": "Primary",
"OnClick": "GoToDayPass"
},
{
"Text": "Ignore",
"Type": "Secondary",
"OnClick": "DismissDialog"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"TimeLimitWarning": {
"Title": "Time Running Low",
"Text": "Heads up, {{MakerAIName}} has about <b>1 hour left</b> before it needs a recharge. Grab another day pass to keep the magic flowing uninterrupted!",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Add More Time",
"Type": "Primary",
"OnClick": "GoToDayPass"
},
{
"Text": "Ignore",
"Type": "Secondary",
"OnClick": "DismissDialog"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"ComingSoonToMakerAI": {
"Title": "Coming Soon to {{MakerAIName}}",
"Text": "There are some things Maker AI doesn't know how to do yet, and we're working on it!\n\nThis includes...\n•understanding circuits you've edited manually\n•generating objects out of basic shapes\n•placing objects relative to other objects\n\nSo, <b>\"put the lamp next to the couch\"</b> or <b>\"fix this bug in my circuit\"</b> will be confusing to Maker AI (for now).",
"SpriteName": "icon_MakerAI",
"Buttons": []
},
"ChatError": {
"Title": "Ready to create?",
"Text": "Your building bestie is taking a power nap. Wake it up with a fresh {{MakerAIDayPassName}}!",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Wake up Maker AI",
"Type": "Primary",
"OnClick": "GoToDayPass"
}
]
},
"PurchaseConfirmation": {
"Title": "Up to {{MakerAIHours}} Hours of Power!",
"Text": "Once a day pass is purchased, its timer keeps running even when you're not playing. The pass can't be paused or refunded even if you hit usage limits early. \n\n<size=75%><i>{{PlatformConfirmationMessage}}</i></size>",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Accept & Continue to Payment",
"Type": "Primary",
"OnClick": "PositiveEvent"
}
],
"Footer": {
"Text": "By pressing \"Accept & Continue to Payment\", I expressly agree that access to {{MakerAIName}} will begin immediately, and I acknowledge that I will lose my right to cancel or get a refund once access starts— even if it expires early due to <u>usage limits</u>.",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"DayPassActiveBanner": {
"Title": "Day Pass Active",
"Text": "You currently have {{MakerAITimeLeftDynamic}} of {{MakerAIName}} access. If you buy another pass now, you'll get an additional {{MakerAIHours}} hours.",
"SpriteName": "icon_hourglass",
"Buttons": []
},
"MakerAIUsageLimits": {
"Title": "{{MakerAIName}} Usage Limits and Conditions",
"Text": "<b>Why does Maker AI have usage limits?</b>\nMaker AI is a powerful tool that allows everyone to create, but every action has a cost. Usage limits ensure we can continue to offer Maker AI to everyone.\n\n<b>What are the usage limits?</b>\nWe limit day passes to many hundreds of simple requests or many dozens of our most expensive requests (e.g., images and circuits). Dont worry—this should only affect less than 15% of users. \n\n<b>What happens if I hit these limits?</b>\n•If it looks like you are getting close, we will provide a warning that you're approaching the limit.\n• If you exceed the limit, your day pass will end early. You will be able to buy another pass and continue working.\n\nWe appreciate your understanding as we work to make this tool available to all.\n\n<u><link=\"{{LearnMoreAboutUsageLimitsUrl}}\">Learn More</link></u>",
"SpriteName": "icon_MakerAI",
"Buttons": []
},
"FreeTrialConfirmation": {
"Title": "Try {{MakerAIName}} for free",
"Text": "Get up to {{MakerAIFreeTrialDurationDynamic}} of {{MakerAIName}} in an R2 room you own, free of charge.\n\nOnce your free trial starts, its timer keeps running even when you're not playing. It can't be paused or reactivated even if you hit usage limits early.",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Accept & Start Free Trial",
"Type": "Primary",
"OnClick": "PositiveEvent"
}
],
"Footer": {
"Text": "By pressing \"Accept & Start Free Trial\", I expressly agree that access to {{MakerAIName}} will begin immediately, and I acknowledge that I will lose my right to cancel once access starts—even if it expires early due to <u>usage limits</u>.",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"FreeTrialSuccess": {
"Title": "Free Trial Activated",
"Text": "Up to {{MakerAIFreeTrialDurationDynamic}} of {{MakerAIName}} access has been added to your account, free of charge. The clock starts now, so go equip your {{MakerPenName}} in one of your R2 rooms to get started.",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Explore my R2 Rooms",
"Type": "Primary",
"OnClick": "GoToCreate"
}
],
"Footer": {
"Text": "{{LearnMoreAboutUsageLimits}}",
"ExternalUrl": "{{LearnMoreAboutUsageLimitsUrl}}"
}
},
"ActivateFreeTrialError": {
"Title": "Something Went Wrong",
"Text": "A problem occurred while attempting to activate your free trial, try again in a few minutes. If the problem persists, you can contact us at <u>https://recroom.zendesk.com</u>.",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Ok",
"Type": "Primary",
"OnClick": "DismissDialog"
}
]
},
"FreeTrialAnnouncement": {
"Title": "Creation for Everyone",
"Text": "Get up to {{MakerAIFreeTrialDurationDynamic}} of {{MakerAIName}} in an R2 room you own, free of charge. No strings attached—just pure creative power to generate images, circuits, and more!",
"SpriteName": "icon_MakerAI",
"Buttons": [
{
"Text": "Unlock This Deal",
"Type": "Primary",
"OnClick": "GoToDayPass"
}
],
"Cooldown": 10080
}
}
}
],
"StringReplacements": {
"{{MakerAIName}}": "Maker AI",
"{{MakerAIDayPassName}}": "Maker AI Day Pass",
"{{DayPassNamePlural}}": "Day Passes",
"{{MakerAIHours}}": "24",
"{{SubjectToDayPassUsageLimits}}": "Subject to <u>usage limits</u>. Currently unavailable on Playstation and Nintendo Switch.",
"{{LearnMoreAboutUsageLimits}}": "<u><link=\"MakerAIUsageLimits\">Learn more about day pass usage limits</link></u>",
"{{LearnMoreAboutUsageLimitsUrl}}": "https://rec.net/creator/p/makerai-daypass",
"{{MakerPenName}}": "Maker Pen"
}
}
+16
View File
@@ -16,6 +16,22 @@
"cache": {
"enabled": false
},
// The JSON config files in `static/config/`, uploaded as Workers static assets rather
// than bundled into the script, so `/config/:name` serves whatever is published there:
// adding a config is dropping in a file (a bundled `import` can't do that — the
// bundler has to see every path at build time).
//
// `run_worker_first: true` because these are the WORKER'S data files, not a site.
// Without it the runtime answers any request whose path matches an asset before the
// Worker runs, which would both publish every config a second time at
// `/config/<name>.json` and put asset routing in front of the R2 routes below. With
// it, the Worker sees every request and the files are reachable only through
// `/config/:name`.
"assets": {
"binding": "ASSETS",
"directory": "./static",
"run_worker_first": true
},
// CDN binaries (signature blobs + room build data) are stored as R2 objects
// and streamed back by key. Keys are prefixed `sigs/` and `room/`.
"r2_buckets": [