Whoa, this will be challenging to explain… So basically there are three parts of a URL: the socket, path and fragment. In your example they are “main:”, “/controller” and “#gui” respectively. The resulting URL is not a hash of the concatenation of these strings. The internal representation is an object with the three fields:
local url = msg.url("main:/controller#gui")
print(url.socket)
print(url.path)
print(url.fragment)
Gives:
DEBUG:SCRIPT: 786443
DEBUG:SCRIPT: hash: [/controller]
DEBUG:SCRIPT: hash: [gui]
For convenience, we provide a lot of magic to create them, like allowing a string to be specified and even a hash. In your example, you use a hash and the engine then assumes this is the path you are interested in, and assumes the socket to be the same as the calling script, and the fragment to just be omitted. When resolving the URL on that socket (your collection), the engine tries to find the game object with path “main:/collection#gui”, when the path actually is “/controller”. In this case that is extremely confusing, as it looks like the engine perfectly understood your URL and reports it back to you.
The fix is to change controller.gui_script to register URLs instead of hashes:
settings.register_listener(msg.url("main:/controller#gui"))
The next annoying thing is since the URL is a complex object (the three fields), it cannot be used as a key into a lua table (which is something we should look into fixing as it causes a lot of grief). So in the settings module, you need to key the url by its path:
M.register_listener = function(url)
state_listeners[url.path] = url
end
This will work fine as long as your settings module does not need to simultaneously care about different dynamic collections, loaded through collection proxies. Because that will mean the socket will be changed for each different one, and you would need to consider that when registering the listeners.