• We have updated our Community Code of Conduct. Please read through the new rules for the forum that are an integral part of Paradox Interactive’s User Agreement.

Kuroko_Fey

Second Lieutenant
95 Badges
Jan 19, 2015
186
109
  • Europa Universalis IV: Cossacks
  • Europa Universalis IV: Res Publica
  • Semper Fi
  • Sword of the Stars II
  • Victoria 2: A House Divided
  • Victoria 2: Heart of Darkness
  • Cities: Skylines Deluxe Edition
  • Pillars of Eternity
  • Cities: Skylines - After Dark
  • Crusader Kings II
  • Cities: Skylines - Snowfall
  • Europa Universalis IV: Mare Nostrum
  • Stellaris
  • Stellaris: Galaxy Edition
  • Crusader Kings II: Reapers Due
  • Age of Wonders III
  • Stellaris: Synthetic Dawn
  • Stellaris - Path to Destruction bundle
  • Crusader Kings II: Sword of Islam
  • Ancient Space
  • Crusader Kings II: Charlemagne
  • Crusader Kings II: Legacy of Rome
  • Crusader Kings II: The Old Gods
  • Crusader Kings II: Rajas of India
  • Crusader Kings II: The Republic
  • Crusader Kings II: Sons of Abraham
  • Crusader Kings II: Sunset Invasion
  • Hearts of Iron III Collection
  • Europa Universalis IV
  • Europa Universalis IV: Art of War
  • Hearts of Iron III: Their Finest Hour
  • Hearts of Iron III
  • Europa Universalis IV: Conquest of Paradise
  • For the Motherland
  • Europa Universalis IV: Wealth of Nations
  • Europa Universalis IV: Call to arms event
  • Hearts of Iron IV: Together for Victory
  • Cities: Skylines - Natural Disasters
  • Stellaris: Leviathans Story Pack
  • Tyranny: Archon Edition
  • Europa Universalis IV: Rights of Man
  • Victoria 2
  • Hearts of Iron IV: Cadet
  • Crusader Kings II: Way of Life
  • Stellaris: Galaxy Edition
  • Cities: Skylines
  • Crusader Kings II: Conclave
  • Europa Universalis IV: El Dorado
  • Mount & Blade: Warband
  • Crusader Kings II: Horse Lords
Hi All,

This was the alpha v0.2 release of the BATTLETECH Mod Manager, BTMod. However, it seems that another mod loader is in development which will replicate much of the functionality of BTMod. Rather than duplicate effort and have multiple mod loaders floating around, I've decided to discontinue BTMod and instead assist where I can with what I hope will become the standard mod loader for the community, ModTek (https://github.com/Mpstark/ModTek). Check it out if you're interested!

In the interests of providing people who would like to follow on with anything I've done here some support, I'll leave the .exe's download link up until it expires (in about a week). I'll also post the source code down below. It's incredibly rough, raw code which I make no apologies for ;)

Download Link: here

How To Use It:
  1. Place the .exe in your BATTLETECH root directory (where BattleTech.exe is located)
    For me on Steam, this is \SteamLibrary\steamapps\common\BATTLETECH
  2. Run BTMod.exe. This will create two folders, Mods and VanillaBackup.
  3. Place your mods in the Mods folder.
    The folder structure is \Mods\<ModName>\<StreamingAssets folder structure>
    For example, I downloaded a mod that changes the shop jsons called "BetterShops". So, I made a folder in Mods called BetterShops. In there, I copied the "data\shops" folder from the mod which contains the modified JSONs. The final result is to have the folder:
    \Mods\BetterShops\data\shops\
    Which contains the modified jsons.
  4. Click "Load Mods".
  5. Launch the game!
How To Revert To Vanilla:
When BTMod applies a mod, it will first back up all the vanilla jsons being changed to the VanillaBackup folder. Therefore, it is critical that the first time you run BTMod, you run without any mods installed. After installing a mod or many mods with BTMod, if you'd like to revert to the non-modified game (to play multiplayer, for example):
  1. Run BTMod.exe
  2. Click "Return To Stock".
How To Create & Package Mods For BTMod:
BTMod (in the current version) only expects you to add or modify files found in the
\BATTLETECH\BattleTech_Data\StreamingAssets
folder. If that's all you're doing, you can package your mod for BTMod by doing the following:
  1. Create a folder in the Mods folder created by BTMod, naming it whatever you'd like to call your mod.
  2. Copy the vanilla files you want to modify from the data folder to \Mods\<your mod name>\
    It's important that you keep the file and folder structure from StreamingAssets, so if you want to modify simgameconstants.json, for example, you would copy simgameconstants.json
    from: \BATTLETECH\BattleTech_Data\StreamingAssets\data\simGameConstants\
    to: \BATTLETECH\Mods\<your mod name>\data\simGameConstants\
  3. Mod the file(s) however you'd like.
  4. Congratulations, your mod is done! To apply it to the game, run BTMod.exe and click "Load Mods".
  5. To package it for others, just put \<your mod name>\ and all the subfolders in to a zip file and share it, linking users to this post for information on how to use BTMod. They'll just need to copy the contents of your zip file in to their Mods folder to use your mod.
Caveats:
  • At the moment mods are applied as they're loaded, with no conflict checks, so if two separate mods affect the same file, the last mod to be applied will overwrite the others.
  • It's hideously ugly. I'll put more work in to making the UI pretty if it ends up being popular.
  • There's no good way to differentiate between modded files and vanilla files, so if an official patch to the game changes jsons, you need to delete the contents of VanillaBackup, verify game files on Steam to ensure you're using all unmodified files, and run BTMod again.
  • I packaged this with pyinstaller. While it appears to run fine on computers without python installed, it might not. If you get .dll errors or the .exe won't run on your PC let me know and I'll use a more robust packaging system.

v0.2 Change Log
  • Added dynamic modification of VersionManifest.csv, allowing for mods including new files to be used with BTMod.
  • Removed restriction on adding new files.
  • Changed root path for vanilla files from data to StreamingAssets.
 
Last edited:
Very cool - especially for 'a few spare hours' of work ;)

Should make life easier.
 
Thanks very much! Seeing how popular your variants mod is, I think I’ll focus on adding support for mods that add new files.

And just a note for anyone interested in trying BTMod, I extensively tested it today and had no trouble both adding multiple mods and uninstalling them to play multiplayer. My alpha warning still stands, but as long as you stick to simple mods I don’t expect you to run in to any serious problems. So if you have a moment, please give it a try. Even just knowing the .exe will run on other systems (various versions of Windows, etc.) would be great data to help in my future development.
 
v0.2 is up, and includes dynamic update of the VersionManifest.csv file.

This is done through Pandas, which has unfortunately increased BTMod's file size somewhat, but as I don't anticipate adding any more modules, it should be fairly stable at roughly 48MB from here on out. With this, the majority of mods are now supported by BTMod!

For mod authors who are also planning to modify the MetadataDatabase.db file, I'm currently considering how best to incorporate this functionality in BTMod. I've got three methods in mind:
  1. Ask brandonm4 to add command line functionality to his BTMDDUpdater program, so BTMod can work through it - most difficult, but easiest for end users and authors.
  2. Ask mod authors to include modified .db files in their mod - easy for me, but only one mod with a modified .db file can be used at once.
  3. Ask users to use the BTMDDUpdater program separately as needed - easy for me and authors, end users may struggle.
Any thoughts on the above would be greatly appreciated.
 
As per the original post, BTMod is now discontinued in favour of ModTek. For anyone interested, you can find the source code of BTMod below. It's a Python script written in 3.6.4, and built in to an executable using pyinstaller. If you need any help with the code, feel free to ping me!

Code:
from shutil import copy2
from tkinter import *
from pandas import DataFrame, read_csv

import os
import sys
import csv
import pandas as pd
import filecmp

master = Tk()

#Scans the \BATTLETECH\Mods\ directory for mods and copies them to the game directory.
#Also checks to see if a mod includes VersionManifest.csv and updates accordingly if it does.
def load_mods():
    for mod in allMods:
        modPath = modsPath+'\\'+mod
        for root, dirs, files in os.walk(modPath):
            for name in dirs:
                modDirectoryPath = os.path.join(root, name)
                vanillaBackupDirectoryPath = modDirectoryPath.replace(modPath, vanillaBackupPath)
                vanillaDirectoryPath = modDirectoryPath.replace(modPath, btDataPath)
                if os.path.exists(vanillaDirectoryPath) and not os.path.exists(vanillaBackupDirectoryPath):
                    os.makedirs(vanillaBackupDirectoryPath)
            for name in files:
                modFilePath = os.path.join(root, name)
                vanillaFilePath = modFilePath.replace(modPath, btDataPath)
                if os.path.isfile(vanillaFilePath):
                    vanillaBackupFilePath = modFilePath.replace(modPath, vanillaBackupPath)
                    if not os.path.isfile(vanillaBackupFilePath):
                        copy2(vanillaFilePath, vanillaBackupFilePath)
                        if name == 'VersionManifest.csv':
                            update_version_manifest(vanillaFilePath, modFilePath)
                        else:
                            copy2(modFilePath, vanillaFilePath)
                    else:
                        if name == 'VersionManifest.csv':
                            update_version_manifest(vanillaFilePath, modFilePath)
                        else:
                            copy2(modFilePath, vanillaFilePath)
                else:
                    copy2(modFilePath, vanillaFilePath)
    statusLabel = Label(master, text='                                                               ').grid(row=iRow+3, sticky=W)
    statusLabel = Label(master, text='Mods installed.').grid(row=iRow+3, sticky=W)

#Returns backed-up vanilla, unmodified files to their original directory.
#Also deletes any additional mod files from the game's data to tidy up after itself.
def to_stock():
    for mod in allMods:
        modPath = modsPath+'\\'+mod
        for root, dirs, files in os.walk(modPath):
            for name in files:
                modFilePath = os.path.join(root, name)
                vanillaFilePath = modFilePath.replace(modPath, btDataPath)
                vanillaBackupFilePath = modFilePath.replace(modPath, vanillaBackupPath)
                if os.path.isfile(vanillaFilePath) and not os.path.isfile(vanillaBackupFilePath):
                    os.remove(vanillaFilePath)
    for root, dirs, files in os.walk(vanillaBackupPath):
        for name in files:
            vanillaBackupFilePath = os.path.join(root, name)
            vanillaFilePath = vanillaBackupFilePath.replace(vanillaBackupPath, btDataPath)
            if not filecmp.cmp(vanillaBackupFilePath, vanillaFilePath):
                if name == 'VersionManifest.csv':
                    restore_version_manifest(vanillaBackupFilePath, vanillaFilePath)
                else:
                    copy2(vanillaBackupFilePath, vanillaFilePath)
    statusLabel = Label(master, text='                                ').grid(row=iRow+3, sticky=W)
    statusLabel = Label(master, text='Returned to stock configuration.').grid(row=iRow+3, sticky=W)

#Used to find the Battletech directory.
def get_script_path():
    return os.path.dirname(os.path.realpath(sys.argv[0]))

#Uses Pandas to dynamically update VersionManifest.csv with a Mod's extra entries.
def update_version_manifest(vanVm, modVm):
    modDf = pd.read_csv(modVm)
    vanDf = pd.read_csv(vanVm)
    outDf = pd.concat([modDf,vanDf]).drop_duplicates()
    with open(vanVm, 'w+') as f:
        outDf.to_csv(f, index=False)

#Rather than dynamically readjusting VersionManifest, it just restores the backed-up copy.
def restore_version_manifest(vanBackVm, vanVm):
    copy2(vanBackVm, vanVm)

btPath = get_script_path()
btDataPath = btPath+'\BattleTech_Data\StreamingAssets'
modsPath = btPath+'\Mods'
if not os.path.exists(modsPath):
    os.makedirs(modsPath)
vanillaBackupPath = btPath+'\VanillaBackup'
if not os.path.exists(vanillaBackupPath):
    os.makedirs(vanillaBackupPath)
allMods = os.listdir(modsPath)
statusText = 'Ready.'

#The below all relates to the GUI.
Label(master, text="Available mods:").grid(row=0, sticky=W)
iRow = 1
for mod in allMods:
    Label(master, text=mod).grid(row=iRow, sticky=W)
    iRow = iRow+1
Button(master, text='Load Mods', command=load_mods).grid(row=iRow+1, sticky=W, pady=4)
Button(master, text='Return To Stock', command=to_stock).grid(row=iRow+2, sticky=W, pady=4)
statusLabel = Label(master, text=statusText).grid(row=iRow+3, sticky=W)
Button(master, text='Quit', command=master.quit).grid(row=iRow+4, sticky=W, pady=4)

#Initiates the GUI. Any cleanup operations should be written below this line.
mainloop()