diff --git a/game/scenes/Levels/location_level.gd b/game/scenes/Levels/location_level.gd index e306ad1..87d3b0b 100644 --- a/game/scenes/Levels/location_level.gd +++ b/game/scenes/Levels/location_level.gd @@ -15,9 +15,10 @@ const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory" @export var tile_label_height := 0.01 @export var tile_label_color: Color = Color(1, 1, 1, 1) -@onready var _block: MeshInstance3D = $TerrainBlock -@onready var _player: RigidBody3D = $Player -@onready var _camera: Camera3D = $Player/Camera3D +@onready var _block: MeshInstance3D = $TerrainBlock +@onready var _player: RigidBody3D = $Player +@onready var _camera: Camera3D = $Player/Camera3D +@onready var _player_visual: Node3D = $Player/TestCharAnimated var _center_coord := Vector2i.ZERO var _tiles_root: Node3D @@ -37,10 +38,10 @@ var _queued_locations_refresh := false var _interact_in_flight := false -func _ready() -> void: - _tiles_root = Node3D.new() - _tiles_root.name = "GeneratedTiles" - add_child(_tiles_root) +func _ready() -> void: + _tiles_root = Node3D.new() + _tiles_root.name = "GeneratedTiles" + add_child(_tiles_root) if _camera: _camera_start_offset = _camera.position @@ -50,15 +51,17 @@ func _ready() -> void: _tracked_node = _player var start_coord := SelectedCharacter.get_coord() - _center_coord = Vector2i(roundi(start_coord.x), roundi(start_coord.y)) - _persisted_coord = _center_coord - _character_id = String(SelectedCharacter.character.get("id", SelectedCharacter.character.get("Id", ""))).strip_edges() - - _block.visible = false - await _load_existing_locations() - _ensure_selected_location_exists(_center_coord) - _rebuild_tiles(_center_coord) - _move_player_to_coord(_center_coord) + _center_coord = Vector2i(roundi(start_coord.x), roundi(start_coord.y)) + _persisted_coord = _center_coord + _character_id = String(SelectedCharacter.character.get("id", SelectedCharacter.character.get("Id", ""))).strip_edges() + + _block.visible = false + _deactivate_player_for_load() + await _load_existing_locations() + _ensure_selected_location_exists(_center_coord) + _rebuild_tiles(_center_coord) + _move_player_to_coord(_center_coord) + _activate_player_after_load() func _process(_delta: float) -> void: @@ -95,12 +98,34 @@ func _coord_to_world(coord: Vector2i) -> Vector3: return Vector3(coord.x * tile_size, block_height * 0.5, coord.y * tile_size) -func _move_player_to_coord(coord: Vector2i) -> void: - if _player == null: - return - _player.global_position = Vector3(coord.x * tile_size, player_spawn_height, coord.y * tile_size) - _player.linear_velocity = Vector3.ZERO - _player.angular_velocity = Vector3.ZERO +func _move_player_to_coord(coord: Vector2i) -> void: + if _player == null: + return + _player.global_position = Vector3(coord.x * tile_size, player_spawn_height, coord.y * tile_size) + _player.linear_velocity = Vector3.ZERO + _player.angular_velocity = Vector3.ZERO + + +func _deactivate_player_for_load() -> void: + if _player == null: + return + _player.freeze = true + _player.sleeping = true + _player.linear_velocity = Vector3.ZERO + _player.angular_velocity = Vector3.ZERO + if _player_visual: + _player_visual.visible = false + + +func _activate_player_after_load() -> void: + if _player == null: + return + _player.linear_velocity = Vector3.ZERO + _player.angular_velocity = Vector3.ZERO + _player.sleeping = false + _player.freeze = false + if _player_visual: + _player_visual.visible = true func _rebuild_tiles(center: Vector2i) -> void: @@ -423,7 +448,7 @@ func _load_existing_locations() -> void: "name": location_name, "biomeKey": String(location.get("biomeKey", "plains")).strip_edges(), "locationObject": _parse_location_object(location.get("locationObject", {})), - "floorItems": [] + "floorItems": _parse_floor_inventory_items(location.get("floorItems", [])) } loaded_count += 1 @@ -431,11 +456,9 @@ func _load_existing_locations() -> void: if loaded_count == 0: push_warning("Visible locations request succeeded but returned 0 locations for character %s." % _character_id) - await _load_visible_location_inventories() - - _locations_loaded = true - _locations_refresh_in_flight = false - _rebuild_tiles(_center_coord) + _locations_loaded = true + _locations_refresh_in_flight = false + _rebuild_tiles(_center_coord) if _queued_locations_refresh: _queued_locations_refresh = false _queue_locations_refresh() @@ -633,6 +656,7 @@ func _parse_floor_inventory_items(value: Variant) -> Array: continue var item := entry as Dictionary items.append({ + "itemId": String(item.get("itemId", item.get("id", ""))).strip_edges(), "itemKey": String(item.get("itemKey", "")).strip_edges(), "quantity": int(item.get("quantity", 0)), "slot": item.get("slot", null) @@ -640,19 +664,6 @@ func _parse_floor_inventory_items(value: Variant) -> Array: return items - -func _load_visible_location_inventories() -> void: - for coord_variant in _known_locations.keys(): - var coord: Vector2i = coord_variant - var location_data: Dictionary = _known_locations[coord] - var location_id := String(location_data.get("id", "")).strip_edges() - if location_id.is_empty(): - continue - var floor_items := await _fetch_location_inventory(location_id) - location_data["floorItems"] = floor_items - _known_locations[coord] = location_data - - func _refresh_location_inventory(location_id: String) -> void: if location_id.is_empty(): return diff --git a/microservices/CharacterApi/Models/FloorInventoryItemResponse.cs b/microservices/CharacterApi/Models/FloorInventoryItemResponse.cs new file mode 100644 index 0000000..35b6b4d --- /dev/null +++ b/microservices/CharacterApi/Models/FloorInventoryItemResponse.cs @@ -0,0 +1,12 @@ +namespace CharacterApi.Models; + +public class FloorInventoryItemResponse +{ + public string ItemId { get; set; } = string.Empty; + + public string ItemKey { get; set; } = string.Empty; + + public int Quantity { get; set; } + + public int? Slot { get; set; } +} diff --git a/microservices/CharacterApi/Models/VisibleLocation.cs b/microservices/CharacterApi/Models/VisibleLocation.cs index f4cd32c..c7f250b 100644 --- a/microservices/CharacterApi/Models/VisibleLocation.cs +++ b/microservices/CharacterApi/Models/VisibleLocation.cs @@ -21,4 +21,7 @@ public class VisibleLocation [BsonElement("locationObject")] public VisibleLocationObject? LocationObject { get; set; } + + [BsonElement("floorItems")] + public List FloorItems { get; set; } = []; } diff --git a/microservices/InventoryApi/Controllers/InventoryController.cs b/microservices/InventoryApi/Controllers/InventoryController.cs index 1ce0b09..0955649 100644 --- a/microservices/InventoryApi/Controllers/InventoryController.cs +++ b/microservices/InventoryApi/Controllers/InventoryController.cs @@ -17,6 +17,37 @@ public class InventoryController : ControllerBase _inventory = inventory; } + [HttpPost("internal/by-owner/{ownerType}")] + public async Task GetByOwnersInternal(string ownerType, [FromBody] InternalOwnerInventoryBatchRequest req) + { + var configuredKey = (HttpContext.RequestServices.GetRequiredService()["InternalApi:Key"] + ?? HttpContext.RequestServices.GetRequiredService()["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(); + + var normalizedOwnerType = ownerType.Trim().ToLowerInvariant(); + if (normalizedOwnerType is not ("character" or "location")) + return BadRequest("Unsupported ownerType"); + + var groupedItems = await _inventory.GetByOwnersAsync(normalizedOwnerType, req.OwnerIds); + var ownerIds = req.OwnerIds + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Select(id => id.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + + var response = ownerIds.Select(ownerId => new OwnerInventorySummaryResponse + { + OwnerType = normalizedOwnerType, + OwnerId = ownerId, + Items = groupedItems.GetValueOrDefault(ownerId, []).Select(InventoryItemResponse.FromModel).ToList() + }).ToList(); + + return Ok(response); + } + [HttpGet("item-definitions")] [Authorize(Roles = "USER,SUPER")] public async Task ListItemDefinitions() diff --git a/microservices/InventoryApi/Models/InternalOwnerInventoryBatchRequest.cs b/microservices/InventoryApi/Models/InternalOwnerInventoryBatchRequest.cs new file mode 100644 index 0000000..44383bf --- /dev/null +++ b/microservices/InventoryApi/Models/InternalOwnerInventoryBatchRequest.cs @@ -0,0 +1,6 @@ +namespace InventoryApi.Models; + +public class InternalOwnerInventoryBatchRequest +{ + public List OwnerIds { get; set; } = []; +} diff --git a/microservices/InventoryApi/Models/OwnerInventorySummaryResponse.cs b/microservices/InventoryApi/Models/OwnerInventorySummaryResponse.cs new file mode 100644 index 0000000..da52550 --- /dev/null +++ b/microservices/InventoryApi/Models/OwnerInventorySummaryResponse.cs @@ -0,0 +1,10 @@ +namespace InventoryApi.Models; + +public class OwnerInventorySummaryResponse +{ + public string OwnerType { get; set; } = string.Empty; + + public string OwnerId { get; set; } = string.Empty; + + public List Items { get; set; } = []; +} diff --git a/microservices/InventoryApi/Services/InventoryStore.cs b/microservices/InventoryApi/Services/InventoryStore.cs index dacab91..ed3a5ce 100644 --- a/microservices/InventoryApi/Services/InventoryStore.cs +++ b/microservices/InventoryApi/Services/InventoryStore.cs @@ -88,6 +88,32 @@ public class InventoryStore .ThenBy(i => i.ItemKey) .ToListAsync(); + public async Task>> GetByOwnersAsync(string ownerType, IEnumerable ownerIds) + { + var normalizedOwnerType = NormalizeOwnerType(ownerType); + if (normalizedOwnerType is null) + return []; + + var ids = ownerIds + .Where(id => !string.IsNullOrWhiteSpace(id)) + .Select(id => id.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (ids.Count == 0) + return []; + + var items = await _items.Find(i => i.OwnerType == normalizedOwnerType && ids.Contains(i.OwnerId)) + .SortBy(i => i.OwnerId) + .ThenBy(i => i.EquippedSlot) + .ThenBy(i => i.Slot) + .ThenBy(i => i.ItemKey) + .ToListAsync(); + + return items + .GroupBy(item => item.OwnerId, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.ToList(), StringComparer.Ordinal); + } + public Task> ListItemDefinitionsAsync() => _definitions.Find(Builders.Filter.Empty).SortBy(d => d.ItemKey).ToListAsync(); diff --git a/microservices/InventoryApi/appsettings.Development.json b/microservices/InventoryApi/appsettings.Development.json index 0c208ae..9895a59 100644 --- a/microservices/InventoryApi/appsettings.Development.json +++ b/microservices/InventoryApi/appsettings.Development.json @@ -1,4 +1,7 @@ { + "InternalApi": { + "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/microservices/InventoryApi/appsettings.json b/microservices/InventoryApi/appsettings.json index b461ae5..55a484f 100644 --- a/microservices/InventoryApi/appsettings.json +++ b/microservices/InventoryApi/appsettings.json @@ -1,6 +1,7 @@ { "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5003" } } }, "MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" }, + "InternalApi": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!" }, "Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" }, "Logging": { "LogLevel": { "Default": "Information" } }, "AllowedHosts": "*" diff --git a/microservices/LocationsApi/Controllers/LocationsController.cs b/microservices/LocationsApi/Controllers/LocationsController.cs index b758fab..e947f7f 100644 --- a/microservices/LocationsApi/Controllers/LocationsController.cs +++ b/microservices/LocationsApi/Controllers/LocationsController.cs @@ -91,6 +91,7 @@ public class LocationsController : ControllerBase return BadRequest("radius must be non-negative"); var result = await _locations.GetOrCreateVisibleLocationsAsync(req.X, req.Y, req.Radius); + await PopulateFloorInventoriesAsync(result); return Ok(result); } @@ -367,4 +368,54 @@ public class LocationsController : ControllerBase } private static string NormalizeBiomeKey(string biomeKey) => biomeKey.Trim().ToLowerInvariant(); + + private async Task PopulateFloorInventoriesAsync(VisibleLocationWindowResponse result) + { + var locationIds = result.Locations + .Where(location => !string.IsNullOrWhiteSpace(location.Id)) + .Select(location => location.Id!) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (locationIds.Count == 0) + return; + + 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 { ownerIds = locationIds }); + + var client = _httpClientFactory.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Post, $"{inventoryBaseUrl}/api/inventory/internal/by-owner/location"); + 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) + { + _logger.LogWarning( + "Failed to load batched floor inventories from InventoryApi. Status={StatusCode} Body={Body}", + (int)response.StatusCode, + responseBody); + return; + } + + var parsed = JsonSerializer.Deserialize>( + responseBody, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? []; + var byOwnerId = parsed.ToDictionary(entry => entry.OwnerId, entry => entry.Items, StringComparer.Ordinal); + + foreach (var location in result.Locations) + { + if (string.IsNullOrWhiteSpace(location.Id)) + continue; + location.FloorItems = byOwnerId.GetValueOrDefault(location.Id!, []); + } + } + + private sealed class OwnerInventorySummaryEnvelope + { + public string OwnerId { get; set; } = string.Empty; + + public List Items { get; set; } = []; + } } diff --git a/microservices/LocationsApi/Models/FloorInventoryItemResponse.cs b/microservices/LocationsApi/Models/FloorInventoryItemResponse.cs new file mode 100644 index 0000000..24b6e81 --- /dev/null +++ b/microservices/LocationsApi/Models/FloorInventoryItemResponse.cs @@ -0,0 +1,12 @@ +namespace LocationsApi.Models; + +public class FloorInventoryItemResponse +{ + public string ItemId { get; set; } = string.Empty; + + public string ItemKey { get; set; } = string.Empty; + + public int Quantity { get; set; } + + public int? Slot { get; set; } +} diff --git a/microservices/LocationsApi/Models/VisibleLocationResponse.cs b/microservices/LocationsApi/Models/VisibleLocationResponse.cs index 06720a5..0a0fbde 100644 --- a/microservices/LocationsApi/Models/VisibleLocationResponse.cs +++ b/microservices/LocationsApi/Models/VisibleLocationResponse.cs @@ -11,4 +11,6 @@ public class VisibleLocationResponse public string BiomeKey { get; set; } = "plains"; public VisibleLocationObjectResponse? LocationObject { get; set; } + + public List FloorItems { get; set; } = []; }