Skip to content
Draft
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
38 changes: 38 additions & 0 deletions StarControl/Compatibility/ItemBagsIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Reflection;
using StardewValley;

namespace StarControl.Compatibility;

internal static class ItemBagsIdentity
{
private static Type? itemBagBaseType;
private static MethodInfo? getTypeIdMethod;

public static string? TryGetBagTypeId(Item item)
{
itemBagBaseType ??= AppDomain
.CurrentDomain.GetAssemblies()
.Select(a => a.GetType("ItemBags.Bags.ItemBag", throwOnError: false))
.FirstOrDefault(t => t is not null);

if (itemBagBaseType is null || !itemBagBaseType.IsInstanceOfType(item))
return null;

getTypeIdMethod ??= itemBagBaseType.GetMethod(
"GetTypeId",
BindingFlags.Public | BindingFlags.Instance
);

if (getTypeIdMethod?.ReturnType != typeof(string))
return null;

try
{
return getTypeIdMethod.Invoke(item, null) as string;
}
catch
{
return null;
}
}
}
3 changes: 3 additions & 0 deletions StarControl/Config/QuickSlotConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class QuickSlotConfiguration : IConfigEquatable<QuickSlotConfiguration>,
/// </summary>
public string Id { get; set; } = "";

public string? SubId { get; set; }

/// <summary>
/// Whether to display a confirmation dialog before activating the item in this slot.
/// </summary>
Expand All @@ -42,6 +44,7 @@ public bool Equals(QuickSlotConfiguration? other)
}
return IdType == other.IdType
&& Id == other.Id
&& SubId == other.SubId
&& RequireConfirmation == other.RequireConfirmation
&& UseSecondaryAction == other.UseSecondaryAction;
}
Expand Down
2 changes: 2 additions & 0 deletions StarControl/Data/RemappingSlot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ public class RemappingSlot : IItemLookup
/// Menu action, depending on the <see cref="IdType"/>.
/// </summary>
public string Id { get; set; } = "";

public string? SubId { get; set; }
}
74 changes: 74 additions & 0 deletions StarControl/Graphics/Sprite.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;

namespace StarControl.Graphics;

Expand All @@ -22,6 +24,78 @@ public static Sprite ForItemId(string id)
return new(data.GetTexture(), data.GetSourceRect());
}

public static Sprite FromItem(Item item)
{
ArgumentNullException.ThrowIfNull(item);

// 1) Prefer the official item registry sprite (works for all vanilla + most mod items that register properly)
var qualifiedId = item.QualifiedItemId;
if (!string.IsNullOrEmpty(qualifiedId))
{
try
{
var data = ItemRegistry.GetData(qualifiedId);
if (data is not null)
{
return new(data.GetTexture(), data.GetSourceRect());
}
}
catch
{
// swallow and fall back below
}
}

// 2) Fallback: render the item into a small texture (works even for "weird" items like Item Bags)
var derivedTexture = TryRenderItemToTexture(item);
if (derivedTexture is not null)
{
return new(derivedTexture, derivedTexture.Bounds);
}

// 3) Final fallback
return Sprites.Error();
}

private static Texture2D? TryRenderItemToTexture(Item item)
{
try
{
var graphicsDevice = Game1.graphics.GraphicsDevice;
var spriteBatch = Game1.spriteBatch;

var previousTargets = graphicsDevice.GetRenderTargets();
var renderTarget = new RenderTarget2D(graphicsDevice, 64, 64);

graphicsDevice.SetRenderTarget(renderTarget);
graphicsDevice.Clear(Color.Transparent);

spriteBatch.Begin(
SpriteSortMode.Deferred,
BlendState.AlphaBlend,
rasterizerState: new() { MultiSampleAntiAlias = false },
samplerState: SamplerState.PointClamp
);

try
{
// drawInMenu is the most compatible "just draw whatever this item is" API
item.drawInMenu(spriteBatch, Vector2.Zero, 1f);
}
finally
{
spriteBatch.End();
graphicsDevice.SetRenderTargets(previousTargets);
}

return renderTarget;
}
catch
{
return null;
}
}

/// <summary>
/// Attempts to load a sprite from configuration data.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions StarControl/IItemLookup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface IItemLookup
/// </summary>
string Id { get; }

string? SubId { get; }

/// <summary>
/// The type of ID that the <see cref="Id"/> refers to.
/// </summary>
Expand Down
76 changes: 73 additions & 3 deletions StarControl/Menus/InventoryMenuItem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using StardewValley.Enchantments;
Expand Down Expand Up @@ -40,7 +41,6 @@ internal class InventoryMenuItem : IRadialMenuItem

public InventoryMenuItem(Item item)
{
Logger.Log(LogCategory.Menus, "Starting refresh of inventory menu.");
Item = item;
Title = item.DisplayName;
Description = UnparseText(item.getDescription());
Expand Down Expand Up @@ -77,6 +77,19 @@ public ItemActivationResult Activate(
ItemActivationType activationType
)
{
// Item Bags: open bag UI from Primary (wheel) and Instant actions.
if (
Item is Tool bagTool
&& (
activationType == ItemActivationType.Primary
|| activationType == ItemActivationType.Instant
)
&& TryOpenItemBagsMenu(bagTool)
)
{
return ItemActivationResult.Used;
}

if (activationType == ItemActivationType.Instant)
{
if (Item is Tool tool)
Expand All @@ -93,7 +106,7 @@ ItemActivationType activationType
{
return ItemActivationResult.Ignored;
}
who.CurrentToolIndex = who.Items.IndexOf(tool);
who.CurrentToolIndex = toolIndex;
}
if (tool is FishingRod rod && rod.fishCaught)
{
Expand Down Expand Up @@ -138,6 +151,7 @@ ItemActivationType activationType
}
}
}

return FuzzyActivation.ConsumeOrSelect(
who,
Item,
Expand Down Expand Up @@ -338,4 +352,60 @@ private static string UnparseText(string text)
}
return sb.ToString();
}

// Cache reflection lookups (don’t re-scan assemblies every press)
private static Type? _itemBagsBaseType;
private static MethodInfo? _openContentsMethod;

private static bool TryOpenItemBagsMenu(Tool tool)
{
try
{
_itemBagsBaseType ??= AppDomain
.CurrentDomain.GetAssemblies()
.Select(a => a.GetType("ItemBags.Bags.ItemBag", throwOnError: false))
.FirstOrDefault(t => t is not null);

if (_itemBagsBaseType is null)
return false;

if (!_itemBagsBaseType.IsInstanceOfType(tool))
return false;

var player = Game1.player;
if (player is null || player.CursorSlotItem is not null)
return false;

_openContentsMethod ??= _itemBagsBaseType.GetMethod(
"OpenContents",
BindingFlags.Public | BindingFlags.Instance
);

if (_openContentsMethod is null)
return false;

var parameters = _openContentsMethod.GetParameters();

if (parameters.Length == 0)
{
_openContentsMethod.Invoke(tool, null);
return true;
}

if (parameters.Length == 3)
{
_openContentsMethod.Invoke(
tool,
new object?[] { player.Items, player.MaxItems, null }
);
return true;
}

return false;
}
catch
{
return false;
}
}
}
2 changes: 1 addition & 1 deletion StarControl/Menus/QuickSlotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private void RefreshSlots()
+ $"secondary action = {slot.UseSecondaryAction}, "
+ $"require confirmation = {slot.RequireConfirmation}"
);
var slottedItem = resolver.ResolveItem(slot.Id, slot.IdType);
var slottedItem = resolver.ResolveItem(slot.Id, slot.SubId, slot.IdType);
if (slottedItem is not null)
{
Logger.Log(
Expand Down
56 changes: 44 additions & 12 deletions StarControl/Menus/QuickSlotRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StarControl.Config;
using StarControl.Data;
using StarControl.Graphics;
using StardewValley;

namespace StarControl.Menus;

Expand Down Expand Up @@ -49,8 +51,6 @@ private enum PromptPosition
(SLOT_DISTANCE + SLOT_SIZE / 2 + MARGIN_OUTER) * 0.7f
);

private static readonly Color OuterBackgroundColor = new(16, 16, 16, 210);

private readonly Dictionary<SButton, ButtonFlash> flashes = [];
private readonly HashSet<SButton> enabledSlots = [];
private readonly Dictionary<SButton, Sprite> slotSprites = [];
Expand Down Expand Up @@ -99,7 +99,11 @@ public void Draw(SpriteBatch b, Rectangle viewport)
leftOrigin.AddX(Scale(SLOT_DISTANCE * MENU_SCALE)),
Scale(BACKGROUND_RADIUS)
);
b.Draw(outerBackground, leftBackgroundRect, OuterBackgroundColor * BackgroundOpacity);
b.Draw(
outerBackground,
leftBackgroundRect,
(Color)config.Style.InnerBackgroundColor * BackgroundOpacity
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this to control Quick Menu color while still keeping transparency

);
DrawSlot(b, leftOrigin, SButton.DPadLeft, PromptPosition.Left);
DrawSlot(
b,
Expand Down Expand Up @@ -158,7 +162,11 @@ public void Draw(SpriteBatch b, Rectangle viewport)
rightOrigin.AddX(-Scale(SLOT_DISTANCE * MENU_SCALE)),
Scale(BACKGROUND_RADIUS)
);
b.Draw(outerBackground, rightBackgroundRect, OuterBackgroundColor * BackgroundOpacity);
b.Draw(
outerBackground,
rightBackgroundRect,
(Color)config.Style.InnerBackgroundColor * BackgroundOpacity
);
DrawSlot(b, rightOrigin, SButton.ControllerB, PromptPosition.Right);
DrawSlot(
b,
Expand Down Expand Up @@ -431,15 +439,26 @@ private Texture2D GetUiTexture()
return uiTexture;
}

private Sprite? GetSlotSprite(IItemLookup itemLookup)
private Sprite? GetSlotSprite(IItemLookup itemLookup, ICollection<Item> inventoryItems)
{
if (string.IsNullOrWhiteSpace(itemLookup.Id))
{
return null;
}

return itemLookup.IdType switch
{
ItemIdType.GameItem => Sprite.ForItemId(itemLookup.Id),
ItemIdType.GameItem => QuickSlotResolver.ResolveInventoryItem(
itemLookup.Id,
itemLookup.SubId,
inventoryItems
)
is Item item
? Sprite.FromItem(item) // ✅ handles Item Bags via drawInMenu fallback
: (
ItemRegistry.GetData(itemLookup.Id) is not null
? Sprite.ForItemId(itemLookup.Id) // only for registered items
: null
), // unregistered (ItemBags): don't force error icon

ItemIdType.ModItem => GetModItemSprite(itemLookup.Id),
_ => null,
};
Expand All @@ -455,7 +474,7 @@ private void RefreshSlots()
{
Logger.Log(LogCategory.QuickSlots, "Starting refresh of quick slot renderer data.");
enabledSlots.Clear();
slotSprites.Clear();
// Keep slotSprites so we can show a "last known" icon when a slot item is temporarily unavailable.
foreach (var (button, slotConfig) in Slots)
{
Logger.Log(LogCategory.QuickSlots, $"Checking slot for {button}...");
Expand Down Expand Up @@ -492,10 +511,23 @@ private void RefreshSlots()
LogLevel.Info
);
}
sprite ??= GetSlotSprite(slotConfig);
sprite ??= GetSlotSprite(
slotConfig,
QuickSlotResolver.GetExpandedPlayerItems(Game1.player)
);
if (sprite is not null)
{
slotSprites.Add(button, sprite);
slotSprites[button] = sprite; // update/insert
}
else
{
// If we had a previous sprite for this slot, keep it so the icon stays visible (greyed out).
// If we have none, fall back to a generic item-id sprite so something renders.
// (like Item Bags), Sprite.ForItemId will show Error_Invalid, which is worse UX.
if (!slotSprites.ContainsKey(button) && !string.IsNullOrWhiteSpace(slotConfig.Id))
{
slotSprites[button] = Sprite.ForItemId("Error_Invalid");
}
}
}
isDirty = false;
Expand Down
Loading