Liveupdate Hell 🔥

I’m having too many issues setting up liveupdate. It seems simple in theory and even the extension appears like it’s plug and play. Unfortunately for me, it’s giving me a lot of issues.

First, mounting. Should mount on start be enabled or not? Does enabling it slow down loading?

Second, what’s the best way to debug? The manual mentions that a terminal will open when opening the application - but it doesn’t. Is there any way to trigger a terminal for the application to test a build? That would help a lot.

Finally, I’m only interseted in Poki integration right now. This means I wouldn’t need hosting, I think. I’m keeping it simple for now with one zip file that has levels 2-20. Level 1 loads with the build.

I’ve excluded the collection proxies. The problem I have right now is the level 2 doesn’t load. I can’t understand why because no prints are available in a build.

My init() looks like this

local zip_url = “http://localhost:8000/levels.zip”
local zip_name = “levels.zip”
local current_level = data.get_level()
local proxy_url = “/levels#level_” .. current_level

if current_level == 1 then
	msg.post(proxy_url, "load")
	return
end

local missing = collectionproxy.missing_resources(proxy_url)
if #missing > 0 then
	http.request(zip_url, "GET", function(self, id, response)
		if response.status == 200 then
			local path = sys.get_save_file("game", zip_name)
			local file = io.open(path, "wb")

			if file then
				file:write(response.response)
				file:close()
			end
			
			liveupdate.add_mount("levels", "zip:" .. path, 10, function(uri, status)
				if status == liveupdate.LIVEUPDATE_OK then
					msg.post(proxy_url, "load")
				end
			end)
		end
	end)
else
	msg.post(proxy_url, "load")
end

It does not slow down loading in a measurable way. When you mount an archive we store the path in a file. Next time you boot we read this file and remount the archives. Mounting an archive means reading the archive index, but not the entire file.

The main reason for not automatically mounting an archive is if you’ve made a mistake and for some reason the archive contains something game breaking and you want to avoid automatically mounting it.

I suppose you are referring to: Live update content in Defold

This is a bit misinforming. There is no bespoke “live update debug console”. The manual refers to how a debug version of the engine outputs log (ie print and engine logs) to the console, like this: Debugging - game and system logs

We should fix this part of the live update manual tbh.

Correct, with Poki you can bundle your game and then zip up your html5 bundle and the live update .zip file when uploading. The live update archive can then be loaded over http to a file and then mounted. Something like this:

local url = "./levels.zip"
local headers = nil
local post_data = nil
local options = { path = "levels.zip" }  -- the local file to store archive in 
http.request(url, "GET", function(self, id, response)
    if response.status == 200 or response.status == 304 then
         print("stored ok")
         liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) end)
    else
        print("error")
    end
end, headers, post_data, options)

Make a debug build and check the browser console.

2 Likes

Thank you so much. I will try this.

Some progress. I’ve ran into this issue before. I think I almost have it.

I think I need to run a check and see if the mount exists before trying?

Maybe this example can be useful. It loads live update, retried if error. Remove old mounts

For test, i use qa poki inspector. Bundle debug build with live update and you will see logs. Liveupdate zip is in bundle folder

2 Likes

Thank you. Right now I run test on a local server. This saves time from uploading new versions constantly to Poki. Is there a major difference between the inspector and local?

1 Like

No difference, if it worked local it should worked on poki

Almost got it working. The first build on a fresh session loads, but refreshing the page doesn’t load the level. I think the order of operations is wrong. I’m testing it again after changing the logic.

Here is the current code.

function init(self)
	self.level = nil

	local current_level = data.get_level()
	local proxy_url = "/levels#level_" .. current_level

	if current_level == 1 then
		msg.post(proxy_url, "load")
		return
	end
	
	local mounts = liveupdate.get_mounts()
	local levels_loaded = false

	for i = 1, #mounts do
		if mounts[i].name == "levels" then
			levels_loaded = true
			pprint(mounts)
			print("levels mounted")
			msg.post(proxy_url, "load")
			break
		end
	end

	if not levels_loaded then
		local url = "./levels.zip"
		local headers = nil
		local post_data = nil
		local options = { path = "levels.zip" }  -- the local file to store archive in 

		http.request(url, "GET", function(self, id, response)
			if response.status == 200 or response.status == 304 then
				print("stored ok")
				liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) end)
				pprint(mounts)
				msg.post(proxy_url, "load")
			else
				print("error")
			end
		end, headers, post_data, options)
	end

	msg.post("#", "acquire_input_focus")

	-- if html5 then
	-- 	poki_sdk.commercial_break(commercial_level_completed)
	-- end
end

I restructured a lot and I think I have it working as intended. I ended up using my “main” proxy as the loader for the levels. Previously, I was using main and game/menu. This created unnecessary friction because the levels were being loaded in the game proxy.

This was a frustrating experience, but I’m glad to have made progress. I’m now testing my 20 levels to see if the faster load times will improve the player experience.

My final code for checking live updates now runs in main, the collection that loads during boot.

if data.get_level() == 1 then
		load_level(self)
	else
		load_menu(self)
	end
	
	local mounts = liveupdate.get_mounts()
	for i = 1, #mounts do
		if mounts[i].name == "levels" then
			pprint(mounts)
			print("levels already mounted")
		else
			local url = "./levels.zip"
			local headers = nil
			local post_data = nil
			local options = { path = "levels.zip" }  -- the local file to store archive in 
			if html5 then
				http.request(url, "GET", function(self, id, response)
					if response.status == 200 or response.status == 304 then
						liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) end)
						print("new levels successfully mounted")
					else
						print("error with mounts")
					end
				end, headers, post_data, options)
			end
		end
	end

One thing I can’t explain is that the prints aren’t printing. I thought maybe it was because they were already loaded but they still don’t print after a cleared session. I’m building a HTML5 build in debug, but still not seeing any prints in the Chrome dev console.

Something else to consider next:

  • Loading each level as needed, but this may be overkill. The game is already under 5mb when first loading. The additional content is maybe 25mb. Maybe I could bundle the levels as load as needed — or — only preload the next level. The game plays one-way, so once a level is completed the player won’t go back to play it again.

Results are in from the 10-player test.

Not bad!

2 Likes

I’m still getting errors from some players, especially returning. This error occured 91 times in a previous test, which means 20% of the players couldn’t continue. It’s a major issue. Not sure how to fix it.

Update: I think I found the issue. It’s trying to load the levels before they are fully downloaded. I had assumed they were small enough to have a quick download, but that’s not always the case. I changed the code so the menu won’t reveal itself until the content is downloaded.

New players who start at level 1 should have the content downloaded by level 2, but returning players wil have a delay. I will have to look at how to load everything better.

This does not sound right. The content should already be downloaded for a returning player?

I think I fixed it. I must not have been checking or loading something in the right order. Recent tests are error free and I stopped seeing players having issues with loading.

Overall, Liveupdate is a very powerful feature! I want to understand it better. I’m still questioning how mounting works.

For example, this print still shows missing resources after loading the page.

local proxy_url = "/levels#level_" ..  data.get_level() + 1
pprint("missing resources - current level + 1", collectionproxy.missing_resources(proxy_url))

But I don’t understand why, because here I’m mounting the levels when the game first loads.

local mounts = liveupdate.get_mounts()

local proxy_url = "/levels#level_" ..  data.get_level() + 1
pprint("missing resources - current level + 1", collectionproxy.missing_resources(proxy_url))

for i = 1, #mounts do
	if mounts[i].name == "levels" then
		data.levels_loaded = true
		break
	end
end

if not data.levels_loaded then

	if html5 then
		local url = "./levels.zip"
		local headers = nil
		local post_data = nil
		local options = { path = "levels.zip" }  -- the local file to store archive in 

		http.request(url, "GET", function(self, id, response)
			if response.status == 200 or response.status == 304 then
				liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) end)
				data.levels_loaded = true
				print("levels mount added")
			else
				print("error with mounts")
			end
		end, headers, post_data, options)
	end
end

if data.get_level() == 1 then
	load_level(self)
else
	load_menu(self)
end

Does it always show missing even if the resources are loaded?

An even more efficient way of loading levels would be to preload the level after the current. So if player is on level 1, get level 2 ready, and so on. And to make that even better, the levels could unload / unmount after the player completes them.

Further testing, I’m still not seeing the levels mount in the mounts print … but the levels after 1 are loading. Maybe I’m misunderstanding how mounting works.

My latest code.

local mounts = liveupdate.get_mounts()

	local proxy_url = "/levels#level_" ..  data.get_level() + 1
	pprint("missing resources - current level + 1", collectionproxy.missing_resources(proxy_url))
	pprint("current mounts", mounts)

	pprint("manifest", liveupdate.get_current_manifest())
	pprint("is using liveupdate data?", liveupdate.is_using_liveupdate_data())
	
	for i = 1, #mounts do
		if mounts[i].name == "levels" then
			data.levels_loaded = true
			break
		end
	end

	if not data.levels_loaded then

		if html5 then
			local url = "./levels.zip"
			local headers = nil
			local post_data = nil
			local options = { path = "levels.zip" }  -- the local file to store archive in 
			-- if html5 then
			http.request(url, "GET", function(self, id, response)
				if response.status == 200 or response.status == 304 then
					liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) end)
					data.levels_loaded = true
					mounts = liveupdate.get_mounts()
					pprint("levels mount added", mounts)
					pprint("missing resources check? current level + 1", collectionproxy.missing_resources(proxy_url))
				else
					print("error with mounts")
				end
			end, headers, post_data, options)
		end
		-- end
	end

And the dev console in Chrome looks like this.

Are mounts loaded once and then persistant? As in they don’t appear in “liveupdate.get_mounts()” after the initial mount session?

That’s what the "mount on start " is about. But we recommend not having it mount automatically on start, as then it may cause trouble for you if it’s an old data format, and you use a new engine.

We recommend that you each session download the zip archive (the http.request() will cache the result, so if it’s not changed on the server, you’ll get a quick response), then mount the .zip file.

Thank you. That’s what I’m doing currently – I’m not using mount on start.

Every session is returning a response.status of 200 during a refresh. If it’s cached, shouldn’t it be 304?

Hmm, so this is only for if mounts are enabled on start?

But if not enabled and using an http request, the process of adding/removing is handled already?

If so, I think I can understand it better now. I also shouldn’t be using “liveupdate.get_mounts()” then during boot to check if the player hasn’t added the mount yet. There’s likely a better way.

I simplified it to this:

data.levels_loaded = false -- flag to check if levels have been loaded, updates gui on success
	
	if html5 then
		local url = "./levels.zip"
		local headers = nil
		local post_data = nil
		local options = { path = "levels.zip" }  -- the local file to store archive in 
		-- if html5 then
		http.request(url, "GET", function(self, id, response)
			if response.status == 200 or response.status == 304 then
				pprint("response", response)
				liveupdate.add_mount("levels", "zip:/levels.zip", 10, function (result) 
					pprint("result: ", result)
					data.levels_loaded = true
				end)
			else
				print("error with mounts")
			end
		end, headers, post_data, options)
		pprint(headers, post_data, options)
	end

	if data.get_level() == 1 then
		load_level(self)
	else
		load_menu(self)
	end

The flag in data is set during boot and checked in the gui. The player cannot continue to the later levels until they’re loaded.

I think this takes care of html5. Next, apps…

Still wrapping my head around mounting.

Here it talks about how mounts are added and removed. It seems like a mount is added if the resources for it don’t exist, then removed after the resources are downloaded. Is that correct?

If so, I don’t understand when get_mounts() would be used. I don’t see the mounts at all when my game loads, but the resources (level collection data) is there. Those load okay.

Mounting is a manual operation (unless you have the automount enable, which we don’t recommend).

I don’t understand when get_mounts() would be used.

It is to inspect the current mounts. For a small game it’s easy to keep track of, but for a complex game with lots of mounts, it’s good to have it, in order to see the actual state.