🩺 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 log files so you need to have loaded your mods in your latest session.

ℹ️ Windows: Copy and paste this path and select the client-main.log file: %APPDATA%\VintagestoryData\Logs
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 label = document.getElementById("fileCountLabel") if len(all_files) > 0: label.textContent = f"Log file '{all_files.item(0).name}' selected" else: label.textContent = "No file selected" # 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 log_file = all_files.item(0) found_ids = [] try: # Read the single log file directly into memory content = await log_file.text() # Look for the specific line pattern target_phrase = "Mods, sorted by dependency:" for line in content.splitlines(): if target_phrase in line: # Extract everything after the phrase parts = line.split(target_phrase) if len(parts) > 1: # Split the mod names by comma and strip extra whitespaces mods_list_str = parts[1].strip() raw_mods = mods_list_str.split(",") found_ids = [m.strip() for m in raw_mods if m.strip()] break # Found the line, no need to keep reading except Exception as e: console.log(f"Error reading log file: {e}") # Show the editor and populate it with the parsed mod IDs editor = document.getElementById("modIdEditor") 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 = f"Scan complete. Found {len(found_ids)} mods." 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())