Kill switch
Deploy Promiscuity Auth API / deploy (push) Successful in 47s
Deploy Promiscuity Crafting API / deploy (push) Has been cancelled
Deploy Promiscuity Inventory API / deploy (push) Has been cancelled
Deploy Promiscuity Locations API / deploy (push) Has been cancelled
Deploy Promiscuity Mail API / deploy (push) Has been cancelled
Deploy Promiscuity World API / deploy (push) Has been cancelled
Deploy Promiscuity Character API / deploy (push) Has been cancelled
k8s smoke test / test (push) Has been cancelled

This commit is contained in:
Zeeshaun
2026-04-01 20:22:05 +00:00
parent bed8e91bc0
commit c718fb343d
14 changed files with 413 additions and 15 deletions
@@ -14,6 +14,8 @@ namespace LocationsApi.Controllers;
[Route("api/[controller]")]
public class LocationsController : ControllerBase
{
private const string ResetWorldConfirmationPhrase = "RESET WORLD TO ORIGIN";
private static readonly Coord OriginCoord = new() { X = 0, Y = 0 };
private readonly LocationStore _locations;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
@@ -131,6 +133,63 @@ public class LocationsController : ControllerBase
return Ok(locations);
}
[HttpPost("reset-world")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> ResetWorld([FromBody] ResetWorldRequest req)
{
if (!req.ConfirmDeleteAllNonOriginLocations)
return BadRequest("confirmDeleteAllNonOriginLocations must be true");
if (!string.Equals(req.ConfirmationPhrase?.Trim(), ResetWorldConfirmationPhrase, StringComparison.Ordinal))
return BadRequest($"confirmationPhrase must exactly match '{ResetWorldConfirmationPhrase}'");
try
{
var limbo = await _locations.EnsureLimboLocationAsync();
if (string.IsNullOrWhiteSpace(limbo.Id))
return StatusCode(StatusCodes.Status500InternalServerError, "Limbo location was created without an id");
var movedCharacterCount = await ResetCharactersToOriginAsync();
var locationIdsToReassign = await _locations.GetLocationIdsForWorldResetAsync(limbo.Id);
var reassignedInventoryItemCount = await ReassignLocationInventoryToLimboAsync(locationIdsToReassign, limbo.Id);
var result = await _locations.ResetWorldToOriginAsync(limbo.Id);
result.MovedCharacterCount = movedCharacterCount;
result.ReassignedInventoryItemCount = reassignedInventoryItemCount;
_logger.LogWarning(
"World reset to origin by user {UserId}. DeletedLocationCount={DeletedLocationCount} MovedCharacterCount={MovedCharacterCount} ReassignedInventoryItemCount={ReassignedInventoryItemCount}",
User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "unknown",
result.DeletedLocationCount,
result.MovedCharacterCount,
result.ReassignedInventoryItemCount);
return Ok(result);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "World reset failed because biome definitions are unavailable.");
return Conflict("Cannot reset world because no biome definitions exist.");
}
catch (InternalServiceCallException ex)
{
_logger.LogError(
ex,
"World reset failed while calling {ServiceName}. StatusCode={StatusCode}",
ex.ServiceName,
ex.StatusCode);
return StatusCode(ex.StatusCode, ex.ResponseBody);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "World reset failed while reaching a dependent internal service.");
return StatusCode(StatusCodes.Status502BadGateway, new
{
type = "https://httpstatuses.com/502",
title = "Bad Gateway",
status = 502,
detail = "Failed to reach a dependent internal service during world reset.",
traceId = HttpContext.TraceIdentifier
});
}
}
[HttpDelete("{id}")]
[Authorize(Roles = "SUPER")]
public async Task<IActionResult> Delete(string id)
@@ -435,4 +494,83 @@ public class LocationsController : ControllerBase
public List<FloorInventoryItemResponse> Items { get; set; } = [];
}
private async Task<int> ResetCharactersToOriginAsync()
{
var characterBaseUrl = (_configuration["Services:CharacterApiBaseUrl"] ?? "http://localhost:50785").TrimEnd('/');
var internalApiKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
var body = JsonSerializer.Serialize(new { coord = OriginCoord });
var client = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, $"{characterBaseUrl}/api/characters/internal/reset-coords");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
request.Headers.Add("X-Internal-Api-Key", internalApiKey);
using var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new InternalServiceCallException("CharacterApi", (int)response.StatusCode, responseBody);
if (string.IsNullOrWhiteSpace(responseBody))
return 0;
using var json = JsonDocument.Parse(responseBody);
return json.RootElement.TryGetProperty("updatedCharacterCount", out var countElement) && countElement.ValueKind == JsonValueKind.Number
? countElement.GetInt32()
: 0;
}
private async Task<int> ReassignLocationInventoryToLimboAsync(IEnumerable<string> fromOwnerIds, string toOwnerId)
{
var ownerIds = fromOwnerIds
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
if (ownerIds.Count == 0)
return 0;
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
var internalApiKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
var body = JsonSerializer.Serialize(new
{
fromOwnerIds = ownerIds,
toOwnerId
});
var client = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, $"{inventoryBaseUrl}/api/inventory/internal/location-owner/reassign");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
request.Headers.Add("X-Internal-Api-Key", internalApiKey);
using var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new InternalServiceCallException("InventoryApi", (int)response.StatusCode, responseBody);
if (string.IsNullOrWhiteSpace(responseBody))
return 0;
using var json = JsonDocument.Parse(responseBody);
return json.RootElement.TryGetProperty("reassignedItemCount", out var countElement) && countElement.ValueKind == JsonValueKind.Number
? countElement.GetInt32()
: 0;
}
private sealed class InternalServiceCallException : Exception
{
public InternalServiceCallException(string serviceName, int statusCode, string responseBody)
: base($"{serviceName} returned {statusCode}")
{
ServiceName = serviceName;
StatusCode = statusCode;
ResponseBody = responseBody;
}
public string ServiceName { get; }
public int StatusCode { get; }
public string ResponseBody { get; }
}
}