# Sending feedback from inside a game

The contract for a game that lets its players report a bug, send an idea, or complain. Both of this
site's games use it: **TGSC** (Touching Grass Simulator Coop, on PC) and **Touch Grass** (on
Android). They are separate games with similar names, and each has its own address and its own
queue.

One call, one form, and the site sorts it. There is no account and no token - a game cannot ask a
player to sign in to a website before telling you the game crashed.

Base address: `https://dasmaffin.com`.

---

## Send one

```
POST /TouchGrass/api/report        (Touch Grass, Android)
POST /Tgsc/api/report              (TGSC, PC)
Content-Type: multipart/form-data
```

### Does it need a key?

Every game chooses one of three, on its own settings page, and **Open is the default** - which is
how both games have always worked and what they still do until somebody changes it.

| Mode | Send this | For |
| --- | --- | --- |
| Open | nothing | Anybody can file a report. No key to ship, no key to leak |
| Public key | `X-Api-Key: dmp_...` | A game with no server of its own. Ships inside the game |
| Private key | `X-Api-Key: dms_...` | A game whose own server relays the report |

A public key is an **identifier, not a secret**: it is in every copy of the game and anybody can
read it out. It is worth having so one build can be turned off without turning the site off, and it
is worth nothing as protection. Do not build anything on it being private, and do not use it where
a private key belongs.

Refusals carry a word to branch on: `401` with `{"error":"no_key"}` when none arrived, and
`{"error":"bad_key"}` when the one that arrived was not this game's. They are told apart because a
build agent that reads one bare `401` as "our key was rotated" will stop publishing over a shell
that ate a header.

The same key covers [replies](replies). It does not replace the separate issued key the private
read API needs - see [reading](reading), which is gated on its own grant in every mode above.

**The address decides the queue.** Nothing in the body says which game this is; the site stamps it
from the endpoint you posted to. A client cannot file into the other game's queue, by accident or
otherwise.

Both addresses are the same endpoint under a different name: a module on this site declares that it
takes feedback, and the site answers `POST /{module}/api/report` for it.
A third game would appear here without a line of new intake code, and would behave exactly as these
two do.

### The fields

| Field | Touch Grass | TGSC | Meaning |
| --- | --- | --- | --- |
| `playerId` | **required** | - | Who is sending. The Unity Gaming Services player id, the same one the save is filed under |
| `steamId` | - | **required** | Who is sending. A SteamID64, digits only |
| `summary` | **required** | **required** | One line. Up to 300 characters |
| `description` | **required** | **required** | What happened, in their words. Up to 8,000 |
| `kind` | optional | optional | `Bug`, `Idea` or `Other`. Anything else becomes `Other` |
| `playerName` | optional | optional | What they call themselves in game. A label, not identity |
| `build` | optional | optional | Your version string |
| `scene` | optional | optional | Where they were - the screen or level |
| `facts` | optional | optional | Device, OS, settings, anything else worth knowing. Up to 8,000 |
| `edition`, `role`, `world` | - | optional | PC game concepts; leave them out |

Anything longer than its limit is cut, not refused - a report is worth having with a truncated log.

**`summary` and `description` are both required.** An empty report costs nothing to send, arrives
looking like feedback, and can only be answered by guessing.

### Attachments

**One field per kind, and each takes as many files as you like.** The field name says what a file
is; the file name is only a label for whoever reads the report afterwards. A part sent as `logs`
carrying `whatever.json` is a log. A part sent as `notes` carrying `screenshot.png` is not a
picture.

| Send it as | What it is | Where it appears on the report |
| --- | --- | --- |
| `images` | Pictures | Shown, full size on click |
| `logs` | Log files | The first is shown as text on the page; every one is downloadable |
| `charts` | Measurements over time | Plotted - see [charts](charts). Also downloadable |
| `saves` | A save or world file | Under Files, as a download |
| `benchmarks` | A finished benchmark run | Shown as text |
| `files` | Anything else | Under Files, as a download |

Repeat a field to send several: two parts both named `logs` are two logs, and there is no numbering
scheme to follow. Multipart allows a repeated field name, so nothing has to be invented per file.

```bash
curl -X POST https://dasmaffin.com/TouchGrass/api/report \
  -F "playerId=..." -F "summary=..." -F "description=..." \
  -F "logs=@session.json;type=application/json" \
  -F "logs=@previous-session.json;type=application/json" \
  -F "images=@shot1.png;type=image/png" \
  -F "images=@shot2.png;type=image/png" \
  -F "charts=@metrics.json;type=application/json"
```

**A field name that is not on that list lands under Files**, as a plain download. Nothing is
guessed from the file's name or its content: a file the site was not told the kind of is one it
will not describe. It used to guess "picture", which put a game's second log in the Pictures
section as a broken image.

**The older names still work**, because both of this site's games send them and neither should
break before it ships a new build: `shot`, `log`, `metrics`, `save`, `benchmark`. That is the whole
list - exactly what those two emit, not a set of spellings that might be accepted. Anything new
should use the fields in the table, which are the kinds themselves.

Set `Content-Encoding: gzip` on a part and it is **unpacked before it is stored**, so what somebody
opens is the plain thing. Compress for the upload's sake, not for the reader's.

### Charts

A file part named `metrics` is plotted on the report's page. Its shape is its own document:
[charts](charts).

### What comes back

```json
{ "id": 412 }
```

| Code | Meaning |
| --- | --- |
| `200` | Stored. The id is the report's |
| `400` | No sender id, or a missing `summary` or `description` |
| `401` | This game needs a key and none came (`no_key`) or the wrong one did (`bad_key`) |
| `403` | That player is barred from sending. Body: `{"banned":true,"reason":"..."}` - show the reason |
| `413` | Everything together came to more than 16 MB |
| `429` | More than 12 reports from that player in the last hour |

A `403` is worth handling properly: the reason is written for the player and is the only explanation
they will get. Touch Grass has no ban list today, so it will not see one - the code is documented
because the endpoint can answer it the moment there is one, and a client that treats an unknown
status as "it worked" is a client that silently loses reports.

---

## An example

```bash
curl -f -X POST \
  -F "playerId=Ax7Bq2Kd9Lm4Np6Rs8Tv0Wx2Yz4A" \
  -F "summary=Garden resets after closing the app" \
  -F "description=Planted three plots, closed the app from recents, came back and the garden was empty." \
  -F "kind=Bug" \
  -F "playerName=Someone" \
  -F "build=1.0.4" \
  -F "scene=Garden" \
  -F "facts=Pixel 7a, Android 15, English" \
  -F "log=@player.log;type=text/plain" \
  https://dasmaffin.com/TouchGrass/api/report
```

---

## Reading them back

Two different jobs, and they are not the same document.

A game client showing a player their own reports, the replies written back, and letting them answer
or close a thread: [replies](replies). No API key, because the caller is the player. TGSC only
today, and that document says exactly why and what Touch Grass would need.

A program on the project's side reading the whole queue: [reading](reading). It needs a private API
key, and so does its documentation.

---

## What happens to it

It lands in that game's queue on this site, where somebody reads it, can answer it, and can close
it. Reports are deleted after 120 days.

**The player id is what the game claimed**, not something this site proved, and it is treated that
way: it is good for grouping one person's reports together and for answering them, and for nothing
else. Send the id you already have rather than inventing one per report, or two reports from the
same person cannot be recognised as such.

Nothing here needs an account, on either side. What a player types is what arrives.
