using CharacterApi.Models; using MongoDB.Bson; using MongoDB.Driver; namespace CharacterApi.Services; public class CharacterStore { private readonly IMongoCollection _col; private readonly IMongoCollection _locations; public CharacterStore(IConfiguration cfg) { var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017"; var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb"; var client = new MongoClient(cs); var db = client.GetDatabase(dbName); _col = db.GetCollection("Characters"); _locations = db.GetCollection("Locations"); var ownerIndex = Builders.IndexKeys.Ascending(c => c.OwnerUserId); _col.Indexes.CreateOne(new CreateIndexModel(ownerIndex)); } public Task CreateAsync(Character character) => _col.InsertOneAsync(character); public Task> GetForOwnerAsync(string ownerUserId) => _col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync(); public async Task GetForOwnerByIdAsync(string id, string ownerUserId, bool allowAnyOwner) { var filter = Builders.Filter.Eq(c => c.Id, id); if (!allowAnyOwner) { filter = Builders.Filter.And( filter, Builders.Filter.Eq(c => c.OwnerUserId, ownerUserId) ); } return await _col.Find(filter).FirstOrDefaultAsync(); } public Task> GetVisibleLocationsAsync(Character character) { return GetVisibleLocationsInternalAsync(character, ensureGenerated: false); } public async Task> GetOrCreateVisibleLocationsAsync(Character character) { await EnsureVisibleLocationsExistAsync(character); return await GetVisibleLocationsInternalAsync(character, ensureGenerated: true); } private Task> GetVisibleLocationsInternalAsync(Character character, bool ensureGenerated) { var radius = character.VisionRadius > 0 ? character.VisionRadius : 3; var minX = character.Coord.X - radius; var maxX = character.Coord.X + radius; var minY = character.Coord.Y - radius; var maxY = character.Coord.Y + radius; var filter = Builders.Filter.And( Builders.Filter.Gte(l => l.Coord.X, minX), Builders.Filter.Lte(l => l.Coord.X, maxX), Builders.Filter.Gte(l => l.Coord.Y, minY), Builders.Filter.Lte(l => l.Coord.Y, maxY) ); return _locations.Find(filter).ToListAsync(); } private async Task EnsureVisibleLocationsExistAsync(Character character) { var radius = character.VisionRadius > 0 ? character.VisionRadius : 3; for (var x = character.Coord.X - radius; x <= character.Coord.X + radius; x++) { for (var y = character.Coord.Y - radius; y <= character.Coord.Y + radius; y++) { var filter = Builders.Filter.And( Builders.Filter.Eq(l => l.Coord.X, x), Builders.Filter.Eq(l => l.Coord.Y, y) ); var update = Builders.Update .SetOnInsert(l => l.Name, DefaultLocationName(x, y)) .SetOnInsert(l => l.Coord, new LocationCoord { X = x, Y = y }) .SetOnInsert(l => l.Id, ObjectId.GenerateNewId().ToString()) .SetOnInsert("createdUtc", DateTime.UtcNow); try { await _locations.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { // Another request or service instance created it first. } } } } private static string DefaultLocationName(int x, int y) { if (x == 0 && y == 0) return "Origin"; return $"Location {x},{y}"; } public async Task DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner) { var filter = Builders.Filter.Eq(c => c.Id, id); if (!allowAnyOwner) { filter = Builders.Filter.And( filter, Builders.Filter.Eq(c => c.OwnerUserId, ownerUserId) ); } var result = await _col.DeleteOneAsync(filter); return result.DeletedCount > 0; } }