using System.Runtime.InteropServices; namespace WeaveLoader.API.Block; /// /// Base class for managed custom blocks. /// Mods can inherit and override callbacks for block behavior. /// public abstract class Block { /// The namespaced ID used during registration. public Identifier? Id { get; internal set; } /// The numeric runtime ID allocated by the game. public int NumericId { get; internal set; } = -1; /// Optional override for the block to drop when broken. public Identifier? DropAsBlockId { get; init; } /// Optional override for the block to clone via pick-block. public Identifier? CloneAsBlockId { get; init; } /// /// Called when the block is placed in the world. /// public virtual void OnPlace(BlockUpdateContext context) { } /// /// Called when a neighboring block changes. /// public virtual void OnNeighborChanged(BlockNeighborChangedContext context) { } /// /// Called when a scheduled tick for this block fires. /// public virtual void OnScheduledTick(BlockTickContext context) { } /// /// Called when the block is used (right-click). /// Return to run vanilla logic, /// or to skip vanilla handling. /// public virtual BlockActionResult OnUse(BlockUseContext context) => BlockActionResult.ContinueVanilla; /// /// Called when an entity steps on the block. /// public virtual void OnStepOn(BlockEntityContext context) { } /// /// Called when an entity is inside the block's volume. /// public virtual void OnEntityInsideTile(BlockEntityContext context) { } /// /// Called when an entity falls onto the block. /// public virtual void OnFallOn(BlockFallContext context) { } /// /// Called before the block is removed from the world. /// public virtual void OnRemoving(BlockRemovingContext context) { } /// /// Called after the block is removed from the world. /// public virtual void OnRemoved(BlockRemoveContext context) { } /// /// Called when the block is destroyed by the world (non-player). /// public virtual void OnDestroyed(BlockDestroyContext context) { } /// /// Called when the block is destroyed by a player. /// public virtual void OnPlayerDestroy(BlockPlayerDestroyContext context) { } /// /// Called just before a player destroys the block. /// public virtual void OnPlayerWillDestroy(BlockPlayerWillDestroyContext context) { } /// /// Called when the block is placed by a player with an item. /// public virtual void OnPlacedBy(BlockPlacedByContext context) { } } /// Managed falling block base class. public class FallingBlock : Block { } /// Managed slab block base class. public class SlabBlock : Block { } /// /// Result of managed block action callbacks. /// public enum BlockActionResult { ContinueVanilla = 0, CancelVanilla = 1 } /// /// Runtime context for block update callbacks. /// public readonly struct BlockUpdateContext { public int BlockId { get; } public bool IsClientSide { get; } public nint NativeLevelPtr { get; } public int X { get; } public int Y { get; } public int Z { get; } internal BlockUpdateContext(int blockId, bool isClientSide, nint nativeLevelPtr, int x, int y, int z) { BlockId = blockId; IsClientSide = isClientSide; NativeLevelPtr = nativeLevelPtr; X = x; Y = y; Z = z; } public bool HasNeighborSignal() { if (NativeLevelPtr == 0) return false; return NativeInterop.native_level_has_neighbor_signal(NativeLevelPtr, X, Y, Z) != 0; } /// /// Set the block at this context's position. /// public bool SetBlock(Identifier id, int data = 0, int flags = 2) { int numericId = IdHelper.GetBlockNumericId(id); if (numericId < 0) return false; return SetBlock(numericId, data, flags); } /// /// Set the block at this context's position using a numeric ID. /// public bool SetBlock(int numericBlockId, int data = 0, int flags = 2) { if (NativeLevelPtr == 0 || numericBlockId < 0) return false; return NativeInterop.native_level_set_tile(NativeLevelPtr, X, Y, Z, numericBlockId, data, flags) != 0; } /// /// Schedule a tick for this block after a delay (in ticks). /// public bool ScheduleTick(int delay) { if (NativeLevelPtr == 0 || delay < 0) return false; return NativeInterop.native_level_schedule_tick(NativeLevelPtr, X, Y, Z, BlockId, delay) != 0; } /// /// Read a block ID relative to this context's position. /// public int GetBlockId(int offsetX = 0, int offsetY = 0, int offsetZ = 0) { if (NativeLevelPtr == 0) return -1; return NativeInterop.native_level_get_tile(NativeLevelPtr, X + offsetX, Y + offsetY, Z + offsetZ); } } /// /// Runtime context for block neighbor-change callback. /// public readonly struct BlockNeighborChangedContext { public BlockUpdateContext Block { get; } public int NeighborBlockId { get; } internal BlockNeighborChangedContext(BlockUpdateContext block, int neighborBlockId) { Block = block; NeighborBlockId = neighborBlockId; } } /// /// Runtime context for scheduled tick callback. /// public readonly struct BlockTickContext { public BlockUpdateContext Block { get; } internal BlockTickContext(BlockUpdateContext block) { Block = block; } } /// /// Runtime context for block use callback. /// public readonly struct BlockUseContext { public BlockUpdateContext Block { get; } public nint NativePlayerPtr { get; } public int Face { get; } public float ClickX { get; } public float ClickY { get; } public float ClickZ { get; } public bool SoundOnly { get; } internal BlockUseContext(BlockUpdateContext block, nint nativePlayerPtr, int face, float clickX, float clickY, float clickZ, bool soundOnly) { Block = block; NativePlayerPtr = nativePlayerPtr; Face = face; ClickX = clickX; ClickY = clickY; ClickZ = clickZ; SoundOnly = soundOnly; } } /// /// Runtime context for entity-on-block callbacks. /// public readonly struct BlockEntityContext { public BlockUpdateContext Block { get; } public nint NativeEntityPtr { get; } internal BlockEntityContext(BlockUpdateContext block, nint nativeEntityPtr) { Block = block; NativeEntityPtr = nativeEntityPtr; } } /// /// Runtime context for block fall-on callback. /// public readonly struct BlockFallContext { public BlockUpdateContext Block { get; } public nint NativeEntityPtr { get; } public float FallDistance { get; } internal BlockFallContext(BlockUpdateContext block, nint nativeEntityPtr, float fallDistance) { Block = block; NativeEntityPtr = nativeEntityPtr; FallDistance = fallDistance; } } /// /// Runtime context for block removing callback. /// public readonly struct BlockRemovingContext { public BlockUpdateContext Block { get; } public int BlockData { get; } internal BlockRemovingContext(BlockUpdateContext block, int blockData) { Block = block; BlockData = blockData; } } /// /// Runtime context for block removed callback. /// public readonly struct BlockRemoveContext { public BlockUpdateContext Block { get; } public int RemovedBlockId { get; } public int RemovedBlockData { get; } internal BlockRemoveContext(BlockUpdateContext block, int removedBlockId, int removedBlockData) { Block = block; RemovedBlockId = removedBlockId; RemovedBlockData = removedBlockData; } } /// /// Runtime context for block destroyed callback. /// public readonly struct BlockDestroyContext { public BlockUpdateContext Block { get; } public int BlockData { get; } internal BlockDestroyContext(BlockUpdateContext block, int blockData) { Block = block; BlockData = blockData; } } /// /// Runtime context for player-destroyed callback. /// public readonly struct BlockPlayerDestroyContext { public BlockUpdateContext Block { get; } public nint NativePlayerPtr { get; } public int BlockData { get; } internal BlockPlayerDestroyContext(BlockUpdateContext block, nint nativePlayerPtr, int blockData) { Block = block; NativePlayerPtr = nativePlayerPtr; BlockData = blockData; } } /// /// Runtime context for player-will-destroy callback. /// public readonly struct BlockPlayerWillDestroyContext { public BlockUpdateContext Block { get; } public nint NativePlayerPtr { get; } public int BlockData { get; } internal BlockPlayerWillDestroyContext(BlockUpdateContext block, nint nativePlayerPtr, int blockData) { Block = block; NativePlayerPtr = nativePlayerPtr; BlockData = blockData; } } /// /// Runtime context for block placed-by callback. /// public readonly struct BlockPlacedByContext { public BlockUpdateContext Block { get; } public nint NativePlacerPtr { get; } public nint NativeItemInstancePtr { get; } internal BlockPlacedByContext(BlockUpdateContext block, nint nativePlacerPtr, nint nativeItemInstancePtr) { Block = block; NativePlacerPtr = nativePlacerPtr; NativeItemInstancePtr = nativeItemInstancePtr; } } [StructLayout(LayoutKind.Sequential)] internal struct BlockUpdateNativeArgs { public int BlockId; public int IsClientSide; public nint LevelPtr; public int X; public int Y; public int Z; } [StructLayout(LayoutKind.Sequential)] internal struct BlockNeighborChangedNativeArgs { public BlockUpdateNativeArgs Block; public int NeighborBlockId; } [StructLayout(LayoutKind.Sequential)] internal struct BlockUseNativeArgs { public BlockUpdateNativeArgs Block; public nint PlayerPtr; public int Face; public float ClickX; public float ClickY; public float ClickZ; public int SoundOnly; } [StructLayout(LayoutKind.Sequential)] internal struct BlockEntityNativeArgs { public BlockUpdateNativeArgs Block; public nint EntityPtr; } [StructLayout(LayoutKind.Sequential)] internal struct BlockFallNativeArgs { public BlockUpdateNativeArgs Block; public nint EntityPtr; public float FallDistance; } [StructLayout(LayoutKind.Sequential)] internal struct BlockRemovingNativeArgs { public BlockUpdateNativeArgs Block; public int BlockData; } [StructLayout(LayoutKind.Sequential)] internal struct BlockRemoveNativeArgs { public BlockUpdateNativeArgs Block; public int RemovedBlockId; public int RemovedBlockData; } [StructLayout(LayoutKind.Sequential)] internal struct BlockDestroyNativeArgs { public BlockUpdateNativeArgs Block; public int BlockData; } [StructLayout(LayoutKind.Sequential)] internal struct BlockPlayerDestroyNativeArgs { public BlockUpdateNativeArgs Block; public nint PlayerPtr; public int BlockData; } [StructLayout(LayoutKind.Sequential)] internal struct BlockPlayerWillDestroyNativeArgs { public BlockUpdateNativeArgs Block; public nint PlayerPtr; public int BlockData; } [StructLayout(LayoutKind.Sequential)] internal struct BlockPlacedByNativeArgs { public BlockUpdateNativeArgs Block; public nint PlacerPtr; public nint ItemInstancePtr; } internal static class ManagedBlockDispatcher { private static readonly object s_lock = new(); private static readonly Dictionary s_blocks = new(); internal static void RegisterBlock(Identifier id, int numericId, Block block) { block.Id = id; block.NumericId = numericId; lock (s_lock) { s_blocks[numericId] = block; } } internal static int HandlePlace(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockUpdateNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.BlockId, out block); } if (block == null) return 0; block.OnPlace(new BlockUpdateContext( nativeArgs.BlockId, nativeArgs.IsClientSide != 0, nativeArgs.LevelPtr, nativeArgs.X, nativeArgs.Y, nativeArgs.Z)); return 1; } internal static int HandleNeighborChanged(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockNeighborChangedNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnNeighborChanged(new BlockNeighborChangedContext(update, nativeArgs.NeighborBlockId)); return 1; } internal static int HandleScheduledTick(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockUpdateNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.BlockId, out block); } if (block == null) return 0; block.OnScheduledTick(new BlockTickContext(new BlockUpdateContext( nativeArgs.BlockId, nativeArgs.IsClientSide != 0, nativeArgs.LevelPtr, nativeArgs.X, nativeArgs.Y, nativeArgs.Z))); return 1; } internal static int HandleUse(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockUseNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); var result = block.OnUse(new BlockUseContext( update, nativeArgs.PlayerPtr, nativeArgs.Face, nativeArgs.ClickX, nativeArgs.ClickY, nativeArgs.ClickZ, nativeArgs.SoundOnly != 0)); return result == BlockActionResult.CancelVanilla ? 2 : 1; } internal static int HandleStepOn(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockEntityNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnStepOn(new BlockEntityContext(update, nativeArgs.EntityPtr)); return 1; } internal static int HandleEntityInsideTile(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockEntityNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnEntityInsideTile(new BlockEntityContext(update, nativeArgs.EntityPtr)); return 1; } internal static int HandleFallOn(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockFallNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnFallOn(new BlockFallContext(update, nativeArgs.EntityPtr, nativeArgs.FallDistance)); return 1; } internal static int HandleRemoving(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockRemovingNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnRemoving(new BlockRemovingContext(update, nativeArgs.BlockData)); return 1; } internal static int HandleRemoved(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockRemoveNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnRemoved(new BlockRemoveContext(update, nativeArgs.RemovedBlockId, nativeArgs.RemovedBlockData)); return 1; } internal static int HandleDestroyed(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockDestroyNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnDestroyed(new BlockDestroyContext(update, nativeArgs.BlockData)); return 1; } internal static int HandlePlayerDestroy(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockPlayerDestroyNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnPlayerDestroy(new BlockPlayerDestroyContext(update, nativeArgs.PlayerPtr, nativeArgs.BlockData)); return 1; } internal static int HandlePlayerWillDestroy(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockPlayerWillDestroyNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnPlayerWillDestroy(new BlockPlayerWillDestroyContext(update, nativeArgs.PlayerPtr, nativeArgs.BlockData)); return 1; } internal static int HandlePlacedBy(IntPtr args, int sizeBytes) { if (args == IntPtr.Zero || sizeBytes < Marshal.SizeOf()) return 0; BlockPlacedByNativeArgs nativeArgs = Marshal.PtrToStructure(args); Block? block; lock (s_lock) { s_blocks.TryGetValue(nativeArgs.Block.BlockId, out block); } if (block == null) return 0; var update = new BlockUpdateContext( nativeArgs.Block.BlockId, nativeArgs.Block.IsClientSide != 0, nativeArgs.Block.LevelPtr, nativeArgs.Block.X, nativeArgs.Block.Y, nativeArgs.Block.Z); block.OnPlacedBy(new BlockPlacedByContext(update, nativeArgs.PlacerPtr, nativeArgs.ItemInstancePtr)); return 1; } }