Skip to content

Commit

Permalink
doc updates + add loaded chan
Browse files Browse the repository at this point in the history
  • Loading branch information
wlwanpan committed Jan 2, 2021
1 parent 2811036 commit 84917db
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 75 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ go get github.com/wlwanpan/minecraft-wrapper

## Usage

- Usage with default configs:
- Starting the server and listening to game events:
```go
wpr := wrapper.NewDefaultWrapper("server.jar", 1024, 1024)
wpr.Start()
Expand All @@ -34,6 +34,16 @@ for {
}
```

- Broadcast a 'Hello' message once the server is loaded:
```go
wpr := wrapper.NewDefaultWrapper("server.jar", 1024, 1024)
wpr.Start()
defer wpr.Stop()

<-wpr.Loaded()
wpr.Say("Hello")
```

- Retrieving a player position from the [`/data get`](https://minecraft.gamepedia.com/Commands/data#get) command:
```go
out, err := wpr.DataGet("entity", PLAYER_NAME|PLAYER_UUID)
Expand All @@ -50,6 +60,8 @@ if err := wpr.SaveAll(true); err != nil {
}
```

For more examples, go to the examples dir from this repo.

Note: This package is developed and tested on Minecraft 1.16, though most functionalities (`Start`, `Stop`, `Seed`, ...) works across all versions. Commands like `/data get` was introduced in version 1.13 and might not work for earlier versions. :warning:

## Overview
Expand All @@ -58,7 +70,7 @@ Note: This package is developed and tested on Minecraft 1.16, though most functi
<img src="https://github.com/wlwanpan/minecraft-wrapper/blob/master/assets/architecture.png?raw=true" alt="Minecraft Wrapper Overview"/>
</p>

If you are interested in learning the basic inner working of the wrapper, you can check out my [Medium article](https://levelup.gitconnected.com/lets-build-a-minecraft-server-wrapper-in-go-122c087e0023) for more details.
If you are interested, you can visit this [Medium article](https://levelup.gitconnected.com/lets-build-a-minecraft-server-wrapper-in-go-122c087e0023) to learn some of the basic inner working of the wrapper.

## Commands :construction:

Expand All @@ -83,10 +95,11 @@ The following apis/commands are from the official minecraft gamepedia [list of c
- [ ] [Fill](https://minecraft.gamepedia.com/Commands/fill)
- [ ] [ForceLoad](https://minecraft.gamepedia.com/Commands/forceload)
- [ ] [Function](https://minecraft.gamepedia.com/Commands/function)
- [x] [GameEvents](https://pkg.go.dev/github.com/wlwanpan/minecraft-wrapper#Wrapper.GameEvents) - Returns a receive-only GameEvent channel (Unofficial)
- [x] [GameEvents](https://pkg.go.dev/github.com/wlwanpan/minecraft-wrapper#Wrapper.GameEvents) - A GameEvent channel of events happening during in-game (Unofficial)
- [ ] [GameMode](https://minecraft.gamepedia.com/Commands/gamemode)
- [ ] [GameRule](https://minecraft.gamepedia.com/Commands/gamerule)
- [x] [Kill](https://godoc.org/github.com/wlwanpan/minecraft-wrapper#Wrapper.Kill) - Terminates the Java Process (Unofficial)
- [x] [Loaded](https://godoc.org/github.com/wlwanpan/minecraft-wrapper#Wrapper.Loaded) - Returns bool from a read-only channel once the server is loaded (Unofficial)
- [x] [SaveAll](https://minecraft.gamepedia.com/Commands/save#save-all)
- [x] [SaveOff](https://minecraft.gamepedia.com/Commands/save#save-off)
- [x] [SaveOn](https://minecraft.gamepedia.com/Commands/save#save-on)
Expand All @@ -101,7 +114,6 @@ The following apis/commands are from the official minecraft gamepedia [list of c
- [ ] [Spectate](https://minecraft.gamepedia.com/Commands/spectate)
- [ ] [SpreadPlayers](https://minecraft.gamepedia.com/Commands/spreadplayers)
- [x] [Start](https://godoc.org/github.com/wlwanpan/minecraft-wrapper#Wrapper.Start) (Unofficial)
- [x] [StartAndWait](https://godoc.org/github.com/wlwanpan/minecraft-wrapper#Wrapper.StartAndWait) - Starts the minecraft server and waits until its fully loaded and 'online' (Unofficial)
- [x] [State](https://godoc.org/github.com/wlwanpan/minecraft-wrapper#Wrapper.State) - Returns the current state of the Wrapper (Unofficial)
- [x] [Stop](https://minecraft.gamepedia.com/Commands/stop)
- [ ] [StopSound](https://minecraft.gamepedia.com/Commands/stopsound)
Expand Down
43 changes: 43 additions & 0 deletions console_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package wrapper

import (
"bufio"
"io"
"os"
)

// testConsole provide a test console implementation of the interface Console,
// that reads from a test log file instead of a running java stdout. This is
// mainly due used for unit tests in test files.
type testConsole struct {
scnr *bufio.Scanner
}

func (tc *testConsole) Start() error {
return nil
}

func (tc *testConsole) Kill() error {
return nil
}

func (tc *testConsole) WriteCmd(c string) error {
return nil
}

func (tc *testConsole) ReadLine() (string, error) {
if tc.scnr.Scan() {
return tc.scnr.Text(), nil
}
return "", io.EOF
}

func newTestConsole(filename string) (*testConsole, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
return &testConsole{
scnr: bufio.NewScanner(file),
}, nil
}
8 changes: 4 additions & 4 deletions events/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package events
type EventType int

const (
TypeNil EventType = iota
TypeState EventType = iota
TypeCmd EventType = iota
TypeGame EventType = iota
TypeNil EventType = iota
TypeState
TypeCmd
TypeGame
)

const (
Expand Down
20 changes: 19 additions & 1 deletion logparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
"github.com/wlwanpan/minecraft-wrapper/events"
)

// LogParser is an interface func to decode any server log line
// to its respective event type. The returned events must be either:
// - Cmd: event holds data to be returned to a user command.
// - State: event affects the state of the wrapper.
// - Game: event related to in-game events, like a player died...
// - Nil: event that hold no value and usually ignored/
type LogParser func(string, int) (events.Event, events.EventType)

type logLine struct {
Expand Down Expand Up @@ -52,7 +58,7 @@ var gameEventToRegex = map[string]*regexp.Regexp{
events.PlayerUUID: regexp.MustCompile(`UUID of player (?s)(.*) is (?s)(.*)`),
events.PlayerSay: regexp.MustCompile(`<(?s)(.*)> (?s)(.*)`),
events.Seed: regexp.MustCompile(`Seed: (.*)`),
events.ServerOverloaded: regexp.MustCompile(`Can't keep up! Is the server overloaded? Running 2007ms or 40 ticks behind`),
events.ServerOverloaded: regexp.MustCompile(`Can't keep up! Is the server overloaded? Running (.*) or (.*) ticks behind`), // Test this?
events.TimeIs: regexp.MustCompile(`The time is (?s)(.*)`),
events.Version: regexp.MustCompile(`Starting minecraft server version (.*)`),
}
Expand Down Expand Up @@ -100,6 +106,8 @@ func logParserFunc(line string, tick int) (events.Event, events.EventType) {
return handleDataGetNoEntity(matches)
case events.Seed:
return handleSeed(matches)
case events.ServerOverloaded:
return handleServerOverloaded(matches, tick)
case events.DefaultGameMode:
return handleDefaultGameMode(matches)
case events.Banned:
Expand Down Expand Up @@ -238,6 +246,16 @@ func handleSeed(matches []string) (events.GameEvent, events.EventType) {
return sdEvent, events.TypeCmd
}

func handleServerOverloaded(matches []string, tick int) (events.GameEvent, events.EventType) {
soEvent := events.NewGameEvent(events.ServerOverloaded)
soEvent.Tick = tick
soEvent.Data = map[string]string{
"lag_time": matches[1],
"lag_tick": matches[2],
}
return soEvent, events.TypeGame
}

func handleDefaultGameMode(matches []string) (events.GameEvent, events.EventType) {
gmEvent := events.NewGameEvent(events.DefaultGameMode)
gmEvent.Data = map[string]string{
Expand Down
8 changes: 4 additions & 4 deletions logparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/wlwanpan/minecraft-wrapper/events"
)

func testParsedEvents(evs []events.Event, testfilename string, t *testing.T) {
func testParsedEvents(t *testing.T, evs []events.Event, testfilename string) {
testfile, err := os.Open(testfilename)
if err != nil {
t.Errorf("failed to load test file: %s", err)
Expand Down Expand Up @@ -37,7 +37,7 @@ func testParsedEvents(evs []events.Event, testfilename string, t *testing.T) {
}
}

func testParsedGameEvents(gevs []events.GameEvent, testfilename string, t *testing.T) {
func testParsedGameEvents(t *testing.T, gevs []events.GameEvent, testfilename string) {
testfile, err := os.Open(testfilename)
if err != nil {
t.Errorf("failed to load test file: %s", err)
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestServerStartLogs(t *testing.T) {
events.StartingEvent,
events.StartedEvent,
}
testParsedEvents(evs, "testdata/server_start_log.txt", t)
testParsedEvents(t, evs, "testdata/server_start_log")
}

func TestPlayerBasicLogs(t *testing.T) {
Expand Down Expand Up @@ -142,5 +142,5 @@ func TestPlayerBasicLogs(t *testing.T) {
},
},
}
testParsedGameEvents(gevs, "testdata/player_basic_log.txt", t)
testParsedGameEvents(t, gevs, "testdata/player_basic_log")
}
4 changes: 4 additions & 0 deletions outputs.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package wrapper

// DataGetOutput represents the structured data logged from the
// '/data get entity' command. Some fields might not be of the
// right or precise type since the decoder will coerse any value
// to either a string, int or float64 for simplicity.
type DataGetOutput struct {
Brain Brain `json:"Brain"`
HurtByTimestamp int `json:"HurtByTimestamp"`
Expand Down
2 changes: 1 addition & 1 deletion snbt/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

func TestFullDataGetDecode(t *testing.T) {
testData, err := ioutil.ReadFile("testdata/data_get_entity.txt")
testData, err := ioutil.ReadFile("testdata/data_get_entity")
if err != nil {
t.Errorf("failed to load testdata: %s", err)
return
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
[17:57:17] [Server thread/INFO]: player1 left the game
[17:57:20] [Server thread/INFO]: player2 lost connection: Disconnected
[17:57:20] [Server thread/INFO]: player2 left the game
[17:57:21] [Server thread/INFO]: Can't keep up! Is the server overloaded? Running 100ms or 1 ticks behind
File renamed without changes.
56 changes: 35 additions & 21 deletions wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ var (
// its respective event from the server logs within some timeframe. Hence
// no output could be decoded for the command.
ErrWrapperResponseTimeout = errors.New("response timeout")
// ErrWrapperNotOnline is returned when a commad is called but the wrapper
// is not 'online'. The minecraft server is not loaded and ready to process
// any commands.
ErrWrapperNotOnline = errors.New("not online")
)

var wrapperFsmEvents = fsm.Events{
Expand Down Expand Up @@ -61,8 +65,6 @@ var wrapperFsmEvents = fsm.Events{
},
}

type StateChangeFunc func(*Wrapper, events.Event, events.Event)

// Wrapper is the minecraft-wrapper core struct, representing an instance
// of a minecraft server (JE). It is used to manage and interact with the
// java process by proxying its stdin and stdout via the Console interface.
Expand All @@ -77,9 +79,12 @@ type Wrapper struct {
clock *clock
eq *eventsQueue
gameEventsChan chan (events.GameEvent)
startedChan chan bool
loadedChan chan bool
}

// NewDefaultWrapper returns a new instance of the Wrapper. This is
// the main method to use for your wrapper but if you wish to read
// and parse your own log lines to events, see 'NewWrapper'. This
func NewDefaultWrapper(server string, initial, max int) *Wrapper {
cmd := javaExecCmd(server, initial, max)
console := newConsole(cmd)
Expand All @@ -93,7 +98,7 @@ func NewWrapper(c Console, p LogParser) *Wrapper {
clock: newClock(),
eq: newEventsQueue(),
gameEventsChan: make(chan events.GameEvent, 10),
startedChan: make(chan bool, 1),
loadedChan: make(chan bool, 1),
}
wpr.newFSM()
return wpr
Expand All @@ -104,9 +109,12 @@ func (w *Wrapper) newFSM() {
WrapperOffline,
wrapperFsmEvents,
fsm.Callbacks{
"enter_state": func(ev *fsm.Event) {
if ev.Src == WrapperStarting && ev.Dst == WrapperOnline {
w.startedChan <- true
"enter_online": func(ev *fsm.Event) {
if ev.Src == WrapperStarting {
select {
case w.loadedChan <- true:
default:
}
}
},
},
Expand Down Expand Up @@ -157,21 +165,28 @@ func (w *Wrapper) handleCmdEvent(ev events.GameEvent) {
w.eq.push(ev)
}

func (w *Wrapper) writeToConsole(cmd string) error {
if w.State() != WrapperOnline {
return ErrWrapperNotOnline
}
return w.console.WriteCmd(cmd)
}

func (w *Wrapper) processClock() {
w.clock.start()
for {
<-w.clock.requestSync()
w.clock.resetLastSync()
w.console.WriteCmd("time query daytime")
w.writeToConsole("time query daytime")
}
}

func (w *Wrapper) processCmdToEvent(cmd, e string, timeout time.Duration) (events.GameEvent, error) {
evChan := w.eq.get(e)
if err := w.console.WriteCmd(cmd); err != nil {
if err := w.writeToConsole(cmd); err != nil {
return events.NilGameEvent, err
}

}
select {
case <-time.After(timeout):
return events.NilGameEvent, ErrWrapperResponseTimeout
Expand All @@ -188,7 +203,7 @@ func (w *Wrapper) processCmdToEvent(cmd, e string, timeout time.Duration) (event

func (w *Wrapper) processCmdToEventArr(cmd, e string, timeout time.Duration) ([]events.GameEvent, error) {
evChan := w.eq.get(e)
if err := w.console.WriteCmd(cmd); err != nil {
if err := w.writeToConsole(cmd); err != nil {
return nil, err
}

Expand Down Expand Up @@ -227,7 +242,7 @@ func (w *Wrapper) GameEvents() <-chan events.GameEvent {

func (w *Wrapper) Ban(player, reason string) error {
cmd := strings.Join([]string{"ban", player, reason}, " ")
return w.console.WriteCmd(cmd)
return w.writeToConsole(cmd)
}

func (w *Wrapper) BanList(t BanListType) ([]string, error) {
Expand Down Expand Up @@ -263,12 +278,12 @@ func (w *Wrapper) DataGet(t, id string) (*DataGetOutput, error) {
// DefaultGameMode sets the default game mode for new players joining.
func (w *Wrapper) DefaultGameMode(mode GameMode) error {
cmd := fmt.Sprintf("defaultgamemode %s", mode)
return w.console.WriteCmd(cmd)
return w.writeToConsole(cmd)
}

// DeOp removes a given player from the operator list.
func (w *Wrapper) DeOp(player string) error {
return w.console.WriteCmd("deop " + player)
return w.writeToConsole("deop " + player)
}

// Difficulty changes the game difficulty level of the world.
Expand All @@ -285,22 +300,22 @@ func (w *Wrapper) SaveAll(flush bool) error {
if flush {
cmd += " flush"
}
return w.console.WriteCmd(cmd)
return w.writeToConsole(cmd)
}

// SaveOn enables automatic saving. The server is allowed to write to the world files.
func (w *Wrapper) SaveOn() error {
return w.console.WriteCmd("save-on")
return w.writeToConsole("save-on")
}

// SaveOff disables automatic saving by preventing the server from writing to the world files.
func (w *Wrapper) SaveOff() error {
return w.console.WriteCmd("save-off")
return w.writeToConsole("save-off")
}

// Say sends the given message in the minecraft in-game chat.
func (w *Wrapper) Say(msg string) error {
return w.console.WriteCmd("say " + msg)
return w.writeToConsole("say " + msg)
}

// Seed returns the world seed.
Expand All @@ -323,9 +338,8 @@ func (w *Wrapper) Start() error {
return w.console.Start()
}

func (w *Wrapper) StartAndWait() (<-chan bool, error) {
err := w.Start()
return w.startedChan, err
func (w *Wrapper) Loaded() <-chan bool {
return w.loadedChan
}

// State returns the current state of the server, it can be one of:
Expand Down
Loading

0 comments on commit 84917db

Please sign in to comment.