Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
Expand Down
8 changes: 7 additions & 1 deletion src/GameLogic/PlayerActions/Guild/GuildListRequestAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;

using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;

/// <summary>
/// Action to request the guild list.
Expand All @@ -26,7 +27,12 @@ public async ValueTask RequestGuildListAsync(Player player)
if ((player.GameContext as IGameServerContext)?.GuildServer is { } guildServer
&& await guildServer.GetGuildAsync(player.GuildStatus.GuildId).ConfigureAwait(false) is { } guild)
{
var players = await guildServer.GetGuildListAsync(player.GuildStatus.GuildId).ConfigureAwait(false);
// The client displays the members in the received order, so we sort by rank
// (master, assistant, battle master, normal members) and then by name.
var players = (await guildServer.GetGuildListAsync(player.GuildStatus.GuildId).ConfigureAwait(false))
.OrderBy(member => member.PlayerPosition, GuildPositionComparer.Instance)
.ThenBy(member => member.PlayerName, StringComparer.OrdinalIgnoreCase)
.ToList();
await player.InvokeViewPlugInAsync<IShowGuildListPlugIn>(p => p.ShowGuildListAsync(players, guild)).ConfigureAwait(false);
}
}
Expand Down
22 changes: 13 additions & 9 deletions src/GameLogic/PlayerActions/Guild/GuildRelationshipChangeAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ public class GuildRelationshipChangeAction
/// <param name="requestType">The type of request (Join or Leave).</param>
public async ValueTask RequestAsync(Player player, ushort targetPlayerId, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType)
{
var (success, (sourceGuildId, serverContext, sourceGuild)) = await this.CommonChecksAsync(player, targetPlayerId, relationshipType, requestType).ConfigureAwait(false);
if (!success)
var (success, guildData) = await this.CommonChecksAsync(player, targetPlayerId, relationshipType, requestType).ConfigureAwait(false);
if (!success || guildData is null)
{
return;
}

var (sourceGuildId, serverContext, sourceGuild) = guildData;

// Find the target player
var targetPlayer = await player.GetObservingPlayerWithIdAsync(targetPlayerId).ConfigureAwait(false);
if (targetPlayer?.GuildStatus is not { } targetGuildStatus
Expand Down Expand Up @@ -114,12 +116,14 @@ await targetPlayer.InvokeViewPlugInAsync<IShowGuildRelationshipRequestPlugIn>(p
/// <param name="targetGuildName">The name of the guild which should be removed. If <see langword="null"/>, then the own guild should be removed.</param>
public async ValueTask RequestLeaveAllianceAsync(Player player, string? targetGuildName = null)
{
var (success, (sourceGuildId, serverContext, sourceGuild)) = await this.CommonChecksAsync(player, 0, GuildRelationshipType.Alliance, GuildRelationshipRequestType.Leave).ConfigureAwait(false);
if (!success)
var (success, guildData) = await this.CommonChecksAsync(player, 0, GuildRelationshipType.Alliance, GuildRelationshipRequestType.Leave).ConfigureAwait(false);
if (!success || guildData is null)
{
return;
}

var (sourceGuildId, serverContext, sourceGuild) = guildData;

var targetGuildId = sourceGuildId;
var leaveWithOwnGuild = string.IsNullOrEmpty(targetGuildName) || sourceGuild.Name == targetGuildName;
if (!leaveWithOwnGuild)
Expand Down Expand Up @@ -192,33 +196,33 @@ await serverContext.GuildServer.CreateAllianceAsync(requesterGuildStatus.GuildId
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, res, guildMasterId)).ConfigureAwait(false);
}

private async ValueTask<(bool Success, GuildData GuildData)> CommonChecksAsync(Player player, ushort? targetPlayerId, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType)
private async ValueTask<(bool Success, GuildData? GuildData)> CommonChecksAsync(Player player, ushort? targetPlayerId, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType)
{
if (player.PendingAllianceRequest != default)
{
// There is already a pending request, so we cannot process another one at the moment. This can happen with multiple requests from different players.
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.RequestCancelled, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
return (false, null);
}

if (player.GuildStatus is not { } guildStatus
|| player.GameContext is not IGameServerContext serverContext)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.Failed, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
return (false, null);
}

if (guildStatus.Position != GuildPosition.GuildMaster)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.NoAuthorization, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
return (false, null);
}

var sourceGuild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
if (sourceGuild is null)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.GuildNotFound, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
return (false, null);
}

return (true, new(guildStatus.GuildId, serverContext, sourceGuild));
Expand Down
101 changes: 101 additions & 0 deletions src/GameLogic/PlayerActions/Guild/GuildRoleAssignAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// <copyright file="GuildRoleAssignAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;

using MUnique.OpenMU.Interfaces;

/// <summary>
/// Action to assign a role (assistant master, battle master, normal member) to a guild member.
/// Only the guild master may assign roles. Leadership transfer is not supported here.
/// </summary>
public class GuildRoleAssignAction
{
/// <summary>
/// Assigns the specified role to the guild member with the specified nickname.
/// </summary>
/// <param name="player">The requesting player. Must be the guild master.</param>
/// <param name="nickname">The nickname of the target guild member. Must be online on the same game server and in the same guild.</param>
/// <param name="newPosition">The new position. Only <see cref="GuildPosition.NormalMember"/>, <see cref="GuildPosition.BattleMaster"/> and <see cref="GuildPosition.AssistantMaster"/> are accepted.</param>
/// <remarks>
/// Failures are only logged; no dedicated client response packet exists for role assignment.
/// On success, the guild server publishes the change which updates the target's guild status and views.
/// </remarks>
public async ValueTask AssignRoleAsync(Player player, string nickname, GuildPosition newPosition)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
player.Logger.LogError($"Account {player.Account?.LoginName} not in the right state, but {player.PlayerState.CurrentState}.");
return;
}

var guildStatus = player.GuildStatus;
if (guildStatus is null)
{
player.Logger.LogError($"Player {player} not in a guild.");
return;
}

// The fixed-size name field may be space-padded by the client.
var targetName = nickname.Trim();
if (string.IsNullOrEmpty(targetName))
{
player.Logger.LogWarning("Rejected guild role assignment of player {PlayerName}: empty target name.", player.Name);
return;
}

if (newPosition is not (GuildPosition.NormalMember or GuildPosition.BattleMaster or GuildPosition.AssistantMaster))
{
player.Logger.LogWarning("Rejected guild role assignment of player {PlayerName} to {TargetName}: invalid position {Position}.", player.Name, targetName, newPosition);
return;
}

if (guildStatus.Position != GuildPosition.GuildMaster)
{
player.Logger.LogWarning("Suspicious role assign request for player with name: {PlayerName} (player is not a guild master) to assign {TargetName}.", player.Name, targetName);
return;
}

var guildServer = (player.GameContext as IGameServerContext)?.GuildServer;
if (guildServer is null)
{
player.Logger.LogWarning("No guild server available");
return;
}

var target = player.GameContext.GetPlayerByCharacterName(targetName);
if (target?.SelectedCharacter is null)
{
player.Logger.LogWarning("Rejected guild role assignment: target {TargetName} is not online on this server.", targetName);
return;
}

if (target.SelectedCharacter.Id == player.SelectedCharacter?.Id)
{
player.Logger.LogWarning("Rejected guild role assignment: guild master {PlayerName} cannot change its own role.", player.Name);
return;
}

if (target.GuildStatus?.GuildId != guildStatus.GuildId)
{
player.Logger.LogWarning("Rejected guild role assignment: target {TargetName} is not in the same guild.", targetName);
return;
}

if (target.GuildStatus.Position == GuildPosition.GuildMaster)
{
player.Logger.LogWarning("Rejected guild role assignment: target {TargetName} is the guild master, leadership transfer is not supported.", targetName);
return;
}

if (target.GuildStatus.Position == newPosition)
{
player.Logger.LogDebug("Guild role assignment skipped: target {TargetName} already has position {Position}.", targetName, newPosition);
return;
}

await guildServer.ChangeGuildMemberPositionAsync(guildStatus.GuildId, target.SelectedCharacter.Id, newPosition).ConfigureAwait(false);
}
}
8 changes: 7 additions & 1 deletion src/GameServer/GameServerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ public async ValueTask RegisterGuildMemberAsync(Player guildMember)
var guildId = guildMember.GuildStatus.GuildId;
var guildList = this._playersByGuild.GetOrAdd(guildId, id => new LockableList<Player>());
using var writeLock = await guildList.Lock.WriterLockAsync();
guildList.Add(guildMember);

// Membership changes (e.g. role assignments) re-publish the guild assignment,
// so registration must be idempotent to avoid duplicate broadcasts and stale entries.
if (!guildList.Contains(guildMember))
{
guildList.Add(guildMember);
}
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// <copyright file="GuildRoleAssignHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameServer.MessageHandler.Guild;

using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameServer.RemoteView.Guild;
using MUnique.OpenMU.Network.Packets.ClientToServer;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// Handler for guild role assign packets.
/// </summary>
/// <remarks>
/// The request's <c>Type</c> byte semantics (values 1..3) are undocumented, so it is
/// intentionally ignored. No dedicated server response packet exists; on success the
/// guild server publishes the change which updates the member's guild status and views.
/// </remarks>
[PlugIn]
[Display(Name = nameof(PlugInResources.GuildRoleAssignHandlerPlugIn_Name), Description = nameof(PlugInResources.GuildRoleAssignHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("76191DD2-AFC3-4FFF-8CB0-BB8DD1641B15")]
internal class GuildRoleAssignHandlerPlugIn : IPacketHandlerPlugIn
{
private readonly GuildRoleAssignAction _roleAssignAction = new();

/// <inheritdoc/>
public bool IsEncryptionExpected => false;

/// <inheritdoc/>
public byte Key => GuildRoleAssignRequest.Code;

/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Player player, Memory<byte> packet)
{
GuildRoleAssignRequest request = packet;
var position = request.Role.ConvertToPosition();

if (position is null)
{
player.Logger.LogWarning("Rejected guild role assignment: invalid role {Role} for target {TargetName}, could be hack attempt.", request.Role, request.PlayerName);
return;
}

await this._roleAssignAction.AssignRoleAsync(player, request.PlayerName, position.Value).ConfigureAwait(false);
}
}
18 changes: 18 additions & 0 deletions src/GameServer/Properties/PlugInResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/GameServer/Properties/PlugInResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@
<data name="GuildRequestHandlerPlugIn_Description" xml:space="preserve">
<value>Handler for guild requests.</value>
</data>
<data name="GuildRoleAssignHandlerPlugIn_Name" xml:space="preserve">
<value>Guild Role Assign Handler</value>
</data>
<data name="GuildRoleAssignHandlerPlugIn_Description" xml:space="preserve">
<value>Handler for guild role assign packets.</value>
</data>
<data name="GuildWarResponseHandlerPlugIn_Name" xml:space="preserve">
<value>Guild War Response Handler</value>
</data>
Expand Down
17 changes: 17 additions & 0 deletions src/GameServer/RemoteView/Guild/EnumExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ public static GuildMemberRole Convert(this GuildPosition playerPosition)
};
}

/// <summary>
/// Converts a wire <see cref="GuildMemberRole"/> into a <see cref="GuildPosition"/>.
/// This is the inverse of <see cref="Convert(GuildPosition)"/>.
/// </summary>
/// <param name="role">The role from the client message.</param>
/// <returns>The position, or <c>null</c> for roles which cannot be assigned, such as guild master.</returns>
public static GuildPosition? ConvertToPosition(this GuildMemberRole role)
{
return role switch
{
GuildMemberRole.NormalMember => GuildPosition.NormalMember,
GuildMemberRole.BattleMaster => GuildPosition.BattleMaster,
GuildMemberRole.AssistantMaster => GuildPosition.AssistantMaster,
_ => null,
};
}

/// <summary>
/// Converts the <see cref="GuildRequestAnswerResult"/> into a <see cref="GuildJoinRequestResult"/>.
/// </summary>
Expand Down
35 changes: 26 additions & 9 deletions src/GuildServer/GuildServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,17 +206,34 @@ public async ValueTask ChangeGuildMemberPositionAsync(uint guildId, Guid charact
{
try
{
if (this._guildDictionary.TryGetValue(guildId, out var guild))
if (!this._guildDictionary.TryGetValue(guildId, out var guild))
{
var guildMember = guild.Guild.Members.FirstOrDefault(m => m.Id == characterId);
if (guildMember != null)
{
guildMember.Status = role;
await guild.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
var listEntry = guild.Members[characterId];
listEntry.PlayerPosition = role;
}
this._logger.LogWarning("Guild {GuildId} not found, so the position of member {CharacterId} can't be changed.", guildId, characterId);
return;
}

var guildMember = guild.Guild.Members.FirstOrDefault(m => m.Id == characterId);
if (guildMember is null)
{
this._logger.LogWarning("Guild {GuildId} member {CharacterId} not found, so its position can't be changed.", guildId, characterId);
return;
}

guildMember.Status = role;
await guild.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
if (guild.Members.TryGetValue(characterId, out var listEntry))
{
listEntry.PlayerPosition = role;

// Offline members keep their cached name while their server id is
// OfflineServerId; publishing to them is pointless (dropped or, over
// Dapr, an error on every call). They pick up the persisted position
// on next login through PlayerEnteredGameAsync.
if (listEntry.PlayerName is not null && listEntry.ServerId != OfflineServerId)
{
await this._changePublisher.AssignGuildToPlayerAsync(listEntry.ServerId, listEntry.PlayerName, new GuildMemberStatus(guildId, role)).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Expand Down
Loading