baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
// <copyright file="CharacterClassExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// Extension methods for <see cref="CharacterClass"/>.
/// </summary>
internal static class CharacterClassExtensions
{
/// <summary>
/// Gets the base character class.
/// </summary>
/// <param name="characterClass">The character class.</param>
/// <param name="gameConfiguration">The game configuration.</param>
/// <returns>The base character class.</returns>
internal static CharacterClass GetBaseClass(this CharacterClass characterClass, GameConfiguration gameConfiguration)
{
var previousClass = gameConfiguration.CharacterClasses.FirstOrDefault(c => c.NextGenerationClass == characterClass);
if (previousClass is null)
{
return characterClass;
}
return previousClass.GetBaseClass(gameConfiguration);
}
}

View File

@@ -0,0 +1,70 @@
// <copyright file="CurrentlyActiveQuestsPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="ICurrentlyActiveQuestsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.CurrentlyActiveQuestsPlugIn_Name), Description = nameof(PlugInResources.CurrentlyActiveQuestsPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("9851157D-97CA-42F3-840C-8448D02B49A4")]
[MinimumClient(5, 0, ClientLanguage.Invariant)]
public class CurrentlyActiveQuestsPlugIn : ICurrentlyActiveQuestsPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="CurrentlyActiveQuestsPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public CurrentlyActiveQuestsPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public async ValueTask ShowActiveQuestsAsync()
{
var connection = this._player.Connection;
if (connection is null || this._player.SelectedCharacter is not { } character)
{
return;
}
int Write()
{
const int maxQuestsPerPacket = 62;
var activeQuests = character.QuestStates
.Where(state => state.Group != QuestConstants.LegacyQuestGroup && state.ActiveQuest != null)
.Select(s => s.ActiveQuest!)
.Take(maxQuestsPerPacket)
.ToList();
var size = QuestStateListRef.GetRequiredSize(activeQuests.Count);
var span = connection.Output.GetSpan(size)[..size];
var message = new QuestStateListRef(span);
byte i = 0;
foreach (var activeQuest in activeQuests)
{
var questIdentification = message[i];
questIdentification.Number = (ushort)activeQuest.Number;
questIdentification.Group = (ushort)activeQuest.Group;
i++;
}
message.QuestCount = i;
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="EnumExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.Network.Packets.ServerToClient;
/// <summary>
/// Extensions to convert enum values.
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// Converts the specified quest reward type.
/// </summary>
/// <param name="questRewardType">Type of the quest reward.</param>
/// <returns>The converted value.</returns>
public static RewardType Convert(this QuestRewardType questRewardType)
{
return questRewardType switch
{
QuestRewardType.Item => RewardType.Item,
QuestRewardType.Experience => RewardType.Experience,
QuestRewardType.Money => RewardType.Money,
QuestRewardType.GensAttribution => RewardType.GensContribution,
QuestRewardType.Undefined => RewardType.None,
_ => throw new ArgumentException($"Unknown reward type {questRewardType}."),
};
}
}

View File

@@ -0,0 +1,73 @@
// <copyright file="LegacyQuestRewardPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlayerActions.Quests;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="ILegacyQuestRewardPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.LegacyQuestRewardPlugIn_Name), Description = nameof(PlugInResources.LegacyQuestRewardPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("53BEDA2A-9FE6-406D-AE6D-8E4DDDA3D73D")]
public class LegacyQuestRewardPlugIn : ILegacyQuestRewardPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyQuestRewardPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public LegacyQuestRewardPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public async ValueTask ShowAsync(Player receiver, QuestRewardType reward, int value, AttributeDefinition? attributeReward)
{
var receiverId = receiver.GetId(this._player);
switch (reward)
{
case QuestRewardType.LevelUpPoints:
await this._player.Connection.SendLegacyQuestRewardAsync(receiverId, LegacyQuestReward.QuestRewardType.LevelUpPoints, (byte)value).ConfigureAwait(false);
break;
case QuestRewardType.CharacterEvolutionFirstToSecond:
await this._player.Connection.SendLegacyQuestRewardAsync(receiverId, LegacyQuestReward.QuestRewardType.CharacterEvolutionFirstToSecond, (byte)(this._player.SelectedCharacter!.CharacterClass!.Number << 3)).ConfigureAwait(false);
break;
case QuestRewardType.CharacterEvolutionSecondToThird:
await this._player.Connection.SendLegacyQuestRewardAsync(receiverId, LegacyQuestReward.QuestRewardType.CharacterEvolutionSecondToThird, (byte)(this._player.SelectedCharacter!.CharacterClass!.Number << 3)).ConfigureAwait(false);
break;
case QuestRewardType.Attribute:
if (attributeReward == Stats.IsSkillComboAvailable)
{
await this._player.Connection.SendLegacyQuestRewardAsync(receiverId, LegacyQuestReward.QuestRewardType.ComboSkill, 0).ConfigureAwait(false);
}
if (attributeReward == Stats.PointsPerLevelUp)
{
var questLevel = this._player.GetQuestState(QuestConstants.LegacyQuestGroup)?.ActiveQuest?.MinimumCharacterLevel ?? 220;
var points = (this._player.Level - questLevel) * value;
await this._player.Connection.SendLegacyQuestRewardAsync(receiverId, LegacyQuestReward.QuestRewardType.LevelUpPointsPerLevelIncrease, (byte)points).ConfigureAwait(false);
}
break;
default:
this._player.Logger.LogDebug("Unknown quest reward: {0}", reward);
break;
}
}
}

View File

@@ -0,0 +1,68 @@
// <copyright file="LegacyQuestStateDialogPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="ILegacyQuestStateDialogPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.LegacyQuestStateDialogPlugIn_Name), Description = nameof(PlugInResources.LegacyQuestStateDialogPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("F43DA7A9-1ACC-4FBB-9E15-8BE977F4CAF9")]
public class LegacyQuestStateDialogPlugIn : ILegacyQuestStateDialogPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyQuestStateDialogPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public LegacyQuestStateDialogPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public async ValueTask ShowAsync()
{
if (this._player.Connection is not { } connection || this._player.SelectedCharacter is null)
{
return;
}
var questState = this._player.SelectedCharacter.QuestStates.FirstOrDefault(s => s.Group == QuestConstants.LegacyQuestGroup);
var quest = questState?.ActiveQuest ?? this._player.GetNextLegacyQuest();
await connection.SendLegacyQuestStateDialogAsync((byte)(quest?.Number ?? 0), this._player.GetLegacyQuestStateByte()).ConfigureAwait(false);
if (quest?.RequiredMonsterKills.Any() ?? false)
{
int Write()
{
var size = LegacyQuestMonsterKillInfoRef.Length;
var span = connection.Output.GetSpan(size)[..size];
var packet = new LegacyQuestMonsterKillInfoRef(span);
packet.QuestIndex = (byte)quest.Number;
int i = 0;
foreach (var requirement in quest.RequiredMonsterKills)
{
var monsterState = packet[i];
monsterState.MonsterNumber = (uint)requirement.Monster!.Number;
monsterState.KillCount = (uint)(questState?.RequirementStates.FirstOrDefault(r => r.Requirement == requirement)?.KillCount ?? 0);
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,44 @@
// <copyright file="QuestCancelledPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestCancelledPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestCancelledPlugIn_Name), Description = nameof(PlugInResources.QuestCancelledPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("733C8E1A-A663-4804-96E3-1EA955438970")]
public class QuestCancelledPlugIn : IQuestCancelledPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestCancelledPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestCancelledPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask QuestCancelledAsync(QuestDefinition quest)
{
if (quest.Group == QuestConstants.LegacyQuestGroup)
{
this._player.SelectedCharacter?.QuestStates.FirstOrDefault(q => q.Group == QuestConstants.LegacyQuestGroup)?.SendLegacyQuestStateAsync(this._player);
return;
}
await this._player.Connection.SendQuestCancelledAsync((ushort)quest.Number, (ushort)quest.Group).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,45 @@
// <copyright file="QuestCompletionResponsePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <seealso cref="IQuestCompletionResponsePlugIn" /> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestCompletionResponsePlugIn_Name), Description = nameof(PlugInResources.QuestCompletionResponsePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("4BCA9C67-A695-4244-9F5A-3B7CAC049DB4")]
public class QuestCompletionResponsePlugIn : IQuestCompletionResponsePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestCompletionResponsePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestCompletionResponsePlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public async ValueTask QuestCompletedAsync(QuestDefinition quest)
{
if (quest.Group == QuestConstants.LegacyQuestGroup)
{
await this._player.Connection.SendLegacySetQuestStateResponseAsync((byte)quest.Number, 0, this._player.GetLegacyQuestStateByte()).ConfigureAwait(false);
}
else
{
await this._player.Connection.SendQuestCompletionResponseAsync((ushort)quest.Number, (ushort)quest.Group, true).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,59 @@
// <copyright file="QuestEventResponsePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestEventResponsePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestEventResponsePlugIn_Name), Description = nameof(PlugInResources.QuestEventResponsePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("5EAE1634-9589-4DEB-AEBA-93D0AC8AC5DF")]
public class QuestEventResponsePlugIn : IQuestEventResponsePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestEventResponsePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestEventResponsePlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask ShowActiveEventQuestsAsync()
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
// Hints: * This is only sent for new characters (level <= 1, no MG or DL).
// * I also found a struct which contains the following fields (each 2 bytes: NpcNumber (0), Count (1), QuestGroup (1), QuestNumber (0)).
// * Always: C1 0C F6 03 00 00 01 00 00 00 01 00
int Write()
{
var size = QuestEventResponseRef.Length;
var span = connection.Output.GetSpan(size)[..size];
var packet = new QuestEventResponseRef(span);
var vanertGroup = packet[0];
vanertGroup.Number = 1;
var duprianGroup = packet[1];
duprianGroup.Number = 1;
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,64 @@
// <copyright file="QuestProgressExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="IQuestProgressPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestProgressExtendedPlugIn_Name), Description = nameof(PlugInResources.QuestProgressExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("30446944-29C6-48EE-A62E-3B724E7E444D")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class QuestProgressExtendedPlugIn : IQuestProgressPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestProgressExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestProgressExtendedPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask ShowQuestProgressAsync(QuestDefinition quest, bool wasProgressionRequested)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int Write()
{
var size = QuestProgressExtendedRef.Length;
var span = connection.Output.GetSpan(size)[..size];
_ = new QuestProgressExtendedRef(span)
{
QuestGroup = (ushort)quest.Group,
};
var questState = this._player.SelectedCharacter?.QuestStates.FirstOrDefault(q => q.Group == quest.Group);
// to write the quest state into the message, we can use the same logic as for the QuestState. The messages are equal in their content.
QuestStateExtendedRef progress = span;
progress.AssignActiveQuestData(questState, this._player);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,62 @@
// <copyright file="QuestProgressPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestProgressPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestProgressPlugIn_Name), Description = nameof(PlugInResources.QuestProgressPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("5D2B7F90-FEFA-4889-B339-D64512471613")]
public class QuestProgressPlugIn : IQuestProgressPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestProgressPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestProgressPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask ShowQuestProgressAsync(QuestDefinition quest, bool wasProgressionRequested)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int Write()
{
var size = QuestProgressRef.Length;
var span = connection.Output.GetSpan(size)[..size];
var packet = new QuestProgressRef(span)
{
QuestGroup = (ushort)quest.Group,
};
var questState = this._player.SelectedCharacter?.QuestStates.FirstOrDefault(q => q.Group == quest.Group);
// to write the quest state into the message, we can use the same logic as for the QuestState. The messages are equal in their content.
QuestStateRef progress = span;
progress.AssignActiveQuestData(questState, this._player);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,57 @@
// <copyright file="QuestStartedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestStartedPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestStartedPlugIn_Name), Description = nameof(PlugInResources.QuestStartedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("2D813203-8266-4BB7-A267-E476AD11AC4B")]
public class QuestStartedPlugIn : IQuestStartedPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestStartedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestStartedPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask QuestStartedAsync(QuestDefinition quest)
{
if (this._player.Connection is null)
{
return;
}
if (quest.Group == QuestConstants.LegacyQuestGroup)
{
await this._player.Connection.SendLegacySetQuestStateResponseAsync((byte)quest.Number, 0, this._player.GetLegacyQuestStateByte()).ConfigureAwait(false);
}
else
{
var state = this._player.GetQuestState(quest.Group);
if (state?.ActiveQuest == quest)
{
await this._player.Connection.SendQuestStepInfoAsync((ushort)quest.Number, (ushort)quest.Group).ConfigureAwait(false);
await this._player.InvokeViewPlugInAsync<IQuestProgressPlugIn>(p => p.ShowQuestProgressAsync(quest, true)).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,92 @@
// <copyright file="QuestStateExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.PlayerActions.Quests;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network.Packets;
/// <summary>
/// Extensions regarding the quest state of a player.
/// </summary>
internal static class QuestStateExtensions
{
/// <summary>
/// Gets the quest state byte for the current quest state of the legacy quest group.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>The quest state byte for the current quest state of the legacy quest group.</returns>
/// <remarks>
/// Coming to this solution was pure brainfuck!.
/// </remarks>
public static byte GetLegacyQuestStateByte(this Player player)
{
var legacyQuestState = player.GetQuestState(QuestConstants.LegacyQuestGroup);
// The first case is the easiest:
if (legacyQuestState is null)
{
// In this case, all Quests are inactive, including the starting one
return 0xFF;
}
// The next one is obvious, too:
if (legacyQuestState.LastFinishedQuest is null && legacyQuestState.ActiveQuest is null)
{
// In this case, it depends if we have an active quest
return (byte)(0b1111_1100 | (legacyQuestState.ActiveQuest is { }
? (byte)LegacyQuestState.Active
: (byte)LegacyQuestState.Inactive));
}
var quest = legacyQuestState.ActiveQuest ?? player.GetNextLegacyQuest() ?? legacyQuestState.LastFinishedQuest;
if (quest is null)
{
// Should never happen.
return 0xFF;
}
var startOffset = (quest.Number / 4) * 4; // 4 quests per byte
var result = 0;
for (int i = 0; i < 4; i++)
{
var shift = i * 2;
if ((legacyQuestState.LastFinishedQuest?.Number >= i + startOffset)
|| (legacyQuestState.ActiveQuest?.Number > i + startOffset))
{
// This case is a bit simplified. There may be previous quests which are not meant for the character class.
// For the sake of simplicity, we leave it like that, for now.
result |= (byte)LegacyQuestState.Complete << shift;
}
else if (legacyQuestState.ActiveQuest == quest && quest.Number == i + startOffset)
{
result |= (byte)LegacyQuestState.Active << shift;
}
else
{
// This is also simplified. This quest may not exist,
// then we wouldn't OR a new state, and just leave these bits as 0.
result |= (byte)LegacyQuestState.Inactive << shift;
}
}
return (byte)result;
}
/// <summary>
/// Gets the next legacy quest of the currently opened NPC.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>The next legacy quest of the currently opened NPC.</returns>
public static QuestDefinition? GetNextLegacyQuest(this Player player)
{
var legacyQuestState = player.GetQuestState(QuestConstants.LegacyQuestGroup);
return player.GetAvailableQuestsOfOpenedNpc()
.OrderBy(quest => quest.Number)
.FirstOrDefault(quest => quest.Number > (legacyQuestState?.LastFinishedQuest?.Number ?? -1));
}
}

View File

@@ -0,0 +1,61 @@
// <copyright file="QuestStateResponseExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="IQuestStateResponsePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestStateResponseExtendedPlugIn_Name), Description = nameof(PlugInResources.QuestStateResponseExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("5EA4F6F0-BF20-48AD-B491-11E707052E95")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class QuestStateResponseExtendedPlugIn : QuestStateResponsePlugIn, IQuestStateResponsePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestStateResponseExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestStateResponseExtendedPlugIn(RemotePlayer player)
: base(player)
{
this._player = player;
}
/// <inheritdoc />
protected override async ValueTask ShowNewQuestStateAsync(CharacterQuestState questState)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int Write()
{
var size = QuestStateExtendedRef.Length;
var span = connection.Output.GetSpan(size)[..size];
var packet = new QuestStateExtendedRef(span)
{
QuestGroup = (ushort)questState.Group,
};
packet.AssignActiveQuestData(questState, this._player);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,82 @@
// <copyright file="QuestStateResponsePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.GameServer.MessageHandler.Quests;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestStateResponsePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestStateResponsePlugIn_Name), Description = nameof(PlugInResources.QuestStateResponsePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("BBE63528-67DC-4D5F-8C9D-D09AB488CC55")]
[MinimumClient(0, 90, ClientLanguage.Invariant)]
public class QuestStateResponsePlugIn : IQuestStateResponsePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestStateResponsePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestStateResponsePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc />
public async ValueTask ShowQuestStateAsync(CharacterQuestState? questState)
{
if (questState is null || questState.Group == QuestConstants.LegacyQuestGroup)
{
await this.ShowLegacyQuestStateAsync(this._player.SelectedCharacter?.QuestStates.FirstOrDefault(state => state.Group == QuestConstants.LegacyQuestGroup)).ConfigureAwait(false);
return;
}
await this.ShowNewQuestStateAsync(questState).ConfigureAwait(false);
}
/// <summary>
/// Shows the legacy quest state.
/// </summary>
/// <param name="questState">State of the quest.</param>
protected virtual async ValueTask ShowLegacyQuestStateAsync(CharacterQuestState? questState)
{
await questState.SendLegacyQuestStateAsync(this._player).ConfigureAwait(false);
}
/// <summary>
/// Shows the quest state of the new quest system.
/// </summary>
/// <param name="questState">State of the quest.</param>
protected virtual async ValueTask ShowNewQuestStateAsync(CharacterQuestState questState)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int Write()
{
var size = QuestStateRef.Length;
var span = connection.Output.GetSpan(size)[..size];
var packet = new QuestStateRef(span)
{
QuestGroup = (ushort)questState.Group,
};
packet.AssignActiveQuestData(questState, this._player);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="QuestStepInfoPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IQuestStepInfoPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.QuestStepInfoPlugIn_Name), Description = nameof(PlugInResources.QuestStepInfoPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("099AACF3-2933-486C-83DE-01A340DACAA6")]
public class QuestStepInfoPlugIn : IQuestStepInfoPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="QuestStepInfoPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public QuestStepInfoPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc/>
public async ValueTask ShowQuestStepInfoAsync(short questGroup, short stepNumber)
{
await this._player.Connection.SendQuestStepInfoAsync((ushort)stepNumber, (ushort)questGroup).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,237 @@
// <copyright file="QuestStructExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using QuestReward = MUnique.OpenMU.DataModel.Configuration.Quests.QuestReward;
/// <summary>
/// Extensions for quest message structs.
/// </summary>
public static class QuestStructExtensions
{
/// <summary>
/// Sends the quest states of the legacy quest system.
/// </summary>
/// <param name="questState">State of the quest.</param>
/// <param name="player">The player.</param>
internal static async ValueTask SendLegacyQuestStateAsync(this CharacterQuestState? questState, RemotePlayer player)
{
var connection = player.Connection;
if (connection is null)
{
return;
}
var questCount = 7;
int Write()
{
var size = LegacyQuestStateListRef.GetRequiredSize(questCount);
var span = connection.Output.GetSpan(size)[..size];
var message = new LegacyQuestStateListRef(span)
{
QuestCount = (byte)questCount,
};
if (questState?.LastFinishedQuest != null)
{
for (int i = 0; i <= questState.LastFinishedQuest.Number; i++)
{
message[i] = LegacyQuestState.Complete;
}
}
if (questState?.ActiveQuest != null)
{
message[questState.ActiveQuest.Number] = LegacyQuestState.Active;
}
if (player.SelectedCharacter?.CharacterClass?.GetBaseClass(player.GameContext.Configuration).Number != (byte)CharacterClassNumber.DarkKnight)
{
message.SecretOfDarkStoneState = LegacyQuestState.Undefined;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
/// <summary>
/// Assigns the <paramref name="questState"/> to this <paramref name="message"/>.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="questState">State of the quest.</param>
/// <param name="player">The player.</param>
internal static void AssignActiveQuestData(this QuestStateRef message, CharacterQuestState? questState, RemotePlayer player)
{
var activeQuest = questState?.ActiveQuest;
if (activeQuest is null)
{
return;
}
message.QuestNumber = (ushort)activeQuest.Number;
var itemSerializer = player.ItemSerializer;
if (activeQuest.RequiredItems.Any())
{
message.ConditionCount = (byte)activeQuest.RequiredItems.Count;
int i = 0;
foreach (var requiredItem in activeQuest.RequiredItems)
{
requiredItem.AssignTo(message.GetQuestCondition(i), player);
i++;
}
}
else if (activeQuest.RequiredMonsterKills.Any())
{
message.ConditionCount = (byte)activeQuest.RequiredMonsterKills.Count;
int i = 0;
foreach (var requiredKill in activeQuest.RequiredMonsterKills)
{
requiredKill.AssignTo(message.GetQuestCondition(i), questState!);
i++;
}
}
else
{
// no requirement states available
}
int r = 0;
foreach (var reward in activeQuest.Rewards)
{
reward.AssignTo(message.GetQuestReward(r), itemSerializer);
r++;
}
message.RewardCount = (byte)r;
}
/// <summary>
/// Assigns the <paramref name="questState"/> to this <paramref name="message"/>.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="questState">State of the quest.</param>
/// <param name="player">The player.</param>
internal static void AssignActiveQuestData(this QuestStateExtendedRef message, CharacterQuestState? questState, RemotePlayer player)
{
var activeQuest = questState?.ActiveQuest;
if (activeQuest is null)
{
return;
}
message.QuestNumber = (ushort)activeQuest.Number;
var itemSerializer = player.ItemSerializer;
if (activeQuest.RequiredItems.Any())
{
message.ConditionCount = (byte)activeQuest.RequiredItems.Count;
int i = 0;
foreach (var requiredItem in activeQuest.RequiredItems)
{
requiredItem.AssignTo(message.GetQuestConditionExtended(i), player);
i++;
}
}
else if (activeQuest.RequiredMonsterKills.Any())
{
message.ConditionCount = (byte)activeQuest.RequiredMonsterKills.Count;
int i = 0;
foreach (var requiredKill in activeQuest.RequiredMonsterKills)
{
requiredKill.AssignTo(message.GetQuestConditionExtended(i), questState!);
i++;
}
}
else
{
// no requirement states available
}
int r = 0;
foreach (var reward in activeQuest.Rewards)
{
reward.AssignTo(message.GetQuestRewardExtended(r), itemSerializer);
r++;
}
message.RewardCount = (byte)r;
}
private static void AssignTo(this QuestItemRequirement itemRequirement, QuestConditionRef condition, RemotePlayer player)
{
condition.Type = ConditionType.Item;
condition.RequiredCount = (uint)itemRequirement.MinimumNumber;
condition.CurrentCount = (uint)(player.Inventory?.Items.Count(item => Equals(item.Definition, itemRequirement.Item)) ?? 0);
condition.RequirementId = itemRequirement.Item!.GetItemType();
var temporaryItem = new TemporaryItem { Definition = itemRequirement.Item };
temporaryItem.Durability = temporaryItem.GetMaximumDurabilityOfOnePiece();
player.ItemSerializer.SerializeItem(condition.RequiredItemData, temporaryItem);
}
private static void AssignTo(this QuestItemRequirement itemRequirement, QuestConditionExtendedRef condition, RemotePlayer player)
{
condition.Type = ConditionType.Item;
condition.RequiredCount = (uint)itemRequirement.MinimumNumber;
condition.CurrentCount = (uint)(player.Inventory?.Items.Count(item => Equals(item.Definition, itemRequirement.Item)) ?? 0);
condition.RequirementId = itemRequirement.Item!.GetItemType();
var temporaryItem = new TemporaryItem { Definition = itemRequirement.Item };
temporaryItem.Durability = temporaryItem.GetMaximumDurabilityOfOnePiece();
player.ItemSerializer.SerializeItem(condition.RequiredItemData, temporaryItem);
}
private static void AssignTo(this QuestMonsterKillRequirement killRequirement, QuestConditionRef condition, CharacterQuestState questState)
{
condition.Type = ConditionType.MonsterKills;
condition.RequiredCount = (uint)killRequirement.MinimumNumber;
condition.RequirementId = (ushort)killRequirement.Monster!.Number;
condition.CurrentCount = (uint)(questState.RequirementStates.FirstOrDefault(s => s.Requirement != null && s.Requirement.Equals(killRequirement))?.KillCount ?? 0);
}
private static void AssignTo(this QuestMonsterKillRequirement killRequirement, QuestConditionExtendedRef condition, CharacterQuestState questState)
{
condition.Type = ConditionType.MonsterKills;
condition.RequiredCount = (uint)killRequirement.MinimumNumber;
condition.RequirementId = (ushort)killRequirement.Monster!.Number;
condition.CurrentCount = (uint)(questState.RequirementStates.FirstOrDefault(s => s.Requirement != null && s.Requirement.Equals(killRequirement))?.KillCount ?? 0);
}
private static void AssignTo(this QuestReward questReward, Network.Packets.ServerToClient.QuestRewardRef rewardStruct, IItemSerializer itemSerializer)
{
rewardStruct.Type = questReward.RewardType.Convert();
rewardStruct.RewardCount = (uint)questReward.Value;
if (questReward.RewardType == QuestRewardType.Item && questReward.ItemReward is { } itemReward)
{
rewardStruct.RewardId = itemReward.Definition!.GetItemType();
itemSerializer.SerializeItem(rewardStruct.RewardedItemData, itemReward);
}
}
private static void AssignTo(this QuestReward questReward, Network.Packets.ServerToClient.QuestRewardExtendedRef rewardStruct, IItemSerializer itemSerializer)
{
rewardStruct.Type = questReward.RewardType.Convert();
rewardStruct.RewardCount = (uint)questReward.Value;
if (questReward.RewardType == QuestRewardType.Item && questReward.ItemReward is { } itemReward)
{
rewardStruct.RewardId = itemReward.Definition!.GetItemType();
itemSerializer.SerializeItem(rewardStruct.RewardedItemData, itemReward);
}
}
private static ushort GetItemType(this ItemDefinition item)
{
// since the original game server/client supports up to 512 items per group, we have to shift the group by 9 bytes instead of 8.
return (ushort)((ushort)(item.Group << 9) | (ushort)item.Number);
}
}

View File

@@ -0,0 +1,67 @@
// <copyright file="ShowAvailableQuestsPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.Quest;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlayerActions.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAvailableQuestsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAvailableQuestsPlugIn_Name), Description = nameof(PlugInResources.ShowAvailableQuestsPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("63C7B3E2-EF66-49BB-A9F8-EFBD2389588F")]
public class ShowAvailableQuestsPlugIn : IShowAvailableQuestsPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowAvailableQuestsPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAvailableQuestsPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public async ValueTask ShowAvailableQuestsAsync()
{
var connection = this._player.Connection;
if (connection is null || this._player.OpenedNpc is null)
{
return;
}
var totalQuests = this._player.GetAvailableQuestsOfOpenedNpc().ToList();
var questCount = totalQuests.Count;
int Write()
{
var size = AvailableQuestsRef.GetRequiredSize(questCount);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AvailableQuestsRef(span)
{
QuestCount = (ushort)questCount,
QuestNpcNumber = (ushort)this._player.OpenedNpc.Definition.Number,
};
for (int i = 0; i < questCount; i++)
{
var block = packet[i];
var quest = totalQuests[i];
block.Number = (ushort)quest.StartingNumber;
block.Group = (ushort)quest.Group;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}