Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6b6e1ab9 | ||
|
|
56c97a2b3f | ||
|
|
9063a0328d | ||
|
|
84b28d4c12 | ||
|
|
30cfc507ba | ||
|
|
d84618c116 | ||
|
|
5a55520126 | ||
|
|
8ac5e00c8e | ||
|
|
d46c678b89 | ||
|
|
0b714edcf5 | ||
|
|
45151add64 | ||
|
|
b90830881f | ||
|
|
94301a4a50 | ||
|
|
aa5d15a4dc | ||
|
|
d388e2c350 | ||
|
|
126ff5bca0 | ||
|
|
a347aedfc2 | ||
|
|
e324a95727 | ||
|
|
7f878f40a1 | ||
|
|
7baa9990cb | ||
|
|
5c5c391aa6 | ||
|
|
b966925212 | ||
|
|
efd7049859 | ||
|
|
3ef9446f50 | ||
|
|
68612969c9 | ||
|
|
b4712ff5c6 | ||
|
|
eab050e291 | ||
|
|
a8b090227b | ||
|
|
201e0dfa44 | ||
|
|
666f250b3e | ||
|
|
b2784574bd | ||
|
|
c0b1501f23 | ||
|
|
4de76d8023 | ||
|
|
f08684fb67 | ||
|
|
9e4f9409c5 | ||
|
|
4d3fbed957 | ||
|
|
b338888b58 | ||
|
|
4956c1d874 | ||
|
|
d4fe362d9f | ||
|
|
5d8ff870df | ||
|
|
58c9a60236 | ||
|
|
3e5b141ec7 | ||
|
|
efeb5ecda6 | ||
|
|
624e8ad4de | ||
|
|
764cfd8c4d | ||
|
|
d43ff7f83d | ||
|
|
a39999b344 | ||
|
|
8ee48e1a45 | ||
|
|
bf6affeb97 | ||
|
|
ede9a47ca2 | ||
|
|
2fb290572a | ||
|
|
021b8ddc1a | ||
|
|
05e8da2ff4 | ||
|
|
a8936d92a5 | ||
|
|
c56898e787 | ||
|
|
c483bf9553 | ||
|
|
b5ee17d1ae | ||
|
|
57cef25e27 | ||
|
|
148302c94d | ||
|
|
02c402251d | ||
|
|
c800c5d471 | ||
|
|
477f51cce7 | ||
|
|
60103ebf47 | ||
|
|
21eeebf0f7 | ||
|
|
bb6f6548d5 | ||
|
|
d6c07cf428 | ||
|
|
e98921755d | ||
|
|
60383fede1 | ||
|
|
3bfa70d384 | ||
|
|
60147fd4b6 | ||
|
|
fe51287cc6 | ||
|
|
a44c5f8606 | ||
|
|
d5979b2c3d | ||
|
|
4b95ea9ae2 | ||
|
|
a80fd29297 | ||
|
|
589f35d147 | ||
|
|
c7b5f6ba0f | ||
|
|
0600315e69 | ||
|
|
12005df68b | ||
|
|
600842a679 | ||
|
|
c93d907d46 | ||
|
|
be6d2fb461 | ||
|
|
b2df88203d | ||
|
|
2ed4498181 | ||
|
|
0316b0aa09 | ||
|
|
62d12386e1 | ||
|
|
00ea02a529 | ||
|
|
e5231dfdb6 | ||
|
|
3b1b436ecf | ||
|
|
47180bf650 | ||
|
|
325db98b99 | ||
|
|
ceae543352 | ||
|
|
aef9615476 | ||
|
|
5d3860b91e | ||
|
|
94d15258d8 | ||
|
|
ab03b36b4f | ||
|
|
da0c3c6f14 | ||
|
|
b01e482bc5 | ||
|
|
c560966177 | ||
|
|
66f86c5599 | ||
|
|
5a2bfcb1c1 | ||
|
|
b34f8d68ff | ||
|
|
c98472165b | ||
|
|
4888a07ebb | ||
|
|
a9bf30a609 | ||
|
|
35030601e1 | ||
|
|
66f32b4cd2 | ||
|
|
e6d08dccbb | ||
|
|
8a4f203621 | ||
|
|
69e230a79c | ||
|
|
1a5f4f4f8e | ||
|
|
d0844dd6b5 | ||
|
|
d8e36aa6fa | ||
|
|
6781e4c16d | ||
|
|
fccba0248a | ||
|
|
52b3c1ce17 | ||
|
|
3511cd2699 | ||
|
|
6dddccab7f | ||
|
|
9109d77f98 | ||
|
|
9eec1e8370 | ||
|
|
96481da0a4 | ||
|
|
159437c1ca | ||
|
|
3d34ad59e9 | ||
|
|
4878c6cc7b | ||
|
|
719a3bacd8 | ||
|
|
ce0c58e7c5 | ||
|
|
8a4e44760e | ||
|
|
e810a60848 | ||
|
|
0981339028 | ||
|
|
7746dd8ee9 | ||
|
|
378b78312e | ||
|
|
bd3139754d | ||
|
|
841d91b91d | ||
|
|
895ec53698 | ||
|
|
f6001c8b22 | ||
|
|
d87c362270 | ||
|
|
236e414242 | ||
|
|
5222390b65 | ||
|
|
dc04fdc7d0 | ||
|
|
8de34dfda1 | ||
|
|
997594d7c3 | ||
|
|
c83a777eac | ||
|
|
65fea61667 | ||
|
|
c2777f2ca9 | ||
|
|
dcb42f1970 | ||
|
|
7a2081f0a0 | ||
|
|
acc583f694 | ||
|
|
76409d07a3 | ||
|
|
bb89730781 | ||
|
|
9f6b1f6bcf | ||
|
|
1b8a9677c6 | ||
|
|
79ab13ca4b | ||
|
|
3cc414185a | ||
|
|
fe4a17dce2 | ||
|
|
891551f9dd | ||
|
|
8fb48c6ea9 | ||
|
|
ec25c28061 | ||
|
|
45287f47d2 | ||
|
|
4cc4b6af5b | ||
|
|
09e6760d61 | ||
|
|
ca74a3c3d4 | ||
|
|
fbc3693219 | ||
|
|
fb69fd93ec | ||
|
|
a3a3c3714e | ||
|
|
9294631c6c | ||
|
|
293f0b1eed | ||
|
|
272dcb4d36 | ||
|
|
abb0fa4d37 | ||
|
|
e5212624af | ||
|
|
35772aca03 | ||
|
|
819d89b26e | ||
|
|
958102e43c | ||
|
|
f749d3bfc5 | ||
|
|
d207cf52cf | ||
|
|
420ec5611c | ||
|
|
4bfaf9b7c5 | ||
|
|
48beb861e8 | ||
|
|
9fd2f517d9 | ||
|
|
1b5af63b75 | ||
|
|
dfb847eb3e | ||
|
|
322bd77f7f | ||
|
|
aefe850281 | ||
|
|
57f88f2176 | ||
|
|
8dd566c0da | ||
|
|
e386a45d96 | ||
|
|
8f771166a6 | ||
|
|
5d6a1a25ea | ||
|
|
ca33185f20 | ||
|
|
e921e9e9d2 | ||
|
|
7337068d86 | ||
|
|
9983062522 | ||
|
|
4fd5324064 | ||
|
|
df0e5e354b | ||
|
|
23b5a5d9b7 | ||
|
|
46770ed980 | ||
|
|
6179c5f6b6 | ||
|
|
f758a10575 | ||
|
|
41d1ab4c45 | ||
|
|
032fa26927 | ||
|
|
f7401a287e | ||
|
|
2d1e8ef4f6 | ||
|
|
2802cc36ce | ||
|
|
ea024fe808 |
@@ -0,0 +1,6 @@
|
||||
**/*_test.go export-ignore
|
||||
**/testdata export-ignore
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.tmcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mca filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,35 @@
|
||||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout with LFS
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.26.2'
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Vet
|
||||
run: go vet -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.devres/
|
||||
.idea/
|
||||
.failed_packets/
|
||||
tools/.*/
|
||||
.vscode/
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "data/minecraft-data"]
|
||||
path = data/minecraft-data
|
||||
url = git@github.com:PrismarineJS/minecraft-data.git
|
||||
@@ -0,0 +1,5 @@
|
||||
# STROM
|
||||
|
||||
# THIS IS WORK IN PROGRESS AND THE API WILL BREAK AS LONG AS THE THE PACKAGE VERSION IS v0.x.x
|
||||
|
||||
feel free to contact me on [discord](https://discord.gg/n2Vjc2vcMf)
|
||||
@@ -1,40 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
)
|
||||
|
||||
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
|
||||
// You must close the connection when you're done with it
|
||||
func Connect(addr string) (ret *proto.Conn, err error) {
|
||||
s, err := Status(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret, err = ConnectVersionLess(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ret.SetProtocolVersion(s.Version.Protocol)
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectVersionLess connects to the given address without setting the version you should set it
|
||||
// when using this function.
|
||||
// You must close the connection when you're done with it
|
||||
func ConnectVersionLess(addr string) (ret *proto.Conn, err error) {
|
||||
ret = &proto.Conn{}
|
||||
ret.SetState(proto_base.Handshaking)
|
||||
ret.SetCompressionThreshold(-1)
|
||||
ret.Actor = proto_base.Client
|
||||
ret.Conn, err = net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.R = ret.Conn
|
||||
ret.W = ret.Conn
|
||||
return
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DNSCheckFunc func(addr string) error
|
||||
|
||||
// DoDns resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
func DoDns(in string) (out string, host string, portN uint16, err error) {
|
||||
return DoDNSChecked(in, nil)
|
||||
}
|
||||
|
||||
// DoDNSChecked resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It also optionally accepts a function to "check" the address
|
||||
func DoDNSChecked(in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
|
||||
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if f == nil {
|
||||
f = func(addr string) error { return nil }
|
||||
}
|
||||
|
||||
err = f(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN = uint16(porti)
|
||||
outIP, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
outIP, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
return
|
||||
}
|
||||
|
||||
// DoDnsSimple resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It does not check the address
|
||||
func DoDnsSimple(in string) (out string, err error) {
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN := uint16(porti)
|
||||
_, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out = in
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
out = host + ":" + strconv.Itoa(int(portN))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("api", flag.ContinueOnError)
|
||||
var tokenFlag = cmd.String("token", os.Getenv("MC_TOKEN"), "token to use")
|
||||
|
||||
var renameFlag = cmd.String("rename", "", "rename the player")
|
||||
|
||||
var uuidFlag = cmd.String("uuid", "", "get data about player with uuid")
|
||||
var nameFlag = cmd.String("name", "", "get data about player with name")
|
||||
var unsignedFlag = cmd.Bool("unsigned", true, "use unsigned mode for properties signatures")
|
||||
|
||||
var PleaseProvideArgumentErr = errors.New("please provide an argument")
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a := &api.Account{}
|
||||
if *tokenFlag != "" {
|
||||
a, err = api.NewAccountFromYGG(*tokenFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var data any
|
||||
if *renameFlag != "" {
|
||||
data, err = a.ChangeName(*renameFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if *uuidFlag != "" || *nameFlag != "" {
|
||||
if *nameFlag != "" {
|
||||
var idName api.IdName
|
||||
idName, err = api.NameToId(*nameFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*uuidFlag = idName.Id
|
||||
}
|
||||
data, err = api.Profile(*uuidFlag, *unsignedFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
slog.Info("account", "name", a.Name, "uuid", a.Uuid)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(os.Stdout).Encode(data)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cmd_util
|
||||
|
||||
import "github.com/admin-else/strom/mc/api"
|
||||
|
||||
func Account(s string) (a *api.Account, err error) {
|
||||
if len(s) > 16 {
|
||||
return api.NewAccountFromYGG(s)
|
||||
}
|
||||
a = api.NewOfflineAccount(s)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
mcdata "github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto_generated"
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
// strom data 1.21.11 blocks id 0 name => air
|
||||
// {
|
||||
// "id": 0,
|
||||
// "name": "air",
|
||||
// "displayName": "Air",
|
||||
// "hardness": 0.0,
|
||||
// "resistance": 0.0,
|
||||
// "stackSize": 64,
|
||||
// "diggable": false,
|
||||
// "material": "default",
|
||||
// "transparent": true,
|
||||
// "emitLight": 0,
|
||||
// "filterLight": 0,
|
||||
// "defaultState": 0,
|
||||
// "minStateId": 0,
|
||||
// "maxStateId": 0,
|
||||
// "states": [],
|
||||
// "drops": [],
|
||||
// "boundingBox": "empty"
|
||||
// },
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
var dataValues any
|
||||
switch len(args) {
|
||||
case 0: // print available versions
|
||||
dataValues = proto_generated.SupportedVersions
|
||||
case 1: // print available data for version
|
||||
dataValues = slices.Collect(maps.Keys(mcdata.Paths.Data[args[0]]))
|
||||
case 2:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
case 3:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataSlices []any
|
||||
dataSlices, err = util.AssertTypeError[[]any](dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var result = make([]any, len(dataSlices))
|
||||
for _, v := range dataSlices {
|
||||
var dataMap map[string]any
|
||||
dataMap, err = util.AssertTypeError[map[string]any](v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var vToAppend any
|
||||
vToAppend, err = util.MapGetError(dataMap, args[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = append(result, vToAppend)
|
||||
}
|
||||
dataValues = result
|
||||
case 4, 5:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataSlices []any
|
||||
dataSlices, err = util.AssertTypeError[[]any](dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, v := range dataSlices {
|
||||
var dataMap map[string]any
|
||||
dataMap, err = util.AssertTypeError[map[string]any](v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if v2, ok := dataMap[args[2]]; ok {
|
||||
if args[3] != fmt.Sprint(v2) {
|
||||
continue
|
||||
}
|
||||
|
||||
dataValues = dataMap
|
||||
found = true
|
||||
if len(args) == 5 {
|
||||
dataValues, err = util.MapGetError(dataMap, args[4])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = fmt.Errorf("not found")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("too many arguments")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", " ")
|
||||
err = e.Encode(dataValues)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package extract_mca
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/level/anvil"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-nbt", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The MCA file to extract")
|
||||
IndexXFlag = cmd.Int("x", 0, "The x chunk coordinate")
|
||||
IndexZFlag = cmd.Int("z", 0, "The z chunk coordinate")
|
||||
ToFlag = cmd.String("to", "mca-chunk.nbt", "The output file")
|
||||
)
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t, err := mca.TimeAt(f, int32(*IndexXFlag), int32(*IndexZFlag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Chunk last written", "time", t)
|
||||
n, err := mca.ChunkAt(f, int32(*IndexXFlag), int32(*IndexZFlag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = nbt.WriteUnstructuredFilePath(*ToFlag, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
|
||||
"github.com/admin-else/strom/cmd/strom/api"
|
||||
"github.com/admin-else/strom/cmd/strom/data"
|
||||
"github.com/admin-else/strom/cmd/strom/extract_mca"
|
||||
"github.com/admin-else/strom/cmd/strom/mca_to_tmcpr"
|
||||
"github.com/admin-else/strom/cmd/strom/messenger"
|
||||
"github.com/admin-else/strom/cmd/strom/offline_uuid"
|
||||
"github.com/admin-else/strom/cmd/strom/packet_info"
|
||||
"github.com/admin-else/strom/cmd/strom/packet_inspector"
|
||||
"github.com/admin-else/strom/cmd/strom/print_nbt"
|
||||
"github.com/admin-else/strom/cmd/strom/print_replay"
|
||||
"github.com/admin-else/strom/cmd/strom/raw_capture"
|
||||
"github.com/admin-else/strom/cmd/strom/registry_data_hunter"
|
||||
"github.com/admin-else/strom/cmd/strom/serve_tmcpr"
|
||||
"github.com/admin-else/strom/cmd/strom/serve_world"
|
||||
"github.com/admin-else/strom/cmd/strom/status"
|
||||
)
|
||||
|
||||
var ExpectedASubcommandErr = errors.New("expected a subcommand")
|
||||
var UnknownSubcommandErr = errors.New("unknown subcommand try 'strom help' for a list of subcommands")
|
||||
|
||||
func ToolVersion() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "devel"
|
||||
}
|
||||
return info.Main.Version
|
||||
}
|
||||
|
||||
func ToolSum() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return info.Main.Sum
|
||||
}
|
||||
|
||||
func Version(args []string) error {
|
||||
fmt.Println("strom cli ", ToolVersion(), " (", ToolSum(), ")")
|
||||
return nil
|
||||
}
|
||||
|
||||
var subcommands = map[string]func(args []string) error{
|
||||
"offline-uuid": offline_uuid.Run,
|
||||
"packet-info": packet_info.Run,
|
||||
"print-nbt": print_nbt.Run,
|
||||
"status": status.Run,
|
||||
"packet-spy": packet_inspector.Run,
|
||||
"api": api.Run,
|
||||
"version": Version,
|
||||
"extract-mca": extract_mca.Run,
|
||||
"data": data.Run,
|
||||
"print-replay": print_replay.Run,
|
||||
"raw-capture": raw_capture.Run,
|
||||
"registry-data-hunter": registry_data_hunter.Run,
|
||||
"serve-world": serve_world.Run,
|
||||
"mca-to-tmcpr": mca_to_tmcpr.Run,
|
||||
"serve-tmcpr": serve_tmcpr.Run,
|
||||
"messenger": messenger.Run,
|
||||
}
|
||||
|
||||
func Help(args []string) (err error) {
|
||||
subcommandNames := slices.Collect(maps.Keys(subcommands))
|
||||
slices.Sort(subcommandNames)
|
||||
fmt.Println("Available subcommands", subcommandNames)
|
||||
return
|
||||
}
|
||||
|
||||
func mainE() (err error) {
|
||||
subcommands["help"] = Help
|
||||
args := os.Args
|
||||
//args = []string{"TEST", "messenger", "-addr", "localhost"}
|
||||
|
||||
if len(args) < 2 {
|
||||
err = ExpectedASubcommandErr
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := subcommands[args[1]]
|
||||
if !ok {
|
||||
err = UnknownSubcommandErr
|
||||
return
|
||||
}
|
||||
err = f(args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := mainE()
|
||||
if err != nil {
|
||||
slog.Error("exiting", "err", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package mca_to_tmcpr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/level"
|
||||
"github.com/admin-else/strom/mc/level/anvil"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto/replay"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("mca-to-tmcpr", flag.ContinueOnError)
|
||||
worldPath = cmd.String("world", "", "Path to world directory")
|
||||
output = cmd.String("output", "", "Output .tmcpr file path")
|
||||
radius = cmd.Int("radius", 14, "Chunk radius around spawn")
|
||||
spawnX = cmd.Float64("spawn-x", 0.5, "Spawn X coordinate")
|
||||
spawnY = cmd.Float64("spawn-y", 100, "Spawn Y coordinate")
|
||||
spawnZ = cmd.Float64("spawn-z", 0.5, "Spawn Z coordinate")
|
||||
)
|
||||
|
||||
var FullLight [][]uint8
|
||||
|
||||
func init() {
|
||||
light := make([]uint8, 2048)
|
||||
for i := range light {
|
||||
light[i] = 0xFF
|
||||
}
|
||||
sectionsPerChunk := 384 / 16
|
||||
FullLight = make([][]uint8, sectionsPerChunk)
|
||||
for i := range sectionsPerChunk {
|
||||
FullLight[i] = light
|
||||
}
|
||||
}
|
||||
|
||||
func GetChunkFromMca(x, z int32, version string) (b []byte, err error) {
|
||||
xmca := util.FloorDiv(x, anvil.ChunksWidthRegionFile)
|
||||
zmca := util.FloorDiv(z, anvil.ChunksWidthRegionFile)
|
||||
x = x % anvil.ChunksWidthRegionFile
|
||||
if x < 0 {
|
||||
x += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
z = z % anvil.ChunksWidthRegionFile
|
||||
if z < 0 {
|
||||
z += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
|
||||
f, err := os.Open(fmt.Sprintf("./.devres/anvil_testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n *nbt.Tag
|
||||
n, err = mca.ChunkAt(f, x, z)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var chunk anvil.Chunk
|
||||
err = nbt.Format.Decode(n.Value, &chunk)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var packetChunk *level.Chunk
|
||||
packetChunk, err = chunk.ToStorage(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = packetChunk.WriteChunkData(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
err := cmd.Parse(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *worldPath == "" {
|
||||
slog.Error("no world path specified")
|
||||
return nil
|
||||
}
|
||||
if *output == "" {
|
||||
slog.Error("no output path specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
outFile, err := os.Create(*output)
|
||||
if err != nil {
|
||||
slog.Error("failed to create output file", "error", err)
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
w := replay.NewWriter(outFile)
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketLogin{
|
||||
EntityId: 73,
|
||||
IsHardcore: false,
|
||||
WorldNames: []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"},
|
||||
MaxPlayers: 100,
|
||||
ViewDistance: 10,
|
||||
SimulationDistance: 10,
|
||||
ReducedDebugInfo: false,
|
||||
EnableRespawnScreen: true,
|
||||
DoLimitedCrafting: false,
|
||||
WorldState: v1_21_11.PlayToClientSpawnInfo{
|
||||
Dimension: 0,
|
||||
Name: "minecraft:overworld",
|
||||
HashedSeed: -8365436745629239539,
|
||||
Gamemode: "survival",
|
||||
PreviousGamemode: 0xff,
|
||||
IsDebug: false,
|
||||
IsFlat: false,
|
||||
Death: nil,
|
||||
PortalCooldown: 0,
|
||||
SeaLevel: 63,
|
||||
},
|
||||
EnforcesSecureChat: false,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write login packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketPosition{
|
||||
TeleportId: 0,
|
||||
X: *spawnX,
|
||||
Y: *spawnY,
|
||||
Z: *spawnZ,
|
||||
Dx: 0,
|
||||
Dy: 0,
|
||||
Dz: 0,
|
||||
Yaw: 0,
|
||||
Pitch: 0,
|
||||
Flags: v1_21_11.PlayToClientPositionUpdateRelatives{},
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write position packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
chunkX := int32(*spawnX) >> 4
|
||||
chunkZ := int32(*spawnZ) >> 4
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketUpdateViewPosition{
|
||||
ChunkX: chunkX,
|
||||
ChunkZ: chunkZ,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write view position packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketGameStateChange{
|
||||
Reason: "level_chunks_load_start",
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write game state packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
radius32 := int32(*radius)
|
||||
|
||||
for x := -radius32; x < radius32; x++ {
|
||||
for z := -radius32; z < radius32; z++ {
|
||||
var chunkData []byte
|
||||
chunkData, err = GetChunkFromMca(x, z, "1.21.11")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chunkData == nil {
|
||||
continue
|
||||
}
|
||||
OLDpacket := &v1_21_11.PlayToClientPacketMapChunk{}
|
||||
OLDpacket.X = x
|
||||
OLDpacket.Z = z
|
||||
OLDpacket.ChunkData = v1_21_11.ByteArray{Val: chunkData}
|
||||
OLDpacket.BlockLight = FullLight
|
||||
OLDpacket.BlockLightMask = []int64{(1 << 24) - 1}
|
||||
err = w.WritePacket(OLDpacket, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("conversion complete", "output", *output)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package messenger
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/admin-else/strom/cmd/strom/cmd_util"
|
||||
"github.com/admin-else/strom/mc/bot/chat"
|
||||
"github.com/admin-else/strom/mc/bot/keepalive"
|
||||
"github.com/admin-else/strom/mc/bot/world"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
)
|
||||
|
||||
func stror(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
||||
connectToFlag = cmd.String("addr", "", "server address to connect to")
|
||||
accFlag = cmd.String("acc", stror(os.Getenv("STROM_ACC"), "strom_messenger"), "the name to use if longer will be treated as ygg token")
|
||||
)
|
||||
|
||||
type Messenger struct {
|
||||
*proto.Conn
|
||||
chat *chat.Module
|
||||
world *world.Module
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
|
||||
m.Log.Info("chat", "author", e.SenderUuid, "m", e.PlainMessage)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChatUnsigned(e *v1_21_11.PlayToClientPacketProfilelessChat) (err error) {
|
||||
m.Log.Info("chatP", "author", e.Name, "m", e.Message)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err error) {
|
||||
m.Log.Info("chatS", "m", e.Content)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
||||
input := strings.TrimSpace(e.Val)
|
||||
if strings.HasPrefix(input, ">say ") {
|
||||
return m.chat.SendMessage(strings.TrimSpace(strings.TrimPrefix(input, ">say")))
|
||||
}
|
||||
if strings.HasPrefix(input, ">getblock ") {
|
||||
return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock")))
|
||||
}
|
||||
return m.chat.SendMessage(input)
|
||||
}
|
||||
|
||||
func (m *Messenger) handleGetBlock(args string) (err error) {
|
||||
fields := strings.Fields(args)
|
||||
if len(fields) != 3 {
|
||||
m.Log.Info("getblock", "error", "expected >getblock <x> <y> <z>")
|
||||
return nil
|
||||
}
|
||||
|
||||
coords := make([]int64, 3)
|
||||
for i, f := range fields {
|
||||
coords[i], err = strconv.ParseInt(f, 10, 32)
|
||||
if err != nil {
|
||||
m.Log.Info("getblock", "error", fmt.Sprintf("bad coordinate %q", f))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
stateId, err := m.world.GetBlock(int32(coords[0]), int32(coords[1]), int32(coords[2]))
|
||||
if err != nil {
|
||||
m.Log.Info("getblock", "error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
block, ok := data.LookupBlockByStateId(m.Version, stateId)
|
||||
if !ok {
|
||||
m.Log.Info("getblock", "stateId", stateId, "name", "unknown")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Log.Info("getblock", "x", coords[0], "y", coords[1], "z", coords[2], "name", block.Name, "stateId", stateId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
acc, err := cmd_util.Account(*accFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := client.Login(*connectToFlag, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
chatMod, err := chat.Start(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Shareable world storage. Other bots can subscribe to the same World.
|
||||
w := world.NewWorld(c.Version, -64, 384)
|
||||
|
||||
m := &Messenger{
|
||||
Conn: c,
|
||||
chat: chatMod,
|
||||
world: world.Start(c, w),
|
||||
}
|
||||
|
||||
event.StartListingStdin(m.Loop)
|
||||
m.Register(m.OnStdin)
|
||||
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
||||
keepalive.Start(m.Conn)
|
||||
|
||||
return m.StartConn()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("offline-uuid", flag.ContinueOnError)
|
||||
@@ -0,0 +1,169 @@
|
||||
package packet_info
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("packet-info", flag.ContinueOnError)
|
||||
var versionFlag = cmd.String("version", "", "protocol version or minecraft version name")
|
||||
var stateFlag = cmd.String("state", "", "handshaking, status, login, configuration, play")
|
||||
var directionFlag = cmd.String("direction", "", "toserver, toclient")
|
||||
var idFlag = cmd.Int("id", -1, "packet id")
|
||||
var nameFlag = cmd.String("name", "", "packet name")
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var version int32
|
||||
if *versionFlag != "" {
|
||||
v, err := data.LookUpVersionByName(*versionFlag)
|
||||
if err != nil {
|
||||
v2, err := parseVersion(*versionFlag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid version: %s", *versionFlag)
|
||||
}
|
||||
v, err = data.LookUpVersionByProtocolVersion(v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
version = v.Version
|
||||
}
|
||||
|
||||
var state proto_base.State
|
||||
if *stateFlag != "" {
|
||||
state, err = parseState(*stateFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var direction proto_base.Direction
|
||||
if *directionFlag != "" {
|
||||
direction, err = parseDirection(*directionFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if *idFlag != -1 && *nameFlag != "" {
|
||||
err = fmt.Errorf("cannot specify both id and name")
|
||||
return
|
||||
}
|
||||
|
||||
if *idFlag != -1 {
|
||||
if *versionFlag == "" || *stateFlag == "" || *directionFlag == "" {
|
||||
err = fmt.Errorf("version, state, and direction are required when looking up by id")
|
||||
return
|
||||
}
|
||||
p, ok := proto.LookUpTypeByPacketInfo(direction, state, int32(*idFlag), version)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
return
|
||||
}
|
||||
|
||||
if *nameFlag != "" {
|
||||
if *versionFlag == "" {
|
||||
err = fmt.Errorf("version is required when looking up by name")
|
||||
return
|
||||
}
|
||||
if state != 0 || direction != 0 {
|
||||
p, ok := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection(*nameFlag, version, state, direction)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
} else {
|
||||
var found bool
|
||||
for _, p := range proto_generated.Packets {
|
||||
if p.Name == *nameFlag && p.ProtocolVersion == version {
|
||||
printPacketInfo(p)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = fmt.Errorf("packet not found")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("must specify either -id or -name")
|
||||
return
|
||||
}
|
||||
|
||||
func parseState(s string) (proto_base.State, error) {
|
||||
switch s {
|
||||
case "handshaking":
|
||||
return proto_base.Handshaking, nil
|
||||
case "status":
|
||||
return proto_base.Status, nil
|
||||
case "login":
|
||||
return proto_base.Login, nil
|
||||
case "configuration":
|
||||
return proto_base.Configuration, nil
|
||||
case "play":
|
||||
return proto_base.Play, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown state: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDirection(s string) (proto_base.Direction, error) {
|
||||
switch s {
|
||||
case "toserver":
|
||||
return proto_base.ToServer, nil
|
||||
case "toclient":
|
||||
return proto_base.ToClient, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown direction: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseVersion(val string) (int32, error) {
|
||||
v, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
func printPacketInfo(p proto_base.PacketInfo) {
|
||||
v, _ := data.LookUpVersionByProtocolVersion(p.ProtocolVersion)
|
||||
fmt.Printf("Name: %s\n", p.Name)
|
||||
fmt.Printf("ID: %d\n", p.PacketId)
|
||||
fmt.Printf("State: %s\n", stateToString(p.State))
|
||||
fmt.Printf("Direction: %s\n", p.Direction)
|
||||
fmt.Printf("Version: %d (%s)\n", p.ProtocolVersion, v.MinecraftVersion)
|
||||
}
|
||||
|
||||
func stateToString(s proto_base.State) string {
|
||||
switch s {
|
||||
case proto_base.Handshaking:
|
||||
return "handshaking"
|
||||
case proto_base.Status:
|
||||
return "status"
|
||||
case proto_base.Login:
|
||||
return "login"
|
||||
case proto_base.Configuration:
|
||||
return "configuration"
|
||||
case proto_base.Play:
|
||||
return "play"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package packet_inspector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
var Cmd = flag.NewFlagSet("packet-inspector", flag.ContinueOnError)
|
||||
|
||||
var OfflineNameFlag = Cmd.String("name", "packet_inspector", "offline player name")
|
||||
var Token = Cmd.String("token", "", "token to use")
|
||||
|
||||
var TargetAddr = Cmd.String("addr", "127.0.0.1:25565", "address to connect to")
|
||||
var ListenAddr = Cmd.String("listen", "127.0.0.1:25566", "address to listen on")
|
||||
var TrueForward = Cmd.Bool("true-forward", false, "forward packets without modification -name and custom server status wont work")
|
||||
|
||||
type Proxy struct {
|
||||
Client, Servee *proto.Conn
|
||||
Data nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) Start() (err error) {
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnFinishLogin)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
|
||||
//p.Servee.RegisterCriticalUntilLatest(p.OnCompress)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- p.Client.StartConn()
|
||||
}()
|
||||
go func() {
|
||||
errChan <- p.Servee.StartConn()
|
||||
}()
|
||||
err = <-errChan
|
||||
return
|
||||
}
|
||||
|
||||
type PacketLogEvent struct {
|
||||
Time time.Time
|
||||
Packet proto_base.EncodeDecodeAble
|
||||
Info PacketInfoNoTypeJson
|
||||
ClientAddr string
|
||||
}
|
||||
|
||||
type PacketInfoNoTypeJson struct {
|
||||
Type proto_base.EncodeDecodeAble `json:"-"`
|
||||
Name string
|
||||
Direction proto_base.Direction
|
||||
State proto_base.State
|
||||
PacketId int32
|
||||
ProtocolVersion int32
|
||||
}
|
||||
|
||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
packetInfo, ok := proto.LookupPacketInfoByType(packet)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonBytes, err := json.Marshal(PacketLogEvent{
|
||||
Time: time.Now(),
|
||||
Packet: packet,
|
||||
ClientAddr: p.Client.RemoteAddr().String(),
|
||||
Info: PacketInfoNoTypeJson(packetInfo),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// :pray:
|
||||
_, _ = os.Stdout.Write(jsonBytes)
|
||||
_, _ = os.Stdout.Write([]byte("\n"))
|
||||
|
||||
switch packetInfo.Direction {
|
||||
case proto_base.ToServer:
|
||||
err = p.Client.Send(packet)
|
||||
case proto_base.ToClient:
|
||||
err = p.Servee.Send(packet)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Play)
|
||||
p.Client.SetState(proto_base.Play)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnFinishLogin(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Configuration)
|
||||
p.Client.SetState(proto_base.Configuration)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
state := proto_base.State(packet.NextState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(state)
|
||||
p.Client.SetState(state)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
|
||||
err = p.Servee.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetCompressionThreshold(packet.Threshold)
|
||||
p.Client.SetCompressionThreshold(packet.Threshold)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
var StatusResponse = server.StatusResponse{
|
||||
Version: server.StatusResponseVersion{
|
||||
Name: text.Pretty("STROM"),
|
||||
Protocol: 772,
|
||||
},
|
||||
Players: server.StatusResponsePlayers{},
|
||||
Description: text.Pretty("Packet spy forwards to " + *TargetAddr),
|
||||
Favicon: "",
|
||||
EnforcesSecureChat: false,
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = Cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Starting packet inspector", "listen", *ListenAddr)
|
||||
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
|
||||
p := &Proxy{Servee: serveeConn, Client: nil}
|
||||
var acc = api.NewOfflineAccount(*OfflineNameFlag)
|
||||
if !*TrueForward {
|
||||
if *Token != "" {
|
||||
acc, err = api.NewAccountFromYGG(*Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode(), server.WithoutEncryption())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*TargetAddr, _, _, err = client.DoDNSChecked(nil, *TargetAddr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := client.Connect(nil, *TargetAddr, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
if !*TrueForward {
|
||||
err = client.LoginRaw(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
p.Client = c
|
||||
p.Client.AlwaysDecode = true
|
||||
p.Servee.AlwaysDecode = true
|
||||
err = p.Start()
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package print_nbt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-nbt", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The NBT file to print")
|
||||
HashFormatFlag = cmd.Bool("snbt", false, "will print with go %#v")
|
||||
)
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
n, err := nbt.ReadUnstructuredFile(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var b []byte
|
||||
if *HashFormatFlag {
|
||||
bb := bytes.NewBuffer(nil)
|
||||
err = nbt.PrintSNBTAny(n.Value, bb)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = bb.Bytes()
|
||||
//b = []byte(strings.ReplaceAll(string(b), ",", ",\n"))
|
||||
} else {
|
||||
b, err = json.MarshalIndent(n, "", " ")
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = os.Stdout.Write(b)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package failed_packets_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
generated "github.com/admin-else/strom/mc/proto_generated/%v"
|
||||
)
|
||||
|
||||
// Error: %v
|
||||
func TestFailedPacket%10X(t *testing.T) {
|
||||
p := &generated.%v{}
|
||||
b := bytes.NewBuffer(%#v)
|
||||
err := p.Decode(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package print_replay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-replay", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The tmcpr file to print")
|
||||
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
|
||||
CreateTestsPath = cmd.String("create-tests", "", "")
|
||||
StateFlag = cmd.String("state", "login", "Initial state: login, config, play")
|
||||
DirectionFlag = cmd.String("direction", "clientbound", "Packet direction: clientbound (ToClient) or serverbound (ToServer)")
|
||||
)
|
||||
|
||||
// TODO: maybe use the proto replay api
|
||||
|
||||
//go:embed failed_packet.go.tmpl
|
||||
var TestSrcF string
|
||||
|
||||
func SaveUnCodeAbleAsTest(packet *proto.UnCodablePacket) (err error) {
|
||||
if packet.Info.Type == nil {
|
||||
slog.Warn("skipping test for unknown packet id", "packet_id", packet.Info.PacketId, "state", packet.Info.State, "error", packet.Err)
|
||||
return
|
||||
}
|
||||
|
||||
hUntrimmed := sha256.Sum256(packet.Data)
|
||||
h := hUntrimmed[:8]
|
||||
if err = os.MkdirAll(*CreateTestsPath, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := data.LookUpVersionByProtocolVersion(packet.Info.ProtocolVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
goPackage := "v" + strings.ReplaceAll(ret.MinecraftVersion, ".", "_")
|
||||
typeName := reflect.TypeOf(packet.Info.Type).Elem().Name()
|
||||
|
||||
src := fmt.Sprintf(TestSrcF, goPackage, packet.Err, h, typeName, packet.Data)
|
||||
formatted, formatErr := format.Source([]byte(src))
|
||||
if formatErr != nil {
|
||||
formatted = []byte(src)
|
||||
}
|
||||
|
||||
filePath := fmt.Sprintf(*CreateTestsPath+"/%v_%10x_test.go", len(packet.Data), h)
|
||||
if err = os.WriteFile(filePath, formatted, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Packet failed to parse", "error", packet.Err, "saved", filePath, "data", fmt.Sprintf("%q", packet.Data))
|
||||
return
|
||||
}
|
||||
|
||||
var state = proto_base.Login
|
||||
var direction = proto_base.ToClient
|
||||
|
||||
func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo, err error) {
|
||||
var timestamp, length uint32
|
||||
err = binary.Read(r, binary.BigEndian, ×tamp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Read the packet data into a byte slice
|
||||
packetData := make([]byte, length)
|
||||
_, err = io.ReadFull(r, packetData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Use bytes.Reader instead of bytes.Buffer
|
||||
b := bytes.NewReader(packetData)
|
||||
|
||||
packetId, err := proto_base.DecodeVarInt(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate position after reading the packet ID
|
||||
// indexafterid represents how many bytes we've read so far
|
||||
indexafterid := int(b.Size()) - b.Len()
|
||||
|
||||
var ok bool
|
||||
i, ok = proto.LookUpTypeByPacketInfo(direction, state, packetId, int32(*ProtocolVersion))
|
||||
if !ok {
|
||||
packet = &proto.UnCodablePacket{Err: proto.BadPacketIdErr, Data: packetData, Info: i}
|
||||
return
|
||||
}
|
||||
switch direction {
|
||||
case proto_base.ToClient:
|
||||
switch i.Name {
|
||||
case "success":
|
||||
state = proto_base.Configuration
|
||||
case "finish_configuration":
|
||||
state = proto_base.Play
|
||||
}
|
||||
case proto_base.ToServer:
|
||||
switch i.Name {
|
||||
case "login_acknowledged":
|
||||
state = proto_base.Configuration
|
||||
case "finish_configuration":
|
||||
state = proto_base.Play
|
||||
}
|
||||
}
|
||||
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
|
||||
err = packet.Decode(b)
|
||||
if err != nil {
|
||||
packet = &proto.UnCodablePacket{Err: err, Data: packetData[indexafterid:], Info: i}
|
||||
return
|
||||
}
|
||||
if b.Len() != 0 {
|
||||
packet = &proto.UnCodablePacket{Err: proto.PacketNotFullyDecodedErr, Data: packetData[indexafterid:], Info: i}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *ProtocolVersion == -1 {
|
||||
return fmt.Errorf("no protocol version specified")
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
switch *StateFlag {
|
||||
case "login":
|
||||
state = proto_base.Login
|
||||
case "config":
|
||||
state = proto_base.Configuration
|
||||
case "play":
|
||||
state = proto_base.Play
|
||||
default:
|
||||
return fmt.Errorf("unknown state: %s", *StateFlag)
|
||||
}
|
||||
switch *DirectionFlag {
|
||||
case "clientbound", "to-client":
|
||||
direction = proto_base.ToClient
|
||||
case "serverbound", "to-server":
|
||||
direction = proto_base.ToServer
|
||||
default:
|
||||
return fmt.Errorf("unknown direction: %s", *DirectionFlag)
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var totalPackets, failedPackets int
|
||||
for {
|
||||
var packet proto_base.EncodeDecodeAble
|
||||
var info proto_base.PacketInfo
|
||||
packet, info, err = UnpackPacket(f)
|
||||
unCodablePacket, ok := packet.(*proto.UnCodablePacket)
|
||||
if ok {
|
||||
slog.Error("failed to decode packet", "error", err, "packet_name", info.Name)
|
||||
if *CreateTestsPath != "" {
|
||||
err = SaveUnCodeAbleAsTest(unCodablePacket)
|
||||
if err != nil {
|
||||
slog.Error("failed to save uncodable packet as test", "error", err)
|
||||
}
|
||||
}
|
||||
failedPackets++
|
||||
}
|
||||
|
||||
fmt.Printf("Packet: %#v\n", packet)
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
break
|
||||
}
|
||||
totalPackets++
|
||||
}
|
||||
fmt.Printf("Total packets: %d, failed packets: %d, %%failed: %.2f%%\n", totalPackets, failedPackets, float64(failedPackets)/float64(totalPackets)*100)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package raw_capture
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto/replay"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
var Cmd = flag.NewFlagSet("raw-capture", flag.ContinueOnError)
|
||||
|
||||
var OfflineNameFlag = Cmd.String("name", "raw_capture", "offline player name")
|
||||
var Token = Cmd.String("token", "", "token to use")
|
||||
|
||||
var TargetAddr = Cmd.String("addr", "127.0.0.1:25565", "address to connect to")
|
||||
var ListenAddr = Cmd.String("listen", "127.0.0.1:25566", "address to listen on")
|
||||
var ClientboundFile = Cmd.String("clientbound-file", "clientbound.tmcpr", "file to write clientbound packets to")
|
||||
var ServerboundFile = Cmd.String("serverbound-file", "serverbound.tmcpr", "file to write serverbound packets to")
|
||||
var Version = Cmd.String("version", "", "client protocol version (e.g. 26.1); if empty, detect from server status")
|
||||
|
||||
var StatusResponse = server.StatusResponse{
|
||||
Version: server.StatusResponseVersion{
|
||||
Name: text.Pretty("STROM raw-capture"),
|
||||
Protocol: 0,
|
||||
},
|
||||
Players: server.StatusResponsePlayers{},
|
||||
Description: text.Pretty("STROM raw-capture proxy"),
|
||||
Favicon: "",
|
||||
EnforcesSecureChat: false,
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Client, Servee *proto.Conn
|
||||
|
||||
clientboundFile *os.File
|
||||
serverboundFile *os.File
|
||||
|
||||
clientboundWriter *replay.Writer
|
||||
serverboundWriter *replay.Writer
|
||||
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (p *Proxy) Start() (err error) {
|
||||
p.startTime = time.Now()
|
||||
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnLoginAcknowledged)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- p.Client.StartConn()
|
||||
}()
|
||||
go func() {
|
||||
errChan <- p.Servee.StartConn()
|
||||
}()
|
||||
err = <-errChan
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) timestamp() uint32 {
|
||||
return uint32(time.Since(p.startTime).Milliseconds())
|
||||
}
|
||||
|
||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
packetInfo, ok := proto.LookupPacketInfoByType(packet)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if packetInfo.Name == "encryption_begin" {
|
||||
return fmt.Errorf("encryption requested by remote server; raw capture cannot continue")
|
||||
}
|
||||
|
||||
switch packetInfo.Direction {
|
||||
case proto_base.ToServer:
|
||||
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = p.Client.Send(packet)
|
||||
case proto_base.ToClient:
|
||||
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = p.Servee.Send(packet)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
|
||||
err = p.clientboundWriter.WritePacket(packet, p.timestamp())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = p.Servee.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetCompressionThreshold(packet.Threshold)
|
||||
p.Client.SetCompressionThreshold(packet.Threshold)
|
||||
return event.DontForwardErr
|
||||
}
|
||||
|
||||
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
|
||||
state := proto_base.State(packet.NextState)
|
||||
p.Servee.SetState(state)
|
||||
return event.DontForwardErr
|
||||
}
|
||||
|
||||
func (p *Proxy) OnLoginAcknowledged(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
|
||||
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Configuration)
|
||||
p.Client.SetState(proto_base.Configuration)
|
||||
return event.DontForwardErr
|
||||
}
|
||||
|
||||
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
|
||||
err = p.serverboundWriter.WritePacket(packet, p.timestamp())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Play)
|
||||
p.Client.SetState(proto_base.Play)
|
||||
return event.DontForwardErr
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = Cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
clientboundFile, err := os.Create(*ClientboundFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create clientbound file: %w", err)
|
||||
}
|
||||
defer clientboundFile.Close()
|
||||
|
||||
serverboundFile, err := os.Create(*ServerboundFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create serverbound file: %w", err)
|
||||
}
|
||||
defer serverboundFile.Close()
|
||||
|
||||
resolvedAddr, _, _, err := client.DoDNSChecked(nil, *TargetAddr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var targetVersion string
|
||||
var targetProtocol int32
|
||||
if *Version == "" {
|
||||
status, err := client.Status(nil, resolvedAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect server version: %w", err)
|
||||
}
|
||||
targetProtocol = status.Version.Protocol
|
||||
versionInfo, err := data.LookUpVersionByProtocolVersion(targetProtocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unknown protocol version %d: %w", targetProtocol, err)
|
||||
}
|
||||
targetVersion = versionInfo.MinecraftVersion
|
||||
} else {
|
||||
versionInfo, err := data.LookUpVersionByName(*Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unknown version %s: %w", *Version, err)
|
||||
}
|
||||
targetVersion = versionInfo.MinecraftVersion
|
||||
targetProtocol = versionInfo.Version
|
||||
}
|
||||
|
||||
StatusResponse.Version.Name = text.Pretty("STROM raw-capture " + targetVersion)
|
||||
StatusResponse.Version.Protocol = targetProtocol
|
||||
StatusResponse.Description = text.Pretty(fmt.Sprintf("Raw capture proxy forwards to %s (%s)", resolvedAddr, targetVersion))
|
||||
|
||||
slog.Info("Starting raw capture", "listen", *ListenAddr, "target", resolvedAddr, "version", targetVersion, "protocol", targetProtocol, "clientbound", *ClientboundFile, "serverbound", *ServerboundFile)
|
||||
|
||||
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
|
||||
proxy := &Proxy{
|
||||
Servee: serveeConn,
|
||||
clientboundFile: clientboundFile,
|
||||
serverboundFile: serverboundFile,
|
||||
clientboundWriter: replay.NewWriter(clientboundFile),
|
||||
serverboundWriter: replay.NewWriter(serverboundFile),
|
||||
}
|
||||
|
||||
acc := api.NewOfflineAccount(*OfflineNameFlag)
|
||||
if *Token != "" {
|
||||
acc, err = api.NewAccountFromYGG(*Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode(), server.WithoutEncryption())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := client.Connect(nil, resolvedAddr, targetVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
err = client.LoginRaw(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
proxy.Client = c
|
||||
proxy.Client.AlwaysDecode = true
|
||||
proxy.Servee.AlwaysDecode = true
|
||||
|
||||
return proxy.Start()
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package registry_data_hunter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/api"
|
||||
"github.com/admin-else/strom/client"
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/nbt"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/server"
|
||||
"github.com/admin-else/strom/text"
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
func wontFail[T any](v T, err error) T {
|
||||
@@ -26,10 +26,8 @@ func wontFail[T any](v T, err error) T {
|
||||
}
|
||||
|
||||
var (
|
||||
// Edit these two
|
||||
|
||||
SaveAs = "1.21.8"
|
||||
Version = "1.21.8"
|
||||
Version = "1.21.11"
|
||||
SaveAs = "data/registry/" + Version + ".nbt"
|
||||
|
||||
CompatibleProtocolVersion = wontFail(data.LookUpVersionByName(Version)).Version
|
||||
FinishedConfigErr = errors.New("finished configuration")
|
||||
@@ -45,15 +43,7 @@ var Status = wontFail(json.Marshal(server.StatusResponse{
|
||||
|
||||
type Proxy struct {
|
||||
Client, Servee *proto.Conn
|
||||
Data nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) OnStart() (err error) {
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
return
|
||||
Data *nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||
@@ -79,11 +69,25 @@ func (p *Proxy) OnFinishConfiguration(_ *v1_21_8.ConfigurationToClientPacketFini
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnSelectKnownPacks(data *v1_21_8.ConfigurationToClientPacketCommonSelectKnownPacks) (err error) {
|
||||
var entries []any
|
||||
for _, entry := range data.Packs {
|
||||
e := map[string]any{"Namespace": entry.Namespace, "Id": entry.Id, "Version": entry.Version}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
p.Data.Value.(map[string]any)["KnownPacks"] = entries
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnData(data *v1_21_8.ConfigurationToClientPacketRegistryData) (err error) {
|
||||
d := p.Data.Value.(map[string]any)["Registries"].([]any)
|
||||
var entries []any
|
||||
for _, entry := range data.Entries {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key, "Value": entry.Value.Value})
|
||||
if entry.Value != nil {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key, "Value": entry.Value.Value})
|
||||
} else {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key})
|
||||
}
|
||||
}
|
||||
d = append(d, map[string]any{"Id": data.Id, "Entries": entries})
|
||||
p.Data.Value.(map[string]any)["Registries"] = d
|
||||
@@ -106,46 +110,43 @@ func (p *Proxy) OnTags(data *v1_21_8.ConfigurationToClientPacketTags) (err error
|
||||
func handleClient(c *proto.Conn) (err error) {
|
||||
acc := api.NewOfflineAccount("RegistryHunter")
|
||||
|
||||
// ignore resource leak warning we own c conn so we should not close it here
|
||||
_, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sockPuppet, err := client.Connect(":25566")
|
||||
sockPuppet, err := client.Login(":25565", acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer sockPuppet.Close()
|
||||
err = sockPuppet.Start(&client.LoginClient{
|
||||
Conn: sockPuppet,
|
||||
Account: acc,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
errChan := make(chan error)
|
||||
p := &Proxy{Client: sockPuppet, Servee: c}
|
||||
p.Data.Value = map[string]any{"Registries": []any{}}
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnData)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnTags)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnSelectKnownPacks)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
|
||||
go func() {
|
||||
errChan <- p.Client.Start(p)
|
||||
errChan <- p.Client.StartConn()
|
||||
}()
|
||||
go func() {
|
||||
errChan <- p.Servee.Start(p)
|
||||
errChan <- p.Servee.StartConn()
|
||||
}()
|
||||
err = <-errChan
|
||||
f, err := os.Create("data/registry/" + SaveAs + ".nbt")
|
||||
f, err := os.Create(SaveAs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
err = nbt.WriteFile(f, p.Data)
|
||||
err = nbt.WriteUnstructuredFile(f, p.Data)
|
||||
slog.Info("saved", "file", SaveAs)
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
err := server.StartServerWithOnConn(":25565", handleClient)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return server.StartServerWithOnConn(":25566", handleClient)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package serve_tmcpr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto/replay"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*proto.Conn
|
||||
*event.Timer
|
||||
}
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("serve-tmcpr", flag.ContinueOnError)
|
||||
tmcprFile = cmd.String("file", "/tmp/world.tmcpr", "Path to .tmcpr replay file")
|
||||
bindAddr = cmd.String("bind", ":25566", "Address to listen on")
|
||||
)
|
||||
|
||||
func Limbo(c *proto.Conn, tmcprPath string) (err error) {
|
||||
s := &Server{Conn: c}
|
||||
|
||||
_, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = server.ServeConfig(s.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(tmcprPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
r := replay.NewReader(f)
|
||||
r.Version = c.ProtocolVersion
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for {
|
||||
var packetData []byte
|
||||
var timestamp uint32
|
||||
packetData, timestamp, _, err = r.ReadRaw()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(timestamp)*time.Millisecond - time.Since(start))
|
||||
err = s.SendRaw(packetData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.Timer = &event.Timer{}
|
||||
s.Timer.Every(time.Second*15, s.SendKeepAlive)
|
||||
s.Timer.Start(c.Loop)
|
||||
|
||||
return s.StartConn()
|
||||
}
|
||||
|
||||
func (s *Server) SendKeepAlive() error {
|
||||
return s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()})
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
err := cmd.Parse(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *tmcprFile == "" {
|
||||
slog.Error("no file specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
return server.StartServerWithOnConn(*bindAddr, func(c *proto.Conn) error {
|
||||
return Limbo(c, *tmcprFile)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package serve_world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/level"
|
||||
"github.com/admin-else/strom/mc/level/anvil"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*proto.Conn
|
||||
*event.Timer
|
||||
|
||||
reconnectCalled bool
|
||||
}
|
||||
|
||||
var FullLight [][]uint8
|
||||
|
||||
func init() {
|
||||
light := make([]uint8, 2048)
|
||||
for i := range light {
|
||||
light[i] = 0xFF
|
||||
}
|
||||
|
||||
sectionsPerChunk := 384 / 16
|
||||
FullLight = make([][]uint8, sectionsPerChunk)
|
||||
for i := range sectionsPerChunk {
|
||||
FullLight[i] = light
|
||||
}
|
||||
}
|
||||
|
||||
func GetChunkFromMca(x, z int32, version string) (b []byte, err error) {
|
||||
xmca := util.FloorDiv(x, anvil.ChunksWidthRegionFile)
|
||||
zmca := util.FloorDiv(z, anvil.ChunksWidthRegionFile)
|
||||
x = x % anvil.ChunksWidthRegionFile
|
||||
if x < 0 {
|
||||
x += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
z = z % anvil.ChunksWidthRegionFile
|
||||
if z < 0 {
|
||||
z += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
|
||||
f, err := os.Open(fmt.Sprintf("./.devres/anvil_testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n *nbt.Tag
|
||||
n, err = mca.ChunkAt(f, x, z)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var chunk anvil.Chunk
|
||||
err = nbt.Format.Decode(n.Value, &chunk)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var packetChunk *level.Chunk
|
||||
packetChunk, err = chunk.ToStorage(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = packetChunk.WriteChunkData(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func Limbo(c *proto.Conn) (err error) {
|
||||
s := &Server{
|
||||
Conn: c,
|
||||
}
|
||||
_, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = server.ServeConfig(s.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
login := &v1_21_8.PlayToClientPacketLogin{EntityId: 73, IsHardcore: false, WorldNames: []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"}, MaxPlayers: 100, ViewDistance: 10, SimulationDistance: 10, ReducedDebugInfo: false, EnableRespawnScreen: true, DoLimitedCrafting: false, WorldState: v1_21_8.PlayToClientSpawnInfo{Dimension: 0, Name: "minecraft:overworld", HashedSeed: -8365436745629239539, Gamemode: "survival", PreviousGamemode: 0xff, IsDebug: false, IsFlat: false, Death: nil, PortalCooldown: 0, SeaLevel: 63}, EnforcesSecureChat: false}
|
||||
err = s.Send(login)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketPosition{
|
||||
TeleportId: 0,
|
||||
X: 0.5,
|
||||
Y: 100,
|
||||
Z: 0.5,
|
||||
Dx: 0,
|
||||
Dy: 0,
|
||||
Dz: 0,
|
||||
Yaw: 0,
|
||||
Pitch: 0,
|
||||
Flags: v1_21_8.PlayToClientPositionUpdateRelatives{},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketUpdateViewPosition{
|
||||
ChunkX: 0,
|
||||
ChunkZ: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketGameStateChange{
|
||||
Reason: "level_chunks_load_start",
|
||||
})
|
||||
|
||||
radius := int32(10)
|
||||
|
||||
for x := -radius; x < radius; x++ {
|
||||
for z := -radius; z < radius; z++ {
|
||||
var chunkData []byte
|
||||
chunkData, err = GetChunkFromMca(x, z, c.Version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if chunkData == nil {
|
||||
continue
|
||||
}
|
||||
OLDpacket := &v1_21_8.PlayToClientPacketMapChunk{}
|
||||
OLDpacket.X = x
|
||||
OLDpacket.Z = z
|
||||
OLDpacket.ChunkData = v1_21_8.ByteArray{Val: chunkData}
|
||||
OLDpacket.BlockLight = FullLight
|
||||
OLDpacket.BlockLightMask = []int64{(1 << 24) - 1}
|
||||
err = s.Send(OLDpacket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Timer = &event.Timer{}
|
||||
s.Timer.Every(time.Second*15, s.SendKeepAlive)
|
||||
s.Timer.Start(c.Loop)
|
||||
|
||||
return s.StartConn()
|
||||
}
|
||||
func (s *Server) SendKeepAlive() (err error) {
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
return server.StartServerWithOnConn(":25566", Limbo)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
||||
var addrFlag = cmd.String("addr", "localhost:25565", "address to connect to")
|
||||
var srvFlag = cmd.Bool("srv", false, "use SRV records to resolve the address")
|
||||
var versionFlag = cmd.String("version", "1.21.8", "version to use")
|
||||
|
||||
type StatusClient struct {
|
||||
*proto.Conn
|
||||
Status string
|
||||
PingSendTime, PingReceiveTime time.Time
|
||||
DoPingRoundTripTime bool
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
||||
s.Status = p.Response
|
||||
s.PingSendTime = time.Now()
|
||||
if s.DoPingRoundTripTime {
|
||||
err = s.Send(&v1_21_8.StatusToServerPacketPing{Time: s.PingSendTime.UnixMilli()})
|
||||
} else {
|
||||
err = event.HandlerDoneErr{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
|
||||
s.PingReceiveTime = time.Now()
|
||||
err = event.HandlerDoneErr{}
|
||||
return
|
||||
}
|
||||
|
||||
// StatusRaw returns a StatusClient that is not connected to a server.
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
|
||||
// It does not resolve SRV records.
|
||||
func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
c, err := client.ConnectVersionLess(nil, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
err = c.SetVersion(*versionFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s = &StatusClient{
|
||||
Conn: c,
|
||||
}
|
||||
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||
|
||||
p, err := client.MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.SetState(proto_base.Status)
|
||||
err = s.Send(&v1_21_8.StatusToServerPacketPingStart{})
|
||||
|
||||
err = s.StartConn()
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *srvFlag {
|
||||
*addrFlag, _, _, err = client.DoDNSChecked(nil, *addrFlag, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
slog.Debug("resolved address to ", "addr", *addrFlag)
|
||||
|
||||
status, err := StatusRaw(*addrFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(status.Status)
|
||||
return
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package data
|
||||
|
||||
// [
|
||||
//
|
||||
// {
|
||||
// "id": 0,
|
||||
// "name": "badlands",
|
||||
// "category": "mesa",
|
||||
// "temperature": 2.0,
|
||||
// "has_precipitation": false,
|
||||
// "dimension": "overworld",
|
||||
// "displayName": "Badlands",
|
||||
// "color": 7254527
|
||||
// },
|
||||
|
||||
type Biome struct {
|
||||
Id int
|
||||
Name string
|
||||
Category string
|
||||
Temperature float64
|
||||
HasPrecipitation bool
|
||||
Dimension string
|
||||
DisplayName string
|
||||
Color int
|
||||
}
|
||||
|
||||
var BiomesCache = make(map[string][]Biome)
|
||||
|
||||
func BiomesForVersion(v string) (ret []Biome) {
|
||||
var ok bool
|
||||
if ret, ok = BiomesCache[v]; ok {
|
||||
return
|
||||
}
|
||||
var b []Biome
|
||||
must(LoadVersionedJson(v, "biomes", &b))
|
||||
BiomesCache[v] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func LookupBiomeById(version string, id int32) (biome Biome, ok bool) {
|
||||
for _, b := range BiomesForVersion(version) {
|
||||
if b.Id == int(id) {
|
||||
biome = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
//go:embed minecraft-data/data
|
||||
var MinecraftData embed.FS
|
||||
|
||||
func LoadJson(path string, data any) (err error) {
|
||||
b, err := MinecraftData.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, data)
|
||||
return
|
||||
}
|
||||
|
||||
func LoadVersionedJson(version, dataName string, data any) (err error) {
|
||||
b, err := MinecraftData.ReadFile("minecraft-data/data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, data)
|
||||
return
|
||||
}
|
||||
|
||||
var UnknownMinecraftVersionError = errors.New("unknown minecraft version")
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
Submodule data/minecraft-data deleted from 3e12d39f67
@@ -1,20 +0,0 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/admin-else/strom/nbt"
|
||||
)
|
||||
|
||||
//go:embed registry/*
|
||||
var Registry embed.FS
|
||||
|
||||
func LoadRegistry(name string) (n *nbt.Tag, err error) {
|
||||
r, err := Registry.Open("registry/" + name + ".nbt")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
n, err = nbt.ReadFile(r)
|
||||
return
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
// Package event is the event handler
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
LoopsPerSecond = 100 // i dunno seems reasonable
|
||||
TimePerLoop = time.Second / LoopsPerSecond
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
HandlerFunctions map[reflect.Type][]reflect.Value
|
||||
|
||||
Ctx context.Context
|
||||
ErrChan chan error
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type Unhandled struct {
|
||||
Val any
|
||||
}
|
||||
|
||||
type Anything struct {
|
||||
Val any
|
||||
}
|
||||
|
||||
type (
|
||||
Tick struct{}
|
||||
)
|
||||
|
||||
// HandlerDoneErr will stop the event loop and return the error if provided.
|
||||
type HandlerDoneErr struct {
|
||||
Return error
|
||||
}
|
||||
|
||||
// Close is fired when the loop shuts down.
|
||||
type Close struct {
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e HandlerDoneErr) Error() string {
|
||||
return "HandlerDoneErr"
|
||||
}
|
||||
|
||||
func (e HandlerDoneErr) Unwrap() error {
|
||||
return e.Return
|
||||
}
|
||||
|
||||
// ErrDontForward will stop the event from being forwarded to other handlers, including anything handlers.
|
||||
var ErrDontForward = errors.New("dont forward")
|
||||
var WhileClosingErr = errors.New("error while closing")
|
||||
|
||||
func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) {
|
||||
hv = reflect.ValueOf(h)
|
||||
if hv.Kind() != reflect.Func {
|
||||
panic("expected method")
|
||||
}
|
||||
if hv.Type().NumIn() != 1 {
|
||||
panic("expected method with one argument")
|
||||
}
|
||||
if hv.Type().NumOut() != 1 || hv.Type().Out(0) != reflect.TypeFor[error]() {
|
||||
panic("expected method that returns an error")
|
||||
}
|
||||
eventType = hv.Type().In(0)
|
||||
return
|
||||
}
|
||||
|
||||
// Register registers a handler that will not close the loop if it returns an error.
|
||||
// Instead, it will log it to slog.Error
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) Register(h any) {
|
||||
l.RegisterCustomType(ValidateHandler(h))
|
||||
}
|
||||
|
||||
// RegisterCustomType registers a handler that will not close the loop if it returns an error.
|
||||
// Instead, it will log it to slog.Error
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) RegisterCustomType(eventType reflect.Type, hv reflect.Value) {
|
||||
handleFunc := func(packet any) error {
|
||||
v := hv.Call([]reflect.Value{reflect.ValueOf(packet)})[0]
|
||||
if !v.IsNil() {
|
||||
slog.Error("handler failed", "err", v.Interface().(error))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
l.RegisterDirect(eventType, reflect.ValueOf(handleFunc))
|
||||
}
|
||||
|
||||
// RegisterCritical registers a handler that will close the loop if it returns an error.
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) RegisterCritical(h any) {
|
||||
eventType, hv := ValidateHandler(h)
|
||||
l.RegisterDirect(eventType, hv)
|
||||
}
|
||||
|
||||
func (l *Loop) RegisterDirect(k reflect.Type, h reflect.Value) {
|
||||
l.HandlerFunctions[k] = append(l.HandlerFunctions[k], h)
|
||||
}
|
||||
|
||||
// FireFound triggers all handlers registered for the specified event type and returns if handlers were found or an error occurred.
|
||||
func (l *Loop) FireFound(event any) (found bool, err error) {
|
||||
var handlers []reflect.Value
|
||||
if handlers, found = l.HandlerFunctions[reflect.TypeOf(event)]; found {
|
||||
for _, handler := range handlers {
|
||||
errV := handler.Call([]reflect.Value{reflect.ValueOf(event)})[0]
|
||||
if !errV.IsNil() {
|
||||
err = errV.Interface().(error)
|
||||
err = errors.Join(fmt.Errorf("event handler failed at: %s(%T)", handler.Type().In(0), event), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fire dispatches the provided event to all appropriate handlers and invokes fallback handlers if the event is unhandled.
|
||||
func (l *Loop) Fire(event any) (err error) {
|
||||
found, err := l.FireFound(event)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrDontForward) {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
_, err = l.FireFound(Unhandled{event})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = l.FireFound(Anything{event})
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Loop) startLoop() (err error) {
|
||||
_ = *l // exit early on nil loop
|
||||
for {
|
||||
tickStartTime := time.Now()
|
||||
err = l.Fire(Tick{})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(TimePerLoop - time.Since(tickStartTime))
|
||||
}
|
||||
if errors.Is(err, HandlerDoneErr{}) {
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
closeErr := l.Fire(Close{err})
|
||||
if closeErr != nil {
|
||||
err = errors.Join(err, errors.Join(WhileClosingErr, closeErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Loop) Start() (err error) {
|
||||
go func() {
|
||||
l.ErrChan <- l.startLoop()
|
||||
}()
|
||||
err = <-l.ErrChan
|
||||
l.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
func NewLoop() (l *Loop) {
|
||||
l = &Loop{}
|
||||
l.ErrChan = make(chan error)
|
||||
l.Ctx, l.cancel = context.WithCancel(context.Background())
|
||||
l.HandlerFunctions = make(map[reflect.Type][]reflect.Value)
|
||||
return
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Stdin struct {
|
||||
Val string
|
||||
}
|
||||
|
||||
type StdinHandler struct {
|
||||
*Loop
|
||||
stdInChan chan Stdin
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func (s *StdinHandler) OnTick(_ Tick) (err error) {
|
||||
select {
|
||||
case stdin := <-s.stdInChan:
|
||||
err = s.Fire(Stdin{stdin.Val})
|
||||
default:
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StdinHandler) ScanJob() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
return
|
||||
default:
|
||||
s.stdInChan <- Stdin{scanner.Text()}
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *StdinHandler) OnStart() (err error) {
|
||||
s.RegisterCritical(s.OnTick)
|
||||
s.stdInChan = make(chan Stdin)
|
||||
go s.ScanJob()
|
||||
return
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IntervalTask struct {
|
||||
F func() error
|
||||
LastTrigger time.Time
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
type OnceTask struct {
|
||||
F func() error
|
||||
Trigger time.Time
|
||||
}
|
||||
|
||||
type Timer struct {
|
||||
Once []OnceTask
|
||||
Interval []IntervalTask
|
||||
Active bool
|
||||
}
|
||||
|
||||
func (t *Timer) Tick() (err error) {
|
||||
if !t.Active {
|
||||
return
|
||||
}
|
||||
for i, task := range t.Interval {
|
||||
if time.Since(task.LastTrigger) >= task.Interval {
|
||||
task.LastTrigger = time.Now()
|
||||
err = task.F()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Interval[i] = task
|
||||
}
|
||||
for i, task := range t.Once {
|
||||
if task.Trigger.After(time.Now()) {
|
||||
err = task.F()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Once = slices.Delete(t.Once, i, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Timer) Every(interval time.Duration, f func() error) {
|
||||
t.Interval = append(t.Interval, IntervalTask{
|
||||
F: f,
|
||||
Interval: interval,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Timer) In(interval time.Duration, f func() error) {
|
||||
t.Once = append(t.Once, OnceTask{
|
||||
F: f,
|
||||
Trigger: time.Now().Add(interval),
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
module github.com/admin-else/strom
|
||||
|
||||
go 1.25
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // FIXME: this should be replaced with my version
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
@@ -2,7 +2,5 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
)
|
||||
|
||||
const (
|
||||
BlocksPerChunkSection = 4096
|
||||
BiomesPerChunkSection = 64
|
||||
)
|
||||
|
||||
func UnpackArrayPalette(r io.Reader, bitsPerEntry uint8, numberOfEntries int) (data []int32, err error) {
|
||||
palletLen, err := proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var pallet []int32 // remember we can't use make because this is user-controlled data
|
||||
for range palletLen {
|
||||
var entry int32
|
||||
entry, err = proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pallet = append(pallet, entry)
|
||||
}
|
||||
data, err = UnpackLongData(r, bitsPerEntry, numberOfEntries)
|
||||
for i, b := range data {
|
||||
data[i] = pallet[b] // maybe check bounds?
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackLongData(r io.Reader, bitsPerEntry uint8, numberOfEntries int) (data []int32, err error) {
|
||||
entriesPerLong := int(64 / bitsPerEntry)
|
||||
numberOfLongs := int(math.Ceil(float64(numberOfEntries) / float64(entriesPerLong)))
|
||||
var dataL []uint64
|
||||
for range numberOfLongs {
|
||||
var entry uint64
|
||||
err = binary.Read(r, binary.BigEndian, &entry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dataL = append(dataL, entry)
|
||||
}
|
||||
|
||||
data = LongsToData(dataL, int32(numberOfEntries), int32(bitsPerEntry))
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackSingleValuePalette(r io.Reader, numberOfEntries int) (data []int32, err error) {
|
||||
entry, err := proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = make([]int32, numberOfEntries)
|
||||
for i := range data {
|
||||
data[i] = entry
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LongsToData(data []uint64, n, bpe int32) (ret []int32) {
|
||||
mask := (uint64(1) << uint32(bpe)) - 1
|
||||
elementsPerLong := 64 / bpe
|
||||
|
||||
ret = make([]int32, n)
|
||||
for i := range n {
|
||||
ret[i] = int32((data[i/elementsPerLong] >> ((i % elementsPerLong) * bpe)) & mask)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnpackBlockData minecraft does this at net.minecraft.world.chunk.PaletteProvider
|
||||
func UnpackBlockData(r io.Reader) (blocks []int32, err error) {
|
||||
var bitsPerEntry uint8
|
||||
err = binary.Read(r, binary.BigEndian, &bitsPerEntry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch bitsPerEntry {
|
||||
case 0:
|
||||
blocks, err = UnpackSingleValuePalette(r, BlocksPerChunkSection)
|
||||
case 1, 2, 3, 4, 5, 6, 7, 8:
|
||||
blocks, err = UnpackArrayPalette(r, bitsPerEntry, BlocksPerChunkSection)
|
||||
default:
|
||||
blocks, err = UnpackLongData(r, bitsPerEntry, BlocksPerChunkSection)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackBiomeData(r io.Reader) (biomes []int32, err error) {
|
||||
// net.minecraft.world.chunk.PaletteProvider#forBiomes
|
||||
var bitsPerEntry uint8
|
||||
err = binary.Read(r, binary.BigEndian, &bitsPerEntry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch bitsPerEntry {
|
||||
case 0:
|
||||
biomes, err = UnpackSingleValuePalette(r, BiomesPerChunkSection)
|
||||
case 1, 2, 3:
|
||||
biomes, err = UnpackArrayPalette(r, bitsPerEntry, BiomesPerChunkSection)
|
||||
default:
|
||||
biomes, err = UnpackLongData(r, bitsPerEntry, BiomesPerChunkSection)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackSection(r io.Reader) (blocks, biomes []int32, err error) {
|
||||
var blockCount int16
|
||||
err = binary.Read(r, binary.BigEndian, &blockCount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
blocks, err = UnpackBlockData(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
biomes, err = UnpackBiomeData(r)
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackNSections(r io.Reader, n int) (blocks, biomes [][]int32, err error) {
|
||||
for range n {
|
||||
var blocksSection, biomesSections []int32
|
||||
blocksSection, biomesSections, err = UnpackSection(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
blocks = append(blocks, blocksSection)
|
||||
biomes = append(biomes, biomesSections)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -9,32 +9,40 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
ServicesHost string
|
||||
SessionHost string
|
||||
ProfilesHost string
|
||||
}
|
||||
|
||||
var DefaultServers = &Servers{
|
||||
ServicesHost: "https://api.minecraftservices.com",
|
||||
SessionHost: "https://sessionserver.mojang.com",
|
||||
ProfilesHost: "https://api.mojang.com",
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
*Servers
|
||||
Name string
|
||||
Uuid uuid.UUID
|
||||
Ygg string
|
||||
}
|
||||
|
||||
const (
|
||||
BaseUrlApi = "https://api.minecraftservices.com"
|
||||
)
|
||||
|
||||
func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) {
|
||||
var b []byte
|
||||
if method != "GET" {
|
||||
b, err = json.Marshal(from)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r, err := http.NewRequest(method, url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
@@ -44,21 +52,22 @@ func (a *Account) doMcApiRequest(method string, url string, from any, to any) (e
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
|
||||
return fmt.Errorf("bad status code %v", resp.StatusCode)
|
||||
}
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, to)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
type PlayerKeys struct {
|
||||
@@ -73,7 +82,7 @@ type PlayerKeys struct {
|
||||
}
|
||||
|
||||
func (a *Account) FetchKeys() (keys PlayerKeys, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/certificates", nil, &keys)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/certificates", nil, &keys)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,10 +99,12 @@ func (a *Account) JoinServer(serverId string) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(a.SessionHost+"/session/minecraft/join", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 204 {
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -112,17 +123,10 @@ type NameChangResp struct {
|
||||
}
|
||||
|
||||
func (a *Account) NameChangeInfo() (r NameChangResp, err error) {
|
||||
err = a.doMcApiRequest("GET", "https://api.minecraftservices.com/minecraft/profile/namechange", nil, &r)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/namechange", nil, &r)
|
||||
return
|
||||
}
|
||||
|
||||
func NewOfflineAccount(name string) *Account {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: crypto.FromOfflinePlayer(name),
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerProfile represents the player's profile information
|
||||
type PlayerProfile struct {
|
||||
ID string `json:"id"`
|
||||
@@ -143,7 +147,7 @@ type PlayerProfile struct {
|
||||
|
||||
// QueryProfile queries the player's profile
|
||||
func (a *Account) QueryProfile() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile", nil, &profile)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +185,7 @@ type PlayerAttributes struct {
|
||||
|
||||
// QueryAttributes queries the player's attributes
|
||||
func (a *Account) QueryAttributes() (attrs PlayerAttributes, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/player/attributes", nil, &attrs)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/player/attributes", nil, &attrs)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@ type ModifyAttributesRequest struct {
|
||||
|
||||
// ModifyAttributes modifies the player's attributes
|
||||
func (a *Account) ModifyAttributes(req ModifyAttributesRequest) (attrs PlayerAttributes, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/attributes", req, &attrs)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/attributes", req, &attrs)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,24 +209,23 @@ type BlockedUsers struct {
|
||||
|
||||
// GetBlocklist gets the list of blocked users
|
||||
func (a *Account) GetBlocklist() (blocklist BlockedUsers, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/privacy/blocklist", nil, &blocklist)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/privacy/blocklist", nil, &blocklist)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckNameAvailability checks if a name is available
|
||||
type NameAvailability struct {
|
||||
Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED
|
||||
}
|
||||
|
||||
// CheckNameAvailability checks if a name is available
|
||||
func (a *Account) CheckNameAvailability(name string) (avail NameAvailability, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile/name/"+name+"/available", nil, &avail)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/name/"+name+"/available", nil, &avail)
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeName changes the player's name
|
||||
func (a *Account) ChangeName(newName string) (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/name/"+newName, nil, &profile)
|
||||
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/name/"+newName, nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,19 +237,19 @@ type ChangeSkinRequest struct {
|
||||
|
||||
// ChangeSkin changes the player's skin
|
||||
func (a *Account) ChangeSkin(req ChangeSkinRequest) (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/minecraft/profile/skins", req, &profile)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/minecraft/profile/skins", req, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// ResetSkin resets the player's skin to default
|
||||
func (a *Account) ResetSkin() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/skins/active", nil, &profile)
|
||||
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/skins/active", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// HideCape hides the player's active cape
|
||||
func (a *Account) HideCape() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/capes/active", nil, &profile)
|
||||
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/capes/active", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,29 +261,58 @@ type ShowCapeRequest struct {
|
||||
// ShowCape shows a specific cape
|
||||
func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) {
|
||||
req := ShowCapeRequest{CapeID: capeID}
|
||||
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/capes/active", req, &profile)
|
||||
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/capes/active", req, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckGiftCodeValidity checks if a gift code is valid
|
||||
func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) {
|
||||
url := BaseUrlApi + "/productvoucher/giftcode"
|
||||
url := a.ServicesHost + "/productvoucher/" + giftCode
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set("Authorization", "Bearer "+a.Ygg)
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Returns HTTP 200 or 204 if valid, 404 if invalid
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 204 {
|
||||
return true, nil
|
||||
valid = true
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewAccountFromYGG creates an Account by authenticating the YGG token against
|
||||
// Mojang's API. It makes an HTTP call to fetch the player profile and returns
|
||||
// an Account with name and UUID populated, or an error if authentication fails.
|
||||
func NewAccountFromYGG(ygg string) (a *Account, err error) {
|
||||
a = &Account{
|
||||
Ygg: ygg,
|
||||
Servers: DefaultServers,
|
||||
}
|
||||
p, err := a.QueryProfile()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a.Name = p.Name
|
||||
a.Uuid, err = uuid.Parse(strings.ReplaceAll(p.ID, "-", ""))
|
||||
return
|
||||
}
|
||||
|
||||
// NewAccount creates an Account from explicit name, UUID, and YGG token without
|
||||
// making any HTTP calls. The account uses Mojang's default API servers.
|
||||
func NewAccount(name string, uuid uuid.UUID, ygg string) (a *Account) {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: uuid,
|
||||
Ygg: ygg,
|
||||
Servers: DefaultServers,
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
)
|
||||
|
||||
// NewOfflineAccount creates an offline-mode Account with a UUID derived from the
|
||||
// player name. No HTTP calls are made; the UUID is computed locally.
|
||||
func NewOfflineAccount(name string) *Account {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: crypto.FromOfflinePlayer(name),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BlockedServers fetches the list of SHA-1 hashes of blocked server IPs from
|
||||
// Mojang's session server. The returned strings are hex-encoded hashes.
|
||||
func BlockedServers() (s []string, err error) {
|
||||
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s = strings.Split(string(d), "\n")
|
||||
return
|
||||
}
|
||||
|
||||
type ProfileProperty struct {
|
||||
Name string `json:"name"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ProfileResp struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Legacy bool `json:"legacy"`
|
||||
Properties []ProfileProperty `json:"properties"`
|
||||
}
|
||||
|
||||
// HasJoined checks with Mojang's session server whether a player has joined a
|
||||
// given server. name is the player name, server is the server hash, and ip is
|
||||
// the optional player IP address. Returns the player's profile on success.
|
||||
func HasJoined(name, server, ip string) (p ProfileResp, err error) {
|
||||
url := "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + name + "&serverId=" + server
|
||||
if ip != "" {
|
||||
url += "&ip=" + ip
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("bad status code %v", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&p)
|
||||
return
|
||||
}
|
||||
|
||||
type MojangPublicKeysResp struct {
|
||||
ProfilePropertyKeys []string `json:"profilePropertyKeys"`
|
||||
PlayerCertificateKeys []string `json:"playerCertificateKeys"`
|
||||
|
||||
// AuthenticationKeys are undocumented by wiki.vg but they exist as of the time of writing so ill include them
|
||||
AuthenticationKeys []string `json:"authenticationKeys"`
|
||||
}
|
||||
|
||||
// MojangPublicKeys fetches Mojang's public keys for profile property signing and
|
||||
// player certificate verification from the session server.
|
||||
func MojangPublicKeys() (keys MojangPublicKeysResp, err error) {
|
||||
resp, err := http.Get("https://sessionserver.mojang.com/session/minecraft/profile/public_key_by_token")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = json.NewDecoder(resp.Body).Decode(&keys)
|
||||
return
|
||||
}
|
||||
|
||||
// Profile fetches a player's profile by UUID from Mojang's session server.
|
||||
// If unsigned is false, signed profile properties are included in the response.
|
||||
func Profile(id string, unsinged bool) (p ProfileResp, err error) {
|
||||
url := "https://sessionserver.mojang.com/session/minecraft/profile/" + id
|
||||
if !unsinged {
|
||||
url += "?unsigned=false"
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = json.NewDecoder(resp.Body).Decode(&p)
|
||||
return
|
||||
}
|
||||
|
||||
type IdName struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// NameToId resolves a Minecraft player name to a UUID and canonical name by
|
||||
// making an HTTP GET to Mojang's API.
|
||||
func NameToId(name string) (idName IdName, err error) {
|
||||
// may also use these:
|
||||
//https://api.mojang.com/users/profiles/minecraft/<player name>
|
||||
//https://api.minecraftservices.com/minecraft/profile/lookup/name/<player name>
|
||||
//https://api.mojang.com/minecraft/profile/lookup/name/<player name>
|
||||
resp, err := http.Get("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = json.NewDecoder(resp.Body).Decode(&idName)
|
||||
return
|
||||
}
|
||||
|
||||
// NameToIdBulk resolves multiple Minecraft player names to UUIDs and canonical
|
||||
// names in a single HTTP POST to Mojang's API.
|
||||
func NameToIdBulk(names []string) (idNames []IdName, err error) {
|
||||
b := bytes.NewBuffer(nil)
|
||||
err = json.NewEncoder(b).Encode(names)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// may also use these:
|
||||
//https://api.mojang.com/profiles/minecraft
|
||||
//https://api.minecraftservices.com/minecraft/profile/lookup/bulk/byname
|
||||
//https://api.mojang.com/minecraft/profile/lookup/bulk/byname
|
||||
r, err := http.NewRequest("POST", "https://api.minecraftservices.com/minecraft/profile/lookup/bulk/byname", b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = json.NewDecoder(resp.Body).Decode(&idNames)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
*proto.Conn
|
||||
|
||||
Account *api.Account
|
||||
|
||||
// Chat signing state
|
||||
canSign bool
|
||||
privKey *rsa.PrivateKey
|
||||
sessionUUID uuid.UUID
|
||||
msgIndex int32
|
||||
}
|
||||
|
||||
// Start creates a chat module for the connection. If acc is an online account,
|
||||
// it fetches the profile key pair from Mojang and enables signed chat.
|
||||
func Start(c *proto.Conn, acc *api.Account) (*Module, error) {
|
||||
m := &Module{Conn: c, Account: acc}
|
||||
if acc != nil && acc.Ygg != "" {
|
||||
if err := m.initSigning(acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Module) initSigning(acc *api.Account) (err error) {
|
||||
keys, err := acc.FetchKeys()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch profile keys: %w", err)
|
||||
}
|
||||
|
||||
priv, err := parsePrivateKey(keys.KeyPair.PrivateKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
|
||||
m.privKey = priv
|
||||
m.sessionUUID = uuid.New()
|
||||
|
||||
pubDer, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal public key: %w", err)
|
||||
}
|
||||
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(keys.PublicKeySignatureV2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode key signature: %w", err)
|
||||
}
|
||||
|
||||
if err = m.Conn.Send(&v1_21_11.PlayToServerPacketChatSessionUpdate{
|
||||
SessionUUID: m.sessionUUID,
|
||||
ExpireTime: keys.ExpiresAt.UnixMilli(),
|
||||
PublicKey: v1_21_11.ByteArray{Val: pubDer},
|
||||
Signature: v1_21_11.ByteArray{Val: sigBytes},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("send chat session update: %w", err)
|
||||
}
|
||||
|
||||
m.canSign = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMessage sends a chat message. If signing is enabled, the message is signed;
|
||||
// otherwise it is sent unsigned.
|
||||
func (m *Module) SendMessage(message string) (err error) {
|
||||
if m.canSign {
|
||||
return m.sendSigned(message)
|
||||
}
|
||||
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: message})
|
||||
}
|
||||
|
||||
func (m *Module) sendSigned(message string) (err error) {
|
||||
now := time.Now()
|
||||
|
||||
saltBytes := make([]byte, 8)
|
||||
if _, err = rand.Read(saltBytes); err != nil {
|
||||
return
|
||||
}
|
||||
salt := int64(binary.BigEndian.Uint64(saltBytes))
|
||||
|
||||
timestamp := now.UnixMilli()
|
||||
|
||||
payload, err := m.buildSignaturePayload(message, salt, timestamp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hashed := sha256.Sum256(payload)
|
||||
sigBytes, err := rsa.SignPKCS1v15(rand.Reader, m.privKey, crypto.SHA256, hashed[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(sigBytes) != 256 {
|
||||
return fmt.Errorf("unexpected signature length: %d", len(sigBytes))
|
||||
}
|
||||
var signature [256]byte
|
||||
copy(signature[:], sigBytes)
|
||||
|
||||
m.msgIndex++
|
||||
|
||||
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{
|
||||
Message: message,
|
||||
Timestamp: timestamp,
|
||||
Salt: salt,
|
||||
Signature: &signature,
|
||||
Offset: 0,
|
||||
Acknowledged: [3]byte{},
|
||||
Checksum: 0,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) buildSignaturePayload(content string, salt, timestampMillis int64) ([]byte, error) {
|
||||
buf := make([]byte, 0, 64+len(content))
|
||||
w := func(b []byte) { buf = append(buf, b...) }
|
||||
|
||||
// Header
|
||||
w(int32Bytes(1))
|
||||
|
||||
// SignedMessageLink: sender UUID, session UUID, index
|
||||
w(uuidBytes(m.Account.Uuid))
|
||||
w(uuidBytes(m.sessionUUID))
|
||||
w(int32Bytes(m.msgIndex))
|
||||
|
||||
// SignedMessageBody: salt, timestamp seconds, content
|
||||
w(int64Bytes(uint64(salt)))
|
||||
w(int64Bytes(uint64(timestampMillis / 1000)))
|
||||
contentBytes := []byte(content)
|
||||
w(int32Bytes(int32(len(contentBytes))))
|
||||
w(contentBytes)
|
||||
|
||||
// LastSeenMessages: empty
|
||||
w(int32Bytes(0))
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func parsePrivateKey(s string) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode([]byte(s))
|
||||
if block != nil {
|
||||
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
k, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key, ok := k.(*rsa.PrivateKey); ok {
|
||||
return key, nil
|
||||
}
|
||||
return nil, fmt.Errorf("not an RSA private key")
|
||||
}
|
||||
|
||||
// Some implementations return raw base64 DER instead of PEM
|
||||
raw, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k, err := x509.ParsePKCS8PrivateKey(raw)
|
||||
if err != nil {
|
||||
k, err = x509.ParsePKCS1PrivateKey(raw)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := k.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not an RSA private key")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func int32Bytes(v int32) []byte {
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, uint32(v))
|
||||
return b
|
||||
}
|
||||
|
||||
func int64Bytes(v uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, v)
|
||||
return b
|
||||
}
|
||||
|
||||
func uuidBytes(u uuid.UUID) []byte {
|
||||
b := make([]byte, 16)
|
||||
copy(b, u[:])
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package keepalive
|
||||
|
||||
import (
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
type mod struct {
|
||||
*proto.Conn
|
||||
}
|
||||
|
||||
func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) {
|
||||
return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId})
|
||||
}
|
||||
|
||||
// Start begins responding to keep-alive packets on the connection.
|
||||
func Start(c *proto.Conn) {
|
||||
m := &mod{Conn: c}
|
||||
m.RegisterUntilLatest(m.onKeepAlive)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/admin-else/strom/mc/level"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
)
|
||||
|
||||
var (
|
||||
ChunkNotLoadedErr = errors.New("chunk not loaded")
|
||||
OutOfBoundsErr = errors.New("coordinates out of bounds")
|
||||
)
|
||||
|
||||
// ChunkPos identifies a chunk column.
|
||||
type ChunkPos struct {
|
||||
X, Z int32
|
||||
}
|
||||
|
||||
// World stores a shareable in-memory view of the Minecraft world.
|
||||
// Multiple bots can subscribe to the same World and update it from their
|
||||
// respective connections.
|
||||
type World struct {
|
||||
mu sync.RWMutex
|
||||
chunks map[ChunkPos]*level.Chunk
|
||||
version string
|
||||
minY int
|
||||
height int
|
||||
}
|
||||
|
||||
// NewWorld creates a new World for the given protocol version and vertical
|
||||
// bounds. For a 1.21 overworld use minY=-64 and height=384.
|
||||
func NewWorld(version string, minY, height int) *World {
|
||||
return &World{
|
||||
chunks: make(map[ChunkPos]*level.Chunk),
|
||||
version: version,
|
||||
minY: minY,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlock returns the global block state ID at the given world coordinates.
|
||||
func (w *World) GetBlock(x, y, z int32) (stateId int32, err error) {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.getBlockLocked(x, y, z)
|
||||
}
|
||||
|
||||
func (w *World) getBlockLocked(x, y, z int32) (stateId int32, err error) {
|
||||
chunkX := floorDiv(x, level.ChunkWidth)
|
||||
chunkZ := floorDiv(z, level.ChunkWidth)
|
||||
chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}]
|
||||
if !ok {
|
||||
err = ChunkNotLoadedErr
|
||||
return
|
||||
}
|
||||
|
||||
sectionIndex := (int(y) - w.minY) / level.ChunkWidth
|
||||
if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) {
|
||||
err = OutOfBoundsErr
|
||||
return
|
||||
}
|
||||
|
||||
lx, ly, lz := blockToLocal(x, y, z)
|
||||
index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx
|
||||
return chunk.Sections[sectionIndex].Blocks.Get(index)
|
||||
}
|
||||
|
||||
// SetBlock updates the global block state ID at the given world coordinates.
|
||||
func (w *World) SetBlock(x, y, z int32, stateId int32) (err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
chunkX := floorDiv(x, level.ChunkWidth)
|
||||
chunkZ := floorDiv(z, level.ChunkWidth)
|
||||
chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}]
|
||||
if !ok {
|
||||
return ChunkNotLoadedErr
|
||||
}
|
||||
|
||||
sectionIndex := (int(y) - w.minY) / level.ChunkWidth
|
||||
if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) {
|
||||
return OutOfBoundsErr
|
||||
}
|
||||
|
||||
lx, ly, lz := blockToLocal(x, y, z)
|
||||
index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx
|
||||
return chunk.Sections[sectionIndex].Blocks.Set(index, stateId)
|
||||
}
|
||||
|
||||
// Module subscribes to chunk and block-change packets and updates a shared
|
||||
// World. Multiple Modules can point to the same World.
|
||||
type Module struct {
|
||||
*proto.Conn
|
||||
world *World
|
||||
}
|
||||
|
||||
// GetBlock returns the block state ID at the given world coordinates via the
|
||||
// shared World.
|
||||
func (m *Module) GetBlock(x, y, z int32) (int32, error) {
|
||||
return m.world.GetBlock(x, y, z)
|
||||
}
|
||||
|
||||
// Start registers packet handlers on c that update the shared World w.
|
||||
func Start(c *proto.Conn, w *World) *Module {
|
||||
m := &Module{Conn: c, world: w}
|
||||
m.RegisterUntilLatest(m.onMapChunk)
|
||||
m.RegisterUntilLatest(m.onBlockChange)
|
||||
m.RegisterUntilLatest(m.onMultiBlockChange)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Module) onMapChunk(p *v1_21_11.PlayToClientPacketMapChunk) (err error) {
|
||||
chunk, err := level.ReadChunkFromChunkPacketData(
|
||||
bytes.NewReader(p.ChunkData.Val),
|
||||
m.world.version,
|
||||
m.world.height,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.world.mu.Lock()
|
||||
m.world.chunks[ChunkPos{p.X, p.Z}] = chunk
|
||||
m.world.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) onBlockChange(p *v1_21_11.PlayToClientPacketBlockChange) (err error) {
|
||||
return m.world.SetBlock(p.Location.X, int32(p.Location.Y), p.Location.Z, p.Type)
|
||||
}
|
||||
|
||||
func (m *Module) onMultiBlockChange(p *v1_21_11.PlayToClientPacketMultiBlockChange) (err error) {
|
||||
chunkX := p.ChunkCoordinates.X
|
||||
chunkZ := p.ChunkCoordinates.Z
|
||||
sectionY := p.ChunkCoordinates.Y
|
||||
|
||||
for _, record := range p.Records {
|
||||
packedPos := record & 0xFFF
|
||||
stateId := record >> 12
|
||||
|
||||
localX := (packedPos >> 8) & 0xF
|
||||
localY := (packedPos >> 4) & 0xF
|
||||
localZ := packedPos & 0xF
|
||||
|
||||
x := chunkX*level.ChunkWidth + int32(localX)
|
||||
y := sectionY*level.ChunkWidth + int32(localY)
|
||||
z := chunkZ*level.ChunkWidth + int32(localZ)
|
||||
|
||||
if err = m.world.SetBlock(x, y, z, stateId); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func floorDiv(a, b int32) int32 {
|
||||
return (a - ((a%b + b) % b)) / b
|
||||
}
|
||||
|
||||
func blockToLocal(x, y, z int32) (lx, ly, lz int32) {
|
||||
lx = x % level.ChunkWidth
|
||||
if lx < 0 {
|
||||
lx += level.ChunkWidth
|
||||
}
|
||||
ly = y % level.ChunkWidth
|
||||
if ly < 0 {
|
||||
ly += level.ChunkWidth
|
||||
}
|
||||
lz = z % level.ChunkWidth
|
||||
if lz < 0 {
|
||||
lz += level.ChunkWidth
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
|
||||
// You must close the connection when you're done with it
|
||||
// version must be a vaild minecraft version eg 1.21.11 if left empty it will be detected with a status-request.
|
||||
func Connect(ctx context.Context, addr string, version string) (ret *proto.Conn, err error) {
|
||||
var protocolVersion int32
|
||||
if version == "" {
|
||||
var s server.StatusResponse
|
||||
s, err = Status(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protocolVersion = s.Version.Protocol
|
||||
} else {
|
||||
var v data.VersionInfo
|
||||
v, err = data.LookUpVersionByName(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protocolVersion = v.Version
|
||||
}
|
||||
ret, err = ConnectVersionLess(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ret.SetProtocolVersion(protocolVersion)
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectVersionLess connects to the given address without setting the version you should set it
|
||||
// when using this function.
|
||||
// You must close the connection when you're done with it
|
||||
func ConnectVersionLess(ctx context.Context, addr string) (ret *proto.Conn, err error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ret = proto.NewConnCtx(ctx)
|
||||
ret.Actor = proto_base.Client
|
||||
ret.Conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.R = ret.Conn
|
||||
ret.W = ret.Conn
|
||||
return
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package client
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
type ConfigIgnorer struct {
|
||||
@@ -19,8 +18,8 @@ func (c *ConfigIgnorer) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ConfigIgnorer) Default(event event.Anything) (err error) {
|
||||
slog.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", event))
|
||||
func (c *ConfigIgnorer) Default(e event.Anything) (err error) {
|
||||
c.Log.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", e))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,6 +47,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet *v1_21_8.ConfigurationToClientPacketK
|
||||
return
|
||||
}
|
||||
|
||||
// IgnoreConfig acknowledges configuration packets on the connection and transitions it to the Play state once configuration is complete.
|
||||
func IgnoreConfig(c *proto.Conn) (err error) {
|
||||
ci := &ConfigIgnorer{c}
|
||||
ci.RegisterCritical(ci.Default)
|
||||
@@ -56,7 +56,7 @@ func IgnoreConfig(c *proto.Conn) (err error) {
|
||||
ci.RegisterUntilLatest(ci.OnPing)
|
||||
ci.RegisterUntilLatest(ci.OnKeepAlive)
|
||||
|
||||
err = ci.Start()
|
||||
err = ci.StartConn()
|
||||
if err != nil {
|
||||
err = errors.Join(err, errors.New("failed to ignore config"))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DNSCheckFunc func(addr string) error
|
||||
|
||||
// DoDNSChecked resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It also optionally accepts a function to "check" the address
|
||||
func DoDNSChecked(ctx context.Context, in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
|
||||
if f == nil {
|
||||
f = func(addr string) error { return nil }
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
if addrErr, ok := errors.AsType[*net.AddrError](err); ok && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = f(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN = uint16(porti)
|
||||
outIP, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
outIP, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
return
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestDoDNS(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Logf("Testing %s", c.Input)
|
||||
_, host, portN, err := DoDns(c.Input)
|
||||
_, host, portN, err := DoDNSChecked(nil, c.Input, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
@@ -4,15 +4,17 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
// MakeHandshakePacket creates a handshake packet using the connection's remote address as the server host.
|
||||
func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
|
||||
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
|
||||
}
|
||||
|
||||
// MakeHandshakePacketAddr gets a custom addr
|
||||
func MakeHandshakePacketAddr(s *proto.Conn, nextState proto_base.State, addr string) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
|
||||
p = &v1_21_8.HandshakingToServerPacketSetProtocol{NextState: int32(nextState), ProtocolVersion: s.ProtocolVersion}
|
||||
var portStr string
|
||||
@@ -1,21 +1,21 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/admin-else/strom/api"
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/text"
|
||||
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v26_2"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
type LoginClient struct {
|
||||
@@ -42,11 +42,10 @@ func (k KickedDuringLoginErr) Error() string {
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
|
||||
err = fmt.Errorf("unexpected event: %#v", event)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnCycle(_ event.Tick) (err error) {
|
||||
if _, ok := event.Val.(proto_base.EncodeDecodeAble); !ok {
|
||||
return
|
||||
}
|
||||
s.Log.Warn("unexpected packet during login client", "packet", event.Val)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,22 +122,31 @@ func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (er
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnSuccess26_2(success *v26_2.LoginToClientPacketSuccess) (err error) {
|
||||
return s.OnSuccess(&v1_21_8.LoginToClientPacketSuccess{
|
||||
Uuid: success.Uuid,
|
||||
Username: success.Username,
|
||||
Properties: success.Properties,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Login(c *proto.Conn, account *api.Account) (err error) {
|
||||
// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling.
|
||||
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
|
||||
lc := &LoginClient{
|
||||
Conn: c,
|
||||
Account: account,
|
||||
}
|
||||
lc.RegisterCritical(lc.OnDefault)
|
||||
lc.RegisterCritical(lc.OnCycle)
|
||||
lc.RegisterCritical(lc.OnClose)
|
||||
lc.RegisterUntilLatest(lc.OnCompress)
|
||||
lc.RegisterUntilLatest(lc.OnEncrypt)
|
||||
lc.RegisterUntilLatest(lc.OnDisconnect)
|
||||
lc.RegisterUntilLatest(lc.OnSuccess)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnCompress)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnEncrypt)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
|
||||
lc.RegisterCriticalUntil("26.1", lc.OnSuccess)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2)
|
||||
|
||||
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login)
|
||||
if err != nil {
|
||||
@@ -152,21 +160,66 @@ func Login(c *proto.Conn, account *api.Account) (err error) {
|
||||
lc.SetState(proto_base.Login)
|
||||
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
||||
|
||||
err = lc.Start()
|
||||
err = lc.StartConn()
|
||||
if err != nil {
|
||||
err = errors.Join(FailedToLoginErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ConnectAndLogin(connectTo string, account *api.Account) (c *proto.Conn, err error) {
|
||||
c, err = Connect(connectTo)
|
||||
// WithoutDns returns a login setting that disables SRV DNS resolution during login.
|
||||
func WithoutDns() func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.NoDns = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext returns a login setting that sets the context used for the connection dial and DNS resolution.
|
||||
func WithContext(ctx context.Context) func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.Ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithVersion returns a login setting that forces a specific Minecraft version string for the connection.
|
||||
func WithVersion(version string) func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
type loginSettings struct {
|
||||
NoDns bool
|
||||
Ctx context.Context
|
||||
Version string
|
||||
IgnoreConfig bool
|
||||
}
|
||||
|
||||
// Login resolves the address with SRV DNS, connects, performs the login handshake, and handles the configuration phase, returning an authenticated connection ready for play.
|
||||
func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) {
|
||||
ls := &loginSettings{}
|
||||
|
||||
for _, setting := range settings {
|
||||
setting(ls)
|
||||
}
|
||||
|
||||
if !ls.NoDns {
|
||||
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c, err = Connect(ls.Ctx, connectTo, ls.Version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = Login(c, account)
|
||||
err = LoginRaw(c, account)
|
||||
if err != nil {
|
||||
err = errors.Join(FailedToLoginErr, err)
|
||||
return
|
||||
}
|
||||
if !ls.IgnoreConfig {
|
||||
err = IgnoreConfig(c)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
func assertClientSendConvertible(t *testing.T, srcType reflect.Type, name string, state proto_base.State, direction proto_base.Direction, targetVersion int32) {
|
||||
t.Helper()
|
||||
targetInfo, ok := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection(name, targetVersion, state, direction)
|
||||
if !ok {
|
||||
t.Fatalf("packet %s not found for version %d", name, targetVersion)
|
||||
}
|
||||
targetType := reflect.TypeOf(targetInfo.Type)
|
||||
if !proto.SmartConvertibleTo(srcType, targetType) {
|
||||
t.Fatalf("%s is not smart-convertible from v1_21_8 to version %d", name, targetVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func assertClientReceiveConvertible(t *testing.T, dstType reflect.Type, name string, state proto_base.State, direction proto_base.Direction, targetVersion int32) {
|
||||
t.Helper()
|
||||
targetInfo, ok := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection(name, targetVersion, state, direction)
|
||||
if !ok {
|
||||
t.Fatalf("packet %s not found for version %d", name, targetVersion)
|
||||
}
|
||||
targetType := reflect.TypeOf(targetInfo.Type)
|
||||
if !proto.SmartConvertibleTo(targetType, dstType) {
|
||||
t.Fatalf("%s is not smart-convertible from version %d to v1_21_8", name, targetVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginClientSendAssumptions(t *testing.T) {
|
||||
v26_1Version := data.MustLookupProtocolVersion("26.1")
|
||||
|
||||
// Packets sent by LoginClient as v1_21_8.
|
||||
assertClientSendConvertible(t, reflect.TypeFor[*v1_21_8.HandshakingToServerPacketSetProtocol](), "set_protocol", proto_base.Handshaking, proto_base.ToServer, v26_1Version)
|
||||
assertClientSendConvertible(t, reflect.TypeFor[*v1_21_8.LoginToServerPacketLoginStart](), "login_start", proto_base.Login, proto_base.ToServer, v26_1Version)
|
||||
assertClientSendConvertible(t, reflect.TypeFor[*v1_21_8.LoginToServerPacketLoginAcknowledged](), "login_acknowledged", proto_base.Login, proto_base.ToServer, v26_1Version)
|
||||
assertClientSendConvertible(t, reflect.TypeFor[*v1_21_8.LoginToServerPacketEncryptionBegin](), "encryption_begin", proto_base.Login, proto_base.ToServer, v26_1Version)
|
||||
}
|
||||
|
||||
func TestLoginClientReceiveAssumptions(t *testing.T) {
|
||||
v26_1Version := data.MustLookupProtocolVersion("26.1")
|
||||
|
||||
// Packets received by LoginClient via RegisterCriticalUntilLatest.
|
||||
// login_success is excluded because 26.2 is not convertible.
|
||||
assertClientReceiveConvertible(t, reflect.TypeFor[*v1_21_8.LoginToClientPacketCompress](), "compress", proto_base.Login, proto_base.ToClient, v26_1Version)
|
||||
assertClientReceiveConvertible(t, reflect.TypeFor[*v1_21_8.LoginToClientPacketEncryptionBegin](), "encryption_begin", proto_base.Login, proto_base.ToClient, v26_1Version)
|
||||
assertClientReceiveConvertible(t, reflect.TypeFor[*v1_21_8.LoginToClientPacketDisconnect](), "disconnect", proto_base.Login, proto_base.ToClient, v26_1Version)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/server"
|
||||
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
type StatusClient struct {
|
||||
@@ -19,10 +19,6 @@ type StatusClient struct {
|
||||
DoPingRoundTripTime bool
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
||||
s.Status = p.Response
|
||||
s.PingSendTime = time.Now()
|
||||
@@ -41,9 +37,10 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
|
||||
}
|
||||
|
||||
// StatusRaw returns a StatusClient that is not connected to a server.
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment
|
||||
func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
c, err := ConnectVersionLess(addr)
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
|
||||
// It does not resolve SRV records.
|
||||
func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
|
||||
c, err := ConnectVersionLess(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -55,9 +52,8 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
s = &StatusClient{
|
||||
Conn: c,
|
||||
}
|
||||
s.Loop = event.NewLoop()
|
||||
s.RegisterUntilLatest(s.OnStatus)
|
||||
s.RegisterUntilLatest(s.OnPong)
|
||||
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||
|
||||
p, err := MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
||||
if err != nil {
|
||||
@@ -74,11 +70,21 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Status(addr string) (status server.StatusResponse, err error) {
|
||||
s, err := StatusRaw(addr)
|
||||
// StatusNoDns is like Status but does not resolve SRV records.
|
||||
func StatusNoDns(addr string) (status server.StatusResponse, err error) {
|
||||
s, err := StatusRaw(context.Background(), addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(s.Status), &status)
|
||||
return
|
||||
}
|
||||
|
||||
// Status resolves the status of the minecraft server at the given address.
|
||||
func Status(ctx context.Context, addr string) (status server.StatusResponse, err error) {
|
||||
addr, _, _, err = DoDNSChecked(ctx, addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return StatusNoDns(addr)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ package crypto
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -37,12 +38,15 @@ type CFB8 struct {
|
||||
ivPos int
|
||||
iv []byte
|
||||
de bool
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewCFB8Decrypt creates a new CFB8 stream cipher in decrypt mode.
|
||||
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
|
||||
return newCFB8(c, iv, true)
|
||||
}
|
||||
|
||||
// NewCFB8Encrypt creates a new CFB8 stream cipher in encrypt mode.
|
||||
func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 {
|
||||
return newCFB8(c, iv, false)
|
||||
}
|
||||
@@ -59,6 +63,8 @@ func newCFB8(c cipher.Block, iv []byte, de bool) *CFB8 {
|
||||
}
|
||||
|
||||
func (cf *CFB8) XORKeyStream(dst, src []byte) {
|
||||
cf.mutex.Lock()
|
||||
defer cf.mutex.Unlock()
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UUIDv3 Variation of google.uuid.NewHash
|
||||
// uuidV3 Variation of google.uuid.NewHash
|
||||
// We need this because we want a 14-byte namespace and Google forces 16
|
||||
func UUIDv3(space []byte, data []byte) uuid.UUID {
|
||||
func uuidV3(space []byte, data []byte) uuid.UUID {
|
||||
h := md5.New()
|
||||
h.Reset()
|
||||
h.Write(space) //nolint:errcheck
|
||||
@@ -21,6 +21,7 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
|
||||
return u
|
||||
}
|
||||
|
||||
// FromOfflinePlayer generates a UUID for an offline-mode (cracked) player from their display name.
|
||||
func FromOfflinePlayer(displayName string) uuid.UUID {
|
||||
return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName))
|
||||
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package data
|
||||
|
||||
import "errors"
|
||||
|
||||
// [
|
||||
//
|
||||
// {
|
||||
// "id": 0,
|
||||
// "name": "badlands",
|
||||
// "category": "mesa",
|
||||
// "temperature": 2.0,
|
||||
// "has_precipitation": false,
|
||||
// "dimension": "overworld",
|
||||
// "displayName": "Badlands",
|
||||
// "color": 7254527
|
||||
// },
|
||||
|
||||
type Biome struct {
|
||||
Id int32
|
||||
Name string
|
||||
Category string
|
||||
Temperature float64
|
||||
HasPrecipitation bool
|
||||
Dimension string
|
||||
DisplayName string
|
||||
Color int
|
||||
}
|
||||
|
||||
var BiomesCache = make(map[string][]*Biome)
|
||||
|
||||
// BiomesForVersion returns the biome definitions for the given Minecraft version, caching them after the first load.
|
||||
func BiomesForVersion(v string) (ret []*Biome) {
|
||||
var ok bool
|
||||
if ret, ok = BiomesCache[v]; ok {
|
||||
return
|
||||
}
|
||||
var b []*Biome
|
||||
must(LoadVersionedJson(v, "biomes", &b))
|
||||
BiomesCache[v] = b
|
||||
return b
|
||||
}
|
||||
|
||||
// LookupBiomeById returns the biome with the given numeric ID in the given version.
|
||||
func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) {
|
||||
for _, b := range BiomesForVersion(version) {
|
||||
if b.Id == id {
|
||||
biome = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var UnknownBiomeNameErr = errors.New("unknown biome Name")
|
||||
|
||||
// LookupBiomeByName returns the biome with the given name in the given version.
|
||||
func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) {
|
||||
for _, b := range BiomesForVersion(version) {
|
||||
if b.Name == name {
|
||||
biome = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package data_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
)
|
||||
|
||||
func TestLookupBiomeById(t *testing.T) {
|
||||
@@ -2,6 +2,10 @@ package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
// {
|
||||
@@ -47,12 +51,15 @@ type Block struct {
|
||||
type BlockState struct {
|
||||
Name string
|
||||
Type string
|
||||
MinValue int32 // this is generated on load and used for state value calculations
|
||||
NumValues int32 `json:"num_values"`
|
||||
Values []string
|
||||
}
|
||||
|
||||
var BlocksCache = make(map[string][]*Block)
|
||||
var BlocksCacheIdMap = make(map[string]map[int32]*Block)
|
||||
|
||||
// BlocksForVersion returns the block definitions for the given Minecraft version, caching them after the first load.
|
||||
func BlocksForVersion(v string) (ret []*Block) {
|
||||
var ok bool
|
||||
if ret, ok = BlocksCache[v]; ok {
|
||||
@@ -61,26 +68,66 @@ func BlocksForVersion(v string) (ret []*Block) {
|
||||
var b []*Block
|
||||
must(LoadVersionedJson(v, "blocks", &b))
|
||||
BlocksCache[v] = b
|
||||
for _, block := range b {
|
||||
for i, state := range block.States {
|
||||
if state.Type != "int" {
|
||||
continue
|
||||
}
|
||||
values := make([]int32, state.NumValues)
|
||||
for j, stateValue := range state.Values {
|
||||
n := util.MustT(strconv.Atoi(stateValue))
|
||||
values[j] = int32(n)
|
||||
}
|
||||
block.States[i].MinValue = slices.Min(values)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// LookupBlockByStateId returns the block for the given global palette state ID in the given version.
|
||||
func LookupBlockByStateId(version string, stateId int32) (block *Block, ok bool) {
|
||||
idmap, found := BlocksCacheIdMap[version]
|
||||
if !found {
|
||||
idmap = make(map[int32]*Block)
|
||||
for _, b := range BlocksForVersion(version) {
|
||||
for i := range b.MaxStateId - b.MinStateId + 1 {
|
||||
idmap[b.MinStateId+i] = b
|
||||
}
|
||||
}
|
||||
BlocksCacheIdMap[version] = idmap
|
||||
}
|
||||
block, ok = idmap[stateId]
|
||||
return
|
||||
}
|
||||
|
||||
var UnknownBlockNameErr = errors.New("unknown block Name")
|
||||
|
||||
// LookupBlockByName returns the block with the given name in the given version.
|
||||
func LookupBlockByName(version string, name string) (block *Block, ok bool) {
|
||||
for _, b := range BlocksForVersion(version) {
|
||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
||||
if b.Name == name {
|
||||
block = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LookupBlockByName(version string, stateId int32) (block *Block, ok bool) {
|
||||
for _, b := range BlocksForVersion(version) {
|
||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
||||
block = b
|
||||
ok = true
|
||||
}
|
||||
// StateIdFromBlocKAndStateMap returns the global palette state ID for the given block name and state property map in the given version.
|
||||
func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) {
|
||||
b, ok := LookupBlockByName(version, name)
|
||||
if !ok {
|
||||
err = BlockNotFoundErr
|
||||
return
|
||||
}
|
||||
if stateMap == nil {
|
||||
stateId = b.DefaultState
|
||||
} else {
|
||||
stateId, err = b.IdFromStateMap(stateMap)
|
||||
}
|
||||
if b.MinStateId > stateId || b.MaxStateId < stateId {
|
||||
err = BlockStateOutOfRangeErr
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -92,7 +139,8 @@ var BadBlockStateTypeErr = errors.New("bad block state type")
|
||||
var BadEnumErr = errors.New("bad enum")
|
||||
var SupplyAllStatesErr = errors.New("supply all states")
|
||||
|
||||
func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]any, err error) {
|
||||
// FromBlockState converts a global palette state ID back into a block definition and a map of state properties.
|
||||
func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]string, err error) {
|
||||
b, ok := LookupBlockByStateId(version, stateId)
|
||||
if !ok {
|
||||
err = BlockNotFoundErr
|
||||
@@ -101,19 +149,17 @@ func FromBlockState(version string, stateId int32) (b *Block, stateData map[stri
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: mc stores these as strings so we will do the same
|
||||
|
||||
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
||||
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v string, err error) {
|
||||
switch s.Type {
|
||||
case "int":
|
||||
v = n
|
||||
v = strconv.Itoa(int(n))
|
||||
case "enum":
|
||||
v = s.Values[n]
|
||||
case "bool":
|
||||
if n == 0 { // MOJANK: 0 is true, 1 is false
|
||||
v = true
|
||||
v = "true"
|
||||
} else if n == 1 {
|
||||
v = false
|
||||
v = "false"
|
||||
} else {
|
||||
err = InvalidBoolValueErr
|
||||
}
|
||||
@@ -123,35 +169,32 @@ func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func mcDataBlockStateTypeSerialize(v any, s *BlockState) (n int32, err error) {
|
||||
switch t := v.(type) {
|
||||
case int32:
|
||||
n = t
|
||||
case bool:
|
||||
if t {
|
||||
n = 0
|
||||
} else {
|
||||
n = 1
|
||||
}
|
||||
case string:
|
||||
for i, enumValue := range s.Values {
|
||||
if enumValue == t {
|
||||
return int32(i), nil
|
||||
}
|
||||
}
|
||||
err = BadEnumErr
|
||||
default:
|
||||
err = BadBlockStateTypeErr
|
||||
func mcDataBlockStateTypeSerialize(v string, s *BlockState) (n int32, err error) {
|
||||
if v == "true" {
|
||||
return 0, nil
|
||||
}
|
||||
if v == "false" {
|
||||
return 1, nil
|
||||
}
|
||||
n1, errMabye := strconv.Atoi(v)
|
||||
if errMabye == nil {
|
||||
return int32(n1) - s.MinValue, nil
|
||||
}
|
||||
for i, enumValue := range s.Values {
|
||||
if enumValue == v {
|
||||
return int32(i), nil
|
||||
}
|
||||
}
|
||||
err = BadEnumErr
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Block) StateMapFromId(stateId int32) (ret map[string]any, err error) {
|
||||
func (b *Block) StateMapFromId(stateId int32) (ret map[string]string, err error) {
|
||||
if stateId < b.MinStateId || stateId > b.MaxStateId {
|
||||
err = BlockStateOutOfRangeErr
|
||||
return
|
||||
}
|
||||
ret = make(map[string]any)
|
||||
ret = make(map[string]string)
|
||||
stateId -= b.MinStateId
|
||||
|
||||
a := int32(1)
|
||||
@@ -166,7 +209,7 @@ func (b *Block) StateMapFromId(stateId int32) (ret map[string]any, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Block) IdFromStateMap(m map[string]any) (ret int32, err error) {
|
||||
func (b *Block) IdFromStateMap(m map[string]string) (ret int32, err error) {
|
||||
a := int32(1)
|
||||
for i := range len(b.States) {
|
||||
s := b.States[len(b.States)-1-i]
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
)
|
||||
|
||||
func TestLookupBlockByStateId(t *testing.T) {
|
||||
@@ -32,3 +32,11 @@ func TestBlockStates(t *testing.T) {
|
||||
t.Fatal("state id mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockStateOutOfRangeBug(t *testing.T) {
|
||||
id, err := data.StateIdFromBlocKAndStateMap("1.21.11", "wildflowers", map[string]string{"facing": "east", "flower_amount": "4"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("State id: %d", id)
|
||||
}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/sh
|
||||
set -xeuo pipefail
|
||||
|
||||
# Copy minecraft-data from the local AnyGate checkout instead of cloning upstream.
|
||||
# This is used when upstream does not yet have the target version or when we want
|
||||
# to test local changes before pushing.
|
||||
|
||||
LOCAL_MINECRAFT_DATA="${LOCAL_MINECRAFT_DATA:-$HOME/src/anygate/minecraft-data}"
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -rf minecraft-data/
|
||||
mkdir -p minecraft-data/pc
|
||||
|
||||
cp "$LOCAL_MINECRAFT_DATA/data/dataPaths.json" minecraft-data/
|
||||
|
||||
copy-mc() {
|
||||
cp -r "$LOCAL_MINECRAFT_DATA/data/pc/$1" minecraft-data/pc
|
||||
}
|
||||
|
||||
copy-mc common
|
||||
|
||||
copy-mc 1.21.8
|
||||
copy-mc 1.21.9
|
||||
copy-mc 1.21.11
|
||||
copy-mc 26.1
|
||||
copy-mc 26.2
|
||||
@@ -0,0 +1,42 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:generate ./download_minecraft_data.sh
|
||||
|
||||
//go:embed minecraft-data/pc minecraft-data/dataPaths.json
|
||||
var MinecraftData embed.FS
|
||||
|
||||
// LoadJson reads a JSON file from the embedded minecraft-data FS and unmarshals it into data.
|
||||
func LoadJson(path string, data any) (err error) {
|
||||
b, err := MinecraftData.ReadFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, data)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadVersionedJson loads a version-specific JSON data file from the embedded minecraft-data FS and unmarshals it into data.
|
||||
func LoadVersionedJson(version, dataName string, data any) (err error) {
|
||||
b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
|
||||
if err != nil {
|
||||
// If you get this error, you need to probably need to update the download script
|
||||
return fmt.Errorf("minecraft-data/%s/%s/%s.json: %w", version, Paths.Data[version][dataName], dataName, err)
|
||||
}
|
||||
err = json.Unmarshal(b, data)
|
||||
return
|
||||
}
|
||||
|
||||
var UnknownMinecraftVersionError = errors.New("unknown minecraft version")
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@ var Paths struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
must(LoadJson("minecraft-data/data/dataPaths.json", &Paths))
|
||||
must(LoadJson("minecraft-data/dataPaths.json", &Paths))
|
||||
}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/sh
|
||||
set -xeuo pipefail
|
||||
|
||||
# Go get / install does not clone git submodules so we have to do it like this
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -rf minecraft-data/
|
||||
git clone --depth 1 git@github.com:PrismarineJS/minecraft-data.git
|
||||
mv minecraft-data minecraft-data-untrimmed
|
||||
|
||||
mkdir minecraft-data
|
||||
mkdir minecraft-data/pc
|
||||
|
||||
mv minecraft-data-untrimmed/data/dataPaths.json minecraft-data
|
||||
|
||||
copy-mc() {
|
||||
mv minecraft-data-untrimmed/data/pc/$1 minecraft-data/pc
|
||||
}
|
||||
|
||||
copy-mc common
|
||||
|
||||
copy-mc 1.21.8
|
||||
copy-mc 1.21.9
|
||||
copy-mc 1.21.11
|
||||
|
||||
rm -rf minecraft-data-untrimmed
|
||||
@@ -1,5 +1,7 @@
|
||||
package data
|
||||
|
||||
import "sync"
|
||||
|
||||
// {
|
||||
// "id": 2,
|
||||
// "name": "granite",
|
||||
@@ -15,18 +17,26 @@ type Item struct {
|
||||
}
|
||||
|
||||
var ItemsCache = make(map[string][]*Item)
|
||||
var ItemsCacheMutex = &sync.RWMutex{}
|
||||
|
||||
// ItemsForVersion returns the item definitions for the given Minecraft version, caching them after the first load.
|
||||
func ItemsForVersion(v string) (ret []*Item) {
|
||||
var ok bool
|
||||
ItemsCacheMutex.RLock()
|
||||
if ret, ok = ItemsCache[v]; ok {
|
||||
ItemsCacheMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
ItemsCacheMutex.RUnlock()
|
||||
var b []*Item
|
||||
must(LoadVersionedJson(v, "items", &b))
|
||||
ItemsCacheMutex.Lock()
|
||||
ItemsCache[v] = b
|
||||
ItemsCacheMutex.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
// LookupItemById returns the item with the given numeric ID in the given version.
|
||||
func LookupItemById(version string, id int32) (item *Item, ok bool) {
|
||||
for _, i := range ItemsForVersion(version) {
|
||||
if i.Id == id {
|
||||
@@ -38,6 +48,7 @@ func LookupItemById(version string, id int32) (item *Item, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// LookupItemByName returns the item with the given name in the given version.
|
||||
func LookupItemByName(version string, name string) (item *Item, ok bool) {
|
||||
for _, i := range ItemsForVersion(version) {
|
||||
if i.Name == name {
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,247 @@
|
||||
[
|
||||
{
|
||||
"name": "armor",
|
||||
"resource": "minecraft:armor",
|
||||
"min": 0.0,
|
||||
"max": 30.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "armorToughness",
|
||||
"resource": "minecraft:armor_toughness",
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "attackDamage",
|
||||
"resource": "minecraft:attack_damage",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 2.0
|
||||
},
|
||||
{
|
||||
"name": "attackKnockback",
|
||||
"resource": "minecraft:attack_knockback",
|
||||
"min": 0.0,
|
||||
"max": 5.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "attackSpeed",
|
||||
"resource": "minecraft:attack_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 4.0
|
||||
},
|
||||
{
|
||||
"name": "blockBreakSpeed",
|
||||
"resource": "minecraft:block_break_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "blockInteractionRange",
|
||||
"resource": "minecraft:block_interaction_range",
|
||||
"min": 0.0,
|
||||
"max": 64.0,
|
||||
"default": 4.5
|
||||
},
|
||||
{
|
||||
"name": "burningTime",
|
||||
"resource": "minecraft:burning_time",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "cameraDistance",
|
||||
"resource": "minecraft:camera_distance",
|
||||
"min": 0.0,
|
||||
"max": 32.0,
|
||||
"default": 4.0
|
||||
},
|
||||
{
|
||||
"name": "explosionKnockbackResistance",
|
||||
"resource": "minecraft:explosion_knockback_resistance",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "entityInteractionRange",
|
||||
"resource": "minecraft:entity_interaction_range",
|
||||
"min": 0.0,
|
||||
"max": 64.0,
|
||||
"default": 3.0
|
||||
},
|
||||
{
|
||||
"name": "fallDamageMultiplier",
|
||||
"resource": "minecraft:fall_damage_multiplier",
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "flyingSpeed",
|
||||
"resource": "minecraft:flying_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.4
|
||||
},
|
||||
{
|
||||
"name": "followRange",
|
||||
"resource": "minecraft:follow_range",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 32.0
|
||||
},
|
||||
{
|
||||
"name": "gravity",
|
||||
"resource": "minecraft:gravity",
|
||||
"min": -1.0,
|
||||
"max": 1.0,
|
||||
"default": 0.08
|
||||
},
|
||||
{
|
||||
"name": "jumpStrength",
|
||||
"resource": "minecraft:jump_strength",
|
||||
"min": 0.0,
|
||||
"max": 32.0,
|
||||
"default": 0.41999998688697815
|
||||
},
|
||||
{
|
||||
"name": "knockbackResistance",
|
||||
"resource": "minecraft:knockback_resistance",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "luck",
|
||||
"resource": "minecraft:luck",
|
||||
"min": -1024.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "maxAbsorption",
|
||||
"resource": "minecraft:max_absorption",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "maxHealth",
|
||||
"resource": "minecraft:max_health",
|
||||
"min": 1.0,
|
||||
"max": 1024.0,
|
||||
"default": 20.0
|
||||
},
|
||||
{
|
||||
"name": "miningEfficiency",
|
||||
"resource": "minecraft:mining_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "movementEfficiency",
|
||||
"resource": "minecraft:movement_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "movementSpeed",
|
||||
"resource": "minecraft:movement_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.7
|
||||
},
|
||||
{
|
||||
"name": "oxygenBonus",
|
||||
"resource": "minecraft:oxygen_bonus",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "safeFallDistance",
|
||||
"resource": "minecraft:safe_fall_distance",
|
||||
"min": -1024.0,
|
||||
"max": 1024.0,
|
||||
"default": 3.0
|
||||
},
|
||||
{
|
||||
"name": "scale",
|
||||
"resource": "minecraft:scale",
|
||||
"min": 0.0625,
|
||||
"max": 16.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "sneakingSpeed",
|
||||
"resource": "minecraft:sneaking_speed",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.3
|
||||
},
|
||||
{
|
||||
"name": "spawnReinforcements",
|
||||
"resource": "minecraft:spawn_reinforcements",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "stepHeight",
|
||||
"resource": "minecraft:step_height",
|
||||
"min": 0.0,
|
||||
"max": 10.0,
|
||||
"default": 0.6
|
||||
},
|
||||
{
|
||||
"name": "submergedMiningSpeed",
|
||||
"resource": "minecraft:submerged_mining_speed",
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"default": 0.2
|
||||
},
|
||||
{
|
||||
"name": "sweepingDamageRatio",
|
||||
"resource": "minecraft:sweeping_damage_ratio",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "temptRange",
|
||||
"resource": "minecraft:tempt_range",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 10.0
|
||||
},
|
||||
{
|
||||
"name": "waterMovementEfficiency",
|
||||
"resource": "minecraft:water_movement_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "waypointTransmitRange",
|
||||
"resource": "minecraft:waypoint_transmit_range",
|
||||
"min": 0.0,
|
||||
"max": 6.0E7,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "waypointReceiveRange",
|
||||
"resource": "minecraft:waypoint_receive_range",
|
||||
"min": 0.0,
|
||||
"max": 6.0E7,
|
||||
"default": 0.0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,652 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Badlands",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "bamboo_jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Bamboo Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "basalt_deltas",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Basalt Deltas",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "beach",
|
||||
"category": "beach",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Beach",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "birch_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.6,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Birch Forest",
|
||||
"color": 8037887
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "cherry_grove",
|
||||
"category": "forest",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Cherry Grove",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "cold_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Cold Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "crimson_forest",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Crimson Forest",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "dark_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Dark Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "deep_cold_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Cold Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "deep_dark",
|
||||
"category": "underground",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Dark",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "deep_frozen_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Frozen Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "deep_lukewarm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Lukewarm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "deep_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "desert",
|
||||
"category": "desert",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Desert",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "dripstone_caves",
|
||||
"category": "underground",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Dripstone Caves",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "end_barrens",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Barrens",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "end_highlands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Highlands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "end_midlands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Midlands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "eroded_badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Eroded Badlands",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "flower_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Flower Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "frozen_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen Ocean",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "frozen_peaks",
|
||||
"category": "ice",
|
||||
"temperature": -0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen Peaks",
|
||||
"color": 8756735
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "frozen_river",
|
||||
"category": "ice",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen River",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "grove",
|
||||
"category": "forest",
|
||||
"temperature": -0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Grove",
|
||||
"color": 8495359
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "ice_spikes",
|
||||
"category": "ice",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Ice Spikes",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "jagged_peaks",
|
||||
"category": "mountain",
|
||||
"temperature": -0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Jagged Peaks",
|
||||
"color": 8756735
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "lukewarm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Lukewarm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "lush_caves",
|
||||
"category": "underground",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Lush Caves",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "mangrove_swamp",
|
||||
"category": "forest",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Mangrove Swamp",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "meadow",
|
||||
"category": "mountain",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Meadow",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "mushroom_fields",
|
||||
"category": "mushroom",
|
||||
"temperature": 0.9,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Mushroom Fields",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "nether_wastes",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Nether Wastes",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "old_growth_birch_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.6,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Birch Forest",
|
||||
"color": 8037887
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "old_growth_pine_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.3,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Pine Taiga",
|
||||
"color": 8168447
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "old_growth_spruce_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.25,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Spruce Taiga",
|
||||
"color": 8233983
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "pale_garden",
|
||||
"category": "none",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Pale Garden",
|
||||
"color": 12171705
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Plains",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "river",
|
||||
"category": "river",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "River",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "savanna",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Savanna",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "savanna_plateau",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Savanna Plateau",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "small_end_islands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "Small End Islands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "snowy_beach",
|
||||
"category": "beach",
|
||||
"temperature": 0.05,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Beach",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "snowy_plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Plains",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "snowy_slopes",
|
||||
"category": "mountain",
|
||||
"temperature": -0.3,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Slopes",
|
||||
"color": 8560639
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "snowy_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": -0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Taiga",
|
||||
"color": 8625919
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "soul_sand_valley",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Soul Sand Valley",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "sparse_jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Sparse Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "stony_peaks",
|
||||
"category": "mountain",
|
||||
"temperature": 1.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Stony Peaks",
|
||||
"color": 7776511
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "stony_shore",
|
||||
"category": "beach",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Stony Shore",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "sunflower_plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Sunflower Plains",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "swamp",
|
||||
"category": "swamp",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Swamp",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.25,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Taiga",
|
||||
"color": 8233983
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "the_end",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "The End",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "the_void",
|
||||
"category": "none",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "The Void",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "warm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Warm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "warped_forest",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Warped Forest",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "windswept_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Forest",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "windswept_gravelly_hills",
|
||||
"category": "extreme_hills",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Gravelly Hills",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "windswept_hills",
|
||||
"category": "extreme_hills",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Hills",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "windswept_savanna",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Savanna",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "wooded_badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Wooded Badlands",
|
||||
"color": 7254527
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Speed",
|
||||
"displayName": "Speed",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Slowness",
|
||||
"displayName": "Slowness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Haste",
|
||||
"displayName": "Haste",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "MiningFatigue",
|
||||
"displayName": "Mining Fatigue",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Strength",
|
||||
"displayName": "Strength",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "InstantHealth",
|
||||
"displayName": "Instant Health",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "InstantDamage",
|
||||
"displayName": "Instant Damage",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "JumpBoost",
|
||||
"displayName": "Jump Boost",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Nausea",
|
||||
"displayName": "Nausea",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Regeneration",
|
||||
"displayName": "Regeneration",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Resistance",
|
||||
"displayName": "Resistance",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "FireResistance",
|
||||
"displayName": "Fire Resistance",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "WaterBreathing",
|
||||
"displayName": "Water Breathing",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Invisibility",
|
||||
"displayName": "Invisibility",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Blindness",
|
||||
"displayName": "Blindness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "NightVision",
|
||||
"displayName": "Night Vision",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Hunger",
|
||||
"displayName": "Hunger",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "Weakness",
|
||||
"displayName": "Weakness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Poison",
|
||||
"displayName": "Poison",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "Wither",
|
||||
"displayName": "Wither",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "HealthBoost",
|
||||
"displayName": "Health Boost",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Absorption",
|
||||
"displayName": "Absorption",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Saturation",
|
||||
"displayName": "Saturation",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "Glowing",
|
||||
"displayName": "Glowing",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "Levitation",
|
||||
"displayName": "Levitation",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "Luck",
|
||||
"displayName": "Luck",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "BadLuck",
|
||||
"displayName": "Bad Luck",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "SlowFalling",
|
||||
"displayName": "Slow Falling",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "ConduitPower",
|
||||
"displayName": "Conduit Power",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "DolphinsGrace",
|
||||
"displayName": "Dolphin's Grace",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "BadOmen",
|
||||
"displayName": "Bad Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "HeroOfTheVillage",
|
||||
"displayName": "Hero of the Village",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "Darkness",
|
||||
"displayName": "Darkness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "TrialOmen",
|
||||
"displayName": "Trial Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "RaidOmen",
|
||||
"displayName": "Raid Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "WindCharged",
|
||||
"displayName": "Wind Charged",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Weaving",
|
||||
"displayName": "Weaving",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Oozing",
|
||||
"displayName": "Oozing",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Infested",
|
||||
"displayName": "Infested",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "BreathOfTheNautilus",
|
||||
"displayName": "Breath of the Nautilus",
|
||||
"type": "good"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,980 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "aqua_affinity",
|
||||
"displayName": "Aqua Affinity",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "head_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "bane_of_arthropods",
|
||||
"displayName": "Bane of Arthropods",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "weapon",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "binding_curse",
|
||||
"displayName": "Curse of Binding",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": true,
|
||||
"exclude": [],
|
||||
"category": "equippable",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "blast_protection",
|
||||
"displayName": "Blast Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"fire_protection",
|
||||
"projectile_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "breach",
|
||||
"displayName": "Breach",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "mace",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "channeling",
|
||||
"displayName": "Channeling",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"riptide"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "density",
|
||||
"displayName": "Density",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "mace",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "depth_strider",
|
||||
"displayName": "Depth Strider",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"frost_walker"
|
||||
],
|
||||
"category": "foot_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "efficiency",
|
||||
"displayName": "Efficiency",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "mining",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "feather_falling",
|
||||
"displayName": "Feather Falling",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "foot_armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "fire_aspect",
|
||||
"displayName": "Fire Aspect",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fire_aspect",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "fire_protection",
|
||||
"displayName": "Fire Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"projectile_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "flame",
|
||||
"displayName": "Flame",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "fortune",
|
||||
"displayName": "Fortune",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"silk_touch"
|
||||
],
|
||||
"category": "mining_loot",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "frost_walker",
|
||||
"displayName": "Frost Walker",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"depth_strider"
|
||||
],
|
||||
"category": "foot_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "impaling",
|
||||
"displayName": "Impaling",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "infinity",
|
||||
"displayName": "Infinity",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"mending"
|
||||
],
|
||||
"category": "bow",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "knockback",
|
||||
"displayName": "Knockback",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "melee_weapon",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "looting",
|
||||
"displayName": "Looting",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "melee_weapon",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "loyalty",
|
||||
"displayName": "Loyalty",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"riptide"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "luck_of_the_sea",
|
||||
"displayName": "Luck of the Sea",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fishing",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "lunge",
|
||||
"displayName": "Lunge",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "lunge",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "lure",
|
||||
"displayName": "Lure",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fishing",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "mending",
|
||||
"displayName": "Mending",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"infinity"
|
||||
],
|
||||
"category": "durability",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "multishot",
|
||||
"displayName": "Multishot",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"piercing"
|
||||
],
|
||||
"category": "crossbow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "piercing",
|
||||
"displayName": "Piercing",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"multishot"
|
||||
],
|
||||
"category": "crossbow",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "projectile_protection",
|
||||
"displayName": "Projectile Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"fire_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "protection",
|
||||
"displayName": "Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"fire_protection",
|
||||
"projectile_protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "punch",
|
||||
"displayName": "Punch",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "quick_charge",
|
||||
"displayName": "Quick Charge",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "crossbow",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "respiration",
|
||||
"displayName": "Respiration",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "head_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "riptide",
|
||||
"displayName": "Riptide",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"channeling",
|
||||
"loyalty"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "sharpness",
|
||||
"displayName": "Sharpness",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"smite"
|
||||
],
|
||||
"category": "sharp_weapon",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "silk_touch",
|
||||
"displayName": "Silk Touch",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"fortune"
|
||||
],
|
||||
"category": "mining_loot",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "smite",
|
||||
"displayName": "Smite",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness"
|
||||
],
|
||||
"category": "weapon",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "soul_speed",
|
||||
"displayName": "Soul Speed",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "foot_armor",
|
||||
"weight": 1,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "sweeping_edge",
|
||||
"displayName": "Sweeping Edge",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "sweeping",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "swift_sneak",
|
||||
"displayName": "Swift Sneak",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "leg_armor",
|
||||
"weight": 1,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "thorns",
|
||||
"displayName": "Thorns",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "armor",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "unbreaking",
|
||||
"displayName": "Unbreaking",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "durability",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "vanishing_curse",
|
||||
"displayName": "Curse of Vanishing",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": true,
|
||||
"exclude": [],
|
||||
"category": "vanishing",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "wind_burst",
|
||||
"displayName": "Wind Burst",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "mace",
|
||||
"weight": 2,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,442 @@
|
||||
[
|
||||
{
|
||||
"id": 893,
|
||||
"name": "apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 19.2,
|
||||
"effectiveQuality": 23.2,
|
||||
"saturationRatio": 4.8
|
||||
},
|
||||
{
|
||||
"id": 947,
|
||||
"name": "mushroom_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Mushroom Stew",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 953,
|
||||
"name": "bread",
|
||||
"stackSize": 64,
|
||||
"displayName": "Bread",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 983,
|
||||
"name": "porkchop",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Porkchop",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 984,
|
||||
"name": "cooked_porkchop",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Porkchop",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 204.8,
|
||||
"effectiveQuality": 212.8,
|
||||
"saturationRatio": 25.6
|
||||
},
|
||||
{
|
||||
"id": 986,
|
||||
"name": "golden_apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Golden Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 80.8,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 987,
|
||||
"name": "enchanted_golden_apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Enchanted Golden Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 80.8,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1019,
|
||||
"name": "pufferfish_bucket",
|
||||
"stackSize": 1,
|
||||
"displayName": "Bucket of Pufferfish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1020,
|
||||
"name": "salmon_bucket",
|
||||
"stackSize": 1,
|
||||
"displayName": "Bucket of Salmon",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1021,
|
||||
"name": "cod_bucket",
|
||||
"stackSize": 1,
|
||||
"displayName": "Bucket of Cod",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1022,
|
||||
"name": "tropical_fish_bucket",
|
||||
"stackSize": 1,
|
||||
"displayName": "Bucket of Tropical Fish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1057,
|
||||
"name": "cod",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Cod",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1058,
|
||||
"name": "salmon",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Salmon",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1059,
|
||||
"name": "tropical_fish",
|
||||
"stackSize": 64,
|
||||
"displayName": "Tropical Fish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1060,
|
||||
"name": "pufferfish",
|
||||
"stackSize": 64,
|
||||
"displayName": "Pufferfish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1061,
|
||||
"name": "cooked_cod",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Cod",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1062,
|
||||
"name": "cooked_salmon",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Salmon",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 115.200005,
|
||||
"effectiveQuality": 121.200005,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1102,
|
||||
"name": "cookie",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cookie",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1106,
|
||||
"name": "melon_slice",
|
||||
"stackSize": 64,
|
||||
"displayName": "Melon Slice",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1107,
|
||||
"name": "dried_kelp",
|
||||
"stackSize": 64,
|
||||
"displayName": "Dried Kelp",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 1.2,
|
||||
"effectiveQuality": 2.2,
|
||||
"saturationRatio": 1.2
|
||||
},
|
||||
{
|
||||
"id": 1110,
|
||||
"name": "beef",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Beef",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 1111,
|
||||
"name": "cooked_beef",
|
||||
"stackSize": 64,
|
||||
"displayName": "Steak",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 204.8,
|
||||
"effectiveQuality": 212.8,
|
||||
"saturationRatio": 25.6
|
||||
},
|
||||
{
|
||||
"id": 1112,
|
||||
"name": "chicken",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Chicken",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1113,
|
||||
"name": "cooked_chicken",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Chicken",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1114,
|
||||
"name": "rotten_flesh",
|
||||
"stackSize": 64,
|
||||
"displayName": "Rotten Flesh",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 6.4,
|
||||
"effectiveQuality": 10.4,
|
||||
"saturationRatio": 1.6
|
||||
},
|
||||
{
|
||||
"id": 1122,
|
||||
"name": "spider_eye",
|
||||
"stackSize": 64,
|
||||
"displayName": "Spider Eye",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 12.8,
|
||||
"effectiveQuality": 14.8,
|
||||
"saturationRatio": 6.4
|
||||
},
|
||||
{
|
||||
"id": 1227,
|
||||
"name": "carrot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Carrot",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 21.6,
|
||||
"effectiveQuality": 24.6,
|
||||
"saturationRatio": 7.2000003
|
||||
},
|
||||
{
|
||||
"id": 1228,
|
||||
"name": "potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Potato",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 1.2,
|
||||
"effectiveQuality": 2.2,
|
||||
"saturationRatio": 1.2
|
||||
},
|
||||
{
|
||||
"id": 1229,
|
||||
"name": "baked_potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Baked Potato",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1230,
|
||||
"name": "poisonous_potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Poisonous Potato",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1232,
|
||||
"name": "golden_carrot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Golden Carrot",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 172.8,
|
||||
"effectiveQuality": 178.8,
|
||||
"saturationRatio": 28.800001
|
||||
},
|
||||
{
|
||||
"id": 1241,
|
||||
"name": "pumpkin_pie",
|
||||
"stackSize": 64,
|
||||
"displayName": "Pumpkin Pie",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 84.8,
|
||||
"saturationRatio": 9.6
|
||||
},
|
||||
{
|
||||
"id": 1249,
|
||||
"name": "rabbit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Rabbit",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 1250,
|
||||
"name": "cooked_rabbit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Rabbit",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1251,
|
||||
"name": "rabbit_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Rabbit Stew",
|
||||
"foodPoints": 10.0,
|
||||
"saturation": 240.0,
|
||||
"effectiveQuality": 250.0,
|
||||
"saturationRatio": 24.0
|
||||
},
|
||||
{
|
||||
"id": 1264,
|
||||
"name": "mutton",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Mutton",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1265,
|
||||
"name": "cooked_mutton",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Mutton",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 115.200005,
|
||||
"effectiveQuality": 121.200005,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1283,
|
||||
"name": "chorus_fruit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Chorus Fruit",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 19.2,
|
||||
"effectiveQuality": 23.2,
|
||||
"saturationRatio": 4.8
|
||||
},
|
||||
{
|
||||
"id": 1287,
|
||||
"name": "beetroot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Beetroot",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 2.4,
|
||||
"effectiveQuality": 3.4,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1289,
|
||||
"name": "beetroot_soup",
|
||||
"stackSize": 1,
|
||||
"displayName": "Beetroot Soup",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1340,
|
||||
"name": "suspicious_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Suspicious Stew",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1373,
|
||||
"name": "sweet_berries",
|
||||
"stackSize": 64,
|
||||
"displayName": "Sweet Berries",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1374,
|
||||
"name": "glow_berries",
|
||||
"stackSize": 64,
|
||||
"displayName": "Glow Berries",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1381,
|
||||
"name": "honey_bottle",
|
||||
"stackSize": 16,
|
||||
"displayName": "Honey Bottle",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 14.400001,
|
||||
"effectiveQuality": 20.400002,
|
||||
"saturationRatio": 2.4
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "harp"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "basedrum"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "snare"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "hat"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "bass"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "flute"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "bell"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "guitar"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "chime"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "xylophone"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "iron_xylophone"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "cow_bell"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "didgeridoo"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "bit"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "banjo"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "pling"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "zombie"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "skeleton"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "creeper"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "dragon"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "wither_skeleton"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "piglin"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "custom_head"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"default": {},
|
||||
"leaves": {
|
||||
"1105": 1.0,
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5
|
||||
},
|
||||
"coweb": {
|
||||
"1105": 15.0,
|
||||
"911": 15.0,
|
||||
"916": 15.0,
|
||||
"921": 15.0,
|
||||
"926": 15.0,
|
||||
"931": 15.0,
|
||||
"936": 15.0,
|
||||
"941": 15.0
|
||||
},
|
||||
"plant": {
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5
|
||||
},
|
||||
"gourd": {
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5
|
||||
},
|
||||
"vine_or_glow_lichen": {
|
||||
"1105": 2.0
|
||||
},
|
||||
"wool": {
|
||||
"1105": 1.0
|
||||
},
|
||||
"sword_instantly_mines": {
|
||||
"911": 2.0,
|
||||
"916": 1.0,
|
||||
"921": 4.0,
|
||||
"926": 12.0,
|
||||
"931": 6.0,
|
||||
"936": 8.0,
|
||||
"941": 9.0
|
||||
},
|
||||
"sword_efficient": {
|
||||
"911": 2.0,
|
||||
"916": 1.0,
|
||||
"921": 4.0,
|
||||
"926": 12.0,
|
||||
"931": 6.0,
|
||||
"936": 8.0,
|
||||
"941": 9.0
|
||||
},
|
||||
"incorrect_for_wooden_tool": {
|
||||
"912": 2.0,
|
||||
"913": 2.0,
|
||||
"914": 2.0,
|
||||
"915": 2.0
|
||||
},
|
||||
"mineable/shovel": {
|
||||
"912": 2.0,
|
||||
"917": 1.0,
|
||||
"922": 4.0,
|
||||
"927": 12.0,
|
||||
"932": 6.0,
|
||||
"937": 8.0,
|
||||
"942": 9.0
|
||||
},
|
||||
"mineable/pickaxe": {
|
||||
"913": 2.0,
|
||||
"918": 1.0,
|
||||
"923": 4.0,
|
||||
"928": 12.0,
|
||||
"933": 6.0,
|
||||
"938": 8.0,
|
||||
"943": 9.0
|
||||
},
|
||||
"mineable/axe": {
|
||||
"914": 2.0,
|
||||
"919": 1.0,
|
||||
"924": 4.0,
|
||||
"929": 12.0,
|
||||
"934": 6.0,
|
||||
"939": 8.0,
|
||||
"944": 9.0
|
||||
},
|
||||
"mineable/hoe": {
|
||||
"915": 2.0,
|
||||
"920": 1.0,
|
||||
"925": 4.0,
|
||||
"930": 12.0,
|
||||
"935": 6.0,
|
||||
"940": 8.0,
|
||||
"945": 9.0
|
||||
},
|
||||
"incorrect_for_copper_tool": {
|
||||
"917": 1.0,
|
||||
"918": 1.0,
|
||||
"919": 1.0,
|
||||
"920": 1.0
|
||||
},
|
||||
"incorrect_for_stone_tool": {
|
||||
"922": 4.0,
|
||||
"923": 4.0,
|
||||
"924": 4.0,
|
||||
"925": 4.0
|
||||
},
|
||||
"incorrect_for_gold_tool": {
|
||||
"927": 12.0,
|
||||
"928": 12.0,
|
||||
"929": 12.0,
|
||||
"930": 12.0
|
||||
},
|
||||
"incorrect_for_iron_tool": {
|
||||
"932": 6.0,
|
||||
"933": 6.0,
|
||||
"934": 6.0,
|
||||
"935": 6.0
|
||||
},
|
||||
"incorrect_for_diamond_tool": {
|
||||
"937": 8.0,
|
||||
"938": 8.0,
|
||||
"939": 8.0,
|
||||
"940": 8.0
|
||||
},
|
||||
"incorrect_for_netherite_tool": {
|
||||
"942": 9.0,
|
||||
"943": 9.0,
|
||||
"944": 9.0,
|
||||
"945": 9.0
|
||||
},
|
||||
"plant;mineable/axe": {
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5,
|
||||
"914": 2.0,
|
||||
"919": 1.0,
|
||||
"924": 4.0,
|
||||
"929": 12.0,
|
||||
"934": 6.0,
|
||||
"939": 8.0,
|
||||
"944": 9.0
|
||||
},
|
||||
"gourd;mineable/axe": {
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5,
|
||||
"914": 2.0,
|
||||
"919": 1.0,
|
||||
"924": 4.0,
|
||||
"929": 12.0,
|
||||
"934": 6.0,
|
||||
"939": 8.0,
|
||||
"944": 9.0
|
||||
},
|
||||
"leaves;mineable/hoe": {
|
||||
"1105": 1.0,
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5,
|
||||
"915": 2.0,
|
||||
"920": 1.0,
|
||||
"925": 4.0,
|
||||
"930": 12.0,
|
||||
"935": 6.0,
|
||||
"940": 8.0,
|
||||
"945": 9.0
|
||||
},
|
||||
"leaves;mineable/axe;mineable/hoe": {
|
||||
"1105": 1.0,
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5,
|
||||
"914": 2.0,
|
||||
"919": 1.0,
|
||||
"924": 4.0,
|
||||
"929": 12.0,
|
||||
"934": 6.0,
|
||||
"939": 8.0,
|
||||
"944": 9.0,
|
||||
"915": 2.0,
|
||||
"920": 1.0,
|
||||
"925": 4.0,
|
||||
"930": 12.0,
|
||||
"935": 6.0,
|
||||
"940": 8.0,
|
||||
"945": 9.0
|
||||
},
|
||||
"vine_or_glow_lichen;plant;mineable/axe": {
|
||||
"1105": 2.0,
|
||||
"911": 1.5,
|
||||
"916": 1.5,
|
||||
"921": 1.5,
|
||||
"926": 1.5,
|
||||
"931": 1.5,
|
||||
"936": 1.5,
|
||||
"941": 1.5,
|
||||
"914": 2.0,
|
||||
"919": 1.0,
|
||||
"924": 4.0,
|
||||
"929": 12.0,
|
||||
"934": 6.0,
|
||||
"939": 8.0,
|
||||
"944": 9.0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "angry_villager"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "block"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "block_marker"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "bubble"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "cloud"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "copper_fire_flame"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "crit"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "damage_indicator"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "dragon_breath"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "dripping_lava"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "falling_lava"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "landing_lava"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "dripping_water"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "falling_water"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "dust"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "dust_color_transition"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "effect"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "elder_guardian"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "enchanted_hit"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "enchant"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "end_rod"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "entity_effect"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "explosion_emitter"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "explosion"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "gust"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "small_gust"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "gust_emitter_large"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "gust_emitter_small"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "sonic_boom"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "falling_dust"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "firework"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "fishing"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "flame"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "infested"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "cherry_leaves"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "pale_oak_leaves"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "tinted_leaves"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "sculk_soul"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "sculk_charge"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "sculk_charge_pop"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "soul_fire_flame"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "soul"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "flash"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "happy_villager"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "composter"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "heart"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "instant_effect"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "item"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "vibration"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "trail"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "item_slime"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "item_cobweb"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "item_snowball"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "large_smoke"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "lava"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "mycelium"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "note"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "poof"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "portal"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "rain"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "smoke"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "white_smoke"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "sneeze"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "spit"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "squid_ink"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"name": "sweep_attack"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"name": "totem_of_undying"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"name": "underwater"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"name": "splash"
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"name": "witch"
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"name": "bubble_pop"
|
||||
},
|
||||
{
|
||||
"id": 71,
|
||||
"name": "current_down"
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"name": "bubble_column_up"
|
||||
},
|
||||
{
|
||||
"id": 73,
|
||||
"name": "nautilus"
|
||||
},
|
||||
{
|
||||
"id": 74,
|
||||
"name": "dolphin"
|
||||
},
|
||||
{
|
||||
"id": 75,
|
||||
"name": "campfire_cosy_smoke"
|
||||
},
|
||||
{
|
||||
"id": 76,
|
||||
"name": "campfire_signal_smoke"
|
||||
},
|
||||
{
|
||||
"id": 77,
|
||||
"name": "dripping_honey"
|
||||
},
|
||||
{
|
||||
"id": 78,
|
||||
"name": "falling_honey"
|
||||
},
|
||||
{
|
||||
"id": 79,
|
||||
"name": "landing_honey"
|
||||
},
|
||||
{
|
||||
"id": 80,
|
||||
"name": "falling_nectar"
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"name": "falling_spore_blossom"
|
||||
},
|
||||
{
|
||||
"id": 82,
|
||||
"name": "ash"
|
||||
},
|
||||
{
|
||||
"id": 83,
|
||||
"name": "crimson_spore"
|
||||
},
|
||||
{
|
||||
"id": 84,
|
||||
"name": "warped_spore"
|
||||
},
|
||||
{
|
||||
"id": 85,
|
||||
"name": "spore_blossom_air"
|
||||
},
|
||||
{
|
||||
"id": 86,
|
||||
"name": "dripping_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 87,
|
||||
"name": "falling_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 88,
|
||||
"name": "landing_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 89,
|
||||
"name": "reverse_portal"
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"name": "white_ash"
|
||||
},
|
||||
{
|
||||
"id": 91,
|
||||
"name": "small_flame"
|
||||
},
|
||||
{
|
||||
"id": 92,
|
||||
"name": "snowflake"
|
||||
},
|
||||
{
|
||||
"id": 93,
|
||||
"name": "dripping_dripstone_lava"
|
||||
},
|
||||
{
|
||||
"id": 94,
|
||||
"name": "falling_dripstone_lava"
|
||||
},
|
||||
{
|
||||
"id": 95,
|
||||
"name": "dripping_dripstone_water"
|
||||
},
|
||||
{
|
||||
"id": 96,
|
||||
"name": "falling_dripstone_water"
|
||||
},
|
||||
{
|
||||
"id": 97,
|
||||
"name": "glow_squid_ink"
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"name": "glow"
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"name": "wax_on"
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "wax_off"
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "electric_spark"
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "scrape"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "shriek"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"name": "egg_crack"
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"name": "dust_plume"
|
||||
},
|
||||
{
|
||||
"id": 106,
|
||||
"name": "trial_spawner_detection"
|
||||
},
|
||||
{
|
||||
"id": 107,
|
||||
"name": "trial_spawner_detection_ominous"
|
||||
},
|
||||
{
|
||||
"id": 108,
|
||||
"name": "vault_connection"
|
||||
},
|
||||
{
|
||||
"id": 109,
|
||||
"name": "dust_pillar"
|
||||
},
|
||||
{
|
||||
"id": 110,
|
||||
"name": "ominous_spawning"
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"name": "raid_omen"
|
||||
},
|
||||
{
|
||||
"id": 112,
|
||||
"name": "trial_omen"
|
||||
},
|
||||
{
|
||||
"id": 113,
|
||||
"name": "block_crumble"
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"name": "firefly"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"grass": {
|
||||
"data": [
|
||||
{
|
||||
"keys": [
|
||||
"badlands",
|
||||
"eroded_badlands",
|
||||
"wooded_badlands"
|
||||
],
|
||||
"color": -7306931
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"bamboo_jungle",
|
||||
"basalt_deltas",
|
||||
"beach",
|
||||
"birch_forest",
|
||||
"cold_ocean",
|
||||
"crimson_forest",
|
||||
"deep_cold_ocean",
|
||||
"deep_dark",
|
||||
"deep_frozen_ocean",
|
||||
"deep_lukewarm_ocean",
|
||||
"deep_ocean",
|
||||
"desert",
|
||||
"dripstone_caves",
|
||||
"end_barrens",
|
||||
"end_highlands",
|
||||
"end_midlands",
|
||||
"flower_forest",
|
||||
"forest",
|
||||
"frozen_ocean",
|
||||
"frozen_peaks",
|
||||
"frozen_river",
|
||||
"grove",
|
||||
"ice_spikes",
|
||||
"jagged_peaks",
|
||||
"jungle",
|
||||
"lukewarm_ocean",
|
||||
"lush_caves",
|
||||
"meadow",
|
||||
"mushroom_fields",
|
||||
"nether_wastes",
|
||||
"ocean",
|
||||
"old_growth_birch_forest",
|
||||
"old_growth_pine_taiga",
|
||||
"old_growth_spruce_taiga",
|
||||
"plains",
|
||||
"river",
|
||||
"savanna",
|
||||
"savanna_plateau",
|
||||
"small_end_islands",
|
||||
"snowy_beach",
|
||||
"snowy_plains",
|
||||
"snowy_slopes",
|
||||
"snowy_taiga",
|
||||
"soul_sand_valley",
|
||||
"sparse_jungle",
|
||||
"stony_peaks",
|
||||
"stony_shore",
|
||||
"sunflower_plains",
|
||||
"taiga",
|
||||
"the_end",
|
||||
"the_void",
|
||||
"warm_ocean",
|
||||
"warped_forest",
|
||||
"windswept_forest",
|
||||
"windswept_gravelly_hills",
|
||||
"windswept_hills",
|
||||
"windswept_savanna"
|
||||
],
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"cherry_grove"
|
||||
],
|
||||
"color": -4793503
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"dark_forest"
|
||||
],
|
||||
"color": 1317381
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"mangrove_swamp",
|
||||
"swamp"
|
||||
],
|
||||
"color": 6975545
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"pale_garden"
|
||||
],
|
||||
"color": -8945038
|
||||
}
|
||||
]
|
||||
},
|
||||
"foliage": {
|
||||
"data": [
|
||||
{
|
||||
"keys": [
|
||||
"badlands",
|
||||
"eroded_badlands",
|
||||
"wooded_badlands"
|
||||
],
|
||||
"color": -6389427
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"bamboo_jungle",
|
||||
"basalt_deltas",
|
||||
"beach",
|
||||
"birch_forest",
|
||||
"cold_ocean",
|
||||
"crimson_forest",
|
||||
"dark_forest",
|
||||
"deep_cold_ocean",
|
||||
"deep_dark",
|
||||
"deep_frozen_ocean",
|
||||
"deep_lukewarm_ocean",
|
||||
"deep_ocean",
|
||||
"desert",
|
||||
"dripstone_caves",
|
||||
"end_barrens",
|
||||
"end_highlands",
|
||||
"end_midlands",
|
||||
"flower_forest",
|
||||
"forest",
|
||||
"frozen_ocean",
|
||||
"frozen_peaks",
|
||||
"frozen_river",
|
||||
"grove",
|
||||
"ice_spikes",
|
||||
"jagged_peaks",
|
||||
"jungle",
|
||||
"lukewarm_ocean",
|
||||
"lush_caves",
|
||||
"meadow",
|
||||
"mushroom_fields",
|
||||
"nether_wastes",
|
||||
"ocean",
|
||||
"old_growth_birch_forest",
|
||||
"old_growth_pine_taiga",
|
||||
"old_growth_spruce_taiga",
|
||||
"plains",
|
||||
"river",
|
||||
"savanna",
|
||||
"savanna_plateau",
|
||||
"small_end_islands",
|
||||
"snowy_beach",
|
||||
"snowy_plains",
|
||||
"snowy_slopes",
|
||||
"snowy_taiga",
|
||||
"soul_sand_valley",
|
||||
"sparse_jungle",
|
||||
"stony_peaks",
|
||||
"stony_shore",
|
||||
"sunflower_plains",
|
||||
"taiga",
|
||||
"the_end",
|
||||
"the_void",
|
||||
"warm_ocean",
|
||||
"warped_forest",
|
||||
"windswept_forest",
|
||||
"windswept_gravelly_hills",
|
||||
"windswept_hills",
|
||||
"windswept_savanna"
|
||||
],
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"cherry_grove"
|
||||
],
|
||||
"color": -4793503
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"mangrove_swamp"
|
||||
],
|
||||
"color": -7491289
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"pale_garden"
|
||||
],
|
||||
"color": -7893642
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"swamp"
|
||||
],
|
||||
"color": -9801671
|
||||
}
|
||||
]
|
||||
},
|
||||
"water": {
|
||||
"data": [
|
||||
{
|
||||
"keys": [
|
||||
"badlands",
|
||||
"bamboo_jungle",
|
||||
"basalt_deltas",
|
||||
"beach",
|
||||
"birch_forest",
|
||||
"crimson_forest",
|
||||
"dark_forest",
|
||||
"deep_dark",
|
||||
"deep_ocean",
|
||||
"desert",
|
||||
"dripstone_caves",
|
||||
"end_barrens",
|
||||
"end_highlands",
|
||||
"end_midlands",
|
||||
"eroded_badlands",
|
||||
"flower_forest",
|
||||
"forest",
|
||||
"frozen_peaks",
|
||||
"grove",
|
||||
"ice_spikes",
|
||||
"jagged_peaks",
|
||||
"jungle",
|
||||
"lush_caves",
|
||||
"mushroom_fields",
|
||||
"nether_wastes",
|
||||
"ocean",
|
||||
"old_growth_birch_forest",
|
||||
"old_growth_pine_taiga",
|
||||
"old_growth_spruce_taiga",
|
||||
"plains",
|
||||
"river",
|
||||
"savanna",
|
||||
"savanna_plateau",
|
||||
"small_end_islands",
|
||||
"snowy_plains",
|
||||
"snowy_slopes",
|
||||
"soul_sand_valley",
|
||||
"sparse_jungle",
|
||||
"stony_peaks",
|
||||
"stony_shore",
|
||||
"sunflower_plains",
|
||||
"taiga",
|
||||
"the_end",
|
||||
"the_void",
|
||||
"warped_forest",
|
||||
"windswept_forest",
|
||||
"windswept_gravelly_hills",
|
||||
"windswept_hills",
|
||||
"windswept_savanna",
|
||||
"wooded_badlands"
|
||||
],
|
||||
"color": -12618012
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"cherry_grove"
|
||||
],
|
||||
"color": -10635281
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"cold_ocean",
|
||||
"deep_cold_ocean",
|
||||
"snowy_beach",
|
||||
"snowy_taiga"
|
||||
],
|
||||
"color": -12757034
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"deep_frozen_ocean",
|
||||
"frozen_ocean",
|
||||
"frozen_river"
|
||||
],
|
||||
"color": -13027127
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"deep_lukewarm_ocean",
|
||||
"lukewarm_ocean"
|
||||
],
|
||||
"color": -12210702
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"mangrove_swamp"
|
||||
],
|
||||
"color": -12944790
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"meadow"
|
||||
],
|
||||
"color": -15839537
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"pale_garden"
|
||||
],
|
||||
"color": -9008995
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"swamp"
|
||||
],
|
||||
"color": -10388636
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"warm_ocean"
|
||||
],
|
||||
"color": -12331538
|
||||
}
|
||||
]
|
||||
},
|
||||
"redstone": {
|
||||
"data": [
|
||||
{
|
||||
"keys": [
|
||||
0
|
||||
],
|
||||
"color": 4980736
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
1
|
||||
],
|
||||
"color": 7340032
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
2
|
||||
],
|
||||
"color": 7995392
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
3
|
||||
],
|
||||
"color": 8650752
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
4
|
||||
],
|
||||
"color": 9306112
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
5
|
||||
],
|
||||
"color": 10027008
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
6
|
||||
],
|
||||
"color": 10682368
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
7
|
||||
],
|
||||
"color": 11337728
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
8
|
||||
],
|
||||
"color": 11993088
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
9
|
||||
],
|
||||
"color": 12648448
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
10
|
||||
],
|
||||
"color": 13369344
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
11
|
||||
],
|
||||
"color": 14024704
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
12
|
||||
],
|
||||
"color": 14680064
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
13
|
||||
],
|
||||
"color": 15336960
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
14
|
||||
],
|
||||
"color": 15997696
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
15
|
||||
],
|
||||
"color": 16724480
|
||||
}
|
||||
]
|
||||
},
|
||||
"constant": {
|
||||
"data": [
|
||||
{
|
||||
"keys": [
|
||||
"birch_leaves"
|
||||
],
|
||||
"color": -8345771
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"spruce_leaves"
|
||||
],
|
||||
"color": -10380959
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"lily_pad"
|
||||
],
|
||||
"color": -14647248
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"attached_melon_stem"
|
||||
],
|
||||
"color": -2046180
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"attached_pumpkin_stem"
|
||||
],
|
||||
"color": -2046180
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"melon_stem"
|
||||
],
|
||||
"color": -16711936
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"pumpkin_stem"
|
||||
],
|
||||
"color": -16711936
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 774,
|
||||
"minecraftVersion": "1.21.11",
|
||||
"majorVersion": "1.21",
|
||||
"releaseType": "release"
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
[
|
||||
{
|
||||
"name": "armor",
|
||||
"resource": "minecraft:armor",
|
||||
"min": 0.0,
|
||||
"max": 30.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "armorToughness",
|
||||
"resource": "minecraft:armor_toughness",
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "attackDamage",
|
||||
"resource": "minecraft:attack_damage",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 2.0
|
||||
},
|
||||
{
|
||||
"name": "attackKnockback",
|
||||
"resource": "minecraft:attack_knockback",
|
||||
"min": 0.0,
|
||||
"max": 5.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "attackSpeed",
|
||||
"resource": "minecraft:attack_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 4.0
|
||||
},
|
||||
{
|
||||
"name": "blockBreakSpeed",
|
||||
"resource": "minecraft:block_break_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "blockInteractionRange",
|
||||
"resource": "minecraft:block_interaction_range",
|
||||
"min": 0.0,
|
||||
"max": 64.0,
|
||||
"default": 4.5
|
||||
},
|
||||
{
|
||||
"name": "burningTime",
|
||||
"resource": "minecraft:burning_time",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "cameraDistance",
|
||||
"resource": "minecraft:camera_distance",
|
||||
"min": 0.0,
|
||||
"max": 32.0,
|
||||
"default": 4.0
|
||||
},
|
||||
{
|
||||
"name": "explosionKnockbackResistance",
|
||||
"resource": "minecraft:explosion_knockback_resistance",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "entityInteractionRange",
|
||||
"resource": "minecraft:entity_interaction_range",
|
||||
"min": 0.0,
|
||||
"max": 64.0,
|
||||
"default": 3.0
|
||||
},
|
||||
{
|
||||
"name": "fallDamageMultiplier",
|
||||
"resource": "minecraft:fall_damage_multiplier",
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "flyingSpeed",
|
||||
"resource": "minecraft:flying_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.4
|
||||
},
|
||||
{
|
||||
"name": "followRange",
|
||||
"resource": "minecraft:follow_range",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 32.0
|
||||
},
|
||||
{
|
||||
"name": "gravity",
|
||||
"resource": "minecraft:gravity",
|
||||
"min": -1.0,
|
||||
"max": 1.0,
|
||||
"default": 0.08
|
||||
},
|
||||
{
|
||||
"name": "jumpStrength",
|
||||
"resource": "minecraft:jump_strength",
|
||||
"min": 0.0,
|
||||
"max": 32.0,
|
||||
"default": 0.41999998688697815
|
||||
},
|
||||
{
|
||||
"name": "knockbackResistance",
|
||||
"resource": "minecraft:knockback_resistance",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "luck",
|
||||
"resource": "minecraft:luck",
|
||||
"min": -1024.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "maxAbsorption",
|
||||
"resource": "minecraft:max_absorption",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "maxHealth",
|
||||
"resource": "minecraft:max_health",
|
||||
"min": 1.0,
|
||||
"max": 1024.0,
|
||||
"default": 20.0
|
||||
},
|
||||
{
|
||||
"name": "miningEfficiency",
|
||||
"resource": "minecraft:mining_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "movementEfficiency",
|
||||
"resource": "minecraft:movement_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "movementSpeed",
|
||||
"resource": "minecraft:movement_speed",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.7
|
||||
},
|
||||
{
|
||||
"name": "oxygenBonus",
|
||||
"resource": "minecraft:oxygen_bonus",
|
||||
"min": 0.0,
|
||||
"max": 1024.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "safeFallDistance",
|
||||
"resource": "minecraft:safe_fall_distance",
|
||||
"min": -1024.0,
|
||||
"max": 1024.0,
|
||||
"default": 3.0
|
||||
},
|
||||
{
|
||||
"name": "scale",
|
||||
"resource": "minecraft:scale",
|
||||
"min": 0.0625,
|
||||
"max": 16.0,
|
||||
"default": 1.0
|
||||
},
|
||||
{
|
||||
"name": "sneakingSpeed",
|
||||
"resource": "minecraft:sneaking_speed",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.3
|
||||
},
|
||||
{
|
||||
"name": "spawnReinforcements",
|
||||
"resource": "minecraft:spawn_reinforcements",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "stepHeight",
|
||||
"resource": "minecraft:step_height",
|
||||
"min": 0.0,
|
||||
"max": 10.0,
|
||||
"default": 0.6
|
||||
},
|
||||
{
|
||||
"name": "submergedMiningSpeed",
|
||||
"resource": "minecraft:submerged_mining_speed",
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"default": 0.2
|
||||
},
|
||||
{
|
||||
"name": "sweepingDamageRatio",
|
||||
"resource": "minecraft:sweeping_damage_ratio",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "temptRange",
|
||||
"resource": "minecraft:tempt_range",
|
||||
"min": 0.0,
|
||||
"max": 2048.0,
|
||||
"default": 10.0
|
||||
},
|
||||
{
|
||||
"name": "waterMovementEfficiency",
|
||||
"resource": "minecraft:water_movement_efficiency",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "waypointTransmitRange",
|
||||
"resource": "minecraft:waypoint_transmit_range",
|
||||
"min": 0.0,
|
||||
"max": 6.0E7,
|
||||
"default": 0.0
|
||||
},
|
||||
{
|
||||
"name": "waypointReceiveRange",
|
||||
"resource": "minecraft:waypoint_receive_range",
|
||||
"min": 0.0,
|
||||
"max": 6.0E7,
|
||||
"default": 0.0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,652 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Badlands",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "bamboo_jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Bamboo Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "basalt_deltas",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Basalt Deltas",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "beach",
|
||||
"category": "beach",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Beach",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "birch_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.6,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Birch Forest",
|
||||
"color": 8037887
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "cherry_grove",
|
||||
"category": "forest",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Cherry Grove",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "cold_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Cold Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "crimson_forest",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Crimson Forest",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "dark_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Dark Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "deep_cold_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Cold Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "deep_dark",
|
||||
"category": "underground",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Dark",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "deep_frozen_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Frozen Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "deep_lukewarm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Lukewarm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "deep_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Deep Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "desert",
|
||||
"category": "desert",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Desert",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "dripstone_caves",
|
||||
"category": "underground",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Dripstone Caves",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "end_barrens",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Barrens",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "end_highlands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Highlands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "end_midlands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "End Midlands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "eroded_badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Eroded Badlands",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "flower_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Flower Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Forest",
|
||||
"color": 7972607
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "frozen_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen Ocean",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "frozen_peaks",
|
||||
"category": "ice",
|
||||
"temperature": -0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen Peaks",
|
||||
"color": 8756735
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "frozen_river",
|
||||
"category": "ice",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Frozen River",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "grove",
|
||||
"category": "forest",
|
||||
"temperature": -0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Grove",
|
||||
"color": 8495359
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "ice_spikes",
|
||||
"category": "ice",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Ice Spikes",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "jagged_peaks",
|
||||
"category": "mountain",
|
||||
"temperature": -0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Jagged Peaks",
|
||||
"color": 8756735
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "lukewarm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Lukewarm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "lush_caves",
|
||||
"category": "underground",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Lush Caves",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "mangrove_swamp",
|
||||
"category": "forest",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Mangrove Swamp",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "meadow",
|
||||
"category": "mountain",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Meadow",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "mushroom_fields",
|
||||
"category": "mushroom",
|
||||
"temperature": 0.9,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Mushroom Fields",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "nether_wastes",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Nether Wastes",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "old_growth_birch_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.6,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Birch Forest",
|
||||
"color": 8037887
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "old_growth_pine_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.3,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Pine Taiga",
|
||||
"color": 8168447
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "old_growth_spruce_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.25,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Old Growth Spruce Taiga",
|
||||
"color": 8233983
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "pale_garden",
|
||||
"category": "none",
|
||||
"temperature": 0.7,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Pale Garden",
|
||||
"color": 12171705
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Plains",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "river",
|
||||
"category": "river",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "River",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "savanna",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Savanna",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "savanna_plateau",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Savanna Plateau",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "small_end_islands",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "Small End Islands",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "snowy_beach",
|
||||
"category": "beach",
|
||||
"temperature": 0.05,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Beach",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "snowy_plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Plains",
|
||||
"color": 8364543
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "snowy_slopes",
|
||||
"category": "mountain",
|
||||
"temperature": -0.3,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Slopes",
|
||||
"color": 8560639
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "snowy_taiga",
|
||||
"category": "taiga",
|
||||
"temperature": -0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Snowy Taiga",
|
||||
"color": 8625919
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "soul_sand_valley",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Soul Sand Valley",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "sparse_jungle",
|
||||
"category": "jungle",
|
||||
"temperature": 0.95,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Sparse Jungle",
|
||||
"color": 7842047
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "stony_peaks",
|
||||
"category": "mountain",
|
||||
"temperature": 1.0,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Stony Peaks",
|
||||
"color": 7776511
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "stony_shore",
|
||||
"category": "beach",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Stony Shore",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "sunflower_plains",
|
||||
"category": "plains",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Sunflower Plains",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "swamp",
|
||||
"category": "swamp",
|
||||
"temperature": 0.8,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Swamp",
|
||||
"color": 7907327
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "taiga",
|
||||
"category": "taiga",
|
||||
"temperature": 0.25,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Taiga",
|
||||
"color": 8233983
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "the_end",
|
||||
"category": "the_end",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "end",
|
||||
"displayName": "The End",
|
||||
"color": 0
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "the_void",
|
||||
"category": "none",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "The Void",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "warm_ocean",
|
||||
"category": "ocean",
|
||||
"temperature": 0.5,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Warm Ocean",
|
||||
"color": 8103167
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "warped_forest",
|
||||
"category": "nether",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "nether",
|
||||
"displayName": "Warped Forest",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "windswept_forest",
|
||||
"category": "forest",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Forest",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "windswept_gravelly_hills",
|
||||
"category": "extreme_hills",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Gravelly Hills",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "windswept_hills",
|
||||
"category": "extreme_hills",
|
||||
"temperature": 0.2,
|
||||
"has_precipitation": true,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Hills",
|
||||
"color": 8233727
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "windswept_savanna",
|
||||
"category": "savanna",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Windswept Savanna",
|
||||
"color": 7254527
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "wooded_badlands",
|
||||
"category": "mesa",
|
||||
"temperature": 2.0,
|
||||
"has_precipitation": false,
|
||||
"dimension": "overworld",
|
||||
"displayName": "Wooded Badlands",
|
||||
"color": 7254527
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,236 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Speed",
|
||||
"displayName": "Speed",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Slowness",
|
||||
"displayName": "Slowness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Haste",
|
||||
"displayName": "Haste",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "MiningFatigue",
|
||||
"displayName": "Mining Fatigue",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Strength",
|
||||
"displayName": "Strength",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "InstantHealth",
|
||||
"displayName": "Instant Health",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "InstantDamage",
|
||||
"displayName": "Instant Damage",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "JumpBoost",
|
||||
"displayName": "Jump Boost",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Nausea",
|
||||
"displayName": "Nausea",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Regeneration",
|
||||
"displayName": "Regeneration",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Resistance",
|
||||
"displayName": "Resistance",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "FireResistance",
|
||||
"displayName": "Fire Resistance",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "WaterBreathing",
|
||||
"displayName": "Water Breathing",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Invisibility",
|
||||
"displayName": "Invisibility",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "Blindness",
|
||||
"displayName": "Blindness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "NightVision",
|
||||
"displayName": "Night Vision",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Hunger",
|
||||
"displayName": "Hunger",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "Weakness",
|
||||
"displayName": "Weakness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Poison",
|
||||
"displayName": "Poison",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "Wither",
|
||||
"displayName": "Wither",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "HealthBoost",
|
||||
"displayName": "Health Boost",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Absorption",
|
||||
"displayName": "Absorption",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Saturation",
|
||||
"displayName": "Saturation",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "Glowing",
|
||||
"displayName": "Glowing",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "Levitation",
|
||||
"displayName": "Levitation",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "Luck",
|
||||
"displayName": "Luck",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "BadLuck",
|
||||
"displayName": "Bad Luck",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "SlowFalling",
|
||||
"displayName": "Slow Falling",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "ConduitPower",
|
||||
"displayName": "Conduit Power",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "DolphinsGrace",
|
||||
"displayName": "Dolphin's Grace",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "BadOmen",
|
||||
"displayName": "Bad Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "HeroOfTheVillage",
|
||||
"displayName": "Hero of the Village",
|
||||
"type": "good"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "Darkness",
|
||||
"displayName": "Darkness",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "TrialOmen",
|
||||
"displayName": "Trial Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "RaidOmen",
|
||||
"displayName": "Raid Omen",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "WindCharged",
|
||||
"displayName": "Wind Charged",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Weaving",
|
||||
"displayName": "Weaving",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Oozing",
|
||||
"displayName": "Oozing",
|
||||
"type": "bad"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Infested",
|
||||
"displayName": "Infested",
|
||||
"type": "bad"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,959 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "aqua_affinity",
|
||||
"displayName": "Aqua Affinity",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "head_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "bane_of_arthropods",
|
||||
"displayName": "Bane of Arthropods",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "weapon",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "binding_curse",
|
||||
"displayName": "Curse of Binding",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": true,
|
||||
"exclude": [],
|
||||
"category": "equippable",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "blast_protection",
|
||||
"displayName": "Blast Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"fire_protection",
|
||||
"projectile_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "breach",
|
||||
"displayName": "Breach",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "mace",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "channeling",
|
||||
"displayName": "Channeling",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"riptide"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "density",
|
||||
"displayName": "Density",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"impaling",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "mace",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "depth_strider",
|
||||
"displayName": "Depth Strider",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"frost_walker"
|
||||
],
|
||||
"category": "foot_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "efficiency",
|
||||
"displayName": "Efficiency",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "mining",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "feather_falling",
|
||||
"displayName": "Feather Falling",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "foot_armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "fire_aspect",
|
||||
"displayName": "Fire Aspect",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fire_aspect",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "fire_protection",
|
||||
"displayName": "Fire Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"projectile_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "flame",
|
||||
"displayName": "Flame",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "fortune",
|
||||
"displayName": "Fortune",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"silk_touch"
|
||||
],
|
||||
"category": "mining_loot",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "frost_walker",
|
||||
"displayName": "Frost Walker",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"depth_strider"
|
||||
],
|
||||
"category": "foot_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "impaling",
|
||||
"displayName": "Impaling",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"sharpness",
|
||||
"smite"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "infinity",
|
||||
"displayName": "Infinity",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"mending"
|
||||
],
|
||||
"category": "bow",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "knockback",
|
||||
"displayName": "Knockback",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "sword",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "looting",
|
||||
"displayName": "Looting",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "sword",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "loyalty",
|
||||
"displayName": "Loyalty",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"riptide"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "luck_of_the_sea",
|
||||
"displayName": "Luck of the Sea",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fishing",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "lure",
|
||||
"displayName": "Lure",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "fishing",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "mending",
|
||||
"displayName": "Mending",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"infinity"
|
||||
],
|
||||
"category": "durability",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "multishot",
|
||||
"displayName": "Multishot",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"piercing"
|
||||
],
|
||||
"category": "crossbow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "piercing",
|
||||
"displayName": "Piercing",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"multishot"
|
||||
],
|
||||
"category": "crossbow",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "projectile_protection",
|
||||
"displayName": "Projectile Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"fire_protection",
|
||||
"protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "protection",
|
||||
"displayName": "Protection",
|
||||
"maxLevel": 4,
|
||||
"minCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 3,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"blast_protection",
|
||||
"fire_protection",
|
||||
"projectile_protection"
|
||||
],
|
||||
"category": "armor",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "punch",
|
||||
"displayName": "Punch",
|
||||
"maxLevel": 2,
|
||||
"minCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 1,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "bow",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "quick_charge",
|
||||
"displayName": "Quick Charge",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "crossbow",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "respiration",
|
||||
"displayName": "Respiration",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "head_armor",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "riptide",
|
||||
"displayName": "Riptide",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"channeling",
|
||||
"loyalty"
|
||||
],
|
||||
"category": "trident",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "sharpness",
|
||||
"displayName": "Sharpness",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"smite"
|
||||
],
|
||||
"category": "sharp_weapon",
|
||||
"weight": 10,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "silk_touch",
|
||||
"displayName": "Silk Touch",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"fortune"
|
||||
],
|
||||
"category": "mining_loot",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "smite",
|
||||
"displayName": "Smite",
|
||||
"maxLevel": 5,
|
||||
"minCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 4,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [
|
||||
"bane_of_arthropods",
|
||||
"breach",
|
||||
"density",
|
||||
"impaling",
|
||||
"sharpness"
|
||||
],
|
||||
"category": "weapon",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "soul_speed",
|
||||
"displayName": "Soul Speed",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "foot_armor",
|
||||
"weight": 1,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "sweeping_edge",
|
||||
"displayName": "Sweeping Edge",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "sword",
|
||||
"weight": 2,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "swift_sneak",
|
||||
"displayName": "Swift Sneak",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "leg_armor",
|
||||
"weight": 1,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "thorns",
|
||||
"displayName": "Thorns",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "armor",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "unbreaking",
|
||||
"displayName": "Unbreaking",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": false,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "durability",
|
||||
"weight": 5,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "vanishing_curse",
|
||||
"displayName": "Curse of Vanishing",
|
||||
"maxLevel": 1,
|
||||
"minCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 0,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": true,
|
||||
"exclude": [],
|
||||
"category": "vanishing",
|
||||
"weight": 1,
|
||||
"tradeable": true,
|
||||
"discoverable": true
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "wind_burst",
|
||||
"displayName": "Wind Burst",
|
||||
"maxLevel": 3,
|
||||
"minCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"maxCost": {
|
||||
"a": 2,
|
||||
"b": 1
|
||||
},
|
||||
"treasureOnly": true,
|
||||
"curse": false,
|
||||
"exclude": [],
|
||||
"category": "mace",
|
||||
"weight": 2,
|
||||
"tradeable": false,
|
||||
"discoverable": false
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,402 @@
|
||||
[
|
||||
{
|
||||
"id": 857,
|
||||
"name": "apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 19.2,
|
||||
"effectiveQuality": 23.2,
|
||||
"saturationRatio": 4.8
|
||||
},
|
||||
{
|
||||
"id": 906,
|
||||
"name": "mushroom_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Mushroom Stew",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 912,
|
||||
"name": "bread",
|
||||
"stackSize": 64,
|
||||
"displayName": "Bread",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 938,
|
||||
"name": "porkchop",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Porkchop",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 939,
|
||||
"name": "cooked_porkchop",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Porkchop",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 204.8,
|
||||
"effectiveQuality": 212.8,
|
||||
"saturationRatio": 25.6
|
||||
},
|
||||
{
|
||||
"id": 941,
|
||||
"name": "golden_apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Golden Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 80.8,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 942,
|
||||
"name": "enchanted_golden_apple",
|
||||
"stackSize": 64,
|
||||
"displayName": "Enchanted Golden Apple",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 80.8,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1012,
|
||||
"name": "cod",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Cod",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1013,
|
||||
"name": "salmon",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Salmon",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1014,
|
||||
"name": "tropical_fish",
|
||||
"stackSize": 64,
|
||||
"displayName": "Tropical Fish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1015,
|
||||
"name": "pufferfish",
|
||||
"stackSize": 64,
|
||||
"displayName": "Pufferfish",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 0.4,
|
||||
"effectiveQuality": 1.4,
|
||||
"saturationRatio": 0.4
|
||||
},
|
||||
{
|
||||
"id": 1016,
|
||||
"name": "cooked_cod",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Cod",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1017,
|
||||
"name": "cooked_salmon",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Salmon",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 115.200005,
|
||||
"effectiveQuality": 121.200005,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1057,
|
||||
"name": "cookie",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cookie",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1061,
|
||||
"name": "melon_slice",
|
||||
"stackSize": 64,
|
||||
"displayName": "Melon Slice",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1062,
|
||||
"name": "dried_kelp",
|
||||
"stackSize": 64,
|
||||
"displayName": "Dried Kelp",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 1.2,
|
||||
"effectiveQuality": 2.2,
|
||||
"saturationRatio": 1.2
|
||||
},
|
||||
{
|
||||
"id": 1065,
|
||||
"name": "beef",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Beef",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 1066,
|
||||
"name": "cooked_beef",
|
||||
"stackSize": 64,
|
||||
"displayName": "Steak",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 204.8,
|
||||
"effectiveQuality": 212.8,
|
||||
"saturationRatio": 25.6
|
||||
},
|
||||
{
|
||||
"id": 1067,
|
||||
"name": "chicken",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Chicken",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1068,
|
||||
"name": "cooked_chicken",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Chicken",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1069,
|
||||
"name": "rotten_flesh",
|
||||
"stackSize": 64,
|
||||
"displayName": "Rotten Flesh",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 6.4,
|
||||
"effectiveQuality": 10.4,
|
||||
"saturationRatio": 1.6
|
||||
},
|
||||
{
|
||||
"id": 1077,
|
||||
"name": "spider_eye",
|
||||
"stackSize": 64,
|
||||
"displayName": "Spider Eye",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 12.8,
|
||||
"effectiveQuality": 14.8,
|
||||
"saturationRatio": 6.4
|
||||
},
|
||||
{
|
||||
"id": 1177,
|
||||
"name": "carrot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Carrot",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 21.6,
|
||||
"effectiveQuality": 24.6,
|
||||
"saturationRatio": 7.2000003
|
||||
},
|
||||
{
|
||||
"id": 1178,
|
||||
"name": "potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Potato",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 1.2,
|
||||
"effectiveQuality": 2.2,
|
||||
"saturationRatio": 1.2
|
||||
},
|
||||
{
|
||||
"id": 1179,
|
||||
"name": "baked_potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Baked Potato",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1180,
|
||||
"name": "poisonous_potato",
|
||||
"stackSize": 64,
|
||||
"displayName": "Poisonous Potato",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1182,
|
||||
"name": "golden_carrot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Golden Carrot",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 172.8,
|
||||
"effectiveQuality": 178.8,
|
||||
"saturationRatio": 28.800001
|
||||
},
|
||||
{
|
||||
"id": 1191,
|
||||
"name": "pumpkin_pie",
|
||||
"stackSize": 64,
|
||||
"displayName": "Pumpkin Pie",
|
||||
"foodPoints": 8.0,
|
||||
"saturation": 76.8,
|
||||
"effectiveQuality": 84.8,
|
||||
"saturationRatio": 9.6
|
||||
},
|
||||
{
|
||||
"id": 1199,
|
||||
"name": "rabbit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Rabbit",
|
||||
"foodPoints": 3.0,
|
||||
"saturation": 10.8,
|
||||
"effectiveQuality": 13.8,
|
||||
"saturationRatio": 3.6000001
|
||||
},
|
||||
{
|
||||
"id": 1200,
|
||||
"name": "cooked_rabbit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Rabbit",
|
||||
"foodPoints": 5.0,
|
||||
"saturation": 60.0,
|
||||
"effectiveQuality": 65.0,
|
||||
"saturationRatio": 12.0
|
||||
},
|
||||
{
|
||||
"id": 1201,
|
||||
"name": "rabbit_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Rabbit Stew",
|
||||
"foodPoints": 10.0,
|
||||
"saturation": 240.0,
|
||||
"effectiveQuality": 250.0,
|
||||
"saturationRatio": 24.0
|
||||
},
|
||||
{
|
||||
"id": 1212,
|
||||
"name": "mutton",
|
||||
"stackSize": 64,
|
||||
"displayName": "Raw Mutton",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 4.8,
|
||||
"effectiveQuality": 6.8,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1213,
|
||||
"name": "cooked_mutton",
|
||||
"stackSize": 64,
|
||||
"displayName": "Cooked Mutton",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 115.200005,
|
||||
"effectiveQuality": 121.200005,
|
||||
"saturationRatio": 19.2
|
||||
},
|
||||
{
|
||||
"id": 1231,
|
||||
"name": "chorus_fruit",
|
||||
"stackSize": 64,
|
||||
"displayName": "Chorus Fruit",
|
||||
"foodPoints": 4.0,
|
||||
"saturation": 19.2,
|
||||
"effectiveQuality": 23.2,
|
||||
"saturationRatio": 4.8
|
||||
},
|
||||
{
|
||||
"id": 1235,
|
||||
"name": "beetroot",
|
||||
"stackSize": 64,
|
||||
"displayName": "Beetroot",
|
||||
"foodPoints": 1.0,
|
||||
"saturation": 2.4,
|
||||
"effectiveQuality": 3.4,
|
||||
"saturationRatio": 2.4
|
||||
},
|
||||
{
|
||||
"id": 1237,
|
||||
"name": "beetroot_soup",
|
||||
"stackSize": 1,
|
||||
"displayName": "Beetroot Soup",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1275,
|
||||
"name": "suspicious_stew",
|
||||
"stackSize": 1,
|
||||
"displayName": "Suspicious Stew",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 86.4,
|
||||
"effectiveQuality": 92.4,
|
||||
"saturationRatio": 14.400001
|
||||
},
|
||||
{
|
||||
"id": 1300,
|
||||
"name": "sweet_berries",
|
||||
"stackSize": 64,
|
||||
"displayName": "Sweet Berries",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1301,
|
||||
"name": "glow_berries",
|
||||
"stackSize": 64,
|
||||
"displayName": "Glow Berries",
|
||||
"foodPoints": 2.0,
|
||||
"saturation": 1.6,
|
||||
"effectiveQuality": 3.6,
|
||||
"saturationRatio": 0.8
|
||||
},
|
||||
{
|
||||
"id": 1308,
|
||||
"name": "honey_bottle",
|
||||
"stackSize": 16,
|
||||
"displayName": "Honey Bottle",
|
||||
"foodPoints": 6.0,
|
||||
"saturation": 14.400001,
|
||||
"effectiveQuality": 20.400002,
|
||||
"saturationRatio": 2.4
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "harp"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "basedrum"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "snare"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "hat"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "bass"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "flute"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "bell"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "guitar"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "chime"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "xylophone"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "iron_xylophone"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "cow_bell"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "didgeridoo"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "bit"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "banjo"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "pling"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "zombie"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "skeleton"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "creeper"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "dragon"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "wither_skeleton"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "piglin"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "custom_head"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"default": {},
|
||||
"leaves": {
|
||||
"1060": 1.0,
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5
|
||||
},
|
||||
"coweb": {
|
||||
"1060": 15.0,
|
||||
"875": 15.0,
|
||||
"880": 15.0,
|
||||
"885": 15.0,
|
||||
"890": 15.0,
|
||||
"895": 15.0,
|
||||
"900": 15.0
|
||||
},
|
||||
"plant": {
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5
|
||||
},
|
||||
"gourd": {
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5
|
||||
},
|
||||
"vine_or_glow_lichen": {
|
||||
"1060": 2.0
|
||||
},
|
||||
"wool": {
|
||||
"1060": 1.0
|
||||
},
|
||||
"sword_instantly_mines": {
|
||||
"875": 2.0,
|
||||
"880": 4.0,
|
||||
"885": 12.0,
|
||||
"890": 6.0,
|
||||
"895": 8.0,
|
||||
"900": 9.0
|
||||
},
|
||||
"sword_efficient": {
|
||||
"875": 2.0,
|
||||
"880": 4.0,
|
||||
"885": 12.0,
|
||||
"890": 6.0,
|
||||
"895": 8.0,
|
||||
"900": 9.0
|
||||
},
|
||||
"incorrect_for_wooden_tool": {
|
||||
"876": 2.0,
|
||||
"877": 2.0,
|
||||
"878": 2.0,
|
||||
"879": 2.0
|
||||
},
|
||||
"mineable/shovel": {
|
||||
"876": 2.0,
|
||||
"881": 4.0,
|
||||
"886": 12.0,
|
||||
"891": 6.0,
|
||||
"896": 8.0,
|
||||
"901": 9.0
|
||||
},
|
||||
"mineable/pickaxe": {
|
||||
"877": 2.0,
|
||||
"882": 4.0,
|
||||
"887": 12.0,
|
||||
"892": 6.0,
|
||||
"897": 8.0,
|
||||
"902": 9.0
|
||||
},
|
||||
"mineable/axe": {
|
||||
"878": 2.0,
|
||||
"883": 4.0,
|
||||
"888": 12.0,
|
||||
"893": 6.0,
|
||||
"898": 8.0,
|
||||
"903": 9.0
|
||||
},
|
||||
"mineable/hoe": {
|
||||
"879": 2.0,
|
||||
"884": 4.0,
|
||||
"889": 12.0,
|
||||
"894": 6.0,
|
||||
"899": 8.0,
|
||||
"904": 9.0
|
||||
},
|
||||
"incorrect_for_stone_tool": {
|
||||
"881": 4.0,
|
||||
"882": 4.0,
|
||||
"883": 4.0,
|
||||
"884": 4.0
|
||||
},
|
||||
"incorrect_for_gold_tool": {
|
||||
"886": 12.0,
|
||||
"887": 12.0,
|
||||
"888": 12.0,
|
||||
"889": 12.0
|
||||
},
|
||||
"incorrect_for_iron_tool": {
|
||||
"891": 6.0,
|
||||
"892": 6.0,
|
||||
"893": 6.0,
|
||||
"894": 6.0
|
||||
},
|
||||
"incorrect_for_diamond_tool": {
|
||||
"896": 8.0,
|
||||
"897": 8.0,
|
||||
"898": 8.0,
|
||||
"899": 8.0
|
||||
},
|
||||
"incorrect_for_netherite_tool": {
|
||||
"901": 9.0,
|
||||
"902": 9.0,
|
||||
"903": 9.0,
|
||||
"904": 9.0
|
||||
},
|
||||
"plant;mineable/axe": {
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5,
|
||||
"878": 2.0,
|
||||
"883": 4.0,
|
||||
"888": 12.0,
|
||||
"893": 6.0,
|
||||
"898": 8.0,
|
||||
"903": 9.0
|
||||
},
|
||||
"gourd;mineable/axe": {
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5,
|
||||
"878": 2.0,
|
||||
"883": 4.0,
|
||||
"888": 12.0,
|
||||
"893": 6.0,
|
||||
"898": 8.0,
|
||||
"903": 9.0
|
||||
},
|
||||
"leaves;mineable/hoe": {
|
||||
"1060": 1.0,
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5,
|
||||
"879": 2.0,
|
||||
"884": 4.0,
|
||||
"889": 12.0,
|
||||
"894": 6.0,
|
||||
"899": 8.0,
|
||||
"904": 9.0
|
||||
},
|
||||
"leaves;mineable/axe;mineable/hoe": {
|
||||
"1060": 1.0,
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5,
|
||||
"878": 2.0,
|
||||
"883": 4.0,
|
||||
"888": 12.0,
|
||||
"893": 6.0,
|
||||
"898": 8.0,
|
||||
"903": 9.0,
|
||||
"879": 2.0,
|
||||
"884": 4.0,
|
||||
"889": 12.0,
|
||||
"894": 6.0,
|
||||
"899": 8.0,
|
||||
"904": 9.0
|
||||
},
|
||||
"vine_or_glow_lichen;plant;mineable/axe": {
|
||||
"1060": 2.0,
|
||||
"875": 1.5,
|
||||
"880": 1.5,
|
||||
"885": 1.5,
|
||||
"890": 1.5,
|
||||
"895": 1.5,
|
||||
"900": 1.5,
|
||||
"878": 2.0,
|
||||
"883": 4.0,
|
||||
"888": 12.0,
|
||||
"893": 6.0,
|
||||
"898": 8.0,
|
||||
"903": 9.0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,458 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "angry_villager"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "block"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "block_marker"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "bubble"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "cloud"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "crit"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "damage_indicator"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "dragon_breath"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "dripping_lava"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "falling_lava"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "landing_lava"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "dripping_water"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "falling_water"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "dust"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "dust_color_transition"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "effect"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "elder_guardian"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "enchanted_hit"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "enchant"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "end_rod"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "entity_effect"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "explosion_emitter"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "explosion"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "gust"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "small_gust"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "gust_emitter_large"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "gust_emitter_small"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "sonic_boom"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "falling_dust"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "firework"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "fishing"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "flame"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "infested"
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "cherry_leaves"
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "pale_oak_leaves"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "tinted_leaves"
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "sculk_soul"
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "sculk_charge"
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "sculk_charge_pop"
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"name": "soul_fire_flame"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"name": "soul"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "flash"
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"name": "happy_villager"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"name": "composter"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"name": "heart"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"name": "instant_effect"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"name": "item"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"name": "vibration"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"name": "trail"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"name": "item_slime"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"name": "item_cobweb"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"name": "item_snowball"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"name": "large_smoke"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"name": "lava"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"name": "mycelium"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"name": "note"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"name": "poof"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"name": "portal"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "rain"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "smoke"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "white_smoke"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "sneeze"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "spit"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "squid_ink"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "sweep_attack"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"name": "totem_of_undying"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"name": "underwater"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"name": "splash"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"name": "witch"
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"name": "bubble_pop"
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"name": "current_down"
|
||||
},
|
||||
{
|
||||
"id": 71,
|
||||
"name": "bubble_column_up"
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"name": "nautilus"
|
||||
},
|
||||
{
|
||||
"id": 73,
|
||||
"name": "dolphin"
|
||||
},
|
||||
{
|
||||
"id": 74,
|
||||
"name": "campfire_cosy_smoke"
|
||||
},
|
||||
{
|
||||
"id": 75,
|
||||
"name": "campfire_signal_smoke"
|
||||
},
|
||||
{
|
||||
"id": 76,
|
||||
"name": "dripping_honey"
|
||||
},
|
||||
{
|
||||
"id": 77,
|
||||
"name": "falling_honey"
|
||||
},
|
||||
{
|
||||
"id": 78,
|
||||
"name": "landing_honey"
|
||||
},
|
||||
{
|
||||
"id": 79,
|
||||
"name": "falling_nectar"
|
||||
},
|
||||
{
|
||||
"id": 80,
|
||||
"name": "falling_spore_blossom"
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"name": "ash"
|
||||
},
|
||||
{
|
||||
"id": 82,
|
||||
"name": "crimson_spore"
|
||||
},
|
||||
{
|
||||
"id": 83,
|
||||
"name": "warped_spore"
|
||||
},
|
||||
{
|
||||
"id": 84,
|
||||
"name": "spore_blossom_air"
|
||||
},
|
||||
{
|
||||
"id": 85,
|
||||
"name": "dripping_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 86,
|
||||
"name": "falling_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 87,
|
||||
"name": "landing_obsidian_tear"
|
||||
},
|
||||
{
|
||||
"id": 88,
|
||||
"name": "reverse_portal"
|
||||
},
|
||||
{
|
||||
"id": 89,
|
||||
"name": "white_ash"
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"name": "small_flame"
|
||||
},
|
||||
{
|
||||
"id": 91,
|
||||
"name": "snowflake"
|
||||
},
|
||||
{
|
||||
"id": 92,
|
||||
"name": "dripping_dripstone_lava"
|
||||
},
|
||||
{
|
||||
"id": 93,
|
||||
"name": "falling_dripstone_lava"
|
||||
},
|
||||
{
|
||||
"id": 94,
|
||||
"name": "dripping_dripstone_water"
|
||||
},
|
||||
{
|
||||
"id": 95,
|
||||
"name": "falling_dripstone_water"
|
||||
},
|
||||
{
|
||||
"id": 96,
|
||||
"name": "glow_squid_ink"
|
||||
},
|
||||
{
|
||||
"id": 97,
|
||||
"name": "glow"
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"name": "wax_on"
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"name": "wax_off"
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"name": "electric_spark"
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"name": "scrape"
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "shriek"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "egg_crack"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"name": "dust_plume"
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"name": "trial_spawner_detection"
|
||||
},
|
||||
{
|
||||
"id": 106,
|
||||
"name": "trial_spawner_detection_ominous"
|
||||
},
|
||||
{
|
||||
"id": 107,
|
||||
"name": "vault_connection"
|
||||
},
|
||||
{
|
||||
"id": 108,
|
||||
"name": "dust_pillar"
|
||||
},
|
||||
{
|
||||
"id": 109,
|
||||
"name": "ominous_spawning"
|
||||
},
|
||||
{
|
||||
"id": 110,
|
||||
"name": "raid_omen"
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"name": "trial_omen"
|
||||
},
|
||||
{
|
||||
"id": 112,
|
||||
"name": "block_crumble"
|
||||
},
|
||||
{
|
||||
"id": 113,
|
||||
"name": "firefly"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user