From 0c0487f12f0df3fea116239c5ccd8a626db7a830 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 1 Dec 2018 12:39:31 -0800 Subject: [PATCH] Add shiny rate editor untested but probably works --- pkNX.Game/Editors/ShinyRate/ShinyRateGG.cs | 86 +++++++ pkNX.Game/Editors/ShinyRate/ShinyRateInfo.cs | 29 +++ pkNX.Game/GameManager.cs | 3 + pkNX.WinForms/MainEditor/EditorGG.cs | 23 ++ pkNX.WinForms/Subforms/ShinyRate.Designer.cs | 236 +++++++++++++++++++ pkNX.WinForms/Subforms/ShinyRate.cs | 72 ++++++ pkNX.WinForms/Subforms/ShinyRate.resx | 120 ++++++++++ pkNX.WinForms/pkNX.WinForms.csproj | 9 + 8 files changed, 578 insertions(+) create mode 100644 pkNX.Game/Editors/ShinyRate/ShinyRateGG.cs create mode 100644 pkNX.Game/Editors/ShinyRate/ShinyRateInfo.cs create mode 100644 pkNX.WinForms/Subforms/ShinyRate.Designer.cs create mode 100644 pkNX.WinForms/Subforms/ShinyRate.cs create mode 100644 pkNX.WinForms/Subforms/ShinyRate.resx 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