-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support Minecraft server 1.13 and newer (#5733)
- Loading branch information
1 parent
01eecee
commit 3c57daf
Showing
9 changed files
with
611 additions
and
554 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,84 @@ | ||
# Minecraft Plugin | ||
# Minecraft Input Plugin | ||
|
||
This plugin uses the RCON protocol to collect [statistics](http://minecraft.gamepedia.com/Statistics) from a [scoreboard](http://minecraft.gamepedia.com/Scoreboard) on a | ||
Minecraft server. | ||
The `minecraft` plugin connects to a Minecraft server using the RCON protocol | ||
to collects scores from the server [scoreboard][]. | ||
|
||
To enable [RCON](http://wiki.vg/RCON) on the minecraft server, add this to your server configuration in the `server.properties` file: | ||
This plugin is known to support Minecraft Java Edition versions 1.11 - 1.14. | ||
When using an version of Minecraft earlier than 1.13, be aware that the values | ||
for some criterion has changed and may need to be modified. | ||
|
||
``` | ||
#### Server Setup | ||
|
||
Enable [RCON][] on the Minecraft server, add this to your server configuration | ||
in the [server.properties][] file: | ||
|
||
```conf | ||
enable-rcon=true | ||
rcon.password=<your password> | ||
rcon.port=<1-65535> | ||
``` | ||
|
||
To create a new scoreboard objective called `jump` on a minecraft server tracking the `stat.jump` criteria, run this command | ||
in the Minecraft console: | ||
|
||
`/scoreboard objectives add jump stat.jump` | ||
Scoreboard [Objectives][] must be added using the server console for the | ||
plugin to collect. These can be added in game by players with op status, | ||
from the server console, or over an RCON connection. | ||
|
||
Stats are collected with the following RCON command, issued by the plugin: | ||
|
||
`/scoreboard players list *` | ||
When getting started pick an easy to test objective. This command will add an | ||
objective that counts the number of times a player has jumped: | ||
``` | ||
/scoreboard objectives add jumps minecraft.custom:minecraft.jump | ||
``` | ||
|
||
### Configuration: | ||
Once a player has triggered the event they will be added to the scoreboard, | ||
you can then list all players with recorded scores: | ||
``` | ||
[[inputs.minecraft]] | ||
# server address for minecraft | ||
server = "localhost" | ||
# port for RCON | ||
port = "25575" | ||
# password RCON for mincraft server | ||
password = "replace_me" | ||
/scoreboard players list | ||
``` | ||
|
||
### Measurements & Fields: | ||
View the current scores with a command, substituting your player name: | ||
``` | ||
/scoreboard players list Etho | ||
``` | ||
|
||
*This plugin uses only one measurement, titled* `minecraft` | ||
### Configuration | ||
|
||
- The field name is the scoreboard objective name. | ||
- The field value is the count of the scoreboard objective | ||
```toml | ||
[[inputs.minecraft]] | ||
## Address of the Minecraft server. | ||
# server = "localhost" | ||
|
||
- `minecraft` | ||
- `<objective_name>` (integer, count) | ||
## Server RCON Port. | ||
# port = "25575" | ||
|
||
### Tags: | ||
## Server RCON Password. | ||
password = "" | ||
``` | ||
|
||
- The `minecraft` measurement: | ||
- `server`: the Minecraft RCON server | ||
- `player`: the Minecraft player | ||
### Metrics | ||
|
||
- minecraft | ||
- tags: | ||
- player | ||
- port (port of the server) | ||
- server (hostname:port, deprecated in 1.11; use `source` and `port` tags) | ||
- source (hostname of the server) | ||
- fields: | ||
- `<objective_name>` (integer, count) | ||
|
||
### Sample Queries: | ||
|
||
Get the number of jumps per player in the last hour: | ||
``` | ||
SELECT SPREAD("jump") FROM "minecraft" WHERE time > now() - 1h GROUP BY "player" | ||
SELECT SPREAD("jumps") FROM "minecraft" WHERE time > now() - 1h GROUP BY "player" | ||
``` | ||
|
||
### Example Output: | ||
|
||
``` | ||
$ telegraf --input-filter minecraft --test | ||
* Plugin: inputs.minecraft, Collection 1 | ||
> minecraft,player=notch,server=127.0.0.1:25575 jumps=178i 1498261397000000000 | ||
> minecraft,player=dinnerbone,server=127.0.0.1:25575 deaths=1i,jumps=1999i,cow_kills=1i 1498261397000000000 | ||
> minecraft,player=jeb,server=127.0.0.1:25575 d_pickaxe=1i,damage_dealt=80i,d_sword=2i,hunger=20i,health=20i,kills=1i,level=33i,jumps=264i,armor=15i 1498261397000000000 | ||
minecraft,player=notch,source=127.0.0.1,port=25575 jumps=178i 1498261397000000000 | ||
minecraft,player=dinnerbone,source=127.0.0.1,port=25575 deaths=1i,jumps=1999i,cow_kills=1i 1498261397000000000 | ||
minecraft,player=jeb,source=127.0.0.1,port=25575 d_pickaxe=1i,damage_dealt=80i,d_sword=2i,hunger=20i,health=20i,kills=1i,level=33i,jumps=264i,armor=15i 1498261397000000000 | ||
``` | ||
|
||
[server.properies]: https://minecraft.gamepedia.com/Server.properties | ||
[scoreboard]: http://minecraft.gamepedia.com/Scoreboard | ||
[objectives]: https://minecraft.gamepedia.com/Scoreboard#Objectives | ||
[rcon]: http://wiki.vg/RCON |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
package minecraft | ||
|
||
import ( | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/influxdata/telegraf/plugins/inputs/minecraft/internal/rcon" | ||
) | ||
|
||
var ( | ||
scoreboardRegexLegacy = regexp.MustCompile(`(?U):\s(?P<value>\d+)\s\((?P<name>.*)\)`) | ||
scoreboardRegex = regexp.MustCompile(`\[(?P<name>[^\]]+)\]: (?P<value>\d+)`) | ||
) | ||
|
||
// Connection is an established connection to the Minecraft server. | ||
type Connection interface { | ||
// Execute runs a command. | ||
Execute(command string) (string, error) | ||
} | ||
|
||
// Connector is used to create connections to the Minecraft server. | ||
type Connector interface { | ||
// Connect establishes a connection to the server. | ||
Connect() (Connection, error) | ||
} | ||
|
||
func NewConnector(hostname, port, password string) (*connector, error) { | ||
return &connector{ | ||
hostname: hostname, | ||
port: port, | ||
password: password, | ||
}, nil | ||
} | ||
|
||
type connector struct { | ||
hostname string | ||
port string | ||
password string | ||
} | ||
|
||
func (c *connector) Connect() (Connection, error) { | ||
p, err := strconv.Atoi(c.port) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rcon, err := rcon.NewClient(c.hostname, p) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
_, err = rcon.Authorize(c.password) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &connection{rcon: rcon}, nil | ||
} | ||
|
||
func NewClient(connector Connector) (*client, error) { | ||
return &client{connector: connector}, nil | ||
} | ||
|
||
type client struct { | ||
connector Connector | ||
conn Connection | ||
} | ||
|
||
func (c *client) Connect() error { | ||
conn, err := c.connector.Connect() | ||
if err != nil { | ||
return err | ||
} | ||
c.conn = conn | ||
return nil | ||
} | ||
|
||
func (c *client) Players() ([]string, error) { | ||
if c.conn == nil { | ||
err := c.Connect() | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
resp, err := c.conn.Execute("/scoreboard players list") | ||
if err != nil { | ||
c.conn = nil | ||
return nil, err | ||
} | ||
|
||
players, err := parsePlayers(resp) | ||
if err != nil { | ||
c.conn = nil | ||
return nil, err | ||
} | ||
|
||
return players, nil | ||
} | ||
|
||
func (c *client) Scores(player string) ([]Score, error) { | ||
if c.conn == nil { | ||
err := c.Connect() | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
resp, err := c.conn.Execute("/scoreboard players list " + player) | ||
if err != nil { | ||
c.conn = nil | ||
return nil, err | ||
} | ||
|
||
scores, err := parseScores(resp) | ||
if err != nil { | ||
c.conn = nil | ||
return nil, err | ||
} | ||
|
||
return scores, nil | ||
} | ||
|
||
type connection struct { | ||
rcon *rcon.Client | ||
} | ||
|
||
func (c *connection) Execute(command string) (string, error) { | ||
packet, err := c.rcon.Execute(command) | ||
if err != nil { | ||
return "", err | ||
} | ||
return packet.Body, nil | ||
} | ||
|
||
func parsePlayers(input string) ([]string, error) { | ||
parts := strings.SplitAfterN(input, ":", 2) | ||
if len(parts) != 2 { | ||
return []string{}, nil | ||
} | ||
|
||
names := strings.Split(parts[1], ",") | ||
|
||
// Detect Minecraft <= 1.12 | ||
if strings.Contains(parts[0], "players on the scoreboard") && len(names) > 0 { | ||
// Split the last two player names: ex: "notch and dinnerbone" | ||
head := names[:len(names)-1] | ||
tail := names[len(names)-1] | ||
names = append(head, strings.SplitN(tail, " and ", 2)...) | ||
} | ||
|
||
var players []string | ||
for _, name := range names { | ||
name := strings.TrimSpace(name) | ||
if name == "" { | ||
continue | ||
} | ||
players = append(players, name) | ||
|
||
} | ||
return players, nil | ||
} | ||
|
||
// Score is an individual tracked scoreboard stat. | ||
type Score struct { | ||
Name string | ||
Value int64 | ||
} | ||
|
||
func parseScores(input string) ([]Score, error) { | ||
if strings.Contains(input, "has no scores") { | ||
return []Score{}, nil | ||
} | ||
|
||
// Detect Minecraft <= 1.12 | ||
var re *regexp.Regexp | ||
if strings.Contains(input, "tracked objective") { | ||
re = scoreboardRegexLegacy | ||
} else { | ||
re = scoreboardRegex | ||
} | ||
|
||
var scores []Score | ||
matches := re.FindAllStringSubmatch(input, -1) | ||
for _, match := range matches { | ||
score := Score{} | ||
for i, subexp := range re.SubexpNames() { | ||
switch subexp { | ||
case "name": | ||
score.Name = match[i] | ||
case "value": | ||
value, err := strconv.ParseInt(match[i], 10, 64) | ||
if err != nil { | ||
continue | ||
} | ||
score.Value = value | ||
default: | ||
continue | ||
} | ||
} | ||
scores = append(scores, score) | ||
} | ||
return scores, nil | ||
} |
Oops, something went wrong.