Skip to content

Instantly share code, notes, and snippets.

@Saturate
Created October 4, 2022 16:56
Show Gist options
  • Save Saturate/1519244dee074f3b6afdea349580f0e0 to your computer and use it in GitHub Desktop.
Save Saturate/1519244dee074f3b6afdea349580f0e0 to your computer and use it in GitHub Desktop.
Project Zomboid - Generate WorkshopItems from Steam Workshop Collection

Project Zomboid - Generate WorkshopItems from Steam Workshop Collection

This snippet will generate a list suited for the Project Zomboid config from a Steam Workshop Collection.

How to use:

  1. Navigate to your collection (eg. https://steamcommunity.com/sharedfiles/filedetails/?id=2871262277)
  2. Copy the code in console.js
  3. Open the devtools for your browser (F12)
  4. Paste and run the code
  5. Copy the output
  6. Paste the config line into your config.

The output will look like this:

WorkshopItems=2625625421;2619072426;2423906082;2618213077;2522173579;2870394916;2846036306;2811383142;2805630347;2772575623;2642541073;2566953935;2516123638;2489148104;2441990998;2169435993;2478768005;2392709985;2490220997
var modIds = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
return mod.id.replace('sharedfile_','');
})
console.log(`This list contains ${modIds.length} mods, copy it to your PZ config.`)
console.log(`WorkshopItems=${modIds.join(';')}`)
@HungryHedgehog
Copy link

HungryHedgehog commented Apr 23, 2023

Simple addition to make the list into a format for Space Engineer servers:

var modIds = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return mod.id.replace('sharedfile_','');
})
var modItems = '';
modIds.forEach(id => 
{
	 modItems += '<ModItem>\n' + `<Name>${id}.sbm</Name>\n` + \`<PublishedFileId>${id}</PublishedFileId>\n` + '</ModItem>\n';
}); 
console.log(modItems);

@Motzumoto
Copy link

Motzumoto commented Sep 27, 2023

Minorly edited this to be faster and log unprocessed urls.

const WorkshopIDs = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return mod.id.replace('sharedfile_','');
})

const WORKSHOP_IDS = new Set()
const MOD_IDS = new Set()
const MAPS = new Set()
const UNPROCESSED_URLS = new Set() // Set to store unprocessed URLs

async function collectIDS(WorkshopIDs) {
    const promises = WorkshopIDs.map(async (id, index) => {
        const url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + id;
        console.log(`processing #${index + 1}/${WorkshopIDs.length}: ${url}`);

        try {
            const res = await fetch(url);
            const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, "\n").replace(/<b>/g, "").replace(/<\/b>/g, "").replace(/&amp;/g, "&");

            WORKSHOP_IDS.add(id);
            collect_mod_ids(html, MOD_IDS);
            collect_maps(html, MAPS);
        } catch (error) {
            // Handle the error and store the unprocessed URL in the Set
            console.error(`Error processing URL: ${url}`);
            UNPROCESSED_URLS.add(url);
        }
    });

    await Promise.all(promises);
}

function collect_mod_ids(html, target_set) {
    const mod_id_matches = html.matchAll(/^(Mod ?ID|MID): *(?<mod_id>([^\r\n\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff<>]+ *)+) */gmi);
    const mod_id_matches_array = [...mod_id_matches];

    if (mod_id_matches_array.length > 0) {
        const mod_ids_for_workshop_id = new Set();
        for (const match_groups of mod_id_matches_array) {
            const mod_id_group = match_groups.groups.mod_id;
            mod_ids_for_workshop_id.add(mod_id_group);
        }

        if (mod_ids_for_workshop_id.length > 1) {
            console.log("Warning. Multiple Mod IDs should always be checked, because you may need to select between them.");
        }
        for (const mod_id of mod_ids_for_workshop_id) {
            target_set.add(mod_id.trim());
        }
    } else {
        console.log("Error. No ModID. Expected to find 'Mod ID' in the description of mod with URL: " + url);
    }
}

function collect_maps(html, target_set) {
    const map_matches = html.matchAll(/^(Map ?Folder|Folder|Map): (?<map>[a-zA-Z0-9_. ]+)/gmi);
    const map_matches_array = [...map_matches];
    if (map_matches_array.length > 0) {
        const maps_for_workshop_id = new Set();
        for (const match_groups of map_matches_array) {
            const map_group = match_groups.groups.map;
            maps_for_workshop_id.add(map_group);
        }

        for (const map of maps_for_workshop_id) {
            target_set.add(map.trim());
        }
    }
}

(async () => {
    await collectIDS(WorkshopIDs);

    MAPS.add("Muldraugh, KY");

    console.log(`WorkshopItems=${[...WORKSHOP_IDS].join(";")}`);
    console.log(`Mods=${[...MOD_IDS].join(";")}`);
    console.log(`Map=${[...MAPS].join(";")}`);

    // Log unprocessed URLs at the end
    if (UNPROCESSED_URLS.size > 0) {
        console.log("Unprocessed URLs:");
        console.log([...UNPROCESSED_URLS].join("\n"));
    }
})();

@Saturate
Copy link
Author

Looks like this code is popular. Thanks for all the additions guys and girls.

Might be time to make a real project out of it, but what would you recon that would be most useful?

I'm thinking if a nodejs module or a python script could be good here, it could do stuff automatically for the server configuration. Maybe a GUI is needed for some people, but what do you think?

@Motzumoto
Copy link

Motzumoto commented Sep 28, 2023

Looks like this code is popular. Thanks for all the additions guys and girls.

Might be time to make a real project out of it, but what would you recon that would be most useful?

I'm thinking if a nodejs module or a python script could be good here, it could do stuff automatically for the server configuration. Maybe a GUI is needed for some people, but what do you think?

I would love if this became an actual project. theres some minor bugs in it that ive found. With the code I uploaded it includes the collections ID in the list, I cant remember if it did that with your original upload. If you do decide to make this into a GUI and whatnot I'd love it if people who contributed to this would be credited.

I made something similar to this in python but because its python there are some serious limitations. I'm considering closing the repo and linking to this in the future if this does come into fruition.

@contrid
Copy link

contrid commented Mar 12, 2024

i've created an improved version that extracts workshop ids and names:

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => {
    return "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_','');
})

const IDS = []
const NAMES = []

async function getIDS(URLS) {
    let index = 0
    for (url of URLS) {
        console.log(`processing #${index++}/${URLS.length}: ${url}`)
        const res = await fetch(url)
        const html = (await res.text()).replace(/<i>/g, "").replace(/<\/i>/g, "").replace(/<br>/g, " ").replace(/<b>/g, "").replace(/<\/b>/g, "")
        //console.log(html)
        const wks_ids = html.match(/Workshop ?ID: (\d*)/gmi) || html.match(/WID: (\d*)/gmi)
        const ids = wks_ids.map(wks => wks.split(": ")[1])
        IDS.push(...ids)
        console.log(ids)
        const wks_names = html.match(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)  || html.match(/ ID: (\d*\w*\d*\w*\d*\.*\d*)/gmi)
        const names = wks_names.map(wks => wks.split(": ")[1])
        NAMES.push(...names)
        console.log(names)
    }
}

await getIDS(URLS)

console.log(`WorkshopItems=${IDS.join(";")}`)
console.log(`Mods=${NAMES.join(";")}`)

Awesome, thank you for posting this! The regex was useful 😁👍🙏

@FlintMcgy
Copy link

FlintMcgy commented Mar 28, 2024

Here is a better snippet made using @contrid code with ChatGPT that is faster and also gives you a Numbered List of all the Mods with their Id's

const URLS = Array.from(document.querySelectorAll('[id^="sharedfile_"]')).map(mod => 
    "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.id.replace('sharedfile_', '')
);

async function getIDS(URLS) {
    return Promise.all(URLS.map(async (url, index) => {
        try {
            console.log(`processing #${index + 1}/${URLS.length}: ${url}`);
            const res = await fetch(url);
            const html = await res.text();
            
            const extractMatches = (pattern) => (html.match(new RegExp(pattern, "gmi")) || []).map(wks => wks.split(": ")[1]);
            
            const ids = extractMatches(/Workshop ?ID: (\d*)/);
            const names = extractMatches(/Mod ?ID: (\d*\w*\d*\w*\d*\.*\d*)/);

            return { ids, names };
        } catch (error) {
            console.error(`Error processing #${index + 1}: ${error.message}`);
            return { ids: [], names: [] };
        }
    }));
}

async function main() {
    const results = await getIDS(URLS);
    const modList = results.map(({ ids, names }, index) => `${index + 1}. WorkshopID:[${ids[0]}] ModIDs[${names.join(", ")}]`);
    const IDS = results.flatMap(({ ids }) => ids);
    const NAMES = results.flatMap(({ names }) => names);

    console.log(`WorkshopItems=${IDS.join(";")}`);
    console.log(`Mods=${NAMES.join(";")}`);
    console.log(`ModList=${modList.join(", ")}`);
}

main();

@sbwns
Copy link

sbwns commented Apr 17, 2024

Made this site so that it's user friendly
https://www.pzutil.com/

Does not log unprocessed URLs yet but will add that shortly

@Motzumoto
Copy link

Made this site so that it's user friendly https://www.pzutil.com/

Does not log unprocessed URLs yet but will add that shortly

Wonderful!

@GamingDaveUk
Copy link

Thanks guys, I was looking at amp and trying to work out what i need to put in these sections:
image
The site is going to help with 2 and 3, the code further up, I think I can use to know what to put into 1.

Going to look at it all when more awake, but I just wanted to say you have turned a task I was dreading, and was making me wonder if i wanted to set up a server into something that should be quite simple. Your time on these projects is GREATLY appreciated! Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment