feat: implement safe leave, profile reset, and full game reset semantics

This commit is contained in:
2026-08-30 23:18:07 +03:00
parent c0c3c73c7a
commit a68023e684
16 changed files with 281 additions and 9 deletions
+40 -1
View File
@@ -118,6 +118,15 @@ static const char *phase_text(phase_t phase) {
return phase <= PHASE_REMATCH_WAIT ? values[phase] : "lobby";
}
static const char *recovery_reason_text(recovery_reason_t reason) {
switch (reason) {
case RECOVERY_REASON_SESSION_LEFT: return "session_left";
case RECOVERY_REASON_PROFILE_RESET: return "profile_reset";
case RECOVERY_REASON_GAME_RESET: return "game_reset";
default: return "session_invalidated";
}
}
static void response_write(http_api_response_t *response, uint16_t status, const char *format, ...) {
va_list arguments;
va_start(arguments, format);
@@ -153,6 +162,16 @@ static void response_lifecycle_error(http_api_response_t *response, lifecycle_re
}
}
static void response_recovery(http_api_response_t *response, const game_lifecycle_t *lifecycle) {
response_write(response, 200U, "{\"ok\":true,\"resetReason\":\"%s\",\"generation\":%" PRIu32 "}",
recovery_reason_text(lifecycle->recovery_reason), lifecycle->recovery_generation);
}
static void response_invalidated(http_api_response_t *response, const game_lifecycle_t *lifecycle) {
response_write(response, 401U, "{\"ok\":false,\"code\":\"SESSION_INVALIDATED\",\"resetReason\":\"%s\",\"generation\":%" PRIu32 "}",
recovery_reason_text(lifecycle->recovery_reason), lifecycle->recovery_generation);
}
static bool expect_post_body(const http_api_request_t *request, size_t maximum, http_api_response_t *response,
uint32_t version) {
if (request->method != HTTP_API_POST || !request->content_type_json) {
@@ -289,10 +308,14 @@ bool http_api_handle(http_api_t *api, const http_api_request_t *request, http_ap
uint8_t token[kSessionTokenBytes];
uint8_t session_index = 0;
if (request->session_token != NULL && request->session_token[0] != '\0') {
if (!parse_token(request->session_token, token) || !application_session_for_token(api->application, token, &session_index)) {
if (!parse_token(request->session_token, token)) {
response_error(response, 401U, "UNAUTHORIZED", "Сессия не найдена", version);
return true;
}
if (!application_session_for_token(api->application, token, &session_index)) {
response_invalidated(response, lifecycle);
return true;
}
viewer = lifecycle->sessions.entries[session_index].role;
}
if (!state_presenter_write_lifecycle(lifecycle, viewer, response->body, sizeof(response->body), &response->body_length)) {
@@ -374,6 +397,18 @@ bool http_api_handle(http_api_t *api, const http_api_request_t *request, http_ap
return true;
}
if (!application_session_for_token(api->application, token, &command.session_index)) {
if (request->route == HTTP_API_ROUTE_LEAVE || request->route == HTTP_API_ROUTE_PROFILE_RESET) {
response_recovery(response, lifecycle);
return true;
}
if (request->route == HTTP_API_ROUTE_RESET && lifecycle->recovery_reason == RECOVERY_REASON_GAME_RESET) {
response_recovery(response, lifecycle);
return true;
}
if (request->route == HTTP_API_ROUTE_RESET) {
response_error(response, 403U, "FORBIDDEN_ROLE", "Роль не может выполнить действие", version);
return true;
}
response_error(response, 401U, "UNAUTHORIZED", "Сессия не найдена", version);
return true;
}
@@ -383,11 +418,15 @@ bool http_api_handle(http_api_t *api, const http_api_request_t *request, http_ap
case HTTP_API_ROUTE_SHOT: command.type = COMMAND_SHOT; break;
case HTTP_API_ROUTE_REMATCH: command.type = COMMAND_REMATCH; break;
case HTTP_API_ROUTE_ABORT: command.type = COMMAND_ABORT; break;
case HTTP_API_ROUTE_LEAVE: command.type = COMMAND_LEAVE; break;
case HTTP_API_ROUTE_PROFILE_RESET: command.type = COMMAND_PROFILE_RESET; break;
case HTTP_API_ROUTE_RESET: command.type = COMMAND_RESET; break;
default: response_error(response, 404U, "MALFORMED_JSON", "Маршрут не найден", version); return true;
}
command.version = version;
const lifecycle_result_t result = application_submit(api->application, &command);
if (result != LIFECYCLE_RESULT_OK) response_lifecycle_error(response, result, lifecycle->game.state.version);
else if (request->route == HTTP_API_ROUTE_LEAVE || request->route == HTTP_API_ROUTE_PROFILE_RESET || request->route == HTTP_API_ROUTE_RESET) response_recovery(response, lifecycle);
else response_write(response, 200U, "{\"ok\":true,\"version\":%" PRIu32 ",\"gameId\":%" PRIu32 "}",
lifecycle->game.state.version, lifecycle->game.state.game_id);
return true;