Here's a dead-simple QR-code based update-checking system for itch.io and sideloaded games

Use this freely if it helps you! Fixes/improvements welcome. There are lots of ways this could be made more sophisticated, but I wanted something super simple to implement with just a few lines of code—while being as friendly as possible to the end user.

Here's how it works

1. The Playdate game reads its build number from the pdxinfo file and makes a URL with that number as a parameter. This is presented as a QR code to scan with a phone. That's how the user checks for updates. (The QR code takes time to generate, so a "please wait" message appears if needed.)

2. The developer website, with just a few lines of JavaScript (nothing server-side), compares that build number with the latest one, and displays a message letting the user know that they are up to date—or that there's a new version available.

3. The user downloads and sideloads the game as usual, wherever it may be hosted. Then, as long as you keep updating the build number for each new release, it should automatically replace any prior pdx on the Playdate. There should be no need to delete the old version manually.

I have sample code here for both the Playdate and website ends of the process.

Here are all the pieces

Make a 105x105 square "placeholder"/"please wait" PNG image to show in case the QR code isn't ready yet. In this example, the filename is qr-placeholder.png.

Run the following Lua code at launch—so the QR code starts asynchronously generating early, not waiting for the user to view whatever Check for Updates screen you make. That way there's a good chance it's already ready when they do go there.

import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/qrcode"

local gfx <const> = pd.graphics

local updateQRimage = gfx.image.new("qr-placeholder")

function QRDone(image, errorMessage)
	updateQRImage = image
end

gfx.generateQRCode("yourdomain.com/anyaddress?build="..playdate.metadata.buildNumber, 105, QRDone)

Replace yourdomain.com/anyaddress with the URL of an update page where you control the HTML. No http/https:// needed. Add ?build= to the end.

It's OK if your page URL has other parameters! Just include build= at the end. (In that case it's &build= instead of the question mark.)

In my experience, 105x105 is a good size that fits a pretty long URL, and takes about 11.5 seconds to generate the code on Playdate hardware. (You could use another size—or even pre-generate a PNG. But with this system, you don't have to! Maintain your build number and the rest is automatic.)

Then on your game's Check for Updates screen, include the following code:

local updateQRSprite
updateQRSprite = gfx.sprite.new(updateQRImage)
updateQRSprite:moveTo(200, 120) --Or wherever you want the QR code
updateQRSprite:add()

And in the update loop (OK to setRefreshRate to a low value) while on that screen do this:

updateQRSprite:setImage(updateQRImage) --Ready for whenever the QR code is done
playdate.timer.updateTimers() --Needed for QR code generator
gfx.sprite.update()

You're done on the Playdate side!

On the web page, include an empty element with ID update-status somewhere up high and very visible (<p>, <div>, <span>, or whatever you wish, styled as you see fit):

<p id="update-status"></p>

And some JavaScript at the end of the page somewhere:

<script>
var latestBuildNumber = 1234;
var appName = "My Game Title";

var lastParameter = window.location.search.substr(1).split("&").slice(-1)[0];

if (lastParameter.substr(0, 6) == "build=") {
	var buildNumber = parseInt(lastParameter.substr(6));
	var statusMessage = "You have " + appName + " build " + buildNumber + ".";
	
	if (buildNumber == latestBuildNumber) {
		statusMessage += "<br><strong>You are up to date!</strong>";
	} else if (buildNumber < latestBuildNumber) {
		statusMessage += "<br><strong>A new version (build " + latestBuildNumber + ") is available!</strong>"
	}
	
	document.getElementById("update-status").innerHTML = statusMessage;
}
</script>

That's it! Customize the message text to say what you want.

EDIT: If you'd like dots displayed to the user ("1.0.5") to make the build number look like a traditional version number, here's JS code to handle that.

Now just remember to keep your buildNumber up to date: both in your game's pdxinfo file and in the JavaScript of the page. (The "version"—as displayed in Settings—is not what matters. You need to include a buildNumber in your pdxinfo. Update it manually if you don't have it automated.)

Edge case: if for some reason the app has a build number greater than the one declared as the latest in the JavaScript, thats's OK: the page simply informs you of the build number you have installed, without offering any further message about that being up to date or not.

There's no need for the URL to be a special web page just for updates—it could be any existing page you already use. The "Up to date" or "New version available" message is ADDED to the page when you include the build= parameter. But if you don't include that, the element update-status remains empty and the rest of the page still works without error.

You probably do want to include a download link, so people can easily obtain the current version.

For example, the page for my Playtime clock is just a note linking to the itch.io page, plus some links to help with sideloading. But above that info, I display the update status in bright red—as long as that build parameter is added to the URL.

Try it out

As a demo, edit the following address in your browser to see what happens with different build numbers (which in reality would come automatically from your pdxinfo via QR code):

https://adamsimmersive.com/apps/playdate/playtime-clocks/?build=102

The latest version coded into that page is higher than the "102" in the QR URL. So you get notified of the new version.

But change that "102" in the URL to match the current version number (no dots), and you'll see the "Up to date" message.

And if you remove the ?build= parameter from the URL entirely, you'll see that the page still works on its own, without trying to perform an update check.

Hope this is useful!

5 Likes

Here's how the Check for Updates screen looks in my app as an example (this QR code is functional, declaring that build 103 is installed):

Update QR code example

1 Like

not all heroes wear capes :star_struck:

I have a shell script that I use in Nova to increment my build number automatically using some simple sed logic. If anyone wants that to help streamline this idea even more, let me know !

1 Like

FWIW, some possible enhancements that I rejected as not worth it to me—but they would be cool:

• A private web form to update the build number server-side without editing the JavaScript variable. (That form could then be fed automatically by your build process if you want to get really crazy! Or is it lazy?)

• A moving progress meter instead of a static placeholder during QR generation. Here's how I'd do it:

  1. Time your QR code generation (11.5 seconds seems typical, doesn't have to be exact) on Playdate hardware.

  2. The SDK's generateQRCode() returns a timer. Check that timer's time, compare to the measured duration, and render the progress meter.

QR code documentation

timer.currentTime

1 Like

That sounds great! Speaking as a fellow Novan. (Although my releases will be few, so manual build numbers aren't such a burden. Famous last words.)

1 Like

This is smart! I hope to see some implementation of this in games I sideload in the future.

2 Likes

I have enhanced my example page: it now automatically adds decimal points to show a "version number" generated from the "build number." (Only for builds above 99.)

It just inserts dots separating the last two digits: 105 --> 1.0.5

You may or may not want that... it's ONLY for end-user readability. So for example, version "2" is still considered older than "1.0.1" because build 2 is lower than build 101.

If this sounds useful to you, here is updated Javascript code with that feature added:

<script>
var latestBuildNumber = 1234;
var appName = "My Game Title";

var lastParameter = window.location.search.substr(1).split("&").slice(-1)[0];

if (lastParameter.substr(0, 6) == "build=") {
	var buildNumber = parseInt(lastParameter.substr(6));
	var statusMessage = "You have " + appName + " version " + insertPoints(buildNumber) + ".";
	
	if (buildNumber == latestBuildNumber) {
		statusMessage += "<br><strong>You are up to date!</strong>";
	} else if (buildNumber < latestBuildNumber) {
		statusMessage += "<br><strong>A new version (" + insertPoints(latestBuildNumber) + ") is available!</strong>"
	}
	
	document.getElementById("update-status").innerHTML = statusMessage;
}

function insertPoints(num) { //Separate last two digits of build number with decimal points
	num += "";
	var length = num.length;
	if (length > 2) {
		num = num.substr(0, length-2) + "." + num.charAt(length-2) + "." + num.charAt(length-1);
	}
	return num;
}
</script>
1 Like