Kill switch
Some checks failed
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
Some checks failed
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:
parent
bed8e91bc0
commit
c718fb343d
@ -68,11 +68,28 @@ public class CharactersController : ControllerBase
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
|
||||
|
||||
var characters = await _characters.GetForOwnerAsync(userId);
|
||||
return Ok(characters);
|
||||
}
|
||||
|
||||
[HttpPost("internal/reset-coords")]
|
||||
public async Task<IActionResult> ResetCoordsInternal([FromBody] InternalResetCharactersToCoordRequest req)
|
||||
{
|
||||
var configuredKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
|
||||
var requestKey = (Request.Headers["X-Internal-Api-Key"].FirstOrDefault() ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(configuredKey) || !string.Equals(configuredKey, requestKey, StringComparison.Ordinal))
|
||||
return Unauthorized();
|
||||
if (req.Coord is null)
|
||||
return BadRequest("coord required");
|
||||
|
||||
var updatedCharacterCount = await _characters.ResetAllCoordsAsync(req.Coord);
|
||||
return Ok(new InternalResetCharactersToCoordResponse
|
||||
{
|
||||
UpdatedCharacterCount = updatedCharacterCount
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{id}/visible-locations")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> VisibleLocations(string id)
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
namespace CharacterApi.Models;
|
||||
|
||||
public class InternalResetCharactersToCoordRequest
|
||||
{
|
||||
public required Coord Coord { get; set; }
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace CharacterApi.Models;
|
||||
|
||||
public class InternalResetCharactersToCoordResponse
|
||||
{
|
||||
public int UpdatedCharacterCount { get; set; }
|
||||
}
|
||||
@ -49,6 +49,15 @@ public class CharacterStore
|
||||
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
||||
}
|
||||
|
||||
public async Task<int> ResetAllCoordsAsync(Coord coord)
|
||||
{
|
||||
var update = Builders<Character>.Update
|
||||
.Set(c => c.Coord, coord)
|
||||
.Set(c => c.LastSeenUtc, DateTime.UtcNow);
|
||||
var result = await _col.UpdateManyAsync(Builders<Character>.Filter.Empty, update);
|
||||
return (int)result.MatchedCount;
|
||||
}
|
||||
|
||||
public async Task<List<VisibleCharacter>> GetVisibleOthersAsync(string characterId, int x, int y, int radius, DateTime onlineSinceUtc)
|
||||
{
|
||||
var characters = await _col.Find(c =>
|
||||
|
||||
@ -48,6 +48,28 @@ public class InventoryController : ControllerBase
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("internal/location-owner/reassign")]
|
||||
public async Task<IActionResult> ReassignLocationOwnerInventoryInternal([FromBody] InternalReassignLocationInventoryRequest req)
|
||||
{
|
||||
var configuredKey = (HttpContext.RequestServices.GetRequiredService<IConfiguration>()["InternalApi:Key"]
|
||||
?? HttpContext.RequestServices.GetRequiredService<IConfiguration>()["Jwt:Key"]
|
||||
?? string.Empty).Trim();
|
||||
var requestKey = (Request.Headers["X-Internal-Api-Key"].FirstOrDefault() ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(configuredKey) || !string.Equals(configuredKey, requestKey, StringComparison.Ordinal))
|
||||
return Unauthorized();
|
||||
if (string.IsNullOrWhiteSpace(req.ToOwnerId))
|
||||
return BadRequest("toOwnerId required");
|
||||
|
||||
var result = await _inventory.ReassignLocationInventoryOwnersAsync(req.FromOwnerIds, req.ToOwnerId);
|
||||
if (!result.TargetLocationExists)
|
||||
return NotFound("Target location not found");
|
||||
|
||||
return Ok(new InternalReassignLocationInventoryResponse
|
||||
{
|
||||
ReassignedItemCount = result.ReassignedItemCount
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("item-definitions")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> ListItemDefinitions()
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
namespace InventoryApi.Models;
|
||||
|
||||
public class InternalReassignLocationInventoryRequest
|
||||
{
|
||||
public List<string> FromOwnerIds { get; set; } = [];
|
||||
|
||||
public string ToOwnerId { get; set; } = string.Empty;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace InventoryApi.Models;
|
||||
|
||||
public class InternalReassignLocationInventoryResponse
|
||||
{
|
||||
public int ReassignedItemCount { get; set; }
|
||||
}
|
||||
@ -22,6 +22,7 @@ public class InventoryStore
|
||||
private readonly string _dbName;
|
||||
|
||||
public sealed record GrantResult(List<InventoryItem> Items, int RequestedQuantity, int GrantedQuantity, int OverflowQuantity);
|
||||
public sealed record ReassignLocationInventoryResult(bool TargetLocationExists, int ReassignedItemCount);
|
||||
|
||||
public InventoryStore(IConfiguration cfg)
|
||||
{
|
||||
@ -114,6 +115,35 @@ public class InventoryStore
|
||||
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public async Task<ReassignLocationInventoryResult> ReassignLocationInventoryOwnersAsync(IEnumerable<string> fromOwnerIds, string toOwnerId)
|
||||
{
|
||||
var normalizedToOwnerId = toOwnerId.Trim();
|
||||
var targetExists = await _locations.Find(location => location.Id == normalizedToOwnerId).AnyAsync();
|
||||
if (!targetExists)
|
||||
return new ReassignLocationInventoryResult(false, 0);
|
||||
|
||||
var sourceOwnerIds = fromOwnerIds
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id.Trim())
|
||||
.Where(id => !string.Equals(id, normalizedToOwnerId, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (sourceOwnerIds.Count == 0)
|
||||
return new ReassignLocationInventoryResult(true, 0);
|
||||
|
||||
var filter = Builders<InventoryItem>.Filter.And(
|
||||
Builders<InventoryItem>.Filter.Eq(item => item.OwnerType, LocationOwnerType),
|
||||
Builders<InventoryItem>.Filter.In(item => item.OwnerId, sourceOwnerIds));
|
||||
var update = Builders<InventoryItem>.Update
|
||||
.Set(item => item.OwnerId, normalizedToOwnerId)
|
||||
.Set(item => item.OwnerUserId, null)
|
||||
.Unset(item => item.Slot)
|
||||
.Unset(item => item.EquippedSlot)
|
||||
.Set(item => item.UpdatedUtc, DateTime.UtcNow);
|
||||
var result = await _items.UpdateManyAsync(filter, update);
|
||||
return new ReassignLocationInventoryResult(true, (int)result.ModifiedCount);
|
||||
}
|
||||
|
||||
public Task<List<ItemDefinition>> ListItemDefinitionsAsync() =>
|
||||
_definitions.Find(Builders<ItemDefinition>.Filter.Empty).SortBy(d => d.ItemKey).ToListAsync();
|
||||
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,14 @@ Inbound JSON documents
|
||||
}
|
||||
```
|
||||
`coord` cannot be updated.
|
||||
- ResetWorldRequest (`POST /api/locations/reset-world`)
|
||||
```json
|
||||
{
|
||||
"confirmDeleteAllNonOriginLocations": true,
|
||||
"confirmationPhrase": "RESET WORLD TO ORIGIN"
|
||||
}
|
||||
```
|
||||
Both fields are required for the request to be accepted.
|
||||
- GatherResourceRequest (`POST /api/locations/{id}/gather`)
|
||||
```json
|
||||
{
|
||||
@ -63,3 +71,40 @@ Outbound JSON documents
|
||||
"inventoryResponseJson": "string (raw InventoryApi response JSON)"
|
||||
}
|
||||
```
|
||||
- ResetWorldResponse (`POST /api/locations/reset-world`)
|
||||
```json
|
||||
{
|
||||
"deletedLocationCount": 42,
|
||||
"remainingLocationCount": 2,
|
||||
"movedCharacterCount": 5,
|
||||
"reassignedInventoryItemCount": 19,
|
||||
"origin": {
|
||||
"id": "string (ObjectId)",
|
||||
"name": "Origin",
|
||||
"coord": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"biomeKey": "plains",
|
||||
"elevation": 0,
|
||||
"resources": [],
|
||||
"locationObject": null,
|
||||
"locationObjectResolved": true,
|
||||
"createdUtc": "string (ISO-8601 datetime)"
|
||||
},
|
||||
"limbo": {
|
||||
"id": "string (ObjectId)",
|
||||
"name": "World Limbo",
|
||||
"coord": {
|
||||
"x": -2000000000,
|
||||
"y": -2000000000
|
||||
},
|
||||
"biomeKey": "plains",
|
||||
"elevation": 0,
|
||||
"resources": [],
|
||||
"locationObject": null,
|
||||
"locationObjectResolved": true,
|
||||
"createdUtc": "string (ISO-8601 datetime)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8
microservices/LocationsApi/Models/ResetWorldRequest.cs
Normal file
8
microservices/LocationsApi/Models/ResetWorldRequest.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace LocationsApi.Models;
|
||||
|
||||
public class ResetWorldRequest
|
||||
{
|
||||
public bool ConfirmDeleteAllNonOriginLocations { get; set; }
|
||||
|
||||
public string ConfirmationPhrase { get; set; } = string.Empty;
|
||||
}
|
||||
16
microservices/LocationsApi/Models/ResetWorldResponse.cs
Normal file
16
microservices/LocationsApi/Models/ResetWorldResponse.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace LocationsApi.Models;
|
||||
|
||||
public class ResetWorldResponse
|
||||
{
|
||||
public int DeletedLocationCount { get; set; }
|
||||
|
||||
public int RemainingLocationCount { get; set; }
|
||||
|
||||
public int MovedCharacterCount { get; set; }
|
||||
|
||||
public int ReassignedInventoryItemCount { get; set; }
|
||||
|
||||
public required Location Origin { get; set; }
|
||||
|
||||
public required Location Limbo { get; set; }
|
||||
}
|
||||
@ -5,6 +5,7 @@ See `DOCUMENTS.md` for request payloads and stored document shapes.
|
||||
|
||||
## Endpoints
|
||||
- `POST /api/locations` Create a location with a unique coord pair (SUPER only).
|
||||
- `POST /api/locations/reset-world` Move all characters to origin, move floor inventory from reset locations into a hidden limbo location, then recreate the world with a fresh origin tile plus that limbo location. Requires an explicit confirmation request body (SUPER only).
|
||||
- `GET /api/locations` List all locations (SUPER only).
|
||||
- `DELETE /api/locations/{id}` Delete a location (SUPER only).
|
||||
- `PUT /api/locations/{id}` Update a location name (SUPER only).
|
||||
|
||||
@ -11,6 +11,9 @@ public class LocationStore
|
||||
private readonly IMongoCollection<CharacterDocument> _characters;
|
||||
private readonly IMongoCollection<BiomeDefinition> _biomeDefinitions;
|
||||
private const string CoordIndexName = "coord_x_1_coord_y_1";
|
||||
private const string LimboLocationName = "World Limbo";
|
||||
private const int LimboCoordX = -2000000000;
|
||||
private const int LimboCoordY = -2000000000;
|
||||
|
||||
public LocationStore(IConfiguration cfg)
|
||||
{
|
||||
@ -339,6 +342,60 @@ public class LocationStore
|
||||
return hasUpper || (hasLower && !(keyDoc.Contains("coord.x") && keyDoc.Contains("coord.y")));
|
||||
}
|
||||
|
||||
public async Task<Location> EnsureLimboLocationAsync()
|
||||
{
|
||||
var filter = Builders<Location>.Filter.And(
|
||||
Builders<Location>.Filter.Eq(location => location.Coord.X, LimboCoordX),
|
||||
Builders<Location>.Filter.Eq(location => location.Coord.Y, LimboCoordY));
|
||||
var existing = await _col.Find(filter).FirstOrDefaultAsync();
|
||||
if (existing is not null)
|
||||
return existing;
|
||||
|
||||
var biomeDefinitions = await LoadBiomeDefinitionsAsync();
|
||||
var limbo = BuildLimboLocation(biomeDefinitions);
|
||||
try
|
||||
{
|
||||
await _col.InsertOneAsync(limbo);
|
||||
return limbo;
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
return await _col.Find(filter).FirstOrDefaultAsync()
|
||||
?? throw new InvalidOperationException("Limbo location was created concurrently but could not be reloaded.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetLocationIdsForWorldResetAsync(string preservedLocationId)
|
||||
{
|
||||
var filter = Builders<Location>.Filter.Ne(location => location.Id, preservedLocationId);
|
||||
var locations = await _col.Find(filter).ToListAsync();
|
||||
return locations
|
||||
.Where(location => !string.IsNullOrWhiteSpace(location.Id))
|
||||
.Select(location => location.Id!)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<ResetWorldResponse> ResetWorldToOriginAsync(string preservedLocationId)
|
||||
{
|
||||
var biomeDefinitions = await LoadBiomeDefinitionsAsync();
|
||||
var deleteFilter = Builders<Location>.Filter.Ne(location => location.Id, preservedLocationId);
|
||||
var deleteResult = await _col.DeleteManyAsync(deleteFilter);
|
||||
|
||||
var origin = BuildOriginLocation(biomeDefinitions);
|
||||
await _col.InsertOneAsync(origin);
|
||||
var limbo = await _col.Find(location => location.Id == preservedLocationId).FirstOrDefaultAsync()
|
||||
?? throw new InvalidOperationException("Limbo location missing after world reset.");
|
||||
var remainingLocationCount = await _col.CountDocumentsAsync(Builders<Location>.Filter.Empty);
|
||||
|
||||
return new ResetWorldResponse
|
||||
{
|
||||
DeletedLocationCount = (int)deleteResult.DeletedCount,
|
||||
RemainingLocationCount = (int)remainingLocationCount,
|
||||
Origin = origin,
|
||||
Limbo = limbo
|
||||
};
|
||||
}
|
||||
|
||||
private void EnsureOriginLocation()
|
||||
{
|
||||
var biomeDefinitions = LoadBiomeDefinitions();
|
||||
@ -356,21 +413,12 @@ public class LocationStore
|
||||
if (existing is not null)
|
||||
return;
|
||||
|
||||
var origin = new Location
|
||||
var origin = BuildOriginLocation(biomeDefinitions, originBiomeKey);
|
||||
|
||||
try
|
||||
{
|
||||
Name = "Origin",
|
||||
Coord = new Coord { X = 0, Y = 0 },
|
||||
BiomeKey = originBiomeKey,
|
||||
Elevation = 0,
|
||||
LocationObject = CreateLocationObjectForBiome(biomeDefinitions, originBiomeKey, 0, 0),
|
||||
LocationObjectResolved = true,
|
||||
CreatedUtc = DateTime.UtcNow
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_col.InsertOne(origin);
|
||||
}
|
||||
_col.InsertOne(origin);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
// Another instance seeded it first.
|
||||
@ -461,6 +509,44 @@ public class LocationStore
|
||||
}
|
||||
}
|
||||
|
||||
private static Location BuildOriginLocation(IReadOnlyList<BiomeDefinition> biomeDefinitions, string? originBiomeKey = null)
|
||||
{
|
||||
var resolvedOriginBiomeKey = string.IsNullOrWhiteSpace(originBiomeKey)
|
||||
? biomeDefinitions.Any(definition => definition.BiomeKey == "plains")
|
||||
? "plains"
|
||||
: biomeDefinitions[0].BiomeKey
|
||||
: originBiomeKey;
|
||||
|
||||
return new Location
|
||||
{
|
||||
Name = "Origin",
|
||||
Coord = new Coord { X = 0, Y = 0 },
|
||||
BiomeKey = resolvedOriginBiomeKey,
|
||||
Elevation = 0,
|
||||
LocationObject = CreateLocationObjectForBiome(biomeDefinitions, resolvedOriginBiomeKey, 0, 0),
|
||||
LocationObjectResolved = true,
|
||||
CreatedUtc = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static Location BuildLimboLocation(IReadOnlyList<BiomeDefinition> biomeDefinitions)
|
||||
{
|
||||
var limboBiomeKey = biomeDefinitions.Any(definition => definition.BiomeKey == "plains")
|
||||
? "plains"
|
||||
: biomeDefinitions[0].BiomeKey;
|
||||
|
||||
return new Location
|
||||
{
|
||||
Name = LimboLocationName,
|
||||
Coord = new Coord { X = LimboCoordX, Y = LimboCoordY },
|
||||
BiomeKey = limboBiomeKey,
|
||||
Elevation = 0,
|
||||
LocationObject = null,
|
||||
LocationObjectResolved = true,
|
||||
CreatedUtc = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Location> EnsureLocationMetadataAsync(Location location)
|
||||
{
|
||||
var locationId = location.Id ?? string.Empty;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user