├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── OldREADME.md ├── README.md ├── ddmc.json └── launcher ├── None.edit.py ├── System Editor.edit.py ├── game ├── EasyDialogsResources.py ├── EasyDialogsWin.py ├── ability.rpy ├── about.rpy ├── add_file.rpy ├── archiver.rpy ├── change_icon.py ├── choose_directory.rpy ├── ddmm_compatibility.py ├── distribute.rpy ├── distribute_gui.rpy ├── dmgcheck.rpy ├── download.rpy ├── editor.rpy ├── entitlements.plist ├── extractor.py ├── fonts │ ├── MTLc3m.ttf │ ├── NanumGothic.ttf │ ├── Naver Nanum Font License.txt │ ├── Roboto-Light.ttf │ ├── Roboto-Regular.ttf │ ├── SourceHanSans-Light-Lite.ttf │ └── SourceHanSansLite.ttf ├── front_page.rpy ├── gui7.rpy ├── gui7 │ ├── __init__.py │ ├── __init__.pyo │ ├── code.py │ ├── code.pyo │ ├── icon.png │ ├── images.py │ ├── images.pyo │ ├── parameters.py │ └── parameters.pyo ├── images │ ├── background.png │ ├── background_custom.png │ ├── background_dark.png │ ├── checkbox_empty.png │ ├── checkbox_full.png │ ├── custom_style.rpy │ ├── logo.png │ ├── logo32.png │ ├── pattern.png │ ├── pattern_custom.png │ ├── scrollbar_center.png │ ├── vscrollbar_center.png │ ├── window.png │ ├── window_custom.png │ └── window_dark.png ├── interface.rpy ├── modmanagement.py ├── navigation.rpy ├── new_project.rpy ├── options.rpy ├── package_formats.rpy ├── pefile.py ├── preferences.rpy ├── project.rpy ├── style.rpy ├── tkaskdir.py ├── tkaskfile.py ├── transfer.rpy ├── updater.rpy └── util.rpy ├── icon.icns └── project.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you encounter a bug with DDML, fill this information out 4 | title: Submitted Bug By User 5 | labels: bug 6 | assignees: GanstaKingofSA 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, Linux, macOS] 28 | - Mod: [e.g. Monika After Story, MAS] 29 | - DDML Version [e.g. DDML 5.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | errors.txt 2 | log.txt 3 | traceback.txt 4 | *.rpyb 5 | *.rpyc 6 | persistent 7 | *.save 8 | launcher/game/saves 9 | tmp 10 | *.pyo -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To contribute to ML's development, fork this respository via the Fork Button and make any edits you like to it. Afterwards request a PR here with your build and your build will be taken into review and into consideration. Thanks for improving DDML! 2 | -------------------------------------------------------------------------------- /OldREADME.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Doki Doki Mod Launcher (DDML) Repository 2 | 3 | [Download the last DDML version here](https://github.com/GanstaKingofSA/DDML/releases/latest) 4 | 5 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/K3K22K8SU) 6 | 7 | Doki Doki Mod Launcher (DDML) is a mod launcher for Doki Doki Literature Club (DDLC) which is designed to help new mod players install their favorite mods without much hassle. 8 | 9 | ## Features 10 | 1. Auto DDLC installation. 11 | 2. Auto DDLC mod installation. 12 | > This will differ from mod to mod due to the lack of standardization. While DDML is able to install most mods just fine, I cannot guarantee that one specific mod will be able to be installed. To report a broken mod install, make a Issue Request in [Issues](https://github.com/GanstaKingofSA/DDML/issues) for me to take a look at. 13 | 3. Dark mode (based off Samsung's One UI). 14 | 4. Mod folder management. 15 | 5. Ability to move mods to different folders. 16 | 6. Ability to reset the save file of mods. 17 | 7. MacOS and Linux Support! 18 | > While MacOS support is available, I cannot guarantee that the code will translate well to it from `renpy.app`. MacOS testing is done on __x86__; *Apple Silicon* has never been tested. Please report these issues when you get a error. 19 | 8. Mod Search Support! 20 | > Please be advised that this database comes from the now defunct Doki Doki Mod Club. Newer mods released after the site shutdown (such as Exit Music Redux) will not show up in the mod list search. 21 | 9. Compatibility with DDMM (Doki Doki Mod Manager)! Transfer your mods easily to DDML using the built-in transfer tool. 22 | > This feature for now is exclusive to Windows. Wine or AppImages of DDMM will not work as they need more research. 23 | 10. Custom theme support! 24 | 25 | **DISCLAIMER** 26 | 27 | Doki Doki Mod Launcher, is a Ren'Py 7.4.5 SDK mod which is not affiliated or supported by RenpyTom. It is not supported by the official Ren'Py Build, or any Ren'Py channels. RenpyTom does not support game modding in general and will not provide support for this Ren'Py mod. 28 | 29 | Doki Doki Mod Launcher is a unofficial Doki Doki Literature Club mod launcher which is unaffiliated with Team Salvato. Mods should only be played once you played DDLC once. 30 | 31 | For the purposes of LGPL compliance, all source code that Doki Doki Mod Launcher and the Ren'Py SDK depends 32 | on is located in one of the following repositories: 33 | 34 | * https://github.com/renpy/renpy (Ren'Py) 35 | * https://github.com/renpy/pygame_sdl2 (Pygame_SDL2) 36 | * https://github.com/renpy/renpy-build (Dependencies) 37 | 38 | For releases prior to Ren'Py 7.4 39 | * https://github.com/renpy/renpy-deps (Desktop dependencies) 40 | 41 | This program contains free software licensed under a number of licenses, including the GNU Lesser General Public License. A complete list of software is available at http://www.renpy.org/doc/html/license.html. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Doki Doki Mod Launcher (DDML) Repository 2 | 3 | This project has been sunsetted and will no longer be maintained in favor for Doki Doki Mod Docker. to visit the successor to Doki Doki Mod Launcher, click [here](https://github.com/GanstaKingofSA/DDModDocker). 4 | -------------------------------------------------------------------------------- /launcher/None.edit.py: -------------------------------------------------------------------------------- 1 | import renpy 2 | 3 | # Do nothing when the editor is invoked. 4 | Editor = renpy.editor.Editor 5 | -------------------------------------------------------------------------------- /launcher/System Editor.edit.py: -------------------------------------------------------------------------------- 1 | import renpy 2 | 3 | # Pass the file off to the system editor (as determined by file associations). 4 | Editor = renpy.editor.SystemEditor 5 | -------------------------------------------------------------------------------- /launcher/game/EasyDialogsResources.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | 3 | 4 | def a2b(a): 5 | return binascii.a2b_hex(''.join(a.split())) 6 | 7 | 8 | resources = { 9 | 260 : a2b(''' 10 | 01 00 ff ff 00 00 00 00 00 00 00 00 c8 00 c0 80 11 | 02 00 00 00 00 00 b8 00 29 00 00 00 00 00 00 00 12 | 08 00 90 01 00 01 4d 00 53 00 20 00 53 00 68 00 13 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 14 | 00 00 00 00 00 00 00 00 01 00 01 50 7f 00 14 00 15 | 32 00 0e 00 01 00 00 00 ff ff 80 00 4f 00 4b 00 16 | 00 00 00 00 00 00 00 00 00 00 00 00 04 08 00 50 17 | 07 00 07 00 aa 00 0c 00 ea 03 00 00 ff ff 81 00 18 | 00 00 00 00 19 | '''), 20 | 261 : a2b(''' 21 | 01 00 ff ff 00 00 00 00 00 00 00 00 c8 00 c0 80 22 | 04 00 00 00 00 00 b8 00 3a 00 00 00 00 00 00 00 23 | 08 00 90 01 00 01 4d 00 53 00 20 00 53 00 68 00 24 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 25 | 00 00 00 00 00 00 00 00 80 00 81 50 07 00 14 00 26 | aa 00 0c 00 ec 03 00 00 ff ff 81 00 00 00 00 00 27 | 00 00 00 00 00 00 00 00 01 00 01 50 7f 00 25 00 28 | 32 00 0e 00 01 00 00 00 ff ff 80 00 4f 00 4b 00 29 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 50 30 | 4a 00 25 00 32 00 0e 00 02 00 00 00 ff ff 80 00 31 | 43 00 61 00 6e 00 63 00 65 00 6c 00 00 00 00 00 32 | 00 00 00 00 00 00 00 00 04 08 00 50 07 00 07 00 33 | aa 00 0c 00 eb 03 00 00 ff ff 81 00 00 00 00 00 34 | '''), 35 | 262 : a2b(''' 36 | 01 00 ff ff 00 00 00 00 00 00 00 00 c8 00 c0 80 37 | 04 00 00 00 00 00 b8 00 29 00 00 00 00 00 00 00 38 | 08 00 90 01 00 01 4d 00 53 00 20 00 53 00 68 00 39 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 40 | 00 00 00 00 00 00 00 00 00 00 01 50 7f 00 14 00 41 | 32 00 0e 00 06 00 00 00 ff ff 80 00 59 00 65 00 42 | 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 | 04 08 00 50 07 00 07 00 aa 00 0c 00 ed 03 00 00 44 | ff ff 81 00 00 00 00 00 00 00 00 00 00 00 00 00 45 | 00 00 01 50 07 00 14 00 32 00 0e 00 07 00 00 00 46 | ff ff 80 00 4e 00 6f 00 00 00 00 00 00 00 00 00 47 | 00 00 00 00 00 00 01 50 4b 00 14 00 32 00 0e 00 48 | 02 00 00 00 ff ff 80 00 43 00 61 00 6e 00 63 00 49 | 65 00 6c 00 00 00 00 00 50 | '''), 51 | 263 : a2b(''' 52 | 01 00 ff ff 00 00 00 00 00 00 00 00 80 00 ca 80 53 | 03 00 00 00 00 00 e2 00 29 00 00 00 00 00 00 00 54 | 00 00 00 00 00 00 00 00 00 00 81 50 07 00 14 00 55 | 97 00 0e 00 eb 03 00 00 6d 00 73 00 63 00 74 00 56 | 6c 00 73 00 5f 00 70 00 72 00 6f 00 67 00 72 00 57 | 65 00 73 00 73 00 33 00 32 00 00 00 00 00 00 00 58 | 00 00 00 00 00 00 00 00 04 08 00 50 07 00 07 00 59 | d4 00 0c 00 ea 03 00 00 ff ff 81 00 00 00 00 00 60 | 00 00 00 00 00 00 00 00 00 00 00 50 a9 00 14 00 61 | 32 00 0e 00 02 00 00 00 ff ff 80 00 43 00 61 00 62 | 6e 00 63 00 65 00 6c 00 00 00 00 00 63 | '''), 64 | 264 : a2b(''' 65 | 01 00 ff ff 00 00 00 00 00 00 00 00 c8 00 c0 80 66 | 04 00 00 00 00 00 b8 00 3a 00 00 00 00 00 00 00 67 | 08 00 90 01 00 01 4d 00 53 00 20 00 53 00 68 00 68 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 69 | 00 00 00 00 00 00 00 00 a0 00 81 50 07 00 14 00 70 | aa 00 0c 00 ec 03 00 00 ff ff 81 00 00 00 00 00 71 | 00 00 00 00 00 00 00 00 01 00 01 50 7f 00 25 00 72 | 32 00 0e 00 01 00 00 00 ff ff 80 00 4f 00 4b 00 73 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 50 74 | 4a 00 25 00 32 00 0e 00 02 00 00 00 ff ff 80 00 75 | 43 00 61 00 6e 00 63 00 65 00 6c 00 00 00 00 00 76 | 00 00 00 00 00 00 00 00 04 08 00 50 07 00 07 00 77 | aa 00 0c 00 eb 03 00 00 ff ff 81 00 00 00 00 00 78 | '''), 79 | 265 : a2b(''' 80 | 01 00 ff ff 00 00 00 00 00 00 00 00 c8 00 c8 80 81 | 12 00 00 00 00 00 d9 00 fc 00 00 00 00 00 00 00 82 | 08 00 90 01 00 01 4d 00 53 00 20 00 53 00 68 00 83 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 84 | 00 00 00 00 00 00 00 00 01 00 01 50 6c 00 ea 00 85 | 30 00 0e 00 01 00 00 00 ff ff 80 00 4f 00 4b 00 86 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 50 87 | a2 00 ea 00 30 00 0e 00 02 00 00 00 ff ff 80 00 88 | 43 00 61 00 6e 00 63 00 65 00 6c 00 00 00 00 00 89 | 00 00 00 00 00 00 00 00 07 00 00 50 06 00 06 00 90 | cc 00 4e 00 ff ff ff ff ff ff 80 00 00 00 00 00 91 | 00 00 00 00 00 00 00 00 03 00 21 50 48 00 12 00 92 | 84 00 64 00 03 00 00 00 ff ff 85 00 00 00 00 00 93 | 00 00 00 00 00 00 00 00 00 00 02 50 0c 00 24 00 94 | c0 00 08 00 04 00 00 00 ff ff 82 00 00 00 00 00 95 | 00 00 00 00 00 00 00 00 80 00 81 50 0c 00 30 00 96 | c0 00 0e 00 05 00 00 00 ff ff 81 00 00 00 00 00 97 | 00 00 00 00 00 00 00 00 00 00 01 50 9c 00 42 00 98 | 30 00 0e 00 06 00 00 00 ff ff 80 00 41 00 64 00 99 | 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 100 | 07 00 00 50 06 00 54 00 cb 00 3c 00 ff ff ff ff 101 | ff ff 80 00 00 00 00 00 00 00 00 00 00 00 00 00 102 | 03 00 21 50 48 00 61 00 84 00 64 00 07 00 00 00 103 | ff ff 85 00 00 00 00 00 00 00 00 00 00 00 00 00 104 | 00 00 02 50 0c 00 72 00 c0 00 08 00 08 00 00 00 105 | ff ff 82 00 00 00 00 00 00 00 00 00 00 00 00 00 106 | 00 00 01 50 9c 00 7e 00 30 00 0e 00 09 00 00 00 107 | ff ff 80 00 41 00 64 00 64 00 00 00 00 00 00 00 108 | 00 00 00 00 00 00 00 00 00 00 01 50 06 00 96 00 109 | 42 00 0e 00 0a 00 00 00 ff ff 80 00 41 00 64 00 110 | 64 00 20 00 66 00 69 00 6c 00 65 00 2e 00 2e 00 111 | 2e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 112 | 00 00 01 50 4e 00 96 00 42 00 0e 00 0b 00 00 00 113 | ff ff 80 00 41 00 64 00 64 00 20 00 6e 00 65 00 114 | 77 00 20 00 66 00 69 00 6c 00 65 00 2e 00 2e 00 115 | 2e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 116 | 00 00 01 50 06 00 a8 00 42 00 0e 00 0c 00 00 00 117 | ff ff 80 00 41 00 64 00 64 00 20 00 66 00 6f 00 118 | 6c 00 64 00 65 00 72 00 2e 00 2e 00 2e 00 00 00 119 | 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 50 120 | 06 00 c6 00 cc 00 1e 00 ff ff ff ff ff ff 80 00 121 | 43 00 6f 00 6d 00 6d 00 61 00 6e 00 64 00 20 00 122 | 6c 00 69 00 6e 00 65 00 3a 00 00 00 00 00 00 00 123 | 00 00 00 00 00 00 00 00 80 00 81 50 0c 00 d2 00 124 | c0 00 0e 00 0e 00 00 00 ff ff 81 00 00 00 00 00 125 | 00 00 00 00 00 00 00 00 00 00 02 50 0c 00 14 00 126 | 3c 00 08 00 1e 00 00 00 ff ff 82 00 4f 00 70 00 127 | 74 00 69 00 6f 00 6e 00 3a 00 00 00 00 00 00 00 128 | 00 00 00 00 00 00 00 00 00 00 02 50 0c 00 63 00 129 | 3c 00 08 00 46 00 00 00 ff ff 82 00 43 00 6f 00 130 | 6d 00 6d 00 61 00 6e 00 64 00 3a 00 00 00 00 00 131 | '''), 132 | 270 : a2b(''' 133 | 01 00 ff ff 00 00 00 00 00 00 00 00 48 04 00 44 134 | 02 00 00 00 00 00 23 01 1a 00 00 00 00 00 00 00 135 | 08 00 00 00 00 00 4d 00 53 00 20 00 53 00 68 00 136 | 65 00 6c 00 6c 00 20 00 44 00 6c 00 67 00 00 00 137 | 00 00 00 00 00 00 00 00 01 00 02 50 00 00 07 00 138 | 23 01 08 00 ff ff ff ff ff ff 82 00 53 00 74 00 139 | 61 00 74 00 69 00 63 00 00 00 00 00 00 00 00 00 140 | 00 00 00 00 00 00 02 40 00 00 12 00 23 01 08 00 141 | 5f 04 00 00 ff ff 82 00 73 00 74 00 63 00 33 00 142 | 32 00 00 00 00 00 143 | '''), 144 | } 145 | -------------------------------------------------------------------------------- /launcher/game/ability.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | # Checks for various abilities that might be taken away from us by 23 | # redistributors. 24 | 25 | init 1 python in ability: 26 | 27 | from store import config 28 | import store 29 | import store.updater as updater 30 | 31 | import os 32 | 33 | EXECUTABLES = [ "renpy.exe", "renpy.app", "renpy.sh" ] 34 | 35 | # can_distribute - True if we can distribute 36 | for i in EXECUTABLES: 37 | if not os.path.exists(os.path.join(config.renpy_base, i)): 38 | can_distribute = False 39 | else: 40 | can_distribute = True 41 | 42 | 43 | # can_update - True if we can update. 44 | can_update = updater.can_update() or (store.UPDATE_SIMULATE is not None) 45 | 46 | -------------------------------------------------------------------------------- /launcher/game/about.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | screen about: 23 | 24 | $ ren_version = renpy.version() 25 | 26 | frame: 27 | style_group "l" 28 | style "l_root" 29 | 30 | window: 31 | xfill True 32 | 33 | has vbox xfill True 34 | 35 | add "images/logo.png" xalign 0.5 yoffset 20 xsize 250 ysize 250 36 | 37 | null height 35 38 | 39 | text _("DDML [config.version!q]") xalign 0.5 bold True 40 | 41 | null height 10 42 | 43 | text _("Running [ren_version!q]") xalign 0.5 bold True 44 | 45 | null height 20 46 | 47 | textbutton _("View license") action interface.OpenLicense() xalign 0.5 48 | 49 | textbutton _("Return") action Jump("front_page") style "l_left_button" 50 | 51 | label about: 52 | call screen about 53 | 54 | -------------------------------------------------------------------------------- /launcher/game/add_file.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | label add_file: 23 | 24 | python hide: 25 | import os 26 | filename = "" 27 | while True: 28 | filename = interface.input( 29 | _("FILENAME"), 30 | _("Enter the name of the script file to create."), 31 | allow=interface.FILENAME_LETTERS, 32 | cancel=Jump("navigation"), 33 | default=filename, 34 | ) 35 | filename = filename.strip() 36 | if not filename: 37 | interface.error(_("The file name may not be empty."), label=None) 38 | continue 39 | 40 | if "." in filename and not filename.endswith(".rpy"): 41 | interface.error(_("The filename must have the .rpy extension."), label=None) 42 | continue 43 | elif "." not in filename: 44 | filename += ".rpy" 45 | 46 | path = os.path.join(project.current.gamedir, filename) 47 | dir = os.path.dirname(path) 48 | 49 | if os.path.exists(path): 50 | interface.error(_("The file already exists."), label=None) 51 | continue 52 | 53 | break 54 | 55 | try: 56 | os.makedirs(dir) 57 | except: 58 | pass 59 | 60 | contents = u"\uFEFF" 61 | contents += _("# Ren'Py automatically loads all script files ending with .rpy. To use this\n# file, define a label and jump to it from another file.\n") 62 | 63 | with open(path, "wb") as f: 64 | f.write(contents.encode("utf-8")) 65 | 66 | jump navigation_refresh 67 | -------------------------------------------------------------------------------- /launcher/game/archiver.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 | # Ren'Py archiver. This builds a Ren'Py archive file, and the 23 | # associated index file. These files are really easy to 24 | # reverse-engineer, but are probably better than nothing. 25 | 26 | init python in archiver: 27 | 28 | import sys 29 | import random 30 | import glob 31 | import zlib 32 | 33 | from pickle import dumps, HIGHEST_PROTOCOL 34 | 35 | 36 | class Archive(object): 37 | """ 38 | Adds files from disk to a rpa archive. 39 | """ 40 | 41 | def __init__(self, filename): 42 | 43 | # The archive file. 44 | self.f = open(filename, "wb") 45 | 46 | # The index to the file. 47 | self.index = _dict() 48 | 49 | # A fixed key minimizes difference between archive versions. 50 | self.key = 0x42424242 51 | 52 | padding = b"RPA-3.0 XXXXXXXXXXXXXXXX XXXXXXXX\n" 53 | self.f.write(padding) 54 | 55 | def add(self, name, path): 56 | """ 57 | Adds a file to the archive. 58 | """ 59 | 60 | self.index[name] = _list() 61 | 62 | with open(path, "rb") as df: 63 | data = df.read() 64 | dlen = len(data) 65 | 66 | # Pad. 67 | padding = b"Made with Ren'Py." 68 | self.f.write(padding) 69 | 70 | offset = self.f.tell() 71 | 72 | self.f.write(data) 73 | 74 | self.index[name].append((offset ^ self.key, dlen ^ self.key, b"")) 75 | 76 | def close(self): 77 | 78 | indexoff = self.f.tell() 79 | 80 | self.f.write(zlib.compress(dumps(self.index, HIGHEST_PROTOCOL))) 81 | 82 | self.f.seek(0) 83 | self.f.write(b"RPA-3.0 %016x %08x\n" % (indexoff, self.key)) 84 | 85 | self.f.close() 86 | 87 | -------------------------------------------------------------------------------- /launcher/game/choose_directory.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 | init python: 23 | 24 | if renpy.windows: 25 | import EasyDialogsWin as EasyDialogs 26 | else: 27 | EasyDialogs = None 28 | 29 | pyobjus = None 30 | 31 | if renpy.macintosh: 32 | try: 33 | import pyobjus 34 | except: 35 | pass 36 | 37 | 38 | def directory_is_writable(path): 39 | test = os.path.join(path, "renpy test do not use") 40 | 41 | try: 42 | if os.path.isdir(test): 43 | os.rmdir(test) 44 | 45 | os.mkdir(test) 46 | os.rmdir(test) 47 | 48 | return True 49 | 50 | except: 51 | return False 52 | 53 | def choose_directory(path): 54 | """ 55 | Pops up a directory chooser. 56 | 57 | `path` 58 | The directory that is selected by default. If None, config.renpy_base 59 | is selected. 60 | 61 | Returns a (path, is_default) tuple, where path is the chosen directory, 62 | and is_default is true if and only if it was chosen by default mechanism 63 | rather than user choice. 64 | """ 65 | 66 | if path: 67 | default_path = path 68 | path = None 69 | else: 70 | try: 71 | default_path = os.path.dirname(os.path.abspath(config.renpy_base)) 72 | except: 73 | default_path = os.path.abspath(config.renpy_base) 74 | 75 | if EasyDialogs: 76 | 77 | choice = EasyDialogs.AskFolder(defaultLocation=default_path, wanted=unicode) 78 | 79 | if choice is not None: 80 | path = choice 81 | else: 82 | path = None 83 | 84 | elif pyobjus: 85 | 86 | from pyobjus import autoclass 87 | from pyobjus.dylib_manager import load_framework, INCLUDE 88 | 89 | load_framework(INCLUDE.AppKit) 90 | NSURL = autoclass('NSURL') 91 | NSOpenPanel = autoclass('NSOpenPanel') 92 | 93 | panel = NSOpenPanel.openPanel() 94 | panel.setCanChooseDirectories_(True) 95 | panel.setCanChooseFiles_(False) 96 | panel.setCanCreateDirectories_(True) 97 | 98 | if default_path: 99 | url = NSURL.fileURLWithPath_(default_path) 100 | panel.setDirectoryURL_(url) 101 | 102 | if panel.runModal(): 103 | path = panel.filename().UTF8String().decode("utf-8") 104 | else: 105 | path = None 106 | 107 | else: 108 | 109 | try: 110 | 111 | if renpy.macintosh: 112 | # tkinter is broken on Python 3, so use it as a last resort - maybe apple fixed it? 113 | system_pythons = [ "/usr/bin/python2", "/usr/bin/python", "/usr/bin/python3" ] 114 | else: 115 | system_pythons = [ "/usr/bin/python3", "/usr/bin/python2", "/usr/bin/python" ] 116 | 117 | for system_python in system_pythons: 118 | if os.path.exists(system_python): 119 | break 120 | else: 121 | system_python = system_pythons[0] 122 | 123 | cmd = [ system_python, os.path.join(config.gamedir, "tkaskdir.py"), renpy.fsencode(default_path) ] 124 | 125 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 126 | choice = p.stdout.read() 127 | code = p.wait() 128 | 129 | except: 130 | import traceback 131 | traceback.print_exc() 132 | 133 | code = 0 134 | choice = "" 135 | path = None 136 | 137 | interface.error(_("Ren'Py was unable to run python with tkinter to choose the directory. Please install the python3-tk or tkinter package."), label=None) 138 | 139 | if code: 140 | interface.error(_("Ren'Py was unable to run python with tkinter to choose the directory. Please install the python3-tk or tkinter package."), label=None) 141 | 142 | elif choice: 143 | path = choice.decode("utf-8") 144 | 145 | is_default = False 146 | 147 | # Path being None or "" means nothing was selected. 148 | if not path: 149 | path = default_path 150 | is_default = True 151 | 152 | path = renpy.fsdecode(path) 153 | 154 | if (not os.path.isdir(path)) or (not directory_is_writable(path)): 155 | interface.error(_("The selected projects directory is not writable.")) 156 | path = default_path 157 | is_default = True 158 | 159 | if is_default and (not directory_is_writable(path)): 160 | path = os.path.expanduser("~") 161 | 162 | return path, is_default 163 | 164 | def choose_file(path, mod = False): 165 | """ 166 | Pops up a file chooser. 167 | 168 | `path` 169 | The directory that is selected by default. If None, config.renpy_base 170 | is selected. 171 | 172 | Returns a (path, is_default) tuple, where path is the chosen directory, 173 | and is_default is true if and only if it was chosen by default mechanism 174 | rather than user choice. 175 | """ 176 | 177 | if path: 178 | default_path = path 179 | path = None 180 | else: 181 | try: 182 | default_path = os.path.dirname(os.path.abspath(config.renpy_base)) 183 | except: 184 | default_path = os.path.abspath(config.renpy_base) 185 | 186 | if EasyDialogs: 187 | 188 | if not mod: 189 | choice = EasyDialogs.AskFileForOpen(typeList=[('DDLC ZIP File', '*.zip')], defaultLocation=default_path, windowTitle="Select DDLC ZIP File", wanted=unicode) 190 | else: 191 | choice = EasyDialogs.AskFileForOpen(typeList=[('DDLC Mod File', '*.zip')], defaultLocation=default_path, windowTitle="Select Mod ZIP File", wanted=unicode) 192 | 193 | if choice is not None: 194 | path = choice 195 | else: 196 | path = None 197 | 198 | elif pyobjus: 199 | 200 | from pyobjus import autoclass 201 | from pyobjus.dylib_manager import load_framework, INCLUDE 202 | 203 | load_framework(INCLUDE.AppKit) 204 | NSURL = autoclass('NSURL') 205 | NSOpenPanel = autoclass('NSOpenPanel') 206 | 207 | panel = NSOpenPanel.openPanel() 208 | panel.setCanChooseDirectories_(False) 209 | panel.setCanChooseFiles_(True) 210 | panel.setCanCreateDirectories_(False) 211 | 212 | if default_path: 213 | url = NSURL.fileURLWithPath_(default_path) 214 | panel.setDirectoryURL_(url) 215 | 216 | if panel.runModal(): 217 | path = panel.filename().UTF8String().decode("utf-8") 218 | else: 219 | path = None 220 | 221 | else: 222 | 223 | try: 224 | 225 | if renpy.macintosh: 226 | # tkinter is broken on Python 3, so use it as a last resort - maybe apple fixed it? 227 | system_pythons = [ "/usr/bin/python2", "/usr/bin/python", "/usr/bin/python3" ] 228 | else: 229 | system_pythons = [ "/usr/bin/python3", "/usr/bin/python2", "/usr/bin/python" ] 230 | 231 | for system_python in system_pythons: 232 | if os.path.exists(system_python): 233 | break 234 | else: 235 | system_python = system_pythons[0] 236 | 237 | cmd = [ system_python, os.path.join(config.gamedir, "tkaskfile.py"), renpy.fsencode(default_path) ] 238 | 239 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 240 | choice = p.stdout.read() 241 | code = p.wait() 242 | 243 | except: 244 | import traceback 245 | traceback.print_exc() 246 | 247 | code = 0 248 | choice = "" 249 | path = None 250 | 251 | interface.error(_("Ren'Py was unable to run python with tkinter to choose the directory. Please install the python3-tk or tkinter package."), label=None) 252 | 253 | if code: 254 | interface.error(_("Ren'Py was unable to run python with tkinter to choose the directory. Please install the python3-tk or tkinter package."), label=None) 255 | 256 | elif choice: 257 | path = choice.decode("utf-8") 258 | 259 | is_default = False 260 | 261 | # Path being None or "" means nothing was selected. 262 | if not path: 263 | path = default_path 264 | is_default = True 265 | 266 | path = renpy.fsdecode(path) 267 | 268 | return path, is_default -------------------------------------------------------------------------------- /launcher/game/ddmm_compatibility.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import os 3 | from modmanagement import ModManagement 4 | import logging 5 | import renpy 6 | from renpy import config 7 | 8 | 9 | class DDMM_Compatibility: 10 | """ 11 | This class adds DDMM (Mod Manager) compatibility features to DDML. 12 | """ 13 | 14 | def __init__(self): 15 | # The DDMM directory in Windows 16 | if renpy.windows: 17 | self.ddmm_gamedir = os.path.join( 18 | os.getenv("APPDATA"), "DokiDokiModManager/GameData/installs" 19 | ) 20 | else: 21 | self.ddmm_gamedir = None 22 | self.modman = ModManagement() 23 | self.log_file = os.path.join(config.basedir, "transfer_log.txt") 24 | 25 | if os.path.exists(self.log_file): 26 | os.remove(self.log_file) 27 | 28 | def ddmm_detection(self): 29 | """ 30 | This define returns True if the DDMM directory in %APPDATA% is present. 31 | """ 32 | 33 | logging.debug("Locating DDMM directory in %APPDATA%.") 34 | 35 | if not renpy.windows: 36 | logging.exception("OS is not Windows! Exiting execution.") 37 | return False 38 | 39 | return os.path.exists(self.ddmm_gamedir) 40 | 41 | def ddmm_path_setup(self): 42 | """ 43 | This define sets the project directory in DDML to the one in DDMM. 44 | """ 45 | 46 | logging.debug( 47 | "Located DDMM directory. Setting it to persistent.projects_directory." 48 | ) 49 | 50 | return self.ddmm_gamedir 51 | 52 | def ddmm_folder_not_compliant(self, project_dir, x): 53 | """ 54 | This define checks if the mod in the DDMM has not been setup already 55 | by DDML if transfer is started again. 56 | """ 57 | 58 | logging.debug("Checking if " + x + " is DDML compliant.") 59 | 60 | return os.path.exists(os.path.join(project_dir, x, "install")) 61 | 62 | def ddmm_folder_setup(self, project_dir, x): 63 | """ 64 | This define adjusts the folders in the DDMM directory to be Ren'Py 65 | Launcher compliant. 66 | """ 67 | 68 | logging.debug("Making " + x + " DDML Compliant.") 69 | 70 | main_path = os.path.join(project_dir, x, "install") 71 | 72 | self.modman.move_mod_folder(main_path, os.path.join(project_dir, x)) 73 | shutil.rmtree(main_path) 74 | 75 | logging.debug(x + " is now DDML Compliant.") 76 | 77 | def ddmm_revert_folder_setup(self, project_dir, x): 78 | """ 79 | This define reverts changes made to the DDMM directory if a error has 80 | occured. 81 | """ 82 | 83 | main_path = os.path.join(project_dir, x) 84 | 85 | self.modman.move_mod_folder(main_path, os.path.join(project_dir, x, "install")) 86 | 87 | logging.debug("Reverted changes made to " + x + ".") 88 | 89 | def ddmm_traceback(self, x): 90 | """ 91 | This define makes a custom traceback file for the DDMM transfer tool. 92 | """ 93 | 94 | logging.exception("Error occured when transferring " + x + ".") 95 | 96 | def ddmm_traceback_start(self): 97 | """ 98 | This define starts the transfer traceback when the transfer tool is 99 | running. 100 | """ 101 | 102 | logging.basicConfig(filename=self.log_file, level=logging.DEBUG) 103 | 104 | def ddmm_traceback_shutdown(self): 105 | """ 106 | This define stops the transfer traceback when the transfer tool is not 107 | running. 108 | """ 109 | 110 | logging.debug("Stopped logging for this session.") 111 | logging.shutdown() 112 | -------------------------------------------------------------------------------- /launcher/game/distribute_gui.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | init python: 23 | class PackageToggle(Action): 24 | def __init__(self, name): 25 | self.name = name 26 | 27 | def get_selected(self): 28 | return self.name in project.current.data['packages'] 29 | 30 | def __call__(self): 31 | packages = project.current.data['packages'] 32 | 33 | if self.name in packages: 34 | packages.remove(self.name) 35 | else: 36 | packages.append(self.name) 37 | 38 | project.current.save_data() 39 | renpy.restart_interaction() 40 | 41 | class DataToggle(Action): 42 | def __init__(self, field): 43 | self.field = field 44 | 45 | def get_selected(self): 46 | return project.current.data[self.field] 47 | 48 | def __call__(self): 49 | project.current.data[self.field] = not project.current.data[self.field] 50 | 51 | project.current.save_data() 52 | renpy.restart_interaction() 53 | 54 | 55 | DEFAULT_BUILD_INFO = """ 56 | 57 | ## This section contains information about how to build your project into 58 | ## distribution files. 59 | init python: 60 | 61 | ## The name that's used for directories and archive files. For example, if 62 | ## this is 'mygame-1.0', the windows distribution will be in the 63 | ## directory 'mygame-1.0-win', in the 'mygame-1.0-win.zip' file. 64 | build.directory_name = "PROJECTNAME-1.0" 65 | 66 | ## The name that's uses for executables - the program that users will run 67 | ## to start the game. For example, if this is 'mygame', then on Windows, 68 | ## users can click 'mygame.exe' to start the game. 69 | build.executable_name = "PROJECTNAME" 70 | 71 | ## If True, Ren'Py will include update information into packages. This 72 | ## allows the updater to run. 73 | build.include_update = False 74 | 75 | ## File patterns: 76 | ## 77 | ## The following functions take file patterns. File patterns are case- 78 | ## insensitive, and matched against the path relative to the base 79 | ## directory, with and without a leading /. If multiple patterns match, 80 | ## the first is used. 81 | ## 82 | ## 83 | ## In a pattern: 84 | ## 85 | ## / 86 | ## Is the directory separator. 87 | ## * 88 | ## Matches all characters, except the directory separator. 89 | ## ** 90 | ## Matches all characters, including the directory separator. 91 | ## 92 | ## For example: 93 | ## 94 | ## *.txt 95 | ## Matches txt files in the base directory. 96 | ## game/**.ogg 97 | ## Matches ogg files in the game directory or any of its subdirectories. 98 | ## **.psd 99 | ## Matches psd files anywhere in the project. 100 | 101 | ## Classify files as None to exclude them from the built distributions. 102 | 103 | build.classify('**~', None) 104 | build.classify('**.bak', None) 105 | build.classify('**/.**', None) 106 | build.classify('**/#**', None) 107 | build.classify('**/thumbs.db', None) 108 | 109 | ## To archive files, classify them as 'archive'. 110 | 111 | # build.classify('game/**.png', 'archive') 112 | # build.classify('game/**.jpg', 'archive') 113 | 114 | ## Files matching documentation patterns are duplicated in a mac app 115 | ## build, so they appear in both the app and the zip file. 116 | 117 | build.documentation('*.html') 118 | build.documentation('*.txt') 119 | """ 120 | 121 | # A screen that displays a file or directory name, and 122 | # lets the user change it, 123 | # 124 | # title 125 | # The title of the link. 126 | # value 127 | # The value of the field. 128 | screen distribute_name: 129 | 130 | add SEPARATOR2 131 | 132 | frame: 133 | style "l_indent" 134 | has vbox 135 | 136 | text title 137 | 138 | add HALF_SPACER 139 | 140 | frame: 141 | style "l_indent" 142 | text "[value!q]" 143 | 144 | add SPACER 145 | 146 | 147 | screen build_distributions: 148 | 149 | frame: 150 | style_group "l" 151 | style "l_root" 152 | 153 | window: 154 | 155 | has vbox 156 | 157 | label _("Build Distributions: [project.current.display_name!q]") 158 | 159 | add HALF_SPACER 160 | 161 | hbox: 162 | 163 | # Left side. 164 | frame: 165 | style "l_indent" 166 | xmaximum ONEHALF 167 | xfill True 168 | 169 | has vbox 170 | 171 | use distribute_name( 172 | title=_("Directory Name:"), 173 | value=project.current.dump["build"]["directory_name"]) 174 | 175 | use distribute_name( 176 | title=_("Executable Name:"), 177 | value=project.current.dump["build"]["executable_name"]) 178 | 179 | add SEPARATOR2 180 | 181 | frame: 182 | style "l_indent" 183 | has vbox 184 | 185 | text _("Actions:") 186 | 187 | add HALF_SPACER 188 | 189 | frame style "l_indent": 190 | 191 | has vbox 192 | 193 | textbutton _("Edit options.rpy") action editor.Edit("game/options.rpy", check=True) 194 | textbutton _("Add from clauses to calls, once") action Jump("add_from") 195 | textbutton _("Refresh") action Jump("build_distributions") 196 | 197 | # Right side. 198 | frame: 199 | style "l_indent" 200 | xmaximum ONEHALF 201 | xfill True 202 | 203 | has vbox 204 | 205 | add SEPARATOR2 206 | 207 | frame: 208 | style "l_indent" 209 | has vbox 210 | 211 | text _("Build Packages:") 212 | 213 | add HALF_SPACER 214 | 215 | $ packages = project.current.dump["build"]["packages"] 216 | 217 | for pkg in packages: 218 | if not pkg["hidden"]: 219 | $ description = pkg["description"] 220 | textbutton "[description!q]" action PackageToggle(pkg["name"]) style "l_checkbox" 221 | 222 | add SPACER 223 | add HALF_SPACER 224 | add SEPARATOR2 225 | 226 | frame: 227 | style "l_indent" 228 | has vbox 229 | 230 | text _("Options:") 231 | 232 | add HALF_SPACER 233 | 234 | if project.current.dump["build"]["include_update"]: 235 | textbutton _("Build Updates") action DataToggle("build_update") style "l_checkbox" 236 | 237 | textbutton _("Add from clauses to calls") action DataToggle("add_from") style "l_checkbox" 238 | textbutton _("Force Recompile") action DataToggle("force_recompile") style "l_checkbox" 239 | 240 | 241 | textbutton _("Return") action Jump("front_page") style "l_left_button" 242 | textbutton _("Build") action Jump("start_distribute") style "l_right_button" 243 | 244 | label add_from_common: 245 | python: 246 | interface.processing(_("Adding from clauses to call statements that do not have them.")) 247 | project.current.launch([ "add_from" ], wait=True) 248 | 249 | return 250 | 251 | label add_from: 252 | call add_from_common 253 | jump build_distributions 254 | 255 | 256 | label start_distribute: 257 | if project.current.data["add_from"]: 258 | call add_from_common 259 | 260 | jump distribute 261 | 262 | label build_update_dump: 263 | python: 264 | project.current.update_dump(True) 265 | 266 | if project.current.dump.get("error", False): 267 | interface.error(_("Errors were detected when running the project. Please ensure the project runs without errors before building distributions.")) 268 | 269 | return 270 | 271 | label build_distributions: 272 | 273 | call build_update_dump 274 | 275 | if not project.current.dump["build"]["directory_name"]: 276 | jump build_missing 277 | 278 | call screen build_distributions 279 | 280 | label build_missing: 281 | 282 | python hide: 283 | 284 | interface.yesno(_("Your project does not contain build information. Would you like to add build information to the end of options.rpy?"), yes=Return(True), no=Jump("front_page")) 285 | 286 | project_name = project.current.name 287 | project_name = project_name.replace(" ", "_") 288 | project_name = project_name.replace(":", "") 289 | project_name = project_name.replace(";", "") 290 | 291 | build_info = DEFAULT_BUILD_INFO.replace("PROJECTNAME", project_name) 292 | 293 | with open(os.path.join(project.current.path, "game", "options.rpy"), "a") as f: 294 | f.write(build_info) 295 | 296 | jump build_distributions 297 | -------------------------------------------------------------------------------- /launcher/game/dmgcheck.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | # This file checks to see if we're inside a dmg, and displays an error 23 | # message to the player if we are. 24 | 25 | 26 | init python: 27 | 28 | def dmgcheck(): 29 | 30 | if not renpy.macintosh: 31 | return 32 | 33 | fn = os.path.join(config.renpy_base, "dmgcheck.txt") 34 | 35 | try: 36 | if os.path.exists(fn): 37 | os.unlink(fn) 38 | 39 | with open(fn, "wb") as f: 40 | f.write("Testing to see if we're in a DMG.\r\n") 41 | 42 | os.unlink(fn) 43 | return 44 | 45 | except: 46 | 47 | # If we're here, we didn't make it through the checks safely. So 48 | # put up a warning message. 49 | 50 | interface.info( 51 | message=_("Ren'Py is running from a read only folder. Some functionality will not work."), 52 | submessage=_("This is probably because Ren'Py is running directly from a Macintosh drive image. To fix this, quit this launcher, copy the entire %s folder somewhere else on your computer, and run Ren'Py again.") % (os.path.basename(config.renpy_base)), 53 | ) 54 | -------------------------------------------------------------------------------- /launcher/game/download.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2020 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 | init python: 23 | 24 | import urllib.request 25 | import os 26 | import threading 27 | import time 28 | 29 | ssl_context_cache = None 30 | 31 | def ssl_context(): 32 | """ 33 | Returns the SSL context. 34 | """ 35 | 36 | global ssl_context_cache 37 | 38 | if ssl_context_cache is None: 39 | import ssl 40 | import certifi 41 | ssl_context_cache = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) 42 | 43 | return ssl_context_cache 44 | 45 | class Downloader(object): 46 | 47 | def __init__(self, url, dest): 48 | """ 49 | Downloads `url` to `dest`, providing progress reports 50 | as necessary. 51 | """ 52 | 53 | self.url = url 54 | 55 | # The destination file, and the destination temp file. 56 | self.dest = dest 57 | self.tmp = dest + ".tmp" 58 | 59 | # Open the tmpfile. 60 | self.safe_unlink(self.tmp) 61 | self.tmpfile = open(self.tmp, "wb") 62 | 63 | # Set by the thread to indicate progress (ranges from 0.0 to 1.0). 64 | self.progress = 0.0 65 | 66 | # This is set to true by cancel() to indicate the download should be cancelled. 67 | self.cancelled = False 68 | 69 | # Set on succes or failure. 70 | self.success = False 71 | self.failure = None 72 | 73 | try: 74 | # Open the URL. 75 | 76 | self.urlfile = urllib.request.urlopen(url, context=ssl_context()) 77 | 78 | t = threading.Thread(target=self.thread) 79 | t.daemon = True 80 | t.start() 81 | 82 | except Exception as e: 83 | self.failure = str(e) 84 | 85 | def thread(self): 86 | 87 | try: 88 | count = 0 89 | 90 | if "content-length" in self.urlfile.headers: 91 | length = int(self.urlfile.headers["content-length"]) 92 | else: 93 | length = 0 94 | 95 | while not self.cancelled: 96 | 97 | data = self.urlfile.read(65536) 98 | 99 | if not data: 100 | break 101 | 102 | count += len(data) 103 | self.tmpfile.write(data) 104 | 105 | if length > 0: 106 | self.progress = 1.0 * count / length 107 | 108 | self.tmpfile.close() 109 | 110 | if self.cancelled: 111 | return 112 | 113 | if length and count != length: 114 | self.failure = "Download length does not match content length." 115 | return 116 | 117 | self.safe_unlink(self.dest) 118 | os.rename(self.tmp, self.dest) 119 | 120 | self.success = True 121 | 122 | except Exception as e: 123 | self.failure = str(e) 124 | 125 | 126 | def safe_unlink(self, fn): 127 | if os.path.exists(fn): 128 | os.unlink(fn) 129 | 130 | def cancel(self): 131 | """ 132 | Cancels the download. 133 | """ 134 | 135 | self.cancelled = True 136 | 137 | def check(self): 138 | """ 139 | Returns True if the download is finished, False if it was cancelled, 140 | None if it's ongoing, and raises an Exception if the download has failed. 141 | """ 142 | 143 | if self.success: 144 | return True 145 | if self.cancelled: 146 | return False 147 | if self.failure: 148 | raise Exception("Downloading {} to {} failed: {}".format(self.url, self.dest, self.failure)) 149 | 150 | return None 151 | 152 | class DownloaderValue(BarValue): 153 | """ 154 | A BarValue that reports the progress of a background download. 155 | """ 156 | 157 | def __init__(self, d): 158 | self.downloader = d 159 | 160 | def get_adjustment(self): 161 | self.adjustment = ui.adjustment(value=0.0, range=1.0, adjustable=False) 162 | return self.adjustment 163 | 164 | def periodic(self, st): 165 | self.adjustment.change(self.downloader.progress) 166 | return .25 167 | 168 | -------------------------------------------------------------------------------- /launcher/game/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /launcher/game/extractor.py: -------------------------------------------------------------------------------- 1 | from zipfile import ZipFile 2 | import tempfile 3 | import os 4 | import shutil 5 | import sys 6 | 7 | 8 | class Extractor: 9 | """ 10 | This class is responsible for the extraction of 11 | zip files for DDLC mods. 12 | """ 13 | 14 | def __init__(self): 15 | self.renpy_script_contents = (".rpa", ".rpyc", ".rpy") 16 | self.ddlc_base_contents = ("characters", "game", "lib", "renpy") 17 | self.renpy_executables = (".exe", ".sh", ".app") 18 | 19 | def valid_zip(self, filePath): 20 | """ 21 | Returns whether the given ZIP file is a valid Ren'Py/DDLC mod ZIP file. 22 | 23 | filePath - the direct path to the ZIP file. 24 | """ 25 | 26 | contents = [] 27 | 28 | with ZipFile(filePath, "r") as z: 29 | contents = z.namelist() 30 | 31 | for x in contents: 32 | if x.endswith((self.renpy_script_contents)): 33 | contents = [] 34 | return True 35 | 36 | return False 37 | 38 | def game_installation(self, filePath, modFolder, copy=False): 39 | """ 40 | Extracts DDLC/Ren'Py game to the mod folder. 41 | 42 | filePath - The given game zip package. 43 | 44 | modFolder - The mod folder inside the mod install folder. 45 | 46 | copy - Makes sure this is a folder or a ZIP we are working with. 47 | """ 48 | 49 | os.makedirs(modFolder) 50 | 51 | if not copy: 52 | td = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempGame") 53 | 54 | with ZipFile(filePath, "r") as z: 55 | z.extractall(td) 56 | 57 | if sys.platform == "darwin": 58 | game_dir = td 59 | else: 60 | game_dir = os.path.join(td, os.listdir(td)[-1]) 61 | else: 62 | game_dir = filePath 63 | 64 | for temp_src, dirs, files in os.walk(game_dir): 65 | dst_dir = temp_src.replace(game_dir, modFolder) 66 | 67 | for d in dirs: 68 | if not os.path.exists(os.path.join(dst_dir, d)): 69 | os.makedirs(os.path.join(dst_dir, d)) 70 | 71 | for f in files: 72 | shutil.move(os.path.join(temp_src, f), os.path.join(dst_dir, f)) 73 | 74 | def installation(self, filePath, modFolder, copy=False): 75 | """ 76 | Extracts the mod archive to the mod folder and attempts to fix the mod 77 | for Ren'Py structure compliance. 78 | 79 | filepath - The given mod zip package. 80 | 81 | modFolder - The mod folder inside the mod install folder. 82 | 83 | copy - Makes sure this is a folder or a ZIP we are working with. 84 | """ 85 | 86 | if not copy: 87 | mod_dir = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempArchive") 88 | 89 | with ZipFile(filePath, "r") as z: 90 | z.extractall(mod_dir) 91 | else: 92 | mod_dir = filePath 93 | 94 | base_files_dir = None 95 | 96 | # Check if the folder copy/extracted has a Renpy7Mod/Mod folder or game 97 | # folder inside to set directory. 98 | for mod_src, dirs, files in os.walk(mod_dir): 99 | for d in dirs: 100 | if d.endswith(("Renpy7Mod", "Mod")): 101 | base_files_dir = os.path.join(mod_src, d) 102 | elif d.endswith(("game")): 103 | base_files_dir = mod_src 104 | elif os.path.exists(os.path.join(mod_src, d, "game")): 105 | base_files_dir = os.path.join(mod_src, d) 106 | 107 | # If we were unable to get a directory from the above check, fix the archive 108 | # by sending it to an new temp folder and applying fixes. 109 | if not base_files_dir: 110 | fix_dir = tempfile.mkdtemp(prefix="NewDDML_", suffix="_TempFixArchive") 111 | os.makedirs(os.path.join(fix_dir, "game")) 112 | 113 | for mod_src, dirs, files in os.walk(mod_dir): 114 | dst_dir = mod_src.replace(mod_dir, fix_dir) 115 | 116 | for d in dirs: 117 | if mod_src.endswith(self.ddlc_base_contents): 118 | if not os.path.exists(os.path.join(dst_dir, d)): 119 | os.makedirs(os.path.join(dst_dir, d)) 120 | else: 121 | if not os.path.exists(os.path.join(dst_dir, "game", d)): 122 | os.makedirs(os.path.join(dst_dir, "game", d)) 123 | 124 | for f in files: 125 | if f.endswith(self.renpy_executables): 126 | shutil.move(os.path.join(mod_src, f), os.path.join(dst_dir, f)) 127 | elif f.endswith(".py") and os.path.join(mod_src, f) == os.path.join(mod_dir, f): 128 | shutil.move(os.path.join(mod_src, f), os.path.join(dst_dir, f)) 129 | elif mod_src.endswith(self.ddlc_base_contents): 130 | if os.path.exists(os.path.join(dst_dir, f)): 131 | if os.path.samefile(os.path.join(mod_src, f), os.path.join(dst_dir, f)): 132 | continue 133 | 134 | os.remove(os.path.join(dst_dir, f)) 135 | 136 | shutil.move(os.path.join(mod_src, f), os.path.join(dst_dir, f)) 137 | else: 138 | if os.path.exists(os.path.join(mod_src.replace(mod_dir, fix_dir + "/game"), f)): 139 | if os.path.samefile(os.path.join(mod_src.replace(mod_dir, fix_dir + "/game"), f), os.path.join(dst_dir, f)): 140 | continue 141 | 142 | os.remove(os.path.join(mod_src.replace(mod_dir, fix_dir + "/game"), f)) 143 | 144 | shutil.move( 145 | os.path.join(mod_src, f), 146 | os.path.join( 147 | mod_src.replace(mod_dir, fix_dir + "/game"), f 148 | ), 149 | ) 150 | 151 | # Set the directory to the fixed mod folder 152 | base_files_dir = fix_dir 153 | 154 | if sys.platform == "darwin": 155 | modFolder = os.path.join(modFolder, "DDLC.app/Contents/Resources/autorun") 156 | 157 | for mod_src, dirs, files in os.walk(base_files_dir): 158 | dst_dir = mod_src.replace(base_files_dir, modFolder) 159 | 160 | for d in dirs: 161 | if not os.path.exists(os.path.join(dst_dir, d)): 162 | os.makedirs(os.path.join(dst_dir, d)) 163 | 164 | for f in files: 165 | shutil.move(os.path.join(mod_src, f), os.path.join(dst_dir, f)) 166 | -------------------------------------------------------------------------------- /launcher/game/fonts/MTLc3m.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/MTLc3m.ttf -------------------------------------------------------------------------------- /launcher/game/fonts/NanumGothic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/NanumGothic.ttf -------------------------------------------------------------------------------- /launcher/game/fonts/Naver Nanum Font License.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/Naver Nanum Font License.txt -------------------------------------------------------------------------------- /launcher/game/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /launcher/game/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /launcher/game/fonts/SourceHanSans-Light-Lite.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/SourceHanSans-Light-Lite.ttf -------------------------------------------------------------------------------- /launcher/game/fonts/SourceHanSansLite.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/fonts/SourceHanSansLite.ttf -------------------------------------------------------------------------------- /launcher/game/front_page.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | define PROJECT_ADJUSTMENT = ui.adjustment() 23 | 24 | init python: 25 | 26 | import os 27 | import subprocess 28 | from modmanagement import ModManagement 29 | from ddmm_compatibility import DDMM_Compatibility 30 | 31 | modman = ModManagement() 32 | mm_compat = DDMM_Compatibility() 33 | 34 | class OpenDirectory(Action): 35 | """ 36 | Opens `directory` in a file browser. `directory` is relative to 37 | the project root. 38 | """ 39 | 40 | alt = _("Open [text] directory.") 41 | 42 | def __init__(self, directory, absolute=False): 43 | if absolute: 44 | self.directory = directory 45 | else: 46 | self.directory = os.path.join(project.current.path, directory) 47 | 48 | def get_sensitive(self): 49 | return os.path.exists(self.directory) 50 | 51 | def __call__(self): 52 | 53 | try: 54 | directory = renpy.fsencode(self.directory) 55 | 56 | if renpy.windows: 57 | os.startfile(directory) 58 | elif renpy.macintosh: 59 | subprocess.Popen([ "open", directory ]) 60 | else: 61 | subprocess.Popen([ "xdg-open", directory ]) 62 | 63 | except: 64 | pass 65 | 66 | # Used for testing. 67 | def Relaunch(): 68 | renpy.quit(relaunch=True) 69 | 70 | screen front_page: 71 | frame: 72 | alt "" 73 | 74 | style_group "l" 75 | style "l_root" 76 | 77 | has hbox 78 | 79 | # Projects list section - on left. 80 | 81 | frame: 82 | style "l_projects" 83 | xmaximum 300 84 | right_margin 2 85 | 86 | top_padding 20 87 | bottom_padding 26 88 | 89 | side "t c b": 90 | 91 | window style "l_label": 92 | 93 | has hbox: 94 | xfill True 95 | 96 | text _("Mods:") style "l_label_text" size 36 yoffset 10 97 | 98 | textbutton _("refresh"): 99 | xalign 1.0 100 | yalign 1.0 101 | yoffset 5 102 | style "l_small_button" 103 | action project.Rescan() 104 | right_margin HALF_INDENT 105 | 106 | side "c r": 107 | 108 | viewport: 109 | yadjustment PROJECT_ADJUSTMENT 110 | mousewheel True 111 | use front_page_project_list 112 | 113 | vbar: 114 | style "l_vscrollbar" 115 | adjustment PROJECT_ADJUSTMENT 116 | 117 | vbox: 118 | add HALF_SPACER 119 | add SEPARATOR 120 | add HALF_SPACER 121 | 122 | vbox: 123 | xfill True 124 | 125 | textbutton _("+ Move Mod Folder"): 126 | left_margin (HALF_INDENT) 127 | action Jump("move_mod_folder") 128 | 129 | add HALF_SPACER 130 | textbutton _("+ Add a Mod"): 131 | left_margin (HALF_INDENT) 132 | action Jump("add_a_mod") 133 | 134 | add HALF_SPACER 135 | textbutton _("+ Add DDLC"): 136 | left_margin (HALF_INDENT) 137 | action Jump("add_base_game") 138 | 139 | add HALF_SPACER 140 | textbutton _("+ Search for Mods"): 141 | left_margin (HALF_INDENT) 142 | action Jump("update") 143 | 144 | # Project section - on right. 145 | 146 | if project.current is not None: 147 | use front_page_project 148 | 149 | if project.current is not None: 150 | textbutton _("Launch Mod") action project.Launch() style "l_right_button" 151 | key "K_F5" action project.Launch() 152 | 153 | 154 | 155 | # This is used by front_page to display the list of known projects on the screen. 156 | screen front_page_project_list: 157 | 158 | $ projects = project.manager.projects 159 | $ templates = project.manager.templates 160 | 161 | vbox: 162 | 163 | if templates and persistent.show_templates: 164 | 165 | for p in templates: 166 | 167 | textbutton _("[p.name!q] (template)"): 168 | action project.Select(p) 169 | alt _("Select project [text].") 170 | style "l_list" 171 | 172 | null height 12 173 | 174 | if projects: 175 | 176 | for p in projects: 177 | 178 | textbutton "[p.name!q]": 179 | action project.Select(p) 180 | alt _("Select project [text].") 181 | style "l_list" 182 | 183 | null height 12 184 | 185 | 186 | # This is used for the right side of the screen, which is where the project-specific 187 | # buttons are. 188 | screen front_page_project: 189 | 190 | $ p = project.current 191 | 192 | window: 193 | 194 | has vbox 195 | 196 | frame style "l_label": 197 | has hbox xfill True 198 | text "[p.display_name!q]" style "l_label_text" 199 | label _("Active Mod") style "l_alternate" 200 | 201 | grid 1 1: 202 | xfill True 203 | spacing HALF_INDENT 204 | 205 | vbox: 206 | 207 | label _("Mod Options") style "l_label_small" 208 | 209 | frame style "l_indent": 210 | has vbox 211 | 212 | textbutton _("Delete 'scripts.rpa'") action Jump("scripts_rpa") 213 | textbutton _("Delete 'images.rpa'") action Jump("images_rpa") 214 | textbutton _("Delete Saves") action Jump("rmpersistent") 215 | 216 | add SPACER 217 | 218 | label _("DDML Options") style "l_label_small" 219 | 220 | grid 1 1: 221 | xfill True 222 | spacing HALF_INDENT 223 | 224 | frame style "l_indent": 225 | has vbox 226 | 227 | if persistent.projects_directory: 228 | textbutton _("Browse Mod Directory") action OpenDirectory(persistent.projects_directory) 229 | textbutton _("Browse Game Directory") action OpenDirectory("game") 230 | if renpy.windows: 231 | textbutton _("Browse Save Directory") action OpenDirectory(os.getenv('APPDATA') + '/RenPy') 232 | elif renpy.macintosh: 233 | textbutton _("Browse Save Directory") action OpenDirectory(os.environ['HOME'] +"/Library/RenPy") 234 | else: 235 | textbutton _("Browse Save Directory") action OpenDirectory(os.environ['HOME'] + "/.renpy") 236 | textbutton _("Delete Mod") action Jump("delete_mod_folder") 237 | 238 | # textbutton "Relaunch" action Relaunch 239 | 240 | label main_menu: 241 | return 242 | 243 | label start: 244 | show screen bottom_info 245 | $ dmgcheck() 246 | 247 | label front_page: 248 | call screen front_page 249 | jump front_page 250 | 251 | label scripts_rpa: 252 | 253 | python: 254 | interface.processing(_("Deleting scripts.rpa...")) 255 | try: 256 | modman.delete_rpa(os.path.join(persistent.projects_directory, project.current.name), "scripts.rpa") 257 | interface.info(_("DDML successfully deleted scripts.rpa from [project.current.name] without errors."),) 258 | except: 259 | interface.error(_("DDML was unable to delete images.rpa as it is missing, in-use or already deleted from [project.current.name]."), 260 | _("Verify if the file was deleted in the game folder. If it still exists, try again or contact the developer on Github if the issue persists."),) 261 | jump front_page 262 | 263 | label images_rpa: 264 | 265 | python: 266 | interface.processing(_("Deleting images.rpa...")) 267 | try: 268 | modman.delete_rpa(os.path.join(persistent.projects_directory, project.current.name), "images.rpa") 269 | interface.info(_("DDML successfully deleted images.rpa from [project.current.name] without errors."),) 270 | except: 271 | interface.error(_("DDML was unable to delete images.rpa as it is missing, in-use or already deleted from [project.current.name]."), 272 | _("Verify if the file was deleted in the game folder. If it still exists, try again or contact the developer on Github if the issue persists."),) 273 | 274 | jump front_page 275 | 276 | label lint: 277 | python hide: 278 | 279 | interface.processing(_("Checking script for potential problems...")) 280 | lint_fn = project.current.temp_filename("lint.txt") 281 | 282 | project.current.launch([ 'lint', lint_fn ], wait=True) 283 | 284 | e = renpy.editor.editor 285 | e.begin(True) 286 | e.open(lint_fn) 287 | e.end() 288 | 289 | jump front_page 290 | 291 | label rmpersistent: 292 | 293 | python hide: 294 | interface.processing(_("Deleting save data...")) 295 | project.current.launch([ 'rmpersistent' ], wait=True) 296 | 297 | jump front_page 298 | 299 | label force_recompile: 300 | 301 | python hide: 302 | interface.processing(_("Recompiling all rpy files into rpyc files...")) 303 | project.current.launch([ 'compile' ], wait=True) 304 | 305 | jump front_page 306 | 307 | # Code to delete a mod from the mod folder 308 | label delete_mod_folder: 309 | 310 | python hide: 311 | while True: 312 | mod_delete_response = interface.input( 313 | _("Deleting a Mod"), 314 | _("Are you sure you want to delete [project.current.name]? Type either Yes or No."), 315 | filename=False, 316 | cancel=Jump("front_page")) 317 | mod_delete_response = mod_delete_response.strip() 318 | 319 | if not mod_delete_response or mod_delete_response.lower() == "no": 320 | interface.error(_("The operation has been cancelled.")) 321 | return 322 | 323 | elif mod_delete_response.lower() == "yes": 324 | 325 | interface.processing(_("Deleting [project.current.name]...")) 326 | 327 | with interface.error_handling(_("deleting mod.")): 328 | modman.delete_mod(persistent.projects_directory, project.current.name) 329 | 330 | interface.info("[project.current.name] has been deleted from the mod folder.") 331 | else: 332 | interface.error(_("Invalid Input. Expected either a Yes or No response.")) 333 | continue 334 | 335 | project.manager.scan() 336 | break 337 | 338 | jump front_page 339 | 340 | # Code to move mod folder 341 | label move_mod_folder: 342 | 343 | python hide: 344 | 345 | interface.interaction(_("New Mod Directory"), _("Please choose the new mod folder using the directory chooser.\n{b}The directory chooser may have opened behind this window.{/b}"), _("DDML will create new mods in this folder, and place your existing mods in here."),) 346 | 347 | path, is_default = choose_directory(None) 348 | 349 | if is_default: 350 | interface.error(_("The operation has been cancelled.")) 351 | renpy.jump("front_page") 352 | 353 | if path == persistent.projects_directory: 354 | inteface.error(_("The new mod folder path is the same as the current mod path.")) 355 | return 356 | 357 | # Moves Mods from old folder to new folder 358 | mm_compat.ddmm_traceback_start() 359 | interface.processing(_("Moving your existing mods to the new mod folder. Please wait...")) 360 | try: 361 | modman.move_mod_folder(persistent.projects_directory, path) 362 | persistent.projects_directory = path 363 | interface.info(_("DDML transferred all your mods to the new mod folder with no errors."),) 364 | except: 365 | mm_compat.ddmm_traceback("new mod folder") 366 | modman.move_mod_folder(path, persistent.projects_directory) 367 | interface.error(_("DDML was unable to move your mods to the new mod folder due to a error."), 368 | _("See {i}transfer_log.txt{/i} for more information. If the issue persists, contact the developer on Github.")) 369 | 370 | project.manager.scan() 371 | mm_compat.ddmm_traceback_shutdown() 372 | 373 | return -------------------------------------------------------------------------------- /launcher/game/gui7/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 gui7.code import CodeGenerator, translate_define, translate_copy, translate_code 23 | from gui7.images import ImageGenerator 24 | from gui7.parameters import GuiParameters 25 | 26 | import renpy.arguments 27 | import os 28 | 29 | 30 | def generate_gui(p): 31 | 32 | ImageGenerator(p).generate_all() 33 | CodeGenerator(p).generate_gui("gui.rpy", defines=True) 34 | CodeGenerator(p).generate_gui("screens.rpy") 35 | CodeGenerator(p).generate_code("options.rpy") 36 | CodeGenerator(p).copy_script("script.rpy") 37 | CodeGenerator(p).copy_files() 38 | 39 | for dn in [ "images", "audio" ]: 40 | 41 | fulldn = os.path.join(p.prefix, dn) 42 | 43 | if not os.path.exists(fulldn): 44 | os.mkdir(fulldn) 45 | 46 | 47 | def generate_gui_command(): 48 | 49 | ap = renpy.arguments.ArgumentParser() 50 | 51 | ap.add_argument("target", action="store", help="The game into which the generated gui should be placed.") 52 | ap.add_argument("--width", default=1280, action="store", type=int, help="The width of the generated gui.") 53 | ap.add_argument("--height", default=720, action="store", type=int, help="The height of the generated gui.") 54 | ap.add_argument("--accent", default="#00b8c3", action="store", help="The accent color used throughout the gui.") 55 | ap.add_argument("--boring", default="#000000", action="store", help="The boring color used for the gui background.") 56 | ap.add_argument("--light", default=False, action="store_true", help="True if this is considered a light theme.") 57 | ap.add_argument("--template", default="gui", action="store", help="The template directory containing source code.") 58 | ap.add_argument("--language", default=None, action="store", help="The language to translate strings and comments to.") 59 | 60 | ap.add_argument("--start", default=False, action="store_true", help="Starts a new project, replacing images and code.") 61 | ap.add_argument("--replace-images", default=False, action="store_true", help="True if existing images should be overwritten.") 62 | ap.add_argument("--replace-code", default=False, action="store_true", help="True if an existing gui.rpy file should be overwritten.") 63 | ap.add_argument("--update-code", default=False, action="store_true", help="True if an existing gui.rpy file should be update.") 64 | 65 | args = ap.parse_args() 66 | 67 | if args.start: 68 | args.replace_images = True 69 | args.replace_code = True 70 | args.update_code = True 71 | 72 | prefix = os.path.join(args.target, "game") 73 | 74 | if not os.path.isdir(prefix): 75 | ap.error("{} does not appear to be a Ren'Py game.".format(prefix)) 76 | 77 | template = os.path.join(args.template, "game") 78 | 79 | if not os.path.isdir(template): 80 | ap.error("{} does not appear to be a Ren'Py game.".format(template)) 81 | 82 | p = GuiParameters( 83 | prefix, 84 | template, 85 | args.width, 86 | args.height, 87 | args.accent, 88 | args.boring, 89 | args.light, 90 | args.language, 91 | args.replace_images, 92 | args.replace_code, 93 | args.update_code, 94 | os.path.basename(args.target), 95 | ) 96 | 97 | generate_gui(p) 98 | 99 | renpy.arguments.register_command("generate_gui", generate_gui_command) 100 | -------------------------------------------------------------------------------- /launcher/game/gui7/__init__.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/gui7/__init__.pyo -------------------------------------------------------------------------------- /launcher/game/gui7/code.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 unicode_literals 23 | 24 | import os 25 | import codecs 26 | import re 27 | import math 28 | import textwrap 29 | import collections 30 | import shutil 31 | 32 | import renpy 33 | 34 | 35 | class Define(object): 36 | 37 | def __init__(self, name, value, comment): 38 | self.name = name 39 | self.value = value 40 | self.comment = comment 41 | 42 | 43 | # A map from language name to a list of defines. 44 | language_defines = collections.defaultdict(list) 45 | 46 | 47 | def translate_define(language, name, value, comment=None): 48 | """ 49 | This function should be called to register the value of a define that is 50 | set when generating code in `language`. 51 | 52 | `name` 53 | A string giving the name of the define. 54 | 55 | `value` 56 | A string giving the value of the define. Be sure to quote it properly, 57 | or use repr(). 58 | 59 | `comment` 60 | If not None, a comment that will be generated before the define. The 61 | comment will only be generated if the define does not exist in 62 | gui.rpy. There is no need to use "## ", as the comment will be 63 | added and wrapped automatically. 64 | """ 65 | 66 | language_defines[language].append(Define(name, value, comment)) 67 | 68 | 69 | # A map from a language name to a list of (src, dst) pairs. Each represents a 70 | # file that is copied into place. 71 | language_copies = collections.defaultdict(list) 72 | 73 | 74 | def translate_copy(language, src, dst): 75 | """ 76 | This function should be called to copy a file from `src` to `dst` 77 | when generating code in `language`. 78 | 79 | `src` 80 | A path, relative to the launcher game directory. 81 | 82 | `dst` 83 | A path, relative to the game directory of the new game. 84 | """ 85 | 86 | language_copies[language].append((src, dst)) 87 | 88 | 89 | # A map from language name and filename to code that should be added to the 90 | # end of a newly-generated file. 91 | language_code = collections.defaultdict(list) 92 | 93 | 94 | def translate_code(language, filename, code): 95 | """ 96 | This function can be called to include a block of code verbatim 97 | into `file` when a game is generated in `language`. 98 | """ 99 | 100 | language_code[language, filename].extend([''] + code.split("\n")) 101 | 102 | 103 | class CodeGenerator(object): 104 | """ 105 | This is used to generate and update the GUI code. 106 | """ 107 | 108 | def __init__(self, parameters): 109 | """ 110 | Generates or updates gui.rpy. 111 | """ 112 | 113 | self.p = parameters 114 | 115 | def load_template(self, filename): 116 | 117 | target = os.path.join(self.p.prefix, filename) 118 | 119 | if os.path.exists(target) and not self.p.replace_code: 120 | template = target 121 | else: 122 | template = os.path.join(self.p.template, filename) 123 | 124 | with codecs.open(template, "r", "utf-8") as f: 125 | self.lines = [ i.rstrip().replace(u"\ufeff", "") for i in f ] 126 | 127 | def remove_scale(self): 128 | 129 | def scale(m): 130 | original = int(m.group(1)) 131 | scaled = int(math.ceil(original * self.p.scale)) 132 | return str(scaled) 133 | 134 | lines = [ ] 135 | 136 | for l in self.lines: 137 | l = re.sub(r'gui.scale\((.*?)\)', scale, l) 138 | lines.append(l) 139 | 140 | self.lines = lines 141 | 142 | def update_size(self): 143 | 144 | gui_init = "gui.init({}, {})".format(self.p.width, self.p.height) 145 | 146 | lines = [ ] 147 | 148 | for l in self.lines: 149 | l = re.sub(r'gui.init\(.*?\)', gui_init, l) 150 | lines.append(l) 151 | 152 | self.lines = lines 153 | 154 | def update_defines(self, replacements, additions=[]): 155 | """ 156 | Replaces define statements in gui.rpy. 157 | """ 158 | 159 | replacements = dict(replacements) 160 | 161 | for d in additions: 162 | replacements[d.name] = d.value 163 | 164 | seen = set() 165 | 166 | lines = [ ] 167 | 168 | for l in self.lines: 169 | 170 | m = re.match('^(\s*)define (.*?) =', l) 171 | 172 | if m: 173 | indent = m.group(1) 174 | variable = m.group(2) 175 | 176 | if variable in replacements: 177 | l = "{}define {} = {}".format(indent, variable, replacements[variable]) 178 | 179 | seen.add(variable) 180 | 181 | lines.append(l) 182 | 183 | for d in additions: 184 | 185 | if d.name in seen: 186 | continue 187 | 188 | seen.add(d.name) 189 | 190 | lines.append("") 191 | 192 | if d.comment: 193 | for s in textwrap.wrap(d.comment): 194 | lines.append("## " + s) 195 | 196 | lines.append("define {} = {}".format(d.name, d.value)) 197 | 198 | self.lines = lines 199 | 200 | def update_gui_defines(self): 201 | """ 202 | Replaces define statements in gui.rpy. 203 | """ 204 | 205 | replacements = { 206 | 'gui.accent_color' : repr(self.p.accent_color.hexcode), 207 | 'gui.selected_color' : repr(self.p.selected_color.hexcode), 208 | 'gui.hover_color' : repr(self.p.hover_color.hexcode), 209 | 'gui.muted_color' : repr(self.p.muted_color.hexcode), 210 | 'gui.hover_muted_color' : repr(self.p.hover_muted_color.hexcode), 211 | 'gui.title_color' : repr(self.p.title_color.hexcode), 212 | 'gui.idle_color' : repr(self.p.idle_color.hexcode), 213 | 'gui.idle_small_color' : repr(self.p.idle_small_color.hexcode), 214 | 'gui.insensitive_color' : repr(self.p.insensitive_color.hexcode), 215 | 'gui.text_color' : repr(self.p.text_color.hexcode), 216 | 'gui.interface_text_color' : repr(self.p.text_color.hexcode), 217 | 'gui.choice_text_color' : repr(self.p.choice_color.hexcode), 218 | } 219 | 220 | self.update_defines(replacements, language_defines[self.p.language]) 221 | 222 | def update_options_defines(self): 223 | """ 224 | Replaces define statements in options.rpy. 225 | """ 226 | 227 | def quote(s): 228 | s = s.replace("\\", "\\\\") 229 | s = s.replace("\"", "\\\"") 230 | return '"' + s + '"' 231 | 232 | replacements = { 233 | 'config.name' : "_({})".format(quote(self.p.name)), 234 | 'build.name' : quote(self.p.simple_name), 235 | 'config.save_directory' : quote(self.p.savedir), 236 | } 237 | 238 | self.update_defines(replacements) 239 | 240 | def write_target(self, filename): 241 | 242 | target = os.path.join(self.p.prefix, filename) 243 | 244 | if os.path.exists(target): 245 | 246 | backup = 1 247 | 248 | while True: 249 | 250 | bfn = "{}.{}.bak".format(target, backup) 251 | 252 | if not os.path.exists(bfn): 253 | break 254 | 255 | backup += 1 256 | 257 | if not self.p.skip_backup: 258 | os.rename(target, bfn) 259 | 260 | with codecs.open(target, "w", "utf-8") as f: 261 | f.write(u"\ufeff") 262 | 263 | for l in self.lines: 264 | f.write(l + "\r\n") 265 | 266 | def translate_strings(self): 267 | 268 | def replace(m): 269 | s = eval(m.group(1)) 270 | s = renpy.translation.translate_string(s, language=self.p.language) 271 | s = renpy.translation.quote_unicode(s) 272 | 273 | quote = m.group(1)[0] 274 | 275 | s = u"_({}{}{})".format(quote, s, quote) 276 | 277 | return s 278 | 279 | lines = [ ] 280 | 281 | for l in self.lines: 282 | 283 | l = re.sub(r'_\((\".*?\")\)', replace, l) 284 | l = re.sub(r'_\((\'.*?\')\)', replace, l) 285 | 286 | lines.append(l) 287 | 288 | self.lines = lines 289 | 290 | def translate_comments(self): 291 | 292 | lines = [ ] 293 | 294 | comment = [ ] 295 | indent = "" 296 | 297 | for l in self.lines: 298 | 299 | m = re.match(r'^(\s*## )(.*)', l.rstrip()) 300 | 301 | if m: 302 | 303 | indent = m.group(1) 304 | c = m.group(2) 305 | 306 | if comment: 307 | c = c.strip() 308 | 309 | comment.append(c) 310 | 311 | else: 312 | 313 | if comment: 314 | s = "## " + ' '.join(comment) 315 | 316 | if s.endswith("#"): 317 | hashpad = True 318 | s = s.rstrip('# ') 319 | else: 320 | hashpad = False 321 | 322 | s = renpy.translation.translate_string(s, language=self.p.language) 323 | 324 | m = re.match(r'## ?([ *]*)(.*)', s) 325 | 326 | prefix = m.group(1) 327 | empty = ' ' * len(prefix) 328 | rest = m.group(2) 329 | 330 | len_prefix = len(indent) + len(prefix) 331 | len_wrap = 80 - len_prefix 332 | 333 | import store.gui 334 | 335 | for i, s in enumerate(renpy.text.extras.textwrap(rest, len_wrap, store.gui.asian)): 336 | 337 | if i == 0: 338 | s = indent + prefix + s 339 | else: 340 | s = indent + empty + s 341 | 342 | if hashpad and len(s) < 79: 343 | s = s + ' ' + "#" * (79 - len(s)) 344 | 345 | lines.append(s) 346 | 347 | comment = [ ] 348 | 349 | lines.append(l) 350 | 351 | self.lines = lines 352 | 353 | def copy_files(self): 354 | 355 | for src, dst in language_copies[self.p.language]: 356 | src = os.path.join(renpy.config.gamedir, src) 357 | dst = os.path.join(self.p.prefix, dst) 358 | 359 | if os.path.exists(dst): 360 | continue 361 | 362 | dstdir = os.path.dirname(dst) 363 | 364 | if not os.path.exists(dstdir): 365 | os.makedirs(dstdir, 0o777) 366 | 367 | shutil.copy(src, dst) 368 | 369 | def copy_script(self, name): 370 | dst = os.path.join(self.p.prefix, name) 371 | 372 | if os.path.exists(dst): 373 | return 374 | 375 | language = renpy.store._preferences.language # @UndefinedVariable 376 | 377 | if language is None: 378 | language = "None" 379 | 380 | src = os.path.join(renpy.config.gamedir, "tl", language, name + "m") 381 | 382 | if not os.path.exists(src): 383 | src = os.path.join(self.p.template, name) 384 | 385 | self.load_template(src) 386 | self.remove_scale() 387 | self.write_target(dst) 388 | 389 | def add_code(self, fn): 390 | 391 | if not self.p.replace_code: 392 | return 393 | 394 | self.lines.extend(language_code[self.p.language, fn]) 395 | 396 | def generate_gui(self, fn, defines=False): 397 | if not self.p.update_code: 398 | return 399 | 400 | self.load_template(fn) 401 | 402 | if defines: 403 | self.update_gui_defines() 404 | 405 | if self.p.replace_code: 406 | self.remove_scale() 407 | self.update_size() 408 | self.translate_strings() 409 | self.translate_comments() 410 | self.add_code(fn) 411 | 412 | self.write_target(fn) 413 | 414 | def generate_code(self, fn): 415 | 416 | target = os.path.join(self.p.prefix, fn) 417 | 418 | if os.path.exists(target): 419 | return 420 | 421 | self.load_template(fn) 422 | 423 | self.translate_strings() 424 | self.translate_comments() 425 | self.update_options_defines() 426 | 427 | self.add_code(fn) 428 | 429 | self.write_target(fn) 430 | -------------------------------------------------------------------------------- /launcher/game/gui7/code.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/gui7/code.pyo -------------------------------------------------------------------------------- /launcher/game/gui7/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/gui7/icon.png -------------------------------------------------------------------------------- /launcher/game/gui7/images.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 | import pygame_sdl2 23 | import os 24 | 25 | from renpy.store import config 26 | import renpy.display 27 | 28 | 29 | class ImageGenerator(object): 30 | 31 | def __init__(self, parameters): 32 | 33 | pygame_sdl2.image.init() 34 | 35 | self.p = p = parameters 36 | 37 | self.width = p.width 38 | self.height = p.height 39 | 40 | self.scale = p.scale 41 | 42 | self.accent_color = p.accent_color 43 | self.boring_color = p.boring_color 44 | 45 | self.hover_color = p.hover_color 46 | self.muted_color = p.muted_color 47 | self.hover_muted_color = p.hover_muted_color 48 | 49 | self.menu_color = p.menu_color 50 | 51 | self.prefix = os.path.join(p.prefix, "gui", "") 52 | 53 | try: 54 | os.mkdir(self.prefix, 0o777) 55 | except: 56 | pass 57 | 58 | self.full_width = self.width / self.scale 59 | self.full_height = self.height / self.scale 60 | 61 | self.overwrite = p.replace_images 62 | 63 | def scale_int(self, n): 64 | rv = int(n * self.scale) 65 | 66 | if rv < 1: 67 | rv = 1 68 | 69 | return rv 70 | 71 | def rescale_template(self, t): 72 | 73 | rv = [ ] 74 | 75 | for pos, opacity in t: 76 | rv.append((pos * self.scale, opacity)) 77 | 78 | return rv 79 | 80 | def generate_line(self, template): 81 | 82 | size = int(max(i[0] for i in template)) 83 | 84 | rv = [ ] 85 | 86 | right_pos, right_value = template[0] 87 | 88 | for i in range(size): 89 | 90 | if i == right_pos: 91 | rv.append(right_value) 92 | continue 93 | 94 | while i >= right_pos: 95 | left_pos = right_pos 96 | left_value = right_value 97 | 98 | right_pos, right_value = template.pop(0) 99 | 100 | done = 1.0 * (i - left_pos) / (right_pos - left_pos) 101 | rv.append(left_value + done * (right_value - left_value)) 102 | 103 | return rv 104 | 105 | def crop_line(self, line, size): 106 | """ 107 | Crops the center `size` pixels out of `line`. 108 | """ 109 | 110 | if len(line) <= size: 111 | return line 112 | 113 | start = (len(line) - size) // 2 114 | 115 | return line[start:start + size ] 116 | 117 | def save(self, s, filename, overwrite=True): 118 | 119 | fn = self.prefix + filename + ".png" 120 | dn = os.path.dirname(fn) 121 | 122 | try: 123 | os.makedirs(dn, 0o777) 124 | except: 125 | pass 126 | 127 | if os.path.exists(fn): 128 | 129 | if not overwrite: 130 | return 131 | 132 | if not self.overwrite: 133 | return 134 | 135 | index = 1 136 | 137 | while True: 138 | bfn = u"{}.{}.bak".format(fn, index) 139 | 140 | if not os.path.exists(bfn): 141 | break 142 | 143 | index += 1 144 | 145 | if not self.p.skip_backup: 146 | os.rename(fn, bfn) 147 | 148 | import cStringIO 149 | sio = cStringIO.StringIO() 150 | renpy.display.module.save_png(s, sio, 3) 151 | 152 | with open(fn, "wb") as f: 153 | f.write(sio.getvalue()) 154 | 155 | def make_surface(self, width, height): 156 | return pygame_sdl2.Surface((width, height), pygame_sdl2.SRCALPHA) 157 | 158 | def generate_image(self, filename, xtmpl, ytmpl, color=(0, 0, 0, 255)): 159 | 160 | r, g, b, a = color 161 | 162 | xtmpl = self.rescale_template(xtmpl) 163 | ytmpl = self.rescale_template(ytmpl) 164 | 165 | xline = self.generate_line(xtmpl) 166 | yline = self.generate_line(ytmpl) 167 | 168 | xline = self.crop_line(xline, self.width) 169 | yline = self.crop_line(yline, self.height) 170 | 171 | s = self.make_surface(len(xline), len(yline)) 172 | 173 | for x, xv in enumerate(xline): 174 | for y, yv in enumerate(yline): 175 | v = xv * yv 176 | s.set_at((x, y), (r, g, b, int(a * v))) 177 | 178 | self.save(s, filename) 179 | 180 | def generate_textbox(self): 181 | 182 | XSIZE = self.full_width 183 | XINSIDE = (XSIZE - 744) // 2 184 | 185 | YSIZE = 185 186 | YBORDER = 5 187 | 188 | X = [ 189 | (0, 0.0), 190 | (XINSIDE, 1.0), 191 | (XSIZE - XINSIDE, 1.0), 192 | (XSIZE, 0.0), 193 | ] 194 | 195 | Y = [ 196 | (0, 0.0), 197 | (YBORDER, 1.0), 198 | (YSIZE, 1.0), 199 | ] 200 | 201 | self.generate_image("textbox", X, Y, self.boring_color.opacity(.8)) 202 | 203 | YSIZE = 240 204 | YBORDER = 5 205 | 206 | X = [ (0, 1.0), (self.full_width, 1.0) ] 207 | 208 | Y = [ 209 | (0, 0.0), 210 | (YBORDER, 1.0), 211 | (YSIZE, 1.0), 212 | ] 213 | 214 | self.generate_image("phone/textbox", X, Y, self.boring_color.opacity(.8)) 215 | 216 | width = self.scale_int(300) 217 | height = self.scale_int(36) 218 | 219 | s = self.make_surface(width, height) 220 | self.save(s, "namebox") 221 | 222 | def generate_nvl(self): 223 | XSIZE = self.full_width 224 | XINSIDE = (XSIZE - 800) // 2 225 | 226 | YSIZE = self.full_height 227 | 228 | X = [ 229 | (0, 0.0), 230 | (XINSIDE, 1.0), 231 | (XSIZE - XINSIDE, 1.0), 232 | (XSIZE, 0.0), 233 | ] 234 | 235 | Y = [ 236 | (0, 1.0), 237 | (YSIZE, 1.0), 238 | ] 239 | 240 | self.generate_image("nvl", X, Y, self.boring_color.opacity(.8)) 241 | 242 | X = [ 243 | (0, 1.0), 244 | (XSIZE, 1.0), 245 | ] 246 | 247 | Y = [ 248 | (0, 1.0), 249 | (YSIZE, 1.0), 250 | ] 251 | 252 | self.generate_image("phone/nvl", X, Y, self.boring_color.opacity(.8)) 253 | 254 | def generate_choice_button(self): 255 | XSIZE = 790 256 | XINSIDE = 100 257 | 258 | YSIZE = 35 259 | YBORDER = 3 260 | 261 | X = [ 262 | (0, 0.0), 263 | (XINSIDE, 1.0), 264 | (XSIZE - XINSIDE, 1.0), 265 | (XSIZE, 0.0), 266 | ] 267 | 268 | Y = [ 269 | (0, 0.0), 270 | (YBORDER, 1.0), 271 | (YSIZE - YBORDER, 1.0), 272 | (YSIZE, 0.0), 273 | ] 274 | 275 | self.generate_image("button/choice_idle_background", X, Y, self.boring_color.opacity(.8)) 276 | self.generate_image("button/choice_hover_background", X, Y, self.accent_color.opacity(.95)) 277 | self.generate_image("phone/button/choice_idle_background", X, Y, self.boring_color.opacity(.8)) 278 | self.generate_image("phone/button/choice_hover_background", X, Y, self.accent_color.opacity(.95)) 279 | 280 | def generate_overlay(self): 281 | 282 | width = self.scale_int(280) 283 | phone_width = self.scale_int(340) 284 | 285 | game_y = self.scale_int(120) 286 | game_height = self.scale_int(570) 287 | 288 | line_width = self.scale_int(3) 289 | 290 | if self.p.light: 291 | opacity = 0.9 292 | else: 293 | opacity = 0.8 294 | 295 | # Main menu. 296 | mm = self.make_surface(self.width, self.height) 297 | mm.subsurface((0, 0, width, self.height)).fill(self.boring_color.opacity(opacity)) 298 | mm.subsurface((width - line_width, 0, line_width, self.height)).fill(self.accent_color) 299 | self.save(mm, "overlay/main_menu") 300 | 301 | mm = self.make_surface(self.width, self.height) 302 | mm.subsurface((0, 0, phone_width, self.height)).fill(self.boring_color.opacity(opacity)) 303 | mm.subsurface((phone_width - line_width, 0, line_width, self.height)).fill(self.accent_color) 304 | self.save(mm, "phone/overlay/main_menu") 305 | 306 | # Game menu. 307 | gm = self.make_surface(self.width, self.height) 308 | gm.fill(self.boring_color.opacity(opacity)) 309 | gm.subsurface((width - line_width, game_y, line_width, game_height)).fill(self.accent_color) 310 | self.save(gm, "overlay/game_menu") 311 | 312 | gm = self.make_surface(self.width, self.height) 313 | gm.fill(self.boring_color.opacity(opacity)) 314 | gm.subsurface((phone_width - line_width, game_y, line_width, game_height)).fill(self.accent_color) 315 | self.save(gm, "phone/overlay/game_menu") 316 | 317 | # Confirm. 318 | gm = self.make_surface(self.width, self.height) 319 | gm.fill(self.boring_color.opacity(.6)) 320 | self.save(gm, "overlay/confirm") 321 | 322 | def generate_frame(self): 323 | width = self.scale_int(600) 324 | height = self.scale_int(250) 325 | 326 | border = self.scale_int(3) 327 | 328 | s = self.make_surface(width, height) 329 | s.fill(self.accent_color) 330 | s.subsurface((border, border, width - 2 * border, height - 2 * border)).fill(self.boring_color) 331 | self.save(s, "frame") 332 | 333 | def generate_quick_buttons(self): 334 | width = self.scale_int(100) 335 | height = self.scale_int(30) 336 | 337 | s = self.make_surface(width, height) 338 | self.save(s, "button/quick_idle_background") 339 | self.save(s, "button/quick_hover_background") 340 | 341 | def generate_skip(self): 342 | XSIZE = 240 343 | XRIGHT = 50 344 | 345 | YSIZE = 43 346 | 347 | X = [ 348 | (0, 1.0), 349 | (XSIZE - XRIGHT, 1.0), 350 | (XSIZE, 0.0), 351 | ] 352 | 353 | Y = [ 354 | (0, 1.0), 355 | (YSIZE, 1.0), 356 | ] 357 | 358 | self.generate_image("skip", X, Y, self.boring_color.opacity(.8)) 359 | 360 | def generate_notify(self): 361 | XSIZE = 922 362 | XRIGHT = 50 363 | 364 | YSIZE = 43 365 | 366 | X = [ 367 | (0, 1.0), 368 | (XSIZE - XRIGHT, 1.0), 369 | (XSIZE, 0.0), 370 | ] 371 | 372 | Y = [ 373 | (0, 1.0), 374 | (YSIZE, 1.0), 375 | ] 376 | 377 | self.generate_image("notify", X, Y, self.boring_color.opacity(.8)) 378 | 379 | def generate_icon(self): 380 | 381 | icon_fn = os.path.join(config.renpy_base, "launcher", "game", "gui7", "icon.png") 382 | icon = pygame_sdl2.image.load(icon_fn) 383 | 384 | width, height = icon.get_size() 385 | surf = pygame_sdl2.Surface((width, height), pygame_sdl2.SRCALPHA) 386 | 387 | ro, go, bo, _ao = tuple(self.accent_color) 388 | 389 | ro -= 23 390 | go -= 23 391 | bo -= 23 392 | 393 | for y in range(height): 394 | for x in range(width): 395 | r, g, b, a = icon.get_at((x, y)) 396 | 397 | r = max(0, min(r + ro, 255)) 398 | g = max(0, min(g + go, 255)) 399 | b = max(0, min(b + bo, 255)) 400 | 401 | surf.set_at((x, y), (r, g, b, a)) 402 | 403 | self.save(surf, "window_icon", overwrite=False) 404 | 405 | def generate_menus(self): 406 | s = self.make_surface(self.width, self.height) 407 | s.fill(self.menu_color) 408 | 409 | self.save(s, "main_menu", overwrite=False) 410 | self.save(s, "game_menu", overwrite=False) 411 | 412 | def generate_all(self): 413 | self.generate_textbox() 414 | self.generate_choice_button() 415 | self.generate_overlay() 416 | self.generate_frame() 417 | self.generate_nvl() 418 | self.generate_quick_buttons() 419 | self.generate_skip() 420 | self.generate_notify() 421 | self.generate_menus() 422 | self.generate_icon() 423 | 424 | 425 | if __name__ == "__main__": 426 | import argparse 427 | 428 | ap = argparse.ArgumentParser() 429 | 430 | ap.add_argument("prefix") 431 | ap.add_argument("width", type=int) 432 | ap.add_argument("height", type=int) 433 | 434 | args = ap.parse_args() 435 | 436 | ImageGenerator(args.prefix, args.width, args.height).generate_all() 437 | -------------------------------------------------------------------------------- /launcher/game/gui7/images.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/gui7/images.pyo -------------------------------------------------------------------------------- /launcher/game/gui7/parameters.py: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 renpy.store import Color 23 | import time 24 | 25 | # The target width used in templates. 26 | WIDTH = 1280 27 | HEIGHT = 720 28 | 29 | 30 | class GuiParameters(object): 31 | """ 32 | This represents the parameters to the gui. This is used to initialize 33 | the ImageGenerator and CodeGenerator objects to a consistent set of 34 | parameters. 35 | """ 36 | 37 | def __init__(self, prefix, template, width, height, accent, boring, light, language, replace_images, replace_code, update_code, name=None): 38 | 39 | self.prefix = prefix 40 | self.template = template 41 | 42 | self.width = width 43 | self.height = height 44 | 45 | self.scale = min(1.0 * width / WIDTH, 1.0 * height / HEIGHT) 46 | 47 | self.accent_color = Color(accent) 48 | self.boring_color = Color(boring) 49 | 50 | # tint = n * color + (1-n) * white 51 | # shade = n * color + (1-n) * black 52 | 53 | self.light = light 54 | 55 | if light: 56 | self.hover_color = self.accent_color # .tint(.95) 57 | self.muted_color = self.accent_color.tint(.6) 58 | self.hover_muted_color = self.accent_color.tint(.4) 59 | else: 60 | self.hover_color = self.accent_color.tint(.6) 61 | self.muted_color = self.accent_color.shade(.4) 62 | self.hover_muted_color = self.accent_color.shade(.6) 63 | 64 | self.menu_color = self.accent_color.replace_hsv_saturation(.25).replace_value(.5) 65 | self.title_color = self.accent_color.replace_hsv_saturation(.5).replace_value(1.0) 66 | 67 | if light: 68 | 69 | self.selected_color = Color("#555555") 70 | self.idle_color = Color("#aaaaaa") 71 | self.idle_small_color = Color("#888888") 72 | self.text_color = Color("#404040") 73 | self.choice_color = Color("#cccccc") 74 | 75 | else: 76 | 77 | self.selected_color = Color("#ffffff") 78 | self.idle_color = Color("#888888") 79 | self.idle_small_color = Color("#aaaaaa") 80 | self.text_color = Color("#ffffff") 81 | self.choice_color = Color("#cccccc") 82 | 83 | self.insensitive_color = self.idle_color.replace_opacity(.5) 84 | 85 | self.language = language 86 | 87 | if replace_code: 88 | update_code = True 89 | 90 | self.replace_images = replace_images 91 | self.replace_code = replace_code 92 | self.update_code = update_code 93 | 94 | self.skip_backup = False 95 | 96 | name = name or '' 97 | 98 | self.name = name 99 | 100 | GOOD_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" 101 | 102 | simple_name = "".join(i for i in name if i in GOOD_CHARACTERS).encode("ascii") 103 | 104 | if not simple_name: 105 | simple_name = "game" 106 | 107 | self.simple_name = simple_name 108 | 109 | self.savedir = self.simple_name + "-" + str(int(time.time())) 110 | -------------------------------------------------------------------------------- /launcher/game/gui7/parameters.pyo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/gui7/parameters.pyo -------------------------------------------------------------------------------- /launcher/game/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/background.png -------------------------------------------------------------------------------- /launcher/game/images/background_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/background_custom.png -------------------------------------------------------------------------------- /launcher/game/images/background_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/background_dark.png -------------------------------------------------------------------------------- /launcher/game/images/checkbox_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/checkbox_empty.png -------------------------------------------------------------------------------- /launcher/game/images/checkbox_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/checkbox_full.png -------------------------------------------------------------------------------- /launcher/game/images/custom_style.rpy: -------------------------------------------------------------------------------- 1 | 2 | default persistent.custom = False 3 | 4 | init -1 python: 5 | if persistent.custom: 6 | # The color of non-interactive text. 7 | TEXT = "#545454" 8 | 9 | # Colors for buttons in various states. 10 | IDLE = "#42637b" 11 | HOVER = "#d86b45" 12 | DISABLED = "#808080" 13 | 14 | # Colors for reversed text buttons (selected list entries). 15 | REVERSE_IDLE = "#78a5c5" 16 | REVERSE_HOVER = "#d86b45" 17 | REVERSE_TEXT = "#ffffff" 18 | 19 | # Colors for the scrollbar thumb. 20 | SCROLLBAR_IDLE = "#dfdfdf" 21 | SCROLLBAR_HOVER = "#d86b45" 22 | 23 | # An image used as a separator pattern. 24 | PATTERN = "images/pattern_custom.png" 25 | 26 | # A displayable used for the background of everything. 27 | BACKGROUND = "images/background_custom.png" 28 | 29 | # A displayable used for the background of windows 30 | # containing commands, preferences, and navigation info. 31 | WINDOW = Frame("images/window_custom.png", 0, 0, tile=True) 32 | 33 | # A displayable used for the background of the projects list. 34 | PROJECTS_WINDOW = Null() 35 | 36 | # A displayable used the background of information boxes. 37 | INFO_WINDOW = "#f9f9f9" 38 | 39 | # Colors for the titles of information boxes. 40 | ERROR_COLOR = "#d15353" 41 | INFO_COLOR = "#545454" 42 | INTERACTION_COLOR = "#d19753" 43 | QUESTION_COLOR = "#d19753" 44 | 45 | # The color of input text. 46 | INPUT_COLOR = "#d86b45" 47 | -------------------------------------------------------------------------------- /launcher/game/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/logo.png -------------------------------------------------------------------------------- /launcher/game/images/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/logo32.png -------------------------------------------------------------------------------- /launcher/game/images/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/pattern.png -------------------------------------------------------------------------------- /launcher/game/images/pattern_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/pattern_custom.png -------------------------------------------------------------------------------- /launcher/game/images/scrollbar_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/scrollbar_center.png -------------------------------------------------------------------------------- /launcher/game/images/vscrollbar_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/vscrollbar_center.png -------------------------------------------------------------------------------- /launcher/game/images/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/window.png -------------------------------------------------------------------------------- /launcher/game/images/window_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/window_custom.png -------------------------------------------------------------------------------- /launcher/game/images/window_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/game/images/window_dark.png -------------------------------------------------------------------------------- /launcher/game/modmanagement.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | 6 | class ModManagement: 7 | """ 8 | This class manages what the user can do to a mod in the mod launcher. 9 | """ 10 | 11 | def delete_mod(self, modFolder, modName): 12 | """ 13 | This define deletes a mod folder from the mod install folder if 14 | confirmed by the user. 15 | """ 16 | 17 | for mod_src, dirs, files in os.walk(os.path.join(modFolder, modName)): 18 | for f in files: 19 | os.remove(os.path.join(mod_src, f)) 20 | 21 | for d in dirs: 22 | shutil.rmtree(os.path.join(mod_src, d)) 23 | 24 | shutil.rmtree(os.path.join(modFolder, modName)) 25 | 26 | def move_mod_folder(self, modFolder, newModFolder): 27 | """ 28 | This define moves the contents of the old mod install folder to the new 29 | mod install folder. 30 | """ 31 | 32 | for mod_src, dirs, files in os.walk(modFolder): 33 | dst_dir = mod_src.replace(modFolder, newModFolder) 34 | 35 | for d in dirs: 36 | shutil.move(os.path.join(mod_src, d), os.path.join(dst_dir, d)) 37 | 38 | def delete_rpa(self, modFolder, rpaName): 39 | """ 40 | This define deletes a RPA from the mod folder if confirmed by the user. 41 | """ 42 | 43 | if sys.platform == "darwin": 44 | modFolder = os.path.join(modFolder, "DDLC.app/Contents/Resources/autorun/game") 45 | else: 46 | modFolder = os.path.join(modFolder, "game") 47 | 48 | os.remove(os.path.join(modFolder, rpaName)) 49 | -------------------------------------------------------------------------------- /launcher/game/navigation.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | init python in navigation: 23 | import store.interface as interface 24 | import store.project as project 25 | import store.editor as editor 26 | from store import persistent, Action 27 | 28 | 29 | # The last navigation screen we've seen. This is the scree we try to go 30 | # to the next time we enter navigation. (We may not be able to go there, 31 | # if the screen is empty.) 32 | if persistent.navigation is None: 33 | persistent.navigation = "label" 34 | 35 | # A map from a kind of information, to how we should sort it. Possible 36 | # sorts are alphabetical, by-file, natural. 37 | if persistent.navigation_sort is None: 38 | persistent.navigation_sort = { } 39 | 40 | if persistent.navigate_private is None: 41 | persistent.navigate_private = False 42 | 43 | if persistent.navigate_library is None: 44 | persistent.navigate_library = False 45 | 46 | # A list of kinds of navigation we support. 47 | KINDS = [ "file", "label", "define", "transform", "screen", "callable", "todo" ] 48 | 49 | # A map from kind name to adjustment. 50 | adjustments = { } 51 | 52 | for i in KINDS: 53 | persistent.navigation_sort.setdefault(i, "by-file") 54 | adjustments[i] = ui.adjustment() 55 | 56 | def group_and_sort(kind): 57 | """ 58 | This is responsible for pulling navigation information of the 59 | appropriate kind out of project.current.dump, grouping it, 60 | and sorting it. 61 | 62 | This returns a list of (group, list of (name, filename, line)). The 63 | group may be a string or None. 64 | """ 65 | 66 | project.current.update_dump() 67 | 68 | sort = persistent.navigation_sort[kind] 69 | 70 | name_map = project.current.dump.get("location", {}).get(kind, { }) 71 | 72 | groups = { } 73 | 74 | for name, loc in name_map.items(): 75 | filename, line = loc 76 | filename = filename.replace("\\", "/") 77 | 78 | if sort == "alphabetical": 79 | group = None 80 | else: 81 | group = filename 82 | if group.startswith("game/"): 83 | group = group[5:] 84 | 85 | g = groups.get(group, None) 86 | if g is None: 87 | groups[group] = g = [ ] 88 | 89 | g.append((name, filename, line)) 90 | 91 | for g in groups.values(): 92 | if sort == "natural": 93 | g.sort(key=lambda a : a[2]) 94 | else: 95 | g.sort(key=lambda a : a[0].lower()) 96 | 97 | rv = list(groups.items()) 98 | rv.sort() 99 | 100 | return rv 101 | 102 | def group_and_sort_files(): 103 | 104 | rv = [ ] 105 | 106 | for fn in project.current.script_files(): 107 | shortfn = fn 108 | shortfn = shortfn.replace("\\", "/") 109 | 110 | if shortfn.startswith("game/"): 111 | shortfn = fn[5:] 112 | 113 | rv.append((shortfn, fn, None)) 114 | 115 | rv.sort() 116 | 117 | return [ (None, rv) ] 118 | 119 | class ChangeKind(Action): 120 | """ 121 | Changes the kind of thing we're navigating over. 122 | """ 123 | 124 | def __init__(self, kind): 125 | self.kind = kind 126 | 127 | def get_selected(self): 128 | return persistent.navigation == self.kind 129 | 130 | def __call__(self): 131 | if persistent.navigation == self.kind: 132 | return 133 | 134 | persistent.navigation = self.kind 135 | renpy.jump("navigation_loop") 136 | 137 | class ChangeSort(Action): 138 | """ 139 | Changes the sort order. 140 | """ 141 | 142 | def __init__(self, sort): 143 | self.sort = sort 144 | 145 | def get_selected(self): 146 | return persistent.navigation_sort[persistent.navigation] == self.sort 147 | 148 | def __call__(self): 149 | if self.get_selected(): 150 | return 151 | 152 | persistent.navigation_sort[persistent.navigation] = self.sort 153 | renpy.jump("navigation_loop") 154 | 155 | 156 | screen navigation: 157 | 158 | frame: 159 | style_group "l" 160 | style "l_root" 161 | 162 | window: 163 | 164 | has vbox 165 | 166 | frame style "l_label": 167 | has hbox xfill True 168 | text _("Navigate: [project.current.display_name!q]") style "l_label_text" 169 | alt _("Navigate Script") 170 | 171 | frame: 172 | style "l_alternate" 173 | style_group "l_small" 174 | 175 | has hbox 176 | 177 | if persistent.navigation != "file": 178 | text _("Order: ") 179 | textbutton _("alphabetical") action navigation.ChangeSort("alphabetical") 180 | text " | " 181 | textbutton _("by-file") action navigation.ChangeSort("by-file") 182 | text " | " 183 | textbutton _("natural") action navigation.ChangeSort("natural") 184 | 185 | null width HALF_INDENT 186 | 187 | textbutton _("refresh") action Jump("navigation_refresh") 188 | 189 | 190 | add HALF_SPACER 191 | 192 | frame style "l_indent": 193 | hbox: 194 | spacing HALF_INDENT 195 | text _("Category:") 196 | alt "" 197 | 198 | textbutton _("files") action navigation.ChangeKind("file") 199 | textbutton _("labels") action navigation.ChangeKind("label") 200 | textbutton _("defines") action navigation.ChangeKind("define") 201 | textbutton _("transforms") action navigation.ChangeKind("transform") 202 | textbutton _("screens") action navigation.ChangeKind("screen") 203 | textbutton _("callables") action navigation.ChangeKind("callable") 204 | textbutton _("TODOs") action navigation.ChangeKind("todo") 205 | 206 | add SPACER 207 | add SEPARATOR 208 | 209 | frame style "l_indent_margin": 210 | 211 | if groups: 212 | 213 | viewport: 214 | mousewheel True 215 | scrollbars "vertical" 216 | yadjustment navigation.adjustments[persistent.navigation] 217 | 218 | vbox: 219 | style_group "l_navigation" 220 | 221 | for group_name, group in groups: 222 | 223 | if group_name is not None: 224 | text "[group_name!q]" 225 | 226 | if persistent.navigation == "todo": 227 | vbox: 228 | for name, filename, line in group: 229 | textbutton "[name!q]" action editor.Edit(filename, line) 230 | 231 | else: 232 | hbox: 233 | box_wrap True 234 | 235 | for name, filename, line in group: 236 | textbutton "[name!q]" action editor.Edit(filename, line) 237 | 238 | if group_name is not None: 239 | add SPACER 240 | 241 | if persistent.navigation == "file": 242 | add SPACER 243 | textbutton _("+ Add script file") action Jump("add_file") style "l_button" 244 | 245 | else: 246 | 247 | fixed: 248 | 249 | if persistent.navigation == "todo": 250 | 251 | text _("No TODO comments found.\n\nTo create one, include \"# TODO\" in your script."): 252 | text_align 0.5 253 | xalign 0.5 254 | yalign 0.5 255 | 256 | else: 257 | 258 | text _("The list of names is empty."): 259 | xalign 0.5 260 | yalign 0.5 261 | 262 | textbutton _("Return") action Jump("front_page") style "l_left_button" 263 | textbutton _("Launch Mod") action project.Launch() style "l_right_button" 264 | 265 | label navigation: 266 | label navigation_loop: 267 | 268 | python in navigation: 269 | 270 | kind = persistent.navigation 271 | 272 | if kind == "file": 273 | groups = group_and_sort_files() 274 | else: 275 | groups = group_and_sort(kind) 276 | 277 | renpy.call_screen("navigation", groups=groups) 278 | 279 | label navigation_refresh: 280 | $ project.current.update_dump(True) 281 | jump navigation_loop 282 | 283 | -------------------------------------------------------------------------------- /launcher/game/new_project.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | init python: 23 | import os 24 | import shutil 25 | from extractor import Extractor 26 | 27 | extract = Extractor() 28 | 29 | label add_a_mod: 30 | if persistent.projects_directory is None: 31 | call choose_projects_directory 32 | if persistent.projects_directory is None: 33 | $ interface.error(_("The mod folder path could not be set. Please try again.")) 34 | if renpy.macintosh: 35 | if persistent.safari is None: 36 | call auto_extract 37 | if persistent.safari is None: 38 | $ interface.error(_("Couldn't check if the OS auto-extracts ZIPs. Please try again.")) 39 | if persistent.zip_directory is None: 40 | call ddlc_location 41 | if persistent.zip_directory is None: 42 | $ interface.error(_("The DDLC path could not be set. Giving up.")) 43 | 44 | python: 45 | while True: 46 | modinstall_foldername = "" 47 | modinstall_foldername = interface.input( 48 | _("Mod Name"), 49 | _("Please type in the name of the mod that you are installing."), 50 | allow=interface.PROJECT_LETTERS, 51 | cancel=Jump("front_page"), 52 | default=modinstall_foldername, 53 | ) 54 | modinstall_foldername = modinstall_foldername.strip() 55 | 56 | if not modinstall_foldername: 57 | interface.error(_("The mod name may not be empty."), label=None) 58 | continue 59 | if modinstall_foldername == "launcher": 60 | interface.error(_("'launcher' is a reserved folder name. Please choose a different mod name."), label=None) 61 | continue 62 | 63 | project_dir = os.path.join(persistent.projects_directory, modinstall_foldername) 64 | 65 | if project.manager.get(modinstall_foldername) is not None: 66 | interface.error(_("[modinstall_foldername!q] already exists. Please choose a different mod name."), modinstall_foldername=modinstall_foldername, label=None) 67 | continue 68 | if os.path.exists(project_dir): 69 | interface.error(_("[project_dir!q] already exists. Please choose a different mod name."), project_dir=project_dir, label=None) 70 | continue 71 | 72 | interface.processing(_("Installing DDLC...")) 73 | if persistent.safari and renpy.macintosh: 74 | with interface.error_handling(_("Copying DDLC...")): 75 | extract.game_installation(persistent.zip_directory, project_dir, True) 76 | else: 77 | with interface.error_handling(_("Extracting DDLC...")): 78 | extract.game_installation(persistent.zip_directory, project_dir) 79 | 80 | if persistent.safari and renpy.macintosh: 81 | interface.interaction(_("Mod Files"), _("Please select the the mod folder you wish to install."),) 82 | 83 | path, is_default = choose_directory(None) 84 | else: 85 | interface.interaction(_("Mod Files"), _("Please select the mod ZIP file you wish to install."),) 86 | 87 | path, is_default = choose_file(None) 88 | 89 | if path is None: 90 | shutil.rmtree(project_dir) 91 | interface.error(_("The operation has been cancelled.")) 92 | renpy.jump("front_page") 93 | 94 | if not persistent.safari: 95 | interface.processing(_("Validating Mod...")) 96 | if path.endswith('.zip'): 97 | valid = extract.valid_zip(path) 98 | if not valid: 99 | shutil.rmtree(project_dir) 100 | interface.error(_("The mod ZIP you selected is not a valid DDLC mod archive.\nSelect a different mod ZIP and try again."),) 101 | renpy.jump("front_page") 102 | elif path.endswith('.rar'): 103 | shutil.rmtree(project_dir) 104 | interface.error(_("RAR files cannot be unzipped or unrarred by DDML.\nConvert the file to a ZIP file and try again."),) 105 | renpy.jump("front_page") 106 | else: 107 | shutil.rmtree(project_dir) 108 | interface.error(_("Unknown file type.\nSelect a DDLC mod ZIP file and try again."),) 109 | renpy.jump("front_page") 110 | 111 | interface.processing(_("Installing Selected Mod...")) 112 | 113 | with interface.error_handling(_("Installing Selected Mod...")): 114 | 115 | if persistent.safari and renpy.macintosh: 116 | extract.installation(path, project_dir, True) 117 | else: 118 | extract.installation(path, project_dir) 119 | 120 | interface.info(_("DDML has installed [modinstall_foldername!q] to the mod folder."), modinstall_foldername=modinstall_foldername) 121 | project.manager.scan() 122 | break 123 | 124 | jump front_page 125 | 126 | label add_base_game: 127 | if persistent.projects_directory is None: 128 | call choose_projects_directory 129 | if persistent.projects_directory is None: 130 | $ interface.error(_("The mod folder path could not be set. Please try again.")) 131 | if renpy.macintosh: 132 | if persistent.safari is None: 133 | call browser 134 | if persistent.safari is None: 135 | $ interface.error(_("Couldn't check if the OS auto-extracts ZIPs. Please try again.")) 136 | if persistent.zip_directory is None: 137 | call ddlc_location 138 | if persistent.zip_directory is None: 139 | $ interface.error(_("The DDLC path could not be set. Giving up.")) 140 | 141 | python: 142 | while True: 143 | modinstall_foldername = "" 144 | modinstall_foldername = interface.input( 145 | _("DDLC Name"), 146 | _("Please provide a name for this copy of DDLC."), 147 | allow=interface.PROJECT_LETTERS, 148 | cancel=Jump("front_page"), 149 | default=modinstall_foldername, 150 | ) 151 | modinstall_foldername = modinstall_foldername.strip() 152 | 153 | if not modinstall_foldername: 154 | interface.error(_("The mod name may not be empty."), label=None) 155 | continue 156 | if modinstall_foldername == "launcher": 157 | interface.error(_("'launcher' is a reserved folder name. Please choose a different mod name."), label=None) 158 | continue 159 | 160 | project_dir = os.path.join(persistent.projects_directory, modinstall_foldername) 161 | 162 | if project.manager.get(modinstall_foldername) is not None: 163 | interface.error(_("[modinstall_foldername!q] already exists. Please choose a different mod name."), modinstall_foldername=modinstall_foldername, label=None) 164 | continue 165 | if os.path.exists(project_dir): 166 | interface.error(_("[project_dir!q] already exists. Please choose a different mod name."), project_dir=project_dir, label=None) 167 | continue 168 | 169 | interface.processing(_("Installing DDLC...")) 170 | if persistent.safari == True and renpy.macintosh: 171 | with interface.error_handling(_("Copying DDLC...")): 172 | extract.game_installation(persistent.zip_directory, project_dir, True) 173 | else: 174 | with interface.error_handling(_("Extracting DDLC...")): 175 | extract.game_installation(persistent.zip_directory, project_dir) 176 | 177 | interface.info(_("DDML has installed DDLC to the mod folder."), modinstall_foldername=modinstall_foldername) 178 | project.manager.scan() 179 | break 180 | 181 | jump front_page 182 | -------------------------------------------------------------------------------- /launcher/game/options.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2021 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 | # This file contains some of the options that can be changed to customize 23 | # your Ren'Py game. It only contains the most common options... there 24 | # is quite a bit more customization you can do. 25 | # 26 | # Lines beginning with two '#' marks are comments, and you shouldn't 27 | # uncomment them. Lines beginning with a single '#' mark are 28 | # commented-out code, and you may want to uncomment them when 29 | # appropriate. 30 | 31 | init -1 python hide: 32 | 33 | # Should we enable the use of developer tools? This should be 34 | # set to False before the game is released, so the user can't 35 | # cheat using developer tools. 36 | 37 | config.developer = True 38 | 39 | # These control the width and height of the screen. 40 | 41 | config.screen_width = 800 42 | config.screen_height = 600 43 | 44 | # This controls the title of the window, when Ren'Py is 45 | # running in a window. 46 | 47 | config.window_title = u"Doki Doki Mod Launcher" 48 | 49 | # These control the name and version of the game, that are reported 50 | # with tracebacks and other debugging logs. 51 | config.name = "DDML" 52 | config.version = "5.3.2" 53 | 54 | ##################### 55 | # Themes 56 | 57 | # We then want to call a theme function. themes.roundrect is 58 | # a theme that features the use of rounded rectangles. It's 59 | # the only theme we currently support. 60 | # 61 | # The theme function takes a number of parameters that can 62 | # customize the color scheme. 63 | 64 | theme.roundrect( 65 | # Theme: Roundrect 66 | # Color scheme: Basic Blue 67 | 68 | # The color of an idle widget face. 69 | widget = "#003c78", 70 | 71 | # The color of a focused widget face. 72 | widget_hover = "#0050a0", 73 | 74 | # The color of the text in a widget. 75 | widget_text = "#c8ffff", 76 | 77 | # The color of the text in a selected widget. (For 78 | # example, the current value of a preference.) 79 | widget_selected = "#ffffc8", 80 | 81 | # The color of a disabled widget face. 82 | disabled = "#404040", 83 | 84 | # The color of disabled widget text. 85 | disabled_text = "#c8c8c8", 86 | 87 | # The color of informational labels. 88 | label = "#ffffff", 89 | 90 | # The color of a frame containing widgets. 91 | frame = "#6496c8", 92 | 93 | # The background of the main menu. This can be a color 94 | # beginning with '#', or an image filename. The latter 95 | # should take up the full height and width of the screen. 96 | mm_root = "#dcebff", 97 | 98 | # The background of the game menu. This can be a color 99 | # beginning with '#', or an image filename. The latter 100 | # should take up the full height and width of the screen. 101 | gm_root = "#dcebff", 102 | 103 | # If this is True, the in-game window is rounded. If False, 104 | # the in-game window is square. 105 | rounded_window = False, 106 | 107 | # And we're done with the theme. The theme will customize 108 | # various styles, so if we want to change them, we should 109 | # do so below. 110 | ) 111 | 112 | ##################### 113 | # Help. 114 | 115 | # This lets you configure the help option on the Ren'Py menus. 116 | # It may be: 117 | # - A label in the script, in which case that label is called to 118 | # show help to the user. 119 | # - A file name relative to the base directory, which is opened in a 120 | # web browser. 121 | # - None, to disable help. 122 | config.help = "README.html" 123 | 124 | ##################### 125 | # Transitions. 126 | 127 | # Used when entering the game menu from the game. 128 | config.enter_transition = None 129 | 130 | # Used when exiting the game menu to the game. 131 | config.exit_transition = None 132 | 133 | # Used between screens of the game menu. 134 | config.intra_transition = None 135 | 136 | # Used when entering the game menu from the main menu. 137 | config.main_game_transition = None 138 | 139 | # Used when returning to the main menu from the game. 140 | config.game_main_transition = None 141 | 142 | # Used when entering the main menu from the splashscreen. 143 | config.end_splash_transition = None 144 | 145 | # Used when entering the main menu after the game has ended. 146 | config.end_game_transition = None 147 | 148 | # Used when a game is loaded. 149 | config.after_load_transition = None 150 | 151 | # Used when the window is shown. 152 | config.window_show_transition = None 153 | 154 | # Used when the window is hidden. 155 | config.window_hide_transition = None 156 | 157 | 158 | ##################### 159 | # This is the name of the directory where the game's data is 160 | # stored. (It needs to be set early, before any other init code 161 | # is run, so the persistent information can be found by the init code.) 162 | python early: 163 | config.save_directory = "DDML" 164 | 165 | init -1 python hide: 166 | ##################### 167 | # Default values of Preferences. 168 | 169 | # Note: These options are only evaluated the first time a 170 | # game is run. To have them run a second time, delete 171 | # game/saves/persistent 172 | 173 | # Should we start in fullscreen mode? 174 | 175 | config.default_fullscreen = False 176 | 177 | # The default text speed in characters per second. 0 is infinite. 178 | 179 | config.default_text_cps = 0 180 | 181 | ##################### 182 | # More customizations can go here. 183 | 184 | config.sound = False 185 | config.quit_action = Quit(confirm=False) 186 | config.window_icon = "images/logo.png" 187 | config.has_autosave = False 188 | config.log_enable = False 189 | config.mouse_hide_time = None 190 | 191 | _game_menu_screen = None 192 | 193 | config.underlay = [ 194 | renpy.Keymap( 195 | screenshot = _screenshot, 196 | reload_game = _reload_game, 197 | developer = _developer, 198 | quit = renpy.quit_event, 199 | iconify = renpy.iconify, 200 | help = _help, 201 | choose_renderer = renpy.curried_call_in_new_context("_choose_renderer"), 202 | console = _console.enter, 203 | profile_once = _profile_once, 204 | memory_profile = _memory_profile, 205 | self_voicing = Preference("self voicing", "toggle"), 206 | clipboard_voicing = Preference("clipboard voicing", "toggle"), 207 | debug_voicing = Preference("debug voicing", "toggle"), 208 | progress_screen = _progress_screen, 209 | ), 210 | ] 211 | 212 | config.rollback_enabled = False 213 | 214 | # This section controls how to build Ren'Py. (Building the launcher is how 215 | # we build Ren'Py distributions.) 216 | init python: 217 | 218 | # We're building Ren'Py tonight. 219 | build.renpy = True 220 | 221 | # The version number that's supplied to the updater. 222 | build.version = "Ren'Py {}".format(config.version) 223 | 224 | # The name that's used for directories and archive files. For example, if 225 | # this is 'mygame-1.0', the windows distribution will be in the 226 | # directory 'mygame-1.0-win', in the 'mygame-1.0-win.zip' file. 227 | 228 | if 'RENPY_BUILD_VERSION' in os.environ: 229 | build.directory_name = "DDML-" + os.environ['RENPY_BUILD_VERSION'] 230 | else: 231 | build.directory_name = "DDML-" + config.version.rsplit('.', 1)[0] 232 | 233 | # The name that's uses for executables - the program that users will run 234 | # to start the game. For example, if this is 'mygame', then on Windows, 235 | # users can click 'mygame.exe' to start the game. 236 | build.executable_name = "renpy" 237 | 238 | # If True, Ren'Py will include update information into packages. This 239 | # allows the updater to run. 240 | build.include_update = True 241 | 242 | # Allow empty directories, so we can distribute the images directory. 243 | build.exclude_empty_directories = False 244 | 245 | # Mac signing options. 246 | import os 247 | build.mac_identity = os.environ.get("RENPY_MAC_IDENTITY", None) 248 | build.mac_codesign_command = [ config.renpy_base + "/scripts/mac/mac_sign_client.sh", "{identity}", "{app}" ] 249 | build.mac_create_dmg_command = [ config.renpy_base + "/scripts/mac/mac_dmg_client.sh", "{identity}", "{volname}", "{sourcedir}", "{dmg}" ] 250 | build.mac_codesign_dmg_command = [ "/bin/true" ] 251 | 252 | # Clear out various file patterns. 253 | build.renpy_patterns = [ ] 254 | build.early_base_patterns = [ ] 255 | build.base_patterns = [ ] 256 | build.late_base_patterns = [ ] 257 | 258 | # We don't need to clear out the executable patterns, since they're 259 | # correct for Ren'Py. 260 | 261 | # Now, add the Ren'Py distribution in using classify_renpy. 262 | 263 | build.classify_renpy("**~", None) 264 | build.classify_renpy("**/#*", None) 265 | build.classify_renpy("**/thumbs.db", None) 266 | build.classify_renpy("**/.*", None) 267 | 268 | build.classify_renpy("**.old", None) 269 | build.classify_renpy("**.new", None) 270 | build.classify_renpy("**.bak", None) 271 | build.classify_renpy("**.pyc", None) 272 | 273 | build.classify_renpy("**/log.txt", None) 274 | build.classify_renpy("**/traceback.txt", None) 275 | build.classify_renpy("**/errors.txt", None) 276 | build.classify_renpy("**/steam_appid.txt", None) 277 | build.classify_renpy("**/saves/", None) 278 | build.classify_renpy("**/tmp/", None) 279 | build.classify_renpy("**/.Editra", None) 280 | 281 | 282 | # main source. 283 | 284 | def source_and_binary(pattern, source="source", binary="binary"): 285 | """ 286 | Classifies source and binary files beginning with `pattern`. 287 | .pyo, .rpyc, .rpycm, and .rpyb go into binary, everything 288 | else goes into source. 289 | """ 290 | 291 | build.classify_renpy(pattern + "/**.pyo", binary) 292 | build.classify_renpy(pattern + "/**.rpyc", binary) 293 | build.classify_renpy(pattern + "/**.rpymc", binary) 294 | build.classify_renpy(pattern + "/**/cache/*", binary) 295 | 296 | build.classify_renpy(pattern + "/**", source) 297 | 298 | build.classify_renpy("renpy.py", "binary") 299 | source_and_binary("renpy") 300 | build.classify_renpy("ddmc.json", "binary") 301 | 302 | # games. 303 | build.classify_renpy("launcher/game/theme/", None) 304 | 305 | source_and_binary("launcher") 306 | # docs. 307 | build.classify_renpy("LICENSE.txt", "source") 308 | 309 | # module. 310 | build.classify_renpy("module/", "source") 311 | build.classify_renpy("module/*.c", "source") 312 | build.classify_renpy("module/gen/", "source") 313 | build.classify_renpy("module/gen/*.c", "source") 314 | build.classify_renpy("module/*.h", "source") 315 | build.classify_renpy("module/*.py*", "source") 316 | build.classify_renpy("module/include/", "source") 317 | build.classify_renpy("module/include/*.pxd", "source") 318 | build.classify_renpy("module/include/*.pxi", "source") 319 | build.classify_renpy("module/pysdlsound/", "source") 320 | build.classify_renpy("module/pysdlsound/*.py", "source") 321 | build.classify_renpy("module/pysdlsound/*.pyx", "source") 322 | build.classify_renpy("module/fribidi-src/**", "source") 323 | 324 | # all-platforms binary. 325 | build.classify_renpy("lib/**", "binary") 326 | build.classify_renpy("renpy.sh", "binary") 327 | # renpy.app is now built from scratch from distribute.rpy. 328 | 329 | # Packages. 330 | build.packages = [ ] 331 | 332 | build.package("all", "zip", "source binary") 333 | 334 | # Enable the special launcher translation mode. 335 | define config.translate_launcher = True 336 | 337 | # Reduce the rate of screen updates. 338 | default preferences.gl_powersave = True 339 | 340 | # Disable steam. 341 | python early: 342 | config.enable_steam = False 343 | -------------------------------------------------------------------------------- /launcher/game/package_formats.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2020 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 | init python in distribute: 23 | 24 | import time 25 | import zipfile 26 | import tarfile 27 | import zlib 28 | import struct 29 | import stat 30 | import shutil 31 | import sys 32 | import threading 33 | 34 | from renpy import six 35 | from zipfile import crc32 36 | 37 | 38 | # Since the long type doesn't exist on py3, define it here 39 | if six.PY3: 40 | long = int 41 | 42 | zlib.Z_DEFAULT_COMPRESSION = 5 43 | 44 | class ZipFile(zipfile.ZipFile): 45 | 46 | 47 | def write_with_info(self, zinfo, filename): 48 | """Put the bytes from filename into the archive under the name 49 | arcname.""" 50 | if not self.fp: 51 | raise RuntimeError( 52 | "Attempt to write to ZIP archive that was already closed") 53 | 54 | st = os.stat(filename) 55 | isdir = stat.S_ISDIR(st.st_mode) 56 | 57 | zinfo.file_size = st.st_size 58 | zinfo.flag_bits = 0x00 59 | zinfo.header_offset = self.fp.tell() # Start of header bytes 60 | 61 | self._writecheck(zinfo) 62 | self._didModify = True 63 | 64 | if isdir: 65 | zinfo.file_size = 0 66 | zinfo.compress_size = 0 67 | zinfo.CRC = 0 68 | self.filelist.append(zinfo) 69 | self.NameToInfo[zinfo.filename] = zinfo 70 | self.fp.write(zinfo.FileHeader()) 71 | return 72 | 73 | with open(filename, "rb") as fp: 74 | # Must overwrite CRC and sizes with correct data later 75 | zinfo.CRC = CRC = 0 76 | zinfo.compress_size = compress_size = 0 77 | file_size = 0 78 | 79 | zip64 = self._allowZip64 and \ 80 | zinfo.file_size * 1.05 > zipfile.ZIP64_LIMIT 81 | 82 | self.fp.write(zinfo.FileHeader(zip64)) 83 | 84 | if zinfo.compress_type == zipfile.ZIP_DEFLATED: 85 | cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, 86 | zlib.DEFLATED, -15) 87 | else: 88 | cmpr = None 89 | while 1: 90 | buf = fp.read(1024 * 1024) 91 | if not buf: 92 | break 93 | file_size = file_size + len(buf) 94 | CRC = crc32(buf, CRC) & 0xffffffff 95 | if cmpr: 96 | buf = cmpr.compress(buf) 97 | compress_size = compress_size + len(buf) 98 | self.fp.write(buf) 99 | if cmpr: 100 | buf = cmpr.flush() 101 | compress_size = compress_size + len(buf) 102 | self.fp.write(buf) 103 | zinfo.compress_size = compress_size 104 | else: 105 | zinfo.compress_size = file_size 106 | zinfo.CRC = CRC 107 | zinfo.file_size = file_size 108 | 109 | if not zip64 and self._allowZip64: 110 | if file_size > zipfile.ZIP64_LIMIT: 111 | raise RuntimeError('File size has increased during compressing') 112 | if compress_size > zipfile.ZIP64_LIMIT: 113 | raise RuntimeError('Compressed size larger than uncompressed size') 114 | 115 | # Seek backwards and write CRC and file sizes 116 | position = self.fp.tell() # Preserve current position in file 117 | self.fp.seek(zinfo.header_offset, 0) 118 | 119 | self.fp.write(zinfo.FileHeader(zip64)) 120 | 121 | self.fp.seek(position, 0) 122 | self.filelist.append(zinfo) 123 | self.NameToInfo[zinfo.filename] = zinfo 124 | 125 | 126 | class ZipPackage(object): 127 | """ 128 | A class that creates a zip file. 129 | """ 130 | 131 | def __init__(self, filename): 132 | self.zipfile = ZipFile(filename, "w", zipfile.ZIP_DEFLATED, True) 133 | 134 | def get_date_time(self, path): 135 | """ 136 | Gets the datetime for a file. If the time doesn't exist or is 137 | weird, use the current time instead. 138 | """ 139 | 140 | try: 141 | s = os.stat(path) 142 | rv = time.gmtime(s.st_mtime)[:6] 143 | 144 | # Check that the time is sensible. 145 | if rv[0] < 2000: 146 | rv = None 147 | except: 148 | rv = None 149 | 150 | if rv is None: 151 | rv = time.gmtime()[:6] 152 | 153 | return rv 154 | 155 | def add_file(self, name, path, xbit): 156 | 157 | if path is None: 158 | raise Exception("path for " + name + " must not be None.") 159 | 160 | zi = zipfile.ZipInfo(name) 161 | zi.date_time = self.get_date_time(path) 162 | zi.compress_type = zipfile.ZIP_DEFLATED 163 | zi.create_system = 3 164 | 165 | if xbit: 166 | zi.external_attr = long(0o100755) << 16 167 | else: 168 | zi.external_attr = long(0o100644) << 16 169 | 170 | self.zipfile.write_with_info(zi, path) 171 | 172 | def add_directory(self, name, path): 173 | if path is None: 174 | return 175 | 176 | zi = zipfile.ZipInfo(name + "/") 177 | zi.date_time = self.get_date_time(path) 178 | zi.compress_type = zipfile.ZIP_STORED 179 | zi.create_system = 3 180 | zi.external_attr = (long(0o040755) << 16) | 0x10 181 | 182 | self.zipfile.write_with_info(zi, path) 183 | 184 | def close(self): 185 | self.zipfile.close() 186 | 187 | 188 | class TarPackage(object): 189 | 190 | def __init__(self, filename, mode, notime=False): 191 | """ 192 | notime 193 | If true, times will be forced to the epoch. 194 | """ 195 | 196 | self.tarfile = tarfile.open(filename, mode) 197 | self.tarfile.dereference = True 198 | self.notime = notime 199 | 200 | def add_file(self, name, path, xbit): 201 | 202 | if path is not None: 203 | info = self.tarfile.gettarinfo(path, name) 204 | else: 205 | info = tarfile.TarInfo(name) 206 | info.size = 0 207 | info.mtime = int(time.time()) 208 | info.type = tarfile.DIRTYPE 209 | 210 | if xbit: 211 | info.mode = 0o755 212 | else: 213 | info.mode = 0o644 214 | 215 | info.uid = 1000 216 | info.gid = 1000 217 | info.uname = "renpy" 218 | info.gname = "renpy" 219 | 220 | if self.notime: 221 | info.mtime = 0 222 | 223 | if info.isreg(): 224 | with open(path, "rb") as f: 225 | self.tarfile.addfile(info, f) 226 | else: 227 | self.tarfile.addfile(info) 228 | 229 | def add_directory(self, name, path): 230 | self.add_file(name, path, True) 231 | 232 | def close(self): 233 | self.tarfile.close() 234 | 235 | class UpdatePackage(TarPackage): 236 | 237 | def __init__(self, filename, basename, destination): 238 | self.path = filename 239 | self.basename = basename 240 | self.destination = destination 241 | 242 | TarPackage.__init__(self, filename, "w", notime=True) 243 | 244 | def close(self): 245 | TarPackage.close(self) 246 | 247 | cmd = [ 248 | updater.zsync_path("zsyncmake"), 249 | "-z", 250 | # -u url to gzipped data - not a local filename! 251 | "-u", self.basename + ".update.gz", 252 | "-o", os.path.join(self.destination, self.basename + ".zsync"), 253 | os.path.abspath(self.path), 254 | ] 255 | 256 | subprocess.check_call([ renpy.fsencode(i) for i in cmd ]) 257 | 258 | # Build the sums file. This is a file with an adler32 hash of each 64k block 259 | # of the zsync file. It's used to help us determine how much of the file is 260 | # downloaded. 261 | with open(self.path, "rb") as src: 262 | with open(renpy.fsencode(os.path.join(self.destination, self.basename + ".sums")), "wb") as sums: 263 | while True: 264 | data = src.read(65536) 265 | 266 | if not data: 267 | break 268 | 269 | sums.write(struct.pack(" 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 | init python: 23 | persistent.b_ddml = None 24 | 25 | if persistent.show_edit_funcs is None: 26 | persistent.show_edit_funcs = True 27 | 28 | if persistent.windows_console is None: 29 | persistent.windows_console = False 30 | 31 | def scan_translations(): 32 | 33 | languages = renpy.known_languages() 34 | 35 | if not languages: 36 | return None 37 | 38 | rv = [ ( "English", None) ] 39 | 40 | for i in languages: 41 | rv.append((i.title(), i)) 42 | 43 | for i in (("Schinese", "schinese"), ("Tchinese", "tchinese")): 44 | if i in rv: 45 | rv.remove(i) 46 | rv.append(({"schinese": "Simplified Chinese", "tchinese": "Traditional Chinese"}.get(i[1]), i[1])) 47 | 48 | rv.sort() 49 | 50 | if ("Piglatin", "piglatin") in rv: 51 | rv.remove(("Piglatin", "piglatin")) 52 | rv.append(("Pig Latin", "piglatin")) 53 | 54 | return rv 55 | 56 | show_legacy = os.path.exists(os.path.join(config.renpy_base, "templates", "english", "game", "script.rpy")) 57 | 58 | default persistent.legacy = False 59 | default persistent.force_new_tutorial = False 60 | default persistent.sponsor_message = True 61 | default persistent.nsfw = False 62 | 63 | screen preferences: 64 | 65 | $ translations = scan_translations() 66 | 67 | frame: 68 | style_group "l" 69 | style "l_root" 70 | alt "Preferences" 71 | 72 | window: 73 | 74 | has vbox 75 | 76 | label _("Settings") 77 | 78 | add HALF_SPACER 79 | 80 | hbox: 81 | 82 | frame: 83 | style "l_indent" 84 | xmaximum ONETHIRD 85 | xfill True 86 | 87 | has vbox 88 | 89 | # Projects directory selection. 90 | add SEPARATOR2 at mod_search_seperator 91 | add HALF_SPACER 92 | 93 | frame: 94 | style "l_indent" 95 | yminimum 75 96 | has vbox 97 | 98 | text _("Mod Folder Path") 99 | 100 | add HALF_SPACER 101 | 102 | 103 | frame style "l_indent": 104 | if persistent.projects_directory: 105 | textbutton _("[persistent.projects_directory!q]"): 106 | action Jump("projects_directory_preference") 107 | alt _("Mod Folder Path [text]") 108 | else: 109 | textbutton _("Not Set"): 110 | action Jump("projects_directory_preference") 111 | alt _("Mod Folder Path [text]") 112 | 113 | add SPACER 114 | 115 | add SEPARATOR2 at mod_search_seperator 116 | add HALF_SPACER 117 | 118 | frame: 119 | style "l_indent" 120 | yminimum 75 121 | has vbox 122 | text _("DDLC Copy Path") 123 | 124 | add HALF_SPACER 125 | 126 | frame style "l_indent": 127 | if persistent.zip_directory: 128 | textbutton _("[persistent.zip_directory!q]"): 129 | action Jump("projects_zip_preference") 130 | alt _("DDLC ZIP Path [text]") 131 | else: 132 | textbutton _("Not Set"): 133 | action Jump("projects_zip_preference") 134 | alt _("DDLC ZIP Path [text]") 135 | 136 | add HALF_SPACER 137 | 138 | frame style "l_indent": 139 | if persistent.steam_release: 140 | text _("Version: Steam") 141 | else: 142 | if persistent.steam_release is None: 143 | text _("Version: Unknown") 144 | else: 145 | text _("Version: DDLC.moe") 146 | 147 | if renpy.macintosh: 148 | frame: 149 | style "l_indent" 150 | xmaximum ONETHIRD 151 | xfill True 152 | 153 | has vbox 154 | add SEPARATOR2 155 | 156 | frame: 157 | style "l_indent" 158 | yminimum 75 159 | has vbox 160 | 161 | text _("ZIP Auto-Extracts?") 162 | 163 | add HALF_SPACER 164 | 165 | frame style "l_indent": 166 | if persistent.safari is not None: 167 | if persistent.safari: 168 | text _("Yes") 169 | else: 170 | text _("No") 171 | else: 172 | text _("Unknown") 173 | 174 | add HALF_SPACER 175 | 176 | textbutton _("Change Auto-Extract Setting") action Jump("auto_extract") 177 | 178 | add SPACER 179 | 180 | frame: 181 | style "l_indent" 182 | if not renpy.macintosh: 183 | xmaximum ONETHIRD 184 | xfill True 185 | else: 186 | yminimum 75 187 | 188 | has vbox 189 | add SEPARATOR2 at mod_search_seperator 190 | add HALF_SPACER 191 | 192 | frame: 193 | style "l_indent" 194 | yminimum 75 195 | has vbox 196 | 197 | text _("Launcher Options:") 198 | 199 | add HALF_SPACER 200 | 201 | textbutton _("Reset Window Size") style "l_nonbox" action Preference("display", 1.0) 202 | textbutton _("Customize Launcher Images") style "l_nonbox" action OpenDirectory(os.getcwd() + "/launcher/game/images") 203 | if renpy.windows: 204 | textbutton _("Transfer DDMM Data") style "l_nonbox" action Jump("transfer") 205 | 206 | textbutton _("Show NSFW Mods In Search") style "l_checkbox" action [ToggleField(persistent, "nsfw")] 207 | 208 | if not renpy.windows: 209 | textbutton _("Developer Options") style "l_checkbox" action ToggleField(persistent, "b_ddml") 210 | if persistent.b_ddml: 211 | textbutton _("Build DDML") style "l_nonbox" action [project.Select("launcher"), Jump("build_distributions")] 212 | 213 | frame: 214 | style "l_indent" 215 | if not renpy.macintosh: 216 | xmaximum ONETHIRD 217 | xfill True 218 | else: 219 | yminimum 75 220 | 221 | has vbox 222 | add SEPARATOR2 at mod_search_seperator 223 | add HALF_SPACER 224 | 225 | frame: 226 | style "l_indent" 227 | yminimum 75 228 | has vbox 229 | 230 | text _("Theme Options:") 231 | 232 | add HALF_SPACER 233 | 234 | textbutton _("One UI (Dark Mode)") style "l_checkbox" action [ToggleField(persistent, "oneui"), If(persistent.custom, true=SetField(persistent, "custom", False), false=None), Jump("restart_ddmm")] 235 | if renpy.loadable("images/custom_style.rpy"): 236 | textbutton _("Custom Theme") style "l_checkbox" action [ToggleField(persistent, "custom"), If(persistent.oneui, true=SetField(persistent, "oneui", False), false=None), Jump("restart_ddmm")] 237 | else: 238 | textbutton _("Custom Theme Unavailable") style "l_nonbox" action Call("no_custom_file") 239 | 240 | textbutton _("Return") action Jump("front_page") style "l_left_button" 241 | 242 | label projects_directory_preference: 243 | python: 244 | release_kind = interface.choice( 245 | _("Are you wanting to move your mods to a new folder or setup a new mod folder?"), 246 | [ ( 'move_mod_folder', _("Move Mods to a New Folder") ), ( 'choose_projects_directory', _("Setup a New Mod Folder")) ], 247 | "choose_projects_directory", 248 | cancel=Jump("preferences"), 249 | ) 250 | 251 | renpy.jump(release_kind) 252 | 253 | jump preferences 254 | 255 | label projects_zip_preference: 256 | call ddlc_location 257 | jump preferences 258 | 259 | label preferences: 260 | call screen preferences 261 | jump preferences 262 | 263 | label restart_ddmm: 264 | python: 265 | renpy.quit(relaunch=True) 266 | return 267 | 268 | label no_custom_file: 269 | python hide: 270 | interface.info(_("Custom Theme is unavailable as the RPY needed to load the theme is missing.\nMake sure a file called {u}custom_style.rpy{/u} is in the {i}images{/i} folder and restart DDML.")) 271 | jump preferences -------------------------------------------------------------------------------- /launcher/game/style.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2019 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 | init -1: 23 | 24 | # Fonts. 25 | define gui.LIGHT_FONT = "fonts/Roboto-Light.ttf" 26 | define gui.REGULAR_FONT = "fonts/Roboto-Regular.ttf" 27 | 28 | # Used to scale the size of fonts. 29 | define gui.FONT_SCALE = 1.0 30 | 31 | # Should places where the regular font used be bolded? 32 | define gui.REGULAR_BOLD = False 33 | 34 | default persistent.oneui = True 35 | 36 | init -1 python: 37 | 38 | config.defer_styles = True 39 | 40 | if persistent.oneui == True: 41 | # The color of non-interactive text. 42 | TEXT = "#F5F5F5" #545454 43 | 44 | # Colors for buttons in various states. 45 | IDLE = "#d1d1d1" #42637b 46 | HOVER = "#78a5c5" #d86b45 47 | DISABLED = "#6b6b6b" #808080 48 | 49 | # Colors for reversed text buttons (selected list entries). 50 | REVERSE_IDLE = "#d1d1d1" #78a5c5 51 | REVERSE_HOVER = "#909090" #d86b45 52 | REVERSE_TEXT = "#111" #ffffff 53 | 54 | # Colors for the scrollbar thumb. 55 | SCROLLBAR_IDLE = "#5c6e91" #dfdfdf 56 | SCROLLBAR_HOVER = "#31326f" #d86b45 57 | # An image used as a separator pattern. 58 | PATTERN = "images/pattern.png" 59 | 60 | # A displayable used for the background of everything. 61 | BACKGROUND = "images/background_dark.png" 62 | 63 | # A displayable used for the background of windows 64 | # containing commands, preferences, and navigation info. 65 | WINDOW = Frame("images/window_dark.png", 0, 0, tile=True) #ffffff80 66 | 67 | # A displayable used for the background of the projects list. 68 | PROJECTS_WINDOW = Null() 69 | 70 | # A displayable used the background of information boxes. 71 | INFO_WINDOW = "#181818" #f9f9f9c0 72 | 73 | # Colors for the titles of information boxes. 74 | ERROR_COLOR = "#f05454" #d15353 75 | INFO_COLOR = "#34626c" #545454 76 | INTERACTION_COLOR = "#9E9E9E" #d19753 77 | QUESTION_COLOR = "#f5b461" #d19753 78 | 79 | # The color of input text. 80 | INPUT_COLOR = "#FAFAFA" #d86b45 81 | elif not persistent.custom: 82 | # The color of non-interactive text. 83 | TEXT = "#545454" 84 | 85 | # Colors for buttons in various states. 86 | IDLE = "#42637b" 87 | HOVER = "#d86b45" 88 | DISABLED = "#808080" 89 | 90 | # Colors for reversed text buttons (selected list entries). 91 | REVERSE_IDLE = "#78a5c5" 92 | REVERSE_HOVER = "#d86b45" 93 | REVERSE_TEXT = "#ffffff" 94 | 95 | # Colors for the scrollbar thumb. 96 | SCROLLBAR_IDLE = "#dfdfdf" 97 | SCROLLBAR_HOVER = "#d86b45" 98 | 99 | # An image used as a separator pattern. 100 | PATTERN = "images/pattern.png" 101 | 102 | # A displayable used for the background of everything. 103 | BACKGROUND = "images/background.png" 104 | 105 | # A displayable used for the background of windows 106 | # containing commands, preferences, and navigation info. 107 | WINDOW = Frame("images/window.png", 0, 0, tile=True) 108 | 109 | # A displayable used for the background of the projects list. 110 | PROJECTS_WINDOW = Null() 111 | 112 | # A displayable used the background of information boxes. 113 | INFO_WINDOW = "#f9f9f9" 114 | 115 | # Colors for the titles of information boxes. 116 | ERROR_COLOR = "#d15353" 117 | INFO_COLOR = "#545454" 118 | INTERACTION_COLOR = "#d19753" 119 | QUESTION_COLOR = "#d19753" 120 | 121 | # The color of input text. 122 | INPUT_COLOR = "#d86b45" 123 | 124 | init 1 python: 125 | 126 | def size(n): 127 | """ 128 | Adjusts the font size if we're in large-print mode. 129 | """ 130 | 131 | if persistent.large_print and n < 18: 132 | n = 18 133 | 134 | n = int(n * gui.FONT_SCALE) 135 | 136 | return n 137 | 138 | def light_font(): 139 | if persistent.large_print: 140 | return gui.REGULAR_FONT 141 | 142 | return gui.LIGHT_FONT 143 | 144 | def regular_font(): 145 | return gui.REGULAR_FONT 146 | 147 | INDENT = 20 148 | HALF_INDENT = 10 149 | 150 | SCROLLBAR_SIZE = 16 151 | 152 | SEPARATOR = Frame(PATTERN, 0, 0, tile=True, ymaximum=5, yalign=1.0) 153 | SEPARATOR2 = Frame(PATTERN, 0, 0, tile=True, ymaximum=10, yalign=1.0) 154 | 155 | SPACER_HEIGHT = 8 156 | SPACER = Null(height=SPACER_HEIGHT) 157 | 158 | HALF_SPACER_HEIGHT = 4 159 | HALF_SPACER = Null(height=HALF_SPACER_HEIGHT) 160 | 161 | # DIVIDING THE SCREEN 162 | ONETHIRD = 258 163 | TWOTHIRDS = 496 164 | ONEHALF = 377 165 | 166 | def checkbox(full, color): 167 | if full: 168 | return im.Twocolor("images/checkbox_full.png", color, color, style="l_checkbox_box") 169 | else: 170 | return im.Twocolor("images/checkbox_empty.png", color, color, style="l_checkbox_box") 171 | 172 | 173 | 174 | # The default style. 175 | style l_default is default: 176 | font light_font() 177 | color TEXT 178 | idle_color IDLE 179 | hover_color HOVER 180 | size size(18) 181 | 182 | style l_text is l_default 183 | 184 | style l_button is l_default 185 | style l_button_text is l_default: 186 | insensitive_color DISABLED 187 | selected_font regular_font() 188 | selected_bold gui.REGULAR_BOLD 189 | 190 | # A small button, used at the bottom of the screen. 191 | style l_link is l_default 192 | style l_link_text is l_default: 193 | size size(14) 194 | font light_font() 195 | 196 | # Action buttons on the bottom of the screen. 197 | style l_right_button is l_default: 198 | xalign 1.0 199 | ypos 600 - 128 + 12 200 | left_margin 8 + INDENT 201 | right_margin 10 + INDENT 202 | 203 | style l_right_button_text is l_default: 204 | size size(30) 205 | 206 | style l_left_button is l_right_button: 207 | xalign 0.0 208 | 209 | style l_left_button_text is l_right_button_text 210 | 211 | 212 | # The root frame. This contains everything but the bottom navigation, 213 | # and buttons. 214 | style l_root is l_default: 215 | background BACKGROUND 216 | xpadding 10 217 | top_padding 64 218 | bottom_padding 128 219 | 220 | # An inner window. 221 | style l_window is l_default: 222 | background WINDOW 223 | left_padding 6 224 | xfill True 225 | yfill True 226 | 227 | # Normal size labels. 228 | style l_label is l_default: 229 | xfill True 230 | top_padding 10 231 | bottom_padding 8 232 | bottom_margin 12 233 | background SEPARATOR 234 | 235 | style l_label_text is l_default: 236 | size size(24) 237 | xpos INDENT 238 | yoffset 6 239 | 240 | style l_label_small is l_default: 241 | xfill True 242 | bottom_padding 8 243 | bottom_margin HALF_SPACER_HEIGHT 244 | background SEPARATOR 245 | 246 | # Small labels. 247 | style l_label_small_text is l_default: 248 | xpos INDENT 249 | yoffset 6 250 | size size(20) 251 | 252 | # Alternate labels. This nests inside an l_label, and gives a button 253 | # or label that's nested inside another label. 254 | 255 | style l_alternate is l_default: 256 | xalign 1.0 257 | yalign 1.0 258 | yoffset 4 259 | right_margin INDENT 260 | 261 | style l_alternate_text is l_default: 262 | size size(14) 263 | font light_font() 264 | text_align 1.0 265 | 266 | style l_small_button is l_button 267 | 268 | style l_small_button_text is l_button_text: 269 | size size(14) 270 | 271 | style l_small_text is l_text: 272 | size size(14) 273 | 274 | # Indents its contents. 275 | style l_indent is l_default: 276 | left_margin INDENT 277 | 278 | # Indents its contents and pads vertically. 279 | style l_indent_margin is l_indent: 280 | ymargin 6 281 | 282 | # Lists. 283 | style l_list is l_default: 284 | left_padding HALF_INDENT 285 | xfill True 286 | selected_background REVERSE_IDLE 287 | selected_hover_background REVERSE_HOVER 288 | 289 | style l_list_text is l_default: 290 | idle_color IDLE 291 | hover_color HOVER 292 | selected_idle_color REVERSE_TEXT 293 | selected_hover_color REVERSE_TEXT 294 | insensitive_color DISABLED 295 | 296 | style l_list2 is l_list: 297 | left_padding (HALF_INDENT + INDENT) 298 | 299 | style l_list2_text is l_list_text 300 | 301 | # Scrollbar. 302 | style l_vscrollbar is l_default: 303 | thumb Fixed( 304 | Solid(SCROLLBAR_IDLE, xmaximum=8, xalign=0.5), 305 | Image("images/vscrollbar_center.png", xalign=0.5, yalign=0.5), 306 | xmaximum = SCROLLBAR_SIZE) 307 | hover_thumb Fixed( 308 | Solid(SCROLLBAR_HOVER, xmaximum=8, xalign=0.5), 309 | Image("images/vscrollbar_center.png", xalign=0.5, yalign=0.5), 310 | xmaximum = SCROLLBAR_SIZE) 311 | xmaximum SCROLLBAR_SIZE 312 | bar_vertical True 313 | bar_invert True 314 | unscrollable "hide" 315 | 316 | # Information window. 317 | style l_info_vbox is vbox: 318 | yalign 0.5 319 | xalign 0.5 320 | xfill True 321 | 322 | style l_info_frame is l_default: 323 | ypadding 21 324 | xfill True 325 | background Fixed( 326 | INFO_WINDOW, 327 | Frame(PATTERN, 0, 0, tile=True, ymaximum=5, yalign=0.0, yoffset=8), 328 | Frame(PATTERN, 0, 0, tile=True, ymaximum=5, yalign=1.0, yoffset=-8), 329 | ) 330 | yminimum 180 331 | ypos 75 332 | 333 | style l_info_label is l_default: 334 | xalign 0.5 335 | ypos 75 336 | yanchor 1.0 337 | yoffset 12 338 | 339 | style l_info_label_text is l_default: 340 | size size(36) 341 | 342 | style l_info_text is l_default: 343 | xalign 0.5 344 | 345 | style l_info_button is l_button: 346 | xalign 0.5 347 | xmargin 50 348 | 349 | style l_info_button_text is l_button_text: 350 | text_align 0.5 351 | layout "subtitle" 352 | 353 | # Progress bar. 354 | style l_progress_frame is l_default: 355 | background Frame(PATTERN, 0, 0, tile=True) 356 | ypadding 5 357 | 358 | style l_progress_bar is l_default: 359 | left_bar REVERSE_IDLE 360 | right_bar Null() 361 | ymaximum 24 362 | 363 | # Navigation. 364 | style l_navigation_button is l_button: 365 | size_group "navigation" 366 | right_margin INDENT 367 | top_margin 3 368 | 369 | style l_navigation_button_text is l_button_text: 370 | size size(14) 371 | font regular_font() 372 | 373 | style l_navigation_text is l_text: 374 | size size(14) 375 | font light_font() 376 | color TEXT 377 | 378 | # Checkboxes. 379 | style l_checkbox is l_button: 380 | left_padding INDENT 381 | background checkbox(False, IDLE) 382 | hover_background checkbox(False, HOVER) 383 | selected_idle_background checkbox(True, IDLE) 384 | selected_hover_background checkbox(True, HOVER) 385 | insensitive_background checkbox(False, DISABLED) 386 | 387 | style l_checkbox_box: 388 | yanchor 0.5 389 | ypos 11 390 | 391 | style l_checkbox_text is l_button_text: 392 | selected_font light_font() 393 | selected_bold False 394 | 395 | # Lines up with a checkbox. 396 | style l_nonbox is l_button: 397 | xpadding INDENT 398 | 399 | style l_nonbox_text is l_button_text: 400 | selected_font light_font() 401 | 402 | # Projects list. 403 | style l_projects is l_default: 404 | background PROJECTS_WINDOW 405 | 406 | style hyperlink_text: 407 | size size(18) 408 | font light_font() 409 | color IDLE 410 | hover_color HOVER 411 | -------------------------------------------------------------------------------- /launcher/game/tkaskdir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2004-2020 Tom Rothamel 4 | # 5 | # Permission is hereby granted, free of charge, to any person 6 | # obtaining a copy of this software and associated documentation files 7 | # (the "Software"), to deal in the Software without restriction, 8 | # including without limitation the rights to use, copy, modify, merge, 9 | # publish, distribute, sublicense, and/or sell copies of the Software, 10 | # and to permit persons to whom the Software is furnished to do so, 11 | # subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | # This is used on Linux and Mac to prompt the user for the projects 25 | # directory. 26 | 27 | import sys 28 | 29 | 30 | # Gtk generally has better support than TKinter on various Linux distributions 31 | def gtk_select_directory(title): 32 | dialog = Gtk.FileChooserNative(title=title, 33 | action=Gtk.FileChooserAction.SELECT_FOLDER) 34 | 35 | dialog.run() 36 | 37 | return dialog.get_filename() 38 | 39 | 40 | # Fall back to TKinter if Gtk isn't available 41 | def tk_select_directory(initialdir, title): 42 | root = Tk() 43 | root.withdraw() 44 | 45 | return askdirectory(initialdir=initialdir, parent=root, title=title) 46 | 47 | 48 | try: 49 | import gi 50 | gi.require_version('Gtk', '3.0') 51 | from gi.repository import Gtk 52 | 53 | def select_directory(title): 54 | result = gtk_select_directory(title) 55 | 56 | return result if result else '' 57 | 58 | except: 59 | # Python3 and Python2-style imports. 60 | try: 61 | from tkinter import Tk 62 | from tkinter.filedialog import askdirectory 63 | except ImportError: 64 | from Tkinter import Tk 65 | from tkFileDialog import askdirectory 66 | 67 | def select_directory(title): 68 | return tk_select_directory(title, sys.argv[1]) 69 | 70 | if __name__ == '__main__': 71 | directory = select_directory('Select Mod Folder Directory') 72 | 73 | sys.stdout.write(directory) 74 | -------------------------------------------------------------------------------- /launcher/game/tkaskfile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # Gtk generally has better support than TKinter on various Linux distributions 4 | def gtk_select_file(title): 5 | dialog = Gtk.FileChooserNative(title=title, 6 | action=Gtk.FileChooserAction.OPEN) 7 | 8 | dialog.run() 9 | 10 | return dialog.get_filename() 11 | 12 | # Fall back to TKinter if Gtk isn't available 13 | def tk_select_file(initialdir, title): 14 | root = Tk() 15 | root.withdraw() 16 | 17 | return askopenfilename(initialdir=initialdir, parent=root, title=title) 18 | 19 | try: 20 | import gi 21 | gi.require_version('Gtk', '3.0') 22 | from gi.repository import Gtk 23 | 24 | def select_file(title): 25 | result = gtk_select_file(title) 26 | 27 | return result if result else '' 28 | 29 | except: 30 | # Python3 and Python2-style imports. 31 | try: 32 | from tkinter import Tk 33 | from tkinter.filedialog import askopenfilename 34 | except ImportError: 35 | from Tkinter import Tk 36 | from tkFileDialog import askopenfilename 37 | 38 | def select_file(title): 39 | return tk_select_file(title, sys.argv[1]) 40 | 41 | if __name__ == '__main__': 42 | directory = select_file('Select File') 43 | 44 | sys.stdout.write(directory) 45 | -------------------------------------------------------------------------------- /launcher/game/transfer.rpy: -------------------------------------------------------------------------------- 1 | 2 | init python: 3 | from ddmm_compatibility import DDMM_Compatibility 4 | mm_compat = DDMM_Compatibility() 5 | 6 | label transfer: 7 | 8 | python hide: 9 | 10 | failed_mods = 0 11 | 12 | interface.info(_("Ready to transfer your DDMM (Doki Doki Mod Manager) data to DDML?"), 13 | _("This transfer tool with help you setup your mods made in DDMM to work DDML.")) 14 | 15 | if not renpy.windows: 16 | interface.error(_("This transfer tool only works on Windows as DDMM was only designed for Windows operating systems."), 17 | _("Run DDML in a Windows OS and try again.")) 18 | 19 | mm_compat.ddmm_traceback_start() 20 | 21 | interface.info(_("First we will try to see where DDMM stores your mods in."), 22 | _("This will help detect what mods you have in DDMM for DDML to use.")) 23 | 24 | interface.interaction( _("Finding DDMM Directory"), _("Finding your DDMM directory. Please wait...")) 25 | 26 | if not mm_compat.ddmm_detection(): 27 | 28 | interface.error(_("The transfer tool was unable to find your DDMM directory."), 29 | _("If you believe this message was made in error, contact the developer on Github.")) 30 | 31 | persistent.projects_directory = mm_compat.ddmm_path_setup() 32 | 33 | interface.info(_("Your DDMM directory was found successfully."), 34 | _("Now the transfer tool will now detect and setup your mods to work with DDMM."), 35 | _("Make sure no mods or DDMM are running on your computer before proceeding.")) 36 | 37 | for x in os.listdir(persistent.projects_directory): 38 | if mm_compat.ddmm_folder_not_compliant(persistent.projects_directory, x): 39 | interface.interaction(_("Setting Up Mods"), _("Setting up " + x + " for DDML. Please wait...")) 40 | try: 41 | mm_compat.ddmm_folder_setup(persistent.projects_directory, x) 42 | except: 43 | mm_compat.ddmm_traceback(x) 44 | mm_compat.ddmm_revert_folder_setup(persistent.projects_directory, x) 45 | failed_mods = 1 46 | 47 | if failed_mods != 0: 48 | interface.error(_("DDML encountered some errors with transferring some mods."), 49 | _("See {i}transfer_log.txt{/i} for more information. If the issue persists, contact the developer on Github.")) 50 | else: 51 | interface.info(_("DDML transferred all your mods from DDMM with no issues.")) 52 | 53 | mm_compat.ddmm_traceback_shutdown() 54 | project.manager.scan() 55 | 56 | jump preferences 57 | -------------------------------------------------------------------------------- /launcher/game/updater.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2020 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 | init python: 23 | import urllib2 24 | import ssl 25 | import json 26 | 27 | # This can be one of None, "available", "not-available", or "error". 28 | # 29 | # It must be None for a release. 30 | UPDATE_SIMULATE = os.environ.get("RENPY_UPDATE_SIMULATE", None) 31 | 32 | PUBLIC_KEY = "renpy_public.pem" 33 | 34 | CHANNELS_URL = "https://www.renpy.org/channels.json" 35 | 36 | version_tuple = renpy.version(tuple=True) 37 | 38 | def check_dlc(name): 39 | """ 40 | Returns true if the named dlc package is present. 41 | """ 42 | 43 | return name in updater.get_installed_packages() 44 | 45 | def add_dlc(name, restart=False): 46 | """ 47 | Adds the DLC package, if it doesn't already exist. 48 | 49 | Returns True if the DLC is installed, False otherwise. 50 | """ 51 | 52 | dlc_url = "http://update.renpy.org/{}/updates.json".format(".".join(str(i) for i in version_tuple[:-1])) 53 | 54 | state = updater.get_installed_state() 55 | 56 | if state is not None: 57 | base_name = state.get("sdk", {}).get('base_name', '') 58 | 59 | if base_name.startswith("renpy-nightly-"): 60 | dlc_url = "http://nightly.renpy.org/{}/updates.json".format(base_name[6:]) 61 | 62 | return renpy.invoke_in_new_context(updater.update, dlc_url, add=[name], public_key=PUBLIC_KEY, simulate=UPDATE_SIMULATE, restart=restart) 63 | 64 | filter_keywords = [] 65 | 66 | def decode_list(): 67 | with interface.error_handling(_("Decoding the mod list...")): 68 | ddmc_data = config.basedir + '/ddmc.json' 69 | with open(ddmc_data, 'r') as f: 70 | return json.load(f) 71 | 72 | def nsfw_tag(modList): 73 | for c in modList: 74 | if c['modNSFW'] and not persistent.nsfw: 75 | modList.remove(c) 76 | elif c['modNSFW'] and persistent.nsfw: 77 | c["modName"] = "{b}(NSFW){/b} " + c["modName"] 78 | 79 | screen update_channel(channels, criteria=None): 80 | 81 | frame: 82 | style_group "l" 83 | style "l_root" 84 | 85 | window: 86 | 87 | has viewport: 88 | scrollbars "vertical" 89 | mousewheel True 90 | 91 | has vbox 92 | 93 | frame style "l_alternate": 94 | has hbox xfill True 95 | text _("Mod List") style "l_label_text" 96 | 97 | frame style "l_label": 98 | has hbox xfill True 99 | 100 | text _("Select the mod you will like to download, then return to the home menu and install it with DDML."): 101 | style "l_small_text" 102 | size 15 103 | xpos INDENT 104 | 105 | add HALF_SPACER 106 | 107 | frame: 108 | style "l_alternate" 109 | style_group "l_small" 110 | 111 | has hbox 112 | 113 | textbutton _("Search") action Jump("search") 114 | 115 | hbox: 116 | frame: 117 | style "l_indent" 118 | xfill True 119 | 120 | has vbox 121 | 122 | if criteria is not None: 123 | 124 | python: 125 | filter_keywords.clear() 126 | 127 | if criteria != " " and not "," in criteria: 128 | filter_keywords.append(criteria) 129 | else: 130 | cs = criteria.split(",") 131 | for k in cs: 132 | filter_keywords.append(k.replace(" ", "")) 133 | 134 | python: 135 | chosen_channels = [] 136 | 137 | for c in channels: 138 | category_kwds = c["modSearch"] 139 | name_kwds = c["modName"] 140 | 141 | for e in filter_keywords: 142 | e = e.lower() 143 | 144 | if e in category_kwds or e in name_kwds.lower(): 145 | 146 | if c not in chosen_channels: 147 | 148 | chosen_channels.append(c) 149 | 150 | channels = chosen_channels 151 | 152 | text "Found {} mods that are tagged with the following search phrases: ".format( 153 | len(chosen_channels)) + ", ".join(filter_keywords) + "." style "l_small_text" 154 | 155 | add SPACER 156 | 157 | for c in channels: 158 | 159 | if c['modShow']: 160 | 161 | textbutton c["modName"].replace("[", "[[").replace("]", "]]"): 162 | text_style "mod_search_name" 163 | action OpenURL(c["modUploadURL"]) 164 | 165 | add HALF_SPACER 166 | 167 | text c["modShortDescription"] style "mod_search_small_text" 168 | 169 | python: 170 | playTime = "Playtime: {i}" 171 | 172 | if c["modPlayTimeHours"]: 173 | 174 | playTime += str(c["modPlayTimeHours"]) + " hour" 175 | 176 | if c["modPlayTimeHours"] > 1: 177 | playTime += "s" 178 | 179 | playTime += " " 180 | 181 | if c["modPlayTimeMinutes"]: 182 | 183 | playTime += str(c["modPlayTimeMinutes"]) + " minute" 184 | 185 | if c["modPlayTimeMinutes"] > 1: 186 | playTime += "s" 187 | 188 | if not c["modPlayTimeHours"] and c["modPlayTimeMinutes"] == 0: 189 | 190 | playTime += "Unknown" 191 | 192 | add SPACER 193 | 194 | text playTime + "{/i}" style "mod_search_playtime" 195 | 196 | add SPACER 197 | add SEPARATOR2 at mod_search_seperator 198 | add SPACER 199 | 200 | textbutton _("Return") action Jump("front_page") style "l_left_button" 201 | 202 | label update: 203 | 204 | python hide: 205 | interface.processing(_("Fetching the mod list...")) 206 | 207 | # Disabled due to obsoleteness but it may be useful in code someday 208 | # Thanks Vige! 209 | 210 | # with interface.error_handling(_("Downloading a updated mod list...")): 211 | # url = "https://www.dokidokimodclub.com/api/mod/" 212 | # headers = {'Authorization': 'Api-Key [REDACTED]'} 213 | # context = ssl._create_unverified_context() 214 | # req = urllib2.Request(url=url, headers=headers) 215 | # response = urllib2.urlopen(req, context=context) 216 | # the_page = response.read() 217 | 218 | channels = decode_list() 219 | nsfw_tag(channels) 220 | 221 | renpy.call_screen("update_channel", channels, None) 222 | 223 | jump front_page 224 | 225 | label search: 226 | 227 | python hide: 228 | 229 | while True: 230 | 231 | criteria = "" 232 | criteria = interface.input( 233 | _("Search a Mod"), 234 | _("Type in the mod name or search phrases (separated by commas) of the mod that you are looking for."), 235 | allow=interface.PROJECT_LETTERS + ",", 236 | cancel=Jump("front_page"), 237 | default="", 238 | ) 239 | 240 | if criteria == "": 241 | interface.error(_("Your search cannot be left empty. Please try again."), label=None) 242 | continue 243 | 244 | channels = decode_list() 245 | nsfw_tag(channels) 246 | 247 | renpy.call_screen("update_channel", channels, criteria) 248 | break 249 | 250 | jump front_page 251 | 252 | style mod_search_small_text is l_small_text: 253 | size 18 254 | 255 | style mod_search_name is l_left_button_text: 256 | size 20 257 | underline True 258 | 259 | style mod_search_playtime is l_small_text: 260 | size 16 261 | 262 | transform mod_search_seperator: 263 | ysize 5 -------------------------------------------------------------------------------- /launcher/game/util.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2020 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 | init -1 python in util: 23 | import os 24 | 25 | def listdir(d): 26 | """ 27 | Returns a list of files and directories in `d` that are accessible with 28 | the filesystem encoding. 29 | """ 30 | 31 | try: 32 | d = renpy.fsdecode(d) 33 | 34 | if not os.path.isdir(d): 35 | return [ ] 36 | 37 | return [ i for i in os.listdir(d) if isinstance(i, str) ] 38 | 39 | except: 40 | return [ ] 41 | 42 | def walk(directory, base=None): 43 | """ 44 | Walks through the directories and files underneath `directory`, 45 | yielding (name, isdir) tuples. The names are given relative to 46 | `base`, which defaults to `directory` if None. 47 | """ 48 | 49 | directory = renpy.fsdecode(directory) 50 | 51 | if base is None: 52 | base = directory 53 | else: 54 | base = renpy.fsdecode(base) 55 | 56 | for subdir, directories, files in os.walk(directory): 57 | for fn in directories: 58 | if not isinstance(fn, unicode): 59 | continue 60 | 61 | fullfn = os.path.join(subdir, fn) 62 | relfn = os.path.relpath(fullfn, base) 63 | 64 | relfn = relfn.replace("\\", "/") 65 | 66 | yield relfn, True 67 | 68 | for fn in files: 69 | if not isinstance(fn, unicode): 70 | continue 71 | 72 | fullfn = os.path.join(subdir, fn) 73 | relfn = os.path.relpath(fullfn, base) 74 | 75 | relfn = relfn.replace("\\", "/") 76 | 77 | yield relfn, False 78 | 79 | -------------------------------------------------------------------------------- /launcher/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bronya-Rand/DDML/e41e7b85aef8a16ceec95946ca6f67f300e5d277/launcher/icon.icns -------------------------------------------------------------------------------- /launcher/project.json: -------------------------------------------------------------------------------- 1 | {"type": "hidden", "renamed_all": true, "renamed_steam": true, "force_recompile": true, "build_update": true, "packages": ["linux", "mac", "win", "sdk", "source", "jedit", "editra-mac", "editra-linux", "editra-windows", "pc", "raspi", "atom-linux", "atom-mac", "atom-windows", "rapt", "renios"], "add_from": true} --------------------------------------------------------------------------------