using System; using System.Text; using Vintagestory.API.Client; using Vintagestory.API.Common; using Vintagestory.API.Datastructures; using Vintagestory.API.MathTools; using Vintagestory.GameContent; namespace ButterflyPins.BlockEntities; public class BlockEntityButterflyPinBoard : BlockEntityDisplay { private const int MaxBoardSlots = 9; private const float BoardMargin = 1f / 16f; private const float BoardSurface = 1f / 16f; private const string ButterflyPinPrefix = "clothes-butterflypin-"; private readonly InventoryGeneric inventory; public override InventoryBase Inventory => inventory; public override string InventoryClassName => "butterflypinboard"; public override int DisplayedItems => ActiveSlotCount; private int Rows => Math.Max(1, Block?.Attributes?["rows"].AsInt(2) ?? 2); private int Columns => Math.Max(1, Block?.Attributes?["columns"].AsInt(2) ?? 2); private float RenderScale => Block?.Attributes?["renderScale"].AsFloat(0.18f) ?? 0.18f; private float DisplayOriginX => GetDisplayFloat("displayOriginX", 0.31666666f); private float DisplayOriginY => GetDisplayFloat("displayOriginY", 0.01333332f); private float DisplayOriginZ => GetDisplayFloat("displayOriginZ", 0.47916666f); private float DisplayStepX => GetDisplayFloat("displayStepX", -0.25f); private float DisplayStepY => GetDisplayFloat("displayStepY", -0.25f); private int ActiveSlotCount => Rows * Columns; public BlockEntityButterflyPinBoard() { inventory = new InventoryGeneric(MaxBoardSlots, null, null); } public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldForResolving) { base.FromTreeAttributes(tree, worldForResolving); RedrawAfterReceivingTreeAttributes(worldForResolving); } public bool TryInsert(IPlayer byPlayer, int slotIndex, ItemSlot activeSlot) { if (!IsValidSlot(slotIndex) || activeSlot.Empty || !CanInsert(slotIndex, activeSlot.Itemstack)) return false; ItemStack sourceStack = activeSlot.Itemstack; ItemStack placedStack = sourceStack.Clone(); placedStack.StackSize = 1; inventory[slotIndex].Itemstack = placedStack; activeSlot.TakeOut(1); activeSlot.MarkDirty(); inventory[slotIndex].MarkDirty(); MarkChanged(); return true; } public bool TryTake(IPlayer byPlayer, int slotIndex) { if (!CanTake(slotIndex)) return false; ItemSlot slot = inventory[slotIndex]; ItemStack stack = slot.Itemstack!; slot.Itemstack = null; slot.MarkDirty(); if (!byPlayer.InventoryManager.TryGiveItemstack(stack, true)) Api.World.SpawnItemEntity(stack, Pos.ToVec3d().Add(0.5, 0.5, 0.5)); MarkChanged(); return true; } public bool CanInsert(int slotIndex, ItemStack stack) { return IsValidSlot(slotIndex) && inventory[slotIndex].Empty && IsButterflyPin(stack); } public bool CanTake(int slotIndex) { return IsValidSlot(slotIndex) && !inventory[slotIndex].Empty; } public void DropContents() { for (int slotIndex = 0; slotIndex < ActiveSlotCount; slotIndex++) { ItemSlot slot = inventory[slotIndex]; ItemStack? stack = slot.Itemstack; if (stack == null) continue; Api.World.SpawnItemEntity(stack, Pos.ToVec3d().Add(0.5, 0.5, 0.5)); slot.Itemstack = null; slot.MarkDirty(); } } public override void GetBlockInfo(IPlayer forPlayer, StringBuilder dsc) { int highlightedSlot = -1; BlockSelection? selection = forPlayer?.CurrentBlockSelection; if (selection?.Position != null && selection.Position.Equals(Pos)) highlightedSlot = GetSlotIndex(selection.HitPosition); AppendContentsBlockInfo(dsc, highlightedSlot); } public bool AppendContentsBlockInfo(StringBuilder builder, int highlightedSlot) { int initialLength = builder.Length; for (int slotIndex = 0; slotIndex < ActiveSlotCount; slotIndex++) { ItemStack? stack = inventory[slotIndex].Itemstack; if (stack == null) continue; if (builder.Length > initialLength) builder.AppendLine(); else if (initialLength > 0) builder.AppendLine(); string itemName = stack.GetName(); if (slotIndex == highlightedSlot) { builder.Append(""); builder.Append(itemName); builder.Append(""); continue; } builder.Append(itemName); } return builder.Length > initialLength; } public int GetSlotIndex(Vec3d hitPosition) { Vec3d local = ToBaseOrientation(hitPosition); if (local.X < BoardMargin || local.X > 1 - BoardMargin || local.Y < BoardMargin || local.Y > 1 - BoardMargin) return -1; double width = (1 - (BoardMargin * 2)) / Columns; double height = (1 - (BoardMargin * 2)) / Rows; int column = GameMath.Clamp((int)((local.X - BoardMargin) / width), 0, Columns - 1); if (IsNorthSouthVariant()) column = (Columns - 1) - column; int rowFromBottom = GameMath.Clamp((int)((local.Y - BoardMargin) / height), 0, Rows - 1); int rowFromTop = Rows - 1 - rowFromBottom; return (rowFromTop * Columns) + column; } private bool IsValidSlot(int slotIndex) { return slotIndex >= 0 && slotIndex < ActiveSlotCount; } private bool IsButterflyPin(ItemStack stack) { AssetLocation? code = stack.Collectible?.Code; return code != null && code.Path.StartsWith(ButterflyPinPrefix, StringComparison.Ordinal); } private void MarkChanged() { RedrawAfterReceivingTreeAttributes(Api.World); base.MarkDirty(true); Api.World.BlockAccessor.MarkBlockDirty(Pos); } private Vec3d ToBaseOrientation(Vec3d hitPosition) { Vec3d centered = new(hitPosition.X - 0.5, hitPosition.Y, hitPosition.Z - 0.5); float angle = -GetRotationY(); float sin = GameMath.Sin(angle); float cos = GameMath.Cos(angle); return new Vec3d( (centered.X * cos) - (centered.Z * sin) + 0.5, hitPosition.Y, (centered.X * sin) + (centered.Z * cos) + 0.5 ); } private float GetDisplayFloat(string key, float defaultValue) { return Block?.Attributes?[key].AsFloat(defaultValue) ?? defaultValue; } protected override float[][] genTransformationMatrices() { float[][] matrices = new float[DisplayedItems][]; float boardRotationY = GetRotationY(); float itemRotationY = boardRotationY - GameMath.PIHALF; for (int slotIndex = 0; slotIndex < DisplayedItems; slotIndex++) { int row = slotIndex / Columns; int column = slotIndex % Columns; Vec3f off = new( DisplayOriginX + (column * DisplayStepX), DisplayOriginY + (row * DisplayStepY), DisplayOriginZ ); off = new Matrixf().RotateY(boardRotationY).TransformVector(off.ToVec4f(0)).XYZ; matrices[slotIndex] = new Matrixf() .Translate(off.X, off.Y, off.Z) .Translate(0.5f, 0.5f, 0.5f) .RotateY(itemRotationY) .Scale(RenderScale, RenderScale, RenderScale) .Translate(-0.5f, -0.5f, -0.5f) .Values; } return matrices; } public float GetRotationY() { string side = Block?.Variant?["side"] ?? "south"; return side switch { "north" => GameMath.PI, "east" => GameMath.PIHALF, "west" => GameMath.PI + GameMath.PIHALF, _ => 0f, }; } private bool IsNorthSouthVariant() { string side = Block?.Variant?["side"] ?? "south"; return side is "north" or "south"; } public float GetRenderScale() { return RenderScale; } }