Каркас проекта: ТЗ, nginx+gameserver(uWebSockets) в Docker, /ws echo
Frontend — чистый HTML/CSS/JS без сборки (резюме, проекты, игра-заглушки). Gameserver — C++ на uWebSockets (CMake FetchContent), протокол hello/lobby.list_rooms/error проверен end-to-end.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
gameserver/build/
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# ТЗ: cactoz.su
|
||||||
|
|
||||||
|
## 1. Общее
|
||||||
|
|
||||||
|
Личный сайт-визитка на домене `cactoz.su`. Три страницы, единая Matrix-стилистика.
|
||||||
|
|
||||||
|
**Тема:** чёрный фон, зелёный и серебро, моноширинный шрифт (JetBrains Mono / Share Tech Mono), строго и технично — без лишних декоративных элементов.
|
||||||
|
|
||||||
|
**Ник `cacto` (кактус) — сквозной мотив:**
|
||||||
|
- Пиксельный кактус (16×16 или 32×32 спрайт, зелёный/серебро на чёрном) как лого в шапке и favicon.
|
||||||
|
- ASCII-арт кактуса в hero-блоке главной страницы.
|
||||||
|
- Терминальные префиксы вместо обычных заголовков/навигации: `cacto@matrix:~$ whoami`, `cacto@matrix:~$ ls projects/`, `cacto@matrix:~$ ./battlecity`.
|
||||||
|
- Названия комнат в лобби генерируются в формате `cactus-room-01`, `cactus-room-02`, ...
|
||||||
|
- Характер, а не просто ник: живучесть, минимум ресурсов, "колючий" стиль текста в отдельных местах (например, error-сообщения).
|
||||||
|
|
||||||
|
## 2. Страницы
|
||||||
|
|
||||||
|
| # | Файл | Назначение |
|
||||||
|
|---|------|------------|
|
||||||
|
| 1 | `index.html` | Резюме — PHP/Go backend, опыт КИПиА/АСУТП |
|
||||||
|
| 2 | `projects.html` | Карточки проектов со ссылками на репозитории |
|
||||||
|
| 3 | `game.html` | BattleCity: лобби + игра |
|
||||||
|
|
||||||
|
## 3. Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
cactoz.su/
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── nginx/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── nginx.conf
|
||||||
|
├── frontend/ # чистый HTML/CSS/JS, без сборки
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── projects.html
|
||||||
|
│ ├── game.html
|
||||||
|
│ ├── favicon.ico
|
||||||
|
│ ├── css/
|
||||||
|
│ │ ├── theme.css # переменные темы, общий сброс
|
||||||
|
│ │ ├── resume.css
|
||||||
|
│ │ ├── projects.css
|
||||||
|
│ │ └── game.css
|
||||||
|
│ ├── js/
|
||||||
|
│ │ ├── matrix-rain.js # canvas-эффект цифрового дождя
|
||||||
|
│ │ ├── projects.js # рендер карточек проектов из data
|
||||||
|
│ │ └── game/
|
||||||
|
│ │ ├── network.js # обёртка над WebSocket, очередь сообщений
|
||||||
|
│ │ ├── lobby.js # UI лобби: список комнат, создание/вход
|
||||||
|
│ │ ├── protocol.js # константы типов сообщений, (де)сериализация
|
||||||
|
│ │ ├── entities.js # клиентские представления Tank/Bullet/Wall для Quintus
|
||||||
|
│ │ └── main.js # инициализация Quintus, игровой цикл рендера
|
||||||
|
│ └── vendor/
|
||||||
|
│ └── quintus/ # Quintus.js, подключается напрямую <script>
|
||||||
|
├── gameserver/ # C++ / uWebSockets
|
||||||
|
│ ├── CMakeLists.txt
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── third_party/ # uWebSockets, uSockets, nlohmann/json (submodules)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.cpp # запуск uWS::App, роутинг /ws
|
||||||
|
│ ├── Protocol.hpp # структуры сообщений + (де)сериализация JSON
|
||||||
|
│ ├── Player.hpp # id, nickname, ws-handle, room_id
|
||||||
|
│ ├── Lobby.hpp / .cpp # реестр комнат, create/join/leave/list
|
||||||
|
│ ├── Room.hpp / .cpp # жизненный цикл комнаты (waiting/playing/finished)
|
||||||
|
│ ├── Game.hpp / .cpp # игровой цикл тика, применение input, коллизии
|
||||||
|
│ ├── Map.hpp # тайловые карты (кирпич/сталь/вода/куст), парсинг
|
||||||
|
│ ├── Tank.hpp
|
||||||
|
│ ├── Bullet.hpp
|
||||||
|
│ └── Ai.hpp / .cpp # AI противника для coop-режима
|
||||||
|
└── TZ.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Docker
|
||||||
|
|
||||||
|
Два контейнера, как договорились:
|
||||||
|
|
||||||
|
- **nginx** — раздаёт статику из `frontend/`, проксирует `/ws` на `gameserver:9001` (с `Upgrade`/`Connection` заголовками для WebSocket). При необходимости терминирует TLS (certbot/сертификаты монтируются volume'ом).
|
||||||
|
- **gameserver** — multi-stage `Dockerfile`: build-стадия (g++/cmake, сборка uWebSockets из submodule), финальный минимальный runtime-образ с одним бинарником.
|
||||||
|
|
||||||
|
`docker-compose.yml` связывает оба сервиса общей сетью; наружу открыт только nginx (80/443), gameserver доступен только изнутри docker-сети.
|
||||||
|
|
||||||
|
## 5. WebSocket-протокол
|
||||||
|
|
||||||
|
Один WS-канал на клиента на всё: hello → лобби → комната → игра. Формат сообщений — JSON-конверт:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "namespace.action", "payload": { ... } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client → Server
|
||||||
|
|
||||||
|
| type | payload | Когда |
|
||||||
|
|---|---|---|
|
||||||
|
| `hello` | `{ nickname: string }` | Сразу после открытия соединения |
|
||||||
|
| `lobby.list_rooms` | `{}` | При входе в лобби / по запросу обновления |
|
||||||
|
| `lobby.create_room` | `{ mode: "coop" \| "pvp" }` | Создание комнаты (имя генерируется сервером: `cactus-room-NN`) |
|
||||||
|
| `lobby.join_room` | `{ room_id: string }` | Присоединение к существующей комнате |
|
||||||
|
| `lobby.leave_room` | `{}` | Выход из комнаты обратно в лобби |
|
||||||
|
| `game.ready` | `{}` | Игрок готов к старту матча |
|
||||||
|
| `game.input` | `{ seq: number, up: bool, down: bool, left: bool, right: bool, fire: bool }` | При каждом изменении состояния клавиш |
|
||||||
|
|
||||||
|
### Server → Client
|
||||||
|
|
||||||
|
| type | payload | Когда |
|
||||||
|
|---|---|---|
|
||||||
|
| `hello.ack` | `{ player_id: string }` | Ответ на `hello` |
|
||||||
|
| `error` | `{ code: string, message: string }` | Любая ошибка протокола/логики |
|
||||||
|
| `lobby.rooms` | `{ rooms: [{ id, name, mode, players: number, max_players: 2, state }] }` | Ответ на `lobby.list_rooms`, и при любом изменении списка (broadcast всем в лобби) |
|
||||||
|
| `lobby.room_joined` | `{ room_id, name, mode, players: [{ id, nickname, slot }] }` | Игроку, который вошёл в комнату |
|
||||||
|
| `lobby.room_updated` | `{ players: [...] }` | Broadcast остальным в комнате при join/leave |
|
||||||
|
| `game.start` | `{ map: Tile[][], mode, players: [...], tick_rate }` | Когда оба игрока готовы (или для coop — один готов + таймаут) |
|
||||||
|
| `game.state` | `{ tick, tanks: [...], bullets: [...], base?: {...}, enemies?: [...] }` | Каждый серверный тик (полный снапшот, без дельт — упрощение для MVP) |
|
||||||
|
| `game.over` | `{ result: "win" \| "lose" \| "draw", reason }` | Конец матча |
|
||||||
|
|
||||||
|
Снапшоты, а не дельты — сознательное упрощение ради простоты клиента и отладки; при необходимости оптимизации это единственное место, которое переделывается (дельты/бинарный формат), протокол на это не завязан жёстко.
|
||||||
|
|
||||||
|
## 6. Игровая логика на сервере
|
||||||
|
|
||||||
|
- **Тик** 20 Гц (50 мс), authoritative-симуляция: клиент не считает физику, только рендерит `game.state` и шлёт `game.input`.
|
||||||
|
- **Сущности:** `Tank {id, x, y, direction, lives, fire_cooldown}`, `Bullet {id, owner_id, x, y, direction, speed}`, тайлы карты (кирпич — разрушается, сталь — нет, вода — препятствие для танков/не для пуль, куст — визуально скрывает).
|
||||||
|
- **Движение** — по классике, сеточное/пошаговое (проще для коллизий и портфолио, чем free-form пиксельная физика).
|
||||||
|
- **Коллизии:** пуля-стена (разрушает кирпич), пуля-танк (урон/уничтожение), танк-стена/танк-танк (блокировка).
|
||||||
|
- **Режим coop:** сервер спавнит AI-противников волнами (спавн-точки сверху карты, простая логика: движение к базе + случайный огонь), оба игрока защищают базу (орла). Поражение — база или оба игрока уничтожены; победа — все враги волны уничтожены.
|
||||||
|
- **Режим pvp:** без AI, симметричная карта, у каждого игрока пул жизней (например, 3), поражение — жизни закончились.
|
||||||
|
- **Жизненный цикл комнаты:** `waiting` (сбор игроков/ready) → `playing` (активен игровой цикл) → `finished` (отправлен `game.over`, комната закрывается через короткий таймаут).
|
||||||
|
- **Дисконнект:** при обрыве соединения во время матча — пауза с таймаутом (например, 30 сек), затем техническое поражение отключившегося.
|
||||||
|
|
||||||
|
## 7. Лобби
|
||||||
|
|
||||||
|
Отдельного REST нет — всё через тот же WS-канал (см. таблицы сообщений в разделе 5). Список комнат обновляется broadcast'ом при любом изменении (создание/удаление/смена состава), не только по запросу клиента.
|
||||||
|
|
||||||
|
## 8. MVP-этапы (предложение)
|
||||||
|
|
||||||
|
1. Каркас: nginx + gameserver в Docker, статика отдаётся, `/ws` echo-проверка.
|
||||||
|
2. Вёрстка страниц 1–2 (резюме, проекты) в теме Matrix/cacto.
|
||||||
|
3. Лобби: список/создание/присоединение комнат (без игровой логики).
|
||||||
|
4. Игровая логика PvP (проще двух режимов) → затем coop + AI.
|
||||||
|
5. Полировка: спрайты танков, звук (опционально), доп. карты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Открытые вопросы, требующие решения до реализации: количество и вид карт для MVP (сколько уровней/волн для coop), спрайты (рисовать самим/найти свободные ассеты в стиле Matrix), таймаут ready-фазы в лобби.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: nginx/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- gameserver
|
||||||
|
|
||||||
|
gameserver:
|
||||||
|
build: ./gameserver
|
||||||
|
expose:
|
||||||
|
- "9001"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* TODO: лобби и игровое поле — этапы 3-6 */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* TODO: карточки проектов — этап 2 */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* TODO: вёрстка резюме — этап 2 */
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
:root {
|
||||||
|
--color-bg: #0a0e0c;
|
||||||
|
--color-fg: #c7c7c7;
|
||||||
|
--color-accent: #00ff41;
|
||||||
|
--color-accent-dim: #0a8f2c;
|
||||||
|
--color-silver: #9fa3a0;
|
||||||
|
--font-mono: 'JetBrains Mono', 'Share Tech Mono', ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-fg);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt::before {
|
||||||
|
content: 'cacto@matrix:~$ ';
|
||||||
|
color: var(--color-accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
#matrix-rain {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>./battlecity — cacto</title>
|
||||||
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
|
<link rel="stylesheet" href="/css/game.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="game-root">
|
||||||
|
<!-- TODO: лобби (этап 3) и игровое поле на Quintus.js (этапы 4-5) -->
|
||||||
|
<h1 class="prompt">./battlecity</h1>
|
||||||
|
<div id="lobby"></div>
|
||||||
|
<div id="battlecity-canvas-container"></div>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="/js/game/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>cacto — Гаммель Дмитрий</title>
|
||||||
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
|
<link rel="stylesheet" href="/css/resume.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="matrix-rain"></canvas>
|
||||||
|
<main id="resume">
|
||||||
|
<!-- TODO: вёрстка резюме, ASCII-арт кактуса — этап 2 -->
|
||||||
|
<h1 class="prompt">whoami</h1>
|
||||||
|
<p>Гаммель Дмитрий Викторович — PHP/Go backend, опыт КИПиА/АСУТП</p>
|
||||||
|
<nav>
|
||||||
|
<a href="/projects.html">ls projects/</a>
|
||||||
|
<a href="/game.html">./battlecity</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="/js/matrix-rain.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// TODO: клиентские представления Tank/Bullet/Wall для Quintus — этапы 4-5
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// TODO: полноценный UI лобби (список/создание/присоединение комнат) — этап 3.
|
||||||
|
// Пока — проверка связки клиент <-> gameserver (hello -> lobby.list_rooms).
|
||||||
|
import { Network, ServerMessage } from './network.js';
|
||||||
|
|
||||||
|
export function connectAndProbe() {
|
||||||
|
const net = new Network();
|
||||||
|
|
||||||
|
net.on('_open', () => net.hello('cacto'));
|
||||||
|
net.on(ServerMessage.HELLO_ACK, () => {
|
||||||
|
console.log('[gameserver] hello.ack received');
|
||||||
|
net.listRooms();
|
||||||
|
});
|
||||||
|
net.on(ServerMessage.LOBBY_ROOMS, (payload) => {
|
||||||
|
console.log('[gameserver] rooms:', payload.rooms);
|
||||||
|
});
|
||||||
|
net.on(ServerMessage.ERROR, (payload) => {
|
||||||
|
console.error('[gameserver] error:', payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
return net;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// TODO: инициализация Quintus и игрового цикла рендера — этапы 4-5
|
||||||
|
import { connectAndProbe } from './lobby.js';
|
||||||
|
|
||||||
|
connectAndProbe();
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ClientMessage, ServerMessage } from './protocol.js';
|
||||||
|
|
||||||
|
export class Network {
|
||||||
|
constructor() {
|
||||||
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
this.socket = new WebSocket(`${proto}://${location.host}/ws`);
|
||||||
|
this.handlers = new Map();
|
||||||
|
|
||||||
|
this.socket.addEventListener('open', () => this._emit('_open'));
|
||||||
|
this.socket.addEventListener('close', () => this._emit('_close'));
|
||||||
|
this.socket.addEventListener('message', (event) => {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
this._emit(msg.type, msg.payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
on(type, handler) {
|
||||||
|
this.handlers.set(type, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit(type, payload) {
|
||||||
|
this.handlers.get(type)?.(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(type, payload = {}) {
|
||||||
|
this.socket.send(JSON.stringify({ type, payload }));
|
||||||
|
}
|
||||||
|
|
||||||
|
hello(nickname) {
|
||||||
|
this.send(ClientMessage.HELLO, { nickname });
|
||||||
|
}
|
||||||
|
|
||||||
|
listRooms() {
|
||||||
|
this.send(ClientMessage.LOBBY_LIST_ROOMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ServerMessage };
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Типы сообщений из TZ.md, раздел 5.
|
||||||
|
export const ClientMessage = {
|
||||||
|
HELLO: 'hello',
|
||||||
|
LOBBY_LIST_ROOMS: 'lobby.list_rooms',
|
||||||
|
LOBBY_CREATE_ROOM: 'lobby.create_room',
|
||||||
|
LOBBY_JOIN_ROOM: 'lobby.join_room',
|
||||||
|
LOBBY_LEAVE_ROOM: 'lobby.leave_room',
|
||||||
|
GAME_READY: 'game.ready',
|
||||||
|
GAME_INPUT: 'game.input',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerMessage = {
|
||||||
|
HELLO_ACK: 'hello.ack',
|
||||||
|
ERROR: 'error',
|
||||||
|
LOBBY_ROOMS: 'lobby.rooms',
|
||||||
|
LOBBY_ROOM_JOINED: 'lobby.room_joined',
|
||||||
|
LOBBY_ROOM_UPDATED: 'lobby.room_updated',
|
||||||
|
GAME_START: 'game.start',
|
||||||
|
GAME_STATE: 'game.state',
|
||||||
|
GAME_OVER: 'game.over',
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// TODO: полноценный canvas-эффект цифрового дождя — этап 2
|
||||||
|
const canvas = document.getElementById('matrix-rain');
|
||||||
|
if (canvas) {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const resize = () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
resize();
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// TODO: рендер карточек проектов из данных — этап 2
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ls projects/ — cacto</title>
|
||||||
|
<link rel="stylesheet" href="/css/theme.css" />
|
||||||
|
<link rel="stylesheet" href="/css/projects.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="projects">
|
||||||
|
<!-- TODO: карточки проектов — этап 2 -->
|
||||||
|
<h1 class="prompt">ls projects/</h1>
|
||||||
|
<ul id="project-list"></ul>
|
||||||
|
<nav>
|
||||||
|
<a href="/">cd ~</a>
|
||||||
|
<a href="/game.html">./battlecity</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="/js/projects.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(cactoz_gameserver CXX C)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
uwebsockets
|
||||||
|
GIT_REPOSITORY https://github.com/uNetworking/uWebSockets.git
|
||||||
|
GIT_TAG v20.79.0
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(uwebsockets)
|
||||||
|
|
||||||
|
# uWebSockets pulls uSockets in via a git submodule, which plain FetchContent
|
||||||
|
# does not check out. Fetch the exact commit uWebSockets v20.79.0 pins instead.
|
||||||
|
FetchContent_Declare(
|
||||||
|
usockets_src
|
||||||
|
GIT_REPOSITORY https://github.com/uNetworking/uSockets.git
|
||||||
|
GIT_TAG 86097c490263ab662d62e8e7b541390bdec7d149
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(usockets_src)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
json
|
||||||
|
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||||
|
GIT_TAG v3.12.0
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(json)
|
||||||
|
|
||||||
|
file(GLOB USOCKETS_SOURCES
|
||||||
|
${usockets_src_SOURCE_DIR}/src/*.c
|
||||||
|
${usockets_src_SOURCE_DIR}/src/eventing/*.c
|
||||||
|
)
|
||||||
|
add_library(usockets STATIC ${USOCKETS_SOURCES})
|
||||||
|
target_include_directories(usockets PUBLIC ${usockets_src_SOURCE_DIR}/src)
|
||||||
|
target_compile_definitions(usockets PUBLIC LIBUS_NO_SSL LIBUS_USE_EPOLL)
|
||||||
|
|
||||||
|
add_executable(gameserver
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(gameserver PRIVATE
|
||||||
|
${uwebsockets_SOURCE_DIR}/src
|
||||||
|
src
|
||||||
|
)
|
||||||
|
target_link_libraries(gameserver PRIVATE usockets nlohmann_json::nlohmann_json pthread z)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM debian:bookworm AS build
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake git ca-certificates zlib1g-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY CMakeLists.txt ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j"$(nproc)"
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends zlib1g \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /app/build/gameserver /usr/local/bin/gameserver
|
||||||
|
EXPOSE 9001
|
||||||
|
CMD ["gameserver"]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Каркас (этап 1 из TZ.md): проверка связки nginx <-ws-> gameserver.
|
||||||
|
// Лобби/комнаты/игровая логика — следующие этапы.
|
||||||
|
|
||||||
|
#include <App.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
struct PerSocketData {};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
uWS::App()
|
||||||
|
.ws<PerSocketData>("/ws", {
|
||||||
|
.open = [](auto *ws) {
|
||||||
|
std::cout << "client connected\n";
|
||||||
|
},
|
||||||
|
.message = [](auto *ws, std::string_view message, uWS::OpCode) {
|
||||||
|
json response;
|
||||||
|
try {
|
||||||
|
json request = json::parse(message);
|
||||||
|
std::string type = request.value("type", "");
|
||||||
|
|
||||||
|
if (type == "hello") {
|
||||||
|
response = {{"type", "hello.ack"}, {"payload", {{"player_id", "stub"}}}};
|
||||||
|
} else if (type == "lobby.list_rooms") {
|
||||||
|
response = {{"type", "lobby.rooms"}, {"payload", {{"rooms", json::array()}}}};
|
||||||
|
} else {
|
||||||
|
response = {{"type", "error"},
|
||||||
|
{"payload", {{"code", "unknown_type"}, {"message", "unknown message type: " + type}}}};
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
response = {{"type", "error"}, {"payload", {{"code", "bad_json"}, {"message", e.what()}}}};
|
||||||
|
}
|
||||||
|
ws->send(response.dump(), uWS::OpCode::TEXT);
|
||||||
|
},
|
||||||
|
.close = [](auto *ws, int, std::string_view) {
|
||||||
|
std::cout << "client disconnected\n";
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.listen(9001, [](auto *token) {
|
||||||
|
if (token) {
|
||||||
|
std::cout << "gameserver listening on :9001\n";
|
||||||
|
} else {
|
||||||
|
std::cerr << "failed to listen on :9001\n";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY frontend/ /usr/share/nginx/html/
|
||||||
|
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name cactoz.su www.cactoz.su;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://gameserver:9001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user