Skip to content

C# Server Plugin API (WIP)#1261

Draft
sylvessa wants to merge 104 commits intosmartcmd:mainfrom
sylvessa:feature/plugin-api
Draft

C# Server Plugin API (WIP)#1261
sylvessa wants to merge 104 commits intosmartcmd:mainfrom
sylvessa:feature/plugin-api

Conversation

@sylvessa
Copy link
Contributor

@sylvessa sylvessa commented Mar 15, 2026

Description

This is a C# Plugin API for the dedicated server... It adds a api very similar to Bukkit.

There is a discord server where i post updates regarding this

Made to use .NET 8.0, will soon upgrade to .NET 10.0

I REAAALLLYY want more people to help work on this. This plans to be almost full parity with Bukkit.

I'll consider this ready to merge when I am able to port my 1.6 Bukkit plugin with no issues.

Changes

i have added plugin initialization under ServerMain.cpp, and a new project called "Minecraft.Server.FourKit"

This project is what links the C# and C++ together, and is also what plugin devs should include in their plugins.

Plugins devs can include either the full project or the DLL that gets built from Minecraft.Server.FourKit in their C# project.

Plugins go under the plugins folder and plugins are compiled as a DLL.

There is also nightly releases for this, and it should work with docker. Anyone updating to latest nightly should update both the DLL and the EXE.

The workflows have also been modified to account for .NET and other changes.

Many files (primarily PlayerConnection.cpp) have been modified to emit events to FourKit.

how to use!

creating le plugin

as stated above, plugin devs must include the DLL or the full "Minecraft.Server.FourKit" project

after that, there must be a file somewhere in the DLL that interfaces with ServerPlugin

public class ExamplePlugin : ServerPlugin
{
    public void OnEnable() {}

    public void OnDisable() {}

    public string GetName() => "My Freaking Awesome Plugin";
    public string GetVersion() => "0.0.1";
    public string GetAuthor() => "Sylvessa";
}

registering a listener

Just like bukkit, you can register a listener with FourKit.addListener(new ListenerClass());

Listener classes must use the Listener interface.

Example:

public class ListenerClass : Listener
{
    [EventHandler]
    public void onJoin(PlayerJoinEvent e)
    {
        Player player = e.getPlayer();
        player.sendMessage("welcome to my cool server");
    }
}

events

To use an event in a listener, each event must have [EventHandler] before the actual function.

Currently available events:

  • PlayerJoinEvent
  • PlayerLeaveEvent
  • PlayerChatEvent
  • BlockBreakEvent
  • BlockPlaceEvent
  • PlayerMoveEvent
  • PlayerKickEvent
  • SignChangeEvent
  • PlayerInteractEvent
  • PlayerDeathEvent
  • PlayerDropItemEvent
  • EntityDamageEvent
  • EntityDamageByEntityEvent
  • InventoryOpenEvent

compile

When you compile a plugin, take its DLL and put it into the plugins folder in the server. Its as simple as that. when u start the server it should just Work.

click to expand this, this is a very long pr message, below will go over usages of every event, enum, how to make commands, etc and an example plugin

event usage

PlayerJoinEvent and PlayerLeaveEvent

Both of these only come with event.getPlayer()

Functions under the Player class include:

  • getName()
  • getHealth()
  • isInsideVehicle()
  • getFood()
  • getFallDistance()
  • getYRot()
  • getXRot()
  • getX()
  • getY()
  • getZ()
  • getDimension()
  • setFallDistance(float)
  • setHealth(float)
  • setFood(int)
  • sendMessage(string message)
  • kickPlayer()
  • teleport(float x, float y, float z)
  • isSneaking()
  • isSprinting()
  • setSprinting(bool)
  • getAddress() returns a InetSocketAddress (just like java bukkit!!)
  • getGamemode() returns a GameMode enum
  • setGameMode(GameMode mode)
  • isSleeping()
  • getItemInHand() returns ItemStack or null
  • setItemInHand(ItemStack item)
  • setAllowFlight(bool)
  • getAllowFlight()
  • getExhaustion()
  • getSaturation()
  • giveExp(int amount)
  • giveExpLevels(int amount)
  • getTotalExperience()
  • isFlying()
  • setExhaustion(float value)
  • setFlying(boolean value)
  • setExp(float exp)
  • setLevel(int level)
  • setSaturation(float value)
  • setWalkSpeed(float value)
  • getWalkSpeed()
  • getInventory() returns PlayerInventory class
  • openInventory(Inventory inv)

PlayerPortalEvent

Cancellable, these functions are available:

  • setFrom(x, y, z)
  • setTo(x,y,z)
  • getFrom()
  • getTo()
  • getCause() which returns a TeleportCause enum

SignChangeEvent

  • getLine(int line)
  • getLines() returns String[]
  • getBlock() returns the block of the sign
  • setLine(int line, string content)

PlayerKickEvent

  • isCancelled()
  • setCancelled(bool)
  • getReason() returns a DisconnectReason
  • setLeaveMessage(string) to set a custom leave msg
  • getLeaveMessage() gets a custom leave message if any
  • getPlayer()

PlayerKickEvent is fired before PlayerLeaveEvent

InventoryOpenEvent

funcs:

  • getPlayer()
  • getInventory() returns Inventory by default, however can be any other type of Inventory
  • isCancelled()
  • setCancelled(bool)

Example usage

[EventHandler]
public void inventoryevent(InventoryOpenEvent e)
{
    if (e.getInventory().getType() == InventoryType.FURNACE)
    {
        FurnaceInventory inv = (FurnaceInventory)e.getInventory(); // cast to furnaceinventory
        inv.setFuel(new ItemStack(1, 64)); // makes the fuel slot of furnace be 64 stone
    } else if (e.getInventory().getType() == InventoryType.ENDER_CHEST)
    {
        // ender chests get cancelled
        e.setCancelled(true);
    } else
    {
        // any other inventories the plr gets a msg of the id of the first three slots
        int slot1 = e.getInventory().getItem(0)?.getTypeId() ?? 0;
        int slot2 = e.getInventory().getItem(1)?.getTypeId() ?? 0;
        int slot3 = e.getInventory().getItem(2)?.getTypeId() ?? 0;
        e.getPlayer().sendMessage($"first three slots: {slot1} {slot2} {slot3}");
    }
}

PlayerInteractEvent

  • getAction() returns Action enum
  • getPlayer()
  • getClickedBlock() returns Block
  • getBlockFace() returns BlockFace
  • hasItem()
  • setCancelled()
  • isCancelled()

PlayerDeathEvent

  • getEntity() same as getPlayer() but bukkit doesnt have that for this event, it was getEntity for some reason lmao
  • getDeathMessage()
  • setDeathMessage(string)
  • setKeepLevel(bool)
  • setKeepInventory(bool)
  • setNewExp(int)
  • setNewLevel(int)
  • getNewExp(int)
  • getNewLevel(int)
  • getKeepLevel()
  • getKeepInventory()

PlayerChatEvent

This comes with event.getPlayer(), event.getMessage(), event.setCancelled(bool), and event.isCancelled()

Cancelling makes it not actually send the chat packet out, but still lets you process the chat event.

BlockBreakEvent and BlockPlaceEvent

Comes with event.getPlayer(), event.getBlock(), event.setCancelled(bool), and event.isCancelled()

Cancelling does what you think it does. It cancels it!!! Pretty simple.

event.getBlock() returns a Block class, which has these methods:

  • breakNaturally()
  • getX()
  • getY()
  • getZ()
  • getType()
  • setType(int id)
  • setData(int aux)
  • getData()

EntityDamageEvent and EntityDamageByEntityEvent

AS OF RIGHT NOW THESE EVENTS ONLY FIRE FOR A Player BEING DAMAGED

EntityDamageEvent funcs:

  • getDamage() gets raw damage (as double)
  • getFinalDamage() gets damage after calculation as double (armor etc)
  • getEntity() returns Entity (can also be Player etc) that was damaged
  • getCause() returns DamageCause Enum
  • setCancelled(bool)
  • isCancelled()
  • setDamage(double damage)

EntityDamageByEntityEvent extends entitydamageevent, and has this additional func:

  • getDamager()

EXAMPLE USAGE:

[EventHandler]
public void onentitydamagebyentity(EntityDamageByEntityEvent e)
{
    Entity damaged = e.getEntity();
    Entity damager = e.getDamager();

    if (damaged is Player player)
    {
        player.sendMessage($"took {e.getFinalDamage()} damage");

        if (damager is Player attacker)
        {
            player.sendMessage($"attacked by {attacker.getName()}");
        }
    }
}

PlayerDropItemEvent

  • getItemDrop() returns Item class
  • getPlayer()
  • isCancelled()
  • setCancelled(bool)

PlayerMoveEvent

Comes with event.getPlayer(), event.getTo(), event.getFrom() event.setCancelled(bool), and event.isCancelled()

Cancelling stops the player from moving.

getTo and getFrom return a Location class, which has these methods:

  • getX()
  • getY()
  • getZ()

I will replace some other stuff previously mentioned to use this Location class sometime soon.

Command System

There is a command system, similar to bukkit. commands registered via this dont work in console yet tho

This will be improved as time goes on.

you can run FourKit.getCommand("test").setExecutor(new CoolCommand()); to point a command to an executor

getCommand("test") returns a PluginCommand which has these funcs rn:

  • setExecutor(CommandExecutor class)
  • getExecutor()
  • setUsage(string usage)
  • setAliases(List<string> aliases
  • setDescription(string)
  • getAliases()
  • getDescription()
  • getUsage()
  • getName()

custom command classes look like this:

public class CoolCommand : CommandExecutor
{
    public bool onCommand(CommandSender sender, Command command, string label, string[] args)
    {
        if (!(sender is Player)) return false;
        Player p = (Player)sender;

        p.sendMessage("Do it work?");
        p.teleport(p.getX(), p.getY() + 3, p.getZ());
        
        return true;
    }
}

just like in bukkit, you can check if the sender is a player. theres no way to check if a sender is Server yet (due to commands ran by server not being bound to this)

Classes

ItemStack

ItemStack can be created by these methods:

  • new ItemStack(int id, int amount)
  • new ItemStack(int id, int amount, int aux)

Item

  • getItemStack() returns ItemStack
  • setItemStack(ItemStack item)
  • setPickupDelay(int ticks)
  • getPickupDelay() Default is 40 ticks
  • getLocation() returns Location

Location

Can be created by new Location(x,y,z)

World

you can get a world by a few methods
any event that returns a player, you can run getWorld() on the player

you can also get world by FourKit.getWorld("world") or FourKit.getWorld(0) it accepts a dimension id or the world name
-# (the world name isnt actually bound to anything, it just converts world to 0 etc)

within the World class, you can run these functions:

  • createExplosion(double x, double y, double z, float power)
  • createExplosion(double x, double y, double z, float power, bool setFire)
  • createExplosion(double x, double y, double z, float power, bool setFire, bool breakBlocks)
  • createExplosion(Location loc, float power)
  • createExplosion(Location loc, float power, bool setFire)
  • createExplosion(Location loc, float power, bool setFire, bool breakBlocks)
  • dropItem(Location location, ItemStack item)
  • dropItemNaturally(Location location, ItemStack item) (drops an item at the specified Location with a random offset)
  • getBlockAt(int x, int y, int z)
  • getBlockAt(Location location)
  • getFullTime()
  • getHighestBlockAt(int x, int z) returns a Block
  • getHighestBlockAt(Location location)
  • getHighestBlockYAt(int x, int z) returns int
  • getHighestBlockYAt(Location location)
  • getPlayers() returns List
  • getSeed()
  • getSpawnLocation() returns a Location
  • getThunderDuration() returns int
  • getTime()
  • hasStorm()
  • isThundering()
  • setFullTime(int64 time)
  • setSpawnLocation(int x, int y, int z)
  • setStorm(bool)
  • setThunderDuration(int duration)
  • setThundering(bool thundering)
  • setTime(int64 time)
  • setWeatherDuration(int duration)
  • strikeLightning(double x, double y, double z)
  • strikeLightning(Location loc)
  • strikeLightningEffect(double x, double y, double z)
  • strikeLightningEffect(Location loc)

FourKit

  • FourKit.broadcastMessage(string message) sends a msg to all players
  • FourKit.addListener(Listener) adds a listener class, example: FourKit.addListener(new CoolListener())
  • FourKit.getCommand(string cmd) returns a PluginCommand for the command
  • FourKit.getPlayer(string username) gets a Player
  • FourKit.getWorld(string worldname) gets a world by name (converts stuff like world to its respective dimension)
  • FourKit.getWorld(int dimension) gets a world by dimension id
  • FourKit.createInventory(int size)
  • FourKit.createInventory(int size, string title)
  • FourKit.createInventory(InventoryType type)
  • FourKit.createInventory(InventoryType type, string title)

PluginCommand

  • setExecutor(CommandExecutor executor) sets executor of a command, example: getCommand("test").setExecutor(new CoolExecutorClass())
  • getExecutor() returns the executor for the command

PlayerInventory and Inventory

Funcs available under Inventory:

  • getType() returns InventoryType enum
  • getSize()
  • getItem(int slot) returns ItemStack or null
  • setItem(int slot, ItemStack item) sets item in a slot
  • getContents() returns ItemStack[]
  • setContents(ItemStack[] contents)
  • firstEmpty() returns first empty slot
  • first() returns the first slot in the inventory containing an ItemStack with the given stack, if not found it returns -1
  • contains(ItemStack item) returns true if the inventory contains any ItemStacks matching the given ItemStack
  • contains(ItemStack item, int amount) returns true if the inventory contains at least the minimum amount specified of exactly matching ItemStacks
  • containsAtLeast(ItemStack item, int amount) returns true if the inventory contains ItemStacks matching the given ItemStack whose amounts sum to at least the minimum amount specified
  • all(ItemStack item) returns a Dictionary<int, ItemStack>, finds all slots in the inventory containing any ItemStacks with the given ItemStack
  • addItem(ItemStack[] items) returns a Dictionary<int, ItemStack>, stores the given ItemStacks in the inventory
  • removeItem(ItemStack[] items) returns a Dictionary<int, ItemStack>, removes the given ItemStacks from the inventory.
  • getTitle() gets the custom title if set

Funcs available under PlayerInventory:

  • setItemInHand(ItemStack item)
  • getItemInHand() returns a ItemStack or null
  • getHolder() returns Player
  • getHelmet() returns ItemStack or null
  • setHelmet(ItemStack item)
  • getChestplate() returns ItemStack or null
  • setChestplate(ItemStack item)
  • getLeggings() returns ItemStack or null
  • setLeggings(ItemStack item)
  • getBoots() returns ItemStack or null
  • setBoots(ItemStack item)
  • getHeldItemSlot()
  • setHeldItemSlot(int id)
  • getArmorContents() returns ItemStack[]
  • setArmorContents(ItemStack[] items)

Example usage of the getArmorContents() and setArmorContents() func:

// getting armor contents
ItemStack[] armor = player.getInventory().getArmorContents();

ItemStack helmet = armor[0];
ItemStack chestplate = armor[1];
ItemStack leggings = armor[2];
ItemStack boots = armor[3];

// settings contents
ItemStack[] newArmor = new ItemStack[4]

newArmor[0] = new ItemStack(2);
newArmor[1] = new ItemStack(3);
newArmor[2] = new ItemStack(4);
newArmor[3] = new ItemStack(5);

player.getInventory().setArmorContents(newArmor);

// alt
ItemStack[] newArmor = [
    new ItemStack(2), 
    new ItemStack(3),
    new ItemStack(4), 
    new ItemStack(5)
];

player.getInventory().setArmorContents(newArmor);

DoubleChestInventory (extends Inventory)

  • getLeftSide() returns ItemStack[]
  • getRightSide() returns ItemStack[]

CraftingInventory (extends Inventory)

  • getResult() returns ItemStack
  • getMatrix() returns ItemStack[]

BrewerInventory (extends Inventory)

  • getIngredient() returns ItemStack
  • setIngredient(ItemStack item) sets ingredient

BeaconInventory (extends Inventory)

  • getItem() returns ItemStack
  • setItem(ItemStack item) sets item powering beacon

FurnaceInventory (extends Inventory)

  • getSmelting() returns ItemStack
  • setSmelting(ItemStack item) sets smelting item
  • getFuel() returns ItemStack
  • setFuel(ItemStack item) sets fuel
  • getResult() returns itemStack
  • setResult(ItemStack item) sets the result

HorseInventory (extends Inventory)

  • getSaddle() returns ItemStack
  • setSaddle(ItemStack item) sets the saddle
  • getArmor() returns ItemStack
  • setArmor(ItemStack item) sets the armor for hte horses inv

Enums

GameMode

  • ADVENTURE
  • CREATIVE
  • SURVIVAL

DamageCause

  • BLOCK_EXPLOSION
  • CONTACT
  • CUSTOM
  • DROWNING
  • ENTITY_ATTACK
  • ENTITY_EXPLOSION
  • FALL
  • FALLING_BLOCK
  • FIRE
  • FIRE_TICK
  • LAVA
  • LIGHTNING
  • MAGIC
  • MELTING
  • POISON
  • PROJECTILE
  • STARVATION
  • SUFFOCATION
  • SUICIDE
  • THORNS
  • VOID_DAMAGE
  • WITHER

InteractAction

  • RIGHT_CLICK_BLOCK
  • LEFT_CLICK_BLOCK

TeleportCause

  • END_PORTAL
  • ENDER_PEARL
  • NETHER_PORTAL
  • PLUGIN
  • UNKNOWN

DisconnectReason

  • NONE
  • LOGIN_TOO_LONG
  • ILLEGAL_STANCE
  • ILLEGAL_POSITION
  • MOVED_TOO_QUICKLY
  • NO_FLYING
  • KICKED
  • TIMED_OUT
  • CONNECTION_OVERFLOW
  • END_OF_STREAM
  • SERVER_FULL
  • OUTDATED_SERVER
  • OUTDATED_CLIENT
  • UNEXPECTED_PACKET
  • NO_MULTIPLAYER_PRIVILEGES_HOST
  • NO_MULTIPLAYER_PRIVILEGES_JOIN
  • NO_UGC_ALL_LOCAL
  • NO_UGC_SINGLE_LOCAL
  • CONTENT_RESTRICTED_ALL_LOCAL
  • CONTENT_RESTRICTED_SINGLE_LOCAL
  • NO_UGC_REMOTE
  • NO_FRIENDS_IN_GAME
  • BANNED
  • NOT_FRIENDS_WITH_HOST
  • NAT_MISMATCH

InventoryType

  • ANVIL
  • BEACON
  • BREWING
  • CHEST
  • CRAFTING
  • CREATIVE
  • DISPENSER
  • DROPPER
  • ENCHANTING
  • ENDER_CHEST
  • FURNACE
  • HOPPER
  • HORSE
  • MERCHANT
  • PLAYER
  • WORKBENCH
  • DOUBLE_CHEST

example full plugin

public class ExamplePlugin : ServerPlugin
{
    public void OnEnable()
    {
        FourKit.addListener(new CoolListener());
    }

    public void OnDisable() {}

    public string GetName() => "My Freaking Awesome Plugin";
    public string GetVersion() => "0.0.1";
    public string GetAuthor() => "Sylvessa";
}

public class CoolListener : Listener
{
    private bool nobreak = false;
    private bool nomove = false;
    private bool noplace = false;

    [EventHandler]
    public void onJoin(PlayerJoinEvent e)
    {
        Player player = e.getPlayer();
        Console.WriteLine($"{player.getName()} has joined the server yayy");
        player.sendMessage("welcome to my cool server");
    }

    [EventHandler]
    public void onLeave(PlayerLeaveEvent e)
    {
        Player player = e.getPlayer();
        Console.WriteLine($"{player.getName()} has left the server aw");
    }


    [EventHandler]
    public void onChat(PlayerChatEvent e)
    {
        Player player = e.getPlayer();

        switch (e.getMessage())
        {
            case "tadpole":
                nobreak = !nobreak;
                player.sendMessage($"nobreak is now {(nobreak ? "enabled" : "disabled")}");
                e.setCancelled(true);
                break;
            case "idk":
                e.setCancelled(true);
                Console.WriteLine($"player coord is {player.getX()} {player.getY()} {player.getZ()}");
                FourKit.getBlockAt(player.getX(), player.getY() + 3, player.getZ()).setType(2);
                break;
            case "nomove":
                e.setCancelled(true);
                nomove = !nomove;
                player.sendMessage(nomove ? "noi moving for u now" : "ok u can move");
                break;
            case "noplace":
                e.setCancelled(true);
                noplace = !noplace;
                player.sendMessage(noplace ? "noi placing for u now" : "ok u can place blocks");
                break;
            default:
                break;
        }
    }

    [EventHandler]
    public void onBreak(BlockBreakEvent e)
    {
        if (nobreak)
        {
            e.setCancelled(true);
            Block block = e.getBlock();
            block.setType(2);
        }
    }

    [EventHandler]
    public void onPlace(BlockPlaceEvent e)
    {
        if (noplace) e.setCancelled(true);
    }

    [EventHandler]
    public void onMove(PlayerMoveEvent e)
    {
        if (nomove) e.setCancelled(true);
    }
}

in this example when a user joins, it sends them a welcome message.

a user can also use the commands tadpole, idk, nomove, noplace (not prefixed by a slash)
dont ask why i made tadpole a thing lol

tadpole makes it cancel all break events, and replaces the block broken with stone.

idk places stone above the players head

nomove prevents the player from moving

noplace prevents the player from placing

kuwacom added 30 commits March 4, 2026 18:12
- Introduced `ServerMain.cpp` for the dedicated server logic, handling command-line arguments, server initialization, and network management.
- Created `postbuild_server.ps1` script for post-build tasks, including copying necessary resources and DLLs for the dedicated server.
- Added `CopyServerAssets.cmake` to manage the copying of server assets during the build process, ensuring required files are available for the dedicated server.
- Defined project filters in `Minecraft.Server.vcxproj.filters` for better organization of server-related files.
- Introduced ServerLogger for logging startup steps and world I/O operations.
- Implemented ServerProperties for loading and saving server configuration from `server.properties`.
- Added WorldManager to handle world loading and creation based on server properties.
- Updated ServerMain to integrate server properties loading and world management.
- Enhanced project files to include new source and header files for the server components.
# Conflicts:
#	Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp
#	Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp
#	Minecraft.Client/Windows64/Network/WinsockNetLayer.h
Since 31881af56936aeef38ff322b975fd0 , `skinHud.swf` for 720 is not included in `MediaWindows64.arc`,
the app crashes unless the virtual screen is set to HD.
…erver

# Conflicts:
#	.gitignore
#	CMakeLists.txt
#	Minecraft.Client/Common/Network/GameNetworkManager.cpp
…ing sources

- remove stale Windows64 Miles (mss64) link/copy references from server build
- add Common/Filesystem/Filesystem.cpp to Minecraft.Server.vcxproj
- add Windows64/PostProcesser.cpp to Minecraft.Server.vcxproj
- fix unresolved externals (PostProcesser::*, FileExists) in dedicated server build
Since the crash caused by the 720p `skinHud.swf` not being included in `MediaWindows64.arc` has been resolved, switching back to 720p to reduce resource usage.
add with entrypoint and build scripts
…ft-LegacyConsoleEdition into feature/dedicated-server
on the server side, I fixed the behavior introduced after commit aadb511, where newly created worlds are intentionally not saved to disk immediately.
LAN-Discovery, which isn’t needed in server mode and could potentially be a security risk, has also been disabled(only server mode).
- Integrated linenoise library for line editing and completion in the server console.
- Updated ServerLogger to handle external writes safely during logging.
- Modified ServerMain to initialize and manage the ServerCli for command input.
- The implementation is separate from everything else, so it doesn't affect anything else.
- The command input section and execution section are separated into threads.
Like most command line tools, it highlights predictions in gray.
…down race

Before this change, server/host shutdown closed sockets directly in
ServerConnection::stop(), which bypassed the normal disconnect flow.
As a result, clients could be dropped without receiving a proper
DisconnectPacket during stop/kill/world-close paths.

Also, WinsockNetLayer::Shutdown() could destroy synchronization objects
while host-side recv threads were still exiting, causing a crash in
RecvThreadProc (access violation on world close in host mode).
- Add client-side host disconnect handling in CPlatformNetworkManagerStub::DoWork() for _WINDOWS64.
- When in QNET_STATE_GAME_PLAY as a non-host and WinsockNetLayer::IsConnected() becomes false, trigger g_NetworkManager.HandleDisconnect(false) to enter the normal disconnect/UI flow.
- Use m_bLeaveGameOnTick as a one-shot guard to prevent repeated disconnect handling while the link remains down.
- Reset m_bLeaveGameOnTick on LeaveGame(), HostGame(), and JoinGame() to avoid stale state across sessions.
Conflict resolution summary:

- .gitignore: kept dedicated-server-specific ignore entries (tmp*/, _server_asset_probe/, server-data/) and also kept main-side entries (*.user, /out).

- Minecraft.Client/Common/Network/GameNetworkManager.cpp: kept int64_t seed from main and preserved dedicatedNoLocalHostPlayer logic from dedicated-server.

- Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp: preserved dedicated-server shutdown flow and also included main-side cleanup for s_smallIdToSocketLock.
Moved file operations to `utils`.
…P bans

- add Access frontend that publishes thread-safe ban manager snapshots for dedicated server use
- add BanManager storage for banned-players.json and banned-ips.json with load/save/update flows
- add persistent player and IP ban checks during dedicated server connection handling
- add UTF-8 BOM-safe JSON parsing and shared file helpers backed by nlohmann/json
- add Unicode-safe ban file read/write and safer atomic replacement behavior on Windows
- add active-ban snapshot APIs and expiry-aware filtering for expires metadata
- add RAII-based dedicated access shutdown handling during server startup and teardown
- As a side effect, saving has become faster!
@OpenSauce04
Copy link

OpenSauce04 commented Mar 15, 2026

Why C#?

@sylvessa
Copy link
Contributor Author

@OpenSauce04 because i had tried lua before and it sucked and C# is good for beginners i think. and i can make it structured similarly to Bukkit as well

@Un1q32
Copy link

Un1q32 commented Mar 15, 2026

Wouldn't C# give preference to windows?

@PineappleJohn
Copy link

This is pretty cool, I have had many issues trying to add basic features to the legacy edition, so a C# plugin system would be much easier as it allows people who have experience in Unity to mod Minecraft LCE.

@ModMaker101
Copy link
Contributor

Wouldn't C# give preference to windows?

C# is cross platform, so no.

@ModMaker101
Copy link
Contributor

Why C#?

C# is performant, cross platform, has strong typing, and many other things and would probably be the best for this unironically.

@Un1q32
Copy link

Un1q32 commented Mar 16, 2026

Wouldn't C# give preference to windows?

C# is cross platform, so no.

Yea but isn't it a lot easier to develop C# on windows, plus you don't need to install a huge runtime dependency since the runtime ships with the OS. The only time I've ever installed mono on Linux is for Terraria. Wouldn't something like WASM be better, esp since you can choose whatever language you want instead of being locked to something that a lot of people won't know.

@sylvessa
Copy link
Contributor Author

sylvessa commented Mar 16, 2026

Wouldn't C# give preference to windows?

C# is cross platform, so no.

Yea but isn't it a lot easier to develop C# on windows, plus you don't need to install a huge runtime dependency since the runtime ships with the OS. The only time I've ever installed mono on Linux is for Terraria. Wouldn't something like WASM be better, esp since you can choose whatever language you want instead of being locked to something that a lot of people won't know.

C# is not an unknown language, a lot of people know it, its used in Unity and a lot of other stuff, and is more beginner friendly than some other languages. Like i can just tell someone to go install .net sdk and visual studio or whatever they want to use for C# development that be vs on windows or whatever for linux and they are practically done with the dev tool setup all they gotta do is make the plugin itself. C# is also one of the first languages I learned outside of lua so i'd say its better for beginners.

.NET is also not bundled with Windows or anything like that lol?

its also a lot more mature compared to WASM, you arent locked to using windows for C# either, its made to be cross platform, the only restriction right now is the fact that the repo cannot be built natively to linux yet.

@Un1q32
Copy link

Un1q32 commented Mar 16, 2026

WASM is pretty mature, about 10 years old, also a lot faster than C#.

Whenever I see people talk about C# it's always people writing Windows first software that then gets ported to other platforms, I've never seen a Linux or macOS exclusive C# program. Kinda like how Swift is always macOS first even if it can technically run on anything. When I see something like this done by Linux people (and honestly even most Windows and mac people) it's always either Lua or WASM.

Also doesn't Windows 11 bundle the .NET runtime? It's at least a common enough dependency that you can rely on almost all Windows users having it already, so defacto default.

@sylvessa
Copy link
Contributor Author

yk this was originally going to be lua but I wanted full parity with bukkit and somrthing easy for beginners, and also when I was trying lua it was very unstable, so I just ditched it and did c#, because ppl like loki and some others suggested it over lua and I lowkey didn't want to deal with lua anymore. This will have support for linux whenever the people make the main repo support linux fully.

@Un1q32
Copy link

Un1q32 commented Mar 16, 2026

Yea lua sucks, I've that's why wasm is the clear choice. if for no other reason than speed.
AFAIK Lua is only common because it's super easy to embed as a dependency and has almost no platform requirements, I think it will just work on anything with an ANSI C compiler.

@codeHusky
Copy link
Collaborator

we will not be using lua or wasm, this is a C# plugin api and the language has already been agreed to be acceptable by the maintainers

@SimplyCEO
Copy link

we will not be using lua

Lua has a better use for local modding of the game, like addon or datapack, while C# give a important role on server API.
Can future addons be implemented with this simple language?

@codeHusky
Copy link
Collaborator

I don't think we're interested in lua anything officially just from talking with existing maintainers, bindings for a more useful language would make sense. Using Lua just perpetuates the "learn a useless programming language" thing Roblox has been doing to lock kids into their platform and that's not smthn we really want to be part of.

@SimplyCEO
Copy link

learn a useless programming language

That is not true. Lua is widely used for modding. Roblox is the tip top of popularity for this language, but years before this is the preferred language for Garry's Mod, MTA, and many others sandbox games/modloaders.
Due to the simple language model, fast learning, dynamic execution of the scripts as well as JIT compiled objects on start.
I take Mineclonia as a very good example of a Minecraft clone made in Lua using the Minetest engine.

@codeHusky
Copy link
Collaborator

I'd rather encourage people to learn a language that gives them a skill useful in the job market.

@ModMaker101
Copy link
Contributor

I just want to add one more perspective in here:

Using .NET for the API gives us a LOT more flexibility long term. Since we are using .NET, we aren't just limited to C#, the entire .NET ecosystem becomes available. This would include F#, VB, and other languages that compile to .NET IL.

And not to mention, performance-wise, modern .NET is extremely fast and mature. .NET has JIT and AOT compilers, and from my experience, it is stable across platforms. WASM is cool, but it would introduce a second runtime, a sandbox model, and a lot of complexity for both maintainers and developers who would just want to create a mod or something. Most plugin developers already know C# from Unity or general .NET work, and C# is similar to Java in a sense, so it would be extremely easy for Java mod developers to come over to this, and .NET is strongly typed and would probably be much easier than forcing someone into WASM.

@SimplyCEO

This comment was marked as off-topic.

@eff3ry
Copy link

eff3ry commented Mar 17, 2026

Agreeing with C# here, very easy to learn and quite similar to java as an object oriented programming language

@eff3ry
Copy link

eff3ry commented Mar 17, 2026

id also like to see the Plugin API written straight in C# rather than in C++ just exposed on export, i think this would give us more flexiblity longterm and easier to document, the plugin API can still be loaded server side via interop then

@sylvessa sylvessa changed the title C# Plugin API (Very WIP) C# Server Plugin API (WIP) Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants