diff --git a/DorkNet.Server/Controllers/API/Sanitize/SanitizeController.cs b/DorkNet.Server/Controllers/API/Sanitize/SanitizeController.cs index 928eaf4..6289c1a 100644 --- a/DorkNet.Server/Controllers/API/Sanitize/SanitizeController.cs +++ b/DorkNet.Server/Controllers/API/Sanitize/SanitizeController.cs @@ -115,4 +115,18 @@ public class SanitizeController(ILogger logger) : Controller ?? textForm ?? textQuery ?? string.Empty; return Ok(ProfanityFilter.IsClean(input)); } + + /// POST api/sanitize/isPure — December 2020 route + /// used by room/settings forms. Its importer reads an IsPure + /// object field, unlike the newer bare-bool route above. + [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) }); + } } diff --git a/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs b/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs index a362247..d6d6851 100644 --- a/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs +++ b/DorkNet.Server/Controllers/API/Store/StorefrontsBuyController.cs @@ -103,7 +103,7 @@ public class StorefrontsBuyController( StoreService.TryGetAvatarItemPayload(item.Slug, out var avatarItemType, out avatarItemDesc); var inventoryAvatarDesc = StoreService.InventoryAvatarItemDesc(avatarItemDesc); 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)) { avatarItemType = 1; @@ -352,7 +352,7 @@ public class StorefrontsBuyController( { StoreService.TryGetAvatarItemPayload(grantedSlug, out avatarItemType, out avatarItemDesc); 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)) { avatarItemType = 1; diff --git a/DorkNet.Server/Data/DorkNetDbContext.cs b/DorkNet.Server/Data/DorkNetDbContext.cs index 62ad6ed..0a22672 100644 --- a/DorkNet.Server/Data/DorkNetDbContext.cs +++ b/DorkNet.Server/Data/DorkNetDbContext.cs @@ -338,6 +338,7 @@ public class DorkNetDbContext(DbContextOptions options) : DbCo modelBuilder.Entity(e => { e.HasKey(p => p.Id); + e.Property(p => p.ItemSlug).HasMaxLength(128); // One row per (player, item) pair. e.HasIndex(p => new { p.PlayerId, p.ItemSlug }).IsUnique(); e.HasIndex(p => p.PlayerId); diff --git a/DorkNet.Server/Data/Entities/PlayerInventoryEntity.cs b/DorkNet.Server/Data/Entities/PlayerInventoryEntity.cs index 8ccf06d..1a5b890 100644 --- a/DorkNet.Server/Data/Entities/PlayerInventoryEntity.cs +++ b/DorkNet.Server/Data/Entities/PlayerInventoryEntity.cs @@ -23,7 +23,7 @@ public class PlayerInventoryEntity /// 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, /// legacy retired items, etc.). - [MaxLength(64)] + [MaxLength(128)] public string ItemSlug { get; set; } = string.Empty; public int Quantity { get; set; } = 1; diff --git a/DorkNet.Server/Migrations/20260525181055_Initial.Designer.cs b/DorkNet.Server/Migrations/20260525181055_Initial.Designer.cs index ade6c3e..687d005 100644 --- a/DorkNet.Server/Migrations/20260525181055_Initial.Designer.cs +++ b/DorkNet.Server/Migrations/20260525181055_Initial.Designer.cs @@ -1669,7 +1669,7 @@ namespace DorkNet.Server.Migrations b.Property("ItemSlug") .IsRequired() - .HasMaxLength(64) + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("PlayerId") diff --git a/DorkNet.Server/Migrations/20260525181055_Initial.cs b/DorkNet.Server/Migrations/20260525181055_Initial.cs index 89b0225..f695470 100644 --- a/DorkNet.Server/Migrations/20260525181055_Initial.cs +++ b/DorkNet.Server/Migrations/20260525181055_Initial.cs @@ -732,7 +732,7 @@ namespace DorkNet.Server.Migrations Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), PlayerId = table.Column(type: "INTEGER", nullable: false), - ItemSlug = table.Column(type: "TEXT", maxLength: 64, nullable: false), + ItemSlug = table.Column(type: "TEXT", maxLength: 128, nullable: false), Quantity = table.Column(type: "INTEGER", nullable: false), IsActive = table.Column(type: "INTEGER", nullable: false), AcquiredAt = table.Column(type: "TEXT", nullable: false) diff --git a/DorkNet.Server/Migrations/DorkNetDbContextModelSnapshot.cs b/DorkNet.Server/Migrations/DorkNetDbContextModelSnapshot.cs index d598652..3591d34 100644 --- a/DorkNet.Server/Migrations/DorkNetDbContextModelSnapshot.cs +++ b/DorkNet.Server/Migrations/DorkNetDbContextModelSnapshot.cs @@ -1666,7 +1666,7 @@ namespace DorkNet.Server.Migrations b.Property("ItemSlug") .IsRequired() - .HasMaxLength(64) + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("PlayerId") diff --git a/DorkNet.Server/Services/StoreService.cs b/DorkNet.Server/Services/StoreService.cs index b2de155..94980fc 100644 --- a/DorkNet.Server/Services/StoreService.cs +++ b/DorkNet.Server/Services/StoreService.cs @@ -376,6 +376,40 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio out string modificationGuid) => 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 existingSlugs) { var catalog = _equipmentSkinCatalog.Value; @@ -942,7 +976,7 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio // here too. TryGetAvatarItemPayload(i.Slug, out var avatarItemType, out var avatarItemDesc); 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); if (isHairDye) { diff --git a/DorkNet.Server/Startup/DatabaseBootstrap.cs b/DorkNet.Server/Startup/DatabaseBootstrap.cs index 2642817..a62ed67 100644 --- a/DorkNet.Server/Startup/DatabaseBootstrap.cs +++ b/DorkNet.Server/Startup/DatabaseBootstrap.cs @@ -101,6 +101,7 @@ public static class DatabaseBootstrap // IF NOT EXISTS no-ops when EnsureCreated already built them). await EnsureSignupCodeTablesAsync(db); await EnsureServerSettingsColumnsAsync(db); + await EnsureInventorySchemaAsync(db); await EnsureLeaderboardSchemaAsync(db); // 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) { var provider = db.Database.ProviderName ?? string.Empty;