feat: Implement "everyone owns all avatar items" toggle; update server settings, controller logic, and admin UI

This commit is contained in:
Alexa
2026-07-09 11:37:47 +03:00
parent 479ea273ea
commit e27b06ce5b
7 changed files with 205 additions and 4 deletions
@@ -62,7 +62,10 @@ namespace DorkNet.Server.Controllers.API.Avatar.V4;
/// Anonymous calls return [].
/// </summary>
[ApiController]
public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) : ControllerBase
public class AvatarItemsController(
DorkNetDbContext db,
IConfiguration config,
ServerSettingsService serverSettings) : ControllerBase
{
/// <summary>Master catalog keyed by GUID. Loaded once on first
/// request from <c>data/avatar_item_lookup.json</c>. Same
@@ -97,6 +100,15 @@ public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) :
var maxItems = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItems", 4000), 1, 8000);
var maxPerSlot = Math.Clamp(config.GetValue("Avatar:MaxUnlockedItemsPerSlot", 1000), 1, 2000);
// "Everyone owns all avatar items" admin toggle (mirrors the
// "everyone is friends" one): report the ENTIRE master catalog plus
// all permanent hair dyes as owned, for every caller, without reading
// or writing any inventory row. Flipping the toggle off instantly
// reverts to the real per-player wardrobe below. Bounded by the same
// maxItems ceiling so a future watch build can't be over-fed.
if (await serverSettings.IsAllAvatarItemsOwnedEnabledAsync())
return Ok(BuildEntireCatalogOwned(maxItems));
var pid = this.CurrentPlayerId();
if (pid is not long me) return Ok(new List<UnlockedAvatarItemDto>());
@@ -187,6 +199,39 @@ public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) :
return Ok(result);
}
/// <summary>The full owned set when the "everyone owns all avatar
/// items" toggle is on: every catalog item (already carrying the
/// "{guid},,," desc) plus every permanent hair dye, capped at
/// <paramref name="maxItems"/>. Hair dyes are emitted in the same
/// shape the per-player path uses (AvatarItemType.HairDye, desc =
/// hair-color GUID) so the wardrobe renders them identically.</summary>
private static List<UnlockedAvatarItemDto> BuildEntireCatalogOwned(int maxItems)
{
var catalog = _catalog.Value;
var result = new List<UnlockedAvatarItemDto>(Math.Min(maxItems, catalog.Items.Count + 32));
foreach (var item in catalog.Items.Values)
{
if (result.Count >= maxItems) return result;
result.Add(item);
}
foreach (var slug in StoreService.AllHairDyeSlugs)
{
if (result.Count >= maxItems) break;
if (!StoreService.TryGetHairDyePayload(slug, out var hairColorGuid)) continue;
StoreService.TryGetHairDyeName(slug, out var friendlyName);
result.Add(new UnlockedAvatarItemDto
{
AvatarItemType = AvatarItemType.HairDye,
AvatarItemDesc = hairColorGuid,
PlatformMask = unchecked((int)0xFFFFFFFF),
FriendlyName = friendlyName,
Tooltip = string.Empty,
Rarity = 0,
});
}
return result;
}
[HttpGet("api/avatar/v1/defaultbaseavataritems")]
[AllowAnonymous]
public IActionResult DefaultBaseAvatarItems()
@@ -230,6 +275,12 @@ public class AvatarItemsController(DorkNetDbContext db, IConfiguration config) :
var requested = RequestedAvatarItemGuids().ToList();
if (requested.Count == 0) return Ok(Array.Empty<object>());
// When everyone owns everything, nothing is "locked" — the store's
// owned/locked diff must see an empty locked set so every tile reads
// as owned, consistent with the full wardrobe returned by Get().
if (await serverSettings.IsAllAvatarItemsOwnedEnabledAsync())
return Ok(Array.Empty<object>());
var owned = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var pid = this.CurrentPlayerId();
if (pid is long me)
@@ -1498,6 +1498,7 @@ public class AdminController(
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.ProfanityFilterDisabled,
row.AllAvatarItemsOwned,
row.UpdatedAt,
});
}
@@ -1505,6 +1506,7 @@ public class AdminController(
public sealed record SignupsToggleRequest(bool Disabled);
public sealed record GlobalFriendsToggleRequest(bool Enabled);
public sealed record ProfanityFilterToggleRequest(bool Disabled);
public sealed record AllAvatarItemsToggleRequest(bool Enabled);
/// <summary>POST <c>api/admin/v1/settings/profanity</c> — flip the
/// server-side profanity filter. While disabled, every
@@ -1523,6 +1525,33 @@ public class AdminController(
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.ProfanityFilterDisabled,
row.AllAvatarItemsOwned,
row.UpdatedAt,
});
}
/// <summary>POST <c>api/admin/v1/settings/all-avatar-items</c> — flip the
/// "everyone owns all avatar items" toggle. When on, the wardrobe/store
/// "unlocked items" endpoints report the full master catalog (plus all
/// permanent hair dyes) as owned for every account. Nothing is written to
/// the inventory tables — it's synthesized at read time (see
/// <see cref="Controllers.API.Avatar.V4.AvatarItemsController"/>), so
/// flipping it off instantly reverts to each player's real wardrobe.
/// Players see the change the next time the watch pulls its unlocked
/// items (avatar menu open / next boot).</summary>
[HttpPost("settings/all-avatar-items")]
public async Task<ActionResult> SetAllAvatarItemsOwned([FromBody] AllAvatarItemsToggleRequest body)
{
var row = await serverSettings.SetAllAvatarItemsOwnedEnabledAsync(body.Enabled);
await LogAsync(body.Enabled ? "all_avatar_items_enabled" : "all_avatar_items_disabled", "system", 0, "");
await db.SaveChangesAsync();
return Ok(new
{
row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.ProfanityFilterDisabled,
row.AllAvatarItemsOwned,
row.UpdatedAt,
});
}
@@ -1636,6 +1665,8 @@ public class AdminController(
row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.ProfanityFilterDisabled,
row.AllAvatarItemsOwned,
row.UpdatedAt,
});
}
@@ -1657,6 +1688,8 @@ public class AdminController(
row.SignupsDisabled,
row.GlobalFriendsEnabled,
row.WeeklyChallengesCompletedRequired,
row.ProfanityFilterDisabled,
row.AllAvatarItemsOwned,
row.UpdatedAt,
});
}