Skip to content

Commit

Permalink
Minor tweaks
Browse files Browse the repository at this point in the history
No functional change

* add more xmldoc/comments to document things
* Show counts for valuables found regardless of [0,59] or [m,n] seconds.
* Adjust specific time increment based on current Seconds value
* Clear result grid if inputs aren't valid rather than retain last result & different textbox.
* Adjacent seed entry now uses NumericUpDown for better interaction. Pasting into a MaskedTextBox was janky, and this NUD can clamp values entered nicely.
  • Loading branch information
kwsch committed Apr 12, 2024
1 parent 72d5ab3 commit d113253
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 57 deletions.
31 changes: 28 additions & 3 deletions ItemPrinterDeGacha.Core/ItemPrinter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,27 @@ public static class ItemPrinter
// Player has both bonus modes unlocked -- otherwise, the bonus mode check won't call rand().
// Player has unlocked Stellar Tera Shards -- the rand max would be different (less).

// Rough summary of the item printer logic:
// Once the games load the table, they sort the rewards by ascending weight (lowest first).
// Then, the game determines the "total weight" by summing all weights.
// Stellar Tera Shards require a specific event flag (storyline progress) -- we assume it's unlocked.
// For each reward printed, the game will give a 2% chance to unlock a bonus mode on the next print set.
// When determining which item to award:
// Roll a random number between 0 and total weight.
// Find the item whose weight range contains the random number.
// - Iterate from Index 0 in the reward table;
// - Subtract the weight from the random number until it's 0 or negative.
// - The item at the current index is the one to award.
// To save time, the game precomputes a "jump table" to find the item index directly from the random number.

// Max values for the random number generator.
// Sum of all weights in the table + 1. Same for both modes, but listed separately for clarity.
private const uint ItemRandMax = 10001;
private const uint BallRandMax = 10001;

// Reward tables for both modes.
// Ball Table json is a different format, but it's manually adjusted (min/max counts are always 1 or 5).
// This allows for a single routine to handle both tables.
// This allows for a single Print routine to handle both tables.
private static readonly LotteryItemValue[] ItemTable;
private static readonly LotteryItemValue[] BallTable;

Expand Down Expand Up @@ -56,6 +69,9 @@ public static class ItemPrinter
/// </summary>
static ItemPrinter()
{
// The games store the regular lottery table in a FlatBuffer format.
// These resources have been converted to JSON for easier parsing.
// Read the resource as byte[] from the dll and let the utf8 parser decode it from json.
var resource = Properties.Resources.item_table_array;
var text = new Utf8JsonReader(resource);
var regular = JsonSerializer.Deserialize<LotteryRoot>(ref text)!.Table;
Expand All @@ -75,8 +91,15 @@ static ItemPrinter()
BallJump = GenerateJumpTable(BallTable, BallRandMax);
}

/// <summary>
/// Generates a jump table for the lottery rand() results to a specific reward index.
/// </summary>
/// <param name="table">Reward table to generate the jump table for.</param>
/// <param name="maxRand">Maximum rand() result value from the RNG.</param>
private static byte[] GenerateJumpTable(ReadOnlySpan<LotteryItemValue> table, [ConstantExpected] uint maxRand)
{
// The jump table is a precomputed array to quickly find the item index from the rand() result.
// The game uses <= instead of <, resulting in the [0]th item having n+1 weight.
var result = new byte[maxRand];
uint index = maxRand - 1u;
for (int i = table.Length - 1; i >= 0; i--)
Expand All @@ -90,8 +113,9 @@ private static byte[] GenerateJumpTable(ReadOnlySpan<LotteryItemValue> table, [C

// Debug Util
item.MaxRoll = max;
item.MinRoll = i == 0 ? 0 : min;
item.MinRoll = min;
}
table[0].MinRoll = 0; // Handle quirk where the first item has n+1 weight.
return result;
}

Expand All @@ -104,7 +128,8 @@ private static byte[] GenerateJumpTable(ReadOnlySpan<LotteryItemValue> table, [C
public static bool TableHasItem(PrintMode mode, ushort itemId)
{
var table = mode == BallBonus ? Balls : Items;
return table.Contains(itemId);
// Item IDs are sorted, so we can use a binary search for faster results.
return table.BinarySearch(itemId) >= 0;
}

/// <summary>
Expand Down
13 changes: 13 additions & 0 deletions ItemPrinterDeGacha.Core/SeedSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@

namespace ItemPrinterDeGacha.Core;

/// <summary>
/// Search routines for finding seeds that match certain criteria.
/// </summary>
public static class SeedSearch
{
/// <summary>
/// Searches for the next activation of a bonus mode.
/// </summary>
/// <param name="startTicks">Starting seed to search from.</param>
/// <param name="targetMode">Desired bonus mode to find.</param>
/// <param name="itemId">Optional item ID to search for.</param>
/// <returns>The seed that activates the bonus mode.</returns>
/// <remarks>
/// Iterates upwards from the starting seed until the desired bonus mode is found.
/// </remarks>
public static ulong FindNextBonusMode(ulong startTicks, PrintMode targetMode, int itemId = 0)
{
if (targetMode is not (ItemBonus or BallBonus))
Expand Down
26 changes: 12 additions & 14 deletions ItemPrinterDeGacha.WinForms/Controls/AdjacentViewer.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 21 additions & 4 deletions ItemPrinterDeGacha.WinForms/Controls/AdjacentViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,47 @@ public partial class AdjacentViewer : UserControl
public AdjacentViewer()
{
InitializeComponent();
NUD_Seed.Minimum = TimeUtil.MinSeed;
NUD_Seed.Maximum = TimeUtil.MaxSeed;
NUD_Seed.Text = ""; // Clear the entry so users can directly paste
CB_Mode.Items.AddRange(Program.Localization.LocalizeEnum<PrintMode>());
CB_Mode.SelectedIndex = 2; // Ball
CB_Count.SelectedIndex = CB_Count.Items.Count - 1; // Default to 10
NUD_Seed.ValueChanged += (_, _) => TryPrint();
NUD_Seed.TextChanged += (_, _) => TryPrintText();
CB_Mode.SelectedIndexChanged += (_, _) => TryPrint();
CB_Count.SelectedIndexChanged += (_, _) => TryPrint();
}

private void MTB_Seed_TextChanged(object sender, EventArgs e) => TryPrint();
private void B_MinusOne_Click(object sender, EventArgs e) => ChangeSeed(-1);
private void B_AddOne_Click(object sender, EventArgs e) => ChangeSeed(+1);

private void ChangeSeed(int bias)
{
if (!TimeUtil.TryGetValidSeed(MTB_Seed.Text, out ulong seed))
ulong seed = (ulong)NUD_Seed.Value + (ulong)bias;
if (!TimeUtil.IsValidSeed(seed))
{
System.Media.SystemSounds.Beep.Play();
return;
}
MTB_Seed.Text = (seed + (ulong)bias).ToString();
NUD_Seed.Value = seed;
}

private void TryPrintText()
{
if (!TimeUtil.TryGetValidSeed(NUD_Seed.Text, out ulong seed))
{
if (NUD_Seed.Text.Length > 0)
System.Media.SystemSounds.Beep.Play();
return;
}
Print(seed);
}

private void TryPrint()
{
if (!TimeUtil.TryGetValidSeed(MTB_Seed.Text, out ulong seed))
ulong seed = (ulong)NUD_Seed.Value;
if (!TimeUtil.IsValidSeed(seed))
{
System.Media.SystemSounds.Beep.Play();
return;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 37 additions & 10 deletions ItemPrinterDeGacha.WinForms/Controls/BallSearch.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using ItemPrinterDeGacha.Core;
using static ItemPrinterDeGacha.WinForms.SearchModeBall;

Expand All @@ -19,6 +20,8 @@ public BallSearch()
CB_Item.InitializeBinding();
CB_Item.DataSource = new BindingSource(items, null);
CB_Item.SelectedValue = 1; // Master Ball

UpdateIncrement();
}

private void B_Search_Click(object sender, EventArgs e)
Expand Down Expand Up @@ -54,7 +57,7 @@ private void B_Search_Click(object sender, EventArgs e)
else if (search == MaxValuables)
SearchAnyTimeValuables(ticks, count, tmp);
else
Populate(Program.Localization.ErrorInvalidSearchCriteria);
PopulateError(Program.Localization.ErrorInvalidSearchCriteria);
}
else
{
Expand All @@ -65,7 +68,7 @@ private void B_Search_Click(object sender, EventArgs e)
else if (search == MaxValuables)
SearchValuables(min, max, ticks, tmp, seed, count);
else
Populate(Program.Localization.ErrorInvalidSearchCriteria);
PopulateError(Program.Localization.ErrorInvalidSearchCriteria);
}
}

Expand All @@ -89,10 +92,13 @@ private void SearchSpecificItem(int item, uint min, uint max, ulong ticks, Span<
{
if (item == 0)
{
Populate(Program.Localization.ErrorNoItem);
PopulateError(Program.Localization.ErrorNoItem);
return;
}

// Print the items for each second in the range, and count the number of the specific item.
// If the count is higher than the previous highest count, update the result.
// If the count is the same, keep the previous result.
int c = -1;
ulong result = 0;
do
Expand All @@ -114,14 +120,20 @@ private void SearchSpecificItem(int item, uint min, uint max, ulong ticks, Span<
result = check;
}

ticks += 60;
ticks += 60; // Next minute
}
while (ticks - seed < count);
Populate(result, tmp.Length);
// Append the total sum of the specific item found.
RTB_Result.Text += Environment.NewLine + string.Format(Program.Localization.F1_Count, c);
}

private void SearchValuables(uint min, uint max, ulong ticks, Span<Item> tmp, ulong seed, uint count)
{
// Print the items for each second in the range, and count the number of Special Balls.
// If the count is higher than the previous highest count, update the result.
// If the count is the same, keep the previous result.
// Special Balls will always have a count of 1, so just use that as our "is valuable" check.
int c = -1;
ulong result = 0;
do
Expand All @@ -143,33 +155,48 @@ private void SearchValuables(uint min, uint max, ulong ticks, Span<Item> tmp, ul
result = check;
}

ticks += 60;
ticks += 60; // Next minute
}
while (ticks - seed < count);
Populate(result, tmp.Length);
// Append the total sum of the valuable items found.
RTB_Result.Text += Environment.NewLine + string.Format(Program.Localization.F1_Count, c);
}

private void Populate(ulong result, int count)
/// <summary>
/// Displays the result of the search in the user interface.
/// </summary>
/// <param name="result">Seed found.</param>
/// <param name="count">Items printed with that seed.</param>
private void Populate(ulong result, [Range(1, 10)] int count)
{
Span<Item> items = stackalloc Item[count];
ItemPrinter.Print(result, items, Mode);
Populate(result, items);
}

private void Populate(ulong result, Span<Item> items)
/// <summary>
/// Displays the result of the search in the user interface.
/// </summary>
private void Populate(ulong result, [Length(1, 10)] Span<Item> items)
{
DGV_View.Populate(items);
Populate(ItemUtil.GetTextResult(result, items));
RTB_Result.Text = ItemUtil.GetTextResult(result, items);
System.Media.SystemSounds.Beep.Play();
}

private void Populate(string result)
private void PopulateError(string error)
{
RTB_Result.Text = result;
RTB_Result.Text = error;
System.Media.SystemSounds.Beep.Play();
DGV_View.Clear();
}

private void CB_Seek_SelectedIndexChanged(object sender, EventArgs e)
{
L_Item.Visible = CB_Item.Visible = CB_Seek.SelectedIndex == 0;
}

private void NUD_Seconds_ValueChanged(object sender, EventArgs e) => UpdateIncrement();
private void UpdateIncrement() => tickToggle1.UpdateIncrement(NUD_Seconds.Value);
}
2 changes: 2 additions & 0 deletions ItemPrinterDeGacha.WinForms/Controls/ItemResultGridView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ public void Populate(ReadOnlySpan<Item> itemSpan)
rows.Add(item.Count, img, GameStrings.GetItemName(item.ItemId));
}
}

public void Clear() => DGV_View.Rows.Clear();
}

0 comments on commit d113253

Please sign in to comment.