Exemple #1
0
        public static void Cmd_Give(string s, string[] args)
        {
            const int defaultQuantity = 25;
            int       quantity        = args.Length == 0
                                ? defaultQuantity
                                : int.TryParse(args[0], out int argQuantity)
                                        ? argQuantity
                                        : defaultQuantity;

            Log.D($"Adding {quantity} of each unlocked raised bed. Use '{ModEntry.CommandPrefix}giveall' to add all varieties.");

            IEnumerable <string> unlockedKeys = Game1.player.craftingRecipes.Keys
                                                .Where(recipe => recipe.StartsWith(OutdoorPot.GenericName))
                                                .Select(recipe => OutdoorPot.GetVariantKeyFromName(recipe));

            if (!unlockedKeys.Any())
            {
                Log.D($"No raised bed recipes are unlocked! Use '{ModEntry.CommandPrefix}giveall' to add all varieties.");
            }
            else
            {
                foreach (string variantKey in unlockedKeys)
                {
                    ModEntry.Give(variantKey: variantKey, quantity: quantity);
                }
            }
        }
Exemple #2
0
        public static List <string> AddNewAvailableRecipes()
        {
            List <string> newVariants = new List <string>();

            for (int i = 0; i < ModEntry.ItemDefinitions.Count; ++i)
            {
                string variantKey = ModEntry.ItemDefinitions.Keys.ElementAt(i);
                string itemName   = OutdoorPot.GetNameFromVariantKey(variantKey);

                if (Game1.player.craftingRecipes.ContainsKey(itemName) ||
                    string.IsNullOrEmpty(ModEntry.ItemDefinitions[variantKey].RecipeConditions) ||
                    !Game1.player.eventsSeen.Contains(ModEntry.EventRootId))
                {
                    continue;
                }

                int    eventID      = ModEntry.EventRootId + i;
                string eventKey     = $"{eventID.ToString()}/{ModEntry.ItemDefinitions[variantKey].RecipeConditions}";
                int    precondition = Game1.getFarm().checkEventPrecondition(eventKey);
                if (precondition != -1)
                {
                    newVariants.Add(variantKey);
                    Game1.player.craftingRecipes.Add(itemName, 0);
                }
            }
            return(newVariants);
        }
        /// <summary>
        /// Required to draw correct object sprites and strings in crafting menu.
        /// Event handlers on StardewModdingAPI.Events.Display.MenuChanged were inconsistent.
        /// </summary>
        public static void CraftingPage_LayoutRecipes_Postfix(
            CraftingPage __instance)
        {
            int unlockedCount = Game1.player.craftingRecipes.Keys.Count(c => c.StartsWith(OutdoorPot.GenericName));

            int[] matchesPerDict = new int[__instance.pagesOfCraftingRecipes.Count];
            int   i = 0;

            foreach (Dictionary <ClickableTextureComponent, CraftingRecipe> dict in __instance.pagesOfCraftingRecipes)
            {
                List <KeyValuePair <ClickableTextureComponent, CraftingRecipe> > matches = dict
                                                                                           .Where(pair => pair.Value.name.StartsWith(OutdoorPot.GenericName))
                                                                                           .ToList();
                matches.ForEach(pair =>
                {
                    string variantKey = OutdoorPot.GetVariantKeyFromName(name: pair.Value.name);

                    // Sprite
                    pair.Key.texture    = ModEntry.Sprites[ModEntry.ItemDefinitions[variantKey].SpriteKey];
                    pair.Key.sourceRect = OutdoorPot.GetSpriteSourceRectangle(spriteIndex: ModEntry.ItemDefinitions[variantKey].SpriteIndex);

                    // Strings
                    pair.Value.DisplayName = OutdoorPot.GetDisplayNameFromName(pair.Value.name);
                    pair.Value.description = OutdoorPot.GetRawDescription();
                });
                matchesPerDict[i++] = matches.Count();
            }
            Log.T($"Found {string.Join("/", matchesPerDict)} garden beds in crafting menu pages ({unlockedCount} unlocked).");
        }
Exemple #4
0
        public static void AddDefaultRecipes()
        {
            List <string> recipesToAdd = new List <string>();

            int[]  eventsSeen            = Game1.player.eventsSeen.ToArray();
            string precondition          = $"{ModEntry.EventRootId}/{ModEntry.EventData[0]["Conditions"]}";
            int    rootEventReady        = Game1.getFarm().checkEventPrecondition(precondition);
            bool   hasOrWillSeeRootEvent = eventsSeen.Contains(ModEntry.EventRootId) || rootEventReady != -1;

            for (int i = 0; i < ModEntry.ItemDefinitions.Count; ++i)
            {
                string variantKey           = ModEntry.ItemDefinitions.Keys.ElementAt(i);
                string craftingRecipeName   = OutdoorPot.GetNameFromVariantKey(variantKey: variantKey);
                bool   isAlreadyKnown       = Game1.player.craftingRecipes.ContainsKey(craftingRecipeName);
                bool   isDefaultRecipe      = ModEntry.ItemDefinitions[variantKey].RecipeIsDefault;
                bool   isInitialEventRecipe = string.IsNullOrEmpty(ModEntry.ItemDefinitions[variantKey].RecipeConditions);
                bool   shouldAdd            = ModEntry.Config.RecipesAlwaysAvailable || isDefaultRecipe || (hasOrWillSeeRootEvent && isInitialEventRecipe);

                if (!isAlreadyKnown && shouldAdd)
                {
                    recipesToAdd.Add(craftingRecipeName);
                }
            }
            if (recipesToAdd.Count > 0)
            {
                Log.T($"Adding {recipesToAdd.Count} default recipes:{recipesToAdd.Aggregate(string.Empty, (str, s) => $"{str}{Environment.NewLine}{s}")}");

                for (int i = 0; i < recipesToAdd.Count; ++i)
                {
                    Game1.player.craftingRecipes.Add(recipesToAdd[i], 0);
                }
            }
        }
 /// <summary>
 /// Replace logic determining item drop-in actions on garden bed objects.
 /// </summary>
 public static bool Utility_IsThereAnObjectHereWhichAcceptsThisItem_Prefix(
     ref bool __result,
     GameLocation location,
     Item item,
     int x,
     int y)
 {
     try
     {
         Vector2 tileLocation = new Vector2(x / Game1.tileSize, y / Game1.tileSize);
         if (location.Objects.TryGetValue(tileLocation, out StardewValley.Object o) && o != null && o is OutdoorPot op)
         {
             if (!OutdoorPot.CanAcceptItemOrSeed(item: item) && OutdoorPot.CanAcceptAnything(op: op))
             {
                 __result = op.performObjectDropInAction(dropInItem: (StardewValley.Object)item, probe: true, who: Game1.player);
             }
             else
             {
                 __result = false;
             }
             return(false);
         }
     }
     catch (Exception e)
     {
         HarmonyPatches.ErrorHandler(e);
     }
     return(true);
 }
Exemple #6
0
 private void GameLoop_DayEnding(object sender, DayEndingEventArgs e)
 {
     // Break ready objects at the start of each season
     if (ModEntry.Config.RaisedBedsMayBreakWithAge && Game1.dayOfMonth == 28)
     {
         Log.T($"Performing end-of-season breakage: Y{Game1.year}/M{1 + Utility.getSeasonNumber(Game1.currentSeason)}/D{Game1.dayOfMonth}");
         OutdoorPot.BreakAll();
     }
 }
        /// <summary>
        /// Replace logic for crafting objects in base game crafting menu to create the appropriate garden bed for the crafting recipe variant.
        /// </summary>
        public static bool CraftingPage_ClickCraftingRecipe_Prefix(
            CraftingPage __instance,
            int ___currentCraftingPage,
            ref Item ___heldItem,
            ClickableTextureComponent c,
            bool playSound = true)
        {
            try
            {
                // Fetch an instance of any clicked-on craftable in the crafting menu
                CraftingRecipe recipe = __instance.pagesOfCraftingRecipes[___currentCraftingPage][c];

                // Fall through to default method for any other craftables
                if (!recipe.name.StartsWith(OutdoorPot.GenericName))
                {
                    return(true);
                }

                OutdoorPot item = new OutdoorPot(
                    variantKey: OutdoorPot.GetVariantKeyFromName(recipe.name),
                    tileLocation: Vector2.Zero);

                // Behaviours as from base method
                recipe.consumeIngredients(null);
                if (playSound)
                {
                    Game1.playSound("coin");
                }
                if (___heldItem == null)
                {
                    ___heldItem = item;
                }
                else if (___heldItem.canStackWith(item))
                {
                    ___heldItem.addToStack(item);
                }
                if (Game1.player.craftingRecipes.ContainsKey(recipe.name))
                {
                    Game1.player.craftingRecipes[recipe.name] += recipe.numberProducedPerCraft;
                }
                Game1.stats.checkForCraftingAchievements();
                if (Game1.options.gamepadControls && ___heldItem != null && Game1.player.couldInventoryAcceptThisItem(___heldItem))
                {
                    Game1.player.addItemToInventoryBool(___heldItem);
                    ___heldItem = null;
                }

                return(false);
            }
            catch (Exception e)
            {
                HarmonyPatches.ErrorHandler(e);
            }
            return(true);
        }
Exemple #8
0
        private static void Give(string variantKey, int quantity)
        {
            OutdoorPot item = new OutdoorPot(variantKey: variantKey, tileLocation: Vector2.Zero)
            {
                Stack = quantity
            };

            if (!Game1.player.addItemToInventoryBool(item))
            {
                Log.D($"Inventory full: Did not add {variantKey} raised bed.");
            }
        }
 /// <summary>
 /// Replace logic for choosing whether objects can be placed into a custom garden bed.
 /// </summary>
 public static void GameLocation_IsTileOccupiedForPlacement_Postfix(
     GameLocation __instance,
     ref bool __result,
     Vector2 tileLocation,
     StardewValley.Object toPlace)
 {
     if (__instance.Objects.TryGetValue(tileLocation, out StardewValley.Object o) && o != null && o is OutdoorPot op)
     {
         bool isPlantable = OutdoorPot.CanAcceptItemOrSeed(toPlace) &&
                            op.hoeDirt.Value.canPlantThisSeedHere(toPlace.ParentSheetIndex, (int)tileLocation.X, (int)tileLocation.Y, toPlace.Category == -19);
         if (OutdoorPot.CanAcceptAnything(op: op) && isPlantable)
         {
             __result = false;
         }
     }
 }
 /// <summary>
 /// Add logic to consider new conditions for planting seeds in garden bed objects.
 /// </summary>
 public static bool Utility_IsViableSeedSpot_Prefix(
     GameLocation location,
     Vector2 tileLocation,
     Item item)
 {
     try
     {
         if (location.Objects.TryGetValue(tileLocation, out StardewValley.Object o) && o != null && o is OutdoorPot op)
         {
             return(OutdoorPot.CanAcceptItemOrSeed(item) && OutdoorPot.CanAcceptSeed(item: item, op: op) && OutdoorPot.CanAcceptAnything(op: op));
         }
     }
     catch (Exception e)
     {
         HarmonyPatches.ErrorHandler(e);
     }
     return(true);
 }
        public NewRecipeMenu(List <string> variantKeys)
            : base(x: 0, y: 0, width: 0, height: 0)
        {
            Log.T($"Opened end of night menu: {this.GetType().FullName}");

            Game1.player.team.endOfNightStatus.UpdateState(ModEntry.EndOfNightState);
            this.VariantKeys = variantKeys;
            this.width       = (int)Dimensions.X;
            this.height      = ((int)Dimensions.Y / 2) + (this.VariantKeys.Count * Game1.smallestTileSize * 3 / 2 * Game1.pixelZoom);
            this.OkButton    = new ClickableTextureComponent(
                bounds: Rectangle.Empty,
                texture: Game1.mouseCursors,
                sourceRect: Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 46),
                scale: 1f)
            {
                myID = NewRecipeMenu.OkButtonId
            };

            this._isActive         = true;
            this._timerBeforeStart = 250;
            Game1.player.completelyStopAnimatingOrDoingAction();
            Game1.player.freezePause = 100;
            this.gameWindowSizeChanged(Rectangle.Empty, Rectangle.Empty);
            this.populateClickableComponentList();

            string craftingString = Game1.content.LoadString("Strings\\UI:LearnedRecipe_crafting");

            this._titleString = Translations.GetTranslation("menu.title.new");
            this._itemStrings =
                this.VariantKeys
                .ToDictionary(
                    vk => vk,
                    vk => Game1.content.LoadString("Strings\\UI:LevelUp_NewRecipe",
                                                   craftingString,
                                                   OutdoorPot.GetDisplayNameFromVariantKey(variantKey: vk)));
            this._itemSprites =
                this.VariantKeys
                .ToDictionary(
                    vk => vk,
                    vk => OutdoorPot.GetSpriteFromVariantKey(variantKey: vk));
        }
Exemple #12
0
        private void SaveLoadedBehaviours()
        {
            Log.T($"Adding endOfNightStatus definition: {ModEntry.EndOfNightState}");
            Game1.player.team.endOfNightStatus.AddSpriteDefinition(
                key: ModEntry.EndOfNightState,
                file: AssetManager.GameContentEndOfNightSpritesPath,
                x: 48, y: 0, width: 16, height: 16);

            Game1.content.Load              // Return value unused; event data is set in AssetManager.Edit()
            <Dictionary <string, object> >
                (AssetManager.GameContentEventDataPath);

            // Reinitialise objects to recalculate XmlIgnore values
            if (Context.IsMainPlayer)
            {
                OutdoorPot.ArrangeAll();
            }
            else
            {
                OutdoorPot.ArrangeAllOnNextTick();
            }
        }
 /// <summary>
 /// Replace logic for garden bed objects being watered by sprinklers.
 /// </summary>
 public static bool Object_ApplySprinkler_Prefix(
     GameLocation location,
     Vector2 tile)
 {
     try
     {
         if (ModEntry.Config.SprinklersEnabled &&
             location.Objects.TryGetValue(tile, out StardewValley.Object o) && o != null && o is OutdoorPot op)
         {
             if (OutdoorPot.CanAcceptAnything(op: op, ignoreCrops: true))
             {
                 op.Water();
             }
             return(false);
         }
     }
     catch (Exception e)
     {
         HarmonyPatches.ErrorHandler(e);
     }
     return(true);
 }
Exemple #14
0
        public void Edit <T>(IAssetData asset)
        {
            /*********
            *  Local data
            *********/

            if (asset.AssetNameEquals(GameContentEventDataPath))
            {
                var events = ((Newtonsoft.Json.Linq.JArray)asset
                              .AsDictionary <string, object>()
                              .Data["Events"])
                             .ToObject <List <Dictionary <string, string> > >();

                // Events are populated with preset tokens and script dialogues depending on game locale.

                // Root event tokenisation
                for (int i = 0; i < events.Count; ++i)
                {
                    // Format event script with event NPC name, as well as their dialogue strings
                    string[] args = new string[] { events[i]["Who"] }
                    .Concat(new int[] { 1, 2, 3, 4 }
                            .Select(j => Translations.GetTranslation($"event.{i}.dialogue.{j}")))
                    .ToArray();
                    events[i]["Script"] = string.Format(
                        format: events[i]["Script"],
                        args: args);
                    events[i]["Conditions"] = string.Format(
                        format: events[i]["Conditions"],
                        events[i]["Who"]);
                }

                Log.T($"Loaded {events.Count} event(s).{Environment.NewLine}Root event: {events[0]["Where"]}/{events[0]["Conditions"]}");

                ModEntry.EventData = events;

                return;
            }

            /********
            *  Game data
            ********/

            int id = OutdoorPot.BaseParentSheetIndex;

            if (asset.AssetNameEquals(Path.Combine("Data", "BigCraftablesInformation")))
            {
                if (ModEntry.ItemDefinitions == null)
                {
                    return;
                }

                string[] fields;
                var      data = asset.AsDictionary <int, string>().Data;

                // Set or reset the item ID for the generic object to some first best available index
                id = OutdoorPot.BaseParentSheetIndex = data.Keys.Max() + 2;

                string name, description;

                // Patch generic object entry into bigcraftables file, including display name and description from localisations file
                name        = Translations.GetTranslation("item.name");
                description = Translations.GetTranslation("item.description.default");
                fields      = data.First().Value.Split('/');            // Use existing data as a template; most fields are common or unused
                fields[0]   = OutdoorPot.GenericName;
                fields[4]   = description;
                fields[8]   = name;
                data[id]    = string.Join("/", fields);

                // Patch in dummy object entries after generic object entry
                for (int i = 1; i < ModEntry.ItemDefinitions.Count; ++i)
                {
                    ItemDefinition d = ModEntry.ItemDefinitions[ModEntry.ItemDefinitions.Keys.ElementAt(i)];
                    name         = Translations.GetNameTranslation(data: d);
                    fields       = data[id].Split('/');
                    fields[4]    = description;
                    fields[8]    = name;
                    data[id + i] = string.Join("/", fields);
                }

                // Don't remove the generic craftable from data lookup, since it's used later for crafting recipes and defaults

                return;
            }
            if (asset.AssetNameEquals(Path.Combine("Data", "CraftingRecipes")))
            {
                if (ModEntry.ItemDefinitions == null || id < 0)
                {
                    return;
                }

                // As above for the craftables dictionary, the recipes dictionary needs to have
                // our varieties patched in to have them appear.
                // Since all objects share a single ParentSheetIndex, each crafting recipe will normally
                // only produce a generic/wooden object.
                // This is handled in HarmonyPatches.CraftingPage_ClickCraftingRecipe_Prefix, which
                // is also needed to produce an OutdoorPot instance rather than a StardewValley.Object.

                // Add crafting recipes for all object variants
                var data = asset.AsDictionary <string, string>().Data;
                foreach (KeyValuePair <string, ItemDefinition> idAndFields in ModEntry.ItemDefinitions)
                {
                    string[] newFields = new string[]
                    {                           // Crafting ingredients:
                        ItemDefinition.ParseRecipeIngredients(data: idAndFields.Value),
                        // Unused field:
                        "blue berry",
                        // Crafted item ID and quantity:
                        $"{OutdoorPot.BaseParentSheetIndex} {idAndFields.Value.RecipeCraftedCount}",
                        // Recipe is bigCraftable:
                        "true",
                        // Recipe conditions (we ignore these):
                        "blue berry",
                        // Recipe display name:
                        Translations.GetNameTranslation(data: idAndFields.Value)
                    };
                    data[OutdoorPot.GetNameFromVariantKey(idAndFields.Key)] = string.Join("/", newFields);
                }

                return;
            }
            if (asset.AssetName.StartsWith(Path.Combine("Data", "Events")) &&
                Path.GetFileNameWithoutExtension(asset.AssetName) is string where)
            {
                // Patch our event data into whatever location happens to match the one specified.
                // Event tokenisation is handled in the Edit block for GameContentEventDataPath.

                if (ModEntry.EventData != null &&
                    ModEntry.EventData.FirstOrDefault(e => e["Where"] == where) is Dictionary <string, string> eventData)
                {
                    string key = $"{ModEntry.EventRootId}{ModEntry.EventData.IndexOf(eventData)}/{eventData["Conditions"]}";
                    asset.AsDictionary <string, string>().Data[key] = eventData["Script"];
                }

                return;
            }
        }
        public override void draw(SpriteBatch b)
        {
            if (this._timerBeforeStart > 0)
            {
                return;
            }

            // Blackout
            b.Draw(
                texture: Game1.fadeToBlackRect,
                destinationRectangle: new Rectangle(0, 0, Game1.uiViewport.Width, Game1.uiViewport.Height),
                color: Color.Black * 0.5f);

            if (!this._informationUp && this._isActive && this.StarIcon != null)
            {
                this.StarIcon.draw(b);
            }
            else
            {
                if (!this._informationUp)
                {
                    return;
                }

                // Draw popup header
                const int   wh        = 16;
                Vector2     padding   = new Vector2(22, -8) * Game1.pixelZoom;
                const float iconScale = 3f;
                Vector2     iconSize  = new Vector2(26, 20);
                Vector2     textSize  = Game1.dialogueFont.MeasureString(this._titleString);
                textSize = new Vector2(
                    textSize.X + padding.X + (iconSize.X * iconScale),
                    Math.Max(textSize.Y, iconSize.Y * iconScale) + padding.Y);
                Vector2 positionPadded = new Vector2(
                    this.xPositionOnScreen + ((this.width - textSize.X - (wh * Game1.pixelZoom)) / 2),
                    this.yPositionOnScreen - textSize.Y - (wh * Game1.pixelZoom / 2) - (2 * Game1.pixelZoom));
                Point sourceOrigin = new Point(260, 310);
                // background
                b.Draw(
                    texture: Game1.mouseCursors,
                    destinationRectangle: new Rectangle(
                        (int)(positionPadded.X + (wh * Game1.pixelZoom / 2)),
                        (int)(positionPadded.Y + (wh * Game1.pixelZoom / 2)),
                        (int)(textSize.X),
                        (int)(textSize.Y + (wh * Game1.pixelZoom))),
                    sourceRectangle: new Rectangle(360, 437, 1, 8),
                    color: Color.White,
                    rotation: 0f,
                    origin: Vector2.Zero,
                    effects: SpriteEffects.None,
                    layerDepth: 1f);

                // icon
                b.Draw(
                    texture: Game1.mouseCursors,
                    position: positionPadded + new Vector2(wh * Game1.pixelZoom) + new Vector2(0, -8f * iconScale),
                    sourceRectangle: new Rectangle(420, 488, (int)iconSize.X, (int)iconSize.Y),
                    color: Color.White,
                    rotation: 0f,
                    origin: Vector2.Zero,
                    scale: iconScale,
                    effects: SpriteEffects.None,
                    layerDepth: 1f);

                // top-left
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X, (int)positionPadded.Y, wh * Game1.pixelZoom, wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X, sourceOrigin.Y, wh, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // top-right
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X + (int)textSize.X, (int)positionPadded.Y, wh * Game1.pixelZoom, wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X + 28, sourceOrigin.Y, wh, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // bottom-left
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X, (int)positionPadded.Y + (int)textSize.Y + (wh * Game1.pixelZoom), wh * Game1.pixelZoom, wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X, sourceOrigin.Y + 16, wh, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // bottom-right
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X + (int)textSize.X, (int)positionPadded.Y + (int)textSize.Y + (wh * Game1.pixelZoom), wh * Game1.pixelZoom, wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X + 28, sourceOrigin.Y + 16, wh, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // top
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)(positionPadded.X + (wh * Game1.pixelZoom)), (int)positionPadded.Y, (int)textSize.X - (wh * Game1.pixelZoom), wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X + wh, sourceOrigin.Y, 1, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // bottom
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)(positionPadded.X + (wh * Game1.pixelZoom)), (int)(positionPadded.Y + textSize.Y + (wh * Game1.pixelZoom)), (int)textSize.X - (wh * Game1.pixelZoom), wh * Game1.pixelZoom),
                       sourceRectangle: new Rectangle(sourceOrigin.X + wh, sourceOrigin.Y + 16, 1, wh),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // left
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X, (int)positionPadded.Y + (wh * Game1.pixelZoom), wh * Game1.pixelZoom, (int)textSize.Y),
                       sourceRectangle: new Rectangle(sourceOrigin.X, sourceOrigin.Y + wh, wh, 1),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // right
                b.Draw(texture: Game1.mouseCursors,
                       destinationRectangle: new Rectangle((int)positionPadded.X + (int)textSize.X, (int)positionPadded.Y + (wh * Game1.pixelZoom), wh * Game1.pixelZoom, (int)textSize.Y),
                       sourceRectangle: new Rectangle(sourceOrigin.X + 28, sourceOrigin.Y + wh, wh, 1),
                       color: Color.White,
                       rotation: 0f,
                       origin: Vector2.Zero,
                       effects: SpriteEffects.None,
                       layerDepth: 1f);
                // title text
                b.DrawString(
                    spriteFont: Game1.dialogueFont,
                    text: this._titleString,
                    position: positionPadded + new Vector2(iconSize.X * iconScale, 0) + new Vector2(wh * Game1.pixelZoom) + new Vector2(padding.X / 4, -6f * iconScale),
                    color: Game1.textColor);


                // Draw actual popup
                Game1.drawDialogueBox(
                    x: this.xPositionOnScreen,
                    y: this.yPositionOnScreen,
                    width: this.width,
                    height: this.height,
                    speaker: false,
                    drawOnlyBox: true);

                const int paddingY = 3;
                int       x        = this.xPositionOnScreen + (this.width / 2);
                int       y        = this.yPositionOnScreen;
                int       yOffset  = IClickableMenu.spaceToClearTopBorder;

                foreach (string variantKey in this.VariantKeys)
                {
                    yOffset += (int)Game1.dialogueFont.MeasureString(_itemStrings[variantKey]).Y + (paddingY * Game1.pixelZoom)
                               + ((Game1.smallestTileSize + paddingY) * 2);
                    int xOffset = -(int)((Game1.smallFont.MeasureString(_itemStrings[variantKey]).X / 2) - (Game1.smallestTileSize * Game1.pixelZoom));
                    b.DrawString(
                        spriteFont: Game1.smallFont,
                        text: _itemStrings[variantKey],
                        position: new Vector2(
                            x + xOffset,
                            y + yOffset),
                        color: Game1.textColor);

                    yOffset -= (Game1.smallestTileSize * Game1.pixelZoom);
                    b.Draw(
                        texture: ModEntry.Sprites[this._itemSprites[variantKey].Key],
                        sourceRectangle: OutdoorPot.GetSpriteSourceRectangle(spriteIndex: this._itemSprites[variantKey].Value),
                        position: new Vector2(
                            x + xOffset - (Game1.smallestTileSize * 1.5f * Game1.pixelZoom),
                            y + yOffset),
                        color: Color.White,
                        rotation: 0f,
                        origin: Vector2.Zero,
                        scale: Game1.pixelZoom,
                        effects: SpriteEffects.None,
                        layerDepth: 1f);
                    yOffset += (Game1.smallestTileSize * Game1.pixelZoom);
                }
                this.OkButton.draw(b);

                if (!Game1.options.SnappyMenus)
                {
                    Game1.mouseCursorTransparency = 1f;
                    this.drawMouse(b);
                }
            }
        }