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
4 changes: 4 additions & 0 deletions Maple2.Database/Storage/Metadata/QuestMetadataStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public IEnumerable<QuestMetadata> GetQuestsByType(QuestType type) {
return GetQuests().Where(x => x.Basic.Type == type);
}

public IEnumerable<QuestMetadata> GetQuestsByChapter(int chapterId) {
return GetQuests().Where(x => x.Basic.ChapterId == chapterId);
}

public List<QuestMetadata> Search(string name) {
return GetQuests().Where(x => x.Name != null && x.Name.Contains(name, StringComparison.OrdinalIgnoreCase)).ToList();
}
Expand Down
6 changes: 3 additions & 3 deletions Maple2.File.Ingest/Mapper/AdditionalEffectMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,13 @@ private static AdditionalEffectMetadataStatus Convert(StatusProperty status, Off
resistances.AddIfNotDefault(BasicAttribute.AttackSpeed, status.resAspR);

Debug.Assert(status.compulsionEventTypes.Length <= 1 && status.compulsionEventRate.Length <= 1);
var compulsionEventType = CompulsionEventType.None;
var compulsionEventType = BuffCompulsionEventType.None;
if (status.compulsionEventTypes.Length > 0) {
compulsionEventType = (CompulsionEventType) status.compulsionEventTypes[0];
compulsionEventType = (BuffCompulsionEventType) status.compulsionEventTypes[0];
}

AdditionalEffectMetadataStatus.CompulsionEvent? compulsionEvent = null;
if (compulsionEventType != CompulsionEventType.None) {
if (compulsionEventType != BuffCompulsionEventType.None) {
float compulsionEventRate = 0;
if (status.compulsionEventRate.Length > 0) {
compulsionEventRate = status.compulsionEventRate[0];
Expand Down
21 changes: 17 additions & 4 deletions Maple2.File.Ingest/Mapper/SkillMapper.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.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Xml.Skill;
Expand All @@ -15,10 +17,13 @@ public SkillMapper(M2dReader xmlReader) {
}

protected override IEnumerable<StoredSkillMetadata> Map() {
List<long> magicPaths = [];
List<long> cubeMagicPaths = [];
foreach ((int id, string name, SkillData data) in parser.Parse()) {
if (data.basic == null) continue; // Old_JobChange_01
Debug.Assert(data.basic.kinds.groupIDs.Length <= 1);


// Note: 90000775 has cubeMagicPathID="2147483647" which should be cubeMagicPathID="9000073111"
Dictionary<short, SkillMetadataLevel> levels = data.level.ToDictionary(
level => level.value,
Expand Down Expand Up @@ -65,7 +70,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
Explosion: attack.arrowProperty.explosion,
RayPhysXTest: attack.arrowProperty.rayPhysxTest,
NonTarget: (SkillTargetType) attack.arrowProperty.nonTarget,
BounceType: attack.arrowProperty.bounceType,
BounceType: (BounceType) attack.arrowProperty.bounceType,
BounceCount: attack.arrowProperty.bounceCount,
BounceRadius: attack.arrowProperty.bounceRadius,
BounceOverlap: attack.arrowProperty.bounceType > 0 && attack.arrowProperty.bounceOverlap,
Expand Down Expand Up @@ -111,7 +116,14 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
RangeType: (RangeType) data.basic.kinds.rangeType,
AttackType: (AttackType) data.basic.ui.attackType,
Element: (Element) data.basic.kinds.element,
State: Enum.TryParse(data.basic.kinds.state, true, out ActorState state) ? state : ActorState.None,
State: string.IsNullOrEmpty(data.basic.kinds.state)
? ActorState.None
: Enum.GetValues<ActorState>()
.FirstOrDefault(enumValue =>
enumValue.GetType()
.GetField(enumValue.ToString())
?.GetCustomAttribute<DescriptionAttribute>()
?.Description == data.basic.kinds.state),
ContinueSkill: data.basic.kinds.continueSkill,
SpRecoverySkill: data.basic.kinds.spRecoverySkill,
ImmediateActive: data.basic.kinds.immediateActive,
Expand All @@ -123,7 +135,7 @@ protected override IEnumerable<StoredSkillMetadata> Map() {
MaxLevel: levels.Keys.Max()),
State: new SkillMetadataState(
InBattle: data.basic.stateAttr.battle == 1,
SuperArmor: data.basic.stateAttr.superArmor,
SuperArmor: (SuperArmor) data.basic.stateAttr.superArmor,
UseInGameTime: data.basic.stateAttr.useInGameTime == 1,
IgnoreReduceCooldown: data.basic.stateAttr.ignoreReduceCooldown == 1,
CooldownGroupId: data.basic.stateAttr.cooldownGroupID,
Expand All @@ -138,6 +150,7 @@ private static SkillMetadataRange Convert(RegionSkill region) {
Type: region.rangeType switch {
"box" => SkillRegion.Box,
"cylinder" => SkillRegion.Cylinder,
"circle" => SkillRegion.Cylinder,
"frustum" => SkillRegion.Frustum,
"hole_cylinder" => SkillRegion.HoleCylinder,
"1200" => SkillRegion.None, // skill/60/60012051.xml
Expand Down
9 changes: 8 additions & 1 deletion Maple2.Model/Enum/Buff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum BuffCategory {
Unknown4 = 4,
EnemyDot = 6,
Stunned = 7, // ?
Slow = 8, // ?
Slow = 8,
BossResistance = 9,
Unknown99 = 99,
MonsterStunned = 1007, // ?
Expand Down Expand Up @@ -91,3 +91,10 @@ public enum InvokeEffectType : byte {
// 57 (90050351) // Triggered from 10500061 (Sharp Eyes) -
IncreaseHealing = 58,
}

public enum BuffCompulsionEventType : byte {
None = 0,
CritChanceOverride = 1,
EvasionChanceOverride = 2,
BlockChance = 3,
}
8 changes: 0 additions & 8 deletions Maple2.Model/Enum/CompulsionEventType.cs

This file was deleted.

22 changes: 19 additions & 3 deletions Maple2.Model/Enum/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ public enum EventConditionType {

public enum CompulsionType {
None = 0,
Critical = 1,
Evasion = 2,
Block = 3,
Hit = 1,
Critical = 2,
Interrupt = 3, // unconfirmed
}

public enum TargetType {
Expand All @@ -200,3 +200,19 @@ public enum TargetType {

HungryMobs = 8
}

public enum BounceType {
None = 0,
Range = 1, // within range
Chain = 2, // Bounce continues as long as another entity is in range of the last bounce
Pierce = 3, // Linear
Boomerang = 4, // Shield Toss, Shadow Cutter
Unknown5 = 5,
}

[Flags]
public enum SuperArmor {
None = 0,
StunImmunity = 1,
KnockbackImmunity = 2,
}
2 changes: 1 addition & 1 deletion Maple2.Model/Metadata/AdditionalEffectMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public record AdditionalEffectMetadataStatus(
int ImmuneBreak,
bool Invincible) {

public record CompulsionEvent(CompulsionEventType Type, float Rate, int[] SkillIds);
public record CompulsionEvent(BuffCompulsionEventType Type, float Rate, int[] SkillIds);

public record StatConversion(BasicAttribute BaseAttribute, BasicAttribute ResultAttribute, float Rate);
}
Expand Down
1 change: 1 addition & 0 deletions Maple2.Model/Metadata/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public static class Constant {
public const int Grade1WeddingCouponItemId = 20303166;
public const int Grade2WeddingCouponItemId = 20303167;
public const int Grade3WeddingCouponItemId = 20303168;
public const int MinStatIntervalTick = 100;

public const int MaxMentees = 3;

Expand Down
4 changes: 2 additions & 2 deletions Maple2.Model/Metadata/SkillMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public record SkillMetadataProperty(

public record SkillMetadataState(
bool InBattle,
int SuperArmor, // 0, 1, 3
SuperArmor SuperArmor,
bool UseInGameTime,
int CooldownGroupId,
bool IgnoreReduceCooldown,
Expand Down Expand Up @@ -139,7 +139,7 @@ public record SkillMetadataArrow(
bool Explosion,
bool RayPhysXTest,
SkillTargetType NonTarget,
int BounceType, // 2: chain, 3: pierce
BounceType BounceType,
int BounceCount,
float BounceRadius,
bool BounceOverlap,
Expand Down
165 changes: 116 additions & 49 deletions Maple2.Server.Game/Commands/PlayerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public ExpCommand(GameSession session) : base("exp", "Add player experience.") {

private void Handle(InvocationContext ctx, long exp) {
try {
session.Exp.AddExp(ExpType.none, exp);
session.Exp.AddExp(ExpType.expDrop, exp);

ctx.ExitCode = 0;
} catch (SystemException ex) {
Expand Down Expand Up @@ -138,52 +138,128 @@ public JobCommand(GameSession session) : base("job", "Set player job.") {

private void Handle(InvocationContext ctx, JobCode jobCode, bool awakening) {
try {
Job job = jobCode switch {
Job selectedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => awakening ? Job.KnightII : Job.Knight,
JobCode.Berserker => awakening ? Job.BerserkerII : Job.Berserker,
JobCode.Wizard => awakening ? Job.WizardII : Job.Wizard,
JobCode.Priest => awakening ? Job.PriestII : Job.Priest,
JobCode.Archer => awakening ? Job.ArcherII : Job.Archer,
JobCode.HeavyGunner => awakening ? Job.HeavyGunnerII : Job.HeavyGunner,
JobCode.Thief => awakening ? Job.ThiefII : Job.Thief,
JobCode.Assassin => awakening ? Job.AssassinII : Job.Assassin,
JobCode.RuneBlader => awakening ? Job.RuneBladerII : Job.RuneBlader,
JobCode.Striker => awakening ? Job.StrikerII : Job.Striker,
JobCode.SoulBinder => awakening ? Job.SoulBinderII : Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}")
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
if (selectedJob == session.Player.Value.Character.Job) {
if (!awakening) {
// nothing to do
ctx.ExitCode = 0;
return;
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
Awaken(ctx, jobCode);
} else {
if (awakening) {
Awaken(ctx, jobCode);
} else {
JobAdvance(selectedJob);
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
ctx.ExitCode = 0;
} catch (SystemException ex) {
ctx.Console.Error.WriteLine(ex.Message);
ctx.ExitCode = 1;
}
}

private void Awaken(InvocationContext ctx, JobCode jobCode) {
Job awakenedJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.KnightII,
JobCode.Berserker => Job.BerserkerII,
JobCode.Wizard => Job.WizardII,
JobCode.Priest => Job.PriestII,
JobCode.Archer => Job.ArcherII,
JobCode.HeavyGunner => Job.HeavyGunnerII,
JobCode.Thief => Job.ThiefII,
JobCode.Assassin => Job.AssassinII,
JobCode.RuneBlader => Job.RuneBladerII,
JobCode.Striker => Job.StrikerII,
JobCode.SoulBinder => Job.SoulBinderII,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};
Job baseJob = jobCode switch {
JobCode.Newbie => Job.Newbie,
JobCode.Knight => Job.Knight,
JobCode.Berserker => Job.Berserker,
JobCode.Wizard => Job.Wizard,
JobCode.Priest => Job.Priest,
JobCode.Archer => Job.Archer,
JobCode.HeavyGunner => Job.HeavyGunner,
JobCode.Thief => Job.Thief,
JobCode.Assassin => Job.Assassin,
JobCode.RuneBlader => Job.RuneBlader,
JobCode.Striker => Job.Striker,
JobCode.SoulBinder => Job.SoulBinder,
_ => throw new ArgumentException($"Invalid JobCode: {jobCode}"),
};

if (!session.TableMetadata.ChangeJobTable.Entries.TryGetValue(baseJob, out ChangeJobMetadata? changeJobMetadata)) {
ctx.Console.Error.WriteLine($"Invalid JobCode: {jobCode}");
return;
}

if (!session.QuestMetadata.TryGet(changeJobMetadata.StartQuestId, out QuestMetadata? startQuestMetadata)) {
ctx.Console.Error.WriteLine($"Invalid StartQuestId for awakening: {changeJobMetadata.StartQuestId}");
return;
}
session.Quest.DebugCompleteChapter(startQuestMetadata.Basic.ChapterId);
JobAdvance(awakenedJob);
UnlockMasterSkills();
}

private void JobAdvance(Job job) {
Job currentJob = session.Player.Value.Character.Job;
if (currentJob.Code() != job.Code()) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
skillTab.Skills.Clear();
}
} else if (job < currentJob) {
foreach (SkillTab skillTab in session.Config.Skill.SkillBook.SkillTabs) {
foreach (int skillId in skillTab.Skills.Keys.ToList()) {
if (session.Config.Skill.SkillInfo.GetMainSkill(skillId, SkillRank.Awakening) != null) {
skillTab.Skills.Remove(skillId);
}
}
}
session.Config.Skill.ResetSkills(SkillRank.Awakening);
}

session.Player.Value.Character.Job = job;
session.Config.Skill.SkillInfo.SetJob(job);

session.Player.Buffs.Clear();
session.Player.Buffs.Initialize();
session.Player.Buffs.LoadFieldBuffs();
session.Stats.Refresh();
session.Field?.Broadcast(JobPacket.Advance(session.Player, session.Config.Skill.SkillInfo));
}

private void UnlockMasterSkills() {
const int masterSkillQuestId = 40002795;
if (session.Quest.TryGetQuest(masterSkillQuestId, out Quest? quest) && quest.State == QuestState.Completed) {
return;
}

session.Quest.Start(masterSkillQuestId, true);
if (!session.Quest.TryGetQuest(masterSkillQuestId, out quest)) {
return;
}
session.Quest.Complete(quest, true);
}
}

private class InfoCommand : Command {
Expand Down Expand Up @@ -424,21 +500,12 @@ private void Handle(InvocationContext ctx, string trophyId, short grade) {
}

private void UnlockAllTrophies(InvocationContext ctx) {
try {
ICollection<AchievementMetadata> achievementMetadataCollection = achievementMetadataStorage.GetAll();
foreach (AchievementMetadata metadata in achievementMetadataCollection) {
int trophyId = metadata.Id;
int maxGrade = metadata.Grades.Keys.Max();

UnlockTrophy(trophyId, metadata, maxGrade);
}
ctx.Console.Out.WriteLine($"Unlocking all trophies... This may take a few seconds");
session.Achievement.DebugCompleteAllTrophies();
session.Achievement.Load();

ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
} catch (Exception ex) {
ctx.Console.Error.WriteLine($"Failed to unlock all trophies: {ex.Message}");
ctx.ExitCode = 1;
}
ctx.Console.Out.WriteLine("All trophies have been successfully unlocked to their maximum grades.");
ctx.ExitCode = 0;
}

private void UnlockSingleTrophy(InvocationContext ctx, int trophyId, short grade) {
Expand Down
Loading