Corrections diverses + compendiums
This commit is contained in:
@@ -1,132 +1,16 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import crypto from "node:crypto"
|
||||
import fs from "node:fs"
|
||||
|
||||
import { Level } from "level"
|
||||
import { buildPacks } from "./pack-builder.mjs"
|
||||
|
||||
const rootDir = path.resolve(import.meta.dirname, "..")
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"))
|
||||
const systemJson = JSON.parse(fs.readFileSync(path.join(rootDir, "system.json"), "utf8"))
|
||||
|
||||
const PACK_SOURCES = [
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "armes.json"),
|
||||
outputPath: path.join(rootDir, "packs", "armes"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "armures.json"),
|
||||
outputPath: path.join(rootDir, "packs", "armures"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "equipements.json"),
|
||||
outputPath: path.join(rootDir, "packs", "equipements"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "pouvoirs-compagnie.json"),
|
||||
outputPath: path.join(rootDir, "packs", "pouvoirs-compagnie"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "competences.json"),
|
||||
outputPath: path.join(rootDir, "packs", "competences"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "races.json"),
|
||||
outputPath: path.join(rootDir, "packs", "races"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "tribus.json"),
|
||||
outputPath: path.join(rootDir, "packs", "tribus"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "metiers.json"),
|
||||
outputPath: path.join(rootDir, "packs", "metiers"),
|
||||
type: "Item",
|
||||
},
|
||||
{
|
||||
sourcePath: path.join(rootDir, "packs-src", "sortileges.json"),
|
||||
outputPath: path.join(rootDir, "packs", "sortileges"),
|
||||
type: "Item",
|
||||
},
|
||||
]
|
||||
|
||||
const now = Date.now()
|
||||
const systemId = systemJson.id
|
||||
const systemVersion = packageJson.version
|
||||
const coreVersion = String(systemJson.compatibility?.verified ?? systemJson.compatibility?.minimum ?? "")
|
||||
|
||||
function slugId(input) {
|
||||
const hash = crypto.createHash("sha256").update(input).digest()
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
let id = ""
|
||||
for (let index = 0; id.length < 16; index += 1) {
|
||||
id += alphabet[hash[index % hash.length] % alphabet.length]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function toPackDocument(entry, index) {
|
||||
const docId = slugId(`${entry.type}:${entry.name}`)
|
||||
return {
|
||||
name: entry.name,
|
||||
type: entry.type,
|
||||
img: entry.img ?? "icons/svg/item-bag.svg",
|
||||
system: entry.system ?? {},
|
||||
effects: Array.isArray(entry.effects) ? entry.effects : [],
|
||||
flags: entry.flags ?? {},
|
||||
_stats: {
|
||||
systemId,
|
||||
systemVersion,
|
||||
coreVersion,
|
||||
createdTime: now,
|
||||
modifiedTime: now,
|
||||
lastModifiedBy: "Copilot",
|
||||
compendiumSource: null,
|
||||
duplicateSource: null,
|
||||
exportSource: null,
|
||||
},
|
||||
_id: docId,
|
||||
folder: null,
|
||||
sort: index * 1000,
|
||||
ownership: {
|
||||
default: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPack({ sourcePath, outputPath, type }) {
|
||||
const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"))
|
||||
if (!Array.isArray(source)) {
|
||||
throw new Error(`Pack source must be an array: ${sourcePath}`)
|
||||
}
|
||||
|
||||
fs.rmSync(outputPath, { recursive: true, force: true })
|
||||
fs.mkdirSync(outputPath, { recursive: true })
|
||||
|
||||
const db = new Level(outputPath, { valueEncoding: "utf8" })
|
||||
|
||||
try {
|
||||
await db.open()
|
||||
const batch = db.batch()
|
||||
source.forEach((entry, index) => {
|
||||
if (!entry.type) {
|
||||
throw new Error(`Missing document type in ${sourcePath}: ${entry.name}`)
|
||||
}
|
||||
const doc = toPackDocument(entry, index)
|
||||
batch.put(`!items!${doc._id}`, JSON.stringify(doc))
|
||||
})
|
||||
await batch.write()
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
}
|
||||
|
||||
for (const pack of PACK_SOURCES) {
|
||||
await buildPack(pack)
|
||||
}
|
||||
await buildPacks({
|
||||
sourceRoot: path.join(rootDir, "packs-src"),
|
||||
outputRoot: path.join(rootDir, "packs"),
|
||||
documentSystemId: systemJson.id,
|
||||
documentSystemVersion: packageJson.version,
|
||||
coreVersion: String(systemJson.compatibility?.verified ?? systemJson.compatibility?.minimum ?? ""),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import crypto from "node:crypto"
|
||||
|
||||
import { Level } from "level"
|
||||
|
||||
export const PACK_DEFINITIONS = [
|
||||
{ sourceFile: "armes.json", outputFolder: "armes", type: "Item" },
|
||||
{ sourceFile: "armures.json", outputFolder: "armures", type: "Item" },
|
||||
{ sourceFile: "equipements.json", outputFolder: "equipements", type: "Item" },
|
||||
{ sourceFile: "pouvoirs-compagnie.json", outputFolder: "pouvoirs-compagnie", type: "Item" },
|
||||
{ sourceFile: "competences.json", outputFolder: "competences", type: "Item" },
|
||||
{ sourceFile: "races.json", outputFolder: "races", type: "Item" },
|
||||
{ sourceFile: "tribus.json", outputFolder: "tribus", type: "Item" },
|
||||
{ sourceFile: "metiers.json", outputFolder: "metiers", type: "Item" },
|
||||
{ sourceFile: "sortileges.json", outputFolder: "sortileges", type: "Item" },
|
||||
]
|
||||
|
||||
function slugId(input) {
|
||||
const hash = crypto.createHash("sha256").update(input).digest()
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
let id = ""
|
||||
for (let index = 0; id.length < 16; index += 1) {
|
||||
id += alphabet[hash[index % hash.length] % alphabet.length]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function toPackDocument(entry, index, {
|
||||
documentSystemId,
|
||||
documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime,
|
||||
lastModifiedBy = "Copilot",
|
||||
} = {}) {
|
||||
const docId = slugId(`${entry.type}:${entry.name}`)
|
||||
return {
|
||||
name: entry.name,
|
||||
type: entry.type,
|
||||
img: entry.img ?? "icons/svg/item-bag.svg",
|
||||
system: entry.system ?? {},
|
||||
effects: Array.isArray(entry.effects) ? entry.effects : [],
|
||||
flags: entry.flags ?? {},
|
||||
_stats: {
|
||||
systemId: documentSystemId,
|
||||
systemVersion: documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime,
|
||||
modifiedTime: createdTime,
|
||||
lastModifiedBy,
|
||||
compendiumSource: null,
|
||||
duplicateSource: null,
|
||||
exportSource: null,
|
||||
},
|
||||
_id: docId,
|
||||
folder: null,
|
||||
sort: index * 1000,
|
||||
ownership: {
|
||||
default: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPack({
|
||||
sourcePath,
|
||||
outputPath,
|
||||
type,
|
||||
documentSystemId,
|
||||
documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime,
|
||||
lastModifiedBy,
|
||||
}) {
|
||||
const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"))
|
||||
if (!Array.isArray(source)) {
|
||||
throw new Error(`Pack source must be an array: ${sourcePath}`)
|
||||
}
|
||||
|
||||
fs.rmSync(outputPath, { recursive: true, force: true })
|
||||
fs.mkdirSync(outputPath, { recursive: true })
|
||||
|
||||
const db = new Level(outputPath, { valueEncoding: "utf8" })
|
||||
|
||||
try {
|
||||
await db.open()
|
||||
const batch = db.batch()
|
||||
source.forEach((entry, index) => {
|
||||
if (!entry.type) {
|
||||
throw new Error(`Missing document type in ${sourcePath}: ${entry.name}`)
|
||||
}
|
||||
const doc = toPackDocument(entry, index, {
|
||||
documentSystemId,
|
||||
documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime,
|
||||
lastModifiedBy,
|
||||
})
|
||||
batch.put(`!items!${doc._id}`, JSON.stringify(doc))
|
||||
})
|
||||
await batch.write()
|
||||
} finally {
|
||||
await db.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildPacks({
|
||||
sourceRoot,
|
||||
outputRoot,
|
||||
packDefinitions = PACK_DEFINITIONS,
|
||||
documentSystemId,
|
||||
documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime = Date.now(),
|
||||
lastModifiedBy = "Copilot",
|
||||
}) {
|
||||
for (const pack of packDefinitions) {
|
||||
await buildPack({
|
||||
sourcePath: path.join(sourceRoot, pack.sourceFile),
|
||||
outputPath: path.join(outputRoot, pack.outputFolder),
|
||||
type: pack.type,
|
||||
documentSystemId,
|
||||
documentSystemVersion,
|
||||
coreVersion,
|
||||
createdTime,
|
||||
lastModifiedBy,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
import { PACK_DEFINITIONS, buildPacks } from "./pack-builder.mjs"
|
||||
|
||||
const systemRoot = path.resolve(import.meta.dirname, "..")
|
||||
const targetRoot = path.resolve(
|
||||
process.env.FVTT_LES_OUBLIES_BASE_ROOT || path.join(systemRoot, "..", "fvtt-les-oublies-base"),
|
||||
)
|
||||
const systemManifestPath = path.join(systemRoot, "system.json")
|
||||
const systemPackagePath = path.join(systemRoot, "package.json")
|
||||
const systemSourceRoot = path.join(systemRoot, "packs-src")
|
||||
const targetSourceRoot = path.join(targetRoot, "packs-src")
|
||||
const targetPacksRoot = path.join(targetRoot, "packs")
|
||||
const moduleRepoUrl = "https://www.uberwald.me/gitea/public/fvtt-les-oublies-base"
|
||||
|
||||
const systemManifest = JSON.parse(fs.readFileSync(systemManifestPath, "utf8"))
|
||||
const systemPackage = JSON.parse(fs.readFileSync(systemPackagePath, "utf8"))
|
||||
const richFieldMap = Object.fromEntries(
|
||||
Object.entries(systemManifest.documentTypes?.Item ?? {}).map(([type, data]) => [type, data.htmlFields ?? []]),
|
||||
)
|
||||
const coreVersion = String(systemManifest.compatibility?.verified ?? systemManifest.compatibility?.minimum ?? "")
|
||||
const basePackDefinitions = PACK_DEFINITIONS.map((pack) => ({
|
||||
...pack,
|
||||
outputFolder: `base-${pack.outputFolder}`,
|
||||
}))
|
||||
|
||||
function setDeepValue(target, propertyPath, value) {
|
||||
const segments = String(propertyPath || "").split(".").filter(Boolean)
|
||||
if (!segments.length) return
|
||||
let cursor = target
|
||||
while (segments.length > 1) {
|
||||
const segment = segments.shift()
|
||||
if (!(segment in cursor) || typeof cursor[segment] !== "object" || cursor[segment] === null) {
|
||||
cursor[segment] = {}
|
||||
}
|
||||
cursor = cursor[segment]
|
||||
}
|
||||
cursor[segments[0]] = value
|
||||
}
|
||||
|
||||
function sanitizeEntries(entries = []) {
|
||||
let clearedFields = 0
|
||||
const sanitized = entries.map((entry) => {
|
||||
const fields = richFieldMap[entry.type] ?? []
|
||||
if (!fields.length) return entry
|
||||
|
||||
const clone = structuredClone(entry)
|
||||
clone.system ??= {}
|
||||
for (const fieldPath of fields) {
|
||||
setDeepValue(clone.system, fieldPath, "")
|
||||
clearedFields += 1
|
||||
}
|
||||
return clone
|
||||
})
|
||||
|
||||
return { sanitized, clearedFields }
|
||||
}
|
||||
|
||||
function countNonEmptyRichFields(entries = []) {
|
||||
let nonEmpty = 0
|
||||
for (const entry of entries) {
|
||||
for (const fieldPath of richFieldMap[entry.type] ?? []) {
|
||||
const value = fieldPath
|
||||
.split(".")
|
||||
.reduce((cursor, segment) => cursor?.[segment], entry.system ?? {})
|
||||
if ((value ?? "") !== "") nonEmpty += 1
|
||||
}
|
||||
}
|
||||
return nonEmpty
|
||||
}
|
||||
|
||||
function parseJsonArray(rawText, filePath) {
|
||||
const parsed = JSON.parse(rawText)
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
parsed,
|
||||
entries: null,
|
||||
isArray: false,
|
||||
error: `${filePath} must contain a JSON array`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parsed,
|
||||
entries: parsed,
|
||||
isArray: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
function ensureWritableTargetRoot() {
|
||||
const parentDir = path.dirname(targetRoot)
|
||||
fs.mkdirSync(parentDir, { recursive: true })
|
||||
fs.accessSync(parentDir, fs.constants.W_OK)
|
||||
fs.mkdirSync(targetRoot, { recursive: true })
|
||||
fs.accessSync(targetRoot, fs.constants.W_OK)
|
||||
}
|
||||
|
||||
function ensureTargetModuleScaffold() {
|
||||
ensureWritableTargetRoot()
|
||||
fs.mkdirSync(targetSourceRoot, { recursive: true })
|
||||
fs.mkdirSync(targetPacksRoot, { recursive: true })
|
||||
|
||||
const moduleManifestPath = path.join(targetRoot, "module.json")
|
||||
|
||||
const moduleManifest = {
|
||||
id: "fvtt-les-oublies-base",
|
||||
title: "Les Oubliés Base",
|
||||
description: "Module de contenu pour Les Oubliés, conservant les compendiums complets avec leurs textes descriptifs.",
|
||||
manifest: `${moduleRepoUrl}/raw/branch/main/module.json`,
|
||||
download: "#{DOWNLOAD}#",
|
||||
url: moduleRepoUrl,
|
||||
version: systemPackage.version,
|
||||
authors: [
|
||||
{
|
||||
name: "Copilot",
|
||||
flags: {},
|
||||
},
|
||||
],
|
||||
compatibility: systemManifest.compatibility,
|
||||
relationships: {
|
||||
requires: [
|
||||
{
|
||||
id: systemManifest.id,
|
||||
type: "system",
|
||||
compatibility: {
|
||||
minimum: systemManifest.compatibility?.minimum ?? undefined,
|
||||
verified: systemManifest.compatibility?.verified ?? undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
systems: [
|
||||
{
|
||||
id: systemManifest.id,
|
||||
type: "system",
|
||||
compatibility: {
|
||||
minimum: systemManifest.compatibility?.minimum ?? undefined,
|
||||
verified: systemManifest.compatibility?.verified ?? undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
packs: (systemManifest.packs ?? []).map((pack) => ({
|
||||
...pack,
|
||||
name: `base-${pack.name}`,
|
||||
path: `packs/base-${pack.name}`,
|
||||
system: systemManifest.id,
|
||||
})),
|
||||
}
|
||||
|
||||
fs.writeFileSync(moduleManifestPath, `${JSON.stringify(moduleManifest, null, 2)}\n`)
|
||||
}
|
||||
|
||||
function pruneStalePackDirectories(outputRoot, expectedDefinitions) {
|
||||
if (!fs.existsSync(outputRoot)) return
|
||||
const expected = new Set(expectedDefinitions.map((definition) => definition.outputFolder))
|
||||
for (const entry of fs.readdirSync(outputRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (expected.has(entry.name)) continue
|
||||
fs.rmSync(path.join(outputRoot, entry.name), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function copyAndSanitizeSources() {
|
||||
const summaries = []
|
||||
const sourceFiles = fs.readdirSync(systemSourceRoot)
|
||||
.filter((entry) => entry.endsWith(".json"))
|
||||
.sort((left, right) => left.localeCompare(right, "fr"))
|
||||
|
||||
for (const fileName of sourceFiles) {
|
||||
const systemSourcePath = path.join(systemSourceRoot, fileName)
|
||||
const targetSourcePath = path.join(targetSourceRoot, fileName)
|
||||
const rawText = fs.readFileSync(systemSourcePath, "utf8")
|
||||
const systemJson = parseJsonArray(rawText, systemSourcePath)
|
||||
const targetRawText = fs.existsSync(targetSourcePath) ? fs.readFileSync(targetSourcePath, "utf8") : null
|
||||
const targetJson = targetRawText ? parseJsonArray(targetRawText, targetSourcePath) : null
|
||||
const systemRichCount = systemJson.isArray ? countNonEmptyRichFields(systemJson.entries) : -1
|
||||
const targetRichCount = targetJson?.isArray ? countNonEmptyRichFields(targetJson.entries) : -1
|
||||
const authoritativeRawText = targetRichCount > systemRichCount ? targetRawText : rawText
|
||||
|
||||
fs.writeFileSync(targetSourcePath, authoritativeRawText)
|
||||
|
||||
if (!systemJson.isArray) {
|
||||
summaries.push({ fileName, clearedFields: 0, copiedOnly: true })
|
||||
continue
|
||||
}
|
||||
|
||||
const { sanitized, clearedFields } = sanitizeEntries(systemJson.entries)
|
||||
fs.writeFileSync(systemSourcePath, `${JSON.stringify(sanitized, null, 2)}\n`)
|
||||
summaries.push({ fileName, clearedFields, copiedOnly: false })
|
||||
}
|
||||
|
||||
return summaries
|
||||
}
|
||||
|
||||
ensureTargetModuleScaffold()
|
||||
const summaries = copyAndSanitizeSources()
|
||||
pruneStalePackDirectories(targetPacksRoot, basePackDefinitions)
|
||||
|
||||
await buildPacks({
|
||||
sourceRoot: systemSourceRoot,
|
||||
outputRoot: path.join(systemRoot, "packs"),
|
||||
packDefinitions: PACK_DEFINITIONS,
|
||||
documentSystemId: systemManifest.id,
|
||||
documentSystemVersion: systemPackage.version,
|
||||
coreVersion,
|
||||
})
|
||||
|
||||
await buildPacks({
|
||||
sourceRoot: targetSourceRoot,
|
||||
outputRoot: targetPacksRoot,
|
||||
packDefinitions: basePackDefinitions,
|
||||
documentSystemId: systemManifest.id,
|
||||
documentSystemVersion: systemPackage.version,
|
||||
coreVersion,
|
||||
})
|
||||
|
||||
console.info(`Base module root: ${targetRoot}`)
|
||||
|
||||
for (const summary of summaries) {
|
||||
if (summary.copiedOnly) {
|
||||
console.info(`${summary.fileName}: copied as-is`)
|
||||
} else {
|
||||
console.info(`${summary.fileName}: cleared ${summary.clearedFields} rich fields in system source`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user