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())