sugarcane

command module
v0.0.0-...-2a33acd Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 20, 2021 License: MIT Imports: 8 Imported by: 0

README

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.

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL