Skip to content

Instantly share code, notes, and snippets.

@pepoluan
Last active September 1, 2025 16:15
Show Gist options
  • Save pepoluan/0b94f4849c75cb34fdcd0293815b2419 to your computer and use it in GitHub Desktop.
Save pepoluan/0b94f4849c75cb34fdcd0293815b2419 to your computer and use it in GitHub Desktop.
Dump a list of Factorio mods in Markdown format
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# ///
# SPDX-License-Identifier: MPL-2.0
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# © 2025, Pandu POLUAN
"""Dumps a markdown list of active mods in Factorio"""
from __future__ import annotations
import argparse
import json
import os
import platform
import re
import zipfile
from pathlib import Path
from typing import Final, Protocol, TypedDict, cast
RE_MOD = re.compile(r".*?_(?P<ver>\d+\.\d+\.\d+)")
RE_INFO = re.compile(r"[^/]+/info\.json")
match platform.system():
case "Windows":
USER_DATA_DIR: Final[str] = os.environ["APPDATA"]
case _:
raise NotImplementedError("Support for non-Windows OS is not yet implemented")
class _Options(Protocol):
all: bool
withver: bool
def options() -> _Options:
parser = argparse.ArgumentParser()
parser.add_argument(
"--all", "-a", action="store_true", help="Show all mods not only those enabled"
)
parser.add_argument(
"--withver", "-w", action="store_true", help="Show version in the link text"
)
return cast(_Options, parser.parse_args())
class ModMeta(TypedDict):
name: str
enabled: bool
class ModList(TypedDict):
mods: list[ModMeta]
VerTuple = tuple[int, int, int]
def main(opts: _Options):
moddir = Path(USER_DATA_DIR) / "Factorio" / "mods"
modlist = moddir / "mod-list.json"
data: ModList
with modlist.open("rt") as fin:
data = json.load(fin)
mods_dump: dict[str, str] = {}
mod: ModMeta
for mod in data["mods"]:
if not opts.all and not mod["enabled"]:
continue
modname = mod["name"]
modvers: list[VerTuple] = []
for modf in moddir.glob(f"{modname}_*.zip"):
if not (m := RE_MOD.match(modf.name)):
raise RuntimeError(f"{modf} not match RE")
modver = cast(VerTuple, tuple(int(v) for v in m.group("ver").split(".")))
modvers.append(modver)
if not modvers:
continue
modver_latest = max(modvers)
modver_latest_s = ".".join(str(v) for v in modver_latest)
modzipf = moddir / f"{modname}_{modver_latest_s}.zip"
if not modzipf.exists():
raise RuntimeError(f"{modzipf} not found!")
with zipfile.ZipFile(modzipf) as modz:
for fname in modz.namelist():
if RE_INFO.match(fname):
break
else:
raise RuntimeError(f"{modzipf} has no info.json!")
with modz.open(fname) as fin:
mod_data = json.load(fin)
headers = [mod_title := mod_data["title"]]
if opts.withver:
headers.append(f"v{modver_latest_s}")
mods_dump[mod_title] = (
f"* [{' '.join(headers)}](https://mods.factorio.com/mod/{modname})"
)
for _, md in sorted(mods_dump.items()):
print(md)
if __name__ == "__main__":
main(options())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment