rune
Version:
CLI to upload your games to Rune
258 lines (253 loc) • 10.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateGameFiles = exports.validateGameFilesWithEval = exports.parseGameIndexHtml = exports.validationOptions = exports.MAX_PLAYERS = void 0;
const eslint_1 = require("eslint");
const eslint_plugin_rune_1 = __importDefault(require("eslint-plugin-rune"));
const node_html_parser_1 = require("node-html-parser");
const path_1 = __importDefault(require("path"));
const semver_1 = __importDefault(require("semver"));
const getGameFiles_js_1 = require("./getGameFiles.js");
exports.MAX_PLAYERS = 6;
exports.validationOptions = {
sdkUrlStartOldRune: "https://cdn.jsdelivr.net/npm/rune-games-sdk",
sdkUrlStartRune: "https://cdn.jsdelivr.net/npm/rune-sdk",
sdkUrlStartDusk: "https://cdn.jsdelivr.net/npm/dusk-games-sdk",
sdkVersionRegex: /(?:rune|rune-games|dusk-games)-sdk@(\d+(\.\d+(\.\d+)?)?)/,
minSdkVersion: "4.8.1",
maxFiles: 1000,
maxSizeMb: 25,
};
const eslint = new eslint_1.ESLint({
overrideConfigFile: true,
allowInlineConfig: false,
baseConfig: [
{
languageOptions: {
sourceType: "module",
},
},
//@ts-expect-error - For some reason types not working here
eslint_plugin_rune_1.default.configs.logic,
],
});
function parseGameIndexHtml(indexHtmlContent) {
if (!(0, node_html_parser_1.valid)(indexHtmlContent))
return null;
const { sdkUrlStartRune, sdkUrlStartOldRune, sdkUrlStartDusk } = exports.validationOptions;
const parsedIndexHtml = (0, node_html_parser_1.parse)(indexHtmlContent);
const scripts = parsedIndexHtml.getElementsByTagName("script");
const sdkScript = scripts.find((script) => script.getAttribute("src")?.startsWith(sdkUrlStartDusk) ||
script.getAttribute("src")?.startsWith(sdkUrlStartRune) ||
script.getAttribute("src")?.startsWith(sdkUrlStartOldRune));
return { parsedIndexHtml, scripts, sdkScript };
}
exports.parseGameIndexHtml = parseGameIndexHtml;
async function validateGameFilesWithEval(logicRunner, files) {
const logicJs = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "logic.js");
//Remove export { ... } from logic.js
const logicWithoutExports = logicJs?.content?.replace(/export {[^}]*}/gm, "");
const gameConfig = logicJs
? eval(`
//Prevent Math precision from being modified
globalThis.Math.__SDK_PRECISION_SET__ = true
${logicRunner}
${logicWithoutExports}
const bindings = globalThis.RUNE_FUNCTION_PREFIX_getLogicRunnerBindings()
if (!bindings) {
const invalidConfig = {}
invalidConfig
} else {
bindings.getConfig()
}
`)
: {};
return validateGameFiles(files, gameConfig);
}
exports.validateGameFilesWithEval = validateGameFilesWithEval;
async function validateGameFiles(files, gameConfig) {
const { sdkVersionRegex, minSdkVersion, maxFiles, maxSizeMb } = exports.validationOptions;
const errors = [];
let sdkName = "Rune";
if (files.length > maxFiles) {
errors.push({ message: `Too many files (>${maxFiles})` });
}
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
if (totalSize > maxSizeMb * 1e6) {
errors.push({ message: `Game size must be less than ${maxSizeMb}MB` });
}
const indexHtml = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "index.html");
const logicJs = (0, getGameFiles_js_1.findShortestPathFileThatEndsWith)(files, "logic.js");
if (logicJs && logicJs.size > 1e6) {
errors.push({ message: `logic.js size can't be more than 1MB` });
}
if (!indexHtml) {
errors.push({ message: "Game must include index.html" });
}
else if (!indexHtml.content) {
errors.push({
message: "index.html content has not been provided for validation",
});
}
else {
const gameIndexHtmlElements = parseGameIndexHtml(indexHtml.content);
if (!gameIndexHtmlElements) {
errors.push({ message: "index.html is not valid HTML" });
}
else if (!gameIndexHtmlElements.sdkScript) {
errors.push({
message: `Game index.html must include ${sdkName} SDK script`,
});
}
else {
const { sdkScript, scripts } = gameIndexHtmlElements;
const { sdkUrlStartDusk, sdkUrlStartRune, sdkUrlStartOldRune } = exports.validationOptions;
sdkName = scripts.some((script) => script.getAttribute("src")?.startsWith(sdkUrlStartDusk))
? "Dusk"
: "Rune";
if (scripts.filter((script) => script.getAttribute("src")?.startsWith(sdkUrlStartRune) ||
script.getAttribute("src")?.startsWith(sdkUrlStartOldRune) ||
script.getAttribute("src")?.startsWith(sdkUrlStartDusk)).length > 1) {
errors.push({
message: `${sdkName} SDK is imported 2+ times in index.html. If using the ${sdkName} Vite plugin, then remove your SDK import in index.html.`,
});
}
if (sdkScript.getAttribute("src")?.endsWith("/multiplayer.js") ||
sdkScript.getAttribute("src")?.endsWith("/multiplayer-dev.js")) {
await validateMultiplayer({
errors,
gameConfig,
logicJs,
indexHtml,
});
if (scripts.indexOf(sdkScript) !== 0) {
errors.push({
message: `${sdkName} SDK must be the first script in index.html`,
});
}
const sdkVersion = sdkScript
.getAttribute("src")
?.match(sdkVersionRegex)?.[1];
if (!sdkVersion) {
errors.push({ message: `${sdkName} SDK must specify a version` });
}
const [major, minor, patch] = (sdkVersion ?? "").split(".");
const maxedOutSdkVersion = `${major}.${minor ?? 999}.${patch ?? 999}`;
const sdkVersionCoerced = semver_1.default.coerce(sdkVersion && maxedOutSdkVersion);
const minSdkVersionCoerced = semver_1.default.coerce(minSdkVersion);
if (sdkVersionCoerced &&
minSdkVersionCoerced &&
semver_1.default.lt(sdkVersionCoerced, minSdkVersionCoerced)) {
errors.push({
message: `${sdkName} SDK is below minimum version (included ${sdkVersion}, min ${minSdkVersion})`,
});
}
}
else {
errors.push({
message: `${sdkName} SDK script url must end with /multiplayer.js or /multiplayer-dev.js`,
});
}
}
}
return {
valid: errors.length === 0,
errors,
sdk: sdkName,
};
}
exports.validateGameFiles = validateGameFiles;
async function validateMultiplayer({ errors, gameConfig, logicJs, indexHtml, }) {
if (!logicJs) {
errors.push({
message: "logic.js must be included in the game files",
});
}
else {
if (path_1.default.dirname(indexHtml.path) !== path_1.default.dirname(logicJs.path)) {
errors.push({
message: "logic.js must be in the same directory as index.html",
});
}
return validateMultiplayerLogicJsContent({
logicJs,
gameConfig,
errors,
});
}
}
async function validateMultiplayerLogicJsContent({ logicJs, gameConfig, errors, }) {
if (!logicJs.content) {
errors.push({
message: "logic.js content has not been provided for validation",
});
}
else {
await eslint
.lintText(logicJs.content)
.then((results) => {
const result = results.at(0);
if (result) {
const lintErrors = result.messages.filter((err) => err.severity === 2);
if (lintErrors.length > 0) {
errors.push({
message: "logic.js contains invalid code",
lintErrors,
});
}
}
else {
errors.push({ message: "failed to lint logic.js" });
}
})
.catch(() => {
errors.push({ message: "failed to lint logic.js" });
});
if (!gameConfig.minPlayers ||
typeof gameConfig.minPlayers !== "number" ||
isNaN(gameConfig.minPlayers)) {
errors.push({
message: "logic.js: minPlayers not found or is invalid",
});
}
else if (gameConfig.minPlayers &&
(gameConfig.minPlayers < 1 || gameConfig.minPlayers > exports.MAX_PLAYERS)) {
errors.push({
message: `logic.js: minPlayers must be between 1 and ${exports.MAX_PLAYERS}`,
});
}
if (!gameConfig.maxPlayers ||
typeof gameConfig.maxPlayers !== "number" ||
isNaN(gameConfig.maxPlayers)) {
errors.push({
message: "logic.js: maxPlayers not found or is invalid",
});
}
else if (gameConfig.maxPlayers &&
(gameConfig.maxPlayers < 1 || gameConfig.maxPlayers > exports.MAX_PLAYERS)) {
errors.push({
message: `logic.js: maxPlayers must be between 1 and ${exports.MAX_PLAYERS}`,
});
}
if (gameConfig.maxPlayers &&
gameConfig.minPlayers &&
gameConfig.maxPlayers < gameConfig.minPlayers) {
errors.push({
message: "logic.js: maxPlayers must be greater than or equal to minPlayers",
});
}
if (gameConfig.updatesPerSecond === undefined) {
errors.push({
message: "logic.js: updatesPerSecond must be a constant (updatesPerSecond: 1-30)",
});
}
else if (gameConfig.updatesPerSecond < 1 ||
gameConfig.updatesPerSecond > 30) {
errors.push({
message: "logic.js: updatesPerSecond must be undefined or between 1 and 30",
});
}
}
}