blockyserver/internal/service/merger.go
2026-01-22 16:26:39 +06:00

230 lines
5.7 KiB
Go

package service
import (
"encoding/json"
"fmt"
"github.com/hytale-tools/blockymodel-merger/pkg/blockymodel"
"github.com/hytale-tools/blockymodel-merger/pkg/character"
"github.com/hytale-tools/blockymodel-merger/pkg/export"
"github.com/hytale-tools/blockymodel-merger/pkg/merger"
"github.com/hytale-tools/blockymodel-merger/pkg/registry"
"github.com/hytale-tools/blockymodel-merger/pkg/texture"
)
const (
basePath = "assets/Characters/Player.blockymodel"
baseTexturePath = "Characters/Player_Textures/Player_Greyscale.png"
)
// MergeService handles character merging operations
type MergeService struct {
registry *registry.Registry
gradientSets *texture.GradientSets
baseModel *blockymodel.BlockyModel
}
// MergeResult contains the results of a merge operation
type MergeResult struct {
Model *blockymodel.BlockyModel
Atlas *texture.Atlas
GLBBytes []byte
}
// NewMergeService creates a new merge service with all required data loaded
func NewMergeService() (*MergeService, error) {
// Load gradient sets for tinting
gradientSets, err := texture.LoadGradientSets()
if err != nil {
return nil, fmt.Errorf("loading gradient sets: %w", err)
}
// Load accessory registry
reg, err := registry.Load()
if err != nil {
return nil, fmt.Errorf("loading registry: %w", err)
}
// Load base player model
baseModel, err := blockymodel.Load(basePath)
if err != nil {
return nil, fmt.Errorf("loading base model: %w", err)
}
return &MergeService{
registry: reg,
gradientSets: gradientSets,
baseModel: baseModel,
}, nil
}
// MergeFromJSON merges a character from JSON data and returns the result
func (s *MergeService) MergeFromJSON(charJSON []byte) (*MergeResult, error) {
// Parse character data
var charData character.CharacterData
if err := json.Unmarshal(charJSON, &charData); err != nil {
return nil, fmt.Errorf("parsing character JSON: %w", err)
}
// Resolve accessories
result, err := charData.ResolveAccessories(s.registry)
if err != nil {
return nil, fmt.Errorf("resolving accessories: %w", err)
}
// Create merger from base model
m, err := merger.New(s.baseModel)
if err != nil {
return nil, fmt.Errorf("creating merger: %w", err)
}
// Merge each accessory
for _, acc := range result.Accessories {
accessory, err := blockymodel.Load(acc.Path)
if err != nil {
return nil, fmt.Errorf("loading accessory %s: %w", acc.Path, err)
}
if err := m.Merge(accessory, acc.Spec.ID); err != nil {
return nil, fmt.Errorf("merging accessory %s: %w", acc.Path, err)
}
}
// Get merged model
mergedModel := m.Result()
// Process textures
var tintedTextures []*texture.TintedTexture
skinTone := charData.GetSkinTone()
// Load and tint base player texture
if skinTone != "" {
baseTinted, err := texture.ProcessAccessoryTexture(
"_base",
baseTexturePath,
"Skin",
skinTone,
s.gradientSets,
)
if err == nil {
tintedTextures = append(tintedTextures, baseTinted)
}
} else {
baseImg, err := texture.LoadImage(baseTexturePath)
if err == nil {
baseTex := &texture.TintedTexture{
Name: "_base",
Image: baseImg,
OriginalPath: baseTexturePath,
}
tintedTextures = append(tintedTextures, baseTex)
}
}
// Process accessory textures
for _, acc := range result.Accessories {
if acc.ResolvedTexture == nil {
continue
}
var tinted *texture.TintedTexture
if acc.ResolvedTexture.DirectTexture != "" {
img, err := texture.LoadImage(acc.ResolvedTexture.DirectTexture)
if err != nil {
continue
}
tinted = &texture.TintedTexture{
Name: acc.Spec.ID,
Image: img,
OriginalPath: acc.ResolvedTexture.DirectTexture,
}
} else if acc.ResolvedTexture.GreyscaleTexture != "" {
var err error
tinted, err = texture.ProcessAccessoryTexture(
acc.Spec.ID,
acc.ResolvedTexture.GreyscaleTexture,
acc.ResolvedTexture.GradientSet,
acc.Spec.Color,
s.gradientSets,
)
if err != nil {
continue
}
} else {
continue
}
tintedTextures = append(tintedTextures, tinted)
}
// Pack textures into atlas
var atlas *texture.Atlas
if len(tintedTextures) > 0 {
var err error
atlas, err = texture.PackAtlasSimple(tintedTextures, 1)
if err != nil {
return nil, fmt.Errorf("packing atlas: %w", err)
}
// Update texture offsets in the merged model
for _, tex := range tintedTextures {
if tex.Name == "_base" {
continue
}
x, y, _, _, ok := atlas.GetPixelCoords(tex.Name)
if !ok {
continue
}
// Find all node IDs that came from this accessory
nodeIDs := make(map[string]bool)
for nodeID, accessoryID := range m.NodeSources {
if accessoryID == tex.Name {
nodeIDs[nodeID] = true
}
}
if len(nodeIDs) > 0 {
offset := blockymodel.AtlasOffset{X: float64(x), Y: float64(y)}
blockymodel.UpdateTextureOffsets(mergedModel.Nodes, nodeIDs, offset)
}
}
}
// Export to GLB
exporter := export.NewGLBExporter()
var materialIdx uint32
if atlas != nil {
w, h := atlas.Image.Bounds().Dx(), atlas.Image.Bounds().Dy()
exporter.SetAtlasSize(float64(w), float64(h))
atlasBytes, err := texture.EncodePNG(atlas.Image)
if err != nil {
return nil, fmt.Errorf("encoding atlas: %w", err)
}
texIdx := exporter.AddTexture(atlasBytes)
materialIdx = exporter.AddMaterial("textured", texIdx)
} else {
exporter.SetAtlasSize(64, 64)
materialIdx = 0
}
if err := exporter.ExportModel(mergedModel, materialIdx); err != nil {
return nil, fmt.Errorf("exporting model: %w", err)
}
glbBytes, err := exporter.Bytes()
if err != nil {
return nil, fmt.Errorf("getting GLB bytes: %w", err)
}
return &MergeResult{
Model: mergedModel,
Atlas: atlas,
GLBBytes: glbBytes,
}, nil
}