Mailbox support + crafting api
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 46s
Deploy Promiscuity Crafting API / deploy (push) Successful in 1m9s
Deploy Promiscuity Inventory API / deploy (push) Successful in 46s
Deploy Promiscuity Locations API / deploy (push) Successful in 48s
Deploy Promiscuity Mail API / deploy (push) Successful in 46s
k8s smoke test / test (push) Successful in 8s
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 46s
Deploy Promiscuity Crafting API / deploy (push) Successful in 1m9s
Deploy Promiscuity Inventory API / deploy (push) Successful in 46s
Deploy Promiscuity Locations API / deploy (push) Successful in 48s
Deploy Promiscuity Mail API / deploy (push) Successful in 46s
k8s smoke test / test (push) Successful in 8s
This commit is contained in:
parent
a8db66b93e
commit
a283969e4c
78
.gitea/workflows/deploy-crafting.yml
Normal file
78
.gitea/workflows/deploy-crafting.yml
Normal file
@ -0,0 +1,78 @@
|
||||
name: Deploy Promiscuity Crafting API
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
|
||||
env:
|
||||
IMAGE_NAME: promiscuity-crafting:latest
|
||||
IMAGE_TAR: /tmp/promiscuity-crafting.tar
|
||||
NODES: "192.168.86.72 192.168.86.73 192.168.86.74"
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
cd microservices/CraftingApi
|
||||
docker build -t "${IMAGE_NAME}" .
|
||||
|
||||
- name: Save Docker image to TAR
|
||||
run: |
|
||||
docker save "${IMAGE_NAME}" -o "${IMAGE_TAR}"
|
||||
|
||||
- name: Copy TAR to nodes
|
||||
run: |
|
||||
for node in ${NODES}; do
|
||||
scp -o StrictHostKeyChecking=no "${IMAGE_TAR}" hz@"$node":/tmp/promiscuity-crafting.tar
|
||||
done
|
||||
|
||||
- name: Import image on nodes
|
||||
run: |
|
||||
for node in ${NODES}; do
|
||||
ssh -o StrictHostKeyChecking=no hz@"$node" "sudo ctr -n k8s.io images import /tmp/promiscuity-crafting.tar"
|
||||
done
|
||||
|
||||
- name: Clean TAR from nodes
|
||||
run: |
|
||||
for node in ${NODES}; do
|
||||
ssh -o StrictHostKeyChecking=no hz@"$node" "rm -f /tmp/promiscuity-crafting.tar"
|
||||
done
|
||||
|
||||
- name: Clean TAR on runner
|
||||
run: |
|
||||
rm -f "${IMAGE_TAR}"
|
||||
|
||||
- name: Write kubeconfig from secret
|
||||
env:
|
||||
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
|
||||
run: |
|
||||
mkdir -p /tmp/kube
|
||||
printf '%s\n' "$KUBECONFIG_CONTENT" > /tmp/kube/config
|
||||
|
||||
- name: Create namespace if missing
|
||||
env:
|
||||
KUBECONFIG: /tmp/kube/config
|
||||
run: |
|
||||
kubectl create namespace promiscuity-crafting --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Apply Crafting deployment & service
|
||||
env:
|
||||
KUBECONFIG: /tmp/kube/config
|
||||
run: |
|
||||
kubectl apply -f microservices/CraftingApi/k8s/deployment.yaml -n promiscuity-crafting
|
||||
kubectl apply -f microservices/CraftingApi/k8s/service.yaml -n promiscuity-crafting
|
||||
|
||||
- name: Restart Crafting deployment
|
||||
env:
|
||||
KUBECONFIG: /tmp/kube/config
|
||||
run: |
|
||||
kubectl rollout restart deployment/promiscuity-crafting -n promiscuity-crafting
|
||||
kubectl rollout status deployment/promiscuity-crafting -n promiscuity-crafting
|
||||
@ -1032,15 +1032,15 @@ func _parse_mail_messages(value: Variant) -> Array:
|
||||
continue
|
||||
var message := entry as Dictionary
|
||||
messages.append({
|
||||
"id": String(message.get("id", "")).strip_edges(),
|
||||
"senderCharacterId": String(message.get("senderCharacterId", "")).strip_edges(),
|
||||
"senderCharacterName": String(message.get("senderCharacterName", "")).strip_edges(),
|
||||
"recipientCharacterId": String(message.get("recipientCharacterId", "")).strip_edges(),
|
||||
"recipientCharacterName": String(message.get("recipientCharacterName", "")).strip_edges(),
|
||||
"subject": String(message.get("subject", "")).strip_edges(),
|
||||
"body": String(message.get("body", "")).strip_edges(),
|
||||
"createdUtc": String(message.get("createdUtc", "")).strip_edges(),
|
||||
"readUtc": String(message.get("readUtc", "")).strip_edges()
|
||||
"id": str(message.get("id", "")).strip_edges(),
|
||||
"senderCharacterId": str(message.get("senderCharacterId", "")).strip_edges(),
|
||||
"senderCharacterName": str(message.get("senderCharacterName", "")).strip_edges(),
|
||||
"recipientCharacterId": str(message.get("recipientCharacterId", "")).strip_edges(),
|
||||
"recipientCharacterName": str(message.get("recipientCharacterName", "")).strip_edges(),
|
||||
"subject": str(message.get("subject", "")).strip_edges(),
|
||||
"body": str(message.get("body", "")).strip_edges(),
|
||||
"createdUtc": str(message.get("createdUtc", "")).strip_edges(),
|
||||
"readUtc": str(message.get("readUtc", "")).strip_edges()
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
@ -99,9 +99,9 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 48
|
||||
theme_override_constants/margin_top = 48
|
||||
theme_override_constants/margin_top = 24
|
||||
theme_override_constants/margin_right = 48
|
||||
theme_override_constants/margin_bottom = 48
|
||||
theme_override_constants/margin_bottom = 24
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="InventoryMenu/MarginContainer"]
|
||||
layout_mode = 2
|
||||
@ -282,7 +282,7 @@ theme_override_constants/separation = 12
|
||||
|
||||
[node name="InboxPanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel"]
|
||||
@ -295,13 +295,14 @@ text = "Inbox"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="InboxItems" type="ItemList" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 220)
|
||||
custom_minimum_size = Vector2(0, 160)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SentPanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel"]
|
||||
@ -314,8 +315,9 @@ text = "Sent"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="SentItems" type="ItemList" parent="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/SentPanel/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 220)
|
||||
custom_minimum_size = Vector2(0, 160)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="DetailsComposePanel" type="PanelContainer" parent="MailMenu/MarginContainer/Panel/VBoxContainer"]
|
||||
@ -330,7 +332,7 @@ layout_mode = 2
|
||||
text = "Selected Mail"
|
||||
|
||||
[node name="SelectedMailBody" type="TextEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 120)
|
||||
custom_minimum_size = Vector2(0, 80)
|
||||
layout_mode = 2
|
||||
editable = false
|
||||
|
||||
@ -343,7 +345,7 @@ layout_mode = 2
|
||||
placeholder_text = "Subject"
|
||||
|
||||
[node name="ComposeBody" type="TextEdit" parent="MailMenu/MarginContainer/Panel/VBoxContainer/DetailsComposePanel/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 120)
|
||||
custom_minimum_size = Vector2(0, 80)
|
||||
layout_mode = 2
|
||||
placeholder_text = "Write your mail here"
|
||||
|
||||
|
||||
135
microservices/CraftingApi/Controllers/CraftingController.cs
Normal file
135
microservices/CraftingApi/Controllers/CraftingController.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using CraftingApi.Models;
|
||||
using CraftingApi.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace CraftingApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CraftingController : ControllerBase
|
||||
{
|
||||
private readonly CraftingStore _crafting;
|
||||
|
||||
public CraftingController(CraftingStore crafting)
|
||||
{
|
||||
_crafting = crafting;
|
||||
}
|
||||
|
||||
[HttpGet("recipes")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> ListRecipes()
|
||||
{
|
||||
var recipes = await _crafting.ListRecipesAsync();
|
||||
return Ok(recipes.Select(CraftingRecipeResponse.FromModel).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("recipes/{recipeKey}")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> GetRecipe(string recipeKey)
|
||||
{
|
||||
var recipe = await _crafting.GetRecipeAsync(recipeKey);
|
||||
return recipe is null ? NotFound() : Ok(CraftingRecipeResponse.FromModel(recipe));
|
||||
}
|
||||
|
||||
[HttpPost("recipes/{recipeKey}")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> CreateRecipe(string recipeKey, [FromBody] UpsertCraftingRecipeRequest request)
|
||||
{
|
||||
var validationError = ValidateRecipeRequest(recipeKey, request);
|
||||
if (validationError is not null)
|
||||
return validationError;
|
||||
|
||||
var recipe = _crafting.BuildRecipe(recipeKey, request);
|
||||
var created = await _crafting.CreateRecipeAsync(recipe);
|
||||
return created ? Ok(CraftingRecipeResponse.FromModel(recipe)) : Conflict("Recipe already exists");
|
||||
}
|
||||
|
||||
[HttpPut("recipes/{recipeKey}")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> UpsertRecipe(string recipeKey, [FromBody] UpsertCraftingRecipeRequest request)
|
||||
{
|
||||
var validationError = ValidateRecipeRequest(recipeKey, request);
|
||||
if (validationError is not null)
|
||||
return validationError;
|
||||
|
||||
var recipe = await _crafting.UpsertRecipeAsync(recipeKey, request);
|
||||
return Ok(CraftingRecipeResponse.FromModel(recipe));
|
||||
}
|
||||
|
||||
[HttpGet("characters/{characterId}/available-recipes")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> AvailableRecipes(string characterId)
|
||||
{
|
||||
var access = await ResolveCharacterAccessAsync(characterId);
|
||||
if (access is IActionResult errorResult)
|
||||
return errorResult;
|
||||
|
||||
var available = await _crafting.GetAvailableRecipesAsync((CraftingStore.CharacterAccessResult)access!);
|
||||
return Ok(available);
|
||||
}
|
||||
|
||||
[HttpPost("characters/{characterId}/craft")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public async Task<IActionResult> Craft(string characterId, [FromBody] CraftRecipeRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.RecipeKey))
|
||||
return BadRequest("recipeKey required");
|
||||
if (request.Quantity <= 0)
|
||||
return BadRequest("quantity must be greater than 0");
|
||||
|
||||
var access = await ResolveCharacterAccessAsync(characterId);
|
||||
if (access is IActionResult errorResult)
|
||||
return errorResult;
|
||||
|
||||
var result = await _crafting.CraftAsync((CraftingStore.CharacterAccessResult)access!, request);
|
||||
return result.Status switch
|
||||
{
|
||||
CraftingStore.CraftStatus.RecipeNotFound => NotFound("Recipe not found"),
|
||||
CraftingStore.CraftStatus.MissingStation => Conflict("Required crafting station is not available"),
|
||||
CraftingStore.CraftStatus.MissingInputs => Conflict(new { missingRequirements = result.MissingRequirements }),
|
||||
CraftingStore.CraftStatus.InvalidRecipe => BadRequest(new { missingRequirements = result.MissingRequirements }),
|
||||
_ => Ok(new CraftRecipeResponse
|
||||
{
|
||||
CharacterId = characterId,
|
||||
RecipeKey = request.RecipeKey.Trim().ToLowerInvariant(),
|
||||
CraftedQuantity = result.CraftedQuantity,
|
||||
Consumed = result.Consumed,
|
||||
Produced = result.Produced
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<object> ResolveCharacterAccessAsync(string characterId)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
|
||||
var access = await _crafting.ResolveCharacterAsync(characterId, userId, User.IsInRole("SUPER"));
|
||||
if (!access.Exists)
|
||||
return NotFound();
|
||||
if (!access.IsAuthorized)
|
||||
return Forbid();
|
||||
|
||||
return access;
|
||||
}
|
||||
|
||||
private IActionResult? ValidateRecipeRequest(string recipeKey, UpsertCraftingRecipeRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(recipeKey))
|
||||
return BadRequest("recipeKey required");
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest("name required");
|
||||
if (request.Inputs.Count == 0)
|
||||
return BadRequest("At least one input is required");
|
||||
if (request.Outputs.Count == 0)
|
||||
return BadRequest("At least one output is required");
|
||||
if (request.Inputs.Any(i => string.IsNullOrWhiteSpace(i.ItemKey) || i.Quantity <= 0))
|
||||
return BadRequest("All inputs must have itemKey and quantity > 0");
|
||||
if (request.Outputs.Any(i => string.IsNullOrWhiteSpace(i.ItemKey) || i.Quantity <= 0))
|
||||
return BadRequest("All outputs must have itemKey and quantity > 0");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
16
microservices/CraftingApi/CraftingApi.csproj
Normal file
16
microservices/CraftingApi/CraftingApi.csproj
Normal file
@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.4.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
microservices/CraftingApi/DOCUMENTS.md
Normal file
10
microservices/CraftingApi/DOCUMENTS.md
Normal file
@ -0,0 +1,10 @@
|
||||
# CraftingApi
|
||||
|
||||
- `GET /api/crafting/recipes`
|
||||
- `GET /api/crafting/recipes/{recipeKey}`
|
||||
- `POST /api/crafting/recipes/{recipeKey}` (`SUPER`)
|
||||
- `PUT /api/crafting/recipes/{recipeKey}` (`SUPER`)
|
||||
- `GET /api/crafting/characters/{characterId}/available-recipes`
|
||||
- `POST /api/crafting/characters/{characterId}/craft`
|
||||
|
||||
Recipes are data-driven and support `stationType`, `inputs`, `outputs`, and `enabled`.
|
||||
10
microservices/CraftingApi/Dockerfile
Normal file
10
microservices/CraftingApi/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 5005
|
||||
ENTRYPOINT ["dotnet", "CraftingApi.dll"]
|
||||
@ -0,0 +1,10 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class AvailableCraftingRecipeResponse
|
||||
{
|
||||
public CraftingRecipeResponse Recipe { get; set; } = new();
|
||||
|
||||
public bool CanCraft { get; set; }
|
||||
|
||||
public List<string> MissingRequirements { get; set; } = [];
|
||||
}
|
||||
12
microservices/CraftingApi/Models/CraftRecipeRequest.cs
Normal file
12
microservices/CraftingApi/Models/CraftRecipeRequest.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class CraftRecipeRequest
|
||||
{
|
||||
public string RecipeKey { get; set; } = string.Empty;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
public string? LocationId { get; set; }
|
||||
|
||||
public string? StationObjectId { get; set; }
|
||||
}
|
||||
14
microservices/CraftingApi/Models/CraftRecipeResponse.cs
Normal file
14
microservices/CraftingApi/Models/CraftRecipeResponse.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class CraftRecipeResponse
|
||||
{
|
||||
public string CharacterId { get; set; } = string.Empty;
|
||||
|
||||
public string RecipeKey { get; set; } = string.Empty;
|
||||
|
||||
public int CraftedQuantity { get; set; }
|
||||
|
||||
public List<CraftingIngredient> Consumed { get; set; } = [];
|
||||
|
||||
public List<CraftingIngredient> Produced { get; set; } = [];
|
||||
}
|
||||
8
microservices/CraftingApi/Models/CraftingIngredient.cs
Normal file
8
microservices/CraftingApi/Models/CraftingIngredient.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class CraftingIngredient
|
||||
{
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
38
microservices/CraftingApi/Models/CraftingRecipe.cs
Normal file
38
microservices/CraftingApi/Models/CraftingRecipe.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
public class CraftingRecipe
|
||||
{
|
||||
[BsonId]
|
||||
[BsonElement("recipeKey")]
|
||||
public string RecipeKey { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("category")]
|
||||
public string Category { get; set; } = "misc";
|
||||
|
||||
[BsonElement("stationType")]
|
||||
public string StationType { get; set; } = "hand";
|
||||
|
||||
[BsonElement("inputs")]
|
||||
public List<CraftingIngredient> Inputs { get; set; } = [];
|
||||
|
||||
[BsonElement("outputs")]
|
||||
public List<CraftingIngredient> Outputs { get; set; } = [];
|
||||
|
||||
[BsonElement("craftTimeSeconds")]
|
||||
public int CraftTimeSeconds { get; set; }
|
||||
|
||||
[BsonElement("enabled")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[BsonElement("createdUtc")]
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[BsonElement("updatedUtc")]
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
32
microservices/CraftingApi/Models/CraftingRecipeResponse.cs
Normal file
32
microservices/CraftingApi/Models/CraftingRecipeResponse.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class CraftingRecipeResponse
|
||||
{
|
||||
public string RecipeKey { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
public string StationType { get; set; } = string.Empty;
|
||||
|
||||
public List<CraftingIngredient> Inputs { get; set; } = [];
|
||||
|
||||
public List<CraftingIngredient> Outputs { get; set; } = [];
|
||||
|
||||
public int CraftTimeSeconds { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public static CraftingRecipeResponse FromModel(CraftingRecipe recipe) => new()
|
||||
{
|
||||
RecipeKey = recipe.RecipeKey,
|
||||
Name = recipe.Name,
|
||||
Category = recipe.Category,
|
||||
StationType = recipe.StationType,
|
||||
Inputs = recipe.Inputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity }).ToList(),
|
||||
Outputs = recipe.Outputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity }).ToList(),
|
||||
CraftTimeSeconds = recipe.CraftTimeSeconds,
|
||||
Enabled = recipe.Enabled
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
namespace CraftingApi.Models;
|
||||
|
||||
public class UpsertCraftingRecipeRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Category { get; set; } = "misc";
|
||||
|
||||
public string StationType { get; set; } = "hand";
|
||||
|
||||
public List<CraftingIngredient> Inputs { get; set; } = [];
|
||||
|
||||
public List<CraftingIngredient> Outputs { get; set; } = [];
|
||||
|
||||
public int CraftTimeSeconds { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
105
microservices/CraftingApi/Program.cs
Normal file
105
microservices/CraftingApi/Program.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using CraftingApi.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddSingleton<CraftingStore>();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Crafting API", Version = "v1" });
|
||||
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT",
|
||||
Description = "Paste your access token here (no 'Bearer ' prefix needed)."
|
||||
});
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{ Type = ReferenceType.SecurityScheme, Id = "bearerAuth" }
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var cfg = builder.Configuration;
|
||||
var jwtKey = cfg["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
|
||||
var issuer = cfg["Jwt:Issuer"] ?? "promiscuity";
|
||||
var aud = cfg["Jwt:Audience"] ?? issuer;
|
||||
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(o =>
|
||||
{
|
||||
o.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = aud,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseExceptionHandler(errorApp =>
|
||||
{
|
||||
errorApp.Run(async context =>
|
||||
{
|
||||
var feature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
var exception = feature?.Error;
|
||||
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("GlobalException");
|
||||
var traceId = context.TraceIdentifier;
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Unhandled exception for {Method} {Path}. TraceId={TraceId}",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
traceId
|
||||
);
|
||||
}
|
||||
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
context.Response.ContentType = "application/problem+json";
|
||||
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
type = "https://httpstatuses.com/500",
|
||||
title = "Internal Server Error",
|
||||
status = 500,
|
||||
detail = exception?.Message ?? "An unexpected server error occurred.",
|
||||
traceId
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.MapGet("/healthz", () => Results.Ok("ok"));
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(o =>
|
||||
{
|
||||
o.SwaggerEndpoint("/swagger/v1/swagger.json", "Crafting API v1");
|
||||
o.RoutePrefix = "swagger";
|
||||
});
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
3
microservices/CraftingApi/README.md
Normal file
3
microservices/CraftingApi/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# CraftingApi
|
||||
|
||||
Data-driven crafting recipes and instant crafting execution.
|
||||
455
microservices/CraftingApi/Services/CraftingStore.cs
Normal file
455
microservices/CraftingApi/Services/CraftingStore.cs
Normal file
@ -0,0 +1,455 @@
|
||||
using CraftingApi.Models;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace CraftingApi.Services;
|
||||
|
||||
public class CraftingStore
|
||||
{
|
||||
private readonly IMongoCollection<CraftingRecipe> _recipes;
|
||||
private readonly IMongoCollection<CharacterDocument> _characters;
|
||||
private readonly IMongoCollection<LocationDocument> _locations;
|
||||
private readonly IMongoCollection<InventoryItemDocument> _items;
|
||||
private readonly IMongoCollection<ItemDefinitionDocument> _definitions;
|
||||
private const string CharacterOwnerType = "character";
|
||||
|
||||
public CraftingStore(IConfiguration cfg)
|
||||
{
|
||||
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||
var dbName = cfg["MongoDB:DatabaseName"] ?? "promiscuity";
|
||||
var client = new MongoClient(cs);
|
||||
var db = client.GetDatabase(dbName);
|
||||
|
||||
_recipes = db.GetCollection<CraftingRecipe>("CraftingRecipes");
|
||||
_characters = db.GetCollection<CharacterDocument>("Characters");
|
||||
_locations = db.GetCollection<LocationDocument>("Locations");
|
||||
_items = db.GetCollection<InventoryItemDocument>("InventoryItems");
|
||||
_definitions = db.GetCollection<ItemDefinitionDocument>("ItemDefinitions");
|
||||
|
||||
_recipes.Indexes.CreateOne(new CreateIndexModel<CraftingRecipe>(
|
||||
Builders<CraftingRecipe>.IndexKeys.Ascending(r => r.Category)));
|
||||
_recipes.Indexes.CreateOne(new CreateIndexModel<CraftingRecipe>(
|
||||
Builders<CraftingRecipe>.IndexKeys.Ascending(r => r.StationType)));
|
||||
}
|
||||
|
||||
public async Task<CharacterAccessResult> ResolveCharacterAsync(string characterId, string userId, bool allowAnyOwner)
|
||||
{
|
||||
var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
|
||||
if (character is null)
|
||||
return new CharacterAccessResult { Exists = false };
|
||||
|
||||
return new CharacterAccessResult
|
||||
{
|
||||
Exists = true,
|
||||
IsAuthorized = allowAnyOwner || character.OwnerUserId == userId,
|
||||
CharacterId = character.Id ?? string.Empty,
|
||||
OwnerUserId = character.OwnerUserId,
|
||||
CoordX = character.Coord.X,
|
||||
CoordY = character.Coord.Y
|
||||
};
|
||||
}
|
||||
|
||||
public Task<List<CraftingRecipe>> ListRecipesAsync() =>
|
||||
_recipes.Find(Builders<CraftingRecipe>.Filter.Empty).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
|
||||
|
||||
public async Task<CraftingRecipe?> GetRecipeAsync(string recipeKey) =>
|
||||
await _recipes.Find(r => r.RecipeKey == NormalizeRecipeKey(recipeKey)).FirstOrDefaultAsync();
|
||||
|
||||
public async Task<bool> CreateRecipeAsync(CraftingRecipe recipe)
|
||||
{
|
||||
var existing = await GetRecipeAsync(recipe.RecipeKey);
|
||||
if (existing is not null)
|
||||
return false;
|
||||
|
||||
await _recipes.InsertOneAsync(recipe);
|
||||
return true;
|
||||
}
|
||||
|
||||
public CraftingRecipe BuildRecipe(string recipeKey, UpsertCraftingRecipeRequest request)
|
||||
{
|
||||
return new CraftingRecipe
|
||||
{
|
||||
RecipeKey = NormalizeRecipeKey(recipeKey),
|
||||
Name = request.Name.Trim(),
|
||||
Category = NormalizeSimpleKey(request.Category, "misc"),
|
||||
StationType = NormalizeSimpleKey(request.StationType, "hand"),
|
||||
Inputs = NormalizeIngredients(request.Inputs),
|
||||
Outputs = NormalizeIngredients(request.Outputs),
|
||||
CraftTimeSeconds = Math.Max(0, request.CraftTimeSeconds),
|
||||
Enabled = request.Enabled,
|
||||
UpdatedUtc = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<CraftingRecipe> UpsertRecipeAsync(string recipeKey, UpsertCraftingRecipeRequest request)
|
||||
{
|
||||
var normalizedRecipeKey = NormalizeRecipeKey(recipeKey);
|
||||
var recipe = BuildRecipe(normalizedRecipeKey, request);
|
||||
|
||||
var existing = await GetRecipeAsync(normalizedRecipeKey);
|
||||
if (existing is not null)
|
||||
recipe.CreatedUtc = existing.CreatedUtc;
|
||||
|
||||
await _recipes.ReplaceOneAsync(r => r.RecipeKey == normalizedRecipeKey, recipe, new ReplaceOptions { IsUpsert = true });
|
||||
return recipe;
|
||||
}
|
||||
|
||||
public async Task<List<AvailableCraftingRecipeResponse>> GetAvailableRecipesAsync(CharacterAccessResult character)
|
||||
{
|
||||
var recipes = await _recipes.Find(r => r.Enabled).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
|
||||
var items = await GetCharacterItemsAsync(character.CharacterId);
|
||||
var itemTotals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
|
||||
var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
|
||||
|
||||
return recipes.Select(recipe =>
|
||||
{
|
||||
var missing = GetMissingRequirements(recipe, itemTotals, location);
|
||||
return new AvailableCraftingRecipeResponse
|
||||
{
|
||||
Recipe = CraftingRecipeResponse.FromModel(recipe),
|
||||
CanCraft = missing.Count == 0,
|
||||
MissingRequirements = missing
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<CraftAttemptResult> CraftAsync(CharacterAccessResult character, CraftRecipeRequest request)
|
||||
{
|
||||
var recipe = await GetRecipeAsync(request.RecipeKey);
|
||||
if (recipe is null || !recipe.Enabled)
|
||||
return new CraftAttemptResult { Status = CraftStatus.RecipeNotFound };
|
||||
|
||||
var craftCount = Math.Max(1, request.Quantity);
|
||||
var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
|
||||
if (!CanUseStation(recipe, location, request.LocationId, request.StationObjectId))
|
||||
return new CraftAttemptResult { Status = CraftStatus.MissingStation };
|
||||
|
||||
var items = await GetCharacterItemsAsync(character.CharacterId);
|
||||
var totals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
|
||||
var missing = GetMissingRequirements(recipe, totals, location, craftCount);
|
||||
if (missing.Count > 0)
|
||||
return new CraftAttemptResult { Status = CraftStatus.MissingInputs, MissingRequirements = missing };
|
||||
|
||||
var definitions = await _definitions.Find(Builders<ItemDefinitionDocument>.Filter.Empty).ToListAsync();
|
||||
var definitionMap = definitions.ToDictionary(d => d.ItemKey, StringComparer.Ordinal);
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
if (!definitionMap.ContainsKey(output.ItemKey))
|
||||
return new CraftAttemptResult { Status = CraftStatus.InvalidRecipe, MissingRequirements = [$"Missing item definition for output '{output.ItemKey}'"] };
|
||||
}
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
await ConsumeItemKeyAsync(character, input.ItemKey, input.Quantity * craftCount);
|
||||
|
||||
foreach (var output in recipe.Outputs)
|
||||
await GrantItemKeyAsync(character, output.ItemKey, output.Quantity * craftCount, definitionMap[output.ItemKey]);
|
||||
|
||||
return new CraftAttemptResult
|
||||
{
|
||||
Status = CraftStatus.Ok,
|
||||
CraftedQuantity = craftCount,
|
||||
Consumed = recipe.Inputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity * craftCount }).ToList(),
|
||||
Produced = recipe.Outputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity * craftCount }).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<InventoryItemDocument>> GetCharacterItemsAsync(string characterId) =>
|
||||
await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == characterId).SortBy(i => i.Slot).ThenBy(i => i.ItemKey).ToListAsync();
|
||||
|
||||
private async Task<LocationDocument?> GetLocationAtCoordAsync(int x, int y) =>
|
||||
await _locations.Find(l => l.Coord.X == x && l.Coord.Y == y).FirstOrDefaultAsync();
|
||||
|
||||
private static List<CraftingIngredient> NormalizeIngredients(IEnumerable<CraftingIngredient> ingredients)
|
||||
{
|
||||
return ingredients
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.ItemKey) && i.Quantity > 0)
|
||||
.Select(i => new CraftingIngredient
|
||||
{
|
||||
ItemKey = NormalizeSimpleKey(i.ItemKey, ""),
|
||||
Quantity = i.Quantity
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeRecipeKey(string recipeKey) => NormalizeSimpleKey(recipeKey, "");
|
||||
|
||||
private static string NormalizeSimpleKey(string value, string fallback)
|
||||
{
|
||||
var normalized = value.Trim().ToLowerInvariant();
|
||||
return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized;
|
||||
}
|
||||
|
||||
private static List<string> GetMissingRequirements(CraftingRecipe recipe, IReadOnlyDictionary<string, int> itemTotals, LocationDocument? location, int craftCount = 1)
|
||||
{
|
||||
var missing = new List<string>();
|
||||
if (!CanUseStation(recipe, location, null, null))
|
||||
missing.Add("Required station is not available");
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
var required = input.Quantity * craftCount;
|
||||
var available = itemTotals.TryGetValue(input.ItemKey, out var amount) ? amount : 0;
|
||||
if (available < required)
|
||||
missing.Add("Need %s x%d".Replace("%s", input.ItemKey).Replace("%d", required.ToString()));
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
private static bool CanUseStation(CraftingRecipe recipe, LocationDocument? location, string? requestedLocationId, string? requestedStationObjectId)
|
||||
{
|
||||
if (recipe.StationType == "hand")
|
||||
return true;
|
||||
if (location is null)
|
||||
return false;
|
||||
if (!string.IsNullOrWhiteSpace(requestedLocationId) && !string.Equals(location.Id, requestedLocationId, StringComparison.Ordinal))
|
||||
return false;
|
||||
if (location.LocationObject is null)
|
||||
return false;
|
||||
if (!string.IsNullOrWhiteSpace(requestedStationObjectId) && !string.Equals(location.LocationObject.ObjectId, requestedStationObjectId, StringComparison.Ordinal))
|
||||
return false;
|
||||
if (!string.Equals(location.LocationObject.ObjectType, "station", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var stationType = NormalizeSimpleKey(location.LocationObject.State.StationType ?? string.Empty, "");
|
||||
return string.Equals(recipe.StationType, stationType, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private async Task ConsumeItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity)
|
||||
{
|
||||
var remaining = quantity;
|
||||
var items = await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == character.CharacterId && i.ItemKey == itemKey && i.EquippedSlot == null)
|
||||
.SortBy(i => i.Slot)
|
||||
.ThenBy(i => i.CreatedUtc)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (remaining <= 0)
|
||||
break;
|
||||
|
||||
if (item.Quantity <= remaining)
|
||||
{
|
||||
remaining -= item.Quantity;
|
||||
await _items.DeleteOneAsync(i => i.Id == item.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Quantity -= remaining;
|
||||
item.UpdatedUtc = DateTime.UtcNow;
|
||||
await _items.ReplaceOneAsync(i => i.Id == item.Id, item);
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GrantItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity, ItemDefinitionDocument definition)
|
||||
{
|
||||
var remaining = quantity;
|
||||
if (definition.Stackable)
|
||||
{
|
||||
var existingStacks = await _items.Find(i =>
|
||||
i.OwnerType == CharacterOwnerType &&
|
||||
i.OwnerId == character.CharacterId &&
|
||||
i.ItemKey == itemKey &&
|
||||
i.EquippedSlot == null &&
|
||||
i.Slot != null)
|
||||
.SortBy(i => i.Slot)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var stack in existingStacks)
|
||||
{
|
||||
if (remaining <= 0)
|
||||
break;
|
||||
|
||||
var availableSpace = definition.MaxStackSize - stack.Quantity;
|
||||
if (availableSpace <= 0)
|
||||
continue;
|
||||
|
||||
var added = Math.Min(remaining, availableSpace);
|
||||
stack.Quantity += added;
|
||||
stack.UpdatedUtc = DateTime.UtcNow;
|
||||
await _items.ReplaceOneAsync(i => i.Id == stack.Id, stack);
|
||||
remaining -= added;
|
||||
}
|
||||
}
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
var slot = await FindFirstOpenSlotAsync(character.CharacterId);
|
||||
if (slot is null)
|
||||
throw new InvalidOperationException("Crafting outputs did not fit in the character inventory.");
|
||||
|
||||
var granted = definition.Stackable ? Math.Min(remaining, definition.MaxStackSize) : 1;
|
||||
var item = new InventoryItemDocument
|
||||
{
|
||||
ItemKey = itemKey,
|
||||
Quantity = granted,
|
||||
OwnerType = CharacterOwnerType,
|
||||
OwnerId = character.CharacterId,
|
||||
OwnerUserId = character.OwnerUserId,
|
||||
Slot = slot.Value,
|
||||
CreatedUtc = DateTime.UtcNow,
|
||||
UpdatedUtc = DateTime.UtcNow
|
||||
};
|
||||
await _items.InsertOneAsync(item);
|
||||
remaining -= granted;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int?> FindFirstOpenSlotAsync(string characterId)
|
||||
{
|
||||
var items = await GetCharacterItemsAsync(characterId);
|
||||
var used = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
|
||||
for (var slot = 0; slot < 6; slot++)
|
||||
{
|
||||
if (!used.Contains(slot))
|
||||
return slot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public class CharacterAccessResult
|
||||
{
|
||||
public bool Exists { get; set; }
|
||||
|
||||
public bool IsAuthorized { get; set; }
|
||||
|
||||
public string CharacterId { get; set; } = string.Empty;
|
||||
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
|
||||
public int CoordX { get; set; }
|
||||
|
||||
public int CoordY { get; set; }
|
||||
}
|
||||
|
||||
public class CraftAttemptResult
|
||||
{
|
||||
public CraftStatus Status { get; set; }
|
||||
|
||||
public int CraftedQuantity { get; set; }
|
||||
|
||||
public List<CraftingIngredient> Consumed { get; set; } = [];
|
||||
|
||||
public List<CraftingIngredient> Produced { get; set; } = [];
|
||||
|
||||
public List<string> MissingRequirements { get; set; } = [];
|
||||
}
|
||||
|
||||
public enum CraftStatus
|
||||
{
|
||||
Ok,
|
||||
RecipeNotFound,
|
||||
MissingInputs,
|
||||
MissingStation,
|
||||
InvalidRecipe
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class CharacterDocument
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
|
||||
public CoordDocument Coord { get; set; } = new();
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class CoordDocument
|
||||
{
|
||||
public int X { get; set; }
|
||||
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class LocationDocument
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[BsonElement("coord")]
|
||||
public CoordDocument Coord { get; set; } = new();
|
||||
|
||||
[BsonElement("locationObject")]
|
||||
public LocationObjectDocument? LocationObject { get; set; }
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class LocationObjectDocument
|
||||
{
|
||||
[BsonElement("id")]
|
||||
public string ObjectId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("objectType")]
|
||||
public string ObjectType { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("state")]
|
||||
public LocationObjectStateDocument State { get; set; } = new();
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class LocationObjectStateDocument
|
||||
{
|
||||
[BsonElement("stationType")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? StationType { get; set; }
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class InventoryItemDocument
|
||||
{
|
||||
[BsonId]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[BsonElement("itemKey")]
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("quantity")]
|
||||
public int Quantity { get; set; }
|
||||
|
||||
[BsonElement("ownerType")]
|
||||
public string OwnerType { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("ownerId")]
|
||||
public string OwnerId { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("ownerUserId")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? OwnerUserId { get; set; }
|
||||
|
||||
[BsonElement("slot")]
|
||||
[BsonIgnoreIfNull]
|
||||
public int? Slot { get; set; }
|
||||
|
||||
[BsonElement("equippedSlot")]
|
||||
[BsonIgnoreIfNull]
|
||||
public string? EquippedSlot { get; set; }
|
||||
|
||||
[BsonElement("createdUtc")]
|
||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[BsonElement("updatedUtc")]
|
||||
public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[BsonIgnoreExtraElements]
|
||||
private class ItemDefinitionDocument
|
||||
{
|
||||
[BsonId]
|
||||
[BsonElement("itemKey")]
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
|
||||
[BsonElement("stackable")]
|
||||
public bool Stackable { get; set; }
|
||||
|
||||
[BsonElement("maxStackSize")]
|
||||
public int MaxStackSize { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
7
microservices/CraftingApi/appsettings.Development.json
Normal file
7
microservices/CraftingApi/appsettings.Development.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5005" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://127.0.0.1:27017", "DatabaseName": "promiscuity" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
7
microservices/CraftingApi/appsettings.json
Normal file
7
microservices/CraftingApi/appsettings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5005" } } },
|
||||
"MongoDB": { "ConnectionString": "mongodb://192.168.86.50:27017", "DatabaseName": "promiscuity" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
28
microservices/CraftingApi/k8s/deployment.yaml
Normal file
28
microservices/CraftingApi/k8s/deployment.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: promiscuity-crafting
|
||||
labels:
|
||||
app: promiscuity-crafting
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: promiscuity-crafting
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: promiscuity-crafting
|
||||
spec:
|
||||
containers:
|
||||
- name: promiscuity-crafting
|
||||
image: promiscuity-crafting:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5005
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 5005
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
15
microservices/CraftingApi/k8s/service.yaml
Normal file
15
microservices/CraftingApi/k8s/service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: promiscuity-crafting
|
||||
labels:
|
||||
app: promiscuity-crafting
|
||||
spec:
|
||||
selector:
|
||||
app: promiscuity-crafting
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 5005
|
||||
nodePort: 30085
|
||||
@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InventoryApi", "InventoryAp
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailApi", "MailApi\MailApi.csproj", "{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CraftingApi", "CraftingApi\CraftingApi.csproj", "{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -38,6 +40,10 @@ Global
|
||||
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2A6A80E9-2D58-4E42-8B93-483F2D6E6D42}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user