Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c47391b6db | ||
|
|
6746d89233 | ||
|
|
cb25497bb8 | ||
|
|
9c7caf50a7 | ||
|
|
df24edd0ab | ||
|
|
fb18a6f289 | ||
|
|
01eec92d48 | ||
|
|
e1bfc2b5d7 | ||
|
|
0a47ac9234 | ||
|
|
de6f0438c5 | ||
|
|
01cefbbe77 | ||
|
|
7b48c01a93 | ||
|
|
d626a66ff7 | ||
|
|
128c5cf16e | ||
|
|
a1c7cfdc9b | ||
|
|
b5566ec73e |
@@ -29,4 +29,27 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose -f docker-compose.prod.yml build
|
docker compose -f docker-compose.prod.yml build
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
docker image prune -f
|
|
||||||
|
# Если сборка/подъём упали — шаг упадёт, а старые контейнеры продолжат
|
||||||
|
# работать (up -d не выполнился). Проверяем, что новое поднялось по
|
||||||
|
# healthcheck'ам обоих сервисов, и что nginx реально отдаёт главную.
|
||||||
|
- name: Smoke check
|
||||||
|
working-directory: /data/docker/compose/cactoz
|
||||||
|
run: |
|
||||||
|
n=starting
|
||||||
|
g=starting
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
n=$(docker inspect -f '{{.State.Health.Status}}' cactoz-nginx 2>/dev/null || echo starting)
|
||||||
|
g_id=$(docker compose -f docker-compose.prod.yml ps -q gameserver)
|
||||||
|
g=$([ -n "$g_id" ] && docker inspect -f '{{.State.Health.Status}}' "$g_id" 2>/dev/null || echo starting)
|
||||||
|
if [ "$n" = "healthy" ] && [ "$g" = "healthy" ]; then break; fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
docker compose -f docker-compose.prod.yml ps
|
||||||
|
[ "$n" = "healthy" ]
|
||||||
|
[ "$g" = "healthy" ]
|
||||||
|
docker exec cactoz-nginx wget -qO /dev/null http://127.0.0.1/
|
||||||
|
|
||||||
|
- name: Prune dangling images
|
||||||
|
working-directory: /data/docker/compose/cactoz
|
||||||
|
run: docker image prune -f
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# cactoz.su
|
||||||
|
|
||||||
|
Личный сайт-визитка на `cactoz.su` в Matrix-стилистике: резюме, проекты и
|
||||||
|
браузерные игры. Три страницы статики + мультиплеерный сервер для `./tanks`.
|
||||||
|
Исходный замысел и игровой протокол — в [TZ.md](TZ.md) (⚠️ местами устарел).
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────── homelab (cacto-homelab) ──────────┐
|
||||||
|
push в main │ │
|
||||||
|
в Gitea ────────────► │ gitea-actions runner (docker.sock хоста) │
|
||||||
|
git.cactoz.su │ │ клонирует, синкает, собирает │
|
||||||
|
│ ▼ │
|
||||||
|
│ /data/docker/compose/cactoz │
|
||||||
|
│ docker compose -f docker-compose.prod.yml │
|
||||||
|
│ ├── nginx (статика + прокси /ws) │
|
||||||
|
│ └── gameserver (C++/uWebSockets, :9001) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ (external docker-сеть proxy) │
|
||||||
|
│ Caddy-стек (TLS-терминация 80/443) │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Деплой**: push в `main` → workflow `.gitea/workflows/deploy.yml` → build +
|
||||||
|
`up -d` + smoke-проверка по healthcheck'ам. Отката нет: если сборка/подъём
|
||||||
|
падают, шлётся уведомление, старые контейнеры продолжают работать.
|
||||||
|
- **TLS** терминирует отдельный Caddy-стек, сеть `proxy` создаётся там
|
||||||
|
(`external: true`) — на этом хосте только подключаемся.
|
||||||
|
- Gameserver изнутри доступен только по docker-сети `internal`; наружу
|
||||||
|
смотрит только nginx (через Caddy).
|
||||||
|
|
||||||
|
## Локальный запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build # http://localhost, /ws на ws://localhost/ws
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка gameserver тянет зависимости через CMake FetchContent (uWebSockets,
|
||||||
|
uSockets, nlohmann/json) — первый билд долгий, дальше кэшируется слоями Docker.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/ чистый HTML/CSS/JS, без сборки
|
||||||
|
vendor/fonts/ self-hosted JetBrains Mono (OFL), 4 сабсета × 4 насыщенности
|
||||||
|
js/game/ клиент ./tanks: network (WS + ping 25с), lobby, battle
|
||||||
|
js/blocks.js, js/invaders.js одиночные мини-игры (canvas, без сервера)
|
||||||
|
gameserver/
|
||||||
|
src/main.cpp uWS::App, /ws, лимиты фреймов/таймауты
|
||||||
|
src/Lobby.* лобби: комнаты, broadcast, ping/pong, события в stdout
|
||||||
|
src/Game.* авторитетная симуляция (20 Гц), pvp и coop с AI
|
||||||
|
nginx/ статика + проксирование /ws (rate-limit на соединения)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runbook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# на homelab:
|
||||||
|
cd /data/docker/compose/cactoz
|
||||||
|
docker compose -f docker-compose.prod.yml ps # статус + healthcheck'и
|
||||||
|
docker logs -f cactoz-gameserver-... 2>&1 | less # события лобби/игр
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --build # ручной перезапуск
|
||||||
|
```
|
||||||
|
|
||||||
|
- Логи ротируются (`max-size 10m × 3`), лимиты ресурсов: nginx 128M/0.5cpu,
|
||||||
|
gameserver 256M/1cpu.
|
||||||
|
- Gameserver не умеет graceful shutdown — при рестарте активные матчи обрываются
|
||||||
|
(комнаты сами поднимаются заново).
|
||||||
|
- Статика без хешей в именах: js/css отдаются с `no-cache`, `/assets/` — час.
|
||||||
|
|
||||||
|
## Доступ и безопасность
|
||||||
|
|
||||||
|
- SSH на homelab: ключи, вход только с доверенных сетей; sudo — по необходимости.
|
||||||
|
- Gitea (`git.cactoz.su`) — источник истины для кода; секретов в репо нет.
|
||||||
|
- За пределами compose на хосте живут Caddy-стек, Gitea и её runner — их
|
||||||
|
конфиги не входят в этот репозиторий и бэкапятся отдельно.
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# ТЗ: cactoz.su
|
# ТЗ: cactoz.su
|
||||||
|
|
||||||
|
> ⚠️ Это ТЗ исходного замысла (MVP). Реальность разошлась: появились мини-игры
|
||||||
|
> `./blocks` и `./invaders`, самостоятельная страница `./tanks` со своим JS,
|
||||||
|
> шрифты self-hosted, healthcheck'и в compose, структура `gameserver/src` компактнее
|
||||||
|
> описанной. Актуальная архитектура деплоя и runbook — в [README.md](README.md).
|
||||||
|
|
||||||
## 1. Общее
|
## 1. Общее
|
||||||
|
|
||||||
Личный сайт-визитка на домене `cactoz.su`. Три страницы, единая Matrix-стилистика.
|
Личный сайт-визитка на домене `cactoz.su`. Три страницы, единая Matrix-стилистика.
|
||||||
|
|||||||
@@ -10,12 +10,54 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- gameserver
|
- gameserver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: "0.5"
|
||||||
|
|
||||||
gameserver:
|
gameserver:
|
||||||
build: ./gameserver
|
build: ./gameserver
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/127.0.0.1/9001'"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
cpus: "1.0"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal:
|
internal:
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>./blocks — cacto</title>
|
<title>./blocks — cacto</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/css/theme.css" />
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
<link rel="stylesheet" href="/css/arcade.css" />
|
<link rel="stylesheet" href="/css/arcade.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/* Self-hosted JetBrains Mono (сабсеты cyrillic/cyrillic-ext/latin/latin-ext).
|
||||||
|
|
||||||
|
Источник: Google Fonts, OFL-лицензия. Заменяет fonts.googleapis.com —
|
||||||
|
|
||||||
|
убирает внешнюю зависимость и лишние хосты из CSP. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-400-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-400-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-400-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-400-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-500-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-500-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-500-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-500-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-600-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-600-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-600-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-600-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-700-cyrillic-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-700-cyrillic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-700-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/fonts/jbmono-700-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
+1
-3
@@ -5,9 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>./tanks — cacto</title>
|
<title>./tanks — cacto</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/css/theme.css" />
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
<link rel="stylesheet" href="/css/game.css" />
|
<link rel="stylesheet" href="/css/game.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+1
-3
@@ -13,9 +13,7 @@
|
|||||||
<meta property="og:image" content="https://cactoz.su/assets/og.png" />
|
<meta property="og:image" content="https://cactoz.su/assets/og.png" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/css/theme.css" />
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
<link rel="stylesheet" href="/css/resume.css" />
|
<link rel="stylesheet" href="/css/resume.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>./invaders — cacto</title>
|
<title>./invaders — cacto</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/css/theme.css" />
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
<link rel="stylesheet" href="/css/arcade.css" />
|
<link rel="stylesheet" href="/css/arcade.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ export class Network {
|
|||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
this._emit(msg.type, msg.payload);
|
this._emit(msg.type, msg.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NAT-таблицы на промежуточных роутерах гаснут раньше, чем серверный
|
||||||
|
// idleTimeout (300с) — application-ping раз в 25с держит соединение живым
|
||||||
|
// и на случай обрыва держим интервал заметно меньше таймаута.
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.socket.readyState === WebSocket.OPEN) {
|
||||||
|
this.send(ClientMessage.PING);
|
||||||
|
}
|
||||||
|
}, 25000);
|
||||||
}
|
}
|
||||||
|
|
||||||
on(type, handler) {
|
on(type, handler) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Типы сообщений из TZ.md, раздел 5.
|
// Типы сообщений из TZ.md, раздел 5.
|
||||||
export const ClientMessage = {
|
export const ClientMessage = {
|
||||||
HELLO: 'hello',
|
HELLO: 'hello',
|
||||||
|
PING: 'ping',
|
||||||
LOBBY_LIST_ROOMS: 'lobby.list_rooms',
|
LOBBY_LIST_ROOMS: 'lobby.list_rooms',
|
||||||
LOBBY_CREATE_ROOM: 'lobby.create_room',
|
LOBBY_CREATE_ROOM: 'lobby.create_room',
|
||||||
LOBBY_JOIN_ROOM: 'lobby.join_room',
|
LOBBY_JOIN_ROOM: 'lobby.join_room',
|
||||||
@@ -11,6 +12,7 @@ export const ClientMessage = {
|
|||||||
|
|
||||||
export const ServerMessage = {
|
export const ServerMessage = {
|
||||||
HELLO_ACK: 'hello.ack',
|
HELLO_ACK: 'hello.ack',
|
||||||
|
PONG: 'pong',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
LOBBY_ROOMS: 'lobby.rooms',
|
LOBBY_ROOMS: 'lobby.rooms',
|
||||||
LOBBY_ROOM_JOINED: 'lobby.room_joined',
|
LOBBY_ROOM_JOINED: 'lobby.room_joined',
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
<meta name="description" content="Проекты Гаммеля Дмитрия: home automation platform и этот сайт." />
|
<meta name="description" content="Проекты Гаммеля Дмитрия: home automation platform и этот сайт." />
|
||||||
<link rel="canonical" href="https://cactoz.su/projects" />
|
<link rel="canonical" href="https://cactoz.su/projects" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/cactus-logo.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="stylesheet" href="/css/fonts.css" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/css/theme.css" />
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
<link rel="stylesheet" href="/css/projects.css" />
|
<link rel="stylesheet" href="/css/projects.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
#include "Lobby.hpp"
|
#include "Lobby.hpp"
|
||||||
#include "Game.hpp"
|
#include "Game.hpp"
|
||||||
|
#include "Log.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
@@ -74,6 +75,10 @@ void Lobby::onOpen(WS *ws) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Lobby::onClose(WS *ws) {
|
void Lobby::onClose(WS *ws) {
|
||||||
|
if (!ws->getUserData()->room_id.empty()) {
|
||||||
|
logLine("connection closed in-room: " + ws->getUserData()->room_id + " " +
|
||||||
|
ws->getUserData()->nickname);
|
||||||
|
}
|
||||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||||
removePlayerFromRoom(ws);
|
removePlayerFromRoom(ws);
|
||||||
sockets_.erase(ws);
|
sockets_.erase(ws);
|
||||||
@@ -107,6 +112,8 @@ void Lobby::onMessage(WS *ws, std::string_view message) {
|
|||||||
handleGameReady(ws);
|
handleGameReady(ws);
|
||||||
} else if (type == "game.input") {
|
} else if (type == "game.input") {
|
||||||
handleGameInput(ws, payload);
|
handleGameInput(ws, payload);
|
||||||
|
} else if (type == "ping") {
|
||||||
|
send(ws, "pong", {});
|
||||||
} else {
|
} else {
|
||||||
sendError(ws, "unknown_type", "unknown message type: " + type);
|
sendError(ws, "unknown_type", "unknown message type: " + type);
|
||||||
}
|
}
|
||||||
@@ -186,6 +193,8 @@ void Lobby::handleCreateRoom(WS *ws, const json &payload) {
|
|||||||
std::string room_id = room.id;
|
std::string room_id = room.id;
|
||||||
rooms_[room_id] = std::move(room);
|
rooms_[room_id] = std::move(room);
|
||||||
|
|
||||||
|
logLine("room created: " + room_id + " mode=" + mode + " by=" + data->nickname);
|
||||||
|
|
||||||
send(ws, "lobby.room_joined", {
|
send(ws, "lobby.room_joined", {
|
||||||
{"room_id", room_id},
|
{"room_id", room_id},
|
||||||
{"name", room_id},
|
{"name", room_id},
|
||||||
@@ -227,6 +236,9 @@ void Lobby::handleJoinRoom(WS *ws, const json &payload) {
|
|||||||
room.players.push_back({data->player_id, data->nickname, ws});
|
room.players.push_back({data->player_id, data->nickname, ws});
|
||||||
data->room_id = room_id;
|
data->room_id = room_id;
|
||||||
|
|
||||||
|
logLine("player joined: " + room_id + " " + data->nickname + " (" +
|
||||||
|
std::to_string(room.players.size()) + "/" + std::to_string(kMaxPlayersPerRoom) + ")");
|
||||||
|
|
||||||
send(ws, "lobby.room_joined", {
|
send(ws, "lobby.room_joined", {
|
||||||
{"room_id", room_id},
|
{"room_id", room_id},
|
||||||
{"name", room_id},
|
{"name", room_id},
|
||||||
@@ -243,6 +255,7 @@ void Lobby::handleLeaveRoom(WS *ws) {
|
|||||||
sendError(ws, "not_in_room", "you are not in a room");
|
sendError(ws, "not_in_room", "you are not in a room");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
logLine("player left: " + ws->getUserData()->room_id + " " + ws->getUserData()->nickname);
|
||||||
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
endActiveGameIfAny(ws->getUserData()->room_id, ws);
|
||||||
removePlayerFromRoom(ws);
|
removePlayerFromRoom(ws);
|
||||||
broadcastRoomsToLobby();
|
broadcastRoomsToLobby();
|
||||||
@@ -301,7 +314,11 @@ void Lobby::maybeStartGame(const std::string &room_id) {
|
|||||||
|
|
||||||
room.state = "playing";
|
room.state = "playing";
|
||||||
|
|
||||||
|
logLine("game started: " + room_id + " mode=" + room.mode + " players=" +
|
||||||
|
std::to_string(room.players.size()));
|
||||||
|
|
||||||
auto game = std::make_unique<Game>(room, [this, room_id]() {
|
auto game = std::make_unique<Game>(room, [this, room_id]() {
|
||||||
|
logLine("game finished: " + room_id);
|
||||||
auto rit = rooms_.find(room_id);
|
auto rit = rooms_.find(room_id);
|
||||||
if (rit != rooms_.end()) {
|
if (rit != rooms_.end()) {
|
||||||
rit->second.state = "waiting";
|
rit->second.state = "waiting";
|
||||||
@@ -358,6 +375,7 @@ void Lobby::removePlayerFromRoom(WS *ws) {
|
|||||||
room.players.end());
|
room.players.end());
|
||||||
|
|
||||||
if (room.players.empty()) {
|
if (room.players.empty()) {
|
||||||
|
logLine("room removed: " + it->first + " (empty)");
|
||||||
rooms_.erase(it);
|
rooms_.erase(it);
|
||||||
} else {
|
} else {
|
||||||
broadcastRoomUpdated(room);
|
broadcastRoomUpdated(room);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// События лобби/игр пишутся в stdout — их забирает `docker logs`, это единственный
|
||||||
|
// способ понять в проде, что происходило с комнатами, не заходя в контейнер.
|
||||||
|
inline void logLine(const std::string &message) {
|
||||||
|
std::time_t now = std::time(nullptr);
|
||||||
|
std::tm tm{};
|
||||||
|
localtime_r(&now, &tm);
|
||||||
|
char ts[20];
|
||||||
|
std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm);
|
||||||
|
// endl, а не "\n": stdout в контейнере идёт в пайп и полностью
|
||||||
|
// буферизуется — без сброса docker logs видит строки только на exit
|
||||||
|
std::cout << "[" << ts << "] " << message << std::endl;
|
||||||
|
}
|
||||||
+12
-1
@@ -4,12 +4,23 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "Lobby.hpp"
|
#include "Lobby.hpp"
|
||||||
|
#include "Log.hpp"
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
Lobby lobby;
|
Lobby lobby;
|
||||||
|
|
||||||
uWS::App()
|
uWS::App()
|
||||||
.ws<PerSocketData>("/ws", {
|
.ws<PerSocketData>("/ws", {
|
||||||
|
// сообщения у нас мелкие (input/lobby) — 16 МиБ дефолта не нужен,
|
||||||
|
// лимит режет попытки завалить сервер гигантскими фреймами
|
||||||
|
.compression = uWS::DISABLED,
|
||||||
|
.maxPayloadLength = 8 * 1024,
|
||||||
|
// сервер сам шлёт WS-ping в пределах idleTimeout; клиент дополнительно
|
||||||
|
// шлёт application-ping раз в 25с — соединение переживает NAT-таймауты
|
||||||
|
.idleTimeout = 300,
|
||||||
|
.maxBackpressure = 16 * 1024,
|
||||||
|
.closeOnBackpressureLimit = true,
|
||||||
|
.sendPingsAutomatically = true,
|
||||||
.open = [&lobby](auto *ws) {
|
.open = [&lobby](auto *ws) {
|
||||||
lobby.onOpen(ws);
|
lobby.onOpen(ws);
|
||||||
},
|
},
|
||||||
@@ -22,7 +33,7 @@ int main() {
|
|||||||
})
|
})
|
||||||
.listen(9001, [](auto *token) {
|
.listen(9001, [](auto *token) {
|
||||||
if (token) {
|
if (token) {
|
||||||
std::cout << "gameserver listening on :9001\n";
|
logLine("gameserver listening on :9001");
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "failed to listen on :9001\n";
|
std::cerr << "failed to listen on :9001\n";
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -13,10 +13,16 @@ server {
|
|||||||
# не светим точную версию nginx в заголовке Server и в error-страницах
|
# не светим точную версию nginx в заголовке Server и в error-страницах
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-Frame-Options DENY always;
|
add_header X-Frame-Options DENY always;
|
||||||
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; base-uri 'none'; frame-ancestors 'none'" always;
|
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri.html $uri/ =404;
|
try_files $uri $uri.html $uri/ =404;
|
||||||
@@ -28,6 +34,13 @@ server {
|
|||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# статичные ассеты меняются редко (лого, og-картинка, CV) — час кэша
|
||||||
|
# компромисс между скоростью и актуальностью без хешей в именах
|
||||||
|
location /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
limit_req zone=ws_connect burst=10 nodelay;
|
limit_req zone=ws_connect burst=10 nodelay;
|
||||||
limit_conn ws_conn 20;
|
limit_conn ws_conn 20;
|
||||||
|
|||||||
Reference in New Issue
Block a user