PKHeX/PKHeX.WinForms/Subforms/Save Editors/Gen4/SAV_Misc4.cs
Kurt 2c541ad422
Update to .NET 10 (#4676)
* Update to .NET 10
* Property fields
* API signature updates
* Extension method blocks

* Completed dark mode support
  Outside of my control:
- vertical tab control (pkm editor)
- datetimepicker controls
- lgpe event flags (no idea)
- some control types having white-borders when they should really be gray

Box background is 50% transparency to effectively darken the image.

* Custom legality report popup
* Event diff dialog, version select dialog
* Add quick overwrite popup for export sav
* Extension methods
* Dark Mode: glow currently editing sprite
* Add invalid encounter hint for trade evolutions
* Extension properties
* Append legality hint on hover card
* Slot image loading: clear the screen-reader description if a slot is empty/invalid, rather than retain the previous description. Changing boxes would easily confuse users on this.
2025-12-31 01:42:05 -06:00

1109 lines
37 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
using PKHeX.Core;
using static System.Buffers.Binary.BinaryPrimitives;
using static PKHeX.WinForms.PoketchDotMatrix;
namespace PKHeX.WinForms;
public partial class SAV_Misc4 : Form
{
private readonly SAV4 Origin;
private readonly SAV4 SAV;
private readonly Hall4? Hall;
private readonly Record4 Record;
private readonly string[] seals, accessories, backdrops, poketchapps;
private readonly string[] backdropsSorted;
public SAV_Misc4(SAV4 sav)
{
InitializeComponent();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
SAV = (SAV4)(Origin = sav).Clone();
seals = GameInfo.Strings.seals;
accessories = GameInfo.Strings.accessories;
backdrops = GameInfo.Strings.backdrops;
poketchapps = GameInfo.Strings.poketchapps;
backdropsSorted = [.. backdrops.Order()]; // sorted copy
StatNUDA = [NUD_Stat0, NUD_Stat1, NUD_Stat2, NUD_Stat3];
StatLabelA = [L_Stat0, L_Stat1, L_Stat2, L_Stat3]; // Current, Trade, Record, Trade
StatRBA = [RB_Stats3_01, RB_Stats3_02];
HallNUDA =
[
NUD_HallType01, NUD_HallType02, NUD_HallType03, NUD_HallType04, NUD_HallType05, NUD_HallType06,
NUD_HallType07, NUD_HallType08, NUD_HallType09, NUD_HallType10, NUD_HallType11, NUD_HallType12,
NUD_HallType13, NUD_HallType14, NUD_HallType15, NUD_HallType16, NUD_HallType17,
];
PrintButtonA = [BTN_PrintTower, BTN_PrintFactory, BTN_PrintHall, BTN_PrintCastle, BTN_PrintArcade];
Record = SAV.Records;
switch (sav)
{
case SAV4DP:
L_CurrentMap.Visible = CB_UpgradeMap.Visible = false;
GB_Prints.Visible = GB_Prints.Enabled = GB_Hall.Visible = GB_Hall.Enabled = GB_Castle.Visible = GB_Castle.Enabled = false;
BFF = [
[0, 1, 0x5FCA, 0x04, 0x6601],
];
break;
case SAV4Pt:
L_CurrentMap.Visible = CB_UpgradeMap.Visible = false;
PrintIndexStart = 79;
BFF = [
[0, 1, 0x68E0, 0x04, 0x723D],
[1, 0, 0x68F4, 0x10, 0x7EF8],
[0, 0, 0x6924, 0x18, 0x7EFC],
[2, 0, 0x696C, 0x10, 0x7F00],
[0, 0, 0x699C, 0x04, 0x7F04],
];
Hall = SAV.GetHall();
break;
case SAV4HGSS:
GB_Poketch.Visible = false;
PrintIndexStart = 77;
BFF = [
// { BFV, BFT, addr, 1BFTlen, checkBit
[0, 1, 0x5264, 0x04, 0x5BC1],
[1, 0, 0x5278, 0x10, 0x687C],
[0, 0, 0x52A8, 0x18, 0x6880],
[2, 0, 0x52F0, 0x10, 0x6884],
[0, 0, 0x5320, 0x04, 0x6888],
];
Hall = SAV.GetHall();
break;
default:
throw new ArgumentOutOfRangeException(nameof(sav), sav, null);
}
ReadMain();
ReadBattleFrontier();
if (SAV is SAV4Sinnoh s)
{
TC_Misc.Controls.Remove(TAB_Walker);
poffinCase4Editor1.Initialize(s);
TC_Misc.Controls.Remove(Tab_PokeGear);
}
else if (SAV is SAV4HGSS hgss)
{
pokeGear4Editor1.Initialize(hgss);
TC_Misc.Controls.Remove(Tab_Poffins);
}
}
private void B_Save_Click(object sender, EventArgs e)
{
SaveMain();
SaveBattleFrontier();
if (SAV is SAV4HGSS)
pokeGear4Editor1.Save();
else if (SAV is SAV4Sinnoh)
poffinCase4Editor1.Save();
Origin.CopyChangesFrom(SAV);
Close();
}
private void B_Cancel_Click(object sender, EventArgs e) => Close();
private const int FlyFlagStart = 2480;
private static ReadOnlySpan<byte> FlyWorkFlagSinnoh => [000, 001, 002, 003, 004, 005, 006, 007, 008, 009, 010, 011, 012, 013, 014, 015, 016, 017, 067, 068];
private static ReadOnlySpan<byte> LocationIDsSinnoh => [001, 002, 003, 004, 005, 082, 083, 006, 007, 008, 009, 010, 011, 012, 013, 014, 054, 081, 055, 015];
private static ReadOnlySpan<byte> FlyWorkFlagHGSS => [000, 001, 002, 003, 004, 005, 006, 007, 008, 009, 010, 011, 012, 013, 014, 015, 016, 017, 018, 019, 020, 021, 022, 027, 030, 033, 035];
private static ReadOnlySpan<byte> LocationIDsHGSS => [138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 229, 227, 221, 225];
private void ReadMain()
{
NUD_Coin.Maximum = SAV.MaxCoins;
NUD_Coin.Value = Math.Clamp(SAV.Coin, 0, SAV.MaxCoins);
NUD_BP.Value = Math.Clamp(SAV.BP, 0, 9999);
var locations = SAV is SAV4Sinnoh ? LocationIDsSinnoh : LocationIDsHGSS;
var flags = SAV is SAV4Sinnoh ? FlyWorkFlagSinnoh : FlyWorkFlagHGSS;
CLB_FlyDest.Items.Clear();
for (int i = 0; i < locations.Length; i++)
{
var flagIndex = FlyFlagStart + flags[i];
var state = SAV.GetEventFlag(flagIndex);
var locationID = locations[i];
var name = GameInfo.Strings.Gen4.Met0[locationID];
CLB_FlyDest.Items.Add(name, state);
}
if (SAV is SAV4Sinnoh sinnoh)
{
ReadPoketch(sinnoh);
NUD_UGFlags.Value = Math.Clamp(sinnoh.UG_FlagsCaptured, 0, SAV4Sinnoh.UG_MAX);
L_PokeathlonPoints.Visible = NUD_PokeathlonPoints.Visible = false;
}
else if (SAV is SAV4HGSS hgss)
{
ReadWalker(hgss);
ReadPokeathlon(hgss);
L_UGFlags.Visible = NUD_UGFlags.Visible = false;
ReadOnlySpan<string> items = ["Map Johto", "Map Johto+", "Map Johto & Kanto"];
var index = hgss.MapUnlockState;
if (index >= MapUnlockState4.Invalid)
index = MapUnlockState4.JohtoKanto;
foreach (var item in items)
CB_UpgradeMap.Items.Add(item);
CB_UpgradeMap.SelectedIndex = (int)index;
}
ReadSeals();
ReadAccessories();
ReadBackdrops();
ReadRecord();
}
private void SaveMain()
{
SAV.Coin = (uint)NUD_Coin.Value;
SAV.BP = (ushort)NUD_BP.Value;
var flags = SAV is SAV4Sinnoh ? FlyWorkFlagSinnoh : FlyWorkFlagHGSS;
for (int i = 0; i < CLB_FlyDest.Items.Count; i++)
{
var index = FlyFlagStart + flags[i];
SAV.SetEventFlag(index, CLB_FlyDest.GetItemChecked(i));
}
if (SAV is SAV4Sinnoh sinnoh)
{
SavePoketch(sinnoh);
sinnoh.UG_FlagsCaptured = (uint)NUD_UGFlags.Value;
}
else if (SAV is SAV4HGSS hgss)
{
SaveWalker(hgss);
SavePokeathlon(hgss);
hgss.MapUnlockState = (MapUnlockState4)CB_UpgradeMap.SelectedIndex;
}
SaveSeals();
SaveAccessories();
SaveBackdrops();
SaveRecord();
}
private void B_AllFlyDest_Click(object sender, EventArgs e)
{
for (int i = 0; i < CLB_FlyDest.Items.Count; i++)
CLB_FlyDest.SetItemChecked(i, true);
}
#region Poketch
private byte[] DotArtistByte = [];
private void ReadPoketch(SAV4Sinnoh s)
{
CB_CurrentApp.Items.Clear();
CLB_Poketch.Items.Clear();
for (PoketchApp i = 0; i <= PoketchApp.Alarm_Clock; i++)
{
var name = poketchapps[(int)i];
var title = $"{(int)i:00} - {name}";
CB_CurrentApp.Items.Add(name);
var value = s.GetPoketchAppUnlocked(i);
CLB_Poketch.Items.Add(title, value);
}
CB_CurrentApp.SelectedIndex = s.CurrentPoketchApp;
DotArtistByte = s.GetPoketchDotArtistData();
SetPictureBoxFromFlags(DotArtistByte);
const string tip = """
Guide about D&D ImageFile Format
width = 24px
height = 20px
used color count <= 4
file size < 2058byte
""";
tip1.SetToolTip(PB_DotArtist, tip);
TAB_Main.AllowDrop = true;
}
private void SavePoketch(SAV4Sinnoh s)
{
int unlockedCount = 0;
s.CurrentPoketchApp = (sbyte)CB_CurrentApp.SelectedIndex;
for (int i = 0; i < CLB_Poketch.Items.Count; i++)
{
var b = CLB_Poketch.GetItemChecked(i);
s.SetPoketchAppUnlocked((PoketchApp)i, b);
if (b) unlockedCount++;
}
s.SetPoketchDotArtistData(DotArtistByte);
s.PoketchUnlockedCount = (byte)unlockedCount;
}
private void SetPictureBoxFromFlags(ReadOnlySpan<byte> inp)
{
if (inp.Length != DotMatrixPixelCount / 4)
return;
var img = GetDotArt(inp);
PB_DotArtist.Image = img;
PB_DotArtist.Size = new(img.Width + 2, img.Height + 2); // 1px border
}
private void SetFlagsFromFileName(string fileName)
{
var dest = DotArtistByte;
TryBuild(fileName, dest);
}
private void SetFlagsFromClickPoint(int inpX, int inpY)
{
inpX = Math.Clamp(inpX, 0, (DotMatrixWidth * DotMatrixUpscaleFactor) - 1);
inpY = Math.Clamp(inpY, 0, (DotMatrixHeight * DotMatrixUpscaleFactor) - 1);
int i = (inpX >> 2) + (DotMatrixWidth * (inpY >> 2));
Span<byte> ndab = stackalloc byte[DotMatrixPixelCount / 4];
DotArtistByte.CopyTo(ndab);
byte c = (byte)((ndab[i >> 2] >> ((i % 4) << 1)) & 3);
if (++c >= 4)
c = 0;
ndab[i >> 2] &= (byte)~(3 << ((i % 4) << 1));
ndab[i >> 2] |= (byte)((c & 3) << ((i % 4) << 1));
ndab.CopyTo(DotArtistByte);
}
private void B_GiveAll_Click(object sender, EventArgs e)
{
// foreach (CheckBox c in Apps) c.Checked = true;
for (int i = 0; i < CLB_Poketch.Items.Count; i++)
CLB_Poketch.SetItemChecked(i, true);
System.Media.SystemSounds.Asterisk.Play();
}
private void TAB_Poketch_DragEnter(object? sender, DragEventArgs? e)
{
if (e?.Data is null)
return;
if (TAB_Main.AllowDrop && e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
else
e.Effect = DragDropEffects.None;
}
private void TAB_Poketch_DragDrop(object? sender, DragEventArgs? e)
{
if (!TAB_Main.AllowDrop)
return;
if (e?.Data?.GetData(DataFormats.FileDrop) is not string[] { Length: not 0 } files)
return;
SetFlagsFromFileName(files[0]);
SetPictureBoxFromFlags(DotArtistByte);
}
private void PB_DotArtist_MouseClick(object sender, MouseEventArgs e)
{
SetFlagsFromClickPoint(e.X, e.Y);
SetPictureBoxFromFlags(DotArtistByte);
}
#endregion
#region BattleFrontier
private readonly RadioButton[] StatRBA;
private readonly NumericUpDown[] StatNUDA;
private readonly Label[] StatLabelA;
private readonly NumericUpDown[] HallNUDA;
private readonly Button[] PrintButtonA;
private readonly int[][] BFF;
private readonly int PrintIndexStart;
private bool editing;
private string[][] BFT = null!;
private int[][] BFV = null!;
private bool HallStatUpdated;
private void ReadBattleFrontier()
{
BFV = [
[2, 0], // Max, Current
[2, 0, 3, 1], // Max, Current, Max(Trade), Current(Trade)
[2, 0, 1, -1, 3], // Max, Current, Current(CP), (UsedCP), Max(CP)
];
BFT = [
["Singles", "Doubles", "Multi"],
["Singles", "Doubles", "Multi (Trainer)", "Multi (Friend)", "Wi-Fi"],
];
if (SAV is not SAV4DP)
{
SetPrintColors(PrintButtonA);
ReadOnlySpan<string> typeNames = GameInfo.Strings.types;
ReadOnlySpan<byte> typenameIndex = [0, 9, 10, 12, 11, 14, 1, 3, 4, 2, 13, 6, 5, 7, 15, 16, 8];
for (int i = 0; i < HallNUDA.Length; i++)
tip2.SetToolTip(HallNUDA[i], typeNames[typenameIndex[i]]);
}
if (Hall is null)
NUD_HallStreaks.Visible = NUD_HallStreaks.Enabled = false;
editing = true;
CB_Stats1.Items.Clear();
for (BattleFrontierFacility4 i = 0; i <= SAV.MaxFacility; i++)
CB_Stats1.Items.Add(i.ToString());
StatRBA[0].Checked = true;
// Clear Listbox and ComboBox
CB_Species.Items.Clear();
// Fill List
CB_Species.InitializeBinding();
var speciesList = GameInfo.FilteredSources.Species.Skip(1).ToList();
CB_Species.DataSource = new BindingSource(speciesList, string.Empty);
editing = false;
CB_Stats1.SelectedIndex = 0;
}
private void SaveBattleFrontier()
{
if (HallStatUpdated)
Hall?.RefreshChecksum();
}
private void SetPrintColors(ReadOnlySpan<Control> controls)
{
for (int i = 0; i < controls.Length; i++)
{
var pb = controls[i];
var workIndex = PrintIndexStart + i;
var value = SAV.GetWork(workIndex);
SetPrintColor(pb, (BattleFrontierPrintStatus4)value);
}
}
private static void SetPrintColor(Control pb, BattleFrontierPrintStatus4 value)
{
bool ready = value is BattleFrontierPrintStatus4.FirstReady or BattleFrontierPrintStatus4.SecondReady;
if (ready)
pb.ForeColor = Color.Red;
else if (value != 0)
pb.ForeColor = Color.Green;
else
pb.ResetForeColor();
if (value is BattleFrontierPrintStatus4.FirstReady or BattleFrontierPrintStatus4.FirstReceived)
pb.BackColor = Color.Silver;
else if (value is BattleFrontierPrintStatus4.SecondReady or BattleFrontierPrintStatus4.SecondReceived)
pb.BackColor = Color.Gold;
else
pb.ResetBackColor();
}
private void BTN_Print_Click(object sender, EventArgs e)
{
if (sender is not Button b)
return;
int index = PrintButtonA.IndexOf(b);
if (index < 0)
return;
index += PrintIndexStart;
var current = SAV.GetWork(index);
current++;
if (current > (int)BattleFrontierPrintStatus4.SecondReceived)
current = 0;
SAV.SetWork(index, current);
SetPrintColor(b, (BattleFrontierPrintStatus4)current);
}
private void ChangeStat1(object sender, EventArgs e)
{
if (editing)
return;
int facility = CB_Stats1.SelectedIndex;
if (facility < 0)
return;
editing = true;
CB_Stats2.Items.Clear();
CB_Stats2.Items.AddRange(BFT[BFF[facility][1]]);
StatRBA[0].Checked = true;
foreach (RadioButton rb in StatRBA)
rb.Visible = rb.Enabled = facility == 1;
for (int i = 0; i < StatLabelA.Length; i++)
StatLabelA[i].Visible = StatLabelA[i].Enabled = StatNUDA[i].Visible = StatNUDA[i].Enabled = BFV[BFF[facility][0]].Contains(i);
if (facility == 0)
{
StatLabelA[1].Visible = StatLabelA[1].Enabled = StatNUDA[1].Visible = StatNUDA[1].Enabled = true;
StatLabelA[1].Text = "Continue";
StatNUDA[1].Maximum = 65535;
}
else
{
if (StatNUDA[1].Value > 9999)
StatNUDA[1].Value = 9999;
StatNUDA[1].Maximum = 9999;
}
if (facility == 1)
StatLabelA[1].Text = StatLabelA[3].Text = "Trade";
else if (facility == 3)
StatLabelA[1].Text = StatLabelA[3].Text = "CP";
GB_Hall.Visible = facility == 2;
GB_Castle.Visible = facility == 3;
editing = false;
CB_Stats2.SelectedIndex = 0;
}
private void ChangeStat(object sender, EventArgs e)
{
if (editing)
return;
if (sender is RadioButton { Checked: false })
return;
StatAddrControl(SetValToSav: -2, SetSavToVal: true);
if (GB_Hall.Visible && CB_Stats2.SelectedItem is string sH)
{
GB_Hall.Text = $"Battle Hall ({sH})";
editing = true;
GetHallStat();
editing = false;
}
else if (GB_Castle.Visible && CB_Stats2.SelectedItem is string sC)
{
GB_Castle.Text = $"Battle Castle ({sC})";
editing = true;
GetCastleStat();
editing = false;
}
}
private void StatAddrControl(int SetValToSav = -2, bool SetSavToVal = false)
{
int Facility = CB_Stats1.SelectedIndex;
int BattleType = CB_Stats2.SelectedIndex;
int RBi = StatRBA[1].Checked ? 1 : 0;
int addrVal = BFF[Facility][2] + (BFF[Facility][3] * BattleType) + (RBi << 3);
int addrFlag = BFF[Facility][4];
byte maskFlag = (byte)(1 << (BattleType + (RBi << 2)));
int TowerContinueCountOfs = SAV is SAV4DP ? 3 : 1;
var general = SAV.General;
if (SetSavToVal)
{
editing = true;
for (int i = 0; i < BFV[BFF[Facility][0]].Length; i++)
{
if (BFV[BFF[Facility][0]][i] < 0)
continue;
int vali = ReadUInt16LittleEndian(general[(addrVal + (i << 1))..]);
StatNUDA[BFV[BFF[Facility][0]][i]].Value = vali > 9999 ? 9999 : vali;
}
CHK_Continue.Checked = (SAV.General[addrFlag] & maskFlag) != 0;
if (Facility == 0) // tower continue count
StatNUDA[1].Value = ReadUInt16LittleEndian(general[(addrFlag + TowerContinueCountOfs + (BattleType << 1))..]);
editing = false;
return;
}
if (SetValToSav >= 0)
{
ushort val = (ushort)StatNUDA[SetValToSav].Value;
if (Facility == 0 && SetValToSav == 1) // tower continue count
{
var offset = addrFlag + TowerContinueCountOfs + (BattleType << 1);
WriteUInt16LittleEndian(general[offset..], val);
}
SetValToSav = BFV[BFF[Facility][0]].IndexOf(SetValToSav);
if (SetValToSav < 0)
return;
var clamp = Math.Min((ushort)9999, val);
WriteUInt16LittleEndian(general[(addrVal + (SetValToSav << 1))..], clamp);
return;
}
if (SetValToSav == -1)
{
if (CHK_Continue.Checked)
{
general[addrFlag] |= maskFlag;
if (Facility == 3)
general[addrFlag + 1] |= 0x01; // not found what this flag means
}
else
{
general[addrFlag] &= (byte)~maskFlag;
}
}
}
private void ChangeStatVal(object sender, EventArgs e)
{
if (editing)
return;
int n = StatNUDA.IndexOf(sender);
if (n < 0)
return;
StatAddrControl(SetValToSav: n, SetSavToVal: false);
if (CB_Stats1.SelectedIndex != 0)
return;
const int bias = 7;
var n0 = StatNUDA[0];
var n1 = StatNUDA[1];
if (Math.Floor(n0.Value / bias) == n1.Value)
return;
if (n == 0)
{
n1.Value = Math.Floor(n0.Value / bias);
}
else if (n == 1)
{
if (n0.Maximum > n1.Value * bias)
n0.Value = n1.Value * bias;
else if (n0.Value < n0.Maximum)
n0.Value = n0.Maximum;
}
}
private void CHK_Continue_CheckedChanged(object sender, EventArgs e)
{
if (editing)
return;
StatAddrControl(SetValToSav: -1, SetSavToVal: false);
}
private ushort species = ushort.MaxValue;
private void ChangeSpecies(object sender, EventArgs e)
{
species = (ushort)WinFormsUtil.GetIndex(CB_Species);
if (editing)
return;
editing = true;
GetHallStat();
editing = false;
}
private void GetCastleStat()
{
int ofs = BFF[3][2] + (BFF[3][3] * CB_Stats2.SelectedIndex) + 0x0A;
NumericUpDown[] na = [NUD_CastleRankRcv, NUD_CastleRankItem, NUD_CastleRankInfo];
for (int i = 0; i < na.Length; i++)
{
int val = ReadInt16LittleEndian(SAV.General[(ofs + (i << 1))..]);
na[i].Value = val > na[i].Maximum ? na[i].Maximum : val < na[i].Minimum ? na[i].Minimum : val;
}
}
private void NUD_CastleRank_ValueChanged(object sender, EventArgs e)
{
if (editing)
return;
NumericUpDown[] na = [NUD_CastleRankRcv, NUD_CastleRankItem, NUD_CastleRankInfo];
int i = na.IndexOf(sender);
if (i < 0)
return;
var offset = BFF[3][2] + (BFF[3][3] * CB_Stats2.SelectedIndex) + 0x0A + (i << 1);
WriteInt32LittleEndian(SAV.General[offset..], (int)na[i].Value);
}
private void GetHallStat()
{
int ofscur = BFF[2][2] + (BFF[2][3] * CB_Stats2.SelectedIndex);
var curspe = ReadUInt16LittleEndian(SAV.General[(ofscur + 4)..]);
bool c = curspe == species;
CHK_HallCurrent.Checked = c;
CHK_HallCurrent.Text = curspe > 0 && curspe <= SAV.MaxSpeciesID
? $"Current: {SpeciesName.GetSpeciesNameGeneration(curspe, GameLanguage.GetLanguageIndex(Main.CurrentLanguage), 4)}"
: "Current: (None)";
int s = 0;
for (int i = 0; i < HallNUDA.Length; i++)
{
var d = c ? Math.Min(10, (SAV.General[ofscur + 6 + ((i >> 1) << 1)] >> ((i & 1) << 2)) & 0x0F) : 0;
HallNUDA[i].Value = d;
HallNUDA[i].Enabled = c;
s += d;
}
L_SumHall.Text = s.ToString();
if (Hall is not null)
{
ushort v = Hall.GetCount(CB_Stats2.SelectedIndex, species);
NUD_HallStreaks.Value = Math.Min((ushort)9999, v);
}
}
private void CHK_HallCurrent_CheckedChanged(object sender, EventArgs e)
{
if (editing)
return;
var offset = BFF[2][2] + (BFF[2][3] * CB_Stats2.SelectedIndex) + 4;
ushort value = (ushort)(CHK_HallCurrent.Checked ? species : 0);
WriteUInt16LittleEndian(SAV.General[offset..], value);
editing = true;
GetHallStat();
editing = false;
}
private void NUD_HallType_ValueChanged(object sender, EventArgs e)
{
if (editing)
return;
int i = HallNUDA.IndexOf(sender);
if (i < 0)
return;
int ofs = BFF[2][2] + (BFF[2][3] * CB_Stats2.SelectedIndex) + 6 + ((i >> 1) << 1);
SAV.General[ofs] = (byte)((SAV.General[ofs] & ~(0xF << ((i & 1) << 2))) | ((int)HallNUDA[i].Value << ((i & 1) << 2)));
L_SumHall.Text = HallNUDA.Sum(x => x.Value).ToString(CultureInfo.InvariantCulture);
}
private void NUD_HallStreaks_ValueChanged(object sender, EventArgs e)
{
if (editing || Hall is null)
return;
Hall.SetCount(CB_Stats2.SelectedIndex, species, (ushort)NUD_HallStreaks.Value);
HallStatUpdated = true;
}
#endregion
#region Walker
private void ReadWalker(SAV4HGSS s)
{
ReadOnlySpan<string> walkercourses = GameInfo.Sources.Strings.walkercourses;
CLB_WalkerCourses.Items.Clear();
foreach (var name in walkercourses)
CLB_WalkerCourses.Items.Add(name);
ReadWalkerCourseUnlockFlags(s);
NUD_Watts.Value = s.PokewalkerWatts;
NUD_Steps.Value = s.PokewalkerSteps;
}
private void ReadWalkerCourseUnlockFlags(SAV4HGSS s)
{
Span<bool> courses = stackalloc bool[SAV4HGSS.PokewalkerCourseFlagCount];
s.GetPokewalkerCoursesUnlocked(courses);
for (int i = 0; i < CLB_WalkerCourses.Items.Count; i++)
CLB_WalkerCourses.SetItemChecked(i, courses[i]);
}
private void SaveWalker(SAV4HGSS s)
{
Span<bool> courses = stackalloc bool[SAV4HGSS.PokewalkerCourseFlagCount];
for (int i = 0; i < CLB_WalkerCourses.Items.Count; i++)
courses[i] = CLB_WalkerCourses.GetItemChecked(i);
s.SetPokewalkerCoursesUnlocked(courses);
s.PokewalkerWatts = (uint)NUD_Watts.Value;
s.PokewalkerSteps = (uint)NUD_Steps.Value;
}
private void B_AllWalkerCourses_Click(object sender, EventArgs e)
{
if (SAV is not SAV4HGSS s)
throw new Exception("Invalid SAV type");
s.PokewalkerCoursesUnlockAll();
ReadWalkerCourseUnlockFlags(s);
}
#endregion
private void ReadPokeathlon(SAV4HGSS s)
{
NUD_PokeathlonPoints.Value = s.PokeathlonPoints;
}
private void SavePokeathlon(SAV4HGSS s)
{
s.PokeathlonPoints = (uint)NUD_PokeathlonPoints.Value;
}
#region Seals
private void ReadSeals()
{
DGV_Seals.Rows.Clear();
DGV_Seals.Columns.Clear();
DataGridViewTextBoxColumn dgvSlot = new()
{
HeaderText = "Slot",
DisplayIndex = 0,
Width = 145,
ReadOnly = true,
};
DataGridViewTextBoxColumn dgvCount = new()
{
DisplayIndex = 1,
Width = 45,
DefaultCellStyle = { Alignment = DataGridViewContentAlignment.MiddleCenter },
MaxInputLength = 2, // 0-99
};
DGV_Seals.Columns.Add(dgvSlot);
DGV_Seals.Columns.Add(dgvCount);
const int count = (int)Seal4.MAX;
DGV_Seals.Rows.Add(count);
for (int i = 0; i < count; i++)
DGV_Seals.Rows[i].Cells[0].Value = seals[i];
LoadSealsCount();
}
private void LoadSealsCount()
{
for (int i = 0; i < (int)Seal4.MAX; i++)
DGV_Seals.Rows[i].Cells[1].Value = SAV.GetSealCount((Seal4)i).ToString();
}
public void ClearSeals()
{
for (int i = 0; i < (int)Seal4.MAX; i++)
DGV_Seals.Rows[i].Cells[1].Value = "0";
}
public void SetAllSeals(bool unreleased = false)
{
var sealIndexCount = (int)(unreleased ? Seal4.MAX : Seal4.MAXLEGAL);
for (int i = 0; i < sealIndexCount; i++)
DGV_Seals.Rows[i].Cells[1].Value = SAV4.SealMaxCount.ToString();
}
private void SaveSeals()
{
for (int i = 0; i < (int)Seal4.MAX; i++)
{
var cells = DGV_Seals.Rows[i].Cells;
var count = int.TryParse(cells[1].Value?.ToString() ?? "0", out var val) ? val : 0;
SAV.SetSealCount((Seal4)i, (byte)Math.Clamp(count, 0, byte.MaxValue));
}
}
private void B_ClearSeals_Click(object sender, EventArgs e) => ClearSeals();
private void OnBAllSealsLegalOnClick(object sender, EventArgs e)
{
bool setUnreleasedIndexes = sender == B_AllSealsIllegal;
SetAllSeals(setUnreleasedIndexes);
System.Media.SystemSounds.Asterisk.Play();
}
#endregion
#region Accessories
private void ReadAccessories()
{
DGV_Accessories.Rows.Clear();
DGV_Accessories.Columns.Clear();
DataGridViewTextBoxColumn dgvSlot = new()
{
HeaderText = "Slot",
DisplayIndex = 0,
Width = 140,
ReadOnly = true,
};
DataGridViewTextBoxColumn dgvCount = new()
{
DisplayIndex = 1,
Width = 50,
DefaultCellStyle = { Alignment = DataGridViewContentAlignment.MiddleCenter },
MaxInputLength = 1, // 0-9
};
DGV_Accessories.Columns.Add(dgvSlot);
DGV_Accessories.Columns.Add(dgvCount);
const int count = AccessoryInfo.Count;
DGV_Accessories.Rows.Add(count);
for (int i = 0; i < count; i++)
DGV_Accessories.Rows[i].Cells[0].Value = accessories[i];
LoadAccessoriesCount();
}
private void LoadAccessoriesCount()
{
for (int i = 0; i < AccessoryInfo.Count; i++)
DGV_Accessories.Rows[i].Cells[1].Value = SAV.GetAccessoryOwnedCount((Accessory4)i).ToString();
}
public void ClearAccessories()
{
for (int i = 0; i < AccessoryInfo.Count; i++)
DGV_Accessories.Rows[i].Cells[1].Value = "0";
}
public void SetAllAccessories(bool unreleased = false)
{
for (int i = 0; i <= AccessoryInfo.MaxMulti; i++)
DGV_Accessories.Rows[i].Cells[1].Value = AccessoryInfo.AccessoryMaxCount.ToString();
var count = unreleased ? AccessoryInfo.Count : (AccessoryInfo.MaxLegal + 1);
for (int i = AccessoryInfo.MaxMulti + 1; i < count; i++)
DGV_Accessories.Rows[i].Cells[1].Value = "1";
}
private void SaveAccessories()
{
for (int i = 0; i < AccessoryInfo.Count; i++)
{
var cells = DGV_Accessories.Rows[i].Cells;
var count = int.TryParse(cells[1].Value?.ToString() ?? "0", out var val) ? val : 0;
SAV.SetAccessoryOwnedCount((Accessory4)i, (byte)Math.Clamp(count, 0, byte.MaxValue));
}
}
private void B_ClearAccessories_Click(object sender, EventArgs e) => ClearAccessories();
private void OnBAllAccessoriesLegalOnClick(object sender, EventArgs e)
{
bool setUnreleasedIndexes = sender == B_AllAccessoriesIllegal;
SetAllAccessories(setUnreleasedIndexes);
System.Media.SystemSounds.Asterisk.Play();
}
#endregion
#region Backdrops
private void ReadBackdrops()
{
DGV_Backdrops.Rows.Clear();
DGV_Backdrops.Columns.Clear();
DataGridViewComboBoxColumn dgv = new()
{
HeaderText = "Slot",
DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing,
DisplayIndex = 0,
Width = 190,
FlatStyle = FlatStyle.Flat,
DataSource = new BindingSource(backdropsSorted, string.Empty),
};
DGV_Backdrops.Columns.Add(dgv);
LoadBackdropPositions();
}
private void LoadBackdropPositions()
{
const int count = BackdropInfo.Count;
DGV_Backdrops.Rows.Add(count);
ClearBackdrops();
for (int i = 0; i < count; i++)
{
var pos = SAV.GetBackdropPosition((Backdrop4)i);
if (pos < BackdropInfo.Count)
DGV_Backdrops.Rows[pos].Cells[0].Value = backdrops[i];
}
}
private void ClearBackdrops()
{
for (int i = 0; i < BackdropInfo.Count; i++)
DGV_Backdrops.Rows[i].Cells[0].Value = backdrops[(int)Backdrop4.Unset];
}
public void SetAllBackdrops(bool unreleased = false)
{
var count = unreleased ? BackdropInfo.Count : ((int)BackdropInfo.MaxLegal + 1);
for (int i = 0; i < count; i++)
DGV_Backdrops.Rows[i].Cells[0].Value = backdrops[i];
}
private void SaveBackdrops()
{
for (int i = 0; i < BackdropInfo.Count; i++)
SAV.RemoveBackdrop((Backdrop4)i); // clear all slots
byte ctr = 0;
for (int i = 0; i < BackdropInfo.Count; i++)
{
var bd = (Backdrop4)backdrops.IndexOf(DGV_Backdrops.Rows[i].Cells[0].Value);
if (bd.IsUnset()) // skip empty slots
continue;
SAV.SetBackdropPosition(bd, ctr);
ctr++;
}
}
private void B_ClearBackdrops_Click(object sender, EventArgs e) => ClearBackdrops();
private void OnBAllBackdropsLegalOnClick(object sender, EventArgs e)
{
bool setUnreleasedIndexes = sender == B_AllBackdropsIllegal;
SetAllBackdrops(setUnreleasedIndexes);
System.Media.SystemSounds.Asterisk.Play();
}
#endregion
#region Records
private void ReadRecord()
{
NUD_Record16.Maximum = Record4.Record16 - 1;
NUD_Record32.Maximum = Record.Record32 - 1;
NUD_Record16V.Value = Record.GetRecord16(0);
NUD_Record32V.Value = Record.GetRecord32(0);
NUD_Record16V.ValueChanged += (_, _) => Record.SetRecord16((int)NUD_Record16.Value, (ushort)NUD_Record16V.Value);
NUD_Record32V.ValueChanged += (_, _) => Record.SetRecord32((int)NUD_Record32.Value, (uint)NUD_Record32V.Value);
NUD_Record16.ValueChanged += (_, _) => NUD_Record16V.Value = Record.GetRecord16((int)NUD_Record16.Value);
NUD_Record32.ValueChanged += (_, _) => NUD_Record32V.Value = Record.GetRecord32((int)NUD_Record32.Value);
}
private void SaveRecord() => Record.EndAccess();
#endregion
}
public static class PoketchDotMatrix
{
public const int DotMatrixHeight = 20;
public const int DotMatrixWidth = 24;
public const int DotMatrixPixelCount = DotMatrixHeight * DotMatrixWidth;
public static ReadOnlySpan<byte> ColorTable => [248, 168, 88, 8];
public static bool TryBuild(string fileName, Span<byte> result)
{
if (FileUtil.GetFileSize(fileName) > 0x80A)
return false;
using var bmp = (Bitmap)Image.FromFile(fileName);
if (bmp.Width != DotMatrixWidth || bmp.Height != DotMatrixHeight)
return false;
Span<byte> brightMap = stackalloc byte[DotMatrixPixelCount];
Span<byte> brightCount = stackalloc byte[0x100];
if (!TryBuildBrightMap(bmp, brightMap, brightCount, out int colorCount))
return false;
Build(colorCount, brightCount, brightMap, result);
return true;
}
private static bool TryBuildBrightMap(Bitmap bmp, Span<byte> brightMap, Span<byte> brightCount, out int colorCount)
{
for (int iy = 0; iy < DotMatrixHeight; iy++) // Height
{
for (int ix = 0; ix < DotMatrixWidth; ix++) // Width
{
var ig = (byte)(0xFF * bmp.GetPixel(ix, iy).GetBrightness());
brightMap[ix + (DotMatrixWidth * iy)] = ig;
brightCount[ig]++;
}
}
colorCount = brightCount.Length - brightCount.Count<byte>(0);
return (colorCount - 1) <= 3; // 1-4
}
private static void Build(int colorCount, Span<byte> brightCount, Span<byte> brightMap, Span<byte> result)
{
int errorsMin = int.MaxValue;
Span<byte> LCT = stackalloc byte[4];
for (int i = 0; i < LCT.Length; i++)
LCT[i] = (byte)(colorCount < i + 1 ? 4 : colorCount - i - 1);
Span<byte> mLCT = stackalloc byte[4];
Span<byte> iBrightCount = stackalloc byte[0x100];
int ee = 0;
while (++ee < 1000)
{
brightCount.CopyTo(iBrightCount);
for (int i = 0, j = 0; i < iBrightCount.Length; i++)
{
if (iBrightCount[i] > 0)
iBrightCount[i] = LCT[j++];
}
var errorsTotal = 0;
for (int i = 0; i < DotMatrixPixelCount; i++)
errorsTotal += Math.Abs(brightMap[i] - ColorTable[iBrightCount[brightMap[i]]]);
if (errorsMin > errorsTotal)
{
errorsMin = errorsTotal;
LCT.CopyTo(mLCT);
}
GetNextLCT(LCT);
if (LCT[0] >= 4)
break;
}
for (int i = 0, j = 0; i < brightCount.Length; i++)
{
if (brightCount[i] > 0)
brightCount[i] = mLCT[j++];
}
for (int i = 0; i < brightMap.Length; i++)
brightMap[i] = brightCount[brightMap[i]];
for (int i = 0; i < brightMap.Length; i++)
result[i >> 2] |= (byte)((brightMap[i] & 3) << ((i % 4) << 1));
}
private static void GetNextLCT(Span<byte> inp)
{
while (true)
{
if (++inp[0] < 4)
continue;
inp[0] = 0;
if (++inp[1] < 4)
continue;
inp[1] = 0;
if (++inp[2] < 4)
continue;
inp[2] = 0;
if (++inp[3] < 4)
continue;
inp[0] = 4;
return;
}
}
public const int DotMatrixUpscaleFactor = 4;
public static Bitmap GetDotArt(ReadOnlySpan<byte> inp)
{
byte[] dupbyte = new byte[23040];
for (int iy = 0; iy < DotMatrixHeight; iy++)
{
for (int ix = 0; ix < DotMatrixWidth; ix++)
{
var ib = ix + (DotMatrixWidth * iy);
var ict = ColorTable[(inp[ib >> 2] >> ((ib % 4) << 1)) & 3];
var iz = (12 * ix) + (0x480 * iy);
for (int izy = 0; izy < 4; izy++)
{
for (int izx = 0; izx < 4; izx++)
{
for (int ic = 0; ic < 3; ic++)
dupbyte[ic + (3 * izx) + (0x120 * izy) + iz] = ict;
}
}
}
}
var dabmp = new Bitmap(DotMatrixWidth * DotMatrixUpscaleFactor, DotMatrixHeight * DotMatrixUpscaleFactor);
var dabdata = dabmp.LockBits(new Rectangle(0, 0, dabmp.Width, dabmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
System.Runtime.InteropServices.Marshal.Copy(dupbyte, 0, dabdata.Scan0, dupbyte.Length);
dabmp.UnlockBits(dabdata);
return dabmp;
}
}