Parse map models and validate them (#13)
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
The enhancements: - Added map validation that checks for some common errors that maps have - /take: Does map validation but will still take the map even if there are problems - /take: Improved the help text and added links to the maptest places - /submit: Does map validation and will not let you submit if there are problems - /submissions: Now shows the map's display name and creator - Added an ESLint config and tidied some things up - Updated some of the packages Reviewed-on: #13 Co-authored-by: Carter Penterman <carterpenterman@gmail.com> Co-committed-by: Carter Penterman <carterpenterman@gmail.com>
This commit is contained in:
parent
f8476577d6
commit
3a167cf686
6
bot.js
6
bot.js
@ -1,8 +1,8 @@
|
|||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const { Client, Collection, Intents } = require("discord.js");
|
const { Client, Collection } = require("discord.js");
|
||||||
const {token} = require("./config/config.json");
|
const {token} = require("./config/config.json");
|
||||||
|
|
||||||
const client = new Client({intents: [Intents.FLAGS.GUILDS]});
|
const client = new Client({intents: ["Guilds"]});
|
||||||
|
|
||||||
client.commands = new Collection();
|
client.commands = new Collection();
|
||||||
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
||||||
@ -29,7 +29,7 @@ client.on('interactionCreate', async interaction => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("error", async error => {
|
client.on("error", async _error => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
const { SlashCommandBuilder } = require('@discordjs/builders');
|
const { SlashCommandBuilder } = require('@discordjs/builders');
|
||||||
const { MessageAttachment } = require("discord.js");
|
const { AttachmentBuilder } = require("discord.js");
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const { submissions, commands } = require("../config/config.js");
|
const { submissions, commands } = require("../config/config.js");
|
||||||
const { parse } = require("csv-parse/sync");
|
const { parse } = require("csv-parse/sync");
|
||||||
const { getSubmissionLine } = require("../common.js");
|
const { getSubmissionLine, safeCsvFormat } = require("../common.js");
|
||||||
const Sugar = require("sugar-date");
|
const Sugar = require("sugar-date");
|
||||||
|
const { Buffer } = require("buffer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ChatInputCommandInteraction} interaction
|
||||||
|
*/
|
||||||
async function execute(interaction) {
|
async function execute(interaction) {
|
||||||
const game = interaction.options.getString("game");
|
const game = interaction.options.getString("game", true);
|
||||||
|
|
||||||
const fname = submissions[game];
|
const fname = submissions[game];
|
||||||
if (fname === undefined) {
|
if (fname === undefined) {
|
||||||
@ -53,7 +57,7 @@ async function execute(interaction) {
|
|||||||
|
|
||||||
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
|
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
|
||||||
|
|
||||||
let csvString = "map_id,unix_timestamp,date_string,user_id,username\n";
|
let csvString = "map_id,unix_timestamp,date_string,user_id,username,display_name,creator\n";
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let lineStr of lines) {
|
for (let lineStr of lines) {
|
||||||
const line = getSubmissionLine(lineStr);
|
const line = getSubmissionLine(lineStr);
|
||||||
@ -66,7 +70,7 @@ async function execute(interaction) {
|
|||||||
|
|
||||||
found = true;
|
found = true;
|
||||||
const dateStr = new Date(line.timestamp * 1000).toLocaleString("en-US", {dateStyle: "short"});
|
const dateStr = new Date(line.timestamp * 1000).toLocaleString("en-US", {dateStyle: "short"});
|
||||||
csvString += `${line.modelId},${line.timestamp},${dateStr},${line.userId},${line.username}\n`;
|
csvString += `${line.modelId},${line.timestamp},${dateStr},${line.userId},${safeCsvFormat(line.username)},${safeCsvFormat(line.displayName)},${safeCsvFormat(line.creator)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
@ -82,7 +86,7 @@ async function execute(interaction) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = new MessageAttachment(Buffer.from(csvString), fname);
|
const file = new AttachmentBuilder(Buffer.from(csvString), { name: fname });
|
||||||
const dateRangeStr = getDateRangeString(afterTimestamp, beforeTimestamp);
|
const dateRangeStr = getDateRangeString(afterTimestamp, beforeTimestamp);
|
||||||
if (dateRangeStr) {
|
if (dateRangeStr) {
|
||||||
await interaction.reply({content: `Using date range ${dateRangeStr}:`, files: [file]});
|
await interaction.reply({content: `Using date range ${dateRangeStr}:`, files: [file]});
|
||||||
|
@ -4,14 +4,14 @@ const fs = require('node:fs');
|
|||||||
const noblox = require("noblox.js");
|
const noblox = require("noblox.js");
|
||||||
const axios = require("axios").default;
|
const axios = require("axios").default;
|
||||||
const { submissions, commands, cookies } = require("../config/config.js");
|
const { submissions, commands, cookies } = require("../config/config.js");
|
||||||
const { AssetType, getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine } = require("../common.js");
|
const { AssetType, getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js");
|
||||||
|
|
||||||
async function robloxUserFromDiscord(id) {
|
async function robloxUserFromDiscord(id) {
|
||||||
if (isNaN(id)) return undefined;
|
if (isNaN(id)) return undefined;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`https://api.fiveman1.net/v1/users/${id}`);
|
const res = await axios.get(`https://api.fiveman1.net/v1/users/${id}`);
|
||||||
return res.data.result.robloxId;
|
return res.data.result.robloxId;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,11 +21,14 @@ async function robloxUsernameFromId(id) {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get(`https://users.roblox.com/v1/users/${id}`);
|
const res = await axios.get(`https://users.roblox.com/v1/users/${id}`);
|
||||||
return res.data.name;
|
return res.data.name;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ChatInputCommandInteraction} interaction
|
||||||
|
*/
|
||||||
async function execute(interaction) {
|
async function execute(interaction) {
|
||||||
const userId = await robloxUserFromDiscord(interaction.user.id);
|
const userId = await robloxUserFromDiscord(interaction.user.id);
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@ -33,7 +36,7 @@ async function execute(interaction) {
|
|||||||
await interaction.reply({content: msg, ephemeral: true});
|
await interaction.reply({content: msg, ephemeral: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const game = interaction.options.getString("game");
|
const game = interaction.options.getString("game", true);
|
||||||
|
|
||||||
const fname = submissions[game];
|
const fname = submissions[game];
|
||||||
if (fname === undefined) {
|
if (fname === undefined) {
|
||||||
@ -45,7 +48,7 @@ async function execute(interaction) {
|
|||||||
fs.writeFileSync(fname, SubmissionColumnsString);
|
fs.writeFileSync(fname, SubmissionColumnsString);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = interaction.options.getInteger("asset_id");
|
const id = interaction.options.getInteger("asset_id", true);
|
||||||
await noblox.setCookie(cookies[game]);
|
await noblox.setCookie(cookies[game]);
|
||||||
|
|
||||||
// Check that the bot owns this model
|
// Check that the bot owns this model
|
||||||
@ -74,23 +77,38 @@ async function execute(interaction) {
|
|||||||
const csvFile = fs.readFileSync(fname);
|
const csvFile = fs.readFileSync(fname);
|
||||||
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
|
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
|
||||||
|
|
||||||
let csvString = SubmissionColumnsString;
|
|
||||||
for (let lineStr of lines) {
|
for (let lineStr of lines) {
|
||||||
const line = getSubmissionLine(lineStr);
|
const line = getSubmissionLine(lineStr);
|
||||||
|
|
||||||
if (id === line.modelId) {
|
if (id === line.modelId) {
|
||||||
await interaction.reply({content: `This map (id: ${id}) was already submitted on <t:${line.timestamp}:d>.`, ephemeral: true});
|
await interaction.reply({content: `This map (id: ${id}) was already submitted on <t:${line.timestamp}:d>.`, ephemeral: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
csvString += createSubmissionLine(line.modelId, line.timestamp, line.userId, line.username);
|
const mapValidatePromise = validateMapAsset(id, game);
|
||||||
|
|
||||||
|
const message = await interaction.reply("⌛ Validating map...");
|
||||||
|
|
||||||
|
const validation = await mapValidatePromise;
|
||||||
|
const msg = getValidationMessage(validation, game, true);
|
||||||
|
await message.edit(msg);
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
await interaction.followUp("Due to having problems, your map was **NOT submitted**.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let csvString = SubmissionColumnsString;
|
||||||
|
for (let lineStr of lines) {
|
||||||
|
const line = getSubmissionLine(lineStr);
|
||||||
|
csvString += createSubmissionLine(line.modelId, line.timestamp, line.userId, line.username, line.displayName, line.creator);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unixTimestamp = Math.round(+new Date()/1000);
|
const unixTimestamp = Math.round(+new Date()/1000);
|
||||||
csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId));
|
csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId), validation.displayName, validation.creator);
|
||||||
fs.writeFileSync(fname, csvString);
|
fs.writeFileSync(fname, csvString);
|
||||||
|
|
||||||
await interaction.reply(`Map (id: ${id}) successfully submitted.`);
|
await interaction.followUp(`Map (id: ${id}) successfully submitted.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
const { SlashCommandBuilder } = require('@discordjs/builders');
|
const { SlashCommandBuilder } = require('@discordjs/builders');
|
||||||
const noblox = require("noblox.js");
|
const noblox = require("noblox.js");
|
||||||
const { cookies, commands } = require("../config/config.js");
|
const { cookies, commands, gamePlaces } = require("../config/config.js");
|
||||||
const { AssetType, getAssetInfo } = require("../common.js");
|
const { AssetType, getAssetInfo, validateMapAsset, getValidationMessage } = require("../common.js");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('discord.js').ChatInputCommandInteraction} interaction
|
||||||
|
*/
|
||||||
async function execute(interaction) {
|
async function execute(interaction) {
|
||||||
const game = interaction.options.getString("game");
|
const game = interaction.options.getString("game", true);
|
||||||
const cookie = cookies[game];
|
const cookie = cookies[game];
|
||||||
if (cookie === undefined) {
|
if (cookie === undefined) {
|
||||||
await interaction.reply({content: "Invalid game specified!", ephemeral: true});
|
await interaction.reply({content: "Invalid game specified!", ephemeral: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = interaction.options.getInteger("asset_id");
|
const id = interaction.options.getInteger("asset_id", true);
|
||||||
await noblox.setCookie(cookie);
|
await noblox.setCookie(cookie);
|
||||||
// Check that the bot doesn't already own this asset
|
// Check that the bot doesn't already own this asset
|
||||||
if (await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset")) {
|
if (await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset")) {
|
||||||
@ -46,16 +49,23 @@ async function execute(interaction) {
|
|||||||
Creator: {
|
Creator: {
|
||||||
Id: assetInfo.creatorId
|
Id: assetInfo.creatorId
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
await noblox.buy({product: productInfo, price: 0});
|
const buyPromise = noblox.buy({product: productInfo, price: 0});
|
||||||
await interaction.reply(
|
const mapValidatePromise = validateMapAsset(id, game);
|
||||||
|
|
||||||
|
const message = await interaction.reply("⌛ Validating map...");
|
||||||
|
|
||||||
|
const validation = await mapValidatePromise;
|
||||||
|
const msg = getValidationMessage(validation, game, false);
|
||||||
|
await message.edit(msg);
|
||||||
|
|
||||||
|
await buyPromise;
|
||||||
|
await interaction.followUp(
|
||||||
`
|
`
|
||||||
Now that your map (id: ${id}) has been taken by the ${game} maptest bot you can load it into the ${game} maptest place. To load your map, join the game and say
|
Now that your [map (id: ${id})](<https://create.roblox.com/store/asset/${id}/>) has been taken by the bot you can load it into the [${game} maptest place](<${gamePlaces[game]}>).
|
||||||
\`\`\`
|
To load your map, join the game and do \`!map ${id}\`. If your map successfully loaded, do \`!rtv\` and then choose your map.
|
||||||
!map ${id}
|
Otherwise, you can expand the chat to view the full error message by clicking and dragging on the edge of the chat.
|
||||||
\`\`\`Read what it says. If your map successfully loaded type !rtv and then choose your map.
|
|
||||||
If it did not load successfully, you can expand the chat to view the full error message by clicking and dragging on the edge of the chat.
|
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
175
common.js
175
common.js
@ -1,4 +1,5 @@
|
|||||||
const axios = require("axios").default;
|
const axios = require("axios").default;
|
||||||
|
const { RobloxModel } = require("rbxm-parser");
|
||||||
|
|
||||||
// https://create.roblox.com/docs/reference/engine/enums/AssetType
|
// https://create.roblox.com/docs/reference/engine/enums/AssetType
|
||||||
const AssetType = {
|
const AssetType = {
|
||||||
@ -34,7 +35,7 @@ async function getAssetInfo(assetId) {
|
|||||||
if (res.status < 200 || res.status > 300) {
|
if (res.status < 200 || res.status > 300) {
|
||||||
return {
|
return {
|
||||||
status: res.status
|
status: res.status
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
@ -49,16 +50,18 @@ async function getAssetInfo(assetId) {
|
|||||||
price: assetInfo.product.price,
|
price: assetInfo.product.price,
|
||||||
productId: assetInfo.product.productId,
|
productId: assetInfo.product.productId,
|
||||||
forSale: assetInfo.product.isForSaleOrIsPublicDomain
|
forSale: assetInfo.product.isForSaleOrIsPublicDomain
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username\n";
|
const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n";
|
||||||
|
|
||||||
const SubmissionColumn = {
|
const SubmissionColumn = {
|
||||||
ModelID: 0,
|
ModelID: 0,
|
||||||
UnixTimestamp: 1,
|
UnixTimestamp: 1,
|
||||||
UserID: 2,
|
UserID: 2,
|
||||||
Username: 3
|
Username: 3,
|
||||||
|
DisplayName: 4,
|
||||||
|
Creator: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSubmissionLine(line) {
|
function getSubmissionLine(line) {
|
||||||
@ -66,6 +69,8 @@ function getSubmissionLine(line) {
|
|||||||
const timestamp = Number(line[SubmissionColumn.UnixTimestamp]);
|
const timestamp = Number(line[SubmissionColumn.UnixTimestamp]);
|
||||||
let userId = "";
|
let userId = "";
|
||||||
let username = "";
|
let username = "";
|
||||||
|
let displayName = "";
|
||||||
|
let creator = "";
|
||||||
|
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
if (line.length > 2) {
|
if (line.length > 2) {
|
||||||
@ -73,16 +78,170 @@ function getSubmissionLine(line) {
|
|||||||
username = line[SubmissionColumn.Username];
|
username = line[SubmissionColumn.Username];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.length > 4) {
|
||||||
|
displayName = line[SubmissionColumn.DisplayName];
|
||||||
|
creator = line[SubmissionColumn.Creator];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
username: username
|
username: username,
|
||||||
|
displayName: displayName,
|
||||||
|
creator: creator
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSubmissionLine(modelId, timestamp, userId, username) {
|
function safeCsvFormat(cell) {
|
||||||
return `${modelId},${timestamp},${userId},${username}\n`;
|
// https://stackoverflow.com/questions/46637955/write-a-string-containing-commas-and-double-quotes-to-csv
|
||||||
|
if (cell.replace(/ /g, '').match(/[\s,"]/)) {
|
||||||
|
return '"' + cell.replace(/"/g, '""') + '"';
|
||||||
|
}
|
||||||
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { AssetType, getAssetInfo, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine };
|
function createSubmissionLine(modelId, timestamp, userId, username, displayName, creator) {
|
||||||
|
return `${modelId},${timestamp},${userId},${safeCsvFormat(username)},${safeCsvFormat(displayName)},${safeCsvFormat(creator)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RobloxModel} model
|
||||||
|
*/
|
||||||
|
function getMapStringValues(model) {
|
||||||
|
if (model.Roots.length < 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = model.Roots[0];
|
||||||
|
const values = root.FindChildrenOfClass("StringValue", (child) => child.Name === "DisplayName" || child.Name === "Creator");
|
||||||
|
if (values.length !== 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name, creator;
|
||||||
|
if (values[0].Name === "DisplayName") {
|
||||||
|
name = values[0].Value;
|
||||||
|
creator = values[1].Value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
name = values[1].Value;
|
||||||
|
creator = values[0].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: name,
|
||||||
|
creator: creator
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalize(str) {
|
||||||
|
if (!str) return "";
|
||||||
|
return str[0].toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateMapAsset(assetId, game) {
|
||||||
|
const model = await RobloxModel.ReadFromAssetId(assetId);
|
||||||
|
if (!model) {
|
||||||
|
// For whatever reason we couldn't parse the model, so we'll just skip doing validation
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
displayName: "",
|
||||||
|
creator: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (model.Roots.length > 1) {
|
||||||
|
errors.push("Your map has more than one root part, it must have a single `Model` as the root.");
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = model.Roots[0];
|
||||||
|
if (!root.IsA("Model")) {
|
||||||
|
errors.push(`The root part of your model is a \`${root.ClassName}\`, it needs to be a \`Model\` instead.`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const prefix = (game === "deathrun") ? "dr" : game;
|
||||||
|
if (!root.Name.startsWith(prefix + "_")) {
|
||||||
|
errors.push(`Your root model's name is \`${root.Name}\`, its name must start with \`${prefix}_\`.`);
|
||||||
|
}
|
||||||
|
if (!/^[a-z0-9_]*$/.test(root.Name)) {
|
||||||
|
errors.push(`Your root model's name is \`${root.Name}\` which contains invalid characters. It must only contain lowercase alphanumeric characters separated by underscores.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = getMapStringValues(model);
|
||||||
|
if (!values) {
|
||||||
|
errors.push("Your map is missing a `StringValue` named `Creator` and/or a `StringValue` named `DisplayName`. You must add both to your map and they must be parented directly to the root model. These are used to set the map name and creator in-game.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!values.creator) {
|
||||||
|
errors.push("Your map's `Creator` `StringValue` does not have a `Value`.");
|
||||||
|
}
|
||||||
|
if (!values.displayName) {
|
||||||
|
errors.push("Your map's `DisplayName` `StringValue` does not have a `Value`.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const checkName = capitalize(values.displayName);
|
||||||
|
if (values.displayName !== checkName) {
|
||||||
|
errors.push(`Your map's \`DisplayName\` must be capitalized. You may change it from \`${values.displayName}\` to \`${checkName}\`.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapParts = root.FindDescendantsOfClass("BasePart", (part) => part.Name === "MapStart" || part.Name === "MapFinish");
|
||||||
|
if (mapParts.length !== 2 || mapParts[0].Name === mapParts[1].Name) {
|
||||||
|
errors.push("Your map must have exactly one part named `MapStart` and one part named `MapFinish`.");
|
||||||
|
}
|
||||||
|
else if (mapParts[0].CanCollide || mapParts[1].CanCollide) {
|
||||||
|
errors.push("The `MapStart` and `MapFinish` parts in your map must have the `CanCollide` property disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawnOnes = root.FindDescendantsOfClass("BasePart", (part) => part.Name === "Spawn1");
|
||||||
|
if (spawnOnes.length !== 1) {
|
||||||
|
errors.push("Your map must have exactly one part named `Spawn1`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why does ModuleScript not inherit from Script, and/or why does BaseScript not have a Source property?
|
||||||
|
const illegalScript = root.FindFirstDescendantOfClass("Script", (script) => sourceHasIllegalKeywords(script.Source));
|
||||||
|
const illegalModuleScript = root.FindFirstDescendantOfClass("ModuleScript", (script) => sourceHasIllegalKeywords(script.Source));
|
||||||
|
if (illegalScript || illegalModuleScript) {
|
||||||
|
errors.push("Your map has a `Script` that contains the keyword `getfenv` or `require`. You must remove these.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
displayName: values.displayName,
|
||||||
|
creator: values.creator
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceHasIllegalKeywords(source) {
|
||||||
|
return source && (source.includes("getfenv") || source.includes("require"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidationMessage(validation, game, errorOnFail) {
|
||||||
|
if (validation.valid) {
|
||||||
|
return `✅ Your map is valid! (game: ${game})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = `${errorOnFail ? "🚫" : "⚠️"} **Your map has problems.** (game: ${game})`;
|
||||||
|
for (const error of validation.errors) {
|
||||||
|
msg += `\n* ${error}`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { AssetType, getAssetInfo, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat };
|
@ -1,14 +1,24 @@
|
|||||||
const { bhopCookie, surfCookie, deathrunCookie, flytrialsCookie } = require("./config.json");
|
const { bhopCookie, surfCookie, deathrunCookie, flytrialsCookie } = require("./config.json");
|
||||||
|
|
||||||
const cookies = {
|
const cookies = {
|
||||||
bhop: bhopCookie,
|
bhop: bhopCookie,
|
||||||
surf: surfCookie,
|
surf: surfCookie,
|
||||||
deathrun: deathrunCookie,
|
deathrun: deathrunCookie,
|
||||||
flytrials: flytrialsCookie,
|
flytrials: flytrialsCookie,
|
||||||
};
|
};
|
||||||
|
|
||||||
const submissions = {};
|
const submissions = {};
|
||||||
const commands = [];
|
const commands = [];
|
||||||
for (const game in cookies) {
|
for (const game in cookies) {
|
||||||
submissions[game] = "files/" + game + "_submissions.csv";
|
submissions[game] = "files/" + game + "_submissions.csv";
|
||||||
commands.push({name: game, value: game});
|
commands.push({name: game, value: game});
|
||||||
}
|
}
|
||||||
module.exports = { cookies, submissions, commands };
|
|
||||||
|
const gamePlaces = {
|
||||||
|
bhop: "https://www.roblox.com/games/517201717/",
|
||||||
|
surf: "https://www.roblox.com/games/517206177/",
|
||||||
|
deathrun: "https://www.roblox.com/games/6870563649/",
|
||||||
|
flytrials: "https://www.roblox.com/games/12724901535/"
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { cookies, submissions, commands, gamePlaces };
|
||||||
|
15
eslint.config.mjs
Normal file
15
eslint.config.mjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
|
||||||
|
{languageOptions: { globals: globals.browser }},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||||
|
"semi": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
16
package.json
16
package.json
@ -1,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/builders": "^0.13.0",
|
"@discordjs/builders": "^1.7.0",
|
||||||
"@discordjs/rest": "^0.4.1",
|
"@discordjs/rest": "^2.2.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^1.6.8",
|
||||||
"csv-parse": "^5.0.4",
|
"csv-parse": "^5.0.4",
|
||||||
"discord-api-types": "^0.32.1",
|
"discord-api-types": "^0.37.81",
|
||||||
"discord.js": "^13.6.0",
|
"discord.js": "^14.14.1",
|
||||||
"noblox.js": "^4.15.1",
|
"noblox.js": "^4.15.1",
|
||||||
"node-csv": "^0.1.2",
|
"node-csv": "^0.1.2",
|
||||||
|
"rbxm-parser": "^1.0.4",
|
||||||
"sugar-date": "^2.0.6"
|
"sugar-date": "^2.0.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.1.1",
|
||||||
|
"eslint": "^9.1.1",
|
||||||
|
"globals": "^15.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user