Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Maple2.File.Ingest/Maple2.File.Ingest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="CsvHelper" Version="32.0.2" />
<PackageReference Include="Maple2.File.Parser.Tadeucci" Version="2.2.6" />
<PackageReference Include="Maple2.File.Parser.Tadeucci" Version="2.2.7" />
<PackageReference Include="DotRecast.Core" Version="2024.2.3" />
<PackageReference Include="DotRecast.Detour" Version="2024.2.3" />
<PackageReference Include="DotRecast.Recast" Version="2024.2.3" />
Expand Down
14 changes: 6 additions & 8 deletions Maple2.File.Ingest/Mapper/AnimationMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,22 @@ public AnimationMapper(M2dReader xmlReader) {
protected override IEnumerable<AnimationMetadata> Map() {
foreach (AnimationData data in parser.Parse()) {
foreach (KeyFrameMotion kfm in data.kfm) {
IEnumerable<(string Name, AnimationSequence Sequence)> sequences = kfm.seq.Select(sequence => {
IEnumerable<(string Name, AnimationSequenceMetadata Sequence)> sequences = kfm.seq.Select(sequence => {
List<AnimationKey> keys = sequence.key.Select(key => new AnimationKey(key.name, (float) key.time)).ToList();
return (sequence.name,
new AnimationSequence(
new AnimationSequenceMetadata(
Name: sequence.name,
Id: (short) sequence.id,
Time: (float) (sequence.key.FirstOrDefault(key => key.name == "end")?.time ?? default), keys)
Time: (float) (sequence.key.FirstOrDefault(key => key.name == "end")?.time ?? 0), keys)
);
});

var lookup = new Dictionary<string, AnimationSequence>();
foreach ((string name, AnimationSequence sequence) in sequences) {
if (lookup.ContainsKey(name)) {
var lookup = new Dictionary<string, AnimationSequenceMetadata>();
foreach ((string name, AnimationSequenceMetadata sequence) in sequences) {
if (!lookup.TryAdd(name, sequence)) {
Console.WriteLine($"Ignore Duplicate: {name} for {kfm.name}");
continue;
}

lookup.Add(name, sequence);
}

yield return new AnimationMetadata(kfm.name, lookup);
Expand Down
71 changes: 67 additions & 4 deletions Maple2.File.Ingest/MapperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using Maple2.File.Ingest.Utils;
using Maple2.File.Parser.Xml;
using Maple2.File.Parser.Xml.Common;
Expand Down Expand Up @@ -322,12 +324,25 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond
Gender: (Gender) beginCondition.gender,
Mesos: beginCondition.money,
Stat: beginCondition.stat.ToDictionary(),
Maps: beginCondition.requireMapCodes.Select(mapCodes => mapCodes.code).ToArray(),
MapTypes: beginCondition.requireMapCategoryCodes.Select(mapType => (MapType) mapType.code).ToArray(),
Continents: beginCondition.requireMapContinentCodes.Select(continent => (Continent) continent.code).ToArray(),
ActiveSkill: beginCondition.requireSkillCodes.Select(skill => skill.code).ToArray(),
JobCode: beginCondition.job.Select(job => (JobCode) job.code).ToArray(),
Probability: beginCondition.probability,
CooldownTime: beginCondition.cooldownTime,
DurationWithoutMoving: (int) TimeSpan.FromSeconds(beginCondition.requireDurationWithoutMove).TotalMilliseconds,
DurationWithoutDamage: (int) TimeSpan.FromSeconds(beginCondition.requireDurationWithoutDamage).TotalMilliseconds,
OnlyShadowWorld: beginCondition.onlyShadowWorld || beginCondition.isShadowWorld,
OnlyFlyableMap: beginCondition.onlyFlyableMap,
OnlySurvival: beginCondition.allowMapleSurvival,
AllowDead: beginCondition.allowDeadState,
AllowOnBattleMount: beginCondition.allowBattleRidingState,
OnlyOnBattleMount: beginCondition.onlyBattleRidingState,
DungeonGroupType: beginCondition.requireDungeonRoomGroupTypes
.Where(type => Enum.TryParse<DungeonGroupType>(type.type, true, out DungeonGroupType _))
.Select(type => Enum.Parse<DungeonGroupType>(type.type, true))
.ToArray(),
Comment thread
Zintixx marked this conversation as resolved.
Weapon: beginCondition.weapon.Select(weapon => new BeginConditionWeapon(
new ItemType(1, (byte) weapon.lh),
new ItemType(1, (byte) weapon.rh))).ToArray(),
Expand All @@ -337,21 +352,47 @@ public static BeginCondition Convert(this Maple2.File.Parser.Xml.Skill.BeginCond
}

// We use this default to avoid writing useless checks
private static readonly BeginConditionTarget DefaultBeginConditionTarget = new(Array.Empty<BeginConditionTarget.HasBuff>(), null);
private static readonly BeginConditionTarget DefaultBeginConditionTarget = new([], null, [], [], [], new Dictionary<MasteryType, int>(), [], []);
private static BeginConditionTarget? Convert(SubConditionTarget? target) {
if (target == null) {
return null;
}

var result = new BeginConditionTarget(
Buff: ParseBuffs(target),
Event: ParseEvent(target));
Event: ParseEvent(target),
Stat: ParseStat(target),
States: target.requireStates
.Select(state => Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == state))
.Where(state => state != ActorState.None)
.ToArray(),
SubStates: target.requireSubStates
.Select(state => Enum.GetValues<ActorSubState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == state))
.Where(state => state != ActorSubState.None)
.ToArray(),
Masteries: target.requireMasteryTypes
.Where(type => Enum.TryParse<MasteryType>(type, true, out MasteryType _))
.Select(type => Enum.Parse<MasteryType>(type, true))
.Zip(target.requireMasteryValues, (type, value) => (Type: type, Value: value))
.ToDictionary(pair => pair.Type, pair => pair.Value),
NpcIds: target.NpcIDs,
HasNotBuffIds: target.hasNotBuffID);

return DefaultBeginConditionTarget.Equals(result) ? null : result;

BeginConditionTarget.HasBuff[] ParseBuffs(SubConditionTarget data) {
if (data.hasBuffID.Length == 0 || data.hasBuffID[0] == 0) {
return Array.Empty<BeginConditionTarget.HasBuff>();
return [];
}

var hasBuff = new BeginConditionTarget.HasBuff[data.hasBuffID.Length];
Expand Down Expand Up @@ -383,6 +424,28 @@ BeginConditionTarget.HasBuff[] ParseBuffs(SubConditionTarget data) {
SkillIds: data.eventSkillID,
BuffIds: data.eventEffectID);
}

BeginConditionTarget.BeginConditionStat[] ParseStat(SubConditionTarget data) {
if (data.compareStat.Count == 0) {
return [];
}

var stats = new BeginConditionTarget.BeginConditionStat[data.compareStat.Count];
for (int i = 0; i < stats.Length; i++) {
foreach (BasicAttribute attribute in Enum.GetValues<BasicAttribute>()) {
float value = data.compareStat[i][(byte) attribute];
if (value != default) {
stats[i] = new BeginConditionTarget.BeginConditionStat(
Attribute: attribute,
Value: value,
Compare: data.compareStat.Count > i ? Enum.Parse<CompareType>(data.compareStat[i].func) : CompareType.Equals,
ValueType: (CompareStatValueType) data.compareStat[i].type);
break;
}
}
}
return stats;
}
}

public static Dictionary<int, IReadOnlyDictionary<int, ItemOption>> ToDictionary(this IEnumerable<ItemOptionData> entries) {
Expand Down
5 changes: 5 additions & 0 deletions Maple2.Model/Enum/CompareType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ public enum CompareType {
Greater = 3,
GreaterEquals = 4,
}

public enum CompareStatValueType {
CurrentPercentage = 0,
TotalValue = 1,
}
4 changes: 2 additions & 2 deletions Maple2.Model/Game/Npc/Npc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ namespace Maple2.Model.Game;

public class Npc {
public readonly NpcMetadata Metadata;
public readonly IReadOnlyDictionary<string, AnimationSequence> Animations;
public readonly IReadOnlyDictionary<string, AnimationSequenceMetadata> Animations;

public int Id => Metadata.Id;

public bool IsBoss => Metadata.Basic.Friendly == 0 && Metadata.Basic.Class >= 3;

public Npc(NpcMetadata metadata, AnimationMetadata? animation) {
Metadata = metadata;
Animations = animation?.Sequences ?? new Dictionary<string, AnimationSequence>();
Animations = animation?.Sequences ?? new Dictionary<string, AnimationSequenceMetadata>();
}
}
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/AnimationMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Maple2.Model.Metadata;

public record AnimationMetadata(string Model, IReadOnlyDictionary<string, AnimationSequence> Sequences);
public record AnimationMetadata(string Model, IReadOnlyDictionary<string, AnimationSequenceMetadata> Sequences);

public record AnimationSequence(string Name, short Id, float Time, List<AnimationKey>? Keys);
public record AnimationSequenceMetadata(string Name, short Id, float Time, List<AnimationKey> Keys);

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

Nullable annotation removed from Keys property

The Keys property is no longer marked as nullable (List<AnimationKey> instead of List<AnimationKey>?). Ensure that all initialization paths provide a non-null list to avoid potential null reference exceptions.


🏁 Script executed:

#!/bin/bash
# Search for places where AnimationSequenceMetadata is created to verify Keys is never null
rg -A 3 "new AnimationSequenceMetadata\(" --type cs

Length of output: 1095


Action Required: Ensure Non-Null Initialization for the Keys Property

Our investigation revealed that the instantiation in Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs is passing null to the Keys parameter:

  • Problematic instance:

    new AnimationSequenceMetadata(string.Empty, -1, 1f, null);

    Since the record now defines Keys as a non-nullable List<AnimationKey>, passing null risks causing a null reference exception.

Next Steps:

  • Update this instantiation to provide a valid non-null List<AnimationKey> (e.g., an empty list) to ensure safe operation.
  • Verify similar instantiation paths across the codebase for consistency.


public record AnimationKey(string Name, float Time);
22 changes: 21 additions & 1 deletion Maple2.Model/Metadata/BeginCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ public record BeginCondition(
JobCode[] JobCode,
float Probability,
float CooldownTime,
int DurationWithoutDamage,
int DurationWithoutMoving,
bool OnlyShadowWorld,
bool OnlyFlyableMap,
bool OnlySurvival,
bool AllowDead,
bool AllowOnBattleMount,
bool OnlyOnBattleMount,
DungeonGroupType[] DungeonGroupType,
IReadOnlyDictionary<BasicAttribute, long> Stat,
int[] Maps,
MapType[] MapTypes,
Continent[] Continents,
int[] ActiveSkill,
BeginConditionWeapon[]? Weapon,
BeginConditionTarget? Target,
BeginConditionTarget? Owner,
Expand All @@ -26,7 +36,14 @@ public record BeginConditionWeapon(

public record BeginConditionTarget(
BeginConditionTarget.HasBuff[] Buff,
BeginConditionTarget.EventCondition? Event
BeginConditionTarget.EventCondition? Event,
BeginConditionTarget.BeginConditionStat[] Stat,
ActorState[] States,
ActorSubState[] SubStates,
IReadOnlyDictionary<MasteryType, int> Masteries,
//string[] NpcTags // not used?
int[] NpcIds,
int[] HasNotBuffIds
) {
public record HasBuff(int Id, short Level, bool Owned, int Count, CompareType Compare);

Expand All @@ -35,4 +52,7 @@ public record HasBuff(int Id, short Level, bool Owned, int Count, CompareType Co
// SkillIds => 4,6,7,14,20
// BuffIds => 16,17,102
public record EventCondition(EventConditionType Type, bool IgnoreOwner, int[] SkillIds, int[] BuffIds);
public record BeginConditionStat(BasicAttribute Attribute, float Value, CompareType Compare, CompareStatValueType ValueType);
}


3 changes: 2 additions & 1 deletion Maple2.Server.Game/Commands/DebugCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Numerics;
using Maple2.Model.Common;
using System;
using Maple2.Server.Game.Model;

namespace Maple2.Server.Game.Commands;

Expand Down Expand Up @@ -94,7 +95,7 @@ public DebugAnimationCommand(GameSession session) : base("anims", "Prints player
}

private void Handle(InvocationContext ctx, bool? enabled) {
session.Player.AnimationState.DebugPrintAnimations = enabled ?? true;
session.Player.Animation.DebugPrintAnimations = enabled ?? true;

string message = enabled ?? true ? "Enabled" : "Disabled";
ctx.Console.Out.WriteLine($"{message} animation debug info printing");
Expand Down
Loading