From ce153c2804879ad3c988a6813c3eea6a285caab9 Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 23 Aug 2026 05:59:03 +0400 Subject: [PATCH 1/2] feat: mate and family experience services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two curves that were living in NosCore and belong here. MateExperienceService is the published curve divided by 20 for a pet and by 5 for a partner. Those divisors come from the XpLoad field of sc_p and sc_n in a packet capture: eleven observations between level 1 and level 88 match to the unit, including 29 312 950 and 39 495 200. Without them a pet needs twenty times the experience it should, which raises nothing and simply reads as slow progress. FamilyExperienceService is the published table as-is. A ginfo line from the same capture puts a level 7 family's bar at 640 000 where the table says 1 900 000, so at least that row is wrong — but one observation cannot rebuild the other eighteen, so the published numbers stand and the disagreement is noted where somebody will find it. Both come with approval-test tables under documentation/. Co-Authored-By: Claude Opus 5 --- ...ilyExperienceDocumentation.approved.md.swp | Bin 0 -> 12288 bytes ...ateExperienceDocumentation.approved.md.swp | Bin 0 -> 12288 bytes ....FamilyExperienceDocumentation.approved.md | 20 ++++ ...st.MateExperienceDocumentation.approved.md | 100 ++++++++++++++++++ src/NosCore.Algorithm/Constants.cs | 1 + .../FamilyExperienceService.cs | 38 +++++++ .../IFamilyExperienceService.cs | 21 ++++ .../IMateExperienceService.cs | 28 +++++ .../MateExperienceService.cs | 96 +++++++++++++++++ .../DocumentationTest.cs | 36 ++++++- 10 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp create mode 100644 documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp create mode 100644 documentation/DocumentationTest.FamilyExperienceDocumentation.approved.md create mode 100644 documentation/DocumentationTest.MateExperienceDocumentation.approved.md create mode 100644 src/NosCore.Algorithm/FamilyExperienceService/FamilyExperienceService.cs create mode 100644 src/NosCore.Algorithm/FamilyExperienceService/IFamilyExperienceService.cs create mode 100644 src/NosCore.Algorithm/MateExperienceService/IMateExperienceService.cs create mode 100644 src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs diff --git a/documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp b/documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp new file mode 100644 index 0000000000000000000000000000000000000000..999f6389c416a02851cf203625eb3a4419836e9c GIT binary patch literal 12288 zcmeI%y-ve05C?FV9TmX~6qp^SAtmibDIgg55CL{KzR*-j93?JlCw3;@kp}?>fdm68 zL!_$G-%@0qb2(pro8__laB5`mb`l!pqOfsQ#G|MPA(j+<;CqJV*s*H0cvm{-d&I>9N$|L%f z;mD#W^%E&4Q~&rYq(ER@1UA`$*LCY|_xE-?JKOi`(m48r00bZa0SG_<0uX=z1R4-9 zMTD$%_Nnjxzt8`l f@rruFwh({-1Rwwb2tWV=5P$##AOHafG%N4{(IHS^ literal 0 HcmV?d00001 diff --git a/documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp b/documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp new file mode 100644 index 0000000000000000000000000000000000000000..81310be2e3e599c676b032e6721bcc874e7acfc0 GIT binary patch literal 12288 zcmeI%O-sWt7zgmoyG-;8Og*RR>Sni#Fy{^)ref85mOOPGZByDz^t8JdKcpW-Q&G@^ zOqehR|3CcYtl5!bEgJ{W`dRo}5 z5Gs{(O=8ITRcU6VY{f53btDoNWltfp?A@w>w?*+ckJNj61*$q292;$h+@uVNO zNmQzoR!s}3h1)+gC3%DN-js=*WZbIRe}4C}{h2zJ~| + /// Provides family experience requirement calculations for different levels + /// + /// + /// The table is the one the older emulators publish. A ginfo line from a packet capture puts + /// a level 7 family's bar at 640 000 where this says 1 900 000, so at least that row is + /// wrong; one observation is not enough to rebuild the rest, so the published numbers stand + /// until more ginfo lines from other levels turn up. + /// + public class FamilyExperienceService : IFamilyExperienceService + { + private static readonly uint[] FamilyXpData = + [ + 100_000, 250_000, 370_000, 560_000, 840_000, + 1_260_000, 1_900_000, 2_850_000, 3_570_000, 3_830_000, + 4_150_000, 4_750_000, 5_500_000, 6_500_000, 7_000_000, + 8_500_000, 9_500_000, 10_000_000, 17_000_000 + ]; + + /// + /// Gets the experience a family needs to leave a specific level + /// + /// The family level + /// The experience required, or uint.MaxValue past the last known level + public uint GetFamilyExperience(byte level) + { + return level >= 1 && level <= FamilyXpData.Length ? FamilyXpData[level - 1] : uint.MaxValue; + } + } +} diff --git a/src/NosCore.Algorithm/FamilyExperienceService/IFamilyExperienceService.cs b/src/NosCore.Algorithm/FamilyExperienceService/IFamilyExperienceService.cs new file mode 100644 index 0000000..a6eafbf --- /dev/null +++ b/src/NosCore.Algorithm/FamilyExperienceService/IFamilyExperienceService.cs @@ -0,0 +1,21 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.Algorithm.FamilyExperienceService +{ + /// + /// Service for calculating family experience requirements + /// + public interface IFamilyExperienceService + { + /// + /// Gets the experience a family needs to leave a specific level + /// + /// The family level + /// The experience required + uint GetFamilyExperience(byte level); + } +} diff --git a/src/NosCore.Algorithm/MateExperienceService/IMateExperienceService.cs b/src/NosCore.Algorithm/MateExperienceService/IMateExperienceService.cs new file mode 100644 index 0000000..fc51680 --- /dev/null +++ b/src/NosCore.Algorithm/MateExperienceService/IMateExperienceService.cs @@ -0,0 +1,28 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.Algorithm.MateExperienceService +{ + /// + /// Service for calculating pet and partner experience requirements + /// + public interface IMateExperienceService + { + /// + /// Gets the experience a pet needs to leave a specific level + /// + /// The pet level + /// The experience required + long GetPetExperience(byte level); + + /// + /// Gets the experience a partner needs to leave a specific level + /// + /// The partner level + /// The experience required + long GetPartnerExperience(byte level); + } +} diff --git a/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs b/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs new file mode 100644 index 0000000..7d39e82 --- /dev/null +++ b/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs @@ -0,0 +1,96 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.Algorithm.MateExperienceService +{ + /// + /// Provides pet and partner experience requirement calculations for different levels + /// + /// + /// The curve is the one the older emulators publish, divided by 20 for a pet and by 5 for a + /// partner. Those divisors come from the XpLoad field of sc_p and sc_n in a packet capture: + /// eleven observations between level 1 and level 88 match to the unit, including + /// 29 312 950 and 39 495 200. Without them a pet needs twenty times the experience it + /// should, which raises nothing and simply looks like slow progress. + /// + public class MateExperienceService : IMateExperienceService + { + private const int PetDivisor = 20; + private const int PartnerDivisor = 5; + + private readonly long[] _mateXpData = new long[Constants.MaxMateLevel + 1]; + + /// + /// Initializes a new instance of the MateExperienceService and pre-calculates experience + /// requirements for all mate levels + /// + public MateExperienceService() + { + var step = new double[_mateXpData.Length]; + step[0] = 540; + step[1] = 960; + for (var i = 2; i < step.Length; i++) + { + step[i] = step[i - 1] + 420 + 120 * (i - 1); + } + + var factor = 1d; + _mateXpData[0] = 300; + for (var i = 1; i < _mateXpData.Length; i++) + { + if (i < 79) + { + factor = i switch + { + 14 => 6 / 3d, + 39 => 19 / 3d, + 59 => 70 / 3d, + _ => factor + }; + + _mateXpData[i] = (long)(_mateXpData[i - 1] + factor * step[i - 1]); + continue; + } + + factor = i switch + { + 79 => 5000, + 82 => 9000, + 84 => 13000, + _ => factor + }; + + _mateXpData[i] = (long)(_mateXpData[i - 1] + factor * (i + 2) * (i + 2)); + } + } + + /// + /// Gets the experience a pet needs to leave a specific level + /// + /// The pet level + /// The experience required + public long GetPetExperience(byte level) + { + return At(level) / PetDivisor; + } + + /// + /// Gets the experience a partner needs to leave a specific level + /// + /// The partner level + /// The experience required + public long GetPartnerExperience(byte level) + { + return At(level) / PartnerDivisor; + } + + private long At(byte level) + { + var index = level < 1 ? 0 : level - 1; + return _mateXpData[index >= _mateXpData.Length ? _mateXpData.Length - 1 : index]; + } + } +} diff --git a/test/NosCore.Algorithm.Tests/DocumentationTest.cs b/test/NosCore.Algorithm.Tests/DocumentationTest.cs index 78049ce..6997799 100644 --- a/test/NosCore.Algorithm.Tests/DocumentationTest.cs +++ b/test/NosCore.Algorithm.Tests/DocumentationTest.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| @@ -405,5 +405,39 @@ public void SumDocumentation() Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md")); } + + [TestMethod] + public void MateExperienceDocumentation() + { + var mateExperienceService = new MateExperienceService.MateExperienceService(); + + var resultBuilder = new StringBuilder("# Mate Experience Table"); + resultBuilder.AppendLine(); + + for (byte level = 1; level < 100; level++) + { + resultBuilder.AppendLine( + $"- Level {level,2} - Pet: {mateExperienceService.GetPetExperience(level)} - Partner: {mateExperienceService.GetPartnerExperience(level)}"); + } + + Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md")); + } + + [TestMethod] + public void FamilyExperienceDocumentation() + { + var familyExperienceService = new FamilyExperienceService.FamilyExperienceService(); + + var resultBuilder = new StringBuilder("# Family Experience Table"); + resultBuilder.AppendLine(); + + for (byte level = 1; level < 20; level++) + { + resultBuilder.AppendLine( + $"- Level {level,2} - XP: {familyExperienceService.GetFamilyExperience(level)}"); + } + + Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md")); + } } } From ae9bf865240d8d459839afd75e4bf503d18df115 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sun, 23 Aug 2026 17:20:06 +1200 Subject: [PATCH 2/2] chore: drop the committed swap files and trim the provenance remarks Two Vim .swp files were checked in alongside the approved documentation, and the two new approved files carried a BOM the generator does not emit - both are artefacts of hand-editing them. Ignoring *.swp so they cannot come back. Remarks cut back to the load-bearing facts, in line with the summary-only style of the services already here. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 5 ++++- ...amilyExperienceDocumentation.approved.md.swp | Bin 12288 -> 0 bytes ....MateExperienceDocumentation.approved.md.swp | Bin 12288 -> 0 bytes ...st.FamilyExperienceDocumentation.approved.md | 2 +- ...Test.MateExperienceDocumentation.approved.md | 2 +- .../FamilyExperienceService.cs | 7 +++---- .../MateExperienceService.cs | 8 +++----- .../DocumentationTest.cs | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp delete mode 100644 documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp diff --git a/.gitignore b/.gitignore index cbd642a..ab56ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -501,4 +501,7 @@ fabric.properties /documentation/*.received.md /documentation/*.received.txt /documentation/*.approved.txt -build_output.txt \ No newline at end of file +build_output.txt +# Vim swap files +*.swp +*.swo diff --git a/documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp b/documentation/.DocumentationTest.FamilyExperienceDocumentation.approved.md.swp deleted file mode 100644 index 999f6389c416a02851cf203625eb3a4419836e9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI%y-ve05C?FV9TmX~6qp^SAtmibDIgg55CL{KzR*-j93?JlCw3;@kp}?>fdm68 zL!_$G-%@0qb2(pro8__laB5`mb`l!pqOfsQ#G|MPA(j+<;CqJV*s*H0cvm{-d&I>9N$|L%f z;mD#W^%E&4Q~&rYq(ER@1UA`$*LCY|_xE-?JKOi`(m48r00bZa0SG_<0uX=z1R4-9 zMTD$%_Nnjxzt8`l f@rruFwh({-1Rwwb2tWV=5P$##AOHafG%N4{(IHS^ diff --git a/documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp b/documentation/.DocumentationTest.MateExperienceDocumentation.approved.md.swp deleted file mode 100644 index 81310be2e3e599c676b032e6721bcc874e7acfc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI%O-sWt7zgmoyG-;8Og*RR>Sni#Fy{^)ref85mOOPGZByDz^t8JdKcpW-Q&G@^ zOqehR|3CcYtl5!bEgJ{W`dRo}5 z5Gs{(O=8ITRcU6VY{f53btDoNWltfp?A@w>w?*+ckJNj61*$q292;$h+@uVNO zNmQzoR!s}3h1)+gC3%DN-js=*WZbIRe}4C}{h2zJ~| /// - /// The table is the one the older emulators publish. A ginfo line from a packet capture puts - /// a level 7 family's bar at 640 000 where this says 1 900 000, so at least that row is - /// wrong; one observation is not enough to rebuild the rest, so the published numbers stand - /// until more ginfo lines from other levels turn up. + /// A ginfo line puts a level 7 family's bar at 640 000 against the 1 900 000 here, so at + /// least that row is suspect. One observation is not enough to rebuild the curve, so the + /// table stands until more levels can be checked. /// public class FamilyExperienceService : IFamilyExperienceService { diff --git a/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs b/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs index 7d39e82..b7f9fb4 100644 --- a/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs +++ b/src/NosCore.Algorithm/MateExperienceService/MateExperienceService.cs @@ -10,11 +10,9 @@ namespace NosCore.Algorithm.MateExperienceService /// Provides pet and partner experience requirement calculations for different levels /// /// - /// The curve is the one the older emulators publish, divided by 20 for a pet and by 5 for a - /// partner. Those divisors come from the XpLoad field of sc_p and sc_n in a packet capture: - /// eleven observations between level 1 and level 88 match to the unit, including - /// 29 312 950 and 39 495 200. Without them a pet needs twenty times the experience it - /// should, which raises nothing and simply looks like slow progress. + /// A pet needs a twentieth of the curve and a partner a fifth. Both divisors come from the + /// XpLoad field of sc_p and sc_n, which match to the unit across eleven levels between 1 + /// and 88. /// public class MateExperienceService : IMateExperienceService { diff --git a/test/NosCore.Algorithm.Tests/DocumentationTest.cs b/test/NosCore.Algorithm.Tests/DocumentationTest.cs index 6997799..625f2d6 100644 --- a/test/NosCore.Algorithm.Tests/DocumentationTest.cs +++ b/test/NosCore.Algorithm.Tests/DocumentationTest.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___|