Mirage Source 6
Extending / New Content Types

Adding a whole new content category

Items, NPCs, spells, and shops are each a ContentStore — a generic id→record collection backed by one Prisma model. Adding a sixth category (this page uses Quests as a worked example) follows the same pattern. Unlike the behavior registries, this genuinely does touch a few engine-side files — a new content type needs a database table, which can't be purely runtime-pluggable.

This is a worked example, not a shipped feature. MS6 does not include a Quests system — the steps below show the pattern you'd follow, using a plausible new content type as the illustration.

The pattern, step by step

1

Prisma schema. Add a model and run a migration:

// packages/server/prisma/schema.prisma
model GameQuest {
  id          Int    @id
  name        String @db.VarChar(128)
  description String @db.Text
  giverNpcId  Int    @default(0)
  rewardExp   Int    @default(0)
  rewardItem  Json
}
pnpm --filter @ms6/server db:generate
pnpm --filter @ms6/server db:push
2

Shared type. Define the in-memory record shape:

// packages/shared/src/types/quest.ts
export interface QuestRec {
  id: number;
  name: string;
  description: string;
  giverNpcId: number;
  rewardExp: number;
  rewardItem: { itemId: number; quantity: number };
}

Export it from packages/shared/src/types/index.ts.

3

Descriptor + store. One ContentDescriptor (translating between the Prisma row and QuestRec), then wire a ContentStore into ContentManager:

// packages/server/src/content/descriptors.ts (add alongside the existing 4)
export const questDescriptor: ContentDescriptor<QuestRec, GameQuest> = {
  idOf: (rec) => rec.id,
  fromRow: (row) => ({
    id: row.id, name: row.name, description: row.description,
    giverNpcId: row.giverNpcId, rewardExp: row.rewardExp,
    rewardItem: row.rewardItem as unknown as QuestRec['rewardItem'],
  }),
  toRow: (rec) => ({
    name: rec.name, description: rec.description, giverNpcId: rec.giverNpcId,
    rewardExp: rec.rewardExp, rewardItem: rec.rewardItem as object,
  }),
  prismaDelegate: asContentDelegate(prisma.gameQuest),
};
// packages/server/src/content/ContentManager.ts
private readonly questStore = new ContentStore(questDescriptor);
// ...in loadFromDb(): await this.questStore.loadFromDb();
getQuest(id: number) { return this.questStore.get(id); }
getAllQuests() { return this.questStore.getAll(); }
async saveQuest(q: QuestRec) { await this.questStore.save(q); }
maxQuestId() { return this.questStore.maxId(); }

There's no automatic JSON seeding to wire up — a fresh database starts with zero quests, same as every other content type, and they're created through the admin editor from there. ContentStore.seedFromJsonFile() is still available if you want to ship optional starter content for a type, but nothing calls it automatically; see Getting Started for how the shipped demo content is distributed instead (a plain SQL import, not JSON auto-seeding).

4

REST endpoints for the standalone editor, mirroring the existing item/NPC/spell/shop routes:

// packages/server/src/routes/content.ts
router.get('/quests', (_req, res) => res.json(ContentManager.getAllQuests()));
router.put('/quests/:id', async (req, res) => {
  const access = await getAccess(req);
  if (!requireDev(access, res)) return;
  const quest: QuestRec = { ...req.body, id: parseInt(req.params.id) };
  await ContentManager.saveQuest(quest);
  broadcastToLiveRoom(ServerPacket.UpdateQuest, { quest }); // if you want a live-update push
  res.json(quest);
});
// ...router.post('/quests', ...) for creating new ones
5

Seed file (optional): content/quests.json, an array of QuestRec objects — picked up automatically on first boot against an empty database, same as the built-in types.

6

Editor UI. Add packages/editor/src/editors/QuestEditor.ts following the same list→edit-page shape as ItemEditor.ts or ShopEditor.ts (the simplest existing examples to copy from) — a render<X>Editor(root, records[]) function with a search/list view and a save-form view, calling a typed api.saveQuest() you add to packages/editor/src/api.ts. Wire it into the sidebar nav in packages/editor/src/main.ts.

7

Gameplay behavior (only if quests need to actually do something at runtime — award exp/items on completion, etc.): a new QuestSystem under packages/server/src/systems/, constructed the same way as the existing systems (new QuestSystem(this) in GameRoom.onCreate()) and wired into registerHandlers() for whatever new ClientPacket/ServerPacket entries it needs.

That's 4–5 files with one clear pattern to copy from an existing content type, plus an optional system if the content is more than static data — a smaller lift than it might look, since steps 3–4 are almost entirely mechanical once you've looked at how items/NPCs/spells/shops already do it.