chore(game-server): merge main (emulator fixes: contracts, shop, fortuna, friends, admin, presence) #16

Merged
neckfire merged 35 commits from main into game-server 2026-07-16 08:25:21 +00:00
5 changed files with 117 additions and 6 deletions
Showing only changes of commit b05782d4e9 - Show all commits
@@ -5,6 +5,7 @@ using System.Text.Json;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Prospect.Server.Api.Services.Database; using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Database.Models; using Prospect.Server.Api.Services.Database.Models;
using Prospect.Server.Api.Services.GameRef;
using Prospect.Server.Api.Services.Steam; using Prospect.Server.Api.Services.Steam;
using Prospect.Server.Api.Services.UserData; using Prospect.Server.Api.Services.UserData;
@@ -21,15 +22,18 @@ public class AdminController : ControllerBase
private readonly DbUserDataService _userDataService; private readonly DbUserDataService _userDataService;
private readonly TitleDataService _titleDataService; private readonly TitleDataService _titleDataService;
private readonly SteamWebApiService _steamWebApi; private readonly SteamWebApiService _steamWebApi;
private readonly GameRefService _gameRef;
public AdminController(DbUserService userService, DbEntityService entityService, public AdminController(DbUserService userService, DbEntityService entityService,
DbUserDataService userDataService, TitleDataService titleDataService, SteamWebApiService steamWebApi) DbUserDataService userDataService, TitleDataService titleDataService, SteamWebApiService steamWebApi,
GameRefService gameRef)
{ {
_userService = userService; _userService = userService;
_entityService = entityService; _entityService = entityService;
_userDataService = userDataService; _userDataService = userDataService;
_titleDataService = titleDataService; _titleDataService = titleDataService;
_steamWebApi = steamWebApi; _steamWebApi = steamWebApi;
_gameRef = gameRef;
} }
// ---------- pages ---------- // ---------- pages ----------
@@ -216,6 +220,37 @@ public class AdminController : ControllerBase
return Html(Layout("Backfill", body)); return Html(Layout("Backfill", body));
} }
[HttpGet("catalog")]
public IActionResult Catalog([FromQuery] string? q, [FromQuery] string? cat)
{
if (!IsLan()) return Denied();
var all = _gameRef.All;
IEnumerable<KeyValuePair<string, GameRefEntry>> items = all;
if (!string.IsNullOrWhiteSpace(cat))
items = items.Where(kv => kv.Value.Category == cat);
if (!string.IsNullOrWhiteSpace(q))
{
var n = q.Trim().ToLowerInvariant();
items = items.Where(kv => kv.Key.ToLowerInvariant().Contains(n) || (kv.Value.Name ?? "").ToLowerInvariant().Contains(n));
}
var cats = all.Values.Select(v => v.Category).Distinct().OrderBy(x => x).ToList();
var list = items.OrderBy(kv => kv.Value.Category).ThenBy(kv => kv.Value.Name).Take(1000).ToList();
var sb = new StringBuilder();
sb.Append($"<p class='muted'>Référence dataminée (TCF-Wiki) — {all.Count} entrées connues.</p>");
sb.Append($"<form method='get' class='search'><input name='q' placeholder='nom / id…' value='{Esc(q ?? "")}'/>");
sb.Append("<select name='cat'><option value=''>(toutes)</option>");
foreach (var c in cats) sb.Append($"<option value='{Esc(c)}'{(c == cat ? " selected" : "")}>{Esc(c)}</option>");
sb.Append("</select><button class='btn'>Filtrer</button></form>");
sb.Append($"<p class='muted'>{list.Count} résultat(s){(list.Count >= 1000 ? " (tronqué)" : "")}</p>");
sb.Append("<table><thead><tr><th>Nom</th><th>Rareté</th><th>Catégorie</th><th>id</th></tr></thead><tbody>");
foreach (var kv in list)
sb.Append($"<tr><td>{Esc(kv.Value.Name)}</td><td>{Rarity(kv.Value.Rarity)}</td><td>{Esc(kv.Value.Category)}</td><td class='mono small'>{Esc(kv.Key)}</td></tr>");
sb.Append("</tbody></table>");
return Html(Layout("Catalogue", sb.ToString()));
}
// ---------- helpers ---------- // ---------- helpers ----------
private bool IsLan() private bool IsLan()
@@ -272,7 +307,7 @@ public class AdminController : ControllerBase
return 0; return 0;
} }
private static string InventoryTable(string? json) private string InventoryTable(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) return "<p class='muted'>vide</p>"; if (string.IsNullOrWhiteSpace(json)) return "<p class='muted'>vide</p>";
try try
@@ -282,12 +317,15 @@ public class AdminController : ControllerBase
var sb = new StringBuilder(); var sb = new StringBuilder();
var count = doc.RootElement.GetArrayLength(); var count = doc.RootElement.GetArrayLength();
sb.Append($"<p class='muted'>{count} item(s)</p>"); sb.Append($"<p class='muted'>{count} item(s)</p>");
sb.Append("<table><thead><tr><th>baseItemId</th><th>amount</th><th>durability</th><th>insurance</th><th>itemId</th></tr></thead><tbody>"); sb.Append("<table><thead><tr><th>Nom</th><th>Rareté</th><th>baseItemId</th><th>qté</th><th>durab.</th><th>assurance</th></tr></thead><tbody>");
foreach (var it in doc.RootElement.EnumerateArray()) foreach (var it in doc.RootElement.EnumerateArray())
{ {
string S(string k) => it.TryGetProperty(k, out var v) ? (v.ValueKind == JsonValueKind.Number ? v.GetRawText() : v.ToString()) : ""; string S(string k) => it.TryGetProperty(k, out var v) ? (v.ValueKind == JsonValueKind.Number ? v.GetRawText() : v.ToString()) : "";
sb.Append($"<tr><td>{Esc(S("baseItemId"))}</td><td>{Esc(S("amount"))}</td><td>{Esc(S("durability"))}</td>" + var baseId = S("baseItemId");
$"<td>{Esc(S("insurance"))}</td><td class='mono small'>{Esc(S("itemId"))}</td></tr>"); var entry = _gameRef.Get(baseId);
var name = entry?.Name ?? baseId;
sb.Append($"<tr><td>{Esc(name)}</td><td>{Rarity(entry?.Rarity)}</td><td class='mono small'>{Esc(baseId)}</td>" +
$"<td>{Esc(S("amount"))}</td><td>{Esc(S("durability"))}</td><td>{Esc(S("insurance"))}</td></tr>");
} }
sb.Append("</tbody></table>"); sb.Append("</tbody></table>");
return sb.ToString(); return sb.ToString();
@@ -295,6 +333,22 @@ public class AdminController : ControllerBase
catch { return $"<pre>{Esc(Pretty(json))}</pre>"; } catch { return $"<pre>{Esc(Pretty(json))}</pre>"; }
} }
// Small coloured rarity chip.
private static string Rarity(string? r)
{
if (string.IsNullOrEmpty(r)) return "";
var color = r.ToLowerInvariant() switch
{
"common" => "#9aa4b2",
"uncommon" => "#4caf50",
"rare" => "#2f81f7",
"epic" => "#a371f7",
"legendary" or "exotic" => "#e8a838",
_ => "#9aa4b2",
};
return $"<span style='color:{color};font-weight:600'>{Esc(r)}</span>";
}
private static string Card(string label, string value) => private static string Card(string label, string value) =>
$"<div class='card'><div class='v'>{Esc(value)}</div><div class='l'>{Esc(label)}</div></div>"; $"<div class='card'><div class='v'>{Esc(value)}</div><div class='l'>{Esc(label)}</div></div>";
@@ -322,7 +376,7 @@ details{{background:var(--panel);border:1px solid var(--line);border-radius:8px;
pre{{margin:0;padding:12px;background:#0b0d11;border-top:1px solid var(--line);overflow:auto;max-height:480px;white-space:pre-wrap;word-break:break-word}} pre{{margin:0;padding:12px;background:#0b0d11;border-top:1px solid var(--line);overflow:auto;max-height:480px;white-space:pre-wrap;word-break:break-word}}
</style></head><body> </style></head><body>
<header><b>The Cycle · Admin</b> <header><b>The Cycle · Admin</b>
<a href='/admin'>Dashboard</a><a href='/admin/players'>Joueurs</a><a href='/admin/titledata'>TitleData</a> <a href='/admin'>Dashboard</a><a href='/admin/players'>Joueurs</a><a href='/admin/catalog'>Catalogue</a><a href='/admin/titledata'>TitleData</a>
<span class='muted' style='margin-left:auto'>read-only · LAN</span></header> <span class='muted' style='margin-left:auto'>read-only · LAN</span></header>
<main>{body}</main></body></html>"; <main>{body}</main></body></html>";
} }
File diff suppressed because one or more lines are too long
@@ -34,4 +34,10 @@
<ProjectReference Include="..\Prospect.Steam\Prospect.Steam.csproj" /> <ProjectReference Include="..\Prospect.Steam\Prospect.Steam.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<!-- Community-datamined id -> name/rarity/category lookup (TCF-Wiki), used by the
admin back-office to show readable item/vanity names instead of raw ids. -->
<Content Update="Data\gameref.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,49 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Prospect.Server.Api.Services.GameRef;
// One reference entry: readable name, rarity, category. Sourced from community-datamined
// data (TCF-Wiki), bundled as Data/gameref.json. Compact keys n/r/c to keep the file small.
public class GameRefEntry
{
[JsonPropertyName("n")] public string Name { get; set; } = "";
[JsonPropertyName("r")] public string Rarity { get; set; } = "";
[JsonPropertyName("c")] public string Category { get; set; } = "";
}
// Resolves internal item / vanity ids to readable names for the admin back-office.
public class GameRefService
{
private readonly Dictionary<string, GameRefEntry> _ref = new(StringComparer.OrdinalIgnoreCase);
public GameRefService(ILogger<GameRefService> logger)
{
try
{
var path = Path.Combine(AppContext.BaseDirectory, "Data", "gameref.json");
if (File.Exists(path))
{
var data = JsonSerializer.Deserialize<Dictionary<string, GameRefEntry>>(File.ReadAllText(path));
if (data != null)
{
foreach (var kv in data)
{
_ref[kv.Key] = kv.Value;
}
}
}
logger.LogInformation("GameRef loaded {Count} entries", _ref.Count);
}
catch (Exception e)
{
logger.LogWarning(e, "GameRef load failed");
}
}
public GameRefEntry? Get(string? id) => id != null && _ref.TryGetValue(id, out var e) ? e : null;
public string Name(string? id) => Get(id)?.Name ?? id ?? "";
public IReadOnlyDictionary<string, GameRefEntry> All => _ref;
}
+1
View File
@@ -31,6 +31,7 @@ public class Startup
services.AddSingleton<TitleDataService>(); services.AddSingleton<TitleDataService>();
services.AddSingleton<Prospect.Server.Api.Services.Steam.SteamWebApiService>(); services.AddSingleton<Prospect.Server.Api.Services.Steam.SteamWebApiService>();
services.AddSingleton<Prospect.Server.Api.Services.GameRef.GameRefService>();
services.AddSingleton<DbUserService>(); services.AddSingleton<DbUserService>();
services.AddSingleton<DbEntityService>(); services.AddSingleton<DbEntityService>();