#!/usr/bin/env python3
# RimWorld Together launcher (Linux) - Trashmans Trashcan
#
# CLI port of the Windows launcher: self-updates from the modpack feed,
# installs the selected server's pack (Server 1 or Server 2) by subscribing
# through Steam's own API, waits for downloads, repairs corrupt mods, writes
# the load order, preconfigures the AI storyteller (Server 1), validates, and
# launches RimWorld through Steam.
#
#   python3 rwt-launcher.py            interactive menu
#   python3 rwt-launcher.py --update   install/update the selected pack
#   python3 rwt-launcher.py --validate
#   python3 rwt-launcher.py --launch
#   python3 rwt-launcher.py --server 2 --update
#
# Needs: python3 (stdlib only), Steam running and logged in, RimWorld installed
# via Steam (native Linux build).

import argparse
import ctypes
import hashlib
import json
import os
import re
import shutil
import sys
import time
import urllib.request
from pathlib import Path
from xml.sax.saxutils import escape

APP_ID = 294100
FEED_SOURCES = [
    {"base": "https://www.iwannaplaygames.com/rwt", "token": None, "cachebust": True},
    {"base": "http://git.tonic.pw:3000/api/v1/repos/Twistedxvs/rwt-launcher/raw",
     "token": "33e35e0665423bd0568966753c748b77e755d41f", "cachebust": False},
]
SERVERS = {
    1: {"config": "config.json",  "label": "Server 1 (192.96.219.70:25555)"},
    2: {"config": "config2.json", "label": "Server 2 (192.96.219.70:25556)"},
}
LLM_ENDPOINT = "https://rimworld.twistedxvs.com/v1"
LLM_MODEL = "gemma4:26b-a4b"
LLM_API_KEY = "a4be33f2c5ca49d4afb6508bee86d3494412354f4d463d71"

STATE_DIR = Path.home() / ".config" / "rwt-launcher"
SELF_PATH = Path(__file__).resolve()

GREEN, YELLOW, RED, CYAN, RESET = "\033[92m", "\033[93m", "\033[91m", "\033[96m", "\033[0m"
def step(msg): print(f"\n{CYAN}=== {msg} ==={RESET}")
def ok(msg): print(f"  {GREEN}[OK]{RESET} {msg}")
def warn(msg): print(f"  {YELLOW}[!!]{RESET} {msg}")
def fail(msg):
    print(f"\n  {RED}[ERROR]{RESET} {msg}")
    sys.exit(1)

# ---------------------------------------------------------------- feed -----
def fetch(name):
    for src in FEED_SOURCES:
        url = f"{src['base']}/{name}"
        if src["cachebust"]:
            url += f"?t={int(time.time() * 1000)}"
        req = urllib.request.Request(url, headers={"User-Agent": "RWT-Launcher-Linux"})
        if src["token"]:
            req.add_header("Authorization", f"token {src['token']}")
        for _ in range(2):
            try:
                with urllib.request.urlopen(req, timeout=15) as r:
                    return r.read()
            except Exception:
                time.sleep(2)
    return None

def self_update(skip):
    if skip:
        return
    remote = fetch("rwt-launcher.py")
    if not remote:
        print("  (update feed unreachable - using local files)")
        return
    if hashlib.sha256(remote).hexdigest() != hashlib.sha256(SELF_PATH.read_bytes()).hexdigest():
        print("  New launcher version found - updating and restarting...")
        SELF_PATH.write_bytes(remote)
        os.execv(sys.executable, [sys.executable, str(SELF_PATH), *sys.argv[1:], "--no-self-update"])

def refresh_configs():
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    for name in ("config.json", "config2.json"):
        data = fetch(name)
        if data:
            (STATE_DIR / name).write_bytes(data)
    if not (STATE_DIR / "config.json").exists():
        fail("could not download the modpack config and no cached copy exists - check your connection")

def beta_enabled():
    # Server 2 is closed beta: unlocked by beta=true in the feed config, or a
    # local beta.txt tester unlock in the state dir.
    try:
        if json.loads((STATE_DIR / "config.json").read_text(encoding="utf-8-sig")).get("beta"):
            return True
    except Exception:
        pass
    return (STATE_DIR / "beta.txt").exists()

def get_server_choice():
    try:
        if (STATE_DIR / "server-choice").read_text().strip() == "2" and beta_enabled():
            return 2
    except Exception:
        pass
    return 1

def set_server_choice(n):
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    (STATE_DIR / "server-choice").write_text(str(n))

def load_pack(choice):
    raw = (STATE_DIR / SERVERS[choice]["config"]).read_text(encoding="utf-8-sig")
    return json.loads(raw)

# --------------------------------------------------------------- steam -----
def steam_roots():
    home = Path.home()
    cands = [home / ".steam" / "root", home / ".steam" / "steam",
             home / ".local" / "share" / "Steam",
             home / ".var" / "app" / "com.valvesoftware.Steam" / ".local" / "share" / "Steam"]
    seen, out = set(), []
    for c in cands:
        try:
            r = c.resolve()
        except Exception:
            continue
        if r.is_dir() and r not in seen:
            seen.add(r)
            out.append(r)
    return out

def find_rimworld():
    for root in steam_roots():
        libs = [root]
        vdf = root / "steamapps" / "libraryfolders.vdf"
        if vdf.is_file():
            for m in re.finditer(r'"path"\s+"([^"]+)"', vdf.read_text(errors="replace")):
                libs.append(Path(m.group(1)))
        for lib in libs:
            if (lib / "steamapps" / f"appmanifest_{APP_ID}.acf").is_file():
                return (lib / "steamapps" / "common" / "RimWorld",
                        lib / "steamapps" / "workshop" / "content" / str(APP_ID),
                        lib / "steamapps" / "workshop" / f"appworkshop_{APP_ID}.acf")
    fail("RimWorld (Steam) not found in any Steam library. Is it installed?")

def modsconfig_path():
    for base in (Path.home() / ".config",
                 Path.home() / ".var" / "app" / "com.valvesoftware.Steam" / ".config"):
        p = base / "unity3d" / "Ludeon Studios" / "RimWorld by Ludeon Studios" / "Config" / "ModsConfig.xml"
        if p.parent.is_dir():
            return p
    # first run before RimWorld ever launched
    p = Path.home() / ".config" / "unity3d" / "Ludeon Studios" / "RimWorld by Ludeon Studios" / "Config" / "ModsConfig.xml"
    p.parent.mkdir(parents=True, exist_ok=True)
    return p

class Steamworks:
    def __init__(self, rimworld_dir):
        so = None
        for cand in rimworld_dir.rglob("libsteam_api.so"):
            so = cand
            break
        if not so:
            fail("libsteam_api.so not found inside the RimWorld install")
        work = STATE_DIR / "steamwork"
        work.mkdir(parents=True, exist_ok=True)
        shutil.copy2(so, work / "libsteam_api.so")
        (work / "steam_appid.txt").write_text(str(APP_ID))
        os.environ["SteamAppId"] = str(APP_ID)
        os.environ["SteamGameId"] = str(APP_ID)
        os.chdir(work)
        self.lib = ctypes.CDLL(str(work / "libsteam_api.so"))
        self.lib.SteamAPI_Init.restype = ctypes.c_bool
        self.lib.SteamAPI_GetHSteamUser.restype = ctypes.c_int
        self.lib.SteamInternal_FindOrCreateUserInterface.restype = ctypes.c_void_p
        self.lib.SteamInternal_FindOrCreateUserInterface.argtypes = [ctypes.c_int, ctypes.c_char_p]
        self.lib.SteamAPI_ISteamUGC_GetItemState.restype = ctypes.c_uint32
        self.lib.SteamAPI_ISteamUGC_GetItemState.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
        self.lib.SteamAPI_ISteamUGC_SubscribeItem.restype = ctypes.c_uint64
        self.lib.SteamAPI_ISteamUGC_SubscribeItem.argtypes = [ctypes.c_void_p, ctypes.c_uint64]
        self.lib.SteamAPI_ISteamUGC_DownloadItem.restype = ctypes.c_bool
        self.lib.SteamAPI_ISteamUGC_DownloadItem.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_bool]
        if not self.lib.SteamAPI_Init():
            fail("could not connect to Steam. Make sure Steam is RUNNING and logged in, then retry.")
        h = self.lib.SteamAPI_GetHSteamUser()
        self.ugc = self.lib.SteamInternal_FindOrCreateUserInterface(h, b"STEAMUGC_INTERFACE_VERSION016")
        if not self.ugc:
            fail("could not get the Steam Workshop interface")

    def run_callbacks(self):
        self.lib.SteamAPI_RunCallbacks()

    def state(self, item):
        return self.lib.SteamAPI_ISteamUGC_GetItemState(self.ugc, item)

    def subscribe(self, item):
        self.lib.SteamAPI_ISteamUGC_SubscribeItem(self.ugc, item)
        self.run_callbacks()

    def download(self, item, high=False):
        self.lib.SteamAPI_ISteamUGC_DownloadItem(self.ugc, item, high)

    def shutdown(self):
        try:
            self.lib.SteamAPI_Shutdown()
        except Exception:
            pass

# ------------------------------------------------------------- install -----
def workshop_mods(pack):
    return [m for m in pack["mods"] if int(m["id"]) != 0]

def check_dlc(pack, rimworld_dir):
    step("Checking official content")
    missing = []
    for m in pack["mods"]:
        if int(m["id"]) == 0 and m.get("dlc"):
            if (rimworld_dir / "Data" / m["dlc"]).is_dir():
                ok(m["name"])
            else:
                warn(f"{m['name']} is NOT installed - you need this DLC to join the server!")
                missing.append(m["packageId"].lower())
    return missing

def acf_sizes(acf_path):
    sizes = {}
    if acf_path.is_file():
        text = acf_path.read_text(errors="replace")
        for m in re.finditer(r'"(\d+)"\s*\{[^}]*?"size"\s+"(\d+)"', text):
            sizes[m.group(1)] = int(m.group(2))
    return sizes

def dir_size(path):
    total = 0
    for p in path.rglob("*"):
        try:
            if p.is_file():
                total += p.stat().st_size
        except Exception:
            pass
    return total

def wait_for(sw, mods, workshop_dir, minutes, label):
    deadline = time.time() + minutes * 60
    last = 0.0
    while True:
        sw.run_callbacks()
        pending = []
        for m in mods:
            st = sw.state(int(m["id"]))
            about = workshop_dir / str(m["id"]) / "About" / "About.xml"
            if not (st & 4) or (st & 8) or (st & 16) or (st & 32) or not about.is_file():
                pending.append(m)
        if not pending:
            ok(f"{label}: all {len(mods)} mods downloaded and up to date")
            return True
        if time.time() > deadline:
            warn(f"timed out with {len(pending)} mod(s) still downloading - let Steam finish before launching")
            return False
        if time.time() - last >= 10:
            names = ", ".join(m["name"] for m in pending[:3])
            more = f" (+{len(pending) - 3} more)" if len(pending) > 3 else ""
            print(f"  {len(mods) - len(pending)}/{len(mods)} ready - waiting on: {names}{more}")
            last = time.time()
        time.sleep(0.5)

def do_update(choice, timeout_minutes=45):
    pack = load_pack(choice)
    mods = pack["mods"]
    ws = workshop_mods(pack)
    print(f"\nRimWorld Together - {SERVERS[choice]['label']}")
    print(f"  pack v{pack.get('version', '?')}: {len(mods)} mods ({len(ws)} Workshop)")

    rimworld_dir, workshop_dir, acf = find_rimworld()
    ok(f"RimWorld: {rimworld_dir}")
    missing_dlc = check_dlc(pack, rimworld_dir)

    step(f"Subscribing to {len(ws)} Workshop mods")
    sw = Steamworks(rimworld_dir)
    try:
        new = 0
        for m in ws:
            if not (sw.state(int(m["id"])) & 1):
                sw.subscribe(int(m["id"]))
                print(f"  subscribing: {m['name']}")
                new += 1
                time.sleep(0.05)
        ok(f"{new} newly subscribed, {len(ws) - new} already subscribed")

        step(f"Waiting for Steam to download mods (up to {timeout_minutes} min)")
        for m in ws:
            st = sw.state(int(m["id"]))
            if not (st & 4) or (st & 8):
                sw.download(int(m["id"]))
        wait_for(sw, ws, workshop_dir, timeout_minutes, "downloads")

        step("Verifying mod files against Steam's manifest")
        sizes = acf_sizes(acf)
        corrupt = []
        for m in ws:
            mod_dir = workshop_dir / str(m["id"])
            if not (mod_dir / "About" / "About.xml").is_file():
                corrupt.append(m)
                continue
            want = sizes.get(str(m["id"]), 0)
            if want > 0 and dir_size(mod_dir) < want:
                warn(f"{m['name']}: corrupt download - will redownload")
                corrupt.append(m)
        if corrupt:
            warn(f"purging and redownloading {len(corrupt)} corrupt mod(s)...")
            for m in corrupt:
                shutil.rmtree(workshop_dir / str(m["id"]), ignore_errors=True)
                sw.download(int(m["id"]), high=True)
            wait_for(sw, corrupt, workshop_dir, timeout_minutes, "repairs")
        else:
            ok(f"all {len(ws)} mods verified against Steam's manifest")
    finally:
        sw.shutdown()

    step("Setting active mod list")
    mc_path = modsconfig_path()
    version = "1.6.4871 rev591"
    if mc_path.is_file():
        old = mc_path.read_text(errors="replace")
        vm = re.search(r"<version>([^<]+)</version>", old)
        if vm:
            version = vm.group(1)
        backup = mc_path.with_name(f"ModsConfig.backup.{time.strftime('%Y%m%d-%H%M%S')}.xml")
        shutil.copy2(mc_path, backup)
        ok(f"backed up current mod list to {backup.name}")
    active = [m["packageId"].lower() for m in mods if m["packageId"].lower() not in missing_dlc]
    known = [m["packageId"].lower() for m in mods
             if int(m["id"]) == 0 and m["packageId"].lower() != "ludeon.rimworld"]
    lines = ['<?xml version="1.0" encoding="utf-8"?>', "<ModsConfigData>",
             f"  <version>{escape(version)}</version>", "  <activeMods>"]
    lines += [f"    <li>{escape(p)}</li>" for p in active]
    lines += ["  </activeMods>", "  <knownExpansions>"]
    lines += [f"    <li>{escape(p)}</li>" for p in known]
    lines += ["  </knownExpansions>", "</ModsConfigData>", ""]
    mc_path.write_text("\n".join(lines))
    ok(f"active mod list set: {len(active)} mods in server load order")

    pack_pids = {m["packageId"].lower() for m in mods}
    cfg_dir = mc_path.parent
    if "og.rimteller" in pack_pids:
        step("Configuring the AI storyteller (Rimteller) for the server's hosted LLM")
        chat = LLM_ENDPOINT.rstrip("/") + "/chat/completions"
        xml = "\n".join([
            '<?xml version="1.0" encoding="utf-8"?>', "<SettingsBlock>",
            '  <ModSettings Class="OgRimteller.OgRimtellerSettings">',
            "    <OgRTProvider>Custom</OgRTProvider>",
            f"    <OgRT_keyCustom>{escape(LLM_API_KEY)}</OgRT_keyCustom>",
            f"    <OgRT_modelCustom>{escape(LLM_MODEL)}</OgRT_modelCustom>",
            f"    <OgRT_urlCustom>{escape(chat)}</OgRT_urlCustom>",
            "    <OgRT_currentAPI>0</OgRT_currentAPI>",
            "    <OgRTrows>", "      <li>",
            f"        <OgRT_APIConfig_model>{escape(LLM_MODEL)}</OgRT_APIConfig_model>",
            f"        <OgRT_APIConfig_key>{escape(LLM_API_KEY)}</OgRT_APIConfig_key>",
            f"        <OgRT_APIConfig_url>{escape(chat)}</OgRT_APIConfig_url>",
            "        <OgRT_APIConfig_Provider>Custom</OgRT_APIConfig_Provider>",
            "      </li>", "    </OgRTrows>", "  </ModSettings>", "</SettingsBlock>", ""])
        (cfg_dir / "Mod_3638662307_OgRimtellerMod.xml").write_text(xml)
        ok(f"Rimteller -> {LLM_ENDPOINT} ({LLM_MODEL})")
    if "boots.sensiblefactions" in pack_pids:
        step("Configuring Sensible Factions (faction kingdoms + biome weights)")
        (cfg_dir / "Mod_3531306011_FactionFilter.xml").write_text(SF_XML)
        ok("Sensible Factions: server-standard faction kingdoms + biome weights applied")

    print(f"\n{GREEN}Done!{RESET} Next launch of RimWorld will use the server's mod list.")
    return True

# ------------------------------------------------------------ validate -----
def do_validate(choice):
    pack = load_pack(choice)
    ws = workshop_mods(pack)
    rimworld_dir, workshop_dir, acf = find_rimworld()
    missing_dlc = check_dlc(pack, rimworld_dir)
    step("Validating files against the server pack")
    failures = 0

    missing, badpid, badsize = [], [], []
    sizes = acf_sizes(acf)
    for m in ws:
        mod_dir = workshop_dir / str(m["id"])
        about = mod_dir / "About" / "About.xml"
        if not about.is_file():
            missing.append(m["name"])
            continue
        pm = re.search(r"<packageId>([^<]+)</packageId>", about.read_text(errors="replace"), re.I)
        if pm and pm.group(1).strip().lower() != m["packageId"].lower():
            badpid.append(f"{m['name']} (disk={pm.group(1).strip().lower()})")
        want = sizes.get(str(m["id"]), 0)
        if want > 0 and dir_size(mod_dir) < want:
            badsize.append(m["name"])
    if missing:
        warn(f"mods MISSING from disk ({len(missing)}): {', '.join(missing)}")
        failures += 1
    else:
        ok(f"all {len(ws)} workshop mods present on disk")
    if badpid:
        warn("packageId mismatches: " + "; ".join(badpid))
        failures += 1
    else:
        ok("all packageIds match the server pack")
    if badsize:
        warn(f"CORRUPT/incomplete downloads ({len(badsize)}): {', '.join(badsize)} - run the update to repair")
        failures += 1
    else:
        ok("all mod folders match Steam's manifest sizes")

    mc_path = modsconfig_path()
    expected = [m["packageId"].lower() for m in pack["mods"] if m["packageId"].lower() not in missing_dlc]
    if mc_path.is_file():
        active = re.findall(r"<li>([^<]+)</li>",
                            re.search(r"<activeMods>([\s\S]*?)</activeMods>", mc_path.read_text(errors="replace")).group(1))
        if [a.strip().lower() for a in active] == expected:
            ok(f"active mod list matches: {len(active)} mods in server load order")
        else:
            warn(f"active mod list DIFFERS (have {len(active)}, expected {len(expected)}) - run the update")
            failures += 1
    else:
        warn("ModsConfig.xml not found - run the update")
        failures += 1

    pack_pids = {m["packageId"].lower() for m in pack["mods"]}
    if "og.rimteller" in pack_pids:
        p = mc_path.parent / "Mod_3638662307_OgRimtellerMod.xml"
        if p.is_file() and LLM_ENDPOINT.rstrip("/") in p.read_text(errors="replace"):
            ok("Rimteller -> server LLM")
        else:
            warn("Rimteller config missing or not pointing at the server LLM - run the update")
            failures += 1
    if "boots.sensiblefactions" in pack_pids:
        p = mc_path.parent / "Mod_3531306011_FactionFilter.xml"
        if p.is_file() and len(re.findall(r"<li>[^<]+</li>", p.read_text(errors="replace"))) >= 37:
            ok("Sensible Factions config present")
        else:
            warn("Sensible Factions config missing/incomplete - run the update")
            failures += 1

    if failures == 0:
        print(f"\n{GREEN}VALIDATION PASSED - ready to join the server{RESET}")
        return True
    print(f"\n{RED}VALIDATION FAILED ({failures} issue(s) above){RESET}")
    return False

def do_launch():
    for cmd in (["steam", f"steam://rungameid/{APP_ID}"], ["xdg-open", f"steam://rungameid/{APP_ID}"]):
        if shutil.which(cmd[0]):
            import subprocess
            subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            print("Launching RimWorld via Steam...")
            return
    warn("could not find 'steam' or 'xdg-open' - start RimWorld from Steam yourself")

# ---------------------------------------------------------------- main -----
def main():
    ap = argparse.ArgumentParser(description="RimWorld Together launcher (Linux)")
    ap.add_argument("--server", type=int, choices=(1, 2))
    ap.add_argument("--update", action="store_true")
    ap.add_argument("--validate", action="store_true")
    ap.add_argument("--launch", action="store_true")
    ap.add_argument("--no-self-update", action="store_true")
    args = ap.parse_args()

    print(f"{CYAN}RimWorld Together Launcher - Trashmans Trashcan{RESET}")
    self_update(args.no_self_update)
    refresh_configs()
    if args.server:
        if args.server == 2 and not beta_enabled():
            fail("Server 2 is in CLOSED BETA - ask the admin for access (testers get a beta.txt unlock)")
        set_server_choice(args.server)
    choice = get_server_choice()
    print(f"Active pack: {SERVERS[choice]['label']}")

    if args.update or args.validate or args.launch:
        if args.update:
            do_update(choice)
        if args.validate or args.update:
            do_validate(choice)
        if args.launch:
            do_launch()
        return

    while True:
        print(f"\n[1] Update mods   [2] Validate   [3] Launch RimWorld   [4] Switch server (now: {choice})   [q] Quit")
        sel = input("> ").strip().lower()
        if sel == "1":
            do_update(choice) and do_validate(choice)
        elif sel == "2":
            do_validate(choice)
        elif sel == "3":
            do_launch()
            return
        elif sel == "4":
            if choice == 1 and not beta_enabled():
                warn("Server 2 is in CLOSED BETA - ask the admin for access (testers get a beta.txt unlock)")
                continue
            choice = 2 if choice == 1 else 1
            set_server_choice(choice)
            print(f"Switched to {SERVERS[choice]['label']} - run [1] Update to install its pack")
        elif sel == "q":
            return

SF_XML = """<?xml version="1.0" encoding="utf-8"?>
<SettingsBlock>
	<ModSettings Class="Boots_SensibleFactions.SensibleFactions_Settings">
		<allowedFactionDefNames>
			<li>XR_Wood_Elves</li>
			<li>SettlerSavage</li>
			<li>CannibalPirate</li>
			<li>TribeCannibal</li>
			<li>VFEM2_CivilClan</li>
			<li>OutlanderCivil</li>
			<li>VFEM2_KingdomCivil</li>
			<li>SettlerCivil</li>
			<li>XR_Dark_Orks</li>
			<li>XR_High_Elves</li>
			<li>XR_DeepDwarves</li>
			<li>TribeRoughNeanderthal</li>
			<li>FPC</li>
			<li>TribeRough</li>
			<li>TribeCivil</li>
			<li>XR_Dark_Elves</li>
			<li>XR_Hillfolk</li>
			<li>VFEP_Junkers</li>
			<li>VFEP_Mercenaries</li>
			<li>Cinder_OutlanderCivil</li>
			<li>NudistTribe</li>
			<li>XR_Mountainfolk</li>
			<li>Pirate</li>
			<li>VFEM2_ClanRough</li>
			<li>VFEM2_KingdomRough</li>
			<li>OutlanderRough</li>
			<li>OutlanderRoughPig</li>
			<li>SettlerRough</li>
			<li>VFEM2_ClanSavage</li>
			<li>TribeSavageImpid</li>
			<li>VFEM2_KingdomSavage</li>
			<li>TribeSavage</li>
			<li>SandyShamblerFaction</li>
			<li>Empire</li>
			<li>PirateWaster</li>
			<li>XR_Pirate_Orks</li>
			<li>PirateYttakin</li>
		</allowedFactionDefNames>
		<factionBiomeWeights>
			<li>
				<factionDefName>VFEM2_ClanRough</factionDefName>
				<biomeDefName>Scarlands</biomeDefName>
				<weight>2</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomRough</factionDefName>
				<biomeDefName>Tundra</biomeDefName>
				<weight>1</weight>
			</li>
			<li>
				<factionDefName>CannibalPirate</factionDefName>
				<biomeDefName>MetalHell</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>CannibalPirate</factionDefName>
				<biomeDefName>LavaField</biomeDefName>
				<weight>2</weight>
			</li>
			<li>
				<factionDefName>VFEM2_CivilClan</factionDefName>
				<biomeDefName>Glowforest</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>VFEM2_CivilClan</factionDefName>
				<biomeDefName>Grasslands</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomCivil</factionDefName>
				<biomeDefName>Grasslands</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomCivil</factionDefName>
				<biomeDefName>Lake</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomCivil</factionDefName>
				<biomeDefName>Ocean</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>SettlerCivil</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>SettlerCivil</factionDefName>
				<biomeDefName>TropicalRainforest</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>SettlerCivil</factionDefName>
				<biomeDefName>TropicalSwamp</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Orks</factionDefName>
				<biomeDefName>Underground</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Orks</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Orks</factionDefName>
				<biomeDefName>Scarlands</biomeDefName>
				<weight>2</weight>
			</li>
			<li>
				<factionDefName>XR_DeepDwarves</factionDefName>
				<biomeDefName>Underground</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_DeepDwarves</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_DeepDwarves</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>XR_DeepDwarves</factionDefName>
				<biomeDefName>MetalHell</biomeDefName>
				<weight>1</weight>
			</li>
			<li>
				<factionDefName>XR_High_Elves</factionDefName>
				<biomeDefName>BMT_FungalForest</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>XR_High_Elves</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_High_Elves</factionDefName>
				<biomeDefName>TropicalRainforest</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_High_Elves</factionDefName>
				<biomeDefName>TropicalSwamp</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>FPC</factionDefName>
				<biomeDefName>Scarlands</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>FPC</factionDefName>
				<biomeDefName>Orbit</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>FPC</factionDefName>
				<biomeDefName>Space</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>VFEM2_CivilClan</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>VFEM2_CivilClan</factionDefName>
				<biomeDefName>BMT_EarthenDepths</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Elves</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Elves</factionDefName>
				<biomeDefName>Underground</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Elves</factionDefName>
				<biomeDefName>TropicalSwamp</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>XR_Dark_Elves</factionDefName>
				<biomeDefName>Scarlands</biomeDefName>
				<weight>2</weight>
			</li>
			<li>
				<factionDefName>XR_Hillfolk</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>XR_Hillfolk</factionDefName>
				<biomeDefName>LavaField</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Hillfolk</factionDefName>
				<biomeDefName>MetalHell</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>XR_Hillfolk</factionDefName>
				<biomeDefName>Lake</biomeDefName>
				<weight>2</weight>
			</li>
			<li>
				<factionDefName>VFEP_Junkers</factionDefName>
				<biomeDefName>Orbit</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>VFEP_Junkers</factionDefName>
				<biomeDefName>Space</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>VFEP_Junkers</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>VFEP_Junkers</factionDefName>
				<biomeDefName>MetalHell</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>Cinder_OutlanderCivil</factionDefName>
				<biomeDefName>Space</biomeDefName>
				<weight>10</weight>
			</li>
			<li>
				<factionDefName>Cinder_OutlanderCivil</factionDefName>
				<biomeDefName>Orbit</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>Cinder_OutlanderCivil</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>1</weight>
			</li>
			<li>
				<factionDefName>VFEP_Mercenaries</factionDefName>
				<biomeDefName>Space</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>VFEP_Mercenaries</factionDefName>
				<biomeDefName>Orbit</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>VFEP_Mercenaries</factionDefName>
				<biomeDefName>BorealForest</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>BorealForest</biomeDefName>
				<weight>9</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>BMT_CrystalCaverns</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>BMT_EarthenDepths</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>Underground</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>XR_Mountainfolk</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>Tundra</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>TropicalSwamp</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>TropicalRainforest</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>TemperateSwamp</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>TemperateForest</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>SeaIce</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>VFEM2_ClanSavage</factionDefName>
				<biomeDefName>ColdBog</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomSavage</factionDefName>
				<biomeDefName>AridShrubland</biomeDefName>
				<weight>10</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomSavage</factionDefName>
				<biomeDefName>BorealForest</biomeDefName>
				<weight>8</weight>
			</li>
			<li>
				<factionDefName>VFEM2_KingdomSavage</factionDefName>
				<biomeDefName>BMT_ChromaticOasis</biomeDefName>
				<weight>7</weight>
			</li>
			<li>
				<factionDefName>SandyShamblerFaction</factionDefName>
				<biomeDefName>Desert</biomeDefName>
				<weight>6</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>Underground</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>Undercave</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>GlacialPlain</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>Glowforest</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>Grasslands</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>BMT_EarthenDepths</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>PirateWaster</factionDefName>
				<biomeDefName>ExtremeDesert</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>XR_Pirate_Orks</factionDefName>
				<biomeDefName>ExtremeDesert</biomeDefName>
				<weight>10</weight>
			</li>
			<li>
				<factionDefName>PirateYttakin</factionDefName>
				<biomeDefName>Desert</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>PirateYttakin</factionDefName>
				<biomeDefName>BMT_CrystalCaverns</biomeDefName>
				<weight>4</weight>
			</li>
			<li>
				<factionDefName>PirateYttakin</factionDefName>
				<biomeDefName>BMT_ChromaticOasis</biomeDefName>
				<weight>3</weight>
			</li>
			<li>
				<factionDefName>PirateYttakin</factionDefName>
				<biomeDefName>BorealForest</biomeDefName>
				<weight>5</weight>
			</li>
			<li>
				<factionDefName>PirateYttakin</factionDefName>
				<biomeDefName>AridShrubland</biomeDefName>
				<weight>7</weight>
			</li>
		</factionBiomeWeights>
		<selectedFactionDefName>PirateYttakin</selectedFactionDefName>
		<selectedSettingsPage>1</selectedSettingsPage>
	</ModSettings>
</SettingsBlock>
"""

if __name__ == "__main__":
    main()
