Guard camp event ids
This commit is contained in:
@@ -6,6 +6,11 @@ const errors = [];
|
||||
|
||||
const itemRewardArrays = collectItemRewardArrays(source);
|
||||
itemRewardArrays.forEach(({ body, index, offset }) => validateItemRewards(body, index, offset));
|
||||
const campBattleIdKeys = collectCampBattleIdKeys(source);
|
||||
const dialogueEvents = collectCampEvents(source, 'campDialogues');
|
||||
const visitEvents = collectCampEvents(source, 'campVisits');
|
||||
validateCampEventCollection(dialogueEvents, 'campDialogues');
|
||||
validateCampEventCollection(visitEvents, 'campVisits');
|
||||
validateNumericProperties('gold', { min: 1, max: 20000 });
|
||||
validateNumericProperties('bondExp', { min: 0, max: 200 });
|
||||
validateNumericProperties('rewardExp', { min: 1, max: 200 });
|
||||
@@ -14,7 +19,9 @@ if (errors.length > 0) {
|
||||
throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
|
||||
}
|
||||
|
||||
console.log(`Verified ${itemRewardArrays.length} camp item reward lists and camp reward numeric fields.`);
|
||||
console.log(
|
||||
`Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${itemRewardArrays.length} camp item reward lists, and camp reward numeric fields.`
|
||||
);
|
||||
|
||||
function collectItemRewardArrays(text) {
|
||||
return [...text.matchAll(/itemRewards:\s*\[([\s\S]*?)\]/g)].map((match, index) => ({
|
||||
@@ -56,6 +63,73 @@ function validateNumericProperties(propertyName, { min, max }) {
|
||||
}
|
||||
}
|
||||
|
||||
function collectCampBattleIdKeys(text) {
|
||||
const objectSource = extractConstObject(text, 'campBattleIds');
|
||||
return new Set([...objectSource.matchAll(/^\s*(\w+):/gm)].map((match) => match[1]));
|
||||
}
|
||||
|
||||
function collectCampEvents(text, arrayName) {
|
||||
const { body, offset } = extractConstArray(text, arrayName);
|
||||
return collectTopLevelObjects(body).map((event) => ({
|
||||
...event,
|
||||
offset: offset + event.offset
|
||||
}));
|
||||
}
|
||||
|
||||
function validateCampEventCollection(events, collectionName) {
|
||||
const seenIds = new Map();
|
||||
if (events.length === 0) {
|
||||
errors.push(`${sourcePath}: ${collectionName} has no entries`);
|
||||
}
|
||||
|
||||
events.forEach((event, index) => {
|
||||
const context = `${collectionName}[${index}]`;
|
||||
const idEntries = collectStringProperties(event.body, 'id');
|
||||
const eventId = idEntries[0]?.value ?? '';
|
||||
if (!eventId.trim()) {
|
||||
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no id`);
|
||||
} else if (seenIds.has(eventId)) {
|
||||
errors.push(
|
||||
`${sourcePath}:${lineNumber(event.offset)} ${context} duplicates id "${eventId}" first used at ${sourcePath}:${seenIds.get(
|
||||
eventId
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
seenIds.set(eventId, lineNumber(event.offset));
|
||||
}
|
||||
|
||||
const choiceIds = idEntries.slice(1).map((entry) => entry.value);
|
||||
const seenChoiceIds = new Set();
|
||||
choiceIds.forEach((choiceId, choiceIndex) => {
|
||||
if (!choiceId.trim()) {
|
||||
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context}.choices[${choiceIndex}] has an empty id`);
|
||||
}
|
||||
if (seenChoiceIds.has(choiceId)) {
|
||||
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has duplicate choice id "${choiceId}"`);
|
||||
}
|
||||
seenChoiceIds.add(choiceId);
|
||||
});
|
||||
|
||||
const availableMatch = /availableAfterBattleIds:\s*\[([\s\S]*?)\]/.exec(event.body);
|
||||
if (!availableMatch || availableMatch[1].trim().length === 0) {
|
||||
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no availableAfterBattleIds`);
|
||||
}
|
||||
for (const reference of event.body.matchAll(/campBattleIds\.(\w+)/g)) {
|
||||
if (!campBattleIdKeys.has(reference[1])) {
|
||||
errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} references unknown campBattleIds.${reference[1]}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function collectStringProperties(text, propertyName) {
|
||||
const pattern = new RegExp(`\\b${propertyName}:\\s*'((?:\\\\'|[^'])*)'`, 'g');
|
||||
return [...text.matchAll(pattern)].map((match) => ({
|
||||
value: match[1].replace(/\\'/g, "'"),
|
||||
offset: match.index ?? 0
|
||||
}));
|
||||
}
|
||||
|
||||
function parseRewardLabel(reward) {
|
||||
const normalized = String(reward).replace(/\s+/g, ' ').trim();
|
||||
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);
|
||||
@@ -72,3 +146,102 @@ function parseRewardLabel(reward) {
|
||||
function lineNumber(offset) {
|
||||
return source.slice(0, offset).split(/\r?\n/).length;
|
||||
}
|
||||
|
||||
function extractConstObject(text, objectName) {
|
||||
const startPattern = new RegExp(`const\\s+${objectName}\\b[^=]*=\\s*\\{`);
|
||||
const match = startPattern.exec(text);
|
||||
if (!match) {
|
||||
throw new Error(`Cannot find ${objectName}.`);
|
||||
}
|
||||
const start = match.index + match[0].length - 1;
|
||||
const end = findMatchingDelimiter(text, start, '{', '}');
|
||||
return text.slice(start, end + 1);
|
||||
}
|
||||
|
||||
function extractConstArray(text, arrayName) {
|
||||
const startPattern = new RegExp(`const\\s+${arrayName}\\b[\\s\\S]*?=\\s*\\[`);
|
||||
const match = startPattern.exec(text);
|
||||
if (!match) {
|
||||
throw new Error(`Cannot find ${arrayName}.`);
|
||||
}
|
||||
const start = match.index + match[0].length - 1;
|
||||
const end = findMatchingDelimiter(text, start, '[', ']');
|
||||
return {
|
||||
body: text.slice(start + 1, end),
|
||||
offset: start + 1
|
||||
};
|
||||
}
|
||||
|
||||
function collectTopLevelObjects(text) {
|
||||
const objects = [];
|
||||
let depth = 0;
|
||||
let start = -1;
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const char = text[index];
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quote) {
|
||||
quote = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === "'" || char === '"' || char === '`') {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === '{') {
|
||||
if (depth === 0) {
|
||||
start = index;
|
||||
}
|
||||
depth += 1;
|
||||
} else if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0 && start >= 0) {
|
||||
objects.push({ body: text.slice(start, index + 1), offset: start });
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
function findMatchingDelimiter(text, start, openChar, closeChar) {
|
||||
let depth = 0;
|
||||
let quote = '';
|
||||
let escaped = false;
|
||||
|
||||
for (let index = start; index < text.length; index += 1) {
|
||||
const char = text[index];
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quote) {
|
||||
quote = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === "'" || char === '"' || char === '`') {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === openChar) {
|
||||
depth += 1;
|
||||
} else if (char === closeChar) {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot find matching ${closeChar} from offset ${start}.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user