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,64 @@
// <copyright file="ElfSoldierBuffRequestAction.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.Quests;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Attributes;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action of requesting the elf soldier buff.
/// </summary>
/// <remarks>
/// Instead of hard-coding all this stuff, we could define something like a 'RequestableBuff' in the MonsterDefinition.
/// </remarks>
public class ElfSoldierBuffRequestAction
{
private static readonly short ElfSoldierNumber = 257;
private static readonly MagicEffectDefinition BuffEffect = new SoldierBuffMagicEffectDefinition
{
InformObservers = true,
Name = "Elf Soldier Buff",
Number = 3,
StopByDeath = true,
};
/// <summary>
/// Requests the buff and adds it to the <see cref="Player.MagicEffectList"/> when the player is allowed to get it.
/// </summary>
/// <param name="player">The player.</param>
public async ValueTask RequestBuffAsync(Player player)
{
if (player.OpenedNpc is null
|| player.OpenedNpc.Definition.NpcWindow != NpcWindow.NpcDialog
|| player.OpenedNpc.Definition.Number != ElfSoldierNumber)
{
return;
}
if (player.Level > 220)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ElfSoldierStrongEnoughMessage)).ConfigureAwait(false);
return;
}
await player.MagicEffectList.AddEffectAsync(new MagicEffect(
TimeSpan.FromMinutes(60),
BuffEffect,
new MagicEffect.ElementWithTarget(new ConstantElement(50 + (player.Level / 5), AggregateType.AddFinal), Stats.DefenseFinal),
new MagicEffect.ElementWithTarget(new ConstantElement(45 + (player.Level / 3)), Stats.GreaterDamageBonus))).ConfigureAwait(false);
}
private sealed class SoldierBuffMagicEffectDefinition : MagicEffectDefinition
{
public SoldierBuffMagicEffectDefinition()
{
this.PowerUpDefinitions = new List<PowerUpDefinition>(2);
}
}
}

View File

@@ -0,0 +1,68 @@
// <copyright file="PlayerQuestExtensions.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.Quests;
using MUnique.OpenMU.DataModel.Configuration.Quests;
/// <summary>
/// Quest related extension methods.
/// </summary>
public static class PlayerQuestExtensions
{
/// <summary>
/// Gets the state of the quest.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group of the quest.</param>
/// <param name="number">The number of the quest.</param>
/// <returns>The quest state, if found; Otherwise, <c>null</c>.</returns>
public static CharacterQuestState? GetQuestState(this Player player, short group, short number)
{
return player.SelectedCharacter?.QuestStates.FirstOrDefault(state => state.Group == group && (state.ActiveQuest?.Number == number || state.ActiveQuest?.StartingNumber == number));
}
/// <summary>
/// Gets the state of the quest of a group.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group of the quest.</param>
/// <returns>The quest state, if found; Otherwise, <c>null</c>.</returns>
public static CharacterQuestState? GetQuestState(this Player player, short group)
{
return player.SelectedCharacter?.QuestStates.FirstOrDefault(state => state.Group == group);
}
/// <summary>
/// Gets the quest definition for the specified group and number depending on the players character class.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group.</param>
/// <param name="number">The number.</param>
/// <returns>The quest definition for the specified group and number depending on the players character class.</returns>
public static QuestDefinition? GetQuest(this Player player, short group, short number)
{
var possibleQuestsOfGroup = player.OpenedNpc?.Definition.Quests
.Where(q => q.Group == group)
.Where(q => q.QualifiedCharacter is null || Equals(q.QualifiedCharacter, player.SelectedCharacter?.CharacterClass))
.OrderBy(q => q.Number);
return possibleQuestsOfGroup?.FirstOrDefault(q => q.StartingNumber == number || q.Number == number);
}
/// <summary>
/// Gets the available quests of the currently opened NPC depending on the players character class.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>The available quests of the currently opened NPC depending on the players character class.</returns>
public static IEnumerable<QuestDefinition> GetAvailableQuestsOfOpenedNpc(this Player player)
{
return player.OpenedNpc?.Definition.Quests
.Where(q => q.QualifiedCharacter is null
|| Equals(q.QualifiedCharacter, player.SelectedCharacter?.CharacterClass))
.Where(q => q.MinimumCharacterLevel <= player.Level
&& (q.MaximumCharacterLevel == default || q.MaximumCharacterLevel >= player.Level))
?? Enumerable.Empty<QuestDefinition>();
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="QuestCancelAction.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.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
/// <summary>
/// Cancels the specified quest for the player.
/// </summary>
public class QuestCancelAction
{
/// <summary>
/// Cancels the quest for the player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group of the quest.</param>
/// <param name="number">The number of the quest.</param>
public async ValueTask CancelQuestAsync(Player player, short group, short number)
{
var questState = player.GetQuestState(group, number);
if (questState?.ActiveQuest is null)
{
return;
}
var quest = questState.ActiveQuest;
await questState.ClearAsync(player.PersistenceContext).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IQuestCancelledPlugIn>(p => p.QuestCancelledAsync(quest)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,31 @@
// <copyright file="QuestClientAction.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.Quests;
/// <summary>
/// Is called when the player completed the an action (e.g. the tutorial).
/// The server checks if the specified quest is currently in progress.
/// If the quest got a Condition(condition type 0x10) for this flag,
/// the condition is flagged as fulfilled.
/// </summary>
public class QuestClientAction
{
/// <summary>
/// Clients the action.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group.</param>
/// <param name="number">The number.</param>
public void ClientAction(Player player, short group, short number)
{
var questState = player.SelectedCharacter?.QuestStates.FirstOrDefault(s => s.Group == group && s.ActiveQuest?.Number == number && s.ActiveQuest.RequiresClientAction);
if (questState is null)
{
return;
}
questState.ClientActionPerformed = true;
}
}

View File

@@ -0,0 +1,197 @@
// <copyright file="QuestCompletionAction.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.Quests;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.GameLogic.Views.Quest;
using MUnique.OpenMU.Persistence;
/// <summary>
/// A player action which implements the completion of a quest.
/// </summary>
public class QuestCompletionAction
{
/// <summary>
/// Tries to completes the quest of the given group and number for the specified player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group.</param>
/// <param name="number">The number.</param>
public async ValueTask CompleteQuestAsync(Player player, short group, short number)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var questState = player.GetQuestState(group, number);
var activeQuest = questState?.ActiveQuest;
if (activeQuest is null)
{
player.Logger.LogDebug("Failed, no active quest");
return;
}
foreach (var requiredItem in activeQuest.RequiredItems)
{
var requiredLevel = requiredItem.DropItemGroup?.ItemLevel;
var itemCount = player.Inventory?.Items
.Count(i => Equals(i.Definition, requiredItem.Item)
&& (requiredLevel is null || requiredLevel == i.Level));
if (requiredItem.MinimumNumber <= itemCount)
{
continue;
}
player.Logger.LogDebug("Failed, required item not found: {0}", requiredItem.Item!.GetNameForLevel(requiredLevel ?? 0));
return;
}
foreach (var requiredKills in activeQuest.RequiredMonsterKills)
{
var currentKillCount = questState!.RequirementStates.FirstOrDefault(r => object.Equals(r.Requirement, requiredKills))?.KillCount ?? 0;
if (currentKillCount >= requiredKills.MinimumNumber)
{
continue;
}
player.Logger.LogDebug("Failed, required kills of monster {0}: {1}/{2};", requiredKills.Monster?.Designation, currentKillCount, requiredKills.MinimumNumber);
return;
}
if (activeQuest.RequiresClientAction && !questState!.ClientActionPerformed)
{
player.Logger.LogDebug("Failed, client action not performed.");
return;
}
questState!.LastFinishedQuest = activeQuest;
foreach (var requiredItem in activeQuest.RequiredItems)
{
var requiredLevel = requiredItem.DropItemGroup?.ItemLevel;
var items = player.Inventory!.Items
.Where(item => Equals(item.Definition, requiredItem.Item)
&& (requiredLevel is null || requiredLevel == item.Level))
.Take(requiredItem.MinimumNumber)
.ToList();
foreach (var item in items)
{
await player.DestroyInventoryItemAsync(item).ConfigureAwait(false);
}
}
foreach (var reward in activeQuest.Rewards)
{
await AddRewardAsync(player, reward, activeQuest).ConfigureAwait(false);
}
await questState.ClearAsync(player.PersistenceContext).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IQuestCompletionResponsePlugIn>(p => p.QuestCompletedAsync(activeQuest)).ConfigureAwait(false);
}
private static async ValueTask AddRewardAsync(Player player, QuestReward reward, QuestDefinition quest)
{
switch (reward.RewardType)
{
case QuestRewardType.Attribute:
var attribute = player.SelectedCharacter!.Attributes.FirstOrDefault(a => object.Equals(a.Definition, reward.AttributeReward));
if (attribute is null)
{
attribute = player.PersistenceContext.CreateNew<StatAttribute>(reward.AttributeReward, 0);
player.SelectedCharacter.Attributes.Add(attribute);
}
attribute.Value += reward.Value;
// Compensate level-up points when doing a quest at a later level.
if (attribute.Definition == Stats.PointsPerLevelUp)
{
var playerLevel = (int)player.Attributes![Stats.Level];
player.SelectedCharacter.LevelUpPoints += (playerLevel - quest.MinimumCharacterLevel) * reward.Value;
}
await player.InvokeViewPlugInAsync<ILegacyQuestRewardPlugIn>(p => p.ShowAsync(player, QuestRewardType.Attribute, reward.Value, attribute.Definition)).ConfigureAwait(false);
break;
case QuestRewardType.Item:
var item = player.PersistenceContext.CreateNew<Item>();
item.AssignValues(reward.ItemReward ?? throw new InvalidOperationException($"Reward {reward.GetId()} is defined as item reward, but has no item assigned"));
if (await player.Inventory!.AddItemAsync(item).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IItemAppearPlugIn>(p => p.ItemAppearAsync(item)).ConfigureAwait(false);
}
else
{
player.CurrentMap?.AddAsync(new DroppedItem(item, player.Position, player.CurrentMap, player, player.GetAsEnumerable()));
}
break;
case QuestRewardType.LevelUpPoints:
player.SelectedCharacter!.LevelUpPoints += reward.Value;
await player.InvokeViewPlugInAsync<ILegacyQuestRewardPlugIn>(p => p.ShowAsync(player, QuestRewardType.LevelUpPoints, reward.Value, null)).ConfigureAwait(false);
break;
case QuestRewardType.CharacterEvolutionFirstToSecond:
player.SelectedCharacter!.CharacterClass = player.SelectedCharacter.CharacterClass?.NextGenerationClass
?? throw new InvalidOperationException($"Current character class has no next generation");
await player.ForEachWorldObserverAsync<ILegacyQuestRewardPlugIn>(
p => p.ShowAsync(
player,
QuestRewardType.CharacterEvolutionFirstToSecond,
reward.Value,
null),
true).ConfigureAwait(false);
break;
case QuestRewardType.CharacterEvolutionSecondToThird:
player.SelectedCharacter!.CharacterClass = player.SelectedCharacter.CharacterClass?.NextGenerationClass
?? throw new InvalidOperationException($"Current character class has no next generation");
await player.ForEachWorldObserverAsync<ILegacyQuestRewardPlugIn>(
p => p.ShowAsync(
player,
QuestRewardType.CharacterEvolutionSecondToThird,
reward.Value,
null),
true).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IUpdateMasterStatsPlugIn>(p => p.SendMasterStatsAsync()).ConfigureAwait(false);
break;
case QuestRewardType.Experience:
await player.AddExperienceAsync(reward.Value, null).ConfigureAwait(false);
break;
case QuestRewardType.Money:
player.TryAddMoney(reward.Value);
await player.InvokeViewPlugInAsync<IUpdateMoneyPlugIn>(p => p.UpdateMoneyAsync()).ConfigureAwait(false);
break;
case QuestRewardType.GensAttribution:
// not yet implemented.
break;
case QuestRewardType.Skill:
if (reward.SkillReward is not { } skill)
{
player.Logger.LogError("Reward has no skill defined.");
return;
}
if (player.SkillList is not { } skillList)
{
player.Logger.LogError("Can't reward the skill; SkillList is null.");
return;
}
if (!skillList.ContainsSkill(skill.Number.ToUnsigned()))
{
await skillList.AddLearnedSkillAsync(skill).ConfigureAwait(false);
}
else
{
player.Logger.LogWarning($"Skill {skill} is already learned.");
}
break;
default:
player.Logger.LogWarning("Unknown reward type: {0}", reward.RewardType);
break;
}
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="QuestExtensions.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.Quests;
using MUnique.OpenMU.Persistence;
/// <summary>
/// Extensions regarding quests.
/// </summary>
public static class QuestExtensions
{
/// <summary>
/// Clears the quest state for the specified player.
/// </summary>
/// <param name="questState">State of the quest.</param>
/// <param name="persistenceContext">The persistence context of the player.</param>
public static async ValueTask ClearAsync(this CharacterQuestState questState, IContext persistenceContext)
{
questState.ActiveQuest = null;
questState.ClientActionPerformed = false;
foreach (var requirementState in questState.RequirementStates)
{
await persistenceContext.DeleteAsync(requirementState).ConfigureAwait(false);
}
questState.RequirementStates.Clear();
}
}

View File

@@ -0,0 +1,56 @@
// <copyright file="QuestSelectAction.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.Quests;
using MUnique.OpenMU.DataModel.Configuration.Quests;
using MUnique.OpenMU.GameLogic.Views.Quest;
/// <summary>
/// A player action which implements the selecting of a quest in the quest list.
/// </summary>
public class QuestSelectAction
{
/// <summary>
/// Tries to select the quest of the given group and number for the specified player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The <see cref="QuestDefinition.Group"/>.</param>
/// <param name="number">The <see cref="QuestDefinition.StartingNumber"/>.</param>
public async ValueTask SelectQuestAsync(Player player, short group, short number)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var quest = player.GetQuest(group, number);
if (quest is null)
{
player.Logger.LogDebug("Failed, quest not found");
return;
}
var questState = player.SelectedCharacter?.QuestStates.FirstOrDefault(q => q.Group == group);
if (questState?.ActiveQuest != null)
{
player.Logger.LogDebug("There is already an active quest of this group.");
await player.InvokeViewPlugInAsync<IQuestProgressPlugIn>(p => p.ShowQuestProgressAsync(questState.ActiveQuest, false)).ConfigureAwait(false);
return;
}
if (quest.Equals(questState?.LastFinishedQuest) && !quest.Repeatable)
{
player.Logger.LogDebug("The quest is not repeatable.");
return;
}
if (quest.MinimumCharacterLevel > player.Level || (quest.MaximumCharacterLevel > 0 && quest.MaximumCharacterLevel < player.Level))
{
player.Logger.LogDebug("Failed, character level {0} not in allowed range {1} to {2}.", player.Level, quest.MinimumCharacterLevel, quest.MaximumCharacterLevel);
return;
}
if (quest.StartingNumber == number && quest.Number != number)
{
await player.InvokeViewPlugInAsync<IQuestStepInfoPlugIn>(p => p.ShowQuestStepInfoAsync(quest.Group, quest.StartingNumber)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,75 @@
// <copyright file="QuestStartAction.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.Quests;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.GameLogic.Views.Quest;
/// <summary>
/// A player action which implements the starting of a quest.
/// </summary>
public class QuestStartAction
{
/// <summary>
/// Tries to start the quest of the given group and number for the specified player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="group">The group.</param>
/// <param name="number">The number.</param>
public async ValueTask StartQuestAsync(Player player, short group, short number)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var quest = player.GetQuest(group, number);
if (quest is null)
{
player.Logger.LogWarning("Failed, quest not found");
return;
}
if (quest.MinimumCharacterLevel > player.Level || (quest.MaximumCharacterLevel > 0 && quest.MaximumCharacterLevel < player.Level))
{
player.Logger.LogDebug("Failed, character level {0} not in allowed range {1} to {2}.", player.Level, quest.MinimumCharacterLevel, quest.MaximumCharacterLevel);
return;
}
var questState = player.SelectedCharacter!.QuestStates.FirstOrDefault(q => q.Group == group);
if (questState is null)
{
questState = player.PersistenceContext.CreateNew<CharacterQuestState>();
questState.Group = group;
player.SelectedCharacter.QuestStates.Add(questState);
}
if (questState.ActiveQuest != null)
{
player.Logger.LogDebug("There is already an active quest of this group.");
await player.InvokeViewPlugInAsync<IQuestProgressPlugIn>(p => p.ShowQuestProgressAsync(questState.ActiveQuest, false)).ConfigureAwait(false);
return;
}
if (Equals(questState.LastFinishedQuest, quest) && !quest.Repeatable)
{
player.Logger.LogDebug("The quest is not repeatable.");
return;
}
if (quest.RequiredStartMoney > 0)
{
if (player.TryRemoveMoney(quest.RequiredStartMoney))
{
await player.InvokeViewPlugInAsync<IUpdateMoneyPlugIn>(p => p.UpdateMoneyAsync()).ConfigureAwait(false);
}
else
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NotEnoughMoneyToProceed)).ConfigureAwait(false);
return;
}
}
await questState.ClearAsync(player.PersistenceContext).ConfigureAwait(false);
questState.ActiveQuest = quest;
await player.InvokeViewPlugInAsync<IQuestStartedPlugIn>(p => p.QuestStartedAsync(quest)).ConfigureAwait(false);
}
}