🩺 Vintage-Vitals

Game Version: Detecting...
Updated: ...
...

Check whether installed Vintage Story mods have support against the latest releases in the ModDB database. All scanning is done locally in your browser—nothing is uploaded.

Updates daily. Uses local cache files so you need to have loaded your mods at least once.

Privacy & Security: Browsers show a warning when selecting folders to prevent unauthorized uploads. Vintage-Vitals scans modinfo.json entirely in your browser. Access is revoked immediately after and no data leaves your browser.
📁 Windows: Copy and paste this path and select your /unpack folder: %APPDATA%\VintagestoryData\Cache
Logo Mod Name (ID) Last Released Compatibility
from js import document, console, fetch import json import asyncio from pyodide.ffi import create_proxy def update_mod_count(event): all_files = document.getElementById("folderPicker").files # Filter for mods immediately mod_files = [f for f in all_files if f.name == "modinfo.json"] count = len(mod_files) label = document.getElementById("fileCountLabel") if count > 0: label.textContent = f"{count} mods detected" else: label.textContent = "No mods found" # api_mods will store: "modidstr": {full_mod_data_object} api_mods = {} async def load_api_data(): global api_mods try: response = await fetch("mods.json") data = await response.text() json_obj = json.loads(data) # 1. Update App Version from the JSON itself app_v = json_obj.get("app_version", "v0.0.0-dev") document.getElementById("versionBadge").textContent = app_v # Update Game Version UI vs_version = json_obj.get("version", "Unknown") document.getElementById("gameVersion").textContent = f"{vs_version}" # Update Sync Date UI sync_date = json_obj.get("last_updated", "Unknown") document.getElementById("syncDate").textContent = sync_date # Map mods into dictionary by their string ID for fast lookup list_data = json_obj.get("mods", []) for m in list_data: # Get the first string ID from the list mid_str = m['modidstrs'][0] if isinstance(m['modidstrs'], list) else m['modidstrs'] # Store the whole object using the string ID as the key api_mods[mid_str] = m console.log(f"API Data loaded. Version: {vs_version}") except Exception as e: console.log(f"Error loading mods.json: {e}") async def scan_folder(event): all_files = document.getElementById("folderPicker").files if not all_files: return mod_files = [f for f in all_files if f.name == "modinfo.json"] found_ids = [] for file in mod_files: try: content = await file.text() data = json.loads(content.replace(",}", "}").replace(",]", "]")) mid = data.get("modid") if mid: found_ids.append(mid) except: continue # Show the editor and populate it editor = document.getElementById("modIdEditor") # Sort and join with newlines editor.value = "\n".join(sorted(list(set(found_ids)))) document.getElementById("reviewSection").style.display = "block" document.getElementById("folderPicker").value = "" # Clear file handles document.getElementById("fileCountLabel").textContent = "Discovery complete. List ready for review." async def run_final_check(event): btn = document.getElementById("runCompatibilityCheck") loading_area = document.getElementById("loadingArea") progress_bar = document.getElementById("scanProgress") label = document.getElementById("progressLabel") # Get IDs from the text area raw_text = document.getElementById("modIdEditor").value mod_ids = [line.strip() for line in raw_text.split('\n') if line.strip()] if not mod_ids: return btn.disabled = True loading_area.classList.add("show") # Process the list found_mods = {} total = len(mod_ids) for i, mid in enumerate(mod_ids): progress_bar.value = int(((i + 1) / total) * 100) label.textContent = f"Checking {mid}..." # We don't have local names anymore since we are just using IDs, # so we default the name to the ID. display_results will try to find better names. found_mods[mid] = mid await asyncio.sleep(0.01) # Small delay for visual feedback display_results(found_mods) loading_area.classList.remove("show") btn.disabled = False def display_results(found_mods): body = document.getElementById("resultsBody") body.innerHTML = "" # --- SORTING LOGIC START --- # Sort the items: (True/False, Name) # This puts True (Compatible) first, then sorts alphabetically by name sorted_items = sorted( found_mods.items(), key=lambda item: (item[0] not in api_mods, item[1].lower()) ) # --- SORTING LOGIC END --- for mid, local_name in sorted_items: semver = document.getElementById("gameVersion").textContent.split('.x')[0].strip() row = document.createElement("tr") mod_info = api_mods.get(mid) is_compat = mod_info is not None # 1. Logo Cell logo_td = document.createElement("td") img_src = mod_info.get("logo") if is_compat else None if not img_src: img_src = "https://mods.vintagestory.at/web/img/mod-default.png" logo_html = f'' # Just run a search instead. URLs arent consisting enough to build. logo_td.innerHTML = f'{logo_html}' # 2. Name Cell name_td = document.createElement("td") display_name = mod_info.get("name", local_name) if is_compat else local_name # If compat, we use the modid from API, otherwise use local ID sub_name = mid moddb_row = f'{display_name}
{sub_name}' local_row = f'{display_name}
{sub_name}' if is_compat: name_td.innerHTML = moddb_row else: sub_name = local_name name_td.innerHTML = local_row # 3. Release Date Cell date_td = document.createElement("td") date_td.textContent = mod_info.get("lastreleased", "N/A") if is_compat else "Unknown" # 4. Status Cell status_td = document.createElement("td") # Get the version string from the header we already set vs_label = document.getElementById("gameVersion").textContent status_class = 'yes' if is_compat else 'no' status_text = f"✅ v{vs_label} Compatible" if is_compat else f"❌ v{vs_label} Not Found" status_td.innerHTML = f'{status_text}' for td in [logo_td, name_td, date_td, status_td]: row.appendChild(td) body.appendChild(row) document.getElementById("resultsTable").classList.add("show") from js import Tablesort Tablesort.new(document.getElementById('resultsTable')) async def setup(): await load_api_data() picker = document.getElementById("folderPicker") picker.onchange = create_proxy(update_mod_count) # Step 1: Scan document.getElementById("scanFolder").onclick = create_proxy(scan_folder) # Step 2: Final Check document.getElementById("runCompatibilityCheck").onclick = create_proxy(run_final_check) asyncio.ensure_future(setup()) asyncio.ensure_future(setup())