# 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
```

**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` |
| `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

There is a private read API for programs on the project's side - not for game clients. It needs a
private API key, and so does its documentation: [reading](reading).

---

## 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.
