Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e324a95727 | ||
|
|
7f878f40a1 | ||
|
|
7baa9990cb | ||
|
|
5c5c391aa6 | ||
|
|
b966925212 | ||
|
|
efd7049859 | ||
|
|
3ef9446f50 | ||
|
|
68612969c9 | ||
|
|
b4712ff5c6 | ||
|
|
eab050e291 | ||
|
|
a8b090227b | ||
|
|
201e0dfa44 | ||
|
|
666f250b3e | ||
|
|
b2784574bd | ||
|
|
c0b1501f23 | ||
|
|
4de76d8023 | ||
|
|
f08684fb67 | ||
|
|
9e4f9409c5 | ||
|
|
4d3fbed957 | ||
|
|
b338888b58 | ||
|
|
4956c1d874 | ||
|
|
d4fe362d9f | ||
|
|
5d8ff870df | ||
|
|
58c9a60236 | ||
|
|
3e5b141ec7 | ||
|
|
efeb5ecda6 | ||
|
|
624e8ad4de | ||
|
|
764cfd8c4d | ||
|
|
d43ff7f83d | ||
|
|
a39999b344 | ||
|
|
8ee48e1a45 | ||
|
|
bf6affeb97 | ||
|
|
ede9a47ca2 | ||
|
|
2fb290572a | ||
|
|
021b8ddc1a | ||
|
|
05e8da2ff4 | ||
|
|
a8936d92a5 | ||
|
|
c56898e787 | ||
|
|
c483bf9553 | ||
|
|
b5ee17d1ae | ||
|
|
57cef25e27 | ||
|
|
148302c94d | ||
|
|
02c402251d | ||
|
|
c800c5d471 | ||
|
|
477f51cce7 | ||
|
|
60103ebf47 | ||
|
|
21eeebf0f7 | ||
|
|
bb6f6548d5 | ||
|
|
d6c07cf428 | ||
|
|
e98921755d | ||
|
|
60383fede1 | ||
|
|
3bfa70d384 | ||
|
|
60147fd4b6 | ||
|
|
fe51287cc6 | ||
|
|
a44c5f8606 | ||
|
|
d5979b2c3d | ||
|
|
4b95ea9ae2 | ||
|
|
a80fd29297 | ||
|
|
589f35d147 | ||
|
|
c7b5f6ba0f | ||
|
|
0600315e69 | ||
|
|
12005df68b | ||
|
|
600842a679 | ||
|
|
c93d907d46 | ||
|
|
be6d2fb461 | ||
|
|
b2df88203d | ||
|
|
2ed4498181 | ||
|
|
0316b0aa09 | ||
|
|
62d12386e1 | ||
|
|
00ea02a529 | ||
|
|
e5231dfdb6 | ||
|
|
3b1b436ecf | ||
|
|
47180bf650 | ||
|
|
325db98b99 | ||
|
|
ceae543352 | ||
|
|
aef9615476 | ||
|
|
5d3860b91e | ||
|
|
94d15258d8 | ||
|
|
ab03b36b4f | ||
|
|
da0c3c6f14 | ||
|
|
b01e482bc5 | ||
|
|
c560966177 | ||
|
|
66f86c5599 | ||
|
|
5a2bfcb1c1 | ||
|
|
b34f8d68ff | ||
|
|
c98472165b | ||
|
|
4888a07ebb | ||
|
|
a9bf30a609 | ||
|
|
35030601e1 | ||
|
|
66f32b4cd2 | ||
|
|
e6d08dccbb | ||
|
|
8a4f203621 | ||
|
|
69e230a79c | ||
|
|
1a5f4f4f8e | ||
|
|
d0844dd6b5 | ||
|
|
d8e36aa6fa | ||
|
|
6781e4c16d | ||
|
|
fccba0248a | ||
|
|
52b3c1ce17 | ||
|
|
3511cd2699 | ||
|
|
6dddccab7f | ||
|
|
9109d77f98 | ||
|
|
9eec1e8370 | ||
|
|
96481da0a4 | ||
|
|
159437c1ca | ||
|
|
3d34ad59e9 | ||
|
|
4878c6cc7b | ||
|
|
719a3bacd8 | ||
|
|
ce0c58e7c5 | ||
|
|
8a4e44760e | ||
|
|
e810a60848 | ||
|
|
0981339028 | ||
|
|
7746dd8ee9 | ||
|
|
378b78312e | ||
|
|
bd3139754d | ||
|
|
841d91b91d | ||
|
|
895ec53698 | ||
|
|
f6001c8b22 | ||
|
|
d87c362270 | ||
|
|
236e414242 | ||
|
|
5222390b65 | ||
|
|
dc04fdc7d0 | ||
|
|
8de34dfda1 | ||
|
|
997594d7c3 | ||
|
|
c83a777eac | ||
|
|
65fea61667 | ||
|
|
c2777f2ca9 | ||
|
|
dcb42f1970 | ||
|
|
7a2081f0a0 | ||
|
|
acc583f694 | ||
|
|
76409d07a3 | ||
|
|
bb89730781 | ||
|
|
9f6b1f6bcf | ||
|
|
1b8a9677c6 | ||
|
|
79ab13ca4b | ||
|
|
3cc414185a | ||
|
|
fe4a17dce2 | ||
|
|
891551f9dd | ||
|
|
8fb48c6ea9 | ||
|
|
ec25c28061 | ||
|
|
45287f47d2 | ||
|
|
4cc4b6af5b | ||
|
|
09e6760d61 | ||
|
|
ca74a3c3d4 | ||
|
|
fbc3693219 | ||
|
|
fb69fd93ec | ||
|
|
a3a3c3714e | ||
|
|
9294631c6c | ||
|
|
293f0b1eed | ||
|
|
272dcb4d36 | ||
|
|
abb0fa4d37 | ||
|
|
e5212624af | ||
|
|
35772aca03 | ||
|
|
819d89b26e | ||
|
|
958102e43c | ||
|
|
f749d3bfc5 | ||
|
|
d207cf52cf | ||
|
|
420ec5611c | ||
|
|
4bfaf9b7c5 | ||
|
|
48beb861e8 | ||
|
|
9fd2f517d9 | ||
|
|
1b5af63b75 | ||
|
|
dfb847eb3e | ||
|
|
322bd77f7f | ||
|
|
aefe850281 | ||
|
|
57f88f2176 | ||
|
|
8dd566c0da | ||
|
|
e386a45d96 | ||
|
|
8f771166a6 | ||
|
|
5d6a1a25ea | ||
|
|
ca33185f20 | ||
|
|
e921e9e9d2 | ||
|
|
7337068d86 | ||
|
|
9983062522 | ||
|
|
4fd5324064 | ||
|
|
df0e5e354b | ||
|
|
23b5a5d9b7 | ||
|
|
46770ed980 | ||
|
|
6179c5f6b6 | ||
|
|
f758a10575 | ||
|
|
41d1ab4c45 | ||
|
|
032fa26927 | ||
|
|
f7401a287e | ||
|
|
2d1e8ef4f6 | ||
|
|
2802cc36ce | ||
|
|
ea024fe808 |
@@ -0,0 +1,6 @@
|
||||
**/*_test.go export-ignore
|
||||
**/testdata export-ignore
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.tmcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mcpr filter=lfs diff=lfs merge=lfs -text
|
||||
*.mca filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -0,0 +1,35 @@
|
||||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout with LFS
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.26.2'
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Vet
|
||||
run: go vet -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.devres/
|
||||
.idea/
|
||||
.failed_packets/
|
||||
tools/.*/
|
||||
.vscode/
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "data/minecraft-data"]
|
||||
path = data/minecraft-data
|
||||
url = git@github.com:PrismarineJS/minecraft-data.git
|
||||
@@ -0,0 +1,5 @@
|
||||
# STROM
|
||||
|
||||
# THIS IS WORK IN PROGRESS AND THE API WILL BREAK AS LONG AS THE THE PACKAGE VERSION IS v0.x.x
|
||||
|
||||
feel free to contact me on [discord](https://discord.gg/n2Vjc2vcMf)
|
||||
@@ -1,40 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
)
|
||||
|
||||
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
|
||||
// You must close the connection when you're done with it
|
||||
func Connect(addr string) (ret *proto.Conn, err error) {
|
||||
s, err := Status(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret, err = ConnectVersionLess(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ret.SetProtocolVersion(s.Version.Protocol)
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectVersionLess connects to the given address without setting the version you should set it
|
||||
// when using this function.
|
||||
// You must close the connection when you're done with it
|
||||
func ConnectVersionLess(addr string) (ret *proto.Conn, err error) {
|
||||
ret = &proto.Conn{}
|
||||
ret.SetState(proto_base.Handshaking)
|
||||
ret.SetCompressionThreshold(-1)
|
||||
ret.Actor = proto_base.Client
|
||||
ret.Conn, err = net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.R = ret.Conn
|
||||
ret.W = ret.Conn
|
||||
return
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DNSCheckFunc func(addr string) error
|
||||
|
||||
// DoDns resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
func DoDns(in string) (out string, host string, portN uint16, err error) {
|
||||
return DoDNSChecked(in, nil)
|
||||
}
|
||||
|
||||
// DoDNSChecked resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It also optionally accepts a function to "check" the address
|
||||
func DoDNSChecked(in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
|
||||
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if f == nil {
|
||||
f = func(addr string) error { return nil }
|
||||
}
|
||||
|
||||
err = f(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN = uint16(porti)
|
||||
outIP, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
outIP, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
return
|
||||
}
|
||||
|
||||
// DoDnsSimple resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It does not check the address
|
||||
func DoDnsSimple(in string) (out string, err error) {
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
var addrErr *net.AddrError
|
||||
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN := uint16(porti)
|
||||
_, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out = in
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
out = host + ":" + strconv.Itoa(int(portN))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("api", flag.ContinueOnError)
|
||||
var tokenFlag = cmd.String("token", os.Getenv("MC_TOKEN"), "token to use")
|
||||
|
||||
var renameFlag = cmd.String("rename", "", "rename the player")
|
||||
|
||||
var uuidFlag = cmd.String("uuid", "", "get data about player with uuid")
|
||||
var nameFlag = cmd.String("name", "", "get data about player with name")
|
||||
var unsignedFlag = cmd.Bool("unsigned", true, "use unsigned mode for properties signatures")
|
||||
|
||||
var PleaseProvideArgumentErr = errors.New("please provide an argument")
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a := &api.Account{}
|
||||
if *tokenFlag != "" {
|
||||
a, err = api.NewAccountFromYGG(*tokenFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var data any
|
||||
if *renameFlag != "" {
|
||||
data, err = a.ChangeName(*renameFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if *uuidFlag != "" || *nameFlag != "" {
|
||||
if *nameFlag != "" {
|
||||
var idName api.IdName
|
||||
idName, err = api.NameToId(*nameFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
*uuidFlag = idName.Id
|
||||
}
|
||||
data, err = api.Profile(*uuidFlag, *unsignedFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
slog.Info("account", "name", a.Name, "uuid", a.Uuid)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(os.Stdout).Encode(data)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cmd_util
|
||||
|
||||
import "github.com/admin-else/strom/mc/api"
|
||||
|
||||
func Account(s string) (a *api.Account, err error) {
|
||||
if len(s) > 16 {
|
||||
return api.NewAccountFromYGG(s)
|
||||
}
|
||||
a = api.NewOfflineAccount(s)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
mcdata "github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto_generated"
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
// strom data 1.21.11 blocks id 0 name => air
|
||||
// {
|
||||
// "id": 0,
|
||||
// "name": "air",
|
||||
// "displayName": "Air",
|
||||
// "hardness": 0.0,
|
||||
// "resistance": 0.0,
|
||||
// "stackSize": 64,
|
||||
// "diggable": false,
|
||||
// "material": "default",
|
||||
// "transparent": true,
|
||||
// "emitLight": 0,
|
||||
// "filterLight": 0,
|
||||
// "defaultState": 0,
|
||||
// "minStateId": 0,
|
||||
// "maxStateId": 0,
|
||||
// "states": [],
|
||||
// "drops": [],
|
||||
// "boundingBox": "empty"
|
||||
// },
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
var dataValues any
|
||||
switch len(args) {
|
||||
case 0: // print available versions
|
||||
dataValues = proto_generated.SupportedVersions
|
||||
case 1: // print available data for version
|
||||
dataValues = slices.Collect(maps.Keys(mcdata.Paths.Data[args[0]]))
|
||||
case 2:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
case 3:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataSlices []any
|
||||
dataSlices, err = util.AssertTypeError[[]any](dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var result = make([]any, len(dataSlices))
|
||||
for _, v := range dataSlices {
|
||||
var dataMap map[string]any
|
||||
dataMap, err = util.AssertTypeError[map[string]any](v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var vToAppend any
|
||||
vToAppend, err = util.MapGetError(dataMap, args[2])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = append(result, vToAppend)
|
||||
}
|
||||
dataValues = result
|
||||
case 4, 5:
|
||||
err = mcdata.LoadVersionedJson(args[0], args[1], &dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dataSlices []any
|
||||
dataSlices, err = util.AssertTypeError[[]any](dataValues)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, v := range dataSlices {
|
||||
var dataMap map[string]any
|
||||
dataMap, err = util.AssertTypeError[map[string]any](v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if v2, ok := dataMap[args[2]]; ok {
|
||||
if args[3] != fmt.Sprint(v2) {
|
||||
continue
|
||||
}
|
||||
|
||||
dataValues = dataMap
|
||||
found = true
|
||||
if len(args) == 5 {
|
||||
dataValues, err = util.MapGetError(dataMap, args[4])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = fmt.Errorf("not found")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("too many arguments")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", " ")
|
||||
err = e.Encode(dataValues)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package extract_mca
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/level/anvil"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-nbt", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The MCA file to extract")
|
||||
IndexXFlag = cmd.Int("x", 0, "The x chunk coordinate")
|
||||
IndexZFlag = cmd.Int("z", 0, "The z chunk coordinate")
|
||||
ToFlag = cmd.String("to", "mca-chunk.nbt", "The output file")
|
||||
)
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t, err := mca.TimeAt(f, int32(*IndexXFlag), int32(*IndexZFlag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Chunk last written", "time", t)
|
||||
n, err := mca.ChunkAt(f, int32(*IndexXFlag), int32(*IndexZFlag))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = nbt.WriteUnstructuredFilePath(*ToFlag, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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/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,
|
||||
"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("./level/anvil/testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n *nbt.Tag
|
||||
n, err = mca.ChunkAt(f, x, z)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var chunk anvil.Chunk
|
||||
err = nbt.Format.Decode(n.Value, &chunk)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var packetChunk *level.Chunk
|
||||
packetChunk, err = chunk.ToStorage(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = packetChunk.WriteChunkData(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
err := cmd.Parse(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *worldPath == "" {
|
||||
slog.Error("no world path specified")
|
||||
return nil
|
||||
}
|
||||
if *output == "" {
|
||||
slog.Error("no output path specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
outFile, err := os.Create(*output)
|
||||
if err != nil {
|
||||
slog.Error("failed to create output file", "error", err)
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
w := replay.NewWriter(outFile)
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketLogin{
|
||||
EntityId: 73,
|
||||
IsHardcore: false,
|
||||
WorldNames: []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"},
|
||||
MaxPlayers: 100,
|
||||
ViewDistance: 10,
|
||||
SimulationDistance: 10,
|
||||
ReducedDebugInfo: false,
|
||||
EnableRespawnScreen: true,
|
||||
DoLimitedCrafting: false,
|
||||
WorldState: v1_21_11.PlayToClientSpawnInfo{
|
||||
Dimension: 0,
|
||||
Name: "minecraft:overworld",
|
||||
HashedSeed: -8365436745629239539,
|
||||
Gamemode: "survival",
|
||||
PreviousGamemode: 0xff,
|
||||
IsDebug: false,
|
||||
IsFlat: false,
|
||||
Death: nil,
|
||||
PortalCooldown: 0,
|
||||
SeaLevel: 63,
|
||||
},
|
||||
EnforcesSecureChat: false,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write login packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketPosition{
|
||||
TeleportId: 0,
|
||||
X: *spawnX,
|
||||
Y: *spawnY,
|
||||
Z: *spawnZ,
|
||||
Dx: 0,
|
||||
Dy: 0,
|
||||
Dz: 0,
|
||||
Yaw: 0,
|
||||
Pitch: 0,
|
||||
Flags: v1_21_11.PlayToClientPositionUpdateRelatives{},
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write position packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
chunkX := int32(*spawnX) >> 4
|
||||
chunkZ := int32(*spawnZ) >> 4
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketUpdateViewPosition{
|
||||
ChunkX: chunkX,
|
||||
ChunkZ: chunkZ,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write view position packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.WritePacket(&v1_21_11.PlayToClientPacketGameStateChange{
|
||||
Reason: "level_chunks_load_start",
|
||||
}, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to write game state packet", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
radius32 := int32(*radius)
|
||||
|
||||
for x := -radius32; x < radius32; x++ {
|
||||
for z := -radius32; z < radius32; z++ {
|
||||
var chunkData []byte
|
||||
chunkData, err = GetChunkFromMca(x, z, "1.21.11")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chunkData == nil {
|
||||
continue
|
||||
}
|
||||
OLDpacket := &v1_21_11.PlayToClientPacketMapChunk{}
|
||||
OLDpacket.X = x
|
||||
OLDpacket.Z = z
|
||||
OLDpacket.ChunkData = v1_21_11.ByteArray{Val: chunkData}
|
||||
OLDpacket.BlockLight = FullLight
|
||||
OLDpacket.BlockLightMask = []int64{(1 << 24) - 1}
|
||||
err = w.WritePacket(OLDpacket, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("conversion complete", "output", *output)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package messenger
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/admin-else/strom/cmd/strom/cmd_util"
|
||||
"github.com/admin-else/strom/mc/bot/chat"
|
||||
"github.com/admin-else/strom/mc/bot/keepalive"
|
||||
"github.com/admin-else/strom/mc/bot/world"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
)
|
||||
|
||||
func stror(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
||||
connectToFlag = cmd.String("addr", "", "server address to connect to")
|
||||
accFlag = cmd.String("acc", stror(os.Getenv("STROM_ACC"), "strom_messenger"), "the name to use if longer will be treated as ygg token")
|
||||
)
|
||||
|
||||
type Messenger struct {
|
||||
*proto.Conn
|
||||
chat *chat.Module
|
||||
world *world.Module
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
|
||||
m.Log.Info("chat", "author", e.SenderUuid, "m", e.PlainMessage)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChatUnsigned(e *v1_21_11.PlayToClientPacketProfilelessChat) (err error) {
|
||||
m.Log.Info("chatP", "author", e.Name, "m", e.Message)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err error) {
|
||||
m.Log.Info("chatS", "m", e.Content)
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
||||
input := strings.TrimSpace(e.Val)
|
||||
if strings.HasPrefix(input, ">say ") {
|
||||
return m.chat.SendMessage(strings.TrimSpace(strings.TrimPrefix(input, ">say")))
|
||||
}
|
||||
if strings.HasPrefix(input, ">getblock ") {
|
||||
return m.handleGetBlock(strings.TrimSpace(strings.TrimPrefix(input, ">getblock")))
|
||||
}
|
||||
return m.chat.SendMessage(input)
|
||||
}
|
||||
|
||||
func (m *Messenger) handleGetBlock(args string) (err error) {
|
||||
fields := strings.Fields(args)
|
||||
if len(fields) != 3 {
|
||||
m.Log.Info("getblock", "error", "expected >getblock <x> <y> <z>")
|
||||
return nil
|
||||
}
|
||||
|
||||
coords := make([]int64, 3)
|
||||
for i, f := range fields {
|
||||
coords[i], err = strconv.ParseInt(f, 10, 32)
|
||||
if err != nil {
|
||||
m.Log.Info("getblock", "error", fmt.Sprintf("bad coordinate %q", f))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
stateId, err := m.world.GetBlock(int32(coords[0]), int32(coords[1]), int32(coords[2]))
|
||||
if err != nil {
|
||||
m.Log.Info("getblock", "error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
block, ok := data.LookupBlockByStateId(m.Version, stateId)
|
||||
if !ok {
|
||||
m.Log.Info("getblock", "stateId", stateId, "name", "unknown")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Log.Info("getblock", "x", coords[0], "y", coords[1], "z", coords[2], "name", block.Name, "stateId", stateId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
acc, err := cmd_util.Account(*accFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := client.Login(*connectToFlag, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
chatMod, err := chat.Start(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Shareable world storage. Other bots can subscribe to the same World.
|
||||
w := world.NewWorld(c.Version, -64, 384)
|
||||
|
||||
m := &Messenger{
|
||||
Conn: c,
|
||||
chat: chatMod,
|
||||
world: world.Start(c, w),
|
||||
}
|
||||
|
||||
event.StartListingStdin(m.Loop)
|
||||
m.Register(m.OnStdin)
|
||||
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
||||
keepalive.Start(m.Conn)
|
||||
|
||||
return m.StartConn()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("offline-uuid", flag.ContinueOnError)
|
||||
@@ -0,0 +1,169 @@
|
||||
package packet_info
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("packet-info", flag.ContinueOnError)
|
||||
var versionFlag = cmd.String("version", "", "protocol version or minecraft version name")
|
||||
var stateFlag = cmd.String("state", "", "handshaking, status, login, configuration, play")
|
||||
var directionFlag = cmd.String("direction", "", "toserver, toclient")
|
||||
var idFlag = cmd.Int("id", -1, "packet id")
|
||||
var nameFlag = cmd.String("name", "", "packet name")
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var version int32
|
||||
if *versionFlag != "" {
|
||||
v, err := data.LookUpVersionByName(*versionFlag)
|
||||
if err != nil {
|
||||
v2, err := parseVersion(*versionFlag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid version: %s", *versionFlag)
|
||||
}
|
||||
v, err = data.LookUpVersionByProtocolVersion(v2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
version = v.Version
|
||||
}
|
||||
|
||||
var state proto_base.State
|
||||
if *stateFlag != "" {
|
||||
state, err = parseState(*stateFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var direction proto_base.Direction
|
||||
if *directionFlag != "" {
|
||||
direction, err = parseDirection(*directionFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if *idFlag != -1 && *nameFlag != "" {
|
||||
err = fmt.Errorf("cannot specify both id and name")
|
||||
return
|
||||
}
|
||||
|
||||
if *idFlag != -1 {
|
||||
if *versionFlag == "" || *stateFlag == "" || *directionFlag == "" {
|
||||
err = fmt.Errorf("version, state, and direction are required when looking up by id")
|
||||
return
|
||||
}
|
||||
p, ok := proto.LookUpTypeByPacketInfo(direction, state, int32(*idFlag), version)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
return
|
||||
}
|
||||
|
||||
if *nameFlag != "" {
|
||||
if *versionFlag == "" {
|
||||
err = fmt.Errorf("version is required when looking up by name")
|
||||
return
|
||||
}
|
||||
if state != 0 || direction != 0 {
|
||||
p, ok := proto.LookupPacketInfoByNameProtocolVersionStateAndDirection(*nameFlag, version, state, direction)
|
||||
if !ok {
|
||||
err = fmt.Errorf("packet not found")
|
||||
return
|
||||
}
|
||||
printPacketInfo(p)
|
||||
} else {
|
||||
var found bool
|
||||
for _, p := range proto_generated.Packets {
|
||||
if p.Name == *nameFlag && p.ProtocolVersion == version {
|
||||
printPacketInfo(p)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = fmt.Errorf("packet not found")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("must specify either -id or -name")
|
||||
return
|
||||
}
|
||||
|
||||
func parseState(s string) (proto_base.State, error) {
|
||||
switch s {
|
||||
case "handshaking":
|
||||
return proto_base.Handshaking, nil
|
||||
case "status":
|
||||
return proto_base.Status, nil
|
||||
case "login":
|
||||
return proto_base.Login, nil
|
||||
case "configuration":
|
||||
return proto_base.Configuration, nil
|
||||
case "play":
|
||||
return proto_base.Play, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown state: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDirection(s string) (proto_base.Direction, error) {
|
||||
switch s {
|
||||
case "toserver":
|
||||
return proto_base.ToServer, nil
|
||||
case "toclient":
|
||||
return proto_base.ToClient, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown direction: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func parseVersion(val string) (int32, error) {
|
||||
v, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
func printPacketInfo(p proto_base.PacketInfo) {
|
||||
v, _ := data.LookUpVersionByProtocolVersion(p.ProtocolVersion)
|
||||
fmt.Printf("Name: %s\n", p.Name)
|
||||
fmt.Printf("ID: %d\n", p.PacketId)
|
||||
fmt.Printf("State: %s\n", stateToString(p.State))
|
||||
fmt.Printf("Direction: %s\n", p.Direction)
|
||||
fmt.Printf("Version: %d (%s)\n", p.ProtocolVersion, v.MinecraftVersion)
|
||||
}
|
||||
|
||||
func stateToString(s proto_base.State) string {
|
||||
switch s {
|
||||
case proto_base.Handshaking:
|
||||
return "handshaking"
|
||||
case proto_base.Status:
|
||||
return "status"
|
||||
case proto_base.Login:
|
||||
return "login"
|
||||
case proto_base.Configuration:
|
||||
return "configuration"
|
||||
case proto_base.Play:
|
||||
return "play"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package packet_inspector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
var Cmd = flag.NewFlagSet("packet-inspector", flag.ContinueOnError)
|
||||
|
||||
var OfflineNameFlag = Cmd.String("name", "packet_inspector", "offline player name")
|
||||
var Token = Cmd.String("token", "", "token to use")
|
||||
|
||||
var TargetAddr = Cmd.String("addr", "127.0.0.1:25565", "address to connect to")
|
||||
var ListenAddr = Cmd.String("listen", "127.0.0.1:25566", "address to listen on")
|
||||
var TrueForward = Cmd.Bool("true-forward", false, "forward packets without modification -name and custom server status wont work")
|
||||
|
||||
type Proxy struct {
|
||||
Client, Servee *proto.Conn
|
||||
Data nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) Start() (err error) {
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnFinishLogin)
|
||||
p.Servee.RegisterCriticalUntilLatest(p.OnHandshake)
|
||||
//p.Servee.RegisterCriticalUntilLatest(p.OnCompress)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnCompress)
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- p.Client.StartConn()
|
||||
}()
|
||||
go func() {
|
||||
errChan <- p.Servee.StartConn()
|
||||
}()
|
||||
err = <-errChan
|
||||
return
|
||||
}
|
||||
|
||||
type PacketLogEvent struct {
|
||||
Time time.Time
|
||||
Packet proto_base.EncodeDecodeAble
|
||||
Info PacketInfoNoTypeJson
|
||||
ClientAddr string
|
||||
}
|
||||
|
||||
type PacketInfoNoTypeJson struct {
|
||||
Type proto_base.EncodeDecodeAble `json:"-"`
|
||||
Name string
|
||||
Direction proto_base.Direction
|
||||
State proto_base.State
|
||||
PacketId int32
|
||||
ProtocolVersion int32
|
||||
}
|
||||
|
||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||
packet, ok := e.Val.(proto_base.EncodeDecodeAble)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
packetInfo, ok := proto.LookupPacketInfoByType(packet)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonBytes, err := json.Marshal(PacketLogEvent{
|
||||
Time: time.Now(),
|
||||
Packet: packet,
|
||||
ClientAddr: p.Client.RemoteAddr().String(),
|
||||
Info: PacketInfoNoTypeJson(packetInfo),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// :pray:
|
||||
_, _ = os.Stdout.Write(jsonBytes)
|
||||
_, _ = os.Stdout.Write([]byte("\n"))
|
||||
|
||||
switch packetInfo.Direction {
|
||||
case proto_base.ToServer:
|
||||
err = p.Client.Send(packet)
|
||||
case proto_base.ToClient:
|
||||
err = p.Servee.Send(packet)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnFinishConfiguration(packet *v1_21_11.ConfigurationToServerPacketFinishConfiguration) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Play)
|
||||
p.Client.SetState(proto_base.Play)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnFinishLogin(packet *v1_21_11.LoginToServerPacketLoginAcknowledged) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(proto_base.Configuration)
|
||||
p.Client.SetState(proto_base.Configuration)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnHandshake(packet *v1_21_11.HandshakingToServerPacketSetProtocol) (err error) {
|
||||
err = p.Client.Send(packet)
|
||||
state := proto_base.State(packet.NextState)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetState(state)
|
||||
p.Client.SetState(state)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnCompress(packet *v1_21_11.LoginToClientPacketCompress) (err error) {
|
||||
err = p.Servee.Send(packet)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.Servee.SetCompressionThreshold(packet.Threshold)
|
||||
p.Client.SetCompressionThreshold(packet.Threshold)
|
||||
err = event.DontForwardErr
|
||||
return
|
||||
}
|
||||
|
||||
var StatusResponse = server.StatusResponse{
|
||||
Version: server.StatusResponseVersion{
|
||||
Name: text.Pretty("STROM"),
|
||||
Protocol: 772,
|
||||
},
|
||||
Players: server.StatusResponsePlayers{},
|
||||
Description: text.Pretty("Packet spy forwards to " + *TargetAddr),
|
||||
Favicon: "",
|
||||
EnforcesSecureChat: false,
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = Cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("Starting packet inspector", "listen", *ListenAddr)
|
||||
err = server.StartServerWithOnConn(*ListenAddr, func(serveeConn *proto.Conn) (err error) {
|
||||
p := &Proxy{Servee: serveeConn, Client: nil}
|
||||
var acc = api.NewOfflineAccount(*OfflineNameFlag)
|
||||
if !*TrueForward {
|
||||
if *Token != "" {
|
||||
acc, err = api.NewAccountFromYGG(*Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = server.ServeLogin(serveeConn, server.WithOtherAccount(acc), server.WithStatus(StatusResponse), server.WithoutOnlineMode(), server.WithoutEncryption())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*TargetAddr, _, _, err = client.DoDNSChecked(nil, *TargetAddr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := client.Connect(nil, *TargetAddr, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
if !*TrueForward {
|
||||
err = client.LoginRaw(c, acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
p.Client = c
|
||||
p.Client.AlwaysDecode = true
|
||||
p.Servee.AlwaysDecode = true
|
||||
err = p.Start()
|
||||
return
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package print_nbt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-nbt", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The NBT file to print")
|
||||
HashFormatFlag = cmd.Bool("snbt", false, "will print with go %#v")
|
||||
)
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
n, err := nbt.ReadUnstructuredFile(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var b []byte
|
||||
if *HashFormatFlag {
|
||||
bb := bytes.NewBuffer(nil)
|
||||
err = nbt.PrintSNBTAny(n.Value, bb)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = bb.Bytes()
|
||||
//b = []byte(strings.ReplaceAll(string(b), ",", ",\n"))
|
||||
} else {
|
||||
b, err = json.MarshalIndent(n, "", " ")
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = os.Stdout.Write(b)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package failed_packets_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
generated "github.com/admin-else/strom/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,161 @@
|
||||
package print_replay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
)
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("print-replay", flag.ContinueOnError)
|
||||
FileFlag = cmd.String("file", "", "The tmcpr file to print")
|
||||
ProtocolVersion = cmd.Int("protocol", -1, "The protocol version")
|
||||
CreateTestsPath = cmd.String("create-tests", "", "")
|
||||
StateFlag = cmd.String("state", "login", "Initial state: login, config, play")
|
||||
)
|
||||
|
||||
// TODO: maybe use the proto replay api
|
||||
|
||||
//go:embed failed_packet.go.tmpl
|
||||
var TestSrcF string
|
||||
|
||||
func SaveUnCodeAbleAsTest(packet *proto.UnCodablePacket) {
|
||||
hUntrimmed := sha256.Sum256(packet.Data)
|
||||
h := hUntrimmed[:8]
|
||||
f, err := os.Create(fmt.Sprintf(*CreateTestsPath+"/%v_%10x_test.go", len(packet.Data), h))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ret, err := data.LookUpVersionByProtocolVersion(packet.Info.ProtocolVersion)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
goPackage := "v" + strings.ReplaceAll(ret.MinecraftVersion, ".", "_")
|
||||
|
||||
//goland:noinspection GoUnhandledErrorResult
|
||||
defer f.Close()
|
||||
_, err = fmt.Fprintf(f, TestSrcF, goPackage, packet.Err, h, reflect.TypeOf(packet.Info.Type).Elem().Name(), packet.Data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
slog.Info("Packet failed to parse", "error", packet.Err, "saved", f.Name(), "data", fmt.Sprintf("%q", packet.Data))
|
||||
}
|
||||
|
||||
var state = proto_base.Login
|
||||
|
||||
func UnpackPacket(r io.Reader) (packet proto_base.EncodeDecodeAble, 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()
|
||||
|
||||
i, ok := proto.LookUpTypeByPacketInfo(proto_base.ToClient, 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
|
||||
}
|
||||
switch i.Name {
|
||||
case "success":
|
||||
state = proto_base.Configuration
|
||||
case "finish_configuration":
|
||||
state = proto_base.Play
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *ProtocolVersion == -1 {
|
||||
return fmt.Errorf("no protocol version specified")
|
||||
}
|
||||
if *FileFlag == "" {
|
||||
return fmt.Errorf("no file specified")
|
||||
}
|
||||
switch *StateFlag {
|
||||
case "login":
|
||||
state = proto_base.Login
|
||||
case "config":
|
||||
state = proto_base.Configuration
|
||||
case "play":
|
||||
state = proto_base.Play
|
||||
default:
|
||||
return fmt.Errorf("unknown state: %s", *StateFlag)
|
||||
}
|
||||
f, err := os.Open(*FileFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var totalPackets, failedPackets int
|
||||
for {
|
||||
var packet proto_base.EncodeDecodeAble
|
||||
packet, err = UnpackPacket(f)
|
||||
unCodablePacket, ok := packet.(*proto.UnCodablePacket)
|
||||
if ok {
|
||||
if *CreateTestsPath != "" {
|
||||
SaveUnCodeAbleAsTest(unCodablePacket)
|
||||
}
|
||||
failedPackets++
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
fmt.Printf("Packet: %#v\n", packet)
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
totalPackets++
|
||||
}
|
||||
fmt.Printf("Total packets: %d, failed packets: %d, %%failed: %.2f%%\n", totalPackets, failedPackets, float64(failedPackets)/float64(totalPackets)*100)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d90ee976987678593a6b1d09136bb9b4fd131390796ea6e413376a6ddc63226
|
||||
size 75035693
|
||||
@@ -0,0 +1 @@
|
||||
[{"realTimestamp":9845034,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9845138,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9844902,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9908896,"value":{"position":{"x":-1.6053634059630886,"y":91.672484820985,"z":-10.594536888795163,"yaw":-1151.6353,"pitch":16.142092,"roll":0.0}}},{"realTimestamp":9844750,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9853735,"value":{"position":{"x":-31.05268144454021,"y":88.0,"z":-25.371850030366133,"yaw":-624.37897,"pitch":16.105839,"roll":0.0}}},{"realTimestamp":9845690,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9938194,"value":{"position":{"x":39.88206777447001,"y":103.87657142499006,"z":58.09267383764874,"yaw":-1781.1023,"pitch":37.354122,"roll":0.0}}},{"realTimestamp":9846991,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-628.46875,"pitch":36.04,"roll":0.0}}},{"realTimestamp":9908638,"value":{"position":{"x":-1.5870303355717672,"y":91.672484820985,"z":-11.359450846926011,"yaw":-1142.6902,"pitch":20.997843,"roll":0.0}}},{"realTimestamp":9845490,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}},{"realTimestamp":9845990,"value":{"position":{"x":-31.946051078575692,"y":93.0,"z":29.523387113769626,"yaw":-627.44653,"pitch":37.573395,"roll":0.0}}}]
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d972b9b5f9d962199fe262eccef32a4b94504283fae97f9963702e7ab1550129
|
||||
size 437123
|
||||
@@ -0,0 +1 @@
|
||||
{"singleplayer":false,"serverName":"protect-test-server.anygate.cloud","customServerName":"proxied test server","duration":3368,"date":1776531741320,"mcversion":"1.21.11","fileFormat":"MCPR","fileFormatVersion":14,"protocol":774,"generator":"ReplayMod v1.21.11-2.6.26","selfId":-1,"players":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"requiredMods":[{"modID":"viafabricplus","modName":"ViaFabricPlus","modVersion":"4.4.4"}]}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cef58ec0e3d1d25e34c2effc5c09241ee4c1b39c085ed9e11b7097dfd5c2d033
|
||||
size 3429878
|
||||
@@ -0,0 +1 @@
|
||||
234002168
|
||||
@@ -0,0 +1 @@
|
||||
{"singleplayer":false,"serverName":"protect-beta.anygate.cloud","customServerName":"protect-beta.anygate.cloud","duration":10198341,"date":1776535673280,"mcversion":"1.21.11","fileFormat":"MCPR","fileFormatVersion":14,"protocol":774,"generator":"ReplayMod v1.21.11-2.6.26","selfId":-1,"players":["fe2d6f64-7ab0-3373-a526-af097abbcb39"]}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"requiredMods":[{"modID":"viafabricplus","modName":"ViaFabricPlus","modVersion":"4.4.4"}]}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be007aa681f7959708e89e39e05390e228753291823a18274068d310a50aa78b
|
||||
size 526306397
|
||||
@@ -0,0 +1 @@
|
||||
270566013
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0da58f5cdd65ef241df8c61f9e633af7317faa77034183d4fb648b039c1e14ed
|
||||
size 842
|
||||
@@ -0,0 +1 @@
|
||||
{"singleplayer":false,"serverName":"protect-test-server.anygate.cloud","customServerName":"proxied test server","duration":476,"date":1776803431635,"mcversion":"1.21.11","fileFormat":"MCPR","fileFormatVersion":14,"protocol":774,"generator":"ReplayMod v1.21.11-2.6.26","selfId":-1,"players":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"requiredMods":[{"modID":"viafabricplus","modName":"ViaFabricPlus","modVersion":"4.4.4"}]}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:52fa049f97918289e018fa4c3bf5ff789d4f99f44f590ad875ce1f65e5fa9563
|
||||
size 37
|
||||
@@ -0,0 +1 @@
|
||||
1105715802
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package registry_data_hunter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/admin-else/strom/api"
|
||||
"github.com/admin-else/strom/client"
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/nbt"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/server"
|
||||
"github.com/admin-else/strom/text"
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
func wontFail[T any](v T, err error) T {
|
||||
@@ -26,10 +26,8 @@ func wontFail[T any](v T, err error) T {
|
||||
}
|
||||
|
||||
var (
|
||||
// Edit these two
|
||||
|
||||
SaveAs = "1.21.8"
|
||||
Version = "1.21.8"
|
||||
Version = "1.21.11"
|
||||
SaveAs = "data/registry/" + Version + ".nbt"
|
||||
|
||||
CompatibleProtocolVersion = wontFail(data.LookUpVersionByName(Version)).Version
|
||||
FinishedConfigErr = errors.New("finished configuration")
|
||||
@@ -45,15 +43,7 @@ var Status = wontFail(json.Marshal(server.StatusResponse{
|
||||
|
||||
type Proxy struct {
|
||||
Client, Servee *proto.Conn
|
||||
Data nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) OnStart() (err error) {
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
return
|
||||
Data *nbt.Tag
|
||||
}
|
||||
|
||||
func (p *Proxy) OnAnything(e event.Anything) (err error) {
|
||||
@@ -79,11 +69,25 @@ func (p *Proxy) OnFinishConfiguration(_ *v1_21_8.ConfigurationToClientPacketFini
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnSelectKnownPacks(data *v1_21_8.ConfigurationToClientPacketCommonSelectKnownPacks) (err error) {
|
||||
var entries []any
|
||||
for _, entry := range data.Packs {
|
||||
e := map[string]any{"Namespace": entry.Namespace, "Id": entry.Id, "Version": entry.Version}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
p.Data.Value.(map[string]any)["KnownPacks"] = entries
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Proxy) OnData(data *v1_21_8.ConfigurationToClientPacketRegistryData) (err error) {
|
||||
d := p.Data.Value.(map[string]any)["Registries"].([]any)
|
||||
var entries []any
|
||||
for _, entry := range data.Entries {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key, "Value": entry.Value.Value})
|
||||
if entry.Value != nil {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key, "Value": entry.Value.Value})
|
||||
} else {
|
||||
entries = append(entries, map[string]any{"TagType": entry.Key})
|
||||
}
|
||||
}
|
||||
d = append(d, map[string]any{"Id": data.Id, "Entries": entries})
|
||||
p.Data.Value.(map[string]any)["Registries"] = d
|
||||
@@ -106,46 +110,43 @@ func (p *Proxy) OnTags(data *v1_21_8.ConfigurationToClientPacketTags) (err error
|
||||
func handleClient(c *proto.Conn) (err error) {
|
||||
acc := api.NewOfflineAccount("RegistryHunter")
|
||||
|
||||
// ignore resource leak warning we own c conn so we should not close it here
|
||||
_, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sockPuppet, err := client.Connect(":25566")
|
||||
sockPuppet, err := client.Login(":25565", acc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer sockPuppet.Close()
|
||||
err = sockPuppet.Start(&client.LoginClient{
|
||||
Conn: sockPuppet,
|
||||
Account: acc,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
errChan := make(chan error)
|
||||
p := &Proxy{Client: sockPuppet, Servee: c}
|
||||
p.Data.Value = map[string]any{"Registries": []any{}}
|
||||
p.Client.RegisterCritical(p.OnAnything)
|
||||
p.Servee.RegisterCritical(p.OnAnything)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnData)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnTags)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnSelectKnownPacks)
|
||||
p.Client.RegisterCriticalUntilLatest(p.OnFinishConfiguration)
|
||||
|
||||
go func() {
|
||||
errChan <- p.Client.Start(p)
|
||||
errChan <- p.Client.StartConn()
|
||||
}()
|
||||
go func() {
|
||||
errChan <- p.Servee.Start(p)
|
||||
errChan <- p.Servee.StartConn()
|
||||
}()
|
||||
err = <-errChan
|
||||
f, err := os.Create("data/registry/" + SaveAs + ".nbt")
|
||||
f, err := os.Create(SaveAs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
err = nbt.WriteFile(f, p.Data)
|
||||
err = nbt.WriteUnstructuredFile(f, p.Data)
|
||||
slog.Info("saved", "file", SaveAs)
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
err := server.StartServerWithOnConn(":25565", handleClient)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return server.StartServerWithOnConn(":25566", handleClient)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package serve_tmcpr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto/replay"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*proto.Conn
|
||||
*event.Timer
|
||||
}
|
||||
|
||||
var (
|
||||
cmd = flag.NewFlagSet("serve-tmcpr", flag.ContinueOnError)
|
||||
tmcprFile = cmd.String("file", "/tmp/world.tmcpr", "Path to .tmcpr replay file")
|
||||
bindAddr = cmd.String("bind", ":25566", "Address to listen on")
|
||||
)
|
||||
|
||||
func Limbo(c *proto.Conn, tmcprPath string) (err error) {
|
||||
s := &Server{Conn: c}
|
||||
|
||||
_, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = server.ServeConfig(s.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(tmcprPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
r := replay.NewReader(f)
|
||||
r.Version = c.ProtocolVersion
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for {
|
||||
var packetData []byte
|
||||
var timestamp uint32
|
||||
packetData, timestamp, _, err = r.ReadRaw()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(timestamp)*time.Millisecond - time.Since(start))
|
||||
err = s.SendRaw(packetData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.Timer = &event.Timer{}
|
||||
s.Timer.Every(time.Second*15, s.SendKeepAlive)
|
||||
s.Timer.Start(c.Loop)
|
||||
|
||||
return s.StartConn()
|
||||
}
|
||||
|
||||
func (s *Server) SendKeepAlive() error {
|
||||
return s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()})
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
err := cmd.Parse(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *tmcprFile == "" {
|
||||
slog.Error("no file specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
return server.StartServerWithOnConn(*bindAddr, func(c *proto.Conn) error {
|
||||
return Limbo(c, *tmcprFile)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package serve_world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/level"
|
||||
"github.com/admin-else/strom/mc/level/anvil"
|
||||
"github.com/admin-else/strom/mc/nbt"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*proto.Conn
|
||||
*event.Timer
|
||||
|
||||
reconnectCalled bool
|
||||
}
|
||||
|
||||
var FullLight [][]uint8
|
||||
|
||||
func init() {
|
||||
light := make([]uint8, 2048)
|
||||
for i := range light {
|
||||
light[i] = 0xFF
|
||||
}
|
||||
|
||||
sectionsPerChunk := 384 / 16
|
||||
FullLight = make([][]uint8, sectionsPerChunk)
|
||||
for i := range sectionsPerChunk {
|
||||
FullLight[i] = light
|
||||
}
|
||||
}
|
||||
|
||||
func GetChunkFromMca(x, z int32, version string) (b []byte, err error) {
|
||||
xmca := util.FloorDiv(x, anvil.ChunksWidthRegionFile)
|
||||
zmca := util.FloorDiv(z, anvil.ChunksWidthRegionFile)
|
||||
x = x % anvil.ChunksWidthRegionFile
|
||||
if x < 0 {
|
||||
x += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
z = z % anvil.ChunksWidthRegionFile
|
||||
if z < 0 {
|
||||
z += anvil.ChunksWidthRegionFile
|
||||
}
|
||||
|
||||
f, err := os.Open(fmt.Sprintf("./level/anvil/testdata/securechattestworld/region/r.%v.%v.mca", xmca, zmca))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
mca, err := anvil.ReadChunkHeader(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n *nbt.Tag
|
||||
n, err = mca.ChunkAt(f, x, z)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var chunk anvil.Chunk
|
||||
err = nbt.Format.Decode(n.Value, &chunk)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var packetChunk *level.Chunk
|
||||
packetChunk, err = chunk.ToStorage(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = packetChunk.WriteChunkData(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func Limbo(c *proto.Conn) (err error) {
|
||||
s := &Server{
|
||||
Conn: c,
|
||||
}
|
||||
_, err = server.ServeLogin(s.Conn, server.WithoutOnlineMode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = server.ServeConfig(s.Conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
login := &v1_21_8.PlayToClientPacketLogin{EntityId: 73, IsHardcore: false, WorldNames: []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"}, MaxPlayers: 100, ViewDistance: 10, SimulationDistance: 10, ReducedDebugInfo: false, EnableRespawnScreen: true, DoLimitedCrafting: false, WorldState: v1_21_8.PlayToClientSpawnInfo{Dimension: 0, Name: "minecraft:overworld", HashedSeed: -8365436745629239539, Gamemode: "survival", PreviousGamemode: 0xff, IsDebug: false, IsFlat: false, Death: nil, PortalCooldown: 0, SeaLevel: 63}, EnforcesSecureChat: false}
|
||||
err = s.Send(login)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketPosition{
|
||||
TeleportId: 0,
|
||||
X: 0.5,
|
||||
Y: 100,
|
||||
Z: 0.5,
|
||||
Dx: 0,
|
||||
Dy: 0,
|
||||
Dz: 0,
|
||||
Yaw: 0,
|
||||
Pitch: 0,
|
||||
Flags: v1_21_8.PlayToClientPositionUpdateRelatives{},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketUpdateViewPosition{
|
||||
ChunkX: 0,
|
||||
ChunkZ: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketGameStateChange{
|
||||
Reason: "level_chunks_load_start",
|
||||
})
|
||||
|
||||
radius := int32(10)
|
||||
|
||||
for x := -radius; x < radius; x++ {
|
||||
for z := -radius; z < radius; z++ {
|
||||
var chunkData []byte
|
||||
chunkData, err = GetChunkFromMca(x, z, c.Version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if chunkData == nil {
|
||||
continue
|
||||
}
|
||||
OLDpacket := &v1_21_8.PlayToClientPacketMapChunk{}
|
||||
OLDpacket.X = x
|
||||
OLDpacket.Z = z
|
||||
OLDpacket.ChunkData = v1_21_8.ByteArray{Val: chunkData}
|
||||
OLDpacket.BlockLight = FullLight
|
||||
OLDpacket.BlockLightMask = []int64{(1 << 24) - 1}
|
||||
err = s.Send(OLDpacket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Timer = &event.Timer{}
|
||||
s.Timer.Every(time.Second*15, s.SendKeepAlive)
|
||||
s.Timer.Start(c.Loop)
|
||||
|
||||
return s.StartConn()
|
||||
}
|
||||
func (s *Server) SendKeepAlive() (err error) {
|
||||
err = s.Send(&v1_21_8.PlayToClientPacketKeepAlive{KeepAliveId: rand.Int64()})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) error {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
return server.StartServerWithOnConn(":25566", Limbo)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/mc/client"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
var cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
||||
var addrFlag = cmd.String("addr", "localhost:25565", "address to connect to")
|
||||
var srvFlag = cmd.Bool("srv", false, "use SRV records to resolve the address")
|
||||
var versionFlag = cmd.String("version", "1.21.8", "version to use")
|
||||
|
||||
type StatusClient struct {
|
||||
*proto.Conn
|
||||
Status string
|
||||
PingSendTime, PingReceiveTime time.Time
|
||||
DoPingRoundTripTime bool
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
||||
s.Status = p.Response
|
||||
s.PingSendTime = time.Now()
|
||||
if s.DoPingRoundTripTime {
|
||||
err = s.Send(&v1_21_8.StatusToServerPacketPing{Time: s.PingSendTime.UnixMilli()})
|
||||
} else {
|
||||
err = event.HandlerDoneErr{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
|
||||
s.PingReceiveTime = time.Now()
|
||||
err = event.HandlerDoneErr{}
|
||||
return
|
||||
}
|
||||
|
||||
// StatusRaw returns a StatusClient that is not connected to a server.
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
|
||||
// It does not resolve SRV records.
|
||||
func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
c, err := client.ConnectVersionLess(nil, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
err = c.SetVersion(*versionFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s = &StatusClient{
|
||||
Conn: c,
|
||||
}
|
||||
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||
|
||||
p, err := client.MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.Send(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.SetState(proto_base.Status)
|
||||
err = s.Send(&v1_21_8.StatusToServerPacketPingStart{})
|
||||
|
||||
err = s.StartConn()
|
||||
return
|
||||
}
|
||||
|
||||
func Run(args []string) (err error) {
|
||||
err = cmd.Parse(args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if *srvFlag {
|
||||
*addrFlag, _, _, err = client.DoDNSChecked(nil, *addrFlag, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
slog.Debug("resolved address to ", "addr", *addrFlag)
|
||||
|
||||
status, err := StatusRaw(*addrFlag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(status.Status)
|
||||
return
|
||||
}
|
||||
Submodule data/minecraft-data deleted from 3e12d39f67
@@ -1,20 +0,0 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/admin-else/strom/nbt"
|
||||
)
|
||||
|
||||
//go:embed registry/*
|
||||
var Registry embed.FS
|
||||
|
||||
func LoadRegistry(name string) (n *nbt.Tag, err error) {
|
||||
r, err := Registry.Open("registry/" + name + ".nbt")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
n, err = nbt.ReadFile(r)
|
||||
return
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
// Package event is the event handler
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
LoopsPerSecond = 100 // i dunno seems reasonable
|
||||
TimePerLoop = time.Second / LoopsPerSecond
|
||||
)
|
||||
|
||||
type Loop struct {
|
||||
HandlerFunctions map[reflect.Type][]reflect.Value
|
||||
|
||||
Ctx context.Context
|
||||
ErrChan chan error
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type Unhandled struct {
|
||||
Val any
|
||||
}
|
||||
|
||||
type Anything struct {
|
||||
Val any
|
||||
}
|
||||
|
||||
type (
|
||||
Tick struct{}
|
||||
)
|
||||
|
||||
// HandlerDoneErr will stop the event loop and return the error if provided.
|
||||
type HandlerDoneErr struct {
|
||||
Return error
|
||||
}
|
||||
|
||||
// Close is fired when the loop shuts down.
|
||||
type Close struct {
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e HandlerDoneErr) Error() string {
|
||||
return "HandlerDoneErr"
|
||||
}
|
||||
|
||||
func (e HandlerDoneErr) Unwrap() error {
|
||||
return e.Return
|
||||
}
|
||||
|
||||
// ErrDontForward will stop the event from being forwarded to other handlers, including anything handlers.
|
||||
var ErrDontForward = errors.New("dont forward")
|
||||
var WhileClosingErr = errors.New("error while closing")
|
||||
|
||||
func ValidateHandler(h any) (eventType reflect.Type, hv reflect.Value) {
|
||||
hv = reflect.ValueOf(h)
|
||||
if hv.Kind() != reflect.Func {
|
||||
panic("expected method")
|
||||
}
|
||||
if hv.Type().NumIn() != 1 {
|
||||
panic("expected method with one argument")
|
||||
}
|
||||
if hv.Type().NumOut() != 1 || hv.Type().Out(0) != reflect.TypeFor[error]() {
|
||||
panic("expected method that returns an error")
|
||||
}
|
||||
eventType = hv.Type().In(0)
|
||||
return
|
||||
}
|
||||
|
||||
// Register registers a handler that will not close the loop if it returns an error.
|
||||
// Instead, it will log it to slog.Error
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) Register(h any) {
|
||||
l.RegisterCustomType(ValidateHandler(h))
|
||||
}
|
||||
|
||||
// RegisterCustomType registers a handler that will not close the loop if it returns an error.
|
||||
// Instead, it will log it to slog.Error
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) RegisterCustomType(eventType reflect.Type, hv reflect.Value) {
|
||||
handleFunc := func(packet any) error {
|
||||
v := hv.Call([]reflect.Value{reflect.ValueOf(packet)})[0]
|
||||
if !v.IsNil() {
|
||||
slog.Error("handler failed", "err", v.Interface().(error))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
l.RegisterDirect(eventType, reflect.ValueOf(handleFunc))
|
||||
}
|
||||
|
||||
// RegisterCritical registers a handler that will close the loop if it returns an error.
|
||||
// Warning: If a bad handler is passed in the program will panic.
|
||||
func (l *Loop) RegisterCritical(h any) {
|
||||
eventType, hv := ValidateHandler(h)
|
||||
l.RegisterDirect(eventType, hv)
|
||||
}
|
||||
|
||||
func (l *Loop) RegisterDirect(k reflect.Type, h reflect.Value) {
|
||||
l.HandlerFunctions[k] = append(l.HandlerFunctions[k], h)
|
||||
}
|
||||
|
||||
// FireFound triggers all handlers registered for the specified event type and returns if handlers were found or an error occurred.
|
||||
func (l *Loop) FireFound(event any) (found bool, err error) {
|
||||
var handlers []reflect.Value
|
||||
if handlers, found = l.HandlerFunctions[reflect.TypeOf(event)]; found {
|
||||
for _, handler := range handlers {
|
||||
errV := handler.Call([]reflect.Value{reflect.ValueOf(event)})[0]
|
||||
if !errV.IsNil() {
|
||||
err = errV.Interface().(error)
|
||||
err = errors.Join(fmt.Errorf("event handler failed at: %s(%T)", handler.Type().In(0), event), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fire dispatches the provided event to all appropriate handlers and invokes fallback handlers if the event is unhandled.
|
||||
func (l *Loop) Fire(event any) (err error) {
|
||||
found, err := l.FireFound(event)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrDontForward) {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
_, err = l.FireFound(Unhandled{event})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = l.FireFound(Anything{event})
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Loop) startLoop() (err error) {
|
||||
_ = *l // exit early on nil loop
|
||||
for {
|
||||
tickStartTime := time.Now()
|
||||
err = l.Fire(Tick{})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(TimePerLoop - time.Since(tickStartTime))
|
||||
}
|
||||
if errors.Is(err, HandlerDoneErr{}) {
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
closeErr := l.Fire(Close{err})
|
||||
if closeErr != nil {
|
||||
err = errors.Join(err, errors.Join(WhileClosingErr, closeErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Loop) Start() (err error) {
|
||||
go func() {
|
||||
l.ErrChan <- l.startLoop()
|
||||
}()
|
||||
err = <-l.ErrChan
|
||||
l.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
func NewLoop() (l *Loop) {
|
||||
l = &Loop{}
|
||||
l.ErrChan = make(chan error)
|
||||
l.Ctx, l.cancel = context.WithCancel(context.Background())
|
||||
l.HandlerFunctions = make(map[reflect.Type][]reflect.Value)
|
||||
return
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Stdin struct {
|
||||
Val string
|
||||
}
|
||||
|
||||
type StdinHandler struct {
|
||||
*Loop
|
||||
stdInChan chan Stdin
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func (s *StdinHandler) OnTick(_ Tick) (err error) {
|
||||
select {
|
||||
case stdin := <-s.stdInChan:
|
||||
err = s.Fire(Stdin{stdin.Val})
|
||||
default:
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StdinHandler) ScanJob() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
return
|
||||
default:
|
||||
s.stdInChan <- Stdin{scanner.Text()}
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *StdinHandler) OnStart() (err error) {
|
||||
s.RegisterCritical(s.OnTick)
|
||||
s.stdInChan = make(chan Stdin)
|
||||
go s.ScanJob()
|
||||
return
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IntervalTask struct {
|
||||
F func() error
|
||||
LastTrigger time.Time
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
type OnceTask struct {
|
||||
F func() error
|
||||
Trigger time.Time
|
||||
}
|
||||
|
||||
type Timer struct {
|
||||
Once []OnceTask
|
||||
Interval []IntervalTask
|
||||
Active bool
|
||||
}
|
||||
|
||||
func (t *Timer) Tick() (err error) {
|
||||
if !t.Active {
|
||||
return
|
||||
}
|
||||
for i, task := range t.Interval {
|
||||
if time.Since(task.LastTrigger) >= task.Interval {
|
||||
task.LastTrigger = time.Now()
|
||||
err = task.F()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Interval[i] = task
|
||||
}
|
||||
for i, task := range t.Once {
|
||||
if task.Trigger.After(time.Now()) {
|
||||
err = task.F()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.Once = slices.Delete(t.Once, i, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Timer) Every(interval time.Duration, f func() error) {
|
||||
t.Interval = append(t.Interval, IntervalTask{
|
||||
F: f,
|
||||
Interval: interval,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Timer) In(interval time.Duration, f func() error) {
|
||||
t.Once = append(t.Once, OnceTask{
|
||||
F: f,
|
||||
Trigger: time.Now().Add(interval),
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
module github.com/admin-else/strom
|
||||
|
||||
go 1.25
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // FIXME: this should be replaced with my version
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
@@ -2,7 +2,5 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
)
|
||||
|
||||
const (
|
||||
BlocksPerChunkSection = 4096
|
||||
BiomesPerChunkSection = 64
|
||||
)
|
||||
|
||||
func UnpackArrayPalette(r io.Reader, bitsPerEntry uint8, numberOfEntries int) (data []int32, err error) {
|
||||
palletLen, err := proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var pallet []int32 // remember we can't use make because this is user-controlled data
|
||||
for range palletLen {
|
||||
var entry int32
|
||||
entry, err = proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pallet = append(pallet, entry)
|
||||
}
|
||||
data, err = UnpackLongData(r, bitsPerEntry, numberOfEntries)
|
||||
for i, b := range data {
|
||||
data[i] = pallet[b] // maybe check bounds?
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackLongData(r io.Reader, bitsPerEntry uint8, numberOfEntries int) (data []int32, err error) {
|
||||
entriesPerLong := int(64 / bitsPerEntry)
|
||||
numberOfLongs := int(math.Ceil(float64(numberOfEntries) / float64(entriesPerLong)))
|
||||
var dataL []uint64
|
||||
for range numberOfLongs {
|
||||
var entry uint64
|
||||
err = binary.Read(r, binary.BigEndian, &entry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dataL = append(dataL, entry)
|
||||
}
|
||||
|
||||
data = LongsToData(dataL, int32(numberOfEntries), int32(bitsPerEntry))
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackSingleValuePalette(r io.Reader, numberOfEntries int) (data []int32, err error) {
|
||||
entry, err := proto_base.DecodeVarInt(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = make([]int32, numberOfEntries)
|
||||
for i := range data {
|
||||
data[i] = entry
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LongsToData(data []uint64, n, bpe int32) (ret []int32) {
|
||||
mask := (uint64(1) << uint32(bpe)) - 1
|
||||
elementsPerLong := 64 / bpe
|
||||
|
||||
ret = make([]int32, n)
|
||||
for i := range n {
|
||||
ret[i] = int32((data[i/elementsPerLong] >> ((i % elementsPerLong) * bpe)) & mask)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnpackBlockData minecraft does this at net.minecraft.world.chunk.PaletteProvider
|
||||
func UnpackBlockData(r io.Reader) (blocks []int32, err error) {
|
||||
var bitsPerEntry uint8
|
||||
err = binary.Read(r, binary.BigEndian, &bitsPerEntry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch bitsPerEntry {
|
||||
case 0:
|
||||
blocks, err = UnpackSingleValuePalette(r, BlocksPerChunkSection)
|
||||
case 1, 2, 3, 4, 5, 6, 7, 8:
|
||||
blocks, err = UnpackArrayPalette(r, bitsPerEntry, BlocksPerChunkSection)
|
||||
default:
|
||||
blocks, err = UnpackLongData(r, bitsPerEntry, BlocksPerChunkSection)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackBiomeData(r io.Reader) (biomes []int32, err error) {
|
||||
// net.minecraft.world.chunk.PaletteProvider#forBiomes
|
||||
var bitsPerEntry uint8
|
||||
err = binary.Read(r, binary.BigEndian, &bitsPerEntry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch bitsPerEntry {
|
||||
case 0:
|
||||
biomes, err = UnpackSingleValuePalette(r, BiomesPerChunkSection)
|
||||
case 1, 2, 3:
|
||||
biomes, err = UnpackArrayPalette(r, bitsPerEntry, BiomesPerChunkSection)
|
||||
default:
|
||||
biomes, err = UnpackLongData(r, bitsPerEntry, BiomesPerChunkSection)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackSection(r io.Reader) (blocks, biomes []int32, err error) {
|
||||
var blockCount int16
|
||||
err = binary.Read(r, binary.BigEndian, &blockCount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
blocks, err = UnpackBlockData(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
biomes, err = UnpackBiomeData(r)
|
||||
return
|
||||
}
|
||||
|
||||
func UnpackNSections(r io.Reader, n int) (blocks, biomes [][]int32, err error) {
|
||||
for range n {
|
||||
var blocksSection, biomesSections []int32
|
||||
blocksSection, biomesSections, err = UnpackSection(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
blocks = append(blocks, blocksSection)
|
||||
biomes = append(biomes, biomesSections)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -9,32 +9,40 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
ServicesHost string
|
||||
SessionHost string
|
||||
ProfilesHost string
|
||||
}
|
||||
|
||||
var DefaultServers = &Servers{
|
||||
ServicesHost: "https://api.minecraftservices.com",
|
||||
SessionHost: "https://sessionserver.mojang.com",
|
||||
ProfilesHost: "https://api.mojang.com",
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
*Servers
|
||||
Name string
|
||||
Uuid uuid.UUID
|
||||
Ygg string
|
||||
}
|
||||
|
||||
const (
|
||||
BaseUrlApi = "https://api.minecraftservices.com"
|
||||
)
|
||||
|
||||
func (a *Account) doMcApiRequest(method string, url string, from any, to any) (err error) {
|
||||
var b []byte
|
||||
if method != "GET" {
|
||||
b, err = json.Marshal(from)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r, err := http.NewRequest(method, url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
@@ -44,21 +52,22 @@ func (a *Account) doMcApiRequest(method string, url string, from any, to any) (e
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
|
||||
return fmt.Errorf("bad status code %v", resp.StatusCode)
|
||||
}
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, to)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
type PlayerKeys struct {
|
||||
@@ -73,7 +82,7 @@ type PlayerKeys struct {
|
||||
}
|
||||
|
||||
func (a *Account) FetchKeys() (keys PlayerKeys, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/certificates", nil, &keys)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/certificates", nil, &keys)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,10 +99,12 @@ func (a *Account) JoinServer(serverId string) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body))
|
||||
resp, err := http.Post(a.SessionHost+"/session/minecraft/join", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 204 {
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -112,17 +123,10 @@ type NameChangResp struct {
|
||||
}
|
||||
|
||||
func (a *Account) NameChangeInfo() (r NameChangResp, err error) {
|
||||
err = a.doMcApiRequest("GET", "https://api.minecraftservices.com/minecraft/profile/namechange", nil, &r)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/namechange", nil, &r)
|
||||
return
|
||||
}
|
||||
|
||||
func NewOfflineAccount(name string) *Account {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: crypto.FromOfflinePlayer(name),
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerProfile represents the player's profile information
|
||||
type PlayerProfile struct {
|
||||
ID string `json:"id"`
|
||||
@@ -143,7 +147,7 @@ type PlayerProfile struct {
|
||||
|
||||
// QueryProfile queries the player's profile
|
||||
func (a *Account) QueryProfile() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile", nil, &profile)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +185,7 @@ type PlayerAttributes struct {
|
||||
|
||||
// QueryAttributes queries the player's attributes
|
||||
func (a *Account) QueryAttributes() (attrs PlayerAttributes, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/player/attributes", nil, &attrs)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/player/attributes", nil, &attrs)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@ type ModifyAttributesRequest struct {
|
||||
|
||||
// ModifyAttributes modifies the player's attributes
|
||||
func (a *Account) ModifyAttributes(req ModifyAttributesRequest) (attrs PlayerAttributes, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/player/attributes", req, &attrs)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/player/attributes", req, &attrs)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,24 +209,23 @@ type BlockedUsers struct {
|
||||
|
||||
// GetBlocklist gets the list of blocked users
|
||||
func (a *Account) GetBlocklist() (blocklist BlockedUsers, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/privacy/blocklist", nil, &blocklist)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/privacy/blocklist", nil, &blocklist)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckNameAvailability checks if a name is available
|
||||
type NameAvailability struct {
|
||||
Status string `json:"status"` // DUPLICATE, AVAILABLE, NOT_ALLOWED
|
||||
}
|
||||
|
||||
// CheckNameAvailability checks if a name is available
|
||||
func (a *Account) CheckNameAvailability(name string) (avail NameAvailability, err error) {
|
||||
err = a.doMcApiRequest("GET", BaseUrlApi+"/minecraft/profile/name/"+name+"/available", nil, &avail)
|
||||
err = a.doMcApiRequest("GET", a.ServicesHost+"/minecraft/profile/name/"+name+"/available", nil, &avail)
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeName changes the player's name
|
||||
func (a *Account) ChangeName(newName string) (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/name/"+newName, nil, &profile)
|
||||
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/name/"+newName, nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,19 +237,19 @@ type ChangeSkinRequest struct {
|
||||
|
||||
// ChangeSkin changes the player's skin
|
||||
func (a *Account) ChangeSkin(req ChangeSkinRequest) (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("POST", BaseUrlApi+"/minecraft/profile/skins", req, &profile)
|
||||
err = a.doMcApiRequest("POST", a.ServicesHost+"/minecraft/profile/skins", req, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// ResetSkin resets the player's skin to default
|
||||
func (a *Account) ResetSkin() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/skins/active", nil, &profile)
|
||||
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/skins/active", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// HideCape hides the player's active cape
|
||||
func (a *Account) HideCape() (profile PlayerProfile, err error) {
|
||||
err = a.doMcApiRequest("DELETE", BaseUrlApi+"/minecraft/profile/capes/active", nil, &profile)
|
||||
err = a.doMcApiRequest("DELETE", a.ServicesHost+"/minecraft/profile/capes/active", nil, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,29 +261,53 @@ type ShowCapeRequest struct {
|
||||
// ShowCape shows a specific cape
|
||||
func (a *Account) ShowCape(capeID string) (profile PlayerProfile, err error) {
|
||||
req := ShowCapeRequest{CapeID: capeID}
|
||||
err = a.doMcApiRequest("PUT", BaseUrlApi+"/minecraft/profile/capes/active", req, &profile)
|
||||
err = a.doMcApiRequest("PUT", a.ServicesHost+"/minecraft/profile/capes/active", req, &profile)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckGiftCodeValidity checks if a gift code is valid
|
||||
func (a *Account) CheckGiftCodeValidity(giftCode string) (valid bool, err error) {
|
||||
url := BaseUrlApi + "/productvoucher/giftcode"
|
||||
url := a.ServicesHost + "/productvoucher/" + giftCode
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Set("Authorization", "Bearer "+a.Ygg)
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Returns HTTP 200 or 204 if valid, 404 if invalid
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 204 {
|
||||
return true, nil
|
||||
valid = true
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
)
|
||||
|
||||
func NewOfflineAccount(name string) *Account {
|
||||
return &Account{
|
||||
Name: name,
|
||||
Uuid: crypto.FromOfflinePlayer(name),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,19 @@
|
||||
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})
|
||||
}
|
||||
|
||||
func Start(c *proto.Conn) {
|
||||
m := &mod{Conn: c}
|
||||
m.RegisterUntilLatest(m.onKeepAlive)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/admin-else/strom/mc/level"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||
)
|
||||
|
||||
var (
|
||||
ChunkNotLoadedErr = errors.New("chunk not loaded")
|
||||
OutOfBoundsErr = errors.New("coordinates out of bounds")
|
||||
)
|
||||
|
||||
// ChunkPos identifies a chunk column.
|
||||
type ChunkPos struct {
|
||||
X, Z int32
|
||||
}
|
||||
|
||||
// World stores a shareable in-memory view of the Minecraft world.
|
||||
// Multiple bots can subscribe to the same World and update it from their
|
||||
// respective connections.
|
||||
type World struct {
|
||||
mu sync.RWMutex
|
||||
chunks map[ChunkPos]*level.Chunk
|
||||
version string
|
||||
minY int
|
||||
height int
|
||||
}
|
||||
|
||||
// NewWorld creates a new World for the given protocol version and vertical
|
||||
// bounds. For a 1.21 overworld use minY=-64 and height=384.
|
||||
func NewWorld(version string, minY, height int) *World {
|
||||
return &World{
|
||||
chunks: make(map[ChunkPos]*level.Chunk),
|
||||
version: version,
|
||||
minY: minY,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlock returns the global block state ID at the given world coordinates.
|
||||
func (w *World) GetBlock(x, y, z int32) (stateId int32, err error) {
|
||||
w.mu.RLock()
|
||||
defer w.mu.RUnlock()
|
||||
return w.getBlockLocked(x, y, z)
|
||||
}
|
||||
|
||||
func (w *World) getBlockLocked(x, y, z int32) (stateId int32, err error) {
|
||||
chunkX := floorDiv(x, level.ChunkWidth)
|
||||
chunkZ := floorDiv(z, level.ChunkWidth)
|
||||
chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}]
|
||||
if !ok {
|
||||
err = ChunkNotLoadedErr
|
||||
return
|
||||
}
|
||||
|
||||
sectionIndex := (int(y) - w.minY) / level.ChunkWidth
|
||||
if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) {
|
||||
err = OutOfBoundsErr
|
||||
return
|
||||
}
|
||||
|
||||
lx, ly, lz := blockToLocal(x, y, z)
|
||||
index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx
|
||||
return chunk.Sections[sectionIndex].Blocks.Get(index)
|
||||
}
|
||||
|
||||
// SetBlock updates the global block state ID at the given world coordinates.
|
||||
func (w *World) SetBlock(x, y, z int32, stateId int32) (err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
chunkX := floorDiv(x, level.ChunkWidth)
|
||||
chunkZ := floorDiv(z, level.ChunkWidth)
|
||||
chunk, ok := w.chunks[ChunkPos{chunkX, chunkZ}]
|
||||
if !ok {
|
||||
return ChunkNotLoadedErr
|
||||
}
|
||||
|
||||
sectionIndex := (int(y) - w.minY) / level.ChunkWidth
|
||||
if sectionIndex < 0 || sectionIndex >= len(chunk.Sections) {
|
||||
return OutOfBoundsErr
|
||||
}
|
||||
|
||||
lx, ly, lz := blockToLocal(x, y, z)
|
||||
index := ly*level.ChunkWidth*level.ChunkWidth + lz*level.ChunkWidth + lx
|
||||
return chunk.Sections[sectionIndex].Blocks.Set(index, stateId)
|
||||
}
|
||||
|
||||
// Module subscribes to chunk and block-change packets and updates a shared
|
||||
// World. Multiple Modules can point to the same World.
|
||||
type Module struct {
|
||||
*proto.Conn
|
||||
world *World
|
||||
}
|
||||
|
||||
// GetBlock returns the block state ID at the given world coordinates via the
|
||||
// shared World.
|
||||
func (m *Module) GetBlock(x, y, z int32) (int32, error) {
|
||||
return m.world.GetBlock(x, y, z)
|
||||
}
|
||||
|
||||
// Start registers packet handlers on c that update the shared World w.
|
||||
func Start(c *proto.Conn, w *World) *Module {
|
||||
m := &Module{Conn: c, world: w}
|
||||
m.RegisterUntilLatest(m.onMapChunk)
|
||||
m.RegisterUntilLatest(m.onBlockChange)
|
||||
m.RegisterUntilLatest(m.onMultiBlockChange)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Module) onMapChunk(p *v1_21_11.PlayToClientPacketMapChunk) (err error) {
|
||||
chunk, err := level.ReadChunkFromChunkPacketData(
|
||||
bytes.NewReader(p.ChunkData.Val),
|
||||
m.world.version,
|
||||
m.world.height,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.world.mu.Lock()
|
||||
m.world.chunks[ChunkPos{p.X, p.Z}] = chunk
|
||||
m.world.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) onBlockChange(p *v1_21_11.PlayToClientPacketBlockChange) (err error) {
|
||||
return m.world.SetBlock(p.Location.X, int32(p.Location.Y), p.Location.Z, p.Type)
|
||||
}
|
||||
|
||||
func (m *Module) onMultiBlockChange(p *v1_21_11.PlayToClientPacketMultiBlockChange) (err error) {
|
||||
chunkX := p.ChunkCoordinates.X
|
||||
chunkZ := p.ChunkCoordinates.Z
|
||||
sectionY := p.ChunkCoordinates.Y
|
||||
|
||||
for _, record := range p.Records {
|
||||
packedPos := record & 0xFFF
|
||||
stateId := record >> 12
|
||||
|
||||
localX := (packedPos >> 8) & 0xF
|
||||
localY := (packedPos >> 4) & 0xF
|
||||
localZ := packedPos & 0xF
|
||||
|
||||
x := chunkX*level.ChunkWidth + int32(localX)
|
||||
y := sectionY*level.ChunkWidth + int32(localY)
|
||||
z := chunkZ*level.ChunkWidth + int32(localZ)
|
||||
|
||||
if err = m.world.SetBlock(x, y, z, stateId); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func floorDiv(a, b int32) int32 {
|
||||
return (a - ((a%b + b) % b)) / b
|
||||
}
|
||||
|
||||
func blockToLocal(x, y, z int32) (lx, ly, lz int32) {
|
||||
lx = x % level.ChunkWidth
|
||||
if lx < 0 {
|
||||
lx += level.ChunkWidth
|
||||
}
|
||||
ly = y % level.ChunkWidth
|
||||
if ly < 0 {
|
||||
ly += level.ChunkWidth
|
||||
}
|
||||
lz = z % level.ChunkWidth
|
||||
if lz < 0 {
|
||||
lz += level.ChunkWidth
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
// Connect connects to the given address note that it does not resolve the SRV records it's just a raw net.Dial
|
||||
// You must close the connection when you're done with it
|
||||
// version must be a vaild minecraft version eg 1.21.11 if left empty it will be detected with a status-request.
|
||||
func Connect(ctx context.Context, addr string, version string) (ret *proto.Conn, err error) {
|
||||
var protocolVersion int32
|
||||
if version == "" {
|
||||
var s server.StatusResponse
|
||||
s, err = Status(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protocolVersion = s.Version.Protocol
|
||||
} else {
|
||||
var v data.VersionInfo
|
||||
v, err = data.LookUpVersionByName(version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protocolVersion = v.Version
|
||||
}
|
||||
ret, err = ConnectVersionLess(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ret.SetProtocolVersion(protocolVersion)
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectVersionLess connects to the given address without setting the version you should set it
|
||||
// when using this function.
|
||||
// You must close the connection when you're done with it
|
||||
func ConnectVersionLess(ctx context.Context, addr string) (ret *proto.Conn, err error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ret = proto.NewConnCtx(ctx)
|
||||
ret.Actor = proto_base.Client
|
||||
ret.Conn, err = (&net.Dialer{}).DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.R = ret.Conn
|
||||
ret.W = ret.Conn
|
||||
return
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package client
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
type ConfigIgnorer struct {
|
||||
@@ -19,8 +18,8 @@ func (c *ConfigIgnorer) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ConfigIgnorer) Default(event event.Anything) (err error) {
|
||||
slog.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", event))
|
||||
func (c *ConfigIgnorer) Default(e event.Anything) (err error) {
|
||||
c.Log.Debug("Config ignorer", "packet", fmt.Sprintf("%#v", e))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ func IgnoreConfig(c *proto.Conn) (err error) {
|
||||
ci.RegisterUntilLatest(ci.OnPing)
|
||||
ci.RegisterUntilLatest(ci.OnKeepAlive)
|
||||
|
||||
err = ci.Start()
|
||||
err = ci.StartConn()
|
||||
if err != nil {
|
||||
err = errors.Join(err, errors.New("failed to ignore config"))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DNSCheckFunc func(addr string) error
|
||||
|
||||
// DoDNSChecked resolves a DNS name to an IP address
|
||||
// It should resolve a minecraft compatible address into something net.Dial
|
||||
// It also optionally accepts a function to "check" the address
|
||||
func DoDNSChecked(ctx context.Context, in string, f DNSCheckFunc) (connectTo string, host string, portN uint16, err error) {
|
||||
if f == nil {
|
||||
f = func(addr string) error { return nil }
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// https://mcsrc.dev/1/26.1-pre-1/net/minecraft/client/multiplayer/resolver/ServerAddress#L34
|
||||
host, port, err := net.SplitHostPort(in)
|
||||
if err != nil {
|
||||
if addrErr, ok := errors.AsType[*net.AddrError](err); ok && addrErr.Err == "missing port in address" {
|
||||
err = nil
|
||||
host = in
|
||||
port = "25565"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
porti, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = f(host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
portN = uint16(porti)
|
||||
outIP, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
if portN != 25565 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second) // otherwise this takes 10+ seconds
|
||||
defer cancel()
|
||||
|
||||
r := &net.Resolver{}
|
||||
|
||||
_, srvs, _ := r.LookupSRV(ctx, "minecraft", "tcp", host)
|
||||
if len(srvs) == 0 {
|
||||
|
||||
return
|
||||
}
|
||||
host = strings.TrimSuffix(srvs[0].Target, ".")
|
||||
portN = srvs[0].Port
|
||||
outIP, err = net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = f(outIP.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
connectTo = outIP.String() + ":" + port
|
||||
return
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestDoDNS(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Logf("Testing %s", c.Input)
|
||||
_, host, portN, err := DoDns(c.Input)
|
||||
_, host, portN, err := DoDNSChecked(nil, c.Input, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
)
|
||||
|
||||
func MakeHandshakePacket(c *proto.Conn, nextState proto_base.State) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
|
||||
return MakeHandshakePacketAddr(c, nextState, c.RemoteAddr().String())
|
||||
}
|
||||
|
||||
// MakeHandshakePacketAddr gets a custom addr
|
||||
func MakeHandshakePacketAddr(s *proto.Conn, nextState proto_base.State, addr string) (p *v1_21_8.HandshakingToServerPacketSetProtocol, err error) {
|
||||
p = &v1_21_8.HandshakingToServerPacketSetProtocol{NextState: int32(nextState), ProtocolVersion: s.ProtocolVersion}
|
||||
var portStr string
|
||||
@@ -1,21 +1,20 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/admin-else/strom/api"
|
||||
"github.com/admin-else/strom/crypto"
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/text"
|
||||
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/api"
|
||||
"github.com/admin-else/strom/mc/crypto"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/text"
|
||||
)
|
||||
|
||||
type LoginClient struct {
|
||||
@@ -42,11 +41,10 @@ func (k KickedDuringLoginErr) Error() string {
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
|
||||
err = fmt.Errorf("unexpected event: %#v", event)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LoginClient) OnCycle(_ event.Tick) (err error) {
|
||||
if _, ok := event.Val.(proto_base.EncodeDecodeAble); !ok {
|
||||
return
|
||||
}
|
||||
s.Log.Warn("unexpected packet during login client", "packet", event.Val)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,18 +125,17 @@ func (s *LoginClient) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Login(c *proto.Conn, account *api.Account) (err error) {
|
||||
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
|
||||
lc := &LoginClient{
|
||||
Conn: c,
|
||||
Account: account,
|
||||
}
|
||||
lc.RegisterCritical(lc.OnDefault)
|
||||
lc.RegisterCritical(lc.OnCycle)
|
||||
lc.RegisterCritical(lc.OnClose)
|
||||
lc.RegisterUntilLatest(lc.OnCompress)
|
||||
lc.RegisterUntilLatest(lc.OnEncrypt)
|
||||
lc.RegisterUntilLatest(lc.OnDisconnect)
|
||||
lc.RegisterUntilLatest(lc.OnSuccess)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnCompress)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnEncrypt)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
|
||||
lc.RegisterCriticalUntilLatest(lc.OnSuccess)
|
||||
|
||||
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login)
|
||||
if err != nil {
|
||||
@@ -152,21 +149,62 @@ func Login(c *proto.Conn, account *api.Account) (err error) {
|
||||
lc.SetState(proto_base.Login)
|
||||
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
||||
|
||||
err = lc.Start()
|
||||
err = lc.StartConn()
|
||||
if err != nil {
|
||||
err = errors.Join(FailedToLoginErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ConnectAndLogin(connectTo string, account *api.Account) (c *proto.Conn, err error) {
|
||||
c, err = Connect(connectTo)
|
||||
func WithoutDns() func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.NoDns = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.Ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func WithVersion(version string) func(ls *loginSettings) {
|
||||
return func(ls *loginSettings) {
|
||||
ls.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
type loginSettings struct {
|
||||
NoDns bool
|
||||
Ctx context.Context
|
||||
Version string
|
||||
IgnoreConfig bool
|
||||
}
|
||||
|
||||
func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) {
|
||||
ls := &loginSettings{}
|
||||
|
||||
for _, setting := range settings {
|
||||
setting(ls)
|
||||
}
|
||||
|
||||
if !ls.NoDns {
|
||||
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c, err = Connect(ls.Ctx, connectTo, ls.Version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = Login(c, account)
|
||||
err = LoginRaw(c, account)
|
||||
if err != nil {
|
||||
err = errors.Join(FailedToLoginErr, err)
|
||||
return
|
||||
}
|
||||
if !ls.IgnoreConfig {
|
||||
err = IgnoreConfig(c)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/admin-else/strom/event"
|
||||
"github.com/admin-else/strom/proto"
|
||||
"github.com/admin-else/strom/proto_base"
|
||||
"github.com/admin-else/strom/server"
|
||||
|
||||
"github.com/admin-else/strom/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/event"
|
||||
"github.com/admin-else/strom/mc/proto"
|
||||
"github.com/admin-else/strom/mc/proto_base"
|
||||
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||
"github.com/admin-else/strom/mc/server"
|
||||
)
|
||||
|
||||
type StatusClient struct {
|
||||
@@ -19,10 +19,6 @@ type StatusClient struct {
|
||||
DoPingRoundTripTime bool
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStart() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StatusClient) OnStatus(p *v1_21_8.StatusToClientPacketServerInfo) (err error) {
|
||||
s.Status = p.Response
|
||||
s.PingSendTime = time.Now()
|
||||
@@ -41,9 +37,10 @@ func (s *StatusClient) OnPong(p *v1_21_8.StatusToClientPacketPing) (err error) {
|
||||
}
|
||||
|
||||
// StatusRaw returns a StatusClient that is not connected to a server.
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment
|
||||
func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
c, err := ConnectVersionLess(addr)
|
||||
// So ignore the resource leak warning. And maybe attach a warn ignore comment.
|
||||
// It does not resolve SRV records.
|
||||
func StatusRaw(ctx context.Context, addr string) (s *StatusClient, err error) {
|
||||
c, err := ConnectVersionLess(ctx, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -55,9 +52,8 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
s = &StatusClient{
|
||||
Conn: c,
|
||||
}
|
||||
s.Loop = event.NewLoop()
|
||||
s.RegisterUntilLatest(s.OnStatus)
|
||||
s.RegisterUntilLatest(s.OnPong)
|
||||
s.RegisterCriticalUntilLatest(s.OnStatus)
|
||||
s.RegisterCriticalUntilLatest(s.OnPong)
|
||||
|
||||
p, err := MakeHandshakePacketAddr(s.Conn, proto_base.Status, addr)
|
||||
if err != nil {
|
||||
@@ -74,11 +70,21 @@ func StatusRaw(addr string) (s *StatusClient, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Status(addr string) (status server.StatusResponse, err error) {
|
||||
s, err := StatusRaw(addr)
|
||||
// StatusNoDns is like Status but does not resolve SRV records.
|
||||
func StatusNoDns(addr string) (status server.StatusResponse, err error) {
|
||||
s, err := StatusRaw(context.Background(), addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(s.Status), &status)
|
||||
return
|
||||
}
|
||||
|
||||
// Status resolves the status of the minecraft server at the given address.
|
||||
func Status(ctx context.Context, addr string) (status server.StatusResponse, err error) {
|
||||
addr, _, _, err = DoDNSChecked(ctx, addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return StatusNoDns(addr)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ package crypto
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,7 @@ type CFB8 struct {
|
||||
ivPos int
|
||||
iv []byte
|
||||
de bool
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 {
|
||||
@@ -59,6 +61,8 @@ func newCFB8(c cipher.Block, iv []byte, de bool) *CFB8 {
|
||||
}
|
||||
|
||||
func (cf *CFB8) XORKeyStream(dst, src []byte) {
|
||||
cf.mutex.Lock()
|
||||
defer cf.mutex.Unlock()
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UUIDv3 Variation of google.uuid.NewHash
|
||||
// uuidV3 Variation of google.uuid.NewHash
|
||||
// We need this because we want a 14-byte namespace and Google forces 16
|
||||
func UUIDv3(space []byte, data []byte) uuid.UUID {
|
||||
func uuidV3(space []byte, data []byte) uuid.UUID {
|
||||
h := md5.New()
|
||||
h.Reset()
|
||||
h.Write(space) //nolint:errcheck
|
||||
@@ -22,5 +22,5 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
|
||||
}
|
||||
|
||||
func FromOfflinePlayer(displayName string) uuid.UUID {
|
||||
return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName))
|
||||
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package data
|
||||
|
||||
import "errors"
|
||||
|
||||
// [
|
||||
//
|
||||
// {
|
||||
@@ -14,7 +16,7 @@ package data
|
||||
// },
|
||||
|
||||
type Biome struct {
|
||||
Id int
|
||||
Id int32
|
||||
Name string
|
||||
Category string
|
||||
Temperature float64
|
||||
@@ -24,22 +26,35 @@ type Biome struct {
|
||||
Color int
|
||||
}
|
||||
|
||||
var BiomesCache = make(map[string][]Biome)
|
||||
var BiomesCache = make(map[string][]*Biome)
|
||||
|
||||
func BiomesForVersion(v string) (ret []Biome) {
|
||||
func BiomesForVersion(v string) (ret []*Biome) {
|
||||
var ok bool
|
||||
if ret, ok = BiomesCache[v]; ok {
|
||||
return
|
||||
}
|
||||
var b []Biome
|
||||
var b []*Biome
|
||||
must(LoadVersionedJson(v, "biomes", &b))
|
||||
BiomesCache[v] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func LookupBiomeById(version string, id int32) (biome Biome, ok bool) {
|
||||
func LookupBiomeById(version string, id int32) (biome *Biome, ok bool) {
|
||||
for _, b := range BiomesForVersion(version) {
|
||||
if b.Id == int(id) {
|
||||
if b.Id == id {
|
||||
biome = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var UnknownBiomeNameErr = errors.New("unknown biome Name")
|
||||
|
||||
func LookupBiomeByName(version string, name string) (biome *Biome, ok bool) {
|
||||
for _, b := range BiomesForVersion(version) {
|
||||
if b.Name == name {
|
||||
biome = b
|
||||
ok = true
|
||||
return
|
||||
@@ -3,7 +3,7 @@ package data_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
)
|
||||
|
||||
func TestLookupBiomeById(t *testing.T) {
|
||||
@@ -2,6 +2,10 @@ package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/admin-else/strom/mc/util"
|
||||
)
|
||||
|
||||
// {
|
||||
@@ -47,11 +51,13 @@ type Block struct {
|
||||
type BlockState struct {
|
||||
Name string
|
||||
Type string
|
||||
MinValue int32 // this is generated on load and used for state value calculations
|
||||
NumValues int32 `json:"num_values"`
|
||||
Values []string
|
||||
}
|
||||
|
||||
var BlocksCache = make(map[string][]*Block)
|
||||
var BlocksCacheIdMap = make(map[string]map[int32]*Block)
|
||||
|
||||
func BlocksForVersion(v string) (ret []*Block) {
|
||||
var ok bool
|
||||
@@ -61,26 +67,63 @@ func BlocksForVersion(v string) (ret []*Block) {
|
||||
var b []*Block
|
||||
must(LoadVersionedJson(v, "blocks", &b))
|
||||
BlocksCache[v] = b
|
||||
for _, block := range b {
|
||||
for i, state := range block.States {
|
||||
if state.Type != "int" {
|
||||
continue
|
||||
}
|
||||
values := make([]int32, state.NumValues)
|
||||
for j, stateValue := range state.Values {
|
||||
n := util.MustT(strconv.Atoi(stateValue))
|
||||
values[j] = int32(n)
|
||||
}
|
||||
block.States[i].MinValue = slices.Min(values)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
func LookupBlockByName(version string, name string) (block *Block, ok bool) {
|
||||
for _, b := range BlocksForVersion(version) {
|
||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
||||
if b.Name == name {
|
||||
block = b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LookupBlockByName(version string, stateId int32) (block *Block, ok bool) {
|
||||
for _, b := range BlocksForVersion(version) {
|
||||
if b.MinStateId <= stateId && stateId <= b.MaxStateId {
|
||||
block = b
|
||||
ok = true
|
||||
}
|
||||
func StateIdFromBlocKAndStateMap(version string, name string, stateMap map[string]string) (stateId int32, err error) {
|
||||
b, ok := LookupBlockByName(version, name)
|
||||
if !ok {
|
||||
err = BlockNotFoundErr
|
||||
return
|
||||
}
|
||||
if stateMap == nil {
|
||||
stateId = b.DefaultState
|
||||
} else {
|
||||
stateId, err = b.IdFromStateMap(stateMap)
|
||||
}
|
||||
if b.MinStateId > stateId || b.MaxStateId < stateId {
|
||||
err = BlockStateOutOfRangeErr
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -92,7 +135,7 @@ var BadBlockStateTypeErr = errors.New("bad block state type")
|
||||
var BadEnumErr = errors.New("bad enum")
|
||||
var SupplyAllStatesErr = errors.New("supply all states")
|
||||
|
||||
func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]any, err error) {
|
||||
func FromBlockState(version string, stateId int32) (b *Block, stateData map[string]string, err error) {
|
||||
b, ok := LookupBlockByStateId(version, stateId)
|
||||
if !ok {
|
||||
err = BlockNotFoundErr
|
||||
@@ -101,19 +144,17 @@ func FromBlockState(version string, stateId int32) (b *Block, stateData map[stri
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: mc stores these as strings so we will do the same
|
||||
|
||||
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
||||
func mcDataBlockStateTypeParse(n int32, s *BlockState) (v string, err error) {
|
||||
switch s.Type {
|
||||
case "int":
|
||||
v = n
|
||||
v = strconv.Itoa(int(n))
|
||||
case "enum":
|
||||
v = s.Values[n]
|
||||
case "bool":
|
||||
if n == 0 { // MOJANK: 0 is true, 1 is false
|
||||
v = true
|
||||
v = "true"
|
||||
} else if n == 1 {
|
||||
v = false
|
||||
v = "false"
|
||||
} else {
|
||||
err = InvalidBoolValueErr
|
||||
}
|
||||
@@ -123,35 +164,32 @@ func mcDataBlockStateTypeParse(n int32, s *BlockState) (v any, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func mcDataBlockStateTypeSerialize(v any, s *BlockState) (n int32, err error) {
|
||||
switch t := v.(type) {
|
||||
case int32:
|
||||
n = t
|
||||
case bool:
|
||||
if t {
|
||||
n = 0
|
||||
} else {
|
||||
n = 1
|
||||
}
|
||||
case string:
|
||||
for i, enumValue := range s.Values {
|
||||
if enumValue == t {
|
||||
return int32(i), nil
|
||||
}
|
||||
}
|
||||
err = BadEnumErr
|
||||
default:
|
||||
err = BadBlockStateTypeErr
|
||||
func mcDataBlockStateTypeSerialize(v string, s *BlockState) (n int32, err error) {
|
||||
if v == "true" {
|
||||
return 0, nil
|
||||
}
|
||||
if v == "false" {
|
||||
return 1, nil
|
||||
}
|
||||
n1, errMabye := strconv.Atoi(v)
|
||||
if errMabye == nil {
|
||||
return int32(n1) - s.MinValue, nil
|
||||
}
|
||||
for i, enumValue := range s.Values {
|
||||
if enumValue == v {
|
||||
return int32(i), nil
|
||||
}
|
||||
}
|
||||
err = BadEnumErr
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Block) StateMapFromId(stateId int32) (ret map[string]any, err error) {
|
||||
func (b *Block) StateMapFromId(stateId int32) (ret map[string]string, err error) {
|
||||
if stateId < b.MinStateId || stateId > b.MaxStateId {
|
||||
err = BlockStateOutOfRangeErr
|
||||
return
|
||||
}
|
||||
ret = make(map[string]any)
|
||||
ret = make(map[string]string)
|
||||
stateId -= b.MinStateId
|
||||
|
||||
a := int32(1)
|
||||
@@ -166,7 +204,7 @@ func (b *Block) StateMapFromId(stateId int32) (ret map[string]any, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Block) IdFromStateMap(m map[string]any) (ret int32, err error) {
|
||||
func (b *Block) IdFromStateMap(m map[string]string) (ret int32, err error) {
|
||||
a := int32(1)
|
||||
for i := range len(b.States) {
|
||||
s := b.States[len(b.States)-1-i]
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/admin-else/strom/data"
|
||||
"github.com/admin-else/strom/mc/data"
|
||||
)
|
||||
|
||||
func TestLookupBlockByStateId(t *testing.T) {
|
||||
@@ -32,3 +32,11 @@ func TestBlockStates(t *testing.T) {
|
||||
t.Fatal("state id mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockStateOutOfRangeBug(t *testing.T) {
|
||||
id, err := data.StateIdFromBlocKAndStateMap("1.21.11", "wildflowers", map[string]string{"facing": "east", "flower_amount": "4"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("State id: %d", id)
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:embed minecraft-data/data
|
||||
//go:generate ./download_minecraft_data.sh
|
||||
|
||||
//go:embed minecraft-data/pc minecraft-data/dataPaths.json
|
||||
var MinecraftData embed.FS
|
||||
|
||||
func LoadJson(path string, data any) (err error) {
|
||||
@@ -19,9 +22,10 @@ func LoadJson(path string, data any) (err error) {
|
||||
}
|
||||
|
||||
func LoadVersionedJson(version, dataName string, data any) (err error) {
|
||||
b, err := MinecraftData.ReadFile("minecraft-data/data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
|
||||
b, err := MinecraftData.ReadFile("minecraft-data/" + Paths.Data[version][dataName] + "/" + dataName + ".json")
|
||||
if err != nil {
|
||||
return
|
||||
// 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
|
||||
@@ -9,5 +9,5 @@ var Paths struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
must(LoadJson("minecraft-data/data/dataPaths.json", &Paths))
|
||||
must(LoadJson("minecraft-data/dataPaths.json", &Paths))
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/sh
|
||||
set -xeuo pipefail
|
||||
|
||||
# Go get / install does not clone git submodules so we have to do it like this
|
||||
|
||||
rm -rf minecraft-data/
|
||||
git clone --depth 1 git@github.com:PrismarineJS/minecraft-data.git
|
||||
mv minecraft-data minecraft-data-untrimmed
|
||||
|
||||
mkdir minecraft-data
|
||||
mkdir minecraft-data/pc
|
||||
|
||||
mv minecraft-data-untrimmed/data/dataPaths.json minecraft-data
|
||||
|
||||
copy-mc() {
|
||||
mv minecraft-data-untrimmed/data/pc/$1 minecraft-data/pc
|
||||
}
|
||||
|
||||
copy-mc common
|
||||
|
||||
copy-mc 1.21.8
|
||||
copy-mc 1.21.9
|
||||
copy-mc 1.21.11
|
||||
|
||||
rm -rf minecraft-data-untrimmed
|
||||
@@ -1,5 +1,7 @@
|
||||
package data
|
||||
|
||||
import "sync"
|
||||
|
||||
// {
|
||||
// "id": 2,
|
||||
// "name": "granite",
|
||||
@@ -15,15 +17,21 @@ type Item struct {
|
||||
}
|
||||
|
||||
var ItemsCache = make(map[string][]*Item)
|
||||
var ItemsCacheMutex = &sync.RWMutex{}
|
||||
|
||||
func ItemsForVersion(v string) (ret []*Item) {
|
||||
var ok bool
|
||||
ItemsCacheMutex.RLock()
|
||||
if ret, ok = ItemsCache[v]; ok {
|
||||
ItemsCacheMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
ItemsCacheMutex.RUnlock()
|
||||
var b []*Item
|
||||
must(LoadVersionedJson(v, "items", &b))
|
||||
ItemsCacheMutex.Lock()
|
||||
ItemsCache[v] = b
|
||||
ItemsCacheMutex.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
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
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user