aether-engine 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. aether/__init__.py +54 -0
  2. aether/audio/__init__.py +0 -0
  3. aether/audio/audio_manager.py +211 -0
  4. aether/audio/music.py +6 -0
  5. aether/audio/sound.py +6 -0
  6. aether/core/__init__.py +211 -0
  7. aether/core/achievement_system.py +304 -0
  8. aether/core/application.py +19 -0
  9. aether/core/config.py +75 -0
  10. aether/core/console.py +366 -0
  11. aether/core/engine.py +50 -0
  12. aether/core/event_bus.py +24 -0
  13. aether/core/input.py +58 -0
  14. aether/core/plugin_system.py +93 -0
  15. aether/core/save_system.py +409 -0
  16. aether/core/time_manager.py +25 -0
  17. aether/core/window.py +57 -0
  18. aether/graphics/__init__.py +0 -0
  19. aether/graphics/camera.py +69 -0
  20. aether/graphics/framebuffer.py +119 -0
  21. aether/graphics/light.py +32 -0
  22. aether/graphics/lod_system.py +170 -0
  23. aether/graphics/material.py +25 -0
  24. aether/graphics/mesh.py +83 -0
  25. aether/graphics/mesh_factory.py +67 -0
  26. aether/graphics/particle_system.py +268 -0
  27. aether/graphics/post_process.py +279 -0
  28. aether/graphics/renderer.py +135 -0
  29. aether/graphics/shader.py +61 -0
  30. aether/graphics/shader_library.py +157 -0
  31. aether/graphics/sky_atmosphere.py +309 -0
  32. aether/graphics/terrain.py +408 -0
  33. aether/graphics/texture.py +34 -0
  34. aether/graphics/water.py +432 -0
  35. aether/physics/__init__.py +0 -0
  36. aether/physics/bullet_backend.py +395 -0
  37. aether/physics/collider.py +22 -0
  38. aether/physics/physics_world.py +436 -0
  39. aether/physics/raycast.py +25 -0
  40. aether/physics/rigidbody.py +7 -0
  41. aether/resources/__init__.py +0 -0
  42. aether/resources/asset_database.py +76 -0
  43. aether/resources/resource_manager.py +147 -0
  44. aether/resources/serialization.py +114 -0
  45. aether/scene/__init__.py +0 -0
  46. aether/scene/component.py +368 -0
  47. aether/scene/entity.py +46 -0
  48. aether/scene/scene.py +33 -0
  49. aether/scene/scene_graph.py +252 -0
  50. aether/scene/scene_manager.py +191 -0
  51. aether/ui/__init__.py +0 -0
  52. aether/ui/button.py +297 -0
  53. aether/ui/canvas.py +11 -0
  54. aether/ui/text.py +286 -0
  55. aether/utils/__init__.py +0 -0
  56. aether/utils/logger.py +16 -0
  57. aether/utils/math_utils.py +31 -0
  58. aether/utils/object_pool.py +137 -0
  59. aether/utils/profiler.py +187 -0
  60. aether_engine-1.0.0.dist-info/METADATA +71 -0
  61. aether_engine-1.0.0.dist-info/RECORD +64 -0
  62. aether_engine-1.0.0.dist-info/WHEEL +5 -0
  63. aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
  64. aether_engine-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,436 @@
1
+ """Мир физики с обнаружением коллизий и разрешением"""
2
+ import numpy as np
3
+ from typing import List, Optional, Tuple
4
+ from aether.physics.collider import Collider, BoxCollider, SphereCollider
5
+
6
+
7
+ class CollisionInfo:
8
+ """Информация о столкновении"""
9
+
10
+ def __init__(self):
11
+ self.collided = False
12
+ self.point = np.array([0.0, 0.0, 0.0], dtype=np.float32)
13
+ self.normal = np.array([0.0, 1.0, 0.0], dtype=np.float32)
14
+ self.depth = 0.0
15
+ self.entity_a = None
16
+ self.entity_b = None
17
+
18
+
19
+ class PhysicsWorld:
20
+ """Мир физики"""
21
+
22
+ def __init__(self, gravity=(0, -9.81, 0)):
23
+ self.gravity = np.array(gravity, dtype=np.float32)
24
+ self.bodies = []
25
+ self.colliders = []
26
+ self.collision_pairs = []
27
+
28
+ # Настройки
29
+ self.solver_iterations = 10
30
+ self.enable_sleeping = True
31
+ self.sleep_threshold = 0.01
32
+ self.sleep_time_threshold = 1.0
33
+
34
+ # События
35
+ self.on_collision_enter = []
36
+ self.on_collision_exit = []
37
+ self.on_trigger_enter = []
38
+
39
+ print(f"[Physics] World created (gravity: {self.gravity})")
40
+
41
+ def add_rigidbody(self, body):
42
+ """Добавляет твёрдое тело в симуляцию"""
43
+ self.bodies.append(body)
44
+ if hasattr(body, 'entity') and body.entity:
45
+ collider = body.entity.get_component("Collider") or \
46
+ body.entity.get_component("BoxCollider") or \
47
+ body.entity.get_component("SphereCollider")
48
+ if collider:
49
+ self.colliders.append((body, collider))
50
+
51
+ def remove_rigidbody(self, body):
52
+ """Удаляет твёрдое тело из симуляции"""
53
+ if body in self.bodies:
54
+ self.bodies.remove(body)
55
+ self.colliders = [(b, c) for b, c in self.colliders if b != body]
56
+
57
+ def step(self, dt: float):
58
+ """Шаг симуляции"""
59
+ # 1. Применяем силы
60
+ self._apply_forces(dt)
61
+
62
+ # 2. Обновляем скорости
63
+ self._integrate_velocities(dt)
64
+
65
+ # 3. Обнаружение коллизий
66
+ collisions = self._detect_collisions()
67
+
68
+ # 4. Разрешение коллизий
69
+ for _ in range(self.solver_iterations):
70
+ self._resolve_collisions(collisions, dt)
71
+
72
+ # 5. Обновляем позиции
73
+ self._integrate_positions(dt)
74
+
75
+ # 6. Проверка на сон
76
+ if self.enable_sleeping:
77
+ self._check_sleeping(dt)
78
+
79
+ def _apply_forces(self, dt: float):
80
+ """Применяет силы к телам"""
81
+ for body in self.bodies:
82
+ if body.use_gravity and not body.is_kinematic:
83
+ body.velocity += self.gravity * dt
84
+
85
+ def _integrate_velocities(self, dt: float):
86
+ """Интегрирует скорости"""
87
+ for body in self.bodies:
88
+ if not body.is_kinematic:
89
+ # Применяем drag
90
+ body.velocity *= (1.0 - body.drag * dt)
91
+ body.angular_velocity *= (1.0 - body.angular_drag * dt)
92
+
93
+ def _integrate_positions(self, dt: float):
94
+ """Интегрирует позиции"""
95
+ for body in self.bodies:
96
+ if not body.is_kinematic and not body.is_sleeping:
97
+ transform = body.entity.get_component("Transform") if hasattr(body, 'entity') and body.entity else None
98
+ if transform:
99
+ transform.position += body.velocity * dt
100
+ transform.rotation += body.angular_velocity * dt
101
+ transform._matrix_dirty = True
102
+
103
+ def _detect_collisions(self) -> List[CollisionInfo]:
104
+ """Обнаруживает все коллизии"""
105
+ collisions = []
106
+
107
+ for i in range(len(self.colliders)):
108
+ for j in range(i + 1, len(self.colliders)):
109
+ body_a, collider_a = self.colliders[i]
110
+ body_b, collider_b = self.colliders[j]
111
+
112
+ # Пропускаем спящие и кинематические пары
113
+ if (body_a.is_sleeping and body_b.is_sleeping) or \
114
+ (body_a.is_kinematic and body_b.is_kinematic):
115
+ continue
116
+
117
+ collision = self._check_collision(collider_a, collider_b)
118
+ if collision:
119
+ collision.entity_a = body_a.entity if hasattr(body_a, 'entity') else None
120
+ collision.entity_b = body_b.entity if hasattr(body_b, 'entity') else None
121
+ collisions.append(collision)
122
+
123
+ return collisions
124
+
125
+ def _check_collision(self, collider_a: Collider, collider_b: Collider) -> Optional[CollisionInfo]:
126
+ """Проверяет столкновение между двумя коллайдерами"""
127
+
128
+ # Box vs Box
129
+ if isinstance(collider_a, BoxCollider) and isinstance(collider_b, BoxCollider):
130
+ return self._box_vs_box(collider_a, collider_b)
131
+
132
+ # Sphere vs Sphere
133
+ if isinstance(collider_a, SphereCollider) and isinstance(collider_b, SphereCollider):
134
+ return self._sphere_vs_sphere(collider_a, collider_b)
135
+
136
+ # Box vs Sphere
137
+ if isinstance(collider_a, BoxCollider) and isinstance(collider_b, SphereCollider):
138
+ return self._box_vs_sphere(collider_a, collider_b)
139
+
140
+ if isinstance(collider_a, SphereCollider) and isinstance(collider_b, BoxCollider):
141
+ collision = self._box_vs_sphere(collider_b, collider_a)
142
+ if collision:
143
+ collision.normal = -collision.normal
144
+ return collision
145
+
146
+ return None
147
+
148
+ def _box_vs_box(self, a: BoxCollider, b: BoxCollider) -> Optional[CollisionInfo]:
149
+ """AABB vs AABB коллизия"""
150
+ # Получаем позиции из трансформов
151
+ pos_a = self._get_collider_position(a)
152
+ pos_b = self._get_collider_position(b)
153
+
154
+ if pos_a is None or pos_b is None:
155
+ return None
156
+
157
+ # AABB проверка
158
+ min_a = pos_a + a.center - a.size / 2
159
+ max_a = pos_a + a.center + a.size / 2
160
+ min_b = pos_b + b.center - b.size / 2
161
+ max_b = pos_b + b.center + b.size / 2
162
+
163
+ # Проверка пересечения
164
+ overlap_x = min(max_a[0], max_b[0]) - max(min_a[0], min_b[0])
165
+ overlap_y = min(max_a[1], max_b[1]) - max(min_a[1], min_b[1])
166
+ overlap_z = min(max_a[2], max_b[2]) - max(min_a[2], min_b[2])
167
+
168
+ if overlap_x <= 0 or overlap_y <= 0 or overlap_z <= 0:
169
+ return None
170
+
171
+ # Определяем нормаль и глубину
172
+ collision = CollisionInfo()
173
+ collision.collided = True
174
+
175
+ if overlap_x < overlap_y and overlap_x < overlap_z:
176
+ collision.depth = overlap_x
177
+ collision.normal = np.array([1, 0, 0] if (pos_a[0] < pos_b[0]) else [-1, 0, 0], dtype=np.float32)
178
+ elif overlap_y < overlap_z:
179
+ collision.depth = overlap_y
180
+ collision.normal = np.array([0, 1, 0] if (pos_a[1] < pos_b[1]) else [0, -1, 0], dtype=np.float32)
181
+ else:
182
+ collision.depth = overlap_z
183
+ collision.normal = np.array([0, 0, 1] if (pos_a[2] < pos_b[2]) else [0, 0, -1], dtype=np.float32)
184
+
185
+ # Точка контакта (центр пересечения)
186
+ collision.point = (pos_a + pos_b) / 2
187
+
188
+ return collision
189
+
190
+ def _sphere_vs_sphere(self, a: SphereCollider, b: SphereCollider) -> Optional[CollisionInfo]:
191
+ """Sphere vs Sphere коллизия"""
192
+ pos_a = self._get_collider_position(a)
193
+ pos_b = self._get_collider_position(b)
194
+
195
+ if pos_a is None or pos_b is None:
196
+ return None
197
+
198
+ center_a = pos_a + a.center
199
+ center_b = pos_b + b.center
200
+
201
+ distance_vec = center_b - center_a
202
+ distance = np.linalg.norm(distance_vec)
203
+ min_distance = a.radius + b.radius
204
+
205
+ if distance >= min_distance:
206
+ return None
207
+
208
+ collision = CollisionInfo()
209
+ collision.collided = True
210
+ collision.depth = min_distance - distance
211
+ collision.normal = distance_vec / distance if distance > 0 else np.array([0, 1, 0], dtype=np.float32)
212
+ collision.point = center_a + collision.normal * a.radius
213
+
214
+ return collision
215
+
216
+ def _box_vs_sphere(self, box: BoxCollider, sphere: SphereCollider) -> Optional[CollisionInfo]:
217
+ """Box vs Sphere коллизия"""
218
+ pos_box = self._get_collider_position(box)
219
+ pos_sphere = self._get_collider_position(sphere)
220
+
221
+ if pos_box is None or pos_sphere is None:
222
+ return None
223
+
224
+ # Находим ближайшую точку на AABB к центру сферы
225
+ box_min = pos_box + box.center - box.size / 2
226
+ box_max = pos_box + box.center + box.size / 2
227
+ sphere_center = pos_sphere + sphere.center
228
+
229
+ closest_point = np.clip(sphere_center, box_min, box_max)
230
+ distance_vec = sphere_center - closest_point
231
+ distance = np.linalg.norm(distance_vec)
232
+
233
+ if distance >= sphere.radius:
234
+ return None
235
+
236
+ collision = CollisionInfo()
237
+ collision.collided = True
238
+ collision.depth = sphere.radius - distance
239
+
240
+ if distance > 0:
241
+ collision.normal = distance_vec / distance
242
+ else:
243
+ # Сфера внутри бокса
244
+ collision.normal = np.array([0, 1, 0], dtype=np.float32)
245
+ collision.depth = sphere.radius + min(
246
+ closest_point[0] - box_min[0],
247
+ box_max[0] - closest_point[0],
248
+ closest_point[1] - box_min[1],
249
+ box_max[1] - closest_point[1],
250
+ closest_point[2] - box_min[2],
251
+ box_max[2] - closest_point[2]
252
+ )
253
+
254
+ collision.point = closest_point
255
+
256
+ return collision
257
+
258
+ def _resolve_collisions(self, collisions: List[CollisionInfo], dt: float):
259
+ """Разрешает коллизии"""
260
+ for collision in collisions:
261
+ if not collision.entity_a or not collision.entity_b:
262
+ continue
263
+
264
+ body_a = self._get_body_from_entity(collision.entity_a)
265
+ body_b = self._get_body_from_entity(collision.entity_b)
266
+
267
+ if not body_a or not body_b:
268
+ continue
269
+
270
+ # Для триггеров только уведомляем
271
+ collider_a = collision.entity_a.get_component("Collider") or \
272
+ collision.entity_a.get_component("BoxCollider") or \
273
+ collision.entity_a.get_component("SphereCollider")
274
+ collider_b = collision.entity_b.get_component("Collider") or \
275
+ collision.entity_b.get_component("BoxCollider") or \
276
+ collision.entity_b.get_component("SphereCollider")
277
+
278
+ if (collider_a and collider_a.is_trigger) or \
279
+ (collider_b and collider_b.is_trigger):
280
+ # Вызываем событие триггера
281
+ for callback in self.on_trigger_enter:
282
+ callback(collision)
283
+ continue
284
+
285
+ # Вычисляем общую инверсную массу
286
+ inv_mass_a = 0 if body_a.is_kinematic else 1.0 / body_a.mass
287
+ inv_mass_b = 0 if body_b.is_kinematic else 1.0 / body_b.mass
288
+ total_inv_mass = inv_mass_a + inv_mass_b
289
+
290
+ if total_inv_mass <= 0:
291
+ continue
292
+
293
+ # Разделение
294
+ separation = collision.normal * (collision.depth / total_inv_mass)
295
+
296
+ transform_a = collision.entity_a.get_component("Transform")
297
+ transform_b = collision.entity_b.get_component("Transform")
298
+
299
+ if transform_a and not body_a.is_kinematic:
300
+ transform_a.position -= separation * inv_mass_a
301
+ transform_a._matrix_dirty = True
302
+
303
+ if transform_b and not body_b.is_kinematic:
304
+ transform_b.position += separation * inv_mass_b
305
+ transform_b._matrix_dirty = True
306
+
307
+ # Обновляем скорости (простое отражение с коэффициентом)
308
+ restitution = 0.3 # Коэффициент упругости
309
+
310
+ relative_velocity = body_b.velocity - body_a.velocity
311
+ velocity_along_normal = np.dot(relative_velocity, collision.normal)
312
+
313
+ if velocity_along_normal > 0:
314
+ continue
315
+
316
+ impulse = collision.normal * (-(1 + restitution) * velocity_along_normal / total_inv_mass)
317
+
318
+ if not body_a.is_kinematic:
319
+ body_a.velocity -= impulse * inv_mass_a
320
+ if not body_b.is_kinematic:
321
+ body_b.velocity += impulse * inv_mass_b
322
+
323
+ # Трение
324
+ tangent = relative_velocity - collision.normal * velocity_along_normal
325
+ tangent_norm = np.linalg.norm(tangent)
326
+
327
+ if tangent_norm > 0.001:
328
+ tangent /= tangent_norm
329
+ friction_coefficient = 0.5
330
+ friction_impulse = tangent * (-friction_coefficient * np.linalg.norm(impulse))
331
+
332
+ if not body_a.is_kinematic:
333
+ body_a.velocity -= friction_impulse * inv_mass_a
334
+ if not body_b.is_kinematic:
335
+ body_b.velocity += friction_impulse * inv_mass_b
336
+
337
+ # Вызываем событие коллизии
338
+ for callback in self.on_collision_enter:
339
+ callback(collision)
340
+
341
+ def _get_collider_position(self, collider: Collider):
342
+ """Получает мировую позицию коллайдера из трансформа"""
343
+ if hasattr(collider, 'entity') and collider.entity:
344
+ transform = collider.entity.get_component("Transform")
345
+ if transform:
346
+ return transform.position.copy()
347
+ return None
348
+
349
+ def _get_body_from_entity(self, entity):
350
+ """Находит RigidbodyComponent у entity"""
351
+ return entity.get_component("Rigidbody")
352
+
353
+ def _check_sleeping(self, dt: float):
354
+ """Проверяет, можно ли усыпить тело"""
355
+ for body in self.bodies:
356
+ if body.is_kinematic:
357
+ continue
358
+
359
+ speed = np.linalg.norm(body.velocity)
360
+
361
+ if speed < self.sleep_threshold:
362
+ body._sleep_timer += dt
363
+ if body._sleep_timer >= self.sleep_time_threshold:
364
+ body.is_sleeping = True
365
+ else:
366
+ body._sleep_timer = 0
367
+ body.is_sleeping = False
368
+
369
+ def raycast(self, origin: np.ndarray, direction: np.ndarray, max_distance=100.0):
370
+ """Пуск луча и поиск пересечений"""
371
+ ray_dir = direction / np.linalg.norm(direction)
372
+ closest_hit = None
373
+ closest_distance = max_distance
374
+
375
+ for body, collider in self.colliders:
376
+ pos = self._get_collider_position(collider)
377
+ if pos is None:
378
+ continue
379
+
380
+ hit_distance = None
381
+
382
+ if isinstance(collider, BoxCollider):
383
+ hit_distance = self._ray_vs_aabb(origin, ray_dir, pos, collider)
384
+ elif isinstance(collider, SphereCollider):
385
+ hit_distance = self._ray_vs_sphere(origin, ray_dir, pos, collider)
386
+
387
+ if hit_distance and hit_distance < closest_distance:
388
+ closest_distance = hit_distance
389
+ from aether.physics.raycast import RaycastHit
390
+ closest_hit = RaycastHit()
391
+ closest_hit.hit = True
392
+ closest_hit.distance = hit_distance
393
+ closest_hit.point = origin + ray_dir * hit_distance
394
+ if hasattr(body, 'entity'):
395
+ closest_hit.entity = body.entity
396
+
397
+ return closest_hit
398
+
399
+ def _ray_vs_aabb(self, origin, direction, box_pos, collider: BoxCollider):
400
+ """Луч vs AABB"""
401
+ box_min = box_pos + collider.center - collider.size / 2
402
+ box_max = box_pos + collider.center + collider.size / 2
403
+
404
+ t_min = (box_min - origin) / (direction + 1e-10)
405
+ t_max = (box_max - origin) / (direction + 1e-10)
406
+
407
+ t1 = np.minimum(t_min, t_max)
408
+ t2 = np.maximum(t_min, t_max)
409
+
410
+ t_near = np.max(t1)
411
+ t_far = np.min(t2)
412
+
413
+ if t_near <= t_far and t_far >= 0:
414
+ return t_near if t_near >= 0 else t_far
415
+ return None
416
+
417
+ def _ray_vs_sphere(self, origin, direction, sphere_pos, collider: SphereCollider):
418
+ """Луч vs Sphere"""
419
+ center = sphere_pos + collider.center
420
+ oc = origin - center
421
+
422
+ a = np.dot(direction, direction)
423
+ b = 2.0 * np.dot(oc, direction)
424
+ c = np.dot(oc, oc) - collider.radius ** 2
425
+
426
+ discriminant = b * b - 4 * a * c
427
+
428
+ if discriminant < 0:
429
+ return None
430
+
431
+ t = (-b - np.sqrt(discriminant)) / (2 * a)
432
+
433
+ if t >= 0:
434
+ return t
435
+
436
+ return None
@@ -0,0 +1,25 @@
1
+ """Raycasting"""
2
+ import numpy as np
3
+
4
+
5
+ class Ray:
6
+ def __init__(self, origin, direction):
7
+ self.origin = np.array(origin, dtype=np.float32)
8
+ self.direction = np.array(direction, dtype=np.float32)
9
+ self.direction = self.direction / np.linalg.norm(self.direction)
10
+
11
+
12
+ class RaycastHit:
13
+ def __init__(self):
14
+ self.hit = False
15
+ self.point = None
16
+ self.normal = None
17
+ self.distance = 0.0
18
+ self.entity = None
19
+
20
+
21
+ def raycast(ray: Ray, colliders, max_distance=100.0) -> RaycastHit:
22
+ """Простой raycast против AABB"""
23
+ hit = RaycastHit()
24
+ # TODO: реализовать
25
+ return hit
@@ -0,0 +1,7 @@
1
+ """Физическое твёрдое тело"""
2
+ class Rigidbody:
3
+ def __init__(self, mass=1.0, use_gravity=True):
4
+ self.mass = mass
5
+ self.use_gravity = use_gravity
6
+ self.velocity = [0.0, 0.0, 0.0]
7
+ self.angular_velocity = [0.0, 0.0, 0.0]
File without changes
@@ -0,0 +1,76 @@
1
+ """База данных ассетов с GUID"""
2
+ import uuid
3
+ import json
4
+ from typing import Dict, Any, Optional
5
+ from pathlib import Path
6
+
7
+
8
+ class AssetInfo:
9
+ """Информация об ассете"""
10
+ def __init__(self, path: str, asset_type: str):
11
+ self.guid = str(uuid.uuid4())
12
+ self.path = Path(path)
13
+ self.asset_type = asset_type
14
+ self.dependencies = []
15
+ self.metadata = {}
16
+
17
+ def to_dict(self) -> dict:
18
+ return {
19
+ 'guid': self.guid,
20
+ 'path': str(self.path),
21
+ 'type': self.asset_type,
22
+ 'dependencies': self.dependencies,
23
+ 'metadata': self.metadata
24
+ }
25
+
26
+ @classmethod
27
+ def from_dict(cls, data: dict) -> 'AssetInfo':
28
+ asset = cls(data['path'], data['type'])
29
+ asset.guid = data['guid']
30
+ asset.dependencies = data.get('dependencies', [])
31
+ asset.metadata = data.get('metadata', {})
32
+ return asset
33
+
34
+
35
+ class AssetDatabase:
36
+ """База данных всех ассетов проекта"""
37
+
38
+ def __init__(self):
39
+ self._assets: Dict[str, AssetInfo] = {}
40
+ self._guid_index: Dict[str, AssetInfo] = {}
41
+
42
+ def register_asset(self, path: str, asset_type: str) -> AssetInfo:
43
+ """Регистрирует ассет"""
44
+ asset = AssetInfo(path, asset_type)
45
+ self._assets[str(asset.path)] = asset
46
+ self._guid_index[asset.guid] = asset
47
+ return asset
48
+
49
+ def get_by_path(self, path: str) -> Optional[AssetInfo]:
50
+ return self._assets.get(path)
51
+
52
+ def get_by_guid(self, guid: str) -> Optional[AssetInfo]:
53
+ return self._guid_index.get(guid)
54
+
55
+ def save(self, path: str):
56
+ """Сохраняет базу данных"""
57
+ data = {
58
+ 'version': '1.0',
59
+ 'assets': [a.to_dict() for a in self._assets.values()]
60
+ }
61
+ with open(path, 'w') as f:
62
+ json.dump(data, f, indent=2)
63
+
64
+ @classmethod
65
+ def load(cls, path: str) -> 'AssetDatabase':
66
+ """Загружает базу данных"""
67
+ with open(path, 'r') as f:
68
+ data = json.load(f)
69
+
70
+ db = cls()
71
+ for asset_data in data.get('assets', []):
72
+ asset = AssetInfo.from_dict(asset_data)
73
+ db._assets[str(asset.path)] = asset
74
+ db._guid_index[asset.guid] = asset
75
+
76
+ return db
@@ -0,0 +1,147 @@
1
+ """Менеджер ресурсов с асинхронной загрузкой и кэшированием"""
2
+ import threading
3
+ from typing import Dict, Any, Callable, Optional
4
+ from pathlib import Path
5
+ from queue import Queue
6
+ import time
7
+
8
+
9
+ class Resource:
10
+ """Базовый класс ресурса"""
11
+
12
+ def __init__(self, path: str):
13
+ self.path = Path(path)
14
+ self.loaded = False
15
+ self.reference_count = 0
16
+
17
+ def load(self):
18
+ """Загружает ресурс"""
19
+ self.loaded = True
20
+
21
+ def unload(self):
22
+ """Выгружает ресурс"""
23
+ self.loaded = False
24
+
25
+ def add_reference(self):
26
+ self.reference_count += 1
27
+
28
+ def remove_reference(self):
29
+ self.reference_count = max(0, self.reference_count - 1)
30
+
31
+
32
+ class ResourceManager:
33
+ """Менеджер ресурсов с кэшированием"""
34
+
35
+ _instance = None
36
+
37
+ def __new__(cls):
38
+ if cls._instance is None:
39
+ cls._instance = super().__new__(cls)
40
+ cls._instance._initialize()
41
+ return cls._instance
42
+
43
+ def _initialize(self):
44
+ """Инициализация менеджера"""
45
+ self._cache: Dict[str, Resource] = {}
46
+ self._loaders: Dict[str, Callable] = {}
47
+ self._load_queue = Queue()
48
+ self._async_loading = False
49
+ self._loader_thread = None
50
+ print("[ResourceManager] Initialized")
51
+
52
+ def register_loader(self, extension: str, loader: Callable):
53
+ """Регистрирует загрузчик для расширения"""
54
+ self._loaders[extension.lower()] = loader
55
+ print(f"[ResourceManager] Registered loader for .{extension}")
56
+
57
+ def load(self, path: str) -> Optional[Resource]:
58
+ """Загружает ресурс (синхронно)"""
59
+ key = str(Path(path).resolve())
60
+
61
+ # Проверяем кэш
62
+ if key in self._cache:
63
+ resource = self._cache[key]
64
+ resource.add_reference()
65
+ return resource
66
+
67
+ # Определяем расширение
68
+ ext = Path(path).suffix.lower()
69
+ if ext not in self._loaders:
70
+ print(f"[ResourceManager] No loader for {ext}")
71
+ return None
72
+
73
+ # Загружаем
74
+ try:
75
+ resource = self._loaders[ext](path)
76
+ if resource:
77
+ resource.load()
78
+ resource.add_reference()
79
+ self._cache[key] = resource
80
+ print(f"[ResourceManager] Loaded: {path}")
81
+ return resource
82
+ except Exception as e:
83
+ print(f"[ResourceManager] Failed to load {path}: {e}")
84
+
85
+ return None
86
+
87
+ def load_async(self, path: str, callback: Callable = None):
88
+ """Загружает ресурс асинхронно"""
89
+ self._load_queue.put((path, callback))
90
+
91
+ if not self._async_loading:
92
+ self._start_async_loader()
93
+
94
+ def _start_async_loader(self):
95
+ """Запускает поток асинхронной загрузки"""
96
+ self._async_loading = True
97
+ self._loader_thread = threading.Thread(target=self._async_loader_loop, daemon=True)
98
+ self._loader_thread.start()
99
+
100
+ def _async_loader_loop(self):
101
+ """Цикл асинхронной загрузки"""
102
+ while self._async_loading:
103
+ try:
104
+ if not self._load_queue.empty():
105
+ path, callback = self._load_queue.get()
106
+ resource = self.load(path)
107
+ if callback:
108
+ callback(resource)
109
+ else:
110
+ time.sleep(0.01)
111
+ except Exception as e:
112
+ print(f"[ResourceManager] Async load error: {e}")
113
+
114
+ def unload(self, path: str):
115
+ """Выгружает ресурс"""
116
+ key = str(Path(path).resolve())
117
+ if key in self._cache:
118
+ resource = self._cache[key]
119
+ resource.remove_reference()
120
+ if resource.reference_count <= 0:
121
+ resource.unload()
122
+ del self._cache[key]
123
+ print(f"[ResourceManager] Unloaded: {path}")
124
+
125
+ def get(self, path: str) -> Optional[Resource]:
126
+ """Получает ресурс из кэша"""
127
+ key = str(Path(path).resolve())
128
+ return self._cache.get(key)
129
+
130
+ def clear_cache(self):
131
+ """Очищает весь кэш"""
132
+ for resource in self._cache.values():
133
+ resource.unload()
134
+ self._cache.clear()
135
+ print("[ResourceManager] Cache cleared")
136
+
137
+ def get_stats(self) -> dict:
138
+ """Возвращает статистику"""
139
+ total_memory = sum(
140
+ getattr(r, 'memory_usage', 0)
141
+ for r in self._cache.values()
142
+ )
143
+ return {
144
+ 'cached_resources': len(self._cache),
145
+ 'total_memory_mb': total_memory / (1024 * 1024),
146
+ 'queue_size': self._load_queue.qsize()
147
+ }