l3rittany Posted November 1 Posted November 1 (edited) Using Local API to Upload PKHeX Saves to PKSM Cloud (2025) aka: Upload PKHeX Files directly to your PKSM straight from your computer. Official Set Up Guide (Linux) RUN THIS GUIDE THROUGH AI TO SEE IF IT CAN GENERATE A WINDOWS/MAC FRIENDLY TUTORIAL AND DON'T FORGET TO VIEW THE SPOILER IN STEP 8. Otherwise, wait for me to release it. If you choose to do this, be sure to read along with this actual tutorial because AI will mess this up if you're not careful. If a kind person wants to translate this guide for these OS for me, I will be forever appreciative. Find me in the Discord Server and DM me if you have done so. ──────────────────────────────────────── Hi there, I'm a mom and I like to make things easy. Last thing I want to do is physically move my SD card into my PC to inject Pokémon into Gen 6 or older games into PKSM. I don't have the time to do all that when I've got a toddler to keep up with. This guide will fix this issue, that way you can just inject PKHeX Pokémon into PKSM Cloud straight from your PC. As far as I am aware the API has been discontinued for some time leaving people unable to conveniently upload their PKHeX saves into their PKSM; today we are going to fix that. I only started using PKSM and PKHeX two days ago in hopes of restoring some legitimate competitive Pokes I spent hours breeding back in 2014. My game cartridges were unfortunately stolen from me... amazingly I had all the necessary data's footprint on the web that's survived 10+ years so I can recreate them in PKHeX. Thank you devs!! WARNING: I haven't had the time to write this out for Windows/Mac but I am very happy to share my steps for Linux users and update this guide in the future for other operating systems. Perhaps you can plug in my code into AI to make it Windows/Mac friendly in the meantime. I'm also writing this guide out strictly from memory, so if you run into any complications please let me know because I might've miswritten something. DO THIS AT YOUR OWN DISCRETION. IT IS HIGHLY ADVISED NOT TO SKIP OR DELETE THE DATABASE BACKUP JUST INCASE SOMETHING GOES AWRY. ──────────────────────────────────────── Read each instruction carefully. 1.) Download local-gpss: Download the latest version of local-gpss. Again, I'm using the Linux version. Don't extract the contents yet. 2.) Create an Apps directory if you haven't already and its subfolders: I'm just going to show you my pathing that I do. I like to keep all my Apps in an App folder in the home directory, Open up a Terminal. mkdir -p ~/Apps/local-gpss/PKM Then, go to your recently downloaded linux64.zip and extract all the contents into ~/Apps/local-gpss. (NOT into the PKM folder, this is where your PKHeX data is going to go when you're finished setting up.) 3.) In the terminal, enter the following code to run your local server: cd ~/Apps/local-gpss/ ./local-gpss/ If this is your first time booting the server, it will ask if you want to download a database. (gpss.db), hit Y. 4.) Test to see if the server is live: You should have been given an IP address and it's port to plug into the API section of PKSM in your terminal. It should look something like this: http://192.168.x.xxx:5000 On your 3DS, go to PKSM > X Button (Settings) > Local API URL > Enter http://192.168.x.xxx:5000 > Press OK > B Button > Select Game of Choice > Load with A Button > Storage > Press Wifi Button on the Bottom Left Corner. If the cloud storage loaded, everything is running smoothly. 5.) Kill server for now: On the server terminal press Ctrl + C. Optional: For good measure, type this in your terminal to be absolutely sure it's dead pkill local-gpss 6.) Create a Backup of your gpss.db: In Thunar, navigate to ~/Apps/local-gpss, copy and paste the gpss.db in the local-gpss directory, and rename it to gpss-backup. 7.) Clear the Database and check to see if it's empty: There are SO many pokemon left in this database from when the database was publicly accessed, I'm assuming, so let's clear them out to make room for our own 'mons. (Sidenote for Devs/Tech Savvy people: I have each DELETE and SELECT separated for other developer's clarity if anyone wants to do anything/make their own modifications with this.) # Clear the tables sqlite3 "$HOME/Apps/local-gpss/gpss.db" <<'EOF' DELETE FROM bundle_pokemon; DELETE FROM bundle; DELETE FROM pokemon; VACUUM; EOF Now, sanity check to see if it's clear. # Verify the tables are empty sqlite3 "$HOME/Apps/local-gpss/gpss.db" <<'EOF' SELECT 'bundle_pokemon count: ' || COUNT(*) FROM bundle_pokemon; SELECT 'bundle count: ' || COUNT(*) FROM bundle; SELECT 'pokemon count: ' || COUNT(*) FROM pokemon; EOF If you see something along the lines of: bundle_pokemon count: 0 bundle count: 0 pokemon count: 0 The data successfully cleared and it's safe to move on to the next Step. Optional: Also create a backup for the empty database. Copy and paste current gpss.db, the empty one, into the local-gpss directory and name it "gpss-empty-backup.db". 8.) Create the shell script and command to regenerate your database: (Tech Savvy individuals check note at bottom.) Lets create a shell script that will allow you to add your Pokemon to your database with one simple terminal command. We will create a shell script called gpss-import-all. gpss-import-all will do the following: Back up your current DB Create a new bundle each run (no overwrites) Auto-detect gen from the file extension Compute bundle min_gen / max_gen from the files it actually imported De-dupe by content (skips exact duplicates) Kill Local-GPSS if you accidentally left it running, restart Local-GPSS and print your API URL. Copy and Paste the ENTIRE code in the spoiler below into your terminal. Spoiler sudo tee /usr/local/bin/gpss-import-all >/dev/null <<'EOF' #!/usr/bin/env bash set -euo pipefail # --- Config --- DB="$HOME/Apps/local-gpss/gpss.db" PKM_DIR="$HOME/Apps/local-gpss/PKM" # put .pk* files here PORT="5000" BUNDLE_NAME="Upload $(date +%F_%H%M)" die(){ echo "ERROR: $*" >&2; exit 1; } [ -f "$DB" ] || die "Database not found: $DB" [ -d "$PKM_DIR" ] || die "PKM directory not found: $PKM_DIR" # Stop server (if running) and back up DB pkill -f local-gpss 2>/dev/null || true cp "$DB" "$DB.backup.$(date +%Y%m%dT%H%M%S)" # Ensure required tables exist for t in pokemon bundle bundle_pokemon; do sqlite3 "$DB" "SELECT name FROM sqlite_master WHERE type='table' AND name='$t';" | grep -q . \ || die "Table '$t' missing in DB." done # Helper: map file extension -> generation string gen_from_ext () { ext="${1,,}" # lower case "$ext" in pk1) echo "1" ;; pk2) echo "2" ;; pk3) echo "3" ;; pk4) echo "4" ;; pk5) echo "5" ;; pk6|pkx) echo "6" ;; # pkx is Gen 6 format pk7) echo "7" ;; *) echo "" ;; esac } # Create a fresh bundle; fill a friendly name if column exists sqlite3 "$DB" "INSERT INTO bundle (download_code, upload_datetime, download_count, legal, min_gen, max_gen) VALUES (lower(hex(randomblob(8))), datetime('now'), 0, 1, '7', '1');" BID="$(sqlite3 "$DB" "SELECT last_insert_rowid();")" if sqlite3 "$DB" "PRAGMA table_info(bundle);" | awk -F'|' '{print $2}' | grep -qx name; then sqlite3 "$DB" "UPDATE bundle SET name='$BUNDLE_NAME' WHERE id=$BID;" fi echo "Created bundle id=$BID name='$BUNDLE_NAME'" # Track min/max gen actually inserted min_gen=99 max_gen=0 inserted=0 shopt -s nullglob # Accept all common pk* single-mon files for f in "$PKM_DIR"/*.pk1 "$PKM_DIR"/*.pk2 "$PKM_DIR"/*.pk3 "$PKM_DIR"/*.pk4 "$PKM_DIR"/*.pk5 "$PKM_DIR"/*.pk6 "$PKM_DIR"/*.pk7 "$PKM_DIR"/*.pkx; do [ -e "$f" ] || continue ext="${f##*.}" gen="$(gen_from_ext "$ext")" if [ -z "$gen" ]; then echo "Skipping (unknown extension -> gen): $(basename "$f")" continue fi B64="$(base64 -w0 "$f")" # Insert if not already present (dedupe by base_64) sqlite3 "$DB" " WITH new(b64, g) AS (SELECT '$B64', '$gen') INSERT INTO pokemon (upload_datetime, generation, legal, base_64, download_count, download_code) SELECT datetime('now'), new.g, 1, new.b64, 0, lower(hex(randomblob(8))) FROM new WHERE NOT EXISTS (SELECT 1 FROM pokemon WHERE base_64 = new.b64); " # Get pokemon id whether newly inserted or existing PID="$(sqlite3 "$DB" "SELECT id FROM pokemon WHERE base_64='$B64' LIMIT 1;")" if [ -z "$PID" ]; then echo "Failed to resolve pokemon id for $(basename "$f"); skipping." continue fi # Link to this bundle; ignore if already linked sqlite3 "$DB" "INSERT OR IGNORE INTO bundle_pokemon (bundle_id, pokemon_id) VALUES ($BID, $PID);" # Update min/max gen from the file's gen [ "$gen" -lt "$min_gen" ] && min_gen="$gen" [ "$gen" -gt "$max_gen" ] && max_gen="$gen" inserted=$((inserted+1)) echo " + $(basename "$f") (Gen $gen) -> pokemon id=$PID" done # If nothing inserted/linked, keep bundle but report if [ "$inserted" -eq 0 ]; then echo "No files imported. (Empty folder or all were duplicates?)" else # Write actual min/max gens used into the bundle sqlite3 "$DB" "UPDATE bundle SET min_gen='$min_gen', max_gen='$max_gen' WHERE id=$BID;" echo "Bundle $BID covers gens $min_gen–$max_gen" fi sqlite3 "$DB" "VACUUM;" # Restart Local-GPSS cd "$(dirname "$DB")" nohup ./local-gpss --urls="http://0.0.0.0:$PORT/" >/dev/null 2>&1 & IP="$(hostname -I | awk '{print $1}')" echo echo " Done. PKSM → Settings → API:" echo " http://$IP:$PORT/" echo "Then PKSM → Storage → Cloud to view bundle id=$BID ($inserted file(s))." EOF sudo chmod +x /usr/local/bin/gpss-import-all For my tech savvy people ONLY: (if you're not a developer skip this section) Spoiler .pk* files are encoded as Base64 inside SQLite. When PKSM connects to a self-hosted Local-GPSS server and tries to load the database (via POST /api/v2/gpss/search/pokemon?page=1), the server may crash immediately with an error like: System.InvalidCastException: The data is NULL at ordinal 2. This method can't be called on NULL values. at Npgsql.NpgsqlDataReader.GetString(Int32 ordinal) at LocalGPSS.Controllers.GpssController.⋯ This happens because the SQLite database contains NULL values in one or more string columns that the .NET API assumes are non-null. The Local-GPSS backend (written in C# using Npgsql / EntityFramework) calls GetString() on certain columns without a null check. If any of these fields are NULL, the API throws an exception and returns HTTP 500 when PKSM requests the Cloud list. The affected columns are in the pokemon and bundle tables: | Table | Column | Type | Must Not Be NULL | Description | |:--|:--|:--:|:--| | pokemon | download_code | TEXT | | Unique identifier string used by the API | | pokemon | generation | TEXT | | Game generation (“6” for X/Y, etc.) | | pokemon | base_64 | TEXT | | The actual Pokémon data, Base64-encoded | | bundle | download_code | TEXT | | Bundle identifier | | bundle | min_gen / max_gen | TEXT | | Generation range values | If any of these are empty or NULL, the API fails when reading them. The gpss-import-all script sets: download_code = lower(hex(randomblob(8))) generation auto-detected from file base_64 Base64-encoded payload string Proper bundle linkage and non-null gen fields These defaults guarantee no NULLs, so the crash can’t happen again. -------------------------------------------- If you encounter the NULL, try this before restarting your server: (Gen 6 example) # Fill missing Pokémon fields sqlite3 gpss.db " UPDATE pokemon SET download_code = lower(hex(randomblob(8))) WHERE download_code IS NULL OR trim(download_code) = ''; UPDATE pokemon SET generation = '6' WHERE generation IS NULL OR trim(generation) = ''; DELETE FROM pokemon WHERE base_64 IS NULL OR length(trim(base_64)) = 0; " # Fix bundles too sqlite3 gpss.db " UPDATE bundle SET download_code = lower(hex(randomblob(8))) WHERE download_code IS NULL OR trim(download_code) = ''; UPDATE bundle SET min_gen = '6' WHERE min_gen IS NULL OR trim(min_gen) = ''; UPDATE bundle SET max_gen = '6' WHERE max_gen IS NULL OR trim(max_gen) = ''; " Then: sqlite3 gpss.db "VACUUM;" ./local-gpss That way PKSM will now load the database normally with no more 500 errors. TLDR for Devs: .pk* files are encoded as Base64 inside SQLite. If you ever get “GetString() on NULL values” or “500 Internal Server Error” when PKSM loads Cloud, run the above SQL to replace missing download_code, generation, and base_64 values... it’s always one of those being NULL. 9.) Add your PKHeX files to the PKM folder in ~/Apps/local-gpss: Pretty self-explanatory. Generate the desired PKHeX files and add them to the PKM folder in your local-gpss directory. 10.) Repopulate your Empty DB: In your terminal, type: gpss-import-all You should have now imported all Pokemon that you've generated and started your server (while creating a backup,) Optional: Do another sanity check to see if the tables have populated. Enter this into a new terminal. # Verify the tables are populated sqlite3 "$HOME/Apps/local-gpss/gpss.db" <<'EOF' SELECT 'bundle_pokemon count: ' || COUNT(*) FROM bundle_pokemon; SELECT 'bundle count: ' || COUNT(*) FROM bundle; SELECT 'pokemon count: ' || COUNT(*) FROM pokemon; EOF You should see: bundle_pokemon count: [THE AMOUNT YOU JUST IMPORTED] bundle count: [THE AMOUNT YOU JUST IMPORTED] pokemon count: [THE AMOUNT YOU JUST IMPORTED] If you see this, congratulations, you've set up everything correctly and ready to Generate like a boss. 11.) Load PKSM and inject your generated pokemon into your game: Select Game of Choice > Load with A Button > Storage > Press Wifi Button on the Bottom Left Corner TA-DA they should be good to go! Enjoy! ──────────────────────────────────────── Important Sidenotes: Might need to unblock port 5000 on your firewall if your 3DS isn't finding it. I highly recommend making these a shell script for your convenience. Spoiler Run this in your terminal to start the server if you don't want to inject Pokemon. cd ~/Apps/local-gpss/ ./local-gpss/ Run this to kill the server. pkill local-gpss Run this to clear database # Clear the tables sqlite3 "$HOME/Apps/local-gpss/gpss.db" <<'EOF' DELETE FROM bundle_pokemon; DELETE FROM bundle; DELETE FROM pokemon; VACUUM; EOF Edited November 4 by l3rittany Updated code to not include generations 8/9 (Obviously irrelevant for 3DS)
l3rittany Posted November 3 Author Posted November 3 (edited) Premade Database Downloads gpss-og-backup.db - Database as is before altering. gpss-empty-backup.db - Empty GPSS database gpss_RoC-Gen1-6_Event.db - All events located in the Gen 6 Folder of RoC's PC. gpss_RoC-Gen1-6_Event.db gpss-empty-backup.db gpss-og-backup.db Edited November 3 by l3rittany
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now