World microapi
Some checks failed
Deploy Promiscuity Auth API / deploy (push) Successful in 49s
Deploy Promiscuity Character API / deploy (push) Successful in 46s
Deploy Promiscuity Crafting API / deploy (push) Successful in 47s
Deploy Promiscuity Inventory API / deploy (push) Successful in 47s
Deploy Promiscuity Locations API / deploy (push) Successful in 48s
Deploy Promiscuity Mail API / deploy (push) Successful in 46s
k8s smoke test / test (push) Has been cancelled
Some checks failed
Deploy Promiscuity Auth API / deploy (push) Successful in 49s
Deploy Promiscuity Character API / deploy (push) Successful in 46s
Deploy Promiscuity Crafting API / deploy (push) Successful in 47s
Deploy Promiscuity Inventory API / deploy (push) Successful in 47s
Deploy Promiscuity Locations API / deploy (push) Successful in 48s
Deploy Promiscuity Mail API / deploy (push) Successful in 46s
k8s smoke test / test (push) Has been cancelled
This commit is contained in:
parent
94473ab1b0
commit
2bed6aae4f
@ -4,11 +4,13 @@ const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
||||
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
||||
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
||||
const MAIL_API_URL := "https://pmail.ranaze.com/api/mail"
|
||||
const WORLD_API_URL := "https://pworld.ranaze.com/api/world"
|
||||
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
||||
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
||||
const CHARACTER_SLOT_COUNT := 6
|
||||
const VISIBLE_CHARACTERS_REFRESH_INTERVAL := 5.0
|
||||
const HEARTBEAT_INTERVAL := 10.0
|
||||
const WORLD_CYCLE_REFRESH_INTERVAL := 15.0
|
||||
|
||||
@export var tile_size := 8.0
|
||||
@export var block_height := 1.0
|
||||
@ -77,6 +79,9 @@ var _mail_sent: Array = []
|
||||
var _mail_request_in_flight := false
|
||||
var _selected_inbox_mail_id := ""
|
||||
var _selected_sent_mail_id := ""
|
||||
var _world_cycle_request_in_flight := false
|
||||
var _world_cycle_refresh_elapsed := 0.0
|
||||
var _world_cycle: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@ -102,6 +107,7 @@ func _ready() -> void:
|
||||
_block.visible = false
|
||||
_deactivate_player_for_load()
|
||||
await _load_existing_locations()
|
||||
_refresh_world_cycle()
|
||||
_refresh_visible_characters()
|
||||
_ensure_selected_location_exists(_center_coord)
|
||||
_rebuild_tiles(_center_coord)
|
||||
@ -114,12 +120,16 @@ func _process(_delta: float) -> void:
|
||||
return
|
||||
_visible_character_refresh_elapsed += _delta
|
||||
_heartbeat_elapsed += _delta
|
||||
_world_cycle_refresh_elapsed += _delta
|
||||
if _visible_character_refresh_elapsed >= VISIBLE_CHARACTERS_REFRESH_INTERVAL:
|
||||
_visible_character_refresh_elapsed = 0.0
|
||||
_refresh_visible_characters()
|
||||
if _heartbeat_elapsed >= HEARTBEAT_INTERVAL:
|
||||
_heartbeat_elapsed = 0.0
|
||||
_send_presence_heartbeat()
|
||||
if _world_cycle_refresh_elapsed >= WORLD_CYCLE_REFRESH_INTERVAL:
|
||||
_world_cycle_refresh_elapsed = 0.0
|
||||
_refresh_world_cycle()
|
||||
if _inventory_menu.visible or _mail_menu.visible:
|
||||
return
|
||||
var target_world_pos := _get_stream_position()
|
||||
@ -1321,8 +1331,52 @@ func _refresh_visible_locations() -> void:
|
||||
_refresh_visible_locations_async()
|
||||
|
||||
|
||||
func _refresh_visible_locations_async() -> void:
|
||||
await _load_existing_locations()
|
||||
func _refresh_visible_locations_async() -> void:
|
||||
await _load_existing_locations()
|
||||
|
||||
|
||||
func _refresh_world_cycle() -> void:
|
||||
if _world_cycle_request_in_flight:
|
||||
return
|
||||
_refresh_world_cycle_async()
|
||||
|
||||
|
||||
func _refresh_world_cycle_async() -> void:
|
||||
_world_cycle_request_in_flight = true
|
||||
|
||||
var request := HTTPRequest.new()
|
||||
add_child(request)
|
||||
|
||||
var headers := PackedStringArray()
|
||||
if not AuthState.access_token.is_empty():
|
||||
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||
|
||||
var err := request.request("%s/cycle" % WORLD_API_URL, headers, HTTPClient.METHOD_GET)
|
||||
if err != OK:
|
||||
push_warning("Failed to request world cycle: %s" % err)
|
||||
request.queue_free()
|
||||
_world_cycle_request_in_flight = false
|
||||
return
|
||||
|
||||
var result: Array = await request.request_completed
|
||||
request.queue_free()
|
||||
|
||||
var result_code: int = result[0]
|
||||
var response_code: int = result[1]
|
||||
var response_body: String = result[3].get_string_from_utf8()
|
||||
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
push_warning("Failed to load world cycle (%s/%s): %s" % [result_code, response_code, response_body])
|
||||
_world_cycle_request_in_flight = false
|
||||
return
|
||||
|
||||
var parsed: Variant = JSON.parse_string(response_body)
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
push_warning("World cycle response was not an object.")
|
||||
_world_cycle_request_in_flight = false
|
||||
return
|
||||
|
||||
_world_cycle = parsed as Dictionary
|
||||
_world_cycle_request_in_flight = false
|
||||
|
||||
|
||||
func _try_interact_current_tile() -> void:
|
||||
|
||||
24
microservices/WorldApi/Controllers/WorldController.cs
Normal file
24
microservices/WorldApi/Controllers/WorldController.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WorldApi.Services;
|
||||
|
||||
namespace WorldApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WorldController : ControllerBase
|
||||
{
|
||||
private readonly WorldCycleService _worldCycle;
|
||||
|
||||
public WorldController(WorldCycleService worldCycle)
|
||||
{
|
||||
_worldCycle = worldCycle;
|
||||
}
|
||||
|
||||
[HttpGet("cycle")]
|
||||
[Authorize(Roles = "USER,SUPER")]
|
||||
public IActionResult GetCycle()
|
||||
{
|
||||
return Ok(_worldCycle.GetCurrentCycle());
|
||||
}
|
||||
}
|
||||
15
microservices/WorldApi/DOCUMENTS.md
Normal file
15
microservices/WorldApi/DOCUMENTS.md
Normal file
@ -0,0 +1,15 @@
|
||||
# WorldApi document shapes
|
||||
|
||||
Outbound JSON documents
|
||||
- WorldCycleResponse (`GET /api/world/cycle`)
|
||||
```json
|
||||
{
|
||||
"currentUtc": "string (ISO-8601 datetime)",
|
||||
"dayLengthSeconds": 1200,
|
||||
"timeOfDaySeconds": 438.2,
|
||||
"timeOfDayNormalized": 0.365,
|
||||
"phase": "day",
|
||||
"isDay": true,
|
||||
"lightLevel": 0.91
|
||||
}
|
||||
```
|
||||
18
microservices/WorldApi/Models/WorldCycleResponse.cs
Normal file
18
microservices/WorldApi/Models/WorldCycleResponse.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace WorldApi.Models;
|
||||
|
||||
public class WorldCycleResponse
|
||||
{
|
||||
public DateTime CurrentUtc { get; set; }
|
||||
|
||||
public double DayLengthSeconds { get; set; }
|
||||
|
||||
public double TimeOfDaySeconds { get; set; }
|
||||
|
||||
public double TimeOfDayNormalized { get; set; }
|
||||
|
||||
public string Phase { get; set; } = "day";
|
||||
|
||||
public bool IsDay { get; set; }
|
||||
|
||||
public double LightLevel { get; set; }
|
||||
}
|
||||
106
microservices/WorldApi/Program.cs
Normal file
106
microservices/WorldApi/Program.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Text;
|
||||
using WorldApi.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddSingleton<WorldCycleService>();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "World 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", "World API v1");
|
||||
o.RoutePrefix = "swagger";
|
||||
});
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
14
microservices/WorldApi/Properties/launchSettings.json
Normal file
14
microservices/WorldApi/Properties/launchSettings.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5185",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
microservices/WorldApi/README.md
Normal file
3
microservices/WorldApi/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# WorldApi
|
||||
|
||||
Global world simulation state such as the shared day/night cycle.
|
||||
56
microservices/WorldApi/Services/WorldCycleService.cs
Normal file
56
microservices/WorldApi/Services/WorldCycleService.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using WorldApi.Models;
|
||||
|
||||
namespace WorldApi.Services;
|
||||
|
||||
public class WorldCycleService
|
||||
{
|
||||
private readonly double _dayLengthSeconds;
|
||||
private readonly DateTime _epochUtc;
|
||||
|
||||
public WorldCycleService(IConfiguration configuration)
|
||||
{
|
||||
_dayLengthSeconds = Math.Max(1.0, configuration.GetValue<double?>("WorldCycle:DayLengthSeconds") ?? 1200.0);
|
||||
|
||||
var configuredEpoch = configuration["WorldCycle:EpochUtc"];
|
||||
_epochUtc = DateTime.TryParse(
|
||||
configuredEpoch,
|
||||
null,
|
||||
System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal,
|
||||
out var parsedEpoch)
|
||||
? parsedEpoch
|
||||
: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
public WorldCycleResponse GetCurrentCycle()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsedSeconds = Math.Max(0.0, (now - _epochUtc).TotalSeconds);
|
||||
var timeOfDaySeconds = elapsedSeconds % _dayLengthSeconds;
|
||||
var normalized = timeOfDaySeconds / _dayLengthSeconds;
|
||||
var solarCurve = Math.Sin(normalized * Math.PI);
|
||||
var lightLevel = Math.Clamp(solarCurve, 0.0, 1.0);
|
||||
|
||||
return new WorldCycleResponse
|
||||
{
|
||||
CurrentUtc = now,
|
||||
DayLengthSeconds = _dayLengthSeconds,
|
||||
TimeOfDaySeconds = timeOfDaySeconds,
|
||||
TimeOfDayNormalized = normalized,
|
||||
Phase = ResolvePhase(normalized),
|
||||
IsDay = lightLevel > 0.0,
|
||||
LightLevel = lightLevel
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolvePhase(double normalized)
|
||||
{
|
||||
return normalized switch
|
||||
{
|
||||
< 0.20 => "night",
|
||||
< 0.30 => "dawn",
|
||||
< 0.70 => "day",
|
||||
< 0.80 => "dusk",
|
||||
_ => "night"
|
||||
};
|
||||
}
|
||||
}
|
||||
15
microservices/WorldApi/WorldApi.csproj
Normal file
15
microservices/WorldApi/WorldApi.csproj
Normal file
@ -0,0 +1,15 @@
|
||||
<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="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
microservices/WorldApi/appsettings.Development.json
Normal file
8
microservices/WorldApi/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
microservices/WorldApi/appsettings.json
Normal file
7
microservices/WorldApi/appsettings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5004" } } },
|
||||
"WorldCycle": { "DayLengthSeconds": 1200, "EpochUtc": "2026-01-01T00:00:00Z" },
|
||||
"Jwt": { "Key": "SuperUltraSecureJwtKeyWithAtLeast32Chars!!", "Issuer": "promiscuity", "Audience": "promiscuity-auth-api" },
|
||||
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailApi", "MailApi\MailApi.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CraftingApi", "CraftingApi\CraftingApi.csproj", "{0B5EE564-C6E1-4BA7-B0E2-B86D363A9C74}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldApi", "WorldApi\WorldApi.csproj", "{C8F20B54-2A76-4BE0-8DA8-E146D1AF4D10}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -44,6 +46,10 @@ Global
|
||||
{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
|
||||
{C8F20B54-2A76-4BE0-8DA8-E146D1AF4D10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C8F20B54-2A76-4BE0-8DA8-E146D1AF4D10}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C8F20B54-2A76-4BE0-8DA8-E146D1AF4D10}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C8F20B54-2A76-4BE0-8DA8-E146D1AF4D10}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user