gd-storage 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gd_storage-1.0.0/LICENSE +21 -0
- gd_storage-1.0.0/PKG-INFO +136 -0
- gd_storage-1.0.0/README.md +108 -0
- gd_storage-1.0.0/cli.py +389 -0
- gd_storage-1.0.0/gd_api.py +245 -0
- gd_storage-1.0.0/gd_storage.egg-info/PKG-INFO +136 -0
- gd_storage-1.0.0/gd_storage.egg-info/SOURCES.txt +20 -0
- gd_storage-1.0.0/gd_storage.egg-info/dependency_links.txt +1 -0
- gd_storage-1.0.0/gd_storage.egg-info/entry_points.txt +2 -0
- gd_storage-1.0.0/gd_storage.egg-info/requires.txt +2 -0
- gd_storage-1.0.0/gd_storage.egg-info/top_level.txt +4 -0
- gd_storage-1.0.0/methods/__init__.py +18 -0
- gd_storage-1.0.0/methods/compression.py +10 -0
- gd_storage-1.0.0/methods/method1_xy.py +81 -0
- gd_storage-1.0.0/methods/method2_raw_groups.py +59 -0
- gd_storage-1.0.0/methods/method3_base10000.py +74 -0
- gd_storage-1.0.0/methods/method4_base64_groups.py +52 -0
- gd_storage-1.0.0/methods/method5_property31.py +49 -0
- gd_storage-1.0.0/methods/method6_optimized.py +202 -0
- gd_storage-1.0.0/pyproject.toml +44 -0
- gd_storage-1.0.0/save_manager.py +114 -0
- gd_storage-1.0.0/setup.cfg +4 -0
gd_storage-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 c4k3ss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gd-storage
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Encode files into Geometry Dash levels
|
|
5
|
+
Author-email: c4k3ss <iustin@mail.c4k3ss.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/c4k3ss/GD-Storage
|
|
8
|
+
Project-URL: Repository, https://github.com/c4k3ss/GD-Storage
|
|
9
|
+
Project-URL: Issues, https://github.com/c4k3ss/GD-Storage/issues
|
|
10
|
+
Keywords: geometry-dash,gd,encoding,steganography,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Games/Entertainment
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: dashlib
|
|
26
|
+
Requires-Dist: pycryptodomex
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# GD Storage
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## What is this?
|
|
34
|
+
|
|
35
|
+
GD Storage lets you hide any file inside a Geometry Dash level by encoding the file's binary data into level object properties. The encoded level can be:
|
|
36
|
+
- Saved to your local GD save file
|
|
37
|
+
- Uploaded to GD servers and shared via level ID
|
|
38
|
+
- Downloaded and decoded back to the original file
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Upload to GD Servers** - Store files on RobTop's servers, get a level ID to share
|
|
43
|
+
- **Fetch from GD Servers** - Download and decode files using just a level ID
|
|
44
|
+
- **Local Save Support** - Inject encoded levels directly into your GD save file
|
|
45
|
+
- **Cross-Platform** - Works on Windows, macOS, and Linux (via Proton)
|
|
46
|
+
- **Configurable** - Custom GD save paths for non-standard installations
|
|
47
|
+
- **Secure** - Path traversal protection, input validation, overwrite confirmation
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Requires Python 3.10+
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install in development mode:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Upload a file to GD servers
|
|
67
|
+
gd-storage --upload photo.png
|
|
68
|
+
|
|
69
|
+
# Download and decode from GD servers
|
|
70
|
+
gd-storage --fetch 123456789
|
|
71
|
+
|
|
72
|
+
# Encode and inject into local GD save
|
|
73
|
+
gd-storage --encode document.pdf
|
|
74
|
+
|
|
75
|
+
# Decode from local GD save
|
|
76
|
+
gd-storage --decode "LevelName"
|
|
77
|
+
|
|
78
|
+
# Configure GD save path (for non-standard installations)
|
|
79
|
+
gd-storage --config
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## How It Works
|
|
83
|
+
|
|
84
|
+
Files are encoded using a base-9999 group encoding method:
|
|
85
|
+
|
|
86
|
+
1. **Compression** - The file is gzip compressed with the filename prepended
|
|
87
|
+
2. **Chunking** - Binary data is split into 8-byte chunks
|
|
88
|
+
3. **Base Conversion** - Each chunk is converted to 5 base-9999 numbers (1-9999)
|
|
89
|
+
4. **Object Encoding** - Numbers are stored in GD object group properties (property 57)
|
|
90
|
+
5. **Level String** - Objects are serialized into a valid GD level string
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Why Base-9999?
|
|
94
|
+
|
|
95
|
+
GD supports up to 9999 groups per object. By treating group numbers as digits in a base-9999 number system, we can efficiently pack 8 bytes into just 5 group values, achieving ~62.5% space efficiency.
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
On first run, GD Storage auto-detects your save path:
|
|
100
|
+
- **Windows**: `%LOCALAPPDATA%\GeometryDash`
|
|
101
|
+
- **macOS**: `~/Library/Application Support/GeometryDash`
|
|
102
|
+
|
|
103
|
+
For custom paths (Linux/Proton, custom installs), run:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
gd-storage --config
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Config is saved to `~/.config/gd-storage/config.json`
|
|
110
|
+
|
|
111
|
+
## Platform Support
|
|
112
|
+
|
|
113
|
+
| Platform | Local Save | Upload/Fetch |
|
|
114
|
+
|----------|------------|--------------|
|
|
115
|
+
| Windows | Yes | Yes |
|
|
116
|
+
| macOS | Yes | Yes |
|
|
117
|
+
| Linux | Yes* | Yes |
|
|
118
|
+
|
|
119
|
+
*Linux requires configuring the Proton GD save path manually
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Dependencies
|
|
123
|
+
|
|
124
|
+
- [dashlib](https://pypi.org/project/dashlib/) - GD server API
|
|
125
|
+
- [pycryptodomex](https://pypi.org/project/pycryptodomex/) - AES encryption for macOS saves
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
GD Storage is licensed under the MIT License
|
|
130
|
+
|
|
131
|
+
## Disclaimer
|
|
132
|
+
|
|
133
|
+
This project is for educational purposes. Use responsibly and don't abuse GD's servers.
|
|
134
|
+
|
|
135
|
+
> GitHub [@c4k3ss](https://github.com/c4k3ss) ·
|
|
136
|
+
> Twitter [@_c4k3ss](https://x.com/_c4k3ss)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# GD Storage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
GD Storage lets you hide any file inside a Geometry Dash level by encoding the file's binary data into level object properties. The encoded level can be:
|
|
8
|
+
- Saved to your local GD save file
|
|
9
|
+
- Uploaded to GD servers and shared via level ID
|
|
10
|
+
- Downloaded and decoded back to the original file
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Upload to GD Servers** - Store files on RobTop's servers, get a level ID to share
|
|
15
|
+
- **Fetch from GD Servers** - Download and decode files using just a level ID
|
|
16
|
+
- **Local Save Support** - Inject encoded levels directly into your GD save file
|
|
17
|
+
- **Cross-Platform** - Works on Windows, macOS, and Linux (via Proton)
|
|
18
|
+
- **Configurable** - Custom GD save paths for non-standard installations
|
|
19
|
+
- **Secure** - Path traversal protection, input validation, overwrite confirmation
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Requires Python 3.10+
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install in development mode:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Upload a file to GD servers
|
|
39
|
+
gd-storage --upload photo.png
|
|
40
|
+
|
|
41
|
+
# Download and decode from GD servers
|
|
42
|
+
gd-storage --fetch 123456789
|
|
43
|
+
|
|
44
|
+
# Encode and inject into local GD save
|
|
45
|
+
gd-storage --encode document.pdf
|
|
46
|
+
|
|
47
|
+
# Decode from local GD save
|
|
48
|
+
gd-storage --decode "LevelName"
|
|
49
|
+
|
|
50
|
+
# Configure GD save path (for non-standard installations)
|
|
51
|
+
gd-storage --config
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How It Works
|
|
55
|
+
|
|
56
|
+
Files are encoded using a base-9999 group encoding method:
|
|
57
|
+
|
|
58
|
+
1. **Compression** - The file is gzip compressed with the filename prepended
|
|
59
|
+
2. **Chunking** - Binary data is split into 8-byte chunks
|
|
60
|
+
3. **Base Conversion** - Each chunk is converted to 5 base-9999 numbers (1-9999)
|
|
61
|
+
4. **Object Encoding** - Numbers are stored in GD object group properties (property 57)
|
|
62
|
+
5. **Level String** - Objects are serialized into a valid GD level string
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
### Why Base-9999?
|
|
66
|
+
|
|
67
|
+
GD supports up to 9999 groups per object. By treating group numbers as digits in a base-9999 number system, we can efficiently pack 8 bytes into just 5 group values, achieving ~62.5% space efficiency.
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
On first run, GD Storage auto-detects your save path:
|
|
72
|
+
- **Windows**: `%LOCALAPPDATA%\GeometryDash`
|
|
73
|
+
- **macOS**: `~/Library/Application Support/GeometryDash`
|
|
74
|
+
|
|
75
|
+
For custom paths (Linux/Proton, custom installs), run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gd-storage --config
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Config is saved to `~/.config/gd-storage/config.json`
|
|
82
|
+
|
|
83
|
+
## Platform Support
|
|
84
|
+
|
|
85
|
+
| Platform | Local Save | Upload/Fetch |
|
|
86
|
+
|----------|------------|--------------|
|
|
87
|
+
| Windows | Yes | Yes |
|
|
88
|
+
| macOS | Yes | Yes |
|
|
89
|
+
| Linux | Yes* | Yes |
|
|
90
|
+
|
|
91
|
+
*Linux requires configuring the Proton GD save path manually
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Dependencies
|
|
95
|
+
|
|
96
|
+
- [dashlib](https://pypi.org/project/dashlib/) - GD server API
|
|
97
|
+
- [pycryptodomex](https://pypi.org/project/pycryptodomex/) - AES encryption for macOS saves
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
GD Storage is licensed under the MIT License
|
|
102
|
+
|
|
103
|
+
## Disclaimer
|
|
104
|
+
|
|
105
|
+
This project is for educational purposes. Use responsibly and don't abuse GD's servers.
|
|
106
|
+
|
|
107
|
+
> GitHub [@c4k3ss](https://github.com/c4k3ss) ·
|
|
108
|
+
> Twitter [@_c4k3ss](https://x.com/_c4k3ss)
|
gd_storage-1.0.0/cli.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GD Storage - Encode files into Geometry Dash levels
|
|
3
|
+
"""
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import base64
|
|
8
|
+
import gzip
|
|
9
|
+
import getpass
|
|
10
|
+
import re
|
|
11
|
+
import json
|
|
12
|
+
import platform
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from methods import METHODS
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Config file location
|
|
19
|
+
CONFIG_DIR = Path.home() / ".config" / "gd-storage"
|
|
20
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_config() -> dict:
|
|
24
|
+
"""Load config from file or return defaults."""
|
|
25
|
+
if CONFIG_FILE.exists():
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
28
|
+
except (json.JSONDecodeError, OSError):
|
|
29
|
+
pass
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_config(config: dict):
|
|
34
|
+
"""Save config to file."""
|
|
35
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_default_gd_path() -> str | None:
|
|
40
|
+
"""Get default GD save path for current platform."""
|
|
41
|
+
system = platform.system()
|
|
42
|
+
if system == "Windows":
|
|
43
|
+
local_app_data = os.getenv("LOCALAPPDATA")
|
|
44
|
+
if local_app_data:
|
|
45
|
+
return os.path.join(local_app_data, "GeometryDash")
|
|
46
|
+
elif system == "Darwin": # macOS
|
|
47
|
+
return os.path.expanduser("~/Library/Application Support/GeometryDash")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_manager(config: dict):
|
|
52
|
+
"""Get GD save manager based on config."""
|
|
53
|
+
from save_manager import new_manager
|
|
54
|
+
|
|
55
|
+
gd_path = config.get("gd_path")
|
|
56
|
+
gd_format = config.get("format", "auto")
|
|
57
|
+
|
|
58
|
+
# Auto-detect path if not configured
|
|
59
|
+
if not gd_path:
|
|
60
|
+
gd_path = get_default_gd_path()
|
|
61
|
+
if not gd_path:
|
|
62
|
+
raise ValueError("GD path not configured. Run: gd-storage --config")
|
|
63
|
+
|
|
64
|
+
# Check path exists
|
|
65
|
+
if not os.path.exists(gd_path):
|
|
66
|
+
raise ValueError(f"GD save folder not found: {gd_path}\nRun: gd-storage --config")
|
|
67
|
+
|
|
68
|
+
return new_manager(path=gd_path, format=gd_format)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_config():
|
|
72
|
+
"""Configure GD storage settings."""
|
|
73
|
+
config = load_config()
|
|
74
|
+
|
|
75
|
+
print("GD Storage Configuration")
|
|
76
|
+
print("=" * 40)
|
|
77
|
+
|
|
78
|
+
# Show current settings
|
|
79
|
+
current_path = config.get("gd_path", get_default_gd_path() or "Not set")
|
|
80
|
+
current_format = config.get("format", "auto")
|
|
81
|
+
print(f"Current path: {current_path}")
|
|
82
|
+
print(f"Current format: {current_format}")
|
|
83
|
+
print()
|
|
84
|
+
|
|
85
|
+
# Get new path
|
|
86
|
+
print("Enter GD save folder path (or press Enter to keep current):")
|
|
87
|
+
print(" Windows: %LOCALAPPDATA%\\GeometryDash")
|
|
88
|
+
print(" macOS: ~/Library/Application Support/GeometryDash")
|
|
89
|
+
print(" Linux/Proton: ~/.steam/steam/steamapps/compatdata/.../GeometryDash")
|
|
90
|
+
new_path = input("> ").strip()
|
|
91
|
+
|
|
92
|
+
if new_path:
|
|
93
|
+
new_path = os.path.expanduser(os.path.expandvars(new_path))
|
|
94
|
+
if not os.path.exists(new_path):
|
|
95
|
+
print(f"Warning: Path does not exist: {new_path}")
|
|
96
|
+
confirm = input("Save anyway? (y/N): ").strip().lower()
|
|
97
|
+
if confirm != 'y':
|
|
98
|
+
return 1
|
|
99
|
+
config["gd_path"] = new_path
|
|
100
|
+
|
|
101
|
+
# Get format
|
|
102
|
+
print()
|
|
103
|
+
print("Save format:")
|
|
104
|
+
print(" 1. auto (detect from platform)")
|
|
105
|
+
print(" 2. windows (XOR encryption - Windows/Linux/Proton)")
|
|
106
|
+
print(" 3. mac (AES encryption - macOS)")
|
|
107
|
+
choice = input("Choice [1]: ").strip() or "1"
|
|
108
|
+
|
|
109
|
+
if choice == "2":
|
|
110
|
+
config["format"] = "windows"
|
|
111
|
+
elif choice == "3":
|
|
112
|
+
config["format"] = "mac"
|
|
113
|
+
else:
|
|
114
|
+
config["format"] = "auto"
|
|
115
|
+
|
|
116
|
+
save_config(config)
|
|
117
|
+
print()
|
|
118
|
+
print(f"Config saved to {CONFIG_FILE}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def save_decoded_file(filename: str, data: bytes) -> Path | None:
|
|
123
|
+
"""Save decoded file to Downloads, checking for overwrites."""
|
|
124
|
+
# Sanitize filename - prevent path traversal
|
|
125
|
+
safe_filename = Path(filename).name
|
|
126
|
+
if not safe_filename:
|
|
127
|
+
safe_filename = "decoded_file"
|
|
128
|
+
|
|
129
|
+
downloads = Path(os.path.expanduser("~")) / "Downloads" / safe_filename
|
|
130
|
+
|
|
131
|
+
if downloads.exists():
|
|
132
|
+
print(f"File already exists: {downloads}")
|
|
133
|
+
choice = input("Overwrite? (y/N): ").strip().lower()
|
|
134
|
+
if choice != 'y':
|
|
135
|
+
# Try adding a number suffix
|
|
136
|
+
stem = downloads.stem
|
|
137
|
+
suffix = downloads.suffix
|
|
138
|
+
for i in range(1, 100):
|
|
139
|
+
alt = downloads.parent / f"{stem}_{i}{suffix}"
|
|
140
|
+
if not alt.exists():
|
|
141
|
+
downloads = alt
|
|
142
|
+
break
|
|
143
|
+
else:
|
|
144
|
+
print("Could not find available filename")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
downloads.write_bytes(data)
|
|
148
|
+
return downloads
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def show_help():
|
|
152
|
+
print("GD Storage - Encode files into Geometry Dash levels")
|
|
153
|
+
print()
|
|
154
|
+
print("Usage:")
|
|
155
|
+
print(" gd-storage --upload <filepath> Encode and upload to GD servers")
|
|
156
|
+
print(" gd-storage --fetch <level_id> Download and decode from GD servers")
|
|
157
|
+
print(" gd-storage --encode <filepath> Encode and inject into local GD save")
|
|
158
|
+
print(" gd-storage --decode <levelname> Decode from local GD save")
|
|
159
|
+
print(" gd-storage --config Configure GD save path")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_upload(filepath: Path, encode_func):
|
|
163
|
+
"""Encode and upload a file to GD servers."""
|
|
164
|
+
import dashlib
|
|
165
|
+
|
|
166
|
+
if not filepath.exists():
|
|
167
|
+
print(f"File not found: {filepath}")
|
|
168
|
+
return 1
|
|
169
|
+
|
|
170
|
+
print(f"Encoding {filepath.name} ({filepath.stat().st_size:,} bytes)...")
|
|
171
|
+
level_str = encode_func(filepath)
|
|
172
|
+
|
|
173
|
+
# Decompress to get raw level for object count
|
|
174
|
+
if level_str.startswith('H4sI'):
|
|
175
|
+
compressed = base64.urlsafe_b64decode(level_str + '==')
|
|
176
|
+
raw_level = gzip.decompress(compressed).decode('utf-8')
|
|
177
|
+
else:
|
|
178
|
+
raw_level = level_str
|
|
179
|
+
|
|
180
|
+
obj_count = raw_level.count(';')
|
|
181
|
+
level_name = filepath.stem[:20]
|
|
182
|
+
description = f"Encoded: {filepath.name}"
|
|
183
|
+
desc_encoded = base64.urlsafe_b64encode(description.encode()).decode()
|
|
184
|
+
|
|
185
|
+
class UploadLevel:
|
|
186
|
+
def __init__(self):
|
|
187
|
+
self.levelString = level_str
|
|
188
|
+
self.levelName = level_name
|
|
189
|
+
self.description = desc_encoded
|
|
190
|
+
self.password = 1
|
|
191
|
+
self.copiedId = 0
|
|
192
|
+
self.version = 1
|
|
193
|
+
self.officialSong = 0
|
|
194
|
+
self.customSongID = 0
|
|
195
|
+
self.coins = 0
|
|
196
|
+
self.lowDetailMode = False
|
|
197
|
+
self.objects = obj_count
|
|
198
|
+
self.isAuto = False
|
|
199
|
+
self.twoPlayer = False
|
|
200
|
+
self.starsRequested = 0
|
|
201
|
+
self.editorTime = 0
|
|
202
|
+
self.editorTimeCopies = 0
|
|
203
|
+
self.length = dashlib.LENGTH_TINY
|
|
204
|
+
|
|
205
|
+
level = UploadLevel()
|
|
206
|
+
|
|
207
|
+
# Get credentials
|
|
208
|
+
username, account_id, gjp2 = get_credentials()
|
|
209
|
+
if not username:
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
print(f"Uploading '{level_name}'...")
|
|
213
|
+
try:
|
|
214
|
+
result = dashlib.uploadLevel(
|
|
215
|
+
level=level,
|
|
216
|
+
username=username,
|
|
217
|
+
accountID=account_id,
|
|
218
|
+
gjp2=gjp2,
|
|
219
|
+
)
|
|
220
|
+
if result == "-1" or result.startswith("-"):
|
|
221
|
+
raise ValueError(f"Server returned: {result}")
|
|
222
|
+
new_level_id = int(result)
|
|
223
|
+
print(f"Uploaded! Level ID: {new_level_id}")
|
|
224
|
+
print(f"Fetch with: gd-storage --fetch {new_level_id}")
|
|
225
|
+
return 0
|
|
226
|
+
except Exception as e:
|
|
227
|
+
print(f"Upload failed: {e}")
|
|
228
|
+
return 1
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_fetch(level_id: int, decode_func):
|
|
232
|
+
"""Download and decode a level from GD servers."""
|
|
233
|
+
from gd_api import download_level
|
|
234
|
+
|
|
235
|
+
print(f"Fetching level {level_id}...")
|
|
236
|
+
try:
|
|
237
|
+
level_data = download_level(level_id)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print(f"Failed to fetch: {e}")
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
level_name = level_data.get("name", "Unknown")
|
|
243
|
+
description = level_data.get("description", "")
|
|
244
|
+
level_str = level_data.get("level_string", "")
|
|
245
|
+
|
|
246
|
+
print(f"Level: {level_name}")
|
|
247
|
+
if description:
|
|
248
|
+
print(f"Description: {description}")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
filename, data = decode_func(level_str)
|
|
252
|
+
saved_path = save_decoded_file(filename, data)
|
|
253
|
+
if saved_path:
|
|
254
|
+
print(f"Saved to {saved_path} ({len(data):,} bytes)")
|
|
255
|
+
return 0
|
|
256
|
+
return 1
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f"Failed to decode: {e}")
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cmd_encode(filepath: Path, encode_func):
|
|
263
|
+
"""Encode and inject into local GD save."""
|
|
264
|
+
if not filepath.exists():
|
|
265
|
+
print(f"File not found: {filepath}")
|
|
266
|
+
return 1
|
|
267
|
+
|
|
268
|
+
print(f"Encoding {filepath.name} ({filepath.stat().st_size:,} bytes)...")
|
|
269
|
+
level_str = encode_func(filepath)
|
|
270
|
+
|
|
271
|
+
config = load_config()
|
|
272
|
+
try:
|
|
273
|
+
manager = get_manager(config)
|
|
274
|
+
except ValueError as e:
|
|
275
|
+
print(f"Error: {e}")
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
manager.injectLevel(level_str, filepath.stem, f"Encoded: {filepath.name}")
|
|
279
|
+
manager.save(ccll=True, ccgm=False)
|
|
280
|
+
print(f"Injected as '{filepath.stem}'")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def cmd_decode(level_name: str, decode_func):
|
|
285
|
+
"""Decode from local GD save."""
|
|
286
|
+
print(f"Extracting '{level_name}'...")
|
|
287
|
+
|
|
288
|
+
config = load_config()
|
|
289
|
+
try:
|
|
290
|
+
manager = get_manager(config)
|
|
291
|
+
except ValueError as e:
|
|
292
|
+
print(f"Error: {e}")
|
|
293
|
+
return 1
|
|
294
|
+
|
|
295
|
+
ccll = manager.ccll.decode('utf-8', errors='ignore')
|
|
296
|
+
|
|
297
|
+
start = ccll.find(f'<s>{level_name}</s>')
|
|
298
|
+
if start == -1:
|
|
299
|
+
print(f"Level '{level_name}' not found!")
|
|
300
|
+
return 1
|
|
301
|
+
|
|
302
|
+
k4_start = ccll.find('<k>k4</k><s>', start) + len('<k>k4</k><s>')
|
|
303
|
+
k4_end = ccll.find('</s>', k4_start)
|
|
304
|
+
level_str = ccll[k4_start:k4_end]
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
filename, data = decode_func(level_str)
|
|
308
|
+
saved_path = save_decoded_file(filename, data)
|
|
309
|
+
if saved_path:
|
|
310
|
+
print(f"Saved to {saved_path} ({len(data):,} bytes)")
|
|
311
|
+
return 0
|
|
312
|
+
return 1
|
|
313
|
+
except Exception as e:
|
|
314
|
+
print(f"Failed to decode: {e}")
|
|
315
|
+
return 1
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_credentials():
|
|
319
|
+
"""Get GD credentials from save file or prompt user."""
|
|
320
|
+
from gd_api import get_account_id
|
|
321
|
+
|
|
322
|
+
config = load_config()
|
|
323
|
+
try:
|
|
324
|
+
manager = get_manager(config)
|
|
325
|
+
ccgm = manager.ccgm.decode('utf-8', errors='ignore')
|
|
326
|
+
|
|
327
|
+
saved_user = re.search(r'<k>GJA_001</k><s>([^<]+)</s>', ccgm)
|
|
328
|
+
saved_id = re.search(r'<k>GJA_003</k><i>(\d+)</i>', ccgm)
|
|
329
|
+
saved_gjp2 = re.search(r'<k>GJA_005</k><s>([^<]+)</s>', ccgm)
|
|
330
|
+
|
|
331
|
+
if saved_user and saved_id and saved_gjp2:
|
|
332
|
+
print(f"Using saved credentials: {saved_user.group(1)}")
|
|
333
|
+
return saved_user.group(1), int(saved_id.group(1)), saved_gjp2.group(1)
|
|
334
|
+
except ValueError:
|
|
335
|
+
pass # No local save, prompt for credentials
|
|
336
|
+
|
|
337
|
+
print("Enter GD credentials:")
|
|
338
|
+
username = input("Username: ")
|
|
339
|
+
password = getpass.getpass("Password: ")
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
account_id, gjp2 = get_account_id(username, password)
|
|
343
|
+
print(f"Logged in as {username}")
|
|
344
|
+
return username, account_id, gjp2
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"Login failed: {e}")
|
|
347
|
+
return None, None, None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
parser = argparse.ArgumentParser(
|
|
352
|
+
description="GD Storage - Encode files into Geometry Dash levels",
|
|
353
|
+
add_help=False
|
|
354
|
+
)
|
|
355
|
+
parser.add_argument('--upload', metavar='FILE', help='Encode and upload to GD servers')
|
|
356
|
+
parser.add_argument('--fetch', metavar='ID', type=int, help='Download and decode from GD servers')
|
|
357
|
+
parser.add_argument('--encode', metavar='FILE', help='Encode and inject into local GD save')
|
|
358
|
+
parser.add_argument('--decode', metavar='NAME', help='Decode from local GD save')
|
|
359
|
+
parser.add_argument('--config', action='store_true', help='Configure GD save path')
|
|
360
|
+
parser.add_argument('--help', '-h', action='store_true', help='Show help')
|
|
361
|
+
|
|
362
|
+
args = parser.parse_args()
|
|
363
|
+
|
|
364
|
+
# Handle config
|
|
365
|
+
if args.config:
|
|
366
|
+
return cmd_config()
|
|
367
|
+
|
|
368
|
+
# Show help if no args or --help
|
|
369
|
+
if args.help or (not args.upload and not args.fetch and not args.encode and not args.decode):
|
|
370
|
+
show_help()
|
|
371
|
+
return 0
|
|
372
|
+
|
|
373
|
+
encode_func, decode_func, _ = METHODS[6]
|
|
374
|
+
|
|
375
|
+
# Run command
|
|
376
|
+
if args.upload:
|
|
377
|
+
return cmd_upload(Path(args.upload), encode_func)
|
|
378
|
+
elif args.fetch:
|
|
379
|
+
return cmd_fetch(args.fetch, decode_func)
|
|
380
|
+
elif args.encode:
|
|
381
|
+
return cmd_encode(Path(args.encode), encode_func)
|
|
382
|
+
elif args.decode:
|
|
383
|
+
return cmd_decode(args.decode, decode_func)
|
|
384
|
+
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
sys.exit(main())
|