feat: add FourKit plugin host with dual server build
Adds the FourKit .NET 10 plugin host as a second dedicated server
build flavour alongside the existing vanilla server. Both flavours
build from the same source tree, with FourKit gated by the
MINECRAFT_SERVER_FOURKIT_BUILD preprocessor define.
Build layout:
Minecraft.Server vanilla, no plugin support, no .NET dep
Minecraft.Server.FourKit FourKit-enabled, ships with bundled
.NET 10 self-contained runtime in runtime/
and an empty plugins/ folder
Both produce a Minecraft.Server.exe in their own per-target output
dir. The variant identity lives in the directory name, not the
binary name, so either flavour can be shipped as a drop-in.
Native bridge (Minecraft.Server/FourKit*.{cpp,h}):
* FourKitRuntime: hosts CoreCLR via hostfxr's command-line init API
(the runtime-config API does not support self-contained components)
* FourKitBridge: ~50 Fire* event entry points, with inline no-op
stubs for the standalone build so gameplay code can call them
unconditionally
* FourKitNatives: ~80 native callbacks the managed side invokes
for player/world/inventory mutations
* FourKitMappers: type and enum mapping helpers
Managed plugin host (Minecraft.Server.FourKit/):
* Bukkit-style API: Player, World, Block, Inventory, Command,
Listener, EventHandler attribute, ~54 event classes
* PluginLoader with per-plugin AssemblyLoadContext
* FourKitHost as the [UnmanagedCallersOnly] entry point table
* Runtime resolves plugins relative to the host process so they
always live next to Minecraft.Server.exe regardless of where the
managed assembly itself is loaded from
Engine hooks (Minecraft.Client/, Minecraft.World/):
* Player lifecycle (PreLogin, Login, Join, Quit, Kick, Move,
Teleport, Portal, Death) wired into PendingConnection and
PlayerConnection without disturbing the cipher handshake or
identity-token security flow
* Inventory open/click/drop hooks across every container menu type
* Block place/break/grow/burn/spread/from-to hooks across the
full tile family
* Bed enter/leave, sign change, entity damage/death, ender pearl
teleport hooks
Regression fixes preserved while applying donor diffs:
* ServerPlayer::die() retains the LCE-Revelations hardcore branch
(setGameMode(ADVENTURE) + banPlayerForHardcoreDeath) in both the
FourKit and non-FourKit code paths
* ServerLevel::entityAdded() retains the sub-entity ID reassignment
loop required by the client's handleAddMob offset, fixing Ender
Dragon and Wither boss multi-part hit detection
* LivingEntity::travel() retains the raw Player* cast and the
cached frictionTile, both Revelations perf wins that the donor
silently reverted
* ServerLogger.cpp keeps the file-logging code donor stripped
* PlayerList.cpp end portal transition fix and UIScene_EndPoem
bounds-check are intact
Build system:
* Top-level CMakeLists.txt adds the Minecraft.Server.FourKit
subdirectory and pulls in the new shared cmake/ServerTarget.cmake
helper
* Minecraft.Server/cmake/sources/Common.cmake is now location
independent (uses CMAKE_CURRENT_LIST_DIR) so the source list
can be consumed from either server target's CMakeLists.txt
* The seven FourKit*.cpp/h files live in their own
_MINECRAFT_SERVER_COMMON_SERVER_FOURKIT variable so the
standalone target omits them
* configure-time .NET 10 SDK check fails fast with a clear
download link if the SDK is missing
* global.json pins the SDK to 10.0.100 with latestFeature
rollforward
Sample plugin (samples/HelloPlugin/) demonstrates the loader and
the PlayerJoinEvent listener pattern.
CI:
* nightly.yml builds both server flavours, ships
LCE-Revelations-Server-Win64.zip and
LCE-Revelations-Server-Win64-FourKit.zip, attests both, and
updates release notes for the dual-flavour layout
* pull-request.yml pulls in actions/setup-dotnet so the FourKit
publish step works in PR validation
* All zip artifacts and the client zip are renamed from
LCREWindows64 to LCE-Revelations-{Client,Server}-Win64
Documentation:
* COMPILE.md gets a VS 2022 quick start, .NET 10 prereq section,
server flavours explanation, and a troubleshooting section
* docs/FOURKIT_PORT_RECON.md captures the file-by-file recon that
drove the port
* docs/FOURKIT_PARITY.md is the canonical reference for which
events FourKit fires
Docker:
* docker-compose.dedicated-server.yml MC_RUNTIME_DIR default points
at the vanilla CMake output. The FourKit Docker image is
intentionally NOT shipped yet because hosting .NET 10 self
contained inside Wine has not been smoke-tested
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="Entity"/> that can take damage and has health.
|
||||
/// </summary>
|
||||
public class Damageable : Entity
|
||||
{
|
||||
private double _health = 20.0;
|
||||
private double _maxHealth = 20.0;
|
||||
private readonly double _originalMaxHealth = 20.0;
|
||||
|
||||
/// <summary>
|
||||
/// Deals the given amount of damage to this entity.
|
||||
/// This calls into the native server to apply real damage.
|
||||
/// </summary>
|
||||
/// <param name="amount">Amount of damage to deal.</param>
|
||||
public void damage(double amount)
|
||||
{
|
||||
NativeBridge.DamagePlayer?.Invoke(getEntityId(), (float)amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity's health from 0 to <see cref="getMaxHealth"/>, where 0 is dead.
|
||||
/// </summary>
|
||||
/// <returns>The current health.</returns>
|
||||
public double getHealth() => _health;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum health this entity has.
|
||||
/// </summary>
|
||||
/// <returns>The maximum health.</returns>
|
||||
public double getMaxHealth() => _maxHealth;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the max health to the original amount.
|
||||
/// </summary>
|
||||
public void resetMaxHealth()
|
||||
{
|
||||
_maxHealth = _originalMaxHealth;
|
||||
if (_health > _maxHealth)
|
||||
_health = _maxHealth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the entity's health from 0 to <see cref="getMaxHealth"/>, where 0 is dead.
|
||||
/// This calls into the native server to apply the health change.
|
||||
/// </summary>
|
||||
/// <param name="health">New health value.</param>
|
||||
public void setHealth(double health)
|
||||
{
|
||||
NativeBridge.SetPlayerHealth?.Invoke(getEntityId(), (float)Math.Clamp(health, 0.0, _maxHealth));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum health this entity can have.
|
||||
/// If the entity's current health exceeds the new maximum, it is clamped.
|
||||
/// </summary>
|
||||
/// <param name="health">New maximum health value.</param>
|
||||
public void setMaxHealth(double health)
|
||||
{
|
||||
_maxHealth = health;
|
||||
if (_health > _maxHealth)
|
||||
_health = _maxHealth;
|
||||
}
|
||||
|
||||
// --- Internal setter used by the bridge ---
|
||||
|
||||
/// <summary>
|
||||
/// Updates health directly. Called internally by the bridge.
|
||||
/// </summary>
|
||||
/// <param name="health">The new health value.</param>
|
||||
internal void SetHealthInternal(double health) => _health = health;
|
||||
|
||||
/// <summary>
|
||||
/// Updates max health directly. Called internally by the bridge.
|
||||
/// </summary>
|
||||
/// <param name="maxHealth">The new max health value.</param>
|
||||
internal void SetMaxHealthInternal(double maxHealth) => _maxHealth = maxHealth;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
// eh
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing the reason a player was disconnected from the server.
|
||||
/// mirrored from <c>DisconnectPacket::eDisconnectReason</c>.
|
||||
/// </summary>
|
||||
public enum DisconnectReason
|
||||
{
|
||||
/// <summary>No specific reason.</summary>
|
||||
NONE = 0,
|
||||
/// <summary>The player quit voluntarily.</summary>
|
||||
QUITTING = 1,
|
||||
/// <summary>The connection was closed.</summary>
|
||||
CLOSED = 2,
|
||||
/// <summary>The login took too long.</summary>
|
||||
LOGIN_TOO_LONG = 3,
|
||||
/// <summary>The player had an illegal stance.</summary>
|
||||
ILLEGAL_STANCE = 4,
|
||||
/// <summary>The player had an illegal position.</summary>
|
||||
ILLEGAL_POSITION = 5,
|
||||
/// <summary>The player moved too quickly.</summary>
|
||||
MOVED_TOO_QUICKLY = 6,
|
||||
/// <summary>The player was flying when not allowed.</summary>
|
||||
NO_FLYING = 7,
|
||||
/// <summary>The player was kicked by an operator or plugin.</summary>
|
||||
KICKED = 8,
|
||||
/// <summary>The connection timed out.</summary>
|
||||
TIME_OUT = 9,
|
||||
/// <summary>Packet overflow.</summary>
|
||||
OVERFLOW = 10,
|
||||
/// <summary>End of stream reached unexpectedly.</summary>
|
||||
END_OF_STREAM = 11,
|
||||
/// <summary>The server is full.</summary>
|
||||
SERVER_FULL = 12,
|
||||
/// <summary>The server is outdated.</summary>
|
||||
OUTDATED_SERVER = 13,
|
||||
/// <summary>The client is outdated.</summary>
|
||||
OUTDATED_CLIENT = 14,
|
||||
/// <summary>An unexpected packet was received.</summary>
|
||||
UNEXPECTED_PACKET = 15,
|
||||
/// <summary>Connection creation failed.</summary>
|
||||
CONNECTION_CREATION_FAILED = 16,
|
||||
/// <summary>The host does not have multiplayer privileges.</summary>
|
||||
NO_MULTIPLAYER_PRIVILEGES_HOST = 17,
|
||||
/// <summary>The joining player does not have multiplayer privileges.</summary>
|
||||
NO_MULTIPLAYER_PRIVILEGES_JOIN = 18,
|
||||
/// <summary>All local players lack UGC permissions.</summary>
|
||||
NO_UGC_ALL_LOCAL = 19,
|
||||
/// <summary>A single local player lacks UGC permissions.</summary>
|
||||
NO_UGC_SINGLE_LOCAL = 20,
|
||||
/// <summary>All local players have content restrictions.</summary>
|
||||
CONTENT_RESTRICTED_ALL_LOCAL = 21,
|
||||
/// <summary>A single local player has content restrictions.</summary>
|
||||
CONTENT_RESTRICTED_SINGLE_LOCAL = 22,
|
||||
/// <summary>A remote player lacks UGC permissions.</summary>
|
||||
NO_UGC_REMOTE = 23,
|
||||
/// <summary>No friends in the game.</summary>
|
||||
NO_FRIENDS_IN_GAME = 24,
|
||||
/// <summary>The player was banned.</summary>
|
||||
BANNED = 25,
|
||||
/// <summary>The player is not friends with the host.</summary>
|
||||
NOT_FRIENDS_WITH_HOST = 26,
|
||||
/// <summary>NAT type mismatch.</summary>
|
||||
NAT_MISMATCH = 27,
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
using Minecraft.Server.FourKit.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a base entity in the world
|
||||
/// </summary>
|
||||
public class Entity
|
||||
{
|
||||
private Location _location = new();
|
||||
private Guid _uniqueId = Guid.NewGuid();
|
||||
private float _fallDistance;
|
||||
private int _dimensionId;
|
||||
private int _entityId;
|
||||
private EntityType _entityType = EntityType.UNKNOWN;
|
||||
private bool _onGround;
|
||||
private double _velocityX, _velocityY, _velocityZ;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity's current position.
|
||||
/// </summary>
|
||||
/// <returns>a new copy of <see cref="Location"/> containing the position of this entity</returns>
|
||||
public Location getLocation() => _location;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a unique id for this entity
|
||||
/// </summary>
|
||||
/// <returns>Entity id</returns>
|
||||
public virtual int getEntityId() => _entityId;
|
||||
|
||||
/// <summary>
|
||||
/// Get the type of the entity.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="EntityType"/> of this entity.</returns>
|
||||
public new virtual EntityType getType() => _entityType;
|
||||
public new virtual EntityType GetType() => _entityType;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a unique and persistent id for this entity. Note that this is not the standard UUID for players.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Guid"/> unique to this entity.</returns>
|
||||
public Guid getUniqueId() => _uniqueId;
|
||||
|
||||
/// <summary>
|
||||
/// Teleports this entity to the given location.
|
||||
/// This calls into the native server to perform the actual teleport.
|
||||
/// </summary>
|
||||
/// <param name="location">The destination location.</param>
|
||||
/// <returns><c>true</c> if the teleport was successful.</returns>
|
||||
public virtual bool teleport(Location location)
|
||||
{
|
||||
int targetDimId = location.LocationWorld?.getDimensionId() ?? _dimensionId;
|
||||
NativeBridge.TeleportEntity?.Invoke(getEntityId(), targetDimId, location.getX(), location.getY(), location.getZ());
|
||||
SetLocation(location);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the fall distance for this entity.
|
||||
/// </summary>
|
||||
/// <param name="distance">The fall distance value.</param>
|
||||
public void setFallDistance(float distance)
|
||||
{
|
||||
_fallDistance = distance;
|
||||
NativeBridge.SetFallDistance?.Invoke(getEntityId(), distance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the distance this entity has fallen.
|
||||
/// </summary>
|
||||
/// <returns>The current fall distance.</returns>
|
||||
public float getFallDistance() => _fallDistance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current world this entity resides in.
|
||||
/// </summary>
|
||||
/// <returns>World containing this entity.</returns>
|
||||
public World getWorld() => FourKit.getWorld(_dimensionId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the entity is supported by a block. This value is a
|
||||
/// state updated by the server and is not recalculated unless the entity moves.
|
||||
/// </summary>
|
||||
/// <returns>True if entity is on ground.</returns>
|
||||
public bool isOnGround() => _onGround;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this entity's current velocity.
|
||||
/// </summary>
|
||||
/// <returns>Current travelling velocity of this entity.</returns>
|
||||
public Vector getVelocity() => new Vector(_velocityX, _velocityY, _velocityZ);
|
||||
|
||||
/// <summary>
|
||||
/// Sets this entity's velocity.
|
||||
/// </summary>
|
||||
/// <param name="velocity">New velocity to travel with.</param>
|
||||
public void setVelocity(Vector velocity)
|
||||
{
|
||||
_velocityX = velocity.getX();
|
||||
_velocityY = velocity.getY();
|
||||
_velocityZ = velocity.getZ();
|
||||
NativeBridge.SetVelocity?.Invoke(getEntityId(), velocity.getX(), velocity.getY(), velocity.getZ());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether this entity is inside a vehicle.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the entity is in a vehicle.</returns>
|
||||
public bool isInsideVehicle()
|
||||
{
|
||||
return (NativeBridge.GetVehicleId?.Invoke(getEntityId()) ?? -1) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leave the current vehicle. If the entity is currently in a vehicle
|
||||
/// (and is removed from it), <c>true</c> will be returned, otherwise
|
||||
/// <c>false</c> will be returned.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the entity was in a vehicle.</returns>
|
||||
public bool leaveVehicle()
|
||||
{
|
||||
return NativeBridge.LeaveVehicle?.Invoke(getEntityId()) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the vehicle that this entity is inside. If there is no vehicle,
|
||||
/// <c>null</c> will be returned.
|
||||
/// </summary>
|
||||
/// <returns>The current vehicle, or <c>null</c>.</returns>
|
||||
public Entity? getVehicle()
|
||||
{
|
||||
int vehicleId = NativeBridge.GetVehicleId?.Invoke(getEntityId()) ?? -1;
|
||||
if (vehicleId < 0) return null;
|
||||
return FourKit.GetEntityByEntityId(vehicleId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eject any passenger.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if there was a passenger.</returns>
|
||||
public bool eject()
|
||||
{
|
||||
return NativeBridge.Eject?.Invoke(getEntityId()) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary passenger of a vehicle. For vehicles that could
|
||||
/// have multiple passengers, this will only return the primary passenger.
|
||||
/// </summary>
|
||||
/// <returns>The passenger entity, or <c>null</c>.</returns>
|
||||
public Entity? getPassenger()
|
||||
{
|
||||
int passengerId = NativeBridge.GetPassengerId?.Invoke(getEntityId()) ?? -1;
|
||||
if (passengerId < 0) return null;
|
||||
return FourKit.GetEntityByEntityId(passengerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the passenger of a vehicle.
|
||||
/// </summary>
|
||||
/// <param name="passenger">The new passenger.</param>
|
||||
/// <returns><c>false</c> if it could not be done for whatever reason.</returns>
|
||||
public bool setPassenger(Entity passenger)
|
||||
{
|
||||
if (passenger == null || NativeBridge.SetPassenger == null) return false;
|
||||
return NativeBridge.SetPassenger(getEntityId(), passenger.getEntityId()) != 0;
|
||||
}
|
||||
|
||||
// INTERNAL
|
||||
internal void SetLocation(Location location)
|
||||
{
|
||||
_location = location;
|
||||
}
|
||||
|
||||
internal void SetFallDistanceInternal(float distance) => _fallDistance = distance;
|
||||
|
||||
internal void SetUniqueId(Guid id)
|
||||
{
|
||||
_uniqueId = id;
|
||||
}
|
||||
|
||||
internal void SetDimensionInternal(int dimensionId) => _dimensionId = dimensionId;
|
||||
internal void SetEntityIdInternal(int entityId) => _entityId = entityId;
|
||||
internal void SetEntityTypeInternal(EntityType entityType) => _entityType = entityType;
|
||||
internal void SetOnGroundInternal(bool onGround) => _onGround = onGround;
|
||||
internal void SetVelocityInternal(double x, double y, double z)
|
||||
{
|
||||
_velocityX = x;
|
||||
_velocityY = y;
|
||||
_velocityZ = z;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type of an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
public enum EntityType
|
||||
{
|
||||
/// <summary>An arrow projectile; may get stuck in the ground.</summary>
|
||||
ARROW,
|
||||
/// <summary>A bat.</summary>
|
||||
BAT,
|
||||
/// <summary>A blaze.</summary>
|
||||
BLAZE,
|
||||
/// <summary>A placed boat.</summary>
|
||||
BOAT,
|
||||
/// <summary>A cave spider.</summary>
|
||||
CAVE_SPIDER,
|
||||
/// <summary>A chicken.</summary>
|
||||
CHICKEN,
|
||||
/// <summary>A complex entity part.</summary>
|
||||
COMPLEX_PART,
|
||||
/// <summary>A cow.</summary>
|
||||
COW,
|
||||
/// <summary>A creeper.</summary>
|
||||
CREEPER,
|
||||
/// <summary>An item resting on the ground.</summary>
|
||||
DROPPED_ITEM,
|
||||
/// <summary>A flying chicken egg.</summary>
|
||||
EGG,
|
||||
/// <summary>An ender crystal.</summary>
|
||||
ENDER_CRYSTAL,
|
||||
/// <summary>An ender dragon.</summary>
|
||||
ENDER_DRAGON,
|
||||
/// <summary>A flying ender pearl.</summary>
|
||||
ENDER_PEARL,
|
||||
/// <summary>An ender eye signal.</summary>
|
||||
ENDER_SIGNAL,
|
||||
/// <summary>An enderman.</summary>
|
||||
ENDERMAN,
|
||||
/// <summary>An experience orb.</summary>
|
||||
EXPERIENCE_ORB,
|
||||
/// <summary>A block that is going to or is about to fall.</summary>
|
||||
FALLING_BLOCK,
|
||||
/// <summary>A flying large fireball, as thrown by a Ghast for example.</summary>
|
||||
FIREBALL,
|
||||
/// <summary>A firework rocket.</summary>
|
||||
FIREWORK,
|
||||
/// <summary>A fishing line and bobber.</summary>
|
||||
FISHING_HOOK,
|
||||
/// <summary>A ghast.</summary>
|
||||
GHAST,
|
||||
/// <summary>A giant.</summary>
|
||||
GIANT,
|
||||
/// <summary>A horse.</summary>
|
||||
HORSE,
|
||||
/// <summary>An iron golem.</summary>
|
||||
IRON_GOLEM,
|
||||
/// <summary>An item frame on a wall.</summary>
|
||||
ITEM_FRAME,
|
||||
/// <summary>A leash attached to a fencepost.</summary>
|
||||
LEASH_HITCH,
|
||||
/// <summary>A bolt of lightning.</summary>
|
||||
LIGHTNING,
|
||||
/// <summary>A magma cube.</summary>
|
||||
MAGMA_CUBE,
|
||||
/// <summary>A minecart.</summary>
|
||||
MINECART,
|
||||
/// <summary>A minecart with a chest.</summary>
|
||||
MINECART_CHEST,
|
||||
/// <summary>A minecart with a command block.</summary>
|
||||
MINECART_COMMAND,
|
||||
/// <summary>A minecart with a furnace.</summary>
|
||||
MINECART_FURNACE,
|
||||
/// <summary>A minecart with a hopper.</summary>
|
||||
MINECART_HOPPER,
|
||||
/// <summary>A minecart with a mob spawner.</summary>
|
||||
MINECART_MOB_SPAWNER,
|
||||
/// <summary>A minecart with TNT.</summary>
|
||||
MINECART_TNT,
|
||||
/// <summary>A mooshroom.</summary>
|
||||
MUSHROOM_COW,
|
||||
/// <summary>An ocelot.</summary>
|
||||
OCELOT,
|
||||
/// <summary>A painting on a wall.</summary>
|
||||
PAINTING,
|
||||
/// <summary>A pig.</summary>
|
||||
PIG,
|
||||
/// <summary>A zombie pigman.</summary>
|
||||
PIG_ZOMBIE,
|
||||
/// <summary>A player.</summary>
|
||||
PLAYER,
|
||||
/// <summary>Primed TNT that is about to explode.</summary>
|
||||
PRIMED_TNT,
|
||||
/// <summary>A sheep.</summary>
|
||||
SHEEP,
|
||||
/// <summary>A silverfish.</summary>
|
||||
SILVERFISH,
|
||||
/// <summary>A skeleton.</summary>
|
||||
SKELETON,
|
||||
/// <summary>A slime.</summary>
|
||||
SLIME,
|
||||
/// <summary>A flying small fireball, such as thrown by a Blaze or player.</summary>
|
||||
SMALL_FIREBALL,
|
||||
/// <summary>A flying snowball.</summary>
|
||||
SNOWBALL,
|
||||
/// <summary>A snowman.</summary>
|
||||
SNOWMAN,
|
||||
/// <summary>A spider.</summary>
|
||||
SPIDER,
|
||||
/// <summary>A flying splash potion.</summary>
|
||||
SPLASH_POTION,
|
||||
/// <summary>A squid.</summary>
|
||||
SQUID,
|
||||
/// <summary>A flying experience bottle.</summary>
|
||||
THROWN_EXP_BOTTLE,
|
||||
/// <summary>An unknown entity without an Entity Class.</summary>
|
||||
UNKNOWN,
|
||||
/// <summary>A villager.</summary>
|
||||
VILLAGER,
|
||||
/// <summary>A weather entity.</summary>
|
||||
WEATHER,
|
||||
/// <summary>A witch.</summary>
|
||||
WITCH,
|
||||
/// <summary>A wither.</summary>
|
||||
WITHER,
|
||||
/// <summary>A flying wither skull projectile.</summary>
|
||||
WITHER_SKULL,
|
||||
/// <summary>A wolf.</summary>
|
||||
WOLF,
|
||||
/// <summary>A zombie.</summary>
|
||||
ZOMBIE,
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a human entity in the world (e.g. a player).
|
||||
/// </summary>
|
||||
public abstract class HumanEntity : LivingEntity, InventoryHolder
|
||||
{
|
||||
private GameMode _gameMode = GameMode.SURVIVAL;
|
||||
private string _name = string.Empty;
|
||||
internal PlayerInventory _playerInventory = new();
|
||||
internal Inventory _enderChestInventory = new("Ender Chest", InventoryType.ENDER_CHEST, 27);
|
||||
private ItemStack? _cursorItem;
|
||||
private bool _sleeping;
|
||||
private int _sleepTicks;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this human's current <see cref="GameMode"/>.
|
||||
/// </summary>
|
||||
/// <returns>The current game mode.</returns>
|
||||
public GameMode getGameMode() => _gameMode;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of this player.
|
||||
/// </summary>
|
||||
/// <returns>The display name.</returns>
|
||||
public string getName() => _name;
|
||||
|
||||
/// <summary>
|
||||
/// Sets this human's current <see cref="GameMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="mode">The new game mode.</param>
|
||||
public void setGameMode(GameMode mode)
|
||||
{
|
||||
NativeBridge.SetPlayerGameMode?.Invoke(getEntityId(), (int)mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's inventory.
|
||||
/// </summary>
|
||||
/// <returns>The inventory of the player, this also contains the armor slots.</returns>
|
||||
Inventory InventoryHolder.getInventory() => getInventory();
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's inventory.
|
||||
/// This also contains the armor slots.
|
||||
/// </summary>
|
||||
/// <returns>The player's inventory.</returns>
|
||||
public PlayerInventory getInventory()
|
||||
{
|
||||
return _playerInventory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's EnderChest inventory.
|
||||
/// </summary>
|
||||
/// <returns>The EnderChest of the player.</returns>
|
||||
public Inventory getEnderChest()
|
||||
{
|
||||
return _enderChestInventory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ItemStack currently in your hand, can be empty.
|
||||
/// </summary>
|
||||
/// <returns>The ItemStack of the item you are currently holding.</returns>
|
||||
public ItemStack? getItemInHand()
|
||||
{
|
||||
return _playerInventory.getItemInHand();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item to the given ItemStack, this will replace whatever the
|
||||
/// user was holding.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack which will end up in the hand.</param>
|
||||
public void setItemInHand(ItemStack? item)
|
||||
{
|
||||
_playerInventory.setItemInHand(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ItemStack currently on your cursor, can be empty.
|
||||
/// Will always be empty if the player currently has no open window.
|
||||
/// </summary>
|
||||
/// <returns>The ItemStack of the item you are currently moving around.</returns>
|
||||
public ItemStack? getItemOnCursor() => _cursorItem;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item to the given ItemStack, this will replace whatever the
|
||||
/// user was moving. Will always be empty if the player currently has no open window.
|
||||
/// </summary>
|
||||
/// <param name="item">The ItemStack which will end up in the hand.</param>
|
||||
public void setItemOnCursor(ItemStack? item) => _cursorItem = item;
|
||||
|
||||
/// <summary>
|
||||
/// If the player currently has an inventory window open, this method will
|
||||
/// close it on both the server and client side.
|
||||
/// </summary>
|
||||
public void closeInventory()
|
||||
{
|
||||
NativeBridge.CloseContainer?.Invoke(getEntityId());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens an inventory window with the specified inventory on the top.
|
||||
/// </summary>
|
||||
/// <param name="inventory">The inventory to open.</param>
|
||||
/// <returns>The newly opened InventoryView, or null if it could not be opened.</returns>
|
||||
public InventoryView? openInventory(Inventory inventory)
|
||||
{
|
||||
if (NativeBridge.OpenVirtualContainer == null)
|
||||
return null;
|
||||
|
||||
closeInventory();
|
||||
|
||||
int nativeType = inventory.getType() switch
|
||||
{
|
||||
InventoryType.CHEST => 0,
|
||||
InventoryType.DISPENSER => 3,
|
||||
InventoryType.DROPPER => 10,
|
||||
InventoryType.HOPPER => 5,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
int size = inventory.getSize();
|
||||
int[] buf = new int[size * 3];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
var item = inventory._items[i];
|
||||
buf[i * 3] = item?.getTypeId() ?? 0;
|
||||
buf[i * 3 + 1] = item?.getAmount() ?? 0;
|
||||
buf[i * 3 + 2] = item?.getDurability() ?? 0;
|
||||
}
|
||||
|
||||
string title = inventory.getName();
|
||||
int titleByteLen = System.Text.Encoding.UTF8.GetByteCount(title);
|
||||
IntPtr titlePtr = Marshal.StringToCoTaskMemUTF8(title);
|
||||
var gh = GCHandle.Alloc(buf, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
NativeBridge.OpenVirtualContainer(getEntityId(), nativeType, titlePtr, titleByteLen, size, gh.AddrOfPinnedObject());
|
||||
}
|
||||
finally
|
||||
{
|
||||
gh.Free();
|
||||
Marshal.FreeCoTaskMem(titlePtr);
|
||||
}
|
||||
|
||||
var view = new InventoryView(inventory, getInventory(), this, inventory.getType());
|
||||
return view;
|
||||
}
|
||||
|
||||
internal void SetGameModeInternal(GameMode mode) => _gameMode = mode;
|
||||
|
||||
internal void SetNameInternal(string name) => _name = name;
|
||||
|
||||
internal void SetSleepingInternal(bool sleeping) => _sleeping = sleeping;
|
||||
|
||||
internal void SetSleepTicksInternal(int ticks) => _sleepTicks = ticks;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether this player is slumbering.
|
||||
/// </summary>
|
||||
/// <returns>slumber state</returns>
|
||||
public bool isSleeping() => _sleeping;
|
||||
|
||||
/// <summary>
|
||||
/// Get the sleep ticks of the player. This value may be capped.
|
||||
/// </summary>
|
||||
/// <returns>slumber ticks</returns>
|
||||
public int getSleepTicks() => _sleepTicks;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
using Minecraft.Server.FourKit.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a dropped item on the ground.
|
||||
/// </summary>
|
||||
public class Item : Entity
|
||||
{
|
||||
private ItemStack _itemStack;
|
||||
|
||||
internal Item(int entityId, int dimId, double x, double y, double z, ItemStack itemStack)
|
||||
{
|
||||
SetEntityIdInternal(entityId);
|
||||
SetEntityTypeInternal(EntityType.DROPPED_ITEM);
|
||||
SetDimensionInternal(dimId);
|
||||
SetLocation(new Location(FourKit.getWorld(dimId), x, y, z));
|
||||
_itemStack = itemStack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item stack associated with this item.
|
||||
/// </summary>
|
||||
/// <returns>An item stack.</returns>
|
||||
public ItemStack getItemStack() => _itemStack;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the item stack of this item.
|
||||
/// </summary>
|
||||
/// <param name="stack">The new item stack.</param>
|
||||
public void setItemStack(ItemStack stack) => _itemStack = stack;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a living entity in the world that has health and can take damage.
|
||||
/// </summary>
|
||||
public class LivingEntity : Damageable
|
||||
{
|
||||
private double _eyeHeight = 1.62;
|
||||
|
||||
internal LivingEntity() { }
|
||||
|
||||
internal LivingEntity(int entityId, EntityType entityType, int dimId, double x, double y, double z,
|
||||
float health = 20f, float maxHealth = 20f)
|
||||
{
|
||||
SetEntityIdInternal(entityId);
|
||||
SetEntityTypeInternal(entityType);
|
||||
SetDimensionInternal(dimId);
|
||||
SetLocation(new Location(FourKit.getWorld(dimId), x, y, z));
|
||||
if (maxHealth > 0)
|
||||
SetMaxHealthInternal(maxHealth);
|
||||
SetHealthInternal(health);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the living entity's eyes above its <see cref="Location"/>.
|
||||
/// </summary>
|
||||
/// <returns>The eye height.</returns>
|
||||
public double getEyeHeight() => _eyeHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the living entity's eyes above its <see cref="Location"/>.
|
||||
/// </summary>
|
||||
/// <param name="ignoreSneaking">If <c>true</c>, returns the standing eye height regardless of sneak state.</param>
|
||||
/// <returns>The eye height.</returns>
|
||||
public double getEyeHeight(bool ignoreSneaking)
|
||||
{
|
||||
if (ignoreSneaking)
|
||||
return _eyeHeight;
|
||||
|
||||
// When sneaking the eye height is slightly lower
|
||||
return _eyeHeight - 0.08;
|
||||
}
|
||||
|
||||
// --- Internal setter used by the bridge ---
|
||||
|
||||
/// <summary>
|
||||
/// Updates the eye height. Called internally by the bridge.
|
||||
/// </summary>
|
||||
/// <param name="eyeHeight">The new eye height.</param>
|
||||
internal void SetEyeHeightInternal(double eyeHeight) => _eyeHeight = eyeHeight;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a player identity that may or may not currently be online.
|
||||
/// </summary>
|
||||
public interface OfflinePlayer
|
||||
{
|
||||
/// <summary>Returns the name of this player.</summary>
|
||||
/// <returns>The player's name.</returns>
|
||||
string getName();
|
||||
|
||||
/// <summary>Gets a Player object that this represents, if there is one.</summary>
|
||||
/// <returns>A <see cref="Player"/> instance if the player is online; otherwise <c>null</c>.</returns>
|
||||
Player? getPlayer();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UUID that uniquely identifies this player across sessions.
|
||||
/// This is the player-specific UUID, not the entity UUID.
|
||||
/// </summary>
|
||||
/// <returns>The player's unique identifier.</returns>
|
||||
Guid getUniqueId();
|
||||
|
||||
/// <summary>Checks if this player is currently online.</summary>
|
||||
/// <returns><c>true</c> if the player is online; otherwise <c>false</c>.</returns>
|
||||
bool isOnline();
|
||||
}
|
||||
@@ -0,0 +1,665 @@
|
||||
namespace Minecraft.Server.FourKit.Entity;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Minecraft.Server.FourKit.Command;
|
||||
using Minecraft.Server.FourKit.Experimental;
|
||||
using Minecraft.Server.FourKit.Inventory;
|
||||
using Minecraft.Server.FourKit.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a player connected to the server.
|
||||
/// </summary>
|
||||
public class Player : HumanEntity, OfflinePlayer, CommandSender
|
||||
{
|
||||
private float _saturation = 5.0f;
|
||||
private float _walkSpeed = 0.2f;
|
||||
private float _exhaustion;
|
||||
private int _foodLevel = 20;
|
||||
private int _level;
|
||||
private float _exp;
|
||||
private int _totalExperience;
|
||||
private Guid _playerUniqueId;
|
||||
private ulong _playerRawOnlineXUID;
|
||||
private ulong _playerRawOfflineXUID;
|
||||
private string? _displayName;
|
||||
private bool _sneaking;
|
||||
private bool _sprinting;
|
||||
private bool _allowFlight;
|
||||
private bool _sleepingIgnored;
|
||||
|
||||
private PlayerConnection _connection;
|
||||
|
||||
internal bool IsOnline { get; set; }
|
||||
|
||||
internal Player(int entityId, string name)
|
||||
{
|
||||
SetEntityIdInternal(entityId);
|
||||
SetEntityTypeInternal(EntityType.PLAYER);
|
||||
SetNameInternal(name);
|
||||
IsOnline = true;
|
||||
_playerInventory._holder = this;
|
||||
_connection = new PlayerConnection(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override EntityType getType() => EntityType.PLAYER;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override EntityType GetType() => EntityType.PLAYER;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool teleport(Location location)
|
||||
{
|
||||
int targetDimId = location.LocationWorld?.getDimensionId() ?? getLocation().LocationWorld?.getDimensionId() ?? 0;
|
||||
NativeBridge.TeleportEntity?.Invoke(getEntityId(), targetDimId, location.X, location.Y, location.Z);
|
||||
SetLocation(location);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <b>Experimental.</b> Gets the player's <see cref="PlayerConnection"/>, which can be used
|
||||
/// to send raw packet data directly to the client.
|
||||
/// </summary>
|
||||
/// <returns>The player's connection.</returns>
|
||||
public PlayerConnection getConnection() => _connection;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Player? getPlayer() => IsOnline ? this : null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the "friendly" name to display of this player.
|
||||
/// This may include color. If no custom display name has been set,
|
||||
/// this returns the player's <see cref="HumanEntity.getName"/>.
|
||||
/// </summary>
|
||||
/// <returns>The display name.</returns>
|
||||
public string getDisplayName() => _displayName ?? getName();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the "friendly" name to display of this player.
|
||||
/// </summary>
|
||||
/// <param name="name">The display name, or <c>null</c> to reset to <see cref="HumanEntity.getName"/>.</param>
|
||||
public void setDisplayName(string? name)
|
||||
{
|
||||
_displayName = name;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool isOnline() => IsOnline;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UUID that uniquely identifies this player across sessions.
|
||||
/// This is the player-specific UUID, not the entity UUID.
|
||||
/// </summary>
|
||||
/// <returns>The player's unique identifier.</returns>
|
||||
public new Guid getUniqueId() => _playerUniqueId;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <b>Experimental.</b> Gets the raw online XUID (Xbox User ID) for this player.
|
||||
/// The online XUID is used for guests.
|
||||
/// </summary>
|
||||
/// <returns>The raw online XUID value.</returns>
|
||||
public ulong getRawOnlineXUID() => _playerRawOnlineXUID;
|
||||
|
||||
/// <summary>
|
||||
/// <b>Experimental.</b> Gets the raw offline XUID (Xbox User ID) for this player.
|
||||
/// The offline XUID is the main XUID used by the client.
|
||||
/// </summary>
|
||||
/// <returns>The raw offline XUID value.</returns>
|
||||
public ulong getRawOfflineXUID() => _playerRawOfflineXUID;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player's estimated ping in milliseconds.
|
||||
/// This value represents a weighted average of the response time to application layer ping packets sent. This value does not represent the network round trip time and as such may have less granularity and be impacted by other sources. For these reasons it should not be used for anti-cheat purposes. Its recommended use is only as a qualitative indicator of connection quality.
|
||||
/// </summary>
|
||||
/// <returns>The player's estimated ping in milliseconds.</returns>
|
||||
public int getPing()
|
||||
{
|
||||
if (NativeBridge.GetPlayerLatency == null)
|
||||
return -1;
|
||||
|
||||
return NativeBridge.GetPlayerLatency(getEntityId());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player's current saturation level.
|
||||
/// Saturation acts as a buffer before hunger begins to deplete.
|
||||
/// </summary>
|
||||
/// <returns>The current saturation level.</returns>
|
||||
public float getSaturation() => _saturation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current allowed speed that a client can walk.
|
||||
/// The default value is 0.2.
|
||||
/// </summary>
|
||||
/// <returns>The current walk speed.</returns>
|
||||
public float getWalkSpeed() => _walkSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the speed at which a client will walk.
|
||||
/// This calls into the native server to apply the change.
|
||||
/// </summary>
|
||||
/// <param name="value">The new walk speed.</param>
|
||||
public void setWalkSpeed(float value)
|
||||
{
|
||||
_walkSpeed = value;
|
||||
NativeBridge.SetWalkSpeed?.Invoke(getEntityId(), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the player is in sneak mode.
|
||||
/// </summary>
|
||||
/// <returns>True if player is in sneak mode.</returns>
|
||||
public bool isSneaking() => _sneaking;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the player is sprinting or not.
|
||||
/// </summary>
|
||||
/// <returns>True if player is sprinting.</returns>
|
||||
public bool isSprinting() => _sprinting;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the player is ignored as not sleeping. If everyone is
|
||||
/// either sleeping or has this flag set, then time will advance to the
|
||||
/// next day. If everyone has this flag set but no one is actually in
|
||||
/// bed, then nothing will happen.
|
||||
/// </summary>
|
||||
/// <param name="isSleeping">Whether to ignore.</param>
|
||||
public void setSleepingIgnored(bool isSleeping)
|
||||
{
|
||||
_sleepingIgnored = isSleeping;
|
||||
NativeBridge.SetSleepingIgnored?.Invoke(getEntityId(), isSleeping ? 1 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the player is sleeping ignored.
|
||||
/// </summary>
|
||||
/// <returns>Whether player is ignoring sleep.</returns>
|
||||
public bool isSleepingIgnored() => _sleepingIgnored;
|
||||
|
||||
/// <summary>
|
||||
/// Play a sound for a player at the location.
|
||||
/// This function will fail silently if Location or Sound are null.
|
||||
/// </summary>
|
||||
/// <param name="location">The location to play the sound.</param>
|
||||
/// <param name="sound">The sound to play.</param>
|
||||
/// <param name="volume">The volume of the sound.</param>
|
||||
/// <param name="pitch">The pitch of the sound.</param>
|
||||
public void playSound(Location location, Sound sound, float volume, float pitch)
|
||||
{
|
||||
if (location == null)
|
||||
return;
|
||||
NativeBridge.PlaySound?.Invoke(getEntityId(), (int)sound, location.X, location.Y, location.Z, volume, pitch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the Player is allowed to fly via jump key double-tap
|
||||
/// like in creative mode.
|
||||
/// </summary>
|
||||
/// <returns>True if the player is allowed to fly.</returns>
|
||||
public bool getAllowFlight() => _allowFlight;
|
||||
|
||||
/// <summary>
|
||||
/// Sets if the Player is allowed to fly via jump key double-tap like
|
||||
/// in creative mode.
|
||||
/// </summary>
|
||||
/// <param name="flight">If flight should be allowed.</param>
|
||||
public void setAllowFlight(bool flight)
|
||||
{
|
||||
_allowFlight = flight;
|
||||
NativeBridge.SetAllowFlight?.Invoke(getEntityId(), flight ? 1 : 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void sendMessage(string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message) || NativeBridge.SendMessage == null)
|
||||
return;
|
||||
if (message.Length > FourKit.MAX_CHAT_LENGTH)
|
||||
message = message[..FourKit.MAX_CHAT_LENGTH];
|
||||
|
||||
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(message);
|
||||
try
|
||||
{
|
||||
NativeBridge.SendMessage(getEntityId(), ptr, System.Text.Encoding.UTF8.GetByteCount(message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void sendMessage(string[] messages)
|
||||
{
|
||||
foreach (var msg in messages)
|
||||
sendMessage(msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks player with the default <see cref="DisconnectReason.KICKED"/> reason.
|
||||
/// </summary>
|
||||
public void kickPlayer()
|
||||
{
|
||||
NativeBridge.KickPlayer?.Invoke(getEntityId(), (int)DisconnectReason.KICKED);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bans the player by UID with the specified reason and disconnects them.
|
||||
/// </summary>
|
||||
/// <param name="reason">The ban reason.</param>
|
||||
/// <returns><c>true</c> if the ban was applied successfully.</returns>
|
||||
public bool banPlayer(string reason)
|
||||
{
|
||||
if (NativeBridge.BanPlayer == null) return false;
|
||||
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(reason ?? string.Empty);
|
||||
try
|
||||
{
|
||||
int byteLen = System.Text.Encoding.UTF8.GetByteCount(reason ?? string.Empty);
|
||||
return NativeBridge.BanPlayer(getEntityId(), ptr, byteLen) != 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bans the player's IP address with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="reason">The ban reason.</param>
|
||||
/// <returns><c>true</c> if the IP ban was applied successfully.</returns>
|
||||
public bool banPlayerIp(string reason)
|
||||
{
|
||||
if (NativeBridge.BanPlayerIp == null) return false;
|
||||
IntPtr ptr = Marshal.StringToCoTaskMemUTF8(reason ?? string.Empty);
|
||||
try
|
||||
{
|
||||
int byteLen = System.Text.Encoding.UTF8.GetByteCount(reason ?? string.Empty);
|
||||
return NativeBridge.BanPlayerIp(getEntityId(), ptr, byteLen) != 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the socket address of this player.
|
||||
/// </summary>
|
||||
/// <returns>The player's socket address, or <c>null</c> if the address could not be determined.</returns>
|
||||
public InetSocketAddress? getAddress()
|
||||
{
|
||||
if (NativeBridge.GetPlayerAddress == null)
|
||||
return null;
|
||||
|
||||
const int ipBufSize = 64;
|
||||
IntPtr ipBuf = Marshal.AllocCoTaskMem(ipBufSize);
|
||||
IntPtr portBuf = Marshal.AllocCoTaskMem(sizeof(int));
|
||||
try
|
||||
{
|
||||
int result = NativeBridge.GetPlayerAddress(getEntityId(), ipBuf, ipBufSize, portBuf);
|
||||
if (result == 0)
|
||||
return null;
|
||||
|
||||
string? ip = Marshal.PtrToStringAnsi(ipBuf);
|
||||
int port = Marshal.ReadInt32(portBuf);
|
||||
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
return null;
|
||||
|
||||
return new InetSocketAddress(new InetAddress(ip), port);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeCoTaskMem(ipBuf);
|
||||
Marshal.FreeCoTaskMem(portBuf);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players current experience level.
|
||||
/// </summary>
|
||||
/// <returns>Current experience level.</returns>
|
||||
public int getLevel() => _level;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the players current experience level.
|
||||
/// </summary>
|
||||
/// <param name="level">New experience level.</param>
|
||||
public void setLevel(int level)
|
||||
{
|
||||
_level = level;
|
||||
NativeBridge.SetLevel?.Invoke(getEntityId(), level);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players current experience points towards the next level.
|
||||
/// This is a percentage value. 0 is "no progress" and 1 is "next level".
|
||||
/// </summary>
|
||||
/// <returns>Current experience points.</returns>
|
||||
public float getExp() => _exp;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the players current experience points towards the next level.
|
||||
/// This is a percentage value. 0 is "no progress" and 1 is "next level".
|
||||
/// </summary>
|
||||
/// <param name="exp">New experience points.</param>
|
||||
public void setExp(float exp)
|
||||
{
|
||||
_exp = exp;
|
||||
NativeBridge.SetExp?.Invoke(getEntityId(), exp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the player the amount of experience specified.
|
||||
/// </summary>
|
||||
/// <param name="amount">Exp amount to give.</param>
|
||||
public void giveExp(int amount)
|
||||
{
|
||||
NativeBridge.GiveExp?.Invoke(getEntityId(), amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the player the amount of experience levels specified.
|
||||
/// Levels can be taken by specifying a negative amount.
|
||||
/// </summary>
|
||||
/// <param name="amount">Amount of experience levels to give or take.</param>
|
||||
public void giveExpLevels(int amount)
|
||||
{
|
||||
NativeBridge.GiveExpLevels?.Invoke(getEntityId(), amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players current exhaustion level.
|
||||
/// Exhaustion controls how fast the food level drops. While you have a
|
||||
/// certain amount of exhaustion, your saturation will drop to zero, and
|
||||
/// then your food will drop to zero.
|
||||
/// </summary>
|
||||
/// <returns>Exhaustion level.</returns>
|
||||
public float getExhaustion() => _exhaustion;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the players current exhaustion level.
|
||||
/// </summary>
|
||||
/// <param name="value">Exhaustion level.</param>
|
||||
public void setExhaustion(float value)
|
||||
{
|
||||
_exhaustion = value;
|
||||
NativeBridge.SetExhaustion?.Invoke(getEntityId(), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the players current saturation level.
|
||||
/// </summary>
|
||||
/// <param name="value">Saturation level.</param>
|
||||
public void setSaturation(float value)
|
||||
{
|
||||
_saturation = value;
|
||||
NativeBridge.SetSaturation?.Invoke(getEntityId(), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the players current food level.
|
||||
/// </summary>
|
||||
/// <returns>Food level.</returns>
|
||||
public int getFoodLevel() => _foodLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the players current food level.
|
||||
/// </summary>
|
||||
/// <param name="value">New food level.</param>
|
||||
public void setFoodLevel(int value)
|
||||
{
|
||||
_foodLevel = value;
|
||||
NativeBridge.SetFoodLevel?.Invoke(getEntityId(), value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
public void spawnParticle(Particle particle, Location location, int count)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
public void spawnParticle(Particle particle, double x, double y, double z, int count)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, Location location, int count, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, double x, double y, double z, int count, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, 0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, 0, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, 0, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, 0, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="extra">The extra data for this particle, depends on the particle used (normally speed).</param>
|
||||
public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, extra, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="extra">The extra data for this particle, depends on the particle used (normally speed).</param>
|
||||
public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="location">The location to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="extra">The extra data for this particle, depends on the particle used (normally speed).</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, Location location, int count, double offsetX, double offsetY, double offsetZ, double extra, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, location.X, location.Y, location.Z, count, offsetX, offsetY, offsetZ, extra, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the particle (the number of times specified by count)
|
||||
/// at the target location. The position of each particle will be
|
||||
/// randomized positively and negatively by the offset parameters
|
||||
/// on each axis. Only this player will see the particle.
|
||||
/// </summary>
|
||||
/// <param name="particle">The particle to spawn.</param>
|
||||
/// <param name="x">The position on the x axis to spawn at.</param>
|
||||
/// <param name="y">The position on the y axis to spawn at.</param>
|
||||
/// <param name="z">The position on the z axis to spawn at.</param>
|
||||
/// <param name="count">The number of particles.</param>
|
||||
/// <param name="offsetX">The maximum random offset on the X axis.</param>
|
||||
/// <param name="offsetY">The maximum random offset on the Y axis.</param>
|
||||
/// <param name="offsetZ">The maximum random offset on the Z axis.</param>
|
||||
/// <param name="extra">The extra data for this particle, depends on the particle used (normally speed).</param>
|
||||
/// <param name="data">The data to use for the particle or null.</param>
|
||||
/// <typeparam name="T">The type of the particle data.</typeparam>
|
||||
public void spawnParticle<T>(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, T? data)
|
||||
{
|
||||
spawnParticleInternal(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data);
|
||||
}
|
||||
|
||||
private void spawnParticleInternal(Particle particle, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double extra, object? data)
|
||||
{
|
||||
if (NativeBridge.SpawnParticle == null)
|
||||
return;
|
||||
|
||||
int particleId = (int)particle;
|
||||
if (data is ItemStack itemStack &&
|
||||
(particle == Particle.ITEM_CRACK || particle == Particle.BLOCK_CRACK))
|
||||
{
|
||||
int id = itemStack.getTypeId();
|
||||
int aux = itemStack.getDurability();
|
||||
particleId = (int)particle | ((id & 0x0FFF) << 8) | (aux & 0xFF);
|
||||
}
|
||||
|
||||
NativeBridge.SpawnParticle(getEntityId(), particleId,
|
||||
(float)x, (float)y, (float)z,
|
||||
(float)offsetX, (float)offsetY, (float)offsetZ,
|
||||
(float)extra, count);
|
||||
}
|
||||
|
||||
// INTERNAL
|
||||
internal void SetSaturationInternal(float saturation) => _saturation = saturation;
|
||||
internal void SetWalkSpeedInternal(float walkSpeed) => _walkSpeed = walkSpeed;
|
||||
internal void SetPlayerUniqueIdInternal(Guid id) => _playerUniqueId = id;
|
||||
internal void SetPlayerRawOnlineXUIDInternal(ulong xuid) => _playerRawOnlineXUID = xuid;
|
||||
internal void SetPlayerRawOfflineXUIDInternal(ulong xuid) => _playerRawOfflineXUID = xuid;
|
||||
internal void SetSneakingInternal(bool sneaking) => _sneaking = sneaking;
|
||||
internal void SetSprintingInternal(bool sprinting) => _sprinting = sprinting;
|
||||
internal void SetAllowFlightInternal(bool allowFlight) => _allowFlight = allowFlight;
|
||||
internal void SetSleepingIgnoredInternal(bool ignored) => _sleepingIgnored = ignored;
|
||||
internal void SetLevelInternal(int level) => _level = level;
|
||||
internal void SetExpInternal(float exp) => _exp = exp;
|
||||
internal void SetTotalExperienceInternal(int totalExp) => _totalExperience = totalExp;
|
||||
internal void SetFoodLevelInternal(int foodLevel) => _foodLevel = foodLevel;
|
||||
internal void SetExhaustionInternal(float exhaustion) => _exhaustion = exhaustion;
|
||||
}
|
||||
Reference in New Issue
Block a user