import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const docusDir = path.join(repoRoot, 'docus'); const contentDir = path.join(docusDir, 'content'); const generatedPages = [ { source: 'docs/FRAMEWORK_ARCHITECTURE.md', target: '1.getting-started/1.architecture.md' }, { source: 'docs/MODULE_REFERENCE.md', target: '1.getting-started/2.module-reference.md' }, { source: 'docs/DEVELOPMENT_GUIDE.md', target: '1.getting-started/3.development.md' }, { source: 'docs/MISSION_DESIGNER_GUIDE.md', target: '1.getting-started/4.mission-designer.md' }, { source: 'docs/surrealdb-setup.md', target: '1.getting-started/5.surrealdb-setup.md' }, { source: 'arma/server/docs/README.md', target: '2.server-extension/0.index.md' }, { source: 'arma/server/docs/api-reference.md', target: '2.server-extension/1.api-reference.md' }, { source: 'arma/server/docs/usage-examples.md', target: '2.server-extension/2.usage-examples.md' }, { source: 'arma/server/addons/common/README.md', target: '2.server-extension/3.common.md' }, { source: 'docs/ICOM_USAGE_GUIDE.md', target: '2.server-extension/4.icom.md' }, { source: 'docs/ACTOR_USAGE_GUIDE.md', target: '3.server-modules/1.actor.md' }, { source: 'docs/BANK_USAGE_GUIDE.md', target: '3.server-modules/2.bank.md' }, { source: 'docs/CAD_USAGE_GUIDE.md', target: '3.server-modules/3.cad.md' }, { source: 'docs/ECONOMY_USAGE_GUIDE.md', target: '3.server-modules/4.economy.md' }, { source: 'docs/GARAGE_USAGE_GUIDE.md', target: '3.server-modules/5.garage.md' }, { source: 'docs/LOCKER_USAGE_GUIDE.md', target: '3.server-modules/6.locker.md' }, { source: 'docs/ORG_USAGE_GUIDE.md', target: '3.server-modules/7.organization.md' }, { source: 'docs/OWNED_STORAGE_USAGE_GUIDE.md', target: '3.server-modules/8.owned-storage.md' }, { source: 'docs/PHONE_USAGE_GUIDE.md', target: '3.server-modules/9.phone.md' }, { source: 'docs/STORE_USAGE_GUIDE.md', target: '3.server-modules/10.store.md' }, { source: 'docs/TASK_USAGE_GUIDE.md', target: '3.server-modules/11.task.md' }, { source: 'docs/CLIENT_USAGE_GUIDE.md', target: '4.client-addons/0.index.md' }, { source: 'docs/CLIENT_MAIN_USAGE_GUIDE.md', target: '4.client-addons/1.main.md' }, { source: 'docs/CLIENT_COMMON_USAGE_GUIDE.md', target: '4.client-addons/2.common.md' }, { source: 'docs/CLIENT_ACTOR_USAGE_GUIDE.md', target: '4.client-addons/3.actor.md' }, { source: 'docs/CLIENT_BANK_USAGE_GUIDE.md', target: '4.client-addons/4.bank.md' }, { source: 'docs/CLIENT_CAD_USAGE_GUIDE.md', target: '4.client-addons/5.cad.md' }, { source: 'docs/CLIENT_GARAGE_USAGE_GUIDE.md', target: '4.client-addons/6.garage.md' }, { source: 'docs/CLIENT_LOCKER_USAGE_GUIDE.md', target: '4.client-addons/7.locker.md' }, { source: 'docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md', target: '4.client-addons/8.notifications.md' }, { source: 'docs/CLIENT_ORG_USAGE_GUIDE.md', target: '4.client-addons/9.organization.md' }, { source: 'docs/CLIENT_PHONE_USAGE_GUIDE.md', target: '4.client-addons/10.phone.md' }, { source: 'docs/CLIENT_STORE_USAGE_GUIDE.md', target: '4.client-addons/11.store.md' } ]; const virtualRoutes = new Map([ ['README.md', '/getting-started'], ['docs/README.md', '/getting-started'] ]); for (const page of generatedPages) { virtualRoutes.set(toPosix(page.source), toRoute(page.target)); } const staticFiles = [ { target: 'index.md', content: `--- seo: title: Forge Framework Documentation description: Documentation for the Forge Arma 3 framework, covering architecture, persistence, extension APIs, gameplay modules, and client UIs. --- ::u-page-hero #title Forge Framework Documentation #description Forge is a persistent Arma 3 framework that combines SQF addons, a Rust \`arma-rs\` extension, SurrealDB persistence, shared domain crates, and browser-backed player interfaces. Use these docs to understand the runtime architecture, extension API surface, server gameplay modules, and client addon integration patterns. Server owners and developers must start SurrealDB and place a matching \`config.toml\` beside \`forge_server_x64.dll\` before launching a Forge-enabled server or local multiplayer test. #links :::u-button --- color: primary size: xl to: /getting-started trailing-icon: i-lucide-arrow-right --- Start here ::: :::u-button --- color: neutral icon: simple-icons-github size: xl to: https://github.com/InnovativeDevSolutions/forge variant: outline --- View source ::: :: ::u-page-section #title What Forge Covers #features :::u-page-feature --- icon: i-lucide-boxes --- #title Domain [Modules]{.text-primary} #description Actor, bank, CAD, garage, locker, organization, phone, store, task, and owned-storage workflows share a consistent service and extension model. ::: :::u-page-feature --- icon: i-lucide-server --- #title Rust [Extension]{.text-primary} #description The server extension keeps command parsing thin, routes domain requests into services, and persists durable state through SurrealDB. ::: :::u-page-feature --- icon: i-lucide-database-zap --- #title Durable [Persistence]{.text-primary} #description Repository traits stay storage-agnostic while concrete adapters in the extension handle schema and database mapping. ::: :::u-page-feature --- icon: i-lucide-monitor-smartphone --- #title Browser [UIs]{.text-primary} #description Client addons host web-based interfaces inside Arma displays and synchronize state through namespaced browser bridge events. ::: :::u-page-feature --- icon: i-lucide-arrow-left-right --- #title Transport [Layer]{.text-primary} #description Large payloads move through chunked request and response transport while smaller commands still use direct \`callExtension\` paths. ::: :::u-page-feature --- icon: i-lucide-wrench --- #title Development [Workflow]{.text-primary} #description The docs cover module boundaries, local validation checks, and where new domain logic belongs across Rust, SQF, and web UI layers. ::: :: ::u-page-section #title Documentation Areas #features :::u-page-feature --- icon: i-lucide-rocket to: /getting-started --- #title [Getting Started]{.text-primary} #description Framework overview, architecture, module reference, and development rules. ::: :::u-page-feature --- icon: i-lucide-map to: /getting-started/mission-designer --- #title Mission [Designers]{.text-primary} #description Eden object placement, garage markers, and CAD-compatible task setup. ::: :::u-page-feature --- icon: i-lucide-server-cog to: /server-extension --- #title Server [Extension]{.text-primary} #description Extension architecture, command surface, and SQF usage examples. ::: :::u-page-feature --- icon: i-lucide-network to: /server-extension/icom --- #title ICOM [Events]{.text-primary} #description Inter-server event routing through the Forge ICOM hub and extension commands. ::: :::u-page-feature --- icon: i-lucide-layers-3 to: /server-modules --- #title Server [Modules]{.text-primary} #description Gameplay-domain usage guides for persistence, hot state, and command flows. ::: :::u-page-feature --- icon: i-lucide-monitor-smartphone to: /client-addons --- #title Client [Addons]{.text-primary} #description Browser bridge, client UX entry points, and addon-specific event contracts. ::: :: ` }, { target: '1.getting-started/.navigation.yml', content: `title: Getting Started icon: i-lucide-rocket ` }, { target: '1.getting-started/0.index.md', content: `--- title: Getting Started description: Use this section as the main entry point for the Forge framework. --- Forge combines: - Arma 3 client addons for UX and browser-hosted interfaces - Arma 3 server addons for mission integration and authoritative flow control - a Rust server extension for command routing and persistence - shared Rust crates for models, repositories, and services - SurrealDB for durable storage ## Launch Prerequisites Before starting a Forge-enabled dedicated server or local multiplayer test, server owners and developers must start SurrealDB and make sure \`config.toml\` is beside \`forge_server_x64.dll\`. The config values must match the running SurrealDB endpoint, namespace, database, username, and password. Mission designers and players do not need their own SurrealDB instance unless they are hosting locally, but the server they join must have these prerequisites ready. ## Common Commands \`\`\`powershell cargo test npm run build:webui .\\build-arma.ps1 \`\`\` ## Start Here ::u-page-grid :::u-page-card --- icon: i-lucide-network title: Architecture to: /getting-started/architecture --- Understand how SQF, Rust services, SurrealDB, and browser UIs fit together. ::: :::u-page-card --- icon: i-lucide-boxes title: Module Reference to: /getting-started/module-reference --- Review gameplay domains, infrastructure modules, and extension command groups. ::: :::u-page-card --- icon: i-lucide-wrench title: Development Guide to: /getting-started/development --- See the rules for adding modules and changing boundaries without regressions. ::: :::u-page-card --- icon: i-lucide-map title: Mission Designer Guide to: /getting-started/mission-designer --- Place Eden interaction objects, garage markers, and Forge task modules for playable missions. ::: :::u-page-card --- icon: i-lucide-database title: SurrealDB Setup to: /getting-started/surrealdb-setup --- Install SurrealDB, match Forge config values, and choose the right setup path for developers or admin-facing roles. ::: :::u-page-card --- icon: i-lucide-server-cog title: Server Extension to: /server-extension --- Follow the extension architecture, API surface, and SQF usage examples. ::: :::u-page-card --- icon: i-lucide-layers-3 title: Server Modules to: /server-modules --- Dive into the actor, bank, CAD, garage, locker, organization, phone, store, task, and owned-storage guides. ::: :::u-page-card --- icon: i-lucide-monitor-smartphone title: Client Addons to: /client-addons --- Explore the client bridge model and addon-specific browser integration rules. ::: :: ` }, { target: '2.server-extension/.navigation.yml', content: `title: Forge Server Extension icon: i-lucide-server-cog ` }, { target: '3.server-modules/.navigation.yml', content: `title: Server Modules icon: i-lucide-layers-3 ` }, { target: '3.server-modules/0.index.md', content: `--- title: Server Module Guides description: These pages document the authoritative server-side workflows in Forge. --- Most modules follow the same shape: 1. Server SQF gathers game context and validates mission/runtime assumptions. 2. The \`forge_server\` extension routes the request into the matching command group. 3. Services apply business rules through storage-agnostic repository traits. 4. The extension persists durable state through SurrealDB adapters when needed. ## Gameplay Domains ::u-page-grid :::u-page-card --- icon: i-lucide-user-round title: Actor to: /server-modules/actor --- Persistent player identity, position, loadout, contact fields, and hot state. ::: :::u-page-card --- icon: i-lucide-wallet title: Bank to: /server-modules/bank --- Player funds, transfers, PIN validation, checkout charging, and bank hot state. ::: :::u-page-card --- icon: i-lucide-map title: CAD to: /server-modules/cad --- Dispatch requests, assignments, profiles, grouped state, and hydrated views. ::: :::u-page-card --- icon: i-lucide-ambulance title: Economy to: /server-modules/economy --- Fuel, service, and medical charging rules across player and organization funds. ::: :::u-page-card --- icon: i-lucide-car-front title: Garage to: /server-modules/garage --- Vehicle storage, hot-state updates, and persistence of vehicle condition. ::: :::u-page-card --- icon: i-lucide-package title: Locker to: /server-modules/locker --- Player inventory storage, unique item limits, and locker hot-state behavior. ::: :::u-page-card --- icon: i-lucide-building-2 title: Organization to: /server-modules/organization --- Membership, treasury, shared assets, fleet, and organization hot workflows. ::: :::u-page-card --- icon: i-lucide-key-round title: Owned Storage to: /server-modules/owned-storage --- Owner-scoped locker and vehicle unlock storage used by org-linked features. ::: :::u-page-card --- icon: i-lucide-smartphone title: Phone to: /server-modules/phone --- Contacts, message threads, and email state for in-game phone workflows. ::: :::u-page-card --- icon: i-lucide-shopping-cart title: Store to: /server-modules/store --- Checkout orchestration across pricing, grants, payment sources, and rollback. ::: :::u-page-card --- icon: i-lucide-flag title: Task to: /server-modules/task --- Task catalog, ownership, status transitions, defuse counters, and rewards. ::: :: ` }, { target: '4.client-addons/.navigation.yml', content: `title: Client Addons icon: i-lucide-monitor-smartphone ` } ]; await fs.rm(contentDir, { recursive: true, force: true }); await fs.mkdir(contentDir, { recursive: true }); for (const file of staticFiles) { await writeContentFile(file.target, file.content); } for (const page of generatedPages) { const sourceRel = toPosix(page.source); const sourcePath = path.join(repoRoot, page.source); const rawContent = await fs.readFile(sourcePath, 'utf8'); const content = prepareGeneratedPageContent(rewriteMarkdownLinks(rawContent, sourceRel)); await writeContentFile(page.target, content); } console.log(`Generated ${staticFiles.length + generatedPages.length} Docus content files.`); function rewriteMarkdownLinks(content, sourceRel) { const sourceDir = path.posix.dirname(sourceRel); return content.replace(/\]\(([^)]+)\)/g, (match, rawTarget) => { if ( rawTarget.startsWith('http://') || rawTarget.startsWith('https://') || rawTarget.startsWith('#') || rawTarget.startsWith('mailto:') ) { return match; } const [targetPath, targetHash] = rawTarget.split('#'); if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) { return match; } const normalizedTarget = toPosix( path.posix.normalize(path.posix.join(sourceDir, targetPath.replace(/\\/g, '/'))) ); const route = virtualRoutes.get(normalizedTarget); if (!route) { return match; } return `](${route}${targetHash ? `#${targetHash}` : ''})`; }); } function prepareGeneratedPageContent(content) { const title = extractFirstH1(content); const description = extractLeadParagraph(content); const body = stripMatchingLeadParagraph(stripFirstH1(content), description).trimStart(); const frontmatter = [ '---', title ? `title: ${yamlString(title)}` : undefined, description ? `description: ${yamlString(description)}` : undefined, '---' ].filter(Boolean).join('\n'); return `${frontmatter}\n\n${body}`; } function extractFirstH1(content) { const match = content.match(/^#\s+(.+?)\s*#*\s*$/m); return match ? match[1].trim() : ''; } function extractLeadParagraph(content) { const lines = stripFirstH1(content).split(/\r?\n/); for (let index = 0; index < lines.length; index += 1) { const line = lines[index].trim(); if (!line) { continue; } if (!isParagraphStart(line)) { continue; } const paragraph = []; for (let paragraphIndex = index; paragraphIndex < lines.length; paragraphIndex += 1) { const paragraphLine = lines[paragraphIndex].trim(); if (!paragraphLine) { break; } paragraph.push(paragraphLine); } return normalizeParagraph(paragraph.join(' ')); } return ''; } function stripFirstH1(content) { const lines = content.split(/\r?\n/); const headingIndex = lines.findIndex((line) => /^#\s+.+/.test(line.trim())); if (headingIndex === -1) { return content; } lines.splice(headingIndex, 1); while (headingIndex < lines.length && !lines[headingIndex].trim()) { lines.splice(headingIndex, 1); } return lines.join('\n'); } function stripMatchingLeadParagraph(content, description) { if (!description) { return content; } const lines = content.split(/\r?\n/); let startIndex = 0; while (startIndex < lines.length && !lines[startIndex].trim()) { startIndex += 1; } if (startIndex < lines.length && /^##\s+overview\s*$/i.test(lines[startIndex].trim())) { const sectionStart = startIndex; startIndex += 1; while (startIndex < lines.length && !lines[startIndex].trim()) { startIndex += 1; } let sectionEnd = startIndex; const sectionLines = []; while (sectionEnd < lines.length && !/^#{2,}\s+/.test(lines[sectionEnd].trim())) { if (lines[sectionEnd].trim()) { sectionLines.push(lines[sectionEnd].trim()); } sectionEnd += 1; } if (normalizeParagraph(sectionLines.join(' ')) === description) { while (sectionEnd < lines.length && !lines[sectionEnd].trim()) { sectionEnd += 1; } return [...lines.slice(0, sectionStart), ...lines.slice(sectionEnd)].join('\n'); } startIndex = sectionStart; } if (startIndex >= lines.length || !isParagraphStart(lines[startIndex].trim())) { return content; } let endIndex = startIndex; const paragraph = []; while (endIndex < lines.length && lines[endIndex].trim()) { paragraph.push(lines[endIndex].trim()); endIndex += 1; } if (normalizeParagraph(paragraph.join(' ')) !== description) { return content; } while (endIndex < lines.length && !lines[endIndex].trim()) { endIndex += 1; } return [...lines.slice(0, startIndex), ...lines.slice(endIndex)].join('\n'); } function isParagraphStart(line) { return !( line.startsWith('#') || line.startsWith('![') || line.startsWith('```') || line.startsWith(':::') || line.startsWith('::') || line.startsWith('|') || /^[-*+]\s+/.test(line) || /^\d+\.\s+/.test(line) ); } function normalizeParagraph(value) { return value.replace(/\s+/g, ' ').trim(); } function yamlString(value) { return JSON.stringify(value); } function toRoute(target) { const withoutExt = toPosix(target).replace(/\.md$/i, ''); const parts = withoutExt.split('/'); if (parts.length === 1 && parts[0] === 'index') { return '/'; } const mapped = parts .map((part, index) => { if (index === parts.length - 1 && (part === '0.index' || part === 'index')) { return ''; } return part.replace(/^\d+\./, ''); }) .filter(Boolean); return `/${mapped.join('/')}`; } function toPosix(value) { return value.replace(/\\/g, '/'); } async function writeContentFile(target, content) { const targetPath = path.join(contentDir, target); await fs.mkdir(path.dirname(targetPath), { recursive: true }); await fs.writeFile(targetPath, content.endsWith('\n') ? content : `${content}\n`, 'utf8'); }