Advertisement
Vearie

ODF File Opener

Feb 12th, 2022 (edited)
944
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 39.34 KB | None | 0 0
  1. """This tool is designed to emulate BZCC file indexing for ODF files and open search queries as tabs in a text editor."""
  2. VERSION = 1.03
  3.  
  4. import os
  5. import subprocess
  6.  
  7. # Default steam directory to be assumed if script could not read from registry.
  8. STEAM_DIRECTORY = "C:\\Program Files (x86)\\Steam"
  9.  
  10. # Path to text editor you want to open ODF files with, see function 'open_files'
  11. TEXT_EDITOR = os.path.join(os.environ["WINDIR"], "system32", "notepad.exe") # Set TEXT_EDITOR_AS_TABS to False to use notepad
  12. #TEXT_EDITOR = "C:\\Program Files\\Notepad++\\notepad++.exe"
  13. # TEXT_EDITOR = "C:\\program files\\wscite510\\SciTE.exe" # @CHANGE
  14.  
  15. # If True, open ODF files as tabs. Otherwise open an instance for each file.
  16. TEXT_EDITOR_AS_TABS = False
  17. TEXT_EDITOR_MAX_TABS = 32
  18. TEXT_EDITOR_MAX_INSTANCES = 4 # Used if TEXT_EDITOR_AS_TABS == False
  19.  
  20. # Where to store cached ODF locations (need write permission)
  21. # Fast storage such as an SSD location would be preferred.
  22. # To rebuild cache, simply delete the files in this directory.
  23. CACHE_DIRECTORY = os.path.join(os.environ["TMP"], "Battlezone II Combat Commander ODF Cache")
  24. CACHE_MYDOCS_DIRECTORIES = False # Set this to True if you have a huge addon folder you rarely change to increase performance.
  25. CACHE_ROOT_DIRECTORIES = True
  26. CACHE_WORKSHOP_DIRECTORIES = True
  27.  
  28. # If True, uses regex matching for queries that contain any regex characters.
  29. # Otherwise simple wildcard * matches take its place.
  30. FULL_REGEX_MATCHING = False
  31.  
  32. # Show partial matches if no ODF matching any query is found.
  33. # This has the performance cost of iterating over all odf names
  34. # if any queries are not matched to check for partial matches.
  35. IMPLEMENT_SUGGESTIONS = True
  36.  
  37. # If True, use the currently running mod instead of the user-selected mod.
  38. # In 'launch.ini', the active mod id value updates as soon as a player clicks
  39. # to join a game and downloads the mod immediately as they enter the lobby.
  40. # If the player hasn't joined a game running a different mod, it will just be the same as selected mod.
  41. USE_ACTIVE_MOD_INSTEAD_OF_SELECTED_MOD = False
  42.  
  43. # This is called when the queries are finalized and the list of ODF files are passed
  44. def open_files(odf_full_file_paths=[]):
  45.     if not os.path.exists(TEXT_EDITOR):
  46.         print("%r not found. Set a valid program to open ODF files with.\n%d query matches:" % (TEXT_EDITOR, len(odf_full_file_paths)))
  47.         for path in odf_full_file_paths:
  48.             print(">", path)
  49.         return
  50.    
  51.     if TEXT_EDITOR_AS_TABS:
  52.         subprocess.call([TEXT_EDITOR] + odf_full_file_paths)
  53.     else:
  54.         for which in odf_full_file_paths:
  55.             subprocess.Popen([TEXT_EDITOR, which])
  56.  
  57. # ############################     ###########################
  58. # ###########################       ##########################
  59. # ###########################       ##########################
  60. # ###################                       ##################
  61. # ###############   ##########  #  ##########   ##############
  62. # #############  ############   #   ############  ############
  63. # ###########  ##############   #   ##############  ##########
  64. # ##########  ##############   ###   ##############  #########
  65. # ######### ######    #####    ###    ####    ######  ########
  66. # ########  #########      ##########       #########  #######
  67. # #######  #############  ###      ###   ############# #######
  68. # #######  ############  ###        ###   ############  ######
  69. # #######  #########     ####      #####     ######### #######
  70. # ########  ######    #####################    ######  #######
  71. # ###############                               ##############
  72. # ######       ###   ##########   ##########   ###       #####
  73. # ######       ################   ################       #####
  74. # #######     ################## ##################     ######
  75. # ###############   ############ ###########   ###############
  76. # ###################     #############     ##################
  77. # ____________________________________________________________
  78.  
  79. import re
  80. import sys
  81. import zlib
  82. import time
  83. import struct
  84. import pickle
  85. import configparser
  86. from datetime import datetime
  87. from collections import namedtuple
  88. from tempfile import NamedTemporaryFile
  89.  
  90. # IMPORT BZ2CFG START
  91. # from bz2cfg import CFG
  92. class ParseError(Exception):
  93.     def __init__(self, file="UNDEFINED", line=0, col=0, msg="Parsing Error"):
  94.         self.file = file
  95.         self.line = line
  96.         self.col = col
  97.         self.msg = msg
  98.    
  99.     def __str__(self):
  100.         return "%s:%d:%d - %s" % (self.file, self.line, self.col, self.msg)
  101.  
  102. class UnterminatedString(ParseError): pass
  103.  
  104. class ConfigContainer:
  105.     def walk(self):
  106.         for child in self.children:
  107.             yield child
  108.             yield from child.walk()
  109.    
  110.     def get_attribute(self, attribute_name, occurrence=1):
  111.         return self._find(attribute_name, self.attribute, occurrence)
  112.    
  113.     def get_attributes(self, name=None):
  114.         return self._findall(name, self.attribute, False)
  115.    
  116.     def get_container(self, container_name, occurrence=1):
  117.         return self._find(container_name, self.children, occurrence)
  118.    
  119.     def get_containers(self, name=None):
  120.         return self._findall(name, self.children)
  121.    
  122.     def _findall(self, name, _list, reverse=False):
  123.         if name:
  124.             name = name.casefold()
  125.        
  126.         for item in (reversed(_list) if reverse else _list):
  127.             if name is None or item.name.casefold() == name:
  128.                 yield item
  129.    
  130.     def _find(self, name, _list, occurrence=1):
  131.         reverse = occurrence < 0
  132.         occurrence = abs(occurrence)
  133.        
  134.         for index, container in enumerate(self._findall(name, _list, reverse), start=1):
  135.             if index == occurrence:
  136.                 return container
  137.        
  138.         raise KeyError("%r not found in %r" % (name, self.name))
  139.    
  140.     def __iter__(self):
  141.         for child in self.children:
  142.             yield child
  143.    
  144.     def __getitem__(self, container_name):
  145.         """Returns child container by name. Raises KeyError if not found."""
  146.         return self.get_container(container_name)
  147.  
  148. class CFG(ConfigContainer):
  149.     def __init__(self, filepath=None):
  150.         self.children = []
  151.         self.attribute = []
  152.         self.filepath = filepath
  153.        
  154.         self.name = filepath or "<Untitled - CFG>"
  155.        
  156.         if self.filepath:
  157.             with open(self.filepath, "r") as f:
  158.                 self.read(f)
  159.    
  160.     def __str__(self):
  161.         return "<%s %r>" % (__class__.__name__, self.name)
  162.    
  163.     # Yields either a word or a control character from a file, 1 at a time
  164.     def parse_words(self, f):
  165.         # Note: these characters do not add to the column counter
  166.         NON_CHARACTER = "\x00\r"
  167.  
  168.         # Characters which count as a valid name
  169.         NAME = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789_.+-"
  170.         WHITESPACE = " \t\n"
  171.        
  172.         line = col = 1
  173.         state = READ_ANY = 0
  174.         in_comment = 1
  175.         in_quote = None
  176.         word = ""
  177.        
  178.         for c in f.read():
  179.             if c in NON_CHARACTER:
  180.                 continue
  181.            
  182.             if c == "\n":
  183.                 line += 1
  184.                 col = 1
  185.             else:
  186.                 col += 1
  187.            
  188.             if in_quote:
  189.                 # Character matches quote, completing the string.
  190.                 # Note that BZ2 does not handle escapes (e.g. \").
  191.                 if c == in_quote:
  192.                     yield word, line, col
  193.                     word = ""
  194.                     in_quote = None
  195.                
  196.                 # New line without terminating quote.
  197.                 elif c == "\n":
  198.                     raise UnterminatedString(self.filepath, line, col, "Unterminated String")
  199.                
  200.                 else:
  201.                     word += c
  202.            
  203.             elif state == READ_ANY:
  204.                 if c in NAME:
  205.                     word += c
  206.                
  207.                 else:
  208.                     # End of continuous word
  209.                     if c in WHITESPACE:
  210.                         if word:
  211.                             yield word, line, col
  212.                             word = ""
  213.                    
  214.                     # Start of comment
  215.                     elif c == "/":
  216.                         if word:
  217.                             yield word, line, col
  218.                        
  219.                         state = in_comment
  220.                         word = c
  221.                    
  222.                     # Start of string quote
  223.                     elif c in "\"'":
  224.                         if word:
  225.                             yield word, line, col
  226.                             word = ""
  227.                        
  228.                         in_quote = c
  229.                    
  230.                     # Special control character
  231.                     elif c in "{}(),;":
  232.                         if word:
  233.                             yield word, line, col
  234.                             word = ""
  235.                        
  236.                         yield c, line, col
  237.                    
  238.                     # Unhandled
  239.                     else:
  240.                         raise ParseError(self.filepath, line, col, "Unexpected Character %r" % c)
  241.            
  242.             elif state == in_comment:
  243.                 # New line character indicates of comment
  244.                 if c == "\n":
  245.                     if word:
  246.                         pass # ignore comments
  247.                         # yield word
  248.                    
  249.                     word = ""
  250.                     state = READ_ANY
  251.                
  252.                 else:
  253.                     word += c
  254.    
  255.     def read(self, f):
  256.         EXPECT_NAME = 0
  257.         EXPECT_PARAM_OPEN = 1
  258.         EXPECT_PARAM = 2
  259.         EXPECT_CONTAINER_TYPE = 3
  260.         state = EXPECT_NAME
  261.        
  262.         # Temporary buffers
  263.         name = ""
  264.         parameters = []
  265.        
  266.         # Empty root container
  267.         container_at_level = [self]
  268.        
  269.         for word, line, col in self.parse_words(f):
  270.             if state == EXPECT_NAME:
  271.                 if word == "}":
  272.                     if len(container_at_level) <= 1:
  273.                         # Extra closing brace: Could raise an exception here, but I think it's safe to ignore.
  274.                         continue
  275.                    
  276.                     del(container_at_level[-1])
  277.                     continue
  278.                
  279.                 name = word
  280.                 parameters = []
  281.                 state = EXPECT_PARAM_OPEN
  282.                 continue
  283.            
  284.             elif state == EXPECT_PARAM_OPEN:
  285.                 if word == "(":
  286.                     state = EXPECT_PARAM
  287.                     continue
  288.            
  289.             elif state == EXPECT_PARAM:
  290.                 if word == ",":
  291.                     continue
  292.                
  293.                 elif word == ")":
  294.                     state = EXPECT_CONTAINER_TYPE
  295.                     continue
  296.                
  297.                 else:
  298.                     parameters += [word]
  299.                     continue
  300.            
  301.             elif state == EXPECT_CONTAINER_TYPE:
  302.                 # Start of a new container
  303.                 if word == "{":
  304.                     container = Container(name, *parameters)
  305.                     container_at_level[-1].children += [container]
  306.                     container_at_level.append(container)
  307.                    
  308.                     state = EXPECT_NAME
  309.                     continue
  310.                
  311.                 # End of attribute
  312.                 elif word == ";":
  313.                     container_at_level[-1].attribute += [Attribute(name, *parameters)]
  314.                     state = EXPECT_NAME
  315.                     continue
  316.            
  317.             raise ParseError(self.filepath, line, col)
  318.  
  319. class Container(ConfigContainer):
  320.     def __init__(self, name, *parameters):
  321.         self.name = name
  322.         self.parameter = parameters
  323.         self.children = []
  324.         self.attribute = []
  325.    
  326.     def __str__(self):
  327.         return "<%s %s(%s)>" % (__class__.__name__, self.name, ", ".join("\"%s\"" % p for p in self.parameter))
  328.  
  329. class Attribute:
  330.     def __init__(self, name, *parameters):
  331.         self.name = name
  332.         self.parameter = parameters
  333.    
  334.     def __str__(self):
  335.         return "<%s %s(%s)>" % (__class__.__name__, self.name, ", ".join("\"%s\"" % p for p in self.parameter))
  336. # IMPORT BZ2CFG END
  337.  
  338. # IMPORT STEAMVDF START
  339. class steamvdf:
  340.     class VDF_ParseError(Exception):
  341.         def __init__(self, msg, line, col):
  342.             self.msg = msg
  343.             self.line = line
  344.             self.col = col
  345.        
  346.         def __str__(self):
  347.             return self.msg + " (line %d column %d)" % (self.line + 1, self.col + 1) # +1 for 0-based to 1-based
  348.  
  349.     def read(path):
  350.         """Returns dictionary of {key: value or sub}, case sensitive."""
  351.         with open(path, "r") as f:
  352.             escape = False
  353.             in_quote = False
  354.             words = []
  355.            
  356.             root = {}
  357.             section = root
  358.             section_history = []
  359.            
  360.             for line, row_content in enumerate(f):
  361.                 for col, c in enumerate(row_content):
  362.                     if in_quote:
  363.                         if c in "\r\n":
  364.                             raise VDF_ParseError("unterminated string %r" % word, line, col)
  365.                         elif escape:
  366.                             escape = False
  367.                             # c is an escaped character (not handled for now)
  368.                             words[-1] += "\\" + c
  369.                         elif c == in_quote:
  370.                             in_quote = False
  371.                             if len(words) == 2:
  372.                                 key, value = tuple(words)
  373.                                 section[key] = value
  374.                                 words = []
  375.                             elif len(words) > 2:
  376.                                 raise VDF_ParseError("expected 2 items for key and value but got %d items" % len(words), line, col)
  377.                         elif c == "\\":
  378.                             escape = True
  379.                         else:
  380.                             words[-1] += c
  381.                     elif c in "'\"":
  382.                         in_quote = c
  383.                         words += [""]
  384.                     elif c == "{":
  385.                         if len(words) != 1:
  386.                             raise VDF_ParseError("expected exactly 1 item before opening brace but got %d items" % len(words), line, col)
  387.                        
  388.                         new_section = {}
  389.                         section_history.append(section)
  390.                         section[words[0]] = new_section
  391.                         section = new_section
  392.                         words = []
  393.                     elif c == "}":
  394.                         if len(section_history) <= 0:
  395.                             raise VDF_ParseError("closing brace not expected", line, col)
  396.                         section = section_history.pop()
  397.                     elif not c.isspace():
  398.                         raise VDF_ParseError("unexpected symbol %r" % c, line, col)
  399.            
  400.             if len(section_history) > 0:
  401.                 raise VDF_ParseError("%d braces left unclosed" % len(section_at)-1, line, col)
  402.            
  403.             return root
  404. # IMPOT STEAMVDF END
  405.  
  406. try:
  407.     winreg = None # Needed to have name in scope
  408.     try:
  409.         import winreg
  410.     except ImportError:
  411.         try:
  412.             import _winreg as winreg
  413.         except ImportError as exception:
  414.             print("Could not import winreg or _winreg.")
  415.             raise exception
  416.    
  417.     registry_key = winreg.OpenKeyEx(winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE), "SOFTWARE\\WOW6432Node\\Valve\\Steam")
  418.     key_value, key_type = winreg.QueryValueEx(registry_key, "InstallPath")
  419.     if key_type == winreg.REG_EXPAND_SZ:
  420.         key_value = os.path.expandvars(key_value)
  421.     elif key_type != winreg.REG_SZ:
  422.         raise TypeError()
  423.    
  424.     STEAM_DIRECTORY = key_value
  425.    
  426.     # Find which steam library BZCC is installed in
  427.     vdf = steamvdf.read(os.path.join(key_value, "steamapps/libraryfolders.vdf"))["libraryfolders"]
  428.     try:
  429.         for index in range(1000):
  430.             vdf_lib = vdf[str(index)]
  431.             for game_id, size in vdf_lib["apps"].items():
  432.                 if game_id == "624970":
  433.                     STEAM_DIRECTORY = vdf_lib["path"]
  434.                     raise KeyError() # Breaks out of both loops
  435.     except KeyError:
  436.         pass
  437. except Exception as e:
  438.     print("Failed to automatically determine steam folder.")
  439.     print("Edit this script to change the STEAM_DIRECTORY variable to your steam directory that has BZCC installed.")
  440.     print("Current STEAM_DIRECTORY set: %r" % STEAM_DIRECTORY)
  441.     print("Exception: %r" % e)
  442.  
  443. # '\' must be replaced with '/' to work with re.sub to avoid decoding character escapes
  444. BZ2R_STEAM_ID = 624970
  445. BZ2R_ROOT_DIRECTORY = ("%s/steamapps/common/BZ2R" % STEAM_DIRECTORY).replace("\\", "/")
  446. BZ2R_MYDOCS_DIRECTORY = ("%s/Documents/My Games/Battlezone Combat Commander" % os.environ["USERPROFILE"]).replace("\\", "/")
  447. BZ2R_WORKSHOP_DIRECTORY = ("%s/steamapps/workshop/content/%d" % (STEAM_DIRECTORY, BZ2R_STEAM_ID)).replace("\\", "/")
  448. BZ2R_ADDON_DIRECTORY = "%s/addon" % BZ2R_MYDOCS_DIRECTORY
  449. BZ2R_LAUNCHINI_FILE = "%s/launch.ini" % BZ2R_MYDOCS_DIRECTORY
  450. BZ2R_BZONECFG_FILE = "%s/bzone.cfg" % BZ2R_ROOT_DIRECTORY
  451. BZ2R_APPMANIFEST_FILE = "%s/steamapps/appmanifest_%d.acf" % (STEAM_DIRECTORY, BZ2R_STEAM_ID)
  452. BZ2R_APPWORKSHOP_FILE = "%s/steamapps/workshop/appworkshop_%d.acf" % (STEAM_DIRECTORY, BZ2R_STEAM_ID)
  453.  
  454. RE_ROOTDIR = re.compile(r"^@rootdir[/\\]+")
  455. RE_MYDOCSDIR = re.compile(r"^@mydocsdir[/\\]+")
  456. RE_INTEGER = re.compile(r"\d+")
  457.  
  458. # If True, the steam .acf file timestamps will be checked against cache file creation dates.
  459. # In the instance an item is newer than the cache file, the cache will be rebuilt automatically.
  460. CACHE_DATE_CHECK = True
  461.  
  462. # Used to format time displays when indicating to user a cache is outdated (e.g. cache file is older than last workshop mod update)
  463. TIME_FORMAT = "%B %a %d %Y at %I:%M%p" # See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
  464.  
  465. # Level cache is compressed with zlib (0 = none, 1-9 slowest to fastest)
  466. CACHE_COMPRESSION_LEVEL = 1 # Just 1 significantly decreases file size. Anything higher seems to be a tiny but unnecessary performance hit.
  467.  
  468. # Overwrite existing cache when applicable
  469. FORCE_GENERATE_CACHE = False
  470.  
  471. # If True, all .odf files will be deleted in the cache directory on start.
  472. # This is to clean up any loose temp files created by pak processing.
  473. # These temps can get left behind if the user kills the process before it cleans up.
  474. AUTO_CLEAN_PAK = True
  475.  
  476. # Print time it took to complete search (set to True to debug performance)
  477. SHOW_TIME = False
  478.  
  479. # Since this script only considers ODF files, this is disabled.
  480. # The functionality to process addons is included for potential future expansion.
  481. LOAD_ADDONS = False
  482. # ADDON_MOD_EXT_WHITELIST = (".pic", ".tga", ".png", ".jpg", ".bmp", ".dds", ".wav", ".ogg", ".cfg", ".bmf", ".bm2", ".inf", ".des", ".otf", ".brf", ".txt")
  483.  
  484. # If True, each ODF file in on-disk directories will be read to see if it is a valid game object.
  485. # This is to prevent items such as old '.odf' map descriptions from showing up.
  486. # Has a performance cost, and will not pick up non-gameobject odfs such as vehicle lists, weapon config, etc...
  487. # Caches would need to be rebuilt if changed. This setting does not apply to pak files.
  488. VALIDATE_ODF_FILE = False
  489.  
  490. GlobalInfo = namedtuple("GlobalInfo", "active_workshop_id selected_workshop_id active_addons language_id")
  491. ModInfo = namedtuple("ModInfo", "name catagory dependencies")
  492. DirectoryInfo = namedtuple("DirectoryInfo", "path cache recursive kind workshop_id cache_id")
  493.  
  494. KIND_ROOT = 0
  495. KIND_PAK = 1
  496. KIND_MYDOCS = 2
  497. KIND_WORKSHOP_CONFIG = 3 # Any DirectoryInfo object that is a workshop item should have a valid workshop_id value
  498. KIND_WORKSHOP_ASSET = 4
  499. KIND_WORKSHOP_ADDON = 5
  500.  
  501. # STEAM_INSTALL_REGISTRY_KEY = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Valve\\Steam", "Language" # https://partner.steamgames.com/doc/store/localization ?
  502. DEFAULT_LANGAUGE_CODE = "en" # "fr" "de"
  503.  
  504. def GetDirectoryInfo(**kwargs):
  505.     re_directory_delimiter = re.compile(r"[/\\]+")
  506.     path = kwargs["path"]
  507.    
  508.     # In BZCC, @rootdir/@mydocsdir only work if at the very start of path and all characters are lowercase.
  509.     path = RE_ROOTDIR.sub(BZ2R_ROOT_DIRECTORY + "/", path)
  510.     path = RE_MYDOCSDIR.sub(BZ2R_MYDOCS_DIRECTORY + "/", path)
  511.    
  512.     if not os.path.isabs(path):
  513.         # BZCC behaviour seems to be that relative paths are treated as being @rootdir.
  514.         # This might be because BZCC is just using its CWD, but in this script we will assume @rootdir.
  515.         path = os.path.join(BZ2R_ROOT_DIRECTORY, path)
  516.    
  517.     path = os.path.normcase(path)
  518.     path = re_directory_delimiter.sub("/", path)
  519.    
  520.     recursive = 0 if kwargs["kind"] == KIND_PAK else int(kwargs["recursive"])
  521.     cache_id = zlib.adler32(path.encode("ascii", "ignore"))
  522.    
  523.     kwargs["cache_id"] = cache_id
  524.     kwargs["cache"] = os.path.join(CACHE_DIRECTORY, "%d%d.cache" % (recursive, cache_id))
  525.     kwargs["path"] = path
  526.    
  527.     if not os.path.exists(path):
  528.         raise FileNotFoundError("%r not found" % path)
  529.    
  530.     return DirectoryInfo(**kwargs)
  531.  
  532. def find_first_file(directory, ext):
  533.     ext = ext.casefold()
  534.     for root, folders, files in os.walk(directory):
  535.         for file in files:
  536.             if file[-len(ext)::].casefold() == ext:
  537.                 return os.path.join(root, file)
  538.         break
  539.    
  540.     raise FileNotFoundError("file ending in %r not found in %r" % (ext, directory))
  541.  
  542. def read_launch_ini(launch_ini_file_path=BZ2R_LAUNCHINI_FILE):
  543.     config = configparser.ConfigParser()
  544.     config.read(launch_ini_file_path)
  545.     return GlobalInfo(
  546.         int(config["config"]["globalConfigWorkshopId"]), # Current mod being played now (e.g. user joined modded game and auto-switched) (0 is stock)
  547.         int(config["config"]["selectedConfigWorkshopId"]), # Last mod selected from the mod menu by user aka default mod (0 is stock)
  548.         [int(i) for i in RE_INTEGER.findall(config["config"]["activeAddons"])], # List of active addon mods
  549.         int(config["config"]["language"]) # Language ID (0 or >= 4=auto, 1=en, 2=fr, 3=de)
  550.     )
  551.  
  552. def read_mod_ini(mod_ini_file_path):
  553.     config = configparser.ConfigParser()
  554.     config.read(mod_ini_file_path)
  555.     return ModInfo(
  556.         config["WORKSHOP"]["modName"].strip("\"") if "modName" in config["WORKSHOP"] else None, # Name of mod
  557.         config["WORKSHOP"]["modType"].strip("\""), # Kind of mod can be: Addon, Config, Asset
  558.         [int(i) for i in RE_INTEGER.findall(config["WORKSHOP"]["assetDependencies"] if "assetDependencies" in config["WORKSHOP"] else "")] # List of other mods this mod inherits
  559.     )
  560.  
  561. def read_game_timestamp(steam_appmanifest_file_path=BZ2R_APPMANIFEST_FILE):
  562.     re_timestamp_entry = re.compile(r"(?i)\"LastUpdated\"[\r\n\s]*\"(\d+)\"")
  563.    
  564.     with open(steam_appmanifest_file_path, "r") as f:
  565.         match = re_timestamp_entry.search(f.read())
  566.         if match:
  567.             return int(match.group(1))
  568.    
  569.     raise Exception("could not find timestamp in %r" % steam_appmanifest_file_path)
  570.  
  571. def read_workshop_timestamps(steam_appworkshop_file_path=BZ2R_APPWORKSHOP_FILE):
  572.     timestamps = dict()
  573.     re_timestamp_entry = re.compile(r"(?i)\"(\d+)\"[\r\n\s]*{[\r\n\s]*\"size\"[\r\n\s]*\"\d+\"[\r\n\s]*\"timeupdated\"[\r\n\s]*\"(\d+)\"")
  574.    
  575.     with open(steam_appworkshop_file_path, "r") as f:
  576.         for workshop_item, last_updated in re_timestamp_entry.findall(f.read()):
  577.             timestamps[int(workshop_item)] = int(last_updated)
  578.    
  579.     return timestamps
  580.  
  581. def odf_file_iter(directory, recursive=False):
  582.     def odf_is_game_object(filepath):
  583.         re_classlabel = re.compile(r"(?i)\s*classLabel\s*=\s*[^\s]")
  584.        
  585.         try:
  586.             with open(filepath, "r") as f:
  587.                 for line in f:
  588.                     if re_classlabel.match(line):
  589.                         return True
  590.         except UnicodeError:
  591.             pass
  592.        
  593.         return False
  594.    
  595.     for root, folders, files in os.walk(directory):
  596.         for file in files:
  597.             name, ext = os.path.splitext(file.casefold())
  598.             if ext != ".odf":
  599.                 continue
  600.            
  601.             full_path = os.path.join(root, file)
  602.             if not VALIDATE_ODF_FILE or odf_is_game_object(full_path):
  603.                 yield name, full_path
  604.        
  605.         if not recursive:
  606.             break
  607.  
  608. PakFileInfo = namedtuple("PakFileInfo", "bz2dir file_name data_offset comp_size real_size")
  609. def pak_archive_iter(bz2dir, extensions=[".odf"]):
  610.     with open(bz2dir.path, "rb") as f:
  611.         if struct.unpack("4sI", f.read(8)) != (b"DOCP", 2):
  612.             raise Exception("invalid pak header in %r" % pak_file_path)
  613.        
  614.         f.seek(16, 0)
  615.         file_count, file_offset = struct.unpack("II", f.read(8))
  616.         f.seek(file_offset)
  617.         for index in range(file_count):
  618.             f.seek(4, 1)
  619.             # Warning: Name not sanitized.
  620.             file_name = f.read(f.read(1)[0]).decode("ascii", "ignore")
  621.             name, ext = os.path.splitext(file_name.casefold())
  622.             if ext in extensions:
  623.                 yield name, PakFileInfo(bz2dir, file_name, *struct.unpack("III", f.read(12)))
  624.             else:
  625.                 f.seek(12, 1)
  626.  
  627. class SearchTool:
  628.     def __init__(self, global_info, default_bzone_cfg_path=BZ2R_BZONECFG_FILE, force_input=None):
  629.         self.global_info = global_info
  630.         self.mod_info = None
  631.         self.bzone_cfg_path = default_bzone_cfg_path
  632.         self.cache = dict()
  633.        
  634.         if global_info.selected_workshop_id:
  635.             mod_workshop_directory = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(global_info.selected_workshop_id))
  636.             cfg_file = default_bzone_cfg_path
  637.            
  638.             try:
  639.                 cfg_file = find_first_file(mod_workshop_directory, ".cfg")
  640.                 ini_file = find_first_file(mod_workshop_directory, ".ini")
  641.                 self.mod_info = read_mod_ini(ini_file)
  642.                
  643.                 if self.mod_info.catagory.casefold() != "config":
  644.                     # After testing, it appears BZCC just ignores non-config mods and runs under the context of stock.
  645.                     self.mod_info = None
  646.                     self.global_info = global_info._replace(selected_workshop_id=0)
  647.                     print("Error - Workshop item %d is an %r mod, but a 'config' mod is required. Reverting to stock..." % (global_info.selected_workshop_id, self.mod_info.catagory))
  648.                     # raise Exception("invalid mod type %r in %r, must be config mod" % (self.mod_info.catagory, cfg_file))
  649.            
  650.             except FileNotFoundError:
  651.                 self.mod_info = None
  652.                 self.global_info = global_info._replace(selected_workshop_id=0)
  653.                 print("Error - Could not find config mod files for workshop item %d. Reverting to stock..." % global_info.selected_workshop_id)
  654.        
  655.         bzone = list(self.read_bzone_cfg())
  656.        
  657.         self.last_game_update = self.last_workshop_update = None
  658.         if CACHE_DATE_CHECK:
  659.             try:
  660.                 self.last_game_update = read_game_timestamp(BZ2R_APPMANIFEST_FILE)
  661.             except:
  662.                 print("Error - failed to get timestamp from %r" % BZ2R_APPMANIFEST_FILE)
  663.            
  664.             try:
  665.                 self.last_workshop_update = read_workshop_timestamps(BZ2R_APPWORKSHOP_FILE)
  666.             except:
  667.                 print("Error - failed to get timestamps from %r" % BZ2R_APPWORKSHOP_FILE)
  668.        
  669.         if self.mod_info:
  670.             print("<%s> w/ %d Active Addon Mods and %d Asset Dependencies" % (self.mod_info.name, len(self.global_info.active_addons), len(self.mod_info.dependencies)))
  671.             print("Workshop ID %d Using Config %r" % (self.global_info.selected_workshop_id, self.bzone_cfg_path))
  672.         else:
  673.             if os.path.normpath(self.bzone_cfg_path) == os.path.normpath(BZ2R_BZONECFG_FILE):
  674.                 print("Stock w/ %d Active Addon Mods" % len(self.global_info.active_addons))
  675.             else:
  676.                 print("%r w/ %d Active Addon Mods" % (os.path.basename(self.bzone_cfg_path), len(self.global_info.active_addons)))
  677.                 print("Using Config %r" % self.bzone_cfg_path)
  678.        
  679.         if FULL_REGEX_MATCHING:
  680.             print("Do not specify the extension '.odf' in your query. Separate multiple queries with spaces. If your query contains regular expression characters it will be used as a regex match.")
  681.         else:
  682.             print("Do not specify the extension '.odf' in your query. Separate multiple queries with spaces. You may use wildcards * for partial matches. Example: (*vscav evtank_* apwrck)")
  683.        
  684.         while True:
  685.             if self.prompt(bzone, force_input):
  686.                 break
  687.    
  688.     def read_bzone_cfg(self):
  689.         global_info, mod_info = self.global_info, self.mod_info
  690.         bzone_cfg_path = self.bzone_cfg_path
  691.         if global_info.selected_workshop_id:
  692.             bzone_cfg_path = self.bzone_cfg_path = find_first_file(os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(global_info.selected_workshop_id)), ".cfg")
  693.        
  694.         if not os.path.exists(bzone_cfg_path):
  695.             raise FileNotFoundError("%r not found" % bzone_cfg_path)
  696.        
  697.         configure_file_system = CFG(bzone_cfg_path)["ConfigureFileSystem"]
  698.        
  699.         if "appendtomydocs" in configure_file_system.attribute:
  700.             # TODO: What is even the point to this feature?
  701.             raise NotImplementedError("AppendToMyDocs in %r not supported" % bzone_cfg_path)
  702.        
  703.         use_stream_name = configure_file_system.get_attribute("SetActiveStream", -1).parameter[0].casefold()
  704.        
  705.         for stream in configure_file_system.get_containers("ConfigureStream"):
  706.             if stream.parameter[0].casefold() == use_stream_name:
  707.                 configure_stream = stream
  708.                 break
  709.         else:
  710.             raise LookupError("stream config %r not found in %r" % (use_stream_name, bzone_cfg_path))
  711.        
  712.         for attribute in configure_stream.get_attributes():
  713.             name = attribute.name.casefold()
  714.             kind = path = None
  715.            
  716.             if len(attribute.parameter) >= 1:
  717.                 path = attribute.parameter[0]
  718.                 if RE_ROOTDIR.match(path):
  719.                     kind = KIND_ROOT
  720.                 if RE_MYDOCSDIR.match(path):
  721.                     kind = KIND_MYDOCS
  722.            
  723.             if path:
  724.                 if name == "addstream":
  725.                     raise NotImplementedError("%r in %r not supported" % (attribute.name, bzone_cfg_path))
  726.                
  727.                 if name == "adddirrecurse":
  728.                     yield GetDirectoryInfo(path=path, recursive=True, kind=kind, workshop_id=0)
  729.                     continue
  730.                 elif name == "adddir":
  731.                     yield GetDirectoryInfo(path=path, recursive=False, kind=kind, workshop_id=0)
  732.                     continue
  733.                
  734.                 elif name == "addlangdir" and path:
  735.                     language_id = global_info.language_id if global_info.language_id in (1, 2, 3) else 0
  736.                     path += "/bz2r_%s" % {0: DEFAULT_LANGAUGE_CODE, 1: "en", 2: "fr", 3: "de"}[language_id]
  737.                     yield GetDirectoryInfo(path=path, recursive=True, kind=kind, workshop_id=0)
  738.                     continue
  739.                
  740.                 elif name == "addpack":
  741.                     yield GetDirectoryInfo(path=path, recursive=None, kind=KIND_PAK, workshop_id=0)
  742.                     continue
  743.                
  744.                 # Note that these AddLocals seem to assume the "selected mod".
  745.                 # They do not fire at all (checked in procmon) if no mod is selected.
  746.                 elif name == "addlocalworkshopdirrecurse":
  747.                     if not global_info.selected_workshop_id:
  748.                         print("Error - %r directive outside context of mod" % attribute)
  749.                         continue
  750.                    
  751.                     path = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(global_info.selected_workshop_id), path)
  752.                     yield GetDirectoryInfo(path=path, recursive=True, kind=KIND_WORKSHOP_CONFIG, workshop_id=global_info.selected_workshop_id)
  753.                     continue
  754.                 elif name == "addlocalworkshopdir":
  755.                     if not global_info.selected_workshop_id:
  756.                         print("Error - %r directive outside context of mod" % attribute)
  757.                         continue
  758.                    
  759.                     path = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(global_info.selected_workshop_id), path)
  760.                     yield GetDirectoryInfo(path=path, recursive=False, kind=KIND_WORKSHOP_CONFIG, workshop_id=global_info.selected_workshop_id)
  761.                     continue
  762.            
  763.             else:
  764.                 if name == "enableaddonmods":
  765.                     if not LOAD_ADDONS:
  766.                         continue
  767.                    
  768.                     for addon_mod_id in global_info.active_addons:
  769.                         # From an addon .INF:
  770.                         # "an addon workshop item must contain addonAssets/ folder."
  771.                         # "That folder will act as the root of a AddDirRecurse() call made by the engine"
  772.                         addon_path = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(addon_mod_id), "addonAssets")
  773.                         yield GetDirectoryInfo(path=addon_path, recursive=True, kind=KIND_WORKSHOP_ADDON, workshop_id=addon_mod_id)
  774.                    
  775.                     continue
  776.                
  777.                 elif name == "addworkshopconfigs":
  778.                     # From the VSR .CFG:
  779.                     # "This is required so that the workshop is populated with the base directories
  780.                     # for each workshop item that contains a global config mod"
  781.                     # I don't think this actually causes bzcc to traverse any directories?
  782.                     continue
  783.                
  784.                 elif name == "addglobalconfigmod":
  785.                     if not global_info.selected_workshop_id:
  786.                         continue
  787.                    
  788.                     for dependency_id in mod_info.dependencies:
  789.                         dep_mod_workshop_directory = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(dependency_id))
  790.                         dep_ini_file_path = find_first_file(dep_mod_workshop_directory, ".ini")
  791.                         dep_mod_info = read_mod_ini(dep_ini_file_path)
  792.                         if dep_mod_info.catagory.casefold() != "asset":
  793.                             raise Exception("%d invalid mod type %r in %r, dependencies must be asset mods" % (dependency_id, mod_info.catagory, dep_ini_file_path))
  794.                        
  795.                         # Assuming asset mods are just treated as AddDirRecurse()
  796.                         asset_path = os.path.join(BZ2R_WORKSHOP_DIRECTORY, str(dependency_id))
  797.                         yield GetDirectoryInfo(path=asset_path, recursive=True, kind=KIND_WORKSHOP_ASSET, workshop_id=dependency_id)
  798.                    
  799.                     continue
  800.            
  801.             raise Exception("unhandled config directive %r in %r" % (attribute.name, bzone_cfg_path))
  802.    
  803.     def prompt(self, bzone, force_input=None):
  804.         re_odf_name = re.compile(r"[^\s]+")
  805.        
  806.         user_input = force_input if force_input != None else input("\nODF: ")
  807.         print(">", user_input)
  808.        
  809.         queries = set()
  810.         regex_queries = set()
  811.        
  812.         for name in re_odf_name.findall(user_input):
  813.             if FULL_REGEX_MATCHING:
  814.                 if re.escape(name) != name:
  815.                     final_regex = "(?i)^" + name + "$" # Must match entire string
  816.                     # final_regex = "(?i)" + name # Must only match beginning
  817.                     try:
  818.                         regex_queries.add(re.compile(final_regex))
  819.                     except re.error as exception:
  820.                         print("invalid regex %r:" % final_regex, exception)
  821.                 else:
  822.                     queries.add(name.casefold())
  823.             elif "*" in name:
  824.                 # Regex matches will always result in the performance cost of checking every possible odf name.
  825.                 re_name = re.compile(".*".join((re.escape(segment.casefold()) for segment in name.split("*"))))
  826.                 regex_queries.add(re_name)
  827.             else:
  828.                 # If any names are not found, and suggestions are enabled, there will be a performance cost of checking every existing name.
  829.                 # Otherwise, if cached, the name is found very quickly using hash lookup (dictionary).
  830.                 queries.add(name.casefold())
  831.        
  832.         if not queries and not regex_queries:
  833.             return False
  834.        
  835.         start = time.time()
  836.         results = self.search(bzone, queries, regex_queries)
  837.         if SHOW_TIME:
  838.             print("Competion Time: %f" % (time.time() - start))
  839.        
  840.         if results:
  841.             maximum = TEXT_EDITOR_MAX_TABS if TEXT_EDITOR_AS_TABS else TEXT_EDITOR_MAX_INSTANCES
  842.            
  843.             if len(results) > maximum:
  844.                 print("Only showing %d/%d results." % (maximum, len(results)))
  845.                 results = {key: results[key] for index, key in enumerate(results, start=1) if index <= maximum}
  846.            
  847.             temp_files = []
  848.             for name, path in results.items():
  849.                 # TODO: Check if pak file was changed during the writing of each file, raising an exception?
  850.                 if type(path) == PakFileInfo:
  851.                     pak_info = path
  852.                     temp_file_path = os.path.join(CACHE_DIRECTORY, "%d-%s" % (pak_info.bz2dir.cache_id, pak_info.file_name))
  853.                     is_compressed = (pak_info.comp_size != pak_info.real_size)
  854.                     with open(pak_info.bz2dir.path, "rb") as pak:
  855.                         pak.seek(pak_info.data_offset)
  856.                         with open(temp_file_path, "wb") as tmp:
  857.                             if is_compressed:
  858.                                 tmp.write(zlib.decompress(pak.read(pak_info.comp_size)))
  859.                             else:
  860.                                 tmp.write(pak.read(pak_info.real_size))
  861.                    
  862.                     temp_files += [temp_file_path]
  863.                     results[name] = temp_file_path
  864.            
  865.             open_files(list(results.values()))
  866.            
  867.             # Cleanup
  868.             for temp_file_path in temp_files:
  869.                 if os.path.exists(temp_file_path):
  870.                     os.unlink(temp_file_path)
  871.        
  872.         return (force_input != None)
  873.        
  874.     def search(self, bzone, queries, regex_queries=[]):
  875.         """Returns results when all queries are matched or at end of search."""
  876.         results = dict()
  877.         suggestions = {query: list() for query in queries} if IMPLEMENT_SUGGESTIONS else None
  878.         temp_files = [] # Temporary file paths marked for deletion (from pak files)
  879.        
  880.         if not os.path.exists(CACHE_DIRECTORY):
  881.             os.makedirs(CACHE_DIRECTORY)
  882.        
  883.         for bz2dir in bzone:
  884.             generate_cache = use_cache = can_cache = False
  885.            
  886.             # Default iterator - iterate over files on disk in directory
  887.             odf_iterator = odf_file_iter(bz2dir.path, recursive=bz2dir.recursive)
  888.            
  889.             if bz2dir.kind == KIND_PAK:
  890.                 odf_iterator = pak_archive_iter(bz2dir)
  891.            
  892.             elif bz2dir.kind == KIND_ROOT and CACHE_ROOT_DIRECTORIES:
  893.                 can_cache = True
  894.                 if self.last_game_update != None and os.path.exists(bz2dir.cache):
  895.                     cache_file_time = os.path.getmtime(bz2dir.cache)
  896.                     if self.last_game_update > cache_file_time:
  897.                         game_date = datetime.fromtimestamp(self.last_game_update).strftime(TIME_FORMAT)
  898.                         cache_date = datetime.fromtimestamp(cache_file_time).strftime(TIME_FORMAT)
  899.                         print("Game was patched on %s, but cache last updated %s." % (game_date, cache_date))
  900.                         generate_cache = True
  901.            
  902.             elif bz2dir.kind == KIND_MYDOCS and CACHE_MYDOCS_DIRECTORIES:
  903.                 can_cache = True
  904.            
  905.             elif bz2dir.workshop_id and CACHE_WORKSHOP_DIRECTORIES:
  906.                 can_cache = True
  907.                 if self.last_workshop_update != None and os.path.exists(bz2dir.cache):
  908.                     if bz2dir.workshop_id in self.last_workshop_update:
  909.                         cache_file_time = os.path.getmtime(bz2dir.cache)
  910.                         if self.last_workshop_update[bz2dir.workshop_id] > cache_file_time:
  911.                             game_date = datetime.fromtimestamp(self.last_workshop_update[bz2dir.workshop_id]).strftime(TIME_FORMAT)
  912.                             cache_date = datetime.fromtimestamp(cache_file_time).strftime(TIME_FORMAT)
  913.                             print("Workshop item %d was updated %s, but cache last updated %s." % (bz2dir.workshop_id, game_date, cache_date))
  914.                             generate_cache = True
  915.            
  916.             if can_cache and not generate_cache:
  917.                 if os.path.exists(bz2dir.cache) and not FORCE_GENERATE_CACHE:
  918.                     if not bz2dir.cache in self.cache:
  919.                         with open(bz2dir.cache, "rb") as f:
  920.                             # Warning: The pickle module is not secure. Only unpickle data you trust!
  921.                             # cache[bz2dir.cache] = pickle.load(f)
  922.                             self.cache[bz2dir.cache] = pickle.loads(zlib.decompress(f.read()))
  923.                             use_cache = True
  924.                 else:
  925.                     generate_cache = True
  926.            
  927.             if use_cache:
  928.                 for query in tuple(queries):
  929.                     if query in self.cache[bz2dir.cache]:
  930.                         odf_file = self.cache[bz2dir.cache][query]
  931.                         if not os.path.exists(odf_file):
  932.                             print("Error - Cache for query %r points to non-existent file: %r" % (query, odf_file))
  933.                             generate_cache = True # Corrupt, file got moved or renamed, etc...
  934.                         else:
  935.                             if not query in results:
  936.                                 results[query] = odf_file
  937.                            
  938.                             queries.remove(query)
  939.                    
  940.                     if not queries and not regex_queries and not generate_cache:
  941.                         return results
  942.                
  943.                 if not suggestions and not regex_queries:
  944.                     continue # Next bz2dir, not found in this one, and not checking for suggestions or regex matches.
  945.                
  946.                 # Iterate over cache values instead of traversing directory.
  947.                 odf_iterator = iter(self.cache[bz2dir.cache].items())
  948.            
  949.             if generate_cache:
  950.                 print("Generating cache for %r..." % bz2dir.path)
  951.                 self.cache[bz2dir.cache] = dict()
  952.            
  953.             for odf_name, odf_file in odf_iterator:
  954.                 if generate_cache:
  955.                     # If multiple files with the same name exist, only 1 will regin supreme
  956.                     self.cache[bz2dir.cache][odf_name] = odf_file
  957.                
  958.                 if not use_cache and odf_name in tuple(queries):
  959.                     if not odf_name in results:
  960.                         results[odf_name] = odf_file
  961.                         queries.remove(odf_name)
  962.                    
  963.                     # All queries matched, and a cache is not being generated.
  964.                     if not queries and not regex_queries and not generate_cache:
  965.                         return results
  966.                    
  967.                     # Do not check regex_queries by skipping the rest of the
  968.                     # loop body for this iteration because we already matched this one.
  969.                     continue
  970.                
  971.                 # Check for partial matches
  972.                 elif suggestions:
  973.                     for query in queries:
  974.                         if query in odf_name:
  975.                             suggestions[query] += [odf_name]
  976.                
  977.                 # Regex (wildcard) matches
  978.                 for re_query in tuple(regex_queries):
  979.                     # if re_query.search(odf_name): # Match anywhere in name
  980.                     if re_query.match(odf_name): # Match beginning of name
  981.                         results[odf_name] = odf_file
  982.            
  983.             if generate_cache:
  984.                 with open(bz2dir.cache, "wb") as f:
  985.                     # pickle.dump(cache[bz2dir.cache], f)
  986.                     f.write(zlib.compress(pickle.dumps(self.cache[bz2dir.cache]), CACHE_COMPRESSION_LEVEL))
  987.        
  988.         if queries:
  989.             if suggestions:
  990.                 # Any matched query with suggestions for it may not have all possible suggestions in its list.
  991.                 # Only trust suggestions for unmatched queries.
  992.                 for query in queries:
  993.                     if suggestions[query]:
  994.                         print("Suggestions for %r:" % query,  ", ".join(suggestions[query]))
  995.                     else:
  996.                         print("Query %r not found in any ODF names." % query)
  997.             else:
  998.                 print("Query %r not found." % query)
  999.        
  1000.         elif regex_queries and len(results) == 0:
  1001.             print("No matches for your query.")
  1002.        
  1003.         return results
  1004.  
  1005. if __name__ == "__main__":
  1006.     if AUTO_CLEAN_PAK:
  1007.         for root, folders, files in os.walk(CACHE_DIRECTORY):
  1008.             for file in files:
  1009.                 name, ext = os.path.splitext(file)
  1010.                 if ext.casefold() == ".odf":
  1011.                     print("Deleting temp file %r..." % file)
  1012.                     os.unlink(os.path.join(root, file))
  1013.             break
  1014.             print("")
  1015.    
  1016.     global_info = read_launch_ini(BZ2R_LAUNCHINI_FILE)
  1017.     mod_info = None
  1018.     bzone_cfg = BZ2R_BZONECFG_FILE
  1019.    
  1020.     # In BZCC 'AddLocal' directives assume selected mod as the global mod, but you can override this by changing the variable below.
  1021.     if USE_ACTIVE_MOD_INSTEAD_OF_SELECTED_MOD:
  1022.         global_info.selected_workshop_id = global_info._replace(selected_workshop_id=global_info.active_workshop_id)
  1023.    
  1024.     # Command line argument '/config "path/to/bzone.cfg"' will override the mod and stock cfg.
  1025.     # This will cause the mod set by launch.ini to be ignored, which seems to be the case in BZCC.
  1026.     if len(sys.argv) > 2:
  1027.         re_config = re.compile("(?i)^\s*[+-/]config\s*$")
  1028.         re_mod = re.compile("(?i)^\s*[+-/]workshopid\s*$")
  1029.        
  1030.         is_config = re_config.match(sys.argv[1])
  1031.         is_mod = re_mod.match(sys.argv[1]) if not is_config else None
  1032.        
  1033.         if len(sys.argv) > 3 and not is_config and not is_mod:
  1034.             raise Exception("only /config 'path\\to\\bzone.cfg' or '/workshopid 1234567890' accepted as command line arguments")
  1035.        
  1036.         if is_config:
  1037.             # Using no mod results in using either the stock bzone.cfg or the "/config" bzone cfg
  1038.             global_info = global_info._replace(active_workshop_id=0, selected_workshop_id=0)
  1039.             bzone_cfg = sys.argv[2]
  1040.            
  1041.             if not os.path.isabs(bzone_cfg):
  1042.                 bzone_cfg = os.path.join(BZ2R_ROOT_DIRECTORY, bzone_cfg)
  1043.        
  1044.         elif is_mod:
  1045.             mod_id = int(sys.argv[2])
  1046.             global_info = global_info._replace(active_workshop_id=mod_id, selected_workshop_id=mod_id)
  1047.    
  1048.     SearchTool(global_info, bzone_cfg)
  1049.     # SearchTool(global_info, bzone_cfg, force_input="ivtank")
  1050.     # SearchTool(GlobalInfo(active_workshop_id=0, selected_workshop_id=0, active_addons=[], language_id=0)) # Force stock with no addons
  1051.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement