Skip to content
Closed
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
30 changes: 30 additions & 0 deletions docs/Packets/C2-F5-02-WeeklyQuestEntry_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# C2 F5 02 - WeeklyQuestEntry (by server)

## Is sent when

After the client requested the list of available chat commands, one message is sent for each active weekly quest. When the progress of a quest changes, a single message is sent as update of this quest.

## Causes the following actions on the client side

The client shows the weekly quests and their progress in a window.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
| 1 | 2 | Short | 520 | Packet header - length of the packet |
| 3 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 4 | 1 | Byte | 0x02 | Packet header - sub packet type identifier |
| 5 | 1 | Byte | | Index; The index of this quest within the list, starting at 0. The first message of a list replaces the previously known quests. |
| 6 | 1 | Byte | | Count; The total number of quests. A list without quests is sent as a single message with a count of 0. |
| 7 | 1 | Boolean | | IsUpdate; If true, this message updates the already known quest with the same id, instead of being part of a list. |
| 8 | 1 | Boolean | | IsCompleted; The objective of the quest has been reached. |
| 9 | 1 | Boolean | | IsRewarded; The rewards of the quest have been handed out. If the quest is completed but not rewarded, the reward is pending, e.g. because the inventory was full. |
| 12 | 4 | IntegerLittleEndian | | CurrentCount |
| 16 | 4 | IntegerLittleEndian | | RequiredCount |
| 20 | 4 | IntegerLittleEndian | | SecondsUntilReset; The seconds until the weekly progress is reset. |
| 24 | 64 | String | | Id; The identifier of the quest. |
| 88 | 48 | String | | Name |
| 136 | 256 | String | | Description |
| 392 | 128 | String | | Rewards; The rewards of the quest as text, in the language of the player. |
39 changes: 39 additions & 0 deletions docs/Packets/C2-F5-03-QuestDetails_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# C2 F5 03 - QuestDetails (by server)

## Is sent when

Right after each WeeklyQuestEntry message, with the same quest id. Clients which don't know it can ignore it and just show the WeeklyQuestEntry.

## Causes the following actions on the client side

The client shows the quest under the tab of its category, with a checklist of its objectives (steps).

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
| 1 | 2 | Short | | Packet header - length of the packet |
| 3 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 4 | 1 | Byte | 0x03 | Packet header - sub packet type identifier |
| 5 | 1 | Byte | | Category; The category of the quest: 0 = weekly, 1 = daily, 2 = main story, 3 = class, 4 = zone. |
| 6 | 1 | Byte | | Period; When the progress starts over: 0 = weekly, 1 = daily, 2 = never (the quest is done once). |
| 7 | 1 | Byte | | CurrentStep; The index of the first objective which isn't done yet. It's the number of objectives when all are done. |
| 8 | 1 | Byte | | ObjectiveCount |
| 9 | 1 | Boolean | | IsSequential; If true, the objectives have to be done in their order. |
| 12 | 4 | IntegerLittleEndian | | SecondsUntilReset; The seconds until the progress of this quest starts over. 0 for quests which are done once. |
| 16 | 64 | String | | Id; The identifier of the quest, as in the WeeklyQuestEntry message. |
| 80 | QuestObjective.Length * ObjectiveCount | Array of QuestObjective | | Objectives; The objectives of the quest, in their order. |

### QuestObjective Structure

An objective (step) of a quest.

Length: 76 Bytes

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 4 | IntegerLittleEndian | | CurrentCount |
| 4 | 4 | IntegerLittleEndian | | RequiredCount |
| 8 | 1 | Boolean | | IsDone |
| 12 | 64 | String | | Text; The text of the objective, in the language of the player, e.g. 'Kill 50 Skeletons'. |
37 changes: 37 additions & 0 deletions docs/Packets/C2-F5-04-SkillRequirements_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# C2 F5 04 - SkillRequirements (by server)

## Is sent when

Once after a successful login, before the character selection.

## Causes the following actions on the client side

The client uses these requirements and costs for the rest of the session, instead of the ones of its own data files, so that it shows and checks exactly what the server checks.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
| 1 | 2 | Short | | Packet header - length of the packet |
| 3 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 4 | 1 | Byte | 0x04 | Packet header - sub packet type identifier |
| 6 | 2 | ShortLittleEndian | | SkillCount |
| 8 | SkillRequirement.Length * SkillCount | Array of SkillRequirement | | Skills; The requirements of every skill of the game configuration. |

### SkillRequirement Structure

The requirements and costs of a skill, as the server checks them. A value of 0 means there is no such requirement.

Length: 16 Bytes

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 2 | ShortLittleEndian | | SkillNumber |
| 2 | 2 | ShortLittleEndian | | Level; The required character level. |
| 4 | 2 | ShortLittleEndian | | Energy; The required total energy. |
| 6 | 2 | ShortLittleEndian | | Leadership; The required total leadership (command). |
| 8 | 2 | ShortLittleEndian | | Strength; The required total strength. |
| 10 | 2 | ShortLittleEndian | | Agility; The required total agility. |
| 12 | 2 | ShortLittleEndian | | Mana; The mana which is consumed by using the skill. |
| 14 | 2 | ShortLittleEndian | | AbilityGauge; The ability (AG) which is consumed by using the skill. |
38 changes: 38 additions & 0 deletions docs/Packets/C2-F5-05-LearnableItemRequirements_by-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# C2 F5 05 - LearnableItemRequirements (by server)

## Is sent when

Once after a successful login, right after the SkillRequirements message.

## Causes the following actions on the client side

The client shows these requirements for the items which teach a skill (orbs, scrolls, parchments, crystals), instead of the ones it calculates from its own data files.

## Structure

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | 0xC2 | [Packet type](PacketTypes.md) |
| 1 | 2 | Short | | Packet header - length of the packet |
| 3 | 1 | Byte | 0xF5 | Packet header - packet type identifier |
| 4 | 1 | Byte | 0x05 | Packet header - sub packet type identifier |
| 6 | 2 | ShortLittleEndian | | ItemCount |
| 8 | LearnableItemRequirement.Length * ItemCount | Array of LearnableItemRequirement | | Items; The requirements to learn the skill of each item. |

### LearnableItemRequirement Structure

The requirements to learn a skill with an item: the highest of the requirements of the item and the requirements of the skill. A value of 0 means there is no such requirement.

Length: 16 Bytes

| Index | Length | Data Type | Value | Description |
|-------|--------|-----------|-------|-------------|
| 0 | 1 | Byte | | Group |
| 1 | 1 | Byte | | ItemLevel; The item level these requirements are for, or 0xFF if they apply to every level. Items which teach a different skill per level (Orb of Summoning) have one entry per level. |
| 2 | 2 | ShortLittleEndian | | Number |
| 4 | 2 | ShortLittleEndian | | Level; The required character level. |
| 6 | 2 | ShortLittleEndian | | Energy; The required total energy. |
| 8 | 2 | ShortLittleEndian | | Leadership; The required total leadership (command). |
| 10 | 2 | ShortLittleEndian | | Strength; The required total strength. |
| 12 | 2 | ShortLittleEndian | | Agility; The required total agility. |
| 14 | 2 | ShortLittleEndian | | SkillNumber; The skill which is learned with the item. |
4 changes: 4 additions & 0 deletions docs/Packets/ServerToClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@
* [C1 F3 52 - MasterSkillLevelUpdate (by server)](C1-F3-52-MasterSkillLevelUpdate_by-server.md)
* [C2 F3 53 - MasterSkillList (by server)](C2-F3-53-MasterSkillList_by-server.md)
* [C2 F5 01 - AvailableChatCommand (by server)](C2-F5-01-AvailableChatCommand_by-server.md)
* [C2 F5 02 - WeeklyQuestEntry (by server)](C2-F5-02-WeeklyQuestEntry_by-server.md)
* [C2 F5 03 - QuestDetails (by server)](C2-F5-03-QuestDetails_by-server.md)
* [C2 F5 04 - SkillRequirements (by server)](C2-F5-04-SkillRequirements_by-server.md)
* [C2 F5 05 - LearnableItemRequirements (by server)](C2-F5-05-LearnableItemRequirements_by-server.md)
* [C1 F6 03 - QuestEventResponse (by server)](C1-F6-03-QuestEventResponse_by-server.md)
* [C1 F6 0A - AvailableQuests (by server)](C1-F6-0A-AvailableQuests_by-server.md)
* [C1 F6 0B - QuestStepInfo (by server)](C1-F6-0B-QuestStepInfo_by-server.md)
Expand Down
4 changes: 4 additions & 0 deletions src/Dapr/Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace MUnique.OpenMU.Dapr.Common;
using MUnique.OpenMU.Persistence.AdminAuth;
using MUnique.OpenMU.Persistence.EntityFramework;
using MUnique.OpenMU.Persistence.EntityFramework.AdminAuth;
using MUnique.OpenMU.Persistence.EntityFramework.WeeklyQuests;
using MUnique.OpenMU.Persistence.WeeklyQuests;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx.Synchronous;
using OpenTelemetry.Exporter;
Expand Down Expand Up @@ -57,6 +59,7 @@ public static IServiceCollection AddPeristenceProvider(this IServiceCollection s
.AddSingleton(s => (IPersistenceContextProvider)s.GetService<IMigratableDatabaseContextProvider>()!)
.AddSingleton(s => new Lazy<IPersistenceContextProvider>(s.GetRequiredService<IPersistenceContextProvider>))
.AddAdminUserRepository()
.AddWeeklyQuestProgressRepository()
.AddSingleton<IBackupService>(s => new BackupService(
s.GetRequiredService<IPersistenceContextProvider>(),
s.GetRequiredService<IAdminUserRepository>()))
Expand Down Expand Up @@ -245,6 +248,7 @@ public static WebApplication BuildAndConfigure(this WebApplicationBuilder builde
var useReverseProxy = !string.IsNullOrWhiteSpace(pathBase);

var app = builder.Build();
WeeklyQuestProgressRepositoryRegistry.Current = app.Services.GetService<IWeeklyQuestProgressRepository>();

if (useReverseProxy)
{
Expand Down
6 changes: 6 additions & 0 deletions src/GameLogic/DroppedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ public DroppedItem(Item item, Point position, GameMap map, Player? dropper, IEnu
/// </summary>
public Item Item { get; }

/// <summary>
/// Gets a value indicating whether the item was dropped from the inventory of a player,
/// instead of by a monster, an event or a game master.
/// </summary>
public bool IsDroppedFromInventory { get; init; }

/// <inheritdoc />
public Point Position { get; set; }

Expand Down
11 changes: 11 additions & 0 deletions src/GameLogic/ItemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,17 @@ public static bool IsBook(this Item item, [NotNullWhen(true)] out float? bookRis
return result;
}

/// <summary>
/// Gets the attribute of the character which is compared with a requirement attribute of an item,
/// e.g. <see cref="Stats.TotalEnergy"/> for <see cref="Stats.TotalEnergyRequirementValue"/>.
/// </summary>
/// <param name="requirementAttribute">The attribute of the requirement.</param>
/// <returns>The attribute of the character which has to reach the required value.</returns>
public static AttributeDefinition GetRequiredAttribute(AttributeDefinition requirementAttribute)
{
return RequirementAttributeMapping.TryGetValue(requirementAttribute, out var totalAttribute) ? totalAttribute : requirementAttribute;
}

/// <summary>
/// Gets the requirement as a tuple of an attribute and the corresponding value.
/// </summary>
Expand Down
99 changes: 99 additions & 0 deletions src/GameLogic/LearnableSkillRequirements.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// <copyright file="LearnableSkillRequirements.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameLogic;

using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration.Items;

/// <summary>
/// The skill which an item (orb, scroll, parchment, crystal) teaches, and what it takes to learn it.
/// </summary>
/// <remarks>
/// A skill which can't be used after learning it shouldn't be learnable either, so learning it
/// requires the highest of the requirements of the item and the requirements of the skill.
/// </remarks>
public static class LearnableSkillRequirements
{
/// <summary>
/// Gets the skill which is learned by consuming an item.
/// </summary>
/// <param name="definition">The definition of the item.</param>
/// <param name="itemLevel">The level of the item.</param>
/// <param name="configuration">The game configuration.</param>
/// <returns>The skill, or <c>null</c> if the item teaches none.</returns>
public static Skill? GetLearnableSkill(ItemDefinition definition, byte itemLevel, GameConfiguration configuration)
{
if (definition.Skill is not { } skill)
{
return null;
}

// There is only one Orb of Summoning, which teaches a different skill per item level.
if (IsSummoningOrb(definition))
{
var skillNumber = skill.Number + itemLevel;
return configuration.Skills.FirstOrDefault(s => s.Number == skillNumber);
}

return skill;
}

/// <summary>
/// Determines whether the item teaches a different skill per item level.
/// </summary>
/// <param name="definition">The definition of the item.</param>
/// <returns><c>true</c>, if it's the Orb of Summoning.</returns>
public static bool IsSummoningOrb(ItemDefinition definition)
{
return definition.Group == ItemConstants.SummonOrb.Group && definition.Number == ItemConstants.SummonOrb.Number;
}

/// <summary>
/// Gets the requirements to learn the skill with the item, by the attribute of the character which has to reach them.
/// </summary>
/// <param name="definition">The definition of the item. It's not wearable, so its requirements don't depend on its level or options.</param>
/// <param name="skill">The skill which is learned with the item.</param>
/// <returns>The required value per attribute.</returns>
public static IReadOnlyDictionary<AttributeDefinition, int> GetRequirements(ItemDefinition definition, Skill skill)
{
var result = new Dictionary<AttributeDefinition, int>();
foreach (var requirement in definition.Requirements)
{
if (requirement.Attribute is { } attribute)
{
Raise(result, ItemExtensions.GetRequiredAttribute(attribute), requirement.MinimumValue);
}
}

foreach (var requirement in skill.Requirements)
{
if (requirement.Attribute is { } attribute)
{
Raise(result, attribute, requirement.MinimumValue);
}
}

return result;
}

/// <summary>
/// Determines whether the player complies with the requirements to use the skill.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="skill">The skill.</param>
/// <returns><c>true</c>, if the player can use the skill.</returns>
public static bool CompliesRequirements(this Player player, Skill skill)
{
return skill.Requirements.All(r => r.Attribute is null || player.Attributes![r.Attribute] >= r.MinimumValue);
}

private static void Raise(Dictionary<AttributeDefinition, int> requirements, AttributeDefinition attribute, int value)
{
if (!requirements.TryGetValue(attribute, out var current) || current < value)
{
requirements[attribute] = value;
}
}
}
5 changes: 5 additions & 0 deletions src/GameLogic/MiniGames/MiniGameContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,11 @@ private async ValueTask StopAsync()
var players = await this._players.GetSnapshotAsync().ConfigureAwait(false);

await this.GameEndedAsync(players).ConfigureAwait(false);

if (this._gameContext.PlugInManager.GetPlugInPoint<IMiniGameEndedPlugIn>() is { } plugInPoint)
{
await plugInPoint.MiniGameEndedAsync(this, players).ConfigureAwait(false);
}
}

private GameMap CreateMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item,
/// <inheritdoc />
protected override bool CheckPreconditions(Player player, Item item)
{
return base.CheckPreconditions(player, item)
&& player.CompliesRequirements(item);
if (!base.CheckPreconditions(player, item) || !player.CompliesRequirements(item))
{
return false;
}

// A skill which can't be used after learning it can't be learned either.
return this.GetLearnableSkill(item, player.GameContext.Configuration) is not { } skill
|| player.CompliesRequirements(skill);
}

/// <summary>
Expand All @@ -52,6 +58,8 @@ protected override bool CheckPreconditions(Player player, Item item)
/// <returns>The skill to learn.</returns>
protected virtual Skill? GetLearnableSkill(Item item, GameConfiguration gameConfiguration)
{
return item.Definition?.Skill;
return item.Definition is { } definition
? LearnableSkillRequirements.GetLearnableSkill(definition, item.Level, gameConfiguration)
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
/// <summary>
/// The summoning orb consume handler.
/// There is only one "Orb" item definition which allows to learn different skills, depending on the item level.
/// This consume handler determines the skill by adding the item level to the skill number
/// of the <see cref="ItemDefinition.Skill"/>.
/// The skill is determined by adding the item level to the skill number of the <see cref="ItemDefinition.Skill"/>,
/// see <see cref="LearnableSkillRequirements.GetLearnableSkill"/>.
/// </summary>
[Guid("71C8E542-4868-487E-BC92-0B7CC7CAEC8B")]
[PlugIn]
Expand All @@ -21,21 +21,4 @@ public class SummoningOrbConsumeHandlerPlugIn : LearnablesConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SummonOrb;

/// <summary>
/// Gets the learnable skill.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="gameConfiguration">The game configuration.</param>
/// <returns>
/// The skill to learn.
/// </returns>
protected override Skill GetLearnableSkill(Item item, GameConfiguration gameConfiguration)
{
item.ThrowNotInitializedProperty(item.Definition?.Skill is null, "Definition.Skill");

var baseSkillNumber = item.Definition.Skill.Number;
var targetSkillNumber = baseSkillNumber + item.Level;
return gameConfiguration.Skills.First(s => s.Number == targetSkillNumber);
}
}
Loading