Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions StackSplit-Redux/API.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
using System;
using StackSplitRedux.MenuHandlers;

using Microsoft.Xna.Framework;

using StardewModdingAPI;

using StardewValley;
using StardewValley.Menus;

namespace StackSplitRedux
{
public class API : IStackSplitAPI
Expand All @@ -22,5 +29,48 @@ public bool TryRegisterMenu(Type menuType) {
Log.Error($"API: Don't know how to handle {menuType}, not registered!");
return false;
}

private static Func<IClickableMenu, O> CastFunc<T, O>(Func<T, O> func) where T : IClickableMenu where O : class {
return menu => menu is T tobj ? func(tobj) : null;
}

private static Func<IClickableMenu,A,O> CastFunc<T,A,O>(Func<T, A, O> func) where T : IClickableMenu where O : class {
return (menu, other) => menu is T tobj ? func(tobj, other) : null;
}

public bool RegisterBasicMenu<T>(
Func<T, InventoryMenu> inventoryGetter,
Func<T, IReflectedField<Item>> hoveredItemFieldGetter,
Func<T, IReflectedField<Item>> heldItemFieldGetter,
Func<T, Point, Tuple<int, Action<bool, int>>> stackChecker
) where T : IClickableMenu {
return RegisterBasicMenu(
typeof(T),
CastFunc(inventoryGetter),
CastFunc(hoveredItemFieldGetter),
CastFunc(heldItemFieldGetter),
CastFunc(stackChecker)
);

}

public bool RegisterBasicMenu(
Type menuType,
Func<IClickableMenu, InventoryMenu> inventoryGetter,
Func<IClickableMenu, IReflectedField<Item>> hoveredItemFieldGetter,
Func<IClickableMenu, IReflectedField<Item>> heldItemFieldGetter,
Func<IClickableMenu, Point, Tuple<int, Action<bool, int>>> stackChecker
) {
ModdedAdapterMapping.Add(
menuType,
new ModdedMenuAdapter(inventoryGetter, hoveredItemFieldGetter, heldItemFieldGetter, stackChecker)
);

HandlerMapping.Add(menuType, typeof(ModdedMenuHandler));

Log.Debug($"API: Registered {menuType} with custom handlers.");

return true;
}
}
}
66 changes: 65 additions & 1 deletion StackSplit-Redux/IStackSplitReduxAPI.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,73 @@
using System;

using Microsoft.Xna.Framework;

using StardewValley;
using StardewValley.Menus;

using StardewModdingAPI;

namespace StackSplitRedux
{
interface IStackSplitAPI
public interface IStackSplitAPI
{
bool TryRegisterMenu(Type menuType);

/// <summary>
/// Register a new menu type with StackSplitRedux with support for
/// an InventoryMenu, along with support for arbitrary clicks via
/// callbacks.
///
/// If inventoryGetter and hoveredItemFieldGetter both exist and return
/// non-null values, SSR will initialize its standard inventory handler
/// support.
///
/// stackChecker is called with the menu instance and a Point representing
/// the mouse coordinates when the user triggers SSR. If your func returns
/// null, nothing will happen.
///
/// The return tuple contains the initial value to display, and an Action
/// to be called when the user confirms the value they want. The action
/// will be called with a success boolean and the user's input as an int.
/// </summary>
/// <example>
/// <code>
/// RegisterBasicMenu(
/// typeof(MyMenu),
/// menu => (menu as MyMenu)?.inventory,
/// menu => menu is MyMenu ? Mod.Helper.Reflection.GetField<Item>(menu, "hoverItem") : null,
/// null,
/// (menu, point) => {
/// if (menu is not MyMenu mm)
/// return null;
/// if (!mm.CanSplit(point.X, point.Y))
/// return null;
/// return new Tuple<int, Action<bool, int>>(5, (success, amount) => {
/// mm.PerformSplit(point.X, point.Y, amount);
/// });
/// }
/// );
/// </code>
/// </example>
/// <typeparam name="T">The class of the menu to be handled</typeparam>
/// <param name="inventoryGetter">Function to get a menu's child InventoryMenu</param>
/// <param name="hoveredItemFieldGetter">Function to get a menu's hovered item field</param>
/// <param name="heldItemFieldGetter">Function to get a menu's held item field. If this is null, Game1.player.CursorSlotItem will be used instead.</param>
/// <param name="stackChecker"></param>
bool RegisterBasicMenu<T>(
Func<T, InventoryMenu> inventoryGetter,
Func<T, IReflectedField<Item>> hoveredItemFieldGetter,
Func<T, IReflectedField<Item>> heldItemFieldGetter,
Func<T, Point, Tuple<int, Action<bool, int>>> stackChecker
) where T : IClickableMenu;

bool RegisterBasicMenu(
Type menuType,
Func<IClickableMenu, InventoryMenu> inventoryGetter,
Func<IClickableMenu, IReflectedField<Item>> hoveredItemFieldGetter,
Func<IClickableMenu, IReflectedField<Item>> heldItemFieldGetter,
Func<IClickableMenu, Point, Tuple<int, Action<bool, int>>> stackChecker
);

}
}
2 changes: 1 addition & 1 deletion StackSplit-Redux/MenuHandlers/BaseMenuHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ protected virtual void InitInventory() {
var inventoryMenu = Mod.Reflection.GetField<IClickableMenu>(this.NativeMenu, "inventory").GetValue() as InventoryMenu;
var hoveredItemField = Mod.Reflection.GetField<Item>(this.NativeMenu, "hoveredItem");

this.InvHandler.Init(inventoryMenu, hoveredItemField);
this.InvHandler.Init(inventoryMenu, hoveredItemField, null);
}
catch (Exception e) {
Log.Error($"[{nameof(BaseMenuHandler<TMenuType>)}.{nameof(InitInventory)}] Failed to initialize the inventory handler. Exception:\n{e}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public override void InitInventory() {
// We need to do this explicitly because the crafting page uses a different variable name for hover item.
var inventoryMenu = this.MenuPage.inventory;
var hoveredItemField = Mod.Reflection.GetField<Item>(this.MenuPage, "hoverItem");
var heldItemField = Mod.Reflection.GetField<Item>(this.MenuPage, "heldItem");

this.InventoryHandler.Init(inventoryMenu, hoveredItemField);
this.InventoryHandler.Init(inventoryMenu, hoveredItemField, heldItemField);
}

/// <summary>Tells the handler that the inventory was shift-clicked.</summary>
Expand Down
31 changes: 24 additions & 7 deletions StackSplit-Redux/MenuHandlers/GameMenuHandlers/GameMenuHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class GameMenuHandler : BaseMenuHandler<GameMenu>
{ GameMenu.craftingTab, new CraftingPageHandler() }
};

private readonly ModdedPageHandler ModdedPageHandler = new();

/// <summary>The handler for the current tab,</summary>
private IGameMenuPageHandler CurrentPageHandler = null;

Expand Down Expand Up @@ -124,19 +126,34 @@ private bool ChangeTabs(int newTab) {

CloseCurrentHandler();

if (!PageHandlers.TryGetValue(newTab, out IGameMenuPageHandler pageHandler)) {
// Please note that this is NOT AN ERROR
Log.TraceIfD($"[{nameof(GameMenuHandler)}.{nameof(ChangeTabs)}] No handler for tab {newTab}");
return false;
var pages = this.NativeMenu.pages;
var page = pages[newTab];

IGameMenuPageHandler pageHandler = null;

// Before using a built-in handler, try checking to see
// if there's a modded adapter handler.

if (ModdedAdapterMapping.TryGetAdapter(page.GetType(), out IModdedMenuAdapter adapter)) {
Log.TraceIfD($"[{nameof(GameMenuHandler)}.{nameof(ChangeTabs)}] Found a modded adapter for tab {newTab} : {page.GetType().FullName}");

pageHandler = ModdedPageHandler;
}

Log.TraceIfD($"[{nameof(GameMenuHandler)}.{nameof(ChangeTabs)}] Found a handler for tab {newTab} : {pageHandler}");
if (pageHandler == null) {
if (!PageHandlers.TryGetValue(newTab, out pageHandler)) {
// Please note that this is NOT AN ERROR
Log.TraceIfD($"[{nameof(GameMenuHandler)}.{nameof(ChangeTabs)}] No handler for tab {newTab}");
return false;
}

Log.TraceIfD($"[{nameof(GameMenuHandler)}.{nameof(ChangeTabs)}] Found a handler for tab {newTab} : {pageHandler}");
}

this.PreviousTab = newTab;
this.CurrentPageHandler = pageHandler;

var pages = this.NativeMenu.pages;
pageHandler.Open(this.NativeMenu, pages[newTab], this.InvHandler);
pageHandler.Open(this.NativeMenu, page, this.InvHandler);

return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public virtual void InitInventory() {
$"[{nameof(GameMenuPageHandler<TPageType>)}.{nameof(InitInventory)}] Initializing InventoryHandler " +
$"for menu = {inventoryMenu}, hovered = {hoveredItemField}"
);
this.InventoryHandler.Init(inventoryMenu, hoveredItemField);
this.InventoryHandler.Init(inventoryMenu, hoveredItemField, null);
}
catch (Exception e) {
Log.Error($"[{nameof(GameMenuPageHandler<TPageType>)}.{nameof(InitInventory)}] Failed to initialize the inventory handler:\n{e}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;

using Microsoft.Xna.Framework;

using StardewValley;
using StardewValley.Menus;


namespace StackSplitRedux.MenuHandlers
{
public class ModdedPageHandler : GameMenuPageHandler<IClickableMenu>
{

private Action<bool, int> StackCallback;

private IModdedMenuAdapter ModdedHandler;

private bool WasInventoryClicked = false;

public ModdedPageHandler()
: base() {
}

public override void Open(IClickableMenu menu, IClickableMenu page, InventoryHandler inventoryHandler) {

ModdedAdapterMapping.TryGetAdapter(page.GetType(), out ModdedHandler);
HasInventory =
ModdedHandler?.GetInventoryMenu(page) != null &&
ModdedHandler?.GetHoverItemField(page) != null;

base.Open(menu, page, inventoryHandler);
}

public override void InitInventory() {
try {
var inventoryMenu = ModdedHandler?.GetInventoryMenu(MenuPage);
var hoverItem = ModdedHandler?.GetHoverItemField(MenuPage);
var heldItem = ModdedHandler?.GetHeldItemField(MenuPage);

this.InventoryHandler.Init(inventoryMenu, hoverItem, heldItem);
}
catch (Exception ex) {
Log.Error($"[{nameof(ModdedPageHandler)}:{MenuPage.GetType().Name}.{nameof(InitInventory)}] Failed to initialize the inventory handler:\n{ex}");
}
}

public override EInputHandled InventoryClicked(out int stackAmount) {
WasInventoryClicked = true;
return base.InventoryClicked(out stackAmount);
}

public override EInputHandled OpenSplitMenu(out int stackAmount) {
this.WasInventoryClicked = false;

var wanted = ModdedHandler?.GetStack(MenuPage, new Point(Game1.getOldMouseX(true), Game1.getOldMouseY(true)));

if (wanted != null) {
stackAmount = wanted.Item1;
StackCallback = wanted.Item2;
return EInputHandled.Consumed;
}

stackAmount = 0;
return EInputHandled.NotHandled;
}

public override void OnStackAmountEntered(int amount) {
if (this.WasInventoryClicked) {
base.OnStackAmountEntered(amount);
return;
}

StackCallback?.Invoke(true, amount);
StackCallback = null;
}
}
}
21 changes: 21 additions & 0 deletions StackSplit-Redux/MenuHandlers/IModdedMenuAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Menus;
using StardewModdingAPI;

namespace StackSplitRedux.MenuHandlers
{
public interface IModdedMenuAdapter
{

InventoryMenu GetInventoryMenu(IClickableMenu menu);

IReflectedField<Item> GetHoverItemField(IClickableMenu menu);

IReflectedField<Item> GetHeldItemField(IClickableMenu menu);

Tuple<int, Action<bool, int>> GetStack(IClickableMenu menu, Point mouse);

}
}
28 changes: 24 additions & 4 deletions StackSplit-Redux/MenuHandlers/InventoryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,36 @@ public class InventoryHandler
/// <summary>Currently hovered item in the inventory.</summary>
private Item HoveredItem;

private Item HeldItem {
get {
if (this.HeldItemField == null)
return Game1.player.CursorSlotItem;
return this.HeldItemField.GetValue();
}
set {
if (this.HeldItemField == null)
Game1.player.CursorSlotItem = value;
else
this.HeldItemField.SetValue(value);
}
}

/// <summary>
/// The held item field owned by the parent menu that contains the inventory, if the menu does not use the global held item.
/// </summary>
private IReflectedField<Item> HeldItemField;

/// <summary>Null constructor that currently only invokes the base null constructor</summary>
public InventoryHandler() {
}

/// <summary>This must be called everytime the inventory is opened/resized.</summary>
/// <param name="inventoryMenu">Native inventory.</param>
public void Init(InventoryMenu inventoryMenu, IReflectedField<Item> hoveredItemField) {
public void Init(InventoryMenu inventoryMenu, IReflectedField<Item> hoveredItemField, IReflectedField<Item> heldItemField) {
Debug.Assert(inventoryMenu is not null);
this.NativeInventoryMenu = inventoryMenu;
this.HoveredItemField = hoveredItemField;
this.HeldItemField = heldItemField;

// Create the bounds around the inventory
var first = this.Inventory[0].bounds;
Expand Down Expand Up @@ -88,7 +108,7 @@ public bool CanSplitSelectedItem() {
Debug.Assert(this.Initialized);

var hoveredItem = this.HoveredItem;
var heldItem = Game1.player.CursorSlotItem;
var heldItem = this.HeldItem;

if (hoveredItem is null) return false;
if (hoveredItem.Stack <= 1) return false;
Expand All @@ -105,7 +125,7 @@ public void SplitSelectedItem(int stackAmount) {
var hoveredItemCount = this.HoveredItem.Stack; // Grab & hold the value
int maxStack = hoveredItem.maximumStackSize();

var heldItem = Game1.player.CursorSlotItem;
var heldItem = this.HeldItem;
var heldItemCount = heldItem?.Stack ?? 0; // Grab & hold the value

// Run native click code to get the selected item
Expand Down Expand Up @@ -133,7 +153,7 @@ public void SplitSelectedItem(int stackAmount) {
heldItem.Stack = heldItemCount;

// Update the native fields
Game1.player.CursorSlotItem = heldItem;
this.HeldItem = heldItem;

// Null it out now that we're done with this operation
this.HoveredItem = null;
Expand Down
Loading