using LiteDB; using Vanadium.Classes; using Vanadium.Classes.DBs.DBClasses; using static Vanadium.Classes.DBs.DBClasses.InventionDBClasses; namespace Vanadium.Classes.DBs { public class InventionDB { public static LiteDatabase InventionDBFile = new LiteDatabase("Filename=" + Path.Join(Program.dataDir, "DBs", "Inventions.db") + ";Connection=shared"); public static readonly ILiteCollection Inventions = InventionDBFile.GetCollection("Inventions"); public static readonly ILiteCollection Versions = InventionDBFile.GetCollection("Versions"); public static void Setup() { Inventions.EnsureIndex(x => x.InventionId); Inventions.EnsureIndex(x => x.CreatorPlayerId); Versions.EnsureIndex(x => x.InventionId); } public static object MapVersion(InventionVersion v) => new { v.InventionId, v.ReplicationId, v.VersionNumber, v.BlobName, v.BlobHash, v.InstantiationCost, v.LightsCost, v.ChipsCost, v.CloudVariablesCost, v.AICost, v.inkCost, CreatedAt = v.CreatedAt.ToString("O") }; public static object MapInventionFull(Invention inv) { var resolvedCurrentVersion = inv.CurrentVersion ?? (inv.Versions ?? new List()) .OrderByDescending(v => v.VersionNumber) .FirstOrDefault(); return new { inv.InventionId, inv.ReplicationId, inv.CreatorPlayerId, inv.Name, inv.Description, inv.LongDescription, inv.ImageName, inv.CurrentVersionNumber, inv.LatestVersionNumber, inv.UgcVersion, inv.ForceCannotPublish, CurrentVersion = resolvedCurrentVersion != null ? MapVersion(resolvedCurrentVersion) : (object?)null, Versions = (inv.Versions ?? new List()).Select(v => MapVersion(v)).ToList(), Accessibility = (int)inv.Accessibility, inv.IsPublished, inv.IsFeatured, inv.IsRecRoomApproved, inv.IsAGInvention, inv.IsCertifiedInvention, ModifiedAt = inv.ModifiedAt.ToString("O"), CreatedAt = inv.CreatedAt.ToString("O"), FirstPublishedAt = inv.FirstPublishedAt.HasValue && inv.FirstPublishedAt.Value != DateTime.MinValue ? inv.FirstPublishedAt.Value.ToString("O") : (string?)null, inv.CreationRoomId, inv.NumPlayersHaveUsedInRoom, inv.NumDownloads, inv.CheerCount, inv.CreatorPermission, inv.GeneralPermission, inv.Price, inv.AllowTrial, inv.HideFromPlayer, inv.DisplayMetadataJson, ReferencedInventions = inv.ReferencedInventions ?? new List(), Tags = inv.Tags ?? new List() }; } public static InventionData MapInventionToData(Invention inv) => new InventionData { AllowTrial = inv.AllowTrial, CheerCount = inv.CheerCount, CreatedAt = inv.CreatedAt.ToString("O"), CreatorPermission = inv.CreatorPermission, CreatorPlayerId = inv.CreatorPlayerId, CurrentVersionNumber = inv.CurrentVersionNumber, Description = inv.Description, GeneralPermission = inv.GeneralPermission, HideFromPlayer = inv.HideFromPlayer, ImageName = inv.ImageName, InventionId = inv.InventionId, IsAGInvention = inv.IsAGInvention, IsCertifiedInvention = inv.IsCertifiedInvention, IsPublished = inv.IsPublished, IsFeatured = inv.IsFeatured, IsRecRoomApproved = inv.IsRecRoomApproved, ModifiedAt = inv.ModifiedAt.ToString("O"), Name = inv.Name, NumDownloads = inv.NumDownloads, NumPlayersHaveUsedInRoom = inv.NumPlayersHaveUsedInRoom, Price = inv.Price, ReplicationId = inv.ReplicationId, LongDescription = inv.LongDescription, UgcVersion = inv.UgcVersion, ForceCannotPublish = inv.ForceCannotPublish, LatestVersionNumber = inv.LatestVersionNumber, Versions = inv.Versions, CurrentVersion = inv.CurrentVersion, Accessibility = (int)inv.Accessibility, FirstPublishedAt = inv.FirstPublishedAt, CreationRoomId = inv.CreationRoomId, DisplayMetadataJson = inv.DisplayMetadataJson, ReferencedInventions = inv.ReferencedInventions ?? new List(), Tags = inv.Tags ?? new List() }; public static Invention? GetInvention(long inventionId) { var invention = Inventions.FindById(inventionId); if (invention == null) return null; if (invention.CurrentVersion == null) { var fallback = Versions .Find(v => v.InventionId == inventionId) .OrderByDescending(v => v.VersionNumber) .FirstOrDefault(); if (fallback != null) { invention.CurrentVersion = fallback; invention.CurrentVersionNumber = fallback.VersionNumber; invention.LatestVersionNumber = fallback.VersionNumber; Inventions.Update(invention); } } return invention; } public static List GetInventionsBatch(List inventionIds) { var inventions = Inventions.Find(x => inventionIds.Contains(x.InventionId)).ToList(); foreach (var invention in inventions.Where(inv => inv.CurrentVersion == null)) { var fallback = Versions .Find(v => v.InventionId == invention.InventionId) .OrderByDescending(v => v.VersionNumber) .FirstOrDefault(); if (fallback != null) { invention.CurrentVersion = fallback; invention.CurrentVersionNumber = fallback.VersionNumber; invention.LatestVersionNumber = fallback.VersionNumber; Inventions.Update(invention); } } return inventions; } public static List GetInventionsByPlayer(long playerId) => Inventions.Find(x => x.DownloadIds.Contains(playerId)).ToList() .Select(inv => MapInventionToData(inv)).ToList(); public static List GetInventionsByCreator(long creatorId) => Inventions.Find(x => x.CreatorPlayerId == creatorId).ToList() .Select(inv => MapInventionFull(inv)).ToList(); public static List GetInventionsByRoom(long roomId) => Inventions.Find(x => x.CreationRoomId == roomId).ToList() .Select(inv => MapInventionFull(inv)).ToList(); public static List GetVersionsForInvention(long inventionId) => Versions.Find(v => v.InventionId == inventionId) .Select(v => MapVersion(v)).ToList(); public static InventionVersion? GetVersion(long inventionId, int versionNumber) => Versions.FindOne(x => x.InventionId == inventionId && x.VersionNumber == versionNumber); public static InventionVersion? GetLatestVersion(long inventionId) => Versions.Find(v => v.InventionId == inventionId) .OrderByDescending(v => v.VersionNumber) .FirstOrDefault(); public static long GetNextInventionId() { if (Inventions.Count() == 0) return 5012352472715840656; return Convert.ToInt64(Inventions.Max(x => x.InventionId)) + 1; } public static (Invention invention, InventionVersion version)? SaveInvention(long creatorPlayerId, SaveInventionRequest request) { try { long inventionId = GetNextInventionId(); var version = new InventionVersion { InventionId = inventionId, VersionNumber = 1, ReplicationId = Guid.NewGuid().ToString(), BlobName = request.inventionDataFilename ?? string.Empty, BlobHash = string.Empty, ChipsCost = request.chipsCost, CloudVariablesCost = request.cloudVariablesCost, InstantiationCost = request.instantiationCost, LightsCost = request.lightsCost, AICost = request.aiCost, inkCost = request.inkCost, CreatedAt = DateTime.UtcNow }; var invention = new Invention { InventionId = inventionId, ReplicationId = Guid.NewGuid().ToString(), CreatorPlayerId = creatorPlayerId, CreationRoomId = request.creationRoomId, Name = request.name ?? string.Empty, Description = request.description ?? string.Empty, ImageName = request.imageName ?? string.Empty, CurrentVersionNumber = 1, LatestVersionNumber = 1, CurrentVersion = version, Versions = new List { version }, Accessibility = RoomDBClasses.RoomAccessibility.Private, AllowTrial = true, HideFromPlayer = false, CreatorPermission = (int)InventionPermissions.Unlimited, GeneralPermission = (int)InventionPermissions.Unassigned, inkCost = 1, CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow, DownloadIds = new List { creatorPlayerId }, ReferencedInventions = request.referencedInventions ?? new List() }; Inventions.Insert(invention); Versions.Insert(version); return (invention, version); } catch (Exception ex) { Console.WriteLine($"[InventionDB] Error saving invention: {ex.Message}"); return null; } } public static (Invention invention, InventionVersion version)? AddVersion(Invention invention, AddVersionRequest request) { int nextVersion = invention.CurrentVersionNumber + 1; var version = new InventionVersion { InventionId = invention.InventionId, VersionNumber = nextVersion, BlobName = request.inventionDataFilename ?? string.Empty, ChipsCost = request.chipsCost, CloudVariablesCost = request.cloudVariablesCost, InstantiationCost = request.instantiationCost, LightsCost = request.lightsCost, AICost = request.aiCost, inkCost = request.inkCost, ReplicationId = Guid.NewGuid().ToString(), CreatedAt = DateTime.UtcNow }; invention.CurrentVersionNumber = nextVersion; invention.LatestVersionNumber = nextVersion; invention.CurrentVersion = version; invention.Versions.Add(version); invention.ModifiedAt = DateTime.UtcNow; Versions.Insert(version); Inventions.Update(invention); return (invention, version); } public static Invention? UpdateInvention(long inventionId, long playerId, string? name, string? description, string? imageName, int? price, bool? isPublished, bool? allowTrial, bool? hideFromPlayer, int? generalPermission, int? creatorPermission) { var invention = GetInvention(inventionId); if (invention == null || invention.CreatorPlayerId != playerId) return null; if (name != null) invention.Name = name; if (description != null) invention.Description = description; if (imageName != null) invention.ImageName = imageName; if (price.HasValue) invention.Price = price.Value; if (isPublished.HasValue) invention.IsPublished = isPublished.Value; if (allowTrial.HasValue) invention.AllowTrial = allowTrial.Value; if (hideFromPlayer.HasValue) invention.HideFromPlayer = hideFromPlayer.Value; if (generalPermission.HasValue) invention.GeneralPermission = generalPermission.Value; if (creatorPermission.HasValue) invention.CreatorPermission = creatorPermission.Value; invention.ModifiedAt = DateTime.UtcNow; Inventions.Update(invention); return invention; } public static Invention? PublishInvention(long inventionId, long playerId, int permissionLevel, int accessibility, int price) { var invention = GetInvention(inventionId); if (invention == null || invention.CreatorPlayerId != playerId) return null; invention.CreatorPermission = 255; invention.GeneralPermission = permissionLevel; invention.Accessibility = (RoomDBClasses.RoomAccessibility)accessibility; invention.Price = price; invention.IsPublished = true; invention.ModifiedAt = DateTime.UtcNow; Inventions.Update(invention); return invention; } public static Invention? UnpublishInvention(long inventionId, long playerId) { var invention = GetInvention(inventionId); if (invention == null || invention.CreatorPlayerId != playerId) return null; invention.IsPublished = false; invention.ModifiedAt = DateTime.UtcNow; Inventions.Update(invention); return invention; } public static List GetTopTodayInventions(bool featuredOnly, bool includeUnpublished = false) { var query = Inventions.FindAll().AsEnumerable(); if (featuredOnly) { query = query.Where(r => r.Tags.Any(t => string.Equals(t.Tag, "featured", StringComparison.OrdinalIgnoreCase) || r.IsFeatured)); } if (!includeUnpublished) query = query.Where(r => r.IsPublished); return query.OrderByDescending(r => r.CheerCount).ToList(); } public static (List Results, int Total) Search(string query, int skip = 0, int take = 30) { if (string.IsNullOrWhiteSpace(query)) return (new List(), 0); var segments = query.Split('|', StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList(); var candidatePool = Inventions.FindAll() .Where(i => i.IsPublished) .Where(i => !i.HideFromPlayer) .Where(i => i.Accessibility == RoomDBClasses.RoomAccessibility.Public) .ToList(); var allPlayers = PlayerDB.Players.FindAll().ToList(); var matched = new Dictionary(); foreach (var segment in segments) { var terms = segment.Split(new char[] { ' ', '+' }, StringSplitOptions.RemoveEmptyEntries); var nameParts = new List(); var tagParts = new List(); var authorParts = new List(); foreach (var term in terms) { if (term.StartsWith("#")) tagParts.Add(term.Substring(1).ToLowerInvariant().Trim()); else if (term.StartsWith("@")) authorParts.Add(term.Substring(1).ToLowerInvariant().Trim()); else nameParts.Add(term.ToLowerInvariant().Trim()); } long? resolvedAuthorId = null; if (authorParts.Count > 0) { var authorQuery = string.Join(" ", authorParts); var exactMatch = allPlayers.FirstOrDefault(p => string.Equals(p.Player?.Username, authorQuery, StringComparison.OrdinalIgnoreCase) || string.Equals(p.Player?.DisplayName, authorQuery, StringComparison.OrdinalIgnoreCase)); if (exactMatch != null) { resolvedAuthorId = exactMatch.PlayerId; } else { var fuzzyMatch = allPlayers .Where(p => p.Player != null) .Select(p => new { Player = p, Score = Math.Max( ScoreShit((p.Player!.Username ?? "").ToLowerInvariant(), authorQuery), ScoreShit((p.Player!.DisplayName ?? "").ToLowerInvariant(), authorQuery)) }) .Where(x => x.Score > 0) .OrderByDescending(x => x.Score) .FirstOrDefault(); if (fuzzyMatch != null) resolvedAuthorId = fuzzyMatch.Player.PlayerId; else continue; } } foreach (var invention in candidatePool) { if (resolvedAuthorId.HasValue && invention.CreatorPlayerId != resolvedAuthorId.Value) continue; if (tagParts.Count > 0) { var inventionTags = (invention.Tags ?? new List()) .Select(t => t.Tag.ToLowerInvariant().Trim()) .ToList(); bool allTagsMatch = tagParts.All(tp => { if (inventionTags.Contains(tp)) return true; foreach (var it in inventionTags) { if (it.Contains(tp) || tp.Contains(it)) return true; int dist = EditDistance(it, tp); int maxLen = Math.Max(it.Length, tp.Length); if (maxLen > 0 && (double)dist / maxLen <= 0.34) return true; } return false; }); if (!allTagsMatch) continue; } if (nameParts.Count > 0) { string inventionNameLower = (invention.Name ?? "").ToLowerInvariant(); bool nameMatch = nameParts.All(np => { if (inventionNameLower.Contains(np)) return true; int dist = EditDistance(inventionNameLower, np); int maxLen = Math.Max(inventionNameLower.Length, np.Length); if (maxLen > 0 && (double)dist / maxLen <= 0.34) return true; return false; }); if (!nameMatch) continue; } matched[invention.InventionId] = invention; } } var all = matched.Values .OrderByDescending(i => i.CheerCount) .ThenByDescending(i => i.NumDownloads) .ToList(); return (all.Skip(skip).Take(take).ToList(), all.Count); } public static (bool success, bool isCheering, int cheerCount)? CheerInvention(long inventionId, long playerId, bool cheer) { var invention = GetInvention(inventionId); if (invention == null) return null; var player = PlayerDB.Players.FindById(playerId); if (player?.Player?.PlayerExtra == null) return null; player.Player.PlayerExtra.PersonalDetails ??= new PlayerDBClasses.PersonalDetails(); player.Player.PlayerExtra.PersonalDetails.inventionsCheered ??= new List(); bool alreadyCheering = player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Contains(inventionId); if (cheer && !alreadyCheering) { invention.CheerCount++; player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Add(inventionId); player.Player.PlayerExtra.PersonalDetails.IsCheering = true; } else if (!cheer && alreadyCheering) { invention.CheerCount = Math.Max(0, invention.CheerCount - 1); player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Remove(inventionId); player.Player.PlayerExtra.PersonalDetails.IsCheering = false; } Inventions.Update(invention); PlayerDB.Players.Update(player); return (true, player.Player.PlayerExtra.PersonalDetails.inventionsCheered.Contains(inventionId), invention.CheerCount); } public static bool DeleteInvention(long inventionId, long playerId) { var invention = GetInvention(inventionId); if (invention == null || invention.CreatorPlayerId != playerId) return false; try { var cdnPath = Path.Join(Program.dataDir, "cdn", "inv"); if (Directory.Exists(cdnPath)) { foreach (var v in Versions.Find(v => v.InventionId == inventionId).ToList()) if (!string.IsNullOrWhiteSpace(v.ReplicationId)) foreach (var file in Directory.GetFiles(cdnPath, v.ReplicationId + "*")) try { File.Delete(file); } catch { } if (!string.IsNullOrWhiteSpace(invention.ReplicationId)) foreach (var file in Directory.GetFiles(cdnPath, invention.ReplicationId + "*")) try { File.Delete(file); } catch { } } Versions.DeleteMany(v => v.InventionId == inventionId); Inventions.Delete(inventionId); return true; } catch (Exception ex) { Console.WriteLine($"[InventionDB] Error deleting invention: {ex.Message}"); return false; } } public static SetTagsResponse SetTags(long inventionId, SetTagsRequest request) { try { var invention = GetInvention(inventionId); if (invention == null) return new SetTagsResponse { Result = -1 }; invention.Tags.Clear(); var allTags = new List(); foreach (var autoTag in request.AutoTags ?? new List()) { invention.Tags.Add(new InventionTag { Tag = autoTag, Type = 0 }); allTags.Add(autoTag); } foreach (var customTag in request.CustomTags ?? new List()) { invention.Tags.Add(new InventionTag { Tag = customTag, Type = 1 }); allTags.Add(customTag); } Inventions.Update(invention); return new SetTagsResponse { Result = 0, Tags = allTags }; } catch (Exception ex) { Console.WriteLine($"[InventionDB] Error setting tags: {ex.Message}"); return new SetTagsResponse { Result = -1 }; } } private static int ScoreShit(string source, string target) { if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(target)) return 0; if (source == target) return 1000; if (source.Contains(target) || target.Contains(source)) return 500; int dist = EditDistance(source, target); int maxLen = Math.Max(source.Length, target.Length); if (maxLen == 0) return 0; double similarity = 1.0 - (double)dist / maxLen; if (similarity >= 0.66) return (int)(similarity * 100); return 0; } private static int EditDistance(string a, string b) { if (string.IsNullOrEmpty(a)) return b?.Length ?? 0; if (string.IsNullOrEmpty(b)) return a.Length; int m = a.Length, n = b.Length; var dp = new int[m + 1, n + 1]; for (int i = 0; i <= m; i++) dp[i, 0] = i; for (int j = 0; j <= n; j++) dp[0, j] = j; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { dp[i, j] = a[i - 1] == b[j - 1] ? dp[i - 1, j - 1] : 1 + Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1])); } } return dp[m, n]; } } }