Skip to content

Commit 0cbbe64

Browse files
committed
fix: use fallback hairstyle when wearing headgear
Apply haircut fallback based on HeadAccessoryType: - FullyCovering: hide hair entirely - HalfCovering: use GenericShort/Medium/Long fallback - Simple: keep original haircut
1 parent 85d8c46 commit 0cbbe64

1 file changed

Lines changed: 171 additions & 6 deletions

File tree

internal/service/merger.go

Lines changed: 171 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package service
33
import (
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
2136
type 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

Comments
 (0)