Add shiny rate editor

untested but probably works
This commit is contained in:
Kurt
2018-12-01 12:39:31 -08:00
parent 0062f6cf24
commit 0c0487f12f
8 changed files with 578 additions and 0 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
/// <summary>
/// Language to use when fetching string &amp; graphic assets.
/// </summary>

View File

@@ -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());
}
}
}

View File

@@ -0,0 +1,236 @@
namespace pkNX.WinForms
{
partial class ShinyRate
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@@ -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))}";
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -89,6 +89,12 @@
<Compile Include="Subforms\BTTE.designer.cs">
<DependentUpon>BTTE.cs</DependentUpon>
</Compile>
<Compile Include="Subforms\ShinyRate.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Subforms\ShinyRate.Designer.cs">
<DependentUpon>ShinyRate.cs</DependentUpon>
</Compile>
<Compile Include="Subforms\GenericEditor.cs">
<SubType>Form</SubType>
</Compile>
@@ -143,6 +149,9 @@
<EmbeddedResource Include="Subforms\BTTE.resx">
<DependentUpon>BTTE.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Subforms\ShinyRate.resx">
<DependentUpon>ShinyRate.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Subforms\GenericEditor.resx">
<DependentUpon>GenericEditor.cs</DependentUpon>
</EmbeddedResource>