@@ -3,6 +3,8 @@ package service
33import (
44 "encoding/json"
55 "fmt"
6+ "os"
7+ "strings"
68
79 "github.com/hytale-tools/blockymodel-merger/pkg/blockymodel"
810 "github.com/hytale-tools/blockymodel-merger/pkg/character"
@@ -17,11 +19,27 @@ const (
1719 baseTexturePath = "assets/Characters/Player_Textures/Player_Greyscale.png"
1820)
1921
22+ // HeadAccessoryEntry extends registry entry with HeadAccessoryType
23+ type HeadAccessoryEntry struct {
24+ ID string `json:"Id"`
25+ HeadAccessoryType string `json:"HeadAccessoryType"`
26+ DisableCharacterPartCategory string `json:"DisableCharacterPartCategory"`
27+ }
28+
29+ // HaircutEntry extends registry entry with HairType
30+ type HaircutEntry struct {
31+ ID string `json:"Id"`
32+ HairType string `json:"HairType"`
33+ }
34+
2035// MergeService handles character merging operations
2136type MergeService struct {
22- registry * registry.Registry
23- gradientSets * texture.GradientSets
24- baseModel * blockymodel.BlockyModel
37+ registry * registry.Registry
38+ gradientSets * texture.GradientSets
39+ baseModel * blockymodel.BlockyModel
40+ headAccessories map [string ]HeadAccessoryEntry
41+ haircuts map [string ]HaircutEntry
42+ haircutFallbacks map [string ]string // HairType -> fallback haircut ID
2543}
2644
2745// MergeResult contains the results of a merge operation
@@ -51,10 +69,31 @@ func NewMergeService() (*MergeService, error) {
5169 return nil , fmt .Errorf ("loading base model: %w" , err )
5270 }
5371
72+ // Load head accessories for HeadAccessoryType
73+ headAccessories , err := loadHeadAccessories ("data/HeadAccessory.json" )
74+ if err != nil {
75+ return nil , fmt .Errorf ("loading head accessories: %w" , err )
76+ }
77+
78+ // Load haircuts for HairType
79+ haircuts , err := loadHaircuts ("data/Haircuts.json" )
80+ if err != nil {
81+ return nil , fmt .Errorf ("loading haircuts: %w" , err )
82+ }
83+
84+ // Load haircut fallbacks
85+ haircutFallbacks , err := loadHaircutFallbacks ("data/HaircutFallbacks.json" )
86+ if err != nil {
87+ return nil , fmt .Errorf ("loading haircut fallbacks: %w" , err )
88+ }
89+
5490 return & MergeService {
55- registry : reg ,
56- gradientSets : gradientSets ,
57- baseModel : baseModel ,
91+ registry : reg ,
92+ gradientSets : gradientSets ,
93+ baseModel : baseModel ,
94+ headAccessories : headAccessories ,
95+ haircuts : haircuts ,
96+ haircutFallbacks : haircutFallbacks ,
5897 }, nil
5998}
6099
@@ -66,6 +105,9 @@ func (s *MergeService) MergeFromJSON(charJSON []byte) (*MergeResult, error) {
66105 return nil , fmt .Errorf ("parsing character JSON: %w" , err )
67106 }
68107
108+ // Apply haircut fallback if headAccessory requires it
109+ s .applyHaircutFallback (& charData )
110+
69111 // Resolve accessories
70112 result , err := charData .ResolveAccessories (s .registry )
71113 if err != nil {
@@ -228,3 +270,126 @@ func (s *MergeService) MergeFromJSON(charJSON []byte) (*MergeResult, error) {
228270 GLBBytes : glbBytes ,
229271 }, nil
230272}
273+
274+ // applyHaircutFallback modifies haircut based on headAccessory type
275+ func (s * MergeService ) applyHaircutFallback (charData * character.CharacterData ) {
276+ if charData .HeadAccessory == nil || * charData .HeadAccessory == "" {
277+ return
278+ }
279+
280+ // Parse head accessory ID
281+ headAccID := strings .Split (* charData .HeadAccessory , "." )[0 ]
282+ headAcc , ok := s .headAccessories [headAccID ]
283+ if ! ok {
284+ return
285+ }
286+
287+ // Check if headAccessory disables haircut entirely
288+ if headAcc .DisableCharacterPartCategory == "Haircut" {
289+ charData .Haircut = nil
290+ return
291+ }
292+
293+ // Check headAccessory type
294+ switch headAcc .HeadAccessoryType {
295+ case "FullyCovering" :
296+ // No hair visible
297+ charData .Haircut = nil
298+ case "HalfCovering" :
299+ // Use fallback hairstyle
300+ if charData .Haircut != nil && * charData .Haircut != "" {
301+ s .setFallbackHaircut (charData )
302+ }
303+ }
304+ // "Simple" or empty: keep original haircut
305+ }
306+
307+ // setFallbackHaircut replaces haircut with appropriate fallback based on HairType
308+ func (s * MergeService ) setFallbackHaircut (charData * character.CharacterData ) {
309+ if charData .Haircut == nil || * charData .Haircut == "" {
310+ return
311+ }
312+
313+ // Parse haircut spec (ID.Color.Variant)
314+ parts := strings .Split (* charData .Haircut , "." )
315+ haircutID := parts [0 ]
316+ color := ""
317+ if len (parts ) > 1 {
318+ color = parts [1 ]
319+ }
320+
321+ // Get haircut entry to find HairType
322+ haircut , ok := s .haircuts [haircutID ]
323+ if ! ok {
324+ return
325+ }
326+
327+ // Get fallback haircut ID for this HairType
328+ fallbackID , ok := s .haircutFallbacks [haircut .HairType ]
329+ if ! ok {
330+ return
331+ }
332+
333+ // Build new haircut string with fallback ID but same color
334+ newHaircut := fallbackID
335+ if color != "" {
336+ newHaircut = fallbackID + "." + color
337+ }
338+ charData .Haircut = & newHaircut
339+ }
340+
341+ // loadHeadAccessories loads head accessory data from JSON file
342+ func loadHeadAccessories (path string ) (map [string ]HeadAccessoryEntry , error ) {
343+ data , err := os .ReadFile (path )
344+ if err != nil {
345+ return nil , err
346+ }
347+
348+ var entries []HeadAccessoryEntry
349+ if err := json .Unmarshal (data , & entries ); err != nil {
350+ return nil , err
351+ }
352+
353+ result := make (map [string ]HeadAccessoryEntry )
354+ for _ , e := range entries {
355+ if e .ID != "" {
356+ result [e .ID ] = e
357+ }
358+ }
359+ return result , nil
360+ }
361+
362+ // loadHaircuts loads haircut data from JSON file
363+ func loadHaircuts (path string ) (map [string ]HaircutEntry , error ) {
364+ data , err := os .ReadFile (path )
365+ if err != nil {
366+ return nil , err
367+ }
368+
369+ var entries []HaircutEntry
370+ if err := json .Unmarshal (data , & entries ); err != nil {
371+ return nil , err
372+ }
373+
374+ result := make (map [string ]HaircutEntry )
375+ for _ , e := range entries {
376+ if e .ID != "" {
377+ result [e .ID ] = e
378+ }
379+ }
380+ return result , nil
381+ }
382+
383+ // loadHaircutFallbacks loads haircut fallback mappings from JSON file
384+ func loadHaircutFallbacks (path string ) (map [string ]string , error ) {
385+ data , err := os .ReadFile (path )
386+ if err != nil {
387+ return nil , err
388+ }
389+
390+ var result map [string ]string
391+ if err := json .Unmarshal (data , & result ); err != nil {
392+ return nil , err
393+ }
394+ return result , nil
395+ }
0 commit comments