feat: enhance inventory schema to support longer ItemSlug and add GiftBox equipment payload handling

This commit is contained in:
Alexa
2026-06-06 22:50:42 +03:00
parent 92bd22d164
commit 73d77dffb6
9 changed files with 67 additions and 7 deletions
@@ -115,4 +115,18 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
?? textForm ?? textQuery ?? string.Empty; ?? textForm ?? textQuery ?? string.Empty;
return Ok(ProfanityFilter.IsClean(input)); return Ok(ProfanityFilter.IsClean(input));
} }
/// <summary>POST <c>api/sanitize/isPure</c> — December 2020 route
/// used by room/settings forms. Its importer reads an <c>IsPure</c>
/// object field, unlike the newer bare-bool route above.</summary>
[HttpPost("api/sanitize/isPure")]
public IActionResult IsPure(
[FromBody] SanitizeBody? body,
[FromForm(Name = "Text")] string? textForm,
[FromQuery(Name = "text")] string? textQuery)
{
var input = body?.Text ?? body?.Input ?? body?.Value
?? textForm ?? textQuery ?? string.Empty;
return Ok(new { IsPure = ProfanityFilter.IsClean(input) });
}
} }
@@ -103,7 +103,7 @@ public class StorefrontsBuyController(
StoreService.TryGetAvatarItemPayload(item.Slug, out var avatarItemType, out avatarItemDesc); StoreService.TryGetAvatarItemPayload(item.Slug, out var avatarItemType, out avatarItemDesc);
var inventoryAvatarDesc = StoreService.InventoryAvatarItemDesc(avatarItemDesc); var inventoryAvatarDesc = StoreService.InventoryAvatarItemDesc(avatarItemDesc);
StoreService.TryGetConsumableItemDesc(item.Slug, out var consumableItemDesc); StoreService.TryGetConsumableItemDesc(item.Slug, out var consumableItemDesc);
StoreService.TryGetEquipmentPayload(item.Slug, out var equipmentPrefabName, out var equipmentModificationGuid); StoreService.TryGetGiftBoxEquipmentPayload(item.Slug, out var equipmentPrefabName, out var equipmentModificationGuid);
if (StoreService.TryGetHairDyePayload(item.Slug, out var hairDyeConsumableDesc, out var hairDyeColorGuid)) if (StoreService.TryGetHairDyePayload(item.Slug, out var hairDyeConsumableDesc, out var hairDyeColorGuid))
{ {
avatarItemType = 1; avatarItemType = 1;
@@ -352,7 +352,7 @@ public class StorefrontsBuyController(
{ {
StoreService.TryGetAvatarItemPayload(grantedSlug, out avatarItemType, out avatarItemDesc); StoreService.TryGetAvatarItemPayload(grantedSlug, out avatarItemType, out avatarItemDesc);
StoreService.TryGetConsumableItemDesc(grantedSlug, out consumableItemDesc); StoreService.TryGetConsumableItemDesc(grantedSlug, out consumableItemDesc);
StoreService.TryGetEquipmentPayload(grantedSlug, out equipmentPrefabName, out equipmentModificationGuid); StoreService.TryGetGiftBoxEquipmentPayload(grantedSlug, out equipmentPrefabName, out equipmentModificationGuid);
if (StoreService.TryGetHairDyePayload(grantedSlug, out var hairDyeConsumableDesc, out var hairDyeColorGuid)) if (StoreService.TryGetHairDyePayload(grantedSlug, out var hairDyeConsumableDesc, out var hairDyeColorGuid))
{ {
avatarItemType = 1; avatarItemType = 1;
+1
View File
@@ -338,6 +338,7 @@ public class DorkNetDbContext(DbContextOptions<DorkNetDbContext> options) : DbCo
modelBuilder.Entity<PlayerInventoryEntity>(e => modelBuilder.Entity<PlayerInventoryEntity>(e =>
{ {
e.HasKey(p => p.Id); e.HasKey(p => p.Id);
e.Property(p => p.ItemSlug).HasMaxLength(128);
// One row per (player, item) pair. // One row per (player, item) pair.
e.HasIndex(p => new { p.PlayerId, p.ItemSlug }).IsUnique(); e.HasIndex(p => new { p.PlayerId, p.ItemSlug }).IsUnique();
e.HasIndex(p => p.PlayerId); e.HasIndex(p => p.PlayerId);
@@ -23,7 +23,7 @@ public class PlayerInventoryEntity
/// Stored as a string rather than an int FK because a player can /// Stored as a string rather than an int FK because a player can
/// be granted items that aren't in the live store (admin-gifted, /// be granted items that aren't in the live store (admin-gifted,
/// legacy retired items, etc.).</summary> /// legacy retired items, etc.).</summary>
[MaxLength(64)] [MaxLength(128)]
public string ItemSlug { get; set; } = string.Empty; public string ItemSlug { get; set; } = string.Empty;
public int Quantity { get; set; } = 1; public int Quantity { get; set; } = 1;
@@ -1669,7 +1669,7 @@ namespace DorkNet.Server.Migrations
b.Property<string>("ItemSlug") b.Property<string>("ItemSlug")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(128)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("PlayerId") b.Property<long>("PlayerId")
@@ -732,7 +732,7 @@ namespace DorkNet.Server.Migrations
Id = table.Column<long>(type: "INTEGER", nullable: false) Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
PlayerId = table.Column<long>(type: "INTEGER", nullable: false), PlayerId = table.Column<long>(type: "INTEGER", nullable: false),
ItemSlug = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false), ItemSlug = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
Quantity = table.Column<int>(type: "INTEGER", nullable: false), Quantity = table.Column<int>(type: "INTEGER", nullable: false),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false), IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
AcquiredAt = table.Column<DateTime>(type: "TEXT", nullable: false) AcquiredAt = table.Column<DateTime>(type: "TEXT", nullable: false)
@@ -1666,7 +1666,7 @@ namespace DorkNet.Server.Migrations
b.Property<string>("ItemSlug") b.Property<string>("ItemSlug")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(128)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<long>("PlayerId") b.Property<long>("PlayerId")
+35 -1
View File
@@ -376,6 +376,40 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
out string modificationGuid) => out string modificationGuid) =>
TryGetEquipmentPayload(slug, out prefabName, out modificationGuid, out _); TryGetEquipmentPayload(slug, out prefabName, out modificationGuid, out _);
public static bool TryGetGiftBoxEquipmentPayload(
string? slug,
out string prefabName,
out string modificationGuid,
out EquipmentSkinEntry? entry)
{
if (!TryGetEquipmentPayload(slug, out prefabName, out modificationGuid, out entry))
return false;
// The 2020 client previews GiftPackage equipment by spawning the
// prefab. Spawning [MakerPen] outside the real tool equip flow wakes
// MakerPen tool scripts without their visuals wired up, causing a
// repeating MakerPenVisuals null-ref/spin. Still grant the skin as
// inventory; just don't ask the gift-box/store-preview path to spawn it.
if (IsMakerPenPrefab(prefabName))
{
prefabName = string.Empty;
modificationGuid = string.Empty;
entry = null;
return false;
}
return true;
}
public static bool TryGetGiftBoxEquipmentPayload(
string? slug,
out string prefabName,
out string modificationGuid) =>
TryGetGiftBoxEquipmentPayload(slug, out prefabName, out modificationGuid, out _);
private static bool IsMakerPenPrefab(string? prefabName) =>
string.Equals(prefabName, "[MakerPen]", StringComparison.OrdinalIgnoreCase);
private async Task SeedEquipmentSkinsAsync(HashSet<string> existingSlugs) private async Task SeedEquipmentSkinsAsync(HashSet<string> existingSlugs)
{ {
var catalog = _equipmentSkinCatalog.Value; var catalog = _equipmentSkinCatalog.Value;
@@ -942,7 +976,7 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
// here too. // here too.
TryGetAvatarItemPayload(i.Slug, out var avatarItemType, out var avatarItemDesc); TryGetAvatarItemPayload(i.Slug, out var avatarItemType, out var avatarItemDesc);
TryGetConsumableItemDesc(i.Slug, out var consumableItemDesc); TryGetConsumableItemDesc(i.Slug, out var consumableItemDesc);
TryGetEquipmentPayload(i.Slug, out var equipmentPrefabName, out var equipmentModificationGuid, out var equipmentEntry); TryGetGiftBoxEquipmentPayload(i.Slug, out var equipmentPrefabName, out var equipmentModificationGuid, out var equipmentEntry);
var isHairDye = TryGetHairDyePayload(i.Slug, out var consumableHairDyeDesc, out var hairDyeColorGuid); var isHairDye = TryGetHairDyePayload(i.Slug, out var consumableHairDyeDesc, out var hairDyeColorGuid);
if (isHairDye) if (isHairDye)
{ {
@@ -101,6 +101,7 @@ public static class DatabaseBootstrap
// IF NOT EXISTS no-ops when EnsureCreated already built them). // IF NOT EXISTS no-ops when EnsureCreated already built them).
await EnsureSignupCodeTablesAsync(db); await EnsureSignupCodeTablesAsync(db);
await EnsureServerSettingsColumnsAsync(db); await EnsureServerSettingsColumnsAsync(db);
await EnsureInventorySchemaAsync(db);
await EnsureLeaderboardSchemaAsync(db); await EnsureLeaderboardSchemaAsync(db);
// Coach system account at Player.Id=1. The RR-Original room seeder // Coach system account at Player.Id=1. The RR-Original room seeder
@@ -303,6 +304,16 @@ public static class DatabaseBootstrap
} }
} }
private static async Task EnsureInventorySchemaAsync(DorkNetDbContext db)
{
var provider = db.Database.ProviderName ?? string.Empty;
if (provider.Contains("Npgsql", StringComparison.OrdinalIgnoreCase))
{
await db.Database.ExecuteSqlRawAsync(
@"ALTER TABLE ""PlayerInventory"" ALTER COLUMN ""ItemSlug"" TYPE character varying(128);");
}
}
private static async Task EnsureLeaderboardSchemaAsync(DorkNetDbContext db) private static async Task EnsureLeaderboardSchemaAsync(DorkNetDbContext db)
{ {
var provider = db.Database.ProviderName ?? string.Empty; var provider = db.Database.ProviderName ?? string.Empty;