Overview
Sugarcane is a server built for minigames. It uses a system of a server and a proxy, so that clients can
easily be switched between servers. The proxy handles all versioning, so the server code only needs to
deal with the latest version of packets. The server also acts as a library, so all of you own minigame
code can just import sugarcane (instead of some plugin system that is hard to setup).
Installing
To download and run the code, I recommend using the dev-server project.
This has some helpful utility scripts, like proxy.sh
, which just builds and runs the proxy. There are build
and run instructions in that repository.
In order to run a server, you need to write a basic main function in your own go project. The dev-server already
does this, and adds some utility functions that will make your life easier when developing.
Planned features
These are all the features I would like to complete before the 1.0 release. Check boxed items can be completed,
and bulleted items are for clarification.
Once 1.0 is released, I will make sure this api stays stable. I may change this planned feature list, but it will
stay within this general scope of goals. Currently, I will be changing the api a lot, so that I can get 1.0 right
on the first try. So, plugin code will probably break a lot.
- Add 1.14-1.16
- Simple to complete, the packets are mostly the same (already done with a skeleton of 1.14)
- Blocks are pretty simple as well, as each version just adds to 1.13
- Packets are completed
- Complete all blocks
- Rewrite most parts of entities
- Entity metadata changes so much between versions, it needs to be redone.
- Entity ids also should be registered, similar to packets.
- New entities should be able to be mapped to old entities, so that it works in 1.8
- Rewrite items
- Again, I need a versioned registry
- There needs to be a way to map 1.8 ids to new item ids (same thing as with blocks)
- Slightly change the way blocks are registered
- They need to be able to be registered for any version
- This is for user code, so that after all blocks are registered,
user code should be able to insert a 1.13 block, which should be
applied before all the new blocks.
- Improve commands
- Make the constructors simpler, with functions to change the parameters.
- This will make the command tree a lot cleaner to create, but will break the existing api.
- Parse commands before sending them to the handler.
- This will make command parsing much simpler, as the api will convert all the arguments into structs.
- This includes 1.8-1.12 tab complete, which should be easy to add once this is setup.
- Better Terrain
- Not needed, but it's very fun to work on
- I plan to add caves and structures at some point, not necessarily by 1.0.
Future plans (could change a lot)
- Plugins?
- I have already implemented part of this. The problem is with Go's plugin
system: it's very difficult to work with. Each plugin must have the entire
source of the library it's being imported to, as Go cannot dynamically link
binaries. Not only does this increase binary size by a lot, but it also means
that all plugins must be recompiled every time there is any change to the
original source. In my mind, this is not a reliable way to make plugins
(at least nothing like what bukkit/spigot can do). Since this is not a very
viable option, I have so far opted to make it not an option at all. If
anyone sees the need for this, I could probably re-implement it.
- Lighting?
- This is essential to minecraft's core look (caves and everything), and
is also based on a relativly algorithm. The problem is, it's very hard
to make it fast. Every time you update a block, you need to change the
lighting values for a large radius around it. This is why I don't want
to bother with it in 1.0: it's too hard to keep fast.
- It is also the same for all versions, so ideally it would be stored
speratly from chunk data, to reduce ram usage. Serializing it would be
easy, as the block light and sky light arrays are the same format for 1.8-1.16
- Entity AI
- This is also essential, and is a lot more trivial than lighting. A*
pathfinding is slow, but easy to implement.
- With an entity registry system, users should be able to register custom
entities that also look like a vanilla entity. But since they are defined
in user code, they should also be able to have their own unique ai and
interactions with the world.
Design
Vanilla minecraft servers have one main problem: you can't jump between servers.
This is essential for large projects, which need many small servers over multiple computers.
So instead of making something like BungeeCord, I opted to make
the sugarcane server use it's own packet format. It uses grpc, and has things like encryption built into
the packet format from the start. This means that the server has to do a lot less work with custom packets,
and it can use a udp implementation of grpc if needed (it doesn't at the moment). The proxy then handles
converting these grpc packets into minecraft packets, and sending them to the client.
The proxy doesn't just convert packets. It also has the ability to switch the client to a different server.
Any server can send a custom "switch server" packet to the proxy, which will be intercepted, and not sent
to the client. Within this packet is info such as the new ip, port, etc. The proxy then starts a new grpc
connection with the new server, and sends a switch dimension packet to the client. Once the loading screen
clears, the client will see themselves on a new server, while still connected to the same proxy.
Flags
Run sugarcane -help
to see available flags. sugarcane
is the binary generated for this project, and
that is the proxy. So all of those flags are documented from the perspective of being in the middle of a
client and a server. Also note that -cluster
and -group
are meant to be used with an AWS ECS cluster.
In production, an ECS cluster makes sure enough proxies/servers are running at all times. These flags are
used so that the proxy can search for servers within a cluster and connect the client to one of them.
Plugins
NOTE: Most of these concepts are planned. Currently, you can add blocks, but there is no sort of translation
into vanilla blocks, so it will just show up as invalid in the client.
I was originally going to write a plugin system for this server, using go's plugin
package. However,
as everything is statically linked, and I have a proxy setup now, it doesn't seem worthwhile to me.
So I have instead opted to make the server package included into your own main package, where you can
then define minigame/plugin functionality. You can of course write extra utility libraries, that you
include with the server, so that you don't need to write everything from scratch with every new minigame.
I will try to include all of the functionality of those libraries in the core server, as I want it to be
as easy as possible to write new games.
One of the unique things about this server are how items/blocks are registered. Firstly, this server will
allow you to add your own blocks/items, that will look like a vanilla block/item to the client. This means
you can add custom functionality to some blocks, as they will still look unique on the server. So a teleporter
compass could be added as it's own item, and it would look like a new item on the server. But to the clients,
it would just look like an enchanted compass.
With all of this is mind, there is a very common type of item in vanilla minecraft: Block items. These are
items you hold, that have a 3d model, and place a block when you right click. This makes up a large portion
of all items, and needs to be easy to register. So I opted to make the item package depend on blocks being
loaded before items could be loaded. This means there are two loading phases: the block phase, where all
you can do is add new blocks, and the item/biomes/entites phase, where you can register everything else.
See the example main.go file for what registering blocks/items looks like.
package main
import (
"gitlab.com/macmv/sugarcane/minecraft"
)
func main() {
mc := minecraft.New(false, 12, false, "default")
// This registers vanilla blocks,
// allowing you to add your own blocks.
mc.Init()
/* Add your own blocks here */
// This finalizes all blocks,
// and allows items and everything
// else to be registered.
mc.FinalizeBlocks()
/* Add your own items/biomes/entities here */
// This loads/generates all worlds,
// and finalizes all other registries.
mc.FinishLoad()
/* Load config files, connect to
other servers, etc. */
// This is a blocking call,
// so this should always be at
// the end of your main function
// This only returns when the
// server has closed.
mc.StartUpdateLoop(":8483")
}
This starts a bare bones server, that has no added functionality.
See the docs for more info on the Minecraft type.
Events
If you want your minigame to actually do anything, you can attach functions to events. Each one of these
events is called whenever a client does something. For example, you could attach a function to the player-join
event:
package main
import (
"gitlab.com/macmv/sugarcane/minecraft"
"gitlab.com/macmv/sugarcane/minecraft/util"
"gitlab.com/macmv/sugarcane/minecraft/player"
)
func main() {
mc := minecraft.New(false, 12, false, "default")
mc.Init()
mc.FinalizeBlocks()
mc.FinishLoad()
mc.On("player-join", onPlayerJoin)
mc.StartUpdateLoop(":8483")
}
func onPlayerJoin(player *player.Player) {
player.SendChat(util.NewChatFromString("Hello " + player.Name()))
}
When you call mc.On
, you are adding that function pointer to a list of handlers for that event. You can add
any number of handlers to a given event. Each time that event is triggered, all of those handlers will be called.
If any of the handlers returns true, then the event is cancelled, but all of the handlers are still called.
Note that the On
function takes a function pointer as the second argument. This function must have the same args
as the event does. See the List of Events for which arguments to use. The server will log an error when the event
is triggered if the args do not match.
Each event can return anything, but it will be ignored if it is not just a single bool. If the function does return
a bool, then that is treated as a cancel flag. So if you were to add a block-change
handler, and return true all
the time, then it would cancel all block change events for the whole world.
List of events
Evant name Args
ready func()
player-join func(player *player.Player)
player-leave func(player *player.Player)
block-change func(new_block_type *world.BlockType, block *world.BlockState, player *player.Player)
right-click func(is_main_hand bool, item *item.ItemStack, block *world.BlockState, player *player.Player)
left-click func(block *world.BlockState, player *player.Player)
client-window func(slot int32, button byte, mode int32, item *item.ItemStack, player *player.Player)
Notes:
- Click events (
right-click
, left-click
) are called before any other events, such as block-change
. If the click
event is cancelled, the following events are never called.
- Some events, such as
player-join
and player-leave
, cannot be cancelled. In those situations, the return value
from the handler is ignored.
Commands
Commands are quite complicated since 1.13. Every single element of a command is a node on a tree, which can have
cycles back on itself, and multiple parents of one node. Each one of these nodes is a tab completion rule for the
client, and is also a validation rule for the server. Once the commands api is finished, commands will be syntax
checked before they are ever passed to user code. For now, no commands are syntax checked, and they are always
passed to user code. Here is how to implement a simple /say
command:
package main
import (
"gitlab.com/macmv/sugarcane/minecraft"
"gitlab.com/macmv/sugarcane/minecraft/util"
"gitlab.com/macmv/sugarcane/minecraft/world"
"gitlab.com/macmv/sugarcane/minecraft/player"
"gitlab.com/macmv/sugarcane/minecraft/event/command"
)
func main() {
mc := minecraft.New(false, 12, false, "default")
mc.Init()
mc.FinalizeBlocks()
mc.FinishLoad()
mc.Events.AddCommand("say", handle_say,
// See https://wiki.vg/Command_Data
// for more info on "brigadier:string" and []byte{0x02}
command.NewArgumentNode("message", "brigadier:string", []byte{0x02}, true, false, false),
)
mc.StartUpdateLoop(":8483")
}
func handle_say(wm *world.WorldManager, player *player.Player, args []string) bool {
if len(args) < 1 {
player.SendChat(util.NewChatFromStringColor("Please enter a valid message!", "red"))
return false
}
player.SendChat(util.NewChatFromString(args[0]))
return true
}
The key part of this is the mc.Events.AddCommand
function. This takes a string, which is the function name,
a function pointer, which will be called every time the command is run. Lastly, it takes any number of command
nodes. These nodes are documented here. There are two different types of
nodes you can create: argument nodes, and literal nodes. Literal nodes are for a set number of keywords.
For example, if I had the command /fill circle 10 4 5
, I would have a literal node, which is just named
circle
, and then a position node, which is named position
. I could also have a node next to the circle
literal, which would be another literal node named rect
. This would then have two positions following it.
You can read more about this in the node documentation listed above.
Creating nodes:
NewArgumentNode(name, parser string, properties []byte, executable, redirect, suggestion bool)
NewLiteralNode(name string, executable, redirect bool)
If you want to make a chain of nodes, the node.AddChild()
function will return itself, so it is easy to chain commands. Example:
minecraft.Events.AddCommand("fill", handle_fill,
command.NewLiteralNode("circle", false, false).AddChild(
command.NewArgumentNode("center", "minecraft:block_pos", []byte{}, false, false, false).AddChild(
command.NewArgumentNode("radius", "brigadier:float", []byte{0x01}, 0, 0, 0, 0}, false, false, false).AddChild(
command.NewArgumentNode("block", "minecraft:block_state", []byte{}, true, false, false),
),
),
),
command.NewLiteralNode("rectangle", false, false).AddChild(
command.NewArgumentNode("position_1", "minecraft:block_pos", []byte{}, false, false, false).AddChild(
command.NewArgumentNode("position_2", "minecraft:block_pos", []byte{}, false, false, false).AddChild(
command.NewArgumentNode("block", "minecraft:block_state", []byte{}, true, false, false),
),
),
),
)
This tree will result in these posible commands, just to name a few:
/fill circle ~ ~ ~ 5.2 minecraft:stone
/fill circle 10 20 3 2 minecraft:oak_log
/fill rectangle ~ ~ ~ ~10 ~ ~10 minecraft:black_wool
/fill rectangle 1 2 3 4 5 6 minecraft:oak_sapling
This is all built into the minecraft client, so all of the positions and block states will tab complete very
nicely. This will also show errors to the user as they are typing, so that the commands are usually only sent
to the server if they are formatted correctly.
Note that right now, any time the user sends a command, it will call the command handler. This is not ideal,
as that means the command handler has to validate the command. In the future, the sugarcane server will parse
commands beforehand, based on the note tree. Then, the parsed positions and other types will be passsed in as
their golang types, so the handlers won't need to parse strings into ints.