feat: support changing quest language

This commit is contained in:
neilchen0812 committed 2026-10-08 02:46:44 +10:30
1 parent 9b8d7109cf
commit 66b0b7ba68
18 files changed
+313 -109

No files matched your search

+116 -31
View File
@@ -3,6 +3,8 @@ import fs from 'fs/promises'
import path from 'path'
import yaml from 'yaml'
import type { QuestLocale } from '../../../packages/constants'
import { SUPPORTED_LOCALES } from '../../../packages/constants'
import {
questSchema,
globalExceptionSchema,
@@ -36,16 +38,22 @@ class QuestValidationError extends Error {
}
}
function questImageId(id: string, locale: QuestLocale) {
return `${id}-${locale.toLowerCase()}`
}
class QuestManager {
private quests = new Map<string, IQuest>()
private quests = new Map<string, Map<QuestLocale, IQuest>>()
private questDirectory = path.join(config.projectRoot, 'quests')
get(id: string) {
return this.quests.get(id)
get(id: string, locale: QuestLocale) {
return this.quests.get(id)?.get(locale)
}
getAll() {
getAll(locale: QuestLocale) {
return Array.from(this.quests.values())
.map((versions) => versions.get(locale))
.filter((quest): quest is IQuest => quest !== undefined)
}
private async mapQuests(callback: QuestCallback) {
@@ -83,42 +91,119 @@ class QuestManager {
async loadAndUpdateQuests() {
const exceptions = await this.loadGlobalExceptions()
return this.mapQuests(async (id, questPath) => {
const files = await fs.readdir(questPath)
if (!files.includes('game.yml')) {
throw new QuestValidationError(`Missing game.yml`, id, null)
const loaded = new Map<string, Map<QuestLocale, IQuest>>()
await this.mapQuests(async (id, questPath) => {
const entries = await fs.readdir(questPath, { withFileTypes: true })
const versions = new Map<QuestLocale, IQuest>()
for (const locale of SUPPORTED_LOCALES) {
const localeEntry = entries.find((entry) => entry.name === locale)
if (!localeEntry) {
logger.warn(`Missing locale "${locale}" for quest "${id}"`)
// eslint-disable-next-line no-continue
continue
}
if (!localeEntry.isDirectory()) {
throw new QuestValidationError(
`${locale} exists but is not a directory`,
id,
null
)
}
const localePath = path.join(questPath, locale)
try {
const files = await fs.readdir(localePath)
if (!files.includes('game.yml')) {
throw new QuestValidationError(
`Missing ${locale}/game.yml`,
id,
null
)
}
const body = await fs.readFile(
path.join(localePath, 'game.yml'),
'utf-8'
)
const questData = yaml.parse(body, {
merge: true,
})
const image = files.includes('Dockerfile')
? questImageId(id, locale)
: 'base'
const quest = questSchema.parse({
id,
image,
...questData,
})
quest.exceptions = [...exceptions]
versions.set(locale, quest)
} catch (error) {
logger.error(`Error parsing quest ${id} for locale ${locale}`, {
error,
})
throw new QuestValidationError(
`Error parsing quest ${id} for locale ${locale}`,
id,
error
)
}
}
try {
const body = await fs.readFile(
path.join(questPath, 'game.yml'),
'utf-8'
)
const questData = yaml.parse(body, {
merge: true,
})
const image = files.includes('Dockerfile') ? id : 'base'
const quest = questSchema.parse({
id,
image,
...questData,
})
quest.exceptions = [...exceptions]
this.quests.set(id, quest)
} catch (error) {
logger.error(`Error parsing quest ${id}`, { error })
throw new QuestValidationError(`Error parsing quest ${id}`, id, error)
if (versions.size > 0) {
loaded.set(id, versions)
} else {
logger.warn(`No valid locales found for quest "${id}"`)
}
})
this.quests = loaded
}
async getQuestDockerfiles() {
const questlist = await this.mapQuests(async (id, questPath) => {
const files = await fs.readdir(questPath)
return files.includes('Dockerfile') ? { id, questPath } : null
const entries = await fs.readdir(questPath, { withFileTypes: true })
const dockerfiles: { id: string; questPath: string }[] = []
for (const locale of SUPPORTED_LOCALES) {
const localeEntry = entries.find((entry) => entry.name === locale)
if (!localeEntry) {
logger.warn(`Missing locale "${locale}" for quest "${id}"`)
// eslint-disable-next-line no-continue
continue
}
if (!localeEntry.isDirectory()) {
throw new QuestValidationError(
`${locale} exists but is not a directory`,
id,
null
)
}
const localePath = path.join(questPath, locale)
const files = await fs.readdir(localePath)
if (files.includes('Dockerfile')) {
dockerfiles.push({
id: questImageId(id, locale),
questPath: localePath,
})
}
}
return dockerfiles.length > 0 ? dockerfiles : null
})
return questlist.filter((quest) => quest !== null)
}
}
+17 -7
View File
@@ -1,4 +1,6 @@
import { HydratedDocument } from 'mongoose'
import type { QuestLocale } from '../../../packages/constants'
import { LOCALE_NAME_MAP } from '../../../packages/constants'
import { Session, UserProfile } from '../../../packages/models'
import type { ISession, IUser } from '../../../packages/models'
import {
@@ -34,11 +36,12 @@ async function deactivateSessions(userId: string, quest: string) {
export async function createNewSession(
userId: string,
questId: string
questId: string,
locale: QuestLocale
): Promise<ISession> {
const quest = await questManager.get(questId)
const quest = await questManager.get(questId, locale)
if (!quest) {
throw new Error(`Quest ${questId} not found`)
throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`)
}
// deactivate all active sessions
@@ -55,12 +58,18 @@ export async function createNewSession(
const containerName = `quest-${quest.id}-${userProfile.user.username}-${Date.now()}`
const container = await createContainer(containerName, quest.id, quest.image)
const container = await createContainer(
containerName,
quest.id,
locale,
quest.image
)
await getAndStartContainer(container.id)
const newSession = new Session({
user: userId,
quest: quest.id,
locale,
containerId: container.id,
containerName,
})
@@ -117,15 +126,16 @@ export async function finishSession(
export async function isQuestUnlocked(
userId: string,
questId: string
questId: string,
locale: QuestLocale
): Promise<boolean> {
const userProfile = await UserProfile.findOne({ user: userId })
if (!userProfile) {
throw new Error(`UserProfile ${userId} not found`)
}
const quest = await questManager.get(questId)
const quest = await questManager.get(questId, locale)
if (!quest) {
throw new Error(`Quest ${questId} not found`)
throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`)
}
if (!quest.requirements) {
return true