Initial import

This commit is contained in:
2026-03-30 22:51:56 -04:00
commit 08e2910b9d
103 changed files with 35475 additions and 0 deletions
+455
View File
@@ -0,0 +1,455 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUTPUT = ROOT / "data" / "game_config_db.toml"
IGNORE_CANDIDATES = {
"steam_autocloud",
"unitycrashhandler64",
"unitycrashhandler32",
"crashhandler",
"crashreporter",
"launcher",
"launch",
"start",
"start_protected_game",
"eac_launcher",
"eosbootstrapper",
"eossdk-win64-shipping",
"notification_helper",
"steamerrorreporter",
"steam-launch-wrapper",
"proton",
"wine",
"wine64-preloader",
"wine-preloader",
"winetricks",
"dxsetup",
"vc_redist.x64",
"vc_redist.x86",
"game",
"app",
"boot",
"data",
"options",
"quickref",
"language",
"settings",
"config",
"global",
"system",
"resources",
"resource",
"readme",
"desc",
"icon",
"logo",
"catalog",
"test",
"tests",
"steam",
"lang",
"langs",
"level0",
"level1",
"level2",
"level3",
"level4",
"level5",
"level6",
"level7",
"level8",
"level9",
"unityplayer",
"gameassembly",
"baselib",
"il2cpp",
"openimagedenoise",
"epicwebhelper",
"steam_api",
"steam_api64",
"sonynp",
"unirx",
"demilib",
"tbb",
"tbb12",
"tbbmalloc",
}
IGNORE_PREFIXES = (
"pakchunk",
"steam_api",
"unitycrashhandler",
"crashreport",
"crashpad",
"openxr",
"fmod",
"lib",
)
IGNORE_EXACT_CASEFOLD = {
"readme",
"readme.txt",
"eula",
"license",
"version",
"preview",
"developer",
"server",
"console",
"temp",
"meta",
"init",
"core",
"path",
"base",
"keys",
"header",
"music",
"sfx_ui",
"master",
"env",
"static",
"wiki",
"mod",
"fish",
"bees",
"note",
"shine",
"track",
"glyph",
"death",
"prog",
"build",
"run",
"dbdata",
"work",
"menus",
"items",
"skills",
"input",
"bgs",
}
CURATED_OVERRIDES = {
"730": {
"title": "Counter-Strike 2",
"aliases": ["cs2", "counter strike 2", "counter-strike 2", "csgo"],
"candidates": ["cs2", "csgo", "wine-cs2"],
"preferred": "cs2",
"verification": "verified",
},
"570": {
"title": "Dota 2",
"aliases": ["dota2", "dota"],
"candidates": ["dota", "wine-dota2"],
"preferred": "dota",
"verification": "verified",
},
"440": {
"title": "Team Fortress 2",
"aliases": ["tf2", "team fortress 2"],
"candidates": ["tf", "wine-tf2"],
"preferred": "tf",
"verification": "verified",
},
"620": {
"title": "Portal 2",
"aliases": ["portal2", "portal 2"],
"candidates": ["portal2", "wine-portal2"],
"preferred": "portal2",
"verification": "verified",
},
"892970": {
"title": "Valheim",
"aliases": ["valheim"],
"candidates": ["valheim", "wine-valheim"],
"preferred": "valheim",
"verification": "verified",
},
"238960": {
"title": "Path of Exile",
"aliases": ["path of exile", "poe"],
"candidates": ["PathOfExileSteam", "wine-PathOfExileSteam"],
"preferred": "PathOfExileSteam",
"verification": "verified",
},
"105600": {
"title": "Terraria",
"aliases": ["terraria"],
"candidates": ["Terraria", "wine-Terraria"],
"preferred": "Terraria",
"verification": "verified",
},
"1145360": {
"title": "Hades",
"aliases": ["hades"],
"candidates": ["Hades", "wine-Hades"],
"preferred": "Hades",
"verification": "verified",
},
"212680": {
"title": "FTL: Faster Than Light",
"aliases": ["ftl", "faster than light"],
"candidates": ["FTL", "wine-FTLGame"],
"preferred": "FTL",
"verification": "verified",
},
"283160": {
"title": "House of the Dying Sun",
"aliases": ["house of the dying sun", "dyingsun"],
"candidates": ["dyingsun", "wine-dyingsun"],
"preferred": "dyingsun",
"verification": "verified",
},
"1794680": {
"title": "Vampire Survivors",
"aliases": ["vampire survivors"],
"candidates": ["VampireSurvivors", "wine-VampireSurvivors"],
"preferred": "VampireSurvivors",
"verification": "verified",
},
"1091500": {
"title": "Cyberpunk 2077",
"aliases": ["cyberpunk", "cyberpunk 2077"],
"candidates": ["Cyberpunk2077", "wine-Cyberpunk2077"],
"preferred": "Cyberpunk2077",
"verification": "verified",
},
"305620": {
"title": "The Long Dark",
"aliases": ["the long dark", "long dark"],
"candidates": ["tld", "wine-tld"],
"preferred": "tld",
"verification": "verified",
},
}
def normalize(text: str) -> str:
return re.sub(r"[^a-z0-9]+", "", text.lower())
def parse_library_paths() -> list[Path]:
candidates = [
Path.home() / ".local/share/Steam/steamapps/libraryfolders.vdf",
Path.home() / ".steam/steam/steamapps/libraryfolders.vdf",
]
for candidate in candidates:
if candidate.exists():
text = candidate.read_text(errors="ignore")
paths = [
Path(match.group(1).replace("\\\\", "/"))
for match in re.finditer(r'"path"\s*\t\t"([^"]+)"', text)
]
return list(dict.fromkeys(paths))
return []
def parse_manifest(path: Path) -> dict[str, str] | None:
text = path.read_text(errors="ignore")
appid = re.search(r'"appid"\s*\t+"(\d+)"', text)
name = re.search(r'"name"\s*\t+"([^"]+)"', text)
installdir = re.search(r'"installdir"\s*\t+"([^"]+)"', text)
if not appid or not name or not installdir:
return None
return {
"appid": appid.group(1),
"title": name.group(1),
"installdir": installdir.group(1),
}
def candidate_score(candidate: str, title: str, installdir: str) -> tuple[int, int]:
norm_candidate = normalize(candidate)
norm_title = normalize(title)
norm_installdir = normalize(installdir)
score = 0
if norm_candidate == norm_installdir:
score += 100
if norm_candidate == norm_title:
score += 90
if len(norm_candidate) >= 4 and (norm_candidate in norm_title or norm_title in norm_candidate):
score += 50
if len(norm_candidate) >= 4 and (
norm_candidate in norm_installdir or norm_installdir in norm_candidate
):
score += 35
if candidate.lower().endswith((".sh", ".exe")):
score -= 10
return (score, -len(candidate))
def is_noise_candidate(stem: str) -> bool:
lowered = stem.casefold()
norm = normalize(stem)
if not norm or norm in IGNORE_CANDIDATES:
return True
if lowered in IGNORE_EXACT_CASEFOLD:
return True
if any(norm.startswith(prefix) for prefix in IGNORE_PREFIXES):
return True
if re.fullmatch(r"level\d+", lowered):
return True
if re.fullmatch(r"pakchunk\d+.*", lowered):
return True
if re.fullmatch(r"sfx[_-].*", lowered):
return True
if re.fullmatch(r"audiogroup\d+", lowered):
return True
if re.fullmatch(r"steamworks(_x64)?", lowered):
return True
return False
def collect_candidates(install_dir: Path, title: str, installdir: str) -> list[str]:
if not install_dir.exists():
return []
candidates: list[str] = []
for path in install_dir.rglob("*"):
if not path.is_file():
continue
if len(path.relative_to(install_dir).parts) > 4:
continue
suffix = path.suffix.lower()
if suffix not in {".exe", ".x86_64", ".x86", ".sh", ".app", ""} and not os.access(path, os.X_OK):
continue
if suffix == "" and not os.access(path, os.X_OK):
continue
if suffix == ".app" and not os.access(path, os.X_OK):
continue
stem = path.stem if suffix else path.name
if is_noise_candidate(stem):
continue
if stem.lower().startswith(("unins", "setup", "install", "crash")):
continue
candidates.append(stem)
unique: list[str] = []
seen: set[str] = set()
for candidate in candidates:
key = candidate.lower()
if key in seen:
continue
seen.add(key)
unique.append(candidate)
if not unique:
fallback = re.sub(r"[^A-Za-z0-9_-]+", "", installdir)
if fallback:
unique.append(fallback)
ranked = sorted(unique, key=lambda item: candidate_score(item, title, installdir), reverse=True)
strong = [item for item in ranked if candidate_score(item, title, installdir)[0] > 0]
if strong:
return strong[:8]
fallback = re.sub(r"[^A-Za-z0-9_-]+", "", installdir)
if fallback:
return [fallback]
return ranked[:1]
def build_entries() -> list[dict[str, object]]:
manifests: list[Path] = []
for library in parse_library_paths():
steamapps = library / "steamapps"
if steamapps.exists():
manifests.extend(sorted(steamapps.glob("appmanifest_*.acf")))
entries_by_appid: dict[str, dict[str, object]] = {}
for manifest in manifests:
parsed = parse_manifest(manifest)
if not parsed:
continue
appid = parsed["appid"]
install_dir = manifest.parent / "common" / parsed["installdir"]
candidates = collect_candidates(install_dir, parsed["title"], parsed["installdir"])
preferred = candidates[0] if candidates else re.sub(r"[^A-Za-z0-9_-]+", "", parsed["installdir"])
entries_by_appid[appid] = {
"appid": int(appid),
"title": parsed["title"],
"aliases": [parsed["installdir"]],
"candidates": candidates,
"preferred": preferred,
"verification": "heuristic",
}
for appid, override in CURATED_OVERRIDES.items():
base = entries_by_appid.get(appid, {
"appid": int(appid),
"title": override["title"],
"aliases": [],
"candidates": [],
"preferred": override["preferred"],
"verification": override["verification"],
})
alias_set = {alias for alias in base.get("aliases", [])}
alias_set.update(override.get("aliases", []))
candidate_list = []
seen_candidates = set()
for candidate in [*override.get("candidates", []), *base.get("candidates", [])]:
key = candidate.lower()
if key in seen_candidates:
continue
seen_candidates.add(key)
candidate_list.append(candidate)
base.update({
"title": override["title"],
"aliases": sorted(alias_set),
"candidates": candidate_list,
"preferred": override["preferred"],
"verification": override["verification"],
})
entries_by_appid[appid] = base
entries = list(entries_by_appid.values())
entries.sort(key=lambda entry: entry["title"].lower())
return entries
def toml_escape(value: str) -> str:
return value.replace("\\", "\\\\").replace("\"", "\\\"")
def render_toml(entries: list[dict[str, object]]) -> str:
lines = [
"# MangoTune game/executable hint database",
"# verification = \"verified\" means manually confirmed",
"# verification = \"heuristic\" means derived from local library scan and should be treated as a suggestion",
"",
]
for entry in entries:
lines.append("[[game]]")
lines.append(f"appid = {entry['appid']}")
lines.append(f"title = \"{toml_escape(entry['title'])}\"")
aliases = ", ".join(f'\"{toml_escape(alias)}\"' for alias in entry.get("aliases", []))
candidates = ", ".join(
f'\"{toml_escape(candidate)}\"' for candidate in entry.get("candidates", [])
)
lines.append(f"aliases = [{aliases}]")
lines.append(f"candidates = [{candidates}]")
lines.append(f"preferred = \"{toml_escape(entry['preferred'])}\"")
lines.append(f"verification = \"{entry['verification']}\"")
lines.append("")
return "\n".join(lines)
def main() -> None:
entries = build_entries()
OUTPUT.write_text(render_toml(entries))
print(f"Wrote {len(entries)} entries to {OUTPUT}")
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
mangohud-position-lab.sh <config> [app] [output_dir]
Examples:
mangohud-position-lab.sh ~/.config/mangotune/profiles/zz_test_right_sparse_top.conf glxgears
mangohud-position-lab.sh ~/.config/mangotune/profiles/zz_test_right_full_top.conf vkcube /tmp/mh-lab
Notes:
- This runs MangoHud directly, outside MangoTune's preview pipeline.
- OpenGL uses MANGOHUD_DLSYM=1.
- If xdotool/import are installed and a real X11 session is available, a screenshot
and basic window geometry will be written to the output directory.
- Override DISPLAY/XAUTHORITY/WAIT_SECS/WIDTH/HEIGHT from the environment when needed.
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -lt 1 ]]; then
usage
exit 0
fi
CONFIG_PATH="$1"
APP="${2:-glxgears}"
OUTPUT_DIR="${3:-/tmp/mangohud-position-lab}"
DISPLAY_VALUE="${DISPLAY:-:0}"
XAUTH_VALUE="${XAUTHORITY:-$HOME/.Xauthority}"
WAIT_SECS="${WAIT_SECS:-4}"
WIDTH="${WIDTH:-1400}"
HEIGHT="${HEIGHT:-760}"
mkdir -p "$OUTPUT_DIR"
rm -f "$OUTPUT_DIR"/run.log "$OUTPUT_DIR"/window.geom "$OUTPUT_DIR"/window.png \
"$OUTPUT_DIR"/screen.png "$OUTPUT_DIR"/window_from_screen.png
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "Config not found: $CONFIG_PATH" >&2
exit 2
fi
cleanup() {
if [[ -n "${PID:-}" ]]; then
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
case "$APP" in
glxgears)
MATCH_NAME="glxgears"
DISPLAY="$DISPLAY_VALUE" \
XAUTHORITY="$XAUTH_VALUE" \
MANGOHUD_CONFIGFILE="$CONFIG_PATH" \
MANGOHUD_DLSYM=1 \
mangohud glxgears -geometry "${WIDTH}x${HEIGHT}" >"$OUTPUT_DIR/run.log" 2>&1 &
;;
vkcube)
MATCH_NAME="Vkcube"
DISPLAY="$DISPLAY_VALUE" \
XAUTHORITY="$XAUTH_VALUE" \
MANGOHUD_CONFIGFILE="$CONFIG_PATH" \
WGPU_BACKEND="${WGPU_BACKEND:-gl}" \
mangohud vkcube --width "$WIDTH" --height "$HEIGHT" >"$OUTPUT_DIR/run.log" 2>&1 &
;;
*)
echo "Unsupported app: $APP" >&2
exit 2
;;
esac
PID=$!
echo "pid=$PID" >>"$OUTPUT_DIR/run.log"
printf 'display=%s\nxauthority=%s\nwidth=%s\nheight=%s\n' \
"$DISPLAY_VALUE" "$XAUTH_VALUE" "$WIDTH" "$HEIGHT" >>"$OUTPUT_DIR/run.log"
sleep "$WAIT_SECS"
if command -v xwininfo >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" \
xwininfo -root -tree >"$OUTPUT_DIR/xwininfo.tree" 2>/dev/null || true
fi
if command -v xdotool >/dev/null 2>&1; then
WINDOW_ID="$(DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" xdotool search --name "$MATCH_NAME" 2>/dev/null | tail -n1 || true)"
else
WINDOW_ID=""
fi
if [[ -n "$WINDOW_ID" ]]; then
echo "window_id=$WINDOW_ID" >>"$OUTPUT_DIR/run.log"
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" \
xdotool getwindowgeometry --shell "$WINDOW_ID" >"$OUTPUT_DIR/window.geom" 2>/dev/null || true
if command -v import >/dev/null 2>&1; then
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" \
import -window "$WINDOW_ID" "$OUTPUT_DIR/window.png" 2>/dev/null || true
DISPLAY="$DISPLAY_VALUE" XAUTHORITY="$XAUTH_VALUE" \
import -window root "$OUTPUT_DIR/screen.png" 2>/dev/null || true
if [[ -f "$OUTPUT_DIR/window.geom" ]] && [[ -f "$OUTPUT_DIR/screen.png" ]] && command -v convert >/dev/null 2>&1; then
# Crop the full-screen capture back down to the app window bounds. This preserves overlays
# when the compositor draws them separately from the client window content.
# shellcheck disable=SC1090
. "$OUTPUT_DIR/window.geom"
if [[ -n "${WIDTH:-}" && -n "${HEIGHT:-}" && -n "${X:-}" && -n "${Y:-}" ]]; then
convert "$OUTPUT_DIR/screen.png" -crop "${WIDTH}x${HEIGHT}+${X}+${Y}" +repage \
"$OUTPUT_DIR/window_from_screen.png" 2>/dev/null || true
fi
fi
fi
fi
echo "Wrote artifacts to $OUTPUT_DIR"
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
mangohud-position-matrix.sh [app] [output_dir] [profile_dir]
Examples:
mangohud-position-matrix.sh vkcube /tmp/mh-matrix
DISPLAY=:1 XAUTHORITY=/root/.Xauthority mangohud-position-matrix.sh glxgears /tmp/mh-matrix /home/aaron/mangotune-test-profiles
What it does:
- finds the standard MangoTune right-alignment test profiles
- generates margin-on and margin-off variants
- runs MangoHud directly for each case using mangohud-position-lab.sh
- stores generated configs and capture artifacts under the output dir
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
APP="${1:-vkcube}"
OUTPUT_DIR="${2:-/tmp/mangohud-position-matrix}"
PROFILE_DIR="${3:-$HOME/.config/mangotune/profiles}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LAB_SCRIPT="$SCRIPT_DIR/mangohud-position-lab.sh"
if [[ ! -x "$LAB_SCRIPT" ]]; then
chmod +x "$LAB_SCRIPT" 2>/dev/null || true
fi
mkdir -p "$OUTPUT_DIR/generated"
profiles=(
"zz_test_right_full_top"
"zz_test_right_full_middle"
"zz_test_right_sparse_top"
"zz_test_right_sparse_middle"
"zz_test_right_sparse_compact_top"
"zz_test_right_sparse_compact_middle"
)
ensure_flag_state() {
local input_file="$1"
local output_file="$2"
local flag="$3"
local enabled="$4"
awk -v flag="$flag" -v enabled="$enabled" '
BEGIN { saw = 0 }
{
trimmed = $0
gsub(/^[[:space:]]+/, "", trimmed)
if (trimmed == flag || trimmed == "# " flag || trimmed == "#" flag) {
if (enabled == "1") {
print flag
} else {
print "# " flag
}
saw = 1
next
}
print $0
}
END {
if (!saw && enabled == "1") {
print flag
}
}
' "$input_file" >"$output_file"
}
for profile in "${profiles[@]}"; do
base="$PROFILE_DIR/$profile.conf"
[[ -f "$base" ]] || continue
for margin_state in margin_on margin_off; do
generated="$OUTPUT_DIR/generated/${profile}_${margin_state}.conf"
case "$margin_state" in
margin_on)
ensure_flag_state "$base" "$generated" "hud_no_margin" 0
;;
margin_off)
ensure_flag_state "$base" "$generated" "hud_no_margin" 1
;;
esac
case_out="$OUTPUT_DIR/${profile}_${margin_state}_${APP}"
"$LAB_SCRIPT" "$generated" "$APP" "$case_out"
done
done
echo "Matrix artifacts written to $OUTPUT_DIR"