diff --git a/Minecraft.Server.FourKit/Block/Block.cs b/Minecraft.Server.FourKit/Block/Block.cs index 022b9b616..1005909ea 100644 --- a/Minecraft.Server.FourKit/Block/Block.cs +++ b/Minecraft.Server.FourKit/Block/Block.cs @@ -121,4 +121,24 @@ public bool breakNaturally() return NativeBridge.BreakBlock(_world.getDimensionId(), _x, _y, _z) != 0; return false; } + + /// + ///Captures the current state of this block. You may then cast that state + ///into any accepted type, such as Furnace or Sign. + ///The returned object will never be updated, and you are not guaranteed + ///that(for example) a sign is still a sign after you capture its state. + /// + ///BlockState with the current state of this block. + public BlockState getState() + { + Material material = getType(); + + switch (material) + { + case Material.SIGN: + return new Sign(this); + default: + return new BlockState(this); + } + } } diff --git a/Minecraft.Server.FourKit/Block/BlockState.cs b/Minecraft.Server.FourKit/Block/BlockState.cs new file mode 100644 index 000000000..aaee6c32e --- /dev/null +++ b/Minecraft.Server.FourKit/Block/BlockState.cs @@ -0,0 +1,264 @@ +namespace Minecraft.Server.FourKit.Block; + + +/// +/// Represents a captured state of a block, which will not change automatically. +/// +/// Unlike Block, which only one object can exist per coordinate, BlockState +/// can exist multiple times for any given Block. +/// +/// +/// Note that another plugin may change the state of the block and you will not know, or they may change the +/// block to another type entirely, causing your BlockState to become invalid. +/// +/// +public class BlockState +{ + private readonly World _world; + private readonly int _x; + private readonly int _y; + private readonly int _z; + protected Material type; + protected byte data; + + public BlockState(Block block) + { + _world = block.getWorld(); + _x = block.getX(); + _y = block.getY(); + _z = block.getZ(); + type = block.getType(); + data = block.getData(); + } + + public static BlockState getBlockState(World world, int x, int y, int z) + { + return new BlockState(world.getBlockAt(x, y, z)); + } + + /// + /// Gets the world which contains this Block + /// + /// World containing this block + public World getWorld() => _world; + + /// + /// Gets the x-coordinate of this block + /// + /// x-coordinate + public int getX() => _x; + + /// + /// Gets the y-coordinate of this block + /// + /// y-coordinate + public int getY() => _y; + + /// + /// Gets the z-coordinate of this block + /// + /// z-coordinate + public int getZ() => _z; + + /// + /// Sets the data value for this block + /// + /// New block specific data + public void setData(byte data) + { + this.data = data; + } + + /// + /// Gets the data value for this block + /// + /// block specific data + public byte getData() => data; + + /// + /// Sets the type of this block, resets block data to 0 if the block type changes + /// + /// Material to change this block to + public void setType(Material type) + { + if (this.type != type) + { + this.type = type; + setData((byte)0); + } + } + + /// + /// Sets the type-id of this block + /// + /// Type-Id to change this block to + /// Whether the change was successful + public bool setTypeId(int typeId) + { + if (!Enum.IsDefined(typeof(Material), typeId)) + { + return false; + } + + setType((Material)typeId); + return true; + } + + /// + /// Gets the type of this block + /// + /// block type + public Material getType() => type; + + /// + /// Gets the type-id of this block + /// + /// block type-id + public int getTypeId() => (int)type; + + + /// + /// Gets the block represented by this BlockState + /// + /// Block that this BlockState represents + public Block getBlock() => _world.getBlockAt(_x, _y, _z); + + /// + /// Gets the location of this block + /// + /// location + public Location getLocation() + { + return new Location(_world, _x, _y, _z); + } + + /// + /// Stores the location of this block in the provided Location object. + /// + /// If the provided Location is null this method does nothing and returns null. + /// + /// + /// Location object to modify + /// The Location object provided or null + public Location getLocation(Location loc) + { + if (loc != null) + { + loc.setWorld(_world); + loc.setX(_x); + loc.setY(_y); + loc.setZ(_z); + loc.setYaw(0f); + loc.setPitch(0f); + } + + return loc; + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// + /// This has the same effect as calling update(false). That is to say, + /// this will not modify the state of a block if it is no longer the same + /// type as it was when this state was taken.It will return false in this + /// eventuality. + /// + /// + /// true if the update was successful, otherwise false + /// + public bool update() + { + return update(false); + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// + /// NOT IMPLEMENTED This has the same effect as calling update(force, true). That is to + /// say, this will trigger a physics update to surrounding blocks. + /// + /// + /// true to forcefully set the state + /// true if the update was successful, otherwise false + public bool update(bool force) + { + return update(force, true); + } + + /// + /// Attempts to update the block represented by this state, setting it to + /// the new values as defined by this state. + /// + /// Unless force is true, this will not modify the state of a block if it + /// is no longer the same type as it was when this state was taken.It will + /// return false in this eventuality. + /// + /// + /// If force is true, it will set the type of the block to match the new + /// state, set the state data and then return true. + /// + /// + /// NOT IMPLEMENTED If applyPhysics is true, it will trigger a physics update on + /// surrounding blocks which could cause them to update or disappear. + /// + /// + /// true to forcefully set the state + /// NOT IMPLEMENTED false to cancel updating physics on surrounding blocks + /// true if the update was successful, otherwise false + public virtual bool update(bool force, bool applyPhysics) + { + Block block = getBlock(); + + if (block.getType() != getType()) + { + if (force) + { + block.setType(type); //pass applyPhysics when implemented, fix docs for this func and related ones when implemented + } else + { + return false; + } + } + + block.setData(data); //pass applyPhysics when implemented + //_world.getHandle.notify(_x, _y, _z); //when implemented + + return true; + } + + //TODO: implement World.Equals + //public override bool Equals(object? obj) + //{ + // if (ReferenceEquals(this, obj)) + // { + // return true; + // } + + // if (obj is not BlockState other) + // { + // return false; + // } + + // return (_world == other._world //|| _world.Equals(other._world)) + // && _x == other._x + // && _y == other._y + // && _z == other._z + // && type == other.type + // && data == other.data; + //} + + //TODO: implement World.GetHashCode + //public override int GetHashCode() + //{ + // int hash = 7; + // hash = 73 * hash + _world.GetHashCode(); + // hash = 73 * hash + _x; + // hash = 73 * hash + _y; + // hash = 73 * hash + _z; + // hash = 73 * hash + (int)type; + // hash = 73 * hash + data.GetHashCode(); + // return hash; + //} +} \ No newline at end of file diff --git a/Minecraft.Server.FourKit/Block/Sign.cs b/Minecraft.Server.FourKit/Block/Sign.cs new file mode 100644 index 000000000..e09f05a44 --- /dev/null +++ b/Minecraft.Server.FourKit/Block/Sign.cs @@ -0,0 +1,142 @@ +namespace Minecraft.Server.FourKit.Block; + +using System.Runtime.InteropServices; + +public class Sign : BlockState +{ + private readonly string[] _lines; + + public Sign(Block block) : base(block) + { + _lines = new string[4]; + + int dimId = block.getWorld().getDimensionId(); + int x = block.getX(); + int y = block.getY(); + int z = block.getZ(); + + for (int i = 0; i < 4; i++) + { + _lines[i] = ReadLineFromNative(dimId, x, y, z, i); + } + } + + + /// + /// Gets all the lines of text currently on this sign. + /// + /// Array of Strings containing each line of text + public string[] getLines() => _lines; + + /// + /// Gets the line of text at the specified index. + /// For example, getLine(0) will return the first line of text. + /// + /// Line number to get the text from, starting at 0 + /// Text on the given line + /// Thrown when the line does not exist + public string getLine(int index) + { + ValidateLineIndex(index); + return _lines[index]; + } + + /// + /// Sets the line of text at the specified index. + /// For example, setLine(0, "Line One") will set the first line of text to + /// "Line One". + /// + /// Line number to set the text at, starting from 0 + /// New text to set at the specified index + /// Thrown when the line does not exist + public void setLine(int index, string line) + { + ValidateLineIndex(index); + _lines[index] = line ?? string.Empty; + } + + public override bool update(bool force, bool applyPhysics) + { + bool result = base.update(force, applyPhysics); + + if (!result) + { + return false; + } + + int dimId = getWorld().getDimensionId(); + int x = getX(); + int y = getY(); + int z = getZ(); + + string[] lines = sanitizeLines(_lines); + for (int i = 0; i < 4; i++) + { + WriteLineToNative(dimId, x, y, z, i, lines[i]); + } + + return true; + } + + public static string[] sanitizeLines(string[] lines) + { + string[] sanitized = new string[4]; + for (int i = 0; i < 4; i++) + { + sanitized[i] = (i < lines.Length && lines[i] != null) ? lines[i] : string.Empty; + } + return sanitized; + } + + private static void ValidateLineIndex(int index) + { + if (index < 0 || index > 3) + { + throw new IndexOutOfRangeException($"Line index must be between 0 and 3, got {index}"); + } + } + + private static string ReadLineFromNative(int dimId, int x, int y, int z, int lineIndex) + { + if (NativeBridge.GetSignLine == null) + { + return string.Empty; + } + + IntPtr buf = Marshal.AllocHGlobal(256); + try + { + int len = NativeBridge.GetSignLine(dimId, x, y, z, lineIndex, buf, 256); + if (len <= 0) + { + return string.Empty; + } + + return Marshal.PtrToStringUTF8(buf, len) ?? string.Empty; + } + finally + { + Marshal.FreeHGlobal(buf); + } + } + + private static void WriteLineToNative(int dimId, int x, int y, int z, int lineIndex, string line) + { + if (NativeBridge.SetSignLine == null) + { + return; + } + + string value = line ?? string.Empty; + IntPtr ptr = Marshal.StringToCoTaskMemUTF8(value); + try + { + int len = System.Text.Encoding.UTF8.GetByteCount(value); + NativeBridge.SetSignLine(dimId, x, y, z, lineIndex, ptr, len); + } + finally + { + Marshal.FreeCoTaskMem(ptr); + } + } +} \ No newline at end of file diff --git a/Minecraft.Server.FourKit/FourKitHost.cs b/Minecraft.Server.FourKit/FourKitHost.cs index 8293af672..5f7db0b6b 100644 --- a/Minecraft.Server.FourKit/FourKitHost.cs +++ b/Minecraft.Server.FourKit/FourKitHost.cs @@ -553,11 +553,11 @@ public static void SetNativeCallbacks(IntPtr damage, IntPtr setHealth, IntPtr te } [UnmanagedCallersOnly] - public static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem) + public static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem, IntPtr getSignLine, IntPtr setSignLine) { try { - NativeBridge.SetWorldCallbacks(getTileId, getTileData, setTile, setTileData, breakBlock, getHighestBlockY, getWorldInfo, setWorldTime, setWeather, createExplosion, strikeLightning, setSpawnLocation, dropItem); + NativeBridge.SetWorldCallbacks(getTileId, getTileData, setTile, setTileData, breakBlock, getHighestBlockY, getWorldInfo, setWorldTime, setWeather, createExplosion, strikeLightning, setSpawnLocation, dropItem, getSignLine, setSignLine); //ServerLog.Info("fourkit", "World native callbacks registered."); } catch (Exception ex) diff --git a/Minecraft.Server.FourKit/NativeBridge.cs b/Minecraft.Server.FourKit/NativeBridge.cs index 8dee54294..ef29dbacf 100644 --- a/Minecraft.Server.FourKit/NativeBridge.cs +++ b/Minecraft.Server.FourKit/NativeBridge.cs @@ -108,6 +108,11 @@ internal static class NativeBridge [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void NativeOpenVirtualContainerDelegate(int entityId, int nativeType, IntPtr titleUtf8, int titleByteLen, int slotCount, IntPtr itemsBuf); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetSignLineDelegate(int dimId, int x, int y, int z, int lineIndex, IntPtr outUtf8, int outUtf8Size); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetSignLineDelegate(int dimId, int x, int y, int z, int lineIndex, IntPtr utf8, int byteLen); internal static NativeDamageDelegate? DamagePlayer; internal static NativeSetHealthDelegate? SetPlayerHealth; @@ -144,6 +149,8 @@ internal static class NativeBridge internal static NativeGetContainerViewerEntityIdsDelegate? GetContainerViewerEntityIds; internal static NativeCloseContainerDelegate? CloseContainer; internal static NativeOpenVirtualContainerDelegate? OpenVirtualContainer; + internal static NativeGetSignLineDelegate? GetSignLine; + internal static NativeSetSignLineDelegate? SetSignLine; internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity) { @@ -159,7 +166,7 @@ internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr telepo TeleportEntity = Marshal.GetDelegateForFunctionPointer(teleportEntity); } - internal static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem) + internal static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, IntPtr setTile, IntPtr setTileData, IntPtr breakBlock, IntPtr getHighestBlockY, IntPtr getWorldInfo, IntPtr setWorldTime, IntPtr setWeather, IntPtr createExplosion, IntPtr strikeLightning, IntPtr setSpawnLocation, IntPtr dropItem, IntPtr getSignLine, IntPtr setSignLine) { GetTileId = Marshal.GetDelegateForFunctionPointer(getTileId); GetTileData = Marshal.GetDelegateForFunctionPointer(getTileData); @@ -174,6 +181,8 @@ internal static void SetWorldCallbacks(IntPtr getTileId, IntPtr getTileData, Int StrikeLightning = Marshal.GetDelegateForFunctionPointer(strikeLightning); SetSpawnLocation = Marshal.GetDelegateForFunctionPointer(setSpawnLocation); DropItem = Marshal.GetDelegateForFunctionPointer(dropItem); + GetSignLine = Marshal.GetDelegateForFunctionPointer(getSignLine); + SetSignLine = Marshal.GetDelegateForFunctionPointer(setSignLine); } internal static void SetPlayerCallbacks(IntPtr kickPlayer, IntPtr banPlayer, IntPtr banPlayerIp, IntPtr getPlayerAddress) diff --git a/Minecraft.Server/FourKitBridge.cpp b/Minecraft.Server/FourKitBridge.cpp index 10ad4b067..c5cdf189e 100644 --- a/Minecraft.Server/FourKitBridge.cpp +++ b/Minecraft.Server/FourKitBridge.cpp @@ -38,6 +38,7 @@ #include "..\Minecraft.World\SimpleContainer.h" #include "..\Minecraft.World\Slot.h" #include "..\Minecraft.World\Tile.h" +#include "..\Minecraft.World\SignTileEntity.h" #include "..\Minecraft.World\net.minecraft.world.entity.player.h" #include "Access\Access.h" #include "Common\NetworkUtils.h" @@ -112,7 +113,7 @@ typedef int(__stdcall *fn_fire_player_move)(int entityId, double toX, double toY, double toZ, double *outCoords); typedef void(__stdcall *fn_set_native_callbacks)(void *damage, void *setHealth, void *teleport, void *setGameMode, void *broadcastMessage, void *setFallDistance, void *getPlayerSnapshot, void *sendMessage, void *setWalkSpeed, void *teleportEntity); -typedef void(__stdcall *fn_set_world_callbacks)(void *getTileId, void *getTileData, void *setTile, void *setTileData, void *breakBlock, void *getHighestBlockY, void *getWorldInfo, void *setWorldTime, void *setWeather, void *createExplosion, void *strikeLightning, void *setSpawnLocation, void *dropItem); +typedef void(__stdcall *fn_set_world_callbacks)(void *getTileId, void *getTileData, void *setTile, void *setTileData, void *breakBlock, void *getHighestBlockY, void *getWorldInfo, void *setWorldTime, void *setWeather, void *createExplosion, void *strikeLightning, void *setSpawnLocation, void *dropItem, void *getSignLine, void *setSignLine); typedef void(__stdcall *fn_update_entity_id)(int oldEntityId, int newEntityId); typedef int(__stdcall *fn_fire_player_chat)(int entityId, const char *msgUtf8, int msgByteLen, char *outBuf, int outBufSize, int *outLen); typedef int(__stdcall *fn_fire_block_place)(int entityId, int dimId, @@ -1063,6 +1064,77 @@ static void __cdecl NativeGetContainerViewerEntityIds(int entityId, int *outIds, *outCount = count; } +static int __cdecl NativeGetSignLine(int dimId, int x, int y, int z, int lineIndex, char *outUtf8, int outUtf8Size) +{ + if (lineIndex < 0 || lineIndex > 3 || !outUtf8 || outUtf8Size <= 0) + { + return 0; + } + + ServerLevel *level = GetLevel(dimId); + if (!level) + { + return 0; + } + + shared_ptr tileEntity = level->getTileEntity(x, y, z); + shared_ptr sign = dynamic_pointer_cast(tileEntity); + if (!sign) + { + return 0; + } + + std::wstring wide = sign->GetMessage(lineIndex); + std::string utf8 = ServerRuntime::StringUtils::WideToUtf8(wide); + + int copyLen = (int)utf8.size(); + if (copyLen > outUtf8Size) + { + copyLen = outUtf8Size; + } + + if (copyLen > 0) + { + memcpy(outUtf8, utf8.data(), copyLen); + } + + if (copyLen < outUtf8Size) + { + outUtf8[copyLen] = '\0'; + } + + return copyLen; +} + +static void __cdecl NativeSetSignLine(int dimId, int x, int y, int z, int lineIndex, const char *utf8, int byteLen) +{ + if (lineIndex < 0 || lineIndex > 3) + { + return; + } + + ServerLevel *level = GetLevel(dimId); + if (!level) + { + return; + } + + shared_ptr tileEntity = level->getTileEntity(x, y, z); + shared_ptr sign = dynamic_pointer_cast(tileEntity); + if (!sign) + { + return; + } + + std::string textUtf8 = (utf8 && byteLen > 0) ? std::string(utf8, byteLen) : std::string(); + std::wstring textWide = ServerRuntime::StringUtils::Utf8ToWide(textUtf8); + + sign->SetMessage(lineIndex, textWide); + sign->setChanged(); + level->sendTileUpdated(x, y, z); +} + + static std::wstring FindNet10SystemRoot() { // overengineered @@ -1339,7 +1411,9 @@ void Initialize() (void *)&NativeCreateExplosion, (void *)&NativeStrikeLightning, (void *)&NativeSetSpawnLocation, - (void *)&NativeDropItem); + (void *)&NativeDropItem, + (void *)&NativeGetSignLine, + (void *)&NativeSetSignLine); s_managedSetPlayerCallbacks( (void *)&NativeKickPlayer,