+1. I had a few ideas for user-driven content that are sadly limited by the lack of networking. Android-esque permissions with a suitable warning from Playdate seems like a sufficient compromise to handle security concerns.
I would love the the prospect of being able to add real-time multiplayer to a project. Has there been any word as to what Panic has in mind for support?
What if networking allows only a fixed network API that can only send pre-defined fixed packet types?
Data/requests can only goto a nearby Playdate in a mesh configuration, or through a Panic server. It seems reasonably straightforward. Just don't allow unfettered network comms.
That should be enough to allow multiple players, and stop abuse.
I could work out an API for disussion if people think it's worthwhile.
This seems like a cool approach! Has any other device or service implemented it in that way?
Maybe the Switch does something like this in local (non internet) mode. The wifi range is really low in this mode, sometimes we can't get it to go > ~3m.
Thinking about this a bit more, here's a few API calls off the top of my head:
- int startLocalNetwork()
- returns your local network game player-id (or maybe the API just holds this for you?)
- -1 on failure
- void stopLocalNetwork()
- List findLocalPlayers( int game_id, string player_name )
- returns a list of player-IDs wishing to network play the given game
- only works for players on the local subnet / ad-hoc network. Not internet play.
- software wanting to network play, calls this function. So players A and B both call this, and A is returned B, and B is returned A.
- the <player_name> is optional, but is a human-readable string able to be set by the player. Profanity filtered (which is impossible IMHO) if necessary.
- addPlayer( List players, int player_id );
- Add the given remote player to some Corpus of "current players".
- Allows certain players to not be added?
- sendEvent( List players, int event_id, float at_x, float at_y, uint32 data_a, uint32 data_b );
- sendEventWithData( List players, int player_id, int event_id, float at_x, float at_y, octet data[64] );
- Send the event to every remote game accepted into the List.
Where a List
might have a player_count
and an array of player id
s. (say with a max of 16?)
I would use fixed size packets for all this, they're small with a known size. In terms of underlying protocol, the first byte of the packet would be it's type/size, so the API knows exactly how much data to expect - and can junk the packet if the remainder it doesn't arrive in a timely manner. With such small packets they'd probably all arrive in a single chunk anyway.
The wifi MTU can be set small in this mode.
The idea with the Event and Location, is that the game only needs to know when an entity is started. Say for whatever reason, a new object appears in the local game. It calls sendEvent()
with the "new object" command ID (any number), and a location (in any sense). The two data parameters can be used for whatever else is needed. Say direction and speed, colour, flavour, hit-points, etc. etc.
For more involved events, there is sendEventWithData()
. This also keeps it simple. There is some extra space for packing bigger payload, but it's still small enough to transmit quickly.
It should never be necessary to transmit say, game assets, as the "other" game already has these. Maybe we'd add some more special-purpose commands, but pretty much everything can be done with these simple events. If you've ever looked at underneath the messaging queue for any window based OS, they use exactly this sort of simple event-messaging. (Back in former times, one had to process these messages manually.) Keeping the packets small should at least hinder sending recorded voices, etc. Doesn't prevent it though, but it only goes to the local sub-net.
Anyway, just some initial thoughts.
Instead if adding a player, wouldn't it make more sense for a player to join a local network ?
Something like:
List findLocalNetworks()
joinLocalNetwork(int network_id)
Yeah, you're right. You only ever need to know about the current game running. I guess there could potentially be multiple local sets of games running though.
NetGameIdList findLocalNetworks( int current_game_id );
boolean joinLocalNetwork( int net_game_id );
Returns a list of network games in progress. [Q: Do game-networks need a human-readable name? What if there's say 3 in progress, how can the user pick which to join? Is this really a problem?]
If you're the first person starting though (even by a nanosecond), this would be an empty List. So perhaps the API can handle this internally by creating a new network-game identifier once the various timeouts & re-tries have failed to find a game. Internally there should be some random-backoff when colliding the "start new game" network token.
Once you have a game-ID, events are sent received against this ID. This makes the sendEvent()
functions something like:
sendEvent( net_game_id, event_id, at_x, at_y, data_a, data_b );
It would be good if the networking API could just send received events directly into the game event queue. Internally I would just keep a local queue/buffer, filling it with a network-handle select()
(immediate timeout). Since we know how big every message is, it's easy to know when a full message is received. Pull those bytes out of the incoming queue, form them into a "Network Event" and post it out the event queue.
I was thinking about the "player-adding" in terms of refusing a player access. Maybe we don't need this at all.
Ugh, chores. BBL.
This sounds very familiar from Nintendo DS. I'll see if there's any information about how they did it.
I'm not sure how helpful this is as it is overkill, however, I found that mirror for Unity was well done.
Their API doesn't sound too disimilar to how @UrlOfSandwich described it with ConnectHost(), Send(), and AddPlayer() etc.
https://storage.googleapis.com/mirror-api-docs/html/df/d6f/class_mirror_1_1_network_client.html
I had a thought on how to expose a simple networking API without risking publishing user specific data:
Have the game register a single URL in their pdxinfo
file:
name=b360
author=Panic Inc.
description=When all you have is a ton of bricks, everything looks like a paddle.
bundleID=com.panic.b360
version=1.0
buildNumber=123
dataUrl=https://example.com/fetch-game-data
This URL cannot be changed at runtime, nor can it be paramerized. GET
only, no POST
s.
Expose access to this URL behind a function that does the āgetā for you:
playdate.dataUrl.fetch(function(string, error)
-- Callback that gets invoked when when the dataUrl is fetched
-- On failure, string is nil and the 2nd parameter contains the error
end)
(I'm not a Lua person, but hopefully that's close enough to get the idea across)
A few random points about this implementation:
- This would be useful for puzzle games that want to provide daily challenges. Think "Wordle". It would also allow developers to push configuration based updates to their games
- It's not designed to solve all use cases, but that doesn't mean it's not useful for a big subset of use cases
- From a security perspective, data flow is strictly one way -- the device can't send any identifying information to the server, it can only read
- The SDK controls the requests itself, so it should be able to sufficiently clean any identifying information about the device. You could even proxy the request through a Panic owned server if you want
- From the outside, this feels fairly low-cost and low-risk as a mechinism to dip your toes into networking without closing the door to more complicated solutions in the future. Obviously, I don't know the internals of the SDK, so take that for what it's worth.
Also, let me add that I'm still new to the playdate community. My apologies if this has already been suggested elsewhere.
No need to apologise, this seems quite elegant, actually.
Very nice, I like this solution.
It would also be super neat if this stripped-down (no params, no body) GET
request could still include some user-specific authorization
credentials / token, to be sent to the server, ... while still providing a nice user experience, and without it becoming a way for a malicious app to leak user data to a malicious server, under the guise of credentials.
Playing devil's advocate.
Some thoughts:
How would this be enforced?
Also...
If the device is offline only until it needs to be online, to save power, then there would be a connection delay before each call. Maybe that could be hidden with good design, but it would exclude game requiring anything more frequent or less intrusive.
Great points! My answers may underwhelm.
I donāt know enough about he build process or runtime isolation to answer wether this is feasible, but maybe someone else can. The general idea is that you use the build to ingest this data and store it in an a location that is inaccessible to the user runtime. Iām assuming there is a concept of user mode vs kernel mode that can enforce the runtime permissions here.
For the use cases Iāve personally got in mind, Iām fine with high latency. For example, downloading ātodays puzzleā or even a drop of new loot for a game.
This URL cannot be changed at runtime
How would this be enforced?
The single URL design can be made to work, but I do not like it. For example, a GraphQL API could be exposed, but GraphQL typically returns JSON. In theory, I might want to download non-JSON assets. I could, however, see enforcing a single host. (Furthermore, I could even see letting users override the host for a certain kind of game.)
dataHost=example.com
HTTPS could be hardcoded. Calling an endpoint would then look something like this.
endpoint = "graphql/or/something";
data = "{some(query:1){data more_data}}";
result = post(endpoint, data);
I am aware of a platform that requires web view content to be whitelisted before it is displayed. This is to prevent people from using the built in browser to view naughty material, but the same pattern can be used for data security.
As I understand it, the point of restricting network access is to prevent a malicious actor from exfiltrating sensitive information from the device. For example, turning on the microphone, secretly recording, and sending the audio somewhere.
The moment you allow the URL to change in any way, you start opening up that vulnerability. Any kind of parameterization is a vector for data exfiltration. Even allowing multiple URLs could be a problem, as theoretically you could call them in a specific sequence to indicate bits being on and off.
The point of my suggestion was to open up something without conceding this point.
(I reserve the right to be corrected by someone more knowledgeable on this, btw)
The point of network access is to exchange information with other parties. Over restricting network access makes it useless for standard use cases. Apple has solved the problem about as well as it can be solved with permissions for iOS apps. The platform gives a list of permissions that the user is free to review.
Users are free to refrain from installing or running any application they do not trust. Permissions can be selectively disallowed, but this make many apps unusable in practice. The important point is to give concerned users enough information to make an informed decision. Most people are arguably dangerously unconcerned. Especially paranoid people correctly do not trust any app with certain permissions.
Without a request-response model, all I can do is poll a fixed URL for the latest update. Any sort of real online game requires two way communication so the player can interact with other users and the game server. Most games and other apps absolutely log information. Some even cross into the territory of inappropriate logging. The kinds of people who play networked games just accept the practical realities of the network.
A platform manufacturer needs to be concerned with the legalities of doing business to stay in business. Maintaining customer goodwill is also important. Therefore, there is an incentive for above board actors to out bad actors if anything becomes a problem.
Youāre asking for fully open network access. Iām not an employee of Panic, nor am I a lawyer, so Iāll not comment on whether thatās reasonable.
My proposal is a compromise that allows the āmalicious actorā question to remain unresolved. I make no assertions that it is an ideal solution, only that it opens up support for a specific set of use cases. And it doesnāt block further APIs from being added down the road, should the decision be made to open the networking flood gates.
Donāt let perfect be the enemy of good.
Read only network access for a single URL is not nothing. It could be designed around, but it is too locked down for the vast majority of regular networked use cases.
Example 1. User generated content, like Mario Maker. The URL could be used to download a few random stages. These stages would need to be uploaded outside of the game. Maybe in an online editor, or maybe the game writes a JSON file that can be extracted from the device. To the extent the player needs to extract files from the devices, it might just make more sense to allow them to browse stages online and upload them manually.
Example 2. An auction house. The operators of the game could push a list of daily or weekly items using the URL, but rules for auction house content could also just be baked into the game. User purchases and new auctions cannot really be posted. As above, some sort of file could be generated for upload, but users need to be fairly invested in the experience to bother with all of that.
My question is, what use case am I missing? What does access to a single read-only URL allow me to build? How is this experience compelling for users?
EDIT: If the URL is not read-only, then the malicious actor problem has not actually been solved. Furthermore, parameterized requests are equivalent to calling different URLs with the same host, but the mechanics are just slightly different.