diff --git a/README.md b/README.md index f817c72..fd4de4e 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,10 @@ The main source file for the CLI version is `seven_mods.py`. General usage is: The available commands are: - `list`: show a listing of all installed mods. Disabled mods appear in red. -Enabled ones appear in green, and are followed by an asterisk (`*`). `list` -also accepts an optional argument, `profiles`, which tells it to list all saved -profiles instead of mods. +Enabled ones appear in green, and are followed by an asterisk (`*`). Mods that +are expected to be able to work server-side only (i.e. "modlets") are followed +by a dollar sign (`$`). `list` also accepts an optional argument, `profiles`, +which tells it to list all saved profiles instead of mods. - `enable`: enable mods. A list of mod names can be given, separated by spaces. The mod names are the names of their respective folders, *not* the names listed in their respective `ModInfo.xml` files. Alternatively, to enable all mods, @@ -88,4 +89,4 @@ and disabled mods are shown in red. Clicking on a mod's list entry will toggle it. Note: If you click twice too fast, tkinter will think you're double-clicking, -and won't change its state twice. \ No newline at end of file +and won't change its state twice. diff --git a/seven_mods.py b/seven_mods.py index 2fc82cb..fbde5bb 100755 --- a/seven_mods.py +++ b/seven_mods.py @@ -35,6 +35,9 @@ MODS_DIR = os.getcwd() STEAM_LIBRARIES_DIR = os.path.join(HOME, ".local/share/Steam/config/libraryfolders.vdf") SEVEN_DIR = os.path.join(HOME, "/.steam/steam/steamapps/common/7 Days To Die") CONFIG_FILE = "seven-mods.cfg" +SERVER_ONLY_FILE_TYPES = ("md", "txt", "xml") +ALL_MODS = ("-a", "-A", "*", "--all") +ALL_SERVER_ONLY_MODS = ("-s", "-S", "--server-only") HELP_TEXT="""Usage: seven-mods.py Valid commands are: @@ -264,6 +267,20 @@ def get_available_mods(cfg: Config) -> list: ret.append(entry.name) return sorted(ret, key = lambda x: x.casefold()) +def mod_is_server_only(mod_dir: str) -> bool: + """Checks if a mod can work when only deployed on a server.""" + with os.scandir(mod_dir) as it: + for entry in it: + if entry.is_dir(): + if not mod_is_server_only(entry.path): + #print(f"Mod {mod_dir} found not server only at {entry.name}") + return False + elif entry.is_file(): + if not entry.name.endswith(SERVER_ONLY_FILE_TYPES): + #print(f"Mod {mod_dir} found not server only at {entry.name}") + return False + return True + def enable_mod(cfg: Config, mod_name: str): link_source = os.path.join(cfg.mods_dir, mod_name) link_dest = os.path.join(get_internal_mods_path(cfg), mod_name) @@ -290,7 +307,8 @@ def toggle_mod(cfg: Config, mod_name: str): os.symlink(link_source, link_dest) def get_mod_data(mod: str, cfg: Config) -> tuple: - mod_info_path = os.path.join(cfg.mods_dir, mod, "ModInfo.xml") + mod_path = os.path.join(cfg.mods_dir, mod) + mod_info_path = os.path.join(mod_path, "ModInfo.xml") try: tree = et.parse(mod_info_path) except: @@ -299,6 +317,7 @@ def get_mod_data(mod: str, cfg: Config) -> tuple: name = "" author = "" version = "" + serverside = str(mod_is_server_only(mod_path)) for node in root[0]: if node.tag == "Name": name = node.attrib["value"] @@ -306,7 +325,7 @@ def get_mod_data(mod: str, cfg: Config) -> tuple: author = node.attrib["value"] elif node.tag == "Version": version = node.attrib["value"] - return name, version, author + return name, version, author, serverside def command_list(args: list, cfg: Config, profiles: ModProfiles): if len(args) == 3 and args[2] == "profiles": @@ -327,41 +346,57 @@ def command_list(args: list, cfg: Config, profiles: ModProfiles): print("".join(["mod".ljust(ts1), "name".ljust(ts2), "version".ljust(ts3), "author".ljust(ts4)])) for mod, name, version, author in zip(available_mods, pretty_names, versions, authors): if mod in loaded_mods: - print(f"\x1b[32m{mod.ljust(ts1)}{name.ljust(ts2)}{version.ljust(ts3)}{author.ljust(ts4)}\x1b[0m *") + print(f"\x1b[32m{mod.ljust(ts1)}{name.ljust(ts2)}{version.ljust(ts3)}{author.ljust(ts4)}\x1b[0m{suffix} *") else: - print(f"\x1b[31m{mod.ljust(ts1)}{name.ljust(ts2)}{version.ljust(ts3)}{author.ljust(ts4)}\x1b[0m") + print(f"\x1b[31m{mod.ljust(ts1)}{name.ljust(ts2)}{version.ljust(ts3)}{author.ljust(ts4)}\x1b[0m{suffix}") else: for mod in available_mods: + suffix = "" + if mod_is_server_only(os.path.join(cfg.mods_dir, mod)): + suffix += " $" if mod in loaded_mods: - print(f"\x1b[32m{mod}\x1b[0m *") + print(f"\x1b[32m{mod}\x1b[0m{suffix} *") else: - print(f"\x1b[31m{mod}\x1b[0m") + print(f"\x1b[31m{mod}\x1b[0m{suffix}") else: print(f"Usage: {args[0]} {args[1]} [profiles]") def command_enable(args: list, cfg: Config, profiles: ModProfiles): args = [a.rstrip("/") for a in args] for mod in args[2:]: - if not mod in ("-a", "-A", "*"): + if not mod in ALL_MODS + ALL_SERVER_ONLY_MODS: enable_mod(cfg, mod) - else: + elif mod in ALL_MODS: available_mods = get_available_mods(cfg) loaded_mods = get_loaded_mods(cfg) for mod in available_mods: if mod not in loaded_mods: enable_mod(cfg, mod) break + elif mod in ALL_SERVER_ONLY_MODS: + available_mods = get_available_mods(cfg) + loaded_mods = get_loaded_mods(cfg) + for mod in available_mods: + if mod not in loaded_mods and mod_is_server_only(os.path.join(cfg.mods_dir, mod)): + enable_mod(cfg, mod) + break def command_disable(args: list, cfg: Config, profiles: ModProfiles): args = [a.rstrip("/") for a in args] for mod in args[2:]: - if not mod in ("-a", "-A", "*"): + if not mod in ALL_MODS + ALL_SERVER_ONLY_MODS: disable_mod(cfg, mod) - else: + elif mod in ALL_MODS: loaded_mods = get_loaded_mods(cfg) for mod in loaded_mods: disable_mod(cfg, mod) break + elif mod in ALL_SERVER_ONLY_MODS: + loaded_mods = get_loaded_mods(cfg) + for mod in loaded_mods: + if mod_is_server_only(os.path.join(cfg.mods_dir, mod)): + disable_mod(cfg, mod) + break def command_toggle(args: list, cfg: Config, profiles: ModProfiles): args = [a.rstrip("/") for a in args] diff --git a/seven_mods_gui.py b/seven_mods_gui.py index cb20871..c338e20 100755 --- a/seven_mods_gui.py +++ b/seven_mods_gui.py @@ -82,13 +82,17 @@ class AppFrame(ttk.Frame): # Mod list self.mod_list = ttk.Treeview(self, - columns = ("name", "version", "author", "enabled"), + columns = ("name", "version", "author", "serverside", "enabled"), height = 20, selectmode = "browse") self.mod_list.heading("name", text = "Name") self.mod_list.heading("version", text = "Version") + self.mod_list.column("version", width = 100) self.mod_list.heading("author", text = "Author") + self.mod_list.heading("serverside", text = "Serverside") + self.mod_list.column("serverside", width = 100) self.mod_list.heading("enabled", text = "Enabled") + self.mod_list.column("enabled", width = 100) self.mod_list.tag_configure("DISABLED", foreground = "red") self.mod_list.tag_configure("ENABLED", foreground = "green") self.mod_list.tag_bind("DISABLED", '', lambda e: self.command_enable(self.mod_list.selection()[0])) @@ -177,7 +181,8 @@ class AppFrame(ttk.Frame): self.refresh_mod_list() def get_mod_data(self, mod: str) -> tuple: - mod_info_path = os.path.join(self.cfg.mods_dir, mod, "ModInfo.xml") + mod_path = os.path.join(self.cfg.mods_dir, mod) + mod_info_path = os.path.join(mod_path, "ModInfo.xml") try: tree = et.parse(mod_info_path) except: @@ -186,6 +191,7 @@ class AppFrame(ttk.Frame): name = "" author = "" version = "" + serverside = str(seven_mods.mod_is_server_only(mod_path)) for node in root[0]: if node.tag == "Name": name = node.attrib["value"] @@ -193,7 +199,7 @@ class AppFrame(ttk.Frame): author = node.attrib["value"] elif node.tag == "Version": version = node.attrib["value"] - return name, version, author + return name, version, author, serverside def refresh_mod_list(self): if self.cfg == None: