Giter Site home page Giter Site logo

modbuddy's Introduction

Mod buddy

After attempting to mod Stalker Anomaly, i had issues finding good tools for the modding scene in Linux. Instead of merging a large amount of files like a caveman, I attempted to create a good enough tool for my use cases.

Features

Showcase of modbuddy

  • Apply any mod, regardless of how the folder structure is.
  • Applied mods are hard linked, saving space
  • Support for an arbitrary amount of games
  • Mod presets
  • Prioritize mod order

Future dreams

  • Conflict detection
  • Revert a modified folder to its unmodded state
  • More user friendlyness
  • Use relative paths on mod settings

Cases

To further understand why this exists in the first place, I have some examples where the use case may be clear:

  • Texture packs with optional patches

This is one use case i find practical. Rather than merging all the patches i want, i can manage them through Mod buddy by importing each patch as a separate mod.

WWHD example

  • STALKER Anomaly

Stalker anomaly has a big variety of addons/mods, and as a lot of the mods overlap with varying grade of compabillity. This workflow is easily manageable as well. Since not all addons have the same file structure ('ROOT/gamedata'), Mod buddy is developed to handle such cases as well.

Explanation

Modbuddy is leveraging the usage of hard links to both avoid duplicated data and avoid any suprises in regards to the filesystem. This is the main functionality residing in modpack.py

Flow example

Usage

  • Requirements can be retrieved with pip install -r requirements.txt
  • Run via main.py

Set up a game folder

  • First you need to set up a destination folder. This is done with the control panel on the upper right ("New Game"). Here you will choose which folder the mods will reside. Empty mod buddy Get mod folder

Presets

  • When setting up a new game, a default profile is created as well (controls at the top middle). Should you need multiple configurations, this is where you can control this.

Add mods

Lastly, you add mods via the "Add mods"-group to the top left.

You can add mods from two different ways:

  • Add mod from archive: Extract target archive to a folder inside Mod buddy, where you then can choose the folder inside
  • Add mod from folder: Choose a folder which contains a mod
    • This is intended for mods that have multiple patches (such as the texture pack mentioned above) or when the user didn't use the above mentioned button.
  • Add mod from source:

Sources

Sources up

Sources is a more complex package handler, where you can organize your mod sources. A mod registered as a source will ease tasks such as:

  • updating
  • applying patches/multiple instances pr. mod
  • bulk downloads
  • setup replication
  • modpack creation

A source is given with the pattern URL[;subfolder], where one line correlates with one source.

Sources example

At the moment it supports links from Moddb and Github

Disclaimer

Before you want to try this out: I'm not a UX-designer, a QT-developer nor a cat. This is a personal project which i have found a practical use for.

modbuddy's People

Contributors

olavstornes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

modbuddy's Issues

Handle issues regarding case sensitivity

Linux is case sensitive on files, while windows-platforms aren't, creating a possible problem with mods that don't follow file casings as strictly.

Alternatives

  • Convert everything to lowercase (including the original files after backup)
    • Works in conjunction of #1 and handling "base content" as a separate mod
  • Implement case insensitivity on appliance of files

Flip the default column spacing for the name and path fields

The path field is helpful when trying to remember where the source files are coming from, but most of the time this text is adding eye fatigue. it would be nice to make the path column toggle-able, and start out minimized (ex: "/path/to/f..."), and have the name column fully expanded as that is for informative when deciding which mods I am trying to use.

Default

image

Preferred

image

Error on first start

Hi,
I know you're going to polish it anyways, but it might be good to know that the program doesn't even start here.
(Arch Linux with Python 3.9.2, pyqt5-5.15.3-2, pyqt5-sip-12.8.1-3 - both pyqt5 components are installed via package management, not via pip)

On an initial launch I get this error:

$ LC_ALL=C ./main.py
Traceback (most recent call last):
  File "/tmp/ModBuddy/./main.py", line 290, in <module>
    window = Ui()
  File "/tmp/ModBuddy/./main.py", line 47, in __init__
    self.update_game_combobox()
  File "/tmp/ModBuddy/./main.py", line 80, in update_game_combobox
    for x in GAME_PRESET_FOLDER.iterdir():
  File "/usr/lib/python3.9/pathlib.py", line 1149, in iterdir
    for name in self._accessor.listdir(self):
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/ModBuddy/games'

When I create the missing games directory manually this one:

$ LC_ALL=C ./main.py
Traceback (most recent call last):
  File "/tmp/ModBuddy/./main.py", line 290, in <module>
    window = Ui()
  File "/tmp/ModBuddy/./main.py", line 50, in __init__
    self.update_fileview()
  File "/tmp/ModBuddy/./main.py", line 133, in update_fileview
    path = str(Path(mod_path).parent)
  File "/usr/lib/python3.9/pathlib.py", line 1071, in __new__
    self = cls._from_parts(args, init=False)
  File "/usr/lib/python3.9/pathlib.py", line 696, in _from_parts
    drv, root, parts = self._parse_args(args)
  File "/usr/lib/python3.9/pathlib.py", line 680, in _parse_args
    a = os.fspath(a)
TypeError: expected str, bytes or os.PathLike object, not NoneType

Did I forget something obvious?
If you need anything else for a proper bug report please let me know.

modbuddy mod file linking some how causing mod loading errors in java game.

Some how the linking of the mods I am loading into the mods directory from the ModBuddy .mods directory is resulting in this games java compiling to fail with a bad absolute path from .mods as /./mods/ for the mods. I flailed a bit by trying to escape the \. in the ModBuddy json file. XD

java.lang.RuntimeException: Error loading [data/missions/kayse_NoSuchOrg_test/descriptor.json] resource, not found in [
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/slightly better techmining,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/adjusted sector v0.4.2,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/another portrait pack,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/apex design collective,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/ss-armaa,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/battletech portrait pack,
/home/alan-cugler/Games/starsector/starsector_linux-0.95.1a-RC6/starsector/./mods/captainslog
...

I am currently testing switching out .mods/ with a non-hidden directory such as modbuddy/ to see if the period is really somehow being treated as its own directory.

Ensure that disabled mods are removed

Given that applying mods only links them to its corresponding place, a disabled mod doesn't mean "delete this mod" as of now.

Todo:

  • Ensure that applying mods start from a clean slate

Tightly related with #1

Base content a duplicated variable caused unexpected bug during repeated testing

The mods starting base content variable strikes me as something that should be hidden from the user as it is always required to be loaded first so mods can then make layered changes on those files. That being said this was missing from multiple scrub testing runs, and it took me three hours to figure out it was missing from the mods list.

ModBuddy/main.py

Lines 222 to 224 in f049634

'mods': {
BASE_CONTENT_NAME: str(initial_mod_content_folder.resolve())
}

When looking at where the error was coming from, I realized a null value for the base content path seems silly since the path is available in the game_mod_folder variable. So not knowing which one of my testing resets messed up the mods dictionary leads me to ask if target_folder should be set to the game_mod_folder and not extracted from the mod_list dictionary?

ModBuddy/modpack.py

Lines 39 to 44 in f049634

def initialize_configs(profile_payload: dict, mod_list: dict, input_folder: Path, output_folder: Path):
for single_mod in profile_payload:
if single_mod.get('enabled'):
target_folder = input_folder / mod_list.get(single_mod.get('name'))
x = ModPack(target_folder, output_folder)
x.add_mod()

Split up application logic from the GUI

Before it grows a tad too big, main.py should split out it's gui-related things and the application logic.

Todo

  • Rip out all logic from main.py into it's own file/class.
  • Ensure that the Ui-class' sole responsibillity is GUI-related tasks, such as binding events and calling functions

Backup volatile mod folder before use

Loading mods is now a one-way street, which could possibly be a bad thing if the folder isn't empty at the start.

Todo:

  • When adding a new game, ensure that the original files are somewhere else
  • Use hard links to avoid duplicated data.
  • Avoid something happening to this folder while using modbuddy.

Improve presets

Presets are a bit crude as of now.

  • When importing a mod, all presets should also receive the imported mod as well.
  • On all presets, the new mod should initially be at the bottom and deactivated

Implement option of new autosaved presets

If enabled and after committing a new set of mods, a separate profile should be made.

Todo:

  • Autocreate a new preset after committing a new set of mods.
  • The name should be timestamped (YYYY-MM-DD-HHMMSS)

Required steps to "resetting" ModBuddy

What are the steps needed to delete meta data for a profile and for deleting a game from ModaBuddy? Currently, I reset the whole application by deleting everything in the games directory and deleting the settings.json.

It would be better for testing and maybe user organization if the games directory was for saving base things about each game added, and then a profile directory for each profile made. I am assuming a bit of metadata to tag which game each profile goes to. This may or may not result in deprecating the settings.json file, which is my goal because its nice to delete just files rather than edit them for resetting an application.

This question/suggest are just that, you may have design choices in mind that want to stick to the current file architecture.

Replace shutil with patool

I am using ModBuddy for Linux game modding. Currently been playing StarSector alot, and the three main archives that are used by the modding community are: zip, rar, and 7z. In ModBuddy, shutil seems to cover zip files and tar balls. I recommend\request expanding the archive supported types to as many as possible as many different games will have modding communities using different compression tools.

ModBuddy/main.py

Lines 267 to 276 in f049634

for archive in archives[0]:
try:
suff = Path(archive).suffix
folder_name = Path(archive).stem
target_folder = Path(default_mod_folder) / folder_name
shutil.unpack_archive(archive, target_folder)
except shutil.ReadError:
QMessageBox.warning(self, '', f'Sorry, but {suff}-archives is not supported')
else:
self.add_mod(target_folder)

I recommend replacing the above with something like the below:

        for archive in archives[0]:
            suff = Path(archive).suffix
            folder_name = Path(archive).stem
            target_folder = Path(default_mod_folder) / folder_name
            try:
                patoolib.extract_archive(archive, outdir=target_folder)
            except FileNotFoundError:
                mod_home = Path(target_folder)
                mod_home.mkdir()
                patoolib.extract_archive(archive, outdir=target_folder)
            else:
                self.add_mod(target_folder)

I think my code is crap at the moment so I know the above proposed solution doesnt fully work or look pythonic. The main reason for switching to patool|patoolib is it does all the work for managing just about every archive type there is.

Implement dirty bit

After modifications, a dirty bit should be set to indicate unsaved changes.

Should fire off when:

  • A new mod is added/removed
  • Mod order is changed
  • A mod is activated/deactivated

Also consider some UX for this as well

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.