From 32f058d07820a3836f38a40a3b184ac0349b1a26 Mon Sep 17 00:00:00 2001
From: sylvessa <225480449+sylvessa@users.noreply.github.com>
Date: Mon, 23 Mar 2026 20:55:27 -0500
Subject: [PATCH] particle stuff
---
Minecraft.Server.FourKit/Entity/Player.cs | 232 ++++++++++++++++++++++
Minecraft.Server.FourKit/FourKitHost.cs | 13 ++
Minecraft.Server.FourKit/NativeBridge.cs | 9 +
Minecraft.Server.FourKit/Particle.cs | 45 +++++
Minecraft.Server/FourKitBridge.cpp | 17 ++
5 files changed, 316 insertions(+)
create mode 100644 Minecraft.Server.FourKit/Particle.cs
diff --git a/Minecraft.Server.FourKit/Entity/Player.cs b/Minecraft.Server.FourKit/Entity/Player.cs
index 17640a5f7..e46930485 100644
--- a/Minecraft.Server.FourKit/Entity/Player.cs
+++ b/Minecraft.Server.FourKit/Entity/Player.cs
@@ -373,6 +373,238 @@ public class Player : HumanEntity, OfflinePlayer, CommandSender
NativeBridge.SetFoodLevel?.Invoke(getEntityId(), value);
}
+ ///
+ /// Spawns the particle (the number of times specified by count)
+ /// at the target location. Only this player will see the particle.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ public void spawnParticle(Particle particle, Location location, int count)
+ {
+ spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, null);
+ }
+
+ ///
+ /// Spawns the particle (the number of times specified by count)
+ /// at the target location. Only this player will see the particle.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ public void spawnParticle(Particle particle, double x, double y, double z, int count)
+ {
+ spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, null);
+ }
+
+ ///
+ /// Spawns the particle (the number of times specified by count)
+ /// at the target location. Only this player will see the particle.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(Particle particle, Location location, int count, T? data)
+ {
+ spawnParticleInternal(particle, location.X, location.Y, location.Z, count, 0, 0, 0, 0, data);
+ }
+
+ ///
+ /// Spawns the particle (the number of times specified by count)
+ /// at the target location. Only this player will see the particle.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(Particle particle, double x, double y, double z, int count, T? data)
+ {
+ spawnParticleInternal(particle, x, y, z, count, 0, 0, 0, 0, data);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The extra data for this particle, depends on the particle used (normally speed).
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The extra data for this particle, depends on the particle used (normally speed).
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The location to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The extra data for this particle, depends on the particle used (normally speed).
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The particle to spawn.
+ /// The position on the x axis to spawn at.
+ /// The position on the y axis to spawn at.
+ /// The position on the z axis to spawn at.
+ /// The number of particles.
+ /// The maximum random offset on the X axis.
+ /// The maximum random offset on the Y axis.
+ /// The maximum random offset on the Z axis.
+ /// The extra data for this particle, depends on the particle used (normally speed).
+ /// The data to use for the particle or null.
+ /// The type of the particle data.
+ public void spawnParticle(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;
diff --git a/Minecraft.Server.FourKit/FourKitHost.cs b/Minecraft.Server.FourKit/FourKitHost.cs
index 266b5f544..45594f359 100644
--- a/Minecraft.Server.FourKit/FourKitHost.cs
+++ b/Minecraft.Server.FourKit/FourKitHost.cs
@@ -625,6 +625,19 @@ public static class FourKitHost
}
}
+ [UnmanagedCallersOnly]
+ public static void SetParticleCallbacks(IntPtr spawnParticle)
+ {
+ try
+ {
+ NativeBridge.SetParticleCallbacks(spawnParticle);
+ }
+ catch (Exception ex)
+ {
+ ServerLog.Error("fourkit", $"SetParticleCallbacks error: {ex}");
+ }
+ }
+
[UnmanagedCallersOnly]
public static long FirePlayerDropItem(int entityId, int itemId, int itemCount, int itemAux,
IntPtr outItemIdPtr, IntPtr outItemCountPtr, IntPtr outItemAuxPtr)
diff --git a/Minecraft.Server.FourKit/NativeBridge.cs b/Minecraft.Server.FourKit/NativeBridge.cs
index 265c3b512..f415c6613 100644
--- a/Minecraft.Server.FourKit/NativeBridge.cs
+++ b/Minecraft.Server.FourKit/NativeBridge.cs
@@ -153,6 +153,9 @@ internal static class NativeBridge
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void NativeSetExhaustionDelegate(int entityId, float exhaustion);
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ internal delegate void NativeSpawnParticleDelegate(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count);
+
internal static NativeDamageDelegate? DamagePlayer;
internal static NativeSetHealthDelegate? SetPlayerHealth;
@@ -204,6 +207,7 @@ internal static class NativeBridge
internal static NativeSetFoodLevelDelegate? SetFoodLevel;
internal static NativeSetSaturationDelegate? SetSaturation;
internal static NativeSetExhaustionDelegate? SetExhaustion;
+ internal static NativeSpawnParticleDelegate? SpawnParticle;
internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity)
{
@@ -277,4 +281,9 @@ internal static class NativeBridge
SetSaturation = Marshal.GetDelegateForFunctionPointer(setSaturation);
SetExhaustion = Marshal.GetDelegateForFunctionPointer(setExhaustion);
}
+
+ internal static void SetParticleCallbacks(IntPtr spawnParticle)
+ {
+ SpawnParticle = Marshal.GetDelegateForFunctionPointer(spawnParticle);
+ }
}
diff --git a/Minecraft.Server.FourKit/Particle.cs b/Minecraft.Server.FourKit/Particle.cs
new file mode 100644
index 000000000..7df6c196f
--- /dev/null
+++ b/Minecraft.Server.FourKit/Particle.cs
@@ -0,0 +1,45 @@
+namespace Minecraft.Server.FourKit;
+
+///
+/// Enum of particle effects that can be spawned for a player.
+///
+public enum Particle
+{
+ WATER_BUBBLE = 0,
+ SMOKE_NORMAL = 1,
+ NOTE = 2,
+ PORTAL = 3,
+ EXPLOSION_NORMAL = 5,
+ FLAME = 6,
+ LAVA = 7,
+ FOOTSTEP = 8,
+ WATER_SPLASH = 9,
+ SMOKE_LARGE = 10,
+ REDSTONE = 11,
+ SNOWBALL = 12,
+ SNOW_SHOVEL = 13,
+ SLIME = 14,
+ HEART = 15,
+ SUSPENDED = 16,
+ SUSPENDED_DEPTH = 17,
+ CRIT = 18,
+ EXPLOSION_HUGE = 19,
+ EXPLOSION_LARGE = 20,
+ TOWN_AURA = 21,
+ SPELL = 22,
+ SPELL_WITCH = 23,
+ SPELL_MOB = 24,
+ SPELL_MOB_AMBIENT = 25,
+ SPELL_INSTANT = 26,
+ CRIT_MAGIC = 27,
+ DRIP_WATER = 28,
+ DRIP_LAVA = 29,
+ ENCHANTMENT_TABLE = 30,
+ DRAGON_BREATH = 31,
+ END_ROD = 32,
+ VILLAGER_ANGRY = 33,
+ VILLAGER_HAPPY = 34,
+ FIREWORKS_SPARK = 35,
+ ITEM_CRACK = 0x100000,
+ BLOCK_CRACK = 0x200000,
+}
diff --git a/Minecraft.Server/FourKitBridge.cpp b/Minecraft.Server/FourKitBridge.cpp
index a10bdb3cc..4cecf840e 100644
--- a/Minecraft.Server/FourKitBridge.cpp
+++ b/Minecraft.Server/FourKitBridge.cpp
@@ -39,6 +39,7 @@
#include "..\Minecraft.World\SetExperiencePacket.h"
#include "..\Minecraft.World\SetHealthPacket.h"
#include "..\Minecraft.World\LevelSoundPacket.h"
+#include "..\Minecraft.World\LevelParticlesPacket.h"
#include "..\Minecraft.World\SimpleContainer.h"
#include "..\Minecraft.World\Slot.h"
#include "..\Minecraft.World\Tile.h"
@@ -180,6 +181,7 @@ typedef int(__stdcall *fn_fire_bed_enter)(int entityId, int dimId, int bedX, int
typedef void(__stdcall *fn_fire_bed_leave)(int entityId, int dimId, int bedX, int bedY, int bedZ);
typedef void(__stdcall *fn_set_entity_callbacks)(void *setSneaking, void *setVelocity, void *setAllowFlight, void *playSound, void *setSleepingIgnored);
typedef void(__stdcall *fn_set_experience_callbacks)(void *setLevel, void *setExp, void *giveExp, void *giveExpLevels, void *setFoodLevel, void *setSaturation, void *setExhaustion);
+typedef void(__stdcall *fn_set_particle_callbacks)(void *spawnParticle);
struct OpenContainerInfo
{
@@ -222,6 +224,7 @@ static fn_fire_bed_enter s_managedFireBedEnter = nullptr;
static fn_fire_bed_leave s_managedFireBedLeave = nullptr;
static fn_set_entity_callbacks s_managedSetEntityCallbacks = nullptr;
static fn_set_experience_callbacks s_managedSetExperienceCallbacks = nullptr;
+static fn_set_particle_callbacks s_managedSetParticleCallbacks = nullptr;
static bool s_initialized = false;
@@ -1432,6 +1435,16 @@ static void __cdecl NativeSetExhaustion(int entityId, float exhaustion)
fd->setExhaustion(exhaustion);
}
+static void __cdecl NativeSpawnParticle(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count)
+{
+ // todo(SYLV): i glaub des geht a gscheider
+ auto player = FindPlayer(entityId);
+ if (!player || !player->connection) return;
+ wchar_t buf[32];
+ swprintf_s(buf, L"%d", particleId);
+ player->connection->send(std::make_shared(std::wstring(buf), x, y, z, offsetX, offsetY, offsetZ, speed, count));
+}
+
static std::wstring FindNet10SystemRoot()
{
// overengineered
@@ -1676,6 +1689,7 @@ void Initialize()
ok = ok && GetManagedEntryPoint(loadAssembly, assemblyPath.c_str(), typeName, L"FireBedLeave", (void **)&s_managedFireBedLeave);
ok = ok && GetManagedEntryPoint(loadAssembly, assemblyPath.c_str(), typeName, L"SetEntityCallbacks", (void **)&s_managedSetEntityCallbacks);
ok = ok && GetManagedEntryPoint(loadAssembly, assemblyPath.c_str(), typeName, L"SetExperienceCallbacks", (void **)&s_managedSetExperienceCallbacks);
+ ok = ok && GetManagedEntryPoint(loadAssembly, assemblyPath.c_str(), typeName, L"SetParticleCallbacks", (void **)&s_managedSetParticleCallbacks);
if (!ok)
{
@@ -1748,6 +1762,9 @@ void Initialize()
(void *)&NativeSetSaturation,
(void *)&NativeSetExhaustion);
+ s_managedSetParticleCallbacks(
+ (void *)&NativeSpawnParticle);
+
LogInfo("fourkit", "FourKit initialized successfully.");
}