ShopGui
A shop in a custom GUI
Source: src/scripting/managed/samples/ShopGui/ · version 1.0.0. The code below is read from the repository at build time.
What it shows
Section titled “What it shows”- commands
- Brigadier command trees (v1)
- config (v1)
- permissions (v1)
Build and run
Section titled “Build and run”dotnet build src/scripting/managed/samples/ShopGui -c Release# the output lands in build/bin/dotnet/samples/ShopGui/ when built through CMake (MC_BUILD_SCRIPTING);# copy the dll into your plugins folder and start the server with --pluginsSource
Section titled “Source”ShopGui.cs
Section titled “ShopGui.cs”using System.Text.Json.Serialization;
namespace ShopGui;
/// <summary>One thing the shop sells.</summary>public sealed class ShopItem{ /// <summary>The item key, e.g. <c>minecraft:bread</c>.</summary> public string Item { get; set; } = "minecraft:bread"; /// <summary>How many the player gets for one purchase.</summary> public int Count { get; set; } = 1; /// <summary>The price in units of the currency item.</summary> public int Price { get; set; } = 1;}
/// <summary>The settings of the ShopGui plugin.</summary>public sealed class ShopConfig{ /// <summary>The item players pay with.</summary> public string Currency { get; set; } = "minecraft:gold_ingot"; /// <summary>The title of the shop window.</summary> public string Title { get; set; } = "Shop"; /// <summary>What the shop sells, in slot order (at most 21 entries: three rows of seven).</summary> public List<ShopItem> Items { get; set; } = [ new() { Item = "minecraft:bread", Count = 8, Price = 1 }, new() { Item = "minecraft:arrow", Count = 16, Price = 2 }, new() { Item = "minecraft:iron_pickaxe", Count = 1, Price = 6 }, new() { Item = "minecraft:golden_apple", Count = 1, Price = 12 }, new() { Item = "minecraft:diamond", Count = 1, Price = 20 }, ];}
[JsonSerializable(typeof(ShopConfig))]internal sealed partial class ShopJson : JsonSerializerContext;
/// <summary>Sample: <c>/shop</c> opens a custom chest GUI. Every product is an icon with a price in its lore; clicking it pays with a currency item from the player's inventory/// (<see cref="EngineInventory.RemoveItem"/>) and hands over the product (<see cref="PlayerInventory.Give"/>). The menu is locked by the engine: nothing in it can be taken out, shift clicked,/// dragged or swapped away, whatever a handler does or forgets to do, so a shop has no duplication holes. It also shows <c>OnClose</c> and the config file with its live reload.</summary>[PluginInfo("ShopGui", Version = "1.0.0", Author = "Ramjet", Description = "A shop in a custom GUI")]public sealed class ShopGuiPlugin : Plugin{ private Config<ShopConfig> _config = null!; private Menu? _menu;
protected override void OnEnable() { _config = Config.Load(ShopJson.Default.ShopConfig); _config.Watch = true; _config.Changed += _ => Rebuild(); Permissions.Register("shop.use", PermissionDefault.True, "Open the shop"); Rebuild();
Commands.Register(Command.Literal("shop").Permission("shop.use").Executes(ctx => { var player = ctx.RequirePlayer(); _menu!.Open(player); return 1; }), description: "Open the shop"); }
// The menu is rebuilt when the config changes: the old one is closed for its viewers and the new one is opened for them again. private void Rebuild() { var viewers = _menu?.Viewers.ToList() ?? []; _menu?.Dispose(); var config = _config.Value; var menu = Menus.Chest(3, Component.Text(config.Title).Color(TextColor.DarkGreen)); menu.FillEmpty(new ItemStack("minecraft:gray_stained_glass_pane").WithName(Component.Text(" "))); for (int i = 0; i < Math.Min(config.Items.Count, 21); i++) { var product = config.Items[i]; int slot = (i / 7) * 9 + 1 + (i % 7); // three rows, columns 1-7 var stack = new ItemStack(product.Item, product.Count); var icon = stack.WithLore( Component.Text($"Price: {product.Price} x {Pretty(config.Currency)}").Color(TextColor.Gold).Italic(false), Component.Text("Click to buy").Color(TextColor.Gray).Italic(false)); menu.Slot(slot, icon, click => Buy(click, product, stack)); } menu.OnClose((player, reason) => Log.Info($"{player.Name} left the shop ({reason})")); _menu = menu; foreach (var viewer in viewers) if (viewer.IsOnline) menu.Open(viewer); }
private void Buy(MenuClick click, ShopItem product, ItemStack stack) { var player = click.Player; var inventory = player.Inventory; var price = new ItemStack(_config.Value.Currency, Math.Max(product.Price, 1)); if (inventory.Count(price) < price.Count) { player.SendActionBar(Component.Text($"You need {price.Count} x {Pretty(price.Type)}").Color(TextColor.Red)); player.PlaySound("minecraft:entity.villager.no", SoundCategory.Players); return; } inventory.RemoveItem(price); int dropped = inventory.Give(stack); player.SendActionBar(Component.Text($"Bought {stack.Count} x {Pretty(stack.Type)}" + (dropped > 0 ? " (your inventory was full: it is at your feet)" : "")).Color(TextColor.Green)); player.PlaySound("minecraft:entity.villager.yes", SoundCategory.Players); }
private static string Pretty(string key) => key[(key.IndexOf(':') + 1)..].Replace('_', ' ');}ShopGui.csproj
Section titled “ShopGui.csproj”<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <McApiProject>$(MSBuildThisFileDirectory)..\..\Mc.Scripting.Api\Mc.Scripting.Api.csproj</McApiProject> </PropertyGroup> <Import Project="..\..\Mc.Scripting.Plugin.props" /> <PropertyGroup Condition="'$(McOutDir)' != ''"> <OutputPath>$(McOutDir)samples/ShopGui/</OutputPath> </PropertyGroup></Project>