rewrite skins to output skin packs

This commit is contained in:
olebeck 2023-02-08 19:08:26 +01:00
parent 2251d7096d
commit 55916e4e55
6 changed files with 393 additions and 225 deletions

149
subcommands/skins/skin.go Normal file
View File

@ -0,0 +1,149 @@
package skins
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"image"
"image/png"
"os"
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
type Skin struct {
*protocol.Skin
}
type SkinGeometry struct {
Texturewidth int `json:"texturewidth"`
Textureheight int `json:"textureheight"`
VisibleBoundsWidth float64 `json:"visible_bounds_width"`
VisibleBoundsHeight float64 `json:"visible_bounds_height"`
VisibleBoundsOffset []float64 `json:"visible_bounds_offset"`
Bones []any `json:"bones"`
}
func (skin *Skin) Hash() uuid.UUID {
h := sha256.New()
h.Write(skin.SkinData)
h.Write(skin.SkinGeometry)
h.Write(skin.CapeData)
h.Write([]byte(skin.SkinID))
return uuid.NewSHA1(uuid.NameSpaceURL, h.Sum(nil))
}
func (skin *Skin) getGeometry() (*SkinGeometry, string, error) {
if !skin.HaveGeometry() {
return nil, "", errors.New("no geometry")
}
var data map[string]any
if err := json.Unmarshal(skin.SkinGeometry, &data); err != nil {
return nil, "", err
}
arr, ok := data["minecraft:geometry"].([]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
geom, ok := arr[0].(map[string]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
desc, ok := geom["description"].(map[string]any)
if !ok {
return nil, "", errors.New("invalid geometry")
}
visibleOffset, _ := desc["visible_bounds_offset"].([]float64)
return &SkinGeometry{
Texturewidth: int(desc["texture_width"].(float64)),
Textureheight: int(desc["texture_height"].(float64)),
VisibleBoundsWidth: desc["visible_bounds_width"].(float64),
VisibleBoundsHeight: desc["visible_bounds_height"].(float64),
VisibleBoundsOffset: visibleOffset,
Bones: geom["bones"].([]any),
}, desc["identifier"].(string), nil
}
// WriteCape writes the cape as a png at output_path
func (skin *Skin) WriteCapePng(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Path": output_path, "Err": err}))
}
defer f.Close()
cape_tex := image.NewRGBA(image.Rect(0, 0, int(skin.CapeImageWidth), int(skin.CapeImageHeight)))
cape_tex.Pix = skin.CapeData
if err := png.Encode(f, cape_tex); err != nil {
return fmt.Errorf(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Err": err}))
}
return nil
}
// WriteTexture writes the main texture for this skin to a file
func (skin *Skin) writeSkinTexturePng(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
}
defer f.Close()
skin_tex := image.NewRGBA(image.Rect(0, 0, int(skin.SkinImageWidth), int(skin.SkinImageHeight)))
skin_tex.Pix = skin.SkinData
if err := png.Encode(f, skin_tex); err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Texture", "Path": output_path, "Err": err}))
}
return nil
}
func (skin *Skin) writeMetadataJson(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
}
defer f.Close()
d, err := json.MarshalIndent(SkinMeta{
skin.SkinID,
skin.PlayFabID,
skin.PremiumSkin,
skin.PersonaSkin,
skin.CapeID,
skin.SkinColour,
skin.ArmSize,
skin.Trusted,
skin.PersonaPieces,
}, "", " ")
if err != nil {
return err
}
f.Write(d)
return nil
}
func (skin *Skin) HaveGeometry() bool {
return len(skin.SkinGeometry) > 0
}
func (skin *Skin) HaveCape() bool {
return len(skin.CapeData) > 0
}
func (skin *Skin) HaveAnimations() bool {
return len(skin.Animations) > 0
}
func (skin *Skin) HaveTint() bool {
return len(skin.PieceTintColours) > 0
}
func (skin *Skin) Complex() bool {
return skin.HaveGeometry() || skin.HaveCape() || skin.HaveAnimations() || skin.HaveTint()
}

View File

@ -0,0 +1,140 @@
package skins
import (
"encoding/json"
"fmt"
"os"
"path"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sirupsen/logrus"
)
type _skinWithIndex struct {
i int
skin *Skin
}
func (s _skinWithIndex) Name(name string) string {
return fmt.Sprintf("%s-%d", name, s.i)
}
type SkinPack struct {
skins map[uuid.UUID]_skinWithIndex
Name string
}
type skinEntry struct {
LocalizationName string `json:"localization_name"`
Geometry string `json:"geometry"`
Texture string `json:"texture"`
Type string `json:"type"`
}
func NewSkinPack(name string) *SkinPack {
return &SkinPack{
skins: make(map[uuid.UUID]_skinWithIndex),
Name: name,
}
}
func (s *SkinPack) AddSkin(skin *Skin) bool {
sh := skin.Hash()
if _, ok := s.skins[sh]; !ok {
s.skins[sh] = _skinWithIndex{len(s.skins) + 1, skin}
return true
}
return false
}
func (s *SkinPack) Save(fpath, serverName string) error {
os.MkdirAll(fpath, 0o755)
var skinsJson []skinEntry
geometryJson := map[string]SkinGeometry{}
for _, s2 := range s.skins { // write skin texture
skinName := s2.Name(s.Name)
if err := s2.skin.writeSkinTexturePng(path.Join(fpath, skinName+".png")); err != nil {
return err
}
if err := s2.skin.writeMetadataJson(path.Join(fpath, skinName+"_metadata.json")); err != nil {
return err
}
if s2.skin.HaveCape() {
if err := s2.skin.WriteCapePng(path.Join(fpath, skinName+"_cape.png")); err != nil {
return err
}
}
entry := skinEntry{LocalizationName: skinName, Texture: skinName, Type: "free"}
if s2.skin.ArmSize == "wide" {
entry.Geometry = "minecraft.geometry.steve"
} else {
entry.Geometry = "minecraft.geometry.alex"
}
if s2.skin.HaveGeometry() {
geometry, geometryName, err := s2.skin.getGeometry()
if err != nil {
logrus.Warnf("failed to decode geometry %s", skinName)
} else {
geometryJson[geometryName] = *geometry
entry.Geometry = geometryName
}
}
skinsJson = append(skinsJson, entry)
}
if len(geometryJson) > 0 { // geometry.json
f, err := os.Create(path.Join(fpath, "geometry.json"))
if err != nil {
return err
}
if err := json.NewEncoder(f).Encode(geometryJson); err != nil {
return err
}
}
{ // skins.json
f, err := os.Create(path.Join(fpath, "skins.json"))
if err != nil {
return err
}
if err := json.NewEncoder(f).Encode(skinsJson); err != nil {
return err
}
}
{ // manifest.json
manifest := resource.Manifest{
FormatVersion: 2,
Header: resource.Header{
Name: s.Name,
Description: serverName + " " + s.Name,
UUID: uuid.NewString(),
Version: [3]int{1, 0, 0},
MinimumGameVersion: [3]int{1, 17, 0},
},
Modules: []resource.Module{
{
UUID: uuid.NewString(),
Description: s.Name + " Skinpack",
Type: "skin_pack",
Version: [3]int{1, 0, 0},
},
},
}
if err := utils.WriteManifest(&manifest, fpath); err != nil {
return err
}
}
return nil
}

View File

@ -40,21 +40,27 @@ func (c *SkinProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf
logrus.Error(err)
return 1
}
out_path := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(out_path, 0o755)
proxy := utils.NewProxy()
s := NewSkinsSession(proxy, hostname)
s.OnlyIfHasGeometry = c.only_with_geometry
s.PlayerNameFilter = c.filter
proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
if !toServer {
process_packet_skins(proxy.Client, out_path, pk, c.filter, c.only_with_geometry)
s.ProcessPacket(pk)
}
return pk, nil
}
if err := proxy.Run(ctx, address); err != nil {
logrus.Error(err)
return 1
}
outPathBase := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(outPathBase, 0o755)
s.Save(outPathBase)
return 0
}

View File

@ -1,15 +1,9 @@
package skins
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"image"
"image/png"
"io"
"os"
"path"
"strings"
@ -18,18 +12,13 @@ import (
"github.com/bedrock-tool/bedrocktool/locale"
"github.com/bedrock-tool/bedrocktool/utils"
"github.com/flytam/filenamify"
"github.com/google/subcommands"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sirupsen/logrus"
)
type Skin struct {
protocol.Skin
}
type SkinMeta struct {
SkinID string
PlayFabID string
@ -42,227 +31,104 @@ type SkinMeta struct {
PersonaPieces []protocol.PersonaPiece
}
// WriteGeometry writes the geometry json for the skin to output_path
func (skin *Skin) WriteGeometry(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Geometry", "Path": output_path, "Err": err}))
}
defer f.Close()
io.Copy(f, bytes.NewReader(skin.SkinGeometry))
return nil
type skinsSession struct {
PlayerNameFilter string
OnlyIfHasGeometry bool
ServerName string
Proxy *utils.ProxyContext
playerSkinPacks map[uuid.UUID]*SkinPack
playerNames map[uuid.UUID]string
}
// WriteCape writes the cape as a png at output_path
func (skin *Skin) WriteCape(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Path": output_path, "Err": err}))
}
defer f.Close()
cape_tex := image.NewRGBA(image.Rect(0, 0, int(skin.CapeImageWidth), int(skin.CapeImageHeight)))
cape_tex.Pix = skin.CapeData
func NewSkinsSession(proxy *utils.ProxyContext, serverName string) *skinsSession {
return &skinsSession{
ServerName: serverName,
Proxy: proxy,
if err := png.Encode(f, cape_tex); err != nil {
return fmt.Errorf(locale.Loc("failed_write", locale.Strmap{"Part": "Cape", "Err": err}))
playerSkinPacks: make(map[uuid.UUID]*SkinPack),
playerNames: make(map[uuid.UUID]string),
}
return nil
}
// WriteAnimations writes skin animations to the folder
func (skin *Skin) WriteAnimations(output_path string) error {
logrus.Warnf("%s has animations (unimplemented)", output_path)
return nil
}
// WriteTexture writes the main texture for this skin to a file
func (skin *Skin) WriteTexture(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
func (s *skinsSession) AddPlayerSkin(playerID uuid.UUID, playerName string, skin *Skin) {
p, ok := s.playerSkinPacks[playerID]
if !ok {
creating := fmt.Sprintf("Creating Skinpack for %s", playerName)
s.Proxy.SendPopup(creating)
logrus.Info(creating)
p = NewSkinPack(playerName)
s.playerSkinPacks[playerID] = p
}
defer f.Close()
skin_tex := image.NewRGBA(image.Rect(0, 0, int(skin.SkinImageWidth), int(skin.SkinImageHeight)))
skin_tex.Pix = skin.SkinData
if err := png.Encode(f, skin_tex); err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Texture", "Path": output_path, "Err": err}))
}
return nil
}
func (skin *Skin) WriteTint(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Tint", "Path": output_path, "Err": err}))
}
defer f.Close()
err = json.NewEncoder(f).Encode(skin.PieceTintColours)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Tint", "Path": output_path, "Err": err}))
}
return nil
}
func (skin *Skin) WriteMeta(output_path string) error {
f, err := os.Create(output_path)
if err != nil {
return errors.New(locale.Loc("failed_write", locale.Strmap{"Part": "Meta", "Path": output_path, "Err": err}))
}
defer f.Close()
d, err := json.MarshalIndent(SkinMeta{
skin.SkinID,
skin.PlayFabID,
skin.PremiumSkin,
skin.PersonaSkin,
skin.CapeID,
skin.SkinColour,
skin.ArmSize,
skin.Trusted,
skin.PersonaPieces,
}, "", " ")
if err != nil {
return err
}
f.Write(d)
return nil
}
func (skin *Skin) Complex() bool {
have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0
return have_geometry || have_cape || have_animations || have_tint
}
// Write writes all data for this skin to a folder
func (skin *Skin) Write(output_path, name string) error {
name, _ = filenamify.FilenamifyV2(name)
skin_dir := path.Join(output_path, name)
have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0
os.MkdirAll(skin_dir, 0o755)
if have_geometry {
if err := skin.WriteGeometry(path.Join(skin_dir, "geometry.json")); err != nil {
return err
if p.AddSkin(skin) {
if ok {
added := fmt.Sprintf("Added a skin to %s", playerName)
s.Proxy.SendPopup(added)
logrus.Info(added)
}
}
if have_cape {
if err := skin.WriteCape(path.Join(skin_dir, "cape.png")); err != nil {
return err
}
}
if have_animations {
if err := skin.WriteAnimations(skin_dir); err != nil {
return err
}
}
if have_tint {
if err := skin.WriteTint(path.Join(skin_dir, "tint.json")); err != nil {
return err
}
}
if err := skin.WriteMeta(path.Join(skin_dir, "metadata.json")); err != nil {
return err
}
return skin.WriteTexture(skin_dir + "/skin.png")
}
// puts the skin at output_path if the filter matches it
// internally converts the struct so it can use the extra methods
func write_skin(output_path, name string, skin *protocol.Skin) {
logrus.Infof("Writing skin for %s", name)
_skin := &Skin{*skin}
if err := _skin.Write(output_path, name); err != nil {
logrus.Errorf("Error writing skin: %s", err)
}
}
var (
skin_players = make(map[string]string)
skin_player_counts = make(map[string]int)
)
func popup_skin_saved(conn *minecraft.Conn, name string) {
if conn != nil {
(&utils.ProxyContext{Client: conn}).SendPopup(fmt.Sprintf("%s Skin was Saved", name))
}
}
func skin_meta_get_skinid(path string) string {
cont, err := os.ReadFile(fmt.Sprintf("%s/metadata.json", path))
if err != nil {
return ""
}
var meta SkinMeta
if err := json.Unmarshal(cont, &meta); err != nil {
return ""
}
return meta.SkinID
}
func save_player_skin(conn *minecraft.Conn, out_path, player_name string, skin *protocol.Skin) {
count := skin_player_counts[player_name]
if count > 0 {
meta_id := skin_meta_get_skinid(fmt.Sprintf("%s/%s_%d", out_path, player_name, count-1))
if meta_id == skin.SkinID {
return // skin same as before
}
}
skin_player_counts[player_name]++
count++
write_skin(out_path, fmt.Sprintf("%s_%d", player_name, count), skin)
popup_skin_saved(conn, player_name)
}
func process_packet_skins(conn *minecraft.Conn, out_path string, pk packet.Packet, filter string, only_if_geom bool) {
switch _pk := pk.(type) {
func (s *skinsSession) ProcessPacket(pk packet.Packet) {
switch pk := pk.(type) {
case *packet.PlayerSkin:
player_name := skin_players[_pk.UUID.String()]
if player_name == "" {
player_name = _pk.UUID.String()
playerName := s.playerNames[pk.UUID]
if playerName == "" {
playerName = pk.UUID.String()
}
if !strings.HasPrefix(player_name, filter) {
return
}
if only_if_geom && len(_pk.Skin.SkinGeometry) == 0 {
if !strings.HasPrefix(playerName, s.PlayerNameFilter) {
return
}
save_player_skin(conn, out_path, player_name, &_pk.Skin)
case *packet.PlayerList:
if _pk.ActionType == 1 { // remove
skin := Skin{&pk.Skin}
if s.OnlyIfHasGeometry && !skin.HaveGeometry() {
return
}
for _, player := range _pk.Entries {
player_name := utils.CleanupName(player.Username)
if player_name == "" {
player_name = player.UUID.String()
s.AddPlayerSkin(pk.UUID, playerName, &skin)
case *packet.PlayerList:
if pk.ActionType == 1 { // remove
return
}
for _, player := range pk.Entries {
playerName := utils.CleanupName(player.Username)
if playerName == "" {
playerName = player.UUID.String()
}
if !strings.HasPrefix(player_name, filter) {
if !strings.HasPrefix(playerName, s.PlayerNameFilter) {
return
}
if only_if_geom && len(player.Skin.SkinGeometry) == 0 {
s.playerNames[player.UUID] = playerName
skin := Skin{&player.Skin}
if s.OnlyIfHasGeometry && !skin.HaveGeometry() {
return
}
skin_players[player.UUID.String()] = player_name
save_player_skin(conn, out_path, player_name, &player.Skin)
s.AddPlayerSkin(player.UUID, playerName, &skin)
}
}
}
func (s *skinsSession) Save(fpath string) error {
logrus.Infof("Saving %d players", len(s.playerSkinPacks))
for id, sp := range s.playerSkinPacks {
err := sp.Save(path.Join(fpath, s.playerNames[id]))
if err != nil {
logrus.Warn(err)
}
}
return nil
}
type SkinCMD struct {
server_address string
filter string
serverAddress string
filter string
}
func (*SkinCMD) Name() string { return "skins" }
func (*SkinCMD) Synopsis() string { return locale.Loc("skins_synopsis", nil) }
func (c *SkinCMD) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.server_address, "address", "", locale.Loc("remote_address", nil))
f.StringVar(&c.serverAddress, "address", "", locale.Loc("remote_address", nil))
f.StringVar(&c.filter, "filter", "", locale.Loc("name_prefix", nil))
}
@ -271,35 +137,35 @@ func (c *SkinCMD) Usage() string {
}
func (c *SkinCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
address, hostname, err := utils.ServerInput(ctx, c.server_address)
address, hostname, err := utils.ServerInput(ctx, c.serverAddress)
if err != nil {
logrus.Error(err)
return 1
}
out_path := fmt.Sprintf("skins/%s", hostname)
p := utils.NewProxy()
p.WithClient = false
p.ConnectCB = func(proxy *utils.ProxyContext) {
logrus.Info(locale.Loc("connected", nil))
proxy := utils.NewProxy()
proxy.WithClient = false
proxy.ConnectCB = func(proxy *utils.ProxyContext) {
logrus.Info(locale.Loc("ctrl_c_to_exit", nil))
os.MkdirAll(out_path, 0o755)
}
p.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
s := NewSkinsSession(proxy, hostname)
proxy.PacketCB = func(pk packet.Packet, _ *utils.ProxyContext, toServer bool, _ time.Time) (packet.Packet, error) {
if !toServer {
process_packet_skins(nil, out_path, pk, c.filter, false)
s.ProcessPacket(pk)
}
return pk, nil
}
err = p.Run(ctx, address)
err = proxy.Run(ctx, address)
if err != nil {
logrus.Error(err)
}
outPathBase := fmt.Sprintf("skins/%s", hostname)
os.MkdirAll(outPathBase, 0o755)
s.Save(outPathBase)
return 0
}

View File

@ -89,16 +89,8 @@ func ns_name_split(identifier string) (ns, name string) {
}
func (bp *BehaviourPack) Save(fpath string) error {
{ // write manifest
w, err := os.Create(path.Join(fpath, "manifest.json"))
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
if err = e.Encode(bp.Manifest); err != nil {
return err
}
if err := utils.WriteManifest(bp.Manifest, fpath); err != nil {
return err
}
_add_thing := func(base, identifier string, thing any) error {

View File

@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"net"
"os"
"path"
"regexp"
"strings"
@ -22,6 +23,7 @@ import (
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
)
var (
@ -159,3 +161,16 @@ func RandSeededUUID(str string) string {
id, _ := uuid.NewRandomFromReader(bytes.NewBuffer(h[:]))
return id.String()
}
func WriteManifest(manifest *resource.Manifest, fpath string) error {
w, err := os.Create(path.Join(fpath, "manifest.json"))
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
if err = e.Encode(manifest); err != nil {
return err
}
return nil
}