SportsBallEngine/Sources/AssetLoader/AssetManager.swift
2025-12-15 16:03:37 -08:00

464 lines
14 KiB
Swift

/// AssetLoader - Asset management and loading system
/// NO PLATFORM-SPECIFIC IMPORTS ALLOWED IN THIS MODULE
/// Handles loading of 3D models, textures, animations, and audio files
import Foundation
// MARK: - Asset Types
/// Asset loading result
public enum AssetResult<T> {
case success(T)
case failure(AssetError)
}
/// Asset loading errors
public enum AssetError: Error, LocalizedError {
case fileNotFound(String)
case invalidFormat(String)
case corruptedData(String)
case unsupportedVersion(String)
case outOfMemory
public var errorDescription: String? {
switch self {
case .fileNotFound(let path):
return "Asset file not found: \(path)"
case .invalidFormat(let format):
return "Invalid asset format: \(format)"
case .corruptedData(let reason):
return "Corrupted asset data: \(reason)"
case .unsupportedVersion(let version):
return "Unsupported asset version: \(version)"
case .outOfMemory:
return "Out of memory while loading asset"
}
}
}
/// Asset handle for reference counting and management
public struct AssetHandle<T>: Hashable, Sendable where T: Hashable {
public let id: UUID
public let path: String
public init(id: UUID = UUID(), path: String) {
self.id = id
self.path = path
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// MARK: - Mesh Data
/// Mesh data structure (vertex and index buffers)
public struct MeshData: Sendable, Hashable {
public var name: String
public var vertices: [MeshVertex]
public var indices: [UInt32]
public var bounds: BoundingBox
public init(name: String, vertices: [MeshVertex], indices: [UInt32], bounds: BoundingBox) {
self.name = name
self.vertices = vertices
self.indices = indices
self.bounds = bounds
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
/// Vertex structure for mesh data
public struct MeshVertex: Sendable, Hashable {
public var position: SIMD3<Float>
public var normal: SIMD3<Float>
public var uv: SIMD2<Float>
public var tangent: SIMD3<Float>
public var boneIndices: SIMD4<UInt8> // For skeletal animation
public var boneWeights: SIMD4<Float> // For skeletal animation
public init(position: SIMD3<Float>, normal: SIMD3<Float>, uv: SIMD2<Float>,
tangent: SIMD3<Float> = .zero, boneIndices: SIMD4<UInt8> = .zero,
boneWeights: SIMD4<Float> = .zero) {
self.position = position
self.normal = normal
self.uv = uv
self.tangent = tangent
self.boneIndices = boneIndices
self.boneWeights = boneWeights
}
}
/// Bounding box for culling
public struct BoundingBox: Sendable, Hashable {
public var min: SIMD3<Float>
public var max: SIMD3<Float>
public init(min: SIMD3<Float>, max: SIMD3<Float>) {
self.min = min
self.max = max
}
}
// MARK: - Texture Data
/// Texture data structure
public struct TextureData: Sendable, Hashable {
public var name: String
public var width: Int
public var height: Int
public var format: TextureFormat
public var data: Data
public var mipLevels: Int
public init(name: String, width: Int, height: Int, format: TextureFormat, data: Data, mipLevels: Int = 1) {
self.name = name
self.width = width
self.height = height
self.format = format
self.data = data
self.mipLevels = mipLevels
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
/// Texture format
public enum TextureFormat: Sendable, Hashable {
case rgba8
case rgba16f
case rgba32f
case dxt1
case dxt5
case bc7
}
// MARK: - Animation Data
/// Skeletal animation data
public struct AnimationData: Sendable, Hashable {
public var name: String
public var duration: Float
public var ticksPerSecond: Float
public var channels: [AnimationChannel]
public init(name: String, duration: Float, ticksPerSecond: Float, channels: [AnimationChannel]) {
self.name = name
self.duration = duration
self.ticksPerSecond = ticksPerSecond
self.channels = channels
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
/// Animation channel (per-bone)
public struct AnimationChannel: Sendable, Hashable {
public var boneName: String
public var positionKeys: [KeyFrame<SIMD3<Float>>]
public var rotationKeys: [KeyFrame<SIMD4<Float>>]
public var scaleKeys: [KeyFrame<SIMD3<Float>>]
public init(boneName: String, positionKeys: [KeyFrame<SIMD3<Float>>],
rotationKeys: [KeyFrame<SIMD4<Float>>], scaleKeys: [KeyFrame<SIMD3<Float>>]) {
self.boneName = boneName
self.positionKeys = positionKeys
self.rotationKeys = rotationKeys
self.scaleKeys = scaleKeys
}
}
/// Animation keyframe
public struct KeyFrame<T: Sendable & Hashable>: Sendable, Hashable {
public var time: Float
public var value: T
public init(time: Float, value: T) {
self.time = time
self.value = value
}
}
// MARK: - Skeleton Data
/// Skeletal hierarchy for character animation
public struct SkeletonData: Sendable, Hashable {
public var bones: [Bone]
public var rootBoneIndex: Int
public init(bones: [Bone], rootBoneIndex: Int = 0) {
self.bones = bones
self.rootBoneIndex = rootBoneIndex
}
}
/// Individual bone in skeleton
public struct Bone: Sendable, Hashable {
public var name: String
public var parentIndex: Int?
public var offsetMatrix: float4x4
public init(name: String, parentIndex: Int?, offsetMatrix: float4x4) {
self.name = name
self.parentIndex = parentIndex
self.offsetMatrix = offsetMatrix
}
}
// MARK: - Asset Manager
/// Main asset management system
public actor AssetManager {
private var meshCache: [String: MeshData] = [:]
private var textureCache: [String: TextureData] = [:]
private var animationCache: [String: AnimationData] = [:]
private var skeletonCache: [String: SkeletonData] = [:]
private var loadedAssets: Set<String> = []
public init() {}
// MARK: - Lifecycle
public func initialize() {
print(" ✓ Asset manager initialized")
}
public func shutdown() {
meshCache.removeAll()
textureCache.removeAll()
animationCache.removeAll()
skeletonCache.removeAll()
loadedAssets.removeAll()
print(" ✓ Asset manager shutdown (cleared caches)")
}
// MARK: - Mesh Loading
/// Load a 3D mesh from file
public func loadMesh(path: String) async -> AssetResult<MeshData> {
// Check cache first
if let cached = meshCache[path] {
return .success(cached)
}
// Load from disk
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return .failure(.fileNotFound(path))
}
// Parse mesh format (OBJ, FBX, GLTF, etc.)
guard let mesh = parseMeshData(data: data, path: path) else {
return .failure(.invalidFormat(path))
}
// Cache and return
meshCache[path] = mesh
loadedAssets.insert(path)
return .success(mesh)
}
private func parseMeshData(data: Data, path: String) -> MeshData? {
// Simplified placeholder - would parse actual formats
// For sports games: support FBX (player models), GLTF (stadiums), custom formats
// Create a simple test mesh (placeholder)
return createTestMesh(name: path)
}
private func createTestMesh(name: String) -> MeshData {
// Create a simple cube for testing
let vertices: [MeshVertex] = [
MeshVertex(position: SIMD3<Float>(-1, -1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(0, 0)),
MeshVertex(position: SIMD3<Float>(1, -1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(1, 0)),
MeshVertex(position: SIMD3<Float>(1, 1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(1, 1)),
MeshVertex(position: SIMD3<Float>(-1, 1, -1), normal: SIMD3<Float>(0, 0, -1), uv: SIMD2<Float>(0, 1)),
]
let indices: [UInt32] = [0, 1, 2, 0, 2, 3]
let bounds = BoundingBox(
min: SIMD3<Float>(-1, -1, -1),
max: SIMD3<Float>(1, 1, 1)
)
return MeshData(name: name, vertices: vertices, indices: indices, bounds: bounds)
}
// MARK: - Texture Loading
/// Load a texture from file
public func loadTexture(path: String) async -> AssetResult<TextureData> {
// Check cache first
if let cached = textureCache[path] {
return .success(cached)
}
// Load from disk
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return .failure(.fileNotFound(path))
}
// Parse texture format (PNG, JPG, DDS, KTX, etc.)
guard let texture = parseTextureData(data: data, path: path) else {
return .failure(.invalidFormat(path))
}
// Cache and return
textureCache[path] = texture
loadedAssets.insert(path)
return .success(texture)
}
private func parseTextureData(data: Data, path: String) -> TextureData? {
// Simplified placeholder - would use image decoding libraries
// For sports games: support DDS (compressed), PNG, JPG, HDR textures
// Create a simple test texture
return createTestTexture(name: path)
}
private func createTestTexture(name: String) -> TextureData {
// Create a 2x2 test texture (white)
let width = 2
let height = 2
var data = Data(count: width * height * 4)
for i in 0..<(width * height * 4) {
data[i] = 255
}
return TextureData(name: name, width: width, height: height, format: .rgba8, data: data)
}
// MARK: - Animation Loading
/// Load skeletal animation from file
public func loadAnimation(path: String) async -> AssetResult<AnimationData> {
// Check cache first
if let cached = animationCache[path] {
return .success(cached)
}
// Load from disk
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return .failure(.fileNotFound(path))
}
// Parse animation format
guard let animation = parseAnimationData(data: data, path: path) else {
return .failure(.invalidFormat(path))
}
// Cache and return
animationCache[path] = animation
loadedAssets.insert(path)
return .success(animation)
}
private func parseAnimationData(data: Data, path: String) -> AnimationData? {
// Simplified placeholder - would parse FBX/GLTF animations
// For sports games: running, shooting, passing, tackling animations
return createTestAnimation(name: path)
}
private func createTestAnimation(name: String) -> AnimationData {
// Create a simple test animation
let channel = AnimationChannel(
boneName: "Root",
positionKeys: [
KeyFrame(time: 0.0, value: SIMD3<Float>(0, 0, 0)),
KeyFrame(time: 1.0, value: SIMD3<Float>(1, 0, 0))
],
rotationKeys: [
KeyFrame(time: 0.0, value: SIMD4<Float>(0, 0, 0, 1)),
KeyFrame(time: 1.0, value: SIMD4<Float>(0, 0, 0, 1))
],
scaleKeys: [
KeyFrame(time: 0.0, value: SIMD3<Float>(1, 1, 1)),
KeyFrame(time: 1.0, value: SIMD3<Float>(1, 1, 1))
]
)
return AnimationData(name: name, duration: 1.0, ticksPerSecond: 30.0, channels: [channel])
}
// MARK: - Skeleton Loading
/// Load skeletal hierarchy from file
public func loadSkeleton(path: String) async -> AssetResult<SkeletonData> {
// Check cache first
if let cached = skeletonCache[path] {
return .success(cached)
}
// Load from disk
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return .failure(.fileNotFound(path))
}
// Parse skeleton format
guard let skeleton = parseSkeletonData(data: data, path: path) else {
return .failure(.invalidFormat(path))
}
// Cache and return
skeletonCache[path] = skeleton
loadedAssets.insert(path)
return .success(skeleton)
}
private func parseSkeletonData(data: Data, path: String) -> SkeletonData? {
// Simplified placeholder
return createTestSkeleton()
}
private func createTestSkeleton() -> SkeletonData {
let rootBone = Bone(name: "Root", parentIndex: nil, offsetMatrix: matrix_identity_float4x4)
return SkeletonData(bones: [rootBone], rootBoneIndex: 0)
}
// MARK: - Cache Management
public func unloadAsset(path: String) {
meshCache.removeValue(forKey: path)
textureCache.removeValue(forKey: path)
animationCache.removeValue(forKey: path)
skeletonCache.removeValue(forKey: path)
loadedAssets.remove(path)
}
public func clearCache() {
meshCache.removeAll()
textureCache.removeAll()
animationCache.removeAll()
skeletonCache.removeAll()
loadedAssets.removeAll()
}
public func getCacheSize() -> (meshes: Int, textures: Int, animations: Int, skeletons: Int) {
return (meshCache.count, textureCache.count, animationCache.count, skeletonCache.count)
}
}
// MARK: - Matrix Helper
extension float4x4 {
static var identity: float4x4 {
return matrix_identity_float4x4
}
}