Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c5c5d5
Add Constants WIP
mfranca0009 Feb 20, 2026
55c8d2a
Organize and clean-up old constants values WIP. Start using new serve…
mfranca0009 Feb 23, 2026
e11eec4
Continue switching hard coded constants to server constants parsed du…
mfranca0009 Feb 23, 2026
af10dca
Continue switching hard coded constants to server constants pared dur…
mfranca0009 Feb 24, 2026
3f5414d
Continue switching hard coded constants to server constants pared dur…
mfranca0009 Feb 26, 2026
f633b68
Fix last hard coded constant value in PartyManager. Add in input clea…
mfranca0009 Feb 27, 2026
24c70fa
Remove hard coded constants and utilize server constants within Inven…
mfranca0009 Feb 28, 2026
dff14e7
Update the constant parsing to make it more efficient by removing the…
mfranca0009 Mar 2, 2026
5d6f087
Correct some misspelt property names in ConstantsTable record to allo…
mfranca0009 Mar 2, 2026
a31f930
Add Xml.m2d constants.xml within the Server.m2d constants.xml parsing…
mfranca0009 Mar 2, 2026
d1c3b13
Fix a crash at character entering world due to JSON deserialization c…
mfranca0009 Mar 2, 2026
2139067
Merge branch 'master' into Issue317
mfranca0009 Mar 3, 2026
80ffb23
Fix changes that were deleted during merge conflict resolution. Added…
mfranca0009 Mar 3, 2026
8adfa4d
Took CodeRabbit suggestion since it pointed out unreachable branches …
mfranca0009 Mar 3, 2026
91ae45c
Remove accidental default values from hardcoded constants in Constant…
mfranca0009 Mar 3, 2026
9cd72f1
Fix a mismatch of currency being used and currency error message bein…
mfranca0009 Mar 3, 2026
d54d651
Replace Parse calls with TryParse to prevent runtime crashes. Additio…
mfranca0009 Mar 3, 2026
a8e748b
Changes per feedback from Zin
mfranca0009 Mar 3, 2026
1b27496
Changes per feedback from Zin #2
mfranca0009 Mar 5, 2026
da59e29
Correct NpcLastSight* constant values after adding them back from fee…
mfranca0009 Mar 5, 2026
fbb0b5c
Update per Zin's feedback round 3
mfranca0009 Mar 6, 2026
0b3b2c6
Revert removal of statLimits from ConfigManager, it is used in two pl…
mfranca0009 Mar 6, 2026
3f1a87c
Remove the last unneccessary ConstantsTable parameter and fix some od…
mfranca0009 Mar 6, 2026
efcb5ed
Fix mistake on unncessary ConstantsTable parameter removal
mfranca0009 Mar 6, 2026
377eed0
Run dotnet format
mfranca0009 Mar 6, 2026
c86521a
Fix dotnet format fail
mfranca0009 Mar 6, 2026
6eb2b5c
Make adjustments based off coderabbitai suggestions.
mfranca0009 Mar 9, 2026
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.Database/Model/Mail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal class Mail {

[return: NotNullIfNotNull(nameof(other))]
public static implicit operator Maple2.Model.Game.Mail?(Mail? other) {
return other == null ? null : new Maple2.Model.Game.Mail {
return other == null ? null : new Maple2.Model.Game.Mail() {
ReceiverId = other.ReceiverId,
Id = other.Id,
SenderId = other.SenderId,
Expand Down
4 changes: 2 additions & 2 deletions Maple2.Database/Storage/Game/GameStorage.Map.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ public IList<PlotCube> LoadCubesForOwner(long ownerId) {
return Context.TrySaveChanges() ? ToPlotInfo(model) : null;
}

public PlotInfo? GetSoonestPlotFromExpire() {
public PlotInfo? GetSoonestPlotFromExpire(TimeSpan ugcHomeSaleWaitingTime) {
IQueryable<UgcMap> maps = Context.UgcMap.Where(map => map.ExpiryTime > DateTimeOffset.MinValue && !map.Indoor);
foreach (UgcMap map in maps) {
if (map.OwnerId == 0) {
map.ExpiryTime = map.ExpiryTime.Add(Constant.UgcHomeSaleWaitingTime);
map.ExpiryTime = map.ExpiryTime.Add(ugcHomeSaleWaitingTime);
}
}
UgcMap? model = maps.OrderBy(map => map.ExpiryTime).FirstOrDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ServerTableMetadataStorage {
private readonly Lazy<CombineSpawnTable> combineSpawnTable;
private readonly Lazy<EnchantOptionTable> enchantOptionTable;
private readonly Lazy<UnlimitedEnchantOptionTable> unlimitedEnchantOptionTable;
private readonly Lazy<ConstantsTable> constantsTable;

public InstanceFieldTable InstanceFieldTable => instanceFieldTable.Value;
public ScriptConditionTable ScriptConditionTable => scriptConditionTable.Value;
Expand All @@ -53,6 +54,7 @@ public class ServerTableMetadataStorage {
public CombineSpawnTable CombineSpawnTable => combineSpawnTable.Value;
public EnchantOptionTable EnchantOptionTable => enchantOptionTable.Value;
public UnlimitedEnchantOptionTable UnlimitedEnchantOptionTable => unlimitedEnchantOptionTable.Value;
public ConstantsTable ConstantsTable => constantsTable.Value;

public ServerTableMetadataStorage(MetadataContext context) {
instanceFieldTable = Retrieve<InstanceFieldTable>(context, ServerTableNames.INSTANCE_FIELD);
Expand All @@ -78,6 +80,7 @@ public ServerTableMetadataStorage(MetadataContext context) {
combineSpawnTable = Retrieve<CombineSpawnTable>(context, ServerTableNames.COMBINE_SPAWN);
enchantOptionTable = Retrieve<EnchantOptionTable>(context, ServerTableNames.ENCHANT_OPTION);
unlimitedEnchantOptionTable = Retrieve<UnlimitedEnchantOptionTable>(context, ServerTableNames.UNLIMITED_ENCHANT_OPTION);
constantsTable = Retrieve<ConstantsTable>(context, ServerTableNames.CONSTANTS);
}

public IEnumerable<GameEvent> GetGameEvents() {
Expand Down
67 changes: 64 additions & 3 deletions Maple2.File.Ingest/Mapper/ServerTableMapper.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Globalization;
using System.Xml;
using Maple2.Database.Extensions;
using Maple2.Database.Extensions;
using Maple2.File.Ingest.Utils;
using Maple2.File.IO;
using Maple2.File.Parser;
using Maple2.File.Parser.Enum;
using Maple2.File.Parser.Flat.Convert;
using Maple2.File.Parser.Xml.Table;
using Maple2.File.Parser.Xml.Table.Server;
using Maple2.Model;
using Maple2.Model.Common;
Expand All @@ -13,6 +13,11 @@
using Maple2.Model.Game;
using Maple2.Model.Game.Shop;
using Maple2.Model.Metadata;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using System.Xml;
using DayOfWeek = System.DayOfWeek;
using ExpType = Maple2.Model.Enum.ExpType;
using Fish = Maple2.File.Parser.Xml.Table.Server.Fish;
Expand Down Expand Up @@ -128,6 +133,10 @@ protected override IEnumerable<ServerTableMetadata> Map() {
Name = ServerTableNames.UNLIMITED_ENCHANT_OPTION,
Table = ParseUnlimitedEnchantOption(),
};
yield return new ServerTableMetadata {
Name = ServerTableNames.CONSTANTS,
Table = ParseConstants(),
};

}

Expand Down Expand Up @@ -2135,4 +2144,56 @@ void AddSpecial(Dictionary<SpecialAttribute, int> values, Dictionary<SpecialAttr
}
}
}

private ConstantsTable ParseConstants() {
var constants = new ConstantsTable();
Dictionary<string, PropertyInfo> propertyLookup = typeof(ConstantsTable).GetProperties()
.ToDictionary(p => p.Name.Trim(), p => p, StringComparer.OrdinalIgnoreCase);
foreach ((string key, Constants.Key constant) in parser.ParseConstants()) {
string trimmedKey = key.Trim();
if (!propertyLookup.TryGetValue(trimmedKey, out PropertyInfo? property)) continue;
string cleanValue = CleanInput(constant.value.Trim(), trimmedKey, property.PropertyType);
GenericHelper.SetValue(property, constants, cleanValue);
}
return constants;

string CleanInput(string input, string propName, Type type) {
// check if string contains the ending 'f' for float designation, strip it if it does.
if (type == typeof(float) && input.Contains('f')) {
input = input.TrimEnd('f', 'F');
}
if (type == typeof(bool)) {
// 1 does not automatically equate to true during bool conversion
if (input == "1") {
input = "true";
}
// 0 does not automatically equate to false during bool conversion
if (input == "0") {
input = "false";
}
}
if (type == typeof(TimeSpan)) {
// Special case - dashes (-) are used instead of colons (:)
if (propName == "DailyTrophyResetDate") {
input = input.Replace('-', ':');
}
// Stored as 0.1 for 100ms
if (propName == "GlobalCubeSkillIntervalTime") {
input = $"0:0:{input}";
}
// Stored as an int value, convert to friendly input string for TimeSpan parsing
if (propName == "UgcHomeSaleWaitingTime") {
int.TryParse(input, out int result);
input = TimeSpan.FromSeconds(result).ToString(); // TODO: may not be correct conversion to TimeSpan
}
}
if (type == typeof(int)) {
// Remove prefix 0 on integers since they do not convert properly
if (input.Length > 1 && input[0] == '0') {
input = input.Remove(0, 1);
}
}
return input;
}
}
}
103 changes: 103 additions & 0 deletions Maple2.File.Ingest/Utils/GenericHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Globalization;
using System.Numerics;
using System.Reflection;

namespace Maple2.File.Ingest.Utils;

public static class GenericHelper {
public static void SetValue(PropertyInfo prop, object? obj, object? value) {
if (obj == null && value == null || value == null) return;
HandleNonIConvertibleTypes(prop, ref value);
if (value == null) return;
if (typeof(IConvertible).IsAssignableFrom(prop.PropertyType)) {
TryParseObject(prop.PropertyType, value, out object? result);
prop.SetValue(obj, result);
return;
}
prop.SetValue(obj, value);
}

private static object? HandleNonIConvertibleTypes(PropertyInfo prop, ref object? value) {
if (value == null) return value;
// Handle TimeSpan type
if (prop.PropertyType == typeof(TimeSpan)) {
TimeSpan.TryParse((string) value, CultureInfo.InvariantCulture, out TimeSpan val);
value = val;
}
// Handle array types (int[], short[], etc.)
if (prop.PropertyType.IsArray) {
var elementType = prop.PropertyType.GetElementType();
if (elementType == null) return value;
string[] segments = ((string) value).Split(',');
Array destinationArray = Array.CreateInstance(elementType, segments.Length);
for (int i = 0; i < segments.Length; i++) {
if (TryParseObject(elementType, segments[i].Trim(), out object? parseResult)) {
destinationArray.SetValue(parseResult ?? default, i);
}else {
destinationArray.SetValue(elementType.IsValueType ? Activator.CreateInstance(elementType) : null, i);
}
}
value = destinationArray;
}
// Handle Vector3 type
if (prop.PropertyType == typeof(Vector3)) {
string[] parts = ((string) value).Split(',');
bool parseXSuccess = float.TryParse(parts[0], CultureInfo.InvariantCulture, out float x);
bool parseYSuccess = float.TryParse(parts[1], CultureInfo.InvariantCulture, out float y);
bool parseZSuccess = float.TryParse(parts[2], CultureInfo.InvariantCulture, out float z);
if (parts.Length != 3 || parseXSuccess && parseYSuccess && parseZSuccess) {
value = Vector3.Zero;
} else {
value = new Vector3(x, y, z);
}
}
return value;
}

private static bool TryParseObject(Type? elementType, object? input, out object? result) {
if (elementType == null || input == null) {
result = null;
return false;
}

string inputString = Convert.ToString(input, CultureInfo.InvariantCulture)!;

// No TryParse method exists for a string, use the result directly.
if (elementType == typeof(string)) {
result = inputString;
return true;
}

Type[] argTypes = {
typeof(string),
typeof(IFormatProvider),
elementType.MakeByRefType()
};

var method = elementType.GetMethod("TryParse",
BindingFlags.Public | BindingFlags.Static,
null, argTypes, null);
if (method != null) {
object[] args = [inputString, CultureInfo.InvariantCulture, null!];
bool success = (bool) method.Invoke(null, args)!;
result = args[2];
return success;
}

// Fallback without CultureInfo provided, in case the type does not have a CultureInfo overload.
Type[] simpleArgs = { typeof(string), elementType.MakeByRefType() };
method = elementType.GetMethod("TryParse",
BindingFlags.Public | BindingFlags.Static,
null, simpleArgs, null);
if (method != null) {
object[] args = { inputString, null! };
bool success = (bool) method.Invoke(null, args)!;
result = args[1];
return success;
}


result = null;
return false;
}
}
1 change: 1 addition & 0 deletions Maple2.Model/Common/ServerTableNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ public static class ServerTableNames {
public const string COMBINE_SPAWN = "combineSpawn*.xml";
public const string ENCHANT_OPTION = "enchantOption.xml";
public const string UNLIMITED_ENCHANT_OPTION = "unlimitedEnchantOption.xml";
public const string CONSTANTS = "constants.xml";
}
10 changes: 5 additions & 5 deletions Maple2.Model/Game/Cube/Nurturing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ public Nurturing(long exp, short claimedGiftForStage, long[] playedBy, DateTimeO
}
}

public void Feed() {
public void Feed(int nurturingEatGrowth) {
if (Exp >= NurturingMetadata.RequiredGrowth.Last().Exp) {
return;
}

Exp += Constant.NurturingEatGrowth;
Exp += nurturingEatGrowth;
if (Exp >= NurturingMetadata.RequiredGrowth.First(x => x.Stage == Stage).Exp) {
Stage++;
}
LastFeedTime = DateTimeOffset.Now;
}

public bool Play(long accountId) {
if (PlayedBy.Count >= Constant.NurturingPlayMaxCount) {
public bool Play(long accountId, int nurturingEatGrowth, int nurturingPlayMaxCount) {
if (PlayedBy.Count >= nurturingPlayMaxCount) {
return false;
}

Expand All @@ -70,7 +70,7 @@ public bool Play(long accountId) {
}

PlayedBy.Add(accountId);
Feed();
Feed(nurturingEatGrowth);
return true;
}

Expand Down
11 changes: 9 additions & 2 deletions Maple2.Model/Game/Mail.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text;
using Maple2.Model.Enum;
using Maple2.Model.Metadata;
using Maple2.PacketLib.Tools;
using Maple2.Tools;
using Maple2.Tools.Extensions;
Expand Down Expand Up @@ -35,11 +34,19 @@ public class Mail : IByteSerializable {
// More than 1 item may not display properly
public readonly IList<Item> Items;

// Specifically for Mail object cloning (Mail.cs:57)
public Mail() {
TitleArgs = new List<(string Key, string Value)>();
ContentArgs = new List<(string Key, string Value)>();
Items = new List<Item>();
ExpiryTime = DateTimeOffset.UtcNow.AddDays(Constant.MailExpiryDays).ToUnixTimeSeconds();
// ExpiryTime will be overwritten, no need to set it here with a parameter passing server constant value.
}

public Mail(int mailExpiryDays) {
TitleArgs = new List<(string Key, string Value)>();
ContentArgs = new List<(string Key, string Value)>();
Items = new List<Item>();
ExpiryTime = DateTimeOffset.UtcNow.AddDays(mailExpiryDays).ToUnixTimeSeconds();
}

public void Update(Mail other) {
Expand Down
26 changes: 14 additions & 12 deletions Maple2.Model/Game/User/StatAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public class StatAttributes : IByteSerializable {
public int TotalPoints => Sources.Count;
public int UsedPoints => Allocation.Count;

public StatAttributes() {
public StatAttributes(IDictionary<string, int> statLimits) {
Sources = new PointSources();
Allocation = new PointAllocation();
Allocation = new PointAllocation(statLimits);
}

public void WriteTo(IByteWriter writer) {
Expand Down Expand Up @@ -52,14 +52,15 @@ public void WriteTo(IByteWriter writer) {

public class PointAllocation : IByteSerializable {
private readonly Dictionary<BasicAttribute, int> points;
private readonly IDictionary<string, int> statLimits;

public BasicAttribute[] Attributes => points.Keys.ToArray();
public int Count => points.Values.Sum();

public int this[BasicAttribute type] {
get => points.GetValueOrDefault(type);
set {
if (value < 0 || value > StatLimit(type)) {
if (value < 0 || value > StatLimit(type, statLimits)) {
return;
}
if (value == 0) {
Expand All @@ -71,19 +72,20 @@ public int this[BasicAttribute type] {
}
}

public PointAllocation() {
public PointAllocation(IDictionary<string, int> statLimits) {
points = new Dictionary<BasicAttribute, int>();
this.statLimits = statLimits;
}

public static int StatLimit(BasicAttribute type) {
public static int StatLimit(BasicAttribute type, IDictionary<string, int> statLimits) {
return type switch {
BasicAttribute.Strength => Constant.StatPointLimit_str,
BasicAttribute.Dexterity => Constant.StatPointLimit_dex,
BasicAttribute.Intelligence => Constant.StatPointLimit_int,
BasicAttribute.Luck => Constant.StatPointLimit_luk,
BasicAttribute.Health => Constant.StatPointLimit_hp,
BasicAttribute.CriticalRate => Constant.StatPointLimit_cap,
_ => 0,
BasicAttribute.Strength => statLimits.GetValueOrDefault("StatPointLimit_str"),
BasicAttribute.Dexterity => statLimits.GetValueOrDefault("StatPointLimit_dex"),
BasicAttribute.Intelligence => statLimits.GetValueOrDefault("StatPointLimit_int"),
BasicAttribute.Luck => statLimits.GetValueOrDefault("StatPointLimit_luk"),
BasicAttribute.Health => statLimits.GetValueOrDefault("StatPointLimit_hp"),
BasicAttribute.CriticalRate => statLimits.GetValueOrDefault("StatPointLimit_cap"),
_ => 0
};
}

Expand Down
Loading
Loading