From 9abe30d55eddc03b115e5087c5eeff0422e0da95 Mon Sep 17 00:00:00 2001 From: Freddie Date: Sun, 11 Oct 2026 19:42:13 -0700 Subject: [PATCH 1/5] Support removing drops via instance data --- apps/tool/Data/IgnoredJournalItems.cs | 5 +-- apps/tool/Enums/InstanceContentAction.cs | 7 ++++ .../Models/Instances/DataInstanceContent.cs | 7 +++- apps/tool/Tools/JournalTool.cs | 42 +++++++++++-------- 4 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 apps/tool/Enums/InstanceContentAction.cs diff --git a/apps/tool/Data/IgnoredJournalItems.cs b/apps/tool/Data/IgnoredJournalItems.cs index f08a4c6b4..9e4073cab 100644 --- a/apps/tool/Data/IgnoredJournalItems.cs +++ b/apps/tool/Data/IgnoredJournalItems.cs @@ -4,12 +4,9 @@ public static partial class Hardcoded { public static readonly HashSet IgnoredJournalItems = new() { - // Things screwed up by Duos - 37686, // Cracked Epoch Grasps (CoS) - 37687, // Gloves of Distorted Time (CoS) - 56376, // Thundercall (now 133251) 120163, // Thruk's Fishing Rod 178708, // Unbound Changeling?? }; } + diff --git a/apps/tool/Enums/InstanceContentAction.cs b/apps/tool/Enums/InstanceContentAction.cs new file mode 100644 index 000000000..c8143f043 --- /dev/null +++ b/apps/tool/Enums/InstanceContentAction.cs @@ -0,0 +1,7 @@ +namespace Wowthing.Tool.Enums; + +public enum InstanceContentAction +{ + Add = 0, + Remove = 1, +} diff --git a/apps/tool/Models/Instances/DataInstanceContent.cs b/apps/tool/Models/Instances/DataInstanceContent.cs index bc85fb6f3..6e375da8b 100644 --- a/apps/tool/Models/Instances/DataInstanceContent.cs +++ b/apps/tool/Models/Instances/DataInstanceContent.cs @@ -1,7 +1,10 @@ -namespace Wowthing.Tool.Models.Instances; +using Wowthing.Tool.Enums; + +namespace Wowthing.Tool.Models.Instances; public class DataInstanceContent { public int ItemId { get; set; } - public string Difficulties { get; set; } + public InstanceContentAction Action { get; set; } + public string? Difficulties { get; set; } } diff --git a/apps/tool/Tools/JournalTool.cs b/apps/tool/Tools/JournalTool.cs index ec7831a2b..f6a7b7204 100644 --- a/apps/tool/Tools/JournalTool.cs +++ b/apps/tool/Tools/JournalTool.cs @@ -3,6 +3,7 @@ using System.Reflection; using Serilog.Context; using Wowthing.Lib.Models.Wow; +using Wowthing.Tool.Enums; using Wowthing.Tool.Models; using Wowthing.Tool.Models.Instances; using Wowthing.Tool.Models.Items; @@ -661,24 +662,31 @@ await context.WowPet { foreach (var dataContent in dataInstanceEncounter.Contents.EmptyIfNull()) { - var dropDifficultiesString = string.IsNullOrEmpty(dataContent.Difficulties) - ? dataInstance.Difficulties - : dataContent.Difficulties; - var parts = dropDifficultiesString.Split('_'); - int[] dropDifficulties = parts.Skip(1) - .Select(part => _dataInstanceDifficulties[parts[0]][part]) - .ToArray(); - difficultiesByEncounterItemId[1_000_000 + dataContent.ItemId] = dropDifficulties; - - encounterItems.Add(new DumpJournalEncounterItem + if (dataContent.Action == InstanceContentAction.Add) { - ID = 1000000 + dataContent.ItemId, - DifficultyMask = 0, - FactionMask = 0, - Flags = 0, - ItemID = dataContent.ItemId, - JournalEncounterID = encounter.ID, - }); + var dropDifficultiesString = string.IsNullOrEmpty(dataContent.Difficulties) + ? dataInstance.Difficulties + : dataContent.Difficulties; + var parts = dropDifficultiesString.Split('_'); + int[] dropDifficulties = parts.Skip(1) + .Select(part => _dataInstanceDifficulties[parts[0]][part]) + .ToArray(); + + difficultiesByEncounterItemId[1_000_000 + dataContent.ItemId] = dropDifficulties; + + encounterItems.Add(new DumpJournalEncounterItem + { + ID = 1000000 + dataContent.ItemId, + DifficultyMask = 0, + FactionMask = 0, + Flags = 0, + ItemID = dataContent.ItemId, + JournalEncounterID = encounter.ID, + }); + } else if (dataContent.Action == InstanceContentAction.Remove) + { + encounterItems.RemoveAll(item => item.ItemID == dataContent.ItemId); + } } } From 584a05161e354d711552335f6243581619430d19 Mon Sep 17 00:00:00 2001 From: Freddie Date: Mon, 12 Oct 2026 15:00:39 -0700 Subject: [PATCH 2/5] Fix season 2 Nebulous Voidcore currency ID --- apps/frontend/data/currencies.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/data/currencies.ts b/apps/frontend/data/currencies.ts index 070bc7564..a2875262a 100644 --- a/apps/frontend/data/currencies.ts +++ b/apps/frontend/data/currencies.ts @@ -218,7 +218,7 @@ export const currencyExtra: Record = { 3028, // Restored Coffer Key 2803, // Undercoin 3356, // Untainted Mana-Crystals - 3513, // Nebulous Voidcore (new?) + 3418, // Nebulous Voidcore null, 3442, // Adventurer Mistcrest 3443, // Veteran Mistcrest From d4158ad857317651d951d160895233dda948f232 Mon Sep 17 00:00:00 2001 From: Freddie Date: Tue, 13 Oct 2026 11:17:12 -0700 Subject: [PATCH 3/5] Add more manual Midnight achievements --- apps/frontend/data/achievements.ts | 42 ++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/frontend/data/achievements.ts b/apps/frontend/data/achievements.ts index b5659ef72..0779698fb 100644 --- a/apps/frontend/data/achievements.ts +++ b/apps/frontend/data/achievements.ts @@ -441,6 +441,29 @@ export const extraCategories: ExtraAchievementCategory[] = [ 41597, // Glory of the Omega Raider ], }, + { + targetSlug: 'player-vs-player/world', + nameType: 1, + onlyAchievementIds: true, + achievementIds: [ + 40087, // Unbound Battle + 40097, // Ruffious's Bid + 40088, // A Champion's Tour: The War Within + 40086, // Tour of Duty: Azj-Kahet + 40085, // Tour of Duty: Hallowfall + 40084, // Tour of Duty: The Ringing Deeps + 40083, // Tour of Duty: Isle of Dorn + 40089, // Spoiled Goods 1 + 40090, // Spoiled Goods 2 + 40091, // Slightly Spoiled + 41522, // Tour of Duty: Undermine + 42131, // Tour of Duty: K'aresh + 40468, // Unbound Glory 1 + 40466, // Unbound Glory 2 + 40467, // Unbound Glory 3 + 40465, // Unbound Bounty + ], + }, null, { targetSlug: 'expansion-features/skyriding', @@ -483,14 +506,28 @@ export const extraCategories: ExtraAchievementCategory[] = [ { targetSlug: 'delves/midnight', nameType: 1, - achievementIds: [], + achievementIds: [ + 63718, // Lock and Key + 63719, // Lock and Key Master + 63720, // Happy Hexmask Collector + 63721, // The Labyrinth of Kindo'jan + 63722, // Whose Labyrinth? + 63723, // My Labyrinth + 63724, // Knock Knock + 63717, // Kindo'jan's Labyrinth Discoveries + 63727, // Treasures Upon Treasures + 63728, // A Few More Can't Hurt + ], }, { targetSlug: 'dungeons-raids/midnight-dungeon', nameType: 3, overrideName: 'Dungeons', overrideSlug: 'dungeons', - achievementIds: [61567, 61568], + achievementIds: [ + 61567, // Midnight Dungeon Hero + 61568, // Glory of the Midnight Hero + ], }, { targetSlug: 'dungeons-raids/midnight-raid', @@ -499,6 +536,7 @@ export const extraCategories: ExtraAchievementCategory[] = [ overrideSlug: 'raids', achievementIds: [ 61380, // Glory of the Midnight Raider + 63254, // Glory of the Venomous Raider ], }, { From 9a2cf20d6464028bb516a4edd2880d3a6c077917 Mon Sep 17 00:00:00 2001 From: Freddie Date: Tue, 13 Oct 2026 11:17:25 -0700 Subject: [PATCH 4/5] Add janky Auctions>Sell>Recipes page --- .../auctions/AuctionsSidebar.svelte | 4 ++ .../components/auctions/AuctionsView.svelte | 2 + .../components/auctions/commodities/store.ts | 3 +- .../components/auctions/recipes/Item.svelte | 39 ++++++++++++ .../auctions/recipes/Recipes.svelte | 42 +++++++++++++ .../components/auctions/recipes/store.ts | 58 +++++++++++++++++ .../web/Controllers/API/AuctionsController.cs | 27 ++++++++ apps/web/Services/AuctionService.cs | 42 +++++++++++++ apps/web/Services/MemoryCacheService.cs | 63 +++++++++++++++++++ .../csharp-lib/Constants/MemoryCacheKeys.cs | 3 + 10 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/components/auctions/recipes/Item.svelte create mode 100644 apps/frontend/components/auctions/recipes/Recipes.svelte create mode 100644 apps/frontend/components/auctions/recipes/store.ts diff --git a/apps/frontend/components/auctions/AuctionsSidebar.svelte b/apps/frontend/components/auctions/AuctionsSidebar.svelte index a12fd2550..da9cf2cf4 100644 --- a/apps/frontend/components/auctions/AuctionsSidebar.svelte +++ b/apps/frontend/components/auctions/AuctionsSidebar.svelte @@ -65,6 +65,10 @@ name: 'Commodities', slug: 'commodities', }, + { + name: 'Recipes', + slug: 'recipes', + }, { name: 'Extra Pets', slug: 'extra-pets', diff --git a/apps/frontend/components/auctions/AuctionsView.svelte b/apps/frontend/components/auctions/AuctionsView.svelte index 89975c47b..bc6a4fbf5 100644 --- a/apps/frontend/components/auctions/AuctionsView.svelte +++ b/apps/frontend/components/auctions/AuctionsView.svelte @@ -21,6 +21,7 @@ import Missing from './AuctionsMissing.svelte'; import MissingBigResults from './AuctionsMissingBigResults.svelte'; import RadioGroup from '@/shared/components/forms/RadioGroup.svelte'; + import Recipes from './recipes/Recipes.svelte'; import Select from '@/shared/components/forms/Select.svelte'; import SpecificItem from './AuctionsSpecificItem.svelte'; import TextInput from '@/shared/components/forms/TextInput.svelte'; @@ -87,6 +88,7 @@ 'missing-recipes': MissingBigResults, 'sell-commodities': Commodities, 'sell-extra-pets': ExtraPets, + 'sell-recipes': Recipes, 'specific-item': SpecificItem, }; diff --git a/apps/frontend/components/auctions/commodities/store.ts b/apps/frontend/components/auctions/commodities/store.ts index d83898a02..e2d6fa87e 100644 --- a/apps/frontend/components/auctions/commodities/store.ts +++ b/apps/frontend/components/auctions/commodities/store.ts @@ -6,8 +6,7 @@ export class CommodityAuctionsStore { private static url = '/api/auctions/commodities'; private cache: CommodityData = null; - async fetch() // auctionState: AuctionState - : Promise { + async fetch(): Promise { let ret: CommodityData; if (this.cache !== null) { diff --git a/apps/frontend/components/auctions/recipes/Item.svelte b/apps/frontend/components/auctions/recipes/Item.svelte new file mode 100644 index 000000000..1d6e5e781 --- /dev/null +++ b/apps/frontend/components/auctions/recipes/Item.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + + {#each auctions as auction} + + + + + {/each} + +
+ + + + + {totalCount} +
{connectedRealmName(auction.connectedRealmId)}{Math.floor(auction.buyoutPrice / 10_000).toLocaleString()} g
diff --git a/apps/frontend/components/auctions/recipes/Recipes.svelte b/apps/frontend/components/auctions/recipes/Recipes.svelte new file mode 100644 index 000000000..5dadd9c69 --- /dev/null +++ b/apps/frontend/components/auctions/recipes/Recipes.svelte @@ -0,0 +1,42 @@ + + +{#await recipesAuctionStore.fetch()} + MOO +{:then auctions} + {@const haveData = processAuctions(auctions)} + {#each haveData as [itemId, auctions, haveItems]} + + {/each} +{/await} diff --git a/apps/frontend/components/auctions/recipes/store.ts b/apps/frontend/components/auctions/recipes/store.ts new file mode 100644 index 000000000..d84edeb52 --- /dev/null +++ b/apps/frontend/components/auctions/recipes/store.ts @@ -0,0 +1,58 @@ +import { + UserAuctionDataAuction, + type UserAuctionDataAuctionArray, +} from '@/types/data/user-auctions'; +import { getNumberKeyedEntries } from '@/utils/get-number-keyed-entries'; + +export type RecipesData = Record>; +type RecipesResponse = Record< + number, + { + rawAuctions: Record; + } +>; + +export class RecipesAuctionsStore { + private static url = '/api/auctions/recipes'; + private cache: RecipesData = null; + + async fetch(): Promise { + let ret: RecipesData; + + if (this.cache !== null) { + ret = this.cache; + } else { + const xsrf = document.getElementById('app').getAttribute('data-xsrf'); + + const response = await fetch(RecipesAuctionsStore.url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + RequestVerificationToken: xsrf, + }, + }); + + ret = {}; + if (response.ok) { + const responseData = (await response.json()) as RecipesResponse; + + for (const [region, itemAuctions] of getNumberKeyedEntries(responseData)) { + ret[region] = {}; + + for (const [itemId, rawAuctions] of getNumberKeyedEntries( + itemAuctions.rawAuctions + )) { + ret[region][itemId] = rawAuctions.map( + (auctionArray) => new UserAuctionDataAuction(...auctionArray) + ); + } + } + } + + this.cache = ret; + } + + return ret; + } +} +export const recipesAuctionStore = new RecipesAuctionsStore(); diff --git a/apps/web/Controllers/API/AuctionsController.cs b/apps/web/Controllers/API/AuctionsController.cs index 9069d0b55..78d60b6e8 100644 --- a/apps/web/Controllers/API/AuctionsController.cs +++ b/apps/web/Controllers/API/AuctionsController.cs @@ -166,6 +166,33 @@ public async Task CommoditiesSpecific([FromBody] ApiAuctionsCommo return Content(json, MediaTypeNames.Application.Json); } + [HttpGet("recipes")] + [Authorize] + public async Task Recipes() + { + var timer = new JankTimer(); + + var user = await _userManager.GetUserAsync(HttpContext.User); + if (user == null) + { + _logger.LogWarning("ruh roh"); + return NotFound(); + } + + timer.AddPoint("User"); + + var data = await _auctionService.RecipesDataForUser(user, timer); + + timer.AddPoint("Data"); + + string json = JsonSerializer.Serialize(data, _jsonSerializerOptions); + + timer.AddPoint("JSON", true); + _logger.LogInformation("{timer}", timer.ToString()); + + return Content(json, MediaTypeNames.Application.Json); + } + [HttpPost("extra-pets")] [Authorize] public async Task ExtraPets([FromBody] ApiExtraPetsForm form) diff --git a/apps/web/Services/AuctionService.cs b/apps/web/Services/AuctionService.cs index 4171e29a3..18db33aac 100644 --- a/apps/web/Services/AuctionService.cs +++ b/apps/web/Services/AuctionService.cs @@ -227,6 +227,48 @@ public async Task CommoditiesDataForUser(ApplicationUser return data; } + public async Task> RecipesDataForUser(ApplicationUser user, JankTimer timer = null) + { + int[] realmIds = await _context.PlayerCharacter + .AsNoTracking() + .Where(pc => pc.Account.UserId == user.Id && + pc.Account.Enabled) + .Select(pc => pc.RealmId) + .Distinct() + .ToArrayAsync(); + + short[] regions = await _context.WowRealm + .AsNoTracking() + .Where(wr => realmIds.Contains(wr.Id)) + .Select(wr => (short)wr.Region) + .Distinct() + .ToArrayAsync(); + + var allRecipeItemIds = new HashSet(); + var recipeItems = await _memoryCacheService.GetProfessionRecipeItems(); + var withoutMountsAndPets = recipeItems.Where(kvp => kvp.Key != 777 && kvp.Key != 778); + foreach (var (_, skillLineAbilities) in withoutMountsAndPets) + { + foreach (int[] itemIds in skillLineAbilities.Values) + { + allRecipeItemIds.UnionWith(itemIds); + } + } + + var connectedRealms = await _memoryCacheService.GetConnectedRealms(); + + timer?.AddPoint("Setup"); + + var data = new Dictionary(); + + foreach (short region in regions) + { + data[region] = await _memoryCacheService.GetRecipeAuctions(region, connectedRealms[(WowRegion)region], allRecipeItemIds); + } + + return data; + } + public async Task ExtraPetDataForUser(ApplicationUser user, JankTimer timer = null) { var data = new UserAuctionData(); diff --git a/apps/web/Services/MemoryCacheService.cs b/apps/web/Services/MemoryCacheService.cs index fdd6e41fe..8bd32cb4d 100644 --- a/apps/web/Services/MemoryCacheService.cs +++ b/apps/web/Services/MemoryCacheService.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Net.Mime; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Memory; using StackExchange.Redis; @@ -8,6 +9,7 @@ using Wowthing.Lib.Models; using Wowthing.Lib.Models.Wow; using Wowthing.Lib.Services; +using Wowthing.Web.Models; using Wowthing.Web.Models.Api; namespace Wowthing.Web.Services; @@ -106,6 +108,33 @@ public async Task> GetCommoditesForRegion(short region) ); } + public async Task> GetConnectedRealms() + { + return await _memoryCache.GetOrCreateAsync( + MemoryCacheKeys.ConnectedRealmIds, + async cacheEntry => + { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); + + var allRealms = await _context.WowRealm + .AsNoTracking() + .Where(realm => realm.ConnectedRealmId > 0) + .ToArrayAsync(); + + return allRealms + .GroupBy(realm => realm.Region) + .ToDictionary( + group => group.Key, + group => group + .Select(realm => realm.ConnectedRealmId) + .Distinct() + .Order() + .ToArray() + ); + } + ); + } + public void ExpireCommoditesForRegion(short region) { _memoryCache.Remove(string.Format(MemoryCacheKeys.AuctionCommodities, region)); @@ -234,6 +263,10 @@ public async Task> GetDecor() }); } + /// + /// + /// + /// A nested dictionary of { skillLineId, { skillLineAbilityId, itemIds } } public async Task>> GetProfessionRecipeItems() { return await _memoryCache.GetOrCreateAsync( @@ -349,4 +382,34 @@ private async Task GetUserModifiedJsonTask(ApiUserResult apiResult) ); return JsonSerializer.Serialize(times); } + + public async Task GetRecipeAuctions(short regionId, int[] connectedRealmIds, HashSet allRecipeItemIds) + { + return await _memoryCache.GetOrCreateAsync( + string.Format(MemoryCacheKeys.RecipeAuctions, regionId), + async cacheEntry => + { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); + + var auctions = await _context.WowAuction + .AsNoTracking() + .Where(auction => connectedRealmIds.Contains(auction.ConnectedRealmId)) + .Where(auction => allRecipeItemIds.Contains(auction.ItemId)) + .ToArrayAsync(); + + return new UserAuctionData + { + RawAuctions = auctions + .GroupBy(auction => auction.ItemId) + .ToDictionary( + group => group.Key, + group => group + .Where(auction => auction.BuyoutPrice > 0) + .OrderBy(auction => auction.BuyoutPrice) + .Take(5) + .ToList() + ) + }; + }); + } } diff --git a/packages/csharp-lib/Constants/MemoryCacheKeys.cs b/packages/csharp-lib/Constants/MemoryCacheKeys.cs index 545bba45b..b87abf6cc 100644 --- a/packages/csharp-lib/Constants/MemoryCacheKeys.cs +++ b/packages/csharp-lib/Constants/MemoryCacheKeys.cs @@ -6,6 +6,7 @@ public static class MemoryCacheKeys public const string AuctionConnectedRealms = "auction-connected-realms"; public const string AuctionHouseUpdatedTimes = "auction-house-updated-times"; public const string BackgroundImages = "background-images"; + public const string ConnectedRealmIds = "connected-realm-ids"; public const string DecorItems = "decor-items"; public const string DynamicData = "dynamic-data:{0}"; public const string ItemBonuses = "item-bonuses"; @@ -15,6 +16,8 @@ public static class MemoryCacheKeys public const string Periods = "periods"; public const string PetIds = "petIds"; public const string ProfessionRecipeItems = "profession-recipe-items"; + public const string RecipeAuctions = "recipe-auctions:{0}"; + public const string RecipeIds = "recipe-ids"; public const string TrustedRole = "trusted-role"; public const string User = "user:{0}"; public const string UserModified = "user-modified:{0}"; From 6d3978601bbc60b1cb7967f689d18fdf19d0c8fb Mon Sep 17 00:00:00 2001 From: Freddie Date: Tue, 13 Oct 2026 11:17:40 -0700 Subject: [PATCH 5/5] Fall back to alliance/horde data for account-wide reps without both data --- .../components/reputations/AccountWide.svelte | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/frontend/user-home/components/reputations/AccountWide.svelte b/apps/frontend/user-home/components/reputations/AccountWide.svelte index 824bbcd76..3bd6a6f00 100644 --- a/apps/frontend/user-home/components/reputations/AccountWide.svelte +++ b/apps/frontend/user-home/components/reputations/AccountWide.svelte @@ -87,6 +87,12 @@ reputationSetsIndex, slug, })} + {@const repId = + reputationSet.both?.id || reputationSet.alliance?.id || reputationSet.horde?.id} + {@const repIcon = + reputationSet.both?.icon || + reputationSet.alliance?.icon || + reputationSet.horde?.icon}