Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Maple2.Database/Model/Ranking/GuildTrophyRankInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Maple2.Model.Game;

namespace Maple2.Database.Model.Ranking;

public record GuildTrophyRankInfo(
int Rank,
long GuildId,
string Name,
string Emblem,
string LeaderName,
AchievementInfo Trophy);
114 changes: 114 additions & 0 deletions Maple2.Database/Storage/Game/GameStorage.Web.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,120 @@ public IList<TrophyRankInfo> GetTrophyRankings() {
Trophy: r.Trophy))
.ToList();
}
public GuildTrophyRankInfo? GetGuildTrophyRankInfo(long guildId) {
var guild = Context.Guild
.Where(g => g.Id == guildId)
.Select(g => new { g.Id, g.Name, g.Emblem, g.LeaderId })
.FirstOrDefault();
if (guild == null) {
return null;
}

string leaderName = Context.Character
.Where(c => c.Id == guild.LeaderId)
.Select(c => c.Name)
.FirstOrDefault() ?? string.Empty;

AchievementInfo guildTrophy = ComputeGuildTrophy(guildId);

// Compute all guild totals to determine rank
var allGuildIds = Context.Guild.Select(g => g.Id).ToList();
var guildTotals = new List<(long GuildId, int Total)>();
foreach (long id in allGuildIds) {
AchievementInfo info = ComputeGuildTrophy(id);
guildTotals.Add((id, info.Total));
}

guildTotals = guildTotals.OrderByDescending(g => g.Total).ToList();
int rank = guildTotals.FindIndex(g => g.GuildId == guildId) + 1;
if (rank == 0) {
rank = guildTotals.Count + 1;
}

return new GuildTrophyRankInfo(
Rank: rank,
GuildId: guild.Id,
Name: guild.Name,
Emblem: guild.Emblem,
LeaderName: leaderName,
Trophy: guildTrophy);
}

public GuildTrophyRankInfo? GetGuildTrophyRankInfo(string guildName) {
long? guildId = Context.Guild
.Where(g => g.Name == guildName)
.Select(g => (long?) g.Id)
.FirstOrDefault();
if (guildId == null) {
return null;
}

return GetGuildTrophyRankInfo(guildId.Value);
}

public long GetGuildIdByCharacterId(long characterId) {
return Context.GuildMember
.Where(m => m.CharacterId == characterId)
.Select(m => m.GuildId)
.FirstOrDefault();
}

public IList<GuildTrophyRankInfo> GetGuildTrophyRankings() {
// Read guild data with Select projection to avoid EF tracking issues
var guilds = Context.Guild
.Select(g => new { g.Id, g.Name, g.Emblem, g.LeaderId })
.ToList();

// Batch lookup leader names
var leaderIds = guilds.Select(g => g.LeaderId).Distinct().ToList();
var leaderNames = Context.Character
.Where(c => leaderIds.Contains(c.Id))
.Select(c => new { c.Id, c.Name })
.ToDictionary(c => c.Id, c => c.Name);

var rankings = new List<GuildTrophyRankInfo>();
foreach (var guild in guilds) {
AchievementInfo trophy = ComputeGuildTrophy(guild.Id);
if (trophy.Total <= 0) {
continue;
}
string leaderName = leaderNames.GetValueOrDefault(guild.LeaderId, string.Empty);
rankings.Add(new GuildTrophyRankInfo(0, guild.Id, guild.Name, guild.Emblem, leaderName, trophy));
}

return rankings
.OrderByDescending(r => r.Trophy.Total)
.Select((r, index) => new GuildTrophyRankInfo(
Rank: index + 1,
GuildId: r.GuildId,
Name: r.Name,
Emblem: r.Emblem,
LeaderName: r.LeaderName,
Trophy: r.Trophy))
.Take(200)
.ToList();
}

private AchievementInfo ComputeGuildTrophy(long guildId) {
// Get all member character IDs and their account IDs via Select projection
var members = Context.GuildMember
.Where(m => m.GuildId == guildId)
.Select(m => m.CharacterId)
.ToList();

// Batch lookup account IDs
var characterInfo = Context.Character
.Where(c => members.Contains(c.Id))
.Select(c => new { c.Id, c.AccountId })
.ToList();

var total = new AchievementInfo();
foreach (var info in characterInfo) {
total += GetAchievementInfo(info.AccountId, info.Id);
}

return total;
}
#endregion

public IList<long> GetMentorList(long accountId, long characterId) {
Expand Down
108 changes: 94 additions & 14 deletions Maple2.Server.Web/Controllers/WebController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Maple2.Database.Model.Ranking;
using Maple2.Database.Storage;
Expand All @@ -13,6 +14,7 @@
using Maple2.Tools.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Serilog;
using Enum = System.Enum;

Expand All @@ -23,10 +25,13 @@ public class WebController : ControllerBase {

private readonly WebStorage webStorage;
private readonly GameStorage gameStorage;
private readonly IMemoryCache cache;
private static readonly TimeSpan RankingCacheDuration = TimeSpan.FromHours(1);

public WebController(WebStorage webStorage, GameStorage gameStorage) {
public WebController(WebStorage webStorage, GameStorage gameStorage, IMemoryCache cache) {
this.webStorage = webStorage;
this.gameStorage = gameStorage;
this.cache = cache;
}

[HttpPost("irrq.aspx")]
Expand Down Expand Up @@ -58,6 +63,8 @@ public async Task<IResult> Rankings() {
ByteWriter pWriter = type switch {
GameRankingType.Trophy => Trophy(userName),
GameRankingType.PersonalTrophy => PersonalTrophy(characterId),
GameRankingType.GuildTrophy => GuildTrophy(userName),
GameRankingType.PersonalGuildTrophy => PersonalGuildTrophy(characterId),
_ => new ByteWriter(),
};

Expand Down Expand Up @@ -280,26 +287,99 @@ private static IResult HandleUnknownMode(UgcType mode) {

#region Ranking
public ByteWriter Trophy(string userName) {
List<TrophyRankInfo> rankInfos = [];
using GameStorage.Request db = gameStorage.Context();
if (!string.IsNullOrEmpty(userName)) {
TrophyRankInfo? info = db.GetTrophyRankInfo(userName);
if (info != null) {
rankInfos.Add(info);
string cacheKey = $"Trophy_{userName ?? "all"}";

if (!cache.TryGetValue(cacheKey, out ByteWriter? cachedResult)) {
List<TrophyRankInfo> rankInfos = [];
using GameStorage.Request db = gameStorage.Context();
if (!string.IsNullOrEmpty(userName)) {
TrophyRankInfo? info = db.GetTrophyRankInfo(userName);
if (info != null) {
rankInfos.Add(info);
}
} else {
IList<TrophyRankInfo> infos = db.GetTrophyRankings();
rankInfos.AddRange(infos);
}
} else {
IList<TrophyRankInfo> infos = db.GetTrophyRankings();
rankInfos.AddRange(infos);

cachedResult = InGameRankPacket.Trophy(rankInfos);
cache.Set(cacheKey, cachedResult, RankingCacheDuration);
}

return InGameRankPacket.Trophy(rankInfos);
return cachedResult!;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public ByteWriter PersonalTrophy(long characterId) {
using GameStorage.Request db = gameStorage.Context();
TrophyRankInfo? info = db.GetTrophyRankInfo(characterId);
return InGameRankPacket.PersonalRank(GameRankingType.PersonalTrophy, info?.Rank ?? 0);
string cacheKey = $"PersonalTrophy_{characterId}";

if (!cache.TryGetValue(cacheKey, out ByteWriter? cachedResult)) {
using GameStorage.Request db = gameStorage.Context();
TrophyRankInfo? info = db.GetTrophyRankInfo(characterId);
cachedResult = InGameRankPacket.PersonalRank(GameRankingType.PersonalTrophy, info?.Rank ?? 0);
cache.Set(cacheKey, cachedResult, RankingCacheDuration);
}

return cachedResult!;
}

public ByteWriter GuildTrophy(string userName) {
if (!string.IsNullOrEmpty(userName)) {
string cacheKey = $"GuildTrophy_{userName}";

if (!cache.TryGetValue(cacheKey, out ByteWriter? cachedResult)) {
// Search by guild name, check full rankings cache first then fall back to DB
IList<GuildTrophyRankInfo> rankings = GetCachedGuildTrophyRankings();
GuildTrophyRankInfo? cached = rankings.FirstOrDefault(r =>
r.Name.Equals(userName, StringComparison.OrdinalIgnoreCase));

if (cached != null) {
cachedResult = InGameRankPacket.GuildTrophy(new[] { cached });
} else {
using GameStorage.Request db = gameStorage.Context();
GuildTrophyRankInfo? info = db.GetGuildTrophyRankInfo(userName);
cachedResult = InGameRankPacket.GuildTrophy(info != null ? new[] { info } : []);
}

cache.Set(cacheKey, cachedResult, RankingCacheDuration);
}

return cachedResult!;
}

return InGameRankPacket.GuildTrophy(GetCachedGuildTrophyRankings());
}

public ByteWriter PersonalGuildTrophy(long characterId) {
string cacheKey = $"PersonalGuildTrophy_{characterId}";

if (!cache.TryGetValue(cacheKey, out ByteWriter? cachedResult)) {
using GameStorage.Request db = gameStorage.Context();
long guildId = db.GetGuildIdByCharacterId(characterId);
if (guildId == 0) {
cachedResult = InGameRankPacket.PersonalRank(GameRankingType.PersonalGuildTrophy, 0);
} else {
// Check cached rankings for this guild's rank
IList<GuildTrophyRankInfo> rankings = GetCachedGuildTrophyRankings();
GuildTrophyRankInfo? cached = rankings.FirstOrDefault(r => r.GuildId == guildId);
cachedResult = InGameRankPacket.PersonalRank(GameRankingType.PersonalGuildTrophy, cached?.Rank ?? 0);
}

cache.Set(cacheKey, cachedResult, RankingCacheDuration);
}

return cachedResult!;
}

private IList<GuildTrophyRankInfo> GetCachedGuildTrophyRankings() {
const string cacheKey = "GuildTrophyRankings";

if (!cache.TryGetValue(cacheKey, out IList<GuildTrophyRankInfo>? rankings)) {
using GameStorage.Request db = gameStorage.Context();
rankings = db.GetGuildTrophyRankings();
cache.Set(cacheKey, rankings, RankingCacheDuration);
}

return rankings!;
}
#endregion

Expand Down
25 changes: 13 additions & 12 deletions Maple2.Server.Web/Packet/InGameRankPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,24 @@ public static ByteWriter PersonalRank(GameRankingType type, int rank) {
return pWriter;
}

public static ByteWriter GuildTrophy() {
public static ByteWriter GuildTrophy(IList<GuildTrophyRankInfo> rankInfos) {
var pWriter = new ByteWriter();
pWriter.Write<GameRankingType>(GameRankingType.GuildTrophy);
pWriter.WriteInt(0);
pWriter.WriteUnicodeStringWithLength(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
pWriter.WriteInt(1); // count

pWriter.WriteInt(0); // rank
pWriter.WriteLong(0); // guildId
pWriter.WriteUnicodeStringWithLength(string.Empty); // guildName
pWriter.WriteUnicodeStringWithLength(string.Empty); // guildEmblem
pWriter.WriteLong(); // guild leader id ?
pWriter.WriteInt(); // guild trophy total
pWriter.WriteInt(0); // guild trophy combat
pWriter.WriteInt(0); // guild trophy adventure
pWriter.WriteInt(0); // guild trophy lifestyle
pWriter.WriteInt(rankInfos.Count);

foreach (GuildTrophyRankInfo info in rankInfos) {
pWriter.WriteInt(info.Rank);
pWriter.WriteLong(info.GuildId);
pWriter.WriteUnicodeStringWithLength(info.Name);
pWriter.WriteUnicodeStringWithLength(info.Emblem);
pWriter.WriteUnicodeStringWithLength(info.LeaderName);
pWriter.WriteInt(info.Trophy.Total);
pWriter.WriteInt(info.Trophy.Combat);
pWriter.WriteInt(info.Trophy.Adventure);
pWriter.WriteInt(info.Trophy.Lifestyle);
}
return pWriter;
}

Expand Down
1 change: 1 addition & 0 deletions Maple2.Server.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
// });
});
builder.Services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromSeconds(15));
builder.Services.AddMemoryCache();
builder.Services.AddControllers();

builder.Logging.ClearProviders();
Expand Down
5 changes: 5 additions & 0 deletions Maple2.Server.World/Containers/GuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ public GuildError UpdateEmblem(long requestorId, string emblem) {
}

Guild.Emblem = emblem;

using (GameStorage.Request db = GameStorage.Context()) {
db.SaveGuild(Guild);
}

Broadcast(new GuildRequest {
UpdateEmblem = new GuildRequest.Types.UpdateEmblem {
RequestorName = requestor.Name,
Expand Down
Loading