Skip to content

Instantly share code, notes, and snippets.

@Gesugao-san
Last active April 3, 2025 17:25
Show Gist options
  • Save Gesugao-san/464ff0864c2d49e4cc9c7aebeb1ac795 to your computer and use it in GitHub Desktop.
Save Gesugao-san/464ff0864c2d49e4cc9c7aebeb1ac795 to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/**
* @file This script extracts a list of mods from a Steam Workshop collection and generates an XML document
* in a format compatible with the Barotrauma Modlist.
*
* @see {@link http://s.team/a/602960} for more information about Barotrauma.
*
* @example
* // How to run:
* // 1. Open the Steam Workshop collection page (e.g., for Barotrauma).
* // 2. Open the browser's Developer Tools (usually by pressing F12).
* // 3. Navigate to the "Console" tab.
* // 4. Paste this script and press Enter.
* // 5. The generated XML code will be printed in the console, ready to be copied and saved in Modlists folder:
* // "C:\Program Files (x86)\Steam\steamapps\common\Barotrauma\ModLists"
*
* @description
* This script performs the following steps:
* 1. Waits for the page to fully load and checks for the presence of the mod collection title.
* 2. Retrieves the collection title.
* 3. Iterates through all mods in the list, extracts their names and IDs.
* 4. Structures the mods into an XML format.
* 5. Adds a comment with the generation date and version.
* 6. Prints the generated XML code to the console.
*
* @returns {void} This script does not return a value but prints the XML code to the console.
* @version 1.2
* @author Gesugao-san
* @license MIT
*/
// Constants
const VERSION = 1.2; // Script version
const PINNED_IDS = new Set([
2795927223, // C#
2559634234, // Lua
3088784724, // LuaC# Enforced
2701251094, // Performance Fix
3329396988, // Network Tweaks
3231293294, // Better Health UI
2986079116 // TRT
]);
const MOD_URL = "https://steamcommunity.com/sharedfiles/filedetails/?id=";
const MODS_FOLDER_PATH = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Barotrauma\\ModLists";
const BBCODE_ENABLED = false; // Set to true if you want to generate BBCode for mod IDs
// Helper functions
/**
* Determines the user's language and returns messages in Russian or English.
* @returns {Object} An object containing language-specific messages.
*/
function getMessages() {
const userLanguage = navigator.language;
if (userLanguage.startsWith("ru")) {
return {
waitingForPage: "Ожидание загрузки страницы...",
pageLoaded: "Страница успешно загружена.",
errorOccurred: "Произошла ошибка:",
skippingMod: "Пропуск мода без ссылки.",
skippingInvalidId: 'Пропуск мода "${title}" из-за неверного ID.',
pageLoadTimeout: "Страница не загрузилась в ожидаемое время.",
titleNotFound: "Не удалось найти заголовок коллекции модов.",
};
} else {
return {
waitingForPage: "Waiting for the page to load...",
pageLoaded: "Page loaded successfully.",
errorOccurred: "An error occurred:",
skippingMod: "Skipping a mod element without a link.",
skippingInvalidId: 'Skipping mod "${title}" due to invalid ID.',
pageLoadTimeout: "Page did not load within the expected time.",
titleNotFound: "Could not find the mod collection title.",
};
}
}
/**
* Waits for the page to fully load by checking for the presence of the mod collection title.
* @returns {Promise<void>} A promise that resolves when the title element is found.
* @throws {Error} If the title element is not found within the specified time.
*/
function waitForPageLoad() {
return new Promise((resolve, reject) => {
const maxAttempts = 30; // Maximum number of attempts
const interval = 500; // Check every 500ms
let attempts = 0;
const checkInterval = setInterval(() => {
const titleElement = document.querySelector(".workshopItemTitle");
if (titleElement) {
clearInterval(checkInterval);
resolve();
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
reject(new Error(messages.pageLoadTimeout));
}
attempts++;
}, interval);
});
}
/**
* Retrieves the title of the mod collection from the Steam Workshop page.
* @returns {string} The title of the mod collection.
* @throws {Error} If the title element is not found.
*/
function getModpackTitle() {
const modpackTitleElement = document.querySelector(".workshopItemTitle");
if (!modpackTitleElement) {
throw new Error(messages.titleNotFound);
}
return modpackTitleElement.textContent;
}
/**
* Extracts mod information (title and ID) from the HTML page.
* @returns {Array<{title: string, id: number}>} An array of objects containing mod titles and IDs.
*/
function extractModsFromPage() {
const modElements = document.querySelectorAll(".collectionItemDetails");
const mods = [];
modElements.forEach(modElement => {
const modLink = modElement.querySelector("a");
if (!modLink) {
console.warn(messages.skippingMod);
return;
}
const title = modLink.innerText; // Extract the mod name
const id = parseInt(modLink.href.split('?id=')[1]); // Extract the mod ID from the URL
if (isNaN(id)) {
console.warn(messages.skippingInvalidId.replace("${title}", title));
return;
}
mods.push({ title, id });
});
return mods;
}
/**
* Generates the XML structure for the mod list.
* @param {string} modpackTitle - The title of the mod collection.
* @param {Array<{title: string, id: number}>} mods - An array of mods with titles and IDs.
* @returns {string} The generated XML code as a string.
*/
function generateXml(modpackTitle, mods) {
let output = `<?xml version="1.0" encoding="utf-8"?>
<mods name="${modpackTitle}">
<Vanilla />
`;
// Add mods to the XML structure
mods.forEach(mod => {
output += ` <Workshop id="${mod.id}" name="${mod.title}" />\n`;
});
// Add a comment with the generation date and version
const currentUTCDate = new Date().toISOString().split('T')[0]; // Extract only the date part
output += ` <!-- Generated with https://gist.github.com/Gesugao-san/464ff0864c2d49e4cc9c7aebeb1ac795/ (v${VERSION.toString()}) on ${currentUTCDate} UTC -->\n`;
// Close the XML structure
output += "</mods>\n";
return output;
}
// Main script
(async () => {
// Get language-specific messages
const messages = getMessages();
try {
// Step 0: Wait for the page to fully load
console.log(messages.waitingForPage);
await waitForPageLoad();
console.log(messages.pageLoaded);
// Step 1: Retrieve the mod collection title
const modpackTitle = getModpackTitle();
// Step 2: Extract mod information from the page
const mods = extractModsFromPage();
// Step 3: Generate the XML structure
const xmlOutput = generateXml(modpackTitle, mods);
// Step 4: Print the generated XML output to the console
console.log(xmlOutput);
} catch (error) {
console.error(messages.errorOccurred, error);
}
})();
#!/usr/bin/env node
/**
* @file This script converts a Steam Workshop Collection into an XML document
* compatible with the Barotrauma Modlist format. It can also be adapted for other games
* or purposes requiring similar mod list conversions.
*
* @see {@link http://s.team/a/602960} for more information about Barotrauma.
*
* @example
* // How to run:
* // 1. Open the Steam Workshop Collection page (e.g., for Barotrauma).
* // 2. Open the browser's Developer Tools (usually by pressing F12).
* // 3. Navigate to the "Console" tab.
* // 4. Paste this script and press Enter.
* // 5. The generated XML code will be saved as a file in UTF-8 with BOM encoding,
* // ready to be copied into the Modlists folder:
* // "C:\Program Files (x86)\Steam\steamapps\common\Barotrauma\ModLists"
*
* @description
* This script performs the following steps:
* 1. Waits for the page to fully load and checks for the presence of the mod collection title.
* 2. Retrieves the mod collection title.
* 3. Extracts mod information (title and ID) from the HTML page.
* 4. Sorts mods by their IDs (creation date).
* 5. Adds "pinned" mods in a separate section.
* 6. Writes all mods to an XML document, creating <Workshop> elements with `id` and `name` attributes.
* 7. Serializes the XML document into a string and formats it for readability.
* 8. Creates an XML file in UTF-8 with BOM encoding and offers it for download to the user.
*
* @returns {void} This script does not return a value but initiates the download of an XML file.
* @version 1.2
* @author Gesugao-san
* @license MIT
*/
// Constants
const VERSION = 1.2; // Script version
const PINNED_IDS = new Set([
2795927223, // C#
2559634234, // Lua
3088784724, // LuaC# Enforced
2701251094, // Performance Fix
3329396988, // Network Tweaks
3231293294, // Better Health UI
2986079116 // TRT
]);
const MOD_URL = "https://steamcommunity.com/sharedfiles/filedetails/?id=";
const MODS_FOLDER_PATH = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Barotrauma\\ModLists";
const BBCODE_ENABLED = false; // Set to true if you want to generate BBCode for mod IDs
let messages;
// Helper functions
/**
* Determines the user's language and returns messages in Russian or English.
* @returns {Object} An object containing language-specific messages.
*/
function getMessages() {
const userLanguage = navigator.language;
if (userLanguage.startsWith("ru")) {
return {
waitingForPage: "Ожидание загрузки страницы...",
pageLoaded: "Страница успешно загружена.",
errorOccurred: "Произошла ошибка:",
skippingMod: "Пропуск мода без ссылки.",
skippingInvalidId: 'Пропуск мода "${title}" из-за неверного ID.',
pageLoadTimeout: "Страница не загрузилась в ожидаемое время.",
titleNotFound: "Не удалось найти заголовок коллекции модов.",
xmlGenerated: "XML-файл успешно создан и скачан. Поместите его сюда:",
};
} else {
return {
waitingForPage: "Waiting for the page to load...",
pageLoaded: "Page loaded successfully.",
errorOccurred: "An error occurred:",
skippingMod: "Skipping a mod element without a link.",
skippingInvalidId: 'Skipping mod "${title}" due to invalid ID.',
pageLoadTimeout: "Page did not load within the expected time.",
titleNotFound: "Could not find the mod collection title.",
xmlGenerated: "XML file has been generated and downloaded. Place it here:",
sortedModsComment: " Sorted by ID ",
};
}
}
/**
* Waits for the page to fully load by checking for the presence of the mod collection title.
* @returns {Promise<void>} A promise that resolves when the title element is found.
* @throws {Error} If the title element is not found within the specified time.
*/
function waitForPageLoad() {
return new Promise((resolve, reject) => {
const maxAttempts = 30; // Maximum number of attempts
const interval = 500; // Check every 500ms
let attempts = 0;
const checkInterval = setInterval(() => {
const titleElement = document.querySelector(".workshopItemTitle");
if (titleElement) {
clearInterval(checkInterval);
resolve();
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
reject(new Error(messages.pageLoadTimeout));
}
attempts++;
}, interval);
});
}
/**
* Retrieves the title of the mod collection from the Steam Workshop page.
* @returns {string} The title of the mod collection.
* @throws {Error} If the title element is not found.
*/
function getModpackTitle() {
const titleElement = document.querySelector(".workshopItemTitle");
if (!titleElement) {
throw new Error(messages.titleNotFound);
}
return titleElement.textContent;
}
/**
* Extracts mod information (title and ID) from the HTML page.
* @returns {Object} An object where keys are mod IDs and values are mod titles.
*/
function extractModsFromPage() {
const modElements = document.querySelectorAll(".collectionItemDetails");
const mods = {};
modElements.forEach((item) => {
const modLink = item.querySelector("a");
if (!modLink) {
console.warn(messages.skippingMod);
return;
}
const title = modLink.innerText;
const id = parseInt(modLink.href.split('?id=')[1]);
if (isNaN(id)) {
console.warn(messages.skippingInvalidId.replace("${title}", title));
return;
}
mods[id] = title;
});
return mods;
}
/**
* Creates an XML document with the root element <mods> and a <Vanilla> child.
* @param {string} modpackTitle - The title of the mod collection.
* @returns {Document} The created XML document.
*/
function createXmlDocument(modpackTitle) {
const xmlDoc = document.implementation.createDocument(null, "mods", null);
const rootElement = xmlDoc.documentElement;
rootElement.setAttribute("name", modpackTitle);
// Add Vanilla element
const vanillaElement = xmlDoc.createElement("Vanilla");
rootElement.appendChild(vanillaElement);
return xmlDoc;
}
/**
* Adds pinned mods to the XML document in a separate section.
* @param {Document} xmlDoc - The XML document to which mods will be added.
* @param {Object} mods - An object containing mod IDs and titles.
*/
function addPinnedMods(xmlDoc, mods) {
let pinnedPresence = false;
PINNED_IDS.forEach((id) => {
if (!mods[id]) return;
if (!pinnedPresence) {
const pinnedHeader = xmlDoc.createComment(" Pinned mods, own sort order ");
xmlDoc.documentElement.appendChild(pinnedHeader);
pinnedPresence = true;
}
const title = mods[id];
const workshopElement = xmlDoc.createElement("Workshop");
workshopElement.setAttribute("id", BBCODE_ENABLED ? `[url=${MOD_URL}${id}]${id}[/url]` : id);
workshopElement.setAttribute("name", title);
xmlDoc.documentElement.appendChild(workshopElement);
});
}
/**
* Adds sorted mods to the XML document, excluding pinned mods.
* @param {Document} xmlDoc - The XML document to which mods will be added.
* @param {Object} mods - An object containing mod IDs and titles.
*/
function addSortedMods(xmlDoc, mods) {
const sortedHeader = xmlDoc.createComment(" Sorted by ID ");
xmlDoc.documentElement.appendChild(sortedHeader);
Object.keys(mods)
.sort((a, b) => parseInt(a) - parseInt(b))
.forEach((idStr) => {
const id = parseInt(idStr);
if (PINNED_IDS.has(id)) return;
const title = mods[id];
const workshopElement = xmlDoc.createElement("Workshop");
workshopElement.setAttribute("id", id);
workshopElement.setAttribute("name", title);
xmlDoc.documentElement.appendChild(workshopElement);
});
}
/**
* Serializes the XML document to a string, formats it for readability, and initiates a download.
* The resulting file is saved in UTF-8 with BOM encoding to ensure compatibility with Barotrauma.
* @param {Document} xmlDoc - The XML document to serialize.
* @param {string} modpackTitle - The title of the mod collection, used as the filename.
*/
function serializeAndDownloadXml(xmlDoc, modpackTitle) {
const serializer = new XMLSerializer();
let xmlString = serializer.serializeToString(xmlDoc);
// Format XML for readability
xmlString = xmlString.replace(/><(\/?)/g, '>\n<$1');
xmlString = `<?xml version="1.0" encoding="utf-8"?>\n${xmlString}`;
xmlString = xmlString.replace(/(<[V!WL].+)/gi, " $1");
// Create and download the file with UTF-8 with BOM encoding
const blob = new Blob(['\ufeff' + xmlString], { encoding: "UTF-8", type: "application/xml; charset=UTF-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${modpackTitle}.xml`;
a.click();
URL.revokeObjectURL(url);
console.log(messages.xmlGenerated);
console.log(MODS_FOLDER_PATH);
}
// Main script
(async () => {
// Get language-specific messages
messages = getMessages();
try {
// Step 0: Wait for the page to fully load
console.log(messages.waitingForPage);
await waitForPageLoad();
console.log(messages.pageLoaded);
// Step 1: Retrieve the mod collection title
const modpackTitle = getModpackTitle();
// Step 2: Extract mod information from the page
const mods = extractModsFromPage();
// Step 3: Create the XML document
const xmlDoc = createXmlDocument(modpackTitle);
// Step 4: Add pinned mods to the XML document
addPinnedMods(xmlDoc, mods);
// Step 5: Add sorted mods to the XML document
addSortedMods(xmlDoc, mods);
// Step 6: Add a comment with the generation date and version
const currentUTCDate = new Date().toISOString().split('T')[0];
const creditsHeader = xmlDoc.createComment(` Generated with https://gist.github.com/Gesugao-san/464ff0864c2d49e4cc9c7aebeb1ac795/ (v${VERSION.toString()}) on ${currentUTCDate} UTC `);
xmlDoc.documentElement.appendChild(creditsHeader);
// Step 7: Serialize the XML document and initiate the download
serializeAndDownloadXml(xmlDoc, modpackTitle);
} catch (error) {
console.error(messages.errorOccurred, error);
}
})();
// ONLY FOR PAGE https://steamcommunity.com/sharedfiles/managecollection/?id=__ID__
// abandoned, do not use please :3
if (typeof jQuery !== 'undefined') {
console.log("jQuery version:", jQuery.fn.jquery);
} else {
new Error("jQuery is not loaded on this page.");
}
function SortByTimeUpdatedWithPriority() {
SetLastSort('timeupdated');
SortChildItems(function(a, b) {
var a_sort_data = $J(a).data("timeupdated");
var b_sort_data = $J(b).data("timeupdated");
// Проверяем, есть ли у элементов приоритет (например, флаг "isPriority")
var a_is_priority = $J(a).data("isPriority") || false;
var b_is_priority = $J(b).data("isPriority") || false;
// Если оба элемента имеют приоритет или оба не имеют, сортируем по времени
if (a_is_priority === b_is_priority) {
var sortFactor = gSortAscending ? 1 : -1;
return sortFactor * (a_sort_data - b_sort_data);
}
// Если один из элементов имеет приоритет, он всегда выше
return a_is_priority ? -1 : 1;
});
}
function addPriorityAttribute(elements, priorityIds) {
// Проходим по каждому элементу
elements.forEach(element => {
// Проверяем, есть ли у элемента атрибут "id"
const id = parseInt(element.getAttribute('id').split("_")[1]);
// Если атрибут "id" есть и его значение содержится в списке priorityIds
if (id && priorityIds.includes(id)) {
// Проверяем, есть ли уже атрибут data-isPriority
if (!element.hasAttribute('data-isPriority')) {
// Добавляем атрибут data-isPriority="true"
element.setAttribute('data-isPriority', 'true');
console.log(`Added data-isPriority="true" to element with id="${id}"`);
} else {
console.log(`Element with id="${id}" already has data-isPriority`);
}
}
});
}
// Пример использования:
// Список элементов (например, полученных через querySelectorAll)
const elements = document.querySelectorAll('[id]'); // Ищем все элементы с атрибутом "id"
// Список id, которые должны получить атрибут data-isPriority="true"
const priorityIds = [
2795927223, // C#
3294275235,
3424487895
];
// Вызываем функцию
addPriorityAttribute(elements, priorityIds);
// Находим элемент по id
const sortButton = document.getElementById('sort_timeupdated');
// Проверяем, существует ли элемент
if (sortButton) {
// Создаем новый div
const sortButtonPriority = document.createElement('div');
sortButtonPriority.class = 'btnv6_blue_blue_innerfade btn_small_thin ico_hover sort_button';
sortButtonPriority.id = 'sort_timeupdated_priority';
sortButtonPriority.textContent = 'SortByTimeUpdatedWithPriority();';
sortButtonPriority.setAttribute('onclick', 'SortByTimeUpdatedWithPriority();');
// Вставляем новый div после элемента sortTimeUpdatedElement
sortButton.insertAdjacentElement('afterend', sortButtonPriority);
console.log('New div added after #sort_timeupdated');
} else {
console.error('Element with id="sort_timeupdated" not found');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment