├── .gitignore ├── README.md └── Submod Updater Plugin ├── game ├── Submods │ └── Submod Updater Plugin │ │ ├── indicator_beta_warning.png │ │ ├── indicator_update_available.png │ │ ├── indicator_update_downloading.png │ │ ├── left_bar.png │ │ ├── right_bar.png │ │ └── submod_updater_plugin.rpy └── python-packages │ └── certifi │ ├── __init__.py │ ├── __main__.py │ ├── cacert.pem │ └── core.py └── lib ├── darwin-x86_64 └── lib │ └── python2.7 │ ├── _ssl.so │ └── ssl.pyo ├── linux-i686 └── lib │ └── python2.7 │ ├── _ssl.so │ └── ssl.pyo ├── linux-x86_64 └── lib │ └── python2.7 │ ├── _ssl.so │ └── ssl.pyo └── windows-i686 └── Lib ├── _ssl.pyd └── ssl.pyo /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.vscode 2 | 3 | *.rpa 4 | *.rpy[cbm] 5 | 6 | __pycache__ 7 | *.pyc 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Submod Updater Plugin 3 | 4 | A util submod that makes updating other submods easier. The util can automatically check updates for installed (and [registered](https://github.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin#usage)) submods, notify the user about those updates, and even download and install them. 5 | 6 | ## Installation: 7 | 1. Make sure you're running the latest version of MAS. 8 | 9 | 2. Download [the latest release](https://github.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/releases/latest) of the submod. 10 | 11 | 3. The packages should be installed into your `DDLC/` folder. Exactly this folder, you should have `DDLC.exe` there. 12 | 13 | ## Currently known submods that use this util: 14 | - [YouTube Music](https://github.com/Booplicate/MAS-Submods-YouTubeMusic) 15 | - [All Scrollable Menus](https://github.com/multimokia/MAS-Submod-consistent-menus) 16 | - [Auto Atmos Change](https://github.com/multimokia/MAS-Submod-Auto-Atmos-Change) 17 | - [Auto Outfit Change](https://github.com/multimokia/MAS-Submod-Auto-Outfit-Change) 18 | - [Better Loading](https://github.com/multimokia/MAS-Util-Better-Loading) (also a util submod) 19 | - [Enhanced Idle](https://github.com/multimokia/MAS-Submod-Enhanced-Idle) 20 | - [English Localization Settings](https://github.com/multimokia/MAS-Submod-English-Localization-Settings) 21 | - [Font Change](https://github.com/multimokia/MAS-Submod-Font-Change) 22 | - [Night Music](https://github.com/multimokia/MAS-Submod-Nightmusic) 23 | - [Room Selection Pack](https://github.com/tw4449/tw4449-Custom-Room-Selection-Pack-Main-Repository/blob/master/README.md) 24 | 25 | ## Usage: 26 | **This part is for the developers that want to add support for this util to their submods, the actual end users do not need to do any manipulations - just install this submod.** 27 | 28 | To use the full power of the updater, you'll need to define your submod first. After your submod is registered in the submods map, you can define an updater for it. Keep in mind that the name you pass in for the updater must be the same you used when defined your `Submod` object. Example: 29 | ```python 30 | # Register the submod 31 | init -990 python: 32 | store.mas_submod_utils.Submod( 33 | author="Your Name", 34 | name="Your Submod Name", 35 | description="A short description.", 36 | version="9.2.2", 37 | settings_pane="settings_screen_for_your_submod" 38 | ) 39 | 40 | # Register the updater 41 | init -989 python: 42 | if store.mas_submod_utils.isSubmodInstalled("Submod Updater Plugin"): 43 | store.sup_utils.SubmodUpdater( 44 | submod="Your Submod Name", 45 | user_name="Your_GitHub_Login", 46 | repository_name="Name_of_the_Repository_for_Your_Submod" 47 | ) 48 | ``` 49 | Alternatively, you can pass in the `Submod` object itself instead of its name. Whatever you feel would suit your needs! Also make sure that the name of the file you're defining your submod in is **unique**! For example you can use the name of your submod for it. 50 | 51 | There're currently 9 additional parameters you can use: 52 | - `should_notify` - toggles if we should notify the user about updates for this submod. Default `True`. 53 | - `auto_check` - toggles if we should automatically check for updates. Default `True`. 54 | - `allow_updates` - toggles if we should allow the user to update the submod. Default `True`. 55 | - `submod_dir` - the **relative** file path to the directory of your submod. If `None` (default), the updater will try to locate your submod. But if it fails and you've not specified the dir, the updater might fail to download and install updates. 56 | - `update_dir` - directory where updates will be installed in. If `None` (default), the updater will set it to the submod directory, if empty string, updates will be installed in the base directory. 57 | - `extraction_depth` - depth of the recursion for the update extractor. Defaut `1` - updater will try to go one folder inside to unpack updates. 58 | - `attachment_id` - id of the attachment with updates on GitHub. If you attach only one file, it'd be `0`, if two, depending on the order it can be either `0` or `1`. And so on. Defaults to `0`. If `None`, the updater will download **the source files**. Note that GitHub doesn't support distributing releases that way. It will be noticeably slower to download and sometimes may fail to download at all. In short: use attachments. 59 | - `tag_formatter` - if not `None`, assuming it's a function that accepts version tag from github as a string, formats it in a way, and returns a new formatted tag as a string. Exceptions are auto-handled. If `None` (default), no formatting applies on version tags. 60 | - `redirected_files` - a string or a list of strings with filenames that the updater will *try* to move to the submod dir during update. If the files don't exist or this's set to empty list/tuple, it will do nothing. If None this will be set to a tuple of 3 items: `("readme.md", "license.md", "changelog.md")`. Default `None`. This's case-insensitive. 61 | 62 | Define your updater at init level `-989`, **after** you defined the submod. 63 | The `store.mas_submod_utils.isSubmodInstalled("Submod Updater Plugin")` check is optional, but it'll allow you to support both versions of your submod: with the updater and without it. On a side note, if you don't do that check and you need to define the updater earlier for some reason, you can init your updater at `-990`. 64 | 65 | ## API: 66 | Some methods of the `SubmodUpdater` class you can work with. 67 | - `hasUpdate` is the main way to check if there's an update, note that it'll send the request only once per session. 68 | - `_checkUpdate` is an alternative to the method above. It'll rerequest data from GitHub when appropriate. Usually there's no need in that if you have `auto_check` enabled. 69 | - `_checkUpdateInThread` runs `_checkUpdate` in a thread. 70 | - `toggleNotifs`, `toggleAutoChecking`, and `toggleUpdates` allows to easily toggle values of the corresponding properties of the updater. 71 | - `isUpdating` checks whether or not we're updating this submod now. 72 | - `hasUpdated` checks whether or not we've updated this submod. 73 | - `isInBetaVersion` checks if the user's using a beta version of the submod. 74 | - `downloadUpdateInThread` allows you to download and install updates. This does not check for an update before downloading, and therefore will do nothing if you've not checked it before (or it wasn't done automatically). 75 | - `getDirectory` returns the path to the submod directory. 76 | - `getDirectoryFor` (class method) checks `getDirectory` for the given submod. 77 | - `hasUpdateFor`(class method) checks `hasUpdate` for the given submod. If submod doesn't exist, return `False` like if there's no update. 78 | - `getUpdater` (class method) returns `SubmodUpdater` object by its name. 79 | - `openURL` (static method) opens an url in the default browser. Safe to use, but you should probably let the user know before opening their browser. Can be used to open the releases page for your submod. 80 | - `openFolder` (static method) like `openURL`, but opens a folder in the default viewer. Can be used to open the game folder, or the folder with your submod. Or whatsoever. 81 | 82 | Rarely used methods. 83 | - `_downloadUpdate` is what `downloadUpdateInThread` uses to download updates. Accepts the same args/kwargs. 84 | - `_checkConflicts` - checks if it's safe to update the submod. Return list of tuples with conflicting submod, submod itself, and its max supported version. 85 | - `getUpdatersForOutdatedSubmods` (class method) just what you think - it returns `SubmodUpdater` objects for each outdated submod. 86 | - `hasOutdatedSubmods` (class method) returns boolean whether or not we have outdated submods. 87 | - `isUpdatingAny` (class method) returns boolean whether or not we're updating a submod. 88 | - `isBulkUpdating` (class method) Returns boolean whether or not we have an ongoing bulk update. 89 | - `_notify` (class method) notifies the user about all available updates at once (if the appropriate updater has the `should_notify` property set to `True`). 90 | 91 | Properties to access (only `get`) json data. These 5 can be `None` as a fallback, keep that in mind. 92 | - `latest_version` - the latest version of the submod available. 93 | - `update_name` - the name of the latest update. 94 | - `update_changelog` - the changelog for the latest update. 95 | - `update_page_url` - link to the latest release page on GitHub. 96 | - `update_package_url` - link to update attachments 97 | 98 | Properties to check status of the update (only `get`). 99 | - `is_updating` - whether we're updating this submod now or not 100 | - `has_updated` - whether we updated this submod or not 101 | 102 | Some other properties. 103 | - `id` - id/name of the updater **and** submod. 104 | - `_submod` - pointer to the `Submod` object. 105 | - `should_notify` 106 | - `auto_check` 107 | - `_submod_dir` - relative path to the submod folder. 108 | - `_json` - json data from GitHub (but better use the appropriate properties to access it). Can be `None`. Might return not what you'd expect it to due to threading. 109 | - `_last_update_check` - `datetime.datetime` of the last time we checked for an update. Can be `None`. Might return not what you'd expect it to due to threading. 110 | 111 | There are probably some more methods and properties. But it's **highly recommended to avoid using them**. Although, if you're really interested, you'll find them in the sources. 112 | 113 | ## Some important notes: 114 | The versioning of your submod and the tags you're using on GitHub must have the same format (`0.0.1`), otherwise you'll have to specify the parser via the `tag_formatter` argument. 115 | 116 | Requests to GitHub should be done with an interval of no less than 1 hour. 117 | 118 | Recommended to have submods in `/game/Submods/`. 119 | 120 | The user can install only one update at a time, to apply the changes, they'll need to restart the game. 121 | -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_beta_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_beta_warning.png -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_update_available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_update_available.png -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_update_downloading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/game/Submods/Submod Updater Plugin/indicator_update_downloading.png -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/left_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/game/Submods/Submod Updater Plugin/left_bar.png -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/right_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/game/Submods/Submod Updater Plugin/right_bar.png -------------------------------------------------------------------------------- /Submod Updater Plugin/game/Submods/Submod Updater Plugin/submod_updater_plugin.rpy: -------------------------------------------------------------------------------- 1 | 2 | # Register the submod 3 | init -990 python: 4 | store.mas_submod_utils.Submod( 5 | author="Booplicate", 6 | name="Submod Updater Plugin", 7 | description=( 8 | "A util submod that adds an in-game updater for other submods. " 9 | "Check {a=https://github.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin}{i}{u}here{/u}{/i}{/a} if you want your submod to use this." 10 | ), 11 | version="1.7", 12 | settings_pane="sup_setting_pane" 13 | ) 14 | 15 | # Register the updater 16 | init -990 python: 17 | store.sup_utils.SubmodUpdater( 18 | submod="Submod Updater Plugin", 19 | user_name="Booplicate", 20 | repository_name="MAS-Submods-SubmodUpdaterPlugin", 21 | update_dir="" 22 | ) 23 | 24 | init -999: 25 | # Add our certifs 26 | python: 27 | import os 28 | os.environ["SSL_CERT_FILE"] = renpy.config.gamedir + "/python-packages/certifi/cacert.pem" 29 | 30 | # Persistent var for our settings 31 | default persistent._sup_settings = dict() 32 | 33 | # Main code 34 | init -991 python in sup_utils: 35 | import re 36 | import os 37 | import shutil 38 | import datetime 39 | import time 40 | import urllib2 41 | import threading 42 | from store import mas_submod_utils, mas_utils, ConditionSwitch, AnimatedValue, persistent 43 | from json import loads as loadJSON 44 | from zipfile import ZipFile 45 | from subprocess import Popen as subprocOpen 46 | from webbrowser import open as openBrowser 47 | 48 | def writeLog(msg, submod=None, e=None, is_error=True): 49 | """ 50 | Writes exceptions in logs 51 | 52 | IN: 53 | msg - the message to write 54 | submod - name of the submod that triggered this 55 | (Default : None) 56 | e - the exception to log 57 | (Default: None) 58 | is_error - whether or not this logs an error 59 | (Default: True) 60 | """ 61 | message_type = " ERROR" if is_error else " REPORT" 62 | 63 | if submod is not None: 64 | formatted_submod_name = " Submod: '{0}'.".format(submod) 65 | 66 | else: 67 | formatted_submod_name = "" 68 | 69 | if e is not None: 70 | formatted_e = " Exception: {0}".format(e) 71 | if not formatted_e.endswith("."): 72 | formatted_e += "." 73 | 74 | else: 75 | formatted_e = "" 76 | 77 | _text = "[SUBMOD UPDATER PLUGIN{0}]: {1}{2}{3}\n".format(message_type, msg, formatted_submod_name, formatted_e) 78 | 79 | mas_submod_utils.writeLog(_text) 80 | 81 | # # # SUPPROGRESSBAR CLASS 82 | class SUPProgressBar(AnimatedValue): 83 | """ 84 | Subclass of AnimatedValue which is a subclass of the BarValue class 85 | Implements advanced animated bar 86 | TODO: 87 | subclass the Bar class, add a screen statement for it 88 | """ 89 | def __init__(self, value=0, range=100, delay=0.25, old_value=None): 90 | if old_value is None: 91 | old_value = value 92 | 93 | self.value = value 94 | self.range = range 95 | self.delay = delay 96 | self.old_value = old_value 97 | self.start_time = None 98 | 99 | self.adjustment = None 100 | 101 | def replaces(self, other): 102 | return 103 | 104 | def add_value(self, value): 105 | if self.value + value > self.range: 106 | value = self.range - self.value 107 | 108 | elif self.value + value < 0: 109 | value = 0 - self.value 110 | 111 | if value == 0: 112 | return 113 | 114 | self.old_value = self.adjustment._value 115 | self.value += value 116 | self.start_time = None 117 | # renpy.restart_interaction() 118 | 119 | def reset(self): 120 | self.value = 0 121 | self.old_value = 0 122 | self.start_time = None 123 | 124 | # # # END OF THE SUPPROGRESSBAR CLASS 125 | 126 | class SubmodUpdaterError(Exception): 127 | """ 128 | Custom exception for Submod Updater Plugin 129 | """ 130 | def __init__(self, msg, submod=None, e=None): 131 | self.msg = msg 132 | self.e = e 133 | writeLog(self.msg, submod=submod, e=e) 134 | 135 | def __str__(self): 136 | return self.msg 137 | 138 | # # # SUBMODUPDATER CLASS 139 | class SubmodUpdater(object): 140 | """ 141 | Submod Updater 142 | 143 | PROPERTIES: 144 | public: 145 | id - id/name of the updater and the submod 146 | submod - pointer to the submod object 147 | should_notify - whether or not we notify the user about updates 148 | auto_check - whether or not we automically check for updates 149 | allow_updates - whether or not allow the user to install updtes for this submod 150 | submod_dir - the relative file path to the submod directory 151 | update_dir - directory where updates will be installed to 152 | extraction_depth - depth of the recursion for the update extractor 153 | json - json data about submod from GitHub 154 | last_update_check - datetime.datetime the last time we checked for update 155 | update_available - whether or not we have an update available 156 | update_exception - the exception that occurred during updating with this updater (if any) 157 | 158 | private: 159 | user_name - the author's user name on GitHub 160 | repository_name - the submod's GitHub repository name 161 | attachment_id - id of the attachment on GitHub (usually 0) 162 | updating - flag whether or not we're currently updating the submod 163 | updated - flag whether was the submod updated 164 | """ 165 | # url parts 166 | URL_API = "https://api.github.com/" 167 | URL_REPOS = "repos/" 168 | URL_LATEST_RELEASE = "/releases/latest" 169 | 170 | # DO NOT change this 171 | HEADERS = { 172 | "User-Agent": "Just Monika! (Monika After Story v{0})".format(renpy.config.version), 173 | "Accept-Language": "en-US", 174 | "Content-Language": "en-US", 175 | "Accept-Charset": "utf8" 176 | } 177 | 178 | # the interval between requests 179 | REQUESTS_INTERVAL = datetime.timedelta(hours=1) 180 | 181 | # number of attempts to requests content size 182 | # before aborting the update 183 | REQUEST_ATTEMPS_LIMIT = 10 184 | # time in seconds before we will give up trying to connect 185 | TIMEOUT = 15 186 | 187 | # IO file chunks 188 | REQUEST_CHUNK = 5242880 189 | WRITING_CHUNK = 262144 190 | 191 | # lock for threading stuff 192 | updateDownloadLock = threading.Lock() 193 | 194 | # Renpy can suck this bar - I have working progress bar for windows 195 | single_progress_bar = SUPProgressBar() 196 | bulk_progress_bar = SUPProgressBar() 197 | 198 | # normalized paths 199 | BASE_DIRECTORY = renpy.config.basedir.replace("\\", "/") 200 | GAME_DIRECTORY = renpy.config.gamedir.replace("\\", "/") 201 | # img paths constants 202 | INDICATOR_UPDATE_DOWNLOADING = "/indicator_update_downloading.png" 203 | INDICATOR_UPDATE_AVAILABLE = "/indicator_update_available.png" 204 | INDICATOR_BETA_WARNING = "/indicator_beta_warning.png" 205 | LEFT_BAR = "/left_bar.png" 206 | RIGHT_BAR = "/right_bar.png" 207 | 208 | # FOLDER_NAME_PATTERN = re.compile(r"[^a-zA-Z0-9_]") 209 | 210 | # NOTE: the order IS important 211 | MD_TAGS_PATTERN = re.compile( 212 | r""" 213 | (?\s+?[\S\s]+?[\n] # Pattern for quotes 230 | """, 231 | flags=re.IGNORECASE | re.VERBOSE 232 | ) 233 | MD_LINK_TAG_PATTERN = re.compile( 234 | r"(?\s+?)([\S\s]+?)([\n])", 267 | flags=re.IGNORECASE | re.UNICODE | re.MULTILINE 268 | ) 269 | HEADING_SIZE_MAP = { 270 | 1: "+6", 271 | 2: "+4", 272 | 3: "+2", 273 | 4: "+0", 274 | 5: "-2", 275 | 6: "-4" 276 | } 277 | 278 | # html codes 279 | OK_CODE = 200 280 | RATE_LIMIT_CODE = 403 281 | 282 | # a map of submods which we will check for updates 283 | registered_updaters = dict() 284 | 285 | # a list of updaters that were queued for updating 286 | queued_updaters = list() 287 | # a list of updaters resently finished updating 288 | finished_updaters = list() 289 | 290 | # Flag whether or not we're checking updates for submods (using _checkUpdates) 291 | is_checking_updates = False 292 | 293 | def __init__( 294 | self, 295 | submod, 296 | user_name, 297 | repository_name, 298 | should_notify=True, 299 | auto_check=True, 300 | allow_updates=True, 301 | submod_dir=None, 302 | update_dir=None, 303 | extraction_depth=1, 304 | attachment_id=0, 305 | tag_formatter=None, 306 | redirected_files=None 307 | ): 308 | """ 309 | Constructor 310 | 311 | IN: 312 | submod - either the name of the submod 313 | or the Submod object itself 314 | 315 | user_name - the author's user name (login) on GitHub 316 | 317 | repository_name - the submod's GitHub repository name 318 | 319 | should_notify - whether or not we notify the user about updates 320 | This includes both: showing notifies and showing update information on the submod screen 321 | (Default: True) 322 | 323 | auto_check - whether or not we automically check for updates (this's not auto updating) 324 | (Default: True) 325 | 326 | allow_updates - whether or not allow the user to install updtes for this submod 327 | If True there wll be a button to prompt for the update in the submod screen 328 | If False you'll need to implement another way to update (or don't if you only want to notify about updates) 329 | (Default: True) 330 | 331 | submod_dir - relative file path to the directory of the submod (relative to config.gamedir) 332 | e.g. '/Submods/Your submod folder' 333 | NOTE: if None, the updater will try to find the path itself 334 | NOTE: if None when we're trying to update the submod and no update_dir specified, the update will be aborted 335 | (Default: None) 336 | 337 | update_dir - directory where updates will be installed in 338 | NOTE: if None, updates will be installed in the submod directory if one was specified 339 | NOTE: if empty string, updates will be installed right in the base directory (the folder with DDLC.exe) 340 | (Default: None) 341 | 342 | extraction_depth - extraction depth, depth of the recursion for the update extractor 343 | See the __extract_files method for more info 344 | (Default: 1) 345 | 346 | attachment_id - id of the attachment on GitHub 347 | (only if you have more than one attachment in releases, not counting source code) 348 | NOTE: if set to None, the updater will download the source files 349 | while it is supported by this util, it's not supported well by GitHub 350 | better use an attachment 351 | (Default: 0) 352 | 353 | tag_formatter = if not None, assuming it's a function that accepts version tag from github as a string, formats it in a way, 354 | and returns a new formatted tag as a string. Exceptions are auto-handled. If None, no formatting applies on version tags 355 | (Default: None) 356 | 357 | redirected_files - a string or a list of strings with filenames that the updater will TRY to move to the submod dir during update. 358 | If the files don't exist or this's set to empty list/tuple, it will do nothing. If None this will be set to ("readme.md", "license.md", "changelog.md") 359 | NOTE: Case-insensitive 360 | NOTE: this's intended to work only on files, NOT folders 361 | NOTE: this requires submod_dir to have a non-None value during update 362 | (Default: None) 363 | """ 364 | if isinstance(submod, basestring): 365 | submod_name = submod 366 | submod_obj = mas_submod_utils.submod_map.get(submod, None) 367 | 368 | elif isinstance(submod, mas_submod_utils.Submod): 369 | submod_name = submod.name 370 | submod_obj = submod 371 | 372 | # otherwise raise because this's critical 373 | else: 374 | raise SubmodUpdaterError( 375 | "\"{0}\" is not a string, nor a Submod object.".format(submod) 376 | ) 377 | 378 | # ignore if the submod doesn't exist 379 | if submod_obj is None: 380 | SubmodUpdaterError("Submod '{0}' had not been registered in MAS Submods Framwork. Ignoring.".format(submod_name)) 381 | return 382 | 383 | # ignore dupes 384 | if submod_name in self.registered_updaters: 385 | SubmodUpdaterError("Submod '{0}' had already been registered in Submod Updater Plugin. Ignoring.".format(submod_name)) 386 | return 387 | 388 | if submod_name not in persistent._sup_settings: 389 | persistent._sup_settings[submod_name] = { 390 | "should_notify": should_notify, 391 | "auto_check": auto_check, 392 | "allow_updates": allow_updates 393 | } 394 | 395 | self.id = submod_name 396 | self._submod = submod_obj 397 | self.__user_name = user_name 398 | self.__repository_name = repository_name 399 | 400 | # Try to load settings from persistent 401 | self.should_notify = persistent._sup_settings[self.id].get("should_notify", should_notify) 402 | self.auto_check = persistent._sup_settings[self.id].get("auto_check", auto_check) 403 | self.allow_updates = persistent._sup_settings[self.id].get("allow_updates", allow_updates) 404 | 405 | self._submod_dir = submod_dir.replace("\\", "/") if submod_dir is not None else self.__getCurrentFilePath() 406 | self._update_dir = update_dir 407 | self._extraction_depth = extraction_depth 408 | self.__attachment_id = attachment_id 409 | self.__tag_formatter = tag_formatter 410 | 411 | if redirected_files is None: 412 | redirected_files = ("readme.md", "license.md", "changelog.md") 413 | 414 | else: 415 | if isinstance(redirected_files, basestring): 416 | redirected_files = [redirected_files] 417 | 418 | elif isinstance(redirected_files, tuple): 419 | redirected_files = list(redirected_files) 420 | 421 | for id, filename in enumerate(redirected_files): 422 | redirected_files[id] = filename.lower() 423 | 424 | redirected_files = tuple(redirected_files) 425 | 426 | self.__redirected_files = redirected_files 427 | 428 | self.__json_request = self.__buildJSONRequest() 429 | self._json = None 430 | self._last_update_check = None 431 | self._update_available = None 432 | self.__updated = False 433 | self.__updating = False 434 | self.update_exception = None 435 | 436 | self.__updateCheckLock = threading.Lock() 437 | 438 | self.registered_updaters[self.id] = self 439 | 440 | @property 441 | def latest_version(self): 442 | """ 443 | Returns the latest version number (tag) 444 | for this submod 445 | NOTE: can return None if there's no update, 446 | or we got an exception somewhere 447 | """ 448 | if self._json: 449 | return self._json["latest_version"] 450 | return None 451 | 452 | @property 453 | def update_name(self): 454 | """ 455 | Returns the name of the latest update 456 | for this submod 457 | NOTE: can return None if there's no update, 458 | or we got an exception somewhere 459 | """ 460 | if self._json: 461 | return self._json["update_name"] 462 | return None 463 | 464 | @property 465 | def update_changelog(self): 466 | """ 467 | Returns the changelog for the latest update 468 | for this submod 469 | NOTE: can return None if there's no update, 470 | or we got an exception somewhere 471 | """ 472 | if self._json: 473 | return self._json["update_changelog"] 474 | return None 475 | 476 | @property 477 | def update_page_url(self): 478 | """ 479 | Returns a link to update page 480 | NOTE: can return None if there's no update, 481 | or we got an exception somewhere 482 | """ 483 | if self._json: 484 | return self._json["update_page_url"] 485 | return None 486 | 487 | @property 488 | def update_package_url(self): 489 | """ 490 | Returns a link to update files 491 | NOTE: can return None if there's no update, 492 | or we got an exception somewhere 493 | """ 494 | if self._json: 495 | return self._json["update_package_url"] 496 | return None 497 | 498 | def toggleNotifs(self): 499 | """ 500 | Toggles the should_notify property 501 | """ 502 | self.should_notify = not self.should_notify 503 | persistent._sup_settings[self.id]["should_notify"] = self.should_notify 504 | 505 | def toggleAutoChecking(self): 506 | """ 507 | Toggles the auto_check property 508 | """ 509 | self.auto_check = not self.auto_check 510 | persistent._sup_settings[self.id]["auto_check"] = self.auto_check 511 | 512 | def toggleUpdates(self): 513 | """ 514 | Toggles the allow_updates property 515 | """ 516 | self.allow_updates = not self.allow_updates 517 | persistent._sup_settings[self.id]["allow_updates"] = self.allow_updates 518 | 519 | def isUpdating(self): 520 | """ 521 | Returns a bool whether we're updating this submod now 522 | """ 523 | return self.__updating 524 | 525 | def hasUpdated(self): 526 | """ 527 | Returns a bool whether we updated this submod 528 | """ 529 | return self.__updated 530 | 531 | def __getCurrentFilePath(self): 532 | """ 533 | Return a relative filepath to the submod 534 | 535 | OUT: 536 | string with the filepath, 537 | or None if we weren't able to get it 538 | """ 539 | # get the filepath 540 | path = renpy.get_filename_line()[0] 541 | # cut the game folder from the path (but only if we have more than 1 folder) 542 | if ( 543 | path.count("/") > 1 544 | and path.startswith("game/") 545 | ): 546 | path = path.partition("game/")[-1] 547 | 548 | test_path = os.path.join(self.GAME_DIRECTORY, path.lstrip("/")).replace("\\", "/") 549 | og_path = test_path.rpartition("/")[0] 550 | reported_wrong_path = False 551 | # Since renpy is junk, we need to make sure we got an existing fp from it 552 | while not os.path.exists(test_path): 553 | # Log it 554 | if not reported_wrong_path: 555 | reported_wrong_path = True 556 | SubmodUpdaterError("Ren'Py returned the wrong filepath: '{0}' for '{1}'.".format(og_path, self.id)) 557 | # Remove one folder and try to check again 558 | path = path.partition("/")[-1] 559 | # If there's nothing left, then we have to return None 560 | if not path: 561 | SubmodUpdaterError("Couldn't automatically locate '{0}'. Some features may not work.".format(self.id)) 562 | return None 563 | # Otherwise try the new path 564 | test_path = os.path.join(self.GAME_DIRECTORY, path.lstrip("/")).replace("\\", "/") 565 | writeLog("Trying to find the submod in: '{0}'.".format(test_path.rpartition("/")[0]), is_error=False) 566 | 567 | # lastly cut the filename to get just the folder 568 | path = path.rpartition("/")[0] 569 | # Log that we found it, if needed 570 | if reported_wrong_path: 571 | writeLog("Found the submod.", is_error=False) 572 | 573 | return path 574 | 575 | def __buildJSONRequest(self): 576 | """ 577 | Builds a request object to use later 578 | for requesting json data from GitHub API 579 | 580 | OUT: 581 | Request object 582 | """ 583 | return urllib2.Request( 584 | url="{}{}{}{}{}{}".format( 585 | self.URL_API, 586 | self.URL_REPOS, 587 | self.__user_name, 588 | "/", 589 | self.__repository_name, 590 | self.URL_LATEST_RELEASE 591 | ), 592 | headers=self.HEADERS 593 | ) 594 | 595 | def __requestJSON(self): 596 | """ 597 | Requests JSON data for latest release 598 | 599 | OUT: 600 | json data as a dict 601 | """ 602 | response = None 603 | 604 | try: 605 | response = urllib2.urlopen( 606 | self.__json_request, 607 | timeout=self.TIMEOUT 608 | ) 609 | 610 | except urllib2.HTTPError as e: 611 | if e.code == self.RATE_LIMIT_CODE: 612 | SubmodUpdaterError("Too many requests. Try again later.", submod=self.id, e=e) 613 | 614 | else: 615 | SubmodUpdaterError("Failed to request JSON data.", submod=self.id, e=e) 616 | 617 | return None 618 | 619 | except Exception as e: 620 | SubmodUpdaterError("Failed to request JSON data.", submod=self.id, e=e) 621 | return None 622 | 623 | if ( 624 | response is not None 625 | and response.getcode() == self.OK_CODE 626 | ): 627 | raw_data = response.read() 628 | 629 | try: 630 | json_data = loadJSON(raw_data) 631 | 632 | except Exception as e: 633 | SubmodUpdaterError("Failed to load JSON data.", submod=self.id, e=e) 634 | return None 635 | 636 | return json_data 637 | 638 | return None 639 | 640 | def __parseJSON(self, json_data): 641 | """ 642 | Parses JSON data to get the bits we need 643 | 644 | IN: 645 | json_data - the data to parse 646 | 647 | OUT: 648 | dict with parsed data: 649 | latest_version (Fallback: current submod ver) 650 | update_name (Fallback: 'Unknown') 651 | update_changelog (Fallback: an empty str) 652 | update_page_url (Fallback: None) 653 | update_package_url (Fallback: None) 654 | or None if was incorrect input 655 | """ 656 | # sanity check 657 | if not json_data: 658 | return None 659 | 660 | latest_version = json_data.get("tag_name", None) 661 | if latest_version is None: 662 | SubmodUpdaterError("Failed to parse JSON data: missing the 'tag_name' field.", submod=self.id) 663 | latest_version = self._submod.version 664 | 665 | elif self.__tag_formatter is not None: 666 | try: 667 | latest_version = self.__tag_formatter(latest_version) 668 | 669 | except Exception as e: 670 | SubmodUpdaterError("Failed to format version tag.", submod=self.id, e=e) 671 | latest_version = self._submod.version 672 | 673 | update_name = json_data.get("name", None) 674 | if update_name is None: 675 | SubmodUpdaterError("Failed to parse JSON data: missing the 'name' field.", submod=self.id) 676 | update_name = "Unknown" 677 | 678 | else: 679 | update_name = update_name.replace("[", "[[").replace("{", "{{") 680 | 681 | update_changelog = json_data.get("body", None) 682 | if update_changelog is None: 683 | SubmodUpdaterError("Failed to parse JSON data: missing the 'body' field.", submod=self.id) 684 | update_changelog = "" 685 | 686 | else: 687 | update_changelog = update_changelog.replace("{", "{{") 688 | update_changelog = SubmodUpdater.formatMDtoRenPy(update_changelog) 689 | update_changelog = update_changelog.replace("[", "[[") 690 | 691 | update_page_url = json_data.get("html_url", None) 692 | if update_page_url is None: 693 | SubmodUpdaterError("Failed to parse JSON data: missing the 'html_url' field.", submod=self.id) 694 | 695 | update_package_url = None 696 | if self.__attachment_id is not None: 697 | assets = json_data.get("assets", None) 698 | 699 | if assets is not None: 700 | try: 701 | attachment = assets[self.__attachment_id] 702 | 703 | except IndexError: 704 | SubmodUpdaterError("Failed to parse JSON data: attachment with id '{0}' doesn't exist.".format(self.__attachment_id), submod=self.id) 705 | 706 | else: 707 | update_package_url = attachment.get("browser_download_url", None) 708 | 709 | if update_package_url is None: 710 | SubmodUpdaterError("Failed to parse JSON data: GitHub didn't provide the download link for the attachment.", submod=self.id) 711 | 712 | else: 713 | SubmodUpdaterError("Failed to parse JSON data: missing the 'assets' field.", submod=self.id) 714 | 715 | else: 716 | update_package_url = json_data.get("zipball_url", None) 717 | 718 | if update_package_url is None: 719 | SubmodUpdaterError("Failed to parse JSON data: missing the 'zipball_url' field.", submod=self.id) 720 | 721 | return { 722 | "latest_version": latest_version, 723 | "update_name": update_name, 724 | "update_changelog": update_changelog, 725 | "update_page_url": update_page_url, 726 | "update_package_url": update_package_url 727 | } 728 | 729 | def getDirectory(self, absolute=True): 730 | """ 731 | Returns the submod directory 732 | 733 | IN: 734 | absolute - True to return the absolute path, 735 | False to return the relative one 736 | 737 | OUT: 738 | string with the path, or None if we couldn't find it 739 | """ 740 | path = None 741 | if self._submod_dir is not None: 742 | if absolute: 743 | path = os.path.join( 744 | self.GAME_DIRECTORY, 745 | self._submod_dir.lstrip("/")# strip just in case, because join doesn't work if there's `/` at the beggining of the path 746 | ).replace("\\", "/") 747 | 748 | else: 749 | path = self._submod_dir 750 | 751 | return path 752 | 753 | def __versionToList(self, version=None): 754 | """ 755 | Converts the given version to a list of int's 756 | 757 | IN: 758 | version - string with submod version in the semantic versioning format, 759 | if None, this'll use this updater submod version 760 | (Default: None) 761 | 762 | OUT: 763 | list with int representing the version 764 | """ 765 | if version is None: 766 | version = self._submod.version 767 | 768 | return map(int, version.split(".")) 769 | 770 | def _compareVersion(self, new_version): 771 | """ 772 | Compare version of this updater submod with the given version 773 | 774 | IN: 775 | new_version - version to compare to 776 | 777 | OUT: 778 | int: 779 | 1 if the current version is greater than the comparitive version 780 | 0 if the current version is the same as the comparitive version 781 | -1 if the current version number is less than the comparitive version 782 | """ 783 | curr_version = self.__versionToList() 784 | if isinstance(new_version, basestring): 785 | new_version = self.__versionToList(new_version) 786 | 787 | return mas_utils.compareVersionLists(curr_version, new_version) 788 | 789 | def isInBetaVersion(self): 790 | """ 791 | Checks if this updater submod version is greater than the version in the latest release (aka BETA). 792 | This may happen if the user installed the source version that hasn't been tested/released yet 793 | NOTE: doesn't check for updates itself, for that use _checkUpdate 794 | NOTE: for general update checking use hasUpdate 795 | 796 | OUT: 797 | boolean: 798 | True if beta version, False otherwise 799 | """ 800 | if ( 801 | self._json 802 | and not self.__updating 803 | and not self.__updated 804 | ): 805 | return self._compareVersion(self._json["latest_version"]) > 0 806 | 807 | return False 808 | 809 | def _checkUpdate(self, bypass=False): 810 | """ 811 | Checks for updates for this submod 812 | This will also update the json property with a new json if available 813 | 814 | IN: 815 | bypass - whether or not we should try to bypass the limit 816 | NOTE: DANGEROUS 817 | (Default: False) 818 | 819 | OUT: 820 | True if we have an update, False otherwise 821 | """ 822 | with self.__updateCheckLock: 823 | _now = datetime.datetime.now() 824 | 825 | # TT protection 826 | if ( 827 | self._last_update_check is not None 828 | and _now < self._last_update_check 829 | ): 830 | self._last_update_check = _now 831 | 832 | # if we checked for update recently, we'll skip this check 833 | if ( 834 | bypass 835 | or self._last_update_check is None 836 | or ( 837 | self._last_update_check is not None 838 | and _now - self._last_update_check > self.REQUESTS_INTERVAL 839 | ) 840 | ): 841 | self._json = self.__parseJSON(self.__requestJSON()) 842 | self._last_update_check = datetime.datetime.now() 843 | 844 | # sanity check 845 | if ( 846 | self._json 847 | and self._compareVersion(self._json["latest_version"]) < 0 848 | ): 849 | self._update_available = True 850 | 851 | else: 852 | self._update_available = False 853 | 854 | return self._update_available 855 | 856 | def _checkUpdateInThread(bypass=False): 857 | """ 858 | Runs _checkUpdate in a thread 859 | 860 | IN: 861 | bypass - whether or not we should try to bypass the limit 862 | NOTE: DANGEROUS 863 | (Default: False) 864 | """ 865 | update_checker = threading.Thread( 866 | target=self._checkUpdate, 867 | kwargs=dict(bypass=bypass) 868 | ) 869 | 870 | update_checker.start() 871 | 872 | def hasUpdate(self, should_check=True, ignore_if_updated=True, ignore_if_updating=True): 873 | """ 874 | Geneal way to check if there's an update for this submod 875 | 876 | IN: 877 | should_check - whether we send a request to GitHub (True), 878 | or return False if we don't have the update data (False) 879 | (Default: True) 880 | 881 | ignore_if_updated - whether or not skip this check if the submod was updated 882 | (Default: True) 883 | 884 | ignore_if_updating - whether or not skip this check if the submod is being updated now 885 | (Default: True) 886 | 887 | OUT: 888 | True if there's an update, False otherwise 889 | """ 890 | if ( 891 | ( 892 | self.__updating 893 | and ignore_if_updating 894 | ) 895 | or ( 896 | self.__updated 897 | and ignore_if_updated 898 | ) 899 | ): 900 | return False 901 | 902 | if self._update_available is not None: 903 | return self._update_available 904 | 905 | if should_check: 906 | return self._checkUpdate() 907 | 908 | else: 909 | return False 910 | 911 | def __check_filepath(self, path): 912 | """ 913 | Checks the given path and makes folders if needed 914 | NOTE: This belongs to _downloadUpdate, it's not an inner method only for easy overrides 915 | 916 | IN: 917 | path - path to check 918 | """ 919 | try: 920 | if not os.path.isdir(path): 921 | os.makedirs(path) 922 | 923 | return True 924 | 925 | except Exception as e: 926 | self.update_exception = SubmodUpdaterError("Failed to check/create folders.", submod=self.id, e=e) 927 | return False 928 | 929 | def __extract_files(self, srs, dst, depth=0): 930 | """ 931 | A helper method to extract files from one folder into another 932 | using the given depth to extract items from 933 | NOTE: This belongs to _downloadUpdate, it's not an inner method only for easy overrides 934 | 935 | For example: 936 | File in 'old_folder/sub_folder/file' 937 | - with the depth 0 would be extracted as 'new_folder/sub_folder/file' 938 | - with the depth 1 (and more in this case) would be extracted as 'new_folder/file' 939 | 940 | if the extracting object is a file, it would be moved to the destination 941 | regardless of the depth used 942 | 943 | NOTE: dst can't end up inside srs 944 | 945 | IN: 946 | srs - the folder which we'll extract files from 947 | dst - the destination 948 | depth - depth of the recursion 949 | 950 | OUT: 951 | list of exceptions (it can be empty) 952 | """ 953 | # List of exceptions we get during this call 954 | exceptions = [] 955 | # Set the next depth 956 | new_depth = depth - 1 957 | # Save it for future uses 958 | og_dst = dst 959 | 960 | if os.path.isdir(srs): 961 | for item in os.listdir(srs): 962 | # Set the new source path 963 | new_srs = os.path.join(srs, item).replace("\\", "/") 964 | # Check if we want to redirect this file 965 | if ( 966 | self.__redirected_files is not None 967 | and self._submod_dir is not None 968 | and os.path.isfile(new_srs) 969 | and "python-packages" not in new_srs 970 | and item.lower() in self.__redirected_files 971 | ): 972 | dst = os.path.join( 973 | self.GAME_DIRECTORY, 974 | self._submod_dir.lstrip("/") 975 | ).replace("\\", "/") 976 | 977 | new_dst = os.path.join( 978 | self.GAME_DIRECTORY, 979 | self._submod_dir.lstrip("/"), 980 | item 981 | ).replace("\\", "/") 982 | 983 | else: 984 | dst = og_dst 985 | new_dst = os.path.join(dst, item).replace("\\", "/") 986 | 987 | # Should we go deeper? 988 | if ( 989 | depth > 0 990 | and os.path.isdir(new_srs) 991 | ): 992 | rv = self.__extract_files(new_srs, dst, new_depth) 993 | exceptions += rv 994 | 995 | # Or just extract as is 996 | else: 997 | # The dir already exists, have to use recursion 998 | if os.path.isdir(new_dst): 999 | rv = self.__extract_files(new_srs, new_dst, 0) 1000 | exceptions += rv 1001 | 1002 | # The file exists, have to delete it first 1003 | elif os.path.isfile(new_dst): 1004 | try: 1005 | os.remove(new_dst) 1006 | shutil.move(new_srs, dst) 1007 | 1008 | except Exception as e: 1009 | exceptions.append(str(e)) 1010 | 1011 | # Simply move it 1012 | else: 1013 | try: 1014 | shutil.move(new_srs, dst) 1015 | 1016 | except Exception as e: 1017 | exceptions.append(str(e)) 1018 | 1019 | return exceptions 1020 | 1021 | def __delete_update_files(self, path): 1022 | """ 1023 | Tries to delete files in path 1024 | NOTE: This belongs to _downloadUpdate, it's not an inner method only for easy overrides 1025 | 1026 | IN: 1027 | path - path to files 1028 | """ 1029 | try: 1030 | if os.path.isdir(path): 1031 | shutil.rmtree(path, ignore_errors=True) 1032 | 1033 | elif os.path.isfile(path): 1034 | os.remove(path) 1035 | 1036 | except Exception as e: 1037 | self.update_exception = SubmodUpdaterError("Failed to delete temp files: {0}".format(path), submod=self.id, e=e) 1038 | 1039 | def __do_bulk_progress_bar_logic(self): 1040 | """ 1041 | Does logic for updating the progress bar for bulk downloads 1042 | NOTE: This belongs to _downloadUpdate, it's not an inner method only for easy overrides 1043 | """ 1044 | if self in self.queued_updaters: 1045 | self.finished_updaters.append(self) 1046 | bar_value = 1.0 / float(len(self.queued_updaters)) * 100 1047 | self.bulk_progress_bar.add_value(bar_value) 1048 | 1049 | def _downloadUpdate(self, update_dir=None, extraction_depth=1): 1050 | """ 1051 | Download the latest update for the submod 1052 | NOTE: does not check for update 1053 | NOTE: won't do anything if we're still updating another submod 1054 | NOTE: For internal uses the arguments for this method the updater will take from the properties 1055 | 1056 | IN: 1057 | update_dir - the directory the updater will extract this update into 1058 | NOTE: if None, the update will be installed in the submod directory 1059 | NOTE: if empty string, the update will be installed right in the base directory (the folder with DDLC.exe) 1060 | (Default: None) 1061 | 1062 | extraction_depth - extraction depth, check the __extract_files method for explanation 1063 | (Default: 1) 1064 | 1065 | OUT: 1066 | True if we successfully downloaded and installed the update, 1067 | False otherwise 1068 | """ 1069 | # only allow one update at a time 1070 | with self.updateDownloadLock: 1071 | # Reset the previous state 1072 | self.single_progress_bar.reset() 1073 | # Reset the previous exception 1074 | self.update_exception = None 1075 | # Mark as updating 1076 | self.__updating = True 1077 | 1078 | # # # Sanity checks 1079 | if ( 1080 | self.__updated 1081 | or not self._update_available 1082 | ): 1083 | self.update_exception = SubmodUpdaterError("Aborting update. No new updates found.", submod=self.id) 1084 | 1085 | self.__updating = False 1086 | 1087 | self.__do_bulk_progress_bar_logic() 1088 | 1089 | return False 1090 | 1091 | if ( 1092 | self._json is None 1093 | or self._json["update_package_url"] is None 1094 | ): 1095 | self.update_exception = SubmodUpdaterError("Missing update JSON data, or update url is incorrect.", submod=self.id) 1096 | 1097 | self.__updating = False 1098 | 1099 | self.__do_bulk_progress_bar_logic() 1100 | 1101 | return False 1102 | 1103 | # # # Set the paths 1104 | 1105 | # You decided to install the update into the base dir 1106 | if update_dir == "": 1107 | update_dir = self.BASE_DIRECTORY 1108 | 1109 | else: 1110 | # If we don't know the folder yet, try to get the one where we have the submod in 1111 | if update_dir is None: 1112 | if not self._submod_dir: 1113 | self.update_exception = SubmodUpdaterError("Couldn't locate the submod directory.", submod=self.id) 1114 | 1115 | self.__updating = False 1116 | 1117 | self.__do_bulk_progress_bar_logic() 1118 | 1119 | return False 1120 | 1121 | update_dir = self._submod_dir 1122 | 1123 | update_dir = update_dir.replace("\\", "/") 1124 | 1125 | # Make it an absolute path if needed 1126 | if ( 1127 | ( 1128 | update_dir.startswith("game/") 1129 | or update_dir.startswith("/game/") 1130 | ) 1131 | and self.BASE_DIRECTORY not in update_dir 1132 | ): 1133 | update_dir = os.path.join( 1134 | self.BASE_DIRECTORY, 1135 | update_dir.lstrip("/")# strip just in case, because join doesn't work if there's `/` at the beggining of the path 1136 | ).replace("\\", "/") 1137 | 1138 | elif self.GAME_DIRECTORY not in update_dir: 1139 | update_dir = os.path.join( 1140 | self.GAME_DIRECTORY, 1141 | update_dir.lstrip("/")# strip just in case 1142 | ).replace("\\", "/") 1143 | 1144 | temp_folder_name = "temp_{0}".format(int(time.time())) 1145 | 1146 | temp_files_dir = os.path.join( 1147 | self.GAME_DIRECTORY, 1148 | self._submod_dir.lstrip("/"), 1149 | temp_folder_name 1150 | ).replace("\\", "/") 1151 | 1152 | temp_file_name = "update.zip" 1153 | 1154 | temp_file = os.path.join(temp_files_dir, temp_file_name).replace("\\", "/") 1155 | 1156 | # # # Check the paths 1157 | path_1 = self.__check_filepath(temp_files_dir) 1158 | path_2 = self.__check_filepath(update_dir) 1159 | 1160 | # abort if we weren't able to create the folders 1161 | if not path_1 or not path_2: 1162 | self.update_exception = SubmodUpdaterError("Failed to create temp folders for update.", submod=self.id) 1163 | 1164 | self.__delete_update_files(temp_files_dir) 1165 | 1166 | self.__updating = False 1167 | 1168 | self.__do_bulk_progress_bar_logic() 1169 | 1170 | return False 1171 | 1172 | update_url = self._json["update_package_url"] 1173 | 1174 | req_size_headers = dict(self.HEADERS) 1175 | req_size_headers.update({"Accept-Encoding": "identity"}) 1176 | 1177 | req_size_request = urllib2.Request( 1178 | url=update_url, 1179 | headers=req_size_headers 1180 | ) 1181 | 1182 | request_attempts_left = self.REQUEST_ATTEMPS_LIMIT# I literally hate GitHub for not allowing me to get Content-Length 1183 | update_size = None 1184 | 1185 | # # # Get update size 1186 | try: 1187 | while ( 1188 | update_size is None 1189 | and request_attempts_left > 0 1190 | ): 1191 | cont_length_list = urllib2.urlopen(req_size_request, timeout=self.TIMEOUT).info().getheaders("Content-Length") 1192 | 1193 | if len(cont_length_list) > 0: 1194 | update_size = int(cont_length_list[0]) 1195 | 1196 | else: 1197 | request_attempts_left -= 1 1198 | # Not sure why, but we do need to make a new request object 1199 | req_size_request = urllib2.Request( 1200 | url=update_url, 1201 | headers=req_size_headers 1202 | ) 1203 | if request_attempts_left > 0: 1204 | time.sleep(1.5) 1205 | 1206 | # I give 10 attempts, if we fail, we fail. Blame GitHub. 1207 | if update_size is None: 1208 | self.update_exception = SubmodUpdaterError("Github failed to return update size. Try again later.", submod=self.id) 1209 | 1210 | self.__delete_update_files(temp_files_dir) 1211 | 1212 | self.__updating = False 1213 | 1214 | self.__do_bulk_progress_bar_logic() 1215 | 1216 | return False 1217 | 1218 | except Exception as e: 1219 | self.update_exception = SubmodUpdaterError("Failed to request update size.", submod=self.id, e=e) 1220 | 1221 | self.__delete_update_files(temp_files_dir) 1222 | 1223 | self.__updating = False 1224 | 1225 | self.__do_bulk_progress_bar_logic() 1226 | 1227 | return False 1228 | 1229 | # # # Prep for updating 1230 | bytes_downloaded = 0 1231 | bottom_bracket = 0 1232 | top_bracket = min(self.REQUEST_CHUNK, update_size) 1233 | downloading_headers = dict(self.HEADERS) 1234 | downloading_headers.update( 1235 | { 1236 | "Range": "bytes={0}-{1}".format( 1237 | bottom_bracket, 1238 | top_bracket 1239 | ) 1240 | } 1241 | ) 1242 | update_request = urllib2.Request( 1243 | url=update_url, 1244 | headers=downloading_headers 1245 | ) 1246 | 1247 | # # # Start updating 1248 | try: 1249 | with open(temp_file, "wb") as update_file: 1250 | response = urllib2.urlopen(update_request, timeout=self.TIMEOUT) 1251 | while True: 1252 | cache_buffer = response.read(self.WRITING_CHUNK) 1253 | 1254 | if not cache_buffer: 1255 | break 1256 | 1257 | bytes_downloaded += len(cache_buffer) 1258 | update_file.write(cache_buffer) 1259 | 1260 | bar_value = float(len(cache_buffer)) / float(update_size) * 100 1261 | self.single_progress_bar.add_value(bar_value) 1262 | time.sleep(0.25) 1263 | 1264 | if ( 1265 | bytes_downloaded == top_bracket 1266 | and bytes_downloaded != update_size 1267 | ): 1268 | bottom_bracket = top_bracket 1269 | top_bracket += min(self.REQUEST_CHUNK, max(update_size-bytes_downloaded, 1)) 1270 | downloading_headers["Range"] = "bytes={0}-{1}".format(bottom_bracket, top_bracket) 1271 | update_request = urllib2.Request(url=update_url, headers=downloading_headers) 1272 | response = urllib2.urlopen(update_request, timeout=self.TIMEOUT) 1273 | 1274 | except Exception as e: 1275 | self.update_exception = SubmodUpdaterError("Failed to download update.", submod=self.id, e=e) 1276 | 1277 | self.__delete_update_files(temp_files_dir) 1278 | 1279 | self.__updating = False 1280 | 1281 | self.__do_bulk_progress_bar_logic() 1282 | 1283 | return False 1284 | 1285 | # # # Extracting update 1286 | try: 1287 | # unzip :S 1288 | with ZipFile(temp_file, "r") as update_file: 1289 | update_file.extractall(temp_files_dir) 1290 | 1291 | except Exception as e: 1292 | should_exit = True 1293 | # If this is an exception about fps lenght on windows, 1294 | # we can try to handle it 1295 | if ( 1296 | renpy.windows 1297 | and ( 1298 | ( 1299 | isinstance(e, IOError) 1300 | and e.errno == 2 1301 | ) 1302 | or ( 1303 | isinstance(e, WindowsError) 1304 | and e.winerror in (3, 123, 206) 1305 | ) 1306 | ) 1307 | ): 1308 | try: 1309 | # Just in case 1310 | self.__delete_update_files(temp_files_dir) 1311 | # Handle the fp 1312 | temp_files_dir = "\\\\?\\" + os.path.normpath(temp_files_dir) 1313 | temp_file = "\\\\?\\" + os.path.normpath(temp_file) 1314 | update_dir = "\\\\?\\" + os.path.normpath(update_dir) 1315 | # Try to unzip again 1316 | with ZipFile(temp_file, "r") as update_file: 1317 | update_file.extractall(temp_files_dir) 1318 | 1319 | except: 1320 | pass 1321 | 1322 | # If we were able to extract, we continue 1323 | else: 1324 | should_exit = False 1325 | 1326 | if should_exit: 1327 | self.update_exception = SubmodUpdaterError("Failed to extract update.", submod=self.id, e=e) 1328 | 1329 | self.__delete_update_files(temp_files_dir) 1330 | 1331 | self.__updating = False 1332 | 1333 | self.__do_bulk_progress_bar_logic() 1334 | 1335 | return False 1336 | 1337 | # delete update.zip 1338 | # even if it fails, it's not so bad, we can continue updating 1339 | self.__delete_update_files(temp_file) 1340 | 1341 | # move the files 1342 | exceptions = self.__extract_files(temp_files_dir, update_dir, extraction_depth) 1343 | 1344 | # even if we fail here, it's too late to abort now 1345 | # but we can log exceptions 1346 | if exceptions: 1347 | SubmodUpdaterError("Failed to move update files. Submod: '{0}'. Exceptions:\n{1}".format(self.id, "\n - ".join(exceptions))) 1348 | 1349 | # self.__delete_update_files(temp_files_dir) 1350 | 1351 | # self.__updating = False 1352 | 1353 | # return False 1354 | 1355 | # delete temp folders 1356 | self.__delete_update_files(temp_files_dir) 1357 | 1358 | # log that we updated this submod 1359 | writeLog( 1360 | "Downloaded and installed the {0} update for '{1}'.".format( 1361 | self._json["latest_version"], 1362 | self.id 1363 | ), 1364 | is_error=False 1365 | ) 1366 | 1367 | # (Re-)set some status vars 1368 | self.update_exception = None 1369 | self.__updating = False 1370 | self.__updated = True 1371 | 1372 | self.__do_bulk_progress_bar_logic() 1373 | 1374 | return True 1375 | 1376 | return False 1377 | 1378 | def downloadUpdateInThread(self, update_dir=None, extraction_depth=1): 1379 | """ 1380 | Download the latest update for the submod using threading 1381 | (basically runs _downloadUpdate in a thread) 1382 | NOTE: does not check for update 1383 | NOTE: won't do anything if we're still updating another submod 1384 | NOTE: For internal uses the arguments for this method the updater will take from the properties 1385 | 1386 | IN: 1387 | update_dir - the directory the updater will extract this update into 1388 | NOTE: if None, the update will be installed in the submod directory 1389 | NOTE: if empty string, the update will be installed right in the base directory (with DDLC.exe) 1390 | (Default: None) 1391 | 1392 | extraction_depth - the extraction depth, check the main method for explanation 1393 | (Default: 1) 1394 | """ 1395 | downloader = threading.Thread( 1396 | target=self._downloadUpdate, 1397 | kwargs=dict(update_dir=update_dir, extraction_depth=extraction_depth) 1398 | ) 1399 | downloader.start() 1400 | 1401 | def _checkConflicts(self): 1402 | """ 1403 | Checks if some of other submods will have issues if we update this submod 1404 | NOTE: doesn't actually forbid updating, only prompts the user 1405 | 1406 | OUT: 1407 | list of tuples with conflicting submods 1408 | format: (submod name, max supported/required version of this submod) 1409 | """ 1410 | conflicting_submods = [] 1411 | 1412 | # we shouldn't get here if we don't have the version number 1413 | if self._json is None: 1414 | return conflicting_submods 1415 | 1416 | for submod in mas_submod_utils.submod_map.itervalues(): 1417 | # so it doesn't check itself 1418 | if submod is not self._submod: 1419 | # we can get by name since we know what we're looking for 1420 | minmax_version_tuple = submod.dependencies.get(self._submod.name, None) 1421 | 1422 | if ( 1423 | minmax_version_tuple is not None 1424 | and len(minmax_version_tuple) > 1 1425 | ): 1426 | max_version = minmax_version_tuple[1] 1427 | 1428 | if max_version: 1429 | rv = mas_utils.compareVersionLists( 1430 | self.__versionToList(max_version), 1431 | self.__versionToList(self._json["latest_version"]) 1432 | ) 1433 | 1434 | # we should prompt the user that this one might cause issues 1435 | if rv < 0: 1436 | conflicting_submods.append((submod.name, self._submod.name, max_version)) 1437 | 1438 | return conflicting_submods 1439 | 1440 | @classmethod 1441 | def updateSubmods(cls, updaters): 1442 | """ 1443 | Queue the given updaters for update 1444 | NOTE: updates only one submod at a time 1445 | NOTE: no guarantees which submod will be updated first 1446 | NOTE: this will use the default submod params: 1447 | update_dir and extraction_depth 1448 | 1449 | IN: 1450 | updaters - list of updaters whose submods we'll update 1451 | """ 1452 | # We should use the lock to modify the list 1453 | with cls.updateDownloadLock: 1454 | # Reset after the previous update 1455 | cls.queued_updaters[:] = [] 1456 | cls.finished_updaters[:] = [] 1457 | cls.bulk_progress_bar.reset() 1458 | 1459 | for updater in updaters: 1460 | cls.queued_updaters.append(updater) 1461 | updater.downloadUpdateInThread( 1462 | update_dir=updater._update_dir, 1463 | extraction_depth=updater._extraction_depth 1464 | ) 1465 | 1466 | @classmethod 1467 | def totalQueuedUpdaters(cls): 1468 | """ 1469 | Returns the number of queued updaters 1470 | """ 1471 | return len(cls.queued_updaters) 1472 | 1473 | @classmethod 1474 | def totalFinishedUpdaters(cls): 1475 | """ 1476 | Returns the number of finished updaters 1477 | """ 1478 | return len(cls.finished_updaters) 1479 | 1480 | @classmethod 1481 | def isBulkUpdating(cls): 1482 | """ 1483 | Returns whether or not we have an ongoing bulk update 1484 | """ 1485 | # in the end they should have equal length 1486 | return cls.totalFinishedUpdaters() < cls.totalQueuedUpdaters() 1487 | 1488 | @classmethod 1489 | def getDirectoryFor(cls, submod_name, absolute=True): 1490 | """ 1491 | Returns the file path to a submod directory 1492 | 1493 | IN: 1494 | submod_name - the name of the submod 1495 | absolute - True to return the absolute path, 1496 | False to return the relative one 1497 | 1498 | OUT: 1499 | string with the path, 1500 | or None if we couldn't find it or submod doesn't exist 1501 | """ 1502 | updater = cls.getUpdater(submod_name) 1503 | if updater: 1504 | return updater.getDirectory(absolute=absolute) 1505 | 1506 | return None 1507 | 1508 | @classmethod 1509 | def getUpdater(cls, submod_name): 1510 | """ 1511 | Gets an updater from the map 1512 | 1513 | IN: 1514 | submod_name - id of the updater 1515 | 1516 | OUR: 1517 | SubmodUpdater object, or None if not found. 1518 | """ 1519 | return cls.registered_updaters.get(submod_name, None) 1520 | 1521 | @classmethod 1522 | def getUpdaters(cls, exclude_sup=True): 1523 | """ 1524 | Returns a list of all registered updaters 1525 | 1526 | IN: 1527 | exclude_sup - whether or not we exclude Submod Updater Plugin's updater from the list 1528 | 1529 | OUT: 1530 | list of updaters 1531 | """ 1532 | rv = cls.registered_updaters.values() 1533 | if exclude_sup: 1534 | rv = filter(lambda updater: updater.id != "Submod Updater Plugin", rv) 1535 | 1536 | return rv 1537 | 1538 | @classmethod 1539 | def _getUpdaterForUpdatingSubmod(cls): 1540 | """ 1541 | Returns the updater for the submod that is currently being updated 1542 | 1543 | OUT: 1544 | SubmodUpdater object, or None if not found. 1545 | """ 1546 | for updater in cls.registered_updaters.itervalues(): 1547 | if updater.__updating: 1548 | return updater 1549 | 1550 | return None 1551 | 1552 | @classmethod 1553 | def isUpdatingAny(cls): 1554 | """ 1555 | Checks if we're updating a submod right now 1556 | 1557 | OUT: 1558 | True if have an ongoing update, 1559 | False otherwise 1560 | """ 1561 | return cls._getUpdaterForUpdatingSubmod() is not None 1562 | 1563 | @classmethod 1564 | def hasUpdateFor(cls, submod_name, should_check=True): 1565 | """ 1566 | Checks if there's an update for a submod 1567 | (basically checks hasUpdate) 1568 | 1569 | IN: 1570 | submod_name - name of the submod to check 1571 | should_check - whether we send a request to GitHub (True), 1572 | or return False if we don't have the update data (False) 1573 | (Default: True) 1574 | 1575 | OUT: 1576 | True if there's an update, 1577 | False if no updates, or the submod doesn't exist 1578 | """ 1579 | updater = cls.getUpdater(submod_name) 1580 | if updater is not None: 1581 | return updater.hasUpdate(should_check=should_check) 1582 | 1583 | return False 1584 | 1585 | @classmethod 1586 | def _notify(cls): 1587 | """ 1588 | Notifies the user about all available submods 1589 | NOTE: does not check for updates 1590 | """ 1591 | additional_lines = list() 1592 | 1593 | for updater in cls.registered_updaters.itervalues(): 1594 | if ( 1595 | updater.should_notify 1596 | and updater.hasUpdate(should_check=False) 1597 | ): 1598 | additional_lines.append( 1599 | "\n '{0}' {1} >>> {2} ".format( 1600 | updater._submod.name, 1601 | updater._submod.version, 1602 | updater.latest_version 1603 | ) 1604 | ) 1605 | 1606 | # one line per submod 1607 | total = len(additional_lines) 1608 | 1609 | if total > 1: 1610 | main_line = "There are submod updates available: {}" 1611 | 1612 | elif total == 1: 1613 | main_line = "There's a submod update available: {}" 1614 | 1615 | # nothing to notify about 1616 | else: 1617 | return 1618 | 1619 | notify_message = main_line.format( 1620 | "".join(additional_lines) 1621 | ) 1622 | renpy.notify(notify_message) 1623 | 1624 | @classmethod 1625 | def _checkUpdates(cls): 1626 | """ 1627 | Check updates for each registered submods 1628 | """ 1629 | cls.is_checking_updates = True 1630 | for updater in cls.registered_updaters.itervalues(): 1631 | if updater.auto_check: 1632 | updater._checkUpdate() 1633 | cls.is_checking_updates = False 1634 | 1635 | @classmethod 1636 | def _doLogic(cls, check_updates=True, notify=True): 1637 | """ 1638 | Checks each submod for available updates 1639 | and notifies the user if needed 1640 | 1641 | IN: 1642 | check_updates - whether or not we check for updates 1643 | notify - whether or not we show the notification 1644 | """ 1645 | if check_updates: 1646 | cls._checkUpdates() 1647 | if notify: 1648 | cls._notify() 1649 | 1650 | @classmethod 1651 | def doLogicInThread(cls, check_updates=True, notify=True): 1652 | """ 1653 | Runs doLogic in a thread 1654 | 1655 | IN: 1656 | check_updates - whether or not we check for updates 1657 | notify - whether or not we show the notification 1658 | """ 1659 | checker = threading.Thread( 1660 | target=cls._doLogic, 1661 | args=(check_updates, notify) 1662 | ) 1663 | checker.daemon = True 1664 | checker.start() 1665 | 1666 | @classmethod 1667 | def getUpdatersForOutdatedSubmods(cls, ignore_if_updated=True, ignore_if_updating=False, ignore_if_cant_update=False): 1668 | """ 1669 | Returns updater object for each outdated submod 1670 | NOTE: does not check for updates itself 1671 | 1672 | IN: 1673 | ignore_if_updated - if True we'll skip already updated submods 1674 | (Default: True) 1675 | 1676 | ignore_if_updating - if True we'll skip currently updating submods 1677 | (Default: False) 1678 | 1679 | ignore_if_cant_update - if True we'll skip submods that can't be updated in-game 1680 | (Default: False) 1681 | 1682 | OUT: 1683 | list of updaters 1684 | """ 1685 | return [ 1686 | updater 1687 | for updater in cls.registered_updaters.itervalues() 1688 | if ( 1689 | updater.hasUpdate( 1690 | should_check=False, 1691 | ignore_if_updated=ignore_if_updated, 1692 | ignore_if_updating=ignore_if_updating 1693 | ) 1694 | and ( 1695 | updater.allow_updates 1696 | or not ignore_if_cant_update 1697 | ) 1698 | ) 1699 | ] 1700 | 1701 | @classmethod 1702 | def hasOutdatedSubmods(cls, ignore_if_updated=True, ignore_if_updating=False, ignore_if_cant_update=False): 1703 | """ 1704 | Returns a boolean whether or not the user has outdated submods 1705 | NOTE: does not check for updates itself 1706 | 1707 | IN: 1708 | ignore_if_updated - if True we'll skip already updated submods 1709 | (Default: True) 1710 | 1711 | ignore_if_updating - if True we'll skip currently updating submods 1712 | (Default: False) 1713 | 1714 | ignore_if_cant_update - if True we'll skip submods that can't be updated in-game 1715 | (Default: False) 1716 | 1717 | OUT: 1718 | True if has, False if has not 1719 | """ 1720 | return len( 1721 | cls.getUpdatersForOutdatedSubmods( 1722 | ignore_if_updated=ignore_if_updated, 1723 | ignore_if_updating=ignore_if_updating, 1724 | ignore_if_cant_update=ignore_if_cant_update 1725 | ) 1726 | ) > 0 1727 | 1728 | @classmethod 1729 | def getIcon(cls, submod_name): 1730 | """ 1731 | Returns an image for the current state of this submod 1732 | 1733 | IN: 1734 | submod_name - name of the submod to get img for 1735 | 1736 | OUT: 1737 | Image object (may return None if coulnd find the img, but this shouldn't happen) 1738 | """ 1739 | updater = cls.getUpdater(submod_name) 1740 | img_key = ("sup_indicator_no_update",) 1741 | 1742 | if updater is not None and updater._json is not None: 1743 | update_state = updater._compareVersion(updater._json["latest_version"]) 1744 | 1745 | if updater.isUpdating(): 1746 | img_key = ("sup_indicator_update_downloading",) 1747 | 1748 | elif updater.hasUpdated(): 1749 | pass 1750 | 1751 | elif update_state < 0: 1752 | img_key = ("sup_indicator_update_available",) 1753 | 1754 | elif update_state > 0: 1755 | img_key = ("sup_indicator_beta_warning",) 1756 | 1757 | return renpy.display.image.images.get(img_key, None) 1758 | 1759 | @classmethod 1760 | def getTooltip(cls, submod_name): 1761 | """ 1762 | Returns a tooltip for the current state of this submod 1763 | 1764 | IN: 1765 | submod_name - name of the submod to get tooltip for 1766 | 1767 | OUT: 1768 | strings as the tooltip, or None if the submod doesn't exist 1769 | """ 1770 | updater = cls.getUpdater(submod_name) 1771 | tooltip = "" 1772 | 1773 | if updater is not None and updater._json is not None: 1774 | update_state = updater._compareVersion(updater._json["latest_version"]) 1775 | 1776 | if updater.isUpdating(): 1777 | tooltip = "Updating..." 1778 | 1779 | elif updater.hasUpdated(): 1780 | pass 1781 | 1782 | elif update_state < 0: 1783 | tooltip = "Update available!" 1784 | 1785 | elif update_state > 0: 1786 | tooltip = "WARNING! You're using the UNTESTED version!" 1787 | 1788 | return tooltip 1789 | 1790 | @classmethod 1791 | def formatMDtoRenPy(cls, text): 1792 | """ 1793 | The most disgusting method here, parses text and replaces some MD tags with RenPy ones. 1794 | NOTE: handles only SOME tags 1795 | 1796 | IN: 1797 | text - text to parse 1798 | 1799 | OUT: 1800 | string with replaced tags 1801 | """ 1802 | def main_tag_parser(match): 1803 | """ 1804 | Parser for a single tag 1805 | 1806 | IN: 1807 | match - MatchObject 1808 | 1809 | OUT: 1810 | string with the parsed tag 1811 | 1812 | ASSUMES: 1813 | match is NOT None 1814 | """ 1815 | match_string = match.group() 1816 | match_string = match_string.lstrip(" ") 1817 | 1818 | # Exactly in this order 1819 | if match_string.startswith("["): 1820 | match_string = re.sub(cls.MD_LINK_TAG_PATTERN, r"{a=\g<2>}{i}{u}\g<1>{/u}{/i}{/a}", match_string) 1821 | 1822 | elif match_string.startswith("#"): 1823 | subbed_string = re.sub(cls.MD_HEADING_TAG_PATTERN, r"\g<1>{{size={0}}}{{b}}\g<2>{{/b}}{{/size}}\g<3>", match_string) 1824 | base_string = subbed_string.lstrip("#") 1825 | heading_size = cls.HEADING_SIZE_MAP.get(len(subbed_string) - len(base_string), "+0") 1826 | match_string = base_string.format(heading_size) 1827 | 1828 | elif match_string.startswith("***"): 1829 | match_string = re.sub(cls.MD_BOLD_ITALIC_TAG_PATTERN, r"{b}{i}\g<1>{/i}{/b}", match_string) 1830 | 1831 | elif match_string.startswith("**"): 1832 | match_string = re.sub(cls.MD_BOLD_ASTERISK_TAG_PATTERN, r"{b}\g<1>{/b}", match_string) 1833 | 1834 | elif match_string.startswith("__"): 1835 | match_string = re.sub(cls.MD_BOLD_UNDERLINE_TAG_PATTERN, r"{b}\g<1>{/b}", match_string) 1836 | 1837 | elif match_string.startswith("*"): 1838 | match_string = re.sub(cls.MD_ITALIC_ASTERISK_TAG_PATTERN, r"{i}\g<1>{/i}", match_string) 1839 | 1840 | elif match_string.startswith("_"): 1841 | match_string = re.sub(cls.MD_ITALIC_UNDERLINE_TAG_PATTERN, r"{i}\g<1>{/i}", match_string) 1842 | 1843 | elif match_string.startswith("~~"): 1844 | match_string = re.sub(cls.MD_STRIKETHROUGH_TAG_PATTERN, r"{s}\g<1>{/s}", match_string) 1845 | 1846 | elif match_string.startswith(">"): 1847 | match_string = re.sub(cls.MD_QUOTING_TAG_PATTERN, r"\g<1>{color=#63605f}'\g<2>'{/color}\g<3>", match_string) 1848 | 1849 | return match_string 1850 | 1851 | def second_tag_parser(match): 1852 | """ 1853 | Parser for a single tag 1854 | 1855 | IN: 1856 | match - MatchObject 1857 | 1858 | OUT: 1859 | string with the parsed tag 1860 | 1861 | ASSUMES: 1862 | match is NOT None 1863 | """ 1864 | match_string = match.group() 1865 | match_string = match_string.lstrip(" ") 1866 | 1867 | # Exactly in this order 1868 | if match_string.startswith("***"): 1869 | match_string = re.sub(cls.MD_BOLD_ITALIC_TAG_PATTERN, r"{b}{i}\g<1>{/i}{/b}", match_string) 1870 | 1871 | elif match_string.startswith("**"): 1872 | match_string = re.sub(cls.MD_BOLD_ASTERISK_TAG_PATTERN, r"{b}\g<1>{/b}", match_string) 1873 | 1874 | elif match_string.startswith("__"): 1875 | match_string = re.sub(cls.MD_BOLD_UNDERLINE_TAG_PATTERN, r"{b}\g<1>{/b}", match_string) 1876 | 1877 | elif match_string.startswith("*"): 1878 | match_string = re.sub(cls.MD_ITALIC_ASTERISK_TAG_PATTERN, r"{i}\g<1>{/i}", match_string) 1879 | 1880 | elif match_string.startswith("_"): 1881 | match_string = re.sub(cls.MD_ITALIC_UNDERLINE_TAG_PATTERN, r"{i}\g<1>{/i}", match_string) 1882 | 1883 | elif match_string.startswith("~~"): 1884 | match_string = re.sub(cls.MD_STRIKETHROUGH_TAG_PATTERN, r"{s}\g<1>{/s}", match_string) 1885 | 1886 | return match_string 1887 | 1888 | try: 1889 | text = re.sub(cls.MD_TAGS_PATTERN, main_tag_parser, text) 1890 | 1891 | except Exception as e: 1892 | SubmodUpdaterError("Failed to parse update changelog using the main parser.", submod=self.id, e=e) 1893 | 1894 | try: 1895 | text = re.sub(cls.MD_TAGS_PATTERN, second_tag_parser, text) 1896 | 1897 | except Exception as e: 1898 | SubmodUpdaterError("Failed to parse update changelog using the second parser.", submod=self.id, e=e) 1899 | 1900 | return text 1901 | 1902 | @staticmethod 1903 | def openURL(url): 1904 | """ 1905 | Tries to open a url in the default browser 1906 | Won't raise exceptions. 1907 | 1908 | IN: 1909 | url - url to open 1910 | 1911 | OUT: 1912 | True if we were able to open the url, 1913 | False otherwise 1914 | """ 1915 | if not url: 1916 | return False 1917 | 1918 | try: 1919 | openBrowser(url, new=2, autoraise=True) 1920 | return True 1921 | 1922 | except: 1923 | return False 1924 | 1925 | @staticmethod 1926 | def openFolder(path): 1927 | """ 1928 | Tried to open a folder in the default file manager 1929 | Won't rise exceptions. 1930 | 1931 | IN: 1932 | path - absolute path to open 1933 | 1934 | OUT: 1935 | True if we were able to open the folder, 1936 | False otherwise 1937 | """ 1938 | # sanity check so you do not fook up 1939 | # trying to do something illegal here 1940 | if not os.path.isdir(path): 1941 | return False 1942 | 1943 | path = path.replace("/", "\\") 1944 | 1945 | try: 1946 | if renpy.windows: 1947 | subprocOpen(["explorer", path]) 1948 | return True 1949 | 1950 | elif renpy.linux: 1951 | subprocOpen(["xdg-open", path]) 1952 | return True 1953 | 1954 | elif renpy.macintosh: 1955 | subprocOpen(["open", path]) 1956 | return True 1957 | 1958 | return False 1959 | 1960 | except: 1961 | return False 1962 | 1963 | # # # END OF THE SUBMODUPDATER CLASS 1964 | 1965 | # # # Register auto-update checks 1966 | init python in sup_utils: 1967 | mas_submod_utils.registerFunction("ch30_reset", SubmodUpdater.doLogicInThread, auto_error_handling=False) 1968 | mas_submod_utils.registerFunction("ch30_day", SubmodUpdater.doLogicInThread, auto_error_handling=False) 1969 | 1970 | # # # Icons for different update states 1971 | image sup_indicator_update_downloading = store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.INDICATOR_UPDATE_DOWNLOADING 1972 | 1973 | image sup_indicator_update_available = store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.INDICATOR_UPDATE_AVAILABLE 1974 | 1975 | # basically a placeholder 1976 | image sup_indicator_no_update = Null(height=20) 1977 | 1978 | image sup_indicator_beta_warning = store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.INDICATOR_BETA_WARNING 1979 | 1980 | transform sup_indicator_transform: 1981 | block: 1982 | ease 0.75 alpha 1.0 1983 | pause 2.0 1984 | ease 0.75 alpha 0.0 1985 | repeat 1986 | 1987 | # predefine these to save some performance 1988 | image sup_text_updating_1 = Text("Updating the submod ", size=15) 1989 | image sup_text_updating_2 = Text("Updating the submod. ", size=15) 1990 | image sup_text_updating_3 = Text("Updating the submod.. ", size=15) 1991 | image sup_text_updating_4 = Text("Updating the submod...", size=15) 1992 | 1993 | image sup_progress_bar_text: 1994 | xanchor 0 1995 | subpixel True 1996 | block: 1997 | "sup_text_updating_1" 1998 | pause 0.75 1999 | "sup_text_updating_2" 2000 | pause 0.75 2001 | "sup_text_updating_3" 2002 | pause 0.75 2003 | "sup_text_updating_4" 2004 | pause 0.75 2005 | repeat 2006 | 2007 | # # # Submod Updater Plugin settings screen 2008 | screen sup_setting_pane(): 2009 | default total_updaters = len(store.sup_utils.SubmodUpdater.getUpdaters()) 2010 | default updatable_submod_updaters = store.sup_utils.SubmodUpdater.getUpdatersForOutdatedSubmods(ignore_if_cant_update=True) 2011 | default total_updatable_submod_updaters = len(updatable_submod_updaters) 2012 | 2013 | vbox: 2014 | xmaximum 800 2015 | xfill True 2016 | style_prefix "check" 2017 | 2018 | textbutton "{b}Check updates{/b}": 2019 | ypos 1 2020 | selected False 2021 | sensitive (not store.sup_utils.SubmodUpdater.is_checking_updates) 2022 | action Function(store.sup_utils.SubmodUpdater.doLogicInThread, check_updates=True, notify=False) 2023 | 2024 | if total_updaters > 0: 2025 | textbutton "{b}Adjust settings{/b}": 2026 | ypos 1 2027 | selected False 2028 | action Show("sup_settings") 2029 | 2030 | if store.sup_utils.SubmodUpdater.hasOutdatedSubmods(): 2031 | textbutton "{b}Select a submod to update{/b}": 2032 | ypos 1 2033 | selected False 2034 | action Show("sup_available_updates") 2035 | 2036 | if total_updatable_submod_updaters > 0: 2037 | textbutton "{b}Start bulk updating{/b}": 2038 | ypos 1 2039 | selected False 2040 | action Show( 2041 | "sup_confirm_bulk_update", 2042 | submod_updaters=updatable_submod_updaters, 2043 | from_submod_screen=True 2044 | ) 2045 | 2046 | if store.sup_utils.SubmodUpdater.is_checking_updates: 2047 | timer 1.0: 2048 | repeat True 2049 | action Function(renpy.restart_interaction) 2050 | 2051 | # # # A screen to change updaters' settings 2052 | screen sup_settings(): 2053 | key "noshift_T" action NullAction() 2054 | key "noshift_t" action NullAction() 2055 | key "noshift_M" action NullAction() 2056 | key "noshift_m" action NullAction() 2057 | key "noshift_P" action NullAction() 2058 | key "noshift_p" action NullAction() 2059 | key "noshift_E" action NullAction() 2060 | key "noshift_e" action NullAction() 2061 | 2062 | default submod_updaters = sorted(store.sup_utils.SubmodUpdater.getUpdaters(), key=lambda updater: updater.id) 2063 | 2064 | modal True 2065 | 2066 | zorder 200 2067 | 2068 | style_prefix "confirm" 2069 | add mas_getTimeFile("gui/overlay/confirm.png") 2070 | 2071 | frame: 2072 | vbox: 2073 | ymaximum 300 2074 | xmaximum 800 2075 | xfill True 2076 | yfill False 2077 | spacing 5 2078 | 2079 | viewport: 2080 | id "viewport" 2081 | scrollbars "vertical" 2082 | ymaximum 250 2083 | xmaximum 780 2084 | xfill True 2085 | yfill False 2086 | mousewheel True 2087 | 2088 | vbox: 2089 | xmaximum 780 2090 | xfill True 2091 | yfill False 2092 | box_wrap False 2093 | 2094 | for submod_updater in submod_updaters: 2095 | text "[submod_updater.id] v[submod_updater._submod.version]" 2096 | 2097 | hbox: 2098 | xpos 5 2099 | spacing 10 2100 | xmaximum 780 2101 | 2102 | textbutton ("Disable notifications" if submod_updater.should_notify else "Enable notifications"): 2103 | style "check_button" 2104 | ypos 1 2105 | action Function(submod_updater.toggleNotifs) 2106 | 2107 | textbutton "Close": 2108 | action Hide("sup_settings") 2109 | 2110 | # # # Screen that show all available updates 2111 | screen sup_available_updates(): 2112 | key "noshift_T" action NullAction() 2113 | key "noshift_t" action NullAction() 2114 | key "noshift_M" action NullAction() 2115 | key "noshift_m" action NullAction() 2116 | key "noshift_P" action NullAction() 2117 | key "noshift_p" action NullAction() 2118 | key "noshift_E" action NullAction() 2119 | key "noshift_e" action NullAction() 2120 | 2121 | default submod_updaters = sorted(store.sup_utils.SubmodUpdater.getUpdatersForOutdatedSubmods(), key=lambda updater: updater.id) 2122 | default updatable_submod_updaters = store.sup_utils.SubmodUpdater.getUpdatersForOutdatedSubmods(ignore_if_cant_update=True) 2123 | default total_updatable_submod_updaters = len(updatable_submod_updaters) 2124 | 2125 | modal True 2126 | 2127 | zorder 200 2128 | 2129 | style_prefix "confirm" 2130 | add mas_getTimeFile("gui/overlay/confirm.png") 2131 | 2132 | frame: 2133 | vbox: 2134 | ymaximum 300 2135 | xmaximum 800 2136 | xfill True 2137 | yfill False 2138 | spacing 5 2139 | 2140 | viewport: 2141 | id "viewport" 2142 | scrollbars "vertical" 2143 | ymaximum 250 2144 | xmaximum 780 2145 | xfill True 2146 | yfill False 2147 | mousewheel True 2148 | 2149 | vbox: 2150 | xmaximum 780 2151 | xfill True 2152 | yfill False 2153 | box_wrap False 2154 | 2155 | for submod_updater in submod_updaters: 2156 | hbox: 2157 | xpos 20 2158 | spacing 10 2159 | xmaximum 780 2160 | 2161 | text "[submod_updater.id]" 2162 | text "v[submod_updater._submod.version]" 2163 | text " >>> " 2164 | text "v[submod_updater.latest_version]" 2165 | 2166 | hbox: 2167 | xpos 5 2168 | spacing 10 2169 | xmaximum 780 2170 | 2171 | textbutton "What's new?": 2172 | style "check_button" 2173 | ypos 1 2174 | action [ 2175 | Show( 2176 | "sup_update_preview", 2177 | title=submod_updater.update_name, 2178 | body=submod_updater.update_changelog 2179 | ), 2180 | Hide("sup_available_updates") 2181 | ] 2182 | 2183 | if ( 2184 | submod_updater.allow_updates 2185 | and not submod_updater.isUpdating() 2186 | ): 2187 | textbutton "Update now!": 2188 | style "check_button" 2189 | ypos 1 2190 | action [ 2191 | Show("sup_confirm_single_update", submod_updater=submod_updater), 2192 | Hide("sup_available_updates") 2193 | ] 2194 | 2195 | hbox: 2196 | xalign 0.5 2197 | spacing 100 2198 | 2199 | if total_updatable_submod_updaters > 0: 2200 | textbutton "Update all": 2201 | action [ 2202 | Show( 2203 | "sup_confirm_bulk_update", 2204 | submod_updaters=updatable_submod_updaters, 2205 | from_submod_screen=False 2206 | ), 2207 | Hide("sup_available_updates") 2208 | ] 2209 | 2210 | textbutton "Close": 2211 | action Hide("sup_available_updates") 2212 | 2213 | # # # Update preview screen 2214 | # 2215 | # IN: 2216 | # title - update title 2217 | # body - update changelog 2218 | # 2219 | screen sup_update_preview(title, body): 2220 | key "noshift_T" action NullAction() 2221 | key "noshift_t" action NullAction() 2222 | key "noshift_M" action NullAction() 2223 | key "noshift_m" action NullAction() 2224 | key "noshift_P" action NullAction() 2225 | key "noshift_p" action NullAction() 2226 | key "noshift_E" action NullAction() 2227 | key "noshift_e" action NullAction() 2228 | 2229 | modal True 2230 | 2231 | zorder 200 2232 | 2233 | style_prefix "confirm" 2234 | add mas_getTimeFile("gui/overlay/confirm.png") 2235 | 2236 | frame: 2237 | vbox: 2238 | align (0.5, 0.5) 2239 | spacing 30 2240 | 2241 | label title: 2242 | style "confirm_prompt" 2243 | xalign 0.5 2244 | 2245 | viewport: 2246 | ymaximum 200 2247 | xmaximum 800 2248 | xfill False 2249 | yfill False 2250 | mousewheel True 2251 | scrollbars "vertical" 2252 | 2253 | text body.replace("\n", "\n\n") 2254 | 2255 | textbutton "Close": 2256 | xalign 0.5 2257 | action [ 2258 | Hide("sup_update_preview"), 2259 | Show("sup_available_updates") 2260 | ] 2261 | 2262 | # # # Confirm screen a single update 2263 | # 2264 | # IN: 2265 | # submod_updater - updater 2266 | # 2267 | screen sup_confirm_single_update(submod_updater): 2268 | key "noshift_T" action NullAction() 2269 | key "noshift_t" action NullAction() 2270 | key "noshift_M" action NullAction() 2271 | key "noshift_m" action NullAction() 2272 | key "noshift_P" action NullAction() 2273 | key "noshift_p" action NullAction() 2274 | key "noshift_E" action NullAction() 2275 | key "noshift_e" action NullAction() 2276 | 2277 | default conflicts = submod_updater._checkConflicts() 2278 | default total_conflicts = len(conflicts) 2279 | 2280 | modal True 2281 | 2282 | zorder 200 2283 | 2284 | style_prefix "confirm" 2285 | add mas_getTimeFile("gui/overlay/confirm.png") 2286 | 2287 | frame: 2288 | vbox: 2289 | align (0.5, 0.5) 2290 | spacing 30 2291 | 2292 | label "Start updating [submod_updater.id] v[submod_updater._submod.version] to v[submod_updater.latest_version]?": 2293 | style "confirm_prompt" 2294 | xalign 0.5 2295 | 2296 | if total_conflicts > 0: 2297 | viewport: 2298 | ymaximum 200 2299 | xmaximum 800 2300 | xfill False 2301 | yfill False 2302 | mousewheel True 2303 | scrollbars "vertical" 2304 | 2305 | vbox: 2306 | spacing 5 2307 | 2308 | text "Warning:" 2309 | 2310 | null height 5 2311 | 2312 | for conflicting_submod, this_submod, max_version in conflicts: 2313 | text " - [conflicting_submod] supports maximum v[max_version] of [this_submod]" 2314 | 2315 | null height 5 2316 | 2317 | if total_conflicts > 1: 2318 | text "Updating those submods to their newer versions might fix that issue." 2319 | 2320 | else: 2321 | text "Updating that submod to its newer version might fix that issue." 2322 | 2323 | hbox: 2324 | xalign 0.5 2325 | spacing 100 2326 | 2327 | textbutton "Yes": 2328 | action [ 2329 | Function( 2330 | submod_updater.downloadUpdateInThread, 2331 | update_dir=submod_updater._update_dir, 2332 | extraction_depth=submod_updater._extraction_depth 2333 | ), 2334 | Hide("sup_confirm_single_update"), 2335 | Show("sup_single_update_screen", submod_updater=submod_updater) 2336 | ] 2337 | 2338 | textbutton "No": 2339 | action [ 2340 | Hide("sup_confirm_single_update"), 2341 | Show("sup_available_updates") 2342 | ] 2343 | 2344 | # # # Confirm screen for a bulk update 2345 | # 2346 | # IN: 2347 | # submod_updaters - updaters 2348 | # from_submod_screen - whether or not we open this screen from the submod screen 2349 | # 2350 | screen sup_confirm_bulk_update(submod_updaters, from_submod_screen=False): 2351 | key "noshift_T" action NullAction() 2352 | key "noshift_t" action NullAction() 2353 | key "noshift_M" action NullAction() 2354 | key "noshift_m" action NullAction() 2355 | key "noshift_P" action NullAction() 2356 | key "noshift_p" action NullAction() 2357 | key "noshift_E" action NullAction() 2358 | key "noshift_e" action NullAction() 2359 | 2360 | default conflicts = [conflict for submod_updater in submod_updaters for conflict in submod_updater._checkConflicts()] 2361 | default total_conflicts = len(conflicts) 2362 | 2363 | modal True 2364 | 2365 | zorder 200 2366 | 2367 | style_prefix "confirm" 2368 | add mas_getTimeFile("gui/overlay/confirm.png") 2369 | 2370 | frame: 2371 | vbox: 2372 | align (0.5, 0.5) 2373 | spacing 30 2374 | 2375 | label "Start updating {b}all{/b} installed submods that can be updated?": 2376 | style "confirm_prompt" 2377 | xalign 0.5 2378 | 2379 | if total_conflicts > 0: 2380 | viewport: 2381 | ymaximum 200 2382 | xmaximum 800 2383 | xfill False 2384 | yfill False 2385 | mousewheel True 2386 | scrollbars "vertical" 2387 | 2388 | vbox: 2389 | spacing 5 2390 | 2391 | text "Warning:" 2392 | 2393 | null height 5 2394 | 2395 | for conflicting_submod, updating_submod, max_version in conflicts: 2396 | text " - [conflicting_submod] supports maximum v[max_version] of [updating_submod]" 2397 | 2398 | null height 5 2399 | 2400 | if total_conflicts > 1: 2401 | text "Updating those submods to their newer versions might fix that issue." 2402 | 2403 | else: 2404 | text "Updating that submod to its newer version might fix that issue." 2405 | 2406 | hbox: 2407 | xalign 0.5 2408 | spacing 100 2409 | 2410 | textbutton "Yes": 2411 | action [ 2412 | Function( 2413 | store.sup_utils.SubmodUpdater.updateSubmods, 2414 | submod_updaters 2415 | ), 2416 | Hide("sup_confirm_bulk_update"), 2417 | Show( 2418 | "sup_bulk_update_screen", 2419 | submod_updaters=submod_updaters, 2420 | from_submod_screen=from_submod_screen 2421 | ) 2422 | ] 2423 | 2424 | textbutton "No": 2425 | action [ 2426 | Hide("sup_confirm_bulk_update"), 2427 | If( 2428 | (not from_submod_screen), 2429 | true=Show("sup_available_updates"), 2430 | false=NullAction() 2431 | ) 2432 | ] 2433 | 2434 | # # # Update screen for single update 2435 | # 2436 | # IN: 2437 | # submod_updater - updater 2438 | # 2439 | screen sup_single_update_screen(submod_updater): 2440 | # for safety 2441 | key "K_ESCAPE" action NullAction() 2442 | key "alt_K_F4" action NullAction() 2443 | key "noshift_T" action NullAction() 2444 | key "noshift_t" action NullAction() 2445 | key "noshift_M" action NullAction() 2446 | key "noshift_m" action NullAction() 2447 | key "noshift_P" action NullAction() 2448 | key "noshift_p" action NullAction() 2449 | key "noshift_E" action NullAction() 2450 | key "noshift_e" action NullAction() 2451 | 2452 | modal True 2453 | 2454 | zorder 200 2455 | 2456 | style_prefix "confirm" 2457 | add mas_getTimeFile("gui/overlay/confirm.png") 2458 | 2459 | frame: 2460 | vbox: 2461 | align (0.5, 0.5) 2462 | xsize 440 2463 | ysize 150 2464 | spacing 0 2465 | 2466 | if store.sup_utils.SubmodUpdater.isUpdatingAny(): 2467 | vbox: 2468 | align (0.5, 0.2) 2469 | spacing 0 2470 | 2471 | bar: 2472 | xalign 0.5 2473 | xysize (400, 25) 2474 | value store.sup_utils.SubmodUpdater.single_progress_bar 2475 | thumb None 2476 | left_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.LEFT_BAR, 2, 2) 2477 | right_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.RIGHT_BAR, 2, 2) 2478 | right_gutter 1 2479 | 2480 | add "sup_progress_bar_text": 2481 | xalign 0.5 2482 | xoffset 5 2483 | ypos -25 2484 | 2485 | else: 2486 | if submod_updater.update_exception is not None: 2487 | text "An error has occurred during updating. Check 'submod_log.txt' for details.": 2488 | align (0.5, 0.2) 2489 | text_align 0.5 2490 | 2491 | else: 2492 | if store.sup_utils.SubmodUpdater.hasOutdatedSubmods(): 2493 | text "Please restart Monika After Story when you have finished installing updates.": 2494 | align (0.5, 0.2) 2495 | text_align 0.5 2496 | 2497 | else: 2498 | text "Please restart Monika After Story.\n": 2499 | align (0.5, 0.2) 2500 | text_align 0.5 2501 | 2502 | textbutton "Ok": 2503 | align (0.5, 0.8) 2504 | sensitive (not store.sup_utils.SubmodUpdater.isUpdatingAny()) 2505 | action [ 2506 | Hide("sup_single_update_screen"), 2507 | If( 2508 | (store.sup_utils.SubmodUpdater.hasOutdatedSubmods()), 2509 | true=Show("sup_available_updates"), 2510 | false=NullAction() 2511 | ) 2512 | ] 2513 | 2514 | timer 0.5: 2515 | repeat True 2516 | action Function(renpy.restart_interaction) 2517 | 2518 | # # # Update screen for bulk update 2519 | # 2520 | # IN: 2521 | # submod_updaters - updaters 2522 | # from_submod_screen - whether or not we open this screen from the submod screen 2523 | # 2524 | screen sup_bulk_update_screen(submod_updaters, from_submod_screen=False): 2525 | # for safety 2526 | key "K_ESCAPE" action NullAction() 2527 | key "alt_K_F4" action NullAction() 2528 | key "noshift_T" action NullAction() 2529 | key "noshift_t" action NullAction() 2530 | key "noshift_M" action NullAction() 2531 | key "noshift_m" action NullAction() 2532 | key "noshift_P" action NullAction() 2533 | key "noshift_p" action NullAction() 2534 | key "noshift_E" action NullAction() 2535 | key "noshift_e" action NullAction() 2536 | 2537 | modal True 2538 | 2539 | zorder 200 2540 | 2541 | style_prefix "confirm" 2542 | add mas_getTimeFile("gui/overlay/confirm.png") 2543 | 2544 | frame: 2545 | vbox: 2546 | align (0.5, 0.5) 2547 | xsize 440 2548 | spacing 10 2549 | 2550 | if ( 2551 | store.sup_utils.SubmodUpdater.isUpdatingAny() 2552 | or store.sup_utils.SubmodUpdater.isBulkUpdating() 2553 | ): 2554 | # total progress 2555 | bar: 2556 | xalign 0.5 2557 | xysize (400, 25) 2558 | value store.sup_utils.SubmodUpdater.bulk_progress_bar 2559 | thumb None 2560 | left_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.LEFT_BAR, 2, 2) 2561 | right_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.RIGHT_BAR, 2, 2) 2562 | right_gutter 1 2563 | 2564 | text "Progress: [store.sup_utils.SubmodUpdater.totalFinishedUpdaters()] / [store.sup_utils.SubmodUpdater.totalQueuedUpdaters()]": 2565 | xalign 0.5 2566 | text_align 0.5 2567 | size 15 2568 | ypos -35 2569 | 2570 | # currently updating submod progress 2571 | bar: 2572 | xalign 0.5 2573 | xysize (400, 25) 2574 | value store.sup_utils.SubmodUpdater.single_progress_bar 2575 | thumb None 2576 | left_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.LEFT_BAR, 2, 2) 2577 | right_bar Frame(store.sup_utils.SubmodUpdater.getDirectoryFor("Submod Updater Plugin", False) + store.sup_utils.SubmodUpdater.RIGHT_BAR, 2, 2) 2578 | right_gutter 1 2579 | 2580 | add "sup_progress_bar_text": 2581 | xalign 0.5 2582 | xoffset 5 2583 | ypos -35 2584 | 2585 | else: 2586 | $ exceptions = [ 2587 | str(submod_updater.update_exception).replace("[", "[[").replace("{", "{{") 2588 | for submod_updater in submod_updaters 2589 | if submod_updater.update_exception is not None 2590 | ] 2591 | if len(exceptions) > 0: 2592 | text "Some errors have occurred during updating. Check 'submod_log.txt' for details.": 2593 | xalign 0.5 2594 | text_align 0.5 2595 | 2596 | null height 65 2597 | 2598 | else: 2599 | text "Please restart Monika After Story.": 2600 | xalign 0.5 2601 | text_align 0.5 2602 | 2603 | null height 80 2604 | 2605 | # null height 10 2606 | 2607 | textbutton "Ok": 2608 | xalign 0.5 2609 | sensitive ( 2610 | not store.sup_utils.SubmodUpdater.isUpdatingAny()# TODO: potentially it should be safe to do only one of these checks 2611 | and not store.sup_utils.SubmodUpdater.isBulkUpdating()# and doing only isBulkUpdating would save some performance 2612 | ) 2613 | action [ 2614 | Hide("sup_bulk_update_screen"), 2615 | If( 2616 | ( 2617 | not from_submod_screen 2618 | and store.sup_utils.SubmodUpdater.hasOutdatedSubmods() 2619 | ), 2620 | true=Show("sup_available_updates"), 2621 | false=NullAction() 2622 | ) 2623 | ] 2624 | 2625 | timer 0.5: 2626 | repeat True 2627 | action Function(renpy.restart_interaction) 2628 | 2629 | # # # Overrides 2630 | init 100: 2631 | screen submods(): 2632 | tag menu 2633 | 2634 | use game_menu(("Submods")): 2635 | 2636 | default tooltip = Tooltip("") 2637 | 2638 | viewport id "scrollme": 2639 | scrollbars "vertical" 2640 | mousewheel True 2641 | draggable True 2642 | 2643 | vbox: 2644 | style_prefix "check" 2645 | xfill True 2646 | xmaximum 1000 2647 | 2648 | for submod in sorted(store.mas_submod_utils.submod_map.values(), key=lambda x: x.name): 2649 | vbox: 2650 | xfill True 2651 | xmaximum 1000 2652 | 2653 | hbox: 2654 | spacing 10 2655 | xmaximum 1000 2656 | 2657 | label submod.name yanchor 0 xalign 0 2658 | 2659 | if store.sup_utils.SubmodUpdater.getUpdater(submod.name) is not None: 2660 | imagebutton: 2661 | idle store.sup_utils.SubmodUpdater.getIcon(submod.name) 2662 | align (0.5, 0.65) 2663 | action NullAction() 2664 | hovered SetField(tooltip, "value", store.sup_utils.SubmodUpdater.getTooltip(submod.name)) 2665 | unhovered SetField(tooltip, "value", tooltip.default) 2666 | 2667 | if not persistent._mas_disable_animations: 2668 | at sup_indicator_transform 2669 | 2670 | hbox: 2671 | spacing 20 2672 | xmaximum 1000 2673 | 2674 | text "v{}".format(submod.version) yanchor 0 xalign 0 style "main_menu_version" 2675 | text "by {}".format(submod.author) yanchor 0 xalign 0 style "main_menu_version" 2676 | 2677 | if submod.description: 2678 | text submod.description 2679 | 2680 | if submod.settings_pane: 2681 | $ renpy.display.screen.use_screen(submod.settings_pane, _name="{0}_{1}".format(submod.author, submod.name)) 2682 | 2683 | text tooltip.value: 2684 | xalign 0 yalign 1.0 2685 | xoffset 300 yoffset -10 2686 | style "main_menu_version" 2687 | -------------------------------------------------------------------------------- /Submod Updater Plugin/game/python-packages/certifi/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import where 2 | 3 | __version__ = "2019.09.11" 4 | -------------------------------------------------------------------------------- /Submod Updater Plugin/game/python-packages/certifi/__main__.py: -------------------------------------------------------------------------------- 1 | from certifi import where 2 | print(where()) 3 | -------------------------------------------------------------------------------- /Submod Updater Plugin/game/python-packages/certifi/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | certifi.py 5 | ~~~~~~~~~~ 6 | 7 | This module returns the installation location of cacert.pem. 8 | """ 9 | import os 10 | 11 | 12 | def where(): 13 | f = os.path.dirname(__file__) 14 | 15 | return os.path.join(f, 'cacert.pem') 16 | -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/darwin-x86_64/lib/python2.7/_ssl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/darwin-x86_64/lib/python2.7/_ssl.so -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/darwin-x86_64/lib/python2.7/ssl.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/darwin-x86_64/lib/python2.7/ssl.pyo -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/linux-i686/lib/python2.7/_ssl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/linux-i686/lib/python2.7/_ssl.so -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/linux-i686/lib/python2.7/ssl.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/linux-i686/lib/python2.7/ssl.pyo -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/linux-x86_64/lib/python2.7/_ssl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/linux-x86_64/lib/python2.7/_ssl.so -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/linux-x86_64/lib/python2.7/ssl.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/linux-x86_64/lib/python2.7/ssl.pyo -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/windows-i686/Lib/_ssl.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/windows-i686/Lib/_ssl.pyd -------------------------------------------------------------------------------- /Submod Updater Plugin/lib/windows-i686/Lib/ssl.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Booplicate/MAS-Submods-SubmodUpdaterPlugin/e306df04e0590203877773429ea047d496f29f91/Submod Updater Plugin/lib/windows-i686/Lib/ssl.pyo --------------------------------------------------------------------------------