SOURCE / PINNED RELEASE
Made of little things.
Powder Tool V600Billion
- Release
- 142767edcab8…
- Author-recorded commit
- 6d92971effd0…
- License
- LICENSE
- Author’s source reference
- nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy
Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
Powder Toy saves on Nostr
=========================
`draft` `nappelin`
How the Powder Toy napplet (this repository) keeps shared saves, votes and
comments on relays. No new kind: a save is NIP-78 application data, social
is NIP-25 and NIP-22 at the save's address, taking something back is NIP-09.
Any client can read, show and answer these events; the napplet is one reader
and writer of them. Implemented in `src/server/events.js`,
checked by `test/server.test.mjs`.
The game's own server API (upstream The Powder Toy, `src/client/http/`) is
answered in the page from these events. That mapping is an implementation
detail of the napplet and is not part of this document.
## Save: `kind:30078` (NIP-78, addressable)
```json
{
"kind": 30078,
"content": "<the .cps save file, base64>",
"tags": [
["d", "powder-toy/save/<first 16 hex of sha256(lowercase(trim(name)))>"],
["t", "powder-toy"],
["title", "<name, 1-100 characters>"],
["summary", "<description, up to 2000 characters>"],
["published_at", "<unix seconds of the first publication>"],
["x", "<sha256 hex of the save file>"],
["size", "<bytes of the save file>"],
["alt", "Powder Toy save: <name>"],
["t", "<tag>"]
]
}
```
- **Addressing.** The `d` tag is derived from the name, so publishing again
under the same name (case and surrounding spaces ignored) replaces the save,
as saving over your own save does in the game. A different name is a
different save. `published_at` carries over from the first version.
- **Content.** The file exactly as the game writes it: `OPS1` magic, then
bzip2-compressed BSON (upstream `GameSave::serialiseOPS`). Readers must
treat it as untrusted input.
- **Size.** At most 45 000 bytes of save file, so the whole event stays under
the 64 KiB many relays accept. Larger saves are kept on the player's device.
- **Discovery.** `["t", "powder-toy"]` on every save; filter
`{"kinds": [30078], "#t": ["powder-toy"]}`.
- **Tags.** Further `t` tags are the save's tags: 1-16 lowercase letters or
digits, at most 20. Only the author changes them, by publishing the save
again.
- **Validity.** A reader ignores an event without the `powder-toy` topic, a
`d` outside `powder-toy/save/`, an empty title, or content that is not
base64 or is longer than the size limit allows.
Unpublished ("private") saves are **not** published at all. The napplet keeps
them in the host's napplet storage on the player's device. There is no
encrypted variant: a napplet cannot decrypt, so it could never read one back.
## Vote: `kind:7` (NIP-25)
```json
{ "kind": 7, "content": "+", "tags": [
["a", "30078:<author>:<d>"], ["e", "<save event id>"], ["p", "<author>"], ["k", "30078"]
] }
```
`+` is up, `-` is down. A player's vote is their newest reaction to the
address that has not been deleted. Authors do not vote on their own saves.
Taking a vote back is a deletion of it (below).
## Comment: `kind:1111` (NIP-22)
```json
{ "kind": 1111, "content": "<text, up to 2000 characters>", "tags": [
["A", "30078:<author>:<d>"], ["K", "30078"], ["P", "<author>"],
["a", "30078:<author>:<d>"], ["e", "<save event id>"], ["k", "30078"], ["p", "<author>"]
] }
```
A comment on the save as a whole: root and parent are the save's address.
Filter `{"kinds": [1111], "#A": ["30078:<author>:<d>"]}`.
## Deletion: `kind:5` (NIP-09)
- **A save**, by its author: `["e", "<save event id>"]`, `["a", "30078:<author>:<d>"]`,
`["k", "30078"]` and `["t", "powder-toy"]`. The topic tag lets readers fetch
deletions with the saves (`{"kinds": [5], "#t": ["powder-toy"]}`). A reader
hides every version of that address up to the deletion's `created_at`. A
deletion signed by anyone but the author is ignored.
- **A vote**, by the voter: `["e", "<reaction id>"]`, `["k", "7"]`. It carries
no `a` tag, because an `a` tag in a deletion means "delete this address" and
the voter does not own it.
## Names
The game needs a username per author. The napplet derives one from the
author's kind-0 `name`, or `display_name` when there is none (letters, digits,
`_` and `-`, at most 16), followed by
`_` and the first four hex digits of the key, for example `alice_84bf`; without
a usable name it is `nym_` and the first eight hex digits. The suffix keeps
two authors with the same profile name apart. Names are display only; the
key is the identity.
## Save IDs
The game shows integer save IDs. The napplet derives them from the address
(32-bit FNV-1a of `30078:<author>:<d>`, lowest 31 bits, never 0), so a save
has the same ID for every player, and typing the ID into the game's search
finds it.
