Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+485
View File
@@ -0,0 +1,485 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Storefront;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Avatar;
using RadicadeServer.RecNet.Config;
using RadicadeServer.RecNet.Keepsakes;
using RadicadeServer.RecNet.Rooms;
using RadicadeServer.Utils;
namespace RadicadeServer.API.Compatibility;
[ApiController]
public sealed class CompatibilityAPI(RecDBContext db): ControllerBase
{
[HttpGet("/api/progressionEvents/active")]
public IActionResult ActiveProgressionEvents()
{
if (!KeepsakeFeature.Enabled) return NoActiveEvent();
var keepsakeRooms = KeepsakeRoomConfig.Current;
if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled) return NoActiveEvent();
if (string.Equals(Environment.GetEnvironmentVariable("RADICADE_PROGRESSION_ACTIVE"), "record", StringComparison.OrdinalIgnoreCase))
return Ok(EventRecord(keepsakeRooms));
return Ok(keepsakeRooms.EventId);
}
private ContentResult NoActiveEvent()
{
return Content("null", "application/json");
}
[HttpGet("/api/progressionEvents/event/{eventId:long}")]
public IActionResult ProgressionEventDefinition(long eventId) => EventById(eventId, "event");
[HttpGet("/api/progressionEvents/record/{eventId:long}")]
public IActionResult ProgressionEventRecord(long eventId) => EventById(eventId, "record");
[HttpGet("/api/progressionEvents/{eventId:long}")]
public IActionResult ProgressionEvent(long eventId) => EventById(eventId, "event");
private IActionResult EventById(long eventId, string route)
{
if (!KeepsakeFeature.Enabled) return NotFound();
var keepsakeRooms = KeepsakeRoomConfig.Current;
if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled || eventId != keepsakeRooms.EventId) return NotFound();
return route == "record"
? Ok(EventRecord(keepsakeRooms, includeDefinitionCopy: true))
: Ok(EventDefinition(keepsakeRooms));
}
private object EventRecord(KeepsakeRoomConfig keepsakeRooms)
=> EventRecord(keepsakeRooms, includeDefinitionCopy: false);
private object EventRecord(KeepsakeRoomConfig keepsakeRooms, bool includeDefinitionCopy)
{
if (!includeDefinitionCopy) return EventDefinition(keepsakeRooms);
var accountId = HttpContext?.User?.GetUserId() ?? 0;
var categories = db == null ? new List<int>() : db.KeepsakeCollectionRecords
.Where(r => r.AccountId == accountId).Select(r => r.KeepsakeCategoryId).ToList();
return new
{
_route = "record",
AccountId = accountId,
Xp = categories.Sum(id => KeepsakeCatalog.Find(id)?.XpValue ?? 0),
RewardsCollected = 0,
BonusRewardsCollected = 0,
GameMinutesToday = 0,
XpBoostLastPurchasedAt = (DateTime?)null,
};
}
private object EventDefinition(KeepsakeRoomConfig keepsakeRooms)
{
var now = DateTime.UtcNow;
var end = now.AddYears(1);
return new
{
_route = "event",
ProgressionEventId = keepsakeRooms.EventId,
Name = keepsakeRooms.EventName,
IsEnabled = true,
StartTime = now.AddDays(-30),
EndTime = end,
CollectionEndTime = end,
Rewards = Array.Empty<object>(),
KeepsakeRoomLists = EventKeepsakeRoomLists(keepsakeRooms),
};
}
private static string KeepsakeListMode()
=> string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase)
? "no"
: "all";
private object EventKeepsakeRoomLists(KeepsakeRoomConfig keepsakeRooms)
=> string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase)
? Array.Empty<object>()
: keepsakeRooms.RoomListEntries(keepsakeRooms.AllRooms && db != null
? db.Rooms.Select(r => r.RoomId).OrderBy(id => id).ToArray()
: keepsakeRooms.ConfiguredRooms);
[HttpGet("/commerce/purchasecampaign/allcurrent/v2")]
public IActionResult CurrentPurchaseCampaigns() => Ok(Array.Empty<object>());
[HttpGet("/api/apple/musicpromotion/active")]
public IActionResult AppleMusicPromotionActive() => Ok(false);
[HttpGet("/accounts/emojiConfig/whitelistedEmojis")]
public IActionResult WhitelistedEmojis() => Ok(ProfileEmojis);
private static readonly string[] ProfileEmojis =
[
"😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "🙂",
"🙃", "😉", "😍", "🥰", "😘", "😎", "🤓", "🤩", "🥳", "🤔",
"😴", "😭", "😱", "😇", "🤠", "🤖", "👻", "👽", "👾", "💀",
"😺", "😸", "😹", "😻", "🙈", "🙉", "🙊", "👋", "👍", "👏",
"🙌", "💪", "🤝", "🤘", "🤙", "✌️", "❤️", "🧡", "💛", "💚",
"💙", "💜", "🖤", "🤍", "🤎", "💕", "💖", "💯", "✨", "⭐",
"🌟", "🔥", "🌈", "☀️", "🌙", "⚡", "❄️", "🌊", "🍀", "🌻",
"🌸", "🌹", "🌵", "🌲", "🐶", "🐱", "🐭", "🐰", "🦊", "🐻",
"🐼", "🐸", "🐵", "🦁", "🐯", "🐨", "🐷", "🐧", "🐢", "🐙",
"🦋", "🐝", "🦄", "🐲", "🍎", "🍉", "🍓", "🍒", "🍍", "🥑",
"🍕", "🍔", "🍟", "🌮", "🍿", "🍩", "🍪", "🎂", "🍦", "☕",
"⚽", "🏀", "🏈", "⚾", "🎾", "🎯", "🎮", "🎲", "🏆", "🎨",
"🎭", "🎵", "🎸", "🥁", "🎤", "🎧", "🚀", "🛸", "✈️", "🚗",
"🚲", "⛵", "🏠", "🏰", "🏝️", "🎡", "💎", "👑", "🎁", "🎈",
"🎉", "🎊", "💡", "📚", "📷", "🔑", "🧩", "🧸", "🏁", "🏳️‍🌈"
];
[HttpGet("/commerce/reminder/currentTokenBundles/v2")]
public IActionResult CurrentTokenBundles() => Ok(Array.Empty<object>());
[HttpPost("/econ/api/freegifts/v1/sendmultiple")]
[HttpPost("/api/freegifts/v1/sendmultiple")]
[Authorize]
public async Task<IActionResult> SendFreeGifts()
{
var senderId = User.GetUserId();
if (senderId is not > 0) return Unauthorized();
var request = await ReadFreeGiftRequestAsync();
var recipients = request.Recipients.Where(id => id > 0).Distinct().ToList();
if (recipients.Count == 0)
return FreeGiftResult(false, "No recipients were given.");
if (recipients.Count > FriendotronService.MaxRecipients)
return FreeGiftResult(false, $"A friend box can be sent to {FriendotronService.MaxRecipients} players at a time.");
if (recipients.Contains(senderId.Value) && !await SelfGiftingAllowedAsync())
return FreeGiftResult(false, "You cannot send a friend box to yourself.");
var known = await db.Players.AsNoTracking()
.Where(player => recipients.Contains(player.AccountId))
.Select(player => player.AccountId)
.ToListAsync();
var missing = recipients.FirstOrDefault(id => !known.Contains(id));
if (missing != 0)
return FreeGiftResult(false, $"Player {missing} does not exist.");
var lastSent = await LastFriendBoxAsync(senderId.Value);
if (lastSent != null && DateTime.UtcNow - lastSent.Value < FriendotronService.Cooldown)
return FreeGiftResult(false,
$"You can send another friend box in {FriendotronService.FormatRemaining(DateTime.UtcNow, lastSent.Value)}.");
var boxes = new List<(long Recipient, Gift Box)>();
foreach (var recipient in recipients)
{
var item = FriendotronService.RollConsumable(DBConfig.DefaultConsumableItems, out var starTier);
if (item == null)
return FreeGiftResult(false, "No consumable is configured for the friend box.");
boxes.Add((recipient, FriendotronService.BuildBox(senderId.Value, request.Message, item, starTier)));
}
await db.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
{
await using var transaction = db.Database.IsRelational() ? await db.Database.BeginTransactionAsync() : null;
foreach (var (recipient, box) in boxes)
await StorefrontShared.PersistGiftBoxAsync(db, recipient, box, notify: false);
await StampFriendBoxAsync(senderId.Value);
if (transaction != null) await transaction.CommitAsync();
});
foreach (var (recipient, box) in boxes)
await StorefrontShared.SendGiftPackageNotificationAsync(recipient, box);
return FreeGiftResult(true, string.Empty);
}
private static OkObjectResult FreeGiftResult(bool success, string error) => new(new Dictionary<string, object?>
{
["ALOCDGMHFKA"] = success,
["FBILGKFKFJE"] = error
});
private sealed class FreeGiftRequest
{
public List<long> Recipients { get; } = new();
public string? Message { get; set; }
}
private async Task<FreeGiftRequest> ReadFreeGiftRequestAsync()
{
var request = new FreeGiftRequest();
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" })
foreach (var value in form[key]) AddRecipients(request, value);
request.Message = FirstFilled(form["Message"], form["message"]);
return request;
}
string raw;
using (var reader = new StreamReader(Request.Body))
raw = await reader.ReadToEndAsync();
if (!string.IsNullOrWhiteSpace(raw))
{
try
{
if (JsonNode.Parse(raw) is JsonObject body)
{
foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" })
{
var node = body[key];
if (node is JsonArray array)
foreach (var entry in array) AddRecipients(request, entry?.ToString());
else if (node != null)
AddRecipients(request, node.ToString());
}
request.Message = FirstFilled(Str(body, "Message"), Str(body, "message"));
}
}
catch (JsonException)
{
}
}
foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds" })
foreach (var value in Request.Query[key]) AddRecipients(request, value);
request.Message ??= FirstFilled(Request.Query["Message"], Request.Query["message"]);
return request;
}
private static void AddRecipients(FreeGiftRequest request, string? value)
{
if (string.IsNullOrWhiteSpace(value)) return;
foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
if (long.TryParse(part, out var id))
request.Recipients.Add(id);
}
private static string? FirstFilled(params string?[] values) =>
values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
private async Task<bool> SelfGiftingAllowedAsync()
{
var row = await db.GameConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "Friendotron_AllowSelfGifting");
if (row == null || string.IsNullOrWhiteSpace(row.Value)) return true;
return !bool.TryParse(row.Value.Trim(), out var allowed) || allowed;
}
private async Task<DateTime?> LastFriendBoxAsync(long accountId)
{
var key = FriendotronService.CooldownKey(accountId);
var row = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == key);
if (row == null) return null;
return DateTime.TryParse(row.Value, CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var stamp)
? stamp
: null;
}
private async Task StampFriendBoxAsync(long accountId)
{
var key = FriendotronService.CooldownKey(accountId);
var value = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture);
var row = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == key);
if (row == null) db.ServerConfig.Add(new ServerConfigVal(key, value));
else row.Value = value;
await db.SaveChangesAsync();
}
[HttpPut("/gamelogs/reports/{reportId}/userinfo")]
public IActionResult ReportUserInfo(string reportId) => Ok();
[HttpDelete("/auth/cachedlogin/current"), Authorize]
public async Task<IActionResult> DeleteCurrentCachedLogin()
{
var accountId = User.GetUserId();
var platformId = User.GetPlatId();
var rawPlatform = User.FindFirst("rn.plat")?.Value;
if (accountId is not > 0 || string.IsNullOrWhiteSpace(platformId) ||
!int.TryParse(rawPlatform, out var platformValue) ||
!Enum.IsDefined(typeof(RadicadeServer.RecNet.Enums.Players.Platforms), platformValue) ||
platformValue == (int)RadicadeServer.RecNet.Enums.Players.Platforms.All)
{
return Ok();
}
var cachedLogins = await db.CachedLogins
.Where(login => login.accountId == accountId.Value &&
login.platform == (RadicadeServer.RecNet.Enums.Players.Platforms)platformValue &&
login.platformId == platformId)
.ToListAsync();
if (cachedLogins.Count > 0)
{
db.CachedLogins.RemoveRange(cachedLogins);
await db.SaveChangesAsync();
}
return Ok();
}
[HttpPost("/roomcomments/comments/create/{roomId:long}"), Authorize]
public async Task<IActionResult> CreateRoomComment(long roomId)
{
var authorId = User.GetUserId();
if (authorId == null) return Unauthorized(new { success = false, error = "Unauthorized" });
var body = await ReadCommentBodyAsync();
var text = body.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return BadRequest(new { success = false, error = "Comment is required" });
}
var comment = new RoomComment
{
RoomId = roomId,
CreatorAccountId = authorId.Value,
Comment = text,
CommentType = string.IsNullOrWhiteSpace(body.Type) ? "Feedback" : body.Type!.Trim(),
PositionX = body.X,
PositionY = body.Y,
PositionZ = body.Z,
CreatedAt = DateTime.UtcNow
};
await db.RoomComments.AddAsync(comment);
await db.SaveChangesAsync();
var roomExists = await db.Rooms.AnyAsync(x => x.RoomId == roomId);
return Ok(new { success = true, message = string.Empty, comment_id = comment.Id });
}
private sealed class CommentBody
{
public string? Text { get; set; }
public string? Type { get; set; }
public float? X { get; set; }
public float? Y { get; set; }
public float? Z { get; set; }
}
private async Task<CommentBody> ReadCommentBodyAsync()
{
var result = new CommentBody();
var query = Request.Query;
result.Text = FirstNonEmpty(query["comment"], query["Comment"], query["commentText"], query["CommentText"],
query["text"], query["body"]);
result.Type = FirstNonEmpty(query["commentType"], query["CommentType"], query["type"]);
result.X = TryFloat(query["x"], query["positionX"], query["PositionX"]);
result.Y = TryFloat(query["y"], query["positionY"], query["PositionY"]);
result.Z = TryFloat(query["z"], query["positionZ"], query["PositionZ"]);
var formKeys = string.Empty;
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
formKeys = string.Join(",", form.Keys);
result.Text = FirstNonEmpty(result.Text,
FirstNonEmpty(form["comment"], form["Comment"], form["commentText"], form["text"], form["body"],
form["message"], form["Message"], form["content"], form["Content"]));
result.Type = FirstNonEmpty(result.Type,
FirstNonEmpty(form["commentType"], form["CommentType"], form["type"]));
result.X ??= TryFloat(FirstNonEmpty(form["x"], form["positionX"], form["PositionX"]));
result.Y ??= TryFloat(FirstNonEmpty(form["y"], form["positionY"], form["PositionY"]));
result.Z ??= TryFloat(FirstNonEmpty(form["z"], form["positionZ"], form["PositionZ"]));
}
var raw = string.Empty;
if (!Request.HasFormContentType && string.IsNullOrWhiteSpace(result.Text))
{
using var reader = new StreamReader(Request.Body);
raw = await reader.ReadToEndAsync();
}
if (string.IsNullOrWhiteSpace(raw)) return result;
try
{
var node = JsonNode.Parse(raw);
if (node is JsonValue value && value.TryGetValue<string>(out var bare))
{
result.Text = FirstNonEmpty(result.Text, bare);
return result;
}
if (node is not JsonObject obj) return result;
result.Text = FirstNonEmpty(result.Text, FirstNonEmpty(
Str(obj, "Comment"), Str(obj, "comment"),
Str(obj, "CommentText"), Str(obj, "commentText"),
Str(obj, "Text"), Str(obj, "text"),
Str(obj, "Message"), Str(obj, "message"),
Str(obj, "Content"), Str(obj, "content")));
result.Type = FirstNonEmpty(result.Type,
FirstNonEmpty(Str(obj, "CommentType"), Str(obj, "commentType"), Str(obj, "Type"), Str(obj, "type")));
var position = obj["Position"] ?? obj["position"];
if (position is JsonObject pos)
{
result.X ??= TryFloat(Str(pos, "x"), Str(pos, "X"));
result.Y ??= TryFloat(Str(pos, "y"), Str(pos, "Y"));
result.Z ??= TryFloat(Str(pos, "z"), Str(pos, "Z"));
}
else
{
result.X ??= TryFloat(Str(obj, "x"), Str(obj, "positionX"));
result.Y ??= TryFloat(Str(obj, "y"), Str(obj, "positionY"));
result.Z ??= TryFloat(Str(obj, "z"), Str(obj, "positionZ"));
}
}
catch (JsonException)
{
result.Text = raw.Trim();
}
return result;
}
private static string? Str(JsonObject obj, string key)
{
var node = obj[key];
if (node == null) return null;
try
{
if (node is JsonValue value && value.TryGetValue<string>(out var text)) return text;
return node.ToJsonString().Trim('"');
}
catch
{
return null;
}
}
private static string? FirstNonEmpty(params string?[] values)
=> values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v));
private static float? TryFloat(params string?[] values)
{
foreach (var value in values)
if (float.TryParse(value, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var parsed))
return parsed;
return null;
}
}