├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── mod-compatibility-bug-report.md │ ├── mod-docker-bug-report.md │ └── mod-docker-feature-request.md └── assets │ ├── CompareThree.png │ ├── Containerization.png │ └── DDMDLogo.png ├── .gitignore ├── .vscode └── settings.json ├── BUILDING.md ├── CREDITS.md ├── How to use DDMD (Windows, Linux).txt ├── How to use DDMD (macOS).txt ├── LICENSE ├── README.md ├── ddmd_settings.json ├── game ├── ddmc.json ├── ml_patches.rpy ├── mod_content.rpy ├── mod_dir_browser.rpy ├── mod_installer.rpy ├── mod_list.rpy ├── mod_patches │ └── chrs │ │ ├── monika.chr │ │ ├── natsuki.chr │ │ ├── sayori.chr │ │ └── yuri.chr ├── mod_prompt.rpy ├── mod_screen.rpy ├── mod_services.rpy ├── mod_settings.rpy ├── mod_styles.rpy ├── mod_transforms.rpy ├── options.rpy ├── python-packages │ ├── ddmd_api.py │ └── singleton.py ├── renpy_patches.rpy ├── saves.rpy └── sdc_system │ ├── DDMDLogo.png │ ├── backups │ ├── ddmc.backup │ └── settings.backup │ ├── ddmd_app │ ├── Lato-Light.ttf │ ├── Lato-Regular.ttf │ ├── OFL.txt │ ├── Quicksand-Light.ttf │ ├── Raleway-Bold.ttf │ ├── close.png │ ├── closeHover.png │ ├── ddmd_confirm_overlay.png │ ├── ddmd_frame.png │ ├── disabled.png │ ├── disabledHover.png │ ├── enabled.png │ ├── enabledHover.png │ ├── modInstall.png │ ├── modInstallHover.png │ ├── openBrowser.png │ ├── openBrowserHover.png │ ├── refresh.png │ ├── refreshHover.png │ ├── restart.png │ ├── restartHover.png │ ├── return.png │ ├── returnHover.png │ ├── search.png │ ├── searchHover.png │ ├── searchWindow.png │ ├── searchWindowHover.png │ ├── secondary_frame.png │ ├── selectedMod.png │ ├── selectedModHover.png │ ├── settings.png │ ├── settingsHover.png │ └── steam_frame.png │ ├── file_app │ ├── FileExplorerHBar.png │ ├── FileExplorerVBar.png │ ├── OSBack.png │ ├── OSFile.png │ ├── OSFolder.png │ ├── networkDrive.png │ └── physicalDrive.png │ └── settings_app │ ├── transfer.png │ └── transferHover.png ├── icon.icns ├── icon.ico ├── renpy.py └── renpy ├── bootstrap.py ├── main.py └── mod_docker.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: bronya_rand 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mod-compatibility-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mod Compatibility Bug Report 3 | about: Create a mod compatibility bug report for mods that don't work under Mod Docker. 4 | title: "[Mod Bug]" 5 | labels: bug, mod bug 6 | assignees: Bronya-Rand 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Windows] 25 | - Version [e.g. (Alpha) 1.0.0] 26 | - Mod Name [e.g. Doki Doki Example Club] 27 | - Mod Version [e.g. 1.0.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mod-docker-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mod Docker Bug Report 3 | about: Create a bug report to help us improve Mod Docker from errors. 4 | title: "[DDMD Bug]" 5 | labels: bug, docker bug 6 | assignees: Bronya-Rand 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Version [e.g. (Alpha) 1.0.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mod-docker-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Mod Docker Feature Request 3 | about: Suggest an idea for the next version of Mod Docker! 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: Bronya-Rand 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/assets/CompareThree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/.github/assets/CompareThree.png -------------------------------------------------------------------------------- /.github/assets/Containerization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/.github/assets/Containerization.png -------------------------------------------------------------------------------- /.github/assets/DDMDLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/.github/assets/DDMDLogo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rpyc 2 | *.bak 3 | poemwords.txt 4 | cache 5 | MLSaves 6 | *.rpa 7 | firstrun 8 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.rpyc": true, 4 | "**/*.rpa": true, 5 | "**/*.rpymc": true, 6 | "**/cache/": true 7 | } 8 | } -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | ## Building Mod Docker With Fixes 2 | 1. Clone this repository or download the code and extract the folder to your Ren'Py projects folder. 3 | 2. Add DDLC's RPA files into Mod Docker's game folder. 4 | 3. Rename `main.py` in your Ren'Py SDKs *renpy* folder to a different extension. 5 | 4. Copy `main.py` from Mod Docker's *renpy* folder to your Ren'Py SDKs *renpy* folder. 6 | 5. Launch your Ren'Py SDK, select DDModDocker and click Build Distributions. 7 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | 2 | # Credits 3 | 4 | - RenPyTom (PyTom) - For Ren'Py In General. Also for borrowed code used for DDMD. 5 | - Dan Salvato - For making DDLC and allowing DDLC mods to be possible. 6 | - Mithost - IPG clarification for DDMD. 7 | - [Matt McInerney, Pablo Impallari, and Rodrigo Fuenzalida](https://fonts.google.com/specimen/Raleway?query=Rale) - Raleway Font (similar to Riffic-Bold for UI text). 8 | - [Andrew Paglinawan](https://fonts.google.com/specimen/Quicksand?query=Andrew+Paglinawan) - Quicksand Font (for time). 9 | - [Łukasz Dziedzic](https://fonts.google.com/specimen/Lato?query=%C5%81ukasz+Dziedzic) - Lato Font (for the UI text in the menus) 10 | - [Google](https://fonts.google.com/icons?query=Rale) - Material Icons and Fonts credited above. 11 | - Samsung - UI inspiration for the DDMD menu (Settings). 12 | - Microsoft - UI inspiration for the DDMD menu (Hover Info). 13 | - Valve - UI inspiration for the DDMD menu (Overall menu and DDMD overlay notification). 14 | - [Stack Overflow](https://stackoverflow.com/) - Coding help. 15 | - Ren'Py Discord - Coding help. 16 | - A Anonymous Russian - Testing and Mod Name/Icon Concept. 17 | - [Docker](https://docker.com) - Inspiration of Mod Name/Icon. -------------------------------------------------------------------------------- /How to use DDMD (Windows, Linux).txt: -------------------------------------------------------------------------------- 1 | 1. Download DDLC and copy all of its' RPA files to the game folder of Mod Docker. 2 | 2. Download your favorite mod. 3 | 3. Make a folder in Mod Docker's game folder called 'mods'. 4 | 4. Open the 'mods' folder and make a folder for the mod you want to install. 5 | (I suggest the name of the mod or it's acroymn) 6 | 5. Copy the 'game' folder of the mod that you want to play to the mod folder you just made. 7 | (If there is no game folder and all you have is just RPAs or RPYCs, make a 'game' folder 8 | in the mod folder you made and copy all the files into that folder.) 9 | 6. Launch Mod Docker via DDMD.exe (DDMD.sh for Linux). 10 | 7. Press F9 to open the Mod Docker menu and select your mod by clicking on it and pressing Select. 11 | 8. Restart the game and relaunch Mod Docker. 12 | 9. Done! 13 | 14 | (To switch back to normal mode, select Stock.) -------------------------------------------------------------------------------- /How to use DDMD (macOS).txt: -------------------------------------------------------------------------------- 1 | 2 | In order to follow these steps, you must open DDMD by right-clicking the app, click "Show Package Contents" 3 | then go to "Contents/Resources/autorun". 4 | 5 | 1. Download DDLC and copy all of its' RPA files to the game folder of Mod Docker. 6 | 2. Download your favorite R6 mod. 7 | 3. Make a folder in Mod Docker's game folder called 'mods'. 8 | 4. Open the 'mods' folder and make a folder for the mod you want to install. 9 | (I suggest the name of the mod or it's acroymn) 10 | 5. Copy the 'game' folder of the mod that you want to play to the mod folder you just made. 11 | (If there is no game folder and all you have is just RPAs or RPYCs, make a 'game' folder 12 | in the mod folder you made and copy all the files into that folder). 13 | 6. Launch Mod Docker via DDMD. 14 | 7. Press F9 to open the Mod Docker menu and select your mod by clicking on it and pressing Select. 15 | 8. Restart the game and relaunch Mod Docker. 16 | 9. Done! 17 | 18 | (To switch back to normal mode, select Stock.) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doki Doki Mod Docker 2 | 3 |

4 | DDMD Logo 5 |

6 | Doki Doki Mod Docker is a Ren'Py application designed to streamline the management and play of multiple Doki Doki Literature Club (DDLC) mods. It effectively 'containerizes' mods, allowing them to run independently within a single copy of Ren'Py/DDLC. 7 | 8 | > [!IMPORTANT] 9 | > This project is currently in alpha. You may encounter bugs or compatibility issues with certain mods. To report mod incompatibility, report it either in [Issues](https://github.com/Bronya-Rand/DDModDocker/issues). 10 | 11 | > [!NOTE] 12 | > This project is unaffiliated with Team Salvato. See [BUILDING.md](BUILDING.md) on how to build Mod Docker with fixes for Pull Requests. Credits for DDMD can be seen by looking at [CREDITS.md](CREDITS.md). 13 | 14 | ## Features 15 | 16 | - **Ren'Py 6 and 7 Mod Compatibility:** Seamlessly run mods built on either the Ren'Py 6 or 7 engine! 17 | > [!NOTE] 18 | > Some Ren'Py 6 mods may not run on Ren'Py 7. If this is the case, install **Doki Doki Mod Docker (SE)**. 19 | - **Mod Installation:** Easily install new mods directly within Mod Docker. 20 | - **Multiple Mod Storage:** Manage as many mods you want to play within a single app! 21 | - **Separate Saves:** Maintain independent save data for each mod (and copies of said mod)! 22 | - **Custom Background:** Personalize the Mod Docker interface with a custom background image (instructions below). 23 | - **[Beta] Auto `scripts.rpa` Removal:** Streamlines the experience for select mods that require the removal of `scripts.rpa`. 24 | - **[Alpha] Auto MAS Template Fixes:** Ensures compatibility for mods built with the Monika After Story template (rather than the Bronya Rand 2.0 Template). 25 | 26 | **Custom Background Instructions** 27 | 1. Place a 16:9 image into Mod Docker's `game` directory. 28 | 2. Name the image `docker_custom_image`. 29 | 30 | ## Installation 31 | > [!IMPORTANT] 32 | > **For macOS Users:** These steps require you to access the directory within the Mod Docker app. Right-click the Mod Docker app, select *Show Package Contents*, then navigate to `Contents/Resources/autorun`. 33 | 34 | 1. Download the latest version of Mod Docker [here](https://github.com/Bronya-Rand/DDModDocker/releases). 35 | 2. Extract Mod Docker to a location of your choice. 36 | > [!CAUTION] 37 | > Avoid installing directly over DDLC 38 | 3. Download DDLC's PC ZIP from [ddlc.moe](https://ddlc.moe) and extract the ZIP file. 39 | 4. Locate the *DDLC-X.X.X-pc/game* folder and copy the following files to Mod Docker's `game` folder: 40 | - `audio.rpa` 41 | - `fonts.rpa` 42 | - `images.rpa` 43 | - `scripts.rpa` 44 | 5. Create a `mods` folder within Mod Docker's `game` folder. 45 | 6. Inside the `mods` folder, create a subfolder for each mod you wish to install 46 | > [!TIP] 47 | > Suggested Mod Folder Naming Scheme: Mod's Full Name or Acronym 48 | 7. Copy the `game` folder from the desired mod into its respective subfolder within the `mods` folder. 49 | > [!IMPORTANT] 50 | > If the mod lacks a `game` folder, create one inside the mod's subfolder and place all mod files (RPAs, RPYCs, etc.) within. 51 | 8. Launch Mod Docker using `DDMD.exe` (Windows), `DDMD` (macOS), or `DDMD.sh` (Linux). 52 | 9. Press F9 to access the Mod Docker menu. Select your mod and click *Select*. 53 | 10. Restart the game and relaunch Mod Docker. 54 | 55 | ## Why Mod Docker? 56 | 57 | - Effortless Mod Management 58 | > Play multiple DDLC mods within a single application! Eliminate the hassle and disk space consumption of separate game copies or complex manual installations. 59 | - Streamlined Play Experience 60 | > Enjoy mods (or multiple copies of the same mod) as if they have been freshly installed! 61 | - Developed with Players and Modders in Mind 62 | > Mod Docker prioritizes ease of use, recognizing the passion both players and mod creators have for the DDLC community. Its design aims to elevate the modding experience for everyone. 63 | 64 | ## What inspired Mod Docker? 65 | 66 | Mod Docker draws inspiration from the earlier vision of the Doki Doki Mod Launcher (DDML), conceived in 2018. While limitations existed in DDML's initial implementation, Mod Docker revisits that core concept, refining it for practicality and ease of use. It draws on the modularity principles of [Docker](https://docker.com) to create a streamlined solution that aligns with standard modding practices. 67 | 68 | ## How does a 'mod container' work? 69 | 70 |

71 | A diagram of how mod container works 72 |

73 | 74 | Think of a mod container as a self-contained space within Mod Docker. It houses all the necessary files for a specific mod, including RPAs, RPYC/RPY's, and folders like mod_assets. When you select a mod, Mod Docker loads only the files from its dedicated container. 75 | 76 | ## Comparing Mod Docker to Other Mod Launchers/Managers 77 | 78 | Mod Docker takes a unique approach to mod management compared to other mod managers/launchers. Here's a breakdown of key differences: 79 | 80 |

81 | A diagram comparing Mod Docker to Doki Doki Mod Launcher/Mod Manager and Standard Installs 82 |

83 | 84 | ### Mod Docker 85 | - Self-Contained Mods: Each mod runs in its own dedicated folder, ensuring complete isolation and conflicts. 86 | - Custom Ren'Py Engine: Utilizes a specialized Ren'Py build (6 (SE), 7 and 8) to enable seamless mod loading. 87 | - Base Game Independence: Functions without external programs or the base game itself (aside from essential RPAs). 88 | 89 | ### Doki Doki Mod Launcher/Doki Doki Mod Manager 90 | - Centralized Mod Storage: Mods reside within a single directory. 91 | - External Dependencies: Relies on a custom Ren'Py SDK (DDML) or a separate program (DDMM) for execution. 92 | - Shared Save Data: Multiple mod copies can access each other's save data. 93 | - Ren'Py 8 Limitations: DDML has no Ren'Py 8 support and DDMM has incompatibility issues with Ren'Py 8 mods. 94 | - Base Game Reliance: Requires the base game (and mod dependencies) for mod execution. 95 | 96 | ### Standard Install 97 | - Manual Process: Involves manually adding mod files to the game directory. 98 | - Base Game Reliance: Requires the base game for mod execution. 99 | 100 | Copyright © 2022-2024 Azariel Del Carmen (Bronya-Rand). All rights reserved. Licensed under GNU AGPL-3.0. See [LICENSE](LICENSE) for more information. 101 | -------------------------------------------------------------------------------- /ddmd_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config_gl2": false 3 | } -------------------------------------------------------------------------------- /game/ml_patches.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | init -100 python: 4 | import hashlib 5 | 6 | for archive in ['audio','images','fonts','scripts']: 7 | if archive + ".rpa" not in os.listdir(persistent.ddml_basedir + "/game"): 8 | raise Exception("'%s.rpa' was not found in the Mod Docker game folder. Check your installation and try again." % archive) 9 | 10 | ## Some older mods fail to load because for some reason, the checks don't associate `mods/X/game/Y` as True 11 | ## To fix this, we hijack config.archives temporarily to "fake" that we have the right RPY files 12 | ## whilst saving the true archives in another variable. 13 | actual_mod_archive = renpy.config.archives 14 | renpy.config.archives = ['audio','images','fonts','scripts'] 15 | 16 | ## Hash as of DDLC 1.1.1 17 | if hashlib.sha256(open(os.path.join(persistent.ddml_basedir, "game/scripts.rpa"), "rb").read()).hexdigest() != 'da7ba6d3cf9ec1ae666ec29ae07995a65d24cca400cd266e470deb55e03a51d4': 18 | raise Exception("Hash mismatch between the current 'scripts.rpa' file and DDLC 1.1.1's 'scripts.rpa'.\nPlease add DDLC's original 1.1.1 'scripts.rpa' into DDML's game directory.") 19 | 20 | if not os.path.exists(persistent.ddml_basedir + "/characters"): 21 | os.makedirs(persistent.ddml_basedir + "/characters") 22 | if not os.path.exists(persistent.ddml_basedir + "/game/mods"): 23 | os.makedirs(persistent.ddml_basedir + "/game/mods") 24 | 25 | ## After splash checks finishes, we then revert the archive back to what is truly there. 26 | init -99 python: 27 | renpy.config.archives = actual_mod_archive 28 | 29 | init 1 python: 30 | if (config.window_title is None or "Doki Doki Mod Docker (Alpha)" not in config.window_title): 31 | container_name = config.window_title or config.name or config.basedir.replace("\\", "/").split("/")[-1] 32 | config.window_title = _("Doki Doki Mod Docker (Alpha) - Mod Container: ") + container_name 33 | 34 | init python: 35 | if not os.path.exists(config.basedir + "/characters"): 36 | os.makedirs(config.basedir + "/characters") 37 | 38 | init 1 python: 39 | # Addresses the Ren'Py/MAS Updater Issue 40 | store.updater.DEFERRED_UPDATE_FILE = os.path.join(config.basedir, "update", "deferred.txt") 41 | store.updater.DEFERRED_UPDATE_LOG = os.path.join(config.basedir, "update", "log.txt") 42 | 43 | init 1 python: 44 | import ddmd_api 45 | 46 | modDocker_api = ddmd_api.ModDocker_API() 47 | modDocker_api.get_mod_info() 48 | 49 | # Re-write in case of new API updates 50 | if modDocker_api.get_current_container_save_folder().split("/")[-1] != "DDLC": 51 | modDocker_api.write_mod_data() 52 | 53 | init -100 python: 54 | 55 | def patched_file(fn): 56 | import re 57 | basechrs = re.compile(r"^(monika|sayori|yuri|natsuki)\.chr") 58 | 59 | if ".." in fn: 60 | fn = fn.replace("..", config.basedir.replace("\\", "/")) 61 | 62 | # Include CHRs if called from patch RPA 63 | if basechrs.match(fn): 64 | return renpy.loader.load('mod_patches/chrs/' + fn) 65 | return renpy.loader.load(fn) 66 | 67 | renpy.file = patched_file 68 | -------------------------------------------------------------------------------- /game/mod_content.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | image ddmd_toggle_on = "sdc_system/ddmd_app/enabled.png" 4 | image ddmd_toggle_off = "sdc_system/ddmd_app/disabled.png" 5 | image ddmd_toggle_off_hover = "sdc_system/ddmd_app/disabledHover.png" 6 | image ddmd_toggle_on_hover = "sdc_system/ddmd_app/enabledHover.png" 7 | image ddmd_search_icon = "sdc_system/ddmd_app/search.png" 8 | image ddmd_search_icon_hover = "sdc_system/ddmd_app/searchHover.png" 9 | image ddmd_return_icon = "sdc_system/ddmd_app/return.png" 10 | image ddmd_return_icon_hover = "sdc_system/ddmd_app/returnHover.png" 11 | image ddmd_restart_icon = "sdc_system/ddmd_app/restart.png" 12 | image ddmd_restart_icon_hover = "sdc_system/ddmd_app/restartHover.png" 13 | image ddmd_refresh_icon = "sdc_system/ddmd_app/refresh.png" 14 | image ddmd_refresh_icon_hover = "sdc_system/ddmd_app/refreshHover.png" 15 | image ddmd_close_icon = "sdc_system/ddmd_app/close.png" 16 | image ddmd_close_icon_hover = "sdc_system/ddmd_app/closeHover.png" 17 | image ddmd_search_window_icon = "sdc_system/ddmd_app/searchWindow.png" 18 | image ddmd_search_window_icon_hover = "sdc_system/ddmd_app/searchWindowHover.png" 19 | image ddmd_openinbrowser_icon = "sdc_system/ddmd_app/openBrowser.png" 20 | image ddmd_openinbrowser_icon_hover = "sdc_system/ddmd_app/openBrowserHover.png" 21 | image ddmd_selectedmod_icon = "sdc_system/ddmd_app/selectedMod.png" 22 | image ddmd_selectedmod_icon_hover = "sdc_system/ddmd_app/selectedModHover.png" 23 | image ddmd_install_icon = "sdc_system/ddmd_app/modInstall.png" 24 | image ddmd_install_icon_hover = "sdc_system/ddmd_app/modInstallHover.png" 25 | image ddmd_settings_icon = "sdc_system/ddmd_app/settings.png" 26 | image ddmd_settings_icon_hover = "sdc_system/ddmd_app/settingsHover.png" 27 | 28 | image ddmd_file_physical_drive = "sdc_system/file_app/physicalDrive.png" 29 | image ddmd_file_network_drive = "sdc_system/file_app/networkDrive.png" 30 | image ddmd_file_file = "sdc_system/file_app/OSFile.png" 31 | image ddmd_file_folder = "sdc_system/file_app/OSFolder.png" 32 | 33 | image ddmd_transfer_icon = "sdc_system/settings_app/transfer.png" 34 | image ddmd_transfer_icon_hover = "sdc_system/settings_app/transferHover.png" 35 | 36 | image ddmd_time_clock = DynamicDisplayable(ddmd_app.get_current_time) 37 | 38 | default persistent.mod_list_disclaimer_accepted = False 39 | 40 | default persistent.self_extract = None 41 | 42 | default persistent.transfer_warning = False 43 | 44 | default persistent.military_time = False -------------------------------------------------------------------------------- /game/mod_dir_browser.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | init python: 4 | import errno 5 | 6 | def can_access(path, drive=False): 7 | if drive: 8 | if os.name == "nt" and len(path) == 2 and path[1] == ":": 9 | return os.path.isdir(path) 10 | return False 11 | 12 | if os.name == "nt": 13 | try: 14 | os.listdir(path) 15 | return True 16 | except OSError as e: 17 | if e.errno in (errno.EACCES, errno.EPERM) or e.winerror == 59: 18 | return False 19 | raise 20 | else: 21 | return os.access(path, os.R_OK) 22 | 23 | # def get_network_drives(): 24 | # result = subprocess.check_output("net use", shell=True) 25 | # output_lines = result.strip().split('\r\n')[1:] 26 | # drives = [line.split()[1].rstrip(':') for line in output_lines if line.startswith('OK')] 27 | # return drives 28 | 29 | # def get_physical_drives(net_drives): 30 | # temp = subprocess.check_output("powershell (Get-PSDrive -PSProvider FileSystem).Name", shell=True) 31 | # temp = temp.decode("utf-8").strip().split("\r\n") 32 | 33 | # for x in temp[:]: 34 | # if x in net_drives: 35 | # temp.remove(x) 36 | 37 | # return temp 38 | 39 | def get_physical_drives(net_drives): 40 | temp = subprocess.check_output("powershell (Get-PSDrive -PSProvider FileSystem).Name", shell=True) 41 | temp = temp.decode("utf-8").strip().split("\r\n") 42 | return temp 43 | 44 | screen pc_directory(loc=None, ml=False, mac=False): 45 | modal True 46 | 47 | zorder 200 48 | style_prefix "pc_dir" 49 | 50 | python: 51 | title = "" 52 | if ml: 53 | title = _("Select DDML Mod Folder") 54 | elif mac: 55 | title = _("Select DDLC Mod Folder") 56 | else: 57 | title = _("Select DDLC Mod ZIP File") 58 | 59 | use ddmd_generic_window(title): 60 | 61 | python: 62 | if loc and loc != "drive": 63 | current_dir = loc 64 | elif (loc == "drive" and renpy.windows): 65 | current_dir = None 66 | else: 67 | current_dir = persistent.ddml_basedir 68 | 69 | if current_dir is not None: 70 | prev_dir = os.path.dirname(current_dir) 71 | dir_size = len(os.listdir(current_dir)) 72 | else: 73 | #net_drives = get_network_drives() 74 | #drives = get_physical_drives(net_drives) 75 | drives = get_physical_drives() 76 | 77 | if (renpy.windows and loc != "drive") or (not renpy.windows and loc != "/"): 78 | hbox: 79 | xalign 0.02 yoffset 6 80 | imagebutton: 81 | idle Transform("sdc_system/file_app/OSBack.png", size=(int(18 * res_scale), int(18 * res_scale))) 82 | hover Composite((int(18 * res_scale), int(18 * res_scale)), (0, 0), Frame("#dbdbdd"), (0, 0), Transform("sdc_system/file_app/OSBack.png", size=(int(18 * res_scale), int(18 * res_scale)))) 83 | action [Hide("pc_directory"), Show("pc_directory", loc=If(current_dir != prev_dir, prev_dir, "drive"), ml=ml, mac=mac)] 84 | 85 | side "c r": 86 | yoffset int(45 * res_scale) 87 | xoffset int(5 * res_scale) 88 | xsize int(470 * res_scale) 89 | ysize int(250 * res_scale) 90 | 91 | viewport id "fe": 92 | #spacing 2 93 | mousewheel True 94 | has vbox 95 | 96 | if current_dir is None: 97 | for x in drives: 98 | hbox: 99 | imagebutton: 100 | idle Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Transform("ddmd_file_physical_drive", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), int(2 * res_scale)), Text(x + ":/", substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 101 | hover Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Frame("#dbdbdd"), (0, 0), Transform("ddmd_file_physical_drive", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), int(2 * res_scale)), Text(x + ":/", substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 102 | action If(can_access(x + ":", True), [Show("pc_directory", loc=x + ":/", ml=ml, mac=mac)], Show("ddmd_dialog", message="You do not have permission to access %s." % (x + ":/"))) 103 | # for x in net_drives: 104 | # hbox: 105 | # imagebutton: 106 | # idle Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Transform("ddmd_file_network_drive", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), 2), Text(x + ":/", substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 107 | # hover Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Frame("#dbdbdd"), (0, 0), Transform("ddmd_file_network_drive", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), int(2 * res_scale)), Text(x + ":/", substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 108 | # action If(can_access(x + ":"), [Show("pc_directory", loc=x + ":/", ml=ml, mac=mac)], Show("ddmd_dialog", message="You do not have permission to access %s." % (x + ":/"))) 109 | else: 110 | for x in os.listdir(current_dir): 111 | if os.path.isdir(os.path.join(current_dir, x)): 112 | hbox: 113 | imagebutton: 114 | idle Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Transform("ddmd_file_folder", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), 2), Text(x, substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 115 | hover Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Frame("#dbdbdd"), (0, 0), Transform("ddmd_file_folder", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), int(2 * res_scale)), Text(x, substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 116 | action If(can_access(os.path.join(current_dir, x)), [Show("pc_directory", loc=os.path.join(current_dir, x), ml=ml, mac=mac)], Show("ddmd_dialog", message="You do not have permission to access %s." % os.path.join(current_dir, x).replace("\\", "/"))) 117 | elif not mac and not ml: 118 | if os.path.join(current_dir, x).endswith(".zip"): 119 | hbox: 120 | imagebutton: 121 | idle Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Transform("ddmd_file_file", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), 2), Text(x, substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 122 | hover Composite((int(460 * res_scale), int(18 * res_scale)), (0, 0), Frame("#dbdbdd"), (0, 0), Transform("ddmd_file_file", size=(int(18 * res_scale), int(18 * res_scale))), (int(20 * res_scale), int(2 * res_scale)), Text(x, substitute=False, size=int(12 * res_scale), style="pc_dir_text")) 123 | action [Hide("pc_directory"), Show("mod_name_input", zipPath=os.path.join(current_dir, x))] 124 | 125 | vbar value YScrollValue("fe") xoffset 20 yoffset 10 ysize int(240 * res_scale) 126 | 127 | if mac or ml: 128 | if (renpy.windows and loc != "drive") or (not renpy.windows and loc != "/"): 129 | textbutton _("Select Current Folder"): 130 | text_style "mods_button_text" 131 | action If(mac, [Hide("pc_directory", Dissolve(0.25)), Show("mod_name_input", zipPath=current_dir, copy=True)], [Hide("pc_directory", Dissolve(0.25)), Function(transfer_data, ddmm_path=current_dir)]) 132 | text_size int(16 * res_scale) 133 | xalign 0.95 yalign 0.98 134 | -------------------------------------------------------------------------------- /game/mod_installer.rpy: -------------------------------------------------------------------------------- 1 | 2 | init python in ddmd_mod_installer: 3 | from store import persistent 4 | from zipfile import ZipFile, BadZipfile 5 | import os 6 | import shutil 7 | import tempfile 8 | 9 | def inInvalidDir(path): 10 | for x in ("lib", "renpy"): 11 | if os.path.normpath(x) in os.path.normpath(path): 12 | return True 13 | if path.endswith(".app"): 14 | return True 15 | return False 16 | 17 | def check_mod_validity(zipPath, copy): 18 | """ 19 | Checks mod validity for mod packages and ZIP files. 20 | 21 | filePath: The direct path to the ZIP file. 22 | Returns: A boolean indicating a valid mod ZIP file. 23 | """ 24 | if not copy: 25 | # ZIP file check 26 | with ZipFile(zipPath, "r") as temp_zip: 27 | zip_contents = temp_zip.namelist() 28 | 29 | mod_files = zip_contents 30 | else: 31 | # Folder check 32 | mod_files = [] 33 | 34 | for mod_src, dirs, files in os.walk(zipPath): 35 | for mod_f in files: 36 | mod_files.append(os.path.join(mod_src, mod_f)) 37 | 38 | # Check if mod files contain Ren'Py files 39 | if any(file.endswith((".rpa", ".rpyc", ".rpy")) for file in mod_files): 40 | return True 41 | return False 42 | 43 | def get_top_level_folder(mod_dir): 44 | """ 45 | Get's the top level folder of a mod archive if available. 46 | 47 | mod_dir: The path to the ZIP temp folder or mod folder package. 48 | Returns: A string of the top level folder or NoneType. 49 | """ 50 | top_level_folder = None 51 | 52 | for entry in os.listdir(mod_dir): 53 | if os.path.isdir(os.path.join(mod_dir, entry)): 54 | top_level_folder = entry 55 | break 56 | 57 | return top_level_folder 58 | 59 | def identify_mod_format(mod_dir): 60 | """ 61 | Identifies the format of the mod package based on its content and structure. 62 | 63 | mod_dir: The path to the mod package directory. 64 | Returns: An integer indicating the format of the mod package. 65 | """ 66 | 67 | # Check for top-level folder 68 | top_level_folder = get_top_level_folder(mod_dir) 69 | 70 | # Standard Format 71 | if top_level_folder is not None: 72 | if os.path.isdir(os.path.join(mod_dir, top_level_folder, "characters")) and os.path.isdir(os.path.join(mod_dir, top_level_folder, "game")): 73 | return 1 74 | elif os.path.isdir(os.path.join(mod_dir, top_level_folder, "game")): 75 | return 2 76 | 77 | # Standard Format without top folder 78 | if os.path.isdir(os.path.join(mod_dir, "characters")) and os.path.isdir(os.path.join(mod_dir, "game")): 79 | return 3 80 | 81 | # Check for game folder and RPAs/RPYC/RPY files 82 | game_dir = os.path.join(mod_dir, "game") 83 | if os.path.isdir(game_dir): 84 | if any(f.endswith((".rpa", ".rpyc", ".rpy")) for f in os.listdir(game_dir)): 85 | return 5 86 | else: 87 | return 4 88 | 89 | # Check for RPAs without game folder 90 | if any(f.endswith(".rpa") for f in os.listdir(mod_dir)): 91 | return 6 92 | 93 | # Check for RPYC/RPY files without game folder 94 | mod_assets_dir = os.path.join(mod_dir, "mod_assets") 95 | if os.path.isdir(mod_assets_dir) or any(f.endswith((".rpyc", ".rpy")) for f in os.listdir(mod_dir)): 96 | return 7 97 | 98 | # Unknown Format 99 | return -1 100 | 101 | def extract_mod_from_zip(zipPath): 102 | """ 103 | Extracts a Mod ZIP package to a temporary folder 104 | 105 | mod_dir: The path to the mod package directory. 106 | Returns: A string path to the temp folder or NoneType 107 | """ 108 | mod_dir = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempArchive") 109 | 110 | try: 111 | with ZipFile(zipPath, "r") as tempzip: 112 | tempzip.extractall(mod_dir) 113 | return mod_dir 114 | except BadZipFile: 115 | shutil.rmtree(mod_dir) 116 | return None 117 | 118 | def move_mod_files(mod_folder_path, mod_dir_path, mod_format_id, copy): 119 | """ 120 | Moves or copies the mod files from the mod directory or mod package folder to the mod folder. 121 | 122 | mod_folder_path: Path to the mod folder. 123 | mod_dir_path: Path to the mod directory or mod package folder. 124 | mod_format_id: Integer value indicating the mod format ID. 125 | copy: Boolean value indicating whether to copy the files (macOS only). 126 | """ 127 | 128 | if mod_format_id in (1, 2, 3, 4, 5): 129 | characters_dir = os.path.join(mod_dir_path, "characters") 130 | game_dir = os.path.join(mod_dir_path, "game") 131 | 132 | # Copy or move characters folder (if applicable) 133 | if os.path.isdir(characters_dir): 134 | if copy: 135 | shutil.copytree(characters_dir, mod_folder_path) 136 | else: 137 | shutil.move(characters_dir, mod_folder_path) 138 | 139 | # Copy or move game folder to mod_folder_path 140 | if copy: 141 | shutil.copytree(game_dir, mod_folder_path) 142 | else: 143 | shutil.move(game_dir, mod_folder_path) 144 | 145 | elif mod_format_id in (6, 7): 146 | game_folder_path = os.path.join(mod_folder_path, "game") 147 | 148 | # Create game folder if it doesn't exist 149 | if not os.path.exists(game_folder_path): 150 | os.makedirs(game_folder_path) 151 | 152 | # Move all files from mod_dir_path to game folder in mod_folder_path 153 | for entry in os.listdir(mod_dir_path): 154 | entry_path = os.path.join(mod_dir_path, entry) 155 | 156 | if os.path.isfile(entry_path): 157 | shutil.move(entry_path, game_folder_path) 158 | elif os.path.isdir(entry_path): 159 | shutil.move(entry_path, os.path.join(game_folder_path, entry)) 160 | 161 | def install_mod(zipPath, modFolderName, copy=False): 162 | if not modFolderName: 163 | renpy.show_screen("ddmd_dialog", message="Error: The folder name cannot be blank.") 164 | return 165 | elif modFolderName.lower() in ("ddlc mode", "stock mode", "ddlc", "stock"): 166 | renpy.show_screen("ddmd_dialog", message="Error: %s is a reserved folder name. Please try another folder name." % modFolderName) 167 | return 168 | elif os.path.exists(os.path.join(persistent.ddml_basedir, "game/mods/" + modFolderName)): 169 | renpy.show_screen("ddmd_dialog", message="Error: This mod folder already exists. Please try another folder name.") 170 | return 171 | else: 172 | renpy.show_screen("ddmd_progress", message="Installing mod. Please wait.") 173 | folderPath = os.path.join(persistent.ddml_basedir, "game/mods", modFolderName) 174 | try: 175 | if not check_mod_validity(zipPath, copy): 176 | raise Exception("Given file/folder is an invalid DDLC Mod Package. Please select a different file/folder.") 177 | 178 | if not copy: 179 | mod_dir = extract_mod_from_zip(zipPath) 180 | if mod_dir is None: 181 | raise Exception("Invalid mod structure. Please select a different ZIP file.") 182 | else: 183 | mod_dir = zipPath 184 | 185 | mod_format_id = identify_mod_format(mod_dir) 186 | print("Mod Format ID: %d" % mod_format_id) 187 | if mod_format_id == -1: 188 | raise Exception("Mod is packaged in a way that is unknown to DDMD. Re-download the mod that follows a proper DDLC mod package standard or contact 'bronya_rand' with the mod in question.") 189 | 190 | os.makedirs(folderPath) 191 | 192 | if mod_format_id in (1, 2): 193 | top_level_folder = get_top_level_folder(mod_dir) 194 | mod_dir_path = os.path.join(mod_dir, top_level_folder) 195 | move_mod_files(folderPath, mod_dir_path, mod_format_id, copy) 196 | else: 197 | move_mod_files(folderPath, mod_dir, mod_format_id, copy) 198 | 199 | renpy.hide_screen("ddmd_progress") 200 | renpy.show_screen("ddmd_dialog", message="%s has been installed successfully." % modFolderName) 201 | modFolderName = "" 202 | except BadZipfile: 203 | renpy.hide_screen("ddmd_progress") 204 | renpy.show_screen("ddmd_dialog", message="Error: Invalid ZIP file. Please select a different ZIP file.") 205 | except OSError as err: 206 | if os.path.exists(folderPath): 207 | shutil.rmtree(folderPath) 208 | renpy.hide_screen("ddmd_progress") 209 | renpy.show_screen("ddmd_dialog", message="An error has occured during installation.", message2=str(err)) 210 | except Exception as err: 211 | if os.path.exists(folderPath): 212 | shutil.rmtree(folderPath) 213 | renpy.hide_screen("ddmd_progress") 214 | renpy.show_screen("ddmd_dialog", message="An unexpected error has occured during installation.", message2=str(err)) -------------------------------------------------------------------------------- /game/mod_list.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | screen mod_list(search=""): 4 | zorder 101 5 | style_prefix "modList" 6 | 7 | use ddmd_generic_window("Mod List", allow_search=True): 8 | 9 | side "c": 10 | xpos 0.05 11 | ypos 0.15 12 | xsize int(450 * res_scale) 13 | ysize int(250 * res_scale) 14 | spacing 10 15 | 16 | python: 17 | ddmc_json = get_ddmc_modlist() 18 | 19 | viewport id "mlv": 20 | mousewheel True 21 | draggable True 22 | has vbox 23 | spacing 3 24 | 25 | for x in ddmc_json: 26 | if not search: 27 | if x["modShow"] and x["modNSFW"]: 28 | textbutton "(NSFW) " + x["modName"].replace("[", "[[").replace("]", "]]"): 29 | action Show("mod_list_info", Dissolve(0.25), mod=x) 30 | elif x["modShow"]: 31 | textbutton x["modName"].replace("[", "[[").replace("]", "]]"): 32 | action Show("mod_list_info", Dissolve(0.25), mod=x) 33 | else: 34 | if search.lower() in x["modName"].lower() or search.lower() in x["modSearch"]: 35 | if x["modShow"] and x["modNSFW"]: 36 | textbutton "(NSFW) " + x["modName"].replace("[", "[[").replace("]", "]]"): 37 | action Show("mod_list_info", Dissolve(0.25), mod=x) 38 | elif x["modShow"]: 39 | textbutton x["modName"].replace("[", "[[").replace("]", "]]"): 40 | action Show("mod_list_info", Dissolve(0.25), mod=x) 41 | 42 | init python: 43 | def search_script(msc): 44 | renpy.show_screen("mod_list", search=msc) 45 | 46 | screen mod_search(xs=480, ys=220): 47 | modal True 48 | zorder 200 49 | 50 | style_prefix "ddmd_confirm" 51 | 52 | use ddmd_generic_notif(xs, ys): 53 | 54 | vbox: 55 | xalign .5 56 | yalign .5 57 | spacing 8 58 | default modSearchCriteria = "" 59 | 60 | label _("Search For?"): 61 | text_size 20 62 | xalign 0.5 63 | 64 | input: 65 | value ScreenVariableInputValue("modSearchCriteria") 66 | length 24 67 | allow "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz[[]] " 68 | copypaste True 69 | 70 | hbox: 71 | xalign 0.5 72 | spacing 100 73 | 74 | textbutton _("OK") action [Hide("mod_search", Dissolve(0.25)), Function(search_script, modSearchCriteria)] 75 | textbutton _("Clear") action SetScreenVariable("modSearchCriteria", "") 76 | 77 | screen mod_list_info(mod): 78 | zorder 102 79 | style_prefix "modInfo" 80 | 81 | use ddmd_generic_window("Mod Info", 800, 550): 82 | 83 | side "c": 84 | xpos 0.05 85 | ypos 0.11 86 | xsize int(740 * res_scale) 87 | ysize int(420 * res_scale) 88 | 89 | viewport id "mlv": 90 | mousewheel True 91 | draggable True 92 | has vbox 93 | 94 | text mod["modName"].replace("[", "[["): 95 | style "mods_label_text" 96 | size int(22 * res_scale) 97 | 98 | python: 99 | mod_release_date = datetime.datetime.strptime(mod['modDate'].replace(" ", "T"), "%Y-%m-%dT%H:%M:%S.%f") 100 | mrd = mod_release_date.strftime("%d %B %Y") 101 | 102 | if mod["modNSFW"]: 103 | text _("{b}This mod is marked as Not Safe For Work{/b}") size int(20 * res_scale) 104 | text _("Released: ") + mrd size int(20 * res_scale) 105 | text _("Status: ") + mod["modStatus"] size int(20 * res_scale) 106 | 107 | python: 108 | playTime = _("Playtime: {u}") 109 | 110 | if not mod["modPlayTimeHours"] and not mod["modPlayTimeMinutes"]: 111 | playTime += _("Unknown") 112 | 113 | if mod["modPlayTimeHours"]: 114 | playTime += str(mod["modPlayTimeHours"]) + _(" hour") 115 | 116 | if mod["modPlayTimeHours"] > 1: 117 | playTime += "s" 118 | 119 | if mod["modPlayTimeMinutes"]: 120 | 121 | if mod["modPlayTimeHours"] > 1: 122 | playTime += " " 123 | playTime += str(mod["modPlayTimeMinutes"]) + _(" minute") 124 | 125 | if mod["modPlayTimeMinutes"] > 1: 126 | playTime += "s" 127 | 128 | text playTime + "{/u}" size int(20 * res_scale) 129 | 130 | null height 20 131 | 132 | text _("{b}Description{/b}") size int(20 * res_scale) 133 | text mod["modDescription"].replace("[", "[[") size int(20 * res_scale) 134 | 135 | imagebutton: 136 | idle Composite((int(250 * res_scale), int(50 * res_scale)), (0, 0), Transform("ddmd_openinbrowser_icon", size=(int(36 * res_scale), int(36 * res_scale))), (int(40 * res_scale), 0), Text(_("Download Page"), style="mods_text")) 137 | hover Composite((int(250 * res_scale), int(50 * res_scale)), (0, 0), Transform("ddmd_openinbrowser_icon_hover", size=(int(36 * res_scale), int(36 * res_scale))), (int(40 * res_scale), 0), Text(_("Download Page"), style="mods_button_text")) 138 | xalign 0.95 139 | yalign 0.98 140 | action OpenURL(mod['modUploadURL']) 141 | -------------------------------------------------------------------------------- /game/mod_patches/chrs/monika.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/mod_patches/chrs/monika.chr -------------------------------------------------------------------------------- /game/mod_patches/chrs/natsuki.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/mod_patches/chrs/natsuki.chr -------------------------------------------------------------------------------- /game/mod_patches/chrs/sayori.chr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/mod_patches/chrs/sayori.chr -------------------------------------------------------------------------------- /game/mod_patches/chrs/yuri.chr: -------------------------------------------------------------------------------- 1 | SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0LCB0aGVuICpjb25ncmF0dWxhdGlvbnMhKiBZb3UgYXJlIHByb2JhYmx5IHRoZSBmaXJzdCBwZXJzb24gdG8gcmVhZCB0aGlzLiBJIGRpZG7igJl0IHJlYWxseSBwbGFuIG9uIHNoYXJpbmcgdGhpcyB3aXRoIGFueWJvZHksIGJ1dCBmb3Igc29tZSByZWFzb24gSSB0aGluayBpdOKAmXMgZXhjaXRpbmcgdGhhdCBzb21lYm9keSBvdXQgdGhlcmUsIGEgY29tcGxldGUgc3RyYW5nZXIsIHdpbGwgY29tZSBhY3Jvc3MgdGhpcyBub3RlIGFuZCByZWFkIG15IHN0b3J5LiBTb21lb25lIEkgd2lsbCBuZXZlciBtZWV0LCBzaGFyaW5nIHN1Y2ggYSBwZXJzb25hbCBib25kIHdpdGggbWUuIEnigJltIGZhc2NpbmF0ZWQgdGhhdCBlaXRoZXIgb25lIG9mIHVzIGNvdWxkIGRpZSAtIGV2ZW4gYXMgc29vbiBhcyB0b21vcnJvdyAtIHdpdGggdGhlIG90aGVyIGJlaW5nIGNvbXBsZXRlbHkgY2x1ZWxlc3MgdG8gdGhlIGZhY3QuIFRvIHlvdSwgbXkgZW50aXJlIGxpZmUgaXMgd2l0aGluIHRoaXMgbm90ZSwgYW5kIHNvIEkgd2lsbCBsaXZlIGZvciBhcyBsb25nIGFzIHlvdXIgbWVtb3J5IGNhbiBjYXJyeSBtZS4gV3JpdGluZyB0aGlzLCBJ4oCZbSB3b25kZXJpbmcgaWYgdGhhdCBtYWtlcyB5b3UgZmVlbCBmYXNjaW5hdGVkIG9yIHZpb2xhdGVkLiBJdOKAmXMgc28gZXhjaXRpbmcuDQoNCknigJltIHNvcnJ5IGlmIG15IHN0b3J5IGlzIGEgYml0IGRpc29yZ2FuaXplZCwgYnV0IEnigJlkIGxpa2UgdG8gZ2V0IGl0IGRvd24gd2hpbGUgaXTigJlzIHN0aWxsIGZyZXNoIG9uIG15IG1pbmQuIEZpcnN0LCBJ4oCZbGwgdGVsbCB5b3UgYSBsaXR0bGUgYml0IGFib3V0IG15c2VsZi4gSeKAmW0gYSBmaXJzdC15ZWFyIGNvbGxlZ2UgZ2lybCBhbmQgaGF2ZSBsZWQsIGJ5IG1vc3Qgc3RhbmRhcmRzLCBhIHByZXR0eSB1bnNwZWN0YWN1bGFyIGxpZmUgdXAgdG8gdGhpcyBwb2ludC4gSSBncmV3IHVwIGluIGFuIHVwcGVyLW1pZGRsZSBjbGFzcyBzY2hvb2wgZGlzdHJpY3Qgd2l0aCBkZWNlbnQgdGVhY2hlcnMuIEkgZGlkIHRyYWNrIGluIG1pZGRsZSBzY2hvb2wgYW5kIHNvbWUgb2YgaGlnaCBzY2hvb2wsIGFuZCBJ4oCZdmUgaGFkIHR3byBib3lmcmllbmRzLiBOb3csIEnigJltIHN0dWR5aW5nIGZvciBhIGNhcmVlciBpbiBvY2N1cGF0aW9uYWwgdGhlcmFweSwgYmVjYXVzZSBJIGZlZWwgdGhlIGZpZWxkIGlzIHVuZGVydmFsdWVkIGFuZCBwcm92aWRlcyB0cmVtZW5kb3VzIGhlbHAgdG8gcGVvcGxlLg0KDQpJ4oCZbSBnaXZpbmcgeW91IHRoaXMgYmFja2dyb3VuZCBiZWNhdXNlIHRoZXJl4oCZcyB0aGlzIHN0cmFuZ2UgbWlzY29uY2VwdGlvbiB0aGF0IGlmIHlvdSB3YW50IHRvIGtpbGwgc29tZW9uZSB0aGVuIHlvdeKAmXJlIGVpdGhlciBzaWNrIGluIHRoZSBoZWFkIG9yIHlvdSBoYXZlIGFuZ2VyIG1hbmFnZW1lbnQgaXNzdWVzLiBCdXQsIGl04oCZcyB2ZXJ5IGFwcGFyZW50IHRoYXQgSSBkb27igJl0IGZhbGwgaW50byBlaXRoZXIgb2YgdGhvc2UgY2F0ZWdvcmllcy4gSXTigJlzIHRydWUgdGhhdCBtb3N0IG11cmRlciBjYXNlcyBhcmUgaW4gYSBkb21lc3RpYyBzZXR0aW5nIHdoZXJlIHNvbWVvbmUgbG9zZXMgY29udHJvbCBvZiB0aGVpciBhbmdlciBvciBzb21ldGhpbmcuIEJ1dCB0aGUgdGhpbmcgaXMgdGhhdCB0aG9zZSBwZW9wbGUga2lsbCB1bmRlciBwcm92b2NhdGlvbiwgd2hldGhlciBieSBhIHNpbmd1bGFyIG91dGJ1cnN0IG9yIGJ5IGEgc2xvdy1idXJuaW5nIHNlcmllcyBvZiBtaXNmb3J0dW5lcy4gVGhvc2UgcGVvcGxlIGtpbGwgYmVjYXVzZSBpbiB0aGF0IGJyaWVmIG1vbWVudCwgdGhleSB3YW50IGEgc3BlY2lmaWMgc29tZW9uZSwgZm9yIGEgc3BlY2lmaWMgcmVhc29uLCB0byBiZSBodXJ0IG9yIGtpbGxlZC4NCg0KV2hhdCBJ4oCZbSB0YWxraW5nIGFib3V0IGlzIHdhbnRpbmcgdG8ga2lsbCBzb21lb25lIGZvciBubyBzcGVjaWZpYyByZWFzb24sIG1heWJlIGp1c3QgdG8gc2VlIHdoYXQgaXTigJlzIGxpa2UuIERvIHlvdSBldmVyIGdldCB0aGF0PyBJIHdvdWxkbuKAmXQga25vdyBob3cgb3RoZXJzIGZlZWwsIGJlY2F1c2UgaXTigJlzIG5vdCBzb21ldGhpbmcgSSBldmVyIHRhbGtlZCBhYm91dC4gQnV0IEnigJl2ZSBiZWVuIGN1cmlvdXMgYWJvdXQgd2hhdCBpdOKAmXMgbGlrZSB0byBraWxsIHNvbWVvbmUgZXZlciBzaW5jZSBJIHdhcyBhIGNoaWxkLiBOb3Qga2lsbGluZyBhbnlvbmUgaW4gcGFydGljdWxhciwganVzdCBhIHJhbmRvbSBwZXJzb24uIEl04oCZcyBhbHdheXMganVzdCBmYXNjaW5hdGVkIG1lIHRoYXQgaWYgSSBwdXQgbXkgbWluZCB0byBpdCwgSSBjYW4gYXBwcm9hY2ggYW55b25lLCBhbmQgaW4gZml2ZSBtaW51dGVzIHRoZXkgd291bGQgYmUgY29tcGxldGVseSBnb25lIGZyb20gdGhpcyBFYXJ0aC4NCg0KQnV0IEnigJl2ZSBuZXZlciBkb25lIHNvIGZvciBhIGNvdXBsZSBvZiByZWFzb25zLiBGaXJzdCBvZiBhbGwsIGZvciBtb3N0IG9mIG15IGxpZmUgaXQgd2FzIGxvZ2lzdGljYWxseSBpbXBvc3NpYmxlIGZvciBtZSB0byBkbyBpdCB3aXRob3V0IGdldHRpbmcgY2F1Z2h0LiBJIG9ubHkgZ290IG15IGRyaXZlcuKAmXMgbGljZW5zZSBhIGNvdXBsZSB5ZWFycyBhZ28sIGFuZCBldmVuIHRoZW4sIHRoZSBwcmVwYXJhdGlvbnMgd291bGQgdGFrZSB0b28gbXVjaCB0aW1lLCBkZWZpbml0ZWx5IHN0aXJyaW5nIHN1c3BpY2lvbi4gSXQgd2FzIG9ubHkgb25jZSBJIHN0YXJ0ZWQgY29sbGVnZSB0aGF0IEkgcmVhbGl6ZWQgdGhpcyB3YXMgbm8gbG9uZ2VyIGFuIG9ic3RhY2xlLg0KDQpBbm90aGVyIHJlYXNvbiBpcyB0aGF0IEkgd2FzIGFmcmFpZCBvZiBjYXVzaW5nIGhhcm0gdG8gdG9vIG1hbnkgcGVvcGxlLiBZb3UgbWlnaHQgbGF1Z2ggcmVhZGluZyB0aGF0LCBhdCBob3cgaHlwb2NyaXRpY2FsIGl0IHNvdW5kcy4gQnV0LCBsZXQgbWUgZXhwbGFpbjogV2h5IHNob3VsZCBJIGZlZWwgYmFkIGFib3V0IGtpbGxpbmcgc29tZW9uZSBpZiB0aGV54oCZcmUgdG9vIGRlYWQgdG8gY2FyZT8gV2hvIHdvdWxkIEkgYmUgZmVlbGluZyBiYWQgZm9yPyBDb250cmFyaWx5LCBpdOKAmXMgdGhlIGdyaWVmIG9mIHRoZSBsaXZpbmcgdGhhdCBJ4oCZZCByYXRoZXIgbm90IGJlIHJlc3BvbnNpYmxlIGZvci4gQmVjYXVzZSBvZiB0aGlzLCBJIGtuZXcgaXQgd291bGQgdGFrZSBhIGdvb2QgZGVhbCBvZiByZXNlYXJjaCBiZWZvcmUgZmluZGluZyBhIHN1aXRhYmxlIHBlcnNvbiB0byBraWxsLCBhbmQgSeKAmXZlIG5ldmVyIGhhZCB0aGUgbWVhbnMgdG8gZG8gc28gLSBhZ2FpbiwgdW50aWwgSSBzdGFydGVkIGNvbGxlZ2UuDQoNCkFuZCBub3csIGhhdmluZyBqdXN0IGV4cGVyaWVuY2VkIGl0LCBJ4oCZZCBzYXkgaXQgd2FzIHByZXR0eSBzYXRpc2Z5aW5nIGluIHRoZSBlbmQuIFNvbWV0aGluZyBJIHdvdWxkIHRyeSBhZ2Fpbj8gUHJvYmFibHkgbm90LCBzaW5jZSBteSBjdXJpb3NpdHkgaGFzIGFscmVhZHkgYmVlbiBzYXRpc2ZpZWQuIEl0IHJlYWxseSB3b3VsZG7igJl0IGJlIHRoZSBzYW1lIGEgc2Vjb25kIHRpbWUuDQoNCkJ1dCBhbnl3YXksIGlmIGJ5IGFueSBjaGFuY2UgeW914oCZcmUgYWxzbyBjdXJpb3VzIHRvIGtpbGwgc29tZW9uZSwgdGhlbiB5b3XigJlyZSB3ZWxjb21lIHRvIHRha2Ugbm90ZXMuIDopDQoNCioqKg0KDQpJIHN0YXJ0ZWQgYSBob2JieSBvZiBwZW9wbGUtd2F0Y2hpbmcgc29vbiBhZnRlciBJIGVudGVyZWQgY29sbGVnZS4gUGVvcGxlLXdhdGNoaW5nIGlzIGludGVyZXN0aW5nIHRvIG1lIGJlY2F1c2UgaXTigJlzIHRha2luZyBvbmUgb2YgdGhlIGluZmluaXRlIGV4dHJhcyBpbiB5b3VyIGxpZmUgYW5kIHR1cm5pbmcgdGhlbSBpbnRvIGEgbWFpbiBjaGFyYWN0ZXIgLSB3aXRob3V0IHRoZW0ga25vd2luZywgb2YgY291cnNlLiBJdOKAmXMgc28gZWFzeSB0byBmb3JnZXQgdGhhdCBldmVyeSBzaW5nbGUgb25lIG9mIHRoZSBodW5kcmVkcyBvZiBzdHJhbmdlcnMgeW91IHBhc3MgZXZlcnkgZGF5IGhhcyBhIGxpZmUgc3RvcnkgYXMgZGVlcCBhbmQgY29tcGxleCBhcyB5b3VyIG93bi4gT25lIHRoaW5nIEkgbm90aWNlZCBhYm91dCBwZW9wbGUtd2F0Y2hpbmcsIGFuZCB3YW50aW5nIHRvIGtpbGwgc29tZW9uZSwgaXMgdGhhdCB5b3UgYXJlIGluIG1vcmUgY29uc3RhbnQgYXdhcmVuZXNzIG9mIHRoaXMuIFdoZW4gSSBmaW5kIGEgcGVyc29uIHRvIG9ic2VydmUsIHRoZWlyIHN0b3J5IHNsb3dseSBiZWNvbWVzIG1vcmUgY2xlYXIgdG8gbWUgb3ZlciB0aW1lLCBnYXBzIGJlaW5nIGZpbGxlZCAtIGl0IHJlYWxseSBpcyBhbWF6aW5nLg0KDQpJIHVzdWFsbHkgd2VudCB0byBncm9jZXJ5IHN0b3JlcyBvbiB3ZWVrZW5kcyBhbmQgbG9va2VkIGFyb3VuZCBpbiBwZW9wbGXigJlzIHNob3BwaW5nIGNhcnRzLiBJZiBJIHNhdyBzb21ldGhpbmcgdGhhdCBpbnRlcmVzdGVkIG1lLCBJIGRlY2lkZWQgdG8gb2JzZXJ2ZSB0aGUgcGVyc29uIGZvciBhIGxpdHRsZSBiaXQuIE9mIGNvdXJzZSwgc2luY2UgbXkgZ29hbCB3YXMgdG8gZmluZCBzb21lb25lIHRvIGtpbGwsIEkgcnVsZWQgb3V0IGFueW9uZSB3aG8gaGFkIGNoaWxkcmVuIG9yIGEgcGFydG5lciB3aXRoIHRoZW0uIFdlZGRpbmcgcmluZ3Mgd2VyZSBhbm90aGVyIHRlbGwtdGFsZSBzaWduLg0KDQpTbyBtYXliZSBvbmNlIGEgd2Vla2VuZCwgSSB3b3VsZCBmaW5kIHNvbWVvbmUgd2hvIGZpdCBteSBjcml0ZXJpYSwgYXQgd2hpY2ggcG9pbnQgSSB3b3VsZCBmb2xsb3cgdGhlbSBob21lIGFuZCBub3RlIHRoZWlyIGFkZHJlc3MuIEZyb20gdGhlcmUsIGl0IGJlY2FtZSBpbmNyZWRpYmx5IGVhc3kgdG8gaW52ZXN0aWdhdGUgYSBsaXR0bGUgYml0IG1vcmU7IG1vc3QgcGVvcGxlIGhhdmUgbm9ybWFsIHdvcmsgaG91cnMsIG1lYW5pbmcgSSBjb3VsZCBzcGVuZCBhZnRlcm5vb25zIGdvaW5nIHRocm91Z2ggdGhlaXIgbWFpbCBvciBsb29raW5nIGFyb3VuZCBpbiB0aGVpciBob3VzZS4gSSByZXBlYXRlZCB0aGlzIHdpdGggc2V2ZXJhbCBwZW9wbGUgKGFuZCBoYWQgb25lIGNsb3NlIGNhbGwpLCBidXQgZm9yIHZhcnlpbmcgcmVhc29ucyBJIGRpZG7igJl0IHJlYWxseSBmZWVsIHNhdGlzZmllZCBlbm91Z2ggd2l0aCB0aGVtIHRvIGtpbGwgYW55IG9mIHRoZW0uDQoNCkkgc3RhcnRlZCBnZXR0aW5nIGEgYml0IGltcGF0aWVudCBhbmQgdGhvdWdodCB0aGF0IEkgbWlnaHQganVzdCBzZXR0bGUgZm9yIGtpbGxpbmcgdGhlIG1hbiBuYW1lZCBEZXZvbiwgZXZlbiB0aG91Z2ggSSBkaWRu4oCZdCByZWFsbHkgd2FudCB0byBraWxsIHNvbWVvbmUgd2VhbHRoeS4gQnV0IHRoZW4sIEkgY2FtZSBhY3Jvc3Mgc29tZW9uZSBuZXcgLSBzb21lb25lIHdobyBqdXN0LCBmZWx0IHBlcmZlY3QuIFRoZSBmZWVsaW5nIG9ubHkgc3RyZW5ndGhlbmVkIGFzIEkgaW52ZXN0aWdhdGVkIGhlciBmdXJ0aGVyLCBhbmQgSSBrbmV3IHRoYXQgc2hlIHdvdWxkIGJlIHRoZSBvbmUgZm9yIG1lIHRvIGtpbGwuDQoNCkEgeW91bmctbG9va2luZyB3b21hbiBJIG1ldCBhdCB0aGUgZ3JvY2VyeSBzdG9yZSwgYXMgcGVyIHVzdWFsLiBTaGUgd2FzIGRvaW5nIHNvbWUgbGlnaHQgc2hvcHBpbmcgd2l0aCBhIGJhc2tldC4gSGVyIGhhaXIgd2FzIHdhdnkgYW5kIGRhcmsgYnJvd24sIHNpdHRpbmcgaW5lbGVnYW50bHkgb24gaGVyIHNsdW1wZWQgc2hvdWxkZXJzIGFuZCBzdXJyb3VuZGluZyBoZXIgdGlyZWQtbG9va2luZyBmYWNlLiBIZXIgYmFyZSBmaW5nZXJzIHRvbGQgbWUgc2hlIG1pZ2h0IGJlIHNpbmdsZSwgYnV0IGJleW9uZCB0aGF0LCBteSBndXQgd2FzIGFsbW9zdCBjZXJ0YWluIG9mIGl0LiBUaGlzIHdvbWFuIGp1c3Qgc2VlbWVkIHNv4oCmcGxhaW4sIHJlYWxseS4gSSBndWVzcyBJIGZlbHQgYSBncmVhdGVyIGFjdWl0eSBmb3IgdGhlIHBlcnNvbmFsIGxpdmVzIG9mIHN0cmFuZ2VycyBldmVyIHNpbmNlIEkgc3RhcnRlZCBteSBwZW9wbGUtd2F0Y2hpbmcuIEJ1dCB0aGUgd2F5IHNoZSBjYXJyaWVkIGhlcnNlbGYsIEkganVzdCBnb3QgdGhlIGZlZWxpbmcgdGhhdCBpZiBzaGUgc3VkZGVubHkgZGllZCwgbm9ib2R5IHdvdWxkIGJlIGFyb3VuZCB0byBtaXNzIGhlci4gT2YgY291cnNlLCBJIHN0aWxsIHdhbnRlZCB0byBpbnZlc3RpZ2F0ZSBoZXIgYSBiaXQuDQoNCkkgZm9sbG93ZWQgbXkgdXN1YWwgcm91dGluZSBvZiBjaGVja2luZyBvdXQgaGVyIHBsYWNlIGR1cmluZyBoZXIgd29yayBob3Vycy4gSSBsZWFybmVkIGltbWVkaWF0ZWx5IGZyb20gaGVyIG1haWwgdGhhdCBoZXIgbmFtZSBpcyBMaW5kYSBXYXRzb24uIExpbmRhIGxpdmVkIGluIGEgcXVpZXQgYXBhcnRtZW50IGNvbXBsZXgsIGhlciBtYWlsYm94IGVhc2lseSBhY2Nlc3NpYmxlIHJpZ2h0IG91dHNpZGUgaGVyIGRvb3IuIEluc3RlYWQgb2YgcXVpY2tseSBzaHVmZmxpbmcgdGhyb3VnaCBpdCwgSSBkZWNpZGVkIEkgY291bGQgdGFrZSBoZXIgbWFpbCBiYWNrIHRvIG15IGRvcm0gYW5kIHJldHVybiBpdCBiZWZvcmUgc2hlIHdhcyBmaW5pc2hlZCB3aXRoIHdvcmsgKHNoZSBvbmx5IGxpdmVkIGFib3V0IDE1IG1pbnV0ZXMgZnJvbSBtZSkuIEkgZGlkIHNvbWUgcmVzZWFyY2ggYW5kIGxlYXJuZWQgaG93IHRvIG9wZW4gYW5kIHJlc2VhbCB0aGUgZW52ZWxvcGVzIHdpdGhvdXQgZGFtYWdpbmcgdGhlbSwgd2hpY2ggdG9vayBzb21lIHRlY2huaXF1ZSBhbG9uZyB3aXRoIGEgaGFpciBkcnllciwgcnViYmluZyBhbGNvaG9sLCBhbmQgUS10aXBzLg0KDQpUaGlzIG1hZGUgaXQgZWFzeSBmb3IgbWUgdG8gbGVhcm4gYSBsaXR0bGUgbW9yZSBhYm91dCBoZXIuIExpbmRhIHdhcyBhIDMzLXllYXItb2xkIHdvbWFuIHdobyB3b3JrZWQgZm9yIGEgc21hbGwgYWNjb3VudGluZyBmaXJtIC0gSeKAmWQgcmF0aGVyIG5vdCBuYW1lIHRoZSBwbGFjZSBvdXRyaWdodC4gSGVyIGJpcnRoZGF5IHdhcyBEZWNlbWJlciAxMXRoIHdoaWNoLCBjb2luY2lkZW50YWxseSwgd2FzIGFwcHJvYWNoaW5nIGluIGEgY291cGxlIHdlZWtzLiBJIGFsc28gbWFuYWdlZCB0byBmaW5kIGEgYmFuayBzdGF0ZW1lbnQgdGhhdCBnYXZlIG1lIGEgbmljZSBsb29rIGludG8gaG93IHNoZeKAmXMgYmVlbiBzcGVuZGluZyBoZXIgcGFzdCBtb250aC4gSXQgd2FzIGF0IHRoaXMgcG9pbnQgSSByZWFsaXplZCB0aGF0IG15IGFzc2Vzc21lbnQgb2YgTGluZGEgV2F0c29uIGFzIGFuIGV4dHJlbWVseSBwbGFpbiB3b21hbiB3YXMgcHJldHR5IHNwb3Qtb24sIGJlY2F1c2UgdGhlcmUgd2FzIGFic29sdXRlbHkgbm90aGluZyBpbnRlcmVzdGluZyBvbiB0aGUgbGlzdC4gQSB0cmlwIHRvIE9sZCBOYXZ5LCBhIGJ1bmNoIG9mIFN0YXJidWNrcywgc29tZXRoaW5nIGFib3V0ICQ0MCBmcm9tIEFtYXpvbiAtIG5vIHJlc3RhdXJhbnRzLCBubyBtb3ZpZXMsIG5vdGhpbmcgdGhhdCB3b3VsZCByZWFsbHkgaW1wbHkgc2hlIHdhcyBzcGVuZGluZyBhbnkgdGltZSBzb2NpYWxpemluZy4gVGhhdCBhc2lkZSwgSSBhbHNvIGZvdW5kIGEgY29va2luZyBtYWdhemluZSwgc28gSSBndWVzcyBzaGUgd2FzIGludG8gY29va2luZy4NCg0KQXBhcnRtZW50cyBhcmUgaGFyZGVyIHRvIGJyZWFrIGludG8gdGhhbiBzdWJ1cmJhbiBob21lcywgYmVjYXVzZSB0aGVyZSBhcmUgZmV3ZXIgZG9vcnMgYW5kIHdpbmRvd3MuIEV2ZXJ5IHRpbWUgSSBnb3QgTGluZGHigJlzIG1haWwsIEkgd291bGQgY2hlY2sgdGhlIGZyb250IGRvb3IgYW5kIHRoZSB3aW5kb3dzIGluIHRoZSBiYWNrLCBidXQgdGhleSB3ZXJlIGFsd2F5cyBsb2NrZWQuIFRoaXMgd2FzIGEgYml0IGZydXN0cmF0aW5nIGJlY2F1c2UgSSB3YXMgcmVhbGx5IGludGVyZXN0ZWQgaW4gZ2V0dGluZyBpbnRvIGhlciBob3VzZS4gU28sIEkgY2FtZSB1cCB3aXRoIGEgc29ydCBvZiBwbGFuIHRoYXQgSSB0aG91Z2h0IHdvdWxkIGJlIGZ1biwgZXZlbiBpZiBpdCBkaWRu4oCZdCB3b3JrLg0KDQpMYXN0IFNhdHVyZGF5LCBJIHZpc2l0ZWQgTGluZGEgV2F0c29u4oCZcyBhcGFydG1lbnQgY29tcGxleCBhcyBJIHdvdWxkIG9uIHdlZWtkYXlzLiBUaGUgZGlmZmVyZW5jZSBpcyB0aGF0IHRoaXMgdGltZSwgSSB3YW50ZWQgaGVyIHRvIGJlIGhvbWUuIEkgdGhvdWdodCBpdCB3b3VsZCBiZSBpbnRlcmVzdGluZyB0byBoYXZlIGEgY29udmVyc2F0aW9uIHdpdGggaGVyLiBJZiBJIGdvdCBsdWNreSwgSSBjb3VsZCB0YWtlIGFkdmFudGFnZSBvZiB0aGUgc2l0dWF0aW9uIHRvIGRpc2NyZWV0bHkgdW5sb2NrIGEgd2luZG93IGZyb20gdGhlIGluc2lkZS4gU28sIEkgd2Fsa2VkIHVwIHRvIGhlciBkb29yIHdlYXJpbmcgbm90aGluZyB3YXJtZXIgdGhhbiBhIGxpZ2h0IHN3ZWF0c2hpcnQsIGFuZCBrbm9ja2VkLiBUaGUgYWRyZW5hbGluZSBydXNoIHdhcyBjcmF6eS4gSSB3YXMgYWZyYWlkIEkgbWlnaHQgc2NyZXcgc29tZXRoaW5nIHVwLg0KDQpUaGUgZG9vciBvcGVuZWQsIGFuZCBpbiBmcm9udCBvZiBtZSBzdG9vZCBMaW5kYSBXYXRzb24sIGV4YWN0bHkgYXMgSSByZW1lbWJlcmVkIGhlciBmcm9tIHRoZSBncm9jZXJ5IHN0b3JlLiBJdCB3YXMgYXQgdGhhdCBtb21lbnQsIG1ha2luZyBleWUgY29udGFjdCBmb3IgdGhlIGZpcnN0IHRpbWUsIHRoYXQgSSByZWFsaXplZCBJIHdhcyBydW5uaW5nIHRoZSByaXNrIG9mIGJlZ2lubmluZyB0byBjYXJlIGFib3V0IHRoaXMgcGVyc29uLiBBcyBzZWxmaXNoIGFzIGl0IGlzLCBJIGNvdWxkbuKAmXQga2lsbCBhIHBlcnNvbiBJIGNhcmVkIGFib3V0LCBldmVuIGlmIGl04oCZcyBhIDMzLXllYXItb2xkIHdvbWFuIHN0YW5kaW5nIGluIGEgZG9vcndheSB3aXRoIGEgc2xpZ2h0bHkgcGVycGxleGVkIGxvb2sgb24gaGVyIGZhY2UsIGdpdmluZyBtZSBhIHJlc2VydmVkIOKAnEhlbGxvLuKAnQ0KDQpBcm1zIGNyb3NzZWQgZnJvbSB0aGUgY29sZCwgSSBzaHlseSByZXR1cm5lZCBMaW5kYeKAmXMgZ3JlZXRpbmcuIEkgZXhwbGFpbmVkIHRoYXQgSSB3YXMgd2Fsa2luZyBteSBkb2cgbmVhciB0aGUgd29vZHN5IGFyZWEgYmVoaW5kIHRoZSBiYWNrIG9mIGhlciBhcGFydG1lbnQsIGFuZCB0aGF0IGhlIGhhZCBnb3R0ZW4gYXdheS4gSSBoYWQgYmVlbiBsb29raW5nIGZvciBteSBkb2cgZm9yIGFuIGhvdXIgYW5kIHdhcyB3b25kZXJpbmcgaWYgTGluZGEgbWF5IGhhdmUgc2VlbiBoaW0gcm9hbWluZyBhYm91dC4gT2YgY291cnNlLCBMaW5kYSBzeW1wYXRoZXRpY2FsbHkgYXBvbG9naXplZCBmb3IgdGhlIHNpdHVhdGlvbiBhbmQgdGhhdCBzaGUgY291bGRu4oCZdCBiZSBvZiB1c2UgdG8gbWUsIGJ1dCB0aGF0IHNoZSB3b3VsZCBrZWVwIGFuIGV5ZSBvdXQuIEkgd29yZSBhIGRlZmVhdGVkIGV4cHJlc3Npb24gaW4gcmVzcG9uc2UsIGFwb2xvZ2l6aW5nIGluIHJldHVybiBmb3IgdHJvdWJsaW5nIGhlci4NCg0KSXQgc29tZWhvdyB3ZW50IGV4YWN0bHkgYXMgSSBoYWQgaG9wZWQgLSBMaW5kYSBpbnZpdGVkIG1lIGluc2lkZSB0byB3YXJtIHVwIGEgYml0IHdpdGggc29tZSBjb2ZmZWUuIEkgb3V0d2FyZGx5IGhlc2l0YXRlZCBiZWZvcmUgYWNjZXB0aW5nIGhlciBvZmZlciwgYWx0aG91Z2ggb24gdGhlIGluc2lkZSBJIHdhbnRlZCB0byBqdW1wIHRocm91Z2ggdGhlIGRvb3IgYW5kIGh1ZyBoZXIgZm9yIGNvb3BlcmF0aW5nIHNvIHdlbGwuIEFuZCB0aGF04oCZcyBob3cgTGluZGEgV2F0c29uIGVuZGVkIHVwIHdpdGggYSAxOS15ZWFyLW9sZCBnaXJsIG5leHQgdG8gaGVyIG9uIHRoZSBjb3VjaCAtIHdobyBrbm93cyBpZiBpdCB3YXMganVzdCBhIG5pY2UgZ2VzdHVyZSBvciBpZiBzaGUgcmVhbGx5IGhhcyBubyBiZXR0ZXIgd2F5IHRvIHNwZW5kIGhlciBTYXR1cmRheXMgdGhhbiB0YWxraW5nIHRvIHNvbWUga2lkIHNoZSBqdXN0IG1ldCAod2hvIGhhcHBlbnMgdG8gYmUgaW50ZXJlc3RlZCBpbiBraWxsaW5nIGhlcikuDQoNCkxpbmRhIHNvb24gbGVhcm5lZCB0aGF0IG15IG5hbWUgaXMgTWFyaWEgKGl04oCZcyBub3QpIGFuZCB0aGF0IEkgYXR0ZW5kIHRoZSBuZWFyYnkgY29tbXVuaXR5IGNvbGxlZ2UgKEkgZG9u4oCZdCkuIEkgd2FzIGEgbGl0dGxlIGJpdCBuZXJ2b3VzIHRoYXQgc2hlIHdvdWxkIGFzayBtZSB0b28gbWFueSBxdWVzdGlvbnMgYmVjYXVzZSBJIGRpZG7igJl0IGhhdmUgbWFueSBhbnN3ZXJzIHByZXBhcmVkLiBJIHdhcyBhYmxlIHRvIHN0ZWVyIHRoZSBjb252ZXJzYXRpb24gdG93YXJkIGhlciwgYW5kIHNoZSB3YXMgcHJldHR5IGhhcHB5IHRvIHRhbGsuIEkgYXNrZWQgd2hhdCBzaGUgZG9lcywgYW5kIHNoZSB0b2xkIG1lIHRoYXQgc2hlIHdvcmtzIGZvciB0aGUgYWNjb3VudGluZyBmaXJtIEkgYWxyZWFkeSBrbmV3IGFib3V0LCBjb21tdW5pY2F0aW5nIHdpdGggb3V0c2lkZSBjbGllbnRzIGFuZCBrZWVwaW5nIHJlY29yZHMuIEkgdG9sZCBoZXIgSSB3YXMgcHJldHR5IG5lcnZvdXMgYWJvdXQgZ3Jvd2luZyB1cC4gU2hlIHRvbGQgbWUgdG8gZW5qb3kgY29sbGVnZSBhbmQgdG8gbWFrZSBsb3RzIG9mIGZyaWVuZHMgYmVjYXVzZSB0aGVyZeKAmXMgbGVzcyBvcHBvcnR1bml0eSBvbmNlIHlvdSBzdGFydCB3b3JraW5nLg0KDQpXaGVuIEkgYXNrZWQgaWYgc2hlIHdhcyBtYXJyaWVkIG9yIGFueXRoaW5nLCBzaGUgbGF1Z2hlZC4gT2YgY291cnNlIEkga25ldyBzaGUgd2FzbuKAmXQgbWFycmllZCwgYnV0IEkgd2FudGVkIHRvIGhlYXIgbW9yZSBhYm91dCBoZXIgbG92ZSBsaWZlLiBTaGUgc2FpZCB0aGF0IHNoZSBkb2VzbuKAmXQgY3VycmVudGx5IGhhdmUgYSBib3lmcmllbmQgKEkgZ3Vlc3Mgc2hl4oCZcyBhdCBsZWFzdCBoYWQgYm95ZnJpZW5kcywgYnV0IHdobyBrbm93cyBob3cgbG9uZyBhZ28pLiBXaGVuIEkgYXNrZWQgaGVyIGFib3V0IGtpZHMsIHNoZSBzYWlkIHNoZSBkb2VzbuKAmXQgd2FudCB0aGVtIHVudGlsIHNoZSBnZXRzIGEgYmV0dGVyIGpvYi4gT24gdG9wIG9mIHRoYXQsIHNoZSB0b2xkIG1lIHRoYXQgaGVyIGZhbWlseSBoYXMgYSBoaXN0b3J5IG9mIHNvbWUgZ2VuZXRpYyBkaXNlYXNlcyBzdWNoIGFzIGFydGhyaXRpcyBhbmQgZGVwcmVzc2lvbiwgd2hpY2ggc2hlIGlzIGFmcmFpZCB0byBnaXZlIHRvIGhlciBraWRzLg0KDQpJdOKAmXMgZnVubnkgdGhhdCBzaGUgbWVudGlvbmVkIHRoYXQgYmVjYXVzZSB3aGVuIEkgYXNrZWQgdG8gdXNlIGhlciBiYXRocm9vbSwgSSBub3RpY2VkIGEgdHViZSBvZiBwcmVzY3JpcHRpb24gcGlsbHMgb24gdGhlIHNpbmsuIEl0IHdhcyBsYWJlbGxlZCBkdWxveGV0aW5lLCB3aGljaCBJIGxvb2tlZCB1cCBsYXRlciBhbmQgZGlzY292ZXJlZCB0aGF0IGl0IGlzIGluIGZhY3QgYW4gYW50aWRlcHJlc3NhbnQuIEkgaGFkIGEgam9raW5nIHRob3VnaHQgdGhhdCBtYXliZSBieSBraWxsaW5nIGhlciBJ4oCZZCBiZSBkb2luZyBoZXIgYSBmYXZvciwgYnV0IHF1aWNrbHkgZGVjaWRlZCBJIHdhcyBhIHRlcnJpYmxlIHBlcnNvbiBmb3IgY29taW5nIHVwIHdpdGggdGhhdC4NCg0KVGhlIHJlc3Qgb2YgdGhlIHZpc2l0IHdhcyBwcmV0dHkgZHVsbC4gV2UgdGFsa2VkIGFib3V0IGZvb2QgYW5kIHNvbWUgb3RoZXIgbXVuZGFuZSBzdHVmZiBiZWZvcmUgSSBldmVudHVhbGx5IG1hZGUgYW4gZXhjdXNlIHRvIGxlYXZlLiBJIGRpZG7igJl0IGdldCB0aGUgY2hhbmNlIHRvIHVubG9jayBhIHdpbmRvdyBvciBhbnl0aGluZyBsaWtlIHRoYXQsIGJ1dCBJIGRpZG7igJl0IHJlYWxseSBmZWVsIHRoZSBuZWVkIHRvIGdvIHRocm91Z2ggaGVyIGFwYXJ0bWVudCBhbnltb3JlLiBBcyBlYXJseSBhcyB0aGUgZHJpdmUgYmFjayB0byBteSBkb3JtLCBJIHdhcyBhbHJlYWR5IHRoaW5raW5nIGFib3V0IGhvdyBJIHdvdWxkIGJlc3QgbGlrZSB0byBraWxsIExpbmRhIFdhdHNvbi4NCg0KVGhlIGNob2ljZSB3YXMgYmV0d2VlbiBlZmZlY3RpdmVuZXNzIGFuZCBmdW4uIEkgZGVjaWRlZCB0byBnbyB3aXRoIGZ1biwgYmVjYXVzZSBpdCB3b3VsZCBiZSB3YXkgbW9yZSBzYXRpc2Z5aW5nIHRvIGtpbmQgb2YgZGlzc2VjdCBoZXIgYXMgSSBraWxsZWQgaGVyLCByYXRoZXIgdGhhbiBqdXN0IGdldHRpbmcgaXQgZG9uZSBhbmQgY2FsbGluZyBpdCBhIGRheS4gRmFzdC1mb3J3YXJkIG9uZSB3ZWVrIHRvIERlY2VtYmVyIDEzdGggLSB0b2RheSwgYWN0dWFsbHkuIExpbmRhIFdhdHNvbiB0dXJuZWQgMzQgdHdvIGRheXMgYWdvLiBJIG1hZGUgYSBmdW4gbGl0dGxlIHdhZ2VyIHdpdGggbXlzZWxmIHdoZXJlIGlmIExpbmRhIHdhcyBzcGVuZGluZyBoZXIgYmlydGhkYXkgd2Vla2VuZCBhbG9uZSwgSSB3b3VsZCBwYXkgaGVyIGEgdmlzaXQgYW5kIGtpbGwgaGVyLiBJZiBzaGUgd2FzIG91dCBvciBoYWQgY29tcGFueSwgSSB3b3VsZCBzdG9wIGJ5IG5leHQgd2VlayBvciBzb21ldGhpbmcgaW5zdGVhZC4NCg0KU28gdGhpcyBtb3JuaW5nLCBJIGRyb3ZlIG92ZXIgdG8gTG93ZeKAmXMgYW5kIGJvdWdodCBhbiBheGUuIEFnYWluLCBJIGV4cGVjdCB5b3XigJlyZSBsYXVnaGluZywgYnV0IHRoYXTigJlzIGFsc28ga2luZCBvZiB0aGUgcG9pbnQuIEFuIGF4ZSBpcyBzbyBraW5kIG9mIGNsaWNoZSBhbmQgYSDigJxtb3ZpZXPigJ0gdGhpbmcgdGhhdCBJIGFjdHVhbGx5IHRob3VnaHQgaXQgd291bGQgYmUgdGhlIG1vc3QgZnVuLiBTd2luZ2luZyBpdCBhdCBzb21lb25lIGFuZCBldmVyeXRoaW5nLCBpdOKAmXMgYSByZWFsbHkgZW50ZXJ0YWluaW5nIGltYWdlLiBUaGV5IGFjdHVhbGx5IGhhZCBhIGJ1bmNoIG9mIGRpZmZlcmVudCBheGVzLCBzbyBJIHBpY2tlZCBvbmUgdGhhdCBoYWQgYSBnb29kIHdlaWdodCBidXQgd2FzIHN0aWxsIGxpZ2h0IGVub3VnaCBmb3IgbWUgdG8gc3dpbmcgcXVpY2tseS4NCg0KVGhlIGRyaXZlIGFmdGVyIGdldHRpbmcgdGhlIGF4ZSB3YXMgd2hlbiB0aGUgYWRyZW5hbGluZSByZWFsbHkgcGlja2VkIHVwLiBBbGwgdGhhdCBrZXB0IGdvaW5nIHRocm91Z2ggbXkgbWluZCBvbiB0aGUgd2F5IG92ZXIgd2FzIOKAnFdvdywgSeKAmW0gcmVhbGx5IGRvaW5nIHRoaXMu4oCdIE5vdCBpbiBhIGJhZCB3YXksIGp1c3QgbGlrZSBhIHN1cnByaXNlZCB0aGlzIGlzIHJlYWwgbGlmZSBzb3J0IG9mIHRoaW5nLiBJIGFsc28gZ290IHRoaXMgc3RyYW5nZSBydXNoIG9mIHJlY29sbGVjdGlvbnMgb2YgdGhlIHRpbWUgSSBzcGVudCB3aXRoIExpbmRhLiBJdCB3YXMgbGlrZSBteSBsaWZlIHdhcyBmbGFzaGluZyBiZWZvcmUgbXkgZXllcywgZXhjZXB0IGl0IHdhcyBqdXN0IHRoZSByYXRoZXIgbXVuZGFuZSBob3VyIEkgc3BlbnQgd2l0aCBMaW5kYSAtIGxpa2Ugc25pcHBldHMgb2Ygb3VyIGNvbnZlcnNhdGlvbnMsIHRoZSBzb3VuZCBvZiBoZXIgbGF1Z2gsIGhlciBmYWNpYWwgZXhwcmVzc2lvbnMgYW5kIHN0dWZmLg0KDQpJIGFsc28gd29uZGVyZWQgdG8gbXlzZWxmIHdoYXQgdGhlIGNyYXp5IHNlcmlhbCBraWxsZXJzIHdvdWxkIGJlIGZlZWxpbmcgYXQgYSB0aW1lIGxpa2UgdGhpcyAtIHNjaGl6b3BocmVuaWMgZGVsdXNpb25zPyBTZXh1YWwgYnVpbGR1cD8gSSBoYXZlIG5vIGlkZWEsIGJ1dCB3aGF0IEkgZmVsdCB3YXMga2luZCBvZiBsaWtlIHJpZGljdWxvdXNseSBhbGVydCBhbmQgbnVtYiBpbiB0aGUgc2Vuc2VzIGF0IHRoZSBzYW1lIHRpbWUsIGhvd2V2ZXIgdGhhdOKAmXMgcG9zc2libGUuDQoNCkJlZm9yZSBnZXR0aW5nIG91dCBvZiB0aGUgY2FyLCBJIGhhZCB0aGUgc2Vuc2UgdG8gc3R1ZmYgdGhlIGF4ZSBpbnRvIG15IGJhY2twYWNrIHRvIGxvb2sgYSBsaXR0bGUgbGVzcyByaWRpY3Vsb3VzIHdhbGtpbmcgYWNyb3NzIHRoZSBwYXJraW5nIGxvdC4gVGhlIGhhbmRsZSB3YXMgc3RpY2tpbmcgb3V0LCBidXQgdGhhdCBkaWRu4oCZdCByZWFsbHkgbWF0dGVyLiBBdCB0aGF0IHBvaW50IG15IGhlYXJ0IHdhcyBwb3VuZGluZyBzbyBoYXJkIEkgY291bGQgZmVlbCBteSB0aHJvYXQgdGhyb2JiaW5nLiBJIHRyaWVkIGNvbnRyb2xsaW5nIG15IGJyZWF0aCwgYnV0IGl04oCZcyByZWFsbHkgaGFyZCB0byBub3QgYnJlYXRoZSBmYXN0IHdoZW4geW91ciBoZWFydCBpcyBwb3VuZGluZyBsaWtlIHRoYXQuDQoNCkkgcmVhY2hlZCBMaW5kYSBXYXRzb27igJlzIGRvb3IgYW5kIHF1aWV0bHkgcHV0IG15IGVhciB0byBpdCBhZnRlciBzZXR0aW5nIGRvd24gbXkgYmFja3BhY2suIEkgaGVhcmQgYSB2b2ljZSB0aGF0IHdhc27igJl0IGhlcnMgLSBjb21wYW55PyBObywgaXQgd2FzIGp1c3QgdGhlIFRWLCBtaXhlZCB3aXRoIGhlciBvY2Nhc2lvbmFsIHRhcHBpbmcgZm9vdHN0ZXBzIGJlaGluZCB0aGUgZG9vci4gSSBhY3R1YWxseSBrZXB0IG15IGVhciB0aGVyZSBmb3IgYSByZWFsbHkgZnJlYWtpbmcgbG9uZyB0aW1lLCBiZWNhdXNlIEkgd2FudGVkIHRvIG1ha2UgYWJzb2x1dGVseSBzdXJlIG5vYm9keSB3YXMgb3Zlci4gUHJvYmFibHkgMTAgbWludXRlcyBvZiB0aGF0IGFuZCBhIGxvdCBvZiByZWFzc3VyaW5nIG15c2VsZiBjb252aW5jZWQgbWUuDQoNCkkgcXVpZXRseSBvcGVuZWQgbXkgYmFja3BhY2sgemlwcGVyIGFuZCBoZWxkIHRoZSBheGUgaW4gbXkgaGFuZHMuIE15IGZpZXJjZWx5IHNoYWtpbmcgaGFuZHMuIFdoYXQgdGhlIGhlbGwgd2FzIHRoaXMga2luZCBvZiByZWFjdGlvbiB0aGF0IG15IGJvZHkgd2FzIG1ha2luZz8gSSB0b2xkIG15IGJvZHkgdG8gc2h1dCB1cCwgdGhhdCBpdOKAmXMgbm8gYmlnIGRlYWwsIGJ1dCBvZiBjb3Vyc2UgaXQgd291bGRu4oCZdCBsaXN0ZW4uIEl0IHdhcyBhY3R1YWxseSBiaXphcnJlIGhvdyBtdWNoIG15IGhhbmRzIHdlcmUgc2hha2luZy4gSXQgbXVzdCBiZSB0aGUgYWRyZW5hbGluZSBidWlsZHVwLiBJIHJvbGxlZCBteSBleWVzIGF0IG15c2VsZiBhbmQgZ290IG15IGhhbmQgdG8gcmVzdCBvbiB0aGUgZG9vcmtub2IuIElmIGl04oCZcyBsb2NrZWQsIEnigJlsbCBrbm9jaywgaXTigJlsbCBiZSBiYXNpY2FsbHkgdGhlIHNhbWUuIEkgdG9vayBhIGRlZXAgYnJlYXRoIGFuZCBmb3JjZWQgbXkgbXVzY2xlcyBpbnRvIGFjdGlvbi4NCg0KSSBzd2lmdGx5IHR1cm5lZCB0aGUgZG9vcmtub2IuIE5vdCBsb2NrZWQuIEluIG9uZSBtb3ZlbWVudCwgSSBvcGVuZWQgdXAgdGhlIGRvb3IgYW5kIHNsaXBwZWQgaW5zaWRlLiBMaW5kYSBXYXRzb24sIGp1c3QgYSBmZXcgc3RlcHMgYXdheSBpbnRvIHRoZSBraXRjaGVuLiBJIHNlZSAtIHNoZSB3YXMgaW4gdGhlIG1pZGRsZSBvZiBjb29raW5nLiBTaGUgaW1tZWRpYXRlbHkganVtcGVkIGFuZCB0dXJuZWQgYXJvdW5kLCBzdGFydGxlZC4gSSBleHBlY3RlZCB0aGF0LiBRdWlja2x5LCBJIGxldCBnbyBvZiB0aGUgZG9vcmtub2IgYW5kIGFkanVzdGVkIHRoZSBheGUgaW50byBib3RoIGhhbmRzLiBJbiB0aGUgZm9sbG93aW5nIHNwbGl0IHNlY29uZCwgSSByZWFsaXplZCB0aGF0IHNoZSB3b3VsZCBwcm9iYWJseSBzdGFydCB0byBtYWtlIGEgbG90IG9mIG5vaXNlLiBMb29raW5nIGJhY2ssIEnigJltIGFuIGlkaW90IGZvciBub3QgY29uc2lkZXJpbmcgdGhhdC4gSnVzdCBhcyBMaW5kYeKAmXMgbW91dGggb3BlbmVkIHRvIHNwZWFrIC0gbWF5YmUgZXZlbiBzdGFydGVkIHNwZWFraW5nIC0gSSBmb3JjZWZ1bGx5IHN3dW5nIG15IGF4ZSBpbnRvIHRoZSBzaWRlIG9mIGhlciBoZWFkLg0KDQpCdXQsIG15IGF4ZSB3YXMgZmFjaW5nIGJhY2t3YXJkcy4gSSBoaXQgaGVyIHdpdGggdGhlIGJsdW50IGVuZCBvZiB0aGUgYmxhZGUuIEkgYWN0dWFsbHkgZGlkIHRoaXMgb24gcHVycG9zZSwgYmVjYXVzZSBpbiB0aGF0IHNwbGl0IHNlY29uZCBJIHNvbWVob3cgZGVjaWRlZCB0aGF0IGl0IHdvdWxkIGJlIHRoZSB3YXkgdG8ga2VlcCBoZXIgbm9pc2UgdG8gYSBtaW5pbXVtLiBJdCBhY3R1YWxseSB3b3JrZWQuIEkgZmVsdCBiYXJlbHkgYW55IHJlc2lzdGFuY2UgaW4gdGhlIHN3aW5nIGFzIEkgY29sbGlkZWQgd2l0aCBoZXIgaGVhZCwga25vY2tpbmcgaXQgY2xlYW4gYXNpZGUuIExpbmRh4oCZcyBoYWxmLWZvcm1lZCBzeWxsYWJsZSBjYW1lIG91dCBhcyBhIGtpbmQgb2Ygd2VpcmQgZ3J1bnQgLSBhIG5vaXN5IGV4aGFsYXRpb24gaXMgcHJvYmFibHkgdGhlIGJlc3QgSSBjb3VsZCBkZXNjcmliZSBpdC4gVGhhdCBoYXBwZW5lZCBhdCB0aGUgc2FtZSB0aW1lIGFzIGhlciBoZWFkIHNtYWNrZWQgaW50byB0aGUgY2FiaW5ldCBmcm9tIHRoZSBmb3JjZSwgYW5kIHNoZSBmZWxsIGJhY2t3YXJkcyB3aXRob3V0IGFueSBhYmlsaXR5IHRvIGtlZXAgaGVyIGJhbGFuY2UuIEkgZGlkbuKAmXQgaGVzaXRhdGUgYXQgYWxsIHRvIGtlZXAgc3dpbmdpbmcgYXQgaGVyIHdoaWxlIHNoZSB3YXMgaGFsZiBseWluZyBkb3duIG9uIHRoZSBncm91bmQsIHRoaXMgdGltZSBteSBheGUgZmFjaW5nIHRoZSByaWdodCB3YXkuIEkgZGlkbuKAmXQgcmVhbGx5IGtub3cgd2hlcmUgdG8gc3dpbmcsIHNvIEkga2luZCBvZiBqdXN0IHN0YXJ0ZWQgaGFja2luZyBhdCBoZXIgY29sbGFyYm9uZSBhcmVhIGFuZCBjaGVzdC4gSXQgZGlkbuKAmXQgZmVlbCBsaWtlIHRoZSBheGUgd2FzIGdvaW5nIHRvbyBkZWVwLCBidXQgdGhlcmUgd2FzIGEgbmljZSDigJx0aHVua+KAnSBzb3J0IG9mIHNvdW5kIGV2ZXJ5IHRpbWUgdGhlIGF4ZSBlbWJlZGRlZCBpbnRvIGhlci4gSSBldmVuIGZlbHQgdGhlIHNvZnQgc2lua2luZyBzZW5zYXRpb24gcmlwcGxlIGludG8gbXkgaGFuZHMsIGxpa2UgdGhlIGF4ZSB3YXMgYSBraW5kIG9mIHBoeXNpY2FsIGV4dGVuc2lvbiBvZiBteSBzZW5zZSBvZiB0b3VjaC4NCg0KT24gYSB3aGltLCBJIHN3dW5nIG9uY2UgYXQgaGVyIHRocm9hdCwgYnV0IG1vc3Qgb2YgdGhlIHN3aW5nIGFjdHVhbGx5IG1pc3NlZCBhbmQgSSBoaXQgdGhlIGZsb29yIGJ5IGFjY2lkZW50LCBjYXVzaW5nIGEgbG91ZCwgZHVsbCB3aGFjayB0byByZXNvbmF0ZSB0aHJvdWdoIHRoZSBhcGFydG1lbnQuIEkgZGlkbuKAmXQgaGF2ZSB0aW1lIHRvIHRoaW5rIGFib3V0IGl0LiBJIHN3dW5nIGFnYWluIHdpdGggYmV0dGVyIGFpbSBhbmQgZ290IGEgbW9yZSBjZW50ZXJlZCBoaXQsIGZlZWxpbmcgdGhlIGJvbmUgb3IgY2FydGlsYWdlIG9yIHdoYXRldmVyIGlzIGluIHRoZXJlLCBzbyBJIG11c3QgaGF2ZSBzcGxpdCBpdCBvcGVuLiBSaWdodCBhZnRlciB0aGF0LCBJIGRlY2lkZWQgdG8gc3dpbmcgYXQgaGVyIGZhY2UsIGFuZCBJIGdvdCB0aGlzIGRpYWdvbmFsIGN1dCBhbG9uZyBoZXIgbm9zZSBhbmQgbW91dGgsIHdoaWNoIGZlbHQgcHJldHR5IGdvb2Qgc28gSSBkaWQgaXQgb25jZSBtb3JlLg0KDQpJIGZpbmFsbHkgYnJpZWZseSBzdG9wcGVkIHRvIHN1cnZleSB0aGUgZGFtYWdlLiBMaW5kYSB3YXMgYmxlZWRpbmcgcmlkaWN1bG91c2x5LiBUaGUgYmxvb2Qgd2FzIGtpbmQgb2YgY29taW5nIG91dCBpbiB3YXZlcywgaW4gc3luYyB3aXRoIGhlciBiZWF0aW5nIGhlYXJ0LCBwcm9iYWJseS4gSXQgd2FzIHBvb2xpbmcgYWxsIGFyb3VuZCBoZXIgYW5kIHJpZGluZyBhbG9uZyB0aGUgY3JhY2tzIGJldHdlZW4gdGhlIHRpbGVzLiBIZXIgbGlnaHQgYmx1ZSBzaGlydCB3YXMgYWxsIHRvcm4gdXAgYW5kIHN0YWluZWQgZGFyaywga2luZCBvZiBtaXhlZCB3aXRoIGEgZmxlc2h5IG1lc3MgYXJvdW5kIGhlciBjaGVzdC4gSXQgd2FzIGFsbCBqdXN0IGdsaXN0ZW5pbmcgcmVkLiBIZXIgZmFjZSB3YXNu4oCZdCBtdWNoIGJldHRlciwgY292ZXJlZCBpbiBkcmlwcGluZyByZWQgYXQgdGhpcyBwb2ludCwgYW5kIGhlciBsaXAgd2FzIGtpbmQgb2YgaGFuZ2luZyBvZmYsIHJldmVhbGluZyByZWQtc3RhaW5lZCB0ZWV0aCBpbiBhIHJlYWxseSB3ZWlyZCB3YXksIGxpa2UgYSB6b21iaWUgb3Igc29tZXRoaW5nLg0KDQpMaW5kYSB3YXNu4oCZdCBkZWFkLCB0aG91Z2guIEhlciBsaW1icyB3ZXJlIGtpbmQgb2Ygd2Vha2x5LCBhaW1sZXNzbHkgdHJ5aW5nIHRvIG1vdmUgd2hpbGUgc2hlIHdhcyBzdHVjayBvbiBoZXIgYmFjay4gTW9yZSB0aGFuIGFueXRoaW5nLCBzaGUgcmVtaW5kZWQgbWUgb2YgYSBidWcgdGhhdCB5b3UgY3J1c2ggYnV0IGl0IHN0aWxsIHBpdGlmdWxseSBtb3ZlcyBpdHMgbGVncyBhcm91bmQgYmVmb3JlIGl0IGRpZXMgY29tcGxldGVseS4gVGhhdOKAmXMgYmFzaWNhbGx5IHdoYXQgc2hlIHdhcyBkb2luZy4gQnV0IEkgZGlkbuKAmXQga25vdyBob3cgbG9uZyBpdCB3b3VsZCB0YWtlIGZvciBoZXIgdG8gZGllLCBvciB3aGF0IGtpbmQgb2YgY29uZGl0aW9uIHNoZSB3YXMgaW4uIEkgZW5kZWQgdXAgZ3JhYmJpbmcgYSBiaWcga25pZmUgdGhhdCB3YXMgb24gdGhlIGNvdW50ZXIgdGhhdCBzaGUgd2FzIHVzaW5nIHRvIGN1dCB1cCBtZWF0LiBUcnlpbmcgdG8gc3RlcCBhcm91bmQgdGhlIGJsb29kLCBJIHJlYWNoZWQgZG93biBhbmQgY2FydmVkIGludG8gdGhlIHVwcGVyIGhhbGYgb2YgaGVyIG5lY2ssIHRyeWluZyB0byBzb3J0IG9mIHNhdyBpdCBmcm9tIHRoZSBsZWZ0IHNpZGUgdG8gdGhlIHJpZ2h0LiBJdCB3YXMgYSBsaXR0bGUgYXdrd2FyZCBiZWNhdXNlIHRoZSBhcmVhIHdhcyBzbyBzb2Z0IGFuZCBzcXVpc2hlZCBhcm91bmQgdGhlIGtuaWZlIGFzIEkgd2FzIGN1dHRpbmcuIEJ1dCB0aGUgc2Vuc2F0aW9uIHdhcyBjb21wbGV0ZWx5IGRpZmZlcmVudCBmcm9tIHRoZSBheGUuIEl0IGFjdHVhbGx5IGZlbHQgbGlrZSBJIHdhcyBjdXR0aW5nIGEgdG91Z2ggcGllY2Ugb2YgcmF3IG1lYXQgKHdoaWNoIEkgZ3Vlc3MgdGVjaG5pY2FsbHksIEkgd2FzKS4NCg0KVGhlIGJsb29kIHN0YXJ0ZWQgcG91cmluZyBvdXQsIGFuZCBJIGhvcGVkIHRoYXQgSSBzZXZlcmVkIHRoZSBtb3N0IG1ham9yIGFydGVyaWVzIGluIHRoZXJlLiBJdCBtdXN0IGhhdmUgd29ya2VkLCBiZWNhdXNlIGFmdGVyIGEgbW9tZW50IExpbmRh4oCZcyBsaW1iIG1vdmVtZW50cyBraW5kIG9mIGp1c3QgaGFkIHRoZSBzdHJlbmd0aCBkcmFpbmVkIGZyb20gdGhlbSwgc29vbiByZXN0aW5nIHN0aWxsIG9uIHRoZSBmbG9vci4gSSB0b29rIGEgZmV3IHNlY29uZHMgdG8gY2F0Y2ggbXkgYnJlYXRoLiBObyB0aW1lIHRvIHN0aWNrIGFyb3VuZCBhbmQgdGhpbmsgYWJvdXQgdGhlIGV4cGVyaWVuY2UuIEkgc2hvb2sgdGhlIGtuaWZlIGJsYWRlIHRocm91Z2ggYSBkaXJ0eSBwYW4gaW4gdGhlIHNpbmsgdG8gY2xlYW4gb2ZmIHRoZSBibG9vZCwgdGhlbiB0aHJldyB0aGUga25pZmUgaW50byBteSBiYWNrcGFjay4gSSBkaWQgdGhlIHNhbWUgd2l0aCB0aGUgYXhlLiBJIGFsc28gdG9vayBoZXIgbGFwdG9wIHRoYXQgd2FzIHNpdHRpbmcgb24gdGhlIGNvdW50ZXIuIEl0IGhhZCBzb21lIHJlY2lwZSBvcGVuIGZvciB2ZWFsIGFuZCBtdXNocm9vbXMuIEkgZGlkbuKAmXQgcmVhbGx5IHRha2UgdGhlIGxhcHRvcCB0byB1c2UgaXQsIHNpbmNlIEkgaGF2ZSBhIHBlcmZlY3RseSBnb29kIG9uZSBteXNlbGYgdGhhdCBJIGdvdCBmb3IgY29sbGVnZS4gSSBqdXN0IHdhbnRlZCB0byBsb29rIHRocm91Z2ggaXQgZm9yIGZ1bi4NCg0KSSBmaW5hbGx5IHdlbnQgb3V0c2lkZSBhbmQgY2xvc2VkIHRoZSBkb29yIGJlaGluZCBtZS4gSSBnb3Qgc29tZSBibG9vZCBvbiBteSBzd2VhdGVyIGFuZCBqZWFucy4gQnV0IGZ1bm5pbHkgZW5vdWdoLCBJIGFjdHVhbGx5IGFudGljaXBhdGVkIHRoYXQgc28gSSB3b3JlIGRhcmsgY29sb3JzLg0KDQpUaGUgZHJpdmUgYmFjayB0byBteSBkb3JtIHdhcyBqdXN0IGEgY29uc3RhbnQgcmVwbGF5aW5nIG9mIHRoZSBleHBlcmllbmNlIGluIG15IGhlYWQuIEkgZ3Vlc3MgdGhhdOKAmXMgc3RpbGwga2luZCBvZiBoYXBwZW5pbmcgZXZlbiBub3csIGFjdHVhbGx5LiBCdXQgaXQgZmVsdCBwcmV0dHkgbmljZS4gTGluZGEgV2F0c29uIGlzIGRlYWQuIEkga2luZCBvZiBsZXQgdGhlIHdlaWdodCBvZiB0aGF0IHNpbmsgaW4uIFRoZSBzZW5zYXRpb24gb2YgaGF2aW5nIGNvbXBsZXRlbHkgcmVtb3ZlZCBhIGh1bWFuIGxpZmUgZnJvbSBleGlzdGVuY2UuIEl04oCZcyBjcmF6eS4gSSBkb27igJl0IGtub3cgaG93IGVsc2UgdG8gZGVzY3JpYmUgaXQuDQoNCkFueXdheSwgSSB0aHJldyB0aGUgYXhlIGFuZCBrbmlmZSBpbnRvIGEgZHVtcHN0ZXIgb24gY2FtcHVzLCB3aGljaCBJIHRoaW5rIGlzIHBpY2tlZCB1cCBldmVyeSBNb25kYXksIHNvIHRoZXnigJlsbCBiZSBnb25lIGJ5IHRoZW4uIE15IHJvb21tYXRlIGdvZXMgaG9tZSBvbiB0aGUgd2Vla2VuZHMsIHNvIEkgaGF2ZSB0aGUgZG9ybSB0byBteXNlbGYgdG9kYXkuIEl0IGdhdmUgbWUgdGhlIGNoYW5jZSB0byBnbyB0aHJvdWdoIExpbmRh4oCZcyB3ZWJzaXRlIGhpc3RvcnkuIEkgd2FzIHJpZ2h0IGluIHRoaW5raW5nIHRoYXTigJlzIHdoZXJlIGhlciBkZWVwZXN0IHNlY3JldHMgd291bGQgbGllLg0KDQpUaGVyZSB3YXMgYWN0dWFsbHkgYSBsb3Qgb2YgZGlydHkgc3R1ZmYsIGxpa2UgdGhlIG5hbWVzIG9mIHdlYnNpdGVzIGZvciBwb3JuIHZpZGVvcyBhbmQgc3RvcmllcyBhbmQgdGhpbmdzIGxpa2UgdGhhdC4gU2FtZSB3aXRoIGhlciBzZWFyY2hlcy4gQSBsb3Qgb2YgdGhlIHdlYnNpdGVzIHdlcmUgYm9yaW5nLCBsaWtlIGNvb2tpbmcgd2Vic2l0ZXMgYW5kIHJlY2lwZXMsIGFuZCBnYW1lIHdlYnNpdGVzIGxpa2UgQmVqZXdlbGVkIGFuZCBzdHVmZi4gSSBldmVudHVhbGx5IGdvdCB0byB0aGUg4oCcb25lIHdlZWsgYWdv4oCdIHNlY3Rpb24gb2YgaGVyIGhpc3RvcnksIGFuZCBpdCBnYXZlIG1lIGEgY2hpbGwuDQoNClRoZXJlIHdlcmUgYSB3aG9sZSBidW5jaCBvZiBzZWFyY2hlcyBsaWtlIOKAnG1ldGhvZHMgb2Ygc3VpY2lkZeKAnSwg4oCcaG93IHRvIHRpZSBhIG5vb3Nl4oCdLCDigJxkYW5nZXJvdXMgaG91c2Vob2xkIGNoZW1pY2Fsc+KAnSwg4oCcY2FyYm9uIG1vbm94aWRlIHBvaXNvbmluZ+KAnSAtIGxpa2UgYSBsb3Qgb2YgdGhlbS4gU2hlIHdhcyBwcm9iYWJseSByZWFkeSB0byB3cml0ZSBhIGJvb2sgb24gc3VpY2lkZSBhZnRlciBhbGwgdGhlIHJlc2VhcmNoIHNoZSBkaWQuIFNvIEkgZ3Vlc3MgTGluZGEgd2FzIGNvbnRlbXBsYXRpbmcgc3VpY2lkZS4gSSB3b25kZXIgaWYgaXQgd2FzIGluZmx1ZW5jZWQgYnkgaGVyIGRlcHJlc3Npb24uDQoNClRoZSBpcm9ueSBpcyBhY3R1YWxseSBzdHJpa2luZy4gTWF5YmUgTGluZGEgd2FzIGdvaW5nIHRvIGRpZSBhbnl3YXkuIE9yIG1heWJlIHNoZSBjb3VsZG7igJl0IGZpbmQgdGhlIGNvdXJhZ2UgdG8gZG8gaXQuIElmIHRoYXQgd2VyZSB0aGUgY2FzZSwgSSBhbG1vc3QgbGl0ZXJhbGx5IGdhdmUgaGVyIGEgYmlydGhkYXkgcHJlc2VudCBieSBraWxsaW5nIGhlci4gVGhhdOKAmXMgYWN0dWFsbHkgcmVhbGx5IGNvbWljYWwgaW4gYSBtZXNzZWQtdXAgd2F5LCBhbmQgaXQgbGVhdmVzIGEgd2VpcmQgdGFzdGUgaW4gbXkgbW91dGguIFRoZSBwYXJ0IEkgZG9u4oCZdCBnZXQgaXMgdGhhdCBJIGRpZG7igJl0IHNlZSBhbnkgb2YgdGhvc2Ugc2VhcmNoZXMgdXAgdW50aWwgdGhlIOKAnG9uZSB3ZWVrIGFnb+KAnSBzZWN0aW9uLCBub3RoaW5nIG1vcmUgcmVjZW50IHRoYW4gdGhhdC4NCg0KSSBlbmRlZCB1cCB0aHJvd2luZyB0aGUgbGFwdG9wIGluIHRoZSBkdW1wc3RlciB3aXRoIHRoZSBvdGhlciBzdHVmZi4gSXTigJlzIGJlZW4gYSBmZXcgaG91cnMgc2luY2UgdGhlbiwgc28gSeKAmXZlIGhhZCBzb21lIHRpbWUgdG8gY2FsbWx5IHRoaW5rIGFib3V0IGV2ZXJ5dGhpbmcuIExpa2UgSSBzYWlkLCBpdCB3YXMgcHJldHR5IHNhdGlzZnlpbmcgYW5kIEnigJltIGdsYWQgSSBmaW5hbGx5IGdvdCBhcm91bmQgdG8gaXQuIEkgZmVlbCBsaWtlIEkgY2FuIGZpbmFsbHkgY3Jvc3MgaXQgb2ZmIG15IGJ1Y2tldCBsaXN0LCBvciBsaWtlIEnigJltIHR5aW5nIGxvb3NlIGVuZHMgd2l0aCBteXNlbGYuIFRoaXMgaXMgcHJvYmFibHkgdGhlIGZpcnN0IGFuZCBsYXN0IHRpbWUgSeKAmWxsIHdyaXRlIHRoZSBuYW1lIExpbmRhIFdhdHNvbiAtIGl04oCZcyBiYWNrIHRvIGxpdmluZyBhIG5vcm1hbCBjb2xsZWdlIGxpZmUsIGV4Y2VwdCBJIG1pZ2h0IGRvIHNvbWUgcGVvcGxlLXdhdGNoaW5nIGV2ZXJ5IG5vdyBhbmQgdGhlbiBiZWNhdXNlIGl04oCZcyBkZWZpbml0ZWx5IGZ1biBhbmQgaW50ZXJlc3RpbmcuDQoNCkJ1dCBJ4oCZbGwgYWx3YXlzIHdvbmRlciBob3cgbWFueSBwZW9wbGUgdGhlcmUgYXJlIGxpa2UgbWUuIEnigJltIHN1cmUgdGhlcmUgaGFzIHRvIGJlIGEgbG90LCBiZWNhdXNlIHRoZXJlIGlzIGp1c3Qgbm90aGluZyBzdHJhbmdlIGFib3V0IGl0IHRvIG1lLCBiZWluZyBjdXJpb3VzIGFib3V0IGtpbGxpbmcgc29tZW9uZS4gU2FkbHksIGl04oCZcyBzb21ldGhpbmcgdGhhdCBwZW9wbGUgY2Fu4oCZdCBleGFjdGx5IGp1c3QgdGFsayBhYm91dCwgc28gSSBndWVzcyBJ4oCZbGwgbmV2ZXIga25vdy4gSeKAmW0gc3VyZSB0aGF0IGFueW9uZSB3b3VsZCBqdXN0IGxpZSBhYm91dCBpdCBldmVuIGlmIHlvdSBhc2tlZCB0aGVtLiBCdXQgeW91IGNhbuKAmXQgaGVscCBidXQgd29uZGVyIGlmIHRoYXQgcGVyc29uIGluIHRoZSBncm9jZXJ5IHN0b3JlLCB3aG8gc3RhcmVzIGF0IHlvdSBhcyB5b3UgcGFzcyBieSwgbWlnaHQgYmUgY29uc2lkZXJpbmcgd2hhdCBpdCB3b3VsZCBiZSBsaWtlIHRvIGtpbGwgeW91LiBJZiBJIGNvdWxkLCBJIHdvdWxkIHRlbGwgdGhlbSBhbGwgYWJvdXQgaXQsIHNvIHRoZXkgY291bGQgZGVjaWRlIGZvciB0aGVtc2VsdmVzLiAgQnV0IHdobyBrbm93cywgbWF5YmUgSSBnb3QgbHVja3ksIGFuZCB0aGF0IHBlcnNvbiBpcyB5b3UuIEkgYWN0dWFsbHkgcmVhbGx5LCByZWFsbHkgaG9wZSBzby4NCg0KfuKZpQ== -------------------------------------------------------------------------------- /game/mod_prompt.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | init python: 4 | from store import ddmd_mod_installer 5 | 6 | screen ddmd_confirm(message, yes_action, no_action, message2=None, xs=480, ys=220): 7 | modal True 8 | zorder 200 9 | 10 | style_prefix "ddmd_confirm" 11 | 12 | use ddmd_generic_notif(xs, ys): 13 | 14 | vbox: 15 | xalign .5 16 | yalign .5 17 | spacing int(8 * res_scale) 18 | 19 | label _(message): 20 | text_size int(20 * res_scale) 21 | xalign 0.0 22 | substitute False 23 | 24 | if message2: 25 | text _(message2): 26 | xalign 0.0 27 | size int(16 * res_scale) 28 | outlines [] 29 | substitute False 30 | 31 | hbox: 32 | xalign 0.5 33 | spacing int(100 * res_scale) 34 | 35 | textbutton _("Yes") action yes_action 36 | textbutton _("No") action no_action 37 | 38 | screen ddmd_dialog(message, message2=None, xs=480, ys=220): 39 | modal True 40 | 41 | zorder 200 42 | 43 | style_prefix "ddmd_confirm" 44 | 45 | use ddmd_generic_notif(xs, ys): 46 | 47 | vbox: 48 | xalign .5 49 | yalign .5 50 | spacing 8 51 | 52 | label _(message): 53 | xalign 0.0 54 | text_size int(16 * res_scale) 55 | substitute False 56 | 57 | if message2: 58 | text _(message2): 59 | xalign 0.0 60 | size int(16 * res_scale) 61 | outlines [] 62 | substitute False 63 | 64 | hbox: 65 | xalign 0.5 66 | spacing 100 67 | 68 | textbutton _("OK") action Hide("ddmd_dialog", Dissolve(0.25)) 69 | 70 | screen mod_name_input(zipPath, copy=False, xs=480, ys=220): 71 | modal True 72 | zorder 200 73 | 74 | style_prefix "ddmd_confirm" 75 | 76 | use ddmd_generic_notif(xs, ys): 77 | 78 | vbox: 79 | xalign .5 80 | yalign .5 81 | spacing 8 82 | default tempFolderName = "" 83 | 84 | label _("Enter the name you wish to call this mod."): 85 | text_size int(18 * res_scale) 86 | xalign 0.5 87 | 88 | input default "" value ScreenVariableInputValue("tempFolderName") length 24 allow "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789:-" 89 | 90 | hbox: 91 | xalign 0.5 92 | spacing 100 93 | 94 | textbutton _("OK") action [Hide("mod_name_input"), Function(ddmd_mod_installer.install_mod, zipPath=zipPath, modFolderName=tempFolderName, copy=copy)] 95 | 96 | screen ddmd_progress(message, xs=480, ys=220): 97 | modal True 98 | zorder 200 99 | 100 | style_prefix "ddmd_confirm" 101 | 102 | use ddmd_generic_notif(xs, ys): 103 | 104 | vbox: 105 | xalign .5 106 | yalign .5 107 | spacing 8 108 | 109 | text _(message): 110 | size 18 111 | xalign 0.5 112 | substitute False 113 | 114 | screen ddmd_generic_notif(xs, ys): 115 | add At("sdc_system/ddmd_app/ddmd_confirm_overlay.png", android_like_overlay) xsize config.screen_width ysize config.screen_height 116 | key "K_RETURN" action NullAction() 117 | 118 | frame at android_like_frame: 119 | xsize int(xs * res_scale) 120 | ysize int(ys * res_scale) 121 | 122 | transclude 123 | 124 | screen ddmd_generic_window(title, xsize=500, ysize=300, allow_search=False): 125 | drag: 126 | drag_handle (0, 0, 1.0, 40) 127 | xsize int(xsize * res_scale) 128 | ysize int(ysize * res_scale) 129 | xpos 0.3 130 | ypos 0.3 131 | 132 | frame: 133 | hbox: 134 | ypos 0.005 135 | xalign 0.52 136 | text _(title) 137 | 138 | hbox: 139 | ypos -0.005 140 | if allow_search: 141 | xalign 0.96 142 | imagebutton: 143 | idle Transform("ddmd_search_window_icon", size=(int(36 * res_scale), int(36 * res_scale))) 144 | hover Transform("ddmd_search_window_icon_hover", size=(int(36 * res_scale), int(36 * res_scale))) 145 | action Show("mod_search", Dissolve(0.25)) 146 | else: 147 | xalign 0.98 148 | imagebutton: 149 | idle Transform("ddmd_close_icon", size=(int(36 * res_scale), int(36 * res_scale))) 150 | hover Transform("ddmd_close_icon_hover", size=(int(36 * res_scale), int(36 * res_scale))) 151 | action Hide(transition=Dissolve(0.25)) 152 | 153 | transclude -------------------------------------------------------------------------------- /game/mod_screen.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | python early: 4 | import os 5 | import json 6 | import threading 7 | from time import sleep 8 | 9 | init -1000 python: 10 | 11 | def _mod_overlay(): 12 | renpy.show_screen("mods") 13 | renpy.restart_interaction() 14 | 15 | config.keymap['mod_overlay'] = ['K_F9'] 16 | config.underlay.append( 17 | renpy.Keymap( 18 | mod_overlay = _mod_overlay 19 | ) 20 | ) 21 | 22 | init python: 23 | class SteamLikeOverlay(): 24 | def __init__(self): 25 | thread = threading.Thread(target=self.run) 26 | thread.start() 27 | 28 | def show_notif(self): 29 | renpy.show_screen("steam_like_overlay", _("Access the Mod Docker menu while playing."), 30 | _("Press: %s" % config.keymap['mod_overlay'][0].replace("K_", ""))) 31 | 32 | def run(self): 33 | sleep(1.5) 34 | self.show_notif() 35 | 36 | start_overlay = SteamLikeOverlay() 37 | 38 | def get_ddmc_modlist(): 39 | with renpy.file("ddmc.json") as mod_json: 40 | return json.load(mod_json) 41 | 42 | def set_settings_json(): 43 | temp = { 44 | "config_gl2": config.gl2 45 | } 46 | with open(persistent.ddml_basedir + "/ddmd_settings.json", "w") as ddmd_settings: 47 | json.dump(temp, ddmd_settings) 48 | 49 | # For > 720p resolutions 50 | def get_dsr_scale(): 51 | return (config.screen_width / 1280.0) 52 | 53 | res_scale = get_dsr_scale() 54 | 55 | init python in ddmd_app: 56 | from store import Text, persistent 57 | from datetime import datetime 58 | 59 | def get_current_time(st, at): 60 | if persistent.military_time: 61 | return Text(datetime.now().strftime("%H:%M"), style="time_text"), 1.0 62 | else: 63 | return Text(datetime.now().strftime("%I:%M %p"), style="time_text"), 1.0 64 | 65 | init python in ddmd_app_settings: 66 | from store import persistent, config 67 | import shutil 68 | import os 69 | import subprocess 70 | 71 | def delete_mod(mod): 72 | try: 73 | shutil.rmtree(persistent.ddml_basedir + "/game/mods/" + mod) 74 | renpy.show_screen("ddmd_dialog", message="Successfully removed %s from Mod Docker." % mod) 75 | except Exception as err: 76 | renpy.show_screen("ddmd_dialog", message="A error occured while removing %s." % mod, message2=str(err)) 77 | 78 | def delete_saves(mod): 79 | try: 80 | shutil.rmtree(os.path.join(os.path.dirname(config.savedir), mod)) 81 | renpy.show_screen("ddmd_dialog", message="Successfully removed %s save data from Mod Docker." % mod) 82 | except OSError as err: 83 | if err.errno == 2: 84 | renpy.show_screen("ddmd_dialog", message="No save files were found. You might have deleted the saves already or not launched this mod yet.") 85 | else: 86 | renpy.show_screen("ddmd_dialog", message="A error occured while removing %s save data." % mod, message2=str(err)) 87 | except Exception as err: 88 | renpy.show_screen("ddmd_dialog", message="A error occured while removing %s save data." % mod, message2=str(err)) 89 | 90 | def open_save_dir(): 91 | if renpy.windows: 92 | os.startfile(config.savedir) 93 | elif renpy.macintosh: 94 | subprocess.Popen([ "open", config.savedir ]) 95 | else: 96 | subprocess.Popen([ "xdg-open", config.savedir ]) 97 | 98 | def open_dir(path): 99 | if renpy.windows: 100 | os.startfile(path) 101 | elif renpy.macintosh: 102 | subprocess.Popen([ "open", path ]) 103 | else: 104 | subprocess.Popen([ "xdg-open", path ]) 105 | 106 | init python in ddmd_app_functions: 107 | from store import persistent, ddmd_app_functions 108 | import os 109 | import json 110 | 111 | def get_mod_json(): 112 | try: 113 | with open(persistent.ddml_basedir + "/selectedmod.json", "r") as mod_json: 114 | temp = json.load(mod_json) 115 | return temp['modName'] 116 | except: 117 | return "DDLC" 118 | 119 | selectedMod = get_mod_json() 120 | loadedMod = selectedMod 121 | 122 | def loadMod(x, folderName): 123 | if ddmd_app_functions.loadedMod == folderName: 124 | renpy.show_screen("ddmd_dialog", message="Error: %s is already the selected mod." % folderName) 125 | return 126 | isRPA = False 127 | 128 | for root, dirs, files in os.walk(x + "/game"): 129 | for f in files: 130 | if f.endswith(".rpa"): 131 | isRPA = True 132 | 133 | mod_dict = { 134 | "modName": folderName, 135 | "isRPA": isRPA, 136 | } 137 | 138 | with open(persistent.ddml_basedir + "/selectedmod.json", "w") as j: 139 | json.dump(mod_dict, j) 140 | 141 | renpy.show_screen("ddmd_dialog", message="Selected %s as the loadable mod. You must restart Mod Docker in order to load the mod." % folderName) 142 | 143 | def clearMod(): 144 | if os.path.exists(persistent.ddml_basedir + "/selectedmod.json"): 145 | os.remove(persistent.ddml_basedir + "/selectedmod.json") 146 | 147 | if renpy.version_tuple == (6, 99, 12, 4, 2187): 148 | renpy.show_screen("ddmd_dialog", message="Returned to DDLC mode. You must restart Mod Docker in order to load the mod.") 149 | else: 150 | renpy.show_screen("ddmd_dialog", message="Returned to stock mode. You must restart Mod Docker in order to apply these settings.") 151 | else: 152 | if renpy.version_tuple == (6, 99, 12, 4, 2187): 153 | renpy.show_screen("ddmd_dialog", message="Error: You are already in DDLC Mode.") 154 | else: 155 | renpy.show_screen("ddmd_dialog", message="Error: You are already in stock mode.") 156 | 157 | init python: 158 | from store import ddmd_app_settings, ddmd_app_functions 159 | from store.ddmd_services import ddmd_modlist_service 160 | 161 | screen mods(): 162 | zorder 100 163 | modal True 164 | 165 | fixed at ml_overlay_effect: 166 | style_prefix "mods" 167 | 168 | if os.path.exists(persistent.ddml_basedir + "/game/docker_custom_image.png"): 169 | add persistent.ddml_basedir + "/game/docker_custom_image.png" xsize config.screen_width ysize config.screen_height 170 | elif os.path.exists(persistent.ddml_basedir + "/game/docker_custom_image.jpg"): 171 | add persistent.ddml_basedir + "/game/docker_custom_image.jpg" xsize config.screen_width ysize config.screen_height 172 | 173 | add Transform("#000", alpha=0.8) xsize int(365 * res_scale) 174 | add Transform("#202020", alpha=0.5) xpos 0.28 175 | 176 | vbox: 177 | label _("Select a Mod") 178 | 179 | side "c": 180 | xpos int(50 * res_scale) 181 | xsize int(250 * res_scale) 182 | ysize int(450 * res_scale) 183 | 184 | viewport id "mlvp": 185 | mousewheel True 186 | has vbox 187 | spacing int(9 * res_scale) 188 | 189 | button: 190 | action [SetField(ddmd_app_functions, "selectedMod", "DDLC"), SensitiveIf(ddmd_app_functions.selectedMod != "DDLC")] 191 | 192 | add If(ddmd_app_functions.loadedMod == "DDLC", Composite((int(310 * res_scale), int(50 * res_scale)), (0, int(1 * res_scale)), Transform("ddmd_selectedmod_icon", size=(int(36 * res_scale), int(36 * res_scale))), 193 | (int(38 * res_scale), 0), Text(If(renpy.version_tuple == (6, 99, 12, 4, 2187), "DDLC Mode", 194 | _("Stock Mode")), style="mods_button_text")), Text(If(renpy.version_tuple == (6, 99, 12, 4, 2187), 195 | _("DDLC Mode"), _("Stock Mode")), style="mods_button_text")) 196 | 197 | for mod in ddmd_modlist_service.mods.keys(): 198 | button: 199 | action [SetField(ddmd_app_functions, "selectedMod", mod), SensitiveIf(mod != ddmd_app_functions.selectedMod)] 200 | 201 | add If(ddmd_app_functions.loadedMod == mod, Composite((int(190 * res_scale), int(50 * res_scale)), (0, int(1 * res_scale)), Transform("ddmd_selectedmod_icon", size=(int(36 * res_scale), int(36 * res_scale))), 202 | (int(38 * res_scale), 0), Text(mod, style="mods_button_text", substitute=False)), 203 | Text(mod, style="mods_button_text", substitute=False)) 204 | 205 | hbox: 206 | style "mods_return_button" 207 | vbox: 208 | imagebutton: 209 | idle Transform("ddmd_return_icon", size=(int(48 * res_scale), int(48 * res_scale))) 210 | hover Transform("ddmd_return_icon_hover", size=(int(48 * res_scale), int(48 * res_scale))) 211 | hovered Show("mods_hover_info", about=_("Exit the DDMD Menu")) 212 | unhovered Hide("mods_hover_info") 213 | action [Hide("mods_hover_info"), Hide("mods"), With(Dissolve(0.5))] 214 | null width 10 215 | vbox: 216 | imagebutton: 217 | idle Transform("ddmd_install_icon", size=(int(48 * res_scale), int(48 * res_scale))) 218 | hover Transform("ddmd_install_icon_hover", size=(int(48 * res_scale), int(48 * res_scale))) 219 | hovered Show("mods_hover_info", about=_("Install a Mod")) 220 | unhovered Hide("mods_hover_info") 221 | action [Hide("mods_hover_info"), If(renpy.macintosh and persistent.self_extract is None, 222 | Show("ddmd_confirm", message="ZIP Extraction On?", message2="Does your version of macOS extract ZIP files after downloading?", 223 | yes_action=[SetField(persistent, "self_extract", True), Hide("ddmd_confirm"), Show("pc_directory", Dissolve(0.25), mac=True)], 224 | no_action=[SetField(persistent, "self_extract", False), Hide("ddmd_confirm"), Show("pc_directory", Dissolve(0.25))]), 225 | If(renpy.macintosh and persistent.self_extract, Show("pc_directory", Dissolve(0.25), mac=True), Show("pc_directory", Dissolve(0.25))))] 226 | null width 10 227 | vbox: 228 | imagebutton: 229 | idle Transform("ddmd_search_icon", size=(int(48 * res_scale), int(48 * res_scale))) 230 | hover Transform("ddmd_search_icon_hover", size=(int(48 * res_scale), int(48 * res_scale))) 231 | hovered Show("mods_hover_info", about=_("Browse the Mod List!")) 232 | unhovered Hide("mods_hover_info") 233 | action [Hide("mods_hover_info"), If(not persistent.mod_list_disclaimer_accepted, 234 | Show("ddmd_confirm", message="Disclaimer", message2="This mod list source is provided by the defunct Doki Doki Mod Club site. Not all mods may be on here while others may be out-of-date. By accepting this prompt, you acknowledge to the following disclaimer above.", 235 | yes_action=[SetField(persistent, "mod_list_disclaimer_accepted", True), Hide("ddmd_confirm"), Show("mod_list", Dissolve(0.25))], 236 | no_action=Hide("ddmd_confirm")), Show("mod_list", Dissolve(0.25)))] 237 | null width 10 238 | vbox: 239 | imagebutton: 240 | idle Transform("ddmd_settings_icon", size=(int(48 * res_scale), int(48 * res_scale))) 241 | hover Transform("ddmd_settings_icon_hover", size=(int(48 * res_scale), int(48 * res_scale))) 242 | hovered Show("mods_hover_info", about=_("View DDMD's Settings")) 243 | unhovered Hide("mods_hover_info") 244 | action [Hide("mods_hover_info"), Show("mod_settings", Dissolve(0.25))] 245 | null width 10 246 | vbox: 247 | imagebutton: 248 | idle Transform("ddmd_restart_icon", size=(int(48 * res_scale), int(48 * res_scale))) 249 | hover Transform("ddmd_restart_icon_hover", size=(int(48 * res_scale), int(48 * res_scale))) 250 | hovered Show("mods_hover_info", about=_("Quit DDMD")) 251 | unhovered Hide("mods_hover_info") 252 | action Quit() 253 | 254 | vbox: 255 | hbox: 256 | if persistent.military_time: 257 | xpos config.screen_width - int(105 * res_scale) 258 | else: 259 | xpos config.screen_width - int(130 * res_scale) 260 | ypos int(25 * res_scale) 261 | add "ddmd_time_clock" 262 | 263 | hbox: 264 | viewport id "modinfoname": 265 | mousewheel True 266 | xpos int(450 * res_scale) 267 | ypos int(50 * res_scale) 268 | xsize int(700 * res_scale) 269 | if ddmd_app_functions.selectedMod == "DDLC" and renpy.version_tuple > (6, 99, 12, 4, 2187): 270 | label _("Stock Mode") 271 | elif ddmd_app_functions.selectedMod == "DDLC" and renpy.version_tuple == (6, 99, 12, 4, 2187): 272 | label _("DDLC Mode") 273 | else: 274 | label "[ddmd_app_functions.selectedMod]" 275 | 276 | vbox: 277 | xpos 0.31 278 | ypos 0.25 279 | label "Options" 280 | vbox: 281 | xpos 0.2 282 | yoffset -20 283 | textbutton _("Open Save Directory") action Function(ddmd_app_settings.open_save_dir) 284 | if ddmd_app_functions.loadedMod != "DDLC": 285 | textbutton _("Open Running Mods' Game Directory") action Function(ddmd_app_settings.open_dir, config.gamedir) 286 | textbutton _("Open Mod Docker's Game Directory") action Function(ddmd_app_settings.open_dir, persistent.ddml_basedir + "/game") 287 | if ddmd_app_functions.selectedMod != ddmd_app_functions.loadedMod: 288 | textbutton _("Delete Saves") action Show("ddmd_confirm", message=_("Are you sure you want to remove %s save files?") % ddmd_app_functions.selectedMod, yes_action=[Hide("ddmd_confirm"), Function(ddmd_app_settings.delete_saves, ddmd_app_functions.selectedMod)], no_action=Hide("ddmd_confirm")) 289 | if ddmd_app_functions.selectedMod != "DDLC": 290 | textbutton _("Delete Mod") action Show("ddmd_confirm", message=_("Are you sure you want to remove %s?") % ddmd_app_functions.selectedMod, yes_action=[Hide("ddmd_confirm"), Function(ddmd_app_settings.delete_mod, ddmd_app_functions.selectedMod)], no_action=Hide("ddmd_confirm")) 291 | 292 | vbox: 293 | xpos 0.9 294 | ypos 0.9 295 | textbutton _("Select") action If(ddmd_app_functions.selectedMod == "DDLC", Function(ddmd_app_functions.clearMod), Function(ddmd_app_functions.loadMod, persistent.ddml_basedir + "/game/mods/" + ddmd_app_functions.selectedMod, ddmd_app_functions.selectedMod)) 296 | 297 | key "K_ESCAPE" action Hide("mods") 298 | 299 | init -1: 300 | screen steam_like_overlay(message, message2): 301 | 302 | zorder 200 303 | style_prefix "steam" 304 | 305 | frame at steam_effect: 306 | xsize int(200 * res_scale) 307 | ysize int(100 * res_scale) 308 | xalign 1.0 309 | yalign 1.0 310 | 311 | vbox: 312 | xalign 0.5 313 | yalign 0.15 314 | text message size int(16 * res_scale) 315 | vbox: 316 | xalign 0.5 317 | yalign 0.9 318 | text message2 size int(16 * res_scale) 319 | 320 | 321 | timer 3.25 action Hide('steam_like_overlay') 322 | 323 | style steam_frame: 324 | background Frame("sdc_system/ddmd_app/steam_frame.png", left=4, top=4, bottom=4, right=4, tile=False) 325 | 326 | style steam_text is renpy_generic_text: 327 | color "#fff" 328 | outlines [] 329 | 330 | transform steam_effect: 331 | subpixel True 332 | on show: 333 | ycenter config.screen_height + int(80 * res_scale) yanchor 1.0 alpha 1.00 nearest True 334 | easein .45 ycenter config.screen_height - int(50 * res_scale) 335 | on hide: 336 | easein .45 ycenter config.screen_height + int(80 * res_scale) nearest True 337 | 338 | screen mods_hover_info(about): 339 | zorder 101 340 | style_prefix "mods_hover" 341 | 342 | python: 343 | currentpos = renpy.get_mouse_pos() 344 | 345 | frame at windows_like_effect: 346 | xpos currentpos[0] 347 | ypos currentpos[1] + 15 348 | xsize int(150 * res_scale) 349 | 350 | text _(about) 351 | -------------------------------------------------------------------------------- /game/mod_services.rpy: -------------------------------------------------------------------------------- 1 | 2 | init -1 python in ddmd_services: 3 | from store import persistent 4 | import threading 5 | import os 6 | from time import sleep 7 | from collections import defaultdict 8 | 9 | class ModListService(object): 10 | def __init__(self): 11 | self.modpath = persistent.ddml_basedir + "/game/mods" 12 | self.mods = {} 13 | thread = threading.Thread(target=self.run) 14 | thread.daemon = True 15 | thread.start() 16 | 17 | def run(self): 18 | for modfolder in os.listdir(self.modpath): 19 | if os.path.isdir(os.path.join(self.modpath, modfolder, "game")): 20 | self.mods[modfolder] = os.path.join(self.modpath, modfolder, "game") 21 | 22 | while True: 23 | modFolders = {} 24 | for entry in os.listdir(self.modpath): 25 | entry_path = os.path.join(self.modpath, entry) 26 | if os.path.isdir(entry_path) and os.path.exists(os.path.join(entry_path, "game")): 27 | modFolders[entry] = entry_path 28 | 29 | for modfolder in os.listdir(self.modpath): 30 | modfolder_path = os.path.join(self.modpath, modfolder) 31 | if os.path.exists(os.path.join(modfolder_path, "game")): 32 | if modfolder not in modFolders: 33 | modFolders[modfolder] = modfolder_path 34 | 35 | if len(modFolders) < len(self.mods): 36 | for mod in self.mods.keys(): 37 | if not os.path.exists(os.path.join(self.modpath, mod)): 38 | self.mods.pop(mod) 39 | elif len(modFolders) > len(self.mods): 40 | for mod in modFolders: 41 | if mod not in self.mods: 42 | self.mods[mod] = os.path.join(self.modpath, mod, "game") 43 | 44 | sleep(2) 45 | 46 | ddmd_modlist_service = ModListService() 47 | -------------------------------------------------------------------------------- /game/mod_settings.rpy: -------------------------------------------------------------------------------- 1 | 2 | init python: 3 | import os 4 | import hashlib 5 | import shutil 6 | 7 | def is_original_file(path): 8 | if path.endswith("audio.rpa"): 9 | return hashlib.sha256(open(path, "rb").read()).hexdigest() == '121fedc50823e2a76d947025cc0f2dfa7c64b2454760b50091a64d1d36b7d2e7' 10 | elif path.endswith("fonts.rpa"): 11 | return hashlib.sha256(open(path, "rb").read()).hexdigest() == 'd48beafa7e1f3171b0e8e312f857af0e7eb387ef1e524a5be2595d46652d2018' 12 | elif path.endswith("images.rpa"): 13 | return hashlib.sha256(open(path, "rb").read()).hexdigest() == '6c3dccd4f35723ca1679b95710d4d09cec3d22439e24264bc6ff60d90640d393' 14 | elif path.endswith("scripts.rpa"): 15 | return hashlib.sha256(open(path, "rb").read()).hexdigest() == 'da7ba6d3cf9ec1ae666ec29ae07995a65d24cca400cd266e470deb55e03a51d4' 16 | 17 | def transfer_data(ddmm_path): 18 | try: 19 | for mod_dir in os.listdir(ddmm_path): 20 | mod_path = os.path.join(persistent.ddml_basedir, "game/mods", mod_dir) 21 | 22 | if not os.path.exists(mod_path): 23 | os.makedirs(mod_path) 24 | 25 | for ddmm_src, mod_dirs, _ in os.walk(os.path.join(ddmm_path, mod_dir)): 26 | dst_dir = ddmm_src.replace(os.path.join(ddmm_path, mod_dir), mod_path) 27 | for d in mod_dirs: 28 | if d in ("characters", "game"): 29 | shutil.copytree(os.path.join(ddmm_src, d), os.path.join(dst_dir, d)) 30 | 31 | renpy.hide_screen("ddmd_progress") 32 | renpy.show_screen("ddmd_dialog", message="Transferred all data sucessfully.") 33 | except OSError as err: 34 | mod_path = os.path.join(persistent.ddml_basedir, "game/mods", mod_dir) 35 | if os.path.exists(mod_path): 36 | shutil.rmtree(mod_path) 37 | renpy.hide_screen("ddmd_progress") 38 | renpy.show_screen("ddmd_dialog", message="A error has occured while transferring %s." % mod_dir, message2=str(err)) 39 | except Exception as err: 40 | mod_path = os.path.join(persistent.ddml_basedir, "game/mods", mod_dir) 41 | if os.path.exists(mod_path): 42 | shutil.rmtree(mod_path) 43 | renpy.hide_screen("ddmd_progress") 44 | renpy.show_screen("ddmd_dialog", message="A unknown error has occured while transferring%s." % mod_dir, message2=str(err)) 45 | 46 | def transfer_ddmm_data(): 47 | if not renpy.windows: 48 | renpy.show_screen("ddmd_dialog", message="Transferring data from DDMM is only supported on Windows.") 49 | return 50 | renpy.show_screen("ddmd_progress", message="Transferring data. Please wait.") 51 | ddmm_path = os.path.join( 52 | os.getenv("APPDATA"), "DokiDokiModManager/GameData/installs" 53 | ) 54 | if os.path.exists(ddmm_path): 55 | transfer_data(ddmm_path) 56 | else: 57 | renpy.show_screen("ddmd_dialog", message="Error: We were unable to locate a Doki Doki Manager folder in your AppData folder.", message2="If this is in error, please report it on Github.") 58 | 59 | screen mod_settings(): 60 | zorder 101 61 | style_prefix "modSettings" 62 | 63 | use ddmd_generic_window("Settings"): 64 | 65 | side "c": 66 | xpos 0.05 67 | ypos 0.15 68 | xsize int(450 * res_scale) 69 | ysize int(250 * res_scale) 70 | spacing 5 71 | 72 | viewport id "msw": 73 | mousewheel True 74 | draggable True 75 | has vbox 76 | spacing 1 77 | 78 | imagebutton: 79 | idle ConditionSwitch("config.gl2", Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_on", size=(int(48 * res_scale), int(48 * res_scale))), 80 | (int(55 * res_scale), int(13 * res_scale)), Text(_("Enable OpenGL 2 Globally"), style="modSettings_text", size=int(18 * res_scale))), "True", 81 | Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_off", size=(int(48 * res_scale), int(48 * res_scale))), (int(55 * res_scale), int(13 * res_scale)), 82 | Text(_("Enable OpenGL 2 Globally"), style="modSettings_text", size=int(18 * res_scale)))) 83 | hover ConditionSwitch("config.gl2", Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_on_hover", size=(int(48 * res_scale), int(48 * res_scale))), 84 | (int(55 * res_scale), 14), Text(_("Enable OpenGL 2 Globally"), style="modSettings_text", size=int(18 * res_scale))), "True", 85 | Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_off_hover", size=(int(48 * res_scale), int(48 * res_scale))), (int(55 * res_scale), int(13 * res_scale)), Text(_("Enable OpenGL 2 Globally"), 86 | style="modSettings_text", size=int(18 * res_scale)))) 87 | action If(config.gl2, Show("ddmd_confirm", Dissolve(0.25), message=_("Disable OpenGL 2?"), 88 | message2=_("Some mods may not have certain effects display if this setting is turned off. {b}A restart is required to load OpenGL 2{/b}."), 89 | yes_action=[SetField(config, "gl2", False), Function(set_settings_json), Quit()], no_action=Hide("ddmd_confirm", 90 | Dissolve(0.25))), Show("ddmd_confirm", Dissolve(0.25), message=_("Enable OpenGL 2?"), 91 | message2=_("Some mods may suffer from broken affects if this setting is turned on. {b}A restart is required to load OpenGL 2{/b}."), 92 | yes_action=[SetField(config, "gl2", True), Function(set_settings_json), Quit()], no_action=Hide("ddmd_confirm", 93 | Dissolve(0.25)))) 94 | 95 | imagebutton: 96 | idle ConditionSwitch("persistent.military_time", Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_on", size=(int(48 * res_scale), int(48 * res_scale))), 97 | (int(55 * res_scale), 13), Text(_("Use 24-Hour Format"), style="modSettings_text", size=int(18 * res_scale))), "True", 98 | Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_off", size=(int(48 * res_scale), int(48 * res_scale))), (int(55 * res_scale), int(13 * res_scale)), 99 | Text(_("Use 24-Hour Format"), style="modSettings_text", size=int(18 * res_scale)))) 100 | hover ConditionSwitch("persistent.military_time", Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_on_hover", size=(int(48 * res_scale), int(48 * res_scale))), 101 | (int(55 * res_scale), 14), Text(_("Use 24-Hour Format"), style="modSettings_text", size=int(18 * res_scale))), "True", 102 | Composite((int(250 * res_scale), int(40 * res_scale)), (0, 0), Transform("ddmd_toggle_off_hover", size=(int(48 * res_scale), int(48 * res_scale))), (int(55 * res_scale), int(13 * res_scale)), Text(_("Use 24-Hour Format"), 103 | style="modSettings_text", size=int(18 * res_scale)))) 104 | action [If(persistent.military_time, SetField(persistent, "military_time", False), 105 | SetField(persistent, "military_time", True))] 106 | 107 | if renpy.windows: 108 | imagebutton: 109 | idle Composite((int(410 * res_scale), int(40 * res_scale)), (10, 0), Transform("ddmd_transfer_icon", size=(int(36 * res_scale), int(36 * res_scale))), (int(55 * res_scale), int(7 * res_scale)), 110 | Text(_("[Beta] Transfer DDMM Mods to DDMD"), style="modSettings_text", substitute=False, size=int(18 * res_scale))) 111 | hover Composite((int(410 * res_scale), int(40 * res_scale)), (10, 0), Transform("ddmd_transfer_icon_hover", size=(int(36 * res_scale), int(36 * res_scale))), (int(55 * res_scale), int(7 * res_scale)), 112 | Text(_("[Beta] Transfer DDMM Mods to DDMD"), style="modSettings_text", substitute=False, size=int(18 * res_scale))) 113 | action If(not persistent.transfer_warning, Show("ddmd_confirm", message=_("Transfer Warning"), 114 | message2=_("Transferring mods is in beta and some mods may not work due to Ren'Py version differences. By accepting this disclaimer, transferring will proceed."), 115 | yes_action=[SetField(persistent, "transfer_warning", True), Hide("ddmd_confirm"), Function(transfer_ddmm_data)], 116 | no_action=Hide("ddmd_confirm")), Function(transfer_ddmm_data)) 117 | 118 | imagebutton: 119 | idle Composite((int(410 * res_scale), int(40 * res_scale)), (10, 0), Transform("ddmd_transfer_icon", size=(int(36 * res_scale), int(36 * res_scale))), (int(55 * res_scale), int(7 * res_scale)), 120 | Text(_("[Beta] Transfer DDML Mods to DDMD"), style="modSettings_text", substitute=False, size=int(18 * res_scale))) 121 | hover Composite((int(410 * res_scale), int(40 * res_scale)), (10, 0), Transform("ddmd_transfer_icon_hover", size=(int(36 * res_scale), int(36 * res_scale))), (int(55 * res_scale), int(7 * res_scale)), 122 | Text(_("[Beta] Transfer DDML Mods to DDMD"), style="modSettings_text", substitute=False, size=int(18 * res_scale))) 123 | action If(not persistent.transfer_warning, Show("ddmd_confirm", message=_("Transfer Warning"), 124 | message2=_("Transferring mods is in beta and some mods may not work due to Ren'Py version differences. By accepting this disclaimer, transferring will proceed."), 125 | yes_action=[SetField(persistent, "transfer_warning", True), Hide("ddmd_confirm"), Show("pc_directory", Dissolve(0.25), ml=True)], 126 | no_action=Hide("ddmd_confirm")), Show("pc_directory", Dissolve(0.25), ml=True)) 127 | -------------------------------------------------------------------------------- /game/mod_styles.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | ## Standard Ren'Py Font 4 | style renpy_generic_text: 5 | font "sdc_system/ddmd_app/Lato-Light.ttf" 6 | color "#fff" 7 | size int(24 * res_scale) 8 | outlines [] 9 | 10 | ## Main UI (Folder List) 11 | style mods_viewport is gui_viewport 12 | style mods_button is gui_button 13 | style mods_button_text is gui_button_text 14 | 15 | style mods_label is gui_label 16 | style mods_label_text is gui_label_text 17 | 18 | style mods_label: 19 | xpos int(50 * res_scale) 20 | ysize int(120 * res_scale) 21 | 22 | style mods_button: 23 | ysize None 24 | hover_sound gui.hover_sound 25 | activate_sound gui.activate_sound 26 | 27 | style mods_label_text: 28 | font "sdc_system/ddmd_app/Raleway-Bold.ttf" 29 | size int(38 * res_scale) 30 | color "#fff" 31 | outlines [(6, "#803366", 0, 0), (3, "#803366", 2, 2)] 32 | yalign 0.5 33 | 34 | style mods_text: 35 | size int(24 * res_scale) 36 | font "sdc_system/ddmd_app/Raleway-Bold.ttf" 37 | outlines [(2, "#803366", 0, 0), (1, "#803366", 1, 1)] 38 | 39 | ## Main UI 2 (Mod Options) 40 | style mods_info_label is mods_label 41 | style mods_info_label_text is mods_label_text 42 | 43 | style mods_info_label: 44 | ypos 0.5 45 | 46 | style mods_button_text: 47 | font "sdc_system/ddmd_app/Raleway-Bold.ttf" 48 | color "#fff" 49 | size int(24 * res_scale) 50 | outlines [(4, "#803366", 0, 0), (2, "#803366", 2, 2)] 51 | hover_outlines [(4, "#bb4c96", 0, 0), (2, "#bb4c96", 2, 2)] 52 | insensitive_outlines [(4, "#f374c9", 0, 0), (2, "#f374c9", 2, 2)] 53 | 54 | ## Main UI 3 (Sidebar Options) 55 | style mods_return_button is gui_button 56 | 57 | style mods_return_button: 58 | xpos int(45 * res_scale) 59 | yalign 1.0 60 | yoffset -30 61 | 62 | style mods_frame: 63 | padding gui.frame_borders.padding 64 | background Frame("sdc_system/ddmd_app/ddmd_frame.png", left=4, top=3, bottom=4, right=4, tile=False) 65 | 66 | ## Mod List 67 | style modList_text is renpy_generic_text 68 | style modList_button: 69 | ysize None 70 | style modList_button_text is mods_button_text: 71 | size int(18 * res_scale) 72 | style modList_frame is mods_frame 73 | 74 | ## Mod List Info 75 | style modInfo_text is renpy_generic_text 76 | style modInfo_button_text is mods_button_text 77 | style modInfo_frame is mods_frame: 78 | background Frame("sdc_system/ddmd_app/ddmd_frame.png", left=4, top=23, bottom=4, right=4, tile=False) 79 | 80 | ## Mod Confirm/Dialog 81 | style ddmd_confirm_frame: 82 | background Frame("sdc_system/ddmd_app/secondary_frame.png", top=1, bottom=1, left=1, right=1, tile=False) 83 | padding gui.confirm_frame_borders.padding 84 | xalign .5 85 | yalign .5 86 | 87 | style ddmd_confirm_prompt is gui_prompt 88 | style ddmd_confirm_prompt_text is gui_prompt_text 89 | style ddmd_confirm_label_text is renpy_generic_text 90 | style ddmd_confirm_text is renpy_generic_text 91 | style ddmd_confirm_button is gui_medium_button 92 | style ddmd_confirm_button_text is mods_button_text 93 | 94 | style ddmd_confirm_prompt_text: 95 | color "#fff" 96 | outlines [] 97 | text_align 0.0 98 | layout "subtitle" 99 | 100 | style ddmd_confirm_button: 101 | ysize None 102 | hover_sound gui.hover_sound 103 | activate_sound gui.activate_sound 104 | 105 | ## Mod Hover Info 106 | style mods_hover_frame: 107 | background Frame("#fff", left=4, top=4, bottom=4, right=4, tile=False) 108 | 109 | style mods_hover_text: 110 | font "sdc_system/ddmd_app/Lato-Regular.ttf" 111 | color "#000" 112 | outlines [] 113 | size int(12 * res_scale) 114 | 115 | ## File Explorer 116 | style pc_dir_frame is mods_frame 117 | style pc_dir_button_text is renpy_generic_text: 118 | text_align 0.0 119 | 120 | style pc_dir_scrollbar: 121 | xsize int(8 * res_scale) 122 | ysize int(96 * res_scale) 123 | base_bar Frame("#222222") 124 | thumb Frame("sdc_system/file_app/FileExplorerHBar.png", tile=False) 125 | 126 | style pc_dir_vscrollbar: 127 | xsize int(8 * res_scale) 128 | ysize int(96 * res_scale) 129 | base_bar Frame("#222222") 130 | thumb Frame("sdc_system/file_app/FileExplorerVBar.png", tile=False) 131 | 132 | style pc_dir_text is pc_dir_button_text 133 | 134 | ## Mod Settings 135 | style modSettings_text is renpy_generic_text 136 | style modSettings_button: 137 | ysize None 138 | hover_sound gui.hover_sound 139 | activate_sound gui.activate_sound 140 | style modSettings_button_text is modList_button_text 141 | style modSettings_frame is modList_frame 142 | 143 | ## Time 144 | style time_text: 145 | color "#fff" 146 | size int(24 * res_scale) 147 | font "sdc_system/ddmd_app/Quicksand-Light.ttf" 148 | outlines [] -------------------------------------------------------------------------------- /game/mod_transforms.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | ## MD Show Transition 4 | transform ml_overlay_effect: 5 | on show: 6 | alpha 0.0 7 | linear 0.5 alpha 1.0 8 | 9 | ## Confirm Transitions 10 | transform android_like_overlay: 11 | on show: 12 | alpha 0.0 13 | linear 0.3 alpha 1.0 14 | on hide: 15 | alpha 1.0 16 | linear 0.3 alpha 0.0 17 | 18 | transform android_like_frame: 19 | subpixel True 20 | on show: 21 | ycenter config.screen_height + int(180 * res_scale) yanchor 1.0 alpha 1.00 nearest True 22 | easein 0.3 ycenter config.screen_height - int(100 * res_scale) 23 | on hide: 24 | easein .75 ycenter config.screen_height + int(180 * res_scale) nearest True 25 | 26 | ## Mod Hover Transition 27 | transform windows_like_effect: 28 | on show: 29 | alpha 0.0 30 | pause 1.0 31 | linear 0.25 alpha 1.0 32 | on hide: 33 | alpha 1.0 34 | linear 0.25 alpha 0.0 -------------------------------------------------------------------------------- /game/options.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | # This file customizes what your mod is and and how it starts and builds! 4 | 5 | # This controls what your mod is called. 6 | define config.name = "Doki Doki Mod Docker (Alpha)" 7 | define config.window_title = config.name 8 | 9 | # This controls whether you want your mod name to show in the main menu. 10 | # If your mod name is big, it is suggested to turn this off. 11 | define gui.show_name = False 12 | 13 | # This controls the version number of your mod. 14 | define config.version = "1.1.1" 15 | 16 | # This adds information about your mod in the About screen. 17 | # DDLC does not have a 'About' screen so you can leave this blank. 18 | define gui.about = _("") 19 | 20 | # This control the name of your mod build when you package your mod 21 | # in the Ren'Py Launcher or DDMM (Doki Doki Mod Maker). 22 | # Note: 23 | # The build name is ASCII only so no numbers, spaces, or semicolons. 24 | # Example: Doki Doki Yuri Time to DokiDokiYuriTime 25 | define build.name = "DokiDokiModDocker" 26 | 27 | # This controls the save folder name of your mod. 28 | # Finding your Saves: 29 | # Windows: %AppData%/RenPy/ 30 | # macOS: $HOME/Library/RenPy/ (Un-hide the Library Folder) 31 | # Linux: $HOME/.renpy/ 32 | define config.save_directory = "DD-ModDocker" 33 | 34 | # This controls the window logo of your mod. 35 | define config.window_icon = "sdc_system/DDMDLogo.png" 36 | 37 | init python: 38 | build.executable_name = "DDMD" 39 | 40 | if len(renpy.loadsave.location.locations) > 1: del(renpy.loadsave.location.locations[1]) 41 | renpy.game.preferences.pad_enabled = False 42 | def replace_text(s): 43 | s = s.replace('--', u'\u2014') 44 | s = s.replace(' - ', u'\u2014') 45 | return s 46 | config.replace_text = replace_text 47 | 48 | def game_menu_check(): 49 | if quick_menu: renpy.call_in_new_context('_game_menu') 50 | 51 | config.game_menu_action = game_menu_check 52 | 53 | def force_integer_multiplier(width, height): 54 | if float(width) / float(height) < float(config.screen_width) / float(config.screen_height): 55 | return (width, float(width) / (float(config.screen_width) / float(config.screen_height))) 56 | else: 57 | return (float(height) * (float(config.screen_width) / float(config.screen_height)), height) 58 | 59 | ## Build configuration ######################################################### 60 | ## 61 | ## This section controls how Ren'Py turns your project into distribution files. 62 | 63 | init python: 64 | ## The following variables take file patterns. File patterns are case- 65 | ## insensitive, and matched against the path relative to the base directory, 66 | ## with and without a leading /. If multiple patterns match, the first is 67 | ## used. 68 | ## 69 | ## In a pattern: 70 | ## * matches all characters, except the directory separator. 71 | ## ** matches all characters, including the directory separator. 72 | ## 73 | ## Examples: 74 | ## "*.txt" matches txt files in the base directory. 75 | ## "game/**.ogg" matches ogg files in the game directory or any of its 76 | ## subdirectories. 77 | ## "**.psd" matches psd files anywhere in the project. 78 | 79 | # These variables declare the packages to build your mod that is Team Salvato 80 | # IPG compliant. Do not mess with these variables whatsoever. 81 | # build.package("DDMD6",'zip','mod',description="Ren'Py 6 DDMD Build (Alpha)") 82 | # build.package("full",'zip','windows linux mac renpy mod all',description="Ren'Py 7 DDMD Build: All") 83 | build.package("ddmd-win",'zip','windows linux renpy mod all',description="Ren'Py 7 DDMD Build: Windows/Linux") 84 | build.package("ddmd-mac",'app-zip','mac renpy mod all',description="Ren'Py 7 DDMD Build: macOS") 85 | 86 | # These variables declare the archives that will be made to your packaged mod. 87 | # To add another archive, make a build.archive variable like in this example: 88 | build.archive("ddml", 'mod') 89 | build.archive("mod_patches", 'mod') 90 | 91 | ############################################################# 92 | # These variables classify packages for PC and Android platforms. 93 | # Make sure to add 'all' to your build.classify variable if you are planning 94 | # to build your mod on Android like in this example. 95 | # Example: build.classify("game/**.pdf", "scripts all") 96 | 97 | build.classify("game/**.rpyc", "ddml") 98 | build.classify("game/sdc_system/**", "ddml") 99 | build.classify("game/python-packages/**", "mod") 100 | build.classify("game/ddmc.json", "mod") 101 | build.classify("How to use DDMD (macOS).txt", "mod") 102 | build.classify("How to use DDMD (Windows, Linux).txt", "mod") 103 | build.classify("ddmd_settings.json", "mod") 104 | build.classify("game/mod_patches/**", "mod_patches") 105 | 106 | build.classify("game/MLSaves/**", None) 107 | build.classify("game/mods/**", None) 108 | build.classify("game/docker_custom_image.png", None) 109 | build.classify("game/firstrun", None) 110 | build.classify("BUILDING.md", None) 111 | build.classify('**~', None) 112 | build.classify('**.bak', None) 113 | build.classify('**/.**', None) 114 | build.classify('**/#**', None) 115 | build.classify('**/thumbs.db', None) 116 | build.classify('**.rpy', None) 117 | build.classify('**.psd', None) 118 | build.classify('**.sublime-project', None) 119 | build.classify('**.sublime-workspace', None) 120 | build.classify('/music/*.*', None) 121 | build.classify('script-regex.txt', None) 122 | build.classify('/game/10', None) 123 | build.classify('/game/cache/*.*', None) 124 | build.classify('**.rpa', None) 125 | 126 | # This sets' README.html as documentation 127 | build.documentation('README.html') 128 | 129 | build.include_old_themes = False 130 | -------------------------------------------------------------------------------- /game/python-packages/ddmd_api.py: -------------------------------------------------------------------------------- 1 | import renpy 2 | import json 3 | import os 4 | 5 | class ModDocker_API: 6 | 7 | def __init__(self): 8 | self.mods_path = os.path.join(renpy.store.persistent.ddml_basedir, "game/mods") 9 | self.mod_basedir = renpy.config.basedir.replace("\\", "/") 10 | self.mod_gamedir = renpy.config.gamedir.replace("\\", "/") 11 | self.mod_info = [] 12 | self.multipersistent = None 13 | 14 | def parse_mod_info(self, path): 15 | """ 16 | Parses a given JSON 17 | """ 18 | with open(path, "r") as mod_info: 19 | return json.load(mod_info) 20 | 21 | def get_mod_info(self): 22 | """ 23 | Get's the current mod containers' build name. 24 | """ 25 | if os.path.exists(os.path.join(self.mod_basedir, "modinfo.json")): 26 | self.mod_info = self.parse_mod_info(os.path.join(self.mod_basedir, "modinfo.json")) 27 | else: 28 | self.mod_info = [ 29 | { 30 | "modName": renpy.config.name, 31 | "modVersion": renpy.config.version, 32 | "buildName": renpy.store.build.name, 33 | "saveDir": renpy.config.savedir.replace("\\", "/"), 34 | } 35 | ] 36 | 37 | def get_current_container_path(self): 38 | """ 39 | Returns the mods base directory 40 | """ 41 | return self.mod_basedir 42 | 43 | def get_current_container_game_folder(self): 44 | """ 45 | Returns the mods game directory 46 | """ 47 | return self.mod_gamedir 48 | 49 | def get_current_container_name(self): 50 | """ 51 | Returns the mods name 52 | """ 53 | return self.mod_info[0]["modName"] 54 | 55 | def get_current_container_version(self): 56 | """ 57 | Returns the mod version 58 | """ 59 | return self.mod_info[0]["modVersion"] 60 | 61 | def get_current_container_build_name(self): 62 | """ 63 | Returns the mod build name 64 | """ 65 | return self.mod_info[0]["buildName"] 66 | 67 | def get_current_container_save_folder(self): 68 | """ 69 | Returns the mod save directory 70 | """ 71 | return self.mod_info[0]["saveDir"] 72 | 73 | def is_multiple_copies(self): 74 | """ 75 | Checks if the user has multiple copies of the same mod 76 | """ 77 | same_mods = [] 78 | for x in os.listdir(self.mods_path): 79 | try: temp = self.parse_mod_info(os.path.join(self.mods_path, x, "modInfo.json")) 80 | except IOError: continue 81 | if temp[0]["buildName"] == self.get_current_container_build_name() and temp[0]["modName"] == self.get_current_container_name(): 82 | same_mods.append(x) 83 | if len(same_mods) != 0: 84 | return True 85 | return False 86 | 87 | def set_multipersistent(self, persist_name): 88 | """ 89 | [BETA] Makes a multipersistent variable to use across mods for variable 90 | data or other things. 91 | """ 92 | self.multipersistent = renpy.persistent.MultiPersistent(persist_name) 93 | 94 | def get_multipersistent(self): 95 | """ 96 | Returns the multipersistent variable 97 | """ 98 | return self.multipersistent 99 | 100 | def write_mod_data(self): 101 | """ 102 | Writes the mod data to a JSON file 103 | """ 104 | with open(os.path.join(self.mod_basedir, "modinfo.json"), "w") as mod_info: 105 | json.dump(self.mod_info, mod_info) -------------------------------------------------------------------------------- /game/python-packages/singleton.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | import os 5 | import tempfile 6 | import logging 7 | 8 | 9 | class SingleInstanceException(BaseException): 10 | pass 11 | 12 | 13 | class SingleInstance: 14 | 15 | """ 16 | If you want to prevent your script from running in parallel just instantiate SingleInstance() class. If is there another instance already running it will throw a `SingleInstanceException`. 17 | 18 | >>> import tendo 19 | ... me = SingleInstance() 20 | 21 | This option is very useful if you have scripts executed by crontab at small amounts of time. 22 | 23 | Remember that this works by creating a lock file with a filename based on the full path to the script file. 24 | 25 | Providing a flavor_id will augment the filename with the provided flavor_id, allowing you to create multiple singleton instances from the same file. This is particularly useful if you want specific functions to have their own singleton instances. 26 | """ 27 | 28 | def __init__(self, flavor_id=""): 29 | import sys 30 | self.initialized = False 31 | basename = os.path.splitext(os.path.abspath(sys.argv[0]))[0].replace( 32 | "/", "-").replace(":", "").replace("\\", "-") + '-%s' % flavor_id + '.lock' 33 | # os.path.splitext(os.path.abspath(sys.modules['__main__'].__file__))[0].replace("/", "-").replace(":", "").replace("\\", "-") + '-%s' % flavor_id + '.lock' 34 | self.lockfile = os.path.normpath( 35 | tempfile.gettempdir() + '/' + basename) 36 | 37 | logger.debug("SingleInstance lockfile: " + self.lockfile) 38 | if sys.platform == 'win32': 39 | try: 40 | # file already exists, we try to remove (in case previous 41 | # execution was interrupted) 42 | if os.path.exists(self.lockfile): 43 | os.unlink(self.lockfile) 44 | self.fd = os.open( 45 | self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) 46 | except OSError: 47 | type, e, tb = sys.exc_info() 48 | if e.errno == 13: 49 | logger.error( 50 | "Another instance is already running, quitting.") 51 | raise SingleInstanceException() 52 | print(e.errno) 53 | raise 54 | else: # non Windows 55 | import fcntl 56 | self.fp = open(self.lockfile, 'w') 57 | self.fp.flush() 58 | try: 59 | fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB) 60 | except IOError: 61 | logger.warning( 62 | "Another instance is already running, quitting.") 63 | raise SingleInstanceException() 64 | self.initialized = True 65 | 66 | def __del__(self): 67 | import sys 68 | import os 69 | if not self.initialized: 70 | return 71 | try: 72 | if sys.platform == 'win32': 73 | if hasattr(self, 'fd'): 74 | os.close(self.fd) 75 | os.unlink(self.lockfile) 76 | else: 77 | import fcntl 78 | fcntl.lockf(self.fp, fcntl.LOCK_UN) 79 | # os.close(self.fp) 80 | if os.path.isfile(self.lockfile): 81 | os.unlink(self.lockfile) 82 | except Exception as e: 83 | if logger: 84 | logger.warning(e) 85 | else: 86 | print("Unloggable error: %s" % e) 87 | sys.exit(-1) 88 | 89 | 90 | def f(name): 91 | tmp = logger.level 92 | logger.setLevel(logging.CRITICAL) # we do not want to see the warning 93 | try: 94 | me2 = SingleInstance(flavor_id=name) # noqa 95 | except SingleInstanceException: 96 | sys.exit(-1) 97 | logger.setLevel(tmp) 98 | pass 99 | 100 | logger = logging.getLogger("tendo.singleton") 101 | logger.addHandler(logging.StreamHandler()) 102 | -------------------------------------------------------------------------------- /game/renpy_patches.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2019-2022-2024 Azariel Del Carmen (bronya_rand). All rights reserved. 2 | 3 | ## Patches 'wmic' environment variables with 'powershell' instead. 4 | python early: 5 | import os 6 | os.environ['wmic process get Description'] = "powershell (Get-Process).ProcessName" 7 | os.environ['wmic os get version'] = "powershell (Get-WmiObject -class Win32_OperatingSystem).Version" 8 | 9 | init -100 python: 10 | if renpy.windows: 11 | onedrive_path = os.environ.get("OneDrive") 12 | if onedrive_path is not None: 13 | if onedrive_path in config.basedir: 14 | raise Exception("Mod Docker cannot be run from a cloud folder. Move Mod Docker to another location and try again.") 15 | 16 | init -1 python: 17 | ## Fixes a issue where some transitions (menu bg) reset themselves 18 | config.atl_start_on_show = False 19 | 20 | init 1 python: 21 | def patched_screenshot(): 22 | srf = renpy.display.draw.screenshot(None) 23 | return srf 24 | 25 | screenshot_srf = patched_screenshot 26 | -------------------------------------------------------------------------------- /game/saves.rpy: -------------------------------------------------------------------------------- 1 | ## Copyright 2023-2024 Azariel Del Carmen (bronya_rand) 2 | 3 | python early: 4 | import os 5 | import json 6 | 7 | def save_path(): 8 | if renpy.macintosh: 9 | rv = "~/Library/RenPy/DD-ModDocker" 10 | return os.path.expanduser(rv) 11 | 12 | elif renpy.windows: 13 | if 'APPDATA' in os.environ: 14 | return os.path.join(os.environ['APPDATA'], "RenPy", "DD-ModDocker") 15 | else: 16 | rv = "~/RenPy/DD-ModDocker" 17 | return os.path.expanduser(rv) 18 | 19 | else: 20 | rv = "~/.renpy/DD-ModDocker" 21 | return os.path.expanduser(rv) 22 | 23 | try: 24 | with open(os.path.join(renpy.config.basedir, "selectedmod.json"), "r") as mod_json: 25 | temp = json.load(mod_json) 26 | selectedMod = temp['modName'] 27 | except IOError: 28 | selectedMod = "DDLC" 29 | 30 | renpy.config.savedir = os.path.join(save_path(), selectedMod) -------------------------------------------------------------------------------- /game/sdc_system/DDMDLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/DDMDLogo.png -------------------------------------------------------------------------------- /game/sdc_system/backups/settings.backup: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "config_gl2": false 4 | } 5 | ] -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/Lato-Light.ttf -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/Lato-Regular.ttf -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name "Raleway". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/Quicksand-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/Quicksand-Light.ttf -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/Raleway-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/Raleway-Bold.ttf -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/close.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/closeHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/closeHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/ddmd_confirm_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/ddmd_confirm_overlay.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/ddmd_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/ddmd_frame.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/disabled.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/disabledHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/disabledHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/enabled.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/enabledHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/enabledHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/modInstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/modInstall.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/modInstallHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/modInstallHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/openBrowser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/openBrowser.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/openBrowserHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/openBrowserHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/refresh.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/refreshHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/refreshHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/restart.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/restartHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/restartHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/return.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/returnHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/returnHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/search.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/searchHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/searchHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/searchWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/searchWindow.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/searchWindowHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/searchWindowHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/secondary_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/secondary_frame.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/selectedMod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/selectedMod.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/selectedModHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/selectedModHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/settings.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/settingsHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/settingsHover.png -------------------------------------------------------------------------------- /game/sdc_system/ddmd_app/steam_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/ddmd_app/steam_frame.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/FileExplorerHBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/FileExplorerHBar.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/FileExplorerVBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/FileExplorerVBar.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/OSBack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/OSBack.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/OSFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/OSFile.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/OSFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/OSFolder.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/networkDrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/networkDrive.png -------------------------------------------------------------------------------- /game/sdc_system/file_app/physicalDrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/file_app/physicalDrive.png -------------------------------------------------------------------------------- /game/sdc_system/settings_app/transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/settings_app/transfer.png -------------------------------------------------------------------------------- /game/sdc_system/settings_app/transferHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/game/sdc_system/settings_app/transferHover.png -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/icon.icns -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDModDocker/6555d6d5e8706e08e34d226bae662738ad02eaf6/icon.ico -------------------------------------------------------------------------------- /renpy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This file is part of Ren'Py. The license below applies to Ren'Py only. 4 | # Games and other projects that use Ren'Py may use a different license. 5 | 6 | # Copyright 2004-2024 Tom Rothamel 7 | # 8 | # Permission is hereby granted, free of charge, to any person 9 | # obtaining a copy of this software and associated documentation files 10 | # (the "Software"), to deal in the Software without restriction, 11 | # including without limitation the rights to use, copy, modify, merge, 12 | # publish, distribute, sublicense, and/or sell copies of the Software, 13 | # and to permit persons to whom the Software is furnished to do so, 14 | # subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | from __future__ import print_function, absolute_import 28 | 29 | import os 30 | import sys 31 | import warnings 32 | 33 | # Functions to be customized by distributors. ################################ 34 | 35 | def path_to_gamedir(basedir, name): 36 | """ 37 | Returns the absolute path to the directory containing the game 38 | scripts an assets. (This becomes config.gamedir.) 39 | 40 | `basedir` 41 | The base directory (config.basedir) 42 | `name` 43 | The basename of the executable, with the extension removed. 44 | """ 45 | 46 | # A list of candidate game directory names. 47 | candidates = [ name ] 48 | 49 | # Add candidate names that are based on the name of the executable, 50 | # split at spaces and underscores. 51 | game_name = name 52 | 53 | while game_name: 54 | prefix = game_name[0] 55 | game_name = game_name[1:] 56 | 57 | if prefix == ' ' or prefix == '_': 58 | candidates.append(game_name) 59 | 60 | # Add default candidates. 61 | candidates.extend([ 'game', 'data', 'launcher/game' ]) 62 | 63 | # Take the first candidate that exists. 64 | for i in candidates: 65 | 66 | if i == "renpy": 67 | continue 68 | 69 | gamedir = os.path.join(basedir, i) 70 | 71 | if os.path.isdir(gamedir): 72 | break 73 | 74 | else: 75 | gamedir = basedir 76 | 77 | return gamedir 78 | 79 | 80 | def path_to_common(renpy_base): 81 | """ 82 | Returns the absolute path to the Ren'Py common directory. 83 | 84 | `renpy_base` 85 | The absolute path to the Ren'Py base directory, the directory 86 | containing this file. 87 | """ 88 | path = renpy_base + "/renpy/common" 89 | 90 | if os.path.isdir(path): 91 | return path 92 | return None 93 | 94 | 95 | def path_to_saves(gamedir, save_directory=None): # type: (str, str|None) -> str 96 | """ 97 | Given the path to a Ren'Py game directory, and the value of config. 98 | save_directory, returns absolute path to the directory where save files 99 | will be placed. 100 | 101 | `gamedir` 102 | The absolute path to the game directory. 103 | 104 | `save_directory` 105 | The value of config.save_directory. 106 | """ 107 | 108 | import renpy # @UnresolvedImport 109 | 110 | if save_directory is None: 111 | save_directory = renpy.config.save_directory 112 | save_directory = renpy.exports.fsencode(save_directory) # type: ignore 113 | 114 | # Makes sure the permissions are right on the save directory. 115 | def test_writable(d): 116 | try: 117 | fn = os.path.join(d, "test.txt") 118 | open(fn, "w").close() 119 | open(fn, "r").close() 120 | os.unlink(fn) 121 | return True 122 | except Exception: 123 | return False 124 | 125 | # Android. 126 | if renpy.android: 127 | paths = [ 128 | os.path.join(os.environ["ANDROID_OLD_PUBLIC"], "game/saves"), 129 | os.path.join(os.environ["ANDROID_PRIVATE"], "saves"), 130 | os.path.join(os.environ["ANDROID_PUBLIC"], "saves"), 131 | ] 132 | 133 | for rv in paths: 134 | if os.path.isdir(rv) and test_writable(rv): 135 | break 136 | else: 137 | rv = paths[-1] 138 | 139 | print("Saving to", rv) 140 | return rv 141 | 142 | if renpy.ios: 143 | from pyobjus import autoclass # type: ignore 144 | from pyobjus.objc_py_types import enum # type: ignore 145 | 146 | NSSearchPathDirectory = enum("NSSearchPathDirectory", NSDocumentDirectory=9) 147 | NSSearchPathDomainMask = enum("NSSearchPathDomainMask", NSUserDomainMask=1) 148 | 149 | NSFileManager = autoclass('NSFileManager') 150 | manager = NSFileManager.defaultManager() 151 | url = manager.URLsForDirectory_inDomains_( 152 | NSSearchPathDirectory.NSDocumentDirectory, 153 | NSSearchPathDomainMask.NSUserDomainMask, 154 | ).lastObject() 155 | 156 | # url.path seems to change type based on iOS version, for some reason. 157 | try: 158 | rv = url.path().UTF8String() 159 | except Exception: 160 | rv = url.path.UTF8String() 161 | 162 | 163 | if isinstance(rv, bytes): 164 | rv = rv.decode("utf-8") 165 | 166 | print("Saving to", rv) 167 | return rv 168 | 169 | # No save directory given. 170 | if not save_directory: 171 | return os.path.join(gamedir, "saves") 172 | 173 | if "RENPY_PATH_TO_SAVES" in os.environ: 174 | return os.environ["RENPY_PATH_TO_SAVES"] + "/" + save_directory 175 | 176 | # Search the path above Ren'Py for a directory named "Ren'Py Data". 177 | # If it exists, then use that for our save directory. 178 | path = renpy.config.renpy_base 179 | 180 | while True: 181 | if os.path.isdir(path + "/Ren'Py Data"): 182 | return path + "/Ren'Py Data/" + save_directory 183 | 184 | newpath = os.path.dirname(path) 185 | if path == newpath: 186 | break 187 | path = newpath 188 | 189 | # Otherwise, put the saves in a platform-specific location. 190 | if renpy.macintosh: 191 | rv = "~/Library/RenPy/" + save_directory 192 | return os.path.expanduser(rv) 193 | 194 | elif renpy.windows: 195 | if 'APPDATA' in os.environ: 196 | return os.environ['APPDATA'] + "/RenPy/" + save_directory 197 | else: 198 | rv = "~/RenPy/" + renpy.config.save_directory # type: ignore 199 | return os.path.expanduser(rv) 200 | 201 | else: 202 | rv = "~/.renpy/" + save_directory 203 | return os.path.expanduser(rv) 204 | 205 | 206 | # Returns the path to the Ren'Py base directory (containing common and 207 | # the launcher, usually.) 208 | def path_to_renpy_base(): 209 | """ 210 | Returns the absolute path to the Ren'Py base directory. 211 | """ 212 | 213 | renpy_base = os.path.dirname(os.path.abspath(__file__)) 214 | renpy_base = os.path.abspath(renpy_base) 215 | 216 | return renpy_base 217 | 218 | def path_to_logdir(basedir): 219 | """ 220 | Returns the absolute path to the log directory. 221 | `basedir` 222 | The base directory (config.basedir) 223 | """ 224 | 225 | import renpy # @UnresolvedImport 226 | 227 | if renpy.android: 228 | return os.environ['ANDROID_PUBLIC'] 229 | 230 | return basedir 231 | 232 | def predefined_searchpath(commondir, old_gamedir): 233 | import renpy # @UnresolvedImport 234 | 235 | # The default gamedir, in private. 236 | if renpy.config.gamedir != old_gamedir: 237 | searchpath = [ 238 | old_gamedir, 239 | renpy.config.gamedir, 240 | ] 241 | else: 242 | searchpath = [ renpy.config.gamedir ] 243 | 244 | if renpy.android: 245 | # The public android directory. 246 | if "ANDROID_PUBLIC" in os.environ: 247 | android_game = os.path.join(os.environ["ANDROID_PUBLIC"], "game") 248 | 249 | if os.path.exists(android_game): 250 | searchpath.insert(0, android_game) 251 | 252 | # Asset packs. 253 | packs = [ 254 | "ANDROID_PACK_FF1", "ANDROID_PACK_FF2", 255 | "ANDROID_PACK_FF3", "ANDROID_PACK_FF4", 256 | ] 257 | 258 | for i in packs: 259 | if i not in os.environ: 260 | continue 261 | 262 | assets = os.environ[i] 263 | 264 | for i in [ "renpy/common", "game" ]: 265 | dn = os.path.join(assets, i) 266 | if os.path.isdir(dn): 267 | searchpath.append(dn) 268 | else: 269 | # Add path from env variable, if any 270 | if "RENPY_SEARCHPATH" in os.environ: 271 | searchpath.extend(os.environ["RENPY_SEARCHPATH"].split("::")) 272 | 273 | if commondir and os.path.isdir(commondir): 274 | searchpath.append(commondir) 275 | 276 | if renpy.android or renpy.ios: 277 | print("Mobile search paths:" , " ".join(searchpath)) 278 | 279 | return searchpath 280 | 281 | ############################################################################## 282 | 283 | 284 | android = ("ANDROID_PRIVATE" in os.environ) 285 | 286 | def main(): 287 | 288 | renpy_base = path_to_renpy_base() 289 | 290 | sys.path.append(renpy_base) 291 | 292 | # Ignore warnings. 293 | warnings.simplefilter("ignore", DeprecationWarning) 294 | 295 | # Start Ren'Py proper. 296 | try: 297 | import renpy.bootstrap 298 | except ImportError: 299 | print("Could not import renpy.bootstrap. Please ensure you decompressed Ren'Py", file=sys.stderr) 300 | print("correctly, preserving the directory structure.", file=sys.stderr) 301 | raise 302 | 303 | # Set renpy.__main__ to this module. 304 | renpy.__main__ = sys.modules[__name__] # type: ignore 305 | 306 | renpy.bootstrap.bootstrap(renpy_base) 307 | 308 | 309 | if __name__ == "__main__": 310 | main() 311 | -------------------------------------------------------------------------------- /renpy/bootstrap.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2024 Tom Rothamel 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation files 5 | # (the "Software"), to deal in the Software without restriction, 6 | # including without limitation the rights to use, copy, modify, merge, 7 | # publish, distribute, sublicense, and/or sell copies of the Software, 8 | # and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from __future__ import division, absolute_import, with_statement, print_function, unicode_literals 23 | from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # * 24 | 25 | from typing import Optional 26 | 27 | import os 28 | import sys 29 | import subprocess 30 | import io 31 | 32 | # Encoding and sys.stderr/stdout handling ###################################### 33 | 34 | FSENCODING = sys.getfilesystemencoding() or "utf-8" 35 | 36 | # Sets the default encoding to utf-8. 37 | old_stdout = sys.stdout 38 | old_stderr = sys.stderr 39 | 40 | if PY2: 41 | sys_executable = sys.executable 42 | reload(sys) # type: ignore 43 | sys.setdefaultencoding("utf-8") # type: ignore 44 | sys.executable = sys_executable 45 | 46 | def _setdefaultencoding(name): 47 | """ 48 | This is install in sys to prevent games from trying to change the default 49 | encoding. 50 | """ 51 | 52 | sys.setdefaultencoding = _setdefaultencoding # type: ignore 53 | 54 | 55 | sys.stdout = old_stdout 56 | sys.stderr = old_stderr 57 | 58 | import renpy.error 59 | 60 | 61 | class NullFile(io.IOBase): 62 | """ 63 | This file raises an error on input, and IOError on read. 64 | """ 65 | 66 | def write(self, s): 67 | return 68 | 69 | def read(self, length=None): 70 | raise IOError("Not implemented.") 71 | 72 | def flush(self): 73 | return 74 | 75 | 76 | def null_files(): 77 | try: 78 | if (sys.stderr is None) or sys.stderr.fileno() < 0: 79 | sys.stderr = NullFile() 80 | 81 | if (sys.stdout is None) or sys.stdout.fileno() < 0: 82 | sys.stdout = NullFile() 83 | except Exception: 84 | pass 85 | 86 | 87 | null_files() 88 | 89 | # Tracing ###################################################################### 90 | 91 | trace_file = None 92 | trace_local = None 93 | 94 | 95 | def trace_function(frame, event, arg): 96 | fn = os.path.basename(frame.f_code.co_filename) 97 | trace_file.write("{} {} {} {}\n".format(fn, frame.f_lineno, frame.f_code.co_name, event)) # type: ignore 98 | return trace_local 99 | 100 | 101 | def enable_trace(level): 102 | global trace_file 103 | global trace_local 104 | 105 | trace_file = open("trace.txt", "w", buffering=1, encoding="utf-8") 106 | 107 | if level > 1: 108 | trace_local = trace_function 109 | else: 110 | trace_local = None 111 | 112 | sys.settrace(trace_function) 113 | 114 | 115 | def mac_start(fn): 116 | """ 117 | os.start compatibility for mac. 118 | """ 119 | 120 | os.system("open " + fn) # type: ignore 121 | 122 | def popen_del(self, *args, **kwargs): 123 | """ 124 | Fix an issue where the __del__ method of popen doesn't work. 125 | """ 126 | 127 | return 128 | 129 | def get_alternate_base(basedir, always=False): 130 | """ 131 | :undocumented: 132 | 133 | Tries to find an alternate base directory. This exists in a writable 134 | location, and is intended for use by a game that downloads its assets 135 | to the device (generally for ios or android, where the assets may be 136 | too big for the app store). 137 | """ 138 | 139 | # Determine the alternate base directory location. 140 | 141 | if renpy.android: 142 | altbase = os.path.join(os.environ["ANDROID_PRIVATE"], "base") 143 | 144 | elif renpy.ios: 145 | from pyobjus import autoclass # type: ignore 146 | from pyobjus.objc_py_types import enum # type: ignore 147 | 148 | NSSearchPathDirectory = enum("NSSearchPathDirectory", NSApplicationSupportDirectory=14) 149 | NSSearchPathDomainMask = enum("NSSearchPathDomainMask", NSUserDomainMask=1) 150 | 151 | NSFileManager = autoclass('NSFileManager') 152 | manager = NSFileManager.defaultManager() 153 | url = manager.URLsForDirectory_inDomains_( 154 | NSSearchPathDirectory.NSApplicationSupportDirectory, 155 | NSSearchPathDomainMask.NSUserDomainMask, 156 | ).lastObject() 157 | 158 | # url.path seems to change type based on iOS version, for some reason. 159 | try: 160 | altbase = url.path().UTF8String() 161 | except Exception: 162 | altbase = url.path.UTF8String() 163 | 164 | if isinstance(altbase, bytes): 165 | altbase = altbase.decode("utf-8") 166 | 167 | else: 168 | altbase = os.path.join(basedir, "base") 169 | 170 | if always: 171 | return altbase 172 | 173 | # Check to see if there's a game in there created with the 174 | # current version of Ren'Py. 175 | 176 | def ver(s): 177 | """ 178 | Returns the first three components of a version string. 179 | """ 180 | 181 | return tuple(int(i) for i in s.split(".")[:3]) 182 | 183 | import json 184 | 185 | version_json = os.path.join(altbase, "update", "version.json") 186 | 187 | if not os.path.exists(version_json): 188 | return basedir 189 | 190 | with open(version_json, "r") as f: 191 | modules = json.load(f) 192 | 193 | for v in modules.values(): 194 | if ver(v["renpy_version"]) != ver(renpy.version_only): 195 | return basedir 196 | 197 | return altbase 198 | 199 | 200 | def bootstrap(renpy_base): 201 | 202 | global renpy 203 | 204 | import renpy.config 205 | import renpy.log 206 | 207 | # Remove a legacy environment setting. 208 | if os.environ.get("SDL_VIDEODRIVER", "") == "windib": 209 | del os.environ["SDL_VIDEODRIVER"] 210 | 211 | if not isinstance(renpy_base, str): 212 | renpy_base = str(renpy_base, FSENCODING) 213 | 214 | # If environment.txt exists, load it into the os.environ dictionary. 215 | if os.path.exists(renpy_base + "/environment.txt"): 216 | evars = { } 217 | with open(renpy_base + "/environment.txt", "r") as f: 218 | code = compile(f.read(), renpy_base + "/environment.txt", 'exec') 219 | exec(code, evars) 220 | for k, v in evars.items(): 221 | if k not in os.environ: 222 | os.environ[k] = str(v) 223 | 224 | # Also look for it in an alternate path (the path that contains the 225 | # .app file.), if on a mac. 226 | alt_path = os.path.abspath("renpy_base") 227 | if ".app" in alt_path: 228 | alt_path = alt_path[:alt_path.find(".app") + 4] 229 | 230 | if os.path.exists(alt_path + "/environment.txt"): 231 | evars = { } 232 | with open(alt_path + "/environment.txt", "rb") as f: 233 | code = compile(f.read(), alt_path + "/environment.txt", 'exec') 234 | exec(code, evars) 235 | for k, v in evars.items(): 236 | if k not in os.environ: 237 | os.environ[k] = str(v) 238 | 239 | # Get a working name for the game. 240 | name = os.path.basename(sys.argv[0]) 241 | 242 | if name.find(".") != -1: 243 | name = name[:name.find(".")] 244 | 245 | # Parse the arguments. 246 | import renpy.arguments 247 | args = renpy.arguments.bootstrap() 248 | 249 | if args.trace: 250 | enable_trace(args.trace) 251 | 252 | if args.basedir: 253 | basedir = os.path.abspath(args.basedir) 254 | if not isinstance(basedir, str): 255 | basedir = basedir.decode(FSENCODING) 256 | else: 257 | basedir = renpy_base 258 | 259 | if not os.path.exists(basedir): 260 | sys.stderr.write("Base directory %r does not exist. Giving up.\n" % (basedir,)) 261 | sys.exit(1) 262 | 263 | # Make game/ on Android. 264 | if renpy.android: 265 | if not os.path.exists(basedir + "/game"): 266 | os.mkdir(basedir + "/game", 0o777) 267 | 268 | sys.path.insert(0, basedir) 269 | 270 | if renpy.macintosh: 271 | # If we're on a mac, install our own os.start. 272 | os.startfile = mac_start # type: ignore 273 | 274 | # Are we starting from inside a mac app resources directory? 275 | if basedir.endswith("Contents/Resources/autorun"): 276 | renpy.macapp = True 277 | 278 | # Check that we have installed pygame properly. This also deals with 279 | # weird cases on Windows and Linux where we can't import modules. (On 280 | # windows ";" is a directory separator in PATH, so if it's in a parent 281 | # directory, we won't get the libraries in the PATH, and hence pygame 282 | # won't import.) 283 | try: 284 | import pygame_sdl2 285 | if not ("pygame" in sys.modules): 286 | pygame_sdl2.import_as_pygame() 287 | except Exception: 288 | print("""\ 289 | Could not import pygame_sdl2. Please ensure that this program has been built 290 | and unpacked properly. Also, make sure that the directories containing 291 | this program do not contain : or ; in their names. 292 | 293 | You may be using a system install of python. Please run {0}.sh, 294 | {0}.exe, or {0}.app instead. 295 | """.format(name), file=sys.stderr) 296 | 297 | raise 298 | 299 | gamedir = renpy.__main__.path_to_gamedir(basedir, name) 300 | 301 | # If we're not given a command, show the presplash. 302 | if args.command == "run" and not renpy.mobile: 303 | import renpy.display.presplash # @Reimport 304 | 305 | from json import load as load_json 306 | 307 | try: 308 | json_path = os.path.join(basedir, "selectedMod.json") 309 | with open(json_path, "r") as sm: 310 | mod = load_json(sm) 311 | mod_game_path = os.path.join(gamedir, "mods", mod["modName"], "game") 312 | renpy.display.presplash.start(basedir, mod_game_path) 313 | except IOError: 314 | renpy.display.presplash.start(basedir, gamedir) 315 | 316 | # Ditto for the Ren'Py module. 317 | try: 318 | import _renpy 319 | except Exception: 320 | print("""\ 321 | Could not import _renpy. Please ensure that this program has been built 322 | and unpacked properly. 323 | 324 | You may be using a system install of python. Please run {0}.sh, 325 | {0}.exe, or {0}.app instead. 326 | """.format(name), file=sys.stderr) 327 | raise 328 | 329 | # Load the rest of Ren'Py. 330 | import renpy 331 | renpy.import_all() 332 | 333 | renpy.loader.init_importer() 334 | 335 | exit_status = None 336 | original_basedir = basedir 337 | original_sys_path = list(sys.path) 338 | 339 | try: 340 | while exit_status is None: 341 | exit_status = 1 342 | 343 | try: 344 | 345 | # Potentially use an alternate base directory. 346 | try: 347 | basedir = get_alternate_base(original_basedir) 348 | except Exception: 349 | import traceback 350 | traceback.print_exc() 351 | 352 | gamedir = renpy.__main__.path_to_gamedir(basedir, name) 353 | 354 | sys.path = list(original_sys_path) 355 | if basedir not in sys.path: 356 | sys.path.insert(0, basedir) 357 | 358 | renpy.game.args = args 359 | renpy.config.renpy_base = renpy_base 360 | renpy.config.basedir = basedir 361 | renpy.config.gamedir = gamedir 362 | renpy.config.args = [ ] # type: ignore 363 | 364 | renpy.config.logdir = renpy.__main__.path_to_logdir(basedir) 365 | 366 | if not os.path.exists(renpy.config.logdir): 367 | os.makedirs(renpy.config.logdir, 0o777) 368 | 369 | renpy.main.main() 370 | 371 | exit_status = 0 372 | 373 | except KeyboardInterrupt: 374 | raise 375 | 376 | except renpy.game.UtterRestartException: 377 | 378 | # On an UtterRestart, reload Ren'Py. 379 | renpy.reload_all() 380 | 381 | exit_status = None 382 | 383 | except renpy.game.QuitException as e: 384 | exit_status = e.status 385 | 386 | if e.relaunch: 387 | if hasattr(sys, "renpy_executable"): 388 | subprocess.Popen([sys.renpy_executable] + sys.argv[1:]) # type: ignore 389 | else: 390 | if PY2: 391 | subprocess.Popen([sys.executable, "-EO"] + sys.argv) 392 | else: 393 | subprocess.Popen([sys.executable] + sys.argv) 394 | 395 | except renpy.game.ParseErrorException: 396 | pass 397 | 398 | except Exception as e: 399 | renpy.error.report_exception(e) 400 | 401 | sys.exit(exit_status) 402 | 403 | finally: 404 | 405 | if "RENPY_SHUTDOWN_TRACE" in os.environ: 406 | enable_trace(int(os.environ["RENPY_SHUTDOWN_TRACE"])) 407 | 408 | renpy.display.tts.tts(None) # type: ignore 409 | 410 | renpy.display.im.cache.quit() # type: ignore 411 | 412 | if renpy.display.draw: # type: ignore 413 | renpy.display.draw.quit() # type: ignore 414 | 415 | renpy.audio.audio.quit() 416 | 417 | # Prevent subprocess from throwing errors while trying to run it's 418 | # __del__ method during shutdown. 419 | if not renpy.emscripten: 420 | subprocess.Popen.__del__ = popen_del # type: ignore 421 | 422 | if renpy.android: 423 | from jnius import autoclass # type: ignore 424 | 425 | import android 426 | android.activity.finishAndRemoveTask() 427 | 428 | # Avoid running Python shutdown, which can cause more harm than good. (#5280) 429 | System = autoclass("java.lang.System") 430 | System.exit(0) 431 | -------------------------------------------------------------------------------- /renpy/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2024 Tom Rothamel 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation files 5 | # (the "Software"), to deal in the Software without restriction, 6 | # including without limitation the rights to use, copy, modify, merge, 7 | # publish, distribute, sublicense, and/or sell copies of the Software, 8 | # and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from __future__ import division, absolute_import, with_statement, print_function, unicode_literals 23 | from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode # * 24 | 25 | from typing import Tuple, List, Dict, Set, Optional, Iterable, Any 26 | 27 | import os 28 | import sys 29 | import time 30 | import zipfile 31 | import gc 32 | import linecache 33 | import json 34 | from renpy.mod_docker import ModDockerMain 35 | 36 | import renpy 37 | import renpy.game as game 38 | 39 | last_clock = time.time() 40 | docker_main = ModDockerMain() 41 | 42 | def log_clock(s): 43 | global last_clock 44 | now = time.time() 45 | s = "{} took {:.2f}s".format(s, now - last_clock) 46 | 47 | renpy.display.log.write(s) 48 | if renpy.android and not renpy.config.log_to_stdout: 49 | print(s) 50 | 51 | # Pump the presplash window to prevent marking 52 | # our process as unresponsive by OS 53 | renpy.display.presplash.pump_window() 54 | 55 | last_clock = now 56 | 57 | 58 | def reset_clock(): 59 | global last_clock 60 | last_clock = time.time() 61 | 62 | 63 | def run(restart): 64 | """ 65 | This is called during a single run of the script. Restarting the script 66 | will cause this to change. 67 | """ 68 | 69 | reset_clock() 70 | 71 | # Reset the store to a clean version of itself. 72 | renpy.python.clean_stores() 73 | log_clock("Cleaning stores") 74 | 75 | # Init translation. 76 | renpy.translation.init_translation() 77 | log_clock("Init translation") 78 | 79 | # Rebuild the various style caches. 80 | renpy.style.build_styles() # @UndefinedVariable 81 | log_clock("Build styles") 82 | 83 | renpy.sl2.slast.load_cache() 84 | log_clock("Load screen analysis") 85 | 86 | # Analyze the screens. 87 | renpy.display.screen.analyze_screens() 88 | log_clock("Analyze screens") 89 | 90 | if not restart: 91 | renpy.sl2.slast.save_cache() 92 | log_clock("Save screen analysis") 93 | 94 | # Prepare the screens. 95 | renpy.display.screen.prepare_screens() 96 | 97 | log_clock("Prepare screens") 98 | 99 | if not restart: 100 | renpy.pyanalysis.save_cache() 101 | log_clock("Save pyanalysis.") 102 | 103 | renpy.game.script.save_bytecode() 104 | log_clock("Save bytecode.") 105 | 106 | # Handle arguments and commands. 107 | if not renpy.arguments.post_init(): 108 | # We use 'exception' instead of exports.quit 109 | # to not call quit label since it's not nessesary. 110 | raise renpy.game.QuitException() 111 | 112 | if renpy.config.clear_lines: 113 | renpy.scriptedit.lines.clear() 114 | 115 | # Sleep to finish the presplash. 116 | renpy.display.presplash.sleep() 117 | 118 | # Re-Initialize the log. 119 | game.log = renpy.python.RollbackLog() 120 | 121 | # Switch contexts, begin logging. 122 | game.contexts = [ renpy.execution.Context(True) ] 123 | 124 | # Jump to an appropriate start label. 125 | if game.script.has_label("_start"): 126 | start_label = '_start' 127 | else: 128 | start_label = 'start' 129 | 130 | game.context().goto_label(start_label) 131 | 132 | try: 133 | renpy.exports.log("--- " + time.ctime()) 134 | renpy.exports.log("") 135 | except Exception: 136 | pass 137 | 138 | # Note if this is a restart. 139 | renpy.store._restart = restart 140 | 141 | # We run until we get an exception. 142 | renpy.display.interface.enter_context() 143 | 144 | log_clock("Running {}".format(start_label)) 145 | 146 | renpy.execution.run_context(True) 147 | 148 | 149 | def load_rpe(fn): 150 | 151 | with zipfile.ZipFile(fn) as zfn: 152 | autorun = zfn.read("autorun.py") 153 | 154 | if fn in sys.path: 155 | sys.path.remove(fn) 156 | sys.path.insert(0, fn) 157 | exec(autorun, {'__file__': os.path.join(fn, "autorun.py")}) 158 | 159 | def choose_variants(): 160 | 161 | if "RENPY_VARIANT" in os.environ: 162 | renpy.config.variants = list(os.environ["RENPY_VARIANT"].split()) + [ None ] # type: ignore 163 | renpy.display.emulator.early_init_emulator() 164 | return 165 | 166 | renpy.config.variants = [ None ] 167 | 168 | if renpy.android: # @UndefinedVariable 169 | 170 | renpy.config.variants.insert(0, 'mobile') # type: ignore 171 | renpy.config.variants.insert(0, 'android') # type: ignore 172 | 173 | import android # type: ignore 174 | import math 175 | import pygame_sdl2 as pygame 176 | 177 | from jnius import autoclass # type: ignore 178 | 179 | # Manufacturer/Model-specific variants. 180 | try: 181 | Build = autoclass("android.os.Build") 182 | 183 | manufacturer = Build.MANUFACTURER 184 | model = Build.MODEL 185 | 186 | print("Manufacturer", manufacturer, "model", model) 187 | 188 | if manufacturer == "Amazon" and model.startswith("AFT"): 189 | print("Running on a Fire TV.") 190 | renpy.config.variants.insert(0, "firetv") # type: ignore 191 | except Exception: 192 | pass 193 | 194 | # Are we running on OUYA or Google TV or something similar? 195 | package_manager = android.activity.getPackageManager() 196 | 197 | if package_manager.hasSystemFeature("android.hardware.type.television"): 198 | print("Running on a television.") 199 | renpy.config.variants.insert(0, "tv") # type: ignore 200 | renpy.config.variants.insert(0, "small") # type: ignore 201 | return 202 | 203 | # Running on a chromebook. 204 | try: 205 | PythonSDLActivity = autoclass("org.renpy.android.PythonSDLActivity") 206 | if PythonSDLActivity.isChromebook(): 207 | print("Running on ChromeOS.") 208 | renpy.config.variants.insert(0, 'chromeos') # type: ignore 209 | except Exception: 210 | pass 211 | 212 | # Otherwise, a phone or tablet. 213 | renpy.config.variants.insert(0, 'touch') # type: ignore 214 | 215 | pygame.display.init() 216 | 217 | info = renpy.display.get_info() 218 | diag = math.hypot(info.current_w, info.current_h) / android.get_dpi() # type: ignore 219 | print("Screen diagonal is", diag, "inches.") 220 | 221 | if diag >= 6: 222 | renpy.config.variants.insert(0, 'tablet') # type: ignore 223 | renpy.config.variants.insert(0, 'medium') # type: ignore 224 | else: 225 | renpy.config.variants.insert(0, 'phone') # type: ignore 226 | renpy.config.variants.insert(0, 'small') # type: ignore 227 | 228 | elif renpy.ios: 229 | renpy.config.variants.insert(0, 'mobile') # type: ignore 230 | renpy.config.variants.insert(0, 'ios') # type: ignore 231 | renpy.config.variants.insert(0, 'touch') # type: ignore 232 | 233 | from pyobjus import autoclass # type: ignore 234 | UIDevice = autoclass("UIDevice") 235 | 236 | idiom = UIDevice.currentDevice().userInterfaceIdiom 237 | 238 | print("iOS device idiom", idiom) 239 | 240 | # idiom 0 is iPhone, 1 is iPad. We assume any bigger idiom will 241 | # be tablet-like. 242 | if idiom >= 1: 243 | renpy.config.variants.insert(0, 'tablet') # type: ignore 244 | renpy.config.variants.insert(0, 'medium') # type: ignore 245 | else: 246 | renpy.config.variants.insert(0, 'phone') # type: ignore 247 | renpy.config.variants.insert(0, 'small') # type: ignore 248 | 249 | elif renpy.emscripten: 250 | import emscripten # type: ignore 251 | import re 252 | 253 | # web 254 | renpy.config.variants.insert(0, 'web') # type: ignore 255 | 256 | # mobile 257 | mobile = emscripten.run_script_int( 258 | r'''/Mobile|Android|iPad|iPhone/.test(navigator.userAgent) 259 | || (navigator.userAgent.indexOf("Mac") != -1 && navigator.maxTouchPoints > 1)''') 260 | if mobile: 261 | renpy.config.variants.insert(0, 'mobile') # type: ignore 262 | # Reserve android/ios for when the OS API is exposed 263 | # if re.search('Android', userAgent): 264 | # renpy.config.variants.insert(0, 'android') 265 | # if re.search('iPad|iPhone', userAgent): 266 | # renpy.config.variants.insert(0, 'ios') 267 | 268 | # touch 269 | touch = emscripten.run_script_int(r''' 270 | ('ontouchstart' in window) || 271 | (navigator.maxTouchPoints > 0) || 272 | (navigator.msMaxTouchPoints > 0)''') 273 | if touch == 1: 274 | # mitigate hybrids (e.g. ms surface) by restricting touch to mobile 275 | if mobile: 276 | renpy.config.variants.insert(0, 'touch') # type: ignore 277 | 278 | # large/medium/small 279 | # tablet/phone 280 | # screen.width/height is auto-adjusted by browser, 281 | # so it can be used as a physical sizereference 282 | # (see also window.devicePixelRatio) 283 | # e.g. Galaxy S5: 284 | # - physical / OpenGL: 1080x1920 285 | # - web screen: 360x640 w/ devicePixelRatio=3 286 | ref_width = emscripten.run_script_int(r'''screen.width''') 287 | ref_height = emscripten.run_script_int(r'''screen.height''') 288 | # medium reference point: ipad 1024x768, ipad pro 1336x1024 (browser "pixels") 289 | if mobile: 290 | if (ref_width < 768 or ref_height < 768): 291 | renpy.config.variants.insert(0, 'small') # type: ignore 292 | renpy.config.variants.insert(0, 'phone') # type: ignore 293 | else: 294 | renpy.config.variants.insert(0, 'medium') # type: ignore 295 | renpy.config.variants.insert(0, 'tablet') # type: ignore 296 | else: 297 | renpy.config.variants.insert(0, 'large') # type: ignore 298 | 299 | else: 300 | renpy.config.variants.insert(0, 'pc') # type: ignore 301 | 302 | renpy.config.variants.insert(0, 'large') # type: ignore 303 | 304 | 305 | def load_build_info(): 306 | """ 307 | Loads cache/build_info.json, and uses it to initialize the 308 | renpy.game.build_info dictionary. 309 | """ 310 | 311 | try: 312 | f = renpy.exports.open_file("cache/build_info.json", "utf-8") 313 | renpy.game.build_info = json.load(f) 314 | except Exception: 315 | renpy.game.build_info = { "info" : { } } 316 | 317 | 318 | def main(): 319 | 320 | gc.set_threshold(*renpy.config.gc_thresholds) 321 | 322 | renpy.game.exception_info = 'Before loading the script.' 323 | 324 | # Clear the line cache, since the script may have changed. 325 | linecache.clearcache() 326 | 327 | # Get ready to accept new arguments. 328 | renpy.arguments.pre_init() 329 | 330 | # Init the screen language parser. 331 | renpy.sl2.slparser.init() 332 | 333 | # Init the config after load. 334 | renpy.config.init() 335 | 336 | # Reset live2d if it exists. 337 | try: 338 | renpy.gl2.live2d.reset() 339 | except Exception: 340 | pass 341 | 342 | # Set up variants. 343 | choose_variants() 344 | renpy.display.touch = "touch" in renpy.config.variants 345 | 346 | if (renpy.android or renpy.ios) and not renpy.config.log_to_stdout: 347 | print("Version:", renpy.version) 348 | 349 | # Note the game directory. 350 | old_gamedir = renpy.config.gamedir 351 | 352 | docker_main.initialize_docker() 353 | 354 | # Note the game directory. 355 | game.basepath = renpy.config.gamedir 356 | renpy.config.commondir = renpy.__main__.path_to_common(renpy.config.renpy_base) # E1101 @UndefinedVariable 357 | renpy.config.searchpath = renpy.__main__.predefined_searchpath(renpy.config.commondir, old_gamedir) # E1101 @UndefinedVariable 358 | 359 | # Load Ren'Py extensions. 360 | for dir in renpy.config.searchpath: # @ReservedAssignment 361 | 362 | if not os.path.isdir(dir): 363 | continue 364 | 365 | for fn in sorted(os.listdir(dir)): 366 | if fn.lower().endswith(".rpe"): 367 | load_rpe(dir + "/" + fn) 368 | 369 | # Generate a list of extensions for each archive handler. 370 | archive_extensions = [ ] 371 | for handler in renpy.loader.archive_handlers: 372 | for ext in handler.get_supported_extensions(): 373 | if not (ext in archive_extensions): 374 | archive_extensions.append(ext) 375 | 376 | # Find archives. 377 | renpy.config.archives = docker_main.find_mod_archives(archive_extensions) 378 | 379 | # Initialize archives. 380 | renpy.loader.index_archives() 381 | 382 | # Start auto-loading. 383 | renpy.loader.auto_init() 384 | 385 | load_build_info() 386 | 387 | log_clock("Early init") 388 | 389 | # Initialize the log. 390 | game.log = renpy.python.RollbackLog() 391 | 392 | # Initialize the store. 393 | renpy.store.store = sys.modules['store'] # type: ignore 394 | 395 | # Set up styles. 396 | game.style = renpy.style.StyleManager() # @UndefinedVariable 397 | renpy.store.style = game.style 398 | 399 | # Run init code in its own context. (Don't log.) 400 | game.contexts = [ renpy.execution.Context(False) ] 401 | game.contexts[0].init_phase = True 402 | 403 | renpy.execution.not_infinite_loop(60) 404 | 405 | # Load the script. 406 | renpy.game.exception_info = 'While loading the script.' 407 | renpy.game.script = renpy.script.Script() 408 | 409 | if renpy.session.get("compile", False): 410 | renpy.game.args.compile = True # type: ignore 411 | 412 | # Set up error handling. 413 | renpy.exports.load_module("_errorhandling") 414 | 415 | if renpy.exports.loadable("tl/None/common.rpym") or renpy.exports.loadable("tl/None/common.rpymc"): 416 | renpy.exports.load_module("tl/None/common") 417 | 418 | renpy.config.init_system_styles() 419 | renpy.style.build_styles() # @UndefinedVariable 420 | 421 | log_clock("Loading error handling") 422 | 423 | # If recompiling everything, remove orphan .rpyc files. 424 | # Otherwise, will fail in case orphan .rpyc have same 425 | # labels as in other scripts (usually happens on script rename). 426 | if (renpy.game.args.command == 'compile') and not (renpy.game.args.keep_orphan_rpyc): # type: ignore 427 | 428 | for (fn, dn) in renpy.game.script.script_files: 429 | 430 | if dn is None: 431 | continue 432 | 433 | if not os.path.isfile(os.path.join(dn, fn + ".rpy")) and not os.path.isfile(os.path.join(dn, fn + "_ren.py")): 434 | 435 | try: 436 | name = os.path.join(dn, fn + ".rpyc") 437 | os.rename(name, name + ".bak") 438 | except OSError: 439 | # This perhaps shouldn't happen since either .rpy or .rpyc should exist 440 | pass 441 | 442 | # Update script files list, so that it doesn't contain removed .rpyc's 443 | renpy.loader.cleardirfiles() 444 | renpy.game.script.scan_script_files() 445 | 446 | renpy.game.script.script_files = docker_main.assign_docker_files_to_script() 447 | 448 | # Hand back a 'normal' formatted archive list for some mods 449 | # that check literally for RPAs 450 | for index, rpa in enumerate(renpy.config.archives): 451 | renpy.config.archives[index] = rpa.split("/")[-1] 452 | 453 | # Load all .rpy files. 454 | renpy.game.script.load_script() # sets renpy.game.script. 455 | 456 | docker_main.verify_setting_integrity() 457 | 458 | log_clock("Loading script") 459 | 460 | if renpy.game.args.command == 'load-test': # type: ignore 461 | start = time.time() 462 | 463 | for i in range(5): 464 | print(i) 465 | renpy.game.script = renpy.script.Script() 466 | renpy.game.script.load_script() 467 | 468 | print(time.time() - start) 469 | sys.exit(0) 470 | 471 | renpy.game.exception_info = 'After loading the script.' 472 | 473 | # Find the save directory. 474 | if renpy.config.savedir is None: 475 | renpy.config.savedir = renpy.__main__.path_to_saves(renpy.config.gamedir) # E1101 @UndefinedVariable 476 | 477 | if renpy.game.args.savedir: # type: ignore 478 | renpy.config.savedir = renpy.game.args.savedir # type: ignore 479 | 480 | # Init the save token system. 481 | renpy.savetoken.init() 482 | 483 | # Init preferences. 484 | game.persistent = renpy.persistent.init() 485 | game.preferences = game.persistent._preferences 486 | 487 | for i in renpy.game.persistent._seen_translates: # type: ignore 488 | if i in renpy.game.script.translator.default_translates: 489 | renpy.game.seen_translates_count += 1 490 | 491 | if game.persistent._virtual_size: 492 | renpy.config.screen_width, renpy.config.screen_height = game.persistent._virtual_size 493 | 494 | # Init save locations and loadsave. 495 | renpy.savelocation.init() 496 | 497 | try: 498 | # Init save slots and save tokens. 499 | renpy.loadsave.init() 500 | renpy.savetoken.upgrade_all_savefiles() 501 | log_clock("Loading save slot metadata") 502 | 503 | # Load persistent data from all save locations. 504 | renpy.persistent.update() 505 | game.preferences = game.persistent._preferences 506 | log_clock("Loading persistent") 507 | 508 | # Clear the list of seen statements in this game. 509 | game.seen_session = { } 510 | 511 | # Initialize persistent variables. 512 | renpy.store.persistent = game.persistent # type: ignore 513 | renpy.store._preferences = game.preferences # type: ignore 514 | renpy.store._test = renpy.test.testast._test # type: ignore 515 | 516 | docker_main.finalize() 517 | 518 | if renpy.parser.report_parse_errors(): 519 | raise renpy.game.ParseErrorException() 520 | 521 | renpy.game.exception_info = 'While executing init code:' 522 | 523 | for id_, (_prio, node) in enumerate(game.script.initcode): 524 | 525 | renpy.game.initcode_ast_id = id_ 526 | 527 | if isinstance(node, renpy.ast.Node): 528 | node_start = time.time() 529 | 530 | renpy.game.context().run(node) 531 | 532 | node_duration = time.time() - node_start 533 | 534 | if node_duration > renpy.config.profile_init: 535 | renpy.display.log.write(" - Init at %s:%d took %.5f s.", node.filename, node.linenumber, node_duration) 536 | 537 | else: 538 | # An init function. 539 | node() 540 | 541 | renpy.game.exception_info = 'After initialization, but before game start.' 542 | 543 | # Check if we should simulate android. 544 | renpy.android = renpy.android or renpy.config.simulate_android # @UndefinedVariable 545 | 546 | # Re-set up the logging. 547 | renpy.log.post_init() 548 | 549 | # Run the post init code, if any. 550 | for i in renpy.game.post_init: 551 | i() 552 | 553 | renpy.game.script.report_duplicate_labels() 554 | 555 | # Sort the images. 556 | renpy.display.image.image_names.sort() 557 | 558 | game.persistent._virtual_size = renpy.config.screen_width, renpy.config.screen_height # type: ignore 559 | 560 | log_clock("Running init code") 561 | 562 | renpy.pyanalysis.load_cache() 563 | log_clock("Loading analysis data") 564 | 565 | # Analyze the script and compile ATL. 566 | renpy.game.script.analyze() 567 | renpy.atl.compile_all() 568 | log_clock("Analyze and compile ATL") 569 | 570 | renpy.savelocation.init() 571 | renpy.loadsave.init() 572 | log_clock("Reloading save slot metadata") 573 | 574 | # Index the archive files. We should not have loaded an image 575 | # before this point. (As pygame will not have been initialized.) 576 | # We need to do this again because the list of known archives 577 | # may have changed. 578 | renpy.loader.index_archives() 579 | log_clock("Index archives") 580 | 581 | # Check some environment variables. 582 | renpy.game.less_memory = "RENPY_LESS_MEMORY" in os.environ 583 | renpy.game.less_mouse = "RENPY_LESS_MOUSE" in os.environ 584 | renpy.game.less_updates = "RENPY_LESS_UPDATES" in os.environ 585 | 586 | renpy.dump.dump(False) 587 | renpy.game.script.make_backups() 588 | log_clock("Dump and make backups") 589 | 590 | # Initialize image cache. 591 | renpy.display.im.cache.init() 592 | log_clock("Cleaning cache") 593 | 594 | # Make a clean copy of the store. 595 | renpy.python.make_clean_stores() 596 | log_clock("Making clean stores") 597 | 598 | # Init the keymap. 599 | renpy.display.behavior.init_keymap() 600 | 601 | gc.collect(2) 602 | 603 | if gc.garbage: 604 | del gc.garbage[:] 605 | 606 | if renpy.config.manage_gc: 607 | gc.set_threshold(*renpy.config.gc_thresholds) 608 | 609 | gc_debug = int(os.environ.get("RENPY_GC_DEBUG", 0)) 610 | 611 | if renpy.config.gc_print_unreachable: 612 | gc_debug |= gc.DEBUG_SAVEALL 613 | 614 | gc.set_debug(gc_debug) 615 | 616 | else: 617 | gc.set_threshold(700, 10, 10) 618 | 619 | log_clock("Initial gc") 620 | 621 | # Start debugging file opens. 622 | renpy.debug.init_main_thread_open() 623 | 624 | # (Perhaps) Initialize graphics. 625 | if not game.interface: 626 | renpy.display.core.Interface() 627 | log_clock("Creating interface object") 628 | 629 | # Start things running. 630 | restart = None 631 | 632 | while True: 633 | 634 | if restart: 635 | renpy.display.screen.before_restart() 636 | 637 | try: 638 | try: 639 | run(restart) 640 | finally: 641 | restart = (renpy.config.end_game_transition, "_invoke_main_menu", "_main_menu") 642 | 643 | except renpy.game.QuitException: 644 | 645 | renpy.audio.audio.fadeout_all() 646 | raise 647 | 648 | except game.FullRestartException as e: 649 | restart = e.reason 650 | 651 | finally: 652 | 653 | renpy.persistent.update(True) 654 | renpy.persistent.save_on_quit_MP() 655 | 656 | # Reset live2d if it exists. 657 | try: 658 | renpy.gl2.live2d.reset_states() 659 | except Exception: 660 | pass 661 | 662 | # Flush any pending interface work. 663 | renpy.display.interface.finish_pending() 664 | 665 | # Give Ren'Py a couple of seconds to finish saving. 666 | renpy.loadsave.autosave_not_running.wait(3.0) 667 | 668 | # Run the at exit callbacks. 669 | for cb in renpy.config.at_exit_callbacks: 670 | cb() 671 | 672 | finally: 673 | 674 | gc.set_debug(0) 675 | 676 | for i in renpy.config.quit_callbacks: 677 | i() 678 | 679 | renpy.loader.auto_quit() 680 | renpy.savelocation.quit() 681 | renpy.translation.write_updated_strings() 682 | 683 | # This is stuff we do on a normal, non-error return. 684 | if not renpy.display.error.error_handled: 685 | renpy.display.render.check_at_shutdown() 686 | -------------------------------------------------------------------------------- /renpy/mod_docker.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Azariel Del Carmen (bronya_rand) 2 | 3 | import renpy 4 | import os 5 | import json 6 | 7 | 8 | class ModDockerMain(object): 9 | 10 | def __init__(self): 11 | self.renpy_sdk_folders = ["launcher", "gui", "doc", "module", "tutorial", "the_question"] 12 | self.mod_name = None 13 | self.ddlc_mode = False 14 | self.rpa_format = False 15 | 16 | def running_renpy_sdk(self): 17 | return any(folder in self.renpy_sdk_folders for folder in os.listdir(renpy.config.basedir)) 18 | 19 | def set_renpy_to_mod(self, mod_data): 20 | self.mod_name = mod_data["modName"] 21 | self.rpa_format = mod_data["isRPA"] 22 | mod_directory = os.path.join(renpy.config.gamedir, "mods", self.mod_name) 23 | mod_game_directory = os.path.join(mod_directory, "game") 24 | mod_directory_normalized = os.path.normpath(mod_directory).replace("\\", "/") 25 | 26 | if not os.path.exists(mod_game_directory): 27 | raise Exception("'game' folder could not be found in {}.".format(mod_directory_normalized)) 28 | 29 | renpy.config.gamedir = os.path.normpath(mod_game_directory).replace("\\", "/") 30 | 31 | def initialize_docker(self): 32 | mod_json_path = os.path.join(renpy.config.basedir, "selectedmod.json") 33 | if not os.path.exists(mod_json_path): 34 | self.ddlc_mode = True 35 | return 36 | 37 | try: 38 | with open(mod_json_path, "r") as mj: 39 | mod_data = json.load(mj) 40 | self.set_renpy_to_mod(mod_data) 41 | except (IOError, ValueError): 42 | self.ddlc_mode = True 43 | return 44 | 45 | def find_mod_archives(self, archive_extensions): 46 | if self.running_renpy_sdk(): 47 | return [] 48 | archives = [] 49 | 50 | if os.path.exists(os.path.join(renpy.config.basedir, "game", "ddml.rpa")): 51 | archives.append("ddml") 52 | 53 | # Base DDLC 54 | archives.append("audio") 55 | archives.append("fonts") 56 | archives.append("images") 57 | 58 | # Base DDLC + RPYC Mode 59 | if self.ddlc_mode or not self.rpa_format: 60 | archives.append("scripts") 61 | else: 62 | # RPA Mode 63 | for i in sorted(os.listdir(renpy.config.gamedir)): 64 | base, ext = os.path.splitext(i) 65 | 66 | if not (ext in archive_extensions): 67 | continue 68 | 69 | if base in archives: 70 | archives.remove(base) 71 | 72 | archives.append("mods/{}/game/{}".format(self.mod_name, base)) 73 | 74 | if os.path.exists(os.path.join(renpy.config.basedir, "game", "mod_patches.rpa")): 75 | archives.append("mod_patches") 76 | 77 | archives.reverse() 78 | 79 | return archives 80 | 81 | def assign_docker_files_to_script(self): 82 | if self.running_renpy_sdk(): 83 | return renpy.game.script.script_files 84 | 85 | if self.ddlc_mode: 86 | return [x for x in renpy.game.script.script_files if "mods/" not in x[0]] 87 | 88 | mods_set = set() 89 | 90 | # Make sure we add the needed DDMD files 91 | ddmd_files = ["mod_installer", "mod_services", "mod_screen", "mod_settings", "ml_patches", "mod_content", "mod_dir_browser", "mod_list", "mod_prompt", "mod_styles", "mod_transforms", "saves"] 92 | 93 | for renpy_file, archive_path in renpy.game.script.script_files: 94 | if renpy_file in ddmd_files or (archive_path is not None and "renpy/" in archive_path.replace("\\", "/")): 95 | temp_tuple = (renpy_file, archive_path) 96 | mods_set.add(temp_tuple) 97 | 98 | for renpy_file, archive_path in renpy.game.script.script_files: 99 | temp_tuple = (renpy_file, archive_path) 100 | if "mods/{}/".format(self.mod_name) in renpy_file: 101 | mods_set.add(temp_tuple) 102 | elif archive_path is None and renpy_file.split("/")[-1] not in [existing_renpy_file.split("/")[-1] for existing_renpy_file, x in mods_set]: 103 | mods_set.add(temp_tuple) 104 | 105 | return list(mods_set) 106 | 107 | def verify_setting_integrity(self): 108 | if self.running_renpy_sdk(): 109 | return 110 | 111 | settings_path = os.path.join(renpy.config.basedir, "ddmd_settings.json") 112 | ddmc_json_path = os.path.join(renpy.config.basedir, "game", "ddmc.json") 113 | 114 | if not os.path.isfile(settings_path): 115 | with open(settings_path, "wb") as ddmd_settings: 116 | ddmd_settings.write( 117 | renpy.exports.file("sdc_system/backups/settings.backup").read() 118 | ) 119 | 120 | if not os.path.isfile(ddmc_json_path): 121 | with open(ddmc_json_path, "wb") as ddmc_json: 122 | ddmc_json.write( 123 | renpy.exports.file("sdc_system/backups/ddmc.backup").read() 124 | ) 125 | 126 | with open(settings_path, "r") as ddmd_settings: 127 | ddmd_configuration = json.load(ddmd_settings) 128 | 129 | renpy.config.gl2 = ddmd_configuration.get("config_gl2", False) 130 | 131 | def finalize(self): 132 | if self.running_renpy_sdk(): 133 | return 134 | 135 | renpy.store.persistent.ddml_basedir = renpy.config.basedir.replace( 136 | "\\", "/" 137 | ) 138 | if not self.ddlc_mode: 139 | renpy.config.basedir = os.path.join(renpy.config.basedir, "game/mods", self.mod_name) 140 | --------------------------------------------------------------------------------