├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------