Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb5fe8d90 | ||
|
|
d9bca7e517 | ||
|
|
b162d0d401 | ||
|
|
3273244c42 | ||
|
|
f579645b88 | ||
|
|
33754d7c71 | ||
|
|
8bd54a04f2 | ||
|
|
492dae8e83 | ||
|
|
d9543de951 | ||
|
|
f1f5374a5c | ||
|
|
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/
|
.devres/
|
||||||
.idea/
|
.idea/
|
||||||
.failed_packets/
|
.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
|
|
||||||
}
|
|
||||||
-172
@@ -1,172 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoginClient struct {
|
|
||||||
*proto.Conn
|
|
||||||
Account *api.Account
|
|
||||||
ServerHost string
|
|
||||||
ServerPort uint16
|
|
||||||
|
|
||||||
GivenAccount *v1_21_8.LoginToClientPacketSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
NoTokenErr = errors.New("the account has no token so we cant join online servers")
|
|
||||||
BadPublicKeyTypeErr = errors.New("the public key is not rsa")
|
|
||||||
FailedToLoginErr = errors.New("failed to login")
|
|
||||||
)
|
|
||||||
|
|
||||||
type KickedDuringLoginErr struct {
|
|
||||||
text.Component
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k KickedDuringLoginErr) Error() string {
|
|
||||||
return k.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) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnClose(_ event.Close) (err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnCompress(compress *v1_21_8.LoginToClientPacketCompress) (err error) {
|
|
||||||
s.SetCompressionThreshold(compress.Threshold)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnEncrypt(packet *v1_21_8.LoginToClientPacketEncryptionBegin) (err error) {
|
|
||||||
sharedSecret := make([]byte, 16)
|
|
||||||
_, _ = rand.Read(sharedSecret) //never fails
|
|
||||||
|
|
||||||
if packet.ShouldAuthenticate {
|
|
||||||
if s.Account.Ygg == "" {
|
|
||||||
err = NoTokenErr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serverId := crypto.AuthDigest([]byte(packet.ServerId), sharedSecret, packet.PublicKey)
|
|
||||||
err = s.Account.JoinServer(serverId)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pubAny, err := x509.ParsePKIXPublicKey(packet.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pub, ok := pubAny.(*rsa.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
err = BadPublicKeyTypeErr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
verifyTokenEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, packet.VerifyToken)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedSecretEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, sharedSecret)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.Send(&v1_21_8.LoginToServerPacketEncryptionBegin{SharedSecret: sharedSecretEnc, VerifyToken: verifyTokenEnc})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = s.SetSecret(sharedSecret)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect) (err error) {
|
|
||||||
var reason text.Component
|
|
||||||
err = json.Unmarshal([]byte(packet.Reason), &reason)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = KickedDuringLoginErr{reason}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) {
|
|
||||||
s.GivenAccount = success
|
|
||||||
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.SetState(proto_base.Configuration)
|
|
||||||
err = event.HandlerDoneErr{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginClient) OnStart() (err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func Login(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)
|
|
||||||
|
|
||||||
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = lc.Send(p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lc.SetState(proto_base.Login)
|
|
||||||
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
|
||||||
|
|
||||||
err = lc.Start()
|
|
||||||
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)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = Login(c, account)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Join(FailedToLoginErr, err)
|
|
||||||
}
|
|
||||||
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,105 @@
|
|||||||
|
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" // removed: use tcpflow instead for raw TCP capture
|
||||||
|
"github.com/admin-else/strom/cmd/strom/raw_to_tmcpr"
|
||||||
|
"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, // removed: use tcpflow instead for raw TCP capture, then raw-to-tmcpr
|
||||||
|
"raw-to-tmcpr": raw_to_tmcpr.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,164 @@
|
|||||||
|
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"
|
||||||
|
"github.com/admin-else/strom/mc/proto_generated/v1_8"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
versionFlag = cmd.String("version", "26.2", "minecraft version to use (e.g. 1.8, 1.21.11)")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Messenger struct {
|
||||||
|
*proto.Conn
|
||||||
|
chat *chat.Module
|
||||||
|
world *world.Module
|
||||||
|
legacy bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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) OnChatLegacy(e *v1_8.PlayToClientPacketChat) (err error) {
|
||||||
|
m.Log.Info("chat", "m", e.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
||||||
|
input := strings.TrimSpace(e.Val)
|
||||||
|
if strings.HasPrefix(input, ">say ") {
|
||||||
|
return m.sendChat(strings.TrimSpace(strings.TrimPrefix(input, ">say")))
|
||||||
|
}
|
||||||
|
if !m.legacy && strings.HasPrefix(input, ">getblock ") {
|
||||||
|
return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock")))
|
||||||
|
}
|
||||||
|
return m.sendChat(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Messenger) sendChat(message string) (err error) {
|
||||||
|
if m.legacy {
|
||||||
|
return m.Send(&v1_8.PlayToServerPacketChat{Message: message})
|
||||||
|
}
|
||||||
|
return m.chat.SendMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, client.WithVersion(*versionFlag))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
isLegacy := c.ProtocolVersion < 764
|
||||||
|
|
||||||
|
var chatMod *chat.Module
|
||||||
|
if !isLegacy {
|
||||||
|
chatMod, err = chat.Start(c, acc)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Messenger{
|
||||||
|
Conn: c,
|
||||||
|
chat: chatMod,
|
||||||
|
legacy: isLegacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLegacy {
|
||||||
|
w := world.NewWorld(c.Version, -64, 384)
|
||||||
|
m.world = world.Start(c, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
event.StartListingStdin(m.Loop)
|
||||||
|
m.Register(m.OnStdin)
|
||||||
|
|
||||||
|
if isLegacy {
|
||||||
|
m.Register(m.OnChatLegacy)
|
||||||
|
} else {
|
||||||
|
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLegacy {
|
||||||
|
keepalive.Start(m.Conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.StartConn()
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/admin-else/strom/crypto"
|
"github.com/admin-else/strom/mc/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmd = flag.NewFlagSet("offline-uuid", flag.ContinueOnError)
|
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,217 @@
|
|||||||
|
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: handshake, login, config, status, 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
trackState(packet, i)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackState(packet proto_base.EncodeDecodeAble, i proto_base.PacketInfo) {
|
||||||
|
switch i.Name {
|
||||||
|
case "set_protocol":
|
||||||
|
v := reflect.ValueOf(packet).Elem()
|
||||||
|
nsField := v.FieldByName("NextState")
|
||||||
|
if nsField.IsValid() && nsField.Kind() == reflect.Int32 {
|
||||||
|
switch nsField.Int() {
|
||||||
|
case 1:
|
||||||
|
state = proto_base.Status
|
||||||
|
case 2:
|
||||||
|
state = proto_base.Login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "login_start":
|
||||||
|
if int32(*ProtocolVersion) <= 47 {
|
||||||
|
state = proto_base.Play
|
||||||
|
}
|
||||||
|
case "success":
|
||||||
|
if int32(*ProtocolVersion) >= 764 {
|
||||||
|
state = proto_base.Configuration
|
||||||
|
} else {
|
||||||
|
state = proto_base.Play
|
||||||
|
}
|
||||||
|
case "finish_configuration":
|
||||||
|
state = proto_base.Play
|
||||||
|
case "login_acknowledged":
|
||||||
|
state = proto_base.Configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "handshake":
|
||||||
|
state = proto_base.Handshaking
|
||||||
|
case "login":
|
||||||
|
state = proto_base.Login
|
||||||
|
case "config":
|
||||||
|
state = proto_base.Configuration
|
||||||
|
case "play":
|
||||||
|
state = proto_base.Play
|
||||||
|
case "status":
|
||||||
|
state = proto_base.Status
|
||||||
|
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,243 @@
|
|||||||
|
package raw_to_tmcpr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/admin-else/strom/mc/proto/replay"
|
||||||
|
"github.com/admin-else/strom/mc/proto_base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cmd = flag.NewFlagSet("raw-to-tmcpr", flag.ContinueOnError)
|
||||||
|
|
||||||
|
var ClientboundIn = Cmd.String("clientbound", "", "raw tcpflow file for clientbound (server->client) direction")
|
||||||
|
var ServerboundIn = Cmd.String("serverbound", "", "raw tcpflow file for serverbound (client->server) direction")
|
||||||
|
var ClientboundOut = Cmd.String("clientbound-out", "clientbound.tmcpr", "output TMCPR file for clientbound")
|
||||||
|
var ServerboundOut = Cmd.String("serverbound-out", "serverbound.tmcpr", "output TMCPR file for serverbound")
|
||||||
|
var ClientboundState = Cmd.String("clientbound-state", "login", "initial state for clientbound: handshake, login, status, play")
|
||||||
|
var ServerboundState = Cmd.String("serverbound-state", "handshake", "initial state for serverbound: handshake, login, status, play")
|
||||||
|
|
||||||
|
type captureReader struct {
|
||||||
|
r io.Reader
|
||||||
|
out *replay.Writer
|
||||||
|
state proto_base.State
|
||||||
|
threshold int32
|
||||||
|
startTime time.Time
|
||||||
|
haveStartTime bool
|
||||||
|
direction proto_base.Direction
|
||||||
|
pendingThreshold int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *captureReader) setStartTime() {
|
||||||
|
if !cr.haveStartTime {
|
||||||
|
cr.startTime = time.Now()
|
||||||
|
cr.haveStartTime = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *captureReader) timestamp() uint32 {
|
||||||
|
return uint32(time.Since(cr.startTime).Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPacketBytes(r io.Reader, threshold int32) ([]byte, error) {
|
||||||
|
packetLen, err := proto_base.DecodeVarInt(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read packet length: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBytes := make([]byte, packetLen)
|
||||||
|
_, err = io.ReadFull(r, rawBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read packet data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if threshold >= 0 {
|
||||||
|
buf := bytes.NewReader(rawBytes)
|
||||||
|
uncompressedLen, err := proto_base.DecodeVarInt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read uncompressed length: %w", err)
|
||||||
|
}
|
||||||
|
if uncompressedLen == 0 {
|
||||||
|
return io.ReadAll(buf)
|
||||||
|
} else {
|
||||||
|
zReader, err := zlib.NewReader(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("zlib reader: %w", err)
|
||||||
|
}
|
||||||
|
defer zReader.Close()
|
||||||
|
decompressed, err := io.ReadAll(zReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decompress: %w", err)
|
||||||
|
}
|
||||||
|
return decompressed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *captureReader) trackState(packetBytes []byte) {
|
||||||
|
buf := bytes.NewReader(packetBytes)
|
||||||
|
packetId, err := proto_base.DecodeVarInt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cr.state {
|
||||||
|
case proto_base.Handshaking:
|
||||||
|
if cr.direction == proto_base.ToServer && packetId == 0 {
|
||||||
|
_, err = proto_base.DecodeVarInt(buf) // protocol version
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = proto_base.DecodeString(buf) // server address
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
portBytes := make([]byte, 2)
|
||||||
|
_, err = io.ReadFull(buf, portBytes)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextState, err := proto_base.DecodeVarInt(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if nextState == 2 {
|
||||||
|
cr.state = proto_base.Login
|
||||||
|
} else if nextState == 1 {
|
||||||
|
cr.state = proto_base.Status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case proto_base.Login:
|
||||||
|
if cr.direction == proto_base.ToClient {
|
||||||
|
if packetId == 0x03 {
|
||||||
|
threshold, err := proto_base.DecodeVarInt(buf)
|
||||||
|
if err == nil {
|
||||||
|
cr.threshold = threshold
|
||||||
|
}
|
||||||
|
} else if packetId == 0x02 {
|
||||||
|
cr.state = proto_base.Play
|
||||||
|
}
|
||||||
|
} else if cr.direction == proto_base.ToServer && packetId == 0 {
|
||||||
|
if cr.pendingThreshold >= 0 {
|
||||||
|
cr.threshold = cr.pendingThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case proto_base.Status:
|
||||||
|
case proto_base.Play:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *captureReader) process() (count int, err error) {
|
||||||
|
for {
|
||||||
|
packetBytes, err := readPacketBytes(cr.r, cr.threshold)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.setStartTime()
|
||||||
|
cr.trackState(packetBytes)
|
||||||
|
|
||||||
|
err = cr.out.WriteRaw(packetBytes, cr.timestamp())
|
||||||
|
if err != nil {
|
||||||
|
return count, fmt.Errorf("write TMCPR: %w", err)
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(args []string) (err error) {
|
||||||
|
err = Cmd.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ClientboundIn == "" && *ServerboundIn == "" {
|
||||||
|
return fmt.Errorf("at least one of -clientbound or -serverbound must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
parseState := func(name string) (proto_base.State, error) {
|
||||||
|
switch name {
|
||||||
|
case "handshake":
|
||||||
|
return proto_base.Handshaking, nil
|
||||||
|
case "login":
|
||||||
|
return proto_base.Login, nil
|
||||||
|
case "play":
|
||||||
|
return proto_base.Play, nil
|
||||||
|
case "status":
|
||||||
|
return proto_base.Status, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown state: %s (valid: handshake, login, status, play)", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var compressionThreshold int32 = -1
|
||||||
|
|
||||||
|
if *ClientboundIn != "" {
|
||||||
|
state, err := parseState(*ClientboundState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
compressionThreshold, err = processFile(*ClientboundIn, *ClientboundOut, state, proto_base.ToClient, -1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("clientbound: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ServerboundIn != "" {
|
||||||
|
state, err := parseState(*ServerboundState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = processFile(*ServerboundIn, *ServerboundOut, state, proto_base.ToServer, compressionThreshold)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("serverbound: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func processFile(inPath, outPath string, initial proto_base.State, dir proto_base.Direction, initialThreshold int32) (finalThreshold int32, err error) {
|
||||||
|
in, err := os.Open(inPath)
|
||||||
|
if err != nil {
|
||||||
|
return initialThreshold, fmt.Errorf("open input: %w", err)
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(outPath)
|
||||||
|
if err != nil {
|
||||||
|
return initialThreshold, fmt.Errorf("create output: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
cr := &captureReader{
|
||||||
|
r: in,
|
||||||
|
out: replay.NewWriter(out),
|
||||||
|
state: initial,
|
||||||
|
threshold: -1,
|
||||||
|
direction: dir,
|
||||||
|
pendingThreshold: initialThreshold,
|
||||||
|
}
|
||||||
|
if dir == proto_base.ToClient {
|
||||||
|
cr.threshold = initialThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := cr.process()
|
||||||
|
if err != nil {
|
||||||
|
return cr.threshold, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s: %d packets written to %s (threshold=%d)\n", inPath, count, outPath, cr.threshold)
|
||||||
|
return cr.threshold, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package registry_data_hunter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -6,16 +6,16 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/admin-else/strom/api"
|
"github.com/admin-else/strom/mc/api"
|
||||||
"github.com/admin-else/strom/client"
|
"github.com/admin-else/strom/mc/client"
|
||||||
"github.com/admin-else/strom/data"
|
"github.com/admin-else/strom/mc/data"
|
||||||
"github.com/admin-else/strom/event"
|
"github.com/admin-else/strom/mc/event"
|
||||||
"github.com/admin-else/strom/nbt"
|
"github.com/admin-else/strom/mc/nbt"
|
||||||
"github.com/admin-else/strom/proto"
|
"github.com/admin-else/strom/mc/proto"
|
||||||
"github.com/admin-else/strom/proto_base"
|
"github.com/admin-else/strom/mc/proto_base"
|
||||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||||
"github.com/admin-else/strom/server"
|
"github.com/admin-else/strom/mc/server"
|
||||||
"github.com/admin-else/strom/text"
|
"github.com/admin-else/strom/mc/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
func wontFail[T any](v T, err error) T {
|
func wontFail[T any](v T, err error) T {
|
||||||
@@ -26,10 +26,8 @@ func wontFail[T any](v T, err error) T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Edit these two
|
Version = "1.21.11"
|
||||||
|
SaveAs = "data/registry/" + Version + ".nbt"
|
||||||
SaveAs = "1.21.8"
|
|
||||||
Version = "1.21.8"
|
|
||||||
|
|
||||||
CompatibleProtocolVersion = wontFail(data.LookUpVersionByName(Version)).Version
|
CompatibleProtocolVersion = wontFail(data.LookUpVersionByName(Version)).Version
|
||||||
FinishedConfigErr = errors.New("finished configuration")
|
FinishedConfigErr = errors.New("finished configuration")
|
||||||
@@ -45,15 +43,7 @@ var Status = wontFail(json.Marshal(server.StatusResponse{
|
|||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
Client, Servee *proto.Conn
|
Client, Servee *proto.Conn
|
||||||
Data nbt.Tag
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||||
@@ -79,11 +69,25 @@ func (p *Proxy) OnFinishConfiguration(_ *v1_21_8.ConfigurationToClientPacketFini
|
|||||||
return
|
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) {
|
func (p *Proxy) OnData(data *v1_21_8.ConfigurationToClientPacketRegistryData) (err error) {
|
||||||
d := p.Data.Value.(map[string]any)["Registries"].([]any)
|
d := p.Data.Value.(map[string]any)["Registries"].([]any)
|
||||||
var entries []any
|
var entries []any
|
||||||
for _, entry := range data.Entries {
|
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})
|
d = append(d, map[string]any{"Id": data.Id, "Entries": entries})
|
||||||
p.Data.Value.(map[string]any)["Registries"] = d
|
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) {
|
func handleClient(c *proto.Conn) (err error) {
|
||||||
acc := api.NewOfflineAccount("RegistryHunter")
|
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))
|
_, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sockPuppet, err := client.Connect(":25566")
|
sockPuppet, err := client.Login(":25565", acc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer sockPuppet.Close()
|
defer sockPuppet.Close()
|
||||||
err = sockPuppet.Start(&client.LoginClient{
|
|
||||||
Conn: sockPuppet,
|
|
||||||
Account: acc,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
p := &Proxy{Client: sockPuppet, Servee: c}
|
p := &Proxy{Client: sockPuppet, Servee: c}
|
||||||
p.Data.Value = map[string]any{"Registries": []any{}}
|
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() {
|
go func() {
|
||||||
errChan <- p.Client.Start(p)
|
errChan <- p.Client.StartConn()
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- p.Servee.Start(p)
|
errChan <- p.Servee.StartConn()
|
||||||
}()
|
}()
|
||||||
err = <-errChan
|
err = <-errChan
|
||||||
f, err := os.Create("data/registry/" + SaveAs + ".nbt")
|
f, err := os.Create(SaveAs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
err = nbt.WriteFile(f, p.Data)
|
err = nbt.WriteUnstructuredFile(f, p.Data)
|
||||||
|
slog.Info("saved", "file", SaveAs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func Run(args []string) error {
|
||||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||||
err := server.StartServerWithOnConn(":25565", handleClient)
|
return server.StartServerWithOnConn(":25566", handleClient)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
package client
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/admin-else/strom/event"
|
"github.com/admin-else/strom/mc/client"
|
||||||
"github.com/admin-else/strom/proto"
|
"github.com/admin-else/strom/mc/event"
|
||||||
"github.com/admin-else/strom/proto_base"
|
"github.com/admin-else/strom/mc/proto"
|
||||||
"github.com/admin-else/strom/server"
|
"github.com/admin-else/strom/mc/proto_base"
|
||||||
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||||
"github.com/admin-else/strom/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 {
|
type StatusClient struct {
|
||||||
*proto.Conn
|
*proto.Conn
|
||||||
Status string
|
Status string
|
||||||
@@ -19,10 +25,6 @@ type StatusClient struct {
|
|||||||
DoPingRoundTripTime bool
|
DoPingRoundTripTime bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatusClient) OnStart() (err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
||||||
s.Status = p.Response
|
s.Status = p.Response
|
||||||
s.PingSendTime = time.Now()
|
s.PingSendTime = time.Now()
|
||||||
@@ -41,25 +43,25 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StatusRaw returns a StatusClient that is not connected to a server.
|
// StatusRaw returns a StatusClient that is not connected to a server.
|
||||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment
|
// 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) {
|
func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||||
c, err := ConnectVersionLess(addr)
|
c, err := client.ConnectVersionLess(nil, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
err = c.SetVersion("1.21.8")
|
err = c.SetVersion(*versionFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s = &StatusClient{
|
s = &StatusClient{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
}
|
}
|
||||||
s.Loop = event.NewLoop()
|
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||||
s.RegisterUntilLatest(s.OnStatus)
|
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||||
s.RegisterUntilLatest(s.OnPong)
|
|
||||||
|
|
||||||
p, err := MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
p, err := client.MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,11 +76,23 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func Status(addr string) (status server.StatusResponse, err error) {
|
func Run(args []string) (err error) {
|
||||||
s, err := StatusRaw(addr)
|
err = cmd.Parse(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(s.Status), &status)
|
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
|
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
|
module github.com/admin-else/strom
|
||||||
|
|
||||||
go 1.25
|
go 1.26.2
|
||||||
|
|
||||||
require (
|
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/go-cmp v0.7.0
|
||||||
github.com/google/uuid v1.6.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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/admin-else/strom/crypto"
|
|
||||||
"github.com/google/uuid"
|
"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 {
|
type Account struct {
|
||||||
|
*Servers
|
||||||
Name string
|
Name string
|
||||||
Uuid uuid.UUID
|
Uuid uuid.UUID
|
||||||
Ygg string
|
Ygg string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
BaseUrlApi = "https://api.minecraftservices.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) {
|
func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) {
|
||||||
var b []byte
|
var b []byte
|
||||||
if method != "GET" {
|
if method != "GET" {
|
||||||
b, err = json.Marshal(from)
|
b, err = json.Marshal(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := http.NewRequest(method, url, bytes.NewReader(b))
|
r, err := http.NewRequest(method, url, bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Header.Set("Content-Type", "application/json")
|
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)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
|
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
|
||||||
return fmt.Errorf("bad status code %v", resp.StatusCode)
|
return fmt.Errorf("bad status code %v", resp.StatusCode)
|
||||||
}
|
}
|
||||||
b, err = io.ReadAll(resp.Body)
|
b, err = io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(b, to)
|
err = json.Unmarshal(b, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerKeys struct {
|
type PlayerKeys struct {
|
||||||
@@ -73,7 +82,7 @@ type PlayerKeys struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) FetchKeys() (keys PlayerKeys, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +99,12 @@ func (a *Account) JoinServer(serverId string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != 204 {
|
if resp.StatusCode != 204 {
|
||||||
body, err = io.ReadAll(resp.Body)
|
body, err = io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -112,17 +123,10 @@ type NameChangResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) NameChangeInfo() (r NameChangResp, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOfflineAccount(name string) *Account {
|
|
||||||
return &Account{
|
|
||||||
Name: name,
|
|
||||||
Uuid: crypto.FromOfflinePlayer(name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayerProfile represents the player's profile information
|
// PlayerProfile represents the player's profile information
|
||||||
type PlayerProfile struct {
|
type PlayerProfile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -143,7 +147,7 @@ type PlayerProfile struct {
|
|||||||
|
|
||||||
// QueryProfile queries the player's profile
|
// QueryProfile queries the player's profile
|
||||||
func (a *Account) QueryProfile() (profile PlayerProfile, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +185,7 @@ type PlayerAttributes struct {
|
|||||||
|
|
||||||
// QueryAttributes queries the player's attributes
|
// QueryAttributes queries the player's attributes
|
||||||
func (a *Account) QueryAttributes() (attrs PlayerAttributes, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +198,7 @@ type ModifyAttributesRequest struct {
|
|||||||
|
|
||||||
// ModifyAttributes modifies the player's attributes
|
// ModifyAttributes modifies the player's attributes
|
||||||
func (a *Account) ModifyAttributes(req ModifyAttributesRequest) (attrs PlayerAttributes, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,24 +209,23 @@ type BlockedUsers struct {
|
|||||||
|
|
||||||
// GetBlocklist gets the list of blocked users
|
// GetBlocklist gets the list of blocked users
|
||||||
func (a *Account) GetBlocklist() (blocklist BlockedUsers, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckNameAvailability checks if a name is available
|
|
||||||
type NameAvailability struct {
|
type NameAvailability struct {
|
||||||
Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED
|
Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckNameAvailability checks if a name is available
|
// CheckNameAvailability checks if a name is available
|
||||||
func (a *Account) CheckNameAvailability(name string) (avail NameAvailability, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeName changes the player's name
|
// ChangeName changes the player's name
|
||||||
func (a *Account) ChangeName(newName string) (profile PlayerProfile, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,19 +237,19 @@ type ChangeSkinRequest struct {
|
|||||||
|
|
||||||
// ChangeSkin changes the player's skin
|
// ChangeSkin changes the player's skin
|
||||||
func (a *Account) ChangeSkin(req ChangeSkinRequest) (profile PlayerProfile, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetSkin resets the player's skin to default
|
// ResetSkin resets the player's skin to default
|
||||||
func (a *Account) ResetSkin() (profile PlayerProfile, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// HideCape hides the player's active cape
|
// HideCape hides the player's active cape
|
||||||
func (a *Account) HideCape() (profile PlayerProfile, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,29 +261,58 @@ type ShowCapeRequest struct {
|
|||||||
// ShowCape shows a specific cape
|
// ShowCape shows a specific cape
|
||||||
func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) {
|
func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) {
|
||||||
req := ShowCapeRequest{CapeID: capeID}
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckGiftCodeValidity checks if a gift code is valid
|
// CheckGiftCodeValidity checks if a gift code is valid
|
||||||
func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) {
|
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)
|
r, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Header.Set("Authorization", "Bearer "+a.Ygg)
|
r.Header.Set("Authorization", "Bearer "+a.Ygg)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Returns HTTP 200 or 204 if valid, 404 if invalid
|
// Returns HTTP 200 or 204 if valid, 404 if invalid
|
||||||
if resp.StatusCode == 200 || resp.StatusCode == 204 {
|
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,82 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
if s.Version.Protocol == 0 {
|
||||||
|
err = errors.New("server didnt return protocol version")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectWithConn wraps an existing net.Conn into a proto.Conn with the given
|
||||||
|
// Minecraft version. The version must be non-empty.
|
||||||
|
func ConnectWithConn(ctx context.Context, conn net.Conn, version string) (ret *proto.Conn, err error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
ret = proto.NewConnCtx(ctx)
|
||||||
|
ret.Actor = proto_base.Client
|
||||||
|
ret.Conn = conn
|
||||||
|
ret.R = conn
|
||||||
|
ret.W = conn
|
||||||
|
v, err := data.LookUpVersionByName(version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret.ProtocolVersion = v.Version
|
||||||
|
ret.Version = version
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -3,12 +3,11 @@ package client
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/admin-else/strom/event"
|
"github.com/admin-else/strom/mc/event"
|
||||||
"github.com/admin-else/strom/proto"
|
"github.com/admin-else/strom/mc/proto"
|
||||||
"github.com/admin-else/strom/proto_base"
|
"github.com/admin-else/strom/mc/proto_base"
|
||||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigIgnorer struct {
|
type ConfigIgnorer struct {
|
||||||
@@ -19,8 +18,8 @@ func (c *ConfigIgnorer) OnStart() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigIgnorer) Default(event event.Anything) (err error) {
|
func (c *ConfigIgnorer) Default(e event.Anything) (err error) {
|
||||||
slog.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", event))
|
c.Log.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", e))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +47,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet *v1_21_8.ConfigurationToClientPacketK
|
|||||||
return
|
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) {
|
func IgnoreConfig(c *proto.Conn) (err error) {
|
||||||
ci := &ConfigIgnorer{c}
|
ci := &ConfigIgnorer{c}
|
||||||
ci.RegisterCritical(ci.Default)
|
ci.RegisterCritical(ci.Default)
|
||||||
@@ -56,7 +56,7 @@ func IgnoreConfig(c *proto.Conn) (err error) {
|
|||||||
ci.RegisterUntilLatest(ci.OnPing)
|
ci.RegisterUntilLatest(ci.OnPing)
|
||||||
ci.RegisterUntilLatest(ci.OnKeepAlive)
|
ci.RegisterUntilLatest(ci.OnKeepAlive)
|
||||||
|
|
||||||
err = ci.Start()
|
err = ci.StartConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Join(err, errors.New("failed to ignore config"))
|
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 {
|
for _, c := range cases {
|
||||||
t.Logf("Testing %s", c.Input)
|
t.Logf("Testing %s", c.Input)
|
||||||
_, host, portN, err := DoDns(c.Input)
|
_, host, portN, err := DoDNSChecked(nil, c.Input, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
continue
|
continue
|
||||||
@@ -4,15 +4,17 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/admin-else/strom/proto"
|
"github.com/admin-else/strom/mc/proto"
|
||||||
"github.com/admin-else/strom/proto_base"
|
"github.com/admin-else/strom/mc/proto_base"
|
||||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
"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) {
|
func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
|
||||||
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
|
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) {
|
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}
|
p = &v1_21_8.HandshakingToServerPacketSetProtocol{NextState: int32(nextState), ProtocolVersion: s.ProtocolVersion}
|
||||||
var portStr string
|
var portStr string
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"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/v1_8"
|
||||||
|
"github.com/admin-else/strom/mc/proto_generated/v26_2"
|
||||||
|
"github.com/admin-else/strom/mc/text"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginClient struct {
|
||||||
|
*proto.Conn
|
||||||
|
Account *api.Account
|
||||||
|
ServerHost string
|
||||||
|
ServerPort uint16
|
||||||
|
|
||||||
|
GivenAccount *v1_21_8.LoginToClientPacketSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
NoTokenErr = errors.New("the account has no token so we cant join online servers")
|
||||||
|
BadPublicKeyTypeErr = errors.New("the public key is not rsa")
|
||||||
|
FailedToLoginErr = errors.New("failed to login")
|
||||||
|
)
|
||||||
|
|
||||||
|
type KickedDuringLoginErr struct {
|
||||||
|
text.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KickedDuringLoginErr) Error() string {
|
||||||
|
return k.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
|
||||||
|
if _, ok := event.Val.(proto_base.EncodeDecodeAble); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Log.Warn("unexpected packet during login client", "packet", event.Val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnClose(_ event.Close) (err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnCompress(compress *v1_21_8.LoginToClientPacketCompress) (err error) {
|
||||||
|
s.SetCompressionThreshold(compress.Threshold)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnEncrypt(packet *v1_21_8.LoginToClientPacketEncryptionBegin) (err error) {
|
||||||
|
sharedSecret := make([]byte, 16)
|
||||||
|
_, _ = rand.Read(sharedSecret) //never fails
|
||||||
|
|
||||||
|
if packet.ShouldAuthenticate {
|
||||||
|
if s.Account.Ygg == "" {
|
||||||
|
err = NoTokenErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverId := crypto.AuthDigest([]byte(packet.ServerId), sharedSecret, packet.PublicKey)
|
||||||
|
err = s.Account.JoinServer(serverId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pubAny, err := x509.ParsePKIXPublicKey(packet.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub, ok := pubAny.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
err = BadPublicKeyTypeErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
verifyTokenEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, packet.VerifyToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedSecretEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, sharedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.Send(&v1_21_8.LoginToServerPacketEncryptionBegin{SharedSecret: sharedSecretEnc, VerifyToken: verifyTokenEnc})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = s.SetSecret(sharedSecret)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect) (err error) {
|
||||||
|
var reason text.Component
|
||||||
|
err = json.Unmarshal([]byte(packet.Reason), &reason)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = KickedDuringLoginErr{reason}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnDisconnectV1_8(packet *v1_8.LoginToClientPacketDisconnect) (err error) {
|
||||||
|
var reason text.Component
|
||||||
|
err = json.Unmarshal([]byte(packet.Reason), &reason)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = KickedDuringLoginErr{reason}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnCompressV1_8(packet *v1_8.LoginToClientPacketCompress) (err error) {
|
||||||
|
s.SetCompressionThreshold(packet.Threshold)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnSuccessV1_8(success *v1_8.LoginToClientPacketSuccess) (err error) {
|
||||||
|
s.GivenAccount = &v1_21_8.LoginToClientPacketSuccess{
|
||||||
|
Uuid: uuid.MustParse(success.Uuid),
|
||||||
|
Username: success.Username,
|
||||||
|
}
|
||||||
|
s.SetState(proto_base.Play)
|
||||||
|
err = event.HandlerDoneErr{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) {
|
||||||
|
s.GivenAccount = success
|
||||||
|
if s.ProtocolVersion >= 764 {
|
||||||
|
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.SetState(proto_base.Configuration)
|
||||||
|
} else {
|
||||||
|
s.SetState(proto_base.Play)
|
||||||
|
}
|
||||||
|
err = event.HandlerDoneErr{}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling.
|
||||||
|
// The handshake address is derived from the connection's RemoteAddr.
|
||||||
|
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
|
||||||
|
return LoginRawAddr(c, account, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRawAddr performs the login handshake on an already-established connection using the given address for the handshake packet.
|
||||||
|
// If hostAddr is empty, the handshake address is derived from the connection's RemoteAddr.
|
||||||
|
func LoginRawAddr(c *proto.Conn, account *api.Account, hostAddr string) (err error) {
|
||||||
|
lc := &LoginClient{
|
||||||
|
Conn: c,
|
||||||
|
Account: account,
|
||||||
|
}
|
||||||
|
lc.RegisterCritical(lc.OnDefault)
|
||||||
|
lc.RegisterCritical(lc.OnClose)
|
||||||
|
lc.RegisterCriticalUntilLatest(lc.OnCompress)
|
||||||
|
lc.RegisterCriticalUntilLatest(lc.OnEncrypt)
|
||||||
|
lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
|
||||||
|
lc.RegisterCriticalUntil("26.1", lc.OnSuccess)
|
||||||
|
lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2)
|
||||||
|
lc.RegisterCritical(lc.OnCompressV1_8)
|
||||||
|
lc.RegisterCritical(lc.OnDisconnectV1_8)
|
||||||
|
lc.RegisterCritical(lc.OnSuccessV1_8)
|
||||||
|
|
||||||
|
var p proto_base.EncodeDecodeAble
|
||||||
|
if hostAddr != "" {
|
||||||
|
p, err = MakeHandshakePacketAddr(lc.Conn, proto_base.Login, hostAddr)
|
||||||
|
} else {
|
||||||
|
p, err = MakeHandshakePacket(lc.Conn, proto_base.Login)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = lc.Send(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lc.SetState(proto_base.Login)
|
||||||
|
if lc.Conn.ProtocolVersion < 759 {
|
||||||
|
err = lc.Send(&v1_8.LoginToServerPacketLoginStart{Username: lc.Account.Name})
|
||||||
|
} else {
|
||||||
|
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = lc.StartConn()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(FailedToLoginErr, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIgnoreConfig() func(ls *loginSettings) {
|
||||||
|
return func(ls *loginSettings) {
|
||||||
|
ls.IgnoreConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialFunc is a context-aware dial function used by Login to establish a TCP connection.
|
||||||
|
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
|
// WithConn returns a login setting that uses an already-established net.Conn instead of dialling.
|
||||||
|
// WithVersion must also be set; auto-detection is not possible with a pre-established connection.
|
||||||
|
func WithConn(conn net.Conn) func(ls *loginSettings) {
|
||||||
|
return func(ls *loginSettings) {
|
||||||
|
ls.Conn = conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDialer returns a login setting that uses the given dial function instead of net.Dialer.
|
||||||
|
// Auto version detection via status ping is still supported when no explicit version is set.
|
||||||
|
func WithDialer(d DialFunc) func(ls *loginSettings) {
|
||||||
|
return func(ls *loginSettings) {
|
||||||
|
ls.Dialer = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrWithConnRequiresVersion = errors.New("WithVersion is required when using WithConn")
|
||||||
|
ErrWithConnNoStatus = errors.New("auto version detection is not supported with WithConn")
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginSettings struct {
|
||||||
|
NoDns bool
|
||||||
|
Ctx context.Context
|
||||||
|
Version string
|
||||||
|
IgnoreConfig bool
|
||||||
|
Conn net.Conn
|
||||||
|
Dialer DialFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Conn != nil {
|
||||||
|
if ls.Version == "" {
|
||||||
|
return nil, ErrWithConnRequiresVersion
|
||||||
|
}
|
||||||
|
c, err = ConnectWithConn(ls.Ctx, ls.Conn, ls.Version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if ls.Dialer != nil {
|
||||||
|
if ls.Version == "" {
|
||||||
|
return nil, ErrWithConnRequiresVersion
|
||||||
|
}
|
||||||
|
if !ls.NoDns {
|
||||||
|
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var conn net.Conn
|
||||||
|
conn, err = ls.Dialer(ls.Ctx, "tcp", connectTo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err = ConnectWithConn(ls.Ctx, conn, ls.Version)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 = LoginRawAddr(c, account, connectTo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ls.IgnoreConfig && c.ProtocolVersion >= 764 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/v1_8"
|
||||||
|
"github.com/admin-else/strom/mc/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatusClient) OnStatusV1_8(p *v1_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) OnPongV1_8(p *v1_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(ctx context.Context, addr string) (s *StatusClient, err error) {
|
||||||
|
return StatusRawWithVersion(ctx, addr, "1.21.8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusRawWithVersion is like StatusRaw but uses the given Minecraft version for the handshake.
|
||||||
|
func StatusRawWithVersion(ctx context.Context, addr, version string) (s *StatusClient, err error) {
|
||||||
|
c, err := ConnectVersionLess(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
err = c.SetVersion(version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = &StatusClient{
|
||||||
|
Conn: c,
|
||||||
|
}
|
||||||
|
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||||
|
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||||
|
s.RegisterCritical(s.OnStatusV1_8)
|
||||||
|
s.RegisterCritical(s.OnPongV1_8)
|
||||||
|
|
||||||
|
p, err := 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusNoDns is like Status but does not resolve SRV records.
|
||||||
|
func StatusNoDns(ctx context.Context, addr string) (status server.StatusResponse, err error) {
|
||||||
|
s, err := StatusRaw(ctx, addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
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(ctx, addr)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ package crypto
|
|||||||
import (
|
import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,12 +38,15 @@ type CFB8 struct {
|
|||||||
ivPos int
|
ivPos int
|
||||||
iv []byte
|
iv []byte
|
||||||
de bool
|
de bool
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCFB8Decrypt creates a new CFB8 stream cipher in decrypt mode.
|
||||||
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
|
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
|
||||||
return newCFB8(c, iv, true)
|
return newCFB8(c, iv, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCFB8Encrypt creates a new CFB8 stream cipher in encrypt mode.
|
||||||
func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 {
|
func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 {
|
||||||
return newCFB8(c, iv, false)
|
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) {
|
func (cf *CFB8) XORKeyStream(dst, src []byte) {
|
||||||
|
cf.mutex.Lock()
|
||||||
|
defer cf.mutex.Unlock()
|
||||||
if len(src) == 0 {
|
if len(src) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"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
|
// 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 := md5.New()
|
||||||
h.Reset()
|
h.Reset()
|
||||||
h.Write(space) //nolint:errcheck
|
h.Write(space) //nolint:errcheck
|
||||||
@@ -21,6 +21,7 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromOfflinePlayer generates a UUID for an offline-mode (cracked) player from their display name.
|
||||||
func FromOfflinePlayer(displayName string) uuid.UUID {
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/admin-else/strom/data"
|
"github.com/admin-else/strom/mc/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLookupBiomeById(t *testing.T) {
|
func TestLookupBiomeById(t *testing.T) {
|
||||||
@@ -2,6 +2,10 @@ package data
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/admin-else/strom/mc/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -47,12 +51,15 @@ type Block struct {
|
|||||||
type BlockState struct {
|
type BlockState struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
|
MinValue int32 // this is generated on load and used for state value calculations
|
||||||
NumValues int32 `json:"num_values"`
|
NumValues int32 `json:"num_values"`
|
||||||
Values []string
|
Values []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var BlocksCache = make(map[string][]*Block)
|
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) {
|
func BlocksForVersion(v string) (ret []*Block) {
|
||||||
var ok bool
|
var ok bool
|
||||||
if ret, ok = BlocksCache[v]; ok {
|
if ret, ok = BlocksCache[v]; ok {
|
||||||
@@ -61,26 +68,66 @@ func BlocksForVersion(v string) (ret []*Block) {
|
|||||||
var b []*Block
|
var b []*Block
|
||||||
must(LoadVersionedJson(v, "blocks", &b))
|
must(LoadVersionedJson(v, "blocks", &b))
|
||||||
BlocksCache[v] = 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
|
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) {
|
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) {
|
for _, b := range BlocksForVersion(version) {
|
||||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
if b.Name == name {
|
||||||
block = b
|
block = b
|
||||||
ok = true
|
ok = true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupBlockByName(version string, stateId int32) (block *Block, ok bool) {
|
// StateIdFromBlocKAndStateMap returns the global palette state ID for the given block name and state property map in the given version.
|
||||||
for _, b := range BlocksForVersion(version) {
|
func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) {
|
||||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
b, ok := LookupBlockByName(version, name)
|
||||||
block = b
|
if !ok {
|
||||||
ok = true
|
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
|
return
|
||||||
}
|
}
|
||||||
@@ -92,7 +139,8 @@ var BadBlockStateTypeErr = errors.New("bad block state type")
|
|||||||
var BadEnumErr = errors.New("bad enum")
|
var BadEnumErr = errors.New("bad enum")
|
||||||
var SupplyAllStatesErr = errors.New("supply all states")
|
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)
|
b, ok := LookupBlockByStateId(version, stateId)
|
||||||
if !ok {
|
if !ok {
|
||||||
err = BlockNotFoundErr
|
err = BlockNotFoundErr
|
||||||
@@ -101,19 +149,17 @@ func FromBlockState(version string, stateId int32) (b *Block, stateData map[stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: mc stores these as strings so we will do the same
|
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v string, err error) {
|
||||||
|
|
||||||
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
case "int":
|
case "int":
|
||||||
v = n
|
v = strconv.Itoa(int(n))
|
||||||
case "enum":
|
case "enum":
|
||||||
v = s.Values[n]
|
v = s.Values[n]
|
||||||
case "bool":
|
case "bool":
|
||||||
if n == 0 { // MOJANK: 0 is true, 1 is false
|
if n == 0 { // MOJANK: 0 is true, 1 is false
|
||||||
v = true
|
v = "true"
|
||||||
} else if n == 1 {
|
} else if n == 1 {
|
||||||
v = false
|
v = "false"
|
||||||
} else {
|
} else {
|
||||||
err = InvalidBoolValueErr
|
err = InvalidBoolValueErr
|
||||||
}
|
}
|
||||||
@@ -123,35 +169,32 @@ func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcDataBlockStateTypeSerialize(v any, s *BlockState) (n int32, err error) {
|
func mcDataBlockStateTypeSerialize(v string, s *BlockState) (n int32, err error) {
|
||||||
switch t := v.(type) {
|
if v == "true" {
|
||||||
case int32:
|
return 0, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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
|
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 {
|
if stateId < b.MinStateId || stateId > b.MaxStateId {
|
||||||
err = BlockStateOutOfRangeErr
|
err = BlockStateOutOfRangeErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ret = make(map[string]any)
|
ret = make(map[string]string)
|
||||||
stateId -= b.MinStateId
|
stateId -= b.MinStateId
|
||||||
|
|
||||||
a := int32(1)
|
a := int32(1)
|
||||||
@@ -166,7 +209,7 @@ func (b *Block) StateMapFromId(stateId int32) (ret map[string]any, err error) {
|
|||||||
return
|
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)
|
a := int32(1)
|
||||||
for i := range len(b.States) {
|
for i := range len(b.States) {
|
||||||
s := b.States[len(b.States)-1-i]
|
s := b.States[len(b.States)-1-i]
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/admin-else/strom/data"
|
"github.com/admin-else/strom/mc/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLookupBlockByStateId(t *testing.T) {
|
func TestLookupBlockByStateId(t *testing.T) {
|
||||||
@@ -32,3 +32,11 @@ func TestBlockStates(t *testing.T) {
|
|||||||
t.Fatal("state id mismatch")
|
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() {
|
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
|
package data
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// "id": 2,
|
// "id": 2,
|
||||||
// "name": "granite",
|
// "name": "granite",
|
||||||
@@ -15,18 +17,26 @@ type Item struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ItemsCache = make(map[string][]*Item)
|
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) {
|
func ItemsForVersion(v string) (ret []*Item) {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
ItemsCacheMutex.RLock()
|
||||||
if ret, ok = ItemsCache[v]; ok {
|
if ret, ok = ItemsCache[v]; ok {
|
||||||
|
ItemsCacheMutex.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ItemsCacheMutex.RUnlock()
|
||||||
var b []*Item
|
var b []*Item
|
||||||
must(LoadVersionedJson(v, "items", &b))
|
must(LoadVersionedJson(v, "items", &b))
|
||||||
|
ItemsCacheMutex.Lock()
|
||||||
ItemsCache[v] = b
|
ItemsCache[v] = b
|
||||||
|
ItemsCacheMutex.Unlock()
|
||||||
return b
|
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) {
|
func LookupItemById(version string, id int32) (item *Item, ok bool) {
|
||||||
for _, i := range ItemsForVersion(version) {
|
for _, i := range ItemsForVersion(version) {
|
||||||
if i.Id == id {
|
if i.Id == id {
|
||||||
@@ -38,6 +48,7 @@ func LookupItemById(version string, id int32) (item *Item, ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupItemByName returns the item with the given name in the given version.
|
||||||
func LookupItemByName(version string, name string) (item *Item, ok bool) {
|
func LookupItemByName(version string, name string) (item *Item, ok bool) {
|
||||||
for _, i := range ItemsForVersion(version) {
|
for _, i := range ItemsForVersion(version) {
|
||||||
if i.Name == name {
|
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user