From 71f034683554a1e15a0a721557860eb8b450a547 Mon Sep 17 00:00:00 2001 From: Nathan Whitehead Date: Wed, 5 Oct 2022 17:09:31 -0600 Subject: [PATCH] Add script for downloading puzzlescript games by url. Add capability to download itch.io puzzlescript games (experimental). Update readme. --- README.md | 21 ++++++++--- tools/get_game.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 tools/get_game.py diff --git a/README.md b/README.md index 1e426a9..c15d1fb 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,16 @@ PuzzleScript games are usually online at the main PuzzleScript site: https://www.puzzlescript.net/ To play games in the core, you will need to download the source for the game and save it as a `.pz` file. -The normal way to do this is to click the "hack" link on the game to go to the PuzzleScript editor, then +The normal way to do this is to click the "hack" or "sourcecode" link on the game to go to the PuzzleScript editor, then copy/paste the entire contents of the program into a file with extension `.pz`. +There is also a Python utility script in `tools/get_game.py` that can download PuzzleScript game source given a url. Typical +use would be: + + python3 tools/get_game.py --url=https://www.puzzlescript.net/play.html?p=2ba817426abc9fcbe9b9980fc4c02519 --output-title=.pz + +This would download and save the file `One_Microban_level.pz`. + You can also find many games in the Accessible PuzzleScript repository. For these, save the `script.txt` file into a file with extension `.pz`. https://github.com/philschatz/puzzlescript/tree/master/games @@ -74,6 +81,7 @@ content. ## Future work * Support more platforms for binary releases. +* Support [Pattern:Script](https://github.com/ClementSparrow/Pattern-Script/) * Add support for background music. * Add volume level control and other audio options. * Incorporate more accessibility features from: https://github.com/philschatz/puzzlescript @@ -211,15 +219,18 @@ BuildKit build. ## References -PuzzleScript project: +PuzzleScript project https://github.com/increpare/PuzzleScript Accessible PuzzleScript https://github.com/philschatz/puzzlescript -PuzzleScript Plus: +PuzzleScript Plus https://github.com/Auroriax/PuzzleScriptPlus +Pattern:Script +https://github.com/ClementSparrow/Pattern-Script/ + For making the retroarch core, I followed this tutorial: https://web.archive.org/web/20190219134028/http://www.beardypig.com/2016/01/22/emulator-build-along-2/ @@ -229,8 +240,8 @@ https://github.com/Narkhos/Puzzlescript-Wrapper QuickJS https://bellard.org/quickjs/ -Docs for using V8: +Docs for using V8 https://v8.dev/docs/ -Linaro toolchain: +Linaro toolchain https://releases.linaro.org/components/toolchain/binaries/ diff --git a/tools/get_game.py b/tools/get_game.py new file mode 100644 index 0000000..ca976b2 --- /dev/null +++ b/tools/get_game.py @@ -0,0 +1,94 @@ +''' +Download a PuzzleScript game + +Given URL to play game on PuzzleScript, this script downloads source and saves to a file. + +''' + +import argparse +import codecs +import re +import requests + +ESCAPE_SEQUENCE_RE = re.compile(r''' + ( \\U........ # 8-digit hex escapes + | \\u.... # 4-digit hex escapes + | \\x.. # 2-digit hex escapes + | \\[0-7]{1,3} # Octal escapes + | \\N\{[^}]+\} # Unicode characters by name + | \\[\\'"abfnrtv] # Single-character escapes + )''', re.UNICODE | re.VERBOSE) + +def decode_escapes(s): + ''' Decode Python escape sequences in a string containing unicode ''' + # Basic problem is that decode with unicode-escape is broken in Python, doesn't work with unicode + # So only apply to actual escape parts, which are guaranteed to be just ascii + def decode_match(match): + return codecs.decode(match.group(0), 'unicode-escape') + return ESCAPE_SEQUENCE_RE.sub(decode_match, s) + +def find_title(content): + lines = content.split('\n') + for line in lines: + spl = line.split(' ') + if spl[0].upper() == 'TITLE': + return ' '.join(spl[1:]) + raise RuntimeError("Could not find title in prelude") + +def convert_dangerous(txt): + txt = txt.replace('-', '_') + txt = txt.replace(' ', '_') + txt = txt.replace("'", '_') + return txt + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Download PuzzleScript game source files') + parser.add_argument('--url', required=True, help="URL for playing game on PuzzleScript.net or itch.io") + parser.add_argument('--output', help="Output filename to save game contents to (if not given will display to stdout)") + parser.add_argument('--output-title', help="Save to filename given prefixed with game title") + args = parser.parse_args() + if "itch.io" in args.url: + # For itch.io downloads, first get contents + r = requests.get(args.url) + # Search for cdn link to game itself + m = re.search(r'(https://\S+\.hwcdn.net/\S+/index.html)', r.text) + if not m: + print('Could not find hwcdn link') + exit(-1) + # Get contents of cdn link + cdn_url = m.group(0) + r = requests.get(cdn_url) + text = r.content.decode('utf-8') + # Regex looks for the sourcecode, inside quotes allowing escapes inside string + pat = r'''sourceCode="([^"\\]*(\\.[^"\\]*)*)"''' + m = re.search(pat, text) + if m is None: + # Try minified version + mpat = r'''compile\(-1, "([^"\\]*(\\.[^"\\]*)*)"''' + m = re.search(mpat, text) + if m is None: + print("Could not find sourceCode in cdn result") + exit(-1) + source = m.group(1) + # We have the source in escaped form, need to unescape it (allowing unicode) + content = decode_escapes(source) + else: + spl = args.url.split('=') + if len(spl) < 2: + print("ERROR: Could not parse URL, must be in form: https://www.puzzlescript.net/play.html?p=IDENTIFIER") + exit(-1) + id = spl[-1] + dl_url = f"https://api.github.com/gists/{id}" + r = requests.get(dl_url) + data = r.json() + content = data['files']['script.txt']['content'] + if args.output_title: + title = find_title(content) + safe_title = convert_dangerous(title) + args.output = f"{safe_title}{args.output_title}" + if args.output: + with open(args.output, "w") as fout: + fout.write(content) + print(f"Wrote {len(content)} bytes of output to file: {args.output}") + else: + print(content)