diff --git a/pkNX.Game/Editors/ShinyRate/ShinyRateGG.cs b/pkNX.Game/Editors/ShinyRate/ShinyRateGG.cs
new file mode 100644
index 00000000..82fc0f4a
--- /dev/null
+++ b/pkNX.Game/Editors/ShinyRate/ShinyRateGG.cs
@@ -0,0 +1,86 @@
+using System;
+using pkNX.Structures;
+
+namespace pkNX.Game
+{
+ public sealed class ShinyRateGG : ShinyRateInfo
+ {
+ /*
+ Shiny Rate Patch -- fix the loop counter regardless of input param value.
+ ARM64 disassembly @ sub_71007399D8
+ LDRB W8, [X19, #0x25] // load loop max
+ ADD W20, W20, #1 // increment counter
+ CMP W20, W8 // compare counter to max
+ BL -44 // branch if less (loop)
+ B [....] // branch (no loop), PID is done!
+
+ Patch creation: accept input for reroll count=1-4091
+ Get u32: ((count & 0xFFF) << 10) | 0b111000100_000000000000_1010011111
+ Convert to bytes, this is our new "CMP W20, XXX" instruction
+
+ Replace these bytes:
+ 68 96 40 39 94 06 00 11 9F 02 08 6B
+ With these bytes:
+ 68 96 40 39 94 06 00 11 [u32 bytes]
+
+ ***
+ Always Shiny Patch -- nop the bl
+ write 1F 20 03 D5 after the above 12 byte sequence
+
+ ***
+ Revert above patch -- restore the bl
+ write AB FE FF 54 after the above 12 byte sequence
+ */
+
+ private readonly int CodeOffset;
+ private static readonly byte[] Pattern = { 0x68, 0x96, 0x40, 0x39, 0x94, 0x06, 0x00, 0x11 };
+ private static readonly byte[] Default = { 0x9F, 0x02, 0x08, 0x6B }; // cmp W20, W8
+ private static readonly byte[] Always12 = { 0x1F, 0x20, 0x03, 0xD5 }; // nop
+ private static readonly byte[] Revert12 = { 0xAB, 0xFE, 0xFF, 0x54 }; // bl
+
+ public ShinyRateGG(byte[] data) : base(data) => CodeOffset = CodePattern.IndexOfBytes(data, Pattern, 0x500_000);
+
+ public override bool IsEditable => CodeOffset > 0;
+
+ public override bool IsDefault => !IsAlways && IsPresent(Data, Default, CodeOffset + 8);
+ public override bool IsFixed => !IsAlways && !IsDefault;
+ public override bool IsAlways => IsPresent(Data, Always12, CodeOffset + 12);
+
+ public override int GetFixedRate() // "CMP W20, {val}" instruction
+ {
+ if (!IsFixed)
+ return -1;
+ var instr = BitConverter.ToUInt32(Data, CodeOffset + 8);
+ return (int)((instr >> 10) & 0xFFF);
+ }
+
+ public override void SetDefault()
+ {
+ Default.CopyTo(Data, CodeOffset + 8);
+ Revert12.CopyTo(Data, CodeOffset + 12);
+ }
+
+ public override void SetFixedRate(int rerollCount)
+ {
+ var instr = GetFixedInstruction(rerollCount);
+ instr.CopyTo(Data, CodeOffset + 8);
+ Revert12.CopyTo(Data, CodeOffset + 12);
+ }
+
+ public override void SetAlwaysShiny()
+ {
+ Default.CopyTo(Data, CodeOffset + 8);
+ Always12.CopyTo(Data, CodeOffset + 12);
+ }
+
+ public byte[] GetFixedInstruction(int count)
+ {
+ if (count <= 0)
+ count = 1;
+ else if (count >= 4092)
+ count = 4091;
+ var val = ((count & 0xFFF) << 10) | 0b111000100_000000000000_1010011111;
+ return BitConverter.GetBytes((uint)val);
+ }
+ }
+}
\ No newline at end of file
diff --git a/pkNX.Game/Editors/ShinyRate/ShinyRateInfo.cs b/pkNX.Game/Editors/ShinyRate/ShinyRateInfo.cs
new file mode 100644
index 00000000..f5292eca
--- /dev/null
+++ b/pkNX.Game/Editors/ShinyRate/ShinyRateInfo.cs
@@ -0,0 +1,29 @@
+namespace pkNX.Game
+{
+ public abstract class ShinyRateInfo
+ {
+ public byte[] Data { get; }
+ protected ShinyRateInfo(byte[] data) => Data = data;
+
+ public abstract bool IsEditable { get; }
+
+ public abstract bool IsDefault { get; }
+ public abstract bool IsFixed { get; }
+ public abstract bool IsAlways { get; }
+
+ public abstract int GetFixedRate();
+ public abstract void SetDefault();
+ public abstract void SetFixedRate(int rerollCount);
+ public abstract void SetAlwaysShiny();
+
+ protected bool IsPresent(byte[] source, byte[] pattern, int offset)
+ {
+ for (int i = 0; i < pattern.Length; i++)
+ {
+ if (source[i + offset] != pattern[i])
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/pkNX.Game/GameManager.cs b/pkNX.Game/GameManager.cs
index 5b02c9fd..9f7822e2 100644
--- a/pkNX.Game/GameManager.cs
+++ b/pkNX.Game/GameManager.cs
@@ -14,6 +14,9 @@ public abstract class GameManager
private readonly GameFileMapping FileMap;
public readonly GameInfo Info;
+ public string PathExeFS => ROM.ExeFS;
+ public string PathRomFS => ROM.RomFS;
+
///
/// Language to use when fetching string & graphic assets.
///
diff --git a/pkNX.WinForms/MainEditor/EditorGG.cs b/pkNX.WinForms/MainEditor/EditorGG.cs
index cb3dfd14..113cef87 100644
--- a/pkNX.WinForms/MainEditor/EditorGG.cs
+++ b/pkNX.WinForms/MainEditor/EditorGG.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Linq;
using System.Windows.Forms;
+using pkNX.Containers;
using pkNX.Game;
using pkNX.Randomization;
using pkNX.Structures;
@@ -220,5 +221,27 @@ public void EditWild()
path = sfd.FileName;
File.WriteAllText(path, result);
}
+
+ public void EditShinyRate()
+ {
+ var path = Path.Combine(ROM.PathExeFS, "main");
+ var data = FileMitm.ReadAllBytes(path);
+ var nso = new NSO(data);
+
+ var shiny = new ShinyRateGG(nso.DecompressedText);
+ if (!shiny.IsEditable)
+ {
+ WinFormsUtil.Alert("Not able to find shiny rate logic in exefs.");
+ return;
+ }
+
+ var editor = new ShinyRate(shiny);
+ editor.ShowDialog();
+ if (!editor.Modified)
+ return;
+
+ nso.DecompressedText = shiny.Data;
+ FileMitm.WriteAllBytes(path, nso.Write());
+ }
}
}
\ No newline at end of file
diff --git a/pkNX.WinForms/Subforms/ShinyRate.Designer.cs b/pkNX.WinForms/Subforms/ShinyRate.Designer.cs
new file mode 100644
index 00000000..40cda48d
--- /dev/null
+++ b/pkNX.WinForms/Subforms/ShinyRate.Designer.cs
@@ -0,0 +1,236 @@
+namespace pkNX.WinForms
+{
+ partial class ShinyRate
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.B_Save = new System.Windows.Forms.Button();
+ this.RB_Default = new System.Windows.Forms.RadioButton();
+ this.RB_Fixed = new System.Windows.Forms.RadioButton();
+ this.RB_Always = new System.Windows.Forms.RadioButton();
+ this.GB_Rerolls = new System.Windows.Forms.GroupBox();
+ this.NUD_Rerolls = new System.Windows.Forms.NumericUpDown();
+ this.L_Overall = new System.Windows.Forms.Label();
+ this.GB_RerollHelper = new System.Windows.Forms.GroupBox();
+ this.NUD_Rate = new System.Windows.Forms.NumericUpDown();
+ this.L_RerollOverall = new System.Windows.Forms.Label();
+ this.L_RerollCount = new System.Windows.Forms.Label();
+ this.L_Note = new System.Windows.Forms.Label();
+ this.GB_Rerolls.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.NUD_Rerolls)).BeginInit();
+ this.GB_RerollHelper.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.NUD_Rate)).BeginInit();
+ this.SuspendLayout();
+ //
+ // B_Save
+ //
+ this.B_Save.Location = new System.Drawing.Point(239, 215);
+ this.B_Save.Name = "B_Save";
+ this.B_Save.Size = new System.Drawing.Size(60, 23);
+ this.B_Save.TabIndex = 1;
+ this.B_Save.Text = "Save";
+ this.B_Save.UseVisualStyleBackColor = true;
+ this.B_Save.Click += new System.EventHandler(this.B_Save_Click);
+ //
+ // RB_Default
+ //
+ this.RB_Default.AutoSize = true;
+ this.RB_Default.Location = new System.Drawing.Point(122, 96);
+ this.RB_Default.Name = "RB_Default";
+ this.RB_Default.Size = new System.Drawing.Size(59, 17);
+ this.RB_Default.TabIndex = 3;
+ this.RB_Default.TabStop = true;
+ this.RB_Default.Text = "Default";
+ this.RB_Default.UseVisualStyleBackColor = true;
+ this.RB_Default.CheckedChanged += new System.EventHandler(this.ChangeSelection);
+ //
+ // RB_Fixed
+ //
+ this.RB_Fixed.AutoSize = true;
+ this.RB_Fixed.Location = new System.Drawing.Point(122, 119);
+ this.RB_Fixed.Name = "RB_Fixed";
+ this.RB_Fixed.Size = new System.Drawing.Size(111, 17);
+ this.RB_Fixed.TabIndex = 4;
+ this.RB_Fixed.TabStop = true;
+ this.RB_Fixed.Text = "Fixed Reroll Count";
+ this.RB_Fixed.UseVisualStyleBackColor = true;
+ this.RB_Fixed.CheckedChanged += new System.EventHandler(this.ChangeSelection);
+ //
+ // RB_Always
+ //
+ this.RB_Always.AutoSize = true;
+ this.RB_Always.Location = new System.Drawing.Point(122, 142);
+ this.RB_Always.Name = "RB_Always";
+ this.RB_Always.Size = new System.Drawing.Size(87, 17);
+ this.RB_Always.TabIndex = 5;
+ this.RB_Always.TabStop = true;
+ this.RB_Always.Text = "Always Shiny";
+ this.RB_Always.UseVisualStyleBackColor = true;
+ this.RB_Always.CheckedChanged += new System.EventHandler(this.ChangeSelection);
+ //
+ // GB_Rerolls
+ //
+ this.GB_Rerolls.Controls.Add(this.NUD_Rerolls);
+ this.GB_Rerolls.Controls.Add(this.L_Overall);
+ this.GB_Rerolls.ForeColor = System.Drawing.Color.Red;
+ this.GB_Rerolls.Location = new System.Drawing.Point(12, 87);
+ this.GB_Rerolls.Name = "GB_Rerolls";
+ this.GB_Rerolls.Size = new System.Drawing.Size(105, 76);
+ this.GB_Rerolls.TabIndex = 15;
+ this.GB_Rerolls.TabStop = false;
+ this.GB_Rerolls.Text = "PID Generation Loop Rerolls";
+ //
+ // NUD_Rerolls
+ //
+ this.NUD_Rerolls.Location = new System.Drawing.Point(16, 33);
+ this.NUD_Rerolls.Maximum = new decimal(new int[] {
+ 65535,
+ 0,
+ 0,
+ 0});
+ this.NUD_Rerolls.Name = "NUD_Rerolls";
+ this.NUD_Rerolls.Size = new System.Drawing.Size(65, 20);
+ this.NUD_Rerolls.TabIndex = 1;
+ this.NUD_Rerolls.Value = new decimal(new int[] {
+ 125,
+ 0,
+ 0,
+ 0});
+ this.NUD_Rerolls.ValueChanged += new System.EventHandler(this.ChangeRerollCount);
+ //
+ // L_Overall
+ //
+ this.L_Overall.AutoSize = true;
+ this.L_Overall.ForeColor = System.Drawing.SystemColors.ControlText;
+ this.L_Overall.Location = new System.Drawing.Point(53, 56);
+ this.L_Overall.Name = "L_Overall";
+ this.L_Overall.Size = new System.Drawing.Size(28, 13);
+ this.L_Overall.TabIndex = 2;
+ this.L_Overall.Text = "PCT";
+ //
+ // GB_RerollHelper
+ //
+ this.GB_RerollHelper.Controls.Add(this.NUD_Rate);
+ this.GB_RerollHelper.Controls.Add(this.L_RerollOverall);
+ this.GB_RerollHelper.Controls.Add(this.L_RerollCount);
+ this.GB_RerollHelper.Location = new System.Drawing.Point(12, 175);
+ this.GB_RerollHelper.Name = "GB_RerollHelper";
+ this.GB_RerollHelper.Size = new System.Drawing.Size(169, 63);
+ this.GB_RerollHelper.TabIndex = 14;
+ this.GB_RerollHelper.TabStop = false;
+ this.GB_RerollHelper.Text = "Reroll Helper";
+ //
+ // NUD_Rate
+ //
+ this.NUD_Rate.DecimalPlaces = 2;
+ this.NUD_Rate.Increment = new decimal(new int[] {
+ 1,
+ 0,
+ 0,
+ 65536});
+ this.NUD_Rate.Location = new System.Drawing.Point(81, 13);
+ this.NUD_Rate.Name = "NUD_Rate";
+ this.NUD_Rate.Size = new System.Drawing.Size(65, 20);
+ this.NUD_Rate.TabIndex = 7;
+ this.NUD_Rate.Value = new decimal(new int[] {
+ 3,
+ 0,
+ 0,
+ 0});
+ this.NUD_Rate.ValueChanged += new System.EventHandler(this.ChangePercent);
+ //
+ // L_RerollOverall
+ //
+ this.L_RerollOverall.AutoSize = true;
+ this.L_RerollOverall.Location = new System.Drawing.Point(24, 15);
+ this.L_RerollOverall.Name = "L_RerollOverall";
+ this.L_RerollOverall.Size = new System.Drawing.Size(51, 13);
+ this.L_RerollOverall.TabIndex = 9;
+ this.L_RerollOverall.Text = "Overall%:";
+ //
+ // L_RerollCount
+ //
+ this.L_RerollCount.AutoSize = true;
+ this.L_RerollCount.Location = new System.Drawing.Point(24, 37);
+ this.L_RerollCount.Name = "L_RerollCount";
+ this.L_RerollCount.Size = new System.Drawing.Size(47, 13);
+ this.L_RerollCount.TabIndex = 8;
+ this.L_RerollCount.Text = "Count: 0";
+ //
+ // L_Note
+ //
+ this.L_Note.Location = new System.Drawing.Point(12, 7);
+ this.L_Note.Name = "L_Note";
+ this.L_Note.Size = new System.Drawing.Size(295, 77);
+ this.L_Note.TabIndex = 13;
+ this.L_Note.Text = "Note: \r\nChanging the rate only changes the amount of PID rerolls.\r\nChanging the r" +
+ "ate does not alter the \"IsShiny\" determination.\r\n\r\nThink of it like a frozen sup" +
+ "er-Shiny Charm.";
+ //
+ // ShinyRate
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(311, 247);
+ this.Controls.Add(this.GB_Rerolls);
+ this.Controls.Add(this.GB_RerollHelper);
+ this.Controls.Add(this.L_Note);
+ this.Controls.Add(this.RB_Always);
+ this.Controls.Add(this.RB_Fixed);
+ this.Controls.Add(this.RB_Default);
+ this.Controls.Add(this.B_Save);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
+ this.MaximizeBox = false;
+ this.Name = "ShinyRate";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
+ this.Text = "Shiny Rate";
+ this.GB_Rerolls.ResumeLayout(false);
+ this.GB_Rerolls.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.NUD_Rerolls)).EndInit();
+ this.GB_RerollHelper.ResumeLayout(false);
+ this.GB_RerollHelper.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.NUD_Rate)).EndInit();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+ private System.Windows.Forms.Button B_Save;
+ private System.Windows.Forms.RadioButton RB_Default;
+ private System.Windows.Forms.RadioButton RB_Fixed;
+ private System.Windows.Forms.RadioButton RB_Always;
+ private System.Windows.Forms.GroupBox GB_Rerolls;
+ private System.Windows.Forms.NumericUpDown NUD_Rerolls;
+ private System.Windows.Forms.Label L_Overall;
+ private System.Windows.Forms.GroupBox GB_RerollHelper;
+ private System.Windows.Forms.NumericUpDown NUD_Rate;
+ private System.Windows.Forms.Label L_RerollOverall;
+ private System.Windows.Forms.Label L_RerollCount;
+ private System.Windows.Forms.Label L_Note;
+ }
+}
\ No newline at end of file
diff --git a/pkNX.WinForms/Subforms/ShinyRate.cs b/pkNX.WinForms/Subforms/ShinyRate.cs
new file mode 100644
index 00000000..edb84ffd
--- /dev/null
+++ b/pkNX.WinForms/Subforms/ShinyRate.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Windows.Forms;
+using pkNX.Game;
+
+namespace pkNX.WinForms
+{
+ public sealed partial class ShinyRate : Form
+ {
+ private readonly ShinyRateInfo Data;
+
+ public ShinyRate(ShinyRateInfo info)
+ {
+ InitializeComponent();
+ Data = info;
+ // load initial state
+ if (Data.IsFixed)
+ {
+ RB_Fixed.Checked = true;
+ NUD_Rerolls.Value = Data.GetFixedRate();
+ }
+ if (Data.IsAlways)
+ RB_Always.Checked = true;
+ if (Data.IsDefault)
+ RB_Default.Checked = true;
+
+ // force update labels
+ ChangePercent(null, null);
+ ChangeRerollCount(null, null);
+ }
+
+ public bool Modified { get; set; }
+
+ private void B_Save_Click(object sender, EventArgs e)
+ {
+ Modified = true;
+ Close();
+ }
+
+ private void ChangeSelection(object sender, EventArgs e)
+ {
+ GB_Rerolls.Enabled = GB_RerollHelper.Enabled = sender == RB_Fixed;
+ if (sender == RB_Default)
+ Data.SetDefault();
+ else if (sender == RB_Always)
+ Data.SetAlwaysShiny();
+ else
+ Data.SetFixedRate((int)NUD_Rerolls.Value);
+ }
+
+ private void ChangeRerollCount(object sender, EventArgs e)
+ {
+ if (RB_Fixed.Checked)
+ Data.SetFixedRate((int)NUD_Rerolls.Value);
+
+ int count = (int)NUD_Rerolls.Value;
+ const int bc = 4096;
+ var pct = 1 - Math.Pow((float)(bc - 1) / bc, count);
+ L_Overall.Text = $"~{pct:P}";
+ }
+
+ private void ChangePercent(object sender, EventArgs e)
+ {
+ var pct = NUD_Rate.Value;
+ const int bc = 4096;
+
+ var inv = (int)Math.Log(1 - ((float)pct / 100), (float)(bc - 1) / bc);
+ if (pct == 0)
+ pct = 0.00001m; // arbitrary nonzero
+ L_RerollCount.Text = $"Count: {inv:0} = 1:{(int)(1 / (pct / 100))}";
+ }
+ }
+}
diff --git a/pkNX.WinForms/Subforms/ShinyRate.resx b/pkNX.WinForms/Subforms/ShinyRate.resx
new file mode 100644
index 00000000..1af7de15
--- /dev/null
+++ b/pkNX.WinForms/Subforms/ShinyRate.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/pkNX.WinForms/pkNX.WinForms.csproj b/pkNX.WinForms/pkNX.WinForms.csproj
index 60de6133..25660b96 100644
--- a/pkNX.WinForms/pkNX.WinForms.csproj
+++ b/pkNX.WinForms/pkNX.WinForms.csproj
@@ -89,6 +89,12 @@
BTTE.cs
+
+ Form
+
+
+ ShinyRate.cs
+
Form
@@ -143,6 +149,9 @@
BTTE.cs
+
+ ShinyRate.cs
+
GenericEditor.cs