Skip to content
Merged
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
62 changes: 62 additions & 0 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,68 @@ public PlayerCommand(GameSession session, AchievementMetadataStorage achievement
AddCommand(new CurrencyCommand(session));
AddCommand(new InventoryCommand(session));
AddCommand(new TrophyCommand(session, achievementMetadataStorage));
AddCommand(new MasteryCommand(session));
}

private class MasteryCommand : Command {
public MasteryCommand(GameSession session) : base("mastery", "Set player mastery.") {
AddCommand(new MasteryExpCommand(session));
AddCommand(new MasteryLevelCommand(session));
}

private class MasteryExpCommand : Command {
private readonly GameSession session;

public MasteryExpCommand(GameSession session) : base("exp", "Set player mastery experience.") {
this.session = session;

var masteryCode = new Argument<MasteryType>("mastery", "MasteryType of the player.");
var exp = new Argument<int>("exp", "Experience points to add.");

AddArgument(masteryCode);
AddArgument(exp);
this.SetHandler<InvocationContext, MasteryType, int>(Handle, masteryCode, exp);
}
Comment on lines +40 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify MasteryType names vs PR examples; consider documenting accepted values or mapping synonyms.

Your PR text lists “Music, Gathering, Breeding, Engraving” but Model/Mastery properties use “Instrument, Foraging, Ranching, Handicrafts.” If the enum uses the latter names, System.CommandLine will reject the former. Either:

  • Update PR examples to match the actual enum names; and/or
  • Accept common synonyms by parsing a string argument and mapping to MasteryType.

Run this to confirm the enum members used at runtime:

If synonyms are needed and you want a lightweight change, I can propose a small mapper without touching other callsites. Want me to draft it?

Also applies to: 65-71


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the MasteryType enum declaration and members
rg -nPU '(?s)\benum\s+MasteryType\s*{.*?}' -C2 --glob '!**/bin/**' --glob '!**/obj/**' | sed -n '/enum MasteryType/,/}/p'

Length of output: 143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "🔍 Searching for any references to MasteryType in C# files..."
rg -n --glob '*.cs' '\bMasteryType\b' -C2 --glob '!**/bin/**' --glob '!**/obj/**' || true

echo
echo "🔍 Looking for a file named MasteryType.cs..."
fd --type f MasteryType.cs || true

# If it exists at root or in a discovered path, dump its declaration
if [ -f MasteryType.cs ]; then
  echo
  echo "📄 Contents of MasteryType.cs (first 200 lines):"
  sed -n '1,200p' MasteryType.cs
fi

Length of output: 21794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Dump the contents of the MasteryType enum file for inspection
echo "📄 Showing the MasteryType enum declaration:"
sed -n '1,200p' Maple2.Model/Enum/MasteryType.cs

Length of output: 451


Document or Align MasteryType Inputs

Please ensure that the command-line examples and handlers use the exact enum identifiers defined in Maple2.Model.Enum.MasteryType (Unknown, Fishing, Music, Mining, Gathering, Breeding, Farming, Blacksmithing, Engraving, Alchemist, Cooking, PetTaming). Currently, System.CommandLine will only accept those names (e.g. “Music”) and numeric values, not the game-facing synonyms (“Instrument”, “Foraging”, “Ranching”, “Handicrafts”).

• Update your PR’s documentation/examples in

  • Maple2.Server.Game/Commands/PlayerCommand.cs (constructor overloads at lines 40–46 and 65–71)
    to list the actual enum values, for instance:
- // Example: mastery Music exp 100
+ // Example: mastery Music exp 100

• If you’d like to support synonyms, replace the raw Argument<MasteryType> with Argument<string> (or keep both) and map input strings to the correct enum before handling. For example:

- var masteryCode = new Argument<MasteryType>("mastery", "MasteryType of the player.");
+ var masteryInput = new Argument<string>("mastery", "MasteryType (e.g. Music, Instrument).");
  var exp = new Argument<int>("exp", "Experience points to add.");
   AddArgument(masteryInput);
   AddArgument(exp);
-  this.SetHandler<InvocationContext, MasteryType, int>(Handle, masteryCode, exp);
+  this.SetHandler<InvocationContext, string, int>((ctx, input, e) =>
+  {
+    var mastery = ParseMastery(input);
+    Handle(ctx, mastery, e);
+  }, masteryInput, exp);

+ static MasteryType ParseMastery(string s) =>
+   s.ToLowerInvariant() switch
+   {
+     "music" or "instrument" => MasteryType.Music,
+     "gathering" or "foraging" => MasteryType.Gathering,
+     "breeding" or "ranching" => MasteryType.Breeding,
+     "engraving" or "handicrafts" => MasteryType.Engraving,
+     _ when Enum.TryParse<MasteryType>(s, true, out var m) => m,
+     _ => throw new ArgumentException($"Unknown mastery '{s}'", nameof(s))
+   };

Let me know if you’d like a fuller PR to add this mapper.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Maple2.Server.Game/Commands/PlayerCommand.cs around lines 40–46 (and also
apply to the constructor overloads at lines 65–71), the command currently
declares Argument<MasteryType> which leads to confusion because
System.CommandLine only accepts the enum identifiers (Unknown, Fishing, Music,
Mining, Gathering, Breeding, Farming, Blacksmithing, Engraving, Alchemist,
Cooking, PetTaming) and numeric values; update the argument help text/examples
to list those exact enum names, or change the signature to accept
Argument<string> (or add a second string argument) and perform a mapping from
input synonyms (e.g., "Instrument" -> "Music", "Foraging" -> "Gathering",
"Ranching" -> "Breeding", "Handicrafts" -> "Blacksmithing") to the MasteryType
enum before invoking Handle; finally ensure invalid inputs produce a clear error
and update the constructor documentation/examples accordingly.


private void Handle(InvocationContext ctx, MasteryType masteryType, int exp) {
try {
session.Mastery[masteryType] = exp;
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}
}

private class MasteryLevelCommand : Command {
private readonly GameSession session;

public MasteryLevelCommand(GameSession session) : base("level", "Set player mastery level.") {
this.session = session;

var masteryCode = new Argument<MasteryType>("mastery", "MasteryType of the player.");
var level = new Argument<int>("level", "Level of the mastery.");

AddArgument(masteryCode);
AddArgument(level);
this.SetHandler<InvocationContext, MasteryType, int>(Handle, masteryCode, level);
}

private void Handle(InvocationContext ctx, MasteryType masteryType, int level) {
try {
int exp = 0;
if (session.TableMetadata.MasteryRewardTable.Entries.TryGetValue(masteryType, out IReadOnlyDictionary<int, MasteryRewardTable.Entry>? masteryRewardMetadata)) {
exp = masteryRewardMetadata.OrderByDescending(mastery => mastery.Key).FirstOrDefault(mastery => level >= mastery.Key).Value.Value;
}
session.Mastery[masteryType] = exp;
ctx.ExitCode = 0;
Comment on lines +73 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid NullReference when no threshold ≤ level; handle missing metadata and invalid levels.

If there’s no mastery grade ≤ requested level, FirstOrDefault returns a default KVP and .Value.Value will throw. Also, missing mastery metadata currently results in silently setting exp=0. Handle both explicitly and fail fast.

Apply this diff to make the computation robust and user-friendly:

             private void Handle(InvocationContext ctx, MasteryType masteryType, int level) {
                 try {
-                    int exp = 0;
-                    if (session.TableMetadata.MasteryRewardTable.Entries.TryGetValue(masteryType, out IReadOnlyDictionary<int, MasteryRewardTable.Entry>? masteryRewardMetadata)) {
-                        exp = masteryRewardMetadata.OrderByDescending(mastery => mastery.Key).FirstOrDefault(mastery => level >= mastery.Key).Value.Value;
-                    }
-                    session.Mastery[masteryType] = exp;
-                    ctx.ExitCode = 0;
+                    if (!session.TableMetadata.MasteryRewardTable.Entries.TryGetValue(masteryType, out IReadOnlyDictionary<int, MasteryRewardTable.Entry>? masteryRewardMetadata)) {
+                        ctx.Console.Error.WriteLine($"Mastery metadata not found for {masteryType}.");
+                        ctx.ExitCode = 1;
+                        return;
+                    }
+
+                    if (level < 1) {
+                        ctx.Console.Error.WriteLine($"Invalid mastery level: {level}. Must be >= 1.");
+                        ctx.ExitCode = 1;
+                        return;
+                    }
+
+                    // Highest grade threshold <= requested level
+                    int applicableGrade = masteryRewardMetadata.Keys.Where(k => k <= level).DefaultIfEmpty(-1).Max();
+                    if (applicableGrade < 0 || !masteryRewardMetadata.TryGetValue(applicableGrade, out var entry)) {
+                        ctx.Console.Error.WriteLine($"No mastery grade threshold found for level {level} ({masteryType}).");
+                        ctx.ExitCode = 1;
+                        return;
+                    }
+
+                    session.Mastery[masteryType] = entry.Value;
+                    ctx.ExitCode = 0;
                 } catch (SystemException ex) {
                     ctx.Console.Error.WriteLine(ex.Message);
                     ctx.ExitCode = 1;
                 }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void Handle(InvocationContext ctx, MasteryType masteryType, int level) {
try {
int exp = 0;
if (session.TableMetadata.MasteryRewardTable.Entries.TryGetValue(masteryType, out IReadOnlyDictionary<int, MasteryRewardTable.Entry>? masteryRewardMetadata)) {
exp = masteryRewardMetadata.OrderByDescending(mastery => mastery.Key).FirstOrDefault(mastery => level >= mastery.Key).Value.Value;
}
session.Mastery[masteryType] = exp;
ctx.ExitCode = 0;
private void Handle(InvocationContext ctx, MasteryType masteryType, int level) {
try {
if (!session.TableMetadata.MasteryRewardTable.Entries.TryGetValue(masteryType, out IReadOnlyDictionary<int, MasteryRewardTable.Entry>? masteryRewardMetadata)) {
ctx.Console.Error.WriteLine($"Mastery metadata not found for {masteryType}.");
ctx.ExitCode = 1;
return;
}
if (level < 1) {
ctx.Console.Error.WriteLine($"Invalid mastery level: {level}. Must be >= 1.");
ctx.ExitCode = 1;
return;
}
// Highest grade threshold <= requested level
int applicableGrade = masteryRewardMetadata.Keys
.Where(k => k <= level)
.DefaultIfEmpty(-1)
.Max();
if (applicableGrade < 0
|| !masteryRewardMetadata.TryGetValue(applicableGrade, out var entry)) {
ctx.Console.Error.WriteLine($"No mastery grade threshold found for level {level} ({masteryType}).");
ctx.ExitCode = 1;
return;
}
session.Mastery[masteryType] = entry.Value;
ctx.ExitCode = 0;

} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}
}
}

private class LevelCommand : Command {
Expand Down
Loading