···55server/bin/
66server/tmp/
77server/lunar-tear
88+server/import-snapshot
89910__pycache__/
1011···13141415# Go
1516server/vendor/
1616-1717-# Certs (regenerate per-environment)
1817server/certs/
1919-2020-# Server assets (binary data, too large for git)
2118server/assets/
1919+db/
22202321# Snapshots (recorded user state)
2422snapshots/
+77-9
README.md
···7788### Prerequisites
991010-- Go 1.24+
1010+- Go 1.25+
1111+- [goose](https://github.com/pressly/goose) migration tool
1112- Populated `server/assets/` directory
12131414+```bash
1515+go install github.com/pressly/goose/v3/cmd/goose@latest
1616+```
1717+1318### Regenerate protobuf stubs
14191520```bash
···1722make proto
1823```
19242525+### Database
2626+2727+Player state is stored in a SQLite database. Run migrations before starting the server:
2828+2929+```bash
3030+cd server
3131+make migrate
3232+```
3333+3434+Or manually:
3535+3636+```bash
3737+cd server
3838+mkdir -p db
3939+goose -dir migrations sqlite3 db/game.db up
4040+```
4141+4242+### Importing a Snapshot
4343+4444+To import a JSON snapshot into the database, use the import tool. The `--uuid` flag must match the UUID your game client sends during authentication:
4545+4646+```bash
4747+cd server
4848+make import SNAPSHOT=snapshots/scene_1.json UUID=<your-client-uuid>
4949+```
5050+5151+Or directly:
5252+5353+```bash
5454+go run ./cmd/import-snapshot \
5555+ --snapshot snapshots/scene_1.json \
5656+ --uuid <your-client-uuid> \
5757+ --db db/game.db
5858+```
5959+6060+| Flag | Default | Description |
6161+| ------------ | ------------ | --------------------------------------------- |
6262+| `--snapshot` | *(required)* | Path to JSON snapshot file |
6363+| `--uuid` | *(required)* | UUID to assign (must match the client's UUID) |
6464+| `--db` | `db/game.db` | SQLite database path |
6565+2066### Run
21672268```bash
2369cd server
2470sudo go run ./cmd/lunar-tear \
2571 --host 10.0.2.2 \
2626- --http-port 8080 \
2727- --scene 13
7272+ --http-port 8080
2873```
29743075`sudo` is needed because gRPC binds to port 443 (privileged). On Linux you can use `setcap` instead:
···3277```bash
3378go build -o lunar-tear ./cmd/lunar-tear
3479sudo setcap cap_net_bind_service=+ep ./lunar-tear
3535-./lunar-tear --host 10.0.2.2 --http-port 8080 --scene 13
8080+./lunar-tear --host 10.0.2.2 --http-port 8080
3681```
37823883### Ports
···44894590### Flags
46914747-| Flag | Default | Description |
4848-| ---------------------- | ------------------- | -------------------------------------------------------- |
4949-| `--host` | `127.0.0.1` | hostname/IP given to the client |
5050-| `--http-port` | `8080` | HTTP/Octo server port |
5151-| `--scene` | `0` | bootstrap new users to scene N (0 = fresh start) |
9292+| Flag | Default | Description |
9393+| ------------- | ------------ | ------------------------------- |
9494+| `--host` | `127.0.0.1` | hostname/IP given to the client |
9595+| `--http-port` | `8080` | HTTP/Octo server port |
9696+| `--db` | `db/game.db` | SQLite database path |
9797+9898+### Docker
9999+100100+Migrations run automatically on container start.
101101+102102+```bash
103103+cd server
104104+docker compose up -d
105105+```
106106+107107+The `db/` directory is mounted as a volume so the database persists across restarts. Make sure `assets/` is populated before starting.
108108+109109+### Makefile Targets
110110+111111+All targets run from the `server/` directory.
112112+113113+| Target | Description |
114114+| -------------- | ------------------------------------------------------- |
115115+| `make proto` | Regenerate protobuf stubs |
116116+| `make build` | Build the server binary |
117117+| `make build-import` | Build the import-snapshot tool |
118118+| `make migrate` | Run goose migrations on `db/game.db` |
119119+| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
5212053121## ⚠️ Legal Disclaimer
54122
+4-1
server/Dockerfile
···1212 libcap
13131414RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest &&\
1515- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest &&\
1515+ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest &&\
1616+ go install github.com/pressly/goose/v3/cmd/goose@latest &&\
1617 PATH="$PATH:$(go env GOPATH)/bin" make proto &&\
1718 go build -o lunar-tear ./cmd/lunar-tear &&\
1819 setcap cap_net_bind_service=+ep ./lunar-tear
···2627USER 1000
27282829COPY --from=builder /usr/local/src/lunar-tear .
3030+COPY --from=builder /root/go/bin/goose /usr/local/bin/goose
3131+COPY --from=builder /usr/local/src/migrations ./migrations
29323033COPY entrypoint.sh .
3134
+20-1
server/Makefile
···77 protoc -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server
88 @echo "Generated in gen/proto/"
991010-.PHONY: proto
1010+build:
1111+ go build -o lunar-tear ./cmd/lunar-tear
1212+1313+build-import:
1414+ go build -o import-snapshot ./cmd/import-snapshot
1515+1616+migrate:
1717+ mkdir -p db
1818+ goose -dir migrations sqlite3 db/game.db up
1919+2020+import:
2121+ifndef SNAPSHOT
2222+ $(error SNAPSHOT is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
2323+endif
2424+ifndef UUID
2525+ $(error UUID is required, e.g. make import SNAPSHOT=snapshots/scene_1.json UUID=...)
2626+endif
2727+ go run ./cmd/import-snapshot --snapshot $(SNAPSHOT) --uuid $(UUID)
2828+2929+.PHONY: proto build build-import migrate import
+56
server/cmd/import-snapshot/main.go
···11+package main
22+33+import (
44+ "encoding/json"
55+ "flag"
66+ "log"
77+ "os"
88+99+ "lunar-tear/server/internal/database"
1010+ "lunar-tear/server/internal/store"
1111+ "lunar-tear/server/internal/store/sqlite"
1212+)
1313+1414+func main() {
1515+ dbPath := flag.String("db", "db/game.db", "SQLite database path")
1616+ snapshotPath := flag.String("snapshot", "", "Path to JSON snapshot file (required)")
1717+ userUuid := flag.String("uuid", "", "UUID to assign to the imported user (must match the client's UUID)")
1818+ flag.Parse()
1919+2020+ if *snapshotPath == "" {
2121+ log.Fatal("--snapshot flag is required")
2222+ }
2323+ if *userUuid == "" {
2424+ log.Fatal("--uuid flag is required")
2525+ }
2626+2727+ data, err := os.ReadFile(*snapshotPath)
2828+ if err != nil {
2929+ log.Fatalf("read snapshot: %v", err)
3030+ }
3131+ log.Printf("read %d bytes from %s", len(data), *snapshotPath)
3232+3333+ var u store.UserState
3434+ if err := json.Unmarshal(data, &u); err != nil {
3535+ log.Fatalf("unmarshal snapshot: %v", err)
3636+ }
3737+ u.EnsureMaps()
3838+ u.Uuid = *userUuid
3939+4040+ log.Printf("parsed user %d (uuid=%s, costumes=%d, weapons=%d, characters=%d, quests=%d)",
4141+ u.UserId, u.Uuid, len(u.Costumes), len(u.Weapons), len(u.Characters), len(u.Quests))
4242+4343+ db, err := database.Open(*dbPath)
4444+ if err != nil {
4545+ log.Fatalf("open database: %v", err)
4646+ }
4747+ defer db.Close()
4848+4949+ userStore := sqlite.New(db, nil)
5050+5151+ if err := userStore.ImportUser(&u); err != nil {
5252+ log.Fatalf("import user: %v", err)
5353+ }
5454+5555+ log.Printf("imported user %d successfully", u.UserId)
5656+}
···11+-- +goose Up
22+33+-- Delete deck characters with empty weapons (always a bug).
44+DELETE FROM user_deck_characters
55+WHERE main_user_weapon_uuid = '';
66+77+-- Delete decks that reference deleted deck characters.
88+DELETE FROM user_decks
99+WHERE user_deck_character_uuid01 NOT IN (SELECT user_deck_character_uuid FROM user_deck_characters)
1010+ AND user_deck_character_uuid01 != '';
1111+1212+-- +goose Down
1313+-- No rollback needed: EnsureDefaultDeck recreates decks on next SetTutorialProgress call.