Files
GaCy-GO/internal/common/scene_metadata.go
T

163 lines
6.3 KiB
Go

package common
import (
"encoding/json"
"fmt"
)
// Scene metadata is exported from the installed Client's RuntimeFloor tables.
// A complete group list allows us to exclude deleted/client-owned groups
// without maintaining special cases for individual maps or group IDs.
type sceneMetadata struct {
ClientVersion string `json:"client_version"`
Floors map[uint32]struct {
MapTeleportEntryID uint32 `json:"map_teleport_entry_id"`
DimensionID *uint32 `json:"dimension_id"`
DimensionGroups map[uint32][]uint32 `json:"dimension_groups"`
GroupProperties map[uint32]map[string]GroupPropertyRule `json:"group_properties"`
SavedValues map[string]int32 `json:"saved_values"`
SavedValueRules map[string]GroupPropertyRule `json:"saved_value_rules"`
EntrySavedValues map[uint32]map[string]int32 `json:"entry_saved_values"`
Props map[uint32][]ScenePropInfo `json:"props"`
StartAnchor *AnchorItem `json:"start_anchor"`
FloorDefaultAnchor *AnchorItem `json:"floor_default_anchor"`
Monsters map[uint32]map[uint32]TeleportInfo `json:"monsters"`
GroupCount int `json:"group_count"`
Groups map[uint32]struct {
LoadSide string `json:"load_side"`
Deleted bool `json:"is_deleted"`
} `json:"groups"`
} `json:"floors"`
}
func applySceneMetadata(res *JsonConfig) {
raw, err := ReadResourceFile("resources/SceneMetadata.json")
if err != nil {
return
}
var metadata sceneMetadata
if err := json.Unmarshal(raw, &metadata); err != nil {
LogInfo("[SCENE] Invalid SceneMetadata: %v", err)
return
}
config, err := LoadRuntimeConfig()
if err != nil || metadata.ClientVersion != config.ClientVersion {
LogInfo("[SCENE] SceneMetadata Client revision does not match runtime config; not applied")
return
}
for entryID, configs := range res.LevelOutputConfigs {
for name, scene := range configs {
var planeID, floorID uint32
if n, _ := fmt.Sscanf(name, "P%d_F%d", &planeID, &floorID); n != 2 {
continue
}
floor, ok := metadata.Floors[floorID]
if !ok {
continue
}
if floor.GroupCount == 0 || len(floor.Groups) != floor.GroupCount {
LogInfo("[SCENE] Incomplete group metadata for floor %d; not applied", floorID)
continue
}
if target := floor.MapTeleportEntryID; target != 0 {
entrance, exists := res.MapEntrances[target]
canonical := fmt.Sprintf("P%d_F%d", planeID, floorID)
if exists && entrance.PlaneID == planeID && entrance.FloorID == floorID &&
entrance.EntranceType == "Town" && res.LevelOutputConfigs[target][canonical] != nil {
if res.MapTeleportEntrances == nil {
res.MapTeleportEntrances = map[uint32]uint32{}
}
res.MapTeleportEntrances[floorID] = target
res.MapDefaultEntranceMap[floorID] = target
} else {
LogInfo("[SCENE] Map teleport entrance %d is not a configured Town on floor %d; routing override not applied", target, floorID)
}
}
if floor.StartAnchor != nil {
res.SceneAnchors[entryID] = []AnchorItem{*floor.StartAnchor}
}
if floor.FloorDefaultAnchor != nil && floor.FloorDefaultAnchor.ID != 0 {
if res.SceneDefaultAnchors == nil {
res.SceneDefaultAnchors = map[uint32]AnchorItem{}
}
res.SceneDefaultAnchors[floorID] = *floor.FloorDefaultAnchor
}
if floor.GroupProperties != nil {
scene.GroupProperties = floor.GroupProperties
}
if floor.DimensionID != nil {
scene.DimensionID = *floor.DimensionID
}
var dimensionGroups map[uint32]bool
if floor.DimensionGroups != nil {
ids, exists := floor.DimensionGroups[scene.DimensionID]
if !exists || len(ids) == 0 {
LogInfo("[SCENE] Missing group membership for floor %d dimension %d; scene override not applied", floorID, scene.DimensionID)
continue
}
dimensionGroups = make(map[uint32]bool, len(ids))
for _, id := range ids {
dimensionGroups[id] = true
}
}
if floor.SavedValues != nil {
// RuntimeDimension exports the actual DefaultValue, not MaxValue.
// This applies only to floors with a complete captured override.
scene.SavedValues = floor.SavedValues
}
if overrides, exists := floor.EntrySavedValues[entryID]; exists {
entrance, known := res.MapEntrances[entryID]
valid := known && entrance.FloorID == floorID && entrance.PlaneID == planeID
values := make(map[string]int32, len(floor.SavedValues))
for key, value := range floor.SavedValues {
values[key] = value
}
for key, value := range overrides {
rule, known := floor.SavedValueRules[key]
if !known || value < rule.MinValue || value > rule.MaxValue {
valid = false
break
}
values[key] = value
}
if valid {
// Never mutate the shared floor defaults: entrances sharing a
// floor can intentionally represent different story states.
scene.SavedValues = values
} else {
LogInfo("[SCENE] Invalid saved-value profile entry=%d floor=%d; override not applied", entryID, floorID)
}
}
for groupID := range scene.Scenes {
group, exists := floor.Groups[groupID]
if !exists || group.Deleted || group.LoadSide == "Client" || (dimensionGroups != nil && !dimensionGroups[groupID]) {
delete(scene.Scenes, groupID)
LogInfo("[SCENE] Excluded group entry=%d floor=%d group=%d present=%t deleted=%t side=%s", entryID, floorID, groupID, exists, group.Deleted, group.LoadSide)
continue
}
data := scene.Scenes[groupID]
if floor.Props != nil {
data.Props = floor.Props[groupID]
}
for index := range data.Monsters {
monster := &data.Monsters[index]
if placement, ok := floor.Monsters[groupID][monster.InstID]; ok {
monster.Pos, monster.Rot = placement.Pos, placement.Rot
}
}
scene.Scenes[groupID] = data
}
// Resource updates may introduce server props in previously empty groups.
for groupID, props := range floor.Props {
group, exists := floor.Groups[groupID]
if !exists || group.Deleted || group.LoadSide == "Client" || (dimensionGroups != nil && !dimensionGroups[groupID]) {
continue
}
data := scene.Scenes[groupID]
data.Props = props
scene.Scenes[groupID] = data
}
}
}
}