├── .vscode └── settings.json ├── images ├── sdk.png ├── deck-1.png ├── deck-2.png ├── deck-3.png └── deck-4.png ├── update_renpy_steam.py ├── files ├── 00achievement.rpy └── 00steam.rpy └── README.rst /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "esbonio.sphinx.confDir": "" 3 | } 4 | -------------------------------------------------------------------------------- /images/sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renpy/steam-deck-guide/HEAD/images/sdk.png -------------------------------------------------------------------------------- /images/deck-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renpy/steam-deck-guide/HEAD/images/deck-1.png -------------------------------------------------------------------------------- /images/deck-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renpy/steam-deck-guide/HEAD/images/deck-2.png -------------------------------------------------------------------------------- /images/deck-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renpy/steam-deck-guide/HEAD/images/deck-3.png -------------------------------------------------------------------------------- /images/deck-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renpy/steam-deck-guide/HEAD/images/deck-4.png -------------------------------------------------------------------------------- /update_renpy_steam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import traceback 4 | import urllib.request 5 | import os 6 | import zipfile 7 | 8 | def download(fn): 9 | """ 10 | Downloads a file and installs it into Ren'Py. 11 | """ 12 | 13 | dn = os.path.dirname(fn) 14 | if not os.path.exists(dn): 15 | print("For {}, the directory doesn't exist, not doing anything.".format(fn)) 16 | return 17 | 18 | basename = os.path.basename(fn) 19 | 20 | url = "https://raw.githubusercontent.com/renpy/steam-deck-guide/master/files/" + basename 21 | 22 | print("Downloading", url, "to", fn + ".") 23 | 24 | with urllib.request.urlopen(url) as response: 25 | data = response.read().decode("utf-8") 26 | 27 | with open(fn, "w", encoding="utf-8") as f: 28 | f.write(data) 29 | 30 | # The path to steamworks_sdk.zip 31 | steamworks_zip = "" 32 | 33 | def find_steamworks_zip(): 34 | global steamworks_zip 35 | 36 | steamworks_zips = [ 37 | i for i in os.listdir(".") 38 | if i.lower().startswith("steamworks_sdk") and i.lower().endswith(".zip") ] 39 | 40 | steamworks_zips.sort() 41 | 42 | if steamworks_zips: 43 | steamworks_zip = steamworks_zips[-1] 44 | print("Found steamworks at", steamworks_zip + ".") 45 | else: 46 | print("Could not find steamworks.") 47 | raise SystemExit() 48 | 49 | def steamworks(src, dst): 50 | """ 51 | Unpacks a steamworks file from `src` to `dst` if required. 52 | """ 53 | 54 | # Add a prefix to src, to save typing. 55 | src = "sdk/redistributable_bin/" + src 56 | dst = dst + "/" + os.path.basename(src) 57 | 58 | dn = os.path.dirname(dst) 59 | if not os.path.exists(dn): 60 | print("For {}, the directory doesn't exist, not doing anything.".format(dst)) 61 | return 62 | 63 | with zipfile.ZipFile(steamworks_zip) as zf: 64 | with zf.open(src) as f: 65 | data = f.read() 66 | 67 | with open(dst, "wb") as f: 68 | f.write(data) 69 | 70 | print("Unpacked {}.".format(dst)) 71 | 72 | 73 | def main(): 74 | 75 | # Change to the directory containing this file. 76 | os.chdir(os.path.dirname(os.path.realpath(__file__))) 77 | 78 | if not os.path.exists("renpy/common"): 79 | print("This file is not inside a Ren'Py SDK or Game.") 80 | return 81 | else: 82 | print("Ren'Py found.") 83 | 84 | # Find steamworks. 85 | find_steamworks_zip() 86 | 87 | # Download the Ren'Py files. 88 | 89 | print() 90 | print("Downloading Ren'Py files:") 91 | print() 92 | 93 | download("renpy/common/00achievement.rpy") 94 | download("renpy/common/00steam.rpy") 95 | download("lib/python2.7/steamapi.py") 96 | download("lib/pythonlib2.7/steamapi.py") 97 | 98 | print() 99 | print("Unpacking steamworks:") 100 | print() 101 | 102 | steamworks("linux32/libsteam_api.so", "lib/linux-i686") 103 | steamworks("linux64/libsteam_api.so", "lib/linux-x86_64") 104 | steamworks("osx/libsteam_api.dylib", "lib/darwin-x86_64") 105 | steamworks("osx/libsteam_api.dylib", "lib/mac-x86_64") 106 | steamworks("steam_api.dll", "lib/windows-i686") 107 | steamworks("win64/steam_api64.dll", "lib/windows-x86_64") 108 | 109 | 110 | print() 111 | print("The Steam API was updated. You can delete this file and {}.".format(steamworks_zip)) 112 | print("I'm making a note here, huge success.") 113 | print() 114 | 115 | 116 | if __name__ == "__main__": 117 | try: 118 | main() 119 | except: 120 | traceback.print_exc() 121 | 122 | print("") 123 | print("") 124 | input("Press enter to end this program.") 125 | -------------------------------------------------------------------------------- /files/00achievement.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2022 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 | 23 | init -1500 python in achievement: 24 | from store import persistent, renpy, config, Action 25 | 26 | # A list of backends that have been registered. 27 | backends = [ ] 28 | 29 | class Backend(object): 30 | """ 31 | Achievement backends should inherit from this class, so new methods 32 | will be ignored. 33 | """ 34 | 35 | 36 | def register(self, name, **kwargs): 37 | """ 38 | Called to register a new achievement. 39 | """ 40 | 41 | def grant(self, name): 42 | """ 43 | Grants the achievement with `name`, if it has not already been 44 | granted. 45 | """ 46 | 47 | def clear(self, name): 48 | """ 49 | Clears the achievement with `name`, if it has been granted. 50 | """ 51 | 52 | def clear_all(self): 53 | """ 54 | Clears all achievements. 55 | """ 56 | 57 | def progress(self, name, complete): 58 | """ 59 | Reports progress towards the achievement with `name`. 60 | """ 61 | 62 | def has(self, name): 63 | """ 64 | Returns true if the achievement with `name` is unlocked. 65 | """ 66 | 67 | return False 68 | 69 | class PersistentBackend(Backend): 70 | """ 71 | A backend that stores achievements in persistent._achievements. 72 | """ 73 | 74 | def __init__(self): 75 | if persistent._achievements is None: 76 | persistent._achievements = _set() 77 | 78 | if persistent._achievement_progress is None: 79 | persistent._achievement_progress = _dict() 80 | 81 | self.stat_max = { } 82 | 83 | def register(self, name, stat_max=None, **kwargs): 84 | if stat_max: 85 | self.stat_max[name] = stat_max 86 | 87 | def grant(self, name): 88 | persistent._achievements.add(name) 89 | 90 | def clear(self, name): 91 | persistent._achievements.discard(name) 92 | if name in persistent._achievement_progress: 93 | del persistent._achievement_progress[name] 94 | 95 | def clear_all(self): 96 | persistent._achievements.clear() 97 | persistent._achievement_progress.clear() 98 | 99 | def has(self, name): 100 | return name in persistent._achievements 101 | 102 | def progress(self, name, completed): 103 | current = persistent._achievement_progress.get(name, 0) 104 | 105 | if (current is not None) and (current >= completed): 106 | return 107 | 108 | persistent._achievement_progress[name] = completed 109 | 110 | if name not in self.stat_max: 111 | if config.developer: 112 | raise Exception("To report progress, you must register {} with a stat_max.".format(name)) 113 | else: 114 | return 115 | 116 | if completed >= self.stat_max[name]: 117 | self.grant(name) 118 | 119 | def merge(old, new, current): 120 | if old is None: 121 | old = set() 122 | 123 | if new is None: 124 | new = set() 125 | 126 | return old | new 127 | 128 | def merge_progress(old, new, current): 129 | 130 | if old is None: 131 | old = { } 132 | if new is None: 133 | new = { } 134 | 135 | rv = _dict() 136 | rv.update(old) 137 | 138 | for k in new: 139 | if k not in rv: 140 | rv[k] = new[k] 141 | else: 142 | rv[k] = max(new[k], rv[k]) 143 | 144 | return rv 145 | 146 | renpy.register_persistent("_achievements", merge) 147 | renpy.register_persistent("_achievement_progress", merge_progress) 148 | 149 | backends.append(PersistentBackend()) 150 | 151 | # The Steam back-end has been moved to 00steam.rpy. 152 | 153 | def register(name, **kwargs): 154 | """ 155 | :doc: achievement 156 | 157 | Registers an achievement. Achievements are not required to be 158 | registered, but doing so allows one to pass information to the 159 | backends. 160 | 161 | `name` 162 | The name of the achievement to register. 163 | 164 | The following keyword parameters are optional. 165 | 166 | `steam` 167 | The name to use on steam. If not given, defaults to `name`. 168 | 169 | `stat_max` 170 | The integer value of the stat at which the achievement unlocks. 171 | 172 | `stat_modulo` 173 | If the progress modulo `stat_max` is 0, progress is displayed 174 | to the user. For example, if stat_modulo is 10, progress will 175 | be displayed to the user when it reaches 10, 20, 30, etc. If 176 | not given, this defaults to 0. 177 | """ 178 | 179 | for i in backends: 180 | i.register(name, **kwargs) 181 | 182 | def grant(name): 183 | """ 184 | :doc: achievement 185 | 186 | Grants the achievement with `name`, if it has not already been 187 | granted. 188 | """ 189 | 190 | if not has(name): 191 | for i in backends: 192 | i.grant(name) 193 | 194 | def clear(name): 195 | """ 196 | :doc: achievement 197 | 198 | Clears the achievement with `name`. 199 | """ 200 | 201 | for i in backends: 202 | i.clear(name) 203 | 204 | def clear_all(): 205 | """ 206 | :doc: achievement 207 | 208 | Clears all achievements. 209 | """ 210 | 211 | for i in backends: 212 | i.clear_all() 213 | 214 | def get_progress(name): 215 | """ 216 | :doc: achievement 217 | 218 | Returns the current progress towards the achievement identified 219 | with `name`, or 0 if no progress has been registered for it or if 220 | the achievement is not known. 221 | """ 222 | 223 | return persistent._achievement_progress.get(name, 0) 224 | 225 | def progress(name, complete, total=None): 226 | """ 227 | :doc: achievement 228 | :args: (name, complete) 229 | 230 | Reports progress towards the achievement with `name`, if that 231 | achievement has not been granted. The achievement must be defined 232 | with a completion amount. 233 | 234 | `name` 235 | The name of the achievement. This should be the name of the 236 | achievement, and not the stat. 237 | 238 | `complete` 239 | An integer giving the number of units completed towards the 240 | achievement. 241 | """ 242 | 243 | if has(name): 244 | return 245 | 246 | for i in backends: 247 | i.progress(name, complete) 248 | 249 | def grant_progress(name, complete, total=None): 250 | progress(name, complete) 251 | 252 | def has(name): 253 | """ 254 | :doc: achievement 255 | 256 | Returns true if the player has been granted the achievement with 257 | `name`. 258 | """ 259 | 260 | for i in backends: 261 | if i.has(name): 262 | return True 263 | 264 | return False 265 | 266 | def sync(): 267 | """ 268 | :doc: achievement 269 | 270 | Synchronizes registered achievements between local storage and 271 | other backends. (For example, Steam.) 272 | """ 273 | 274 | for a in persistent._achievements: 275 | for i in backends: 276 | if not i.has(a): 277 | i.grant(a) 278 | 279 | class Sync(Action): 280 | """ 281 | :doc: achievement 282 | 283 | An action that calls achievement.sync(). This is only sensitive if 284 | achievements are out of sync. 285 | """ 286 | 287 | def __call__(self): 288 | sync() 289 | 290 | def get_sensitive(self): 291 | for a in persistent._achievements: 292 | for i in backends: 293 | if not i.has(a): 294 | return True 295 | return False 296 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This is currently a draft. It might be incorrect, or it might be correct and 2 | give bad advice. Here be dragons.** 3 | 4 | ============================== 5 | Ren'Py on Steam Deck - A Guide 6 | ============================== 7 | 8 | The goal of this document is to try to help Ren'Py games pass Steam Deck 9 | compatibility review, and gain "Great on Deck" status, with a minimum amount 10 | of effort from the game's developer. 11 | 12 | This isn't a complete guide to getting on Steam itself. There's a very good 13 | document, `Ren’Py Visual Novels on Steam: A Step-by-Step Guide `_ by 14 | Bob Conway (bobcgames) that covers this process, that I won't repeat here. 15 | 16 | This also isn't a direct substitute for Valve's `Steam Deck documentation `_, 17 | especially the sections on `How to load and run games on Steam Deck `_ and 18 | the `Steam Deck Compatibility Review Process `_. 19 | 20 | Rather, this is a discussion about how to make sure a Ren'Py game runs on Steam 21 | Deck and satisifies those guidelines. Where possible, I want to try to get the 22 | game running with minimal changes, though some games will require a re-release. 23 | 24 | Linux Build 25 | =========== 26 | 27 | The Steam Deck is running a build of Linux. Ren'Py is developed on Linux, and 28 | Ren'Py runs well on Linux. It makes a lot of sense to uploade a SteamOS + Linux 29 | build of your game, and make sure that's the build that's submitted for 30 | compatibility testing. 31 | 32 | I've seen some problem when running a Windows build through Steam Play and 33 | Proton, and while it may work most of the time, having an unnecessary translation 34 | layer isn't likely to help. 35 | 36 | 37 | Controller Support 38 | ================== 39 | 40 | The first requirement is that "Your game must support Steam Deck's physical 41 | controls. The default controller configuration must provide users with the 42 | ability to access all content. Players must not need to adjust any in-game settings 43 | in order to enable controller support or this configuration." 44 | 45 | Recommended Controller Mapping 46 | ------------------------------- 47 | 48 | Since Ren'Py 6.99.6, Ren'Py games include native controller support for the 49 | Xinput-style controller that is emulated by the Steam Input system. The 50 | default mapping of the controls is: 51 | 52 | Right Trigger, A 53 | Dismiss dialogue, select buttons and bars. 54 | 55 | Y 56 | Hide the interface. 57 | 58 | Left Shoulder, Left Trigger, Back 59 | Perform a rollback. 60 | 61 | Right Shoulder 62 | Perform a roll-forward. 63 | 64 | Menu/Start 65 | Enter and exit the game menu. 66 | 67 | Left Dpad, Left Stick, Right stick. 68 | Navigate through the game's interface. 69 | 70 | In addition to this, on the Steam Deck, it's suggested that the right touchpad 71 | move the mouse around the screen, and pressing the right touchpad causes a click 72 | to occur. 73 | 74 | 75 | I'd suggest most games try to map their controllers in this manner, as consitency 76 | will help players adapt to new games. 77 | 78 | Ren'Py 6.99.6 and Higher 79 | ------------------------- 80 | 81 | Since Ren'Py 6.99.6, Ren'Py has had built-in support for controllers, and 82 | that support should work well with Steam Deck, and especially the "Generic Gamepad" 83 | controller configuration. To set this, visit `your dashboard `_, 84 | choose your game: 85 | 86 | .. image:: images/deck-1.png 87 | 88 | Then navigate to "Edit SteamWorks Settings": 89 | 90 | .. image:: images/deck-2.png 91 | 92 | And finally, navigate to "Applications", "Steam Input". 93 | 94 | .. image:: images/deck-3.png 95 | 96 | You can then opt into the "Generic Gamepad" configuration, make sure all kinds 97 | of controllers are opted into Steam Input. 98 | 99 | .. image:: images/deck-4.png 100 | 101 | When you select "Save", your game should now support generic gamepad input. 102 | 103 | **Valve: Do they have to publish after saving?** 104 | 105 | 106 | Ren'Py 6.99.5 and Lower 107 | ----------------------- 108 | 109 | **Valve: How do I get the URL for a controller configuration that I created on te deck? Can it be shared between apps?** 110 | 111 | Older Ren'Py games don't support the controller input. However, controller 112 | support is very similar to the way Ren'Py navigates games using the keyboard, 113 | and so it's possible to use Steam Input to map the Steam Deck's input to 114 | the keyboard, allowing the game to be accessed. 115 | 116 | .. image:: images/deck-1.png 117 | 118 | Then navigate to "Edit SteamWorks Settings": 119 | 120 | .. image:: images/deck-2.png 121 | 122 | And finally, navigate to "Applications", "Steam Input". 123 | 124 | .. image:: images/deck-3.png 125 | 126 | For the default configuration, choose "Custom Configuraton (Hosted on Steam Workshop)", 127 | then "Add Custom Configuration", and then enter in the URL for the configuration. 128 | 129 | When you select "Save", your game should now support mapping the gamepad input to 130 | the keyboard. 131 | 132 | 133 | Keyboard Support 134 | ================= 135 | 136 | Another requirement is that "If your game requires text input (eg., for naming a 137 | character or a save file), you must either use a Steamworks API for text entry 138 | to open the on-screen keyboard for players using a controller, or have your 139 | own built-in entry that allows users to enter text in their language using only 140 | a controller." 141 | 142 | **Many games will trivially satisfy this requirement, by not requiring text 143 | input.** In that case, great - you can ignore the rest of ths section. 144 | 145 | If your game requires keyboard input - perhaps it prompts for the main character's 146 | name, what you need to do depends on the version of Ren'Py that you are running. 147 | 148 | Ren'Py 7.5.0 or Later 149 | --------------------- 150 | 151 | (Note that this version is not released at the time of the writing of this 152 | document.) 153 | 154 | New versions of Ren'Py include built-in support for managing the Steam Deck 155 | keyboard. Ren'Py will automatically determine if the floating keyboard needs 156 | to be show, and if text input is required, Ren'Py will show the keyboard. 157 | When text input ends, Ren'Py will shift down the keyboard. 158 | 159 | There are a few variables that can be set to customize this 160 | process. These live in the _renpysteam namespace, which is the module that 161 | implements them. 162 | 163 | \_renpysteam.keyboard\_mode = "always" 164 | This should be one of "always", "once", and "never". 165 | 166 | * "always" means the keyboard is always show when text input is requested. 167 | * "once" means the keyboard is shown once per interaction. If the keyboard is hidden, it will not be automatically re-show. (It can be shown again with Steam+X.) 168 | * "never" means the keyboard should not be automatically managed. 169 | 170 | \_renpysteam.keyboard\_shift = True 171 | If True, interface layers (by default "screens", "transient", and "overlay") 172 | are shifted upwards so the input text is visible to the user. The input text is shifted 173 | up so that its baseline is aligned with \_renpysteam.text\_baseline. Input text is 174 | never shifted down. 175 | 176 | \_renpysteam.keyboard\_baseline = 0.5 177 | This is the baseline that input text is shifted to. 178 | 179 | These can be set with the define statement:: 180 | 181 | define _renpysteam.keyboard_shift = False 182 | 183 | Ren'Py 7.4.11 or Earlier 184 | ------------------------ 185 | 186 | To use the floating keyboard with Ren'Py 7.4.11 or earlier, it's necessary 187 | to upgrade Ren'Py's steam support to the version included in Ren'Py 7.5. 188 | This upgraded has been tested to work with Ren'Py 7.3.5 and 7.4.11, and will 189 | likely work with some but not all older versions. 190 | 191 | The upgrade may be performed on a Ren'Py SDK or an unpacked Ren'Py game 192 | distribution. For the purpose of these instructions, the base directory 193 | is the directory with the renpy.sh or gamename.sh file in it. 194 | 195 | * Install any version of `Python 3 `_ on your computer. 196 | * Download `update_renpy_steam.py `_ and place it into the base directory. 197 | * Download the latest Steamworks SDK zip file from `Steamworks `_, and place it into the base directory. 198 | 199 | The Steamworks SDK should have a filename like steamworks_sdk_153a.zip. When 200 | ready, your base directory will look like: 201 | 202 | .. image:: images/sdk.png 203 | 204 | You'll then want to run update_renpy_steam.py. Make sure you run in in Python, 205 | not open it in a text editor - you may need to right click and open with 206 | Python to be sure of this. When this script completes without errors, your 207 | game or the SDK is updated. You should delete steamworks_sdk_153a.zip and update_renpy_steam.py so these 208 | files won't be distributed. 209 | 210 | After this step, you have the same Steam support that's included in Ren'Py 211 | 7.5.0 and later, with support for the floating keyboard. 212 | 213 | 214 | Variant 215 | ======= 216 | 217 | When the Steam support is at the 7.5.0 version (or upgraded as described 218 | above), Ren'Py will define a "steam_deck" `screen variant `_ when the Steam Deck 219 | hardware is detected. 220 | 221 | This can be used to select alternate screens, or checked with the renpy.variant 222 | function:: 223 | 224 | screen test(): 225 | 226 | vbox: 227 | 228 | text "Something common." 229 | 230 | if renpy.variant("steam_deck"): 231 | text "On Steam Deck." 232 | else: 233 | text "On other platform." 234 | 235 | 236 | Other Requirements 237 | ================== 238 | 239 | Be sure to check `valve's page `_ to make 240 | sure these requirements haven't been updated. 241 | 242 | Controller Glyphs 243 | ----------------- 244 | 245 | "When using Steam Deck's physical controls, on-screen glyphs must either match 246 | Deck button names, or match Xbox 360/One button names. Mouse and keyboard 247 | glyphs should not be shown if they are not the active input. Interacting 248 | with any physical Deck controls using the default configuration must not 249 | show non-controller glyphs." 250 | 251 | Ren'Py does not display glyphs by default, so this requirement is trivially 252 | satisifed. If your game displays glyphs, it will have to be changed to make 253 | sure the correct glyphs are displayed, perhaps using screen variants. 254 | 255 | Resolution Support 256 | ------------------ 257 | 258 | "The game must run at a resolution supported by Steam Deck." 259 | 260 | All modern Ren'Py games adjust to the size of the window the game is 261 | displayed in. 262 | 263 | Default Configuration 264 | --------------------- 265 | 266 | "The game must ship with a default configuration on Deck that results in a 267 | playable framerate." 268 | 269 | The Steam Deck hardware is fairly powerful, so this is unlikely to be a 270 | problem. Please let the reviewers know that Ren'Py may vary the framerate, 271 | lowering it to save power on static scenes. 272 | 273 | Text Legibility 274 | ---------------- 275 | 276 | "interface text must be easily readable at a distance of 12 inches/30 cm from the screen. 277 | In other words, the smallest on-screen font character should never fall below 9 pixels 278 | in height at 1280x800." 279 | 280 | Ren'Py's default text is around 22px high at 1280x720, and text scales with 281 | window size. There should be ample margin for size changes. 282 | 283 | It may make sense for some games to activate the small variant, meant for mobile 284 | devices, when on a Steam Deck, using::: 285 | 286 | init python: 287 | if renpy.variant("steam_deck"): 288 | config.variants.remove("large") 289 | config.variants.insert(0, "small") 290 | 291 | This could be the case if the game wants to re-use a moble UI on Steam Deck. 292 | 293 | No Device Compatibility Warnings 294 | -------------------------------- 295 | 296 | "the app must not present the user with information that the Deck software 297 | (ie., specific Linux distribution) or hardware (ie., GPU) is unsupported." 298 | 299 | Ren'Py doesn't perform such checks. 300 | 301 | Launchers 302 | --------- 303 | 304 | "For games with launchers, those launchers also must meet the requirements listed here" 305 | 306 | Ren'Py games do not require a launcher to run. 307 | 308 | 309 | Troubleshooting 310 | =============== 311 | 312 | *Typed text shows up as '1's.* This seems to be a problem with Ren'Py and Steam Play. It's suggested to create 313 | a Steam OS + Linux build to ensure that input functions without a translation 314 | layer. 315 | -------------------------------------------------------------------------------- /files/00steam.rpy: -------------------------------------------------------------------------------- 1 | # Copyright 2004-2022 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 | python early: 23 | 24 | # Should steam be enabled? 25 | config.enable_steam = True 26 | 27 | 28 | init -1499 python in _renpysteam: 29 | 30 | import collections 31 | import time 32 | 33 | def retrieve_stats(): 34 | """ 35 | :doc: steam_stats 36 | 37 | Retrieves achievements and statistics from Steam. 38 | """ 39 | """ 40 | `callback` will be 41 | called with no parameters if and when the statistics become available. 42 | """ 43 | 44 | steamapi.SteamUserStats().RequestCurrentStats() 45 | 46 | 47 | def store_stats(): 48 | """ 49 | :doc: steam_stats 50 | 51 | Stores statistics and achievements on the Steam server. 52 | """ 53 | 54 | steamapi.SteamUserStats().StoreStats() 55 | 56 | 57 | def list_achievements(): 58 | """ 59 | :doc: steam_stats 60 | 61 | Returns a list of achievement names. 62 | """ 63 | 64 | rv = [ ] 65 | 66 | na = steamapi.SteamUserStats().GetNumAchievements() 67 | 68 | for i in range(na): 69 | rv.append(steamapi.SteamUserStats().GetAchievementName(i).decode("utf-8")) 70 | 71 | return rv 72 | 73 | 74 | def get_achievement(name): 75 | """ 76 | :doc: steam_stats 77 | 78 | Gets the state of the achievements with `name`. This returns True if the 79 | achievement has been granted, False if it hasn't, and None if the achievement 80 | is unknown or an error occurs. 81 | """ 82 | 83 | from ctypes import byref, c_bool 84 | 85 | rv = c_bool(False) 86 | 87 | if not steamapi.SteamUserStats().GetAchievement(name.encode("utf-8"), byref(rv)): 88 | return None 89 | 90 | return rv.value 91 | 92 | 93 | def grant_achievement(name): 94 | """ 95 | :doc: steam_stats 96 | 97 | Grants the achievement with `name`. Call :func:`_renpysteam.store_stats` to 98 | push this change to the server. 99 | """ 100 | 101 | return steamapi.SteamUserStats().SetAchievement(name.encode("utf-8")) 102 | 103 | 104 | def clear_achievement(name): 105 | """ 106 | :doc: steam_stats 107 | 108 | Clears the achievement with `name`. Call :func:`_renpysteam.store_stats` to 109 | push this change to the server. 110 | """ 111 | 112 | return steamapi.SteamUserStats().ClearAchievement(name.encode("utf-8")) 113 | 114 | 115 | def indicate_achievement_progress(name, cur_progress, max_progress): 116 | """ 117 | :doc: steam_stats 118 | 119 | Indicates achievement progress to the user. This does *not* unlock the 120 | achievement. 121 | """ 122 | 123 | return steamapi.SteamUserStats().IndicateAchievementProgress(name.encode("utf-8"), cur_progress, max_progress) 124 | 125 | 126 | def get_float_stat(name): 127 | """ 128 | :doc: steam_stats 129 | 130 | Returns the value of the stat with `name`, or None if no such stat 131 | exits. 132 | """ 133 | 134 | from ctypes import c_float, byref 135 | 136 | rv = c_float(0) 137 | 138 | if not steamapi.SteamUserStats().GetStatFloat(name.encode("utf-8"), byref(rv)): 139 | return None 140 | 141 | return rv.value 142 | 143 | 144 | def set_float_stat(name, value): 145 | """ 146 | :doc: steam_stats 147 | 148 | Sets the value of the stat with `name`, which must have the type of 149 | FLOAT. Call :func:`_renpysteam.store_stats` to push this change to the 150 | server. 151 | """ 152 | 153 | return steamapi.SteamUserStats().SetStat(name.encode("utf-8"), v) 154 | 155 | 156 | def get_int_stat(name): 157 | """ 158 | :doc: steam_stats 159 | 160 | Returns the value of the stat with `name`, or None if no such stat 161 | exits. 162 | """ 163 | from ctypes import c_float, byref 164 | 165 | rv = c_int(0) 166 | 167 | if not steamapi.SteamUserStats().GetStatInt32(name.encode("utf-8"), byref(rv)): 168 | return None 169 | 170 | return rv.value 171 | 172 | 173 | def set_int_stat(name, value): 174 | """ 175 | :doc: steam_stats 176 | 177 | Sets the value of the stat with `name`, which must have the type of 178 | INT. Call :func:`_renpysteam.store_stats` to push this change to the 179 | server. 180 | """ 181 | 182 | return steamapi.SteamUserStats().SetStatInt32(name.encode("utf-8"), v) 183 | 184 | 185 | ########################################################################### Apps 186 | 187 | def is_subscribed_app(appid): 188 | """ 189 | :doc: steam_apps 190 | 191 | Returns true if the user owns the app with `appid`, and false otherwise. 192 | """ 193 | 194 | return steamapi.SteamApps().BIsSubscribedApp(appid) 195 | 196 | 197 | def get_current_game_language(): 198 | """ 199 | :doc: steam_apps 200 | 201 | Return the name of the language the user has selected. 202 | """ 203 | 204 | return steamapi.SteamApps().GetCurrentGameLanguage().decode("utf-8") 205 | 206 | 207 | def get_steam_ui_language(): 208 | """ 209 | :doc: steam_apps 210 | 211 | Return the name of the language the steam UI is using. 212 | """ 213 | 214 | return steamapi.SteamUtils().GetSteamUILanguage().decode("utf-8") 215 | 216 | 217 | def get_current_beta_name(): 218 | """ 219 | :doc: steam_apps 220 | 221 | Returns the name of the current beta, or None if it can't. 222 | """ 223 | 224 | from ctypes import create_string_buffer, byref 225 | 226 | rv = create_string_buffer(256) 227 | 228 | if not steamapi.SteamApps().GetCurrentBetaName(byref(rv), 256): 229 | return None 230 | 231 | return rv.value.decode("utf-8") 232 | 233 | 234 | def dlc_installed(appid): 235 | """ 236 | :doc: steam_apps 237 | 238 | Returns True if `dlc` is installed, or False otherwise. 239 | """ 240 | 241 | return steamapi.SteamApps().BIsDlcInstalled(appid) 242 | 243 | 244 | def install_dlc(appid): 245 | """ 246 | :doc: steam_apps 247 | 248 | Requests the DLC with `appid` be installed. 249 | """ 250 | 251 | steamapi.SteamApps().InstallDLC(appid) 252 | 253 | 254 | def uninstall_dlc(appid): 255 | """ 256 | :doc: steam_apps 257 | 258 | Requests that the DLC with `appid` be uninstalled. 259 | """ 260 | 261 | steamapi.SteamApps().UninstallDLC(appid) 262 | 263 | 264 | def dlc_progress(appid): 265 | """ 266 | :doc: steam_apps 267 | 268 | Reports the progress towards DLC download completion. 269 | 270 | """ 271 | 272 | from ctypes import c_ulonglong, byref 273 | 274 | done = c_ulonglong(0) 275 | total = c_ulonglong(0) 276 | 277 | if steamapi.SteamApps().GetDlcDownloadProgress(appid, byref(done), byref(total)): 278 | return done.value, total.value 279 | else: 280 | return None 281 | 282 | 283 | def get_app_build_id(): 284 | """ 285 | :doc: steam_apps 286 | 287 | Returns the build ID of the installed game. 288 | """ 289 | 290 | return steamapi.SteamApps().GetAppBuildId() 291 | 292 | 293 | ######################################################################## Overlay 294 | 295 | def is_overlay_enabled(): 296 | """ 297 | :doc: steam_overlay 298 | 299 | Returns true if the steam overlay is enabled. (This might take a while to 300 | return true once the game starts.) 301 | """ 302 | 303 | return steamapi.SteamUtils().IsOverlayEnabled() 304 | 305 | 306 | def overlay_needs_present(): 307 | """ 308 | :doc: steam_overlay 309 | 310 | Returns true if the steam overlay is enabled. (This might take a while to 311 | return true once the game starts.) 312 | """ 313 | 314 | return steamapi.SteamUtils().BOverlayNeedsPresent() 315 | 316 | 317 | def set_overlay_notification_position(position): 318 | """ 319 | :doc: steam_overlay 320 | 321 | Sets the position of the steam overlay. `Position` should be one of 322 | _renpysteam.POSTION_TOP_LEFT, .POSITION_TOP_RIGHT, .POSITION_BOTTOM_LEFT, 323 | or .POSITION_BOTTOM_RIGHT. 324 | """ 325 | 326 | steamapi.SteamUtils().SetOverlayNotificationPosition(position) 327 | 328 | 329 | def activate_overlay(dialog): 330 | """ 331 | :doc: steam_overlay 332 | 333 | Activates the Steam overlay. 334 | 335 | `dialog` 336 | The dialog to open the overlay to. One of "Friends", "Community", 337 | "Players", "Settings", "OfficialGameGroup", "Stats", "Achievements" 338 | """ 339 | 340 | steamapi.SteamFriends().ActivateGameOverlay(dialog.encode("utf-8")) 341 | 342 | 343 | def activate_overlay_to_web_page(url): 344 | """ 345 | :doc: steam_overlay 346 | 347 | Activates the Steam overlay, and opens the web page at `url`. 348 | """ 349 | 350 | steamapi.SteamFriends().ActivateGameOverlayToWebPage(url.encode("utf-8")) 351 | 352 | def activate_overlay_to_store(appid, flag=None): 353 | """ 354 | :doc: steam_overlay 355 | 356 | Opens the steam overlay to the store. 357 | 358 | `appid` 359 | The appid to open. 360 | 361 | `flag` 362 | One of achievements.steam.STORE_NONE, .STORE_ADD_TO_CART, or .STORE_ADD_TO_CART_AND_SHOW. 363 | """ 364 | 365 | if flag is None: 366 | flag = STORE_NONE 367 | 368 | steamapi.SteamFriends().ActivateGameOverlayToStore(appid, flag) 369 | 370 | ########################################################################### User 371 | 372 | def get_persona_name(): 373 | """ 374 | :doc: steam_user 375 | 376 | Returns the user's publicly-visible name. 377 | """ 378 | 379 | return steamapi.SteamFriends().GetPersonaName().decode("utf-8") 380 | 381 | 382 | def get_csteam_id(): 383 | """ 384 | :doc: steam_user 385 | 386 | Returns the user's full CSteamID as a 64-bit number.. 387 | """ 388 | 389 | # Accessing methods on CSteamID was crashing on Windows, so use 390 | # the flat API instead. 391 | 392 | return steamapi.SteamUser().GetSteamID() 393 | 394 | 395 | def get_account_id(): 396 | """ 397 | :doc: steam_user 398 | 399 | Returns the user's account ID. 400 | """ 401 | 402 | return get_csteam_id() & 0xffffffff 403 | 404 | 405 | def get_session_ticket(): 406 | """ 407 | :doc: steam_user 408 | 409 | Gets a ticket that can be sent to the server to authenticate this user. 410 | """ 411 | 412 | from ctypes import c_uint, create_string_buffer, byref 413 | 414 | global ticket 415 | global h_ticket 416 | 417 | if ticket is not None: 418 | return ticket 419 | 420 | ticket_buf = create_string_buffer(2048) 421 | ticket_len = c_uint() 422 | 423 | h_ticket = steamapi.SteamUser().GetAuthSessionTicket(byref(ticket_buf), 2048, byref(ticket_len)) 424 | 425 | if h_ticket: 426 | ticket = ticket_buf.raw[0:ticket_len] 427 | 428 | return ticket 429 | 430 | 431 | def cancel_ticket(): 432 | """ 433 | :doc: steam_user 434 | 435 | Cancels the ticket returned by :func:`_renpysteam.get_session_ticket`. 436 | """ 437 | 438 | global h_ticket 439 | global ticket 440 | 441 | steamapi.SteamUser().CancelAuthTicket(h_ticket) 442 | 443 | h_ticket = 0 444 | ticket = None 445 | 446 | 447 | def get_game_badge_level(series, foil): 448 | """ 449 | :doc: steam_user 450 | 451 | Gets the level of the users Steam badge for your game. 452 | """ 453 | 454 | return steamapi.SteamUser().GetGameBadgeLevel(series, foil) 455 | 456 | 457 | ########################################################################### UGC 458 | 459 | def get_subscribed_items(): 460 | """ 461 | :doc: steam_ugc 462 | 463 | Returns a list of the item ids the user has subscribed to in the steam 464 | workshop. 465 | """ 466 | 467 | from ctypes import c_ulonglong, byref 468 | 469 | subscribed = (c_ulonglong * 512)() 470 | 471 | count = steamapi.SteamUGC().GetSubscribedItems(byref(subscribed), 512) 472 | 473 | rv = [ ] 474 | 475 | for i in range(count): 476 | rv.append(subscribed[i]) 477 | 478 | return rv 479 | 480 | def get_subscribed_item_path(item_id): 481 | """ 482 | :doc: steam_ugc 483 | 484 | Returns the path where an item of user-generated content was installed. Returns 485 | None if the item was not installed. 486 | 487 | `item_id` 488 | The item id. 489 | """ 490 | 491 | from ctypes import c_uint, c_ulonglong, create_string_buffer, byref 492 | 493 | path = create_strng_buffer(4096) 494 | size = c_ulonglong() 495 | timestamp = c_int() 496 | 497 | if not steamapi.SteamUGC().GetItemInstallInfo(item_id, byref(size), byref(path), 4096, byref(timestamp)): 498 | return None 499 | 500 | return renpy.exports.fsdecode(path.value) 501 | 502 | ############################################ Import API after steam is found. 503 | def import_api(): 504 | 505 | global steamapi 506 | import steamapi 507 | 508 | global POSITION_TOP_LEFT, POSITION_TOP_RIGHT, POSITION_BOTTOM_LEFT, POSITION_BOTTOM_RIGHT 509 | 510 | POSITION_TOP_LEFT = steamapi.k_EPositionTopLeft 511 | POSITION_TOP_RIGHT = steamapi.k_EPositionTopRight 512 | POSITION_BOTTOM_LEFT = steamapi.k_EPositionBottomLeft 513 | POSITION_BOTTOM_RIGHT = steamapi.k_EPositionBottomRight 514 | 515 | global STORE_NONE, STORE_ADD_TO_CART, STORE_ADD_TO_CART_AND_SHOW 516 | 517 | STORE_NONE = steamapi.k_EOverlayToStoreFlag_None 518 | STORE_ADD_TO_CART = steamapi.k_EOverlayToStoreFlag_AddToCart 519 | STORE_ADD_TO_CART_AND_SHOW = steamapi.k_EOverlayToStoreFlag_AddToCartAndShow 520 | 521 | ################################################################## Callbacks 522 | 523 | # A map from callback class name to a list of callables that will be called 524 | # with the callback instance. 525 | 526 | 527 | callback_handlers = collections.defaultdict(list) 528 | 529 | def periodic(): 530 | """ 531 | Called periodically to run Steam callbacks. 532 | """ 533 | 534 | for cb in steamapi.generate_callbacks(): 535 | # print(type(cb).__name__, {k : getattr(cb, k) for k in dir(cb) if not k.startswith("_")}) 536 | 537 | for handler in callback_handlers.get(type(cb).__name__, [ ]): 538 | handler(cb) 539 | 540 | if renpy.variant("steam_deck"): 541 | keyboard_periodic() 542 | 543 | ################################################################## Keyboard 544 | 545 | # True to show the keyboard once, False otherwise. 546 | keyboard_mode = "always" 547 | 548 | # True if this is the start of a new interaction, and so the keyboard 549 | # should be shown if a text box appears. 550 | keyboard_primed = True 551 | 552 | # True if the keyboard is currently showing. 553 | keyboard_showing = None 554 | 555 | # Should the layers be shifted so the baseline is in view? 556 | keyboard_shift = True 557 | 558 | # Where the basline is shifted to on the screen. This is a floating point number, 559 | # with 0.0 being the top of the screen and 1.0 being the bottom. 560 | keyboard_baseline = 0.5 561 | 562 | def prime_keyboard(): 563 | global keyboard_primed 564 | keyboard_primed = True 565 | 566 | renpy.config.start_interact_callbacks.append(prime_keyboard) 567 | 568 | def keyboard_periodic(): 569 | 570 | global keyboard_showing 571 | global keyboard_primed 572 | global keyboard_shift 573 | global keyboard_baseline 574 | 575 | if keyboard_mode == "never": 576 | return 577 | elif keyboard_mode == "always": 578 | keyboard_primed = True 579 | elif keyboard_mode != "once": 580 | raise Exception("Bad keyboard_mode.") 581 | 582 | keyboard_text_rect = renpy.display.interface.text_rect 583 | _KeyboardShift.text_rect = keyboard_text_rect 584 | 585 | if keyboard_primed and (keyboard_showing is None) and keyboard_text_rect: 586 | x, y, w, h = (int(i) for i in keyboard_text_rect) 587 | 588 | if keyboard_shift: 589 | y = int(renpy.exports.get_physical_size()[1] * keyboard_baseline) - h 590 | 591 | steamapi.SteamUtils().ShowFloatingGamepadTextInput( 592 | steamapi.k_EFloatingGamepadTextInputModeModeSingleLine, 593 | x, y, w, h) 594 | 595 | print("Showing keyboard.") 596 | 597 | keyboard_showing = time.time() 598 | keyboard_primed = False 599 | 600 | if keyboard_shift and keyboard_showing and keyboard_text_rect: 601 | for l in renpy.config.transient_layers + renpy.config.overlay_layers + renpy.config.context_clear_layers: 602 | if not renpy.display.interface.ongoing_transition.get(l) is _KeyboardShift: 603 | renpy.display.interface.set_transition(_KeyboardShift, layer=l, force=True) 604 | renpy.exports.restart_interaction() 605 | 606 | if keyboard_showing and not keyboard_text_rect: 607 | steamapi.SteamUtils().DismissFloatingGamepadTextInput() 608 | 609 | if keyboard_showing is None: 610 | _KeyboardShift.last_offset = 0 611 | else: 612 | _KeyboardShift.rendered_offset = _KeyboardShift.last_offset 613 | 614 | 615 | def keyboard_dismissed(cb): 616 | """ 617 | Called when the keyboard is dismissed. 618 | """ 619 | 620 | global keyboard_showing 621 | keyboard_showing = None 622 | 623 | callback_handlers["FloatingGamepadTextInputDismissed_t"].append(keyboard_dismissed) 624 | 625 | class _KeyboardShift(renpy.display.layout.Container): 626 | """ 627 | This is a transition that shifts the screen up, intended for use only 628 | with the steam deck keyboard. 629 | """ 630 | 631 | # Store the text rectangle in the class, so it's not saved, and 632 | # is available during render(). 633 | text_rect = None 634 | 635 | # The last offset we computed. 636 | last_offset = 0 637 | 638 | # The offset we computed last time we rendered. 639 | rendered_offset = 0 640 | 641 | def __init__(self, new_widget, old_widget, **properties): 642 | super(_KeyboardShift, self).__init__(**properties) 643 | 644 | self.delay = 0 645 | self.add(new_widget) 646 | 647 | def render(self, width, height, st, at): 648 | rv = renpy.display.render.Render(width, height) 649 | cr = renpy.display.render.render(self.child, width, height, st, at) 650 | 651 | if (keyboard_showing is not None) and self.text_rect: 652 | 653 | yscale = renpy.config.screen_height / renpy.exports.get_physical_size()[1] 654 | x, y, w, h = self.text_rect 655 | y -= self.rendered_offset 656 | 657 | text_baseline = y + h 658 | desired_baseline = int(keyboard_baseline * renpy.config.screen_height) 659 | 660 | offset = int(desired_baseline - text_baseline) 661 | offset = min(0, offset) 662 | 663 | done = (time.time() - keyboard_showing) / .3 664 | done = min(1.0, done) 665 | done = max(0.0, done) 666 | 667 | if offset and done < 1.0: 668 | renpy.display.render.redraw(self, 0) 669 | 670 | offset = int(offset * done) 671 | 672 | else: 673 | offset = 0 674 | 675 | _KeyboardShift.last_offset = offset 676 | 677 | rv.blit(cr, (0, offset)) 678 | self.offsets = [ (0, offset) ] 679 | 680 | return rv 681 | 682 | init -1499 python in achievement: 683 | 684 | steam_maximum_framerate = 15 685 | 686 | # The position of the steam notification popup. One of "top left", "top right", 687 | # "bottom left", or "bottom right". 688 | steam_position = None 689 | 690 | class SteamBackend(Backend): 691 | """ 692 | A backend that sends achievements to Steam. This is only used if steam 693 | has loaded and initialized successfully. 694 | """ 695 | 696 | def __init__(self): 697 | # A map from achievement name to steam name. 698 | self.names = { } 699 | self.stats = { } 700 | 701 | steam.retrieve_stats() 702 | renpy.maximum_framerate(steam_maximum_framerate) 703 | 704 | def register(self, name, steam=None, steam_stat=None, stat_max=None, stat_modulo=1, **kwargs): 705 | if steam is not None: 706 | self.names[name] = steam 707 | 708 | self.stats[name] = (steam_stat, stat_max, stat_modulo) 709 | 710 | def grant(self, name): 711 | name = self.names.get(name, name) 712 | 713 | renpy.maximum_framerate(steam_maximum_framerate) 714 | steam.grant_achievement(name) 715 | steam.store_stats() 716 | 717 | def clear(self, name): 718 | name = self.names.get(name, name) 719 | 720 | steam.clear_achievement(name) 721 | steam.store_stats() 722 | 723 | def clear_all(self): 724 | for i in steam.list_achievements(): 725 | steam.clear_achievement(i) 726 | 727 | steam.store_stats() 728 | 729 | def progress(self, name, completed): 730 | 731 | orig_name = name 732 | 733 | completed = int(completed) 734 | 735 | if name not in self.stats: 736 | if config.developer: 737 | raise Exception("To report progress, you must register {} with a stat_max.".format(name)) 738 | else: 739 | return 740 | 741 | current = persistent._achievement_progress.get(name, 0) 742 | 743 | steam_stat, stat_max, stat_modulo = self.stats[name] 744 | 745 | name = self.names.get(name, name) 746 | 747 | if (current is not None) and (current >= completed): 748 | return 749 | 750 | renpy.maximum_framerate(steam_maximum_framerate) 751 | 752 | if completed >= stat_max: 753 | steam.grant_achievement(name) 754 | else: 755 | if (stat_modulo is None) or (completed % stat_modulo) == 0: 756 | steam.indicate_achievement_progress(name, completed, stat_max) 757 | 758 | steam.store_stats() 759 | 760 | def has(self, name): 761 | name = self.names.get(name, name) 762 | 763 | return steam.get_achievement(name) 764 | 765 | def steam_preinit(): 766 | """ 767 | This sets up the steam appid when in development mode. 768 | """ 769 | 770 | import os, sys 771 | 772 | try: 773 | if config.early_script_version is not None: 774 | return 775 | except: 776 | return 777 | 778 | if config.steam_appid is None: 779 | return 780 | 781 | with open(os.path.join(os.path.dirname(sys.executable), "steam_appid.txt"), "w") as f: 782 | f.write(str(config.steam_appid) + "\n") 783 | 784 | # The _renpysteam namespace, or None if steam isn't loaded. 785 | steam = None 786 | 787 | # The full steam api. 788 | steamapi = None 789 | 790 | # Are the steam libraries installed? Used by the launcher. 791 | has_steam = False 792 | 793 | def steam_init(): 794 | 795 | global has_steam 796 | global steam 797 | global steamapi 798 | 799 | try: 800 | import sys 801 | import os 802 | import ctypes 803 | 804 | if renpy.windows and (sys.maxsize > (1 << 32)): 805 | dll_name = "steam_api64.dll" 806 | elif renpy.windows: 807 | dll_name = "steam_api.dll" 808 | elif renpy.macintosh: 809 | dll_name = "libsteam_api.dylib" 810 | else: 811 | dll_name = "libsteam_api.so" 812 | 813 | dll_path = os.path.join(os.path.dirname(sys.executable), dll_name) 814 | has_steam = os.path.exists(dll_path) 815 | 816 | if not config.enable_steam: 817 | return 818 | 819 | dll = ctypes.cdll[dll_path] 820 | 821 | import steamapi 822 | steamapi.load(dll) 823 | 824 | if not steamapi.Init(): 825 | raise Exception("Init returned false.") 826 | 827 | import store._renpysteam as steam 828 | sys.modules["_renpysteam"] = steam 829 | 830 | steam.import_api() 831 | steamapi.init_callbacks() 832 | 833 | config.periodic_callbacks.append(steam.periodic) 834 | config.needs_redraw_callbacks.append(steam.overlay_needs_present) 835 | steam.set_overlay_notification_position(steam.POSITION_TOP_RIGHT) 836 | 837 | if steamapi.SteamUtils().IsSteamInBigPictureMode(): 838 | config.variants.insert(0, "steam_big_picture") 839 | 840 | if steamapi.SteamUtils().IsSteamRunningOnSteamDeck(): 841 | config.variants.insert(0, "steam_deck") 842 | 843 | backends.insert(0, SteamBackend()) 844 | renpy.write_log("Initialized steam.") 845 | 846 | except Exception as e: 847 | renpy.write_log("Faled to initialize steam: %r", e) 848 | steam = None 849 | steamapi = None 850 | 851 | steam_preinit() 852 | steam_init() 853 | 854 | 855 | init 1500 python in achievement: 856 | 857 | # Steam position. 858 | if steam is not None: 859 | if steam_position == "top left": 860 | steam.set_overlay_notification_position(steam.POSITION_TOP_LEFT) 861 | elif steam_position == "top right": 862 | steam.set_overlay_notification_position(steam.POSITION_TOP_RIGHT) 863 | elif steam_position == "bottom left": 864 | steam.set_overlay_notification_position(steam.POSITION_BOTTOM_LEFT) 865 | elif steam_position == "bottom right": 866 | steam.set_overlay_notification_position(steam.POSITION_BOTTOM_RIGHT) 867 | --------------------------------------------------------------------------------