mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Keepsakes;
|
||||
|
||||
namespace RadicadeServer.API.Keepsakes;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/keepsakes")]
|
||||
public sealed class KeepsakesAPI : ControllerBase
|
||||
{
|
||||
private readonly RecDBContext? db;
|
||||
|
||||
public KeepsakesAPI(RecDBContext db) => this.db = db;
|
||||
|
||||
private long? CallerAccountId => ControllerContext?.HttpContext?.User?.GetUserId();
|
||||
|
||||
[HttpGet("globalconfig")]
|
||||
public IActionResult GlobalConfig()
|
||||
{
|
||||
var enabled = KeepsakeFeature.Enabled;
|
||||
return Ok(new
|
||||
{
|
||||
KeepsakeFeatureEnabled = enabled,
|
||||
KeepsakeRoomLimit = enabled ? KeepsakeRoomConfig.Current.RoomLimit : 0,
|
||||
SocialXpBoostEnabled = false
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("categories")]
|
||||
public IActionResult Categories()
|
||||
{
|
||||
var results = KeepsakeFeature.Enabled ? KeepsakeCatalog.Results() : Array.Empty<object>();
|
||||
return Ok(new { Results = results, TotalResults = results.Length });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId:long}")]
|
||||
public async Task<IActionResult> Room(long roomId)
|
||||
{
|
||||
if (db == null) return NoContent();
|
||||
|
||||
if (!KeepsakeFeature.Enabled) return NoContent();
|
||||
|
||||
var instances = await db.KeepsakeInstances
|
||||
.Where(i => i.RoomId == roomId && !i.Removed)
|
||||
.ToListAsync();
|
||||
|
||||
var keepsakeRooms = KeepsakeRoomConfig.Current;
|
||||
var isKeepsakeRoom = keepsakeRooms.IsKeepsakeRoom(roomId);
|
||||
if (instances.Count == 0 && !isKeepsakeRoom) return NoContent();
|
||||
|
||||
var accountId = CallerAccountId;
|
||||
var records = accountId == null
|
||||
? new List<KeepsakeCollectionRecord>()
|
||||
: await db.KeepsakeCollectionRecords
|
||||
.Where(r => r.AccountId == accountId && instances.Select(i => i.KeepsakeInstanceId).Contains(r.KeepsakeInstanceId))
|
||||
.ToListAsync();
|
||||
|
||||
var collected = records.Select(r => r.KeepsakeInstanceId).ToHashSet();
|
||||
|
||||
var uncollected = instances
|
||||
.Where(i => !collected.Contains(i.KeepsakeInstanceId))
|
||||
.Select(ToInstancePayload)
|
||||
.ToArray();
|
||||
var collectionRecords = records
|
||||
.Select(r => new
|
||||
{
|
||||
AccountId = r.AccountId,
|
||||
KeepsakeInstanceId = r.KeepsakeInstanceId,
|
||||
KeepsakeCategoryConfigId = r.KeepsakeCategoryId,
|
||||
CollectedAt = r.CollectedAt
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["Instances"] = instances.Select(ToInstancePayload).ToArray(),
|
||||
["KeepsakeInstances"] = uncollected,
|
||||
["Keepsakes"] = uncollected,
|
||||
["KeepsakeCollectionRecords"] = collectionRecords,
|
||||
["CollectionRecords"] = collectionRecords,
|
||||
["KeepsakeProgressionEventIds"] = isKeepsakeRoom && keepsakeRooms.EventEnabled ? new[] { keepsakeRooms.EventId } : Array.Empty<long>()
|
||||
};
|
||||
|
||||
if (isKeepsakeRoom) AddKeepsakeRoomFields(payload, roomId, keepsakeRooms, instances.Count);
|
||||
|
||||
return Ok(payload);
|
||||
}
|
||||
|
||||
private static void AddKeepsakeRoomFields(Dictionary<string, object?> payload, long roomId, KeepsakeRoomConfig keepsakeRooms, int placed)
|
||||
{
|
||||
var rooms = keepsakeRooms.AdvertisedRooms(roomId);
|
||||
|
||||
payload["KeepsakeRoomId"] = roomId;
|
||||
payload["KeepsakeRoomLimit"] = keepsakeRooms.RoomLimit;
|
||||
payload["KeepsakeRoomListId"] = keepsakeRooms.RoomListId;
|
||||
payload["KeepsakeRooms"] = rooms;
|
||||
payload["KeepsakeRoomLists"] = keepsakeRooms.RoomListEntries(rooms);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}")]
|
||||
public async Task<IActionResult> Place(long roomId)
|
||||
=> !KeepsakeFeature.Enabled ? NotFound() : await PlaceCore(roomId, await ReadRawBody());
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PlaceFromClient()
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled) return NotFound();
|
||||
var raw = await ReadRawBody();
|
||||
var roomId = ReadLong(ParseJson(raw), "RoomId");
|
||||
if (roomId is not > 0) return BadRequest(new { error = "RoomId is required." });
|
||||
var result = await PlaceCore(roomId.Value, raw);
|
||||
if (result is not OkObjectResult ok) return result;
|
||||
return Ok(JsonSerializer.SerializeToElement(ok.Value).GetProperty("KeepsakeInstanceId").GetGuid());
|
||||
}
|
||||
|
||||
private async Task<IActionResult> PlaceCore(long roomId, string raw)
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (db == null) return NoContent();
|
||||
|
||||
var accountId = CallerAccountId;
|
||||
if (accountId == null) return Unauthorized();
|
||||
var config = KeepsakeRoomConfig.Current;
|
||||
if (!config.IsKeepsakeRoom(roomId)) return BadRequest(new { error = "This room does not have keepsake permissions." });
|
||||
|
||||
var body = ParseJson(raw);
|
||||
var categoryId = ReadInt(body, "KeepsakeCategoryConfigId", "KeepsakeCategoryId", "KeepsakeCategory", "CategoryId");
|
||||
if (categoryId == null || KeepsakeCatalog.Find(categoryId.Value) == null)
|
||||
{
|
||||
return BadRequest(new { error = "Unknown or missing keepsake category." });
|
||||
}
|
||||
|
||||
var instance = new KeepsakeInstance
|
||||
{
|
||||
KeepsakeInstanceId = ReadGuid(body, "KeepsakeInstanceId") ?? Guid.NewGuid(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = ReadLong(body, "SubRoomId", "KeepsakeSubRoomId"),
|
||||
KeepsakeCategoryId = categoryId.Value,
|
||||
PlacedByAccountId = accountId.Value,
|
||||
PlacedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var existing = await db.KeepsakeInstances.FindAsync(instance.KeepsakeInstanceId);
|
||||
if (instance.KeepsakeInstanceId == Guid.Empty)
|
||||
return BadRequest(new { error = "A keepsake instance ID cannot be empty." });
|
||||
if (existing != null && (existing.RoomId != roomId || existing.PlacedByAccountId != accountId))
|
||||
return Conflict(new { error = "This keepsake instance belongs to another room or player." });
|
||||
var otherInstances = db.KeepsakeInstances.Where(i => i.RoomId == roomId && !i.Removed
|
||||
&& i.KeepsakeInstanceId != instance.KeepsakeInstanceId);
|
||||
if (await otherInstances.CountAsync() >= config.RoomLimit
|
||||
|| await otherInstances.CountAsync(i => i.KeepsakeCategoryId == categoryId.Value) >= KeepsakeCatalog.Find(categoryId.Value)!.LimitPerRoom)
|
||||
return BadRequest(new { error = "Cannot place additional keepsakes in this room." });
|
||||
if (existing == null) await db.KeepsakeInstances.AddAsync(instance);
|
||||
else
|
||||
{
|
||||
existing.RoomId = instance.RoomId;
|
||||
existing.SubRoomId = instance.SubRoomId;
|
||||
existing.KeepsakeCategoryId = instance.KeepsakeCategoryId;
|
||||
existing.Removed = false;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(ToInstancePayload(existing ?? instance));
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/collect")]
|
||||
public async Task<IActionResult> Collect(long roomId)
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (db == null) return NoContent();
|
||||
|
||||
var accountId = CallerAccountId;
|
||||
if (accountId == null) return Unauthorized();
|
||||
|
||||
var raw = await ReadRawBody();
|
||||
var body = ParseJson(raw);
|
||||
var wanted = ReadGuidList(body, "KeepsakeInstanceIds", "InstanceIds", "KeepsakeInstanceId");
|
||||
if (wanted.Count == 0) return BadRequest(new { error = "At least one keepsake instance ID is required." });
|
||||
var instances = await db.KeepsakeInstances
|
||||
.Where(i => i.RoomId == roomId && !i.Removed && wanted.Contains(i.KeepsakeInstanceId))
|
||||
.ToListAsync();
|
||||
|
||||
var mine = await db.KeepsakeCollectionRecords
|
||||
.Where(r => r.AccountId == accountId && wanted.Contains(r.KeepsakeInstanceId))
|
||||
.Select(r => r.KeepsakeInstanceId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var instance in instances.Where(i => !mine.Contains(i.KeepsakeInstanceId)))
|
||||
{
|
||||
await db.KeepsakeCollectionRecords.AddAsync(new KeepsakeCollectionRecord
|
||||
{
|
||||
AccountId = accountId.Value,
|
||||
KeepsakeInstanceId = instance.KeepsakeInstanceId,
|
||||
KeepsakeCategoryId = instance.KeepsakeCategoryId,
|
||||
CollectedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
KeepsakeRoomId = roomId,
|
||||
KeepsakeInstanceIds = instances.Select(i => i.KeepsakeInstanceId).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId:long}/{instanceId:guid}")]
|
||||
public async Task<IActionResult> Remove(long roomId, Guid instanceId)
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled) return NotFound();
|
||||
if (db == null) return NoContent();
|
||||
var accountId = CallerAccountId;
|
||||
if (accountId == null) return Unauthorized();
|
||||
var instance = await db.KeepsakeInstances.FindAsync(instanceId);
|
||||
if (instance == null || instance.RoomId != roomId) return NotFound();
|
||||
if (instance.PlacedByAccountId != accountId) return Forbid();
|
||||
instance.Removed = true;
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("events")]
|
||||
public IActionResult Events() => Ok(Array.Empty<object>());
|
||||
|
||||
[HttpGet("events/{eventId:long}")]
|
||||
public async Task<IActionResult> EventInstances(long eventId)
|
||||
{
|
||||
var config = KeepsakeRoomConfig.Current;
|
||||
if (!KeepsakeFeature.Enabled || !config.EventEnabled || eventId != config.EventId) return NotFound();
|
||||
if (db == null) return Ok(new { Instances = Array.Empty<object>(), CollectionRecords = Array.Empty<object>() });
|
||||
var instances = await db.KeepsakeInstances.Where(i => !i.Removed).ToListAsync();
|
||||
instances = instances.Where(i => config.IsKeepsakeRoom(i.RoomId)).ToList();
|
||||
var accountId = CallerAccountId;
|
||||
var collected = accountId == null ? new List<Guid>() : await db.KeepsakeCollectionRecords
|
||||
.Where(r => r.AccountId == accountId).Select(r => r.KeepsakeInstanceId).ToListAsync();
|
||||
object[] Groups(IEnumerable<KeepsakeInstance> rows) => rows.GroupBy(i => i.RoomId)
|
||||
.Select(g => (object)new { RoomId = g.Key, KeepsakeInstanceIds = g.Select(i => i.KeepsakeInstanceId).ToArray() }).ToArray();
|
||||
return Ok(new { Instances = Groups(instances), CollectionRecords = Groups(instances.Where(i => collected.Contains(i.KeepsakeInstanceId))) });
|
||||
}
|
||||
|
||||
[HttpDelete("{instanceId:guid}")]
|
||||
public async Task<IActionResult> RemoveFromClient(Guid instanceId)
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled) return NotFound();
|
||||
if (CallerAccountId == null) return Unauthorized();
|
||||
var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId);
|
||||
return instance == null ? NotFound() : await Remove(instance.RoomId, instanceId);
|
||||
}
|
||||
|
||||
[HttpPost("{instanceId:guid}/collect")]
|
||||
public async Task<IActionResult> CollectFromClient(Guid instanceId)
|
||||
{
|
||||
if (!KeepsakeFeature.Enabled) return CollectionNotFound(instanceId, "Keepsakes are disabled on this server.");
|
||||
var accountId = CallerAccountId;
|
||||
if (accountId == null) return Unauthorized();
|
||||
var instance = db == null ? null : await db.KeepsakeInstances.FindAsync(instanceId);
|
||||
if (instance == null) return CollectionNotFound(instanceId, "The keepsake instance is not registered on this server.");
|
||||
if (instance.Removed) return CollectionNotFound(instanceId, "The keepsake instance has been removed.");
|
||||
var newCollection = !await db!.KeepsakeCollectionRecords.AnyAsync(r => r.AccountId == accountId && r.KeepsakeInstanceId == instanceId);
|
||||
if (newCollection)
|
||||
{
|
||||
db.KeepsakeCollectionRecords.Add(new KeepsakeCollectionRecord
|
||||
{
|
||||
AccountId = accountId.Value, KeepsakeInstanceId = instanceId,
|
||||
KeepsakeCategoryId = instance.KeepsakeCategoryId, CollectedAt = DateTime.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
return Ok(new { TotalXp = newCollection ? KeepsakeCatalog.Find(instance.KeepsakeCategoryId)?.XpValue ?? 0 : 0, SocialBoostXp = 0 });
|
||||
}
|
||||
|
||||
private IActionResult CollectionNotFound(Guid instanceId, string reason)
|
||||
{
|
||||
Console.WriteLine($"[Keepsakes] collect instance {instanceId} REFUSED: {reason}");
|
||||
return Problem(statusCode: StatusCodes.Status404NotFound, title: "Keepsake collection failed", detail: reason);
|
||||
}
|
||||
|
||||
[HttpGet("{**rest}")]
|
||||
[HttpPost("{**rest}")]
|
||||
[HttpPut("{**rest}")]
|
||||
[HttpDelete("{**rest}")]
|
||||
[HttpPatch("{**rest}")]
|
||||
public async Task<IActionResult> Unclaimed(string? rest)
|
||||
{
|
||||
var body = await ReadRawBody();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private static object ToInstancePayload(KeepsakeInstance instance) => new
|
||||
{
|
||||
KeepsakeInstanceId = instance.KeepsakeInstanceId,
|
||||
KeepsakeCategoryConfigId = instance.KeepsakeCategoryId,
|
||||
PlacedByAccountId = instance.PlacedByAccountId,
|
||||
RoomId = instance.RoomId,
|
||||
SubRoomId = instance.SubRoomId,
|
||||
KeepsakeRoomId = instance.RoomId,
|
||||
PlacedAt = instance.PlacedAt
|
||||
};
|
||||
|
||||
private async Task<string> ReadRawBody()
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var raw = await reader.ReadToEndAsync();
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static string LogPreview(string raw) => raw.Length > 2048 ? raw[..2048] + "..." : raw;
|
||||
|
||||
private static JsonElement ParseJson(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return default;
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(raw);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryProp(JsonElement root, out JsonElement value, params string[] names)
|
||||
{
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in root.EnumerateObject())
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = property.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int? ReadInt(JsonElement root, params string[] names)
|
||||
{
|
||||
if (!TryProp(root, out var value, names)) return null;
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when value.TryGetInt32(out var number) => number,
|
||||
JsonValueKind.String when int.TryParse(value.GetString(), out var parsed) => parsed,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static long? ReadLong(JsonElement root, params string[] names)
|
||||
{
|
||||
if (!TryProp(root, out var value, names)) return null;
|
||||
if (value.ValueKind == JsonValueKind.Null) return null;
|
||||
return value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when value.TryGetInt64(out var number) => number,
|
||||
JsonValueKind.String when long.TryParse(value.GetString(), out var parsed) => parsed,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static Guid? ReadGuid(JsonElement root, params string[] names)
|
||||
{
|
||||
if (!TryProp(root, out var value, names)) return null;
|
||||
return value.ValueKind == JsonValueKind.String && Guid.TryParse(value.GetString(), out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static List<Guid> ReadGuidList(JsonElement root, params string[] names)
|
||||
{
|
||||
var found = new List<Guid>();
|
||||
if (!TryProp(root, out var value, names)) return found;
|
||||
switch (value.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Array:
|
||||
found.AddRange(value.EnumerateArray()
|
||||
.Where(item => item.ValueKind == JsonValueKind.String && Guid.TryParse(item.GetString(), out _))
|
||||
.Select(item => Guid.Parse(item.GetString()!)));
|
||||
break;
|
||||
case JsonValueKind.String when Guid.TryParse(value.GetString(), out var single):
|
||||
found.Add(single);
|
||||
break;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user