Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
124 changes: 109 additions & 15 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,34 +287,121 @@ 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 byte[]? cachedData)) {
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);

ByteWriter writer = InGameRankPacket.Trophy(rankInfos);
cachedData = writer.Buffer[..writer.Length];
cache.Set(cacheKey, cachedData, RankingCacheDuration);
}

return InGameRankPacket.Trophy(rankInfos);
var result = new ByteWriter();
result.WriteBytes(cachedData!);
return result;
}

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 byte[]? cachedData)) {
using GameStorage.Request db = gameStorage.Context();
TrophyRankInfo? info = db.GetTrophyRankInfo(characterId);
ByteWriter writer = InGameRankPacket.PersonalRank(GameRankingType.PersonalTrophy, info?.Rank ?? 0);
cachedData = writer.Buffer[..writer.Length];
cache.Set(cacheKey, cachedData, RankingCacheDuration);
}

var result = new ByteWriter();
result.WriteBytes(cachedData!);
return result;
}

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

if (!cache.TryGetValue(cacheKey, out byte[]? cachedData)) {
// 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));

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

cachedData = writer.Buffer[..writer.Length];
cache.Set(cacheKey, cachedData, RankingCacheDuration);
}

var result = new ByteWriter();
result.WriteBytes(cachedData!);
return result;
}

return InGameRankPacket.GuildTrophy(GetCachedGuildTrophyRankings());
}

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

if (!cache.TryGetValue(cacheKey, out byte[]? cachedData)) {
using GameStorage.Request db = gameStorage.Context();
long guildId = db.GetGuildIdByCharacterId(characterId);
ByteWriter writer;
if (guildId == 0) {
writer = 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);
writer = InGameRankPacket.PersonalRank(GameRankingType.PersonalGuildTrophy, cached?.Rank ?? 0);
}

cachedData = writer.Buffer[..writer.Length];
cache.Set(cacheKey, cachedData, RankingCacheDuration);
}

var result = new ByteWriter();
result.WriteBytes(cachedData!);
return result;
}

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

public ByteWriter MenteeList(long accountId, long characterId) {
using GameStorage.Request db = gameStorage.Context();
IList<long> list = db.GetMentorList(accountId, characterId);

IList<PlayerInfo> players = new List<PlayerInfo>();
IList<PlayerInfo> players = [];
foreach (long menteeId in list) {
PlayerInfo? playerInfo = db.GetPlayerInfo(menteeId);
if (playerInfo != null) {
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